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:
@@ -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
10
app/lint.xml
Normal 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>
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
36
app/src/main/java/org/schabi/newpipe/ExitActivity.kt
Normal file
36
app/src/main/java/org/schabi/newpipe/ExitActivity.kt
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
281
app/src/main/java/org/schabi/newpipe/error/ErrorActivity.kt
Normal file
281
app/src/main/java/org/schabi/newpipe/error/ErrorActivity.kt
Normal 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"
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -126,6 +126,7 @@ public class ReCaptchaActivity extends AppCompatActivity {
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressLint("MissingSuperCall") // saveCookiesAndFinish method handles back navigation
|
||||
public void onBackPressed() {
|
||||
saveCookiesAndFinish();
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -185,7 +185,9 @@ class FeedLoadService : Service() {
|
||||
}
|
||||
}
|
||||
|
||||
notificationManager.notify(NOTIFICATION_ID, notificationBuilder.build())
|
||||
if (notificationManager.areNotificationsEnabled()) {
|
||||
notificationManager.notify(NOTIFICATION_ID, notificationBuilder.build())
|
||||
}
|
||||
}
|
||||
|
||||
// /////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -47,6 +47,9 @@ abstract class BasePlayerGestureListener(
|
||||
startMultiDoubleTap(event)
|
||||
} else if (portion === DisplayPortion.MIDDLE) {
|
||||
player.playPause()
|
||||
if (player.isPlaying) {
|
||||
playerUi.hideControls(0, 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
52
app/src/main/java/org/schabi/newpipe/util/PeertubeHelper.kt
Normal file
52
app/src/main/java/org/schabi/newpipe/util/PeertubeHelper.kt
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
168
app/src/main/java/org/schabi/newpipe/util/ServiceHelper.kt
Normal file
168
app/src/main/java/org/schabi/newpipe/util/ServiceHelper.kt
Normal 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) }
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
54
app/src/main/java/org/schabi/newpipe/util/StreamTypeUtil.kt
Normal file
54
app/src/main/java/org/schabi/newpipe/util/StreamTypeUtil.kt
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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Сэрвіс:\\nЧас GMT:\\nПакет:\\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>
|
||||
|
||||
@@ -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Версия:\\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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
\n
|
||||
\nبۆیە هەڵژرادن بۆ خۆت دەگەڕێتەوە: زانیاری تەواو یان خێرا.</string>
|
||||
<string name="app_license">نیوپایپ نهرمهوالایهكی سەرچاوە کراوەیە : دەتوانیت بەکاریبهێنیت، بیخوێنیتەوە، هاوبەشی پێبکەیت ،بەرەوپێشی ببەیت. بەتایبەتی دەتوانی دابەشیبکەیتەوە یاخوود بگۆڕیت بەپێی مەرجەکانی GNU مۆڵەتنامەی گشتی وەک نهرمهواڵایهكی بڵاوی خۆڕایی, بەهۆی وەشانی ٣ ی مۆڵەتنامە، یان هەر وەشانێکی دوواتر.</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="privacy_policy_encouragement">پڕۆژەی نیوپایپ زانیارییە تایبەتییەکانت بە وردی دەپارێزێت. هەروەها بهرنامهكه هیچ زانایارییەکت بەبێ ئاگاداری تۆ بەکارنابات.
|
||||
\nسیاسەتی تایبەتی نیوپایپ بە وردی ڕوونکردنەوەت دەداتێ لەسەر ئەو زانیاریانەی وەریاندەگرێت و بەکاریاندەبات.</string>
|
||||
<string name="download_to_sdcard_error_message">ناتوانرێت لە بیرگەی دەرەکیدا داببەزێنرێت . شوێنی فۆڵدهری دابهزاندنەکان ڕێکبخرێتەوە؟</string>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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Υπηρεσία:\\nΏρα GMT:\\nΠακέτο:\\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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">این اجازه برای گشودن در حالت
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
3
app/src/main/res/values-gd/strings.xml
Normal file
3
app/src/main/res/values-gd/strings.xml
Normal file
@@ -0,0 +1,3 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
</resources>
|
||||
@@ -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>
|
||||
|
||||
@@ -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גרסה:\\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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">Búа 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 > Efni > 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 > Efni > Sjálfgefið landsvæði efnis\".</string>
|
||||
</resources>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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داواکراو:\\nناوەڕۆک:\\nلانگ:\\nخزمەتگوزاری:\\nGMT:\\nکات:\\nپاکێج:\\nوەشان:\\nوەشانی سیستەم:</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>
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user