diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index f6708fa83..a184dd83d 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -72,8 +72,8 @@ jobs:
- api-level: 21
target: default
arch: x86
- - api-level: 33
- target: google_apis # emulator API 33 only exists with Google APIs
+ - api-level: 35
+ target: default
arch: x86_64
permissions:
diff --git a/README.md b/README.md
index c19144064..aa4332165 100644
--- a/README.md
+++ b/README.md
@@ -9,12 +9,8 @@
-
-
-
-
-
-
+
+
diff --git a/app/build.gradle b/app/build.gradle
index c5525552f..c2a4f9265 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -17,20 +17,20 @@ plugins {
}
android {
- compileSdk 35
+ compileSdk 36
namespace 'org.schabi.newpipe'
defaultConfig {
applicationId "org.schabi.newpipe"
resValue "string", "app_name", "NewPipe"
minSdk 21
- targetSdk 33
+ targetSdk 35
if (System.properties.containsKey('versionCodeOverride')) {
versionCode System.getProperty('versionCodeOverride') as Integer
} else {
- versionCode 1004
+ versionCode 1005
}
- versionName "0.27.7"
+ versionName "0.28.0"
if (System.properties.containsKey('versionNameSuffix')) {
versionNameSuffix System.getProperty('versionNameSuffix')
}
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 3faa59f7d..0ac368898 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -10,6 +10,7 @@
+
@@ -58,6 +59,15 @@
+
+
+
+
-
+
+
-
+
- implements BackPressable,
- PlayerServiceExtendedEventListener,
- OnKeyDownListener {
- public static final String KEY_SWITCHING_PLAYERS = "switching_players";
-
- private static final float MAX_OVERLAY_ALPHA = 0.9f;
- private static final float MAX_PLAYER_HEIGHT = 0.7f;
-
- public static final String ACTION_SHOW_MAIN_PLAYER =
- App.PACKAGE_NAME + ".VideoDetailFragment.ACTION_SHOW_MAIN_PLAYER";
- public static final String ACTION_HIDE_MAIN_PLAYER =
- App.PACKAGE_NAME + ".VideoDetailFragment.ACTION_HIDE_MAIN_PLAYER";
- public static final String ACTION_PLAYER_STARTED =
- App.PACKAGE_NAME + ".VideoDetailFragment.ACTION_PLAYER_STARTED";
- public static final String ACTION_VIDEO_FRAGMENT_RESUMED =
- App.PACKAGE_NAME + ".VideoDetailFragment.ACTION_VIDEO_FRAGMENT_RESUMED";
- public static final String ACTION_VIDEO_FRAGMENT_STOPPED =
- App.PACKAGE_NAME + ".VideoDetailFragment.ACTION_VIDEO_FRAGMENT_STOPPED";
-
- private static final String COMMENTS_TAB_TAG = "COMMENTS";
- private static final String RELATED_TAB_TAG = "NEXT VIDEO";
- private static final String DESCRIPTION_TAB_TAG = "DESCRIPTION TAB";
- private static final String EMPTY_TAB_TAG = "EMPTY TAB";
-
- // tabs
- private boolean showComments;
- private boolean showRelatedItems;
- private boolean showDescription;
- private String selectedTabTag;
- @AttrRes
- @NonNull
- final List tabIcons = new ArrayList<>();
- @StringRes
- @NonNull
- final List tabContentDescriptions = new ArrayList<>();
- private boolean tabSettingsChanged = false;
- private int lastAppBarVerticalOffset = Integer.MAX_VALUE; // prevents useless updates
-
- private final SharedPreferences.OnSharedPreferenceChangeListener preferenceChangeListener =
- (sharedPreferences, key) -> {
- if (getString(R.string.show_comments_key).equals(key)) {
- showComments = sharedPreferences.getBoolean(key, true);
- tabSettingsChanged = true;
- } else if (getString(R.string.show_next_video_key).equals(key)) {
- showRelatedItems = sharedPreferences.getBoolean(key, true);
- tabSettingsChanged = true;
- } else if (getString(R.string.show_description_key).equals(key)) {
- showDescription = sharedPreferences.getBoolean(key, true);
- tabSettingsChanged = true;
- }
- };
-
- @State
- int serviceId = Constants.NO_SERVICE_ID;
- @State
- @NonNull
- String title = "";
- @State
- @Nullable
- String url = null;
- @Nullable
- private PlayQueue playQueue = null;
- @State
- int bottomSheetState = BottomSheetBehavior.STATE_EXPANDED;
- @State
- int lastStableBottomSheetState = BottomSheetBehavior.STATE_EXPANDED;
- @State
- boolean autoPlayEnabled = true;
-
- @Nullable
- private StreamInfo currentInfo = null;
- private Disposable currentWorker;
- @NonNull
- private final CompositeDisposable disposables = new CompositeDisposable();
- @Nullable
- private Disposable positionSubscriber = null;
-
- private BottomSheetBehavior bottomSheetBehavior;
- private BottomSheetBehavior.BottomSheetCallback bottomSheetCallback;
- private BroadcastReceiver broadcastReceiver;
-
- /*//////////////////////////////////////////////////////////////////////////
- // Views
- //////////////////////////////////////////////////////////////////////////*/
-
- private FragmentVideoDetailBinding binding;
-
- private TabAdapter pageAdapter;
-
- private ContentObserver settingsContentObserver;
- @Nullable
- private PlayerService playerService;
- private Player player;
- private final PlayerHolder playerHolder = PlayerHolder.getInstance();
-
- /*//////////////////////////////////////////////////////////////////////////
- // Service management
- //////////////////////////////////////////////////////////////////////////*/
- @Override
- public void onServiceConnected(@NonNull final PlayerService connectedPlayerService) {
- playerService = connectedPlayerService;
- }
-
- @Override
- public void onPlayerConnected(@NonNull final Player connectedPlayer,
- final boolean playAfterConnect) {
- player = connectedPlayer;
-
- // It will do nothing if the player is not in fullscreen mode
- hideSystemUiIfNeeded();
-
- final Optional playerUi = player.UIs().getOpt(MainPlayerUi.class);
- if (!player.videoPlayerSelected() && !playAfterConnect) {
- return;
- }
-
- if (DeviceUtils.isLandscape(requireContext())) {
- // If the video is playing but orientation changed
- // let's make the video in fullscreen again
- checkLandscape();
- } else if (playerUi.map(ui -> ui.isFullscreen() && !ui.isVerticalVideo()).orElse(false)
- // Tablet UI has orientation-independent fullscreen
- && !DeviceUtils.isTablet(activity)) {
- // Device is in portrait orientation after rotation but UI is in fullscreen.
- // Return back to non-fullscreen state
- playerUi.ifPresent(MainPlayerUi::toggleFullscreen);
- }
-
- if (playAfterConnect
- || (currentInfo != null
- && isAutoplayEnabled()
- && playerUi.isEmpty())) {
- autoPlayEnabled = true; // forcefully start playing
- openVideoPlayerAutoFullscreen();
- }
- updateOverlayPlayQueueButtonVisibility();
- }
-
- @Override
- public void onPlayerDisconnected() {
- player = null;
- // the binding could be null at this point, if the app is finishing
- if (binding != null) {
- restoreDefaultBrightness();
- }
- }
-
- @Override
- public void onServiceDisconnected() {
- playerService = null;
- }
-
-
- /*////////////////////////////////////////////////////////////////////////*/
-
- public static VideoDetailFragment getInstance(final int serviceId,
- @Nullable final String url,
- @NonNull final String name,
- @Nullable final PlayQueue queue) {
- final VideoDetailFragment instance = new VideoDetailFragment();
- instance.setInitialData(serviceId, url, name, queue);
- return instance;
- }
-
- public static VideoDetailFragment getInstanceInCollapsedState() {
- final VideoDetailFragment instance = new VideoDetailFragment();
- instance.updateBottomSheetState(BottomSheetBehavior.STATE_COLLAPSED);
- return instance;
- }
-
-
- /*//////////////////////////////////////////////////////////////////////////
- // Fragment's Lifecycle
- //////////////////////////////////////////////////////////////////////////*/
-
- @Override
- public void onCreate(final Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
-
- final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(activity);
- showComments = prefs.getBoolean(getString(R.string.show_comments_key), true);
- showRelatedItems = prefs.getBoolean(getString(R.string.show_next_video_key), true);
- showDescription = prefs.getBoolean(getString(R.string.show_description_key), true);
- selectedTabTag = prefs.getString(
- getString(R.string.stream_info_selected_tab_key), COMMENTS_TAB_TAG);
- prefs.registerOnSharedPreferenceChangeListener(preferenceChangeListener);
-
- setupBroadcastReceiver();
-
- settingsContentObserver = new ContentObserver(new Handler()) {
- @Override
- public void onChange(final boolean selfChange) {
- if (activity != null && !globalScreenOrientationLocked(activity)) {
- activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED);
- }
- }
- };
- activity.getContentResolver().registerContentObserver(
- Settings.System.getUriFor(Settings.System.ACCELEROMETER_ROTATION), false,
- settingsContentObserver);
- }
-
- @Override
- public View onCreateView(@NonNull final LayoutInflater inflater, final ViewGroup container,
- final Bundle savedInstanceState) {
- binding = FragmentVideoDetailBinding.inflate(inflater, container, false);
- return binding.getRoot();
- }
-
- @Override
- public void onPause() {
- super.onPause();
- if (currentWorker != null) {
- currentWorker.dispose();
- }
- restoreDefaultBrightness();
- PreferenceManager.getDefaultSharedPreferences(requireContext())
- .edit()
- .putString(getString(R.string.stream_info_selected_tab_key),
- pageAdapter.getItemTitle(binding.viewPager.getCurrentItem()))
- .apply();
- }
-
- @Override
- public void onResume() {
- super.onResume();
- if (DEBUG) {
- Log.d(TAG, "onResume() called");
- }
-
- activity.sendBroadcast(new Intent(ACTION_VIDEO_FRAGMENT_RESUMED));
-
- updateOverlayPlayQueueButtonVisibility();
-
- setupBrightness();
-
- if (tabSettingsChanged) {
- tabSettingsChanged = false;
- initTabs();
- if (currentInfo != null) {
- updateTabs(currentInfo);
- }
- }
-
- // Check if it was loading when the fragment was stopped/paused
- if (wasLoading.getAndSet(false) && !wasCleared()) {
- startLoading(false);
- }
- }
-
- @Override
- public void onStop() {
- super.onStop();
-
- if (!activity.isChangingConfigurations()) {
- activity.sendBroadcast(new Intent(ACTION_VIDEO_FRAGMENT_STOPPED));
- }
- }
-
- @Override
- public void onDestroy() {
- super.onDestroy();
-
- // Stop the service when user leaves the app with double back press
- // if video player is selected. Otherwise unbind
- if (activity.isFinishing() && isPlayerAvailable() && player.videoPlayerSelected()) {
- playerHolder.stopService();
- } else {
- playerHolder.setListener(null);
- }
-
- PreferenceManager.getDefaultSharedPreferences(activity)
- .unregisterOnSharedPreferenceChangeListener(preferenceChangeListener);
- activity.unregisterReceiver(broadcastReceiver);
- activity.getContentResolver().unregisterContentObserver(settingsContentObserver);
-
- if (positionSubscriber != null) {
- positionSubscriber.dispose();
- }
- if (currentWorker != null) {
- currentWorker.dispose();
- }
- disposables.clear();
- positionSubscriber = null;
- currentWorker = null;
- bottomSheetBehavior.removeBottomSheetCallback(bottomSheetCallback);
-
- if (activity.isFinishing()) {
- playQueue = null;
- currentInfo = null;
- stack = new LinkedList<>();
- }
- }
-
- @Override
- public void onDestroyView() {
- super.onDestroyView();
- binding = null;
- }
-
- @Override
- public void onActivityResult(final int requestCode, final int resultCode, final Intent data) {
- super.onActivityResult(requestCode, resultCode, data);
- if (requestCode == ReCaptchaActivity.RECAPTCHA_REQUEST) {
- if (resultCode == Activity.RESULT_OK) {
- NavigationHelper.openVideoDetailFragment(requireContext(), getFM(),
- serviceId, url, title, null, false);
- } else {
- Log.e(TAG, "ReCaptcha failed");
- }
- } else {
- Log.e(TAG, "Request code from activity not supported [" + requestCode + "]");
- }
- }
-
- /*//////////////////////////////////////////////////////////////////////////
- // OnClick
- //////////////////////////////////////////////////////////////////////////*/
-
- private void setOnClickListeners() {
- binding.detailTitleRootLayout.setOnClickListener(v -> toggleTitleAndSecondaryControls());
- binding.detailUploaderRootLayout.setOnClickListener(makeOnClickListener(info -> {
- if (isEmpty(info.getSubChannelUrl())) {
- if (!isEmpty(info.getUploaderUrl())) {
- openChannel(info.getUploaderUrl(), info.getUploaderName());
- }
-
- if (DEBUG) {
- Log.i(TAG, "Can't open sub-channel because we got no channel URL");
- }
- } else {
- openChannel(info.getSubChannelUrl(), info.getSubChannelName());
- }
- }));
- binding.detailThumbnailRootLayout.setOnClickListener(v -> {
- autoPlayEnabled = true; // forcefully start playing
- // FIXME Workaround #7427
- if (isPlayerAvailable()) {
- player.setRecovery();
- }
- openVideoPlayerAutoFullscreen();
- });
-
- binding.detailControlsBackground.setOnClickListener(v -> openBackgroundPlayer(false));
- binding.detailControlsPopup.setOnClickListener(v -> openPopupPlayer(false));
- binding.detailControlsPlaylistAppend.setOnClickListener(makeOnClickListener(info -> {
- if (getFM() != null && currentInfo != null) {
- final Fragment fragment = getParentFragmentManager().
- findFragmentById(R.id.fragment_holder);
-
- // commit previous pending changes to database
- if (fragment instanceof LocalPlaylistFragment) {
- ((LocalPlaylistFragment) fragment).saveImmediate();
- } else if (fragment instanceof MainFragment) {
- ((MainFragment) fragment).commitPlaylistTabs();
- }
-
- disposables.add(PlaylistDialog.createCorrespondingDialog(requireContext(),
- List.of(new StreamEntity(info)),
- dialog -> dialog.show(getParentFragmentManager(), TAG)));
- }
- }));
- binding.detailControlsDownload.setOnClickListener(v -> {
- if (PermissionHelper.checkStoragePermissions(activity,
- PermissionHelper.DOWNLOAD_DIALOG_REQUEST_CODE)) {
- openDownloadDialog();
- }
- });
- binding.detailControlsShare.setOnClickListener(makeOnClickListener(info ->
- ShareUtils.shareText(requireContext(), info.getName(), info.getUrl(),
- info.getThumbnails())));
- binding.detailControlsOpenInBrowser.setOnClickListener(makeOnClickListener(info ->
- ShareUtils.openUrlInBrowser(requireContext(), info.getUrl())));
- binding.detailControlsPlayWithKodi.setOnClickListener(makeOnClickListener(info ->
- KoreUtils.playWithKore(requireContext(), Uri.parse(info.getUrl()))));
- if (DEBUG) {
- binding.detailControlsCrashThePlayer.setOnClickListener(v ->
- VideoDetailPlayerCrasher.onCrashThePlayer(requireContext(), player));
- }
-
- final View.OnClickListener overlayListener = v -> bottomSheetBehavior
- .setState(BottomSheetBehavior.STATE_EXPANDED);
- binding.overlayThumbnail.setOnClickListener(overlayListener);
- binding.overlayMetadataLayout.setOnClickListener(overlayListener);
- binding.overlayButtonsLayout.setOnClickListener(overlayListener);
- binding.overlayCloseButton.setOnClickListener(v -> bottomSheetBehavior
- .setState(BottomSheetBehavior.STATE_HIDDEN));
- binding.overlayPlayQueueButton.setOnClickListener(v -> openPlayQueue(requireContext()));
- binding.overlayPlayPauseButton.setOnClickListener(v -> {
- if (playerIsNotStopped()) {
- player.playPause();
- player.UIs().getOpt(VideoPlayerUi.class).ifPresent(ui -> ui.hideControls(0, 0));
- showSystemUi();
- } else {
- autoPlayEnabled = true; // forcefully start playing
- openVideoPlayer(false);
- }
-
- setOverlayPlayPauseImage(isPlayerAvailable() && player.isPlaying());
- });
- }
-
- private View.OnClickListener makeOnClickListener(final Consumer consumer) {
- return v -> {
- if (!isLoading.get() && currentInfo != null) {
- consumer.accept(currentInfo);
- }
- };
- }
-
- private void setOnLongClickListeners() {
- binding.detailTitleRootLayout.setOnLongClickListener(makeOnLongClickListener(info ->
- ShareUtils.copyToClipboard(requireContext(),
- binding.detailVideoTitleView.getText().toString())));
- binding.detailUploaderRootLayout.setOnLongClickListener(makeOnLongClickListener(info -> {
- if (isEmpty(info.getSubChannelUrl())) {
- Log.w(TAG, "Can't open parent channel because we got no parent channel URL");
- } else {
- openChannel(info.getUploaderUrl(), info.getUploaderName());
- }
- }));
-
- binding.detailControlsBackground.setOnLongClickListener(makeOnLongClickListener(info ->
- openBackgroundPlayer(true)
- ));
- binding.detailControlsPopup.setOnLongClickListener(makeOnLongClickListener(info ->
- openPopupPlayer(true)
- ));
- binding.detailControlsDownload.setOnLongClickListener(makeOnLongClickListener(info ->
- NavigationHelper.openDownloads(activity)));
-
- final View.OnLongClickListener overlayListener = makeOnLongClickListener(info ->
- openChannel(info.getUploaderUrl(), info.getUploaderName()));
- binding.overlayThumbnail.setOnLongClickListener(overlayListener);
- binding.overlayMetadataLayout.setOnLongClickListener(overlayListener);
- }
-
- private View.OnLongClickListener makeOnLongClickListener(final Consumer consumer) {
- return v -> {
- if (isLoading.get() || currentInfo == null) {
- return false;
- }
- consumer.accept(currentInfo);
- return true;
- };
- }
-
- private void openChannel(final String subChannelUrl, final String subChannelName) {
- try {
- NavigationHelper.openChannelFragment(getFM(), currentInfo.getServiceId(),
- subChannelUrl, subChannelName);
- } catch (final Exception e) {
- ErrorUtil.showUiErrorSnackbar(this, "Opening channel fragment", e);
- }
- }
-
- private void toggleTitleAndSecondaryControls() {
- if (binding.detailSecondaryControlPanel.getVisibility() == View.GONE) {
- binding.detailVideoTitleView.setMaxLines(10);
- animateRotation(binding.detailToggleSecondaryControlsView,
- VideoPlayerUi.DEFAULT_CONTROLS_DURATION, 180);
- binding.detailSecondaryControlPanel.setVisibility(View.VISIBLE);
- } else {
- binding.detailVideoTitleView.setMaxLines(1);
- animateRotation(binding.detailToggleSecondaryControlsView,
- VideoPlayerUi.DEFAULT_CONTROLS_DURATION, 0);
- binding.detailSecondaryControlPanel.setVisibility(View.GONE);
- }
- // view pager height has changed, update the tab layout
- updateTabLayoutVisibility();
- }
-
- /*//////////////////////////////////////////////////////////////////////////
- // Init
- //////////////////////////////////////////////////////////////////////////*/
-
- @Override // called from onViewCreated in {@link BaseFragment#onViewCreated}
- protected void initViews(final View rootView, final Bundle savedInstanceState) {
- super.initViews(rootView, savedInstanceState);
-
- pageAdapter = new TabAdapter(getChildFragmentManager());
- binding.viewPager.setAdapter(pageAdapter);
- binding.tabLayout.setupWithViewPager(binding.viewPager);
-
- binding.detailThumbnailRootLayout.requestFocus();
-
- binding.detailControlsPlayWithKodi.setVisibility(
- KoreUtils.shouldShowPlayWithKodi(requireContext(), serviceId)
- ? View.VISIBLE
- : View.GONE
- );
- binding.detailControlsCrashThePlayer.setVisibility(
- DEBUG && PreferenceManager.getDefaultSharedPreferences(getContext())
- .getBoolean(getString(R.string.show_crash_the_player_key), false)
- ? View.VISIBLE
- : View.GONE
- );
- accommodateForTvAndDesktopMode();
- }
-
- @Override
- @SuppressLint("ClickableViewAccessibility")
- protected void initListeners() {
- super.initListeners();
-
- setOnClickListeners();
- setOnLongClickListeners();
-
- final View.OnTouchListener controlsTouchListener = (view, motionEvent) -> {
- if (motionEvent.getAction() == MotionEvent.ACTION_DOWN
- && PlayButtonHelper.shouldShowHoldToAppendTip(activity)) {
-
- animate(binding.touchAppendDetail, true, 250, AnimationType.ALPHA, 0, () ->
- animate(binding.touchAppendDetail, false, 1500, AnimationType.ALPHA, 1000));
- }
- return false;
- };
- binding.detailControlsBackground.setOnTouchListener(controlsTouchListener);
- binding.detailControlsPopup.setOnTouchListener(controlsTouchListener);
-
- binding.appBarLayout.addOnOffsetChangedListener((layout, verticalOffset) -> {
- // prevent useless updates to tab layout visibility if nothing changed
- if (verticalOffset != lastAppBarVerticalOffset) {
- lastAppBarVerticalOffset = verticalOffset;
- // the view was scrolled
- updateTabLayoutVisibility();
- }
- });
-
- setupBottomPlayer();
- if (!playerHolder.isBound()) {
- setHeightThumbnail();
- } else {
- playerHolder.startService(false, this);
- }
- }
-
- /*//////////////////////////////////////////////////////////////////////////
- // OwnStack
- //////////////////////////////////////////////////////////////////////////*/
-
- /**
- * Stack that contains the "navigation history".
- * The peek is the current video.
- */
- private static LinkedList stack = new LinkedList<>();
-
- @Override
- public boolean onKeyDown(final int keyCode) {
- return isPlayerAvailable()
- && player.UIs().getOpt(VideoPlayerUi.class)
- .map(playerUi -> playerUi.onKeyDown(keyCode)).orElse(false);
- }
-
- @Override
- public boolean onBackPressed() {
- if (DEBUG) {
- Log.d(TAG, "onBackPressed() called");
- }
-
- // If we are in fullscreen mode just exit from it via first back press
- if (isFullscreen()) {
- if (!DeviceUtils.isTablet(activity)) {
- player.pause();
- }
- restoreDefaultOrientation();
- setAutoPlay(false);
- return true;
- }
-
- // If we have something in history of played items we replay it here
- if (isPlayerAvailable()
- && player.getPlayQueue() != null
- && player.videoPlayerSelected()
- && player.getPlayQueue().previous()) {
- return true; // no code here, as previous() was used in the if
- }
-
- // That means that we are on the start of the stack,
- if (stack.size() <= 1) {
- restoreDefaultOrientation();
- return false; // let MainActivity handle the onBack (e.g. to minimize the mini player)
- }
-
- // Remove top
- stack.pop();
- // Get stack item from the new top
- setupFromHistoryItem(Objects.requireNonNull(stack.peek()));
-
- return true;
- }
-
- private void setupFromHistoryItem(final StackItem item) {
- setAutoPlay(false);
- hideMainPlayerOnLoadingNewStream();
-
- setInitialData(item.getServiceId(), item.getUrl(),
- item.getTitle() == null ? "" : item.getTitle(), item.getPlayQueue());
- startLoading(false);
-
- // Maybe an item was deleted in background activity
- if (item.getPlayQueue().getItem() == null) {
- return;
- }
-
- final PlayQueueItem playQueueItem = item.getPlayQueue().getItem();
- // Update title, url, uploader from the last item in the stack (it's current now)
- final boolean isPlayerStopped = !isPlayerAvailable() || player.isStopped();
- if (playQueueItem != null && isPlayerStopped) {
- updateOverlayData(playQueueItem.getTitle(),
- playQueueItem.getUploader(), playQueueItem.getThumbnails());
- }
- }
-
- /*//////////////////////////////////////////////////////////////////////////
- // Info loading and handling
- //////////////////////////////////////////////////////////////////////////*/
-
- @Override
- protected void doInitialLoadLogic() {
- if (wasCleared()) {
- return;
- }
-
- if (currentInfo == null) {
- prepareAndLoadInfo();
- } else {
- prepareAndHandleInfoIfNeededAfterDelay(currentInfo, false, 50);
- }
- }
-
- public void selectAndLoadVideo(final int newServiceId,
- @Nullable final String newUrl,
- @NonNull final String newTitle,
- @Nullable final PlayQueue newQueue) {
- if (isPlayerAvailable() && newQueue != null && playQueue != null
- && playQueue.getItem() != null && !playQueue.getItem().getUrl().equals(newUrl)) {
- // Preloading can be disabled since playback is surely being replaced.
- player.disablePreloadingOfCurrentTrack();
- }
-
- setInitialData(newServiceId, newUrl, newTitle, newQueue);
- startLoading(false, true);
- }
-
- private void prepareAndHandleInfoIfNeededAfterDelay(final StreamInfo info,
- final boolean scrollToTop,
- final long delay) {
- new Handler(Looper.getMainLooper()).postDelayed(() -> {
- if (activity == null) {
- return;
- }
- // Data can already be drawn, don't spend time twice
- if (info.getName().equals(binding.detailVideoTitleView.getText().toString())) {
- return;
- }
- prepareAndHandleInfo(info, scrollToTop);
- }, delay);
- }
-
- private void prepareAndHandleInfo(final StreamInfo info, final boolean scrollToTop) {
- if (DEBUG) {
- Log.d(TAG, "prepareAndHandleInfo() called with: "
- + "info = [" + info + "], scrollToTop = [" + scrollToTop + "]");
- }
-
- showLoading();
- initTabs();
-
- if (scrollToTop) {
- scrollToTop();
- }
- handleResult(info);
- showContent();
-
- }
-
- private void prepareAndLoadInfo() {
- scrollToTop();
- startLoading(false);
- }
-
- @Override
- public void startLoading(final boolean forceLoad) {
- startLoading(forceLoad, null);
- }
-
- private void startLoading(final boolean forceLoad, final @Nullable Boolean addToBackStack) {
- super.startLoading(forceLoad);
-
- initTabs();
- currentInfo = null;
- if (currentWorker != null) {
- currentWorker.dispose();
- }
-
- runWorker(forceLoad, addToBackStack != null ? addToBackStack : stack.isEmpty());
- }
-
- private void runWorker(final boolean forceLoad, final boolean addToBackStack) {
- final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(activity);
- currentWorker = ExtractorHelper.getStreamInfo(serviceId, url, forceLoad)
- .subscribeOn(Schedulers.io())
- .observeOn(AndroidSchedulers.mainThread())
- .subscribe(result -> {
- isLoading.set(false);
- hideMainPlayerOnLoadingNewStream();
- if (result.getAgeLimit() != NO_AGE_LIMIT && !prefs.getBoolean(
- getString(R.string.show_age_restricted_content), false)) {
- hideAgeRestrictedContent();
- } else {
- handleResult(result);
- showContent();
- if (addToBackStack) {
- if (playQueue == null) {
- playQueue = new SinglePlayQueue(result);
- }
- if (stack.isEmpty() || !stack.peek().getPlayQueue()
- .equalStreams(playQueue)) {
- stack.push(new StackItem(serviceId, url, title, playQueue));
- }
- }
-
- if (isAutoplayEnabled()) {
- openVideoPlayerAutoFullscreen();
- }
- }
- }, throwable -> showError(new ErrorInfo(throwable, UserAction.REQUESTED_STREAM,
- url == null ? "no url" : url, serviceId)));
- }
-
- /*//////////////////////////////////////////////////////////////////////////
- // Tabs
- //////////////////////////////////////////////////////////////////////////*/
-
- private void initTabs() {
- if (pageAdapter.getCount() != 0) {
- selectedTabTag = pageAdapter.getItemTitle(binding.viewPager.getCurrentItem());
- }
- pageAdapter.clearAllItems();
- tabIcons.clear();
- tabContentDescriptions.clear();
-
- if (shouldShowComments()) {
- pageAdapter.addFragment(CommentsFragment.getInstance(serviceId, url), COMMENTS_TAB_TAG);
- tabIcons.add(R.drawable.ic_comment);
- tabContentDescriptions.add(R.string.comments_tab_description);
- }
-
- if (showRelatedItems && binding.relatedItemsLayout == null) {
- // temp empty fragment. will be updated in handleResult
- pageAdapter.addFragment(EmptyFragment.newInstance(false), RELATED_TAB_TAG);
- tabIcons.add(R.drawable.ic_art_track);
- tabContentDescriptions.add(R.string.related_items_tab_description);
- }
-
- if (showDescription) {
- // temp empty fragment. will be updated in handleResult
- pageAdapter.addFragment(EmptyFragment.newInstance(false), DESCRIPTION_TAB_TAG);
- tabIcons.add(R.drawable.ic_description);
- tabContentDescriptions.add(R.string.description_tab_description);
- }
-
- if (pageAdapter.getCount() == 0) {
- pageAdapter.addFragment(EmptyFragment.newInstance(true), EMPTY_TAB_TAG);
- }
- pageAdapter.notifyDataSetUpdate();
-
- if (pageAdapter.getCount() >= 2) {
- final int position = pageAdapter.getItemPositionByTitle(selectedTabTag);
- if (position != -1) {
- binding.viewPager.setCurrentItem(position);
- }
- updateTabIconsAndContentDescriptions();
- }
- // the page adapter now contains tabs: show the tab layout
- updateTabLayoutVisibility();
- }
-
- /**
- * To be called whenever {@link #pageAdapter} is modified, since that triggers a refresh in
- * {@link FragmentVideoDetailBinding#tabLayout} resetting all tab's icons and content
- * descriptions. This reads icons from {@link #tabIcons} and content descriptions from
- * {@link #tabContentDescriptions}, which are all set in {@link #initTabs()}.
- */
- private void updateTabIconsAndContentDescriptions() {
- for (int i = 0; i < tabIcons.size(); ++i) {
- final TabLayout.Tab tab = binding.tabLayout.getTabAt(i);
- if (tab != null) {
- tab.setIcon(tabIcons.get(i));
- tab.setContentDescription(tabContentDescriptions.get(i));
- }
- }
- }
-
- private void updateTabs(@NonNull final StreamInfo info) {
- if (showRelatedItems) {
- if (binding.relatedItemsLayout == null) { // phone
- pageAdapter.updateItem(RELATED_TAB_TAG, RelatedItemsFragment.getInstance(info));
- } else { // tablet + TV
- getChildFragmentManager().beginTransaction()
- .replace(R.id.relatedItemsLayout, RelatedItemsFragment.getInstance(info))
- .commitAllowingStateLoss();
- binding.relatedItemsLayout.setVisibility(isFullscreen() ? View.GONE : View.VISIBLE);
- }
- }
-
- if (showDescription) {
- pageAdapter.updateItem(DESCRIPTION_TAB_TAG, new DescriptionFragment(info));
- }
-
- binding.viewPager.setVisibility(View.VISIBLE);
- // make sure the tab layout is visible
- updateTabLayoutVisibility();
- pageAdapter.notifyDataSetUpdate();
- updateTabIconsAndContentDescriptions();
- }
-
- private boolean shouldShowComments() {
- try {
- return showComments && NewPipe.getService(serviceId)
- .getServiceInfo()
- .getMediaCapabilities()
- .contains(COMMENTS);
- } catch (final ExtractionException e) {
- return false;
- }
- }
-
- public void updateTabLayoutVisibility() {
-
- if (binding == null) {
- //If binding is null we do not need to and should not do anything with its object(s)
- return;
- }
-
- if (pageAdapter.getCount() < 2 || binding.viewPager.getVisibility() != View.VISIBLE) {
- // hide tab layout if there is only one tab or if the view pager is also hidden
- binding.tabLayout.setVisibility(View.GONE);
- } else {
- // call `post()` to be sure `viewPager.getHitRect()`
- // is up to date and not being currently recomputed
- binding.tabLayout.post(() -> {
- final var activity = getActivity();
- if (activity != null) {
- final Rect pagerHitRect = new Rect();
- binding.viewPager.getHitRect(pagerHitRect);
-
- final int height = DeviceUtils.getWindowHeight(activity.getWindowManager());
- final int viewPagerVisibleHeight = height - pagerHitRect.top;
- // see TabLayout.DEFAULT_HEIGHT, which is equal to 48dp
- final float tabLayoutHeight = TypedValue.applyDimension(
- TypedValue.COMPLEX_UNIT_DIP, 48, getResources().getDisplayMetrics());
-
- if (viewPagerVisibleHeight > tabLayoutHeight * 2) {
- // no translation at all when viewPagerVisibleHeight > tabLayout.height * 3
- binding.tabLayout.setTranslationY(
- Math.max(0, tabLayoutHeight * 3 - viewPagerVisibleHeight));
- binding.tabLayout.setVisibility(View.VISIBLE);
- } else {
- // view pager is not visible enough
- binding.tabLayout.setVisibility(View.GONE);
- }
- }
- });
- }
- }
-
- public void scrollToTop() {
- binding.appBarLayout.setExpanded(true, true);
- // notify tab layout of scrolling
- updateTabLayoutVisibility();
- }
-
- /*//////////////////////////////////////////////////////////////////////////
- // Play Utils
- //////////////////////////////////////////////////////////////////////////*/
-
- private void toggleFullscreenIfInFullscreenMode() {
- // If a user watched video inside fullscreen mode and than chose another player
- // return to non-fullscreen mode
- if (isPlayerAvailable()) {
- player.UIs().getOpt(MainPlayerUi.class).ifPresent(playerUi -> {
- if (playerUi.isFullscreen()) {
- playerUi.toggleFullscreen();
- }
- });
- }
- }
-
- private void openBackgroundPlayer(final boolean append) {
- final boolean useExternalAudioPlayer = PreferenceManager
- .getDefaultSharedPreferences(activity)
- .getBoolean(activity.getString(R.string.use_external_audio_player_key), false);
-
- toggleFullscreenIfInFullscreenMode();
-
- if (isPlayerAvailable()) {
- // FIXME Workaround #7427
- player.setRecovery();
- }
-
- if (useExternalAudioPlayer) {
- showExternalAudioPlaybackDialog();
- } else {
- openNormalBackgroundPlayer(append);
- }
- }
-
- private void openPopupPlayer(final boolean append) {
- if (!PermissionHelper.isPopupEnabledElseAsk(activity)) {
- return;
- }
-
- // See UI changes while remote playQueue changes
- if (!isPlayerAvailable()) {
- playerHolder.startService(false, this);
- } else {
- // FIXME Workaround #7427
- player.setRecovery();
- }
-
- toggleFullscreenIfInFullscreenMode();
-
- final PlayQueue queue = setupPlayQueueForIntent(append);
- if (append) { //resumePlayback: false
- NavigationHelper.enqueueOnPlayer(activity, queue, PlayerType.POPUP);
- } else {
- replaceQueueIfUserConfirms(() -> NavigationHelper
- .playOnPopupPlayer(activity, queue, true));
- }
- }
-
- /**
- * Opens the video player, in fullscreen if needed. In order to open fullscreen, the activity
- * is toggled to landscape orientation (which will then cause fullscreen mode).
- *
- * @param directlyFullscreenIfApplicable whether to open fullscreen if we are not already
- * in landscape and screen orientation is locked
- */
- public void openVideoPlayer(final boolean directlyFullscreenIfApplicable) {
- if (directlyFullscreenIfApplicable
- && !DeviceUtils.isLandscape(requireContext())
- && PlayerHelper.globalScreenOrientationLocked(requireContext())) {
- // Make sure the bottom sheet turns out expanded. When this code kicks in the bottom
- // sheet could not have fully expanded yet, and thus be in the STATE_SETTLING state.
- // When the activity is rotated, and its state is saved and then restored, the bottom
- // sheet would forget what it was doing, since even if STATE_SETTLING is restored, it
- // doesn't tell which state it was settling to, and thus the bottom sheet settles to
- // STATE_COLLAPSED. This can be solved by manually setting the state that will be
- // restored (i.e. bottomSheetState) to STATE_EXPANDED.
- updateBottomSheetState(BottomSheetBehavior.STATE_EXPANDED);
- // toggle landscape in order to open directly in fullscreen
- onScreenRotationButtonClicked();
- }
-
- if (PreferenceManager.getDefaultSharedPreferences(activity)
- .getBoolean(this.getString(R.string.use_external_video_player_key), false)) {
- showExternalVideoPlaybackDialog();
- } else {
- replaceQueueIfUserConfirms(this::openMainPlayer);
- }
- }
-
- /**
- * If the option to start directly fullscreen is enabled, calls
- * {@link #openVideoPlayer(boolean)} with {@code directlyFullscreenIfApplicable = true}, so that
- * if the user is not already in landscape and he has screen orientation locked the activity
- * rotates and fullscreen starts. Otherwise, if the option to start directly fullscreen is
- * disabled, calls {@link #openVideoPlayer(boolean)} with {@code directlyFullscreenIfApplicable
- * = false}, hence preventing it from going directly fullscreen.
- */
- public void openVideoPlayerAutoFullscreen() {
- openVideoPlayer(PlayerHelper.isStartMainPlayerFullscreenEnabled(requireContext()));
- }
-
- private void openNormalBackgroundPlayer(final boolean append) {
- // See UI changes while remote playQueue changes
- if (!isPlayerAvailable()) {
- playerHolder.startService(false, this);
- }
-
- final PlayQueue queue = setupPlayQueueForIntent(append);
- if (append) {
- NavigationHelper.enqueueOnPlayer(activity, queue, PlayerType.AUDIO);
- } else {
- replaceQueueIfUserConfirms(() -> NavigationHelper
- .playOnBackgroundPlayer(activity, queue, true));
- }
- }
-
- private void openMainPlayer() {
- if (noPlayerServiceAvailable()) {
- playerHolder.startService(autoPlayEnabled, this);
- return;
- }
- if (currentInfo == null) {
- return;
- }
-
- final PlayQueue queue = setupPlayQueueForIntent(false);
- tryAddVideoPlayerView();
-
- final Intent playerIntent = NavigationHelper.getPlayerIntent(requireContext(),
- PlayerService.class, queue, true, autoPlayEnabled);
- ContextCompat.startForegroundService(activity, playerIntent);
- }
-
- /**
- * When the video detail fragment is already showing details for a video and the user opens a
- * new one, the video detail fragment changes all of its old data to the new stream, so if there
- * is a video player currently open it should be hidden. This method does exactly that. If
- * autoplay is enabled, the underlying player is not stopped completely, since it is going to
- * be reused in a few milliseconds and the flickering would be annoying.
- */
- private void hideMainPlayerOnLoadingNewStream() {
- final var root = getRoot();
- if (noPlayerServiceAvailable() || root.isEmpty() || !player.videoPlayerSelected()) {
- return;
- }
-
- removeVideoPlayerView();
- if (isAutoplayEnabled()) {
- playerService.stopForImmediateReusing();
- root.ifPresent(view -> view.setVisibility(View.GONE));
- } else {
- playerHolder.stopService();
- }
- }
-
- private PlayQueue setupPlayQueueForIntent(final boolean append) {
- if (append) {
- return new SinglePlayQueue(currentInfo);
- }
-
- PlayQueue queue = playQueue;
- // Size can be 0 because queue removes bad stream automatically when error occurs
- if (queue == null || queue.isEmpty()) {
- queue = new SinglePlayQueue(currentInfo);
- }
-
- return queue;
- }
-
- /*//////////////////////////////////////////////////////////////////////////
- // Utils
- //////////////////////////////////////////////////////////////////////////*/
-
- public void setAutoPlay(final boolean autoPlay) {
- this.autoPlayEnabled = autoPlay;
- }
-
- private void startOnExternalPlayer(@NonNull final Context context,
- @NonNull final StreamInfo info,
- @NonNull final Stream selectedStream) {
- NavigationHelper.playOnExternalPlayer(context, currentInfo.getName(),
- currentInfo.getSubChannelName(), selectedStream);
-
- final HistoryRecordManager recordManager = new HistoryRecordManager(requireContext());
- disposables.add(recordManager.onViewed(info).onErrorComplete()
- .subscribe(
- ignored -> { /* successful */ },
- error -> Log.e(TAG, "Register view failure: ", error)
- ));
- }
-
- private boolean isExternalPlayerEnabled() {
- return PreferenceManager.getDefaultSharedPreferences(requireContext())
- .getBoolean(getString(R.string.use_external_video_player_key), false);
- }
-
- // This method overrides default behaviour when setAutoPlay() is called.
- // Don't auto play if the user selected an external player or disabled it in settings
- private boolean isAutoplayEnabled() {
- return autoPlayEnabled
- && !isExternalPlayerEnabled()
- && (!isPlayerAvailable() || player.videoPlayerSelected())
- && bottomSheetState != BottomSheetBehavior.STATE_HIDDEN
- && PlayerHelper.isAutoplayAllowedByUser(requireContext());
- }
-
- private void tryAddVideoPlayerView() {
- if (isPlayerAvailable() && getView() != null) {
- // Setup the surface view height, so that it fits the video correctly; this is done also
- // here, and not only in the Handler, to avoid a choppy fullscreen rotation animation.
- setHeightThumbnail();
- }
-
- // do all the null checks in the posted lambda, too, since the player, the binding and the
- // view could be set or unset before the lambda gets executed on the next main thread cycle
- new Handler(Looper.getMainLooper()).post(() -> {
- if (!isPlayerAvailable() || getView() == null) {
- return;
- }
-
- // setup the surface view height, so that it fits the video correctly
- setHeightThumbnail();
-
- player.UIs().getOpt(MainPlayerUi.class).ifPresent(playerUi -> {
- // sometimes binding would be null here, even though getView() != null above u.u
- if (binding != null) {
- // prevent from re-adding a view multiple times
- playerUi.removeViewFromParent();
- binding.playerPlaceholder.addView(playerUi.getBinding().getRoot());
- playerUi.setupVideoSurfaceIfNeeded();
- }
- });
- });
- }
-
- private void removeVideoPlayerView() {
- makeDefaultHeightForVideoPlaceholder();
-
- if (player != null) {
- player.UIs().getOpt(VideoPlayerUi.class).ifPresent(VideoPlayerUi::removeViewFromParent);
- }
- }
-
- private void makeDefaultHeightForVideoPlaceholder() {
- if (getView() == null) {
- return;
- }
-
- binding.playerPlaceholder.getLayoutParams().height = FrameLayout.LayoutParams.MATCH_PARENT;
- binding.playerPlaceholder.requestLayout();
- }
-
- private final ViewTreeObserver.OnPreDrawListener preDrawListener =
- new ViewTreeObserver.OnPreDrawListener() {
- @Override
- public boolean onPreDraw() {
- final DisplayMetrics metrics = getResources().getDisplayMetrics();
-
- if (getView() != null) {
- final int height = (DeviceUtils.isInMultiWindow(activity)
- ? requireView()
- : activity.getWindow().getDecorView()).getHeight();
- setHeightThumbnail(height, metrics);
- getView().getViewTreeObserver().removeOnPreDrawListener(preDrawListener);
- }
- return false;
- }
- };
-
- /**
- * Method which controls the size of thumbnail and the size of main player inside
- * a layout with thumbnail. It decides what height the player should have in both
- * screen orientations. It knows about multiWindow feature
- * and about videos with aspectRatio ZOOM (the height for them will be a bit higher,
- * {@link #MAX_PLAYER_HEIGHT})
- */
- private void setHeightThumbnail() {
- final DisplayMetrics metrics = getResources().getDisplayMetrics();
- final boolean isPortrait = metrics.heightPixels > metrics.widthPixels;
- requireView().getViewTreeObserver().removeOnPreDrawListener(preDrawListener);
-
- if (isFullscreen()) {
- final int height = (DeviceUtils.isInMultiWindow(activity)
- ? requireView()
- : activity.getWindow().getDecorView()).getHeight();
- // Height is zero when the view is not yet displayed like after orientation change
- if (height != 0) {
- setHeightThumbnail(height, metrics);
- } else {
- requireView().getViewTreeObserver().addOnPreDrawListener(preDrawListener);
- }
- } else {
- final int height = (int) (isPortrait
- ? metrics.widthPixels / (16.0f / 9.0f)
- : metrics.heightPixels / 2.0f);
- setHeightThumbnail(height, metrics);
- }
- }
-
- private void setHeightThumbnail(final int newHeight, final DisplayMetrics metrics) {
- binding.detailThumbnailImageView.setLayoutParams(
- new FrameLayout.LayoutParams(
- RelativeLayout.LayoutParams.MATCH_PARENT, newHeight));
- binding.detailThumbnailImageView.setMinimumHeight(newHeight);
- if (isPlayerAvailable()) {
- final int maxHeight = (int) (metrics.heightPixels * MAX_PLAYER_HEIGHT);
- player.UIs().getOpt(VideoPlayerUi.class).ifPresent(ui ->
- ui.getBinding().surfaceView.setHeights(newHeight,
- ui.isFullscreen() ? newHeight : maxHeight));
- }
- }
-
- private void showContent() {
- binding.detailContentRootHiding.setVisibility(View.VISIBLE);
- }
-
- private void setInitialData(final int newServiceId,
- @Nullable final String newUrl,
- @NonNull final String newTitle,
- @Nullable final PlayQueue newPlayQueue) {
- this.serviceId = newServiceId;
- this.url = newUrl;
- this.title = newTitle;
- this.playQueue = newPlayQueue;
- }
-
- private void setErrorImage() {
- if (binding == null || activity == null) {
- return;
- }
-
- binding.detailThumbnailImageView.setImageDrawable(
- AppCompatResources.getDrawable(requireContext(), R.drawable.not_available_monkey));
- animate(binding.detailThumbnailImageView, false, 0, AnimationType.ALPHA,
- 0, () -> animate(binding.detailThumbnailImageView, true, 500));
- }
-
- @Override
- public void handleError() {
- super.handleError();
- setErrorImage();
-
- if (binding.relatedItemsLayout != null) { // hide related streams for tablets
- binding.relatedItemsLayout.setVisibility(View.INVISIBLE);
- }
-
- // hide comments / related streams / description tabs
- binding.viewPager.setVisibility(View.GONE);
- binding.tabLayout.setVisibility(View.GONE);
- }
-
- private void hideAgeRestrictedContent() {
- showTextError(getString(R.string.restricted_video,
- getString(R.string.show_age_restricted_content_title)));
- }
-
- private void setupBroadcastReceiver() {
- broadcastReceiver = new BroadcastReceiver() {
- @Override
- public void onReceive(final Context context, final Intent intent) {
- switch (intent.getAction()) {
- case ACTION_SHOW_MAIN_PLAYER:
- bottomSheetBehavior.setState(BottomSheetBehavior.STATE_EXPANDED);
- break;
- case ACTION_HIDE_MAIN_PLAYER:
- bottomSheetBehavior.setState(BottomSheetBehavior.STATE_HIDDEN);
- break;
- case ACTION_PLAYER_STARTED:
- // If the state is not hidden we don't need to show the mini player
- if (bottomSheetBehavior.getState() == BottomSheetBehavior.STATE_HIDDEN) {
- bottomSheetBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
- }
- // Rebound to the service if it was closed via notification or mini player
- if (!playerHolder.isBound()) {
- playerHolder.startService(
- false, VideoDetailFragment.this);
- }
- break;
- }
- }
- };
- final IntentFilter intentFilter = new IntentFilter();
- intentFilter.addAction(ACTION_SHOW_MAIN_PLAYER);
- intentFilter.addAction(ACTION_HIDE_MAIN_PLAYER);
- intentFilter.addAction(ACTION_PLAYER_STARTED);
- activity.registerReceiver(broadcastReceiver, intentFilter);
- }
-
-
- /*//////////////////////////////////////////////////////////////////////////
- // Orientation listener
- //////////////////////////////////////////////////////////////////////////*/
-
- private void restoreDefaultOrientation() {
- if (isPlayerAvailable() && player.videoPlayerSelected()) {
- toggleFullscreenIfInFullscreenMode();
- }
-
- // This will show systemUI and pause the player.
- // User can tap on Play button and video will be in fullscreen mode again
- // Note for tablet: trying to avoid orientation changes since it's not easy
- // to physically rotate the tablet every time
- if (activity != null && !DeviceUtils.isTablet(activity)) {
- activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED);
- }
- }
-
- /*//////////////////////////////////////////////////////////////////////////
- // Contract
- //////////////////////////////////////////////////////////////////////////*/
-
- @Override
- public void showLoading() {
-
- super.showLoading();
-
- //if data is already cached, transition from VISIBLE -> INVISIBLE -> VISIBLE is not required
- if (!ExtractorHelper.isCached(serviceId, url, InfoCache.Type.STREAM)) {
- binding.detailContentRootHiding.setVisibility(View.INVISIBLE);
- }
-
- animate(binding.detailThumbnailPlayButton, false, 50);
- animate(binding.detailDurationView, false, 100);
- binding.detailPositionView.setVisibility(View.GONE);
- binding.positionView.setVisibility(View.GONE);
-
- binding.detailVideoTitleView.setText(title);
- binding.detailVideoTitleView.setMaxLines(1);
- animate(binding.detailVideoTitleView, true, 0);
-
- binding.detailToggleSecondaryControlsView.setVisibility(View.GONE);
- binding.detailTitleRootLayout.setClickable(false);
- binding.detailSecondaryControlPanel.setVisibility(View.GONE);
-
- if (binding.relatedItemsLayout != null) {
- if (showRelatedItems) {
- binding.relatedItemsLayout.setVisibility(
- isFullscreen() ? View.GONE : View.INVISIBLE);
- } else {
- binding.relatedItemsLayout.setVisibility(View.GONE);
- }
- }
-
- CoilUtils.dispose(binding.detailThumbnailImageView);
- CoilUtils.dispose(binding.detailSubChannelThumbnailView);
- CoilUtils.dispose(binding.overlayThumbnail);
- CoilUtils.dispose(binding.detailUploaderThumbnailView);
-
- binding.detailThumbnailImageView.setImageBitmap(null);
- binding.detailSubChannelThumbnailView.setImageBitmap(null);
- }
-
- @Override
- public void handleResult(@NonNull final StreamInfo info) {
- super.handleResult(info);
-
- currentInfo = info;
- setInitialData(info.getServiceId(), info.getOriginalUrl(), info.getName(), playQueue);
-
- updateTabs(info);
-
- animate(binding.detailThumbnailPlayButton, true, 200);
- binding.detailVideoTitleView.setText(title);
-
- binding.detailSubChannelThumbnailView.setVisibility(View.GONE);
-
- if (!isEmpty(info.getSubChannelName())) {
- displayBothUploaderAndSubChannel(info);
- } else {
- displayUploaderAsSubChannel(info);
- }
-
- if (info.getViewCount() >= 0) {
- if (info.getStreamType().equals(StreamType.AUDIO_LIVE_STREAM)) {
- binding.detailViewCountView.setText(Localization.listeningCount(activity,
- info.getViewCount()));
- } else if (info.getStreamType().equals(StreamType.LIVE_STREAM)) {
- binding.detailViewCountView.setText(Localization
- .localizeWatchingCount(activity, info.getViewCount()));
- } else {
- binding.detailViewCountView.setText(Localization
- .localizeViewCount(activity, info.getViewCount()));
- }
- binding.detailViewCountView.setVisibility(View.VISIBLE);
- } else {
- binding.detailViewCountView.setVisibility(View.GONE);
- }
-
- if (info.getDislikeCount() == -1 && info.getLikeCount() == -1) {
- binding.detailThumbsDownImgView.setVisibility(View.VISIBLE);
- binding.detailThumbsUpImgView.setVisibility(View.VISIBLE);
- binding.detailThumbsUpCountView.setVisibility(View.GONE);
- binding.detailThumbsDownCountView.setVisibility(View.GONE);
-
- binding.detailThumbsDisabledView.setVisibility(View.VISIBLE);
- } else {
- if (info.getDislikeCount() >= 0) {
- binding.detailThumbsDownCountView.setText(Localization
- .shortCount(activity, info.getDislikeCount()));
- binding.detailThumbsDownCountView.setVisibility(View.VISIBLE);
- binding.detailThumbsDownImgView.setVisibility(View.VISIBLE);
- } else {
- binding.detailThumbsDownCountView.setVisibility(View.GONE);
- binding.detailThumbsDownImgView.setVisibility(View.GONE);
- }
-
- if (info.getLikeCount() >= 0) {
- binding.detailThumbsUpCountView.setText(Localization.shortCount(activity,
- info.getLikeCount()));
- binding.detailThumbsUpCountView.setVisibility(View.VISIBLE);
- binding.detailThumbsUpImgView.setVisibility(View.VISIBLE);
- } else {
- binding.detailThumbsUpCountView.setVisibility(View.GONE);
- binding.detailThumbsUpImgView.setVisibility(View.GONE);
- }
- binding.detailThumbsDisabledView.setVisibility(View.GONE);
- }
-
- if (info.getDuration() > 0) {
- binding.detailDurationView.setText(Localization.getDurationString(info.getDuration()));
- binding.detailDurationView.setBackgroundColor(
- ContextCompat.getColor(activity, R.color.duration_background_color));
- animate(binding.detailDurationView, true, 100);
- } else if (info.getStreamType() == StreamType.LIVE_STREAM) {
- binding.detailDurationView.setText(R.string.duration_live);
- binding.detailDurationView.setBackgroundColor(
- ContextCompat.getColor(activity, R.color.live_duration_background_color));
- animate(binding.detailDurationView, true, 100);
- } else {
- binding.detailDurationView.setVisibility(View.GONE);
- }
-
- binding.detailTitleRootLayout.setClickable(true);
- binding.detailToggleSecondaryControlsView.setRotation(0);
- binding.detailToggleSecondaryControlsView.setVisibility(View.VISIBLE);
- binding.detailSecondaryControlPanel.setVisibility(View.GONE);
-
- checkUpdateProgressInfo(info);
- CoilHelper.INSTANCE.loadDetailsThumbnail(binding.detailThumbnailImageView,
- info.getThumbnails());
- showMetaInfoInTextView(info.getMetaInfo(), binding.detailMetaInfoTextView,
- binding.detailMetaInfoSeparator, disposables);
-
- if (!isPlayerAvailable() || player.isStopped()) {
- updateOverlayData(info.getName(), info.getUploaderName(), info.getThumbnails());
- }
-
- if (!info.getErrors().isEmpty()) {
- // Bandcamp fan pages are not yet supported and thus a ContentNotAvailableException is
- // thrown. This is not an error and thus should not be shown to the user.
- for (final Throwable throwable : info.getErrors()) {
- if (throwable instanceof ContentNotSupportedException
- && "Fan pages are not supported".equals(throwable.getMessage())) {
- info.getErrors().remove(throwable);
- }
- }
-
- if (!info.getErrors().isEmpty()) {
- showSnackBarError(new ErrorInfo(info.getErrors(),
- UserAction.REQUESTED_STREAM, info.getUrl(), info));
- }
- }
-
- binding.detailControlsDownload.setVisibility(
- StreamTypeUtil.isLiveStream(info.getStreamType()) ? View.GONE : View.VISIBLE);
- binding.detailControlsBackground.setVisibility(
- info.getAudioStreams().isEmpty() && info.getVideoStreams().isEmpty()
- ? View.GONE : View.VISIBLE);
-
- final boolean noVideoStreams =
- info.getVideoStreams().isEmpty() && info.getVideoOnlyStreams().isEmpty();
- binding.detailControlsPopup.setVisibility(noVideoStreams ? View.GONE : View.VISIBLE);
- binding.detailThumbnailPlayButton.setImageResource(
- noVideoStreams ? R.drawable.ic_headset_shadow : R.drawable.ic_play_arrow_shadow);
- }
-
- private void displayUploaderAsSubChannel(final StreamInfo info) {
- binding.detailSubChannelTextView.setText(info.getUploaderName());
- binding.detailSubChannelTextView.setVisibility(View.VISIBLE);
- binding.detailSubChannelTextView.setSelected(true);
-
- if (info.getUploaderSubscriberCount() > -1) {
- binding.detailUploaderTextView.setText(
- Localization.shortSubscriberCount(activity, info.getUploaderSubscriberCount()));
- binding.detailUploaderTextView.setVisibility(View.VISIBLE);
- } else {
- binding.detailUploaderTextView.setVisibility(View.GONE);
- }
-
- CoilHelper.INSTANCE.loadAvatar(binding.detailSubChannelThumbnailView,
- info.getUploaderAvatars());
- binding.detailSubChannelThumbnailView.setVisibility(View.VISIBLE);
- binding.detailUploaderThumbnailView.setVisibility(View.GONE);
- }
-
- private void displayBothUploaderAndSubChannel(final StreamInfo info) {
- binding.detailSubChannelTextView.setText(info.getSubChannelName());
- binding.detailSubChannelTextView.setVisibility(View.VISIBLE);
- binding.detailSubChannelTextView.setSelected(true);
-
- final StringBuilder subText = new StringBuilder();
- if (!isEmpty(info.getUploaderName())) {
- subText.append(
- String.format(getString(R.string.video_detail_by), info.getUploaderName()));
- }
- if (info.getUploaderSubscriberCount() > -1) {
- if (subText.length() > 0) {
- subText.append(Localization.DOT_SEPARATOR);
- }
- subText.append(
- Localization.shortSubscriberCount(activity, info.getUploaderSubscriberCount()));
- }
-
- if (subText.length() > 0) {
- binding.detailUploaderTextView.setText(subText);
- binding.detailUploaderTextView.setVisibility(View.VISIBLE);
- binding.detailUploaderTextView.setSelected(true);
- } else {
- binding.detailUploaderTextView.setVisibility(View.GONE);
- }
-
- CoilHelper.INSTANCE.loadAvatar(binding.detailSubChannelThumbnailView,
- info.getSubChannelAvatars());
- binding.detailSubChannelThumbnailView.setVisibility(View.VISIBLE);
- CoilHelper.INSTANCE.loadAvatar(binding.detailUploaderThumbnailView,
- info.getUploaderAvatars());
- binding.detailUploaderThumbnailView.setVisibility(View.VISIBLE);
- }
-
- public void openDownloadDialog() {
- if (currentInfo == null) {
- return;
- }
-
- try {
- final DownloadDialog downloadDialog = new DownloadDialog(activity, currentInfo);
- downloadDialog.show(activity.getSupportFragmentManager(), "downloadDialog");
- } catch (final Exception e) {
- ErrorUtil.showSnackbar(activity, new ErrorInfo(e, UserAction.DOWNLOAD_OPEN_DIALOG,
- "Showing download dialog", currentInfo));
- }
- }
-
- /*//////////////////////////////////////////////////////////////////////////
- // Stream Results
- //////////////////////////////////////////////////////////////////////////*/
-
- private void checkUpdateProgressInfo(@NonNull final StreamInfo info) {
- if (positionSubscriber != null) {
- positionSubscriber.dispose();
- }
- if (!getResumePlaybackEnabled(activity)) {
- binding.positionView.setVisibility(View.GONE);
- binding.detailPositionView.setVisibility(View.GONE);
- return;
- }
- final HistoryRecordManager recordManager = new HistoryRecordManager(requireContext());
- positionSubscriber = recordManager.loadStreamState(info)
- .subscribeOn(Schedulers.io())
- .onErrorComplete()
- .observeOn(AndroidSchedulers.mainThread())
- .subscribe(state -> {
- updatePlaybackProgress(
- state.getProgressMillis(), info.getDuration() * 1000);
- }, e -> {
- // impossible since the onErrorComplete()
- }, () -> {
- binding.positionView.setVisibility(View.GONE);
- binding.detailPositionView.setVisibility(View.GONE);
- });
- }
-
- private void updatePlaybackProgress(final long progress, final long duration) {
- if (!getResumePlaybackEnabled(activity)) {
- return;
- }
- final int progressSeconds = (int) TimeUnit.MILLISECONDS.toSeconds(progress);
- final int durationSeconds = (int) TimeUnit.MILLISECONDS.toSeconds(duration);
- // If the old and the new progress values have a big difference then use animation.
- // Otherwise don't because it affects CPU
- final int progressDifference = Math.abs(binding.positionView.getProgress()
- - progressSeconds);
- binding.positionView.setMax(durationSeconds);
- if (progressDifference > 2) {
- binding.positionView.setProgressAnimated(progressSeconds);
- } else {
- binding.positionView.setProgress(progressSeconds);
- }
- final String position = Localization.getDurationString(progressSeconds);
- if (position != binding.detailPositionView.getText()) {
- binding.detailPositionView.setText(position);
- }
- if (binding.positionView.getVisibility() != View.VISIBLE) {
- animate(binding.positionView, true, 100);
- animate(binding.detailPositionView, true, 100);
- }
- }
-
- /*//////////////////////////////////////////////////////////////////////////
- // Player event listener
- //////////////////////////////////////////////////////////////////////////*/
-
- @Override
- public void onViewCreated() {
- tryAddVideoPlayerView();
- }
-
- @Override
- public void onQueueUpdate(final PlayQueue queue) {
- playQueue = queue;
- if (DEBUG) {
- Log.d(TAG, "onQueueUpdate() called with: serviceId = ["
- + serviceId + "], url = [" + url + "], name = ["
- + title + "], playQueue = [" + playQueue + "]");
- }
-
- // Register broadcast receiver to listen to playQueue changes
- // and hide the overlayPlayQueueButton when the playQueue is empty / destroyed.
- if (playQueue != null && playQueue.getBroadcastReceiver() != null) {
- playQueue.getBroadcastReceiver().subscribe(
- event -> updateOverlayPlayQueueButtonVisibility()
- );
- }
-
- // This should be the only place where we push data to stack.
- // It will allow to have live instance of PlayQueue with actual information about
- // deleted/added items inside Channel/Playlist queue and makes possible to have
- // a history of played items
- @Nullable final StackItem stackPeek = stack.peek();
- if (stackPeek != null && !stackPeek.getPlayQueue().equalStreams(queue)) {
- @Nullable final PlayQueueItem playQueueItem = queue.getItem();
- if (playQueueItem != null) {
- stack.push(new StackItem(playQueueItem.getServiceId(), playQueueItem.getUrl(),
- playQueueItem.getTitle(), queue));
- return;
- } // else continue below
- }
-
- @Nullable final StackItem stackWithQueue = findQueueInStack(queue);
- if (stackWithQueue != null) {
- // On every MainPlayer service's destroy() playQueue gets disposed and
- // no longer able to track progress. That's why we update our cached disposed
- // queue with the new one that is active and have the same history.
- // Without that the cached playQueue will have an old recovery position
- stackWithQueue.setPlayQueue(queue);
- }
- }
-
- @Override
- public void onPlaybackUpdate(final int state,
- final int repeatMode,
- final boolean shuffled,
- final PlaybackParameters parameters) {
- setOverlayPlayPauseImage(player != null && player.isPlaying());
-
- if (state == Player.STATE_PLAYING) {
- if (binding.positionView.getAlpha() != 1.0f
- && player.getPlayQueue() != null
- && player.getPlayQueue().getItem() != null
- && player.getPlayQueue().getItem().getUrl().equals(url)) {
- animate(binding.positionView, true, 100);
- animate(binding.detailPositionView, true, 100);
- }
- }
- }
-
- @Override
- public void onProgressUpdate(final int currentProgress,
- final int duration,
- final int bufferPercent) {
- // Progress updates every second even if media is paused. It's useless until playing
- if (!player.isPlaying() || playQueue == null) {
- return;
- }
-
- if (player.getPlayQueue().getItem().getUrl().equals(url)) {
- updatePlaybackProgress(currentProgress, duration);
- }
- }
-
- @Override
- public void onMetadataUpdate(final StreamInfo info, final PlayQueue queue) {
- final StackItem item = findQueueInStack(queue);
- if (item != null) {
- // When PlayQueue can have multiple streams (PlaylistPlayQueue or ChannelPlayQueue)
- // every new played stream gives new title and url.
- // StackItem contains information about first played stream. Let's update it here
- item.setTitle(info.getName());
- item.setUrl(info.getUrl());
- }
- // They are not equal when user watches something in popup while browsing in fragment and
- // then changes screen orientation. In that case the fragment will set itself as
- // a service listener and will receive initial call to onMetadataUpdate()
- if (!queue.equalStreams(playQueue)) {
- return;
- }
-
- updateOverlayData(info.getName(), info.getUploaderName(), info.getThumbnails());
- if (currentInfo != null && info.getUrl().equals(currentInfo.getUrl())) {
- return;
- }
-
- currentInfo = info;
- setInitialData(info.getServiceId(), info.getUrl(), info.getName(), queue);
- setAutoPlay(false);
- // Delay execution just because it freezes the main thread, and while playing
- // next/previous video you see visual glitches
- // (when non-vertical video goes after vertical video)
- prepareAndHandleInfoIfNeededAfterDelay(info, true, 200);
- }
-
- @Override
- public void onPlayerError(final PlaybackException error, final boolean isCatchableException) {
- if (!isCatchableException) {
- // Properly exit from fullscreen
- toggleFullscreenIfInFullscreenMode();
- hideMainPlayerOnLoadingNewStream();
- }
- }
-
- @Override
- public void onServiceStopped() {
- // the binding could be null at this point, if the app is finishing
- if (binding != null) {
- setOverlayPlayPauseImage(false);
- if (currentInfo != null) {
- updateOverlayData(currentInfo.getName(),
- currentInfo.getUploaderName(),
- currentInfo.getThumbnails());
- }
- updateOverlayPlayQueueButtonVisibility();
- }
- }
-
- @Override
- public void onFullscreenStateChanged(final boolean fullscreen) {
- setupBrightness();
- if (!isPlayerAndPlayerServiceAvailable()
- || player.UIs().getOpt(MainPlayerUi.class).isEmpty()
- || getRoot().map(View::getParent).isEmpty()) {
- return;
- }
-
- if (fullscreen) {
- hideSystemUiIfNeeded();
- binding.overlayPlayPauseButton.requestFocus();
- } else {
- showSystemUi();
- }
-
- if (binding.relatedItemsLayout != null) {
- binding.relatedItemsLayout.setVisibility(fullscreen ? View.GONE : View.VISIBLE);
- }
- scrollToTop();
-
- tryAddVideoPlayerView();
- }
-
- @Override
- public void onScreenRotationButtonClicked() {
- // In tablet user experience will be better if screen will not be rotated
- // from landscape to portrait every time.
- // Just turn on fullscreen mode in landscape orientation
- // or portrait & unlocked global orientation
- final boolean isLandscape = DeviceUtils.isLandscape(requireContext());
- if (DeviceUtils.isTablet(activity)
- && (!globalScreenOrientationLocked(activity) || isLandscape)) {
- player.UIs().getOpt(MainPlayerUi.class).ifPresent(MainPlayerUi::toggleFullscreen);
- return;
- }
-
- final int newOrientation = isLandscape
- ? ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
- : ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE;
-
- activity.setRequestedOrientation(newOrientation);
- }
-
- /*
- * Will scroll down to description view after long click on moreOptionsButton
- * */
- @Override
- public void onMoreOptionsLongClicked() {
- final CoordinatorLayout.LayoutParams params =
- (CoordinatorLayout.LayoutParams) binding.appBarLayout.getLayoutParams();
- final AppBarLayout.Behavior behavior = (AppBarLayout.Behavior) params.getBehavior();
- final ValueAnimator valueAnimator = ValueAnimator
- .ofInt(0, -binding.playerPlaceholder.getHeight());
- valueAnimator.setInterpolator(new DecelerateInterpolator());
- valueAnimator.addUpdateListener(animation -> {
- behavior.setTopAndBottomOffset((int) animation.getAnimatedValue());
- binding.appBarLayout.requestLayout();
- });
- valueAnimator.setInterpolator(new DecelerateInterpolator());
- valueAnimator.setDuration(500);
- valueAnimator.start();
- }
-
- /*//////////////////////////////////////////////////////////////////////////
- // Player related utils
- //////////////////////////////////////////////////////////////////////////*/
-
- private void showSystemUi() {
- if (DEBUG) {
- Log.d(TAG, "showSystemUi() called");
- }
-
- if (activity == null) {
- return;
- }
-
- // Prevent jumping of the player on devices with cutout
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
- activity.getWindow().getAttributes().layoutInDisplayCutoutMode =
- WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT;
- }
- activity.getWindow().getDecorView().setSystemUiVisibility(0);
- activity.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
- activity.getWindow().setStatusBarColor(ThemeHelper.resolveColorFromAttr(
- requireContext(), android.R.attr.colorPrimary));
- }
-
- private void hideSystemUi() {
- if (DEBUG) {
- Log.d(TAG, "hideSystemUi() called");
- }
-
- if (activity == null) {
- return;
- }
-
- // Prevent jumping of the player on devices with cutout
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
- activity.getWindow().getAttributes().layoutInDisplayCutoutMode =
- WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
- }
- int visibility = View.SYSTEM_UI_FLAG_LAYOUT_STABLE
- | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
- | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
- | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
- | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY;
-
- // In multiWindow mode status bar is not transparent for devices with cutout
- // if I include this flag. So without it is better in this case
- final boolean isInMultiWindow = DeviceUtils.isInMultiWindow(activity);
- if (!isInMultiWindow) {
- visibility |= View.SYSTEM_UI_FLAG_FULLSCREEN;
- }
- activity.getWindow().getDecorView().setSystemUiVisibility(visibility);
-
- if (isInMultiWindow || isFullscreen()) {
- activity.getWindow().setStatusBarColor(Color.TRANSPARENT);
- activity.getWindow().setNavigationBarColor(Color.TRANSPARENT);
- }
- activity.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
- }
-
- // Listener implementation
- @Override
- public void hideSystemUiIfNeeded() {
- if (isFullscreen()
- && bottomSheetBehavior.getState() == BottomSheetBehavior.STATE_EXPANDED) {
- hideSystemUi();
- }
- }
-
- private boolean isFullscreen() {
- return isPlayerAvailable() && player.UIs().getOpt(VideoPlayerUi.class)
- .map(VideoPlayerUi::isFullscreen).orElse(false);
- }
-
- private boolean playerIsNotStopped() {
- return isPlayerAvailable() && !player.isStopped();
- }
-
- private void restoreDefaultBrightness() {
- final WindowManager.LayoutParams lp = activity.getWindow().getAttributes();
- if (lp.screenBrightness == -1) {
- return;
- }
-
- // Restore the old brightness when fragment.onPause() called or
- // when a player is in portrait
- lp.screenBrightness = -1;
- activity.getWindow().setAttributes(lp);
- }
-
- private void setupBrightness() {
- if (activity == null) {
- return;
- }
-
- final WindowManager.LayoutParams lp = activity.getWindow().getAttributes();
- if (!isFullscreen() || bottomSheetState != BottomSheetBehavior.STATE_EXPANDED) {
- // Apply system brightness when the player is not in fullscreen
- restoreDefaultBrightness();
- } else {
- // Do not restore if user has disabled brightness gesture
- if (!PlayerHelper.getActionForRightGestureSide(activity)
- .equals(getString(R.string.brightness_control_key))
- && !PlayerHelper.getActionForLeftGestureSide(activity)
- .equals(getString(R.string.brightness_control_key))) {
- return;
- }
- // Restore already saved brightness level
- final float brightnessLevel = PlayerHelper.getScreenBrightness(activity);
- if (brightnessLevel == lp.screenBrightness) {
- return;
- }
- lp.screenBrightness = brightnessLevel;
- activity.getWindow().setAttributes(lp);
- }
- }
-
- /**
- * Make changes to the UI to accommodate for better usability on bigger screens such as TVs
- * or in Android's desktop mode (DeX etc).
- */
- private void accommodateForTvAndDesktopMode() {
- if (DeviceUtils.isTv(getContext())) {
- // remove ripple effects from detail controls
- final int transparent = ContextCompat.getColor(requireContext(),
- R.color.transparent_background_color);
- binding.detailControlsPlaylistAppend.setBackgroundColor(transparent);
- binding.detailControlsBackground.setBackgroundColor(transparent);
- binding.detailControlsPopup.setBackgroundColor(transparent);
- binding.detailControlsDownload.setBackgroundColor(transparent);
- binding.detailControlsShare.setBackgroundColor(transparent);
- binding.detailControlsOpenInBrowser.setBackgroundColor(transparent);
- binding.detailControlsPlayWithKodi.setBackgroundColor(transparent);
- }
- if (DeviceUtils.isDesktopMode(getContext())) {
- // Remove the "hover" overlay (since it is visible on all mouse events and interferes
- // with the video content being played)
- binding.detailThumbnailRootLayout.setForeground(null);
- }
- }
-
- private void checkLandscape() {
- if ((!player.isPlaying() && player.getPlayQueue() != playQueue)
- || player.getPlayQueue() == null) {
- setAutoPlay(true);
- }
-
- player.UIs().getOpt(MainPlayerUi.class).ifPresent(MainPlayerUi::checkLandscape);
- // Let's give a user time to look at video information page if video is not playing
- if (globalScreenOrientationLocked(activity) && !player.isPlaying()) {
- player.play();
- }
- }
-
- /*
- * Means that the player fragment was swiped away via BottomSheetLayout
- * and is empty but ready for any new actions. See cleanUp()
- * */
- private boolean wasCleared() {
- return url == null;
- }
-
- @Nullable
- private StackItem findQueueInStack(final PlayQueue queue) {
- StackItem item = null;
- final Iterator iterator = stack.descendingIterator();
- while (iterator.hasNext()) {
- final StackItem next = iterator.next();
- if (next.getPlayQueue().equalStreams(queue)) {
- item = next;
- break;
- }
- }
- return item;
- }
-
- private void replaceQueueIfUserConfirms(final Runnable onAllow) {
- @Nullable final PlayQueue activeQueue = isPlayerAvailable() ? player.getPlayQueue() : null;
-
- // Player will have STATE_IDLE when a user pressed back button
- if (isClearingQueueConfirmationRequired(activity)
- && playerIsNotStopped()
- && activeQueue != null
- && !activeQueue.equalStreams(playQueue)) {
- showClearingQueueConfirmation(onAllow);
- } else {
- onAllow.run();
- }
- }
-
- private void showClearingQueueConfirmation(final Runnable onAllow) {
- new AlertDialog.Builder(activity)
- .setTitle(R.string.clear_queue_confirmation_description)
- .setNegativeButton(R.string.cancel, null)
- .setPositiveButton(R.string.ok, (dialog, which) -> {
- onAllow.run();
- dialog.dismiss();
- })
- .show();
- }
-
- private void showExternalVideoPlaybackDialog() {
- if (currentInfo == null) {
- return;
- }
-
- final AlertDialog.Builder builder = new AlertDialog.Builder(activity);
- builder.setTitle(R.string.select_quality_external_players);
- builder.setNeutralButton(R.string.open_in_browser, (dialog, i) ->
- ShareUtils.openUrlInBrowser(requireActivity(), url));
-
- final List videoStreamsForExternalPlayers =
- ListHelper.getSortedStreamVideosList(
- activity,
- getUrlAndNonTorrentStreams(currentInfo.getVideoStreams()),
- getUrlAndNonTorrentStreams(currentInfo.getVideoOnlyStreams()),
- false,
- false
- );
-
- if (videoStreamsForExternalPlayers.isEmpty()) {
- builder.setMessage(R.string.no_video_streams_available_for_external_players);
- builder.setPositiveButton(R.string.ok, null);
-
- } else {
- final int selectedVideoStreamIndexForExternalPlayers =
- ListHelper.getDefaultResolutionIndex(activity, videoStreamsForExternalPlayers);
- final CharSequence[] resolutions = videoStreamsForExternalPlayers.stream()
- .map(VideoStream::getResolution).toArray(CharSequence[]::new);
-
- builder.setSingleChoiceItems(resolutions, selectedVideoStreamIndexForExternalPlayers,
- null);
- builder.setNegativeButton(R.string.cancel, null);
- builder.setPositiveButton(R.string.ok, (dialog, i) -> {
- final int index = ((AlertDialog) dialog).getListView().getCheckedItemPosition();
- // We don't have to manage the index validity because if there is no stream
- // available for external players, this code will be not executed and if there is
- // no stream which matches the default resolution, 0 is returned by
- // ListHelper.getDefaultResolutionIndex.
- // The index cannot be outside the bounds of the list as its always between 0 and
- // the list size - 1, .
- startOnExternalPlayer(activity, currentInfo,
- videoStreamsForExternalPlayers.get(index));
- });
- }
- builder.show();
- }
-
- private void showExternalAudioPlaybackDialog() {
- if (currentInfo == null) {
- return;
- }
-
- final List audioStreams = getUrlAndNonTorrentStreams(
- currentInfo.getAudioStreams());
- final List audioTracks =
- ListHelper.getFilteredAudioStreams(activity, audioStreams);
-
- if (audioTracks.isEmpty()) {
- Toast.makeText(activity, R.string.no_audio_streams_available_for_external_players,
- Toast.LENGTH_SHORT).show();
- } else if (audioTracks.size() == 1) {
- startOnExternalPlayer(activity, currentInfo, audioTracks.get(0));
- } else {
- final int selectedAudioStream =
- ListHelper.getDefaultAudioFormat(activity, audioTracks);
- final CharSequence[] trackNames = audioTracks.stream()
- .map(audioStream -> Localization.audioTrackName(activity, audioStream))
- .toArray(CharSequence[]::new);
-
- new AlertDialog.Builder(activity)
- .setTitle(R.string.select_audio_track_external_players)
- .setNeutralButton(R.string.open_in_browser, (dialog, i) ->
- ShareUtils.openUrlInBrowser(requireActivity(), url))
- .setSingleChoiceItems(trackNames, selectedAudioStream, null)
- .setNegativeButton(R.string.cancel, null)
- .setPositiveButton(R.string.ok, (dialog, i) -> {
- final int index = ((AlertDialog) dialog).getListView()
- .getCheckedItemPosition();
- startOnExternalPlayer(activity, currentInfo, audioTracks.get(index));
- })
- .show();
- }
- }
-
- /*
- * Remove unneeded information while waiting for a next task
- * */
- private void cleanUp() {
- // New beginning
- stack.clear();
- if (currentWorker != null) {
- currentWorker.dispose();
- }
- playerHolder.stopService();
- setInitialData(0, null, "", null);
- currentInfo = null;
- updateOverlayData(null, null, List.of());
- }
-
- /*//////////////////////////////////////////////////////////////////////////
- // Bottom mini player
- //////////////////////////////////////////////////////////////////////////*/
-
- /**
- * That's for Android TV support. Move focus from main fragment to the player or back
- * based on what is currently selected
- *
- * @param toMain if true than the main fragment will be focused or the player otherwise
- */
- private void moveFocusToMainFragment(final boolean toMain) {
- setupBrightness();
- final ViewGroup mainFragment = requireActivity().findViewById(R.id.fragment_holder);
- // Hamburger button steels a focus even under bottomSheet
- final Toolbar toolbar = requireActivity().findViewById(R.id.toolbar);
- final int afterDescendants = ViewGroup.FOCUS_AFTER_DESCENDANTS;
- final int blockDescendants = ViewGroup.FOCUS_BLOCK_DESCENDANTS;
- if (toMain) {
- mainFragment.setDescendantFocusability(afterDescendants);
- toolbar.setDescendantFocusability(afterDescendants);
- ((ViewGroup) requireView()).setDescendantFocusability(blockDescendants);
- // Only focus the mainFragment if the mainFragment (e.g. search-results)
- // or the toolbar (e.g. Textfield for search) don't have focus.
- // This was done to fix problems with the keyboard input, see also #7490
- if (!mainFragment.hasFocus() && !toolbar.hasFocus()) {
- mainFragment.requestFocus();
- }
- } else {
- mainFragment.setDescendantFocusability(blockDescendants);
- toolbar.setDescendantFocusability(blockDescendants);
- ((ViewGroup) requireView()).setDescendantFocusability(afterDescendants);
- // Only focus the player if it not already has focus
- if (!binding.getRoot().hasFocus()) {
- binding.detailThumbnailRootLayout.requestFocus();
- }
- }
- }
-
- /**
- * When the mini player exists the view underneath it is not touchable.
- * Bottom padding should be equal to the mini player's height in this case
- *
- * @param showMore whether main fragment should be expanded or not
- */
- private void manageSpaceAtTheBottom(final boolean showMore) {
- final int peekHeight = getResources().getDimensionPixelSize(R.dimen.mini_player_height);
- final ViewGroup holder = requireActivity().findViewById(R.id.fragment_holder);
- final int newBottomPadding;
- if (showMore) {
- newBottomPadding = 0;
- } else {
- newBottomPadding = peekHeight;
- }
- if (holder.getPaddingBottom() == newBottomPadding) {
- return;
- }
- holder.setPadding(holder.getPaddingLeft(),
- holder.getPaddingTop(),
- holder.getPaddingRight(),
- newBottomPadding);
- }
-
- private void setupBottomPlayer() {
- final CoordinatorLayout.LayoutParams params =
- (CoordinatorLayout.LayoutParams) binding.appBarLayout.getLayoutParams();
- final AppBarLayout.Behavior behavior = (AppBarLayout.Behavior) params.getBehavior();
-
- final FrameLayout bottomSheetLayout = activity.findViewById(R.id.fragment_player_holder);
- bottomSheetBehavior = BottomSheetBehavior.from(bottomSheetLayout);
- bottomSheetBehavior.setState(lastStableBottomSheetState);
- updateBottomSheetState(lastStableBottomSheetState);
-
- final int peekHeight = getResources().getDimensionPixelSize(R.dimen.mini_player_height);
- if (bottomSheetState != BottomSheetBehavior.STATE_HIDDEN) {
- manageSpaceAtTheBottom(false);
- bottomSheetBehavior.setPeekHeight(peekHeight);
- if (bottomSheetState == BottomSheetBehavior.STATE_COLLAPSED) {
- binding.overlayLayout.setAlpha(MAX_OVERLAY_ALPHA);
- } else if (bottomSheetState == BottomSheetBehavior.STATE_EXPANDED) {
- binding.overlayLayout.setAlpha(0);
- setOverlayElementsClickable(false);
- }
- }
-
- bottomSheetCallback = new BottomSheetBehavior.BottomSheetCallback() {
- @Override
- public void onStateChanged(@NonNull final View bottomSheet, final int newState) {
- updateBottomSheetState(newState);
-
- switch (newState) {
- case BottomSheetBehavior.STATE_HIDDEN:
- moveFocusToMainFragment(true);
- manageSpaceAtTheBottom(true);
-
- bottomSheetBehavior.setPeekHeight(0);
- cleanUp();
- break;
- case BottomSheetBehavior.STATE_EXPANDED:
- moveFocusToMainFragment(false);
- manageSpaceAtTheBottom(false);
-
- bottomSheetBehavior.setPeekHeight(peekHeight);
- // Disable click because overlay buttons located on top of buttons
- // from the player
- setOverlayElementsClickable(false);
- hideSystemUiIfNeeded();
- // Conditions when the player should be expanded to fullscreen
- if (DeviceUtils.isLandscape(requireContext())
- && isPlayerAvailable()
- && player.isPlaying()
- && !isFullscreen()
- && !DeviceUtils.isTablet(activity)) {
- player.UIs().getOpt(MainPlayerUi.class)
- .ifPresent(MainPlayerUi::toggleFullscreen);
- }
- setOverlayLook(binding.appBarLayout, behavior, 1);
- break;
- case BottomSheetBehavior.STATE_COLLAPSED:
- moveFocusToMainFragment(true);
- manageSpaceAtTheBottom(false);
-
- bottomSheetBehavior.setPeekHeight(peekHeight);
-
- // Re-enable clicks
- setOverlayElementsClickable(true);
- if (isPlayerAvailable()) {
- player.UIs().getOpt(MainPlayerUi.class)
- .ifPresent(MainPlayerUi::closeItemsList);
- }
- setOverlayLook(binding.appBarLayout, behavior, 0);
- break;
- case BottomSheetBehavior.STATE_DRAGGING:
- case BottomSheetBehavior.STATE_SETTLING:
- if (isFullscreen()) {
- showSystemUi();
- }
- if (isPlayerAvailable()) {
- player.UIs().getOpt(MainPlayerUi.class).ifPresent(ui -> {
- if (ui.isControlsVisible()) {
- ui.hideControls(0, 0);
- }
- });
- }
- break;
- case BottomSheetBehavior.STATE_HALF_EXPANDED:
- break;
- }
- }
-
- @Override
- public void onSlide(@NonNull final View bottomSheet, final float slideOffset) {
- setOverlayLook(binding.appBarLayout, behavior, slideOffset);
- }
- };
-
- bottomSheetBehavior.addBottomSheetCallback(bottomSheetCallback);
-
- // User opened a new page and the player will hide itself
- activity.getSupportFragmentManager().addOnBackStackChangedListener(() -> {
- if (bottomSheetBehavior.getState() == BottomSheetBehavior.STATE_EXPANDED) {
- bottomSheetBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
- }
- });
- }
-
- private void updateOverlayPlayQueueButtonVisibility() {
- final boolean isPlayQueueEmpty =
- player == null // no player => no play queue :)
- || player.getPlayQueue() == null
- || player.getPlayQueue().isEmpty();
- if (binding != null) {
- // binding is null when rotating the device...
- binding.overlayPlayQueueButton.setVisibility(
- isPlayQueueEmpty ? View.GONE : View.VISIBLE);
- }
- }
-
- private void updateOverlayData(@Nullable final String overlayTitle,
- @Nullable final String uploader,
- @NonNull final List thumbnails) {
- binding.overlayTitleTextView.setText(isEmpty(overlayTitle) ? "" : overlayTitle);
- binding.overlayChannelTextView.setText(isEmpty(uploader) ? "" : uploader);
- binding.overlayThumbnail.setImageDrawable(null);
- CoilHelper.INSTANCE.loadDetailsThumbnail(binding.overlayThumbnail, thumbnails);
- }
-
- private void setOverlayPlayPauseImage(final boolean playerIsPlaying) {
- final int drawable = playerIsPlaying
- ? R.drawable.ic_pause
- : R.drawable.ic_play_arrow;
- binding.overlayPlayPauseButton.setImageResource(drawable);
- }
-
- private void setOverlayLook(final AppBarLayout appBar,
- final AppBarLayout.Behavior behavior,
- final float slideOffset) {
- // SlideOffset < 0 when mini player is about to close via swipe.
- // Stop animation in this case
- if (behavior == null || slideOffset < 0) {
- return;
- }
- binding.overlayLayout.setAlpha(Math.min(MAX_OVERLAY_ALPHA, 1 - slideOffset));
- // These numbers are not special. They just do a cool transition
- behavior.setTopAndBottomOffset(
- (int) (-binding.detailThumbnailImageView.getHeight() * 2 * (1 - slideOffset) / 3));
- appBar.requestLayout();
- }
-
- private void setOverlayElementsClickable(final boolean enable) {
- binding.overlayThumbnail.setClickable(enable);
- binding.overlayThumbnail.setLongClickable(enable);
- binding.overlayMetadataLayout.setClickable(enable);
- binding.overlayMetadataLayout.setLongClickable(enable);
- binding.overlayButtonsLayout.setClickable(enable);
- binding.overlayPlayQueueButton.setClickable(enable);
- binding.overlayPlayPauseButton.setClickable(enable);
- binding.overlayCloseButton.setClickable(enable);
- }
-
- // helpers to check the state of player and playerService
- boolean isPlayerAvailable() {
- return player != null;
- }
-
- boolean noPlayerServiceAvailable() {
- return playerService == null;
- }
-
- boolean isPlayerAndPlayerServiceAvailable() {
- return player != null && playerService != null;
- }
-
- public Optional getRoot() {
- return Optional.ofNullable(player)
- .flatMap(player1 -> player1.UIs().getOpt(VideoPlayerUi.class))
- .map(playerUi -> playerUi.getBinding().getRoot());
- }
-
- private void updateBottomSheetState(final int newState) {
- bottomSheetState = newState;
- if (newState != BottomSheetBehavior.STATE_DRAGGING
- && newState != BottomSheetBehavior.STATE_SETTLING) {
- lastStableBottomSheetState = newState;
- }
- }
-}
diff --git a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.kt b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.kt
new file mode 100644
index 000000000..407936926
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.kt
@@ -0,0 +1,2316 @@
+package org.schabi.newpipe.fragments.detail
+
+import android.animation.ValueAnimator
+import android.annotation.SuppressLint
+import android.app.Activity
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import android.content.SharedPreferences.OnSharedPreferenceChangeListener
+import android.content.pm.ActivityInfo
+import android.database.ContentObserver
+import android.graphics.Color
+import android.graphics.Rect
+import android.os.Build
+import android.os.Bundle
+import android.os.Handler
+import android.os.Looper
+import android.provider.Settings
+import android.util.DisplayMetrics
+import android.util.Log
+import android.util.TypedValue
+import android.view.LayoutInflater
+import android.view.MotionEvent
+import android.view.View
+import android.view.View.OnLongClickListener
+import android.view.View.OnTouchListener
+import android.view.ViewGroup
+import android.view.ViewTreeObserver.OnPreDrawListener
+import android.view.WindowManager
+import android.view.animation.DecelerateInterpolator
+import android.widget.FrameLayout
+import android.widget.RelativeLayout
+import android.widget.Toast
+import androidx.annotation.AttrRes
+import androidx.annotation.StringRes
+import androidx.appcompat.app.AlertDialog
+import androidx.appcompat.content.res.AppCompatResources
+import androidx.appcompat.widget.Toolbar
+import androidx.coordinatorlayout.widget.CoordinatorLayout
+import androidx.core.content.ContextCompat
+import androidx.core.content.edit
+import androidx.core.net.toUri
+import androidx.core.os.postDelayed
+import androidx.core.view.isGone
+import androidx.core.view.isVisible
+import androidx.preference.PreferenceManager
+import coil3.util.CoilUtils
+import com.evernote.android.state.State
+import com.google.android.exoplayer2.PlaybackException
+import com.google.android.exoplayer2.PlaybackParameters
+import com.google.android.material.appbar.AppBarLayout
+import com.google.android.material.bottomsheet.BottomSheetBehavior
+import com.google.android.material.bottomsheet.BottomSheetBehavior.BottomSheetCallback
+import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
+import io.reactivex.rxjava3.disposables.CompositeDisposable
+import io.reactivex.rxjava3.disposables.Disposable
+import io.reactivex.rxjava3.schedulers.Schedulers
+import org.schabi.newpipe.App
+import org.schabi.newpipe.R
+import org.schabi.newpipe.database.stream.model.StreamEntity
+import org.schabi.newpipe.databinding.FragmentVideoDetailBinding
+import org.schabi.newpipe.download.DownloadDialog
+import org.schabi.newpipe.error.ErrorInfo
+import org.schabi.newpipe.error.ErrorUtil.Companion.showSnackbar
+import org.schabi.newpipe.error.ErrorUtil.Companion.showUiErrorSnackbar
+import org.schabi.newpipe.error.ReCaptchaActivity
+import org.schabi.newpipe.error.UserAction
+import org.schabi.newpipe.extractor.Image
+import org.schabi.newpipe.extractor.NewPipe
+import org.schabi.newpipe.extractor.StreamingService.ServiceInfo.MediaCapability
+import org.schabi.newpipe.extractor.exceptions.ContentNotSupportedException
+import org.schabi.newpipe.extractor.exceptions.ExtractionException
+import org.schabi.newpipe.extractor.stream.Stream
+import org.schabi.newpipe.extractor.stream.StreamExtractor
+import org.schabi.newpipe.extractor.stream.StreamInfo
+import org.schabi.newpipe.extractor.stream.StreamType
+import org.schabi.newpipe.fragments.BackPressable
+import org.schabi.newpipe.fragments.BaseStateFragment
+import org.schabi.newpipe.fragments.EmptyFragment
+import org.schabi.newpipe.fragments.MainFragment
+import org.schabi.newpipe.fragments.list.comments.CommentsFragment.Companion.getInstance
+import org.schabi.newpipe.fragments.list.videos.RelatedItemsFragment.Companion.getInstance
+import org.schabi.newpipe.ktx.AnimationType
+import org.schabi.newpipe.ktx.animate
+import org.schabi.newpipe.ktx.animateRotation
+import org.schabi.newpipe.local.dialog.PlaylistDialog
+import org.schabi.newpipe.local.history.HistoryRecordManager
+import org.schabi.newpipe.local.playlist.LocalPlaylistFragment
+import org.schabi.newpipe.player.Player
+import org.schabi.newpipe.player.PlayerService
+import org.schabi.newpipe.player.PlayerType
+import org.schabi.newpipe.player.event.OnKeyDownListener
+import org.schabi.newpipe.player.event.PlayerServiceExtendedEventListener
+import org.schabi.newpipe.player.helper.PlayerHelper
+import org.schabi.newpipe.player.helper.PlayerHolder
+import org.schabi.newpipe.player.playqueue.PlayQueue
+import org.schabi.newpipe.player.playqueue.SinglePlayQueue
+import org.schabi.newpipe.player.ui.MainPlayerUi
+import org.schabi.newpipe.player.ui.VideoPlayerUi
+import org.schabi.newpipe.util.DependentPreferenceHelper
+import org.schabi.newpipe.util.DeviceUtils
+import org.schabi.newpipe.util.ExtractorHelper
+import org.schabi.newpipe.util.InfoCache
+import org.schabi.newpipe.util.ListHelper
+import org.schabi.newpipe.util.Localization
+import org.schabi.newpipe.util.NO_SERVICE_ID
+import org.schabi.newpipe.util.NavigationHelper
+import org.schabi.newpipe.util.PermissionHelper
+import org.schabi.newpipe.util.PermissionHelper.checkStoragePermissions
+import org.schabi.newpipe.util.PlayButtonHelper
+import org.schabi.newpipe.util.StreamTypeUtil
+import org.schabi.newpipe.util.ThemeHelper
+import org.schabi.newpipe.util.external_communication.KoreUtils
+import org.schabi.newpipe.util.external_communication.ShareUtils
+import org.schabi.newpipe.util.image.CoilHelper
+import java.util.LinkedList
+import java.util.concurrent.TimeUnit
+import kotlin.math.abs
+import kotlin.math.max
+import kotlin.math.min
+
+class VideoDetailFragment :
+ BaseStateFragment(),
+ BackPressable,
+ PlayerServiceExtendedEventListener,
+ OnKeyDownListener {
+
+ // stream info
+ @JvmField @State var serviceId: Int = NO_SERVICE_ID
+ @JvmField @State var title: String = ""
+ @JvmField @State var url: String? = null
+ private var currentInfo: StreamInfo? = null
+
+ // player objects
+ private var playQueue: PlayQueue? = null
+ @JvmField @State var autoPlayEnabled: Boolean = true
+ private var playerService: PlayerService? = null
+ private var player: Player? = null
+
+ // views
+ // can't make this lateinit because it needs to be set to null when the view is destroyed
+ private var nullableBinding: FragmentVideoDetailBinding? = null
+ private val binding: FragmentVideoDetailBinding get() = nullableBinding!!
+ private lateinit var pageAdapter: TabAdapter
+ private var settingsContentObserver: ContentObserver? = null
+
+ // tabs
+ private var showComments = false
+ private var showRelatedItems = false
+ private var showDescription = false
+ private lateinit var selectedTabTag: String
+ @AttrRes val tabIcons = ArrayList()
+ @StringRes val tabContentDescriptions = ArrayList()
+ private var tabSettingsChanged = false
+ private var lastAppBarVerticalOffset = Int.Companion.MAX_VALUE // prevents useless updates
+
+ private val preferenceChangeListener =
+ OnSharedPreferenceChangeListener { sharedPreferences, key ->
+ if (getString(R.string.show_comments_key) == key) {
+ showComments = sharedPreferences.getBoolean(key, true)
+ tabSettingsChanged = true
+ } else if (getString(R.string.show_next_video_key) == key) {
+ showRelatedItems = sharedPreferences.getBoolean(key, true)
+ tabSettingsChanged = true
+ } else if (getString(R.string.show_description_key) == key) {
+ showDescription = sharedPreferences.getBoolean(key, true)
+ tabSettingsChanged = true
+ }
+ }
+
+ // bottom sheet
+ @JvmField @State var bottomSheetState: Int = BottomSheetBehavior.STATE_EXPANDED
+ @JvmField @State var lastStableBottomSheetState: Int = BottomSheetBehavior.STATE_EXPANDED
+ private lateinit var bottomSheetBehavior: BottomSheetBehavior
+ private lateinit var bottomSheetCallback: BottomSheetCallback
+ private lateinit var broadcastReceiver: BroadcastReceiver
+
+ // disposables
+ private var currentWorker: Disposable? = null
+ private val disposables = CompositeDisposable()
+ private var positionSubscriber: Disposable? = null
+
+ /*//////////////////////////////////////////////////////////////////////////
+ // Service management
+ ////////////////////////////////////////////////////////////////////////// */
+ override fun onServiceConnected(connectedPlayerService: PlayerService) {
+ playerService = connectedPlayerService
+ }
+
+ override fun onPlayerConnected(connectedPlayer: Player, playAfterConnect: Boolean) {
+ player = connectedPlayer
+
+ // It will do nothing if the player is not in fullscreen mode
+ hideSystemUiIfNeeded()
+
+ if (player?.videoPlayerSelected() != true && !playAfterConnect) {
+ return
+ }
+
+ val mainUi = player?.UIs()?.get(MainPlayerUi::class)
+ if (DeviceUtils.isLandscape(requireContext())) {
+ // If the video is playing but orientation changed
+ // let's make the video in fullscreen again
+ checkLandscape()
+ } else if (mainUi != null && mainUi.isFullscreen && !mainUi.isVerticalVideo &&
+ // Tablet UI has orientation-independent fullscreen
+ !DeviceUtils.isTablet(activity)
+ ) {
+ // Device is in portrait orientation after rotation but UI is in fullscreen.
+ // Return back to non-fullscreen state
+ mainUi.toggleFullscreen()
+ }
+
+ if (playAfterConnect || (currentInfo != null && this.isAutoplayEnabled && mainUi == null)) {
+ autoPlayEnabled = true // forcefully start playing
+ openVideoPlayerAutoFullscreen()
+ }
+ updateOverlayPlayQueueButtonVisibility()
+ }
+
+ override fun onPlayerDisconnected() {
+ player = null
+ // the binding could be null at this point, if the app is finishing
+ if (nullableBinding != null) {
+ restoreDefaultBrightness()
+ }
+ }
+
+ override fun onServiceDisconnected() {
+ playerService = null
+ }
+
+ /*//////////////////////////////////////////////////////////////////////////
+ // Fragment's Lifecycle
+ ////////////////////////////////////////////////////////////////////////// */
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ val prefs = PreferenceManager.getDefaultSharedPreferences(activity)
+ showComments = prefs.getBoolean(getString(R.string.show_comments_key), true)
+ showRelatedItems = prefs.getBoolean(getString(R.string.show_next_video_key), true)
+ showDescription = prefs.getBoolean(getString(R.string.show_description_key), true)
+ selectedTabTag = prefs.getString(
+ getString(R.string.stream_info_selected_tab_key), COMMENTS_TAB_TAG
+ )!!
+ prefs.registerOnSharedPreferenceChangeListener(preferenceChangeListener)
+
+ setupBroadcastReceiver()
+
+ settingsContentObserver = object : ContentObserver(Handler(Looper.getMainLooper())) {
+ override fun onChange(selfChange: Boolean) {
+ if (activity != null && !PlayerHelper.globalScreenOrientationLocked(activity)) {
+ activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED)
+ }
+ }
+ }
+ activity.contentResolver.registerContentObserver(
+ Settings.System.getUriFor(Settings.System.ACCELEROMETER_ROTATION), false,
+ settingsContentObserver!!
+ )
+ }
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View {
+ val newBinding = FragmentVideoDetailBinding.inflate(inflater, container, false)
+ nullableBinding = newBinding
+ return newBinding.getRoot()
+ }
+
+ override fun onPause() {
+ super.onPause()
+ currentWorker?.dispose()
+ restoreDefaultBrightness()
+ PreferenceManager.getDefaultSharedPreferences(requireContext()).edit {
+ putString(
+ getString(R.string.stream_info_selected_tab_key),
+ pageAdapter.getItemTitle(binding.viewPager.currentItem)
+ )
+ }
+ }
+
+ override fun onResume() {
+ super.onResume()
+ if (DEBUG) {
+ Log.d(TAG, "onResume() called")
+ }
+
+ activity.sendBroadcast(Intent(ACTION_VIDEO_FRAGMENT_RESUMED))
+
+ updateOverlayPlayQueueButtonVisibility()
+
+ setupBrightness()
+
+ if (tabSettingsChanged) {
+ tabSettingsChanged = false
+ initTabs()
+ currentInfo?.let { updateTabs(it) }
+ }
+
+ // Check if it was loading when the fragment was stopped/paused
+ if (wasLoading.getAndSet(false) && !wasCleared()) {
+ startLoading(false)
+ }
+ }
+
+ override fun onStop() {
+ super.onStop()
+
+ if (!activity.isChangingConfigurations) {
+ activity.sendBroadcast(Intent(ACTION_VIDEO_FRAGMENT_STOPPED))
+ }
+ }
+
+ override fun onDestroy() {
+ super.onDestroy()
+
+ // Stop the service when user leaves the app with double back press
+ // if video player is selected. Otherwise unbind
+ if (activity.isFinishing && player?.videoPlayerSelected() == true) {
+ PlayerHolder.stopService()
+ } else {
+ PlayerHolder.setListener(null)
+ }
+
+ PreferenceManager.getDefaultSharedPreferences(activity)
+ .unregisterOnSharedPreferenceChangeListener(preferenceChangeListener)
+ activity.unregisterReceiver(broadcastReceiver)
+ activity.contentResolver.unregisterContentObserver(settingsContentObserver!!)
+
+ positionSubscriber?.dispose()
+ currentWorker?.dispose()
+ disposables.clear()
+ positionSubscriber = null
+ currentWorker = null
+ bottomSheetBehavior.removeBottomSheetCallback(bottomSheetCallback)
+
+ if (activity.isFinishing) {
+ playQueue = null
+ currentInfo = null
+ stack = LinkedList()
+ }
+ }
+
+ override fun onDestroyView() {
+ super.onDestroyView()
+ nullableBinding = null
+ }
+
+ override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
+ super.onActivityResult(requestCode, resultCode, data)
+ if (requestCode == ReCaptchaActivity.RECAPTCHA_REQUEST) {
+ if (resultCode == Activity.RESULT_OK) {
+ NavigationHelper.openVideoDetailFragment(
+ requireContext(), getFM(), serviceId, url, title, null, false
+ )
+ } else {
+ Log.e(TAG, "ReCaptcha failed")
+ }
+ } else {
+ Log.e(TAG, "Request code from activity not supported [$requestCode]")
+ }
+ }
+
+ /*//////////////////////////////////////////////////////////////////////////
+ // OnClick
+ ////////////////////////////////////////////////////////////////////////// */
+ private fun setOnClickListeners() {
+ binding.detailTitleRootLayout.setOnClickListener { toggleTitleAndSecondaryControls() }
+ binding.detailUploaderRootLayout.setOnClickListener(
+ makeOnClickListener { info ->
+ if (info.subChannelUrl.isEmpty()) {
+ if (info.uploaderUrl.isNotEmpty()) {
+ openChannel(info.uploaderUrl, info.uploaderName, info.serviceId)
+ } else if (DEBUG) {
+ Log.w(TAG, "Can't open sub-channel because we got no channel URL")
+ }
+ } else {
+ openChannel(info.subChannelUrl, info.subChannelName, info.serviceId)
+ }
+ }
+ )
+ binding.detailThumbnailRootLayout.setOnClickListener {
+ autoPlayEnabled = true // forcefully start playing
+ // FIXME Workaround #7427
+ player?.setRecovery()
+ openVideoPlayerAutoFullscreen()
+ }
+
+ binding.detailControlsBackground.setOnClickListener { openBackgroundPlayer(false) }
+ binding.detailControlsPopup.setOnClickListener { openPopupPlayer(false) }
+ binding.detailControlsPlaylistAppend.setOnClickListener(
+ makeOnClickListener { info ->
+ if (getFM() != null) {
+ val fragment = getParentFragmentManager().findFragmentById(R.id.fragment_holder)
+
+ // commit previous pending changes to database
+ if (fragment is LocalPlaylistFragment) {
+ fragment.saveImmediate()
+ } else if (fragment is MainFragment) {
+ fragment.commitPlaylistTabs()
+ }
+
+ disposables.add(
+ PlaylistDialog.createCorrespondingDialog(
+ requireContext(),
+ listOf(StreamEntity(info))
+ ) { dialog -> dialog.show(getParentFragmentManager(), TAG) }
+ )
+ }
+ }
+ )
+ binding.detailControlsDownload.setOnClickListener {
+ if (checkStoragePermissions(activity, PermissionHelper.DOWNLOAD_DIALOG_REQUEST_CODE)) {
+ openDownloadDialog()
+ }
+ }
+ binding.detailControlsShare.setOnClickListener(
+ makeOnClickListener { info ->
+ ShareUtils.shareText(requireContext(), info.name, info.url, info.thumbnails)
+ }
+ )
+ binding.detailControlsOpenInBrowser.setOnClickListener(
+ makeOnClickListener { info ->
+ ShareUtils.openUrlInBrowser(requireContext(), info.url)
+ }
+ )
+ binding.detailControlsPlayWithKodi.setOnClickListener(
+ makeOnClickListener { info ->
+ KoreUtils.playWithKore(requireContext(), info.url.toUri())
+ }
+ )
+ if (DEBUG) {
+ binding.detailControlsCrashThePlayer.setOnClickListener {
+ VideoDetailPlayerCrasher.onCrashThePlayer(requireContext(), player)
+ }
+ }
+
+ val overlayListener = View.OnClickListener {
+ bottomSheetBehavior.setState(BottomSheetBehavior.STATE_EXPANDED)
+ }
+ binding.overlayThumbnail.setOnClickListener(overlayListener)
+ binding.overlayMetadataLayout.setOnClickListener(overlayListener)
+ binding.overlayButtonsLayout.setOnClickListener(overlayListener)
+ binding.overlayCloseButton.setOnClickListener {
+ bottomSheetBehavior.setState(BottomSheetBehavior.STATE_HIDDEN)
+ }
+ binding.overlayPlayQueueButton.setOnClickListener {
+ NavigationHelper.openPlayQueue(requireContext())
+ }
+ binding.overlayPlayPauseButton.setOnClickListener {
+ if (!playerIsStopped) {
+ player!!.playPause()
+ player!!.UIs().get(VideoPlayerUi::class)?.hideControls(0, 0)
+ showSystemUi()
+ } else {
+ autoPlayEnabled = true // forcefully start playing
+ openVideoPlayer(false)
+ }
+ setOverlayPlayPauseImage(player?.isPlaying == true)
+ }
+ }
+
+ private fun makeOnClickListener(listener: (StreamInfo) -> Unit): View.OnClickListener {
+ return View.OnClickListener {
+ currentInfo?.takeIf { !isLoading.get() }?.let(listener)
+ }
+ }
+
+ private fun setOnLongClickListeners() {
+ binding.detailTitleRootLayout.setOnLongClickListener {
+ binding.detailVideoTitleView.text?.toString()?.let {
+ if (!it.isBlank()) {
+ ShareUtils.copyToClipboard(requireContext(), it)
+ return@setOnLongClickListener true
+ }
+ }
+ return@setOnLongClickListener false
+ }
+ binding.detailUploaderRootLayout.setOnLongClickListener(
+ makeOnLongClickListener { info ->
+ if (info.subChannelUrl.isEmpty()) {
+ Log.w(TAG, "Can't open parent channel because we got no parent channel URL")
+ } else {
+ openChannel(info.uploaderUrl, info.uploaderName, info.serviceId)
+ }
+ }
+ )
+
+ binding.detailControlsBackground.setOnLongClickListener(
+ makeOnLongClickListener { info ->
+ openBackgroundPlayer(true)
+ }
+ )
+ binding.detailControlsPopup.setOnLongClickListener(
+ makeOnLongClickListener { info ->
+ openPopupPlayer(true)
+ }
+ )
+ binding.detailControlsDownload.setOnLongClickListener(
+ makeOnLongClickListener { info ->
+ NavigationHelper.openDownloads(activity)
+ }
+ )
+
+ val overlayListener = makeOnLongClickListener { info ->
+ openChannel(info.uploaderUrl, info.uploaderName, info.serviceId)
+ }
+ binding.overlayThumbnail.setOnLongClickListener(overlayListener)
+ binding.overlayMetadataLayout.setOnLongClickListener(overlayListener)
+ }
+
+ private fun makeOnLongClickListener(listener: (StreamInfo) -> Unit): OnLongClickListener {
+ return OnLongClickListener {
+ currentInfo?.takeIf { !isLoading.get() }?.let(listener) != null
+ }
+ }
+
+ private fun openChannel(subChannelUrl: String?, subChannelName: String, serviceId: Int) {
+ try {
+ NavigationHelper.openChannelFragment(getFM(), serviceId, subChannelUrl, subChannelName)
+ } catch (e: Exception) {
+ showUiErrorSnackbar(this, "Opening channel fragment", e)
+ }
+ }
+
+ private fun toggleTitleAndSecondaryControls() {
+ if (binding.detailSecondaryControlPanel.isGone) {
+ binding.detailVideoTitleView.setMaxLines(10)
+ binding.detailToggleSecondaryControlsView
+ .animateRotation(VideoPlayerUi.DEFAULT_CONTROLS_DURATION, 180)
+ binding.detailSecondaryControlPanel.visibility = View.VISIBLE
+ } else {
+ binding.detailVideoTitleView.setMaxLines(1)
+ binding.detailToggleSecondaryControlsView
+ .animateRotation(VideoPlayerUi.DEFAULT_CONTROLS_DURATION, 0)
+ binding.detailSecondaryControlPanel.visibility = View.GONE
+ }
+ // view pager height has changed, update the tab layout
+ updateTabLayoutVisibility()
+ }
+
+ /*//////////////////////////////////////////////////////////////////////////
+ // Init
+ ////////////////////////////////////////////////////////////////////////// */
+ // called from onViewCreated in {@link BaseFragment#onViewCreated}
+ override fun initViews(rootView: View?, savedInstanceState: Bundle?) {
+ super.initViews(rootView, savedInstanceState)
+
+ pageAdapter = TabAdapter(getChildFragmentManager())
+ binding.viewPager.setAdapter(pageAdapter)
+ binding.tabLayout.setupWithViewPager(binding.viewPager)
+
+ binding.detailThumbnailRootLayout.requestFocus()
+
+ binding.detailControlsPlayWithKodi.isVisible =
+ KoreUtils.shouldShowPlayWithKodi(requireContext(), serviceId)
+ binding.detailControlsCrashThePlayer.isVisible =
+ DEBUG && PreferenceManager.getDefaultSharedPreferences(requireContext())
+ .getBoolean(getString(R.string.show_crash_the_player_key), false)
+
+ accommodateForTvAndDesktopMode()
+ }
+
+ @SuppressLint("ClickableViewAccessibility")
+ override fun initListeners() {
+ super.initListeners()
+
+ setOnClickListeners()
+ setOnLongClickListeners()
+
+ val controlsTouchListener = OnTouchListener { view, motionEvent ->
+ if (motionEvent.action == MotionEvent.ACTION_DOWN &&
+ PlayButtonHelper.shouldShowHoldToAppendTip(activity)
+ ) {
+ binding.touchAppendDetail.animate(true, 250, AnimationType.ALPHA, 0) {
+ binding.touchAppendDetail.animate(false, 1500, AnimationType.ALPHA, 1000)
+ }
+ }
+ false
+ }
+ binding.detailControlsBackground.setOnTouchListener(controlsTouchListener)
+ binding.detailControlsPopup.setOnTouchListener(controlsTouchListener)
+
+ binding.appBarLayout.addOnOffsetChangedListener { layout, verticalOffset ->
+ // prevent useless updates to tab layout visibility if nothing changed
+ if (verticalOffset != lastAppBarVerticalOffset) {
+ lastAppBarVerticalOffset = verticalOffset
+ // the view was scrolled
+ updateTabLayoutVisibility()
+ }
+ }
+
+ setupBottomPlayer()
+ if (!PlayerHolder.isBound) {
+ setHeightThumbnail()
+ } else {
+ PlayerHolder.startService(false, this)
+ }
+ }
+
+ override fun onKeyDown(keyCode: Int): Boolean {
+ return player?.UIs()?.get(VideoPlayerUi::class)?.onKeyDown(keyCode) == true
+ }
+
+ override fun onBackPressed(): Boolean {
+ if (DEBUG) {
+ Log.d(TAG, "onBackPressed() called")
+ }
+
+ // If we are in fullscreen mode just exit from it via first back press
+ if (this.isFullscreen) {
+ if (!DeviceUtils.isTablet(activity)) {
+ player!!.pause()
+ }
+ restoreDefaultOrientation()
+ setAutoPlay(false)
+ return true
+ }
+
+ // If we have something in history of played items we replay it here
+ if (player?.videoPlayerSelected() == true && player?.playQueue?.previous() == true) {
+ return true // no code here, as previous() was used in the if
+ }
+
+ // That means that we are on the start of the stack,
+ if (stack.size <= 1) {
+ restoreDefaultOrientation()
+ return false // let MainActivity handle the onBack (e.g. to minimize the mini player)
+ }
+
+ // Remove top
+ stack.pop()
+ // Get stack item from the new top
+ setupFromHistoryItem(stack.peek()!!)
+
+ return true
+ }
+
+ private fun setupFromHistoryItem(item: StackItem) {
+ setAutoPlay(false)
+ hideMainPlayerOnLoadingNewStream()
+
+ setInitialData(item.serviceId, item.url, item.title ?: "", item.playQueue)
+ startLoading(false)
+
+ // Maybe an item was deleted in background activity
+ if (item.playQueue.item == null) {
+ return
+ }
+
+ val playQueueItem = item.playQueue.item
+ // Update title, url, uploader from the last item in the stack (it's current now)
+ if (playQueueItem != null && playerIsStopped) {
+ updateOverlayData(playQueueItem.title, playQueueItem.uploader, playQueueItem.thumbnails)
+ }
+ }
+
+ /*//////////////////////////////////////////////////////////////////////////
+ // Info loading and handling
+ ////////////////////////////////////////////////////////////////////////// */
+ override fun doInitialLoadLogic() {
+ if (wasCleared()) {
+ return
+ }
+
+ when (val info = currentInfo) {
+ null -> prepareAndLoadInfo()
+ else -> prepareAndHandleInfoIfNeededAfterDelay(info, false, 50)
+ }
+ }
+
+ fun selectAndLoadVideo(
+ newServiceId: Int,
+ newUrl: String,
+ newTitle: String,
+ newQueue: PlayQueue?
+ ) {
+ if (newQueue != null && playQueue?.item?.url != newUrl) {
+ // Preloading can be disabled since playback is surely being replaced.
+ player?.disablePreloadingOfCurrentTrack()
+ }
+
+ setInitialData(newServiceId, newUrl, newTitle, newQueue)
+ startLoading(false, true)
+ }
+
+ private fun prepareAndHandleInfoIfNeededAfterDelay(
+ info: StreamInfo,
+ scrollToTop: Boolean,
+ delay: Long
+ ) {
+ Handler(Looper.getMainLooper()).postDelayed(delay) {
+ if (activity == null) {
+ return@postDelayed
+ }
+ // Data can already be drawn, don't spend time twice
+ if (info.name == binding.detailVideoTitleView.getText().toString()) {
+ return@postDelayed
+ }
+ prepareAndHandleInfo(info, scrollToTop)
+ }
+ }
+
+ private fun prepareAndHandleInfo(info: StreamInfo, scrollToTop: Boolean) {
+ if (DEBUG) {
+ Log.d(TAG, "prepareAndHandleInfo(info=[$info], scrollToTop=[$scrollToTop]) called")
+ }
+
+ showLoading()
+ initTabs()
+
+ if (scrollToTop) {
+ scrollToTop()
+ }
+ handleResult(info)
+ showContent()
+ }
+
+ private fun prepareAndLoadInfo() {
+ scrollToTop()
+ startLoading(false)
+ }
+
+ public override fun startLoading(forceLoad: Boolean) {
+ startLoading(forceLoad, null)
+ }
+
+ private fun startLoading(forceLoad: Boolean, addToBackStack: Boolean?) {
+ super.startLoading(forceLoad)
+
+ initTabs()
+ currentInfo = null
+ currentWorker?.dispose()
+
+ runWorker(forceLoad, addToBackStack ?: stack.isEmpty())
+ }
+
+ private fun runWorker(forceLoad: Boolean, addToBackStack: Boolean) {
+ val prefs = PreferenceManager.getDefaultSharedPreferences(activity)
+ currentWorker = ExtractorHelper.getStreamInfo(serviceId, url, forceLoad)
+ .subscribeOn(Schedulers.io())
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe(
+ { result ->
+ isLoading.set(false)
+ hideMainPlayerOnLoadingNewStream()
+ if (result.ageLimit != StreamExtractor.NO_AGE_LIMIT &&
+ !prefs.getBoolean(getString(R.string.show_age_restricted_content), false)
+ ) {
+ hideAgeRestrictedContent()
+ } else {
+ handleResult(result)
+ showContent()
+ if (addToBackStack) {
+ if (playQueue == null) {
+ playQueue = SinglePlayQueue(result)
+ }
+ if (stack.peek()?.playQueue != playQueue) { // also if stack empty (!)
+ stack.push(StackItem(serviceId, url, title, playQueue))
+ }
+ }
+
+ if (this.isAutoplayEnabled) {
+ openVideoPlayerAutoFullscreen()
+ }
+ }
+ },
+ { throwable ->
+ showError(
+ ErrorInfo(throwable, UserAction.REQUESTED_STREAM, url ?: "no url", serviceId)
+ )
+ }
+ )
+ }
+
+ /*//////////////////////////////////////////////////////////////////////////
+ // Tabs
+ ////////////////////////////////////////////////////////////////////////// */
+ private fun initTabs() {
+ pageAdapter.getItemTitle(binding.viewPager.currentItem)
+ ?.let { tag -> selectedTabTag = tag }
+
+ pageAdapter.clearAllItems()
+ tabIcons.clear()
+ tabContentDescriptions.clear()
+
+ if (shouldShowComments()) {
+ pageAdapter.addFragment(getInstance(serviceId, url), COMMENTS_TAB_TAG)
+ tabIcons.add(R.drawable.ic_comment)
+ tabContentDescriptions.add(R.string.comments_tab_description)
+ }
+
+ if (showRelatedItems && binding.relatedItemsLayout == null) {
+ // temp empty fragment. will be updated in handleResult
+ pageAdapter.addFragment(EmptyFragment.newInstance(false), RELATED_TAB_TAG)
+ tabIcons.add(R.drawable.ic_art_track)
+ tabContentDescriptions.add(R.string.related_items_tab_description)
+ }
+
+ if (showDescription) {
+ // temp empty fragment. will be updated in handleResult
+ pageAdapter.addFragment(EmptyFragment.newInstance(false), DESCRIPTION_TAB_TAG)
+ tabIcons.add(R.drawable.ic_description)
+ tabContentDescriptions.add(R.string.description_tab_description)
+ }
+
+ if (pageAdapter.count == 0) {
+ pageAdapter.addFragment(EmptyFragment.newInstance(true), EMPTY_TAB_TAG)
+ }
+ pageAdapter.notifyDataSetUpdate()
+
+ if (pageAdapter.count >= 2) {
+ val position = pageAdapter.getItemPositionByTitle(selectedTabTag)
+ if (position != -1) {
+ binding.viewPager.setCurrentItem(position)
+ }
+ updateTabIconsAndContentDescriptions()
+ }
+ // the page adapter now contains tabs: show the tab layout
+ updateTabLayoutVisibility()
+ }
+
+ /**
+ * To be called whenever [.pageAdapter] is modified, since that triggers a refresh in
+ * [FragmentVideoDetailBinding.tabLayout] resetting all tab's icons and content
+ * descriptions. This reads icons from [.tabIcons] and content descriptions from
+ * [.tabContentDescriptions], which are all set in [.initTabs].
+ */
+ private fun updateTabIconsAndContentDescriptions() {
+ for (i in tabIcons.indices) {
+ val tab = binding.tabLayout.getTabAt(i)
+ if (tab != null) {
+ tab.setIcon(tabIcons[i])
+ tab.setContentDescription(tabContentDescriptions[i])
+ }
+ }
+ }
+
+ private fun updateTabs(info: StreamInfo) {
+ if (showRelatedItems) {
+ when (val relatedItemsLayout = binding.relatedItemsLayout) {
+ null -> pageAdapter.updateItem(RELATED_TAB_TAG, getInstance(info)) // phone
+ else -> { // tablet + TV
+ getChildFragmentManager().beginTransaction()
+ .replace(R.id.relatedItemsLayout, getInstance(info))
+ .commitAllowingStateLoss()
+ relatedItemsLayout.isVisible = !this.isFullscreen
+ }
+ }
+ }
+
+ if (showDescription) {
+ pageAdapter.updateItem(DESCRIPTION_TAB_TAG, DescriptionFragment(info))
+ }
+
+ binding.viewPager.visibility = View.VISIBLE
+ // make sure the tab layout is visible
+ updateTabLayoutVisibility()
+ pageAdapter.notifyDataSetUpdate()
+ updateTabIconsAndContentDescriptions()
+ }
+
+ private fun shouldShowComments(): Boolean {
+ return showComments && try {
+ NewPipe.getService(serviceId).serviceInfo.mediaCapabilities
+ .contains(MediaCapability.COMMENTS)
+ } catch (_: ExtractionException) {
+ false
+ }
+ }
+
+ fun updateTabLayoutVisibility() {
+ if (nullableBinding == null) {
+ // If binding is null we do not need to and should not do anything with its object(s)
+ return
+ }
+
+ if (pageAdapter.count < 2 || binding.viewPager.visibility != View.VISIBLE) {
+ // hide tab layout if there is only one tab or if the view pager is also hidden
+ binding.tabLayout.visibility = View.GONE
+ } else {
+ // call `post()` to be sure `viewPager.getHitRect()`
+ // is up to date and not being currently recomputed
+ binding.tabLayout.post {
+ getActivity()?.let { activity ->
+ val pagerHitRect = Rect()
+ binding.viewPager.getHitRect(pagerHitRect)
+
+ val height = DeviceUtils.getWindowHeight(activity.windowManager)
+ val viewPagerVisibleHeight = height - pagerHitRect.top
+ // see TabLayout.DEFAULT_HEIGHT, which is equal to 48dp
+ val tabLayoutHeight = TypedValue.applyDimension(
+ TypedValue.COMPLEX_UNIT_DIP, 48f, resources.displayMetrics
+ )
+
+ if (viewPagerVisibleHeight > tabLayoutHeight * 2) {
+ // no translation at all when viewPagerVisibleHeight > tabLayout.height * 3
+ binding.tabLayout.translationY =
+ max(0.0f, tabLayoutHeight * 3 - viewPagerVisibleHeight)
+ binding.tabLayout.visibility = View.VISIBLE
+ } else {
+ // view pager is not visible enough
+ binding.tabLayout.visibility = View.GONE
+ }
+ }
+ }
+ }
+ }
+
+ fun scrollToTop() {
+ binding.appBarLayout.setExpanded(true, true)
+ // notify tab layout of scrolling
+ updateTabLayoutVisibility()
+ }
+
+ /*//////////////////////////////////////////////////////////////////////////
+ // Play Utils
+ ////////////////////////////////////////////////////////////////////////// */
+ private fun toggleFullscreenIfInFullscreenMode() {
+ // If a user watched video inside fullscreen mode and than chose another player
+ // return to non-fullscreen mode
+ player?.UIs()?.get(MainPlayerUi::class)?.let {
+ if (it.isFullscreen) {
+ it.toggleFullscreen()
+ }
+ }
+ }
+
+ private fun openBackgroundPlayer(append: Boolean) {
+ val useExternalAudioPlayer = PreferenceManager
+ .getDefaultSharedPreferences(activity)
+ .getBoolean(activity.getString(R.string.use_external_audio_player_key), false)
+
+ toggleFullscreenIfInFullscreenMode()
+
+ // FIXME Workaround #7427
+ player?.setRecovery()
+
+ if (useExternalAudioPlayer) {
+ showExternalAudioPlaybackDialog()
+ } else {
+ openNormalBackgroundPlayer(append)
+ }
+ }
+
+ private fun openPopupPlayer(append: Boolean) {
+ if (!PermissionHelper.isPopupEnabledElseAsk(activity)) {
+ return
+ }
+
+ // See UI changes while remote playQueue changes
+ if (player == null) {
+ PlayerHolder.startService(false, this)
+ } else {
+ // FIXME Workaround #7427
+ player?.setRecovery()
+ }
+
+ toggleFullscreenIfInFullscreenMode()
+
+ val queue = setupPlayQueueForIntent(append)
+ if (append) { // resumePlayback: false
+ NavigationHelper.enqueueOnPlayer(activity, queue, PlayerType.POPUP)
+ } else {
+ replaceQueueIfUserConfirms { NavigationHelper.playOnPopupPlayer(activity, queue, true) }
+ }
+ }
+
+ /**
+ * Opens the video player, in fullscreen if needed. In order to open fullscreen, the activity
+ * is toggled to landscape orientation (which will then cause fullscreen mode).
+ *
+ * @param directlyFullscreenIfApplicable whether to open fullscreen if we are not already
+ * in landscape and screen orientation is locked
+ */
+ fun openVideoPlayer(directlyFullscreenIfApplicable: Boolean) {
+ if (directlyFullscreenIfApplicable &&
+ !DeviceUtils.isLandscape(requireContext()) &&
+ PlayerHelper.globalScreenOrientationLocked(requireContext())
+ ) {
+ // Make sure the bottom sheet turns out expanded. When this code kicks in the bottom
+ // sheet could not have fully expanded yet, and thus be in the STATE_SETTLING state.
+ // When the activity is rotated, and its state is saved and then restored, the bottom
+ // sheet would forget what it was doing, since even if STATE_SETTLING is restored, it
+ // doesn't tell which state it was settling to, and thus the bottom sheet settles to
+ // STATE_COLLAPSED. This can be solved by manually setting the state that will be
+ // restored (i.e. bottomSheetState) to STATE_EXPANDED.
+ updateBottomSheetState(BottomSheetBehavior.STATE_EXPANDED)
+ // toggle landscape in order to open directly in fullscreen
+ onScreenRotationButtonClicked()
+ }
+
+ if (PreferenceManager.getDefaultSharedPreferences(activity)
+ .getBoolean(this.getString(R.string.use_external_video_player_key), false)
+ ) {
+ showExternalVideoPlaybackDialog()
+ } else {
+ replaceQueueIfUserConfirms { this.openMainPlayer() }
+ }
+ }
+
+ /**
+ * If the option to start directly fullscreen is enabled, calls
+ * [.openVideoPlayer] with `directlyFullscreenIfApplicable = true`, so that
+ * if the user is not already in landscape and he has screen orientation locked the activity
+ * rotates and fullscreen starts. Otherwise, if the option to start directly fullscreen is
+ * disabled, calls [.openVideoPlayer] with `directlyFullscreenIfApplicable
+ * = false`, hence preventing it from going directly fullscreen.
+ */
+ fun openVideoPlayerAutoFullscreen() {
+ openVideoPlayer(PlayerHelper.isStartMainPlayerFullscreenEnabled(requireContext()))
+ }
+
+ private fun openNormalBackgroundPlayer(append: Boolean) {
+ // See UI changes while remote playQueue changes
+ if (player == null) {
+ PlayerHolder.startService(false, this)
+ }
+
+ val queue = setupPlayQueueForIntent(append)
+ if (append) {
+ NavigationHelper.enqueueOnPlayer(activity, queue, PlayerType.AUDIO)
+ } else {
+ replaceQueueIfUserConfirms {
+ NavigationHelper.playOnBackgroundPlayer(activity, queue, true)
+ }
+ }
+ }
+
+ private fun openMainPlayer() {
+ if (playerService == null) {
+ PlayerHolder.startService(autoPlayEnabled, this)
+ return
+ }
+ if (currentInfo == null) {
+ return
+ }
+
+ val queue = setupPlayQueueForIntent(false)
+ tryAddVideoPlayerView()
+
+ val playerIntent = NavigationHelper.getPlayerIntent(
+ requireContext(), PlayerService::class.java, queue, true, autoPlayEnabled
+ )
+ ContextCompat.startForegroundService(activity, playerIntent)
+ }
+
+ /**
+ * When the video detail fragment is already showing details for a video and the user opens a
+ * new one, the video detail fragment changes all of its old data to the new stream, so if there
+ * is a video player currently open it should be hidden. This method does exactly that. If
+ * autoplay is enabled, the underlying player is not stopped completely, since it is going to
+ * be reused in a few milliseconds and the flickering would be annoying.
+ */
+ private fun hideMainPlayerOnLoadingNewStream() {
+ val root = this.root
+ if (root == null || playerService == null || player?.videoPlayerSelected() != true) {
+ return
+ }
+
+ removeVideoPlayerView()
+ if (this.isAutoplayEnabled) {
+ playerService?.stopForImmediateReusing()
+ root.visibility = View.GONE
+ } else {
+ PlayerHolder.stopService()
+ }
+ }
+
+ private fun setupPlayQueueForIntent(append: Boolean): PlayQueue {
+ if (append) {
+ return SinglePlayQueue(currentInfo)
+ }
+
+ var queue = playQueue
+ // Size can be 0 because queue removes bad stream automatically when error occurs
+ if (queue == null || queue.isEmpty) {
+ queue = SinglePlayQueue(currentInfo)
+ }
+
+ return queue
+ }
+
+ /*//////////////////////////////////////////////////////////////////////////
+ // Utils
+ ////////////////////////////////////////////////////////////////////////// */
+ fun setAutoPlay(autoPlay: Boolean) {
+ this.autoPlayEnabled = autoPlay
+ }
+
+ private fun startOnExternalPlayer(
+ context: Context,
+ info: StreamInfo,
+ selectedStream: Stream
+ ) {
+ NavigationHelper.playOnExternalPlayer(
+ context, info.name, info.subChannelName, selectedStream
+ )
+
+ val recordManager = HistoryRecordManager(requireContext())
+ disposables.add(
+ recordManager.onViewed(info)
+ .subscribe(
+ { /* successful */ },
+ { throwable -> Log.e(TAG, "Register view failure: ", throwable) }
+ )
+ )
+ }
+
+ private val isExternalPlayerEnabled: Boolean
+ get() = PreferenceManager.getDefaultSharedPreferences(requireContext())
+ .getBoolean(getString(R.string.use_external_video_player_key), false)
+
+ @Suppress("NullableBooleanElvis") // ?: true is clearer than != false
+ private val isAutoplayEnabled: Boolean
+ // This method overrides default behaviour when setAutoPlay() is called.
+ get() = autoPlayEnabled &&
+ !this.isExternalPlayerEnabled &&
+ (player?.videoPlayerSelected() ?: true) && // if no player present, consider it video
+ bottomSheetState != BottomSheetBehavior.STATE_HIDDEN &&
+ PlayerHelper.isAutoplayAllowedByUser(requireContext())
+
+ private fun tryAddVideoPlayerView() {
+ if (player != null && view != null) {
+ // Setup the surface view height, so that it fits the video correctly; this is done also
+ // here, and not only in the Handler, to avoid a choppy fullscreen rotation animation.
+ setHeightThumbnail()
+ }
+
+ // do all the null checks in the posted lambda, too, since the player, the binding and the
+ // view could be set or unset before the lambda gets executed on the next main thread cycle
+ Handler(Looper.getMainLooper()).post {
+ if (player == null || view == null) {
+ return@post
+ }
+
+ // setup the surface view height, so that it fits the video correctly
+ setHeightThumbnail()
+
+ player?.UIs()?.get(MainPlayerUi::class)?.let { playerUi ->
+ // sometimes binding would be null here, even though getView() != null above u.u
+ nullableBinding?.let { b ->
+ // prevent from re-adding a view multiple times
+ playerUi.removeViewFromParent()
+ b.playerPlaceholder.addView(playerUi.getBinding().getRoot())
+ playerUi.setupVideoSurfaceIfNeeded()
+ }
+ }
+ }
+ }
+
+ private fun removeVideoPlayerView() {
+ makeDefaultHeightForVideoPlaceholder()
+ player?.UIs()?.get(VideoPlayerUi::class)?.removeViewFromParent()
+ }
+
+ private fun makeDefaultHeightForVideoPlaceholder() {
+ if (view == null) {
+ return
+ }
+
+ binding.playerPlaceholder.layoutParams.height = FrameLayout.LayoutParams.MATCH_PARENT
+ binding.playerPlaceholder.requestLayout()
+ }
+
+ private val preDrawListener: OnPreDrawListener = OnPreDrawListener {
+ view?.let { view ->
+ val decorView = if (DeviceUtils.isInMultiWindow(activity))
+ view
+ else
+ activity.window.decorView
+ setHeightThumbnail(decorView.height, resources.displayMetrics)
+ view.getViewTreeObserver().removeOnPreDrawListener(preDrawListener)
+ }
+ return@OnPreDrawListener false
+ }
+
+ /**
+ * Method which controls the size of thumbnail and the size of main player inside
+ * a layout with thumbnail. It decides what height the player should have in both
+ * screen orientations. It knows about multiWindow feature
+ * and about videos with aspectRatio ZOOM (the height for them will be a bit higher,
+ * [.MAX_PLAYER_HEIGHT])
+ */
+ private fun setHeightThumbnail() {
+ val metrics = resources.displayMetrics
+ requireView().getViewTreeObserver().removeOnPreDrawListener(preDrawListener)
+
+ if (this.isFullscreen) {
+ val height = (
+ if (DeviceUtils.isInMultiWindow(activity))
+ requireView()
+ else
+ activity.window.decorView
+ ).height
+ // Height is zero when the view is not yet displayed like after orientation change
+ if (height != 0) {
+ setHeightThumbnail(height, metrics)
+ } else {
+ requireView().getViewTreeObserver().addOnPreDrawListener(preDrawListener)
+ }
+ } else {
+ val isPortrait = metrics.heightPixels > metrics.widthPixels
+ val height = (
+ if (isPortrait)
+ metrics.widthPixels / (16.0f / 9.0f)
+ else
+ metrics.heightPixels / 2.0f
+ ).toInt()
+ setHeightThumbnail(height, metrics)
+ }
+ }
+
+ private fun setHeightThumbnail(newHeight: Int, metrics: DisplayMetrics) {
+ binding.detailThumbnailImageView.setLayoutParams(
+ FrameLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT, newHeight)
+ )
+ binding.detailThumbnailImageView.setMinimumHeight(newHeight)
+ player?.UIs()?.get(VideoPlayerUi::class)?.let {
+ val maxHeight = (metrics.heightPixels * MAX_PLAYER_HEIGHT).toInt()
+ it.binding.surfaceView.setHeights(
+ newHeight,
+ if (it.isFullscreen) newHeight else maxHeight
+ )
+ }
+ }
+
+ private fun showContent() {
+ binding.detailContentRootHiding.visibility = View.VISIBLE
+ }
+
+ private fun setInitialData(
+ newServiceId: Int,
+ newUrl: String?,
+ newTitle: String,
+ newPlayQueue: PlayQueue?
+ ) {
+ this.serviceId = newServiceId
+ this.url = newUrl
+ this.title = newTitle
+ this.playQueue = newPlayQueue
+ }
+
+ private fun setErrorImage() {
+ if (nullableBinding == null || activity == null) {
+ return
+ }
+
+ binding.detailThumbnailImageView.setImageDrawable(
+ AppCompatResources.getDrawable(requireContext(), R.drawable.not_available_monkey)
+ )
+ binding.detailThumbnailImageView.animate(false, 0, AnimationType.ALPHA, 0) {
+ binding.detailThumbnailImageView.animate(true, 500)
+ }
+ }
+
+ override fun handleError() {
+ super.handleError()
+ setErrorImage()
+
+ // hide related streams for tablets
+ binding.relatedItemsLayout?.visibility = View.INVISIBLE
+
+ // hide comments / related streams / description tabs
+ binding.viewPager.visibility = View.GONE
+ binding.tabLayout.visibility = View.GONE
+ }
+
+ private fun hideAgeRestrictedContent() {
+ showTextError(
+ getString(
+ R.string.restricted_video,
+ getString(R.string.show_age_restricted_content_title)
+ )
+ )
+ }
+
+ private fun setupBroadcastReceiver() {
+ broadcastReceiver = object : BroadcastReceiver() {
+ override fun onReceive(context: Context?, intent: Intent) {
+ when (intent.action) {
+ ACTION_SHOW_MAIN_PLAYER -> bottomSheetBehavior.setState(BottomSheetBehavior.STATE_EXPANDED)
+ ACTION_HIDE_MAIN_PLAYER -> bottomSheetBehavior.setState(BottomSheetBehavior.STATE_HIDDEN)
+ ACTION_PLAYER_STARTED -> {
+ // If the state is not hidden we don't need to show the mini player
+ if (bottomSheetBehavior.getState() == BottomSheetBehavior.STATE_HIDDEN) {
+ bottomSheetBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED)
+ }
+ // Rebound to the service if it was closed via notification or mini player
+ if (!PlayerHolder.isBound) {
+ PlayerHolder.startService(false, this@VideoDetailFragment)
+ }
+ }
+ }
+ }
+ }
+ val intentFilter = IntentFilter()
+ intentFilter.addAction(ACTION_SHOW_MAIN_PLAYER)
+ intentFilter.addAction(ACTION_HIDE_MAIN_PLAYER)
+ intentFilter.addAction(ACTION_PLAYER_STARTED)
+ ContextCompat.registerReceiver(activity, broadcastReceiver, intentFilter, ContextCompat.RECEIVER_EXPORTED)
+ }
+
+ /*//////////////////////////////////////////////////////////////////////////
+ // Orientation listener
+ ////////////////////////////////////////////////////////////////////////// */
+ private fun restoreDefaultOrientation() {
+ if (player?.videoPlayerSelected() == true) {
+ toggleFullscreenIfInFullscreenMode()
+ }
+
+ // This will show systemUI and pause the player.
+ // User can tap on Play button and video will be in fullscreen mode again
+ // Note for tablet: trying to avoid orientation changes since it's not easy
+ // to physically rotate the tablet every time
+ if (activity != null && !DeviceUtils.isTablet(activity)) {
+ activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED)
+ }
+ }
+
+ /*//////////////////////////////////////////////////////////////////////////
+ // Contract
+ ////////////////////////////////////////////////////////////////////////// */
+ override fun showLoading() {
+ super.showLoading()
+
+ // if data is already cached, transition from VISIBLE -> INVISIBLE -> VISIBLE is not required
+ if (!ExtractorHelper.isCached(serviceId, url!!, InfoCache.Type.STREAM)) {
+ binding.detailContentRootHiding.visibility = View.INVISIBLE
+ }
+
+ binding.detailThumbnailPlayButton.animate(false, 50)
+ binding.detailDurationView.animate(false, 100)
+ binding.detailPositionView.visibility = View.GONE
+ binding.positionView.visibility = View.GONE
+
+ binding.detailVideoTitleView.text = title
+ binding.detailVideoTitleView.setMaxLines(1)
+ binding.detailVideoTitleView.animate(true, 0)
+
+ binding.detailToggleSecondaryControlsView.visibility = View.GONE
+ binding.detailTitleRootLayout.isClickable = false
+ binding.detailSecondaryControlPanel.visibility = View.GONE
+
+ binding.relatedItemsLayout?.isVisible = showRelatedItems && !this.isFullscreen
+
+ CoilUtils.dispose(binding.detailThumbnailImageView)
+ CoilUtils.dispose(binding.detailSubChannelThumbnailView)
+ CoilUtils.dispose(binding.overlayThumbnail)
+ CoilUtils.dispose(binding.detailUploaderThumbnailView)
+
+ binding.detailThumbnailImageView.setImageBitmap(null)
+ binding.detailSubChannelThumbnailView.setImageBitmap(null)
+ }
+
+ override fun handleResult(info: StreamInfo) {
+ super.handleResult(info)
+
+ currentInfo = info
+ setInitialData(info.serviceId, info.originalUrl, info.name, playQueue)
+
+ updateTabs(info)
+
+ binding.detailThumbnailPlayButton.animate(true, 200)
+ binding.detailVideoTitleView.text = title
+
+ binding.detailSubChannelThumbnailView.visibility = View.GONE
+
+ if (info.subChannelName.isEmpty()) {
+ displayUploaderAsSubChannel(info)
+ } else {
+ displayBothUploaderAndSubChannel(info)
+ }
+
+ if (info.viewCount >= 0) {
+ binding.detailViewCountView.text =
+ if (info.streamType == StreamType.AUDIO_LIVE_STREAM) {
+ Localization.listeningCount(activity, info.viewCount)
+ } else if (info.streamType == StreamType.LIVE_STREAM) {
+ Localization.localizeWatchingCount(activity, info.viewCount)
+ } else {
+ Localization.localizeViewCount(activity, info.viewCount)
+ }
+ binding.detailViewCountView.visibility = View.VISIBLE
+ } else {
+ binding.detailViewCountView.visibility = View.GONE
+ }
+
+ if (info.dislikeCount == -1L && info.likeCount == -1L) {
+ binding.detailThumbsDownImgView.visibility = View.VISIBLE
+ binding.detailThumbsUpImgView.visibility = View.VISIBLE
+ binding.detailThumbsUpCountView.visibility = View.GONE
+ binding.detailThumbsDownCountView.visibility = View.GONE
+ binding.detailThumbsDisabledView.visibility = View.VISIBLE
+ } else {
+ if (info.dislikeCount >= 0) {
+ binding.detailThumbsDownCountView.text =
+ Localization.shortCount(activity, info.dislikeCount)
+ binding.detailThumbsDownCountView.visibility = View.VISIBLE
+ binding.detailThumbsDownImgView.visibility = View.VISIBLE
+ } else {
+ binding.detailThumbsDownCountView.visibility = View.GONE
+ binding.detailThumbsDownImgView.visibility = View.GONE
+ }
+
+ if (info.likeCount >= 0) {
+ binding.detailThumbsUpCountView.text =
+ Localization.shortCount(activity, info.likeCount)
+ binding.detailThumbsUpCountView.visibility = View.VISIBLE
+ binding.detailThumbsUpImgView.visibility = View.VISIBLE
+ } else {
+ binding.detailThumbsUpCountView.visibility = View.GONE
+ binding.detailThumbsUpImgView.visibility = View.GONE
+ }
+ binding.detailThumbsDisabledView.visibility = View.GONE
+ }
+
+ if (info.duration > 0) {
+ binding.detailDurationView.text = Localization.getDurationString(info.duration)
+ binding.detailDurationView.setBackgroundColor(
+ ContextCompat.getColor(activity, R.color.duration_background_color)
+ )
+ binding.detailDurationView.animate(true, 100)
+ } else if (info.streamType == StreamType.LIVE_STREAM) {
+ binding.detailDurationView.setText(R.string.duration_live)
+ binding.detailDurationView.setBackgroundColor(
+ ContextCompat.getColor(activity, R.color.live_duration_background_color)
+ )
+ binding.detailDurationView.animate(true, 100)
+ } else {
+ binding.detailDurationView.visibility = View.GONE
+ }
+
+ binding.detailTitleRootLayout.isClickable = true
+ binding.detailToggleSecondaryControlsView.rotation = 0f
+ binding.detailToggleSecondaryControlsView.visibility = View.VISIBLE
+ binding.detailSecondaryControlPanel.visibility = View.GONE
+
+ checkUpdateProgressInfo(info)
+ CoilHelper.loadDetailsThumbnail(binding.detailThumbnailImageView, info.thumbnails)
+ ExtractorHelper.showMetaInfoInTextView(
+ info.metaInfo, binding.detailMetaInfoTextView,
+ binding.detailMetaInfoSeparator, disposables
+ )
+
+ if (playerIsStopped) {
+ updateOverlayData(info.name, info.uploaderName, info.thumbnails)
+ }
+
+ if (!info.errors.isEmpty()) {
+ // Bandcamp fan pages are not yet supported and thus a ContentNotAvailableException is
+ // thrown. This is not an error and thus should not be shown to the user.
+ info.errors.removeIf {
+ it is ContentNotSupportedException && "Fan pages are not supported" == it.message
+ }
+
+ if (!info.errors.isEmpty()) {
+ showSnackBarError(
+ ErrorInfo(info.errors, UserAction.REQUESTED_STREAM, info.url, info)
+ )
+ }
+ }
+
+ binding.detailControlsDownload.isVisible = !StreamTypeUtil.isLiveStream(info.streamType)
+
+ val hasAudioStreams = info.videoStreams.isNotEmpty() || info.audioStreams.isNotEmpty()
+ binding.detailControlsBackground.isVisible = hasAudioStreams
+
+ val hasVideoStreams = info.videoStreams.isNotEmpty() || info.videoOnlyStreams.isNotEmpty()
+ binding.detailControlsPopup.isVisible = hasVideoStreams
+ binding.detailThumbnailPlayButton.setImageResource(
+ if (hasVideoStreams) R.drawable.ic_play_arrow_shadow else R.drawable.ic_headset_shadow
+ )
+ }
+
+ private fun displayUploaderAsSubChannel(info: StreamInfo) {
+ binding.detailSubChannelTextView.text = info.uploaderName
+ binding.detailSubChannelTextView.visibility = View.VISIBLE
+ binding.detailSubChannelTextView.setSelected(true)
+
+ if (info.uploaderSubscriberCount > -1) {
+ binding.detailUploaderTextView.text =
+ Localization.shortSubscriberCount(activity, info.uploaderSubscriberCount)
+ binding.detailUploaderTextView.visibility = View.VISIBLE
+ } else {
+ binding.detailUploaderTextView.visibility = View.GONE
+ }
+
+ CoilHelper.loadAvatar(binding.detailSubChannelThumbnailView, info.uploaderAvatars)
+ binding.detailSubChannelThumbnailView.visibility = View.VISIBLE
+ binding.detailUploaderThumbnailView.visibility = View.GONE
+ }
+
+ private fun displayBothUploaderAndSubChannel(info: StreamInfo) {
+ binding.detailSubChannelTextView.text = info.subChannelName
+ binding.detailSubChannelTextView.visibility = View.VISIBLE
+ binding.detailSubChannelTextView.setSelected(true)
+
+ val subText = StringBuilder()
+ if (info.uploaderName.isNotEmpty()) {
+ subText.append(getString(R.string.video_detail_by, info.uploaderName))
+ }
+ if (info.uploaderSubscriberCount > -1) {
+ if (subText.isNotEmpty()) {
+ subText.append(Localization.DOT_SEPARATOR)
+ }
+ subText.append(
+ Localization.shortSubscriberCount(activity, info.uploaderSubscriberCount)
+ )
+ }
+
+ if (subText.isEmpty()) {
+ binding.detailUploaderTextView.visibility = View.GONE
+ } else {
+ binding.detailUploaderTextView.text = subText
+ binding.detailUploaderTextView.visibility = View.VISIBLE
+ binding.detailUploaderTextView.setSelected(true)
+ }
+
+ CoilHelper.loadAvatar(binding.detailSubChannelThumbnailView, info.subChannelAvatars)
+ binding.detailSubChannelThumbnailView.visibility = View.VISIBLE
+ CoilHelper.loadAvatar(binding.detailUploaderThumbnailView, info.uploaderAvatars)
+ binding.detailUploaderThumbnailView.visibility = View.VISIBLE
+ }
+
+ fun openDownloadDialog() {
+ val info = currentInfo ?: return
+
+ try {
+ val downloadDialog = DownloadDialog(activity, info)
+ downloadDialog.show(activity.supportFragmentManager, "downloadDialog")
+ } catch (e: Exception) {
+ showSnackbar(
+ activity,
+ ErrorInfo(e, UserAction.DOWNLOAD_OPEN_DIALOG, "Showing download dialog", info)
+ )
+ }
+ }
+
+ /*//////////////////////////////////////////////////////////////////////////
+ // Stream Results
+ ////////////////////////////////////////////////////////////////////////// */
+ private fun checkUpdateProgressInfo(info: StreamInfo) {
+ positionSubscriber?.dispose()
+ if (!DependentPreferenceHelper.getResumePlaybackEnabled(activity)) {
+ binding.positionView.visibility = View.GONE
+ binding.detailPositionView.visibility = View.GONE
+ return
+ }
+ val recordManager = HistoryRecordManager(requireContext())
+ positionSubscriber = recordManager.loadStreamState(info)
+ .subscribeOn(Schedulers.io())
+ .onErrorComplete()
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe(
+ { state -> updatePlaybackProgress(state.progressMillis, info.duration * 1000) },
+ { throwable -> /* impossible due to the onErrorComplete() */ },
+ { /* onComplete */
+ binding.positionView.visibility = View.GONE
+ binding.detailPositionView.visibility = View.GONE
+ }
+ )
+ }
+
+ private fun updatePlaybackProgress(progress: Long, duration: Long) {
+ if (!DependentPreferenceHelper.getResumePlaybackEnabled(activity)) {
+ return
+ }
+ val progressSeconds = TimeUnit.MILLISECONDS.toSeconds(progress).toInt()
+ val durationSeconds = TimeUnit.MILLISECONDS.toSeconds(duration).toInt()
+ binding.positionView.setMax(durationSeconds)
+ // If the old and the new progress values have a big difference then use animation.
+ // Otherwise don't because it affects CPU
+ if (abs(binding.positionView.progress - progressSeconds) > 2) {
+ binding.positionView.setProgressAnimated(progressSeconds)
+ } else {
+ binding.positionView.progress = progressSeconds
+ }
+ val position = Localization.getDurationString(progressSeconds.toLong())
+ if (position != binding.detailPositionView.getText()) {
+ binding.detailPositionView.text = position
+ }
+ if (binding.positionView.visibility != View.VISIBLE) {
+ binding.positionView.animate(true, 100)
+ binding.detailPositionView.animate(true, 100)
+ }
+ }
+
+ /*//////////////////////////////////////////////////////////////////////////
+ // Player event listener
+ ////////////////////////////////////////////////////////////////////////// */
+ override fun onViewCreated() {
+ tryAddVideoPlayerView()
+ }
+
+ override fun onQueueUpdate(queue: PlayQueue) {
+ playQueue = queue
+ if (DEBUG) {
+ Log.d(
+ TAG,
+ "onQueueUpdate() called with: serviceId = [$serviceId], url = [${
+ url}], name = [$title], playQueue = [$playQueue]"
+ )
+ }
+
+ // Register broadcast receiver to listen to playQueue changes
+ // and hide the overlayPlayQueueButton when the playQueue is empty / destroyed.
+ playQueue?.broadcastReceiver?.subscribe { updateOverlayPlayQueueButtonVisibility() }
+ ?.let { disposables.add(it) }
+
+ // This should be the only place where we push data to stack.
+ // It will allow to have live instance of PlayQueue with actual information about
+ // deleted/added items inside Channel/Playlist queue and makes possible to have
+ // a history of played items
+ if (stack.peek()?.playQueue?.equals(queue) == false) {
+ queue.item?.let { queueItem ->
+ stack.push(StackItem(queueItem.serviceId, queueItem.url, queueItem.title, queue))
+ return@onQueueUpdate
+ } // if queue.item == null continue below
+ }
+
+ // On every MainPlayer service's destroy() playQueue gets disposed and
+ // no longer able to track progress. That's why we update our cached disposed
+ // queue with the new one that is active and have the same history.
+ // Without that the cached playQueue will have an old recovery position
+ findQueueInStack(queue)?.playQueue = queue
+ }
+
+ override fun onPlaybackUpdate(
+ state: Int,
+ repeatMode: Int,
+ shuffled: Boolean,
+ parameters: PlaybackParameters?
+ ) {
+ setOverlayPlayPauseImage(player?.isPlaying == true)
+
+ if (state == Player.STATE_PLAYING && binding.positionView.alpha != 1.0f &&
+ player?.playQueue?.item?.url?.equals(url) == true
+ ) {
+ binding.positionView.animate(true, 100)
+ binding.detailPositionView.animate(true, 100)
+ }
+ }
+
+ override fun onProgressUpdate(
+ currentProgress: Int,
+ duration: Int,
+ bufferPercent: Int
+ ) {
+ // Progress updates are received every second even if media is paused. It's useless until
+ // playing, hence the `player?.isPlaying == true` check.
+ if (player?.isPlaying == true && player?.playQueue?.item?.url?.equals(url) == true) {
+ updatePlaybackProgress(currentProgress.toLong(), duration.toLong())
+ }
+ }
+
+ override fun onMetadataUpdate(info: StreamInfo, queue: PlayQueue) {
+ findQueueInStack(queue)?.let { item ->
+ // When PlayQueue can have multiple streams (PlaylistPlayQueue or ChannelPlayQueue)
+ // every new played stream gives new title and url.
+ // StackItem contains information about first played stream. Let's update it here
+ item.title = info.name
+ item.url = info.url
+ }
+ // They are not equal when user watches something in popup while browsing in fragment and
+ // then changes screen orientation. In that case the fragment will set itself as
+ // a service listener and will receive initial call to onMetadataUpdate()
+ if (queue != playQueue) {
+ return
+ }
+
+ updateOverlayData(info.name, info.uploaderName, info.thumbnails)
+ if (info.url == currentInfo?.url) {
+ return
+ }
+
+ currentInfo = info
+ setInitialData(info.serviceId, info.url, info.name, queue)
+ setAutoPlay(false)
+ // Delay execution just because it freezes the main thread, and while playing
+ // next/previous video you see visual glitches
+ // (when non-vertical video goes after vertical video)
+ prepareAndHandleInfoIfNeededAfterDelay(info, true, 200)
+ }
+
+ override fun onPlayerError(error: PlaybackException?, isCatchableException: Boolean) {
+ if (!isCatchableException) {
+ // Properly exit from fullscreen
+ toggleFullscreenIfInFullscreenMode()
+ hideMainPlayerOnLoadingNewStream()
+ }
+ }
+
+ override fun onServiceStopped() {
+ // the binding could be null at this point, if the app is finishing
+ if (nullableBinding != null) {
+ setOverlayPlayPauseImage(false)
+ currentInfo?.let { updateOverlayData(it.name, it.uploaderName, it.thumbnails) }
+ updateOverlayPlayQueueButtonVisibility()
+ }
+ }
+
+ override fun onFullscreenStateChanged(fullscreen: Boolean) {
+ setupBrightness()
+ if (playerService == null ||
+ player?.UIs()?.get(MainPlayerUi::class) == null ||
+ this.root?.parent == null
+ ) {
+ return
+ }
+
+ if (fullscreen) {
+ hideSystemUiIfNeeded()
+ binding.overlayPlayPauseButton.requestFocus()
+ } else {
+ showSystemUi()
+ }
+
+ binding.relatedItemsLayout?.isVisible = !fullscreen
+ scrollToTop()
+
+ tryAddVideoPlayerView()
+ }
+
+ override fun onScreenRotationButtonClicked() {
+ // In tablet user experience will be better if screen will not be rotated
+ // from landscape to portrait every time.
+ // Just turn on fullscreen mode in landscape orientation
+ // or portrait & unlocked global orientation
+ val isLandscape = DeviceUtils.isLandscape(requireContext())
+ if (DeviceUtils.isTablet(activity) &&
+ (!PlayerHelper.globalScreenOrientationLocked(activity) || isLandscape)
+ ) {
+ player!!.UIs().get(MainPlayerUi::class)?.toggleFullscreen()
+ return
+ }
+
+ val newOrientation = if (isLandscape)
+ ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
+ else
+ ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE
+
+ activity.setRequestedOrientation(newOrientation)
+ }
+
+ /*
+ * Will scroll down to description view after long click on moreOptionsButton
+ * */
+ override fun onMoreOptionsLongClicked() {
+ val params = binding.appBarLayout.layoutParams as CoordinatorLayout.LayoutParams
+ val behavior = params.behavior as AppBarLayout.Behavior
+ val valueAnimator = ValueAnimator.ofInt(0, -binding.playerPlaceholder.height)
+ valueAnimator.addUpdateListener { animation ->
+ behavior.setTopAndBottomOffset(animation.getAnimatedValue() as Int)
+ binding.appBarLayout.requestLayout()
+ }
+ valueAnimator.interpolator = DecelerateInterpolator()
+ valueAnimator.duration = 500
+ valueAnimator.start()
+ }
+
+ /*//////////////////////////////////////////////////////////////////////////
+ // Player related utils
+ ////////////////////////////////////////////////////////////////////////// */
+ private fun showSystemUi() {
+ if (DEBUG) {
+ Log.d(TAG, "showSystemUi() called")
+ }
+
+ if (activity == null) {
+ return
+ }
+
+ // Prevent jumping of the player on devices with cutout
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
+ activity.window.attributes.layoutInDisplayCutoutMode =
+ WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT
+ }
+ activity.window.decorView.systemUiVisibility = 0
+ activity.window.clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN)
+ activity.window.statusBarColor = ThemeHelper.resolveColorFromAttr(
+ requireContext(), android.R.attr.colorPrimary
+ )
+ }
+
+ private fun hideSystemUi() {
+ if (DEBUG) {
+ Log.d(TAG, "hideSystemUi() called")
+ }
+
+ if (activity == null) {
+ return
+ }
+
+ // Prevent jumping of the player on devices with cutout
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
+ activity.window.attributes.layoutInDisplayCutoutMode =
+ WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
+ }
+ var visibility = (
+ View.SYSTEM_UI_FLAG_LAYOUT_STABLE
+ or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
+ or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
+ or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
+ or View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
+ )
+
+ // In multiWindow mode status bar is not transparent for devices with cutout
+ // if I include this flag. So without it is better in this case
+ val isInMultiWindow = DeviceUtils.isInMultiWindow(activity)
+ if (!isInMultiWindow) {
+ visibility = visibility or View.SYSTEM_UI_FLAG_FULLSCREEN
+ }
+ activity.window.decorView.systemUiVisibility = visibility
+
+ if (isInMultiWindow || this.isFullscreen) {
+ activity.window.statusBarColor = Color.TRANSPARENT
+ activity.window.navigationBarColor = Color.TRANSPARENT
+ }
+ activity.window.clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN)
+ }
+
+ // Listener implementation
+ override fun hideSystemUiIfNeeded() {
+ if (this.isFullscreen &&
+ bottomSheetBehavior.getState() == BottomSheetBehavior.STATE_EXPANDED
+ ) {
+ hideSystemUi()
+ }
+ }
+
+ private val isFullscreen: Boolean
+ get() = player?.UIs()?.get(VideoPlayerUi::class)?.isFullscreen == true
+
+ /**
+ * @return true if the player is null, or if the player is nonnull but is stopped.
+ */
+ @Suppress("NullableBooleanElvis") // rewriting as "!= false" creates more confusion
+ private val playerIsStopped
+ get() = player?.isStopped ?: true
+
+ private fun restoreDefaultBrightness() {
+ val lp = activity.window.attributes
+ if (lp.screenBrightness == -1f) {
+ return
+ }
+
+ // Restore the old brightness when fragment.onPause() called or
+ // when a player is in portrait
+ lp.screenBrightness = -1f
+ activity.window.setAttributes(lp)
+ }
+
+ private fun setupBrightness() {
+ if (activity == null) {
+ return
+ }
+
+ val lp = activity.window.attributes
+ if (!this.isFullscreen || bottomSheetState != BottomSheetBehavior.STATE_EXPANDED) {
+ // Apply system brightness when the player is not in fullscreen
+ restoreDefaultBrightness()
+ } else {
+ // Do not restore if user has disabled brightness gesture
+ val brightnessControlKey = getString(R.string.brightness_control_key)
+ if (PlayerHelper.getActionForRightGestureSide(activity) != brightnessControlKey &&
+ PlayerHelper.getActionForLeftGestureSide(activity) != brightnessControlKey
+ ) {
+ return
+ }
+ // Restore already saved brightness level
+ val brightnessLevel = PlayerHelper.getScreenBrightness(activity)
+ if (brightnessLevel == lp.screenBrightness) {
+ return
+ }
+ lp.screenBrightness = brightnessLevel
+ activity.window.setAttributes(lp)
+ }
+ }
+
+ /**
+ * Make changes to the UI to accommodate for better usability on bigger screens such as TVs
+ * or in Android's desktop mode (DeX etc).
+ */
+ private fun accommodateForTvAndDesktopMode() {
+ if (DeviceUtils.isTv(context)) {
+ // remove ripple effects from detail controls
+ val transparent = ContextCompat.getColor(
+ requireContext(),
+ R.color.transparent_background_color
+ )
+ binding.detailControlsPlaylistAppend.setBackgroundColor(transparent)
+ binding.detailControlsBackground.setBackgroundColor(transparent)
+ binding.detailControlsPopup.setBackgroundColor(transparent)
+ binding.detailControlsDownload.setBackgroundColor(transparent)
+ binding.detailControlsShare.setBackgroundColor(transparent)
+ binding.detailControlsOpenInBrowser.setBackgroundColor(transparent)
+ binding.detailControlsPlayWithKodi.setBackgroundColor(transparent)
+ }
+ if (DeviceUtils.isDesktopMode(requireContext())) {
+ // Remove the "hover" overlay (since it is visible on all mouse events and interferes
+ // with the video content being played)
+ binding.detailThumbnailRootLayout.setForeground(null)
+ }
+ }
+
+ private fun checkLandscape() {
+ if ((!player!!.isPlaying && player!!.playQueue !== playQueue) ||
+ player!!.playQueue == null
+ ) {
+ setAutoPlay(true)
+ }
+
+ player!!.UIs().get(MainPlayerUi::class)?.checkLandscape()
+ // Let's give a user time to look at video information page if video is not playing
+ if (PlayerHelper.globalScreenOrientationLocked(activity) && !player!!.isPlaying) {
+ player!!.play()
+ }
+ }
+
+ /*
+ * Means that the player fragment was swiped away via BottomSheetLayout
+ * and is empty but ready for any new actions. See cleanUp()
+ * */
+ private fun wasCleared(): Boolean {
+ return url == null
+ }
+
+ private fun findQueueInStack(queue: PlayQueue): StackItem? {
+ return stack.descendingIterator().asSequence()
+ .firstOrNull { it?.playQueue?.equals(queue) == true }
+ }
+
+ private fun replaceQueueIfUserConfirms(onAllow: Runnable) {
+ // Player will have STATE_IDLE when a user pressed back button
+ if (PlayerHelper.isClearingQueueConfirmationRequired(activity) &&
+ !playerIsStopped && player?.playQueue != playQueue
+ ) {
+ showClearingQueueConfirmation(onAllow)
+ } else {
+ onAllow.run()
+ }
+ }
+
+ private fun showClearingQueueConfirmation(onAllow: Runnable) {
+ AlertDialog.Builder(activity)
+ .setTitle(R.string.clear_queue_confirmation_description)
+ .setNegativeButton(R.string.cancel, null)
+ .setPositiveButton(R.string.ok) { dialog, which ->
+ onAllow.run()
+ dialog?.dismiss()
+ }
+ .show()
+ }
+
+ private fun showExternalVideoPlaybackDialog() {
+ val info = currentInfo ?: return
+
+ val builder = AlertDialog.Builder(activity)
+ builder.setTitle(R.string.select_quality_external_players)
+ builder.setNeutralButton(R.string.open_in_browser) { dialog, which ->
+ ShareUtils.openUrlInBrowser(requireActivity(), url)
+ }
+
+ val videoStreamsForExternalPlayers = ListHelper.getSortedStreamVideosList(
+ activity,
+ ListHelper.getUrlAndNonTorrentStreams(info.videoStreams),
+ ListHelper.getUrlAndNonTorrentStreams(info.videoOnlyStreams),
+ false,
+ false
+ )
+
+ if (videoStreamsForExternalPlayers.isEmpty()) {
+ builder.setMessage(R.string.no_video_streams_available_for_external_players)
+ builder.setPositiveButton(R.string.ok, null)
+ } else {
+ val selectedVideoStreamIndexForExternalPlayers =
+ ListHelper.getDefaultResolutionIndex(activity, videoStreamsForExternalPlayers)
+ val resolutions = videoStreamsForExternalPlayers
+ .map { it.getResolution() as CharSequence }
+ .toTypedArray()
+
+ builder
+ .setSingleChoiceItems(resolutions, selectedVideoStreamIndexForExternalPlayers, null)
+ builder.setNegativeButton(R.string.cancel, null)
+ builder.setPositiveButton(R.string.ok) { dialog, which ->
+ val index = (dialog as AlertDialog).listView.getCheckedItemPosition()
+ // We don't have to manage the index validity because if there is no stream
+ // available for external players, this code will be not executed and if there is
+ // no stream which matches the default resolution, 0 is returned by
+ // ListHelper.getDefaultResolutionIndex.
+ // The index cannot be outside the bounds of the list as its always between 0 and
+ // the list size - 1, .
+ startOnExternalPlayer(activity, info, videoStreamsForExternalPlayers[index])
+ }
+ }
+ builder.show()
+ }
+
+ private fun showExternalAudioPlaybackDialog() {
+ val info = currentInfo ?: return
+
+ val audioStreams = ListHelper.getUrlAndNonTorrentStreams(info.audioStreams)
+ val audioTracks = ListHelper.getFilteredAudioStreams(activity, audioStreams)
+
+ if (audioTracks.isEmpty()) {
+ Toast.makeText(
+ activity, R.string.no_audio_streams_available_for_external_players,
+ Toast.LENGTH_SHORT
+ ).show()
+ } else if (audioTracks.size == 1) {
+ startOnExternalPlayer(activity, info, audioTracks[0])
+ } else {
+ val selectedAudioStream = ListHelper.getDefaultAudioFormat(activity, audioTracks)
+ val trackNames = audioTracks.map { Localization.audioTrackName(activity, it) }
+
+ AlertDialog.Builder(activity)
+ .setTitle(R.string.select_audio_track_external_players)
+ .setNeutralButton(R.string.open_in_browser) { dialog, which ->
+ ShareUtils.openUrlInBrowser(requireActivity(), url)
+ }
+ .setSingleChoiceItems(trackNames.toTypedArray(), selectedAudioStream, null)
+ .setNegativeButton(R.string.cancel, null)
+ .setPositiveButton(R.string.ok) { dialog, which ->
+ val index = (dialog as AlertDialog).listView.getCheckedItemPosition()
+ startOnExternalPlayer(activity, info, audioTracks[index])
+ }
+ .show()
+ }
+ }
+
+ /*
+ * Remove unneeded information while waiting for a next task
+ * */
+ private fun cleanUp() {
+ // New beginning
+ stack.clear()
+ currentWorker?.dispose()
+ PlayerHolder.stopService()
+ setInitialData(0, null, "", null)
+ currentInfo = null
+ updateOverlayData(null, null, listOf())
+ }
+
+ /*//////////////////////////////////////////////////////////////////////////
+ // Bottom mini player
+ ////////////////////////////////////////////////////////////////////////// */
+ /**
+ * That's for Android TV support. Move focus from main fragment to the player or back
+ * based on what is currently selected
+ *
+ * @param toMain if true than the main fragment will be focused or the player otherwise
+ */
+ private fun moveFocusToMainFragment(toMain: Boolean) {
+ setupBrightness()
+ val mainFragment = requireActivity().findViewById(R.id.fragment_holder)
+ // Hamburger button steels a focus even under bottomSheet
+ val toolbar = requireActivity().findViewById(R.id.toolbar)
+ val afterDescendants = ViewGroup.FOCUS_AFTER_DESCENDANTS
+ val blockDescendants = ViewGroup.FOCUS_BLOCK_DESCENDANTS
+ if (toMain) {
+ mainFragment.setDescendantFocusability(afterDescendants)
+ toolbar.setDescendantFocusability(afterDescendants)
+ (requireView() as ViewGroup).setDescendantFocusability(blockDescendants)
+ // Only focus the mainFragment if the mainFragment (e.g. search-results)
+ // or the toolbar (e.g. TextField for search) don't have focus.
+ // This was done to fix problems with the keyboard input, see also #7490
+ if (!mainFragment.hasFocus() && !toolbar.hasFocus()) {
+ mainFragment.requestFocus()
+ }
+ } else {
+ mainFragment.setDescendantFocusability(blockDescendants)
+ toolbar.setDescendantFocusability(blockDescendants)
+ (requireView() as ViewGroup).setDescendantFocusability(afterDescendants)
+ // Only focus the player if it not already has focus
+ if (!binding.getRoot().hasFocus()) {
+ binding.detailThumbnailRootLayout.requestFocus()
+ }
+ }
+ }
+
+ /**
+ * When the mini player exists the view underneath it is not touchable.
+ * Bottom padding should be equal to the mini player's height in this case
+ *
+ * @param showMore whether main fragment should be expanded or not
+ */
+ private fun manageSpaceAtTheBottom(showMore: Boolean) {
+ val peekHeight = resources.getDimensionPixelSize(R.dimen.mini_player_height)
+ val holder = requireActivity().findViewById(R.id.fragment_holder)
+ val newBottomPadding = if (showMore) 0 else peekHeight
+ if (holder.paddingBottom == newBottomPadding) {
+ return
+ }
+ holder.setPadding(
+ holder.getPaddingLeft(),
+ holder.paddingTop,
+ holder.getPaddingRight(),
+ newBottomPadding
+ )
+ }
+
+ private fun setupBottomPlayer() {
+ val params = binding.appBarLayout.layoutParams as CoordinatorLayout.LayoutParams
+ val behavior = params.behavior as AppBarLayout.Behavior?
+
+ val bottomSheetLayout = activity.findViewById(R.id.fragment_player_holder)
+ bottomSheetBehavior = BottomSheetBehavior.from(bottomSheetLayout)
+ bottomSheetBehavior.state = lastStableBottomSheetState
+ updateBottomSheetState(lastStableBottomSheetState)
+
+ val peekHeight = resources.getDimensionPixelSize(R.dimen.mini_player_height)
+ if (bottomSheetState != BottomSheetBehavior.STATE_HIDDEN) {
+ manageSpaceAtTheBottom(false)
+ bottomSheetBehavior.peekHeight = peekHeight
+ if (bottomSheetState == BottomSheetBehavior.STATE_COLLAPSED) {
+ binding.overlayLayout.alpha = MAX_OVERLAY_ALPHA
+ } else if (bottomSheetState == BottomSheetBehavior.STATE_EXPANDED) {
+ binding.overlayLayout.alpha = 0f
+ setOverlayElementsClickable(false)
+ }
+ }
+
+ bottomSheetCallback = object : BottomSheetCallback() {
+ override fun onStateChanged(bottomSheet: View, newState: Int) {
+ updateBottomSheetState(newState)
+
+ when (newState) {
+ BottomSheetBehavior.STATE_HIDDEN -> {
+ moveFocusToMainFragment(true)
+ manageSpaceAtTheBottom(true)
+
+ bottomSheetBehavior.peekHeight = 0
+ cleanUp()
+ }
+
+ BottomSheetBehavior.STATE_EXPANDED -> {
+ moveFocusToMainFragment(false)
+ manageSpaceAtTheBottom(false)
+
+ bottomSheetBehavior.peekHeight = peekHeight
+ // Disable click because overlay buttons located on top of buttons
+ // from the player
+ setOverlayElementsClickable(false)
+ hideSystemUiIfNeeded()
+ // Conditions when the player should be expanded to fullscreen
+ if (DeviceUtils.isLandscape(requireContext()) &&
+ player?.isPlaying == true &&
+ !this@VideoDetailFragment.isFullscreen &&
+ !DeviceUtils.isTablet(activity)
+ ) {
+ player?.UIs()?.get(MainPlayerUi::class)?.toggleFullscreen()
+ }
+ setOverlayLook(binding.appBarLayout, behavior, 1f)
+ }
+
+ BottomSheetBehavior.STATE_COLLAPSED -> {
+ moveFocusToMainFragment(true)
+ manageSpaceAtTheBottom(false)
+
+ bottomSheetBehavior.peekHeight = peekHeight
+
+ // Re-enable clicks
+ setOverlayElementsClickable(true)
+ player?.UIs()?.get(MainPlayerUi::class)?.closeItemsList()
+ setOverlayLook(binding.appBarLayout, behavior, 0f)
+ }
+
+ BottomSheetBehavior.STATE_DRAGGING, BottomSheetBehavior.STATE_SETTLING -> {
+ if (this@VideoDetailFragment.isFullscreen) {
+ showSystemUi()
+ }
+ player?.UIs()?.get(MainPlayerUi::class)?.let {
+ if (it.isControlsVisible) {
+ it.hideControls(0, 0)
+ }
+ }
+ }
+
+ BottomSheetBehavior.STATE_HALF_EXPANDED -> {}
+ }
+ }
+
+ override fun onSlide(bottomSheet: View, slideOffset: Float) {
+ setOverlayLook(binding.appBarLayout, behavior, slideOffset)
+ }
+ }
+
+ bottomSheetBehavior.addBottomSheetCallback(bottomSheetCallback)
+
+ // User opened a new page and the player will hide itself
+ activity.supportFragmentManager.addOnBackStackChangedListener {
+ if (bottomSheetBehavior.getState() == BottomSheetBehavior.STATE_EXPANDED) {
+ bottomSheetBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED)
+ }
+ }
+ }
+
+ private fun updateOverlayPlayQueueButtonVisibility() {
+ // hide the button if the queue is empty; no player => no play queue :)
+ nullableBinding?.overlayPlayQueueButton?.isVisible = player?.playQueue?.isEmpty == false
+ }
+
+ private fun updateOverlayData(
+ overlayTitle: String?,
+ uploader: String?,
+ thumbnails: List
+ ) {
+ binding.overlayTitleTextView.text = overlayTitle ?: ""
+ binding.overlayChannelTextView.text = uploader ?: ""
+ binding.overlayThumbnail.setImageDrawable(null)
+ CoilHelper.loadDetailsThumbnail(binding.overlayThumbnail, thumbnails)
+ }
+
+ private fun setOverlayPlayPauseImage(playerIsPlaying: Boolean) {
+ val drawable = if (playerIsPlaying) R.drawable.ic_pause else R.drawable.ic_play_arrow
+ binding.overlayPlayPauseButton.setImageResource(drawable)
+ }
+
+ private fun setOverlayLook(
+ appBar: AppBarLayout,
+ behavior: AppBarLayout.Behavior?,
+ slideOffset: Float
+ ) {
+ // SlideOffset < 0 when mini player is about to close via swipe.
+ // Stop animation in this case
+ if (behavior == null || slideOffset < 0) {
+ return
+ }
+ binding.overlayLayout.alpha = min(MAX_OVERLAY_ALPHA, 1 - slideOffset)
+ // These numbers are not special. They just do a cool transition
+ behavior.setTopAndBottomOffset(
+ (-binding.detailThumbnailImageView.height * 2 * (1 - slideOffset) / 3).toInt()
+ )
+ appBar.requestLayout()
+ }
+
+ private fun setOverlayElementsClickable(enable: Boolean) {
+ binding.overlayThumbnail.isClickable = enable
+ binding.overlayThumbnail.isLongClickable = enable
+ binding.overlayMetadataLayout.isClickable = enable
+ binding.overlayMetadataLayout.isLongClickable = enable
+ binding.overlayButtonsLayout.isClickable = enable
+ binding.overlayPlayQueueButton.isClickable = enable
+ binding.overlayPlayPauseButton.isClickable = enable
+ binding.overlayCloseButton.isClickable = enable
+ }
+
+ val root: View?
+ get() = player?.UIs()?.get(VideoPlayerUi::class)?.binding?.root
+
+ private fun updateBottomSheetState(newState: Int) {
+ bottomSheetState = newState
+ if (newState != BottomSheetBehavior.STATE_DRAGGING &&
+ newState != BottomSheetBehavior.STATE_SETTLING
+ ) {
+ lastStableBottomSheetState = newState
+ }
+ }
+
+ companion object {
+ const val KEY_SWITCHING_PLAYERS: String = "switching_players"
+
+ private const val MAX_OVERLAY_ALPHA = 0.9f
+ private const val MAX_PLAYER_HEIGHT = 0.7f
+
+ const val ACTION_SHOW_MAIN_PLAYER: String =
+ App.PACKAGE_NAME + ".VideoDetailFragment.ACTION_SHOW_MAIN_PLAYER"
+ const val ACTION_HIDE_MAIN_PLAYER: String =
+ App.PACKAGE_NAME + ".VideoDetailFragment.ACTION_HIDE_MAIN_PLAYER"
+ const val ACTION_PLAYER_STARTED: String =
+ App.PACKAGE_NAME + ".VideoDetailFragment.ACTION_PLAYER_STARTED"
+ const val ACTION_VIDEO_FRAGMENT_RESUMED: String =
+ App.PACKAGE_NAME + ".VideoDetailFragment.ACTION_VIDEO_FRAGMENT_RESUMED"
+ const val ACTION_VIDEO_FRAGMENT_STOPPED: String =
+ App.PACKAGE_NAME + ".VideoDetailFragment.ACTION_VIDEO_FRAGMENT_STOPPED"
+
+ private const val COMMENTS_TAB_TAG = "COMMENTS"
+ private const val RELATED_TAB_TAG = "NEXT VIDEO"
+ private const val DESCRIPTION_TAB_TAG = "DESCRIPTION TAB"
+ private const val EMPTY_TAB_TAG = "EMPTY TAB"
+
+ /*//////////////////////////////////////////////////////////////////////// */
+ @JvmStatic
+ fun getInstance(
+ serviceId: Int,
+ url: String?,
+ name: String,
+ queue: PlayQueue?
+ ): VideoDetailFragment {
+ val instance = VideoDetailFragment()
+ instance.setInitialData(serviceId, url, name, queue)
+ return instance
+ }
+
+ @JvmStatic
+ fun getInstanceInCollapsedState(): VideoDetailFragment {
+ val instance = VideoDetailFragment()
+ instance.updateBottomSheetState(BottomSheetBehavior.STATE_COLLAPSED)
+ return instance
+ }
+
+ /*//////////////////////////////////////////////////////////////////////////
+ // OwnStack
+ ////////////////////////////////////////////////////////////////////////// */
+ /**
+ * Stack that contains the "navigation history".
+ * The peek is the current video.
+ */
+ private var stack = LinkedList()
+ }
+}
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 61a361f23..7f594734a 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,7 @@ import android.util.Log;
import android.view.View;
import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
import com.evernote.android.state.State;
@@ -42,6 +43,7 @@ public abstract class BaseListInfoFragment menuItemToFilterName = new SparseArrayCompat<>();
private StreamingService service;
+ @Nullable
private Page nextPage;
private boolean showLocalSuggestions = true;
private boolean showRemoteSuggestions = true;
@@ -221,6 +222,15 @@ public class SearchFragment extends BaseListFragment result) {
showListFooter(false);
infoListAdapter.addInfoItemList(result.getItems());
- nextPage = result.getNextPage();
if (!result.getErrors().isEmpty()) {
- showSnackBarError(new ErrorInfo(result.getErrors(), UserAction.SEARCHED,
- "\"" + searchString + "\" → pageUrl: " + nextPage.getUrl() + ", "
- + "pageIds: " + nextPage.getIds() + ", "
- + "pageCookies: " + nextPage.getCookies(),
- serviceId));
+ // nextPage should be non-null at this point, because it refers to the page
+ // whose results are handled here, but let's check it anyway
+ if (nextPage == null) {
+ showSnackBarError(new ErrorInfo(result.getErrors(), UserAction.SEARCHED,
+ "\"" + searchString + "\" → nextPage == null", serviceId));
+ } else {
+ showSnackBarError(new ErrorInfo(result.getErrors(), UserAction.SEARCHED,
+ "\"" + searchString + "\" → pageUrl: " + nextPage.getUrl() + ", "
+ + "pageIds: " + nextPage.getIds() + ", "
+ + "pageCookies: " + nextPage.getCookies(),
+ serviceId));
+ }
}
+
+ // keep the reassignment of nextPage after the error handling to ensure that nextPage
+ // still holds the correct value during the error handling
+ nextPage = result.getNextPage();
super.handleNextItems(result);
}
diff --git a/app/src/main/java/org/schabi/newpipe/info_list/dialog/InfoItemDialog.java b/app/src/main/java/org/schabi/newpipe/info_list/dialog/InfoItemDialog.java
index dcf01e190..cbaae2834 100644
--- a/app/src/main/java/org/schabi/newpipe/info_list/dialog/InfoItemDialog.java
+++ b/app/src/main/java/org/schabi/newpipe/info_list/dialog/InfoItemDialog.java
@@ -252,7 +252,7 @@ public final class InfoItemDialog {
* @return the current {@link Builder} instance
*/
public Builder addEnqueueEntriesIfNeeded() {
- final PlayerHolder holder = PlayerHolder.getInstance();
+ final PlayerHolder holder = PlayerHolder.INSTANCE;
if (holder.isPlayQueueReady()) {
addEntry(StreamDialogDefaultEntry.ENQUEUE);
diff --git a/app/src/main/java/org/schabi/newpipe/local/feed/service/FeedLoadService.kt b/app/src/main/java/org/schabi/newpipe/local/feed/service/FeedLoadService.kt
index f960040de..4aa825ca8 100644
--- a/app/src/main/java/org/schabi/newpipe/local/feed/service/FeedLoadService.kt
+++ b/app/src/main/java/org/schabi/newpipe/local/feed/service/FeedLoadService.kt
@@ -31,6 +31,7 @@ import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.core.app.PendingIntentCompat
import androidx.core.app.ServiceCompat
+import androidx.core.content.ContextCompat
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.core.Flowable
import io.reactivex.rxjava3.disposables.Disposable
@@ -200,7 +201,7 @@ class FeedLoadService : Service() {
}
}
}
- registerReceiver(broadcastReceiver, IntentFilter(ACTION_CANCEL))
+ ContextCompat.registerReceiver(this, broadcastReceiver, IntentFilter(ACTION_CANCEL), ContextCompat.RECEIVER_NOT_EXPORTED)
}
// /////////////////////////////////////////////////////////////////////////
diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/ImportConfirmationDialog.java b/app/src/main/java/org/schabi/newpipe/local/subscription/ImportConfirmationDialog.java
index a4b3ea399..3dc6d7b46 100644
--- a/app/src/main/java/org/schabi/newpipe/local/subscription/ImportConfirmationDialog.java
+++ b/app/src/main/java/org/schabi/newpipe/local/subscription/ImportConfirmationDialog.java
@@ -1,7 +1,5 @@
package org.schabi.newpipe.local.subscription;
-import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
-
import android.app.Dialog;
import android.os.Bundle;
@@ -39,7 +37,6 @@ public class ImportConfirmationDialog extends DialogFragment {
@Override
public Dialog onCreateDialog(@Nullable final Bundle savedInstanceState) {
final var context = requireContext();
- assureCorrectAppLanguage(context);
return new AlertDialog.Builder(context)
.setMessage(R.string.import_network_expensive_warning)
.setCancelable(true)
diff --git a/app/src/main/java/org/schabi/newpipe/player/PlayQueueActivity.java b/app/src/main/java/org/schabi/newpipe/player/PlayQueueActivity.java
index 9d680da4d..7f3a8dbd5 100644
--- a/app/src/main/java/org/schabi/newpipe/player/PlayQueueActivity.java
+++ b/app/src/main/java/org/schabi/newpipe/player/PlayQueueActivity.java
@@ -2,7 +2,6 @@ package org.schabi.newpipe.player;
import static org.schabi.newpipe.QueueItemMenuUtil.openPopupMenu;
import static org.schabi.newpipe.player.helper.PlayerHelper.formatSpeed;
-import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
import android.content.ComponentName;
import android.content.Intent;
@@ -84,7 +83,6 @@ public final class PlayQueueActivity extends AppCompatActivity
@Override
protected void onCreate(final Bundle savedInstanceState) {
- assureCorrectAppLanguage(this);
super.onCreate(savedInstanceState);
ThemeHelper.setTheme(this, ServiceHelper.getSelectedServiceId(this));
@@ -97,8 +95,48 @@ public final class PlayQueueActivity extends AppCompatActivity
getSupportActionBar().setTitle(R.string.title_activity_play_queue);
}
- serviceConnection = getServiceConnection();
- bind();
+ serviceConnection = new ServiceConnection() {
+ @Override
+ public void onServiceDisconnected(final ComponentName name) {
+ Log.d(TAG, "Player service is disconnected");
+ }
+
+ @Override
+ public void onServiceConnected(final ComponentName name, final IBinder binder) {
+ Log.d(TAG, "Player service is connected");
+
+ if (binder instanceof PlayerService.LocalBinder) {
+ @Nullable final PlayerService s =
+ ((PlayerService.LocalBinder) binder).getService();
+ if (s == null) {
+ throw new IllegalArgumentException(
+ "PlayerService.LocalBinder.getService() must never be"
+ + "null after the service connects");
+ }
+ player = s.getPlayer();
+ }
+
+ if (player == null || player.getPlayQueue() == null || player.exoPlayerIsNull()) {
+ unbind();
+ } else {
+ onQueueUpdate(player.getPlayQueue());
+ buildComponents();
+ if (player != null) {
+ player.setActivityListener(PlayQueueActivity.this);
+ }
+ }
+ }
+ };
+
+ // Note: this code should not really exist, and PlayerHolder should be used instead, but
+ // it will be rewritten when NewPlayer will replace the current player.
+ final Intent bindIntent = new Intent(this, PlayerService.class);
+ bindIntent.setAction(PlayerService.BIND_PLAYER_HOLDER_ACTION);
+ final boolean success = bindService(bindIntent, serviceConnection, BIND_AUTO_CREATE);
+ if (!success) {
+ unbindService(serviceConnection);
+ }
+ serviceBound = success;
}
@Override
@@ -180,19 +218,6 @@ public final class PlayQueueActivity extends AppCompatActivity
////////////////////////////////////////////////////////////////////////////
// Service Connection
- ////////////////////////////////////////////////////////////////////////////
-
- private void bind() {
- // Note: this code should not really exist, and PlayerHolder should be used instead, but
- // it will be rewritten when NewPlayer will replace the current player.
- final Intent bindIntent = new Intent(this, PlayerService.class);
- bindIntent.setAction(PlayerService.BIND_PLAYER_HOLDER_ACTION);
- final boolean success = bindService(bindIntent, serviceConnection, BIND_AUTO_CREATE);
- if (!success) {
- unbindService(serviceConnection);
- }
- serviceBound = success;
- }
private void unbind() {
if (serviceBound) {
@@ -212,41 +237,6 @@ public final class PlayQueueActivity extends AppCompatActivity
}
}
- private ServiceConnection getServiceConnection() {
- return new ServiceConnection() {
- @Override
- public void onServiceDisconnected(final ComponentName name) {
- Log.d(TAG, "Player service is disconnected");
- }
-
- @Override
- public void onServiceConnected(final ComponentName name, final IBinder binder) {
- Log.d(TAG, "Player service is connected");
-
- if (binder instanceof PlayerService.LocalBinder) {
- @Nullable final PlayerService s =
- ((PlayerService.LocalBinder) binder).getService();
- if (s == null) {
- throw new IllegalArgumentException(
- "PlayerService.LocalBinder.getService() must never be"
- + "null after the service connects");
- }
- player = s.getPlayer();
- }
-
- if (player == null || player.getPlayQueue() == null || player.exoPlayerIsNull()) {
- unbind();
- } else {
- onQueueUpdate(player.getPlayQueue());
- buildComponents();
- if (player != null) {
- player.setActivityListener(PlayQueueActivity.this);
- }
- }
- }
- };
- }
-
////////////////////////////////////////////////////////////////////////////
// Component Building
////////////////////////////////////////////////////////////////////////////
diff --git a/app/src/main/java/org/schabi/newpipe/player/Player.java b/app/src/main/java/org/schabi/newpipe/player/Player.java
index d3e3ff1df..b09593c17 100644
--- a/app/src/main/java/org/schabi/newpipe/player/Player.java
+++ b/app/src/main/java/org/schabi/newpipe/player/Player.java
@@ -44,7 +44,6 @@ import static org.schabi.newpipe.player.notification.NotificationConstants.ACTIO
import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_SHUFFLE;
import static org.schabi.newpipe.util.ListHelper.getPopupResolutionIndex;
import static org.schabi.newpipe.util.ListHelper.getResolutionIndex;
-import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static coil3.Image_androidKt.toBitmap;
@@ -61,6 +60,7 @@ import android.view.LayoutInflater;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import androidx.core.content.ContextCompat;
import androidx.core.math.MathUtils;
import androidx.preference.PreferenceManager;
@@ -133,6 +133,10 @@ import io.reactivex.rxjava3.disposables.CompositeDisposable;
import io.reactivex.rxjava3.disposables.Disposable;
import io.reactivex.rxjava3.disposables.SerialDisposable;
+/**
+ * The ExoPlayer wrapper & Player business logic.
+ * Only instantiated once, from {@link PlayerService}.
+ */
public final class Player implements PlaybackListener, Listener {
public static final boolean DEBUG = MainActivity.DEBUG;
public static final String TAG = Player.class.getSimpleName();
@@ -397,7 +401,7 @@ public final class Player implements PlaybackListener, Listener {
&& newQueue.size() == 1 && newQueue.getItem() != null
&& playQueue != null && playQueue.size() == 1 && playQueue.getItem() != null
&& newQueue.getItem().getUrl().equals(playQueue.getItem().getUrl())
- && newQueue.getItem().getRecoveryPosition() != PlayQueueItem.RECOVERY_UNSET) {
+ && newQueue.getItem().getRecoveryPosition() != Long.MIN_VALUE) {
// Player can have state = IDLE when playback is stopped or failed
// and we should retry in this case
if (simpleExoPlayer.getPlaybackState()
@@ -425,7 +429,7 @@ public final class Player implements PlaybackListener, Listener {
&& !samePlayQueue
&& !newQueue.isEmpty()
&& newQueue.getItem() != null
- && newQueue.getItem().getRecoveryPosition() == PlayQueueItem.RECOVERY_UNSET) {
+ && newQueue.getItem().getRecoveryPosition() == Long.MIN_VALUE) {
databaseUpdateDisposable.add(recordManager.loadStreamState(newQueue.getItem())
.observeOn(AndroidSchedulers.mainThread())
// Do not place initPlayback() in doFinally() because
@@ -473,22 +477,23 @@ public final class Player implements PlaybackListener, Listener {
}
private void initUIsForCurrentPlayerType() {
- if ((UIs.getOpt(MainPlayerUi.class).isPresent() && playerType == PlayerType.MAIN)
- || (UIs.getOpt(PopupPlayerUi.class).isPresent()
+ if ((UIs.get(MainPlayerUi.class) != null && playerType == PlayerType.MAIN)
+ || (UIs.get(PopupPlayerUi.class) != null
&& playerType == PlayerType.POPUP)) {
// correct UI already in place
return;
}
// try to reuse binding if possible
- final PlayerBinding binding = UIs.getOpt(VideoPlayerUi.class).map(VideoPlayerUi::getBinding)
- .orElseGet(() -> {
- if (playerType == PlayerType.AUDIO) {
- return null;
- } else {
- return PlayerBinding.inflate(LayoutInflater.from(context));
- }
- });
+ @Nullable final VideoPlayerUi ui = UIs.get(VideoPlayerUi.class);
+ final PlayerBinding binding;
+ if (ui != null) {
+ binding = ui.getBinding();
+ } else if (playerType == PlayerType.AUDIO) {
+ binding = null;
+ } else {
+ binding = PlayerBinding.inflate(LayoutInflater.from(context));
+ }
switch (playerType) {
case MAIN:
@@ -751,7 +756,6 @@ public final class Player implements PlaybackListener, Listener {
toggleShuffleModeEnabled();
break;
case Intent.ACTION_CONFIGURATION_CHANGED:
- assureCorrectAppLanguage(service);
if (DEBUG) {
Log.d(TAG, "ACTION_CONFIGURATION_CHANGED received");
}
@@ -764,7 +768,8 @@ public final class Player implements PlaybackListener, Listener {
private void registerBroadcastReceiver() {
// Try to unregister current first
unregisterBroadcastReceiver();
- context.registerReceiver(broadcastReceiver, intentFilter);
+ ContextCompat.registerReceiver(context, broadcastReceiver, intentFilter,
+ ContextCompat.RECEIVER_EXPORTED);
}
private void unregisterBroadcastReceiver() {
@@ -1588,7 +1593,7 @@ public final class Player implements PlaybackListener, Listener {
}
// sync the player index with the queue index, and seek to the correct position
- if (item.getRecoveryPosition() != PlayQueueItem.RECOVERY_UNSET) {
+ if (item.getRecoveryPosition() != Long.MIN_VALUE) {
simpleExoPlayer.seekTo(playQueueIndex, item.getRecoveryPosition());
playQueue.unsetRecovery(playQueueIndex);
} else {
diff --git a/app/src/main/java/org/schabi/newpipe/player/PlayerService.kt b/app/src/main/java/org/schabi/newpipe/player/PlayerService.kt
index c335611b0..dc7bdaa92 100644
--- a/app/src/main/java/org/schabi/newpipe/player/PlayerService.kt
+++ b/app/src/main/java/org/schabi/newpipe/player/PlayerService.kt
@@ -34,10 +34,8 @@ import org.schabi.newpipe.player.mediabrowser.MediaBrowserImpl
import org.schabi.newpipe.player.mediabrowser.MediaBrowserPlaybackPreparer
import org.schabi.newpipe.player.mediasession.MediaSessionPlayerUi
import org.schabi.newpipe.player.notification.NotificationPlayerUi
-import org.schabi.newpipe.util.Localization
import org.schabi.newpipe.util.ThemeHelper
import java.lang.ref.WeakReference
-import java.util.function.BiConsumer
import java.util.function.Consumer
/**
@@ -47,13 +45,13 @@ class PlayerService : MediaBrowserServiceCompat() {
// These objects are used to cleanly separate the Service implementation (in this file) and the
// media browser and playback preparer implementations. At the moment the playback preparer is
// only used in conjunction with the media browser.
- private var mediaBrowserImpl: MediaBrowserImpl? = null
- private var mediaBrowserPlaybackPreparer: MediaBrowserPlaybackPreparer? = null
+ private lateinit var mediaBrowserImpl: MediaBrowserImpl
+ private lateinit var mediaBrowserPlaybackPreparer: MediaBrowserPlaybackPreparer
// these are instantiated in onCreate() as per
// https://developer.android.com/training/cars/media#browser_workflow
- private var mediaSession: MediaSessionCompat? = null
- private var sessionConnector: MediaSessionConnector? = null
+ private lateinit var mediaSession: MediaSessionCompat
+ private lateinit var sessionConnector: MediaSessionConnector
/**
* @return the current active player instance. May be null, since the player service can outlive
@@ -68,7 +66,7 @@ class PlayerService : MediaBrowserServiceCompat() {
* The parameter taken by this [Consumer] can be null to indicate the player is being
* stopped.
*/
- private var onPlayerStartedOrStopped: Consumer? = null
+ private var onPlayerStartedOrStopped: ((player: Player?) -> Unit)? = null
//region Service lifecycle
override fun onCreate() {
@@ -77,17 +75,9 @@ class PlayerService : MediaBrowserServiceCompat() {
if (DEBUG) {
Log.d(TAG, "onCreate() called")
}
- Localization.assureCorrectAppLanguage(this)
ThemeHelper.setTheme(this)
- mediaBrowserImpl = MediaBrowserImpl(
- this,
- Consumer { parentId: String ->
- this.notifyChildrenChanged(
- parentId
- )
- }
- )
+ mediaBrowserImpl = MediaBrowserImpl(this, this::notifyChildrenChanged)
// see https://developer.android.com/training/cars/media#browser_workflow
val session = MediaSessionCompat(this, "MediaSessionPlayerServ")
@@ -98,17 +88,10 @@ class PlayerService : MediaBrowserServiceCompat() {
connector.setMetadataDeduplicationEnabled(true)
mediaBrowserPlaybackPreparer = MediaBrowserPlaybackPreparer(
- this,
- BiConsumer { message: String, code: Int ->
- connector.setCustomErrorMessage(
- message,
- code
- )
- },
- Runnable { connector.setCustomErrorMessage(null) },
- Consumer { playWhenReady: Boolean? ->
- player?.onPrepare()
- }
+ context = this,
+ setMediaSessionError = connector::setCustomErrorMessage,
+ clearMediaSessionError = { connector.setCustomErrorMessage(null) },
+ onPrepare = { player?.onPrepare() }
)
connector.setPlaybackPreparer(mediaBrowserPlaybackPreparer)
@@ -125,11 +108,8 @@ class PlayerService : MediaBrowserServiceCompat() {
if (DEBUG) {
Log.d(
TAG,
- (
- "onStartCommand() called with: intent = [" + intent +
- "], extras = [" + intent.extras.toDebugString() +
- "], flags = [" + flags + "], startId = [" + startId + "]"
- )
+ "onStartCommand() called with: intent = [$intent], extras = [${
+ intent.extras.toDebugString()}], flags = [$flags], startId = [$startId]"
)
}
@@ -140,7 +120,7 @@ class PlayerService : MediaBrowserServiceCompat() {
val playerWasNull = (player == null)
if (playerWasNull) {
// make sure the player exists, in case the service was resumed
- player = Player(this, mediaSession!!, sessionConnector!!)
+ player = Player(this, mediaSession, sessionConnector)
}
// Be sure that the player notification is set and the service is started in foreground,
@@ -150,35 +130,29 @@ class PlayerService : MediaBrowserServiceCompat() {
// no one already and starting the service in foreground should not create any issues.
// If the service is already started in foreground, requesting it to be started
// shouldn't do anything.
- player!!.UIs().get(NotificationPlayerUi::class.java)
- ?.createNotificationAndStartForeground()
+ player?.UIs()?.get(NotificationPlayerUi::class)?.createNotificationAndStartForeground()
- val startedOrStopped = onPlayerStartedOrStopped
- if (playerWasNull && startedOrStopped != null) {
+ if (playerWasNull) {
// notify that a new player was created (but do it after creating the foreground
// notification just to make sure we don't incur, due to slowness, in
// "Context.startForegroundService() did not then call Service.startForeground()")
- startedOrStopped.accept(player)
+ onPlayerStartedOrStopped?.invoke(player)
}
}
val p = player
- if (Intent.ACTION_MEDIA_BUTTON == intent.action &&
- (p == null || p.playQueue == null)
- ) {
- /*
- No need to process media button's actions if the player is not working, otherwise
- the player service would strangely start with nothing to play
- Stop the service in this case, which will be removed from the foreground and its
- notification cancelled in its destruction
- */
+ if (Intent.ACTION_MEDIA_BUTTON == intent.action && p?.playQueue == null) {
+ // No need to process media button's actions if the player is not working, otherwise
+ // the player service would strangely start with nothing to play
+ // Stop the service in this case, which will be removed from the foreground and its
+ // notification cancelled in its destruction
destroyPlayerAndStopService()
return START_NOT_STICKY
}
if (p != null) {
p.handleIntent(intent)
- p.UIs().get(MediaSessionPlayerUi::class.java)
+ p.UIs().get(MediaSessionPlayerUi::class)
?.handleMediaButtonIntent(intent)
}
@@ -218,22 +192,22 @@ class PlayerService : MediaBrowserServiceCompat() {
cleanup()
- mediaBrowserPlaybackPreparer?.dispose()
- mediaSession?.release()
- mediaBrowserImpl?.dispose()
+ mediaBrowserPlaybackPreparer.dispose()
+ mediaSession.release()
+ mediaBrowserImpl.dispose()
}
private fun cleanup() {
val p = player
if (p != null) {
// notify that the player is being destroyed
- onPlayerStartedOrStopped?.accept(null)
+ onPlayerStartedOrStopped?.invoke(null)
p.saveAndShutdown()
player = null
}
// Should already be handled by MediaSessionPlayerUi, but just to be sure.
- mediaSession?.setActive(false)
+ mediaSession.setActive(false)
// Should already be handled by NotificationUtil.cancelNotificationAndStopForeground() in
// NotificationPlayerUi, but let's make sure that the foreground service is stopped.
@@ -273,29 +247,27 @@ class PlayerService : MediaBrowserServiceCompat() {
if (DEBUG) {
Log.d(
TAG,
- (
- "onBind() called with: intent = [" + intent +
- "], extras = [" + intent.extras.toDebugString() + "]"
- )
+ "onBind() called with: intent = [$intent], extras = [${
+ intent.extras.toDebugString()}]"
)
}
- if (BIND_PLAYER_HOLDER_ACTION == intent.action) {
+ return if (BIND_PLAYER_HOLDER_ACTION == intent.action) {
// Note that this binder might be reused multiple times while the service is alive, even
// after unbind() has been called: https://stackoverflow.com/a/8794930 .
- return mBinder
+ mBinder
} else if (SERVICE_INTERFACE == intent.action) {
// MediaBrowserService also uses its own binder, so for actions related to the media
// browser service, pass the onBind to the superclass.
- return super.onBind(intent)
+ super.onBind(intent)
} else {
// This is an unknown request, avoid returning any binder to not leak objects.
- return null
+ null
}
}
class LocalBinder internal constructor(playerService: PlayerService) : Binder() {
- private val playerService = WeakReference(playerService)
+ private val playerService = WeakReference(playerService)
val service: PlayerService?
get() = playerService.get()
@@ -307,9 +279,9 @@ class PlayerService : MediaBrowserServiceCompat() {
* by the [Consumer] can be null to indicate that the player is stopping.
* @param listener the listener to set or unset
*/
- fun setPlayerListener(listener: Consumer?) {
+ fun setPlayerListener(listener: ((player: Player?) -> Unit)?) {
this.onPlayerStartedOrStopped = listener
- listener?.accept(player)
+ listener?.invoke(player)
}
//endregion
@@ -320,14 +292,14 @@ class PlayerService : MediaBrowserServiceCompat() {
rootHints: Bundle?
): BrowserRoot? {
// TODO check if the accessing package has permission to view data
- return mediaBrowserImpl?.onGetRoot(clientPackageName, clientUid, rootHints)
+ return mediaBrowserImpl.onGetRoot(clientPackageName, clientUid, rootHints)
}
override fun onLoadChildren(
parentId: String,
result: Result>
) {
- mediaBrowserImpl?.onLoadChildren(parentId, result)
+ mediaBrowserImpl.onLoadChildren(parentId, result)
}
override fun onSearch(
@@ -335,7 +307,7 @@ class PlayerService : MediaBrowserServiceCompat() {
extras: Bundle?,
result: Result>
) {
- mediaBrowserImpl?.onSearch(query, result)
+ mediaBrowserImpl.onSearch(query, result)
} //endregion
companion object {
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 7e74c3848..c5d6ada4b 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
@@ -2,7 +2,6 @@ package org.schabi.newpipe.player.helper;
import static org.schabi.newpipe.ktx.ViewUtils.animateRotation;
import static org.schabi.newpipe.player.Player.DEBUG;
-import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
import static org.schabi.newpipe.util.ThemeHelper.resolveDrawable;
import android.app.Dialog;
@@ -145,7 +144,6 @@ public class PlaybackParameterDialog extends DialogFragment {
@NonNull
@Override
public Dialog onCreateDialog(@Nullable final Bundle savedInstanceState) {
- assureCorrectAppLanguage(getContext());
Bridge.restoreInstanceState(this, savedInstanceState);
binding = DialogPlaybackParameterBinding.inflate(getLayoutInflater());
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 a110a80d6..266d65f36 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
@@ -33,11 +33,9 @@ import com.google.android.exoplayer2.trackselection.ExoTrackSelection;
import com.google.android.exoplayer2.ui.AspectRatioFrameLayout;
import com.google.android.exoplayer2.ui.AspectRatioFrameLayout.ResizeMode;
import com.google.android.exoplayer2.ui.CaptionStyleCompat;
-import com.google.android.exoplayer2.util.MimeTypes;
import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.InfoItem;
-import org.schabi.newpipe.extractor.MediaFormat;
import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
import org.schabi.newpipe.extractor.stream.SubtitlesStream;
@@ -47,13 +45,14 @@ import org.schabi.newpipe.player.playqueue.PlayQueue;
import org.schabi.newpipe.player.playqueue.PlayQueueItem;
import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
import org.schabi.newpipe.util.ListHelper;
+import org.schabi.newpipe.util.Localization;
import java.lang.annotation.Retention;
import java.text.DecimalFormat;
+import java.text.DecimalFormatSymbols;
import java.text.NumberFormat;
import java.util.ArrayList;
import java.util.Collections;
-import java.util.Formatter;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
@@ -62,11 +61,7 @@ import java.util.Set;
import java.util.concurrent.TimeUnit;
public final class PlayerHelper {
- private static final StringBuilder STRING_BUILDER = new StringBuilder();
- private static final Formatter STRING_FORMATTER =
- new Formatter(STRING_BUILDER, Locale.getDefault());
- private static final NumberFormat SPEED_FORMATTER = new DecimalFormat("0.##x");
- private static final NumberFormat PITCH_FORMATTER = new DecimalFormat("##%");
+ private static final FormattersProvider FORMATTERS_PROVIDER = new FormattersProvider();
@Retention(SOURCE)
@IntDef({AUTOPLAY_TYPE_ALWAYS, AUTOPLAY_TYPE_WIFI,
@@ -89,9 +84,11 @@ public final class PlayerHelper {
private PlayerHelper() {
}
- ////////////////////////////////////////////////////////////////////////////
- // Exposed helpers
- ////////////////////////////////////////////////////////////////////////////
+ // region Exposed helpers
+
+ public static void resetFormat() {
+ FORMATTERS_PROVIDER.reset();
+ }
@NonNull
public static String getTimeString(final int milliSeconds) {
@@ -100,35 +97,24 @@ public final class PlayerHelper {
final int hours = (milliSeconds % 86400000) / 3600000;
final int days = (milliSeconds % (86400000 * 7)) / 86400000;
- STRING_BUILDER.setLength(0);
- return (days > 0
- ? STRING_FORMATTER.format("%d:%02d:%02d:%02d", days, hours, minutes, seconds)
- : hours > 0
- ? STRING_FORMATTER.format("%d:%02d:%02d", hours, minutes, seconds)
- : STRING_FORMATTER.format("%02d:%02d", minutes, seconds)
- ).toString();
+ final Formatters formatters = FORMATTERS_PROVIDER.formatters();
+ if (days > 0) {
+ return formatters.stringFormat("%d:%02d:%02d:%02d", days, hours, minutes, seconds);
+ }
+
+ return hours > 0
+ ? formatters.stringFormat("%d:%02d:%02d", hours, minutes, seconds)
+ : formatters.stringFormat("%02d:%02d", minutes, seconds);
}
@NonNull
public static String formatSpeed(final double speed) {
- return SPEED_FORMATTER.format(speed);
+ return FORMATTERS_PROVIDER.formatters().speed().format(speed);
}
@NonNull
public static String formatPitch(final double pitch) {
- return PITCH_FORMATTER.format(pitch);
- }
-
- @NonNull
- public static String subtitleMimeTypesOf(@NonNull final MediaFormat format) {
- switch (format) {
- case VTT:
- return MimeTypes.TEXT_VTT;
- case TTML:
- return MimeTypes.APPLICATION_TTML;
- default:
- throw new IllegalArgumentException("Unrecognized mime type: " + format.name());
- }
+ return FORMATTERS_PROVIDER.formatters().pitch().format(pitch);
}
@NonNull
@@ -219,9 +205,8 @@ public final class PlayerHelper {
? null : getAutoQueuedSinglePlayQueue(autoQueueItems.get(0));
}
- ////////////////////////////////////////////////////////////////////////////
- // Settings Resolution
- ////////////////////////////////////////////////////////////////////////////
+ // endregion
+ // region Resolution
public static boolean isResumeAfterAudioFocusGain(@NonNull final Context context) {
return getPreferences(context)
@@ -405,9 +390,8 @@ public final class PlayerHelper {
return Integer.parseInt(preferredIntervalBytes) * 1024;
}
- ////////////////////////////////////////////////////////////////////////////
- // Private helpers
- ////////////////////////////////////////////////////////////////////////////
+ // endregion
+ // region Private helpers
@NonNull
private static SharedPreferences getPreferences(@NonNull final Context context) {
@@ -427,9 +411,8 @@ public final class PlayerHelper {
}
- ////////////////////////////////////////////////////////////////////////////
- // Utils used by player
- ////////////////////////////////////////////////////////////////////////////
+ // endregion
+ // region Utils used by player
@RepeatMode
public static int nextRepeatMode(@RepeatMode final int repeatMode) {
@@ -503,4 +486,43 @@ public final class PlayerHelper {
player.getContext().getString(R.string.seek_duration_key),
player.getContext().getString(R.string.seek_duration_default_value))));
}
+
+ // endregion
+ // region Format
+
+ static class FormattersProvider {
+ private Formatters formatters;
+
+ public Formatters formatters() {
+ if (formatters == null) {
+ formatters = Formatters.create();
+ }
+ return formatters;
+ }
+
+ public void reset() {
+ formatters = null;
+ }
+ }
+
+ record Formatters(
+ Locale locale,
+ NumberFormat speed,
+ NumberFormat pitch) {
+
+ static Formatters create() {
+ final Locale locale = Localization.getAppLocale();
+ final DecimalFormatSymbols dfs = DecimalFormatSymbols.getInstance(locale);
+ return new Formatters(
+ locale,
+ new DecimalFormat("0.##x", dfs),
+ new DecimalFormat("##%", dfs));
+ }
+
+ String stringFormat(final String format, final Object... args) {
+ return String.format(locale, format, args);
+ }
+ }
+
+ // endregion
}
diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHolder.java b/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHolder.java
deleted file mode 100644
index ba8a5e0ff..000000000
--- a/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHolder.java
+++ /dev/null
@@ -1,385 +0,0 @@
-package org.schabi.newpipe.player.helper;
-
-import android.content.ComponentName;
-import android.content.Context;
-import android.content.Intent;
-import android.content.ServiceConnection;
-import android.os.IBinder;
-import android.util.Log;
-
-import androidx.annotation.Nullable;
-import androidx.core.content.ContextCompat;
-
-import com.google.android.exoplayer2.PlaybackException;
-import com.google.android.exoplayer2.PlaybackParameters;
-
-import org.schabi.newpipe.App;
-import org.schabi.newpipe.MainActivity;
-import org.schabi.newpipe.extractor.stream.StreamInfo;
-import org.schabi.newpipe.player.PlayerService;
-import org.schabi.newpipe.player.Player;
-import org.schabi.newpipe.player.PlayerType;
-import org.schabi.newpipe.player.event.PlayerServiceEventListener;
-import org.schabi.newpipe.player.event.PlayerServiceExtendedEventListener;
-import org.schabi.newpipe.player.playqueue.PlayQueue;
-import org.schabi.newpipe.util.NavigationHelper;
-
-import java.util.Optional;
-import java.util.function.Consumer;
-
-public final class PlayerHolder {
-
- private PlayerHolder() {
- }
-
- private static PlayerHolder instance;
- public static synchronized PlayerHolder getInstance() {
- if (PlayerHolder.instance == null) {
- PlayerHolder.instance = new PlayerHolder();
- }
- return PlayerHolder.instance;
- }
-
- private static final boolean DEBUG = MainActivity.DEBUG;
- private static final String TAG = PlayerHolder.class.getSimpleName();
-
- @Nullable private PlayerServiceExtendedEventListener listener;
-
- private final PlayerServiceConnection serviceConnection = new PlayerServiceConnection();
- private boolean bound;
- @Nullable private PlayerService playerService;
-
- private Optional getPlayer() {
- return Optional.ofNullable(playerService)
- .flatMap(s -> Optional.ofNullable(s.getPlayer()));
- }
-
- private Optional getPlayQueue() {
- // player play queue might be null e.g. while player is starting
- return getPlayer().flatMap(p -> Optional.ofNullable(p.getPlayQueue()));
- }
-
- /**
- * Returns the current {@link PlayerType} of the {@link PlayerService} service,
- * otherwise `null` if no service is running.
- *
- * @return Current PlayerType
- */
- @Nullable
- public PlayerType getType() {
- return getPlayer().map(Player::getPlayerType).orElse(null);
- }
-
- public boolean isPlaying() {
- return getPlayer().map(Player::isPlaying).orElse(false);
- }
-
- public boolean isPlayerOpen() {
- return getPlayer().isPresent();
- }
-
- /**
- * Use this method to only allow the user to manipulate the play queue (e.g. by enqueueing via
- * the stream long press menu) when there actually is a play queue to manipulate.
- * @return true only if the player is open and its play queue is ready (i.e. it is not null)
- */
- public boolean isPlayQueueReady() {
- return getPlayQueue().isPresent();
- }
-
- public boolean isBound() {
- return bound;
- }
-
- public int getQueueSize() {
- return getPlayQueue().map(PlayQueue::size).orElse(0);
- }
-
- public int getQueuePosition() {
- return getPlayQueue().map(PlayQueue::getIndex).orElse(0);
- }
-
- public void setListener(@Nullable final PlayerServiceExtendedEventListener newListener) {
- listener = newListener;
-
- if (listener == null) {
- return;
- }
-
- // Force reload data from service
- if (playerService != null) {
- listener.onServiceConnected(playerService);
- startPlayerListener();
- // ^ will call listener.onPlayerConnected() down the line if there is an active player
- }
- }
-
- // helper to handle context in common place as using the same
- // context to bind/unbind a service is crucial
- private Context getCommonContext() {
- return App.getInstance();
- }
-
- /**
- * Connect to (and if needed start) the {@link PlayerService}
- * and bind {@link PlayerServiceConnection} to it.
- * If the service is already started, only set the listener.
- * @param playAfterConnect If this holder’s service was already started,
- * start playing immediately
- * @param newListener set this listener
- * */
- public void startService(final boolean playAfterConnect,
- final PlayerServiceExtendedEventListener newListener) {
- if (DEBUG) {
- Log.d(TAG, "startService() called with playAfterConnect=" + playAfterConnect);
- }
- final Context context = getCommonContext();
- setListener(newListener);
- if (bound) {
- return;
- }
- // startService() can be called concurrently and it will give a random crashes
- // and NullPointerExceptions inside the service because the service will be
- // bound twice. Prevent it with unbinding first
- unbind(context);
- final Intent intent = new Intent(context, PlayerService.class);
- intent.putExtra(PlayerService.SHOULD_START_FOREGROUND_EXTRA, true);
- ContextCompat.startForegroundService(context, intent);
- serviceConnection.doPlayAfterConnect(playAfterConnect);
- bind(context);
- }
-
- public void stopService() {
- if (DEBUG) {
- Log.d(TAG, "stopService() called");
- }
- if (playerService != null) {
- playerService.destroyPlayerAndStopService();
- }
- final Context context = getCommonContext();
- unbind(context);
- // destroyPlayerAndStopService() already runs the next line of code, but run it again just
- // to make sure to stop the service even if playerService is null by any chance.
- context.stopService(new Intent(context, PlayerService.class));
- }
-
- class PlayerServiceConnection implements ServiceConnection {
-
- private boolean playAfterConnect = false;
-
- /**
- * @param playAfterConnection Sets the value of `playAfterConnect` to pass to the {@link
- * PlayerServiceExtendedEventListener#onPlayerConnected(Player, boolean)} the next time it
- * is called. The value of `playAfterConnect` will be reset to false after that.
- */
- public void doPlayAfterConnect(final boolean playAfterConnection) {
- this.playAfterConnect = playAfterConnection;
- }
-
- @Override
- public void onServiceDisconnected(final ComponentName compName) {
- if (DEBUG) {
- Log.d(TAG, "Player service is disconnected");
- }
-
- final Context context = getCommonContext();
- unbind(context);
- }
-
- @Override
- public void onServiceConnected(final ComponentName compName, final IBinder service) {
- if (DEBUG) {
- Log.d(TAG, "Player service is connected");
- }
- final PlayerService.LocalBinder localBinder = (PlayerService.LocalBinder) service;
-
- @Nullable final PlayerService s = localBinder.getService();
- if (s == null) {
- throw new IllegalArgumentException(
- "PlayerService.LocalBinder.getService() must never be"
- + "null after the service connects");
- }
- playerService = s;
- if (listener != null) {
- listener.onServiceConnected(s);
- getPlayer().ifPresent(p -> listener.onPlayerConnected(p, playAfterConnect));
- }
- startPlayerListener();
- // ^ will call listener.onPlayerConnected() down the line if there is an active player
-
- // notify the main activity that binding the service has completed, so that it can
- // open the bottom mini-player
- NavigationHelper.sendPlayerStartedEvent(s);
- }
- }
-
- private void bind(final Context context) {
- if (DEBUG) {
- Log.d(TAG, "bind() called");
- }
- // BIND_AUTO_CREATE starts the service if it's not already running
- bound = bind(context, Context.BIND_AUTO_CREATE);
- if (!bound) {
- context.unbindService(serviceConnection);
- }
- }
-
- public void tryBindIfNeeded(final Context context) {
- if (!bound) {
- // flags=0 means the service will not be started if it does not already exist. In this
- // case the return value is not useful, as a value of "true" does not really indicate
- // that the service is going to be bound.
- bind(context, 0);
- }
- }
-
- private boolean bind(final Context context, final int flags) {
- final Intent serviceIntent = new Intent(context, PlayerService.class);
- serviceIntent.setAction(PlayerService.BIND_PLAYER_HOLDER_ACTION);
- return context.bindService(serviceIntent, serviceConnection, flags);
- }
-
- private void unbind(final Context context) {
- if (DEBUG) {
- Log.d(TAG, "unbind() called");
- }
-
- if (bound) {
- context.unbindService(serviceConnection);
- bound = false;
- stopPlayerListener();
- playerService = null;
- if (listener != null) {
- listener.onPlayerDisconnected();
- listener.onServiceDisconnected();
- }
- }
- }
-
- private void startPlayerListener() {
- if (playerService != null) {
- // setting the player listener will take care of calling relevant callbacks if the
- // player in the service is (not) already active, also see playerStateListener below
- playerService.setPlayerListener(playerStateListener);
- }
- getPlayer().ifPresent(p -> p.setFragmentListener(internalListener));
- }
-
- private void stopPlayerListener() {
- if (playerService != null) {
- playerService.setPlayerListener(null);
- }
- getPlayer().ifPresent(p -> p.removeFragmentListener(internalListener));
- }
-
- /**
- * This listener will be held by the players created by {@link PlayerService}.
- */
- private final PlayerServiceEventListener internalListener =
- new PlayerServiceEventListener() {
- @Override
- public void onViewCreated() {
- if (listener != null) {
- listener.onViewCreated();
- }
- }
-
- @Override
- public void onFullscreenStateChanged(final boolean fullscreen) {
- if (listener != null) {
- listener.onFullscreenStateChanged(fullscreen);
- }
- }
-
- @Override
- public void onScreenRotationButtonClicked() {
- if (listener != null) {
- listener.onScreenRotationButtonClicked();
- }
- }
-
- @Override
- public void onMoreOptionsLongClicked() {
- if (listener != null) {
- listener.onMoreOptionsLongClicked();
- }
- }
-
- @Override
- public void onPlayerError(final PlaybackException error,
- final boolean isCatchableException) {
- if (listener != null) {
- listener.onPlayerError(error, isCatchableException);
- }
- }
-
- @Override
- public void hideSystemUiIfNeeded() {
- if (listener != null) {
- listener.hideSystemUiIfNeeded();
- }
- }
-
- @Override
- public void onQueueUpdate(final PlayQueue queue) {
- if (listener != null) {
- listener.onQueueUpdate(queue);
- }
- }
-
- @Override
- public void onPlaybackUpdate(final int state,
- final int repeatMode,
- final boolean shuffled,
- final PlaybackParameters parameters) {
- if (listener != null) {
- listener.onPlaybackUpdate(state, repeatMode, shuffled, parameters);
- }
- }
-
- @Override
- public void onProgressUpdate(final int currentProgress,
- final int duration,
- final int bufferPercent) {
- if (listener != null) {
- listener.onProgressUpdate(currentProgress, duration, bufferPercent);
- }
- }
-
- @Override
- public void onMetadataUpdate(final StreamInfo info, final PlayQueue queue) {
- if (listener != null) {
- listener.onMetadataUpdate(info, queue);
- }
- }
-
- @Override
- public void onServiceStopped() {
- if (listener != null) {
- listener.onServiceStopped();
- }
- unbind(getCommonContext());
- }
- };
-
- /**
- * This listener will be held by bound {@link PlayerService}s to notify of the player starting
- * or stopping. This is necessary since the service outlives the player e.g. to answer Android
- * Auto media browser queries.
- */
- private final Consumer playerStateListener = (@Nullable final Player player) -> {
- if (listener != null) {
- if (player == null) {
- // player.fragmentListener=null is already done by player.stopActivityBinding(),
- // which is called by player.destroy(), which is in turn called by PlayerService
- // before setting its player to null
- listener.onPlayerDisconnected();
- } else {
- listener.onPlayerConnected(player, serviceConnection.playAfterConnect);
- // reset the value of playAfterConnect: if it was true before, it is now "consumed"
- serviceConnection.playAfterConnect = false;
- player.setFragmentListener(internalListener);
- }
- }
- };
-}
diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHolder.kt b/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHolder.kt
new file mode 100644
index 000000000..1b0cedfc5
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHolder.kt
@@ -0,0 +1,316 @@
+package org.schabi.newpipe.player.helper
+
+import android.content.ComponentName
+import android.content.Context
+import android.content.Intent
+import android.content.ServiceConnection
+import android.os.IBinder
+import android.util.Log
+import androidx.core.content.ContextCompat
+import com.google.android.exoplayer2.PlaybackException
+import com.google.android.exoplayer2.PlaybackParameters
+import org.schabi.newpipe.App
+import org.schabi.newpipe.MainActivity
+import org.schabi.newpipe.extractor.stream.StreamInfo
+import org.schabi.newpipe.player.Player
+import org.schabi.newpipe.player.PlayerService
+import org.schabi.newpipe.player.PlayerService.LocalBinder
+import org.schabi.newpipe.player.PlayerType
+import org.schabi.newpipe.player.event.PlayerServiceEventListener
+import org.schabi.newpipe.player.event.PlayerServiceExtendedEventListener
+import org.schabi.newpipe.player.playqueue.PlayQueue
+import org.schabi.newpipe.util.NavigationHelper
+
+private val DEBUG = MainActivity.DEBUG
+private val TAG: String = PlayerHolder::class.java.getSimpleName()
+
+/**
+ * Singleton that manages a `PlayerService`
+ * and can be used to control the player instance through the service.
+ */
+object PlayerHolder {
+ private var listener: PlayerServiceExtendedEventListener? = null
+
+ var isBound: Boolean = false
+ private set
+
+ private var playerService: PlayerService? = null
+
+ private val player: Player?
+ get() = playerService?.player
+
+ // player play queue might be null e.g. while player is starting
+ private val playQueue: PlayQueue?
+ get() = this.player?.playQueue
+
+ val type: PlayerType?
+ /**
+ * Returns the current [PlayerType] of the [PlayerService] service,
+ * otherwise `null` if no service is running.
+ *
+ * @return Current PlayerType
+ */
+ get() = this.player?.playerType
+
+ val isPlaying: Boolean
+ get() = this.player?.isPlaying == true
+
+ val isPlayerOpen: Boolean
+ get() = this.player != null
+
+ val isPlayQueueReady: Boolean
+ /**
+ * Use this method to only allow the user to manipulate the play queue (e.g. by enqueueing via
+ * the stream long press menu) when there actually is a play queue to manipulate.
+ * @return true only if the player is open and its play queue is ready (i.e. it is not null)
+ */
+ get() = this.playQueue != null
+
+ val queueSize: Int
+ get() = this.playQueue?.size() ?: 0
+
+ val queuePosition: Int
+ get() = this.playQueue?.index ?: 0
+
+ fun setListener(newListener: PlayerServiceExtendedEventListener?) {
+ listener = newListener
+
+ // Force reload data from service
+ newListener?.let { listener ->
+ playerService?.let { service ->
+ listener.onServiceConnected(service)
+ startPlayerListener()
+ // ^ will call listener.onPlayerConnected() down the line if there is an active player
+ }
+ }
+ }
+
+ private val commonContext: Context
+ // helper to handle context in common place as using the same
+ get() = App.instance
+
+ /**
+ * Connect to (and if needed start) the [PlayerService]
+ * and bind [PlayerServiceConnection] to it.
+ * If the service is already started, only set the listener.
+ * @param playAfterConnect If this holder’s service was already started,
+ * start playing immediately
+ * @param newListener set this listener
+ */
+ fun startService(
+ playAfterConnect: Boolean,
+ newListener: PlayerServiceExtendedEventListener?
+ ) {
+ if (DEBUG) {
+ Log.d(TAG, "startService() called with playAfterConnect=$playAfterConnect")
+ }
+ val context = this.commonContext
+ setListener(newListener)
+ if (this.isBound) {
+ return
+ }
+ // startService() can be called concurrently and it will give a random crashes
+ // and NullPointerExceptions inside the service because the service will be
+ // bound twice. Prevent it with unbinding first
+ unbind(context)
+ val intent = Intent(context, PlayerService::class.java)
+ intent.putExtra(PlayerService.SHOULD_START_FOREGROUND_EXTRA, true)
+ ContextCompat.startForegroundService(context, intent)
+ PlayerServiceConnection.doPlayAfterConnect(playAfterConnect)
+ bind(context)
+ }
+
+ fun stopService() {
+ if (DEBUG) {
+ Log.d(TAG, "stopService() called")
+ }
+ playerService?.destroyPlayerAndStopService()
+ val context = this.commonContext
+ unbind(context)
+ // destroyPlayerAndStopService() already runs the next line of code, but run it again just
+ // to make sure to stop the service even if playerService is null by any chance.
+ context.stopService(Intent(context, PlayerService::class.java))
+ }
+
+ internal object PlayerServiceConnection : ServiceConnection {
+ internal var playAfterConnect = false
+
+ /**
+ * @param playAfterConnection Sets the value of [playAfterConnect] to pass to the
+ * [PlayerServiceExtendedEventListener.onPlayerConnected] the next time it
+ * is called. The value of [playAfterConnect] will be reset to false after that.
+ */
+ fun doPlayAfterConnect(playAfterConnection: Boolean) {
+ this.playAfterConnect = playAfterConnection
+ }
+
+ override fun onServiceDisconnected(compName: ComponentName?) {
+ if (DEBUG) {
+ Log.d(TAG, "Player service is disconnected")
+ }
+
+ val context: Context = this@PlayerHolder.commonContext
+ unbind(context)
+ }
+
+ override fun onServiceConnected(compName: ComponentName?, service: IBinder?) {
+ if (DEBUG) {
+ Log.d(TAG, "Player service is connected")
+ }
+ val localBinder = service as LocalBinder
+
+ val s = localBinder.service
+ requireNotNull(s) {
+ "PlayerService.LocalBinder.getService() must never be" +
+ "null after the service connects"
+ }
+ playerService = s
+ listener?.let { l ->
+ l.onServiceConnected(s)
+ player?.let { l.onPlayerConnected(it, playAfterConnect) }
+ }
+ startPlayerListener()
+ // ^ will call listener.onPlayerConnected() down the line if there is an active player
+
+ // notify the main activity that binding the service has completed, so that it can
+ // open the bottom mini-player
+ NavigationHelper.sendPlayerStartedEvent(s)
+ }
+ }
+
+ private fun bind(context: Context) {
+ if (DEBUG) {
+ Log.d(TAG, "bind() called")
+ }
+ // BIND_AUTO_CREATE starts the service if it's not already running
+ this.isBound = bind(context, Context.BIND_AUTO_CREATE)
+ if (!this.isBound) {
+ context.unbindService(PlayerServiceConnection)
+ }
+ }
+
+ fun tryBindIfNeeded(context: Context) {
+ if (!this.isBound) {
+ // flags=0 means the service will not be started if it does not already exist. In this
+ // case the return value is not useful, as a value of "true" does not really indicate
+ // that the service is going to be bound.
+ bind(context, 0)
+ }
+ }
+
+ private fun bind(context: Context, flags: Int): Boolean {
+ val serviceIntent = Intent(context, PlayerService::class.java)
+ serviceIntent.setAction(PlayerService.BIND_PLAYER_HOLDER_ACTION)
+ return context.bindService(serviceIntent, PlayerServiceConnection, flags)
+ }
+
+ private fun unbind(context: Context) {
+ if (DEBUG) {
+ Log.d(TAG, "unbind() called")
+ }
+
+ if (this.isBound) {
+ context.unbindService(PlayerServiceConnection)
+ this.isBound = false
+ stopPlayerListener()
+ playerService = null
+ listener?.onPlayerDisconnected()
+ listener?.onServiceDisconnected()
+ }
+ }
+
+ private fun startPlayerListener() {
+ // setting the player listener will take care of calling relevant callbacks if the
+ // player in the service is (not) already active, also see playerStateListener below
+ playerService?.setPlayerListener(playerStateListener)
+ this.player?.setFragmentListener(HolderPlayerServiceEventListener)
+ }
+
+ private fun stopPlayerListener() {
+ playerService?.setPlayerListener(null)
+ this.player?.removeFragmentListener(HolderPlayerServiceEventListener)
+ }
+
+ /**
+ * This listener will be held by the players created by [PlayerService].
+ */
+ private object HolderPlayerServiceEventListener : PlayerServiceEventListener {
+ override fun onViewCreated() {
+ listener?.onViewCreated()
+ }
+
+ override fun onFullscreenStateChanged(fullscreen: Boolean) {
+ listener?.onFullscreenStateChanged(fullscreen)
+ }
+
+ override fun onScreenRotationButtonClicked() {
+ listener?.onScreenRotationButtonClicked()
+ }
+
+ override fun onMoreOptionsLongClicked() {
+ listener?.onMoreOptionsLongClicked()
+ }
+
+ override fun onPlayerError(
+ error: PlaybackException?,
+ isCatchableException: Boolean
+ ) {
+ listener?.onPlayerError(error, isCatchableException)
+ }
+
+ override fun hideSystemUiIfNeeded() {
+ listener?.hideSystemUiIfNeeded()
+ }
+
+ override fun onQueueUpdate(queue: PlayQueue?) {
+ listener?.onQueueUpdate(queue)
+ }
+
+ override fun onPlaybackUpdate(
+ state: Int,
+ repeatMode: Int,
+ shuffled: Boolean,
+ parameters: PlaybackParameters?
+ ) {
+ listener?.onPlaybackUpdate(state, repeatMode, shuffled, parameters)
+ }
+
+ override fun onProgressUpdate(
+ currentProgress: Int,
+ duration: Int,
+ bufferPercent: Int
+ ) {
+ listener?.onProgressUpdate(currentProgress, duration, bufferPercent)
+ }
+
+ override fun onMetadataUpdate(info: StreamInfo?, queue: PlayQueue?) {
+ listener?.onMetadataUpdate(info, queue)
+ }
+
+ override fun onServiceStopped() {
+ listener?.onServiceStopped()
+ unbind(this@PlayerHolder.commonContext)
+ }
+ }
+
+ /**
+ * This listener will be held by bound [PlayerService]s to notify of the player starting
+ * or stopping. This is necessary since the service outlives the player e.g. to answer Android
+ * Auto media browser queries.
+ */
+ private val playerStateListener: (Player?) -> Unit = { player: Player? ->
+ listener?.let { l ->
+ if (player == null) {
+ // player.fragmentListener=null is already done by player.stopActivityBinding(),
+ // which is called by player.destroy(), which is in turn called by PlayerService
+ // before setting its player to null
+ l.onPlayerDisconnected()
+ } else {
+ l.onPlayerConnected(player, PlayerServiceConnection.playAfterConnect)
+ // reset the value of playAfterConnect: if it was true before, it is now "consumed"
+ PlayerServiceConnection.playAfterConnect = false
+ player.setFragmentListener(HolderPlayerServiceEventListener)
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/org/schabi/newpipe/player/mediabrowser/MediaBrowserImpl.kt b/app/src/main/java/org/schabi/newpipe/player/mediabrowser/MediaBrowserImpl.kt
index f15d7ab08..149f47c1a 100644
--- a/app/src/main/java/org/schabi/newpipe/player/mediabrowser/MediaBrowserImpl.kt
+++ b/app/src/main/java/org/schabi/newpipe/player/mediabrowser/MediaBrowserImpl.kt
@@ -9,7 +9,9 @@ import android.support.v4.media.MediaDescriptionCompat
import android.util.Log
import androidx.annotation.DrawableRes
import androidx.core.net.toUri
+import androidx.core.os.bundleOf
import androidx.media.MediaBrowserServiceCompat
+import androidx.media.MediaBrowserServiceCompat.BrowserRoot.EXTRA_RECENT
import androidx.media.MediaBrowserServiceCompat.Result
import androidx.media.utils.MediaConstants
import io.reactivex.rxjava3.core.Flowable
@@ -36,7 +38,6 @@ import org.schabi.newpipe.local.playlist.RemotePlaylistManager
import org.schabi.newpipe.util.ExtractorHelper
import org.schabi.newpipe.util.ServiceHelper
import org.schabi.newpipe.util.image.ImageStrategy
-import java.util.function.Consumer
/**
* This class is used to cleanly separate the Service implementation (in
@@ -46,16 +47,15 @@ import java.util.function.Consumer
*/
class MediaBrowserImpl(
private val context: Context,
- notifyChildrenChanged: Consumer, // parentId
+ notifyChildrenChanged: (parentId: String) -> Unit,
) {
+ private val packageValidator = PackageValidator(context)
private val database = NewPipeDatabase.getInstance(context)
private var disposables = CompositeDisposable()
init {
// this will listen to changes in the bookmarks until this MediaBrowserImpl is dispose()d
- disposables.add(
- getMergedPlaylists().subscribe { notifyChildrenChanged.accept(ID_BOOKMARKS) }
- )
+ disposables.add(getMergedPlaylists().subscribe { notifyChildrenChanged(ID_BOOKMARKS) })
}
//region Cleanup
@@ -69,11 +69,22 @@ class MediaBrowserImpl(
clientPackageName: String,
clientUid: Int,
rootHints: Bundle?
- ): MediaBrowserServiceCompat.BrowserRoot {
+ ): MediaBrowserServiceCompat.BrowserRoot? {
if (DEBUG) {
Log.d(TAG, "onGetRoot($clientPackageName, $clientUid, $rootHints)")
}
+ if (!packageValidator.isKnownCaller(clientPackageName, clientUid)) {
+ // this is a caller we can't trust (see PackageValidator's rules taken from uamp)
+ return null
+ }
+
+ if (rootHints?.getBoolean(EXTRA_RECENT, false) == true) {
+ // the system is asking for a root to do media resumption, but we can't handle that yet,
+ // see https://developer.android.com/media/implement/surfaces/mobile#mediabrowserservice_implementation
+ return null
+ }
+
val extras = Bundle()
extras.putBoolean(
MediaConstants.BROWSER_SERVICE_EXTRAS_KEY_SEARCH_SUPPORTED, true
@@ -183,17 +194,16 @@ class MediaBrowserImpl(
private fun createPlaylistMediaItem(playlist: PlaylistLocalItem): MediaBrowserCompat.MediaItem {
val builder = MediaDescriptionCompat.Builder()
- builder
.setMediaId(createMediaIdForInfoItem(playlist is PlaylistRemoteEntity, playlist.uid))
.setTitle(playlist.orderingName)
.setIconUri(imageUriOrNullIfDisabled(playlist.thumbnailUrl))
+ .setExtras(
+ bundleOf(
+ MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_GROUP_TITLE
+ to context.resources.getString(R.string.tab_bookmarks)
+ )
+ )
- val extras = Bundle()
- extras.putString(
- MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_GROUP_TITLE,
- context.resources.getString(R.string.tab_bookmarks),
- )
- builder.setExtras(extras)
return MediaBrowserCompat.MediaItem(
builder.build(),
MediaBrowserCompat.MediaItem.FLAG_BROWSABLE,
@@ -202,8 +212,9 @@ class MediaBrowserImpl(
private fun createInfoItemMediaItem(item: InfoItem): MediaBrowserCompat.MediaItem? {
val builder = MediaDescriptionCompat.Builder()
- builder.setMediaId(createMediaIdForInfoItem(item))
+ .setMediaId(createMediaIdForInfoItem(item))
.setTitle(item.name)
+ .setIconUri(ImageStrategy.choosePreferredImage(item.thumbnails)?.toUri())
when (item.infoType) {
InfoType.STREAM -> builder.setSubtitle((item as StreamInfoItem).uploaderName)
@@ -212,10 +223,6 @@ class MediaBrowserImpl(
else -> return null
}
- ImageStrategy.choosePreferredImage(item.thumbnails)?.let {
- builder.setIconUri(imageUriOrNullIfDisabled(it))
- }
-
return MediaBrowserCompat.MediaItem(
builder.build(),
MediaBrowserCompat.MediaItem.FLAG_PLAYABLE
@@ -256,7 +263,7 @@ class MediaBrowserImpl(
index: Int,
): MediaBrowserCompat.MediaItem {
val builder = MediaDescriptionCompat.Builder()
- builder.setMediaId(createMediaIdForPlaylistIndex(false, playlistId, index))
+ .setMediaId(createMediaIdForPlaylistIndex(false, playlistId, index))
.setTitle(item.streamEntity.title)
.setSubtitle(item.streamEntity.uploader)
.setIconUri(imageUriOrNullIfDisabled(item.streamEntity.thumbnailUrl))
@@ -276,10 +283,7 @@ class MediaBrowserImpl(
builder.setMediaId(createMediaIdForPlaylistIndex(true, playlistId, index))
.setTitle(item.name)
.setSubtitle(item.uploaderName)
-
- ImageStrategy.choosePreferredImage(item.thumbnails)?.let {
- builder.setIconUri(imageUriOrNullIfDisabled(it))
- }
+ .setIconUri(ImageStrategy.choosePreferredImage(item.thumbnails)?.toUri())
return MediaBrowserCompat.MediaItem(
builder.build(),
diff --git a/app/src/main/java/org/schabi/newpipe/player/mediabrowser/MediaBrowserPlaybackPreparer.kt b/app/src/main/java/org/schabi/newpipe/player/mediabrowser/MediaBrowserPlaybackPreparer.kt
index a3791e2e7..2948eeaf8 100644
--- a/app/src/main/java/org/schabi/newpipe/player/mediabrowser/MediaBrowserPlaybackPreparer.kt
+++ b/app/src/main/java/org/schabi/newpipe/player/mediabrowser/MediaBrowserPlaybackPreparer.kt
@@ -6,6 +6,7 @@ import android.os.Bundle
import android.os.ResultReceiver
import android.support.v4.media.session.PlaybackStateCompat
import android.util.Log
+import androidx.core.content.ContextCompat
import androidx.core.net.toUri
import com.google.android.exoplayer2.Player
import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector.PlaybackPreparer
@@ -109,14 +110,14 @@ class MediaBrowserPlaybackPreparer(
//region Errors
private fun onUnsupportedError() {
setMediaSessionError.accept(
- context.getString(R.string.content_not_supported),
+ ContextCompat.getString(context, R.string.content_not_supported),
PlaybackStateCompat.ERROR_CODE_NOT_SUPPORTED
)
}
private fun onPrepareError() {
setMediaSessionError.accept(
- context.getString(R.string.error_snackbar_message),
+ ContextCompat.getString(context, R.string.error_snackbar_message),
PlaybackStateCompat.ERROR_CODE_APP_ERROR
)
}
diff --git a/app/src/main/java/org/schabi/newpipe/player/mediabrowser/PackageValidator.kt b/app/src/main/java/org/schabi/newpipe/player/mediabrowser/PackageValidator.kt
new file mode 100644
index 000000000..05719b6d4
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/player/mediabrowser/PackageValidator.kt
@@ -0,0 +1,240 @@
+/*
+ * Copyright 2018 Google Inc. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// THIS FILE WAS TAKEN FROM UAMP, EXCEPT FOR THINGS RELATED TO THE WHITELIST. UPDATE IT WHEN NEEDED.
+// https://github.com/android/uamp/blob/329a21b63c247e9bd35f6858d4fc0e448fa38603/common/src/main/java/com/example/android/uamp/media/PackageValidator.kt
+
+package org.schabi.newpipe.player.mediabrowser
+
+import android.Manifest.permission.MEDIA_CONTENT_CONTROL
+import android.annotation.SuppressLint
+import android.content.Context
+import android.content.pm.PackageInfo
+import android.content.pm.PackageInfo.REQUESTED_PERMISSION_GRANTED
+import android.content.pm.PackageManager
+import android.os.Process
+import android.support.v4.media.session.MediaSessionCompat
+import android.util.Log
+import androidx.core.app.NotificationManagerCompat
+import androidx.media.MediaBrowserServiceCompat
+import org.schabi.newpipe.BuildConfig
+import java.security.MessageDigest
+import java.security.NoSuchAlgorithmException
+
+/**
+ * Validates that the calling package is authorized to browse a [MediaBrowserServiceCompat].
+ *
+ * The list of allowed signing certificates and their corresponding package names is defined in
+ * res/xml/allowed_media_browser_callers.xml.
+ *
+ * If you want to add a new caller to allowed_media_browser_callers.xml and you don't know
+ * its signature, this class will print to logcat (INFO level) a message with the proper
+ * xml tags to add to allow the caller.
+ *
+ * For more information, see res/xml/allowed_media_browser_callers.xml.
+ */
+internal class PackageValidator(context: Context) {
+ private val context: Context = context.applicationContext
+ private val packageManager: PackageManager = this.context.packageManager
+ private val platformSignature: String = getSystemSignature()
+ private val callerChecked = mutableMapOf>()
+
+ /**
+ * Checks whether the caller attempting to connect to a [MediaBrowserServiceCompat] is known.
+ * See [MusicService.onGetRoot] for where this is utilized.
+ *
+ * @param callingPackage The package name of the caller.
+ * @param callingUid The user id of the caller.
+ * @return `true` if the caller is known, `false` otherwise.
+ */
+ fun isKnownCaller(callingPackage: String, callingUid: Int): Boolean {
+ // If the caller has already been checked, return the previous result here.
+ val (checkedUid, checkResult) = callerChecked[callingPackage] ?: Pair(0, false)
+ if (checkedUid == callingUid) {
+ return checkResult
+ }
+
+ /**
+ * Because some of these checks can be slow, we save the results in [callerChecked] after
+ * this code is run.
+ *
+ * In particular, there's little reason to recompute the calling package's certificate
+ * signature (SHA-256) each call.
+ *
+ * This is safe to do as we know the UID matches the package's UID (from the check above),
+ * and app UIDs are set at install time. Additionally, a package name + UID is guaranteed to
+ * be constant until a reboot. (After a reboot then a previously assigned UID could be
+ * reassigned.)
+ */
+
+ // Build the caller info for the rest of the checks here.
+ val callerPackageInfo = buildCallerInfo(callingPackage)
+ ?: throw IllegalStateException("Caller wasn't found in the system?")
+
+ // Verify that things aren't ... broken. (This test should always pass.)
+ if (callerPackageInfo.uid != callingUid) {
+ throw IllegalStateException("Caller's package UID doesn't match caller's actual UID?")
+ }
+
+ val callerSignature = callerPackageInfo.signature
+
+ val isCallerKnown = when {
+ // If it's our own app making the call, allow it.
+ callingUid == Process.myUid() -> true
+ // If the system is making the call, allow it.
+ callingUid == Process.SYSTEM_UID -> true
+ // If the app was signed by the same certificate as the platform itself, also allow it.
+ callerSignature == platformSignature -> true
+ /**
+ * [MEDIA_CONTENT_CONTROL] permission is only available to system applications, and
+ * while it isn't required to allow these apps to connect to a
+ * [MediaBrowserServiceCompat], allowing this ensures optimal compatability with apps
+ * such as Android TV and the Google Assistant.
+ */
+ callerPackageInfo.permissions.contains(MEDIA_CONTENT_CONTROL) -> true
+ /**
+ * If the calling app has a notification listener it is able to retrieve notifications
+ * and can connect to an active [MediaSessionCompat].
+ *
+ * It's not required to allow apps with a notification listener to
+ * connect to your [MediaBrowserServiceCompat], but it does allow easy compatibility
+ * with apps such as Wear OS.
+ */
+ NotificationManagerCompat.getEnabledListenerPackages(this.context)
+ .contains(callerPackageInfo.packageName) -> true
+
+ // If none of the previous checks succeeded, then the caller is unrecognized.
+ else -> false
+ }
+
+ if (!isCallerKnown) {
+ logUnknownCaller(callerPackageInfo)
+ }
+
+ // Save our work for next time.
+ callerChecked[callingPackage] = Pair(callingUid, isCallerKnown)
+ return isCallerKnown
+ }
+
+ /**
+ * Logs an info level message with details of how to add a caller to the allowed callers list
+ * when the app is debuggable.
+ */
+ private fun logUnknownCaller(callerPackageInfo: CallerPackageInfo) {
+ if (BuildConfig.DEBUG) {
+ Log.w(TAG, "Unknown caller $callerPackageInfo")
+ }
+ }
+
+ /**
+ * Builds a [CallerPackageInfo] for a given package that can be used for all the
+ * various checks that are performed before allowing an app to connect to a
+ * [MediaBrowserServiceCompat].
+ */
+ private fun buildCallerInfo(callingPackage: String): CallerPackageInfo? {
+ val packageInfo = getPackageInfo(callingPackage) ?: return null
+
+ val appName = packageInfo.applicationInfo?.loadLabel(packageManager).toString()
+ val uid = packageInfo.applicationInfo?.uid ?: -1
+ val signature = getSignature(packageInfo)
+
+ val requestedPermissions = packageInfo.requestedPermissions?.asSequence().orEmpty()
+ val permissionFlags = packageInfo.requestedPermissionsFlags?.asSequence().orEmpty()
+ val activePermissions = (requestedPermissions zip permissionFlags)
+ .filter { (permission, flag) -> flag and REQUESTED_PERMISSION_GRANTED != 0 }
+ .mapTo(mutableSetOf()) { (permission, flag) -> permission }
+
+ return CallerPackageInfo(appName, callingPackage, uid, signature, activePermissions.toSet())
+ }
+
+ /**
+ * Looks up the [PackageInfo] for a package name.
+ * This requests both the signatures (for checking if an app is on the allow list) and
+ * the app's permissions, which allow for more flexibility in the allow list.
+ *
+ * @return [PackageInfo] for the package name or null if it's not found.
+ */
+ @Suppress("deprecation")
+ @SuppressLint("PackageManagerGetSignatures")
+ private fun getPackageInfo(callingPackage: String): PackageInfo? =
+ packageManager.getPackageInfo(
+ callingPackage,
+ PackageManager.GET_SIGNATURES or PackageManager.GET_PERMISSIONS
+ )
+
+ /**
+ * Gets the signature of a given package's [PackageInfo].
+ *
+ * The "signature" is a SHA-256 hash of the public key of the signing certificate used by
+ * the app.
+ *
+ * If the app is not found, or if the app does not have exactly one signature, this method
+ * returns `null` as the signature.
+ */
+ @Suppress("deprecation")
+ private fun getSignature(packageInfo: PackageInfo): String? =
+ if (packageInfo.signatures == null || packageInfo.signatures!!.size != 1) {
+ // Security best practices dictate that an app should be signed with exactly one (1)
+ // signature. Because of this, if there are multiple signatures, reject it.
+ null
+ } else {
+ val certificate = packageInfo.signatures!![0].toByteArray()
+ getSignatureSha256(certificate)
+ }
+
+ /**
+ * Finds the Android platform signing key signature. This key is never null.
+ */
+ private fun getSystemSignature(): String =
+ getPackageInfo(ANDROID_PLATFORM)?.let { platformInfo ->
+ getSignature(platformInfo)
+ } ?: throw IllegalStateException("Platform signature not found")
+
+ /**
+ * Creates a SHA-256 signature given a certificate byte array.
+ */
+ private fun getSignatureSha256(certificate: ByteArray): String {
+ val md: MessageDigest
+ try {
+ md = MessageDigest.getInstance("SHA256")
+ } catch (noSuchAlgorithmException: NoSuchAlgorithmException) {
+ Log.e(TAG, "No such algorithm: $noSuchAlgorithmException")
+ throw RuntimeException("Could not find SHA256 hash algorithm", noSuchAlgorithmException)
+ }
+ md.update(certificate)
+
+ // This code takes the byte array generated by `md.digest()` and joins each of the bytes
+ // to a string, applying the string format `%02x` on each digit before it's appended, with
+ // a colon (':') between each of the items.
+ // For example: input=[0,2,4,6,8,10,12], output="00:02:04:06:08:0a:0c"
+ return md.digest().joinToString(":") { String.format("%02x", it) }
+ }
+
+ /**
+ * Convenience class to hold all of the information about an app that's being checked
+ * to see if it's a known caller.
+ */
+ private data class CallerPackageInfo(
+ val name: String,
+ val packageName: String,
+ val uid: Int,
+ val signature: String?,
+ val permissions: Set
+ )
+}
+
+private const val TAG = "PackageValidator"
+private const val ANDROID_PLATFORM = "android"
diff --git a/app/src/main/java/org/schabi/newpipe/player/mediasession/MediaSessionPlayerUi.java b/app/src/main/java/org/schabi/newpipe/player/mediasession/MediaSessionPlayerUi.java
index 085da5eb7..850dd02e3 100644
--- a/app/src/main/java/org/schabi/newpipe/player/mediasession/MediaSessionPlayerUi.java
+++ b/app/src/main/java/org/schabi/newpipe/player/mediasession/MediaSessionPlayerUi.java
@@ -124,8 +124,10 @@ public class MediaSessionPlayerUi extends PlayerUi
MediaButtonReceiver.handleIntent(mediaSession, intent);
}
- public Optional getSessionToken() {
- return Optional.ofNullable(mediaSession).map(MediaSessionCompat::getSessionToken);
+
+ @NonNull
+ public MediaSessionCompat.Token getSessionToken() {
+ return mediaSession.getSessionToken();
}
@@ -138,7 +140,10 @@ public class MediaSessionPlayerUi extends PlayerUi
public void play() {
player.play();
// hide the player controls even if the play command came from the media session
- player.UIs().getOpt(VideoPlayerUi.class).ifPresent(ui -> ui.hideControls(0, 0));
+ final VideoPlayerUi ui = player.UIs().get(VideoPlayerUi.class);
+ if (ui != null) {
+ ui.hideControls(0, 0);
+ }
}
@Override
diff --git a/app/src/main/java/org/schabi/newpipe/player/notification/NotificationUtil.java b/app/src/main/java/org/schabi/newpipe/player/notification/NotificationUtil.java
index 5658693f2..cc3889973 100644
--- a/app/src/main/java/org/schabi/newpipe/player/notification/NotificationUtil.java
+++ b/app/src/main/java/org/schabi/newpipe/player/notification/NotificationUtil.java
@@ -101,10 +101,10 @@ public final class NotificationUtil {
final int[] compactSlots = initializeNotificationSlots();
mediaStyle.setShowActionsInCompactView(compactSlots);
}
- player.UIs()
- .getOpt(MediaSessionPlayerUi.class)
- .flatMap(MediaSessionPlayerUi::getSessionToken)
- .ifPresent(mediaStyle::setMediaSession);
+ @Nullable final MediaSessionPlayerUi ui = player.UIs().get(MediaSessionPlayerUi.class);
+ if (ui != null) {
+ mediaStyle.setMediaSession(ui.getSessionToken());
+ }
// setup notification builder
builder.setStyle(mediaStyle)
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 88d7145bc..9092906fa 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
@@ -38,9 +38,9 @@ import io.reactivex.rxjava3.internal.subscriptions.EmptySubscription;
import io.reactivex.rxjava3.schedulers.Schedulers;
import io.reactivex.rxjava3.subjects.PublishSubject;
+import static org.schabi.newpipe.BuildConfig.DEBUG;
import static org.schabi.newpipe.player.mediasource.FailedMediaSource.MediaSourceResolutionException;
import static org.schabi.newpipe.player.mediasource.FailedMediaSource.StreamInfoLoadException;
-import static org.schabi.newpipe.player.playqueue.PlayQueue.DEBUG;
import static org.schabi.newpipe.util.ServiceHelper.getCacheExpirationMillis;
public class MediaSourceManager {
diff --git a/app/src/main/java/org/schabi/newpipe/player/playqueue/AbstractInfoPlayQueue.java b/app/src/main/java/org/schabi/newpipe/player/playqueue/AbstractInfoPlayQueue.java
index dbfac5cca..02bb6b5ba 100644
--- a/app/src/main/java/org/schabi/newpipe/player/playqueue/AbstractInfoPlayQueue.java
+++ b/app/src/main/java/org/schabi/newpipe/player/playqueue/AbstractInfoPlayQueue.java
@@ -3,6 +3,7 @@ package org.schabi.newpipe.player.playqueue;
import android.util.Log;
import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.extractor.ListExtractor;
@@ -23,6 +24,7 @@ abstract class AbstractInfoPlayQueue>
final int serviceId;
final String baseUrl;
+ @Nullable
Page nextPage;
private transient Disposable fetchReactor;
diff --git a/app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueue.java b/app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueue.java
deleted file mode 100644
index cfa2ab316..000000000
--- a/app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueue.java
+++ /dev/null
@@ -1,561 +0,0 @@
-package org.schabi.newpipe.player.playqueue;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
-import org.schabi.newpipe.MainActivity;
-import org.schabi.newpipe.player.playqueue.events.AppendEvent;
-import org.schabi.newpipe.player.playqueue.events.ErrorEvent;
-import org.schabi.newpipe.player.playqueue.events.InitEvent;
-import org.schabi.newpipe.player.playqueue.events.MoveEvent;
-import org.schabi.newpipe.player.playqueue.events.PlayQueueEvent;
-import org.schabi.newpipe.player.playqueue.events.RecoveryEvent;
-import org.schabi.newpipe.player.playqueue.events.RemoveEvent;
-import org.schabi.newpipe.player.playqueue.events.ReorderEvent;
-import org.schabi.newpipe.player.playqueue.events.SelectEvent;
-
-import java.io.Serializable;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-import java.util.concurrent.atomic.AtomicInteger;
-
-import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
-import io.reactivex.rxjava3.core.BackpressureStrategy;
-import io.reactivex.rxjava3.core.Flowable;
-import io.reactivex.rxjava3.subjects.BehaviorSubject;
-
-/**
- * PlayQueue is responsible for keeping track of a list of streams and the index of
- * the stream that should be currently playing.
- *
- * This class contains basic manipulation of a playlist while also functions as a
- * message bus, providing all listeners with new updates to the play queue.
- *
- *
- * This class can be serialized for passing intents, but in order to start the
- * message bus, it must be initialized.
- *
- */
-public abstract class PlayQueue implements Serializable {
- public static final boolean DEBUG = MainActivity.DEBUG;
- @NonNull
- private final AtomicInteger queueIndex;
- private final List history = new ArrayList<>();
-
- private List backup;
- private List streams;
-
- private transient BehaviorSubject eventBroadcast;
- private transient Flowable broadcastReceiver;
- private transient boolean disposed = false;
-
- PlayQueue(final int index, final List startWith) {
- streams = new ArrayList<>(startWith);
-
- if (streams.size() > index) {
- history.add(streams.get(index));
- }
-
- queueIndex = new AtomicInteger(index);
- }
-
- /*//////////////////////////////////////////////////////////////////////////
- // Playlist actions
- //////////////////////////////////////////////////////////////////////////*/
-
- /**
- * Initializes the play queue message buses.
- *
- * Also starts a self reporter for logging if debug mode is enabled.
- *
- */
- public void init() {
- eventBroadcast = BehaviorSubject.create();
-
- broadcastReceiver = eventBroadcast.toFlowable(BackpressureStrategy.BUFFER)
- .observeOn(AndroidSchedulers.mainThread())
- .startWithItem(new InitEvent());
- }
-
- /**
- * Dispose the play queue by stopping all message buses.
- */
- public void dispose() {
- if (eventBroadcast != null) {
- eventBroadcast.onComplete();
- }
-
- eventBroadcast = null;
- broadcastReceiver = null;
- disposed = true;
- }
-
- /**
- * Checks if the queue is complete.
- *
- * A queue is complete if it has loaded all items in an external playlist
- * single stream or local queues are always complete.
- *
- *
- * @return whether the queue is complete
- */
- public abstract boolean isComplete();
-
- /**
- * Load partial queue in the background, does nothing if the queue is complete.
- */
- public abstract void fetch();
-
- /*//////////////////////////////////////////////////////////////////////////
- // Readonly ops
- //////////////////////////////////////////////////////////////////////////*/
-
- /**
- * @return the current index that should be played
- */
- public int getIndex() {
- return queueIndex.get();
- }
-
- /**
- * Changes the current playing index to a new index.
- *
- * This method is guarded using in a circular manner for index exceeding the play queue size.
- *
- *
- * Will emit a {@link SelectEvent} if the index is not the current playing index.
- *
- *
- * @param index the index to be set
- */
- public synchronized void setIndex(final int index) {
- final int oldIndex = getIndex();
-
- final int newIndex;
-
- if (index < 0) {
- newIndex = 0;
- } else if (index < streams.size()) {
- // Regular assignment for index in bounds
- newIndex = index;
- } else if (streams.isEmpty()) {
- // Out of bounds from here on
- // Need to check if stream is empty to prevent arithmetic error and negative index
- newIndex = 0;
- } else if (isComplete()) {
- // Circular indexing
- newIndex = index % streams.size();
- } else {
- // Index of last element
- newIndex = streams.size() - 1;
- }
-
- queueIndex.set(newIndex);
-
- if (oldIndex != newIndex) {
- history.add(streams.get(newIndex));
- }
-
- /*
- TODO: Documentation states that a SelectEvent will only be emitted if the new index is...
- different from the old one but this is emitted regardless? Not sure what this what it does
- exactly so I won't touch it
- */
- broadcast(new SelectEvent(oldIndex, newIndex));
- }
-
- /**
- * @return the current item that should be played, or null if the queue is empty
- */
- @Nullable
- public PlayQueueItem getItem() {
- return getItem(getIndex());
- }
-
- /**
- * @param index the index of the item to return
- * @return the item at the given index, or null if the index is out of bounds
- */
- @Nullable
- public PlayQueueItem getItem(final int index) {
- if (index < 0 || index >= streams.size()) {
- return null;
- }
- return streams.get(index);
- }
-
- /**
- * Returns the index of the given item using referential equality.
- * May be null despite play queue contains identical item.
- *
- * @param item the item to find the index of
- * @return the index of the given item
- */
- public int indexOf(@NonNull final PlayQueueItem item) {
- return streams.indexOf(item);
- }
-
- /**
- * @return the current size of play queue.
- */
- public int size() {
- return streams.size();
- }
-
- /**
- * Checks if the play queue is empty.
- *
- * @return whether the play queue is empty
- */
- public boolean isEmpty() {
- return streams.isEmpty();
- }
-
- /**
- * Determines if the current play queue is shuffled.
- *
- * @return whether the play queue is shuffled
- */
- public boolean isShuffled() {
- return backup != null;
- }
-
- /**
- * @return an immutable view of the play queue
- */
- @NonNull
- public List getStreams() {
- return Collections.unmodifiableList(streams);
- }
-
- /*//////////////////////////////////////////////////////////////////////////
- // Write ops
- //////////////////////////////////////////////////////////////////////////*/
-
- /**
- * Returns the play queue's update broadcast.
- * May be null if the play queue message bus is not initialized.
- *
- * @return the play queue's update broadcast
- */
- @Nullable
- public Flowable getBroadcastReceiver() {
- return broadcastReceiver;
- }
-
- /**
- * Changes the current playing index by an offset amount.
- *
- * Will emit a {@link SelectEvent} if offset is non-zero.
- *
- *
- * @param offset the offset relative to the current index
- */
- public synchronized void offsetIndex(final int offset) {
- setIndex(getIndex() + offset);
- }
-
- /**
- * Notifies that a change has occurred.
- */
- public synchronized void notifyChange() {
- broadcast(new AppendEvent(0));
- }
-
- /**
- * Appends the given {@link PlayQueueItem}s to the current play queue.
- *
- * If the play queue is shuffled, then append the items to the backup queue as is and
- * append the shuffle items to the play queue.
- *
- *
- * Will emit a {@link AppendEvent} on any given context.
- *
- *
- * @param items {@link PlayQueueItem}s to append
- */
- public synchronized void append(@NonNull final List items) {
- final List itemList = new ArrayList<>(items);
-
- if (isShuffled()) {
- backup.addAll(itemList);
- Collections.shuffle(itemList);
- }
- if (!streams.isEmpty() && streams.get(streams.size() - 1).isAutoQueued()
- && !itemList.get(0).isAutoQueued()) {
- streams.remove(streams.size() - 1);
- }
- streams.addAll(itemList);
-
- broadcast(new AppendEvent(itemList.size()));
- }
-
- /**
- * Removes the item at the given index from the play queue.
- *
- * The current playing index will decrement if it is greater than the index being removed.
- * On cases where the current playing index exceeds the playlist range, it is set to 0.
- *
- *
- * Will emit a {@link RemoveEvent} if the index is within the play queue index range.
- *
- *
- * @param index the index of the item to remove
- */
- public synchronized void remove(final int index) {
- if (index >= streams.size() || index < 0) {
- return;
- }
- removeInternal(index);
- broadcast(new RemoveEvent(index, getIndex()));
- }
-
- /**
- * Report an exception for the item at the current index in order and skip to the next one
- *
- * This is done as a separate event as the underlying manager may have
- * different implementation regarding exceptions.
- *
- */
- public synchronized void error() {
- final int oldIndex = getIndex();
- queueIndex.incrementAndGet();
- if (streams.size() > queueIndex.get()) {
- history.add(streams.get(queueIndex.get()));
- }
- broadcast(new ErrorEvent(oldIndex, getIndex()));
- }
-
- private synchronized void removeInternal(final int removeIndex) {
- final int currentIndex = queueIndex.get();
- final int size = size();
-
- if (currentIndex > removeIndex) {
- queueIndex.decrementAndGet();
-
- } else if (currentIndex >= size) {
- queueIndex.set(currentIndex % (size - 1));
-
- } else if (currentIndex == removeIndex && currentIndex == size - 1) {
- queueIndex.set(0);
- }
-
- if (backup != null) {
- backup.remove(getItem(removeIndex));
- }
-
- history.remove(streams.remove(removeIndex));
- if (streams.size() > queueIndex.get()) {
- history.add(streams.get(queueIndex.get()));
- }
- }
-
- /**
- * Moves a queue item at the source index to the target index.
- *
- * If the item being moved is the currently playing, then the current playing index is set
- * to that of the target.
- * If the moved item is not the currently playing and moves to an index AFTER the
- * current playing index, then the current playing index is decremented.
- * Vice versa if the an item after the currently playing is moved BEFORE.
- *
- *
- * @param source the original index of the item
- * @param target the new index of the item
- */
- public synchronized void move(final int source, final int target) {
- if (source < 0 || target < 0) {
- return;
- }
- if (source >= streams.size() || target >= streams.size()) {
- return;
- }
-
- final int current = getIndex();
- if (source == current) {
- queueIndex.set(target);
- } else if (source < current && target >= current) {
- queueIndex.decrementAndGet();
- } else if (source > current && target <= current) {
- queueIndex.incrementAndGet();
- }
-
- final PlayQueueItem playQueueItem = streams.remove(source);
- playQueueItem.setAutoQueued(false);
- streams.add(target, playQueueItem);
- broadcast(new MoveEvent(source, target));
- }
-
- /**
- * Sets the recovery record of the item at the index.
- *
- * Broadcasts a recovery event.
- *
- *
- * @param index index of the item
- * @param position the recovery position
- */
- public synchronized void setRecovery(final int index, final long position) {
- if (index < 0 || index >= streams.size()) {
- return;
- }
-
- streams.get(index).setRecoveryPosition(position);
- broadcast(new RecoveryEvent(index, position));
- }
-
- /**
- * Revoke the recovery record of the item at the index.
- *
- * Broadcasts a recovery event.
- *
- *
- * @param index index of the item
- */
- public synchronized void unsetRecovery(final int index) {
- setRecovery(index, PlayQueueItem.RECOVERY_UNSET);
- }
-
- /**
- * Shuffles the current play queue
- *
- * This method first backs up the existing play queue and item being played. Then a newly
- * shuffled play queue will be generated along with currently playing item placed at the
- * beginning of the queue. This item will also be added to the history.
- *
- *
- * Will emit a {@link ReorderEvent} if shuffled.
- *
- *
- * @implNote Does nothing if the queue has a size <= 2 (the currently playing video must stay on
- * top, so shuffling a size-2 list does nothing)
- */
- public synchronized void shuffle() {
- // Create a backup if it doesn't already exist
- // Note: The backup-list has to be created at all cost (even when size <= 2).
- // Otherwise it's not possible to enter shuffle-mode!
- if (backup == null) {
- backup = new ArrayList<>(streams);
- }
- // Can't shuffle a list that's empty or only has one element
- if (size() <= 2) {
- return;
- }
-
- final int originalIndex = getIndex();
- final PlayQueueItem currentItem = getItem();
-
- Collections.shuffle(streams);
-
- // Move currentItem to the head of the queue
- streams.remove(currentItem);
- streams.add(0, currentItem);
- queueIndex.set(0);
-
- history.add(currentItem);
-
- broadcast(new ReorderEvent(originalIndex, 0));
- }
-
- /**
- * Unshuffles the current play queue if a backup play queue exists.
- *
- * This method undoes shuffling and index will be set to the previously playing item if found,
- * otherwise, the index will reset to 0.
- *
- *
- * Will emit a {@link ReorderEvent} if a backup exists.
- *
- */
- public synchronized void unshuffle() {
- if (backup == null) {
- return;
- }
- final int originIndex = getIndex();
- final PlayQueueItem current = getItem();
-
- streams = backup;
- backup = null;
-
- final int newIndex = streams.indexOf(current);
- if (newIndex != -1) {
- queueIndex.set(newIndex);
- } else {
- queueIndex.set(0);
- }
- if (streams.size() > queueIndex.get()) {
- history.add(streams.get(queueIndex.get()));
- }
-
- broadcast(new ReorderEvent(originIndex, queueIndex.get()));
- }
-
- /**
- * Selects previous played item.
- *
- * This method removes currently playing item from history and
- * starts playing the last item from history if it exists
- *
- * @return true if history is not empty and the item can be played
- * */
- public synchronized boolean previous() {
- if (history.size() <= 1) {
- return false;
- }
-
- history.remove(history.size() - 1);
-
- final PlayQueueItem last = history.remove(history.size() - 1);
- setIndex(indexOf(last));
-
- return true;
- }
-
- /*
- * Compares two PlayQueues. Useful when a user switches players but queue is the same so
- * we don't have to do anything with new queue.
- * This method also gives a chance to track history of items in a queue in
- * VideoDetailFragment without duplicating items from two identical queues
- */
- public boolean equalStreams(@Nullable final PlayQueue other) {
- if (other == null) {
- return false;
- }
- if (size() != other.size()) {
- return false;
- }
- for (int i = 0; i < size(); i++) {
- final PlayQueueItem stream = streams.get(i);
- final PlayQueueItem otherStream = other.streams.get(i);
- // Check is based on serviceId and URL
- if (stream.getServiceId() != otherStream.getServiceId()
- || !stream.getUrl().equals(otherStream.getUrl())) {
- return false;
- }
- }
- return true;
- }
-
- public boolean equalStreamsAndIndex(@Nullable final PlayQueue other) {
- if (equalStreams(other)) {
- //noinspection ConstantConditions
- return other.getIndex() == getIndex(); //NOSONAR: other is not null
- }
- return false;
- }
-
- public boolean isDisposed() {
- return disposed;
- }
- /*//////////////////////////////////////////////////////////////////////////
- // Rx Broadcast
- //////////////////////////////////////////////////////////////////////////*/
-
- private void broadcast(@NonNull final PlayQueueEvent event) {
- if (eventBroadcast != null) {
- eventBroadcast.onNext(event);
- }
- }
-}
-
diff --git a/app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueue.kt b/app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueue.kt
new file mode 100644
index 000000000..1ae7e5cdb
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueue.kt
@@ -0,0 +1,497 @@
+package org.schabi.newpipe.player.playqueue
+
+import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
+import io.reactivex.rxjava3.core.BackpressureStrategy
+import io.reactivex.rxjava3.core.Flowable
+import io.reactivex.rxjava3.subjects.BehaviorSubject
+import org.schabi.newpipe.player.playqueue.events.AppendEvent
+import org.schabi.newpipe.player.playqueue.events.ErrorEvent
+import org.schabi.newpipe.player.playqueue.events.InitEvent
+import org.schabi.newpipe.player.playqueue.events.MoveEvent
+import org.schabi.newpipe.player.playqueue.events.PlayQueueEvent
+import org.schabi.newpipe.player.playqueue.events.RecoveryEvent
+import org.schabi.newpipe.player.playqueue.events.RemoveEvent
+import org.schabi.newpipe.player.playqueue.events.ReorderEvent
+import org.schabi.newpipe.player.playqueue.events.SelectEvent
+import java.io.Serializable
+import java.util.Collections
+import java.util.concurrent.atomic.AtomicInteger
+
+/**
+ * PlayQueue is responsible for keeping track of a list of streams and the index of
+ * the stream that should be currently playing.
+ *
+ * This class contains basic manipulation of a playlist while also functions as a
+ * message bus, providing all listeners with new updates to the play queue.
+ *
+ * This class can be serialized for passing intents, but in order to start the
+ * message bus, it must be initialized.
+ */
+abstract class PlayQueue internal constructor(
+ index: Int,
+ startWith: List,
+) : Serializable {
+ private val queueIndex = AtomicInteger(index)
+ private val history = mutableListOf()
+ private var backup = mutableListOf()
+ private var streams = startWith.toMutableList()
+
+ @Transient
+ private var eventBroadcast: BehaviorSubject? = null
+
+ /**
+ * Returns the play queue's update broadcast.
+ * May be null if the play queue message bus is not initialized.
+ *
+ * @return the play queue's update broadcast
+ */
+ @Transient
+ var broadcastReceiver: Flowable? = null
+ private set
+
+ @Transient
+ var isDisposed: Boolean = false
+ private set
+
+ init {
+ if (streams.size > index) {
+ history.add(streams[index])
+ }
+ }
+
+ /*//////////////////////////////////////////////////////////////////////////
+ // Playlist actions
+ ////////////////////////////////////////////////////////////////////////// */
+
+ /**
+ * Initializes the play queue message buses.
+ *
+ * Also starts a self reporter for logging if debug mode is enabled.
+ */
+ fun init() {
+ eventBroadcast = BehaviorSubject.create()
+
+ broadcastReceiver =
+ eventBroadcast!!
+ .toFlowable(BackpressureStrategy.BUFFER)
+ .observeOn(AndroidSchedulers.mainThread())
+ .startWithItem(InitEvent())
+ }
+
+ /**
+ * Dispose the play queue by stopping all message buses.
+ */
+ open fun dispose() {
+ eventBroadcast?.onComplete()
+ eventBroadcast = null
+ broadcastReceiver = null
+ this.isDisposed = true
+ }
+
+ /**
+ * Checks if the queue is complete.
+ *
+ * A queue is complete if it has loaded all items in an external playlist
+ * single stream or local queues are always complete.
+ *
+ * @return whether the queue is complete
+ */
+ abstract val isComplete: Boolean
+
+ /**
+ * Load partial queue in the background, does nothing if the queue is complete.
+ */
+ abstract fun fetch()
+
+ /*//////////////////////////////////////////////////////////////////////////
+ // Readonly ops
+ ////////////////////////////////////////////////////////////////////////// */
+ @set:Synchronized
+ var index: Int = 0
+ /**
+ * @return the current index that should be played
+ */
+ get() = queueIndex.get()
+
+ /**
+ * Changes the current playing index to a new index.
+ *
+ * This method is guarded using in a circular manner for index exceeding the play queue size.
+ *
+ * Will emit a [SelectEvent] if the index is not the current playing index.
+ *
+ * @param index the index to be set
+ */
+ set(index) {
+ val oldIndex = field
+
+ val newIndex: Int
+
+ if (index < 0) {
+ newIndex = 0
+ } else if (index < streams.size) {
+ // Regular assignment for index in bounds
+ newIndex = index
+ } else if (streams.isEmpty()) {
+ // Out of bounds from here on
+ // Need to check if stream is empty to prevent arithmetic error and negative index
+ newIndex = 0
+ } else if (this.isComplete) {
+ // Circular indexing
+ newIndex = index % streams.size
+ } else {
+ // Index of last element
+ newIndex = streams.size - 1
+ }
+
+ queueIndex.set(newIndex)
+
+ if (oldIndex != newIndex) {
+ history.add(streams[newIndex])
+ }
+
+ /*
+ TODO: Documentation states that a SelectEvent will only be emitted if the new index is...
+ different from the old one but this is emitted regardless? Not sure what this what it does
+ exactly so I won't touch it
+ */
+ broadcast(SelectEvent(oldIndex, newIndex))
+ }
+
+ /**
+ * @return the current item that should be played, or null if the queue is empty
+ */
+ val item get() = getItem(this.index)
+
+ /**
+ * @param index the index of the item to return
+ * @return the item at the given index, or null if the index is out of bounds
+ */
+ fun getItem(index: Int) = streams.getOrNull(index)
+
+ /**
+ * Returns the index of the given item using referential equality.
+ * May be null despite play queue contains identical item.
+ *
+ * @param item the item to find the index of
+ * @return the index of the given item
+ */
+ fun indexOf(item: PlayQueueItem): Int = streams.indexOf(item)
+
+ /**
+ * @return the current size of play queue.
+ */
+ fun size(): Int = streams.size
+
+ /**
+ * Checks if the play queue is empty.
+ *
+ * @return whether the play queue is empty
+ */
+ val isEmpty: Boolean
+ get() = streams.isEmpty()
+
+ /**
+ * Determines if the current play queue is shuffled.
+ *
+ * @return whether the play queue is shuffled
+ */
+ val isShuffled: Boolean
+ get() = backup.isNotEmpty()
+
+ /**
+ * @return an immutable view of the play queue
+ */
+ fun getStreams(): List = Collections.unmodifiableList(streams)
+
+ /*//////////////////////////////////////////////////////////////////////////
+ // Write ops
+ ////////////////////////////////////////////////////////////////////////// */
+
+ /**
+ * Changes the current playing index by an offset amount.
+ *
+ * Will emit a [SelectEvent] if offset is non-zero.
+ *
+ * @param offset the offset relative to the current index
+ */
+ @Synchronized
+ fun offsetIndex(offset: Int) {
+ this.index += offset
+ }
+
+ /**
+ * Notifies that a change has occurred.
+ */
+ @Synchronized
+ fun notifyChange() {
+ broadcast(AppendEvent(0))
+ }
+
+ /**
+ * Appends the given [PlayQueueItem]s to the current play queue.
+ *
+ * If the play queue is shuffled, then append the items to the backup queue as is and
+ * append the shuffle items to the play queue.
+ *
+ * Will emit a [AppendEvent] on any given context.
+ *
+ * @param items [PlayQueueItem]s to append
+ */
+ @Synchronized
+ fun append(items: List) {
+ val itemList = items.toMutableList()
+
+ if (this.isShuffled) {
+ backup.addAll(itemList)
+ itemList.shuffle()
+ }
+ if (!streams.isEmpty() && streams.last().isAutoQueued && !itemList[0].isAutoQueued) {
+ streams.removeAt(streams.lastIndex)
+ }
+ streams.addAll(itemList)
+
+ broadcast(AppendEvent(itemList.size))
+ }
+
+ /**
+ * Removes the item at the given index from the play queue.
+ *
+ * The current playing index will decrement if it is greater than the index being removed.
+ * On cases where the current playing index exceeds the playlist range, it is set to 0.
+ *
+ * Will emit a [RemoveEvent] if the index is within the play queue index range.
+ *
+ * @param index the index of the item to remove
+ */
+ @Synchronized
+ fun remove(index: Int) {
+ if (index >= streams.size || index < 0) {
+ return
+ }
+ removeInternal(index)
+ broadcast(RemoveEvent(index, this.index))
+ }
+
+ /**
+ * Report an exception for the item at the current index in order and skip to the next one
+ *
+ * This is done as a separate event as the underlying manager may have
+ * different implementation regarding exceptions.
+ */
+ @Synchronized
+ fun error() {
+ val oldIndex = this.index
+ queueIndex.incrementAndGet()
+ if (streams.size > queueIndex.get()) {
+ history.add(streams[queueIndex.get()])
+ }
+ broadcast(ErrorEvent(oldIndex, this.index))
+ }
+
+ @Synchronized
+ private fun removeInternal(removeIndex: Int) {
+ val currentIndex = queueIndex.get()
+ val size = size()
+
+ if (currentIndex > removeIndex) {
+ queueIndex.decrementAndGet()
+ } else if (currentIndex >= size) {
+ queueIndex.set(currentIndex % (size - 1))
+ } else if (currentIndex == removeIndex && currentIndex == size - 1) {
+ queueIndex.set(0)
+ }
+
+ backup.remove(getItem(removeIndex)!!)
+
+ history.remove(streams.removeAt(removeIndex))
+ if (streams.size > queueIndex.get()) {
+ history.add(streams[queueIndex.get()])
+ }
+ }
+
+ /**
+ * Moves a queue item at the source index to the target index.
+ *
+ * If the item being moved is the currently playing, then the current playing index is set
+ * to that of the target.
+ * If the moved item is not the currently playing and moves to an index **AFTER** the
+ * current playing index, then the current playing index is decremented.
+ * Vice versa if the an item after the currently playing is moved **BEFORE**.
+ *
+ * @param source the original index of the item
+ * @param target the new index of the item
+ */
+ @Synchronized
+ fun move(
+ source: Int,
+ target: Int,
+ ) {
+ if (source < 0 || target < 0) {
+ return
+ }
+ if (source >= streams.size || target >= streams.size) {
+ return
+ }
+
+ val current = this.index
+ if (source == current) {
+ queueIndex.set(target)
+ } else if (source < current && target >= current) {
+ queueIndex.decrementAndGet()
+ } else if (source > current && target <= current) {
+ queueIndex.incrementAndGet()
+ }
+
+ val playQueueItem = streams.removeAt(source)
+ playQueueItem.isAutoQueued = false
+ streams.add(target, playQueueItem)
+ broadcast(MoveEvent(source, target))
+ }
+
+ /**
+ * Sets the recovery record of the item at the index.
+ *
+ * Broadcasts a recovery event.
+ *
+ * @param index index of the item
+ * @param position the recovery position
+ */
+ @Synchronized
+ fun setRecovery(
+ index: Int,
+ position: Long,
+ ) {
+ streams.getOrNull(index)?.let {
+ it.recoveryPosition = position
+ broadcast(RecoveryEvent(index, position))
+ }
+ }
+
+ /**
+ * Revoke the recovery record of the item at the index.
+ *
+ * Broadcasts a recovery event.
+ *
+ * @param index index of the item
+ */
+ @Synchronized
+ fun unsetRecovery(index: Int) {
+ setRecovery(index, Long.Companion.MIN_VALUE)
+ }
+
+ /**
+ * Shuffles the current play queue
+ *
+ * This method first backs up the existing play queue and item being played. Then a newly
+ * shuffled play queue will be generated along with currently playing item placed at the
+ * beginning of the queue. This item will also be added to the history.
+ *
+ * Will emit a [ReorderEvent] if shuffled.
+ *
+ * @implNote Does nothing if the queue has a size <= 2 (the currently playing video must stay on
+ * top, so shuffling a size-2 list does nothing)
+ */
+ @Synchronized
+ fun shuffle() {
+ // Create a backup if it doesn't already exist
+ // Note: The backup-list has to be created at all cost (even when size <= 2).
+ // Otherwise it's not possible to enter shuffle-mode!
+ if (backup.isEmpty()) {
+ backup = streams.toMutableList()
+ }
+ // Can't shuffle a list that's empty or only has one element
+ if (size() <= 2) {
+ return
+ }
+
+ val originalIndex = this.index
+ val currentItem = this.item
+
+ streams.shuffle()
+
+ // Move currentItem to the head of the queue
+ streams.remove(currentItem!!)
+ streams.add(0, currentItem)
+ queueIndex.set(0)
+
+ history.add(currentItem)
+
+ broadcast(ReorderEvent(originalIndex, 0))
+ }
+
+ /**
+ * Unshuffles the current play queue if a backup play queue exists.
+ *
+ * This method undoes shuffling and index will be set to the previously playing item if found,
+ * otherwise, the index will reset to 0.
+ *
+ * Will emit a [ReorderEvent] if a backup exists.
+ */
+ @Synchronized
+ fun unshuffle() {
+ if (backup.isEmpty()) {
+ return
+ }
+ val originIndex = this.index
+ val current = this.item
+
+ streams = backup
+ backup = mutableListOf()
+
+ val newIndex = streams.indexOf(current!!)
+ if (newIndex != -1) {
+ queueIndex.set(newIndex)
+ } else {
+ queueIndex.set(0)
+ }
+ if (streams.size > queueIndex.get()) {
+ history.add(streams[queueIndex.get()])
+ }
+
+ broadcast(ReorderEvent(originIndex, queueIndex.get()))
+ }
+
+ /**
+ * Selects previous played item.
+ *
+ * This method removes currently playing item from history and
+ * starts playing the last item from history if it exists
+ *
+ * @return true if history is not empty and the item can be played
+ */
+ @Synchronized
+ fun previous(): Boolean {
+ if (history.size <= 1) {
+ return false
+ }
+
+ history.removeAt(history.size - 1)
+
+ val last = history.removeAt(history.size - 1)
+ this.index = indexOf(last)
+
+ return true
+ }
+
+ /*
+ * Compares two PlayQueues. Useful when a user switches players but queue is the same so
+ * we don't have to do anything with new queue.
+ * This method also gives a chance to track history of items in a queue in
+ * VideoDetailFragment without duplicating items from two identical queues
+ */
+ override fun equals(o: Any?): Boolean = o is PlayQueue && streams == o.streams
+
+ override fun hashCode(): Int = streams.hashCode()
+
+ fun equalStreamsAndIndex(other: PlayQueue?): Boolean {
+ return equals(other) && other!!.index == this.index // NOSONAR: other is not null
+ }
+
+ /*//////////////////////////////////////////////////////////////////////////
+ // Rx Broadcast
+ ////////////////////////////////////////////////////////////////////////// */
+ private fun broadcast(event: PlayQueueEvent) {
+ eventBroadcast?.onNext(event)
+ }
+}
diff --git a/app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueueItem.java b/app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueueItem.java
deleted file mode 100644
index 759c51267..000000000
--- a/app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueueItem.java
+++ /dev/null
@@ -1,142 +0,0 @@
-package org.schabi.newpipe.player.playqueue;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
-import org.schabi.newpipe.extractor.Image;
-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.util.ExtractorHelper;
-
-import java.io.Serializable;
-import java.util.List;
-
-import io.reactivex.rxjava3.core.Single;
-import io.reactivex.rxjava3.schedulers.Schedulers;
-
-public class PlayQueueItem implements Serializable {
- public static final long RECOVERY_UNSET = Long.MIN_VALUE;
- private static final String EMPTY_STRING = "";
-
- @NonNull
- private final String title;
- @NonNull
- private final String url;
- private final int serviceId;
- private final long duration;
- @NonNull
- private final List thumbnails;
- @NonNull
- private final String uploader;
- private final String uploaderUrl;
- @NonNull
- private final StreamType streamType;
-
- private boolean isAutoQueued;
-
- private long recoveryPosition;
- private Throwable error;
-
- PlayQueueItem(@NonNull final StreamInfo info) {
- this(info.getName(), info.getUrl(), info.getServiceId(), info.getDuration(),
- info.getThumbnails(), info.getUploaderName(),
- info.getUploaderUrl(), info.getStreamType());
-
- if (info.getStartPosition() > 0) {
- setRecoveryPosition(info.getStartPosition() * 1000);
- }
- }
-
- PlayQueueItem(@NonNull final StreamInfoItem item) {
- this(item.getName(), item.getUrl(), item.getServiceId(), item.getDuration(),
- item.getThumbnails(), item.getUploaderName(),
- item.getUploaderUrl(), item.getStreamType());
- }
-
- @SuppressWarnings("ParameterNumber")
- private PlayQueueItem(@Nullable final String name, @Nullable final String url,
- final int serviceId, final long duration,
- final List thumbnails, @Nullable final String uploader,
- final String uploaderUrl, @NonNull final StreamType streamType) {
- this.title = name != null ? name : EMPTY_STRING;
- this.url = url != null ? url : EMPTY_STRING;
- this.serviceId = serviceId;
- this.duration = duration;
- this.thumbnails = thumbnails;
- this.uploader = uploader != null ? uploader : EMPTY_STRING;
- this.uploaderUrl = uploaderUrl;
- this.streamType = streamType;
-
- this.recoveryPosition = RECOVERY_UNSET;
- }
-
- @NonNull
- public String getTitle() {
- return title;
- }
-
- @NonNull
- public String getUrl() {
- return url;
- }
-
- public int getServiceId() {
- return serviceId;
- }
-
- public long getDuration() {
- return duration;
- }
-
- @NonNull
- public List getThumbnails() {
- return thumbnails;
- }
-
- @NonNull
- public String getUploader() {
- return uploader;
- }
-
- public String getUploaderUrl() {
- return uploaderUrl;
- }
-
- @NonNull
- public StreamType getStreamType() {
- return streamType;
- }
-
- public long getRecoveryPosition() {
- return recoveryPosition;
- }
-
- /*package-private*/ void setRecoveryPosition(final long recoveryPosition) {
- this.recoveryPosition = recoveryPosition;
- }
-
- @Nullable
- public Throwable getError() {
- return error;
- }
-
- @NonNull
- public Single getStream() {
- return ExtractorHelper.getStreamInfo(this.serviceId, this.url, false)
- .subscribeOn(Schedulers.io())
- .doOnError(throwable -> error = throwable);
- }
-
- public boolean isAutoQueued() {
- return isAutoQueued;
- }
-
- ////////////////////////////////////////////////////////////////////////////
- // Item States, keep external access out
- ////////////////////////////////////////////////////////////////////////////
-
- public void setAutoQueued(final boolean autoQueued) {
- isAutoQueued = autoQueued;
- }
-}
diff --git a/app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueueItem.kt b/app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueueItem.kt
new file mode 100644
index 000000000..d6b4b0402
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueueItem.kt
@@ -0,0 +1,71 @@
+package org.schabi.newpipe.player.playqueue
+
+import io.reactivex.rxjava3.core.Single
+import io.reactivex.rxjava3.schedulers.Schedulers
+import org.schabi.newpipe.extractor.Image
+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.util.ExtractorHelper
+import java.io.Serializable
+import java.util.Objects
+
+class PlayQueueItem private constructor(
+ val title: String,
+ val url: String,
+ val serviceId: Int,
+ val duration: Long,
+ val thumbnails: List,
+ val uploader: String,
+ val uploaderUrl: String?,
+ val streamType: StreamType,
+) : Serializable {
+ //
+ // ////////////////////////////////////////////////////////////////////// */
+ // Item States, keep external access out
+ //
+ // ////////////////////////////////////////////////////////////////////// */
+ var isAutoQueued: Boolean = false
+
+ // package-private
+ var recoveryPosition = Long.Companion.MIN_VALUE
+ var error: Throwable? = null
+ private set
+
+ constructor(info: StreamInfo) : this(
+ info.name.orEmpty(),
+ info.url.orEmpty(),
+ info.serviceId,
+ info.duration,
+ info.thumbnails,
+ info.uploaderName.orEmpty(),
+ info.uploaderUrl,
+ info.streamType,
+ ) {
+ if (info.startPosition > 0) {
+ this.recoveryPosition = info.startPosition * 1000
+ }
+ }
+
+ constructor(item: StreamInfoItem) : this(
+ item.name.orEmpty(),
+ item.url.orEmpty(),
+ item.serviceId,
+ item.duration,
+ item.thumbnails,
+ item.uploaderName.orEmpty(),
+ item.uploaderUrl,
+ item.streamType,
+ )
+
+ val stream: Single
+ get() =
+ ExtractorHelper
+ .getStreamInfo(serviceId, url, false)
+ .subscribeOn(Schedulers.io())
+ .doOnError { throwable -> error = throwable }
+
+ override fun equals(o: Any?) = o is PlayQueueItem && serviceId == o.serviceId && url == o.url
+
+ override fun hashCode() = Objects.hash(url, serviceId)
+}
diff --git a/app/src/main/java/org/schabi/newpipe/player/ui/PlayerUiList.kt b/app/src/main/java/org/schabi/newpipe/player/ui/PlayerUiList.kt
index 190da81e6..5419027a5 100644
--- a/app/src/main/java/org/schabi/newpipe/player/ui/PlayerUiList.kt
+++ b/app/src/main/java/org/schabi/newpipe/player/ui/PlayerUiList.kt
@@ -1,25 +1,20 @@
package org.schabi.newpipe.player.ui
import org.schabi.newpipe.util.GuardedByMutex
-import java.util.Optional
+import kotlin.reflect.KClass
+import kotlin.reflect.safeCast
+/**
+ * Creates a [PlayerUiList] starting with the provided player uis. The provided player uis
+ * will not be prepared like those passed to [.addAndPrepare], because when
+ * the [PlayerUiList] constructor is called, the player is still not running and it
+ * wouldn't make sense to initialize uis then. Instead the player will initialize them by doing
+ * proper calls to [.call].
+ *
+ * @param initialPlayerUis the player uis this list should start with; the order will be kept
+ */
class PlayerUiList(vararg initialPlayerUis: PlayerUi) {
- private val playerUis = GuardedByMutex(mutableListOf())
-
- /**
- * Creates a [PlayerUiList] starting with the provided player uis. The provided player uis
- * will not be prepared like those passed to [.addAndPrepare], because when
- * the [PlayerUiList] constructor is called, the player is still not running and it
- * wouldn't make sense to initialize uis then. Instead the player will initialize them by doing
- * proper calls to [.call].
- *
- * @param initialPlayerUis the player uis this list should start with; the order will be kept
- */
- init {
- playerUis.runWithLockSync {
- lockData.addAll(listOf(*initialPlayerUis))
- }
- }
+ private val playerUis = GuardedByMutex(mutableListOf(*initialPlayerUis))
/**
* Adds the provided player ui to the list and calls on it the initialization functions that
@@ -83,30 +78,22 @@ class PlayerUiList(vararg initialPlayerUis: PlayerUi) {
* @param T the class type parameter
* @return the first player UI of the required type found in the list, or null
*/
- fun get(playerUiType: Class): T? =
+ fun get(playerUiType: KClass): T? =
playerUis.runWithLockSync {
for (ui in lockData) {
if (playerUiType.isInstance(ui)) {
- when (val r = playerUiType.cast(ui)) {
- // try all UIs before returning null
- null -> continue
- else -> return@runWithLockSync r
- }
+ // try all UIs before returning null
+ playerUiType.safeCast(ui)?.let { return@runWithLockSync it }
}
}
return@runWithLockSync null
}
/**
- * @param playerUiType the class of the player UI to return;
- * the [Class.isInstance] method will be used, so even subclasses could be returned
- * @param T the class type parameter
- * @return the first player UI of the required type found in the list, or an empty
- * [Optional] otherwise
- */
- @Deprecated("use get", ReplaceWith("get(playerUiType)"))
- fun getOpt(playerUiType: Class): Optional =
- Optional.ofNullable(get(playerUiType))
+ * See [get] above
+ */
+ fun get(playerUiType: Class): T? =
+ get(playerUiType.kotlin)
/**
* Calls the provided consumer on all player UIs in the list, in order of addition.
diff --git a/app/src/main/java/org/schabi/newpipe/settings/BackupRestoreSettingsFragment.java b/app/src/main/java/org/schabi/newpipe/settings/BackupRestoreSettingsFragment.java
index 97df1549b..e794ace72 100644
--- a/app/src/main/java/org/schabi/newpipe/settings/BackupRestoreSettingsFragment.java
+++ b/app/src/main/java/org/schabi/newpipe/settings/BackupRestoreSettingsFragment.java
@@ -1,7 +1,6 @@
package org.schabi.newpipe.settings;
import static org.schabi.newpipe.extractor.utils.Utils.isBlank;
-import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
import android.app.Activity;
import android.app.AlertDialog;
@@ -17,7 +16,6 @@ import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
-import androidx.core.content.ContextCompat;
import androidx.preference.Preference;
import androidx.preference.PreferenceManager;
@@ -35,12 +33,10 @@ import org.schabi.newpipe.streams.io.StoredFileHelper;
import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.ZipHelper;
-import java.io.File;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
-import java.util.Objects;
public class BackupRestoreSettingsFragment extends BasePreferenceFragment {
@@ -61,13 +57,10 @@ public class BackupRestoreSettingsFragment extends BasePreferenceFragment {
@Override
public void onCreatePreferences(@Nullable final Bundle savedInstanceState,
@Nullable final String rootKey) {
- final File homeDir = ContextCompat.getDataDir(requireContext());
- Objects.requireNonNull(homeDir);
- manager = new ImportExportManager(new BackupFileLocator(homeDir));
+ manager = new ImportExportManager(new BackupFileLocator(requireContext()));
importExportDataPathKey = getString(R.string.import_export_data_path);
-
addPreferencesFromResourceRegistry();
final Preference importDataPreference = requirePreference(R.string.import_data);
@@ -126,7 +119,6 @@ public class BackupRestoreSettingsFragment extends BasePreferenceFragment {
}
private void requestExportPathResult(final ActivityResult result) {
- assureCorrectAppLanguage(requireContext());
if (result.getResultCode() == Activity.RESULT_OK && result.getData() != null) {
// will be saved only on success
final Uri lastExportDataUri = result.getData().getData();
@@ -139,7 +131,6 @@ public class BackupRestoreSettingsFragment extends BasePreferenceFragment {
}
private void requestImportPathResult(final ActivityResult result) {
- assureCorrectAppLanguage(requireContext());
if (result.getResultCode() == Activity.RESULT_OK && result.getData() != null) {
// will be saved only on success
final Uri lastImportDataUri = result.getData().getData();
@@ -183,9 +174,7 @@ public class BackupRestoreSettingsFragment extends BasePreferenceFragment {
}
try {
- if (!manager.ensureDbDirectoryExists()) {
- throw new IOException("Could not create databases dir");
- }
+ manager.ensureDbDirectoryExists();
// replace the current database
if (!manager.extractDb(file)) {
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 b998b6337..7e1f22776 100644
--- a/app/src/main/java/org/schabi/newpipe/settings/ContentSettingsFragment.java
+++ b/app/src/main/java/org/schabi/newpipe/settings/ContentSettingsFragment.java
@@ -10,13 +10,14 @@ import android.util.Log;
import android.widget.Toast;
import androidx.appcompat.app.AppCompatDelegate;
+import androidx.core.os.LocaleListCompat;
import androidx.preference.Preference;
import org.schabi.newpipe.DownloaderImpl;
import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.NewPipe;
-import org.schabi.newpipe.extractor.localization.ContentCountry;
-import org.schabi.newpipe.extractor.localization.Localization;
+import org.schabi.newpipe.player.helper.PlayerHelper;
+import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.image.ImageStrategy;
import org.schabi.newpipe.util.image.PreferredImageQuality;
@@ -27,26 +28,27 @@ import coil3.SingletonImageLoader;
public class ContentSettingsFragment extends BasePreferenceFragment {
private String youtubeRestrictedModeEnabledKey;
- private String initialLanguage;
-
@Override
public void onCreatePreferences(final Bundle savedInstanceState, final String rootKey) {
youtubeRestrictedModeEnabledKey = getString(R.string.youtube_restricted_mode_enabled);
addPreferencesFromResourceRegistry();
- initialLanguage = defaultPreferences.getString(getString(R.string.app_language_key), "en");
+ setupAppLanguagePreferences();
+ setupImageQualityPref();
+ }
+
+ private void setupAppLanguagePreferences() {
+ final Preference appLanguagePref = requirePreference(R.string.app_language_key);
+ // Android 13+ allows to set app specific languages
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ appLanguagePref.setVisible(false);
- if (Build.VERSION.SDK_INT >= 33) {
- requirePreference(R.string.app_language_key).setVisible(false);
final Preference newAppLanguagePref =
requirePreference(R.string.app_language_android_13_and_up_key);
newAppLanguagePref.setSummaryProvider(preference -> {
- final Locale customLocale = AppCompatDelegate.getApplicationLocales().get(0);
- if (customLocale != null) {
- return customLocale.getDisplayName();
- }
- return getString(R.string.systems_language);
+ final Locale loc = AppCompatDelegate.getApplicationLocales().get(0);
+ return loc != null ? loc.getDisplayName() : getString(R.string.systems_language);
});
newAppLanguagePref.setOnPreferenceClickListener(preference -> {
final Intent intent = new Intent(Settings.ACTION_APP_LOCALE_SETTINGS)
@@ -55,22 +57,32 @@ public class ContentSettingsFragment extends BasePreferenceFragment {
return true;
});
newAppLanguagePref.setVisible(true);
+ return;
}
- final Preference imageQualityPreference = requirePreference(R.string.image_quality_key);
- imageQualityPreference.setOnPreferenceChangeListener(
- (preference, newValue) -> {
- ImageStrategy.setPreferredImageQuality(PreferredImageQuality
- .fromPreferenceKey(requireContext(), (String) newValue));
- final var loader = SingletonImageLoader.get(preference.getContext());
- loader.getMemoryCache().clear();
- loader.getDiskCache().clear();
- Toast.makeText(preference.getContext(),
- R.string.thumbnail_cache_wipe_complete_notice, Toast.LENGTH_SHORT)
- .show();
+ appLanguagePref.setOnPreferenceChangeListener((preference, newValue) -> {
+ final String language = (String) newValue;
+ final String systemLang = getString(R.string.default_localization_key);
+ final String tag = systemLang.equals(language) ? null : language;
+ AppCompatDelegate.setApplicationLocales(LocaleListCompat.forLanguageTags(tag));
+ return true;
+ });
+ }
- return true;
- });
+ private void setupImageQualityPref() {
+ requirePreference(R.string.image_quality_key).setOnPreferenceChangeListener(
+ (preference, newValue) -> {
+ ImageStrategy.setPreferredImageQuality(PreferredImageQuality
+ .fromPreferenceKey(requireContext(), (String) newValue));
+ final var loader = SingletonImageLoader.get(preference.getContext());
+ loader.getMemoryCache().clear();
+ loader.getDiskCache().clear();
+ Toast.makeText(preference.getContext(),
+ R.string.thumbnail_cache_wipe_complete_notice, Toast.LENGTH_SHORT)
+ .show();
+
+ return true;
+ });
}
@Override
@@ -91,22 +103,10 @@ public class ContentSettingsFragment extends BasePreferenceFragment {
public void onDestroy() {
super.onDestroy();
- final String selectedLanguage =
- defaultPreferences.getString(getString(R.string.app_language_key), "en");
-
- if (!selectedLanguage.equals(initialLanguage)) {
- if (Build.VERSION.SDK_INT < 33) {
- Toast.makeText(
- requireContext(),
- R.string.localization_changes_requires_app_restart,
- Toast.LENGTH_LONG
- ).show();
- }
- final Localization selectedLocalization = org.schabi.newpipe.util.Localization
- .getPreferredLocalization(requireContext());
- final ContentCountry selectedContentCountry = org.schabi.newpipe.util.Localization
- .getPreferredContentCountry(requireContext());
- NewPipe.setupLocalization(selectedLocalization, selectedContentCountry);
- }
+ final Context context = requireContext();
+ NewPipe.setupLocalization(
+ Localization.getPreferredLocalization(context),
+ Localization.getPreferredContentCountry(context));
+ PlayerHelper.resetFormat();
}
}
diff --git a/app/src/main/java/org/schabi/newpipe/settings/DownloadSettingsFragment.java b/app/src/main/java/org/schabi/newpipe/settings/DownloadSettingsFragment.java
index ff7811af3..356dcd9b2 100644
--- a/app/src/main/java/org/schabi/newpipe/settings/DownloadSettingsFragment.java
+++ b/app/src/main/java/org/schabi/newpipe/settings/DownloadSettingsFragment.java
@@ -1,7 +1,5 @@
package org.schabi.newpipe.settings;
-import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
-
import android.app.Activity;
import android.content.ContentResolver;
import android.content.Context;
@@ -209,8 +207,6 @@ public class DownloadSettingsFragment extends BasePreferenceFragment {
}
private void requestDownloadPathResult(final ActivityResult result, final String key) {
- assureCorrectAppLanguage(getContext());
-
if (result.getResultCode() != Activity.RESULT_OK) {
return;
}
diff --git a/app/src/main/java/org/schabi/newpipe/settings/NewPipeSettings.java b/app/src/main/java/org/schabi/newpipe/settings/NewPipeSettings.java
index 9fe5240cc..5daa3ad82 100644
--- a/app/src/main/java/org/schabi/newpipe/settings/NewPipeSettings.java
+++ b/app/src/main/java/org/schabi/newpipe/settings/NewPipeSettings.java
@@ -13,6 +13,7 @@ import androidx.preference.PreferenceManager;
import org.schabi.newpipe.App;
import org.schabi.newpipe.R;
+import org.schabi.newpipe.settings.migration.MigrationManager;
import org.schabi.newpipe.util.DeviceUtils;
import java.io.File;
@@ -46,7 +47,7 @@ public final class NewPipeSettings {
public static void initSettings(final Context context) {
// first run migrations, then setDefaultValues, since the latter requires the correct types
- SettingMigrations.runMigrationsIfNeeded(context);
+ MigrationManager.runMigrationsIfNeeded(context);
// readAgain is true so that if new settings are added their default value is set
PreferenceManager.setDefaultValues(context, R.xml.main_settings, true);
diff --git a/app/src/main/java/org/schabi/newpipe/settings/SettingsActivity.java b/app/src/main/java/org/schabi/newpipe/settings/SettingsActivity.java
index 0d57ce174..d5089cb7d 100644
--- a/app/src/main/java/org/schabi/newpipe/settings/SettingsActivity.java
+++ b/app/src/main/java/org/schabi/newpipe/settings/SettingsActivity.java
@@ -1,7 +1,5 @@
package org.schabi.newpipe.settings;
-import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
-
import android.content.Context;
import android.os.Bundle;
import android.text.TextUtils;
@@ -89,7 +87,6 @@ public class SettingsActivity extends AppCompatActivity implements
@Override
protected void onCreate(final Bundle savedInstanceBundle) {
setTheme(ThemeHelper.getSettingsThemeStyle(this));
- assureCorrectAppLanguage(this);
super.onCreate(savedInstanceBundle);
Bridge.restoreInstanceState(this, savedInstanceBundle);
@@ -228,7 +225,6 @@ public class SettingsActivity extends AppCompatActivity implements
// Build search items
final Context searchContext = getApplicationContext();
- assureCorrectAppLanguage(searchContext);
final PreferenceParser parser = new PreferenceParser(searchContext, config);
final PreferenceSearcher searcher = new PreferenceSearcher(config);
diff --git a/app/src/main/java/org/schabi/newpipe/settings/export/BackupFileLocator.kt b/app/src/main/java/org/schabi/newpipe/settings/export/BackupFileLocator.kt
index c864e4a0d..38227e10e 100644
--- a/app/src/main/java/org/schabi/newpipe/settings/export/BackupFileLocator.kt
+++ b/app/src/main/java/org/schabi/newpipe/settings/export/BackupFileLocator.kt
@@ -1,11 +1,13 @@
package org.schabi.newpipe.settings.export
-import java.io.File
+import android.content.Context
+import java.nio.file.Path
+import kotlin.io.path.div
/**
* Locates specific files of NewPipe based on the home directory of the app.
*/
-class BackupFileLocator(private val homeDir: File) {
+class BackupFileLocator(context: Context) {
companion object {
const val FILE_NAME_DB = "newpipe.db"
@Deprecated(
@@ -16,13 +18,8 @@ class BackupFileLocator(private val homeDir: File) {
const val FILE_NAME_JSON_PREFS = "preferences.json"
}
- val dbDir by lazy { File(homeDir, "/databases") }
-
- val db by lazy { File(dbDir, FILE_NAME_DB) }
-
- val dbJournal by lazy { File(dbDir, "$FILE_NAME_DB-journal") }
-
- val dbShm by lazy { File(dbDir, "$FILE_NAME_DB-shm") }
-
- val dbWal by lazy { File(dbDir, "$FILE_NAME_DB-wal") }
+ val db: Path = context.getDatabasePath(FILE_NAME_DB).toPath()
+ val dbJournal: Path = db.resolveSibling("$FILE_NAME_DB-journal")
+ val dbShm: Path = db.resolveSibling("$FILE_NAME_DB-shm")
+ val dbWal: Path = db.resolveSibling("$FILE_NAME_DB-wal")
}
diff --git a/app/src/main/java/org/schabi/newpipe/settings/export/ImportExportManager.kt b/app/src/main/java/org/schabi/newpipe/settings/export/ImportExportManager.kt
index 36e0b9ce1..cbf860d2c 100644
--- a/app/src/main/java/org/schabi/newpipe/settings/export/ImportExportManager.kt
+++ b/app/src/main/java/org/schabi/newpipe/settings/export/ImportExportManager.kt
@@ -12,6 +12,8 @@ import java.io.FileNotFoundException
import java.io.IOException
import java.io.ObjectOutputStream
import java.util.zip.ZipOutputStream
+import kotlin.io.path.createParentDirectories
+import kotlin.io.path.deleteIfExists
class ImportExportManager(private val fileLocator: BackupFileLocator) {
companion object {
@@ -28,11 +30,8 @@ class ImportExportManager(private val fileLocator: BackupFileLocator) {
// previous file size, the file will retain part of the previous content and be corrupted
ZipOutputStream(SharpOutputStream(file.openAndTruncateStream()).buffered()).use { outZip ->
// add the database
- ZipHelper.addFileToZip(
- outZip,
- BackupFileLocator.FILE_NAME_DB,
- fileLocator.db.path,
- )
+ val name = BackupFileLocator.FILE_NAME_DB
+ ZipHelper.addFileToZip(outZip, name, fileLocator.db)
// add the legacy vulnerable serialized preferences (will be removed in the future)
ZipHelper.addFileToZip(
@@ -61,11 +60,10 @@ class ImportExportManager(private val fileLocator: BackupFileLocator) {
/**
* Tries to create database directory if it does not exist.
- *
- * @return Whether the directory exists afterwards.
*/
- fun ensureDbDirectoryExists(): Boolean {
- return fileLocator.dbDir.exists() || fileLocator.dbDir.mkdir()
+ @Throws(IOException::class)
+ fun ensureDbDirectoryExists() {
+ fileLocator.db.createParentDirectories()
}
/**
@@ -75,16 +73,13 @@ class ImportExportManager(private val fileLocator: BackupFileLocator) {
* @return true if the database was successfully extracted, false otherwise
*/
fun extractDb(file: StoredFileHelper): Boolean {
- val success = ZipHelper.extractFileFromZip(
- file,
- BackupFileLocator.FILE_NAME_DB,
- fileLocator.db.path,
- )
+ val name = BackupFileLocator.FILE_NAME_DB
+ val success = ZipHelper.extractFileFromZip(file, name, fileLocator.db)
if (success) {
- fileLocator.dbJournal.delete()
- fileLocator.dbWal.delete()
- fileLocator.dbShm.delete()
+ fileLocator.dbJournal.deleteIfExists()
+ fileLocator.dbWal.deleteIfExists()
+ fileLocator.dbShm.deleteIfExists()
}
return success
diff --git a/app/src/main/java/org/schabi/newpipe/settings/migration/MigrationManager.java b/app/src/main/java/org/schabi/newpipe/settings/migration/MigrationManager.java
new file mode 100644
index 000000000..d5b0e783d
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/settings/migration/MigrationManager.java
@@ -0,0 +1,103 @@
+package org.schabi.newpipe.settings.migration;
+
+import android.content.Context;
+
+import androidx.annotation.NonNull;
+import androidx.appcompat.app.AlertDialog;
+import androidx.core.util.Consumer;
+
+import org.schabi.newpipe.R;
+import org.schabi.newpipe.error.ErrorUtil;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * MigrationManager is responsible for running migrations and showing the user information about
+ * the migrations that were applied.
+ */
+public final class MigrationManager {
+
+ private static final String TAG = MigrationManager.class.getSimpleName();
+ /**
+ * List of UI actions that are performed after the UI is initialized (e.g. showing alert
+ * dialogs) to inform the user about changes that were applied by migrations.
+ */
+ private static final List> MIGRATION_INFO = new ArrayList<>();
+
+ private MigrationManager() {
+ // MigrationManager is a utility class that is completely static
+ }
+
+ /**
+ * Run all migrations that are needed for the current version of NewPipe.
+ * This method should be called at the start of the application, before any other operations
+ * that depend on the settings.
+ *
+ * @param context Context that can be used to run migrations
+ */
+ public static void runMigrationsIfNeeded(@NonNull final Context context) {
+ SettingMigrations.runMigrationsIfNeeded(context);
+ }
+
+ /**
+ * Perform UI actions informing about migrations that took place if they are present.
+ * @param context Context that can be used to show dialogs/snackbars/toasts
+ */
+ public static void showUserInfoIfPresent(@NonNull final Context context) {
+ if (MIGRATION_INFO.isEmpty()) {
+ return;
+ }
+
+ try {
+ MIGRATION_INFO.get(0).accept(context);
+ } catch (final Exception e) {
+ ErrorUtil.showUiErrorSnackbar(context, "Showing migration info to the user", e);
+ // Remove the migration that caused the error and continue with the next one
+ MIGRATION_INFO.remove(0);
+ showUserInfoIfPresent(context);
+ }
+ }
+
+ /**
+ * Add a migration info action that will be executed after the UI is initialized.
+ * This can be used to show dialogs/snackbars/toasts to inform the user about changes that
+ * were applied by migrations.
+ *
+ * @param info the action to be executed
+ */
+ public static void addMigrationInfo(final Consumer info) {
+ MIGRATION_INFO.add(info);
+ }
+
+ /**
+ * This method should be called when the user dismisses the migration info
+ * to check if there are any more migration info actions to be shown.
+ * @param context Context that can be used to show dialogs/snackbars/toasts
+ */
+ public static void onMigrationInfoDismissed(@NonNull final Context context) {
+ MIGRATION_INFO.remove(0);
+ showUserInfoIfPresent(context);
+ }
+
+ /**
+ * Creates a dialog to inform the user about the migration.
+ * @param uiContext Context that can be used to show dialogs/snackbars/toasts
+ * @param title the title of the dialog
+ * @param message the message of the dialog
+ * @return the dialog that can be shown to the user with a custom dismiss listener
+ */
+ static AlertDialog createMigrationInfoDialog(@NonNull final Context uiContext,
+ @NonNull final String title,
+ @NonNull final String message) {
+ return new AlertDialog.Builder(uiContext)
+ .setTitle(title)
+ .setMessage(message)
+ .setPositiveButton(R.string.ok, null)
+ .setOnDismissListener(dialog ->
+ MigrationManager.onMigrationInfoDismissed(uiContext))
+ .setCancelable(false) // prevents the dialog from being dismissed accidentally
+ .create();
+ }
+
+}
diff --git a/app/src/main/java/org/schabi/newpipe/settings/SettingMigrations.java b/app/src/main/java/org/schabi/newpipe/settings/migration/SettingMigrations.java
similarity index 65%
rename from app/src/main/java/org/schabi/newpipe/settings/SettingMigrations.java
rename to app/src/main/java/org/schabi/newpipe/settings/migration/SettingMigrations.java
index a77e1c514..92520ec7e 100644
--- a/app/src/main/java/org/schabi/newpipe/settings/SettingMigrations.java
+++ b/app/src/main/java/org/schabi/newpipe/settings/migration/SettingMigrations.java
@@ -1,12 +1,15 @@
-package org.schabi.newpipe.settings;
+package org.schabi.newpipe.settings.migration;
import static org.schabi.newpipe.MainActivity.DEBUG;
+import static org.schabi.newpipe.extractor.ServiceList.SoundCloud;
+import static org.schabi.newpipe.extractor.ServiceList.YouTube;
import android.content.Context;
import android.content.SharedPreferences;
import android.util.Log;
import androidx.annotation.NonNull;
+import androidx.core.util.Consumer;
import androidx.preference.PreferenceManager;
import org.schabi.newpipe.App;
@@ -14,18 +17,32 @@ import org.schabi.newpipe.R;
import org.schabi.newpipe.error.ErrorInfo;
import org.schabi.newpipe.error.ErrorUtil;
import org.schabi.newpipe.error.UserAction;
+import org.schabi.newpipe.settings.tabs.Tab;
+import org.schabi.newpipe.settings.tabs.TabsManager;
import org.schabi.newpipe.util.DeviceUtils;
import java.util.Collections;
import java.util.HashSet;
+import java.util.List;
import java.util.Set;
+import java.util.stream.Collectors;
/**
- * In order to add a migration, follow these steps, given P is the previous version:
- * - in the class body add a new {@code MIGRATION_P_P+1 = new Migration(P, P+1) { ... }} and put in
- * the {@code migrate()} method the code that need to be run when migrating from P to P+1
- * - add {@code MIGRATION_P_P+1} at the end of {@link SettingMigrations#SETTING_MIGRATIONS}
- * - increment {@link SettingMigrations#VERSION}'s value by 1 (so it should become P+1)
+ * This class contains the code to migrate the settings from one version to another.
+ * Migrations are run automatically when the app is started and the settings version changed.
+ *
+ * In order to add a migration, follow these steps, given {@code P} is the previous version:
+ *
+ * - in the class body add a new {@code MIGRATION_P_P+1 = new Migration(P, P+1) { ... }} and put
+ * in the {@code migrate()} method the code that need to be run
+ * when migrating from {@code P} to {@code P+1}
+ * - add {@code MIGRATION_P_P+1} at the end of {@link SettingMigrations#SETTING_MIGRATIONS}
+ * - increment {@link SettingMigrations#VERSION}'s value by 1
+ * (so it becomes {@code P+1})
+ *
+ * Migrations can register UI actions using {@link MigrationManager#addMigrationInfo(Consumer)}
+ * that will be performed after the UI is initialized to inform the user about changes
+ * that were applied by migrations.
*/
public final class SettingMigrations {
@@ -129,7 +146,7 @@ public final class SettingMigrations {
}
};
- public static final Migration MIGRATION_5_6 = new Migration(5, 6) {
+ private static final Migration MIGRATION_5_6 = new Migration(5, 6) {
@Override
protected void migrate(@NonNull final Context context) {
final boolean loadImages = sp.getBoolean("download_thumbnail_key", true);
@@ -143,6 +160,67 @@ public final class SettingMigrations {
}
};
+ private static final Migration MIGRATION_6_7 = new Migration(6, 7) {
+ @Override
+ protected void migrate(@NonNull final Context context) {
+ // The SoundCloud Top 50 Kiosk was removed in the extractor,
+ // so we remove the corresponding tab if it exists.
+ final TabsManager tabsManager = TabsManager.getManager(context);
+ final List tabs = tabsManager.getTabs();
+ final List cleanedTabs = tabs.stream()
+ .filter(tab -> !(tab instanceof Tab.KioskTab kioskTab
+ && kioskTab.getKioskServiceId() == SoundCloud.getServiceId()
+ && kioskTab.getKioskId().equals("Top 50")))
+ .collect(Collectors.toUnmodifiableList());
+ if (tabs.size() != cleanedTabs.size()) {
+ tabsManager.saveTabs(cleanedTabs);
+ // create an AlertDialog to inform the user about the change
+ MigrationManager.addMigrationInfo(uiContext ->
+ MigrationManager.createMigrationInfoDialog(
+ uiContext,
+ uiContext.getString(R.string.migration_info_6_7_title),
+ uiContext.getString(R.string.migration_info_6_7_message))
+ .show());
+ }
+ }
+ };
+
+ private static final Migration MIGRATION_7_8 = new Migration(7, 8) {
+ @Override
+ protected void migrate(@NonNull final Context context) {
+ // YouTube remove the combined Trending kiosk, see
+ // https://github.com/TeamNewPipe/NewPipe/discussions/12445 for more information.
+ // If the user has a dedicated YouTube/Trending kiosk tab,
+ // it is removed and replaced with the new live kiosk tab.
+ // The default trending kiosk tab is not touched
+ // because it uses the default kiosk provided by the extractor
+ // and is thus updated automatically.
+ final TabsManager tabsManager = TabsManager.getManager(context);
+ final List tabs = tabsManager.getTabs();
+ final List cleanedTabs = tabs.stream()
+ .filter(tab -> !(tab instanceof Tab.KioskTab kioskTab
+ && kioskTab.getKioskServiceId() == YouTube.getServiceId()
+ && kioskTab.getKioskId().equals("Trending")))
+ .collect(Collectors.toUnmodifiableList());
+ if (tabs.size() != cleanedTabs.size()) {
+ tabsManager.saveTabs(cleanedTabs);
+ }
+
+ final boolean hasDefaultTrendingTab = tabs.stream()
+ .anyMatch(tab -> tab instanceof Tab.DefaultKioskTab);
+
+ if (tabs.size() != cleanedTabs.size() || hasDefaultTrendingTab) {
+ // User is informed about the change
+ MigrationManager.addMigrationInfo(uiContext ->
+ MigrationManager.createMigrationInfoDialog(
+ uiContext,
+ uiContext.getString(R.string.migration_info_7_8_title),
+ uiContext.getString(R.string.migration_info_7_8_message))
+ .show());
+ }
+ }
+ };
+
/**
* List of all implemented migrations.
*
@@ -156,15 +234,17 @@ public final class SettingMigrations {
MIGRATION_3_4,
MIGRATION_4_5,
MIGRATION_5_6,
+ MIGRATION_6_7,
+ MIGRATION_7_8,
};
/**
* Version number for preferences. Must be incremented every time a migration is necessary.
*/
- private static final int VERSION = 6;
+ private static final int VERSION = 8;
- public static void runMigrationsIfNeeded(@NonNull final Context context) {
+ static void runMigrationsIfNeeded(@NonNull final Context context) {
// setup migrations and check if there is something to do
sp = PreferenceManager.getDefaultSharedPreferences(context);
final String lastPrefVersionKey = context.getString(R.string.last_used_preferences_version);
diff --git a/app/src/main/java/org/schabi/newpipe/ui/components/items/stream/StreamMenu.kt b/app/src/main/java/org/schabi/newpipe/ui/components/items/stream/StreamMenu.kt
index 935bda85f..7619515e7 100644
--- a/app/src/main/java/org/schabi/newpipe/ui/components/items/stream/StreamMenu.kt
+++ b/app/src/main/java/org/schabi/newpipe/ui/components/items/stream/StreamMenu.kt
@@ -28,10 +28,9 @@ fun StreamMenu(
) {
val context = LocalContext.current
val streamViewModel = viewModel()
- val playerHolder = PlayerHolder.getInstance()
DropdownMenu(expanded = expanded, onDismissRequest = onDismissRequest) {
- if (playerHolder.isPlayQueueReady) {
+ if (PlayerHolder.isPlayQueueReady) {
DropdownMenuItem(
text = { Text(text = stringResource(R.string.enqueue_stream)) },
onClick = {
@@ -42,7 +41,7 @@ fun StreamMenu(
}
)
- if (playerHolder.queuePosition < playerHolder.queueSize - 1) {
+ if (PlayerHolder.queuePosition < PlayerHolder.queueSize - 1) {
DropdownMenuItem(
text = { Text(text = stringResource(R.string.enqueue_next_stream)) },
onClick = {
diff --git a/app/src/main/java/org/schabi/newpipe/ui/components/video/comment/CommentRepliesHeader.kt b/app/src/main/java/org/schabi/newpipe/ui/components/video/comment/CommentRepliesHeader.kt
index 3a3fae480..f9c44b80c 100644
--- a/app/src/main/java/org/schabi/newpipe/ui/components/video/comment/CommentRepliesHeader.kt
+++ b/app/src/main/java/org/schabi/newpipe/ui/components/video/comment/CommentRepliesHeader.kt
@@ -81,12 +81,11 @@ fun CommentRepliesHeader(comment: CommentsInfoItem, onCommentAuthorOpened: () ->
style = MaterialTheme.typography.titleSmall,
)
- Text(
- text = Localization.relativeTimeOrTextual(
- context, comment.uploadDate, comment.textualUploadDate
- ),
- style = MaterialTheme.typography.bodySmall,
- )
+ Localization.relativeTimeOrTextual(
+ context, comment.uploadDate, comment.textualUploadDate
+ )?.let {
+ Text(text = it, style = MaterialTheme.typography.bodySmall)
+ }
}
}
diff --git a/app/src/main/java/org/schabi/newpipe/util/ChannelTabHelper.java b/app/src/main/java/org/schabi/newpipe/util/ChannelTabHelper.java
index 8e8d38490..cde6e3fef 100644
--- a/app/src/main/java/org/schabi/newpipe/util/ChannelTabHelper.java
+++ b/app/src/main/java/org/schabi/newpipe/util/ChannelTabHelper.java
@@ -24,6 +24,7 @@ public final class ChannelTabHelper {
switch (tab) {
case ChannelTabs.VIDEOS:
case ChannelTabs.TRACKS:
+ case ChannelTabs.LIKES:
case ChannelTabs.SHORTS:
case ChannelTabs.LIVESTREAMS:
return true;
@@ -62,6 +63,8 @@ public final class ChannelTabHelper {
return R.string.show_channel_tabs_playlists;
case ChannelTabs.ALBUMS:
return R.string.show_channel_tabs_albums;
+ case ChannelTabs.LIKES:
+ return R.string.show_channel_tabs_likes;
default:
return -1;
}
@@ -78,6 +81,8 @@ public final class ChannelTabHelper {
return R.string.fetch_channel_tabs_shorts;
case ChannelTabs.LIVESTREAMS:
return R.string.fetch_channel_tabs_livestreams;
+ case ChannelTabs.LIKES:
+ return R.string.fetch_channel_tabs_likes;
default:
return -1;
}
@@ -100,6 +105,8 @@ public final class ChannelTabHelper {
return R.string.channel_tab_playlists;
case ChannelTabs.ALBUMS:
return R.string.channel_tab_albums;
+ case ChannelTabs.LIKES:
+ return R.string.channel_tab_likes;
default:
return R.string.unknown_content;
}
diff --git a/app/src/main/java/org/schabi/newpipe/util/KioskTranslator.java b/app/src/main/java/org/schabi/newpipe/util/KioskTranslator.java
index b8c2ff236..5aa332159 100644
--- a/app/src/main/java/org/schabi/newpipe/util/KioskTranslator.java
+++ b/app/src/main/java/org/schabi/newpipe/util/KioskTranslator.java
@@ -52,6 +52,14 @@ public final class KioskTranslator {
return c.getString(R.string.featured);
case "Radio":
return c.getString(R.string.radio);
+ case "trending_gaming":
+ return c.getString(R.string.trending_gaming);
+ case "trending_music":
+ return c.getString(R.string.trending_music);
+ case "trending_movies_and_shows":
+ return c.getString(R.string.trending_movies);
+ case "trending_podcasts_episodes":
+ return c.getString(R.string.trending_podcasts);
default:
return kioskId;
}
@@ -77,6 +85,14 @@ public final class KioskTranslator {
return R.drawable.ic_stars;
case "Radio":
return R.drawable.ic_radio;
+ case "trending_gaming":
+ return R.drawable.ic_videogame_asset;
+ case "trending_music":
+ return R.drawable.ic_music_note;
+ case "trending_movies_and_shows":
+ return R.drawable.ic_movie;
+ case "trending_podcasts_episodes":
+ return R.drawable.ic_podcasts;
default:
return 0;
}
diff --git a/app/src/main/java/org/schabi/newpipe/util/ListHelper.java b/app/src/main/java/org/schabi/newpipe/util/ListHelper.java
index 282a88b1e..ea41f3e81 100644
--- a/app/src/main/java/org/schabi/newpipe/util/ListHelper.java
+++ b/app/src/main/java/org/schabi/newpipe/util/ListHelper.java
@@ -322,7 +322,7 @@ public final class ListHelper {
}
// Sort collected streams by name
- return collectedStreams.values().stream().sorted(getAudioTrackNameComparator(context))
+ return collectedStreams.values().stream().sorted(getAudioTrackNameComparator())
.collect(Collectors.toList());
}
@@ -359,7 +359,7 @@ public final class ListHelper {
}
// Sort tracks alphabetically, sort track streams by quality
- final Comparator nameCmp = getAudioTrackNameComparator(context);
+ final Comparator nameCmp = getAudioTrackNameComparator();
final Comparator formatCmp = getAudioFormatComparator(context);
return collectedStreams.values().stream()
@@ -867,12 +867,10 @@ public final class ListHelper {
* Get a {@link Comparator} to compare {@link AudioStream}s by their languages and track types
* for alphabetical sorting.
*
- * @param context app context for localization
* @return Comparator
*/
- private static Comparator getAudioTrackNameComparator(
- @NonNull final Context context) {
- final Locale appLoc = Localization.getAppLocale(context);
+ private static Comparator getAudioTrackNameComparator() {
+ final Locale appLoc = Localization.getAppLocale();
return Comparator.comparing(AudioStream::getAudioLocale, Comparator.nullsLast(
Comparator.comparing(locale -> locale.getDisplayName(appLoc))))
diff --git a/app/src/main/java/org/schabi/newpipe/util/Localization.java b/app/src/main/java/org/schabi/newpipe/util/Localization.java
index 825bee343..890981e90 100644
--- a/app/src/main/java/org/schabi/newpipe/util/Localization.java
+++ b/app/src/main/java/org/schabi/newpipe/util/Localization.java
@@ -5,14 +5,12 @@ import static org.schabi.newpipe.MainActivity.DEBUG;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.SharedPreferences;
-import android.content.res.Configuration;
import android.content.res.Resources;
import android.icu.text.CompactDecimalFormat;
import android.os.Build;
+import android.text.BidiFormatter;
import android.text.TextUtils;
import android.text.format.DateUtils;
-import android.text.BidiFormatter;
-import android.util.DisplayMetrics;
import android.util.Log;
import androidx.annotation.NonNull;
@@ -43,7 +41,6 @@ import java.time.format.FormatStyle;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
-import java.util.Objects;
import java.util.stream.Collectors;
@@ -120,39 +117,34 @@ public final class Localization {
return getLocaleFromPrefs(context, R.string.content_language_key);
}
- public static Locale getAppLocale(@NonNull final Context context) {
- if (Build.VERSION.SDK_INT >= 33) {
- final Locale customLocale = AppCompatDelegate.getApplicationLocales().get(0);
- return Objects.requireNonNullElseGet(customLocale, Locale::getDefault);
- }
- return getLocaleFromPrefs(context, R.string.app_language_key);
+ public static Locale getAppLocale() {
+ final Locale customLocale = AppCompatDelegate.getApplicationLocales().get(0);
+ return customLocale != null ? customLocale : Locale.getDefault();
}
- public static String localizeNumber(@NonNull final Context context, final long number) {
- return localizeNumber(context, (double) number);
+ public static String localizeNumber(final long number) {
+ return localizeNumber((double) number);
}
- public static String localizeNumber(@NonNull final Context context, final double number) {
- final NumberFormat nf = NumberFormat.getInstance(getAppLocale(context));
- return nf.format(number);
+ public static String localizeNumber(final double number) {
+ return NumberFormat.getInstance(getAppLocale()).format(number);
}
- public static String formatDate(@NonNull final Context context,
- @NonNull final OffsetDateTime offsetDateTime) {
+ public static String formatDate(@NonNull final OffsetDateTime offsetDateTime) {
return DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM)
- .withLocale(getAppLocale(context)).format(offsetDateTime
- .atZoneSameInstant(ZoneId.systemDefault()));
+ .withLocale(getAppLocale())
+ .format(offsetDateTime.atZoneSameInstant(ZoneId.systemDefault()));
}
@SuppressLint("StringFormatInvalid")
public static String localizeUploadDate(@NonNull final Context context,
@NonNull final OffsetDateTime offsetDateTime) {
- return context.getString(R.string.upload_date_text, formatDate(context, offsetDateTime));
+ return context.getString(R.string.upload_date_text, formatDate(offsetDateTime));
}
public static String localizeViewCount(@NonNull final Context context, final long viewCount) {
return getQuantity(context, R.plurals.views, R.string.no_views, viewCount,
- localizeNumber(context, viewCount));
+ localizeNumber(viewCount));
}
public static String localizeStreamCount(@NonNull final Context context,
@@ -166,7 +158,7 @@ public final class Localization {
return context.getResources().getString(R.string.more_than_100_videos);
default:
return getQuantity(context, R.plurals.videos, R.string.no_videos, streamCount,
- localizeNumber(context, streamCount));
+ localizeNumber(streamCount));
}
}
@@ -187,27 +179,33 @@ public final class Localization {
public static String localizeWatchingCount(@NonNull final Context context,
final long watchingCount) {
return getQuantity(context, R.plurals.watching, R.string.no_one_watching, watchingCount,
- localizeNumber(context, watchingCount));
+ localizeNumber(watchingCount));
}
public static String shortCount(@NonNull final Context context, final long count) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
- return CompactDecimalFormat.getInstance(getAppLocale(context),
+ return CompactDecimalFormat.getInstance(getAppLocale(),
CompactDecimalFormat.CompactStyle.SHORT).format(count);
}
final double value = (double) count;
if (count >= 1000000000) {
- return localizeNumber(context, round(value / 1000000000))
- + context.getString(R.string.short_billion);
+ final double shortenedValue = value / 1000000000;
+ final int scale = shortenedValue >= 100 ? 0 : 1;
+ return context.getString(R.string.short_billion,
+ localizeNumber(round(shortenedValue, scale)));
} else if (count >= 1000000) {
- return localizeNumber(context, round(value / 1000000))
- + context.getString(R.string.short_million);
+ final double shortenedValue = value / 1000000;
+ final int scale = shortenedValue >= 100 ? 0 : 1;
+ return context.getString(R.string.short_million,
+ localizeNumber(round(shortenedValue, scale)));
} else if (count >= 1000) {
- return localizeNumber(context, round(value / 1000))
- + context.getString(R.string.short_thousand);
+ final double shortenedValue = value / 1000;
+ final int scale = shortenedValue >= 100 ? 0 : 1;
+ return context.getString(R.string.short_thousand,
+ localizeNumber(round(shortenedValue, scale)));
} else {
- return localizeNumber(context, value);
+ return localizeNumber(value);
}
}
@@ -372,8 +370,8 @@ public final class Localization {
prettyTime.removeUnit(Decade.class);
}
- public static PrettyTime resolvePrettyTime(@NonNull final Context context) {
- return new PrettyTime(getAppLocale(context));
+ public static PrettyTime resolvePrettyTime() {
+ return new PrettyTime(getAppLocale());
}
public static String relativeTime(@NonNull final OffsetDateTime offsetDateTime) {
@@ -391,9 +389,10 @@ public final class Localization {
* {@code parsed != null} and the relevant setting is enabled, {@code textual} will
* be appended to the returned string for debugging purposes.
*/
+ @Nullable
public static String relativeTimeOrTextual(@Nullable final Context context,
@Nullable final DateWrapper parsed,
- final String textual) {
+ @Nullable final String textual) {
if (parsed == null) {
return textual;
} else if (DEBUG && context != null && PreferenceManager
@@ -405,14 +404,6 @@ public final class Localization {
}
}
- public static void assureCorrectAppLanguage(final Context c) {
- final Resources res = c.getResources();
- final DisplayMetrics dm = res.getDisplayMetrics();
- final Configuration conf = res.getConfiguration();
- conf.setLocale(getAppLocale(c));
- res.updateConfiguration(conf, dm);
- }
-
private static Locale getLocaleFromPrefs(@NonNull final Context context,
@StringRes final int prefKey) {
final SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(context);
@@ -426,8 +417,8 @@ public final class Localization {
}
}
- private static double round(final double value) {
- return new BigDecimal(value).setScale(1, RoundingMode.HALF_UP).doubleValue();
+ private static double round(final double value, final int scale) {
+ return new BigDecimal(value).setScale(scale, RoundingMode.HALF_UP).doubleValue();
}
private static String getQuantity(@NonNull final Context context,
@@ -447,29 +438,32 @@ public final class Localization {
return context.getResources().getQuantityString(pluralId, safeCount, formattedCount);
}
+ // Starting with pull request #12093, NewPipe exclusively uses Android's
+ // public per-app language APIs to read and set the UI language for NewPipe.
+ // The following code will migrate any existing custom app language in SharedPreferences to
+ // use the public per-app language APIs instead.
+ // For reference, see
+ // https://android-developers.googleblog.com/2022/11/per-app-language-preferences-part-1.html
public static void migrateAppLanguageSettingIfNecessary(@NonNull final Context context) {
- // Starting with pull request #12093, NewPipe on Android 13+ exclusively uses Android's
- // public per-app language APIs to read and set the UI language for NewPipe.
- // If running on Android 13+, the following code will migrate any existing custom
- // app language in SharedPreferences to use the public per-app language APIs instead.
- if (Build.VERSION.SDK_INT >= 33) {
- final SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(context);
- final String appLanguageKey = context.getString(R.string.app_language_key);
- final String appLanguageValue = sp.getString(appLanguageKey, null);
- if (appLanguageValue != null) {
+ final SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(context);
+ final String appLanguageKey = context.getString(R.string.app_language_key);
+ final String appLanguageValue = sp.getString(appLanguageKey, null);
+ if (appLanguageValue != null) {
+ // The app language key is used on Android versions < 33
+ // for more info, see ContentSettingsFragment
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
sp.edit().remove(appLanguageKey).apply();
- final String appLanguageDefaultValue =
- context.getString(R.string.default_localization_key);
- if (!appLanguageValue.equals(appLanguageDefaultValue)) {
- try {
- AppCompatDelegate.setApplicationLocales(
- LocaleListCompat.forLanguageTags(appLanguageValue)
- );
- } catch (final RuntimeException e) {
- Log.e(TAG, "Failed to migrate previous custom app language "
- + "setting to public per-app language APIs"
- );
- }
+ }
+ final String appLanguageDefaultValue =
+ context.getString(R.string.default_localization_key);
+ if (!appLanguageValue.equals(appLanguageDefaultValue)) {
+ try {
+ AppCompatDelegate.setApplicationLocales(
+ LocaleListCompat.forLanguageTags(appLanguageValue));
+ } catch (final RuntimeException e) {
+ Log.e(TAG, "Failed to migrate previous custom app language "
+ + "setting to public per-app language APIs"
+ );
}
}
}
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 55a5b1915..e1b48cd89 100644
--- a/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java
+++ b/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java
@@ -200,7 +200,7 @@ public final class NavigationHelper {
}
public static void enqueueOnPlayer(final Context context, final PlayQueue queue) {
- PlayerType playerType = PlayerHolder.getInstance().getType();
+ PlayerType playerType = PlayerHolder.INSTANCE.getType();
if (playerType == null) {
Log.e(TAG, "Enqueueing but no player is open; defaulting to background player");
playerType = PlayerType.AUDIO;
@@ -211,7 +211,7 @@ public final class NavigationHelper {
/* ENQUEUE NEXT */
public static void enqueueNextOnPlayer(final Context context, final PlayQueue queue) {
- PlayerType playerType = PlayerHolder.getInstance().getType();
+ PlayerType playerType = PlayerHolder.INSTANCE.getType();
if (playerType == null) {
Log.e(TAG, "Enqueueing next but no player is open; defaulting to background player");
playerType = PlayerType.AUDIO;
@@ -421,13 +421,13 @@ public final class NavigationHelper {
final boolean switchingPlayers) {
final boolean autoPlay;
- @Nullable final PlayerType playerType = PlayerHolder.getInstance().getType();
+ @Nullable final PlayerType playerType = PlayerHolder.INSTANCE.getType();
if (playerType == null) {
// no player open
autoPlay = PlayerHelper.isAutoplayAllowedByUser(context);
} else if (switchingPlayers) {
// switching player to main player
- autoPlay = PlayerHolder.getInstance().isPlaying(); // keep play/pause state
+ autoPlay = PlayerHolder.INSTANCE.isPlaying(); // keep play/pause state
} else if (playerType == PlayerType.MAIN) {
// opening new stream while already playing in main player
autoPlay = PlayerHelper.isAutoplayAllowedByUser(context);
diff --git a/app/src/main/java/org/schabi/newpipe/util/ZipHelper.java b/app/src/main/java/org/schabi/newpipe/util/ZipHelper.java
index b2aebac42..bccfc7f38 100644
--- a/app/src/main/java/org/schabi/newpipe/util/ZipHelper.java
+++ b/app/src/main/java/org/schabi/newpipe/util/ZipHelper.java
@@ -6,12 +6,12 @@ import org.schabi.newpipe.streams.io.StoredFileHelper;
import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardCopyOption;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import java.util.zip.ZipOutputStream;
@@ -37,9 +37,6 @@ import java.util.zip.ZipOutputStream;
*/
public final class ZipHelper {
-
- private static final int BUFFER_SIZE = 2048;
-
@FunctionalInterface
public interface InputStreamConsumer {
void acceptStream(InputStream inputStream) throws IOException;
@@ -55,17 +52,17 @@ public final class ZipHelper {
/**
- * This function helps to create zip files. Caution this will overwrite the original file.
+ * This function helps to create zip files. Caution, this will overwrite the original file.
*
* @param outZip the ZipOutputStream where the data should be stored in
* @param nameInZip the path of the file inside the zip
- * @param fileOnDisk the path of the file on the disk that should be added to zip
+ * @param path the path of the file on the disk that should be added to zip
*/
public static void addFileToZip(final ZipOutputStream outZip,
final String nameInZip,
- final String fileOnDisk) throws IOException {
- try (FileInputStream fi = new FileInputStream(fileOnDisk)) {
- addFileToZip(outZip, nameInZip, fi);
+ final Path path) throws IOException {
+ try (var inputStream = Files.newInputStream(path)) {
+ addFileToZip(outZip, nameInZip, inputStream);
}
}
@@ -80,13 +77,13 @@ public final class ZipHelper {
final String nameInZip,
final OutputStreamConsumer streamConsumer) throws IOException {
final byte[] bytes;
- try (ByteArrayOutputStream byteOutput = new ByteArrayOutputStream()) {
+ try (var byteOutput = new ByteArrayOutputStream()) {
streamConsumer.acceptStream(byteOutput);
bytes = byteOutput.toByteArray();
}
- try (ByteArrayInputStream byteInput = new ByteArrayInputStream(bytes)) {
- ZipHelper.addFileToZip(outZip, nameInZip, byteInput);
+ try (var byteInput = new ByteArrayInputStream(bytes)) {
+ addFileToZip(outZip, nameInZip, byteInput);
}
}
@@ -97,49 +94,26 @@ public final class ZipHelper {
* @param nameInZip the path of the file inside the zip
* @param inputStream the content to put inside the file
*/
- public static void addFileToZip(final ZipOutputStream outZip,
- final String nameInZip,
- final InputStream inputStream) throws IOException {
- final byte[] data = new byte[BUFFER_SIZE];
- try (BufferedInputStream bufferedInputStream =
- new BufferedInputStream(inputStream, BUFFER_SIZE)) {
- final ZipEntry entry = new ZipEntry(nameInZip);
- outZip.putNextEntry(entry);
- int count;
- while ((count = bufferedInputStream.read(data, 0, BUFFER_SIZE)) != -1) {
- outZip.write(data, 0, count);
- }
- }
+ private static void addFileToZip(final ZipOutputStream outZip,
+ final String nameInZip,
+ final InputStream inputStream) throws IOException {
+ outZip.putNextEntry(new ZipEntry(nameInZip));
+ inputStream.transferTo(outZip);
}
/**
- * This will extract data from ZipInputStream. Caution this will overwrite the original file.
+ * This will extract data from ZipInputStream. Caution, this will overwrite the original file.
*
* @param zipFile the zip file to extract from
* @param nameInZip the path of the file inside the zip
- * @param fileOnDisk the path of the file on the disk where the data should be extracted to
+ * @param path the path of the file on the disk where the data should be extracted to
* @return will return true if the file was found within the zip file
*/
public static boolean extractFileFromZip(final StoredFileHelper zipFile,
final String nameInZip,
- final String fileOnDisk) throws IOException {
- return extractFileFromZip(zipFile, nameInZip, input -> {
- // delete old file first
- final File oldFile = new File(fileOnDisk);
- if (oldFile.exists()) {
- if (!oldFile.delete()) {
- throw new IOException("Could not delete " + fileOnDisk);
- }
- }
-
- final byte[] data = new byte[BUFFER_SIZE];
- try (FileOutputStream outFile = new FileOutputStream(fileOnDisk)) {
- int count;
- while ((count = input.read(data)) != -1) {
- outFile.write(data, 0, count);
- }
- }
- });
+ final Path path) throws IOException {
+ return extractFileFromZip(zipFile, nameInZip, input ->
+ Files.copy(input, path, StandardCopyOption.REPLACE_EXISTING));
}
/**
diff --git a/app/src/main/res/drawable/ic_podcasts.xml b/app/src/main/res/drawable/ic_podcasts.xml
new file mode 100644
index 000000000..c297e8fd3
--- /dev/null
+++ b/app/src/main/res/drawable/ic_podcasts.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/app/src/main/res/layout/fragment_blank.xml b/app/src/main/res/layout/fragment_blank.xml
index 6c2978e95..4d874ebdb 100644
--- a/app/src/main/res/layout/fragment_blank.xml
+++ b/app/src/main/res/layout/fragment_blank.xml
@@ -4,7 +4,9 @@
android:layout_width="match_parent"
android:layout_height="match_parent">
-
+
+
diff --git a/app/src/main/res/values-ar-rLY/strings.xml b/app/src/main/res/values-ar-rLY/strings.xml
index bbf5b8bdf..c9cedc392 100644
--- a/app/src/main/res/values-ar-rLY/strings.xml
+++ b/app/src/main/res/values-ar-rLY/strings.xml
@@ -75,13 +75,11 @@
الملفات المحملة
لا يوجد مثل هذا الملف/مصدر المحتوى
الأكثر إعجابًا
- بليون
تعذر تحميل موجز \'%s\'.
؟
التحقق من وجود تحديثات
مثيلات خوادم پيرتيوب
+100 فيديو
- ألف
مثيل الخادم موجود بالفعل
طلب تأكيد قبل مسح قائمة الانتظار
المشتركون
@@ -227,7 +225,6 @@
المميزة
عرض المحتوى الذي يُحتمل أن يكون غير مناسب للأطفال لأن له حدًا عمريًا (مثل 18+)
بدأ التشغيل في الخلفية
- ستتغير اللغة بمجرد إعادة تشغيل التطبيق
القصيرة
قوائم التشغيل
تنظيف
@@ -649,7 +646,6 @@
تسريع إلى الأمام/-ترجيع وقت البحث
تم رفضها من قبل النظام
ليس هناك تعليقات
- مليون
جاري التحقق من وجود تحديثات…
المحتوى
اسأل عن مكان التنزيل
diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml
index b2c6c4b64..7b0480e73 100644
--- a/app/src/main/res/values-ar/strings.xml
+++ b/app/src/main/res/values-ar/strings.xml
@@ -37,7 +37,7 @@
اعرض خيار لتشغيل الفيديو عبر مركز وسائط Kodi
عرض خيار التشغيل بواسطة كودي
السمة
- تم النشر في %1$s
+ منشورة على %1$s
رابط غير مدعوم
استخدام مشغل صوت خارجي
استخدام مشغل فيديو خارجي
@@ -116,9 +116,6 @@
لا شيء هنا سوى الصراصير
الصوت
إعادة المحاولة
- ألف
- مليون
- بليون
ليس هناك مشترِكون
- %s مشارك
@@ -427,7 +424,6 @@
الكشك الافتراضي
لا توجد مشاهدة
لا أحد يستمع
- ستتغير اللغة بمجرد إعادة تشغيل التطبيق
- %s مشاهدة
- %s مشاهدة
@@ -879,4 +875,14 @@
لا
تستخدم الإعدادات الموجودة في عملية التصدير التي يتم استيرادها تنسيقًا عرضة للاختراق تم إهماله منذ NewPipe 0.27.0. تأكد من أن التصدير الذي يتم استيراده من مصدر موثوق به، ويفضل استخدام عمليات التصدير التي تم الحصول عليها من NewPipe 0.27.0 أو الأحدث في المستقبل فقط. سيتم قريبًا إزالة دعم استيراد الإعدادات بهذا التنسيق الضعيف تمامًا، وبعد ذلك لن تتمكن الإصدارات القديمة من NewPipe من استيراد إعدادات التصدير من الإصدارات الجديدة بعد الآن.
الثانوي
+ المشاركة كقائمة تشغيل مؤقتة على YouTube
+ قوائم التشغيل
+ صفحة مجموعة القناة
+ حدد مجموعة المحتوى
+ لم تنشئ مجموعة محتوى
+ الإعجابات
+ البحث %1$s
+ البحث %1$s (%2$s)
+ تمت إزالة صفحة أفضل 50 من SoundCloud
+ أوقفت SoundCloud صفحة أفضل 50 الأصلية. تمت إزالة علامة التبويب المقابلة من صفحتك الرئيسية.
diff --git a/app/src/main/res/values-az/strings.xml b/app/src/main/res/values-az/strings.xml
index 3055db9e6..460c2f73d 100644
--- a/app/src/main/res/values-az/strings.xml
+++ b/app/src/main/res/values-az/strings.xml
@@ -297,9 +297,6 @@
Bəyən
Bəyənmə
Yenidən sıralamaq üçün sürüklə
- min
- Mln
- Mlrd
Xidməti dəyiş, hazırda seçilmiş:
Abunəçi yoxdur
Baxış yoxdur
@@ -342,7 +339,6 @@
Etibarlı ZIP faylı yoxdur
Xəbərdarlıq: Bütün faylları idxal etmək mümkün olmadı.
Tənzimləmələri də idxal etmək istəyirsiniz\?
- Tətbiq yenidən başladıldıqdan sonra dil dəyişəcəkdir
Ən yaxşı 50
Yeni və populyar
Yerli
@@ -801,4 +797,9 @@
Məlumat və yaddaş istifadəsini azaltmaq üçün şəkillərin keyfiyyətini və ya şəkillərin əsla yüklənib-yüklənilməməsini seçin. Dəyişikliklər həm yaddaşdaxili, həm də diskdə olan təsvir qalığın təmizləyir — %s
URL siyahısını paylaşın
ikinci dərəcəli
+ YouTube müvəqqəti pleylisti kimi paylaş
+ Pleylistlər
+ Axın qrupu seçin
+ Hələ heç bir axın qrupu yaradılmayıb
+ Kanal qrupu səhifəsi
diff --git a/app/src/main/res/values-azb/strings.xml b/app/src/main/res/values-azb/strings.xml
new file mode 100644
index 000000000..55344e519
--- /dev/null
+++ b/app/src/main/res/values-azb/strings.xml
@@ -0,0 +1,3 @@
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values-b+ast/strings.xml b/app/src/main/res/values-b+ast/strings.xml
index 454ddd152..81b212f80 100644
--- a/app/src/main/res/values-b+ast/strings.xml
+++ b/app/src/main/res/values-b+ast/strings.xml
@@ -43,9 +43,6 @@
Tarrezmes
Formatu de videu predetermináu
Prietu
- mil
- mill.
- mil mill.
Precísase esti permisu p\'abrir
\nnel mou ventanu
Retu de reCAPTCHA
@@ -223,7 +220,6 @@
Política de privacidá de NewPipe
El ficheru nun pue crease
El sirvidor nun unvia datos
- La llingua va camudar namás que se reanicie l\'aplicación.
Buscar
Compartir con
Soscribiéstite
diff --git a/app/src/main/res/values-b+uz+Latn/strings.xml b/app/src/main/res/values-b+uz+Latn/strings.xml
index 780061c73..2be37ea7c 100644
--- a/app/src/main/res/values-b+uz+Latn/strings.xml
+++ b/app/src/main/res/values-b+uz+Latn/strings.xml
@@ -267,9 +267,6 @@
Obunachilar yo\'q
Hozirda tanlangan xizmatni yoqish:
- B
- M
- k
Qayta
Audio
Video
@@ -551,7 +548,6 @@
Yangi va qaynoqlari
Top 50
Ommabop
- Ilova qayta ishga tushirilgandan so\'ng til o\'zgaradi.
Fikrlarni yuklab bo‘lmadi
Sozlamalarni ham import qilmoqchimisiz\?
Bu sizning joriy sozlamangizni bekor qiladi.
diff --git a/app/src/main/res/values-be/strings.xml b/app/src/main/res/values-be/strings.xml
index 19f3c8b26..a0930a9f8 100644
--- a/app/src/main/res/values-be/strings.xml
+++ b/app/src/main/res/values-be/strings.xml
@@ -55,7 +55,7 @@
Памятаць апошнія памер і пазіцыю ўсплывальнага акна
Хуткі пошук пазіцыі
Недакладны пошук дазваляе плэеру знаходзіць пазіцыі хутчэй са зніжанай дакладнасцю. Пошук цягам 5, 15 ці 25 секунд пры гэтым немажлівы
- Кэш малюнкаў ачышчаны
+ Кэш відарысаў ачышчаны
Ачысціць кэш метаданых
Выдаліць усе даныя вэб-старонак у кэшы
Кэш метаданых ачышчаны
@@ -71,8 +71,8 @@
Працягваць прайграванне пасля перапынкаў (напрыклад, тэлефонных званкоў)
Спампаваць
«Наступнае» і «Прапанаванае» відэа
- Паказваць падказку «Зацісніце, каб дадаць»
- Паказваць падказку пры націсканні «У акне» або «У фоне» на старонцы звестак аб відэа
+ Паказваць падказку «Утрымлівайце, каб дадаць у чаргу»
+ Паказваць падказку пры націсканні кнопкі «У акне» або «У фоне» на старонцы відэа
URL не падтрымліваецца
Прадвызначаная краіна кантэнту
Прадвызначаная мова кантэнту
@@ -92,7 +92,7 @@
Справаздача пра памылку
Усе
Каналы
- Плэйлісты
+ Плэй-лісты
Трэкі
Карыстальнікі
Адключана
@@ -111,8 +111,8 @@
Перайсці ў галоўнае акно
Імпартаваць даныя
Экспартаваць даныя
- Перавызначае вашу бягучую гісторыю, падпіскі, плэйлісты і (неабавязкова) налады
- Экспарт гісторыі, падпісак, плэйлістоў і налад
+ Перавызначае вашу бягучую гісторыю, падпіскі, плэй-лісты і (неабавязкова) налады
+ Экспарт гісторыі, падпісак, плэй-лістоў і налад
Ачысціць гісторыю праглядаў
Выдаліць гісторыю прайграных патокаў і пазіцыі прайгравання
Выдаліць усю гісторыю праглядаў\?
@@ -135,9 +135,9 @@
Відэапатокі не знойдзены
Аўдыяпатокі не знойдзены
Такой папкі не існуе
- Крыніца кантэнту або файла не існуе
+ Такога файла або крыніцы кантэнту не існуе
Файл не існуе або няма дазволу на яго чытанне ці запіс
- Імя файла не можа быць пустым
+ Назва файла не можа быць пустой
Адбылася памылка: %1$s
Няма трансляцый, даступных для спампоўвання
Прабачце, гэта не павінна было адбыцца.
@@ -147,21 +147,18 @@
Інфармацыя:
Што адбылося:
Што:\\nЗапыт:\\nМова кантэнту:\\nКраіна кантэнту:\\nМова праграмы:\\nСэрвіс:\\nЧас GMT:\\nПакет:\\nВерсія:\\nВерсія АС:
- Ваш каментарый (на англійскай):
+ Ваш каментарый (па-англійску):
Падрабязнасці:
Прайграць відэа, працягласць:
Мініяцюра аватара карыстальніка
Спадабалася
Не спадабалася
Няма вынікаў
- Нічога няма, акрамя цвыркуноў
+ Нічога няма, хоць сабак ганяй
Перацягніце, каб змяніць парадак
Відэа
Аўдыя
Паспрабаваць зноў
- тыс.
- млн
- млрд
Няма падпісчыкаў
- %s падпісчык
@@ -191,7 +188,7 @@
Адхіліць
Перайменаваць
ОК
- Імя файла
+ Назва файла
Патокі
Памылка
NewPipe спампоўвае
@@ -204,7 +201,7 @@
Запыт reCAPTCHA
Запытаны ўвод reCAPTCHA
Спампоўванне
- Дапушчальныя ў назвах файлаў сімвалы
+ Сімвалы, дапушчальныя ў назвах файлаў
Недапушчальныя сімвалы замяняюцца на гэты
Сімвал для замены
Літары і лічбы
@@ -253,7 +250,7 @@
Выдаліць
Падрабязнасці
Налады аўдыя
- Зацісніце, каб дадаць у чаргу
+ Утрымлівайце, каб дадаць у чаргу
Пачаць прайграванне ў фоне
Пачаць прайграванне ў акне
Адкрыць бакавую панэль
@@ -266,17 +263,17 @@
Заўсёды пытаць
Атрыманне звестак…
Загрузка запытанага кантэнту
- Стварыць плэйліст
+ Стварыць плэй-ліст
Перайменаваць
- Імя
- Дадаць у плэйліст
- Зрабіць мініяцюрай плэйліста
- Дадаць плэйліст у закладкі
+ Назва
+ Дадаць у плэй-ліст
+ Зрабіць мініяцюрай плэй-ліста
+ Дадаць плэй-ліст у закладкі
Выдаліць закладку
- Выдаліць плэйліст\?
- Плэйліст створаны
- Дададзена ў плэйліст
- Мініяцюра плэйліста зменена.
+ Выдаліць плэй-ліст?
+ Плэй-ліст створаны
+ Дададзена ў плэй-ліст
+ Мініяцюра плэй-ліста зменена.
Без субцітраў
Падагнаць
Запоўніць
@@ -327,10 +324,10 @@
Апавяшчэнні пра новыя версіі NewPipe
Знешняе сховішча недаступна
Спампоўванне на знешнюю SD-карту немагчыма. Скінуць размяшчэнне папкі спампоўвання?
- Памылка чытання захаваных укладак. Выкарыстоўваюцца ўкладкі па змаўчанні
+ Не ўдалося прачытаць захаваныя ўкладкі, таму выкарыстоўваюцца прадвызначаныя
Аднавіць прадвызначаныя значэнні
Аднавіць прадвызначаныя значэнні?
- Колькасць падпісчыкаў недаступная
+ Колькасць падпісчыкаў недаступна
Укладкі, бачныя на галоўнай старонцы
Абнаўленні
Паказваць апавяшчэнне пры наяўнасці новай версіі
@@ -347,9 +344,9 @@
Дадаць у чаргу
Дзеянне забаронена сістэмай
Памылка спампоўвання
- Стварыць унікальнае імя
+ Стварыць унікальную назву
Перазапісаць
- Файл з такім імем ужо спампоўваецца
+ Файл з такой назвай ўжо спампоўваецца
Паказаць тэкст памылкі
Немагчыма стварыць папку прызначэння
Немагчыма стварыць файл
@@ -376,19 +373,19 @@
Працягваць прайграванне
Аднаўляць апошнюю пазіцыю
Пазіцыі ў спісах
- Адлюстроўваць індыкатары пазіцый прагляду ў спісах
+ Паказваць у спісах пазіцыю прайгравання
Ачыстка даных
Пазіцыі прайгравання выдалены
- Файл перамешчаны ці выдалены
- Файл з такім імем ужо існуе
- Файл з такім імем ужо існуе
+ Файл перамешчаны або выдалены
+ Файл з такой назвай ужо існуе
+ Спампаваны файл з такой назвай ужо існуе
немагчыма перазапісаць файл
- Файл з такім імем ужо дададзены ў чаргу спампоўвання
+ Файл з такой назвай ужо ў чарзе спампоўвання
Праграма NewPipe была закрыта падчас працы з файлам
На прыладзе скончылася вольнае месца
Прагрэс страчаны, бо файл быў выдалены
Скончыўся час злучэння
- Вы хочаце ачысціць гісторыю спампоўвання ці выдаліць спампаваныя файлы?
+ Ачысціць гісторыю спампоўвання або выдаліць спампаваныя файлы?
Абмежаваць чаргу спампоўвання
Толькі адно адначасовае спампоўванне
Пачаць спампоўванне
@@ -397,7 +394,7 @@
Пры кожным спампоўванні вам будзе прапанавана выбраць месца захавання. \nУключыце сістэмны сродак выбару папак (SAF), калі хочаце спампоўваць файлы на знешнюю SD-карту
Выкарыстоўваць сістэмны сродак выбару папак (SAF)
«Storage Access Framework» дазваляе выконваць спампоўванне на знешнюю SD-карту
- Пераключыць службу, выбраную ў дадзены момант:
+ Пераключэнне сэрвісу, зараз выбраны:
Выдаліць усе пазіцыі прайгравання
Уключыць «Абмежаваны рэжым» YouTube
Падтрымліваюцца толькі URL-адрасы HTTPS
@@ -435,15 +432,15 @@
Найбольш папулярнае
Лакальнае
Нядаўна дададзенае
- Плэйлісты яшчэ не дададзены
- Выберыце плэйліст
+ Плэй-лісты яшчэ не дададзены
+ Выберыце плэй-ліст
Прадвызначаны кіёск
Так
Па завяршэнні націсніце «Гатова»
∞ відэа
100+ відэа
- Багрэпарт на GitHub
- Скапіруйце адфарматаваны багрэпарт
+ Паведаміць на GitHub
+ Скапіяваць адфарматаваную справаздачу
Дайце дазвол на адлюстраванне паверх іншых праграм
Выдаліць усе пазіцыі прайгравання\?
Выдаліць пазіцыі прайгравання
@@ -485,7 +482,7 @@
Выберыце любімую начную тэму - %s
Дазвол вылучэння тэксту ў апісанні
Вы можаце выбраць сваю любімую начную тэму ніжэй
- Гэта опцыя даступна толькі тады, калі %s будзе выбранай тэмаю
+ Параметр даступны, толькі калі выбрана тэма %s
Спампоўванне пачалося
Апавяшчэнні адключаны
Рэжым планшэта
@@ -505,7 +502,7 @@
Начная тэма
Адкрыць вэб-сайт
Цяпер можна вылучаць тэкст у апісанні. Звярніце ўвагу, што ў рэжыме вылучэння старонка можа мільгаць, а спасылкі не націскацца.
- Запускаць галоўны прайгравальнік у поўнаэкранным рэжыме
+ Запускаць асноўны прайгравальнік у поўнаэкранным рэжыме
Паказаць дэталі канала
Нізкая якасць (менш)
Апавяшчэнне пра відэахэшаванне
@@ -537,17 +534,16 @@
- %d дзён
Ачысціць гісторыю спампоўвання
- Мова зменіцца пасля перазапуску праграмы
Ніхто не слухае
Уключыць
Апавяшчэнні пра ход відэахэшавання
Стварыць паведамленне пра памылку
Выберыце падпіскі
- Імпарт ці экспарт падпісак з 3-кропкавага меню
+ Імпартуйце або экспартуйце падпіскі праз меню з трыма кропкамі ⁝
Забарона вылучэння тэксту ў апісанні
Хуткі рэжым
Калі ў вас узніклі праблемы з выкарыстаннем праграмы, абавязкова азнаёмцеся з адказамі на частыя пытанні!
- Адключыць тунэляванне медыя
+ Адключыць тунэляванне мультымедыя
Мініяцюра з перадпраглядам у паласе перамотвання
Высокая якасць (больш)
Не паказваць
@@ -576,12 +572,12 @@
Апавяшчэнне аб памылцы
Апавяшчэнні для паведамлення аб памылках
Адбылася памылка NewPipe, націсніце, каб адправіць справаздачу
- Запускаць відэа ва ўвесь экран, калі адключаны аўтапаварот. Міні-плэер даступны пры выхадзе з поўнаэкраннага рэжыму
+ Калі аўтапаварот адключаны, відэа адразу запускаецца ў поўнаэкранным рэжыме. Міні-плэер застаецца даступным, трэба толькі выйсці з поўнаэкраннага рэжыму
Шукайце серверы, якія вам даспадобы, на %s
Паказваць метаданыя
Ігнараваць падзеі апаратных медыякнопак
Паказваць змесціва, магчыма непрыдатнае для дзяцей, таму што яно мае ўзроставыя абмежаванні (напрыклад, 18+)
- Калі ласка, праверце, ці існуе ўжо праблема з абмеркаваннем вашага збою. Пры стварэнні дублікатаў тыкетаў вы забіраеце ў нас час, які мы маглі б патраціць на выпраўленне фактычнай памылкі.
+ Праверце, ці не існуе заяўкі з абмеркаваннем вашай праблемы. Дублікаты марнуюць наш час і праз гэта адцягваецца вырашэнне сапраўдных задач.
Адбылася памылка, глядзіце апавяшчэнне
Збой плэера
Карысна, напрыклад, калі вы карыстаецеся гарнітурай са зламанымі фізічнымі кнопкамі
@@ -593,7 +589,7 @@
Разлік хэша
Вырашана
Створана аўтаматычна (запампавальнік не знойдзены)
- Плэйлісты, якія пазначаны шэрым, ужо ўтрымліваюць гэты элемент.
+ Плэй-лісты, якія пазначаны шэрым, ужо ўтрымліваюць гэты элемент.
- %s новая трансляцыя
- %s новыя трансляцыі
@@ -608,15 +604,15 @@
Дублікат дададзены %d раз(ы)
LeakCanary недаступны
Паказаць уцечкі памяці
- Адключыце мультымедыйнае тунэляванне, калі ў вас з\'яўляецца чорны экран або заіканне падчас прайгравання відэа.
+ Адключыце тунэляванне мультымедыя, калі відэа прайграецца перарывіста або паказваецца чорны экран.
Не ўдалося скапіяваць у буфер абмену
Папка спампоўвання яшчэ не зададзена, выберыце папку спампоўвання цяпер
Частыя пытанні
Перайсці на вэб-сайт
- Правядзіце пальцам па элементах, каб выдаліць іх
+ Каб выдаліць элемент, змахніце яго ўбок
Прыбраць пастаянную мініяцюру
- Паказваць індыкатары выяў
- Паказваць каляровыя стужкі Пікаса на выявах, якія пазначаюць іх крыніцу: чырвоная для сеткі, сіняя для дыска і зялёная для памяці
+ Паказваць на відарысах указальнікі
+ Паказваць на відарысах каляровыя меткі Picasso, якія абазначаюць яго крыніцу: чырвоная — сетка, сіняя — дыск, зялёная — памяць
Апрацоўка стужкі…
Пры кожным спампоўванні вам будзе прапанавана выбраць месца захавання
Загрузка канала…
@@ -642,10 +638,10 @@
- %d выбраных
- %d выбраных
- Пустая назва групы
- Выдаліць гэту групу?
+ Назва групы пустая
+ Выдаліць групу?
Новая
- Паказаць толькі разгрупаваныя падпіскі
+ Паказваць толькі не згрупаваныя падпіскі
Запланаваныя
Паказваць «Збой плэера»
Запусціце праверку новых патокаў
@@ -667,13 +663,13 @@
Заўсёды абнаўляць
Парог абнаўлення стужкі
- Немагчыма загрузіць канал для «%s».
+ Не ўдалося загрузіць канал для «%s».
Стужка
Час пасля апошняга абнаўлення, перш чым падпіска лічыцца састарэлай — %s
Памылка загрузкі канала
Уліковы запіс аўтара быў спынены. \nNewPipe не зможа загрузіць гэты канал у будучыні. \nАдпісацца ад канала?
Рэжым хуткай загрузкі стужкі не дае дадатковай інфармацыі аб гэтым.
- Атрымлівайце са спецыяльнага канала, калі ён даступны
+ Атрыманне даных са спецыяльнага канала, калі ён ёсць
Уключыць хуткі рэжым
Катэгорыя
Тэгі
@@ -686,7 +682,7 @@
Трансляцыі, спампоўванне якіх яшчэ не падтрымліваецца, не паказваюцца
Мініяцюра аватара канала
Аўтар: %s
- Аўтару відэа спадабалася гэта
+ Спадабалася аўтару відэа
Створана %s
Адключыць хуткі рэжым
Публічная
@@ -699,7 +695,7 @@
Гэты кантэнт яшчэ не падтрымліваецца NewPipe.
\n
\nСпадзяюся, ён будзе падтрымлівацца ў наступных версіях.
- Старонка плэйліста
+ Старонка плэй-ліста
Паказваць мініяцюру
Выкарыстоўваць мініяцюру як фон для экрана блакіроўкі і апавяшчэнняў
Для гэтага дзеяння не знойдзены прыдатны файлавы менеджар. \nУсталюйце файлавы менеджар або паспрабуйце адключыць «%s» у наладах спампоўвання
@@ -713,10 +709,10 @@
Унутраная
Прагледжаныя цалкам
Гэты кантэнт даступны толькі для аплачаных карыстальнікаў, таму NewPipe не можа яго трансляваць або спампоўваць.
- Даступны ў некаторых службах, звычайна нашмат хутчэй, але можа вяртаць абмежаваную колькасць элементаў і часта няпоўную інфармацыю (напрыклад, без працягласці, тыпу элемента, без актыўнага стану)
+ Даступна для некаторых сэрвісаў, звычайна значна хутчэй, але можа перадаваць абмежаваную колькасць элементаў і не ўсю інфармацыю (можа адсутнічаць працягласць, тып элемента, паказчык трансляцыі)
Узроставае абмежаванне
Для гэтага дзеяння не знойдзены прыдатны файлавы менеджар. \nУсталюйце файлавы менеджар, сумяшчальны з Storage Access Framework
- Ніякая праграма на вашай прыладзе не можа адкрыць гэта
+ На прыладзе няма праграмы, каб адкрыць гэты файл
Стандартнае значэнне ExoPlayer
Прагледжаныя часткова
Лічыце, што загрузка каналаў адбываецца занадта павольна? Калі так, паспрабуйце ўключыць хуткую загрузку (можна змяніць у наладах або націснуўшы кнопку ніжэй). \n \nNewPipe прапануе два спосабы загрузкі каналаў: \n• Атрыманне ўсяго канала падпіскі. Павольны, але інфармацыя поўная). \n• Выкарыстанне спецыяльнай канчатковай кропкі абслугоўвання. Хуткі, але звычайна інфармацыя няпоўная). \n \nРозніца паміж імі ў тым, што ў хуткім звычайна адсутнічае частка інфармацыі, напрыклад, працягласць або тып (немагчыма адрозніць трансляцыі ад звычайных відэа), і ён можа вяртаць менш элементаў. \n \nYouTube з\'яўляецца прыкладам сэрвісу, які прапануе гэты хуткі метад праз RSS-канал. \n \nТакім чынам, выбар залежыць ад таго, чаму вы аддаяце перавагу: хуткасці або дакладнасці інфармацыя.
@@ -748,8 +744,8 @@
У гэтым патоку ўжо павінна быць гукавая дарожка
Уключыце гэту опцыю, калі ў вас ёсць праблемы з ініцыялізацыяй дэкодэра, якая вяртаецца да дэкодэраў з больш нізкім прыярытэтам, калі ініцыялізацыя асноўных дэкодэраў не ўдаецца. Гэта можа прывесці да нізкай прадукцыйнасці прайгравання, чым пры выкарыстанні асноўных дэкодэраў
Кіраванне некаторымі наладамі ExoPlayer. Каб гэтыя змены ўступілі ў сілу, патрабуецца перазапуск прайгравальніка
- Гэты абыходны шлях вызваляе і паўторна стварае відэакодэкі, калі адбываецца змяненне паверхні, замест таго, каб зажаваць паверхню непасрэдна для кодэка. Ужо выкарыстоўваецца ExoPlayer на некаторых прыладах з такой праблемай, гэты параметр ужываецца толькі на прыладах з Android 6 і вышэй\n\nУключэнне параметра можа прадухіліць памылкі прайгравання пры пераключэнні бягучага відэаплэера або пераключэнні ў поўнаэкранны рэжым
- Якасць выяў
+ Гэты абыходны шлях вызваляе і паўторна стварае відэакодэкі, калі адбываецца змяненне паверхні, замест таго, каб задаваць паверхню непасрэдна для кодэка. Ужо выкарыстоўваецца ExoPlayer на некаторых прыладах з такой праблемай, гэты параметр ужываецца толькі на прыладах з Android 6 і вышэй\n\nУключэнне параметра можа прадухіліць памылкі прайгравання пры пераключэнні бягучага відэаплэера або пераключэнні ў поўнаэкранны рэжым
+ Якасць відарысаў
Відэа
\?
Падпісчыкі
@@ -768,28 +764,28 @@
Атрыманне ўкладак канала
Аватары
Наступны паток
- Прадвызначана на вашай прыладзе адключана медыятунэляванне, бо гэтая мадэль прылады яго не падтрымлівае.
+ Прадвызначана на вашай прыладзе адключана тунэляванне мультымедыя, бо вядома, што гэта мадэль яго не падтрымлівае.
Аватары падканалаў
Адкрыць чаргу прайгравання
- Не загружаць выявы
+ Не загружаць відарысы
Высокая якасць
- Аб канале
- Абагуліць плэйліст
+ Пра канал
+ Абагуліць плэй-ліст
Пераматаць наперад
Альбомы
Пераматаць назад
Паўтарыць
- Атрыманыя ўкладкі пры абнаўленні стужкі. Гэты параметр не прымяняецца, калі канал абнаўляецца ў хуткім рэжыме.
+ Укладкі, для якіх атрымліваюцца даныя пры абнаўленні стужкі. Гэты параметр не дзейнічае, калі канал абнаўляецца з выкарыстаннем хуткага рэжыму.
Сярэдняя якасць
Загрузнік аватараў
Банеры
- Плэйлісты
+ Плэй-лісты
- %1$s: %2$s
Перамясціць панэль укладак уніз
Няма жывых трансляцый
- Выберыце якасць выяў і ці трэба спампоўваць выявы ўвогуле, каб паменшыць выкарыстанне даных і памяці. Змены ачышчаюць кэш выяў як у памяці, так і на дыску - %s
+ Выберыце якасць відарысаў ці ўвогуле не загружаць відарысы, каб паменшыць выкарыстанне даных і памяці. Змены ачышчаюць кэш відарысаў у памяці і на дыску (%s)
Прайграць
- Іншыя опцыі
+ Іншыя параметры
Мініяцюры
Трэкі
Працягласць
@@ -808,7 +804,7 @@
Каб адрэдагаваць кожнае з дзеянняў у апавяшчэнні, націсніце на яго. Першыя тры дзеянні (прайграванне/паўза, папярэдні і наступны) зададзены сістэмай, іх змяніць немагчыма.
Недастаткова вольнага месца на прыладзе
Так
- NewPipe можа аўтаматычна правяраць наяўнасць абнаўленняў і паведаміць вам, калі яны будуць даступны. \nУключыць гэту функцыю?
+ NewPipe можа час ад часу аўтаматычна правяраць наяўнасць новай версіі і апавяшчаць, калі яна будзе даступна. \nУключыць гэту функцыю?
Налады ў імпартаваным экспарце выкарыстоўваюць уразлівы фармат, які састарэў з версіі NewPipe 0.27.0. Пераканайцеся, што імпартаваны экспарт атрыманы з надзейнай крыніцы, і ў будучыні пераважней выкарыстоўваць толькі экспарт, атрыманы з NewPipe 0.27.0 ці навей. Падтрымка імпарту налад у гэтым уразлівым фармаце хутка будзе цалкам выдаленая, і тады старыя версіі NewPipe больш не змогуць імпартаваць наладкі з экспарту з новых версій.
Не
Рэзервовае капіяванне і аднаўленне
@@ -816,4 +812,12 @@
Скінуць усе налады на іх прадвызначаныя значэнні
Пры скіданні ўсіх налад будуць адхілены ўсе вашы змены налад і праграма перазапусціцца. \n \nСапраўды хочаце працягнуць?
другасны
+ Абагуліць як часовы плэйліст YouTube
+ Плэй-лісты
+ Выберыце групу каналаў
+ Група каналаў яшчэ не створана
+ Старонка групы каналаў
+ Пошук %1$s
+ Пошук %1$s (%2$s)
+ Спадабалася
diff --git a/app/src/main/res/values-ber/strings.xml b/app/src/main/res/values-ber/strings.xml
index 3912381fc..39247f49b 100644
--- a/app/src/main/res/values-ber/strings.xml
+++ b/app/src/main/res/values-ber/strings.xml
@@ -45,9 +45,6 @@
∞ ⵉⴼⵉⴷⵢⵓⵜⵏ
100+ ⵉⴼⵉⴷⵢⵓⵜⵏ
- ⴱ
- ⵎ
- ⴽ
ⴰⵎⵙⵍⴰⵢ
ⴰⴼⵉⴷⵢⵓ
ⵉⵔⵉⵜⵏ
diff --git a/app/src/main/res/values-bg/strings.xml b/app/src/main/res/values-bg/strings.xml
index 926498649..a8e9a75f0 100644
--- a/app/src/main/res/values-bg/strings.xml
+++ b/app/src/main/res/values-bg/strings.xml
@@ -183,9 +183,6 @@
Името на файла не може да бъде празно
Възникна грешка: %1$s
Не са налични източници за изтегляне
- хил.
- млн.
- млрд.
Няма абонати
Създай
Откажи
@@ -229,7 +226,7 @@
Съдържание на главната страница
Празна страница
Страница-павилион
- Страница на определен канал
+ Страница на канал
Изберете канал
За момента нямате абонаменти
Изберете павилион
@@ -432,7 +429,6 @@
Най-харесвани
Готово
Коментари
- Езикът ще се смени след рестартиране на приложението
Скрит
Частен
Предложения за отдалечено търсене
@@ -809,4 +805,14 @@
Винаги използвайте заобикаляне на настройката на повърхността на видеоизхода на ExoPlayer
Изтрий позиции за възпроизвеждане
вторичен
+ Споделяне като временен плейлист в YouTube
+ Плейлисти
+ Все още няма създадена група за емисии
+ Страница на групата канали
+ Изберете група емисии
+ Търсене %1$s
+ Търсене %1$s (%2$s)
+ Харесвания
+ Страница SoundCloud Top 50 е премахната
+ SoundCloud преустанови оригиналните класации Топ 50. Съответният раздел е премахнат от главната ви страница.
diff --git a/app/src/main/res/values-bn-rBD/strings.xml b/app/src/main/res/values-bn-rBD/strings.xml
index ddc32e418..2b0ffe918 100644
--- a/app/src/main/res/values-bn-rBD/strings.xml
+++ b/app/src/main/res/values-bn-rBD/strings.xml
@@ -82,9 +82,6 @@
ভিডিও
অডিও
পুনরায় চেষ্টা করো
- হা
- M
- বি
শুরু
বিরতি
diff --git a/app/src/main/res/values-bn/strings.xml b/app/src/main/res/values-bn/strings.xml
index 14201bc9e..23ad8e947 100644
--- a/app/src/main/res/values-bn/strings.xml
+++ b/app/src/main/res/values-bn/strings.xml
@@ -104,9 +104,6 @@
কোন ভিডিও নেই
কোন ভিউ নেই
কোন সাবস্ক্রাইবার নেই
- B
- M
- K
পুনরায় চেষ্টা করো
অডিও
ভিডিও
@@ -534,7 +531,6 @@
প্রত্যেক ডাউনলোড কোথায় রাখা হবে তা জিজ্ঞেস করা হবে।
\nমেমোরি কার্ডে ডাউনলোড করতে সিস্টেম ফোল্ডার পিকার (SAF) এনেবল করুন
এই নামের একটি ডাউনলোড চলমান
- অ্যাপ আবার শুরু হলে ভাষা পাল্টাবে
মিডিয়া সুরঙ্গকরণ অক্ষম
দ্রুত ফিড অবস্থা এ বিষয়ে এর বেশি তথ্য দেয় না।
কোনো ডাউনলোড ফোল্ডার নির্দিষ্ট করা হয়নি, এখনই একটা সহজাত ডাউনলোড ফোল্ডার নির্বাচন করো
diff --git a/app/src/main/res/values-bqi/strings.xml b/app/src/main/res/values-bqi/strings.xml
new file mode 100644
index 000000000..a6b3daec9
--- /dev/null
+++ b/app/src/main/res/values-bqi/strings.xml
@@ -0,0 +1,2 @@
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values-br/strings.xml b/app/src/main/res/values-br/strings.xml
new file mode 100644
index 000000000..a0a7744f6
--- /dev/null
+++ b/app/src/main/res/values-br/strings.xml
@@ -0,0 +1,121 @@
+
+
+ Nullañ
+ Mat eo
+ Ket
+ Digeriñ e-barzh ar merdeer
+ Digeriñ gant
+ Rannañ
+ Pellgargañ
+ Klask a raec\'h \"%1$s\"?
+ Rannañ gant
+ Arverañ ul lenner aodio diavaez
+ Koumanantiñ
+ Digoumanantiñ
+ N\'haller ket kemmañ ar c\'houmanant
+ N\'haller ket hizivaat ar c\'houmanant
+ Diskouez an titouroù
+ Rolloù-lenn enrollet
+ Rolloù-lenn
+ Dibab un ivinell
+ Drekleur
+ Diflugell
+ Ouzhpennañ da
+ Teuliad pellgargañ ar videoioù
+ Amañ e vez kadavet ar restroù aodio pellgarget
+ Amañ e vez kadavet ar restroù video pellgarget
+ O kargañ
+ Netra
+ Mentrezh aodio dre ziouer
+ Dodenn
+ Dodenn noz
+ Sklaer
+ Teñval
+ Tregern
+ Roll istor enklask
+ Pellgargañ
+ Lañsañ al lenner pennañ e mod skramm a-bezh
+ Lenn emgefreek
+ Bro an endalc\'had dre ziouer
+ Erioloù PeerTube
+ Tra ebet
+ Mentrezh video dre ziouer
+ Lenn mell-divell
+ Aodio
+ Lenn gant Kodi
+ Lintr
+ Skarzhañ ar roadennoù
+ Klask
+ Arverañ ul lenner video diavaez
+ Teuliad pellgargañ ar restroù aodio
+ Du
+ Kinnigoù enklask
+ Kenderc\'hel al lenn
+ URL anskor
+ Yezh an endalc\'had dre ziouer
+ Pellgargañ restr al lanv
+ Staliañ
+ Ya
+ Koumanantoù
+ Arventennoù
+ Setu an disoc\'hoù evit: %s
+ Lamet e vez an aodio gant diarunustedoù \'zo
+ Digoumanantet oc\'h bet d\'ar chadenn
+ Sellet ouzh ar roll istor
+ Koumanantet
+ Diarunusted dre ziouer
+ Diuzit hoc\'h erioloù PeerTube gwell ganeoc\'h
+ Kavit an erioloù a blij deoc\'h war %s
+ Ouzhpennañ un eriol
+ Lenner
+ Video hag aodio
+ Roll istor ha krubuilh
+ Neuz
+ Diveugañ
+ Hizivadurioù
+ Rebuzadur al lenner
+ Assav ha gwarediñ
+ War-eeun
+ Pellgargadurioù
+ Pep tra
+ Chadennoù
+ Videoioù
+ Loabroù
+ Arveriaded
+ Degouezhioù
+ Tonioù
+ Albomoù
+ Arzourien
+ Diweredekaet
+ Skarzhañ
+ Dizober
+ Dilamet eo bet ar restr
+ Lenn pep tra
+ Atav
+ Restr
+ Rebuzadurioù
+ Rebuzadur NewPipe
+ Rebuzadurioù evit al lenner NewPipe
+ Rebuzadurioù evit handelvoù nevez NewPipe
+ Lanvioù nevez
+ Ur wech nemetken
+ Diarunusted wellañ
+ Fazi
+ Rebuzadur hizivadur an arload
+ Endalc\'had
+ Emzalc\'h
+ Rolloù-lenn
+ Pellgargadurioù
+ Sevel un danevell
+ Munudoù:
+ Aodio
+ Klask en-dro
+ Deskrivadur
+ Disoc\'h ebet
+ Endalc’had ebet
+ Petra zo c\'hoarvezet:
+ Lenn ar video, pad:
+ Titouroù:
+ Video
+
+
diff --git a/app/src/main/res/values-ca/strings.xml b/app/src/main/res/values-ca/strings.xml
index 1a9463d05..6e266509f 100644
--- a/app/src/main/res/values-ca/strings.xml
+++ b/app/src/main/res/values-ca/strings.xml
@@ -54,6 +54,7 @@
Àudio
- %s subscriptor
+ - %s subscriptors
- %s subscriptors
D\'acord
@@ -169,11 +170,13 @@
Cap reproducció
- %s reproducció
+ - %s reproduccions
- %s reproduccions
Cap vídeo
- %s vídeo
+ - %s vídeos
- %s vídeos
Pausa
@@ -228,9 +231,6 @@
S\'està recuperant el reproductor després de l\'error
Bé, és lamentable.
Arrossegueu per reordenar la llista
- mil
- milions
- mil milions
Inicia
Feu un toc aquí per a més detalls
Defineix una carpeta de baixades més endavant als paràmetres
@@ -274,15 +274,7 @@
La supervisió de fugues de memòria pot fer que l\'aplicació deixi de respondre mentre es bolca la memòria
Informa d\'errors fora del cicle de vida
Força l\'informe d\'excepcions Rx que no es puguin transmetre que tinguin lloc fora del cicle de vida d\'un fragment o activitat després de disposar-los
- Importeu les vostres subscripcions de YouTube mitjançant la còpia de contingut de Google Takeout:
-\n
-\n1. Aneu a : %1$s
-\n2. Inicieu la sessió si se us demana
-\n3. Premeu \"Totes les dades incloses\", després \"Dessel·lecciona-ho tot\", llavors sel·leccioneu només \"Subscripcions\" i finalment premeu \"D\'acord\".
-\n4. Premeu \"Pas següent\" i llavors a \"Crea una exportació\"
-\n5. Premeu el botó \"Baixa\" un cop hagi aparegut
-\n6. Premeu a IMPORTA EL FITXER i sel·leccioneu el fitxer .zip descarregat
-\n7. [En cas que la importació del fitxer .zip hagi fallat] extreieu-ne el fitxer subscripcions.csv (es troba generalment a \"Takeout/YouTube i YouTube Music/subscripcions/subscripcions.csv\"), premeu a IMPORTA EL FITXER i sel·leccioneu el fitxer .csv extret.
+ Importeu les vostres subscripcions de YouTube mitjançant la còpia de contingut de Google Takeout: \n \n1. Aneu a : %1$s \n2. Inicieu la sessió si se us demana \n3. Premeu \"Totes les dades incloses\", després \"Dessel·lecciona-ho tot\", llavors sel·leccioneu només \"Subscripcions\" i finalment premeu \"D\'acord\". \n4. Premeu \"Pas següent\" i llavors a \"Crea una exportació\" \n5. Premeu el botó \"Baixa\" un cop hagi aparegut \n6. Premeu a IMPORTA EL FITXER i sel·leccioneu el fitxer .zip descarregat \n7. [En cas que la importació del fitxer .zip hagi fallat] extreieu-ne el fitxer subscripcions.csv (es troba generalment a \"Takeout/YouTube i YouTube Music/subscripcions/subscripcions.csv\"), premeu a IMPORTA EL FITXER i sel·leccioneu el fitxer .csv extret
Importeu un perfil del SoundCloud mitjançant l\'URL o l\'identificador del vostre perfil:
\n
\n1. Activeu el «Mode d\'ordinador» en un navegador (el lloc web no està disponible per a dispositius mòbils)
@@ -407,7 +399,7 @@
El fitxer s\'ha mogut o suprimit
Només una baixada alhora
Fes servir el SAF
- El SAF (Storage Access Framework; estructura d\'accés a l\'emmagatzematge) us permet realitzar baixades a una memòria externa com una targeta SD.
+ El SAF (Storage Access Framework; estructura d\'accés a l\'emmagatzematge) us permet realitzar baixades a una memòria externa com una targeta SD
Esborra les posicions de reproducció
Esborra totes les posicions de reproducció
Voleu suprimir tots els punts de reproducció\?
@@ -415,14 +407,15 @@
Cap visualització
- %s visualització
+ - %s visualitzacions
- %s visualitzacions
Cap reproducció
- %s escoltant
+ - %s escoltants
- %s escoltants
- Es canviarà l\'idioma en reiniciar l\'aplicació
Tendències
Ensenya el temps passat original sobre els \"items\"
Auto-generat (no es troba cap uploader)
@@ -477,17 +470,18 @@
Actualitza sempre
Temps que ha de passar perquè una subscripció es consideri obsoleta — %s
Llindar d\'actualització del contingut
- Feed
+ Flux
Mostra només les subscripcions sense grup
Nou
Esteu segurs de voler suprimir aquest grup\?
Nom de grup buit
- - %d de sel·leccionat
- - %d de sel·leccionats
+ - %d de seleccionat
+ - %d de seleccionats
+ - %d de seleccionats
- Cap subscripció sel·leccionada
- Sel·leccioneu les subscripcions
+ Cap subscripció seleccionada
+ Selecciona subscripcions
Processant el contingut…
Carregant el contingut…
No carregat: %d
@@ -495,18 +489,22 @@
Grups de canals
- %d dia
+ - %d dies
- %d dies
- %d hora
+ - %d hores
- %d hores
- %d minut
+ - %d minuts
- %d minuts
- %d segon
+ - %d segons
- %d segons
A causa de les limitacions d\'ExoPlayer, la durada de cerca és de %d segons
@@ -643,13 +641,12 @@
Comprovar manualment si hi ha noves versions
- Baixada finalitzada
+ - %s baixades finalitzades
- %s baixades finalitzades
Vista prèvia de les miniatures de la barra de cerca
- No s\'ha trobat cap gestor de fitxers adequat per a aquesta acció.
-\nInstal·leu un gestor de fitxers compatible amb l\'entorn d\'accés d\'emmagatzematge.
- No s\'ha trobat cap gestor de fitxers adequat per a aquesta acció.
-\nInstal·leu un gestor de fitxers o intenteu desactivar «%s» als paràmetres de baixada.
+ No s\'ha trobat cap gestor de fitxers adequat per a aquesta acció. \nInstal·leu un gestor de fitxers compatible amb l\'entorn d\'accés d\'emmagatzematge
+ No s\'ha trobat cap gestor de fitxers adequat per a aquesta acció. \nInstal·leu un gestor de fitxers o intenteu desactivar «%s» als paràmetres de baixada
S\'ha produït un error, consulteu la notificació
Afegit el següent vídeo a la cua
NewPipe ha trobat un error, toca per informar
@@ -660,6 +657,7 @@
S\'estan comprovant les actualitzacions…
- S\'ha suprimit %1$s baixada
+ - S\'han suprimit %1$s baixades
- S\'han suprimit %1$s baixades
A partir de l\'Android 10 només s\'admet el \"Sistema d\'Accés a l\'Emmagatzematge\"
@@ -689,8 +687,8 @@
Format desconegut
Cualitat desconeguda
Ordenar
- Configura la notificació de reproducció actual.
- Canvia la mida de l\'interval de càrrega en continguts progressius (actualment %s). Un valor inferior pot accelerar la càrrega inicial del vídeo.
+ Configura la notificació de reproducció actual
+ Canvia la mida de l\'interval de càrrega en continguts progressius (actualment %s). Un valor inferior pot accelerar la càrrega inicial del vídeo
Ignora els esdeveniments dels botons de reproducció físics
Útil, per exemple, si feu servir uns auriculars amb els botons físicament trencats
Trieu un gest per la part esquerra de la pantalla
@@ -733,4 +731,95 @@
Duplicat afegit/s %d vegada/es
El túnel multimèdia s\'ha desactivat de manera predeterminada al dispositiu perquè se sap que el vostre model de dispositiu no ho permet.
Semiton
+ Estàs fent servir la darrera versió de NewPipe
+ No hi ha prou espai lliure al dispositiu
+ Llistes de reproducció
+ Targeta
+ Vols suprimir tots els elements duplicats d\'aquesta llista de reproducció?
+ Llistes de reproducció
+ Suprimeix els duplicats
+ Restableix la configuració
+ NewPipe pot cercar automàticament actualitzacions i fer-t\'ho saber en estar disponibles.\nVols habilitar-ho?
+ Suprimeixo els duplicats?
+ Restableix tots els paràmetres als valors per defecte
+ Restablir tots els paràmetres descartarà els teus paràmetres preferits i reiniciarà l\'aplicació.\n\nN\'estàs segur?
+ Clica per descarregar%s
+ doblat
+ Commuta-ho tot
+ Pròximament
+ En directe
+ Reprodueix
+ Torna a reproduir
+ Més opcions
+ Comparteix la llista dels URLs
+ - %1$s: %2$s
+ Avatars de l\'autor
+ Avatars del sots-canal
+ Subscriptors
+ Ja hi hauria d\'haver una pista d\'àudio en aquest flux
+ El contingut escollit no és suportat per cap reproductor extern
+ No hi ha cap flux de vídeo disponible per a reproductors externs
+ Escull la pista d\'àudio per a reproductors externs
+ Desconegut
+ %1$s%2$s
+ original
+ descriptiu
+ Vídeos
+ Quines pestanyes es mostren a les pàgines del canal
+ Obre la cua de reproducció
+ Canvia l\'orientació de la pantalla
+ Vídeo anterior
+ Avança
+ Qualitat de la imatge
+ No carregues les imatges
+ Qualitat mitjana
+ Qualitat baixa
+ Comparteix com a llista de reproducció temporal de Youtube
+ %1$s\n%2$s
+ Mostra més
+ Avatars
+ Bàners
+ Durada
+ Rebobina
+ Comparteix amb els títols
+ No es mostren els contiguts que no suporten descàrrega
+ Mostra els vídeos següents
+ No hi ha cap flux d\'àudio disponible per a reproductors externs
+ Mostra/Amaga els vídeos
+ Aquesta opció només està disponible si%ss\'ha seleccionat per al tema
+ Vídeo següent
+
+ - %sresposta
+ - %srespostes
+ - %srespostes
+
+ Recupera les pestanyes del canal
+ Valor per defecte d\'ExoPlayer
+ Miniatures
+ Quant a
+ Usa sempre la sortida de vídeo d\'ExoPlayer com a solució de contingència
+ La configuració exportada que vols importar té un format vulnerable que és obsolet des de NewPipe 0.27.0. Assegura\'t que l\'exportació que vols importar prové d\'una font de confiança i prefereix només les exportacions fetes amb NewPipe 0.27.0 o posterior d\'ara endavant. El suport a la importació de configuracions en aquest format vulnerable aviat serà suprimit completament i aleshores les antigues versions de NewPipe ja no podran importar les exportacions de les configuracions des de les noves versions.
+ Àlbums
+ Pestanyes del canal
+ Qualitat alta
+ Tria la qualitat de les imatges i si carregar-les totalment o no per reduir l\'ús de les dades i la memòria. Els canvis suprimiran la memòria cau de les imatges a la memòria i al disc — %s
+ Pestanyes que es recuperaran en actualitzar el contingut. Aquesta opció no s\'aplica si el canal s\'actualitza en mode ràpid.
+ Vist parcialment
+ Vist completament
+ Paràmetres d\'ExoPlayer
+ Gestiona alguns paràmetres d\'ExoPlayer. Caldrà reinciciar el reproductor per activar-los
+ Usa la funció de suport de decodificació d\'ExoPlayer
+ Habilita aquesta opció si tens problemes en iniciar el decodificador. S\'usaran decodificadors alternatius de baixa prioritat si falla el decodificador primari. Això pot provocar una disminució de la qualitat de la reproducció en relació a l\'ús del decodificador primari
+ Aquesta alternativa allibera i reinstancia els còdecs de vídeo si hi ha un canvi de màscara en lloc de configurar-la directament al còdec. ExoPlayer ja ho aplica en alguns dispositius amb aquest problema. Aquesta configuració només té efecte en Android 6 i posteriors\n\nHabilitar aquest opció pot prevenir errors de reproducció en canviar el reproductor actual o en passar a pantalla completa
+ Pistes
+ Curts
+ Canvia a pantalla completa
+ \?
+ Comparteix la llista de reproducció
+ Mostra menys
+ secundària
+ Canals
+ Encara no s\'ha creat cap grup de continguts
+ Tria un grup de continguts
+ Pàgina del grup de canals
diff --git a/app/src/main/res/values-ckb/strings.xml b/app/src/main/res/values-ckb/strings.xml
index 5d6c1d9d9..f532cce70 100644
--- a/app/src/main/res/values-ckb/strings.xml
+++ b/app/src/main/res/values-ckb/strings.xml
@@ -25,7 +25,6 @@
ژمارەی بەژداری نادیارە
ناتوانرێت لەسەر ئەو فایلهوه جێگیر بکرێت
پهڕه هەڵبژێرە
- ملیۆن
+١٠٠ ڤیدیۆیان
لێدهر
هاوردە
@@ -46,7 +45,6 @@
ئەمە لەسەر ڕێکخستنەکانی ئێستات جێگیر دەبێت.
پەیامەکانی نیوپایپ
نیوپایپ لەلایەن چەند خۆبەخشێکەوە دروستکراوە کە کاتهكانی خۆیان پێ بەخشیووە تاکو باشترین خزمەتگوزاریت پێشکەش بکەن. هیچ نەبێت بە کڕینی کوپێک قاوە یارمەتی گەشەپێدەرەکانمان بدە بۆ ئەوەی کاتی زیاتر تەرخان بکەین بۆ بەرەوپێشبردنی نیوپایپ.
- ملیار
گەڕانی پێشنیارکراوەکان
خێرا
فایل سڕایەوە
@@ -107,7 +105,6 @@
ناتوانرێت بهژداریكردنهكه نوێبكرێتهوه
پشت شاشە
بێ ئەنجامه
- زمان دەگۆڕدرێت لەدوای داگیرساندنەوەی بهرنامهكه
لادانی سەیرکراو
پیشاندانی نیشانەکەری شوێنی کارپێکەر لە خشتەکاندا
شوێنەکان لە خشتەکاندا
@@ -374,7 +371,6 @@
ناتوانرێت داببهزێنرێت
ناتوانرێت بە ڕاژەكهوە پەیوەست ببیت
لێدانی ڤیدیۆ، مهودا:
- هەزار
زۆرترین بەدڵ
سڕینەوە
جۆری بنەڕەتی ڤیدیۆ
diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml
index bb0a1ec7b..169b6e0ba 100644
--- a/app/src/main/res/values-cs/strings.xml
+++ b/app/src/main/res/values-cs/strings.xml
@@ -57,7 +57,7 @@
Jméno souboru
Vlákna
Zastavit
- Smazat
+ Odstranit
Start
Zkusit znovu
Video
@@ -83,9 +83,7 @@
Určete prosím složku pro stahování později v nastavení
Co:\\nŽádost:\\nJazyk obsahu:\\nZemě obsahu:\\nJazyk aplikace:\\nSlužba:\\nČas GMT:\\nBalíček:\\nVerze:\\nVerze OS:
Vše
- tis.
Otevřít ve vyskakovacím okně
- mil.
Toto oprávnění je vyžadováno
\npro otevření ve vyskakovacím okně
Odstraňuje zvuk v některých rozlišeních
@@ -124,7 +122,6 @@
Oznámení pro NewPipe přehrávač
Žádné výsledky
Je tu sranda jak v márnici
- mld.
Žádní odběratelé
- %s odběratel
@@ -139,9 +136,9 @@
Žádná videa
- - %s Video
- - %s Videa
- - %s Videí
+ - %s video
+ - %s videa
+ - %s videí
Stahování
Povolené znaky v názvech souborů
@@ -234,8 +231,8 @@
Přidat do playlistu
Nastavit jako náhled playlistu
Přidat playlist do záložek
- Smazat záložku
- Smazat tento playlist\?
+ Odstranit záložku
+ Odstranit tento playlist?
Playlist vytvořen
V playlistu
Náhled playlistu změněn.
@@ -411,9 +408,9 @@
Zobrazit pozici přehrávání v seznamech
Pozice playbacku smazány
Timeout spojení
- Smazat pozice playbacku
- Smazat všechny pozice playbacku
- Smazat všechny pozice playbacku\?
+ Vymazat pozice přehrávání
+ Vymaže všechny pozice přehrávání
+ Vymazat všechny pozice přehrávání?
Přepnout službu, právě vybráno:
Nikdo nesleduje
@@ -427,7 +424,6 @@
- %s posluchači
- %s posluchačů
- Ke změně jazyka dojde po restartu aplikace
Výchozí kiosek
Délka přetočení vpřed/zpět
Instance PeerTube
@@ -445,8 +441,8 @@
obnovuji
Toto stahování nelze obnovit
Vyberte instanci
- Smazat historii stahování
- Smazat stažené soubory
+ Vymazat historii stahování
+ Odstranit stažené soubory
Souhlasit se zobrazením přes jiné aplikace
Jazyk aplikace
Jazyk systému
@@ -489,7 +485,7 @@
- %d vybráno
Prázdné jméno skupiny
- Přejete si smazat tuto skupinu\?
+ Přejete si odstranit tuto skupinu?
Nová
Novinky
Limit aktualizace novinek
@@ -688,7 +684,7 @@
Frekvence kontroly
Jakákoli síť
Požadované síťové připojení
- Smazat všechny stažené soubory z disku\?
+ Odstranit všechny stažené soubory z disku?
Objednali jste si nyní tento kanál
Všechny přepnout
Nové streamy
@@ -837,4 +833,14 @@
\nChcete tuto funkci povolit?
Nastavení v importovaném exportu používají zranitelný formát. NewPipe používá nový formát od verze 0.27.0. Ujistěte se, že export importujete z důvěryhodného zdroje a v budoucnu upřednostňujte používání exportů získaných z NewPipe 0.27.0 nebo novějších. Podpora importu nastavení v tomto zranitelném formátu bude brzy kompletně odstraněna, kvůli čemuž staré verze NewPipe nebudou moci importovat nastavení z exportů z nových verzí.
sekundární
+ Sdílet jako dočasný playlist YouTube
+ Playlisty
+ Vybrat skupinu kanálů
+ Zatím nebyla vytvořena žádná skupina kanálů
+ Stránka skupiny kanálů
+ Hledat %1$s
+ Hledat %1$s (%2$s)
+ Líbí se
+ Stránka SoundCloud Top 50 odstraněna
+ SoundCloud zrušil původní žebříčky Top 50. Příslušná karta byla odstraněna z vaší hlavní stránky.
diff --git a/app/src/main/res/values-da/strings.xml b/app/src/main/res/values-da/strings.xml
index 0312b3c74..fb63a9387 100644
--- a/app/src/main/res/values-da/strings.xml
+++ b/app/src/main/res/values-da/strings.xml
@@ -305,9 +305,6 @@
Stop
Hændelser
Ikke andet end fårekyllinger her
- t
- mio.
- mia.
- %s abonnent
- %s abonnenter
@@ -507,7 +504,6 @@
Stryg på elementer for at fjerne dem
Vælg en playliste
Ingen playliste-bogmærker endnu
- Sproget ændres, når appen genstarter
Afspillerkø
Vis kanalens detaljer
Sæt i kø
@@ -823,4 +819,9 @@
Indstillingerne i den eksport, der importeres, bruger et sårbart format, der er blevet forældet siden NewPipe 0.27.0. Sørg for, at den eksport, der importeres, er fra en pålidelig kilde, og brug helst kun eksport fra NewPipe 0.27.0 eller nyere i fremtiden. Understøttelse af import af indstillinger i dette sårbare format fjernes snart helt, og så vil gamle versioner af NewPipe ikke længere være i stand til at importere indstillinger fra eksport fra nye versioner.
Sikkerhedskopiering og gendannelse
sekundær
+ Del som midlertidig YouTube-playliste
+ Playlister
+ Kanalgruppeside
+ Vælg en feed-gruppe
+ Ingen feed-gruppe oprettet endnu
diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml
index e782e700a..26fc13641 100644
--- a/app/src/main/res/values-de/strings.xml
+++ b/app/src/main/res/values-de/strings.xml
@@ -69,9 +69,6 @@
Fehlerbericht
Löschen
Prüfsumme
- Tsd.
- Mio.
- Mrd.
Dateiname
Fehler
Bitte warten …
@@ -432,7 +429,6 @@
- %s Zuhörer
- %s Zuhörer
- Die Sprache ändert sich, sobald die App neu gestartet wird
PeerTube-Instanzen
Finde auf %s die Instanzen, die dir gefallen
Instanz hinzufügen
@@ -587,7 +583,7 @@
Keine App auf deinem Gerät kann dies öffnen
Dieser Inhalt ist privat, kann also nicht von NewPipe gestreamt oder heruntergeladen werden.
Diese Inhalte sind nur für Benutzer verfügbar, die bezahlt haben, können also nicht von NewPipe gestreamt oder heruntergeladen werden.
- Dieses Video ist nur für YouTube Music Premium-Mitglieder verfügbar und kann daher nicht von NewPipe gestreamt oder heruntergeladen werden.
+ Dieses Video ist nur für YouTube-Music-Premium-Mitglieder verfügbar und kann daher nicht von NewPipe gestreamt oder heruntergeladen werden.
Dies ist ein SoundCloud Go+ Track, zumindest in deinem Land, kann er von NewPipe nicht gestreamt oder heruntergeladen werden.
Dieser Inhalt ist in deinem Land nicht verfügbar.
App abstürzen lassen
@@ -823,4 +819,14 @@
\nMöchtest du wirklich fortfahren?
Die Einstellungen in dem zu importierenden Export verwenden ein angreifbares Format, das seit NewPipe 0.27.0 veraltet ist. Stellen Sie sicher, dass der zu importierende Export aus einer vertrauenswürdigen Quelle stammt, und verwenden Sie in Zukunft nur noch Exporte, die aus NewPipe 0.27.0 oder neuer stammen. Die Unterstützung für den Import von Einstellungen in diesem angreifbaren Format wird bald vollständig entfernt werden, und dann werden alte Versionen von NewPipe nicht mehr in der Lage sein, Einstellungen von Exporten aus neuen Versionen zu importieren.
Sekundär
+ Als temporäre YouTube-Wiedergabeliste teilen
+ Wiedergabelisten
+ Eine Feed-Gruppe auswählen
+ Kanalgruppen-Seite
+ Es wurde noch keine Feed-Gruppe erstellt
+ Suche %1$s
+ Suche %1$s (%2$s)
+ Gefällt mir
+ SoundCloud-Top-50-Seite entfernt
+ SoundCloud hat die ursprünglichen Top-50-Charts abgeschafft. Der entsprechende Tab wurde von deiner Hauptseite entfernt.
diff --git a/app/src/main/res/values-el/strings.xml b/app/src/main/res/values-el/strings.xml
index c0b09ad13..82041e3b4 100644
--- a/app/src/main/res/values-el/strings.xml
+++ b/app/src/main/res/values-el/strings.xml
@@ -1,7 +1,7 @@
Δημοσιεύθηκε στις %1$s
- Δε βρέθηκε πρόγραμμα αναπαραγωγής. Εγκατάσταση του VLC;
+ Δε βρέθηκε αναπαραγωγός ροής δεδομένων. Εγκατάσταση του VLC;
Εγκατάσταση
Άκυρο
Άνοιγμα σε πρόγραμμα περιήγησης
@@ -41,7 +41,6 @@
Μικρογραφία εικόνας προφίλ του χρήστη
Like
Dislike
- δισ/ρια
Άνοιγμα σε αναδυόμενο παράθυρο
Εγγραφή
Εγγεγραμμένος
@@ -71,7 +70,7 @@
Ιστορικό
Εμφάνιση πληροφοριών
Πατήστε το μεγεθυντικό φακό για να ξεκινήσετε.
- Δε βρέθηκε πρόγραμμα αναπαραγωγής ροής δεδομένων (μπορείτε να εγκαταστήσετε το VLC για να κάνετε αναπαραγωγή).
+ Δε βρέθηκε αναπαραγωγός ροής δεδομένων (μπορείτε να εγκαταστήσετε το VLC για να κάνετε αναπαραγωγή).
Λήψη του αρχείου ροής
Αφαιρείται ο ήχος από κάποιες αναλύσεις
Το κανάλι διαγράφηκε
@@ -103,7 +102,7 @@
Ανάκτηση αναπαραγωγής
Συνέχιση της αναπαραγωγής έπειτα από διακοπές (π.χ. κλήσεις)
Εμφάνιση επεξήγησης του «Πιέστε παρατεταμένα για προσθήκη στην ουρά»
- Εμφάνιση υπόδειξης όταν πατηθεί το κουμπί παρασκηνίου ή αναδυόμενου παραθύρου στις \"Λεπτομέρειες:\\ στο βίντεο
+ Εμφάνιση συμβουλής κατά το πάτημα του φόντου ή του αναδυόμενου κουμπιού στο βίντεο «Λεπτομέρειες:»
Προεπιλεγμένη χώρα περιεχομένου
Αναπαραγωγός
Συμπεριφορά
@@ -169,8 +168,6 @@
Δεν υπάρχει τίποτα εδώ
Σύρετε για ταξινόμηση
Προσπάθεια εκ νέου
- χιλ.
- εκ/ρια
Κανένας συνδρομητής
- %s συνδρομητής
@@ -184,7 +181,7 @@
Κανένα βίντεο
- %s βίντεο
- - %s βίντεο
+ - %s βίντεο(πολλά)
Εκκίνηση
Δημιουργία
@@ -422,7 +419,6 @@
- %s ακροατής
- %s ακροατές
- Η γλώσσα θα αλλάξει αφού επανεκκινηθεί η εφαρμογή
Προεπιλεγμένο περίπτερο
Μόνο HTTPS σύνδεσμοι υποστηρίζονται
Τοπικά
@@ -472,7 +468,7 @@
Αυτό το βίντεο έχει περιορισμό ηλικίας.
\n
\nΕνεργοποιήστε το «%1$s» στις ρυθμίσεις εάν θέλετε να το δείτε.
- Ενεργοποίηση \"Περιορισμένη Λειτουργία\\ του YouTube
+ Ενεργοποίηση \"Περιορισμένη Λειτουργία\" του YouTube
Δεν ήταν δυνατή η αναγνώριση της διεύθυνσης URL. Άνοιγμα με άλλη εφαρμογή;
Αυτόματη προσθήκη στην ουρά
Η ουρά του ενεργού αναπαραγωγού θα αντικατασταθεί
@@ -654,7 +650,7 @@
Χειροκίνητος έλεγχος για νέα έκδοση
Έλεγχος αναβάθμισης
Νέα αντικείμενα τροφοδοσίας
- Εμφάνιση «Κατάρρευση αναπαραγωγέα\\
+ Εμφάνιση \"Κατάρρευση αναπαραγωγέα\"
Εμφανίζει μια επιλογή κατάρρευσης κατά τη χρήση του αναπαραγωγέα
Κατάρρευση αναπαραγωγέα
Ειδοποίηση αναφοράς σφάλματος
@@ -823,4 +819,14 @@
\nΕίστε βέβαιοι ότι θέλετε να συνεχίσετε;
Οι ρυθμίσεις στην εξαγωγή που εισάγεται χρησιμοποιούν μια ευάλωτη μορφή που είχε καταργηθεί από το NewPipe 0.27.0. Βεβαιωθείτε ότι η εξαγωγή που εισάγεται προέρχεται από αξιόπιστη πηγή και προτιμήστε να χρησιμοποιείτε μόνο εξαγωγές που λαμβάνονται από το NewPipe 0.27.0 ή νεότερο στο μέλλον. Η υποστήριξη για εισαγωγή ρυθμίσεων σε αυτήν την ευάλωτη μορφή θα καταργηθεί σύντομα εντελώς και, στη συνέχεια, οι παλιές εκδόσεις του NewPipe δεν θα μπορούν πλέον να εισάγουν ρυθμίσεις εξαγωγών από νέες εκδόσεις.
δευτερεύων
+ Λίστες αναπαραγωγής
+ Μοιραστείτε ως προσωρινή λίστα αναπαραγωγής στο YouTube
+ Επιλογή ομάδας τροφοδοσίας
+ Δεν δημιουργήθηκε ομάδα τροφοδοσίας ακόμα
+ Σελίδα καναλιού ομάδας
+ Αναζήτηση %1$s
+ Αναζήτηση %1$s (%2$s)
+ Likes
+ Η σελίδα των SoundCloud Top 50 αφαιρέθηκε
+ Το SoundCloud έχει καταργήσει τα αρχικά charts με τα Top 50. Η αντίστοιχη καρτέλα έχει αφαιρεθεί από την κύρια σελίδα σας.
diff --git a/app/src/main/res/values-eo/strings.xml b/app/src/main/res/values-eo/strings.xml
index 66e2c4d10..408035e00 100644
--- a/app/src/main/res/values-eo/strings.xml
+++ b/app/src/main/res/values-eo/strings.xml
@@ -261,9 +261,6 @@
- %s spekto
- %s spektoj
- k
- M
- Mrd
Pri NewPipe
Eksteraj permesiloj
© %1$s de %2$s sub %3$s
@@ -421,7 +418,6 @@
- %s aŭskultanto
- %s aŭskultantoj
- La lingvo ŝanĝos kiam la apo restartos
Daŭro de rapidpluiga/revolva serĉo
Instancoj de PeerTube
Elekti viajn preferitajn instancojn de PeerTube
diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml
index c1efd2ee7..41a44f3ba 100644
--- a/app/src/main/res/values-es/strings.xml
+++ b/app/src/main/res/values-es/strings.xml
@@ -47,7 +47,7 @@
Miniatura del avatar del usuario
Contenido
Mostrar contenido con restricción de edad
- Toca la lupa para comenzar..
+ Toca la lupa para comenzar.
En directo
Descargas
Descargas
@@ -78,12 +78,9 @@
Copiado al portapapeles
Defina una carpeta de descargas más tarde en los ajustes
La interfaz de la aplicación dejó de funcionar
- Qué:\\nSolicitar:\\nIntenga de contenido:\\nPaíse de contenido:\\nIdiomaño de la aplicación:\\nServicio:\\nTiempo de GTT:\\nPaquete:\\nVersión:\\nVersion de SO:
+ Qué:\\nSolicitud:\\nIdioma del contenido:\\nPaís del contenido:\\nIdioma de la aplicación:\\nServicio:\\nMarca de tiempo:\\nPaquete:\\nVersión:\\nVersión del SO:
Negro
Todo
- k
- M
- MM
Abrir en modo emergente
Se necesita este permiso
\npara abrir en modo emergente
@@ -91,7 +88,7 @@
Reto reCAPTCHA requerido
Reproduciendo en modo emergente
Formato de vídeo predefinido
- Desactivado
+ Deshabilitado
Mostrar resoluciones más altas
Solo algunos dispositivos pueden reproducir vídeos en 2K / 4K
Resolución predefinida de emergente
@@ -130,7 +127,7 @@
Carácter de reemplazo
Letras y dígitos
La mayoría de los caracteres especiales
- Historial de búsquedas
+ Historial de búsqueda
Almacenar búsquedas localmente
Historial de vistas
Almacenar historial de vídeos vistos
@@ -184,7 +181,7 @@
Comenzar a reproducir en segundo plano
Reproducir en modo emergente
Mostrar la sugerencia \"Mantener presionado para poner a la cola\"
- Nuevo y lo mejor
+ Lo nuevo y lo mejor
Mantener pulsado para añadir a la cola
Donar
NewPipe es desarrollado por voluntarios que emplean su tiempo libre para brindarle la mejor experiencia. Haz una aportación para ayudarlos a crear un NewPipe mejor mientras disfrutan de una taza de café.
@@ -277,12 +274,7 @@
\n5. Haga clic en el botón de \"Descargar\" una vez que aparezca
\n6. Haga clic en el botón IMPORTAR ARCHIVO que se muestra abajo y seleccione el archivo zip descargado
\n7. [En caso de que la importación falle] Extraiga el archivo .csv (generalmente dentro de \"Youtube y Youtube Music/suscripciones/suscripciones.csv\"), haga clic en IMPORTAR ARCHIVO y seleccione el archivo csv extraído anteriormente
- Importe un perfil de SoundCloud escribiendo la URL o su ID:
-\n
-\n1. Active el «modo escritorio» en un navegador web (el sitio no está disponible para dispositivos móviles)
-\n2. Vaya a esta URL: %1$s
-\n3. Inicie sesión cuando se le pida
-\n4. Copie la URL del perfil a la que fue redireccionado.
+ Importa un perfil de SoundCloud escribiendo la URL o tu ID: \n \n1. Habilita el «modo escritorio» en un navegador web (el sitio no está disponible para dispositivos móviles) \n2. Ve a esta URL: %1$s \n3. Inicia sesión cuando se te pida \n4. Copia la URL del perfil a la que fuiste redireccionado.
tuID, soundcloud.com/tuID
Esta operación puede causar un uso intensivo de la red.
\n
@@ -381,8 +373,7 @@
El tiempo de conexión expiro
No se puede recuperar esta descarga
Preguntar dónde descargar
- Se le preguntará dónde guardar cada descarga.
-\nHabilite Elegir carpetas del sistema (SAF) si desea guardar las descargas en una tarjeta SD externa
+ Se te preguntará dónde guardar cada descarga. \nHabilita elegir carpetas del sistema (SAF) si quieres guardar las descargas en una tarjeta SD externa
Usar Elegir carpetas del sistema (SAF)
El \'Sistema de Acceso al Almacenamiento\' permite descargar en una tarjeta SD externa
Desuscribirse
@@ -405,7 +396,7 @@
Automático
¡Actualización de NewPipe disponible!
Mostrar comentarios
- Desactivar para ocultar comentarios
+ Deshabilitar para ocultar comentarios
Reproducción automática
Sin comentarios
No se pudieron cargar los comentarios
@@ -433,7 +424,6 @@
- %s oyentes
- %s oyentes
- El idioma cambiará luego de que se reinicie la aplicación
Duración de búsqueda al avanzar y/o retroceder
Instancias de PeerTube
Selecciona tus instancias favoritas de PeerTube
@@ -494,14 +484,14 @@
Nombre de grupo vacío
¿Quieres borrar este grupo?
Nuevo
- Fuente
+ Contenido
Velocidad de actualización del contenido
Tiempo para que una suscripción se considere desactualizada — %s
Actualizar siempre
Extraer desde feed dedicado cuando esté disponible
Disponible para algunos servicios, suele ser más rápido pero puede mostrar una cantidad limitada de ítems y a menudo información incompleta (por ejemplo falta de duración, tipo de ítem o estado)
- Activar modo rápido
- Desactivar modo rápido
+ Habilitar modo rápido
+ Deshabilitar modo rápido
¿Piensas que la carga de contenidos es muy lenta\? Entonces intenta habilitar la carga rápida (puedes cambiarlo en los ajustes o pulsando el botón debajo).
\n
\nNewpipe ofrece dos formas de cargar los contenidos:
@@ -521,9 +511,7 @@
Artistas
Álbumes
Canciones
- Este vídeo tiene restricción de edad.
-\n
-\nActivar \"%1$s\" en los ajustes si quieres verlo.
+ Este vídeo tiene restricción de edad. \n \nHabilitar \"%1$s\" en los ajustes si quieres verlo.
Sí, y también vídeos vistos parcialmente
Los vídeos que ya se hayan visto luego de añadidos a la lista de reproducción, serán quitados.
\n¿Estás seguro\? ¡Esta acción no se puede deshacer!
@@ -534,7 +522,7 @@
Miniatura de avatar del canal
Los textos originales de los servicios serán visibles en los ítems de transmisiones
Mostrar tiempo atrás original en ítems
- Activar el «Modo restringido» de YouTube
+ Habilitar el «Modo restringido» de YouTube
Página de lista de reproducción
Mostrar solo suscripciones desagrupadas
Aún no hay marcadores para listas de reproducción
@@ -577,8 +565,8 @@
Hacer que Android personalice el color de la notificación de acuerdo con el color principal de la miniatura (tenga en cuenta que esto no está disponible en todos los dispositivos)
Usar miniatura como fondo de pantalla de bloqueo y notificaciones
Mostrar vista previa
- Desactivar para ocultar información adicional sobre el creador o contenido de la transmisión
- Desactivar para ocultar la descripción del vídeo y la información adicional
+ Deshabilitar para ocultar información adicional sobre el creador o contenido de la transmisión
+ Deshabilitar para ocultar la descripción del vídeo y la información adicional
Ninguna aplicación en su dispositivo puede abrir esto
Capítulos
Reciente
@@ -614,24 +602,24 @@
No listado
Público
Soporte
- Lenguaje
+ Idioma
Límite de edad
Privacidad
Licencia
Etiquetas
Categoría
- Inhabilitar la selección de texto de la descripción
+ Deshabilitar la selección de texto de la descripción
Habilitar la selección de texto de la descripción
Ahora puede seleccionar el texto dentro de la descripción. Note que la página puede parpadear y los links no serán cliqueables mientras está en el modo de selección.
%s da esta razón:
- No fue posible cargar el muro por \'%s\'.
+ No fue posible cargar el feed por \'%s\'.
Cuenta cancelada
El modo de muro rápido no arroja más información sobre esto.
La cuenta del autor ha sido cancelada.\nNewPipe no podrá acceder a ella en el futuro.\n¿Quieres desuscribirte de este canal?
Error al cargar el muro
Desde Android 10 solo el \'Sistema de Acceso al Almacenamiento\' es soportado
Se le preguntará dónde guardar cada descarga
- Desactiva la tunelización de los medios si experimentas una pantalla negra durante la reproducción o si la visualización de la imagen es intermitente.
+ Deshabilita la tunelización de medios si experimentas una pantalla negra durante la reproducción o si la visualización de la imagen es intermitente.
Deshabilitar el túnel de medios
Aún no se ha seleccionado ninguna carpeta de descargas, elija la carpeta de descargas por defecto ahora
Anfitrión
@@ -666,7 +654,7 @@
Buscar actualizaciones
Buscar nuevas versiones manualmente
Buscando actualizaciones…
- Nuevos elementos en el muro
+ Nuevos elementos en el feed
Cerrar abruptamente el reproductor
Muestra una opción de cierre abrupto al usar el reproductor
Mostrar \"Cerrar abruptamente el reproductor\"
@@ -676,8 +664,7 @@
Se produjo un error, vea la notificación
Crear una notificación de error
Mostrar una barra de error
- No se ha encontrado un gestor de archivos apropiado para esta acción.
-\nPor favor, instale un gestor de archivos o intente desactivar \'%s\' en los ajustes de la descarga
+ No se ha encontrado un gestor de archivos apropiado para esta acción. \nPor favor, instala un gestor de archivos o intenta deshabilitarlo \'%s\' en los ajustes de la descarga
No se encontró ningún administrador de archivos apropiado para esta acción.
\n Instale un administrador de archivos compatible con Storage Access Framework
Comentario fijado
@@ -695,7 +682,7 @@
Notificar de nuevos directos desde las suscripciones
Frecuencia de comprobación
¿Desea borrar del disco todos los archivos descargados\?
- Las notificaciones están desactivadas
+ Las notificaciones están deshabilitadas
Recibir notificaciones
Conmutar todo
Cargando detalles del directo…
@@ -735,7 +722,7 @@
¿Eliminar los duplicados\?
¿Quieres eliminar todas las secuencias duplicadas de esta lista de reproducción?
Mostrar las siguientes secuencias
- Mostrar/Ocultar secuencias
+ Mostrar/ocultar secuencias
Próximamente
Eliminar los duplicados
Completamente visto
@@ -765,7 +752,7 @@
Cambia el tamaño del intervalo de carga en contenidos progresivos (actualmente %s). Un valor más bajo puede acelerar la carga inicial
Ajustes de ExoPlayer
Gestiona algunos ajustes de ExoPlayer. Estos cambios requieren reiniciar el reproductor para que surtan efecto
- Habilite esta opción si tiene problemas con la inicialización del decodificador recurriendo a decodificadores de menor prioridad si el decodificador principal no se inicializa. Esto puede dar como resultado un rendimiento de reproducción más bajo que cuando se usan decodificadores primarios
+ Habilita esta opción si tiene problemas con la inicialización del decodificador recurriendo a decodificadores de menor prioridad si el decodificador principal no se inicializa. Esto puede dar como resultado un rendimiento de reproducción más bajo que cuando se usan decodificadores primarios
Esta solución alternativa libera los códecs de video y los vuelve a instanciar cuando cambia la máscara, en lugar de configurar la máscara directamente en el códec. ExoPlayer ya usa esta configuración en algunos dispositivos con este problema y solo afecta a Android 6 y versiones posteriores
\n
\nHabilitar esta opción puede evitar errores de reproducción al cambiar el reproductor de video actual o cambiar al modo de pantalla completa
@@ -776,7 +763,7 @@
Sin transmisiones en directo
Vídeos
Suscriptores
- Qué pestañas se muestran en las páginas de los canales
+ Qué pestañas se muestran en las páginas del canal
Pestañas del canal
Shorts
Cargando metadatos…
@@ -829,13 +816,16 @@
Respaldar y restaurar
Restablecer ajustes
Restablecer todos los ajustes a sus valores predeterminados
- Restablecer todos los ajustes descartará todos sus ajustes preferidos y reiniciará la aplicación.
-\n
-\n¿Estas seguro que deseas continuar?
+ Restablecer todos los ajustes descartará todos sus ajustes preferidos y reiniciará la aplicación. \n \n¿Estás seguro que quieres continuar?
Sí
No
NewPipe puede buscar automáticamente nuevas versiones de vez en cuando y notificarle cuando estén disponibles.
\n¿Quieres habilitar esto?
La configuración de la exportación que se importa utiliza un formato vulnerable que quedó obsoleto desde NewPipe 0.27.0. Asegúrese de que la exportación que se está importando provenga de una fuente confiable y prefiera usar solo exportaciones obtenidas de NewPipe 0.27.0 o posterior en el futuro. La compatibilidad con la importación de configuraciones en este formato vulnerable pronto se eliminará por completo y, luego, las versiones antiguas de NewPipe ya no podrán importar configuraciones de exportaciones desde las nuevas versiones.
secundaria
+ Compartir como lista de reproducción temporal de YouTube
+ Lista de reproducción
+ Selecciona un grupo de feed
+ Aún no se ha creado ningún grupo de feed
+ Página de grupo de canales
diff --git a/app/src/main/res/values-et/strings.xml b/app/src/main/res/values-et/strings.xml
index a89d2846b..3bc372c2b 100644
--- a/app/src/main/res/values-et/strings.xml
+++ b/app/src/main/res/values-et/strings.xml
@@ -146,7 +146,7 @@
Üksikasjad:
Esita video, kestus:
Üleslaadiaja avatari pisipilt
- Meeldib
+ Meeldimisi
Ei meeldi
Tulemusi pole
Siin pole veel midagi
@@ -154,9 +154,6 @@
Video
Audio
Proovi uuesti
- tuh
- mln
- mld
Tellijaid pole
- %s tellija
@@ -206,8 +203,8 @@
Anneta
Veebisait
Täiendava info ja uudiste lugemiseks külasta NewPipe\'i veebisaiti.
- NewPipe\'i privaatsuspoliitika
- Loe privaatsuspoliitikat
+ NewPipe\'i andmekaitsepõhimõtted
+ Loe andmekaitsepõhimõtteid
NewPipe\'i litsents
Loe litsentsi
Ajalugu
@@ -217,7 +214,7 @@
Enim esitatud
Avalehe sisu
Tühi leht
- Kioski leht
+ Kioskivaade
Kanali leht
Vali kanal
Kanaleid pole veel tellitud
@@ -305,7 +302,7 @@
Kui sul on ideid kujunduse muutmisest, koodi puhastamisest või suurtest koodi muudatustest - abi on alati teretulnud. Mida rohkem tehtud, seda paremaks läheb!
NewPipe\'i arendajad on vabatahtlikud, kes kulutavad oma vaba aega, toomaks sulle parimat kasutuskogemust. On aeg anda tagasi aidates arendajaid ja muuta NewPipe veel paremaks, nautides ise tassi kohvi.
Anneta
- NewPipe võtab privaatsust väga tõsiselt. Seetõttu ei kogu rakendus ilma nõusolekuta mingeid andmeid. \nNewPipe\'i privaatsuspoliitika selgitab üksikasjalikult, milliseid andmeid saadetakse ja kogutakse veateate saatmisel.
+ NewPipe võtab privaatsust väga tõsiselt. Seetõttu ei kogu rakendus ilma nõusolekuta mingeid andmeid. \nNewPipe\'i andmekaitsepõhimõtted selgitavad üksikasjalikult, milliseid andmeid saadetakse ja kogutakse veateate saatmisel.
NewPipe on vaba ja avatud lähtekoodiga tarkvara. Seada võid kasutada, uurida, jagada ja parandada nii, nagu õigemaks pead. Täpsemalt - seda võid levitada ja/või muuta vastavalt Vaba Tarkvara Sihtasutuse avaldatud GNU Üldise Avaliku Litsentsi v.3 (või sinu valikul hilisema versiooni) tingimustele.
Teavita elutsüklist väljas vigadest
Impordi SoundCloudi profiil trükkides URL või oma ID:
@@ -320,7 +317,7 @@
Keri helitu koht edasi
Samm
Lähtesta
- Selleks, et täita Euroopa Üldist Andmekaitse Määrust (GDPR), juhime tähelepanu NewPipe\'i privaatsuspoliitikale. Palun loe seda hoolikalt. \nMeile veateate saatmiseks pead sellega nõustuma.
+ Selleks, et täita Euroopa Üldist Andmekaitse Määrust (GDPR), juhime tähelepanu NewPipe\'i andmekaitsepõhimõtetele. Palun loe seda hoolikalt. \nMeile veateate saatmiseks pead sellega nõustuma.
Minimeeri, kui kasutad teisi rakendusi
Tegevus lülitusel peamiselt videopleierilt teisele rakendusele — %s
Pole
@@ -487,7 +484,6 @@
Lisa esitusjärjekorda
Hiljuti lisatud
Kohalikud
- Keele muutus jõustub rakenduse uuesti käivitamisel
Kommentaaride laadimine ei õnnestunud
Esitusloendi järjehoidjaid veel pole
Vali esitusloend
@@ -808,4 +804,14 @@
Ei
Imporditavad andmed kasutavad turvaprobleemidega vormingut, mida alates versioonist 0.27.0 NewPipe enam kasutada ei suuda. Palun kontrolli, et impordifail on loodud usaldusväärse osapoole poolt ning eelista ekspordifaile, mis on loodud NewPipe\'i versiooniga 0.27.0 või uuemaga. Tugi sellise vana vormingu kasutamisele kaob õige pea ja seejärel NewPipe\'i uuemad ja vanemad versioonid ei saa omavahel andmeid enam vahetada.
täiendav
+ Jaga YouTube\'i ajutise esitusloendina
+ Esitusloendid
+ Vali andmevoo grupp
+ Ühtegi andmevoo gruppi pole veel loodud
+ Kanalirühmade leht
+ Otsi: %1$s
+ Otsi: %1$s (%2$s)
+ Meeldimisi
+ SoundCloudi „Top 50“ leht on eemaldatud
+ SoundCloud on lõpetanud oma algse „Top 50“ edetabeli pidamise. Seega on ka vastav vahekaart meie rakenduse põhivaatest eemaldatud.
diff --git a/app/src/main/res/values-eu/strings.xml b/app/src/main/res/values-eu/strings.xml
index 415d2a8d3..24b24599e 100644
--- a/app/src/main/res/values-eu/strings.xml
+++ b/app/src/main/res/values-eu/strings.xml
@@ -84,9 +84,6 @@
Bideoa
Audioa
Saiatu berriro
- k
- M
- MM
Hasi
Pausatu
Ezabatu
@@ -422,7 +419,6 @@
Erreprodukziorako kokapen guztiak ezabatzen ditu
Ezabatu erreprodukziorako kokapen guztiak\?
Aktibatu zerbitzua, orain hautatua:
- Hizkuntza aldatuko da aplikazioa berrabiarazterakoan
Kiosko Lehenetsia
Aurreratze/atzeratze bilaketaren iraupena
PeerTube instantziak
diff --git a/app/src/main/res/values-fa/strings.xml b/app/src/main/res/values-fa/strings.xml
index 6a252e22f..2b5165ded 100644
--- a/app/src/main/res/values-fa/strings.xml
+++ b/app/src/main/res/values-fa/strings.xml
@@ -148,9 +148,6 @@
خطایی رخ داد: %1$s
جریانی برای بارگیری در دسترس نیست
بدون نتیجه
- K
- M
- B
- %s مشترک
- %s مشترک
@@ -469,7 +466,6 @@
مورد پسندترینها
اخیرا اضافه شده
محلی
- با آغاز دوبارهٔ کاره، زبان تغییر خواهد کرد
کیوسک پیشفرض
انجام شد
وقتی انجام شد، «Done» یا «انجام شد» را بفشارید
@@ -764,4 +760,59 @@
؟
پشتیبانگیری و بازیابی
بدون جریان زنده
+ کیفیت تصویر
+ کیفیت متوسّط
+ کیفیت زیاد
+ ترجیح صدای اصلی
+ همرسانی سیاههٔ پخش
+ کیفیت کم
+ سیاهههای پخش
+ بازنشانی تنظیمات
+ ثانویه
+ نمایش کمتر
+ جابهجایی گزینشگر زبانهٔ اصلی به پایین
+ تونل زدن رسانه به صورت پیشگزیده روی افزارهتان از کار افتاده چرا که از آن پشتیبانی نمیکند.
+ کنش ژست راست
+ قطعهای صوتی باید از پیش در این جریان موجود باشد
+ برای نمونه اگر از گوشیای با دکمههای خراب استفاده میکنید مفید است
+ بازنشانی همهٔ تنظیمات همهٔ تنظیمات ترجیحیتان را دور اندعخته و کاره را دوباره آغاز میکند.\n\nمطمئنید که میخواهید ادامه دهید؟
+ استفاده از ویژگی پشتیبان کدگشای اگزوپلیر
+ زبانههای نشان داده شده روی صفحههای کانال
+ تغییر جهت صفحه
+ بار نکردن تصویرها
+ همرسانی سیاههٔ نشانی
+ همرسانی به شکل سیاههٔ پخش موقّتی یوتوب
+ - %1$s: %2$s
+ %1$s\n%2$s
+ استفادهٔ همیشگی از دور زدن تنظیمات سطح خروجی ویدیوی اگزوپلیر
+ موقعیت زبانههای اصلی
+ واکشی زبانههای کانال
+ گزینش قطعهٔ صوتی برای پخش کنندههای خارجی
+
+ - %s پاسخ
+ - %s پاسخ
+
+ همرسانی با عنوانها
+ گزینش قطعهٔ صوتی با شرح برای افزار کمبینا در صورت وجود
+ ترجیح صدای شرح دهنده
+ گزینش کنش ژست نیمهٔ راست صفحه
+ تعداد %d بار تکرار شده
+ چهرکهای بارکننده
+ چهرکهای زیرکانال
+ کنش ژست چپ
+ گزینش کیفیت تصویرها و این که اصلاً بار شوند یا نه، برای کاهش استفادهٔ حافظه و داده. تغییرات انبارهٔ تصویر حافظه و دیسک را پاک میکند — %s
+ اگر مشکل شروع رمزگشایی دارید ، این گزینه را فعال کنید ، که اگر رمزگشایی اولیه شکست بخورد ، به رمزگشایی های با اولویت پایین تر باز می گردد. این ممکن است منجر به عملکرد پخش ضعیف نسبت به هنگام استفاده از رمزگشایان اولیه شود
+ این روش دور زدن مشکل به جای تنظیم مستقیم سطح روی رمزینه، آنها را هنگام تغییر سطح آزاد کرده و دوباره راهاندازی میکند. این تنظیم که از پیش روی برخی افزارهها به دست اگزوپلیر استفاده میشد فقط روی اندروید ۶ و بالاتر تأثیر دارد\n\nبه کار انداختن این گزینه میتواند از خطاهای پخش هنگام تغییر پخشکنندهٔ ویدیوی کنونی یا تغییر به حالت تمامصفحه جلوگیری کند
+ مدیریت برخی تنظیمات اگزوپلیر. اعمال این تغییرات نیازمند آغاز دوبارهٔ پخشکننده است
+ سیاهههای پخشی که خاکستری شدهاند این مورد را از پیش دارند.
+ ویرایش هر کنش آگاهی زیر با زدن رویش. سه کنش نخست (پخش/مکث، پیشین و بعدی) به دست سامانه تنظیم شده و قابل سفارشی سازی نیستند.
+ گزینش کنش ژست نیمهٔ چپ صفحه
+ گزینش قطعهٔ صوتی اصلی فارغ از زبان
+ نیوپایپ میتواند گهگاه به صورت خودکار نگارشهای جدید را بررسی کرده و از وجودشان آگاهتان کند.\nمیخواهید به کارش بیندازید؟
+ بازنشانی همهٔ تنظیمات به مقدارهای پیشگزیدهشان
+ نمایش بیشتر
+ تنظیمات داخل برونریزی از قالبی آسیبپذیر استفاده میکند که از نگارش ۰٫۲۷٫۰ منسوخ شده. مطمئن شوید برونریزی از منبعی مطمئن آمده و ترجیحاً فقط از برونریزیهای آمده از نگارش ۰٫۲۷٫۰ به بعد استفاده کنید. پشتیبانی از درونریزی تنظیمات به این قالب آسیبپذیر به زودی کاملاًبرداشته خواهد شد و دیگر نگارشخای قدیمیتر قادر به درون ریزی تنظیمات از نگارشهای جدید نخواهند بود.
+ گشودن صف پخش
+ میخواهید همهٔ جریانهای تکراری را در این سیاههٔ پخش بردارید؟
+ زبانههایی که هنگام بهروز رسانی خوراک واکشی میشوند. این گزینه تأثیری روی کانالهایی که با ساتفاده از حالت سریع بهروز میشوند ندارد.
diff --git a/app/src/main/res/values-fi/strings.xml b/app/src/main/res/values-fi/strings.xml
index 300493b52..5050cb886 100644
--- a/app/src/main/res/values-fi/strings.xml
+++ b/app/src/main/res/values-fi/strings.xml
@@ -102,9 +102,6 @@
Video
Ääni
Toista uudelleen
- t.
- milj.
- bilj.
Ei tilaajia
- %s tilaaja
@@ -337,7 +334,6 @@
Hiljattain lisätyt
Paikalliset
Pidetyimmät
- Kieli vaihtuu, kun sovellus uudelleenkäynnistetään
Kommentteja ei voitu ladata
Mitkä välilehdet näytetään pääsivulla
Valmis
@@ -807,4 +803,9 @@
NewPipe voi automaattisesti tarkistaa päivitysten saatavuuden silloin tällöin ja ilmoittaa kun niitä on saatavilla.
\nHaluatko ottaa tämän käyttöön?
Laitteella ei ole riittävästi vapaata tilaa
+ Jaa tilapäisenä YouTube-soittolistana
+ Raidat
+ \?
+ toissijainen
+ Soittolistat
diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml
index 48710619b..fe71d35c0 100644
--- a/app/src/main/res/values-fr/strings.xml
+++ b/app/src/main/res/values-fr/strings.xml
@@ -5,7 +5,7 @@
Vouliez-vous dire « %1$s » \?
Télécharger
Dossier de téléchargement vidéo
- Choisissez le dossier de téléchargement des vidéos
+ Choisissez le dossier de téléchargement pour les fichiers vidéos
Les vidéos téléchargées sont stockées ici
Installer
Installer l’application Kore manquante \?
@@ -87,8 +87,6 @@
Lecture en mode flottant
Désactivés
Quoi :\\nRequest :\\nContent Language :\\nContent Country :\\nApp Language :\\nService :\\nGMT Time :\\nPackage :\\nVersion :\\nOS version :
- k
- M
Cette autorisation est nécessaire pour
\nutiliser le mode flottant
Arrière-plan
@@ -100,7 +98,6 @@
Mémoriser les propriétés de la fenêtre flottante
Mémoriser les dernières taille et position de la fenêtre flottante
Effacer
- G
Le son peut être absent à certaines définitions
Suggestions de recherche
Sélectionner les suggestions à afficher lors d’une recherche
@@ -164,7 +161,7 @@
Caractères spéciaux
Voulez-vous supprimer cet élément de l’historique de recherche \?
Contenu de la page principale
- Page vide
+ Page blanche
Chaîne
Sélectionner une chaîne
Tendances
@@ -428,7 +425,6 @@
- %s auditeurs
- %s auditeurs
- La langue changera une fois que l’application aura redémarré
Durée de l’avance et retour rapide
Instances PeerTube
Veuillez choisir vos instances PeerTube préférées
@@ -839,4 +835,14 @@
Pas assez d\'espace disponible sur l\'appareil
Les paramètres de l\'export en cours d\'importation utilisent un format vulnérable qui a été déprécié depuis NewPipe 0.27.0. Assurez-vous que l\'export en cours d\'importation provient d\'une source fiable. Privilégiez les exports obtenues à partir de NewPipe 0.27.0 ou des versions plus récentes à l\'avenir. Le support pour l\'importation des paramètres dans ce format vulnérable sera bientôt complètement supprimé et les anciennes versions de NewPipe ne pourront plus importer les paramètres des exports des nouvelles versions.
secondaire
+ Partager comme liste de lecture YouTube temporaire
+ Listes de lecture
+ Sélectionnez un groupe de flux
+ Aucun groupe de flux n\'a encore été créé
+ Page du groupe de chaînes
+ Rechercher %1$s
+ Rechercher %1$s (%2$s)
+ Likes
+ Page SoundCloud Top 50 supprimée
+ SoundCloud a abandonné le classement original du Top 50. L\'onglet correspondant a été supprimé de votre page d\'accueil.
diff --git a/app/src/main/res/values-gl/strings.xml b/app/src/main/res/values-gl/strings.xml
index 586e351e2..3371bb211 100644
--- a/app/src/main/res/values-gl/strings.xml
+++ b/app/src/main/res/values-gl/strings.xml
@@ -158,9 +158,6 @@
Vídeo
Audio
Tentar de novo
- k
- M
- B
Ningún subscrito
- %s subscrito
@@ -463,7 +460,6 @@
Conferencias
O que ten mais gústames
Engadiuse recentemente
- O idioma cambiará unha vez que se reinicie o aplicativo
Non se puideron cargar os comentarios
Aínda non hai marcadores nesta lista de reprodución
Seleccionar unha lista de reprodución
diff --git a/app/src/main/res/values-he/strings.xml b/app/src/main/res/values-he/strings.xml
index b4c16a473..a8bc152f3 100644
--- a/app/src/main/res/values-he/strings.xml
+++ b/app/src/main/res/values-he/strings.xml
@@ -105,16 +105,13 @@
פרטים:
נגינת סרטון, משך:
תמונה ייצוגית של המפרסם
- אהבו
+ לייקים
לא אהבו
אין תוצאות
אין כאן כלום מלבד צרצרים
סרטון
שמע
ניסיון חוזר
- אלפ.
- מיל.
- מיליארד
אין מנויים
- מנוי אחד
@@ -179,9 +176,9 @@
היסטוריה
למחוק את הפריט הזה מהיסטוריית החיפושים\?
תוכן הדף הראשי
- דף ריק
- דף גישה מזדמנת
- דף ערוצים
+ עמוד ריק
+ עמוד גישה מזדמנת
+ עמוד הערוץ
נא לבחור ערוץ
אין עדיין מינויים לערוצים
נא לבחור סוג גישה מזדמנת
@@ -196,7 +193,7 @@
להתחיל לנגן בנגן צף
הורדת קובץ הזרמה
הצגת מידע
- רשימות נגינה מסומנות
+ רשימות השמעה מסומנות
הוספה אל
מדינת תוכן כברירת מחדל
ניפוי שגיאות
@@ -432,7 +429,6 @@
- %s מאזינים
- %s מאזינים
- השפה תוחלף עם הפעלת היישומון מחדש
קיוסק ברירת מחדל
משך קפיצה מהירה קדימה/אחורה
מופעים של PeerTube
@@ -850,4 +846,12 @@
אין מספיק מקום פנוי במכשיר
ההגדרות בייצוא המיובא משתמשות בתסדיר פגיע שהוצא משימוש מאז NewPipe 0.27.0. יש לוודא שהייצוא המיובא הוא ממקור מהימן, ועדיף להשתמש רק בייצוא שהושג מ־NewPipe 0.27.0 ומעלה בעתיד. תמיכה בייבוא הגדרות בתסדיר פגיע זה תוסר בקרוב לחלוטין, ואז גרסאות ישנות של NewPipe לא יוכלו לייבא עוד הגדרות של ייצוא מגרסאות חדשות.
משני
+ רשימות נגינה
+ שיתוף כרשימת נגינה זמנית של YouTube
+ הגדרת קבוצת ערוצי עדכונים
+ לא נוצרו עדיין קבוצות ערוצי עדכונים
+ עמוד קבוצת ערוצים
+ חיפוש ב־%1$s
+ חיפוש ב־%1$s (%2$s)
+ לייקים
diff --git a/app/src/main/res/values-hi/strings.xml b/app/src/main/res/values-hi/strings.xml
index 890ef1347..73d070398 100644
--- a/app/src/main/res/values-hi/strings.xml
+++ b/app/src/main/res/values-hi/strings.xml
@@ -126,9 +126,6 @@
वीडियो
ऑडियो
फिर से कोशिश करें
- हज़ार
- मिलियन
- अरब
कोई सब्सक्राइबर नहीं
- %s सब्सक्राइबर
@@ -423,7 +420,6 @@
- %s श्रोता
- %s श्रोता
- ऐप के पुनः आरंभ होने के बाद भाषा बदल जाएगी
तेज मोड सक्षम करें
तेज मोड अक्षम करें
क्या आपको लगता है कि फीड लोडिंग बहुत धीमी है\? यदि ऐसा है, तो तेज़ लोडिंग को सक्षम करने का प्रयास करें (आप इसे सेटिंग्स में या नीचे दिए गए बटन को दबाकर बदल सकते हैं)।
@@ -823,4 +819,12 @@
बैकअप और रिस्टोर
आयात किए जा रहे निर्यात में सेटिंग्स एक कमजोर प्रारूप का उपयोग करती हैं जिसे न्यूपाइप 0.27.0 के बाद से हटा दिया गया था। सुनिश्चित करें कि आयात किया जा रहा निर्यात किसी विश्वसनीय स्रोत से है, और भविष्य में केवल न्यूपाइप 0.27.0 या नए से प्राप्त निर्यात का उपयोग करना पसंद करें। इस असुरक्षित प्रारूप में सेटिंग्स आयात करने के लिए समर्थन जल्द ही पूरी तरह से हटा दिया जाएगा, और फिर न्यूपाइप के पुराने संस्करण अब नए संस्करणों से निर्यात की सेटिंग्स आयात नहीं कर पाएंगे।
सेकेंडरी
+ %1$s खोजें
+ %1$s (%2$s) खोजें
+ प्लेलिस्ट
+ कृपया एक फ़ीड समूह चुनें
+ अभी तक कोई फ़ीड समूह नहीं बनाया गया है
+ चैनल समूह पेज
+ पसंद
+ यूट्यूब अस्थायी प्लेलिस्ट के रूप में साझा करें
diff --git a/app/src/main/res/values-hr/strings.xml b/app/src/main/res/values-hr/strings.xml
index 960106e0d..4361e2fa9 100644
--- a/app/src/main/res/values-hr/strings.xml
+++ b/app/src/main/res/values-hr/strings.xml
@@ -97,9 +97,6 @@
Video
Audio
Pokušaj ponovo
- tis.
- mil
- mlrd.
Počni
Pauziraj
Izbriši
@@ -406,7 +403,6 @@
Izbrisati sve pozicije reprodukcije\?
Nitko ne gleda
Nitko ne sluša
- Jezik će se promijeniti nakon ponovnog pokretanja aplikcije
Standardni kiosk
Podržani su samo HTTP URL-ovi
Lokalni
diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml
index fb04aa724..0b84cce90 100644
--- a/app/src/main/res/values-hu/strings.xml
+++ b/app/src/main/res/values-hu/strings.xml
@@ -49,7 +49,7 @@
Jelentés
Információ:
Ez történt:
- Az Ön megjegyzése (angolul):
+ Saját hozzászólás (angolul):
Részletek:
Elnézést, valami balul sült el.
Elnézést, ennek nem kellett volna megtörténnie.
@@ -127,18 +127,18 @@
Az alkalmazás/kezelőfelület összeomlott
Nem sikerült a videó lejátszása
A külső lejátszó nem támogatja az ilyen típusú hivatkozásokat
- Nem található videó adatfolyam
- Nem található hang adatfolyam
+ Nem található videófolyam
+ Nem található hangfolyam
Mi:\\nKérés:\\nTartalom nyelve:\\nTartalom származási országa:\\nAlkalmazás nyelve:\\nSzolgáltatás:\\nGMT idő:\\nCsomag:\\nVerzió:\\nOperációs rendszer verzió:
Nincs találat
Közvetítési fájl letöltése
Hozzáadás ehhez
Gyorsabb, de pontatlan tekerés használata
A pontatlan tekerés lehetővé teszi, hogy gyorsabban ugorjon a pozíciókra, de kisebb pontossággal. Az 5, 15, vagy 25 másodperces tekerés nem működik ebben a módban
- A bélyegkép gyorsítótára törölve
+ Bélyegkép gyorsítótára törölve
Gyorsítótárazott metaadatok törlése
- Minden gyorsítótárazott weboldaladat törlése
- A metaadatok gyorsítótára törölve lett
+ Minden gyorsítótárazott weboldaladat törölve
+ Metaadatok gyorsítótára törölve
Következő videó automatikus sorba állítása
Keresési előzmények helyi tárolása
Csatornák
@@ -166,9 +166,6 @@
Nincs letölthető adatfolyam
Nincs itt semmi pár tücskön kívül
Húzza az átrendezéshez
- e
- m
- M
Nincs feliratkozó
- %s feliratkozó
@@ -202,13 +199,13 @@
Licencek
Szabad, egyszerű közvetítésnézés Androidon.
Közreműködés
- Legyen ötleted a fordítással, a dizájnnal, a forráskód tisztításával vagy egy komolyabb átszervezésével kapcsolatban, bármilyen segítséget szívesen fogadunk. Minél több minden készül el, annál jobb lesz!
+ Akár fordítással, tervezési változtatásokkal, kódtisztítással, vagy valódi nehéz kódváltoztatással kapcsolatos ötletei vannak, bármilyen segítséget szívesen fogadunk. Minél több minden készül el, annál jobb lesz!
Megtekintés a GitHubon
Adományozás
A NewPipe alkalmazást önkéntesek fejlesztik a szabadidejükben, hogy a lehető legjobb felhasználói élményt nyújtsák. Járuljon hozzá, hogy a fejlesztők még jobbá tegyék alkalmazást, miközben egy csésze kávét szürcsölnek.
Hozzájárulás
- Honlap
- Látogassa meg a NewPipe honlapját további információkért és hírekért.
+ Weboldal
+ Látogasson el a NewPipe weboldalára további információkért és hírekért.
A NewPipe adatvédelmi irányelvei
A NewPipe projekt komolyan veszi az adatvédelmét. Az alkalmazás nem gyűjt semmilyen adatot a beleegyezése nélkül.
\nA NewPipe adatvédelmi irányelve részletesen elmagyarázza, mely adatok kerülnek elküldésre és tárolásra az alkalmazás összeomlásának jelentésekor.
@@ -270,23 +267,10 @@
Exportálás…
Fájl importálása
Előző exportálás
- A feliratkozások importálása nem sikerült
- A feliratkozások exportálása nem sikerült
- YouTube feliratkozások importálása a Google Takeoutból:
-\n
-\n1. Navigáljon erre az oldalra: %1$s
-\n2. Jelentkezzen be, ha kérik
-\n3. Kattintson „Az összes adatot tartalmazza” gombra, majd a „Kijelölések megszüntetése” gombra, majd válassza ki a „feliratkozások” lehetőséget és kattintson az „OK” gombra
-\n4. Kattintson a „Következő lépés”, majd az \"Exportálás indítása” gombra
-\n5. Kattintson a „Letöltés” gombra, amikor megjelenik,
-\n6. Kattintson a lenti FÁJL IMPORTÁLÁSA gombra, és válassza ki a letöltött ZIP-fájlt
-\n7. [Ha a ZIP-fájl importálása nem sikerül] Bontsa ki a .csv fájlt (általában: „YouTube és YouTube Music/feliratkozások/feliratkozások.csv\"), majd kattintson lent a FÁJL IMPORTÁLÁSA gombra, és válassza az exportált CSV-fájlt
- SoundCloud-profil importálása a webcím vagy az azonosítójának begépelésével:
-\n
-\n1. A webböngészőben engedélyezze az „asztali módot” (az oldal nem érhető el mobileszközökön)
-\n2. Navigáljon erre a webcímre: %1$s
-\n3. Jelentkezzen be, ha kéri
-\n4. Másolja ki a profil webcímét, ahova át lett irányítva.
+ A feliratkozások importálása sikertelen
+ A feliratkozások exportálása sikertelen
+ YouTube feliratkozások importálása a Google Takeoutból: \n \n1. Navigáljon erre az oldalra: %1$s \n2. Jelentkezzen be, ha kérik \n3. Kattintson „Az összes adatot tartalmazza” gombra, majd a „Kijelölések megszüntetése” gombra, majd válassza ki a „feliratkozások” lehetőséget és kattintson az „OK” gombra \n4. Kattintson a „Következő lépés”, majd az \"Exportálás indítása” gombra \n5. Kattintson a „Letöltés” gombra, amikor megjelenik, \n6. Kattintson a lenti FÁJL IMPORTÁLÁSA gombra, és válassza ki a letöltött ZIP-fájlt \n7. [Ha a ZIP-fájl importálása nem sikerül] Bontsa ki a .csv fájlt (általában: „YouTube és YouTube Music/feliratkozások/feliratkozások.csv”), majd kattintson lent a FÁJL IMPORTÁLÁSA gombra, és válassza az exportált CSV-fájlt
+ SoundCloud-profil importálása a webcím vagy az azonosítójának begépelésével: \n \n1. A webböngészőben engedélyezze az „asztali módot” (az oldal nem érhető el mobileszközökön) \n2. Navigáljon a következő webcímre: %1$s \n3. Jelentkezzen be, ha kéri \n4. Másolja ki a profil webcímét, ahova át lett irányítva.
saját azonosítója, soundcloud.com/azonosító
Ez a művelet adatforgalom-igényes lehet.
\n
@@ -305,11 +289,11 @@
Lejátszás folytatása a háttérben
Lejátszás folytatása felugró ablakban
Lejátszás folytatása
- A „Tartsa lenyomva a sorba állításhoz\" tipp megjelenítése
+ A „Tartsa lenyomva a sorba állításhoz” tipp megjelenítése
Leiratkozás
Válasszon lapot
- Megjegyzések megjelenítése
- Kapcsolja ki a megjegyzések elrejtéséhez
+ Hozzászólások megjelenítése
+ Kapcsolja ki a hozzászólások elrejtéséhez
Tartalom alapértelmezett országa
Folytatás főnézetben
Eltüntetés
@@ -350,7 +334,7 @@
Az előrehaladás elveszett, mert a fájlt törölték
Nincs hely az eszközön
A NewPipe leállt a fájl feldolgozása közben
- Utófeldolgozás sikertelen
+ Az utófeldolgozás sikertelen
Nincs talalat
A kiszolgáló nem fogad többszálú letöltést, próbálkozzon újra ezzel: @string/msg_threads = 1
A kiszolgáló nem küld adatokat
@@ -366,7 +350,7 @@
Ilyen névű fájl már létezik
Felülírás
Egyedi név előállítása
- Letöltés sikertelen
+ A letöltés sikertelen
helyrehozás
utófeldolgozás
sorba állítva
@@ -398,10 +382,9 @@
Automatikus sorba állítás
Kapcsolja ki, hogy elrejtse a videó leírását és a további információkat
Visszaállítja az alapértelmezéseket\?
- Ez a videó korhatáros.
-\nAz új, korhatáros videókkal kapcsolatos YouTube irányelvek miatt a NewPipe nem férhet hozzá a videóhoz, így nem tudja lejátszani.
+ Ez a videó korhatáros. \nAz új, korhatáros videókkal kapcsolatos YouTube irányelvek miatt a NewPipe nem férhet hozzá a videófolyamokhoz, így nem tudja lejátszani.
Leírás
- Megjegyzések
+ Hozzászólások
Formázott jelentés másolása
Adjon engedélyt a más alkalmazások feletti megjelenéshez
Még nincs könyvjelző lejátszási listához
@@ -421,8 +404,7 @@
Az ExoPlayer korlátai miatt az előre- és visszatekerés időtartama %d másodpercre lett állítva
Csatornacsoportok
Rendszer alapértelmezése
- Az Általános adatvédelmi rendeletnek (GDPR) való megfelelés érdekében felhívjuk figyelmét a NewPipe adatvédelmi nyilatkozatára. Olvassa el figyelmesen.
-\nEl kell fogadnia, ha hibajelentést szeretne küldeni.
+ Az Általános adatvédelmi rendeletnek (GDPR) való megfelelés érdekében felhívjuk figyelmét a NewPipe adatvédelmi irányelveire. Olvassa el figyelmesen. \nEl kell fogadnia, ha hibajelentést szeretne küldeni.
Alkalmazás összeomlasztása
Memóriaszivárgások megjelenítése
A memóriaszivárgás-monitorozás az alkalmazás megállását okozhatja, amíg a dinamikus memória mentése folyik
@@ -443,8 +425,7 @@
Senki sem nézi
A feliratkozók száma nem érhető el
Helyi
- A nyelv az alkalmazás újraindításakor fog megváltozni
- Megjegyzések betöltése sikertelen
+ A megjegyzések betöltése sikertelen
Válasszon egy lejátszási listát
Lejátszás automatikus indítása — %s
Lejátszás sebességének beállítása
@@ -458,7 +439,7 @@
© %1$s %2$s, %3$s licenc alatt
Harmadik féltől származó licencek
Kész
- Nincs megjegyzés
+ Nincsenek hozzászólások
∞ videó
100+ videó
Jelentés a GitHubon
@@ -468,7 +449,7 @@
reCAPTCHA sütik törlése
Előadók
Albumok
- Számok
+ Dalok
Események
Videók
Ez a videó korhatáros.
@@ -516,7 +497,7 @@
Ezt a tartalmat még nem támogatja a NewPipe.
\n
\nRemélhetőleg egy következő verzióban már támogatott lesz.
- Nincs a készülékén olyan alkalmazás, amely meg tudja ezt nyitni
+ Az eszközön nincs olyan alkalmazás, amely meg tudja ezt nyitni
Ez a videó csak YouTube Music Prémium előfizetők számára érhető el, így nem tekinthető meg és nem tölthető le a NewPipe-pal.
Automatikus (rendszertéma)
Ez a tartalom csak előfizetőknek érhető el, nem tekinthető meg és nem tölthető le a NewPipe-pal.
@@ -536,7 +517,7 @@
A YouTube biztosít egy „Korlátozott módot”, amely elrejti a lehetséges felnőtteknek szóló tartalmat
A YouTube „Korlátozott mód” bekapcsolása
A példány már létezik
- A példány érvényesítése nem sikerült
+ A példány érvényesítése sikertelen
Adja meg a példány webcímét
Példány hozzáadása
Találjon Önnek tetsző példányokat itt: %s
@@ -562,7 +543,7 @@
A fő lejátszó teljes képernyős indítása
A videókat ne a kis lejátszóban indítsa el, hanem kapcsolja be a teljes képernyős módot, ha az automatikus forgatás zárolva van. Továbbra is elérheti a kis lejátszót, ha kilép a teljes képernyőből
Szolgáltatás be/ki, jelenleg kiválasztott:
- A megjegyzések ki vannak kapcsolva
+ A hozzászólások ki vannak kapcsolva
Húzza oldalra az elemeket az eltávolításukhoz
A következő sorba állítása
A következő sorba állítva
@@ -572,29 +553,17 @@
Tiltsa le a médiacsatornázást, ha fekete képernyőt vagy akadozást tapasztal videólejátszáskor.
Minden letöltésnél meg fogja kérdezni, hogy hova mentse el
Válasszon egy példányt
- Lista legutóbbi frissítése: %s
- Lista betöltése…
+ Hírfolyam utoljára frissítve: %s
+ Hírfolyam betöltése…
Csak a nem csoportosított feliratkozások megjelenítése
- Lista
- Lista frissítési küszöb
+ Hírfolyam
+ Hírfolyam frissítési küszöb
A legutóbbi frissítés óta eltelt idő, ami után a feliratkozás elavultnak számít – %s
- A szerző fiókját eltávolították.
-\nA NewPipe nem fogja tudni betölteni ezt a listát a jövőben.
-\nLeiratkozik erről a csatornáról\?
- A gyors listamód nem ad ennél több információt.
- Lekérés egy dedikált listából, ha lehetséges
+ A szerző fiókját eltávolították. \nA NewPipe nem fogja tudni betölteni ezt a hírfolyamot a jövőben. \nLeiratkozik erről a csatornáról?
+ A gyors hírfolyammód nem szolgáltat több információt.
+ Lekérés egy dedikált hírfolyamból, ha lehetséges
Gyors mód engedélyezése
- Úgy gondolja, hogy a lista betöltése lassú\? Ha így van, akkor próbálja engedélyezni a gyors betöltést (ezt a beállításokban változtathatja meg, vagy a lenti gomb megnyomásával).
-\n
-\nA NewPipe két listabetöltési stratégiát kínál:
-\n• A teljes feliratkozott csatorna lekérése, amely lassú, de teljes.
-\n• Egy dedikált szolgáltatási végpont, amely gyors, de általában nem teljes.
-\n
-\nA különbség a kettő között az, hogy a gyorsból általában hiányoznak egyes információk, mint az elem hossza vagy a típusa (nem lehet megkülönböztetni az élő videókat a normálaktól), valamint kevesebb elemet adhat vissza.
-\n
-\nA YouTube például egy olyan szolgáltatás, amely ezt a gyors módot RSS hírcsatornával kínálja.
-\n
-\nÍgy a választása azon múlik, hogy melyiket tartja fontosabbnak: a sebességet vagy a pontos információkat.
+ Úgy gondolja, hogy a hírfolyam betöltése lassú? Ha így van, akkor próbálja engedélyezni a gyors betöltést (ezt a beállításokban változtathatja meg, vagy a lenti gomb megnyomásával). \n \nA NewPipe két hírfolyam betöltési stratégiát kínál: \n• A teljes feliratkozott csatorna lekérése, amely lassú, de teljes. \n• Egy dedikált szolgáltatási végpont, amely gyors, de általában nem teljes. \n \nA különbség a kettő között az, hogy a gyorsból általában hiányoznak egyes információk, mint az elem hossza vagy a típusa (nem lehet megkülönböztetni az élő videókat a normálaktól), valamint kevesebb elemet adhat vissza. \n \nA YouTube például egy olyan szolgáltatás, amely ezt a gyors módot RSS hírcsatornával kínálja. \n \nÍgy a választása azon múlik, hogy melyiket tartja fontosabbnak: a sebességet vagy a pontos információkat.
Csatorna profilképének bélyegképe
Legutóbbi
Kiemelt
@@ -612,25 +581,25 @@
- Letöltés befejezve
- %s letöltés befejezve
- Lista feldolgozása…
+ Hírfolyam feldolgozása…
Egyes szolgáltatásoknál érhető el, általában sokkal gyorsabb, és korlátozott számú elemet adhat vissza, gyakran hiányos információkkal (például nincs hossz, elemtípus, vagy élő videó állapot)
Fiók eltávolítva
Megjelölés megnézettként
Még nincs letöltési mappa beállítva, válassza ki az alapértelmezett letöltési mappát most
Tekerősáv bélyegkép-előnézete
Magas minőségű (nagyobb)
- Hiba a lista betöltésekor
+ Hiba a hírfolyam betöltésekor
Nyelv
Támogatás
Weboldal megnyitása
Táblagép mód
Az Android 10-től kezdve, csak a „Storage Access Framework” támogatott
- Új listaelemek
+ Új hírfolyamelemek
Privát
Belső
Készítő által szívecskézve
Be
- A(z) „%s” listája nem tölthető be.
+ A(z) „%s” hírfolyam nem tölthető be.
Ez egy SoundCloud Go+ szám, legalábbis az Ön országában, így nem játszható le vagy tölthető le a NewPipe-pal.
Kapcsolja ki, hogy elrejtse a metainformációs dobozokat, melyek további információkat tartalmaznak a közvetítés létrehozójáról, annak tartalmáról vagy egy keresési kérésről
Hibajelentési értesítés
@@ -668,9 +637,9 @@
- %1$s letöltés törölve
- %1$s letöltés törölve
- Rögzített megjegyzés
+ Kitűzött hozzászólás
LeakCanary nem elérhető
- Lejátszó értesítés
+ Lejátszási értesítés
Módosítsa a progresszív tartalmak betöltési intervallumának méretét (jelenleg %s). Az alacsonyabb érték felgyorsíthatja a kezdeti betöltésüket.
Jelenleg játszott közvetítés értesítésének testreszabása
Értesítések
@@ -678,14 +647,14 @@
Értesítések új élő közvetítésekről a feliratkozott csatornák esetén
Közvetítés részleteinek betöltése.…
Keressen új élő közvetítést
- Új közvetítésértesítések
+ Új közvetítések értesítései
Értesítésen új élő közvetítés esetén a feliratkozott csatornákhoz
Ellenőrzési gyakoriság
Szükséges hálózati kapcsolat
Bármilyen hálózat
Törli az összes letöltött fájlt a lemezről\?
Értesítsen
- Értesítéstek kikapcsolva
+ Az értesítések le vannak tiltva
Lejátszás betöltési intervallumának mérete
Százaléka
@@ -693,14 +662,14 @@
- %s új elő közvetítés
ExoPlayer alapértelmezett
- Feliratkoztál erre a csatornára
+ Feliratkozott erre a csatornára
,
Azok az élő adások melyek nem támogatottak a letöltő által, rejtve vannak
A választott élő adást nem lehet külső lejátszóval lejátszani
- Összes váltása
- Külső lejátszók számára nem érhető el az hang csatorna
- Külső lejátszók számára nem érhető el videó
- Válassz minőséget külső lejátszókhoz
+ Összes be/ki
+ Külső lejátszók számára nem érhető el hangfolyam
+ Külső lejátszók számára nem érhető el videófolyamok
+ Válasszon minőséget a külső lejátszókhoz
Ismeretlen formátum
Ismeretlen minőség
Félhang
@@ -720,11 +689,11 @@
A kiszürkített lejátszólisták már tartalmazzák ezt az elemet.
Állandó bélyegkép feloldása
Ismétlődések eltávolítása
- Végignézve
- Részben megnézve
+ Teljesen megtekintett
+ Részben megtekintett
Kártya
Ez a beállítás csak a(z) %s téma esetén érhető el
- Hardveresmédiagomb-események figyelmen kívül hagyása
+ Hardveres médialejátszó gombok eseményeinek figyelmen kívül hagyása
A következő közvetítések megjelenítése
Az eredeti hangsáv választása, a nyelvtől függetlenül
A látássérülteknek szóló leírást tartalmazó hangsáv választása, ha van ilyen
@@ -741,9 +710,7 @@
Az ExoPlayer dekódoló tartalék funkciójának használata
Engedélyezze ezt a beállítást, ha dekóder előkészítési problémái vannak, ami alacsonyabb prioritású dekóderekre váltást okoz, ha az elsődleges dekóderek előkészítése sikertelen. Ez rosszabb lejátszási teljesítményt eredményezhet, mint az elsődleges dekóderek használata.
Kerülőmegoldás: mindig az ExoPlayer videokimeneti felületének használata
- Ez a kerülőmegoldás elengedi és újból előkészíti a videokodekeket, ha felületváltozás történik, ahelyett, hogy közvetlenül a kodeknél állítaná be a felületet. Ez már alapból használatban van egyes, az ezzel a problémával érintett eszközöknél, a beállításnak Android 6 vagy újabb esetén van hatása.
-\n
-\nA beállítás bekapcsolása megakadályozhatja a lejátszási hibákat, ha átváltja a jelenlegi videolejátszót, vagy teljes képernyőre vált.
+ Ez a kerülőmegoldás elengedi és újból előkészíti a videokodekeket, ha felületváltozás történik, ahelyett, hogy közvetlenül a kodeknél állítaná be a felületet. Ez már alapból használatban van egyes, az ezzel a problémával érintett eszközöknél, a beállításnak Android 6 vagy újabb esetén van hatása\n\nA beállítás bekapcsolása megakadályozhatja a lejátszási hibákat abban az esetben, ha átváltja a jelenlegi videolejátszót, vagy teljes képernyőre vált
%1$s %2$s
szinkronizált
leíró
@@ -757,18 +724,18 @@
Eltávolítja az összes ismétlődő közvetítést ebből a lejátszólistáról\?
eredeti
Kezdőlap pozíciója
- A médiacsatornázás alapértelmezés szerint le van tiltva az Ön készülékén, mivel az Ön készülékmodellje nem támogatja azt.
+ A médiacsatornázás alapértelmezés szerint le van tiltva a saját eszközén, mivel a saját eszközmodellje nem támogatja azt.
Kezdőlapválasztó alulra helyezése
Nincs élő közvetítés
Nincs adatfolyam
Az alábbi értesítési műveletek szerkesztéséhez koppintson rá. Az első három műveletet (lejátszás/szünet, előző és következő) a rendszer állítja be, és nem szabhatók testre.
Csatornalapok lekérése
- A hírcsatorna frissítésekor lekérendő lapok. Ennek az opciónak nincs hatása, ha egy csatorna frissítése gyors módban történik.
+ A hírfolyam frissítésekor lekérendő lapok. Ennek a beállításnak nincs hatása, ha egy csatorna frissítése gyors módban történik.
Miniatűrök
- Feltöltő avatarjai
- Alcsatorna avatarok
- Avatarok
- Bannerek
+ Feltöltő profilképei
+ Alcsatorna profilképei
+ Profilképek
+ Borítóképek
Feliratkozók
Csatornák
Lejátszási listák
@@ -777,13 +744,13 @@
Csatorna fülek
Milyen lapok jelennek meg a csatornaoldalakon
Lejátszási sor megnyitása
- Képernyő tájolásának váltása
- Teljes képernyőre váltás
+ Képernyő tájolás be/ki
+ Teljes képernyő be/ki
Következő közvetítés
Előző közvetítés
Lejátszás
Visszajátszás
- További opciók
+ További lehetőségek
Időtartam
Visszatekerés
Előre
@@ -808,7 +775,7 @@
Webcímlista megosztása
- %1$s: %2$s
Videók
- Dalok
+ Zeneszámok
Rövidek
Élő
Nincs elég szabad hely az eszközön
@@ -822,6 +789,16 @@
Az összes beállítás visszaállítása elveti az összes preferált beállítást, és újraindítja az alkalmazást.
\n
\nBiztosan folytatja?
- Az importálandó exportban lévő beállítások sérülékeny formátumot használnak, amely a NewPipe 0.27.0-s verziója óta elavult. Győződjön meg arról, hogy megbízható forrásból importálja, és a jövőben csak a NewPipe 0.27.0-s vagy újabb verziójából származó exportokat használjon. A beállítások ebből a sérülékeny forrásból történő importálása hamarosan végleg el lesz távolítva, és a NewPipe régi verziói nem fogják tudni importálni az újabb verziókból származó exportokat.
+ Az importálandó exportban lévő beállítások sérülékeny formátumot használnak, amely a NewPipe 0.27.0-ás verziója óta elavult. Győződjön meg arról, hogy megbízható forrásból importálja, és a jövőben csak a NewPipe 0.27.0-ás vagy újabb verziójából származó exportokat használjon. A beállítások ebből a sérülékeny forrásból történő importálása hamarosan végleg el lesz távolítva, és a NewPipe régi verziói nem fogják tudni importálni az újabb verziókból származó exportokat.
másodlagos
+ Megosztás YouTube ideiglenes lejátszási listaként
+ Lejátszási listák
+ Válasszon ki egy hírfolyamcsoportot
+ Még nincs létrehozott hírfolyamcsoport
+ Csatornacsoport-oldal
+ Keresés %1$s
+ Keresés %1$s (%2$s)
+ Kedvelések
+ SoundCloud Top 50 oldal eltávolítva
+ A SoundCloud megszüntette az eredeti Top 50-es listákat. A megfelelő lap el lett távolítva a főoldalról.
diff --git a/app/src/main/res/values-in/strings.xml b/app/src/main/res/values-in/strings.xml
index 4117e3a18..4df522eda 100644
--- a/app/src/main/res/values-in/strings.xml
+++ b/app/src/main/res/values-in/strings.xml
@@ -82,9 +82,6 @@
Meminta kode reCAPTCHA
Hitam
Semua
- r
- J
- T
Buka pada mode sembulan
Izin ini dibutuhkan untuk
\nmembuka di mode sembul
@@ -193,8 +190,8 @@
Terakhir Diputar
Sering Diputar
Konten halaman utama
- Halaman Kosong
- Halaman Kedai
+ Halaman kosong
+ Halaman kiosk
Halaman saluran
Pilih saluran
Belum ada saluran langganan
@@ -417,7 +414,6 @@
- %s pendengar
- Bahasa yang diubah akan diterapkan setelah aplikasi dimulai ulang
Situs PeerTube
Pilih situs PeerTube favorit Anda
Temukan situs yang Anda suka di %s
@@ -809,4 +805,14 @@
Cadangkan dan pulihkan
Pengaturan dalam ekspor yang diimpor menggunakan format rentan yang tidak digunakan lagi sejak NewPipe 0.27.0. Pastikan ekspor yang diimpor berasal dari sumber tepercaya, dan lebih memilih hanya menggunakan ekspor yang diperoleh dari NewPipe 0.27.0 atau yang lebih baru di masa mendatang. Dukungan untuk mengimpor pengaturan dalam format rentan ini akan segera dihapus sepenuhnya, dan NewPipe versi lama tidak akan dapat lagi mengimpor pengaturan ekspor dari versi baru.
sekunder
+ Daftar putar
+ Daftar putar
+ Halaman grup saluran
+ Belum ada grup umpan yang dibuat
+ Pilih grup umpan
+ Cari %1$s
+ Cari %1$s (%2$s)
+ Suka
+ Halaman Top 50 SoundCloud dihapus
+ SoundCloud telah menghentikan dukungan tangga lagu Top 50. Tab terkait telah dihapus dari halaman utama Anda.
diff --git a/app/src/main/res/values-is/strings.xml b/app/src/main/res/values-is/strings.xml
index c5d5d02c6..8d5c71a03 100644
--- a/app/src/main/res/values-is/strings.xml
+++ b/app/src/main/res/values-is/strings.xml
@@ -97,7 +97,6 @@
Hljóðstillingar
Spila í bakgrunni
Þegar hlekkur er opnaður — %s
- þús.
Líkar ekki við
Reyna aftur
Lýsing
@@ -217,7 +216,6 @@
Athugasemd þín (á ensku):
Engar niðurstöður
Myndskeið
- ma.
Engin áhorf
- %s áhorf
@@ -268,7 +266,6 @@
Nýlega spilað
Mest spilað
Aðalsíða
- Tungumálið breytist þegar forritið er endurræst
Flutt út
Flutt inn
Staðbundið
@@ -353,7 +350,6 @@
Flokkur
Merki
NewPipe er þróað af sjálfboðaliðum sem eyða frítíma sínum í að færa þér bestu notendaupplifunina. Gefðu til baka til að hjálpa forriturum að gera NewPipe enn betri á meðan þeir njóta kaffibolla.
- millj.
Slökktu á til að fela lýsingu og viðbótarupplýsingar myndskeiðs
Villa kom upp: %1$s
Þraut reCAPTCHA
@@ -802,4 +798,14 @@
Sýna villustiku
Veldu gæði mynda og hvort eigi að hlaða myndum inn yfirhöfuð, til að minnka notun gagna og minnis. Breytingar munu hreinsa bæði vinnsluminni og diskminni - %s
auka
+ Deila sem YouTube-bráðabirgðaspilunarlista
+ Spilunarlistar
+ Leita í %1$s
+ Leita í %1$s (%2$s)
+ Veldu hóp streyma
+ Enginn hópur streyma útbúinn ennþá
+ Síða rásahóps
+ Líkar við
+ Topp 50 síða SoundCloud fjarlægð
+ SoundCloud er hætt með Topp 50 vinsældalistann. Viðkomandi flipi hefur verið fjarlægður af aðalsíðunni þinni.
diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml
index 6767f8572..f7511b7c7 100644
--- a/app/src/main/res/values-it/strings.xml
+++ b/app/src/main/res/values-it/strings.xml
@@ -82,9 +82,6 @@
Risoluzione reCAPTCHA
Nero
Tutto
- k
- M
- Mrd
È richiesta la risoluzione del reCAPTCHA
Apri in modalità popup
Riproduzione in modalità popup
@@ -427,7 +424,6 @@
- %s ascoltatori
- %s ascoltatori
- La lingua verrà cambiata al riavvio dell\'applicazione
Contenuti in evidenza predefiniti
Durata avanzamento e riavvolgimento rapidi
Istanze PeerTube
@@ -837,4 +833,14 @@
\nVuoi attivarlo?
Le impostazioni nell\'export che viene importato usano un formato vulnerabile che è stato deprecato dalla versione 0.27.0 di NewPipe. Assicuratevi che l\'export importato venga da una fonte fidata, sarebbe preferibile usare solo exports ottenuti da NewPipe 0.27.0 o superiori, nel futuro. Il supporto all\'importazione di Impostazioni in questo formato vulnerabile sarà presto rimosso completamente, da quel momento le versioni di NewPipe più vecchie non saranno più in grado di importare impostazioni tramite export di versioni più recenti.
secondaria
+ Condividi come playlist YouTube temporanea
+ Playlist
+ Seleziona un gruppo di feed
+ Ancora nessun gruppo di feed creato
+ Pagina gruppo canali
+ Cerca %1$s (%2$s)
+ Cerca su %1$s
+ Mi piace
+ Pagina Top 50 di SoundCloud rimossa
+ SoundCloud ha dismesso i grafici Top 50 originali. La scheda relativa è stata rimossa dalla pagina principale.
diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml
index 64e415559..910a6e6a3 100644
--- a/app/src/main/res/values-ja/strings.xml
+++ b/app/src/main/res/values-ja/strings.xml
@@ -83,9 +83,6 @@
reCAPTCHA を要求しました
ブラック
すべて
- k
- M
- B
ポップアップモードで開く
ポップアップモードで開くには
\n権限の許可が必要です
@@ -418,7 +415,6 @@
- %s 人が聴取中
- アプリを再起動すると、言語が変更されます
高速早送り/巻き戻し間隔
PeerTube インスタンス
PeerTube インスタンスを選択する
@@ -808,4 +804,6 @@
\n
\n続行しますか?
インポートされているエクスポートの設定は、NewPipe 0.27.0以降は非推奨であった脆弱な形式を使用します。 インポートされているエクスポートは信頼できる情報源からであり、将来的にはNewPipe 0.27.0かこれより新しいバージョンから得られるエクスポートのみを優先して使用します。 この脆弱な形式で設定をインポートするための対応はすぐに完全に削除され、新しいバージョンからエクスポートの設定をインポートすることは出来ません。
+ YouTubeの一時的なプレイリストとして共有
+ 二次的
diff --git a/app/src/main/res/values-ka/strings.xml b/app/src/main/res/values-ka/strings.xml
index fd0b3f2ab..b74b22991 100644
--- a/app/src/main/res/values-ka/strings.xml
+++ b/app/src/main/res/values-ka/strings.xml
@@ -237,9 +237,6 @@
ვიდეო
აუდიო
ხელახლა სცადეთ
- ათასი
- მლნ
- ბლნ
სერვისის გადართვა, ამჟამად არჩეულია:
გამოწერები არ არის
@@ -333,7 +330,6 @@
გაფრთხილება: ყველა ფაილის იმპორტი ვერ მოხერხდა.
ეს უგულებელყოფს თქვენს მიმდინარე პარამეტრს.
კომენტარების ჩატვირთვა ვერ მოხერხდა
- ენა შეიცვლება აპის გადატვირთვის შემდეგ
ტრენდული
ტოპ 50
ახალი და ცხელი
diff --git a/app/src/main/res/values-kab/strings.xml b/app/src/main/res/values-kab/strings.xml
index eccaaccb9..0ded7974c 100644
--- a/app/src/main/res/values-kab/strings.xml
+++ b/app/src/main/res/values-kab/strings.xml
@@ -1,7 +1,7 @@
- Tavidyutt akked d imesli
- Γef
+ Avidyu akked d imesli
+ Ɣef & ISTEQSIYEN
Amaynut
Akk
Sbedd asnas n Kore yexxuṣen\?
@@ -82,7 +82,6 @@
Sider
Asfaylu udhim
Ttu
- A
Kter
Ih
Amazray
@@ -90,7 +89,7 @@
Iseqdacen
Sfeḍ isefka
Ulac
- Aḍris yettwanγel γef afus
+ Yettwanɣel ɣef afus
Tibdarin n tɣuri
Aneggaru yettwaslekmen
Taɣuri tawurmant
@@ -116,7 +115,7 @@
Beqqeḍ iwenniten
Ameɣri
Isaragen
- Isedγiten
+ Ileqman
Asentel
Ubrik
Bḍu
@@ -128,10 +127,9 @@
Snifel isem
Asider ur yeddi ara
Tamwalit
- o
Aɣawas n deffir
Amazray
- Asteεfu
+ yesteɛfay
Snifel isem
Tividyutin
Ldi deg uminig
@@ -224,4 +222,5 @@
Amaṭṭaf
Tibzimin
Asebter d ilem
-
\ No newline at end of file
+ Iɣewwaṛen n ExoPlayer
+
diff --git a/app/src/main/res/values-kmr/strings.xml b/app/src/main/res/values-kmr/strings.xml
index 9f19ced10..24bc5574e 100644
--- a/app/src/main/res/values-kmr/strings.xml
+++ b/app/src/main/res/values-kmr/strings.xml
@@ -49,9 +49,6 @@
Ne abone
Karûbarê veguheztinê, niha hatî hilbijartin:
- B
- M
- k
Dîsa biceribîne
Deng
Vîdyo
@@ -139,7 +136,6 @@
Pel hate jêbirin
Betal bike
Çareseriya çêtirîn
- Gava ku sepanê ji nû ve dest pê kir dê ziman biguhere.
Ouldîrove nehat barkirin
Ma hûn dixwazin mîhengan jî îthal bikin\?
Ev ê sazkirina xweya heyî ji holê rabike.
diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml
index 0bd08a5e4..8194bd0bc 100644
--- a/app/src/main/res/values-ko/strings.xml
+++ b/app/src/main/res/values-ko/strings.xml
@@ -116,9 +116,6 @@
무엇:\\n요청:\\n콘텐츠 언어:\\n콘텐츠 국가:\\n앱 언어:\\n서비스:\\nGMT 시간:\\n패키지:\\n버전:\\nOS 버전:
결과 없음
구독할 항목을 추가하세요
- 천
- 백만
- 십억
구독자 없음
- 구독자 %s명
@@ -418,7 +415,6 @@
- %s명 청취중
- 앱이 다시 시작되면 언어가 변경됩니다
빠른-감기/되감기 찾는 시간
피어튜브 인스턴스
선호하는 PeerTube 인스턴스 선택
diff --git a/app/src/main/res/values-ku/strings.xml b/app/src/main/res/values-ku/strings.xml
index 4b4c6f2c9..3dc51fcc8 100644
--- a/app/src/main/res/values-ku/strings.xml
+++ b/app/src/main/res/values-ku/strings.xml
@@ -133,9 +133,6 @@
ڤیدیۆ
دەنگ
هەوڵدانەوە
- هەزار
- ملیۆن
- بلیۆن
هیچ بەشداربوویەک نییە
- %s بەشداربوو
@@ -398,7 +395,6 @@
فایلێک بەهەمان ناو هەیە
فایلێکی دابەزێنراو بەم ناوەوە هەیە
ناتوانرێ لەسەر ئەو فایلە جێگیربکرێ
- زمان دەگۆڕدرێ لەدوای داخستن و پاشان کردنەوەی ئەپ.
پەڕەی بنەڕەتی
ماوەی خێرا بردنە پێشەوە\\ گێڕانەوە بۆ دواوە
دۆخی پێرتووبی
diff --git a/app/src/main/res/values-lt/strings.xml b/app/src/main/res/values-lt/strings.xml
index 40fe8e8ad..a3da1721b 100644
--- a/app/src/main/res/values-lt/strings.xml
+++ b/app/src/main/res/values-lt/strings.xml
@@ -6,7 +6,7 @@
Įdiegti
Atšaukti
Atverti naršyklėje
- Atverti iššokančio lango rėžime
+ Atverti iššokančio lango režime
Dalintis
Atsisiųsti
Paieška
@@ -139,9 +139,6 @@
Atstatoma po grotuvo klaidos
Nėra rezultatų
Čia nieko nėra išskyrus svirplius
- Tūkst.
- Mln.
- Mlrd.
Nėra prenumeratorių
Nėra peržiūrų
@@ -494,7 +491,6 @@
Labiausiai patikę
Nesenai pridėta
Vietinė
- Kalba pasikeis paleidus programą iš naujo
Komentarų įkelti nepavyko
Ar norite importuoti nustatymus\?
Kol kas nėra grojaraščio žymų
@@ -836,4 +832,7 @@
\n%2$s
\?
- %1$s: %2$s
+ Grojaraščiai
+ Antrinis
+ Dalintis kaip laikinuoju youtube grojaraščiu
diff --git a/app/src/main/res/values-lv/strings.xml b/app/src/main/res/values-lv/strings.xml
index 9d99c14d6..06600d10a 100644
--- a/app/src/main/res/values-lv/strings.xml
+++ b/app/src/main/res/values-lv/strings.xml
@@ -136,9 +136,6 @@
Nav abonamentu
Izvēlaties pakalpojumu, šobrīd izvēlēts:
- B
- M
- k
Atkārtot
Audio
Video
@@ -272,7 +269,6 @@
Atrodiet instances, kas jums patīk ar %s
Izvēlaties jūsu mīļākās PeerTube instances
PeerTube serveri (instances)
- Valoda nomainīsies, kad aplikāciju restartēs
Neviena lietotne jūsu ierīcē nevar šo atvērt
Nodaļas
Jaunākie
@@ -414,7 +410,7 @@
\n2. Dodieties uz šo URL:%1$s
\n3. Ierakstaties, kad tiek prasīts
\n4. Nokopējiet profila URL, uz kuru tikāt novirzīts.
- YouTube abonementu importēšana no Google Takeout:\n\n1. Dodieties uz šo vietni: %1$s\n2. Autorizējieties, ja nepieciešams\n3. Noklikšķiniet uz \"Visi dati iekļauti\", pēc tam uz \"Atcelt visu atlasi\", pēc tam atlasiet tikai \"Abonementi\" un noklikšķiniet uz \"Labi\"\n4. Noklikšķiniet uz \"Nākamais solis\" un pēc tam uz \"Izveidot eksportu\"\n5. Pēc tam, kad tā parādās, noklikšķiniet uz pogas \"Lejupielādēt\"\n6. Noklikšķiniet IMPORTĒT DATNI zemāk un izvēlaties lejupielādēto .zip failu\n7. [Ja .zip failu neizdodas importēt] Izvelciet .csv failu (parasti zem \"YouTube un YouTube Music/subscriptions/subscriptions.csv\") no arhīva, tad noklikšķiniet uz IMPORTĒT DATNI zemāk un izvēlaties tikko izvilkto csv failu
+ Ievietot YouTube abonementus no Google Takeout:\n\n1. Dodieties uz šo vietni: %1$s\n2. Autorizējieties, ja nepieciešams\n3. Noklikšķiniet uz \"Visi dati iekļauti\", pēc tam uz \"Atcelt visu atlasi\", pēc tam atlasiet tikai \"Abonementi\" un noklikšķiniet uz \"Labi\"\n4. Noklikšķiniet uz \"Nākamais solis\" un pēc tam uz \"Izveidot eksportu\"\n5. Pēc tam, kad tā parādās, noklikšķiniet uz pogas \"Lejupielādēt\"\n6. Noklikšķiniet IEVIETOT DATNI zemāk un izvēlaties lejupielādēto .zip failu\n7. [Ja .zip failu neizdodas ievietot] Izvelciet .csv failu (parasti zem \"YouTube un YouTube Music/subscriptions/subscriptions.csv\") no arhīva, tad noklikšķiniet uz IEVIETOT DATNI zemāk un atlasiet tikko izvilkto csv failu
Noklusējuma darbība, kad atver saturu — %s
Pakalpojumu oriģinālteksti būs redzami video vienumos
Pēc izdzēšanas, piespiedu kārtā ziņot par nepiegādātiem Rx izņēmumiem, ārpus fragmenta vai darbības dzīves cikla
@@ -527,7 +523,7 @@
Atvērt pārlūkā
Atcelt
Uzstādīt
- Netika atrasts video atskaņotājs (jūs varat uzstādīt VLC, lai to atskaņotu).
+ Netika atrasts video atskaņotājs (jūs variet uzstādīt VLC, lai to atskaņotu).
Netika atrasts video atskaņotājs. Uzstādīt VLC?
Publicēts %1$s
Nospiediet uz meklēšanas ikonas, lai sāktu.
@@ -560,7 +556,7 @@
Video lejupielādes mape
Pievienot
Fonā
- Izvēlieties Cilni
+ Atlasiet cilni
Saglabātie saraksti
Abonementi
Rādīt informāciju
@@ -574,11 +570,11 @@
Noņem skaņu dažās izšķirtspējās
Izmantot ārējo video atskaņotāju
Kopīgot ar
- Tiek rādīti rezultāti priekš: %s
+ Tiek rādīti %s rezultāti
Vai jūs domājāt \"%1$s\"\?
Iestatījumi
Meklēt
- Lejupielādēt video failu
+ Lejupielādēt video datni
Lejupielādēt
Populāri
Lejupielāde ir sākusies
@@ -643,7 +639,7 @@
Privātums
Sarakstā neiekļauts
Uzņēmums
- Attālinātie meklēšanas ieteikumi
+ Servera meklēšanas ieteikumi
Atzīmēt kā skatītu
Apstrādā... Var aizņemt kādu laiku
@@ -826,4 +822,7 @@
Kopīgot nosaukumus
Importētā eksporta iestatījumi izmanto ievainojamo formātu, kas tika pārtraukts kopš NewPipe 0.27.0 versijas. Pārliecinieties, ka importētie dati ir no uzticama avota, un turpmāk ir vēlams izmantot tikai datus, kas veikti NewPipe 0.27.0 vai jaunākās versijās. Iestatījumu importēšanas atbalsts šajā neaizsargātajā formātā drīzumā tiks pilnībā aizvākts, un tad vecās NewPipe versijas vairs nevarēs importēt iestatījumus, kas veikti jaunajās versijās.
Šis risinājums problēmas novēršanai atbrīvo un atkārtoti instantiē video kodekus, kad notiek virsmas maiņa, nevis tieši iestatīt virsmu kodekam. ExoPlayer jau izmanto šo risinājumu dažās ierīcēs, kurām ir šī problēma. Šis iestatījums darbosies tikai ierīcēs, kurās uzstādīta operētājsistēma Android 6 un jaunāka.\n\nIespējojot šo iestatījumu, var novērst atskaņošanas kļūdas, pārslēdzot pašreizējo video atskaņotāju vai pārejot uz pilnekrāna režīmu
-
\ No newline at end of file
+ Atskaņošanas saraksti
+ Kopīgot kā pagaidu YouTube atskaņošanas sarakstu
+ sekundārais
+
diff --git a/app/src/main/res/values-mk/strings.xml b/app/src/main/res/values-mk/strings.xml
index ba41b730b..8b493ba08 100644
--- a/app/src/main/res/values-mk/strings.xml
+++ b/app/src/main/res/values-mk/strings.xml
@@ -146,9 +146,6 @@
Видео
Звук
Пробај повторно
- илјади
- M
- милијарди
Нема зачленети
- %s зачленет
@@ -685,7 +682,6 @@
- %s нови стримови
Ве молиме, проверете дали веќе има дискусија за проблем како овој. Создадените дупликати ни одземаат од времето, коешто можеме да го посветиме на поправање на проблемот.
- Јазикот ќе се смени откако апликацијата ќе биде рестартирана
Додај го во редослед следното
Нема гледачи
Готово
@@ -722,4 +718,4 @@
Изберете гестикулација за десната половина од екранот на плеерот
Видеата нема да започнат со емитување во миниплеерот, туку директно ќе се вклучат на цел екран, доколку автоматското ротирање е заклучено. Сѐ уште можете да добиете пристап до миниплеерот, кога ќе излезете од целиот екран
URL адресата не може да биде распознаена. Да се отвори со друга апликација?
-
\ No newline at end of file
+
diff --git a/app/src/main/res/values-ml/strings.xml b/app/src/main/res/values-ml/strings.xml
index c6191ca7b..fb8a32c2e 100644
--- a/app/src/main/res/values-ml/strings.xml
+++ b/app/src/main/res/values-ml/strings.xml
@@ -81,7 +81,6 @@
പുതിയതും ചൂടേറിയതും
മികച്ച 50
ട്രെൻഡിങ്ങ്
- അപ്ലിക്കേഷൻ പുനരാരംഭിച്ചുകഴിഞ്ഞാൽ ഭാഷ മാറും
കമെന്റുകൾ ലോഡുചെയ്യാനായില്ല
ക്രമീകരണങ്ങളും ഇമ്പോർട്ടുചെയ്യാൻ നിങ്ങൾ ആഗ്രഹിക്കുന്നുണ്ടോ\?
ഇത് നിങ്ങളുടെ നിലവിലെ സജ്ജീകരണത്തെ അസാധുവാക്കും.
@@ -184,9 +183,6 @@
സബ്ക്രൈബേഴ്സ് ഇല്ല
സേവനം മാറ്റുക, ഇപ്പോൾ തിരഞ്ഞെടുത്തത്:
- B
- k
- M
വീണ്ടും ശ്രമിക്കുക
ഓഡിയോ
വീഡിയോ
diff --git a/app/src/main/res/values-mr/strings.xml b/app/src/main/res/values-mr/strings.xml
index 304858d84..7a6b2eda4 100644
--- a/app/src/main/res/values-mr/strings.xml
+++ b/app/src/main/res/values-mr/strings.xml
@@ -138,11 +138,8 @@
डेबग
अपडेट
थेट
- ब
प्लेलिस्ट
- म
फाईल
- के
परवाना
चेकसम
इतिहास
diff --git a/app/src/main/res/values-ms/strings.xml b/app/src/main/res/values-ms/strings.xml
index bb0527655..e70e61a74 100644
--- a/app/src/main/res/values-ms/strings.xml
+++ b/app/src/main/res/values-ms/strings.xml
@@ -171,9 +171,6 @@
Video
Audio
Cuba semula
- K
- J
- B
Tiada pelanggan
- %s pelanggan
diff --git a/app/src/main/res/values-nb-rNO/strings.xml b/app/src/main/res/values-nb-rNO/strings.xml
index 9e3916607..3cf92519a 100644
--- a/app/src/main/res/values-nb-rNO/strings.xml
+++ b/app/src/main/res/values-nb-rNO/strings.xml
@@ -89,9 +89,6 @@
Spiller av i oppsprettsmodus
Alle
Avskrudd
- k
- M
- Mrd.
Denne tilgangen trengs for
\nåpning i oppsprettsmodus
reCAPTCHA-oppgave forespurt
@@ -422,7 +419,6 @@
- %s lytter
- %s lyttere
- Språket vil endres etter at appen har startet på nytt
Standard kiosk
PeerTube-instanser
Lokal
diff --git a/app/src/main/res/values-ne/strings.xml b/app/src/main/res/values-ne/strings.xml
index 679b10846..b40145aa6 100644
--- a/app/src/main/res/values-ne/strings.xml
+++ b/app/src/main/res/values-ne/strings.xml
@@ -176,9 +176,6 @@
भिडियो
अडियो
पुन: प्रयास
- हजार
- करोड
- अर्ब
कुनै सदस्यहरू छैनन्
- %s सदस्य
@@ -416,7 +413,6 @@
- %s श्रोता
- %s श्रोताहरु
- भाषा परिवर्तन एप पून:सुरु हुदा लागु हुनेछ।
पूर्वनिर्धारित किओस्क (Kiosk)
छिटो-अगाडि /-पछाडी खोज्न अवधि
PeerTube उदाहरणहरू
diff --git a/app/src/main/res/values-nl-rBE/strings.xml b/app/src/main/res/values-nl-rBE/strings.xml
index d96dc0694..505351b52 100644
--- a/app/src/main/res/values-nl-rBE/strings.xml
+++ b/app/src/main/res/values-nl-rBE/strings.xml
@@ -147,9 +147,6 @@
Video
Geluid
Opnieuw proberen
- k
- M
- mld.
Geen abonnees
- %s abonnee
@@ -486,7 +483,6 @@
Meest leuk gevonden
Recent toegevoegd
Lokaal
- De taal zal veranderen zodra de app opnieuw is opgestart
Geen afspeellijst bladwijzers
Selecteer een afspeellijst
Standaard kiosk
@@ -551,7 +547,7 @@
Shuffle
Herhaal
Je kan maximaal drie acties selecteren om te tonen in de compacte notificatie!
- Pas elke notificatie actie hieronder aan door er op te tikken. Selecteer tot drie acties die getoond worden in de compacte notificatie door gebruik te maken van de selectie vakjes aan de rechterkant
+ Pas elke notificatie actie hieronder aan door er op te tikken. Selecteer tot drie acties die getoond worden in de compacte notificatie door gebruik te maken van de selectie vakjes aan de rechterkant.
Vijfde actie knop
Vierde actie knop
Derde actie knop
@@ -580,7 +576,7 @@
\nAls gevolg van het nieuwe YouTube-beleid met video\'s met leeftijdsbeperkingen heeft NewPipe geen toegang tot deze videostreams en kan deze dus niet afspelen.
Zet uit om videobeschrijving en extra informatie te verbergen
Toon beschrijving
- Nacht Thema
+ Nacht thema
Open met
Featured
Deze inhoud is privé, waardoor het niet kan worden gestreamd of gedownload door NewPipe.
@@ -628,4 +624,14 @@
Meldingen om fouten te rapporteren
Verwerken... Dit kan even duren
LeakCanary is niet beschikbaar
+ Verander de intervalgrootte voor het laden van progressieve inhoud (momenteel %s). Een lagere waarde kan het initiële laden versnellen
+ Afspeellijsten
+ Bewerk elke meldingsactie hieronder door erop te tikken. De eerste drie acties (afspelen/pauzeren, vorige en volgende) zijn ingesteld door het systeem en kunnen niet worden aangepast.
+ Intervalgrootte voor afspelen laden
+ Ja
+ Nee
+ Handig, bijvoorbeeld, als je een hoofdtelefoon gebruikte met kapotte fysieke knoppen
+ Verkies beschrijvende audio
+ Verkies originele audio
+ Selecteer het oorspronkelijke audiospoor, ongeacht de taal
diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml
index 00f095e39..5447ecf49 100644
--- a/app/src/main/res/values-nl/strings.xml
+++ b/app/src/main/res/values-nl/strings.xml
@@ -59,7 +59,7 @@
Video
Geluid
Opnieuw proberen
- Druk op het vergrootglas om te beginnen.
+ Tik op het vergrootglas om te beginnen.
Live
Downloads
Downloads
@@ -68,7 +68,7 @@
Wat:\\nVerzoek:\\nInhoudstaal:\\nInhoudsland:\\nApp-taal:\\nDienst:\\nGMT-tijd:\\nPakket:\\nVersie:\\nVersie van besturingssysteem:
Starten
Pauzeren
- Verwijderen
+ Verwijderen
Controlesom
Oké
Bestandsnaam
@@ -84,9 +84,6 @@
reCAPTCHA-uitdaging gevraagd
Openen in pop-upmodus
Alles
- dznd.
- mln.
- mld.
Deze machtiging is vereist om te
\nopenen in pop-upmodus
Speelt af in pop-upmodus
@@ -420,7 +417,6 @@
- %s luisteraar
- %s luisteraars
- De taal zal veranderen zodra de app opnieuw is opgestart
Standaard kiosk
Duur voor-/achteruit spoelen
PeerTube-instanties
@@ -823,4 +819,9 @@
Instellingen resetten
De instellingen in de export die wordt geïmporteerd, gebruiken een kwetsbaar formaat dat verouderd is sinds NewPipe 0.27.0. Zorg ervoor dat de export die wordt geïmporteerd afkomstig is van een vertrouwde bron, en geef er de voorkeur aan om in de toekomst alleen exporten te gebruiken die zijn verkregen van NewPipe 0.27.0 of nieuwer. Ondersteuning voor het importeren van instellingen in dit kwetsbare formaat zal binnenkort volledig worden verwijderd, en oude versies van NewPipe zullen dan geen exportinstellingen meer uit nieuwe versies kunnen importeren.
secundair
+ Delen als tijdelijke YouTube-afspeellijst
+ Afspeellijsten
+ Selecteer een feedgroep
+ Kanaalgroeppagina
+ Nog geen feedgroep geselecteerd
diff --git a/app/src/main/res/values-nqo/strings.xml b/app/src/main/res/values-nqo/strings.xml
index e0b812410..fd32a527e 100644
--- a/app/src/main/res/values-nqo/strings.xml
+++ b/app/src/main/res/values-nqo/strings.xml
@@ -178,9 +178,6 @@
ߞߐߝߟߌ߫ ߕߍ߫ ߦߋ߲߬
ߡߍ߲ߕߊ
ߞߵߊ߬ ߡߊߛߊ߬ߦߌ߬
- ߥߊ߯
- ߞߋ߲߬
- ߥߟߡ
ߞߊ߬ ߥߏ߬ߦߏ߫ ߣߊ߬ߕߊ ߝߙߊ߬ ߕߎ߲߰ߠߌ߲ ߠߊ߫ ߞߍ߲ߖߘߍߡߊߓߟߏ ߡߊ߬
ߞߊ߬ ߕߎ߲߰ߠߌ߲ ߘߐߞߊ߬ߙߊ߲ ߓߟߏߕߎ߰ (ߞߊߣߊ߬ ߡߊߛߊ߬ߦߌ߬) ߥߏ߬ߦߏ߫ ߢߐ߲߰ߘߐ ߟߎ߫ ߟߊ߫
ߕߏߟߏ߲ߟߊ߲߫ ߥߊ߲߬ߥߊ߲ ߣߎߡߊ߲߫ ߕߟߊ ߖߍ߰ߙߍ ߛߎߥߊ߲ߘߌ߫
@@ -408,7 +405,6 @@
ߖߊ߲߬ߕߏ߬ߒߘߐ: ߞߐߕߐ߮ ߟߎ߬ ߓߍ߯ ߕߍ߫ ߛߋ߫ ߟߊߛߣߍ߫ ߟߊ߫.
ߕߏߟߏ߲߫ ߛߙߍߘߍ ߘߏ߫ ߛߎߥߊ߲ߘߌ߫
ߕߏߟߏ߲߫ ߛߙߍߘߍ߫ ߟߊߞߎ߲߬ߘߎ߬ߣߍ߲߬ ߕߍ߫ ߝߟߐ߫
- ߞߊ߲ ߘߌߣߊ߬ ߡߊߝߊ߬ߟߋ߲߬ ߣߌ߫ ߟߥߊߟߌߟߊ߲ ߣߊ߬ ߘߊ߫ ߘߐߟߥߌ߬
ߛߊ߲ߞߊߥߟߌ ߟߎ߬
ߞߊ߲߬ߞߎߡߊ ߟߎ߬ ߕߍ߫ ߛߋ߫ ߟߊ߫ ߖߛߐ߫ ߟߊ߫
ߘߌ߲߬ߞߌ߬ߙߊ߬ߡߊ
diff --git a/app/src/main/res/values-or/strings.xml b/app/src/main/res/values-or/strings.xml
index 92e6cff37..2ac1ea97c 100644
--- a/app/src/main/res/values-or/strings.xml
+++ b/app/src/main/res/values-or/strings.xml
@@ -62,7 +62,6 @@
ଇତିହାସ ବିଲୋପ ଦେଖନ୍ତୁ
ପ୍ଲେକ୍ ପୋଜିସନ୍ ଡିଲିଟ୍ ହୋଇଛି
ସନ୍ଧାନ ଇତିହାସ ବିଲୋପ ହେଲା
- ଆପ୍ ପୁନଃଆରମ୍ଭ ହେବା ପରେ ଭାଷା ପରିବର୍ତ୍ତନ ହେବ
ପ୍ଲେଲିଷ୍ଟ ଥମ୍ବନେଲ ଭାବରେ ସେଟ୍ କରନ୍ତୁ
ଫାଇଲ୍ କୁ ନବଲିଖନ କରିପାରିବ ନାହିଁ
ଫାଇଲ୍ ସୃଷ୍ଟି ହୋଇପାରିବ ନାହିଁ
@@ -438,7 +437,6 @@
ନାପସନ୍ଦ
ମନ୍ତବ୍ୟ ଗୁଡିକ
ବର୍ଣ୍ଣନା
- ନିୟୁତ
ସମାଧାନ
ପ୍ଲେବେକ୍ ସ୍ପିଡ୍ ନିୟନ୍ତ୍ରଣ
ଟେମ୍ପୋ
@@ -528,7 +526,6 @@
ସମ୍ବନ୍ଧୀୟ ଆଇଟମ୍ ଗୁଡ଼ିକ
ପୁନଃ ସଯାଇବାକୁ ଡ୍ରାଗ୍ କରନ୍ତୁ
ବିରାମ
- ଵୃନ୍ଦ
କୌଣସି ଗ୍ରାହକ ନାହାଁନ୍ତି
ସୃଷ୍ଟି କରନ୍ତୁ
ବିବରଣୀ ପାଇଁ ଟ୍ୟାପ୍ କରନ୍ତୁ
@@ -609,7 +606,6 @@
ବହିଃ-ଚାଳକ ପାଇଁ ଗୁଣବତ୍ତା ଚୟନ କରନ୍ତୁ
ପିନ୍ ହୋଇଥିବା ମନ୍ତବ୍ୟ
ୱେବସାଇଟ୍ ଖୋଲନ୍ତୁ
- ହଜାର
ସୂଚନା ପାଇବା…
- %s ଗ୍ରାହକ
diff --git a/app/src/main/res/values-pa/strings.xml b/app/src/main/res/values-pa/strings.xml
index 3d3975251..274733ecb 100644
--- a/app/src/main/res/values-pa/strings.xml
+++ b/app/src/main/res/values-pa/strings.xml
@@ -84,7 +84,7 @@
ਬੈਕਗ੍ਰਾਊਂਡ ਵਿੱਚ ਚੱਲ ਰਿਹਾ ਹੈ
ਪੌਪ-ਅਪ ਮੋਡ ਵਿੱਚ ਚੱਲ ਰਿਹਾ ਹੈ
ਸਮੱਗਰੀ
- ਉਮਰ-ਮੁਤਾਬਕ-ਪਾਬੰਦੀਸ਼ੁਦਾ ਸਮੱਗਰੀ ਵਿਖਾਓ
+ ਉਮਰ ਮੁਤਾਬਕ ਪਾਬੰਦੀਸ਼ੁਦਾ ਸਮੱਗਰੀ ਵਿਖਾਓ
ਲਾਈਵ
ਡਾਊਨਲੋਡਸ
ਡਾਊਨਲੋਡਸ
@@ -153,13 +153,10 @@
ਵੀਡੀਓ
ਆਡੀਓ
ਦੋਬਾਰਾ ਕੋਸ਼ਿਸ਼ ਕਰੋ
- ਹਜ਼ਾਰ
- ਮਿਲੀਅਨ
- ਅਰਬ
ਕੋਈ ਸਬਸਕ੍ਰਾਈਬਰ ਨਹੀਂ
- %s ਸਬਸਕ੍ਰਾਈਬਰ
- - %s ਸਬਸਕ੍ਰਾਈਬਰ
+ - %s ਸਬਸਕ੍ਰਾਈਬਰਸ
ਕੋਈ ਵਿਊ ਨਹੀਂ
@@ -540,7 +537,6 @@
ਵਧੇਰੇ ਪਸੰਦ ਕੀਤੇ ਗਏ
ਹਾਲ ਹੀ ਵਿੱਚ ਸ਼ਾਮਿਲ
ਸਥਾਨਕ
- ਭਾਸ਼ਾ ਐਪ ਨੂੰ ਦੋਬਾਰਾ ਚਲਾਉਣ \'ਤੇ ਬਦਲੇਗੀ
ਪਲੇਲਿਸਟ ਚੁਣੋ
ਹੋ ਗਿਆ
ਹੱਲ ਕਰੋ
@@ -613,8 +609,8 @@
ਆਨ
,
- - ਡਾਉਨਲੋਡ ਮੁਕੰਮਲ
- - %s ਡਾਉਨਲੋਡ ਮੁਕੰਮਲ
+ - %s ਡਾਉਨਲੋਡ ਮੁਕੰਮਲ
+ - %s ਡਾਊਨਲੋਡਾਂ ਮੁਕੰਮਲ
ਆਫ
ਕਤਾਰ ਵਿੱਚ ਅੱਗੇ ਸ਼ਾਮਿਲ ਕਰੋ
@@ -823,4 +819,12 @@
ਨਹੀਂ
ਇੰਪੋਰਟ ਕੀਤੇ ਜਾ ਰਹੇ ਐਕਸਪੋਰਟ ਵਿੱਚ ਸੈਟਿੰਗਾਂ ਇੱਕ ਕਮਜ਼ੋਰ ਫਾਰਮੈਟ ਦੀ ਵਰਤੋਂ ਕਰਦੀਆਂ ਹਨ ਜੋ ਨਿਊਪਾਈਪ 0.27.0 ਤੋਂ ਬਰਤਰਫ਼ ਕੀਤਾ ਗਿਆ ਸੀ। ਯਕੀਨੀ ਬਣਾਓ ਕਿ ਇੰਪੋਰਟ ਕੀਤਾ ਜਾ ਰਿਹਾ ਨਿਰਯਾਤ ਇੱਕ ਭਰੋਸੇਯੋਗ ਸਰੋਤ ਤੋਂ ਹੈ, ਅਤੇ ਸਿਰਫ਼ ਨਿਊਪਾਈਪ 0.27.0 ਜਾਂ ਇਸਤੋਂ ਨਵੇਂ ਤੋਂ ਪ੍ਰਾਪਤ ਕੀਤੇ ਐਕਸਪੋਰਟ ਦੀ ਵਰਤੋਂ ਕਰਨ ਨੂੰ ਤਰਜੀਹ ਦਿਓ। ਇਸ ਕਮਜ਼ੋਰ ਫਾਰਮੈਟ ਵਿੱਚ ਸੈਟਿੰਗਾਂ ਨੂੰ ਆਯਾਤ ਕਰਨ ਲਈ ਸਮਰਥਨ ਜਲਦੀ ਹੀ ਪੂਰੀ ਤਰ੍ਹਾਂ ਹਟਾ ਦਿੱਤਾ ਜਾਵੇਗਾ ਅਤੇ ਫਿਰ ਨਿਊਪਾਈਪ ਦੇ ਪੁਰਾਣੇ ਸੰਸਕਰਣ ਹੁਣ ਨਵੇਂ ਸੰਸਕਰਣਾਂ ਤੋਂ ਐਕਸਪੋਰਟ ਦੀਆਂ ਸੈਟਿੰਗਾਂ ਨੂੰ ਇੰਪੋਰਟ ਕਰਨ ਦੇ ਯੋਗ ਨਹੀਂ ਹੋਣਗੇ।
ਸੈਕੰਡਰੀ
+ ਅਸਥਾਈ ਯੂਟਿਊਬ ਪਲੇਲਿਸਟ ਵਜੋਂ ਸਾਂਝਾ ਕਰੋ
+ ਪਲੇਲਿਸਟਾਂ
+ %1$s ਦੀ ਖੋਜ ਕਰੋ
+ %1$s (%2$s) ٪1$s ਦੀ ਖੋਜ ਕਰੋ
+ ਫੀਡ ਗਰੁੱਪ ਚੁਣੋ
+ ਅਜੇ ਤੱਕ ਕੋਈ ਫੀਡ ਗਰੁੱਪ ਨਹੀਂ ਬਣਾਇਆ ਗਿਆ
+ ਚੈਨਲ ਗਰੁੱਪ ਪੰਨਾ
+ ਪਸੰਦ
diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml
index e260cc887..8e70ad599 100644
--- a/app/src/main/res/values-pl/strings.xml
+++ b/app/src/main/res/values-pl/strings.xml
@@ -96,10 +96,7 @@
Wszystkie
Wyłączone
Wyczyść
- tys.
- mln
- mld
- To pozwolenie jest wymagane, aby
+ To pozwolenie jest wymagane, aby
\notworzyć w trybie okienkowym
Otwórz w trybie okienkowym
Tryb okienkowy
@@ -432,7 +429,6 @@
- %s słuchaczy
- %s słuchaczy
- Język zmieni się po ponownym uruchomieniu aplikacji
Wielkość skoku przy przewijaniu
Serwery PeerTube
Wybierz swoje ulubione serwery PeerTube
@@ -846,4 +842,14 @@
\nCzy chcesz to włączyć?
Ustawienia w importowanym eksporcie korzystają z podatnego na ataki formatu, który został wycofany od wersji NewPipe 0.27.0. Upewnij się, że importowany eksport pochodzi z zaufanego źródła, i w przyszłości używaj wyłącznie eksportów uzyskanych z NewPipe 0.27.0 lub nowszego. Obsługa importowania ustawień w tym podatnym formacie zostanie wkrótce całkowicie usunięta, a wtedy starsze wersje NewPipe nie będą już mogły importować ustawień z eksportu z nowych wersji.
dodatkowa
+ Udostępnij jako tymczasową playlistę YouTube
+ Playlisty
+ Wybierz grupę kanałów
+ Strona grupy kanałów
+ Nie utworzono jeszcze grupy kanałów
+ Szukaj %1$s
+ Szukaj %1$s (%2$s)
+ Polubienia
+ Usunięto stronę SoundCloud 50 najlepszych
+ SoundCloud wycofał oryginalną listę 50 najlepszych. Odpowiadająca karta została usunięta ze strony głównej.
diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml
index e88e27a18..59ef66671 100644
--- a/app/src/main/res/values-pt-rBR/strings.xml
+++ b/app/src/main/res/values-pt-rBR/strings.xml
@@ -76,7 +76,7 @@
O site não pôde ser analisado
Áudio
Reproduzir no Kodi
- Pesquisar
+ Buscar
Mostrar opção para reproduzir o vídeo no Kodi
Usar player de áudio externo
Usar player de vídeo externo
@@ -90,9 +90,6 @@
Reproduzindo em modo Popup
Todos
Desativado
- mil
- mi
- bi
Essa permissão é necessária
\npara abrir em modo Popup
Limpar
@@ -101,7 +98,7 @@
Lembrar propriedades do Popup
Lembrar último tamanho e posição do Popup
Remove áudio em algumas resoluções
- Sugestões de pesquisa
+ Sugestões de busca
Escolha as sugestões a serem exibidas enquanto estiver buscando
Melhor resolução
Licenças de Terceiros
@@ -111,7 +108,7 @@
Ver no GitHub
Licença do NewPipe
Se você tem ideias de: tradução, mudança no design, limpeza de código ou grandes mudanças de código — ajuda é sempre bem-vinda. Quanto mais se faz, melhor fica!
- Ler licença
+ Ver licença
Colaborar
© %1$s %2$s protegido pela licença %3$s
Sobre o NewPipe
@@ -129,9 +126,9 @@
Inscrições
Novidades
Continuar reprodução
- Continua vídeo após interrupções (ex: ligações)
- Histórico de pesquisa
- Armazena o histórico de pesquisa localmente
+ Continuar a reprodução após interrupções (por exemplo, chamadas telefônicas)
+ Histórico de busca
+ Armazena o histórico de busca localmente
Histórico de exibição
Mantenha o controle dos vídeos assistidos
Histórico
@@ -163,7 +160,7 @@
Player
Nada aqui além de grilos
Deseja excluir este item do histórico de busca\?
- Conteúdo da página inicial
+ Conteúdo da tela inicial
Página em branco
Página do Kiosk
Página do canal
@@ -186,7 +183,7 @@
[Desconhecido]
Reproduzir em segundo plano
Reproduzir em um Popup
- Doar
+ Fazer doação
O NewPipe é desenvolvido por voluntários que usam seu tempo livre para trazer a você a melhor experiência de usuário. Retribua e ajude os desenvolvedores a tornarem o NewPipe ainda melhor enquanto eles desfrutam de uma xícara de café.
Retribuir
Site oficial
@@ -208,7 +205,7 @@
Carregando conteúdo solicitado
Importar base de dados
Exportar base de dados
- Substitui seu histórico atual, inscrições, playlists e (opcionalmente) configurações
+ Substitui seu histórico atual, inscrições, playlists e configurações (opcional)
Exporta histórico, inscrições, playlists e configurações
Exportado
Importado
@@ -291,7 +288,7 @@
Velocidade
Afinação
Desvincular (pode causar distorção)
- Ação de \'abrir\' preferida
+ Ação preferida ao tocar em \'Abrir\'
Ação padrão ao abrir conteúdo — %s
Nenhum vídeo disponível para download
Abrir gaveta
@@ -302,10 +299,10 @@
Remove o histórico de vídeos assistidos e as posições de reprodução
Remover todo o histórico de exibição?
Histórico de exibição removido
- Remover histórico de pesquisas
- Remove o histórico de pesquisas
- Remover todo o histórico de pesquisas?
- Histórico de pesquisa removido
+ Remover histórico de buscas
+ Remove o histórico de buscas
+ Remover todo o histórico de buscas?
+ Histórico de busca removido
1 item excluído.
NewPipe é um copyleft de software livre: Você pode usar, estudar, compartilhar e melhorar a seu gosto. Especificamente você pode redistribuir e/ou modificá-lo sob os termos da GNU General Public License como publicado pela Fundação de Software Livre, na versão 3 da Licença, ou (a seu critério) qualquer versão posterior.
Você também quer importar as configurações?
@@ -341,7 +338,7 @@
Notificações para novas versões do NewPipe
Armazenamento externo indisponível
Não é possível baixar para o cartão SD externo. Redefinir o local da pasta de download\?
- Não foi possível ler as guias salvas, portanto, usamos as guias padrão
+ Não foi possível carregar as guias salvas, portanto, usamos as guias padrão
Restaurar configurações
Deseja restaurar os padrões?
Número de inscritos indisponível
@@ -428,7 +425,6 @@
- %s ouvintes
- %s ouvintes
- O idioma será alterado após reiniciar o aplicativo
Duração de avanço/retrocesso rápido
Instâncias PeerTube
Selecione suas instâncias favoritas do PeerTube
@@ -554,7 +550,7 @@
Nada
Repetir
Você pode selecionar até no máximo três botões para mostrar na notificação compacta!
- Edite os botões de ação de notificação abaixo tocando em cada um. Selecione até três deles para serem mostrados na notificação compacta usando as caixas de seleção à direita.
+ Edite os botões de ação da notificação abaixo tocando em cada um. Selecione até três deles para serem mostrados na notificação compacta usando as caixas de seleção à direita.
Quinto botão de ação
Quarto botão de ação
Terceiro botão de ação
@@ -577,7 +573,7 @@
Calculando hash
Notificações sobre o progresso do hashing de vídeo
Notificar hash de vídeo
- Desative para ocultar as caixas de informações de metadados com informações adicionais sobre o criador, conteúdo da transmissão ou uma solicitação de pesquisa
+ Desative para ocultar as caixas de informações de metadados com informações adicionais sobre o autor, conteúdo da transmissão ou uma solicitação de busca
Mostrar informação de metadados
Recente
Capítulos
@@ -638,9 +634,9 @@
Ativado
Modo tablet
Não mostrar
- Baixa qualidade (pior)
+ Baixa qualidade (menor)
Alta qualidade (melhor)
- Pré visualização da miniatura da barra de busca
+ Pré visualização da miniatura na barra de busca
Os comentários estão desabilitados
Marcar como assistido
Curtido pelo criador
@@ -658,7 +654,7 @@
Enfileira a próxima
Deslize os itens para remove-los
Não inicie os vídeos no mini player, mas vá diretamente para o modo de tela cheia, se a rotação automática estiver bloqueada. Você ainda pode acessar o mini player saindo da tela cheia
- Iniciar player principal em tela cheia
+ Iniciar reprodução principal em tela cheia
Sugestões de busca remotas
Sugestões de busca locais
Processando… Pode demorar um pouco
@@ -715,7 +711,7 @@
Formato desconhecido
Qualidade desconhecida
Tamanho do intervalo de carregamento da reprodução
- Visualizar no site
+ Ver no site
Se você está com problemas ao usar o aplicativo, confira estas respostas para perguntas comuns!
Perguntas frequentes
Classificar
@@ -729,7 +725,7 @@
Falha ao copiar para a área de transferência
Duplicata adicionada %d vez(es)
As playlists em cinza já contêm este item.
- Ignorar eventos de botão de mídia de hardware
+ Ignorar eventos de botões físicos (hardware)
Útil, por exemplo, se você estiver usando um fone de ouvido com botões físicos quebrados
Remover duplicados
Remover duplicados\?
@@ -838,4 +834,13 @@
\nTem certeza de que deseja continuar?
As configurações na exportação que está sendo importada usam um formato vulnerável que foi descontinuado desde o NewPipe 0.27.0. Certifique-se de que a exportação que está sendo importada seja de uma fonte confiável e prefira usar apenas exportações obtidas do NewPipe 0.27.0 ou mais recente no futuro. O suporte para importação de configurações neste formato vulnerável será completamente removido em breve e as versões antigas do NewPipe não poderão mais importar configurações de exportações de novas versões.
secundário
+ Playlists
+ Selecione um grupo de feeds
+ Nenhum grupo de feeds criado ainda
+ Página do grupo do canal
+ Pesquisar %1$s
+ Pesquisar %1$s (%2$s)
+ Curtidas
+ Página Top 50 do SoundCloud removida
+ O SoundCloud descontinuou as paradas originais do Top 50. A aba correspondente foi removida da sua página principal.
diff --git a/app/src/main/res/values-pt-rPT/strings.xml b/app/src/main/res/values-pt-rPT/strings.xml
index 15c174cd8..b7fc0471c 100644
--- a/app/src/main/res/values-pt-rPT/strings.xml
+++ b/app/src/main/res/values-pt-rPT/strings.xml
@@ -229,7 +229,6 @@
OK
Não foi possível atualizar a subscrição
Sim e também os vídeos parcialmente vistos
- M
Ainda não há listas de reprodução favoritas
- %s ouvinte
@@ -423,8 +422,6 @@
Importado
Automático
Substitui o seu histórico, subscrições, listas de reprodução e (opcionalmente) definições
- k
- MM
Remover marcador
Útil ao trocar para dados móveis, mas algumas transferências não podem ser suspensas
Toque longo para colocar na fila
@@ -527,7 +524,6 @@
Modificar escala de legendas e estilo de fundo. Tem que reiniciar a aplicação para aplicar as alterações
Ação recusada pelo sistema
Músicas
- O idioma será alterado assim que reiniciar a app
Adicionar instância
Faixas
Reproduzir no Kodi
@@ -837,4 +833,11 @@
Não há espaço suficiente no aparelho
As configurações na exportação a serem importadas usam um formato vulnerável depreciado desde NewPipe 0.27.0. Certifique-se de que a exportação que é importada é de uma fonte confiável e prefira usar apenas as exportações obtidas do NewPipe 0.27.0 ou mais recentes no futuro. O suporte para importar configurações neste formato vulnerável será removido em breve completamente e, em seguida, versões antigas do NewPipe não serão capazes de importar configurações de exportações de novas versões.
secundário
+ Partilhar como lista de reprodução temporária do YouTube
+ Listas de reprodução
+ Selecione um grupo de feeds
+ Ainda nenhum grupo de feeds criado
+ Página do grupo do canal
+ Pesquisar %1$s
+ Pesquisar %1$s (%2$s)
diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml
index 6d53e9fd9..176baedaa 100644
--- a/app/src/main/res/values-pt/strings.xml
+++ b/app/src/main/res/values-pt/strings.xml
@@ -82,9 +82,6 @@
Abrir no modo popup
Preto
Tudo
- K
- M
- MM
Esta permissão é necessária
\npara o modo popup
Desafio reCAPTCHA
@@ -281,7 +278,7 @@
Limpar histórico de visualizações
Continuar (sem repetição) a fila de reprodução anexando um vídeo relacionado
Mostrar dica \"Toque longo para colocar na fila\"
- Mostrar dica ao premir em segundo plano ou no botão \"Detalhes\" da janela popup:
+ Mostrar dica ao premir em segundo plano ou no botão \"Detalhes:\" da janela popup
Canais
Listas de reprodução
Faixas
@@ -428,7 +425,6 @@
- %s ouvintes
- %s ouvintes
- O idioma será alterado assim que reiniciar a aplicação
Duração de avanço/recuo rápido
Instâncias PeerTube
Defina as suas instâncias PeerTube preferidas
@@ -652,7 +648,7 @@
- %1$s descargas eliminadas
- - Descarga concluída
+ - %s descarga concluída
- %s descargas concluídas
- %s descargas concluídas
@@ -837,4 +833,11 @@
\nDeseja ativar essa opção?
As configurações na exportação a serem importadas usam um formato vulnerável depreciado desde NewPipe 0.27.0. Certifique-se de que a exportação que é importada é de uma fonte confiável e prefira usar apenas as exportações obtidas do NewPipe 0.27.0 ou mais recentes no futuro. O suporte para importar configurações neste formato vulnerável será removido em breve completamente e, em seguida, versões antigas do NewPipe não serão capazes de importar configurações de exportações de novas versões.
secundário
+ Partilhar como lista de reprodução temporária do YouTube
+ Listas de reprodução
+ Selecione um grupo de feeds
+ Ainda nenhum grupo de feeds criado
+ Página do grupo do canal
+ Pesquisar %1$s
+ Pesquisar %1$s (%2$s)
diff --git a/app/src/main/res/values-ro/strings.xml b/app/src/main/res/values-ro/strings.xml
index 5b1238b0c..563cb9914 100644
--- a/app/src/main/res/values-ro/strings.xml
+++ b/app/src/main/res/values-ro/strings.xml
@@ -92,9 +92,6 @@
Dezactivat
Aplicația/UI s-a oprit
Ce:\\nSolicitare:\\nLimba conținutului:\\nȚara conținutului:\\nLimba aplicației:\\nServiciu:\\nOra GMT:\\nPachet:\\nVersiune:\\nVersiune SO:
- k
- mil.
- mld.
Elimină sunetul audio la anumite rezoluții
Fundal
Pop-up
@@ -164,9 +161,9 @@
Istoric
Doriți să ștergeți acest element din istoricul de căutare?
Conținutul pagini principale
- Pagină Goală
+ Pagină goală
Pagina de chioșc
- Pagină Canale
+ Pagină canale
Alegeți un canal
Nu v-ați abonat la niciun canal deocamdată
Alegeți un chioșc
@@ -559,7 +556,6 @@
Cele mai apreciate
Adăugate recent
Local
- Limba se va schimba odată ce aplicația este repornită
Nu există încă marcaje în playlist
Selectați un playlist
Chioșc implicit
@@ -837,4 +833,14 @@
\nSigur doriți să continuați?
Setările din exportul importat folosesc un format vulnerabil care a fost depreciat de la NewPipe 0.27.0. Asigurați-vă că exportul care este importat este dintr-o sursă de încredere și preferați să utilizați numai exporturi obținute din NewPipe 0.27.0 sau mai nou în viitor. Suportul pentru importul setărilor în acest format vulnerabil va fi în curând eliminat complet, iar versiunile vechi ale NewPipe nu vor mai putea importa setările exporturilor din versiunile noi.
secundar
+ Distribuie ca listă de redare temporară YouTube
+ Liste de redare
+ Pagina grupului de canale
+ Caută: %1$s
+ Caută %1$s (%2$s)
+ Selectează un grup de fluxuri
+ Încă nu a fost creat niciun grup de fluxuri
+ Aprecieri
+ Pagina SoundCloud Top 50 a fost eliminată
+ SoundCloud a eliminat Top 50. Fila corespunzătoare a fost eliminată din pagina principală.
diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml
index a212d9888..e00df0413 100644
--- a/app/src/main/res/values-ru/strings.xml
+++ b/app/src/main/res/values-ru/strings.xml
@@ -94,9 +94,6 @@
Выберите отображаемые предложения поиска
Отключено
Убирает звук в некоторых разрешениях
- млн
- млрд
- тыс.
Разрешение всплывающего окна
Помнить последние размер и позицию всплывающего окна
Предложения поиска
@@ -221,7 +218,7 @@
Загрузка запрошенного контента
Скачать файл трансляции
Показать сведения
- Плейлисты
+ Плейлисты в закладках
В плейлист
Быстрый поиск позиции
Выполняется быстрее, но менее точно. Не работает для перемотки на 5, 15 или 25 секунд
@@ -434,7 +431,6 @@
- %s слушателя
- %s слушателей
- Язык будет изменён после перезапуска
Шаг перемотки
Серверы PeerTube
Выберите предпочтительные серверы
@@ -842,4 +838,14 @@
\nВы уверены, что хотите продолжить?
Настройки в импортируемом экспорте используют уязвимый формат, который устарел с версии NewPipe 0.27.0. Убедитесь, что импортируемый экспорт получен из надёжного источника, и в будущем предпочтительнее использовать только экспорт, полученный из NewPipe 0.27.0 или новее. Поддержка импорта настроек в этом уязвимом формате скоро будет полностью удалена, и тогда старые версии NewPipe больше не смогут импортировать настройки из экспорта из новых версий.
вторичный
+ Поделиться как временным плейлистом YouTube
+ Плейлисты
+ Страница группы каналов
+ Выберите группу кормов
+ Группа кормов еще не создана
+ Поиск %1$s
+ Поиск %1$s (%2$s)
+ Лайки
+ Страница SoundCloud Top 50 удалена
+ SoundCloud прекратил поддерживать оригинальные чарты Top 50. Соответствующая вкладка была удалена с вашей главной страницы.
diff --git a/app/src/main/res/values-ryu/strings.xml b/app/src/main/res/values-ryu/strings.xml
index 1a1383015..fcc889ea1 100644
--- a/app/src/main/res/values-ryu/strings.xml
+++ b/app/src/main/res/values-ryu/strings.xml
@@ -83,9 +83,6 @@
reCAPTCHAようきゅうさびたん
ブラック
まじり
- k
- M
- B
ポップアップモードっしふぃらちゅん
ポップアップモードっしふぃらちゅんがー
\nきんぎんぬきょかがふぃちようでぃす
@@ -423,7 +420,6 @@
- %sんかいんがちょうしゅちゅう
- %sんかいんがちょうしゅちゅう
- アプリさいきちゃーしーねー、ぎんぐがへいるかんさりやびーん
こうすくはやうくい/まきむどぅしかんかく
PeerTubeインスタンス
PeerTubeインスタンスさんたくすん
diff --git a/app/src/main/res/values-sat/strings.xml b/app/src/main/res/values-sat/strings.xml
index c1a5aacca..cce0ea919 100644
--- a/app/src/main/res/values-sat/strings.xml
+++ b/app/src/main/res/values-sat/strings.xml
@@ -216,8 +216,6 @@
ᱡᱟᱦᱟᱱ ᱡᱤᱱᱤᱥ
ᱱᱚᱣᱟ ᱨᱮᱫᱚ ᱡᱟᱹᱥᱛᱤ ᱡᱟᱹᱥᱛᱤ ᱠᱨᱤᱠᱮᱴ ᱢᱮᱱᱟᱜᱼᱟ ᱾
ᱵᱷᱤᱰᱤᱭᱳ
- k
- M
ᱥᱮᱞᱮᱫᱤᱭᱟᱹ ᱠᱚᱣᱟᱜ ᱞᱮᱠᱷᱟ ᱵᱟᱭ ᱦᱟᱹᱴᱤᱧ ᱟᱠᱟᱱᱟ
ᱵᱟᱱᱩᱜ ᱧᱮᱞ
ᱵᱷᱤᱰᱤᱭᱳ ᱵᱟᱹᱱᱩᱜᱼᱟ
@@ -477,7 +475,6 @@
ᱱᱟᱣᱟ ᱟᱹᱨᱡᱤ ᱞᱟᱹᱜᱤᱫ ᱟᱹᱪᱩᱨ ᱢᱮ
ᱚᱰᱤᱭᱳ
ᱟᱨᱦᱚᱸ ᱯᱟᱲᱦᱟᱣ ᱢᱮ
- ᱵᱤ
ᱱᱤᱛᱚᱜ ᱵᱟᱪᱷᱟᱣ ᱟᱠᱟᱱ ᱴᱳᱜᱞ ᱥᱮᱵᱟ:
ᱚᱵᱷᱤᱱᱮᱛᱟᱨ ᱵᱟᱹᱱᱩᱜᱼᱟ
ᱚᱠᱚᱭ ᱦᱚᱸ ᱵᱟᱝ ᱧᱮᱞᱚᱜ ᱠᱟᱱᱟ
@@ -519,7 +516,6 @@
ᱢᱤᱫ ᱯᱷᱟᱤᱞᱤᱥᱴ ᱵᱟᱪᱷᱟᱣ ᱢᱮ
ᱤᱢᱯᱳᱨᱴ
ᱱᱤᱛ ᱦᱟᱹᱵᱤᱡ playlist bookmarks ᱵᱟᱹᱱᱩᱜᱼᱟ
- ᱮᱯ ᱮᱦᱚᱵ ᱞᱮᱠᱷᱟᱱ ᱯᱟᱹᱨᱥᱤ ᱵᱚᱫᱚᱞᱚᱜ-ᱟ
ᱮᱥᱯᱟᱨᱴ ᱟᱠᱟᱱᱟ
ᱴᱨᱮᱱᱰᱤᱝ
ᱚᱰᱤᱭᱳ ᱥᱮᱴᱤᱝ
diff --git a/app/src/main/res/values-sc/strings.xml b/app/src/main/res/values-sc/strings.xml
index fc3558ea3..d0a910116 100644
--- a/app/src/main/res/values-sc/strings.xml
+++ b/app/src/main/res/values-sc/strings.xml
@@ -66,7 +66,6 @@
Noos e Populares
Sos mègius 50
Tendèntzias
- Sa limba at a mudare a pustis chi as a torrare a allùghere s\'aplicatzione
Carrigamentu de sos cummentos fallidu
Cheres fintzas importare sas impostatziones\?
Custu at a subraiscrìere sas impostatziones tuas de como.
@@ -166,9 +165,6 @@
Perunu iscritu
Allughe/istuda su servìtziu. Ischertadu como:
- Mrd
- Mlln
- mìg
Torra a proare
Àudio
Vìdeu
@@ -822,4 +818,7 @@
\n
\nSes seguru de bòlere sighire?
Sas impostatziones in s\'esportatzione chi benit importada impreant unu formadu vulneràbile chi est disusadu dae sa versione 0.27.0 de NewPipe. Assegura·ti chi s\'esportatzione importada bèngiat dae una fonte fidada, e preferi a impreare petzi esportatziones otentas dae NewPipe 0.27.0 o prus nou, in su benidore. Su suportu a s\'importatzione de impostatziones in custu formadu vulneràbile at a èssere luego bogadu de su totu, e dae cussu momentu sas versiones de NewPipe prus betzas no ant a èssere prus in gradu de importare impostatziones pro mèdiu de esportatziones de versiones prus noas.
+ Iscalitas
+ Cumpartzi comente un\'iscalita temporànea de YouTube
+ segundàriu
diff --git a/app/src/main/res/values-sk/strings.xml b/app/src/main/res/values-sk/strings.xml
index 88f1b13f5..569011c86 100644
--- a/app/src/main/res/values-sk/strings.xml
+++ b/app/src/main/res/values-sk/strings.xml
@@ -13,12 +13,12 @@
Zdieľať s
Použiť externý prehrávač videa
Použiť externý prehrávač zvuku
- Prevzaté video ukladať do
- Prevzaté video súbory sú uložené tu
+ Adresár stiahnutých videí
+ Stiahnuté video súbory sú uložené tu
Vyberte priečinok pre stiahnuté video
Priečinok pre stiahnuté audio
Vyberte priečinok pre audio súbory
- Prevzaté zvukové súbory sú uložené tu
+ Stiahnuté zvukové súbory sú uložené tu
Štandardné rozlíšenie
Prehrať cez Kodi
Nainštalovať chýbajúcu aplikáciu Kore\?
@@ -74,17 +74,14 @@
Čakajte prosím…
Skopírované do schránky
Priečinok na sťahovanie zadefinujte prosím neskôr v nastaveniach
- Prevzaté
- Prevzaté
+ Sťahované súbory
+ Stiahnuté
Hlásenie o chybe
Aplikácia/UP zlyhalo
Čo:\\nPožiadavka:\\nJazyk obsahu:\\nKrajina Obsahu:\\nJazyk Aplikácie:\\nSlužba:\\nČas v GMT:\\nBalík:\\nVerzia:\\nOS:
Výzva reCAPTCHA
Čierna
Všetko
- k
- M
- B
Požiadavka reCAPTCHA
Otvoriť vo vyskakovacom okne
Tieto práva sú potrebné pre
@@ -165,7 +162,7 @@
Nebol nájdený žiadny prehrávač pre stream (môžete si nainštalovať napr. VLC).
Stiahnuť súbor streamu
Zobraziť info
- Uložené zoznamy
+ Uložené playlisty
Pridať do
Zobrazovať tip \"Pridať podržaním\"
Zobrazí tip pri stlačení tlačidiel pozadia alebo vyskakovacieho okna videa \"Podrobnosti:\"
@@ -179,8 +176,8 @@
Prepnúť na Video
Importovať databázu
Exportovať databázu
- Prepíše aktuálnu históriu, odbery, zoznamy skladieb a (voliteľne) nastavenia
- Exportuje históriu, odbery, zoznamy skladieb a nastavenia
+ Prepíše aktuálnu históriu, odbery, playlisty a (voliteľne) nastavenia
+ Exportuje históriu, odbery, playlisty a nastavenia
Nepodarilo sa prehrať tento stream
Pri prehrávaní došlo k chybe a nemožno pokračovať
Zotavovanie po chybe v prehrávaní
@@ -200,7 +197,7 @@
Naposledy prehrávané
Najprehrávanejšie
Obsah na hlavnej stránke
- Prázdna strana
+ Prázdna stránka
Kiosk
Kanál
Vyberte si kanál
@@ -228,17 +225,17 @@
Vždy sa opýtať
Získavajú sa informácie…
Načítanie požadované obsahu
- Nový zoznam skladieb
+ Nový playlist
Premenovať
Názov
- Pridať do zoznamu skladieb
- Nastaviť ako miniatúru zoznamu skladieb
- Záložka zoznamu skladieb
- Odstrániť Záložku
- Odstrániť tento zoznam skladieb\?
- Zoznam skladieb vytvorený
+ Pridať do playlistu
+ Nastaviť ako miniatúru playlistu
+ Pridať playlist medzi záložky
+ Odstrániť záložku
+ Odstrániť tento playlist?
+ Playlist bol vytvorený
V playliste
- Miniatúra zoznamu skladieb bola zmenená.
+ Miniatúra playlistu bola zmenená.
Bez titulkov
Prispôsobiť
Vyplniť
@@ -321,7 +318,7 @@
Bez limitu
Limitovať rozlíšenie pri použití mobilných dát
Kanály
- Zoznamy skladieb
+ Playlisty
Skladby
Používatelia
Pretáčať tiché pasáže
@@ -428,7 +425,6 @@
- %s poslucháči
- %s poslucháčov
- Zmena jazyka sa prejaví po reštarte aplikácie
Dĺžka rýchleho pretáčania
Inštancie PeerTube
Vyberte si svoje obľúbené inštancie PeerTube
@@ -514,8 +510,7 @@
\n
\nMožno v budúcnosti sa to zmení.
Áno aj čiastočne pozreté videá
- Pozreté videá, ktoré ste pozreli pred a po ich pridaní do zoznamu, budú odstránené.
-\nSte si istí ich odstránením zo zoznamu\? Táto operácia je nezvratná!
+ Pozreté videá, ktoré ste pozreli pred a po ich pridaní do playlistu, budú odstránené. \nSte si istí ich odstránením z playlistu? Táto operácia je nezvratná!
Odstrániť pozreté videá\?
Odstrániť pozreté
Pôvodné texty zo služieb budú viditeľné v položkách streamu
@@ -535,14 +530,14 @@
Nahlásiť na GitHub-e
Kopírovať formátované hlásenie
Zobrazujú sa výsledky pre: %s
- Zoznamy skladieb
+ Zoznam playlistov
Zobraziť iba nezoskupené odbery
Nikdy
Iba na WiFi
Spustí automatické prehrávanie - %s
Prehrať zoznam
- Zatiaľ bez záložiek zoznamu
- Vyberte zoznam skladieb
+ Žiadne záložky playlistov
+ Vyberte playlist
Skontrolujte prosím, či rovnaká chyba už nie je nahlásená. Vytváranie duplicitných hlásení komplikuje prácu vývojárov.
Nemožno rozpoznať URL. Otvoriť pomocou inej aplikácie\?
Automatický rad
@@ -721,7 +716,7 @@
Ak máte problémy s používaním aplikácie, určite si prečítajte tieto odpovede na časté otázky!
Vypnutie trvalého náhľadu
Kopírovanie do schránky zlyhalo
- Zoznamy zobrazené šedou farbou už obsahujú danú položku.
+ Playlisty zobrazené šedou farbou už obsahujú danú položku.
Karta
Dotykom stiahnite %s
Duplikát bol pridaný %d-krát
@@ -748,7 +743,7 @@
Preferovať prehrávanie popisu
Zvuk: %s
Zvuková stopa
- Chcete odstrániť všetky duplikátne streamy z tohoto zoznamu\?
+ Chcete odstrániť všetky duplikátne streamy z tohoto playlistu?
Zobrazovať nasledovné streamy
V tomto streame by už mala byť prítomná zvuková stopa
Výber zvukovej stopy pre externé prehrávače
@@ -787,7 +782,7 @@
Dozadu
Opäť prehrať
Karty, ktoré sa majú načítať pri aktualizácii informačného kanála. Táto možnosť nemá žiadny účinok, ak je kanál aktualizovaný pomocou rýchleho režimu.
- Zoznamy skladieb
+ Playlisty
Presunie výber hlavnej karty do spodnej časti
Žiadne živé prenosy
Prehrať
@@ -837,4 +832,14 @@
\nChcete to povoliť?
Nastavenia v importovanom exporte používajú zraniteľný formát, ktorý bol od verzie NewPipe 0.27.0 zrušený. Uistite sa, že importovaný export pochádza z dôveryhodného zdroja, a v budúcnosti radšej používajte len exporty získané z verzie NewPipe 0.27.0 alebo novšej. Podpora importu nastavení v tomto zraniteľnom formáte bude čoskoro úplne odstránená a potom už staré verzie programu NewPipe nebudú môcť importovať nastavenia exportov z nových verzií.
sekundárny
+ Zdieľať ako dočasný playlist YouTube
+ Playlisty
+ Vybrať skupinu kanálov
+ Skupina kanálov zatiaľ nie je vytvorená
+ Stránka skupiny kanálov
+ Hľadať %1$s (%2$s)
+ Hľadať %1$s
+ Páči sa
+ SoundCloud Top 50 stránka odstránená
+ SoundCloud prestal používať pôvodnú Top 50. Daná stránka bola odstránená z hlavnej stránky.
diff --git a/app/src/main/res/values-sl/strings.xml b/app/src/main/res/values-sl/strings.xml
index b0fcec406..474be8127 100644
--- a/app/src/main/res/values-sl/strings.xml
+++ b/app/src/main/res/values-sl/strings.xml
@@ -83,9 +83,6 @@
Predmet:\\nZahteva:\\nJezik vsebine:\\nDržava vsebine:\\nJezik aplikacije:\\nStoritev:\\nČas v GMT:\\nPaket:\\nRazličica:\\nRazličica OS:
Črna
Vse
- k
- mio
- mrd
Odpri v pojavnem načinu
To dovoljenje je potrebno za odpiranje
\nv pojavnem načinu
@@ -253,7 +250,6 @@
Najbolj všečkan
Dodano nedolgo nazaj
Lokalno
- Jezik bo spremenjen ob ponovnem zagonu aplikacije
Ni mogoče naložiti komentarjev
Ni veljavne ZIP datoteke
Uvoženo
@@ -460,4 +456,4 @@
Označi kot že ogledano
Uporabite hitro nenatančno iskanje
Sesuj predvajalnik
-
\ No newline at end of file
+
diff --git a/app/src/main/res/values-so/strings.xml b/app/src/main/res/values-so/strings.xml
index 7577cfba9..de5178842 100644
--- a/app/src/main/res/values-so/strings.xml
+++ b/app/src/main/res/values-so/strings.xml
@@ -139,7 +139,6 @@
Shiddan
Cusub oo Shiddan
50ka Sare
- Luuqadu waxay isbaddali doontaa marka appka dib loo soo kiciyo
Faallooyinka lama soo kicin karo
Inaad sidoo kale fadhiga soo galiso ma rabtaa\?
Tani waxay baddali fadhiga siduu kuu yahay hadda.
@@ -243,9 +242,6 @@
Dad rukuntay ma jiraan
Furo adeega, hada waxaa dooran:
- B
- K
- M
ku celi
Dhagaysi
Muuqaal
diff --git a/app/src/main/res/values-sq/strings.xml b/app/src/main/res/values-sq/strings.xml
index 873234e23..9aac66de7 100644
--- a/app/src/main/res/values-sq/strings.xml
+++ b/app/src/main/res/values-sq/strings.xml
@@ -207,7 +207,6 @@
Të rejat dhe të nxehtat
Top 50
E trendit
- Gjuha do të ndryshojë sapo aplikacioni të riniset
Nuk mundën të ngarkohen komentet
A dëshironi që të importoni dhe aranzhimet gjithashtu\?
Kjo do të mbishkruajë strukturimin tuaj të tanishëm.
@@ -296,9 +295,6 @@
Nuk ka abonues
Aktivizoje shërbimin, momentalisht e zgjedhur:
- B
- M
- k
Riprovo
Audio
Video
diff --git a/app/src/main/res/values-sr/strings.xml b/app/src/main/res/values-sr/strings.xml
index e212a480b..08726379f 100644
--- a/app/src/main/res/values-sr/strings.xml
+++ b/app/src/main/res/values-sr/strings.xml
@@ -1,7 +1,7 @@
Објављено %1$s
- Нема плејера стримова. Инсталирати VLC\?
+ Нема извођача довода. Да ли уградити ВЛЦ?
Инсталирај
Откажи
Отвори у прегледачу
@@ -60,7 +60,7 @@
Аудио снимак
Покушај поново
Уживо
- Додирните лупу да бисте започели.
+ Додирните лупу ради почетка.
Почни
Паузирај
Избриши
@@ -83,10 +83,7 @@
Решите „reCAPTCHA“ задатак
Црна
Све
- хиљ.
- мил.
- млрд.
- Отвори у искачућем режиму
+ Отвори у искачућем облику
Ова дозвола је потребна за
\nотварање у искачућем режиму
Пуштање у режиму искачућег прозора
@@ -181,7 +178,7 @@
Уклони
Детаљи
Подешавања аудио снимка
- Није пронађен ниједан плејер стримова (можете инсталирати VLC да бисте га покренули).
+ Није пронађен ниједан извођач довода (можете уградити ВЛЦ ради извођења садржаја).
Преузимање фајла стрима
Прикажи информације
Обележене плејлисте
@@ -368,7 +365,6 @@
Највише свиђања
Недавно додато
Локално
- Језик ће бити промењен након рестартовања апликације
Није могуће учитати коментаре
Желите ли да увезете и подешавања\?
Ово ће заменити ваше тренутно подешавање.
@@ -376,8 +372,8 @@
Нема важећег ZIP фајла
Увезено
Извезено
- Још нема обележивача на листи
- Изаберите листу пуштања
+ Још нема обележивача плејлисте
+ Изаберите плејлисту
Подразумевани киоск
Које картице се приказују на главној страници
Највише пуштано
@@ -411,7 +407,7 @@
Нико не гледа
Број пратилаца није доступан
- Изабери услугу, тренутно изабрана:
+ Изаберите услугу, тренутно изабрана:
Превуците за преуређивање
Опис
Коментари
@@ -515,7 +511,7 @@
Мешање
Понављање
Можете да изаберете највише три радње за приказ у компактном обавештењу!
- Измените сваку радњу обавештења додиром на њу. Означите до три радње које ће се приказивати у компактном обавештењу помоћу поља за потврду са десне стране
+ Измените сваку радњу обавештења додиром на њу. Означите до три радње које ће се приказивати у компактном обавештењу помоћу поља за потврду са десне стране.
Дугме пете радње
Дугме четврте радње
Дугме треће радње
@@ -837,4 +833,10 @@
\nЖелите ли да омогућите ово?
Подешавања у извозу који се увозе користе рањив формат који је застарео од NewPipe верзије 0.27.0. Уверите се да извоз који се увози долази из поузданог извора и радије користите само извозе добијене из NewPipe 0.27.0 или новије верзије, у будућности. Подршка за увоз подешавања у овом рањивом формату ће ускоро бити потпуно уклоњена и тада старе NewPipe верзије више неће моћи да увозе подешавања извоза из нових верзија.
секундарни
+ Дели као YouTube привремену плејлисту
+ Плејлисте
+ Још није направљена ниједна група фидова
+ Изаберите групу фидова
+ Страница групе канала
+ Ликовања
diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml
index de74c27f7..1e3df00fc 100644
--- a/app/src/main/res/values-sv/strings.xml
+++ b/app/src/main/res/values-sv/strings.xml
@@ -112,9 +112,6 @@
Video
Ljud
Försök igen
- t
- mn
- md
Inga prenumeranter
- %s prenumerant
@@ -436,7 +433,6 @@
Lokala
Klar
Slå på YouTubes \"Begränsat läge\"
- Språket ändras när appen startas om
Det gick inte att läsa in kommentarerna
Inaktivera snabbläge
Aktivera snabbläge
@@ -823,4 +819,9 @@
\nÄr du säker på att du vill fortsätta?
Inställningarna i exporten som importeras använder ett sårbart format som fasades ut i NewPipe 0.27.0. Se till att exporten som importeras är från en betrodd källa, och överväg att endast använda exporter som erhållits från NewPipe 0.27.0 eller nyare i framtiden. Stöd för import av inställningar från detta sårbara format kommer snart att tas bort helt, och sedan kommer gamla versioner av NewPipe inte att kunna importera inställningar av exporter från nya versioner längre.
sekundär
+ Dela som YouTube temporär spellista
+ Spellistor
+ Välj en flödesgrupp
+ Ingen flödesgrupp har skapats ännu
+ Kanalgruppsida
diff --git a/app/src/main/res/values-ta/strings.xml b/app/src/main/res/values-ta/strings.xml
index 0056f7416..7221ab74d 100644
--- a/app/src/main/res/values-ta/strings.xml
+++ b/app/src/main/res/values-ta/strings.xml
@@ -145,8 +145,8 @@
- %s பார்வைகள்
- - %sகாணொளிகள்
- - %sகாணொளிகள்
+ - %s காணொளி
+ - %s காணொளிகள்
முதற்பக்கத்துக்கு மாற்று
எந்த காணொலியும் இல்லை
@@ -245,7 +245,6 @@
உம் அபிமான பியர்டியூப் நிகழ்வுகளைத் தேர்ந்தெடு
உள்ளடக்க இயல்பிருப்பு மொழி
இயக்குதலைத் மறுதொடர்
- நி
நிகழ்வு ஏற்கனவே உள்ளது
யூடியூபின் \"கட்டுப்பாடு பயன்முறை\"ஐ இயக்கு
பாடல்கள்
@@ -260,8 +259,6 @@
என்ன:\\nகோரிக்கை:\\nஉள்ளடக்க மொழி:\\nஉள்ளடக்க நாடு:\\nசெயலி மொழி:\\nசேவை:\\nGMT நேரம்:\\nசிப்பம்:\\nபதிப்பு:\\nOS பதிப்பு:
காணொளியை இயக்கு, காலவளவு:
கருத்தளிப்புகள்
- ஆ
- ப.ல
இயக்கியைச் சிதை
பட்டியல்களில் இயக்கக குறியட நிலைகாட்டிகளைக் காட்டு
துணையியக்கியில் காணொளிகளை துவக்காதே, ஆனால் தானாக சுழற்றல் பூட்டப்பட்டிருந்தால் நேரடியாக முழுதிரைக்குத் திரும்பு. முழுதிரையை வெளியேறி நீங்கள் இன்னும் துணையியக்கியை அணுகலாம்
@@ -459,7 +456,7 @@
அண்மைக் கால
பாடங்கள்
அகவை வரம்பு
- கூகிள் டேக்அவுட்டிலிருந்து YouTube சந்தாக்களை இறக்குமதி செய்க:\n\n 1. இந்த முகவரி க்குச் செல்லுங்கள்: %1$s\n 2. கேட்டபோது உள்நுழைக\n 3. \"அனைத்து தரவுகளும் சேர்க்கப்பட்டுள்ளன\" என்பதைக் சொடுக்கு செய்க, பின்னர் \"அனைத்தையும் தேர்வு செய்யுங்கள்\", பின்னர் \"சந்தாக்கள்\" மட்டுமே தேர்ந்தெடுத்து \"சரி\" என்பதைக் சொடுக்கு செய்க\n 4. \"அடுத்த படி\" என்பதைக் சொடுக்கு செய்து, பின்னர் \"ஏற்றுமதி உருவாக்கு\"\n 5. \"பதிவிறக்கு\" பொத்தானைக் சொடுக்கு செய்த பிறகு சொடுக்கு செய்க\n 6. கீழே உள்ள இறக்குமதி கோப்பைக் சொடுக்கு செய்து பதிவிறக்கம் செய்யப்பட்ட .zip கோப்பைத் தேர்ந்தெடுக்கவும்\n 7.
+ கூகிள் டேக்அவுட்டிலிருந்து YouTube சந்தாக்களை இறக்குமதி செய்க:\n\n 1. இந்த முகவரி க்குச் செல்லுங்கள்: %1$s\n 2. கேட்டபோது உள்நுழைக\n 3. \"அனைத்து தரவுகளும் சேர்க்கப்பட்டுள்ளன\" என்பதைக் சொடுக்கு செய்க, பின்னர் \"அனைத்தையும் தேர்வு செய்யுங்கள்\", பின்னர் \"சந்தாக்கள்\" மட்டுமே தேர்ந்தெடுத்து \"சரி\" என்பதைக் சொடுக்கு செய்க\n 4. \"அடுத்த படி\" என்பதைக் சொடுக்கு செய்து, பின்னர் \"ஏற்றுமதி உருவாக்கு\"\n 5. \"பதிவிறக்கு\" பொத்தானைக் சொடுக்கு செய்த பிறகு சொடுக்கு செய்க\n 6. கீழே உள்ள இறக்குமதி கோப்பைக் சொடுக்கு செய்து பதிவிறக்கம் செய்யப்பட்ட .zip கோப்பைத் தேர்ந்தெடுக்கவும்\n 7.[.zip இறக்குமதி தோல்வியடைந்தால்] .csv கோப்பை பிரித்தெடுக்கவும் (வழக்கமாக \"YouTube மற்றும் YouTube Music/subscriptions/subscriptions.csv\" என்பதன் கீழ்), கீழே உள்ள IMPORT FILE என்பதைக் கிளிக் செய்து, பிரித்தெடுக்கப்பட்ட csv கோப்பைத் தேர்ந்தெடு
பிளேலிச்ட்டில் சேர்க்கப்படுவதற்கு முன்னும் பின்னும் பார்க்கப்பட்ட வீடியோக்கள் அகற்றப்படும்.\n நீங்கள் உறுதியாக இருக்கிறீர்களா? இதை செயல்தவிர்க்க முடியாது!
இந்த செயலுக்கு பொருத்தமான கோப்பு மேலாளர் எதுவும் கிடைக்கவில்லை.\n சேமிப்பக அணுகல் கட்டமைப்பு இணக்கமான கோப்பு மேலாளரை நிறுவவும்
இரண்டாம் நிலை
@@ -602,7 +599,7 @@
மேலும் செய்தி மற்றும் செய்திகளுக்கு நியூபைப் வலைத்தளத்தைப் பார்வையிடவும்.
நியூபைப் திட்டம் உங்கள் தனியுரிமையை மிகவும் தீவிரமாக எடுத்துக்கொள்கிறது. எனவே, பயன்பாடு உங்கள் அனுமதியின்றி எந்த தரவையும் சேகரிக்காது.\n நீங்கள் ஒரு செயலிழப்பு அறிக்கையை அனுப்பும்போது என்ன தரவு அனுப்பப்படுகிறது மற்றும் சேமிக்கப்படுகிறது என்பதை நியூபிப்பின் தனியுரிமைக் கொள்கை விரிவாக விளக்குகிறது.
நியூபைப்பின் உரிமம்
- நியூபைப் என்பது நகலெடுக்கப்பட்ட லிப்ரே மென்பொருள்: நீங்கள் அதைப் பயன்படுத்தலாம், படிக்கலாம், பகிரலாம் மற்றும் மேம்படுத்தலாம். குறிப்பாக நீங்கள் இலவச மென்பொருள் அறக்கட்டளையால் வெளியிடப்பட்ட குனு பொது பொது உரிமத்தின் விதிமுறைகளின் கீழ் மறுபகிர்வு மற்றும்/அல்லது மாற்றியமைக்கலாம், உரிமத்தின் பதிப்பு 3 அல்லது (உங்கள் விருப்பத்தில்) பின்னர் எந்த பதிப்பையும் மாற்றலாம்.
+ நியூபைப் என்பது நகலெடுக்கப்பட்ட லிப்ரே மென்பொருள்: நீங்கள் அதைப் பயன்படுத்தலாம், படிக்கலாம், பகிரலாம் மற்றும் மேம்படுத்தலாம். குறிப்பாக நீங்கள் இலவச மென்பொருள் அறக்கட்டளையால் வெளியிடப்பட்ட குனு பொது பொதுமக்கள் உரிமத்தின் விதிமுறைகளின் கீழ் மறுபகிர்வு மற்றும்/அல்லது மாற்றியமைக்கலாம், உரிமத்தின் பதிப்பு 3 அல்லது (உங்கள் விருப்பத்தில்) பின்னர் எந்தப் பதிப்பையும் மாற்றலாம்.
பயன்பாட்டைப் பயன்படுத்துவதில் சிக்கல் இருந்தால், பொதுவான கேள்விகளுக்கு இந்த பதில்களைப் பார்க்கவும்!
இணையதளத்தில் காண்க
கடைசியாக விளையாடியது
@@ -616,7 +613,6 @@
செல்லுபடியாகும் சிப் கோப்பு இல்லை
இது உங்கள் தற்போதைய அமைப்பை மேலெழுதும்.
கருத்துகளை ஏற்ற முடியவில்லை
- பயன்பாடு மறுதொடக்கம் செய்யப்பட்டவுடன் மொழி மாறும்
அண்மைக் காலத்தில் சேர்க்கப்பட்டது
மிகவும் பிடித்தது
மாநாடுகள்
@@ -715,7 +711,7 @@
சந்தா எதுவும் தேர்ந்தெடுக்கப்படவில்லை
- %d தேர்ந்தெடுக்கப்பட்டது
- - %d தேர்ந்தெடுக்கப்பட்டது
+ - %d தேர்ந்தெடுக்கப்பட்டன
புதிய
எப்போதும் புதுப்பிக்கவும்
@@ -783,4 +779,9 @@
அடுத்த ச்ட்ரீம்
மீண்டும்
தரவு மற்றும் நினைவக பயன்பாட்டைக் குறைக்க, படங்களின் தகுதி மற்றும் படங்களை ஏற்ற வேண்டுமா என்பதைத் தேர்வுசெய்க. மாற்றங்கள் நினைவகம் மற்றும் ஆன்-வட்டு பட தற்காலிக சேமிப்பு இரண்டையும் அழிக்கின்றன- %s
+ YouTube தற்காலிக பிளேலிச்ட்டாக பகிரவும்
+ பிளேலிச்ட்கள்
+ தீவனக் குழுவைத் தேர்ந்தெடுக்கவும்
+ இதுவரை எந்த ஊட்டக் குழுவும் உருவாக்கப்படவில்லை
+ சேனல் குழு பக்கம்
diff --git a/app/src/main/res/values-te/strings.xml b/app/src/main/res/values-te/strings.xml
index 538ace593..a422dc996 100644
--- a/app/src/main/res/values-te/strings.xml
+++ b/app/src/main/res/values-te/strings.xml
@@ -77,9 +77,6 @@
వీడియో
ఆడియో
మళ్ళీ ప్రయత్నించు
- కి
- ఎం
- బిలియన్
సభ్యులు లేరు
- %s సభ్యుడు
@@ -363,7 +360,6 @@
reCAPTCHA సవాలు అభ్యర్థించబడింది
ప్లేజాబితాను ఎంచుకోండి
డాటాబేసుని ఎగుమతిచేయుము
- యాప్ పునఃప్రారంభించబడిన తర్వాత భాష మారుతుంది
ఛానెల్ వివరాలను చూపు
కంటెంట్ని తెరిచేటప్పుడు డిఫాల్ట్ చర్య — %s
ప్రాధాన్య \'తెరవండి\' చర్య
diff --git a/app/src/main/res/values-th/strings.xml b/app/src/main/res/values-th/strings.xml
index bcbbca0a4..c2cb4669e 100644
--- a/app/src/main/res/values-th/strings.xml
+++ b/app/src/main/res/values-th/strings.xml
@@ -165,9 +165,6 @@
วิดีโอ
เสียง
ลองอีกครั้ง
- พัน
- ล้าน
- พันล้าน
ไม่มีสมาชิกที่สมัครรับ
- %s บอกรับ
diff --git a/app/src/main/res/values-ti/strings.xml b/app/src/main/res/values-ti/strings.xml
index 252f13954..f25b86cc1 100644
--- a/app/src/main/res/values-ti/strings.xml
+++ b/app/src/main/res/values-ti/strings.xml
@@ -2,22 +2,22 @@
ሰረዝ
ኣብ መርበብ-ሓበሬታ ክፉት
- ውጽኢት ምርኣዩ ን፦ %s
- መጀመርታ ምእንቲ ኽትጅምር ነቲ ዜጕልሕ መረጼን ጠውቆ።
- ኣብ %1$s እተሓትመ
- ወሓይዝ ዚጻወት ኣይተረኽበን። VLC፧
+ ውጽኢት ናይ፦ %s
+ ንኽትጅምር ነቲ ምድላይ ምልክት ጠውቆ።
+ ዝተሓትመሉ ዕለት %1$s
+ ናይ ዥረት ተጻዋታይ ኣይተረኽበን። VLC ይውርድ፧
ሐራይ
ቅጥዕታት
\"%1$s\" ማለቱ ድዩ፧
ዚተሪየ ጋባራ
ክፉቴ ምሲ
- ፋይል ውሕጅ ኣራግፍ
+ ስትሪም ፋይል ኣውርድ
ድለ
ኣካፍሎ
ኣራግፍ
ኣካፍሎም ምሲ
መስመር ኣይትጽንበሩ
- ኣብ ሓፍ፡ሓፍ ቅዲ ክፉት
+ ኣብ ፖፕኣፕ ኣገባብ ክፉት
ሰዓብካ\'ኪ
ክቕይር ኣይከኣለን ምዝገብ
ድምጺታት ኣብ ገለ ርዝነት የወግድ
@@ -26,7 +26,7 @@
ሰዓበ
ናይ ደገ ቪድዮ ተጠቐም መጻወቲ
ምውራድ
- ኣይትጽንበሩ
+ ምስዓብ ኣቋርጽ
ነቲ ኣብ\'ቲ ምልክታ ዝተርኣየ ናይ ቪድዮ ምስሊ ካብ 16:9 ናብ 1:1 ርሕቐት ኣቀራርባ ቅረጽ
ምስ Kodi ተጻወት
ድምር ምስ
@@ -43,11 +43,11 @@
ናይ ደገ ድምጺ መጻወቲ ተጠቐም
Android ሕብሪ ናይቲ መተሓሳሰቢ ብመሰረት እቲ ኣብቲ ንእሽቶ ስእሊ ዘሎ ቀንዲ ሕብሪ ከም ዝጥዕሞ ግበር (እዚ ኣብ ኩሉ መሳርሒታት ከምዘይርከብ ኣስተውዕል)
ትሑዝ ፖፕኣፕ ድንቀት
- ነፍሲ ወከፍ መፍለጢ ተግባር ኣብ ታሕቲ ብምጥዋቕ ኣርትዖ። ኣብቲ ውህሉል መተሓሳሰቢ ንኽርአ ክሳብ ሰለስተ ካብኣቶም ምረጽ፡ ኣብ የማናይ ሸነኽ ዘሎ ሳጹናት ብምጥቃም
+ ነፍሲ ወከፍ መፍለጢ ተግባር ኣብ ታሕቲ ብምጥዋቕ ኣርትዖ። ኣብቲ ውህሉል መተሓሳሰቢ ንኽርአ ክሳብ ሰለስተ ካብኣቶም ምረጽ፡ ኣብ የማናይ ሸነኽ ዘሎ ሳጹናት ብምጥቃም።
ፖፕኣፕ
ትሑዝ ድንቀት
ዝወረዱ ናይ ተንቃሳቀሴ-ምስሌ ፋይላት ኣብዚ ይኽዘኑ
-
+ ነባሪ ቪድዮ ፎርማት
ናይ እኽሊ ምስሊ ምስ ናይ 1:1 ርክባት
ቀዳማይ ወሰን ተጠዋቃ
ደገመ
@@ -56,7 +56,7 @@
ናይ ድምጺ ፋይል ኣራግፍ ምረጽ
ዝጎደለ ኮረ ኣፕፕ ኣውራድ፧
ነባሪ ቅርጺ ድምጺ
- ምዝገባ
+ እትስዕቦም
Kodi ሚድያ ማእኸል ቪድዮ ንምጽዋት ዝሕግዝ ኣማራጺ
ኣብቲ ውሱን መፍለጢ ንምርኣይ እንተበዝሐ ሰለስተ ተግባራት ክትመርጽ ትኽእል ኢኻ!
ተንቃሳቀሴ-ምስሌ ፋይል ኣራግፍ
@@ -70,5 +70,28 @@
እወ
ኣይፋልን
ጸሊም
- ኣርእስቲ
-
\ No newline at end of file
+ ቆርበት ኣርእስቲ
+ ለይታው ቆርበት
+ ናይ ፖፕኣፕ ባህርያት ዘክር
+ ናይ ፖፕኣፕ ዝነበሮ ቦታ ዘክር
+ ቅድሚ ምጽራይ ተራታት ናይ መረጋገጺ ሕተት
+ ርእይቶታት ኣርእዩ
+ ርእይቶታት ንምሕባእ እዚ ኣጥፍእ
+ ዋላ-ሓደ
+ ውራድ
+ ኣጻዋታይ
+ ትርኢት
+ ትሕዝቶ
+ ህያው
+ ርጋፍታት
+ ርጋፍታት
+ ኵሉ
+ መስመርት
+ ዝርዝር-ጸወታ
+ ቪዶ
+ ተጠቀምቲ
+ ፍጻመታት
+ ደርፍታት
+ ኣልበማት
+ ስነ-ጥበባውያን
+
diff --git a/app/src/main/res/values-tl/strings.xml b/app/src/main/res/values-tl/strings.xml
index 02794a620..d41b317e8 100644
--- a/app/src/main/res/values-tl/strings.xml
+++ b/app/src/main/res/values-tl/strings.xml
@@ -66,5 +66,4 @@
\nBuksan ang \"%1$s\" sa ayos ng app kung gusto mong makita ito.
Mga Artista
Nakalutang
-
\ No newline at end of file
diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml
index 1c736bb99..c6a1c8557 100644
--- a/app/src/main/res/values-tr/strings.xml
+++ b/app/src/main/res/values-tr/strings.xml
@@ -88,9 +88,6 @@
Devre dışı
Yorumunuz (İngilizce):
Ayrıntılar:
- b
- M
- M
Bu izin, açılır pencere kipinde
\naçmak için gereklidir
reCAPTCHA formu
@@ -164,9 +161,9 @@
Kurtarılamayan oynatıcı hatası oluştu
Oynatıcı hatasından kurtarılıyor
Ana sayfanın içeriği
- Boş Sayfa
- Kiosk Sayfası
- Kanal Sayfası
+ Boş sayfa
+ Kiosk sayfası
+ Kanal sayfası
Kanal seçin
Henüz kanal aboneliği yok
Kiosk seçin
@@ -212,7 +209,7 @@
Bu, var olan kurulumunuzu geçersiz kılacaktır.
Akış dosyasını indir
Bilgi göster
- İmlenen Çalma Listeleri
+ İmlenen Oynatma Listeleri
Ekle
Yeniden düzenlemek için sürükleyin
Oluştur
@@ -422,7 +419,6 @@
- %s dinleyici
- %s dinleyici
- Uygulama yeniden başlatıldıktan sonra dil değişecektir
Hızlı ileri/geri atlama süresi
PeerTube örnekleri
Favori PeerTube örneklerinizi seçin
@@ -823,4 +819,14 @@
\nBunu etkinleştirmek istiyor musunuz?
İçe aktarılmakta olan dışa aktarımdaki ayarlar, NewPipe 0.27.0\'dan itibaren kullanımdan kaldırılan güvenlik açığı olan bir biçim kullanmaktadır. İçe aktarılmakta olan dışa aktarımın güvenilir bir kaynaktan alındığından emin olun ve gelecekte yalnızca NewPipe 0.27.0 veya daha yeni sürümlerden alınan dışa aktarımları kullanmayı tercih edin. Bu güvenlik açığı bulunan biçimdeki ayarları içe aktarma desteği yakında tamamen kaldırılacak ve ardından NewPipe\'ın eski sürümleri artık yeni sürümlerden dışa aktarılan ayarların içe aktarımını yapamayacaktır.
ikincil
+ Oynatma Listeleri
+ YouTube geçici oynatma listesi olarak paylaş
+ Besleme kümesi seç
+ Kanal küme sayfası
+ Besleme kümesi oluşturulmadı
+ Beğeni
+ %1$s İle Ara
+ %1$s İle Ara (%2$s)
+ SoundCloud Top 50 sayfası kaldırıldı
+ SoundCloud, özgün Top 50 listesini artık yayınlamıyor. İlgili sekme ana sayfanızdan kaldırıldı.
diff --git a/app/src/main/res/values-tzm/strings.xml b/app/src/main/res/values-tzm/strings.xml
index 31d89bdd5..a62b37fa3 100644
--- a/app/src/main/res/values-tzm/strings.xml
+++ b/app/src/main/res/values-tzm/strings.xml
@@ -113,7 +113,6 @@
Kkes
Senulfu
Senti
- ifḍ
Als-arem
Imesli
Avidyu
@@ -178,4 +177,4 @@
Ilbumen
Tilgamin n tɣuri
Taɣuri tawurmant
-
\ No newline at end of file
+
diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml
index c9fa9f25c..75498798a 100644
--- a/app/src/main/res/values-uk/strings.xml
+++ b/app/src/main/res/values-uk/strings.xml
@@ -65,9 +65,6 @@
Відео
Аудіо
Повторити спробу
- тис
- млн
- млрд
Почати
Пауза
Видалити
@@ -218,8 +215,8 @@
Відтворювалося найбільше
Вміст на головній сторінці
Порожня сторінка
- Кіоск-сторінка
- Канал
+ Сторінка кіоску
+ Сторінка каналу
Обрати канал
Немає підписок на канали
Обрати кіоск
@@ -428,7 +425,6 @@
- %s слухача
- %s слухачів
- Мова зміниться після перезапуску застосунку
Швидке перемотування
Екземпляри PeerTube
Виберіть сервер PeerTube
@@ -842,4 +838,14 @@
\nВи впевнені, що хочете продовжити?
Параметри експорту, який імпортується, використовують вразливий формат, який не підтримується з NewPipe 0.27.0. Переконайтеся, що експорт, який імпортується, походить із надійного джерела, і в майбутньому віддайте перевагу використанню лише експорту, отриманого з NewPipe 0.27.0 або новішої версії. Підтримку імпорту налаштувань у цьому вразливому форматі незабаром буде повністю припинено, і тоді старі версії NewPipe більше не зможуть імпортувати налаштування експорту з нових версій.
вторинний
+ Поділитися як тимчасовим списком відтворення YouTube
+ Списки відтворення
+ Сторінка групи каналів
+ Виберіть групу каналів
+ Групу каналів ще не створено
+ Вподобання
+ Пошук %1$s
+ Пошук %1$s (%2$s)
+ Сторінку SoundCloud Top 50 видалено
+ SoundCloud припинив підтримку оригінальних чартів Топ-50. Відповідну вкладку видалено з вашої головної сторінки.
diff --git a/app/src/main/res/values-und/strings.xml b/app/src/main/res/values-und/strings.xml
index 94e4f9dd6..0bd3bf442 100644
--- a/app/src/main/res/values-und/strings.xml
+++ b/app/src/main/res/values-und/strings.xml
@@ -70,7 +70,6 @@
ویڈیو
آڈیو
فیر کرو
- ہزار
بݨاؤ
بارے
لائیسنس
diff --git a/app/src/main/res/values-ur/strings.xml b/app/src/main/res/values-ur/strings.xml
index 3bfa5deff..bb95e2811 100644
--- a/app/src/main/res/values-ur/strings.xml
+++ b/app/src/main/res/values-ur/strings.xml
@@ -146,9 +146,6 @@
ویڈیو
آڈیو
دوبارہ کوشش کریں
- ہزار
- دہ لاکھ
- ارب
کوئی صارفین نہیں
- %s صارف
@@ -417,7 +414,6 @@
- %s سننے والا
- %s سننے والے
- جب ایپ دوبارہ شروع ہو گی تو زبان تبدیل ہو جائے گی
طے شدہ کیوسک
آگے بھگانے /- پیچھے کرنے کی مدت
پیر ٹیوب واقعات
@@ -478,7 +474,7 @@
شفل
دوہرائیں
آپ کومپیکٹ نوٹیفکیشن میں زیادہ سے زیادہ تین ایکشن منتخب کرسکتے ہیں!
- ذیل میں ہر اطلاع کی کارروائی پر ٹیپ کرکے اس میں ترمیم کریں۔ دائیں طرف کے چیک باکسز کا استعمال کرکے کمپیکٹ نوٹیفکیشن میں دکھائے جانے والے ان میں سے تین تک کا انتخاب کریں:
+ ذیل میں ہر اطلاع کی کارروائی پر ٹیپ کرکے اس میں ترمیم کریں۔ دائیں طرف کے چیک باکسز کا استعمال کرکے کمپیکٹ نوٹیفکیشن میں دکھائے جانے والے ان میں سے تین تک کا انتخاب کریں۔
پانچواں ایکشن بٹن
چوتھا ایکشن بٹن
تیسرا ایکشن بٹن
@@ -555,4 +551,9 @@
مستقل تھمب نیل کو ان سیٹ کریں
ہارڈ ویئر میڈیا بٹن کے واقعات کو نظر انداز کریں
کار آمد، مثلاً، اگر آپ ہیڈسیٹ ٹوٹے ہوئے فزیکل بٹن کے ساتھ استعمال کر رہے ہیں
+ ذیل میں ہر اطلاعی کارروائی پر ٹیپ کرکے ترمیم کریں۔ پہلے تین ایکشن (پلے/پاؤز، پچھلی اور اگلی) سسٹم کے ذریعے سیٹ کیے گئے ہیں اور انہیں اپنی مرضی کے مطابق نہیں بنایا جا سکتا۔
+ پروگریسو مواد پر لوڈ وقفہ کا سائز تبدیل کریں (فی الحال %s)۔ کم کیمیت ان کی ابتدائی لوڈنگ کو تیز کر سکتی ہے
+ پلے لسٹ
+ ہاں
+ نہیں
diff --git a/app/src/main/res/values-v35/styles.xml b/app/src/main/res/values-v35/styles.xml
new file mode 100644
index 000000000..beb16bcdf
--- /dev/null
+++ b/app/src/main/res/values-v35/styles.xml
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml
index 179f566e8..45934e55c 100644
--- a/app/src/main/res/values-vi/strings.xml
+++ b/app/src/main/res/values-vi/strings.xml
@@ -82,9 +82,6 @@
Video
Âm thanh
Thử lại
- nghìn
- triệu
- tỉ
Bắt đầu
Dừng
Xóa
@@ -441,7 +438,6 @@
Tắt tiếng
Được yêu thích nhất
Đã thêm gần đây
- Ngôn ngữ sẽ thay đổi khi ứng dụng khởi động lại
Bấm \"Xong\" khi hoàn thành
Đã hoàn thành
∞ video
diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml
index 7f9576d7f..2dab8e7ef 100644
--- a/app/src/main/res/values-zh-rCN/strings.xml
+++ b/app/src/main/res/values-zh-rCN/strings.xml
@@ -71,8 +71,6 @@
- %s 次观看
- 千
- 百万
开始
暂停
删除
@@ -153,7 +151,6 @@
详细信息:
播放视频,时长:
视频上传者的头像缩略图
- 十亿
NewPipe 正在下载
请稍后在设置中设定下载目录
使用悬浮窗模式
@@ -198,7 +195,7 @@
是否删除此条搜索历史记录?
主页面的显示内容
空白页
- 『时下流行』页-自定义
+ Kiosk 页面
频道页
选择一个频道
尚未订阅频道
@@ -418,7 +415,6 @@
- %s 位听众
- 语言更改将在重启应用后生效
PeerTube 服务器
设置自定义 PeerTube 服务器
查找你需要的服务器 %s
@@ -809,4 +805,14 @@
是
正导入的导出文件中的设置使用了有漏洞的格式,该格式已从 NewPipe 0.27.0 版本起被弃用。请确保导入的导出文件来自可信来源,今后最好只使用从 NewPipe 0.27.0 或更新版本获取的导出文件。不久后将完全不支持导入这一有漏洞格式的设置,到时旧版 NewPipe 再不能导入来自新版应用的设置导出文件。
第二
+ 分享为 YouTube 临时播放列表
+ 播放列表
+ 频道组页面
+ 选择一个订阅源组
+ 尚未创建订阅源组
+ 赞
+ 搜索%1$s
+ 搜索 %1$s (%2$s)
+ 移除了 SoundCloud Top 50 页面
+ SoundCloud 已停止发布原创 Top 50 榜单。相应的标签页已从你的主页移除。
diff --git a/app/src/main/res/values-zh-rHK/strings.xml b/app/src/main/res/values-zh-rHK/strings.xml
index de343d5e7..750f68f5d 100644
--- a/app/src/main/res/values-zh-rHK/strings.xml
+++ b/app/src/main/res/values-zh-rHK/strings.xml
@@ -87,8 +87,6 @@
全部嘢
App/界面閃退
經過:\\n請求:\\n內容語言:\\n內容國家:\\nApp 語言:\\n服務:\\nGMT 時間:\\n封裝:\\n版本:\\nOS 版本:
- 千
- 百萬
reCAPTCHA 考驗
以浮面模式開啟
\n有呢個權限至得
@@ -103,7 +101,6 @@
不適用
抹走
最佳解像度
- 十億
關於 NewPipe
第三方版權協議
© %1$s %2$s 版權所有,根據 %3$s 嘅條款授權
@@ -326,7 +323,6 @@
頭版要擺放邊啲分頁
打橫掃走啲項目去剷走佢
空白頁
- 重新開過個 app 之後就會轉新語言
時興
頭 50 位
最新同大熱
diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml
index 5dcc6b0da..aa9ecc618 100644
--- a/app/src/main/res/values-zh-rTW/strings.xml
+++ b/app/src/main/res/values-zh-rTW/strings.xml
@@ -82,9 +82,6 @@
影片
音訊
重試
- 千
- 百萬
- 十億
開始
暫停
刪除
@@ -418,7 +415,6 @@
- %s 位聽眾
- 語言將會在重新啟動應用程式後變更
快轉/快退搜尋持續時間
PeerTube 站臺
選取您最愛的 PeerTube 站臺
@@ -809,4 +805,14 @@
\n您想要啟用此功能嗎?
匯入的匯出中的設定使用自 NewPipe 0.27.0 起已廢棄的脆弱格式。請確定匯入的匯出是來自可信賴的來源,並在未來只使用從 NewPipe 0.27.0 或更新版本取得的匯出。對於匯入此脆弱格式設定的支援即將完全移除,屆時舊版本的 NewPipe 將無法再匯入新版本匯出的設定。
次要
+ 分享為 YouTube 臨時播放清單
+ 播放清單
+ 頻道群組頁面
+ 選取 feed 群組
+ 尚未建立 feed 群組
+ 喜歡
+ 搜尋 %1$s
+ 搜尋 %1$s (%2$s)
+ 已移除 SoundCloud Top 50 頁面
+ SoundCloud 已停止原有的 Top 50 排行榜。對應的標籤已從您的首頁移除。
diff --git a/app/src/main/res/values/settings_keys.xml b/app/src/main/res/values/settings_keys.xml
index 5c10c21f8..d95d1270c 100644
--- a/app/src/main/res/values/settings_keys.xml
+++ b/app/src/main/res/values/settings_keys.xml
@@ -294,6 +294,7 @@
show_channel_tabs_channels
show_channel_tabs_playlists
show_channel_tabs_albums
+ show_channel_tabs_likes
show_channel_tabs_about
- @string/show_channel_tabs_videos
@@ -303,6 +304,7 @@
- @string/show_channel_tabs_channels
- @string/show_channel_tabs_playlists
- @string/show_channel_tabs_albums
+ - @string/show_channel_tabs_likes
- @string/show_channel_tabs_about
@@ -313,6 +315,7 @@
- @string/channel_tab_channels
- @string/channel_tab_playlists
- @string/channel_tab_albums
+ - @string/channel_tab_likes
- @string/channel_tab_about
show_search_suggestions
@@ -390,17 +393,20 @@
fetch_channel_tabs_tracks
fetch_channel_tabs_shorts
fetch_channel_tabs_livestreams
+ fetch_channel_tabs_likes
- @string/fetch_channel_tabs_videos
- @string/fetch_channel_tabs_tracks
- @string/fetch_channel_tabs_shorts
- @string/fetch_channel_tabs_livestreams
+ - @string/fetch_channel_tabs_likes
- @string/channel_tab_videos
- @string/channel_tab_tracks
- @string/channel_tab_shorts
- @string/channel_tab_livestreams
+ - @string/channel_tab_likes
import_export_data_path
@@ -1186,6 +1192,8 @@
- bn
- bn-bd
- bn-in
+ - br
+ - bs
- ca
- ckb
- cs
@@ -1201,7 +1209,9 @@
- fi
- fil
- fr
+ - frc
- gl
+ - gu
- he
- hi
- hr
@@ -1213,23 +1223,31 @@
- it
- ja
- jv
+ - ka
- kab
- kmr
+ - kn
- ko
- ku
+ - la
- lt
- lv
- mk
- ml
+ - mn
+ - mr
- ms
- nb-no
+ - nn
- ne
- nl
- nl-be
+
- nqo
- oc
- or
- pa
+ - pa-pk
- pl
- pt
- pt-br
@@ -1248,7 +1266,11 @@
- ta
- te
- th
+ - ti
+ - tl
+ - tok
- tr
+ - tt
- tzm
- uk
- ur
@@ -1271,6 +1293,8 @@
- বাংলা
- বাংলা (বাংলাদেশ)
- বাংলা (भारत)
+ - Brezhoneg
+ - Босански
- Català
- کوردیی سۆرانی
- Čeština
@@ -1286,7 +1310,9 @@
- Suomen kieli
- Wikang Filipino
- Français
+ - Français (Louisiana)
- Galego
+ - ગુજરાતી
- עברית
- हिन्दी
- Hrvatski
@@ -1298,23 +1324,31 @@
- Italiano
- 日本語
- ꦧꦱꦗꦮ
+ - ქართული
- Taqbaylit
- Kurmancî
+ - ಕನ್ನಡ
- 한국어
- کوردی
+ - Latina
- Lietuvių kalba
- latviski
- македонски јазик
- മലയാളം
+ - Монгол хэл
+ - मराठी
- Bahasa Melayu
- Norsk bokmål
- - Nनेपाली
+ - Norsk Nynorsk
+ - नेपाली
- Nederlands (NL)
- Nederlands (BE)
+
- ߒߞߏ
- Occitan
- ଓଡ଼ିଆ
- ਪੰਜਾਬੀ
+ - ਪੰਜਾਬੀ (PK)
- Polski
- Português
- Português (BR)
@@ -1333,7 +1367,11 @@
- தமிழ்
- తెలుగు
- ไทย
+ - ትግርኛ
+ - Wikang Tagalog
+ - Toki Pona
- Türkçe
+ - Татар теле
- Tamaziɣt
- українська мова
- اردو
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index ef0522044..c329d871c 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -17,6 +17,8 @@
Download
Download stream file
Search
+ Search %1$s
+ Search %1$s (%2$s)
Settings
Did you mean \"%1$s\"?
Showing results for: %s
@@ -285,9 +287,9 @@
Video
Audio
Retry
- k
- M
- B
+ %sK
+ %sM
+ %sB
Toggle service, currently selected:
No subscribers
@@ -395,10 +397,10 @@
Content of main page
What tabs are shown on the main page
Swipe items to remove them
- Blank Page
- Kiosk Page
+ Blank page
+ Kiosk page
Default Kiosk
- Channel Page
+ Channel page
Select a channel
No channel subscriptions yet
Select a playlist
@@ -411,7 +413,6 @@
This will override your current setup.
Do you want to also import settings?
Could not load comments
- The language will change once the app is restarted
Select a feed group
No feed group created yet
@@ -827,6 +828,7 @@
Channels
Playlists
Albums
+ Likes
About
Channel tabs
What tabs are shown on the channel pages
@@ -861,6 +863,8 @@
Show more
Show less
The settings in the export being imported use a vulnerable format that was deprecated since NewPipe 0.27.0. Make sure the export being imported is from a trusted source, and prefer using only exports obtained from NewPipe 0.27.0 or newer in the future. Support for importing settings in this vulnerable format will soon be removed completely, and then old versions of NewPipe will not be able to import settings of exports from new versions anymore.
+ SoundCloud Top 50 page removed
+ SoundCloud has discontinued the original Top 50 charts. The corresponding tab has been removed from your main page.
Next
NewPipeExtractor is a library for extracting things from streaming sites. It is a core component of NewPipe, but could be used independently.
Could not load streams
@@ -881,4 +885,10 @@
- Importing %d subscription…
- Importing %d subscriptions…
+ YouTube combined trending removed
+ YouTube has discontinued the combined trending page as of 21st July 2025. NewPipe replaced the default trending page with the trending livestreams.\n\nYou can also select different trending pages in \"Settings > Content > Content of main page\".
+ Gaming trends
+ Trending podcasts
+ Trending movies and shows
+ Trending music
diff --git a/app/src/test/java/org/schabi/newpipe/player/playqueue/PlayQueueItemTest.java b/app/src/test/java/org/schabi/newpipe/player/playqueue/PlayQueueItemTest.java
index d10d33f7e..9addcfc1e 100644
--- a/app/src/test/java/org/schabi/newpipe/player/playqueue/PlayQueueItemTest.java
+++ b/app/src/test/java/org/schabi/newpipe/player/playqueue/PlayQueueItemTest.java
@@ -3,17 +3,15 @@ package org.schabi.newpipe.player.playqueue;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotEquals;
public class PlayQueueItemTest {
public static final String URL = "MY_URL";
@Test
- public void equalsMustNotBeOverloaded() {
+ public void equalsMustWork() {
final PlayQueueItem a = PlayQueueTest.makeItemWithUrl(URL);
final PlayQueueItem b = PlayQueueTest.makeItemWithUrl(URL);
- assertEquals(a, a);
- assertNotEquals(a, b); // they should compare different even if they have the same data
+ assertEquals(a, b);
}
}
diff --git a/app/src/test/java/org/schabi/newpipe/player/playqueue/PlayQueueTest.java b/app/src/test/java/org/schabi/newpipe/player/playqueue/PlayQueueTest.java
index 022089f37..24212b786 100644
--- a/app/src/test/java/org/schabi/newpipe/player/playqueue/PlayQueueTest.java
+++ b/app/src/test/java/org/schabi/newpipe/player/playqueue/PlayQueueTest.java
@@ -3,6 +3,8 @@ package org.schabi.newpipe.player.playqueue;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
+import org.junit.experimental.runners.Enclosed;
+import org.junit.runner.RunWith;
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
import org.schabi.newpipe.extractor.stream.StreamType;
@@ -13,12 +15,14 @@ import java.util.Objects;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.spy;
+@RunWith(Enclosed.class)
@SuppressWarnings("checkstyle:HideUtilityClassConstructor")
public class PlayQueueTest {
static PlayQueue makePlayQueue(final int index, final List streams) {
@@ -168,7 +172,7 @@ public class PlayQueueTest {
final List streams = Collections.nCopies(5, item1);
final PlayQueue queue1 = makePlayQueue(0, streams);
final PlayQueue queue2 = makePlayQueue(0, streams);
- assertTrue(queue1.equalStreams(queue2));
+ assertEquals(queue1, queue2);
assertTrue(queue1.equalStreamsAndIndex(queue2));
}
@@ -177,7 +181,7 @@ public class PlayQueueTest {
final List streams = Collections.nCopies(5, item1);
final PlayQueue queue1 = makePlayQueue(1, streams);
final PlayQueue queue2 = makePlayQueue(4, streams);
- assertTrue(queue1.equalStreams(queue2));
+ assertEquals(queue1, queue2);
assertFalse(queue1.equalStreamsAndIndex(queue2));
}
@@ -187,7 +191,7 @@ public class PlayQueueTest {
final List streams2 = Collections.nCopies(5, item2);
final PlayQueue queue1 = makePlayQueue(0, streams1);
final PlayQueue queue2 = makePlayQueue(0, streams2);
- assertFalse(queue1.equalStreams(queue2));
+ assertNotEquals(queue1, queue2);
}
@Test
@@ -196,7 +200,7 @@ public class PlayQueueTest {
final List streams2 = Collections.nCopies(6, item2);
final PlayQueue queue1 = makePlayQueue(0, streams1);
final PlayQueue queue2 = makePlayQueue(0, streams2);
- assertFalse(queue1.equalStreams(queue2));
+ assertNotEquals(queue1, queue2);
}
}
}
diff --git a/app/src/test/java/org/schabi/newpipe/settings/ImportAllCombinationsTest.kt b/app/src/test/java/org/schabi/newpipe/settings/ImportAllCombinationsTest.kt
index 862ac3b80..2d8a29acb 100644
--- a/app/src/test/java/org/schabi/newpipe/settings/ImportAllCombinationsTest.kt
+++ b/app/src/test/java/org/schabi/newpipe/settings/ImportAllCombinationsTest.kt
@@ -10,7 +10,9 @@ import org.schabi.newpipe.streams.io.StoredFileHelper
import us.shandian.giga.io.FileStream
import java.io.File
import java.io.IOException
-import java.nio.file.Files
+import kotlin.io.path.createTempFile
+import kotlin.io.path.exists
+import kotlin.io.path.fileSize
class ImportAllCombinationsTest {
@@ -47,10 +49,10 @@ class ImportAllCombinationsTest {
BackupFileLocator::class.java,
Mockito.withSettings().stubOnly()
)
- val db = File.createTempFile("newpipe_", "")
- val dbJournal = File.createTempFile("newpipe_", "")
- val dbWal = File.createTempFile("newpipe_", "")
- val dbShm = File.createTempFile("newpipe_", "")
+ val db = createTempFile("newpipe_", "")
+ val dbJournal = createTempFile("newpipe_", "")
+ val dbWal = createTempFile("newpipe_", "")
+ val dbShm = createTempFile("newpipe_", "")
Mockito.`when`(fileLocator.db).thenReturn(db)
Mockito.`when`(fileLocator.dbJournal).thenReturn(dbJournal)
Mockito.`when`(fileLocator.dbShm).thenReturn(dbShm)
@@ -62,7 +64,7 @@ class ImportAllCombinationsTest {
Assert.assertFalse(dbJournal.exists())
Assert.assertFalse(dbWal.exists())
Assert.assertFalse(dbShm.exists())
- Assert.assertTrue("database file size is zero", Files.size(db.toPath()) > 0)
+ Assert.assertTrue("database file size is zero", db.fileSize() > 0)
}
} else {
runTest {
@@ -70,7 +72,7 @@ class ImportAllCombinationsTest {
Assert.assertTrue(dbJournal.exists())
Assert.assertTrue(dbWal.exists())
Assert.assertTrue(dbShm.exists())
- Assert.assertEquals(0, Files.size(db.toPath()))
+ Assert.assertEquals(0, db.fileSize())
}
}
diff --git a/app/src/test/java/org/schabi/newpipe/settings/ImportExportManagerTest.kt b/app/src/test/java/org/schabi/newpipe/settings/ImportExportManagerTest.kt
index 5b8023561..6f6ba671e 100644
--- a/app/src/test/java/org/schabi/newpipe/settings/ImportExportManagerTest.kt
+++ b/app/src/test/java/org/schabi/newpipe/settings/ImportExportManagerTest.kt
@@ -25,8 +25,15 @@ import org.schabi.newpipe.streams.io.StoredFileHelper
import us.shandian.giga.io.FileStream
import java.io.File
import java.io.ObjectInputStream
-import java.nio.file.Files
+import java.nio.file.Paths
import java.util.zip.ZipFile
+import kotlin.io.path.createTempDirectory
+import kotlin.io.path.createTempFile
+import kotlin.io.path.deleteIfExists
+import kotlin.io.path.div
+import kotlin.io.path.exists
+import kotlin.io.path.fileSize
+import kotlin.io.path.inputStream
@RunWith(MockitoJUnitRunner::class)
class ImportExportManagerTest {
@@ -46,7 +53,7 @@ class ImportExportManagerTest {
@Test
fun `The settings must be exported successfully in the correct format`() {
- val db = File(classloader.getResource("settings/newpipe.db")!!.file)
+ val db = Paths.get(classloader.getResource("settings/newpipe.db")!!.toURI())
`when`(fileLocator.db).thenReturn(db)
val expectedPreferences = mapOf("such pref" to "much wow")
@@ -81,29 +88,29 @@ class ImportExportManagerTest {
@Test
fun `Ensuring db directory existence must work`() {
- val dir = Files.createTempDirectory("newpipe_").toFile()
- Assume.assumeTrue(dir.delete())
- `when`(fileLocator.dbDir).thenReturn(dir)
+ val path = createTempDirectory("newpipe_") / BackupFileLocator.FILE_NAME_DB
+ Assume.assumeTrue(path.parent.deleteIfExists())
+ `when`(fileLocator.db).thenReturn(path)
ImportExportManager(fileLocator).ensureDbDirectoryExists()
- assertTrue(dir.exists())
+ assertTrue(path.parent.exists())
}
@Test
fun `Ensuring db directory existence must work when the directory already exists`() {
- val dir = Files.createTempDirectory("newpipe_").toFile()
- `when`(fileLocator.dbDir).thenReturn(dir)
+ val path = createTempDirectory("newpipe_") / BackupFileLocator.FILE_NAME_DB
+ `when`(fileLocator.db).thenReturn(path)
ImportExportManager(fileLocator).ensureDbDirectoryExists()
- assertTrue(dir.exists())
+ assertTrue(path.parent.exists())
}
@Test
fun `The database must be extracted from the zip file`() {
- val db = File.createTempFile("newpipe_", "")
- val dbJournal = File.createTempFile("newpipe_", "")
- val dbWal = File.createTempFile("newpipe_", "")
- val dbShm = File.createTempFile("newpipe_", "")
+ val db = createTempFile("newpipe_", "")
+ val dbJournal = createTempFile("newpipe_", "")
+ val dbWal = createTempFile("newpipe_", "")
+ val dbShm = createTempFile("newpipe_", "")
`when`(fileLocator.db).thenReturn(db)
`when`(fileLocator.dbJournal).thenReturn(dbJournal)
`when`(fileLocator.dbShm).thenReturn(dbShm)
@@ -117,15 +124,15 @@ class ImportExportManagerTest {
assertFalse(dbJournal.exists())
assertFalse(dbWal.exists())
assertFalse(dbShm.exists())
- assertTrue("database file size is zero", Files.size(db.toPath()) > 0)
+ assertTrue("database file size is zero", db.fileSize() > 0)
}
@Test
fun `Extracting the database from an empty zip must not work`() {
- val db = File.createTempFile("newpipe_", "")
- val dbJournal = File.createTempFile("newpipe_", "")
- val dbWal = File.createTempFile("newpipe_", "")
- val dbShm = File.createTempFile("newpipe_", "")
+ val db = createTempFile("newpipe_", "")
+ val dbJournal = createTempFile("newpipe_", "")
+ val dbWal = createTempFile("newpipe_", "")
+ val dbShm = createTempFile("newpipe_", "")
`when`(fileLocator.db).thenReturn(db)
val emptyZip = File(classloader.getResource("settings/nodb_noser_nojson.zip")?.file!!)
@@ -136,7 +143,7 @@ class ImportExportManagerTest {
assertTrue(dbJournal.exists())
assertTrue(dbWal.exists())
assertTrue(dbShm.exists())
- assertEquals(0, Files.size(db.toPath()))
+ assertEquals(0, db.fileSize())
}
@Test
diff --git a/doc/README.ar.md b/doc/README.ar.md
index f005050c7..0d89e5d59 100644
--- a/doc/README.ar.md
+++ b/doc/README.ar.md
@@ -6,12 +6,8 @@
-
-
-
-
-
-
+
+
diff --git a/doc/README.asm.md b/doc/README.asm.md
index 37d0949b7..5958fe280 100644
--- a/doc/README.asm.md
+++ b/doc/README.asm.md
@@ -6,12 +6,8 @@
-
-
-
-
-
-
+
+
diff --git a/doc/README.de.md b/doc/README.de.md
index 34ad94ab1..ab1ab2727 100644
--- a/doc/README.de.md
+++ b/doc/README.de.md
@@ -9,12 +9,8 @@
-
-
-
-
-
-
+
+
diff --git a/doc/README.es.md b/doc/README.es.md
index 4a08cba08..155b004a1 100644
--- a/doc/README.es.md
+++ b/doc/README.es.md
@@ -6,12 +6,8 @@
-
-
-
-
-
-
+
+
diff --git a/doc/README.fr.md b/doc/README.fr.md
index cfebcb2a6..7b450b04d 100644
--- a/doc/README.fr.md
+++ b/doc/README.fr.md
@@ -9,12 +9,8 @@
-
-
-
-
-
-
+
+
diff --git a/doc/README.hi.md b/doc/README.hi.md
index 6098c6c26..3f51960b5 100644
--- a/doc/README.hi.md
+++ b/doc/README.hi.md
@@ -6,12 +6,8 @@
-
-
-
-
-
-
+
+
diff --git a/doc/README.it.md b/doc/README.it.md
index d926db6bc..b8621a8fe 100644
--- a/doc/README.it.md
+++ b/doc/README.it.md
@@ -6,12 +6,8 @@
-
-
-
-
-
-
+
+
diff --git a/doc/README.ja.md b/doc/README.ja.md
index 1e751855b..13ddebb02 100644
--- a/doc/README.ja.md
+++ b/doc/README.ja.md
@@ -6,12 +6,8 @@
-
-
-
-
-
-
+
+
diff --git a/doc/README.ko.md b/doc/README.ko.md
index 39fb7e11c..5f731d076 100644
--- a/doc/README.ko.md
+++ b/doc/README.ko.md
@@ -6,12 +6,8 @@
-
-
-
-
-
-
+
+
diff --git a/doc/README.pa.md b/doc/README.pa.md
index 9b84ded18..12229f1d8 100644
--- a/doc/README.pa.md
+++ b/doc/README.pa.md
@@ -6,12 +6,8 @@
-
-
-
-
-
-
+
+
diff --git a/doc/README.pl.md b/doc/README.pl.md
index 9574491c7..f95b62e33 100644
--- a/doc/README.pl.md
+++ b/doc/README.pl.md
@@ -6,12 +6,8 @@
-
-
-
-
-
-
+
+
diff --git a/doc/README.pt_BR.md b/doc/README.pt_BR.md
index b73da2de1..c8257ce3e 100644
--- a/doc/README.pt_BR.md
+++ b/doc/README.pt_BR.md
@@ -10,12 +10,8 @@
-
-
-
-
-
-
+
+
diff --git a/doc/README.ro.md b/doc/README.ro.md
index 3f146f7e4..5ce14ca1d 100644
--- a/doc/README.ro.md
+++ b/doc/README.ro.md
@@ -6,12 +6,8 @@
-
-
-
-
-
-
+
+
diff --git a/doc/README.ru.md b/doc/README.ru.md
index 8a9955707..56d3b48ed 100644
--- a/doc/README.ru.md
+++ b/doc/README.ru.md
@@ -6,12 +6,8 @@
-
-
-
-
-
-
+
+
diff --git a/doc/README.ryu.md b/doc/README.ryu.md
index f3ca31af0..23081f70d 100644
--- a/doc/README.ryu.md
+++ b/doc/README.ryu.md
@@ -6,12 +6,8 @@
-
-
-
-
-
-
+
+
diff --git a/doc/README.so.md b/doc/README.so.md
index 843bed749..a66f7d12b 100644
--- a/doc/README.so.md
+++ b/doc/README.so.md
@@ -6,12 +6,8 @@
-
-
-
-
-
-
+
+
diff --git a/doc/README.sr.md b/doc/README.sr.md
index 21e4d857c..9dbb439d0 100644
--- a/doc/README.sr.md
+++ b/doc/README.sr.md
@@ -9,12 +9,8 @@
-
-
-
-
-
-
+
+
diff --git a/doc/README.tr.md b/doc/README.tr.md
index 6e95e54de..5a0096e55 100644
--- a/doc/README.tr.md
+++ b/doc/README.tr.md
@@ -6,12 +6,8 @@
-
-
-
-
-
-
+
+
diff --git a/doc/README.zh_TW.md b/doc/README.zh_TW.md
index 05518624f..8cfcbd640 100644
--- a/doc/README.zh_TW.md
+++ b/doc/README.zh_TW.md
@@ -6,12 +6,8 @@
-
-
-
-
-
-
+
+
diff --git a/fastlane/metadata/android/ar/changelogs/1004.txt b/fastlane/metadata/android/ar/changelogs/1004.txt
index 562f16944..27c377cf2 100644
--- a/fastlane/metadata/android/ar/changelogs/1004.txt
+++ b/fastlane/metadata/android/ar/changelogs/1004.txt
@@ -1 +1,3 @@
-تم إصلاح YouTube الذي لا يقوم بتشغيل أي دفق
+يعمل هذا الإصدار على إصلاح YouTube الذي يوفر دفق 360 بكسل فقط.
+
+لاحظ أن الحل المستخدم في هذا الإصدار مؤقت على الأرجح، وعلى المدى الطويل يجب تنفيذ بروتوكول فيديو SABR، لكن أعضاء TeamNewPipe مشغولون حاليًا، لذا فإن أي مساعدة ستكون موضع تقدير كبير! https://github.com/TeamNewPipe/NewPipe/issues/12248
diff --git a/fastlane/metadata/android/ar/changelogs/1005.txt b/fastlane/metadata/android/ar/changelogs/1005.txt
new file mode 100644
index 000000000..933c7286a
--- /dev/null
+++ b/fastlane/metadata/android/ar/changelogs/1005.txt
@@ -0,0 +1,17 @@
+جديد
+• أضف دعمًا لنظام Android Auto
+• السماح لإعداد مجموعات التغذية كعلامات شاشة رئيسية
+• [يوتيوب] شارك كقائمة تشغيل مؤقتة
+• [SoundCloud] Leges Table Tab
+
+تحسن
+• تلميحات شريط بحث أفضل
+• عرض تاريخ التنزيل في التنزيلات
+• استخدام Android 13 لكل لغة
+
+مُثَبَّت
+• إصلاح ألوان النص المكسورة في الوضع المظلم
+• [youtube] إصلاح قوائم التشغيل لا تحميل أكثر من 100 عنصر
+• [youtube] إصلاح مقاطع الفيديو الموصى بها مفقودة
+• إصلاح حوادث في عرض قائمة التاريخ
+• إصلاح الطوابع الزمنية في ردود التعليقات
diff --git a/fastlane/metadata/android/azb/short_description.txt b/fastlane/metadata/android/azb/short_description.txt
new file mode 100644
index 000000000..4f991f505
--- /dev/null
+++ b/fastlane/metadata/android/azb/short_description.txt
@@ -0,0 +1 @@
+اندرویددا یوتیوب اوچون بیر اؤزگور و یونگول قاپاخ.
diff --git a/fastlane/metadata/android/cs/changelogs/1004.txt b/fastlane/metadata/android/cs/changelogs/1004.txt
index 7035a1112..2568518e7 100644
--- a/fastlane/metadata/android/cs/changelogs/1004.txt
+++ b/fastlane/metadata/android/cs/changelogs/1004.txt
@@ -1 +1,3 @@
-Opraveno nepřehrávání jakéhokoli streamu ve službě YouTube
+Tato verze opravuje problém, kdy YouTube poskytoval stream pouze v rozlišení 360p.
+
+Upozorňujeme, že použité řešení je pravděpodobně dočasné a z dlouhodobého hlediska bude potřeba implementovat video protokol SABR, ale členové týmu TeamNewPipe jsou momentálně zaneprázdnění, takže jakákoli pomoc bude velmi vítána! https://github.com/TeamNewPipe/NewPipe/issues/12248
diff --git a/fastlane/metadata/android/cs/changelogs/1005.txt b/fastlane/metadata/android/cs/changelogs/1005.txt
new file mode 100644
index 000000000..2059e43de
--- /dev/null
+++ b/fastlane/metadata/android/cs/changelogs/1005.txt
@@ -0,0 +1,17 @@
+Novinky
+• Přidána podpora pro Android Auto.
+• Možnost nastavit skupiny kanálů jako záložky na hlavní obrazovce.
+• [YouTube] Sdílení jako dočasný seznam skladeb.
+• [SoundCloud] Záložka Oblíbené kanály
+
+Vylepšeno
+• Lepší nápověda pro vyhledávací lištu
+• Zobrazení data stažení v sekci Stažené soubory
+• Použití jazyka Android 13 pro jednotlivé aplikace
+
+Opraveno
+• Oprava chybných barev textu v tmavém režimu
+• [YouTube] Oprava seznamů skladeb, které nenačtou více než 100 položek
+• [YouTube] Oprava chybějících doporučených videí
+• Oprava pádů v zobrazení seznamu historie
+• Oprava časových značek v odpovědích na komentáře
diff --git a/fastlane/metadata/android/de/changelogs/1004.txt b/fastlane/metadata/android/de/changelogs/1004.txt
index 43623578f..0e1705c34 100644
--- a/fastlane/metadata/android/de/changelogs/1004.txt
+++ b/fastlane/metadata/android/de/changelogs/1004.txt
@@ -1 +1,3 @@
-Behoben, dass YouTube keinen Stream abspielte
+Behoben, dass YouTube nur einen 360p-Stream bereitstellt.
+
+Beachte, dass die in dieser Version verwendete Lösung wahrscheinlich nur vorübergehend ist und langfristig das SABR-Videoprotokoll implementiert werden muss, aber die Mitglieder von TeamNewPipe sind derzeit sehr beschäftigt, daher wäre jede Hilfe sehr willkommen! https://github.com/TeamNewPipe/NewPipe/issues/12248
diff --git a/fastlane/metadata/android/de/changelogs/1005.txt b/fastlane/metadata/android/de/changelogs/1005.txt
new file mode 100644
index 000000000..ac9000b65
--- /dev/null
+++ b/fastlane/metadata/android/de/changelogs/1005.txt
@@ -0,0 +1,17 @@
+Neu
+• Android Auto
+• Feed-Gruppen als Hauptbildschirm-Tabs
+• [YouTube] Teilen als temporäre Wiedergabeliste
+• [SoundCloud] Gefällt-Kanal-Tab
+
+Verbessert
+• Bessere Suchleisten-Hinweise
+• Anzeige des Downloaddatums
+• App-spezifische Spracheinstellungen (Android 13)
+
+Behoben
+• Fehlerhafte Textfarben im dunklen Modus
+• [YouTube] Wiedergabelisten laden nicht mehr als 100 Einträge
+• [YouTube] Fehlende empfohlene Videos
+• Abstürze in der Verlaufslisten-Ansicht
+• Zeitstempel in Kommentarantworten
diff --git a/fastlane/metadata/android/el/changelogs/1001.txt b/fastlane/metadata/android/el/changelogs/1001.txt
new file mode 100644
index 000000000..a6b41b138
--- /dev/null
+++ b/fastlane/metadata/android/el/changelogs/1001.txt
@@ -0,0 +1,6 @@
+Βελτιωμένο
+- Επιτρέπει πάντα την αλλαγή των προτιμήσεων ειδοποίησης παίκτη στο Android 13+
+
+Διορθωμένο
+- Διόρθωση Η εξαγωγή βάσης δεδομένων/συνδρομών δεν έκοβε ένα ήδη υπάρχον αρχείο, οδηγώντας ενδεχομένως σε κατεστραμμένη εξαγωγή
+- Διόρθωση της συνέχισης του παίκτη από την αρχή όταν κάνετε κλικ σε μια χρονοσφραγίδα
diff --git a/fastlane/metadata/android/el/changelogs/1002.txt b/fastlane/metadata/android/el/changelogs/1002.txt
new file mode 100644
index 000000000..0d790eba9
--- /dev/null
+++ b/fastlane/metadata/android/el/changelogs/1002.txt
@@ -0,0 +1,4 @@
+Διορθώθηκε το YouTube που δεν αναπαράγει καμία ροή.
+
+Αυτή η έκδοση αντιμετωπίζει μόνο το πιο πιεστικό σφάλμα που εμποδίζει τη φόρτωση λεπτομερειών βίντεο του YouTube.
+Γνωρίζουμε ότι υπάρχουν και άλλα προβλήματα και σύντομα θα προβούμε σε ξεχωριστή έκδοση για την επίλυσή τους.
diff --git a/fastlane/metadata/android/el/changelogs/1003.txt b/fastlane/metadata/android/el/changelogs/1003.txt
new file mode 100644
index 000000000..dbc62399e
--- /dev/null
+++ b/fastlane/metadata/android/el/changelogs/1003.txt
@@ -0,0 +1,6 @@
+Αυτή είναι μια έκδοση hotfix που διορθώνει σφάλματα του YouTube:
+- [YouTube] Διόρθωση της μη φόρτωσης πληροφοριών βίντεο, διόρθωση σφαλμάτων HTTP 403 κατά την αναπαραγωγή βίντεο και αποκατάσταση της αναπαραγωγής ορισμένων βίντεο με περιορισμούς ηλικίας.
+- Διόρθωση της μη αλλαγής των μεγεθών των λεζάντων
+- Διόρθωση της λήψης πληροφοριών δύο φορές κατά το άνοιγμα μιας ροής
+- [Soundcloud] Αφαίρεση μη αναπαραγώγιμων ροών με προστασία DRM
+- Ενημερωμένες μεταφράσεις
diff --git a/fastlane/metadata/android/el/changelogs/66.txt b/fastlane/metadata/android/el/changelogs/66.txt
new file mode 100644
index 000000000..0417512c7
--- /dev/null
+++ b/fastlane/metadata/android/el/changelogs/66.txt
@@ -0,0 +1,26 @@
+### Βελτιώσεις
+
+- Απενεργοποίηση της κίνησης του εικονιδίου του burgermenu #1486
+- Αναίρεση διαγραφής λήψεων #1472
+- Επιλογή λήψης στο μενού κοινής χρήσης #1498
+- Προστέθηκε επιλογή κοινής χρήσης στο μενού παρατεταμένου πατήματος #1454
+- Ελαχιστοποίηση του κύριου προγράμματος αναπαραγωγής κατά την έξοδο #1354
+- Ενημέρωση της έκδοσης της βιβλιοθήκης και διόρθωση αντιγράφων ασφαλείας της βάσης δεδομένων #1510
+- Ενημέρωση ExoPlayer 2.8.2 #1392
+ - Ανασχεδιάστηκε ο διάλογος ελέγχου της ταχύτητας αναπαραγωγής ώστε να υποστηρίζει διαφορετικά μεγέθη βημάτων για ταχύτερη αλλαγή ταχύτητας.
+ - Προστέθηκε μια εναλλαγή για γρήγορη μετακίνηση προς τα εμπρός κατά τη διάρκεια σιωπής στον έλεγχο ταχύτητας αναπαραγωγής. Αυτό θα πρέπει να είναι χρήσιμο για τα ακουστικά βιβλία και ορισμένα είδη μουσικής και μπορεί να φέρει μια πραγματικά απρόσκοπτη εμπειρία (και μπορεί να σπάσει ένα τραγούδι με πολλές σιωπές =\\).
+ - Αναδιαμόρφωση της ανάλυσης πηγής πολυμέσων ώστε να επιτρέπεται η μετάδοση μεταδεδομένων μαζί με τα πολυμέσα εσωτερικά στον αναπαραγωγέα, αντί να γίνεται χειροκίνητα. Τώρα έχουμε μια ενιαία πηγή μεταδεδομένων και είναι άμεσα διαθέσιμα κατά την έναρξη της αναπαραγωγής.
+ - Διορθώθηκε η μη ενημέρωση των απομακρυσμένων μεταδεδομένων της λίστας αναπαραγωγής όταν είναι διαθέσιμα νέα μεταδεδομένα κατά το άνοιγμα του τμήματος της λίστας αναπαραγωγής.
+ - Διάφορες διορθώσεις στο UI: #1383, τα στοιχεία ελέγχου ειδοποίησης αναπαραγωγής στο παρασκήνιο είναι τώρα πάντα λευκά, είναι ευκολότερο να κλείσετε το αναδυόμενο πρόγραμμα αναπαραγωγής μέσω εκτίναξης
+- Χρήση νέου εξαηωγέα με ανασχεδιασμένη αρχιτεκτονική για πολλαπλές υπηρεσίες
+
+### Διορθώσεις
+
+- Διόρθωση #1440 Προβληματική διάταξη πληροφοριών βίντεο #1491
+- Διόρθωση ιστορικού προβολής #1497
+ - #1495, ενημέρωση των μεταδεδομένων (μικρογραφία, τίτλος και αριθμός βίντεο) μόλις ο χρήστης αποκτήσει πρόσβαση στη λίστα αναπαραγωγής.
+ - #1475, με την καταχώριση μιας προβολής στη βάση δεδομένων όταν ο χρήστης ξεκινάει ένα βίντεο σε εξωτερική συσκευή αναπαραγωγής σε λεπτομερές θραύσμα.
+- Διόρθωση του χρονικού ορίου creen σε περίπτωση λειτουργίας αναδυόμενου παραθύρου. #1463 (Διορθώθηκε το #640)
+- Διόρθωση του κύριου προγράμματος αναπαραγωγής βίντεο #1509
+ - #1412] Διορθώθηκε η λειτουργία επανάληψης που προκαλεί NPE του αναπαραγωγέα όταν λαμβάνεται νέα εντολή ενώ εργάζεται στο παρασκήνιο.
+ - Διορθώθηκε κατά την ελαχιστοποίηση του αναπαραγωγέα σε αναδυόμενο παράθυρο, να μην τον καταστρέφει όταν δεν έχει χορηγηθεί άδεια αναδυόμενου παραθύρου.
diff --git a/fastlane/metadata/android/el/changelogs/68.txt b/fastlane/metadata/android/el/changelogs/68.txt
new file mode 100644
index 000000000..307e9d647
--- /dev/null
+++ b/fastlane/metadata/android/el/changelogs/68.txt
@@ -0,0 +1,31 @@
+# αλλαγές της v0.14.1
+
+### Διορθώθηκε
+- Διορθώθηκε η αποτυχία αποκρυπτογράφησης url βίντεο #1659
+- Διορθώθηκε ο σύνδεσμος περιγραφής που δεν εξάγεται καλά #1657
+
+# αλλαγές της v0.14.0
+
+### Νέα
+- Νέος σχεδιασμός συρταριού #1461
+- Νέα προσαρμόσιμη αρχική σελίδα #1461
+
+### Βελτιώσεις
+- Αναθεωρημένοι έλεγχοι χειρονομιών #1604
+- Νέος τρόπος για να κλείσετε το αναδυόμενο πρόγραμμα αναπαραγωγής #1597
+
+### Διορθώσεις
+- Διόρθωση σφάλματος όταν ο αριθμός των συνδρομών δεν είναι διαθέσιμος. Κλείνει το #1649.
+ - Εμφάνιση της φράσης «Ο αριθμός των συνδρομητών δεν είναι διαθέσιμος» σε αυτές τις περιπτώσεις.
+- Διόρθωση NPE όταν μια λίστα αναπαραγωγής του YouTube είναι κενή
+- Γρήγορη διόρθωση για τα περίπτερα στο SoundCloud
+- Αναδιαμόρφωση και διόρθωση σφάλματος #1623
+ - Διόρθωση κυκλικού αποτελέσματος αναζήτησης #1562
+ - Διόρθωση της γραμμής αναζήτησης που δεν είναι στατικά τοποθετημένη
+ - Διόρθωση Τα βίντεο YT Premium δεν μπλοκάρονται σωστά
+ - Διόρθωση Βίντεο που μερικές φορές δεν φορτώνονται (λόγω ανάλυσης DASH)
+ - Διόρθωση συνδέσμων στην περιγραφή βίντεο
+ - Εμφάνιση προειδοποίησης όταν κάποιος προσπαθεί να κάνει λήψη σε εξωτερική sdcard
+ - διόρθωση της εξαίρεσης που δεν εμφανίζεται τίποτα προκαλεί αναφορά
+ - η μικρογραφία δεν εμφανίζεται στο πρόγραμμα αναπαραγωγής στο παρασκήνιο για το Android 8.1 [δείτε εδώ](https://github.com/TeamNewPipe/NewPipe/issues/943)
+- Διορθώστε την εγγραφή του δέκτη εκπομπής. Κλείνει το #1641.
diff --git a/fastlane/metadata/android/el/changelogs/69.txt b/fastlane/metadata/android/el/changelogs/69.txt
new file mode 100644
index 000000000..d3f51f3f3
--- /dev/null
+++ b/fastlane/metadata/android/el/changelogs/69.txt
@@ -0,0 +1,19 @@
+### New
+- Μεγάλο πάτημα διαγραφής και κοινής χρήσης στις συνδρομές #1516
+- UI tablet και διάταξη λίστας πλέγματος #1617
+
+### Βελτιώσεις
+- αποθήκευση και επαναφόρτωση της τελευταίας χρησιμοποιούμενης αναλογίας διαστάσεων #1748
+- Ενεργοποίηση γραμμικής διάταξης στη δραστηριότητα «Λήψεις» με πλήρη ονόματα βίντεο #1771
+- Διαγραφή και κοινή χρήση συνδρομών απευθείας από την καρτέλα συνδρομών #1516
+- Η δημιουργία ουράς ενεργοποιεί τώρα την αναπαραγωγή βίντεο εάν η ουρά αναπαραγωγής έχει ήδη ολοκληρωθεί #1783
+- Ξεχωριστές ρυθμίσεις για τις χειρονομίες έντασης και φωτεινότητας #1644
+- Προσθήκη υποστήριξης για την τοπική προσαρμογή #1792
+
+### Διορθώσεις
+- Διορθώστε την ανάλυση της ώρας για τη μορφή ., ώστε το NewPipe να μπορεί να χρησιμοποιηθεί στη Φινλανδία
+- Διορθώστε την καταμέτρηση συνδρομών
+- Προσθήκη δικαιώματος υπηρεσίας foreground για συσκευές API 28+ #1830
+
+### Γνωστά σφάλματα
+- Η κατάσταση αναπαραγωγής δεν μπορεί να αποθηκευτεί στο Android P
diff --git a/fastlane/metadata/android/en-US/changelogs/1005.txt b/fastlane/metadata/android/en-US/changelogs/1005.txt
new file mode 100644
index 000000000..35a5eab31
--- /dev/null
+++ b/fastlane/metadata/android/en-US/changelogs/1005.txt
@@ -0,0 +1,17 @@
+New
+• Add support for Android Auto
+• Allow setting feed groups as main screen tabs
+• [YouTube] Share as temporary playlist
+• [SoundCloud] Likes channel tab
+
+Improved
+• Better search bar hints
+• Show download date in Downloads
+• Use Android 13 per-app language
+
+Fixed
+• Fix broken text colors in dark mode
+• [YouTube] Fix playlists not loading more than 100 items
+• [YouTube] Fix missing recommended videos
+• Fix crashes in History list view
+• Fix timestamps in comment replies
\ No newline at end of file
diff --git a/fastlane/metadata/android/en_GB/changelogs/1000.txt b/fastlane/metadata/android/en_GB/changelogs/1000.txt
new file mode 100644
index 000000000..6a884f967
--- /dev/null
+++ b/fastlane/metadata/android/en_GB/changelogs/1000.txt
@@ -0,0 +1,13 @@
+Improved
+• Make playlist description clickable to show more / less content
+• [PeerTube] Handle `subscribeto.me` instance links automatically
+• Only start playing single item in history screen
+
+Fixed
+• Fix RSS button visibility
+• Fix seekbar preview crashes
+• Fix playlisting a thumbnail-less item
+• Fix exiting the download dialog before it appears
+• Fix related items list enqueue popup
+• Fix order in add to playlist dialog
+• Adjust the playlist bookmark item layout
diff --git a/fastlane/metadata/android/en_GB/changelogs/1001.txt b/fastlane/metadata/android/en_GB/changelogs/1001.txt
new file mode 100644
index 000000000..8d74eef7b
--- /dev/null
+++ b/fastlane/metadata/android/en_GB/changelogs/1001.txt
@@ -0,0 +1,6 @@
+Improved
+• Always allow changing player notification preferences on Android 13+
+
+Fixed
+• Fix exporting database/subscriptions would not truncate an already existing file, possibly leading to a corrupted export
+• Fix player resuming from start when clicking on a timestamp
diff --git a/fastlane/metadata/android/en_GB/changelogs/1002.txt b/fastlane/metadata/android/en_GB/changelogs/1002.txt
new file mode 100644
index 000000000..f05e351e4
--- /dev/null
+++ b/fastlane/metadata/android/en_GB/changelogs/1002.txt
@@ -0,0 +1,4 @@
+Fixed YouTube not playing any stream.
+
+This release only addresses the most pressing error that prevents YouTube video details from loading.
+We are aware there are other problems, and we will soon make a separate release to solve them.
diff --git a/fastlane/metadata/android/en_GB/changelogs/1003.txt b/fastlane/metadata/android/en_GB/changelogs/1003.txt
new file mode 100644
index 000000000..211c5a55b
--- /dev/null
+++ b/fastlane/metadata/android/en_GB/changelogs/1003.txt
@@ -0,0 +1,6 @@
+This is a hotfix release that fixes YouTube errors:
+• [YouTube] Fix not loading any video information, fix HTTP 403 errors while playing videos and restore playback of some age-restricted videos
+• Fix caption sizes not being changed
+• Fix downloading info twice when opening a stream
+• [Soundcloud] Remove unplayable DRM-protected streams
+• Updated translations
diff --git a/fastlane/metadata/android/en_GB/changelogs/1004.txt b/fastlane/metadata/android/en_GB/changelogs/1004.txt
new file mode 100644
index 000000000..1d3485059
--- /dev/null
+++ b/fastlane/metadata/android/en_GB/changelogs/1004.txt
@@ -0,0 +1,3 @@
+This release fixes YouTube only providing a 360p stream.
+
+Note that the solution employed in this version is likely temporary, and in the long run the SABR video protocol needs to be implemented, but TeamNewPipe members are currently busy so any help would be greatly appreciated! https://github.com/TeamNewPipe/NewPipe/issues/12248
diff --git a/fastlane/metadata/android/en_GB/full_description.txt b/fastlane/metadata/android/en_GB/full_description.txt
new file mode 100644
index 000000000..4284b3a4a
--- /dev/null
+++ b/fastlane/metadata/android/en_GB/full_description.txt
@@ -0,0 +1 @@
+NewPipe does not use any Google framework libraries, or the YouTube API. It only parses the website in order to gain the information it needs. Therefore this app can be used on devices without Google Services installed. Also, you don't need a YouTube account to use NewPipe, and it's FLOSS.
diff --git a/fastlane/metadata/android/en_GB/short_description.txt b/fastlane/metadata/android/en_GB/short_description.txt
new file mode 100644
index 000000000..1a5d9fc28
--- /dev/null
+++ b/fastlane/metadata/android/en_GB/short_description.txt
@@ -0,0 +1 @@
+A free lightweight YouTube frontend for Android.
diff --git a/fastlane/metadata/android/et/changelogs/1005.txt b/fastlane/metadata/android/et/changelogs/1005.txt
new file mode 100644
index 000000000..685803416
--- /dev/null
+++ b/fastlane/metadata/android/et/changelogs/1005.txt
@@ -0,0 +1,17 @@
+Uus
+• Android Auto tugi
+• Meedivoogude grupid põhivaates
+• [YouTube] Jagamine ajutise esitusloendina
+• [SoundCloud] Meeldimiste kanali vahekaart
+
+Täiendatud
+• Paremad otsinguvihjed
+• Allalaadimise kuupäev vastavas vaates
+• Android 13 puhul rakendusekohane keel
+
+Parandatud
+• Tekstivärvid tumedas kujundused
+• [YouTube] Esitusloend ei laadinud üle 100 kirje
+• [YouTube] Puuduvad videosoovitused
+• Ajaloovaate kookujooksmine
+• Ajatemplid kommentaaride vastustes
diff --git a/fastlane/metadata/android/et/changelogs/64.txt b/fastlane/metadata/android/et/changelogs/64.txt
new file mode 100644
index 000000000..17290e39d
--- /dev/null
+++ b/fastlane/metadata/android/et/changelogs/64.txt
@@ -0,0 +1,8 @@
+### Täiustused
+- Lisatud võimalus piirata videokvaliteeti mobiilse andmeside kasutamisel. #1339
+- Pea seansi heledus meeles #1442
+- Parandatud allalaadimise jõudlust nõrgematel protsessoritel #1431
+- Lisatud (töötav) tugi meediaseansile #1433
+
+### Parandus
+- Parandatud allalaadimiste avamisel tekkinud krahh #1441
diff --git a/fastlane/metadata/android/fr/changelogs/1003.txt b/fastlane/metadata/android/fr/changelogs/1003.txt
index 13c76e921..161ee7fbb 100644
--- a/fastlane/metadata/android/fr/changelogs/1003.txt
+++ b/fastlane/metadata/android/fr/changelogs/1003.txt
@@ -1,6 +1,6 @@
-Il s'agit d'une version de correction qui résout les erreurs de YouTube :
-• [YouTube] Correction du non-chargement des informations des vidéos, correction des erreurs HTTP 403 lors de la lecture des vidéos et restauration de la lecture de certaines vidéos restreintes par l'âge
+Ceci est une version de correction qui résout les erreurs de YouTube :
+• [YouTube] Correction du non-chargement des informations des vidéos, correction des erreurs HTTP 403 lors de la lecture des vidéos et restauration de la lecture de certaines vidéos à âge restreint
• Correction des tailles de sous-titres qui ne changent pas
-• Correction du téléchargement des informations deux fois lors de l'ouverture d'un flux
-• [Soundcloud] Suppression des flux protégés par DRM non lisibles
+• Correction du téléchargement des informations deux fois lors de l'ouverture d'un stream
+• [Soundcloud] Suppression des streams protégés par DRM non lisibles
• Traductions mises à jour
diff --git a/fastlane/metadata/android/fr/changelogs/1005.txt b/fastlane/metadata/android/fr/changelogs/1005.txt
new file mode 100644
index 000000000..b7e5ff49f
--- /dev/null
+++ b/fastlane/metadata/android/fr/changelogs/1005.txt
@@ -0,0 +1,17 @@
+Nouveau
+• Prise en charge d'Android Auto
+• Possibilité de définir des groupes de flux comme onglets de l'écran principal
+• [YouTube] Partager comme playlist temporaire
+• [SoundCloud] Onglet « J'aime »
+
+Amélioration
+• Amélioration des astuces de la barre de recherche
+• Affichage de la date de téléchargement dans Téléchargements
+• Utilisation de la langue par application d'Android 13
+
+Corrigé
+• Correction des couleurs de texte défectueuses en mode sombre
+• [YouTube] Correction des playlists ne chargeant pas plus de 100 éléments
+• [YouTube] Correction des vidéos recommandées manquantes
+• Correction des plantages dans la vue Historique
+• Correction des horodatages dans les réponses aux commentaires
diff --git a/fastlane/metadata/android/hi/changelogs/1005.txt b/fastlane/metadata/android/hi/changelogs/1005.txt
new file mode 100644
index 000000000..495894e76
--- /dev/null
+++ b/fastlane/metadata/android/hi/changelogs/1005.txt
@@ -0,0 +1,17 @@
+नया
++ • Android Auto के लिए समर्थन जोड़ें
++ • फ़ीड समूहों को मुख्य स्क्रीन टैब के रूप में सेट करने की अनुमति दें
++ • [YouTube] अस्थायी प्लेलिस्ट के रूप में साझा करें
++ • [SoundCloud] "पसंद" चैनल टैब जोङी गई
+
+बेहतर किए
++ • खोज बार संकेत
++ • डाउनलोडस स्क्रीन में डाउनलोड की तारीख दिखाएं
++ • Android 13+ पर प्रति-ऐप भाषा का उपयोग करें
+
+फिक्स किए
++ • डार्क मोड में पाठ के रंग ठीक करें
++ • [YouTube] 100 से अधिक आइटम लोड नहीं करने वाली प्लेलिस्ट को ठीक करें
++ • [YouTube] अनुपलब्ध अनुशंसित वीडियो को ठीक करें
++ • इतिहास सूची दृश्य में क्रैश ठीक करें
++ • टिप्पणी के उत्तरों में टाइमस्टैम्प को ठीक करें
diff --git a/fastlane/metadata/android/hu/changelogs/1004.txt b/fastlane/metadata/android/hu/changelogs/1004.txt
index f4de95e68..97e36d1b0 100644
--- a/fastlane/metadata/android/hu/changelogs/1004.txt
+++ b/fastlane/metadata/android/hu/changelogs/1004.txt
@@ -1 +1,3 @@
-Immáron minden YouTube videó lejátszásra kerül
+Ez a kiadás javítja, hogy a YouTube csak 360p felbontásban képes lejátszani.
+
+Vegyük figyelembe, hogy az ebben a verzióban alkalmazott megoldás valószínűleg ideiglenes, és hosszú távon a SABR videó protokollt kell megvalósítani, de a TeamNewPipe tagjai jelenleg elfoglaltak, így minden segítséget nagyra értékelnénk! https://github.com/TeamNewPipe/NewPipe/issues/12248
diff --git a/fastlane/metadata/android/hu/changelogs/1005.txt b/fastlane/metadata/android/hu/changelogs/1005.txt
new file mode 100644
index 000000000..6d6974391
--- /dev/null
+++ b/fastlane/metadata/android/hu/changelogs/1005.txt
@@ -0,0 +1,17 @@
+Újdonság
+- Android Auto támogatás hozzáadása
+- A hírfolyamcsoportok főképernyőfülekként való beállítása
+- [YouTube] Megosztás ideiglenes lejátszási listaként
+- [SoundCloud] „Kedvelések” csatorna lap
+
+Továbbfejlesztve
+- Jobb keresősáv-súgók
+- Letöltési dátum megjelenítése a letöltések között
+- Android 13 alkalmazásonkénti nyelv használata
+
+Javítva
+- Törött szövegszínek javítása sötét módban
+- [YouTube] A lejátszási listák 100-nál több elemet nem töltöttek be.
+- [YouTube] Az ajánlott videók hiányának javítása
+- Az előzmények listanézetben bekövetkező összeomlások javítása
+- A hozzászólásválaszok időbélyegeinek javítása
diff --git a/fastlane/metadata/android/hu/full_description.txt b/fastlane/metadata/android/hu/full_description.txt
index 14b11b201..c822f8486 100644
--- a/fastlane/metadata/android/hu/full_description.txt
+++ b/fastlane/metadata/android/hu/full_description.txt
@@ -1 +1 @@
-A NewPipe nem használ semmilyen Google keretrendszer programkönyvtárat, sem YouTube API-t. Csupán a weboldalt dolgozza fel, hogy kinyerje a szükséges információkat. Így ez az alkalmazás a Google Szolgáltatások nélküli eszközökön is használható. Továbbá a NewPipe használatához nincs szükség YouTube fiókra, emellett szabad és nyílt forráskódú szoftver (FLOSS).
+A NewPipe nem használ semmilyen Google keretrendszer-programkönyvtárat, sem a YouTube API-t. Csupán a weboldalt dolgozza fel, hogy kinyerje a szükséges információkat. Így ez az alkalmazás a Google Szolgáltatások nélküli eszközökön is használható. Továbbá a NewPipe használatához nincs szükség YouTube-fiókra, emellett szabad és nyílt forráskódú szoftver (FLOSS).
diff --git a/fastlane/metadata/android/it/changelogs/1005.txt b/fastlane/metadata/android/it/changelogs/1005.txt
new file mode 100644
index 000000000..e70610077
--- /dev/null
+++ b/fastlane/metadata/android/it/changelogs/1005.txt
@@ -0,0 +1,17 @@
+Novità
+• Ora NewPipe funziona su Android Auto
+• Aggiungi feed di gruppi di canali nella schermata principale
+• [YouTube] Condividi come playlist temporanea
+• [SoundCloud] Mi piace nei canali
+
+Migliorie
+• Indizi nella barra di ricerca
+• Mostrata la data di scaricamento nella relativa sezione
+• Uso della possibilità di scegliere la lingua dell'app, funzione nativa di Android 13
+
+Correzioni
+• Sistemati i colori del testo col tema scuro
+• [YouTube] Ora si caricano più di 100 video nelle playlist
+• [YouTube] Ripristinati i video consigliati
+• Sistemati i crash nella cronologia
+* Sistemato un errore nell'orario di inserimento di una risposta nei commenti
diff --git a/fastlane/metadata/android/lv/changelogs/1001.txt b/fastlane/metadata/android/lv/changelogs/1001.txt
new file mode 100644
index 000000000..4525874fd
--- /dev/null
+++ b/fastlane/metadata/android/lv/changelogs/1001.txt
@@ -0,0 +1,6 @@
+Uzlabojumi
+• Vienmēr atļauj nomainīt atskaņotāja paziņojumu iestatījumus uz ierīcēm ar Android 13+
+
+Salabots
+• Datubāzes/abonementu izgūšana vairs neapcērp iepriekš eksistējošo datni, kas, iespējams, izraisīja bojātu datni
+• Novērsta atskaņotāja atsākšana no paša sākuma, kad noklikšķina uz laika zīmoga
diff --git a/fastlane/metadata/android/lv/changelogs/64.txt b/fastlane/metadata/android/lv/changelogs/64.txt
index 3d49ae26b..9a362aa95 100644
--- a/fastlane/metadata/android/lv/changelogs/64.txt
+++ b/fastlane/metadata/android/lv/changelogs/64.txt
@@ -1,8 +1,8 @@
### Uzlabojumi
- Pievienota iespēja ierobežot video kvalitāti, ja tiek lietoti mobilie dati. #1339
-- Iegaumē spilgtuma iestatījumu visu sesijas laiku #1442
-- Uzlabots lejupielāžu ātrums uz vājākiem procesoriem #1431
-- pievienots (working) mēdiju sesijas atbalsts #1433
+- Iegaumē spilgtuma iestatījumus visu sesijas laiku #1442
+- Uzlabots lejupielāžu ātrums uz vājākiem procesoriem #1431
+- pievienots (darbojošs) multimediju sesijas atbalsts #1433
### Salabots
-- Salabota aplikācijas nobrukšana, kad atver lejupielādes (labojums pieejams relīzes laidienos) #1441
+- Salabota lietotnes nobrukšana, kad atvēra Lejupielādes (labojums pieejams relīzes laidienos) #1441
diff --git a/fastlane/metadata/android/lv/changelogs/989.txt b/fastlane/metadata/android/lv/changelogs/989.txt
new file mode 100644
index 000000000..f680eced4
--- /dev/null
+++ b/fastlane/metadata/android/lv/changelogs/989.txt
@@ -0,0 +1,3 @@
+• [YouTube] Novērsta bezgalīgā video ielāde pie jebkuru video atskaņošanu
+• [YouTube] Novērsta dažu videoklipu lēnā ielāde (straumēšanas ierobežošanas problēma)
+• Atjaunināta jsoup bibliotēka uz 1.15.3 versiju, kas ietver drošības labojumus
diff --git a/fastlane/metadata/android/lv/changelogs/998.txt b/fastlane/metadata/android/lv/changelogs/998.txt
new file mode 100644
index 000000000..79e4f3162
--- /dev/null
+++ b/fastlane/metadata/android/lv/changelogs/998.txt
@@ -0,0 +1,4 @@
+Salabota YouTube nespēja atskaņot jebkādu straumi HTTP 403 kļūdu dēļ.
+
+Nejaušas HTTP 403 kļūdas YouTube video skatīšanās laikā vēl nav novērstas.
+Konkrētā problēma tiks atrisināta nākamajā labojumfailu laidienā, cik drīz vien iespējams.
diff --git a/fastlane/metadata/android/lv/short_description.txt b/fastlane/metadata/android/lv/short_description.txt
index d947cb8f1..231c57069 100644
--- a/fastlane/metadata/android/lv/short_description.txt
+++ b/fastlane/metadata/android/lv/short_description.txt
@@ -1 +1 @@
-Viegla, bezmaksas YouTube aplikācija priekš Android.
+Viegla, bezmaksas YouTube Android lietotne.
diff --git a/fastlane/metadata/android/pa/changelogs/1005.txt b/fastlane/metadata/android/pa/changelogs/1005.txt
new file mode 100644
index 000000000..f1492a1ac
--- /dev/null
+++ b/fastlane/metadata/android/pa/changelogs/1005.txt
@@ -0,0 +1,17 @@
+ਨਵਾਂ
++ • Android Auto ਲਈ ਸਮਰਥਨ ਸ਼ਾਮਿਲ ਕਰੋ
++ • ਫੀਡ ਗਰੁੱਪਾਂ ਨੂੰ ਮੁੱਖ ਸਕ੍ਰੀਨ ਟੈਬਾਂ ਵਜੋਂ ਸੈੱਟ ਕਰਨ ਦੀ ਆਗਿਆ ਦਿਓ
++ • [YouTube] ਅਸਥਾਈ ਪਲੇਲਿਸਟ ਦੇ ਵਜੋਂ ਸਾਂਝਾ ਕਰੋ
++ • [SoundCloud] "ਪਸੰਦ" ਚੈਨਲ ਟੈਬ ਜੋੜੀ ਗਈ
+
+ਬਿਹਤਰ ਕੀਤੇ
++ • ਖੋਜ ਬਾਰ ਸੰਕੇਤ
++ • ਡਾਊਨਲੋਡਸ ਸੂਚੀ ਵਿੱਚ ਡਾਊਨਲੋਡ ਤਾਰੀਖ ਦਿਖਾਓ
++ • ਐਂਡਰਾਇਡ 13+ 'ਤੇ ਪ੍ਰਤੀ ਐਪ ਭਾਸ਼ਾ ਦੀ ਵਰਤੋਂ ਕਰੋ
+
+ਦਰੁਸਤ ਕੀਤੇ
++ • ਡਾਰਕ ਥੀਮਾਂ 'ਤੇ ਟੈਕਸਟ ਰੰਗਾਂ ਨੂੰ ਠੀਕ ਕਰੋ
++ • [YouTube] ਪਲੇਲਿਸਟਾਂ ਨੂੰ ਠੀਕ ਕਰੋ ਜੋ 100 ਤੋਂ ਵੱਧ ਆਈਟਮਾਂ ਲੋਡ ਨਹੀਂ ਕਰਦੀਆਂ
++ • [YouTube] ਸਿਫਾਰਸ਼ ਕੀਤੀਆਂ ਵੀਡੀਓਜ਼ ਦੀ ਅਣਉਪਲੱਬਧਤਾ ਨੂੰ ਠੀਕ ਕਰੋ
++ • ਇਤਿਹਾਸ ਸੂਚੀ ਦ੍ਰਿਸ਼ ਵਿੱਚ ਕ੍ਰੈਸ਼ ਨੂੰ ਠੀਕ ਕਰੋ
++ • ਟਿੱਪਣੀਆਂ ਦੇ ਜਵਾਬ ਵਿੱਚ ਟਾਈਮਸਟੈਂਪਾਂ ਨੂੰ ਠੀਕ ਕਰੋ
diff --git a/fastlane/metadata/android/pl/changelogs/1005.txt b/fastlane/metadata/android/pl/changelogs/1005.txt
new file mode 100644
index 000000000..336aa1fa6
--- /dev/null
+++ b/fastlane/metadata/android/pl/changelogs/1005.txt
@@ -0,0 +1,17 @@
+Nowe
+- Obsł. Android Auto
+- Opcja ustawiania grup kanałów jako kart ekranu głównego
+- [YouTube] Udostęp. jako tymczasowa playlista
+- [SoundCloud] Karta kanału polubień
+
+Ulepszone
+- Lepsze podpow. paska wysz.
+- Wyśw. daty pobrania w Pobranych
+- Używanie języka aplikacji w Androidzie 13
+
+Naprawione
+- Uszkodzone kolory tekstu w trybie ciemnym
+- [YouTube] Playlisty ładujące maks. 100 pozycji
+- [YouTube] Brakujące polecane wideo
+- Awarie w widoku listy Historii
+- Znaczniki czasu w odp. na komentarze
diff --git a/fastlane/metadata/android/pt-BR/changelogs/1001.txt b/fastlane/metadata/android/pt-BR/changelogs/1001.txt
new file mode 100644
index 000000000..50dd12a29
--- /dev/null
+++ b/fastlane/metadata/android/pt-BR/changelogs/1001.txt
@@ -0,0 +1,6 @@
+Melhorado
+• Sempre permitir alterar preferências de notificação do player no 13+
+
+Corrigido
+• Corrigido exportar banco de dados/inscrições não truncaria um arquivo já existente, podendo levar a uma exportação corrompida
+• Corrigido o player retomando do início ao clicar em um tempo específico
diff --git a/fastlane/metadata/android/pt-BR/changelogs/961.txt b/fastlane/metadata/android/pt-BR/changelogs/961.txt
new file mode 100644
index 000000000..301637b3a
--- /dev/null
+++ b/fastlane/metadata/android/pt-BR/changelogs/961.txt
@@ -0,0 +1,12 @@
+• [YouTube] Suporte a mixagem
+• [YouTube] Exibição de informações sobre emissoras públicas e Covid-19
+• [media.ccc.de] Adicionado vídeos recentes
+• Adicionada tradução para Somali
+
+• Vários melhoramentos internos
+
+• Corrigido compartilhar vídeos a partir do player
+• Corrigido ReCaptcha no webview em branco
+• Corrigido travamento que ocorre ao remover um stream da lista
+• [PeerTube] Corrigidos streams relacionados
+• [YouTube] Corrigida a pesquisa do YouTube Music
diff --git a/fastlane/metadata/android/pt-BR/changelogs/964.txt b/fastlane/metadata/android/pt-BR/changelogs/964.txt
new file mode 100644
index 000000000..f27426074
--- /dev/null
+++ b/fastlane/metadata/android/pt-BR/changelogs/964.txt
@@ -0,0 +1,8 @@
+• Suporte para capítulos adicionado nos controles do player
+• [PeerTube] Busca Sépia adicionada
+• Botão de compartilhamento adicionado novamente na visualização de detalhes do vídeo e descrição da transmissão movida para o layout de abas
+• Desativação da restauração do brilho se o gesto de brilho estiver desativado
+• Item de lista adicionado para reproduzir vídeo no Kodi
+• Travamento corrigido quando nenhum navegador padrão está definido em alguns dispositivos e diálogos de compartilhamento aprimorados
+• Alternar reprodução/pausa com o botão de espaço físico no player em tela cheia
+• [media.ccc.de] Diversas correções e melhorias
diff --git a/fastlane/metadata/android/pt-BR/changelogs/965.txt b/fastlane/metadata/android/pt-BR/changelogs/965.txt
new file mode 100644
index 000000000..9862a1a4f
--- /dev/null
+++ b/fastlane/metadata/android/pt-BR/changelogs/965.txt
@@ -0,0 +1,6 @@
+Corrigida a falha que ocorria ao reordenar grupos de canais.
+Corrigida a obtenção de mais vídeos do YouTube de canais e playlists.
+Corrigida a obtenção de comentários do YouTube.
+Adicionado suporte para subcaminhos /watch/, /v/ e /w/ em URLs do YouTube.
+Corrigida a extração do ID do cliente do SoundCloud e de conteúdo com restrição geográfica.
+Adicionada a localização para o Curdo do Norte.
diff --git a/fastlane/metadata/android/pt-PT/changelogs/951.txt b/fastlane/metadata/android/pt-PT/changelogs/951.txt
index c391aa519..d48369bac 100644
--- a/fastlane/metadata/android/pt-PT/changelogs/951.txt
+++ b/fastlane/metadata/android/pt-PT/changelogs/951.txt
@@ -13,3 +13,5 @@ Melhorado
Fixo
• Desync de áudio/vídeo
• [PeerTube] Gerir comentários apagados
+
+...
diff --git a/fastlane/metadata/android/ru/changelogs/1000.txt b/fastlane/metadata/android/ru/changelogs/1000.txt
index 735794877..90c03f3cf 100644
--- a/fastlane/metadata/android/ru/changelogs/1000.txt
+++ b/fastlane/metadata/android/ru/changelogs/1000.txt
@@ -1,24 +1,13 @@
-Улучшенный
+Улучшения
+• Сделать описание плейлиста кликабельным, чтобы показать больше/меньше контента.
+• [PeerTube] Автоматически обрабатывать ссылки экземпляра `subscribeto.me`.
+• Начинать воспроизведение только одного элемента на экране истории.
-- Сделайте описание плейлиста кликабельным, чтобы показать больше/меньше контента
-
-- [PeerTube] Автоматически обрабатывать ссылки экземпляра `subscribeto.me
-
-- Запуск воспроизведения только одного элемента на экране истории
-
-
-Исправлено
-
-- Исправление видимости кнопки RSS
-
-- Исправление сбоев предварительного просмотра панели поиска
-
-- Исправление создания плейлиста без миниатюр
-
-- Исправление выхода из диалога загрузки до его появления
-
-- Исправить всплывающее окно списка связанных элементов
-
-- Исправить порядок в диалоге добавления в плейлист
-
-- Настроить расположение элементов закладок плейлиста
+Исправления
+• Исправить видимость кнопки RSS.
+• Исправить сбои при предварительном просмотре полосы прокрутки.
+• Исправить добавление в плейлист элемента без миниатюры.
+• Исправить выход из диалогового окна загрузки до его появления.
+• Исправить всплывающее окно с списком связанных элементов.
+• Исправлена последовательность в диалоговом окне добавления в плейлист.
+• Настроена компоновка элементов закладок плейлиста.
diff --git a/fastlane/metadata/android/ru/changelogs/1005.txt b/fastlane/metadata/android/ru/changelogs/1005.txt
new file mode 100644
index 000000000..3dead0f05
--- /dev/null
+++ b/fastlane/metadata/android/ru/changelogs/1005.txt
@@ -0,0 +1,17 @@
+Новое
+• Поддержка Android Auto
+• Использование групп лент как вкладок главного экрана
+• [YouTube] Возможность поделиться временным плейлистом
+• [SoundCloud] Вкладка канала "Лайки"
+
+Улучшено
+• Поисковые подсказки
+• Показ даты загрузки в Загрузках
+• Установка языка для каждого приложения
+
+Исправлено
+• Цвета текста в тёмном режиме
+• [YouTube] Плейлисты не загружали более чем 100 элементов
+• [YouTube] Отсутствующие рекомендованные видео
+• Вылеты в Истории
+• Временные метки в ответах на комментарии
diff --git a/fastlane/metadata/android/sk/changelogs/1000.txt b/fastlane/metadata/android/sk/changelogs/1000.txt
index 61cabd89d..29e2182f4 100644
--- a/fastlane/metadata/android/sk/changelogs/1000.txt
+++ b/fastlane/metadata/android/sk/changelogs/1000.txt
@@ -1,13 +1,13 @@
Vylepšené
-- Umožnené kliknutie na popis playlistu, aby sa zobrazilo viac/menej obsahu
-- [PeerTube] Automatické spracovanie odkazov inštancie `subscribeto.me`
-- Spustenie prehrávania iba jednej položky v histórii
+- Kliknutím na playlist sa zobrazí viac/menej obsahu
+- [PeerTube] Automatické spracovanie odkazov z `subscribeto.me`
+- Spustiť prehrávanie iba jednej položky v histórii
Opravené
-- Oprava viditeľnosti tlačidla RSS
-- Oprava pádov náhľadu na paneli vyhľadávania
-- Oprava pridania položky bez miniatúry do playlistu
-- Oprava ukončenia okna sťahovania pred jeho zobrazením
-- Oprava vyskakovacieho okna zoznamu súvisiacich položiek
-- Oprava poradia v okne pridania do playlistu
-- Úprava rozloženia položiek záložiek playlistu
+- Viditeľnosť tlačidla RSS
+- Pády pri náhľadoch
+- Pridanie položky bez miniatúry do playlistu
+- Zatváranie okna sťahovania pred jeho zobrazením
+- Vyskakovacie okno zoznamu súvisiacich položiek
+- Poradie v okne pridania do playlistu
+- Rozloženie položiek záložiek playlistu
diff --git a/fastlane/metadata/android/sk/changelogs/1005.txt b/fastlane/metadata/android/sk/changelogs/1005.txt
new file mode 100644
index 000000000..8f2bfbfab
--- /dev/null
+++ b/fastlane/metadata/android/sk/changelogs/1005.txt
@@ -0,0 +1,18 @@
+Novinky
+• Pridaná podpora pre Android Auto
+• Možnosť nastaviť skupiny kanálov ako hlavné karty na obrazovke
+• [YouTube] Zdieľanie ako dočasný playlist
+
+• [SoundCloud] Karta „Páči sa“ kanál.
+
+Vylepšenia
+• Lepšia nápoveda v paneli vyhľadávania
+• Zobrazenie dátumu stiahnutia v sekcii „Stiahnuté”
+• Použitie jazyka Android 13 pre jednotlivé aplikácie
+
+Opravy
+• Oprava chybných farieb textu v tmavom režime
+• [YouTube] Oprava playlistov, ktoré nenačítavajú viac ako 100 položiek
+• [YouTube] Oprava chýbajúcich odporúčaných videí
+• Oprava pádov v zobrazení zoznamu histórie
+• Oprava časových značiek v odpovediach na komentáre.
diff --git a/fastlane/metadata/android/sk/full_description.txt b/fastlane/metadata/android/sk/full_description.txt
index 9de86c677..e1483d4fd 100644
--- a/fastlane/metadata/android/sk/full_description.txt
+++ b/fastlane/metadata/android/sk/full_description.txt
@@ -1 +1 @@
-NewPipe nepoužíva žiadne Google framework knižnice, ani YouTube API rozhranie. Len analyzuje web, aby získal potrebné informácie. Preto je možné túto aplikáciu používať na zariadeniach bez nainštalovaných Google služieb. Taktiež nepotrebujete účet na YouTube. Appka je FLOSS.
+NewPipe nepoužíva Google framework knižnice, ani YouTube API rozhranie. Iba spracováva YouTube stránku aby získal potrebné informácie. Túto aplikáciu je teda možné používať na zariadeniach bez nainštalovaných Google služieb. Taktiež nepotrebujete účet na YouTube. Aplikácia je FLOSS.
diff --git a/fastlane/metadata/android/sr/full_description.txt b/fastlane/metadata/android/sr/full_description.txt
index e488a0ea4..42f1ef8f2 100644
--- a/fastlane/metadata/android/sr/full_description.txt
+++ b/fastlane/metadata/android/sr/full_description.txt
@@ -1 +1 @@
-NewPipe не користи никакве библиотеке Google оквира, нити YouTube API. Само анализира веб-сајт како би добио потребне информације. Из тог разлога, ова апликација се може користити на уређајима без инсталираних Google услуга. Такође, није Вам потребан YouTube налог да бисте користили NewPipe, a чак је и слободног, отвореног кода.
+Њупипе не користи никакве књижнице радног окврира Гугла, нити АПИ Јутуба. Само рашчлањује мрежне странице како би добио потребне податке. Из тог разлога, овај програм се може користити на уређајима без уграђених услуга Гугла. Такође, није Вам потребан налог Јутуба ради коришћења Њупипе, a чак је и слободна мекотворина.
diff --git a/fastlane/metadata/android/sr/short_description.txt b/fastlane/metadata/android/sr/short_description.txt
index 82608e9db..4e963ee94 100644
--- a/fastlane/metadata/android/sr/short_description.txt
+++ b/fastlane/metadata/android/sr/short_description.txt
@@ -1 +1 @@
-Бесплатна и лагана замена за YouTube за Android.
+Слободно и лако сучеље Јутуба за Андроид.
diff --git a/fastlane/metadata/android/sv/changelogs/1004.txt b/fastlane/metadata/android/sv/changelogs/1004.txt
index 35f298dbf..a9a551885 100644
--- a/fastlane/metadata/android/sv/changelogs/1004.txt
+++ b/fastlane/metadata/android/sv/changelogs/1004.txt
@@ -1 +1,3 @@
-Åtgärdat att YouTube inte spelar någon stream
+Denna version fixar att enbart 360p strömmar var tillgängliga för YouTube.
+
+Observera att den lösning som används i den här versionen är sannolikt tillfällig, och i det långa loppet måste SABR-videoprotokollet implementeras, men TeamNewPipe-medlemmarna är för närvarande upptagna så all hjälp uppskattas! https://github.com/TeamNewPipe/NewPipe/issues/12248
diff --git a/fastlane/metadata/android/ta/changelogs/1000.txt b/fastlane/metadata/android/ta/changelogs/1000.txt
index 2e7774ce0..43842685f 100644
--- a/fastlane/metadata/android/ta/changelogs/1000.txt
+++ b/fastlane/metadata/android/ta/changelogs/1000.txt
@@ -1,13 +1,13 @@
மேம்படுத்தப்பட்டது
- / மேலும் / குறைவான உள்ளடக்கத்தைக் காட்ட பிளேலிச்ட் விளக்கத்தை சொடுக்கு செய்யவும்
- • [PEERTUBE] `charbisto.me` உதாரணமாக இணைப்புகளை தானாகவே கையாளவும்
+ / மேலும் / குறைவான உள்ளடக்கத்தைக் காட்ட பிளேலிச்ட் விளக்கத்தைச் சொடுக்கு செய்யவும்
+ • [PEERTUBE] `charbisto.me` உதாரணமாக இணைப்புகளைத் தானாகவே கையாளவும்
The வரலாற்றுத் திரையில் ஒற்றை உருப்படியை மட்டுமே இயக்கத் தொடங்குங்கள்
சரி
RS RSS பொத்தான் தெரிவுநிலையை சரிசெய்யவும்
See gaekbar முன்னோட்ட செயலிழப்புகளை சரிசெய்யவும்
- Play ஒரு சிறுபடம் இல்லாத உருப்படியை பிளேலிச்டிங் சரிசெய்யவும்
+ Play ஒரு சிறுபடம் இல்லாத உருப்படியைப் பிளேலிச்டிங் சரிசெய்யவும்
The பதிவிறக்க உரையாடல் தோன்றுவதற்கு முன்பு வெளியேறுவதை சரிசெய்யவும்
- Tied தொடர்புடைய உருப்படிகளை சரிசெய்யவும் பட்டியல் enqueue பாப்அப்
+ Tied தொடர்புடைய உருப்படிகளைச் சரிசெய்யவும் பட்டியல் enqueue பாப்அப்
Plale பிளேலிச்ட் உரையாடலில் கூட்டு இல் ஆர்டரை சரிசெய்யவும்
Plale பிளேலிச்ட் புத்தகக்குறி உருப்படி தளவமைப்பை சரிசெய்யவும்
diff --git a/fastlane/metadata/android/ta/changelogs/1002.txt b/fastlane/metadata/android/ta/changelogs/1002.txt
new file mode 100644
index 000000000..8e2bb3642
--- /dev/null
+++ b/fastlane/metadata/android/ta/changelogs/1002.txt
@@ -0,0 +1,4 @@
+நிலையான YouTube எந்த ச்ட்ரீமையும் இயக்கவில்லை.
+
+இந்த வெளியீடு யூடியூப் வீடியோ விவரங்களை ஏற்றுவதைத் தடுக்கும் மிக அழுத்தமான பிழையை மட்டுமே குறிக்கிறது.
+வேறு சிக்கல்கள் இருப்பதை நாங்கள் அறிவோம், அவற்றை தீர்க்க விரைவில் ஒரு தனி வெளியீட்டை உருவாக்குவோம்.
diff --git a/fastlane/metadata/android/ta/changelogs/1003.txt b/fastlane/metadata/android/ta/changelogs/1003.txt
new file mode 100644
index 000000000..95bc6e7b4
--- /dev/null
+++ b/fastlane/metadata/android/ta/changelogs/1003.txt
@@ -0,0 +1,6 @@
+இது YouTube பிழைகளை சரிசெய்யும் ஆட்ஃபிக்ச் வெளியீடு:
+• [YouTube] எந்த வீடியோ தகவலையும் ஏற்றாததை சரிசெய்யவும், வீடியோக்களை இயக்கும்போது HTTP 403 பிழைகளை சரிசெய்யவும், சில வயதுக்கு தடைசெய்யப்பட்ட வீடியோக்களின் பின்னணியை மீட்டெடுக்கவும்
+• தலைப்பு அளவுகள் மாற்றப்படவில்லை
+Stramp ஒரு ச்ட்ரீமை திறக்கும்போது தகவலைப் பதிவிறக்குவதை இரண்டு முறை சரிசெய்யவும்
+• [சவுண்ட்க்ளூட்] விளையாட முடியாத டிஆர்எம்-பாதுகாக்கப்பட்ட நீரோடைகளை அகற்று
+• புதுப்பிக்கப்பட்ட மொழிபெயர்ப்புகள்
diff --git a/fastlane/metadata/android/ti/full_description.txt b/fastlane/metadata/android/ti/full_description.txt
index f0afa90ab..f58ede5da 100644
--- a/fastlane/metadata/android/ti/full_description.txt
+++ b/fastlane/metadata/android/ti/full_description.txt
@@ -1 +1 @@
-ኒውፓይፕ ዝዀነ ይኹን ናይ ጎልጋል ቤተ-መጻሕፍቲ ወይ ናይ ዩቱብ ኤፒኢ ኣይጥቀምን ኢዩ። ነቲ ወብ ሳይት ዜድልዮ ሓበሬታ ንምርካብ ጥራይ እዩ ዚምርምሮ ። ስለዚ እዚ ኣፕሊኬሽን እዚ ብዘይ ናይ ጎልጋል ሰርቪስ ኣብ ኤለክትሮኒካዊ መሳርሒታት ክትጥቀመሉ ትኽእል ኢኻ ። ኒውፓይፕ ንምጥቃም እውን ናይ ዩቱብ ሕሳብ ኣየድልየካን ኢዩ እዚ ኸኣ FLOSS ኢዩ።
+ኒውፓይፕ ዝዀነ ናይ ጉግል ቤተ-ምስሊታት ወይ ናይ ዩቱብ ኤፒኢይ ኣይጥቀምን ኢዩ። ነቲ መርበብ- ስፍራ ዜድልዮ ሓበሬታ ንምርካብ ጥራይ እዩ ዚምርምሮ። ስለዚ እዚ ኣፕሊኬሽን እዚ ብዘይ ናይ ጉግል ኣገልግሎት ኣብ ኤለክትሮኒካዊ-መሳርሒታት ክትጥቀመሉ ትኽእል ኢኻ። ኒውፓይፕ ንምጥቃም ናይ ዩቱብ ሕሳብ ኣየድልየካን ኢዩ፣ ኒውፓይፕ FLOSS ኢዩ።
diff --git a/fastlane/metadata/android/ti/short_description.txt b/fastlane/metadata/android/ti/short_description.txt
index f7f2099a1..107cabea0 100644
--- a/fastlane/metadata/android/ti/short_description.txt
+++ b/fastlane/metadata/android/ti/short_description.txt
@@ -1 +1 @@
-ብናጻ ፈኲስ ናይ ዩቱብ ግንባር ንኣንድሮይድ ።
+ነጻ ናይ ዩቱብ ግንባር ንኣንድሮይድ።
diff --git a/fastlane/metadata/android/uk/changelogs/1002.txt b/fastlane/metadata/android/uk/changelogs/1002.txt
index a90cfff6b..f52bf43af 100644
--- a/fastlane/metadata/android/uk/changelogs/1002.txt
+++ b/fastlane/metadata/android/uk/changelogs/1002.txt
@@ -1 +1,4 @@
-Виправлено проблему невідтворюваності трансляцій YouTube
+Виправлено помилку, через яку YouTube не відтворював жодної трансляції.
+
+У цьому випуску вирішено лише найактуальнішу помилку, яка перешкоджала завантаженню деталей відео YouTube.
+Ми знаємо про інші проблеми, і незабаром випустимо окремий випуск для їх вирішення.
diff --git a/fastlane/metadata/android/uk/changelogs/1003.txt b/fastlane/metadata/android/uk/changelogs/1003.txt
index a90cfff6b..b969e091c 100644
--- a/fastlane/metadata/android/uk/changelogs/1003.txt
+++ b/fastlane/metadata/android/uk/changelogs/1003.txt
@@ -1 +1,6 @@
-Виправлено проблему невідтворюваності трансляцій YouTube
+Це виправлення, яке виправляє помилки YouTube:
+• [YouTube] Виправлено не завантаження інформації про відео, виправлено помилки HTTP 403 під час відтворення відео та відновлено відтворення деяких відео з віковими обмеженнями
+• Виправлено незмінні розміри субтитрів
+• Виправлено подвійне завантаження інформації під час відкриття потоку
+• [Soundcloud] Видалено невідтворювані потоки, захищені DRM
+• Оновлені переклади
diff --git a/fastlane/metadata/android/uk/changelogs/1004.txt b/fastlane/metadata/android/uk/changelogs/1004.txt
index a90cfff6b..1c2ec5d7f 100644
--- a/fastlane/metadata/android/uk/changelogs/1004.txt
+++ b/fastlane/metadata/android/uk/changelogs/1004.txt
@@ -1 +1,3 @@
-Виправлено проблему невідтворюваності трансляцій YouTube
+У цьому випуску виправлено помилку, через яку YouTube відтворював лише 360p-потік.
+
+Зверніть увагу, що рішення, що використовується в цій версії, ймовірно, тимчасове, і в довгостроковій перспективі потрібно буде впровадити відеопротокол SABR, але учасники TeamNewPipe зараз зайняті, тому будь-яка допомога буде дуже вдячна! https://github.com/TeamNewPipe/NewPipe/issues/12248
diff --git a/fastlane/metadata/android/uk/changelogs/1005.txt b/fastlane/metadata/android/uk/changelogs/1005.txt
new file mode 100644
index 000000000..474127f95
--- /dev/null
+++ b/fastlane/metadata/android/uk/changelogs/1005.txt
@@ -0,0 +1,17 @@
+Нове
+• Додано підтримку Android Auto
+• Дозволено налаштування груп стрічок як вкладок головного екрана
+• [YouTube] Поділитися як тимчасовий список відтворення
+• [SoundCloud] Вкладка каналу «Вподобання»
+
+Покращено
+• Кращі підказки в рядку пошуку
+• Показувати дату завантаження в розділі «Завантаження»
+• Використовувати мову Android 13 для кожної програми
+
+Виправлено
+• Виправлено пошкоджені кольори тексту в темному режимі
+• [YouTube] Виправлено списки відтворення, які не завантажували більше 100 елементів
+• [YouTube] Виправлено відсутні рекомендовані відео
+• Виправлено збої в перегляді історії
+• Виправлено позначки часу у відповідях на коментарі
diff --git a/fastlane/metadata/android/zh-Hans/changelogs/1001.txt b/fastlane/metadata/android/zh-Hans/changelogs/1001.txt
new file mode 100644
index 000000000..cdebebf55
--- /dev/null
+++ b/fastlane/metadata/android/zh-Hans/changelogs/1001.txt
@@ -0,0 +1,6 @@
+改进
+- 始终允许在 Android 13以上系统上更改播放器通知首选项
+
+修复
+- 修复了导出数据库/订阅时不会截断已存在的文件,从而可能导致导出损坏的问题
+- 修复了点击时间戳时,播放器从头开始恢复的问题
diff --git a/fastlane/metadata/android/zh-Hant/changelogs/1001.txt b/fastlane/metadata/android/zh-Hant/changelogs/1001.txt
new file mode 100644
index 000000000..033b9460e
--- /dev/null
+++ b/fastlane/metadata/android/zh-Hant/changelogs/1001.txt
@@ -0,0 +1,6 @@
+改進
+• 一直允許在Android 13以上的裝置更改通知欄播放器
+
+修正
+• 修復匯出資料庫/訂閱不會截斷已存在的文件,可能導致匯出損壞的問題
+• 修正點擊時間戳記時播放器從頭開始撥放的問題
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 8cb637cac..103a80284 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -3,7 +3,7 @@ aboutLibraries = "11.2.3"
acraCore = "5.11.3"
androidState = "1.4.1"
androidx-junit = "1.1.5"
-appcompat = "1.6.1"
+appcompat = "1.7.1"
assertjCore = "3.24.2"
auto-service = "1.1.1"
bridge = "2.0.2"
@@ -53,12 +53,12 @@ swiperefreshlayout = "1.1.0"
# name and the commit hash with the commit hash of the (pushed) commit you want to test
# This works thanks to JitPack: https://jitpack.io/
teamnewpipe-filepicker = "5.0.0"
-teamnewpipe-nanojson = "1d9e1aea9049fc9f85e68b43ba39fe7be1c1f751"
+teamnewpipe-nanojson = "e9d656ddb49a412a5a0a5d5ef20ca7ef09549996"
# WORKAROUND: if you get errors with the NewPipeExtractor dependency, replace `v0.24.3` with
# the corresponding commit hash, since JitPack sometimes deletes artifacts.
# If there’s already a git hash, just add more of it to the end (or remove a letter)
# to cause jitpack to regenerate the artifact.
-teamnewpipe-newpipe-extractor = "v0.24.6"
+teamnewpipe-newpipe-extractor = "v0.24.8"
webkit = "1.9.0"
work = "2.10.0"