1
0
mirror of https://github.com/TeamNewPipe/NewPipe synced 2026-02-28 12:59:44 +00:00

Merge branch 'dev' into refactor

This commit is contained in:
Aayush Gupta
2026-02-22 19:29:56 +08:00
215 changed files with 2106 additions and 2418 deletions

View File

@@ -84,7 +84,7 @@ configure<ApplicationExtension> {
resValue("string", "app_name", "NewPipe $suffix")
}
isMinifyEnabled = true
isShrinkResources = false // disabled to fix F-Droid"s reproducible build
isShrinkResources = true
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
@@ -93,13 +93,9 @@ configure<ApplicationExtension> {
}
lint {
checkReleaseBuilds = false
// Or, if you prefer, you can continue to check for errors in release builds,
// but continue the build even when errors are found:
lintConfig = file("lint.xml")
// Continue the debug build even when errors are found
abortOnError = false
// suppress false warning ("Resource IDs will be non-final in Android Gradle Plugin version
// 5.0, avoid using them in switch case statements"), which affects only library projects
disable += "NonConstantResourceId"
}
compileOptions {

10
app/lint.xml Normal file
View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ SPDX-FileCopyrightText: 2026 NewPipe e.V. <https://newpipe-ev.de>
~ SPDX-License-Identifier: GPL-3.0-or-later
-->
<lint>
<issue id="MissingTranslation" severity="ignore" />
<issue id="MissingQuantity" severity="ignore" />
<issue id="ImpliedQuantity" severity="ignore" />
</lint>

View File

@@ -1,50 +0,0 @@
package org.schabi.newpipe;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import org.schabi.newpipe.util.NavigationHelper;
/*
* Copyright (C) Hans-Christoph Steiner 2016 <hans@eds.org>
* ExitActivity.java is part of NewPipe.
*
* NewPipe is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* NewPipe is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
*/
public class ExitActivity extends Activity {
public static void exitAndRemoveFromRecentApps(final Activity activity) {
final Intent intent = new Intent(activity, ExitActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK
| Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS
| Intent.FLAG_ACTIVITY_CLEAR_TASK
| Intent.FLAG_ACTIVITY_NO_ANIMATION);
activity.startActivity(intent);
}
@SuppressLint("NewApi")
@Override
protected void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
finishAndRemoveTask();
NavigationHelper.restartApp(this);
}
}

View File

@@ -0,0 +1,36 @@
/*
* SPDX-FileCopyrightText: 2016-2026 NewPipe contributors <https://newpipe.net>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package org.schabi.newpipe
import android.annotation.SuppressLint
import android.app.Activity
import android.content.Intent
import android.os.Bundle
import org.schabi.newpipe.util.NavigationHelper
class ExitActivity : Activity() {
@SuppressLint("NewApi")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
finishAndRemoveTask()
NavigationHelper.restartApp(this)
}
companion object {
@JvmStatic
fun exitAndRemoveFromRecentApps(activity: Activity) {
val intent = Intent(activity, ExitActivity::class.java)
intent.addFlags(
Intent.FLAG_ACTIVITY_NEW_TASK
or Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS
or Intent.FLAG_ACTIVITY_CLEAR_TASK
or Intent.FLAG_ACTIVITY_NO_ANIMATION
)
activity.startActivity(intent)
}
}
}

View File

@@ -305,25 +305,21 @@ public class MainActivity extends AppCompatActivity {
}
private boolean drawerItemSelected(final MenuItem item) {
switch (item.getGroupId()) {
case R.id.menu_services_group:
changeService(item);
break;
case R.id.menu_tabs_group:
tabSelected(item);
break;
case R.id.menu_kiosks_group:
try {
kioskSelected(item);
} catch (final Exception e) {
ErrorUtil.showUiErrorSnackbar(this, "Selecting drawer kiosk", e);
}
break;
case R.id.menu_options_about_group:
optionsAboutSelected(item);
break;
default:
return false;
final int groupId = item.getGroupId();
if (groupId == R.id.menu_services_group) {
changeService(item);
} else if (groupId == R.id.menu_tabs_group) {
tabSelected(item);
} else if (groupId == R.id.menu_kiosks_group) {
try {
kioskSelected(item);
} catch (final Exception e) {
ErrorUtil.showUiErrorSnackbar(this, "Selecting drawer kiosk", e);
}
} else if (groupId == R.id.menu_options_about_group) {
optionsAboutSelected(item);
} else {
return false;
}
mainBinding.getRoot().closeDrawers();

View File

@@ -82,7 +82,9 @@ class NewVersionWorker(
)
val notificationManager = NotificationManagerCompat.from(applicationContext)
notificationManager.notify(2000, notificationBuilder.build())
if (notificationManager.areNotificationsEnabled()) {
notificationManager.notify(2000, notificationBuilder.build())
}
}
@Throws(IOException::class, ReCaptchaException::class)

View File

@@ -41,50 +41,50 @@ public final class QueueItemMenuUtil {
}
popupMenu.setOnMenuItemClickListener(menuItem -> {
switch (menuItem.getItemId()) {
case R.id.menu_item_remove:
final int index = playQueue.indexOf(item);
playQueue.remove(index);
return true;
case R.id.menu_item_details:
// playQueue is null since we don't want any queue change
NavigationHelper.openVideoDetail(context, item.getServiceId(),
item.getUrl(), item.getTitle(), null,
false);
return true;
case R.id.menu_item_append_playlist:
PlaylistDialog.createCorrespondingDialog(
context,
List.of(new StreamEntity(item)),
dialog -> dialog.show(
fragmentManager,
"QueueItemMenuUtil@append_playlist"
)
);
final int itemId = menuItem.getItemId();
if (itemId == R.id.menu_item_remove) {
final int index = playQueue.indexOf(item);
playQueue.remove(index);
return true;
} else if (itemId == R.id.menu_item_details) {
// playQueue is null since we don't want any queue change
NavigationHelper.openVideoDetail(context, item.getServiceId(),
item.getUrl(), item.getTitle(), null,
false);
return true;
} else if (itemId == R.id.menu_item_append_playlist) {
PlaylistDialog.createCorrespondingDialog(
context,
List.of(new StreamEntity(item)),
dialog -> dialog.show(
fragmentManager,
"QueueItemMenuUtil@append_playlist"
)
);
return true;
case R.id.menu_item_channel_details:
SparseItemUtil.fetchUploaderUrlIfSparse(context, item.getServiceId(),
item.getUrl(), item.getUploaderUrl(),
// An intent must be used here.
// Opening with FragmentManager transactions is not working,
// as PlayQueueActivity doesn't use fragments.
uploaderUrl -> NavigationHelper.openChannelFragmentUsingIntent(
context, item.getServiceId(), uploaderUrl, item.getUploader()
));
return true;
case R.id.menu_item_share:
shareText(context, item.getTitle(), item.getUrl(),
item.getThumbnails());
return true;
case R.id.menu_item_download:
fetchStreamInfoAndSaveToDatabase(context, item.getServiceId(), item.getUrl(),
info -> {
final DownloadDialog downloadDialog = new DownloadDialog(context,
info);
downloadDialog.show(fragmentManager, "downloadDialog");
});
return true;
return true;
} else if (itemId == R.id.menu_item_channel_details) {
SparseItemUtil.fetchUploaderUrlIfSparse(context, item.getServiceId(),
item.getUrl(), item.getUploaderUrl(),
// An intent must be used here.
// Opening with FragmentManager transactions is not working,
// as PlayQueueActivity doesn't use fragments.
uploaderUrl -> NavigationHelper.openChannelFragmentUsingIntent(
context, item.getServiceId(), uploaderUrl, item.getUploader()
));
return true;
} else if (itemId == R.id.menu_item_share) {
shareText(context, item.getTitle(), item.getUrl(),
item.getThumbnails());
return true;
} else if (itemId == R.id.menu_item_download) {
fetchStreamInfoAndSaveToDatabase(context, item.getServiceId(), item.getUrl(),
info -> {
final DownloadDialog downloadDialog = new DownloadDialog(context,
info);
downloadDialog.show(fragmentManager, "downloadDialog");
});
return true;
}
return false;
});

View File

@@ -343,8 +343,7 @@ public class RouterActivity extends AppCompatActivity {
return;
}
final List<StreamingService.ServiceInfo.MediaCapability> capabilities =
currentService.getServiceInfo().getMediaCapabilities();
final var capabilities = currentService.getServiceInfo().getMediaCapabilities();
// Check if the service supports the choice
if ((isVideoPlayerSelected && capabilities.contains(VIDEO))
@@ -522,8 +521,7 @@ public class RouterActivity extends AppCompatActivity {
final List<AdapterChoiceItem> returnedItems = new ArrayList<>();
returnedItems.add(showInfo); // Always present
final List<StreamingService.ServiceInfo.MediaCapability> capabilities =
service.getServiceInfo().getMediaCapabilities();
final var capabilities = service.getServiceInfo().getMediaCapabilities();
if (linkType == LinkType.STREAM || linkType == LinkType.PLAYLIST) {
if (capabilities.contains(VIDEO)) {

View File

@@ -62,11 +62,7 @@ data class PlaylistRemoteEntity(
orderingName = playlistInfo.name,
url = playlistInfo.url,
thumbnailUrl = ImageStrategy.imageListToDbUrl(
if (playlistInfo.thumbnails.isEmpty()) {
playlistInfo.uploaderAvatars
} else {
playlistInfo.thumbnails
}
playlistInfo.thumbnails.ifEmpty { playlistInfo.uploaderAvatars }
),
uploader = playlistInfo.uploaderName,
streamCount = playlistInfo.streamCount

View File

@@ -88,7 +88,7 @@ abstract class StreamDAO : BasicDAO<StreamEntity> {
private fun compareAndUpdateStream(newerStream: StreamEntity) {
val existentMinimalStream = getMinimalStreamForCompare(newerStream.serviceId, newerStream.url)
?: throw IllegalStateException("Stream cannot be null just after insertion.")
?: error("Stream cannot be null just after insertion.")
newerStream.uid = existentMinimalStream.uid
if (!StreamTypeUtil.isLiveStream(newerStream.streamType)) {

View File

@@ -100,7 +100,7 @@ abstract class SubscriptionDAO : BasicDAO<SubscriptionEntity> {
entity.uid = uidFromInsert
} else {
val subscriptionIdFromDb = getSubscriptionIdInternal(entity.serviceId, entity.url!!)
?: throw IllegalStateException("Subscription cannot be null just after insertion.")
?: error("Subscription cannot be null just after insertion.")
entity.uid = subscriptionIdFromDb
update(entity)

View File

@@ -16,6 +16,7 @@ import android.os.IBinder;
import android.provider.Settings;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
@@ -31,7 +32,6 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.view.menu.ActionMenuItemView;
import androidx.appcompat.widget.Toolbar;
import androidx.collection.SparseArrayCompat;
import androidx.documentfile.provider.DocumentFile;
@@ -113,7 +113,7 @@ public class DownloadDialog extends DialogFragment
private StoredDirectoryHelper mainStorageAudio = null;
private StoredDirectoryHelper mainStorageVideo = null;
private DownloadManager downloadManager = null;
private ActionMenuItemView okButton = null;
private MenuItem okButton = null;
private Context context = null;
private boolean askForSavePath;
@@ -558,17 +558,13 @@ public class DownloadDialog extends DialogFragment
}
boolean flag = true;
switch (checkedId) {
case R.id.audio_button:
setupAudioSpinner();
break;
case R.id.video_button:
setupVideoSpinner();
break;
case R.id.subtitle_button:
setupSubtitleSpinner();
flag = false;
break;
if (checkedId == R.id.audio_button) {
setupAudioSpinner();
} else if (checkedId == R.id.video_button) {
setupVideoSpinner();
} else if (checkedId == R.id.subtitle_button) {
setupSubtitleSpinner();
flag = false;
}
dialogBinding.threads.setEnabled(flag);
@@ -585,29 +581,26 @@ public class DownloadDialog extends DialogFragment
+ "position = [" + position + "], id = [" + id + "]");
}
switch (parent.getId()) {
case R.id.quality_spinner:
switch (dialogBinding.videoAudioGroup.getCheckedRadioButtonId()) {
case R.id.video_button:
selectedVideoIndex = position;
onVideoStreamSelected();
break;
case R.id.subtitle_button:
selectedSubtitleIndex = position;
break;
}
onItemSelectedSetFileName();
break;
case R.id.audio_track_spinner:
final boolean trackChanged = selectedAudioTrackIndex != position;
selectedAudioTrackIndex = position;
if (trackChanged) {
updateSecondaryStreams();
fetchStreamsSize();
}
break;
case R.id.audio_stream_spinner:
selectedAudioIndex = position;
final int parentId = parent.getId();
if (parentId == R.id.quality_spinner) {
final int checkedRadioButtonId = dialogBinding.videoAudioGroup
.getCheckedRadioButtonId();
if (checkedRadioButtonId == R.id.video_button) {
selectedVideoIndex = position;
onVideoStreamSelected();
} else if (checkedRadioButtonId == R.id.subtitle_button) {
selectedSubtitleIndex = position;
}
onItemSelectedSetFileName();
} else if (parentId == R.id.audio_track_spinner) {
final boolean trackChanged = selectedAudioTrackIndex != position;
selectedAudioTrackIndex = position;
if (trackChanged) {
updateSecondaryStreams();
fetchStreamsSize();
}
} else if (parentId == R.id.audio_stream_spinner) {
selectedAudioIndex = position;
}
}
@@ -622,23 +615,20 @@ public class DownloadDialog extends DialogFragment
|| prevFileName.startsWith(getString(R.string.caption_file_name, fileName, ""))) {
// only update the file name field if it was not edited by the user
switch (dialogBinding.videoAudioGroup.getCheckedRadioButtonId()) {
case R.id.audio_button:
case R.id.video_button:
if (!prevFileName.equals(fileName)) {
// since the user might have switched between audio and video, the correct
// text might already be in place, so avoid resetting the cursor position
dialogBinding.fileName.setText(fileName);
}
break;
case R.id.subtitle_button:
final String setSubtitleLanguageCode = subtitleStreamsAdapter
.getItem(selectedSubtitleIndex).getLanguageTag();
// this will reset the cursor position, which is bad UX, but it can't be avoided
dialogBinding.fileName.setText(getString(
R.string.caption_file_name, fileName, setSubtitleLanguageCode));
break;
final int radioButtonId = dialogBinding.videoAudioGroup
.getCheckedRadioButtonId();
if (radioButtonId == R.id.audio_button || radioButtonId == R.id.video_button) {
if (!prevFileName.equals(fileName)) {
// since the user might have switched between audio and video, the correct
// text might already be in place, so avoid resetting the cursor position
dialogBinding.fileName.setText(fileName);
}
} else if (radioButtonId == R.id.subtitle_button) {
final String setSubtitleLanguageCode = subtitleStreamsAdapter
.getItem(selectedSubtitleIndex).getLanguageTag();
// this will reset the cursor position, which is bad UX, but it can't be avoided
dialogBinding.fileName.setText(getString(
R.string.caption_file_name, fileName, setSubtitleLanguageCode));
}
}
}
@@ -770,47 +760,44 @@ public class DownloadDialog extends DialogFragment
filenameTmp = getNameEditText().concat(".");
switch (dialogBinding.videoAudioGroup.getCheckedRadioButtonId()) {
case R.id.audio_button:
selectedMediaType = getString(R.string.last_download_type_audio_key);
mainStorage = mainStorageAudio;
format = audioStreamsAdapter.getItem(selectedAudioIndex).getFormat();
size = getWrappedAudioStreams().getSizeInBytes(selectedAudioIndex);
if (format == MediaFormat.WEBMA_OPUS) {
mimeTmp = "audio/ogg";
filenameTmp += "opus";
} else if (format != null) {
mimeTmp = format.mimeType;
filenameTmp += format.getSuffix();
}
break;
case R.id.video_button:
selectedMediaType = getString(R.string.last_download_type_video_key);
mainStorage = mainStorageVideo;
format = videoStreamsAdapter.getItem(selectedVideoIndex).getFormat();
size = wrappedVideoStreams.getSizeInBytes(selectedVideoIndex);
if (format != null) {
mimeTmp = format.mimeType;
filenameTmp += format.getSuffix();
}
break;
case R.id.subtitle_button:
selectedMediaType = getString(R.string.last_download_type_subtitle_key);
mainStorage = mainStorageVideo; // subtitle & video files go together
format = subtitleStreamsAdapter.getItem(selectedSubtitleIndex).getFormat();
size = wrappedSubtitleStreams.getSizeInBytes(selectedSubtitleIndex);
if (format != null) {
mimeTmp = format.mimeType;
}
final int checkedRadioButtonId = dialogBinding.videoAudioGroup.getCheckedRadioButtonId();
if (checkedRadioButtonId == R.id.audio_button) {
selectedMediaType = getString(R.string.last_download_type_audio_key);
mainStorage = mainStorageAudio;
format = audioStreamsAdapter.getItem(selectedAudioIndex).getFormat();
size = getWrappedAudioStreams().getSizeInBytes(selectedAudioIndex);
if (format == MediaFormat.WEBMA_OPUS) {
mimeTmp = "audio/ogg";
filenameTmp += "opus";
} else if (format != null) {
mimeTmp = format.mimeType;
filenameTmp += format.getSuffix();
}
} else if (checkedRadioButtonId == R.id.video_button) {
selectedMediaType = getString(R.string.last_download_type_video_key);
mainStorage = mainStorageVideo;
format = videoStreamsAdapter.getItem(selectedVideoIndex).getFormat();
size = wrappedVideoStreams.getSizeInBytes(selectedVideoIndex);
if (format != null) {
mimeTmp = format.mimeType;
filenameTmp += format.getSuffix();
}
} else if (checkedRadioButtonId == R.id.subtitle_button) {
selectedMediaType = getString(R.string.last_download_type_subtitle_key);
mainStorage = mainStorageVideo; // subtitle & video files go together
format = subtitleStreamsAdapter.getItem(selectedSubtitleIndex).getFormat();
size = wrappedSubtitleStreams.getSizeInBytes(selectedSubtitleIndex);
if (format != null) {
mimeTmp = format.mimeType;
}
if (format == MediaFormat.TTML) {
filenameTmp += MediaFormat.SRT.getSuffix();
} else if (format != null) {
filenameTmp += format.getSuffix();
}
break;
default:
throw new RuntimeException("No stream selected");
if (format == MediaFormat.TTML) {
filenameTmp += MediaFormat.SRT.getSuffix();
} else if (format != null) {
filenameTmp += format.getSuffix();
}
} else {
throw new RuntimeException("No stream selected");
}
if (!askForSavePath && (mainStorage == null
@@ -1057,59 +1044,56 @@ public class DownloadDialog extends DialogFragment
long nearLength = 0;
// more download logic: select muxer, subtitle converter, etc.
switch (dialogBinding.videoAudioGroup.getCheckedRadioButtonId()) {
case R.id.audio_button:
kind = 'a';
selectedStream = audioStreamsAdapter.getItem(selectedAudioIndex);
final int checkedRadioButtonId = dialogBinding.videoAudioGroup.getCheckedRadioButtonId();
if (checkedRadioButtonId == R.id.audio_button) {
kind = 'a';
selectedStream = audioStreamsAdapter.getItem(selectedAudioIndex);
if (selectedStream.getFormat() == MediaFormat.M4A) {
psName = Postprocessing.ALGORITHM_M4A_NO_DASH;
} else if (selectedStream.getFormat() == MediaFormat.WEBMA_OPUS) {
psName = Postprocessing.ALGORITHM_OGG_FROM_WEBM_DEMUXER;
if (selectedStream.getFormat() == MediaFormat.M4A) {
psName = Postprocessing.ALGORITHM_M4A_NO_DASH;
} else if (selectedStream.getFormat() == MediaFormat.WEBMA_OPUS) {
psName = Postprocessing.ALGORITHM_OGG_FROM_WEBM_DEMUXER;
}
} else if (checkedRadioButtonId == R.id.video_button) {
kind = 'v';
selectedStream = videoStreamsAdapter.getItem(selectedVideoIndex);
final SecondaryStreamHelper<AudioStream> secondary = videoStreamsAdapter
.getAllSecondary()
.get(wrappedVideoStreams.getStreamsList().indexOf(selectedStream));
if (secondary != null) {
secondaryStream = secondary.getStream();
if (selectedStream.getFormat() == MediaFormat.MPEG_4) {
psName = Postprocessing.ALGORITHM_MP4_FROM_DASH_MUXER;
} else {
psName = Postprocessing.ALGORITHM_WEBM_MUXER;
}
break;
case R.id.video_button:
kind = 'v';
selectedStream = videoStreamsAdapter.getItem(selectedVideoIndex);
final SecondaryStreamHelper<AudioStream> secondary = videoStreamsAdapter
.getAllSecondary()
.get(wrappedVideoStreams.getStreamsList().indexOf(selectedStream));
final long videoSize = wrappedVideoStreams.getSizeInBytes(
(VideoStream) selectedStream);
if (secondary != null) {
secondaryStream = secondary.getStream();
if (selectedStream.getFormat() == MediaFormat.MPEG_4) {
psName = Postprocessing.ALGORITHM_MP4_FROM_DASH_MUXER;
} else {
psName = Postprocessing.ALGORITHM_WEBM_MUXER;
}
final long videoSize = wrappedVideoStreams.getSizeInBytes(
(VideoStream) selectedStream);
// set nearLength, only, if both sizes are fetched or known. This probably
// does not work on slow networks but is later updated in the downloader
if (secondary.getSizeInBytes() > 0 && videoSize > 0) {
nearLength = secondary.getSizeInBytes() + videoSize;
}
// set nearLength, only, if both sizes are fetched or known. This probably
// does not work on slow networks but is later updated in the downloader
if (secondary.getSizeInBytes() > 0 && videoSize > 0) {
nearLength = secondary.getSizeInBytes() + videoSize;
}
break;
case R.id.subtitle_button:
threads = 1; // use unique thread for subtitles due small file size
kind = 's';
selectedStream = subtitleStreamsAdapter.getItem(selectedSubtitleIndex);
}
} else if (checkedRadioButtonId == R.id.subtitle_button) {
threads = 1; // use unique thread for subtitles due small file size
kind = 's';
selectedStream = subtitleStreamsAdapter.getItem(selectedSubtitleIndex);
if (selectedStream.getFormat() == MediaFormat.TTML) {
psName = Postprocessing.ALGORITHM_TTML_CONVERTER;
psArgs = new String[] {
selectedStream.getFormat().getSuffix(),
"false" // ignore empty frames
};
}
break;
default:
return;
if (selectedStream.getFormat() == MediaFormat.TTML) {
psName = Postprocessing.ALGORITHM_TTML_CONVERTER;
psArgs = new String[]{
selectedStream.getFormat().getSuffix(),
"false" // ignore empty frames
};
}
} else {
return;
}
if (secondaryStream == null) {

View File

@@ -1,323 +0,0 @@
package org.schabi.newpipe.error;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.util.Log;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.content.IntentCompat;
import com.grack.nanojson.JsonWriter;
import org.schabi.newpipe.BuildConfig;
import org.schabi.newpipe.R;
import org.schabi.newpipe.databinding.ActivityErrorBinding;
import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.ThemeHelper;
import org.schabi.newpipe.util.external_communication.ShareUtils;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Arrays;
import java.util.stream.Collectors;
/*
* Created by Christian Schabesberger on 24.10.15.
*
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
* ErrorActivity.java is part of NewPipe.
*
* NewPipe is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
* <
* NewPipe is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
* <
* You should have received a copy of the GNU General Public License
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
*/
/**
* This activity is used to show error details and allow reporting them in various ways. Use {@link
* ErrorUtil#openActivity(Context, ErrorInfo)} to correctly open this activity.
*/
public class ErrorActivity extends AppCompatActivity {
// LOG TAGS
public static final String TAG = ErrorActivity.class.toString();
// BUNDLE TAGS
public static final String ERROR_INFO = "error_info";
public static final String ERROR_EMAIL_ADDRESS = "crashreport@newpipe.schabi.org";
public static final String ERROR_EMAIL_SUBJECT = "Exception in ";
public static final String ERROR_GITHUB_ISSUE_URL =
"https://github.com/TeamNewPipe/NewPipe/issues";
private ErrorInfo errorInfo;
private String currentTimeStamp;
private ActivityErrorBinding activityErrorBinding;
////////////////////////////////////////////////////////////////////////
// Activity lifecycle
////////////////////////////////////////////////////////////////////////
@Override
protected void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ThemeHelper.setDayNightMode(this);
ThemeHelper.setTheme(this);
activityErrorBinding = ActivityErrorBinding.inflate(getLayoutInflater());
setContentView(activityErrorBinding.getRoot());
final Intent intent = getIntent();
setSupportActionBar(activityErrorBinding.toolbarLayout.toolbar);
final ActionBar actionBar = getSupportActionBar();
if (actionBar != null) {
actionBar.setDisplayHomeAsUpEnabled(true);
actionBar.setTitle(R.string.error_report_title);
actionBar.setDisplayShowTitleEnabled(true);
}
errorInfo = IntentCompat.getParcelableExtra(intent, ERROR_INFO, ErrorInfo.class);
// important add guru meditation
addGuruMeditation();
// print current time, as zoned ISO8601 timestamp
final ZonedDateTime now = ZonedDateTime.now();
currentTimeStamp = now.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME);
activityErrorBinding.errorReportEmailButton.setOnClickListener(v ->
openPrivacyPolicyDialog(this, "EMAIL"));
activityErrorBinding.errorReportCopyButton.setOnClickListener(v ->
ShareUtils.copyToClipboard(this, buildMarkdown()));
activityErrorBinding.errorReportGitHubButton.setOnClickListener(v ->
openPrivacyPolicyDialog(this, "GITHUB"));
// normal bugreport
buildInfo(errorInfo);
activityErrorBinding.errorMessageView.setText(errorInfo.getMessage(this));
activityErrorBinding.errorView.setText(formErrorText(errorInfo.getStackTraces()));
// print stack trace once again for debugging:
for (final String e : errorInfo.getStackTraces()) {
Log.e(TAG, e);
}
}
@Override
public boolean onCreateOptionsMenu(final Menu menu) {
final MenuInflater inflater = getMenuInflater();
inflater.inflate(R.menu.error_menu, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(final MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home:
onBackPressed();
return true;
case R.id.menu_item_share_error:
ShareUtils.shareText(getApplicationContext(),
getString(R.string.error_report_title), buildJson());
return true;
default:
return false;
}
}
private void openPrivacyPolicyDialog(final Context context, final String action) {
new AlertDialog.Builder(context)
.setIcon(android.R.drawable.ic_dialog_alert)
.setTitle(R.string.privacy_policy_title)
.setMessage(R.string.start_accept_privacy_policy)
.setCancelable(false)
.setNeutralButton(R.string.read_privacy_policy, (dialog, which) ->
ShareUtils.openUrlInApp(context,
context.getString(R.string.privacy_policy_url)))
.setPositiveButton(R.string.accept, (dialog, which) -> {
if (action.equals("EMAIL")) { // send on email
final Intent i = new Intent(Intent.ACTION_SENDTO)
.setData(Uri.parse("mailto:")) // only email apps should handle this
.putExtra(Intent.EXTRA_EMAIL, new String[]{ERROR_EMAIL_ADDRESS})
.putExtra(Intent.EXTRA_SUBJECT, ERROR_EMAIL_SUBJECT
+ getString(R.string.app_name) + " "
+ BuildConfig.VERSION_NAME)
.putExtra(Intent.EXTRA_TEXT, buildJson());
ShareUtils.openIntentInApp(context, i);
} else if (action.equals("GITHUB")) { // open the NewPipe issue page on GitHub
ShareUtils.openUrlInApp(this, ERROR_GITHUB_ISSUE_URL);
}
})
.setNegativeButton(R.string.decline, null)
.show();
}
private String formErrorText(final String[] el) {
final String separator = "-------------------------------------";
return Arrays.stream(el)
.collect(Collectors.joining(separator + "\n", separator + "\n", separator));
}
private void buildInfo(final ErrorInfo info) {
String text = "";
activityErrorBinding.errorInfoLabelsView.setText(getString(R.string.info_labels)
.replace("\\n", "\n"));
text += getUserActionString(info.getUserAction()) + "\n"
+ info.getRequest() + "\n"
+ getContentLanguageString() + "\n"
+ getContentCountryString() + "\n"
+ getAppLanguage() + "\n"
+ info.getServiceName() + "\n"
+ currentTimeStamp + "\n"
+ getPackageName() + "\n"
+ BuildConfig.VERSION_NAME + "\n"
+ getOsString();
activityErrorBinding.errorInfosView.setText(text);
}
private String buildJson() {
try {
return JsonWriter.string()
.object()
.value("user_action", getUserActionString(errorInfo.getUserAction()))
.value("request", errorInfo.getRequest())
.value("content_language", getContentLanguageString())
.value("content_country", getContentCountryString())
.value("app_language", getAppLanguage())
.value("service", errorInfo.getServiceName())
.value("package", getPackageName())
.value("version", BuildConfig.VERSION_NAME)
.value("os", getOsString())
.value("time", currentTimeStamp)
.array("exceptions", Arrays.asList(errorInfo.getStackTraces()))
.value("user_comment", activityErrorBinding.errorCommentBox.getText()
.toString())
.end()
.done();
} catch (final Throwable e) {
Log.e(TAG, "Error while erroring: Could not build json");
e.printStackTrace();
}
return "";
}
private String buildMarkdown() {
try {
final StringBuilder htmlErrorReport = new StringBuilder();
final String userComment = activityErrorBinding.errorCommentBox.getText().toString();
if (!userComment.isEmpty()) {
htmlErrorReport.append(userComment).append("\n");
}
// basic error info
htmlErrorReport
.append("## Exception")
.append("\n* __User Action:__ ")
.append(getUserActionString(errorInfo.getUserAction()))
.append("\n* __Request:__ ").append(errorInfo.getRequest())
.append("\n* __Content Country:__ ").append(getContentCountryString())
.append("\n* __Content Language:__ ").append(getContentLanguageString())
.append("\n* __App Language:__ ").append(getAppLanguage())
.append("\n* __Service:__ ").append(errorInfo.getServiceName())
.append("\n* __Timestamp:__ ").append(currentTimeStamp)
.append("\n* __Package:__ ").append(getPackageName())
.append("\n* __Service:__ ").append(errorInfo.getServiceName())
.append("\n* __Version:__ ").append(BuildConfig.VERSION_NAME)
.append("\n* __OS:__ ").append(getOsString()).append("\n");
// Collapse all logs to a single paragraph when there are more than one
// to keep the GitHub issue clean.
if (errorInfo.getStackTraces().length > 1) {
htmlErrorReport
.append("<details><summary><b>Exceptions (")
.append(errorInfo.getStackTraces().length)
.append(")</b></summary><p>\n");
}
// add the logs
for (int i = 0; i < errorInfo.getStackTraces().length; i++) {
htmlErrorReport.append("<details><summary><b>Crash log ");
if (errorInfo.getStackTraces().length > 1) {
htmlErrorReport.append(i + 1);
}
htmlErrorReport.append("</b>")
.append("</summary><p>\n")
.append("\n```\n").append(errorInfo.getStackTraces()[i]).append("\n```\n")
.append("</details>\n");
}
// make sure to close everything
if (errorInfo.getStackTraces().length > 1) {
htmlErrorReport.append("</p></details>\n");
}
htmlErrorReport.append("<hr>\n");
return htmlErrorReport.toString();
} catch (final Throwable e) {
Log.e(TAG, "Error while erroring: Could not build markdown");
e.printStackTrace();
return "";
}
}
private String getUserActionString(final UserAction userAction) {
if (userAction == null) {
return "Your description is in another castle.";
} else {
return userAction.getMessage();
}
}
private String getContentCountryString() {
return Localization.getPreferredContentCountry(this).getCountryCode();
}
private String getContentLanguageString() {
return Localization.getPreferredLocalization(this).getLocalizationCode();
}
private String getAppLanguage() {
return Localization.getAppLocale().toString();
}
private String getOsString() {
final String osBase = Build.VERSION.BASE_OS;
return System.getProperty("os.name")
+ " " + (osBase.isEmpty() ? "Android" : osBase)
+ " " + Build.VERSION.RELEASE
+ " - " + Build.VERSION.SDK_INT;
}
private void addGuruMeditation() {
//just an easter egg
String text = activityErrorBinding.errorSorryView.getText().toString();
text += "\n" + getString(R.string.guru_meditation);
activityErrorBinding.errorSorryView.setText(text);
}
}

