Merge branch 'refactor' into Video-description-compose

This commit is contained in:
Isira Seneviratne 2025-09-07 09:56:59 +05:30
commit 6872e7c6f6
158 changed files with 1505 additions and 1078 deletions

View File

@ -28,9 +28,9 @@ android {
if (System.properties.containsKey('versionCodeOverride')) {
versionCode System.getProperty('versionCodeOverride') as Integer
} else {
versionCode 1004
versionCode 1005
}
versionName "0.27.7"
versionName "0.28.0"
if (System.properties.containsKey('versionNameSuffix')) {
versionNameSuffix System.getProperty('versionNameSuffix')
}

View File

@ -12,6 +12,7 @@ import org.schabi.newpipe.extractor.ServiceList;
import org.schabi.newpipe.extractor.exceptions.ParsingException;
import java.util.Arrays;
import java.util.Objects;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
@ -23,8 +24,23 @@ import static org.junit.Assert.assertTrue;
@LargeTest
public class ErrorInfoTest {
/**
* @param errorInfo the error info to access
* @return the private field errorInfo.message.stringRes using reflection
*/
private int getMessageFromErrorInfo(final ErrorInfo errorInfo)
throws NoSuchFieldException, IllegalAccessException {
final var message = ErrorInfo.class.getDeclaredField("message");
message.setAccessible(true);
final var messageValue = (ErrorInfo.Companion.ErrorMessage) message.get(errorInfo);
final var stringRes = ErrorInfo.Companion.ErrorMessage.class.getDeclaredField("stringRes");
stringRes.setAccessible(true);
return (int) Objects.requireNonNull(stringRes.get(messageValue));
}
@Test
public void errorInfoTestParcelable() {
public void errorInfoTestParcelable() throws NoSuchFieldException, IllegalAccessException {
final ErrorInfo info = new ErrorInfo(new ParsingException("Hello"),
UserAction.USER_REPORT, "request", ServiceList.YouTube.getServiceId());
// Obtain a Parcel object and write the parcelable object to it:
@ -39,7 +55,7 @@ public class ErrorInfoTest {
assertEquals(ServiceList.YouTube.getServiceInfo().getName(),
infoFromParcel.getServiceName());
assertEquals("request", infoFromParcel.getRequest());
assertEquals(R.string.parsing_error, infoFromParcel.getMessageStringId());
assertEquals(R.string.parsing_error, getMessageFromErrorInfo(infoFromParcel));
parcel.recycle();
}

View File

@ -435,6 +435,7 @@
</activity>
<service
android:name=".RouterActivity$FetcherService"
android:foregroundServiceType="dataSync"
android:exported="false" />
<!-- opting out of sending metrics to Google in Android System WebView -->

View File

@ -29,7 +29,7 @@ import okhttp3.ResponseBody;
public final class DownloaderImpl extends Downloader {
public static final String USER_AGENT =
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:128.0) Gecko/20100101 Firefox/128.0";
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:140.0) Gecko/20100101 Firefox/140.0";
public static final String YOUTUBE_RESTRICTED_MODE_COOKIE_KEY =
"youtube_restricted_mode_key";
public static final String YOUTUBE_RESTRICTED_MODE_COOKIE = "PREF=f2=8000000";

View File

@ -75,8 +75,8 @@ import org.schabi.newpipe.player.Player;
import org.schabi.newpipe.player.event.OnKeyDownListener;
import org.schabi.newpipe.player.helper.PlayerHolder;
import org.schabi.newpipe.player.playqueue.PlayQueue;
import org.schabi.newpipe.settings.SettingMigrations;
import org.schabi.newpipe.settings.UpdateSettingsFragment;
import org.schabi.newpipe.settings.migration.MigrationManager;
import org.schabi.newpipe.util.Constants;
import org.schabi.newpipe.util.DeviceUtils;
import org.schabi.newpipe.util.KioskTranslator;
@ -192,7 +192,7 @@ public class MainActivity extends AppCompatActivity {
UpdateSettingsFragment.askForConsentToUpdateChecks(this);
}
SettingMigrations.showUserInfoIfPresent(this);
MigrationManager.showUserInfoIfPresent(this);
}
@Override
@ -260,19 +260,6 @@ public class MainActivity extends AppCompatActivity {
*/
private void addDrawerMenuForCurrentService() throws ExtractionException {
//Tabs
final int currentServiceId = ServiceHelper.getSelectedServiceId(this);
final StreamingService service = NewPipe.getService(currentServiceId);
int kioskMenuItemId = 0;
for (final String ks : service.getKioskList().getAvailableKiosks()) {
drawerLayoutBinding.navigation.getMenu()
.add(R.id.menu_tabs_group, kioskMenuItemId, 0, KioskTranslator
.getTranslatedKioskName(ks, this))
.setIcon(KioskTranslator.getKioskIcon(ks));
kioskMenuItemId++;
}
drawerLayoutBinding.navigation.getMenu()
.add(R.id.menu_tabs_group, ITEM_ID_SUBSCRIPTIONS, ORDER,
R.string.tab_subscriptions)
@ -290,6 +277,20 @@ public class MainActivity extends AppCompatActivity {
.add(R.id.menu_tabs_group, ITEM_ID_HISTORY, ORDER, R.string.action_history)
.setIcon(R.drawable.ic_history);
//Kiosks
final int currentServiceId = ServiceHelper.getSelectedServiceId(this);
final StreamingService service = NewPipe.getService(currentServiceId);
int kioskMenuItemId = 0;
for (final String ks : service.getKioskList().getAvailableKiosks()) {
drawerLayoutBinding.navigation.getMenu()
.add(R.id.menu_kiosks_group, kioskMenuItemId, 0, KioskTranslator
.getTranslatedKioskName(ks, this))
.setIcon(KioskTranslator.getKioskIcon(ks));
kioskMenuItemId++;
}
//Settings and About
drawerLayoutBinding.navigation.getMenu()
.add(R.id.menu_options_about_group, ITEM_ID_SETTINGS, ORDER, R.string.settings)
@ -309,10 +310,13 @@ public class MainActivity extends AppCompatActivity {
changeService(item);
break;
case R.id.menu_tabs_group:
tabSelected(item);
break;
case R.id.menu_kiosks_group:
try {
tabSelected(item);
kioskSelected(item);
} catch (final Exception e) {
ErrorUtil.showUiErrorSnackbar(this, "Selecting main page tab", e);
ErrorUtil.showUiErrorSnackbar(this, "Selecting drawer kiosk", e);
}
break;
case R.id.menu_options_about_group:
@ -336,7 +340,7 @@ public class MainActivity extends AppCompatActivity {
.setChecked(true);
}
private void tabSelected(final MenuItem item) throws ExtractionException {
private void tabSelected(final MenuItem item) {
switch (item.getItemId()) {
case ITEM_ID_SUBSCRIPTIONS:
NavigationHelper.openSubscriptionFragment(getSupportFragmentManager());
@ -353,18 +357,19 @@ public class MainActivity extends AppCompatActivity {
case ITEM_ID_HISTORY:
NavigationHelper.openStatisticFragment(getSupportFragmentManager());
break;
default:
final StreamingService currentService = ServiceHelper.getSelectedService(this);
int kioskMenuItemId = 0;
for (final String kioskId : currentService.getKioskList().getAvailableKiosks()) {
if (kioskMenuItemId == item.getItemId()) {
NavigationHelper.openKioskFragment(getSupportFragmentManager(),
currentService.getServiceId(), kioskId);
break;
}
kioskMenuItemId++;
}
}
}
private void kioskSelected(final MenuItem item) throws ExtractionException {
final StreamingService currentService = ServiceHelper.getSelectedService(this);
int kioskMenuItemId = 0;
for (final String kioskId : currentService.getKioskList().getAvailableKiosks()) {
if (kioskMenuItemId == item.getItemId()) {
NavigationHelper.openKioskFragment(getSupportFragmentManager(),
currentService.getServiceId(), kioskId);
break;
}
kioskMenuItemId++;
}
}
@ -405,6 +410,7 @@ public class MainActivity extends AppCompatActivity {
drawerLayoutBinding.navigation.getMenu().removeGroup(R.id.menu_services_group);
drawerLayoutBinding.navigation.getMenu().removeGroup(R.id.menu_tabs_group);
drawerLayoutBinding.navigation.getMenu().removeGroup(R.id.menu_kiosks_group);
drawerLayoutBinding.navigation.getMenu().removeGroup(R.id.menu_options_about_group);
// Show up or down arrow

View File

@ -58,20 +58,10 @@ import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.StreamingService;
import org.schabi.newpipe.extractor.StreamingService.LinkType;
import org.schabi.newpipe.extractor.channel.ChannelInfo;
import org.schabi.newpipe.extractor.exceptions.AgeRestrictedContentException;
import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException;
import org.schabi.newpipe.extractor.exceptions.ContentNotSupportedException;
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
import org.schabi.newpipe.extractor.exceptions.GeographicRestrictionException;
import org.schabi.newpipe.extractor.exceptions.PaidContentException;
import org.schabi.newpipe.extractor.exceptions.PrivateContentException;
import org.schabi.newpipe.extractor.exceptions.ReCaptchaException;
import org.schabi.newpipe.extractor.exceptions.SoundCloudGoPlusContentException;
import org.schabi.newpipe.extractor.exceptions.YoutubeMusicPremiumContentException;
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler;
import org.schabi.newpipe.extractor.playlist.PlaylistInfo;
import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.ktx.ExceptionUtils;
import org.schabi.newpipe.local.dialog.PlaylistDialog;
import org.schabi.newpipe.player.PlayerType;
import org.schabi.newpipe.player.helper.PlayerHelper;
@ -260,7 +250,8 @@ public class RouterActivity extends AppCompatActivity {
showUnsupportedUrlDialog(url);
}
}, throwable -> handleError(this, new ErrorInfo(throwable,
UserAction.SHARE_TO_NEWPIPE, "Getting service from url: " + url))));
UserAction.SHARE_TO_NEWPIPE, "Getting service from url: " + url,
null, url))));
}
/**
@ -269,40 +260,19 @@ public class RouterActivity extends AppCompatActivity {
* @param errorInfo the error information
*/
private static void handleError(final Context context, final ErrorInfo errorInfo) {
if (errorInfo.getThrowable() != null) {
errorInfo.getThrowable().printStackTrace();
}
if (errorInfo.getThrowable() instanceof ReCaptchaException) {
if (errorInfo.getRecaptchaUrl() != null) {
Toast.makeText(context, R.string.recaptcha_request_toast, Toast.LENGTH_LONG).show();
// Starting ReCaptcha Challenge Activity
final Intent intent = new Intent(context, ReCaptchaActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.putExtra(ReCaptchaActivity.RECAPTCHA_URL_EXTRA, errorInfo.getRecaptchaUrl());
context.startActivity(intent);
} else if (errorInfo.getThrowable() != null
&& ExceptionUtils.isNetworkRelated(errorInfo.getThrowable())) {
Toast.makeText(context, R.string.network_error, Toast.LENGTH_LONG).show();
} else if (errorInfo.getThrowable() instanceof AgeRestrictedContentException) {
Toast.makeText(context, R.string.restricted_video_no_stream,
Toast.LENGTH_LONG).show();
} else if (errorInfo.getThrowable() instanceof GeographicRestrictionException) {
Toast.makeText(context, R.string.georestricted_content, Toast.LENGTH_LONG).show();
} else if (errorInfo.getThrowable() instanceof PaidContentException) {
Toast.makeText(context, R.string.paid_content, Toast.LENGTH_LONG).show();
} else if (errorInfo.getThrowable() instanceof PrivateContentException) {
Toast.makeText(context, R.string.private_content, Toast.LENGTH_LONG).show();
} else if (errorInfo.getThrowable() instanceof SoundCloudGoPlusContentException) {
Toast.makeText(context, R.string.soundcloud_go_plus_content,
Toast.LENGTH_LONG).show();
} else if (errorInfo.getThrowable() instanceof YoutubeMusicPremiumContentException) {
Toast.makeText(context, R.string.youtube_music_premium_content,
Toast.LENGTH_LONG).show();
} else if (errorInfo.getThrowable() instanceof ContentNotAvailableException) {
Toast.makeText(context, R.string.content_not_available, Toast.LENGTH_LONG).show();
} else if (errorInfo.getThrowable() instanceof ContentNotSupportedException) {
Toast.makeText(context, R.string.content_not_supported, Toast.LENGTH_LONG).show();
} else {
} else if (errorInfo.isReportable()) {
ErrorUtil.createNotification(context, errorInfo);
} else {
// this exception does not usually indicate a problem that should be reported,
// so just show a toast instead of the notification
Toast.makeText(context, errorInfo.getMessage(context), Toast.LENGTH_LONG).show();
}
if (context instanceof RouterActivity) {
@ -665,7 +635,8 @@ public class RouterActivity extends AppCompatActivity {
startActivity(intent);
finish();
}, throwable -> handleError(this, new ErrorInfo(throwable,
UserAction.SHARE_TO_NEWPIPE, "Starting info activity: " + currentUrl)))
UserAction.SHARE_TO_NEWPIPE, "Starting info activity: " + currentUrl,
null, currentUrl)))
);
return;
}
@ -852,10 +823,10 @@ public class RouterActivity extends AppCompatActivity {
})
)),
throwable -> runOnVisible(ctx -> handleError(ctx, new ErrorInfo(
throwable,
UserAction.REQUESTED_STREAM,
throwable, UserAction.REQUESTED_STREAM,
"Tried to add " + currentUrl + " to a playlist",
((RouterActivity) ctx).currentService.getServiceId())
((RouterActivity) ctx).currentService.getServiceId(),
currentUrl)
))
)
);
@ -995,7 +966,7 @@ public class RouterActivity extends AppCompatActivity {
}
}, throwable -> handleError(this, new ErrorInfo(throwable, finalUserAction,
choice.url + " opened with " + choice.playerChoice,
choice.serviceId)));
choice.serviceId, choice.url)));
}
}

View File

@ -389,8 +389,7 @@ public class DownloadDialog extends DialogFragment
}
}, throwable -> ErrorUtil.showSnackbar(context,
new ErrorInfo(throwable, UserAction.DOWNLOAD_OPEN_DIALOG,
"Downloading video stream size",
currentInfo.getServiceId()))));
"Downloading video stream size", currentInfo))));
disposables.add(StreamInfoWrapper.fetchMoreInfoForWrapper(getWrappedAudioStreams())
.subscribe(result -> {
if (dialogBinding.videoAudioGroup.getCheckedRadioButtonId()
@ -399,8 +398,7 @@ public class DownloadDialog extends DialogFragment
}
}, throwable -> ErrorUtil.showSnackbar(context,
new ErrorInfo(throwable, UserAction.DOWNLOAD_OPEN_DIALOG,
"Downloading audio stream size",
currentInfo.getServiceId()))));
"Downloading audio stream size", currentInfo))));
disposables.add(StreamInfoWrapper.fetchMoreInfoForWrapper(wrappedSubtitleStreams)
.subscribe(result -> {
if (dialogBinding.videoAudioGroup.getCheckedRadioButtonId()
@ -409,8 +407,7 @@ public class DownloadDialog extends DialogFragment
}
}, throwable -> ErrorUtil.showSnackbar(context,
new ErrorInfo(throwable, UserAction.DOWNLOAD_OPEN_DIALOG,
"Downloading subtitle stream size",
currentInfo.getServiceId()))));
"Downloading subtitle stream size", currentInfo))));
}
private void setupAudioTrackSpinner() {

View File

@ -36,8 +36,8 @@ public class AcraReportSender implements ReportSender {
ErrorUtil.openActivity(context, new ErrorInfo(
new String[]{report.getString(ReportField.STACK_TRACE)},
UserAction.UI_ERROR,
ErrorInfo.SERVICE_NONE,
"ACRA report",
null,
R.string.app_ui_crash));
}
}

View File

