Merge branch 'dev' into fix-download-failure-handling-bug

This commit is contained in:
CloudyRowly 2024-03-21 01:08:51 +11:00 committed by GitHub
commit 4d759aee6a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
303 changed files with 6250 additions and 2635 deletions

View File

@ -36,7 +36,7 @@ jobs:
contents: read
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- uses: gradle/wrapper-validation-action@v1
- name: create and checkout branch
@ -47,7 +47,7 @@ jobs:
run: git checkout -B "$BRANCH"
- name: set up JDK 17
uses: actions/setup-java@v3
uses: actions/setup-java@v4
with:
java-version: 17
distribution: "temurin"
@ -57,7 +57,7 @@ jobs:
run: ./gradlew assembleDebug lintDebug testDebugUnitTest --stacktrace -DskipFormatKtlint
- name: Upload APK
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: app
path: app/build/outputs/apk/debug/*.apk
@ -80,10 +80,10 @@ jobs:
contents: read
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: set up JDK 17
uses: actions/setup-java@v3
uses: actions/setup-java@v4
with:
java-version: 17
distribution: "temurin"
@ -98,7 +98,7 @@ jobs:
script: ./gradlew connectedCheck --stacktrace
- name: Upload test report when tests fail # because the printed out stacktrace (console) is too short, see also #7553
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
if: failure()
with:
name: android-test-report-api${{ matrix.api-level }}
@ -111,12 +111,12 @@ jobs:
contents: read
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
with:
fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis
- name: Set up JDK 17
uses: actions/setup-java@v3
uses: actions/setup-java@v4
with:
java-version: 17
distribution: "temurin"

View File

@ -17,9 +17,9 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- uses: actions/setup-node@v3
- uses: actions/setup-node@v4
with:
node-version: 16
@ -27,7 +27,7 @@ jobs:
run: npm i probe-image-size@7.2.3 --ignore-scripts
- name: Minimize simple images
uses: actions/github-script@v6
uses: actions/github-script@v7
timeout-minutes: 3
with:
script: |

View File

@ -13,7 +13,7 @@
<a href="https://github.com/TeamNewPipe/NewPipe/actions" alt="Build Status"><img src="https://github.com/TeamNewPipe/NewPipe/workflows/CI/badge.svg?branch=dev&event=push"></a>
<a href="https://hosted.weblate.org/engage/newpipe/" alt="Translation Status"><img src="https://hosted.weblate.org/widgets/newpipe/-/svg-badge.svg"></a>
<a href="https://web.libera.chat/#newpipe" alt="IRC channel: #newpipe"><img src="https://img.shields.io/badge/IRC%20chat-%23newpipe-brightgreen.svg"></a>
<a href="https://www.bountysource.com/teams/newpipe" alt="Bountysource bounties"><img src="https://img.shields.io/bountysource/team/newpipe/activity.svg?colorB=cd201f"></a>
<a href="https://matrix.to/#/#newpipe:matrix.newpipe-ev.de" alt="Matrix channel: #newpipe"><img src="https://img.shields.io/badge/Matrix%20chat-%23newpipe-blue"></a>
</p>
<hr>
<p align="center"><a href="#screenshots">Screenshots</a> &bull; <a href="#supported-services">Supported Services</a> &bull; <a href="#description">Description</a> &bull; <a href="#features">Features</a> &bull; <a href="#installation-and-updates">Installation and updates</a> &bull; <a href="#contribution">Contribution</a> &bull; <a href="#donate">Donate</a> &bull; <a href="#license">License</a></p>
@ -126,16 +126,6 @@ If you like NewPipe, you're welcome to send a donation. We prefer Liberapay, as
<td><a href="https://liberapay.com/TeamNewPipe/"><img src="assets/liberapay_qr_code.png" alt="Visit NewPipe at liberapay.com" width="100px"></a></td>
<td><a href="https://liberapay.com/TeamNewPipe/donate"><img src="assets/liberapay_donate_button.svg" alt="Donate via Liberapay" height="35px"></a></td>
</tr>
<tr>
<td><img src="https://bitcoin.org/img/icons/logotop.svg" alt="Bitcoin"></td>
<td><img src="assets/bitcoin_qr_code.png" alt="Bitcoin QR code" width="100px"></td>
<td><samp>16A9J59ahMRqkLSZjhYj33n9j3fMztFxnh</samp></td>
</tr>
<tr>
<td><a href="https://www.bountysource.com/teams/newpipe"><img src="https://upload.wikimedia.org/wikipedia/commons/thumb/2/22/Bountysource.png/320px-Bountysource.png" alt="Bountysource" width="190px"></a></td>
<td><a href="https://www.bountysource.com/teams/newpipe"><img src="assets/bountysource_qr_code.png" alt="Visit NewPipe at bountysource.com" width="100px"></a></td>
<td><a href="https://www.bountysource.com/teams/newpipe/issues"><img src="https://img.shields.io/bountysource/team/newpipe/activity.svg?colorB=cd201f" height="30px" alt="Check out how many bounties you can earn."></a></td>
</tr>
</table>
## Privacy Policy

View File

@ -12,7 +12,7 @@ plugins {
}
android {
compileSdk 33
compileSdk 34
namespace 'org.schabi.newpipe'
defaultConfig {
@ -20,8 +20,8 @@ android {
resValue "string", "app_name", "NewPipe"
minSdk 21
targetSdk 33
versionCode 994
versionName "0.25.2"
versionCode 996
versionName "0.26.1"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
@ -98,7 +98,9 @@ android {
resources {
// remove two files which belong to jsoup
// no idea how they ended up in the META-INF dir...
excludes += ['META-INF/README.md', 'META-INF/CHANGES']
excludes += ['META-INF/README.md', 'META-INF/CHANGES',
// 'COPYRIGHT' belongs to RxJava...
'META-INF/COPYRIGHT']
}
}
}
@ -106,9 +108,9 @@ android {
ext {
checkstyleVersion = '10.12.1'
androidxLifecycleVersion = '2.5.1'
androidxRoomVersion = '2.5.2'
androidxWorkVersion = '2.7.1'
androidxLifecycleVersion = '2.6.2'
androidxRoomVersion = '2.6.1'
androidxWorkVersion = '2.8.1'
icepickVersion = '3.2.0'
exoPlayerVersion = '2.18.7'
@ -118,7 +120,6 @@ ext {
leakCanaryVersion = '2.12'
stethoVersion = '1.6.0'
mockitoVersion = '4.0.0'
}
configurations {
@ -133,7 +134,7 @@ checkstyle {
toolVersion = checkstyleVersion
}
task runCheckstyle(type: Checkstyle) {
tasks.register('runCheckstyle', Checkstyle) {
source 'src'
include '**/*.java'
exclude '**/gen/**'
@ -154,7 +155,7 @@ task runCheckstyle(type: Checkstyle) {
def outputDir = "${project.buildDir}/reports/ktlint/"
def inputFiles = project.fileTree(dir: "src", include: "**/*.kt")
task runKtlint(type: JavaExec) {
tasks.register('runKtlint', JavaExec) {
inputs.files(inputFiles)
outputs.dir(outputDir)
getMainClass().set("com.pinterest.ktlint.Main")
@ -163,7 +164,7 @@ task runKtlint(type: JavaExec) {
jvmArgs("--add-opens", "java.base/java.lang=ALL-UNNAMED")
}
task formatKtlint(type: JavaExec) {
tasks.register('formatKtlint', JavaExec) {
inputs.files(inputFiles)
outputs.dir(outputDir)
getMainClass().set("com.pinterest.ktlint.Main")
@ -189,7 +190,7 @@ sonar {
dependencies {
/** Desugaring **/
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs_nio:2.0.3'
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs_nio:2.0.4'
/** NewPipe libraries **/
// You can use a local version by uncommenting a few lines in settings.gradle
@ -197,7 +198,7 @@ dependencies {
// name and the commit hash with the commit hash of the (pushed) commit you want to test
// This works thanks to JitPack: https://jitpack.io/
implementation 'com.github.TeamNewPipe:nanojson:1d9e1aea9049fc9f85e68b43ba39fe7be1c1f751'
implementation 'com.github.TeamNewPipe:NewPipeExtractor:289db1178ab66694c23893e6a487d4708343c4'
implementation 'com.github.TeamNewPipe:NewPipeExtractor:v0.23.1'
implementation 'com.github.TeamNewPipe:NoNonsense-FilePicker:5.0.0'
/** Checkstyle **/
@ -208,28 +209,28 @@ dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib:${kotlin_version}"
/** AndroidX **/
implementation 'androidx.appcompat:appcompat:1.5.1'
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'androidx.cardview:cardview:1.0.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation 'androidx.core:core-ktx:1.10.0'
implementation 'androidx.core:core-ktx:1.12.0'
implementation 'androidx.documentfile:documentfile:1.0.1'
implementation 'androidx.fragment:fragment-ktx:1.4.1'
implementation 'androidx.fragment:fragment-ktx:1.6.2'
implementation "androidx.lifecycle:lifecycle-livedata-ktx:${androidxLifecycleVersion}"
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:${androidxLifecycleVersion}"
implementation 'androidx.localbroadcastmanager:localbroadcastmanager:1.1.0'
implementation 'androidx.media:media:1.6.0'
implementation 'androidx.preference:preference:1.2.0'
implementation 'androidx.recyclerview:recyclerview:1.2.1'
implementation 'androidx.media:media:1.7.0'
implementation 'androidx.preference:preference:1.2.1'
implementation 'androidx.recyclerview:recyclerview:1.3.2'
implementation "androidx.room:room-runtime:${androidxRoomVersion}"
implementation "androidx.room:room-rxjava3:${androidxRoomVersion}"
kapt "androidx.room:room-compiler:${androidxRoomVersion}"
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
// Newer version specified to prevent accessibility regressions with RecyclerView, see:
// https://developer.android.com/jetpack/androidx/releases/viewpager2#1.1.0-alpha01
implementation 'androidx.viewpager2:viewpager2:1.1.0-beta01'
implementation 'androidx.viewpager2:viewpager2:1.1.0-beta02'
implementation "androidx.work:work-runtime-ktx:${androidxWorkVersion}"
implementation "androidx.work:work-rxjava3:${androidxWorkVersion}"
implementation 'com.google.android.material:material:1.9.0'
implementation 'com.google.android.material:material:1.11.0'
/** Third-party libraries **/
// Instance state boilerplate elimination
@ -237,13 +238,10 @@ dependencies {
kapt "frankiesardo:icepick-processor:${icepickVersion}"
// HTML parser
implementation "org.jsoup:jsoup:1.16.1"
implementation "org.jsoup:jsoup:1.17.2"
// HTTP client
implementation "com.squareup.okhttp3:okhttp:4.11.0"
// okhttp3:4.11.0 introduces a vulnerability from com.squareup.okio:okio@3.3.0,
// remove com.squareup.okio:okio when updating okhttp
implementation "com.squareup.okio:okio:3.4.0"
implementation "com.squareup.okhttp3:okhttp:4.12.0"
// Media player
implementation "com.google.android.exoplayer:exoplayer-core:${exoPlayerVersion}"
@ -272,19 +270,19 @@ dependencies {
implementation "io.noties.markwon:linkify:${markwonVersion}"
// Crash reporting
implementation "ch.acra:acra-core:5.10.1"
implementation "ch.acra:acra-core:5.11.3"
// Properly restarting
implementation 'com.jakewharton:process-phoenix:2.1.2'
// Reactive extensions for Java VM
implementation "io.reactivex.rxjava3:rxjava:3.1.6"
implementation "io.reactivex.rxjava3:rxjava:3.1.8"
implementation "io.reactivex.rxjava3:rxandroid:3.0.2"
// RxJava binding APIs for Android UI widgets
implementation "com.jakewharton.rxbinding4:rxbinding:4.0.0"
// Date and time formatting
implementation "org.ocpsoft.prettytime:prettytime:5.0.6.Final"
implementation "org.ocpsoft.prettytime:prettytime:5.0.7.Final"
/** Debugging **/
// Memory leak detection
@ -297,13 +295,12 @@ dependencies {
/** Testing **/
testImplementation 'junit:junit:4.13.2'
testImplementation "org.mockito:mockito-core:${mockitoVersion}"
testImplementation "org.mockito:mockito-inline:${mockitoVersion}"
testImplementation 'org.mockito:mockito-core:5.6.0'
androidTestImplementation "androidx.test.ext:junit:1.1.5"
androidTestImplementation "androidx.test:runner:1.5.2"
androidTestImplementation "androidx.room:room-testing:${androidxRoomVersion}"
androidTestImplementation "org.assertj:assertj-core:3.23.1"
androidTestImplementation "org.assertj:assertj-core:3.24.2"
}
static String getGitWorkingBranch() {

View File

@ -0,0 +1,130 @@
package org.schabi.newpipe.database
import android.content.Context
import androidx.room.Room
import androidx.test.core.app.ApplicationProvider
import io.reactivex.rxjava3.core.Single
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull
import org.junit.Before
import org.junit.Test
import org.schabi.newpipe.database.feed.dao.FeedDAO
import org.schabi.newpipe.database.feed.model.FeedEntity
import org.schabi.newpipe.database.feed.model.FeedGroupEntity
import org.schabi.newpipe.database.stream.StreamWithState
import org.schabi.newpipe.database.stream.dao.StreamDAO
import org.schabi.newpipe.database.stream.model.StreamEntity
import org.schabi.newpipe.database.subscription.SubscriptionDAO
import org.schabi.newpipe.database.subscription.SubscriptionEntity
import org.schabi.newpipe.extractor.ServiceList
import org.schabi.newpipe.extractor.channel.ChannelInfo
import org.schabi.newpipe.extractor.stream.StreamType
import java.io.IOException
import java.time.OffsetDateTime
import kotlin.streams.toList
class FeedDAOTest {
private lateinit var db: AppDatabase
private lateinit var feedDAO: FeedDAO
private lateinit var streamDAO: StreamDAO
private lateinit var subscriptionDAO: SubscriptionDAO
private val serviceId = ServiceList.YouTube.serviceId
private val stream1 = StreamEntity(1, serviceId, "https://youtube.com/watch?v=1", "stream 1", StreamType.VIDEO_STREAM, 1000, "channel-1", "https://youtube.com/channel/1", "https://i.ytimg.com/vi/1/hqdefault.jpg", 100, "2023-01-01", OffsetDateTime.parse("2023-01-01T00:00:00Z"))
private val stream2 = StreamEntity(2, serviceId, "https://youtube.com/watch?v=2", "stream 2", StreamType.VIDEO_STREAM, 1000, "channel-1", "https://youtube.com/channel/1", "https://i.ytimg.com/vi/1/hqdefault.jpg", 100, "2023-01-02", OffsetDateTime.parse("2023-01-02T00:00:00Z"))
private val stream3 = StreamEntity(3, serviceId, "https://youtube.com/watch?v=3", "stream 3", StreamType.LIVE_STREAM, 1000, "channel-1", "https://youtube.com/channel/1", "https://i.ytimg.com/vi/1/hqdefault.jpg", 100, "2023-01-03", OffsetDateTime.parse("2023-01-03T00:00:00Z"))
private val stream4 = StreamEntity(4, serviceId, "https://youtube.com/watch?v=4", "stream 4", StreamType.VIDEO_STREAM, 1000, "channel-2", "https://youtube.com/channel/2", "https://i.ytimg.com/vi/1/hqdefault.jpg", 100, "2023-08-10", OffsetDateTime.parse("2023-08-10T00:00:00Z"))
private val stream5 = StreamEntity(5, serviceId, "https://youtube.com/watch?v=5", "stream 5", StreamType.VIDEO_STREAM, 1000, "channel-2", "https://youtube.com/channel/2", "https://i.ytimg.com/vi/1/hqdefault.jpg", 100, "2023-08-20", OffsetDateTime.parse("2023-08-20T00:00:00Z"))
private val stream6 = StreamEntity(6, serviceId, "https://youtube.com/watch?v=6", "stream 6", StreamType.VIDEO_STREAM, 1000, "channel-3", "https://youtube.com/channel/3", "https://i.ytimg.com/vi/1/hqdefault.jpg", 100, "2023-09-01", OffsetDateTime.parse("2023-09-01T00:00:00Z"))
private val stream7 = StreamEntity(7, serviceId, "https://youtube.com/watch?v=7", "stream 7", StreamType.VIDEO_STREAM, 1000, "channel-4", "https://youtube.com/channel/4", "https://i.ytimg.com/vi/1/hqdefault.jpg", 100, "2023-08-10", OffsetDateTime.parse("2023-08-10T00:00:00Z"))
private val allStreams = listOf(
stream1, stream2, stream3, stream4, stream5, stream6, stream7
)
@Before
fun createDb() {
val context = ApplicationProvider.getApplicationContext<Context>()
db = Room.inMemoryDatabaseBuilder(
context, AppDatabase::class.java
).build()
feedDAO = db.feedDAO()
streamDAO = db.streamDAO()
subscriptionDAO = db.subscriptionDAO()
}
@After
@Throws(IOException::class)
fun closeDb() {
db.close()
}
@Test
fun testUnlinkStreamsOlderThan_KeepOne() {
setupUnlinkDelete("2023-08-15T00:00:00Z")
val streams = feedDAO.getStreams(
FeedGroupEntity.GROUP_ALL_ID, includePlayed = true, includePartiallyPlayed = true, null
)
.blockingGet()
val allowedStreams = listOf(stream3, stream5, stream6, stream7)
assertEqual(streams, allowedStreams)
}
@Test
fun testUnlinkStreamsOlderThan_KeepMultiple() {
setupUnlinkDelete("2023-08-01T00:00:00Z")
val streams = feedDAO.getStreams(
FeedGroupEntity.GROUP_ALL_ID, includePlayed = true, includePartiallyPlayed = true, null
)
.blockingGet()
val allowedStreams = listOf(stream3, stream4, stream5, stream6, stream7)
assertEqual(streams, allowedStreams)
}
private fun assertEqual(streams: List<StreamWithState>?, allowedStreams: List<StreamEntity>) {
assertNotNull(streams)
assertEquals(
allowedStreams,
streams!!
.map { it.stream }
.sortedBy { it.uid }
.toList()
)
}
private fun setupUnlinkDelete(time: String) {
clearAndFillTables()
Single.fromCallable {
feedDAO.unlinkStreamsOlderThan(OffsetDateTime.parse(time))
}.blockingSubscribe()
Single.fromCallable {
streamDAO.deleteOrphans()
}.blockingSubscribe()
}
private fun clearAndFillTables() {
db.clearAllTables()
streamDAO.insertAll(allStreams)
subscriptionDAO.insertAll(
listOf(
SubscriptionEntity.from(ChannelInfo(serviceId, "1", "https://youtube.com/channel/1", "https://youtube.com/channel/1", "channel-1")),
SubscriptionEntity.from(ChannelInfo(serviceId, "2", "https://youtube.com/channel/2", "https://youtube.com/channel/2", "channel-2")),
SubscriptionEntity.from(ChannelInfo(serviceId, "3", "https://youtube.com/channel/3", "https://youtube.com/channel/3", "channel-3")),
SubscriptionEntity.from(ChannelInfo(serviceId, "4", "https://youtube.com/channel/4", "https://youtube.com/channel/4", "channel-4")),
)
)
feedDAO.insertAll(
listOf(
FeedEntity(1, 1),
FeedEntity(2, 1),
FeedEntity(3, 1),
FeedEntity(4, 2),
FeedEntity(5, 2),
FeedEntity(6, 3),
FeedEntity(7, 4),
)
)
}
}

View File

@ -25,6 +25,7 @@ import android.view.ViewGroup;
import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.os.BundleCompat;
import androidx.lifecycle.Lifecycle;
import androidx.viewpager.widget.PagerAdapter;
@ -284,7 +285,7 @@ public abstract class FragmentStatePagerAdapterMenuWorkaround extends PagerAdapt
Bundle state = null;
if (!mSavedState.isEmpty()) {
state = new Bundle();
state.putParcelableArray("states", mSavedState.toArray(new Fragment.SavedState[0]));
state.putParcelableArrayList("states", mSavedState);
}
for (int i = 0; i < mFragments.size(); i++) {
final Fragment f = mFragments.get(i);
@ -311,13 +312,12 @@ public abstract class FragmentStatePagerAdapterMenuWorkaround extends PagerAdapt
if (state != null) {
final Bundle bundle = (Bundle) state;
bundle.setClassLoader(loader);
final Parcelable[] fss = bundle.getParcelableArray("states");
final var states = BundleCompat.getParcelableArrayList(bundle, "states",
Fragment.SavedState.class);
mSavedState.clear();
mFragments.clear();
if (fss != null) {
for (final Parcelable parcelable : fss) {
mSavedState.add((Fragment.SavedState) parcelable);
}
if (states != null) {
mSavedState.addAll(states);
}
final Iterable<String> keys = bundle.keySet();
for (final String key : keys) {

View File

@ -120,9 +120,20 @@ public abstract class BaseFragment extends Fragment {
}
}
/**
* Finds the root fragment by looping through all of the parent fragments. The root fragment
* is supposed to be {@link org.schabi.newpipe.fragments.MainFragment}, and is the fragment that
* handles keeping the backstack of opened fragments in NewPipe, and also the player bottom
* sheet. This function therefore returns the fragment manager of said fragment.
*
* @return the fragment manager of the root fragment, i.e.
* {@link org.schabi.newpipe.fragments.MainFragment}
*/
protected FragmentManager getFM() {
return getParentFragment() == null
? getFragmentManager()
: getParentFragment().getFragmentManager();
Fragment current = this;
while (current.getParentFragment() != null) {
current = current.getParentFragment();
}
return current.getFragmentManager();
}
}

View File

@ -44,6 +44,7 @@ import android.widget.FrameLayout;
import android.widget.Spinner;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.ActionBarDrawerToggle;
import androidx.appcompat.app.AppCompatActivity;
@ -51,6 +52,7 @@ import androidx.core.app.ActivityCompat;
import androidx.core.view.GravityCompat;
import androidx.drawerlayout.widget.DrawerLayout;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentContainerView;
import androidx.fragment.app.FragmentManager;
import androidx.preference.PreferenceManager;
@ -64,11 +66,13 @@ import org.schabi.newpipe.databinding.ToolbarLayoutBinding;
import org.schabi.newpipe.error.ErrorUtil;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.StreamingService;
import org.schabi.newpipe.extractor.comments.CommentsInfoItem;
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
import org.schabi.newpipe.extractor.services.peertube.PeertubeInstance;
import org.schabi.newpipe.fragments.BackPressable;
import org.schabi.newpipe.fragments.MainFragment;
import org.schabi.newpipe.fragments.detail.VideoDetailFragment;
import org.schabi.newpipe.fragments.list.comments.CommentRepliesFragment;
import org.schabi.newpipe.fragments.list.search.SearchFragment;
import org.schabi.newpipe.local.feed.notifications.NotificationWorker;
import org.schabi.newpipe.player.Player;
@ -546,14 +550,21 @@ public class MainActivity extends AppCompatActivity {
// interacts with a fragment inside fragment_holder so all back presses should be
// handled by it
if (bottomSheetHiddenOrCollapsed()) {
final Fragment fragment = getSupportFragmentManager()
.findFragmentById(R.id.fragment_holder);
final FragmentManager fm = getSupportFragmentManager();
final Fragment fragment = fm.findFragmentById(R.id.fragment_holder);
// If current fragment implements BackPressable (i.e. can/wanna handle back press)
// delegate the back press to it
if (fragment instanceof BackPressable) {
if (((BackPressable) fragment).onBackPressed()) {
return;
}
} else if (fragment instanceof CommentRepliesFragment) {
// expand DetailsFragment if CommentRepliesFragment was opened
// to show the top level comments again
// Expand DetailsFragment if CommentRepliesFragment was opened
// and no other CommentRepliesFragments are on top of the back stack
// to show the top level comments again.
openDetailFragmentFromCommentReplies(fm, false);
}
} else {
@ -629,10 +640,17 @@ public class MainActivity extends AppCompatActivity {
* </pre>
*/
private void onHomeButtonPressed() {
// If search fragment wasn't found in the backstack...
if (!NavigationHelper.tryGotoSearchFragment(getSupportFragmentManager())) {
// ...go to the main fragment
NavigationHelper.gotoMainFragment(getSupportFragmentManager());
final FragmentManager fm = getSupportFragmentManager();
final Fragment fragment = fm.findFragmentById(R.id.fragment_holder);
if (fragment instanceof CommentRepliesFragment) {
// Expand DetailsFragment if CommentRepliesFragment was opened
// and no other CommentRepliesFragments are on top of the back stack
// to show the top level comments again.
openDetailFragmentFromCommentReplies(fm, true);
} else if (!NavigationHelper.tryGotoSearchFragment(fm)) {
// If search fragment wasn't found in the backstack go to the main fragment
NavigationHelper.gotoMainFragment(fm);
}
}
@ -828,6 +846,68 @@ public class MainActivity extends AppCompatActivity {
}
}
private void openDetailFragmentFromCommentReplies(
@NonNull final FragmentManager fm,
final boolean popBackStack
) {
// obtain the name of the fragment under the replies fragment that's going to be popped
@Nullable final String fragmentUnderEntryName;
if (fm.getBackStackEntryCount() < 2) {
fragmentUnderEntryName = null;
} else {
fragmentUnderEntryName = fm.getBackStackEntryAt(fm.getBackStackEntryCount() - 2)
.getName();
}
// the root comment is the comment for which the user opened the replies page
@Nullable final CommentRepliesFragment repliesFragment =
(CommentRepliesFragment) fm.findFragmentByTag(CommentRepliesFragment.TAG);
@Nullable final CommentsInfoItem rootComment =
repliesFragment == null ? null : repliesFragment.getCommentsInfoItem();
// sometimes this function pops the backstack, other times it's handled by the system
if (popBackStack) {
fm.popBackStackImmediate();
}
// only expand the bottom sheet back if there are no more nested comment replies fragments
// stacked under the one that is currently being popped
if (CommentRepliesFragment.TAG.equals(fragmentUnderEntryName)) {
return;
}
final BottomSheetBehavior<FragmentContainerView> behavior = BottomSheetBehavior
.from(mainBinding.fragmentPlayerHolder);
// do not return to the comment if the details fragment was closed
if (behavior.getState() == BottomSheetBehavior.STATE_HIDDEN) {
return;
}
// scroll to the root comment once the bottom sheet expansion animation is finished
behavior.addBottomSheetCallback(new BottomSheetBehavior.BottomSheetCallback() {
@Override
public void onStateChanged(@NonNull final View bottomSheet,
final int newState) {
if (newState == BottomSheetBehavior.STATE_EXPANDED) {
final Fragment detailFragment = fm.findFragmentById(
R.id.fragment_player_holder);
if (detailFragment instanceof VideoDetailFragment && rootComment != null) {
// should always be the case
((VideoDetailFragment) detailFragment).scrollToComment(rootComment);
}
behavior.removeBottomSheetCallback(this);
}
}
@Override
public void onSlide(@NonNull final View bottomSheet, final float slideOffset) {
// not needed, listener is removed once the sheet is expanded
}
});
behavior.setState(BottomSheetBehavior.STATE_EXPANDED);
}
private boolean bottomSheetHiddenOrCollapsed() {
final BottomSheetBehavior<FrameLayout> bottomSheetBehavior =
BottomSheetBehavior.from(mainBinding.fragmentPlayerHolder);

View File

@ -20,9 +20,7 @@ import com.grack.nanojson.JsonParser
import com.grack.nanojson.JsonParserException
import org.schabi.newpipe.extractor.downloader.Response
import org.schabi.newpipe.extractor.exceptions.ReCaptchaException
import org.schabi.newpipe.util.ReleaseVersionUtil.coerceUpdateCheckExpiry
import org.schabi.newpipe.util.ReleaseVersionUtil.isLastUpdateCheckExpired
import org.schabi.newpipe.util.ReleaseVersionUtil.isReleaseApk
import org.schabi.newpipe.util.ReleaseVersionUtil
import java.io.IOException
class NewVersionWorker(
@ -84,7 +82,7 @@ class NewVersionWorker(
@Throws(IOException::class, ReCaptchaException::class)
private fun checkNewVersion() {
// Check if the current apk is a github one or not.
if (!isReleaseApk()) {
if (!ReleaseVersionUtil.isReleaseApk) {
return
}
@ -93,7 +91,7 @@ class NewVersionWorker(
// Check if the last request has happened a certain time ago
// to reduce the number of API requests.
val expiry = prefs.getLong(applicationContext.getString(R.string.update_expiry_key), 0)
if (!isLastUpdateCheckExpired(expiry)) {
if (!ReleaseVersionUtil.isLastUpdateCheckExpired(expiry)) {
return
}
}
@ -108,7 +106,7 @@ class NewVersionWorker(
try {
// Store a timestamp which needs to be exceeded,
// before a new request to the API is made.
val newExpiry = coerceUpdateCheckExpiry(response.getHeader("expires"))
val newExpiry = ReleaseVersionUtil.coerceUpdateCheckExpiry(response.getHeader("expires"))
prefs.edit {
putLong(applicationContext.getString(R.string.update_expiry_key), newExpiry)
}
@ -120,13 +118,13 @@ class NewVersionWorker(
// Parse the json from the response.
try {
val githubStableObject = JsonParser.`object`()
val newpipeVersionInfo = JsonParser.`object`()
.from(response.responseBody()).getObject("flavors")
.getObject("github").getObject("stable")
.getObject("newpipe")
val versionName = githubStableObject.getString("version")
val versionCode = githubStableObject.getInt("version_code")
val apkLocationUrl = githubStableObject.getString("apk")
val versionName = newpipeVersionInfo.getString("version")
val versionCode = newpipeVersionInfo.getInt("version_code")
val apkLocationUrl = newpipeVersionInfo.getString("apk")
compareAppVersionAndShowNotification(versionName, apkLocationUrl, versionCode)
} catch (e: JsonParserException) {
// Most likely something is wrong in data received from NEWPIPE_API_URL.

View File

@ -116,7 +116,7 @@ class AboutActivity : AppCompatActivity() {
/**
* List of all software components.
*/
private val SOFTWARE_COMPONENTS = arrayOf(
private val SOFTWARE_COMPONENTS = arrayListOf(
SoftwareComponent(
"ACRA", "2013", "Kevin Gaudin",
"https://github.com/ACRA/acra", StandardLicenses.APACHE2

View File

@ -18,6 +18,7 @@ import org.schabi.newpipe.BuildConfig
import org.schabi.newpipe.R
import org.schabi.newpipe.databinding.FragmentLicensesBinding
import org.schabi.newpipe.databinding.ItemSoftwareComponentBinding
import org.schabi.newpipe.ktx.parcelableArrayList
import org.schabi.newpipe.util.Localization
import org.schabi.newpipe.util.external_communication.ShareUtils
@ -25,16 +26,15 @@ import org.schabi.newpipe.util.external_communication.ShareUtils
* Fragment containing the software licenses.
*/
class LicenseFragment : Fragment() {
private lateinit var softwareComponents: Array<SoftwareComponent>
private lateinit var softwareComponents: List<SoftwareComponent>
private var activeSoftwareComponent: SoftwareComponent? = null
private val compositeDisposable = CompositeDisposable()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
softwareComponents = arguments?.getParcelableArray(ARG_COMPONENTS) as Array<SoftwareComponent>
softwareComponents = arguments?.parcelableArrayList<SoftwareComponent>(ARG_COMPONENTS)!!
.sortedBy { it.name } // Sort components by name
activeSoftwareComponent = savedInstanceState?.getSerializable(SOFTWARE_COMPONENT_KEY) as? SoftwareComponent
// Sort components by name
softwareComponents.sortBy { it.name }
}
override fun onDestroy() {
@ -130,7 +130,8 @@ class LicenseFragment : Fragment() {
StandardLicenses.GPL3,
BuildConfig.VERSION_NAME
)
fun newInstance(softwareComponents: Array<SoftwareComponent>): LicenseFragment {
fun newInstance(softwareComponents: ArrayList<SoftwareComponent>): LicenseFragment {
val fragment = LicenseFragment()
fragment.arguments = bundleOf(ARG_COMPONENTS to softwareComponents)
return fragment

View File

@ -7,7 +7,7 @@ import java.time.Instant
import java.time.OffsetDateTime
import java.time.ZoneOffset
object Converters {
class Converters {
/**
* Convert a long value to a [OffsetDateTime].
*
@ -47,6 +47,6 @@ object Converters {
@TypeConverter
fun feedGroupIconOf(id: Int): FeedGroupIcon {
return FeedGroupIcon.values().first { it.id == id }
return FeedGroupIcon.entries.first { it.id == id }
}
}

View File

@ -93,18 +93,30 @@ abstract class FeedDAO {
uploadDateBefore: OffsetDateTime?
): Maybe<List<StreamWithState>>
/**
* Remove links to streams that are older than the given date
* **but keep at least one stream per uploader**.
*
* One stream per uploader is kept because it is needed as reference
* when fetching new streams to check if they are new or not.
* @param offsetDateTime the newest date to keep, older streams are removed
*/
@Query(
"""
DELETE FROM feed WHERE
feed.stream_id IN (
SELECT s.uid FROM streams s
INNER JOIN feed f
ON s.uid = f.stream_id
WHERE s.upload_date < :offsetDateTime
)
DELETE FROM feed
WHERE feed.stream_id IN (SELECT uid from (
SELECT s.uid,
(SELECT MAX(upload_date)
FROM streams s1
INNER JOIN feed f1
ON s1.uid = f1.stream_id
WHERE f1.subscription_id = f.subscription_id) max_upload_date
FROM streams s
INNER JOIN feed f
ON s.uid = f.stream_id
WHERE s.upload_date < :offsetDateTime
AND s.upload_date <> max_upload_date))
"""
)
abstract fun unlinkStreamsOlderThan(offsetDateTime: OffsetDateTime)

View File

@ -75,6 +75,7 @@ import org.schabi.newpipe.util.ThemeHelper;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
@ -272,8 +273,8 @@ public class DownloadDialog extends DialogFragment
if (!videoStreams.get(i).isVideoOnly()) {
continue;
}
final AudioStream audioStream = SecondaryStreamHelper
.getAudioStreamFor(audioStreams.getStreamsList(), videoStreams.get(i));
final AudioStream audioStream = SecondaryStreamHelper.getAudioStreamFor(
context, audioStreams.getStreamsList(), videoStreams.get(i));
if (audioStream != null) {
secondaryStreams.append(i, new SecondaryStreamHelper<>(audioStreams, audioStream));
@ -1081,7 +1082,7 @@ public class DownloadDialog extends DialogFragment
final char kind;
int threads = dialogBinding.threads.getProgress() + 1;
final String[] urls;
final MissionRecoveryInfo[] recoveryInfo;
final List<MissionRecoveryInfo> recoveryInfo;
String psName = null;
String[] psArgs = null;
long nearLength = 0;
@ -1146,9 +1147,7 @@ public class DownloadDialog extends DialogFragment
urls = new String[] {
selectedStream.getContent()
};
recoveryInfo = new MissionRecoveryInfo[] {
new MissionRecoveryInfo(selectedStream)
};
recoveryInfo = List.of(new MissionRecoveryInfo(selectedStream));
} else {
if (secondaryStream.getDeliveryMethod() != PROGRESSIVE_HTTP) {
throw new IllegalArgumentException("Unsupported stream delivery format"
@ -1158,12 +1157,14 @@ public class DownloadDialog extends DialogFragment
urls = new String[] {
selectedStream.getContent(), secondaryStream.getContent()
};
recoveryInfo = new MissionRecoveryInfo[] {new MissionRecoveryInfo(selectedStream),
new MissionRecoveryInfo(secondaryStream)};
recoveryInfo = List.of(
new MissionRecoveryInfo(selectedStream),
new MissionRecoveryInfo(secondaryStream)
);
}
DownloadManagerService.startMission(context, urls, storage, kind, threads,
currentInfo.getUrl(), psName, psArgs, nearLength, recoveryInfo);
currentInfo.getUrl(), psName, psArgs, nearLength, new ArrayList<>(recoveryInfo));
Toast.makeText(context, getString(R.string.download_has_started),
Toast.LENGTH_SHORT).show();

View File

@ -17,6 +17,7 @@ import androidx.annotation.Nullable;
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;
@ -105,7 +106,7 @@ public class ErrorActivity extends AppCompatActivity {
actionBar.setDisplayShowTitleEnabled(true);
}
errorInfo = intent.getParcelableExtra(ERROR_INFO);
errorInfo = IntentCompat.getParcelableExtra(intent, ERROR_INFO, ErrorInfo.class);
// important add guru meditation
addGuruMeditation();

View File

@ -19,6 +19,7 @@ public enum UserAction {
REQUESTED_PLAYLIST("requested playlist"),
REQUESTED_KIOSK("requested kiosk"),
REQUESTED_COMMENTS("requested comments"),
REQUESTED_COMMENT_REPLIES("requested comment replies"),
REQUESTED_FEED("requested feed"),
REQUESTED_BOOKMARK("bookmark"),
DELETE_FROM_HISTORY("delete from history"),

View File

@ -194,7 +194,6 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte
}
binding.pager.setAdapter(null);
binding.pager.setOffscreenPageLimit(tabsList.size());
binding.pager.setAdapter(pagerAdapter);
updateTabsIconAndDescription();

View File

@ -24,7 +24,6 @@ import android.content.pm.ActivityInfo;
import android.database.ContentObserver;
import android.graphics.Color;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
@ -75,6 +74,7 @@ import org.schabi.newpipe.error.UserAction;
import org.schabi.newpipe.extractor.Image;
import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.comments.CommentsInfoItem;
import org.schabi.newpipe.extractor.exceptions.ContentNotSupportedException;
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
import org.schabi.newpipe.extractor.stream.AudioStream;
@ -1013,6 +1013,20 @@ public final class VideoDetailFragment
updateTabLayoutVisibility();
}
public void scrollToComment(final CommentsInfoItem comment) {
final int commentsTabPos = pageAdapter.getItemPositionByTitle(COMMENTS_TAB_TAG);
final Fragment fragment = pageAdapter.getItem(commentsTabPos);
if (!(fragment instanceof CommentsFragment)) {
return;
}
// unexpand the app bar only if scrolling to the comment succeeded
if (((CommentsFragment) fragment).scrollToComment(comment)) {
binding.appBarLayout.setExpanded(false, false);
binding.viewPager.setCurrentItem(commentsTabPos, false);
}
}
/*//////////////////////////////////////////////////////////////////////////
// Play Utils
//////////////////////////////////////////////////////////////////////////*/
@ -1482,11 +1496,6 @@ public final class VideoDetailFragment
displayUploaderAsSubChannel(info);
}
final Drawable buddyDrawable =
AppCompatResources.getDrawable(activity, R.drawable.placeholder_person);
binding.detailSubChannelThumbnailView.setImageDrawable(buddyDrawable);
binding.detailUploaderThumbnailView.setImageDrawable(buddyDrawable);
if (info.getViewCount() >= 0) {
if (info.getStreamType().equals(StreamType.AUDIO_LIVE_STREAM)) {
binding.detailViewCountView.setText(Localization.listeningCount(activity,

View File

@ -231,6 +231,8 @@ public abstract class BaseListInfoFragment<I extends InfoItem, L extends ListInf
if (!result.getRelatedItems().isEmpty()) {
infoListAdapter.addInfoItemList(result.getRelatedItems());
showListFooter(hasMoreItems());
} else if (hasMoreItems()) {
loadMoreItems();
} else {
infoListAdapter.clearStreamItemList();
showEmptyState();

View File

@ -1,6 +1,7 @@
package org.schabi.newpipe.fragments.list.channel;
import android.os.Bundle;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
@ -14,7 +15,10 @@ import org.schabi.newpipe.error.UserAction;
import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.extractor.ListExtractor;
import org.schabi.newpipe.extractor.channel.tabs.ChannelTabInfo;
import org.schabi.newpipe.extractor.exceptions.ParsingException;
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler;
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandlerFactory;
import org.schabi.newpipe.extractor.linkhandler.ReadyChannelTabListLinkHandler;
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
import org.schabi.newpipe.fragments.list.BaseListInfoFragment;
import org.schabi.newpipe.fragments.list.playlist.PlaylistControlViewHolder;
@ -114,6 +118,30 @@ public class ChannelTabFragment extends BaseListInfoFragment<InfoItem, ChannelTa
public void handleResult(@NonNull final ChannelTabInfo result) {
super.handleResult(result);
// FIXME this is a really hacky workaround, to avoid storing useless data in the fragment
// state. The problem is, `ReadyChannelTabListLinkHandler` might contain raw JSON data that
// uses a lot of memory (e.g. ~800KB for YouTube). While 800KB doesn't seem much, if
// you combine just a couple of channel tab fragments you easily go over the 1MB
// save&restore transaction limit, and get `TransactionTooLargeException`s. A proper
// solution would require rethinking about `ReadyChannelTabListLinkHandler`s.
if (tabHandler instanceof ReadyChannelTabListLinkHandler) {
try {
// once `handleResult` is called, the parsed data was already saved to cache, so
// we can discard any raw data in ReadyChannelTabListLinkHandler and create a
// link handler with identical properties, but without any raw data
final ListLinkHandlerFactory channelTabLHFactory = result.getService()
.getChannelTabLHFactory();
if (channelTabLHFactory != null) {
// some services do not not have a ChannelTabLHFactory
tabHandler = channelTabLHFactory.fromQuery(tabHandler.getId(),
tabHandler.getContentFilters(), tabHandler.getSortFilter());
}
} catch (final ParsingException e) {
// silently ignore the error, as the app can continue to function normally
Log.w(TAG, "Could not recreate channel tab handler", e);
}
}
if (playlistControlBinding != null) {
// PlaylistControls should be visible only if there is some item in
// infoListAdapter other than header

View File

@ -0,0 +1,168 @@
package org.schabi.newpipe.fragments.list.comments;
import static org.schabi.newpipe.util.ServiceHelper.getServiceById;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.constraintlayout.widget.ConstraintLayout;
import androidx.core.text.HtmlCompat;
import org.schabi.newpipe.R;
import org.schabi.newpipe.databinding.CommentRepliesHeaderBinding;
import org.schabi.newpipe.error.UserAction;
import org.schabi.newpipe.extractor.ListExtractor;
import org.schabi.newpipe.extractor.comments.CommentsInfoItem;
import org.schabi.newpipe.fragments.list.BaseListInfoFragment;
import org.schabi.newpipe.info_list.ItemViewMode;
import org.schabi.newpipe.util.DeviceUtils;
import org.schabi.newpipe.util.ExtractorHelper;
import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.image.ImageStrategy;
import org.schabi.newpipe.util.image.PicassoHelper;
import org.schabi.newpipe.util.text.TextLinkifier;
import java.util.Queue;
import java.util.function.Supplier;
import io.reactivex.rxjava3.core.Single;
import io.reactivex.rxjava3.disposables.CompositeDisposable;
public final class CommentRepliesFragment
extends BaseListInfoFragment<CommentsInfoItem, CommentRepliesInfo> {
public static final String TAG = CommentRepliesFragment.class.getSimpleName();
private CommentsInfoItem commentsInfoItem; // the comment to show replies of
private final CompositeDisposable disposables = new CompositeDisposable();
/*//////////////////////////////////////////////////////////////////////////
// Constructors and lifecycle
//////////////////////////////////////////////////////////////////////////*/
// only called by the Android framework, after which readFrom is called and restores all data
public CommentRepliesFragment() {
super(UserAction.REQUESTED_COMMENT_REPLIES);
}
public CommentRepliesFragment(@NonNull final CommentsInfoItem commentsInfoItem) {
this();
this.commentsInfoItem = commentsInfoItem;
// setting "" as title since the title will be properly set right after
setInitialData(commentsInfoItem.getServiceId(), commentsInfoItem.getUrl(), "");
}
@Nullable
@Override
public View onCreateView(@NonNull final LayoutInflater inflater,
@Nullable final ViewGroup container,
@Nullable final Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment_comments, container, false);
}
@Override
public void onDestroyView() {
disposables.clear();
super.onDestroyView();
}
@Override
protected Supplier<View> getListHeaderSupplier() {
return () -> {
final CommentRepliesHeaderBinding binding = CommentRepliesHeaderBinding
.inflate(activity.getLayoutInflater(), itemsList, false);
final CommentsInfoItem item = commentsInfoItem;
// load the author avatar
PicassoHelper.loadAvatar(item.getUploaderAvatars()).into(binding.authorAvatar);
binding.authorAvatar.setVisibility(ImageStrategy.shouldLoadImages()
? View.VISIBLE : View.GONE);
// setup author name and comment date
binding.authorName.setText(item.getUploaderName());
binding.uploadDate.setText(Localization.relativeTimeOrTextual(
getContext(), item.getUploadDate(), item.getTextualUploadDate()));
binding.authorTouchArea.setOnClickListener(
v -> NavigationHelper.openCommentAuthorIfPresent(requireActivity(), item));
// setup like count, hearted and pinned
binding.thumbsUpCount.setText(
Localization.likeCount(requireContext(), item.getLikeCount()));
// for heartImage goneMarginEnd was used, but there is no way to tell ConstraintLayout
// not to use a different margin only when both the next two views are gone
((ConstraintLayout.LayoutParams) binding.thumbsUpCount.getLayoutParams())
.setMarginEnd(DeviceUtils.dpToPx(
(item.isHeartedByUploader() || item.isPinned() ? 8 : 16),
requireContext()));
binding.heartImage.setVisibility(item.isHeartedByUploader() ? View.VISIBLE : View.GONE);
binding.pinnedImage.setVisibility(item.isPinned() ? View.VISIBLE : View.GONE);
// setup comment content
TextLinkifier.fromDescription(binding.commentContent, item.getCommentText(),
HtmlCompat.FROM_HTML_MODE_LEGACY, getServiceById(item.getServiceId()),
item.getUrl(), disposables, null);
return binding.getRoot();
};
}
/*//////////////////////////////////////////////////////////////////////////
// State saving
//////////////////////////////////////////////////////////////////////////*/
@Override
public void writeTo(final Queue<Object> objectsToSave) {
super.writeTo(objectsToSave);
objectsToSave.add(commentsInfoItem);
}
@Override
public void readFrom(@NonNull final Queue<Object> savedObjects) throws Exception {
super.readFrom(savedObjects);
commentsInfoItem = (CommentsInfoItem) savedObjects.poll();
}
/*//////////////////////////////////////////////////////////////////////////
// Data loading
//////////////////////////////////////////////////////////////////////////*/
@Override
protected Single<CommentRepliesInfo> loadResult(final boolean forceLoad) {
return Single.fromCallable(() -> new CommentRepliesInfo(commentsInfoItem,
// the reply count string will be shown as the activity title
Localization.replyCount(requireContext(), commentsInfoItem.getReplyCount())));
}
@Override
protected Single<ListExtractor.InfoItemsPage<CommentsInfoItem>> loadMoreItemsLogic() {
// commentsInfoItem.getUrl() should contain the url of the original
// ListInfo<CommentsInfoItem>, which should be the stream url
return ExtractorHelper.getMoreCommentItems(
serviceId, commentsInfoItem.getUrl(), currentNextPage);
}
/*//////////////////////////////////////////////////////////////////////////
// Utils
//////////////////////////////////////////////////////////////////////////*/
@Override
protected ItemViewMode getItemViewMode() {
return ItemViewMode.LIST;
}
/**
* @return the comment to which the replies are shown
*/
public CommentsInfoItem getCommentsInfoItem() {
return commentsInfoItem;
}
}

View File

@ -0,0 +1,22 @@
package org.schabi.newpipe.fragments.list.comments;
import org.schabi.newpipe.extractor.ListInfo;
import org.schabi.newpipe.extractor.comments.CommentsInfoItem;
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler;
import java.util.Collections;
public final class CommentRepliesInfo extends ListInfo<CommentsInfoItem> {
/**
* This class is used to wrap the comment replies page into a ListInfo object.
*
* @param comment the comment from which to get replies
* @param name will be shown as the fragment title
*/
public CommentRepliesInfo(final CommentsInfoItem comment, final String name) {
super(comment.getServiceId(),
new ListLinkHandler("", "", "", Collections.emptyList(), null), name);
setNextPage(comment.getReplies());
setRelatedItems(Collections.emptyList()); // since it must be non-null
}
}

View File

@ -110,4 +110,14 @@ public class CommentsFragment extends BaseListInfoFragment<CommentsInfoItem, Com
protected ItemViewMode getItemViewMode() {
return ItemViewMode.LIST;
}
public boolean scrollToComment(final CommentsInfoItem comment) {
final int position = infoListAdapter.getItemsList().indexOf(comment);
if (position < 0) {
return false;
}
itemsList.scrollToPosition(position);
return true;
}
}

View File

@ -1,7 +1,9 @@
package org.schabi.newpipe.fragments.list.playlist;
import static org.schabi.newpipe.extractor.utils.Utils.isBlank;
import static org.schabi.newpipe.ktx.ViewUtils.animate;
import static org.schabi.newpipe.ktx.ViewUtils.animateHideRecyclerViewAllowingScrolling;
import static org.schabi.newpipe.util.ServiceHelper.getServiceById;
import android.content.Context;
import android.os.Bundle;
@ -37,6 +39,7 @@ import org.schabi.newpipe.extractor.ListExtractor;
import org.schabi.newpipe.extractor.ServiceList;
import org.schabi.newpipe.extractor.playlist.PlaylistInfo;
import org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper;
import org.schabi.newpipe.extractor.stream.Description;
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
import org.schabi.newpipe.fragments.list.BaseListInfoFragment;
import org.schabi.newpipe.info_list.dialog.InfoItemDialog;
@ -48,9 +51,10 @@ import org.schabi.newpipe.player.playqueue.PlaylistPlayQueue;
import org.schabi.newpipe.util.ExtractorHelper;
import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.image.PicassoHelper;
import org.schabi.newpipe.util.external_communication.ShareUtils;
import org.schabi.newpipe.util.PlayButtonHelper;
import org.schabi.newpipe.util.external_communication.ShareUtils;
import org.schabi.newpipe.util.image.PicassoHelper;
import org.schabi.newpipe.util.text.TextEllipsizer;
import java.util.ArrayList;
import java.util.List;
@ -321,6 +325,29 @@ public class PlaylistFragment extends BaseListInfoFragment<StreamInfoItem, Playl
headerBinding.playlistStreamCount.setText(Localization
.localizeStreamCount(getContext(), result.getStreamCount()));
final Description description = result.getDescription();
if (description != null && description != Description.EMPTY_DESCRIPTION
&& !isBlank(description.getContent())) {
final TextEllipsizer ellipsizer = new TextEllipsizer(
headerBinding.playlistDescription, 5, getServiceById(result.getServiceId()));
ellipsizer.setStateChangeListener(isEllipsized ->
headerBinding.playlistDescriptionReadMore.setText(
Boolean.TRUE.equals(isEllipsized) ? R.string.show_more : R.string.show_less
));
ellipsizer.setOnContentChanged(canBeEllipsized -> {
headerBinding.playlistDescriptionReadMore.setVisibility(
Boolean.TRUE.equals(canBeEllipsized) ? View.VISIBLE : View.GONE);
if (Boolean.TRUE.equals(canBeEllipsized)) {
ellipsizer.ellipsize();
}
});
ellipsizer.setContent(description);
headerBinding.playlistDescriptionReadMore.setOnClickListener(v -> ellipsizer.toggle());
} else {
headerBinding.playlistDescription.setVisibility(View.GONE);
headerBinding.playlistDescriptionReadMore.setVisibility(View.GONE);
}
if (!result.getErrors().isEmpty()) {
showSnackBarError(new ErrorInfo(result.getErrors(), UserAction.REQUESTED_PLAYLIST,
result.getUrl(), result));

View File

@ -21,18 +21,17 @@ import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.fragments.list.BaseListInfoFragment;
import org.schabi.newpipe.info_list.ItemViewMode;
import org.schabi.newpipe.ktx.ViewUtils;
import org.schabi.newpipe.util.RelatedItemInfo;
import java.io.Serializable;
import java.util.function.Supplier;
import io.reactivex.rxjava3.core.Single;
public class RelatedItemsFragment extends BaseListInfoFragment<InfoItem, RelatedItemInfo>
public class RelatedItemsFragment extends BaseListInfoFragment<InfoItem, RelatedItemsInfo>
implements SharedPreferences.OnSharedPreferenceChangeListener {
private static final String INFO_KEY = "related_info_key";
private RelatedItemInfo relatedItemInfo;
private RelatedItemsInfo relatedItemsInfo;
/*//////////////////////////////////////////////////////////////////////////
// Views
@ -69,7 +68,7 @@ public class RelatedItemsFragment extends BaseListInfoFragment<InfoItem, Related
@Override
protected Supplier<View> getListHeaderSupplier() {
if (relatedItemInfo == null || relatedItemInfo.getRelatedItems() == null) {
if (relatedItemsInfo == null || relatedItemsInfo.getRelatedItems() == null) {
return null;
}
@ -97,8 +96,8 @@ public class RelatedItemsFragment extends BaseListInfoFragment<InfoItem, Related
//////////////////////////////////////////////////////////////////////////*/
@Override
protected Single<RelatedItemInfo> loadResult(final boolean forceLoad) {
return Single.fromCallable(() -> relatedItemInfo);
protected Single<RelatedItemsInfo> loadResult(final boolean forceLoad) {
return Single.fromCallable(() -> relatedItemsInfo);
}
@Override
@ -110,7 +109,7 @@ public class RelatedItemsFragment extends BaseListInfoFragment<InfoItem, Related
}
@Override
public void handleResult(@NonNull final RelatedItemInfo result) {
public void handleResult(@NonNull final RelatedItemsInfo result) {
super.handleResult(result);
if (headerBinding != null) {
@ -137,23 +136,23 @@ public class RelatedItemsFragment extends BaseListInfoFragment<InfoItem, Related
private void setInitialData(final StreamInfo info) {
super.setInitialData(info.getServiceId(), info.getUrl(), info.getName());
if (this.relatedItemInfo == null) {
this.relatedItemInfo = RelatedItemInfo.getInfo(info);
if (this.relatedItemsInfo == null) {
this.relatedItemsInfo = new RelatedItemsInfo(info);
}
}
@Override
public void onSaveInstanceState(@NonNull final Bundle outState) {
super.onSaveInstanceState(outState);
outState.putSerializable(INFO_KEY, relatedItemInfo);
outState.putSerializable(INFO_KEY, relatedItemsInfo);
}
@Override
protected void onRestoreInstanceState(@NonNull final Bundle savedState) {
super.onRestoreInstanceState(savedState);
final Serializable serializable = savedState.getSerializable(INFO_KEY);
if (serializable instanceof RelatedItemInfo) {
this.relatedItemInfo = (RelatedItemInfo) serializable;
if (serializable instanceof RelatedItemsInfo) {
this.relatedItemsInfo = (RelatedItemsInfo) serializable;
}
}

View File

@ -0,0 +1,22 @@
package org.schabi.newpipe.fragments.list.videos;
import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.extractor.ListInfo;
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler;
import org.schabi.newpipe.extractor.stream.StreamInfo;
import java.util.ArrayList;
import java.util.Collections;
public final class RelatedItemsInfo extends ListInfo<InfoItem> {
/**
* This class is used to wrap the related items of a StreamInfo into a ListInfo object.
*
* @param info the stream info from which to get related items
*/
public RelatedItemsInfo(final StreamInfo info) {
super(info.getServiceId(), new ListLinkHandler(info.getOriginalUrl(), info.getUrl(),
info.getId(), Collections.emptyList(), null), info.getName());
setRelatedItems(new ArrayList<>(info.getRelatedItems()));
}
}

View File

@ -13,8 +13,7 @@ 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.CommentsInfoItemHolder;
import org.schabi.newpipe.info_list.holder.CommentsMiniInfoItemHolder;
import org.schabi.newpipe.info_list.holder.CommentInfoItemHolder;
import org.schabi.newpipe.info_list.holder.InfoItemHolder;
import org.schabi.newpipe.info_list.holder.PlaylistInfoItemHolder;
import org.schabi.newpipe.info_list.holder.PlaylistMiniInfoItemHolder;
@ -87,8 +86,7 @@ public class InfoItemBuilder {
return useMiniVariant ? new PlaylistMiniInfoItemHolder(this, parent)
: new PlaylistInfoItemHolder(this, parent);
case COMMENT:
return useMiniVariant ? new CommentsMiniInfoItemHolder(this, parent)
: new CommentsInfoItemHolder(this, parent);
return new CommentInfoItemHolder(this, parent);
default:
throw new RuntimeException("InfoType not expected = " + infoType.name());
}

View File

@ -21,8 +21,7 @@ import org.schabi.newpipe.info_list.holder.ChannelCardInfoItemHolder;
import org.schabi.newpipe.info_list.holder.ChannelGridInfoItemHolder;
import org.schabi.newpipe.info_list.holder.ChannelInfoItemHolder;
import org.schabi.newpipe.info_list.holder.ChannelMiniInfoItemHolder;
import org.schabi.newpipe.info_list.holder.CommentsInfoItemHolder;
import org.schabi.newpipe.info_list.holder.CommentsMiniInfoItemHolder;
import org.schabi.newpipe.info_list.holder.CommentInfoItemHolder;
import org.schabi.newpipe.info_list.holder.InfoItemHolder;
import org.schabi.newpipe.info_list.holder.PlaylistCardInfoItemHolder;
import org.schabi.newpipe.info_list.holder.PlaylistGridInfoItemHolder;
@ -79,8 +78,7 @@ public class InfoListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolde
private static final int PLAYLIST_HOLDER_TYPE = 0x301;
private static final int GRID_PLAYLIST_HOLDER_TYPE = 0x302;
private static final int CARD_PLAYLIST_HOLDER_TYPE = 0x303;
private static final int MINI_COMMENT_HOLDER_TYPE = 0x400;
private static final int COMMENT_HOLDER_TYPE = 0x401;
private static final int COMMENT_HOLDER_TYPE = 0x400;
private final LayoutInflater layoutInflater;
private final InfoItemBuilder infoItemBuilder;
@ -271,7 +269,7 @@ public class InfoListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolde
return PLAYLIST_HOLDER_TYPE;
}
case COMMENT:
return useMiniVariant ? MINI_COMMENT_HOLDER_TYPE : COMMENT_HOLDER_TYPE;
return COMMENT_HOLDER_TYPE;
default:
return -1;
}
@ -320,10 +318,8 @@ public class InfoListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolde
return new PlaylistGridInfoItemHolder(infoItemBuilder, parent);
case CARD_PLAYLIST_HOLDER_TYPE:
return new PlaylistCardInfoItemHolder(infoItemBuilder, parent);
case MINI_COMMENT_HOLDER_TYPE:
return new CommentsMiniInfoItemHolder(infoItemBuilder, parent);
case COMMENT_HOLDER_TYPE:
return new CommentsInfoItemHolder(infoItemBuilder, parent);
return new CommentInfoItemHolder(infoItemBuilder, parent);
default:
return new FallbackViewHolder(new View(parent.getContext()));
}

View File

@ -0,0 +1,188 @@
package org.schabi.newpipe.info_list.holder;
import static org.schabi.newpipe.util.ServiceHelper.getServiceById;
import android.text.method.LinkMovementMethod;
import android.text.style.URLSpan;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.RelativeLayout;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.fragment.app.FragmentActivity;
import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.extractor.comments.CommentsInfoItem;
import org.schabi.newpipe.info_list.InfoItemBuilder;
import org.schabi.newpipe.local.history.HistoryRecordManager;
import org.schabi.newpipe.util.DeviceUtils;
import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.external_communication.ShareUtils;
import org.schabi.newpipe.util.image.ImageStrategy;
import org.schabi.newpipe.util.image.PicassoHelper;
import org.schabi.newpipe.util.text.CommentTextOnTouchListener;
import org.schabi.newpipe.util.text.TextEllipsizer;
public class CommentInfoItemHolder extends InfoItemHolder {
private static final int COMMENT_DEFAULT_LINES = 2;
private final int commentHorizontalPadding;
private final int commentVerticalPadding;
private final RelativeLayout itemRoot;
private final ImageView itemThumbnailView;
private final TextView itemContentView;
private final ImageView itemThumbsUpView;
private final TextView itemLikesCountView;
private final TextView itemTitleView;
private final ImageView itemHeartView;
private final ImageView itemPinnedView;
private final Button repliesButton;
@NonNull
private final TextEllipsizer textEllipsizer;
public CommentInfoItemHolder(final InfoItemBuilder infoItemBuilder,
final ViewGroup parent) {
super(infoItemBuilder, R.layout.list_comment_item, parent);
itemRoot = itemView.findViewById(R.id.itemRoot);
itemThumbnailView = itemView.findViewById(R.id.itemThumbnailView);
itemContentView = itemView.findViewById(R.id.itemCommentContentView);
itemThumbsUpView = itemView.findViewById(R.id.detail_thumbs_up_img_view);
itemLikesCountView = itemView.findViewById(R.id.detail_thumbs_up_count_view);
itemTitleView = itemView.findViewById(R.id.itemTitleView);
itemHeartView = itemView.findViewById(R.id.detail_heart_image_view);
itemPinnedView = itemView.findViewById(R.id.detail_pinned_view);
repliesButton = itemView.findViewById(R.id.replies_button);
commentHorizontalPadding = (int) infoItemBuilder.getContext()
.getResources().getDimension(R.dimen.comments_horizontal_padding);
commentVerticalPadding = (int) infoItemBuilder.getContext()
.getResources().getDimension(R.dimen.comments_vertical_padding);
textEllipsizer = new TextEllipsizer(itemContentView, COMMENT_DEFAULT_LINES, null);
textEllipsizer.setStateChangeListener(isEllipsized -> {
if (Boolean.TRUE.equals(isEllipsized)) {
denyLinkFocus();
} else {
determineMovementMethod();
}
});
}
@Override
public void updateFromItem(final InfoItem infoItem,
final HistoryRecordManager historyRecordManager) {
if (!(infoItem instanceof CommentsInfoItem)) {
return;
}
final CommentsInfoItem item = (CommentsInfoItem) infoItem;
// load the author avatar
PicassoHelper.loadAvatar(item.getUploaderAvatars()).into(itemThumbnailView);
if (ImageStrategy.shouldLoadImages()) {
itemThumbnailView.setVisibility(View.VISIBLE);
itemRoot.setPadding(commentVerticalPadding, commentVerticalPadding,
commentVerticalPadding, commentVerticalPadding);
} else {
itemThumbnailView.setVisibility(View.GONE);
itemRoot.setPadding(commentHorizontalPadding, commentVerticalPadding,
commentHorizontalPadding, commentVerticalPadding);
}
itemThumbnailView.setOnClickListener(view -> openCommentAuthor(item));
// setup the top row, with pinned icon, author name and comment date
itemPinnedView.setVisibility(item.isPinned() ? View.VISIBLE : View.GONE);
itemTitleView.setText(Localization.concatenateStrings(item.getUploaderName(),
Localization.relativeTimeOrTextual(itemBuilder.getContext(), item.getUploadDate(),
item.getTextualUploadDate())));
// setup bottom row, with likes, heart and replies button
itemLikesCountView.setText(
Localization.likeCount(itemBuilder.getContext(), item.getLikeCount()));
itemHeartView.setVisibility(item.isHeartedByUploader() ? View.VISIBLE : View.GONE);
final boolean hasReplies = item.getReplies() != null;
repliesButton.setOnClickListener(hasReplies ? v -> openCommentReplies(item) : null);
repliesButton.setVisibility(hasReplies ? View.VISIBLE : View.GONE);
repliesButton.setText(hasReplies
? Localization.replyCount(itemBuilder.getContext(), item.getReplyCount()) : "");
((RelativeLayout.LayoutParams) itemThumbsUpView.getLayoutParams()).topMargin =
hasReplies ? 0 : DeviceUtils.dpToPx(6, itemBuilder.getContext());
// setup comment content and click listeners to expand/ellipsize it
textEllipsizer.setStreamingService(getServiceById(item.getServiceId()));
textEllipsizer.setStreamUrl(item.getUrl());
textEllipsizer.setContent(item.getCommentText());
textEllipsizer.ellipsize();
//noinspection ClickableViewAccessibility
itemContentView.setOnTouchListener(CommentTextOnTouchListener.INSTANCE);
itemView.setOnClickListener(view -> {
textEllipsizer.toggle();
if (itemBuilder.getOnCommentsSelectedListener() != null) {
itemBuilder.getOnCommentsSelectedListener().selected(item);
}
});
itemView.setOnLongClickListener(view -> {
if (DeviceUtils.isTv(itemBuilder.getContext())) {
openCommentAuthor(item);
} else {
final CharSequence text = itemContentView.getText();
if (text != null) {
ShareUtils.copyToClipboard(itemBuilder.getContext(), text.toString());
}
}
return true;
});
}
private void openCommentAuthor(@NonNull final CommentsInfoItem item) {
NavigationHelper.openCommentAuthorIfPresent((FragmentActivity) itemBuilder.getContext(),
item);
}
private void openCommentReplies(@NonNull final CommentsInfoItem item) {
NavigationHelper.openCommentRepliesFragment((FragmentActivity) itemBuilder.getContext(),
item);
}
private void allowLinkFocus() {
itemContentView.setMovementMethod(LinkMovementMethod.getInstance());
}
private void denyLinkFocus() {
itemContentView.setMovementMethod(null);
}
private boolean shouldFocusLinks() {
if (itemView.isInTouchMode()) {
return false;
}
final URLSpan[] urls = itemContentView.getUrls();
return urls != null && urls.length != 0;
}
private void determineMovementMethod() {
if (shouldFocusLinks()) {
allowLinkFocus();
} else {
denyLinkFocus();
}
}
}

View File

@ -1,63 +0,0 @@
package org.schabi.newpipe.info_list.holder;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.extractor.comments.CommentsInfoItem;
import org.schabi.newpipe.info_list.InfoItemBuilder;
import org.schabi.newpipe.local.history.HistoryRecordManager;
/*
* Created by Christian Schabesberger on 12.02.17.
*
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
* ChannelInfoItemHolder .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 CommentsInfoItemHolder extends CommentsMiniInfoItemHolder {
public final TextView itemTitleView;
private final ImageView itemHeartView;
private final ImageView itemPinnedView;
public CommentsInfoItemHolder(final InfoItemBuilder infoItemBuilder, final ViewGroup parent) {
super(infoItemBuilder, R.layout.list_comments_item, parent);
itemTitleView = itemView.findViewById(R.id.itemTitleView);
itemHeartView = itemView.findViewById(R.id.detail_heart_image_view);
itemPinnedView = itemView.findViewById(R.id.detail_pinned_view);
}
@Override
public void updateFromItem(final InfoItem infoItem,
final HistoryRecordManager historyRecordManager) {
super.updateFromItem(infoItem, historyRecordManager);
if (!(infoItem instanceof CommentsInfoItem)) {
return;
}
final CommentsInfoItem item = (CommentsInfoItem) infoItem;
itemTitleView.setText(item.getUploaderName());
itemHeartView.setVisibility(item.isHeartedByUploader() ? View.VISIBLE : View.GONE);
itemPinnedView.setVisibility(item.isPinned() ? View.VISIBLE : View.GONE);
}
}

View File

@ -1,280 +0,0 @@
package org.schabi.newpipe.info_list.holder;
import static android.text.TextUtils.isEmpty;
import android.graphics.Paint;
import android.text.Layout;
import android.text.method.LinkMovementMethod;
import android.text.style.URLSpan;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.RelativeLayout;
import android.widget.TextView;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.text.HtmlCompat;
import org.schabi.newpipe.R;
import org.schabi.newpipe.error.ErrorUtil;
import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.ServiceList;
import org.schabi.newpipe.extractor.StreamingService;
import org.schabi.newpipe.extractor.comments.CommentsInfoItem;
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
import org.schabi.newpipe.extractor.stream.Description;
import org.schabi.newpipe.info_list.InfoItemBuilder;
import org.schabi.newpipe.local.history.HistoryRecordManager;
import org.schabi.newpipe.util.DeviceUtils;
import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.image.ImageStrategy;
import org.schabi.newpipe.util.image.PicassoHelper;
import org.schabi.newpipe.util.external_communication.ShareUtils;
import org.schabi.newpipe.util.text.CommentTextOnTouchListener;
import org.schabi.newpipe.util.text.TextLinkifier;
import java.util.function.Consumer;
import io.reactivex.rxjava3.disposables.CompositeDisposable;
public class CommentsMiniInfoItemHolder extends InfoItemHolder {
private static final String TAG = "CommentsMiniIIHolder";
private static final String ELLIPSIS = "";
private static final int COMMENT_DEFAULT_LINES = 2;
private static final int COMMENT_EXPANDED_LINES = 1000;
private final int commentHorizontalPadding;
private final int commentVerticalPadding;
private final Paint paintAtContentSize;
private final float ellipsisWidthPx;
private final RelativeLayout itemRoot;
private final ImageView itemThumbnailView;
private final TextView itemContentView;
private final TextView itemLikesCountView;
private final TextView itemPublishedTime;
private final CompositeDisposable disposables = new CompositeDisposable();
@Nullable private Description commentText;
@Nullable private StreamingService streamService;
@Nullable private String streamUrl;
CommentsMiniInfoItemHolder(final InfoItemBuilder infoItemBuilder, final int layoutId,
final ViewGroup parent) {
super(infoItemBuilder, layoutId, parent);
itemRoot = itemView.findViewById(R.id.itemRoot);
itemThumbnailView = itemView.findViewById(R.id.itemThumbnailView);
itemLikesCountView = itemView.findViewById(R.id.detail_thumbs_up_count_view);
itemPublishedTime = itemView.findViewById(R.id.itemPublishedTime);
itemContentView = itemView.findViewById(R.id.itemCommentContentView);
commentHorizontalPadding = (int) infoItemBuilder.getContext()
.getResources().getDimension(R.dimen.comments_horizontal_padding);
commentVerticalPadding = (int) infoItemBuilder.getContext()
.getResources().getDimension(R.dimen.comments_vertical_padding);
paintAtContentSize = new Paint();
paintAtContentSize.setTextSize(itemContentView.getTextSize());
ellipsisWidthPx = paintAtContentSize.measureText(ELLIPSIS);
}
public CommentsMiniInfoItemHolder(final InfoItemBuilder infoItemBuilder,
final ViewGroup parent) {
this(infoItemBuilder, R.layout.list_comments_mini_item, parent);
}
@Override
public void updateFromItem(final InfoItem infoItem,
final HistoryRecordManager historyRecordManager) {
if (!(infoItem instanceof CommentsInfoItem)) {
return;
}
final CommentsInfoItem item = (CommentsInfoItem) infoItem;
PicassoHelper.loadAvatar(item.getUploaderAvatars()).into(itemThumbnailView);
if (ImageStrategy.shouldLoadImages()) {
itemThumbnailView.setVisibility(View.VISIBLE);
itemRoot.setPadding(commentVerticalPadding, commentVerticalPadding,
commentVerticalPadding, commentVerticalPadding);
} else {
itemThumbnailView.setVisibility(View.GONE);
itemRoot.setPadding(commentHorizontalPadding, commentVerticalPadding,
commentHorizontalPadding, commentVerticalPadding);
}
itemThumbnailView.setOnClickListener(view -> openCommentAuthor(item));
try {
streamService = NewPipe.getService(item.getServiceId());
} catch (final ExtractionException e) {
// should never happen
ErrorUtil.showUiErrorSnackbar(itemBuilder.getContext(), "Getting StreamingService", e);
Log.w(TAG, "Cannot obtain service from comment service id, defaulting to YouTube", e);
streamService = ServiceList.YouTube;
}
streamUrl = item.getUrl();
commentText = item.getCommentText();
ellipsize();
//noinspection ClickableViewAccessibility
itemContentView.setOnTouchListener(CommentTextOnTouchListener.INSTANCE);
if (item.getLikeCount() >= 0) {
itemLikesCountView.setText(
Localization.shortCount(
itemBuilder.getContext(),
item.getLikeCount()));
} else {
itemLikesCountView.setText("-");
}
if (item.getUploadDate() != null) {
itemPublishedTime.setText(Localization.relativeTime(item.getUploadDate()
.offsetDateTime()));
} else {
itemPublishedTime.setText(item.getTextualUploadDate());
}
itemView.setOnClickListener(view -> {
toggleEllipsize();
if (itemBuilder.getOnCommentsSelectedListener() != null) {
itemBuilder.getOnCommentsSelectedListener().selected(item);
}
});
itemView.setOnLongClickListener(view -> {
if (DeviceUtils.isTv(itemBuilder.getContext())) {
openCommentAuthor(item);
} else {
final CharSequence text = itemContentView.getText();
if (text != null) {
ShareUtils.copyToClipboard(itemBuilder.getContext(), text.toString());
}
}
return true;
});
}
private void openCommentAuthor(final CommentsInfoItem item) {
if (isEmpty(item.getUploaderUrl())) {
return;
}
final AppCompatActivity activity = (AppCompatActivity) itemBuilder.getContext();
try {
NavigationHelper.openChannelFragment(
activity.getSupportFragmentManager(),
item.getServiceId(),
item.getUploaderUrl(),
item.getUploaderName());
} catch (final Exception e) {
ErrorUtil.showUiErrorSnackbar(activity, "Opening channel fragment", e);
}
}
private void allowLinkFocus() {
itemContentView.setMovementMethod(LinkMovementMethod.getInstance());
}
private void denyLinkFocus() {
itemContentView.setMovementMethod(null);
}
private boolean shouldFocusLinks() {
if (itemView.isInTouchMode()) {
return false;
}
final URLSpan[] urls = itemContentView.getUrls();
return urls != null && urls.length != 0;
}
private void determineMovementMethod() {
if (shouldFocusLinks()) {
allowLinkFocus();
} else {
denyLinkFocus();
}
}
private void ellipsize() {
itemContentView.setMaxLines(COMMENT_EXPANDED_LINES);
linkifyCommentContentView(v -> {
boolean hasEllipsis = false;
final CharSequence charSeqText = itemContentView.getText();
if (charSeqText != null && itemContentView.getLineCount() > COMMENT_DEFAULT_LINES) {
// Note that converting to String removes spans (i.e. links), but that's something
// we actually want since when the text is ellipsized we want all clicks on the
// comment to expand the comment, not to open links.
final String text = charSeqText.toString();
final Layout layout = itemContentView.getLayout();
final float lineWidth = layout.getLineWidth(COMMENT_DEFAULT_LINES - 1);
final float layoutWidth = layout.getWidth();
final int lineStart = layout.getLineStart(COMMENT_DEFAULT_LINES - 1);
final int lineEnd = layout.getLineEnd(COMMENT_DEFAULT_LINES - 1);
// remove characters up until there is enough space for the ellipsis
// (also summing 2 more pixels, just to be sure to avoid float rounding errors)
int end = lineEnd;
float removedCharactersWidth = 0.0f;
while (lineWidth - removedCharactersWidth + ellipsisWidthPx + 2.0f > layoutWidth
&& end >= lineStart) {
end -= 1;
// recalculate each time to account for ligatures or other similar things
removedCharactersWidth = paintAtContentSize.measureText(
text.substring(end, lineEnd));
}
// remove trailing spaces and newlines
while (end > 0 && Character.isWhitespace(text.charAt(end - 1))) {
end -= 1;
}
final String newVal = text.substring(0, end) + ELLIPSIS;
itemContentView.setText(newVal);
hasEllipsis = true;
}
itemContentView.setMaxLines(COMMENT_DEFAULT_LINES);
if (hasEllipsis) {
denyLinkFocus();
} else {
determineMovementMethod();
}
});
}
private void toggleEllipsize() {
final CharSequence text = itemContentView.getText();
if (!isEmpty(text) && text.charAt(text.length() - 1) == ELLIPSIS.charAt(0)) {
expand();
} else if (itemContentView.getLineCount() > COMMENT_DEFAULT_LINES) {
ellipsize();
}
}
private void expand() {
itemContentView.setMaxLines(COMMENT_EXPANDED_LINES);
linkifyCommentContentView(v -> determineMovementMethod());
}
private void linkifyCommentContentView(@Nullable final Consumer<TextView> onCompletion) {
disposables.clear();
if (commentText != null) {
TextLinkifier.fromDescription(itemContentView, commentText,
HtmlCompat.FROM_HTML_MODE_LEGACY, streamService, streamUrl, disposables,
onCompletion);
}
}
}

View File

@ -12,10 +12,6 @@ import org.schabi.newpipe.info_list.InfoItemBuilder;
import org.schabi.newpipe.local.history.HistoryRecordManager;
import org.schabi.newpipe.util.Localization;
import androidx.preference.PreferenceManager;
import static org.schabi.newpipe.MainActivity.DEBUG;
/*
* Created by Christian Schabesberger on 01.08.16.
* <p>
@ -81,7 +77,9 @@ public class StreamInfoItemHolder extends StreamMiniInfoItemHolder {
}
}
final String uploadDate = getFormattedRelativeUploadDate(infoItem);
final String uploadDate = Localization.relativeTimeOrTextual(itemBuilder.getContext(),
infoItem.getUploadDate(),
infoItem.getTextualUploadDate());
if (!TextUtils.isEmpty(uploadDate)) {
if (viewsAndDate.isEmpty()) {
return uploadDate;
@ -92,20 +90,4 @@ public class StreamInfoItemHolder extends StreamMiniInfoItemHolder {
return viewsAndDate;
}
private String getFormattedRelativeUploadDate(final StreamInfoItem infoItem) {
if (infoItem.getUploadDate() != null) {
String formattedRelativeTime = Localization
.relativeTime(infoItem.getUploadDate().offsetDateTime());
if (DEBUG && PreferenceManager.getDefaultSharedPreferences(itemBuilder.getContext())
.getBoolean(itemBuilder.getContext()
.getString(R.string.show_original_time_ago_key), false)) {
formattedRelativeTime += " (" + infoItem.getTextualUploadDate() + ")";
}
return formattedRelativeTime;
} else {
return infoItem.getTextualUploadDate();
}
}
}

View File

@ -0,0 +1,9 @@
package org.schabi.newpipe.ktx
import android.os.Bundle
import android.os.Parcelable
import androidx.core.os.BundleCompat
inline fun <reified T : Parcelable> Bundle.parcelableArrayList(key: String?): ArrayList<T>? {
return BundleCompat.getParcelableArrayList(this, key, T::class.java)
}

View File

@ -607,9 +607,13 @@ class FeedFragment : BaseStateFragment<FeedState>() {
execOnEnd = {
// Disabled animations would result in immediately hiding the button
// after it showed up
if (DeviceUtils.hasAnimationsAnimatorDurationEnabled(context)) {
// Hide the new items-"popup" after 10s
hideNewItemsLoaded(true, 10000)
// Context can be null in some cases, so we have to make sure it is not null in
// order to avoid a NullPointerException
context?.let {
if (DeviceUtils.hasAnimationsAnimatorDurationEnabled(it)) {
// Hide the new items button after 10s
hideNewItemsLoaded(true, 10000)
}
}
}
)

View File

@ -58,7 +58,7 @@ class NotificationHelper(val context: Context) {
.setAutoCancel(true)
.setCategory(NotificationCompat.CATEGORY_SOCIAL)
.setGroupSummary(true)
.setGroup(data.originalInfo.url)
.setGroup(data.url)
.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_SUMMARY)
// Build a summary notification for Android versions < 7.0
@ -73,7 +73,7 @@ class NotificationHelper(val context: Context) {
context,
data.pseudoId,
NavigationHelper
.getChannelIntent(context, data.originalInfo.serviceId, data.originalInfo.url)
.getChannelIntent(context, data.serviceId, data.url)
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK),
0,
false
@ -88,7 +88,7 @@ class NotificationHelper(val context: Context) {
// Show individual stream notifications, set channel icon only if there is actually
// one
showStreamNotifications(newStreams, data.originalInfo.serviceId, bitmap)
showStreamNotifications(newStreams, data.serviceId, bitmap)
// Show summary notification
manager.notify(data.pseudoId, summaryBuilder.build())
@ -97,7 +97,7 @@ class NotificationHelper(val context: Context) {
override fun onBitmapFailed(e: Exception, errorDrawable: Drawable) {
// Show individual stream notifications
showStreamNotifications(newStreams, data.originalInfo.serviceId, null)
showStreamNotifications(newStreams, data.serviceId, null)
// Show summary notification
manager.notify(data.pseudoId, summaryBuilder.build())
iconLoadingTargets.remove(this) // allow it to be garbage-collected

View File

@ -137,7 +137,7 @@ class NotificationWorker(
.enqueueUniquePeriodicWork(
WORK_TAG,
if (force) {
ExistingPeriodicWorkPolicy.REPLACE
ExistingPeriodicWorkPolicy.CANCEL_AND_REENQUEUE
} else {
ExistingPeriodicWorkPolicy.KEEP
},

View File

@ -26,7 +26,7 @@ object FeedEventManager {
}
sealed class Event {
object IdleEvent : Event()
data object IdleEvent : Event()
data class ProgressEvent(val currentProgress: Int = -1, val maxProgress: Int = -1, @StringRes val progressMessage: Int = 0) : Event() {
constructor(@StringRes progressMessage: Int) : this(-1, -1, progressMessage)
}

View File

@ -277,14 +277,14 @@ class FeedLoadManager(private val context: Context) {
notification.value!!.newStreams = filterNewStreams(info.streams)
feedDatabaseManager.upsertAll(info.uid, info.streams)
subscriptionManager.updateFromInfo(info.uid, info.originalInfo)
subscriptionManager.updateFromInfo(info)
if (info.errors.isNotEmpty()) {
feedResultsHolder.addErrors(
info.errors.map {
FeedLoadService.RequestException(
info.uid,
"${info.originalInfo.serviceId}:${info.originalInfo.url}",
"${info.serviceId}:${info.url}",
it
)
}

View File

@ -3,29 +3,48 @@ package org.schabi.newpipe.local.feed.service
import org.schabi.newpipe.database.subscription.NotificationMode
import org.schabi.newpipe.database.subscription.SubscriptionEntity
import org.schabi.newpipe.extractor.Info
import org.schabi.newpipe.extractor.channel.ChannelInfo
import org.schabi.newpipe.extractor.stream.StreamInfoItem
import org.schabi.newpipe.util.image.ImageStrategy
/**
* Instances of this class might stay around in memory for some time while fetching the feed,
* because of [FeedLoadManager.BUFFER_COUNT_BEFORE_INSERT]. Therefore this class should contain
* as little data as possible to avoid out of memory errors. In particular, avoid storing whole
* [ChannelInfo] objects, as they might contain raw JSON info in ready channel tabs link handlers.
*/
data class FeedUpdateInfo(
val uid: Long,
@NotificationMode
val notificationMode: Int,
val name: String,
val avatarUrl: String,
val originalInfo: Info,
val url: String,
val serviceId: Int,
// description and subscriberCount are null if the constructor info is from the fast feed method
val description: String?,
val subscriberCount: Long?,
val streams: List<StreamInfoItem>,
val errors: List<Throwable>,
) {
constructor(
subscription: SubscriptionEntity,
originalInfo: Info,
info: Info,
streams: List<StreamInfoItem>,
errors: List<Throwable>,
) : this(
uid = subscription.uid,
notificationMode = subscription.notificationMode,
name = subscription.name,
avatarUrl = subscription.avatarUrl,
originalInfo = originalInfo,
name = info.name,
avatarUrl = (info as? ChannelInfo)?.avatars?.let {
// if the newly fetched info is not from fast feed, then it contains updated avatars
ImageStrategy.imageListToDbUrl(it)
} ?: subscription.avatarUrl,
url = info.url,
serviceId = info.serviceId,
// there is no description and subscriberCount in the fast feed
description = (info as? ChannelInfo)?.description,
subscriberCount = (info as? ChannelInfo)?.subscriberCount,
streams = streams,
errors = errors,
)
@ -34,7 +53,7 @@ data class FeedUpdateInfo(
* Integer id, can be used as notification id, etc.
*/
val pseudoId: Int
get() = originalInfo.url.hashCode()
get() = url.hashCode()
lateinit var newStreams: List<StreamInfoItem>
}

View File

@ -12,12 +12,11 @@ import org.schabi.newpipe.database.stream.model.StreamEntity
import org.schabi.newpipe.database.subscription.NotificationMode
import org.schabi.newpipe.database.subscription.SubscriptionDAO
import org.schabi.newpipe.database.subscription.SubscriptionEntity
import org.schabi.newpipe.extractor.Info
import org.schabi.newpipe.extractor.channel.ChannelInfo
import org.schabi.newpipe.extractor.channel.tabs.ChannelTabInfo
import org.schabi.newpipe.extractor.feed.FeedInfo
import org.schabi.newpipe.extractor.stream.StreamInfoItem
import org.schabi.newpipe.local.feed.FeedDatabaseManager
import org.schabi.newpipe.local.feed.service.FeedUpdateInfo
import org.schabi.newpipe.util.ExtractorHelper
import org.schabi.newpipe.util.image.ImageStrategy
@ -97,19 +96,15 @@ class SubscriptionManager(context: Context) {
}
}
fun updateFromInfo(subscriptionId: Long, info: Info) {
val subscriptionEntity = subscriptionTable.getSubscription(subscriptionId)
fun updateFromInfo(info: FeedUpdateInfo) {
val subscriptionEntity = subscriptionTable.getSubscription(info.uid)
if (info is FeedInfo) {
subscriptionEntity.name = info.name
} else if (info is ChannelInfo) {
subscriptionEntity.setData(
info.name,
ImageStrategy.imageListToDbUrl(info.avatars),
info.description,
info.subscriberCount
)
}
subscriptionEntity.name = info.name
subscriptionEntity.avatarUrl = info.avatarUrl
// these two fields are null if the feed info was fetched using the fast feed method
info.description?.let { subscriptionEntity.description = it }
info.subscriberCount?.let { subscriptionEntity.subscriberCount = it }
subscriptionTable.update(subscriptionEntity)
}

View File

@ -55,10 +55,10 @@ class FeedGroupDialog : DialogFragment(), BackPressable {
private var groupSortOrder: Long = -1
sealed class ScreenState : Serializable {
object InitialScreen : ScreenState()
object IconPickerScreen : ScreenState()
object SubscriptionsPickerScreen : ScreenState()
object DeleteScreen : ScreenState()
data object InitialScreen : ScreenState()
data object IconPickerScreen : ScreenState()
data object SubscriptionsPickerScreen : ScreenState()
data object DeleteScreen : ScreenState()
}
@State @JvmField var selectedIcon: FeedGroupIcon? = null
@ -370,7 +370,7 @@ class FeedGroupDialog : DialogFragment(), BackPressable {
private fun setupIconPicker() {
val groupAdapter = GroupieAdapter()
groupAdapter.addAll(FeedGroupIcon.values().map { PickerIconItem(it) })
groupAdapter.addAll(FeedGroupIcon.entries.map { PickerIconItem(it) })
feedGroupCreateBinding.iconSelector.apply {
layoutManager = GridLayoutManager(requireContext(), 7, RecyclerView.VERTICAL, false)

View File

@ -110,8 +110,8 @@ class FeedGroupDialogViewModel(
}
sealed class DialogEvent {
object ProcessingEvent : DialogEvent()
object SuccessEvent : DialogEvent()
data object ProcessingEvent : DialogEvent()
data object SuccessEvent : DialogEvent()
}
data class Filter(val query: String, val showOnlyUngrouped: Boolean)

View File

@ -25,6 +25,7 @@ import android.content.Intent;
import android.net.Uri;
import android.util.Log;
import androidx.core.content.IntentCompat;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import org.reactivestreams.Subscriber;
@ -65,7 +66,7 @@ public class SubscriptionsExportService extends BaseImportExportService {
return START_NOT_STICKY;
}
final Uri path = intent.getParcelableExtra(KEY_FILE_PATH);
final Uri path = IntentCompat.getParcelableExtra(intent, KEY_FILE_PATH, Uri.class);
if (path == null) {
stopAndReportError(new IllegalStateException(
"Exporting to a file, but the path is null"),

View File

@ -30,6 +30,7 @@ import android.util.Pair;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.content.IntentCompat;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import org.reactivestreams.Subscriber;
@ -108,7 +109,7 @@ public class SubscriptionsImportService extends BaseImportExportService {
if (currentMode == CHANNEL_URL_MODE) {
channelUrl = intent.getStringExtra(KEY_VALUE);
} else {
final Uri uri = intent.getParcelableExtra(KEY_VALUE);
final Uri uri = IntentCompat.getParcelableExtra(intent, KEY_VALUE, Uri.class);
if (uri == null) {
stopAndReportError(new IllegalStateException(
"Importing from input stream, but file path is null"),

View File

@ -619,11 +619,13 @@ public final class PlayQueueActivity extends AppCompatActivity
final MenuItem audioTrackSelector = menu.findItem(R.id.action_audio_track);
final List<AudioStream> availableStreams =
Optional.ofNullable(player.getCurrentMetadata())
Optional.ofNullable(player)
.map(Player::getCurrentMetadata)
.flatMap(MediaItemTag::getMaybeAudioTrack)
.map(MediaItemTag.AudioTrack::getAudioStreams)
.orElse(null);
final Optional<AudioStream> selectedAudioStream = player.getSelectedAudioStream();
final Optional<AudioStream> selectedAudioStream = Optional.ofNullable(player)
.flatMap(Player::getSelectedAudioStream);
if (availableStreams == null || availableStreams.size() < 2
|| selectedAudioStream.isEmpty()) {

View File

@ -29,6 +29,7 @@ import android.os.IBinder;
import android.util.Log;
import org.schabi.newpipe.player.mediasession.MediaSessionPlayerUi;
import org.schabi.newpipe.player.notification.NotificationPlayerUi;
import org.schabi.newpipe.util.ThemeHelper;
import java.lang.ref.WeakReference;
@ -59,6 +60,14 @@ public final class PlayerService extends Service {
ThemeHelper.setTheme(this);
player = new Player(this);
/*
Create the player notification and start immediately the service in foreground,
otherwise if nothing is played or initializing the player and its components (especially
loading stream metadata) takes a lot of time, the app would crash on Android 8+ as the
service would never be put in the foreground while we said to the system we would do so
*/
player.UIs().get(NotificationPlayerUi.class)
.ifPresent(NotificationPlayerUi::createNotificationAndStartForeground);
}
@Override
@ -68,16 +77,38 @@ public final class PlayerService extends Service {
+ "], flags = [" + flags + "], startId = [" + startId + "]");
}
/*
Be sure that the player notification is set and the service is started in foreground,
otherwise, the app may crash on Android 8+ as the service would never be put in the
foreground while we said to the system we would do so
The service is always requested to be started in foreground, so always creating a
notification if there is no one already and starting the service in foreground should
not create any issues
If the service is already started in foreground, requesting it to be started shouldn't
do anything
*/
if (player != null) {
player.UIs().get(NotificationPlayerUi.class)
.ifPresent(NotificationPlayerUi::createNotificationAndStartForeground);
}
if (Intent.ACTION_MEDIA_BUTTON.equals(intent.getAction())
&& player.getPlayQueue() == null) {
// No need to process media button's actions if the player is not working, otherwise the
// player service would strangely start with nothing to play
&& (player == null || player.getPlayQueue() == null)) {
/*
No need to process media button's actions if the player is not working, otherwise
the player service would strangely start with nothing to play
Stop the service in this case, which will be removed from the foreground and its
notification cancelled in its destruction
*/
stopSelf();
return START_NOT_STICKY;
}
player.handleIntent(intent);
player.UIs().get(MediaSessionPlayerUi.class)
.ifPresent(ui -> ui.handleMediaButtonIntent(intent));
if (player != null) {
player.handleIntent(intent);
player.UIs().get(MediaSessionPlayerUi.class)
.ifPresent(ui -> ui.handleMediaButtonIntent(intent));
}
return START_NOT_STICKY;
}
@ -87,7 +118,7 @@ public final class PlayerService extends Service {
Log.d(TAG, "stopForImmediateReusing() called");
}
if (!player.exoPlayerIsNull()) {
if (player != null && !player.exoPlayerIsNull()) {
// Releases wifi & cpu, disables keepScreenOn, etc.
// We can't just pause the player here because it will make transition
// from one stream to a new stream not smooth
@ -98,7 +129,7 @@ public final class PlayerService extends Service {
@Override
public void onTaskRemoved(final Intent rootIntent) {
super.onTaskRemoved(rootIntent);
if (!player.videoPlayerSelected()) {
if (player != null && !player.videoPlayerSelected()) {
return;
}
onDestroy();

View File

@ -160,13 +160,12 @@ class MainPlayerGestureListener(
}
override fun onScroll(
initialEvent: MotionEvent,
initialEvent: MotionEvent?,
movingEvent: MotionEvent,
distanceX: Float,
distanceY: Float
): Boolean {
if (!playerUi.isFullscreen) {
if (initialEvent == null || !playerUi.isFullscreen) {
return false
}

View File

@ -167,7 +167,7 @@ class PopupPlayerGestureListener(
}
override fun onFling(
e1: MotionEvent,
e1: MotionEvent?,
e2: MotionEvent,
velocityX: Float,
velocityY: Float
@ -218,11 +218,14 @@ class PopupPlayerGestureListener(
}
override fun onScroll(
initialEvent: MotionEvent,
initialEvent: MotionEvent?,
movingEvent: MotionEvent,
distanceX: Float,
distanceY: Float
): Boolean {
if (initialEvent == null) {
return false
}
if (isResizing) {
return super.onScroll(initialEvent, movingEvent, distanceX, distanceY)

View File

@ -81,8 +81,9 @@ public interface MediaItemTag {
@NonNull
default MediaItem asMediaItem() {
final String thumbnailUrl = getThumbnailUrl();
final MediaMetadata mediaMetadata = new MediaMetadata.Builder()
.setArtworkUri(Uri.parse(getThumbnailUrl()))
.setArtworkUri(thumbnailUrl == null ? null : Uri.parse(thumbnailUrl))
.setArtist(getUploaderName())
.setDescription(getTitle())
.setDisplayTitle(getTitle())

View File

@ -1,10 +1,12 @@
package org.schabi.newpipe.player.mediasession;
import static org.schabi.newpipe.MainActivity.DEBUG;
import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_RECREATE_NOTIFICATION;
import android.content.Intent;
import android.content.SharedPreferences;
import android.graphics.Bitmap;
import android.os.Build;
import android.support.v4.media.MediaMetadataCompat;
import android.support.v4.media.session.MediaSessionCompat;
import android.util.Log;
@ -14,15 +16,23 @@ import androidx.annotation.Nullable;
import androidx.media.session.MediaButtonReceiver;
import com.google.android.exoplayer2.ForwardingPlayer;
import com.google.android.exoplayer2.Player.RepeatMode;
import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector;
import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.player.Player;
import org.schabi.newpipe.player.notification.NotificationActionData;
import org.schabi.newpipe.player.notification.NotificationConstants;
import org.schabi.newpipe.player.ui.PlayerUi;
import org.schabi.newpipe.player.ui.VideoPlayerUi;
import org.schabi.newpipe.util.StreamTypeUtil;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
public class MediaSessionPlayerUi extends PlayerUi
implements SharedPreferences.OnSharedPreferenceChangeListener {
@ -34,6 +44,10 @@ public class MediaSessionPlayerUi extends PlayerUi
private final String ignoreHardwareMediaButtonsKey;
private boolean shouldIgnoreHardwareMediaButtons = false;
// used to check whether any notification action changed, before sending costly updates
private List<NotificationActionData> prevNotificationActions = List.of();
public MediaSessionPlayerUi(@NonNull final Player player) {
super(player);
ignoreHardwareMediaButtonsKey =
@ -63,6 +77,10 @@ public class MediaSessionPlayerUi extends PlayerUi
sessionConnector.setMetadataDeduplicationEnabled(true);
sessionConnector.setMediaMetadataProvider(exoPlayer -> buildMediaMetadata());
// force updating media session actions by resetting the previous ones
prevNotificationActions = List.of();
updateMediaSessionActions();
}
@Override
@ -80,6 +98,7 @@ public class MediaSessionPlayerUi extends PlayerUi
mediaSession.release();
mediaSession = null;
}
prevNotificationActions = List.of();
}
@Override
@ -163,4 +182,109 @@ public class MediaSessionPlayerUi extends PlayerUi
return builder.build();
}
private void updateMediaSessionActions() {
// On Android 13+ (or Android T or API 33+) the actions in the player notification can't be
// controlled directly anymore, but are instead derived from custom media session actions.
// However the system allows customizing only two of these actions, since the other three
// are fixed to play-pause-buffering, previous, next.
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
// Although setting media session actions on older android versions doesn't seem to
// cause any trouble, it also doesn't seem to do anything, so we don't do anything to
// save battery. Check out NotificationUtil.updateActions() to see what happens on
// older android versions.
return;
}
// only use the fourth and fifth actions (the settings page also shows only the last 2 on
// Android 13+)
final List<NotificationActionData> newNotificationActions = IntStream.of(3, 4)
.map(i -> player.getPrefs().getInt(
player.getContext().getString(NotificationConstants.SLOT_PREF_KEYS[i]),
NotificationConstants.SLOT_DEFAULTS[i]))
.mapToObj(action -> NotificationActionData
.fromNotificationActionEnum(player, action))
.filter(Objects::nonNull)
.collect(Collectors.toList());
// avoid costly notification actions update, if nothing changed from last time
if (!newNotificationActions.equals(prevNotificationActions)) {
prevNotificationActions = newNotificationActions;
sessionConnector.setCustomActionProviders(
newNotificationActions.stream()
.map(data -> new SessionConnectorActionProvider(data, context))
.toArray(SessionConnectorActionProvider[]::new));
}
}
@Override
public void onBlocked() {
super.onBlocked();
updateMediaSessionActions();
}
@Override
public void onPlaying() {
super.onPlaying();
updateMediaSessionActions();
}
@Override
public void onBuffering() {
super.onBuffering();
updateMediaSessionActions();
}
@Override
public void onPaused() {
super.onPaused();
updateMediaSessionActions();
}
@Override
public void onPausedSeek() {
super.onPausedSeek();
updateMediaSessionActions();
}
@Override
public void onCompleted() {
super.onCompleted();
updateMediaSessionActions();
}
@Override
public void onRepeatModeChanged(@RepeatMode final int repeatMode) {
super.onRepeatModeChanged(repeatMode);
updateMediaSessionActions();
}
@Override
public void onShuffleModeEnabledChanged(final boolean shuffleModeEnabled) {
super.onShuffleModeEnabledChanged(shuffleModeEnabled);
updateMediaSessionActions();
}
@Override
public void onBroadcastReceived(final Intent intent) {
super.onBroadcastReceived(intent);
if (ACTION_RECREATE_NOTIFICATION.equals(intent.getAction())) {
// the notification actions changed
updateMediaSessionActions();
}
}
@Override
public void onMetadataChanged(@NonNull final StreamInfo info) {
super.onMetadataChanged(info);
updateMediaSessionActions();
}
@Override
public void onPlayQueueEdited() {
super.onPlayQueueEdited();
updateMediaSessionActions();
}
}

View File

@ -0,0 +1,47 @@
package org.schabi.newpipe.player.mediasession;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.support.v4.media.session.PlaybackStateCompat;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector;
import org.schabi.newpipe.player.notification.NotificationActionData;
import java.lang.ref.WeakReference;
public class SessionConnectorActionProvider implements MediaSessionConnector.CustomActionProvider {
private final NotificationActionData data;
@NonNull
private final WeakReference<Context> context;
public SessionConnectorActionProvider(final NotificationActionData notificationActionData,
@NonNull final Context context) {
this.data = notificationActionData;
this.context = new WeakReference<>(context);
}
@Override
public void onCustomAction(@NonNull final Player player,
@NonNull final String action,
@Nullable final Bundle extras) {
final Context actualContext = context.get();
if (actualContext != null) {
actualContext.sendBroadcast(new Intent(action));
}
}
@Nullable
@Override
public PlaybackStateCompat.CustomAction getCustomAction(@NonNull final Player player) {
return new PlaybackStateCompat.CustomAction.Builder(
data.action(), data.name(), data.icon()
).build();
}
}

View File

@ -0,0 +1,187 @@
package org.schabi.newpipe.player.notification;
import static com.google.android.exoplayer2.Player.REPEAT_MODE_ALL;
import static com.google.android.exoplayer2.Player.REPEAT_MODE_ONE;
import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_CLOSE;
import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_FAST_FORWARD;
import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_FAST_REWIND;
import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_PLAY_NEXT;
import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_PLAY_PAUSE;
import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_PLAY_PREVIOUS;
import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_REPEAT;
import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_SHUFFLE;
import android.annotation.SuppressLint;
import android.content.Context;
import androidx.annotation.DrawableRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.schabi.newpipe.R;
import org.schabi.newpipe.player.Player;
import java.util.Objects;
public final class NotificationActionData {
@NonNull
private final String action;
@NonNull
private final String name;
@DrawableRes
private final int icon;
public NotificationActionData(@NonNull final String action, @NonNull final String name,
@DrawableRes final int icon) {
this.action = action;
this.name = name;
this.icon = icon;
}
@NonNull
public String action() {
return action;
}
@NonNull
public String name() {
return name;
}
@DrawableRes
public int icon() {
return icon;
}
@SuppressLint("PrivateResource") // we currently use Exoplayer's internal strings and icons
@Nullable
public static NotificationActionData fromNotificationActionEnum(
@NonNull final Player player,
@NotificationConstants.Action final int selectedAction
) {
final int baseActionIcon = NotificationConstants.ACTION_ICONS[selectedAction];
final Context ctx = player.getContext();
switch (selectedAction) {
case NotificationConstants.PREVIOUS:
return new NotificationActionData(ACTION_PLAY_PREVIOUS,
ctx.getString(R.string.exo_controls_previous_description), baseActionIcon);
case NotificationConstants.NEXT:
return new NotificationActionData(ACTION_PLAY_NEXT,
ctx.getString(R.string.exo_controls_next_description), baseActionIcon);
case NotificationConstants.REWIND:
return new NotificationActionData(ACTION_FAST_REWIND,
ctx.getString(R.string.exo_controls_rewind_description), baseActionIcon);
case NotificationConstants.FORWARD:
return new NotificationActionData(ACTION_FAST_FORWARD,
ctx.getString(R.string.exo_controls_fastforward_description),
baseActionIcon);
case NotificationConstants.SMART_REWIND_PREVIOUS:
if (player.getPlayQueue() != null && player.getPlayQueue().size() > 1) {
return new NotificationActionData(ACTION_PLAY_PREVIOUS,
ctx.getString(R.string.exo_controls_previous_description),
R.drawable.exo_notification_previous);
} else {
return new NotificationActionData(ACTION_FAST_REWIND,
ctx.getString(R.string.exo_controls_rewind_description),
R.drawable.exo_controls_rewind);
}
case NotificationConstants.SMART_FORWARD_NEXT:
if (player.getPlayQueue() != null && player.getPlayQueue().size() > 1) {
return new NotificationActionData(ACTION_PLAY_NEXT,
ctx.getString(R.string.exo_controls_next_description),
R.drawable.exo_notification_next);
} else {
return new NotificationActionData(ACTION_FAST_FORWARD,
ctx.getString(R.string.exo_controls_fastforward_description),
R.drawable.exo_controls_fastforward);
}
case NotificationConstants.PLAY_PAUSE_BUFFERING:
if (player.getCurrentState() == Player.STATE_PREFLIGHT
|| player.getCurrentState() == Player.STATE_BLOCKED
|| player.getCurrentState() == Player.STATE_BUFFERING) {
return new NotificationActionData(ACTION_PLAY_PAUSE,
ctx.getString(R.string.notification_action_buffering),
R.drawable.ic_hourglass_top);
}
// fallthrough
case NotificationConstants.PLAY_PAUSE:
if (player.getCurrentState() == Player.STATE_COMPLETED) {
return new NotificationActionData(ACTION_PLAY_PAUSE,
ctx.getString(R.string.exo_controls_pause_description),
R.drawable.ic_replay);
} else if (player.isPlaying()
|| player.getCurrentState() == Player.STATE_PREFLIGHT
|| player.getCurrentState() == Player.STATE_BLOCKED
|| player.getCurrentState() == Player.STATE_BUFFERING) {
return new NotificationActionData(ACTION_PLAY_PAUSE,
ctx.getString(R.string.exo_controls_pause_description),
R.drawable.exo_notification_pause);
} else {
return new NotificationActionData(ACTION_PLAY_PAUSE,
ctx.getString(R.string.exo_controls_play_description),
R.drawable.exo_notification_play);
}
case NotificationConstants.REPEAT:
if (player.getRepeatMode() == REPEAT_MODE_ALL) {
return new NotificationActionData(ACTION_REPEAT,
ctx.getString(R.string.exo_controls_repeat_all_description),
R.drawable.exo_media_action_repeat_all);
} else if (player.getRepeatMode() == REPEAT_MODE_ONE) {
return new NotificationActionData(ACTION_REPEAT,
ctx.getString(R.string.exo_controls_repeat_one_description),
R.drawable.exo_media_action_repeat_one);
} else /* player.getRepeatMode() == REPEAT_MODE_OFF */ {
return new NotificationActionData(ACTION_REPEAT,
ctx.getString(R.string.exo_controls_repeat_off_description),
R.drawable.exo_media_action_repeat_off);
}
case NotificationConstants.SHUFFLE:
if (player.getPlayQueue() != null && player.getPlayQueue().isShuffled()) {
return new NotificationActionData(ACTION_SHUFFLE,
ctx.getString(R.string.exo_controls_shuffle_on_description),
R.drawable.exo_controls_shuffle_on);
} else {
return new NotificationActionData(ACTION_SHUFFLE,
ctx.getString(R.string.exo_controls_shuffle_off_description),
R.drawable.exo_controls_shuffle_off);
}
case NotificationConstants.CLOSE:
return new NotificationActionData(ACTION_CLOSE, ctx.getString(R.string.close),
R.drawable.ic_close);
case NotificationConstants.NOTHING:
default:
// do nothing
return null;
}
}
@Override
public boolean equals(@Nullable final Object obj) {
return (obj instanceof NotificationActionData other)
&& this.action.equals(other.action)
&& this.name.equals(other.name)
&& this.icon == other.icon;
}
@Override
public int hashCode() {
return Objects.hash(action, name, icon);
}
}

View File

@ -13,7 +13,7 @@ import org.schabi.newpipe.util.Localization;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.SortedSet;
import java.util.TreeSet;
@ -65,10 +65,16 @@ public final class NotificationConstants {
public static final int CLOSE = 11;
@Retention(RetentionPolicy.SOURCE)
@IntDef({NOTHING, PREVIOUS, NEXT, REWIND, FORWARD, SMART_REWIND_PREVIOUS, SMART_FORWARD_NEXT,
PLAY_PAUSE, PLAY_PAUSE_BUFFERING, REPEAT, SHUFFLE, CLOSE})
@IntDef({NOTHING, PREVIOUS, NEXT, REWIND, FORWARD,
SMART_REWIND_PREVIOUS, SMART_FORWARD_NEXT, PLAY_PAUSE, PLAY_PAUSE_BUFFERING, REPEAT,
SHUFFLE, CLOSE})
public @interface Action { }
@Action
public static final int[] ALL_ACTIONS = {NOTHING, PREVIOUS, NEXT, REWIND, FORWARD,
SMART_REWIND_PREVIOUS, SMART_FORWARD_NEXT, PLAY_PAUSE, PLAY_PAUSE_BUFFERING, REPEAT,
SHUFFLE, CLOSE};
@DrawableRes
public static final int[] ACTION_ICONS = {
0,
@ -95,16 +101,6 @@ public final class NotificationConstants {
CLOSE,
};
@Action
public static final int[][] SLOT_ALLOWED_ACTIONS = {
new int[] {PREVIOUS, REWIND, SMART_REWIND_PREVIOUS},
new int[] {REWIND, PLAY_PAUSE, PLAY_PAUSE_BUFFERING},
new int[] {NEXT, FORWARD, SMART_FORWARD_NEXT, PLAY_PAUSE, PLAY_PAUSE_BUFFERING},
new int[] {NOTHING, PREVIOUS, NEXT, REWIND, FORWARD, SMART_REWIND_PREVIOUS,
SMART_FORWARD_NEXT, REPEAT, SHUFFLE, CLOSE},
new int[] {NOTHING, NEXT, FORWARD, SMART_FORWARD_NEXT, REPEAT, SHUFFLE, CLOSE},
};
public static final int[] SLOT_PREF_KEYS = {
R.string.notification_slot_0_key,
R.string.notification_slot_1_key,
@ -165,14 +161,11 @@ public final class NotificationConstants {
/**
* @param context the context to use
* @param sharedPreferences the shared preferences to query values from
* @param slotCount remove indices >= than this value (set to {@code 5} to do nothing, or make
* it lower if there are slots with empty actions)
* @return a sorted list of the indices of the slots to use as compact slots
*/
public static List<Integer> getCompactSlotsFromPreferences(
public static Collection<Integer> getCompactSlotsFromPreferences(
@NonNull final Context context,
final SharedPreferences sharedPreferences,
final int slotCount) {
final SharedPreferences sharedPreferences) {
final SortedSet<Integer> compactSlots = new TreeSet<>();
for (int i = 0; i < 3; i++) {
final int compactSlot = sharedPreferences.getInt(
@ -180,14 +173,14 @@ public final class NotificationConstants {
if (compactSlot == Integer.MAX_VALUE) {
// settings not yet populated, return default values
return new ArrayList<>(SLOT_COMPACT_DEFAULTS);
return SLOT_COMPACT_DEFAULTS;
}
// a negative value (-1) is set when the user does not want a particular compact slot
if (compactSlot >= 0 && compactSlot < slotCount) {
if (compactSlot >= 0) {
// compact slot is < 0 if there are less than 3 checked checkboxes
compactSlots.add(compactSlot);
}
}
return new ArrayList<>(compactSlots);
return compactSlots;
}
}

View File

@ -17,7 +17,6 @@ import org.schabi.newpipe.player.helper.PlayerHelper;
import org.schabi.newpipe.player.ui.PlayerUi;
public final class NotificationPlayerUi extends PlayerUi {
private boolean foregroundNotificationAlreadyCreated = false;
private final NotificationUtil notificationUtil;
public NotificationPlayerUi(@NonNull final Player player) {
@ -25,15 +24,6 @@ public final class NotificationPlayerUi extends PlayerUi {
notificationUtil = new NotificationUtil(player);
}
@Override
public void initPlayer() {
super.initPlayer();
if (!foregroundNotificationAlreadyCreated) {
notificationUtil.createNotificationAndStartForeground();
foregroundNotificationAlreadyCreated = true;
}
}
@Override
public void destroy() {
super.destroy();
@ -122,4 +112,8 @@ public final class NotificationPlayerUi extends PlayerUi {
super.onPlayQueueEdited();
notificationUtil.createNotificationIfNeededAndUpdate(false);
}
public void createNotificationAndStartForeground() {
notificationUtil.createNotificationAndStartForeground();
}
}

View File

@ -1,16 +1,19 @@
package org.schabi.newpipe.player.notification;
import static android.app.PendingIntent.FLAG_UPDATE_CURRENT;
import static androidx.media.app.NotificationCompat.MediaStyle;
import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_CLOSE;
import android.annotation.SuppressLint;
import android.app.PendingIntent;
import android.content.Intent;
import android.content.pm.ServiceInfo;
import android.graphics.Bitmap;
import android.os.Build;
import android.util.Log;
import androidx.annotation.DrawableRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import androidx.core.app.NotificationCompat;
import androidx.core.app.NotificationManagerCompat;
import androidx.core.app.PendingIntentCompat;
@ -23,23 +26,12 @@ import org.schabi.newpipe.player.Player;
import org.schabi.newpipe.player.mediasession.MediaSessionPlayerUi;
import org.schabi.newpipe.util.NavigationHelper;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import static android.app.PendingIntent.FLAG_UPDATE_CURRENT;
import static androidx.media.app.NotificationCompat.MediaStyle;
import static com.google.android.exoplayer2.Player.REPEAT_MODE_ALL;
import static com.google.android.exoplayer2.Player.REPEAT_MODE_ONE;
import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_CLOSE;
import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_FAST_FORWARD;
import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_FAST_REWIND;
import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_PLAY_NEXT;
import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_PLAY_PAUSE;
import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_PLAY_PREVIOUS;
import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_REPEAT;
import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_SHUFFLE;
/**
* This is a utility class for player notifications.
*/
@ -100,29 +92,21 @@ public final class NotificationUtil {
final NotificationCompat.Builder builder =
new NotificationCompat.Builder(player.getContext(),
player.getContext().getString(R.string.notification_channel_id));
final MediaStyle mediaStyle = new MediaStyle();
initializeNotificationSlots();
// count the number of real slots, to make sure compact slots indices are not out of bound
int nonNothingSlotCount = 5;
if (notificationSlots[3] == NotificationConstants.NOTHING) {
--nonNothingSlotCount;
// setup media style (compact notification slots and media session)
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
// notification actions are ignored on Android 13+, and are replaced by code in
// MediaSessionPlayerUi
final int[] compactSlots = initializeNotificationSlots();
mediaStyle.setShowActionsInCompactView(compactSlots);
}
if (notificationSlots[4] == NotificationConstants.NOTHING) {
--nonNothingSlotCount;
}
// build the compact slot indices array (need code to convert from Integer... because Java)
final List<Integer> compactSlotList = NotificationConstants.getCompactSlotsFromPreferences(
player.getContext(), player.getPrefs(), nonNothingSlotCount);
final int[] compactSlots = compactSlotList.stream().mapToInt(Integer::intValue).toArray();
final MediaStyle mediaStyle = new MediaStyle().setShowActionsInCompactView(compactSlots);
player.UIs()
.get(MediaSessionPlayerUi.class)
.flatMap(MediaSessionPlayerUi::getSessionToken)
.ifPresent(mediaStyle::setMediaSession);
// setup notification builder
builder.setStyle(mediaStyle)
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
@ -157,7 +141,11 @@ public final class NotificationUtil {
notificationBuilder.setContentText(player.getUploaderName());
notificationBuilder.setTicker(player.getVideoTitle());
updateActions(notificationBuilder);
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
// notification actions are ignored on Android 13+, and are replaced by code in
// MediaSessionPlayerUi
updateActions(notificationBuilder);
}
}
@ -209,12 +197,35 @@ public final class NotificationUtil {
// ACTIONS
/////////////////////////////////////////////////////
private void initializeNotificationSlots() {
/**
* The compact slots array from settings contains indices from 0 to 4, each referring to one of
* the five actions configurable by the user. However, if the user sets an action to "Nothing",
* then all of the actions coming after will have a "settings index" different than the index
* of the corresponding action when sent to the system.
*
* @return the indices of compact slots referred to the list of non-nothing actions that will be
* sent to the system
*/
private int[] initializeNotificationSlots() {
final Collection<Integer> settingsCompactSlots = NotificationConstants
.getCompactSlotsFromPreferences(player.getContext(), player.getPrefs());
final List<Integer> adjustedCompactSlots = new ArrayList<>();
int nonNothingIndex = 0;
for (int i = 0; i < 5; ++i) {
notificationSlots[i] = player.getPrefs().getInt(
player.getContext().getString(NotificationConstants.SLOT_PREF_KEYS[i]),
NotificationConstants.SLOT_DEFAULTS[i]);
if (notificationSlots[i] != NotificationConstants.NOTHING) {
if (settingsCompactSlots.contains(i)) {
adjustedCompactSlots.add(nonNothingIndex);
}
nonNothingIndex += 1;
}
}
return adjustedCompactSlots.stream().mapToInt(Integer::intValue).toArray();
}
@SuppressLint("RestrictedApi")
@ -227,115 +238,15 @@ public final class NotificationUtil {
private void addAction(final NotificationCompat.Builder builder,
@NotificationConstants.Action final int slot) {
final NotificationCompat.Action action = getAction(slot);
if (action != null) {
builder.addAction(action);
@Nullable final NotificationActionData data =
NotificationActionData.fromNotificationActionEnum(player, slot);
if (data == null) {
return;
}
}
@Nullable
private NotificationCompat.Action getAction(
@NotificationConstants.Action final int selectedAction) {
final int baseActionIcon = NotificationConstants.ACTION_ICONS[selectedAction];
switch (selectedAction) {
case NotificationConstants.PREVIOUS:
return getAction(baseActionIcon,
R.string.exo_controls_previous_description, ACTION_PLAY_PREVIOUS);
case NotificationConstants.NEXT:
return getAction(baseActionIcon,
R.string.exo_controls_next_description, ACTION_PLAY_NEXT);
case NotificationConstants.REWIND:
return getAction(baseActionIcon,
R.string.exo_controls_rewind_description, ACTION_FAST_REWIND);
case NotificationConstants.FORWARD:
return getAction(baseActionIcon,
R.string.exo_controls_fastforward_description, ACTION_FAST_FORWARD);
case NotificationConstants.SMART_REWIND_PREVIOUS:
if (player.getPlayQueue() != null && player.getPlayQueue().size() > 1) {
return getAction(R.drawable.exo_notification_previous,
R.string.exo_controls_previous_description, ACTION_PLAY_PREVIOUS);
} else {
return getAction(R.drawable.exo_controls_rewind,
R.string.exo_controls_rewind_description, ACTION_FAST_REWIND);
}
case NotificationConstants.SMART_FORWARD_NEXT:
if (player.getPlayQueue() != null && player.getPlayQueue().size() > 1) {
return getAction(R.drawable.exo_notification_next,
R.string.exo_controls_next_description, ACTION_PLAY_NEXT);
} else {
return getAction(R.drawable.exo_controls_fastforward,
R.string.exo_controls_fastforward_description, ACTION_FAST_FORWARD);
}
case NotificationConstants.PLAY_PAUSE_BUFFERING:
if (player.getCurrentState() == Player.STATE_PREFLIGHT
|| player.getCurrentState() == Player.STATE_BLOCKED
|| player.getCurrentState() == Player.STATE_BUFFERING) {
// null intent -> show hourglass icon that does nothing when clicked
return new NotificationCompat.Action(R.drawable.ic_hourglass_top,
player.getContext().getString(R.string.notification_action_buffering),
null);
}
// fallthrough
case NotificationConstants.PLAY_PAUSE:
if (player.getCurrentState() == Player.STATE_COMPLETED) {
return getAction(R.drawable.ic_replay,
R.string.exo_controls_pause_description, ACTION_PLAY_PAUSE);
} else if (player.isPlaying()
|| player.getCurrentState() == Player.STATE_PREFLIGHT
|| player.getCurrentState() == Player.STATE_BLOCKED
|| player.getCurrentState() == Player.STATE_BUFFERING) {
return getAction(R.drawable.exo_notification_pause,
R.string.exo_controls_pause_description, ACTION_PLAY_PAUSE);
} else {
return getAction(R.drawable.exo_notification_play,
R.string.exo_controls_play_description, ACTION_PLAY_PAUSE);
}
case NotificationConstants.REPEAT:
if (player.getRepeatMode() == REPEAT_MODE_ALL) {
return getAction(R.drawable.exo_media_action_repeat_all,
R.string.exo_controls_repeat_all_description, ACTION_REPEAT);
} else if (player.getRepeatMode() == REPEAT_MODE_ONE) {
return getAction(R.drawable.exo_media_action_repeat_one,
R.string.exo_controls_repeat_one_description, ACTION_REPEAT);
} else /* player.getRepeatMode() == REPEAT_MODE_OFF */ {
return getAction(R.drawable.exo_media_action_repeat_off,
R.string.exo_controls_repeat_off_description, ACTION_REPEAT);
}
case NotificationConstants.SHUFFLE:
if (player.getPlayQueue() != null && player.getPlayQueue().isShuffled()) {
return getAction(R.drawable.exo_controls_shuffle_on,
R.string.exo_controls_shuffle_on_description, ACTION_SHUFFLE);
} else {
return getAction(R.drawable.exo_controls_shuffle_off,
R.string.exo_controls_shuffle_off_description, ACTION_SHUFFLE);
}
case NotificationConstants.CLOSE:
return getAction(R.drawable.ic_close,
R.string.close, ACTION_CLOSE);
case NotificationConstants.NOTHING:
default:
// do nothing
return null;
}
}
private NotificationCompat.Action getAction(@DrawableRes final int drawable,
@StringRes final int title,
final String intentAction) {
return new NotificationCompat.Action(drawable, player.getContext().getString(title),
PendingIntentCompat.getBroadcast(player.getContext(), NOTIFICATION_ID,
new Intent(intentAction), FLAG_UPDATE_CURRENT, false));
final PendingIntent intent = PendingIntentCompat.getBroadcast(player.getContext(),
NOTIFICATION_ID, new Intent(data.action()), FLAG_UPDATE_CURRENT, false);
builder.addAction(new NotificationCompat.Action(data.icon(), data.name(), intent));
}
private Intent getIntentForNotification() {
@ -364,7 +275,7 @@ public final class NotificationUtil {
final Bitmap thumbnail = player.getThumbnail();
if (thumbnail == null || !showThumbnail) {
// since the builder is reused, make sure the thumbnail is unset if there is not one
builder.setLargeIcon(null);
builder.setLargeIcon((Bitmap) null);
return;
}

View File

@ -23,7 +23,7 @@ public class MainSettingsFragment extends BasePreferenceFragment {
setHasOptionsMenu(true); // Otherwise onCreateOptionsMenu is not called
// Check if the app is updatable
if (!ReleaseVersionUtil.isReleaseApk()) {
if (!ReleaseVersionUtil.INSTANCE.isReleaseApk()) {
getPreferenceScreen().removePreference(
findPreference(getString(R.string.update_pref_screen_key)));

View File

@ -266,7 +266,7 @@ public class SettingsActivity extends AppCompatActivity implements
*/
private void ensureSearchRepresentsApplicationState() {
// Check if the update settings are available
if (!ReleaseVersionUtil.isReleaseApk()) {
if (!ReleaseVersionUtil.INSTANCE.isReleaseApk()) {
SettingsResourceRegistry.getInstance()
.getEntryByPreferencesResId(R.xml.update_settings)
.setSearchable(false);

View File

@ -5,35 +5,22 @@ import static org.schabi.newpipe.player.notification.NotificationConstants.ACTIO
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.res.ColorStateList;
import android.os.Build;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.CheckBox;
import android.widget.ImageView;
import android.widget.RadioButton;
import android.widget.RadioGroup;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.content.res.AppCompatResources;
import androidx.core.widget.TextViewCompat;
import androidx.preference.Preference;
import androidx.preference.PreferenceViewHolder;
import org.schabi.newpipe.App;
import org.schabi.newpipe.R;
import org.schabi.newpipe.databinding.ListRadioIconItemBinding;
import org.schabi.newpipe.databinding.SingleChoiceDialogViewBinding;
import org.schabi.newpipe.player.notification.NotificationConstants;
import org.schabi.newpipe.util.DeviceUtils;
import org.schabi.newpipe.util.ThemeHelper;
import org.schabi.newpipe.views.FocusOverlayView;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.IntStream;
@ -45,8 +32,9 @@ public class NotificationActionsPreference extends Preference {
}
@Nullable private NotificationSlot[] notificationSlots = null;
@Nullable private List<Integer> compactSlots = null;
private NotificationSlot[] notificationSlots;
private List<Integer> compactSlots;
////////////////////////////////////////////////////////////////////////////
// Lifecycle
@ -56,6 +44,11 @@ public class NotificationActionsPreference extends Preference {
public void onBindViewHolder(@NonNull final PreferenceViewHolder holder) {
super.onBindViewHolder(holder);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
((TextView) holder.itemView.findViewById(R.id.summary))
.setText(R.string.notification_actions_summary_android13);
}
holder.itemView.setClickable(false);
setupActions(holder.itemView);
}
@ -75,13 +68,29 @@ public class NotificationActionsPreference extends Preference {
////////////////////////////////////////////////////////////////////////////
private void setupActions(@NonNull final View view) {
compactSlots = NotificationConstants.getCompactSlotsFromPreferences(getContext(),
getSharedPreferences(), 5);
compactSlots = new ArrayList<>(NotificationConstants.getCompactSlotsFromPreferences(
getContext(), getSharedPreferences()));
notificationSlots = IntStream.range(0, 5)
.mapToObj(i -> new NotificationSlot(i, view))
.mapToObj(i -> new NotificationSlot(getContext(), getSharedPreferences(), i, view,
compactSlots.contains(i), this::onToggleCompactSlot))
.toArray(NotificationSlot[]::new);
}
private void onToggleCompactSlot(final int i, final CheckBox checkBox) {
if (checkBox.isChecked()) {
compactSlots.remove((Integer) i);
} else if (compactSlots.size() < 3) {
compactSlots.add(i);
} else {
Toast.makeText(getContext(),
R.string.notification_actions_at_most_three,
Toast.LENGTH_SHORT).show();
return;
}
checkBox.toggle();
}
////////////////////////////////////////////////////////////////////////////
// Saving
@ -99,143 +108,10 @@ public class NotificationActionsPreference extends Preference {
for (int i = 0; i < 5; i++) {
editor.putInt(getContext().getString(NotificationConstants.SLOT_PREF_KEYS[i]),
notificationSlots[i].selectedAction);
notificationSlots[i].getSelectedAction());
}
editor.apply();
}
}
////////////////////////////////////////////////////////////////////////////
// Notification action
////////////////////////////////////////////////////////////////////////////
private static final int[] SLOT_ITEMS = {
R.id.notificationAction0,
R.id.notificationAction1,
R.id.notificationAction2,
R.id.notificationAction3,
R.id.notificationAction4,
};
private static final int[] SLOT_TITLES = {
R.string.notification_action_0_title,
R.string.notification_action_1_title,
R.string.notification_action_2_title,
R.string.notification_action_3_title,
R.string.notification_action_4_title,
};
private class NotificationSlot {
final int i;
@NotificationConstants.Action int selectedAction;
ImageView icon;
TextView summary;
NotificationSlot(final int actionIndex, final View parentView) {
this.i = actionIndex;
final View view = parentView.findViewById(SLOT_ITEMS[i]);
setupSelectedAction(view);
setupTitle(view);
setupCheckbox(view);
}
void setupTitle(final View view) {
((TextView) view.findViewById(R.id.notificationActionTitle))
.setText(SLOT_TITLES[i]);
view.findViewById(R.id.notificationActionClickableArea).setOnClickListener(
v -> openActionChooserDialog());
}
void setupCheckbox(final View view) {
final CheckBox compactSlotCheckBox = view.findViewById(R.id.notificationActionCheckBox);
compactSlotCheckBox.setChecked(compactSlots.contains(i));
view.findViewById(R.id.notificationActionCheckBoxClickableArea).setOnClickListener(
v -> {
if (compactSlotCheckBox.isChecked()) {
compactSlots.remove((Integer) i);
} else if (compactSlots.size() < 3) {
compactSlots.add(i);
} else {
Toast.makeText(getContext(),
R.string.notification_actions_at_most_three,
Toast.LENGTH_SHORT).show();
return;
}
compactSlotCheckBox.toggle();
});
}
void setupSelectedAction(final View view) {
icon = view.findViewById(R.id.notificationActionIcon);
summary = view.findViewById(R.id.notificationActionSummary);
selectedAction = getSharedPreferences().getInt(
getContext().getString(NotificationConstants.SLOT_PREF_KEYS[i]),
NotificationConstants.SLOT_DEFAULTS[i]);
updateInfo();
}
void updateInfo() {
if (NotificationConstants.ACTION_ICONS[selectedAction] == 0) {
icon.setImageDrawable(null);
} else {
icon.setImageDrawable(AppCompatResources.getDrawable(getContext(),
NotificationConstants.ACTION_ICONS[selectedAction]));
}
summary.setText(NotificationConstants.getActionName(getContext(), selectedAction));
}
void openActionChooserDialog() {
final LayoutInflater inflater = LayoutInflater.from(getContext());
final SingleChoiceDialogViewBinding binding =
SingleChoiceDialogViewBinding.inflate(inflater);
final AlertDialog alertDialog = new AlertDialog.Builder(getContext())
.setTitle(SLOT_TITLES[i])
.setView(binding.getRoot())
.setCancelable(true)
.create();
final View.OnClickListener radioButtonsClickListener = v -> {
selectedAction = NotificationConstants.SLOT_ALLOWED_ACTIONS[i][v.getId()];
updateInfo();
alertDialog.dismiss();
};
for (int id = 0; id < NotificationConstants.SLOT_ALLOWED_ACTIONS[i].length; ++id) {
final int action = NotificationConstants.SLOT_ALLOWED_ACTIONS[i][id];
final RadioButton radioButton = ListRadioIconItemBinding.inflate(inflater)
.getRoot();
// if present set action icon with correct color
final int iconId = NotificationConstants.ACTION_ICONS[action];
if (iconId != 0) {
radioButton.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, iconId, 0);
final var color = ColorStateList.valueOf(ThemeHelper
.resolveColorFromAttr(getContext(), android.R.attr.textColorPrimary));
TextViewCompat.setCompoundDrawableTintList(radioButton, color);
}
radioButton.setText(NotificationConstants.getActionName(getContext(), action));
radioButton.setChecked(action == selectedAction);
radioButton.setId(id);
radioButton.setLayoutParams(new RadioGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT));
radioButton.setOnClickListener(radioButtonsClickListener);
binding.list.addView(radioButton);
}
alertDialog.show();
if (DeviceUtils.isTv(getContext())) {
FocusOverlayView.setupFocusObserver(alertDialog);
}
}
}
}

View File

@ -0,0 +1,172 @@
package org.schabi.newpipe.settings.custom;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.res.ColorStateList;
import android.os.Build;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.CheckBox;
import android.widget.ImageView;
import android.widget.RadioButton;
import android.widget.RadioGroup;
import android.widget.TextView;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.content.res.AppCompatResources;
import androidx.core.widget.TextViewCompat;
import org.schabi.newpipe.R;
import org.schabi.newpipe.databinding.ListRadioIconItemBinding;
import org.schabi.newpipe.databinding.SingleChoiceDialogViewBinding;
import org.schabi.newpipe.player.notification.NotificationConstants;
import org.schabi.newpipe.util.DeviceUtils;
import org.schabi.newpipe.util.ThemeHelper;
import org.schabi.newpipe.views.FocusOverlayView;
import java.util.Objects;
import java.util.function.BiConsumer;
class NotificationSlot {
private static final int[] SLOT_ITEMS = {
R.id.notificationAction0,
R.id.notificationAction1,
R.id.notificationAction2,
R.id.notificationAction3,
R.id.notificationAction4,
};
private static final int[] SLOT_TITLES = {
R.string.notification_action_0_title,
R.string.notification_action_1_title,
R.string.notification_action_2_title,
R.string.notification_action_3_title,
R.string.notification_action_4_title,
};
private final int i;
private @NotificationConstants.Action int selectedAction;
private final Context context;
private final BiConsumer<Integer, CheckBox> onToggleCompactSlot;
private ImageView icon;
private TextView summary;
NotificationSlot(final Context context,
final SharedPreferences prefs,
final int actionIndex,
final View parentView,
final boolean isCompactSlotChecked,
final BiConsumer<Integer, CheckBox> onToggleCompactSlot) {
this.context = context;
this.i = actionIndex;
this.onToggleCompactSlot = onToggleCompactSlot;
selectedAction = Objects.requireNonNull(prefs).getInt(
context.getString(NotificationConstants.SLOT_PREF_KEYS[i]),
NotificationConstants.SLOT_DEFAULTS[i]);
final View view = parentView.findViewById(SLOT_ITEMS[i]);
// only show the last two notification slots on Android 13+
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU || i >= 3) {
setupSelectedAction(view);
setupTitle(view);
setupCheckbox(view, isCompactSlotChecked);
} else {
view.setVisibility(View.GONE);
}
}
void setupTitle(final View view) {
((TextView) view.findViewById(R.id.notificationActionTitle))
.setText(SLOT_TITLES[i]);
view.findViewById(R.id.notificationActionClickableArea).setOnClickListener(
v -> openActionChooserDialog());
}
void setupCheckbox(final View view, final boolean isCompactSlotChecked) {
final CheckBox compactSlotCheckBox = view.findViewById(R.id.notificationActionCheckBox);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
// there are no compact slots to customize on Android 13+
compactSlotCheckBox.setVisibility(View.GONE);
view.findViewById(R.id.notificationActionCheckBoxClickableArea)
.setVisibility(View.GONE);
return;
}
compactSlotCheckBox.setChecked(isCompactSlotChecked);
view.findViewById(R.id.notificationActionCheckBoxClickableArea).setOnClickListener(
v -> onToggleCompactSlot.accept(i, compactSlotCheckBox));
}
void setupSelectedAction(final View view) {
icon = view.findViewById(R.id.notificationActionIcon);
summary = view.findViewById(R.id.notificationActionSummary);
updateInfo();
}
void updateInfo() {
if (NotificationConstants.ACTION_ICONS[selectedAction] == 0) {
icon.setImageDrawable(null);
} else {
icon.setImageDrawable(AppCompatResources.getDrawable(context,
NotificationConstants.ACTION_ICONS[selectedAction]));
}
summary.setText(NotificationConstants.getActionName(context, selectedAction));
}
void openActionChooserDialog() {
final LayoutInflater inflater = LayoutInflater.from(context);
final SingleChoiceDialogViewBinding binding =
SingleChoiceDialogViewBinding.inflate(inflater);
final AlertDialog alertDialog = new AlertDialog.Builder(context)
.setTitle(SLOT_TITLES[i])
.setView(binding.getRoot())
.setCancelable(true)
.create();
final View.OnClickListener radioButtonsClickListener = v -> {
selectedAction = NotificationConstants.ALL_ACTIONS[v.getId()];
updateInfo();
alertDialog.dismiss();
};
for (int id = 0; id < NotificationConstants.ALL_ACTIONS.length; ++id) {
final int action = NotificationConstants.ALL_ACTIONS[id];
final RadioButton radioButton = ListRadioIconItemBinding.inflate(inflater)
.getRoot();
// if present set action icon with correct color
final int iconId = NotificationConstants.ACTION_ICONS[action];
if (iconId != 0) {
radioButton.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, iconId, 0);
final var color = ColorStateList.valueOf(ThemeHelper
.resolveColorFromAttr(context, android.R.attr.textColorPrimary));
TextViewCompat.setCompoundDrawableTintList(radioButton, color);
}
radioButton.setText(NotificationConstants.getActionName(context, action));
radioButton.setChecked(action == selectedAction);
radioButton.setId(id);
radioButton.setLayoutParams(new RadioGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT));
radioButton.setOnClickListener(radioButtonsClickListener);
binding.list.addView(radioButton);
}
alertDialog.show();
if (DeviceUtils.isTv(context)) {
FocusOverlayView.setupFocusObserver(alertDialog);
}
}
@NotificationConstants.Action
public int getSelectedAction() {
return selectedAction;
}
}

View File

@ -162,6 +162,15 @@ public final class ExtractorHelper {
CommentsInfo.getMoreItems(NewPipe.getService(serviceId), info, nextPage));
}
public static Single<InfoItemsPage<CommentsInfoItem>> getMoreCommentItems(
final int serviceId,
final String url,
final Page nextPage) {
checkServiceId(serviceId);
return Single.fromCallable(() ->
CommentsInfo.getMoreItems(NewPipe.getService(serviceId), url, nextPage));
}
public static Single<PlaylistInfo> getPlaylistInfo(final int serviceId,
final String url,
final boolean forceLoad) {

View File

@ -7,6 +7,7 @@ import androidx.preference.PreferenceManager;
import org.schabi.newpipe.R;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public final class FilenameUtils {
@ -51,7 +52,7 @@ public final class FilenameUtils {
final Pattern pattern = Pattern.compile(charset);
return createFilename(title, pattern, replacementChar);
return createFilename(title, pattern, Matcher.quoteReplacement(replacementChar));
}
/**

View File

@ -46,10 +46,10 @@ public final class ListHelper {
List.of(MediaFormat.MP3, MediaFormat.M4A, MediaFormat.WEBMA);
// Use a Set for better performance
private static final Set<String> HIGH_RESOLUTION_LIST = Set.of("1440p", "2160p");
// Audio track types in order of priotity. 0=lowest, n=highest
// Audio track types in order of priority. 0=lowest, n=highest
private static final List<AudioTrackType> AUDIO_TRACK_TYPE_RANKING =
List.of(AudioTrackType.DESCRIPTIVE, AudioTrackType.DUBBED, AudioTrackType.ORIGINAL);
// Audio track types in order of priotity when descriptive audio is preferred.
// Audio track types in order of priority when descriptive audio is preferred.
private static final List<AudioTrackType> AUDIO_TRACK_TYPE_RANKING_DESCRIPTIVE =
List.of(AudioTrackType.ORIGINAL, AudioTrackType.DUBBED, AudioTrackType.DESCRIPTIVE);
@ -189,13 +189,16 @@ public final class ListHelper {
/**
* Return a {@link Stream} list which only contains streams which can be played by the player.
* <br>
* Some formats are not supported. For more info, see {@link #SUPPORTED_ITAG_IDS}.
* Torrent streams are also removed, because they cannot be retrieved.
*
* <p>
* Some formats are not supported, see {@link #SUPPORTED_ITAG_IDS} for more details.
* Torrent streams are also removed, because they cannot be retrieved, like OPUS streams using
* HLS as their delivery method, since they are not supported by ExoPlayer.
* </p>
*
* @param <S> the item type's class that extends {@link Stream}
* @param streamList the original stream list
* @param serviceId
* @param serviceId the service ID from which the streams' list comes from
* @return a stream list which only contains streams that can be played the player
*/
@NonNull
@ -204,6 +207,8 @@ public final class ListHelper {
final int youtubeServiceId = YouTube.getServiceId();
return getFilteredStreamList(streamList,
stream -> stream.getDeliveryMethod() != DeliveryMethod.TORRENT
&& (stream.getDeliveryMethod() != DeliveryMethod.HLS
|| stream.getFormat() != MediaFormat.OPUS)
&& (serviceId != youtubeServiceId
|| stream.getItagItem() == null
|| SUPPORTED_ITAG_IDS.contains(stream.getItagItem().id)));
@ -295,7 +300,9 @@ public final class ListHelper {
final Comparator<AudioStream> cmp = getAudioFormatComparator(context);
for (final AudioStream stream : audioStreams) {
if (stream.getDeliveryMethod() == DeliveryMethod.TORRENT) {
if (stream.getDeliveryMethod() == DeliveryMethod.TORRENT
|| (stream.getDeliveryMethod() == DeliveryMethod.HLS
&& stream.getFormat() == MediaFormat.OPUS)) {
continue;
}
@ -689,7 +696,7 @@ public final class ListHelper {
}
}
private static boolean isLimitingDataUsage(final Context context) {
static boolean isLimitingDataUsage(@NonNull final Context context) {
return getResolutionLimit(context) != null;
}
@ -731,7 +738,7 @@ public final class ListHelper {
/**
* Get a {@link Comparator} to compare {@link AudioStream}s by their format and bitrate.
*
* <p>The prefered stream will be ordered last.</p>
* <p>The preferred stream will be ordered last.</p>
*
* @param context app context
* @return Comparator
@ -746,7 +753,7 @@ public final class ListHelper {
/**
* Get a {@link Comparator} to compare {@link AudioStream}s by their format and bitrate.
*
* <p>The prefered stream will be ordered last.</p>
* <p>The preferred stream will be ordered last.</p>
*
* @param defaultFormat the default format to look for
* @param limitDataUsage choose low bitrate audio stream
@ -788,7 +795,7 @@ public final class ListHelper {
* <li>Language is English</li>
* </ol>
*
* <p>The prefered track will be ordered last.</p>
* <p>The preferred track will be ordered last.</p>
*
* @param context App context
* @return Comparator
@ -825,7 +832,7 @@ public final class ListHelper {
* <li>Language is English</li>
* </ol>
*
* <p>The prefered track will be ordered last.</p>
* <p>The preferred track will be ordered last.</p>
*
* @param preferredLanguage Preferred audio stream language
* @param preferOriginalAudio Get the original audio track regardless of its language

View File

@ -1,5 +1,7 @@
package org.schabi.newpipe.util;
import static org.schabi.newpipe.MainActivity.DEBUG;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.SharedPreferences;
@ -22,6 +24,7 @@ import org.ocpsoft.prettytime.units.Decade;
import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.ListExtractor;
import org.schabi.newpipe.extractor.localization.ContentCountry;
import org.schabi.newpipe.extractor.localization.DateWrapper;
import org.schabi.newpipe.extractor.stream.AudioStream;
import org.schabi.newpipe.extractor.stream.AudioTrackType;
@ -82,7 +85,7 @@ public final class Localization {
.fromLocale(getPreferredLocale(context));
}
public static ContentCountry getPreferredContentCountry(final Context context) {
public static ContentCountry getPreferredContentCountry(@NonNull final Context context) {
final String contentCountry = PreferenceManager.getDefaultSharedPreferences(context)
.getString(context.getString(R.string.content_country_key),
context.getString(R.string.default_localization_key));
@ -92,41 +95,43 @@ public final class Localization {
return new ContentCountry(contentCountry);
}
public static Locale getPreferredLocale(final Context context) {
public static Locale getPreferredLocale(@NonNull final Context context) {
return getLocaleFromPrefs(context, R.string.content_language_key);
}
public static Locale getAppLocale(final Context context) {
public static Locale getAppLocale(@NonNull final Context context) {
return getLocaleFromPrefs(context, R.string.app_language_key);
}
public static String localizeNumber(final Context context, final long number) {
public static String localizeNumber(@NonNull final Context context, final long number) {
return localizeNumber(context, (double) number);
}
public static String localizeNumber(final Context context, final double number) {
public static String localizeNumber(@NonNull final Context context, final double number) {
final NumberFormat nf = NumberFormat.getInstance(getAppLocale(context));
return nf.format(number);
}
public static String formatDate(final OffsetDateTime offsetDateTime, final Context context) {
public static String formatDate(@NonNull final Context context,
@NonNull final OffsetDateTime offsetDateTime) {
return DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM)
.withLocale(getAppLocale(context)).format(offsetDateTime
.atZoneSameInstant(ZoneId.systemDefault()));
}
@SuppressLint("StringFormatInvalid")
public static String localizeUploadDate(final Context context,
final OffsetDateTime offsetDateTime) {
return context.getString(R.string.upload_date_text, formatDate(offsetDateTime, context));
public static String localizeUploadDate(@NonNull final Context context,
@NonNull final OffsetDateTime offsetDateTime) {
return context.getString(R.string.upload_date_text, formatDate(context, offsetDateTime));
}
public static String localizeViewCount(final Context context, final long viewCount) {
public static String localizeViewCount(@NonNull final Context context, final long viewCount) {
return getQuantity(context, R.plurals.views, R.string.no_views, viewCount,
localizeNumber(context, viewCount));
}
public static String localizeStreamCount(final Context context, final long streamCount) {
public static String localizeStreamCount(@NonNull final Context context,
final long streamCount) {
switch ((int) streamCount) {
case (int) ListExtractor.ITEM_COUNT_UNKNOWN:
return "";
@ -140,7 +145,8 @@ public final class Localization {
}
}
public static String localizeStreamCountMini(final Context context, final long streamCount) {
public static String localizeStreamCountMini(@NonNull final Context context,
final long streamCount) {
switch ((int) streamCount) {
case (int) ListExtractor.ITEM_COUNT_UNKNOWN:
return "";
@ -153,12 +159,13 @@ public final class Localization {
}
}
public static String localizeWatchingCount(final Context context, final long watchingCount) {
public static String localizeWatchingCount(@NonNull final Context context,
final long watchingCount) {
return getQuantity(context, R.plurals.watching, R.string.no_one_watching, watchingCount,
localizeNumber(context, watchingCount));
}
public static String shortCount(final Context context, final long count) {
public static String shortCount(@NonNull final Context context, final long count) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
return CompactDecimalFormat.getInstance(getAppLocale(context),
CompactDecimalFormat.CompactStyle.SHORT).format(count);
@ -179,36 +186,58 @@ public final class Localization {
}
}
public static String listeningCount(final Context context, final long listeningCount) {
public static String listeningCount(@NonNull final Context context, final long listeningCount) {
return getQuantity(context, R.plurals.listening, R.string.no_one_listening, listeningCount,
shortCount(context, listeningCount));
}
public static String shortWatchingCount(final Context context, final long watchingCount) {
public static String shortWatchingCount(@NonNull final Context context,
final long watchingCount) {
return getQuantity(context, R.plurals.watching, R.string.no_one_watching, watchingCount,
shortCount(context, watchingCount));
}
public static String shortViewCount(final Context context, final long viewCount) {
public static String shortViewCount(@NonNull final Context context, final long viewCount) {
return getQuantity(context, R.plurals.views, R.string.no_views, viewCount,
shortCount(context, viewCount));
}
public static String shortSubscriberCount(final Context context, final long subscriberCount) {
public static String shortSubscriberCount(@NonNull final Context context,
final long subscriberCount) {
return getQuantity(context, R.plurals.subscribers, R.string.no_subscribers, subscriberCount,
shortCount(context, subscriberCount));
}
public static String downloadCount(final Context context, final int downloadCount) {
public static String downloadCount(@NonNull final Context context, final int downloadCount) {
return getQuantity(context, R.plurals.download_finished_notification, 0,
downloadCount, shortCount(context, downloadCount));
}
public static String deletedDownloadCount(final Context context, final int deletedCount) {
public static String deletedDownloadCount(@NonNull final Context context,
final int deletedCount) {
return getQuantity(context, R.plurals.deleted_downloads_toast, 0,
deletedCount, shortCount(context, deletedCount));
}
public static String replyCount(@NonNull final Context context, final int replyCount) {
return getQuantity(context, R.plurals.replies, 0, replyCount,
String.valueOf(replyCount));
}
/**
* @param context the Android context
* @param likeCount the like count, possibly negative if unknown
* @return if {@code likeCount} is smaller than {@code 0}, the string {@code "-"}, otherwise
* the result of calling {@link #shortCount(Context, long)} on the like count
*/
public static String likeCount(@NonNull final Context context, final int likeCount) {
if (likeCount < 0) {
return "-";
} else {
return shortCount(context, likeCount);
}
}
public static String getDurationString(final long duration) {
final String output;
@ -241,7 +270,8 @@ public final class Localization {
* @return duration in a human readable string.
*/
@NonNull
public static String localizeDuration(final Context context, final int durationInSecs) {
public static String localizeDuration(@NonNull final Context context,
final int durationInSecs) {
if (durationInSecs < 0) {
throw new IllegalArgumentException("duration can not be negative");
}
@ -278,7 +308,7 @@ public final class Localization {
* @param track an {@link AudioStream} of the track
* @return the localized name of the audio track
*/
public static String audioTrackName(final Context context, final AudioStream track) {
public static String audioTrackName(@NonNull final Context context, final AudioStream track) {
final String name;
if (track.getAudioLocale() != null) {
name = track.getAudioLocale().getDisplayLanguage(getAppLocale(context));
@ -298,7 +328,8 @@ public final class Localization {
}
@Nullable
private static String audioTrackType(final Context context, final AudioTrackType trackType) {
private static String audioTrackType(@NonNull final Context context,
final AudioTrackType trackType) {
switch (trackType) {
case ORIGINAL:
return context.getString(R.string.audio_track_type_original);
@ -314,20 +345,45 @@ public final class Localization {
// Pretty Time
//////////////////////////////////////////////////////////////////////////*/
public static void initPrettyTime(final PrettyTime time) {
public static void initPrettyTime(@NonNull final PrettyTime time) {
prettyTime = time;
// Do not use decades as YouTube doesn't either.
prettyTime.removeUnit(Decade.class);
}
public static PrettyTime resolvePrettyTime(final Context context) {
public static PrettyTime resolvePrettyTime(@NonNull final Context context) {
return new PrettyTime(getAppLocale(context));
}
public static String relativeTime(final OffsetDateTime offsetDateTime) {
public static String relativeTime(@NonNull final OffsetDateTime offsetDateTime) {
return prettyTime.formatUnrounded(offsetDateTime);
}
/**
* @param context the Android context; if {@code null} then even if in debug mode and the
* setting is enabled, {@code textual} will not be shown next to {@code parsed}
* @param parsed the textual date or time ago parsed by NewPipeExtractor, or {@code null} if
* the extractor could not parse it
* @param textual the original textual date or time ago string as provided by services
* @return {@link #relativeTime(OffsetDateTime)} is used if {@code parsed != null}, otherwise
* {@code textual} is returned. If in debug mode, {@code context != null},
* {@code parsed != null} and the relevant setting is enabled, {@code textual} will
* be appended to the returned string for debugging purposes.
*/
public static String relativeTimeOrTextual(@Nullable final Context context,
@Nullable final DateWrapper parsed,
final String textual) {
if (parsed == null) {
return textual;
} else if (DEBUG && context != null && PreferenceManager
.getDefaultSharedPreferences(context)
.getBoolean(context.getString(R.string.show_original_time_ago_key), false)) {
return relativeTime(parsed.offsetDateTime()) + " (" + textual + ")";
} else {
return relativeTime(parsed.offsetDateTime());
}
}
public static void assureCorrectAppLanguage(final Context c) {
final Resources res = c.getResources();
final DisplayMetrics dm = res.getDisplayMetrics();
@ -336,7 +392,8 @@ public final class Localization {
res.updateConfiguration(conf, dm);
}
private static Locale getLocaleFromPrefs(final Context context, @StringRes final int prefKey) {
private static Locale getLocaleFromPrefs(@NonNull final Context context,
@StringRes final int prefKey) {
final SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(context);
final String defaultKey = context.getString(R.string.default_localization_key);
final String languageCode = sp.getString(context.getString(prefKey), defaultKey);
@ -352,8 +409,10 @@ public final class Localization {
return new BigDecimal(value).setScale(1, RoundingMode.HALF_UP).doubleValue();
}
private static String getQuantity(final Context context, @PluralsRes final int pluralId,
@StringRes final int zeroCaseStringId, final long count,
private static String getQuantity(@NonNull final Context context,
@PluralsRes final int pluralId,
@StringRes final int zeroCaseStringId,
final long count,
final String formattedCount) {
if (count == 0) {
return context.getString(zeroCaseStringId);

View File

@ -1,5 +1,6 @@
package org.schabi.newpipe.util;
import static android.text.TextUtils.isEmpty;
import static org.schabi.newpipe.util.ListHelper.getUrlAndNonTorrentStreams;
import android.annotation.SuppressLint;
@ -17,6 +18,7 @@ import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.content.ContextCompat;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentActivity;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentTransaction;
@ -29,8 +31,10 @@ import org.schabi.newpipe.RouterActivity;
import org.schabi.newpipe.about.AboutActivity;
import org.schabi.newpipe.database.feed.model.FeedGroupEntity;
import org.schabi.newpipe.download.DownloadActivity;
import org.schabi.newpipe.error.ErrorUtil;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.StreamingService;
import org.schabi.newpipe.extractor.comments.CommentsInfoItem;
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
import org.schabi.newpipe.extractor.stream.AudioStream;
import org.schabi.newpipe.extractor.stream.DeliveryMethod;
@ -41,6 +45,7 @@ import org.schabi.newpipe.extractor.stream.VideoStream;
import org.schabi.newpipe.fragments.MainFragment;
import org.schabi.newpipe.fragments.detail.VideoDetailFragment;
import org.schabi.newpipe.fragments.list.channel.ChannelFragment;
import org.schabi.newpipe.fragments.list.comments.CommentRepliesFragment;
import org.schabi.newpipe.fragments.list.kiosk.KioskFragment;
import org.schabi.newpipe.fragments.list.playlist.PlaylistFragment;
import org.schabi.newpipe.fragments.list.search.SearchFragment;
@ -476,6 +481,35 @@ public final class NavigationHelper {
item.getServiceId(), uploaderUrl, item.getUploaderName());
}
/**
* Opens the comment author channel fragment, if the {@link CommentsInfoItem#getUploaderUrl()}
* of {@code comment} is non-null. Shows a UI-error snackbar if something goes wrong.
*
* @param activity the activity with the fragment manager and in which to show the snackbar
* @param comment the comment whose uploader/author will be opened
*/
public static void openCommentAuthorIfPresent(@NonNull final FragmentActivity activity,
@NonNull final CommentsInfoItem comment) {
if (isEmpty(comment.getUploaderUrl())) {
return;
}
try {
openChannelFragment(activity.getSupportFragmentManager(), comment.getServiceId(),
comment.getUploaderUrl(), comment.getUploaderName());
} catch (final Exception e) {
ErrorUtil.showUiErrorSnackbar(activity, "Opening channel fragment", e);
}
}
public static void openCommentRepliesFragment(@NonNull final FragmentActivity activity,
@NonNull final CommentsInfoItem comment) {
defaultTransaction(activity.getSupportFragmentManager())
.replace(R.id.fragment_holder, new CommentRepliesFragment(comment),
CommentRepliesFragment.TAG)
.addToBackStack(CommentRepliesFragment.TAG)
.commit();
}
public static void openPlaylistFragment(final FragmentManager fragmentManager,
final int serviceId, final String url,
@NonNull final String name) {

View File

@ -1,27 +0,0 @@
package org.schabi.newpipe.util;
import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.extractor.ListInfo;
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler;
import org.schabi.newpipe.extractor.stream.StreamInfo;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class RelatedItemInfo extends ListInfo<InfoItem> {
public RelatedItemInfo(final int serviceId, final ListLinkHandler listUrlIdHandler,
final String name) {
super(serviceId, listUrlIdHandler, name);
}
public static RelatedItemInfo getInfo(final StreamInfo info) {
final ListLinkHandler handler = new ListLinkHandler(
info.getOriginalUrl(), info.getUrl(), info.getId(), Collections.emptyList(), null);
final RelatedItemInfo relatedItemInfo = new RelatedItemInfo(
info.getServiceId(), handler, info.getName());
final List<InfoItem> relatedItems = new ArrayList<>(info.getRelatedItems());
relatedItemInfo.setRelatedItems(relatedItems);
return relatedItemInfo;
}
}

View File

@ -1,97 +1,39 @@
package org.schabi.newpipe.util
import android.content.pm.PackageManager
import android.content.pm.Signature
import androidx.core.content.pm.PackageInfoCompat
import org.schabi.newpipe.App
import org.schabi.newpipe.error.ErrorInfo
import org.schabi.newpipe.error.ErrorUtil.Companion.createNotification
import org.schabi.newpipe.error.UserAction
import java.security.MessageDigest
import java.security.NoSuchAlgorithmException
import java.security.cert.CertificateEncodingException
import java.security.cert.CertificateException
import java.security.cert.CertificateFactory
import java.security.cert.X509Certificate
import java.time.Instant
import java.time.ZonedDateTime
import java.time.format.DateTimeFormatter
object ReleaseVersionUtil {
// Public key of the certificate that is used in NewPipe release versions
private const val RELEASE_CERT_PUBLIC_KEY_SHA1 =
"B0:2E:90:7C:1C:D6:FC:57:C3:35:F0:88:D0:8F:50:5F:94:E4:D2:15"
private const val RELEASE_CERT_PUBLIC_KEY_SHA256 =
"cb84069bd68116bafae5ee4ee5b08a567aa6d898404e7cb12f9e756df5cf5cab"
@JvmStatic
fun isReleaseApk(): Boolean {
return certificateSHA1Fingerprint == RELEASE_CERT_PUBLIC_KEY_SHA1
}
/**
* Method to get the APK's SHA1 key. See https://stackoverflow.com/questions/9293019/#22506133.
*
* @return String with the APK's SHA1 fingerprint in hexadecimal
*/
private val certificateSHA1Fingerprint: String
get() {
val app = App.getApp()
val signatures: List<Signature> = try {
PackageInfoCompat.getSignatures(app.packageManager, app.packageName)
} catch (e: PackageManager.NameNotFoundException) {
showRequestError(app, e, "Could not find package info")
return ""
}
if (signatures.isEmpty()) {
return ""
}
val x509cert = try {
val cf = CertificateFactory.getInstance("X509")
cf.generateCertificate(signatures[0].toByteArray().inputStream()) as X509Certificate
} catch (e: CertificateException) {
showRequestError(app, e, "Certificate error")
return ""
}
return try {
val md = MessageDigest.getInstance("SHA1")
val publicKey = md.digest(x509cert.encoded)
byte2HexFormatted(publicKey)
} catch (e: NoSuchAlgorithmException) {
showRequestError(app, e, "Could not retrieve SHA1 key")
""
} catch (e: CertificateEncodingException) {
showRequestError(app, e, "Could not retrieve SHA1 key")
""
}
}
private fun byte2HexFormatted(arr: ByteArray): String {
val str = StringBuilder(arr.size * 2)
for (i in arr.indices) {
var h = Integer.toHexString(arr[i].toInt())
val l = h.length
if (l == 1) {
h = "0$h"
}
if (l > 2) {
h = h.substring(l - 2, l)
}
str.append(h.uppercase())
if (i < arr.size - 1) {
str.append(':')
}
}
return str.toString()
}
private fun showRequestError(app: App, e: Exception, request: String) {
createNotification(
app, ErrorInfo(e, UserAction.CHECK_FOR_NEW_APP_VERSION, request)
@OptIn(ExperimentalStdlibApi::class)
val isReleaseApk by lazy {
@Suppress("NewApi")
val certificates = mapOf(
RELEASE_CERT_PUBLIC_KEY_SHA256.hexToByteArray() to PackageManager.CERT_INPUT_SHA256
)
val app = App.getApp()
try {
PackageInfoCompat.hasSignatures(app.packageManager, app.packageName, certificates, false)
} catch (e: PackageManager.NameNotFoundException) {
createNotification(
app, ErrorInfo(e, UserAction.CHECK_FOR_NEW_APP_VERSION, "Could not find package info")
)
false
}
}
fun isLastUpdateCheckExpired(expiry: Long): Boolean {
return Instant.ofEpochSecond(expiry).isBefore(Instant.now())
return Instant.ofEpochSecond(expiry) < Instant.now()
}
/**

View File

@ -1,5 +1,7 @@
package org.schabi.newpipe.util;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@ -9,6 +11,7 @@ import org.schabi.newpipe.extractor.stream.Stream;
import org.schabi.newpipe.extractor.stream.VideoStream;
import org.schabi.newpipe.util.StreamItemAdapter.StreamInfoWrapper;
import java.util.Comparator;
import java.util.List;
public class SecondaryStreamHelper<T extends Stream> {
@ -25,14 +28,19 @@ public class SecondaryStreamHelper<T extends Stream> {
}
/**
* Find the correct audio stream for the desired video stream.
* Finds an audio stream compatible with the provided video-only stream, so that the two streams
* can be combined in a single file by the downloader. If there are multiple available audio
* streams, chooses either the highest or the lowest quality one based on
* {@link ListHelper#isLimitingDataUsage(Context)}.
*
* @param context Android context
* @param audioStreams list of audio streams
* @param videoStream desired video ONLY stream
* @return selected audio stream or null if a candidate was not found
* @param videoStream desired video-ONLY stream
* @return the selected audio stream or null if a candidate was not found
*/
@Nullable
public static AudioStream getAudioStreamFor(@NonNull final List<AudioStream> audioStreams,
public static AudioStream getAudioStreamFor(@NonNull final Context context,
@NonNull final List<AudioStream> audioStreams,
@NonNull final VideoStream videoStream) {
final MediaFormat mediaFormat = videoStream.getFormat();
if (mediaFormat == null) {
@ -41,33 +49,36 @@ public class SecondaryStreamHelper<T extends Stream> {
switch (mediaFormat) {
case WEBM:
case MPEG_4:// ¿is mpeg-4 DASH?
case MPEG_4: // Is MPEG-4 DASH?
break;
default:
return null;
}
final boolean m4v = (mediaFormat == MediaFormat.MPEG_4);
final boolean m4v = mediaFormat == MediaFormat.MPEG_4;
final boolean isLimitingDataUsage = ListHelper.isLimitingDataUsage(context);
for (final AudioStream audio : audioStreams) {
if (audio.getFormat() == (m4v ? MediaFormat.M4A : MediaFormat.WEBMA)) {
return audio;
Comparator<AudioStream> comparator = ListHelper.getAudioFormatComparator(
m4v ? MediaFormat.M4A : MediaFormat.WEBMA, isLimitingDataUsage);
int preferredAudioStreamIndex = ListHelper.getAudioIndexByHighestRank(
audioStreams, comparator);
if (preferredAudioStreamIndex == -1) {
if (m4v) {
return null;
}
comparator = ListHelper.getAudioFormatComparator(
MediaFormat.WEBMA_OPUS, isLimitingDataUsage);
preferredAudioStreamIndex = ListHelper.getAudioIndexByHighestRank(
audioStreams, comparator);
if (preferredAudioStreamIndex == -1) {
return null;
}
}
if (m4v) {
return null;
}
// retry, but this time in reverse order
for (int i = audioStreams.size() - 1; i >= 0; i--) {
final AudioStream audio = audioStreams.get(i);
if (audio.getFormat() == MediaFormat.WEBMA_OPUS) {
return audio;
}
}
return null;
return audioStreams.get(preferredAudioStreamIndex);
}
public T getStream() {

View File

@ -144,6 +144,19 @@ public final class ServiceHelper {
.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 {

View File

@ -27,6 +27,7 @@ import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.os.BundleCompat;
import org.schabi.newpipe.BuildConfig;
import org.schabi.newpipe.MainActivity;
@ -82,7 +83,8 @@ public final class StateSaver {
return null;
}
final SavedState savedState = outState.getParcelable(KEY_SAVED_STATE);
final SavedState savedState = BundleCompat.getParcelable(
outState, KEY_SAVED_STATE, SavedState.class);
if (savedState == null) {
return null;
}

View File

@ -0,0 +1,193 @@
package org.schabi.newpipe.util.text;
import android.graphics.Paint;
import android.text.Layout;
import android.view.View;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.text.HtmlCompat;
import org.schabi.newpipe.extractor.StreamingService;
import org.schabi.newpipe.extractor.stream.Description;
import java.util.function.Consumer;
import io.reactivex.rxjava3.disposables.CompositeDisposable;
/**
* <p>Class to ellipsize text inside a {@link TextView}.</p>
* This class provides all utils to automatically ellipsize and expand a text
*/
public final class TextEllipsizer {
private static final int EXPANDED_LINES = Integer.MAX_VALUE;
private static final String ELLIPSIS = "";
@NonNull private final CompositeDisposable disposable = new CompositeDisposable();
@NonNull private final TextView view;
private final int maxLines;
@NonNull private Description content;
@Nullable private StreamingService streamingService;
@Nullable private String streamUrl;
private boolean isEllipsized = false;
@Nullable private Boolean canBeEllipsized = null;
@NonNull private final Paint paintAtContentSize = new Paint();
private final float ellipsisWidthPx;
@Nullable private Consumer<Boolean> stateChangeListener = null;
@Nullable private Consumer<Boolean> onContentChanged;
public TextEllipsizer(@NonNull final TextView view,
final int maxLines,
@Nullable final StreamingService streamingService) {
this.view = view;
this.maxLines = maxLines;
this.content = Description.EMPTY_DESCRIPTION;
this.streamingService = streamingService;
paintAtContentSize.setTextSize(view.getTextSize());
ellipsisWidthPx = paintAtContentSize.measureText(ELLIPSIS);
}
public void setOnContentChanged(@Nullable final Consumer<Boolean> onContentChanged) {
this.onContentChanged = onContentChanged;
}
public void setContent(@NonNull final Description content) {
this.content = content;
canBeEllipsized = null;
linkifyContentView(v -> {
final int currentMaxLines = view.getMaxLines();
view.setMaxLines(EXPANDED_LINES);
canBeEllipsized = view.getLineCount() > maxLines;
view.setMaxLines(currentMaxLines);
if (onContentChanged != null) {
onContentChanged.accept(canBeEllipsized);
}
});
}
public void setStreamUrl(@Nullable final String streamUrl) {
this.streamUrl = streamUrl;
}
public void setStreamingService(@NonNull final StreamingService streamingService) {
this.streamingService = streamingService;
}
/**
* Expand the {@link TextEllipsizer#content} to its full length.
*/
public void expand() {
view.setMaxLines(EXPANDED_LINES);
linkifyContentView(v -> isEllipsized = false);
}
/**
* Shorten the {@link TextEllipsizer#content} to the given number of
* {@link TextEllipsizer#maxLines maximum lines} and add trailing '{@code }'
* if the text was shorted.
*/
public void ellipsize() {
// expand text to see whether it is necessary to ellipsize the text
view.setMaxLines(EXPANDED_LINES);
linkifyContentView(v -> {
final CharSequence charSeqText = view.getText();
if (charSeqText != null && view.getLineCount() > maxLines) {
// Note that converting to String removes spans (i.e. links), but that's something
// we actually want since when the text is ellipsized we want all clicks on the
// comment to expand the comment, not to open links.
final String text = charSeqText.toString();
final Layout layout = view.getLayout();
final float lineWidth = layout.getLineWidth(maxLines - 1);
final float layoutWidth = layout.getWidth();
final int lineStart = layout.getLineStart(maxLines - 1);
final int lineEnd = layout.getLineEnd(maxLines - 1);
// remove characters up until there is enough space for the ellipsis
// (also summing 2 more pixels, just to be sure to avoid float rounding errors)
int end = lineEnd;
float removedCharactersWidth = 0.0f;
while (lineWidth - removedCharactersWidth + ellipsisWidthPx + 2.0f > layoutWidth
&& end >= lineStart) {
end -= 1;
// recalculate each time to account for ligatures or other similar things
removedCharactersWidth = paintAtContentSize.measureText(
text.substring(end, lineEnd));
}
// remove trailing spaces and newlines
while (end > 0 && Character.isWhitespace(text.charAt(end - 1))) {
end -= 1;
}
final String newVal = text.substring(0, end) + ELLIPSIS;
view.setText(newVal);
isEllipsized = true;
} else {
isEllipsized = false;
}
view.setMaxLines(maxLines);
});
}
/**
* Toggle the view between the ellipsized and expanded state.
*/
public void toggle() {
if (isEllipsized) {
expand();
} else {
ellipsize();
}
}
/**
* Whether the {@link #view} can be ellipsized.
* This is only the case when the {@link #content} has more lines
* than allowed via {@link #maxLines}.
* @return {@code true} if the {@link #content} has more lines than allowed via
* {@link #maxLines} and thus can be shortened, {@code false} if the {@code content} fits into
* the {@link #view} without being shortened and {@code null} if the initialization is not
* completed yet.
*/
@Nullable
public Boolean canBeEllipsized() {
return canBeEllipsized;
}
private void linkifyContentView(final Consumer<View> consumer) {
final boolean oldState = isEllipsized;
disposable.clear();
TextLinkifier.fromDescription(view, content,
HtmlCompat.FROM_HTML_MODE_LEGACY, streamingService, streamUrl, disposable,
v -> {
consumer.accept(v);
notifyStateChangeListener(oldState);
});
}
/**
* Add a listener which is called when the given content is changed,
* either from <em>ellipsized</em> to <em>full</em> or vice versa.
* @param listener The listener to be called, or {@code null} to remove it.
* The Boolean parameter is the new state.
* <em>Ellipsized</em> content is represented as {@code true},
* normal or <em>full</em> content by {@code false}.
*/
public void setStateChangeListener(@Nullable final Consumer<Boolean> listener) {
this.stateChangeListener = listener;
}
private void notifyStateChangeListener(final boolean oldState) {
if (oldState != isEllipsized && stateChangeListener != null) {
stateChangeListener.accept(isEllipsized);
}
}
}

View File

@ -92,7 +92,7 @@ public final class TextLinkifier {
* {@link HtmlCompat#fromHtml(String, int)}.
* </p>
*
* @param textView the {@link TextView} to set the the HTML string block linked
* @param textView the {@link TextView} to set the HTML string block linked
* @param htmlBlock the HTML string block to be linked
* @param htmlCompatFlag the int flag to be set when {@link HtmlCompat#fromHtml(String,
* int)} will be called

View File

@ -80,10 +80,10 @@ class CircleClipTapView(context: Context?, attrs: AttributeSet) : View(context,
updatePathShape()
}
override fun onDraw(canvas: Canvas?) {
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
canvas?.clipPath(shapePath)
canvas?.drawPath(shapePath, backgroundPaint)
canvas.clipPath(shapePath)
canvas.drawPath(shapePath, backgroundPaint)
}
}

View File

@ -23,7 +23,6 @@ import android.os.Handler;
import android.os.Handler.Callback;
import android.os.IBinder;
import android.os.Message;
import android.os.Parcelable;
import android.util.Log;
import android.widget.Toast;
@ -36,6 +35,7 @@ import androidx.core.app.NotificationCompat.Builder;
import androidx.core.app.PendingIntentCompat;
import androidx.core.app.ServiceCompat;
import androidx.core.content.ContextCompat;
import androidx.core.content.IntentCompat;
import androidx.preference.PreferenceManager;
import org.schabi.newpipe.R;
@ -49,6 +49,7 @@ import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import us.shandian.giga.get.DownloadMission;
import us.shandian.giga.get.MissionRecoveryInfo;
@ -359,29 +360,29 @@ public class DownloadManagerService extends Service {
*/
public static void startMission(Context context, String[] urls, StoredFileHelper storage,
char kind, int threads, String source, String psName,
String[] psArgs, long nearLength, MissionRecoveryInfo[] recoveryInfo) {
Intent intent = new Intent(context, DownloadManagerService.class);
intent.setAction(Intent.ACTION_RUN);
intent.putExtra(EXTRA_URLS, urls);
intent.putExtra(EXTRA_KIND, kind);
intent.putExtra(EXTRA_THREADS, threads);
intent.putExtra(EXTRA_SOURCE, source);
intent.putExtra(EXTRA_POSTPROCESSING_NAME, psName);
intent.putExtra(EXTRA_POSTPROCESSING_ARGS, psArgs);
intent.putExtra(EXTRA_NEAR_LENGTH, nearLength);
intent.putExtra(EXTRA_RECOVERY_INFO, recoveryInfo);
intent.putExtra(EXTRA_PARENT_PATH, storage.getParentUri());
intent.putExtra(EXTRA_PATH, storage.getUri());
intent.putExtra(EXTRA_STORAGE_TAG, storage.getTag());
String[] psArgs, long nearLength,
ArrayList<MissionRecoveryInfo> recoveryInfo) {
final Intent intent = new Intent(context, DownloadManagerService.class)
.setAction(Intent.ACTION_RUN)
.putExtra(EXTRA_URLS, urls)
.putExtra(EXTRA_KIND, kind)
.putExtra(EXTRA_THREADS, threads)
.putExtra(EXTRA_SOURCE, source)
.putExtra(EXTRA_POSTPROCESSING_NAME, psName)
.putExtra(EXTRA_POSTPROCESSING_ARGS, psArgs)
.putExtra(EXTRA_NEAR_LENGTH, nearLength)
.putExtra(EXTRA_RECOVERY_INFO, recoveryInfo)
.putExtra(EXTRA_PARENT_PATH, storage.getParentUri())
.putExtra(EXTRA_PATH, storage.getUri())
.putExtra(EXTRA_STORAGE_TAG, storage.getTag());
context.startService(intent);
}
private void startMission(Intent intent) {
String[] urls = intent.getStringArrayExtra(EXTRA_URLS);
Uri path = intent.getParcelableExtra(EXTRA_PATH);
Uri parentPath = intent.getParcelableExtra(EXTRA_PARENT_PATH);
Uri path = IntentCompat.getParcelableExtra(intent, EXTRA_PATH, Uri.class);
Uri parentPath = IntentCompat.getParcelableExtra(intent, EXTRA_PARENT_PATH, Uri.class);
int threads = intent.getIntExtra(EXTRA_THREADS, 1);
char kind = intent.getCharExtra(EXTRA_KIND, '?');
String psName = intent.getStringExtra(EXTRA_POSTPROCESSING_NAME);
@ -389,7 +390,9 @@ public class DownloadManagerService extends Service {
String source = intent.getStringExtra(EXTRA_SOURCE);
long nearLength = intent.getLongExtra(EXTRA_NEAR_LENGTH, 0);
String tag = intent.getStringExtra(EXTRA_STORAGE_TAG);
Parcelable[] parcelRecovery = intent.getParcelableArrayExtra(EXTRA_RECOVERY_INFO);
final var recovery = IntentCompat.getParcelableArrayListExtra(intent, EXTRA_RECOVERY_INFO,
MissionRecoveryInfo.class);
Objects.requireNonNull(recovery);
StoredFileHelper storage;
try {
@ -404,15 +407,11 @@ public class DownloadManagerService extends Service {
else
ps = Postprocessing.getAlgorithm(psName, psArgs);
MissionRecoveryInfo[] recovery = new MissionRecoveryInfo[parcelRecovery.length];
for (int i = 0; i < parcelRecovery.length; i++)
recovery[i] = (MissionRecoveryInfo) parcelRecovery[i];
final DownloadMission mission = new DownloadMission(urls, storage, kind, ps);
mission.threadCount = threads;
mission.source = source;
mission.nearLength = nearLength;
mission.recoveryInfo = recovery;
mission.recoveryInfo = recovery.toArray(MissionRecoveryInfo[]::new);
if (ps != null)
ps.setTemporalDir(DownloadManager.pickAvailableTemporalDir(this));

View File

@ -0,0 +1,137 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.google.android.material.imageview.ShapeableImageView
android:id="@+id/authorAvatar"
android:layout_width="42dp"
android:layout_height="42dp"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:focusable="false"
android:src="@drawable/placeholder_person"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:shapeAppearance="@style/CircularImageView"
tools:ignore="RtlHardcoded" />
<org.schabi.newpipe.views.NewPipeTextView
android:id="@+id/authorName"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
android:ellipsize="end"
android:lines="1"
android:textAppearance="?android:attr/textAppearanceLarge"
android:textSize="16sp"
app:layout_constraintBottom_toTopOf="@+id/uploadDate"
app:layout_constraintEnd_toStartOf="@+id/thumbsUpImage"
app:layout_constraintStart_toEndOf="@+id/authorAvatar"
app:layout_constraintTop_toTopOf="@+id/authorAvatar"
tools:text="@tools:sample/lorem/random" />
<org.schabi.newpipe.views.NewPipeTextView
android:id="@+id/uploadDate"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
android:ellipsize="end"
android:lines="1"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textSize="14sp"
app:layout_constraintBottom_toBottomOf="@+id/authorAvatar"
app:layout_constraintEnd_toStartOf="@+id/thumbsUpImage"
app:layout_constraintStart_toEndOf="@+id/authorAvatar"
app:layout_constraintTop_toBottomOf="@+id/authorName"
tools:text="5 months ago" />
<ImageView
android:id="@+id/thumbsUpImage"
android:layout_width="21sp"
android:layout_height="21sp"
android:layout_marginEnd="@dimen/video_item_detail_like_margin"
android:contentDescription="@string/detail_likes_img_view_description"
android:src="@drawable/ic_thumb_up"
app:layout_constraintBottom_toBottomOf="@+id/authorAvatar"
app:layout_constraintEnd_toStartOf="@+id/thumbsUpCount"
app:layout_constraintTop_toTopOf="@+id/authorAvatar" />
<org.schabi.newpipe.views.NewPipeTextView
android:id="@+id/thumbsUpCount"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:gravity="center"
android:lines="1"
android:textAppearance="?android:attr/textAppearanceMedium"
android:textSize="14sp"
app:layout_constraintBottom_toBottomOf="@+id/authorAvatar"
app:layout_constraintEnd_toStartOf="@+id/heartImage"
app:layout_constraintTop_toTopOf="@+id/authorAvatar"
tools:text="12M" />
<ImageView
android:id="@+id/heartImage"
android:layout_width="21sp"
android:layout_height="21sp"
android:layout_marginEnd="4dp"
android:contentDescription="@string/detail_heart_img_view_description"
android:src="@drawable/ic_heart"
app:layout_constraintBottom_toBottomOf="@+id/authorAvatar"
app:layout_constraintEnd_toStartOf="@+id/pinnedImage"
app:layout_constraintTop_toTopOf="@+id/authorAvatar"
app:layout_goneMarginEnd="16dp" />
<View
android:id="@+id/authorTouchArea"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_margin="8dp"
android:background="?attr/selectableItemBackground"
app:layout_constraintBottom_toTopOf="@+id/commentContent"
app:layout_constraintEnd_toStartOf="@+id/thumbsUpImage"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<ImageView
android:id="@+id/pinnedImage"
android:layout_width="21sp"
android:layout_height="21sp"
android:layout_marginEnd="16dp"
android:contentDescription="@string/detail_pinned_comment_view_description"
android:src="@drawable/ic_pin"
app:layout_constraintBottom_toBottomOf="@+id/authorAvatar"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@+id/authorAvatar" />
<org.schabi.newpipe.views.NewPipeTextView
android:id="@+id/commentContent"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp"
android:textAppearance="?android:attr/textAppearanceLarge"
android:textSize="14sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/authorAvatar"
tools:text="@tools:sample/lorem/random[10]" />
<View
android:layout_width="match_parent"
android:layout_height="1px"
android:layout_marginTop="16dp"
android:layout_marginBottom="8dp"
android:background="?attr/separator_color"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/commentContent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -9,7 +9,7 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scrollbars="vertical"
tools:listitem="@layout/list_comments_item" />
tools:listitem="@layout/list_comment_item" />
<ProgressBar
android:id="@+id/loading_progress_bar"

View File

@ -15,10 +15,8 @@
android:layout_width="42dp"
android:layout_height="42dp"
android:layout_alignParentStart="true"
android:layout_alignParentLeft="true"
android:layout_alignParentTop="true"
android:layout_marginLeft="3dp"
android:layout_marginRight="@dimen/comment_item_avatar_right_margin"
android:layout_marginEnd="@dimen/comment_item_avatar_right_margin"
android:focusable="false"
android:src="@drawable/placeholder_person"
app:shapeAppearance="@style/CircularImageView"
@ -29,83 +27,78 @@
android:layout_width="@dimen/video_item_detail_pinned_image_width"
android:layout_height="@dimen/video_item_detail_pinned_image_height"
android:layout_alignParentTop="true"
android:layout_marginRight="@dimen/video_item_detail_pinned_right_margin"
android:layout_marginEnd="@dimen/video_item_detail_pinned_right_margin"
android:layout_toEndOf="@+id/itemThumbnailView"
android:contentDescription="@string/detail_pinned_comment_view_description"
android:src="@drawable/ic_pin"
android:visibility="gone"
tools:visibility="visible" />
android:src="@drawable/ic_pin" />
<org.schabi.newpipe.views.NewPipeTextView
android:id="@+id/itemTitleView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:layout_marginBottom="@dimen/video_item_search_image_right_margin"
android:layout_toEndOf="@+id/detail_pinned_view"
android:ellipsize="end"
android:lines="1"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textSize="@dimen/comment_item_title_text_size"
tools:text="Author Name, Lorem ipsum" />
tools:text="Author Name, Lorem ipsum • 5 months ago" />
<org.schabi.newpipe.views.NewPipeTextView
android:id="@+id/itemCommentContentView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/itemTitleView"
android:layout_marginBottom="@dimen/channel_item_description_to_details_margin"
android:layout_marginTop="6dp"
android:layout_toEndOf="@+id/itemThumbnailView"
android:layout_toRightOf="@+id/itemThumbnailView"
android:textAppearance="?android:attr/textAppearanceLarge"
android:textSize="@dimen/comment_item_content_text_size"
tools:text="Comment Content, Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc tristique vitae sem vitae blanditLorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc tristique vitae sem vitae blanditLorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc tristique vitae sem vitae blandit" />
tools:text="@tools:sample/lorem/random[1]" />
<ImageView
android:id="@+id/detail_thumbs_up_img_view"
android:layout_width="@dimen/video_item_detail_like_image_width"
android:layout_height="@dimen/video_item_detail_like_image_height"
android:layout_below="@id/itemCommentContentView"
android:layout_toRightOf="@+id/itemThumbnailView"
android:layout_alignBottom="@+id/replies_button"
android:layout_toEndOf="@+id/itemThumbnailView"
android:contentDescription="@string/detail_likes_img_view_description"
android:src="@drawable/ic_thumb_up" />
<org.schabi.newpipe.views.NewPipeTextView
android:id="@+id/detail_thumbs_up_count_view"
android:layout_width="wrap_content"
android:layout_height="@dimen/video_item_detail_like_image_height"
android:layout_below="@id/itemCommentContentView"
android:layout_marginLeft="@dimen/video_item_detail_like_margin"
android:layout_toRightOf="@id/detail_thumbs_up_img_view"
android:layout_height="wrap_content"
android:layout_alignTop="@id/detail_thumbs_up_img_view"
android:layout_alignBottom="@id/detail_thumbs_up_img_view"
android:layout_marginStart="@dimen/video_item_detail_like_margin"
android:layout_toEndOf="@id/detail_thumbs_up_img_view"
android:gravity="center"
android:lines="1"
android:textAppearance="?android:attr/textAppearanceMedium"
android:textSize="@dimen/video_item_detail_likes_text_size"
tools:ignore="RtlHardcoded"
tools:text="12M" />
<ImageView
android:id="@+id/detail_heart_image_view"
android:layout_width="@dimen/video_item_detail_heart_image_size"
android:layout_height="@dimen/video_item_detail_heart_image_size"
android:layout_below="@id/itemCommentContentView"
android:layout_marginLeft="@dimen/video_item_detail_heart_margin"
android:layout_toRightOf="@+id/detail_thumbs_up_count_view"
android:layout_alignTop="@id/detail_thumbs_up_img_view"
android:layout_alignBottom="@id/detail_thumbs_up_img_view"
android:layout_marginStart="@dimen/video_item_detail_heart_margin"
android:layout_toEndOf="@+id/detail_thumbs_up_count_view"
android:contentDescription="@string/detail_heart_img_view_description"
android:src="@drawable/ic_heart"
android:visibility="gone"
tools:visibility="visible" />
android:src="@drawable/ic_heart" />
<org.schabi.newpipe.views.NewPipeTextView
android:id="@+id/itemPublishedTime"
<Button
android:id="@+id/replies_button"
style="?android:attr/borderlessButtonStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/itemCommentContentView"
android:layout_marginLeft="12dp"
android:layout_toRightOf="@id/detail_heart_image_view"
android:lines="1"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textSize="@dimen/video_item_search_upload_date_text_size"
tools:text="1 year ago" />
android:layout_alignParentEnd="true"
android:layout_marginStart="@dimen/video_item_detail_heart_margin"
android:minHeight="0dp"
tools:text="543 replies" />
</RelativeLayout>

View File

@ -1,69 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/itemRoot"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackground"
android:clickable="true"
android:focusable="true"
android:padding="@dimen/video_item_search_padding">
<com.google.android.material.imageview.ShapeableImageView
android:id="@+id/itemThumbnailView"
android:layout_width="42dp"
android:layout_height="42dp"
android:layout_centerVertical="true"
android:layout_marginStart="3dp"
android:layout_marginRight="15dp"
android:src="@drawable/placeholder_person"
app:shapeAppearance="@style/CircularImageView"
tools:ignore="RtlHardcoded" />
<org.schabi.newpipe.views.NewPipeTextView
android:id="@+id/itemCommentContentView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="@dimen/channel_item_description_to_details_margin"
android:layout_toRightOf="@+id/itemThumbnailView"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textSize="@dimen/comment_item_content_text_size"
tools:text="Channel description, Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc tristique vitae sem vitae blanditLorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc tristique vitae sem vitae blanditLorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc tristique vitae sem vitae blandit" />
<ImageView
android:id="@+id/detail_thumbs_up_img_view"
android:layout_width="@dimen/video_item_detail_like_image_width"
android:layout_height="@dimen/video_item_detail_like_image_height"
android:layout_below="@id/itemCommentContentView"
android:layout_toRightOf="@+id/itemThumbnailView"
android:contentDescription="@string/detail_likes_img_view_description"
android:src="@drawable/ic_thumb_up" />
<org.schabi.newpipe.views.NewPipeTextView
android:id="@+id/detail_thumbs_up_count_view"
android:layout_width="wrap_content"
android:layout_height="@dimen/video_item_detail_like_image_height"
android:layout_below="@id/itemCommentContentView"
android:layout_marginLeft="@dimen/video_item_detail_like_margin"
android:layout_toRightOf="@id/detail_thumbs_up_img_view"
android:lines="1"
android:textAppearance="?android:attr/textAppearanceMedium"
android:textSize="@dimen/video_item_detail_likes_text_size"
tools:ignore="RtlHardcoded"
tools:text="12M" />
<org.schabi.newpipe.views.NewPipeTextView
android:id="@+id/itemPublishedTime"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/itemCommentContentView"
android:layout_marginLeft="12dp"
android:layout_toRightOf="@id/detail_thumbs_up_count_view"
android:lines="1"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textSize="@dimen/video_item_search_upload_date_text_size"
tools:text="1 year ago" />
</RelativeLayout>

View File

@ -80,10 +80,32 @@
tools:text="234 videos" />
</RelativeLayout>
<org.schabi.newpipe.views.NewPipeTextView
android:id="@+id/playlist_description"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/playlist_meta"
android:paddingHorizontal="@dimen/video_item_search_padding"
android:paddingTop="6dp"
android:maxLines="5"
tools:text="This is a multiline playlist description. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc tristique vitae sem vitae blandit Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc tristique vitae sem vitae blandit. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc tristique vitae sem vitae blandit. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc tristique vitae sem vitae blandit" />
<org.schabi.newpipe.views.NewPipeTextView
android:id="@+id/playlist_description_read_more"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/playlist_description"
android:gravity="end"
android:paddingHorizontal="@dimen/video_item_search_padding"
android:paddingTop="6dp"
android:text="@string/show_more"
android:layout_marginBottom="6dp"
android:textColor="?attr/colorPrimary" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/playlist_meta">
android:layout_below="@id/playlist_description_read_more">
<include
android:id="@+id/playlist_control"

View File

@ -1,5 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
@ -8,64 +7,64 @@
android:paddingTop="16dp">
<org.schabi.newpipe.views.NewPipeTextView
android:id="@+id/textView"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:clickable="false"
android:focusable="false"
android:gravity="center"
android:text="@string/notification_actions_summary"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
android:id="@+id/summary"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:clickable="false"
android:focusable="false"
android:gravity="center"
android:text="@string/notification_actions_summary"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<include
android:id="@+id/notificationAction0"
layout="@layout/settings_notification_action"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/textView" />
<include
android:id="@+id/notificationAction0"
layout="@layout/settings_notification_action"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/summary" />
<include
android:id="@+id/notificationAction1"
layout="@layout/settings_notification_action"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/notificationAction0" />
<include
android:id="@+id/notificationAction1"
layout="@layout/settings_notification_action"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/notificationAction0" />
<include
android:id="@+id/notificationAction2"
layout="@layout/settings_notification_action"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/notificationAction1" />
<include
android:id="@+id/notificationAction2"
layout="@layout/settings_notification_action"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/notificationAction1" />
<include
android:id="@+id/notificationAction3"
layout="@layout/settings_notification_action"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/notificationAction2" />
<include
android:id="@+id/notificationAction3"
layout="@layout/settings_notification_action"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/notificationAction2" />
<include
android:id="@+id/notificationAction4"
layout="@layout/settings_notification_action"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/notificationAction3" />
<include
android:id="@+id/notificationAction4"
layout="@layout/settings_notification_action"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/notificationAction3" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

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

View File

@ -5,13 +5,13 @@
<string name="settings">الإعدادات</string>
<string name="search">بحث</string>
<string name="controls_download_desc">تنزيل ملف البث</string>
<string name="download">تحميل</string>
<string name="download">تنزيل</string>
<string name="share">إعادة النشر</string>
<string name="open_in_popup_mode">فتح في نافدة منبثقة</string>
<string name="open_in_browser">افتح في المتصفح</string>
<string name="cancel">إلغاء</string>
<string name="install">تثبيت</string>
<string name="no_player_found_toast">لم يتم العثور على مشغل بث (يمكنك تثبيت VLC لتشغيله).</string>
<string name="no_player_found_toast">لم يتم العثور على مشغل بث (يمكنك تثبيت VLC لتشغيلها).</string>
<string name="no_player_found">لم يتم العثور على مشغل بث. يرجى تثبيت VLC؟</string>
<string name="upload_date_text">تم النشر في %1$s</string>
<string name="main_bg_subtitle">اضغط على عدسة المكبرة للبدء.</string>

View File

@ -584,7 +584,7 @@
<string name="notification_action_shuffle">خلط</string>
<string name="notification_action_repeat">تكرار</string>
<string name="notification_actions_at_most_three">يمكنك تحديد ثلاثة إجراءات كحد أقصى لإظهارها في الإشعار المضغوط!</string>
<string name="notification_actions_summary">قم بتحرير كل إشعار أدناه من خلال النقر عليه. حدد ما يصل إلى ثلاثة منها لتظهر في الإشعار المضغوط باستخدام مربعات الاختيار الموجودة على اليمين</string>
<string name="notification_actions_summary">قم بتحرير كل إجراء إعلام أدناه من خلال النقر عليه. حدد ما يصل إلى ثلاثة منها ليتم عرضها في الإشعار المضغوط باستخدام مربعات الاختيار الموجودة على اليمين.</string>
<string name="notification_action_4_title">زر الإجراء الخامس</string>
<string name="notification_action_3_title">زر الإجراء الرابع</string>
<string name="notification_action_2_title">زر الإجراء الثالث</string>
@ -858,4 +858,15 @@
<string name="share_playlist">مشاركة قائمة التشغيل</string>
<string name="share_playlist_with_titles_message">شارك تفاصيل قائمة التشغيل مثل اسم قائمة التشغيل وعناوين الفيديو أو كقائمة بسيطة من عناوين URL للفيديو</string>
<string name="video_details_list_item">- %1$s: %2$s</string>
<plurals name="replies">
<item quantity="zero">رد %s</item>
<item quantity="one">رد %s</item>
<item quantity="two">ردان%s</item>
<item quantity="few">ردود%s</item>
<item quantity="many">ردود %s</item>
<item quantity="other">ردود %s</item>
</plurals>
<string name="show_more">عرض المزيد</string>
<string name="show_less">عرض أقل</string>
<string name="notification_actions_summary_android13">قم بتحرير كل إجراء إعلام أدناه من خلال النقر عليه. يتم تعيين الإجراءات الثلاثة الأولى (تشغيل/إيقاف مؤقت، السابق والتالي) بواسطة النظام ولا يمكن تخصيصها.</string>
</resources>

View File

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

View File

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

View File

@ -694,7 +694,7 @@
<string name="youtube_music_premium_content">Bu video yalnız YouTube Music Premium üzvləri üçün əlçatandır, ona görə də NewPipe tərəfindən yayımlamaq və ya endirmək mümkün deyil.</string>
<string name="description_select_note">İndi açıqlamadakı mətni seçə bilərsiniz. Nəzərə alın ki, seçim rejimində səhifə titrəyə və linklər kliklənməyə bilər.</string>
<string name="notification_scale_to_square_image_summary">Bildirişdə göstərilən video miniatürünü 16:9-dan 1:1 görünüş nisbətinə qədər kəs</string>
<string name="notification_actions_summary">Aşağıdakı hər bir bildiriş fəaliyyətini üzərinə toxunaraq redaktə et. Sağdakı təsdiq qutularından istifadə edərək yığcam bildirişdə göstərmək üçün onların üçünü seç</string>
<string name="notification_actions_summary">Aşağıdakı hər bir bildiriş fəaliyyətini üzərinə toxunaraq düzəliş edin. Sağdakı təsdiq qutularından istifadə edərək yığcam bildirişdə göstərmək üçün onların üçünü seçin.</string>
<string name="invalid_source">Belə fayl/məzmun mənbəyi yoxdur</string>
<string name="selected_stream_external_player_not_supported">Seçilən yayım xarici oynadıcılar tərəfindən dəstəklənmir</string>
<string name="streams_not_yet_supported_removed">Yükləyici tərəfindən hələ dəstəklənməyən yayımlar göstərilmir</string>
@ -769,4 +769,5 @@
<string name="feed_fetch_channel_tabs_summary">Axın yenilənərkən əldə edilən səhifələr.Kanal sürətli rejim istifadə edərək yenilənirsə, bu seçimin heç bir təsiri yoxdur.</string>
<string name="metadata_uploader_avatars">Yükləyici avatarları</string>
<string name="metadata_thumbnails">Miniatürlər</string>
<string name="notification_actions_summary_android13">Aşağıdakı hər bildirişə vuraraq ona düzəliş edin. İlk üç əməl (oynatma/fasilə, əvvəlki və sonrakı) sistem tərəfindən təyin olunub və dəyişdirilə bilməz.</string>
</resources>

View File

@ -58,7 +58,7 @@
<string name="play_with_kodi_title">Kodi bilan ijro etish</string>
<string name="show_higher_resolutions_summary">Faqat ba\'zi qurilmalar 2K / 4K videolarni ijro etishi mumkin</string>
<string name="show_higher_resolutions_title">Yuqori o\'lchamlarni ko\'rsatish</string>
<string name="default_popup_resolution_title">"Standart pop-up o\'lchamlari"</string>
<string name="default_popup_resolution_title">Standart pop-up o\'lchamlari</string>
<string name="default_resolution_title">Standart o\'lchamlari</string>
<string name="download_path_audio_dialog_title">Audio fayllar uchun yuklab olish papkasini tanlash</string>
<string name="download_path_summary">Yuklab olingan videofayllar shu yerda saqlanadi</string>

View File

@ -6,8 +6,8 @@
<string name="no_player_found_toast">Патокавы плэер не знойдзены (вы можаце ўсталяваць VLC каб прайграць).</string>
<string name="install">Усталяваць</string>
<string name="cancel">Скасаваць</string>
<string name="open_in_browser">Адкрыць у браўзеры</string>
<string name="open_in_popup_mode">Адкрыць у асобным акне</string>
<string name="open_in_browser">Адкрыць ў браўзеры</string>
<string name="open_in_popup_mode">Адкрыць ў асобным акне</string>
<string name="share">Падзяліцца</string>
<string name="download">Спампаваць</string>
<string name="controls_download_desc">Загрузка файла прамой трансляцыі</string>
@ -37,12 +37,12 @@
<string name="download_path_audio_summary">Загружаныя аўдыёфайлы захоўваюцца тут</string>
<string name="download_path_audio_dialog_title">Абярыце тэчку загрузкі для аўдыёфайлаў</string>
<string name="default_resolution_title">Разрознянне па змаўчанні</string>
<string name="default_popup_resolution_title">Разрозненне усплываючага акна</string>
<string name="default_popup_resolution_title">Разрозненне ўсплываючага акна</string>
<string name="show_higher_resolutions_title">Высокія разрозненні</string>
<string name="show_higher_resolutions_summary">Толькі некаторыя прылады могуць прайграваць відэа ў 2K/4K</string>
<string name="play_with_kodi_title">Прайграць у Kodi</string>
<string name="kore_not_found">Усталяваць адсутную праграму Kore\?</string>
<string name="show_play_with_kodi_title">Паказаць опцыю \"Прайграць у Kodi\"</string>
<string name="play_with_kodi_title">Прайграць ў Kodi</string>
<string name="kore_not_found">Ўсталяваць адсутную праграму Kore?</string>
<string name="show_play_with_kodi_title">Паказаць опцыю \"Прайграць ў Kodi\"</string>
<string name="show_play_with_kodi_summary">Паказаць опцыю прайгравання відэа праз медыяцэнтр Kodi</string>
<string name="play_audio">Аўдыё</string>
<string name="default_audio_format_title">Фармат аўдыё па змаўчанні</string>
@ -70,7 +70,7 @@
<string name="resume_on_audio_focus_gain_title">Узнавіць прайграванне</string>
<string name="resume_on_audio_focus_gain_summary">Працягваць прайграванне пасля перапынкаў (напрыклад, тэлефонных званкоў)</string>
<string name="download_dialog_title">Загрузіць</string>
<string name="show_next_and_similar_title">\"Наступнае\" и \"Прапанаванае\" відэа</string>
<string name="show_next_and_similar_title">\"Наступнае\" і \"Прапанаванае\" відэа</string>
<string name="show_hold_to_append_title">Паказаць падказку \"Утрымлівайце, каб паставіць у чаргу\"</string>
<string name="show_hold_to_append_summary">Паказаць падказку пры націсканні фонавай або ўсплывальнай кнопкі ў відэа \"Падрабязнасці:\"</string>
<string name="unsupported_url">URL не падтрымліваецца</string>
@ -227,7 +227,7 @@
\nПалітыка прыватнасці NewPipe падрабязна тлумачыць, якія дадзеныя адпраўляюцца і захоўваюцца пры адпраўцы справаздачы аб збоях.</string>
<string name="read_privacy_policy">Прачытаць палітыку</string>
<string name="app_license_title">Ліцэнзія NewPipe</string>
<string name="app_license">NewPipe - гэта праграмнае забеспячэнне, свабоднае ад копілефта: вы можаце выкарыстоўваць, вывучаць, дзяліцца і паляпшаць яго па жаданні. У прыватнасці, вы можаце распаўсюджваць і/ці змяняць яго ў адпаведнасці з умовамі Агульнай грамадскай ліцэнзіі GNU, апублікаванай Фондам свабоднага праграмнага забеспячэння, альбо версіі 3 Ліцэнзіі, альбо (на ваш выбар) любой пазнейшай версіі.</string>
<string name="app_license">NewPipe - гэта праграмнае забеспячэнне, свабоднае ад копілефта: вы можаце выкарыстоўваць, вывучаць, дзяліцца і паляпшаць яго па жаданні. Ў прыватнасці, вы можаце распаўсюджваць і/ці змяняць яго ў адпаведнасці з умовамі Агульнай грамадскай ліцэнзіі GNU, апублікаванай Фондам свабоднага праграмнага забеспячэння, альбо версіі 3 Ліцэнзіі, альбо (на ваш выбар) любой пазнейшай версіі.</string>
<string name="read_full_license">Прачытаць ліцэнзію</string>
<string name="title_activity_history">Гісторыя</string>
<string name="action_history">Гісторыя</string>
@ -253,8 +253,8 @@
<string name="play_queue_remove">Выдаліць</string>
<string name="play_queue_stream_detail">Падрабязнасці</string>
<string name="play_queue_audio_settings">Налады аўдыё</string>
<string name="hold_to_append">Утрымлівайце, каб дадаць у чаргу</string>
<string name="start_here_on_background">Пачаць адсюль у фоне</string>
<string name="hold_to_append">Утрымлівайце, каб дадаць ў чаргу</string>
<string name="start_here_on_background">Пачаць адсюль ў фоне</string>
<string name="start_here_on_popup">Пачніце гуляць ва ўсплываючым акне</string>
<string name="drawer_open">Адкрыць бакавую панэль</string>
<string name="drawer_close">Зачыніць бакавую панэль</string>
@ -262,16 +262,16 @@
<string name="preferred_open_action_settings_summary">Пры адкрыцці спасылкі на кантэнт — %s</string>
<string name="video_player">Відэаплэер</string>
<string name="background_player">Фонавы плэер</string>
<string name="popup_player">Плэер у акне</string>
<string name="popup_player">Аконны прайгравальнік</string>
<string name="always_ask_open_action">Заўсёды пытацца</string>
<string name="preferred_player_fetcher_notification_title">Атрыманне звестак…</string>
<string name="preferred_player_fetcher_notification_message">Загрузка запытанага кантэнту</string>
<string name="create_playlist">Стварыць плэйліст</string>
<string name="rename_playlist">Перайменаваць</string>
<string name="name">Імя</string>
<string name="add_to_playlist">Дадаць у плэйліст</string>
<string name="set_as_playlist_thumbnail">Усталяваць як мініяцюру плэйліста</string>
<string name="bookmark_playlist">Дадаць плэйліст у закладкі</string>
<string name="add_to_playlist">Дадаць ў плэйліст</string>
<string name="set_as_playlist_thumbnail">Ўсталяваць як мініяцюру плэйліста</string>
<string name="bookmark_playlist">Дадаць плэйліст ў закладкі</string>
<string name="unbookmark_playlist">Выдаліць закладку</string>
<string name="delete_playlist_prompt">Выдаліць плэйліст\?</string>
<string name="playlist_creation_success">Плэйліст створаны</string>
@ -289,7 +289,7 @@
<string name="enable_disposed_exceptions_summary">Прымусова паведамляць пра недастаўляемыя Rx-выключэнні па-за фрагментам або жыццёвым цыкле пасля выдалення</string>
<string name="import_title">Імпарт</string>
<string name="import_from">Імпарт з</string>
<string name="export_to">Экспарт у</string>
<string name="export_to">Экспарт ў</string>
<string name="import_ongoing">Імпарт…</string>
<string name="export_ongoing">Экспарт…</string>
<string name="import_file_title">Імпарт файла</string>
@ -299,17 +299,17 @@
<string name="import_youtube_instructions">Імпарт падпісак YouTube з Google Takeout:
\n
\n1. Перайдзіце па гэтым URL: %1$s
\n2. Увайдзіце, калі вас папросяць
\n2. Ўвайдзіце, калі вас папросяць
\n3. Націсніце на «Усе дадзеныя ўключаны», затым на «Адмяніць выбар усіх», затым выберыце толькі «падпіскі» і націсніце «ОК»
\n4. Націсніце на «Наступны крок», а затым на «Стварыць экспарт»
\n5. Націсніце на кнопку «Спампаваць» пасля таго, як яна з\'явіцца
\n6. Пстрыкніце ФАЙЛ ІМПАРТУВАЦЬ ніжэй і выберыце спампаваны файл .zip
\n7. [Калі імпарт .zip не ўдаецца] Распакуйце файл .csv (звычайна ў раздзеле \"YouTube і YouTube Music/subscriptions/subscriptions.csv\"), націсніце ФАЙЛ ІМПАРТУВАЦЬ ніжэй і выберыце выняты файл CSV</string>
<string name="import_soundcloud_instructions">Імпарт падпісак з SoundCloud набраўшы альбо URL, альбо ваш ID:
\n
\n1. Уключыце \"рэжым працоўнага стала\" у браўзэры (сайт недаступны на тэлефоне)
\n2. Перайдзіце на: %1$s
\n3. Увайдзіце, калі неабходна
<string name="import_soundcloud_instructions">Імпарт падпісак з SoundCloud набраўшы альбо URL, альбо ваш ID:
\n
\n1. Ўключыце \"рэжым працоўнага стала\" ў браўзэры (сайт недаступны на тэлефоне)
\n2. Перайдзіце на: %1$s
\n3. Увайдзіце, калі неабходна
\n4. Скапіруйце адрас з адраснага радка.</string>
<string name="import_soundcloud_instructions_hint">вашID, soundcloud.com/вашID</string>
<string name="import_network_expensive_warning">Гэтае дзеянне можа выклікаць вялікі расход трафіку.
@ -322,7 +322,7 @@
<string name="skip_silence_checkbox">Прапускаць цішыню</string>
<string name="playback_step">Крок</string>
<string name="playback_reset">Скід</string>
<string name="start_accept_privacy_policy">У адпаведнасці з Агульным рэгламентам па абароне дадзеных ЕС (GDPR), звяртаем вашу ўвагу на палітыку прыватнасці NewPipe. Калі ласка, уважліва азнаёмцеся з ёй.
<string name="start_accept_privacy_policy">Ў адпаведнасці з Агульным рэгламентам па абароне дадзеных ЕС (GDPR), звяртаем вашу ўвагу на палітыку прыватнасці NewPipe. Калі ласка, уважліва азнаёмцеся з ёй.
\nВам неабходна прыняць яе ўмовы, каб адправіць нам справаздачу пра памылку.</string>
<string name="accept">Прыняць</string>
<string name="decline">Адмовіцца</string>
@ -331,8 +331,8 @@
<string name="minimize_on_exit_title">Пры згортванні плэера</string>
<string name="minimize_on_exit_summary">Дзеянне пры пераключэнні са стандартнага плэера на іншае прыкладанне — %s</string>
<string name="minimize_on_exit_none_description">Нічога не рабіць</string>
<string name="minimize_on_exit_background_description">Згарнуць у фонавы плэер</string>
<string name="minimize_on_exit_popup_description">Плэер у акне</string>
<string name="minimize_on_exit_background_description">Згарнуць ў фонавы плэер</string>
<string name="minimize_on_exit_popup_description">Плэер ў акне</string>
<string name="unsubscribe">Адпісацца</string>
<string name="tab_choose">Абярыце ўкладку</string>
<string name="settings_category_updates_title">Абнаўленні</string>
@ -356,14 +356,14 @@
<string name="missions_header_finished">Скончана</string>
<string name="missions_header_pending">У чарзе</string>
<string name="paused">прыпынена</string>
<string name="queued">у чарзе</string>
<string name="queued">дададзены ў чаргу</string>
<string name="post_processing">постапрацоўка</string>
<string name="enqueue">Паставіць у чаргу</string>
<string name="enqueue">Дадаць ў чаргу</string>
<string name="permission_denied">Дзеянне забаронена сістэмай</string>
<string name="download_failed">Памылка загрузкі</string>
<string name="generate_unique_name">Стварыць унікальнае імя</string>
<string name="overwrite">Перазапісаць</string>
<string name="download_already_running">Загрузка з такім імем ужо выконваецца</string>
<string name="download_already_running">Загрузка з такім імем ўжо выконваецца</string>
<string name="show_error">Паказаць тэкст памылкі</string>
<string name="error_path_creation">Немагчыма стварыць папку прызначэння</string>
<string name="error_file_creation">Немагчыма стварыць файл</string>
@ -377,7 +377,7 @@
<string name="stop">Спыніць</string>
<string name="max_retry_msg">Максімум спробаў</string>
<string name="max_retry_desc">Колькасць спробаў перад адменай загрузкі</string>
<string name="pause_downloads_on_mobile">Перапыніць у платных сетках</string>
<string name="pause_downloads_on_mobile">Перапыніць ў платных сетках</string>
<string name="pause_downloads_on_mobile_desc">Карысна пры пераключэнні на мабільную сетку, хоць некаторыя загрузкі не могуць быць прыпыненыя</string>
<string name="events">Падзеі</string>
<string name="conferences">Канферэнцыі</string>
@ -394,10 +394,10 @@
<string name="settings_category_clear_data_title">Ачысціць дадзеныя</string>
<string name="watch_history_states_deleted">Пазіцыі прайгравання выдалены</string>
<string name="missing_file">Файл перамешчаны ці выдалены</string>
<string name="overwrite_unrelated_warning">Файл з такім імем ужо існуе</string>
<string name="overwrite_finished_warning">Файл з такім імем ужо існуе</string>
<string name="overwrite_unrelated_warning">Файл з такім імем ўжо існуе</string>
<string name="overwrite_finished_warning">Файл з такім імем ўжо існуе</string>
<string name="overwrite_failed">немагчыма перазапісаць файл</string>
<string name="download_already_pending">У чарзе ўжо ёсць загрузка з такім імем</string>
<string name="download_already_pending">Ў чарзе ўжо ёсць загрузка з такім імем</string>
<string name="error_postprocessing_stopped">NewPipe была зачынена падчас працы над файлам</string>
<string name="error_insufficient_storage">Скончылася вольнае месца на прыладзе</string>
<string name="error_progress_lost">Прагрэс страчаны, так як файл быў выдалены</string>
@ -408,8 +408,8 @@
<string name="start_downloads">Пачаць загрузку</string>
<string name="pause_downloads">Прыпыніць загрузку</string>
<string name="downloads_storage_ask_title">Запытваць тэчку загрузкі</string>
<string name="downloads_storage_ask_summary">Вам будзе прапанавана ўказаць месца захавання кожнай загрузкі.
\nУключыце сістэмны выбарнік тэчкі (SAF), калі вы хочаце загружаць файлы на знешнюю SD-картку</string>
<string name="downloads_storage_ask_summary">Вам будзе прапанавана указаць месца захавання кожнай загрузкі.
\nЎключыце сістэмны выбарнік тэчкі (SAF), калі вы хочаце загружаць файлы на знешнюю SD-картку</string>
<string name="downloads_storage_use_saf_title">Выкарыстоўвайце сродак выбару сістэмных тэчак (SAF)</string>
<string name="downloads_storage_use_saf_summary">\'Storage Access Framework\' дазваляе загружаць на знешнюю SD-картку</string>
<string name="drawer_header_description">Пераключыць службу, выбраную ў дадзены момант:</string>
@ -426,7 +426,7 @@
<string name="notification_action_1_title">Кнопка другога дзеяння</string>
<string name="notification_action_0_title">Кнопка першага дзеяння</string>
<string name="feed_groups_header_title">Групы каналаў</string>
<string name="systems_language">Як у сістэме</string>
<string name="systems_language">Як ў сістэме</string>
<string name="app_language_title">Мова прылады</string>
<string name="choose_instance_prompt">Выберыце экзэмпляр</string>
<string name="delete_downloaded_files">Выдаліць загружаныя файлы</string>
@ -442,15 +442,15 @@
<string name="never">Ніколі</string>
<string name="wifi_only">Толькі па Wi-Fi</string>
<string name="show_original_time_ago_title">Паказаць арыгінальны час на элементах</string>
<string name="unmute">Уключыць гук</string>
<string name="unmute">Ўключыць гук</string>
<string name="mute">Цішына</string>
<string name="enqueue_stream">Дадаць у чаргу</string>
<string name="enqueued">Даданае у чаргу</string>
<string name="enqueue_stream">Дадаць ў чаргу</string>
<string name="enqueued">Даданае ў чаргу</string>
<string name="title_activity_play_queue">Чарга прайгравання</string>
<string name="most_liked">Найбольш папулярнае</string>
<string name="local">Лакальнае</string>
<string name="recently_added">Нядаўна дададзенае</string>
<string name="no_playlist_bookmarked_yet">Няма закладак у плейлісце</string>
<string name="no_playlist_bookmarked_yet">Няма закладак ў плейлісце</string>
<string name="select_a_playlist">Выберыце плэйліст</string>
<string name="default_kiosk_page_summary">Кіёск па змаўчанні</string>
<string name="done">Так</string>
@ -478,7 +478,7 @@
<string name="notification_action_4_title">Кнопка пятага дзеяння</string>
<string name="notification_colorize_summary">Афарбоўваць апавяшчэнне асноўным колерам мініяцюры. Падтрымваецца не ўсімі прыладамі</string>
<string name="notification_actions_at_most_three">У кампактным апавяшчэнні дасяжна не больш за тры дзеянні!</string>
<string name="notification_actions_summary">Дзеянні можна змяніць, націснуўшы на іх. Адзначце не больш за трох для адлюстравання ў кампактным апавяшчэнні</string>
<string name="notification_actions_summary">Адрэдагуйце кожнае дзеянне апавяшчэння, націснуўшы на яго. Выберыце да трох з іх, якія будуць адлюстроўвацца ў кампактным апавяшчэнні, выкарыстоўваючы сцяжкі справа.</string>
<string name="unsupported_url_dialog_message">Не ўдалося распазнаць URL-адрас. Адкрыць у іншай праграме\?</string>
<string name="settings_category_player_notification_title">Апавяшченне плэера</string>
<string name="notifications">Апавяшчэнні</string>
@ -497,9 +497,9 @@
<string name="feed_group_dialog_empty_selection">Падпіскі не выбраны</string>
<string name="feed_oldest_subscription_update">Апошняе абнаўленне: %s</string>
<string name="auto_device_theme_title">Аўтаматычна (тэма прылады)</string>
<string name="night_theme_summary">Выберыце ўлюбёную начную тэму - %s</string>
<string name="night_theme_summary">Выберыце любімую начную тэму - %s</string>
<string name="description_select_enable">Дазвол вылучэння тэксту ў апісанні</string>
<string name="select_night_theme_toast">Ніжэй вы можаце абраць улюбёную начную тэму</string>
<string name="select_night_theme_toast">Вы можаце выбраць сваю любімую начную тэму ніжэй</string>
<string name="night_theme_available">Гэта опцыя даступна толькі тады, калі %s будзе выбранай тэмаю</string>
<string name="download_has_started">Загрузка пачалась</string>
<string name="notifications_disabled">Апавяшчэнні адключаныя</string>
@ -519,7 +519,7 @@
<string name="open_with">Адкрыць з дапамогай</string>
<string name="night_theme_title">Начная тэма</string>
<string name="open_website_license">Адкрыць вэб-сайт</string>
<string name="description_select_note">Цяпер Вы можаце вылучаць тэкст у апісанні. Звярніце ўвагу, што ў рэжыме вылучэння старонка можа мігацець, а спасылкі могуць быць недаступныя для націскання.</string>
<string name="description_select_note">Цяпер Вы можаце вылучаць тэкст ў апісанні. Звярніце ўвагу, што ў рэжыме вылучэння старонка можа мігацець, а спасылкі могуць быць недаступныя для націскання.</string>
<string name="start_main_player_fullscreen_title">Запусціць галоўны прайгравальнік у поўнаэкранным рэжыме</string>
<string name="show_channel_details">Паказаць дэталі канала</string>
<string name="low_quality_smaller">Нізкая якасць (менш)</string>
@ -574,14 +574,14 @@
<item quantity="many">Выдалена %1$s зазагрузак</item>
<item quantity="other">Выдалена %1$s зазагрузак</item>
</plurals>
<string name="delete_downloaded_files_confirm">Выдаліць усе загружаныя файлы з дыска\?</string>
<string name="delete_downloaded_files_confirm">Выдаліць ўсе загружаныя файлы з дыска?</string>
<plurals name="minutes">
<item quantity="one">%d хвіліна</item>
<item quantity="few">%d хвіліны</item>
<item quantity="many">%d хвілінаў</item>
<item quantity="other">%d хвілінаў</item>
</plurals>
<string name="progressive_load_interval_summary">Змяніць памер інтэрвалу загрузкі прагрэсіўнага змесціва (у цяперашні час %s). Меншае значэнне можа паскорыць іх першапачатковую загрузку</string>
<string name="progressive_load_interval_summary">Змяніць памер інтэрвалу загрузкі прагрэсіўнага змесціва (ў цяперашні час %s). Меншае значэнне можа паскорыць іх першапачатковую загрузку</string>
<string name="show_description_summary">Выключыце, каб схаваць апісанне відэа і дадатковую інфармацыю</string>
<string name="local_search_suggestions">Прапановы лакальнага пошуку</string>
<string name="settings_category_player_notification_summary">Наладзіць апавяшчэнне аб бягучым прайграванні патоку</string>
@ -608,7 +608,7 @@
<string name="msg_calculating_hash">Разлік хэша</string>
<string name="recaptcha_solve">Вырашана</string>
<string name="playlist_no_uploader">Створана аўтаматычна (запампавальнік не знойдзены)</string>
<string name="duplicate_in_playlist">Плэйлісты, якія пазначаны шэрым, ужо ўтрымліваюць гэты элемент.</string>
<string name="duplicate_in_playlist">Плэйлісты, якія пазначаны шэрым, ўжо ўтрымліваюць гэты элемент.</string>
<plurals name="new_streams">
<item quantity="one">%s новы стрым</item>
<item quantity="few">%s новыя стрымы</item>
@ -616,8 +616,8 @@
<item quantity="other">%s новых стрымаў</item>
</plurals>
<string name="comments_tab_description">Каментарыі</string>
<string name="enqueue_next_stream">У чаргу далей</string>
<string name="enqueued_next">У чарзе наступны</string>
<string name="enqueue_next_stream">Ў чаргу далей</string>
<string name="enqueued_next">Ў чарзе наступны</string>
<string name="loading_stream_details">Загрузка звестак аб стрыме…</string>
<string name="processing_may_take_a_moment">Апрацоўка... Можа заняць некаторы час</string>
<string name="playlist_add_stream_success_duplicate">Дублікат дададзены %d раз</string>
@ -650,7 +650,7 @@
<string name="checking_updates_toast">Праверка абнаўленняў…</string>
<string name="remove_duplicates_title">Выдаліць дублікаты\?</string>
<string name="remove_duplicates">Выдаліць дублікаты</string>
<string name="remove_duplicates_message">Вы хочаце выдаліць усе паўтаральныя стрымы ў гэтым плэйлісце\?</string>
<string name="remove_duplicates_message">Вы хочаце выдаліць ўсе паўтаральныя стрымы ў гэтым плэйлісце?</string>
<string name="feed_new_items">Новыя элементы стужкі</string>
<plurals name="feed_group_dialog_selection_count">
<item quantity="one">%d выбраны</item>
@ -687,12 +687,12 @@
<string name="settings_category_feed_title">Стужка</string>
<string name="feed_update_threshold_summary">Час пасля апошняга абнаўлення, перш чым падпіска лічыцца састарэлай — %s</string>
<string name="feed_load_error">Памылка загрузкі стужкі</string>
<string name="feed_load_error_terminated">Уліковы запіс аўтара быў спынены.
\nNewPipe не зможа загрузіць гэты канал у будучыні.
\nВы хочаце адмовіцца ад падпіскі на гэты канал\?</string>
<string name="feed_load_error_terminated">Ўліковы запіс аўтара быў спынены.
\nNewPipe не зможа загрузіць гэты канал ў будучыні.
\nВы хочаце адмовіцца ад падпіскі на гэты канал?</string>
<string name="feed_load_error_fast_unknown">Рэжым хуткай загрузкі стужкі не дае дадатковай інфармацыі аб гэтым.</string>
<string name="feed_use_dedicated_fetch_method_title">Атрымлівайце са спецыяльнага канала, калі ён даступны</string>
<string name="feed_use_dedicated_fetch_method_enable_button">Уключыць хуткі рэжым</string>
<string name="feed_use_dedicated_fetch_method_enable_button">Ўключыць хуткі рэжым</string>
<string name="metadata_category">Катэгорыя</string>
<string name="metadata_tags">Тэгі</string>
<string name="metadata_licence">Ліцэнзія</string>
@ -700,7 +700,7 @@
<string name="metadata_privacy_unlisted">Не ў спісе</string>
<string name="metadata_privacy_private">Прыватная</string>
<string name="enumeration_comma">,</string>
<string name="toggle_all">Пераключыць усё</string>
<string name="toggle_all">Пераключыць ўсё</string>
<string name="streams_not_yet_supported_removed">Стрымы, якія яшчэ не падтрымліваюцца загрузчыкам, не адлюстроўваюцца</string>
<string name="detail_sub_channel_thumbnail_view_description">Мініяцюра аватара канала</string>
<string name="video_detail_by">Аўтар: %s</string>
@ -729,7 +729,7 @@
<string name="account_terminated">Уліковы запіс спынены</string>
<string name="service_provides_reason">%s дае наступную прычыну:</string>
<string name="featured">Рэкамендаваны</string>
<string name="metadata_privacy_internal">Унутраная</string>
<string name="metadata_privacy_internal">Ўнутраная</string>
<string name="feed_show_watched">Цалкам прагледзеў</string>
<string name="paid_content">Гэты кантэнт даступны толькі для аплачаных карыстальнікаў, таму NewPipe не можа яго трансляваць або спампоўваць.</string>
<string name="feed_use_dedicated_fetch_method_summary">Даступны ў некаторых службах, звычайна нашмат хутчэй, але можа вяртаць абмежаваную колькасць элементаў і часта няпоўную інфармацыю (напрыклад, без працягласці, тыпу элемента, без актыўнага стану)</string>
@ -739,7 +739,7 @@
<string name="no_app_to_open_intent">Ніякая праграма на вашай прыладзе не можа адкрыць гэта</string>
<string name="progressive_load_interval_exoplayer_default">Стандартнае значэнне ExoPlayer</string>
<string name="feed_show_partially_watched">Часткова прагледжана</string>
<string name="feed_use_dedicated_fetch_method_help_text">Як вы думаеце, загрузка корму адбываецца занадта павольна\? Калі так, паспрабуйце ўключыць хуткую загрузку (гэта можна змяніць у наладах або націснуўшы кнопку ніжэй).
<string name="feed_use_dedicated_fetch_method_help_text">Як вы думаеце, загрузка корму адбываецца занадта павольна? Калі так, паспрабуйце ўключыць хуткую загрузку (гэта можна змяніць ў наладах або націснуўшы кнопку ніжэй).
\n
\nNewPipe прапануе дзве стратэгіі загрузкі корму:
\n• Атрыманне ўсяго канала падпіскі павольнае, але поўнае.
@ -775,8 +775,8 @@
<string name="audio_track_type_original">арыгінальны</string>
<string name="audio_track_type_dubbed">дубляваны</string>
<string name="audio_track_type_descriptive">апісальны</string>
<string name="audio_track_present_in_video">Гукавая дарожка ўжо павінна прысутнічаць у гэтай плыні</string>
<string name="use_exoplayer_decoder_fallback_summary">Уключыце гэту опцыю, калі ў вас ёсць праблемы з ініцыялізацыяй дэкодэра, якая вяртаецца да дэкодэраў з больш нізкім прыярытэтам, калі ініцыялізацыя асноўных дэкодэраў не ўдаецца. Гэта можа прывесці да нізкай прадукцыйнасці прайгравання, чым пры выкарыстанні асноўных дэкодэраў</string>
<string name="audio_track_present_in_video">Гукавая дарожка ўжо павінна прысутнічаць ў гэтай плыні</string>
<string name="use_exoplayer_decoder_fallback_summary">Ўключыце гэту опцыю, калі ў вас ёсць праблемы з ініцыялізацыяй дэкодэра, якая вяртаецца да дэкодэраў з больш нізкім прыярытэтам, калі ініцыялізацыя асноўных дэкодэраў не ўдаецца. Гэта можа прывесці да нізкай прадукцыйнасці прайгравання, чым пры выкарыстанні асноўных дэкодэраў</string>
<string name="settings_category_exoplayer_summary">Кіраванне некаторымі наладамі ExoPlayer. Каб гэтыя змены ўступілі ў сілу, патрабуецца перазапуск гульца</string>
<string name="always_use_exoplayer_set_output_surface_workaround_summary">Гэты абыходны шлях вызваляе і паўторна стварае відэакодэкі, калі адбываецца змяненне паверхні, замест таго, каб усталёўваць паверхню непасрэдна для кодэка. ExoPlayer ужо выкарыстоўваецца на некаторых прыладах з гэтай праблемай, гэты параметр мае ўплыў толькі на прыладах з Android 6 і вышэй
\n
@ -820,7 +820,7 @@
<string name="video_details_list_item">- %1$s: %2$s</string>
<string name="main_tabs_position_summary">Перамясціць селектар галоўнай укладкі ўніз</string>
<string name="no_live_streams">Няма жывых трансляцый</string>
<string name="image_quality_summary">Выберыце якасць выявы і ці спампоўваць выявы ўвогуле, каб паменшыць выкарыстанне дадзеных і памяці. Змены ачышчаюць кэш малюнкаў як у памяці, так і на дыску - %s</string>
<string name="image_quality_summary">Выберыце якасць выявы і ці спампоўваць выявы ўвогуле, каб паменшыць выкарыстанне дадзеных і памяці. Змены ачышчаюць кэш малюнкаў як ў памяці, так і на дыску - %s</string>
<string name="play">Прайграць</string>
<string name="more_options">Іншыя опцыі</string>
<string name="metadata_thumbnails">Мініяцюры</string>
@ -830,4 +830,13 @@
<string name="channel_tab_channels">Каналы</string>
<string name="previous_stream">Папярэдні стрым</string>
<string name="channel_tab_livestreams">Жывая трансляцыя</string>
<plurals name="replies">
<item quantity="one">%s адказ</item>
<item quantity="few">%s адказы</item>
<item quantity="many">%s адказаў</item>
<item quantity="other">%s адказаў</item>
</plurals>
<string name="show_more">Паказаць больш</string>
<string name="show_less">Паказаць менш</string>
<string name="notification_actions_summary_android13">Адрэдагуйце кожнае дзеянне апавяшчэння, націснуўшы на яго. Першыя тры дзеянні (прайграванне/паўза, папярэдняе і наступнае) задаюцца сістэмай і не могуць быць зменены.</string>
</resources>

View File

@ -555,4 +555,19 @@
<string name="recently_added">Наскоро добавено</string>
<string name="notification_action_buffering">Буфериране</string>
<string name="crash_the_player">Разбийте плейъра</string>
<string name="show_meta_info_summary">Изключете, за да скриете полетата с мета информация с допълнителна информация за създателя на потока, съдържанието на потока или заявка за търсене</string>
<string name="auto_queue_toggle">Автоматично поставяне в опашка</string>
<string name="notification_actions_summary">Редактирайте всяко действие за известяване по-долу, като го докоснете. Изберете до три от тях, които да бъдат показани в компактното известие, като използвате квадратчетата за отметка вдясно</string>
<string name="prefer_original_audio_summary">Изберете оригиналния аудио запис независимо от езика</string>
<string name="clear_queue_confirmation_summary">Превключването от един плейър на друг може да замени вашата опашка</string>
<string name="prefer_descriptive_audio_summary">Изберете аудиозапис с описания за хора с увредено зрение, ако има такъв</string>
<string name="left_gesture_control_title">Действие с жест наляво</string>
<string name="prefer_descriptive_audio_title">Предпочитай описателно аудио</string>
<string name="progressive_load_interval_summary">Променете размера на интервала на зареждане на прогресивно съдържание (в момента %s). По-ниска стойност може да ускори първоначалното им зареждане</string>
<string name="prefer_original_audio_title">Предпочитай оригинално аудио</string>
<string name="clear_queue_confirmation_description">Опашката на активния плейър ще бъде заменена</string>
<string name="ignore_hardware_media_buttons_summary">Полезно, например, ако използвате слушалки със счупени физически бутони</string>
<string name="progressive_load_interval_title">Размер на интервала на зареждане при възпроизвеждане</string>
<string name="ignore_hardware_media_buttons_title">Игнорирайте събитията с хардуерни медийни бутони</string>
<string name="left_gesture_control_summary">Изберете жест за лявата половина на екрана на плейъра</string>
</resources>

View File

@ -297,7 +297,7 @@
<string name="notification_scale_to_square_image_title">থাম্বনেল ১:১ অনুপাতে সেট করো</string>
<string name="systems_language">সিস্টেম ডিফল্ট</string>
<string name="bookmark_playlist">প্লেলিস্ট বুকমার্ক করুন</string>
<string name="feed_use_dedicated_fetch_method_title">"যখন পর্যাপ্ত নিবেদিত ফিড থেকে ডাটা সংগ্রহ করুন"</string>
<string name="feed_use_dedicated_fetch_method_title">যখন পর্যাপ্ত নিবেদিত ফিড থেকে ডাটা সংগ্রহ করুন</string>
<string name="feed_update_threshold_option_always_update">সবসময় হালনগাদ করুন</string>
<string name="feed_update_threshold_summary">শেষ হালনাগাদের পর একটি সাবস্ক্রিপশনের আগের সময় সেকেলে বিবেচিত — %s</string>
<string name="feed_update_threshold_title">ফিড হালনাগাদ প্রবেশস্থল</string>
@ -317,11 +317,11 @@
<string name="feed_groups_header_title">চ্যানেল গ্রুপ</string>
<plurals name="days">
<item quantity="one">%d দিন</item>
<item quantity="other">"%d দিন"</item>
<item quantity="other">%d দিন</item>
</plurals>
<plurals name="hours">
<item quantity="one">%d ঘন্টা</item>
<item quantity="other">"%d ঘন্টা"</item>
<item quantity="other">%d ঘন্টা</item>
</plurals>
<plurals name="minutes">
<item quantity="one">%d মিনিট</item>

View File

@ -275,7 +275,7 @@
<string name="website_encouragement">নিউ পাইপ ওয়েব সাইট এ যান বিস্তারিত বিবরণ ও খবর এর জন্য</string>
<string name="more_than_100_videos">১০০+ ভিডিও</string>
<plurals name="listening">
<item quantity="one">"%s শ্রোতা"</item>
<item quantity="one">%s শ্রোতা</item>
<item quantity="other">%s শ্রোতা গন</item>
</plurals>
<string name="description_tab_description">বিবরণ</string>

View File

@ -243,17 +243,17 @@
<string name="notification_action_0_title">প্রথম ক্রিয়া বোতাম</string>
<string name="notification_scale_to_square_image_title">থাম্বনেলে ১:১ অনুপাতে করো</string>
<string name="show_play_with_kodi_summary">Kodi মিডিয়া সেন্টারে এর মাধ্যমে ভিডিও প্লে করার জন্য একটি বিকল্প প্রদর্শন কর</string>
<string name="show_play_with_kodi_title">\"Kodi দ্বারা চালান\" বিকল্পটি প্রদর্শন কর</string>
<string name="kore_not_found">অনুপস্থিত কোড অ্যাপ ইনস্টল করবেন\?</string>
<string name="play_with_kodi_title">Kodi দ্বারা চালাও</string>
<string name="show_play_with_kodi_title">\"Kodi-তে প্লে করুন\" দেখান</string>
<string name="kore_not_found">Kodi ইনস্টল করবেন?</string>
<string name="play_with_kodi_title">Kodi-তে প্লে করুন</string>
<string name="show_higher_resolutions_summary">শুধুমাত্র কিছু ডিভাইস 2K/4K ভিডিও চালাতে পারে</string>
<string name="show_higher_resolutions_title">উচ্চতর রেজুলেশন প্রদর্শন করা হবে</string>
<string name="default_popup_resolution_title">হজাত ভাসমান আকার</string>
<string name="default_resolution_title">হজাত আকার</string>
<string name="download_path_audio_dialog_title">অডিও ফাইলগুলির জন্য ডাউনলোডের ফোল্ডার নির্বাচন করুন</string>
<string name="download_path_audio_summary">ডাউনলোড করা অডিও ফাইলগুলি এখানে সঞ্চিত থাকে</string>
<string name="show_higher_resolutions_title">উচ্চতর রেজুলেশন দেখান</string>
<string name="default_popup_resolution_title">্বাভাবিক পপ-আপ রেজুলেশন</string>
<string name="default_resolution_title">্বাভাবিক রেজুলেশন</string>
<string name="download_path_audio_dialog_title">অডিও ডাউনলোডের জন্য ফোল্ডার নির্বাচন করুন</string>
<string name="download_path_audio_summary">ডাউনলোড করা অডিও এখানে থাক</string>
<string name="download_path_audio_title">অডিও ডাউনলোড ফোল্ডার</string>
<string name="download_path_dialog_title">ভিডিওগুলি ডাউনলোডের জন্য ফোল্ডার নির্বাচন করুন</string>
<string name="download_path_dialog_title">ভিডিও ডাউনলোডের জন্য ফোল্ডার নির্বাচন করুন</string>
<string name="download_path_summary">ডাউনলোড করা ভিডিওগুলো এখানে থাকে</string>
<string name="download_path_title">ভিডিও ডাউনলোড করার ফোল্ডার</string>
<string name="controls_add_to_playlist_title">যুক্ত করুন</string>
@ -261,33 +261,33 @@
<string name="controls_background_title">ব্যাকগ্রাউন্ড</string>
<string name="tab_choose">ট্যাব পছন্দ করুন</string>
<string name="tab_bookmarks">বুকমার্ক করা প্লেলিস্টসমূহ</string>
<string name="tab_subscriptions">দস্যতা</string>
<string name="tab_subscriptions">াবস্ক্রিবশন</string>
<string name="show_info">তথ্য দেখুন</string>
<string name="subscription_update_failed">দস্যতা হালনাগাদে ব্যর্থ</string>
<string name="subscription_change_failed">দস্যতা পরিবর্তন করা যায়নি</string>
<string name="channel_unsubscribed">চ্যানেল থেকে আনসাবস্ক্রাইব্ড</string>
<string name="subscription_update_failed">াবস্ক্রিবশন হালনাগাদ করা সম্ভব হয়নি</string>
<string name="subscription_change_failed">াবস্ক্রিবশন পরিবর্তন করা সম্ভব হয়নি</string>
<string name="channel_unsubscribed">চ্যানেল আনসাবস্ক্রাইব করা হয়েছে</string>
<string name="unsubscribe">আনসাবস্ক্রাইব</string>
<string name="subscribed_button_title">সাবস্ক্রাইব করা আছে</string>
<string name="subscribed_button_title">পূর্ব-সাবস্ক্রাইবকৃত</string>
<string name="subscribe_button_title">সাবস্ক্রাইব</string>
<string name="use_external_audio_player_title">বহির্গত অডিও প্লেয়ার ব্যবহার করুন</string>
<string name="use_external_video_player_summary">কিছু রেজোলিউশনে অডিও অপসারণ করে দেয়</string>
<string name="use_external_video_player_title">বাইরের ভিডিও প্লেয়ার ব্যবহার করুন</string>
<string name="use_external_audio_player_title">অন্যান্য অডিও প্লেয়ার ব্যবহার করুন</string>
<string name="use_external_video_player_summary">কিছু কিছু রেজুলেশনে অডিও ঠিকঠাক থাকবে না</string>
<string name="use_external_video_player_title">অন্যান্য ভিডিও প্লেয়ার ব্যবহার করুন</string>
<string name="share_dialog_title">শেয়ার করুন</string>
<string name="search_showing_result_for">রেজাল্ট দেখানো হচ্ছেঃ %s</string>
<string name="did_you_mean">তুমি কি বুঝিয়েছো %1$s\?</string>
<string name="search_showing_result_for">ফলাফল দেখানো হচ্ছেঃ %s</string>
<string name="did_you_mean">আপনি কি %1$s বোঝাচ্ছেন?</string>
<string name="settings">সেটিংস</string>
<string name="search">খুঁজুন</string>
<string name="controls_download_desc">স্ট্রিম ফাইল ডাউনলোড করুন</string>
<string name="controls_download_desc">ডাউনলোড করুন</string>
<string name="download">ডাউনলোড</string>
<string name="share">শেয়ার</string>
<string name="open_in_popup_mode">ভাসমান অবস্থায় খুলো</string>
<string name="open_in_browser">ব্রাউজারে খুলো</string>
<string name="open_in_popup_mode">পপআপ মুডে চালু করুন</string>
<string name="open_in_browser">ব্রাউজারে চালু করুন</string>
<string name="cancel">বাতিল</string>
<string name="install">ইনস্টল</string>
<string name="no_player_found_toast">কোনো ধারা চালক পাওয়া যায়নি (প্লে করতে VLC ইন্সটল করতে পারো)।</string>
<string name="no_player_found">কোন স্ট্রিম প্লেয়ার পাওয়া যায়নি। VLC ইনস্টল করতে চান\?</string>
<string name="no_player_found_toast">কোনো মিডিয়া প্লেয়ার পাওয়া যায়নি (মিডিয়া প্লে করতে VLC ইন্সটল করতে পারেন)।</string>
<string name="no_player_found">কোন স্ট্রিম প্লেয়ার পাওয়া যায়নি। VLC ইনস্টল করতে চান কি?</string>
<string name="upload_date_text">প্রকাশকাল %1$s</string>
<string name="main_bg_subtitle">আতশী কাঁচে টিপ দিয়ে শুরু করো।</string>
<string name="main_bg_subtitle">আতশ কাঁচে চাপ দিয়ে শুরু করো।</string>
<string name="notification_action_buffering">বাফারিং</string>
<string name="notification_action_shuffle">সাফল</string>
<string name="notification_action_4_title">পঞ্চম অ্যাকশন বাটন</string>
@ -432,7 +432,7 @@
<string name="chapters">অধ্যায়</string>
<string name="comments_tab_description">মতামত</string>
<string name="description_tab_description">বর্ণনা</string>
<string name="open_with">দিয়ে খুলো</string>
<string name="open_with">অন্য অ্যাপে ওপেন করুন</string>
<string name="feed_update_threshold_title">ফিড হালনাগাদ সীমা</string>
<string name="feed_group_dialog_empty_name">খালি গ্রুপ নাম</string>
<string name="feed_group_dialog_empty_selection">কোনো সদস্যতা নির্বাচিত হয়নি</string>
@ -570,7 +570,7 @@
<item quantity="one">ডাউনলোড শেষ</item>
<item quantity="other">%sটি ডাউনলোড শেষ</item>
</plurals>
<string name="mark_as_watched">দেখা হয়েছে চিহ্নিত করো</string>
<string name="mark_as_watched">দেখা হয়েছে বলে চিহ্নিত করুন</string>
<string name="settings_category_player_notification_title">চালক বিজ্ঞপ্তি</string>
<string name="low_quality_smaller">নিম্ন মান(ছোট)</string>
<string name="detail_heart_img_view_description">মূল তৈরিকারকের পছন্দ করা</string>

View File

@ -151,7 +151,7 @@
<string name="invalid_file">El fitxer no existeix o bé no teniu permisos de lectura/escriptura</string>
<string name="file_name_empty_error">El nom del fitxer no pot estar en blanc</string>
<string name="error_occurred_detail">S\'ha produït un error: %1$s</string>
<string name="error_report_button_text">Informeu de l\'error per correu electrònic</string>
<string name="error_report_button_text">Informeu per correu electrònic</string>
<string name="error_snackbar_message">S\'han produït alguns errors.</string>
<string name="error_snackbar_action">Informe</string>
<string name="what_device_headline">Informació:</string>
@ -561,7 +561,7 @@
<string name="notification_action_shuffle">Mescla</string>
<string name="notification_action_repeat">Repeteix</string>
<string name="notification_actions_at_most_three">El màxim d\'accions que poden aparèixer en una notificació compacta és de tres!</string>
<string name="notification_actions_summary">Editeu cada acció de la notificació tocant el botó corresponent. Podeu seleccionar-ne fins a tres, que es mostraran a les notificacions en format compacte</string>
<string name="notification_actions_summary">Editeu cada acció de la notificació tocant el botó corresponent. Podeu seleccionar-ne fins a tres, que es mostraran a les notificacions en format compacte.</string>
<string name="notification_action_4_title">Cinquè botó d\'acció</string>
<string name="notification_action_3_title">Quart botó d\'acció</string>
<string name="notification_action_2_title">Tercer botó d\'acció</string>
@ -692,9 +692,26 @@
<string name="unknown_quality">Cualitat desconeguda</string>
<string name="sort">Ordenar</string>
<string name="settings_category_player_notification_summary">Configura la notificació de reproducció actual.</string>
<string name="progressive_load_interval_summary">Canvia la mida de l\'interval de càrrega (actualment %s). Un valor inferior pot accelerar la càrrega inicial del vídeo. Els canvis requereixen un reinici del jugador.</string>
<string name="progressive_load_interval_summary">Canvia la mida de l\'interval de càrrega en continguts progressius (actualment %s). Un valor inferior pot accelerar la càrrega inicial del vídeo.</string>
<string name="ignore_hardware_media_buttons_title">Ignora els esdeveniments dels botons de reproducció físics</string>
<string name="ignore_hardware_media_buttons_summary">Útil, per exemple, si feu servir uns auriculars amb els botons físicament trencats</string>
<string name="left_gesture_control_summary">Trieu un gest per la part esquerra de la pantalla</string>
<string name="progressive_load_interval_title">Mida de l\'interval de càrrega de reproducció</string>
<string name="left_gesture_control_title">Acció de gest esquerra</string>
<string name="notification_actions_summary_android13">Editeu cada acció de notificació de sota tocant-la. Les tres primeres accions (reproduir/pausa, anterior i següent) són establertes pel sistema i no es poden personalitzar.</string>
<string name="right_gesture_control_summary">Tria un gest per a la meitat dreta del reproductor</string>
<string name="right_gesture_control_title">Acció del gest dret</string>
<string name="brightness">Brillantor</string>
<string name="volume">Volum</string>
<string name="none">Cap</string>
<string name="main_tabs_position_summary">Mou el selector de pestanya principal a la part inferior</string>
<string name="main_tabs_position_title">Posició de les pestanyes principals</string>
<string name="prefer_descriptive_audio_title">Prefereix àudio descriptiu</string>
<string name="prefer_original_audio_summary">Seleccioneu la pista d\'àudio original independentment de l\'idioma</string>
<string name="prefer_original_audio_title">Prefereix l\'àudio original</string>
<string name="fast_mode">Mode ràpid</string>
<string name="loading_metadata_title">Carregant Metadades…</string>
<string name="prefer_descriptive_audio_summary">Seleccioneu una pista d\'àudio amb descripcions per a persones amb discapacitat visual si està disponible</string>
<string name="streams_notification_channel_name">Nous streams</string>
<string name="streams_notification_channel_description">Notificacions sobre nous streams per a subscripcions</string>
</resources>

View File

@ -16,7 +16,7 @@
<string name="privacy_policy_encouragement">پڕۆژەی نیوپایپ زانیارییە تایبەتییەکانت بە وردی دەپارێزێت. هەروەها به‌رنامه‌كه‌ هیچ زانایارییەکت بەبێ ئاگاداری تۆ بەکارنابات.
\nسیاسەتی تایبەتی نیوپایپ بە وردی ڕوونکردنەوەت دەداتێ لەسەر ئەو زانیاریانەی وەریاندەگرێت و بەکاریاندەبات.</string>
<string name="download_to_sdcard_error_message">ناتوانرێت لە بیرگەی دەرەکیدا داببەزێنرێت . شوێنی فۆڵده‌ری دابه‌زاندنەکان ڕێکبخرێتەوە؟</string>
<string name="did_you_mean">ئایا مەبەستت ئه‌مه‌یه‌ \"%1$s\"؟</string>
<string name="did_you_mean">مەبەستت لە ئەمەیە ٪1$s ؟</string>
<string name="feed_update_threshold_title">ماوەی نوێكردنه‌وه‌ی فیید</string>
<string name="grid">هێڵەکی</string>
<string name="auto_queue_summary">به‌رده‌وامبوون له‌ (به‌بێ دووباره‌كردنه‌وه‌) نۆبه‌تی کارپێکەر به‌پێی په‌خشی هاوشێوه‌</string>
@ -33,7 +33,7 @@
<string name="name">ناو</string>
<string name="error_postprocessing_failed">چارەسەرکردن هه‌ره‌سی هێنا</string>
<string name="minimize_on_exit_title">بچوکبوونەوە لەکاتی گۆڕینی به‌رنامه‌</string>
<string name="download_path_summary">فایلی ڤیدیۆ دابه‌زێنراوەکان لێرەدا هەڵدەگیرێن</string>
<string name="download_path_summary">فایلی ڤیدیۆ داگیراوەکان لێرەدا هەڵدەگیرێن</string>
<string name="export_data_summary">هەناردە کردنی مێژوو ، بەژدارییه‌كان ، خشته‌لێدانه‌كان و ڕێكخستنه‌كان</string>
<string name="use_inexact_seek_summary">بردنەپێشی ناتەواوی خێرا وا لە لێدەرەکە دەکات کە بەخێرایی شوێنەکە بگۆڕێت. بردنەپێشی ٥ یان ١٥ یان ٢٥ چرکەیی لەگەڵ ئەمەدا کارناکات</string>
<string name="enable_disposed_exceptions_summary">سکاڵاکردن لەسەر نەگەیاندنی Rx ی پەسەندنەکرا لە دەرەوەی پارچە یان چالاکی لەدوای پوختەکردن</string>
@ -90,7 +90,7 @@
<string name="play_with_kodi_title">لێدان به‌ Kodi</string>
<string name="error_unable_to_load_comments">ناتوانرێت لێدوانەکان باربکرێن</string>
<string name="peertube_instance_add_help">بەستەری دۆخ دابنێ</string>
<string name="download_path_audio_summary">فایلی دەنگە دابه‌زێنراوەکان لێرەدا هەڵدەگیرێن</string>
<string name="download_path_audio_summary">فایلی دەنگە داگیراوەکان لێرەدا هەڵدەگیرێن</string>
<string name="error_snackbar_message">ببورە، هەندێك کێشە ڕوویدا.</string>
<string name="export_to">هەناردە کردن بۆ</string>
<string name="settings_category_player_behavior_title">ڕەفتار</string>
@ -105,13 +105,13 @@
<string name="unbookmark_playlist">لادانی نیشانه‌كراو</string>
<string name="tab_licenses">مۆڵەتەکان</string>
<string name="subscription_update_failed">ناتوانرێت به‌ژداریكردنه‌كه‌ نوێبكرێته‌وه‌</string>
<string name="controls_background_title">پاشبنەما</string>
<string name="controls_background_title">پشت شاشە</string>
<string name="search_no_results">بێ ئەنجامه‌</string>
<string name="localization_changes_requires_app_restart">زمان دەگۆڕدرێت لەدوای داگیرساندنەوەی به‌رنامه‌كه‌</string>
<string name="remove_watched">لادانی سەیرکراو</string>
<string name="enable_playback_state_lists_summary">پیشاندانی نیشانەکەری شوێنی کارپێکەر لە خشتەکاندا</string>
<string name="enable_playback_state_lists_title">شوێنەکان لە خشتەکاندا</string>
<string name="subscribed_button_title">به‌ژداریت</string>
<string name="subscribed_button_title">به‌ژداریتکرد</string>
<string name="caption_setting_description">بەهۆی گۆڕانکاری لە شێوەی ژێرنووسکردنەکە. پێویستە به‌رنامه‌كه‌ دابگیرسێنیته‌وه‌</string>
<plurals name="feed_group_dialog_selection_count">
<item quantity="one">%d دیار کراوه‌</item>
@ -132,8 +132,8 @@
<item quantity="one">%s بینراو</item>
<item quantity="other">%s بینراوان</item>
</plurals>
<string name="pause">ڕاگرتن</string>
<string name="download_path_audio_dialog_title">فۆڵدەری دابه‌زاندنی فایله‌ دەنگییەکان هەڵبژێرە</string>
<string name="pause">وەستاندن</string>
<string name="download_path_audio_dialog_title">فۆڵدەری داگرتنی فایله‌ دەنگییەکان هەڵبژێرە</string>
<string name="feed_create_new_group_button_title">نوێ</string>
<string name="clear_views_history_title">سڕینەوەی مێژووی سەیرکراو</string>
<string name="enable_playback_resume_title">بەردەوام بوونی کارپێکەر</string>
@ -158,7 +158,7 @@
<string name="play_all">لێدانی گشتی</string>
<string name="invalid_source">هەمان فایل/بابەت بوونی نییە</string>
<string name="start">دەستپێکردن</string>
<string name="subscribe_button_title">به‌ژداری</string>
<string name="subscribe_button_title">به‌ژداریکردن</string>
<string name="show_play_with_kodi_title">بژاردەی ”لێدان بە Kodi“ پیشانبدرێت</string>
<string name="tab_subscriptions">به‌ژدارییه‌كان</string>
<string name="blank_page_summary">پەڕەی بەتاڵ</string>
@ -199,12 +199,12 @@
<string name="detail_sub_channel_thumbnail_view_description">وێنۆچکەی سه‌روێنه‌ی کەناڵ</string>
<string name="import_settings">دەتەوێت ڕێکخستنەکانیش هاوردە بكرینه‌وه‌؟</string>
<string name="no_player_found">هیچ لێدەرێکی ڤیدیۆیی نه‌دۆزرایه‌وه‌. ده‌ته‌وێت VLC دابمەزرێنیت؟</string>
<string name="download_path_title">فۆڵده‌ری دابه‌زاندنی ڤیدیۆ</string>
<string name="download_path_title">فۆڵده‌ری داگرتنی ڤیدیۆ</string>
<string name="drawer_open">کردنەوەی پلیکانە</string>
<string name="light_theme_title">ڕووناك</string>
<string name="show_search_suggestions_summary">ئەو پێشنیازکراوانە هەڵبژێرە کە پیشان دەدرێن لەکاتی گەڕاندا</string>
<string name="error_progress_lost">کردارەکە هه‌ره‌سی هێنا, چونکە ئەو فایله‌ سڕاوەتەوە</string>
<string name="controls_add_to_playlist_title">زیادکردن بۆ</string>
<string name="controls_add_to_playlist_title">زیادی بکە بۆ</string>
<string name="no_subscribers">به‌ژداری نییه‌</string>
<string name="peertube_instance_url_title">دۆخی پێرتووب</string>
<string name="playlist_creation_success">خشتەلێدان سازکرا</string>
@ -214,7 +214,7 @@
<string name="infinite_videos">∞ ڤیدیۆ</string>
<string name="use_inexact_seek_title">بەکارهێنانی بردنەپێشی ناتەواوی خێرا</string>
<string name="error_occurred_detail">هەڵەیەک ڕوویدا : %1$s</string>
<string name="download_path_dialog_title">فۆڵده‌ری دابه‌زاندن بۆ فایلی ڤیدیۆکان هەڵبژێرە</string>
<string name="download_path_dialog_title">فۆڵده‌ری داگرتن بۆ فایلی ڤیدیۆکان هەڵبژێرە</string>
<string name="channel_created_by">ساز کراوه‌ لەلایەن %s</string>
<string name="users">بەکارهێنەران</string>
<string name="content">بابەت</string>
@ -228,7 +228,7 @@
<string name="privacy_policy_title">سیاسەتی تایبەتی نیوپایپ</string>
<string name="settings_category_downloads_title">دابه‌زاندن</string>
<string name="feed_use_dedicated_fetch_method_disable_button">ناكاراکردنی دۆخی خێرا</string>
<string name="open_in_browser">كردنه‌وه‌ له‌ وێبگه‌ر</string>
<string name="open_in_browser">ئەم بڕگەی پێڕستە ڤیدیۆیەک یان ستریمێکی دەنگی دەکاتەوە لە وێبگەڕێکدا</string>
<string name="error_http_no_content">ڕاژەکە هیچ داتایەک نانێرێت</string>
<string name="watch_history_states_deleted">شوێنی کارپێکراوەکان سڕانەوە</string>
<string name="app_update_notification_channel_description">پەیامەکانی وەشانە نوێیەکانی نیوپایپ</string>
@ -261,7 +261,7 @@
<item quantity="other">%d ڕۆژان</item>
</plurals>
<string name="rename_playlist">ناولێنانه‌وه‌</string>
<string name="download">دابه‌زاندن</string>
<string name="download">داگرتن</string>
<string name="ok">باشە</string>
<string name="metadata_cache_wipe_title">سڕینه‌وه‌ی پاشماوەی مێتاداتا</string>
<string name="error_download_resource_gone">ناتوانرێت ئەمه‌ داببه‌زێنرێته‌وه‌</string>
@ -326,7 +326,7 @@
<string name="default_resolution_title">قه‌باره‌ی بنەڕەتی</string>
<string name="minimize_on_exit_popup_description">بچووککردنەوە بۆ پەنجەرە</string>
<string name="songs">گۆرانییەکان</string>
<string name="controls_download_desc">دابه‌زاندنی فایلی پەخش</string>
<string name="controls_download_desc">داگرتنی فایلی پەخش</string>
<string name="list_view_mode">شێوازی پیشاندانی خشتە</string>
<string name="peertube_instance_add_title">زیادکردنی دۆخ</string>
<string name="accept">پەسەند</string>
@ -342,7 +342,7 @@
<string name="main_page_content">بابەتی پەڕەی سەرەکی</string>
<string name="feed_group_dialog_select_subscriptions">دیار کردنی بەژدارییەکان</string>
<string name="import_file_title">هاورده‌كردنی فایل</string>
<string name="download_path_audio_title">فۆڵده‌ری دابه‌زاندنی ده‌نگ</string>
<string name="download_path_audio_title">فۆڵده‌ری داگرتنی ده‌نگ</string>
<string name="use_external_video_player_summary">هه‌ندێك له‌ قه‌باره‌كان ده‌نگیان تێدا نامێنێته‌وه‌</string>
<string name="events">ڕووداوەکان</string>
<string name="detail_uploader_thumbnail_view_description">وێنۆچکەی کەسی بەرزکەرەوە</string>
@ -390,7 +390,7 @@
<string name="start_here_on_background">دەستپێکردنی لێدان لە پاشبنەماوە</string>
<string name="msg_name">ناوفایل</string>
<string name="set_as_playlist_thumbnail">دانان لەسەر وێنۆچکەی خشتەلێدان</string>
<string name="title_activity_about">دەربارەی نیوپایپ</string>
<string name="title_activity_about">دەربارەی NewPipe</string>
<string name="add_to_playlist">زیادکردن بۆ خشتەلێدان</string>
<string name="unknown_content">(نەزانراو)</string>
<string name="app_language_title">زمانی به‌رنامه‌</string>
@ -469,7 +469,7 @@
<string name="metadata_cache_wipe_summary">سڕینەوەی پاشماوەی هەموو ماڵپه‌ڕه‌كان</string>
<string name="kore_not_found">بەرنامەکە نه‌دۆزرایه‌وه‌. دابمه‌زرێت؟</string>
<string name="peertube_instance_add_fail">ناتوانرێ پشتگیری دۆخەکە بکرێ</string>
<string name="install">دامەزراندن</string>
<string name="install">دابەزاندن</string>
<string name="videos_string">ڤیدیۆکان</string>
<string name="unsupported_url">بەستەرەکە پشتگیری نەکراوە</string>
<string name="playback_pitch">قیڕ</string>
@ -484,7 +484,7 @@
<string name="enable_queue_limit_desc">لەیەک کاتدا تەنیا یەک بابەت دادەبەزێنرێت</string>
<string name="restore_defaults_confirmation">دەتەوێت بگەڕێنرێتەوە بۆ شێوازی بنەڕەتی؟</string>
<string name="pause_downloads">وەستاندنی دابەزاندنەکان</string>
<string name="tab_about">دەربارە</string>
<string name="tab_about">دەربارە و پرسیارەکان</string>
<string name="show_comments_title">پیشاندانی لێدوانەکان</string>
<string name="start_accept_privacy_policy">بۆ جێبەجێکردنی فرمانەکان لەگەڵ یاسای پاراستنی داتای گشتی ئەوروپیدا (GDPR) , ئێمە سەرنجت ڕادەکێشین بۆ سیاسەتە تایبەتییەکانی نیوپایپ. تکایە بەئاگادارییەوە بیخوێنەره‌وە.
\nپێویستە په‌سه‌ندی بکەیت بۆ ناردنی سکاڵاکانت.</string>
@ -502,7 +502,7 @@
<string name="feed_update_threshold_summary">کاتی دوای دواین نوێکردنەوە پێش بەژداربوون ڕەچاوکراوە — %s</string>
<string name="download_to_sdcard_error_title">بیرگەی دەرەکی بەردەست نییە</string>
<string name="enable_playback_resume_summary">گێڕانەوەی کارپێکەر بۆ شوێنی پێشووتر</string>
<string name="cancel">پاشگه‌زبوونه‌وه‌</string>
<string name="cancel">هەڵوەشاندنەوه</string>
<string name="tracks">تراکەکان</string>
<string name="play_queue_audio_settings">ڕێکخستنەکانی دەنگ</string>
<string name="downloads_storage_ask_summary">پرست پێ دەکرێت بۆ شوێنی دابەزاندنی هەر بابەتێک.
@ -541,7 +541,7 @@
<string name="notification_action_shuffle">تێکەڵکردن</string>
<string name="notification_action_repeat">دووبارە</string>
<string name="notification_actions_at_most_three">دەتوانیت تا سێ كردار دیار بكه‌یت تا پیشان بدرێن له‌ پەیامەکەدا!</string>
<string name="notification_actions_summary">ده‌ستكاری هه‌ر یه‌كێك له‌م كردارانه‌ی خواره‌وه‌ بكه‌ له‌ڕێگه‌ی كرته‌ له‌سه‌ریان. ده‌توانیت تا زیاتر له‌ سێ دانه‌یان هه‌ڵبژێریت له‌ ڕێگای چوارگۆشه‌كانی لای ڕاسته‌وه‌یان، تا پیشان بدرێن له‌ پەیامەکاندا</string>
<string name="notification_actions_summary">دەستکاریکردنی هەر کردارێکی ئاگادارکەرەوە لە خوارەوە بە دەستلێدان. ۳- دانە هەڵبژێرە لە ڕێگەی بەکارهێنانی سندوقەبچوکەکە لای ڕاستەوە نیشان دەدرێت</string>
<string name="notification_action_4_title">پێنجه‌م كرداری دوگمه‌</string>
<string name="notification_action_3_title">چواره‌م كرداری دوگمه‌</string>
<string name="notification_action_2_title">سێیه‌م كرداری دوگمه‌</string>
@ -555,7 +555,7 @@
<string name="show_meta_info_title">پیشاندانی زانیاری مێتا</string>
<string name="show_description_summary">ناكارایبكه‌ بۆ شاردنه‌وه‌ی دیسکریپشن له‌سه‌ر ڤیدیۆ و زانیاری زیاتر</string>
<string name="show_description_title">پیشاندانی دیسکریپشن</string>
<string name="night_theme_title">ڕووكاری شه‌و</string>
<string name="night_theme_title">ڕووكاری تاریک</string>
<string name="notification_colorize_summary">ئه‌ندرۆید ڕه‌نگی پەیام دڵخواز ده‌كات به‌پێی ڕه‌نگی سه‌ره‌كی وێنۆچكه‌كه‌ ( ڕه‌چاوی ئه‌وه‌ بكه‌ كه‌ ئه‌م تایبه‌تمه‌ندییه‌ هه‌موو ئامێرێك ناگرێته‌وه‌ )</string>
<string name="notification_colorize_title">ڕه‌نگكردنی پەیام</string>
<string name="youtube_restricted_mode_enabled_summary">یوتوب ”دۆخی قه‌ده‌غه‌كراو” پێشكه‌ش ده‌كات كه‌ بابەتە نه‌شیاوه‌كان ده‌شارێته‌وه‌</string>
@ -582,7 +582,7 @@
\nجا ده‌ته‌وێت به‌ژداری لابده‌یت له‌م كه‌ناڵه‌؟</string>
<string name="feed_load_error_account_info">ناتوانرێت فیید باربکرێت تا ً`%s` .</string>
<string name="feed_load_error">هه‌ڵه‌ له‌ باركردنی فیید</string>
<string name="disable_media_tunneling_summary">ئه‌م تایبه‌تمه‌ندییه‌ كارابكه‌ گه‌ر ڕوونمای ڕه‌ش یاخوود جامبوونی کارپێکەرت ئه‌زموون كرد</string>
<string name="disable_media_tunneling_summary">ئەگەر تووشی شاشەی ڕەش یان لکەلکە بوویت لە کاتی پەخشکردنی ڤیدیۆدا، تونێلکردنی میدیا لەکاربخە.</string>
<string name="metadata_privacy_internal">ناوەکی</string>
<string name="metadata_privacy_private">تایبەتی</string>
<string name="metadata_privacy_unlisted">خشتەنەکراو</string>
@ -665,11 +665,11 @@
<string name="detail_pinned_comment_view_description">لێدوانی هەڵواسراو</string>
<string name="crash_the_player">کڕاش کردنی لێدەر</string>
<string name="show_error_snackbar">پیشاندانی هەڵەی سناکباڕ</string>
<string name="no_appropriate_file_manager_message">هیچ ڕێکخەرێکی فایلی گونجاو نەدۆزرایەوە بۆ ئەم کردارە.
\nتکایە ڕێکخەری فایلییەک دابمەزرێنە لۆ هەوڵدانی ناکاراکردنی \'%s\' لە ڕێکخستنەکانی دابەزاندندا.</string>
<string name="no_appropriate_file_manager_message">هیچ FileManager پەڕگەی گونجاو بۆ ئەم کردارە نەدۆزراوەتەوە.
\nتکایە بەڕێوەبەری پەڕگەیەک دابمەزرێنە یان هەوڵبدە \'%s\' لە Settings بڕۆ Download لەکاربخە</string>
<string name="leak_canary_not_available">LeakCanary بەردەست نییە</string>
<string name="no_appropriate_file_manager_message_android_10">هیچ ڕێکخەرێکی فایلی گونجاو نەدۆزرایەوە بۆ ئەم کردارە.
\nتکایە ڕێکخەرێکی فایلی دابمەزرێنە کە گونجاوبێت لەگەڵ دەسەڵاتی گەیشتن بە بیرگە.</string>
<string name="no_appropriate_file_manager_message_android_10">هیچ FileManager گونجاو نەدۆزرایەوە بۆ ئەم کردارە.
\nتکایە FileManager دابمەزرێنە کە گونجاوبێت لەگەڵ دەسەڵاتی گەیشتن بە بیرگە.</string>
<string name="check_new_streams">پشکنین کردن بۆ پەخشی نوێ</string>
<string name="enable_streams_notifications_title">پەیامەکانی پەخشە نوێیەکان</string>
<string name="enable_streams_notifications_summary">پەیام بکرێم لەکاتی هەبوونی پەخشی نوێی بەژدارییەکان</string>
@ -694,4 +694,12 @@
<string name="percent">لەسەدا</string>
<string name="semitone">نیمچەتەن</string>
<string name="progressive_load_interval_exoplayer_default">بنەڕەتی ExoPlayer</string>
<string name="prefer_original_audio_summary">دیاریکردنی تراکی دەنگی ئەسڵی بێ گوێدانە زمانەکە</string>
<string name="notification_actions_summary_android13">دەستکاریکردنی هەر کردارێکی ئاگادارکەرەوە لە خوارەوە بە دەستلێدان. یەکەم سێ کردار (لێدان/وەستان، پێشوو و دواتر) لەلایەن سیستەمەکەوە دانراوە و ناتوانرێت دەستکاری بکرێت.</string>
<string name="prefer_descriptive_audio_title">پەسەند کردنی دەنگی وەسفکراو</string>
<string name="progressive_load_interval_summary">گۆڕینی قەبارەی ماوەی لۆد لەسەر ناوەڕۆکی پێشکەوتوو (ئێستا ٪s). بەهایەکی کەمتر لەوانەیە بارکردنی سەرەتا خێراتر بکات</string>
<string name="prefer_original_audio_title">پەسەندکردنی دەنگی ئەسڵی</string>
<string name="ignore_hardware_media_buttons_summary">بەسوودە، بۆ نموونە، ئەگەر هێدسێتێک بەکاربهێنیت لەگەڵ دوگمەی فیزیکی شکاو</string>
<string name="progressive_load_interval_title">قەبارەی نێوان بارکردنی پەخشکردن</string>
<string name="ignore_hardware_media_buttons_title">دوگمەی ڕووداوەکانی میدیای هاردوێر بەجێبهێڵە</string>
</resources>

View File

@ -554,7 +554,7 @@
<string name="notification_action_shuffle">Promíchat</string>
<string name="notification_action_repeat">Opakovat</string>
<string name="notification_actions_at_most_three">Do kompaktního oznámení lze vybrat nejvíce tři akce!</string>
<string name="notification_actions_summary">Upravte každou akci oznámení níže poklepáním. Pomocí zaškrtávacích políček vpravo vyberte až tři z nich, které se mají zobrazit v kompaktním oznámení</string>
<string name="notification_actions_summary">Upravte každou akci oznámení níže poklepáním. Pomocí zaškrtávacích políček vpravo vyberte až tři z nich, které se mají zobrazit v kompaktním oznámení.</string>
<string name="notification_action_4_title">Páté akční tlačítko</string>
<string name="notification_action_3_title">Čtvrté akční tlačítko</string>
<string name="notification_action_2_title">Třetí akční tlačítko</string>
@ -819,4 +819,12 @@
<string name="channel_tab_channels">Kanály</string>
<string name="previous_stream">Předchozí stream</string>
<string name="channel_tab_livestreams">Živě</string>
<plurals name="replies">
<item quantity="one">%s odpověď</item>
<item quantity="few">%s odpovědi</item>
<item quantity="other">%s odpovědí</item>
</plurals>
<string name="show_more">Zobrazit více</string>
<string name="notification_actions_summary_android13">Upravte každou akci oznámení níže poklepáním. První tři akce (přehrání/pozastavení, předchozí a další) jsou nastaveny systémem a nemohou být přizpůsobeny.</string>
<string name="show_less">Zobrazit méně</string>
</resources>

View File

@ -4,64 +4,64 @@
<string name="upload_date_text">Udgivet den %1$s</string>
<string name="no_player_found">Ingen streamafspiller blev fundet. Installér VLC\?</string>
<string name="no_player_found_toast">Ingen streamafspiller fundet (du kan installere VLC for at afspille den).</string>
<string name="install">Installer</string>
<string name="cancel">Annuller</string>
<string name="install">Installér</string>
<string name="cancel">Annullér</string>
<string name="open_in_browser">Åbn i browser</string>
<string name="open_in_popup_mode">Åbn i pop op-tilstand</string>
<string name="open_in_popup_mode">Åbn i popup-tilstand</string>
<string name="share">Del</string>
<string name="download">Download</string>
<string name="controls_download_desc">Download stream-fil</string>
<string name="download">Hent</string>
<string name="controls_download_desc">Hent stream-fil</string>
<string name="search">Søg</string>
<string name="settings">Indstillinger</string>
<string name="did_you_mean">Mente du \"%1$s\"\?</string>
<string name="share_dialog_title">Del med</string>
<string name="use_external_video_player_title">Benyt ekstern videoafspiller</string>
<string name="use_external_video_player_title">Brug ekstern videoafspiller</string>
<string name="use_external_video_player_summary">Fjerner lyd ved nogle opløsninger</string>
<string name="use_external_audio_player_title">Brug ekstern lydafspiller</string>
<string name="subscribe_button_title">Abonner</string>
<string name="subscribe_button_title">Abonnér</string>
<string name="subscribed_button_title">Abonnerer</string>
<string name="unsubscribe">Afmeld abonnement</string>
<string name="channel_unsubscribed">Abonnement afmeldt</string>
<string name="unsubscribe">Afmeld</string>
<string name="channel_unsubscribed">Kanal afmeldt</string>
<string name="subscription_change_failed">Kunne ikke ændre abonnement</string>
<string name="subscription_update_failed">Kunne ikke opdatere abonnement</string>
<string name="show_info">Vis info</string>
<string name="tab_subscriptions">Abonnementer</string>
<string name="tab_bookmarks">Gemte spillelister</string>
<string name="tab_choose">Vælg fane</string>
<string name="tab_bookmarks">Gemte Playlister</string>
<string name="tab_choose">Vælg Fane</string>
<string name="fragment_feed_title">Nyheder</string>
<string name="controls_background_title">Baggrund</string>
<string name="controls_popup_title">Pop op</string>
<string name="controls_popup_title">Popup</string>
<string name="controls_add_to_playlist_title">Føj til</string>
<string name="download_path_title">Mappe til download af video</string>
<string name="download_path_summary">Downloadede videoer gemmes her</string>
<string name="download_path_dialog_title">Angiv download-mappe for videofiler</string>
<string name="download_path_audio_title">Download-mappe for lydfiler</string>
<string name="download_path_audio_summary">Downloadede lydfiler gemmes her</string>
<string name="download_path_audio_dialog_title">Angiv download-mappe for lydfiler</string>
<string name="download_path_title">Lagringsmappe til videoer</string>
<string name="download_path_summary">Hentede videoer gemmes her</string>
<string name="download_path_dialog_title">Vælg lagringsmappe til videofiler</string>
<string name="download_path_audio_title">Lagringsmappe til lydfiler</string>
<string name="download_path_audio_summary">Hentede lydfiler gemmes her</string>
<string name="download_path_audio_dialog_title">Vælg lagringsmappe til lydfiler</string>
<string name="default_resolution_title">Standardopløsning</string>
<string name="default_popup_resolution_title">Standardopløsning for pop op</string>
<string name="default_popup_resolution_title">Standardopløsning for popup</string>
<string name="show_higher_resolutions_title">Vis højere opløsninger</string>
<string name="show_higher_resolutions_summary">Kun nogle enheder kan afspille 2K-/4K-videoer</string>
<string name="play_with_kodi_title">Afspil med Kodi</string>
<string name="kore_not_found">Installer manglede Kode-app\?</string>
<string name="kore_not_found">Installér manglende Kore-app?</string>
<string name="show_play_with_kodi_title">Vis valgmuligheden \"Afspil med Kodi\"</string>
<string name="show_play_with_kodi_summary">Vis en knap til at afspille en video via Kodi-mediecenteret</string>
<string name="play_audio">Lyd</string>
<string name="default_audio_format_title">Standardformat for lydfiler</string>
<string name="default_video_format_title">Standardformat for videofiler</string>
<string name="theme_title">Tema</string>
<string name="light_theme_title">Lyst</string>
<string name="dark_theme_title">Mørkt</string>
<string name="light_theme_title">Lys</string>
<string name="dark_theme_title">Mørk</string>
<string name="black_theme_title">Sort</string>
<string name="popup_remember_size_pos_title">Husk størrelse og placering af pop op</string>
<string name="popup_remember_size_pos_summary">Husk sidste størrelse og placering af pop op-afspiller</string>
<string name="popup_remember_size_pos_title">Husk popup-egenskaber</string>
<string name="popup_remember_size_pos_summary">Husk sidste størrelse og placering af popup-afspiller</string>
<string name="use_inexact_seek_title">Brug hurtig og upræcis søgning</string>
<string name="use_inexact_seek_summary">Upræcis søgning lader afspilleren finde placeringer hurtigere, men mindre præcist. Søgninger på 5, 15 eller 25 sekunder fungerer ikke med denne indstilling slået til</string>
<string name="thumbnail_cache_wipe_complete_notice">Billedcache slettet</string>
<string name="metadata_cache_wipe_title">Slet metadata-cachen</string>
<string name="metadata_cache_wipe_summary">Slet alle websidedata fra cachen</string>
<string name="metadata_cache_wipe_title">Slet metadata-cache</string>
<string name="metadata_cache_wipe_summary">Fjern alle cached websidedata</string>
<string name="metadata_cache_wipe_complete_notice">Metadata-cache slettet</string>
<string name="auto_queue_title">Føj automatisk næste stream til køen</string>
<string name="auto_queue_title">Føj automatisk næste stream til kø</string>
<string name="auto_queue_summary">Fortsæt en afspilningskø, der afsluttes (ikke-gentagende), ved at tilføje en lignende stream</string>
<string name="show_search_suggestions_title">Søgeforslag</string>
<string name="show_search_suggestions_summary">Vælg forslagene, der vises, når der søges</string>
@ -71,63 +71,63 @@
<string name="enable_watch_history_summary">Husk sete videoer</string>
<string name="resume_on_audio_focus_gain_title">Fortsæt afspilning</string>
<string name="resume_on_audio_focus_gain_summary">Fortsæt afspilning efter afbrydelser (fx telefonopkald)</string>
<string name="download_dialog_title">Download</string>
<string name="download_dialog_title">Hent</string>
<string name="show_next_and_similar_title">Vis \'Næste\' og \'Lignende\' videoer</string>
<string name="show_hold_to_append_title">Vis \"Hold for at sætte i kø\"-tip</string>
<string name="show_hold_to_append_summary">Vis et tip når der trykkes på baggrunds- eller pop op-knappen på siden med videodetaljer</string>
<string name="unsupported_url">Denne webadresse er ikke understøttet</string>
<string name="show_hold_to_append_summary">Vis tip, når du trykker på baggrunden eller popup-knappen i video \"Detaljer:\"</string>
<string name="unsupported_url">Ikke-understøttet URL</string>
<string name="default_content_country_title">Standardland for indhold</string>
<string name="content_language_title">Standardsprog for indhold</string>
<string name="settings_category_player_title">Afspiller</string>
<string name="settings_category_player_behavior_title">Opførsel</string>
<string name="settings_category_player_behavior_title">Adfærd</string>
<string name="settings_category_video_audio_title">Video og lyd</string>
<string name="settings_category_history_title">Historik og cache</string>
<string name="settings_category_appearance_title">Udseende</string>
<string name="settings_category_debug_title">Fejlretning</string>
<string name="settings_category_updates_title">Opdateringer</string>
<string name="background_player_playing_toast">Afspiller i baggrunden</string>
<string name="popup_playing_toast">Afspiller i pop op-tilstand</string>
<string name="popup_playing_toast">Afspiller i popup-tilstand</string>
<string name="content">Indhold</string>
<string name="show_age_restricted_content_title">Vis aldersbegrænset indhold</string>
<string name="duration_live">Live</string>
<string name="downloads">Downloads</string>
<string name="downloads_title">Downloads</string>
<string name="downloads">Hentet</string>
<string name="downloads_title">Hentet</string>
<string name="error_report_title">Fejlrapport</string>
<string name="all">Alle</string>
<string name="channels">Kanaler</string>
<string name="playlists">Spillelister</string>
<string name="playlists">Playlister</string>
<plurals name="videos">
<item quantity="one">Én video</item>
<item quantity="other">%s videoer</item>
</plurals>
<string name="tracks">Numre</string>
<string name="users">Brugere</string>
<string name="disabled">Slået fra</string>
<string name="clear">Slet</string>
<string name="disabled">Deaktiveret</string>
<string name="clear">Ryd</string>
<string name="best_resolution">Bedste opløsning</string>
<string name="undo">Fortryd</string>
<string name="file_deleted">Fil slettet</string>
<string name="play_all">Afspil alle</string>
<string name="play_all">Afspil Alle</string>
<string name="always">Altid</string>
<string name="just_once">Kun én gang</string>
<string name="just_once">Kun Én Gang</string>
<string name="file">Fil</string>
<string name="notification_channel_name">NewPipe notifikation</string>
<string name="notification_channel_name">NewPipe-notifikation</string>
<string name="notification_channel_description">Notifikationer for NewPipes afspiller</string>
<string name="app_update_notification_channel_name">Notifikation om opdatering af app</string>
<string name="app_update_notification_channel_description">Notifikationer for nye NewPipe versioner</string>
<string name="unknown_content">[Ukendt]</string>
<string name="switch_to_background">Skift til baggrund</string>
<string name="switch_to_popup">Skift til pop op</string>
<string name="switch_to_popup">Skift til popup</string>
<string name="switch_to_main">Skift til hovedafspiller</string>
<string name="import_data_title">Importer database</string>
<string name="export_data_title">Eksporter database</string>
<string name="import_data_title">Importér database</string>
<string name="export_data_title">Eksportér database</string>
<string name="import_data_summary">Overskriver din nuværende historik, abonnementer, spillelister og (hvis det ønskes) indstillinger</string>
<string name="export_data_summary">Eksporter historik, abonnementer, spillelister og indstillinger</string>
<string name="clear_views_history_title">Slet visningshistorik</string>
<string name="export_data_summary">Eksportér historik, abonnementer, spillelister og indstillinger</string>
<string name="clear_views_history_title">Ryd visningshistorik</string>
<string name="clear_views_history_summary">Sletter historikken over afspillede streams og afspilningspositionerne</string>
<string name="delete_view_history_alert">Slet hele visningshistorikken\?</string>
<string name="watch_history_deleted">Visningshistorikken blev slettet</string>
<string name="clear_search_history_title">Slet søgehistorik</string>
<string name="clear_search_history_title">Ryd søgehistorik</string>
<string name="clear_search_history_summary">Sletter historikken for søgeord</string>
<string name="delete_search_history_alert">Slet hele søgehistorikken\?</string>
<string name="search_history_deleted">Søgehistorikken blev slettet</string>
@ -367,12 +367,12 @@
<string name="max_retry_desc">Maksimalt antal forsøg før downloaden opgives</string>
<string name="pause_downloads_on_mobile">Afbryd på forbrugsafregnede netværk</string>
<string name="pause_downloads_on_mobile_desc">Nyttigt ved skift til mobildata, selv om nogle downloads ikke kan sættes på pause</string>
<string name="peertube_instance_add_https_only">Kun HTTPS adresser understøttes</string>
<string name="peertube_instance_add_https_only">Kun HTTPS-URL\'er understøttes</string>
<string name="peertube_instance_add_exists">Instansen findes allerede</string>
<string name="peertube_instance_add_fail">Kunne ikke validere instansen</string>
<string name="peertube_instance_add_help">Skriv instansens adresse</string>
<string name="peertube_instance_add_help">Indtast instans-URL</string>
<string name="peertube_instance_add_title">Tilføj instans</string>
<string name="peertube_instance_url_help">Find de instanserne du kan lide på %s</string>
<string name="peertube_instance_url_help">Find de instanser, du kan lide på %s</string>
<string name="peertube_instance_url_summary">Vælg dine yndlings PeerTube-instanser</string>
<string name="peertube_instance_url_title">PeerTube-instanser</string>
<string name="autoplay_title">Afspil automatisk</string>
@ -384,11 +384,11 @@
<string name="show_comments_title">Vis kommentarer</string>
<string name="notification_action_nothing">Ingenting</string>
<string name="notification_action_repeat">Gentag</string>
<string name="notification_action_4_title">Femte handlingstast</string>
<string name="notification_action_3_title">Fjerde handlingstast</string>
<string name="notification_action_0_title">Første handlingstast</string>
<string name="notification_action_1_title">Anden handlingstast</string>
<string name="notification_action_2_title">Tredje handlingstast</string>
<string name="notification_action_4_title">Femte handlingsknap</string>
<string name="notification_action_3_title">Fjerde handlingsknap</string>
<string name="notification_action_0_title">Første handlingsknap</string>
<string name="notification_action_1_title">Anden handlingsknap</string>
<string name="notification_action_2_title">Tredje handlingsknap</string>
<string name="search_showing_result_for">Viser resultater for: %s</string>
<string name="open_with">Åbn med</string>
<string name="leak_canary_not_available">LeakCanary er ikke tilgængelig</string>
@ -426,42 +426,42 @@
</plurals>
<string name="delete_downloaded_files_confirm">Slet alle downloadede filer fra drevet\?</string>
<string name="pause_downloads">Sæt downloads på pause</string>
<string name="start_main_player_fullscreen_title">Start hovedafspilleren i fuldskærmstilstand</string>
<string name="start_main_player_fullscreen_title">Start hovedafspiller i fuld skærm</string>
<string name="no_dir_yet">Downloadmappe endnu ikke valgt. Vælg standardmappen nu</string>
<string name="auto_queue_toggle">Læg automatisk i kø</string>
<string name="settings_category_player_notification_summary">Konfigurer det spillende streams notifikation</string>
<string name="show_age_restricted_content_summary">Vis aldersbegrænset indhold (f.eks. 18+)</string>
<string name="youtube_restricted_mode_enabled_title">Slå YouTube \"begrænset tilstand\" til</string>
<string name="youtube_restricted_mode_enabled_summary">YouTube har en \"begrænset tilstand\" der skjuler videoer som potientielt er skadelige for børn</string>
<string name="auto_queue_toggle">Sæt automatisk i kø</string>
<string name="settings_category_player_notification_summary">Konfigurér notifikation om igangværende stream</string>
<string name="show_age_restricted_content_summary">Vis indhold, der muligvis er uegnet for børn, fordi det har en aldersgrænse (f.eks. 18+)</string>
<string name="youtube_restricted_mode_enabled_title">Slå YouTubes \"Begrænset Tilstand\" til</string>
<string name="youtube_restricted_mode_enabled_summary">YouTube tilbyder en \"Begrænset Tilstand\", som skjuler potentielt voksenindhold</string>
<string name="restricted_video">Denne video er aldersbegrænset.
\n
\nSlå \"%1$s\" fra i indstillingerne hvis du vil se den.</string>
\nSlå \"%1$s\" til i indstillingerne, hvis du vil se den.</string>
<string name="streams_notification_channel_name">Nye streams</string>
<string name="streams_notification_channel_description">Notifikationer om nye streams fra abonnementer</string>
<string name="recaptcha_cookies_cleared">reCAPTCHA cookies er ryddet</string>
<string name="recaptcha_cookies_cleared">reCAPTCHA-cookies blev ryddet</string>
<string name="delete_playback_states_alert">Slet alle afspilningspositioner\?</string>
<string name="missing_file">Filen er flyttet eller slettet</string>
<string name="error_report_notification_title">NewPipe stødte ind i en fejl, tryk for at rapportere</string>
<string name="error_report_open_issue_button_text">Rapporter på GitHub</string>
<string name="high_quality_larger">Høj kvalitet (større)</string>
<string name="enable_queue_limit">Begræns downloadkøen</string>
<string name="clear_cookie_summary">Ryd de cookies som NewPipe opbevarer når du løser en reCAPTCHA</string>
<string name="notification_colorize_title">Farvelæg notifikationen</string>
<string name="settings_category_player_notification_title">Afspillernotifikation</string>
<string name="clear_cookie_summary">Ryd de cookies, som NewPipe opbevarer, når du løser en reCAPTCHA</string>
<string name="notification_colorize_title">Farvelæg notifikation</string>
<string name="settings_category_player_notification_title">Afspiller-notifikation</string>
<string name="error_report_notification_toast">En fejl opstod, se notifikationen</string>
<string name="show_description_summary">Slå fra for at skjule videobeskrivelsen og yderligere information</string>
<string name="show_meta_info_summary">Slå fra for at gemme metainformationskasser med yderligere information om streammets skaber, streammets indhold eller en søgeforespørgsel</string>
<string name="show_meta_info_summary">Slå fra for at skjule metainfo-bokse med yderligere information om streamskaberen, streamindhold eller en søgeforespørgsel</string>
<plurals name="download_finished_notification">
<item quantity="one">Download fuldført</item>
<item quantity="other">%s downloads fuldført</item>
</plurals>
<string name="clear_queue_confirmation_description">Den aktive spilleliste bliver udskiftet</string>
<string name="clear_queue_confirmation_summary">Hvis du skifter fra en spiller til en anden, kan din kø blive erstattet</string>
<string name="show_meta_info_title">Vis metainformation</string>
<string name="clear_queue_confirmation_description">Den aktive afspillerkø bliver udskiftet</string>
<string name="clear_queue_confirmation_summary">Ændring fra én afspiller til en anden kan erstatte din kø</string>
<string name="show_meta_info_title">Vis metainfo</string>
<string name="local_search_suggestions">Lokale søgeforslag</string>
<string name="remote_search_suggestions">Fjerne søgeforslag</string>
<string name="start_main_player_fullscreen_summary">Start ikke videoer i miniafspilleren, men gå direkte til fuldskærmstilstand, hvis automatisk rotering er låst. Du kan stadig se miniafspilleren, hvis du går ud af fuldskærmstilstand</string>
<string name="unsupported_url_dialog_message">Kunne ikke genkende addressen. Vil du åbne den i en anden app\?</string>
<string name="remote_search_suggestions">Forslag til fjernsøgning</string>
<string name="start_main_player_fullscreen_summary">Start ikke videoer i miniafspilleren, men skift direkte til fuldskærmstilstand, hvis automatisk rotation er låst. Du kan stadig få adgang til miniafspilleren ved at forlade fuldskærm</string>
<string name="unsupported_url_dialog_message">Kunne ikke genkende URL. Åbn med en anden app?</string>
<string name="hash_channel_name">Videohashfunktion notifikation</string>
<string name="hash_channel_description">Notifikationer om videohashfunktioners status</string>
<string name="error_report_channel_name">Fejlrapport-notifikation</string>
@ -479,22 +479,22 @@
<string name="copy_for_github">Kopier en formatteret rapport</string>
<string name="permission_display_over_apps">Giv tilladelse til at vise over andre apps</string>
<string name="enable_playback_state_lists_summary">Vis indikatorer for afspilningsposition i lister</string>
<string name="watch_history_states_deleted">Afspilningspositioner slettet</string>
<string name="clear_cookie_title">Ryd reCAPTCHA cookies</string>
<string name="watch_history_states_deleted">Afspilningspositioner blev slettet</string>
<string name="clear_cookie_title">Ryd reCAPTCHA-cookies</string>
<string name="download_already_pending">Der er en afventende download med dette navn</string>
<string name="start_downloads">Start downloads</string>
<string name="notification_scale_to_square_image_title">Beskær miniaturebillede til 1:1 format</string>
<string name="notification_scale_to_square_image_summary">Beskær video-miniaturebillede i notifikationen fra 16:9 til 1:1 format</string>
<string name="notification_actions_summary">Rediger hver eneste varselshandling nedenunder ved at trykke på dem. Vælg op til tre af dem som bliver vist i den lille notifikation, via afkrydsningsfelterne til højre</string>
<string name="notification_actions_summary">Redigér hver underretningshandling nedenfor ved at trykke på dem. Vælg op til tre af dem, som bliver vist i den lille notifikation via afkrydsningsfelterne til højre.</string>
<string name="notification_actions_at_most_three">Du kan kun vælge op til tre handlinger som kan vises i den lille notifikation!</string>
<string name="notification_action_buffering">Buffer</string>
<string name="notification_action_buffering">Buffering</string>
<string name="notification_colorize_summary">Få Android til at vælge notifikationens farve ud fra den primære farve i miniaturebilledet (virker ikke på alle enheder)</string>
<string name="night_theme_title">Nattema</string>
<string name="seek_duration_title">Frem- og tilbagesøgningstid</string>
<string name="night_theme_title">Nat-tema</string>
<string name="seek_duration_title">Søgningsvarighed for spole frem/tilbage</string>
<string name="restricted_video_no_stream">Denne video er aldersbegrænset.
\nPga. YouTubes politik om aldersbegrænsede videoer har NewPipe ikke adgang til videoen.</string>
\nPga. nye YouTube-politikker om aldersbegrænsede videoer har NewPipe ikke adgang til nogen af dens videostreams og kan derfor ikke afspille dem.</string>
<string name="crash_the_player">Crash afspilleren</string>
<string name="clear_queue_confirmation_title">Spørg om bekræftelse før du rydder en kø</string>
<string name="clear_queue_confirmation_title">Spørg om bekræftelse, før du rydder en kø</string>
<string name="seekbar_preview_thumbnail_title">Forhåndsvisning af miniaturebilleder på statuslinjen</string>
<string name="enqueue_next_stream">Sæt i kø som næste</string>
<string name="enqueued_next">Er sat som næste i køen</string>
@ -661,7 +661,7 @@
<string name="unknown_format">Ukendt format</string>
<string name="unknown_quality">Ukendt kvalitet</string>
<string name="detail_heart_img_view_description">Hjertemarkeret af indholdsskaberen</string>
<string name="progressive_load_interval_title">Intervalstørrelse for afspilningsindlæsning</string>
<string name="progressive_load_interval_title">Størrelse på afspilningsinterval</string>
<string name="progressive_load_interval_exoplayer_default">ExoPlayer-standard</string>
<string name="feed_group_dialog_empty_name">Tomt gruppenavn</string>
<string name="downloads_storage_ask_summary">Du vil blive spurgt, hvor du vil gemme hver enkelt download.
@ -713,9 +713,76 @@
<string name="no_audio_streams_available_for_external_players">Ingen lydstreams er tilgængelige for eksterne afspillere</string>
<string name="select_quality_external_players">Vælg kvalitet til eksterne afspillere</string>
<string name="sort">Sortér</string>
<string name="ignore_hardware_media_buttons_title">Ignorer hardware medie knapper</string>
<string name="ignore_hardware_media_buttons_title">Ignorér hardware medie-knap begivenheder</string>
<string name="ignore_hardware_media_buttons_summary">Brugbart f.eks. hvis du bruger et headset med ødelagte fysiske knapper</string>
<string name="duplicate_in_playlist">Playlists der er grået ud, indeholder allerede dette objekt.</string>
<string name="unset_playlist_thumbnail">Inaktiver permanent thumbnail</string>
<string name="msg_failed_to_copy">Fejlede at kopiere til udklipsholderen</string>
</resources>
<string name="prefer_original_audio_summary">Brug det originale lydspor uanset sprog</string>
<string name="prefer_descriptive_audio_title">Foretræk lydbeskrivelser</string>
<string name="prefer_original_audio_title">Foretræk original lyd</string>
<string name="prefer_descriptive_audio_summary">Brug lydbeskrivelser for personer med nedsat syn, hvis tilgængeligt</string>
<string name="notification_actions_summary_android13">Redigér hver underretningshandling nedenfor ved at trykke på dem. De første tre handlinger (afspil/sæt på pause, forrige og næste) er indstillet af systemet og kan ikke brugerdefineres.</string>
<string name="loading_metadata_title">Indlæser Metadata…</string>
<string name="remove_duplicates_title">Fjern duplikater?</string>
<string name="image_quality_summary">Vælg kvaliteten af billeder, og om billeder overhovedet skal indlæses, for at reducere data- og hukommelsesforbrug. Ændringer rydder både billedcachen i hukommelsen og på disken — %s</string>
<string name="image_quality_medium">Middel kvalitet</string>
<string name="image_quality_high">Høj kvalitet</string>
<string name="none">Ingen</string>
<string name="feed_show_watched">Set helt</string>
<string name="no_streams">Ingen streams</string>
<string name="feed_show_hide_streams">Vis/skjul streams</string>
<string name="brightness">Lysstyrke</string>
<string name="volume">Lydstyrke</string>
<string name="left_gesture_control_summary">Vælg bevægelse til venstre halvdel af afspillerens skærm</string>
<string name="right_gesture_control_summary">Vælg bevægelse til højre halvdel af afspillerens skærm</string>
<string name="right_gesture_control_title">Højre bevægelseshandling</string>
<string name="no_live_streams">Ingen live streams</string>
<string name="play_queue_audio_track">Lyd: %s</string>
<string name="audio_track">Lydspor</string>
<string name="remove_duplicates">Fjern duplikater</string>
<string name="feed_hide_streams_title">Vis følgende streams</string>
<string name="feed_fetch_channel_tabs">Hent kanal-faner</string>
<string name="feed_fetch_channel_tabs_summary">Faner, der skal hentes, når feedet opdateres. Denne indstilling har ingen effekt, hvis en kanal opdateres i hurtig tilstand.</string>
<string name="metadata_thumbnails">Miniaturebilleder</string>
<string name="select_audio_track_external_players">Vælg lydspor til eksterne afspillere</string>
<string name="unknown_audio_track">Ukendt</string>
<string name="feed_show_partially_watched">Delvist set</string>
<string name="feed_show_upcoming">Kommende</string>
<string name="audio_track_type_original">original</string>
<string name="channel_tab_videos">Videoer</string>
<string name="channel_tab_tracks">Numre</string>
<string name="channel_tab_livestreams">Live</string>
<string name="channel_tab_channels">Kanaler</string>
<string name="channel_tab_playlists">Playlister</string>
<string name="channel_tab_albums">Album</string>
<string name="channel_tab_about">Om</string>
<string name="show_channel_tabs">Kanal-faner</string>
<string name="show_channel_tabs_summary">Hvilke faner vises på kanalsiderne</string>
<string name="open_play_queue">Åbn afspilningskø</string>
<string name="toggle_fullscreen">Skift til fuld skærm</string>
<string name="toggle_screen_orientation">Skift skærmretning</string>
<string name="previous_stream">Forrige stream</string>
<string name="next_stream">Næste stream</string>
<string name="play">Afspil</string>
<string name="replay">Afspil igen</string>
<string name="duration">Varighed</string>
<string name="rewind">Spol tilbage</string>
<string name="image_quality_title">Billedkvalitet</string>
<string name="image_quality_none">Indlæs ikke billeder</string>
<string name="image_quality_low">Lav kvalitet</string>
<string name="share_playlist">Del playliste</string>
<string name="share_playlist_with_titles_message">Del playliste med detajler såsom playliste navn og videotitler eller som en simpel liste over video-URL\'er</string>
<string name="share_playlist_with_titles">Del med Titler</string>
<string name="share_playlist_with_list">Del URL-liste</string>
<plurals name="replies">
<item quantity="one">%s svar</item>
<item quantity="other">%s svar</item>
</plurals>
<string name="show_more">Vis mere</string>
<string name="show_less">Vis mindre</string>
<string name="progressive_load_interval_summary">Skift intervalstørrelsen for indlæsning af progressivt indhold (i øjeblikket %s). En lavere værdi kan fremskynde den første indlæsning</string>
<string name="remove_duplicates_message">Ønsker du at fjerne alle duplikerede streams i denne playliste?</string>
<string name="forward">Spol frem</string>
<string name="left_gesture_control_title">Venstre bevægelseshandling</string>
</resources>

View File

@ -99,12 +99,12 @@
<string name="show_higher_resolutions_summary">Nur manche Geräte können Videos in 2K/4K abspielen</string>
<string name="controls_background_title">Hintergrund</string>
<string name="controls_popup_title">Pop-up</string>
<string name="popup_remember_size_pos_title">Pop-up Eigenschaften merken</string>
<string name="popup_remember_size_pos_title">Eigenschaften des Pop-ups merken</string>
<string name="use_external_video_player_summary">Entfernt Tonspur bei manchen Auflösungen</string>
<string name="popup_remember_size_pos_summary">Letzte Größe und Position des Pop-ups merken</string>
<string name="show_search_suggestions_title">Suchvorschläge</string>
<string name="show_search_suggestions_summary">Vorschläge auswählen, die bei der Suche angezeigt werden sollen</string>
<string name="clear">Löschen</string>
<string name="clear">löschen</string>
<string name="best_resolution">Beste Auflösung</string>
<string name="title_activity_about">Über NewPipe</string>
<string name="tab_licenses">Lizenzen</string>
@ -178,10 +178,10 @@
<string name="player_recoverable_failure">Wiederherstellen nach einem Wiedergabefehler</string>
<string name="kiosk_page_summary">Kiosk-Seite</string>
<string name="select_a_kiosk">Kiosk auswählen</string>
<string name="show_hold_to_append_summary">Tipp anzeigen, wenn der Hintergrundwiedergabe- oder Pop-up-Knopf „Details:“ im Video gedrückt wird</string>
<string name="show_hold_to_append_summary">Tipp anzeigen, wenn die Hintergrundwiedergabe- oder Pop-up-Schaltfläche „Details:“ im Video gedrückt wird</string>
<string name="new_and_hot">Neu und Heiß</string>
<string name="hold_to_append">Halten, um zur Wiedergabeliste hinzuzufügen</string>
<string name="show_hold_to_append_title">„Halten zum Einreihen“ Tipp anzeigen</string>
<string name="show_hold_to_append_title">„Halten zum Einreihen“-Tipp anzeigen</string>
<string name="unknown_content">[Unbekannt]</string>
<string name="start_here_on_background">Wiedergabe im Hintergrund starten</string>
<string name="start_here_on_popup">Wiedergabe in einem Pop-up starten</string>
@ -425,7 +425,7 @@
<string name="no_one_watching">Niemand schaut zu</string>
<plurals name="watching">
<item quantity="one">%s Zuschauer</item>
<item quantity="other">%s Zuschauer</item>
<item quantity="other">%s Zuschauende</item>
</plurals>
<string name="no_one_listening">Niemand hört zu</string>
<plurals name="listening">
@ -541,7 +541,7 @@
<string name="wifi_only">Nur über WLAN</string>
<string name="never">Nie</string>
<string name="notification_actions_at_most_three">Du kannst maximal drei Aktionen auswählen, die in der Kompaktbenachrichtigung angezeigt werden sollen!</string>
<string name="notification_actions_summary">Bearbeite jede Benachrichtigungsaktion unten, indem du darauf tippst. Wähle mithilfe der Kontrollkästchen rechts bis zu drei aus, die in der Kompaktbenachrichtigung angezeigt werden sollen</string>
<string name="notification_actions_summary">Bearbeite jede Benachrichtigungsaktion unten, indem du auf sie tippst. Wähle mithilfe der Kontrollkästchen rechts bis zu drei aus, die in der Kompaktbenachrichtigung angezeigt werden sollen.</string>
<string name="unsupported_url_dialog_message">Konnte die angegebene URL nicht erkennen. Mit einer anderen Anwendung öffnen\?</string>
<string name="notification_action_4_title">Fünfte Aktionstaste</string>
<string name="notification_action_3_title">Vierte Aktionstaste</string>
@ -806,4 +806,11 @@
<string name="share_playlist">Wiedergabeliste teilen</string>
<string name="share_playlist_with_titles_message">Teile die Wiedergabeliste mit Details wie dem Namen der Wiedergabeliste und den Videotiteln oder als einfache Liste von Video-URLs</string>
<string name="video_details_list_item">- %1$s: %2$s</string>
<plurals name="replies">
<item quantity="one">%s Antwort</item>
<item quantity="other">%s Antworten</item>
</plurals>
<string name="show_more">Mehr zeigen</string>
<string name="show_less">Weniger zeigen</string>
<string name="notification_actions_summary_android13">Bearbeite jede Benachrichtigungsaktion unten, indem du auf sie tippst. Die ersten drei Aktionen (Abspielen/Pause, Zurück und Weiter) sind vom System vorgegeben und können nicht angepasst werden.</string>
</resources>

View File

@ -482,7 +482,7 @@
<string name="notification_action_shuffle">Ανάμιξη</string>
<string name="notification_action_repeat">Επανάληψη</string>
<string name="notification_actions_at_most_three">Μπορείτε να επιλέξετε το πολύ τρεις ενέργειες για εμφάνιση στη σύντομη ειδοποίηση!</string>
<string name="notification_actions_summary">Επεξεργαστείτε κάθε ενέργεια ειδοποίησης παρακάτω πατώντας πάνω της. Επιλέξτε έως και τρεις από αυτές για να εμφανίζονται στη σύντομη ειδοποίηση, χρησιμοποιώντας τα πλαίσια ελέγχου στα δεξιά</string>
<string name="notification_actions_summary">Επεξεργαστείτε κάθε ενέργεια ειδοποίησης παρακάτω πατώντας πάνω της. Επιλέξτε έως και τρεις από αυτές για να εμφανίζονται στη σύντομη ειδοποίηση, χρησιμοποιώντας τα πλαίσια ελέγχου στα δεξιά.</string>
<string name="notification_action_4_title">Κουμπί πέμπτης ενέργειας</string>
<string name="notification_action_3_title">Κουμπί τέταρτης ενέργειας</string>
<string name="notification_action_2_title">Κουμπί τρίτης ενέργειας</string>
@ -595,7 +595,7 @@
<string name="auto_device_theme_title">Αυτόματο (θέμα συσκευής)</string>
<string name="night_theme_title">Νυχτερινό θέμα</string>
<string name="show_channel_details">Εμφάνιση λεπτομερειών καναλιού</string>
<string name="disable_media_tunneling_summary">Απενεργοποιήστε το media tunneling, αν εμφανίζεται μαύρη οθόνη ή διακοπτόμενος ήχος κατά την αναπαραγωγή βίντεο</string>
<string name="disable_media_tunneling_summary">Απενεργοποιήστε το media tunneling, αν παρατηρείτε μαύρη οθόνη ή διακοπές κατά την αναπαραγωγή βίντεο.</string>
<string name="disable_media_tunneling_title">Απενεργοποίηση media tunneling</string>
<string name="metadata_privacy_internal">Εσωτερικό</string>
<string name="metadata_privacy_private">Ιδιωτικό</string>
@ -767,7 +767,7 @@
<string name="metadata_subscribers">Συνδρομητές</string>
<string name="show_channel_tabs_summary">Ποιες καρτέλες εμφανίζονται στις σελίδες των καναλιών</string>
<string name="show_channel_tabs">Καρτέλες καναλιών</string>
<string name="channel_tab_shorts">Shorts</string>
<string name="channel_tab_shorts">Σύντομα</string>
<string name="feed_fetch_channel_tabs">Λήψη καρτελών καναλιών</string>
<string name="channel_tab_about">Σχετικά</string>
<string name="channel_tab_albums">Άλμπουμ</string>
@ -806,4 +806,11 @@
<string name="share_playlist">Κοινοποίηση λίστας</string>
<string name="share_playlist_with_titles_message">Μοιραστείτε τη λίστα αναπαραγωγής με λεπτομέρειες όπως το όνομα της λίστας αναπαραγωγής και τους τίτλους βίντεο ή ως μια απλή λίστα διευθύνσεων URL βίντεο</string>
<string name="video_details_list_item">- %1$s: %2$s</string>
<plurals name="replies">
<item quantity="one">%s απάντηση</item>
<item quantity="other">%s απαντήσεις</item>
</plurals>
<string name="show_more">Εμφάνιση περισσοτέρων</string>
<string name="show_less">Εμφάνιση λιγότερων</string>
<string name="notification_actions_summary_android13">Επεξεργαστείτε κάθε ενέργεια ειδοποίησης παρακάτω πατώντας σε αυτήν. Οι τρεις πρώτες ενέργειες (αναπαραγωγή/παύση, προηγούμενηο και επόμενο) ορίζονται από το σύστημα και δεν μπορούν να τροποποιηθούν.</string>
</resources>

View File

@ -111,8 +111,8 @@
<string name="enable_search_history_summary">Konservi la historio de serĉo lokale</string>
<string name="enable_watch_history_title">Rigardu historion</string>
<string name="enable_watch_history_summary">Spuri la viditajn filmetojn</string>
<string name="notification_channel_name">NewPipe Sciigo</string>
<string name="notification_channel_description">Sciigoj por NewPipe fonaj kaj ŝprucfenestraj ludiloj</string>
<string name="notification_channel_name">Sciigo de NewPipe</string>
<string name="notification_channel_description">Sciigoj por ludilo de NewPipe</string>
<string name="settings_category_player_title">Ludilo</string>
<string name="settings_category_player_behavior_title">Konduto</string>
<string name="settings_category_history_title">Historio kaj kaŝmemoro</string>
@ -201,8 +201,8 @@
<string name="tab_choose">Elektu ongleton</string>
<string name="settings_category_updates_title">Ĝisdatigoj</string>
<string name="file_deleted">Dosiero forviŝita</string>
<string name="app_update_notification_channel_name">Sciigo por ĝisdatigi apon</string>
<string name="app_update_notification_channel_description">Sciigo por nova versio de NewPipe</string>
<string name="app_update_notification_channel_name">Sciigo por ĝisdatigo de apo</string>
<string name="app_update_notification_channel_description">Sciigo por novaj versioj de NewPipe</string>
<string name="download_to_sdcard_error_title">Ekstera konservejo malhavebla</string>
<string name="download_to_sdcard_error_message">Elŝuti al ekstera SD-karto ne eblas. Ĉu vi volas restarigi la elŝutan dosierujon \?</string>
<string name="queued">viciĝita</string>
@ -546,4 +546,69 @@
<string name="crash_the_player">Kraŝi la ludilo</string>
<string name="enqueued">Envicigita</string>
<string name="enqueue_stream">Envicigi</string>
<string name="volume">Laŭteco</string>
<string name="none">Neniu</string>
<string name="notification_colorize_summary">Permesi al Android agordi koloron de sciigo laŭ la precipa koloro de videaĵminiaturo (noti, ke ĉi tio ne disponeblas en ĉiuj iloj)</string>
<string name="auto_queue_toggle">Aŭtomata vicigado</string>
<string name="right_gesture_control_title">Ago de dekstra gesto</string>
<string name="notification_actions_summary">Redakti ĉiun agon de sciigo per tuŝi gin. Elekti maksimume tri agon por montri en la kompakta sciigo per markobutonoj dekstre.</string>
<string name="prefer_original_audio_summary">Elekti la originalan aŭdiotrakon malgraŭ lingvo</string>
<string name="prefer_descriptive_audio_summary">Elekti aŭdiotrakon kun priskriboj por vidmalkapabluloj kiam ebla</string>
<string name="left_gesture_control_title">Ago de maldekstra gesto</string>
<string name="prefer_descriptive_audio_title">Preferi priskribajn aŭdiotrakojn</string>
<string name="progressive_load_interval_summary">Ŝangi la grandecon de elŝuta intervalo por progresiva enhavo (aktuale %s). Malplia valoro eble povas rapidigi ĝian komencan ŝargadon</string>
<string name="prefer_original_audio_title">Preferi originalan aŭdaĵon</string>
<string name="right_gesture_control_summary">Elekti geston por dekstra duono de ludil-ekrano</string>
<string name="ignore_hardware_media_buttons_summary">Utila, ekzemple, se vi uzas kaptelefonon, kiu havas difektajn fizikajn butonojn</string>
<string name="progressive_load_interval_title">Grandeco de intervalo de legada elŝuto</string>
<string name="brightness">Heleco</string>
<string name="notification_actions_at_most_three">Vi povas elekti maksimume tri agoj por montri en la kompakta sciigo!</string>
<string name="ignore_hardware_media_buttons_title">Ignori eventoj de aparataroj plurmediaj butonoj</string>
<string name="left_gesture_control_summary">Elekti geston por maldekstra duono de ludil-ekrano</string>
<string name="youtube_restricted_mode_enabled_title">Ŝalti \"Limigitan Reĝimon\" de YouTube</string>
<string name="fast_mode">Rapida reĝimo</string>
<string name="unsupported_url_dialog_message">Ne eblas rekoni la ligilon. Ĉu malfermi per alia apo\?</string>
<string name="recaptcha_cookies_cleared">Kuketojn de reCAPTCHA estis forigita</string>
<string name="show_age_restricted_content_summary">Montri enhavon, kiu eble maltaŭgas por infanoj, ĉar ĝi havas aĝo-limon (kiel \"18+\")</string>
<string name="streams_notification_channel_name">Novaj fluoj</string>
<string name="remote_search_suggestions">Foraj serĉsugestoj</string>
<string name="error_report_channel_description">Sciigoj por raporti erarojn</string>
<string name="loading_metadata_title">Ŝargante metadatumoj…</string>
<string name="hash_channel_description">Sciigo por kreado de haketaĵoj de videoj</string>
<string name="youtube_restricted_mode_enabled_summary">YouTube provizas \"Limigitan Reĝimon\", kiu kaŝas enhavon, kiu potence maltaŭgas por infanoj</string>
<string name="restricted_video_no_stream">Ĉi tiu video estas aĝo-limigita.
\nPro novaj reguloj de YouTube, kiuj aplikas al aĝo-limigitaj videoj, NewPipe ne povas atingi iun ajn video-fluoj de ĉi tiu video kaj konsekvence ne povas ludi ĝin.</string>
<string name="error_report_channel_name">Sciigo por erar-raportoj</string>
<string name="notifications">Sciigoj</string>
<string name="settings_category_player_notification_title">Ludila sciigo</string>
<string name="hash_channel_name">Sciigo por haketado de videoj</string>
<string name="local_search_suggestions">Lokaj serĉsugestoj</string>
<string name="start_main_player_fullscreen_title">Ŝalti ĉefan ludilon plenekrane</string>
<string name="streams_notification_channel_description">Sciigo por novaj fluoj de abonoj</string>
<string name="clear_cookie_title">Forigi kuketojn de reCAPTCHA</string>
<string name="settings_category_player_notification_summary">Agordi la sciigon por ĉi-momente ludantaj datumtorentoj</string>
<string name="start_main_player_fullscreen_summary">Ne komenci ludi videojn en la mini-ludilo, sed ŝalti plenekranan reĝimon rekte, se aŭtomata rotacio ŝlositas. Vi ankoraŭ povus atingi mini-ludilon, se vi elirus plenekranan reĝimon.</string>
<string name="clear_cookie_summary">Forigi kuketojn, kiujn NewPipe konservas, kiam vi solvas reCAPTCHA-taskojn</string>
<string name="error_report_notification_title">NewPipe renkontis eraron, tuŝi por raporti</string>
<string name="main_tabs_position_title">Pozicio de la ĉefaj langetoj</string>
<string name="main_tabs_position_summary">Transloki la ĉefan langet-elektilon al la malsupro</string>
<plurals name="new_streams">
<item quantity="one">%s nova fluo</item>
<item quantity="other">%s novaj fluoj</item>
</plurals>
<string name="error_report_open_issue_button_text">Raporti per GitHub</string>
<string name="comments_are_disabled">Komentoj malŝaltitas</string>
<string name="error_report_notification_toast">Eraro okazis, vidu sciigon</string>
<string name="no_live_streams">Neniuj tujelsendoj</string>
<string name="no_streams">Neniuj fluoj</string>
<string name="copy_for_github">Kopii formatitan raporton</string>
<string name="import_subscriptions_hint">Importi aŭ eksporti abonojn per la tri-punkta menuo</string>
<string name="msg_calculating_hash">Kalkulado de haketaĵo</string>
<string name="faq_title">Oftaj demandoj</string>
<string name="no_dir_yet">Neniu dosierujo por elŝutoj agordita, bonvolu elekti la defaŭltan elŝuto-dosierujon nun</string>
<string name="error_report_open_github_notice">Bonvolu certigi, ĉu erarraporto, kiu diskutas pri via eraro, jam ekzistas. Kreado de duoblaĵaj erarraportoj forprenas tempon el ni, kiun ni povus uzi por ripari la veran eraron.</string>
<string name="related_items_tab_description">Rilatajn erojn</string>
<string name="recaptcha_solve">Solvi</string>
<string name="msg_failed_to_copy">Malsukcesis kopii al la tondujo</string>
<string name="downloads_storage_ask_summary_no_saf_notice">Oni petos al vi kien salvi ĉiujn elŝutojn</string>
</resources>

View File

@ -47,7 +47,7 @@
<string name="detail_uploader_thumbnail_view_description">Miniatura del avatar del usuario</string>
<string name="content">Contenido</string>
<string name="show_age_restricted_content_title">Mostrar contenido con restricción de edad</string>
<string name="main_bg_subtitle">Pulsa la lupa para empezar.</string>
<string name="main_bg_subtitle">Toca la lupa para empezar.</string>
<string name="duration_live">En directo</string>
<string name="downloads">Descargas</string>
<string name="downloads_title">Descargas</string>
@ -558,7 +558,7 @@
<string name="notification_action_buffering">Almacenar en memoria (búfer)</string>
<string name="notification_action_repeat">Repetir</string>
<string name="notification_actions_at_most_three">¡Puedes seleccionar como máximo tres acciones para mostrar en la notificación compacta!</string>
<string name="notification_actions_summary">Edite cada una de las acciones de notificación que aparecen a continuación pulsando sobre ellas. Seleccione hasta tres de ellas para que se muestren en la notificación compacta utilizando las casillas de verificación de la derecha</string>
<string name="notification_actions_summary">Edite cada acción de notificación pulsando sobre ella. Seleccione hasta tres de ellas para que se muestren en la notificación compacta utilizando las casillas de verificación de la derecha.</string>
<string name="notification_action_4_title">Botón de quinta acción</string>
<string name="notification_action_3_title">Botón de cuarta acción</string>
<string name="notification_action_2_title">Botón de tercera acción</string>
@ -822,4 +822,12 @@
<string name="share_playlist">Compartir la lista de reproducción</string>
<string name="share_playlist_with_titles_message">Compartir las listas de reproducción con los detalles como el nombre de la lista y los títulos de los vídeos o como una simple lista de una dirección URL con los vídeos</string>
<string name="video_details_list_item">- %1$s: %2$s</string>
<plurals name="replies">
<item quantity="one">%s respuesta</item>
<item quantity="many">%s respuestas</item>
<item quantity="other">%s respuestas</item>
</plurals>
<string name="show_more">Ver más</string>
<string name="show_less">Mostrar menos</string>
<string name="notification_actions_summary_android13">Edite cada acción de notificación pulsando sobre ella. Las tres primeras acciones (reproducir/pausa, anterior y siguiente) las establece el sistema y no se pueden personalizar.</string>
</resources>

View File

@ -411,7 +411,7 @@
<string name="notification_action_shuffle">Aja segi</string>
<string name="notification_action_repeat">Korda</string>
<string name="notification_actions_at_most_three">Sa saad valida kuni kolm tegevust, mida kuvatakse lühiteavituses!</string>
<string name="notification_actions_summary">Muuda iga teavituse tegevusi sellel toksates. Vali märkekastides paremal kuni kolm teavitust, mida kuvada lühiteates</string>
<string name="notification_actions_summary">Muuda iga teavituse tegevusi sellel toksates. Vali märkekastides paremal kuni kolm teavitust, mida kuvada lühiteates.</string>
<string name="notification_action_4_title">Viies tegevusnupp</string>
<string name="notification_action_3_title">Neljas tegevusnupp</string>
<string name="notification_action_2_title">Kolmas tegevusnupp</string>
@ -806,4 +806,11 @@
<string name="share_playlist">Jaga esitusloendit</string>
<string name="share_playlist_with_titles_message">Jaga esitusloendit kas väga detailse teabega palade kohta või lihtsa url\'ide loendina</string>
<string name="video_details_list_item">- %1$s: %2$s</string>
<string name="show_more">Näita veel</string>
<plurals name="replies">
<item quantity="one">%s vastus</item>
<item quantity="other">%s vastust</item>
</plurals>
<string name="show_less">Näita vähem</string>
<string name="notification_actions_summary_android13">Muuda iga teavituse tegevust sellel toksates. Kolm esimest tegevust (esita/peata esitus, eelmine video, järgmine video) on süsteemsed ja neid ei saa muuta.</string>
</resources>

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