View File

@@ -0,0 +1,281 @@
/*
* SPDX-FileCopyrightText: 2015-2026 NewPipe contributors <https://newpipe.net>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package org.schabi.newpipe.error
import android.content.Context
import android.content.Intent
import android.os.Build
import android.os.Bundle
import android.util.Log
import android.view.Menu
import android.view.MenuItem
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.IntentCompat
import androidx.core.net.toUri
import com.grack.nanojson.JsonWriter
import java.time.ZonedDateTime
import java.time.format.DateTimeFormatter
import org.schabi.newpipe.BuildConfig
import org.schabi.newpipe.R
import org.schabi.newpipe.databinding.ActivityErrorBinding
import org.schabi.newpipe.util.Localization
import org.schabi.newpipe.util.ThemeHelper
import org.schabi.newpipe.util.external_communication.ShareUtils
/**
* This activity is used to show error details and allow reporting them in various ways.
* Use [ErrorUtil.openActivity] to correctly open this activity.
*/
class ErrorActivity : AppCompatActivity() {
private lateinit var errorInfo: ErrorInfo
private lateinit var currentTimeStamp: String
private lateinit var binding: ActivityErrorBinding
private val contentCountryString: String
get() = Localization.getPreferredContentCountry(this).countryCode
private val contentLanguageString: String
get() = Localization.getPreferredLocalization(this).localizationCode
private val appLanguage: String
get() = Localization.getAppLocale().toString()
private val osString: String
get() {
val name = System.getProperty("os.name")!!
val osBase = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
Build.VERSION.BASE_OS.ifEmpty { "Android" }
} else {
"Android"
}
return "$name $osBase ${Build.VERSION.RELEASE} - ${Build.VERSION.SDK_INT}"
}
private val errorEmailSubject: String
get() = "$ERROR_EMAIL_SUBJECT ${getString(R.string.app_name)} ${BuildConfig.VERSION_NAME}"
// /////////////////////////////////////////////////////////////////////
// Activity lifecycle
// /////////////////////////////////////////////////////////////////////
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
ThemeHelper.setDayNightMode(this)
ThemeHelper.setTheme(this)
binding = ActivityErrorBinding.inflate(layoutInflater)
setContentView(binding.getRoot())
setSupportActionBar(binding.toolbarLayout.toolbar)
supportActionBar?.apply {
setDisplayHomeAsUpEnabled(true)
setTitle(R.string.error_report_title)
setDisplayShowTitleEnabled(true)
}
errorInfo = IntentCompat.getParcelableExtra(intent, ERROR_INFO, ErrorInfo::class.java)!!
// important add guru meditation
addGuruMeditation()
// print current time, as zoned ISO8601 timestamp
currentTimeStamp = ZonedDateTime.now().format(DateTimeFormatter.ISO_OFFSET_DATE_TIME)
binding.errorReportEmailButton.setOnClickListener { _ ->
openPrivacyPolicyDialog(this, "EMAIL")
}
binding.errorReportCopyButton.setOnClickListener { _ ->
ShareUtils.copyToClipboard(this, buildMarkdown())
}
binding.errorReportGitHubButton.setOnClickListener { _ ->
openPrivacyPolicyDialog(this, "GITHUB")
}
// normal bugreport
buildInfo(errorInfo)
binding.errorMessageView.text = errorInfo.getMessage(this)
binding.errorView.text = formErrorText(errorInfo.stackTraces)
// print stack trace once again for debugging:
errorInfo.stackTraces.forEach { Log.e(TAG, it) }
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.error_menu, menu)
return true
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
android.R.id.home -> {
onBackPressed()
true
}
R.id.menu_item_share_error -> {
ShareUtils.shareText(
applicationContext,
getString(R.string.error_report_title),
buildJson()
)
true
}
else -> false
}
}
private fun openPrivacyPolicyDialog(context: Context, action: String) {
AlertDialog.Builder(context)
.setIcon(android.R.drawable.ic_dialog_alert)
.setTitle(R.string.privacy_policy_title)
.setMessage(R.string.start_accept_privacy_policy)
.setCancelable(false)
.setNeutralButton(R.string.read_privacy_policy) { _, _ ->
ShareUtils.openUrlInApp(context, context.getString(R.string.privacy_policy_url))
}
.setPositiveButton(R.string.accept) { _, _ ->
if (action == "EMAIL") { // send on email
val intent = Intent(Intent.ACTION_SENDTO)
.setData("mailto:".toUri()) // only email apps should handle this
.putExtra(Intent.EXTRA_EMAIL, arrayOf(ERROR_EMAIL_ADDRESS))
.putExtra(Intent.EXTRA_SUBJECT, errorEmailSubject)
.putExtra(Intent.EXTRA_TEXT, buildJson())
ShareUtils.openIntentInApp(context, intent)
} else if (action == "GITHUB") { // open the NewPipe issue page on GitHub
ShareUtils.openUrlInApp(this, ERROR_GITHUB_ISSUE_URL)
}
}
.setNegativeButton(R.string.decline, null)
.show()
}
private fun formErrorText(stacktrace: Array<String>): String {
val separator = "-------------------------------------"
return stacktrace.joinToString(separator + "\n", separator + "\n", separator)
}
private fun buildInfo(info: ErrorInfo) {
binding.errorInfoLabelsView.text = getString(R.string.info_labels)
val text = info.userAction.message + "\n" +
info.request + "\n" +
contentLanguageString + "\n" +
contentCountryString + "\n" +
appLanguage + "\n" +
info.getServiceName() + "\n" +
currentTimeStamp + "\n" +
packageName + "\n" +
BuildConfig.VERSION_NAME + "\n" +
osString
binding.errorInfosView.text = text
}
private fun buildJson(): String {
try {
return JsonWriter.string()
.`object`()
.value("user_action", errorInfo.userAction.message)
.value("request", errorInfo.request)
.value("content_language", contentLanguageString)
.value("content_country", contentCountryString)
.value("app_language", appLanguage)
.value("service", errorInfo.getServiceName())
.value("package", packageName)
.value("version", BuildConfig.VERSION_NAME)
.value("os", osString)
.value("time", currentTimeStamp)
.array("exceptions", errorInfo.stackTraces.toList())
.value("user_comment", binding.errorCommentBox.getText().toString())
.end()
.done()
} catch (exception: Exception) {
Log.e(TAG, "Error while erroring: Could not build json", exception)
}
return ""
}
private fun buildMarkdown(): String {
try {
return buildString(1024) {
val userComment = binding.errorCommentBox.text.toString()
if (userComment.isNotEmpty()) {
appendLine(userComment)
}
// basic error info
appendLine("## Exception")
appendLine("* __User Action:__ ${errorInfo.userAction.message}")
appendLine("* __Request:__ ${errorInfo.request}")
appendLine("* __Content Country:__ $contentCountryString")
appendLine("* __Content Language:__ $contentLanguageString")
appendLine("* __App Language:__ $appLanguage")
appendLine("* __Service:__ ${errorInfo.getServiceName()}")
appendLine("* __Timestamp:__ $currentTimeStamp")
appendLine("* __Package:__ $packageName")
appendLine("* __Service:__ ${errorInfo.getServiceName()}")
appendLine("* __Version:__ ${BuildConfig.VERSION_NAME}")
appendLine("* __OS:__ $osString")
// Collapse all logs to a single paragraph when there are more than one
// to keep the GitHub issue clean.
if (errorInfo.stackTraces.size > 1) {
append("<details><summary><b>Exceptions (")
append(errorInfo.stackTraces.size)
append(")</b></summary><p>\n")
}
// add the logs
errorInfo.stackTraces.forEachIndexed { index, stacktrace ->
append("<details><summary><b>Crash log ")
if (errorInfo.stackTraces.size > 1) {
append(index + 1)
}
append("</b>")
append("</summary><p>\n")
append("\n```\n${stacktrace}\n```\n")
append("</details>\n")
}
// make sure to close everything
if (errorInfo.stackTraces.size > 1) {
append("</p></details>\n")
}
append("<hr>\n")
}
} catch (exception: Exception) {
Log.e(TAG, "Error while erroring: Could not build markdown", exception)
return ""
}
}
private fun addGuruMeditation() {
// just an easter egg
var text = binding.errorSorryView.text.toString()
text += "\n" + getString(R.string.guru_meditation)
binding.errorSorryView.text = text
}
companion object {
// LOG TAGS
private val TAG = ErrorActivity::class.java.toString()
// BUNDLE TAGS
const val ERROR_INFO = "error_info"
private const val ERROR_EMAIL_ADDRESS = "crashreport@newpipe.schabi.org"
private const val ERROR_EMAIL_SUBJECT = "Exception in "
private const val ERROR_GITHUB_ISSUE_URL = "https://github.com/TeamNewPipe/NewPipe/issues"
}
}

View File

