SponsorBlock: Merge branch 'dev' into sponsorblock

# Conflicts:
#	app/src/main/java/org/schabi/newpipe/player/VideoPlayerImpl.java
#	app/src/main/res/layout/player.xml
#	app/src/main/res/values/strings.xml
This commit is contained in:
polymorphicshade 2020-09-27 11:46:35 -06:00
commit 5ed80f425b
148 changed files with 5188 additions and 1973 deletions

View file

@ -33,7 +33,7 @@ android {
// suffix the app id and the app name with git branch name
def workingBranch = getGitWorkingBranch()
def normalizedWorkingBranch = workingBranch.replaceAll("[^A-Za-z]+", "").toLowerCase()
def normalizedWorkingBranch = workingBranch.replaceFirst("^[^A-Za-z]+", "").replaceAll("[^0-9A-Za-z]+", "")
if (normalizedWorkingBranch.isEmpty() || workingBranch == "master" || workingBranch == "dev") {
// default values when branch name could not be determined or is master or dev
applicationIdSuffix ".debug"

View file

@ -44,8 +44,9 @@
</receiver>
<service
android:name=".player.MainPlayer"
android:exported="false">
android:name=".player.MainPlayer"
android:exported="false"
android:foregroundServiceType="mediaPlayback">
<intent-filter>
<action android:name="android.intent.action.MEDIA_BUTTON" />
</intent-filter>

View file

@ -39,6 +39,7 @@ import org.schabi.newpipe.extractor.exceptions.ExtractionException;
import org.schabi.newpipe.extractor.playlist.PlaylistInfo;
import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.extractor.stream.VideoStream;
import org.schabi.newpipe.player.helper.PlayerHelper;
import org.schabi.newpipe.player.playqueue.ChannelPlayQueue;
import org.schabi.newpipe.player.playqueue.PlayQueue;
import org.schabi.newpipe.player.playqueue.PlaylistPlayQueue;
@ -268,7 +269,7 @@ public class RouterActivity extends AppCompatActivity {
final LayoutInflater inflater = LayoutInflater.from(themeWrapperContext);
final LinearLayout rootLayout = (LinearLayout) inflater.inflate(
R.layout.preferred_player_dialog_view, null, false);
R.layout.single_choice_dialog_view, null, false);
final RadioGroup radioGroup = rootLayout.findViewById(android.R.id.list);
final DialogInterface.OnClickListener dialogButtonsClickListener = (dialog, which) -> {
@ -278,6 +279,7 @@ public class RouterActivity extends AppCompatActivity {
handleChoice(choice.key);
// open future streams always like this one, because "always" button was used by user
if (which == DialogInterface.BUTTON_POSITIVE) {
preferences.edit()
.putString(getString(R.string.preferred_open_action_key), choice.key)
@ -377,23 +379,50 @@ public class RouterActivity extends AppCompatActivity {
final boolean isExtAudioEnabled = preferences.getBoolean(
getString(R.string.use_external_audio_player_key), false);
returnList.add(new AdapterChoiceItem(getString(R.string.show_info_key),
getString(R.string.show_info),
resolveResourceIdFromAttr(context, R.attr.ic_info_outline)));
final AdapterChoiceItem videoPlayer = new AdapterChoiceItem(
getString(R.string.video_player_key), getString(R.string.video_player),
resolveResourceIdFromAttr(context, R.attr.ic_play_arrow));
final AdapterChoiceItem showInfo = new AdapterChoiceItem(
getString(R.string.show_info_key), getString(R.string.show_info),
resolveResourceIdFromAttr(context, R.attr.ic_info_outline));
final AdapterChoiceItem popupPlayer = new AdapterChoiceItem(
getString(R.string.popup_player_key), getString(R.string.popup_player),
resolveResourceIdFromAttr(context, R.attr.ic_popup));
final AdapterChoiceItem backgroundPlayer = new AdapterChoiceItem(
getString(R.string.background_player_key), getString(R.string.background_player),
resolveResourceIdFromAttr(context, R.attr.ic_headset));
if (capabilities.contains(VIDEO) && !(isExtVideoEnabled && linkType != LinkType.STREAM)) {
returnList.add(new AdapterChoiceItem(getString(R.string.video_player_key),
getString(R.string.video_player),
resolveResourceIdFromAttr(context, R.attr.ic_play_arrow)));
returnList.add(new AdapterChoiceItem(getString(R.string.popup_player_key),
getString(R.string.popup_player),
resolveResourceIdFromAttr(context, R.attr.ic_popup)));
}
if (linkType == LinkType.STREAM) {
if (isExtVideoEnabled) {
// show both "show info" and "video player", they are two different activities
returnList.add(showInfo);
returnList.add(videoPlayer);
} else if (capabilities.contains(VIDEO)
&& PlayerHelper.isAutoplayAllowedByUser(context)) {
// show only "video player" since the details activity will be opened and the video
// will be autoplayed there and "show info" would do the exact same thing
returnList.add(videoPlayer);
} else {
// show only "show info" if video player is not applicable or autoplay is disabled
returnList.add(showInfo);
}
if (capabilities.contains(AUDIO) && !(isExtAudioEnabled && linkType != LinkType.STREAM)) {
returnList.add(new AdapterChoiceItem(getString(R.string.background_player_key),
getString(R.string.background_player),
resolveResourceIdFromAttr(context, R.attr.ic_headset)));
if (capabilities.contains(VIDEO)) {
returnList.add(popupPlayer);
}
if (capabilities.contains(AUDIO)) {
returnList.add(backgroundPlayer);
}
} else {
returnList.add(showInfo);
if (capabilities.contains(VIDEO) && !isExtVideoEnabled) {
returnList.add(videoPlayer);
returnList.add(popupPlayer);
}
if (capabilities.contains(AUDIO) && !isExtAudioEnabled) {
returnList.add(backgroundPlayer);
}
}
returnList.add(new AdapterChoiceItem(getString(R.string.download_key),

View file

@ -11,6 +11,7 @@ import android.content.ServiceConnection;
import android.content.SharedPreferences;
import android.content.pm.ActivityInfo;
import android.database.ContentObserver;
import android.graphics.Color;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.os.Bundle;
@ -102,13 +103,12 @@ import org.schabi.newpipe.util.ListHelper;
import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.PermissionHelper;
import org.schabi.newpipe.util.SerializedCache;
import org.schabi.newpipe.util.ShareUtils;
import org.schabi.newpipe.util.ThemeHelper;
import org.schabi.newpipe.views.AnimatedProgressBar;
import org.schabi.newpipe.views.LargeTextMovementMethod;
import java.io.Serializable;
import java.util.Collection;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
@ -125,6 +125,7 @@ import io.reactivex.schedulers.Schedulers;
import static org.schabi.newpipe.extractor.StreamingService.ServiceInfo.MediaCapability.COMMENTS;
import static org.schabi.newpipe.extractor.stream.StreamExtractor.NO_AGE_LIMIT;
import static org.schabi.newpipe.player.helper.PlayerHelper.globalScreenOrientationLocked;
import static org.schabi.newpipe.player.helper.PlayerHelper.isClearingQueueConfirmationRequired;
import static org.schabi.newpipe.player.playqueue.PlayQueueItem.RECOVERY_UNSET;
import static org.schabi.newpipe.util.AnimationUtils.animateView;
@ -337,7 +338,7 @@ public class VideoDetailFragment
stopPlayerListener();
playerService = null;
player = null;
saveCurrentAndRestoreDefaultBrightness();
restoreDefaultBrightness();
}
}
@ -404,7 +405,7 @@ public class VideoDetailFragment
settingsContentObserver = new ContentObserver(new Handler()) {
@Override
public void onChange(final boolean selfChange) {
if (activity != null && !PlayerHelper.globalScreenOrientationLocked(activity)) {
if (activity != null && !globalScreenOrientationLocked(activity)) {
activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED);
}
}
@ -426,7 +427,7 @@ public class VideoDetailFragment
if (currentWorker != null) {
currentWorker.dispose();
}
saveCurrentAndRestoreDefaultBrightness();
restoreDefaultBrightness();
PreferenceManager.getDefaultSharedPreferences(requireContext())
.edit()
.putString(getString(R.string.stream_info_selected_tab_key),
@ -538,31 +539,51 @@ public class VideoDetailFragment
super.onSaveInstanceState(outState);
if (!isLoading.get() && currentInfo != null && isVisible()) {
outState.putSerializable(INFO_KEY, currentInfo);
final String infoCacheKey = SerializedCache.getInstance()
.put(currentInfo, StreamInfo.class);
if (infoCacheKey != null) {
outState.putString(INFO_KEY, infoCacheKey);
}
}
if (playQueue != null) {
outState.putSerializable(VideoPlayer.PLAY_QUEUE_KEY, playQueue);
final String queueCacheKey = SerializedCache.getInstance()
.put(playQueue, PlayQueue.class);
if (queueCacheKey != null) {
outState.putString(VideoPlayer.PLAY_QUEUE_KEY, queueCacheKey);
}
}
final String stackCacheKey = SerializedCache.getInstance().put(stack, LinkedList.class);
if (stackCacheKey != null) {
outState.putString(STACK_KEY, stackCacheKey);
}
outState.putSerializable(STACK_KEY, stack);
}
@Override
protected void onRestoreInstanceState(@NonNull final Bundle savedState) {
super.onRestoreInstanceState(savedState);
Serializable serializable = savedState.getSerializable(INFO_KEY);
if (serializable instanceof StreamInfo) {
currentInfo = (StreamInfo) serializable;
InfoCache.getInstance().putInfo(serviceId, url, currentInfo, InfoItem.InfoType.STREAM);
final String infoCacheKey = savedState.getString(INFO_KEY);
if (infoCacheKey != null) {
currentInfo = SerializedCache.getInstance().take(infoCacheKey, StreamInfo.class);
if (currentInfo != null) {
InfoCache.getInstance()
.putInfo(serviceId, url, currentInfo, InfoItem.InfoType.STREAM);
}
}
serializable = savedState.getSerializable(STACK_KEY);
if (serializable instanceof Collection) {
//noinspection unchecked
stack.addAll((Collection<? extends StackItem>) serializable);
final String stackCacheKey = savedState.getString(STACK_KEY);
if (stackCacheKey != null) {
final LinkedList<StackItem> cachedStack =
SerializedCache.getInstance().take(stackCacheKey, LinkedList.class);
if (cachedStack != null) {
stack.addAll(cachedStack);
}
}
final String queueCacheKey = savedState.getString(VideoPlayer.PLAY_QUEUE_KEY);
if (queueCacheKey != null) {
playQueue = SerializedCache.getInstance().take(queueCacheKey, PlayQueue.class);
}
playQueue = (PlayQueue) savedState.getSerializable(VideoPlayer.PLAY_QUEUE_KEY);
}
/*//////////////////////////////////////////////////////////////////////////
@ -958,6 +979,9 @@ public class VideoDetailFragment
return;
}
setInitialData(sid, videoUrl, title, queue);
if (player != null) {
player.disablePreloadingOfCurrentTrack();
}
startLoading(false, true);
}
@ -1233,7 +1257,7 @@ public class VideoDetailFragment
}
private boolean isExternalPlayerEnabled() {
return PreferenceManager.getDefaultSharedPreferences(getContext())
return PreferenceManager.getDefaultSharedPreferences(requireContext())
.getBoolean(getString(R.string.use_external_video_player_key), false);
}
@ -1244,23 +1268,7 @@ public class VideoDetailFragment
&& !isExternalPlayerEnabled()
&& (player == null || player.videoPlayerSelected())
&& bottomSheetState != BottomSheetBehavior.STATE_HIDDEN
&& isAutoplayAllowedByUser();
}
private boolean isAutoplayAllowedByUser() {
if (activity == null) {
return false;
}
switch (PlayerHelper.getAutoplayType(activity)) {
case PlayerHelper.AutoplayType.AUTOPLAY_TYPE_NEVER:
return false;
case PlayerHelper.AutoplayType.AUTOPLAY_TYPE_WIFI:
return !ListHelper.isMeteredNetwork(activity);
case PlayerHelper.AutoplayType.AUTOPLAY_TYPE_ALWAYS:
default:
return true;
}
&& PlayerHelper.isAutoplayAllowedByUser(requireContext());
}
private void addVideoPlayerView() {
@ -1821,9 +1829,6 @@ public class VideoDetailFragment
setOverlayPlayPauseImage();
switch (state) {
case BasePlayer.STATE_COMPLETED:
restoreDefaultOrientation();
break;
case BasePlayer.STATE_PLAYING:
if (positionView.getAlpha() != 1.0f
&& player.getPlayQueue() != null
@ -1882,10 +1887,11 @@ public class VideoDetailFragment
public void onPlayerError(final ExoPlaybackException error) {
if (error.type == ExoPlaybackException.TYPE_SOURCE
|| error.type == ExoPlaybackException.TYPE_UNEXPECTED) {
hideMainPlayer();
// Properly exit from fullscreen
if (playerService != null && player.isFullscreen()) {
player.toggleFullscreen();
}
hideMainPlayer();
}
}
@ -1924,7 +1930,13 @@ public class VideoDetailFragment
}
scrollToTop();
addVideoPlayerView();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
addVideoPlayerView();
} else {
// KitKat needs a delay before addVideoPlayerView call or it reports wrong height in
// activity.getWindow().getDecorView().getHeight()
new Handler().post(this::addVideoPlayerView);
}
}
@Override
@ -1932,13 +1944,15 @@ public class VideoDetailFragment
// 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
if (isLandscape() && DeviceUtils.isTablet(activity)) {
// or portrait & unlocked global orientation
if (DeviceUtils.isTablet(activity)
&& (!globalScreenOrientationLocked(activity) || isLandscape())) {
player.toggleFullscreen();
return;
}
final int newOrientation = isLandscape()
? ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
? ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
: ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE;
activity.setRequestedOrientation(newOrientation);
@ -1983,7 +1997,11 @@ public class VideoDetailFragment
WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT;
}
activity.getWindow().getDecorView().setSystemUiVisibility(0);
activity.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS);
activity.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
activity.getWindow().setStatusBarColor(ThemeHelper.resolveColorFromAttr(
requireContext(), android.R.attr.colorPrimary));
}
}
private void hideSystemUi() {
@ -1998,18 +2016,26 @@ public class VideoDetailFragment
// 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_NEVER;
WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
}
final int visibility = View.SYSTEM_UI_FLAG_LAYOUT_STABLE
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_FULLSCREEN
| 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
if (!isInMultiWindow()) {
visibility |= View.SYSTEM_UI_FLAG_FULLSCREEN;
}
activity.getWindow().getDecorView().setSystemUiVisibility(visibility);
activity.getWindow().setFlags(
WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS,
WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP
&& (isInMultiWindow() || (player != null && player.isFullscreen()))) {
activity.getWindow().setStatusBarColor(Color.TRANSPARENT);
activity.getWindow().setNavigationBarColor(Color.TRANSPARENT);
}
activity.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
}
// Listener implementation
@ -2027,13 +2053,11 @@ public class VideoDetailFragment
&& player.getPlayer().getPlaybackState() != Player.STATE_IDLE;
}
private void saveCurrentAndRestoreDefaultBrightness() {
private void restoreDefaultBrightness() {
final WindowManager.LayoutParams lp = activity.getWindow().getAttributes();
if (lp.screenBrightness == -1) {
return;
}
// Save current brightness level
PlayerHelper.setScreenBrightness(activity, lp.screenBrightness);
// Restore the old brightness when fragment.onPause() called or
// when a player is in portrait
@ -2052,7 +2076,7 @@ public class VideoDetailFragment
|| !player.isFullscreen()
|| bottomSheetState != BottomSheetBehavior.STATE_EXPANDED) {
// Apply system brightness when the player is not in fullscreen
saveCurrentAndRestoreDefaultBrightness();
restoreDefaultBrightness();
} else {
// Restore already saved brightness level
final float brightnessLevel = PlayerHelper.getScreenBrightness(activity);
@ -2071,11 +2095,9 @@ public class VideoDetailFragment
}
player.checkLandscape();
final boolean orientationLocked = PlayerHelper.globalScreenOrientationLocked(activity);
// Let's give a user time to look at video information page if video is not playing
if (orientationLocked && !player.isPlaying()) {
if (globalScreenOrientationLocked(activity) && !player.isPlaying()) {
player.onPlay();
player.showControlsThenHide();
}
}
@ -2278,6 +2300,7 @@ public class VideoDetailFragment
&& player.videoPlayerSelected()) {
player.toggleFullscreen();
}
setOverlayLook(appBarLayout, behavior, 1);
break;
case BottomSheetBehavior.STATE_COLLAPSED:
moveFocusToMainFragment(true);
@ -2287,6 +2310,7 @@ public class VideoDetailFragment
if (player != null) {
player.onQueueClosed();
}
setOverlayLook(appBarLayout, behavior, 0);
break;
case BottomSheetBehavior.STATE_DRAGGING:
case BottomSheetBehavior.STATE_SETTLING:

View file

@ -52,6 +52,7 @@ import org.schabi.newpipe.report.UserAction;
import org.schabi.newpipe.util.DeviceUtils;
import org.schabi.newpipe.util.AnimationUtils;
import org.schabi.newpipe.util.Constants;
import org.schabi.newpipe.util.ExceptionUtils;
import org.schabi.newpipe.util.ExtractorHelper;
import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.ServiceHelper;
@ -78,7 +79,7 @@ import static androidx.recyclerview.widget.ItemTouchHelper.Callback.makeMovement
import static java.util.Arrays.asList;
import static org.schabi.newpipe.util.AnimationUtils.animateView;
public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.InfoItemsPage>
public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.InfoItemsPage<?>>
implements BackPressable {
/*//////////////////////////////////////////////////////////////////////////
// Search
@ -133,7 +134,6 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
private Map<Integer, String> menuItemToFilterName;
private StreamingService service;
private Page nextPage;
private String contentCountry;
private boolean isSuggestionsEnabled = true;
private Disposable searchDisposable;
@ -154,6 +154,7 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
private TextView correctSuggestion;
private View suggestionsPanel;
private boolean suggestionsPanelVisible = false;
private RecyclerView suggestionsRecyclerView;
/*////////////////////////////////////////////////////////////////////////*/
@ -204,8 +205,6 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
= PreferenceManager.getDefaultSharedPreferences(activity);
isSuggestionsEnabled = preferences
.getBoolean(getString(R.string.show_search_suggestions_key), true);
contentCountry = preferences.getString(getString(R.string.content_country_key),
getString(R.string.default_localization_key));
}
@Override
@ -233,9 +232,7 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
if (suggestionDisposable != null) {
suggestionDisposable.dispose();
}
if (disposables != null) {
disposables.clear();
}
disposables.clear();
hideKeyboardSearch();
}
@ -249,8 +246,8 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
try {
service = NewPipe.getService(serviceId);
} catch (final Exception e) {
ErrorActivity.reportError(getActivity(), e, getActivity().getClass(),
getActivity().findViewById(android.R.id.content),
ErrorActivity.reportError(getActivity(), e, requireActivity().getClass(),
requireActivity().findViewById(android.R.id.content),
ErrorActivity.ErrorInfo.make(UserAction.UI_ERROR,
"",
"", R.string.general_error));
@ -303,26 +300,20 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
if (suggestionDisposable != null) {
suggestionDisposable.dispose();
}
if (disposables != null) {
disposables.clear();
}
disposables.clear();
}
@Override
public void onActivityResult(final int requestCode, final int resultCode, final Intent data) {
switch (requestCode) {
case ReCaptchaActivity.RECAPTCHA_REQUEST:
if (resultCode == Activity.RESULT_OK
&& !TextUtils.isEmpty(searchString)) {
search(searchString, contentFilter, sortFilter);
} else {
Log.e(TAG, "ReCaptcha failed");
}
break;
default:
Log.e(TAG, "Request code from activity not supported [" + requestCode + "]");
break;
if (requestCode == ReCaptchaActivity.RECAPTCHA_REQUEST) {
if (resultCode == Activity.RESULT_OK
&& !TextUtils.isEmpty(searchString)) {
search(searchString, contentFilter, sortFilter);
} else {
Log.e(TAG, "ReCaptcha failed");
}
} else {
Log.e(TAG, "Request code from activity not supported [" + requestCode + "]");
}
}
@ -340,7 +331,7 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
@Override
public int getMovementFlags(@NonNull final RecyclerView recyclerView,
@NonNull final RecyclerView.ViewHolder viewHolder) {
return getSuggestionMovementFlags(recyclerView, viewHolder);
return getSuggestionMovementFlags(viewHolder);
}
@Override
@ -352,7 +343,7 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
@Override
public void onSwiped(@NonNull final RecyclerView.ViewHolder viewHolder, final int i) {
onSuggestionItemSwiped(viewHolder, i);
onSuggestionItemSwiped(viewHolder);
}
}).attachToRecyclerView(suggestionsRecyclerView);
@ -627,6 +618,7 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
if (DEBUG) {
Log.d(TAG, "showSuggestionsPanel() called");
}
suggestionsPanelVisible = true;
animateView(suggestionsPanel, AnimationUtils.Type.LIGHT_SLIDE_AND_ALPHA, true, 200);
}
@ -634,6 +626,7 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
if (DEBUG) {
Log.d(TAG, "hideSuggestionsPanel() called");
}
suggestionsPanelVisible = false;
animateView(suggestionsPanel, AnimationUtils.Type.LIGHT_SLIDE_AND_ALPHA, false, 200);
}
@ -669,8 +662,7 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
}
private void showDeleteSuggestionDialog(final SuggestionItem item) {
if (activity == null || historyRecordManager == null || suggestionPublisher == null
|| searchEditText == null || disposables == null) {
if (activity == null || historyRecordManager == null || searchEditText == null) {
return;
}
final String query = item.query;
@ -695,7 +687,7 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
@Override
public boolean onBackPressed() {
if (suggestionsPanel.getVisibility() == View.VISIBLE
if (suggestionsPanelVisible
&& infoListAdapter.getItemsList().size() > 0
&& !isLoading.get()) {
hideSuggestionsPanel();
@ -742,6 +734,13 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
final Observable<List<SuggestionItem>> network = ExtractorHelper
.suggestionsFor(serviceId, query)
.onErrorReturn(throwable -> {
if (!ExceptionUtils.isNetworkRelated(throwable)) {
showSnackBarError(throwable, UserAction.GET_SUGGESTIONS,
NewPipe.getNameOfService(serviceId), searchString, 0);
}
return new ArrayList<>();
})
.toObservable()
.map(strings -> {
final List<SuggestionItem> result = new ArrayList<>();
@ -791,21 +790,23 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
// no-op
}
private void search(final String ss, final String[] cf, final String sf) {
private void search(final String theSearchString,
final String[] theContentFilter,
final String theSortFilter) {
if (DEBUG) {
Log.d(TAG, "search() called with: query = [" + ss + "]");
Log.d(TAG, "search() called with: query = [" + theSearchString + "]");
}
if (ss.isEmpty()) {
if (theSearchString.isEmpty()) {
return;
}
try {
final StreamingService streamingService = NewPipe.getServiceByUrl(ss);
final StreamingService streamingService = NewPipe.getServiceByUrl(theSearchString);
if (streamingService != null) {
showLoading();
disposables.add(Observable
.fromCallable(() ->
NavigationHelper.getIntentByLink(activity, streamingService, ss))
.fromCallable(() -> NavigationHelper.getIntentByLink(activity,
streamingService, theSearchString))
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(intent -> {
@ -820,29 +821,27 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
}
lastSearchedString = this.searchString;
this.searchString = ss;
this.searchString = theSearchString;
infoListAdapter.clearStreamItemList();
hideSuggestionsPanel();
hideKeyboardSearch();
historyRecordManager.onSearched(serviceId, ss)
disposables.add(historyRecordManager.onSearched(serviceId, theSearchString)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
ignored -> {
},
error -> showSnackBarError(error, UserAction.SEARCHED,
NewPipe.getNameOfService(serviceId), ss, 0)
);
suggestionPublisher.onNext(ss);
NewPipe.getNameOfService(serviceId), theSearchString, 0)
));
suggestionPublisher.onNext(theSearchString);
startLoading(false);
}
@Override
public void startLoading(final boolean forceLoad) {
super.startLoading(forceLoad);
if (disposables != null) {
disposables.clear();
}
disposables.clear();
if (searchDisposable != null) {
searchDisposable.dispose();
}
@ -881,8 +880,7 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
@Override
protected boolean hasMoreItems() {
// TODO: No way to tell if search has more items in the moment
return true;
return Page.isValid(nextPage);
}
@Override
@ -895,22 +893,25 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
// Utils
//////////////////////////////////////////////////////////////////////////*/
private void changeContentFilter(final MenuItem item, final List<String> cf) {
this.filterItemCheckedId = item.getItemId();
private void changeContentFilter(final MenuItem item, final List<String> theContentFilter) {
filterItemCheckedId = item.getItemId();
item.setChecked(true);
this.contentFilter = new String[]{cf.get(0)};
contentFilter = new String[]{theContentFilter.get(0)};
if (!TextUtils.isEmpty(searchString)) {
search(searchString, this.contentFilter, sortFilter);
search(searchString, contentFilter, sortFilter);
}
}
private void setQuery(final int sid, final String ss, final String[] cf, final String sf) {
this.serviceId = sid;
this.searchString = searchString;
this.contentFilter = cf;
this.sortFilter = sf;
private void setQuery(final int theServiceId,
final String theSearchString,
final String[] theContentFilter,
final String theSortFilter) {
serviceId = theServiceId;
searchString = theSearchString;
contentFilter = theContentFilter;
sortFilter = theSortFilter;
}
/*//////////////////////////////////////////////////////////////////////////
@ -924,7 +925,7 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
suggestionsRecyclerView.smoothScrollToPosition(0);
suggestionsRecyclerView.post(() -> suggestionListAdapter.setItems(suggestions));
if (errorPanelRoot.getVisibility() == View.VISIBLE) {
if (suggestionsPanelVisible && errorPanelRoot.getVisibility() == View.VISIBLE) {
hideLoading();
}
}
@ -1027,7 +1028,7 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
}
@Override
public void handleNextItems(final ListExtractor.InfoItemsPage result) {
public void handleNextItems(final ListExtractor.InfoItemsPage<?> result) {
showListFooter(false);
infoListAdapter.addInfoItemList(result.getItems());
nextPage = result.getNextPage();
@ -1066,8 +1067,7 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
// Suggestion item touch helper
//////////////////////////////////////////////////////////////////////////*/
public int getSuggestionMovementFlags(@NonNull final RecyclerView recyclerView,
@NonNull final RecyclerView.ViewHolder viewHolder) {
public int getSuggestionMovementFlags(@NonNull final RecyclerView.ViewHolder viewHolder) {
final int position = viewHolder.getAdapterPosition();
if (position == RecyclerView.NO_POSITION) {
return 0;
@ -1078,8 +1078,7 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT) : 0;
}
public void onSuggestionItemSwiped(@NonNull final RecyclerView.ViewHolder viewHolder,
final int i) {
public void onSuggestionItemSwiped(@NonNull final RecyclerView.ViewHolder viewHolder) {
final int position = viewHolder.getAdapterPosition();
final String query = suggestionListAdapter.getItem(position).query;
final Disposable onDelete = historyRecordManager.deleteSearchHistory(query)

View file

@ -191,6 +191,8 @@ public abstract class BasePlayer implements
@NonNull
protected final HistoryRecordManager recordManager;
@NonNull
protected final SharedPreferences sharedPreferences;
@NonNull
protected final CustomTrackSelector trackSelector;
@NonNull
protected final PlayerDataSource dataSource;
@ -223,6 +225,7 @@ public abstract class BasePlayer implements
setupBroadcastReceiver(intentFilter);
this.recordManager = new HistoryRecordManager(context);
this.sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
this.progressUpdateReactor = new SerialDisposable();
this.databaseUpdateReactor = new CompositeDisposable();
@ -1422,7 +1425,15 @@ public abstract class BasePlayer implements
Log.d(TAG, "seekBy() called with: position = [" + positionMillis + "]");
}
if (simpleExoPlayer != null) {
simpleExoPlayer.seekTo(positionMillis);
// prevent invalid positions when fast-forwarding/-rewinding
long normalizedPositionMillis = positionMillis;
if (normalizedPositionMillis < 0) {
normalizedPositionMillis = 0;
} else if (normalizedPositionMillis > simpleExoPlayer.getDuration()) {
normalizedPositionMillis = simpleExoPlayer.getDuration();
}
simpleExoPlayer.seekTo(normalizedPositionMillis);
}
}
@ -1593,6 +1604,11 @@ public abstract class BasePlayer implements
return currentMetadata;
}
@NonNull
public LoadController getLoadController() {
return (LoadController) loadControl;
}
@NonNull
public String getVideoUrl() {
return currentMetadata == null

View file

@ -19,35 +19,18 @@
package org.schabi.newpipe.player;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.os.Binder;
import android.os.Build;
import android.os.IBinder;
import androidx.preference.PreferenceManager;
import android.util.DisplayMetrics;
import android.view.ViewGroup;
import android.view.WindowManager;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.core.app.NotificationCompat;
import android.util.Log;
import android.view.View;
import android.widget.RemoteViews;
import android.view.ViewGroup;
import android.view.WindowManager;
import com.google.android.exoplayer2.Player;
import org.schabi.newpipe.BuildConfig;
import org.schabi.newpipe.MainActivity;
import org.schabi.newpipe.R;
import org.schabi.newpipe.util.BitmapUtils;
import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.ThemeHelper;
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
@ -64,7 +47,6 @@ public final class MainPlayer extends Service {
private VideoPlayerImpl playerImpl;
private WindowManager windowManager;
private SharedPreferences sharedPreferences;
private final IBinder mBinder = new MainPlayer.LocalBinder();
@ -78,30 +60,26 @@ public final class MainPlayer extends Service {
// Notification
//////////////////////////////////////////////////////////////////////////*/
static final int NOTIFICATION_ID = 123789;
private NotificationManager notificationManager;
private NotificationCompat.Builder notBuilder;
private RemoteViews notRemoteView;
private RemoteViews bigNotRemoteView;
static final String ACTION_CLOSE =
"org.schabi.newpipe.player.MainPlayer.CLOSE";
static final String ACTION_PLAY_PAUSE =
"org.schabi.newpipe.player.MainPlayer.PLAY_PAUSE";
static final String ACTION_OPEN_CONTROLS =
"org.schabi.newpipe.player.MainPlayer.OPEN_CONTROLS";
static final String ACTION_REPEAT =
"org.schabi.newpipe.player.MainPlayer.REPEAT";
static final String ACTION_PLAY_NEXT =
"org.schabi.newpipe.player.MainPlayer.ACTION_PLAY_NEXT";
static final String ACTION_PLAY_PREVIOUS =
"org.schabi.newpipe.player.MainPlayer.ACTION_PLAY_PREVIOUS";
static final String ACTION_FAST_REWIND =
"org.schabi.newpipe.player.MainPlayer.ACTION_FAST_REWIND";
static final String ACTION_FAST_FORWARD =
"org.schabi.newpipe.player.MainPlayer.ACTION_FAST_FORWARD";
private static final String SET_IMAGE_RESOURCE_METHOD = "setImageResource";
static final String ACTION_CLOSE
= "org.schabi.newpipe.player.MainPlayer.CLOSE";
static final String ACTION_PLAY_PAUSE
= "org.schabi.newpipe.player.MainPlayer.PLAY_PAUSE";
static final String ACTION_OPEN_CONTROLS
= "org.schabi.newpipe.player.MainPlayer.OPEN_CONTROLS";
static final String ACTION_REPEAT
= "org.schabi.newpipe.player.MainPlayer.REPEAT";
static final String ACTION_PLAY_NEXT
= "org.schabi.newpipe.player.MainPlayer.ACTION_PLAY_NEXT";
static final String ACTION_PLAY_PREVIOUS
= "org.schabi.newpipe.player.MainPlayer.ACTION_PLAY_PREVIOUS";
static final String ACTION_FAST_REWIND
= "org.schabi.newpipe.player.MainPlayer.ACTION_FAST_REWIND";
static final String ACTION_FAST_FORWARD
= "org.schabi.newpipe.player.MainPlayer.ACTION_FAST_FORWARD";
static final String ACTION_SHUFFLE
= "org.schabi.newpipe.player.MainPlayer.ACTION_SHUFFLE";
public static final String ACTION_RECREATE_NOTIFICATION
= "org.schabi.newpipe.player.MainPlayer.ACTION_RECREATE_NOTIFICATION";
/*//////////////////////////////////////////////////////////////////////////
// Service's LifeCycle
@ -113,9 +91,7 @@ public final class MainPlayer extends Service {
Log.d(TAG, "onCreate() called");
}
assureCorrectAppLanguage(this);
notificationManager = ((NotificationManager) getSystemService(NOTIFICATION_SERVICE));
windowManager = (WindowManager) getSystemService(WINDOW_SERVICE);
sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
ThemeHelper.setTheme(this);
createView();
@ -143,7 +119,7 @@ public final class MainPlayer extends Service {
if (Intent.ACTION_MEDIA_BUTTON.equals(intent.getAction())
|| intent.getStringExtra(VideoPlayer.PLAY_QUEUE_KEY) != null) {
showNotificationAndStartForeground();
NotificationUtil.getInstance().createNotificationAndStartForeground(playerImpl, this);
}
playerImpl.handleIntent(intent);
@ -171,12 +147,13 @@ public final class MainPlayer extends Service {
// Android TV will handle back button in case controls will be visible
// (one more additional unneeded click while the player is hidden)
playerImpl.hideControls(0, 0);
playerImpl.onQueueClosed();
// Notification shows information about old stream but if a user selects
// a stream from backStack it's not actual anymore
// So we should hide the notification at all.
// When autoplay enabled such notification flashing is annoying so skip this case
if (!autoplayEnabled) {
stopForeground(true);
NotificationUtil.getInstance().cancelNotificationAndStopForeground(this);
}
}
}
@ -219,6 +196,10 @@ public final class MainPlayer extends Service {
}
if (playerImpl != null) {
// Exit from fullscreen when user closes the player via notification
if (playerImpl.isFullscreen()) {
playerImpl.toggleFullscreen();
}
removeViewFromParent();
playerImpl.setRecovery();
@ -227,11 +208,8 @@ public final class MainPlayer extends Service {
playerImpl.removePopupFromView();
playerImpl.destroy();
}
if (notificationManager != null) {
notificationManager.cancel(NOTIFICATION_ID);
}
stopForeground(true);
NotificationUtil.getInstance().cancelNotificationAndStopForeground(this);
stopSelf();
}
@ -270,206 +248,6 @@ public final class MainPlayer extends Service {
}
}
private void showNotificationAndStartForeground() {
resetNotification();
if (getBigNotRemoteView() != null) {
getBigNotRemoteView().setProgressBar(R.id.notificationProgressBar, 100, 0, false);
}
if (getNotRemoteView() != null) {
getNotRemoteView().setProgressBar(R.id.notificationProgressBar, 100, 0, false);
}
startForeground(NOTIFICATION_ID, getNotBuilder().build());
}
/*//////////////////////////////////////////////////////////////////////////
// Notification
//////////////////////////////////////////////////////////////////////////*/
void resetNotification() {
notBuilder = createNotification();
playerImpl.timesNotificationUpdated = 0;
}
private NotificationCompat.Builder createNotification() {
notRemoteView = new RemoteViews(BuildConfig.APPLICATION_ID,
R.layout.player_notification);
bigNotRemoteView = new RemoteViews(BuildConfig.APPLICATION_ID,
R.layout.player_notification_expanded);
setupNotification(notRemoteView);
setupNotification(bigNotRemoteView);
final NotificationCompat.Builder builder = new NotificationCompat
.Builder(this, getString(R.string.notification_channel_id))
.setOngoing(true)
.setSmallIcon(R.drawable.ic_newpipe_triangle_white)
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
.setCustomContentView(notRemoteView)
.setCustomBigContentView(bigNotRemoteView);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
setLockScreenThumbnail(builder);
}
builder.setPriority(NotificationCompat.PRIORITY_MAX);
return builder;
}
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
private void setLockScreenThumbnail(final NotificationCompat.Builder builder) {
final boolean isLockScreenThumbnailEnabled = sharedPreferences.getBoolean(
getString(R.string.enable_lock_screen_video_thumbnail_key), true);
if (isLockScreenThumbnailEnabled) {
playerImpl.mediaSessionManager.setLockScreenArt(
builder,
getCenteredThumbnailBitmap()
);
} else {
playerImpl.mediaSessionManager.clearLockScreenArt(builder);
}
}
@Nullable
private Bitmap getCenteredThumbnailBitmap() {
final int screenWidth = Resources.getSystem().getDisplayMetrics().widthPixels;
final int screenHeight = Resources.getSystem().getDisplayMetrics().heightPixels;
return BitmapUtils.centerCrop(playerImpl.getThumbnail(), screenWidth, screenHeight);
}
private void setupNotification(final RemoteViews remoteViews) {
// Don't show anything until player is playing
if (playerImpl == null) {
return;
}
remoteViews.setTextViewText(R.id.notificationSongName, playerImpl.getVideoTitle());
remoteViews.setTextViewText(R.id.notificationArtist, playerImpl.getUploaderName());
remoteViews.setImageViewBitmap(R.id.notificationCover, playerImpl.getThumbnail());
remoteViews.setOnClickPendingIntent(R.id.notificationPlayPause,
PendingIntent.getBroadcast(this, NOTIFICATION_ID,
new Intent(ACTION_PLAY_PAUSE), PendingIntent.FLAG_UPDATE_CURRENT));
remoteViews.setOnClickPendingIntent(R.id.notificationStop,
PendingIntent.getBroadcast(this, NOTIFICATION_ID,
new Intent(ACTION_CLOSE), PendingIntent.FLAG_UPDATE_CURRENT));
// Starts VideoDetailFragment or opens BackgroundPlayerActivity.
remoteViews.setOnClickPendingIntent(R.id.notificationContent,
PendingIntent.getActivity(this, NOTIFICATION_ID,
getIntentForNotification(), PendingIntent.FLAG_UPDATE_CURRENT));
remoteViews.setOnClickPendingIntent(R.id.notificationRepeat,
PendingIntent.getBroadcast(this, NOTIFICATION_ID,
new Intent(ACTION_REPEAT), PendingIntent.FLAG_UPDATE_CURRENT));
if (playerImpl.playQueue != null && playerImpl.playQueue.size() > 1) {
remoteViews.setInt(R.id.notificationFRewind, SET_IMAGE_RESOURCE_METHOD,
R.drawable.exo_controls_previous);
remoteViews.setInt(R.id.notificationFForward, SET_IMAGE_RESOURCE_METHOD,
R.drawable.exo_controls_next);
remoteViews.setOnClickPendingIntent(R.id.notificationFRewind,
PendingIntent.getBroadcast(this, NOTIFICATION_ID,
new Intent(ACTION_PLAY_PREVIOUS), PendingIntent.FLAG_UPDATE_CURRENT));
remoteViews.setOnClickPendingIntent(R.id.notificationFForward,
PendingIntent.getBroadcast(this, NOTIFICATION_ID,
new Intent(ACTION_PLAY_NEXT), PendingIntent.FLAG_UPDATE_CURRENT));
} else {
remoteViews.setInt(R.id.notificationFRewind, SET_IMAGE_RESOURCE_METHOD,
R.drawable.exo_controls_rewind);
remoteViews.setInt(R.id.notificationFForward, SET_IMAGE_RESOURCE_METHOD,
R.drawable.exo_controls_fastforward);
remoteViews.setOnClickPendingIntent(R.id.notificationFRewind,
PendingIntent.getBroadcast(this, NOTIFICATION_ID,
new Intent(ACTION_FAST_REWIND), PendingIntent.FLAG_UPDATE_CURRENT));
remoteViews.setOnClickPendingIntent(R.id.notificationFForward,
PendingIntent.getBroadcast(this, NOTIFICATION_ID,
new Intent(ACTION_FAST_FORWARD), PendingIntent.FLAG_UPDATE_CURRENT));
}
setRepeatModeIcon(remoteViews, playerImpl.getRepeatMode());
}
/**
* Updates the notification, and the play/pause button in it.
* Used for changes on the remoteView
*
* @param drawableId if != -1, sets the drawable with that id on the play/pause button
*/
synchronized void updateNotification(final int drawableId) {
/*if (DEBUG) {
Log.d(TAG, "updateNotification() called with: drawableId = [" + drawableId + "]");
}*/
if (notBuilder == null) {
return;
}
if (drawableId != -1) {
if (notRemoteView != null) {
notRemoteView.setImageViewResource(R.id.notificationPlayPause, drawableId);
}
if (bigNotRemoteView != null) {
bigNotRemoteView.setImageViewResource(R.id.notificationPlayPause, drawableId);
}
}
notificationManager.notify(NOTIFICATION_ID, notBuilder.build());
playerImpl.timesNotificationUpdated++;
}
/*//////////////////////////////////////////////////////////////////////////
// Utils
//////////////////////////////////////////////////////////////////////////*/
private void setRepeatModeIcon(final RemoteViews remoteViews, final int repeatMode) {
if (remoteViews == null) {
return;
}
switch (repeatMode) {
case Player.REPEAT_MODE_OFF:
remoteViews.setInt(R.id.notificationRepeat,
SET_IMAGE_RESOURCE_METHOD, R.drawable.exo_controls_repeat_off);
break;
case Player.REPEAT_MODE_ONE:
remoteViews.setInt(R.id.notificationRepeat,
SET_IMAGE_RESOURCE_METHOD, R.drawable.exo_controls_repeat_one);
break;
case Player.REPEAT_MODE_ALL:
remoteViews.setInt(R.id.notificationRepeat,
SET_IMAGE_RESOURCE_METHOD, R.drawable.exo_controls_repeat_all);
break;
}
}
private Intent getIntentForNotification() {
final Intent intent;
if (playerImpl.audioPlayerSelected() || playerImpl.popupPlayerSelected()) {
// Means we play in popup or audio only. Let's show BackgroundPlayerActivity
intent = NavigationHelper.getBackgroundPlayerActivityIntent(getApplicationContext());
} else {
// We are playing in fragment. Don't open another activity just show fragment. That's it
intent = NavigationHelper.getPlayerIntent(this, MainActivity.class, null, true);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.setAction(Intent.ACTION_MAIN);
intent.addCategory(Intent.CATEGORY_LAUNCHER);
}
return intent;
}
/*//////////////////////////////////////////////////////////////////////////
// Getters
//////////////////////////////////////////////////////////////////////////*/
NotificationCompat.Builder getNotBuilder() {
return notBuilder;
}
RemoteViews getBigNotRemoteView() {
return bigNotRemoteView;
}
RemoteViews getNotRemoteView() {
return notRemoteView;
}
public class LocalBinder extends Binder {

View file

@ -0,0 +1,165 @@
package org.schabi.newpipe.player;
import android.content.Context;
import android.content.SharedPreferences;
import androidx.annotation.DrawableRes;
import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
import org.schabi.newpipe.R;
import org.schabi.newpipe.util.Localization;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.SortedSet;
import java.util.TreeSet;
public final class NotificationConstants {
private NotificationConstants() { }
public static final int NOTHING = 0;
public static final int PREVIOUS = 1;
public static final int NEXT = 2;
public static final int REWIND = 3;
public static final int FORWARD = 4;
public static final int SMART_REWIND_PREVIOUS = 5;
public static final int SMART_FORWARD_NEXT = 6;
public static final int PLAY_PAUSE = 7;
public static final int PLAY_PAUSE_BUFFERING = 8;
public static final int REPEAT = 9;
public static final int SHUFFLE = 10;
public static final int CLOSE = 11;
@Retention(RetentionPolicy.SOURCE)
@IntDef({NOTHING, PREVIOUS, NEXT, REWIND, FORWARD, SMART_REWIND_PREVIOUS, SMART_FORWARD_NEXT,
PLAY_PAUSE, PLAY_PAUSE_BUFFERING, REPEAT, SHUFFLE, CLOSE})
public @interface Action { }
@DrawableRes
public static final int[] ACTION_ICONS = {
0,
R.drawable.exo_icon_previous,
R.drawable.exo_icon_next,
R.drawable.exo_icon_rewind,
R.drawable.exo_icon_fastforward,
R.drawable.exo_icon_previous,
R.drawable.exo_icon_next,
R.drawable.ic_pause_white_24dp,
R.drawable.ic_hourglass_top_white_24dp,
R.drawable.exo_icon_repeat_all,
R.drawable.exo_icon_shuffle_on,
R.drawable.ic_close_white_24dp,
};
@Action
public static final int[] SLOT_DEFAULTS = {
SMART_REWIND_PREVIOUS,
PLAY_PAUSE_BUFFERING,
SMART_FORWARD_NEXT,
REPEAT,
CLOSE,
};
@Action
public static final int[][] SLOT_ALLOWED_ACTIONS = {
new int[] {PREVIOUS, REWIND, SMART_REWIND_PREVIOUS},
new int[] {REWIND, PLAY_PAUSE, PLAY_PAUSE_BUFFERING},
new int[] {NEXT, FORWARD, SMART_FORWARD_NEXT, PLAY_PAUSE, PLAY_PAUSE_BUFFERING},
new int[] {NOTHING, PREVIOUS, NEXT, REWIND, FORWARD, SMART_REWIND_PREVIOUS,
SMART_FORWARD_NEXT, REPEAT, SHUFFLE, CLOSE},
new int[] {NOTHING, NEXT, FORWARD, SMART_FORWARD_NEXT, REPEAT, SHUFFLE, CLOSE},
};
public static final int[] SLOT_PREF_KEYS = {
R.string.notification_slot_0_key,
R.string.notification_slot_1_key,
R.string.notification_slot_2_key,
R.string.notification_slot_3_key,
R.string.notification_slot_4_key,
};
public static final Integer[] SLOT_COMPACT_DEFAULTS = {0, 1, 2};
public static final int[] SLOT_COMPACT_PREF_KEYS = {
R.string.notification_slot_compact_0_key,
R.string.notification_slot_compact_1_key,
R.string.notification_slot_compact_2_key,
};
public static String getActionName(@NonNull final Context context, @Action final int action) {
switch (action) {
case PREVIOUS:
return context.getString(R.string.exo_controls_previous_description);
case NEXT:
return context.getString(R.string.exo_controls_next_description);
case REWIND:
return context.getString(R.string.exo_controls_rewind_description);
case FORWARD:
return context.getString(R.string.exo_controls_fastforward_description);
case SMART_REWIND_PREVIOUS:
return Localization.concatenateStrings(
context.getString(R.string.exo_controls_rewind_description),
context.getString(R.string.exo_controls_previous_description));
case SMART_FORWARD_NEXT:
return Localization.concatenateStrings(
context.getString(R.string.exo_controls_fastforward_description),
context.getString(R.string.exo_controls_next_description));
case PLAY_PAUSE:
return Localization.concatenateStrings(
context.getString(R.string.exo_controls_play_description),
context.getString(R.string.exo_controls_pause_description));
case PLAY_PAUSE_BUFFERING:
return Localization.concatenateStrings(
context.getString(R.string.exo_controls_play_description),
context.getString(R.string.exo_controls_pause_description),
context.getString(R.string.notification_action_buffering));
case REPEAT:
return context.getString(R.string.notification_action_repeat);
case SHUFFLE:
return context.getString(R.string.notification_action_shuffle);
case CLOSE:
return context.getString(R.string.close);
case NOTHING: default:
return context.getString(R.string.notification_action_nothing);
}
}
/**
* @param context the context to use
* @param sharedPreferences the shared preferences to query values from
* @param slotCount remove indices >= than this value (set to {@code 5} to do nothing, or make
* it lower if there are slots with empty actions)
* @return a sorted list of the indices of the slots to use as compact slots
*/
public static List<Integer> getCompactSlotsFromPreferences(
@NonNull final Context context,
final SharedPreferences sharedPreferences,
final int slotCount) {
final SortedSet<Integer> compactSlots = new TreeSet<>();
for (int i = 0; i < 3; i++) {
final int compactSlot = sharedPreferences.getInt(
context.getString(SLOT_COMPACT_PREF_KEYS[i]), Integer.MAX_VALUE);
if (compactSlot == Integer.MAX_VALUE) {
// settings not yet populated, return default values
return new ArrayList<>(Arrays.asList(SLOT_COMPACT_DEFAULTS));
}
// a negative value (-1) is set when the user does not want a particular compact slot
if (compactSlot >= 0 && compactSlot < slotCount) {
compactSlots.add(compactSlot);
}
}
return new ArrayList<>(compactSlots);
}
}

View file

@ -0,0 +1,371 @@
package org.schabi.newpipe.player;
import android.annotation.SuppressLint;
import android.app.PendingIntent;
import android.app.Service;
import android.content.Intent;
import android.content.pm.ServiceInfo;
import android.graphics.Bitmap;
import android.graphics.Matrix;
import android.os.Build;
import android.util.Log;
import androidx.annotation.DrawableRes;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import androidx.core.app.NotificationCompat;
import androidx.core.app.NotificationManagerCompat;
import androidx.core.content.ContextCompat;
import org.schabi.newpipe.MainActivity;
import org.schabi.newpipe.R;
import org.schabi.newpipe.util.NavigationHelper;
import java.util.List;
import static android.app.PendingIntent.FLAG_UPDATE_CURRENT;
import static com.google.android.exoplayer2.Player.REPEAT_MODE_ALL;
import static com.google.android.exoplayer2.Player.REPEAT_MODE_ONE;
import static org.schabi.newpipe.player.MainPlayer.ACTION_CLOSE;
import static org.schabi.newpipe.player.MainPlayer.ACTION_FAST_FORWARD;
import static org.schabi.newpipe.player.MainPlayer.ACTION_FAST_REWIND;
import static org.schabi.newpipe.player.MainPlayer.ACTION_PLAY_NEXT;
import static org.schabi.newpipe.player.MainPlayer.ACTION_PLAY_PAUSE;
import static org.schabi.newpipe.player.MainPlayer.ACTION_PLAY_PREVIOUS;
import static org.schabi.newpipe.player.MainPlayer.ACTION_REPEAT;
import static org.schabi.newpipe.player.MainPlayer.ACTION_SHUFFLE;
/**
* This is a utility class for player notifications.
*
* @author cool-student
*/
public final class NotificationUtil {
private static final String TAG = NotificationUtil.class.getSimpleName();
private static final boolean DEBUG = BasePlayer.DEBUG;
private static final int NOTIFICATION_ID = 123789;
@Nullable private static NotificationUtil instance = null;
@NotificationConstants.Action
private int[] notificationSlots = NotificationConstants.SLOT_DEFAULTS.clone();
private NotificationManagerCompat notificationManager;
private NotificationCompat.Builder notificationBuilder;
private NotificationUtil() {
}
public static NotificationUtil getInstance() {
if (instance == null) {
instance = new NotificationUtil();
}
return instance;
}
/////////////////////////////////////////////////////
// NOTIFICATION
/////////////////////////////////////////////////////
/**
* Creates the notification if it does not exist already and recreates it if forceRecreate is
* true. Updates the notification with the data in the player.
* @param player the player currently open, to take data from
* @param forceRecreate whether to force the recreation of the notification even if it already
* exists
*/
synchronized void createNotificationIfNeededAndUpdate(final VideoPlayerImpl player,
final boolean forceRecreate) {
if (forceRecreate || notificationBuilder == null) {
notificationBuilder = createNotification(player);
}
updateNotification(player);
notificationManager.notify(NOTIFICATION_ID, notificationBuilder.build());
}
private synchronized NotificationCompat.Builder createNotification(
final VideoPlayerImpl player) {
if (DEBUG) {
Log.d(TAG, "createNotification()");
}
notificationManager = NotificationManagerCompat.from(player.context);
final NotificationCompat.Builder builder = new NotificationCompat.Builder(player.context,
player.context.getString(R.string.notification_channel_id));
initializeNotificationSlots(player);
// count the number of real slots, to make sure compact slots indices are not out of bound
int nonNothingSlotCount = 5;
if (notificationSlots[3] == NotificationConstants.NOTHING) {
--nonNothingSlotCount;
}
if (notificationSlots[4] == NotificationConstants.NOTHING) {
--nonNothingSlotCount;
}
// build the compact slot indices array (need code to convert from Integer... because Java)
final List<Integer> compactSlotList = NotificationConstants.getCompactSlotsFromPreferences(
player.context, player.sharedPreferences, nonNothingSlotCount);
final int[] compactSlots = new int[compactSlotList.size()];
for (int i = 0; i < compactSlotList.size(); i++) {
compactSlots[i] = compactSlotList.get(i);
}
builder.setStyle(new androidx.media.app.NotificationCompat.MediaStyle()
.setMediaSession(player.mediaSessionManager.getSessionToken())
.setShowActionsInCompactView(compactSlots))
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setSmallIcon(R.drawable.ic_newpipe_triangle_white)
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
.setColor(ContextCompat.getColor(player.context, R.color.gray))
.setCategory(NotificationCompat.CATEGORY_TRANSPORT)
.setDeleteIntent(PendingIntent.getBroadcast(player.context, NOTIFICATION_ID,
new Intent(ACTION_CLOSE), FLAG_UPDATE_CURRENT));
return builder;
}
/**
* Updates the notification builder and the button icons depending on the playback state.
* @param player the player currently open, to take data from
*/
private synchronized void updateNotification(final VideoPlayerImpl player) {
if (DEBUG) {
Log.d(TAG, "updateNotification()");
}
// also update content intent, in case the user switched players
notificationBuilder.setContentIntent(PendingIntent.getActivity(player.context,
NOTIFICATION_ID, getIntentForNotification(player), FLAG_UPDATE_CURRENT));
notificationBuilder.setContentTitle(player.getVideoTitle());
notificationBuilder.setContentText(player.getUploaderName());
notificationBuilder.setTicker(player.getVideoTitle());
updateActions(notificationBuilder, player);
setLargeIcon(notificationBuilder, player);
}
@SuppressLint("RestrictedApi")
boolean shouldUpdateBufferingSlot() {
if (notificationBuilder.mActions.size() < 3) {
// this should never happen, but let's make sure notification actions are populated
return true;
}
// only second and third slot could contain PLAY_PAUSE_BUFFERING, update them only if they
// are not already in the buffering state (the only one with a null action intent)
return (notificationSlots[1] == NotificationConstants.PLAY_PAUSE_BUFFERING
&& notificationBuilder.mActions.get(1).actionIntent != null)
|| (notificationSlots[2] == NotificationConstants.PLAY_PAUSE_BUFFERING
&& notificationBuilder.mActions.get(2).actionIntent != null);
}
void createNotificationAndStartForeground(final VideoPlayerImpl player, final Service service) {
if (notificationBuilder == null) {
notificationBuilder = createNotification(player);
}
updateNotification(player);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
service.startForeground(NOTIFICATION_ID, notificationBuilder.build(),
ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK);
} else {
service.startForeground(NOTIFICATION_ID, notificationBuilder.build());
}
}
void cancelNotificationAndStopForeground(final Service service) {
service.stopForeground(true);
if (notificationManager != null) {
notificationManager.cancel(NOTIFICATION_ID);
}
notificationManager = null;
notificationBuilder = null;
}
/////////////////////////////////////////////////////
// ACTIONS
/////////////////////////////////////////////////////
private void initializeNotificationSlots(final VideoPlayerImpl player) {
for (int i = 0; i < 5; ++i) {
notificationSlots[i] = player.sharedPreferences.getInt(
player.context.getString(NotificationConstants.SLOT_PREF_KEYS[i]),
NotificationConstants.SLOT_DEFAULTS[i]);
}
}
@SuppressLint("RestrictedApi")
private void updateActions(final NotificationCompat.Builder builder,
final VideoPlayerImpl player) {
builder.mActions.clear();
for (int i = 0; i < 5; ++i) {
addAction(builder, player, notificationSlots[i]);
}
}
private void addAction(final NotificationCompat.Builder builder,
final VideoPlayerImpl player,
@NotificationConstants.Action final int slot) {
final NotificationCompat.Action action = getAction(player, slot);
if (action != null) {
builder.addAction(action);
}
}
@Nullable
private NotificationCompat.Action getAction(
final VideoPlayerImpl player,
@NotificationConstants.Action final int selectedAction) {
final int baseActionIcon = NotificationConstants.ACTION_ICONS[selectedAction];
switch (selectedAction) {
case NotificationConstants.PREVIOUS:
return getAction(player, baseActionIcon,
R.string.exo_controls_previous_description, ACTION_PLAY_PREVIOUS);
case NotificationConstants.NEXT:
return getAction(player, baseActionIcon,
R.string.exo_controls_next_description, ACTION_PLAY_NEXT);
case NotificationConstants.REWIND:
return getAction(player, baseActionIcon,
R.string.exo_controls_rewind_description, ACTION_FAST_REWIND);
case NotificationConstants.FORWARD:
return getAction(player, baseActionIcon,
R.string.exo_controls_fastforward_description, ACTION_FAST_FORWARD);
case NotificationConstants.SMART_REWIND_PREVIOUS:
if (player.playQueue != null && player.playQueue.size() > 1) {
return getAction(player, R.drawable.exo_notification_previous,
R.string.exo_controls_previous_description, ACTION_PLAY_PREVIOUS);
} else {
return getAction(player, R.drawable.exo_controls_rewind,
R.string.exo_controls_rewind_description, ACTION_FAST_REWIND);
}
case NotificationConstants.SMART_FORWARD_NEXT:
if (player.playQueue != null && player.playQueue.size() > 1) {
return getAction(player, R.drawable.exo_notification_next,
R.string.exo_controls_next_description, ACTION_PLAY_NEXT);
} else {
return getAction(player, R.drawable.exo_controls_fastforward,
R.string.exo_controls_fastforward_description, ACTION_FAST_FORWARD);
}
case NotificationConstants.PLAY_PAUSE_BUFFERING:
if (player.getCurrentState() == BasePlayer.STATE_PREFLIGHT
|| player.getCurrentState() == BasePlayer.STATE_BLOCKED
|| player.getCurrentState() == BasePlayer.STATE_BUFFERING) {
// null intent -> show hourglass icon that does nothing when clicked
return new NotificationCompat.Action(R.drawable.ic_hourglass_top_white_24dp_png,
player.context.getString(R.string.notification_action_buffering),
null);
}
case NotificationConstants.PLAY_PAUSE:
if (player.getCurrentState() == BasePlayer.STATE_COMPLETED) {
return getAction(player, R.drawable.ic_replay_white_24dp_png,
R.string.exo_controls_pause_description, ACTION_PLAY_PAUSE);
} else if (player.isPlaying()
|| player.getCurrentState() == BasePlayer.STATE_PREFLIGHT
|| player.getCurrentState() == BasePlayer.STATE_BLOCKED
|| player.getCurrentState() == BasePlayer.STATE_BUFFERING) {
return getAction(player, R.drawable.exo_notification_pause,
R.string.exo_controls_pause_description, ACTION_PLAY_PAUSE);
} else {
return getAction(player, R.drawable.exo_notification_play,
R.string.exo_controls_play_description, ACTION_PLAY_PAUSE);
}
case NotificationConstants.REPEAT:
if (player.getRepeatMode() == REPEAT_MODE_ALL) {
return getAction(player, R.drawable.exo_media_action_repeat_all,
R.string.exo_controls_repeat_all_description, ACTION_REPEAT);
} else if (player.getRepeatMode() == REPEAT_MODE_ONE) {
return getAction(player, R.drawable.exo_media_action_repeat_one,
R.string.exo_controls_repeat_one_description, ACTION_REPEAT);
} else /* player.getRepeatMode() == REPEAT_MODE_OFF */ {
return getAction(player, R.drawable.exo_media_action_repeat_off,
R.string.exo_controls_repeat_off_description, ACTION_REPEAT);
}
case NotificationConstants.SHUFFLE:
if (player.playQueue != null && player.playQueue.isShuffled()) {
return getAction(player, R.drawable.exo_controls_shuffle_on,
R.string.exo_controls_shuffle_on_description, ACTION_SHUFFLE);
} else {
return getAction(player, R.drawable.exo_controls_shuffle_off,
R.string.exo_controls_shuffle_off_description, ACTION_SHUFFLE);
}
case NotificationConstants.CLOSE:
return getAction(player, R.drawable.ic_close_white_24dp_png,
R.string.close, ACTION_CLOSE);
case NotificationConstants.NOTHING:
default:
// do nothing
return null;
}
}
private NotificationCompat.Action getAction(final VideoPlayerImpl player,
@DrawableRes final int drawable,
@StringRes final int title,
final String intentAction) {
return new NotificationCompat.Action(drawable, player.context.getString(title),
PendingIntent.getBroadcast(player.context, NOTIFICATION_ID,
new Intent(intentAction), FLAG_UPDATE_CURRENT));
}
private Intent getIntentForNotification(final VideoPlayerImpl player) {
if (player.audioPlayerSelected() || player.popupPlayerSelected()) {
// Means we play in popup or audio only. Let's show the play queue
return NavigationHelper.getPlayQueueActivityIntent(player.context);
} else {
// We are playing in fragment. Don't open another activity just show fragment. That's it
final Intent intent = NavigationHelper.getPlayerIntent(
player.context, MainActivity.class, null, true);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.setAction(Intent.ACTION_MAIN);
intent.addCategory(Intent.CATEGORY_LAUNCHER);
return intent;
}
}
/////////////////////////////////////////////////////
// BITMAP
/////////////////////////////////////////////////////
private void setLargeIcon(final NotificationCompat.Builder builder,
final VideoPlayerImpl player) {
final boolean scaleImageToSquareAspectRatio = player.sharedPreferences.getBoolean(
player.context.getString(R.string.scale_to_square_image_in_notifications_key),
false);
if (scaleImageToSquareAspectRatio) {
builder.setLargeIcon(getBitmapWithSquareAspectRatio(player.getThumbnail()));
} else {
builder.setLargeIcon(player.getThumbnail());
}
}
private Bitmap getBitmapWithSquareAspectRatio(final Bitmap bitmap) {
return getResizedBitmap(bitmap, bitmap.getWidth(), bitmap.getWidth());
}
private Bitmap getResizedBitmap(final Bitmap bitmap, final int newWidth, final int newHeight) {
final int width = bitmap.getWidth();
final int height = bitmap.getHeight();
final float scaleWidth = ((float) newWidth) / width;
final float scaleHeight = ((float) newHeight) / height;
final Matrix matrix = new Matrix();
matrix.postScale(scaleWidth, scaleHeight);
return Bitmap.createBitmap(bitmap, 0, 0, width, height, matrix, false);
}
}

View file

@ -131,6 +131,8 @@ public abstract class VideoPlayer extends BasePlayer
private View controlsRoot;
private TextView currentDisplaySeek;
private View playerTopShadow;
private View playerBottomShadow;
private View bottomControlsRoot;
private SeekBar playbackSeekBar;
@ -193,6 +195,8 @@ public abstract class VideoPlayer extends BasePlayer
this.controlAnimationView = view.findViewById(R.id.controlAnimationView);
this.controlsRoot = view.findViewById(R.id.playbackControlRoot);
this.currentDisplaySeek = view.findViewById(R.id.currentDisplaySeek);
this.playerTopShadow = view.findViewById(R.id.playerTopShadow);
this.playerBottomShadow = view.findViewById(R.id.playerBottomShadow);
this.playbackSeekBar = view.findViewById(R.id.playbackSeekBar);
this.playbackCurrentTime = view.findViewById(R.id.playbackCurrentTime);
this.playbackEndTime = view.findViewById(R.id.playbackEndTime);
@ -359,11 +363,11 @@ public abstract class VideoPlayer extends BasePlayer
return true;
});
// apply caption language from previous user preference
if (userPreferredLanguage != null && (captionLanguage.equals(userPreferredLanguage)
|| searchForAutogenerated && captionLanguage.startsWith(userPreferredLanguage)
|| userPreferredLanguage.contains("(") && captionLanguage.startsWith(
userPreferredLanguage
.substring(0, userPreferredLanguage.indexOf('('))))) {
if (userPreferredLanguage != null
&& (captionLanguage.equals(userPreferredLanguage)
|| (searchForAutogenerated && captionLanguage.startsWith(userPreferredLanguage))
|| (userPreferredLanguage.contains("(") && captionLanguage.startsWith(
userPreferredLanguage.substring(0, userPreferredLanguage.indexOf('(')))))) {
final int textRendererIndex = getRendererIndex(C.TRACK_TYPE_TEXT);
if (textRendererIndex != RENDERER_UNAVAILABLE) {
trackSelector.setPreferredTextLanguage(captionLanguage);
@ -857,7 +861,6 @@ public abstract class VideoPlayer extends BasePlayer
}
qualityPopupMenu.show();
isSomePopupMenuVisible = true;
showControls(DEFAULT_CONTROLS_DURATION);
final VideoStream videoStream = getSelectedVideoStream();
if (videoStream != null) {
@ -875,7 +878,6 @@ public abstract class VideoPlayer extends BasePlayer
}
playbackSpeedPopupMenu.show();
isSomePopupMenuVisible = true;
showControls(DEFAULT_CONTROLS_DURATION);
}
private void onCaptionClicked() {
@ -884,7 +886,6 @@ public abstract class VideoPlayer extends BasePlayer
}
captionPopupMenu.show();
isSomePopupMenuVisible = true;
showControls(DEFAULT_CONTROLS_DURATION);
}
void onResizeClicked() {
@ -1061,6 +1062,7 @@ public abstract class VideoPlayer extends BasePlayer
? DEFAULT_CONTROLS_HIDE_TIME
: DPAD_CONTROLS_HIDE_TIME;
showHideShadow(true, DEFAULT_CONTROLS_DURATION, 0);
animateView(controlsRoot, true, DEFAULT_CONTROLS_DURATION, 0,
() -> hideControls(DEFAULT_CONTROLS_DURATION, hideTime));
}
@ -1070,6 +1072,7 @@ public abstract class VideoPlayer extends BasePlayer
Log.d(TAG, "showControls() called");
}
controlsVisibilityHandler.removeCallbacksAndMessages(null);
showHideShadow(true, duration, 0);
animateView(controlsRoot, true, duration);
}
@ -1089,8 +1092,10 @@ public abstract class VideoPlayer extends BasePlayer
Log.d(TAG, "hideControls() called with: delay = [" + delay + "]");
}
controlsVisibilityHandler.removeCallbacksAndMessages(null);
controlsVisibilityHandler.postDelayed(() ->
animateView(controlsRoot, false, duration), delay);
controlsVisibilityHandler.postDelayed(() -> {
showHideShadow(false, duration, 0);
animateView(controlsRoot, false, duration);
}, delay);
}
public void hideControlsAndButton(final long duration, final long delay, final View button) {
@ -1109,6 +1114,11 @@ public abstract class VideoPlayer extends BasePlayer
};
}
void showHideShadow(final boolean show, final long duration, final long delay) {
animateView(playerTopShadow, show, duration, delay, null);
animateView(playerBottomShadow, show, duration, delay, null);
}
public abstract void hideSystemUIIfNeeded();
/*//////////////////////////////////////////////////////////////////////////

View file

@ -27,22 +27,21 @@ import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.database.ContentObserver;
import android.graphics.Bitmap;
import android.graphics.Color;
import android.graphics.PixelFormat;
import android.graphics.Point;
import android.net.Uri;
import android.os.Build;
import android.os.Handler;
import android.view.DisplayCutout;
import androidx.preference.PreferenceManager;
import android.provider.Settings;
import android.util.DisplayMetrics;
import android.util.Log;
import android.util.TypedValue;
import android.view.Display;
import android.view.GestureDetector;
import android.view.Gravity;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.Surface;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
@ -57,7 +56,6 @@ import android.widget.RelativeLayout;
import android.widget.SeekBar;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
@ -67,6 +65,7 @@ import androidx.recyclerview.widget.RecyclerView;
import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.SimpleExoPlayer;
import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.text.CaptionStyleCompat;
import com.google.android.exoplayer2.ui.AspectRatioFrameLayout;
@ -107,7 +106,6 @@ import java.util.List;
import java.util.Set;
import static android.content.Context.WINDOW_SERVICE;
import static android.content.res.Configuration.ORIENTATION_LANDSCAPE;
import static org.schabi.newpipe.player.MainPlayer.ACTION_CLOSE;
import static org.schabi.newpipe.player.MainPlayer.ACTION_FAST_FORWARD;
import static org.schabi.newpipe.player.MainPlayer.ACTION_FAST_REWIND;
@ -115,10 +113,11 @@ import static org.schabi.newpipe.player.MainPlayer.ACTION_OPEN_CONTROLS;
import static org.schabi.newpipe.player.MainPlayer.ACTION_PLAY_NEXT;
import static org.schabi.newpipe.player.MainPlayer.ACTION_PLAY_PAUSE;
import static org.schabi.newpipe.player.MainPlayer.ACTION_PLAY_PREVIOUS;
import static org.schabi.newpipe.player.MainPlayer.ACTION_RECREATE_NOTIFICATION;
import static org.schabi.newpipe.player.MainPlayer.ACTION_REPEAT;
import static org.schabi.newpipe.player.MainPlayer.NOTIFICATION_ID;
import static org.schabi.newpipe.player.MainPlayer.ACTION_SHUFFLE;
import static org.schabi.newpipe.player.helper.PlayerHelper.MinimizeMode.MINIMIZE_ON_EXIT_MODE_BACKGROUND;
import static org.schabi.newpipe.player.helper.PlayerHelper.getTimeString;
import static org.schabi.newpipe.player.helper.PlayerHelper.globalScreenOrientationLocked;
import static org.schabi.newpipe.util.AnimationUtils.Type.SLIDE_AND_ALPHA;
import static org.schabi.newpipe.util.AnimationUtils.animateRotation;
import static org.schabi.newpipe.util.AnimationUtils.animateView;
@ -141,14 +140,12 @@ public class VideoPlayerImpl extends VideoPlayer
static final String POPUP_SAVED_WIDTH = "popup_saved_width";
static final String POPUP_SAVED_X = "popup_saved_x";
static final String POPUP_SAVED_Y = "popup_saved_y";
private static final int MINIMUM_SHOW_EXTRA_WIDTH_DP = 300;
private static final int IDLE_WINDOW_FLAGS = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
| WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM;
private static final int ONGOING_PLAYBACK_WINDOW_FLAGS = IDLE_WINDOW_FLAGS
| WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON;
private static final float MAX_GESTURE_LENGTH = 0.75f;
private static final int NOTIFICATION_UPDATES_BEFORE_RESET = 60;
private TextView titleTextView;
private TextView channelTextView;
@ -195,7 +192,6 @@ public class VideoPlayerImpl extends VideoPlayer
private boolean isVerticalVideo = false;
private boolean fragmentIsVisible = false;
boolean shouldUpdateOnProgress;
int timesNotificationUpdated;
private final MainPlayer service;
private PlayerServiceEventListener fragmentListener;
@ -206,9 +202,6 @@ public class VideoPlayerImpl extends VideoPlayer
@NonNull
private final AudioPlaybackResolver resolver;
private int cachedDuration;
private String cachedDurationString;
// Popup
private WindowManager.LayoutParams popupLayoutParams;
public WindowManager windowManager;
@ -263,6 +256,7 @@ public class VideoPlayerImpl extends VideoPlayer
} else {
getRootView().setVisibility(View.VISIBLE);
initVideoPlayer();
onQueueClosed();
// Android TV: without it focus will frame the whole player
playPauseButton.requestFocus();
}
@ -320,6 +314,9 @@ public class VideoPlayerImpl extends VideoPlayer
titleTextView.setSelected(true);
channelTextView.setSelected(true);
// Prevent hiding of bottom sheet via swipe inside queue
this.itemsList.setNestedScrollingEnabled(false);
}
@Override
@ -344,7 +341,6 @@ public class VideoPlayerImpl extends VideoPlayer
* This method ensures that popup and main players have different look.
* We use one layout for both players and need to decide what to show and what to hide.
* Additional measuring should be done inside {@link #setupElementsSize}.
* {@link #setControlsSize} is used to adapt the UI to fullscreen mode, multiWindow, navBar, etc
*/
private void setupElementsVisibility() {
if (popupPlayerSelected()) {
@ -497,6 +493,17 @@ public class VideoPlayerImpl extends VideoPlayer
Settings.System.getUriFor(Settings.System.ACCELEROMETER_ROTATION), false,
settingsContentObserver);
getRootView().addOnLayoutChangeListener(this);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
queueLayout.setOnApplyWindowInsetsListener((view, windowInsets) -> {
final DisplayCutout cutout = windowInsets.getDisplayCutout();
if (cutout != null) {
view.setPadding(cutout.getSafeInsetLeft(), cutout.getSafeInsetTop(),
cutout.getSafeInsetRight(), cutout.getSafeInsetBottom());
}
return windowInsets;
});
}
}
public boolean onKeyDown(final int keyCode) {
@ -594,29 +601,32 @@ public class VideoPlayerImpl extends VideoPlayer
setupScreenRotationButton();
}
/*//////////////////////////////////////////////////////////////////////////
// ExoPlayer Video Listener
//////////////////////////////////////////////////////////////////////////*/
/*//////////////////////////////////////////////////////////////////////////
// ExoPlayer Video Listener
//////////////////////////////////////////////////////////////////////////*/
void onShuffleOrRepeatModeChanged() {
updatePlaybackButtons();
updatePlayback();
NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, false);
}
@Override
public void onRepeatModeChanged(final int i) {
super.onRepeatModeChanged(i);
updatePlaybackButtons();
updatePlayback();
service.resetNotification();
service.updateNotification(-1);
onShuffleOrRepeatModeChanged();
}
@Override
public void onShuffleClicked() {
super.onShuffleClicked();
updatePlaybackButtons();
updatePlayback();
onShuffleOrRepeatModeChanged();
}
/*//////////////////////////////////////////////////////////////////////////
// Playback Listener
//////////////////////////////////////////////////////////////////////////*/
/*//////////////////////////////////////////////////////////////////////////
// Playback Listener
//////////////////////////////////////////////////////////////////////////*/
@Override
public void onPlayerError(final ExoPlaybackException error) {
@ -627,6 +637,13 @@ public class VideoPlayerImpl extends VideoPlayer
}
}
@Override
public void onTimelineChanged(final Timeline timeline, final int reason) {
super.onTimelineChanged(timeline, reason);
// force recreate notification to ensure seek bar is shown when preparation finishes
NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, true);
}
protected void onMetadataChanged(@NonNull final MediaSourceTag tag) {
super.onMetadataChanged(tag);
@ -637,8 +654,7 @@ public class VideoPlayerImpl extends VideoPlayer
titleTextView.setText(tag.getMetadata().getName());
channelTextView.setText(tag.getMetadata().getUploaderName());
service.resetNotification();
service.updateNotification(-1);
NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, false);
updateMetadata();
}
@ -680,35 +696,17 @@ public class VideoPlayerImpl extends VideoPlayer
public void onUpdateProgress(final int currentProgress,
final int duration, final int bufferPercent) {
super.onUpdateProgress(currentProgress, duration, bufferPercent);
updateProgress(currentProgress, duration, bufferPercent);
if (!shouldUpdateOnProgress || getCurrentState() == BasePlayer.STATE_COMPLETED
|| getCurrentState() == BasePlayer.STATE_PAUSED || getPlayQueue() == null) {
return;
}
// setMetadata only updates the metadata when any of the metadata keys are null
mediaSessionManager.setMetadata(getVideoTitle(), getUploaderName(), getThumbnail(),
duration);
}
if (timesNotificationUpdated > NOTIFICATION_UPDATES_BEFORE_RESET) {
service.resetNotification();
}
if (service.getBigNotRemoteView() != null) {
if (cachedDuration != duration) {
cachedDuration = duration;
cachedDurationString = getTimeString(duration);
}
service.getBigNotRemoteView()
.setProgressBar(R.id.notificationProgressBar,
duration, currentProgress, false);
service.getBigNotRemoteView()
.setTextViewText(R.id.notificationTime,
getTimeString(currentProgress) + " / " + cachedDurationString);
}
if (service.getNotRemoteView() != null) {
service.getNotRemoteView()
.setProgressBar(R.id.notificationProgressBar, duration, currentProgress, false);
}
service.updateNotification(-1);
@Override
public void onPlayQueueEdited() {
updatePlayback();
NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, false);
}
@Override
@ -746,53 +744,33 @@ public class VideoPlayerImpl extends VideoPlayer
}
/*//////////////////////////////////////////////////////////////////////////
// Player Overrides
//////////////////////////////////////////////////////////////////////////*/
// Player Overrides
//////////////////////////////////////////////////////////////////////////*/
@Override
public void toggleFullscreen() {
if (DEBUG) {
Log.d(TAG, "toggleFullscreen() called");
}
if (simpleExoPlayer == null || getCurrentMetadata() == null) {
if (popupPlayerSelected()
|| simpleExoPlayer == null
|| getCurrentMetadata() == null
|| fragmentListener == null) {
return;
}
if (popupPlayerSelected()) {
setRecovery();
service.removeViewFromParent();
final Intent intent = NavigationHelper.getPlayerIntent(
service,
MainActivity.class,
this.getPlayQueue(),
this.getRepeatMode(),
this.getPlaybackSpeed(),
this.getPlaybackPitch(),
this.getPlaybackSkipSilence(),
null,
true,
!isPlaying(),
isMuted()
);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.putExtra(Constants.KEY_SERVICE_ID,
getCurrentMetadata().getMetadata().getServiceId());
intent.putExtra(Constants.KEY_LINK_TYPE, StreamingService.LinkType.STREAM);
intent.putExtra(Constants.KEY_URL, getVideoUrl());
intent.putExtra(Constants.KEY_TITLE, getVideoTitle());
intent.putExtra(VideoDetailFragment.AUTO_PLAY, true);
service.onDestroy();
context.startActivity(intent);
return;
isFullscreen = !isFullscreen;
if (!isFullscreen) {
// Apply window insets because Android will not do it when orientation changes
// from landscape to portrait (open vertical video to reproduce)
getControlsRoot().setPadding(0, 0, 0, 0);
} else {
if (fragmentListener == null) {
return;
}
isFullscreen = !isFullscreen;
setControlsSize();
fragmentListener.onFullscreenStateChanged(isFullscreen());
// Android needs tens milliseconds to send new insets but a user is able to see
// how controls changes it's position from `0` to `nav bar height` padding.
// So just hide the controls to hide this visual inconsistency
hideControls(0, 0);
}
fragmentListener.onFullscreenStateChanged(isFullscreen());
if (!isFullscreen()) {
titleTextView.setVisibility(View.GONE);
@ -806,6 +784,40 @@ public class VideoPlayerImpl extends VideoPlayer
setupScreenRotationButton();
}
public void switchFromPopupToMain() {
if (DEBUG) {
Log.d(TAG, "switchFromPopupToMain() called");
}
if (!popupPlayerSelected() || simpleExoPlayer == null || getCurrentMetadata() == null) {
return;
}
setRecovery();
service.removeViewFromParent();
final Intent intent = NavigationHelper.getPlayerIntent(
service,
MainActivity.class,
this.getPlayQueue(),
this.getRepeatMode(),
this.getPlaybackSpeed(),
this.getPlaybackPitch(),
this.getPlaybackSkipSilence(),
null,
true,
!isPlaying(),
isMuted()
);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.putExtra(Constants.KEY_SERVICE_ID,
getCurrentMetadata().getMetadata().getServiceId());
intent.putExtra(Constants.KEY_LINK_TYPE, StreamingService.LinkType.STREAM);
intent.putExtra(Constants.KEY_URL, getVideoUrl());
intent.putExtra(Constants.KEY_TITLE, getVideoTitle());
intent.putExtra(VideoDetailFragment.AUTO_PLAY, true);
service.onDestroy();
context.startActivity(intent);
}
@Override
public void onClick(final View v) {
super.onClick(v);
@ -833,9 +845,12 @@ public class VideoPlayerImpl extends VideoPlayer
} else if (v.getId() == openInBrowser.getId()) {
onOpenInBrowserClicked();
} else if (v.getId() == fullscreenButton.getId()) {
toggleFullscreen();
switchFromPopupToMain();
} else if (v.getId() == screenRotationButton.getId()) {
if (!isVerticalVideo) {
// Only if it's not a vertical video or vertical video but in landscape with locked
// orientation a screen orientation can be changed automatically
if (!isVerticalVideo
|| (service.isLandscape() && globalScreenOrientationLocked(service))) {
fragmentListener.onScreenRotationButtonClicked();
} else {
toggleFullscreen();
@ -850,9 +865,12 @@ public class VideoPlayerImpl extends VideoPlayer
if (getCurrentState() != STATE_COMPLETED) {
getControlsVisibilityHandler().removeCallbacksAndMessages(null);
showHideShadow(true, DEFAULT_CONTROLS_DURATION, 0);
animateView(getControlsRoot(), true, DEFAULT_CONTROLS_DURATION, 0, () -> {
if (getCurrentState() == STATE_PLAYING && !isSomePopupMenuVisible()) {
if (v.getId() == playPauseButton.getId()) {
if (v.getId() == playPauseButton.getId()
// Hide controls in fullscreen immediately
|| (v.getId() == screenRotationButton.getId() && isFullscreen)) {
hideControls(0, 0);
} else {
hideControls(DEFAULT_CONTROLS_DURATION, DEFAULT_CONTROLS_HIDE_TIME);
@ -918,7 +936,7 @@ public class VideoPlayerImpl extends VideoPlayer
buildQueue();
updatePlaybackButtons();
getControlsRoot().setVisibility(View.INVISIBLE);
hideControls(0, 0);
queueLayout.requestFocus();
animateView(queueLayout, SLIDE_AND_ALPHA, true,
DEFAULT_CONTROLS_DURATION);
@ -1010,9 +1028,8 @@ public class VideoPlayerImpl extends VideoPlayer
private void setupScreenRotationButton() {
final boolean orientationLocked = PlayerHelper.globalScreenOrientationLocked(service);
final boolean tabletInLandscape = DeviceUtils.isTablet(service) && service.isLandscape();
final boolean showButton = videoPlayerSelected()
&& (orientationLocked || isVerticalVideo || tabletInLandscape);
&& (orientationLocked || isVerticalVideo || DeviceUtils.isTablet(service));
screenRotationButton.setVisibility(showButton ? View.VISIBLE : View.GONE);
screenRotationButton.setImageDrawable(AppCompatResources.getDrawable(service, isFullscreen()
? R.drawable.ic_fullscreen_exit_white_24dp
@ -1024,6 +1041,8 @@ public class VideoPlayerImpl extends VideoPlayer
if (orientationLocked
&& isFullscreen()
&& service.isLandscape() == isVerticalVideo
&& !DeviceUtils.isTv(service)
&& !DeviceUtils.isTablet(service)
&& fragmentListener != null) {
fragmentListener.onScreenRotationButtonClicked();
}
@ -1054,6 +1073,7 @@ public class VideoPlayerImpl extends VideoPlayer
super.onDismiss(menu);
if (isPlaying()) {
hideControls(DEFAULT_CONTROLS_DURATION, 0);
hideSystemUIIfNeeded();
}
}
@ -1078,15 +1098,6 @@ public class VideoPlayerImpl extends VideoPlayer
setInitialGestureValues();
queueLayout.getLayoutParams().height = height - queueLayout.getTop();
if (popupPlayerSelected()) {
final float widthDp = Math.abs(r - l) / service.getResources()
.getDisplayMetrics().density;
final int visibility = widthDp > MINIMUM_SHOW_EXTRA_WIDTH_DP
? View.VISIBLE
: View.GONE;
secondaryControls.setVisibility(visibility);
}
}
}
@ -1169,8 +1180,7 @@ public class VideoPlayerImpl extends VideoPlayer
animatePlayButtons(false, 100);
getRootView().setKeepScreenOn(false);
service.resetNotification();
service.updateNotification(R.drawable.exo_controls_play);
NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, false);
}
@Override
@ -1178,8 +1188,9 @@ public class VideoPlayerImpl extends VideoPlayer
super.onBuffering();
getRootView().setKeepScreenOn(true);
service.resetNotification();
service.updateNotification(R.drawable.exo_controls_play);
if (NotificationUtil.getInstance().shouldUpdateBufferingSlot()) {
NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, false);
}
}
@Override
@ -1197,10 +1208,7 @@ public class VideoPlayerImpl extends VideoPlayer
checkLandscape();
getRootView().setKeepScreenOn(true);
service.resetNotification();
service.updateNotification(R.drawable.exo_controls_pause);
service.startForeground(NOTIFICATION_ID, service.getNotBuilder().build());
NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, false);
}
@Override
@ -1216,13 +1224,12 @@ public class VideoPlayerImpl extends VideoPlayer
updateWindowFlags(IDLE_WINDOW_FLAGS);
service.resetNotification();
service.updateNotification(R.drawable.exo_controls_play);
// Remove running notification when user don't want music (or video in popup)
// to be played in background
if (!minimizeOnPopupEnabled() && !backgroundPlaybackEnabled() && videoPlayerSelected()) {
service.stopForeground(true);
NotificationUtil.getInstance().cancelNotificationAndStopForeground(service);
} else {
NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, false);
}
getRootView().setKeepScreenOn(false);
@ -1234,8 +1241,7 @@ public class VideoPlayerImpl extends VideoPlayer
animatePlayButtons(false, 100);
getRootView().setKeepScreenOn(true);
service.resetNotification();
service.updateNotification(R.drawable.exo_controls_play);
NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, false);
}
@ -1245,20 +1251,20 @@ public class VideoPlayerImpl extends VideoPlayer
playPauseButton.setImageResource(R.drawable.ic_replay_white_24dp);
animatePlayButtons(true, DEFAULT_CONTROLS_DURATION);
});
getRootView().setKeepScreenOn(false);
getRootView().setKeepScreenOn(false);
updateWindowFlags(IDLE_WINDOW_FLAGS);
service.resetNotification();
service.updateNotification(R.drawable.ic_replay_white_24dp);
NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, false);
if (isFullscreen) {
toggleFullscreen();
}
super.onCompleted();
}
@Override
public void destroy() {
super.destroy();
service.getContentResolver().unregisterContentObserver(settingsContentObserver);
}
@ -1282,6 +1288,8 @@ public class VideoPlayerImpl extends VideoPlayer
intentFilter.addAction(ACTION_PLAY_NEXT);
intentFilter.addAction(ACTION_FAST_REWIND);
intentFilter.addAction(ACTION_FAST_FORWARD);
intentFilter.addAction(ACTION_SHUFFLE);
intentFilter.addAction(ACTION_RECREATE_NOTIFICATION);
intentFilter.addAction(VideoDetailFragment.ACTION_VIDEO_FRAGMENT_RESUMED);
intentFilter.addAction(VideoDetailFragment.ACTION_VIDEO_FRAGMENT_STOPPED);
@ -1331,6 +1339,17 @@ public class VideoPlayerImpl extends VideoPlayer
case ACTION_REPEAT:
onRepeatClicked();
break;
case ACTION_SHUFFLE:
onShuffleClicked();
break;
case ACTION_RECREATE_NOTIFICATION:
NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, true);
break;
case Intent.ACTION_HEADSET_PLUG: //FIXME
/*notificationManager.cancel(NOTIFICATION_ID);
mediaSessionManager.dispose();
mediaSessionManager.enable(getBaseContext(), basePlayerImpl.simpleExoPlayer);*/
break;
case VideoDetailFragment.ACTION_VIDEO_FRAGMENT_RESUMED:
fragmentIsVisible = true;
useVideoSource(true);
@ -1349,20 +1368,6 @@ public class VideoPlayerImpl extends VideoPlayer
updatePopupSize(getPopupLayoutParams().width, -1);
checkPopupPositionBounds();
}
// The only situation I need to re-calculate elements sizes is
// when a user rotates a device from landscape to landscape
// because in that case the controls should be aligned to another side of a screen.
// The problem is when user leaves the app and returns back
// (while the app in landscape) Android reports via DisplayMetrics that orientation
// is portrait and it gives wrong sizes calculations.
// Let's skip re-calculation in every case but landscape
final boolean reportedOrientationIsLandscape = service.isLandscape();
final boolean actualOrientationIsLandscape = context.getResources()
.getConfiguration().orientation == ORIENTATION_LANDSCAPE;
if (reportedOrientationIsLandscape && actualOrientationIsLandscape) {
setControlsSize();
}
// Close it because when changing orientation from portrait
// (in fullscreen mode) the size of queue layout can be larger than the screen size
onQueueClosed();
@ -1372,23 +1377,18 @@ public class VideoPlayerImpl extends VideoPlayer
// Interrupt playback only when screen turns on
// and user is watching video in popup player.
// Same actions for video player will be handled in ACTION_VIDEO_FRAGMENT_RESUMED
if (backgroundPlaybackEnabled()
&& popupPlayerSelected()
&& (isPlaying() || isLoading())) {
if (popupPlayerSelected() && (isPlaying() || isLoading())) {
useVideoSource(true);
}
break;
case Intent.ACTION_SCREEN_OFF:
shouldUpdateOnProgress = false;
// Interrupt playback only when screen turns off with popup player working
if (backgroundPlaybackEnabled()
&& popupPlayerSelected()
&& (isPlaying() || isLoading())) {
if (popupPlayerSelected() && (isPlaying() || isLoading())) {
useVideoSource(false);
}
break;
}
service.resetNotification();
}
/*//////////////////////////////////////////////////////////////////////////
@ -1400,10 +1400,7 @@ public class VideoPlayerImpl extends VideoPlayer
final View view,
final Bitmap loadedImage) {
super.onLoadingComplete(imageUri, view, loadedImage);
// rebuild notification here since remote view does not release bitmaps,
// causing memory leaks
service.resetNotification();
service.updateNotification(-1);
NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, false);
}
@Override
@ -1411,20 +1408,18 @@ public class VideoPlayerImpl extends VideoPlayer
final View view,
final FailReason failReason) {
super.onLoadingFailed(imageUri, view, failReason);
service.resetNotification();
service.updateNotification(-1);
NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, false);
}
@Override
public void onLoadingCancelled(final String imageUri, final View view) {
super.onLoadingCancelled(imageUri, view);
service.resetNotification();
service.updateNotification(-1);
NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, false);
}
/*//////////////////////////////////////////////////////////////////////////
// Utils
//////////////////////////////////////////////////////////////////////////*/
/*//////////////////////////////////////////////////////////////////////////
// Utils
//////////////////////////////////////////////////////////////////////////*/
private void setInitialGestureValues() {
if (getAudioReactor() != null) {
@ -1526,9 +1521,10 @@ public class VideoPlayerImpl extends VideoPlayer
showOrHideButtons();
getControlsVisibilityHandler().removeCallbacksAndMessages(null);
getControlsVisibilityHandler().postDelayed(() ->
animateView(getControlsRoot(), false, duration, 0,
this::hideSystemUIIfNeeded), delay
getControlsVisibilityHandler().postDelayed(() -> {
showHideShadow(false, duration, 0);
animateView(getControlsRoot(), false, duration, 0, this::hideSystemUIIfNeeded);
}, delay
);
}
@ -1556,11 +1552,17 @@ public class VideoPlayerImpl extends VideoPlayer
}
private void showSystemUIPartially() {
if (isFullscreen() && getParentActivity() != null) {
final AppCompatActivity activity = getParentActivity();
if (isFullscreen() && activity != null) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
activity.getWindow().setStatusBarColor(Color.TRANSPARENT);
activity.getWindow().setNavigationBarColor(Color.TRANSPARENT);
}
final int visibility = View.SYSTEM_UI_FLAG_LAYOUT_STABLE
| View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION;
getParentActivity().getWindow().getDecorView().setSystemUiVisibility(visibility);
activity.getWindow().getDecorView().setSystemUiVisibility(visibility);
activity.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
}
}
@ -1571,90 +1573,8 @@ public class VideoPlayerImpl extends VideoPlayer
}
}
/**
* Measures width and height of controls visible on screen.
* It ensures that controls will be side-by-side with NavigationBar and notches
* but not under them. Tablets have only bottom NavigationBar
*/
public void setControlsSize() {
final Point size = new Point();
final Display display = getRootView().getDisplay();
if (display == null || !videoPlayerSelected()) {
return;
}
// This method will give a correct size of a usable area of a window.
// It doesn't include NavigationBar, notches, etc.
display.getSize(size);
final boolean isLandscape = service.isLandscape();
final int width = isFullscreen
? (isLandscape ? size.x : size.y)
: ViewGroup.LayoutParams.MATCH_PARENT;
final int gravity = isFullscreen
? (display.getRotation() == Surface.ROTATION_90
? Gravity.START : Gravity.END)
: Gravity.TOP;
getTopControlsRoot().getLayoutParams().width = width;
final RelativeLayout.LayoutParams topParams =
((RelativeLayout.LayoutParams) getTopControlsRoot().getLayoutParams());
topParams.removeRule(RelativeLayout.ALIGN_PARENT_START);
topParams.removeRule(RelativeLayout.ALIGN_PARENT_END);
topParams.addRule(gravity == Gravity.END
? RelativeLayout.ALIGN_PARENT_END
: RelativeLayout.ALIGN_PARENT_START);
getTopControlsRoot().requestLayout();
getBottomControlsRoot().getLayoutParams().width = width;
final RelativeLayout.LayoutParams bottomParams =
((RelativeLayout.LayoutParams) getBottomControlsRoot().getLayoutParams());
bottomParams.removeRule(RelativeLayout.ALIGN_PARENT_START);
bottomParams.removeRule(RelativeLayout.ALIGN_PARENT_END);
bottomParams.addRule(gravity == Gravity.END
? RelativeLayout.ALIGN_PARENT_END
: RelativeLayout.ALIGN_PARENT_START);
getBottomControlsRoot().requestLayout();
final ViewGroup controlsRoot = getRootView().findViewById(R.id.playbackWindowRoot);
// In tablet navigationBar located at the bottom of the screen.
// And the situations when we need to set custom height is
// in fullscreen mode in tablet in non-multiWindow mode or with vertical video.
// Other than that MATCH_PARENT is good
final boolean navBarAtTheBottom = DeviceUtils.isTablet(service) || !isLandscape;
controlsRoot.getLayoutParams().height = isFullscreen && !isInMultiWindow()
&& navBarAtTheBottom ? size.y : ViewGroup.LayoutParams.MATCH_PARENT;
controlsRoot.requestLayout();
final DisplayMetrics metrics = getRootView().getResources().getDisplayMetrics();
int topPadding = isFullscreen && !isInMultiWindow() ? getStatusBarHeight() : 0;
topPadding = !isLandscape && DeviceUtils.hasCutout(topPadding, metrics) ? 0 : topPadding;
getRootView().findViewById(R.id.playbackWindowRoot).setTranslationY(topPadding);
getBottomControlsRoot().setTranslationY(-topPadding);
}
/**
* @return statusBar height that was found inside system resources
* or default value if no value was provided inside resources
*/
private int getStatusBarHeight() {
int statusBarHeight = 0;
final int resourceId = service.isLandscape()
? service.getResources().getIdentifier(
"status_bar_height_landscape", "dimen", "android")
: service.getResources().getIdentifier(
"status_bar_height", "dimen", "android");
if (resourceId > 0) {
statusBarHeight = service.getResources().getDimensionPixelSize(resourceId);
}
if (statusBarHeight == 0) {
// Some devices provide wrong value for status bar height in landscape mode,
// this is workaround
final DisplayMetrics metrics = getRootView().getResources().getDisplayMetrics();
statusBarHeight = (int) TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP, 24, metrics);
}
return statusBarHeight;
public void disablePreloadingOfCurrentTrack() {
getLoadController().disablePreloadingOfCurrentTrack();
}
protected void setMuteButton(final ImageButton button, final boolean isMuted) {
@ -1724,8 +1644,6 @@ public class VideoPlayerImpl extends VideoPlayer
&& !DeviceUtils.isTablet(service)) {
toggleFullscreen();
}
setControlsSize();
}
private void buildQueue() {
@ -2122,6 +2040,12 @@ public class VideoPlayerImpl extends VideoPlayer
public void setFragmentListener(final PlayerServiceEventListener listener) {
fragmentListener = listener;
fragmentIsVisible = true;
// Apply window insets because Android will not do it when orientation changes
// from landscape to portrait
if (!isFullscreen) {
getControlsRoot().setPadding(0, 0, 0, 0);
}
queueLayout.setPadding(0, 0, 0, 0);
updateMetadata();
updatePlayback();
triggerProgressUpdate();

View file

@ -39,12 +39,13 @@ public class CustomBottomSheetBehavior extends BottomSheetBehavior<FrameLayout>
}
// Found that user still swiping, continue following
if (skippingInterception) {
if (skippingInterception || getState() == BottomSheetBehavior.STATE_SETTLING) {
return false;
}
// Don't need to do anything if bottomSheet isn't expanded
if (getState() == BottomSheetBehavior.STATE_EXPANDED) {
if (getState() == BottomSheetBehavior.STATE_EXPANDED
&& event.getAction() == MotionEvent.ACTION_DOWN) {
// Without overriding scrolling will not work when user touches these elements
for (final Integer element : skipInterceptionOfElements) {
final ViewGroup viewGroup = child.findViewById(element);

View file

@ -9,6 +9,7 @@ import android.view.View;
import android.view.ViewConfiguration;
import android.view.Window;
import android.view.WindowManager;
import android.widget.ProgressBar;
import androidx.appcompat.content.res.AppCompatResources;
import org.schabi.newpipe.R;
import org.schabi.newpipe.player.BasePlayer;
@ -264,14 +265,19 @@ public class PlayerGestureListener
}
final Window window = parent.getWindow();
playerImpl.getBrightnessProgressBar().incrementProgressBy((int) distanceY);
final float currentProgressPercent = (float) playerImpl.getBrightnessProgressBar()
.getProgress() / playerImpl.getMaxGestureLength();
final WindowManager.LayoutParams layoutParams = window.getAttributes();
final ProgressBar bar = playerImpl.getBrightnessProgressBar();
final float oldBrightness = layoutParams.screenBrightness;
bar.setProgress((int) (bar.getMax() * Math.max(0, Math.min(1, oldBrightness))));
bar.incrementProgressBy((int) distanceY);
final float currentProgressPercent = (float) bar.getProgress() / bar.getMax();
layoutParams.screenBrightness = currentProgressPercent;
window.setAttributes(layoutParams);
// Save current brightness level
PlayerHelper.setScreenBrightness(parent, currentProgressPercent);
if (DEBUG) {
Log.d(TAG, "onScroll().brightnessControl, "
+ "currentBrightness = " + currentProgressPercent);

View file

@ -13,6 +13,7 @@ public class LoadController implements LoadControl {
private final long initialPlaybackBufferUs;
private final LoadControl internalLoadControl;
private boolean preloadingEnabled = true;
/*//////////////////////////////////////////////////////////////////////////
// Default Load Control
@ -41,6 +42,7 @@ public class LoadController implements LoadControl {
@Override
public void onPrepared() {
preloadingEnabled = true;
internalLoadControl.onPrepared();
}
@ -52,11 +54,13 @@ public class LoadController implements LoadControl {
@Override
public void onStopped() {
preloadingEnabled = true;
internalLoadControl.onStopped();
}
@Override
public void onReleased() {
preloadingEnabled = true;
internalLoadControl.onReleased();
}
@ -78,6 +82,9 @@ public class LoadController implements LoadControl {
@Override
public boolean shouldContinueLoading(final long bufferedDurationUs,
final float playbackSpeed) {
if (!preloadingEnabled) {
return false;
}
return internalLoadControl.shouldContinueLoading(bufferedDurationUs, playbackSpeed);
}
@ -90,4 +97,8 @@ public class LoadController implements LoadControl {
.shouldStartPlayback(bufferedDurationUs, playbackSpeed, rebuffering);
return isInitialPlaybackBufferFilled || isInternalStartingPlayback;
}
public void disablePreloadingOfCurrentTrack() {
preloadingEnabled = false;
}
}

View file

@ -3,44 +3,58 @@ package org.schabi.newpipe.player.helper;
import android.content.Context;
import android.content.Intent;
import android.graphics.Bitmap;
import android.media.MediaMetadata;
import android.os.Build;
import android.support.v4.media.MediaMetadataCompat;
import android.support.v4.media.session.MediaSessionCompat;
import android.support.v4.media.session.PlaybackStateCompat;
import android.util.Log;
import android.view.KeyEvent;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.core.app.NotificationCompat;
import androidx.media.app.NotificationCompat.MediaStyle;
import androidx.media.session.MediaButtonReceiver;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector;
import org.schabi.newpipe.MainActivity;
import org.schabi.newpipe.player.mediasession.MediaSessionCallback;
import org.schabi.newpipe.player.mediasession.PlayQueueNavigator;
import org.schabi.newpipe.player.mediasession.PlayQueuePlaybackController;
public class MediaSessionManager {
private static final String TAG = "MediaSessionManager";
private static final String TAG = MediaSessionManager.class.getSimpleName();
public static final boolean DEBUG = MainActivity.DEBUG;
@NonNull
private final MediaSessionCompat mediaSession;
@NonNull
private final MediaSessionConnector sessionConnector;
private int lastAlbumArtHashCode;
public MediaSessionManager(@NonNull final Context context,
@NonNull final Player player,
@NonNull final MediaSessionCallback callback) {
this.mediaSession = new MediaSessionCompat(context, TAG);
this.mediaSession.setActive(true);
mediaSession = new MediaSessionCompat(context, TAG);
mediaSession.setFlags(MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS
| MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS);
mediaSession.setActive(true);
this.sessionConnector = new MediaSessionConnector(mediaSession);
this.sessionConnector.setControlDispatcher(new PlayQueuePlaybackController(callback));
this.sessionConnector.setQueueNavigator(new PlayQueueNavigator(mediaSession, callback));
this.sessionConnector.setPlayer(player);
mediaSession.setPlaybackState(new PlaybackStateCompat.Builder()
.setState(PlaybackStateCompat.STATE_NONE, -1, 1)
.setActions(PlaybackStateCompat.ACTION_SEEK_TO
| PlaybackStateCompat.ACTION_PLAY
| PlaybackStateCompat.ACTION_PAUSE // was play and pause now play/pause
| PlaybackStateCompat.ACTION_SKIP_TO_NEXT
| PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS
| PlaybackStateCompat.ACTION_SET_REPEAT_MODE
| PlaybackStateCompat.ACTION_STOP)
.build());
sessionConnector = new MediaSessionConnector(mediaSession);
sessionConnector.setControlDispatcher(new PlayQueuePlaybackController(callback));
sessionConnector.setQueueNavigator(new PlayQueueNavigator(mediaSession, callback));
sessionConnector.setPlayer(player);
}
@Nullable
@ -49,46 +63,78 @@ public class MediaSessionManager {
return MediaButtonReceiver.handleIntent(mediaSession, intent);
}
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
public void setLockScreenArt(final NotificationCompat.Builder builder,
@Nullable final Bitmap thumbnailBitmap) {
if (thumbnailBitmap == null || !mediaSession.isActive()) {
public MediaSessionCompat.Token getSessionToken() {
return mediaSession.getSessionToken();
}
public void setMetadata(final String title,
final String artist,
final Bitmap albumArt,
final long duration) {
if (albumArt == null || !mediaSession.isActive()) {
return;
}
mediaSession.setMetadata(
new MediaMetadataCompat.Builder()
.putBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART, thumbnailBitmap)
.build()
);
if (DEBUG) {
if (getMetadataAlbumArt() == null) {
Log.d(TAG, "N_getMetadataAlbumArt: thumb == null");
}
if (getMetadataTitle() == null) {
Log.d(TAG, "N_getMetadataTitle: title == null");
}
if (getMetadataArtist() == null) {
Log.d(TAG, "N_getMetadataArtist: artist == null");
}
if (getMetadataDuration() <= 1) {
Log.d(TAG, "N_getMetadataDuration: duration <= 1; " + getMetadataDuration());
}
}
final MediaStyle mediaStyle = new MediaStyle()
.setMediaSession(mediaSession.getSessionToken());
if (getMetadataAlbumArt() == null || getMetadataTitle() == null
|| getMetadataArtist() == null || getMetadataDuration() <= 1
|| albumArt.hashCode() != lastAlbumArtHashCode) {
if (DEBUG) {
Log.d(TAG, "setMetadata: N_Metadata update: t: " + title + " a: " + artist
+ " thumb: " + albumArt.hashCode() + " d: " + duration);
}
builder.setStyle(mediaStyle);
mediaSession.setMetadata(new MediaMetadataCompat.Builder()
.putString(MediaMetadataCompat.METADATA_KEY_TITLE, title)
.putString(MediaMetadataCompat.METADATA_KEY_ARTIST, artist)
.putBitmap(MediaMetadataCompat.METADATA_KEY_ALBUM_ART, albumArt)
.putBitmap(MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON, albumArt)
.putLong(MediaMetadataCompat.METADATA_KEY_DURATION, duration).build());
lastAlbumArtHashCode = albumArt.hashCode();
}
}
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
public void clearLockScreenArt(final NotificationCompat.Builder builder) {
mediaSession.setMetadata(
new MediaMetadataCompat.Builder()
.putBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART, null)
.build()
);
private Bitmap getMetadataAlbumArt() {
return mediaSession.getController().getMetadata()
.getBitmap(MediaMetadataCompat.METADATA_KEY_ALBUM_ART);
}
final MediaStyle mediaStyle = new MediaStyle()
.setMediaSession(mediaSession.getSessionToken());
private String getMetadataTitle() {
return mediaSession.getController().getMetadata()
.getString(MediaMetadataCompat.METADATA_KEY_TITLE);
}
builder.setStyle(mediaStyle);
private String getMetadataArtist() {
return mediaSession.getController().getMetadata()
.getString(MediaMetadataCompat.METADATA_KEY_ARTIST);
}
private long getMetadataDuration() {
return mediaSession.getController().getMetadata()
.getLong(MediaMetadataCompat.METADATA_KEY_DURATION);
}
/**
* Should be called on player destruction to prevent leakage.
*/
public void dispose() {
this.sessionConnector.setPlayer(null);
this.sessionConnector.setQueueNavigator(null);
this.mediaSession.setActive(false);
this.mediaSession.release();
sessionConnector.setPlayer(null);
sessionConnector.setQueueNavigator(null);
mediaSession.setActive(false);
mediaSession.release();
}
}

View file

@ -28,6 +28,7 @@ import org.schabi.newpipe.extractor.stream.VideoStream;
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 java.lang.annotation.Retention;
import java.text.DecimalFormat;
@ -248,6 +249,18 @@ public final class PlayerHelper {
}
}
public static boolean isAutoplayAllowedByUser(@NonNull final Context context) {
switch (PlayerHelper.getAutoplayType(context)) {
case PlayerHelper.AutoplayType.AUTOPLAY_TYPE_NEVER:
return false;
case PlayerHelper.AutoplayType.AUTOPLAY_TYPE_WIFI:
return !ListHelper.isMeteredNetwork(context);
case PlayerHelper.AutoplayType.AUTOPLAY_TYPE_ALWAYS:
default:
return true;
}
}
@NonNull
public static SeekParameters getSeekParameters(@NonNull final Context context) {
return isUsingInexactSeek(context) ? SeekParameters.CLOSEST_SYNC : SeekParameters.EXACT;

View file

@ -255,21 +255,21 @@ public class MediaSourceManager {
// Loading and Syncing
switch (event.type()) {
case INIT:
case REORDER:
case ERROR:
case SELECT:
case INIT: case REORDER: case ERROR: case SELECT:
loadImmediate(); // low frequency, critical events
break;
case APPEND:
case REMOVE:
case MOVE:
case RECOVERY:
case APPEND: case REMOVE: case MOVE: case RECOVERY:
default:
loadDebounced(); // high frequency or noncritical events
break;
}
// update ui and notification
switch (event.type()) {
case APPEND: case REMOVE: case MOVE: case REORDER:
playbackListener.onPlayQueueEdited();
}
if (!isPlayQueueReady()) {
maybeBlock();
playQueue.fetch();

View file

@ -69,7 +69,7 @@ public interface PlaybackListener {
MediaSource sourceOf(PlayQueueItem item, StreamInfo info);
/**
* Called when the play queue can no longer to played or used.
* Called when the play queue can no longer be played or used.
* Currently, this means the play queue is empty and complete.
* Signals to the listener that it should shutdown.
* <p>
@ -77,4 +77,13 @@ public interface PlaybackListener {
* </p>
*/
void onPlaybackShutdown();
/**
* Called whenever the play queue was edited (items were added, deleted or moved),
* use this to e.g. update notification buttons or fragment ui.
* <p>
* May be called at any time.
* </p>
*/
void onPlayQueueEdited();
}

View file

@ -20,7 +20,8 @@ public enum UserAction {
DELETE_FROM_HISTORY("delete from history"),
PLAY_STREAM("Play stream"),
DOWNLOAD_POSTPROCESSING("download post-processing"),
DOWNLOAD_FAILED("download failed");
DOWNLOAD_FAILED("download failed"),
PREFERENCES_MIGRATION("migration of preferences");
private final String message;

View file

@ -5,12 +5,12 @@ import android.os.Bundle;
import androidx.preference.PreferenceManager;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AppCompatActivity;
import androidx.preference.PreferenceFragmentCompat;
import org.schabi.newpipe.MainActivity;
import org.schabi.newpipe.util.ThemeHelper;
public abstract class BasePreferenceFragment extends PreferenceFragmentCompat {
protected final String TAG = getClass().getSimpleName() + "@" + Integer.toHexString(hashCode());
@ -25,24 +25,16 @@ public abstract class BasePreferenceFragment extends PreferenceFragmentCompat {
}
@Override
public void onViewCreated(final View view, @Nullable final Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
public void onViewCreated(@NonNull final View rootView,
@Nullable final Bundle savedInstanceState) {
super.onViewCreated(rootView, savedInstanceState);
setDivider(null);
updateTitle();
ThemeHelper.setTitleToAppCompatActivity(getActivity(), getPreferenceScreen().getTitle());
}
@Override
public void onResume() {
super.onResume();
updateTitle();
}
private void updateTitle() {
if (getActivity() instanceof AppCompatActivity) {
final ActionBar actionBar = ((AppCompatActivity) getActivity()).getSupportActionBar();
if (actionBar != null) {
actionBar.setTitle(getPreferenceScreen().getTitle());
}
}
ThemeHelper.setTitleToAppCompatActivity(getActivity(), getPreferenceScreen().getTitle());
}
}

View file

@ -10,6 +10,7 @@ import androidx.preference.PreferenceManager;
import org.schabi.newpipe.R;
import java.io.File;
import java.util.Set;
/*
* Created by k3b on 07.01.2016.
@ -38,6 +39,22 @@ public final class NewPipeSettings {
private NewPipeSettings() { }
public static void initSettings(final Context context) {
// check if there are entries in the prefs to determine whether this is the first app run
Boolean isFirstRun = null;
final Set<String> prefsKeys = PreferenceManager.getDefaultSharedPreferences(context)
.getAll().keySet();
for (final String key: prefsKeys) {
// ACRA stores some info in the prefs during app initialization
// which happens before this method is called. Therefore ignore ACRA-related keys.
if (!key.toLowerCase().startsWith("acra")) {
isFirstRun = false;
break;
}
}
if (isFirstRun == null) {
isFirstRun = true;
}
PreferenceManager.setDefaultValues(context, R.xml.appearance_settings, true);
PreferenceManager.setDefaultValues(context, R.xml.content_settings, true);
PreferenceManager.setDefaultValues(context, R.xml.download_settings, true);
@ -49,6 +66,8 @@ public final class NewPipeSettings {
getVideoDownloadFolder(context);
getAudioDownloadFolder(context);
SettingMigrations.initMigrations(context, isFirstRun);
}
private static void getVideoDownloadFolder(final Context context) {

View file

@ -0,0 +1,270 @@
package org.schabi.newpipe.settings;
import android.content.Intent;
import android.content.SharedPreferences;
import android.graphics.PorterDuff;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.CheckBox;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.RadioButton;
import android.widget.RadioGroup;
import android.widget.Switch;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.content.res.AppCompatResources;
import androidx.fragment.app.Fragment;
import org.schabi.newpipe.R;
import org.schabi.newpipe.player.MainPlayer;
import org.schabi.newpipe.player.NotificationConstants;
import org.schabi.newpipe.util.DeviceUtils;
import org.schabi.newpipe.util.ThemeHelper;
import org.schabi.newpipe.views.FocusOverlayView;
import java.util.List;
public class NotificationSettingsFragment extends Fragment {
private Switch scaleSwitch;
private NotificationSlot[] notificationSlots;
private SharedPreferences pref;
private List<Integer> compactSlots;
private String scaleKey;
////////////////////////////////////////////////////////////////////////////
// Lifecycle
////////////////////////////////////////////////////////////////////////////
@Override
public void onCreate(@Nullable final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
pref = PreferenceManager.getDefaultSharedPreferences(requireContext());
scaleKey = getString(R.string.scale_to_square_image_in_notifications_key);
}
@Override
public View onCreateView(@NonNull final LayoutInflater inflater,
final ViewGroup container,
@Nullable final Bundle savedInstanceState) {
return inflater.inflate(R.layout.settings_notification, container, false);
}
@Override
public void onViewCreated(@NonNull final View rootView,
@Nullable final Bundle savedInstanceState) {
super.onViewCreated(rootView, savedInstanceState);
setupScaleSwitch(rootView);
setupActions(rootView);
}
@Override
public void onResume() {
super.onResume();
ThemeHelper.setTitleToAppCompatActivity(getActivity(),
getString(R.string.settings_category_notification_title));
}
@Override
public void onPause() {
super.onPause();
saveChanges();
requireContext().sendBroadcast(new Intent(MainPlayer.ACTION_RECREATE_NOTIFICATION));
}
////////////////////////////////////////////////////////////////////////////
// Setup
////////////////////////////////////////////////////////////////////////////
private void setupScaleSwitch(@NonNull final View view) {
scaleSwitch = view.findViewById(R.id.notificationScaleSwitch);
scaleSwitch.setChecked(pref.getBoolean(scaleKey, false));
view.findViewById(R.id.notificationScaleSwitchClickableArea)
.setOnClickListener(v -> scaleSwitch.toggle());
}
private void setupActions(@NonNull final View view) {
compactSlots =
NotificationConstants.getCompactSlotsFromPreferences(requireContext(), pref, 5);
notificationSlots = new NotificationSlot[5];
for (int i = 0; i < 5; i++) {
notificationSlots[i] = new NotificationSlot(i, view);
}
}
////////////////////////////////////////////////////////////////////////////
// Saving
////////////////////////////////////////////////////////////////////////////
private void saveChanges() {
final SharedPreferences.Editor editor = pref.edit();
editor.putBoolean(scaleKey, scaleSwitch.isChecked());
for (int i = 0; i < 3; i++) {
editor.putInt(getString(NotificationConstants.SLOT_COMPACT_PREF_KEYS[i]),
(i < compactSlots.size() ? compactSlots.get(i) : -1));
}
for (int i = 0; i < 5; i++) {
editor.putInt(getString(NotificationConstants.SLOT_PREF_KEYS[i]),
notificationSlots[i].selectedAction);
}
editor.apply();
}
////////////////////////////////////////////////////////////////////////////
// Notification action
////////////////////////////////////////////////////////////////////////////
private static final int[] SLOT_ITEMS = {
R.id.notificationAction0,
R.id.notificationAction1,
R.id.notificationAction2,
R.id.notificationAction3,
R.id.notificationAction4,
};
private static final int[] SLOT_TITLES = {
R.string.notification_action_0_title,
R.string.notification_action_1_title,
R.string.notification_action_2_title,
R.string.notification_action_3_title,
R.string.notification_action_4_title,
};
private class NotificationSlot {
final int i;
@NotificationConstants.Action int selectedAction;
ImageView icon;
TextView summary;
NotificationSlot(final int actionIndex, final View parentView) {
this.i = actionIndex;
final View view = parentView.findViewById(SLOT_ITEMS[i]);
setupSelectedAction(view);
setupTitle(view);
setupCheckbox(view);
}
void setupTitle(final View view) {
((TextView) view.findViewById(R.id.notificationActionTitle))
.setText(SLOT_TITLES[i]);
view.findViewById(R.id.notificationActionClickableArea).setOnClickListener(
v -> openActionChooserDialog());
}
void setupCheckbox(final View view) {
final CheckBox compactSlotCheckBox = view.findViewById(R.id.notificationActionCheckBox);
compactSlotCheckBox.setChecked(compactSlots.contains(i));
view.findViewById(R.id.notificationActionCheckBoxClickableArea).setOnClickListener(
v -> {
if (compactSlotCheckBox.isChecked()) {
compactSlots.remove((Integer) i);
} else if (compactSlots.size() < 3) {
compactSlots.add(i);
} else {
Toast.makeText(requireContext(),
R.string.notification_actions_at_most_three,
Toast.LENGTH_SHORT).show();
return;
}
compactSlotCheckBox.toggle();
});
}
void setupSelectedAction(final View view) {
icon = view.findViewById(R.id.notificationActionIcon);
summary = view.findViewById(R.id.notificationActionSummary);
selectedAction = pref.getInt(getString(NotificationConstants.SLOT_PREF_KEYS[i]),
NotificationConstants.SLOT_DEFAULTS[i]);
updateInfo();
}
void updateInfo() {
if (NotificationConstants.ACTION_ICONS[selectedAction] == 0) {
icon.setImageDrawable(null);
} else {
icon.setImageDrawable(AppCompatResources.getDrawable(requireContext(),
NotificationConstants.ACTION_ICONS[selectedAction]));
}
summary.setText(NotificationConstants.getActionName(requireContext(), selectedAction));
}
void openActionChooserDialog() {
final LayoutInflater inflater = LayoutInflater.from(requireContext());
final LinearLayout rootLayout = (LinearLayout) inflater.inflate(
R.layout.single_choice_dialog_view, null, false);
final RadioGroup radioGroup = rootLayout.findViewById(android.R.id.list);
final AlertDialog alertDialog = new AlertDialog.Builder(requireContext())
.setTitle(SLOT_TITLES[i])
.setView(radioGroup)
.setCancelable(true)
.create();
final View.OnClickListener radioButtonsClickListener = v -> {
selectedAction = NotificationConstants.SLOT_ALLOWED_ACTIONS[i][v.getId()];
updateInfo();
alertDialog.dismiss();
};
for (int id = 0; id < NotificationConstants.SLOT_ALLOWED_ACTIONS[i].length; ++id) {
final int action = NotificationConstants.SLOT_ALLOWED_ACTIONS[i][id];
final RadioButton radioButton
= (RadioButton) inflater.inflate(R.layout.list_radio_icon_item, null);
// if present set action icon with correct color
if (NotificationConstants.ACTION_ICONS[action] != 0) {
final Drawable drawable = AppCompatResources.getDrawable(requireContext(),
NotificationConstants.ACTION_ICONS[action]);
if (drawable != null) {
final int color = ThemeHelper.resolveColorFromAttr(requireContext(),
android.R.attr.textColorPrimary);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
drawable.setTint(color);
} else {
drawable.mutate().setColorFilter(color, PorterDuff.Mode.SRC_IN);
}
radioButton.setCompoundDrawablesWithIntrinsicBounds(
null, null, drawable, null);
}
}
radioButton.setText(NotificationConstants.getActionName(requireContext(), action));
radioButton.setChecked(action == selectedAction);
radioButton.setId(id);
radioButton.setLayoutParams(new RadioGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT));
radioButton.setOnClickListener(radioButtonsClickListener);
radioGroup.addView(radioButton);
}
alertDialog.show();
if (DeviceUtils.isTv(requireContext())) {
FocusOverlayView.setupFocusObserver(alertDialog);
}
}
}
}

View file

@ -22,9 +22,7 @@ import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.content.res.AppCompatResources;
import androidx.appcompat.widget.AppCompatImageView;
import androidx.fragment.app.Fragment;
@ -117,7 +115,8 @@ public class PeertubeInstanceListFragment extends Fragment {
@Override
public void onResume() {
super.onResume();
updateTitle();
ThemeHelper.setTitleToAppCompatActivity(getActivity(),
getString(R.string.peertube_instance_url_title));
}
@Override
@ -176,15 +175,6 @@ public class PeertubeInstanceListFragment extends Fragment {
sharedPreferences.edit().putBoolean(Constants.KEY_MAIN_PAGE_CHANGE, true).apply();
}
private void updateTitle() {
if (getActivity() instanceof AppCompatActivity) {
final ActionBar actionBar = ((AppCompatActivity) getActivity()).getSupportActionBar();
if (actionBar != null) {
actionBar.setTitle(R.string.peertube_instance_url_title);
}
}
}
private void saveChanges() {
final JsonStringWriter jsonWriter = JsonWriter.string().object().array("instances");
for (final PeertubeInstance instance : instanceList) {

View file

@ -0,0 +1,140 @@
package org.schabi.newpipe.settings;
import android.content.Context;
import android.content.SharedPreferences;
import android.util.Log;
import androidx.preference.PreferenceManager;
import org.schabi.newpipe.R;
import org.schabi.newpipe.report.ErrorActivity;
import org.schabi.newpipe.report.ErrorActivity.ErrorInfo;
import org.schabi.newpipe.report.UserAction;
import static org.schabi.newpipe.MainActivity.DEBUG;
public final class SettingMigrations {
private static final String TAG = SettingMigrations.class.toString();
/**
* Version number for preferences. Must be incremented every time a migration is necessary.
*/
public static final int VERSION = 2;
private static SharedPreferences sp;
public static final Migration MIGRATION_0_1 = new Migration(0, 1) {
@Override
public void migrate(final Context context) {
// We changed the content of the dialog which opens when sharing a link to NewPipe
// by removing the "open detail page" option.
// Therefore, show the dialog once again to ensure users need to choose again and are
// aware of the changed dialog.
final SharedPreferences.Editor editor = sp.edit();
editor.putString(context.getString(R.string.preferred_open_action_key),
context.getString(R.string.always_ask_open_action_key));
editor.apply();
}
};
public static final Migration MIGRATION_1_2 = new Migration(1, 2) {
@Override
protected void migrate(final Context context) {
// The new application workflow introduced in #2907 allows minimizing videos
// while playing to do other stuff within the app.
// For an even better workflow, we minimize a stream when switching the app to play in
// background.
// Therefore, set default value to background, if it has not been changed yet.
final String minimizeOnExitKey = context.getString(R.string.minimize_on_exit_key);
if (sp.getString(minimizeOnExitKey, "")
.equals(context.getString(R.string.minimize_on_exit_none_key))) {
final SharedPreferences.Editor editor = sp.edit();
editor.putString(minimizeOnExitKey,
context.getString(R.string.minimize_on_exit_background_key));
editor.apply();
}
}
};
/**
* List of all implemented migrations.
* <p>
* <b>Append new migrations to the end of the list</b> to keep it sorted ascending.
* If not sorted correctly, migrations which depend on each other, may fail.
*/
private static final Migration[] SETTING_MIGRATIONS = {
MIGRATION_0_1,
MIGRATION_1_2
};
public static void initMigrations(final Context context, final boolean isFirstRun) {
// 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);
final int lastPrefVersion = sp.getInt(lastPrefVersionKey, 0);
// no migration to run, already up to date
if (isFirstRun) {
sp.edit().putInt(lastPrefVersionKey, VERSION).apply();
return;
} else if (lastPrefVersion == VERSION) {
return;
}
// run migrations
int currentVersion = lastPrefVersion;
for (final Migration currentMigration : SETTING_MIGRATIONS) {
try {
if (currentMigration.shouldMigrate(currentVersion)) {
if (DEBUG) {
Log.d(TAG, "Migrating preferences from version "
+ currentVersion + " to " + currentMigration.newVersion);
}
currentMigration.migrate(context);
currentVersion = currentMigration.newVersion;
}
} catch (final Exception e) {
// save the version with the last successful migration and report the error
sp.edit().putInt(lastPrefVersionKey, currentVersion).apply();
final ErrorInfo errorInfo = ErrorInfo.make(
UserAction.PREFERENCES_MIGRATION,
"none",
"Migrating preferences from version " + lastPrefVersion + " to "
+ VERSION + ". "
+ "Error at " + currentVersion + " => " + ++currentVersion,
0
);
ErrorActivity.reportError(context, e, SettingMigrations.class, null, errorInfo);
return;
}
}
// store the current preferences version
sp.edit().putInt(lastPrefVersionKey, currentVersion).apply();
}
private SettingMigrations() { }
abstract static class Migration {
public final int oldVersion;
public final int newVersion;
protected Migration(final int oldVersion, final int newVersion) {
this.oldVersion = oldVersion;
this.newVersion = newVersion;
}
/**
* @param currentVersion current settings version
* @return Returns whether this migration should be run.
* A migration is necessary if the old version of this migration is lower than or equal to
* the current settings version.
*/
private boolean shouldMigrate(final int currentVersion) {
return oldVersion >= currentVersion;
}
protected abstract void migrate(Context context);
}
}

View file

@ -16,9 +16,7 @@ import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.content.res.AppCompatResources;
import androidx.appcompat.widget.AppCompatImageView;
import androidx.fragment.app.Fragment;
@ -92,7 +90,8 @@ public class ChooseTabsFragment extends Fragment {
@Override
public void onResume() {
super.onResume();
updateTitle();
ThemeHelper.setTitleToAppCompatActivity(getActivity(),
getString(R.string.main_page_content));
}
@Override
@ -137,15 +136,6 @@ public class ChooseTabsFragment extends Fragment {
tabList.addAll(tabsManager.getTabs());
}
private void updateTitle() {
if (getActivity() instanceof AppCompatActivity) {
final ActionBar actionBar = ((AppCompatActivity) getActivity()).getSupportActionBar();
if (actionBar != null) {
actionBar.setTitle(R.string.main_page_content);
}
}
}
private void saveChanges() {
tabsManager.saveTabs(tabList);
}

View file

@ -6,8 +6,6 @@ import android.content.pm.PackageManager;
import android.content.res.Configuration;
import android.os.BatteryManager;
import android.os.Build;
import android.util.DisplayMetrics;
import android.util.TypedValue;
import android.view.KeyEvent;
import androidx.annotation.NonNull;
@ -74,17 +72,4 @@ public final class DeviceUtils {
return false;
}
}
/*
* Compares current status bar height with default status bar height in Android and decides,
* does the device has cutout or not
* */
public static boolean hasCutout(final float statusBarHeight, final DisplayMetrics metrics) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
final float defaultStatusBarHeight = TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP, 25, metrics);
return statusBarHeight > defaultStatusBarHeight;
}
return false;
}
}

View file

@ -57,7 +57,7 @@ import org.schabi.newpipe.settings.SettingsActivity;
import java.util.ArrayList;
@SuppressWarnings({"unused", "WeakerAccess"})
@SuppressWarnings({"unused"})
public final class NavigationHelper {
public static final String MAIN_FRAGMENT_TAG = "main_fragment_tag";
public static final String SEARCH_FRAGMENT_TAG = "search_fragment_tag";
@ -69,16 +69,18 @@ public final class NavigationHelper {
//////////////////////////////////////////////////////////////////////////*/
@NonNull
public static Intent getPlayerIntent(@NonNull final Context context,
@NonNull final Class targetClazz,
@NonNull final PlayQueue playQueue,
@Nullable final String quality,
final boolean resumePlayback) {
public static <T> Intent getPlayerIntent(@NonNull final Context context,
@NonNull final Class<T> targetClazz,
@Nullable final PlayQueue playQueue,
@Nullable final String quality,
final boolean resumePlayback) {
final Intent intent = new Intent(context, targetClazz);
final String cacheKey = SerializedCache.getInstance().put(playQueue, PlayQueue.class);
if (cacheKey != null) {
intent.putExtra(VideoPlayer.PLAY_QUEUE_KEY, cacheKey);
if (playQueue != null) {
final String cacheKey = SerializedCache.getInstance().put(playQueue, PlayQueue.class);
if (cacheKey != null) {
intent.putExtra(VideoPlayer.PLAY_QUEUE_KEY, cacheKey);
}
}
if (quality != null) {
intent.putExtra(VideoPlayer.PLAYBACK_QUALITY, quality);
@ -90,53 +92,51 @@ public final class NavigationHelper {
}
@NonNull
public static Intent getPlayerIntent(@NonNull final Context context,
@NonNull final Class targetClazz,
@NonNull final PlayQueue playQueue,
final boolean resumePlayback) {
public static <T> Intent getPlayerIntent(@NonNull final Context context,
@NonNull final Class<T> targetClazz,
@Nullable final PlayQueue playQueue,
final boolean resumePlayback) {
return getPlayerIntent(context, targetClazz, playQueue, null, resumePlayback);
}
@NonNull
public static Intent getPlayerEnqueueIntent(@NonNull final Context context,
@NonNull final Class targetClazz,
@NonNull final PlayQueue playQueue,
final boolean selectOnAppend,
final boolean resumePlayback) {
public static <T> Intent getPlayerEnqueueIntent(@NonNull final Context context,
@NonNull final Class<T> targetClazz,
@Nullable final PlayQueue playQueue,
final boolean selectOnAppend,
final boolean resumePlayback) {
return getPlayerIntent(context, targetClazz, playQueue, resumePlayback)
.putExtra(BasePlayer.APPEND_ONLY, true)
.putExtra(BasePlayer.SELECT_ON_APPEND, selectOnAppend);
}
@NonNull
public static Intent getPlayerIntent(@NonNull final Context context,
@NonNull final Class targetClazz,
@NonNull final PlayQueue playQueue,
final int repeatMode,
final float playbackSpeed,
final float playbackPitch,
final boolean playbackSkipSilence,
@Nullable final String playbackQuality,
final boolean resumePlayback,
final boolean startPaused,
final boolean isMuted) {
public static <T> Intent getPlayerIntent(@NonNull final Context context,
@NonNull final Class<T> targetClazz,
@Nullable final PlayQueue playQueue,
final int repeatMode,
final float playbackSpeed,
final float playbackPitch,
final boolean playbackSkipSilence,
@Nullable final String playbackQuality,
final boolean resumePlayback,
final boolean startPaused,
final boolean isMuted) {
return getPlayerIntent(context, targetClazz, playQueue, playbackQuality, resumePlayback)
.putExtra(BasePlayer.REPEAT_MODE, repeatMode)
.putExtra(BasePlayer.START_PAUSED, startPaused)
.putExtra(BasePlayer.IS_MUTED, isMuted);
}
public static void playOnMainPlayer(
final AppCompatActivity activity,
final PlayQueue queue,
final boolean autoPlay) {
public static void playOnMainPlayer(final AppCompatActivity activity,
final PlayQueue queue,
final boolean autoPlay) {
playOnMainPlayer(activity.getSupportFragmentManager(), queue, autoPlay);
}
public static void playOnMainPlayer(
final FragmentManager fragmentManager,
final PlayQueue queue,
final boolean autoPlay) {
public static void playOnMainPlayer(final FragmentManager fragmentManager,
final PlayQueue queue,
final boolean autoPlay) {
final PlayQueueItem currentStream = queue.getItem();
openVideoDetailFragment(
fragmentManager,
@ -148,7 +148,7 @@ public final class NavigationHelper {
}
public static void playOnMainPlayer(@NonNull final Context context,
@NonNull final PlayQueue queue,
@Nullable final PlayQueue queue,
@NonNull final StreamingService.LinkType linkType,
@NonNull final String url,
@NonNull final String title,
@ -553,18 +553,14 @@ public final class NavigationHelper {
return true;
}
public static Intent getBackgroundPlayerActivityIntent(final Context context) {
return getServicePlayerActivityIntent(context, BackgroundPlayerActivity.class);
}
private static Intent getServicePlayerActivityIntent(final Context context,
final Class activityClass) {
final Intent intent = new Intent(context, activityClass);
public static Intent getPlayQueueActivityIntent(final Context context) {
final Intent intent = new Intent(context, BackgroundPlayerActivity.class);
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
}
return intent;
}
/*//////////////////////////////////////////////////////////////////////////
// Link handling
//////////////////////////////////////////////////////////////////////////*/

View file

@ -19,6 +19,7 @@
package org.schabi.newpipe.util;
import android.app.Activity;
import android.content.Context;
import android.content.res.TypedArray;
import androidx.preference.PreferenceManager;
@ -26,7 +27,10 @@ import android.util.TypedValue;
import android.view.ContextThemeWrapper;
import androidx.annotation.AttrRes;
import androidx.annotation.Nullable;
import androidx.annotation.StyleRes;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.content.ContextCompat;
import org.schabi.newpipe.R;
@ -231,4 +235,20 @@ public final class ThemeHelper {
return PreferenceManager.getDefaultSharedPreferences(context)
.getString(themeKey, defaultTheme);
}
/**
* Sets the title to the activity, if the activity is an {@link AppCompatActivity} and has an
* action bar.
* @param activity the activity to set the title of
* @param title the title to set to the activity
*/
public static void setTitleToAppCompatActivity(@Nullable final Activity activity,
final CharSequence title) {
if (activity instanceof AppCompatActivity) {
final ActionBar actionBar = ((AppCompatActivity) activity).getSupportActionBar();
if (actionBar != null) {
actionBar.setTitle(title);
}
}
}
}

View file

@ -0,0 +1,39 @@
package org.schabi.newpipe.views;
import android.content.Context;
import android.util.AttributeSet;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowInsetsCompat;
import com.google.android.material.appbar.CollapsingToolbarLayout;
public class CustomCollapsingToolbarLayout extends CollapsingToolbarLayout {
public CustomCollapsingToolbarLayout(@NonNull final Context context) {
super(context);
overrideListener();
}
public CustomCollapsingToolbarLayout(@NonNull final Context context,
@Nullable final AttributeSet attrs) {
super(context, attrs);
overrideListener();
}
public CustomCollapsingToolbarLayout(@NonNull final Context context,
@Nullable final AttributeSet attrs,
final int defStyleAttr) {
super(context, attrs, defStyleAttr);
overrideListener();
}
/**
* CollapsingToolbarLayout sets it's own setOnApplyInsetsListener which consumes
* system insets {@link CollapsingToolbarLayout#onWindowInsetChanged(WindowInsetsCompat)}
* so we will not receive them in subviews with fitsSystemWindows = true.
* Override Google's behavior
* */
public void overrideListener() {
ViewCompat.setOnApplyWindowInsetsListener(this, (v, insets) -> insets);
}
}

View file

@ -1,14 +1,17 @@
package org.schabi.newpipe.views;
import android.content.Context;
import android.os.Build.VERSION;
import android.os.Build.VERSION_CODES;
import android.util.AttributeSet;
import android.view.SurfaceView;
import com.google.android.exoplayer2.ui.AspectRatioFrameLayout;
import static com.google.android.exoplayer2.ui.AspectRatioFrameLayout.RESIZE_MODE_FIT;
import static com.google.android.exoplayer2.ui.AspectRatioFrameLayout.RESIZE_MODE_ZOOM;
public class ExpandableSurfaceView extends SurfaceView {
private int resizeMode = AspectRatioFrameLayout.RESIZE_MODE_FIT;
private int resizeMode = RESIZE_MODE_FIT;
private int baseHeight = 0;
private int maxHeight = 0;
private float videoAspectRatio = 0.0f;
@ -30,7 +33,7 @@ public class ExpandableSurfaceView extends SurfaceView {
final boolean verticalVideo = videoAspectRatio < 1;
// Use maxHeight only on non-fit resize mode and in vertical videos
int height = maxHeight != 0
&& resizeMode != AspectRatioFrameLayout.RESIZE_MODE_FIT
&& resizeMode != RESIZE_MODE_FIT
&& verticalVideo ? maxHeight : baseHeight;
if (height == 0) {
@ -42,26 +45,22 @@ public class ExpandableSurfaceView extends SurfaceView {
scaleX = 1.0f;
scaleY = 1.0f;
switch (resizeMode) {
case AspectRatioFrameLayout.RESIZE_MODE_FIT:
if (aspectDeformation > 0) {
height = (int) (width / videoAspectRatio);
} else {
width = (int) (height * videoAspectRatio);
}
break;
case RESIZE_MODE_ZOOM:
if (aspectDeformation < 0) {
scaleY = viewAspectRatio / videoAspectRatio;
} else {
scaleX = videoAspectRatio / viewAspectRatio;
}
break;
default:
break;
if (resizeMode == RESIZE_MODE_FIT
// KitKat doesn't work well when a view has a scale like needed for ZOOM
|| (resizeMode == RESIZE_MODE_ZOOM && VERSION.SDK_INT < VERSION_CODES.LOLLIPOP)) {
if (aspectDeformation > 0) {
height = (int) (width / videoAspectRatio);
} else {
width = (int) (height * videoAspectRatio);
}
} else if (resizeMode == RESIZE_MODE_ZOOM) {
if (aspectDeformation < 0) {
scaleY = viewAspectRatio / videoAspectRatio;
} else {
scaleX = videoAspectRatio / viewAspectRatio;
}
}
super.onMeasure(MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY));
}

View file

@ -17,15 +17,19 @@
*/
package org.schabi.newpipe.views;
import android.annotation.TargetApi;
import android.content.Context;
import android.graphics.Rect;
import android.os.Build;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowInsets;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.coordinatorlayout.widget.CoordinatorLayout;
import org.schabi.newpipe.R;
public final class FocusAwareCoordinator extends CoordinatorLayout {
private final Rect childFocus = new Rect();
@ -63,4 +67,41 @@ public final class FocusAwareCoordinator extends CoordinatorLayout {
requestChildRectangleOnScreen(child, childFocus, false);
}
}
/**
* Applies window insets to all children, not just for the first who consume the insets.
* Makes possible for multiple fragments to co-exist. Without this code
* the first ViewGroup who consumes will be the last who receive the insets
*/
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
@Override
public WindowInsets dispatchApplyWindowInsets(final WindowInsets insets) {
boolean consumed = false;
for (int i = 0; i < getChildCount(); i++) {
final View child = getChildAt(i);
final WindowInsets res = child.dispatchApplyWindowInsets(insets);
if (res.isConsumed()) {
consumed = true;
}
}
if (consumed) {
insets.consumeSystemWindowInsets();
}
return insets;
}
/**
* Adjusts player's controls manually because fitsSystemWindows doesn't work when multiple
* receivers adjust its bounds. So when two listeners are present (like in profile page)
* the player's controls will not receive insets. This method fixes it
*/
@Override
protected boolean fitSystemWindows(final Rect insets) {
final ViewGroup controls = findViewById(R.id.playbackControlRoot);
if (controls != null) {
controls.setPadding(insets.left, insets.top, insets.right, insets.bottom);
}
return super.fitSystemWindows(insets);
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 221 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 415 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 302 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 675 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 621 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 175 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 285 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 246 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 457 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 411 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 257 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 602 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 413 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 908 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 837 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 347 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 614 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 436 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 777 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2 KiB

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FFFFFFFF"
android:pathData="M19,6.41L17.59,5 12,10.59 6.41,5 5,6.41 10.59,12 5,17.59 6.41,19 12,13.41 17.59,19 19,17.59 13.41,12z"/>
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M6,2l0.01,6L10,12l-3.99,4.01L6,22h12v-6l-4,-4l4,-3.99V2H6zM16,16.5V20H8v-3.5l4,-4L16,16.5z"
android:fillColor="#ffffff"/>
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FFFFFFFF"
android:pathData="M12,5V1L7,6l5,5V7c3.31,0 6,2.69 6,6s-2.69,6 -6,6 -6,-2.69 -6,-6H4c0,4.42 3.58,8 8,8s8,-3.58 8,-8 -3.58,-8 -8,-8z"/>
</vector>

View file

@ -20,7 +20,6 @@
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="5"
android:fitsSystemWindows="true"
android:isScrollContainer="true">
<com.google.android.material.appbar.AppBarLayout
@ -28,12 +27,11 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@android:color/transparent"
android:fitsSystemWindows="true"
android:touchscreenBlocksFocus="false"
app:elevation="0dp"
app:layout_behavior="com.google.android.material.appbar.FlingBehavior">
<com.google.android.material.appbar.CollapsingToolbarLayout
<org.schabi.newpipe.views.CustomCollapsingToolbarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_scrollFlags="scroll">
@ -162,7 +160,7 @@
</FrameLayout>
</com.google.android.material.appbar.CollapsingToolbarLayout>
</org.schabi.newpipe.views.CustomCollapsingToolbarLayout>
<!-- CONTENT -->
<RelativeLayout

View file

@ -28,6 +28,22 @@
android:layout_centerInParent="true"
android:layout_gravity="center"/>
<View
android:id="@+id/playerTopShadow"
android:layout_width="match_parent"
android:layout_height="30dp"
android:background="@drawable/player_controls_top_background"
android:layout_alignParentTop="true"
android:visibility="gone"/>
<View
android:id="@+id/playerBottomShadow"
android:layout_width="match_parent"
android:layout_height="30dp"
android:background="@drawable/player_controls_background"
android:layout_alignParentBottom="true"
android:visibility="gone"/>
<ImageView
android:id="@+id/endScreen"
android:layout_width="match_parent"
@ -42,28 +58,16 @@
android:id="@+id/playbackControlRoot"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
android:background="@color/video_overlay_color"
android:visibility="gone"
tools:visibility="visible">
<View
android:layout_width="match_parent"
android:layout_height="30dp"
android:background="@drawable/player_controls_top_background"
android:layout_alignParentTop="true" />
<View
android:layout_width="match_parent"
android:layout_height="30dp"
android:background="@drawable/player_controls_background"
android:layout_alignParentBottom="true" />
<!-- All top controls in this layout -->
<RelativeLayout
android:id="@+id/playbackWindowRoot"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true">
android:layout_height="match_parent">
<LinearLayout
android:id="@+id/topControls"
@ -228,29 +232,29 @@
tools:ignore="HardcodedText,RtlHardcoded"
tools:text="FIT"/>
<Button
android:id="@+id/captionTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="@dimen/player_main_buttons_padding"
android:layout_marginEnd="8dp"
android:gravity="center|left"
android:minHeight="35dp"
android:lines="1"
android:ellipsize="end"
android:minWidth="50dp"
android:textColor="@android:color/white"
android:textStyle="bold"
android:textAllCaps="false"
android:background="?attr/selectableItemBackground"
tools:ignore="RelativeOverlap,RtlHardcoded"
tools:text="English"/>
<Space
android:id="@+id/spaceBeforeButton"
<FrameLayout
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_weight="3"/>
android:layout_height="wrap_content"
android:layout_weight="3">
<TextView
android:id="@+id/captionTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="@dimen/player_main_buttons_padding"
android:layout_marginEnd="8dp"
android:gravity="center|left"
android:minHeight="35dp"
android:lines="1"
android:ellipsize="end"
android:minWidth="50dp"
android:textColor="@android:color/white"
android:textStyle="bold"
android:background="?attr/selectableItemBackground"
tools:ignore="RelativeOverlap,RtlHardcoded"
tools:text="English"/>
</FrameLayout>
<androidx.appcompat.widget.AppCompatImageButton
android:id="@+id/playWithKodi"
@ -307,25 +311,24 @@
android:contentDescription="@string/mute"
tools:ignore="RtlHardcoded" />
<androidx.appcompat.widget.AppCompatImageButton
android:id="@+id/fullScreenButton"
android:layout_width="40dp"
android:layout_height="40dp"
android:padding="@dimen/player_main_buttons_padding"
android:background="?attr/selectableItemBackground"
android:clickable="true"
android:focusable="true"
android:scaleType="fitCenter"
app:srcCompat="@drawable/ic_fullscreen_white_24dp"
tools:ignore="ContentDescription,RtlHardcoded"
android:visibility="gone"
tools:visibility="visible"/>
</LinearLayout>
</LinearLayout>
<androidx.appcompat.widget.AppCompatImageButton
android:id="@+id/fullScreenButton"
android:layout_width="40dp"
android:layout_height="40dp"
android:padding="@dimen/player_main_buttons_padding"
android:layout_alignParentRight="true"
android:background="?attr/selectableItemBackground"
android:clickable="true"
android:focusable="true"
android:scaleType="fitCenter"
app:srcCompat="@drawable/ic_fullscreen_white_24dp"
tools:ignore="ContentDescription,RtlHardcoded"
android:visibility="gone"
tools:visibility="visible"/>
<LinearLayout
android:id="@+id/bottomControls"
android:layout_width="match_parent"
@ -353,10 +356,10 @@
android:id="@+id/playbackSeekBar"
style="@style/Widget.AppCompat.SeekBar"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_weight="1"
android:paddingBottom="4dp"
android:paddingTop="8dp"
android:layout_marginTop="2dp"
tools:progress="25"
android:nextFocusDown="@id/screenRotationButton"
tools:secondaryProgress="50"/>
@ -465,21 +468,20 @@
android:layout_height="60dp"
android:id="@+id/playQueueControl">
<ImageButton
<androidx.appcompat.widget.AppCompatImageButton
android:id="@+id/playQueueClose"
android:layout_width="50dp"
android:layout_height="50dp"
android:layout_centerVertical="true"
android:layout_alignParentEnd="true"
android:layout_centerVertical="true"
android:layout_marginEnd="40dp"
android:padding="10dp"
android:clickable="true"
android:focusable="true"
android:scaleType="fitXY"
android:tint="?attr/colorAccent"
android:src="@drawable/ic_close_white_24dp"
android:background="?android:selectableItemBackground"
tools:ignore="ContentDescription"/>
android:clickable="true"
android:contentDescription="@string/close"
android:focusable="true"
android:padding="10dp"
android:scaleType="fitXY"
app:srcCompat="@drawable/ic_close_white_24dp" />
<ImageButton
android:id="@+id/repeatButton"

View file

@ -4,8 +4,7 @@
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/drawer_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true">
android:layout_height="match_parent">
<org.schabi.newpipe.views.FocusAwareCoordinator
android:layout_width="match_parent"

View file

@ -12,6 +12,6 @@
android:inputType="text"
android:maxLines="1"
android:layout_margin="10dp"
android:hint="@string/playlist_name_input"/>
android:hint="@string/name"/>
</LinearLayout>

View file

@ -38,7 +38,7 @@
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:gravity="center_vertical"
android:hint="@string/feed_group_dialog_name_input"
android:hint="@string/name"
android:paddingTop="6dp"
android:paddingBottom="6dp"
app:errorEnabled="true"

View file

@ -16,6 +16,6 @@
android:layout_marginRight="10dp"
android:saveEnabled="true"
android:inputType="text"
android:hint="@string/playlist_name_input"
android:hint="@string/name"
android:maxLines="1"/>
</RelativeLayout>

View file

@ -18,11 +18,10 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@android:color/transparent"
android:fitsSystemWindows="true"
app:elevation="0dp"
app:layout_behavior="com.google.android.material.appbar.FlingBehavior">
<com.google.android.material.appbar.CollapsingToolbarLayout
<org.schabi.newpipe.views.CustomCollapsingToolbarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_scrollFlags="scroll">
@ -148,8 +147,7 @@
/>
</FrameLayout>
</com.google.android.material.appbar.CollapsingToolbarLayout>
</org.schabi.newpipe.views.CustomCollapsingToolbarLayout>
<!-- CONTENT -->
<RelativeLayout

View file

@ -10,10 +10,8 @@
android:maxLines="2"
android:minHeight="?attr/listPreferredItemHeightSmall"
android:paddingEnd="?attr/listPreferredItemPaddingRight"
android:paddingLeft="?attr/listPreferredItemPaddingLeft"
android:paddingRight="?attr/listPreferredItemPaddingRight"
android:paddingStart="?attr/listPreferredItemPaddingLeft"
android:background="?attr/checked_selector"
android:textColor="?attr/textColorAlertDialogListItem"
tools:drawableLeft="?attr/ic_play"
tools:drawableLeft="?attr/ic_play_arrow"
tools:text="Lorem ipsum dolor sit amet" />

View file

@ -1,8 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/black"
@ -12,21 +12,37 @@
android:id="@+id/surfaceView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_centerInParent="true"/>
android:layout_centerInParent="true" />
<View
android:id="@+id/surfaceForeground"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/black"
android:layout_alignBottom="@+id/surfaceView"/>
android:layout_alignBottom="@+id/surfaceView"
android:background="@android:color/black" />
<com.google.android.exoplayer2.ui.SubtitleView
android:id="@+id/subtitleView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_centerInParent="true"
android:layout_gravity="center"/>
android:layout_gravity="center" />
<View
android:id="@+id/playerTopShadow"
android:layout_width="match_parent"
android:layout_height="30dp"
android:layout_alignParentTop="true"
android:background="@drawable/player_controls_top_background"
android:visibility="gone" />
<View
android:id="@+id/playerBottomShadow"
android:layout_width="match_parent"
android:layout_height="30dp"
android:layout_alignParentBottom="true"
android:background="@drawable/player_controls_background"
android:visibility="gone" />
<ImageView
android:id="@+id/endScreen"
@ -36,325 +52,319 @@
android:visibility="gone"
tools:background="@android:color/white"
tools:ignore="ContentDescription"
tools:visibility="visible"/>
tools:visibility="visible" />
<RelativeLayout
android:id="@+id/playbackControlRoot"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/video_overlay_color"
android:fitsSystemWindows="true"
android:visibility="gone"
tools:visibility="visible">
<View
android:layout_width="match_parent"
android:layout_height="30dp"
android:background="@drawable/player_controls_top_background"
android:layout_alignParentTop="true" />
<View
android:layout_width="match_parent"
android:layout_height="30dp"
android:background="@drawable/player_controls_background"
android:layout_alignParentBottom="true" />
<!-- All top controls in this layout -->
<RelativeLayout
android:id="@+id/playbackWindowRoot"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true">
android:layout_height="match_parent">
<LinearLayout
android:id="@+id/topControls"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:orientation="vertical"
android:gravity="top"
android:paddingTop="@dimen/player_main_top_padding"
android:paddingStart="@dimen/player_main_controls_padding"
android:paddingEnd="@dimen/player_main_controls_padding"
android:baselineAligned="false">
<LinearLayout
android:id="@+id/primaryControls"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="45dp"
android:baselineAligned="false"
android:gravity="top"
tools:ignore="RtlHardcoded">
<ImageButton
android:id="@+id/playerCloseButton"
android:layout_width="36dp"
android:layout_height="36dp"
android:padding="@dimen/player_main_buttons_padding"
android:layout_marginEnd="8dp"
android:clickable="true"
android:focusable="true"
android:scaleType="fitXY"
android:src="?attr/ic_close"
android:background="?attr/selectableItemBackgroundBorderless"
tools:ignore="ContentDescription,RtlHardcoded"
android:visibility="gone" />
android:orientation="vertical"
android:paddingStart="@dimen/player_main_controls_padding"
android:paddingTop="@dimen/player_main_top_padding"
android:paddingEnd="@dimen/player_main_controls_padding">
<LinearLayout
android:id="@+id/metadataView"
android:layout_width="0dp"
android:id="@+id/primaryControls"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:baselineAligned="false"
android:gravity="top"
android:orientation="vertical"
android:layout_marginTop="6dp"
android:layout_marginRight="8dp"
tools:ignore="RtlHardcoded"
android:layout_weight="1">
android:minHeight="45dp"
tools:ignore="RtlHardcoded">
<TextView
android:id="@+id/titleTextView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ellipsize="marquee"
android:fadingEdge="horizontal"
android:marqueeRepeatLimit="marquee_forever"
android:scrollHorizontally="true"
android:singleLine="true"
android:textColor="@android:color/white"
android:textSize="15sp"
android:textStyle="bold"
<androidx.appcompat.widget.AppCompatImageButton
android:id="@+id/playerCloseButton"
android:layout_width="36dp"
android:layout_height="36dp"
android:layout_marginEnd="8dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:clickable="true"
android:focusable="true"
tools:ignore="RtlHardcoded"
tools:text="The Video Title LONG very LONG"/>
android:padding="@dimen/player_main_buttons_padding"
android:scaleType="fitXY"
android:visibility="gone"
app:srcCompat="?attr/ic_close"
tools:ignore="ContentDescription,RtlHardcoded" />
<TextView
android:id="@+id/channelTextView"
android:layout_width="match_parent"
<LinearLayout
android:id="@+id/metadataView"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:ellipsize="marquee"
android:fadingEdge="horizontal"
android:marqueeRepeatLimit="marquee_forever"
android:scrollHorizontally="true"
android:singleLine="true"
android:textColor="@android:color/white"
android:textSize="12sp"
android:clickable="true"
android:focusable="true"
tools:text="The Video Artist LONG very LONG very Long"/>
</LinearLayout>
android:layout_marginTop="6dp"
android:layout_marginRight="8dp"
android:layout_weight="1"
android:gravity="top"
android:orientation="vertical"
tools:ignore="RtlHardcoded">
<TextView
android:id="@+id/qualityTextView"
android:layout_width="wrap_content"
android:layout_height="35dp"
android:minWidth="0dp"
android:padding="@dimen/player_main_buttons_padding"
android:layout_marginEnd="8dp"
android:gravity="center"
android:text="720p"
android:textColor="@android:color/white"
android:textStyle="bold"
android:background="?attr/selectableItemBackground"
android:visibility="visible"
tools:ignore="HardcodedText,RtlHardcoded"/>
<TextView
android:id="@+id/titleTextView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clickable="true"
android:ellipsize="marquee"
android:fadingEdge="horizontal"
android:focusable="true"
android:marqueeRepeatLimit="marquee_forever"
android:scrollHorizontally="true"
android:singleLine="true"
android:textColor="@android:color/white"
android:textSize="15sp"
android:textStyle="bold"
tools:ignore="RtlHardcoded"
tools:text="The Video Title LONG very LONG" />
<TextView
android:id="@+id/playbackSpeed"
android:layout_width="wrap_content"
android:layout_height="35dp"
android:padding="@dimen/player_main_buttons_padding"
android:layout_marginEnd="8dp"
android:gravity="center"
android:minWidth="0dp"
android:textColor="@android:color/white"
android:textStyle="bold"
android:background="?attr/selectableItemBackground"
tools:ignore="RtlHardcoded,RtlSymmetry"
tools:text="1x"/>
<androidx.appcompat.widget.AppCompatImageButton
android:id="@+id/queueButton"
android:layout_width="35dp"
android:layout_height="35dp"
android:paddingTop="5dp"
android:paddingStart="3dp"
android:paddingEnd="3dp"
android:paddingBottom="3dp"
android:layout_marginEnd="8dp"
android:clickable="true"
android:focusable="true"
android:scaleType="fitCenter"
app:srcCompat="@drawable/ic_list_white_24dp"
android:background="?attr/selectableItemBackground"
tools:ignore="ContentDescription,RtlHardcoded"
android:visibility="gone"/>
<androidx.appcompat.widget.AppCompatImageButton
android:id="@+id/moreOptionsButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="@dimen/player_main_buttons_padding"
android:clickable="true"
android:focusable="true"
android:scaleType="fitXY"
app:srcCompat="@drawable/ic_expand_more_white_24dp"
android:background="?attr/selectableItemBackgroundBorderless"
tools:ignore="ContentDescription,RtlHardcoded"/>
</LinearLayout>
<LinearLayout
android:id="@+id/secondaryControls"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="top"
android:visibility="invisible"
tools:ignore="RtlHardcoded"
tools:visibility="visible"
android:baselineAligned="false">
<LinearLayout
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1">
<TextView
android:id="@+id/channelTextView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clickable="true"
android:ellipsize="marquee"
android:fadingEdge="horizontal"
android:focusable="true"
android:marqueeRepeatLimit="marquee_forever"
android:scrollHorizontally="true"
android:singleLine="true"
android:textColor="@android:color/white"
android:textSize="12sp"
tools:text="The Video Artist LONG very LONG very Long" />
</LinearLayout>
<TextView
android:id="@+id/resizeTextView"
android:id="@+id/qualityTextView"
android:layout_width="wrap_content"
android:layout_height="35dp"
android:padding="@dimen/player_main_buttons_padding"
android:layout_marginEnd="8dp"
android:background="?attr/selectableItemBackground"
android:gravity="center"
android:minWidth="50dp"
android:minWidth="0dp"
android:padding="@dimen/player_main_buttons_padding"
android:text="720p"
android:textColor="@android:color/white"
android:textStyle="bold"
android:background="?attr/selectableItemBackground"
tools:ignore="HardcodedText,RtlHardcoded"
tools:text="FIT"/>
android:visibility="visible"
tools:ignore="HardcodedText,RtlHardcoded" />
<TextView
android:id="@+id/captionTextView"
android:id="@+id/playbackSpeed"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="@dimen/player_main_buttons_padding"
android:layout_height="35dp"
android:layout_marginEnd="8dp"
android:gravity="center|left"
android:minHeight="35dp"
android:lines="1"
android:ellipsize="end"
android:minWidth="50dp"
android:background="?attr/selectableItemBackground"
android:gravity="center"
android:minWidth="0dp"
android:padding="@dimen/player_main_buttons_padding"
android:textColor="@android:color/white"
android:textStyle="bold"
tools:ignore="RtlHardcoded,RtlSymmetry"
tools:text="1x" />
<androidx.appcompat.widget.AppCompatImageButton
android:id="@+id/queueButton"
android:layout_width="35dp"
android:layout_height="35dp"
android:layout_marginEnd="8dp"
android:background="?attr/selectableItemBackground"
tools:ignore="RelativeOverlap,RtlHardcoded"
tools:text="English"/>
android:clickable="true"
android:focusable="true"
android:paddingStart="3dp"
android:paddingTop="5dp"
android:paddingEnd="3dp"
android:paddingBottom="3dp"
android:scaleType="fitCenter"
android:visibility="gone"
app:srcCompat="@drawable/ic_list_white_24dp"
tools:ignore="ContentDescription,RtlHardcoded" />
<androidx.appcompat.widget.AppCompatImageButton
android:id="@+id/moreOptionsButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackgroundBorderless"
android:clickable="true"
android:focusable="true"
android:padding="@dimen/player_main_buttons_padding"
android:scaleType="fitXY"
app:srcCompat="@drawable/ic_expand_more_white_24dp"
tools:ignore="ContentDescription,RtlHardcoded" />
</LinearLayout>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="right">
<LinearLayout
android:id="@+id/secondaryControls"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="top"
android:visibility="invisible"
tools:ignore="RtlHardcoded"
tools:visibility="visible"
android:baselineAligned="false">
<androidx.appcompat.widget.AppCompatImageButton
android:id="@+id/playWithKodi"
android:layout_width="0dp"
android:layout_height="35dp"
android:padding="@dimen/player_main_buttons_padding"
android:clickable="true"
android:focusable="true"
android:scaleType="center"
app:srcCompat="@drawable/ic_cast_white_24dp"
android:background="?attr/selectableItemBackground"
android:contentDescription="@string/play_with_kodi_title"
tools:ignore="RtlHardcoded"
android:layout_weight="1"/>
<LinearLayout
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1">
<androidx.appcompat.widget.AppCompatImageButton
android:id="@+id/openInBrowser"
android:layout_width="0dp"
android:layout_height="35dp"
android:padding="@dimen/player_main_buttons_padding"
android:clickable="true"
android:focusable="true"
android:scaleType="center"
app:srcCompat="@drawable/ic_language_white_24dp"
android:background="?attr/selectableItemBackground"
android:contentDescription="@string/open_in_browser"
tools:ignore="RtlHardcoded"
android:layout_weight="1"/>
<TextView
android:id="@+id/resizeTextView"
android:layout_width="wrap_content"
android:layout_height="35dp"
android:layout_marginEnd="8dp"
android:background="?attr/selectableItemBackground"
android:gravity="center"
android:minWidth="50dp"
android:padding="@dimen/player_main_buttons_padding"
android:textColor="@android:color/white"
android:textStyle="bold"
tools:ignore="HardcodedText,RtlHardcoded"
tools:text="FIT" />
<androidx.appcompat.widget.AppCompatImageButton
android:id="@+id/share"
android:layout_width="0dp"
android:layout_height="35dp"
android:padding="@dimen/player_main_buttons_padding"
android:clickable="true"
android:focusable="true"
android:scaleType="center"
app:srcCompat="@drawable/ic_share_white_24dp"
android:background="?attr/selectableItemBackground"
android:contentDescription="@string/share"
tools:ignore="RtlHardcoded"
android:layout_weight="1"/>
<FrameLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="3">
<androidx.appcompat.widget.AppCompatImageButton
android:id="@+id/switchMute"
android:layout_width="0dp"
android:layout_height="35dp"
android:padding="@dimen/player_main_buttons_padding"
android:clickable="true"
android:focusable="true"
android:scaleType="center"
app:srcCompat="@drawable/ic_volume_off_white_24dp"
android:background="?attr/selectableItemBackground"
android:contentDescription="@string/mute"
tools:ignore="RtlHardcoded"
android:layout_weight="1"/>
<TextView
android:id="@+id/captionTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:background="?attr/selectableItemBackground"
android:ellipsize="end"
android:gravity="center|left"
android:lines="1"
android:minWidth="50dp"
android:minHeight="35dp"
android:padding="@dimen/player_main_buttons_padding"
android:textColor="@android:color/white"
android:textStyle="bold"
tools:ignore="RelativeOverlap,RtlHardcoded"
tools:text="English" />
<androidx.appcompat.widget.AppCompatImageButton
android:id="@+id/switchSponsorBlocking"
android:layout_width="0dp"
android:layout_height="35dp"
android:padding="@dimen/player_main_buttons_padding"
android:clickable="true"
android:focusable="true"
android:scaleType="center"
app:srcCompat="@drawable/ic_sponsor_block_disable_white_24dp"
android:background="?attr/selectableItemBackground"
android:contentDescription="@string/sponsor_block_toggle_skipping"
tools:ignore="RtlHardcoded"
android:layout_weight="1"/>
</FrameLayout>
</LinearLayout>
</LinearLayout>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="right">
<androidx.appcompat.widget.AppCompatImageButton
android:id="@+id/playWithKodi"
android:layout_width="0dp"
android:layout_height="35dp"
android:layout_weight="1"
android:background="?attr/selectableItemBackground"
android:clickable="true"
android:contentDescription="@string/play_with_kodi_title"
android:focusable="true"
android:padding="@dimen/player_main_buttons_padding"
android:scaleType="center"
app:srcCompat="@drawable/ic_cast_white_24dp"
tools:ignore="RtlHardcoded" />
<androidx.appcompat.widget.AppCompatImageButton
android:id="@+id/openInBrowser"
android:layout_width="0dp"
android:layout_height="35dp"
android:layout_weight="1"
android:background="?attr/selectableItemBackground"
android:clickable="true"
android:contentDescription="@string/open_in_browser"
android:focusable="true"
android:padding="@dimen/player_main_buttons_padding"
android:scaleType="center"
app:srcCompat="@drawable/ic_language_white_24dp"
tools:ignore="RtlHardcoded" />
<androidx.appcompat.widget.AppCompatImageButton
android:id="@+id/share"
android:layout_width="0dp"
android:layout_height="35dp"
android:layout_weight="1"
android:background="?attr/selectableItemBackground"
android:clickable="true"
android:contentDescription="@string/share"
android:focusable="true"
android:padding="@dimen/player_main_buttons_padding"
android:scaleType="center"
app:srcCompat="@drawable/ic_share_white_24dp"
tools:ignore="RtlHardcoded" />
<androidx.appcompat.widget.AppCompatImageButton
android:id="@+id/switchMute"
android:layout_width="0dp"
android:layout_height="35dp"
android:layout_weight="1"
android:background="?attr/selectableItemBackground"
android:clickable="true"
android:contentDescription="@string/mute"
android:focusable="true"
android:padding="@dimen/player_main_buttons_padding"
android:scaleType="center"
app:srcCompat="@drawable/ic_volume_off_white_24dp"
tools:ignore="RtlHardcoded" />
<androidx.appcompat.widget.AppCompatImageButton
android:id="@+id/switchSponsorBlocking"
android:layout_width="0dp"
android:layout_height="35dp"
android:layout_weight="1"
android:background="?attr/selectableItemBackground"
android:clickable="true"
android:contentDescription="@string/sponsor_block_toggle_skipping"
android:focusable="true"
android:padding="@dimen/player_main_buttons_padding"
android:scaleType="center"
app:srcCompat="@drawable/ic_sponsor_block_disable_white_24dp"
tools:ignore="RtlHardcoded" />
<androidx.appcompat.widget.AppCompatImageButton
android:id="@+id/fullScreenButton"
android:layout_width="40dp"
android:layout_height="40dp"
android:background="?attr/selectableItemBackground"
android:clickable="true"
android:focusable="true"
android:padding="@dimen/player_main_buttons_padding"
android:scaleType="fitCenter"
android:visibility="gone"
app:srcCompat="@drawable/ic_fullscreen_white_24dp"
tools:ignore="ContentDescription,RtlHardcoded"
tools:visibility="visible" />
</LinearLayout>
</LinearLayout>
</LinearLayout>
<androidx.appcompat.widget.AppCompatImageButton
android:id="@+id/fullScreenButton"
android:layout_width="40dp"
android:layout_height="40dp"
android:padding="@dimen/player_main_buttons_padding"
android:layout_alignParentRight="true"
android:background="?attr/selectableItemBackground"
android:clickable="true"
android:focusable="true"
android:scaleType="fitCenter"
app:srcCompat="@drawable/ic_fullscreen_white_24dp"
tools:ignore="ContentDescription,RtlHardcoded"
android:visibility="gone"
tools:visibility="visible"/>
<LinearLayout
android:id="@+id/bottomControls"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="40dp"
android:layout_alignParentBottom="true"
android:gravity="center"
android:minHeight="40dp"
android:orientation="horizontal"
android:paddingLeft="@dimen/player_main_controls_padding"
android:paddingRight="@dimen/player_main_controls_padding">
@ -368,19 +378,18 @@
android:text="-:--:--"
android:textColor="@android:color/white"
tools:ignore="HardcodedText"
tools:text="1:06:29"/>
tools:text="1:06:29" />
<org.schabi.newpipe.views.MarkableSeekBar
android:id="@+id/playbackSeekBar"
style="@style/Widget.AppCompat.SeekBar"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginTop="2dp"
android:layout_weight="1"
android:paddingBottom="4dp"
android:paddingTop="8dp"
tools:progress="25"
tools:secondaryProgress="50"/>
tools:secondaryProgress="50" />
<TextView
android:id="@+id/playbackEndTime"
@ -390,20 +399,20 @@
android:text="-:--:--"
android:textColor="@android:color/white"
tools:ignore="HardcodedText"
tools:text="1:23:49"/>
tools:text="1:23:49" />
<TextView
android:id="@+id/playbackLiveSync"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:background="?attr/selectableItemBackground"
android:gravity="center"
android:paddingLeft="4dp"
android:paddingRight="4dp"
android:gravity="center"
android:text="@string/duration_live"
android:textAllCaps="true"
android:textColor="@android:color/white"
android:visibility="gone"
android:background="?attr/selectableItemBackground"
tools:ignore="HardcodedText,RtlHardcoded,RtlSymmetry" />
<androidx.appcompat.widget.AppCompatImageButton
@ -411,15 +420,15 @@
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_marginStart="4dp"
android:padding="@dimen/player_main_buttons_padding"
android:background="?attr/selectableItemBackground"
android:clickable="true"
android:focusable="true"
android:padding="@dimen/player_main_buttons_padding"
android:scaleType="fitCenter"
android:visibility="gone"
app:srcCompat="@drawable/ic_fullscreen_white_24dp"
tools:ignore="ContentDescription,RtlHardcoded"
android:visibility="gone"
tools:visibility="visible"/>
tools:visibility="visible" />
</LinearLayout>
</RelativeLayout>
@ -430,42 +439,42 @@
android:orientation="horizontal"
android:weightSum="5.5">
<androidx.appcompat.widget.AppCompatImageButton
android:id="@+id/playPreviousButton"
android:layout_width="0dp"
android:layout_height="40dp"
android:layout_weight="1"
android:layout_marginEnd="10dp"
android:clickable="true"
android:focusable="true"
android:background="?attr/selectableItemBackgroundBorderless"
android:scaleType="fitCenter"
app:srcCompat="@drawable/ic_previous_white_24dp"
tools:ignore="ContentDescription"/>
<androidx.appcompat.widget.AppCompatImageButton
android:id="@+id/playPreviousButton"
android:layout_width="0dp"
android:layout_height="40dp"
android:layout_marginEnd="10dp"
android:layout_weight="1"
android:background="?attr/selectableItemBackgroundBorderless"
android:clickable="true"
android:focusable="true"
android:scaleType="fitCenter"
app:srcCompat="@drawable/ic_previous_white_24dp"
tools:ignore="ContentDescription" />
<androidx.appcompat.widget.AppCompatImageButton
android:id="@+id/playPauseButton"
android:layout_width="0dp"
android:layout_height="60dp"
android:layout_weight="1"
android:background="?attr/selectableItemBackgroundBorderless"
android:scaleType="fitCenter"
app:srcCompat="@drawable/ic_pause_white_24dp"
tools:ignore="ContentDescription"/>
<androidx.appcompat.widget.AppCompatImageButton
android:id="@+id/playPauseButton"
android:layout_width="0dp"
android:layout_height="60dp"
android:layout_weight="1"
android:background="?attr/selectableItemBackgroundBorderless"
android:scaleType="fitCenter"
app:srcCompat="@drawable/ic_pause_white_24dp"
tools:ignore="ContentDescription" />
<androidx.appcompat.widget.AppCompatImageButton
android:id="@+id/playNextButton"
android:layout_width="0dp"
android:layout_height="40dp"
android:layout_weight="1"
android:layout_marginStart="10dp"
android:clickable="true"
android:focusable="true"
android:background="?attr/selectableItemBackgroundBorderless"
android:scaleType="fitCenter"
app:srcCompat="@drawable/ic_next_white_24dp"
tools:ignore="ContentDescription"/>
<androidx.appcompat.widget.AppCompatImageButton
android:id="@+id/playNextButton"
android:layout_width="0dp"
android:layout_height="40dp"
android:layout_marginStart="10dp"
android:layout_weight="1"
android:background="?attr/selectableItemBackgroundBorderless"
android:clickable="true"
android:focusable="true"
android:scaleType="fitCenter"
app:srcCompat="@drawable/ic_next_white_24dp"
tools:ignore="ContentDescription" />
</LinearLayout>
@ -475,48 +484,47 @@
android:id="@+id/playQueuePanel"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="gone"
android:background="?attr/queue_background_color"
android:visibility="gone"
tools:visibility="visible">
<RelativeLayout
android:id="@+id/playQueueControl"
android:layout_width="match_parent"
android:layout_height="60dp"
android:id="@+id/playQueueControl">
android:layout_height="60dp">
<ImageButton
<androidx.appcompat.widget.AppCompatImageButton
android:id="@+id/playQueueClose"
android:layout_width="50dp"
android:layout_height="50dp"
android:layout_centerVertical="true"
android:layout_alignParentEnd="true"
android:layout_centerVertical="true"
android:layout_marginEnd="40dp"
android:padding="10dp"
android:clickable="true"
android:focusable="true"
android:scaleType="fitXY"
android:tint="?attr/colorAccent"
android:src="@drawable/ic_close_white_24dp"
android:background="?android:selectableItemBackground"
tools:ignore="ContentDescription"/>
android:clickable="true"
android:contentDescription="@string/close"
android:focusable="true"
android:padding="10dp"
android:scaleType="fitXY"
app:srcCompat="@drawable/ic_close_white_24dp" />
<ImageButton
android:id="@+id/repeatButton"
android:layout_width="50dp"
android:layout_height="50dp"
android:layout_centerVertical="true"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_marginLeft="40dp"
android:layout_alignParentLeft="true"
android:layout_centerVertical="true"
android:layout_marginStart="40dp"
android:padding="10dp"
android:layout_marginLeft="40dp"
android:background="?android:selectableItemBackground"
android:clickable="true"
android:focusable="true"
android:padding="10dp"
android:scaleType="fitXY"
android:tint="?attr/colorAccent"
android:src="@drawable/exo_controls_repeat_off"
android:background="?android:selectableItemBackground"
tools:ignore="ContentDescription,RtlHardcoded"/>
android:tint="?attr/colorAccent"
tools:ignore="ContentDescription,RtlHardcoded" />
<androidx.appcompat.widget.AppCompatImageButton
android:id="@+id/shuffleButton"
@ -524,14 +532,14 @@
android:layout_height="50dp"
android:layout_centerVertical="true"
android:layout_toRightOf="@id/repeatButton"
android:padding="10dp"
android:background="?android:selectableItemBackground"
android:clickable="true"
android:focusable="true"
android:padding="10dp"
android:scaleType="fitXY"
android:tint="?attr/colorAccent"
app:srcCompat="@drawable/ic_shuffle_white_24dp"
android:background="?android:selectableItemBackground"
tools:ignore="ContentDescription,RtlHardcoded"/>
tools:ignore="ContentDescription,RtlHardcoded" />
</RelativeLayout>
<androidx.recyclerview.widget.RecyclerView
@ -541,7 +549,7 @@
android:layout_below="@id/playQueueControl"
android:scrollbars="vertical"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:listitem="@layout/play_queue_item"/>
tools:listitem="@layout/play_queue_item" />
</RelativeLayout>
@ -555,8 +563,8 @@
android:id="@+id/controlAnimationView"
android:layout_width="100dp"
android:layout_height="100dp"
android:padding="15dp"
android:background="@drawable/background_oval_black_transparent"
android:padding="15dp"
android:visibility="gone"
tools:ignore="ContentDescription"
tools:src="@drawable/ic_fast_rewind_white_24dp"
@ -577,7 +585,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:indeterminate="true"/>
android:indeterminate="true" />
</RelativeLayout>
<RelativeLayout
@ -645,10 +653,10 @@
android:layout_centerInParent="true"
android:layout_marginBottom="58dp"
android:background="#64000000"
android:paddingBottom="10dp"
android:paddingLeft="30dp"
android:paddingRight="30dp"
android:paddingTop="10dp"
android:paddingRight="30dp"
android:paddingBottom="10dp"
android:textColor="@android:color/white"
android:textSize="26sp"
android:textStyle="bold"

View file

@ -1,9 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="64dp"
xmlns:tools="http://schemas.android.com/tools">
android:layout_height="64dp"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto">
<LinearLayout
android:id="@+id/notificationContent"
@ -107,8 +107,8 @@
android:focusable="true"
android:padding="5dp"
android:scaleType="fitCenter"
android:src="@drawable/ic_close_white_24dp"
tools:ignore="ContentDescription,RtlHardcoded"/>
app:srcCompat="@drawable/ic_close_white_24dp"
tools:ignore="ContentDescription,RtlHardcoded" />
</LinearLayout>
<ProgressBar
@ -121,4 +121,4 @@
android:progressDrawable="@drawable/custom_progress_bar"
tools:ignore="RtlHardcoded"
tools:progress="52"/>
</FrameLayout>
</FrameLayout>

View file

@ -29,8 +29,8 @@
android:focusable="true"
android:padding="8dp"
android:scaleType="fitCenter"
android:src="@drawable/ic_close_white_24dp"
tools:ignore="ContentDescription,RtlHardcoded"/>
app:srcCompat="@drawable/ic_close_white_24dp"
tools:ignore="ContentDescription,RtlHardcoded" />
<LinearLayout
@ -162,4 +162,4 @@
android:src="@drawable/exo_controls_next"
tools:ignore="ContentDescription"/>
</RelativeLayout>
</RelativeLayout>
</RelativeLayout>

View file

@ -1,6 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
@ -11,8 +10,8 @@
android:layout_height="wrap_content"
android:layout_gravity="bottom|center_horizontal"
android:layout_marginBottom="24dp"
app:srcCompat="@drawable/ic_close_white_24dp"
app:backgroundTint="@color/light_youtube_primary_color"
app:borderWidth="0dp"
app:fabSize="normal"/>
</FrameLayout>
app:fabSize="normal"
app:srcCompat="@drawable/ic_close_white_24dp" />
</FrameLayout>

View file

@ -12,7 +12,7 @@
android:layout_height="wrap_content"
android:layout_marginLeft="12dp"
android:layout_alignBaseline="@+id/autoplay_switch"
android:text="@string/next_video_title"
android:text="@string/exo_controls_next_description"
android:textAppearance="?android:attr/textAppearanceMedium"
android:textSize="12sp"
tools:ignore="RtlHardcoded" />

View file

@ -0,0 +1,135 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_height="wrap_content"
android:layout_width="match_parent">
<Switch
android:id="@+id/notificationScaleSwitch"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:clickable="false"
android:focusable="false"
app:layout_constraintBottom_toBottomOf="@+id/textView2"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@+id/textView" />
<TextView
android:id="@+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp"
android:maxLines="1"
android:text="@string/notification_scale_to_square_image_title"
android:textAppearance="?android:attr/textAppearanceLarge"
android:textSize="14sp"
app:layout_constraintEnd_toStartOf="@+id/notificationScaleSwitch"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/textView2"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:text="@string/notification_scale_to_square_image_summary"
app:layout_constraintEnd_toStartOf="@+id/notificationScaleSwitch"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/textView" />
<View
android:id="@+id/notificationScaleSwitchClickableArea"
android:layout_width="match_parent"
android:layout_height="0dp"
android:clickable="true"
android:focusable="true"
app:layout_constraintBottom_toTopOf="@+id/divider"
app:layout_constraintTop_toTopOf="parent"
android:background="?android:selectableItemBackground" />
<View
android:id="@+id/divider"
android:layout_width="0dp"
android:layout_height="1dp"
android:layout_marginStart="32dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="32dp"
android:background="?android:attr/listDivider"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/textView2" />
<TextView
android:id="@+id/textView4"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp"
android:clickable="false"
android:focusable="false"
android:gravity="center"
android:lines="4"
android:text="@string/notification_actions_summary"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/divider" />
<include
android:id="@+id/notificationAction0"
layout="@layout/settings_notification_action"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/textView4" />
<include
android:id="@+id/notificationAction1"
layout="@layout/settings_notification_action"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/notificationAction0" />
<include
android:id="@+id/notificationAction2"
layout="@layout/settings_notification_action"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/notificationAction1" />
<include
android:id="@+id/notificationAction3"
layout="@layout/settings_notification_action"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/notificationAction2" />
<include
android:id="@+id/notificationAction4"
layout="@layout/settings_notification_action"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/notificationAction3" />
</androidx.constraintlayout.widget.ConstraintLayout>
</ScrollView>

View file

@ -0,0 +1,88 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?android:selectableItemBackground">
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/notificationActionIcon"
android:layout_width="0dp"
android:layout_height="48dp"
android:layout_marginStart="12dp"
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp"
app:tint="?android:textColorPrimary"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintDimensionRatio="H,1:1"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:ignore="ContentDescription"
tools:src="@drawable/ic_previous_white_24dp" />
<TextView
android:id="@+id/notificationActionTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:ellipsize="end"
android:maxLines="1"
android:textAppearance="?android:attr/textAppearanceLarge"
android:textSize="14sp"
app:layout_constraintBottom_toTopOf="@+id/notificationActionSummary"
app:layout_constraintEnd_toEndOf="@id/notificationActionClickableArea"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toEndOf="@+id/notificationActionIcon"
app:layout_constraintTop_toTopOf="@+id/notificationActionIcon"
app:layout_constraintVertical_chainStyle="packed"
tools:text="@string/notification_action_1_title" />
<TextView
android:id="@+id/notificationActionSummary"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ellipsize="end"
android:maxLines="1"
app:layout_constraintBottom_toBottomOf="@+id/notificationActionIcon"
app:layout_constraintEnd_toEndOf="@+id/notificationActionClickableArea"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="@+id/notificationActionTitle"
app:layout_constraintTop_toBottomOf="@+id/notificationActionTitle"
tools:text="@string/notification_action_play_pause_buffering_value" />
<View
android:id="@+id/notificationActionClickableArea"
android:layout_width="0dp"
android:layout_height="0dp"
android:background="?android:selectableItemBackground"
android:clickable="true"
android:focusable="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/notificationActionCheckBoxClickableArea"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<CheckBox
android:id="@+id/notificationActionCheckBox"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:clickable="false"
android:focusable="false"
app:layout_constraintBottom_toBottomOf="@+id/notificationActionCheckBoxClickableArea"
app:layout_constraintEnd_toEndOf="@+id/notificationActionCheckBoxClickableArea"
app:layout_constraintStart_toStartOf="@+id/notificationActionCheckBoxClickableArea"
app:layout_constraintTop_toTopOf="parent" />
<View
android:id="@+id/notificationActionCheckBoxClickableArea"
android:layout_width="0dp"
android:layout_height="0dp"
android:clickable="true"
android:focusable="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintDimensionRatio="H,1:1"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -47,7 +47,7 @@
android:contentDescription="@string/search"
android:scaleType="fitCenter"
app:srcCompat="?attr/ic_close"
tools:ignore="RtlHardcoded"/>
tools:ignore="RtlHardcoded" />
</FrameLayout>
</FrameLayout>
</FrameLayout>

View file

@ -1,2 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
<resources>
<string name="no_player_found">Geen stream-speler gevind nie. Installeer VLC\?</string>
<string name="main_bg_subtitle">Tik op \"Soek\" om aan die gang te kom</string>
</resources>

View file

@ -143,7 +143,7 @@
<string name="error_snackbar_action">أبلِغ</string>
<string name="what_device_headline">معلومات:</string>
<string name="what_happened_headline">ماذا حدث:</string>
<string name="info_labels">ماذا:\\nطلب:\\nيحتوى اللغة:\\nSالخدمات:\\nتوقيت غرينتش:\\nحزمة:\\nالإصدار:\\nOS إصدار نظام التشغيل:</string>
<string name="info_labels">ماذا:\\nطلب:\\nلغة المحتوى:\\nبلد المحتوى:\\nلغة التطبيق:\\nالخدمات:\\nتوقيت غرينتش:\\nالحزمة:\\nالإصدار:\\nOS الإصدار:</string>
<string name="your_comment">تعليقك (باللغة الإنجليزية):</string>
<string name="error_details_headline">التفاصيل:</string>
<string name="report_error">الإبلاغ عن خطأ</string>
@ -155,15 +155,15 @@
<string name="storage_permission_denied">ترخيص الوصول إلى التخزين أوّلا</string>
<string name="short_thousand">ألف</string>
<string name="short_million">مليون</string>
<string name="short_billion">B</string>
<string name="short_billion">بليون</string>
<string name="no_subscribers">ليس هناك مشترِكون</string>
<plurals name="subscribers">
<item quantity="zero">%s لا يوجد مشترك</item>
<item quantity="one">%s مشترك</item>
<item quantity="two">%s المشتركين</item>
<item quantity="few">%s المشتركين</item>
<item quantity="many">%s المشتركين</item>
<item quantity="other">%s المشتركين</item>
<item quantity="zero">%s لا مشتركين</item>
<item quantity="one">%s مشترِك</item>
<item quantity="two">مشتركين</item>
<item quantity="few">%s مشتركين</item>
<item quantity="many">%s مشتركين</item>
<item quantity="other">%s الاشتراكات</item>
</plurals>
<string name="no_views">دون مشاهدات</string>
<string name="no_videos">لاتوجد فيديوهات</string>
@ -235,12 +235,12 @@
<string name="title_activity_recaptcha">تحدي الكابتشا</string>
<string name="hold_to_append">ضغط مطول للإدراج الى قائمة الانتظار</string>
<plurals name="views">
<item quantity="zero">%s بدون مشهادة</item>
<item quantity="zero">%s بدون مشاهد</item>
<item quantity="one">%s مشاهدة</item>
<item quantity="two">%s مشاهدة</item>
<item quantity="few">%s مشاهدة</item>
<item quantity="many">%s مشاهدة</item>
<item quantity="other">%s مشاهدة</item>
<item quantity="two">%s مشاهدات</item>
<item quantity="few">%s مشاهدات</item>
<item quantity="many">%s مشاهدات</item>
<item quantity="other">%s مشاهدات</item>
</plurals>
<plurals name="videos">
<item quantity="zero">%s فيديو</item>
@ -272,7 +272,7 @@
<string name="no_player_found_toast">لم يتم العثور على مشغل دفق (يمكنك تثبيت VLC لتشغيله).</string>
<string name="import_data_title">استيراد قاعدة البيانات</string>
<string name="export_data_title">تصدير قاعدة البيانات</string>
<string name="import_data_summary">"الكتابة فوق سجل التاريخ والاشتراكات الحالية "</string>
<string name="import_data_summary">يتجاوز السجل والاشتراكات الحالية</string>
<string name="export_data_summary">تصدير السجل، الإشتراكات وقوائم التشغيل</string>
<string name="show_info">عرض المعلومات</string>
<string name="controls_add_to_playlist_title">إضافة إلى</string>
@ -295,8 +295,8 @@
<string name="preferred_player_fetcher_notification_message">تحميل المحتوى المطلوب</string>
<string name="create_playlist">إنشاء قائمة تشغيل جديدة</string>
<string name="delete_playlist">حذف قائمة التشغيل</string>
<string name="rename_playlist">"إعادة تسمية قائمة التشغيل "</string>
<string name="playlist_name_input">التسمية</string>
<string name="rename_playlist">إعادة تسمية</string>
<string name="name">التسمية</string>
<string name="append_playlist">إضافة إلى قائمة تشغيل</string>
<string name="delete_playlist_prompt">هل تريد حذف قائمة التشغيل هذه ؟</string>
<string name="playlist_creation_success">تم إنشاء قائمة التشغيل</string>
@ -325,7 +325,7 @@
<string name="import_ongoing">عملية الإستعادة جارية …</string>
<string name="export_ongoing">عملية التصدير جارية …</string>
<string name="import_file_title">إستيراد ملف</string>
<string name="import_soundcloud_instructions_hint">"معرفك , soundcloud.com/ الخاص بك "</string>
<string name="import_soundcloud_instructions_hint">معرفك, soundcloud.com/هويتك</string>
<string name="download_thumbnail_summary">عند إيقاف تحميل أي صور مصغرة ، وتوفير البيانات واستخدام الذاكرة. تعمل التغييرات على محو ذاكرة التخزين المؤقت للصورة الموجودة على الذاكرة أو على القرص.</string>
<string name="metadata_cache_wipe_title">امسح البيانات الوصفية المخزنة مؤقتًا</string>
<string name="metadata_cache_wipe_summary">إزالة جميع بيانات صفحات الويب المخزنة مؤقتًا</string>
@ -338,7 +338,6 @@
<string name="caption_none">بدون تسميات توضيحية</string>
<string name="caption_setting_title">تسميات توضيحية</string>
<string name="caption_setting_description">تعديل مشغل نص التسمية التوضيحية وأنماط الخلفية. يتطلب إعادة تشغيل التطبيق لتصبح التغييرات سارية المفعول.</string>
<string name="enable_leak_canary_title">تمكين تسرب الكناري</string>
<string name="enable_leak_canary_summary">قد تتسبب مراقبة تسرب الذاكرة في عدم استجابة التطبيق عند تفريغ السجلات</string>
<string name="enable_disposed_exceptions_title">تقرير الأخطاء خارج دورة الحياة</string>
<string name="enable_disposed_exceptions_summary">فرض الإبلاغ عن استثناءات Rx غير القابلة للتسليم خارج دورة حياة الجزء أو النشاط بعد التخلص منها</string>
@ -363,7 +362,7 @@
<string name="title_most_played">الأكثر تشغيلا</string>
<string name="override_current_data">هذا سوف يُزيل إعداداتك الحالية.</string>
<string name="preferred_open_action_settings_title">طريقة \'التشغيل\' المفضلة</string>
<string name="preferred_open_action_settings_summary">"الإجراء الافتراضي عند فتح المحتوى — %s"</string>
<string name="preferred_open_action_settings_summary">الإجراء الافتراضي عند فتح المحتوى — %s</string>
<string name="background_player">مشغل الخلفية</string>
<string name="popup_player">المشغل المنبثق</string>
<string name="previous_export">نسخة احتياطية</string>
@ -388,7 +387,7 @@
<string name="playback_pitch">تردد الصوت</string>
<string name="unhook_checkbox">حل (قد يسبب تشويه)</string>
<string name="import_settings">هل تريد أيضا استيراد الإعدادات؟</string>
<string name="privacy_policy_title">"سياسة الخصوصية في NewPipe "</string>
<string name="privacy_policy_title">سياسة خصوصية NewPipe</string>
<string name="privacy_policy_encouragement">يأخذ مشروع NewPipe خصوصيتك على محمل الجد. لذلك ، لا يجمع التطبيق أي بيانات دون موافقتك.
\nتوضح سياسة خصوصية NewPipe بالتفصيل البيانات التي يتم إرسالها وتخزينها عند إرسال تقرير الأعطال.</string>
<string name="read_privacy_policy">الإطلاع على سياسة الخصوصية</string>
@ -423,7 +422,7 @@
<string name="events">الأحداث</string>
<string name="app_update_notification_channel_description">الإخطارات لإصدار NewPipe الجديد</string>
<string name="download_to_sdcard_error_title">وحدة التخزين الخارجية غير متوفرة</string>
<string name="download_to_sdcard_error_message">"التنزيل على بطاقة SD الخارجية غير ممكن. إعادة تعيين موقع مجلد التحميل؟"</string>
<string name="download_to_sdcard_error_message">لا يمكن التنزيل على بطاقة SD الخارجية. هل تريد إعادة تعيين موقع مجلد التنزيل؟</string>
<string name="saved_tabs_invalid_json">تعذرت قراءة علامات التبويب المحفوظة, لذا استخدم علامات التبويب الافتراضية</string>
<string name="restore_defaults">استعادة الضبط الافتراضي</string>
<string name="restore_defaults_confirmation">هل تريد استعادة الإعدادات الافتراضية؟</string>
@ -469,7 +468,7 @@
<string name="stop">توقف</string>
<string name="max_retry_msg">أقصى عدد للمحاولات</string>
<string name="max_retry_desc">الحد الأقصى لعدد محاولات قبل إلغاء التحميل</string>
<string name="pause_downloads_on_mobile">"إنقطع الإتصال بالشبكة عند التحويل إلى البيانات المتنقلة"</string>
<string name="pause_downloads_on_mobile">المقاطعة على الشبكات المقيسة</string>
<string name="pause_downloads_on_mobile_desc">مفيد عند التبديل إلى بيانات الجوال ، على الرغم من أنه لا يمكن تعليق بعض التنزيلات</string>
<string name="show_comments_title">إظهار التعليقات</string>
<string name="show_comments_summary">عطّله لإخفاء التعليقات</string>
@ -498,7 +497,8 @@
<string name="pause_downloads">إيقاف التحميل مؤقتا</string>
<string name="downloads_storage_ask_title">اسأل عن مكان التنزيل</string>
<string name="downloads_storage_ask_summary">سيُطلب منك مكان حفظ كل تنزيل</string>
<string name="downloads_storage_ask_summary_kitkat">سيُطلب منك مكان حفظ كل تنزيل. اختر SAF إذا كنت تريد التنزيل على بطاقة SD خارجية</string>
<string name="downloads_storage_ask_summary_kitkat">سيطلب منك مكان حفظ كل تنزيل.
\nاختر SAF إذا كنت تريد التنزيل على بطاقة SD خارجية</string>
<string name="downloads_storage_use_saf_title">استخدام آمن</string>
<string name="downloads_storage_use_saf_summary">يسمح \"إطار الوصول إلى التخزين\" بالتنزيل على بطاقة SD خارجية.
\nبعض الأجهزة غير متوافقة</string>
@ -583,7 +583,6 @@
<string name="settings_category_feed_title">تغذية</string>
<string name="feed_create_new_group_button_title">جديد</string>
<string name="feed_group_dialog_delete_message">هل تريد حذف هذه المجموعة\?</string>
<string name="feed_group_dialog_name_input">الاسم</string>
<string name="feed_group_dialog_empty_name">اسم المجموعة فارغ</string>
<plurals name="feed_group_dialog_selection_count">
<item quantity="zero">%d تحديد</item>
@ -650,4 +649,12 @@
<string name="video_detail_by">لـ %s</string>
<string name="channel_created_by">أنشأها %s</string>
<string name="detail_sub_channel_thumbnail_view_description">الصورة الرمزية للقناة</string>
<string name="playlist_page_summary">صفحة قائمة التشغيل</string>
<string name="feed_group_show_only_ungrouped_subscriptions">إظهار الاشتراكات غير المجمعة فقط</string>
<string name="no_playlist_bookmarked_yet">لا توجد إشارات مرجعية لقائمة التشغيل حتى الآن</string>
<string name="select_a_playlist">حدد قائمة تشغيل</string>
<string name="error_report_open_github_notice">يرجى التحقق مما إذا كانت هناك مشكلة في مناقشة تعطلك بالفعل. عند إنشاء تذاكر مكررة ، ستأخذ وقتًا منا يمكن أن نقضيه في إصلاح الخطأ الفعلي.</string>
<string name="error_report_open_issue_button_text">أبلغ عن خطأ على GitHub</string>
<string name="copy_for_github">نسخ تقرير منسق</string>
<string name="search_showing_result_for">عرض النتائج ل : %s</string>
</resources>

View file

@ -1,5 +1,6 @@
<?xml version='1.0' encoding='UTF-8'?>
<resources><string name="main_bg_subtitle">Başlamaq üçün axtarışa bas</string>
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="main_bg_subtitle">Başlamaq üçün axtarışa bas</string>
<string name="view_count_text">%1$s baxış</string>
<string name="upload_date_text">Dərc edilib %1$s</string>
<string name="no_player_found">Axın oynadıcısı tapılmadı. VLC ni yükləmək istərdinizmi?</string>
@ -22,22 +23,17 @@
<string name="subscribed_button_title">Abunə olunub</string>
<string name="channel_unsubscribed">Kanal</string>
<string name="show_info">Məlumat göstər</string>
<string name="tab_main">Əsas</string>
<string name="tab_subscriptions">Abunəliklər</string>
<string name="tab_bookmarks">Əlfəcinlər</string>
<string name="fragment_feed_title">Yeni nə var</string>
<string name="controls_background_title">Arxa fon</string>
<string name="download_path_title">Video yükləmə ünvanı</string>
<string name="download_path_summary">Yüklənən videoları saxlamaq üçün yer</string>
<string name="download_path_dialog_title">Videolar üçün yükləmə yerini daxil et</string>
<string name="download_path_audio_title">Audio yükləmə yeri</string>
<string name="download_path_audio_summary">Yüklənən audioları saxlamaq üçün yer</string>
<string name="download_path_audio_dialog_title">Audiolar üçün yükləmə yerini daxil et</string>
<string name="autoplay_by_calling_app_summary">NewPipe başqa bir uyğulamadan çağrıldığı zaman avtomatik olaraq videonu oynadır</string>
<string name="default_resolution_title">Defolt ölçü</string>
<string name="show_higher_resolutions_title">Daha böyük ölçüləri göstər</string>
@ -52,4 +48,8 @@
<string name="light_theme_title">ıq</string>
<string name="dark_theme_title">Qaranlıq</string>
<string name="black_theme_title">Qara</string>
</resources>
<string name="unsubscribe">Abunəlik ləğvi</string>
<string name="popup_mode_share_menu_title">ılan pəncərə</string>
<string name="screen_rotation">Fırlanma</string>
<string name="open_in_popup_mode">ılan pəncərə modunda aç</string>
</resources>

View file

@ -150,7 +150,7 @@
<string name="delete_all_history_prompt">¿De xuru que quies desaniciar tolos elementos del historial\?</string>
<string name="drawer_header_action_paceholder_text">Equí va apaecer dalgo ceo ;D</string>
<string name="create_playlist">Llista nueva de repoducción</string>
<string name="playlist_name_input">Nome</string>
<string name="name">Nome</string>
<string name="append_playlist">Amestar a una llista de repoducción</string>
<string name="delete_playlist_prompt">¿Desaniciar esta llista de reproducción\?</string>
<string name="playlist_delete_failure">Nun pudo desaniciase la llista de reproducción.</string>

View file

@ -71,7 +71,7 @@
<string name="subscribers_count_not_available">无法得知订阅人数</string>
<string name="updates_setting_description">发布新版本时,通知我升级应用</string>
<string name="grid">网格</string>
<string name="app_update_notification_content_title">NewPipe更新!</string>
<string name="app_update_notification_content_title">NewPipe更新!</string>
<string name="error_http_unsupported_range">服务器不接受 接收 multi-threaded 下载, 以 @string/msg_threads = 1 重试</string>
<string name="autoplay_title">自动播放</string>
<string name="settings_category_clear_data_title">清除数据</string>
@ -191,7 +191,7 @@
<string name="error_snackbar_action">报告</string>
<string name="what_device_headline">信息:</string>
<string name="what_happened_headline">发生了什么:</string>
<string name="info_labels">详情:\\n请求\\n内容语言\\n服务\\nGMT时间\\n包\\n版本\\n操作系统版本</string>
<string name="info_labels">详情:\\n请求\\n内容语言\\n内容国家:\\n客户端语言\\n服务:\\nGMT时间\\n包\\n版本\\n操作系统版本</string>
<string name="your_comment">您的附近说明(请用英文):</string>
<string name="error_details_headline">详细信息:</string>
<string name="list_thumbnail_view_description">视频预览缩略图</string>
@ -324,7 +324,7 @@
<string name="create_playlist">新建播放列表</string>
<string name="delete_playlist">删除</string>
<string name="rename_playlist">重命名</string>
<string name="playlist_name_input">名称</string>
<string name="name">名称</string>
<string name="append_playlist">添加到播放列表</string>
<string name="set_as_playlist_thumbnail">设为播放列表缩略图</string>
<string name="bookmark_playlist">收藏播放列表</string>
@ -341,7 +341,6 @@
<string name="drawer_header_action_paceholder_text">敬请期待</string>
<string name="settings_category_debug_title">调试</string>
<string name="caption_auto_generated">自动生成</string>
<string name="enable_leak_canary_title">启用LeakCanary</string>
<string name="enable_leak_canary_summary">『内存泄漏监视』可能导致应用在『核心转储』时无响应</string>
<string name="enable_disposed_exceptions_title">报告『提前结束Android生命周期』错误</string>
<string name="enable_disposed_exceptions_summary">强制报告处理后的未送达的Activity或Fragment生命周期之外的Rx异常</string>
@ -556,7 +555,6 @@
<item quantity="other">已选中%d</item>
</plurals>
<string name="feed_group_dialog_empty_name">组名为空</string>
<string name="feed_group_dialog_name_input">名称</string>
<string name="feed_group_dialog_delete_message">您要删除该组吗?</string>
<string name="feed_create_new_group_button_title">新建</string>
<string name="settings_category_feed_title">订阅</string>
@ -602,4 +600,11 @@
<string name="show_original_time_ago_title">在项目上显示原始时间</string>
<string name="youtube_restricted_mode_enabled_title">YouTube受限模式</string>
<string name="feed_group_show_only_ungrouped_subscriptions">仅显示未分组订阅</string>
<string name="playlist_page_summary">播放列表页</string>
<string name="no_playlist_bookmarked_yet">尚无播放列表书签</string>
<string name="select_a_playlist">选择播放列表</string>
<string name="error_report_open_github_notice">请检查您的问题是否已经存在。创建重复票证时您需要从我们那里花些时间来让我们修复真正的bug。</string>
<string name="error_report_open_issue_button_text">去GitHub上报告错误</string>
<string name="copy_for_github">复制格式报告</string>
<string name="search_showing_result_for">显示结果为:%s</string>
</resources>

View file

@ -337,7 +337,7 @@
<string name="create_playlist">Стварыць плэйліст</string>
<string name="delete_playlist">Выдаліць</string>
<string name="rename_playlist">Перайменаваць</string>
<string name="playlist_name_input">Імя</string>
<string name="name">Імя</string>
<string name="append_playlist">Дадаць у плэйліст</string>
<string name="set_as_playlist_thumbnail">На мініяцюру плэйліста</string>
<string name="bookmark_playlist">Дадаць плэйліст у закладкі</string>
@ -354,7 +354,6 @@
<string name="caption_auto_generated">Створаны аўтаматычна</string>
<string name="caption_setting_title">Тытры</string>
<string name="caption_setting_description">Змяніць памер тэкста і фон тытраў. Патрэбен перазапуск</string>
<string name="enable_leak_canary_title">Уключыць LeakCanary</string>
<string name="enable_leak_canary_summary">Маніторынг уцечкі памяці можа прывесці да завісання прыкладання</string>
<string name="enable_disposed_exceptions_title">Паведамляць пра памылкі жыццёвага цыклу</string>
<string name="enable_disposed_exceptions_summary">Прымусова паведамляць пра недастаўляемыя Rx-выключэнні па-за фрагментам або жыццёвым цыкле пасля выдалення</string>
@ -494,4 +493,11 @@
<string name="downloads_storage_use_saf_title">Выкарыстоўваць SAF</string>
<string name="downloads_storage_use_saf_summary">Storage Framework Access дазваляе захоўваць файлы на вонкавым назапашвальніку.
\nПадтрымліваецца не ўсімі прыладамі</string>
<string name="drawer_header_description">Пераключыць службу, выбраную ў дадзены момант:</string>
<string name="clear_playback_states_summary">Выдаліць ўсе пазіцыі прайгравання</string>
<string name="youtube_restricted_mode_enabled_title">Абмежаваны рэжым YouTube</string>
<string name="peertube_instance_add_https_only">Падтрымліваюцца толькі адрасы URL HTTPS</string>
<string name="peertube_instance_add_title">Дадаць інстанцыю</string>
<string name="peertube_instance_url_title">Інстанцыі PeerTube</string>
<string name="download_choose_new_path">Змяніце папкі загрузкі, каб змены ўступілі ў сілу</string>
</resources>

View file

@ -0,0 +1,131 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="feed_create_new_group_button_title">ⴰⵎⴰⵢⵏⵓ</string>
<string name="name">ⵉⵙⵎ</string>
<plurals name="days">
<item quantity="one">%d ⵡⴰⵙⵙ</item>
<item quantity="other">%d ⵓⵙⵙⴰⵏ</item>
</plurals>
<plurals name="hours">
<item quantity="one">%d ⵜⵙⵔⵉⴳⵜ</item>
<item quantity="other">%d ⵜⵙⵔⵉⴳⵉⵏ</item>
</plurals>
<plurals name="minutes">
<item quantity="one">%d ⵜⵙⴷⵉⴷⵉⵜ</item>
<item quantity="other">%d ⵜⵙⴷⵉⴷⵉⵏ</item>
</plurals>
<string name="app_language_title">ⵜⵓⵜⵍⴰⵢⵜ ⵓⵙⵏⵙⵉ</string>
<string name="close">ⵇⵇⵏ</string>
<string name="minimize_on_exit_none_description">ⵡⴰⵍⵓ</string>
<string name="playback_step">ⴰⵙⵓⵔⵉⴼ</string>
<string name="resize_zoom">ⵙⵎⵖⵕ</string>
<string name="mute">ⵙⵓⵙⵎ</string>
<string name="rename_playlist">ⵙⵏⴼⵍ ⵉⵙⵎ</string>
<string name="delete_playlist">ⴽⴽⵙ</string>
<string name="play_queue_audio_settings">ⵜⵉⵙⵖⴰⵍ ⵓⵎⵙⵍⴰⵢ</string>
<string name="play_queue_remove">ⴽⴽⵙ</string>
<string name="action_history">ⴰⵎⵣⵔⵓⵢ</string>
<string name="title_history_search">ⵜⴻⵜⵜⵡⴰⵔⵣⵓⵏ</string>
<string name="title_history_view">ⵜⴻⵜⵜⵓⵥⵕ</string>
<string name="title_activity_history">ⴰⵎⵣⵔⵓⵢ</string>
<string name="website_title">ⴰⵙⵉⵜ</string>
<string name="view_on_github">ⵥⵕ ⴳ GitHub</string>
<string name="tab_licenses">ⵜⵓⵔⴰⴳⵜ</string>
<string name="tab_about">ⵅⴼ</string>
<string name="action_open_website">ⵕⵥⵎ ⴰⵙⵉⵜ</string>
<string name="copyright">© %1$s ⵙⴳ %2$s ⴷⴷⴰⵡ %3$s</string>
<string name="action_about">ⵅⴼ</string>
<string name="action_settings">ⵜⵉⵙⵖⴰⵍ</string>
<string name="title_activity_about">ⵅⴼ NewPipe</string>
<string name="msg_wait">ⵕⴰⵊⴰ…</string>
<string name="msg_error">ⵜⴰⵣⴳⵍⵜ</string>
<string name="msg_name">ⵉⵙⵎ ⵓⴼⴰⵢⵍⵓ</string>
<string name="finish">ⵡⴰⵅⵅⴰ</string>
<string name="rename">ⵙⵙⵏⴼⵍ ⵉⵙⵎ</string>
<string name="dismiss">ⵙⵙⵔ</string>
<string name="delete_all">ⴽⴽⵙ ⵎⴰⵕⵕⴰ</string>
<string name="delete_one">ⴽⵙⵙ ⵢⴰⵏ</string>
<string name="delete">ⴽⴽⵙ</string>
<string name="view">ⵖⵔ</string>
<string name="no_comments">ⵓⵔ ⵍⵍⵉⵏ ⵉⵡⴼⴰⵡⴰⵍⵏ</string>
<plurals name="videos">
<item quantity="one">%s ⵓⴼⵉⴷⵢⵓ</item>
<item quantity="other">%s ⵉⴼⵉⴷⵢⵓⵜⵏ</item>
</plurals>
<string name="infinite_videos">∞ ⵉⴼⵉⴷⵢⵓⵜⵏ</string>
<string name="more_than_100_videos">100+ ⵉⴼⵉⴷⵢⵓⵜⵏ</string>
<string name="short_billion"></string>
<string name="short_million"></string>
<string name="short_thousand"></string>
<string name="audio">ⴰⵎⵙⵍⴰⵢ</string>
<string name="video">ⴰⴼⵉⴷⵢⵓ</string>
<string name="detail_likes_img_view_description">ⵉⵔⵉⵜⵏ</string>
<string name="general_error">ⵜⴰⵣⴳⵍⵜ</string>
<string name="help">ⵜⵉⵡⵉⵙⵉ</string>
<string name="file">ⴰⴼⴰⵢⵍⵓ</string>
<string name="play_all">ⵖⵔ ⵎⴰⵕⵕⴰ</string>
<string name="file_deleted">ⵉⵜⵜⵡⴰⴽⴽⵙ ⵓⴼⴰⵢⵍⵓ</string>
<string name="filter">ⴰⵙⵜⵜⴰⵢ</string>
<string name="yes">ⵢⴰⵀ</string>
<string name="videos_string">ⵉⴼⵉⴷⵢⵓⵜⵏ</string>
<string name="all">ⵎⴰⵕⵕⴰ</string>
<string name="downloads_title">ⵓⴳⴳⴰⵎⵏ</string>
<string name="downloads">ⵓⴳⴳⴰⵎⵏ</string>
<string name="duration_live">ⵓⵙⵔⵉⴷ</string>
<string name="play_btn_text">ⵖⵔ</string>
<string name="settings_category_other_title">ⵢⴰⴹⵏ</string>
<string name="settings_category_video_audio_title">ⴰⴼⵉⴷⵢⵓ ⴷ ⵓⵎⵙⵍⴰⵢ</string>
<string name="download_dialog_title">ⴰⴳⵎ</string>
<string name="enable_watch_history_title">ⵥⵕ ⴰⵎⵣⵔⵓⵢ</string>
<string name="enable_search_history_title">ⴰⵎⵣⵔⵓⵢ ⵓⵔⵣⵣⵓ</string>
<string name="black_theme_title">ⴰⴱⵔⴽⴰⵏ</string>
<string name="play_audio">ⴰⵎⵙⵍⴰⵢ</string>
<string name="play_with_kodi_title">ⵖⵔ ⵙ ⴽⵓⴷⵉ</string>
<string name="controls_add_to_playlist_title">ⵔⵏⵓ ⵖⵔ</string>
<string name="tab_choose">ⵙⵜⵉ ⴰⵙⴽⵙⵍ</string>
<string name="tab_new">ⴰⵙⴽⵙⵍ ⴰⵎⴰⵢⵏⵓ</string>
<string name="share_dialog_title">ⴱⴹⵓ ⵙ</string>
<string name="settings">ⵜⵉⵙⵖⴰⵍ</string>
<string name="search">ⵔⵣⵓ</string>
<string name="download">ⴰⴳⵎ</string>
<string name="share">ⴱⴹⵓ</string>
<string name="install">ⵙⵔⵙ</string>
<string name="cancel">ⵙⵔ</string>
<string name="video_detail_by">ⵙ %s</string>
<string name="channel_created_by">ⵔⵏⵏⵓ ⵜ %s</string>
<string name="feed_group_dialog_delete_message">ⵉⵙ ⵜⵅⵙⴷ ⴰⴷ ⵜⴽⴽⵙⴷ ⵜⵔⴰⴱⴱⵓⵜ ⴰ\?</string>
<string name="stop">ⴱⴷⴷ</string>
<string name="label_code">ⵉⵏⵉⴳⵍ</string>
<string name="grid">ⴰⵥⵟⵟⴰ</string>
<string name="list">ⵜⴰⵍⴳⴰⵎⵜ</string>
<string name="updates_setting_title">ⵜⵓⵙⴷⵖⵉⵏ</string>
<string name="decline">ⴰⴳⵉ</string>
<string name="accept">ⵜⴰⵢⵢⵉⵀⵜ</string>
<string name="resize_fill">ⴽⵜⵔ</string>
<string name="resize_fit">ⵙⴰⵙⵜⵡⴰ</string>
<string name="unmute">ⴽⴽⵙ ⴰⵙⵓⵔⵎ</string>
<string name="background_player">ⴰⵎⵖⵔⵉ ⴷⴼⴼⵉⵔ</string>
<string name="video_player">ⴰⵎⵖⵔⵉ ⵓⴼⵉⴷⵢⵓ</string>
<string name="start_here_on_main">ⵖⵔ ⵙⴰ</string>
<string name="use_tor_title">ⵙⵙⵎⵔⵙ ⵟⵓⵕ</string>
<string name="detail_thumbnail_view_description">ⵖⵔ ⴰⴼⵉⴷⵢⵓ,ⴰⵣⵎⵣ:</string>
<string name="your_comment">ⵉⵅⴼⴰⵡⴰⵍ ⵏⵏⴽ (ⵙ ⵜⵏⴳⵍⵉⵣⵜ):</string>
<string name="what_device_headline">ⴰⵏⵖⵎⵉⵙ:</string>
<string name="error_snackbar_action">ⵎⵍ</string>
<string name="download_path_title">ⴰⵙⴷⴰⵡ ⵓⴳⴰⵎ ⵓⴼⵉⴷⵢⵓ</string>
<string name="tab_subscriptions">ⵜⵉⵙⵓⵔⴰ</string>
<string name="tab_main">ⴰⵙⵏⵓⴱⴳ</string>
<string name="show_info">ⵙⵎⴰⵍ ⴰⵏⵖⵎⵉⵙ</string>
<string name="channel_unsubscribed">ⴽⴽⵙ ⵜⴰⵙⵓⵔⵉ ⵉ ⵓⵙⴰⵔⵓ</string>
<string name="unsubscribe">ⴽⴽⵙ ⵜⴰⵙⵓⵔⵉ</string>
<string name="subscribed_button_title">ⵉⵙⵙⵓⵔ</string>
<string name="subscribe_button_title">ⵙⵙⵓⵔ</string>
<string name="use_external_audio_player_title">ⵙⵎⵔⵙ ⵉⵎⵖⵔⵉ ⵉⵎⵙⵍⵉ ⴰⴱⵕⵕⴰⵏⵉ</string>
<string name="use_external_video_player_title">ⵙⵎⵔⵙ ⵉⵎⵖⵔⵉ ⵓⴼⵉⴷⵢⵓ ⴰⴱⵕⵕⴰⵏⵉ</string>
<string name="screen_rotation">ⵓⵜⵓⵢ</string>
<string name="choose_browser">ⵙⵜⵉ ⵉⵎⵉⵏⵉⴳ</string>
<string name="search_showing_result_for">ⴰⵙⵎⴰⵍ ⵜⴰⵢⴰⴼⵓⵜ ⵉ: %s</string>
<string name="did_you_mean">ⵎⵉⵏ ⵜⵅⵙⴷ ⴰⴷ ⵜⵜ ⵜⵉⵏⵉⴷ: %1$s\?</string>
<string name="view_count_text">%1$s ⵜⴰⵏⵏⴰⵢⵉⵏ</string>
<string name="main_bg_subtitle">ⴰⴷⵔ ⵅ \"ⵔⵣⵓ\"ⴰⴼⴰⴷ ⴰⴷ ⵜⵜⴰⵡⵍⴷ</string>
</resources>

View file

@ -330,7 +330,7 @@
<string name="create_playlist">Нов Плейлист</string>
<string name="delete_playlist">Изтрий</string>
<string name="rename_playlist">"Преименувай "</string>
<string name="playlist_name_input">Име</string>
<string name="name">Име</string>
<string name="append_playlist">Добави Към Плейлист</string>
<string name="set_as_playlist_thumbnail">Задай като миниатюра на плейлиста</string>
<string name="bookmark_playlist">Миниатюрата на плейлиста е сменена</string>
@ -347,7 +347,6 @@
<string name="caption_auto_generated">Авто-генерирани</string>
<string name="caption_setting_title">Надписи</string>
<string name="caption_setting_description">Модифицирай мащаба на текста и фона на надписите. Изисква рестарт на приложението, за да се приложат промените.</string>
<string name="enable_leak_canary_title">Включи LeakCanary</string>
<string name="enable_leak_canary_summary">Следенето за пропускане на памет може да направи приложението нестабилно</string>
<string name="enable_disposed_exceptions_title">Докладвай за извънредни грешки</string>
<string name="import_export_title">Импортиране/Експортиране</string>
@ -413,4 +412,5 @@
<string name="settings_category_updates_title">Промени</string>
<string name="enable_playback_resume_title">Продължи възпроизвеждане</string>
<string name="settings_category_clear_data_title">Изтрии данни</string>
<string name="search_showing_result_for">Показване на резултати за: %s</string>
</resources>

View file

@ -196,4 +196,16 @@
<string name="volume_gesture_control_title">ভলিউম সংকেত নিয়ন্ত্রণ</string>
<string name="missions_header_finished">সম্পূর্ণ</string>
<string name="list">তালিকা</string>
<string name="enable_playback_state_lists_title">তালিকাতে পজিশন</string>
<string name="enable_playback_resume_summary">শেষ প্লেব্যাক পজিশন এ যাও</string>
<string name="enable_playback_resume_title">পুনরায় প্লে ব্যাক চালু করো</string>
<string name="enable_search_history_summary">সার্চগুলো স্থানীয়ভাবে জমা করো</string>
<string name="show_search_suggestions_summary">সার্চের সময় পরামর্শ দেখাও</string>
<string name="show_search_suggestions_title">সার্চ পরামর্শ</string>
<string name="player_gesture_controls_summary">প্লেয়ারের উজ্জ্বলতা এবং ভলিউম নিয়ন্ত্রণ করতে সংকেত ব্যবহার করো</string>
<string name="search_showing_result_for">রেজাল্ট দেখানো হচ্ছেঃ %s</string>
<string name="tab_about">সম্পর্কিত</string>
<string name="action_about">সম্পর্কিত</string>
<string name="title_activity_about">নিউপাইপ এর সম্বন্ধে</string>
<string name="trending">ট্রেন্ডিং</string>
</resources>

View file

@ -98,7 +98,7 @@
<string name="settings_category_player_title">প্লেয়ার</string>
<string name="content_language_title">কন্টেন্ট এর জন্য পছন্দসই ভাষা</string>
<string name="service_title">সেবা</string>
<string name="url_not_supported_toast">URL সমর্থিত নয়</string>
<string name="unsupported_url">URL সমর্থিত নয়</string>
<string name="show_next_and_similar_title">পরবর্তী এবং অনুরূপ ভিডিওগুলি দেখাও</string>
<string name="next_video_title">পরবর্তী ভিডিও</string>
<string name="download_dialog_title">ডাউনলোড</string>
@ -186,7 +186,6 @@
<string name="view_count_text">%1$s জন দেখেছে</string>
<string name="main_bg_subtitle">অনুসন্ধান এ চাপ দিয়ে শুরু করুন</string>
<string name="feed_create_new_group_button_title">নতুন</string>
<string name="feed_group_dialog_name_input">নাম</string>
<string name="fragment_feed_title">নতুন কি</string>
<string name="app_language_title">অ্যাপ এর ভাষা</string>
<string name="stop">বন্ধ করুন</string>
@ -239,7 +238,7 @@
<string name="invalid_url_toast">অবৈধ ইউ আর এল</string>
<string name="download_to_sdcard_error_title">বাহ্যিক স্টোরেজ নেই</string>
<string name="search_history_deleted">সার্চ ইতিহাস ডিলিট হয়েছে।</string>
<string name="playlist_name_input">নাম</string>
<string name="name">নাম</string>
<string name="rename_playlist">নাম পরিবর্তন</string>
<string name="action_history">ইতিহাস</string>
<string name="read_full_license">লাইসেন্স পড়ুন</string>
@ -329,4 +328,13 @@
<string name="enable_search_history_summary">সার্চ গুলি স্থানীয় ভাবে জমা করুন</string>
<string name="show_search_suggestions_summary">সার্চ এর সময় সাজেশন দেখান</string>
<string name="show_search_suggestions_title">সার্চ সাজেশন</string>
<string name="feed_notification_loading">ফিড লোড হচ্ছে…</string>
<string name="show_error">এরর দেখান</string>
<string name="select_a_playlist">একটি প্লে লিস্ট পছন্দ করুন</string>
<string name="tab_about">সম্পর্কিত</string>
<string name="title_licenses">থার্ড-পার্টি লাইসেন্স সমূহ</string>
<string name="action_about">সম্পর্কিত</string>
<string name="error_report_open_issue_button_text">গিটহাব এ এরর রিপোর্ট করুন</string>
<string name="restore_defaults">ডিফল্ট এ ফিরে যান</string>
<string name="search_showing_result_for">রেজাল্ট দেখান হচ্ছেঃ %s</string>
</resources>

View file

@ -38,7 +38,7 @@
<string name="content">Contingut</string>
<string name="show_age_restricted_content_title">Desactiva les restriccions per edat</string>
<string name="video_is_age_restricted">Mostra el vídeo restringit per edat. Podeu permetre aquesta mena de continguts des dels paràmetres.</string>
<string name="duration_live">EN DIRECTE</string>
<string name="duration_live">Directe</string>
<string name="downloads">Baixades</string>
<string name="downloads_title">Baixades</string>
<string name="all">Tot</string>
@ -105,7 +105,7 @@
<string name="create_playlist">Crea una llista de reproducció</string>
<string name="delete_playlist">Elimina</string>
<string name="rename_playlist">Canvia el nom</string>
<string name="playlist_name_input">Nom</string>
<string name="name">Nom</string>
<string name="append_playlist">Afegeix a una llista de reproducció</string>
<string name="import_title">Importa</string>
<string name="import_from">Importa des de</string>
@ -123,7 +123,7 @@
<string name="share_dialog_title">Comparteix-ho amb</string>
<string name="screen_rotation">rotació</string>
<string name="use_external_video_player_title">Reproductor de vídeo extern</string>
<string name="popup_mode_share_menu_title">Mode emergent del NewPipe</string>
<string name="popup_mode_share_menu_title">Mode emergent</string>
<string name="channel_unsubscribed">Heu eliminat la subscripció a aquest canal</string>
<string name="subscription_change_failed">No s\'ha pogut modificar la subscripció</string>
<string name="subscription_update_failed">No s\'ha pogut actualitzar la subscripció</string>
@ -140,13 +140,13 @@
<string name="show_higher_resolutions_title">Mostra resolucions superiors</string>
<string name="show_higher_resolutions_summary">No tots els dispositius són compatibles amb la reproducció de vídeos en 2K/4K</string>
<string name="play_with_kodi_title">Reprodueix amb el Kodi</string>
<string name="kore_not_found">No s\'ha trobat l\'aplicació Kodi. Voleu instal·lar-la\?</string>
<string name="kore_not_found">No s\'ha trobat l\'aplicació Kore. Voleu instal·lar-la\?</string>
<string name="show_play_with_kodi_title">Mostra «Reprodueix amb el Kodi»</string>
<string name="show_play_with_kodi_summary">Mostra una opció per reproduir un vídeo amb el centre multimèdia Kodi</string>
<string name="popup_remember_size_pos_title">Reproductor emergent intel·ligent</string>
<string name="popup_remember_size_pos_summary">Recorda la darrera mida i posició del reproductor emergent</string>
<string name="use_inexact_seek_title">Cerca ràpida poc precisa</string>
<string name="use_inexact_seek_summary">La cerca poc precisa permet que el reproductor cerqui una posició més ràpidament amb menys precisió</string>
<string name="use_inexact_seek_summary">La cerca poc precisa permet que el reproductor cerqui una posició més ràpidament amb menys precisió. Cerques de 5, 15 o 25 segons no hi funcionaran.</string>
<string name="download_thumbnail_title">Carrega les miniatures</string>
<string name="thumbnail_cache_wipe_complete_notice">S\'ha eliminat la memòria cau d\'imatges</string>
<string name="metadata_cache_wipe_title">Elimina les metadades de la memòria cau</string>
@ -193,7 +193,7 @@
<string name="error_occurred_detail">S\'ha produït un error: %1$s</string>
<string name="error_report_button_text">Informeu de l\'error per correu electrònic</string>
<string name="error_snackbar_message">S\'han produït alguns errors.</string>
<string name="error_snackbar_action">INFORME</string>
<string name="error_snackbar_action">Informe</string>
<string name="what_device_headline">Informació:</string>
<string name="what_happened_headline">Què ha passat:</string>
<string name="your_comment">Comentari (en anglès):</string>
@ -205,7 +205,7 @@
<string name="detail_dislikes_img_view_description">No m\'agrada</string>
<string name="use_tor_title">Fes servir el Tor</string>
<string name="use_tor_summary">(En proves) Força el trànsit de baixada a través del Tor per a més privadesa (no compatible encara amb les emissions de vídeo).</string>
<string name="report_error">Notifiqueu un error</string>
<string name="report_error">Notifiqueu error</string>
<string name="user_report">Informe de l\'usuari</string>
<string name="search_no_results">Cap resultat</string>
<string name="empty_subscription_feed_subtitle">No hi ha res aquí</string>
@ -272,7 +272,7 @@
<string name="playback_speed_control">Controls de la velocitat de reproducció</string>
<string name="playback_tempo">Tempo</string>
<string name="playback_pitch">To</string>
<string name="main_bg_subtitle">Feu un toc al botó de cerca per començar</string>
<string name="main_bg_subtitle">Toca \"Cercar\" per començar</string>
<string name="use_external_video_player_summary">Elimina l\'àudio en algunes resolucions</string>
<string name="use_external_audio_player_title">Reproductor d\'àudio extern</string>
<string name="download_thumbnail_summary">Desactiveu-ho per no generar miniatures i estalviar dades i memòria. Canviant aquesta opció, s\'eliminarà la memòria cau d\'imatges tant de la memòria com de l\'emmagatzematge.</string>
@ -329,7 +329,6 @@
<string name="resize_fill">Omple</string>
<string name="resize_zoom">Escala</string>
<string name="caption_auto_generated">Generats automàticament</string>
<string name="enable_leak_canary_title">Habilita el LeakCanary</string>
<string name="previous_export">Darrera exportació</string>
<string name="subscriptions_import_unsuccessful">No s\'han pogut importar les subscripcions</string>
<string name="subscriptions_export_unsuccessful">No s\'han pogut exportar les subscripcions</string>
@ -503,8 +502,34 @@
<string name="no_one_listening">Cap reproducció</string>
<plurals name="listening">
<item quantity="one">%s escoltant</item>
<item quantity="other">%s escoltant</item>
<item quantity="other">%s escoltants</item>
</plurals>
<string name="localization_changes_requires_app_restart">Es canviarà la llengua en reiniciar l\'aplicació.</string>
<string name="default_kiosk_page_summary">Tendències</string>
<string name="show_original_time_ago_title">Ensenya el temps passat original sobre els \"items\"</string>
<string name="playlist_no_uploader">Auto-generat (no es troba cap uploader)</string>
<string name="unmute">Desactivar Silenci</string>
<string name="mute">Silenciar</string>
<string name="subtitle_activity_recaptcha">Prem \"Fet\" quan estigui resolt</string>
<string name="infinite_videos">∞ vídeos</string>
<string name="more_than_100_videos">100+ vídeos</string>
<string name="permission_display_over_apps">Dona permís per mostrarse per sobre d\'altres apps.</string>
<string name="help">Ajuda</string>
<string name="artists">Artistes</string>
<string name="albums">Albums</string>
<string name="songs">Cançons</string>
<string name="videos_string">Vídeos</string>
<string name="restricted_video">Aquest vídeo té una restricció per edat.
\n
\nSi desitges visualitzar-lo, activa el «Contingut restringit per edat» en la configuració.</string>
<string name="youtube_restricted_mode_enabled_title">Mode de restricció del YouTube</string>
<string name="peertube_instance_add_exists">La instància introduïda ja existeix</string>
<string name="peertube_instance_add_https_only">Només estàn soportades URLs HTTPS</string>
<string name="peertube_instance_add_fail">No ha estat possible validar la instància</string>
<string name="peertube_instance_add_help">Introdueix l\'enllaç d\'una instància</string>
<string name="peertube_instance_add_title">Afegeix-hi una instància</string>
<string name="peertube_instance_url_help">Troba les instàncies que t\'agraden en %s</string>
<string name="peertube_instance_url_summary">Selecciona les teves instàncies preferides del PeerTube</string>
<string name="peertube_instance_url_title">Instàncies del PeerTube</string>
<string name="seek_duration_title">Avançar/-rebobinar duració cerca</string>
</resources>

View file

@ -31,7 +31,7 @@
<string name="settings_category_player_title">کارپێکەر</string>
<string name="import_title">هێنانەوە</string>
<string name="error_report_button_text">سکاڵا لەسەر کێشە لەڕێگای ئیمێڵ</string>
<string name="playlist_name_input">ناو</string>
<string name="name">ناو</string>
<string name="error_postprocessing_failed">چارەسەرکردن شکستی هێنا</string>
<string name="minimize_on_exit_title">بچوکبوونەوە لەکاتی گۆڕینی داوانامە</string>
<string name="feed_page_summary">پەڕەی نوێترینەکان</string>
@ -141,7 +141,6 @@
<item quantity="other">%s گوێی لێدەگرن</item>
</plurals>
<string name="msg_running">داگرتنەکانی داوانامە</string>
<string name="feed_group_dialog_name_input">ناو</string>
<string name="channels">کەناڵەکان</string>
<string name="action_history">مێژوو</string>
<string name="subscriptions_export_unsuccessful">ناتوانرێ بەشدارییەکان پاشەکەوت بکرێن</string>
@ -248,7 +247,6 @@
<string name="channel_page_summary">پەڕەی کەناڵەکان</string>
<string name="switch_to_background">گۆڕین بۆ پاشبنەما</string>
<string name="infinite_videos">∞ ڤیدیۆ</string>
<string name="enable_leak_canary_title">چالاککردنی LeakCanary</string>
<string name="use_inexact_seek_title">بەکارهێنانی بردنەپێشی ناتەواوی خێرا</string>
<string name="error_occurred_detail">هەڵەیەک ڕوویدا : %1$s</string>
<string name="download_path_dialog_title">بوخچەی داگرتن بۆ پەڕگەی ڤیدیۆکان هەڵبژێرە</string>
@ -556,7 +554,7 @@
<string name="install">دامەزراندن</string>
<string name="info_dir_created">شوێنی داگرتن دانرا \'%1$s\'</string>
<string name="videos_string">ڤیدیۆکان</string>
<string name="url_not_supported_toast">بەستەرەکە پشتگیری نەکراوە</string>
<string name="unsupported_url">بەستەرەکە پشتگیری نەکراوە</string>
<string name="playback_pitch">شەپۆلی دەنگ</string>
<string name="minimize_on_exit_summary">کرداری کاتی گۆڕین بۆ داوانامەیەکی تر لە کارپێکەری ڤیدیۆییەوە — %s</string>
<string name="msg_copied">لەبەرگیرایەوە</string>

View file

@ -108,7 +108,7 @@
<string name="checksum">Kontrolní součet</string>
<string name="no_available_dir">Určete prosím složku pro stahování později v nastavení</string>
<string name="user_report">Hlášení uživatele</string>
<string name="info_labels">Co:\\nŽádost:\\nJazyk obsahu:\\nSlužba:\\nČas GMT:\\nBalíček:\\nVerze:\\nVerze OS:</string>
<string name="info_labels">Co:\\nŽádost:\\nJazyk obsahu\\nZemě obsahu:\\nJazyk aplikace:\\nSlužba:\\nČas GMT:\\nBalíček:\\nVerze:\\nVerze OS:</string>
<string name="all">Vše</string>
<string name="channel">Kanál</string>
<string name="yes">Ano</string>
@ -116,8 +116,8 @@
<string name="short_thousand">tis.</string>
<string name="open_in_popup_mode">Otevřít ve vyskakovacím okně</string>
<string name="short_million">mil.</string>
<string name="msg_popup_permission">Toto oprávnění je vyžadováno pro
otevření ve vyskakovacím okně</string>
<string name="msg_popup_permission">Toto oprávnění je vyžadováno
\npro otevření ve vyskakovacím okně</string>
<string name="use_external_video_player_summary">Odstraňuje zvuk v některých rozlišeních</string>
<string name="show_higher_resolutions_title">Zobrazovat vyšší rozlišení</string>
<string name="show_higher_resolutions_summary">Pouze některá zařízení dokáží přehrát 2K/4K videa</string>
@ -297,7 +297,7 @@ otevření ve vyskakovacím okně</string>
<string name="create_playlist">Nový playlist</string>
<string name="delete_playlist">Vymazat</string>
<string name="rename_playlist">Přejmenovat</string>
<string name="playlist_name_input">Jméno</string>
<string name="name">Jméno</string>
<string name="append_playlist">Přidat do playlistu</string>
<string name="set_as_playlist_thumbnail">Nastavit jako náhled playlistu</string>
<string name="bookmark_playlist">Přidat playlist do záložek</string>
@ -312,8 +312,7 @@ otevření ve vyskakovacím okně</string>
<string name="resize_fill">Vyplnit</string>
<string name="resize_zoom">Zvětšit</string>
<string name="settings_category_debug_title">Ladění</string>
<string name="caption_auto_generated">"Automaticky generováno "</string>
<string name="enable_leak_canary_title">LeakCanary</string>
<string name="caption_auto_generated">Automaticky generováno</string>
<string name="enable_leak_canary_summary">Monitoring úniku paměti může způsobit nereagování aplikace při heap dumpingu</string>
<string name="enable_disposed_exceptions_title">Nahlásit mimo-cyklické chyby</string>
<string name="enable_disposed_exceptions_summary">Vynutit hlášení nedoručitelných výjimek Rx mimo životnost fragmentu nebo aktivity po odstranění</string>
@ -333,12 +332,9 @@ otevření ve vyskakovacím okně</string>
<string name="invalid_file">Soubor neexistuje nebo chybí oprávnění k jeho čtení či zápisu</string>
<string name="file_name_empty_error">Název souboru nesmí být prázdný</string>
<string name="error_occurred_detail">Došlo k chybě: %1$s</string>
<string name="import_export_title">Import/export
\n</string>
<string name="import_title">Importovat
\n</string>
<string name="import_from">Importovat z
\n</string>
<string name="import_export_title">Import/export</string>
<string name="import_title">Importovat</string>
<string name="import_from">Importovat z</string>
<string name="export_to">Exportovat do</string>
<string name="import_ongoing">Importuji…</string>
<string name="export_ongoing">Exportuji…</string>
@ -580,7 +576,6 @@ otevření ve vyskakovacím okně</string>
<item quantity="other">%d vybráno</item>
</plurals>
<string name="feed_group_dialog_empty_name">Prázdné jméno skupiny</string>
<string name="feed_group_dialog_name_input">Jméno</string>
<string name="feed_group_dialog_delete_message">Přejete si smazat tuto skupinu\?</string>
<string name="feed_create_new_group_button_title">Nová</string>
<string name="settings_category_feed_title">Novinky</string>
@ -624,4 +619,12 @@ otevření ve vyskakovacím okně</string>
<string name="video_detail_by">%s</string>
<string name="channel_created_by">Založil %s</string>
<string name="detail_sub_channel_thumbnail_view_description">Miniatura avatara kanálu</string>
<string name="playlist_page_summary">Strana playlistů</string>
<string name="feed_group_show_only_ungrouped_subscriptions">Ukázat jen neseskupené objednávky</string>
<string name="no_playlist_bookmarked_yet">Zatím žádné záložky playlistů</string>
<string name="select_a_playlist">Vybrat playlist</string>
<string name="error_report_open_github_notice">Prosím, ověřte, zda chyba již existuje. Pokud založíte duplikovaný tiket, obíráte nás o čas, který bychom mohli věnovat řešení skutečných chyb.</string>
<string name="error_report_open_issue_button_text">Podat hlášení na GitHubu</string>
<string name="copy_for_github">Zkopírovat formátované hlášení</string>
<string name="search_showing_result_for">Ukazuji výsledky pro: %s</string>
</resources>

View file

@ -332,7 +332,7 @@
<string name="create_playlist">Ny spilleliste</string>
<string name="delete_playlist">Slet</string>
<string name="rename_playlist">Omdøb</string>
<string name="playlist_name_input">Navn</string>
<string name="name">Navn</string>
<string name="append_playlist">Føj til spilleliste</string>
<string name="delete_playlist_prompt">Slet denne spilleliste\?</string>
<string name="playlist_creation_success">Spilleliste oprettet</string>
@ -409,7 +409,6 @@
<string name="playlist_thumbnail_change_success">Miniaturebillede for spilleliste ændret.</string>
<string name="playlist_delete_failure">Kunne ikke slette spilleliste.</string>
<string name="caption_setting_description">Ændr undertekststørrelse og baggrundsstil. Kræver genstart af appen for at træde i kraft.</string>
<string name="enable_leak_canary_title">Aktiver LeakCanary</string>
<string name="enable_leak_canary_summary">Monitorering for hukommelseslækager kan få appen til ikke at svare under heap dumping</string>
<string name="enable_disposed_exceptions_title">Rapporter out-of-lifecycle-fejl</string>
<string name="import_export_title">Importer/eksporter</string>

View file

@ -33,8 +33,8 @@
<string name="list_thumbnail_view_description">Video-Vorschaubild</string>
<string name="detail_thumbnail_view_description">Video abspielen, Dauer:</string>
<string name="detail_uploader_thumbnail_view_description">Avatarbild des Uploaders</string>
<string name="detail_dislikes_img_view_description">Gefällt nicht</string>
<string name="detail_likes_img_view_description">Gefällt</string>
<string name="detail_dislikes_img_view_description">Gefällt mir nicht</string>
<string name="detail_likes_img_view_description">Gefällt mir</string>
<string name="use_external_video_player_title">Externen Video-Player verwenden</string>
<string name="use_external_audio_player_title">Externen Audio-Player verwenden</string>
<string name="background_player_playing_toast">Im Hintergrund abspielen</string>
@ -310,7 +310,6 @@
<string name="file_name_empty_error">Dateiname darf nicht leer sein</string>
<string name="error_occurred_detail">Ein Fehler ist aufgetreten: %1$s</string>
<string name="caption_auto_generated">Automatisch erzeugt</string>
<string name="enable_leak_canary_title">LeakCanary</string>
<string name="import_from">Import von</string>
<string name="export_to">Export nach</string>
<string name="import_ongoing">Importiere…</string>
@ -329,7 +328,7 @@
<string name="invalid_source">Datei-/Inhaltsquelle existiert nicht</string>
<string name="export_complete_toast">Exportiert</string>
<string name="import_complete_toast">Importiert</string>
<string name="playlist_name_input">Name</string>
<string name="name">Name</string>
<string name="import_export_title">Import/Export</string>
<string name="import_title">Import</string>
<string name="subscriptions_import_unsuccessful">Abonnements konnten nicht importiert werden</string>
@ -562,7 +561,6 @@
<item quantity="other">%d ausgewählte</item>
</plurals>
<string name="feed_group_dialog_empty_name">Leerer Gruppenname</string>
<string name="feed_group_dialog_name_input">Name</string>
<string name="feed_group_dialog_delete_message">Möchtest du diese Gruppe löschen\?</string>
<string name="feed_create_new_group_button_title">Neu</string>
<string name="feed_update_threshold_option_always_update">Immer aktualisieren</string>
@ -579,7 +577,7 @@
<string name="feed_oldest_subscription_update">Abos zuletzt aktualisiert: %s</string>
<string name="feed_update_threshold_title">Grenzwert für Feed-Aktualisierung</string>
<string name="feed_use_dedicated_fetch_method_title">Aus fest zugeordnetem Feed abholen wenn verfügbar</string>
<string name="feed_use_dedicated_fetch_method_summary">Steht in manchen Diensten zur Verfügung, ist meist viel schneller, liefert aber eventuell eine eingeschränkte Anzahl an Elementen und oft inkomplette Informationen (z. B. keine Videolänge, keinen Elementtyp, keinen Live-Status).</string>
<string name="feed_use_dedicated_fetch_method_summary">Steht in manchen Diensten zur Verfügung, ist meist viel schneller, liefert aber eventuell eine eingeschränkte Anzahl an Elementen und oft unvollständige Informationen (z. B. keine Videolänge, keinen Elementtyp, keinen Live-Status).</string>
<string name="feed_use_dedicated_fetch_method_help_text">Glaubst du, dass das Laden von Feeds zu langsam ist\? Wenn ja, versuche den Schnelllademodus einzuschalten (du kannst ihn in den Einstellungen oder über die Schaltfläche unten ändern).
\n
\nNewPipe bietet zwei Feed-Ladestrategien:
@ -614,4 +612,9 @@
<string name="channel_created_by">Erstellt von %s</string>
<string name="video_detail_by">Von %s</string>
<string name="feed_group_show_only_ungrouped_subscriptions">Nur nicht gruppierte Abonnements anzeigen</string>
<string name="playlist_page_summary">Playlist-Seite</string>
<string name="no_playlist_bookmarked_yet">Bisher keine Lesezeichen für Wiedergabelisten</string>
<string name="select_a_playlist">Playlist auswählen</string>
<string name="error_report_open_github_notice">Bitte überprüfen Sie, ob es schon Fragen zu diesem Thema gibt. Doppelt erstellte Tickets kosten uns Zeit, die wir nutzen könnten, um diesen Fehler zu beheben.</string>
<string name="search_showing_result_for">Zeige Ergebnisse für: %s</string>
</resources>

View file

@ -122,15 +122,15 @@
<string name="auto_queue_title">Αυτόματη πρόσθεση της επόμενης ροής στην ουρά</string>
<string name="auto_queue_summary">Αυτόματη πρόσθεση μιας σχετικής ροής όταν αναπαράγεται η προηγούμενη ροή σε μια μη-επαναλαμβανόμενη ουρά</string>
<string name="player_gesture_controls_title">Έλεγχος αναπαραγωγής με χειρονομίες</string>
<string name="player_gesture_controls_summary">Χρήση χειρονομιών για τον έλεγχο της φωτεινότητας και της έντασης ήχου της εφαρμογής</string>
<string name="player_gesture_controls_summary">Χρήση χειρονομιών για τον έλεγχο της φωτεινότητας και της έντασης ήχου</string>
<string name="show_search_suggestions_summary">Εμφάνιση υποδείξεων ενώ κάνετε αναζήτηση</string>
<string name="enable_search_history_summary">Αποθήκευση αναζητήσεων στη συσκευή</string>
<string name="enable_watch_history_title">Προβολή Ιστορικού</string>
<string name="enable_watch_history_summary">Κρατήστε ιστορικό των βίντεο που έχετε δει</string>
<string name="resume_on_audio_focus_gain_title">Συνέχεια όταν η εφαρμογή έρθει σε πρώτο πλάνο</string>
<string name="resume_on_audio_focus_gain_summary">Συνέχιση της αναπαραγωγής έπειτα από διακοπές (π.χ.: κλήσεις)</string>
<string name="resume_on_audio_focus_gain_title">Συνέχεια αναπαραγωγής</string>
<string name="resume_on_audio_focus_gain_summary">Συνέχιση της αναπαραγωγής έπειτα από διακοπές (π.χ. κλήσεις)</string>
<string name="show_hold_to_append_title">Εμφάνιση της βοήθειας \"Πιέστε παρατεταμένα για πρόσθεση\"</string>
<string name="show_hold_to_append_summary">Εμφάνιση της βοήθειας όταν έχει πατηθεί το κουμπί Παρασκηνίου ή Αναδυόμενου παραθύρου στη σελίδα λεπτομερειών του βίντεο</string>
<string name="show_hold_to_append_summary">Εμφάνιση υπόδειξης όταν πατηθεί το κουμπί Παρασκηνίου ή Αναδυόμενου παραθύρου στη σελίδα λεπτομερειών του βίντεο</string>
<string name="default_content_country_title">Προεπιλεγμένη χώρα περιεχομένου</string>
<string name="service_title">Υπηρεσία</string>
<string name="settings_category_player_title">Συσκευή Αναπαραγωγής</string>
@ -332,7 +332,7 @@
<string name="create_playlist">Νέα Λίστα Αναπαραγωγής</string>
<string name="delete_playlist">Διαγραφή</string>
<string name="rename_playlist">Μετονομασία</string>
<string name="playlist_name_input">Όνομα</string>
<string name="name">Όνομα</string>
<string name="append_playlist">Προσθήκη στη Λίστα</string>
<string name="set_as_playlist_thumbnail">Ορισμός ως μικρογραφία λίστας αναπαραγωγής</string>
<string name="bookmark_playlist">Προσθήκη Σελιδοδείκτη στη Λίστα</string>
@ -349,7 +349,6 @@
<string name="caption_auto_generated">Αυτόματοι</string>
<string name="caption_setting_title">Υπότιτλοι</string>
<string name="caption_setting_description">Τροποποίηση του μεγέθους και του φόντου των υπότιτλων. Απαιτεί επανεκκίνηση της εφαρμογής.</string>
<string name="enable_leak_canary_title">Ενεργοποίηση του LeakCanary</string>
<string name="enable_leak_canary_summary">Η παρακολούθηση των διαρροών μνήμης μπορεί να προκαλέσει την διακοπή της εφαρμογής</string>
<string name="enable_disposed_exceptions_summary">Υποχρεωτική αναφορά μη παραδοτέων Rx εξαιρέσεων έξω από το κομμάτι ή τον κύκλο δραστηριότητας μετά από απόρριψη</string>
<string name="import_export_title">Εισαγωγή/Εξαγωγή</string>
@ -402,9 +401,9 @@
<string name="tab_new">Νέα Καρτέλα</string>
<string name="tab_choose">Επιλογή Καρτέλας</string>
<string name="volume_gesture_control_title">Ρυθμίσεις χειρονομιών ήχου</string>
<string name="volume_gesture_control_summary">Χρησιμοποιήστε χειρονομίες για τον έλεγχο της έντασης του ήχου</string>
<string name="volume_gesture_control_summary">Χρησιμοποιήστε χειρονομίες για τον έλεγχο έντασης του ήχου</string>
<string name="brightness_gesture_control_title">Ρυθμίσεις χειρονομιών φωτεινότητας</string>
<string name="brightness_gesture_control_summary">Χρησιμοποιήστε χειρονομίες για τον έλεγχο της φωτεινότητας</string>
<string name="brightness_gesture_control_summary">Χρησιμοποιήστε χειρονομίες για τον έλεγχο φωτεινότητας</string>
<string name="settings_category_updates_title">Ενημερώσεις</string>
<string name="events">Συμβάντα</string>
<string name="file_deleted">Το αρχείο διαγράφηκε</string>
@ -517,4 +516,15 @@
<string name="enable_lock_screen_video_thumbnail_title">Μικρογραφίας βίντεο στην οθόνη κλειδώματος</string>
<string name="enable_lock_screen_video_thumbnail_summary">Χρήση μικρογραφίας βίντεο στην οθόνη κλειδώματος</string>
<string name="seek_duration_title">Διάρκεια fastforward και rewind</string>
<string name="systems_language">Προεπιλογή συστήματος</string>
<string name="videos_string">Βίντεο</string>
<string name="search_showing_result_for">Εμφάνιση αποτελεσμάτων για:</string>
<string name="feed_use_dedicated_fetch_method_enable_button">Ενεργοποίηση γρήγορης λειτουργίας</string>
<string name="feed_use_dedicated_fetch_method_disable_button">Απενεργοποίηση γρήγορης λειτουργίας</string>
<string name="content_not_supported">Αυτό το περιεχόμενο δεν υποστηρίζεται ακόμη από το NewPipe.
\n
\nΕλπίζουμε ότι θα υποστηριχθεί σε μια μελλοντική έκδοση.</string>
<string name="detail_sub_channel_thumbnail_view_description">Μικρογραφία avatar του καναλιού</string>
<string name="channel_created_by">Δημιουργήθηκε από %s</string>
<string name="playlist_page_summary">Σελίδα λίστας αναπαραγωγής</string>
</resources>

View file

@ -190,7 +190,7 @@
<string name="create_playlist">Nova ludlisto</string>
<string name="delete_playlist">Forviŝi</string>
<string name="rename_playlist">Alinomi</string>
<string name="playlist_name_input">Nomo</string>
<string name="name">Nomo</string>
<string name="append_playlist">Aldoni al la ludlisto</string>
<string name="set_as_playlist_thumbnail">Meti kiel bildeto de ludlisto</string>
<string name="bookmark_playlist">Legosigno Ludlisto</string>
@ -402,7 +402,6 @@
<string name="resize_zoom">Zomi</string>
<string name="drawer_header_action_paceholder_text">Io aperos ĉi tie baldaŭ ;D</string>
<string name="caption_auto_generated">Aŭtomate generita</string>
<string name="enable_leak_canary_title">LeakCanary</string>
<string name="enable_leak_canary_summary">La monitorado de la memorlikadoj povas frostigi la apon dum la hejta dumpingo</string>
<string name="enable_disposed_exceptions_title">Signali ekster-vivciklajn erarojn</string>
<string name="enable_disposed_exceptions_summary">Perforti signalante neenretigaj Rx esceptoj eksere la fragmento aŭ aktiveco vivciklo post dispono</string>
@ -564,7 +563,6 @@
</plurals>
<string name="feed_group_dialog_empty_selection">No subscription selected</string>
<string name="feed_group_dialog_empty_name">Malplena grupa nomo</string>
<string name="feed_group_dialog_name_input">Nomo</string>
<string name="feed_group_dialog_delete_message">Ĉu vi volas forviŝi tion grupon?</string>
<string name="feed_create_new_group_button_title">Nova</string>
<string name="settings_category_feed_title">Abonfluo</string>

View file

@ -51,17 +51,17 @@
<string name="could_not_load_thumbnails">No se pudo cargar las miniaturas</string>
<string name="youtube_signature_decryption_error">No se pudo descifrar la URL del vídeo</string>
<string name="parsing_error">No se pudo analizar el sitio web</string>
<string name="show_next_and_similar_title">Mostrar vídeos «siguientes» y «similares»</string>
<string name="show_next_and_similar_title">Mostrar vídeos \'Siguientes\' y \'Similares\'</string>
<string name="content_language_title">Idioma predeterminado del contenido</string>
<string name="list_thumbnail_view_description">Miniatura de previsualización del vídeo</string>
<string name="detail_thumbnail_view_description">Reproducir vídeo; duración:</string>
<string name="detail_likes_img_view_description">Me gusta</string>
<string name="detail_dislikes_img_view_description">No me gusta</string>
<string name="detail_uploader_thumbnail_view_description">Miniatura del avatar del usuario</string>
<string name="live_streams_not_supported">Aún no se admiten las transmisiones en vivo</string>
<string name="live_streams_not_supported">Las transmisiones en vivo no son soportadas aún</string>
<string name="content">Contenido</string>
<string name="show_age_restricted_content_title">Contenido restringido por edad</string>
<string name="video_is_age_restricted">Mostrar vídeo restringido por edad. Se pueden realizar más cambios desde los ajustes.</string>
<string name="video_is_age_restricted">Mostrar vídeo restringido por edad. Se pueden realizar cambios futuros desde los ajustes.</string>
<string name="main_bg_subtitle">Toque «Buscar» para empezar</string>
<string name="autoplay_by_calling_app_title">Reproducción automática</string>
<string name="autoplay_by_calling_app_summary">Reproducir un vídeo cuando NewPipe es llamado desde otra app</string>
@ -101,12 +101,12 @@
<string name="msg_url_malform">URL mal escrito o Internet no disponible</string>
<string name="msg_running">NewPipe está descargando</string>
<string name="msg_running_detail">Toque para ver detalles</string>
<string name="msg_wait">Espere…</string>
<string name="msg_wait">Espere, por favor</string>
<string name="msg_copied">Copiado en el portapapeles</string>
<string name="no_available_dir">Defina una carpeta de descargas más tarde en la configuración</string>
<string name="could_not_load_image">No se pudo cargar la imagen</string>
<string name="app_ui_crash">La interfaz de la app dejó de funcionar</string>
<string name="info_labels">Lo sucedido:\\nPetición:\\nIdioma del contenido:\\nServicio:\\nHora GMT:\\nPaquete:\\nVersión:\\nVersión del SO:</string>
<string name="info_labels">Lo sucedido:\\nPetición:\\nIdioma del Contenido:\\nPaís del contenido:\\nIdioma de la aplicación:\\nServicio:\\nHora GMT:\\nPaquete:\\nVersión:\\nVersión del SO:</string>
<string name="black_theme_title">Negro</string>
<string name="all">Todo</string>
<string name="channel">Canal</string>
@ -114,7 +114,7 @@
<string name="later">Después</string>
<string name="short_thousand">k</string>
<string name="short_million">M</string>
<string name="short_billion">MM</string>
<string name="short_billion">G</string>
<string name="open_in_popup_mode">Abrir en modo emergente</string>
<string name="msg_popup_permission">Se necesita este permiso
\npara abrir en modo emergente</string>
@ -193,7 +193,7 @@
<string name="playlist">Lista de reproducción</string>
<string name="undo">Deshacer</string>
<string name="search_no_results">No hay resultados</string>
<string name="empty_subscription_feed_subtitle">Aquí no hay nada más que grillos</string>
<string name="empty_subscription_feed_subtitle">Nada aquí, sino grillos</string>
<string name="no_subscribers">Sin suscriptores</string>
<plurals name="subscribers">
<item quantity="one">%s suscriptor</item>
@ -239,12 +239,12 @@
<string name="start_here_on_main">Comenzar a reproducir aquí</string>
<string name="start_here_on_background">Comenzar a reproducir en segundo plano</string>
<string name="start_here_on_popup">Reproducir en modo emergente</string>
<string name="show_hold_to_append_title">Mostrar consejo «Mantener presionado para agregar»</string>
<string name="show_hold_to_append_title">Mostrar consejo \"Mantener presionado para añadir\"</string>
<string name="new_and_hot">Novedades</string>
<string name="hold_to_append">Mantener presionado para agregar a la cola</string>
<string name="donation_title">Donar</string>
<string name="donation_encouragement">NewPipe es desarrollado por voluntarios que emplean su tiempo libre para brindarle la mejor experiencia. Haga una aportación para ayudarlos a crear un NewPipe mejor mientras disfrutan de una taza de café.</string>
<string name="give_back">Donar</string>
<string name="give_back">Dar de vuelta</string>
<string name="website_title">Sitio web</string>
<string name="website_encouragement">Visite el sitio web de NewPipe para más información y noticias.</string>
<string name="default_content_country_title">País predeterminado del contenido</string>
@ -294,7 +294,7 @@
<string name="create_playlist">Lista de reproducción nueva</string>
<string name="delete_playlist">Eliminar</string>
<string name="rename_playlist">Cambiar nombre</string>
<string name="playlist_name_input">Nombre</string>
<string name="name">Nombre</string>
<string name="append_playlist">Añadir a la lista de reproducción</string>
<string name="set_as_playlist_thumbnail">Definir como miniatura de lista de reproducción</string>
<string name="bookmark_playlist">Marcar lista de reproducción</string>
@ -311,7 +311,6 @@
<string name="resize_zoom">Zoom</string>
<string name="settings_category_debug_title">Depuración</string>
<string name="caption_auto_generated">Auto generados</string>
<string name="enable_leak_canary_title">Activar LeakCanary</string>
<string name="enable_leak_canary_summary">La monitorización de fugas de memoria puede causar que la app no responda cuando hay Heap Dump</string>
<string name="enable_disposed_exceptions_title">Reportar errores fuera del ciclo de duración</string>
<string name="enable_disposed_exceptions_summary">Forzar reporte de excepciones no entregables de RX fuera del fragmento o del ciclo de actividad después del descarte</string>
@ -322,7 +321,7 @@
<string name="file">Archivo</string>
<string name="missing_file">Archivo movido o eliminado</string>
<string name="invalid_directory">La carpeta no existe</string>
<string name="invalid_source">No existe la fuente del archivo/contenido</string>
<string name="invalid_source">No existe tal archivo/origen del contenido</string>
<string name="invalid_file">El archivo no existe o carece de los permisos para leer o escribir en él</string>
<string name="file_name_empty_error">El nombre del archivo no puede estar vacío</string>
<string name="error_occurred_detail">Ocurrió un error: %1$s</string>
@ -379,9 +378,11 @@
<string name="app_license">NewPipe es un software copyleft libre: puedes usarlo, estudiarlo, compartirlo y mejorarlo a voluntad. Específicamente, puedes redistribuirlo y/o modificarlo bajo los términos de la Licencia Pública General GNU publicada por la Free Software Foundation, ya sea la versión 3 de la Licencia, o (a tu elección) cualquier versión posterior.</string>
<string name="import_settings">¿Quiere importar también la configuración\?</string>
<string name="privacy_policy_title">Normativa de privacidad de NewPipe</string>
<string name="privacy_policy_encouragement">El proyecto NewPipe toma su privacidad muy en serio. Por ello, la aplicación no recopila ningún dato sin su consentimiento. La normativa de privacidad de NewPipe explica en detalle qué datos se envían y almacenan cuando envía un informe de fallo.</string>
<string name="privacy_policy_encouragement">El proyecto NewPipe toma su privacidad muy en serio. Por ello, la aplicación no recopila algún dato sin su consentimiento.
\nLa normativa de privacidad de NewPipe explica en detalle qué datos se envían y almacenan cuando envía un informe de fallo.</string>
<string name="read_privacy_policy">Leer la normativa de privacidad</string>
<string name="start_accept_privacy_policy">Para cumplir con el Reglamento general europeo de protección de datos (GDPR), podemos llamar su atención sobre la política de privacidad de NewPipe. Por favor léelo cuidadosamente. Debe aceptarlo para enviarnos el informe de error.</string>
<string name="start_accept_privacy_policy">Para cumplir con el Reglamento general europeo de protección de datos (GDPR), atraemos su atención sobre la política de privacidad de NewPipe. Por favor léase cuidadosamente.
\nDebe aceptarlo para enviarnos el informe de error.</string>
<string name="accept">Aceptar</string>
<string name="decline">Declinar</string>
<string name="limit_data_usage_none_description">Sin límite</string>
@ -456,7 +457,7 @@
<string name="downloads_storage_use_saf_title">Usar SAF</string>
<string name="downloads_storage_use_saf_summary">El \'Framework de Acceso al Almacenamiento\' permite descargar en una tarjeta SD externa.
\nAlgunos dispositivos no son compatibles</string>
<string name="unsubscribe">Cancelar suscripción</string>
<string name="unsubscribe">Desuscribirse</string>
<string name="tab_new">Pestaña nueva</string>
<string name="tab_choose">Elija la pestaña</string>
<string name="volume_gesture_control_title">Control de volumen por gestos</string>
@ -526,11 +527,11 @@
<string name="playlist_no_uploader">Generado automáticamente (no se encontró creador)</string>
<string name="choose_instance_prompt">Elige una instancia</string>
<string name="enable_lock_screen_video_thumbnail_title">Miniatura de vídeo en pantalla de bloqueo</string>
<string name="enable_lock_screen_video_thumbnail_summary">Se mostrará una miniatura del vídeo en la pantalla de bloqueo al usar el reproductor en segundo plano</string>
<string name="enable_lock_screen_video_thumbnail_summary">Una miniatura del vídeo es mostrada en la pantalla de bloqueo cuando se está usando el reproductor en segundo plano</string>
<string name="clear_download_history">Limpiar historial de descargas</string>
<string name="delete_downloaded_files">Eliminar archivos descargados</string>
<string name="deleted_downloads">Eliminadas %1$d descargas</string>
<string name="permission_display_over_apps">Dar permisos para que se muestre por sobre otras apps</string>
<string name="permission_display_over_apps">Permitir mostrar sobre otras aplicaciones</string>
<string name="app_language_title">Idioma de aplicación</string>
<string name="systems_language">Predeterminado del sistema</string>
<string name="subtitle_activity_recaptcha">Pulse en «Hecho» cuando esté resuelto</string>
@ -568,7 +569,6 @@
<item quantity="other">%d elegidas</item>
</plurals>
<string name="feed_group_dialog_empty_name">Nombre de grupo vacío</string>
<string name="feed_group_dialog_name_input">Nombre</string>
<string name="feed_group_dialog_delete_message">¿Borrar este grupo\?</string>
<string name="feed_create_new_group_button_title">Nuevo</string>
<string name="settings_category_feed_title">Contenido</string>
@ -612,4 +612,12 @@
<string name="show_original_time_ago_summary">Los textos originales de los servicios serán visibles en los ítems de transmisiones</string>
<string name="show_original_time_ago_title">Mostrar tiempo atrás original en ítems</string>
<string name="youtube_restricted_mode_enabled_title">Modo restringido de YouTube</string>
<string name="playlist_page_summary">Página de lista de reproducción</string>
<string name="feed_group_show_only_ungrouped_subscriptions">Mostrar sólo suscripciones desagrupadas</string>
<string name="no_playlist_bookmarked_yet">Sin marcadores de lista de reproducción aún</string>
<string name="select_a_playlist">Seleccione una lista de reproducción</string>
<string name="error_report_open_github_notice">Por favor revise si ya existe una discusión sobre su problema. Cuando se crean entradas duplicadas, toma tiempo de nosotros que podríamos usar para arreglar tal problema.</string>
<string name="error_report_open_issue_button_text">Reportar error en Github</string>
<string name="copy_for_github">Copiar reporte con formato</string>
<string name="search_showing_result_for">Mostrando resultados para: %s</string>
</resources>

View file

@ -318,7 +318,7 @@
<string name="create_playlist">Uus pleilist</string>
<string name="delete_playlist">Kustuta</string>
<string name="rename_playlist">Nimeta ümber</string>
<string name="playlist_name_input">Nimi</string>
<string name="name">Nimi</string>
<string name="append_playlist">Lisa pleilisti</string>
<string name="set_as_playlist_thumbnail">Määra pleilisti pisipildiks</string>
<string name="bookmark_playlist">Lisa pleilist järjehoidjaks</string>
@ -376,7 +376,6 @@
<string name="privacy_policy_encouragement">NewPipe võtab privaatsust väga tõsiselt. Seetõttu ei kogu rakendus ilma nõusolekuta mingeid andmeid.
\nNewPipe privaatsuspoliitika selgitab üksikasjalikult, milliseid andmeid saadetakse ja kogutakse veateate saatmisel.</string>
<string name="app_license">NewPipe vaba avatud koodiga tarkvara. Seada võib kasutada, uurida, jagada ja parandada. Täpsemalt - seda võib levitada ja/või muuta vastavalt Vaba Tarkvara Sihtasutuse avaldatud GNU Üldise Avaliku Litsentsi v.3 (või hilisem) tingimustele.</string>
<string name="enable_leak_canary_title">Luba LeakCanary</string>
<string name="enable_disposed_exceptions_title">Teavita elutsüklist väljas vigadest</string>
<string name="import_soundcloud_instructions">Impordi SoundCloudi profiil trükkides URL või oma ID:
\n

View file

@ -322,7 +322,7 @@
<string name="create_playlist">Erreprodukzio-zerrenda berria</string>
<string name="delete_playlist">Ezabatu</string>
<string name="rename_playlist">Aldatu izena</string>
<string name="playlist_name_input">Izena</string>
<string name="name">Izena</string>
<string name="append_playlist">Gehitu erreprodukzio-zerrendara</string>
<string name="set_as_playlist_thumbnail">Ezarri erreprodukzio-zerrendaren iruditxo gisa</string>
<string name="bookmark_playlist">Gogoko erreprodukzio-zerrenda</string>
@ -339,7 +339,6 @@
<string name="caption_auto_generated">Automatikoki sortuak</string>
<string name="caption_setting_title">Azpitituluak</string>
<string name="caption_setting_description">Aldatu azpitituluen testuaren eskala eta atzealdeko estiloa. Aplikazioa berrabiarazi behar da aldaketak aplikatzeko.</string>
<string name="enable_leak_canary_title">LeakCanary</string>
<string name="enable_leak_canary_summary">Memoria galeren monitorizazioa. Aplikazioak agian ez du erantzungo memoriaren aitortza egin bitartean</string>
<string name="enable_disposed_exceptions_title">Eman bizitza-ziklo kanpoko erroreen berri</string>
<string name="import_export_title">Inportatu/esportatu</string>
@ -561,7 +560,6 @@
<string name="settings_category_feed_title">Jarioa</string>
<string name="feed_create_new_group_button_title">Berria</string>
<string name="feed_group_dialog_delete_message">Talde hau ezabatu nahi duzu\?</string>
<string name="feed_group_dialog_name_input">Izena</string>
<string name="feed_group_dialog_empty_name">Talde izena hutsik</string>
<plurals name="feed_group_dialog_selection_count">
<item quantity="one">%d hautatuta</item>

View file

@ -269,7 +269,7 @@
<string name="create_playlist">فهرست پخش جدید</string>
<string name="delete_playlist">پاک‌کردن</string>
<string name="rename_playlist">تغییر نام</string>
<string name="playlist_name_input">نام</string>
<string name="name">نام</string>
<string name="append_playlist">افزودن به فهرست پخش</string>
<string name="set_as_playlist_thumbnail">استفاده به عنوان تصویر فهرست پخش</string>
<string name="delete_playlist_prompt">این فهرست پخش پاک شود؟</string>
@ -416,7 +416,7 @@
<string name="metadata_cache_wipe_complete_notice">فراداده‌های کش شده پاکش شدند</string>
<string name="playback_tempo">تندا</string>
<string name="playback_pitch">زیر و بمی</string>
<string name="unhook_checkbox">قطع پیوند (ممکن است باعث اعوجاج شود)</string>
<string name="unhook_checkbox">قطع پیوند (ممکن است باعث انحراف شود)</string>
<string name="preferred_open_action_settings_title">ترجیح کنش «باز کردن»</string>
<string name="preferred_open_action_settings_summary">کنش پیش‌فرض در زمان باز کردن محتوا — %s</string>
<string name="caption_setting_description">سبک پس‌زمینه و اندازه متن توضیحات پخش‌کننده را تغییر بده. برای تاثیرگذاری، نیازمند بازراه‌اندازی برنامه است.</string>
@ -431,7 +431,7 @@
<string name="playback_reset">بازنشانی</string>
<string name="download_to_sdcard_error_title">فضای ذخیره‌سازی خارجی در دسترس نیست</string>
<string name="download_to_sdcard_error_message">بارگیری روی کارت SD خارجی ممکن نیست. مایلید محل پوشه بارگیری را دوباره تعیین کنید؟</string>
<string name="saved_tabs_invalid_json">استفاده از برگه‌های پیش‌فرض، خطا حین خواندن برگه‌های ذخیره شده</string>
<string name="saved_tabs_invalid_json">به دلیل ناتوانی در خواندن برگه‌های ذخیره شده، برگه‌های پیش‌فرض استفاده می‌شوند</string>
<string name="subscribers_count_not_available">تعداد مشترک‌شدگان دردسترس نیست</string>
<string name="main_page_content_summary">چه برگه‌هایی در صفحه اصلی نمایش پیدا کنند</string>
<string name="updates_setting_description">زمانی که نسخه جدید برنامه دردسرس است، اعلانی برای به‌روزرسانی نمایش بده</string>
@ -516,7 +516,6 @@
<string name="settings_category_feed_title">خوراک</string>
<string name="feed_create_new_group_button_title">جدید</string>
<string name="feed_group_dialog_delete_message">می‌خواهید این گروه را پاک کنید؟</string>
<string name="feed_group_dialog_name_input">نام</string>
<plurals name="feed_group_dialog_selection_count">
<item quantity="one">%d مورد انتخاب شده</item>
<item quantity="other">%d مورد انتخاب شده</item>
@ -582,4 +581,17 @@
<string name="albums">آلبوم‌ها</string>
<string name="songs">موسیقی‌ها</string>
<string name="youtube_restricted_mode_enabled_title">حالت محدودیت یوتیوب</string>
<string name="search_showing_result_for">نمایش نتایج برای: %s</string>
<string name="error_report_open_issue_button_text">گزارش خطا در گیت‌هاب</string>
<string name="error_report_open_github_notice">لطفا بررسی کنید که آیا گفتگویی درباره مشکل‌تان از قبل وجود دارد یا خیر. ایجاد گزارش تکراری، وقتی را از ما می‌گیرد که ما می‌توانستیم صرف رفع مشکلات واقعی کنیم.</string>
<string name="select_a_playlist">انتخاب یک فهرست پخش</string>
<string name="no_playlist_bookmarked_yet">نشانک‌های فهرست پخش هنوز موجود نیستند</string>
<string name="copy_for_github">گزارش قالب بندی شده را رونویسی کنید</string>
<string name="playlist_no_uploader">تولید شده به صورت خودکار (بارگذارکننده ای یافت نشد)</string>
<string name="feed_groups_header_title">دسته بندی کانال ها</string>
<string name="feed_group_dialog_select_subscriptions">انتخاب اشتراک ها</string>
<string name="feed_group_dialog_empty_selection">اشتراکی انتخاب نشده است</string>
<string name="feed_group_show_only_ungrouped_subscriptions">نمایش اشتراک های دسته بندی نشده</string>
<string name="detail_sub_channel_thumbnail_view_description">آواتار بندانگشتی کانال</string>
<string name="playlist_page_summary">صفحه فهرست پخش</string>
</resources>

View file

@ -17,14 +17,14 @@
<string name="choose_browser">Valitse selain</string>
<string name="screen_rotation">kierto</string>
<string name="use_external_video_player_title">Käytä ulkoista videosoitinta</string>
<string name="use_external_video_player_summary">Poistaa äänen joillakin resoluutioilla</string>
<string name="use_external_video_player_summary">Ääni saattaa lakata toimimasta joillakin resoluutioilla</string>
<string name="use_external_audio_player_title">Käytä ulkoista äänisoitinta</string>
<string name="popup_mode_share_menu_title">Ponnahdusikkunatila</string>
<string name="subscribe_button_title">Tilaa</string>
<string name="subscribed_button_title">Tilattu</string>
<string name="channel_unsubscribed">Kanavan tilaus peruttu</string>
<string name="subscription_change_failed">Tilauksen vaihtaminen epäonnistui</string>
<string name="subscription_update_failed">Ei pystytty päivittämään tilausta</string>
<string name="subscription_update_failed">Tilausta ei voitu päivittää</string>
<string name="tab_main">Päävalikko</string>
<string name="tab_subscriptions">Tilaukset</string>
<string name="fragment_feed_title">Uudet</string>
@ -40,12 +40,12 @@
<string name="autoplay_by_calling_app_summary">Toistaa videon automaattisesti, kun NewPipe avataan toisesta ohjelmasta</string>
<string name="default_resolution_title">Oletusresoluutio</string>
<string name="default_popup_resolution_title">Ponnahdusikkunan oletusresoluutio</string>
<string name="show_higher_resolutions_title">Näytä korkeampia resoluutioita</string>
<string name="show_higher_resolutions_summary">Vain jotkin laitteet voivat toistaa 2K/4K-videoa</string>
<string name="play_with_kodi_title">Toista Kodi:ssa</string>
<string name="show_higher_resolutions_title">Näytä korkeammat resoluutiot</string>
<string name="show_higher_resolutions_summary">Vain jotkin laitteet voivat toistaa 2K/4K-videota</string>
<string name="play_with_kodi_title">Toista Kodissa</string>
<string name="kore_not_found">Asennetaanko puuttuva Kore-sovellus\?</string>
<string name="show_play_with_kodi_title">Näytä \"Toista Kodi:ssa\" vaihtoehto</string>
<string name="show_play_with_kodi_summary">Näyttää painikkeen, jolla voi toistaa videon Kodi media center:llä</string>
<string name="show_play_with_kodi_title">Näytä \"Toista Kodissa\"-vaihtoehto</string>
<string name="show_play_with_kodi_summary">Näyttää vaihtoehdon videon toistamiseen Kodi-mediasoittimessa</string>
<string name="play_audio">Ääni</string>
<string name="default_audio_format_title">Oletusääniformaatti</string>
<string name="default_video_format_title">Oletusvideoformaatti</string>
@ -68,7 +68,7 @@
<string name="next_video_title">Seuraava</string>
<string name="show_next_and_similar_title">Näytä seuraavia ja samankaltaisia videoita</string>
<string name="unsupported_url">URL ei tuettu</string>
<string name="content_language_title">Oletus-sisällon kieli</string>
<string name="content_language_title">Sisällon oletuskieli</string>
<string name="settings_category_player_title">Soitin</string>
<string name="settings_category_player_behavior_title">Käyttäytyminen</string>
<string name="settings_category_video_audio_title">Video &amp; ääni</string>
@ -102,12 +102,12 @@
<string name="notification_channel_description">Ilmoitukset NewPipen tausta- ja ponnahdusikkunasoittimille</string>
<string name="general_error">Virhe</string>
<string name="network_error">Verkkovirhe</string>
<string name="could_not_load_thumbnails">Ei pystytty lataamaan kaikkia esikatselukuvia</string>
<string name="youtube_signature_decryption_error">Ei pystytty purkamaan salausta videon URL allekirjoitukselle</string>
<string name="parsing_error">Ei pystytty jäsentämään websivua</string>
<string name="light_parsing_error">Ei pystytty jäsentämään websivua kokonaan</string>
<string name="could_not_load_thumbnails">Kaikkia esikatselukuvia ei voitu ladata</string>
<string name="youtube_signature_decryption_error">Videon URL-allekirjoituksen salausta ei voitu purkaa</string>
<string name="parsing_error">Verkkosivua ei voitu jäsentää</string>
<string name="light_parsing_error">Verkkosivua ei voitu täysin jäsentää</string>
<string name="content_not_available">Sisältö ei ole saatavilla</string>
<string name="could_not_setup_download_menu">Ei pystytty asettamaan latausvalikkoa</string>
<string name="could_not_setup_download_menu">Latausvalikkoa ei voitu asettaa</string>
<string name="live_streams_not_supported">Live-suoratoistoa ei vielä tueta</string>
<string name="could_not_get_stream">Suoratoistosisältöä ei saatu</string>
<string name="could_not_load_image">Kuvan lataus epäonnistui</string>
@ -178,9 +178,9 @@
<string name="recaptcha_request_toast">reCAPTCHA-haaste pyydetty</string>
<string name="settings_category_downloads_title">Lataus</string>
<string name="settings_file_charset_title">Sallitut merkit tiedostonimissä</string>
<string name="settings_file_replacement_character_summary">Epäkelvot merkit korvataan tällä arvolla</string>
<string name="settings_file_replacement_character_summary">Kielletyt merkit korvataan tällä arvolla</string>
<string name="settings_file_replacement_character_title">Korvaava merkki</string>
<string name="charset_letters_and_digits">Kirjaimia ja numeroita</string>
<string name="charset_letters_and_digits">Kirjaimet ja numerot</string>
<string name="charset_most_special_characters">Suurin osa erikoismerkeistä</string>
<string name="title_activity_about">Tietoja NewPipe</string>
<string name="action_settings">Asetukset</string>
@ -208,7 +208,7 @@
<string name="delete_item_search_history">Haluatko poistaa tämän hakuhistoriasta?</string>
<string name="resume_on_audio_focus_gain_title">Jatka toistoa</string>
<string name="what_device_headline">Info:</string>
<string name="info_labels">Mikä:\\nPyyntö:\\nSisällön kieli:\\nPalvelu:\\nGMT Aika:\\nPaketti:\\nVersio:\\nOS versio:</string>
<string name="info_labels">Mikä:\\nPyyntö:\\nSisällön kieli:\\nSisällön maa:\\n:Sovelluksen kieli:\\nPalvelu:\\nGMT Aika:\\nPaketti:\\nVersio:\\nOS versio:</string>
<string name="copyright" formatted="true">© %1$s %2$s %3$s alla</string>
<string name="main_page_content">Pääsivun sisältö</string>
<string name="blank_page_summary">Tyhjä sivu</string>
@ -249,7 +249,7 @@
<string name="no_player_found_toast">Suoratoistosoitinta ei löytynyt (voit asentaa VLC:n toistaaksesi).</string>
<string name="controls_download_desc">Lataa suoratoistotiedosto</string>
<string name="show_info">Näytä lisätietoja</string>
<string name="tab_bookmarks">Kirjanmerkityt soittolistat</string>
<string name="tab_bookmarks">Soittolistakirjanmerkit</string>
<string name="controls_add_to_playlist_title">Lisää soittolistaan</string>
<string name="use_inexact_seek_title">Käytä nopeampaa epätarkkaa pikakelausta</string>
<string name="use_inexact_seek_summary">Epätarkka kelaus mahdollistaa videon kelauksen nopeammin huonommalla tarkkuudella. Kelaaminen 5, 15 tai 25 sekuntia ei toimi tämän kanssa.</string>
@ -334,7 +334,7 @@
<string name="create_playlist">Uusi soittolista</string>
<string name="delete_playlist">Poista</string>
<string name="rename_playlist">Uudelleennimeä</string>
<string name="playlist_name_input">Nimi</string>
<string name="name">Nimi</string>
<string name="append_playlist">Lisää soittolistaan</string>
<string name="set_as_playlist_thumbnail">Aseta soittolistan kuvakkeeksi</string>
<string name="bookmark_playlist">Tallenna soittolista kirjanmerkkeihin</string>
@ -351,7 +351,6 @@
<string name="caption_auto_generated">Automaattisesti luotu</string>
<string name="caption_setting_title">Tekstitykset</string>
<string name="caption_setting_description">Muokkaa soittimen tekstitysten kokoa ja taustatyylejä. Asetusten käyttöönotto vaatii uudelleenkäynnistyksen.</string>
<string name="enable_leak_canary_title">LeakCanary</string>
<string name="enable_leak_canary_summary">Muistivuotojen valvonta voi aiheuttaa ohjelman hidastumisen virhetilanteissa</string>
<string name="enable_disposed_exceptions_title">Raportoi yhteensopivuusvirheitä, jotka aiheutuvat vanhoista ohjelmista</string>
<string name="enable_disposed_exceptions_summary">Pakota raportointi toimituskelvottomille Rx-poikkeuksille, jotka ovat poiston jälkeen muistisirpaleiden tai aktiviteettielämänkaaren ulkopuolella</string>
@ -392,7 +391,7 @@
<string name="accept">Hyväksy</string>
<string name="decline">Hylkää</string>
<string name="limit_data_usage_none_description">Ei rajaa</string>
<string name="limit_mobile_data_usage_title">Rajoita resoluutiota kun mobiilidata on käytössä</string>
<string name="limit_mobile_data_usage_title">Rajoita resoluutiota mobiilidataa käytettäessä</string>
<string name="minimize_on_exit_title">Pienennä vaihdettaessa ohjelmaa</string>
<string name="minimize_on_exit_summary">Toiminto vaihdettaessa toiseen ohjelmaan päävideosoittimesta — %s</string>
<string name="minimize_on_exit_none_description">Ei koskaan</string>
@ -510,11 +509,10 @@
<string name="feed_use_dedicated_fetch_method_title">Hae erityisestä syötteestä, kun sellainen on saatavilla</string>
<string name="feed_update_threshold_option_always_update">Päivitä aina</string>
<string name="feed_update_threshold_summary">Edellisestä päivityksestä kulunut aika, jonka jälkeen tilaus katsotaan vanhentuneeksi</string>
<string name="feed_update_threshold_title">Syötteen päivitysvälin kynnysarvo</string>
<string name="feed_update_threshold_title">Syötteen päivitysväli</string>
<string name="settings_category_feed_title">Syöte</string>
<string name="feed_create_new_group_button_title">Uusi</string>
<string name="feed_group_dialog_delete_message">Haluatko poistaa tämän ryhmän\?</string>
<string name="feed_group_dialog_name_input">Nimi</string>
<string name="feed_group_dialog_empty_name">Tyhjä ryhmän nimi</string>
<plurals name="feed_group_dialog_selection_count">
<item quantity="one">%d valittu</item>
@ -550,13 +548,13 @@
<string name="choose_instance_prompt">Valitse instanssi</string>
<string name="downloads_storage_use_saf_summary">\'Storage Access Framework\' sallii lataukset ulkoiselle SD-kortille.
\nJotkin laitteet eivät ole yhteensopivia</string>
<string name="downloads_storage_use_saf_title">Käytä SAF:ää</string>
<string name="downloads_storage_use_saf_title">Ota SAF käyttöön</string>
<string name="downloads_storage_ask_summary_kitkat">Jokaisen latauksen kohde kysytään.
\nValitse SAF, jos haluat ladata ulkoiselle SD-kortille</string>
<string name="downloads_storage_ask_summary">Jokaisen latauksen kohde kysytään</string>
<string name="downloads_storage_ask_title">Kysy mihin ladataan</string>
<string name="start_downloads">Aloita lataukset</string>
<string name="enable_queue_limit_desc">Yksi lataus kerrallaan on käynnissä</string>
<string name="enable_queue_limit_desc">Salli vain yksi lataus kerrallaan</string>
<string name="enable_queue_limit">Rajoita latausjonon kokoa</string>
<string name="max_retry_desc">Suurin määrä yrityksiä ennen kuin lataus perutaan</string>
<string name="max_retry_msg">Uudelleenyritysten maksimimäärä</string>
@ -596,7 +594,7 @@
<string name="show_original_time_ago_title">Näytä alkuperäinen aika sisällölle</string>
<string name="pause_downloads">Tauota lataukset</string>
<string name="pause_downloads_on_mobile_desc">Hyödyllinen vaihdettaessa mobiilidataan, vaikka joitakin latauksia ei voi pysäyttää</string>
<string name="pause_downloads_on_mobile">Keskeytä käytön mukaan laskutettavilla yhteyksillä</string>
<string name="pause_downloads_on_mobile">Keskeytä, kun yhteys on käytön mukaan laskutettava</string>
<string name="paused">tauotettu</string>
<string name="app_update_notification_content_text">Napauta ladataksesi</string>
<string name="app_update_notification_content_title">NewPipe-päivitys on saatavilla!</string>
@ -611,4 +609,12 @@
<string name="unmute">Poista mykistys</string>
<string name="mute">Mykistä</string>
<string name="conferences">Konferenssit</string>
<string name="playlist_page_summary">Soittolistasivu</string>
<string name="feed_group_show_only_ungrouped_subscriptions">Näytä vain ryhmittelemättömät tilaukset</string>
<string name="no_playlist_bookmarked_yet">Ei soittolistakirjanmerkkejä vielä</string>
<string name="select_a_playlist">Valitse soittolista</string>
<string name="error_report_open_github_notice">Ole hyvä ja tarkasta onko kaatumiseen liittyvä ongelma jo raportoitu. Tikettien kaksoiskappaleiden selvittely vie aikaa varsinaisten ohjelmavirheiden korjaamiselta.</string>
<string name="error_report_open_issue_button_text">Raportoi virhe GitHubissa</string>
<string name="copy_for_github">Kopioi muotoiltu raportti</string>
<string name="search_showing_result_for">Näytetään tulokset haulle: %s</string>
</resources>

View file

@ -42,4 +42,9 @@
<string name="download_path_dialog_title">Pumili ng folder kung saan ido-download ang mga bidyo</string>
<string name="download_path_audio_summary">Nakaimbak sa folder na ito ang mga nai-download na mga audio files</string>
<string name="download_path_audio_dialog_title">Pumili ng folder kung saan ido-download ang mga audio files</string>
<string name="light_theme_title">Maliwanag</string>
<string name="play_with_kodi_title">Buksan gamit Kodi</string>
<string name="dark_theme_title">Madilim</string>
<string name="download_choose_new_path">Palitan ang folder na paglalagyan ng download para umipekto</string>
<string name="download_path_summary">Dito makikita ang mga nadownload na video</string>
</resources>

View file

@ -116,7 +116,7 @@
<string name="yes">Oui</string>
<string name="later">Plus tard</string>
<string name="disabled">Désactivés</string>
<string name="info_labels">Quoi :\\nRequête :\\nLangue du contenu :\\nService :\\nHeure UTC :\\nPaquet :\\nVersion :\\nVersion du système dexploitation :</string>
<string name="info_labels">Quoi :\\nRequête :\\nLangue du contenu :\\nPays du contenu :\\nLangue de lapplication :\\nService :\\nDate UTC :\\nPaquet :\\nVersion :\\nVersion du système dexploitation :</string>
<string name="short_thousand">k</string>
<string name="short_million">M</string>
<string name="msg_popup_permission">Cette autorisation est nécessaire pour
@ -290,7 +290,7 @@
<string name="create_playlist">Nouvelle liste de lecture</string>
<string name="delete_playlist">Supprimer</string>
<string name="rename_playlist">Renommer</string>
<string name="playlist_name_input">Nom</string>
<string name="name">Nom</string>
<string name="append_playlist">Ajouter à la liste de lecture</string>
<string name="set_as_playlist_thumbnail">Définir comme miniature de la liste de lecture</string>
<string name="bookmark_playlist">Enregister la liste de lecture</string>
@ -325,7 +325,6 @@
<string name="settings_category_debug_title">Débogage</string>
<string name="resize_fill">Remplir</string>
<string name="caption_auto_generated">Générés automatiquement</string>
<string name="enable_leak_canary_title">LeakCanary</string>
<string name="enable_leak_canary_summary">La surveillance des fuites de mémoire peut geler lapplication durant le vidage du tas</string>
<string name="enable_disposed_exceptions_title">Rapporter les erreurs hors cycle de vie</string>
<string name="enable_disposed_exceptions_summary">Forcer le rapport des exceptions Rx non livrables en dehors des fragments ou activités durant le cycle de vie après traitement</string>
@ -568,7 +567,6 @@
<item quantity="other">%d sélectionnés</item>
</plurals>
<string name="feed_group_dialog_empty_name">Nom de groupe vide</string>
<string name="feed_group_dialog_name_input">Nom</string>
<string name="feed_group_dialog_delete_message">Voulez-vous supprimer ce groupe ?</string>
<string name="feed_create_new_group_button_title">Nouveau</string>
<string name="settings_category_feed_title">Flux</string>
@ -612,4 +610,11 @@
<string name="show_original_time_ago_title">Afficher la date originelle sur les items</string>
<string name="youtube_restricted_mode_enabled_title">Mode restreint de YouTube</string>
<string name="feed_group_show_only_ungrouped_subscriptions">Afficher les abonnements sans groupes uniquement</string>
<string name="playlist_page_summary">Page des listes de lecture</string>
<string name="no_playlist_bookmarked_yet">Aucune liste de lecture encore enregistrée</string>
<string name="select_a_playlist">Sélectionner une liste de lecture</string>
<string name="error_report_open_github_notice">Veuillez vérifier si un ticket concernant votre problème existe déjà. Lorsque vous créez des tickets dupliqués, cela nous prend du temps que nous pourrions passer à résoudre effectivement le problème.</string>
<string name="error_report_open_issue_button_text">Rapporter lerreur sur GitHub</string>
<string name="copy_for_github">Copier le rapport formaté</string>
<string name="search_showing_result_for">Affichage des résultats pour : %s</string>
</resources>

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