@ -115,7 +115,7 @@ public class ErrorActivity extends AppCompatActivity {
// normal bugreport
buildInfo(errorInfo);
activityErrorBinding.errorMessageView.setText(errorInfo.getMessageStringId());
activityErrorBinding.errorMessageView.setText(errorInfo.getMessage(this));
activityErrorBinding.errorView.setText(formErrorText(errorInfo.getStackTraces()));
// print stack trace once again for debugging:

View File

@ -1,115 +1,304 @@
package org.schabi.newpipe.error
import android.content.Context
import android.os.Parcelable
import androidx.annotation.StringRes
import androidx.core.content.ContextCompat
import com.google.android.exoplayer2.ExoPlaybackException
import kotlinx.parcelize.IgnoredOnParcel
import com.google.android.exoplayer2.upstream.HttpDataSource
import com.google.android.exoplayer2.upstream.Loader
import kotlinx.parcelize.Parcelize
import org.schabi.newpipe.R
import org.schabi.newpipe.extractor.Info
import org.schabi.newpipe.extractor.ServiceList
import org.schabi.newpipe.extractor.ServiceList.YouTube
import org.schabi.newpipe.extractor.exceptions.AccountTerminatedException
import org.schabi.newpipe.extractor.exceptions.AgeRestrictedContentException
import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException
import org.schabi.newpipe.extractor.exceptions.ContentNotSupportedException
import org.schabi.newpipe.extractor.exceptions.ExtractionException
import org.schabi.newpipe.extractor.exceptions.GeographicRestrictionException
import org.schabi.newpipe.extractor.exceptions.PaidContentException
import org.schabi.newpipe.extractor.exceptions.PrivateContentException
import org.schabi.newpipe.extractor.exceptions.ReCaptchaException
import org.schabi.newpipe.extractor.exceptions.SignInConfirmNotBotException
import org.schabi.newpipe.extractor.exceptions.SoundCloudGoPlusContentException
import org.schabi.newpipe.extractor.exceptions.UnsupportedContentInCountryException
import org.schabi.newpipe.extractor.exceptions.YoutubeMusicPremiumContentException
import org.schabi.newpipe.ktx.isNetworkRelated
import org.schabi.newpipe.util.ServiceHelper
import org.schabi.newpipe.player.mediasource.FailedMediaSource
import org.schabi.newpipe.player.resolver.PlaybackResolver
import java.net.UnknownHostException
/**
* An error has occurred in the app. This class contains plain old parcelable data that can be used
* to report the error and to show it to the user along with correct action buttons.
*/
@Parcelize
class ErrorInfo(
class ErrorInfo private constructor(
val stackTraces: Array<String>,
val userAction: UserAction,
val serviceName: String,
val request: String,
val messageStringId: Int
val serviceId: Int?,
private val message: ErrorMessage,
/**
* If `true`, a report button will be shown for this error. Otherwise the error is not something
* that can really be reported (e.g. a network issue, or content not being available at all).
*/
val isReportable: Boolean,
/**
* If `true`, the process causing this error can be retried, otherwise not.
*/
val isRetryable: Boolean,
/**
* If present, indicates that the exception was a ReCaptchaException, and this is the URL
* provided by the service that can be used to solve the ReCaptcha challenge.
*/
val recaptchaUrl: String?,
/**
* If present, this resource can alternatively be opened in browser (useful if NewPipe is
* badly broken).
*/
val openInBrowserUrl: String?,
) : Parcelable {
// no need to store throwable, all data for report is in other variables
// also, the throwable might not be serializable, see TeamNewPipe/NewPipe#7302
@IgnoredOnParcel
var throwable: Throwable? = null
private constructor(
@JvmOverloads
constructor(
throwable: Throwable,
userAction: UserAction,
serviceName: String,
request: String
request: String,
serviceId: Int? = null,
openInBrowserUrl: String? = null,
) : this(
throwableToStringList(throwable),
userAction,
serviceName,
request,
getMessageStringId(throwable, userAction)
) {
this.throwable = throwable
}
serviceId,
getMessage(throwable, userAction, serviceId),
isReportable(throwable),
isRetryable(throwable),
(throwable as? ReCaptchaException)?.url,
openInBrowserUrl,
)
private constructor(
throwable: List<Throwable>,
@JvmOverloads
constructor(
throwables: List<Throwable>,
userAction: UserAction,
serviceName: String,
request: String
request: String,
serviceId: Int? = null,
openInBrowserUrl: String? = null,
) : this(
throwableListToStringList(throwable),
throwableListToStringList(throwables),
userAction,
serviceName,
request,
getMessageStringId(throwable.firstOrNull(), userAction)
) {
this.throwable = throwable.firstOrNull()
serviceId,
getMessage(throwables.firstOrNull(), userAction, serviceId),
throwables.any(::isReportable),
throwables.isEmpty() || throwables.any(::isRetryable),
throwables.firstNotNullOfOrNull { it as? ReCaptchaException }?.url,
openInBrowserUrl,
)
// constructor to manually build ErrorInfo when no throwable is available
constructor(
stackTraces: Array<String>,
userAction: UserAction,
request: String,
serviceId: Int?,
@StringRes message: Int
) :
this(
stackTraces, userAction, request, serviceId, ErrorMessage(message),
true, false, null, null
)
// constructor with only one throwable to extract service id and openInBrowserUrl from an Info
constructor(
throwable: Throwable,
userAction: UserAction,
request: String,
info: Info?,
) :
this(throwable, userAction, request, info?.serviceId, info?.url)
// constructor with multiple throwables to extract service id and openInBrowserUrl from an Info
constructor(
throwables: List<Throwable>,
userAction: UserAction,
request: String,
info: Info?,
) :
this(throwables, userAction, request, info?.serviceId, info?.url)
fun getServiceName(): String {
return getServiceName(serviceId)
}
// constructors with single throwable
constructor(throwable: Throwable, userAction: UserAction, request: String) :
this(throwable, userAction, SERVICE_NONE, request)
constructor(throwable: Throwable, userAction: UserAction, request: String, serviceId: Int) :
this(throwable, userAction, ServiceHelper.getNameOfServiceById(serviceId), request)
constructor(throwable: Throwable, userAction: UserAction, request: String, info: Info?) :
this(throwable, userAction, getInfoServiceName(info), request)
// constructors with list of throwables
constructor(throwable: List<Throwable>, userAction: UserAction, request: String) :
this(throwable, userAction, SERVICE_NONE, request)
constructor(throwable: List<Throwable>, userAction: UserAction, request: String, serviceId: Int) :
this(throwable, userAction, ServiceHelper.getNameOfServiceById(serviceId), request)
constructor(throwable: List<Throwable>, userAction: UserAction, request: String, info: Info?) :
this(throwable, userAction, getInfoServiceName(info), request)
fun getMessage(context: Context): String {
return message.getString(context)
}
companion object {
const val SERVICE_NONE = "none"
@Parcelize
class ErrorMessage(
@StringRes
private val stringRes: Int,
private vararg val formatArgs: String,
) : Parcelable {
fun getString(context: Context): String {
return if (formatArgs.isEmpty()) {
// use ContextCompat.getString() just in case context is not AppCompatActivity
ContextCompat.getString(context, stringRes)
} else {
// ContextCompat.getString() with formatArgs does not exist, so we just
// replicate its source code but with formatArgs
ContextCompat.getContextForLanguage(context).getString(stringRes, *formatArgs)
}
}
}
const val SERVICE_NONE = "<unknown_service>"
private fun getServiceName(serviceId: Int?) =
// not using getNameOfServiceById since we want to accept a nullable serviceId and we
// want to default to SERVICE_NONE
ServiceList.all()?.firstOrNull { it.serviceId == serviceId }?.serviceInfo?.name
?: SERVICE_NONE
fun throwableToStringList(throwable: Throwable) = arrayOf(throwable.stackTraceToString())
fun throwableListToStringList(throwableList: List<Throwable>) =
throwableList.map { it.stackTraceToString() }.toTypedArray()
private fun getInfoServiceName(info: Info?) =
if (info == null) SERVICE_NONE else ServiceHelper.getNameOfServiceById(info.serviceId)
@StringRes
private fun getMessageStringId(
fun getMessage(
throwable: Throwable?,
action: UserAction
): Int {
action: UserAction?,
serviceId: Int?,
): ErrorMessage {
return when {
throwable is AccountTerminatedException -> R.string.account_terminated
throwable is ContentNotAvailableException -> R.string.content_not_available
throwable != null && throwable.isNetworkRelated -> R.string.network_error
throwable is ContentNotSupportedException -> R.string.content_not_supported
throwable is ExtractionException -> R.string.parsing_error
// player exceptions
// some may be IOException, so do these checks before isNetworkRelated!
throwable is ExoPlaybackException -> {
when (throwable.type) {
ExoPlaybackException.TYPE_SOURCE -> R.string.player_stream_failure
ExoPlaybackException.TYPE_UNEXPECTED -> R.string.player_recoverable_failure
else -> R.string.player_unrecoverable_failure
val cause = throwable.cause
when {
cause is HttpDataSource.InvalidResponseCodeException -> {
if (cause.responseCode == 403) {
if (serviceId == YouTube.serviceId) {
ErrorMessage(R.string.youtube_player_http_403)
} else {
ErrorMessage(R.string.player_http_403)
}
} else {
ErrorMessage(R.string.player_http_invalid_status, cause.responseCode.toString())
}
}
cause is Loader.UnexpectedLoaderException && cause.cause is ExtractionException ->
getMessage(throwable, action, serviceId)
throwable.type == ExoPlaybackException.TYPE_SOURCE ->
ErrorMessage(R.string.player_stream_failure)
throwable.type == ExoPlaybackException.TYPE_UNEXPECTED ->
ErrorMessage(R.string.player_recoverable_failure)
else ->
ErrorMessage(R.string.player_unrecoverable_failure)
}
}
action == UserAction.UI_ERROR -> R.string.app_ui_crash
action == UserAction.REQUESTED_COMMENTS -> R.string.error_unable_to_load_comments
action == UserAction.SUBSCRIPTION_CHANGE -> R.string.subscription_change_failed
action == UserAction.SUBSCRIPTION_UPDATE -> R.string.subscription_update_failed
action == UserAction.LOAD_IMAGE -> R.string.could_not_load_thumbnails
action == UserAction.DOWNLOAD_OPEN_DIALOG -> R.string.could_not_setup_download_menu
else -> R.string.general_error
throwable is FailedMediaSource.FailedMediaSourceException ->
getMessage(throwable.cause, action, serviceId)
throwable is PlaybackResolver.ResolverException ->
ErrorMessage(R.string.player_stream_failure)
// content not available exceptions
throwable is AccountTerminatedException ->
throwable.message
?.takeIf { reason -> !reason.isEmpty() }
?.let { reason ->
ErrorMessage(
R.string.account_terminated_service_provides_reason,
getServiceName(serviceId),
reason
)
}
?: ErrorMessage(R.string.account_terminated)
throwable is AgeRestrictedContentException ->
ErrorMessage(R.string.restricted_video_no_stream)
throwable is GeographicRestrictionException ->
ErrorMessage(R.string.georestricted_content)
throwable is PaidContentException ->
ErrorMessage(R.string.paid_content)
throwable is PrivateContentException ->
ErrorMessage(R.string.private_content)
throwable is SoundCloudGoPlusContentException ->
ErrorMessage(R.string.soundcloud_go_plus_content)
throwable is UnsupportedContentInCountryException ->
ErrorMessage(R.string.unsupported_content_in_country)
throwable is YoutubeMusicPremiumContentException ->
ErrorMessage(R.string.youtube_music_premium_content)
throwable is SignInConfirmNotBotException ->
ErrorMessage(R.string.sign_in_confirm_not_bot_error, getServiceName(serviceId))
throwable is ContentNotAvailableException ->
ErrorMessage(R.string.content_not_available)
// other extractor exceptions
throwable is ContentNotSupportedException ->
ErrorMessage(R.string.content_not_supported)
// ReCaptchas will be handled in a special way anyway
throwable is ReCaptchaException ->
ErrorMessage(R.string.recaptcha_request_toast)
// test this at the end as many exceptions could be a subclass of IOException
throwable != null && throwable.isNetworkRelated ->
ErrorMessage(R.string.network_error)
// an extraction exception unrelated to the network
// is likely an issue with parsing the website
throwable is ExtractionException ->
ErrorMessage(R.string.parsing_error)
// user actions (in case the exception is null or unrecognizable)
action == UserAction.UI_ERROR ->
ErrorMessage(R.string.app_ui_crash)
action == UserAction.REQUESTED_COMMENTS ->
ErrorMessage(R.string.error_unable_to_load_comments)
action == UserAction.SUBSCRIPTION_CHANGE ->
ErrorMessage(R.string.subscription_change_failed)
action == UserAction.SUBSCRIPTION_UPDATE ->
ErrorMessage(R.string.subscription_update_failed)
action == UserAction.LOAD_IMAGE ->
ErrorMessage(R.string.could_not_load_thumbnails)
action == UserAction.DOWNLOAD_OPEN_DIALOG ->
ErrorMessage(R.string.could_not_setup_download_menu)
else ->
ErrorMessage(R.string.error_snackbar_message)
}
}
fun isReportable(throwable: Throwable?): Boolean {
return when (throwable) {
// we don't have an exception, so this is a manually built error, which likely
// indicates that it's important and is thus reportable
null -> true
// the service explicitly said that content is not available (e.g. age restrictions,
// video deleted, etc.), there is no use in letting users report it
is ContentNotAvailableException -> false
// we know the content is not supported, no need to let the user report it
is ContentNotSupportedException -> false
// happens often when there is no internet connection; we don't use
// `throwable.isNetworkRelated` since any `IOException` would make that function
// return true, but not all `IOException`s are network related
is UnknownHostException -> false
// by default, this is an unexpected exception, which the user could report
else -> true
}
}
fun isRetryable(throwable: Throwable?): Boolean {
return when (throwable) {
// we know the content is not available, retrying won't help
is ContentNotAvailableException -> false
// we know the content is not supported, retrying won't help
is ContentNotSupportedException -> false
// by default (including if throwable is null), enable retrying (though the retry
// button will be shown only if a way to perform the retry is implemented)
else -> true
}
}
}

View File

@ -2,7 +2,6 @@ package org.schabi.newpipe.error
import android.content.Context
import android.content.Intent
import android.util.Log
import android.view.View
import android.widget.Button
import android.widget.TextView
@ -14,21 +13,7 @@ import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.disposables.Disposable
import org.schabi.newpipe.MainActivity
import org.schabi.newpipe.R
import org.schabi.newpipe.extractor.exceptions.AccountTerminatedException
import org.schabi.newpipe.extractor.exceptions.AgeRestrictedContentException
import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException
import org.schabi.newpipe.extractor.exceptions.ContentNotSupportedException
import org.schabi.newpipe.extractor.exceptions.GeographicRestrictionException
import org.schabi.newpipe.extractor.exceptions.PaidContentException
import org.schabi.newpipe.extractor.exceptions.PrivateContentException
import org.schabi.newpipe.extractor.exceptions.ReCaptchaException
import org.schabi.newpipe.extractor.exceptions.SoundCloudGoPlusContentException
import org.schabi.newpipe.extractor.exceptions.YoutubeMusicPremiumContentException
import org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty
import org.schabi.newpipe.ktx.animate
import org.schabi.newpipe.ktx.isInterruptedCaused
import org.schabi.newpipe.ktx.isNetworkRelated
import org.schabi.newpipe.util.ServiceHelper
import org.schabi.newpipe.util.external_communication.ShareUtils
import java.util.concurrent.TimeUnit
@ -78,64 +63,32 @@ class ErrorPanelHelper(
}
fun showError(errorInfo: ErrorInfo) {
if (errorInfo.throwable != null && errorInfo.throwable!!.isInterruptedCaused) {
if (DEBUG) {
Log.w(TAG, "onError() isInterruptedCaused! = [$errorInfo.throwable]")
}
return
}
ensureDefaultVisibility()
errorTextView.text = errorInfo.getMessage(context)
if (errorInfo.throwable is ReCaptchaException) {
errorTextView.setText(R.string.recaptcha_request_toast)
showAndSetErrorButtonAction(
R.string.recaptcha_solve
) {
if (errorInfo.recaptchaUrl != null) {
showAndSetErrorButtonAction(R.string.recaptcha_solve) {
// Starting ReCaptcha Challenge Activity
val intent = Intent(context, ReCaptchaActivity::class.java)
intent.putExtra(
ReCaptchaActivity.RECAPTCHA_URL_EXTRA,
(errorInfo.throwable as ReCaptchaException).url
)
intent.putExtra(ReCaptchaActivity.RECAPTCHA_URL_EXTRA, errorInfo.recaptchaUrl)
fragment.startActivityForResult(intent, ReCaptchaActivity.RECAPTCHA_REQUEST)
errorActionButton.setOnClickListener(null)
}
errorRetryButton.isVisible = retryShouldBeShown
showAndSetOpenInBrowserButtonAction(errorInfo)
} else if (errorInfo.throwable is AccountTerminatedException) {
errorTextView.setText(R.string.account_terminated)
if (!isNullOrEmpty((errorInfo.throwable as AccountTerminatedException).message)) {
errorServiceInfoTextView.text = context.resources.getString(
R.string.service_provides_reason,
ServiceHelper.getSelectedService(context)?.serviceInfo?.name ?: "<unknown>"
)
errorServiceInfoTextView.isVisible = true
errorServiceExplanationTextView.text =
(errorInfo.throwable as AccountTerminatedException).message
errorServiceExplanationTextView.isVisible = true
}
} else {
showAndSetErrorButtonAction(
R.string.error_snackbar_action
) {
} else if (errorInfo.isReportable) {
showAndSetErrorButtonAction(R.string.error_snackbar_action) {
ErrorUtil.openActivity(context, errorInfo)
}
}
errorTextView.setText(getExceptionDescription(errorInfo.throwable))
if (errorInfo.isRetryable) {
errorRetryButton.isVisible = retryShouldBeShown
}
if (errorInfo.throwable !is ContentNotAvailableException &&
errorInfo.throwable !is ContentNotSupportedException
) {
// show retry button only for content which is not unavailable or unsupported
errorRetryButton.isVisible = retryShouldBeShown
if (errorInfo.openInBrowserUrl != null) {
errorOpenInBrowserButton.isVisible = true
errorOpenInBrowserButton.setOnClickListener {
ShareUtils.openUrlInBrowser(context, errorInfo.openInBrowserUrl)
}
showAndSetOpenInBrowserButtonAction(errorInfo)
}
setRootVisible()
@ -153,15 +106,6 @@ class ErrorPanelHelper(
errorActionButton.setOnClickListener(listener)
}
fun showAndSetOpenInBrowserButtonAction(
errorInfo: ErrorInfo
) {
errorOpenInBrowserButton.isVisible = true
errorOpenInBrowserButton.setOnClickListener {
ShareUtils.openUrlInBrowser(context, errorInfo.request)
}
}
fun showTextError(errorString: String) {
ensureDefaultVisibility()
@ -192,27 +136,5 @@ class ErrorPanelHelper(
companion object {
val TAG: String = ErrorPanelHelper::class.simpleName!!
val DEBUG: Boolean = MainActivity.DEBUG
@StringRes
fun getExceptionDescription(throwable: Throwable?): Int {
return when (throwable) {
is AgeRestrictedContentException -> R.string.restricted_video_no_stream
is GeographicRestrictionException -> R.string.georestricted_content
is PaidContentException -> R.string.paid_content
is PrivateContentException -> R.string.private_content
is SoundCloudGoPlusContentException -> R.string.soundcloud_go_plus_content
is YoutubeMusicPremiumContentException -> R.string.youtube_music_premium_content
is ContentNotAvailableException -> R.string.content_not_available
is ContentNotSupportedException -> R.string.content_not_supported
else -> {
// show retry button only for content which is not unavailable or unsupported
if (throwable != null && throwable.isNetworkRelated) {
R.string.network_error
} else {
R.string.error_snackbar_message
}
}
}
}
}
}

View File

@ -122,7 +122,7 @@ class ErrorUtil {
)
.setSmallIcon(R.drawable.ic_bug_report)
.setContentTitle(context.getString(R.string.error_report_notification_title))
.setContentText(context.getString(errorInfo.messageStringId))
.setContentText(errorInfo.getMessage(context))
.setAutoCancel(true)
.setContentIntent(
PendingIntentCompat.getActivity(
@ -156,10 +156,10 @@ class ErrorUtil {
// fallback to showing a notification if no root view is available
createNotification(context, errorInfo)
} else {
Snackbar.make(rootView, R.string.error_snackbar_message, Snackbar.LENGTH_LONG)
Snackbar.make(rootView, errorInfo.getMessage(context), Snackbar.LENGTH_LONG)
.setActionTextColor(Color.YELLOW)
.setAction(context.getString(R.string.error_snackbar_action).uppercase()) {
openActivity(context, errorInfo)
context.startActivity(getErrorActivityIntent(context, errorInfo))
}.show()
}
}

View File

@ -33,7 +33,9 @@ public enum UserAction {
SHARE_TO_NEWPIPE("share to newpipe"),
CHECK_FOR_NEW_APP_VERSION("check for new app version"),
OPEN_INFO_ITEM_DIALOG("open info item dialog"),
GETTING_MAIN_SCREEN_TAB("getting main screen tab");
GETTING_MAIN_SCREEN_TAB("getting main screen tab"),
PLAY_ON_POPUP("play on popup"),
SUBSCRIPTIONS("loading subscriptions");
private final String message;

View File

@ -775,7 +775,7 @@ class VideoDetailFragment :
},
{ throwable ->
showError(
ErrorInfo(throwable, UserAction.REQUESTED_STREAM, url ?: "no url", serviceId)
ErrorInfo(throwable, UserAction.REQUESTED_STREAM, url ?: "no url", serviceId, url)
)
}
)
@ -1465,7 +1465,7 @@ class VideoDetailFragment :
if (!info.errors.isEmpty()) {
showSnackBarError(
ErrorInfo(info.errors, UserAction.REQUESTED_STREAM, info.url, info)
ErrorInfo(info.errors, UserAction.REQUESTED_STREAM, "Some info not extracted: " + info.url, info)
)
}
}

View File

@ -153,7 +153,7 @@ public abstract class BaseListInfoFragment<I extends InfoItem, L extends ListInf
handleResult(result);
}, throwable ->
showError(new ErrorInfo(throwable, errorUserAction,
"Start loading: " + url, serviceId)));
"Start loading: " + url, serviceId, url)));
}
/**
@ -184,7 +184,7 @@ public abstract class BaseListInfoFragment<I extends InfoItem, L extends ListInf
handleNextItems(infoItemsPage);
}, (@NonNull Throwable throwable) ->
dynamicallyShowErrorPanelOrSnackbar(new ErrorInfo(throwable,
errorUserAction, "Loading more items: " + url, serviceId)));
errorUserAction, "Loading more items: " + url, serviceId, url)));
}
private void forbidDownwardFocusScroll() {
@ -210,7 +210,7 @@ public abstract class BaseListInfoFragment<I extends InfoItem, L extends ListInf
if (!result.getErrors().isEmpty()) {
dynamicallyShowErrorPanelOrSnackbar(new ErrorInfo(result.getErrors(), errorUserAction,
"Get next items of: " + url, serviceId));
"Get next items of: " + url, serviceId, url));
}
}
@ -250,7 +250,7 @@ public abstract class BaseListInfoFragment<I extends InfoItem, L extends ListInf
if (!errors.isEmpty()) {
dynamicallyShowErrorPanelOrSnackbar(new ErrorInfo(result.getErrors(),
errorUserAction, "Start loading: " + url, serviceId));
errorUserAction, "Start loading: " + url, serviceId, url));
}
}
}

View File

@ -3,6 +3,7 @@ package org.schabi.newpipe.fragments.list.channel;
import static org.schabi.newpipe.ktx.TextViewUtils.animateTextColor;
import static org.schabi.newpipe.ktx.ViewUtils.animate;
import static org.schabi.newpipe.ktx.ViewUtils.animateBackgroundColor;
import static org.schabi.newpipe.ui.emptystate.EmptyStateUtil.setEmptyStateComposable;
import android.content.Context;
import android.content.SharedPreferences;
@ -45,7 +46,6 @@ import org.schabi.newpipe.ktx.AnimationType;
import org.schabi.newpipe.local.feed.notifications.NotificationHelper;
import org.schabi.newpipe.local.subscription.SubscriptionManager;
import org.schabi.newpipe.ui.emptystate.EmptyStateSpec;
import org.schabi.newpipe.ui.emptystate.EmptyStateUtil;
import org.schabi.newpipe.util.ChannelTabHelper;
import org.schabi.newpipe.util.Constants;
import org.schabi.newpipe.util.ExtractorHelper;
@ -200,10 +200,7 @@ public class ChannelFragment extends BaseStateFragment<ChannelInfo>
protected void initViews(final View rootView, final Bundle savedInstanceState) {
super.initViews(rootView, savedInstanceState);
EmptyStateUtil.setEmptyStateComposable(
binding.emptyStateView,
EmptyStateSpec.Companion.getContentNotSupported()
);
setEmptyStateComposable(binding.emptyStateView, EmptyStateSpec.ContentNotSupported);
tabAdapter = new TabAdapter(getChildFragmentManager());
binding.viewPager.setAdapter(tabAdapter);
@ -583,7 +580,7 @@ public class ChannelFragment extends BaseStateFragment<ChannelInfo>
isLoading.set(false);
handleResult(result);
}, throwable -> showError(new ErrorInfo(throwable, UserAction.REQUESTED_CHANNEL,
url == null ? "No URL" : url, serviceId)));
url == null ? "No URL" : url, serviceId, url)));
}
@Override

View File

@ -3,6 +3,7 @@ package org.schabi.newpipe.fragments.list.search;
import static androidx.recyclerview.widget.ItemTouchHelper.Callback.makeMovementFlags;
import static org.schabi.newpipe.extractor.utils.Utils.isBlank;
import static org.schabi.newpipe.ktx.ViewUtils.animate;
import static org.schabi.newpipe.ui.emptystate.EmptyStateUtil.setEmptyStateComposable;
import static org.schabi.newpipe.util.ExtractorHelper.showMetaInfoInTextView;
import static java.util.Arrays.asList;
@ -54,6 +55,7 @@ import org.schabi.newpipe.extractor.MetaInfo;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.Page;
import org.schabi.newpipe.extractor.StreamingService;
import org.schabi.newpipe.extractor.exceptions.ParsingException;
import org.schabi.newpipe.extractor.search.SearchExtractor;
import org.schabi.newpipe.extractor.search.SearchInfo;
import org.schabi.newpipe.extractor.services.peertube.linkHandler.PeertubeSearchQueryHandlerFactory;
@ -65,7 +67,6 @@ import org.schabi.newpipe.ktx.ExceptionUtils;
import org.schabi.newpipe.local.history.HistoryRecordManager;
import org.schabi.newpipe.settings.NewPipeSettings;
import org.schabi.newpipe.ui.emptystate.EmptyStateSpec;
import org.schabi.newpipe.ui.emptystate.EmptyStateUtil;
import org.schabi.newpipe.util.Constants;
import org.schabi.newpipe.util.DeviceUtils;
import org.schabi.newpipe.util.ExtractorHelper;
@ -356,9 +357,7 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
protected void initViews(final View rootView, final Bundle savedInstanceState) {
super.initViews(rootView, savedInstanceState);
EmptyStateUtil.setEmptyStateComposable(
searchBinding.emptyStateView,
EmptyStateSpec.Companion.getNoSearchResult());
setEmptyStateComposable(searchBinding.emptyStateView, EmptyStateSpec.NoSearchResult);
searchBinding.suggestionsList.setAdapter(suggestionListAdapter);
// animations are just strange and useless, since the suggestions keep changing too much
@ -940,7 +939,21 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
infoListAdapter.clearStreamItemList();
showEmptyState();
} else {
showError(new ErrorInfo(exception, UserAction.SEARCHED, searchString, serviceId));
showError(new ErrorInfo(exception, UserAction.SEARCHED, searchString, serviceId,
getOpenInBrowserUrlForErrors()));
}
}
@Nullable
private String getOpenInBrowserUrlForErrors() {
if (TextUtils.isEmpty(searchString)) {
return null;
}
try {
return service.getSearchQHFactory().getUrl(searchString,
Arrays.asList(contentFilter), sortFilter);
} catch (final NullPointerException | ParsingException ignored) {
return null;
}
}
@ -1028,7 +1041,7 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
&& !(exceptions.size() == 1
&& exceptions.get(0) instanceof SearchExtractor.NothingFoundException)) {
showSnackBarError(new ErrorInfo(result.getErrors(), UserAction.SEARCHED,
searchString, serviceId));
searchString, serviceId, getOpenInBrowserUrlForErrors()));
}
searchSuggestion = result.getSearchSuggestion();
@ -1095,15 +1108,26 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
public void handleNextItems(final ListExtractor.InfoItemsPage<?> result) {
showListFooter(false);
infoListAdapter.addInfoItemList(result.getItems());
nextPage = result.getNextPage();
if (!result.getErrors().isEmpty() && nextPage != null) {
showSnackBarError(new ErrorInfo(result.getErrors(), UserAction.SEARCHED,
"\"" + searchString + "\" → pageUrl: " + nextPage.getUrl() + ", "
+ "pageIds: " + nextPage.getIds() + ", "
+ "pageCookies: " + nextPage.getCookies(),
serviceId));
if (!result.getErrors().isEmpty()) {
// nextPage should be non-null at this point, because it refers to the page
// whose results are handled here, but let's check it anyway
if (nextPage == null) {
showSnackBarError(new ErrorInfo(result.getErrors(), UserAction.SEARCHED,
"\"" + searchString + "\" → nextPage == null", serviceId,
getOpenInBrowserUrlForErrors()));
} else {
showSnackBarError(new ErrorInfo(result.getErrors(), UserAction.SEARCHED,
"\"" + searchString + "\" → pageUrl: " + nextPage.getUrl() + ", "
+ "pageIds: " + nextPage.getIds() + ", "
+ "pageCookies: " + nextPage.getCookies(),
serviceId, getOpenInBrowserUrlForErrors()));
}
}
// keep the reassignment of nextPage after the error handling to ensure that nextPage
// still holds the correct value during the error handling
nextPage = result.getNextPage();
super.handleNextItems(result);
}

View File

@ -15,6 +15,7 @@ import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.compose.ui.platform.ComposeView;
import androidx.fragment.app.FragmentManager;
import androidx.recyclerview.widget.ItemTouchHelper;
import androidx.recyclerview.widget.RecyclerView;
@ -125,10 +126,8 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL
super.initViews(rootView, savedInstanceState);
itemListAdapter.setUseItemHandle(true);
EmptyStateUtil.setEmptyStateComposable(
rootView.findViewById(R.id.empty_state_view),
EmptyStateSpec.Companion.getNoBookmarkedPlaylist()
);
final ComposeView emptyView = rootView.findViewById(R.id.empty_state_view);
EmptyStateUtil.setEmptyStateComposable(emptyView, EmptyStateSpec.NoBookmarkedPlaylist);
}
@Override

View File

@ -1,6 +1,8 @@
package org.schabi.newpipe.local.feed.notifications
import android.content.Context
import android.content.pm.ServiceInfo
import android.os.Build
import android.util.Log
import androidx.core.app.NotificationCompat
import androidx.work.Constraints
@ -83,7 +85,9 @@ class NotificationWorker(
.setPriority(NotificationCompat.PRIORITY_LOW)
.setContentTitle(applicationContext.getString(R.string.feed_notification_loading))
.build()
setForegroundAsync(ForegroundInfo(FeedLoadService.NOTIFICATION_ID, notification))
// ServiceInfo constants are not used below Android Q, so 0 is set here
val serviceType = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC else 0
setForegroundAsync(ForegroundInfo(FeedLoadService.NOTIFICATION_ID, notification, serviceType))
}
companion object {

View File

@ -85,8 +85,8 @@ public class SubscriptionsImportFragment extends BaseFragment {
if (supportedSources.isEmpty() && currentServiceId != Constants.NO_SERVICE_ID) {
ErrorUtil.showSnackbar(activity,
new ErrorInfo(new String[]{}, UserAction.SUBSCRIPTION_IMPORT_EXPORT,
ServiceHelper.getNameOfServiceById(currentServiceId),
"Service does not support importing subscriptions",
currentServiceId,
R.string.general_error));
activity.finish();
}

View File

@ -1283,7 +1283,8 @@ public final class Player implements PlaybackListener, Listener {
UserAction.PLAY_STREAM,
"Loading failed for [" + currentMetadata.getTitle()
+ "]: " + currentMetadata.getStreamUrl(),
currentMetadata.getServiceId());
currentMetadata.getServiceId(),
currentMetadata.getStreamUrl());
ErrorUtil.createNotification(context, errorInfo);
}
@ -1499,7 +1500,7 @@ public final class Player implements PlaybackListener, Listener {
errorInfo = new ErrorInfo(error, UserAction.PLAY_STREAM,
"Player error[type=" + error.getErrorCodeName()
+ "] occurred while playing " + currentMetadata.getStreamUrl(),
currentMetadata.getServiceId());
currentMetadata.getServiceId(), currentMetadata.getStreamUrl());
}
ErrorUtil.createNotification(context, errorInfo);
}

View File

@ -33,11 +33,9 @@ import com.google.android.exoplayer2.trackselection.ExoTrackSelection;
import com.google.android.exoplayer2.ui.AspectRatioFrameLayout;
import com.google.android.exoplayer2.ui.AspectRatioFrameLayout.ResizeMode;
import com.google.android.exoplayer2.ui.CaptionStyleCompat;
import com.google.android.exoplayer2.util.MimeTypes;
import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.extractor.MediaFormat;
import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
import org.schabi.newpipe.extractor.stream.SubtitlesStream;
@ -47,13 +45,14 @@ import org.schabi.newpipe.player.playqueue.PlayQueue;
import org.schabi.newpipe.player.playqueue.PlayQueueItem;
import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
import org.schabi.newpipe.util.ListHelper;
import org.schabi.newpipe.util.Localization;
import java.lang.annotation.Retention;
import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
import java.text.NumberFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Formatter;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
@ -62,11 +61,7 @@ import java.util.Set;
import java.util.concurrent.TimeUnit;
public final class PlayerHelper {
private static final StringBuilder STRING_BUILDER = new StringBuilder();
private static final Formatter STRING_FORMATTER =
new Formatter(STRING_BUILDER, Locale.getDefault());
private static final NumberFormat SPEED_FORMATTER = new DecimalFormat("0.##x");
private static final NumberFormat PITCH_FORMATTER = new DecimalFormat("##%");
private static final FormattersProvider FORMATTERS_PROVIDER = new FormattersProvider();
@Retention(SOURCE)
@IntDef({AUTOPLAY_TYPE_ALWAYS, AUTOPLAY_TYPE_WIFI,
@ -89,9 +84,11 @@ public final class PlayerHelper {
private PlayerHelper() {
}
////////////////////////////////////////////////////////////////////////////
// Exposed helpers
////////////////////////////////////////////////////////////////////////////
// region Exposed helpers
public static void resetFormat() {
FORMATTERS_PROVIDER.reset();
}
@NonNull
public static String getTimeString(final int milliSeconds) {
@ -100,35 +97,24 @@ public final class PlayerHelper {
final int hours = (milliSeconds % 86400000) / 3600000;
final int days = (milliSeconds % (86400000 * 7)) / 86400000;
STRING_BUILDER.setLength(0);
return (days > 0
? STRING_FORMATTER.format("%d:%02d:%02d:%02d", days, hours, minutes, seconds)
: hours > 0
? STRING_FORMATTER.format("%d:%02d:%02d", hours, minutes, seconds)
: STRING_FORMATTER.format("%02d:%02d", minutes, seconds)
).toString();
final Formatters formatters = FORMATTERS_PROVIDER.formatters();
if (days > 0) {
return formatters.stringFormat("%d:%02d:%02d:%02d", days, hours, minutes, seconds);
}
return hours > 0
? formatters.stringFormat("%d:%02d:%02d", hours, minutes, seconds)
: formatters.stringFormat("%02d:%02d", minutes, seconds);
}
@NonNull
public static String formatSpeed(final double speed) {
return SPEED_FORMATTER.format(speed);
return FORMATTERS_PROVIDER.formatters().speed().format(speed);
}
@NonNull
public static String formatPitch(final double pitch) {
return PITCH_FORMATTER.format(pitch);
}
@NonNull
public static String subtitleMimeTypesOf(@NonNull final MediaFormat format) {
switch (format) {
case VTT:
return MimeTypes.TEXT_VTT;
case TTML:
return MimeTypes.APPLICATION_TTML;
default:
throw new IllegalArgumentException("Unrecognized mime type: " + format.name());
}
return FORMATTERS_PROVIDER.formatters().pitch().format(pitch);
}
@NonNull
@ -219,9 +205,8 @@ public final class PlayerHelper {
? null : getAutoQueuedSinglePlayQueue(autoQueueItems.get(0));
}
////////////////////////////////////////////////////////////////////////////
// Settings Resolution
////////////////////////////////////////////////////////////////////////////
// endregion
// region Resolution
public static boolean isResumeAfterAudioFocusGain(@NonNull final Context context) {
return getPreferences(context)
@ -405,9 +390,8 @@ public final class PlayerHelper {
return Integer.parseInt(preferredIntervalBytes) * 1024;
}
////////////////////////////////////////////////////////////////////////////
// Private helpers
////////////////////////////////////////////////////////////////////////////
// endregion
// region Private helpers
@NonNull
private static SharedPreferences getPreferences(@NonNull final Context context) {
@ -427,9 +411,8 @@ public final class PlayerHelper {
}
////////////////////////////////////////////////////////////////////////////
// Utils used by player
////////////////////////////////////////////////////////////////////////////
// endregion
// region Utils used by player
@RepeatMode
public static int nextRepeatMode(@RepeatMode final int repeatMode) {
@ -503,4 +486,43 @@ public final class PlayerHelper {
player.getContext().getString(R.string.seek_duration_key),
player.getContext().getString(R.string.seek_duration_default_value))));
}
// endregion
// region Format
static class FormattersProvider {
private Formatters formatters;
public Formatters formatters() {
if (formatters == null) {
formatters = Formatters.create();
}
return formatters;
}
public void reset() {
formatters = null;
}
}
record Formatters(
Locale locale,
NumberFormat speed,
NumberFormat pitch) {
static Formatters create() {
final Locale locale = Localization.getAppLocale();
final DecimalFormatSymbols dfs = DecimalFormatSymbols.getInstance(locale);
return new Formatters(
locale,
new DecimalFormat("0.##x", dfs),
new DecimalFormat("##%", dfs));
}
String stringFormat(final String format, final Object... args) {
return String.format(locale, format, args);
}
}
// endregion
}

View File

@ -17,6 +17,7 @@ import io.reactivex.rxjava3.schedulers.Schedulers
import org.schabi.newpipe.MainActivity
import org.schabi.newpipe.NewPipeDatabase
import org.schabi.newpipe.R
import org.schabi.newpipe.error.ErrorInfo
import org.schabi.newpipe.extractor.InfoItem.InfoType
import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler
@ -84,7 +85,7 @@ class MediaBrowserPlaybackPreparer(
},
{ throwable ->
Log.e(TAG, "Failed to start playback of media ID [$mediaId]", throwable)
onPrepareError()
onPrepareError(throwable)
}
)
}
@ -115,9 +116,9 @@ class MediaBrowserPlaybackPreparer(
)
}
private fun onPrepareError() {
private fun onPrepareError(throwable: Throwable) {
setMediaSessionError.accept(
ContextCompat.getString(context, R.string.error_snackbar_message),
ErrorInfo.getMessage(throwable, null, null).getString(context),
PlaybackStateCompat.ERROR_CODE_APP_ERROR
)
}

View File

@ -167,19 +167,17 @@ public final class NotificationUtil {
&& notificationBuilder.mActions.get(2).actionIntent != null);
}
public void createNotificationAndStartForeground() {
if (notificationBuilder == null) {
notificationBuilder = createNotification();
}
updateNotification();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
player.getService().startForeground(NOTIFICATION_ID, notificationBuilder.build(),
ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK);
} else {
player.getService().startForeground(NOTIFICATION_ID, notificationBuilder.build());
}
// ServiceInfo constants are not used below Android Q, so 0 is set here
final int serviceType = Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q
? ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK : 0;
ServiceCompat.startForeground(player.getService(), NOTIFICATION_ID,
notificationBuilder.build(), serviceType);
}
public void cancelNotificationAndStopForeground() {

View File

@ -57,9 +57,7 @@ public class BackupRestoreSettingsFragment extends BasePreferenceFragment {
@Override
public void onCreatePreferences(@Nullable final Bundle savedInstanceState,
@Nullable final String rootKey) {
final var dbDir = requireContext().getDatabasePath(BackupFileLocator.FILE_NAME_DB).toPath()
.getParent();
manager = new ImportExportManager(new BackupFileLocator(dbDir));
manager = new ImportExportManager(new BackupFileLocator(requireContext()));
importExportDataPathKey = getString(R.string.import_export_data_path);

View File

@ -16,6 +16,7 @@ import androidx.preference.Preference;
import org.schabi.newpipe.DownloaderImpl;
import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.player.helper.PlayerHelper;
import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.image.ImageStrategy;
import org.schabi.newpipe.util.image.PreferredImageQuality;
@ -106,5 +107,6 @@ public class ContentSettingsFragment extends BasePreferenceFragment {
NewPipe.setupLocalization(
Localization.getPreferredLocalization(context),
Localization.getPreferredContentCountry(context));
PlayerHelper.resetFormat();
}
}

View File

@ -13,6 +13,7 @@ import androidx.preference.PreferenceManager;
import org.schabi.newpipe.App;
import org.schabi.newpipe.R;
import org.schabi.newpipe.settings.migration.MigrationManager;
import org.schabi.newpipe.util.DeviceUtils;
import java.io.File;
@ -46,7 +47,7 @@ public final class NewPipeSettings {
public static void initSettings(final Context context) {
// first run migrations, then setDefaultValues, since the latter requires the correct types
SettingMigrations.runMigrationsIfNeeded(context);
MigrationManager.runMigrationsIfNeeded(context);
// readAgain is true so that if new settings are added their default value is set
PreferenceManager.setDefaultValues(context, R.xml.main_settings, true);

View File

@ -95,8 +95,7 @@ public class SelectChannelFragment extends DialogFragment {
progressBar = v.findViewById(R.id.progressBar);
emptyView = v.findViewById(R.id.empty_state_view);
EmptyStateUtil.setEmptyStateComposable(emptyView,
EmptyStateSpec.Companion.getNoSubscriptions());
EmptyStateUtil.setEmptyStateComposable(emptyView, EmptyStateSpec.NoSubscriptions);
progressBar.setVisibility(View.VISIBLE);
recyclerView.setVisibility(View.GONE);
emptyView.setVisibility(View.GONE);

View File

@ -65,8 +65,7 @@ public class SelectPlaylistFragment extends DialogFragment {
recyclerView = v.findViewById(R.id.items_list);
emptyView = v.findViewById(R.id.empty_state_view);
EmptyStateUtil.setEmptyStateComposable(emptyView,
EmptyStateSpec.Companion.getNoBookmarkedPlaylist());
EmptyStateUtil.setEmptyStateComposable(emptyView, EmptyStateSpec.NoBookmarkedPlaylist);
recyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
final SelectPlaylistAdapter playlistAdapter = new SelectPlaylistAdapter();
recyclerView.setAdapter(playlistAdapter);

View File

@ -1,12 +1,13 @@
package org.schabi.newpipe.settings.export
import android.content.Context
import java.nio.file.Path
import kotlin.io.path.div
/**
* Locates specific files of NewPipe based on the home directory of the app.
*/
class BackupFileLocator(homeDir: Path) {
class BackupFileLocator(context: Context) {
companion object {
const val FILE_NAME_DB = "newpipe.db"
@Deprecated(
@ -17,9 +18,8 @@ class BackupFileLocator(homeDir: Path) {
const val FILE_NAME_JSON_PREFS = "preferences.json"
}
val dbDir = homeDir / "databases"
val db = homeDir / FILE_NAME_DB
val dbJournal = homeDir / "$FILE_NAME_DB-journal"
val dbShm = dbDir / "$FILE_NAME_DB-shm"
val dbWal = dbDir / "$FILE_NAME_DB-wal"
val db: Path = context.getDatabasePath(FILE_NAME_DB).toPath()
val dbJournal: Path = db.resolveSibling("$FILE_NAME_DB-journal")
val dbShm: Path = db.resolveSibling("$FILE_NAME_DB-shm")
val dbWal: Path = db.resolveSibling("$FILE_NAME_DB-wal")
}

View File

@ -12,7 +12,7 @@ import java.io.FileNotFoundException
import java.io.IOException
import java.io.ObjectOutputStream
import java.util.zip.ZipOutputStream
import kotlin.io.path.createDirectories
import kotlin.io.path.createParentDirectories
import kotlin.io.path.deleteIfExists
class ImportExportManager(private val fileLocator: BackupFileLocator) {
@ -63,7 +63,7 @@ class ImportExportManager(private val fileLocator: BackupFileLocator) {
*/
@Throws(IOException::class)
fun ensureDbDirectoryExists() {
fileLocator.dbDir.createDirectories()
fileLocator.db.createParentDirectories()
}
/**

View File

@ -0,0 +1,103 @@
package org.schabi.newpipe.settings.migration;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.core.util.Consumer;
import org.schabi.newpipe.R;
import org.schabi.newpipe.error.ErrorUtil;
import java.util.ArrayList;
import java.util.List;
/**
* MigrationManager is responsible for running migrations and showing the user information about
* the migrations that were applied.
*/
public final class MigrationManager {
private static final String TAG = MigrationManager.class.getSimpleName();
/**
* List of UI actions that are performed after the UI is initialized (e.g. showing alert
* dialogs) to inform the user about changes that were applied by migrations.
*/
private static final List<Consumer<Context>> MIGRATION_INFO = new ArrayList<>();
private MigrationManager() {
// MigrationManager is a utility class that is completely static
}
/**
* Run all migrations that are needed for the current version of NewPipe.
* This method should be called at the start of the application, before any other operations
* that depend on the settings.
*
* @param context Context that can be used to run migrations
*/
public static void runMigrationsIfNeeded(@NonNull final Context context) {
SettingMigrations.runMigrationsIfNeeded(context);
}
/**
* Perform UI actions informing about migrations that took place if they are present.
* @param context Context that can be used to show dialogs/snackbars/toasts
*/
public static void showUserInfoIfPresent(@NonNull final Context context) {
if (MIGRATION_INFO.isEmpty()) {
return;
}
try {
MIGRATION_INFO.get(0).accept(context);
} catch (final Exception e) {
ErrorUtil.showUiErrorSnackbar(context, "Showing migration info to the user", e);
// Remove the migration that caused the error and continue with the next one
MIGRATION_INFO.remove(0);
showUserInfoIfPresent(context);
}
}
/**
* Add a migration info action that will be executed after the UI is initialized.
* This can be used to show dialogs/snackbars/toasts to inform the user about changes that
* were applied by migrations.
*
* @param info the action to be executed
*/
public static void addMigrationInfo(final Consumer<Context> info) {
MIGRATION_INFO.add(info);
}
/**
* This method should be called when the user dismisses the migration info
* to check if there are any more migration info actions to be shown.
* @param context Context that can be used to show dialogs/snackbars/toasts
*/
public static void onMigrationInfoDismissed(@NonNull final Context context) {
MIGRATION_INFO.remove(0);
showUserInfoIfPresent(context);
}
/**
* Creates a dialog to inform the user about the migration.
* @param uiContext Context that can be used to show dialogs/snackbars/toasts
* @param title the title of the dialog
* @param message the message of the dialog
* @return the dialog that can be shown to the user with a custom dismiss listener
*/
static AlertDialog createMigrationInfoDialog(@NonNull final Context uiContext,
@NonNull final String title,
@NonNull final String message) {
return new AlertDialog.Builder(uiContext)
.setTitle(title)
.setMessage(message)
.setPositiveButton(R.string.ok, null)
.setOnDismissListener(dialog ->
MigrationManager.onMigrationInfoDismissed(uiContext))
.setCancelable(false) // prevents the dialog from being dismissed accidentally
.create();
}
}

View File

@ -1,11 +1,14 @@
package org.schabi.newpipe.settings;
package org.schabi.newpipe.settings.migration;
import static org.schabi.newpipe.MainActivity.DEBUG;
import static org.schabi.newpipe.extractor.ServiceList.SoundCloud;
import static org.schabi.newpipe.extractor.ServiceList.YouTube;
import android.content.Context;
import android.content.SharedPreferences;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.core.util.Consumer;
import androidx.preference.PreferenceManager;
@ -18,34 +21,34 @@ import org.schabi.newpipe.settings.tabs.Tab;
import org.schabi.newpipe.settings.tabs.TabsManager;
import org.schabi.newpipe.util.DeviceUtils;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import static org.schabi.newpipe.MainActivity.DEBUG;
import static org.schabi.newpipe.extractor.ServiceList.SoundCloud;
/**
* In order to add a migration, follow these steps, given P is the previous version:<br>
* - in the class body add a new {@code MIGRATION_P_P+1 = new Migration(P, P+1) { ... }} and put in
* the {@code migrate()} method the code that need to be run when migrating from P to P+1<br>
* - add {@code MIGRATION_P_P+1} at the end of {@link SettingMigrations#SETTING_MIGRATIONS}<br>
* - increment {@link SettingMigrations#VERSION}'s value by 1 (so it should become P+1)
* This class contains the code to migrate the settings from one version to another.
* Migrations are run automatically when the app is started and the settings version changed.
* <br>
* In order to add a migration, follow these steps, given {@code P} is the previous version:
* <ul>
* <li>in the class body add a new {@code MIGRATION_P_P+1 = new Migration(P, P+1) { ... }} and put
* in the {@code migrate()} method the code that need to be run
* when migrating from {@code P} to {@code P+1}</li>
* <li>add {@code MIGRATION_P_P+1} at the end of {@link SettingMigrations#SETTING_MIGRATIONS}</li>
* <li>increment {@link SettingMigrations#VERSION}'s value by 1
* (so it becomes {@code P+1})</li>
* </ul>
* Migrations can register UI actions using {@link MigrationManager#addMigrationInfo(Consumer)}
* that will be performed after the UI is initialized to inform the user about changes
* that were applied by migrations.
*/
public final class SettingMigrations {
private static final String TAG = SettingMigrations.class.toString();
private static SharedPreferences sp;
/**
* List of UI actions that are performed after the UI is initialized (e.g. showing alert
* dialogs) to inform the user about changes that were applied by migrations.
*/
private static final List<Consumer<Context>> MIGRATION_INFO = new ArrayList<>();
private static final Migration MIGRATION_0_1 = new Migration(0, 1) {
@Override
public void migrate(@NonNull final Context context) {
@ -172,13 +175,48 @@ public final class SettingMigrations {
if (tabs.size() != cleanedTabs.size()) {
tabsManager.saveTabs(cleanedTabs);
// create an AlertDialog to inform the user about the change
MIGRATION_INFO.add((Context uiContext) -> new AlertDialog.Builder(uiContext)
.setTitle(R.string.migration_info_6_7_title)
.setMessage(R.string.migration_info_6_7_message)
.setPositiveButton(R.string.ok, null)
.setCancelable(false)
.create()
.show());
MigrationManager.addMigrationInfo(uiContext ->
MigrationManager.createMigrationInfoDialog(
uiContext,
uiContext.getString(R.string.migration_info_6_7_title),
uiContext.getString(R.string.migration_info_6_7_message))
.show());
}
}
};
private static final Migration MIGRATION_7_8 = new Migration(7, 8) {
@Override
protected void migrate(@NonNull final Context context) {
// YouTube remove the combined Trending kiosk, see
// https://github.com/TeamNewPipe/NewPipe/discussions/12445 for more information.
// If the user has a dedicated YouTube/Trending kiosk tab,
// it is removed and replaced with the new live kiosk tab.
// The default trending kiosk tab is not touched
// because it uses the default kiosk provided by the extractor
// and is thus updated automatically.
final TabsManager tabsManager = TabsManager.getManager(context);
final List<Tab> tabs = tabsManager.getTabs();
final List<Tab> cleanedTabs = tabs.stream()
.filter(tab -> !(tab instanceof Tab.KioskTab kioskTab
&& kioskTab.getKioskServiceId() == YouTube.getServiceId()
&& kioskTab.getKioskId().equals("Trending")))
.collect(Collectors.toUnmodifiableList());
if (tabs.size() != cleanedTabs.size()) {
tabsManager.saveTabs(cleanedTabs);
}
final boolean hasDefaultTrendingTab = tabs.stream()
.anyMatch(tab -> tab instanceof Tab.DefaultKioskTab);
if (tabs.size() != cleanedTabs.size() || hasDefaultTrendingTab) {
// User is informed about the change
MigrationManager.addMigrationInfo(uiContext ->
MigrationManager.createMigrationInfoDialog(
uiContext,
uiContext.getString(R.string.migration_info_7_8_title),
uiContext.getString(R.string.migration_info_7_8_message))
.show());
}
}
};
@ -196,16 +234,17 @@ public final class SettingMigrations {
MIGRATION_3_4,
MIGRATION_4_5,
MIGRATION_5_6,
MIGRATION_6_7
MIGRATION_6_7,
MIGRATION_7_8,
};
/**
* Version number for preferences. Must be incremented every time a migration is necessary.
*/
private static final int VERSION = 7;
private static final int VERSION = 8;
public static void runMigrationsIfNeeded(@NonNull final Context context) {
static void runMigrationsIfNeeded(@NonNull final Context context) {
// setup migrations and check if there is something to do
sp = PreferenceManager.getDefaultSharedPreferences(context);
final String lastPrefVersionKey = context.getString(R.string.last_used_preferences_version);
@ -249,21 +288,6 @@ public final class SettingMigrations {
sp.edit().putInt(lastPrefVersionKey, currentVersion).apply();
}
/**
* Perform UI actions informing about migrations that took place if they are present.
* @param context Context that can be used to show dialogs/snackbars/toasts
*/
public static void showUserInfoIfPresent(@NonNull final Context context) {
for (final Consumer<Context> consumer : MIGRATION_INFO) {
try {
consumer.accept(context);
} catch (final Exception e) {
ErrorUtil.showUiErrorSnackbar(context, "Showing migration info to the user", e);
}
}
MIGRATION_INFO.clear();
}
private SettingMigrations() { }
abstract static class Migration {

View File

@ -1,5 +1,7 @@
package org.schabi.newpipe.settings.preferencesearch;
import static org.schabi.newpipe.ui.emptystate.EmptyStateUtil.setEmptyStateComposable;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
@ -12,7 +14,6 @@ import androidx.recyclerview.widget.LinearLayoutManager;
import org.schabi.newpipe.databinding.SettingsPreferencesearchFragmentBinding;
import org.schabi.newpipe.ui.emptystate.EmptyStateSpec;
import org.schabi.newpipe.ui.emptystate.EmptyStateUtil;
import java.util.List;
@ -41,9 +42,7 @@ public class PreferenceSearchFragment extends Fragment {
binding = SettingsPreferencesearchFragmentBinding.inflate(inflater, container, false);
binding.searchResults.setLayoutManager(new LinearLayoutManager(getContext()));
EmptyStateUtil.setEmptyStateComposable(
binding.emptyStateView,
EmptyStateSpec.Companion.getNoSearchMaxSizeResult());
setEmptyStateComposable(binding.emptyStateView, EmptyStateSpec.NoSearchResult);
adapter = new PreferenceSearchAdapter();
adapter.setOnItemClickListener(this::onItemClicked);

View File

@ -1,12 +1,14 @@
package org.schabi.newpipe.ui.components.about
import androidx.annotation.StringRes
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.foundation.layout.wrapContentWidth
import androidx.compose.foundation.rememberScrollState
@ -16,7 +18,6 @@ import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.NonRestartableComposable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
@ -26,13 +27,12 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
import androidx.compose.ui.unit.dp
import androidx.core.content.ContextCompat.getDrawable
import coil3.compose.AsyncImage
import my.nanihadesuka.compose.ColumnScrollbar
import org.schabi.newpipe.BuildConfig
import org.schabi.newpipe.R
import org.schabi.newpipe.ui.components.common.defaultThemedScrollbarSettings
import org.schabi.newpipe.util.external_communication.ShareUtils
import org.schabi.newpipe.util.image.NewPipeSquircleIcon
private val ABOUT_ITEMS = listOf(
AboutData(R.string.faq_title, R.string.faq_description, R.string.faq, R.string.faq_url),
@ -83,12 +83,10 @@ fun AboutTab() {
.wrapContentSize(Alignment.Center),
horizontalAlignment = Alignment.CenterHorizontally
) {
// note: the preview
val context = LocalContext.current
val launcherDrawable = remember { getDrawable(context, R.mipmap.ic_launcher) }
AsyncImage(
model = launcherDrawable,
Image(
imageVector = NewPipeSquircleIcon,
contentDescription = stringResource(R.string.app_name),
modifier = Modifier.size(64.dp),
)
Spacer(Modifier.height(4.dp))
Text(

View File

@ -19,7 +19,6 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.rememberNestedScrollInteropConnection
import androidx.compose.ui.res.pluralStringResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.datasource.LoremIpsum
import androidx.compose.ui.unit.dp
@ -122,28 +121,23 @@ private fun CommentRepliesDialog(
if (comments.itemCount == 0) {
item {
val refresh = comments.loadState.refresh
if (refresh is LoadState.Loading) {
LoadingIndicator(modifier = Modifier.padding(top = 8.dp))
} else if (refresh is LoadState.Error) {
// TODO use error panel instead
EmptyStateComposable(
spec = EmptyStateSpec.DisabledComments.copy(
descriptionText = {
stringResource(R.string.error_unable_to_load_comments)
when (val refresh = comments.loadState.refresh) {
is LoadState.Loading -> {
LoadingIndicator(modifier = Modifier.padding(top = 8.dp))
}
else -> {
// TODO use error panel instead
EmptyStateComposable(
spec = if (refresh is LoadState.Error) {
EmptyStateSpec.ErrorLoadingComments
} else {
EmptyStateSpec.NoComments
},
),
modifier = Modifier
.fillMaxWidth()
.heightIn(min = 128.dp)
)
} else {
EmptyStateComposable(
spec = EmptyStateSpec.NoComments,
modifier = Modifier
.fillMaxWidth()
.heightIn(min = 128.dp)
)
modifier = Modifier
.fillMaxWidth()
.heightIn(min = 128.dp)
)
}
}
}
} else {

View File

@ -81,12 +81,11 @@ fun CommentRepliesHeader(comment: CommentsInfoItem, onCommentAuthorOpened: () ->
style = MaterialTheme.typography.titleSmall,
)
Text(
text = Localization.relativeTimeOrTextual(
context, comment.uploadDate, comment.textualUploadDate
),
style = MaterialTheme.typography.bodySmall,
)
Localization.relativeTimeOrTextual(
context, comment.uploadDate, comment.textualUploadDate
)?.let {
Text(text = it, style = MaterialTheme.typography.bodySmall)
}
}
}

View File

@ -15,7 +15,6 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.rememberNestedScrollInteropConnection
import androidx.compose.ui.res.pluralStringResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
@ -75,7 +74,6 @@ private fun CommentSection(
modifier = Modifier
.fillMaxWidth()
.heightIn(min = 128.dp)
)
}
} else if (count == 0) {
@ -111,13 +109,7 @@ private fun CommentSection(
is LoadState.Error -> {
item {
// TODO use error panel instead
EmptyStateComposable(
EmptyStateSpec.DisabledComments.copy(
descriptionText = {
stringResource(R.string.error_unable_to_load_comments)
}
)
)
EmptyStateComposable(EmptyStateSpec.ErrorLoadingComments)
}
}
@ -134,11 +126,7 @@ private fun CommentSection(
item {
// TODO use error panel instead
EmptyStateComposable(
spec = EmptyStateSpec.DisabledComments.copy(
descriptionText = {
stringResource(R.string.error_unable_to_load_comments)
}
),
spec = EmptyStateSpec.ErrorLoadingComments,
modifier = Modifier
.fillMaxWidth()
.heightIn(min = 128.dp)

View File

@ -1,6 +1,7 @@
package org.schabi.newpipe.ui.emptystate
import android.graphics.Color
import androidx.annotation.StringRes
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
@ -22,25 +23,14 @@ import org.schabi.newpipe.ui.theme.AppTheme
fun EmptyStateComposable(
spec: EmptyStateSpec,
modifier: Modifier = Modifier,
) = EmptyStateComposable(
emojiText = spec.emojiText(),
descriptionText = spec.descriptionText(),
modifier = modifier
)
@Composable
private fun EmptyStateComposable(
emojiText: String,
descriptionText: String,
modifier: Modifier = Modifier,
) {
Column(
modifier = modifier,
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
verticalArrangement = Arrangement.Center,
) {
Text(
text = emojiText,
text = spec.emojiText,
style = MaterialTheme.typography.titleLarge,
textAlign = TextAlign.Center,
)
@ -49,7 +39,7 @@ private fun EmptyStateComposable(
modifier = Modifier
.padding(top = 6.dp)
.padding(horizontal = 16.dp),
text = descriptionText,
text = stringResource(spec.descriptionText),
style = MaterialTheme.typography.bodyMedium,
textAlign = TextAlign.Center,
)
@ -82,66 +72,48 @@ fun EmptyStateComposableNoCommentPreview() {
}
}
data class EmptyStateSpec(
val emojiText: @Composable () -> String,
val descriptionText: @Composable () -> String,
enum class EmptyStateSpec(
val emojiText: String,
@field:StringRes val descriptionText: Int,
) {
companion object {
val GenericError =
EmptyStateSpec(
emojiText = { "¯\\_(ツ)_/¯" },
descriptionText = { stringResource(id = R.string.empty_list_subtitle) },
)
val NoVideos =
EmptyStateSpec(
emojiText = { "(╯°-°)╯" },
descriptionText = { stringResource(id = R.string.no_videos) },
)
val NoComments =
EmptyStateSpec(
emojiText = { "¯\\_(╹x╹)_/¯" },
descriptionText = { stringResource(id = R.string.no_comments) },
)
val DisabledComments =
NoComments.copy(
descriptionText = { stringResource(id = R.string.comments_are_disabled) },
)
val NoSearchResult =
NoComments.copy(
emojiText = { "╰(°●°╰)" },
descriptionText = { stringResource(id = R.string.search_no_results) }
)
val NoSearchMaxSizeResult =
NoSearchResult
val ContentNotSupported =
NoComments.copy(
emojiText = { "(︶︹︺)" },
descriptionText = { stringResource(id = R.string.content_not_supported) },
)
val NoBookmarkedPlaylist =
EmptyStateSpec(
emojiText = { "(╥﹏╥)" },
descriptionText = { stringResource(id = R.string.no_playlist_bookmarked_yet) },
)
val NoSubscriptionsHint =
EmptyStateSpec(
emojiText = { "(꩜ᯅ꩜)" },
descriptionText = { stringResource(id = R.string.import_subscriptions_hint) },
)
val NoSubscriptions =
NoSubscriptionsHint.copy(
descriptionText = { stringResource(id = R.string.no_channel_subscribed_yet) },
)
}
GenericError(
emojiText = "¯\\_(ツ)_/¯",
descriptionText = R.string.empty_list_subtitle,
),
NoVideos(
emojiText = "(╯°-°)╯",
descriptionText = R.string.no_videos,
),
NoComments(
emojiText = "¯\\_(╹x╹)_/¯",
descriptionText = R.string.no_comments,
),
DisabledComments(
emojiText = "¯\\_(╹x╹)_/¯",
descriptionText = R.string.comments_are_disabled,
),
ErrorLoadingComments(
emojiText = "¯\\_(╹x╹)_/¯",
descriptionText = R.string.error_unable_to_load_comments,
),
NoSearchResult(
emojiText = "╰(°●°╰)",
descriptionText = R.string.search_no_results,
),
ContentNotSupported(
emojiText = "(︶︹︺)",
descriptionText = R.string.content_not_supported,
),
NoBookmarkedPlaylist(
emojiText = "(╥﹏╥)",
descriptionText = R.string.no_playlist_bookmarked_yet,
),
NoSubscriptionsHint(
emojiText = "(꩜ᯅ꩜)",
descriptionText = R.string.import_subscriptions_hint,
),
NoSubscriptions(
emojiText = "(꩜ᯅ꩜)",
descriptionText = R.string.no_channel_subscribed_yet,
),
}

View File

@ -52,6 +52,14 @@ public final class KioskTranslator {
return c.getString(R.string.featured);
case "Radio":
return c.getString(R.string.radio);
case "trending_gaming":
return c.getString(R.string.trending_gaming);
case "trending_music":
return c.getString(R.string.trending_music);
case "trending_movies_and_shows":
return c.getString(R.string.trending_movies);
case "trending_podcasts_episodes":
return c.getString(R.string.trending_podcasts);
default:
return kioskId;
}
@ -77,6 +85,14 @@ public final class KioskTranslator {
return R.drawable.ic_stars;
case "Radio":
return R.drawable.ic_radio;
case "trending_gaming":
return R.drawable.ic_videogame_asset;
case "trending_music":
return R.drawable.ic_music_note;
case "trending_movies_and_shows":
return R.drawable.ic_movie;
case "trending_podcasts_episodes":
return R.drawable.ic_podcasts;
default:
return 0;
}

View File

@ -127,14 +127,13 @@ public final class Localization {
}
public static String localizeNumber(final double number) {
final NumberFormat nf = NumberFormat.getInstance(getAppLocale());
return nf.format(number);
return NumberFormat.getInstance(getAppLocale()).format(number);
}
public static String formatDate(@NonNull final OffsetDateTime offsetDateTime) {
return DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM)
.withLocale(getAppLocale()).format(offsetDateTime
.atZoneSameInstant(ZoneId.systemDefault()));
.withLocale(getAppLocale())
.format(offsetDateTime.atZoneSameInstant(ZoneId.systemDefault()));
}
@SuppressLint("StringFormatInvalid")
@ -191,14 +190,20 @@ public final class Localization {
final double value = (double) count;
if (count >= 1000000000) {
return localizeNumber(round(value / 1000000000))
+ context.getString(R.string.short_billion);
final double shortenedValue = value / 1000000000;
final int scale = shortenedValue >= 100 ? 0 : 1;
return context.getString(R.string.short_billion,
localizeNumber(round(shortenedValue, scale)));
} else if (count >= 1000000) {
return localizeNumber(round(value / 1000000))
+ context.getString(R.string.short_million);
final double shortenedValue = value / 1000000;
final int scale = shortenedValue >= 100 ? 0 : 1;
return context.getString(R.string.short_million,
localizeNumber(round(shortenedValue, scale)));
} else if (count >= 1000) {
return localizeNumber(round(value / 1000))
+ context.getString(R.string.short_thousand);
final double shortenedValue = value / 1000;
final int scale = shortenedValue >= 100 ? 0 : 1;
return context.getString(R.string.short_thousand,
localizeNumber(round(shortenedValue, scale)));
} else {
return localizeNumber(value);
}
@ -384,9 +389,10 @@ public final class Localization {
* {@code parsed != null} and the relevant setting is enabled, {@code textual} will
* be appended to the returned string for debugging purposes.
*/
@Nullable
public static String relativeTimeOrTextual(@Nullable final Context context,
@Nullable final DateWrapper parsed,
final String textual) {
@Nullable final String textual) {
if (parsed == null) {
return textual;
} else if (DEBUG && context != null && PreferenceManager
@ -411,8 +417,8 @@ public final class Localization {
}
}
private static double round(final double value) {
return new BigDecimal(value).setScale(1, RoundingMode.HALF_UP).doubleValue();
private static double round(final double value, final int scale) {
return new BigDecimal(value).setScale(scale, RoundingMode.HALF_UP).doubleValue();
}
private static String getQuantity(@NonNull final Context context,

View File

@ -9,9 +9,11 @@ import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Build;
import android.provider.Settings;
import android.text.Html;
import android.widget.Toast;
import androidx.annotation.RequiresApi;
import androidx.appcompat.app.AlertDialog;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
@ -113,14 +115,47 @@ public final class PermissionHelper {
@RequiresApi(api = Build.VERSION_CODES.M)
public static boolean checkSystemAlertWindowPermission(final Context context) {
if (!Settings.canDrawOverlays(context)) {
final Intent i = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION,
Uri.parse("package:" + context.getPackageName()));
i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
try {
context.startActivity(i);
} catch (final ActivityNotFoundException ignored) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
final Intent i = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION,
Uri.parse("package:" + context.getPackageName()));
i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
try {
context.startActivity(i);
} catch (final ActivityNotFoundException ignored) {
}
return false;
// from Android R the ACTION_MANAGE_OVERLAY_PERMISSION will only point to the menu,
// so lets add a dialog that points the user to the right setting.
} else {
final String appName = context.getApplicationInfo()
.loadLabel(context.getPackageManager()).toString();
final String title = context.getString(R.string.permission_display_over_apps);
final String permissionName =
context.getString(R.string.permission_display_over_apps_permission_name);
final String appNameItalic = "<i>" + appName + "</i>";
final String permissionNameItalic = "<i>" + permissionName + "</i>";
final String message =
context.getString(R.string.permission_display_over_apps_message,
appNameItalic,
permissionNameItalic
);
new AlertDialog.Builder(context)
.setTitle(title)
.setMessage(Html.fromHtml(message, Html.FROM_HTML_MODE_COMPACT))
.setPositiveButton("OK", (dialog, which) -> {
// we dont need the package name here, since it wont do anything on >R
final Intent intent =
new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION);
try {
context.startActivity(intent);
} catch (final ActivityNotFoundException ignored) {
}
})
.setCancelable(true)
.show();
return false;
}
return false;
} else {
return true;
}

View File

@ -121,7 +121,7 @@ public final class SparseItemUtil {
callback.accept(result);
}, throwable -> ErrorUtil.createNotification(context,
new ErrorInfo(throwable, UserAction.REQUESTED_STREAM,
"Loading stream info: " + url, serviceId)
"Loading stream info: " + url, serviceId, url)
));
}
}

View File

@ -0,0 +1,99 @@
package org.schabi.newpipe.util.image
import androidx.compose.foundation.Image
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.graphics.vector.path
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
/**
* Generated with https://github.com/rafaeltonholo/svg-to-compose/
* based on assets/newpipe_squircle.svg.
*/
val NewPipeSquircleIcon: ImageVector
get() {
val current = _newPipeIcon
if (current != null) return current
return ImageVector.Builder(
name = "org.schabi.newpipe.ui.theme.AppTheme.NewPipeSquircleIcon",
defaultWidth = 100.0.dp,
defaultHeight = 100.0.dp,
viewportWidth = 100.0f,
viewportHeight = 100.0f,
).apply {
// M0 50 C0 15 15 0 50 0 s50 15 50 50 -15 50 -50 50 S0 85 0 50
path(
fill = SolidColor(Color(0xFFCD201F)),
) {
// M 0 50
moveTo(x = 0.0f, y = 50.0f)
// C 0 15 15 0 50 0
curveTo(
x1 = 0.0f,
y1 = 15.0f,
x2 = 15.0f,
y2 = 0.0f,
x3 = 50.0f,
y3 = 0.0f,
)
// s 50 15 50 50
reflectiveCurveToRelative(
dx1 = 50.0f,
dy1 = 15.0f,
dx2 = 50.0f,
dy2 = 50.0f,
)
// s -15 50 -50 50
reflectiveCurveToRelative(
dx1 = -15.0f,
dy1 = 50.0f,
dx2 = -50.0f,
dy2 = 50.0f,
)
// S 0 85 0 50
reflectiveCurveTo(
x1 = 0.0f,
y1 = 85.0f,
x2 = 0.0f,
y2 = 50.0f,
)
}
// M31.7 19.2 v61.7 l9.7 -5.73 V36 l23.8 14 -17.6 10.35 V71.5 L84 50
path(
fill = SolidColor(Color(0xFFFFFFFF)),
) {
// M 31.7 19.2
moveTo(x = 31.7f, y = 19.2f)
// v 61.7
verticalLineToRelative(dy = 61.7f)
// l 9.7 -5.73
lineToRelative(dx = 9.7f, dy = -5.73f)
// V 36
verticalLineTo(y = 36.0f)
// l 23.8 14
lineToRelative(dx = 23.8f, dy = 14.0f)
// l -17.6 10.35
lineToRelative(dx = -17.6f, dy = 10.35f)
// V 71.5
verticalLineTo(y = 71.5f)
// L 84 50
lineTo(x = 84.0f, y = 50.0f)
}
}.build().also { _newPipeIcon = it }
}
@Preview
@Composable
private fun IconPreview() {
Image(
imageVector = NewPipeSquircleIcon,
contentDescription = null,
)
}
@Suppress("ObjectPropertyName")
private var _newPipeIcon: ImageVector? = null

View File

@ -1,14 +1,13 @@
package org.schabi.newpipe.util.text;
import android.content.Context;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import org.schabi.newpipe.MainActivity;
import org.schabi.newpipe.R;
import org.schabi.newpipe.error.ErrorPanelHelper;
import org.schabi.newpipe.error.ErrorInfo;
import org.schabi.newpipe.error.ErrorUtil;
import org.schabi.newpipe.error.UserAction;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.StreamingService;
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
@ -158,19 +157,13 @@ public final class InternalUrlsHandler {
disposables.add(single.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(info -> {
final PlayQueue playQueue =
new SinglePlayQueue(info, seconds * 1000L);
final PlayQueue playQueue = new SinglePlayQueue(info, seconds * 1000L);
NavigationHelper.playOnPopupPlayer(context, playQueue, false);
}, throwable -> {
if (DEBUG) {
Log.e(TAG, "Could not play on popup: " + url, throwable);
}
new AlertDialog.Builder(context)
.setTitle(R.string.player_stream_failure)
.setMessage(
ErrorPanelHelper.Companion.getExceptionDescription(throwable))
.setPositiveButton(R.string.ok, null)
.show();
// This will only show a snackbar if the passed context has a root view:
// otherwise it will resort to showing a notification, so we are safe here.
ErrorUtil.showSnackbar(context,
new ErrorInfo(throwable, UserAction.PLAY_ON_POPUP, url, null, url));
}));
return true;
}

View File

@ -265,7 +265,7 @@ public class DownloadManager {
}
}
public void deleteMission(Mission mission) {
public void deleteMission(Mission mission, boolean alsoDeleteFile) {
synchronized (this) {
if (mission instanceof DownloadMission) {
mMissionsPending.remove(mission);
@ -274,7 +274,9 @@ public class DownloadManager {
mFinishedMissionStore.deleteMission(mission);
}
mission.delete();
if (alsoDeleteFile) {
mission.delete();
}
}
}

View File

@ -563,16 +563,16 @@ public class MissionAdapter extends Adapter<ViewHolder> implements Handler.Callb
}
request.append("]");
String service;
Integer service;
try {
service = NewPipe.getServiceByUrl(mission.source).getServiceInfo().getName();
service = NewPipe.getServiceByUrl(mission.source).getServiceId();
} catch (Exception e) {
service = ErrorInfo.SERVICE_NONE;
service = null;
}
ErrorUtil.createNotification(mContext,
new ErrorInfo(ErrorInfo.Companion.throwableToStringList(mission.errObject), action,
service, request.toString(), reason));
request.toString(), service, reason));
}
public void clearFinishedDownloads(boolean delete) {
@ -614,7 +614,7 @@ public class MissionAdapter extends Adapter<ViewHolder> implements Handler.Callb
while (i.hasNext()) {
Mission mission = i.next();
if (mission != null) {
mDownloadManager.deleteMission(mission);
mDownloadManager.deleteMission(mission, true);
mContext.sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, mission.storage.getUri()));
}
i.remove();
@ -667,7 +667,14 @@ public class MissionAdapter extends Adapter<ViewHolder> implements Handler.Callb
shareFile(h.item.mission);
return true;
case R.id.delete:
mDeleter.append(h.item.mission);
// delete the entry and the file
mDeleter.append(h.item.mission, true);
applyChanges();
checkMasterButtonsVisibility();
return true;
case R.id.delete_entry:
// just delete the entry
mDeleter.append(h.item.mission, false);
applyChanges();
checkMasterButtonsVisibility();
return true;
@ -676,7 +683,7 @@ public class MissionAdapter extends Adapter<ViewHolder> implements Handler.Callb
final StoredFileHelper storage = h.item.mission.storage;
if (!storage.existsAsFile()) {
Toast.makeText(mContext, R.string.missing_file, Toast.LENGTH_SHORT).show();
mDeleter.append(h.item.mission);
mDeleter.append(h.item.mission, true);
applyChanges();
return true;
}

View File

@ -13,7 +13,9 @@ import com.google.android.material.snackbar.Snackbar;
import org.schabi.newpipe.R;
import java.util.ArrayList;
import java.util.Optional;
import kotlin.Pair;
import us.shandian.giga.get.FinishedMission;
import us.shandian.giga.get.Mission;
import us.shandian.giga.service.DownloadManager;
@ -30,7 +32,8 @@ public class Deleter {
private static final int DELAY_RESUME = 400;// ms
private Snackbar snackbar;
private ArrayList<Mission> items;
// list of missions to be deleted, and whether to also delete the corresponding file
private ArrayList<Pair<Mission, Boolean>> items;
private boolean running = true;
private final Context mContext;
@ -51,7 +54,7 @@ public class Deleter {
items = new ArrayList<>(2);
}
public void append(Mission item) {
public void append(Mission item, boolean alsoDeleteFile) {
/* If a mission is removed from the list while the Snackbar for a previously
* removed item is still showing, commit the action for the previous item
* immediately. This prevents Snackbars from stacking up in reverse order.
@ -60,13 +63,13 @@ public class Deleter {
commit();
mIterator.hide(item);
items.add(0, item);
items.add(0, new Pair<>(item, alsoDeleteFile));
show();
}
private void forget() {
mIterator.unHide(items.remove(0));
mIterator.unHide(items.remove(0).getFirst());
mAdapter.applyChanges();
show();
@ -84,7 +87,19 @@ public class Deleter {
private void next() {
if (items.size() < 1) return;
String msg = mContext.getString(R.string.file_deleted).concat(":\n").concat(items.get(0).storage.getName());
final Optional<String> fileToBeDeleted = items.stream()
.filter(Pair::getSecond)
.map(p -> p.getFirst().storage.getName())
.findFirst();
String msg;
if (fileToBeDeleted.isPresent()) {
msg = mContext.getString(R.string.file_deleted)
.concat(":\n")
.concat(fileToBeDeleted.get());
} else {
msg = mContext.getString(R.string.entry_deleted);
}
snackbar = Snackbar.make(mView, msg, Snackbar.LENGTH_INDEFINITE);
snackbar.setAction(R.string.undo, s -> forget());
@ -98,11 +113,13 @@ public class Deleter {
if (items.size() < 1) return;
while (items.size() > 0) {
Mission mission = items.remove(0);
Pair<Mission, Boolean> missionAndAlsoDeleteFile = items.remove(0);
Mission mission = missionAndAlsoDeleteFile.getFirst();
boolean alsoDeleteFile = missionAndAlsoDeleteFile.getSecond();
if (mission.deleted) continue;
mIterator.unHide(mission);
mDownloadManager.deleteMission(mission);
mDownloadManager.deleteMission(mission, alsoDeleteFile);
if (mission instanceof FinishedMission) {
mContext.sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, mission.storage.getUri()));
@ -137,7 +154,11 @@ public class Deleter {
pause();
for (Mission mission : items) mDownloadManager.deleteMission(mission);
for (Pair<Mission, Boolean> missionAndAlsoDeleteFile : items) {
Mission mission = missionAndAlsoDeleteFile.getFirst();
boolean alsoDeleteFile = missionAndAlsoDeleteFile.getSecond();
mDownloadManager.deleteMission(mission, alsoDeleteFile);
}
items = null;
}
}

View File

@ -0,0 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
<path android:fillColor="@android:color/white" android:pathData="M14,12c0,0.74 -0.4,1.38 -1,1.72V22h-2v-8.28c-0.6,-0.35 -1,-0.98 -1,-1.72c0,-1.1 0.9,-2 2,-2S14,10.9 14,12zM12,6c-3.31,0 -6,2.69 -6,6c0,1.74 0.75,3.31 1.94,4.4l1.42,-1.42C8.53,14.25 8,13.19 8,12c0,-2.21 1.79,-4 4,-4s4,1.79 4,4c0,1.19 -0.53,2.25 -1.36,2.98l1.42,1.42C17.25,15.31 18,13.74 18,12C18,8.69 15.31,6 12,6zM12,2C6.48,2 2,6.48 2,12c0,2.85 1.2,5.41 3.11,7.24l1.42,-1.42C4.98,16.36 4,14.29 4,12c0,-4.41 3.59,-8 8,-8s8,3.59 8,8c0,2.29 -0.98,4.36 -2.53,5.82l1.42,1.42C20.8,17.41 22,14.85 22,12C22,6.48 17.52,2 12,2z"/>
</vector>

View File

@ -2,5 +2,6 @@
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<group android:id="@+id/menu_services_group" />
<group android:id="@+id/menu_tabs_group" />
<group android:id="@+id/menu_kiosks_group" />
<group android:id="@+id/menu_options_about_group" />
</menu>

View File

@ -27,7 +27,11 @@
<item
android:id="@+id/delete"
android:title="@string/delete" />
android:title="@string/delete_file" />
<item
android:id="@+id/delete_entry"
android:title="@string/delete_entry" />
<item
android:id="@+id/error_message_view"

View File

@ -75,13 +75,11 @@
<string name="downloads_title">الملفات المحملة</string>
<string name="invalid_source">لا يوجد مثل هذا الملف/مصدر المحتوى</string>
<string name="most_liked">الأكثر إعجابًا</string>
<string name="short_billion">بليون</string>
<string name="feed_load_error_account_info">تعذر تحميل موجز \'%s\'.</string>
<string name="question_mark">؟</string>
<string name="check_for_updates">التحقق من وجود تحديثات</string>
<string name="peertube_instance_url_title">مثيلات خوادم پيرتيوب</string>
<string name="more_than_100_videos">+100 فيديو</string>
<string name="short_thousand">ألف</string>
<string name="peertube_instance_add_exists">مثيل الخادم موجود بالفعل</string>
<string name="clear_queue_confirmation_title">طلب تأكيد قبل مسح قائمة الانتظار</string>
<string name="metadata_subscribers">المشتركون</string>
@ -642,13 +640,11 @@
<string name="play_queue_audio_track">الصوت : %s</string>
<string name="playback_step">خطوة</string>
<string name="recaptcha_solve">حل</string>
<string name="service_provides_reason">%s يقدم هذا السبب:</string>
<string name="selected_stream_external_player_not_supported">الدفق المحدد غير مدعوم من قبل المشغلون الخارجيون</string>
<string name="title_activity_about">عن تطبيق نيوپايپ</string>
<string name="seek_duration_title">تسريع إلى الأمام/-ترجيع وقت البحث</string>
<string name="permission_denied">تم رفضها من قبل النظام</string>
<string name="no_comments">ليس هناك تعليقات</string>
<string name="short_million">مليون</string>
<string name="checking_updates_toast">جاري التحقق من وجود تحديثات…</string>
<string name="content">المحتوى</string>
<string name="downloads_storage_ask_title">اسأل عن مكان التنزيل</string>

View File

@ -116,9 +116,6 @@
<string name="empty_list_subtitle">لا شيء هنا سوى الصراصير</string>
<string name="audio">الصوت</string>
<string name="retry">إعادة المحاولة</string>
<string name="short_thousand">ألف</string>
<string name="short_million">مليون</string>
<string name="short_billion">بليون</string>
<string name="no_subscribers">ليس هناك مشترِكون</string>
<plurals name="subscribers">
<item quantity="zero">%s مشارك</item>
@ -652,7 +649,6 @@
<string name="description_select_enable">تمكين تحديد نص في الوصف</string>
<string name="description_select_note">يمكنك الآن تحديد نص داخل الوصف. لاحظ أن الصفحة قد تومض وقد لا تكون الروابط قابلة للنقر أثناء وضع التحديد.</string>
<string name="open_website_license">فتح الموقع</string>
<string name="service_provides_reason">%s يقدم هذا السبب:</string>
<string name="account_terminated">تم إنهاء الحساب</string>
<string name="feed_load_error_fast_unknown">لا يوفر وضع التغذية السريعة مزيدًا من المعلومات حول هذا الموضوع.</string>
<string name="feed_load_error_terminated">حساب منشئ المحتوى قد تم إنهائه.
@ -883,4 +879,9 @@
<string name="feed_group_page_summary">صفحة مجموعة القناة</string>
<string name="select_a_feed_group">حدد مجموعة المحتوى</string>
<string name="no_feed_group_created_yet">لم تنشئ مجموعة محتوى</string>
<string name="channel_tab_likes">الإعجابات</string>
<string name="search_with_service_name">البحث %1$s</string>
<string name="search_with_service_name_and_filter">البحث %1$s (%2$s)</string>
<string name="migration_info_6_7_title">تمت إزالة صفحة أفضل 50 من SoundCloud</string>
<string name="migration_info_6_7_message">أوقفت SoundCloud صفحة أفضل 50 الأصلية. تمت إزالة علامة التبويب المقابلة من صفحتك الرئيسية.</string>
</resources>

View File

@ -297,9 +297,6 @@
<string name="detail_likes_img_view_description">Bəyən</string>
<string name="detail_dislikes_img_view_description">Bəyənmə</string>
<string name="detail_drag_description">Yenidən sıralamaq üçün sürüklə</string>
<string name="short_thousand">min</string>
<string name="short_million">Mln</string>
<string name="short_billion">Mlrd</string>
<string name="drawer_header_description">Xidməti dəyiş, hazırda seçilmiş:</string>
<string name="no_subscribers">Abunəçi yoxdur</string>
<string name="no_views">Baxış yoxdur</string>
@ -502,7 +499,6 @@
<string name="enable_queue_limit">Endirmə növbəsini məhdudlaşdır</string>
<string name="enable_queue_limit_desc">Eyni vaxtda ancaq bir endirmə həyata keçiriləcək</string>
<string name="account_terminated">Hesab ləğv edildi</string>
<string name="service_provides_reason">%s bu səbəbi təmin edir:</string>
<string name="download_has_started">Yükləmə başladı</string>
<string name="description_select_disable">ıqlamadakı mətni seçməyi qeyri-aktiv et</string>
<string name="metadata_category">Kateqoriya</string>

View File

@ -43,9 +43,6 @@
<string name="detail_dislikes_img_view_description">Tarrezmes</string>
<string name="default_video_format_title">Formatu de videu predetermináu</string>
<string name="black_theme_title">Prietu</string>
<string name="short_thousand">mil</string>
<string name="short_million">mill.</string>
<string name="short_billion">mil mill.</string>
<string name="msg_popup_permission">Precísase esti permisu p\'abrir
\nnel mou ventanu</string>
<string name="title_activity_recaptcha">Retu de reCAPTCHA</string>

View File

@ -267,9 +267,6 @@
</plurals>
<string name="no_subscribers">Obunachilar yo\'q</string>
<string name="drawer_header_description">Hozirda tanlangan xizmatni yoqish:</string>
<string name="short_billion">B</string>
<string name="short_million">M</string>
<string name="short_thousand">k</string>
<string name="retry">Qayta</string>
<string name="audio">Audio</string>
<string name="video">Video</string>

View File

@ -55,7 +55,7 @@
<string name="popup_remember_size_pos_summary">Памятаць апошнія памер і пазіцыю ўсплывальнага акна</string>
<string name="use_inexact_seek_title">Хуткі пошук пазіцыі</string>
<string name="use_inexact_seek_summary">Недакладны пошук дазваляе плэеру знаходзіць пазіцыі хутчэй са зніжанай дакладнасцю. Пошук цягам 5, 15 ці 25 секунд пры гэтым немажлівы</string>
<string name="thumbnail_cache_wipe_complete_notice">Кэш малюнкаў ачышчаны</string>
<string name="thumbnail_cache_wipe_complete_notice">Кэш відарысаў ачышчаны</string>
<string name="metadata_cache_wipe_title">Ачысціць кэш метаданых</string>
<string name="metadata_cache_wipe_summary">Выдаліць усе даныя вэб-старонак у кэшы</string>
<string name="metadata_cache_wipe_complete_notice">Кэш метаданых ачышчаны</string>
@ -71,8 +71,8 @@
<string name="resume_on_audio_focus_gain_summary">Працягваць прайграванне пасля перапынкаў (напрыклад, тэлефонных званкоў)</string>
<string name="download_dialog_title">Спампаваць</string>
<string name="show_next_and_similar_title">«Наступнае» і «Прапанаванае» відэа</string>
<string name="show_hold_to_append_title">Паказваць падказку «Зацісніце, каб дадаць»</string>
<string name="show_hold_to_append_summary">Паказваць падказку пры націсканні «У акне» або «У фоне» на старонцы звестак аб відэа</string>
<string name="show_hold_to_append_title">Паказваць падказку «Утрымлівайце, каб дадаць у чаргу»</string>
<string name="show_hold_to_append_summary">Паказваць падказку пры націсканні кнопкі «У акне» або «У фоне» на старонцы відэа</string>
<string name="unsupported_url">URL не падтрымліваецца</string>
<string name="default_content_country_title">Прадвызначаная краіна кантэнту</string>
<string name="content_language_title">Прадвызначаная мова кантэнту</string>
@ -137,7 +137,7 @@
<string name="invalid_directory">Такой папкі не існуе</string>
<string name="invalid_source">Такога файла або крыніцы кантэнту не існуе</string>
<string name="invalid_file">Файл не існуе або няма дазволу на яго чытанне ці запіс</string>
<string name="file_name_empty_error">Імя файла не можа быць пустым</string>
<string name="file_name_empty_error">Назва файла не можа быць пустой</string>
<string name="error_occurred_detail">Адбылася памылка: %1$s</string>
<string name="no_streams_available_download">Няма трансляцый, даступных для спампоўвання</string>
<string name="sorry_string">Прабачце, гэта не павінна было адбыцца.</string>
@ -154,14 +154,11 @@
<string name="detail_likes_img_view_description">Спадабалася</string>
<string name="detail_dislikes_img_view_description">Не спадабалася</string>
<string name="search_no_results">Няма вынікаў</string>
<string name="empty_list_subtitle">Нічога няма, акрамя цвыркуноў</string>
<string name="empty_list_subtitle">Нічога няма, хоць сабак ганяй</string>
<string name="detail_drag_description">Перацягніце, каб змяніць парадак</string>
<string name="video">Відэа</string>
<string name="audio">Аўдыя</string>
<string name="retry">Паспрабаваць зноў</string>
<string name="short_thousand">тыс.</string>
<string name="short_million">млн</string>
<string name="short_billion">млрд</string>
<string name="no_subscribers">Няма падпісчыкаў</string>
<plurals name="subscribers">
<item quantity="one">%s падпісчык</item>
@ -191,7 +188,7 @@
<string name="dismiss">Адхіліць</string>
<string name="rename">Перайменаваць</string>
<string name="ok">ОК</string>
<string name="msg_name">Імя файла</string>
<string name="msg_name">Назва файла</string>
<string name="msg_threads">Патокі</string>
<string name="msg_error">Памылка</string>
<string name="msg_running">NewPipe спампоўвае</string>
@ -204,7 +201,7 @@
<string name="title_activity_recaptcha">Запыт reCAPTCHA</string>
<string name="recaptcha_request_toast">Запытаны ўвод reCAPTCHA</string>
<string name="settings_category_downloads_title">Спампоўванне</string>
<string name="settings_file_charset_title">Дапушчальныя ў назвах файлаў сімвалы</string>
<string name="settings_file_charset_title">Сімвалы, дапушчальныя ў назвах файлаў</string>
<string name="settings_file_replacement_character_summary">Недапушчальныя сімвалы замяняюцца на гэты</string>
<string name="settings_file_replacement_character_title">Сімвал для замены</string>
<string name="charset_letters_and_digits">Літары і лічбы</string>
@ -253,7 +250,7 @@
<string name="play_queue_remove">Выдаліць</string>
<string name="play_queue_stream_detail">Падрабязнасці</string>
<string name="play_queue_audio_settings">Налады аўдыя</string>
<string name="hold_to_append">Зацісніце, каб дадаць у чаргу</string>
<string name="hold_to_append">Утрымлівайце, каб дадаць у чаргу</string>
<string name="start_here_on_background">Пачаць прайграванне ў фоне</string>
<string name="start_here_on_popup">Пачаць прайграванне ў акне</string>
<string name="drawer_open">Адкрыць бакавую панэль</string>
@ -268,7 +265,7 @@
<string name="preferred_player_fetcher_notification_message">Загрузка запытанага кантэнту</string>
<string name="create_playlist">Стварыць плэй-ліст</string>
<string name="rename_playlist">Перайменаваць</string>
<string name="name">Імя</string>
<string name="name">Назва</string>
<string name="add_to_playlist">Дадаць у плэй-ліст</string>
<string name="set_as_playlist_thumbnail">Зрабіць мініяцюрай плэй-ліста</string>
<string name="bookmark_playlist">Дадаць плэй-ліст у закладкі</string>
@ -327,7 +324,7 @@
<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="saved_tabs_invalid_json">Памылка чытання захаваных укладак. Выкарыстоўваюцца ўкладкі па змаўчанні</string>
<string name="saved_tabs_invalid_json">Не ўдалося прачытаць захаваныя ўкладкі, таму выкарыстоўваюцца прадвызначаныя</string>
<string name="restore_defaults">Аднавіць прадвызначаныя значэнні</string>
<string name="restore_defaults_confirmation">Аднавіць прадвызначаныя значэнні?</string>
<string name="subscribers_count_not_available">Колькасць падпісчыкаў недаступна</string>
@ -347,9 +344,9 @@
<string name="enqueue">Дадаць у чаргу</string>
<string name="permission_denied">Дзеянне забаронена сістэмай</string>
<string name="download_failed">Памылка спампоўвання</string>
<string name="generate_unique_name">Стварыць унікальнае імя</string>
<string name="generate_unique_name">Стварыць унікальную назву</string>
<string name="overwrite">Перазапісаць</string>
<string name="download_already_running">Файл з такім імем ужо спампоўваецца</string>
<string name="download_already_running">Файл з такой назвай ўжо спампоўваецца</string>
<string name="show_error">Паказаць тэкст памылкі</string>
<string name="error_path_creation">Немагчыма стварыць папку прызначэння</string>
<string name="error_file_creation">Немагчыма стварыць файл</string>
@ -376,14 +373,14 @@
<string name="enable_playback_resume_title">Працягваць прайграванне</string>
<string name="enable_playback_resume_summary">Аднаўляць апошнюю пазіцыю</string>
<string name="enable_playback_state_lists_title">Пазіцыі ў спісах</string>
<string name="enable_playback_state_lists_summary">Адлюстроўваць індыкатары пазіцый прагляду ў спісах</string>
<string name="enable_playback_state_lists_summary">Паказваць у спісах пазіцыю прайгравання</string>
<string name="settings_category_clear_data_title">Ачыстка даных</string>
<string name="watch_history_states_deleted">Пазіцыі прайгравання выдалены</string>
<string name="missing_file">Файл перамешчаны або выдалены</string>
<string name="overwrite_unrelated_warning">Файл з такім імем ужо існуе</string>
<string name="overwrite_finished_warning">Файл з такім імем ужо існуе</string>
<string name="overwrite_unrelated_warning">Файл з такой назвай ужо існуе</string>
<string name="overwrite_finished_warning">Спампаваны файл з такой назвай ужо існуе</string>
<string name="overwrite_failed">немагчыма перазапісаць файл</string>
<string name="download_already_pending">Файл з такім імем ужо дададзены ў чаргу спампоўвання</string>
<string name="download_already_pending">Файл з такой назвай ужо ў чарзе спампоўвання</string>
<string name="error_postprocessing_stopped">Праграма NewPipe была закрыта падчас працы з файлам</string>
<string name="error_insufficient_storage_left">На прыладзе скончылася вольнае месца</string>
<string name="error_progress_lost">Прагрэс страчаны, бо файл быў выдалены</string>
@ -542,11 +539,11 @@
<string name="hash_channel_description">Апавяшчэнні пра ход відэахэшавання</string>
<string name="create_error_notification">Стварыць паведамленне пра памылку</string>
<string name="feed_group_dialog_select_subscriptions">Выберыце падпіскі</string>
<string name="import_subscriptions_hint">Імпарт ці экспарт падпісак з 3-кропкавага меню</string>
<string name="import_subscriptions_hint">Імпартуйце або экспартуйце падпіскі праз меню з трыма кропкамі ⁝</string>
<string name="description_select_disable">Забарона вылучэння тэксту ў апісанні</string>
<string name="fast_mode">Хуткі рэжым</string>
<string name="faq_description">Калі ў вас узніклі праблемы з выкарыстаннем праграмы, абавязкова азнаёмцеся з адказамі на частыя пытанні!</string>
<string name="disable_media_tunneling_title">Адключыць тунэляванне медыя</string>
<string name="disable_media_tunneling_title">Адключыць тунэляванне мультымедыя</string>
<string name="seekbar_preview_thumbnail_title">Мініяцюра з перадпраглядам у паласе перамотвання</string>
<string name="high_quality_larger">Высокая якасць (больш)</string>
<string name="dont_show">Не паказваць</string>
@ -607,15 +604,15 @@
<string name="playlist_add_stream_success_duplicate">Дублікат дададзены %d раз(ы)</string>
<string name="leak_canary_not_available">LeakCanary недаступны</string>
<string name="show_memory_leaks">Паказаць уцечкі памяці</string>
<string name="disable_media_tunneling_summary">Адключыце мультымедыйнае тунэляванне, калі ў вас з\'яўляецца чорны экран або заіканне падчас прайгравання відэа.</string>
<string name="disable_media_tunneling_summary">Адключыце тунэляванне мультымедыя, калі відэа прайграецца перарывіста або паказваецца чорны экран.</string>
<string name="msg_failed_to_copy">Не ўдалося скапіяваць у буфер абмену</string>
<string name="no_dir_yet">Папка спампоўвання яшчэ не зададзена, выберыце папку спампоўвання цяпер</string>
<string name="faq_title">Частыя пытанні</string>
<string name="faq">Перайсці на вэб-сайт</string>
<string name="main_page_content_swipe_remove">Правядзіце пальцам па элементах, каб выдаліць іх</string>
<string name="main_page_content_swipe_remove">Каб выдаліць элемент, змахніце яго ўбок</string>
<string name="unset_playlist_thumbnail">Прыбраць пастаянную мініяцюру</string>
<string name="show_image_indicators_title">Паказваць індыкатары выяў</string>
<string name="show_image_indicators_summary">Паказваць каляровыя стужкі Пікаса на выявах, якія пазначаюць іх крыніцу: чырвоная для сеткі, сіняя для дыска і зялёная для памяці</string>
<string name="show_image_indicators_title">Паказваць на відарысах указальнікі</string>
<string name="show_image_indicators_summary">Паказваць на відарысах каляровыя меткі Picasso, якія абазначаюць яго крыніцу: чырвоная — сетка, сіняя — дыск, зялёная — памяць</string>
<string name="feed_processing_message">Апрацоўка стужкі…</string>
<string name="downloads_storage_ask_summary_no_saf_notice">Пры кожным спампоўванні вам будзе прапанавана выбраць месца захавання</string>
<string name="feed_notification_loading">Загрузка канала…</string>
@ -641,10 +638,10 @@
<item quantity="many">%d выбраных</item>
<item quantity="other">%d выбраных</item>
</plurals>
<string name="feed_group_dialog_empty_name">Пустая назва групы</string>
<string name="feed_group_dialog_delete_message">Выдаліць гэту групу?</string>
<string name="feed_group_dialog_empty_name">Назва групы пустая</string>
<string name="feed_group_dialog_delete_message">Выдаліць групу?</string>
<string name="feed_create_new_group_button_title">Новая</string>
<string name="feed_group_show_only_ungrouped_subscriptions">Паказаць толькі разгрупаваныя падпіскі</string>
<string name="feed_group_show_only_ungrouped_subscriptions">Паказваць толькі не згрупаваныя падпіскі</string>
<string name="feed_show_upcoming">Запланаваныя</string>
<string name="show_crash_the_player_title">Паказваць «Збой плэера»</string>
<string name="check_new_streams">Запусціце праверку новых патокаў</string>
@ -707,7 +704,6 @@
<string name="private_content">Гэта змесціва з\'яўляецца прыватным, таму NewPipe не можа яго трансляваць або спампоўваць.</string>
<string name="youtube_music_premium_content">Гэта відэа даступна толькі для падпісчыкаў YouTube Music Premium, таму NewPipe не можа яго трансляваць або спампоўваць.</string>
<string name="account_terminated">Уліковы запіс спынены</string>
<string name="service_provides_reason">%s дае наступную прычыну:</string>
<string name="featured">Вартае ўвагі</string>
<string name="metadata_privacy_internal">Унутраная</string>
<string name="feed_show_watched">Прагледжаныя цалкам</string>
@ -715,7 +711,7 @@
<string name="feed_use_dedicated_fetch_method_summary">Даступна для некаторых сэрвісаў, звычайна значна хутчэй, але можа перадаваць абмежаваную колькасць элементаў і не ўсю інфармацыю (можа адсутнічаць працягласць, тып элемента, паказчык трансляцыі)</string>
<string name="metadata_age_limit">Узроставае абмежаванне</string>
<string name="no_appropriate_file_manager_message_android_10">Для гэтага дзеяння не знойдзены прыдатны файлавы менеджар. \nУсталюйце файлавы менеджар, сумяшчальны з Storage Access Framework</string>
<string name="no_app_to_open_intent">Ніякая праграма на вашай прыладзе не можа адкрыць гэта</string>
<string name="no_app_to_open_intent">На прыладзе няма праграмы, каб адкрыць гэты файл</string>
<string name="progressive_load_interval_exoplayer_default">Стандартнае значэнне ExoPlayer</string>
<string name="feed_show_partially_watched">Прагледжаныя часткова</string>
<string name="feed_use_dedicated_fetch_method_help_text">Лічыце, што загрузка каналаў адбываецца занадта павольна? Калі так, паспрабуйце ўключыць хуткую загрузку (можна змяніць у наладах або націснуўшы кнопку ніжэй). \n \nNewPipe прапануе два спосабы загрузкі каналаў: \n• Атрыманне ўсяго канала падпіскі. Павольны, але інфармацыя поўная). \n• Выкарыстанне спецыяльнай канчатковай кропкі абслугоўвання. Хуткі, але звычайна інфармацыя няпоўная). \n \nРозніца паміж імі ў тым, што ў хуткім звычайна адсутнічае частка інфармацыі, напрыклад, працягласць або тып (немагчыма адрозніць трансляцыі ад звычайных відэа), і ён можа вяртаць менш элементаў. \n \nYouTube з\'яўляецца прыкладам сэрвісу, які прапануе гэты хуткі метад праз RSS-канал. \n \nТакім чынам, выбар залежыць ад таго, чаму вы аддаяце перавагу: хуткасці або дакладнасці інфармацыя.</string>
@ -747,8 +743,8 @@
<string name="audio_track_present_in_video">У гэтым патоку ўжо павінна быць гукавая дарожка</string>
<string name="use_exoplayer_decoder_fallback_summary">Уключыце гэту опцыю, калі ў вас ёсць праблемы з ініцыялізацыяй дэкодэра, якая вяртаецца да дэкодэраў з больш нізкім прыярытэтам, калі ініцыялізацыя асноўных дэкодэраў не ўдаецца. Гэта можа прывесці да нізкай прадукцыйнасці прайгравання, чым пры выкарыстанні асноўных дэкодэраў</string>
<string name="settings_category_exoplayer_summary">Кіраванне некаторымі наладамі ExoPlayer. Каб гэтыя змены ўступілі ў сілу, патрабуецца перазапуск прайгравальніка</string>
<string name="always_use_exoplayer_set_output_surface_workaround_summary">Гэты абыходны шлях вызваляе і паўторна стварае відэакодэкі, калі адбываецца змяненне паверхні, замест таго, каб зажаваць паверхню непасрэдна для кодэка. Ужо выкарыстоўваецца ExoPlayer на некаторых прыладах з такой праблемай, гэты параметр ужываецца толькі на прыладах з Android 6 і вышэй\n\nУключэнне параметра можа прадухіліць памылкі прайгравання пры пераключэнні бягучага відэаплэера або пераключэнні ў поўнаэкранны рэжым</string>
<string name="image_quality_title">Якасць выяў</string>
<string name="always_use_exoplayer_set_output_surface_workaround_summary">Гэты абыходны шлях вызваляе і паўторна стварае відэакодэкі, калі адбываецца змяненне паверхні, замест таго, каб задаваць паверхню непасрэдна для кодэка. Ужо выкарыстоўваецца ExoPlayer на некаторых прыладах з такой праблемай, гэты параметр ужываецца толькі на прыладах з Android 6 і вышэй\n\nУключэнне параметра можа прадухіліць памылкі прайгравання пры пераключэнні бягучага відэаплэера або пераключэнні ў поўнаэкранны рэжым</string>
<string name="image_quality_title">Якасць відарысаў</string>
<string name="channel_tab_videos">Відэа</string>
<string name="question_mark">\?</string>
<string name="metadata_subscribers">Падпісчыкі</string>
@ -767,12 +763,12 @@
<string name="feed_fetch_channel_tabs">Атрыманне ўкладак канала</string>
<string name="metadata_avatars">Аватары</string>
<string name="next_stream">Наступны паток</string>
<string name="disable_media_tunneling_automatic_info">Прадвызначана на вашай прыладзе адключана медыятунэляванне, бо гэтая мадэль прылады яго не падтрымлівае.</string>
<string name="disable_media_tunneling_automatic_info">Прадвызначана на вашай прыладзе адключана тунэляванне мультымедыя, бо вядома, што гэта мадэль яго не падтрымлівае.</string>
<string name="metadata_subchannel_avatars">Аватары падканалаў</string>
<string name="open_play_queue">Адкрыць чаргу прайгравання</string>
<string name="image_quality_none">Не загружаць выявы</string>
<string name="image_quality_none">Не загружаць відарысы</string>
<string name="image_quality_high">Высокая якасць</string>
<string name="channel_tab_about">Аб канале</string>
<string name="channel_tab_about">Пра канал</string>
<string name="share_playlist">Абагуліць плэй-ліст</string>
<string name="forward">Пераматаць наперад</string>
<string name="channel_tab_albums">Альбомы</string>
@ -786,9 +782,9 @@
<string name="video_details_list_item">- %1$s: %2$s</string>
<string name="main_tabs_position_summary">Перамясціць панэль укладак уніз</string>
<string name="no_live_streams">Няма жывых трансляцый</string>
<string name="image_quality_summary">Выберыце якасць выяў і ці трэба спампоўваць выявы ўвогуле, каб паменшыць выкарыстанне даных і памяці. Змены ачышчаюць кэш выяў як у памяці, так і на дыску - %s</string>
<string name="image_quality_summary">Выберыце якасць відарысаў ці ўвогуле не загружаць відарысы, каб паменшыць выкарыстанне даных і памяці. Змены ачышчаюць кэш відарысаў у памяці і на дыску (%s)</string>
<string name="play">Прайграць</string>
<string name="more_options">Іншыя опцыі</string>
<string name="more_options">Іншыя параметры</string>
<string name="metadata_thumbnails">Мініяцюры</string>
<string name="channel_tab_tracks">Трэкі</string>
<string name="duration">Працягласць</string>
@ -807,7 +803,7 @@
<string name="notification_actions_summary_android13">Каб адрэдагаваць кожнае з дзеянняў у апавяшчэнні, націсніце на яго. Першыя тры дзеянні (прайграванне/паўза, папярэдні і наступны) зададзены сістэмай, іх змяніць немагчыма.</string>
<string name="error_insufficient_storage">Недастаткова вольнага месца на прыладзе</string>
<string name="yes">Так</string>
<string name="auto_update_check_description">NewPipe можа аўтаматычна правяраць наяўнасць абнаўленняў і паведаміць вам, калі яны будуць даступны. \nУключыць гэту функцыю?</string>
<string name="auto_update_check_description">NewPipe можа час ад часу аўтаматычна правяраць наяўнасць новай версіі і апавяшчаць, калі яна будзе даступна. \nУключыць гэту функцыю?</string>
<string name="import_settings_vulnerable_format">Налады ў імпартаваным экспарце выкарыстоўваюць уразлівы фармат, які састарэў з версіі NewPipe 0.27.0. Пераканайцеся, што імпартаваны экспарт атрыманы з надзейнай крыніцы, і ў будучыні пераважней выкарыстоўваць толькі экспарт, атрыманы з NewPipe 0.27.0 ці навей. Падтрымка імпарту налад у гэтым уразлівым фармаце хутка будзе цалкам выдаленая, і тады старыя версіі NewPipe больш не змогуць імпартаваць наладкі з экспарту з новых версій.</string>
<string name="no">Не</string>
<string name="settings_category_backup_restore_title">Рэзервовае капіяванне і аднаўленне</string>
@ -820,4 +816,7 @@
<string name="select_a_feed_group">Выберыце групу каналаў</string>
<string name="no_feed_group_created_yet">Група каналаў яшчэ не створана</string>
<string name="feed_group_page_summary">Старонка групы каналаў</string>
<string name="search_with_service_name">Пошук %1$s</string>
<string name="search_with_service_name_and_filter">Пошук %1$s (%2$s)</string>
<string name="channel_tab_likes">Спадабалася</string>
</resources>

View File

@ -45,9 +45,6 @@
</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>

View File

@ -183,9 +183,6 @@
<string name="file_name_empty_error">Името на файла не може да бъде празно</string>
<string name="error_occurred_detail">Възникна грешка: %1$s</string>
<string name="no_streams_available_download">Не са налични източници за изтегляне</string>
<string name="short_thousand">хил.</string>
<string name="short_million">млн.</string>
<string name="short_billion">млрд.</string>
<string name="no_subscribers">Няма абонати</string>
<string name="create">Създай</string>
<string name="dismiss">Откажи</string>
@ -229,7 +226,7 @@
<string name="main_page_content">Съдържание на главната страница</string>
<string name="blank_page_summary">Празна страница</string>
<string name="kiosk_page_summary">Страница-павилион</string>
<string name="channel_page_summary">Страница на определен канал</string>
<string name="channel_page_summary">Страница на канал</string>
<string name="select_a_channel">Изберете канал</string>
<string name="no_channel_subscribed_yet">За момента нямате абонаменти</string>
<string name="select_a_kiosk">Изберете павилион</string>
@ -419,7 +416,6 @@
<string name="playlist_page_summary">Страница на плейлиста</string>
<string name="chapters">Глави</string>
<string name="metadata_licence">Лиценз</string>
<string name="service_provides_reason">%s посочва следната причина:</string>
<string name="metadata_tags">Маркери</string>
<string name="metadata_privacy">Поверителност</string>
<string name="metadata_language">Език</string>
@ -816,4 +812,6 @@
<string name="search_with_service_name">Търсене %1$s</string>
<string name="search_with_service_name_and_filter">Търсене %1$s (%2$s)</string>
<string name="channel_tab_likes">Харесвания</string>
<string name="migration_info_6_7_title">Страница SoundCloud Top 50 е премахната</string>
<string name="migration_info_6_7_message">SoundCloud преустанови оригиналните класации Топ 50. Съответният раздел е премахнат от главната ви страница.</string>
</resources>

View File

@ -82,9 +82,6 @@
<string name="video">ভিডিও</string>
<string name="audio">অডিও</string>
<string name="retry">পুনরায় চেষ্টা করো</string>
<string name="short_thousand">হা</string>
<string name="short_million">M</string>
<string name="short_billion">বি</string>
<!-- Missions -->
<string name="start">শুরু</string>
<string name="pause">বিরতি</string>

View File

@ -104,9 +104,6 @@
<string name="no_videos">কোন ভিডিও নেই</string>
<string name="no_views">কোন ভিউ নেই</string>
<string name="no_subscribers">কোন সাবস্ক্রাইবার নেই</string>
<string name="short_billion">B</string>
<string name="short_million">M</string>
<string name="short_thousand">K</string>
<string name="retry">পুনরায় চেষ্টা করো</string>
<string name="audio">অডিও</string>
<string name="video">ভিডিও</string>
@ -543,7 +540,6 @@
<string name="feed_load_error_account_info">\'%s\' এর জন্য ফিড প্রক্রিয়া করা যাচ্ছে না।</string>
<string name="description_select_disable">বর্ণনার লেখা নির্বাচন করা নিষ্ক্রিয় করো</string>
<string name="description_select_enable">বর্ণনার লেখা নির্বাচন করা সক্ষম করো</string>
<string name="service_provides_reason">%s এই কারণ বলছে:</string>
<string name="feed_load_error">প্রক্রিয়াকরণ ফিডে ত্রুটি</string>
<string name="open_website_license">ওয়েবসাইট খুলুন</string>
<string name="account_terminated">অ্যাকাউন্ট ধ্বংসকৃত</string>

View File

@ -231,9 +231,6 @@
<string name="player_recoverable_failure">S\'està recuperant el reproductor després de l\'error</string>
<string name="sorry_string">Bé, és lamentable.</string>
<string name="detail_drag_description">Arrossegueu per reordenar la llista</string>
<string name="short_thousand">mil</string>
<string name="short_million">milions</string>
<string name="short_billion">mil milions</string>
<string name="start">Inicia</string>
<string name="msg_running_detail">Feu un toc aquí per a més detalls</string>
<string name="no_available_dir">Defineix una carpeta de baixades més endavant als paràmetres</string>
@ -615,7 +612,6 @@
<string name="select_night_theme_toast">Pot seleccionar el seu tema fosc favorit aqui sota</string>
<string name="night_theme_summary">Selecciona el teu tema fosc favorit — %s</string>
<string name="auto_device_theme_title">Automàtic (tema del dispositiu)</string>
<string name="service_provides_reason">%s dóna aquesta raó:</string>
<string name="account_terminated">Usuari suspes</string>
<string name="feed_load_error_terminated">El compte de l\'autor ha estat esborrat.
\nNewPipe no serà capaç de carregar aquest fil en el futur.

View File

@ -25,7 +25,6 @@
<string name="subscribers_count_not_available">ژمارەی بەژداری نادیارە</string>
<string name="overwrite_failed">ناتوانرێت لەسەر ئەو فایله‌وه‌ جێگیر بکرێت</string>
<string name="tab_choose">په‌ڕه‌ هەڵبژێرە</string>
<string name="short_million">ملیۆن</string>
<string name="more_than_100_videos">+١٠٠ ڤیدیۆیان</string>
<string name="settings_category_player_title">لێده‌ر</string>
<string name="import_title">هاوردە</string>
@ -46,7 +45,6 @@
<string name="override_current_data">ئەمە لەسەر ڕێکخستنەکانی ئێستات جێگیر دەبێت.</string>
<string name="notification_channel_name">پەیامەکانی نیوپایپ</string>
<string name="donation_encouragement">نیوپایپ لەلایەن چەند خۆبەخشێکەوە دروستکراوە کە کاته‌كانی خۆیان پێ بەخشیووە تاکو باشترین خزمەتگوزاریت پێشکەش بکەن. هیچ نەبێت بە کڕینی کوپێک قاوە یارمەتی گەشەپێدەرەکانمان بدە بۆ ئەوەی کاتی زیاتر تەرخان بکەین بۆ بەرەوپێشبردنی نیوپایپ.</string>
<string name="short_billion">ملیار</string>
<string name="show_search_suggestions_title">گەڕانی پێشنیارکراوەکان</string>
<string name="playback_tempo">خێرا</string>
<string name="file_deleted">فایل سڕایەوە</string>
@ -373,7 +371,6 @@
<string name="download_failed">ناتوانرێت داببه‌زێنرێت</string>
<string name="error_connect_host">ناتوانرێت بە ڕاژەكه‌وە پەیوەست ببیت</string>
<string name="detail_thumbnail_view_description">لێدانی ڤیدیۆ، مه‌ودا:</string>
<string name="short_thousand">هەزار</string>
<string name="most_liked">زۆرترین بەدڵ</string>
<string name="delete">سڕینەوە</string>
<string name="default_video_format_title">جۆری بنەڕەتی ڤیدیۆ</string>
@ -599,7 +596,6 @@
<string name="radio">ڕادیۆ</string>
<string name="featured">تایبەتکراو</string>
<string name="paid_content">ئه‌م بابه‌ته‌ ته‌نیا بۆ ئه‌و كه‌سانه‌ به‌رده‌سته‌ كه‌ پاره‌یان داوه‌ ، بۆیه‌ ناتوانرێت له‌ نیوپایپه‌وه‌ داببه‌زێنرێت.</string>
<string name="service_provides_reason">%s ئه‌م هۆكاره‌ دابین ده‌كات:</string>
<string name="account_terminated">هه‌ژمار له‌ناوبراوه‌</string>
<string name="youtube_music_premium_content">ئه‌م ڤیدیۆیه‌ ته‌نیا له‌ وه‌شانی نایابی یوتوب میوزیك به‌رده‌سته‌ ، بۆیه‌ ناتوانرێت له‌ نیوپایپه‌وه‌ داببه‌زێنرێت.</string>
<string name="soundcloud_go_plus_content">ئه‌مه‌ تراكی SoundCloud Go+ ه ، لانی كه‌م له‌ وڵاته‌كه‌ی تۆدا، ناتوانرێت له‌لایه‌ن نیوپایپه‌وه‌ داببه‌زێنرێت.</string>

View File

@ -57,7 +57,7 @@
<string name="msg_name">Jméno souboru</string>
<string name="msg_threads">Vlákna</string>
<string name="pause">Zastavit</string>
<string name="delete">Smazat</string>
<string name="delete">Odstranit</string>
<string name="start">Start</string>
<string name="retry">Zkusit znovu</string>
<string name="video">Video</string>
@ -83,9 +83,7 @@
<string name="no_available_dir">Určete prosím složku pro stahování později v nastavení</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="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
\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>
@ -124,7 +122,6 @@
<string name="notification_channel_description">Oznámení pro NewPipe přehrávač</string>
<string name="search_no_results">Žádné výsledky</string>
<string name="empty_list_subtitle">Je tu sranda jak v márnici</string>
<string name="short_billion">mld.</string>
<string name="no_subscribers">Žádní odběratelé</string>
<plurals name="subscribers">
<item quantity="one">%s odběratel</item>
@ -234,8 +231,8 @@
<string name="add_to_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>
<string name="unbookmark_playlist">Smazat záložku</string>
<string name="delete_playlist_prompt">Smazat tento playlist\?</string>
<string name="unbookmark_playlist">Odstranit záložku</string>
<string name="delete_playlist_prompt">Odstranit tento playlist?</string>
<string name="playlist_creation_success">Playlist vytvořen</string>
<string name="playlist_add_stream_success">V playlistu</string>
<string name="playlist_thumbnail_change_success">Náhled playlistu změněn.</string>
@ -411,9 +408,9 @@
<string name="enable_playback_state_lists_summary">Zobrazit pozici přehrávání v seznamech</string>
<string name="watch_history_states_deleted">Pozice playbacku smazány</string>
<string name="error_timeout">Timeout spojení</string>
<string name="clear_playback_states_title">Smazat pozice playbacku</string>
<string name="clear_playback_states_summary">Smazat všechny pozice playbacku</string>
<string name="delete_playback_states_alert">Smazat všechny pozice playbacku\?</string>
<string name="clear_playback_states_title">Vymazat pozice přehrávání</string>
<string name="clear_playback_states_summary">Vymaže všechny pozice přehrávání</string>
<string name="delete_playback_states_alert">Vymazat všechny pozice přehrávání?</string>
<string name="drawer_header_description">Přepnout službu, právě vybráno:</string>
<string name="no_one_watching">Nikdo nesleduje</string>
<plurals name="watching">
@ -444,8 +441,8 @@
<string name="recovering">obnovuji</string>
<string name="error_download_resource_gone">Toto stahování nelze obnovit</string>
<string name="choose_instance_prompt">Vyberte instanci</string>
<string name="clear_download_history">Smazat historii stahování</string>
<string name="delete_downloaded_files">Smazat stažené soubory</string>
<string name="clear_download_history">Vymazat historii stahování</string>
<string name="delete_downloaded_files">Odstranit stažené soubory</string>
<string name="permission_display_over_apps">Souhlasit se zobrazením přes jiné aplikace</string>
<string name="app_language_title">Jazyk aplikace</string>
<string name="systems_language">Jazyk systému</string>
@ -488,7 +485,7 @@
<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_delete_message">Přejete si smazat tuto skupinu\?</string>
<string name="feed_group_dialog_delete_message">Přejete si odstranit tuto skupinu?</string>
<string name="feed_create_new_group_button_title">Nová</string>
<string name="settings_category_feed_title">Novinky</string>
<string name="feed_update_threshold_title">Limit aktualizace novinek</string>
@ -622,7 +619,6 @@
<string name="description_select_disable">Vypnout výběr textu v popisu</string>
<string name="description_select_enable">Zapnout výběr textu v popisu</string>
<string name="description_select_note">Nyní můžete vybrat v popisu text. Pamatujte, že v režimu výběru může stránka blikat a odkazy nemusí reagovat na kliknutí.</string>
<string name="service_provides_reason">%s udává teno důvod:</string>
<string name="account_terminated">Účet uzavřen</string>
<string name="feed_load_error_fast_unknown">Režim rychlého feedu o tom neposkytuje více informací.</string>
<string name="feed_load_error_terminated">Autorův účet byl uzavřen.
@ -687,7 +683,7 @@
<string name="streams_notifications_interval_title">Frekvence kontroly</string>
<string name="any_network">Jakákoli síť</string>
<string name="streams_notifications_network_title">Požadované síťové připojení</string>
<string name="delete_downloaded_files_confirm">Smazat všechny stažené soubory z disku\?</string>
<string name="delete_downloaded_files_confirm">Odstranit všechny stažené soubory z disku?</string>
<string name="you_successfully_subscribed">Objednali jste si nyní tento kanál</string>
<string name="toggle_all">Všechny přepnout</string>
<string name="streams_notification_channel_name">Nové streamy</string>
@ -844,4 +840,6 @@
<string name="search_with_service_name">Hledat %1$s</string>
<string name="search_with_service_name_and_filter">Hledat %1$s (%2$s)</string>
<string name="channel_tab_likes">Líbí se</string>
<string name="migration_info_6_7_title">Stránka SoundCloud Top 50 odstraněna</string>
<string name="migration_info_6_7_message">SoundCloud zrušil původní žebříčky Top 50. Příslušná karta byla odstraněna z vaší hlavní stránky.</string>
</resources>

View File

@ -305,9 +305,6 @@
<string name="stop">Stop</string>
<string name="events">Hændelser</string>
<string name="empty_list_subtitle">Ikke andet end fårekyllinger her</string>
<string name="short_thousand">t</string>
<string name="short_million">mio.</string>
<string name="short_billion">mia.</string>
<plurals name="subscribers">
<item quantity="one">%s abonnent</item>
<item quantity="other">%s abonnenter</item>
@ -556,7 +553,6 @@
<string name="private_content">Dette indhold er privat, så det kan ikke streames eller hentes af NewPipe.</string>
<string name="recently_added">Nyligt tilføjede</string>
<string name="featured">Fremhævede</string>
<string name="service_provides_reason">%s giver denne grund:</string>
<plurals name="listening">
<item quantity="one">%s lytter</item>
<item quantity="other">%s lyttere</item>

View File

@ -69,9 +69,6 @@
<string name="error_report_title">Fehlerbericht</string>
<string name="delete">Löschen</string>
<string name="checksum">Prüfsumme</string>
<string name="short_thousand">Tsd.</string>
<string name="short_million">Mio.</string>
<string name="short_billion">Mrd.</string>
<string name="msg_name">Dateiname</string>
<string name="msg_error">Fehler</string>
<string name="msg_wait">Bitte warten </string>
@ -630,7 +627,6 @@
<string name="downloads_storage_ask_summary_no_saf_notice">Du wirst jedes Mal gefragt werden, wohin der Download gespeichert werden soll</string>
<string name="feed_load_error">Fehler beim Laden des Feeds</string>
<string name="feed_load_error_account_info">Konnte Feed für \'%s\' nicht laden.</string>
<string name="service_provides_reason">%s gibt diesen Grund an:</string>
<string name="on">An</string>
<string name="tablet_mode_title">Tablet-Modus</string>
<string name="off">Aus</string>
@ -827,4 +823,9 @@
<string name="select_a_feed_group">Eine Feed-Gruppe auswählen</string>
<string name="feed_group_page_summary">Kanalgruppen-Seite</string>
<string name="no_feed_group_created_yet">Es wurde noch keine Feed-Gruppe erstellt</string>
<string name="search_with_service_name">Suche %1$s</string>
<string name="search_with_service_name_and_filter">Suche %1$s (%2$s)</string>
<string name="channel_tab_likes">Gefällt mir</string>
<string name="migration_info_6_7_title">SoundCloud-Top-50-Seite entfernt</string>
<string name="migration_info_6_7_message">SoundCloud hat die ursprünglichen Top-50-Charts abgeschafft. Der entsprechende Tab wurde von deiner Hauptseite entfernt.</string>
</resources>

View File

@ -41,7 +41,6 @@
<string name="detail_uploader_thumbnail_view_description">Μικρογραφία εικόνας προφίλ του χρήστη</string>
<string name="detail_likes_img_view_description">Like</string>
<string name="detail_dislikes_img_view_description">Dislike</string>
<string name="short_billion">δισ/ρια</string>
<string name="open_in_popup_mode">Άνοιγμα σε αναδυόμενο παράθυρο</string>
<string name="subscribe_button_title">Εγγραφή</string>
<string name="subscribed_button_title">Εγγεγραμμένος</string>
@ -169,8 +168,6 @@
<string name="empty_list_subtitle">Δεν υπάρχει τίποτα εδώ</string>
<string name="detail_drag_description">Σύρετε για ταξινόμηση</string>
<string name="retry">Προσπάθεια εκ νέου</string>
<string name="short_thousand">χιλ.</string>
<string name="short_million">εκ/ρια</string>
<string name="no_subscribers">Κανένας συνδρομητής</string>
<plurals name="subscribers">
<item quantity="one">%s συνδρομητής</item>
@ -611,7 +608,6 @@
<string name="description_select_enable">Ενεργοποίηση επιλογής κειμένου στην περιγραφή</string>
<string name="description_select_note">Τώρα μπορείτε να επιλέξετε κείμενο εντός της περιγραφής. Σημειώστε ότι, η σελίδα μπορεί να παρουσιάζει αστάθεια κατά τη διάρκεια της κατάστασης επιλογής κειμένου.</string>
<string name="open_website_license">Ανοικτή ιστοσελίδα</string>
<string name="service_provides_reason">Το %s παρέχει αυτή την αιτία:</string>
<string name="account_terminated">Ο λογαριασμός διαγράφηκε</string>
<string name="feed_load_error_fast_unknown">Η κατάσταση γρήγορης τροφοδοσίας δεν παρέχει περισσότερες πληροφορίες.</string>
<string name="feed_load_error_terminated">Ο λογαριασμός του δημιουργού έχει διαγραφεί.
@ -827,4 +823,9 @@
<string name="select_a_feed_group">Επιλογή ομάδας τροφοδοσίας</string>
<string name="no_feed_group_created_yet">Δεν δημιουργήθηκε ομάδα τροφοδοσίας ακόμα</string>
<string name="feed_group_page_summary">Σελίδα καναλιού ομάδας</string>
<string name="search_with_service_name">Αναζήτηση %1$s</string>
<string name="search_with_service_name_and_filter">Αναζήτηση %1$s (%2$s)</string>
<string name="channel_tab_likes">Likes</string>
<string name="migration_info_6_7_title">Η σελίδα των SoundCloud Top 50 αφαιρέθηκε</string>
<string name="migration_info_6_7_message">Το SoundCloud έχει καταργήσει τα αρχικά charts με τα Top 50. Η αντίστοιχη καρτέλα έχει αφαιρεθεί από την κύρια σελίδα σας.</string>
</resources>

View File

@ -261,9 +261,6 @@
<item quantity="one">%s spekto</item>
<item quantity="other">%s spektoj</item>
</plurals>
<string name="short_thousand">k</string>
<string name="short_million">M</string>
<string name="short_billion">Mrd</string>
<string name="title_activity_about">Pri NewPipe</string>
<string name="title_licenses">Eksteraj permesiloj</string>
<string name="copyright" formatted="true">© %1$s de %2$s sub %3$s</string>
@ -507,7 +504,6 @@
<string name="on">Ŝaltita</string>
<string name="metadata_tags">Etikedoj</string>
<string name="download_has_started">Elŝutado komenciĝis</string>
<string name="service_provides_reason">%s donas tiun kialon:</string>
<string name="georestricted_content">Tiu enaĵo ne disponeblas en via lando.</string>
<string name="recent">Freŝaj</string>
<string name="video_detail_by">De %s</string>

View File

@ -81,9 +81,6 @@
<string name="info_labels">Qué:\\nSolicitud:\\nIdioma del contenido:\\nPaís del contenido:\\nIdioma de la aplicación:\\nServicio:\\nMarca de tiempo:\\nPaquete:\\nVersión:\\nVersión del SO:</string>
<string name="black_theme_title">Negro</string>
<string name="all">Todo</string>
<string name="short_thousand">k</string>
<string name="short_million">M</string>
<string name="short_billion">MM</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>
@ -614,7 +611,6 @@
<string name="description_select_disable">Deshabilitar la selección de texto de la descripción</string>
<string name="description_select_enable">Habilitar la selección de texto de la descripción</string>
<string name="description_select_note">Ahora puede seleccionar el texto dentro de la descripción. Note que la página puede parpadear y los links no serán cliqueables mientras está en el modo de selección.</string>
<string name="service_provides_reason">%s da esta razón:</string>
<string name="feed_load_error_account_info">No fue posible cargar el feed por \'%s\'.</string>
<string name="account_terminated">Cuenta cancelada</string>
<string name="feed_load_error_fast_unknown">El modo de muro rápido no arroja más información sobre esto.</string>

View File

@ -154,9 +154,6 @@
<string name="video">Video</string>
<string name="audio">Audio</string>
<string name="retry">Proovi uuesti</string>
<string name="short_thousand">tuh</string>
<string name="short_million">mln</string>
<string name="short_billion">mld</string>
<string name="no_subscribers">Tellijaid pole</string>
<plurals name="subscribers">
<item quantity="one">%s tellija</item>
@ -217,7 +214,7 @@
<string name="title_most_played">Enim esitatud</string>
<string name="main_page_content">Avalehe sisu</string>
<string name="blank_page_summary">Tühi leht</string>
<string name="kiosk_page_summary">Kioski leht</string>
<string name="kiosk_page_summary">Kioskivaade</string>
<string name="channel_page_summary">Kanali leht</string>
<string name="select_a_channel">Vali kanal</string>
<string name="no_channel_subscribed_yet">Kanaleid pole veel tellitud</string>
@ -565,7 +562,6 @@
<string name="show_thumbnail_title">Näita pisipilte</string>
<string name="show_thumbnail_summary">Kasuta pisipilti nii lukustusvaate kui teavituste taustana</string>
<string name="account_terminated">Kasutajakonto on suletud</string>
<string name="service_provides_reason">%s toob põhjuseks:</string>
<string name="description_select_enable">Võimalda valida kirjelduse teksti</string>
<string name="description_select_disable">Ära võimalda valida kirjelduse teksti</string>
<string name="metadata_category">Kategooria</string>
@ -815,4 +811,6 @@
<string name="search_with_service_name">Otsi: %1$s</string>
<string name="search_with_service_name_and_filter">Otsi: %1$s (%2$s)</string>
<string name="channel_tab_likes">Meeldimisi</string>
<string name="migration_info_6_7_title">SoundCloudi „Top 50“ leht on eemaldatud</string>
<string name="migration_info_6_7_message">SoundCloud on lõpetanud oma algse „Top 50“ edetabeli pidamise. Seega on ka vastav vahekaart meie rakenduse põhivaatest eemaldatud.</string>
</resources>

View File

@ -84,9 +84,6 @@
<string name="video">Bideoa</string>
<string name="audio">Audioa</string>
<string name="retry">Saiatu berriro</string>
<string name="short_thousand">k</string>
<string name="short_million">M</string>
<string name="short_billion">MM</string>
<string name="start">Hasi</string>
<string name="pause">Pausatu</string>
<string name="delete">Ezabatu</string>
@ -618,7 +615,6 @@
<string name="downloads_storage_ask_summary_no_saf_notice">Non gorde galdetuko zaizu deskarga bakoitzean</string>
<string name="no_dir_yet">Ez da deskargatzeko karpetarik ezarri oraindik, aukeratu lehenetsitako deskargatzeko karpeta orain</string>
<string name="metadata_privacy">Pribatutasuna</string>
<string name="service_provides_reason">%s arrazoi hau ematen du:</string>
<string name="account_terminated">Kontua ezabatu da</string>
<string name="feed_load_error_fast_unknown">Jario azkarrak ez du honi buruz informazio gehiagorik ematen.</string>
<string name="metadata_age_limit">Adin muga</string>

View File

@ -148,9 +148,6 @@
<string name="error_occurred_detail">خطایی رخ داد: %1$s</string>
<string name="no_streams_available_download">جریانی برای بارگیری در دسترس نیست</string>
<string name="search_no_results">بدون نتیجه</string>
<string name="short_thousand">K</string>
<string name="short_million">M</string>
<string name="short_billion">B</string>
<plurals name="subscribers">
<item quantity="one">%s مشترک</item>
<item quantity="other">%s مشترک</item>
@ -628,7 +625,6 @@
\nنیوپایپ قادر به بار کردن این خوراک در آینده نیست.
\nمیخواهید اشتراک این کانال را لغو کنید؟</string>
<string name="feed_load_error_fast_unknown">حالت خوراک سریع، اطَلاعات بیش‌تری در این باره نمی‌دهد.</string>
<string name="service_provides_reason">%s این دلیل را آورد:</string>
<string name="seekbar_preview_thumbnail_title">پیش‌نمایش بندانگشتی نوار جویش</string>
<string name="detail_heart_img_view_description">قلب‌شده به دست ایجادگر</string>
<string name="local_search_suggestions">پیشنهادهای جست‌وجوی محلّی</string>

View File

@ -102,9 +102,6 @@
<string name="video">Video</string>
<string name="audio">Ääni</string>
<string name="retry">Toista uudelleen</string>
<string name="short_thousand">t.</string>
<string name="short_million">milj.</string>
<string name="short_billion">bilj.</string>
<string name="no_subscribers">Ei tilaajia</string>
<plurals name="subscribers">
<item quantity="one">%s tilaaja</item>
@ -595,7 +592,6 @@
<string name="night_theme_title">Yöteema</string>
<string name="description_select_disable">Poista käytöstä tekstinvalinta kuvauskentän sisältä</string>
<string name="description_select_note">Voit nyt valita tekstin kuvauskentän sisältä. Huomioithan, että valintatilan aikana sivu voi vilkkua ja linkit eivät ehkä ole klikattavia.</string>
<string name="service_provides_reason">%s tuo tämän syyn:</string>
<string name="seekbar_preview_thumbnail_title">Säätövivun kuvakkeen esikatselu</string>
<string name="disable_media_tunneling_summary">Poista median tunnelointi käytöstä, jos havaitset mustan näyttöruudun tai änkytystä videon toistossa.</string>
<string name="disable_media_tunneling_title">Poista median tunnelointi käytöstä</string>

View File

@ -87,8 +87,6 @@
<string name="popup_playing_toast">Lecture en mode flottant</string>
<string name="disabled">Désactivés</string>
<string name="info_labels">Quoi:\\nRequest:\\nContent Language:\\nContent Country:\\nApp Language:\\nService:\\nGMT Time:\\nPackage:\\nVersion:\\nOS version:</string>
<string name="short_thousand">k</string>
<string name="short_million">M</string>
<string name="msg_popup_permission">Cette autorisation est nécessaire pour
\nutiliser le mode flottant</string>
<string name="controls_background_title">Arrière-plan</string>
@ -100,7 +98,6 @@
<string name="popup_remember_size_pos_title">Mémoriser les propriétés de la fenêtre flottante</string>
<string name="popup_remember_size_pos_summary">Mémoriser les dernières taille et position de la fenêtre flottante</string>
<string name="clear">Effacer</string>
<string name="short_billion">G</string>
<string name="use_external_video_player_summary">Le son peut être absent à certaines définitions</string>
<string name="show_search_suggestions_title">Suggestions de recherche</string>
<string name="show_search_suggestions_summary">Sélectionner les suggestions à afficher lors dune recherche</string>
@ -164,7 +161,7 @@
<string name="charset_most_special_characters">Caractères spéciaux</string>
<string name="delete_item_search_history">Voulez-vous supprimer cet élément de lhistorique de recherche\?</string>
<string name="main_page_content">Contenu de la page principale</string>
<string name="blank_page_summary">Page vide</string>
<string name="blank_page_summary">Page blanche</string>
<string name="channel_page_summary">Chaîne</string>
<string name="select_a_channel">Sélectionner une chaîne</string>
<string name="trending">Tendances</string>
@ -623,7 +620,6 @@
<string name="metadata_tags">Étiquettes</string>
<string name="metadata_category">Catégorie</string>
<string name="description_select_note">Vous pouvez maintenant sélectionner du texte à lintérieur de la description. Notez que la page peut scintiller et que les liens peuvent ne pas être cliquables en mode sélection.</string>
<string name="service_provides_reason">%s indique le motif :</string>
<string name="no_dir_yet">Aucun dossier de téléchargement nest défini pour le moment, sélectionnez le dossier de téléchargement par défaut</string>
<string name="open_website_license">Ouvrir le site web</string>
<string name="account_terminated">Compte résilié</string>
@ -846,4 +842,6 @@
<string name="search_with_service_name">Rechercher %1$s</string>
<string name="search_with_service_name_and_filter">Rechercher %1$s (%2$s)</string>
<string name="channel_tab_likes">Likes</string>
<string name="migration_info_6_7_title">Page SoundCloud Top 50 supprimée</string>
<string name="migration_info_6_7_message">SoundCloud a abandonné le classement original du Top 50. L\'onglet correspondant a été supprimé de votre page d\'accueil.</string>
</resources>

View File

@ -158,9 +158,6 @@
<string name="video">Vídeo</string>
<string name="audio">Audio</string>
<string name="retry">Tentar de novo</string>
<string name="short_thousand">k</string>
<string name="short_million">M</string>
<string name="short_billion">B</string>
<string name="no_subscribers">Ningún subscrito</string>
<plurals name="subscribers">
<item quantity="one">%s subscrito</item>
@ -557,7 +554,6 @@
<string name="description_select_note">Agora pode seleccionar o texto na descrición. Teña en conta que a páxina pode cintilar e as ligazóns poden non ser clicábeis no modo selección.</string>
<string name="auto_device_theme_title">Automático (Tema do dispositivo)</string>
<string name="radio">Radio</string>
<string name="service_provides_reason">%s dá este motivo:</string>
<string name="georestricted_content">Este contido non está dispoñíbel no seu país.</string>
<string name="chapters">Capítulos</string>
<string name="recent">Recentes</string>

View File

@ -112,9 +112,6 @@
<string name="video">סרטון</string>
<string name="audio">שמע</string>
<string name="retry">ניסיון חוזר</string>
<string name="short_thousand">אלפ.</string>
<string name="short_million">מיל.</string>
<string name="short_billion">מיליארד</string>
<string name="no_subscribers">אין מנויים</string>
<plurals name="subscribers">
<item quantity="one">מנוי אחד</item>
@ -179,9 +176,9 @@
<string name="action_history">היסטוריה</string>
<string name="delete_item_search_history">למחוק את הפריט הזה מהיסטוריית החיפושים\?</string>
<string name="main_page_content">תוכן הדף הראשי</string>
<string name="blank_page_summary">דף ריק</string>
<string name="kiosk_page_summary">דף גישה מזדמנת</string>
<string name="channel_page_summary">דף ערוצים</string>
<string name="blank_page_summary">עמוד ריק</string>
<string name="kiosk_page_summary">עמוד גישה מזדמנת</string>
<string name="channel_page_summary">עמוד הערוץ</string>
<string name="select_a_channel">נא לבחור ערוץ</string>
<string name="no_channel_subscribed_yet">אין עדיין מינויים לערוצים</string>
<string name="select_a_kiosk">נא לבחור סוג גישה מזדמנת</string>
@ -635,7 +632,6 @@
\nל־NewPipe לא תהיה אפשרות להוריד את ההזנה הזאת בעתיד.
\nלהסיר את המינוי מהערוץ הזה\?</string>
<string name="open_website_license">פתיחת האתר</string>
<string name="service_provides_reason">%s מספק את הסיבה הבאה:</string>
<string name="account_terminated">החשבון הושמד</string>
<string name="feed_load_error_fast_unknown">מצב ההזנה המהירה לא מספק מידע נוסף על כך.</string>
<string name="feed_load_error_account_info">לא ניתן לטעון את ההזנה עבור %s.</string>

View File

@ -126,9 +126,6 @@
<string name="video">वीडियो</string>
<string name="audio">ऑडियो</string>
<string name="retry">फिर से कोशिश करें</string>
<string name="short_thousand">हज़ार</string>
<string name="short_million">मिलियन</string>
<string name="short_billion">अरब</string>
<string name="no_subscribers">कोई सब्सक्राइबर नहीं</string>
<plurals name="subscribers">
<item quantity="one">%s सब्सक्राइबर</item>
@ -657,7 +654,6 @@
<string name="disable_media_tunneling_title">मीडिया टनलिंग अक्षम करें</string>
<string name="show_crash_the_player_title">\"क्रैश द प्लेयर\" दिखाएं</string>
<string name="feed_subscription_not_loaded_count">लोड नहीं हुआ: %d</string>
<string name="service_provides_reason">%s इसका कारण प्रदान करता है:</string>
<string name="metadata_tags">टैग</string>
<string name="metadata_licence">लाइसेंस</string>
<string name="faq_description">यदि आपको ऐप का उपयोग करने में परेशानी हो रही है, तो सामान्य प्रश्नों के इन उत्तरों को देखना सुनिश्चित करें!</string>
@ -822,4 +818,12 @@
<string name="settings_category_backup_restore_title">बैकअप और रिस्टोर</string>
<string name="import_settings_vulnerable_format">आयात किए जा रहे निर्यात में सेटिंग्स एक कमजोर प्रारूप का उपयोग करती हैं जिसे न्यूपाइप 0.27.0 के बाद से हटा दिया गया था। सुनिश्चित करें कि आयात किया जा रहा निर्यात किसी विश्वसनीय स्रोत से है, और भविष्य में केवल न्यूपाइप 0.27.0 या नए से प्राप्त निर्यात का उपयोग करना पसंद करें। इस असुरक्षित प्रारूप में सेटिंग्स आयात करने के लिए समर्थन जल्द ही पूरी तरह से हटा दिया जाएगा, और फिर न्यूपाइप के पुराने संस्करण अब नए संस्करणों से निर्यात की सेटिंग्स आयात नहीं कर पाएंगे।</string>
<string name="audio_track_type_secondary">सेकेंडरी</string>
<string name="search_with_service_name">%1$s खोजें</string>
<string name="search_with_service_name_and_filter">%1$s (%2$s) खोजें</string>
<string name="tab_bookmarks_short">प्लेलिस्ट</string>
<string name="select_a_feed_group">कृपया एक फ़ीड समूह चुनें</string>
<string name="no_feed_group_created_yet">अभी तक कोई फ़ीड समूह नहीं बनाया गया है</string>
<string name="feed_group_page_summary">चैनल समूह पेज</string>
<string name="channel_tab_likes">पसंद</string>
<string name="share_playlist_as_youtube_temporary_playlist">यूट्यूब अस्थायी प्लेलिस्ट के रूप में साझा करें</string>
</resources>

View File

@ -97,9 +97,6 @@
<string name="video">Video</string>
<string name="audio">Audio</string>
<string name="retry">Pokušaj ponovo</string>
<string name="short_thousand">tis.</string>
<string name="short_million">mil</string>
<string name="short_billion">mlrd.</string>
<string name="start">Počni</string>
<string name="pause">Pauziraj</string>
<string name="delete">Izbriši</string>
@ -642,7 +639,6 @@
<string name="metadata_privacy_internal">Interno</string>
<string name="metadata_privacy">Privatnost</string>
<string name="description_select_note">Sada možeš odabrati tekst u opisu. Napomena: stranica će možda treperiti i možda nećeš moći kliknuti poveznice u načinu rada za odabir teksta.</string>
<string name="service_provides_reason">%s pruža ovaj razlog:</string>
<string name="processing_may_take_a_moment">Obrada u tijeku … Može malo potrajati</string>
<string name="main_page_content_swipe_remove">Za ukljanjanje stavki povuci ih</string>
<plurals name="download_finished_notification">

View File

@ -166,9 +166,6 @@
<string name="no_streams_available_download">Nincs letölthető adatfolyam</string>
<string name="empty_list_subtitle">Nincs itt semmi pár tücskön kívül</string>
<string name="detail_drag_description">Húzza az átrendezéshez</string>
<string name="short_thousand">e</string>
<string name="short_million">m</string>
<string name="short_billion">M</string>
<string name="no_subscribers">Nincs feliratkozó</string>
<plurals name="subscribers">
<item quantity="one">%s feliratkozó</item>
@ -540,7 +537,6 @@
<string name="related_items_tab_description">Kapcsolódó elemek</string>
<string name="error_report_open_github_notice">Ellenőrizze, hogy létezik-e már olyan jegy, amely az összeomlásával foglalkozik. Ha duplikált jegyet ad fel, akkor olyan időt vesz el tőlünk, amelyet a hiba javítására tudnánk fordítani.</string>
<string name="minimize_on_exit_title">Minimalizálás alkalmazásváltáskor</string>
<string name="service_provides_reason">A(z) %s ezt az okot adta meg:</string>
<string name="local_search_suggestions">Helyi keresési javaslatok</string>
<string name="remote_search_suggestions">Távoli keresési javaslatok</string>
<string name="start_main_player_fullscreen_title">A fő lejátszó teljes képernyős indítása</string>
@ -802,4 +798,6 @@
<string name="search_with_service_name">Keresés %1$s</string>
<string name="search_with_service_name_and_filter">Keresés %1$s (%2$s)</string>
<string name="channel_tab_likes">Kedvelések</string>
<string name="migration_info_6_7_title">SoundCloud Top 50 oldal eltávolítva</string>
<string name="migration_info_6_7_message">A SoundCloud megszüntette az eredeti Top 50-es listákat. A megfelelő lap el lett távolítva a főoldalról.</string>
</resources>

View File

@ -82,9 +82,6 @@
<string name="recaptcha_request_toast">Meminta kode reCAPTCHA</string>
<string name="black_theme_title">Hitam</string>
<string name="all">Semua</string>
<string name="short_thousand">r</string>
<string name="short_million">J</string>
<string name="short_billion">T</string>
<string name="open_in_popup_mode">Buka pada mode sembulan</string>
<string name="msg_popup_permission">Izin ini dibutuhkan untuk
\nmembuka di mode sembul</string>
@ -193,8 +190,8 @@
<string name="title_last_played">Terakhir Diputar</string>
<string name="title_most_played">Sering Diputar</string>
<string name="main_page_content">Konten halaman utama</string>
<string name="blank_page_summary">Halaman Kosong</string>
<string name="kiosk_page_summary">Halaman Kedai</string>
<string name="blank_page_summary">Halaman kosong</string>
<string name="kiosk_page_summary">Halaman kiosk</string>
<string name="channel_page_summary">Halaman saluran</string>
<string name="select_a_channel">Pilih saluran</string>
<string name="no_channel_subscribed_yet">Belum ada saluran langganan</string>
@ -602,7 +599,6 @@
<string name="description_select_enable">Aktifkan dapat memilih teks pada deskripsi</string>
<string name="description_select_note">Anda sekarang dapat memilih teks di dalam deskripsi. Perhatikan bahwa halaman mungkin berkedip dan tautan tidak dapat diklik saat dalam mode pemilihan.</string>
<string name="open_website_license">Buka situs web</string>
<string name="service_provides_reason">%s menyediakan alasan ini:</string>
<string name="account_terminated">Akun dinonaktifkan</string>
<string name="feed_load_error_fast_unknown">Mode langganan cepat tidak menyediakan lebih banyak info tentang ini.</string>
<string name="feed_load_error_terminated">Akun kreator telah dinonaktifkan.
@ -813,4 +809,9 @@
<string name="feed_group_page_summary">Halaman grup saluran</string>
<string name="no_feed_group_created_yet">Belum ada grup umpan yang dibuat</string>
<string name="select_a_feed_group">Pilih grup umpan</string>
<string name="search_with_service_name">Cari %1$s</string>
<string name="search_with_service_name_and_filter">Cari %1$s (%2$s)</string>
<string name="channel_tab_likes">Suka</string>
<string name="migration_info_6_7_title">Halaman Top 50 SoundCloud dihapus</string>
<string name="migration_info_6_7_message">SoundCloud telah menghentikan dukungan tangga lagu Top 50. Tab terkait telah dihapus dari halaman utama Anda.</string>
</resources>

View File

@ -97,7 +97,6 @@
<string name="play_queue_audio_settings">Hljóðstillingar</string>
<string name="start_here_on_background">Spila í bakgrunni</string>
<string name="preferred_open_action_settings_summary">Þegar hlekkur er opnaður — %s</string>
<string name="short_thousand">þús.</string>
<string name="detail_dislikes_img_view_description">Líkar ekki við</string>
<string name="retry">Reyna aftur</string>
<string name="description_tab_description">Lýsing</string>
@ -217,7 +216,6 @@
<string name="your_comment">Athugasemd þín (á ensku):</string>
<string name="search_no_results">Engar niðurstöður</string>
<string name="video">Myndskeið</string>
<string name="short_billion">ma.</string>
<string name="no_views">Engin áhorf</string>
<plurals name="views">
<item quantity="one">%s áhorf</item>
@ -352,7 +350,6 @@
<string name="metadata_category">Flokkur</string>
<string name="metadata_tags">Merki</string>
<string name="donation_encouragement">NewPipe er þróað af sjálfboðaliðum sem eyða frítíma sínum í að færa þér bestu notendaupplifunina. Gefðu til baka til að hjálpa forriturum að gera NewPipe enn betri á meðan þeir njóta kaffibolla.</string>
<string name="short_million">millj.</string>
<string name="show_description_summary">Slökktu á til að fela lýsingu og viðbótarupplýsingar myndskeiðs</string>
<string name="error_occurred_detail">Villa kom upp: %1$s</string>
<string name="title_activity_recaptcha">Þraut reCAPTCHA</string>
@ -576,7 +573,6 @@
<string name="no_appropriate_file_manager_message_android_10">Enginn viðeigandi skráarstjóri fannst fyrir þessa aðgerð.
\nVinsamlegast settu upp skráarstjóra sem styður Geymsluaðgangsramma (SAF)</string>
<string name="georestricted_content">Þetta efni er ekki fáanlegt í þínu landi.</string>
<string name="service_provides_reason">%s gefur þessa ástæðu:</string>
<string name="paid_content">Þetta efni er aðeins í boði fyrir notendur sem hafa greitt — það er ekki hægt að streyma því eða sækja með NewPipe.</string>
<string name="auto_device_theme_title">Sjálfvirk (þema tækis)</string>
<string name="night_theme_summary">Veldu uppáhalds næturþemu þína — %s</string>
@ -803,4 +799,12 @@
<string name="audio_track_type_secondary">auka</string>
<string name="share_playlist_as_youtube_temporary_playlist">Deila sem YouTube-bráðabirgðaspilunarlista</string>
<string name="tab_bookmarks_short">Spilunarlistar</string>
<string name="search_with_service_name">Leita í %1$s</string>
<string name="search_with_service_name_and_filter">Leita í %1$s (%2$s)</string>
<string name="select_a_feed_group">Veldu hóp streyma</string>
<string name="no_feed_group_created_yet">Enginn hópur streyma útbúinn ennþá</string>
<string name="feed_group_page_summary">Síða rásahóps</string>
<string name="channel_tab_likes">Líkar við</string>
<string name="migration_info_6_7_title">Topp 50 síða SoundCloud fjarlægð</string>
<string name="migration_info_6_7_message">SoundCloud er hætt með Topp 50 vinsældalistann. Viðkomandi flipi hefur verið fjarlægður af aðalsíðunni þinni.</string>
</resources>

View File

@ -82,9 +82,6 @@
<string name="title_activity_recaptcha">Risoluzione reCAPTCHA</string>
<string name="black_theme_title">Nero</string>
<string name="all">Tutto</string>
<string name="short_thousand">k</string>
<string name="short_million">M</string>
<string name="short_billion">Mrd</string>
<string name="recaptcha_request_toast">È richiesta la risoluzione del reCAPTCHA</string>
<string name="open_in_popup_mode">Apri in modalità popup</string>
<string name="popup_playing_toast">Riproduzione in modalità popup</string>
@ -622,7 +619,6 @@
<string name="description_select_enable">Attiva la selezione del testo nella descrizione</string>
<string name="description_select_note">È possibile selezionare il testo all\'interno della descrizione. In modalità selezione la pagina potrebbe sfarfallare e i collegamenti potrebbero non essere cliccabili.</string>
<string name="open_website_license">Visita il sito</string>
<string name="service_provides_reason">%s fornisce questa motivazione:</string>
<string name="account_terminated">Account chiuso</string>
<string name="feed_load_error_fast_unknown">Il recupero veloce dei feed non fornisce ulteriori informazioni al riguardo.</string>
<string name="feed_load_error_terminated">L\'account dell\'autore è stato chiuso.
@ -844,4 +840,6 @@
<string name="search_with_service_name_and_filter">Cerca %1$s (%2$s)</string>
<string name="search_with_service_name">Cerca su %1$s</string>
<string name="channel_tab_likes">Mi piace</string>
<string name="migration_info_6_7_title">Pagina Top 50 di SoundCloud rimossa</string>
<string name="migration_info_6_7_message">SoundCloud ha dismesso i grafici Top 50 originali. La scheda relativa è stata rimossa dalla pagina principale.</string>
</resources>

View File

@ -83,9 +83,6 @@
<string name="recaptcha_request_toast">reCAPTCHA を要求しました</string>
<string name="black_theme_title">ブラック</string>
<string name="all">すべて</string>
<string name="short_thousand">k</string>
<string name="short_million">M</string>
<string name="short_billion">B</string>
<string name="open_in_popup_mode">ポップアップモードで開く</string>
<string name="msg_popup_permission">ポップアップモードで開くには
\n権限の許可が必要です</string>
@ -615,7 +612,6 @@
<string name="off">オフ</string>
<string name="on">オン</string>
<string name="tablet_mode_title">タブレットモード</string>
<string name="service_provides_reason">%s がこの理由を提示:</string>
<string name="dont_show">表示しない</string>
<string name="low_quality_smaller">低品質 (小)</string>
<string name="high_quality_larger">高品質 (大)</string>

View File

@ -237,9 +237,6 @@
<string name="video">ვიდეო</string>
<string name="audio">აუდიო</string>
<string name="retry">ხელახლა სცადეთ</string>
<string name="short_thousand">ათასი</string>
<string name="short_million">მლნ</string>
<string name="short_billion">ბლნ</string>
<string name="drawer_header_description">სერვისის გადართვა, ამჟამად არჩეულია:</string>
<string name="no_subscribers">გამოწერები არ არის</string>
<plurals name="subscribers">
@ -547,7 +544,6 @@
<string name="georestricted_content">ეს ხელმიუწვდომელია თქვენი ქვეყნიდან.</string>
<string name="private_content">ეს მასალა პირადულია, ამიტომაც NewPipe-ს მისი არც მთლიანად და არც თანდათანობით ჩამოწერა არ შეუძლია.</string>
<string name="account_terminated">ანგარიში შეწყვეტილია</string>
<string name="service_provides_reason">%s იძლევა ამ მიზეზს:</string>
<string name="paid_content">ეს მასალა ხელმისაწვდომია მხოლოდ გადამხდელებისთვის, ამიტომაც NewPipe-ს მისი არც მთლიანად და არც თანდათანობით ჩამოწერა არ შეუძლია.</string>
<string name="featured">გამორჩეული</string>
<string name="radio">რადიო</string>

View File

@ -82,7 +82,6 @@
<string name="download_dialog_title">Sider</string>
<string name="controls_popup_title">Asfaylu udhim</string>
<string name="dismiss">Ttu</string>
<string name="short_million">A</string>
<string name="import_title">Kter</string>
<string name="ok">Ih</string>
<string name="action_history">Amazray</string>
@ -128,7 +127,6 @@
<string name="rename">Snifel isem</string>
<string name="download_failed">Asider ur yeddi ara</string>
<string name="video">Tamwalit</string>
<string name="short_billion">o</string>
<string name="controls_background_title">Aɣawas n deffir</string>
<string name="title_activity_history">Amazray</string>
<string name="paused">yesteɛfay</string>

View File

@ -49,9 +49,6 @@
</plurals>
<string name="no_subscribers">Ne abone</string>
<string name="drawer_header_description">Karûbarê veguheztinê, niha hatî hilbijartin:</string>
<string name="short_billion">B</string>
<string name="short_million">M</string>
<string name="short_thousand">k</string>
<string name="retry">Dîsa biceribîne</string>
<string name="audio">Deng</string>
<string name="video">Vîdyo</string>

View File

@ -116,9 +116,6 @@
<string name="info_labels">무엇:\\n요청:\\n콘텐츠 언어:\\n콘텐츠 국가:\\n앱 언어:\\n서비스:\\nGMT 시간:\\n패키지:\\n버전:\\nOS 버전:</string>
<string name="search_no_results">결과 없음</string>
<string name="empty_list_subtitle">구독할 항목을 추가하세요</string>
<string name="short_thousand"></string>
<string name="short_million">백만</string>
<string name="short_billion">십억</string>
<string name="no_subscribers">구독자 없음</string>
<plurals name="subscribers">
<item quantity="other">구독자 %s명</item>
@ -646,7 +643,6 @@
<string name="chapters">챕터</string>
<string name="recent">최근</string>
<string name="account_terminated">계정이 해지됨</string>
<string name="service_provides_reason">%s은(는) 다음과 같은 이유를 제공:</string>
<string name="soundcloud_go_plus_content">이것은 적어도 귀하의 국가에서 SoundCloud Go+ 트랙이므로 NewPipe에서 스트리밍하거나 다운로드할 수 없습니다.</string>
<string name="auto_device_theme_title">자동 (장치 테마)</string>
<string name="detail_pinned_comment_view_description">고정된 댓글</string>

View File

@ -133,9 +133,6 @@
<string name="video">ڤیدیۆ</string>
<string name="audio">دەنگ</string>
<string name="retry">هەوڵدانەوە</string>
<string name="short_thousand">هەزار</string>
<string name="short_million">ملیۆن</string>
<string name="short_billion">بلیۆن</string>
<string name="no_subscribers">هیچ بەشداربوویەک نییە</string>
<plurals name="subscribers">
<item quantity="one">%s بەشداربوو</item>

View File

@ -139,9 +139,6 @@
<string name="player_recoverable_failure">Atstatoma po grotuvo klaidos</string>
<string name="search_no_results">Nėra rezultatų</string>
<string name="empty_list_subtitle">Čia nieko nėra išskyrus svirplius</string>
<string name="short_thousand">Tūkst.</string>
<string name="short_million">Mln.</string>
<string name="short_billion">Mlrd.</string>
<string name="no_subscribers">Nėra prenumeratorių</string>
<string name="no_views">Nėra peržiūrų</string>
<plurals name="views">
@ -626,7 +623,6 @@
<string name="description_select_enable">Įgalinti teksto pasirinkimą apraše</string>
<string name="description_select_disable">Neleisti pasirinkti teksto apraše</string>
<string name="description_select_note">Dabar apraše galite pasirinkti tekstą aprašyme. Atminkite, kad puslapis gali mirgėti, o nuorodos gali būti nespustelėjamos, kai veikia pasirinkimo režimas.</string>
<string name="service_provides_reason">%s pateikia šią priežastį:</string>
<string name="account_terminated">Paskyra anuliuota</string>
<string name="feed_load_error_fast_unknown">Greito srauto režimas nesuteikia daugiau informacijos apie tai.</string>
<string name="feed_load_error_terminated">Autoriaus paskyra anuliuota.

View File

@ -136,9 +136,6 @@
</plurals>
<string name="no_subscribers">Nav abonamentu</string>
<string name="drawer_header_description">Izvēlaties pakalpojumu, šobrīd izvēlēts:</string>
<string name="short_billion">B</string>
<string name="short_million">M</string>
<string name="short_thousand">k</string>
<string name="retry">Atkārtot</string>
<string name="audio">Audio</string>
<string name="video">Video</string>
@ -632,7 +629,6 @@
\nNewPipe turpmāk nevarēs ielādēt šo plūsmu.
\nVai vēlaties atteikties no šī kanāla abonēšanas\?</string>
<string name="feed_load_error_fast_unknown">Ātrās straumes režīms nesniedz vairāk informācijas par šo.</string>
<string name="service_provides_reason">%s dod šādu pamatojumu:</string>
<string name="description_select_disable">Izslēgt teksta atlasīšanu video aprakstā</string>
<string name="metadata_privacy_internal">Iekšeji</string>
<string name="detail_heart_img_view_description">Autors piekrīt</string>

View File

@ -146,9 +146,6 @@
<string name="video">Видео</string>
<string name="audio">Звук</string>
<string name="retry">Пробај повторно</string>
<string name="short_thousand">илјади</string>
<string name="short_million">M</string>
<string name="short_billion">милијарди</string>
<string name="no_subscribers">Нема зачленети</string>
<plurals name="subscribers">
<item quantity="one">%s зачленет</item>
@ -434,7 +431,6 @@
<string name="feed_load_error_account_info">Неуспешно вчитување на новинска лента за „%s“.</string>
<string name="feed_show_hide_streams">Прикажи / скриј стримови</string>
<string name="private_content">Оваа содржина е приватна, така што не може да биде емитувана или преземена од страна на NewPipe.</string>
<string name="service_provides_reason">%s ја посочува следната причина:</string>
<string name="featured">Истакнато</string>
<string name="radio">Радио</string>
<string name="auto_device_theme_title">Автоматски (режим на уредот)</string>
@ -721,4 +717,4 @@
<string name="right_gesture_control_summary">Изберете гестикулација за десната половина од екранот на плеерот</string>
<string name="start_main_player_fullscreen_summary">Видеата нема да започнат со емитување во миниплеерот, туку директно ќе се вклучат на цел екран, доколку автоматското ротирање е заклучено. Сѐ уште можете да добиете пристап до миниплеерот, кога ќе излезете од целиот екран</string>
<string name="unsupported_url_dialog_message">URL адресата не може да биде распознаена. Да се отвори со друга апликација?</string>
</resources>
</resources>

View File

@ -183,9 +183,6 @@
</plurals>
<string name="no_subscribers">സബ്ക്രൈബേഴ്സ് ഇല്ല</string>
<string name="drawer_header_description">സേവനം മാറ്റുക, ഇപ്പോൾ തിരഞ്ഞെടുത്തത്:</string>
<string name="short_billion">B</string>
<string name="short_thousand">k</string>
<string name="short_million">M</string>
<string name="retry">വീണ്ടും ശ്രമിക്കുക</string>
<string name="audio">ഓഡിയോ</string>
<string name="video">വീഡിയോ</string>
@ -615,7 +612,6 @@
<string name="metadata_tags">ടാഗുക്കൾ</string>
<string name="metadata_category">വിഭാഗം</string>
<string name="description_select_note">താക്കൾക് ഇപ്പോൾ ഡിസ്ക്രിപ്ഷൻ ബോക്സിലെ ടെക്സ്റ്റ്‌ തിരഞ്ഞെടുക്കാൻ സാധിക്കും. ശ്രെദ്ധിക്കുക സെലെക്ഷൻ മോഡിൽ പേജ് ചിലപ്പോൾ മിന്നുകയും ലിങ്കുകൾ ക്ലിക്ക് ചെയ്യാനാകാതെയും വന്നേക്കാം.</string>
<string name="service_provides_reason">ഇതിന്റെ കാരണം %s നൽകും:</string>
<string name="account_terminated">അക്കൗണ്ട് ഇല്ലാതായിരിക്കുന്നു</string>
<string name="feed_load_error_fast_unknown">ഫാസ്റ്റ് ഫീഡ് മോഡ് കൂടുതൽ വിവരങ്ങൾ നൽകില്ല.</string>
<string name="feed_load_error_terminated">സൃഷ്ടാവിന്റെ അക്കൗണ്ട് ഇല്ലാതായിരിക്കുന്നു.

View File

@ -138,11 +138,8 @@
<string name="settings_category_debug_title">डेबग</string>
<string name="settings_category_updates_title">अपडेट</string>
<string name="duration_live">थेट</string>
<string name="short_billion"></string>
<string name="playlists">प्लेलिस्ट</string>
<string name="short_million"></string>
<string name="file">फाईल</string>
<string name="short_thousand">के</string>
<string name="tab_licenses">परवाना</string>
<string name="checksum">चेकसम</string>
<string name="action_history">इतिहास</string>

View File

@ -171,9 +171,6 @@
<string name="video">Video</string>
<string name="audio">Audio</string>
<string name="retry">Cuba semula</string>
<string name="short_thousand">K</string>
<string name="short_million">J</string>
<string name="short_billion">B</string>
<string name="no_subscribers">Tiada pelanggan</string>
<plurals name="subscribers">
<item quantity="other">%s pelanggan</item>

View File

@ -89,9 +89,6 @@
<string name="popup_playing_toast">Spiller av i oppsprettsmodus</string>
<string name="all">Alle</string>
<string name="disabled">Avskrudd</string>
<string name="short_thousand">k</string>
<string name="short_million">M</string>
<string name="short_billion">Mrd.</string>
<string name="msg_popup_permission">Denne tilgangen trengs for
\nåpning i oppsprettsmodus</string>
<string name="recaptcha_request_toast">reCAPTCHA-oppgave forespurt</string>
@ -603,7 +600,6 @@
\nØnsker du å oppheve ditt abonnement på denne kanalen\?</string>
<string name="description_select_disable">Skru av merking av tekst i beskrivelsen</string>
<string name="description_select_enable">Skru på merking av tekst i beskrivelsen</string>
<string name="service_provides_reason">%s oppgav denne grunnen:</string>
<string name="account_terminated">Konto terminert</string>
<string name="feed_load_error_account_info">Kunne ikke laste inn informasjonskanal for «%s».</string>
<string name="feed_load_error">Kunne ikke laste inn informasjonskanal</string>

View File

@ -176,9 +176,6 @@
<string name="video">भिडियो</string>
<string name="audio">अडियो</string>
<string name="retry">पुन: प्रयास</string>
<string name="short_thousand">हजार</string>
<string name="short_million">करोड</string>
<string name="short_billion">अर्ब</string>
<string name="no_subscribers">कुनै सदस्यहरू छैनन्</string>
<plurals name="subscribers">
<item quantity="one">%s सदस्य</item>

View File

@ -147,9 +147,6 @@
<string name="video">Video</string>
<string name="audio">Geluid</string>
<string name="retry">Opnieuw proberen</string>
<string name="short_thousand">k</string>
<string name="short_million">M</string>
<string name="short_billion">mld.</string>
<string name="no_subscribers">Geen abonnees</string>
<plurals name="subscribers">
<item quantity="one">%s abonnee</item>

View File

@ -84,9 +84,6 @@
<string name="recaptcha_request_toast">reCAPTCHA-uitdaging gevraagd</string>
<string name="open_in_popup_mode">Openen in pop-upmodus</string>
<string name="all">Alles</string>
<string name="short_thousand">dznd.</string>
<string name="short_million">mln.</string>
<string name="short_billion">mld.</string>
<string name="msg_popup_permission">Deze machtiging is vereist om te
\nopenen in pop-upmodus</string>
<string name="popup_playing_toast">Speelt af in pop-upmodus</string>
@ -615,7 +612,6 @@
<string name="on">Aan</string>
<string name="tablet_mode_title">Tablet-modus</string>
<string name="open_website_license">Website openen</string>
<string name="service_provides_reason">%s geeft de volgende reden:</string>
<string name="account_terminated">Account getermineerd</string>
<string name="feed_load_error_fast_unknown">De snelle feed mode levert hierover niet meer informatie.</string>
<string name="feed_load_error_terminated">De account van de auteur is getermineerd.

View File

@ -178,9 +178,6 @@
<string name="search_no_results">ߞߐߝߟߌ߫ ߕߍ߫ ߦߋ߲߬</string>
<string name="audio">ߡߍ߲ߕߊ</string>
<string name="retry">ߞߵߊ߬ ߡߊߛߊ߬ߦߌ߬</string>
<string name="short_thousand">ߥߊ߯</string>
<string name="short_million">ߞߋ߲߬</string>
<string name="short_billion">ߥߟߡ</string>
<string name="auto_queue_title">ߞߊ߬ ߥߏ߬ߦߏ߫ ߣߊ߬ߕߊ ߝߙߊ߬ ߕߎ߲߰ߠߌ߲ ߠߊ߫ ߞߍ߲ߖߘߍߡߊߓߟߏ ߡߊ߬</string>
<string name="auto_queue_summary">ߞߊ߬ ߕߎ߲߰ߠߌ߲ ߘߐߞߊ߬ߙߊ߲ ߓߟߏߕߎ߰ (ߞߊߣߊ߬ ߡߊߛߊ߬ߦߌ߬) ߥߏ߬ߦߏ߫ ߢߐ߲߰ߘߐ ߟߎ߫ ߟߊ߫</string>
<string name="left_gesture_control_summary">ߕߏߟߏ߲ߟߊ߲߫ ߥߊ߲߬ߥߊ߲ ߣߎߡߊ߲߫ ߕߟߊ ߖߍ߰ߙߍ ߛߎߥߊ߲ߘߌ߫</string>
@ -628,7 +625,6 @@
<string name="no_appropriate_file_manager_message_android_10">ߞߐߕߐ߯ ߡߊߡߙߊߟߊ߲߫ ߛߌ߫ ߡߊ߫ ߛߐ߬ߘߐ߲߬ ߞߋߥߊߟߌ ߣߌ߲߬ ߞߊ߲ߡߊ߬.
\nߘߌ߬ߢߍ߬ ߦߋ߫ ߞߐߕߐ߯ ߡߊߡߙߊߟߊ߲ ߘߏ߫ ߡߊߞߍ߫ ߡߍ߲ ߣߌ߫ ߡߙߊ߬ߘߐ߬ߦߊ ߟߊߛߐ߬ߘߐ߲ ߡߎ߬ߙߊ߲߬ߞߊ߲ߞߋ ߘߌ߫ ߓߍ߲߬</string>
<string name="youtube_music_premium_content">ߦߋߡߍ߲ߕߊ ߘߌ߫ ߡߊߛߐ߬ߘߐ߲߬ YouTube Music Premium ߛߌ߲߬ߝߏ߲ ߠߎ߬ ߟߋ߬ ߘߐߙߐ߲߫ ߓߟߏ߫߸ ߏ߬ ߘߐ߫ ߊ߬ ߕߍ߫ ߛߋ߫ ߘߐߛߊߙߌ߫ ߟߊ߫ ߥߟߊ߫ ߞߵߊ߬ ߟߊߖߌ߰ ߣߌߎߔߌߔ ߓߟߏ.</string>
<string name="service_provides_reason">%s ߦߋ߫ ߞߎ߲߭ ߣߌ߲߬ ߠߋ߬ ߝߐ߫ ߟߊ߫:</string>
<string name="featured">ߛߊ߲ߞߊߥߟߌ</string>
<string name="radio">ߥߎߢߊ߲ߓߍ߲</string>
<string name="auto_device_theme_title">ߖߘߍ߬ߢߍ߫ (ߕߙߏߞߏ߫ ߛߊߛߊ)</string>

View File

@ -437,7 +437,6 @@
<string name="detail_dislikes_img_view_description">ନାପସନ୍ଦ</string>
<string name="comments_tab_description">ମନ୍ତବ୍ୟ ଗୁଡିକ</string>
<string name="description_tab_description">ବର୍ଣ୍ଣନା</string>
<string name="short_million">ନିୟୁତ</string>
<string name="recaptcha_solve">ସମାଧାନ</string>
<string name="playback_speed_control">ପ୍ଲେବେକ୍ ସ୍ପିଡ୍ ନିୟନ୍ତ୍ରଣ</string>
<string name="playback_tempo">ଟେମ୍ପୋ</string>
@ -486,7 +485,6 @@
<string name="feed_group_dialog_select_subscriptions">ସଦସ୍ୟତା ଚୟନ କରନ୍ତୁ</string>
<string name="feed_group_dialog_empty_selection">କୌଣସି ସଦସ୍ୟତା ଚୟନ ହୋଇନାହିଁ</string>
<string name="feed_use_dedicated_fetch_method_enable_button">ଦ୍ରୁତ ମୋଡ୍ ସକ୍ଷମ କରନ୍ତୁ</string>
<string name="service_provides_reason">%s ଏହି କାରଣ ପ୍ରଦାନ କରେ:</string>
<string name="detail_sub_channel_thumbnail_view_description">ଚ୍ୟାନେଲର ଅବତାର ଥମ୍ୱନେଲ୍</string>
<string name="featured">ବୈଶିଷ୍ଟ୍ୟ</string>
<string name="radio">ରେଡିଓ</string>
@ -527,7 +525,6 @@
<string name="related_items_tab_description">ସମ୍ବନ୍ଧୀୟ ଆଇଟମ୍ ଗୁଡ଼ିକ</string>
<string name="detail_drag_description">ପୁନଃ ସଯାଇବାକୁ ଡ୍ରାଗ୍ କରନ୍ତୁ</string>
<string name="pause">ବିରାମ</string>
<string name="short_billion">ଵୃନ୍ଦ</string>
<string name="no_subscribers">କୌଣସି ଗ୍ରାହକ ନାହାଁନ୍ତି</string>
<string name="create">ସୃଷ୍ଟି କରନ୍ତୁ</string>
<string name="msg_running_detail">ବିବରଣୀ ପାଇଁ ଟ୍ୟାପ୍ କରନ୍ତୁ</string>
@ -608,7 +605,6 @@
<string name="select_quality_external_players">ବହିଃ-ଚାଳକ ପାଇଁ ଗୁଣବତ୍ତା ଚୟନ କରନ୍ତୁ</string>
<string name="detail_pinned_comment_view_description">ପିନ୍ ହୋଇଥିବା ମନ୍ତବ୍ୟ</string>
<string name="open_website_license">ୱେବସାଇଟ୍ ଖୋଲନ୍ତୁ</string>
<string name="short_thousand">ହଜାର</string>
<string name="preferred_player_fetcher_notification_title">ସୂଚନା ପାଇବା…</string>
<plurals name="subscribers">
<item quantity="one">%s ଗ୍ରାହକ</item>

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