@@ -134,8 +134,11 @@ class ErrorUtil {
)
)
NotificationManagerCompat.from(context)
.notify(ERROR_REPORT_NOTIFICATION_ID, notificationBuilder.build())
val notificationManager = NotificationManagerCompat.from(context)
if (notificationManager.areNotificationsEnabled()) {
notificationManager
.notify(ERROR_REPORT_NOTIFICATION_ID, notificationBuilder.build())
}
ContextCompat.getMainExecutor(context).execute {
// since the notification is silent, also show a toast, otherwise the user is confused

View File

@@ -126,6 +126,7 @@ public class ReCaptchaActivity extends AppCompatActivity {
}
@Override
@SuppressLint("MissingSuperCall") // saveCookiesAndFinish method handles back navigation
public void onBackPressed() {
saveCookiesAndFinish();
}

View File

@@ -600,6 +600,12 @@ class VideoDetailFragment :
override fun initListeners() {
super.initListeners()
// Workaround for #5600
// Forcefully catch click events uncaught by children because otherwise
// they will be caught by underlying view and "click through" will happen
binding.root.setOnClickListener { _ -> }
binding.root.setOnLongClickListener { _ -> true }
setOnClickListeners()
setOnLongClickListeners()

View File

@@ -161,34 +161,29 @@ public class ChannelFragment extends BaseStateFragment<ChannelInfo>
@Override
public boolean onMenuItemSelected(@NonNull final MenuItem item) {
switch (item.getItemId()) {
case R.id.menu_item_notify:
final boolean value = !item.isChecked();
item.setEnabled(false);
setNotify(value);
break;
case R.id.action_settings:
NavigationHelper.openSettings(requireContext());
break;
case R.id.menu_item_rss:
if (currentInfo != null) {
ShareUtils.openUrlInApp(requireContext(), currentInfo.getFeedUrl());
}
break;
case R.id.menu_item_openInBrowser:
if (currentInfo != null) {
ShareUtils.openUrlInBrowser(requireContext(),
currentInfo.getOriginalUrl());
}
break;
case R.id.menu_item_share:
if (currentInfo != null) {
ShareUtils.shareText(requireContext(), name,
currentInfo.getOriginalUrl(), currentInfo.getAvatars());
}
break;
default:
return false;
final int itemId = item.getItemId();
if (itemId == R.id.menu_item_notify) {
final boolean value = !item.isChecked();
item.setEnabled(false);
setNotify(value);
} else if (itemId == R.id.action_settings) {
NavigationHelper.openSettings(requireContext());
} else if (itemId == R.id.menu_item_rss) {
if (currentInfo != null) {
ShareUtils.openUrlInApp(requireContext(), currentInfo.getFeedUrl());
}
} else if (itemId == R.id.menu_item_openInBrowser) {
if (currentInfo != null) {
ShareUtils.openUrlInBrowser(requireContext(),
currentInfo.getOriginalUrl());
}
} else if (itemId == R.id.menu_item_share) {
if (currentInfo != null) {
ShareUtils.shareText(requireContext(), name,
currentInfo.getOriginalUrl(), currentInfo.getAvatars());
}
} else {
return false;
}
return true;
}

View File

@@ -231,35 +231,30 @@ public class PlaylistFragment extends BaseListInfoFragment<StreamInfoItem, Playl
@Override
public boolean onOptionsItemSelected(final MenuItem item) {
switch (item.getItemId()) {
case R.id.action_settings:
NavigationHelper.openSettings(requireContext());
break;
case R.id.menu_item_openInBrowser:
ShareUtils.openUrlInBrowser(requireContext(), url);
break;
case R.id.menu_item_share:
ShareUtils.shareText(requireContext(), name, url,
currentInfo == null ? List.of() : currentInfo.getThumbnails());
break;
case R.id.menu_item_bookmark:
onBookmarkClicked();
break;
case R.id.menu_item_append_playlist:
if (currentInfo != null) {
disposables.add(PlaylistDialog.createCorrespondingDialog(
getContext(),
getPlayQueue()
.getStreams()
.stream()
.map(StreamEntity::new)
.collect(Collectors.toList()),
dialog -> dialog.show(getFM(), TAG)
));
}
break;
default:
return super.onOptionsItemSelected(item);
final int itemId = item.getItemId();
if (itemId == R.id.action_settings) {
NavigationHelper.openSettings(requireContext());
} else if (itemId == R.id.menu_item_openInBrowser) {
ShareUtils.openUrlInBrowser(requireContext(), url);
} else if (itemId == R.id.menu_item_share) {
ShareUtils.shareText(requireContext(), name, url,
currentInfo == null ? List.of() : currentInfo.getThumbnails());
} else if (itemId == R.id.menu_item_bookmark) {
onBookmarkClicked();
} else if (itemId == R.id.menu_item_append_playlist) {
if (currentInfo != null) {
disposables.add(PlaylistDialog.createCorrespondingDialog(
getContext(),
getPlayQueue()
.getStreams()
.stream()
.map(StreamEntity::new)
.collect(Collectors.toList()),
dialog -> dialog.show(getFM(), TAG)
));
}
} else {
return super.onOptionsItemSelected(item);
}
return true;
}

View File

@@ -1013,7 +1013,11 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
Log.d(TAG, "handleSuggestions() called with: suggestions = [" + suggestions + "]");
}
suggestionListAdapter.submitList(suggestions,
() -> searchBinding.suggestionsList.scrollToPosition(0));
() -> {
if (searchBinding != null) {
searchBinding.suggestionsList.scrollToPosition(0);
}
});
if (suggestionsPanelVisible && isErrorPanelVisible()) {
hideLoading();

View File

@@ -1,125 +0,0 @@
package org.schabi.newpipe.info_list;
import android.content.Context;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.extractor.channel.ChannelInfoItem;
import org.schabi.newpipe.extractor.comments.CommentsInfoItem;
import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem;
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
import org.schabi.newpipe.info_list.holder.ChannelInfoItemHolder;
import org.schabi.newpipe.info_list.holder.ChannelMiniInfoItemHolder;
import org.schabi.newpipe.info_list.holder.InfoItemHolder;
import org.schabi.newpipe.info_list.holder.PlaylistInfoItemHolder;
import org.schabi.newpipe.info_list.holder.PlaylistMiniInfoItemHolder;
import org.schabi.newpipe.info_list.holder.StreamInfoItemHolder;
import org.schabi.newpipe.info_list.holder.StreamMiniInfoItemHolder;
import org.schabi.newpipe.local.history.HistoryRecordManager;
import org.schabi.newpipe.util.OnClickGesture;
/*
* Created by Christian Schabesberger on 26.09.16.
* <p>
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
* InfoItemBuilder.java is part of NewPipe.
* </p>
* <p>
* NewPipe is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
* </p>
* <p>
* NewPipe is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
* </p>
* <p>
* You should have received a copy of the GNU General Public License
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
* </p>
*/
public class InfoItemBuilder {
private final Context context;
private OnClickGesture<StreamInfoItem> onStreamSelectedListener;
private OnClickGesture<ChannelInfoItem> onChannelSelectedListener;
private OnClickGesture<PlaylistInfoItem> onPlaylistSelectedListener;
private OnClickGesture<CommentsInfoItem> onCommentsSelectedListener;
public InfoItemBuilder(final Context context) {
this.context = context;
}
public View buildView(@NonNull final ViewGroup parent, @NonNull final InfoItem infoItem,
final HistoryRecordManager historyRecordManager) {
return buildView(parent, infoItem, historyRecordManager, false);
}
public View buildView(@NonNull final ViewGroup parent, @NonNull final InfoItem infoItem,
final HistoryRecordManager historyRecordManager,
final boolean useMiniVariant) {
final InfoItemHolder holder =
holderFromInfoType(parent, infoItem.getInfoType(), useMiniVariant);
holder.updateFromItem(infoItem, historyRecordManager);
return holder.itemView;
}
private InfoItemHolder holderFromInfoType(@NonNull final ViewGroup parent,
@NonNull final InfoItem.InfoType infoType,
final boolean useMiniVariant) {
return switch (infoType) {
case STREAM -> useMiniVariant ? new StreamMiniInfoItemHolder(this, parent)
: new StreamInfoItemHolder(this, parent);
case CHANNEL -> useMiniVariant ? new ChannelMiniInfoItemHolder(this, parent)
: new ChannelInfoItemHolder(this, parent);
case PLAYLIST -> useMiniVariant ? new PlaylistMiniInfoItemHolder(this, parent)
: new PlaylistInfoItemHolder(this, parent);
case COMMENT ->
throw new IllegalArgumentException("Comments should be rendered using Compose");
};
}
public Context getContext() {
return context;
}
public OnClickGesture<StreamInfoItem> getOnStreamSelectedListener() {
return onStreamSelectedListener;
}
public void setOnStreamSelectedListener(final OnClickGesture<StreamInfoItem> listener) {
this.onStreamSelectedListener = listener;
}
public OnClickGesture<ChannelInfoItem> getOnChannelSelectedListener() {
return onChannelSelectedListener;
}
public void setOnChannelSelectedListener(final OnClickGesture<ChannelInfoItem> listener) {
this.onChannelSelectedListener = listener;
}
public OnClickGesture<PlaylistInfoItem> getOnPlaylistSelectedListener() {
return onPlaylistSelectedListener;
}
public void setOnPlaylistSelectedListener(final OnClickGesture<PlaylistInfoItem> listener) {
this.onPlaylistSelectedListener = listener;
}
public OnClickGesture<CommentsInfoItem> getOnCommentsSelectedListener() {
return onCommentsSelectedListener;
}
public void setOnCommentsSelectedListener(
final OnClickGesture<CommentsInfoItem> onCommentsSelectedListener) {
this.onCommentsSelectedListener = onCommentsSelectedListener;
}
}

View File

@@ -0,0 +1,20 @@
/*
* SPDX-FileCopyrightText: 2016-2026 NewPipe contributors <https://newpipe.net>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package org.schabi.newpipe.info_list
import android.content.Context
import org.schabi.newpipe.extractor.channel.ChannelInfoItem
import org.schabi.newpipe.extractor.comments.CommentsInfoItem
import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem
import org.schabi.newpipe.extractor.stream.StreamInfoItem
import org.schabi.newpipe.util.OnClickGesture
class InfoItemBuilder(val context: Context) {
var onStreamSelectedListener: OnClickGesture<StreamInfoItem>? = null
var onChannelSelectedListener: OnClickGesture<ChannelInfoItem>? = null
var onPlaylistSelectedListener: OnClickGesture<PlaylistInfoItem>? = null
var onCommentsSelectedListener: OnClickGesture<CommentsInfoItem>? = null
}

View File

@@ -129,8 +129,7 @@ class FeedViewModel(
fun setSaveShowPlayedItems(showPlayedItems: Boolean) {
this.showPlayedItems.onNext(showPlayedItems)
PreferenceManager.getDefaultSharedPreferences(application).edit {
this.putBoolean(application.getString(R.string.feed_show_watched_items_key), showPlayedItems)
this.apply()
putBoolean(application.getString(R.string.feed_show_watched_items_key), showPlayedItems)
}
}
@@ -139,8 +138,7 @@ class FeedViewModel(
fun setSaveShowPartiallyPlayedItems(showPartiallyPlayedItems: Boolean) {
this.showPartiallyPlayedItems.onNext(showPartiallyPlayedItems)
PreferenceManager.getDefaultSharedPreferences(application).edit {
this.putBoolean(application.getString(R.string.feed_show_partially_watched_items_key), showPartiallyPlayedItems)
this.apply()
putBoolean(application.getString(R.string.feed_show_partially_watched_items_key), showPartiallyPlayedItems)
}
}
@@ -149,8 +147,7 @@ class FeedViewModel(
fun setSaveShowFutureItems(showFutureItems: Boolean) {
this.showFutureItems.onNext(showFutureItems)
PreferenceManager.getDefaultSharedPreferences(application).edit {
this.putBoolean(application.getString(R.string.feed_show_future_items_key), showFutureItems)
this.apply()
putBoolean(application.getString(R.string.feed_show_future_items_key), showFutureItems)
}
}

View File

@@ -82,7 +82,9 @@ class NotificationHelper(val context: Context) {
// Show individual stream notifications, set channel icon only if there is actually one
showStreamNotifications(newStreams, data.serviceId, avatarIcon)
// Show summary notification
manager.notify(data.pseudoId, summaryBuilder.build())
if (manager.areNotificationsEnabled()) {
manager.notify(data.pseudoId, summaryBuilder.build())
}
}
private fun showStreamNotifications(
@@ -90,9 +92,12 @@ class NotificationHelper(val context: Context) {
serviceId: Int,
channelIcon: Bitmap?
) {
for (stream in newStreams) {
val notification = createStreamNotification(stream, serviceId, channelIcon)
manager.notify(stream.url.hashCode(), notification)
if (manager.areNotificationsEnabled()) {
newStreams.forEach { stream ->
val notification =
createStreamNotification(stream, serviceId, channelIcon)
manager.notify(stream.url.hashCode(), notification)
}
}
}

View File

@@ -185,7 +185,9 @@ class FeedLoadService : Service() {
}
}
notificationManager.notify(NOTIFICATION_ID, notificationBuilder.build())
if (notificationManager.areNotificationsEnabled()) {
notificationManager.notify(NOTIFICATION_ID, notificationBuilder.build())
}
}
// /////////////////////////////////////////////////////////////////////////

View File

@@ -328,7 +328,7 @@ class FeedGroupDialog : DialogFragment(), BackPressable {
groupIcon = feedGroupEntity?.icon
groupSortOrder = feedGroupEntity?.sortOrder ?: -1
val feedGroupIcon = if (selectedIcon == null) icon else selectedIcon!!
val feedGroupIcon = selectedIcon ?: icon
feedGroupCreateBinding.iconPreview.setImageResource(feedGroupIcon.getDrawableRes())
if (feedGroupCreateBinding.groupNameInput.text.isNullOrBlank()) {
@@ -507,7 +507,7 @@ class FeedGroupDialog : DialogFragment(), BackPressable {
private fun hideKeyboardSearch() {
inputMethodManager.hideSoftInputFromWindow(
searchLayoutBinding.toolbarSearchEditText.windowToken,
InputMethodManager.RESULT_UNCHANGED_SHOWN
InputMethodManager.HIDE_NOT_ALWAYS
)
searchLayoutBinding.toolbarSearchEditText.clearFocus()
}
@@ -524,7 +524,7 @@ class FeedGroupDialog : DialogFragment(), BackPressable {
private fun hideKeyboard() {
inputMethodManager.hideSoftInputFromWindow(
feedGroupCreateBinding.groupNameInput.windowToken,
InputMethodManager.RESULT_UNCHANGED_SHOWN
InputMethodManager.HIDE_NOT_ALWAYS
)
feedGroupCreateBinding.groupNameInput.clearFocus()
}

View File

@@ -167,39 +167,39 @@ public final class PlayQueueActivity extends AppCompatActivity
@Override
public boolean onOptionsItemSelected(final MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home:
finish();
return true;
case R.id.action_settings:
NavigationHelper.openSettings(this);
return true;
case R.id.action_append_playlist:
PlaylistDialog.showForPlayQueue(player, getSupportFragmentManager());
return true;
case R.id.action_playback_speed:
openPlaybackParameterDialog();
return true;
case R.id.action_mute:
player.toggleMute();
return true;
case R.id.action_system_audio:
startActivity(new Intent(Settings.ACTION_SOUND_SETTINGS));
return true;
case R.id.action_switch_main:
final int itemId = item.getItemId();
if (itemId == android.R.id.home) {
finish();
return true;
} else if (itemId == R.id.action_settings) {
NavigationHelper.openSettings(this);
return true;
} else if (itemId == R.id.action_append_playlist) {
PlaylistDialog.showForPlayQueue(player, getSupportFragmentManager());
return true;
} else if (itemId == R.id.action_playback_speed) {
openPlaybackParameterDialog();
return true;
} else if (itemId == R.id.action_mute) {
player.toggleMute();
return true;
} else if (itemId == R.id.action_system_audio) {
startActivity(new Intent(Settings.ACTION_SOUND_SETTINGS));
return true;
} else if (itemId == R.id.action_switch_main) {
this.player.setRecovery();
NavigationHelper.playOnMainPlayer(this, player.getPlayQueue(), true);
return true;
} else if (itemId == R.id.action_switch_popup) {
if (PermissionHelper.isPopupEnabledElseAsk(this)) {
this.player.setRecovery();
NavigationHelper.playOnMainPlayer(this, player.getPlayQueue(), true);
return true;
case R.id.action_switch_popup:
if (PermissionHelper.isPopupEnabledElseAsk(this)) {
this.player.setRecovery();
NavigationHelper.playOnPopupPlayer(this, player.getPlayQueue(), true);
}
return true;
case R.id.action_switch_background:
this.player.setRecovery();
NavigationHelper.playOnBackgroundPlayer(this, player.getPlayQueue(), true);
return true;
NavigationHelper.playOnPopupPlayer(this, player.getPlayQueue(), true);
}
return true;
} else if (itemId == R.id.action_switch_background) {
this.player.setRecovery();
NavigationHelper.playOnBackgroundPlayer(this, player.getPlayQueue(), true);
return true;
}
if (item.getGroupId() == MENU_ID_AUDIO_TRACK) {

View File

@@ -47,6 +47,9 @@ abstract class BasePlayerGestureListener(
startMultiDoubleTap(event)
} else if (portion === DisplayPortion.MIDDLE) {
player.playPause()
if (player.isPlaying) {
playerUi.hideControls(0, 0)
}
}
}

View File

@@ -49,12 +49,12 @@ import java.text.DecimalFormatSymbols;
import java.text.NumberFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
public final class PlayerHelper {
private static final FormattersProvider FORMATTERS_PROVIDER = new FormattersProvider();
@@ -87,11 +87,11 @@ public final class PlayerHelper {
}
@NonNull
public static String getTimeString(final int milliSeconds) {
final int seconds = (milliSeconds % 60000) / 1000;
final int minutes = (milliSeconds % 3600000) / 60000;
final int hours = (milliSeconds % 86400000) / 3600000;
final int days = (milliSeconds % (86400000 * 7)) / 86400000;
public static String getTimeString(final long milliSeconds) {
final long seconds = (milliSeconds % 60000) / 1000;
final long minutes = (milliSeconds % 3600000) / 60000;
final long hours = (milliSeconds % 86400000) / 3600000;
final long days = (milliSeconds % (86400000 * 7)) / 86400000;
final Formatters formatters = FORMATTERS_PROVIDER.formatters();
if (days > 0) {
@@ -174,10 +174,9 @@ public final class PlayerHelper {
@Nullable
public static PlayQueue autoQueueOf(@NonNull final StreamInfo info,
@NonNull final List<PlayQueueItem> existingItems) {
final Set<String> urls = new HashSet<>(existingItems.size());
for (final PlayQueueItem item : existingItems) {
urls.add(item.getUrl());
}
final Set<String> urls = existingItems.stream()
.map(PlayQueueItem::getUrl)
.collect(Collectors.toUnmodifiableSet());
final List<InfoItem> relatedItems = info.getRelatedItems();
if (Utils.isNullOrEmpty(relatedItems)) {

View File

@@ -22,7 +22,7 @@ internal fun infoItemTypeToString(type: InfoType): String {
InfoType.STREAM -> ID_STREAM
InfoType.PLAYLIST -> ID_PLAYLIST
InfoType.CHANNEL -> ID_CHANNEL
else -> throw IllegalStateException("Unexpected value: $type")
else -> error("Unexpected value: $type")
}
}
@@ -31,7 +31,7 @@ internal fun infoItemTypeFromString(type: String): InfoType {
ID_STREAM -> InfoType.STREAM
ID_PLAYLIST -> InfoType.PLAYLIST
ID_CHANNEL -> InfoType.CHANNEL
else -> throw IllegalStateException("Unexpected value: $type")
else -> error("Unexpected value: $type")
}
}

View File

@@ -82,11 +82,11 @@ internal class PackageValidator(context: Context) {
// Build the caller info for the rest of the checks here.
val callerPackageInfo = buildCallerInfo(callingPackage)
?: throw IllegalStateException("Caller wasn't found in the system?")
?: error("Caller wasn't found in the system?")
// Verify that things aren't ... broken. (This test should always pass.)
if (callerPackageInfo.uid != callingUid) {
throw IllegalStateException("Caller's package UID doesn't match caller's actual UID?")
check(callerPackageInfo.uid == callingUid) {
"Caller's package UID doesn't match caller's actual UID?"
}
val callerSignature = callerPackageInfo.signature
@@ -202,7 +202,7 @@ internal class PackageValidator(context: Context) {
*/
private fun getSystemSignature(): String = getPackageInfo(ANDROID_PLATFORM)?.let { platformInfo ->
getSignature(platformInfo)
} ?: throw IllegalStateException("Platform signature not found")
} ?: error("Platform signature not found")
/**
* Creates a SHA-256 signature given a certificate byte array.

View File

@@ -72,7 +72,9 @@ public final class NotificationUtil {
notificationBuilder = createNotification();
}
updateNotification();
notificationManager.notify(NOTIFICATION_ID, notificationBuilder.build());
if (notificationManager.areNotificationsEnabled()) {
notificationManager.notify(NOTIFICATION_ID, notificationBuilder.build());
}
}
public synchronized void updateThumbnail() {
@@ -84,7 +86,9 @@ public final class NotificationUtil {
}
setLargeIcon(notificationBuilder);
notificationManager.notify(NOTIFICATION_ID, notificationBuilder.build());
if (notificationManager.areNotificationsEnabled()) {
notificationManager.notify(NOTIFICATION_ID, notificationBuilder.build());
}
}
}

View File

@@ -5,8 +5,8 @@ import androidx.annotation.NonNull;
import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
public final class SinglePlayQueue extends PlayQueue {
public SinglePlayQueue(final StreamInfoItem item) {
@@ -29,11 +29,7 @@ public final class SinglePlayQueue extends PlayQueue {
}
private static List<PlayQueueItem> playQueueItemsOf(@NonNull final List<StreamInfoItem> items) {
final List<PlayQueueItem> playQueueItems = new ArrayList<>(items.size());
for (final StreamInfoItem item : items) {
playQueueItems.add(new PlayQueueItem(item));
}
return playQueueItems;
return items.stream().map(PlayQueueItem::new).collect(Collectors.toList());
}
@Override

View File

@@ -77,6 +77,7 @@ import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Collectors;
public final class MainPlayerUi extends VideoPlayerUi implements View.OnLayoutChangeListener {
private static final String TAG = MainPlayerUi.class.getSimpleName();
@@ -749,13 +750,13 @@ public final class MainPlayerUi extends VideoPlayerUi implements View.OnLayoutCh
}
private int getNearestStreamSegmentPosition(final long playbackPosition) {
int nearestPosition = 0;
final List<StreamSegment> segments = player.getCurrentStreamInfo()
.map(StreamInfo::getStreamSegments)
.orElse(Collections.emptyList());
for (int i = 0; i < segments.size(); i++) {
if (segments.get(i).getStartTimeSeconds() * 1000L > playbackPosition) {
int nearestPosition = 0;
for (final var segment : segments) {
if (segment.getStartTimeSeconds() * 1000L > playbackPosition) {
break;
}
nearestPosition++;
@@ -816,22 +817,13 @@ public final class MainPlayerUi extends VideoPlayerUi implements View.OnLayoutCh
}
final int currentStream = playQueue.getIndex();
int before = 0;
int after = 0;
final List<PlayQueueItem> streams = playQueue.getStreams();
final int nStreams = streams.size();
for (int i = 0; i < nStreams; i++) {
if (i < currentStream) {
before += streams.get(i).getDuration();
} else {
after += streams.get(i).getDuration();
}
}
final long before = streams.subList(0, currentStream).stream()
.collect(Collectors.summingLong(PlayQueueItem::getDuration)) * 1000;
before *= 1000;
after *= 1000;
final long after = streams.subList(currentStream, streams.size()).stream()
.collect(Collectors.summingLong(PlayQueueItem::getDuration)) * 1000;
binding.itemsListHeaderDuration.setText(
String.format("%s/%s",

View File

@@ -1,58 +0,0 @@
package org.schabi.newpipe.settings.export;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectStreamClass;
import java.util.Set;
/**
* An {@link ObjectInputStream} that only allows preferences-related types to be deserialized, to
* prevent injections. The only allowed types are: all primitive types, all boxed primitive types,
* null, strings. HashMap, HashSet and arrays of previously defined types are also allowed. Sources:
* <a href="https://wiki.sei.cmu.edu/confluence/display/java/SER00-J.+Enable+serialization+compatibility+during+class+evolution">
* cmu.edu
* </a>,
* <a href="https://cheatsheetseries.owasp.org/cheatsheets/Deserialization_Cheat_Sheet.html#harden-your-own-javaioobjectinputstream">
* OWASP cheatsheet
* </a>,
* <a href="https://commons.apache.org/proper/commons-io/apidocs/src-html/org/apache/commons/io/serialization/ValidatingObjectInputStream.html#line-118">
* Apache's {@code ValidatingObjectInputStream}
* </a>
*/
public class PreferencesObjectInputStream extends ObjectInputStream {
/**
* Primitive types, strings and other built-in types do not pass through resolveClass() but
* instead have a custom encoding; see
* <a href="https://docs.oracle.com/javase/6/docs/platform/serialization/spec/protocol.html#10152">
* official docs</a>.
*/
private static final Set<String> CLASS_WHITELIST = Set.of(
"java.lang.Boolean",
"java.lang.Byte",
"java.lang.Character",
"java.lang.Short",
"java.lang.Integer",
"java.lang.Long",
"java.lang.Float",
"java.lang.Double",
"java.lang.Void",
"java.util.HashMap",
"java.util.HashSet"
);
public PreferencesObjectInputStream(final InputStream in) throws IOException {
super(in);
}
@Override
protected Class<?> resolveClass(final ObjectStreamClass desc)
throws ClassNotFoundException, IOException {
if (CLASS_WHITELIST.contains(desc.getName())) {
return super.resolveClass(desc);
} else {
throw new ClassNotFoundException("Class not allowed: " + desc.getName());
}
}
}

View File

@@ -0,0 +1,52 @@
/*
* SPDX-FileCopyrightText: 2024-2026 NewPipe contributors <https://newpipe.net>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package org.schabi.newpipe.settings.export
import java.io.IOException
import java.io.InputStream
import java.io.ObjectInputStream
import java.io.ObjectStreamClass
/**
* An [ObjectInputStream] that only allows preferences-related types to be deserialized, to
* prevent injections. The only allowed types are: all primitive types, all boxed primitive types,
* null, strings. HashMap, HashSet and arrays of previously defined types are also allowed. Sources:
* [cmu.edu](https://wiki.sei.cmu.edu/confluence/display/java/SER00-J.+Enable+serialization+compatibility+during+class+evolution) * ,
* [OWASP cheatsheet](https://cheatsheetseries.owasp.org/cheatsheets/Deserialization_Cheat_Sheet.html#harden-your-own-javaioobjectinputstream) * ,
* [Apache's `ValidatingObjectInputStream`](https://commons.apache.org/proper/commons-io/apidocs/src-html/org/apache/commons/io/serialization/ValidatingObjectInputStream.html#line-118) *
*/
class PreferencesObjectInputStream(stream: InputStream) : ObjectInputStream(stream) {
@Throws(ClassNotFoundException::class, IOException::class)
override fun resolveClass(desc: ObjectStreamClass): Class<*> {
if (desc.name in CLASS_WHITELIST) {
return super.resolveClass(desc)
} else {
throw ClassNotFoundException("Class not allowed: $desc.name")
}
}
companion object {
/**
* Primitive types, strings and other built-in types do not pass through resolveClass() but
* instead have a custom encoding; see
* [
* official docs](https://docs.oracle.com/javase/6/docs/platform/serialization/spec/protocol.html#10152).
*/
private val CLASS_WHITELIST = setOf<String>(
"java.lang.Boolean",
"java.lang.Byte",
"java.lang.Character",
"java.lang.Short",
"java.lang.Integer",
"java.lang.Long",
"java.lang.Float",
"java.lang.Double",
"java.lang.Void",
"java.util.HashMap",
"java.util.HashSet"
)
}
}

View File

@@ -26,14 +26,13 @@ data class PreferenceSearchItem(
val breadcrumbs: String,
@XmlRes val searchIndexItemResId: Int
) {
val allRelevantSearchFields: List<String>
get() = listOf(title, summary, entries, breadcrumbs)
fun hasData(): Boolean {
return !key.isEmpty() && !title.isEmpty()
}
fun getAllRelevantSearchFields(): MutableList<String?> {
return mutableListOf(title, summary, entries, breadcrumbs)
}
override fun toString(): String {
return "PreferenceItem: $title $summary $key"
}

View File

@@ -9,8 +9,9 @@ import com.grack.nanojson.JsonParserException;
import com.grack.nanojson.JsonStringWriter;
import com.grack.nanojson.JsonWriter;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
/**
* Class to get a JSON representation of a list of tabs, and the other way around.
@@ -44,39 +45,25 @@ public final class TabsJsonHelper {
return getDefaultTabs();
}
final List<Tab> returnTabs = new ArrayList<>();
final JsonObject outerJsonObject;
try {
outerJsonObject = JsonParser.object().from(tabsJson);
final JsonObject outerJsonObject = JsonParser.object().from(tabsJson);
if (!outerJsonObject.has(JSON_TABS_ARRAY_KEY)) {
throw new InvalidJsonException("JSON doesn't contain \"" + JSON_TABS_ARRAY_KEY
+ "\" array");
}
final JsonArray tabsArray = outerJsonObject.getArray(JSON_TABS_ARRAY_KEY);
final JsonArray tabsArray = outerJsonObject.getArray(JSON_TABS_ARRAY_KEY, null);
for (final Object o : tabsArray) {
if (!(o instanceof JsonObject)) {
continue;
}
final var returnTabs = tabsArray.streamAsJsonObjects()
.map(Tab::from)
.filter(Objects::nonNull)
.collect(Collectors.toUnmodifiableList());
final Tab tab = Tab.from((JsonObject) o);
if (tab != null) {
returnTabs.add(tab);
}
}
return returnTabs.isEmpty() ? getDefaultTabs() : returnTabs;
} catch (final JsonParserException e) {
throw new InvalidJsonException(e);
}
if (returnTabs.isEmpty()) {
return getDefaultTabs();
}
return returnTabs;
}
/**

View File

@@ -1,51 +0,0 @@
package org.schabi.newpipe.util;
import android.content.Context;
import android.content.SharedPreferences;
import androidx.preference.PreferenceManager;
import org.schabi.newpipe.R;
/**
* For preferences with dependencies and multiple use case,
* this class can be used to reduce the lines of code.
*/
public final class DependentPreferenceHelper {
private DependentPreferenceHelper() {
// no instance
}
/**
* Option `Resume playback` depends on `Watch history`, this method can be used to retrieve if
* `Resume playback` and its dependencies are all enabled.
*
* @param context the Android context
* @return returns true if `Resume playback` and `Watch history` are both enabled
*/
public static boolean getResumePlaybackEnabled(final Context context) {
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
return prefs.getBoolean(context.getString(
R.string.enable_watch_history_key), true)
&& prefs.getBoolean(context.getString(
R.string.enable_playback_resume_key), true);
}
/**
* Option `Position in lists` depends on `Watch history`, this method can be used to retrieve if
* `Position in lists` and its dependencies are all enabled.
*
* @param context the Android context
* @return returns true if `Positions in lists` and `Watch history` are both enabled
*/
public static boolean getPositionsInListsEnabled(final Context context) {
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
return prefs.getBoolean(context.getString(
R.string.enable_watch_history_key), true)
&& prefs.getBoolean(context.getString(
R.string.enable_playback_state_lists_key), true);
}
}

View File

@@ -0,0 +1,46 @@
/*
* SPDX-FileCopyrightText: 2023-2026 NewPipe contributors <https://newpipe.net>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package org.schabi.newpipe.util
import android.content.Context
import androidx.preference.PreferenceManager
import org.schabi.newpipe.R
/**
* For preferences with dependencies and multiple use case,
* this class can be used to reduce the lines of code.
*/
object DependentPreferenceHelper {
/**
* Option `Resume playback` depends on `Watch history`, this method can be used to retrieve if
* `Resume playback` and its dependencies are all enabled.
*
* @param context the Android context
* @return returns true if `Resume playback` and `Watch history` are both enabled
*/
@JvmStatic
fun getResumePlaybackEnabled(context: Context): Boolean {
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
return prefs.getBoolean(context.getString(R.string.enable_watch_history_key), true) &&
prefs.getBoolean(context.getString(R.string.enable_playback_resume_key), true)
}
/**
* Option `Position in lists` depends on `Watch history`, this method can be used to retrieve if
* `Position in lists` and its dependencies are all enabled.
*
* @param context the Android context
* @return returns true if `Positions in lists` and `Watch history` are both enabled
*/
@JvmStatic
fun getPositionsInListsEnabled(context: Context): Boolean {
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
return prefs.getBoolean(context.getString(R.string.enable_watch_history_key), true) &&
prefs.getBoolean(context.getString(R.string.enable_playback_state_lists_key), true)
}
}

View File

@@ -48,7 +48,7 @@ public final class KeyboardUtil {
final InputMethodManager imm = ContextCompat.getSystemService(activity,
InputMethodManager.class);
imm.hideSoftInputFromWindow(editText.getWindowToken(),
InputMethodManager.RESULT_UNCHANGED_SHOWN);
InputMethodManager.HIDE_NOT_ALWAYS);
editText.clearFocus();
}

View File

@@ -1,61 +0,0 @@
package org.schabi.newpipe.util;
import android.content.Context;
import android.text.Selection;
import android.text.Spannable;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.schabi.newpipe.util.external_communication.ShareUtils;
import org.schabi.newpipe.views.NewPipeEditText;
import org.schabi.newpipe.views.NewPipeTextView;
public final class NewPipeTextViewHelper {
private NewPipeTextViewHelper() {
}
/**
* Share the selected text of {@link NewPipeTextView NewPipeTextViews} and
* {@link NewPipeEditText NewPipeEditTexts} with
* {@link ShareUtils#shareText(Context, String, String)}.
*
* <p>
* This allows EMUI users to get the Android share sheet instead of the EMUI share sheet when
* using the {@code Share} command of the popup menu which appears when selecting text.
* </p>
*
* @param textView the {@link TextView} on which sharing the selected text. It should be a
* {@link NewPipeTextView} or a {@link NewPipeEditText} (even if
* {@link TextView standard TextViews} are supported).
*/
public static void shareSelectedTextWithShareUtils(@NonNull final TextView textView) {
final CharSequence textViewText = textView.getText();
shareSelectedTextIfNotNullAndNotEmpty(textView, getSelectedText(textView, textViewText));
if (textViewText instanceof Spannable) {
Selection.setSelection((Spannable) textViewText, textView.getSelectionEnd());
}
}
@Nullable
private static CharSequence getSelectedText(@NonNull final TextView textView,
@Nullable final CharSequence text) {
if (!textView.hasSelection() || text == null) {
return null;
}
final int start = textView.getSelectionStart();
final int end = textView.getSelectionEnd();
return String.valueOf(start > end ? text.subSequence(end, start)
: text.subSequence(start, end));
}
private static void shareSelectedTextIfNotNullAndNotEmpty(
@NonNull final TextView textView,
@Nullable final CharSequence selectedText) {
if (selectedText != null && selectedText.length() != 0) {
ShareUtils.shareText(textView.getContext(), "", selectedText.toString());
}
}
}

View File

@@ -0,0 +1,60 @@
/*
* SPDX-FileCopyrightText: 2021-2026 NewPipe contributors <https://newpipe.net>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package org.schabi.newpipe.util
import android.text.Selection
import android.text.Spannable
import android.widget.TextView
import org.schabi.newpipe.util.external_communication.ShareUtils
object NewPipeTextViewHelper {
/**
* Share the selected text of [NewPipeTextViews][org.schabi.newpipe.views.NewPipeTextView] and
* [NewPipeEditTexts][org.schabi.newpipe.views.NewPipeEditText] with
* [ShareUtils.shareText].
*
*
*
* This allows EMUI users to get the Android share sheet instead of the EMUI share sheet when
* using the `Share` command of the popup menu which appears when selecting text.
*
*
* @param textView the [TextView] on which sharing the selected text. It should be a
* [org.schabi.newpipe.views.NewPipeTextView] or a [org.schabi.newpipe.views.NewPipeEditText]
* (even if [standard TextViews][TextView] are supported).
*/
@JvmStatic
fun shareSelectedTextWithShareUtils(textView: TextView) {
val textViewText = textView.getText()
shareSelectedTextIfNotNullAndNotEmpty(textView, getSelectedText(textView, textViewText))
if (textViewText is Spannable) {
Selection.setSelection(textViewText, textView.selectionEnd)
}
}
private fun getSelectedText(textView: TextView, text: CharSequence?): CharSequence? {
if (!textView.hasSelection() || text == null) {
return null
}
val start = textView.selectionStart
val end = textView.selectionEnd
return if (start > end) {
text.subSequence(end, start)
} else {
text.subSequence(start, end)
}
}
private fun shareSelectedTextIfNotNullAndNotEmpty(
textView: TextView,
selectedText: CharSequence?
) {
if (!selectedText.isNullOrEmpty()) {
ShareUtils.shareText(textView.context, "", selectedText.toString())
}
}
}

View File

@@ -1,69 +0,0 @@
package org.schabi.newpipe.util;
import android.content.Context;
import android.content.SharedPreferences;
import androidx.preference.PreferenceManager;
import com.grack.nanojson.JsonArray;
import com.grack.nanojson.JsonObject;
import com.grack.nanojson.JsonParser;
import com.grack.nanojson.JsonParserException;
import com.grack.nanojson.JsonStringWriter;
import com.grack.nanojson.JsonWriter;
import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.ServiceList;
import org.schabi.newpipe.extractor.services.peertube.PeertubeInstance;
import java.util.ArrayList;
import java.util.List;
public final class PeertubeHelper {
private PeertubeHelper() { }
public static List<PeertubeInstance> getInstanceList(final Context context) {
final SharedPreferences sharedPreferences = PreferenceManager
.getDefaultSharedPreferences(context);
final String savedInstanceListKey = context.getString(R.string.peertube_instance_list_key);
final String savedJson = sharedPreferences.getString(savedInstanceListKey, null);
if (null == savedJson) {
return List.of(getCurrentInstance());
}
try {
final JsonArray array = JsonParser.object().from(savedJson).getArray("instances");
final List<PeertubeInstance> result = new ArrayList<>();
for (final Object o : array) {
if (o instanceof JsonObject) {
final JsonObject instance = (JsonObject) o;
final String name = instance.getString("name");
final String url = instance.getString("url");
result.add(new PeertubeInstance(url, name));
}
}
return result;
} catch (final JsonParserException e) {
return List.of(getCurrentInstance());
}
}
public static PeertubeInstance selectInstance(final PeertubeInstance instance,
final Context context) {
final SharedPreferences sharedPreferences = PreferenceManager
.getDefaultSharedPreferences(context);
final String selectedInstanceKey =
context.getString(R.string.peertube_selected_instance_key);
final JsonStringWriter jsonWriter = JsonWriter.string().object();
jsonWriter.value("name", instance.getName());
jsonWriter.value("url", instance.getUrl());
final String jsonToSave = jsonWriter.end().done();
sharedPreferences.edit().putString(selectedInstanceKey, jsonToSave).apply();
ServiceList.PeerTube.setInstance(instance);
return instance;
}
public static PeertubeInstance getCurrentInstance() {
return ServiceList.PeerTube.getInstance();
}
}

View File

@@ -0,0 +1,52 @@
/*
* SPDX-FileCopyrightText: 2019-2026 NewPipe contributors <https://newpipe.net>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package org.schabi.newpipe.util
import android.content.Context
import androidx.core.content.edit
import androidx.preference.PreferenceManager
import com.grack.nanojson.JsonObject
import com.grack.nanojson.JsonParser
import com.grack.nanojson.JsonWriter
import org.schabi.newpipe.R
import org.schabi.newpipe.extractor.ServiceList
import org.schabi.newpipe.extractor.services.peertube.PeertubeInstance
object PeertubeHelper {
@JvmStatic
val currentInstance: PeertubeInstance
get() = ServiceList.PeerTube.instance
@JvmStatic
fun getInstanceList(context: Context): List<PeertubeInstance> {
val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
val savedInstanceListKey = context.getString(R.string.peertube_instance_list_key)
val savedJson = sharedPreferences.getString(savedInstanceListKey, null)
?: return listOf(currentInstance)
return runCatching {
JsonParser.`object`().from(savedJson).getArray("instances")
.filterIsInstance<JsonObject>()
.map { PeertubeInstance(it.getString("url"), it.getString("name")) }
}.getOrDefault(listOf(currentInstance))
}
@JvmStatic
fun selectInstance(instance: PeertubeInstance, context: Context): PeertubeInstance {
val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
val selectedInstanceKey = context.getString(R.string.peertube_selected_instance_key)
val jsonWriter = JsonWriter.string().`object`()
jsonWriter.value("name", instance.name)
jsonWriter.value("url", instance.url)
val jsonToSave = jsonWriter.end().done()
sharedPreferences.edit { putString(selectedInstanceKey, jsonToSave) }
ServiceList.PeerTube.instance = instance
return instance
}
}

View File

@@ -1,94 +0,0 @@
package org.schabi.newpipe.util;
import android.content.Context;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.preference.PreferenceManager;
import org.schabi.newpipe.R;
import org.schabi.newpipe.databinding.PlaylistControlBinding;
import org.schabi.newpipe.fragments.list.playlist.PlaylistControlViewHolder;
import org.schabi.newpipe.player.PlayerType;
/**
* Utility class for play buttons and their respective click listeners.
*/
public final class PlayButtonHelper {
private PlayButtonHelper() {
// utility class
}
/**
* Initialize {@link android.view.View.OnClickListener OnClickListener}
* and {@link android.view.View.OnLongClickListener OnLongClickListener} for playlist control
* buttons defined in {@link R.layout#playlist_control}.
*
* @param activity The activity to use for the {@link android.widget.Toast Toast}.
* @param playlistControlBinding The binding of the
* {@link R.layout#playlist_control playlist control layout}.
* @param fragment The fragment to get the play queue from.
*/
public static void initPlaylistControlClickListener(
@NonNull final AppCompatActivity activity,
@NonNull final PlaylistControlBinding playlistControlBinding,
@NonNull final PlaylistControlViewHolder fragment) {
// click listener
playlistControlBinding.playlistCtrlPlayAllButton.setOnClickListener(view -> {
NavigationHelper.playOnMainPlayer(activity, fragment.getPlayQueue());
showHoldToAppendToastIfNeeded(activity);
});
playlistControlBinding.playlistCtrlPlayPopupButton.setOnClickListener(view -> {
NavigationHelper.playOnPopupPlayer(activity, fragment.getPlayQueue(), false);
showHoldToAppendToastIfNeeded(activity);
});
playlistControlBinding.playlistCtrlPlayBgButton.setOnClickListener(view -> {
NavigationHelper.playOnBackgroundPlayer(activity, fragment.getPlayQueue(), false);
showHoldToAppendToastIfNeeded(activity);
});
// long click listener
playlistControlBinding.playlistCtrlPlayAllButton.setOnLongClickListener(view -> {
NavigationHelper.enqueueOnPlayer(activity, fragment.getPlayQueue(), PlayerType.MAIN);
return true;
});
playlistControlBinding.playlistCtrlPlayPopupButton.setOnLongClickListener(view -> {
NavigationHelper.enqueueOnPlayer(activity, fragment.getPlayQueue(), PlayerType.POPUP);
return true;
});
playlistControlBinding.playlistCtrlPlayBgButton.setOnLongClickListener(view -> {
NavigationHelper.enqueueOnPlayer(activity, fragment.getPlayQueue(), PlayerType.AUDIO);
return true;
});
}
/**
* Show the "hold to append" toast if the corresponding preference is enabled.
*
* @param context The context to show the toast.
*/
private static void showHoldToAppendToastIfNeeded(@NonNull final Context context) {
if (shouldShowHoldToAppendTip(context)) {
Toast.makeText(context, R.string.hold_to_append, Toast.LENGTH_SHORT).show();
}
}
/**
* Check if the "hold to append" toast should be shown.
*
* <p>
* The tip is shown if the corresponding preference is enabled.
* This is the default behaviour.
* </p>
*
* @param context The context to get the preference.
* @return {@code true} if the tip should be shown, {@code false} otherwise.
*/
public static boolean shouldShowHoldToAppendTip(@NonNull final Context context) {
return PreferenceManager.getDefaultSharedPreferences(context)
.getBoolean(context.getString(R.string.show_hold_to_append_key), true);
}
}

View File

@@ -0,0 +1,96 @@
/*
* SPDX-FileCopyrightText: 2023-2026 NewPipe contributors <https://newpipe.net>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package org.schabi.newpipe.util
import android.content.Context
import android.view.View
import android.view.View.OnLongClickListener
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.preference.PreferenceManager
import org.schabi.newpipe.R
import org.schabi.newpipe.databinding.PlaylistControlBinding
import org.schabi.newpipe.fragments.list.playlist.PlaylistControlViewHolder
import org.schabi.newpipe.player.PlayerType
/**
* Utility class for play buttons and their respective click listeners.
*/
object PlayButtonHelper {
/**
* Initialize [OnClickListener][View.OnClickListener]
* and [OnLongClickListener][OnLongClickListener] for playlist control
* buttons defined in [R.layout.playlist_control].
*
* @param activity The activity to use for the [Toast][Toast].
* @param playlistControlBinding The binding of the
* [playlist control layout][R.layout.playlist_control].
* @param fragment The fragment to get the play queue from.
*/
@JvmStatic
fun initPlaylistControlClickListener(
activity: AppCompatActivity,
playlistControlBinding: PlaylistControlBinding,
fragment: PlaylistControlViewHolder
) {
// click listener
playlistControlBinding.playlistCtrlPlayAllButton.setOnClickListener {
NavigationHelper.playOnMainPlayer(activity, fragment.getPlayQueue())
showHoldToAppendToastIfNeeded(activity)
}
playlistControlBinding.playlistCtrlPlayPopupButton.setOnClickListener {
NavigationHelper.playOnPopupPlayer(activity, fragment.getPlayQueue(), false)
showHoldToAppendToastIfNeeded(activity)
}
playlistControlBinding.playlistCtrlPlayBgButton.setOnClickListener {
NavigationHelper.playOnBackgroundPlayer(activity, fragment.getPlayQueue(), false)
showHoldToAppendToastIfNeeded(activity)
}
// long click listener
playlistControlBinding.playlistCtrlPlayAllButton.setOnLongClickListener {
NavigationHelper.enqueueOnPlayer(activity, fragment.getPlayQueue(), PlayerType.MAIN)
true
}
playlistControlBinding.playlistCtrlPlayPopupButton.setOnLongClickListener {
NavigationHelper.enqueueOnPlayer(activity, fragment.getPlayQueue(), PlayerType.POPUP)
true
}
playlistControlBinding.playlistCtrlPlayBgButton.setOnLongClickListener {
NavigationHelper.enqueueOnPlayer(activity, fragment.getPlayQueue(), PlayerType.AUDIO)
true
}
}
/**
* Show the "hold to append" toast if the corresponding preference is enabled.
*
* @param context The context to show the toast.
*/
private fun showHoldToAppendToastIfNeeded(context: Context) {
if (shouldShowHoldToAppendTip(context)) {
Toast.makeText(context, R.string.hold_to_append, Toast.LENGTH_SHORT).show()
}
}
/**
* Check if the "hold to append" toast should be shown.
*
*
*
* The tip is shown if the corresponding preference is enabled.
* This is the default behaviour.
*
*
* @param context The context to get the preference.
* @return `true` if the tip should be shown, `false` otherwise.
*/
@JvmStatic
fun shouldShowHoldToAppendTip(context: Context): Boolean {
return PreferenceManager.getDefaultSharedPreferences(context)
.getBoolean(context.getString(R.string.show_hold_to_append_key), true)
}
}

View File

@@ -1,187 +0,0 @@
package org.schabi.newpipe.util;
import static org.schabi.newpipe.extractor.ServiceList.SoundCloud;
import android.content.Context;
import android.content.SharedPreferences;
import androidx.annotation.DrawableRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import androidx.preference.PreferenceManager;
import com.grack.nanojson.JsonObject;
import com.grack.nanojson.JsonParser;
import com.grack.nanojson.JsonParserException;
import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.ServiceList;
import org.schabi.newpipe.extractor.StreamingService;
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
import org.schabi.newpipe.extractor.services.peertube.PeertubeInstance;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
public final class ServiceHelper {
private static final StreamingService DEFAULT_FALLBACK_SERVICE = ServiceList.YouTube;
private ServiceHelper() { }
@DrawableRes
public static int getIcon(final int serviceId) {
return switch (serviceId) {
case 0 -> R.drawable.ic_smart_display;
case 1 -> R.drawable.ic_cloud;
case 2 -> R.drawable.ic_placeholder_media_ccc;
case 3 -> R.drawable.ic_placeholder_peertube;
case 4 -> R.drawable.ic_placeholder_bandcamp;
default -> R.drawable.ic_circle;
};
}
public static String getTranslatedFilterString(final String filter, final Context c) {
return switch (filter) {
case "all" -> c.getString(R.string.all);
case "videos", "sepia_videos", "music_videos" -> c.getString(R.string.videos_string);
case "channels" -> c.getString(R.string.channels);
case "playlists", "music_playlists" -> c.getString(R.string.playlists);
case "tracks" -> c.getString(R.string.tracks);
case "users" -> c.getString(R.string.users);
case "conferences" -> c.getString(R.string.conferences);
case "events" -> c.getString(R.string.events);
case "music_songs" -> c.getString(R.string.songs);
case "music_albums" -> c.getString(R.string.albums);
case "music_artists" -> c.getString(R.string.artists);
default -> filter;
};
}
/**
* Get a resource string with instructions for importing subscriptions for each service.
*
* @param serviceId service to get the instructions for
* @return the string resource containing the instructions or -1 if the service don't support it
*/
@StringRes
public static int getImportInstructions(final int serviceId) {
return switch (serviceId) {
case 0 -> R.string.import_youtube_instructions;
case 1 -> R.string.import_soundcloud_instructions;
default -> -1;
};
}
/**
* For services that support importing from a channel url, return a hint that will
* be used in the EditText that the user will type in his channel url.
*
* @param serviceId service to get the hint for
* @return the hint's string resource or -1 if the service don't support it
*/
@StringRes
public static int getImportInstructionsHint(final int serviceId) {
return switch (serviceId) {
case 1 -> R.string.import_soundcloud_instructions_hint;
default -> -1;
};
}
public static int getSelectedServiceId(final Context context) {
return Optional.ofNullable(getSelectedService(context))
.orElse(DEFAULT_FALLBACK_SERVICE)
.getServiceId();
}
@Nullable
public static StreamingService getSelectedService(final Context context) {
final String serviceName = PreferenceManager.getDefaultSharedPreferences(context)
.getString(context.getString(R.string.current_service_key),
context.getString(R.string.default_service_value));
try {
return NewPipe.getService(serviceName);
} catch (final ExtractionException e) {
return null;
}
}
@NonNull
public static String getNameOfServiceById(final int serviceId) {
return ServiceList.all().stream()
.filter(s -> s.getServiceId() == serviceId)
.findFirst()
.map(StreamingService::getServiceInfo)
.map(StreamingService.ServiceInfo::getName)
.orElse("<unknown>");
}
/**
* @param serviceId the id of the service
* @return the service corresponding to the provided id
* @throws java.util.NoSuchElementException if there is no service with the provided id
*/
@NonNull
public static StreamingService getServiceById(final int serviceId) {
return ServiceList.all().stream()
.filter(s -> s.getServiceId() == serviceId)
.findFirst()
.orElseThrow();
}
public static void setSelectedServiceId(final Context context, final int serviceId) {
String serviceName;
try {
serviceName = NewPipe.getService(serviceId).getServiceInfo().getName();
} catch (final ExtractionException e) {
serviceName = DEFAULT_FALLBACK_SERVICE.getServiceInfo().getName();
}
setSelectedServicePreferences(context, serviceName);
}
private static void setSelectedServicePreferences(final Context context,
final String serviceName) {
PreferenceManager.getDefaultSharedPreferences(context).edit().
putString(context.getString(R.string.current_service_key), serviceName).apply();
}
public static long getCacheExpirationMillis(final int serviceId) {
if (serviceId == SoundCloud.getServiceId()) {
return TimeUnit.MILLISECONDS.convert(5, TimeUnit.MINUTES);
} else {
return TimeUnit.MILLISECONDS.convert(1, TimeUnit.HOURS);
}
}
public static void initService(final Context context, final int serviceId) {
if (serviceId == ServiceList.PeerTube.getServiceId()) {
final SharedPreferences sharedPreferences = PreferenceManager
.getDefaultSharedPreferences(context);
final String json = sharedPreferences.getString(context.getString(
R.string.peertube_selected_instance_key), null);
if (null == json) {
return;
}
final JsonObject jsonObject;
try {
jsonObject = JsonParser.object().from(json);
} catch (final JsonParserException e) {
return;
}
final String name = jsonObject.getString("name");
final String url = jsonObject.getString("url");
final PeertubeInstance instance = new PeertubeInstance(url, name);
ServiceList.PeerTube.setInstance(instance);
}
}
public static void initServices(final Context context) {
for (final StreamingService s : ServiceList.all()) {
initService(context, s.getServiceId());
}
}
}

View File

@@ -0,0 +1,168 @@
/*
* SPDX-FileCopyrightText: 2018-2026 NewPipe contributors <https://newpipe.net>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package org.schabi.newpipe.util
import android.content.Context
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import androidx.core.content.edit
import androidx.preference.PreferenceManager
import com.grack.nanojson.JsonParser
import java.util.concurrent.TimeUnit
import org.schabi.newpipe.R
import org.schabi.newpipe.extractor.NewPipe
import org.schabi.newpipe.extractor.ServiceList
import org.schabi.newpipe.extractor.StreamingService
import org.schabi.newpipe.extractor.services.peertube.PeertubeInstance
import org.schabi.newpipe.ktx.getStringSafe
object ServiceHelper {
private val DEFAULT_FALLBACK_SERVICE: StreamingService = ServiceList.YouTube
@JvmStatic
@DrawableRes
fun getIcon(serviceId: Int): Int {
return when (serviceId) {
0 -> R.drawable.ic_smart_display
1 -> R.drawable.ic_cloud
2 -> R.drawable.ic_placeholder_media_ccc
3 -> R.drawable.ic_placeholder_peertube
4 -> R.drawable.ic_placeholder_bandcamp
else -> R.drawable.ic_circle
}
}
@JvmStatic
fun getTranslatedFilterString(filter: String, context: Context): String {
return when (filter) {
"all" -> context.getString(R.string.all)
"videos", "sepia_videos", "music_videos" -> context.getString(R.string.videos_string)
"channels" -> context.getString(R.string.channels)
"playlists", "music_playlists" -> context.getString(R.string.playlists)
"tracks" -> context.getString(R.string.tracks)
"users" -> context.getString(R.string.users)
"conferences" -> context.getString(R.string.conferences)
"events" -> context.getString(R.string.events)
"music_songs" -> context.getString(R.string.songs)
"music_albums" -> context.getString(R.string.albums)
"music_artists" -> context.getString(R.string.artists)
else -> filter
}
}
/**
* Get a resource string with instructions for importing subscriptions for each service.
*
* @param serviceId service to get the instructions for
* @return the string resource containing the instructions or -1 if the service don't support it
*/
@JvmStatic
@StringRes
fun getImportInstructions(serviceId: Int): Int {
return when (serviceId) {
0 -> R.string.import_youtube_instructions
1 -> R.string.import_soundcloud_instructions
else -> -1
}
}
/**
* For services that support importing from a channel url, return a hint that will
* be used in the EditText that the user will type in his channel url.
*
* @param serviceId service to get the hint for
* @return the hint's string resource or -1 if the service don't support it
*/
@JvmStatic
@StringRes
fun getImportInstructionsHint(serviceId: Int): Int {
return when (serviceId) {
1 -> R.string.import_soundcloud_instructions_hint
else -> -1
}
}
@JvmStatic
fun getSelectedServiceId(context: Context): Int {
return (getSelectedService(context) ?: DEFAULT_FALLBACK_SERVICE).serviceId
}
@JvmStatic
fun getSelectedService(context: Context): StreamingService? {
val serviceName: String = PreferenceManager.getDefaultSharedPreferences(context)
.getStringSafe(
context.getString(R.string.current_service_key),
context.getString(R.string.default_service_value)
)
return runCatching { NewPipe.getService(serviceName) }.getOrNull()
}
@JvmStatic
fun getNameOfServiceById(serviceId: Int): String {
return ServiceList.all().stream()
.filter { it.serviceId == serviceId }
.findFirst()
.map(StreamingService::getServiceInfo)
.map(StreamingService.ServiceInfo::getName)
.orElse("<unknown>")
}
/**
* @param serviceId the id of the service
* @return the service corresponding to the provided id
* @throws java.util.NoSuchElementException if there is no service with the provided id
*/
@JvmStatic
fun getServiceById(serviceId: Int): StreamingService {
return ServiceList.all().firstNotNullOf { it.takeIf { it.serviceId == serviceId } }
}
@JvmStatic
fun setSelectedServiceId(context: Context, serviceId: Int) {
val serviceName = runCatching { NewPipe.getService(serviceId).serviceInfo.name }
.getOrDefault(DEFAULT_FALLBACK_SERVICE.serviceInfo.name)
setSelectedServicePreferences(context, serviceName)
}
private fun setSelectedServicePreferences(context: Context, serviceName: String?) {
val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
sharedPreferences.edit { putString(context.getString(R.string.current_service_key), serviceName) }
}
@JvmStatic
fun getCacheExpirationMillis(serviceId: Int): Long {
return if (serviceId == ServiceList.SoundCloud.serviceId) {
TimeUnit.MILLISECONDS.convert(5, TimeUnit.MINUTES)
} else {
TimeUnit.MILLISECONDS.convert(1, TimeUnit.HOURS)
}
}
fun initService(context: Context, serviceId: Int) {
if (serviceId == ServiceList.PeerTube.serviceId) {
val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
val json = sharedPreferences.getString(
context.getString(R.string.peertube_selected_instance_key),
null
) ?: return
val jsonObject = runCatching { JsonParser.`object`().from(json) }
.getOrElse { return@initService }
ServiceList.PeerTube.instance = PeertubeInstance(
jsonObject.getString("url"),
jsonObject.getString("name")
)
}
}
@JvmStatic
fun initServices(context: Context) {
ServiceList.all().forEach { initService(context, it.serviceId) }
}
}

View File

@@ -1,50 +0,0 @@
package org.schabi.newpipe.util;
import org.schabi.newpipe.extractor.stream.StreamType;
/**
* Utility class for {@link StreamType}.
*/
public final class StreamTypeUtil {
private StreamTypeUtil() {
// No impl pls
}
/**
* Check if the {@link StreamType} of a stream is a livestream.
*
* @param streamType the stream type of the stream
* @return whether the stream type is {@link StreamType#AUDIO_STREAM},
* {@link StreamType#AUDIO_LIVE_STREAM} or {@link StreamType#POST_LIVE_AUDIO_STREAM}
*/
public static boolean isAudio(final StreamType streamType) {
return streamType == StreamType.AUDIO_STREAM
|| streamType == StreamType.AUDIO_LIVE_STREAM
|| streamType == StreamType.POST_LIVE_AUDIO_STREAM;
}
/**
* Check if the {@link StreamType} of a stream is a livestream.
*
* @param streamType the stream type of the stream
* @return whether the stream type is {@link StreamType#VIDEO_STREAM},
* {@link StreamType#LIVE_STREAM} or {@link StreamType#POST_LIVE_STREAM}
*/
public static boolean isVideo(final StreamType streamType) {
return streamType == StreamType.VIDEO_STREAM
|| streamType == StreamType.LIVE_STREAM
|| streamType == StreamType.POST_LIVE_STREAM;
}
/**
* Check if the {@link StreamType} of a stream is a livestream.
*
* @param streamType the stream type of the stream
* @return whether the stream type is {@link StreamType#LIVE_STREAM} or
* {@link StreamType#AUDIO_LIVE_STREAM}
*/
public static boolean isLiveStream(final StreamType streamType) {
return streamType == StreamType.LIVE_STREAM
|| streamType == StreamType.AUDIO_LIVE_STREAM;
}
}

View File

@@ -0,0 +1,54 @@
/*
* SPDX-FileCopyrightText: 2021-2026 NewPipe contributors <https://newpipe.net>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package org.schabi.newpipe.util
import org.schabi.newpipe.extractor.stream.StreamType
/**
* Utility class for [StreamType].
*/
object StreamTypeUtil {
/**
* Check if the [StreamType] of a stream is a livestream.
*
* @param streamType the stream type of the stream
* @return whether the stream type is [StreamType.AUDIO_STREAM],
* [StreamType.AUDIO_LIVE_STREAM] or [StreamType.POST_LIVE_AUDIO_STREAM]
*/
@JvmStatic
fun isAudio(streamType: StreamType): Boolean {
return streamType == StreamType.AUDIO_STREAM ||
streamType == StreamType.AUDIO_LIVE_STREAM ||
streamType == StreamType.POST_LIVE_AUDIO_STREAM
}
/**
* Check if the [StreamType] of a stream is a livestream.
*
* @param streamType the stream type of the stream
* @return whether the stream type is [StreamType.VIDEO_STREAM],
* [StreamType.LIVE_STREAM] or [StreamType.POST_LIVE_STREAM]
*/
@JvmStatic
fun isVideo(streamType: StreamType): Boolean {
return streamType == StreamType.VIDEO_STREAM ||
streamType == StreamType.LIVE_STREAM ||
streamType == StreamType.POST_LIVE_STREAM
}
/**
* Check if the [StreamType] of a stream is a livestream.
*
* @param streamType the stream type of the stream
* @return whether the stream type is [StreamType.LIVE_STREAM] or
* [StreamType.AUDIO_LIVE_STREAM]
*/
@JvmStatic
fun isLiveStream(streamType: StreamType): Boolean {
return streamType == StreamType.LIVE_STREAM ||
streamType == StreamType.AUDIO_LIVE_STREAM
}
}

View File

@@ -40,7 +40,6 @@ import android.view.Window;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.appcompat.view.WindowCallbackWrapper;
import org.schabi.newpipe.R;
@@ -232,7 +231,7 @@ public final class FocusOverlayView extends Drawable implements
// Unfortunately many such forms of "scrolling" do not count as scrolling for purpose
// of dispatching ViewTreeObserver callbacks, so we have to intercept them by directly
// receiving keys from Window.
window.setCallback(new WindowCallbackWrapper(window.getCallback()) {
window.setCallback(new SimpleWindowCallback(window.getCallback()) {
@Override
public boolean dispatchKeyEvent(final KeyEvent event) {
final boolean res = super.dispatchKeyEvent(event);

View File

@@ -0,0 +1,39 @@
/*
* SPDX-FileCopyrightText: 2026 NewPipe e.V. <https://newpipe-ev.de>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package org.schabi.newpipe.views
import android.os.Build
import android.view.KeyEvent
import android.view.KeyboardShortcutGroup
import android.view.Menu
import android.view.Window
import androidx.annotation.RequiresApi
/**
* Simple window callback class to allow intercepting key events
* @see FocusOverlayView.setupOverlay
*/
open class SimpleWindowCallback(private val baseCallback: Window.Callback) :
Window.Callback by baseCallback {
override fun dispatchKeyEvent(event: KeyEvent?): Boolean {
return baseCallback.dispatchKeyEvent(event)
}
@RequiresApi(Build.VERSION_CODES.O)
override fun onPointerCaptureChanged(hasCapture: Boolean) {
baseCallback.onPointerCaptureChanged(hasCapture)
}
@RequiresApi(Build.VERSION_CODES.N)
override fun onProvideKeyboardShortcuts(
data: List<KeyboardShortcutGroup?>?,
menu: Menu?,
deviceId: Int
) {
baseCallback.onProvideKeyboardShortcuts(data, menu, deviceId)
}
}

View File

@@ -21,7 +21,7 @@ import static us.shandian.giga.get.DownloadMission.ERROR_HTTP_FORBIDDEN;
* Single-threaded fallback mode
*/
public class DownloadRunnableFallback extends Thread {
private static final String TAG = "DownloadRunnableFallback";
private static final String TAG = "DLRunnableFallback";
private final DownloadMission mMission;

View File

@@ -102,14 +102,23 @@ public class FinishedMissionStore extends SQLiteOpenHelper {
db.beginTransaction();
while (cursor.moveToNext()) {
ContentValues values = new ContentValues();
values.put(KEY_SOURCE, cursor.getString(cursor.getColumnIndex(KEY_SOURCE)));
values.put(KEY_DONE, cursor.getString(cursor.getColumnIndex(KEY_DONE)));
values.put(KEY_TIMESTAMP, cursor.getLong(cursor.getColumnIndex(KEY_TIMESTAMP)));
values.put(KEY_KIND, cursor.getString(cursor.getColumnIndex(KEY_KIND)));
values.put(
KEY_SOURCE,
cursor.getString(cursor.getColumnIndexOrThrow(KEY_SOURCE))
);
values.put(
KEY_DONE,
cursor.getString(cursor.getColumnIndexOrThrow(KEY_DONE))
);
values.put(
KEY_TIMESTAMP,
cursor.getLong(cursor.getColumnIndexOrThrow(KEY_TIMESTAMP))
);
values.put(KEY_KIND, cursor.getString(cursor.getColumnIndexOrThrow(KEY_KIND)));
values.put(KEY_PATH, Uri.fromFile(
new File(
cursor.getString(cursor.getColumnIndex(KEY_LOCATION)),
cursor.getString(cursor.getColumnIndex(KEY_NAME))
cursor.getString(cursor.getColumnIndexOrThrow(KEY_LOCATION)),
cursor.getString(cursor.getColumnIndexOrThrow(KEY_NAME))
)
).toString());
@@ -141,7 +150,8 @@ public class FinishedMissionStore extends SQLiteOpenHelper {
}
private FinishedMission getMissionFromCursor(Cursor cursor) {
String kind = Objects.requireNonNull(cursor).getString(cursor.getColumnIndex(KEY_KIND));
String kind = Objects.requireNonNull(cursor)
.getString(cursor.getColumnIndexOrThrow(KEY_KIND));
if (kind == null || kind.isEmpty()) kind = "?";
String path = cursor.getString(cursor.getColumnIndexOrThrow(KEY_PATH));

View File

@@ -29,9 +29,12 @@ class TtmlConverter extends Postprocessing {
try {
writer.build(sources[0]);
} catch (IOException err) {
Log.e(TAG, "subtitle conversion failed due to I/O error", err);
throw err;
} catch (Exception err) {
Log.e(TAG, "subtitle parse failed", err);
return err instanceof IOException ? 1 : 8;
Log.e(TAG, "subtitle conversion failed", err);
throw new IOException("TTML to SRT conversion failed", err);
}
return OK_RESULT;

View File

@@ -632,103 +632,95 @@ public class MissionAdapter extends Adapter<ViewHolder> implements Handler.Callb
DownloadMission mission = h.item.mission instanceof DownloadMission ? (DownloadMission) h.item.mission : null;
if (mission != null) {
switch (id) {
case R.id.start:
h.status.setText(UNDEFINED_PROGRESS);
mDownloadManager.resumeMission(mission);
return true;
case R.id.pause:
mDownloadManager.pauseMission(mission);
return true;
case R.id.error_message_view:
showError(mission);
return true;
case R.id.queue:
boolean flag = !h.queue.isChecked();
h.queue.setChecked(flag);
mission.setEnqueued(flag);
updateProgress(h);
return true;
case R.id.retry:
if (mission.isPsRunning()) {
mission.psContinue(true);
} else {
mDownloadManager.tryRecover(mission);
if (mission.storage.isInvalid())
mRecover.tryRecover(mission);
else
recoverMission(mission);
}
return true;
case R.id.cancel:
mission.psContinue(false);
return false;
if (id == R.id.start) {
h.status.setText(UNDEFINED_PROGRESS);
mDownloadManager.resumeMission(mission);
return true;
} else if (id == R.id.pause) {
mDownloadManager.pauseMission(mission);
return true;
} else if (id == R.id.error_message_view) {
showError(mission);
return true;
} else if (id == R.id.queue) {
boolean flag = !h.queue.isChecked();
h.queue.setChecked(flag);
mission.setEnqueued(flag);
updateProgress(h);
return true;
} else if (id == R.id.retry) {
if (mission.isPsRunning()) {
mission.psContinue(true);
} else {
mDownloadManager.tryRecover(mission);
if (mission.storage.isInvalid())
mRecover.tryRecover(mission);
else
recoverMission(mission);
}
return true;
} else if (id == R.id.cancel) {
mission.psContinue(false);
return false;
}
}
switch (id) {
case R.id.menu_item_share:
shareFile(h.item.mission);
return true;
case R.id.delete:
// delete the entry and the file
if (id == R.id.menu_item_share) {
shareFile(h.item.mission);
return true;
} else if (id == R.id.delete) {// delete the entry and the file
mDeleter.append(h.item.mission, true);
applyChanges();
checkMasterButtonsVisibility();
return true;
} else if (id == R.id.delete_entry) {// just delete the entry
mDeleter.append(h.item.mission, false);
applyChanges();
checkMasterButtonsVisibility();
return true;
} else if (id == R.id.md5 || id == R.id.sha1) {
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, true);
applyChanges();
checkMasterButtonsVisibility();
return true;
case R.id.delete_entry:
// just delete the entry
mDeleter.append(h.item.mission, false);
applyChanges();
checkMasterButtonsVisibility();
return true;
case R.id.md5:
case R.id.sha1:
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, true);
applyChanges();
return true;
}
final NotificationManager notificationManager
= ContextCompat.getSystemService(mContext, NotificationManager.class);
final NotificationCompat.Builder progressNotificationBuilder
= new NotificationCompat.Builder(mContext,
mContext.getString(R.string.hash_channel_id))
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setSmallIcon(R.drawable.ic_newpipe_triangle_white)
.setContentTitle(mContext.getString(R.string.msg_calculating_hash))
.setContentText(mContext.getString(R.string.msg_wait))
.setProgress(0, 0, true)
.setOngoing(true);
}
final NotificationManager notificationManager
= ContextCompat.getSystemService(mContext, NotificationManager.class);
final NotificationCompat.Builder progressNotificationBuilder
= new NotificationCompat.Builder(mContext,
mContext.getString(R.string.hash_channel_id))
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setSmallIcon(R.drawable.ic_newpipe_triangle_white)
.setContentTitle(mContext.getString(R.string.msg_calculating_hash))
.setContentText(mContext.getString(R.string.msg_wait))
.setProgress(0, 0, true)
.setOngoing(true);
notificationManager.notify(HASH_NOTIFICATION_ID, progressNotificationBuilder
.build());
compositeDisposable.add(
Observable.fromCallable(() -> Utility.checksum(storage, id))
.subscribeOn(Schedulers.computation())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(result -> {
ShareUtils.copyToClipboard(mContext, result);
notificationManager.cancel(HASH_NOTIFICATION_ID);
})
);
return true;
case R.id.source:
/*Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(h.item.mission.source));
mContext.startActivity(intent);*/
try {
Intent intent = NavigationHelper.getIntentByLink(mContext, h.item.mission.source);
intent.addFlags(Intent.FLAG_ACTIVITY_PREVIOUS_IS_TOP);
mContext.startActivity(intent);
} catch (Exception e) {
Log.w(TAG, "Selected item has a invalid source", e);
}
return true;
default:
return false;
notificationManager.notify(HASH_NOTIFICATION_ID, progressNotificationBuilder
.build());
compositeDisposable.add(
Observable.fromCallable(() -> Utility.checksum(storage, id))
.subscribeOn(Schedulers.computation())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(result -> {
ShareUtils.copyToClipboard(mContext, result);
notificationManager.cancel(HASH_NOTIFICATION_ID);
})
);
return true;
} else if (id == R.id.source) {
try {
Intent intent = NavigationHelper.getIntentByLink(mContext, h.item.mission.source);
intent.addFlags(Intent.FLAG_ACTIVITY_PREVIOUS_IS_TOP);
mContext.startActivity(intent);
} catch (Exception e) {
Log.w(TAG, "Selected item has a invalid source", e);
}
return true;
}
return false;
}
public void applyChanges() {

View File

@@ -189,23 +189,24 @@ public class MissionsFragment extends Fragment {
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.switch_mode:
mLinear = !mLinear;
updateList();
return true;
case R.id.clear_list:
showClearDownloadHistoryPrompt();
return true;
case R.id.start_downloads:
mBinder.getDownloadManager().startAllMissions();
return true;
case R.id.pause_downloads:
mBinder.getDownloadManager().pauseAllMissions(false);
mAdapter.refreshMissionItems();// update items view
default:
return super.onOptionsItemSelected(item);
int itemId = item.getItemId();
if (itemId == R.id.switch_mode) {
mLinear = !mLinear;
updateList();
return true;
} else if (itemId == R.id.clear_list) {
showClearDownloadHistoryPrompt();
return true;
} else if (itemId == R.id.start_downloads) {
mBinder.getDownloadManager().startAllMissions();
return true;
} else if (itemId == R.id.pause_downloads) {
mBinder.getDownloadManager().pauseAllMissions(false);
mAdapter.refreshMissionItems();// update items view
return super.onOptionsItemSelected(item);
}
return super.onOptionsItemSelected(item);
}
public void showClearDownloadHistoryPrompt() {

View File

@@ -5,6 +5,6 @@
android:viewportWidth="24"
android:tint="@color/defaultIconTint">
<path
android:pathData="M20 4H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2zM9.5 16.5v-9l7 4.5-7 4.5z"
android:pathData="M20 4H4c-1.1 0-2 0.9-2 2v12c0 1.1 0.9 2 2 2h16c1.1 0 2-0.9 2-2V6c0-1.1-0.9-2-2-2zM9.5 16.5v-9l7 4.5-7 4.5z"
android:fillColor="#FF000000"/>
</vector>

View File

@@ -122,8 +122,8 @@
android:focusable="true"
android:scaleType="fitCenter"
android:src="@drawable/exo_controls_rewind"
android:tint="?attr/colorAccent"
android:contentDescription="@string/rewind" />
android:contentDescription="@string/rewind"
app:tint="?attr/colorAccent" />
<ImageButton
android:id="@+id/control_play_pause"
@@ -139,8 +139,8 @@
android:focusable="true"
android:scaleType="fitCenter"
android:src="@drawable/ic_pause"
android:tint="?attr/colorAccent"
android:contentDescription="@string/pause" />
android:contentDescription="@string/pause"
app:tint="?attr/colorAccent" />
<ProgressBar
android:id="@+id/control_progress_bar"
@@ -172,8 +172,8 @@
android:focusable="true"
android:scaleType="fitCenter"
android:src="@drawable/exo_controls_fastforward"
android:tint="?attr/colorAccent"
android:contentDescription="@string/forward" />
android:contentDescription="@string/forward"
app:tint="?attr/colorAccent" />
</RelativeLayout>
<RelativeLayout
@@ -215,8 +215,8 @@
android:focusable="true"
android:scaleType="fitXY"
android:src="@drawable/ic_repeat"
android:tint="?attr/colorAccent"
android:contentDescription="@string/notification_action_repeat" />
android:contentDescription="@string/notification_action_repeat"
app:tint="?attr/colorAccent" />
<View
android:id="@+id/anchor"
@@ -236,8 +236,8 @@
android:focusable="true"
android:scaleType="fitXY"
android:src="@drawable/ic_shuffle"
android:tint="?attr/colorAccent"
android:contentDescription="@string/notification_action_shuffle" />
android:contentDescription="@string/notification_action_shuffle"
app:tint="?attr/colorAccent" />
<androidx.appcompat.widget.AppCompatImageButton
android:id="@+id/control_forward"

View File

@@ -175,7 +175,7 @@
android:focusable="true"
android:scaleType="fitXY"
android:src="@drawable/ic_repeat"
android:tint="?attr/colorAccent"
app:tint="?attr/colorAccent"
tools:ignore="ContentDescription" />
<androidx.appcompat.widget.AppCompatImageButton
@@ -205,7 +205,7 @@
android:focusable="true"
android:scaleType="fitCenter"
android:src="@drawable/exo_controls_rewind"
android:tint="?attr/colorAccent" />
app:tint="?attr/colorAccent" />
<ImageButton
android:id="@+id/control_play_pause"
@@ -220,7 +220,7 @@
android:focusable="true"
android:scaleType="fitCenter"
android:src="@drawable/ic_pause"
android:tint="?attr/colorAccent"
app:tint="?attr/colorAccent"
tools:ignore="ContentDescription" />
<ProgressBar
@@ -255,7 +255,7 @@
android:focusable="true"
android:scaleType="fitCenter"
android:src="@drawable/exo_controls_fastforward"
android:tint="?attr/colorAccent" />
app:tint="?attr/colorAccent" />
<androidx.appcompat.widget.AppCompatImageButton
android:id="@+id/control_forward"
@@ -285,7 +285,7 @@
android:focusable="true"
android:scaleType="fitXY"
android:src="@drawable/ic_shuffle"
android:tint="?attr/colorAccent"
app:tint="?attr/colorAccent"
tools:ignore="ContentDescription" />
</RelativeLayout>

View File

@@ -380,7 +380,7 @@
<string name="channels">القنوات</string>
<string name="dont_show">لا تظهر</string>
<string name="peertube_instance_add_help">أدخل عنوان للمثيل</string>
<string name="info_labels">ماذا:\\nطلب:\\nلغة المحتوى:\\nبلد المحتوى:\\nلغة التطبيق:\\nالخدمات:\\nتوقيت جرينتش:\\nالحزمة:\\nالإصدار:\\nOS نسخة:</string>
<string name="info_labels">ماذا:\nطلب:\nلغة المحتوى:\nبلد المحتوى:\nلغة التطبيق:\nالخدمات:\nتوقيت جرينتش:\nالحزمة:\nالإصدار:\nOS نسخة:</string>
<string name="use_external_video_player_summary">يزيل الصوت في بعض الجودات</string>
<string name="feed_fetch_channel_tabs">جلب ألسنة القنوات</string>
<string name="high_quality_larger">جودة عالية (أكبر)</string>

View File

@@ -109,7 +109,7 @@
<string name="error_snackbar_action">أبلِغ</string>
<string name="what_device_headline">معلومات:</string>
<string name="what_happened_headline">ماذا حدث:</string>
<string name="info_labels">ماذا:\\nطلب:\\nلغة المحتوى:\\nبلد المحتوى:\\nلغة التطبيق:\\nالخدمات:\\nتوقيت جرينتش:\\nالحزمة:\\nالإصدار:\\nOS نسخة:</string>
<string name="info_labels">ماذا:\nطلب:\nلغة المحتوى:\nبلد المحتوى:\nلغة التطبيق:\nالخدمات:\nتوقيت جرينتش:\nالحزمة:\nالإصدار:\nOS نسخة:</string>
<string name="your_comment">تعليقك (باللغة الإنجليزية):</string>
<string name="error_details_headline">التفاصيل:</string>
<string name="search_no_results">لم يتم العثور على نتائج</string>

View File

@@ -575,7 +575,7 @@
</plurals>
<string name="audio">Səs</string>
<string name="error_details_headline">Təfərrüatlar:</string>
<string name="info_labels">Nə:\\nSorğu:\\nMəzmun Dili:\\nMəzmun Ölkəsi:\\nTətbiq Dili:\\nXidmət:\\nGMT Saatı:\\nPaket:\\nVersiya:\\nƏS versiyası:</string>
<string name="info_labels">Nə:\nSorğu:\nMəzmun Dili:\nMəzmun Ölkəsi:\nTətbiq Dili:\nXidmət:\nGMT Saatı:\nPaket:\nVersiya:\nƏS versiyası:</string>
<string name="error_snackbar_message">Bağışlayın, nəsə səhv oldu.</string>
<string name="copy_for_github">Formatlanmış hesabatı köçür</string>
<string name="peertube_instance_add_help">Server URL\'sini daxil et</string>

View File

@@ -267,7 +267,7 @@
<string name="metadata_cache_wipe_title">Llimpiar los metadatos de la caché</string>
<string name="thumbnail_cache_wipe_complete_notice">Llimpióse la caché d\'imáxenes</string>
<string name="kore_not_found">¿Instalar Kode\?</string>
<string name="info_labels">Qué asocedió:\\nSolicitú:\\nLlingua del conteníu:\\nPaís del conteníu:\\nLlingua de l\'aplicación:\\nServiciu:\\nHora en GMT:\\nPaquete:\\nVersión de l\'aplicación:\\nVersión del SO:</string>
<string name="info_labels">Qué asocedió:\nSolicitú:\nLlingua del conteníu:\nPaís del conteníu:\nLlingua de l\'aplicación:\nServiciu:\nHora en GMT:\nPaquete:\nVersión de l\'aplicación:\nVersión del SO:</string>
<string name="no_player_found_toast">Nun s\'atopó nengún reproductor de fluxos (pues instalar VLC pa reproducilos).</string>
<string name="show_thumbnail_summary">Amuesa una miniatura nel fondu de la pantalla de bloquéu y dientro de los avisos</string>
<string name="show_thumbnail_title">Amosar una miniatura</string>

View File

@@ -279,7 +279,7 @@
<string name="detail_thumbnail_view_description">Videoni ijro etish muddati, davomiyligi:</string>
<string name="error_details_headline">Detallar:</string>
<string name="your_comment">Sizning sharhingiz (ingliz tilida):</string>
<string name="info_labels">Nima: \\n So\'rov: \\nTarkib tili: \\nTarkib mamlakati: \\nIlova tili: \\ nXizmat: \\ nGMT vaqti: \\ nPaket: \\ nVersion: \\ nOS versiyasi:</string>
<string name="info_labels">Nima: \n So\'rov: \nTarkib tili: \nTarkib mamlakati: \nIlova tili: \\ nXizmat: \\ nGMT vaqti: \\ nPaket: \\ nVersion: \\ nOS versiyasi:</string>
<string name="what_happened_headline">Nima sodir bo\'ldi:</string>
<string name="what_device_headline">Info:</string>
<string name="error_snackbar_action">Hisobot</string>

View File

@@ -146,7 +146,7 @@
<string name="error_snackbar_action">Справаздача</string>
<string name="what_device_headline">Інфармацыя:</string>
<string name="what_happened_headline">Што адбылося:</string>
<string name="info_labels">Што:\\nЗапыт:\\nМова кантэнту:\\nКраіна кантэнту:\\nМова праграмы:\\nСэрвіс:\\ас GMT:\\акет:\\nВерсія:\\nВерсія АС:</string>
<string name="info_labels">Што:\nЗапыт:\nМова кантэнту:\nКраіна кантэнту:\nМова праграмы:\nСэрвіс:\nЧас GMT:\nПакет:\nВерсія:\nВерсія АС:</string>
<string name="your_comment">Ваш каментарый (па-англійску):</string>
<string name="error_details_headline">Падрабязнасці:</string>
<string name="detail_thumbnail_view_description">Прайграць відэа, працягласць:</string>
@@ -611,8 +611,6 @@
<string name="faq">Перайсці на вэб-сайт</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">Паказваць на відарысах каляровыя меткі Picasso, якія абазначаюць яго крыніцу: чырвоная — сетка, сіняя — дыск, зялёная — памяць</string>
<string name="feed_processing_message">Апрацоўка стужкі…</string>
<string name="downloads_storage_ask_summary_no_saf_notice">Пры кожным спампоўванні вам будзе прапанавана выбраць месца захавання</string>
<string name="feed_notification_loading">Загрузка канала…</string>

View File

@@ -204,7 +204,7 @@
<string name="clear_views_history_summary">Изтрива историята на възпроизвежданите стриймове и позицията на възпроизвеждането</string>
<string name="video_streams_empty">Не са намерени видео стриймове</string>
<string name="audio_streams_empty">Не са намерени аудио стриймове</string>
<string name="info_labels">Какво:\\nЗаявка:\\nЕзик на съдържанието:\\nДържава на съдържанието:\\nЕзик на приложението:\\nУслуга:\\nGMT Време:\\акет:\\nВерсия:\\nВерсия на ОС:</string>
<string name="info_labels">Какво:\nЗаявка:\nЕзик на съдържанието:\nДържава на съдържанието:\nЕзик на приложението:\nУслуга:\nGMT Време:\nПакет:\nВерсия:\nВерсия на ОС:</string>
<string name="detail_drag_description">Пренареди чрез плъзгане</string>
<string name="start">Начало</string>
<string name="rename">Преименувай</string>
@@ -787,7 +787,6 @@
<string name="error_report_notification_title">NewPipe откри грешка, докоснете, за да докладвате</string>
<string name="no_streams">Няма потоци</string>
<string name="disable_media_tunneling_title">Деактивиране на медийното тунелиране</string>
<string name="show_image_indicators_title">Покажи индикатори за изображения</string>
<string name="missions_header_pending">В очакване</string>
<string name="error_postprocessing_failed">Неуспешна последваща обработка</string>
<string name="pause_downloads_on_mobile">Прекъсване на мрежи с измерване</string>

View File

@@ -103,7 +103,7 @@
<string name="title_activity_recaptcha">reCAPTCHA চ্যালেঞ্জ</string>
<string name="recaptcha_request_toast">reCAPTCHA চ্যালেঞ্জ অনুরোধ করা হয়েছে</string>
<!-- End of GigaGet's Strings -->
<string name="info_labels">কি:\\nঅনুরোধ:\\nকন্টেন্ট ভাষা:\\nসার্ভিস:\\nসময়(GMT এ):\\nপ্যাকেজ:\\nসংস্করণ:\\nওএস সংস্করণ:\\nআইপি পরিসর:</string>
<string name="info_labels">কি:\nঅনুরোধ:\nকন্টেন্ট ভাষা:\nসার্ভিস:\nসময়(GMT এ):\nপ্যাকেজ:\nসংস্করণ:\nওএস সংস্করণ:\nআইপি পরিসর:</string>
<string name="controls_download_desc">স্ট্রিম ফাইল ডাউনলোড করুন</string>
<string name="show_info">তথ্য দেখাও</string>
<string name="fragment_feed_title">নতুন যা কিছু</string>

View File

@@ -33,7 +33,7 @@
<string name="detail_thumbnail_view_description">ভিডিও প্রাকদর্শন, সময়ঃ</string>
<string name="error_details_headline">বর্ণনা:</string>
<string name="your_comment">আপনার মন্তব্য (ইংরেজিতে):</string>
<string name="info_labels">কি:\\nঅনুরোধ:\\nকন্টেন্ট ভাষা:\\nকন্টেন্ট দেশ:\\nঅ্যাপ ভাষা:\\nসার্ভিস:\\nসময়(GMT এ):\\nপ্যাকেজ:\\nসংস্করণ:\\nওএস সংস্করণ:</string>
<string name="info_labels">কি:\nঅনুরোধ:\nকন্টেন্ট ভাষা:\nকন্টেন্ট দেশ:\nঅ্যাপ ভাষা:\nসার্ভিস:\nসময়(GMT এ):\nপ্যাকেজ:\nসংস্করণ:\nওএস সংস্করণ:</string>
<string name="what_happened_headline">কি হয়েছিল:</string>
<string name="what_device_headline">তথ্য:</string>
<string name="error_snackbar_action">প্রতিবেদন</string>
@@ -309,6 +309,6 @@
<string name="notification_action_nothing">কিছু না</string>
<string name="yes">হ্যাঁ</string>
<string name="no">না</string>
<string name="search_with_service_name">সার্চ</string>
<string name="search_with_service_name_and_filter">খুঁজুন</string>
<string name="search_with_service_name">সার্চ %1$s</string>
<string name="search_with_service_name_and_filter">খুঁজুন %1$s (%2$s)</string>
</resources>

View File

@@ -114,7 +114,7 @@
<string name="detail_thumbnail_view_description">ভিডিও চালাও, সময়ঃ</string>
<string name="error_details_headline">বর্ণনা:</string>
<string name="your_comment">তোমার মন্তব্য (ইংরেজিতে):</string>
<string name="info_labels">কি:\\nঅনুরোধ:\\nকন্টেন্ট ভাষা:\\nসার্ভিস:\\nসময়(GMT এ):\\nপ্যাকেজ:\\nসংস্করণ:\\nওএস সংস্করণ:\\nআইপি পরিসর:</string>
<string name="info_labels">কি:\nঅনুরোধ:\nকন্টেন্ট ভাষা:\nসার্ভিস:\nসময়(GMT এ):\nপ্যাকেজ:\nসংস্করণ:\nওএস সংস্করণ:\nআইপি পরিসর:</string>
<string name="what_happened_headline">কি হয়েছিল:</string>
<string name="what_device_headline">তথ্য:</string>
<string name="error_snackbar_action">প্রতিবেদন</string>

View File

@@ -268,7 +268,7 @@
<string name="error_snackbar_action">Prijavi</string>
<string name="what_device_headline">Info:</string>
<string name="what_happened_headline">Šta se dogodilo:</string>
<string name="info_labels">Šta:\\nZahtjev:\\nJezik sadržaja:\\nZemlja sadržaja:\\nJezik aplikacije:\\nUsluga:\\nVremenska oznaka:\\nPaket:\\nVerzija:\\nVerzija OS-a:</string>
<string name="info_labels">Šta:\nZahtjev:\nJezik sadržaja:\nZemlja sadržaja:\nJezik aplikacije:\nUsluga:\nVremenska oznaka:\nPaket:\nVerzija:\nVerzija OS-a:</string>
<string name="your_comment">Vaš komentar (na engleskom):</string>
<string name="error_details_headline">Detalji:</string>
<string name="detail_thumbnail_view_description">Reproduciraj video, trajanje:</string>
@@ -449,8 +449,6 @@
<string name="disable_media_tunneling_title">Onemogući tuneliranje medija</string>
<string name="disable_media_tunneling_summary">Onemogućite tuneliranje medija ako se pojavi crni ekran ili se prilikom reprodukcije videa pojavi prekid.</string>
<string name="disable_media_tunneling_automatic_info">Tuneliranje medija je onemogućeno prema zadanim postavkama na vašem uređaju jer je poznato da vaš model uređaja to ne podržava.</string>
<string name="show_image_indicators_title">Prikaži indikatore slike</string>
<string name="show_image_indicators_summary">Prikažite Picasso obojene trake preko slika koje označavaju njihov izvor: crvena za mrežu, plava za disk i zelena za memoriju</string>
<string name="show_crash_the_player_title">Prikaži \"Sruši plejer\"</string>
<string name="show_crash_the_player_summary">Prikazuje opciju pada sistema prilikom korištenja plejera</string>
<string name="check_new_streams">Pokreni provjeru za nove tokove</string>

View File

@@ -268,7 +268,7 @@
<string name="auto_queue_summary">Acaba de reproduir la cua (sense repetició) quan s\'hi afegeixi un vídeo relacionat</string>
<string name="show_hold_to_append_title">Mostra l\'indicador «Mantenir per posar a la cua»</string>
<string name="show_hold_to_append_summary">Mostra un missatge d\'ajuda quan el botó de mode en segon pla o emergent estigui premut a la pàgina de detalls d\'un vídeo</string>
<string name="info_labels">Què ha passat:\\nPetició:\\nIdioma del contingut:\\nPaís del contingut:\\nLlengua de l\'aplicació:\\nServei:\\nHora GMT:\\nPaquet:\\nVersió:\\nVersió del SO:</string>
<string name="info_labels">Què ha passat:\nPetició:\nIdioma del contingut:\nPaís del contingut:\nLlengua de l\'aplicació:\nServei:\nHora GMT:\nPaquet:\nVersió:\nVersió del SO:</string>
<string name="preferred_open_action_settings_title">Acció d\'obertura preferida</string>
<string name="preferred_open_action_settings_summary">Acció per defecte en obrir continguts — %s</string>
<string name="enable_leak_canary_summary">La supervisió de fugues de memòria pot fer que l\'aplicació deixi de respondre mentre es bolca la memòria</string>
@@ -621,7 +621,7 @@
<string name="dont_show">No mostris</string>
<string name="low_quality_smaller">Baixa qualitat (més petit)</string>
<string name="high_quality_larger">Alta qualitat (més gran)</string>
<string name="disable_media_tunneling_summary">Desactiva l\'entunelament del contingut si en els videos hi ha una pantalla negre o tartamudegen</string>
<string name="disable_media_tunneling_summary">Desactiva l\'entunelament del contingut si en reproduir el vídeos la pantalla se\'n va a negre o s\'entretallen.</string>
<string name="show_channel_details">Mostra detalls del canal</string>
<string name="no_dir_yet">No s\'ha establert una carpeta de descàrregues, selecciona la carpeta per defecte ara</string>
<string name="comments_are_disabled">Els comentaris estan desactivats</string>

View File

@@ -12,7 +12,7 @@
\n
\nبۆیە هەڵژرادن بۆ خۆت دەگەڕێتەوە: زانیاری تەواو یان خێرا.</string>
<string name="app_license">نیوپایپ نه‌رمه‌والایه‌كی سەرچاوە کراوەیە : دەتوانیت بەکاریبهێنیت، بیخوێنیتەوە، هاوبەشی پێبکەیت ،بەرەوپێشی ببەیت. بەتایبەتی دەتوانی دابەشیبکەیتەوە یاخوود بگۆڕیت بەپێی مەرجەکانی GNU مۆڵەتنامەی گشتی وەک نه‌رمه‌واڵایه‌كی بڵاوی خۆڕایی, بەهۆی وەشانی ٣ ی مۆڵەتنامە، یان هەر وەشانێکی دوواتر.</string>
<string name="info_labels">چی:\\اواكاری:\\nزمانی بابەت:\\nوڵاتی بابەت:\\nزمانی به‌رنامه‌:\\nخزمهتگوزاری:\\nGMT كات:\\اكێج:\\هشان:\\nOS وه‌شان:</string>
<string name="info_labels">چی:\nداواكاری:\nزمانی بابەت:\nوڵاتی بابەت:\nزمانی به‌رنامه‌:\nخزمهتگوزاری:\nGMT كات:\nپاكێج:\nوهشان:\nOS وه‌شان:</string>
<string name="privacy_policy_encouragement">پڕۆژەی نیوپایپ زانیارییە تایبەتییەکانت بە وردی دەپارێزێت. هەروەها به‌رنامه‌كه‌ هیچ زانایارییەکت بەبێ ئاگاداری تۆ بەکارنابات.
\nسیاسەتی تایبەتی نیوپایپ بە وردی ڕوونکردنەوەت دەداتێ لەسەر ئەو زانیاریانەی وەریاندەگرێت و بەکاریاندەبات.</string>
<string name="download_to_sdcard_error_message">ناتوانرێت لە بیرگەی دەرەکیدا داببەزێنرێت . شوێنی فۆڵده‌ری دابه‌زاندنەکان ڕێکبخرێتەوە؟</string>

View File

@@ -81,7 +81,7 @@
<string name="black_theme_title">Černé</string>
<string name="checksum">Kontrolní součet</string>
<string name="no_available_dir">Určete prosím složku pro stahování později v nastavení</string>
<string name="info_labels">Co:\\nŽádost:\\nJazyk obsahu:\\nZemě obsahu:\\nJazyk aplikace:\\nSlužba:\\nČas GMT:\\nBalíček:\\nVerze:\\nVerze OS:</string>
<string name="info_labels">Co:\nŽádost:\nJazyk obsahu:\nZemě obsahu:\nJazyk aplikace:\nSlužba:\nČas GMT:\nBalíček:\nVerze:\nVerze OS:</string>
<string name="all">Vše</string>
<string name="open_in_popup_mode">Otevřít ve vyskakovacím okně</string>
<string name="msg_popup_permission">Toto oprávnění je vyžadováno

View File

@@ -317,7 +317,7 @@
<string name="start_here_on_popup">Start afspilning i et popup</string>
<string name="drawer_open">Åbn Skuffe</string>
<string name="drawer_close">Luk Skuffe</string>
<string name="info_labels">Hvad:\\nForespørgsel:\\nIndholdssprog:\\nIndholdsland:\\nApp-sprog:\\nTjeneste:\\nGMT-tid:\\nPakke:\\nVersion:\\nOS-version:</string>
<string name="info_labels">Hvad:\nForespørgsel:\nIndholdssprog:\nIndholdsland:\nApp-sprog:\nTjeneste:\nGMT-tid:\nPakke:\nVersion:\nOS-version:</string>
<string name="preferred_open_action_settings_summary">Standardhandling ved åbning af indhold — %s</string>
<string name="set_as_playlist_thumbnail">Anvend som playlistens miniaturebillede</string>
<string name="bookmark_playlist">Bogmærk Playliste</string>

View File

@@ -54,7 +54,7 @@
<string name="error_snackbar_action">Melden</string>
<string name="what_device_headline">Info:</string>
<string name="what_happened_headline">Dies ist passiert:</string>
<string name="info_labels">Was:\\nAnfrage:\\nSprache des Inhalts:\\nLand des Inhalts:\\nSprache der App:\\nDienst:\\nZeit (GMT):\\nPaket:\\nVersion:\\nOS-Version:</string>
<string name="info_labels">Was:\nAnfrage:\nSprache des Inhalts:\nLand des Inhalts:\nSprache der App:\nDienst:\nZeit (GMT):\nPaket:\nVersion:\nOS-Version:</string>
<string name="error_details_headline">Details:</string>
<string name="video">Video</string>
<string name="audio">Audio</string>

View File

@@ -163,7 +163,7 @@
<string name="sorry_string">Λυπούμαστε, αυτό δεν έπρεπε να έχει συμβεί.</string>
<string name="error_report_button_text">Αναφορά μέσω ηλεκτρονικού ταχυδρομείου</string>
<string name="error_snackbar_message">Συγγνώμη, κάτι πήγε στραβά.</string>
<string name="info_labels">Τι:\\nΑίτημα:\\nΓλώσσα περιεχομένου:\\nΧώρα περιεχομένου:\\nΓλώσσα εφαρμογής:\\nΥπηρεσία:\\ρα GMT:\\ακέτο:\\nΈκδοση:\\nΈκδοση λειτουργικού συστήματος:</string>
<string name="info_labels">Τι:\nΑίτημα:\nΓλώσσα περιεχομένου:\nΧώρα περιεχομένου:\nΓλώσσα εφαρμογής:\nΥπηρεσία:\nΏρα GMT:\nΠακέτο:\nΈκδοση:\nΈκδοση λειτουργικού συστήματος:</string>
<string name="search_no_results">Κανένα αποτέλεσμα</string>
<string name="empty_list_subtitle">Δεν υπάρχει τίποτα εδώ</string>
<string name="detail_drag_description">Σύρετε για ταξινόμηση</string>

View File

@@ -242,7 +242,7 @@
<string name="sorry_string">Pardonu, eraro okazis.</string>
<string name="error_snackbar_message">Pardonon, io mizokasis.</string>
<string name="what_happened_headline">Kio okazis:</string>
<string name="info_labels">Kio:\\nPeto:\\nEnhavlingvo:\\nEnhavlando:\\nAplingvo:\\nServo:\\nGMT Horo:\\nPako:\\nVersio:\\nOperaciumo versio:</string>
<string name="info_labels">Kio:\nPeto:\nEnhavlingvo:\nEnhavlando:\nAplingvo:\nServo:\nGMT Horo:\nPako:\nVersio:\nOperaciumo versio:</string>
<string name="audio">Aŭdio</string>
<string name="start">Komenci</string>
<string name="pause">Paŭzigi</string>

View File

@@ -78,7 +78,7 @@
<string name="msg_copied">Copiado al portapapeles</string>
<string name="no_available_dir">Defina una carpeta de descargas más tarde en los ajustes</string>
<string name="app_ui_crash">La interfaz de la aplicación dejó de funcionar</string>
<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="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="open_in_popup_mode">Abrir en modo emergente</string>
@@ -513,8 +513,8 @@
<string name="songs">Canciones</string>
<string name="restricted_video">Este vídeo tiene restricción de edad. \n \nHabilitar \"%1$s\" en los ajustes si quieres verlo.</string>
<string name="remove_watched_popup_partially_watched_streams">Sí, y también vídeos vistos parcialmente</string>
<string name="remove_watched_popup_warning">Los vídeos que ya se hayan visto luego de añadidos a la lista de reproducción, serán quitados. \n¿Estás seguro? ¡Esta acción no se puede deshacer!</string>
<string name="remove_watched_popup_title">¿Quitar vídeos ya vistos?</string>
<string name="remove_watched_popup_warning">Los vídeos que ya se hayan visto antes y después de ser añadidos a la lista de reproducción serán quitados. \n¿Estás seguro?</string>
<string name="remove_watched_popup_title">¿Quitar streams ya vistos?</string>
<string name="remove_watched">Quitar vídeos ya vistos</string>
<string name="video_detail_by">Por %s</string>
<string name="channel_created_by">Creado por %s</string>

View File

@@ -141,7 +141,7 @@
<string name="error_snackbar_action">Teata</string>
<string name="what_device_headline">Info:</string>
<string name="what_happened_headline">Mis juhtus:</string>
<string name="info_labels">Mis:\\nPäring:\\nSisu keel:\\nSisu maa:\\nRakenduse keel:\\nTeenus:\\nGMT aeg:\\nPakett:\\nVersioon:\\nOS versioon:</string>
<string name="info_labels">Mis:\nPäring:\nSisu keel:\nSisu maa:\nRakenduse keel:\nTeenus:\nGMT aeg:\nPakett:\nVersioon:\nOS versioon:</string>
<string name="your_comment">Oma kommentaar (inglise keeles):</string>
<string name="error_details_headline">Üksikasjad:</string>
<string name="detail_thumbnail_view_description">Esita video, kestus:</string>

View File

@@ -78,7 +78,7 @@
<string name="error_snackbar_action">Salatu</string>
<string name="what_device_headline">Informazioa:</string>
<string name="what_happened_headline">Zer gertatu da:</string>
<string name="info_labels">Zer:\\nEskaria:\\nEdukiaren hizkuntza:\\nEdukiaren herrialdea:\\nAplikazioaren hizkuntza:\\nZerbitzua:\\nDenbora-zigilua:\\nPaketea:\\nBertsioa:\\nSE bertsioa:</string>
<string name="info_labels">Zer:\nEskaria:\nEdukiaren hizkuntza:\nEdukiaren herrialdea:\nAplikazioaren hizkuntza:\nZerbitzua:\nDenbora-zigilua:\nPaketea:\nBertsioa:\nSE bertsioa:</string>
<string name="your_comment">Zure iruzkina (Ingelesez):</string>
<string name="error_details_headline">Xehetasunak:</string>
<string name="video">Bideoa</string>
@@ -494,8 +494,8 @@
<string name="detail_sub_channel_thumbnail_view_description">Kanalaren avatarraren miniatura</string>
<string name="feed_group_show_only_ungrouped_subscriptions">Erakutsi taldekatuta ez dauden harpidetzak soilik</string>
<string name="remove_watched_popup_partially_watched_streams">Bai, partzialki ikusitako bideoak ere bai</string>
<string name="remove_watched_popup_warning">Dagoeneko ikusi eta gero erreprodukzio-zerrendara gehitu diren bideoak kendu egingo dira. \nJarraitu nahi duzu? Ekintza hau ezin da desegin!</string>
<string name="remove_watched_popup_title">Ikusitako bideoak kendu?</string>
<string name="remove_watched_popup_warning">Dagoeneko ikusi eta gero erreprodukzio-zerrendara gehitu diren igorpenak kendu egingo dira. \nJarraitu nahi duzu?</string>
<string name="remove_watched_popup_title">Ikusitako igorpenak kendu?</string>
<string name="remove_watched">Kendu ikusitako bideoak</string>
<string name="never">Inoiz ez</string>
<string name="wifi_only">WiFi-arekin soilik</string>

View File

@@ -275,7 +275,7 @@
<string name="show_hold_to_append_summary">نمایش راهنما هنگام فشردن پس زمینه یا دکمهٔ تصویر در تصویر در «جزییات:» ویدیو</string>
<string name="hold_to_append">برای در صف قرار دادن، نگه دارید</string>
<string name="undo">بازگردانی</string>
<string name="info_labels">چی:\\nدرخواست:\\nزبان محتوا:\\nکشور محتوا:\\nزبان اپ:\\nخدمت:\\nزمان GMT\\nپکیج:T:\\nنسخه:\\nنسخهاندروید:</string>
<string name="info_labels">چی:\nدرخواست:\nزبان محتوا:\nکشور محتوا:\nزبان اپ:\nخدمت:\nزمان GMT\nپکیج:T:\nنسخه:\nنسخهاندروید:</string>
<string name="title_activity_recaptcha">چالش ری‌کپچا</string>
<string name="recaptcha_request_toast">نیاز به چالش ری‌کپچا است</string>
<string name="msg_popup_permission">این اجازه برای گشودن در حالت

View File

@@ -155,7 +155,7 @@
<string name="delete_item_search_history">Haluatko poistaa tämän hakuhistoriasta?</string>
<string name="resume_on_audio_focus_gain_title">Jatka toistoa</string>
<string name="what_device_headline">Info:</string>
<string name="info_labels">Mikä:\\nPyyntö:\\nSisällön kieli:\\nSisällön maa:\\n:Sovelluksen kieli:\\nPalvelu:\\nGMT Aika:\\nPaketti:\\nVersio:\\nOS versio:</string>
<string name="info_labels">Mikä:\nPyyntö:\nSisällön kieli:\nSisällön maa:\n:Sovelluksen kieli:\nPalvelu:\nGMT Aika:\nPaketti:\nVersio:\nOS versio:</string>
<string name="copyright" formatted="true">© %1$s %2$s %3$s alla</string>
<string name="main_page_content">Pääsivun sisältö</string>
<string name="blank_page_summary">Tyhjä sivu</string>

View File

@@ -86,7 +86,7 @@
<string name="open_in_popup_mode">Ouvrir en mode flottant</string>
<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="info_labels">Quoi:\nRequest:\nContent Language:\nContent Country:\nApp Language:\nService:\nGMT Time:\nPackage:\nVersion:\nOS version:</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>

View File

@@ -0,0 +1,3 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
</resources>

View File

@@ -145,7 +145,7 @@
<string name="error_snackbar_action">Informe</string>
<string name="what_device_headline">Información:</string>
<string name="what_happened_headline">Que ocorreu:</string>
<string name="info_labels">Que: \\n Solicitar: \\n Idioma de contido: \\n País de contido: \\n Idioma do aplicativo: \\nServicio: \\n Tempo GMT: \\n Paquete: \\n Versión: \\n versión de nOS:</string>
<string name="info_labels">Que: \n Solicitar: \n Idioma de contido: \n País de contido: \n Idioma do aplicativo: \nServicio: \n Tempo GMT: \n Paquete: \n Versión: \n versión de nOS:</string>
<string name="your_comment">O teu comentario (en inglés):</string>
<string name="error_details_headline">Detalles:</string>
<string name="detail_thumbnail_view_description">Reproducir o vídeo, duración:</string>

View File

@@ -73,7 +73,7 @@
<string name="error_snackbar_action">דיווח</string>
<string name="what_device_headline">מידע:</string>
<string name="what_happened_headline">מה קרה:</string>
<string name="info_labels">מה:\\nבקשה:\\nשפת התוכן:\\nמדינת התוכן:\\nשפת היישומון:\\ירות:\\nשעון גריניץ׳:\\nחבילה:\\nגרסה:\\nגרסת מערכת ההפעלה:</string>
<string name="info_labels">מה:\nבקשה:\nשפת התוכן:\nמדינת התוכן:\nשפת היישומון:\nשירות:\nשעון גריניץ׳:\nחבילה:\nגרסה:\nגרסת מערכת ההפעלה:</string>
<string name="subscribe_button_title">רישום למינוי</string>
<string name="subscribed_button_title">נרשמת</string>
<string name="channel_unsubscribed">ביטול מינוי לערוץ</string>

View File

@@ -114,7 +114,7 @@
<string name="error_snackbar_action">रिपोर्ट करें</string>
<string name="what_device_headline">जानकारी:</string>
<string name="what_happened_headline">क्या हुआ:</string>
<string name="info_labels">क्या:\\nअनुरोध:\\nसामग्री भाषा:\\nसामग्री देश:\\nऐप भाषा:\\nसेवा:\\nजीएमटी समय:\\nपैकेज:\\nसंस्करण:\\nOS संस्करण:</string>
<string name="info_labels">क्या:\nअनुरोध:\nसामग्री भाषा:\nसामग्री देश:\nऐप भाषा:\nसेवा:\nजीएमटी समय:\nपैकेज:\nसंस्करण:\nOS संस्करण:</string>
<string name="your_comment">आपकी टिप्पणी(अंग्रेजी में):</string>
<string name="error_details_headline">विवरण:</string>
<string name="detail_thumbnail_view_description">वीडियो चलाएं, अवधि :</string>

View File

@@ -87,7 +87,7 @@
<string name="error_snackbar_action">Prijavi</string>
<string name="what_device_headline">Informacije:</string>
<string name="what_happened_headline">Što se dogodilo:</string>
<string name="info_labels">Što:\\nZahtjev:\\nJezik sadržaja:\\nZemlja sadržaja:\\nJezik aplikacije:\\nUsluga:\\nGMT vrijeme:\\nPaket:\\nVerzija:\\nVerzija OS-a:</string>
<string name="info_labels">Što:\nZahtjev:\nJezik sadržaja:\nZemlja sadržaja:\nJezik aplikacije:\nUsluga:\nGMT vrijeme:\nPaket:\nVerzija:\nVerzija OS-a:</string>
<string name="your_comment">Tvoj komentar (na engleskom):</string>
<string name="error_details_headline">Detalji:</string>
<string name="detail_thumbnail_view_description">Pokreni video, trajanje:</string>
@@ -437,7 +437,7 @@
<string name="enable_queue_limit">Ograniči popis preuzimanja</string>
<string name="downloads_storage_use_saf_title">Koristi sustavksi birač mapa (SAF)</string>
<string name="remove_watched">Ukloni pregledano</string>
<string name="remove_watched_popup_title">Ukloni pogledana videa?</string>
<string name="remove_watched_popup_title">Ukloniti pogledana emitiranja?</string>
<plurals name="seconds">
<item quantity="one">%d sekunda</item>
<item quantity="few">%d sekunde</item>
@@ -510,7 +510,7 @@
<string name="error_progress_lost">Napredak je izgubljen, jer je datoteka izbrisana</string>
<string name="error_postprocessing_stopped">NewPipe se zatvorio tijekom rada s datotekom</string>
<string name="playlist_page_summary">Stranica playliste</string>
<string name="remove_watched_popup_warning">Videa koji su gledani prije i nakon dodavanja u playlistu će se ukloniti. \nStvarno ih želiš ukloniti? Ovo je nepovratna radnja!</string>
<string name="remove_watched_popup_warning">Emitiranja koji su gledani prije i nakon dodavanja u playlistu će se ukloniti. \nStvarno ih želiš ukloniti?</string>
<string name="no_playlist_bookmarked_yet">Još nema zabilježenih playlista</string>
<string name="select_a_playlist">Odaberi playlistu</string>
<string name="recovering">obnavljanje</string>

View File

@@ -129,7 +129,7 @@
<string name="external_player_unsupported_link_type">A külső lejátszó nem támogatja az ilyen típusú hivatkozásokat</string>
<string name="video_streams_empty">Nem található videófolyam</string>
<string name="audio_streams_empty">Nem található hangfolyam</string>
<string name="info_labels">Mi:\\nKérés:\\nTartalom nyelve:\\nTartalom származási országa:\\nAlkalmazás nyelve:\\nSzolgáltatás:\\nGMT idő:\\nCsomag:\\nVerzió:\\nOperációs rendszer verzió:</string>
<string name="info_labels">Mi:\nKérés:\nTartalom nyelve:\nTartalom származási országa:\nAlkalmazás nyelve:\nSzolgáltatás:\nGMT idő:\nCsomag:\nVerzió:\nOperációs rendszer verzió:</string>
<string name="search_no_results">Nincs találat</string>
<string name="controls_download_desc">Közvetítési fájl letöltése</string>
<string name="controls_add_to_playlist_title">Hozzáadás ehhez</string>
@@ -623,7 +623,6 @@
<string name="remove_watched_popup_warning">A lejátszási listához való hozzáadás előtt és után megtekintett közvetítések el lesznek távolítva.\nBiztos benne?</string>
<string name="show_original_time_ago_summary">A szolgáltatásokból származó eredeti szövegek láthatók lesznek a közvetítési elemeken</string>
<string name="crash_the_player">Lejátszó összeomlasztása</string>
<string name="show_image_indicators_title">Képjelölők megjelenítése</string>
<string name="show_crash_the_player_title">A „Lejátszó összeomlasztása” lehetőség megjelenítése</string>
<string name="show_crash_the_player_summary">Megjeleníti az összeomlasztási lehetőséget a lejátszó használatakor</string>
<string name="unhook_checkbox">Hangmagasság megtartása (torzítást okozhat)</string>

View File

@@ -76,7 +76,7 @@
<string name="no_available_dir">Silakan pilih folder unduhan di pengaturan</string>
<string name="no_player_found">Pemutar penjaliran tidak ditemukan. Pasang VLC?</string>
<string name="app_ui_crash">App/UI rusak</string>
<string name="info_labels">Apa:\\nPermintaan:\\nBahasa Konten:\\nNegara Konten:\\nBahasa Apl:\\nLayanan:\\nWaktu GMT:\\nPaket:\\nVersi:\\nVersi OS:</string>
<string name="info_labels">Apa:\nPermintaan:\nBahasa Konten:\nNegara Konten:\nBahasa Apl:\nLayanan:\nWaktu GMT:\nPaket:\nVersi:\nVersi OS:</string>
<string name="msg_threads">Thread</string>
<string name="title_activity_recaptcha">Tantangan reCAPTCHA</string>
<string name="recaptcha_request_toast">Meminta kode reCAPTCHA</string>

View File

@@ -212,7 +212,7 @@
<string name="error_snackbar_message">Því miður fór eitthvað úrskeiðis.</string>
<string name="detail_likes_img_view_description">Líkar við</string>
<string name="what_happened_headline">Það sem gerðist:</string>
<string name="info_labels">Hvað:\\nBeiðni:\\nTungumál Efnis:\\nLand Efnis:\\nTungumál forrits:\\nÞjónusta:\\nGMT Tími:\\nPakki:\\nÚtgáfa:\\nÚtgáfu Stýrikerfis:</string>
<string name="info_labels">Hvað:\nBeiðni:\nTungumál efnis:\nLandsvæði efnis:\nTungumál forrits:\nÞjónusta:\nTímastimpill:\nPakki:\nÚtgáfa:\nÚtgáfa stýrikerfis:</string>
<string name="your_comment">Athugasemd þín (á ensku):</string>
<string name="search_no_results">Engar niðurstöður</string>
<string name="video">Myndskeið</string>
@@ -369,7 +369,7 @@
<string name="remote_search_suggestions">Fjarleitar leitartillögur</string>
<string name="resume_on_audio_focus_gain_title">Halda áfram</string>
<string name="unsupported_url">Óstudd vefslóð</string>
<string name="default_content_country_title">Sjálfgefið efnisland</string>
<string name="default_content_country_title">Sjálfgefið landsvæði efnis</string>
<string name="peertube_instance_url_title">PeerTube þjónar</string>
<string name="peertube_instance_add_title">Bæta við</string>
<string name="peertube_instance_add_help">Sláðu inn slóð tilviks</string>
@@ -637,7 +637,7 @@
<string name="show_original_time_ago_summary">Upprunalegir textar frá þjónustu verða sýnilegir í streymisatriðum</string>
<string name="disable_media_tunneling_title">Slökkva á margmiðlagöngum</string>
<string name="disable_media_tunneling_summary">Slökktu á margmiðlunargöngum (media tunneling) ef vart verður við svartan skjá eða hökt við spilun myndskeiða.</string>
<string name="show_crash_the_player_title">Sýna „Láta spilara hrynja\\</string>
<string name="show_crash_the_player_title">Sýna „Láta spilara hrynja\"</string>
<string name="show_crash_the_player_summary">Sýna valkost til að hrynja spilara</string>
<string name="crash_the_app">Hrynja forrit</string>
<string name="create_error_notification">а til villutilkynningu</string>
@@ -815,4 +815,10 @@
<string name="delete_entry">Eyða færslu</string>
<string name="account_terminated_service_provides_reason">Aðgangi lokað\n\n%1$s gefur þessa ástæðu: %2$s</string>
<string name="entry_deleted">Færslu eytt</string>
<string name="migration_info_7_8_title">Samsettur vinsældalisti YouTube fjarlægður</string>
<string name="migration_info_7_8_message">YouTube hætti með samsetta vinsældalistann sinn frá og með 21. júlí 2025. NewPipe skipti út sjálfgefna vinsældalistanum fyrir vinsæl streymi í beinni útsendingu.\n\nÞú getur líka valið annað vinsælt efni með því að fara í \"Stillingar &gt; Efni &gt; Efni aðalsíðu\".</string>
<string name="player_http_403">Tók við HTTP-villu 403 frá þjóni á meðan afspilun stóð, líklega vegna útrunninar URL-slóðar streymis eða banns á IP-vistfang</string>
<string name="player_http_invalid_status">Tók við HTTP-villu %1$s frá þjóni á meðan afspilun stóð</string>
<string name="youtube_player_http_403">Tók við HTTP-villu 403 frá þjóni á meðan afspilun stóð, líklega vegna banns á IP-vistfang eða vandamála með afkóðun URL-slóðar streymis</string>
<string name="unsupported_content_in_country">Þetta efni er ekki í boði fyrir valið landsvæði efnis.\n\nBreyttu valinu með því að fara í \"Stillingar &gt; Efni &gt; Sjálfgefið landsvæði efnis\".</string>
</resources>

View File

@@ -78,7 +78,7 @@
<string name="msg_copied">Copiato negli appunti</string>
<string name="no_available_dir">Seleziona una cartella per i file scaricati</string>
<string name="app_ui_crash">L\'app/UI si è interrotta</string>
<string name="info_labels">Cosa:\\nRichiesta:\\nLingua contenuti:\\nPaese contenuti\\nLingua app:\\nServizio:\\nOrario GMT:\\nPacchetto:\\nVersione:\\nVersione SO:</string>
<string name="info_labels">Cosa:\nRichiesta:\nLingua contenuti:\nPaese contenuti\nLingua app:\nServizio:\nOrario GMT:\nPacchetto:\nVersione:\nVersione SO:</string>
<string name="title_activity_recaptcha">Risoluzione reCAPTCHA</string>
<string name="black_theme_title">Nero</string>
<string name="all">Tutto</string>

View File

@@ -78,7 +78,7 @@
<string name="downloads_title">ダウンロード</string>
<string name="error_report_title">不具合報告</string>
<string name="app_ui_crash">アプリ(UI)がクラッシュしました</string>
<string name="info_labels">どんな問題:\\nリクエスト:\\nコンテンツ言語:\\nコンテンツ国:\\nアプリ言語:\\nサービス:\\nGMT 時間:\\nパッケージ:\\nバージョン:\\nOSバージョン:</string>
<string name="info_labels">どんな問題:\nリクエスト:\nコンテンツ言語:\nコンテンツ国:\nアプリ言語:\nサービス:\nGMT 時間:\nパッケージ:\nバージョン:\nOSバージョン:</string>
<string name="title_activity_recaptcha">reCAPTCHA の要求</string>
<string name="recaptcha_request_toast">reCAPTCHA を要求しました</string>
<string name="black_theme_title">ブラック</string>

View File

@@ -603,7 +603,7 @@
<string name="external_player_unsupported_link_type">გარე დამკვრელს არ აქვთ ამ ტიპის ბმულების მხარდაჭერა</string>
<string name="invalid_file">ფაილი არ არსებობს ან მასზე წაკითხვის ან ჩაწერის ნებართვა აკლია</string>
<string name="saved_tabs_invalid_json">შენახული ჩანართების წაკითხვა ვერ მოხერხდა, ამიტომ გამოიყენეთ ნაგულისხმევი ჩანართები</string>
<string name="info_labels">რა:\\nმოითხოვეთ:\\nშემცველობის ენა:\\nშემცველობის ქვეყანა:\\nაპლიკაციის ენა:\\nსერვისი:\\nGMT დრო:\\nპაკეტი:\\nვერსია:\\nOS ვერსია:</string>
<string name="info_labels">რა:\nმოითხოვეთ:\nშემცველობის ენა:\nშემცველობის ქვეყანა:\nაპლიკაციის ენა:\nსერვისი:\nGMT დრო:\nპაკეტი:\nვერსია:\nOS ვერსია:</string>
<string name="detail_uploader_thumbnail_view_description">ამტვირთველის ავატარის ესკიზი</string>
<string name="comments_are_disabled">კომენტარები გამორთულია</string>
<string name="create">Შექმნა</string>

View File

@@ -64,7 +64,7 @@
<string name="detail_thumbnail_view_description">Vîdeo, demdirêj bilîze:</string>
<string name="error_details_headline">Hûrî:</string>
<string name="your_comment">Şîroveya we (bi Îngilîzî):</string>
<string name="info_labels">Çi:\\nRequest:\\nContent Language:\\nContent Welat:\\nApp Language:\\nService:\\nGMT Dem:\\nPackage:\\nVersion:\\nOS version:</string>
<string name="info_labels">Çi:\nRequest:\nContent Language:\nContent Welat:\nApp Language:\nService:\nGMT Dem:\nPackage:\nVersion:\nOS version:</string>
<string name="what_happened_headline">Çi qewimî:</string>
<string name="what_device_headline">Agahdarî:</string>
<string name="error_snackbar_action">Nûçe</string>

View File

@@ -113,7 +113,7 @@
<string name="player_stream_failure">이 스트림을 재생할 수 없습니다</string>
<string name="player_unrecoverable_failure">복구할 수 없는 플레이어 오류가 발생했습니다</string>
<string name="player_recoverable_failure">플레이어 오류로부터 복구 중</string>
<string name="info_labels">무엇:\\n요청:\\n콘텐츠 언어:\\n콘텐츠 국가:\\n앱 언어:\\n서비스:\\nGMT 시간:\\n패키지:\\n버전:\\nOS 버전:</string>
<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="no_subscribers">구독자 없음</string>

View File

@@ -328,7 +328,7 @@
<string name="player_recoverable_failure">گێڕانەوەی کارپێکەر بۆکاتی پێش کێشە</string>
<string name="invalid_directory">هەمان فۆڵدەر بوونی نییە</string>
<string name="invalid_source">هەمان فایل/بابەت بوونی نییە</string>
<string name="info_labels">چی:\\اواکراو:\\اوەڕۆک:\\انگ:\\nخزمەتگوزاری:\\nGMT:\\ات:\\اکێج:\\ەشان:\\ەشانی سیستەم:</string>
<string name="info_labels">چی:\nداواکراو:\nناوەڕۆک:\nلانگ:\nخزمەتگوزاری:\nGMT:\nکات:\nپاکێج:\nوەشان:\nوەشانی سیستەم:</string>
<string name="settings_file_charset_title">هێما ڕێگەپێدراوەکان لە فایلێکی ناویدا</string>
<string name="settings_file_replacement_character_summary">هێما نادروستەکان بەم بەهایە جێگۆڕکێ دەکرێن</string>
<string name="settings_file_replacement_character_title">هێمای جێگۆڕین</string>

View File

@@ -74,7 +74,7 @@
<string name="error_snackbar_action">Ataskaita</string>
<string name="what_device_headline">Informacija:</string>
<string name="what_happened_headline">Kas nutiko:</string>
<string name="info_labels">Kas:\\nUžklausa:\\nTurinio Kalba:\\nTurinio Šalis:\\nProgramėlės Kalba:\\nPaslauga:\\nGMT Laikas:\\nPaketas:\\nVersija:\\nOS versija:</string>
<string name="info_labels">Kas:\nUžklausa:\nTurinio Kalba:\nTurinio Šalis:\nProgramėlės Kalba:\nPaslauga:\nGMT Laikas:\nPaketas:\nVersija:\nOS versija:</string>
<string name="your_comment">Jūsų komentaras (anglų kalba):</string>
<string name="error_details_headline">Išsami informacija:</string>
<string name="detail_thumbnail_view_description">Paleisti vaizdo įrašą, trukmė:</string>

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