mirror of
https://github.com/TeamNewPipe/NewPipe
synced 2024-12-23 00:20:32 +00:00
fetch and merge
This commit is contained in:
commit
47798febed
1
.github/FUNDING.yml
vendored
1
.github/FUNDING.yml
vendored
@ -1 +1,2 @@
|
||||
liberapay: TeamNewPipe
|
||||
custom: 'https://newpipe.net/donate/'
|
||||
|
8
.github/workflows/ci.yml
vendored
8
.github/workflows/ci.yml
vendored
@ -52,6 +52,7 @@ jobs:
|
||||
test-android:
|
||||
# macos has hardware acceleration. See android-emulator-runner action
|
||||
runs-on: macos-latest
|
||||
timeout-minutes: 20
|
||||
strategy:
|
||||
matrix:
|
||||
# api-level 19 is min sdk, but throws errors related to desugaring
|
||||
@ -73,6 +74,13 @@ jobs:
|
||||
# workaround to emulator bug: https://github.com/ReactiveCircus/android-emulator-runner/issues/160
|
||||
emulator-build: 7425822
|
||||
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@v2
|
||||
if: failure()
|
||||
with:
|
||||
name: android-test-report-api${{ matrix.api-level }}
|
||||
path: app/build/reports/androidTests/connected/**
|
||||
|
||||
sonar:
|
||||
runs-on: ubuntu-latest
|
||||
|
4
.gitignore
vendored
4
.gitignore
vendored
@ -8,8 +8,8 @@ captures/
|
||||
*~
|
||||
.weblate
|
||||
*.class
|
||||
**/debug/
|
||||
**/release/
|
||||
app/debug/
|
||||
app/release/
|
||||
|
||||
# vscode / eclipse files
|
||||
*.classpath
|
||||
|
@ -17,7 +17,7 @@
|
||||
<p align="center"><a href="https://newpipe.net">Website</a> • <a href="https://newpipe.net/blog/">Blog</a> • <a href="https://newpipe.net/FAQ/">FAQ</a> • <a href="https://newpipe.net/press/">Press</a></p>
|
||||
<hr>
|
||||
|
||||
*Read this in other languages: [English](README.md), [Español](README.es.md), [한국어](README.ko.md), [Soomaali](README.so.md), [Português Brasil](README.pt_BR.md), [日本語](README.ja.md), [Română](README.ro.md), [Türkçe](README.tr.md).*
|
||||
*Read this in other languages: [English](README.md), [Español](doc/README.es.md), [한국어](doc/README.ko.md), [Soomaali](doc/README.so.md), [Português Brasil](doc/README.pt_BR.md), [Polski](doc/README.pl.md), [日本語](doc/README.ja.md), [Română](doc/README.ro.md), [Türkçe](doc/README.tr.md), [正體中文](doc/README.zh_TW.md).*
|
||||
|
||||
<b>WARNING: THIS IS A BETA VERSION, THEREFORE YOU MAY ENCOUNTER BUGS. IF YOU DO, OPEN AN ISSUE VIA OUR GITHUB REPOSITORY.</b>
|
||||
|
||||
|
@ -8,16 +8,16 @@ plugins {
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdkVersion 30
|
||||
compileSdk 30
|
||||
buildToolsVersion '30.0.3'
|
||||
|
||||
defaultConfig {
|
||||
applicationId "org.schabi.newpipe"
|
||||
resValue "string", "app_name", "NewPipe"
|
||||
minSdkVersion 19
|
||||
targetSdkVersion 29
|
||||
versionCode 981
|
||||
versionName "0.21.15"
|
||||
minSdk 19
|
||||
targetSdk 29
|
||||
versionCode 982
|
||||
versionName "0.21.16"
|
||||
|
||||
multiDexEnabled true
|
||||
|
||||
@ -65,7 +65,7 @@ android {
|
||||
}
|
||||
}
|
||||
|
||||
lintOptions {
|
||||
lint {
|
||||
checkReleaseBuilds false
|
||||
// Or, if you prefer, you can continue to check for errors in release builds,
|
||||
// but continue the build even when errors are found:
|
||||
@ -98,7 +98,7 @@ android {
|
||||
}
|
||||
|
||||
ext {
|
||||
checkstyleVersion = '9.2'
|
||||
checkstyleVersion = '9.2.1'
|
||||
|
||||
androidxLifecycleVersion = '2.3.1'
|
||||
androidxRoomVersion = '2.3.0'
|
||||
@ -112,6 +112,7 @@ ext {
|
||||
leakCanaryVersion = '2.5'
|
||||
stethoVersion = '1.6.0'
|
||||
mockitoVersion = '4.0.0'
|
||||
assertJVersion = '3.22.0'
|
||||
}
|
||||
|
||||
configurations {
|
||||
@ -188,7 +189,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:v0.21.12'
|
||||
implementation 'com.github.TeamNewPipe:NewPipeExtractor:65129e6'
|
||||
|
||||
/** Checkstyle **/
|
||||
checkstyle "com.puppycrawl.tools:checkstyle:${checkstyleVersion}"
|
||||
@ -290,6 +291,7 @@ dependencies {
|
||||
androidTestImplementation "androidx.test.ext:junit:1.1.3"
|
||||
androidTestImplementation "androidx.test:runner:1.4.0"
|
||||
androidTestImplementation "androidx.room:room-testing:${androidxRoomVersion}"
|
||||
androidTestImplementation "org.assertj:assertj-core:${assertJVersion}"
|
||||
}
|
||||
|
||||
static String getGitWorkingBranch() {
|
||||
|
@ -1,19 +1,19 @@
|
||||
package org.schabi.newpipe.local.history
|
||||
|
||||
import androidx.test.core.app.ApplicationProvider
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.junit.After
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.rules.Timeout
|
||||
import org.schabi.newpipe.database.AppDatabase
|
||||
import org.schabi.newpipe.database.history.model.SearchHistoryEntry
|
||||
import org.schabi.newpipe.testUtil.TestDatabase
|
||||
import org.schabi.newpipe.testUtil.TrampolineSchedulerRule
|
||||
import java.time.LocalDateTime
|
||||
import java.time.OffsetDateTime
|
||||
import java.util.concurrent.TimeUnit
|
||||
import java.time.ZoneOffset
|
||||
|
||||
class HistoryRecordManagerTest {
|
||||
|
||||
@ -23,9 +23,6 @@ class HistoryRecordManagerTest {
|
||||
@get:Rule
|
||||
val trampolineScheduler = TrampolineSchedulerRule()
|
||||
|
||||
@get:Rule
|
||||
val timeout = Timeout(1, TimeUnit.SECONDS)
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
database = TestDatabase.createReplacingNewPipeDatabase()
|
||||
@ -45,109 +42,137 @@ class HistoryRecordManagerTest {
|
||||
// that the number of Lists it returns is exactly 1, we can only check if the first List is
|
||||
// correct. Why on earth has a Flowable been used instead of a Single for getAll()?!?
|
||||
val entities = database.searchHistoryDAO().all.blockingFirst()
|
||||
assertEquals(1, entities.size)
|
||||
assertEquals(1, entities[0].id)
|
||||
assertEquals(0, entities[0].serviceId)
|
||||
assertEquals("Hello", entities[0].search)
|
||||
assertThat(entities).hasSize(1)
|
||||
assertThat(entities[0].id).isEqualTo(1)
|
||||
assertThat(entities[0].serviceId).isEqualTo(0)
|
||||
assertThat(entities[0].search).isEqualTo("Hello")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun deleteSearchHistory() {
|
||||
val entries = listOf(
|
||||
SearchHistoryEntry(OffsetDateTime.now(), 0, "A"),
|
||||
SearchHistoryEntry(OffsetDateTime.now(), 2, "A"),
|
||||
SearchHistoryEntry(OffsetDateTime.now(), 1, "B"),
|
||||
SearchHistoryEntry(OffsetDateTime.now(), 0, "B"),
|
||||
SearchHistoryEntry(time.minusSeconds(1), 0, "A"),
|
||||
SearchHistoryEntry(time.minusSeconds(2), 2, "A"),
|
||||
SearchHistoryEntry(time.minusSeconds(3), 1, "B"),
|
||||
SearchHistoryEntry(time.minusSeconds(4), 0, "B"),
|
||||
)
|
||||
|
||||
// make sure all 4 were inserted
|
||||
database.searchHistoryDAO().insertAll(entries)
|
||||
assertEquals(entries.size, database.searchHistoryDAO().all.blockingFirst().size)
|
||||
assertThat(database.searchHistoryDAO().all.blockingFirst()).hasSameSizeAs(entries)
|
||||
|
||||
// try to delete only "A" entries, "B" entries should be untouched
|
||||
manager.deleteSearchHistory("A").test().await().assertValue(2)
|
||||
val entities = database.searchHistoryDAO().all.blockingFirst()
|
||||
assertEquals(2, entities.size)
|
||||
assertTrue(entries[2].hasEqualValues(entities[0]))
|
||||
assertTrue(entries[3].hasEqualValues(entities[1]))
|
||||
assertThat(entities).hasSize(2)
|
||||
assertThat(entities).usingElementComparator { o1, o2 -> if (o1.hasEqualValues(o2)) 0 else 1 }
|
||||
.containsExactly(*entries.subList(2, 4).toTypedArray())
|
||||
|
||||
// assert that nothing happens if we delete a search query that does exist in the db
|
||||
manager.deleteSearchHistory("A").test().await().assertValue(0)
|
||||
val entities2 = database.searchHistoryDAO().all.blockingFirst()
|
||||
assertEquals(2, entities2.size)
|
||||
assertTrue(entries[2].hasEqualValues(entities2[0]))
|
||||
assertTrue(entries[3].hasEqualValues(entities2[1]))
|
||||
assertThat(entities2).hasSize(2)
|
||||
assertThat(entities2).usingElementComparator { o1, o2 -> if (o1.hasEqualValues(o2)) 0 else 1 }
|
||||
.containsExactly(*entries.subList(2, 4).toTypedArray())
|
||||
|
||||
// delete all remaining entries
|
||||
manager.deleteSearchHistory("B").test().await().assertValue(2)
|
||||
assertEquals(0, database.searchHistoryDAO().all.blockingFirst().size)
|
||||
assertThat(database.searchHistoryDAO().all.blockingFirst()).isEmpty()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun deleteCompleteSearchHistory() {
|
||||
val entries = listOf(
|
||||
SearchHistoryEntry(OffsetDateTime.now(), 1, "A"),
|
||||
SearchHistoryEntry(OffsetDateTime.now(), 2, "B"),
|
||||
SearchHistoryEntry(OffsetDateTime.now(), 0, "C"),
|
||||
SearchHistoryEntry(time.minusSeconds(1), 1, "A"),
|
||||
SearchHistoryEntry(time.minusSeconds(2), 2, "B"),
|
||||
SearchHistoryEntry(time.minusSeconds(3), 0, "C"),
|
||||
)
|
||||
|
||||
// make sure all 3 were inserted
|
||||
database.searchHistoryDAO().insertAll(entries)
|
||||
assertEquals(entries.size, database.searchHistoryDAO().all.blockingFirst().size)
|
||||
assertThat(database.searchHistoryDAO().all.blockingFirst()).hasSameSizeAs(entries)
|
||||
|
||||
// should remove everything
|
||||
manager.deleteCompleteSearchHistory().test().await().assertValue(entries.size)
|
||||
assertEquals(0, database.searchHistoryDAO().all.blockingFirst().size)
|
||||
assertThat(database.searchHistoryDAO().all.blockingFirst()).isEmpty()
|
||||
}
|
||||
|
||||
private fun insertShuffledRelatedSearches(relatedSearches: Collection<SearchHistoryEntry>) {
|
||||
|
||||
// shuffle to make sure the order of items returned by queries depends only on
|
||||
// SearchHistoryEntry.creationDate, not on the actual insertion time, so that we can
|
||||
// verify that the `ORDER BY` clause does its job
|
||||
database.searchHistoryDAO().insertAll(relatedSearches.shuffled())
|
||||
|
||||
// make sure all entries were inserted
|
||||
assertEquals(
|
||||
relatedSearches.size,
|
||||
database.searchHistoryDAO().all.blockingFirst().size
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun getRelatedSearches_emptyQuery() {
|
||||
// make sure all entries were inserted
|
||||
database.searchHistoryDAO().insertAll(RELATED_SEARCHES_ENTRIES)
|
||||
assertEquals(
|
||||
RELATED_SEARCHES_ENTRIES.size,
|
||||
database.searchHistoryDAO().all.blockingFirst().size
|
||||
)
|
||||
insertShuffledRelatedSearches(RELATED_SEARCHES_ENTRIES)
|
||||
|
||||
// make sure correct number of searches is returned and in correct order
|
||||
val searches = manager.getRelatedSearches("", 6, 4).blockingFirst()
|
||||
assertEquals(4, searches.size)
|
||||
assertEquals(RELATED_SEARCHES_ENTRIES[6].search, searches[0]) // A (even if in two places)
|
||||
assertEquals(RELATED_SEARCHES_ENTRIES[4].search, searches[1]) // B
|
||||
assertEquals(RELATED_SEARCHES_ENTRIES[5].search, searches[2]) // AA
|
||||
assertEquals(RELATED_SEARCHES_ENTRIES[2].search, searches[3]) // BA
|
||||
assertThat(searches).containsExactly(
|
||||
RELATED_SEARCHES_ENTRIES[6].search, // A (even if in two places)
|
||||
RELATED_SEARCHES_ENTRIES[4].search, // B
|
||||
RELATED_SEARCHES_ENTRIES[5].search, // AA
|
||||
RELATED_SEARCHES_ENTRIES[2].search, // BA
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun getRelatedSearches_emptyQuery_manyDuplicates() {
|
||||
insertShuffledRelatedSearches(
|
||||
listOf(
|
||||
SearchHistoryEntry(time.minusSeconds(9), 3, "A"),
|
||||
SearchHistoryEntry(time.minusSeconds(8), 3, "AB"),
|
||||
SearchHistoryEntry(time.minusSeconds(7), 3, "A"),
|
||||
SearchHistoryEntry(time.minusSeconds(6), 3, "A"),
|
||||
SearchHistoryEntry(time.minusSeconds(5), 3, "BA"),
|
||||
SearchHistoryEntry(time.minusSeconds(4), 3, "A"),
|
||||
SearchHistoryEntry(time.minusSeconds(3), 3, "A"),
|
||||
SearchHistoryEntry(time.minusSeconds(2), 0, "A"),
|
||||
SearchHistoryEntry(time.minusSeconds(1), 2, "AA"),
|
||||
)
|
||||
)
|
||||
|
||||
val searches = manager.getRelatedSearches("", 9, 3).blockingFirst()
|
||||
assertThat(searches).containsExactly("AA", "A", "BA")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun getRelatedSearched_nonEmptyQuery() {
|
||||
// make sure all entries were inserted
|
||||
database.searchHistoryDAO().insertAll(RELATED_SEARCHES_ENTRIES)
|
||||
assertEquals(
|
||||
RELATED_SEARCHES_ENTRIES.size,
|
||||
database.searchHistoryDAO().all.blockingFirst().size
|
||||
)
|
||||
insertShuffledRelatedSearches(RELATED_SEARCHES_ENTRIES)
|
||||
|
||||
// make sure correct number of searches is returned and in correct order
|
||||
val searches = manager.getRelatedSearches("A", 3, 5).blockingFirst()
|
||||
assertEquals(3, searches.size)
|
||||
assertEquals(RELATED_SEARCHES_ENTRIES[6].search, searches[0]) // A (even if in two places)
|
||||
assertEquals(RELATED_SEARCHES_ENTRIES[5].search, searches[1]) // AA
|
||||
assertEquals(RELATED_SEARCHES_ENTRIES[1].search, searches[2]) // BA
|
||||
assertThat(searches).containsExactly(
|
||||
RELATED_SEARCHES_ENTRIES[6].search, // A (even if in two places)
|
||||
RELATED_SEARCHES_ENTRIES[5].search, // AA
|
||||
RELATED_SEARCHES_ENTRIES[1].search, // BA
|
||||
)
|
||||
|
||||
// also make sure that the string comparison is case insensitive
|
||||
val searches2 = manager.getRelatedSearches("a", 3, 5).blockingFirst()
|
||||
assertEquals(searches, searches2)
|
||||
assertThat(searches).isEqualTo(searches2)
|
||||
}
|
||||
|
||||
companion object {
|
||||
val RELATED_SEARCHES_ENTRIES = listOf(
|
||||
SearchHistoryEntry(OffsetDateTime.now().minusSeconds(7), 2, "AC"),
|
||||
SearchHistoryEntry(OffsetDateTime.now().minusSeconds(6), 0, "ABC"),
|
||||
SearchHistoryEntry(OffsetDateTime.now().minusSeconds(5), 1, "BA"),
|
||||
SearchHistoryEntry(OffsetDateTime.now().minusSeconds(4), 3, "A"),
|
||||
SearchHistoryEntry(OffsetDateTime.now().minusSeconds(2), 0, "B"),
|
||||
SearchHistoryEntry(OffsetDateTime.now().minusSeconds(3), 2, "AA"),
|
||||
SearchHistoryEntry(OffsetDateTime.now().minusSeconds(1), 1, "A"),
|
||||
private val time = OffsetDateTime.of(LocalDateTime.of(2000, 1, 1, 1, 1), ZoneOffset.UTC)
|
||||
|
||||
private val RELATED_SEARCHES_ENTRIES = listOf(
|
||||
SearchHistoryEntry(time.minusSeconds(7), 2, "AC"),
|
||||
SearchHistoryEntry(time.minusSeconds(6), 0, "ABC"),
|
||||
SearchHistoryEntry(time.minusSeconds(5), 1, "BA"),
|
||||
SearchHistoryEntry(time.minusSeconds(4), 3, "A"),
|
||||
SearchHistoryEntry(time.minusSeconds(2), 0, "B"),
|
||||
SearchHistoryEntry(time.minusSeconds(3), 2, "AA"),
|
||||
SearchHistoryEntry(time.minusSeconds(1), 1, "A"),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -4,13 +4,11 @@ import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.rules.Timeout
|
||||
import org.schabi.newpipe.database.AppDatabase
|
||||
import org.schabi.newpipe.database.stream.model.StreamEntity
|
||||
import org.schabi.newpipe.extractor.stream.StreamType
|
||||
import org.schabi.newpipe.testUtil.TestDatabase
|
||||
import org.schabi.newpipe.testUtil.TrampolineSchedulerRule
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class LocalPlaylistManagerTest {
|
||||
|
||||
@ -20,9 +18,6 @@ class LocalPlaylistManagerTest {
|
||||
@get:Rule
|
||||
val trampolineScheduler = TrampolineSchedulerRule()
|
||||
|
||||
@get:Rule
|
||||
val timeout = Timeout(1, TimeUnit.SECONDS)
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
database = TestDatabase.createReplacingNewPipeDatabase()
|
||||
|
@ -0,0 +1,20 @@
|
||||
package org.schabi.newpipe.settings;
|
||||
|
||||
import android.content.Intent;
|
||||
|
||||
import leakcanary.LeakCanary;
|
||||
|
||||
/**
|
||||
* Build variant dependent (BVD) leak canary API implementation for the debug settings fragment.
|
||||
* This class is loaded via reflection by
|
||||
* {@link DebugSettingsFragment.DebugSettingsBVDLeakCanaryAPI}.
|
||||
*/
|
||||
@SuppressWarnings("unused") // Class is used but loaded via reflection
|
||||
public class DebugSettingsBVDLeakCanary
|
||||
implements DebugSettingsFragment.DebugSettingsBVDLeakCanaryAPI {
|
||||
|
||||
@Override
|
||||
public Intent getNewLeakDisplayActivityIntent() {
|
||||
return LeakCanary.INSTANCE.newLeakDisplayActivityIntent();
|
||||
}
|
||||
}
|
@ -1,63 +0,0 @@
|
||||
package org.schabi.newpipe.settings;
|
||||
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.preference.Preference;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.error.ErrorInfo;
|
||||
import org.schabi.newpipe.error.ErrorUtil;
|
||||
import org.schabi.newpipe.error.UserAction;
|
||||
import org.schabi.newpipe.util.PicassoHelper;
|
||||
|
||||
import leakcanary.LeakCanary;
|
||||
|
||||
public class DebugSettingsFragment extends BasePreferenceFragment {
|
||||
@Override
|
||||
public void onCreatePreferences(final Bundle savedInstanceState, final String rootKey) {
|
||||
addPreferencesFromResource(R.xml.debug_settings);
|
||||
|
||||
final Preference showMemoryLeaksPreference
|
||||
= findPreference(getString(R.string.show_memory_leaks_key));
|
||||
final Preference showImageIndicatorsPreference
|
||||
= findPreference(getString(R.string.show_image_indicators_key));
|
||||
final Preference crashTheAppPreference
|
||||
= findPreference(getString(R.string.crash_the_app_key));
|
||||
final Preference showErrorSnackbarPreference
|
||||
= findPreference(getString(R.string.show_error_snackbar_key));
|
||||
final Preference createErrorNotificationPreference
|
||||
= findPreference(getString(R.string.create_error_notification_key));
|
||||
|
||||
assert showMemoryLeaksPreference != null;
|
||||
assert showImageIndicatorsPreference != null;
|
||||
assert crashTheAppPreference != null;
|
||||
assert showErrorSnackbarPreference != null;
|
||||
assert createErrorNotificationPreference != null;
|
||||
|
||||
showMemoryLeaksPreference.setOnPreferenceClickListener(preference -> {
|
||||
startActivity(LeakCanary.INSTANCE.newLeakDisplayActivityIntent());
|
||||
return true;
|
||||
});
|
||||
|
||||
showImageIndicatorsPreference.setOnPreferenceChangeListener((preference, newValue) -> {
|
||||
PicassoHelper.setIndicatorsEnabled((Boolean) newValue);
|
||||
return true;
|
||||
});
|
||||
|
||||
crashTheAppPreference.setOnPreferenceClickListener(preference -> {
|
||||
throw new RuntimeException();
|
||||
});
|
||||
|
||||
showErrorSnackbarPreference.setOnPreferenceClickListener(preference -> {
|
||||
ErrorUtil.showUiErrorSnackbar(DebugSettingsFragment.this,
|
||||
"Dummy", new RuntimeException("Dummy"));
|
||||
return true;
|
||||
});
|
||||
|
||||
createErrorNotificationPreference.setOnPreferenceClickListener(preference -> {
|
||||
ErrorUtil.createNotification(requireContext(),
|
||||
new ErrorInfo(new RuntimeException("Dummy"), UserAction.UI_ERROR, "Dummy"));
|
||||
return true;
|
||||
});
|
||||
}
|
||||
}
|
@ -0,0 +1,148 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.apache.commons.text.similarity;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
* A matching algorithm that is similar to the searching algorithms implemented in editors such
|
||||
* as Sublime Text, TextMate, Atom and others.
|
||||
*
|
||||
* <p>
|
||||
* One point is given for every matched character. Subsequent matches yield two bonus points.
|
||||
* A higher score indicates a higher similarity.
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
* This code has been adapted from Apache Commons Lang 3.3.
|
||||
* </p>
|
||||
*
|
||||
* @since 1.0
|
||||
*
|
||||
* Note: This class was forked from
|
||||
* <a href="https://git.io/JyYJg">
|
||||
* apache/commons-text (8cfdafc) FuzzyScore.java
|
||||
* </a>
|
||||
*/
|
||||
public class FuzzyScore {
|
||||
|
||||
/**
|
||||
* Locale used to change the case of text.
|
||||
*/
|
||||
private final Locale locale;
|
||||
|
||||
|
||||
/**
|
||||
* This returns a {@link Locale}-specific {@link FuzzyScore}.
|
||||
*
|
||||
* @param locale The string matching logic is case insensitive.
|
||||
A {@link Locale} is necessary to normalize both Strings to lower case.
|
||||
* @throws IllegalArgumentException
|
||||
* This is thrown if the {@link Locale} parameter is {@code null}.
|
||||
*/
|
||||
public FuzzyScore(final Locale locale) {
|
||||
if (locale == null) {
|
||||
throw new IllegalArgumentException("Locale must not be null");
|
||||
}
|
||||
this.locale = locale;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the Fuzzy Score which indicates the similarity score between two
|
||||
* Strings.
|
||||
*
|
||||
* <pre>
|
||||
* score.fuzzyScore(null, null) = IllegalArgumentException
|
||||
* score.fuzzyScore("not null", null) = IllegalArgumentException
|
||||
* score.fuzzyScore(null, "not null") = IllegalArgumentException
|
||||
* score.fuzzyScore("", "") = 0
|
||||
* score.fuzzyScore("Workshop", "b") = 0
|
||||
* score.fuzzyScore("Room", "o") = 1
|
||||
* score.fuzzyScore("Workshop", "w") = 1
|
||||
* score.fuzzyScore("Workshop", "ws") = 2
|
||||
* score.fuzzyScore("Workshop", "wo") = 4
|
||||
* score.fuzzyScore("Apache Software Foundation", "asf") = 3
|
||||
* </pre>
|
||||
*
|
||||
* @param term a full term that should be matched against, must not be null
|
||||
* @param query the query that will be matched against a term, must not be
|
||||
* null
|
||||
* @return result score
|
||||
* @throws IllegalArgumentException if the term or query is {@code null}
|
||||
*/
|
||||
public Integer fuzzyScore(final CharSequence term, final CharSequence query) {
|
||||
if (term == null || query == null) {
|
||||
throw new IllegalArgumentException("CharSequences must not be null");
|
||||
}
|
||||
|
||||
// fuzzy logic is case insensitive. We normalize the Strings to lower
|
||||
// case right from the start. Turning characters to lower case
|
||||
// via Character.toLowerCase(char) is unfortunately insufficient
|
||||
// as it does not accept a locale.
|
||||
final String termLowerCase = term.toString().toLowerCase(locale);
|
||||
final String queryLowerCase = query.toString().toLowerCase(locale);
|
||||
|
||||
// the resulting score
|
||||
int score = 0;
|
||||
|
||||
// the position in the term which will be scanned next for potential
|
||||
// query character matches
|
||||
int termIndex = 0;
|
||||
|
||||
// index of the previously matched character in the term
|
||||
int previousMatchingCharacterIndex = Integer.MIN_VALUE;
|
||||
|
||||
for (int queryIndex = 0; queryIndex < queryLowerCase.length(); queryIndex++) {
|
||||
final char queryChar = queryLowerCase.charAt(queryIndex);
|
||||
|
||||
boolean termCharacterMatchFound = false;
|
||||
for (; termIndex < termLowerCase.length()
|
||||
&& !termCharacterMatchFound; termIndex++) {
|
||||
final char termChar = termLowerCase.charAt(termIndex);
|
||||
|
||||
if (queryChar == termChar) {
|
||||
// simple character matches result in one point
|
||||
score++;
|
||||
|
||||
// subsequent character matches further improve
|
||||
// the score.
|
||||
if (previousMatchingCharacterIndex + 1 == termIndex) {
|
||||
score += 2;
|
||||
}
|
||||
|
||||
previousMatchingCharacterIndex = termIndex;
|
||||
|
||||
// we can leave the nested loop. Every character in the
|
||||
// query can match at most one character in the term.
|
||||
termCharacterMatchFound = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return score;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the locale.
|
||||
*
|
||||
* @return The locale
|
||||
*/
|
||||
public Locale getLocale() {
|
||||
return locale;
|
||||
}
|
||||
|
||||
}
|
@ -14,6 +14,7 @@ import org.schabi.newpipe.local.dialog.PlaylistDialog;
|
||||
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
||||
import org.schabi.newpipe.player.playqueue.PlayQueueItem;
|
||||
import org.schabi.newpipe.util.NavigationHelper;
|
||||
import org.schabi.newpipe.util.SaveUploaderUrlHelper;
|
||||
|
||||
import java.util.Collections;
|
||||
|
||||
@ -61,11 +62,13 @@ public final class QueueItemMenuUtil {
|
||||
|
||||
return true;
|
||||
case R.id.menu_item_channel_details:
|
||||
// An intent must be used here.
|
||||
// Opening with FragmentManager transactions is not working,
|
||||
// as PlayQueueActivity doesn't use fragments.
|
||||
NavigationHelper.openChannelFragmentUsingIntent(context, item.getServiceId(),
|
||||
item.getUploaderUrl(), item.getUploader());
|
||||
SaveUploaderUrlHelper.saveUploaderUrlIfNeeded(context, item,
|
||||
// An intent must be used here.
|
||||
// Opening with FragmentManager transactions is not working,
|
||||
// as PlayQueueActivity doesn't use fragments.
|
||||
uploaderUrl -> NavigationHelper.openChannelFragmentUsingIntent(
|
||||
context, item.getServiceId(), uploaderUrl, item.getUploader()
|
||||
));
|
||||
return true;
|
||||
case R.id.menu_item_share:
|
||||
shareText(context, item.getTitle(), item.getUrl(),
|
||||
|
@ -185,7 +185,11 @@ class AboutActivity : AppCompatActivity() {
|
||||
SoftwareComponent(
|
||||
"RxJava", "2016 - 2020", "RxJava Contributors",
|
||||
"https://github.com/ReactiveX/RxJava", StandardLicenses.APACHE2
|
||||
)
|
||||
),
|
||||
SoftwareComponent(
|
||||
"SearchPreference", "2018", "ByteHamster",
|
||||
"https://github.com/ByteHamster/SearchPreference", StandardLicenses.MIT
|
||||
),
|
||||
)
|
||||
private const val POS_ABOUT = 0
|
||||
private const val POS_LICENSE = 1
|
||||
|
@ -1,79 +0,0 @@
|
||||
package org.schabi.newpipe.database.history.model;
|
||||
|
||||
import androidx.room.ColumnInfo;
|
||||
import androidx.room.Entity;
|
||||
import androidx.room.Ignore;
|
||||
import androidx.room.Index;
|
||||
import androidx.room.PrimaryKey;
|
||||
|
||||
import java.time.OffsetDateTime;
|
||||
|
||||
import static org.schabi.newpipe.database.history.model.SearchHistoryEntry.SEARCH;
|
||||
|
||||
@Entity(tableName = SearchHistoryEntry.TABLE_NAME,
|
||||
indices = {@Index(value = SEARCH)})
|
||||
public class SearchHistoryEntry {
|
||||
public static final String ID = "id";
|
||||
public static final String TABLE_NAME = "search_history";
|
||||
public static final String SERVICE_ID = "service_id";
|
||||
public static final String CREATION_DATE = "creation_date";
|
||||
public static final String SEARCH = "search";
|
||||
|
||||
@ColumnInfo(name = ID)
|
||||
@PrimaryKey(autoGenerate = true)
|
||||
private long id;
|
||||
|
||||
@ColumnInfo(name = CREATION_DATE)
|
||||
private OffsetDateTime creationDate;
|
||||
|
||||
@ColumnInfo(name = SERVICE_ID)
|
||||
private int serviceId;
|
||||
|
||||
@ColumnInfo(name = SEARCH)
|
||||
private String search;
|
||||
|
||||
public SearchHistoryEntry(final OffsetDateTime creationDate, final int serviceId,
|
||||
final String search) {
|
||||
this.serviceId = serviceId;
|
||||
this.creationDate = creationDate;
|
||||
this.search = search;
|
||||
}
|
||||
|
||||
public long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(final long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public OffsetDateTime getCreationDate() {
|
||||
return creationDate;
|
||||
}
|
||||
|
||||
public void setCreationDate(final OffsetDateTime creationDate) {
|
||||
this.creationDate = creationDate;
|
||||
}
|
||||
|
||||
public int getServiceId() {
|
||||
return serviceId;
|
||||
}
|
||||
|
||||
public void setServiceId(final int serviceId) {
|
||||
this.serviceId = serviceId;
|
||||
}
|
||||
|
||||
public String getSearch() {
|
||||
return search;
|
||||
}
|
||||
|
||||
public void setSearch(final String search) {
|
||||
this.search = search;
|
||||
}
|
||||
|
||||
@Ignore
|
||||
public boolean hasEqualValues(final SearchHistoryEntry otherEntry) {
|
||||
return getServiceId() == otherEntry.getServiceId()
|
||||
&& getSearch().equals(otherEntry.getSearch());
|
||||
}
|
||||
}
|
@ -0,0 +1,40 @@
|
||||
package org.schabi.newpipe.database.history.model
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.Ignore
|
||||
import androidx.room.Index
|
||||
import androidx.room.PrimaryKey
|
||||
import java.time.OffsetDateTime
|
||||
|
||||
@Entity(
|
||||
tableName = SearchHistoryEntry.TABLE_NAME,
|
||||
indices = [Index(value = [SearchHistoryEntry.SEARCH])]
|
||||
)
|
||||
data class SearchHistoryEntry(
|
||||
@field:ColumnInfo(name = CREATION_DATE) var creationDate: OffsetDateTime?,
|
||||
@field:ColumnInfo(
|
||||
name = SERVICE_ID
|
||||
) var serviceId: Int,
|
||||
@field:ColumnInfo(name = SEARCH) var search: String?
|
||||
) {
|
||||
@ColumnInfo(name = ID)
|
||||
@PrimaryKey(autoGenerate = true)
|
||||
var id: Long = 0
|
||||
|
||||
@Ignore
|
||||
fun hasEqualValues(otherEntry: SearchHistoryEntry): Boolean {
|
||||
return (
|
||||
serviceId == otherEntry.serviceId &&
|
||||
search == otherEntry.search
|
||||
)
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val ID = "id"
|
||||
const val TABLE_NAME = "search_history"
|
||||
const val SERVICE_ID = "service_id"
|
||||
const val CREATION_DATE = "creation_date"
|
||||
const val SEARCH = "search"
|
||||
}
|
||||
}
|
@ -1098,6 +1098,11 @@ public final class VideoDetailFragment
|
||||
|
||||
toggleFullscreenIfInFullscreenMode();
|
||||
|
||||
if (isPlayerAvailable()) {
|
||||
// FIXME Workaround #7427
|
||||
player.setRecovery();
|
||||
}
|
||||
|
||||
if (!useExternalAudioPlayer) {
|
||||
openNormalBackgroundPlayer(append);
|
||||
} else {
|
||||
@ -1114,6 +1119,9 @@ public final class VideoDetailFragment
|
||||
// See UI changes while remote playQueue changes
|
||||
if (!isPlayerAvailable()) {
|
||||
playerHolder.startService(false, this);
|
||||
} else {
|
||||
// FIXME Workaround #7427
|
||||
player.setRecovery();
|
||||
}
|
||||
|
||||
toggleFullscreenIfInFullscreenMode();
|
||||
@ -1521,6 +1529,8 @@ public final class VideoDetailFragment
|
||||
animate(binding.detailThumbnailPlayButton, true, 200);
|
||||
binding.detailVideoTitleView.setText(title);
|
||||
|
||||
binding.detailSubChannelThumbnailView.setVisibility(View.GONE);
|
||||
|
||||
if (!isEmpty(info.getSubChannelName())) {
|
||||
displayBothUploaderAndSubChannel(info);
|
||||
} else if (!isEmpty(info.getUploaderName())) {
|
||||
@ -2206,12 +2216,20 @@ public final class VideoDetailFragment
|
||||
mainFragment.setDescendantFocusability(afterDescendants);
|
||||
toolbar.setDescendantFocusability(afterDescendants);
|
||||
((ViewGroup) requireView()).setDescendantFocusability(blockDescendants);
|
||||
mainFragment.requestFocus();
|
||||
// Only focus the mainFragment if the mainFragment (e.g. search-results)
|
||||
// or the toolbar (e.g. Textfield for search) don't have focus.
|
||||
// This was done to fix problems with the keyboard input, see also #7490
|
||||
if (!mainFragment.hasFocus() && !toolbar.hasFocus()) {
|
||||
mainFragment.requestFocus();
|
||||
}
|
||||
} else {
|
||||
mainFragment.setDescendantFocusability(blockDescendants);
|
||||
toolbar.setDescendantFocusability(blockDescendants);
|
||||
((ViewGroup) requireView()).setDescendantFocusability(afterDescendants);
|
||||
binding.detailThumbnailRootLayout.requestFocus();
|
||||
// Only focus the player if it not already has focus
|
||||
if (!binding.getRoot().hasFocus()) {
|
||||
binding.detailThumbnailRootLayout.requestFocus();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -268,7 +268,10 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
|
||||
ShareUtils.openUrlInBrowser(requireContext(), url);
|
||||
break;
|
||||
case R.id.menu_item_share:
|
||||
ShareUtils.shareText(requireContext(), name, url, currentInfo.getThumbnailUrl());
|
||||
if (currentInfo != null) {
|
||||
ShareUtils.shareText(requireContext(), name, url,
|
||||
currentInfo.getThumbnailUrl());
|
||||
}
|
||||
break;
|
||||
case R.id.menu_item_bookmark:
|
||||
onBookmarkClicked();
|
||||
|
@ -25,7 +25,6 @@ import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.animation.DecelerateInterpolator;
|
||||
import android.view.inputmethod.EditorInfo;
|
||||
import android.view.inputmethod.InputMethodManager;
|
||||
import android.widget.EditText;
|
||||
import android.widget.TextView;
|
||||
|
||||
@ -34,7 +33,6 @@ import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.appcompat.widget.TooltipCompat;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.core.text.HtmlCompat;
|
||||
import androidx.preference.PreferenceManager;
|
||||
import androidx.recyclerview.widget.ItemTouchHelper;
|
||||
@ -65,6 +63,7 @@ import org.schabi.newpipe.settings.NewPipeSettings;
|
||||
import org.schabi.newpipe.util.Constants;
|
||||
import org.schabi.newpipe.util.DeviceUtils;
|
||||
import org.schabi.newpipe.util.ExtractorHelper;
|
||||
import org.schabi.newpipe.util.KeyboardUtil;
|
||||
import org.schabi.newpipe.util.NavigationHelper;
|
||||
import org.schabi.newpipe.util.ServiceHelper;
|
||||
|
||||
@ -670,31 +669,15 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "showKeyboardSearch() called");
|
||||
}
|
||||
if (searchEditText == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (searchEditText.requestFocus()) {
|
||||
final InputMethodManager imm = ContextCompat.getSystemService(activity,
|
||||
InputMethodManager.class);
|
||||
imm.showSoftInput(searchEditText, InputMethodManager.SHOW_FORCED);
|
||||
}
|
||||
KeyboardUtil.showKeyboard(activity, searchEditText);
|
||||
}
|
||||
|
||||
private void hideKeyboardSearch() {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "hideKeyboardSearch() called");
|
||||
}
|
||||
if (searchEditText == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final InputMethodManager imm = ContextCompat.getSystemService(activity,
|
||||
InputMethodManager.class);
|
||||
imm.hideSoftInputFromWindow(searchEditText.getWindowToken(),
|
||||
InputMethodManager.RESULT_UNCHANGED_SHOWN);
|
||||
|
||||
searchEditText.clearFocus();
|
||||
KeyboardUtil.hideKeyboard(activity, searchEditText);
|
||||
}
|
||||
|
||||
private void showDeleteSuggestionDialog(final SuggestionItem item) {
|
||||
|
@ -34,12 +34,14 @@ import org.schabi.newpipe.local.history.HistoryRecordManager;
|
||||
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
|
||||
@ -55,5 +57,7 @@ public class CommentsInfoItemHolder extends CommentsMiniInfoItemHolder {
|
||||
itemTitleView.setText(item.getUploaderName());
|
||||
|
||||
itemHeartView.setVisibility(item.isHeartedByUploader() ? View.VISIBLE : View.GONE);
|
||||
|
||||
itemPinnedView.setVisibility(item.isPinned() ? View.VISIBLE : View.GONE);
|
||||
}
|
||||
}
|
||||
|
@ -75,6 +75,7 @@ fun View.animate(
|
||||
}
|
||||
animate().setListener(null).cancel()
|
||||
isVisible = true
|
||||
|
||||
when (animationType) {
|
||||
AnimationType.ALPHA -> animateAlpha(enterOrExit, duration, delay, execOnEnd)
|
||||
AnimationType.SCALE_AND_ALPHA -> animateScaleAndAlpha(enterOrExit, duration, delay, execOnEnd)
|
||||
|
@ -271,7 +271,7 @@ class FeedFragment : BaseStateFragment<FeedState>() {
|
||||
|
||||
override fun onDestroyView() {
|
||||
// Ensure that all animations are canceled
|
||||
feedBinding.newItemsLoadedButton?.clearAnimation()
|
||||
tryGetNewItemsLoadedButton()?.clearAnimation()
|
||||
|
||||
feedBinding.itemsList.adapter = null
|
||||
_feedBinding = null
|
||||
|
@ -51,9 +51,6 @@ import static java.util.concurrent.TimeUnit.MILLISECONDS;
|
||||
|
||||
import android.animation.Animator;
|
||||
import android.animation.AnimatorListenerAdapter;
|
||||
import android.animation.ObjectAnimator;
|
||||
import android.animation.PropertyValuesHolder;
|
||||
import android.animation.ValueAnimator;
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
@ -154,6 +151,7 @@ import org.schabi.newpipe.info_list.StreamSegmentAdapter;
|
||||
import org.schabi.newpipe.ktx.AnimationType;
|
||||
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
||||
import org.schabi.newpipe.player.MainPlayer.PlayerType;
|
||||
import org.schabi.newpipe.player.event.DisplayPortion;
|
||||
import org.schabi.newpipe.player.event.PlayerEventListener;
|
||||
import org.schabi.newpipe.player.event.PlayerGestureListener;
|
||||
import org.schabi.newpipe.player.event.PlayerServiceEventListener;
|
||||
@ -188,6 +186,7 @@ import org.schabi.newpipe.util.StreamTypeUtil;
|
||||
import org.schabi.newpipe.util.external_communication.KoreUtils;
|
||||
import org.schabi.newpipe.util.external_communication.ShareUtils;
|
||||
import org.schabi.newpipe.views.ExpandableSurfaceView;
|
||||
import org.schabi.newpipe.views.player.PlayerFastSeekOverlay;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
@ -247,6 +246,7 @@ public final class Player implements
|
||||
public static final int DEFAULT_CONTROLS_DURATION = 300; // 300 millis
|
||||
public static final int DEFAULT_CONTROLS_HIDE_TIME = 2000; // 2 Seconds
|
||||
public static final int DPAD_CONTROLS_HIDE_TIME = 7000; // 7 Seconds
|
||||
public static final int SEEK_OVERLAY_DURATION = 450; // 450 millis
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Other constants
|
||||
@ -313,7 +313,6 @@ public final class Player implements
|
||||
|
||||
private PlayerBinding binding;
|
||||
|
||||
private ValueAnimator controlViewAnimator;
|
||||
private final Handler controlsVisibilityHandler = new Handler();
|
||||
|
||||
// fullscreen player
|
||||
@ -365,6 +364,7 @@ public final class Player implements
|
||||
|
||||
private int maxGestureLength; // scaled
|
||||
private GestureDetectorCompat gestureDetector;
|
||||
private PlayerGestureListener playerGestureListener;
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Listeners and disposables
|
||||
@ -449,6 +449,8 @@ public final class Player implements
|
||||
initPlayer(true);
|
||||
}
|
||||
initListeners();
|
||||
|
||||
setupPlayerSeekOverlay();
|
||||
}
|
||||
|
||||
private void initViews(@NonNull final PlayerBinding playerBinding) {
|
||||
@ -525,9 +527,9 @@ public final class Player implements
|
||||
binding.resizeTextView.setOnClickListener(this);
|
||||
binding.playbackLiveSync.setOnClickListener(this);
|
||||
|
||||
final PlayerGestureListener listener = new PlayerGestureListener(this, service);
|
||||
gestureDetector = new GestureDetectorCompat(context, listener);
|
||||
binding.getRoot().setOnTouchListener(listener);
|
||||
playerGestureListener = new PlayerGestureListener(this, service);
|
||||
gestureDetector = new GestureDetectorCompat(context, playerGestureListener);
|
||||
binding.getRoot().setOnTouchListener(playerGestureListener);
|
||||
|
||||
binding.queueButton.setOnClickListener(this);
|
||||
binding.segmentsButton.setOnClickListener(this);
|
||||
@ -578,6 +580,68 @@ public final class Player implements
|
||||
v.getPaddingRight(),
|
||||
v.getPaddingBottom()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the Fast-For/Backward overlay.
|
||||
*/
|
||||
private void setupPlayerSeekOverlay() {
|
||||
binding.fastSeekOverlay
|
||||
.seekSecondsSupplier(
|
||||
() -> (int) (retrieveSeekDurationFromPreferences(this) / 1000.0f))
|
||||
.performListener(new PlayerFastSeekOverlay.PerformListener() {
|
||||
|
||||
@Override
|
||||
public void onDoubleTap() {
|
||||
animate(binding.fastSeekOverlay, true, SEEK_OVERLAY_DURATION);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDoubleTapEnd() {
|
||||
animate(binding.fastSeekOverlay, false, SEEK_OVERLAY_DURATION);
|
||||
}
|
||||
|
||||
@Override
|
||||
public FastSeekDirection getFastSeekDirection(
|
||||
@NonNull final DisplayPortion portion
|
||||
) {
|
||||
if (exoPlayerIsNull()) {
|
||||
// Abort seeking
|
||||
playerGestureListener.endMultiDoubleTap();
|
||||
return FastSeekDirection.NONE;
|
||||
}
|
||||
if (portion == DisplayPortion.LEFT) {
|
||||
// Check if it's possible to rewind
|
||||
// Small puffer to eliminate infinite rewind seeking
|
||||
if (simpleExoPlayer.getCurrentPosition() < 500L) {
|
||||
return FastSeekDirection.NONE;
|
||||
}
|
||||
return FastSeekDirection.BACKWARD;
|
||||
} else if (portion == DisplayPortion.RIGHT) {
|
||||
// Check if it's possible to fast-forward
|
||||
if (currentState == STATE_COMPLETED
|
||||
|| simpleExoPlayer.getCurrentPosition()
|
||||
>= simpleExoPlayer.getDuration()) {
|
||||
return FastSeekDirection.NONE;
|
||||
}
|
||||
return FastSeekDirection.FORWARD;
|
||||
}
|
||||
/* portion == DisplayPortion.MIDDLE */
|
||||
return FastSeekDirection.NONE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void seek(final boolean forward) {
|
||||
playerGestureListener.keepInDoubleTapMode();
|
||||
if (forward) {
|
||||
fastForward();
|
||||
} else {
|
||||
fastRewind();
|
||||
}
|
||||
}
|
||||
});
|
||||
playerGestureListener.doubleTapControls(binding.fastSeekOverlay);
|
||||
}
|
||||
|
||||
//endregion
|
||||
|
||||
|
||||
@ -635,6 +699,7 @@ public final class Player implements
|
||||
final boolean isMuted = intent.getBooleanExtra(IS_MUTED, isMuted());
|
||||
|
||||
/*
|
||||
* TODO As seen in #7427 this does not work:
|
||||
* There are 3 situations when playback shouldn't be started from scratch (zero timestamp):
|
||||
* 1. User pressed on a timestamp link and the same video should be rewound to the timestamp
|
||||
* 2. User changed a player from, for example. main to popup, or from audio to main, etc
|
||||
@ -1795,71 +1860,6 @@ public final class Player implements
|
||||
return binding != null && binding.playbackControlRoot.getVisibility() == View.VISIBLE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Show a animation, and depending on goneOnEnd, will stay on the screen or be gone.
|
||||
*
|
||||
* @param drawableId the drawable that will be used to animate,
|
||||
* pass -1 to clear any animation that is visible
|
||||
* @param goneOnEnd will set the animation view to GONE on the end of the animation
|
||||
*/
|
||||
public void showAndAnimateControl(final int drawableId, final boolean goneOnEnd) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "showAndAnimateControl() called with: "
|
||||
+ "drawableId = [" + drawableId + "], goneOnEnd = [" + goneOnEnd + "]");
|
||||
}
|
||||
if (controlViewAnimator != null && controlViewAnimator.isRunning()) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "showAndAnimateControl: controlViewAnimator.isRunning");
|
||||
}
|
||||
controlViewAnimator.end();
|
||||
}
|
||||
|
||||
if (drawableId == -1) {
|
||||
if (binding.controlAnimationView.getVisibility() == View.VISIBLE) {
|
||||
controlViewAnimator = ObjectAnimator.ofPropertyValuesHolder(
|
||||
binding.controlAnimationView,
|
||||
PropertyValuesHolder.ofFloat(View.ALPHA, 1.0f, 0.0f),
|
||||
PropertyValuesHolder.ofFloat(View.SCALE_X, 1.4f, 1.0f),
|
||||
PropertyValuesHolder.ofFloat(View.SCALE_Y, 1.4f, 1.0f)
|
||||
).setDuration(DEFAULT_CONTROLS_DURATION);
|
||||
controlViewAnimator.addListener(new AnimatorListenerAdapter() {
|
||||
@Override
|
||||
public void onAnimationEnd(final Animator animation) {
|
||||
binding.controlAnimationView.setVisibility(View.GONE);
|
||||
}
|
||||
});
|
||||
controlViewAnimator.start();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
final float scaleFrom = goneOnEnd ? 1f : 1f;
|
||||
final float scaleTo = goneOnEnd ? 1.8f : 1.4f;
|
||||
final float alphaFrom = goneOnEnd ? 1f : 0f;
|
||||
final float alphaTo = goneOnEnd ? 0f : 1f;
|
||||
|
||||
|
||||
controlViewAnimator = ObjectAnimator.ofPropertyValuesHolder(
|
||||
binding.controlAnimationView,
|
||||
PropertyValuesHolder.ofFloat(View.ALPHA, alphaFrom, alphaTo),
|
||||
PropertyValuesHolder.ofFloat(View.SCALE_X, scaleFrom, scaleTo),
|
||||
PropertyValuesHolder.ofFloat(View.SCALE_Y, scaleFrom, scaleTo)
|
||||
);
|
||||
controlViewAnimator.setDuration(goneOnEnd ? 1000 : 500);
|
||||
controlViewAnimator.addListener(new AnimatorListenerAdapter() {
|
||||
@Override
|
||||
public void onAnimationEnd(final Animator animation) {
|
||||
binding.controlAnimationView.setVisibility(goneOnEnd ? View.GONE : View.VISIBLE);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
binding.controlAnimationView.setVisibility(View.VISIBLE);
|
||||
binding.controlAnimationView.setImageDrawable(
|
||||
AppCompatResources.getDrawable(context, drawableId));
|
||||
controlViewAnimator.start();
|
||||
}
|
||||
|
||||
public void showControlsThenHide() {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "showControlsThenHide() called");
|
||||
@ -1904,6 +1904,7 @@ public final class Player implements
|
||||
}
|
||||
|
||||
private void showHideShadow(final boolean show, final long duration) {
|
||||
animate(binding.playbackControlsShadow, show, duration, AnimationType.ALPHA, 0, null);
|
||||
animate(binding.playerTopShadow, show, duration, AnimationType.ALPHA, 0, null);
|
||||
animate(binding.playerBottomShadow, show, duration, AnimationType.ALPHA, 0, null);
|
||||
}
|
||||
@ -2047,7 +2048,7 @@ public final class Player implements
|
||||
if (currentState == STATE_BLOCKED) {
|
||||
changeState(STATE_BUFFERING);
|
||||
}
|
||||
simpleExoPlayer.setMediaSource(mediaSource);
|
||||
simpleExoPlayer.setMediaSource(mediaSource, false);
|
||||
simpleExoPlayer.prepare();
|
||||
}
|
||||
|
||||
@ -2101,8 +2102,8 @@ public final class Player implements
|
||||
startProgressLoop();
|
||||
}
|
||||
|
||||
controlsVisibilityHandler.removeCallbacksAndMessages(null);
|
||||
animate(binding.playbackControlRoot, false, DEFAULT_CONTROLS_DURATION);
|
||||
// if we are e.g. switching players, hide controls
|
||||
hideControls(DEFAULT_CONTROLS_DURATION, 0);
|
||||
|
||||
binding.playbackSeekBar.setEnabled(false);
|
||||
binding.playbackSeekBar.getThumb()
|
||||
@ -2129,8 +2130,6 @@ public final class Player implements
|
||||
|
||||
updateStreamRelatedViews();
|
||||
|
||||
showAndAnimateControl(-1, true);
|
||||
|
||||
binding.playbackSeekBar.setEnabled(true);
|
||||
binding.playbackSeekBar.getThumb()
|
||||
.setColorFilter(new PorterDuffColorFilter(Color.RED, PorterDuff.Mode.SRC_IN));
|
||||
@ -2178,18 +2177,21 @@ public final class Player implements
|
||||
stopProgressLoop();
|
||||
}
|
||||
|
||||
showControls(400);
|
||||
binding.loadingPanel.setVisibility(View.GONE);
|
||||
|
||||
animate(binding.playPauseButton, false, 80, AnimationType.SCALE_AND_ALPHA, 0,
|
||||
() -> {
|
||||
binding.playPauseButton.setImageResource(R.drawable.ic_play_arrow);
|
||||
animatePlayButtons(true, 200);
|
||||
if (!isQueueVisible) {
|
||||
binding.playPauseButton.requestFocus();
|
||||
}
|
||||
});
|
||||
// Don't let UI elements popup during double tap seeking. This state is entered sometimes
|
||||
// during seeking/loading. This if-else check ensures that the controls aren't popping up.
|
||||
if (!playerGestureListener.isDoubleTapping()) {
|
||||
showControls(400);
|
||||
binding.loadingPanel.setVisibility(View.GONE);
|
||||
|
||||
animate(binding.playPauseButton, false, 80, AnimationType.SCALE_AND_ALPHA, 0,
|
||||
() -> {
|
||||
binding.playPauseButton.setImageResource(R.drawable.ic_play_arrow);
|
||||
animatePlayButtons(true, 200);
|
||||
if (!isQueueVisible) {
|
||||
binding.playPauseButton.requestFocus();
|
||||
}
|
||||
});
|
||||
}
|
||||
changePopupWindowFlags(IDLE_WINDOW_FLAGS);
|
||||
|
||||
// Remove running notification when user does not want minimization to background or popup
|
||||
@ -2207,7 +2209,6 @@ public final class Player implements
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "onPausedSeek() called");
|
||||
}
|
||||
showAndAnimateControl(-1, true);
|
||||
|
||||
animatePlayButtons(false, 100);
|
||||
binding.getRoot().setKeepScreenOn(true);
|
||||
@ -2516,23 +2517,11 @@ public final class Player implements
|
||||
Log.e(TAG, "ExoPlayer - onPlayerError() called with:", error);
|
||||
|
||||
saveStreamProgressState();
|
||||
|
||||
// create error notification
|
||||
final ErrorInfo errorInfo;
|
||||
if (currentMetadata == null) {
|
||||
errorInfo = new ErrorInfo(error, UserAction.PLAY_STREAM,
|
||||
"Player error[type=" + error.type + "] occurred, currentMetadata is null");
|
||||
} else {
|
||||
errorInfo = new ErrorInfo(error, UserAction.PLAY_STREAM,
|
||||
"Player error[type=" + error.type + "] occurred while playing "
|
||||
+ currentMetadata.getMetadata().getUrl(),
|
||||
currentMetadata.getMetadata());
|
||||
}
|
||||
ErrorUtil.createNotification(context, errorInfo);
|
||||
boolean isCatchableException = false;
|
||||
|
||||
switch (error.type) {
|
||||
case ExoPlaybackException.TYPE_SOURCE:
|
||||
processSourceError(error.getSourceException());
|
||||
isCatchableException = processSourceError(error.getSourceException());
|
||||
break;
|
||||
case ExoPlaybackException.TYPE_UNEXPECTED:
|
||||
setRecovery();
|
||||
@ -2545,22 +2534,60 @@ public final class Player implements
|
||||
break;
|
||||
}
|
||||
|
||||
if (isCatchableException) {
|
||||
return;
|
||||
}
|
||||
|
||||
createErrorNotification(error);
|
||||
|
||||
if (fragmentListener != null) {
|
||||
fragmentListener.onPlayerError(error);
|
||||
}
|
||||
}
|
||||
|
||||
private void processSourceError(final IOException error) {
|
||||
if (exoPlayerIsNull() || playQueue == null) {
|
||||
return;
|
||||
private void createErrorNotification(@NonNull final ExoPlaybackException error) {
|
||||
final ErrorInfo errorInfo;
|
||||
if (currentMetadata == null) {
|
||||
errorInfo = new ErrorInfo(error, UserAction.PLAY_STREAM,
|
||||
"Player error[type=" + error.type + "] occurred, currentMetadata is null");
|
||||
} else {
|
||||
errorInfo = new ErrorInfo(error, UserAction.PLAY_STREAM,
|
||||
"Player error[type=" + error.type + "] occurred while playing "
|
||||
+ currentMetadata.getMetadata().getUrl(),
|
||||
currentMetadata.getMetadata());
|
||||
}
|
||||
ErrorUtil.createNotification(context, errorInfo);
|
||||
}
|
||||
|
||||
/**
|
||||
* Process an {@link IOException} returned by {@link ExoPlaybackException#getSourceException()}
|
||||
* for {@link ExoPlaybackException#TYPE_SOURCE} exceptions.
|
||||
*
|
||||
* <p>
|
||||
* This method sets the recovery position and sends an error message to the play queue if the
|
||||
* exception is not a {@link BehindLiveWindowException}.
|
||||
* </p>
|
||||
* @param error the source error which was thrown by ExoPlayer
|
||||
* @return whether the exception thrown is a {@link BehindLiveWindowException} ({@code false}
|
||||
* is always returned if ExoPlayer or the play queue is null)
|
||||
*/
|
||||
private boolean processSourceError(final IOException error) {
|
||||
if (exoPlayerIsNull() || playQueue == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
setRecovery();
|
||||
|
||||
if (error instanceof BehindLiveWindowException) {
|
||||
reloadPlayQueueManager();
|
||||
} else {
|
||||
playQueue.error();
|
||||
simpleExoPlayer.seekToDefaultPosition();
|
||||
simpleExoPlayer.prepare();
|
||||
// Inform the user that we are reloading the stream by switching to the buffering state
|
||||
onBuffering();
|
||||
return true;
|
||||
}
|
||||
|
||||
playQueue.error();
|
||||
return false;
|
||||
}
|
||||
//endregion
|
||||
|
||||
@ -2837,7 +2864,6 @@ public final class Player implements
|
||||
}
|
||||
seekBy(retrieveSeekDurationFromPreferences(this));
|
||||
triggerProgressUpdate();
|
||||
showAndAnimateControl(R.drawable.ic_fast_forward, true);
|
||||
}
|
||||
|
||||
public void fastRewind() {
|
||||
@ -2846,7 +2872,6 @@ public final class Player implements
|
||||
}
|
||||
seekBy(-retrieveSeekDurationFromPreferences(this));
|
||||
triggerProgressUpdate();
|
||||
showAndAnimateControl(R.drawable.ic_fast_rewind, true);
|
||||
}
|
||||
//endregion
|
||||
|
||||
@ -4278,6 +4303,10 @@ public final class Player implements
|
||||
return binding.currentDisplaySeek;
|
||||
}
|
||||
|
||||
public PlayerFastSeekOverlay getFastSeekOverlay() {
|
||||
return binding.fastSeekOverlay;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public WindowManager.LayoutParams getPopupLayoutParams() {
|
||||
return popupLayoutParams;
|
||||
|
@ -411,7 +411,7 @@ abstract class BasePlayerGestureListener(
|
||||
var doubleTapControls: DoubleTapListener? = null
|
||||
private set
|
||||
|
||||
val isDoubleTapEnabled: Boolean
|
||||
private val isDoubleTapEnabled: Boolean
|
||||
get() = doubleTapDelay > 0
|
||||
|
||||
var isDoubleTapping = false
|
||||
@ -459,10 +459,6 @@ abstract class BasePlayerGestureListener(
|
||||
doubleTapControls?.onDoubleTapFinished()
|
||||
}
|
||||
|
||||
fun enableMultiDoubleTap(enable: Boolean) = apply {
|
||||
doubleTapDelay = if (enable) DOUBLE_TAP_DELAY else 0
|
||||
}
|
||||
|
||||
// ///////////////////////////////////////////////////////////////////
|
||||
// Utils
|
||||
// ///////////////////////////////////////////////////////////////////
|
||||
|
@ -55,12 +55,10 @@ public class PlayerGestureListener
|
||||
player.hideControls(0, 0);
|
||||
}
|
||||
|
||||
if (portion == DisplayPortion.LEFT) {
|
||||
player.fastRewind();
|
||||
if (portion == DisplayPortion.LEFT || portion == DisplayPortion.RIGHT) {
|
||||
startMultiDoubleTap(event);
|
||||
} else if (portion == DisplayPortion.MIDDLE) {
|
||||
player.playPause();
|
||||
} else if (portion == DisplayPortion.RIGHT) {
|
||||
player.fastForward();
|
||||
}
|
||||
}
|
||||
|
||||
@ -232,10 +230,10 @@ public class PlayerGestureListener
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "onPopupResizingStart called");
|
||||
}
|
||||
player.showAndAnimateControl(-1, true);
|
||||
player.getLoadingPanel().setVisibility(View.GONE);
|
||||
|
||||
player.hideControls(0, 0);
|
||||
animate(player.getFastSeekOverlay(), false, 0);
|
||||
animate(player.getCurrentDisplaySeek(), false, 0, ALPHA, 0);
|
||||
}
|
||||
|
||||
|
@ -9,6 +9,7 @@ import com.google.android.exoplayer2.source.SingleSampleMediaSource;
|
||||
import com.google.android.exoplayer2.source.dash.DashMediaSource;
|
||||
import com.google.android.exoplayer2.source.dash.DefaultDashChunkSource;
|
||||
import com.google.android.exoplayer2.source.hls.HlsMediaSource;
|
||||
import com.google.android.exoplayer2.source.hls.playlist.DefaultHlsPlaylistTracker;
|
||||
import com.google.android.exoplayer2.source.smoothstreaming.DefaultSsChunkSource;
|
||||
import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource;
|
||||
import com.google.android.exoplayer2.upstream.DataSource;
|
||||
@ -17,9 +18,18 @@ import com.google.android.exoplayer2.upstream.DefaultLoadErrorHandlingPolicy;
|
||||
import com.google.android.exoplayer2.upstream.TransferListener;
|
||||
|
||||
public class PlayerDataSource {
|
||||
|
||||
public static final int LIVE_STREAM_EDGE_GAP_MILLIS = 10000;
|
||||
|
||||
/**
|
||||
* An approximately 4.3 times greater value than the
|
||||
* {@link DefaultHlsPlaylistTracker#DEFAULT_PLAYLIST_STUCK_TARGET_DURATION_COEFFICIENT default}
|
||||
* to ensure that (very) low latency livestreams which got stuck for a moment don't crash too
|
||||
* early.
|
||||
*/
|
||||
private static final double PLAYLIST_STUCK_TARGET_DURATION_COEFFICIENT = 15;
|
||||
private static final int MANIFEST_MINIMUM_RETRY = 5;
|
||||
private static final int EXTRACTOR_MINIMUM_RETRY = Integer.MAX_VALUE;
|
||||
public static final int LIVE_STREAM_EDGE_GAP_MILLIS = 10000;
|
||||
|
||||
private final DataSource.Factory cacheDataSourceFactory;
|
||||
private final DataSource.Factory cachelessDataSourceFactory;
|
||||
@ -44,8 +54,13 @@ public class PlayerDataSource {
|
||||
public HlsMediaSource.Factory getLiveHlsMediaSourceFactory() {
|
||||
return new HlsMediaSource.Factory(cachelessDataSourceFactory)
|
||||
.setAllowChunklessPreparation(true)
|
||||
.setLoadErrorHandlingPolicy(
|
||||
new DefaultLoadErrorHandlingPolicy(MANIFEST_MINIMUM_RETRY));
|
||||
.setLoadErrorHandlingPolicy(new DefaultLoadErrorHandlingPolicy(
|
||||
MANIFEST_MINIMUM_RETRY))
|
||||
.setPlaylistTrackerFactory((dataSourceFactory, loadErrorHandlingPolicy,
|
||||
playlistParserFactory) ->
|
||||
new DefaultHlsPlaylistTracker(dataSourceFactory, loadErrorHandlingPolicy,
|
||||
playlistParserFactory, PLAYLIST_STUCK_TARGET_DURATION_COEFFICIENT)
|
||||
);
|
||||
}
|
||||
|
||||
public DashMediaSource.Factory getLiveDashMediaSourceFactory() {
|
||||
|
@ -2,7 +2,6 @@ package org.schabi.newpipe.settings;
|
||||
|
||||
import android.content.ActivityNotFoundException;
|
||||
import android.content.Intent;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.provider.Settings;
|
||||
import android.widget.Toast;
|
||||
@ -15,14 +14,10 @@ import org.schabi.newpipe.util.Constants;
|
||||
import org.schabi.newpipe.util.ThemeHelper;
|
||||
|
||||
public class AppearanceSettingsFragment extends BasePreferenceFragment {
|
||||
private static final boolean CAPTIONING_SETTINGS_ACCESSIBLE =
|
||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT;
|
||||
|
||||
private String captionSettingsKey;
|
||||
|
||||
@Override
|
||||
public void onCreatePreferences(final Bundle savedInstanceState, final String rootKey) {
|
||||
addPreferencesFromResource(R.xml.appearance_settings);
|
||||
addPreferencesFromResourceRegistry();
|
||||
|
||||
final String themeKey = getString(R.string.theme_key);
|
||||
// the key of the active theme when settings were opened (or recreated after theme change)
|
||||
@ -51,16 +46,11 @@ public class AppearanceSettingsFragment extends BasePreferenceFragment {
|
||||
} else {
|
||||
removePreference(nightThemeKey);
|
||||
}
|
||||
|
||||
captionSettingsKey = getString(R.string.caption_settings_key);
|
||||
if (!CAPTIONING_SETTINGS_ACCESSIBLE) {
|
||||
removePreference(captionSettingsKey);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onPreferenceTreeClick(final Preference preference) {
|
||||
if (preference.getKey().equals(captionSettingsKey) && CAPTIONING_SETTINGS_ACCESSIBLE) {
|
||||
if (preference.getKey().equals(getString(R.string.caption_settings_key))) {
|
||||
try {
|
||||
startActivity(new Intent(Settings.ACTION_CAPTIONING_SETTINGS));
|
||||
} catch (final ActivityNotFoundException e) {
|
||||
|
@ -28,6 +28,11 @@ public abstract class BasePreferenceFragment extends PreferenceFragmentCompat {
|
||||
super.onCreate(savedInstanceState);
|
||||
}
|
||||
|
||||
protected void addPreferencesFromResourceRegistry() {
|
||||
addPreferencesFromResource(
|
||||
SettingsResourceRegistry.getInstance().getPreferencesResId(this.getClass()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(@NonNull final View rootView,
|
||||
@Nullable final Bundle savedInstanceState) {
|
||||
|
@ -1,5 +1,8 @@
|
||||
package org.schabi.newpipe.settings;
|
||||
|
||||
import static org.schabi.newpipe.extractor.utils.Utils.isBlank;
|
||||
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
@ -21,7 +24,6 @@ import org.schabi.newpipe.DownloaderImpl;
|
||||
import org.schabi.newpipe.NewPipeDatabase;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.error.ErrorUtil;
|
||||
import org.schabi.newpipe.error.ReCaptchaActivity;
|
||||
import org.schabi.newpipe.extractor.NewPipe;
|
||||
import org.schabi.newpipe.extractor.localization.ContentCountry;
|
||||
import org.schabi.newpipe.extractor.localization.Localization;
|
||||
@ -38,9 +40,6 @@ import java.util.Date;
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
|
||||
import static org.schabi.newpipe.extractor.utils.Utils.isBlank;
|
||||
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
|
||||
|
||||
public class ContentSettingsFragment extends BasePreferenceFragment {
|
||||
private static final String ZIP_MIME_TYPE = "application/zip";
|
||||
|
||||
@ -70,7 +69,7 @@ public class ContentSettingsFragment extends BasePreferenceFragment {
|
||||
importExportDataPathKey = getString(R.string.import_export_data_path);
|
||||
youtubeRestrictedModeEnabledKey = getString(R.string.youtube_restricted_mode_enabled);
|
||||
|
||||
addPreferencesFromResource(R.xml.content_settings);
|
||||
addPreferencesFromResourceRegistry();
|
||||
|
||||
final Preference importDataPreference = requirePreference(R.string.import_data);
|
||||
importDataPreference.setOnPreferenceClickListener((Preference p) -> {
|
||||
@ -105,21 +104,6 @@ public class ContentSettingsFragment extends BasePreferenceFragment {
|
||||
.getPreferredContentCountry(requireContext());
|
||||
initialLanguage = defaultPreferences.getString(getString(R.string.app_language_key), "en");
|
||||
|
||||
final Preference clearCookiePref = requirePreference(R.string.clear_cookie_key);
|
||||
clearCookiePref.setOnPreferenceClickListener(preference -> {
|
||||
defaultPreferences.edit()
|
||||
.putString(getString(R.string.recaptcha_cookies_key), "").apply();
|
||||
DownloaderImpl.getInstance().setCookie(ReCaptchaActivity.RECAPTCHA_COOKIES_KEY, "");
|
||||
Toast.makeText(getActivity(), R.string.recaptcha_cookies_cleared,
|
||||
Toast.LENGTH_SHORT).show();
|
||||
clearCookiePref.setVisible(false);
|
||||
return true;
|
||||
});
|
||||
|
||||
if (defaultPreferences.getString(getString(R.string.recaptcha_cookies_key), "").isEmpty()) {
|
||||
clearCookiePref.setVisible(false);
|
||||
}
|
||||
|
||||
findPreference(getString(R.string.download_thumbnail_key)).setOnPreferenceChangeListener(
|
||||
(preference, newValue) -> {
|
||||
PicassoHelper.setShouldLoadImages((Boolean) newValue);
|
||||
|
@ -0,0 +1,108 @@
|
||||
package org.schabi.newpipe.settings;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.preference.Preference;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.error.ErrorInfo;
|
||||
import org.schabi.newpipe.error.ErrorUtil;
|
||||
import org.schabi.newpipe.error.UserAction;
|
||||
import org.schabi.newpipe.util.PicassoHelper;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
public class DebugSettingsFragment extends BasePreferenceFragment {
|
||||
private static final String DUMMY = "Dummy";
|
||||
|
||||
@Override
|
||||
public void onCreatePreferences(final Bundle savedInstanceState, final String rootKey) {
|
||||
addPreferencesFromResourceRegistry();
|
||||
|
||||
final Preference allowHeapDumpingPreference
|
||||
= findPreference(getString(R.string.allow_heap_dumping_key));
|
||||
final Preference showMemoryLeaksPreference
|
||||
= findPreference(getString(R.string.show_memory_leaks_key));
|
||||
final Preference showImageIndicatorsPreference
|
||||
= findPreference(getString(R.string.show_image_indicators_key));
|
||||
final Preference crashTheAppPreference
|
||||
= findPreference(getString(R.string.crash_the_app_key));
|
||||
final Preference showErrorSnackbarPreference
|
||||
= findPreference(getString(R.string.show_error_snackbar_key));
|
||||
final Preference createErrorNotificationPreference
|
||||
= findPreference(getString(R.string.create_error_notification_key));
|
||||
|
||||
assert allowHeapDumpingPreference != null;
|
||||
assert showMemoryLeaksPreference != null;
|
||||
assert showImageIndicatorsPreference != null;
|
||||
assert crashTheAppPreference != null;
|
||||
assert showErrorSnackbarPreference != null;
|
||||
assert createErrorNotificationPreference != null;
|
||||
|
||||
final Optional<DebugSettingsBVDLeakCanaryAPI> optBVLeakCanary = getBVDLeakCanary();
|
||||
|
||||
allowHeapDumpingPreference.setEnabled(optBVLeakCanary.isPresent());
|
||||
showMemoryLeaksPreference.setEnabled(optBVLeakCanary.isPresent());
|
||||
|
||||
if (optBVLeakCanary.isPresent()) {
|
||||
final DebugSettingsBVDLeakCanaryAPI pdLeakCanary = optBVLeakCanary.get();
|
||||
|
||||
showMemoryLeaksPreference.setOnPreferenceClickListener(preference -> {
|
||||
startActivity(pdLeakCanary.getNewLeakDisplayActivityIntent());
|
||||
return true;
|
||||
});
|
||||
} else {
|
||||
allowHeapDumpingPreference.setSummary(R.string.leak_canary_not_available);
|
||||
showMemoryLeaksPreference.setSummary(R.string.leak_canary_not_available);
|
||||
}
|
||||
|
||||
showImageIndicatorsPreference.setOnPreferenceChangeListener((preference, newValue) -> {
|
||||
PicassoHelper.setIndicatorsEnabled((Boolean) newValue);
|
||||
return true;
|
||||
});
|
||||
|
||||
crashTheAppPreference.setOnPreferenceClickListener(preference -> {
|
||||
throw new RuntimeException(DUMMY);
|
||||
});
|
||||
|
||||
showErrorSnackbarPreference.setOnPreferenceClickListener(preference -> {
|
||||
ErrorUtil.showUiErrorSnackbar(DebugSettingsFragment.this,
|
||||
DUMMY, new RuntimeException(DUMMY));
|
||||
return true;
|
||||
});
|
||||
|
||||
createErrorNotificationPreference.setOnPreferenceClickListener(preference -> {
|
||||
ErrorUtil.createNotification(requireContext(),
|
||||
new ErrorInfo(new RuntimeException(DUMMY), UserAction.UI_ERROR, DUMMY));
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to find the {@link DebugSettingsBVDLeakCanaryAPI#IMPL_CLASS} and loads it if available.
|
||||
* @return An {@link Optional} which is empty if the implementation class couldn't be loaded.
|
||||
*/
|
||||
private Optional<DebugSettingsBVDLeakCanaryAPI> getBVDLeakCanary() {
|
||||
try {
|
||||
// Try to find the implementation of the LeakCanary API
|
||||
return Optional.of((DebugSettingsBVDLeakCanaryAPI)
|
||||
Class.forName(DebugSettingsBVDLeakCanaryAPI.IMPL_CLASS)
|
||||
.getDeclaredConstructor()
|
||||
.newInstance());
|
||||
} catch (final Exception e) {
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build variant dependent (BVD) leak canary API for this fragment.
|
||||
* Why is LeakCanary not used directly? Because it can't be assured
|
||||
*/
|
||||
public interface DebugSettingsBVDLeakCanaryAPI {
|
||||
String IMPL_CLASS =
|
||||
"org.schabi.newpipe.settings.DebugSettingsBVDLeakCanary";
|
||||
|
||||
Intent getNewLeakDisplayActivityIntent();
|
||||
}
|
||||
}
|
@ -54,7 +54,7 @@ public class DownloadSettingsFragment extends BasePreferenceFragment {
|
||||
|
||||
@Override
|
||||
public void onCreatePreferences(final Bundle savedInstanceState, final String rootKey) {
|
||||
addPreferencesFromResource(R.xml.download_settings);
|
||||
addPreferencesFromResourceRegistry();
|
||||
|
||||
downloadPathVideoPreference = getString(R.string.download_path_video_key);
|
||||
downloadPathAudioPreference = getString(R.string.download_path_audio_key);
|
||||
|
@ -8,9 +8,11 @@ import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.preference.Preference;
|
||||
|
||||
import org.schabi.newpipe.DownloaderImpl;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.error.ErrorInfo;
|
||||
import org.schabi.newpipe.error.ErrorUtil;
|
||||
import org.schabi.newpipe.error.ReCaptchaActivity;
|
||||
import org.schabi.newpipe.error.UserAction;
|
||||
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
||||
import org.schabi.newpipe.util.InfoCache;
|
||||
@ -29,7 +31,7 @@ public class HistorySettingsFragment extends BasePreferenceFragment {
|
||||
|
||||
@Override
|
||||
public void onCreatePreferences(final Bundle savedInstanceState, final String rootKey) {
|
||||
addPreferencesFromResource(R.xml.history_settings);
|
||||
addPreferencesFromResourceRegistry();
|
||||
|
||||
cacheWipeKey = getString(R.string.metadata_cache_wipe_key);
|
||||
viewsHistoryClearKey = getString(R.string.clear_views_history_key);
|
||||
@ -37,6 +39,21 @@ public class HistorySettingsFragment extends BasePreferenceFragment {
|
||||
searchHistoryClearKey = getString(R.string.clear_search_history_key);
|
||||
recordManager = new HistoryRecordManager(getActivity());
|
||||
disposables = new CompositeDisposable();
|
||||
|
||||
final Preference clearCookiePref = requirePreference(R.string.clear_cookie_key);
|
||||
clearCookiePref.setOnPreferenceClickListener(preference -> {
|
||||
defaultPreferences.edit()
|
||||
.putString(getString(R.string.recaptcha_cookies_key), "").apply();
|
||||
DownloaderImpl.getInstance().setCookie(ReCaptchaActivity.RECAPTCHA_COOKIES_KEY, "");
|
||||
Toast.makeText(getActivity(), R.string.recaptcha_cookies_cleared,
|
||||
Toast.LENGTH_SHORT).show();
|
||||
clearCookiePref.setEnabled(false);
|
||||
return true;
|
||||
});
|
||||
|
||||
if (defaultPreferences.getString(getString(R.string.recaptcha_cookies_key), "").isEmpty()) {
|
||||
clearCookiePref.setEnabled(false);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -1,8 +1,11 @@
|
||||
package org.schabi.newpipe.settings;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
|
||||
import androidx.preference.Preference;
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.schabi.newpipe.App;
|
||||
import org.schabi.newpipe.CheckForNewAppVersion;
|
||||
@ -12,16 +15,58 @@ import org.schabi.newpipe.R;
|
||||
public class MainSettingsFragment extends BasePreferenceFragment {
|
||||
public static final boolean DEBUG = MainActivity.DEBUG;
|
||||
|
||||
private SettingsActivity settingsActivity;
|
||||
|
||||
@Override
|
||||
public void onCreatePreferences(final Bundle savedInstanceState, final String rootKey) {
|
||||
addPreferencesFromResource(R.xml.main_settings);
|
||||
addPreferencesFromResourceRegistry();
|
||||
|
||||
setHasOptionsMenu(true); // Otherwise onCreateOptionsMenu is not called
|
||||
|
||||
// Check if the app is updatable
|
||||
if (!CheckForNewAppVersion.isReleaseApk(App.getApp())) {
|
||||
final Preference update
|
||||
= findPreference(getString(R.string.update_pref_screen_key));
|
||||
getPreferenceScreen().removePreference(update);
|
||||
getPreferenceScreen().removePreference(
|
||||
findPreference(getString(R.string.update_pref_screen_key)));
|
||||
|
||||
defaultPreferences.edit().putBoolean(getString(R.string.update_app_key), false).apply();
|
||||
}
|
||||
|
||||
// Hide debug preferences in RELEASE build variant
|
||||
if (!DEBUG) {
|
||||
getPreferenceScreen().removePreference(
|
||||
findPreference(getString(R.string.debug_pref_screen_key)));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreateOptionsMenu(
|
||||
@NonNull final Menu menu,
|
||||
@NonNull final MenuInflater inflater
|
||||
) {
|
||||
super.onCreateOptionsMenu(menu, inflater);
|
||||
|
||||
// -- Link settings activity and register menu --
|
||||
settingsActivity = (SettingsActivity) getActivity();
|
||||
|
||||
inflater.inflate(R.menu.menu_settings_main_fragment, menu);
|
||||
|
||||
final MenuItem menuSearchItem = menu.getItem(0);
|
||||
|
||||
settingsActivity.setMenuSearchItem(menuSearchItem);
|
||||
|
||||
menuSearchItem.setOnMenuItemClickListener(ev -> {
|
||||
settingsActivity.setSearchActive(true);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
// Unlink activity so that we don't get memory problems
|
||||
if (settingsActivity != null) {
|
||||
settingsActivity.setMenuSearchItem(null);
|
||||
settingsActivity = null;
|
||||
}
|
||||
super.onDestroy();
|
||||
}
|
||||
}
|
||||
|
@ -7,7 +7,7 @@ import org.schabi.newpipe.R
|
||||
|
||||
class NotificationSettingsFragment : BasePreferenceFragment() {
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
addPreferencesFromResource(R.xml.notification_settings)
|
||||
addPreferencesFromResourceRegistry()
|
||||
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
|
||||
val colorizePref: Preference? = findPreference(getString(R.string.notification_colorize_key))
|
||||
|
@ -1,6 +1,5 @@
|
||||
package org.schabi.newpipe.settings;
|
||||
|
||||
import android.content.DialogInterface;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
@ -51,16 +50,11 @@ public class SelectKioskFragment extends DialogFragment {
|
||||
private SelectKioskAdapter selectKioskAdapter = null;
|
||||
|
||||
private OnSelectedListener onSelectedListener = null;
|
||||
private OnCancelListener onCancelListener = null;
|
||||
|
||||
public void setOnSelectedListener(final OnSelectedListener listener) {
|
||||
onSelectedListener = listener;
|
||||
}
|
||||
|
||||
public void setOnCancelListener(final OnCancelListener listener) {
|
||||
onCancelListener = listener;
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Init
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
@ -91,14 +85,6 @@ public class SelectKioskFragment extends DialogFragment {
|
||||
// Handle actions
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public void onCancel(@NonNull final DialogInterface dialogInterface) {
|
||||
super.onCancel(dialogInterface);
|
||||
if (onCancelListener != null) {
|
||||
onCancelListener.onCancel();
|
||||
}
|
||||
}
|
||||
|
||||
private void clickedItem(final SelectKioskAdapter.Entry entry) {
|
||||
if (onSelectedListener != null) {
|
||||
onSelectedListener.onKioskSelected(entry.serviceId, entry.kioskId, entry.kioskName);
|
||||
@ -114,10 +100,6 @@ public class SelectKioskFragment extends DialogFragment {
|
||||
void onKioskSelected(int serviceId, String kioskId, String kioskName);
|
||||
}
|
||||
|
||||
public interface OnCancelListener {
|
||||
void onCancel();
|
||||
}
|
||||
|
||||
private class SelectKioskAdapter
|
||||
extends RecyclerView.Adapter<SelectKioskAdapter.SelectKioskItemHolder> {
|
||||
private final List<Entry> kioskList = new Vector<>();
|
||||
|
@ -1,22 +1,48 @@
|
||||
package org.schabi.newpipe.settings;
|
||||
|
||||
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.widget.EditText;
|
||||
|
||||
import androidx.annotation.IdRes;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
import androidx.preference.Preference;
|
||||
import androidx.preference.PreferenceFragmentCompat;
|
||||
|
||||
import com.jakewharton.rxbinding4.widget.RxTextView;
|
||||
|
||||
import org.schabi.newpipe.App;
|
||||
import org.schabi.newpipe.CheckForNewAppVersion;
|
||||
import org.schabi.newpipe.MainActivity;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.databinding.SettingsLayoutBinding;
|
||||
import org.schabi.newpipe.settings.preferencesearch.PreferenceParser;
|
||||
import org.schabi.newpipe.settings.preferencesearch.PreferenceSearchConfiguration;
|
||||
import org.schabi.newpipe.settings.preferencesearch.PreferenceSearchFragment;
|
||||
import org.schabi.newpipe.settings.preferencesearch.PreferenceSearchItem;
|
||||
import org.schabi.newpipe.settings.preferencesearch.PreferenceSearchResultHighlighter;
|
||||
import org.schabi.newpipe.settings.preferencesearch.PreferenceSearchResultListener;
|
||||
import org.schabi.newpipe.settings.preferencesearch.PreferenceSearcher;
|
||||
import org.schabi.newpipe.util.DeviceUtils;
|
||||
import org.schabi.newpipe.util.KeyboardUtil;
|
||||
import org.schabi.newpipe.util.ThemeHelper;
|
||||
import org.schabi.newpipe.views.FocusOverlayView;
|
||||
|
||||
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import icepick.Icepick;
|
||||
import icepick.State;
|
||||
|
||||
/*
|
||||
* Created by Christian Schabesberger on 31.08.15.
|
||||
@ -38,21 +64,54 @@ import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
|
||||
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
public class SettingsActivity extends AppCompatActivity
|
||||
implements BasePreferenceFragment.OnPreferenceStartFragmentCallback {
|
||||
public class SettingsActivity extends AppCompatActivity implements
|
||||
PreferenceFragmentCompat.OnPreferenceStartFragmentCallback,
|
||||
PreferenceSearchResultListener {
|
||||
private static final String TAG = "SettingsActivity";
|
||||
private static final boolean DEBUG = MainActivity.DEBUG;
|
||||
|
||||
@IdRes
|
||||
private static final int FRAGMENT_HOLDER_ID = R.id.settings_fragment_holder;
|
||||
|
||||
private PreferenceSearchFragment searchFragment;
|
||||
|
||||
@Nullable
|
||||
private MenuItem menuSearchItem;
|
||||
|
||||
private View searchContainer;
|
||||
private EditText searchEditText;
|
||||
|
||||
// State
|
||||
@State
|
||||
String searchText;
|
||||
@State
|
||||
boolean wasSearchActive;
|
||||
|
||||
@Override
|
||||
protected void onCreate(final Bundle savedInstanceBundle) {
|
||||
setTheme(ThemeHelper.getSettingsThemeStyle(this));
|
||||
assureCorrectAppLanguage(this);
|
||||
|
||||
super.onCreate(savedInstanceBundle);
|
||||
Icepick.restoreInstanceState(this, savedInstanceBundle);
|
||||
final boolean restored = savedInstanceBundle != null;
|
||||
|
||||
final SettingsLayoutBinding settingsLayoutBinding =
|
||||
SettingsLayoutBinding.inflate(getLayoutInflater());
|
||||
setContentView(settingsLayoutBinding.getRoot());
|
||||
initSearch(settingsLayoutBinding, restored);
|
||||
|
||||
setSupportActionBar(settingsLayoutBinding.settingsToolbarLayout.toolbar);
|
||||
|
||||
if (savedInstanceBundle == null) {
|
||||
if (restored) {
|
||||
// Restore state
|
||||
if (this.wasSearchActive) {
|
||||
setSearchActive(true);
|
||||
if (!TextUtils.isEmpty(this.searchText)) {
|
||||
this.searchEditText.setText(this.searchText);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
getSupportFragmentManager().beginTransaction()
|
||||
.replace(R.id.settings_fragment_holder, new MainSettingsFragment())
|
||||
.commit();
|
||||
@ -63,6 +122,12 @@ public class SettingsActivity extends AppCompatActivity
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onSaveInstanceState(@NonNull final Bundle outState) {
|
||||
super.onSaveInstanceState(outState);
|
||||
Icepick.saveInstanceState(this, outState);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(final Menu menu) {
|
||||
final ActionBar actionBar = getSupportActionBar();
|
||||
@ -74,10 +139,25 @@ public class SettingsActivity extends AppCompatActivity
|
||||
return super.onCreateOptionsMenu(menu);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
if (isSearchActive()) {
|
||||
setSearchActive(false);
|
||||
return;
|
||||
}
|
||||
super.onBackPressed();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(final MenuItem item) {
|
||||
final int id = item.getItemId();
|
||||
if (id == android.R.id.home) {
|
||||
// Check if the search is active and if so: Close it
|
||||
if (isSearchActive()) {
|
||||
setSearchActive(false);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (getSupportFragmentManager().getBackStackEntryCount() == 0) {
|
||||
finish();
|
||||
} else {
|
||||
@ -91,14 +171,221 @@ public class SettingsActivity extends AppCompatActivity
|
||||
@Override
|
||||
public boolean onPreferenceStartFragment(final PreferenceFragmentCompat caller,
|
||||
final Preference preference) {
|
||||
final Fragment fragment = Fragment
|
||||
.instantiate(this, preference.getFragment(), preference.getExtras());
|
||||
showSettingsFragment(instantiateFragment(preference.getFragment()));
|
||||
return true;
|
||||
}
|
||||
|
||||
private Fragment instantiateFragment(@NonNull final String className) {
|
||||
return getSupportFragmentManager()
|
||||
.getFragmentFactory()
|
||||
.instantiate(this.getClassLoader(), className);
|
||||
}
|
||||
|
||||
private void showSettingsFragment(final Fragment fragment) {
|
||||
getSupportFragmentManager().beginTransaction()
|
||||
.setCustomAnimations(R.animator.custom_fade_in, R.animator.custom_fade_out,
|
||||
R.animator.custom_fade_in, R.animator.custom_fade_out)
|
||||
.replace(R.id.settings_fragment_holder, fragment)
|
||||
.replace(FRAGMENT_HOLDER_ID, fragment)
|
||||
.addToBackStack(null)
|
||||
.commit();
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
setMenuSearchItem(null);
|
||||
searchFragment = null;
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Search
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
//region Search
|
||||
|
||||
private void initSearch(
|
||||
final SettingsLayoutBinding settingsLayoutBinding,
|
||||
final boolean restored
|
||||
) {
|
||||
searchContainer =
|
||||
settingsLayoutBinding.settingsToolbarLayout.toolbar
|
||||
.findViewById(R.id.toolbar_search_container);
|
||||
|
||||
// Configure input field for search
|
||||
searchEditText = searchContainer.findViewById(R.id.toolbar_search_edit_text);
|
||||
RxTextView.textChanges(searchEditText)
|
||||
// Wait some time after the last input before actually searching
|
||||
.debounce(200, TimeUnit.MILLISECONDS)
|
||||
.subscribe(v -> runOnUiThread(this::onSearchChanged));
|
||||
|
||||
// Configure clear button
|
||||
searchContainer.findViewById(R.id.toolbar_search_clear)
|
||||
.setOnClickListener(ev -> resetSearchText());
|
||||
|
||||
ensureSearchRepresentsApplicationState();
|
||||
|
||||
// Build search configuration using SettingsResourceRegistry
|
||||
final PreferenceSearchConfiguration config = new PreferenceSearchConfiguration();
|
||||
|
||||
|
||||
// Build search items
|
||||
final PreferenceParser parser = new PreferenceParser(getApplicationContext(), config);
|
||||
final PreferenceSearcher searcher = new PreferenceSearcher(config);
|
||||
|
||||
// Find all searchable SettingsResourceRegistry fragments
|
||||
SettingsResourceRegistry.getInstance().getAllEntries().stream()
|
||||
.filter(SettingsResourceRegistry.SettingRegistryEntry::isSearchable)
|
||||
// Get the resId
|
||||
.map(SettingsResourceRegistry.SettingRegistryEntry::getPreferencesResId)
|
||||
// Parse
|
||||
.map(parser::parse)
|
||||
// Add it to the searcher
|
||||
.forEach(searcher::add);
|
||||
|
||||
if (restored) {
|
||||
searchFragment = (PreferenceSearchFragment) getSupportFragmentManager()
|
||||
.findFragmentByTag(PreferenceSearchFragment.NAME);
|
||||
if (searchFragment != null) {
|
||||
// Hide/Remove the search fragment otherwise we get an exception
|
||||
// when adding it (because it's already present)
|
||||
hideSearchFragment();
|
||||
}
|
||||
}
|
||||
if (searchFragment == null) {
|
||||
searchFragment = new PreferenceSearchFragment();
|
||||
}
|
||||
searchFragment.setSearcher(searcher);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures that the search shows the correct/available search results.
|
||||
* <br/>
|
||||
* Some features are e.g. only available for debug builds, these should not
|
||||
* be found when searching inside a release.
|
||||
*/
|
||||
private void ensureSearchRepresentsApplicationState() {
|
||||
// Check if the update settings are available
|
||||
if (!CheckForNewAppVersion.isReleaseApk(App.getApp())) {
|
||||
SettingsResourceRegistry.getInstance()
|
||||
.getEntryByPreferencesResId(R.xml.update_settings)
|
||||
.setSearchable(false);
|
||||
}
|
||||
|
||||
// Hide debug preferences in RELEASE build variant
|
||||
if (DEBUG) {
|
||||
SettingsResourceRegistry.getInstance()
|
||||
.getEntryByPreferencesResId(R.xml.debug_settings)
|
||||
.setSearchable(true);
|
||||
}
|
||||
}
|
||||
|
||||
public void setMenuSearchItem(final MenuItem menuSearchItem) {
|
||||
this.menuSearchItem = menuSearchItem;
|
||||
|
||||
// Ensure that the item is in the correct state when adding it. This is due to
|
||||
// Android's lifecycle (the Activity is recreated before the Fragment that registers this)
|
||||
if (menuSearchItem != null) {
|
||||
menuSearchItem.setVisible(!isSearchActive());
|
||||
}
|
||||
}
|
||||
|
||||
public void setSearchActive(final boolean active) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "setSearchActive called active=" + active);
|
||||
}
|
||||
|
||||
// Ignore if search is already in correct state
|
||||
if (isSearchActive() == active) {
|
||||
return;
|
||||
}
|
||||
|
||||
wasSearchActive = active;
|
||||
|
||||
searchContainer.setVisibility(active ? View.VISIBLE : View.GONE);
|
||||
if (menuSearchItem != null) {
|
||||
menuSearchItem.setVisible(!active);
|
||||
}
|
||||
|
||||
if (active) {
|
||||
getSupportFragmentManager()
|
||||
.beginTransaction()
|
||||
.add(FRAGMENT_HOLDER_ID, searchFragment, PreferenceSearchFragment.NAME)
|
||||
.addToBackStack(PreferenceSearchFragment.NAME)
|
||||
.commit();
|
||||
|
||||
KeyboardUtil.showKeyboard(this, searchEditText);
|
||||
} else if (searchFragment != null) {
|
||||
hideSearchFragment();
|
||||
getSupportFragmentManager()
|
||||
.popBackStack(
|
||||
PreferenceSearchFragment.NAME,
|
||||
FragmentManager.POP_BACK_STACK_INCLUSIVE);
|
||||
|
||||
KeyboardUtil.hideKeyboard(this, searchEditText);
|
||||
}
|
||||
|
||||
resetSearchText();
|
||||
}
|
||||
|
||||
private void hideSearchFragment() {
|
||||
getSupportFragmentManager().beginTransaction().remove(searchFragment).commit();
|
||||
}
|
||||
|
||||
private void resetSearchText() {
|
||||
searchEditText.setText("");
|
||||
}
|
||||
|
||||
private boolean isSearchActive() {
|
||||
return searchContainer.getVisibility() == View.VISIBLE;
|
||||
}
|
||||
|
||||
private void onSearchChanged() {
|
||||
if (!isSearchActive()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (searchFragment != null) {
|
||||
searchText = this.searchEditText.getText().toString();
|
||||
searchFragment.updateSearchResults(searchText);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSearchResultClicked(@NonNull final PreferenceSearchItem result) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "onSearchResultClicked called result=" + result);
|
||||
}
|
||||
|
||||
// Hide the search
|
||||
setSearchActive(false);
|
||||
|
||||
// -- Highlight the result --
|
||||
// Find out which fragment class we need
|
||||
final Class<? extends Fragment> targetedFragmentClass =
|
||||
SettingsResourceRegistry.getInstance()
|
||||
.getFragmentClass(result.getSearchIndexItemResId());
|
||||
|
||||
if (targetedFragmentClass == null) {
|
||||
// This should never happen
|
||||
Log.w(TAG, "Unable to locate fragment class for resId="
|
||||
+ result.getSearchIndexItemResId());
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the currentFragment is the one which contains the result
|
||||
Fragment currentFragment =
|
||||
getSupportFragmentManager().findFragmentById(FRAGMENT_HOLDER_ID);
|
||||
if (!targetedFragmentClass.equals(currentFragment.getClass())) {
|
||||
// If it's not the correct one display the correct one
|
||||
currentFragment = instantiateFragment(targetedFragmentClass.getName());
|
||||
showSettingsFragment(currentFragment);
|
||||
}
|
||||
|
||||
// Run the highlighting
|
||||
if (currentFragment instanceof PreferenceFragmentCompat) {
|
||||
PreferenceSearchResultHighlighter
|
||||
.highlight(result, (PreferenceFragmentCompat) currentFragment);
|
||||
}
|
||||
}
|
||||
|
||||
//endregion
|
||||
}
|
||||
|
@ -0,0 +1,148 @@
|
||||
package org.schabi.newpipe.settings;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.XmlRes;
|
||||
import androidx.fragment.app.Fragment;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* A registry that contains information about SettingsFragments.
|
||||
* <br/>
|
||||
* includes:
|
||||
* <ul>
|
||||
* <li>Class of the SettingsFragment</li>
|
||||
* <li>XML-Resource</li>
|
||||
* <li>...</li>
|
||||
* </ul>
|
||||
*
|
||||
* E.g. used by the preference search.
|
||||
*/
|
||||
public final class SettingsResourceRegistry {
|
||||
|
||||
private static final SettingsResourceRegistry INSTANCE = new SettingsResourceRegistry();
|
||||
|
||||
private final Set<SettingRegistryEntry> registeredEntries = new HashSet<>();
|
||||
|
||||
private SettingsResourceRegistry() {
|
||||
add(MainSettingsFragment.class, R.xml.main_settings).setSearchable(false);
|
||||
|
||||
add(AppearanceSettingsFragment.class, R.xml.appearance_settings);
|
||||
add(ContentSettingsFragment.class, R.xml.content_settings);
|
||||
add(DebugSettingsFragment.class, R.xml.debug_settings).setSearchable(false);
|
||||
add(DownloadSettingsFragment.class, R.xml.download_settings);
|
||||
add(HistorySettingsFragment.class, R.xml.history_settings);
|
||||
add(NotificationSettingsFragment.class, R.xml.notification_settings);
|
||||
add(UpdateSettingsFragment.class, R.xml.update_settings);
|
||||
add(VideoAudioSettingsFragment.class, R.xml.video_audio_settings);
|
||||
}
|
||||
|
||||
private SettingRegistryEntry add(
|
||||
@NonNull final Class<? extends Fragment> fragmentClass,
|
||||
@XmlRes final int preferencesResId
|
||||
) {
|
||||
final SettingRegistryEntry entry =
|
||||
new SettingRegistryEntry(fragmentClass, preferencesResId);
|
||||
this.registeredEntries.add(entry);
|
||||
return entry;
|
||||
}
|
||||
|
||||
public SettingRegistryEntry getEntryByFragmentClass(
|
||||
final Class<? extends Fragment> fragmentClass
|
||||
) {
|
||||
Objects.requireNonNull(fragmentClass);
|
||||
return registeredEntries.stream()
|
||||
.filter(e -> Objects.equals(e.getFragmentClass(), fragmentClass))
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
public SettingRegistryEntry getEntryByPreferencesResId(@XmlRes final int preferencesResId) {
|
||||
return registeredEntries.stream()
|
||||
.filter(e -> Objects.equals(e.getPreferencesResId(), preferencesResId))
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
public int getPreferencesResId(@NonNull final Class<? extends Fragment> fragmentClass) {
|
||||
final SettingRegistryEntry entry = getEntryByFragmentClass(fragmentClass);
|
||||
if (entry == null) {
|
||||
return -1;
|
||||
}
|
||||
return entry.getPreferencesResId();
|
||||
}
|
||||
|
||||
public Class<? extends Fragment> getFragmentClass(@XmlRes final int preferencesResId) {
|
||||
final SettingRegistryEntry entry = getEntryByPreferencesResId(preferencesResId);
|
||||
if (entry == null) {
|
||||
return null;
|
||||
}
|
||||
return entry.getFragmentClass();
|
||||
}
|
||||
|
||||
public Set<SettingRegistryEntry> getAllEntries() {
|
||||
return new HashSet<>(registeredEntries);
|
||||
}
|
||||
|
||||
public static SettingsResourceRegistry getInstance() {
|
||||
return INSTANCE;
|
||||
}
|
||||
|
||||
|
||||
public static class SettingRegistryEntry {
|
||||
@NonNull
|
||||
private final Class<? extends Fragment> fragmentClass;
|
||||
@XmlRes
|
||||
private final int preferencesResId;
|
||||
|
||||
private boolean searchable = true;
|
||||
|
||||
public SettingRegistryEntry(
|
||||
@NonNull final Class<? extends Fragment> fragmentClass,
|
||||
@XmlRes final int preferencesResId
|
||||
) {
|
||||
this.fragmentClass = Objects.requireNonNull(fragmentClass);
|
||||
this.preferencesResId = preferencesResId;
|
||||
}
|
||||
|
||||
@SuppressWarnings("HiddenField")
|
||||
public SettingRegistryEntry setSearchable(final boolean searchable) {
|
||||
this.searchable = searchable;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Class<? extends Fragment> getFragmentClass() {
|
||||
return fragmentClass;
|
||||
}
|
||||
|
||||
public int getPreferencesResId() {
|
||||
return preferencesResId;
|
||||
}
|
||||
|
||||
public boolean isSearchable() {
|
||||
return searchable;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(final Object o) {
|
||||
if (this == o) {
|
||||
return true;
|
||||
}
|
||||
if (o == null || getClass() != o.getClass()) {
|
||||
return false;
|
||||
}
|
||||
final SettingRegistryEntry that = (SettingRegistryEntry) o;
|
||||
return getPreferencesResId() == that.getPreferencesResId()
|
||||
&& getFragmentClass().equals(that.getFragmentClass());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(getFragmentClass(), getPreferencesResId());
|
||||
}
|
||||
}
|
||||
}
|
@ -38,7 +38,7 @@ public class UpdateSettingsFragment extends BasePreferenceFragment {
|
||||
|
||||
@Override
|
||||
public void onCreatePreferences(final Bundle savedInstanceState, final String rootKey) {
|
||||
addPreferencesFromResource(R.xml.update_settings);
|
||||
addPreferencesFromResourceRegistry();
|
||||
|
||||
findPreference(getString(R.string.update_app_key))
|
||||
.setOnPreferenceChangeListener(updatePreferenceChange);
|
||||
|
@ -23,7 +23,7 @@ public class VideoAudioSettingsFragment extends BasePreferenceFragment {
|
||||
|
||||
@Override
|
||||
public void onCreatePreferences(final Bundle savedInstanceState, final String rootKey) {
|
||||
addPreferencesFromResource(R.xml.video_audio_settings);
|
||||
addPreferencesFromResourceRegistry();
|
||||
|
||||
updateSeekOptions();
|
||||
|
||||
|
@ -0,0 +1,111 @@
|
||||
package org.schabi.newpipe.settings.preferencesearch;
|
||||
|
||||
import android.text.TextUtils;
|
||||
|
||||
import org.apache.commons.text.similarity.FuzzyScore;
|
||||
|
||||
import java.util.Comparator;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
public class PreferenceFuzzySearchFunction
|
||||
implements PreferenceSearchConfiguration.PreferenceSearchFunction {
|
||||
|
||||
private static final FuzzyScore FUZZY_SCORE = new FuzzyScore(Locale.ROOT);
|
||||
|
||||
@Override
|
||||
public Stream<PreferenceSearchItem> search(
|
||||
final Stream<PreferenceSearchItem> allAvailable,
|
||||
final String keyword
|
||||
) {
|
||||
final int maxScore = (keyword.length() + 1) * 3 - 2; // First can't get +2 bonus score
|
||||
|
||||
return allAvailable
|
||||
// General search
|
||||
// Check all fields if anyone contains something that kind of matches the keyword
|
||||
.map(item -> new FuzzySearchGeneralDTO(item, keyword))
|
||||
.filter(dto -> dto.getScore() / maxScore >= 0.3f)
|
||||
.map(FuzzySearchGeneralDTO::getItem)
|
||||
// Specific search - Used for determining order of search results
|
||||
// Calculate a score based on specific search fields
|
||||
.map(item -> new FuzzySearchSpecificDTO(item, keyword))
|
||||
.sorted(Comparator.comparing(FuzzySearchSpecificDTO::getScore).reversed())
|
||||
.map(FuzzySearchSpecificDTO::getItem)
|
||||
// Limit the amount of search results
|
||||
.limit(20);
|
||||
}
|
||||
|
||||
static class FuzzySearchGeneralDTO {
|
||||
private final PreferenceSearchItem item;
|
||||
private final float score;
|
||||
|
||||
FuzzySearchGeneralDTO(
|
||||
final PreferenceSearchItem item,
|
||||
final String keyword) {
|
||||
this.item = item;
|
||||
this.score = FUZZY_SCORE.fuzzyScore(
|
||||
TextUtils.join(";", item.getAllRelevantSearchFields()),
|
||||
keyword);
|
||||
}
|
||||
|
||||
public PreferenceSearchItem getItem() {
|
||||
return item;
|
||||
}
|
||||
|
||||
public float getScore() {
|
||||
return score;
|
||||
}
|
||||
}
|
||||
|
||||
static class FuzzySearchSpecificDTO {
|
||||
private static final Map<Function<PreferenceSearchItem, String>, Float> WEIGHT_MAP = Map.of(
|
||||
// The user will most likely look for the title -> prioritize it
|
||||
PreferenceSearchItem::getTitle, 1.5f,
|
||||
// The summary is also important as it usually contains a larger desc
|
||||
// Example: Searching for '4k' → 'show higher resolution' is shown
|
||||
PreferenceSearchItem::getSummary, 1f,
|
||||
// Entries are also important as they provide all known/possible values
|
||||
// Example: Searching where the resolution can be changed to 720p
|
||||
PreferenceSearchItem::getEntries, 1f
|
||||
);
|
||||
|
||||
private final PreferenceSearchItem item;
|
||||
private final float score;
|
||||
|
||||
FuzzySearchSpecificDTO(
|
||||
final PreferenceSearchItem item,
|
||||
final String keyword) {
|
||||
this.item = item;
|
||||
|
||||
float attributeScoreSum = 0;
|
||||
int countOfAttributesWithScore = 0;
|
||||
for (final Map.Entry<Function<PreferenceSearchItem, String>, Float> we
|
||||
: WEIGHT_MAP.entrySet()) {
|
||||
final String valueToProcess = we.getKey().apply(item);
|
||||
if (valueToProcess.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
attributeScoreSum +=
|
||||
FUZZY_SCORE.fuzzyScore(valueToProcess, keyword) * we.getValue();
|
||||
countOfAttributesWithScore++;
|
||||
}
|
||||
|
||||
if (countOfAttributesWithScore != 0) {
|
||||
this.score = attributeScoreSum / countOfAttributesWithScore;
|
||||
} else {
|
||||
this.score = 0;
|
||||
}
|
||||
}
|
||||
|
||||
public PreferenceSearchItem getItem() {
|
||||
return item;
|
||||
}
|
||||
|
||||
public float getScore() {
|
||||
return score;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,199 @@
|
||||
package org.schabi.newpipe.settings.preferencesearch;
|
||||
|
||||
import android.content.Context;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.XmlRes;
|
||||
import androidx.preference.PreferenceManager;
|
||||
|
||||
import org.xmlpull.v1.XmlPullParser;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Parses the corresponding preference-file(s).
|
||||
*/
|
||||
public class PreferenceParser {
|
||||
private static final String TAG = "PreferenceParser";
|
||||
|
||||
private static final String NS_ANDROID = "http://schemas.android.com/apk/res/android";
|
||||
private static final String NS_SEARCH = "http://schemas.android.com/apk/preferencesearch";
|
||||
|
||||
private final Context context;
|
||||
private final Map<String, ?> allPreferences;
|
||||
private final PreferenceSearchConfiguration searchConfiguration;
|
||||
|
||||
public PreferenceParser(
|
||||
final Context context,
|
||||
final PreferenceSearchConfiguration searchConfiguration
|
||||
) {
|
||||
this.context = context;
|
||||
this.allPreferences = PreferenceManager.getDefaultSharedPreferences(context).getAll();
|
||||
this.searchConfiguration = searchConfiguration;
|
||||
}
|
||||
|
||||
public List<PreferenceSearchItem> parse(
|
||||
@XmlRes final int resId
|
||||
) {
|
||||
final List<PreferenceSearchItem> results = new ArrayList<>();
|
||||
final XmlPullParser xpp = context.getResources().getXml(resId);
|
||||
|
||||
try {
|
||||
xpp.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true);
|
||||
xpp.setFeature(XmlPullParser.FEATURE_REPORT_NAMESPACE_ATTRIBUTES, true);
|
||||
|
||||
final List<String> breadcrumbs = new ArrayList<>();
|
||||
while (xpp.getEventType() != XmlPullParser.END_DOCUMENT) {
|
||||
if (xpp.getEventType() == XmlPullParser.START_TAG) {
|
||||
final PreferenceSearchItem result = parseSearchResult(
|
||||
xpp,
|
||||
joinBreadcrumbs(breadcrumbs),
|
||||
resId
|
||||
);
|
||||
|
||||
if (!searchConfiguration.getParserIgnoreElements().contains(xpp.getName())
|
||||
&& result.hasData()
|
||||
&& !"true".equals(getAttribute(xpp, NS_SEARCH, "ignore"))) {
|
||||
results.add(result);
|
||||
}
|
||||
if (searchConfiguration.getParserContainerElements().contains(xpp.getName())) {
|
||||
// This code adds breadcrumbs for certain containers (e.g. PreferenceScreen)
|
||||
// Example: Video and Audio > Player
|
||||
breadcrumbs.add(result.getTitle() == null ? "" : result.getTitle());
|
||||
}
|
||||
} else if (xpp.getEventType() == XmlPullParser.END_TAG
|
||||
&& searchConfiguration.getParserContainerElements()
|
||||
.contains(xpp.getName())) {
|
||||
breadcrumbs.remove(breadcrumbs.size() - 1);
|
||||
}
|
||||
|
||||
xpp.next();
|
||||
}
|
||||
} catch (final Exception e) {
|
||||
Log.w(TAG, "Failed to parse resid=" + resId, e);
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
private String joinBreadcrumbs(final List<String> breadcrumbs) {
|
||||
return breadcrumbs.stream()
|
||||
.filter(crumb -> !TextUtils.isEmpty(crumb))
|
||||
.collect(Collectors.joining(" > "));
|
||||
}
|
||||
|
||||
private String getAttribute(
|
||||
final XmlPullParser xpp,
|
||||
@NonNull final String attribute
|
||||
) {
|
||||
final String nsSearchAttr = getAttribute(xpp, NS_SEARCH, attribute);
|
||||
if (nsSearchAttr != null) {
|
||||
return nsSearchAttr;
|
||||
}
|
||||
return getAttribute(xpp, NS_ANDROID, attribute);
|
||||
}
|
||||
|
||||
private String getAttribute(
|
||||
final XmlPullParser xpp,
|
||||
@NonNull final String namespace,
|
||||
@NonNull final String attribute
|
||||
) {
|
||||
return xpp.getAttributeValue(namespace, attribute);
|
||||
}
|
||||
|
||||
private PreferenceSearchItem parseSearchResult(
|
||||
final XmlPullParser xpp,
|
||||
final String breadcrumbs,
|
||||
@XmlRes final int searchIndexItemResId
|
||||
) {
|
||||
final String key = readString(getAttribute(xpp, "key"));
|
||||
final String[] entries = readStringArray(getAttribute(xpp, "entries"));
|
||||
final String[] entryValues = readStringArray(getAttribute(xpp, "entryValues"));
|
||||
|
||||
return new PreferenceSearchItem(
|
||||
key,
|
||||
tryFillInPreferenceValue(
|
||||
readString(getAttribute(xpp, "title")),
|
||||
key,
|
||||
entries,
|
||||
entryValues),
|
||||
tryFillInPreferenceValue(
|
||||
readString(getAttribute(xpp, "summary")),
|
||||
key,
|
||||
entries,
|
||||
entryValues),
|
||||
TextUtils.join(",", entries),
|
||||
breadcrumbs,
|
||||
searchIndexItemResId
|
||||
);
|
||||
}
|
||||
|
||||
private String[] readStringArray(@Nullable final String s) {
|
||||
if (s == null) {
|
||||
return new String[0];
|
||||
}
|
||||
if (s.startsWith("@")) {
|
||||
try {
|
||||
return context.getResources().getStringArray(Integer.parseInt(s.substring(1)));
|
||||
} catch (final Exception e) {
|
||||
Log.w(TAG, "Unable to readStringArray from '" + s + "'", e);
|
||||
}
|
||||
}
|
||||
return new String[0];
|
||||
}
|
||||
|
||||
private String readString(@Nullable final String s) {
|
||||
if (s == null) {
|
||||
return "";
|
||||
}
|
||||
if (s.startsWith("@")) {
|
||||
try {
|
||||
return context.getString(Integer.parseInt(s.substring(1)));
|
||||
} catch (final Exception e) {
|
||||
Log.w(TAG, "Unable to readString from '" + s + "'", e);
|
||||
}
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
private String tryFillInPreferenceValue(
|
||||
@Nullable final String s,
|
||||
@Nullable final String key,
|
||||
final String[] entries,
|
||||
final String[] entryValues
|
||||
) {
|
||||
if (s == null) {
|
||||
return "";
|
||||
}
|
||||
if (key == null) {
|
||||
return s;
|
||||
}
|
||||
|
||||
// Resolve value
|
||||
Object prefValue = allPreferences.get(key);
|
||||
if (prefValue == null) {
|
||||
return s;
|
||||
}
|
||||
|
||||
/*
|
||||
* Resolve ListPreference values
|
||||
*
|
||||
* entryValues = Values/Keys that are saved
|
||||
* entries = Actual human readable names
|
||||
*/
|
||||
if (entries.length > 0 && entryValues.length == entries.length) {
|
||||
final int entryIndex = Arrays.asList(entryValues).indexOf(prefValue);
|
||||
if (entryIndex != -1) {
|
||||
prefValue = entries[entryIndex];
|
||||
}
|
||||
}
|
||||
|
||||
return String.format(s, prefValue.toString());
|
||||
}
|
||||
}
|
@ -0,0 +1,87 @@
|
||||
package org.schabi.newpipe.settings.preferencesearch;
|
||||
|
||||
import android.text.TextUtils;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import org.schabi.newpipe.databinding.SettingsPreferencesearchListItemResultBinding;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
class PreferenceSearchAdapter
|
||||
extends RecyclerView.Adapter<PreferenceSearchAdapter.PreferenceViewHolder> {
|
||||
private List<PreferenceSearchItem> dataset = new ArrayList<>();
|
||||
private Consumer<PreferenceSearchItem> onItemClickListener;
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public PreferenceViewHolder onCreateViewHolder(
|
||||
@NonNull final ViewGroup parent,
|
||||
final int viewType
|
||||
) {
|
||||
return new PreferenceViewHolder(
|
||||
SettingsPreferencesearchListItemResultBinding.inflate(
|
||||
LayoutInflater.from(parent.getContext()),
|
||||
parent,
|
||||
false));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(
|
||||
@NonNull final PreferenceViewHolder holder,
|
||||
final int position
|
||||
) {
|
||||
final PreferenceSearchItem item = dataset.get(position);
|
||||
|
||||
holder.binding.title.setText(item.getTitle());
|
||||
|
||||
if (TextUtils.isEmpty(item.getSummary())) {
|
||||
holder.binding.summary.setVisibility(View.GONE);
|
||||
} else {
|
||||
holder.binding.summary.setVisibility(View.VISIBLE);
|
||||
holder.binding.summary.setText(item.getSummary());
|
||||
}
|
||||
|
||||
if (TextUtils.isEmpty(item.getBreadcrumbs())) {
|
||||
holder.binding.breadcrumbs.setVisibility(View.GONE);
|
||||
} else {
|
||||
holder.binding.breadcrumbs.setVisibility(View.VISIBLE);
|
||||
holder.binding.breadcrumbs.setText(item.getBreadcrumbs());
|
||||
}
|
||||
|
||||
holder.itemView.setOnClickListener(v -> {
|
||||
if (onItemClickListener != null) {
|
||||
onItemClickListener.accept(item);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void setContent(final List<PreferenceSearchItem> items) {
|
||||
dataset = new ArrayList<>(items);
|
||||
this.notifyDataSetChanged();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return dataset.size();
|
||||
}
|
||||
|
||||
void setOnItemClickListener(final Consumer<PreferenceSearchItem> onItemClickListener) {
|
||||
this.onItemClickListener = onItemClickListener;
|
||||
}
|
||||
|
||||
static class PreferenceViewHolder extends RecyclerView.ViewHolder {
|
||||
final SettingsPreferencesearchListItemResultBinding binding;
|
||||
|
||||
PreferenceViewHolder(final SettingsPreferencesearchListItemResultBinding binding) {
|
||||
super(binding.getRoot());
|
||||
this.binding = binding;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,43 @@
|
||||
package org.schabi.newpipe.settings.preferencesearch;
|
||||
|
||||
import androidx.preference.PreferenceCategory;
|
||||
import androidx.preference.PreferenceScreen;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
public class PreferenceSearchConfiguration {
|
||||
private PreferenceSearchFunction searcher = new PreferenceFuzzySearchFunction();
|
||||
|
||||
private final List<String> parserIgnoreElements = Arrays.asList(
|
||||
PreferenceCategory.class.getSimpleName());
|
||||
private final List<String> parserContainerElements = Arrays.asList(
|
||||
PreferenceCategory.class.getSimpleName(),
|
||||
PreferenceScreen.class.getSimpleName());
|
||||
|
||||
|
||||
public void setSearcher(final PreferenceSearchFunction searcher) {
|
||||
this.searcher = Objects.requireNonNull(searcher);
|
||||
}
|
||||
|
||||
public PreferenceSearchFunction getSearcher() {
|
||||
return searcher;
|
||||
}
|
||||
|
||||
public List<String> getParserIgnoreElements() {
|
||||
return parserIgnoreElements;
|
||||
}
|
||||
|
||||
public List<String> getParserContainerElements() {
|
||||
return parserContainerElements;
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
public interface PreferenceSearchFunction {
|
||||
Stream<PreferenceSearchItem> search(
|
||||
Stream<PreferenceSearchItem> allAvailable,
|
||||
String keyword);
|
||||
}
|
||||
}
|
@ -0,0 +1,80 @@
|
||||
package org.schabi.newpipe.settings.preferencesearch;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.text.TextUtils;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
|
||||
import org.schabi.newpipe.databinding.SettingsPreferencesearchFragmentBinding;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Displays the search results.
|
||||
*/
|
||||
public class PreferenceSearchFragment extends Fragment {
|
||||
public static final String NAME = PreferenceSearchFragment.class.getSimpleName();
|
||||
|
||||
private PreferenceSearcher searcher;
|
||||
|
||||
private SettingsPreferencesearchFragmentBinding binding;
|
||||
private PreferenceSearchAdapter adapter;
|
||||
|
||||
public void setSearcher(final PreferenceSearcher searcher) {
|
||||
this.searcher = searcher;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public View onCreateView(
|
||||
@NonNull final LayoutInflater inflater,
|
||||
@Nullable final ViewGroup container,
|
||||
@Nullable final Bundle savedInstanceState
|
||||
) {
|
||||
binding = SettingsPreferencesearchFragmentBinding.inflate(inflater, container, false);
|
||||
|
||||
binding.searchResults.setLayoutManager(new LinearLayoutManager(getContext()));
|
||||
|
||||
adapter = new PreferenceSearchAdapter();
|
||||
adapter.setOnItemClickListener(this::onItemClicked);
|
||||
binding.searchResults.setAdapter(adapter);
|
||||
|
||||
return binding.getRoot();
|
||||
}
|
||||
|
||||
public void updateSearchResults(final String keyword) {
|
||||
if (adapter == null || searcher == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final List<PreferenceSearchItem> results =
|
||||
!TextUtils.isEmpty(keyword)
|
||||
? searcher.searchFor(keyword)
|
||||
: new ArrayList<>();
|
||||
|
||||
adapter.setContent(new ArrayList<>(results));
|
||||
|
||||
setEmptyViewShown(results.isEmpty());
|
||||
}
|
||||
|
||||
private void setEmptyViewShown(final boolean shown) {
|
||||
binding.emptyStateView.setVisibility(shown ? View.VISIBLE : View.GONE);
|
||||
binding.searchResults.setVisibility(shown ? View.GONE : View.VISIBLE);
|
||||
}
|
||||
|
||||
public void onItemClicked(final PreferenceSearchItem item) {
|
||||
if (!(getActivity() instanceof PreferenceSearchResultListener)) {
|
||||
throw new ClassCastException(
|
||||
getActivity().toString() + " must implement SearchPreferenceResultListener");
|
||||
}
|
||||
|
||||
((PreferenceSearchResultListener) getActivity()).onSearchResultClicked(item);
|
||||
}
|
||||
}
|
@ -0,0 +1,102 @@
|
||||
package org.schabi.newpipe.settings.preferencesearch;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.XmlRes;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Represents a preference-item inside the search.
|
||||
*/
|
||||
public class PreferenceSearchItem {
|
||||
/**
|
||||
* Key of the setting/preference. E.g. used inside {@link android.content.SharedPreferences}.
|
||||
*/
|
||||
@NonNull
|
||||
private final String key;
|
||||
/**
|
||||
* Title of the setting, e.g. 'Default resolution' or 'Show higher resolutions'.
|
||||
*/
|
||||
@NonNull
|
||||
private final String title;
|
||||
/**
|
||||
* Summary of the setting, e.g. '480p' or 'Only some devices can play 2k/4k'.
|
||||
*/
|
||||
@NonNull
|
||||
private final String summary;
|
||||
/**
|
||||
* Possible entries of the setting, e.g. 480p,720p,...
|
||||
*/
|
||||
@NonNull
|
||||
private final String entries;
|
||||
/**
|
||||
* Breadcrumbs - a hint where the setting is located e.g. 'Video and Audio > Player'
|
||||
*/
|
||||
@NonNull
|
||||
private final String breadcrumbs;
|
||||
/**
|
||||
* The xml-resource where this item was found/built from.
|
||||
*/
|
||||
@XmlRes
|
||||
private final int searchIndexItemResId;
|
||||
|
||||
public PreferenceSearchItem(
|
||||
@NonNull final String key,
|
||||
@NonNull final String title,
|
||||
@NonNull final String summary,
|
||||
@NonNull final String entries,
|
||||
@NonNull final String breadcrumbs,
|
||||
@XmlRes final int searchIndexItemResId
|
||||
) {
|
||||
this.key = Objects.requireNonNull(key);
|
||||
this.title = Objects.requireNonNull(title);
|
||||
this.summary = Objects.requireNonNull(summary);
|
||||
this.entries = Objects.requireNonNull(entries);
|
||||
this.breadcrumbs = Objects.requireNonNull(breadcrumbs);
|
||||
this.searchIndexItemResId = searchIndexItemResId;
|
||||
}
|
||||
|
||||
public String getKey() {
|
||||
return key;
|
||||
}
|
||||
|
||||
public String getTitle() {
|
||||
return title;
|
||||
}
|
||||
|
||||
public String getSummary() {
|
||||
return summary;
|
||||
}
|
||||
|
||||
public String getEntries() {
|
||||
return entries;
|
||||
}
|
||||
|
||||
public String getBreadcrumbs() {
|
||||
return breadcrumbs;
|
||||
}
|
||||
|
||||
public int getSearchIndexItemResId() {
|
||||
return searchIndexItemResId;
|
||||
}
|
||||
|
||||
boolean hasData() {
|
||||
return !key.isEmpty() && !title.isEmpty();
|
||||
}
|
||||
|
||||
public List<String> getAllRelevantSearchFields() {
|
||||
return Arrays.asList(
|
||||
getTitle(),
|
||||
getSummary(),
|
||||
getEntries(),
|
||||
getBreadcrumbs());
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "PreferenceItem: " + title + " " + summary + " " + key;
|
||||
}
|
||||
}
|
@ -0,0 +1,127 @@
|
||||
package org.schabi.newpipe.settings.preferencesearch;
|
||||
|
||||
import android.content.res.Resources;
|
||||
import android.content.res.TypedArray;
|
||||
import android.graphics.PorterDuff;
|
||||
import android.graphics.PorterDuffColorFilter;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.graphics.drawable.RippleDrawable;
|
||||
import android.os.Build;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.util.Log;
|
||||
import android.util.TypedValue;
|
||||
|
||||
import androidx.appcompat.content.res.AppCompatResources;
|
||||
import androidx.preference.Preference;
|
||||
import androidx.preference.PreferenceFragmentCompat;
|
||||
import androidx.preference.PreferenceGroup;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
|
||||
|
||||
public final class PreferenceSearchResultHighlighter {
|
||||
private static final String TAG = "PrefSearchResHighlter";
|
||||
|
||||
private PreferenceSearchResultHighlighter() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Highlight the specified preference.
|
||||
* <br/>
|
||||
* Note: This function is Thread independent (can be called from outside of the main thread).
|
||||
*
|
||||
* @param item The item to highlight
|
||||
* @param prefsFragment The fragment where the items is located on
|
||||
*/
|
||||
public static void highlight(
|
||||
final PreferenceSearchItem item,
|
||||
final PreferenceFragmentCompat prefsFragment
|
||||
) {
|
||||
new Handler(Looper.getMainLooper()).post(() -> doHighlight(item, prefsFragment));
|
||||
}
|
||||
|
||||
private static void doHighlight(
|
||||
final PreferenceSearchItem item,
|
||||
final PreferenceFragmentCompat prefsFragment
|
||||
) {
|
||||
final Preference prefResult = prefsFragment.findPreference(item.getKey());
|
||||
|
||||
if (prefResult == null) {
|
||||
Log.w(TAG, "Preference '" + item.getKey() + "' not found on '" + prefsFragment + "'");
|
||||
return;
|
||||
}
|
||||
|
||||
final RecyclerView recyclerView = prefsFragment.getListView();
|
||||
final RecyclerView.Adapter<?> adapter = recyclerView.getAdapter();
|
||||
if (adapter instanceof PreferenceGroup.PreferencePositionCallback) {
|
||||
final int position = ((PreferenceGroup.PreferencePositionCallback) adapter)
|
||||
.getPreferenceAdapterPosition(prefResult);
|
||||
if (position != RecyclerView.NO_POSITION) {
|
||||
recyclerView.scrollToPosition(position);
|
||||
recyclerView.postDelayed(() -> {
|
||||
final RecyclerView.ViewHolder holder =
|
||||
recyclerView.findViewHolderForAdapterPosition(position);
|
||||
if (holder != null) {
|
||||
final Drawable background = holder.itemView.getBackground();
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP
|
||||
&& background instanceof RippleDrawable) {
|
||||
showRippleAnimation((RippleDrawable) background);
|
||||
return;
|
||||
}
|
||||
}
|
||||
highlightFallback(prefsFragment, prefResult);
|
||||
}, 200);
|
||||
return;
|
||||
}
|
||||
}
|
||||
highlightFallback(prefsFragment, prefResult);
|
||||
}
|
||||
|
||||
/**
|
||||
* Alternative highlighting (shows an → arrow in front of the setting)if ripple does not work.
|
||||
*
|
||||
* @param prefsFragment
|
||||
* @param prefResult
|
||||
*/
|
||||
private static void highlightFallback(
|
||||
final PreferenceFragmentCompat prefsFragment,
|
||||
final Preference prefResult
|
||||
) {
|
||||
// Get primary color from text for highlight icon
|
||||
final TypedValue typedValue = new TypedValue();
|
||||
final Resources.Theme theme = prefsFragment.getActivity().getTheme();
|
||||
theme.resolveAttribute(android.R.attr.textColorPrimary, typedValue, true);
|
||||
final TypedArray arr = prefsFragment.getActivity()
|
||||
.obtainStyledAttributes(
|
||||
typedValue.data,
|
||||
new int[]{android.R.attr.textColorPrimary});
|
||||
final int color = arr.getColor(0, 0xffE53935);
|
||||
arr.recycle();
|
||||
|
||||
// Show highlight icon
|
||||
final Drawable oldIcon = prefResult.getIcon();
|
||||
final boolean oldSpaceReserved = prefResult.isIconSpaceReserved();
|
||||
final Drawable highlightIcon =
|
||||
AppCompatResources.getDrawable(
|
||||
prefsFragment.requireContext(),
|
||||
R.drawable.ic_play_arrow);
|
||||
highlightIcon.setColorFilter(new PorterDuffColorFilter(color, PorterDuff.Mode.SRC_IN));
|
||||
prefResult.setIcon(highlightIcon);
|
||||
|
||||
prefsFragment.scrollToPreference(prefResult);
|
||||
|
||||
new Handler(Looper.getMainLooper()).postDelayed(() -> {
|
||||
prefResult.setIcon(oldIcon);
|
||||
prefResult.setIconSpaceReserved(oldSpaceReserved);
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
private static void showRippleAnimation(final RippleDrawable rippleDrawable) {
|
||||
rippleDrawable.setState(
|
||||
new int[]{android.R.attr.state_pressed, android.R.attr.state_enabled});
|
||||
new Handler(Looper.getMainLooper())
|
||||
.postDelayed(() -> rippleDrawable.setState(new int[]{}), 1000);
|
||||
}
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
package org.schabi.newpipe.settings.preferencesearch;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
public interface PreferenceSearchResultListener {
|
||||
void onSearchResultClicked(@NonNull PreferenceSearchItem result);
|
||||
}
|
@ -0,0 +1,35 @@
|
||||
package org.schabi.newpipe.settings.preferencesearch;
|
||||
|
||||
import android.text.TextUtils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class PreferenceSearcher {
|
||||
private final List<PreferenceSearchItem> allEntries = new ArrayList<>();
|
||||
|
||||
private final PreferenceSearchConfiguration configuration;
|
||||
|
||||
public PreferenceSearcher(final PreferenceSearchConfiguration configuration) {
|
||||
this.configuration = configuration;
|
||||
}
|
||||
|
||||
public void add(final List<PreferenceSearchItem> items) {
|
||||
allEntries.addAll(items);
|
||||
}
|
||||
|
||||
List<PreferenceSearchItem> searchFor(final String keyword) {
|
||||
if (TextUtils.isEmpty(keyword)) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
|
||||
return configuration.getSearcher()
|
||||
.search(allEntries.stream(), keyword)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
public void clear() {
|
||||
allEntries.clear();
|
||||
}
|
||||
}
|
@ -0,0 +1,10 @@
|
||||
/**
|
||||
* Contains classes for searching inside the preferences.
|
||||
* <br/>
|
||||
* This code is based on
|
||||
* <a href="https://github.com/ByteHamster/SearchPreference">ByteHamster/SearchPreference</a>
|
||||
* (MIT license) but was heavily modified/refactored for our use.
|
||||
*
|
||||
* @author litetex
|
||||
*/
|
||||
package org.schabi.newpipe.settings.preferencesearch;
|
@ -44,8 +44,6 @@ import java.util.List;
|
||||
import static org.schabi.newpipe.settings.tabs.Tab.typeFrom;
|
||||
|
||||
public class ChooseTabsFragment extends Fragment {
|
||||
private static final int MENU_ITEM_RESTORE_ID = 123456;
|
||||
|
||||
private TabsManager tabsManager;
|
||||
|
||||
private final List<Tab> tabList = new ArrayList<>();
|
||||
@ -110,21 +108,14 @@ public class ChooseTabsFragment extends Fragment {
|
||||
@NonNull final MenuInflater inflater) {
|
||||
super.onCreateOptionsMenu(menu, inflater);
|
||||
|
||||
final MenuItem restoreItem = menu.add(Menu.NONE, MENU_ITEM_RESTORE_ID, Menu.NONE,
|
||||
R.string.restore_defaults);
|
||||
final MenuItem restoreItem = menu.add(R.string.restore_defaults);
|
||||
restoreItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
|
||||
restoreItem.setIcon(AppCompatResources.getDrawable(requireContext(),
|
||||
R.drawable.ic_settings_backup_restore));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(final MenuItem item) {
|
||||
if (item.getItemId() == MENU_ITEM_RESTORE_ID) {
|
||||
restoreItem.setOnMenuItemClickListener(ev -> {
|
||||
restoreDefaults();
|
||||
return true;
|
||||
}
|
||||
|
||||
return super.onOptionsItemSelected(item);
|
||||
});
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
|
43
app/src/main/java/org/schabi/newpipe/util/KeyboardUtil.java
Normal file
43
app/src/main/java/org/schabi/newpipe/util/KeyboardUtil.java
Normal file
@ -0,0 +1,43 @@
|
||||
package org.schabi.newpipe.util;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.view.inputmethod.InputMethodManager;
|
||||
import android.widget.EditText;
|
||||
|
||||
import androidx.core.content.ContextCompat;
|
||||
|
||||
/**
|
||||
* Utility class for the Android keyboard.
|
||||
* <p>
|
||||
* See also <a href="https://stackoverflow.com/q/1109022">https://stackoverflow.com/q/1109022</a>
|
||||
* </p>
|
||||
*/
|
||||
public final class KeyboardUtil {
|
||||
private KeyboardUtil() {
|
||||
}
|
||||
|
||||
public static void showKeyboard(final Activity activity, final EditText editText) {
|
||||
if (activity == null || editText == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (editText.requestFocus()) {
|
||||
final InputMethodManager imm = ContextCompat.getSystemService(activity,
|
||||
InputMethodManager.class);
|
||||
imm.showSoftInput(editText, InputMethodManager.SHOW_FORCED);
|
||||
}
|
||||
}
|
||||
|
||||
public static void hideKeyboard(final Activity activity, final EditText editText) {
|
||||
if (activity == null || editText == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final InputMethodManager imm = ContextCompat.getSystemService(activity,
|
||||
InputMethodManager.class);
|
||||
imm.hideSoftInputFromWindow(editText.getWindowToken(),
|
||||
InputMethodManager.RESULT_UNCHANGED_SHOWN);
|
||||
|
||||
editText.clearFocus();
|
||||
}
|
||||
}
|
@ -157,9 +157,8 @@ public final class NavigationHelper {
|
||||
return;
|
||||
}
|
||||
|
||||
if (PlayerHolder.getInstance().getType() != PlayerType.POPUP) {
|
||||
Toast.makeText(context, R.string.popup_playing_toast, Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
Toast.makeText(context, R.string.popup_playing_toast, Toast.LENGTH_SHORT).show();
|
||||
|
||||
final Intent intent = getPlayerIntent(context, MainPlayer.class, queue, resumePlayback);
|
||||
intent.putExtra(Player.PLAYER_TYPE, MainPlayer.PlayerType.POPUP.ordinal());
|
||||
ContextCompat.startForegroundService(context, intent);
|
||||
@ -168,10 +167,9 @@ public final class NavigationHelper {
|
||||
public static void playOnBackgroundPlayer(final Context context,
|
||||
final PlayQueue queue,
|
||||
final boolean resumePlayback) {
|
||||
if (PlayerHolder.getInstance().getType() != MainPlayer.PlayerType.AUDIO) {
|
||||
Toast.makeText(context, R.string.background_player_playing_toast, Toast.LENGTH_SHORT)
|
||||
.show();
|
||||
}
|
||||
Toast.makeText(context, R.string.background_player_playing_toast, Toast.LENGTH_SHORT)
|
||||
.show();
|
||||
|
||||
final Intent intent = getPlayerIntent(context, MainPlayer.class, queue, resumePlayback);
|
||||
intent.putExtra(Player.PLAYER_TYPE, MainPlayer.PlayerType.AUDIO.ordinal());
|
||||
ContextCompat.startForegroundService(context, intent);
|
||||
|
@ -0,0 +1,94 @@
|
||||
package org.schabi.newpipe.util;
|
||||
|
||||
import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty;
|
||||
|
||||
import android.content.Context;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.Fragment;
|
||||
|
||||
import org.schabi.newpipe.NewPipeDatabase;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.error.ErrorInfo;
|
||||
import org.schabi.newpipe.error.ErrorUtil;
|
||||
import org.schabi.newpipe.error.UserAction;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||
import org.schabi.newpipe.player.playqueue.PlayQueueItem;
|
||||
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers;
|
||||
|
||||
/**
|
||||
* Utility class for putting the uploader url into the database - when required.
|
||||
*/
|
||||
public final class SaveUploaderUrlHelper {
|
||||
private SaveUploaderUrlHelper() {
|
||||
}
|
||||
|
||||
// Public functions which call the function that does
|
||||
// the actual work with the correct parameters
|
||||
public static void saveUploaderUrlIfNeeded(@NonNull final Fragment fragment,
|
||||
@NonNull final StreamInfoItem infoItem,
|
||||
@NonNull final SaveUploaderUrlCallback callback) {
|
||||
saveUploaderUrlIfNeeded(fragment.requireContext(),
|
||||
infoItem.getServiceId(),
|
||||
infoItem.getUrl(),
|
||||
infoItem.getUploaderUrl(),
|
||||
callback);
|
||||
}
|
||||
public static void saveUploaderUrlIfNeeded(@NonNull final Context context,
|
||||
@NonNull final PlayQueueItem queueItem,
|
||||
@NonNull final SaveUploaderUrlCallback callback) {
|
||||
saveUploaderUrlIfNeeded(context,
|
||||
queueItem.getServiceId(),
|
||||
queueItem.getUrl(),
|
||||
queueItem.getUploaderUrl(),
|
||||
callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches and saves the uploaderUrl if it is empty (meaning that it does
|
||||
* not exist in the video item). The callback is called with either the
|
||||
* fetched uploaderUrl, or the already saved uploaderUrl, but it is always
|
||||
* called with a valid uploaderUrl that can be used to show channel details.
|
||||
*
|
||||
* @param context Context
|
||||
* @param serviceId The serviceId of the item
|
||||
* @param url The item url
|
||||
* @param uploaderUrl The uploaderUrl of the item, if null or empty, it
|
||||
* will be fetched using the item url.
|
||||
* @param callback The callback that returns the fetched or existing
|
||||
* uploaderUrl
|
||||
*/
|
||||
private static void saveUploaderUrlIfNeeded(@NonNull final Context context,
|
||||
final int serviceId,
|
||||
@NonNull final String url,
|
||||
// Only used if not null or empty
|
||||
@Nullable final String uploaderUrl,
|
||||
@NonNull final SaveUploaderUrlCallback callback) {
|
||||
if (isNullOrEmpty(uploaderUrl)) {
|
||||
Toast.makeText(context, R.string.loading_channel_details,
|
||||
Toast.LENGTH_SHORT).show();
|
||||
ExtractorHelper.getStreamInfo(serviceId, url, false)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(result -> {
|
||||
NewPipeDatabase.getInstance(context).streamDAO()
|
||||
.setUploaderUrl(serviceId, url, result.getUploaderUrl())
|
||||
.subscribeOn(Schedulers.io()).subscribe();
|
||||
callback.onCallback(result.getUploaderUrl());
|
||||
}, throwable -> ErrorUtil.createNotification(context,
|
||||
new ErrorInfo(throwable, UserAction.REQUESTED_CHANNEL,
|
||||
"Could not load channel details")
|
||||
));
|
||||
} else {
|
||||
callback.onCallback(uploaderUrl);
|
||||
}
|
||||
}
|
||||
|
||||
public interface SaveUploaderUrlCallback {
|
||||
void onCallback(@NonNull String uploaderUrl);
|
||||
}
|
||||
}
|
@ -2,12 +2,11 @@ package org.schabi.newpipe.util;
|
||||
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
import android.widget.Toast;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.preference.PreferenceManager;
|
||||
|
||||
import org.schabi.newpipe.NewPipeDatabase;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.database.stream.model.StreamEntity;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||
@ -21,40 +20,19 @@ import org.schabi.newpipe.util.external_communication.ShareUtils;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers;
|
||||
|
||||
import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty;
|
||||
|
||||
public enum StreamDialogEntry {
|
||||
//////////////////////////////////////
|
||||
// enum values with DEFAULT actions //
|
||||
//////////////////////////////////////
|
||||
|
||||
show_channel_details(R.string.show_channel_details, (fragment, item) -> {
|
||||
if (isNullOrEmpty(item.getUploaderUrl())) {
|
||||
final int serviceId = item.getServiceId();
|
||||
final String url = item.getUrl();
|
||||
Toast.makeText(fragment.getContext(), R.string.loading_channel_details,
|
||||
Toast.LENGTH_SHORT).show();
|
||||
ExtractorHelper.getStreamInfo(serviceId, url, false)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(result -> {
|
||||
NewPipeDatabase.getInstance(fragment.requireContext()).streamDAO()
|
||||
.setUploaderUrl(serviceId, url, result.getUploaderUrl())
|
||||
.subscribeOn(Schedulers.io()).subscribe();
|
||||
openChannelFragment(fragment, item, result.getUploaderUrl());
|
||||
}, throwable -> Toast.makeText(
|
||||
// TODO: Open the Error Activity
|
||||
fragment.getContext(),
|
||||
R.string.error_show_channel_details,
|
||||
Toast.LENGTH_SHORT
|
||||
).show());
|
||||
} else {
|
||||
openChannelFragment(fragment, item, item.getUploaderUrl());
|
||||
}
|
||||
SaveUploaderUrlHelper.saveUploaderUrlIfNeeded(fragment, item,
|
||||
uploaderUrl -> openChannelFragment(fragment, item, uploaderUrl));
|
||||
}),
|
||||
|
||||
/**
|
||||
@ -63,20 +41,24 @@ public enum StreamDialogEntry {
|
||||
* Info: Add this entry within showStreamDialog.
|
||||
*/
|
||||
enqueue(R.string.enqueue_stream, (fragment, item) -> {
|
||||
NavigationHelper.enqueueOnPlayer(fragment.getContext(), new SinglePlayQueue(item));
|
||||
fetchItemInfoIfSparse(fragment, item, fullItem ->
|
||||
NavigationHelper.enqueueOnPlayer(fragment.getContext(), fullItem));
|
||||
}),
|
||||
|
||||
enqueue_next(R.string.enqueue_next_stream, (fragment, item) -> {
|
||||
NavigationHelper.enqueueNextOnPlayer(fragment.getContext(), new SinglePlayQueue(item));
|
||||
fetchItemInfoIfSparse(fragment, item, fullItem ->
|
||||
NavigationHelper.enqueueNextOnPlayer(fragment.getContext(), fullItem));
|
||||
}),
|
||||
|
||||
start_here_on_background(R.string.start_here_on_background, (fragment, item) ->
|
||||
NavigationHelper.playOnBackgroundPlayer(fragment.getContext(),
|
||||
new SinglePlayQueue(item), true)),
|
||||
start_here_on_background(R.string.start_here_on_background, (fragment, item) -> {
|
||||
fetchItemInfoIfSparse(fragment, item, fullItem ->
|
||||
NavigationHelper.playOnBackgroundPlayer(fragment.getContext(), fullItem, true));
|
||||
}),
|
||||
|
||||
start_here_on_popup(R.string.start_here_on_popup, (fragment, item) ->
|
||||
NavigationHelper.playOnPopupPlayer(fragment.getContext(),
|
||||
new SinglePlayQueue(item), true)),
|
||||
start_here_on_popup(R.string.start_here_on_popup, (fragment, item) -> {
|
||||
fetchItemInfoIfSparse(fragment, item, fullItem ->
|
||||
NavigationHelper.playOnPopupPlayer(fragment.getContext(), fullItem, true));
|
||||
}),
|
||||
|
||||
set_as_playlist_thumbnail(R.string.set_as_playlist_thumbnail, (fragment, item) -> {
|
||||
}), // has to be set manually
|
||||
@ -218,4 +200,39 @@ public enum StreamDialogEntry {
|
||||
fragment.requireActivity().getSupportFragmentManager(),
|
||||
item.getServiceId(), uploaderUrl, item.getUploaderName());
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////
|
||||
// helper functions //
|
||||
/////////////////////////////////////////////
|
||||
|
||||
private static void fetchItemInfoIfSparse(final Fragment fragment,
|
||||
final StreamInfoItem item,
|
||||
final Consumer<SinglePlayQueue> callback) {
|
||||
if (!(item.getStreamType() == StreamType.LIVE_STREAM
|
||||
|| item.getStreamType() == StreamType.AUDIO_LIVE_STREAM)
|
||||
&& item.getDuration() < 0) {
|
||||
// Sparse item: fetched by fast fetch
|
||||
ExtractorHelper.getStreamInfo(
|
||||
item.getServiceId(),
|
||||
item.getUrl(),
|
||||
false
|
||||
)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(result -> {
|
||||
final HistoryRecordManager recordManager =
|
||||
new HistoryRecordManager(fragment.getContext());
|
||||
recordManager.saveStreamState(result, 0)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.doOnError(throwable -> Log.e("StreamDialogEntry",
|
||||
throwable.toString()))
|
||||
.subscribe();
|
||||
|
||||
callback.accept(new SinglePlayQueue(result));
|
||||
}, throwable -> Log.e("StreamDialogEntry", throwable.toString()));
|
||||
} else {
|
||||
callback.accept(new SinglePlayQueue(item));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,89 @@
|
||||
package org.schabi.newpipe.views.player
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Paint
|
||||
import android.graphics.Path
|
||||
import android.util.AttributeSet
|
||||
import android.view.View
|
||||
|
||||
class CircleClipTapView(context: Context?, attrs: AttributeSet) : View(context, attrs) {
|
||||
|
||||
private var backgroundPaint = Paint()
|
||||
|
||||
private var widthPx = 0
|
||||
private var heightPx = 0
|
||||
|
||||
// Background
|
||||
|
||||
private var shapePath = Path()
|
||||
private var arcSize: Float = 80f
|
||||
private var isLeft = true
|
||||
|
||||
init {
|
||||
requireNotNull(context) { "Context is null." }
|
||||
|
||||
backgroundPaint.apply {
|
||||
style = Paint.Style.FILL
|
||||
isAntiAlias = true
|
||||
color = 0x30000000
|
||||
}
|
||||
|
||||
val dm = context.resources.displayMetrics
|
||||
widthPx = dm.widthPixels
|
||||
heightPx = dm.heightPixels
|
||||
|
||||
updatePathShape()
|
||||
}
|
||||
|
||||
fun updateArcSize(baseView: View) {
|
||||
val newArcSize = baseView.height / 11.4f
|
||||
if (arcSize != newArcSize) {
|
||||
arcSize = newArcSize
|
||||
updatePathShape()
|
||||
}
|
||||
}
|
||||
|
||||
fun updatePosition(newIsLeft: Boolean) {
|
||||
if (isLeft != newIsLeft) {
|
||||
isLeft = newIsLeft
|
||||
updatePathShape()
|
||||
}
|
||||
}
|
||||
|
||||
private fun updatePathShape() {
|
||||
val halfWidth = widthPx * 0.5f
|
||||
|
||||
shapePath.reset()
|
||||
|
||||
val w = if (isLeft) 0f else widthPx.toFloat()
|
||||
val f = if (isLeft) 1 else -1
|
||||
|
||||
shapePath.moveTo(w, 0f)
|
||||
shapePath.lineTo(f * (halfWidth - arcSize) + w, 0f)
|
||||
shapePath.quadTo(
|
||||
f * (halfWidth + arcSize) + w,
|
||||
heightPx.toFloat() / 2,
|
||||
f * (halfWidth - arcSize) + w,
|
||||
heightPx.toFloat()
|
||||
)
|
||||
shapePath.lineTo(w, heightPx.toFloat())
|
||||
|
||||
shapePath.close()
|
||||
invalidate()
|
||||
}
|
||||
|
||||
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
|
||||
super.onSizeChanged(w, h, oldw, oldh)
|
||||
widthPx = w
|
||||
heightPx = h
|
||||
updatePathShape()
|
||||
}
|
||||
|
||||
override fun onDraw(canvas: Canvas?) {
|
||||
super.onDraw(canvas)
|
||||
|
||||
canvas?.clipPath(shapePath)
|
||||
canvas?.drawPath(shapePath, backgroundPaint)
|
||||
}
|
||||
}
|
@ -0,0 +1,145 @@
|
||||
package org.schabi.newpipe.views.player
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.util.Log
|
||||
import android.view.LayoutInflater
|
||||
import androidx.annotation.NonNull
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import androidx.constraintlayout.widget.ConstraintLayout.LayoutParams.END
|
||||
import androidx.constraintlayout.widget.ConstraintLayout.LayoutParams.PARENT_ID
|
||||
import androidx.constraintlayout.widget.ConstraintLayout.LayoutParams.START
|
||||
import androidx.constraintlayout.widget.ConstraintSet
|
||||
import org.schabi.newpipe.MainActivity
|
||||
import org.schabi.newpipe.R
|
||||
import org.schabi.newpipe.player.event.DisplayPortion
|
||||
import org.schabi.newpipe.player.event.DoubleTapListener
|
||||
|
||||
class PlayerFastSeekOverlay(context: Context, attrs: AttributeSet?) :
|
||||
ConstraintLayout(context, attrs), DoubleTapListener {
|
||||
|
||||
private var secondsView: SecondsView
|
||||
private var circleClipTapView: CircleClipTapView
|
||||
private var rootConstraintLayout: ConstraintLayout
|
||||
|
||||
private var wasForwarding: Boolean = false
|
||||
|
||||
init {
|
||||
LayoutInflater.from(context).inflate(R.layout.player_fast_seek_overlay, this, true)
|
||||
|
||||
secondsView = findViewById(R.id.seconds_view)
|
||||
circleClipTapView = findViewById(R.id.circle_clip_tap_view)
|
||||
rootConstraintLayout = findViewById(R.id.root_constraint_layout)
|
||||
|
||||
addOnLayoutChangeListener { view, _, _, _, _, _, _, _, _ ->
|
||||
circleClipTapView.updateArcSize(view)
|
||||
}
|
||||
}
|
||||
|
||||
private var performListener: PerformListener? = null
|
||||
|
||||
fun performListener(listener: PerformListener) = apply {
|
||||
performListener = listener
|
||||
}
|
||||
|
||||
private var seekSecondsSupplier: () -> Int = { 0 }
|
||||
|
||||
fun seekSecondsSupplier(supplier: () -> Int) = apply {
|
||||
seekSecondsSupplier = supplier
|
||||
}
|
||||
|
||||
// Indicates whether this (double) tap is the first of a series
|
||||
// Decides whether to call performListener.onAnimationStart or not
|
||||
private var initTap: Boolean = false
|
||||
|
||||
override fun onDoubleTapStarted(portion: DisplayPortion) {
|
||||
if (DEBUG)
|
||||
Log.d(TAG, "onDoubleTapStarted called with portion = [$portion]")
|
||||
|
||||
initTap = false
|
||||
|
||||
secondsView.stopAnimation()
|
||||
}
|
||||
|
||||
override fun onDoubleTapProgressDown(portion: DisplayPortion) {
|
||||
val shouldForward: Boolean =
|
||||
performListener?.getFastSeekDirection(portion)?.directionAsBoolean ?: return
|
||||
|
||||
if (DEBUG)
|
||||
Log.d(
|
||||
TAG,
|
||||
"onDoubleTapProgressDown called with " +
|
||||
"shouldForward = [$shouldForward], " +
|
||||
"wasForwarding = [$wasForwarding], " +
|
||||
"initTap = [$initTap], "
|
||||
)
|
||||
|
||||
/*
|
||||
* Check if a initial tap occurred or if direction was switched
|
||||
*/
|
||||
if (!initTap || wasForwarding != shouldForward) {
|
||||
// Reset seconds and update position
|
||||
secondsView.seconds = 0
|
||||
changeConstraints(shouldForward)
|
||||
circleClipTapView.updatePosition(!shouldForward)
|
||||
secondsView.setForwarding(shouldForward)
|
||||
|
||||
wasForwarding = shouldForward
|
||||
|
||||
if (!initTap) {
|
||||
initTap = true
|
||||
}
|
||||
}
|
||||
|
||||
performListener?.onDoubleTap()
|
||||
|
||||
secondsView.seconds += seekSecondsSupplier.invoke()
|
||||
performListener?.seek(forward = shouldForward)
|
||||
}
|
||||
|
||||
override fun onDoubleTapFinished() {
|
||||
if (DEBUG)
|
||||
Log.d(TAG, "onDoubleTapFinished called with initTap = [$initTap]")
|
||||
|
||||
if (initTap) performListener?.onDoubleTapEnd()
|
||||
initTap = false
|
||||
|
||||
secondsView.stopAnimation()
|
||||
}
|
||||
|
||||
private fun changeConstraints(forward: Boolean) {
|
||||
val constraintSet = ConstraintSet()
|
||||
with(constraintSet) {
|
||||
clone(rootConstraintLayout)
|
||||
clear(secondsView.id, if (forward) START else END)
|
||||
connect(
|
||||
secondsView.id, if (forward) END else START,
|
||||
PARENT_ID, if (forward) END else START
|
||||
)
|
||||
secondsView.startAnimation()
|
||||
applyTo(rootConstraintLayout)
|
||||
}
|
||||
}
|
||||
|
||||
interface PerformListener {
|
||||
fun onDoubleTap()
|
||||
fun onDoubleTapEnd()
|
||||
/**
|
||||
* Determines if the playback should forward/rewind or do nothing.
|
||||
*/
|
||||
@NonNull
|
||||
fun getFastSeekDirection(portion: DisplayPortion): FastSeekDirection
|
||||
fun seek(forward: Boolean)
|
||||
|
||||
enum class FastSeekDirection(val directionAsBoolean: Boolean?) {
|
||||
NONE(null),
|
||||
FORWARD(true),
|
||||
BACKWARD(false);
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "PlayerFastSeekOverlay"
|
||||
private val DEBUG = MainActivity.DEBUG
|
||||
}
|
||||
}
|
181
app/src/main/java/org/schabi/newpipe/views/player/SecondsView.kt
Normal file
181
app/src/main/java/org/schabi/newpipe/views/player/SecondsView.kt
Normal file
@ -0,0 +1,181 @@
|
||||
package org.schabi.newpipe.views.player
|
||||
|
||||
import android.animation.Animator
|
||||
import android.animation.ValueAnimator
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.view.LayoutInflater
|
||||
import android.widget.LinearLayout
|
||||
import org.schabi.newpipe.R
|
||||
import org.schabi.newpipe.databinding.PlayerFastSeekSecondsViewBinding
|
||||
import org.schabi.newpipe.util.DeviceUtils
|
||||
|
||||
class SecondsView(context: Context, attrs: AttributeSet?) : LinearLayout(context, attrs) {
|
||||
|
||||
companion object {
|
||||
const val ICON_ANIMATION_DURATION = 750L
|
||||
}
|
||||
|
||||
var cycleDuration: Long = ICON_ANIMATION_DURATION
|
||||
set(value) {
|
||||
firstAnimator.duration = value / 5
|
||||
secondAnimator.duration = value / 5
|
||||
thirdAnimator.duration = value / 5
|
||||
fourthAnimator.duration = value / 5
|
||||
fifthAnimator.duration = value / 5
|
||||
field = value
|
||||
}
|
||||
|
||||
var seconds: Int = 0
|
||||
set(value) {
|
||||
binding.tvSeconds.text = context.resources.getQuantityString(
|
||||
R.plurals.seconds, value, value
|
||||
)
|
||||
field = value
|
||||
}
|
||||
|
||||
// Done as a field so that we don't have to compute on each tab if animations are enabled
|
||||
private val animationsEnabled = DeviceUtils.hasAnimationsAnimatorDurationEnabled(context)
|
||||
|
||||
val binding = PlayerFastSeekSecondsViewBinding.inflate(LayoutInflater.from(context), this)
|
||||
|
||||
init {
|
||||
orientation = VERTICAL
|
||||
layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)
|
||||
}
|
||||
|
||||
fun setForwarding(isForward: Boolean) {
|
||||
binding.triangleContainer.rotation = if (isForward) 0f else 180f
|
||||
}
|
||||
|
||||
fun startAnimation() {
|
||||
stopAnimation()
|
||||
|
||||
if (animationsEnabled) {
|
||||
firstAnimator.start()
|
||||
} else {
|
||||
// If no animations are enable show the arrow(s) without animation
|
||||
showWithoutAnimation()
|
||||
}
|
||||
}
|
||||
|
||||
fun stopAnimation() {
|
||||
firstAnimator.cancel()
|
||||
secondAnimator.cancel()
|
||||
thirdAnimator.cancel()
|
||||
fourthAnimator.cancel()
|
||||
fifthAnimator.cancel()
|
||||
|
||||
reset()
|
||||
}
|
||||
|
||||
private fun reset() {
|
||||
binding.icon1.alpha = 0f
|
||||
binding.icon2.alpha = 0f
|
||||
binding.icon3.alpha = 0f
|
||||
}
|
||||
|
||||
private fun showWithoutAnimation() {
|
||||
binding.icon1.alpha = 1f
|
||||
binding.icon2.alpha = 1f
|
||||
binding.icon3.alpha = 1f
|
||||
}
|
||||
|
||||
private val firstAnimator: ValueAnimator = CustomValueAnimator(
|
||||
{
|
||||
binding.icon1.alpha = 0f
|
||||
binding.icon2.alpha = 0f
|
||||
binding.icon3.alpha = 0f
|
||||
},
|
||||
{
|
||||
binding.icon1.alpha = it
|
||||
},
|
||||
{
|
||||
secondAnimator.start()
|
||||
}
|
||||
)
|
||||
|
||||
private val secondAnimator: ValueAnimator = CustomValueAnimator(
|
||||
{
|
||||
binding.icon1.alpha = 1f
|
||||
binding.icon2.alpha = 0f
|
||||
binding.icon3.alpha = 0f
|
||||
},
|
||||
{
|
||||
binding.icon2.alpha = it
|
||||
},
|
||||
{
|
||||
thirdAnimator.start()
|
||||
}
|
||||
)
|
||||
|
||||
private val thirdAnimator: ValueAnimator = CustomValueAnimator(
|
||||
{
|
||||
binding.icon1.alpha = 1f
|
||||
binding.icon2.alpha = 1f
|
||||
binding.icon3.alpha = 0f
|
||||
},
|
||||
{
|
||||
binding.icon1.alpha = 1f - binding.icon3.alpha
|
||||
binding.icon3.alpha = it
|
||||
},
|
||||
{
|
||||
fourthAnimator.start()
|
||||
}
|
||||
)
|
||||
|
||||
private val fourthAnimator: ValueAnimator = CustomValueAnimator(
|
||||
{
|
||||
binding.icon1.alpha = 0f
|
||||
binding.icon2.alpha = 1f
|
||||
binding.icon3.alpha = 1f
|
||||
},
|
||||
{
|
||||
binding.icon2.alpha = 1f - it
|
||||
},
|
||||
{
|
||||
fifthAnimator.start()
|
||||
}
|
||||
)
|
||||
|
||||
private val fifthAnimator: ValueAnimator = CustomValueAnimator(
|
||||
{
|
||||
binding.icon1.alpha = 0f
|
||||
binding.icon2.alpha = 0f
|
||||
binding.icon3.alpha = 1f
|
||||
},
|
||||
{
|
||||
binding.icon3.alpha = 1f - it
|
||||
},
|
||||
{
|
||||
firstAnimator.start()
|
||||
}
|
||||
)
|
||||
|
||||
private inner class CustomValueAnimator(
|
||||
start: () -> Unit,
|
||||
update: (value: Float) -> Unit,
|
||||
end: () -> Unit
|
||||
) : ValueAnimator() {
|
||||
|
||||
init {
|
||||
duration = cycleDuration / 5
|
||||
setFloatValues(0f, 1f)
|
||||
|
||||
addUpdateListener { update(it.animatedValue as Float) }
|
||||
addListener(object : AnimatorListener {
|
||||
override fun onAnimationStart(animation: Animator?) {
|
||||
start()
|
||||
}
|
||||
|
||||
override fun onAnimationEnd(animation: Animator?) {
|
||||
end()
|
||||
}
|
||||
|
||||
override fun onAnimationCancel(animation: Animator?) = Unit
|
||||
|
||||
override fun onAnimationRepeat(animation: Animator?) = Unit
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
19
app/src/main/res/drawable-night/ic_pin.xml
Normal file
19
app/src/main/res/drawable-night/ic_pin.xml
Normal file
@ -0,0 +1,19 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:tint="#FFFFFF"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:pathData="M14.566,1.729l7.705,7.704c0.355,0.355 0.354,0.93 0,1.284l-1.926,1.926c-0.354,0.355 -0.93,0.355 -1.285,0l-7.704,-7.704c-0.354,-0.354 -0.354,-0.929 0,-1.284l1.926,-1.926C13.639,1.374 14.213,1.374 14.566,1.729z"
|
||||
android:fillColor="#FF000000"/>
|
||||
<path
|
||||
android:pathData="M2.369,12.643l8.988,8.989c0.356,0.352 0.929,0.352 1.284,0c1.417,-1.418 1.417,-3.719 0,-5.137l-5.136,-5.136c-1.418,-1.418 -3.718,-1.418 -5.136,0C2.016,11.714 2.016,12.287 2.369,12.643z"
|
||||
android:fillColor="#FF000000"/>
|
||||
<path
|
||||
android:pathData="M13.2823,15.8538l-5.1357,-5.1357l3.8523,-3.8523l5.1357,5.1357z"
|
||||
android:fillColor="#FF000000"/>
|
||||
<path
|
||||
android:pathData="M1.727,22.273c0.355,0.353 0.929,0.353 1.284,0l3.852,-3.853l-1.284,-1.283l-3.852,3.852C1.374,21.345 1.375,21.918 1.727,22.273z"
|
||||
android:fillColor="#FF000000"/>
|
||||
</vector>
|
18
app/src/main/res/drawable/ic_pin.xml
Normal file
18
app/src/main/res/drawable/ic_pin.xml
Normal file
@ -0,0 +1,18 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:pathData="M14.566,1.729l7.705,7.704c0.355,0.355 0.354,0.93 0,1.284l-1.926,1.926c-0.354,0.355 -0.93,0.355 -1.285,0l-7.704,-7.704c-0.354,-0.354 -0.354,-0.929 0,-1.284l1.926,-1.926C13.639,1.374 14.213,1.374 14.566,1.729z"
|
||||
android:fillColor="#FF000000"/>
|
||||
<path
|
||||
android:pathData="M2.369,12.643l8.988,8.989c0.356,0.352 0.929,0.352 1.284,0c1.417,-1.418 1.417,-3.719 0,-5.137l-5.136,-5.136c-1.418,-1.418 -3.718,-1.418 -5.136,0C2.016,11.714 2.016,12.287 2.369,12.643z"
|
||||
android:fillColor="#FF000000"/>
|
||||
<path
|
||||
android:pathData="M13.2823,15.8538l-5.1357,-5.1357l3.8523,-3.8523l5.1357,5.1357z"
|
||||
android:fillColor="#FF000000"/>
|
||||
<path
|
||||
android:pathData="M1.727,22.273c0.355,0.353 0.929,0.353 1.284,0l3.852,-3.853l-1.284,-1.283l-3.852,3.852C1.374,21.345 1.375,21.918 1.727,22.273z"
|
||||
android:fillColor="#FF000000"/>
|
||||
</vector>
|
11
app/src/main/res/drawable/ic_play_seek_triangle.xml
Normal file
11
app/src/main/res/drawable/ic_play_seek_triangle.xml
Normal file
@ -0,0 +1,11 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="16dp"
|
||||
android:height="20dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:pathData="M3,2 L22,12 L3,22 Z" />
|
||||
|
||||
</vector>
|
@ -651,7 +651,6 @@
|
||||
android:layout_height="60dp"
|
||||
android:layout_alignParentStart="true"
|
||||
android:background="@color/transparent_background_color"
|
||||
android:contentDescription="@string/list_thumbnail_view_description"
|
||||
android:gravity="center_vertical"
|
||||
android:paddingLeft="@dimen/video_item_search_padding"
|
||||
android:paddingRight="@dimen/video_item_search_padding"
|
||||
|
@ -54,11 +54,21 @@
|
||||
tools:ignore="ContentDescription"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<View
|
||||
android:id="@+id/playbackControlsShadow"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_alignBottom="@+id/playbackControlRoot"
|
||||
android:background="@color/video_overlay_color"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<!-- transparent background is needed for selectableItemBackgroundBorderless to work -->
|
||||
<RelativeLayout
|
||||
android:id="@+id/playbackControlRoot"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@color/video_overlay_color"
|
||||
android:background="@color/transparent_background_color"
|
||||
android:fitsSystemWindows="true"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible">
|
||||
@ -469,8 +479,8 @@
|
||||
android:padding="@dimen/player_main_buttons_padding"
|
||||
android:scaleType="fitCenter"
|
||||
android:visibility="gone"
|
||||
app:tint="@color/white"
|
||||
app:srcCompat="@drawable/ic_fullscreen"
|
||||
app:tint="@color/white"
|
||||
tools:ignore="ContentDescription,RtlHardcoded"
|
||||
tools:visibility="visible" />
|
||||
</LinearLayout>
|
||||
@ -493,8 +503,8 @@
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:scaleType="fitCenter"
|
||||
app:tint="@color/white"
|
||||
app:srcCompat="@drawable/ic_previous"
|
||||
app:tint="@color/white"
|
||||
tools:ignore="ContentDescription" />
|
||||
|
||||
|
||||
@ -505,8 +515,8 @@
|
||||
android:layout_weight="1"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:scaleType="fitCenter"
|
||||
app:tint="@color/white"
|
||||
app:srcCompat="@drawable/ic_pause"
|
||||
app:tint="@color/white"
|
||||
tools:ignore="ContentDescription" />
|
||||
|
||||
<androidx.appcompat.widget.AppCompatImageButton
|
||||
@ -519,8 +529,8 @@
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:scaleType="fitCenter"
|
||||
app:tint="@color/white"
|
||||
app:srcCompat="@drawable/ic_next"
|
||||
app:tint="@color/white"
|
||||
tools:ignore="ContentDescription" />
|
||||
|
||||
</LinearLayout>
|
||||
@ -572,8 +582,8 @@
|
||||
android:focusable="true"
|
||||
android:padding="10dp"
|
||||
android:scaleType="fitXY"
|
||||
app:tint="@color/white"
|
||||
app:srcCompat="@drawable/ic_close" />
|
||||
app:srcCompat="@drawable/ic_close"
|
||||
app:tint="@color/white" />
|
||||
|
||||
<androidx.appcompat.widget.AppCompatImageButton
|
||||
android:id="@+id/repeatButton"
|
||||
@ -637,24 +647,6 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:gravity="center"
|
||||
android:orientation="vertical">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/controlAnimationView"
|
||||
android:layout_width="100dp"
|
||||
android:layout_height="100dp"
|
||||
android:background="@drawable/background_oval_black_transparent"
|
||||
android:padding="15dp"
|
||||
android:visibility="gone"
|
||||
tools:ignore="ContentDescription"
|
||||
tools:src="@drawable/ic_fast_rewind"
|
||||
tools:visibility="visible" />
|
||||
</LinearLayout>
|
||||
|
||||
<RelativeLayout
|
||||
android:id="@+id/loading_panel"
|
||||
android:layout_width="match_parent"
|
||||
@ -754,4 +746,11 @@
|
||||
android:textColor="@color/white"
|
||||
android:visibility="gone" />
|
||||
|
||||
<org.schabi.newpipe.views.player.PlayerFastSeekOverlay
|
||||
android:id="@+id/fast_seek_overlay"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:alpha="0"
|
||||
android:visibility="invisible" /> <!-- Required for the first appearance fading correctly -->
|
||||
|
||||
</RelativeLayout>
|
||||
|
@ -626,7 +626,6 @@
|
||||
android:layout_height="60dp"
|
||||
android:layout_alignParentStart="true"
|
||||
android:background="@color/transparent_background_color"
|
||||
android:contentDescription="@string/list_thumbnail_view_description"
|
||||
android:gravity="center_vertical"
|
||||
android:paddingLeft="@dimen/video_item_search_padding"
|
||||
android:paddingRight="@dimen/video_item_search_padding"
|
||||
|
@ -63,7 +63,6 @@
|
||||
android:layout_width="@dimen/video_item_search_thumbnail_image_width"
|
||||
android:layout_height="@dimen/video_item_search_thumbnail_image_height"
|
||||
android:layout_marginRight="@dimen/video_item_search_image_right_margin"
|
||||
android:contentDescription="@string/list_thumbnail_view_description"
|
||||
android:src="@drawable/buddy"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@+id/itemTitleView"
|
||||
|
@ -15,7 +15,6 @@
|
||||
android:layout_height="42dp"
|
||||
android:layout_centerVertical="true"
|
||||
android:layout_marginRight="12dp"
|
||||
android:contentDescription="@string/list_thumbnail_view_description"
|
||||
android:src="@drawable/buddy_channel_item"
|
||||
tools:ignore="RtlHardcoded" />
|
||||
|
||||
|
@ -18,18 +18,29 @@
|
||||
android:layout_alignParentLeft="true"
|
||||
android:layout_alignParentTop="true"
|
||||
android:layout_marginRight="@dimen/video_item_search_image_right_margin"
|
||||
android:contentDescription="@string/list_thumbnail_view_description"
|
||||
android:focusable="false"
|
||||
android:src="@drawable/buddy"
|
||||
tools:ignore="RtlHardcoded" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/detail_pinned_view"
|
||||
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_toEndOf="@+id/itemThumbnailView"
|
||||
android:contentDescription="@string/detail_pinned_comment_view_description"
|
||||
android:visibility="gone"
|
||||
app:srcCompat="@drawable/ic_pin"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<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/itemThumbnailView"
|
||||
android:layout_toEndOf="@+id/detail_pinned_view"
|
||||
android:ellipsize="end"
|
||||
android:lines="1"
|
||||
android:textAppearance="?android:attr/textAppearanceSmall"
|
||||
|
@ -16,7 +16,6 @@
|
||||
android:layout_height="42dp"
|
||||
android:layout_centerVertical="true"
|
||||
android:layout_marginRight="12dp"
|
||||
android:contentDescription="@string/list_thumbnail_view_description"
|
||||
android:src="@drawable/buddy_channel_item"
|
||||
tools:ignore="RtlHardcoded" />
|
||||
|
||||
|
@ -17,7 +17,6 @@
|
||||
android:layout_alignParentTop="true"
|
||||
android:layout_centerHorizontal="true"
|
||||
android:layout_marginRight="@dimen/video_item_search_image_right_margin"
|
||||
android:contentDescription="@string/list_thumbnail_view_description"
|
||||
android:scaleType="centerCrop"
|
||||
android:src="@drawable/dummy_thumbnail_playlist"
|
||||
tools:ignore="RtlHardcoded" />
|
||||
|
@ -18,7 +18,6 @@
|
||||
android:layout_alignParentLeft="true"
|
||||
android:layout_alignParentTop="true"
|
||||
android:layout_marginRight="@dimen/video_item_search_image_right_margin"
|
||||
android:contentDescription="@string/list_thumbnail_view_description"
|
||||
android:scaleType="centerCrop"
|
||||
android:src="@drawable/dummy_thumbnail_playlist"
|
||||
tools:ignore="RtlHardcoded" />
|
||||
|
@ -18,7 +18,6 @@
|
||||
android:layout_alignParentLeft="true"
|
||||
android:layout_alignParentTop="true"
|
||||
android:layout_marginRight="@dimen/video_item_search_image_right_margin"
|
||||
android:contentDescription="@string/list_thumbnail_view_description"
|
||||
android:scaleType="centerCrop"
|
||||
android:src="@drawable/dummy_thumbnail_playlist"
|
||||
tools:ignore="RtlHardcoded" />
|
||||
|
@ -14,7 +14,6 @@
|
||||
android:id="@+id/itemThumbnailView"
|
||||
android:layout_width="@dimen/video_item_grid_thumbnail_image_width"
|
||||
android:layout_height="@dimen/video_item_grid_thumbnail_image_height"
|
||||
android:contentDescription="@string/list_thumbnail_view_description"
|
||||
android:scaleType="centerCrop"
|
||||
android:src="@drawable/dummy_thumbnail"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
|
@ -14,7 +14,6 @@
|
||||
android:id="@+id/itemThumbnailView"
|
||||
android:layout_width="@dimen/video_item_search_thumbnail_image_width"
|
||||
android:layout_height="@dimen/video_item_search_thumbnail_image_height"
|
||||
android:contentDescription="@string/list_thumbnail_view_description"
|
||||
android:scaleType="centerCrop"
|
||||
android:src="@drawable/dummy_thumbnail"
|
||||
app:layout_constraintBottom_toTopOf="@+id/itemProgressView"
|
||||
|
@ -17,7 +17,6 @@
|
||||
android:layout_alignParentLeft="true"
|
||||
android:layout_alignParentTop="true"
|
||||
android:layout_marginRight="@dimen/video_item_search_image_right_margin"
|
||||
android:contentDescription="@string/list_thumbnail_view_description"
|
||||
android:scaleType="centerCrop"
|
||||
android:src="@drawable/dummy_thumbnail"
|
||||
tools:ignore="RtlHardcoded" />
|
||||
|
@ -17,7 +17,6 @@
|
||||
android:layout_alignParentTop="true"
|
||||
android:layout_centerHorizontal="true"
|
||||
android:layout_marginRight="@dimen/video_item_search_image_right_margin"
|
||||
android:contentDescription="@string/list_thumbnail_view_description"
|
||||
android:scaleType="centerCrop"
|
||||
android:src="@drawable/dummy_thumbnail"
|
||||
tools:ignore="RtlHardcoded" />
|
||||
|
@ -18,7 +18,6 @@
|
||||
android:layout_alignParentLeft="true"
|
||||
android:layout_alignParentTop="true"
|
||||
android:layout_marginRight="@dimen/video_item_search_image_right_margin"
|
||||
android:contentDescription="@string/list_thumbnail_view_description"
|
||||
android:scaleType="centerCrop"
|
||||
android:src="@drawable/dummy_thumbnail"
|
||||
tools:ignore="RtlHardcoded" />
|
||||
|
@ -21,7 +21,6 @@
|
||||
android:id="@+id/thumbnail_view"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:contentDescription="@string/list_thumbnail_view_description"
|
||||
tools:src="@drawable/buddy_channel_item" />
|
||||
|
||||
<org.schabi.newpipe.views.NewPipeTextView
|
||||
|
@ -17,7 +17,6 @@
|
||||
android:layout_marginStart="@dimen/video_item_search_image_right_margin"
|
||||
android:layout_marginTop="@dimen/video_item_search_image_right_margin"
|
||||
android:layout_marginBottom="@dimen/video_item_search_image_right_margin"
|
||||
android:contentDescription="@string/list_thumbnail_view_description"
|
||||
android:scaleType="centerCrop"
|
||||
android:src="@drawable/dummy_thumbnail"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
|
@ -54,11 +54,21 @@
|
||||
tools:ignore="ContentDescription"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<View
|
||||
android:id="@+id/playbackControlsShadow"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:visibility="gone"
|
||||
android:layout_alignBottom="@+id/playbackControlRoot"
|
||||
android:background="@color/video_overlay_color"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<!-- transparent background is needed for selectableItemBackgroundBorderless to work -->
|
||||
<RelativeLayout
|
||||
android:id="@+id/playbackControlRoot"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@color/video_overlay_color"
|
||||
android:background="@color/transparent_background_color"
|
||||
android:fitsSystemWindows="true"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible">
|
||||
@ -633,24 +643,6 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:gravity="center"
|
||||
android:orientation="vertical">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/controlAnimationView"
|
||||
android:layout_width="100dp"
|
||||
android:layout_height="100dp"
|
||||
android:background="@drawable/background_oval_black_transparent"
|
||||
android:padding="15dp"
|
||||
android:visibility="gone"
|
||||
tools:ignore="ContentDescription"
|
||||
tools:src="@drawable/ic_fast_rewind"
|
||||
tools:visibility="visible" />
|
||||
</LinearLayout>
|
||||
|
||||
<RelativeLayout
|
||||
android:id="@+id/loading_panel"
|
||||
android:layout_width="match_parent"
|
||||
@ -751,4 +743,11 @@
|
||||
android:textColor="@color/white"
|
||||
android:visibility="gone" />
|
||||
|
||||
<org.schabi.newpipe.views.player.PlayerFastSeekOverlay
|
||||
android:id="@+id/fast_seek_overlay"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:alpha="0"
|
||||
android:visibility="invisible" /> <!-- Required for the first appearance fading correctly -->
|
||||
|
||||
</RelativeLayout>
|
||||
|
27
app/src/main/res/layout/player_fast_seek_overlay.xml
Normal file
27
app/src/main/res/layout/player_fast_seek_overlay.xml
Normal file
@ -0,0 +1,27 @@
|
||||
<?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"
|
||||
android:id="@+id/root_constraint_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<org.schabi.newpipe.views.player.CircleClipTapView
|
||||
android:id="@+id/circle_clip_tap_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:clickable="false"
|
||||
android:focusable="false" />
|
||||
|
||||
<org.schabi.newpipe.views.player.SecondsView
|
||||
android:id="@+id/seconds_view"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:clickable="false"
|
||||
android:focusable="false"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintWidth_default="percent"
|
||||
app:layout_constraintWidth_percent="0.5" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
51
app/src/main/res/layout/player_fast_seek_seconds_view.xml
Normal file
51
app/src/main/res/layout/player_fast_seek_seconds_view.xml
Normal file
@ -0,0 +1,51 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<merge 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"
|
||||
tools:ignore="ContentDescription"
|
||||
tools:layout_height="wrap_content"
|
||||
tools:layout_width="match_parent"
|
||||
tools:orientation="vertical"
|
||||
tools:parentTag="android.widget.LinearLayout">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/triangle_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_horizontal"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<androidx.appcompat.widget.AppCompatImageView
|
||||
android:id="@+id/icon_1"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:srcCompat="@drawable/ic_play_seek_triangle"
|
||||
tools:alpha="0.18" />
|
||||
|
||||
<androidx.appcompat.widget.AppCompatImageView
|
||||
android:id="@+id/icon_2"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:srcCompat="@drawable/ic_play_seek_triangle"
|
||||
tools:alpha="0.5" />
|
||||
|
||||
<androidx.appcompat.widget.AppCompatImageView
|
||||
android:id="@+id/icon_3"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:srcCompat="@drawable/ic_play_seek_triangle"
|
||||
tools:alpha="1" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_seconds"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:padding="4dp"
|
||||
android:textColor="@android:color/white"
|
||||
android:textSize="14sp"
|
||||
tools:text="20 seconds" />
|
||||
|
||||
</merge>
|
@ -17,7 +17,6 @@
|
||||
android:layout_alignParentLeft="true"
|
||||
android:layout_alignParentTop="true"
|
||||
android:layout_marginRight="5dp"
|
||||
android:contentDescription="@string/list_thumbnail_view_description"
|
||||
android:src="@drawable/buddy"
|
||||
tools:ignore="RtlHardcoded" />
|
||||
|
||||
|
@ -6,14 +6,14 @@
|
||||
android:orientation="vertical"
|
||||
tools:context="org.schabi.newpipe.MainActivity">
|
||||
|
||||
<include
|
||||
android:id="@+id/settings_toolbar_layout"
|
||||
layout="@layout/toolbar_layout" />
|
||||
|
||||
<androidx.fragment.app.FragmentContainerView
|
||||
android:id="@+id/settings_fragment_holder"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginTop="?attr/actionBarSize" />
|
||||
|
||||
<include
|
||||
layout="@layout/toolbar_layout"
|
||||
android:id="@+id/settings_toolbar_layout"/>
|
||||
|
||||
</RelativeLayout>
|
||||
|
@ -0,0 +1,49 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:clipToPadding="false"
|
||||
android:orientation="vertical"
|
||||
android:background="?android:attr/windowBackground">
|
||||
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="4dp"
|
||||
android:background="?attr/toolbar_shadow" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/empty_state_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:gravity="center"
|
||||
android:orientation="vertical"
|
||||
android:visibility="gone"
|
||||
tools:visibility="gone">
|
||||
|
||||
<org.schabi.newpipe.views.NewPipeTextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:layout_marginBottom="10dp"
|
||||
android:fontFamily="monospace"
|
||||
android:text="╰(°●°╰)"
|
||||
android:textSize="35sp"
|
||||
tools:ignore="HardcodedText,UnusedAttribute" />
|
||||
|
||||
<org.schabi.newpipe.views.NewPipeTextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:text="@string/search_no_results"
|
||||
android:textSize="24sp" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/searchResults"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:clipToPadding="false"
|
||||
android:scrollbars="vertical" />
|
||||
</LinearLayout>
|
@ -0,0 +1,36 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?android:attr/selectableItemBackground"
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp"
|
||||
android:paddingTop="12dp"
|
||||
android:paddingBottom="12dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/title"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textColor="?android:attr/editTextColor"
|
||||
android:textSize="16sp"
|
||||
tools:text="Title" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/summary"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textColor="?android:attr/editTextColor"
|
||||
android:textSize="14sp"
|
||||
tools:text="Summary" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/breadcrumbs"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:alpha="0.6"
|
||||
android:textColor="?android:attr/editTextColor"
|
||||
android:textSize="14sp"
|
||||
tools:text="Breadcrumb" />
|
||||
</LinearLayout>
|
11
app/src/main/res/menu/menu_settings_main_fragment.xml
Normal file
11
app/src/main/res/menu/menu_settings_main_fragment.xml
Normal file
@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<item
|
||||
android:id="@+id/action_search"
|
||||
android:icon="@drawable/ic_search"
|
||||
android:orderInCategory="1"
|
||||
android:title="@string/search"
|
||||
app:showAsAction="always" />
|
||||
</menu>
|
Binary file not shown.
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.4 KiB |
@ -21,7 +21,6 @@
|
||||
<string name="install">تثبيت</string>
|
||||
<string name="kore_not_found">تطبيق Kore غير موجود. هل تريد تثبيته؟</string>
|
||||
<string name="light_theme_title">فاتح</string>
|
||||
<string name="list_thumbnail_view_description">صور معاينة الفيديو</string>
|
||||
<string name="network_error">خطأ في الشبكة</string>
|
||||
<string name="no_player_found">لم يتم العثور على مشغل بث. تثبيت VLC؟</string>
|
||||
<string name="open_in_browser">افتح في المتصفح</string>
|
||||
@ -99,7 +98,7 @@
|
||||
<string name="undo">تراجع</string>
|
||||
<string name="play_all">تشغيل الكل</string>
|
||||
<string name="notification_channel_name">تنبيهات NewPipe</string>
|
||||
<string name="notification_channel_description">تنبيهات مشغل NewPipe للخلفية والنوافذ المنبثقة</string>
|
||||
<string name="notification_channel_description">تنبيهات مشغل NewPipe</string>
|
||||
<string name="unknown_content">[غير معروف]</string>
|
||||
<string name="could_not_setup_download_menu">تعذر إعداد قائمة التنزيل</string>
|
||||
<string name="app_ui_crash">تعطل التطبيق / واجهة المستخدم</string>
|
||||
@ -108,7 +107,7 @@
|
||||
<string name="player_recoverable_failure">استرداد المشغل من الخطأ</string>
|
||||
<string name="sorry_string">عذرًا، لم ينبغِ أن يحدث ذلك.</string>
|
||||
<string name="error_report_button_text">الإبلاغ عن هذا الخطأ عبر البريد الإلكتروني</string>
|
||||
<string name="error_snackbar_message">عذرا، حدث خطأ ما.</string>
|
||||
<string name="error_snackbar_message">عذرًا، حدث خطأ ما.</string>
|
||||
<string name="error_snackbar_action">أبلِغ</string>
|
||||
<string name="what_device_headline">معلومات:</string>
|
||||
<string name="what_happened_headline">ماذا حدث:</string>
|
||||
@ -158,7 +157,7 @@
|
||||
<string name="tab_licenses">التراخيص</string>
|
||||
<string name="app_description">تطبيق مجاني خفيف البث على أندرويد.</string>
|
||||
<string name="contribution_title">ساهم</string>
|
||||
<string name="contribution_encouragement">إذا كانت لديك أفكار؛ أو ترجمة، أو تغييرات تخص التصميم، أو تنظيف و تحسين الشفرة البرمجية ، أو تعديلات عميقة عليها، فتذكر أنّ مساعدتك دائما موضع ترحيب. وكلما أتممنا شيئا كلما كان ذلك أفضل !</string>
|
||||
<string name="contribution_encouragement">إذا كانت لديك أفكار؛ أو ترجمة، أو تغييرات تخص التصميم، أو تنظيف و تحسين الشفرة البرمجية، أو تعديلات عميقة عليها، فتذكر أنّ مساعدتك دائما موضع ترحيب. وكلما أتممنا شيئا كلما كان ذلك أفضل!</string>
|
||||
<string name="view_on_github">عرض على GitHub</string>
|
||||
<string name="donation_title">تبرع</string>
|
||||
<string name="donation_encouragement">يتم تطوير NewPipe من قبل متطوعين يقضون وقت فراغهم لتقديم أفضل تجربة لك. حان الوقت لرد المساعدة مع المطورين وجعل NewPipe أكثر و أفضل بينما يستمتعون بفنجان من القهوة.</string>
|
||||
@ -291,7 +290,7 @@
|
||||
<string name="no_streams_available_download">لا يوجد بث متاح للتنزيل</string>
|
||||
<string name="one_item_deleted">تم حذف عنصر واحد.</string>
|
||||
<string name="toast_no_player">لم يتم تثبيت أي تطبيق لتشغيل هذا الملف</string>
|
||||
<string name="app_license">NewPipe هو برنامج مفتوح المصدر و بحقوق متروكة: يمكنك استخدام الكود ودراسته وتحسينه كما شئت. و على وجه التحديد يمكنك إعادة توزيعه / أو تعديله تحت شروط رخصة GNU العمومية والتي نشرتها مؤسسة البرمجيات الحرة، سواء الإصدار 3 من الرخصة، أو (باختيارك) أي إصدار جديد.</string>
|
||||
<string name="app_license">NewPipe هو برنامج مفتوح المصدر وبحقوق متروكة: يمكنك استخدام الكود ودراسته وتحسينه كما شئت. وعلى وجه التحديد يمكنك إعادة توزيعه / أو تعديله تحت شروط رخصة GNU العمومية والتي نشرتها مؤسسة البرمجيات الحرة، سواء الإصدار 3 من الرخصة، أو (باختيارك) أي إصدار جديد.</string>
|
||||
<string name="title_last_played">آخر ما تم تشغيله</string>
|
||||
<string name="title_most_played">الأكثر تشغيلا</string>
|
||||
<string name="override_current_data">هذا سوف يُزيل إعداداتك الحالية.</string>
|
||||
@ -326,10 +325,10 @@
|
||||
<string name="unhook_checkbox">إلغاء الإرتباط (قد يسبب تشويه)</string>
|
||||
<string name="import_settings">هل تريد أيضا استيراد الإعدادات؟</string>
|
||||
<string name="privacy_policy_title">سياسة خصوصية NewPipe</string>
|
||||
<string name="privacy_policy_encouragement">يأخذ مشروع NewPipe خصوصيتك على محمل الجد. لذلك ، لا يجمع التطبيق أي بيانات دون موافقتك.
|
||||
<string name="privacy_policy_encouragement">يأخذ مشروع NewPipe خصوصيتك على محمل الجد. لذلك، لا يجمع التطبيق أي بيانات دون موافقتك.
|
||||
\nتوضح سياسة خصوصية NewPipe بالتفصيل البيانات التي يتم إرسالها وتخزينها عند إرسال تقرير الأعطال.</string>
|
||||
<string name="read_privacy_policy">الإطلاع على سياسة الخصوصية</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>
|
||||
@ -354,7 +353,7 @@
|
||||
<string name="brightness_gesture_control_summary">استخدام الإيماءات للتحكم بسطوع المشغّل</string>
|
||||
<string name="settings_category_updates_title">التحديثات</string>
|
||||
<string name="file_deleted">تم حذف الملف</string>
|
||||
<string name="app_update_notification_channel_name">تتبيه تحديث التطبيق</string>
|
||||
<string name="app_update_notification_channel_name">تنبيه تحديث التطبيق</string>
|
||||
<string name="volume_gesture_control_title">إيماء التحكم بالصوت</string>
|
||||
<string name="events">الأحداث</string>
|
||||
<string name="app_update_notification_channel_description">إشعارات لإصدار NewPipe الجديد</string>
|
||||
@ -379,7 +378,7 @@
|
||||
<string name="paused">متوقف</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>
|
||||
@ -434,7 +433,7 @@
|
||||
<string name="clear_playback_states_title">حذف مواقف التشغيل</string>
|
||||
<string name="clear_playback_states_summary">حذف كل مواقف التشغيل</string>
|
||||
<string name="delete_playback_states_alert">حذف كل مواقف التشغيل؟</string>
|
||||
<string name="drawer_header_description">تبديل الخدمة ، المحدد حاليًا:</string>
|
||||
<string name="drawer_header_description">تبديل الخدمة، المحدد حاليًا:</string>
|
||||
<string name="default_kiosk_page_summary">الكشك الافتراضي</string>
|
||||
<string name="no_one_watching">لا توجد مشاهدة</string>
|
||||
<string name="no_one_listening">لا أحد يستمع</string>
|
||||
@ -481,21 +480,21 @@
|
||||
<string name="videos_string">الفيديوهات</string>
|
||||
<plurals name="seconds">
|
||||
<item quantity="zero">%d ثانية</item>
|
||||
<item quantity="one">%d ثواني</item>
|
||||
<item quantity="two">%d ثواني</item>
|
||||
<item quantity="one">%d ثانية</item>
|
||||
<item quantity="two">%d ثانية</item>
|
||||
<item quantity="few">%d ثواني</item>
|
||||
<item quantity="many">%d ثواني</item>
|
||||
<item quantity="other">%d ثواني</item>
|
||||
<item quantity="many">%d ثانية</item>
|
||||
<item quantity="other">%d ثانية</item>
|
||||
</plurals>
|
||||
<string name="feed_use_dedicated_fetch_method_help_text">هل تعتقد تحميل تغذية بطيء جدا؟ إذا كان الأمر كذلك ، فحاول تمكين التحميل السريع (يمكنك تغييره في الإعدادات أو بالضغط على الزر أدناه).
|
||||
<string name="feed_use_dedicated_fetch_method_help_text">هل تعتقد تحميل التغذية بطيء جدا؟ إذا كان الأمر كذلك، فحاول تمكين التحميل السريع (يمكنك تغييره في الإعدادات أو بالضغط على الزر أدناه).
|
||||
\n
|
||||
\nيقدم NewPipe استراتيجيتين لتحميل الخلاصة:
|
||||
\n• جلب قناة الاشتراك بأكملها ، وهي بطيئة ولكنها كاملة.
|
||||
\n• استخدام نقطة نهاية خدمة مخصصة ، وهي سريعة ولكنها عادة لا تكتمل.
|
||||
\n• جلب قناة الاشتراك بأكملها، وهي بطيئة ولكنها كاملة.
|
||||
\n• استخدام نقطة نهاية خدمة مخصصة، وهي سريعة ولكنها عادة لا تكتمل.
|
||||
\n
|
||||
\nالفرق بين الاثنين هو أن العنصر السريع عادة ما يفتقر إلى بعض المعلومات ، مثل مدة العنصر أو نوعه (لا يمكن التمييز بين مقاطع الفيديو المباشرة والأخرى العادية) وقد يعيد عناصر أقل.
|
||||
\nالفرق بين الاثنين هو أن العنصر السريع عادة ما يفتقر إلى بعض المعلومات، مثل مدة العنصر أو نوعه (لا يمكن التمييز بين مقاطع الفيديو المباشرة والأخرى العادية) وقد يعيد عناصر أقل.
|
||||
\n
|
||||
\n يوتيوب هو مثال على الخدمة التي تقدمها هذه طريقة سريعة مع تغذية RSS الخاصة بها.
|
||||
\nيوتيوب هو مثال على الخدمة التي تقدمها هذه طريقة سريعة مع تغذية RSS الخاصة بها.
|
||||
\n
|
||||
\nلذا فإن الاختيار يتلخص في ما تفضله: السرعة أو المعلومات الدقيقة.</string>
|
||||
<string name="feed_use_dedicated_fetch_method_disable_button">تعطيل الوضع السريع</string>
|
||||
@ -534,19 +533,19 @@
|
||||
</plurals>
|
||||
<plurals name="hours">
|
||||
<item quantity="zero">%d ساعة</item>
|
||||
<item quantity="one">%d ساعات</item>
|
||||
<item quantity="two">%d ساعات</item>
|
||||
<item quantity="one">%d ساع</item>
|
||||
<item quantity="two">%d ساعة</item>
|
||||
<item quantity="few">%d ساعات</item>
|
||||
<item quantity="many">%d ساعات</item>
|
||||
<item quantity="other">%d ساعات</item>
|
||||
<item quantity="many">%d ساعة</item>
|
||||
<item quantity="other">%d ساعة</item>
|
||||
</plurals>
|
||||
<plurals name="minutes">
|
||||
<item quantity="zero">%d دقيقة</item>
|
||||
<item quantity="one">%d الدقائق</item>
|
||||
<item quantity="two">%d الدقائق</item>
|
||||
<item quantity="few">%d الدقائق</item>
|
||||
<item quantity="many">%d الدقائق</item>
|
||||
<item quantity="other">%d الدقائق</item>
|
||||
<item quantity="one">%d دقيقة</item>
|
||||
<item quantity="two">%d دقيقة</item>
|
||||
<item quantity="few">%d دقائق</item>
|
||||
<item quantity="many">%d دقيقة</item>
|
||||
<item quantity="other">%d دقيقة</item>
|
||||
</plurals>
|
||||
<string name="new_seek_duration_toast">نظرا لقيود مشغل ExoPlayer مدة التقديم تم ضبطها الى %d ثانية</string>
|
||||
<string name="unmute">إلغاء كتم الصوت</string>
|
||||
@ -595,7 +594,7 @@
|
||||
<string name="notification_action_nothing">لا شيء</string>
|
||||
<string name="notification_action_buffering">جارٍ التحميل</string>
|
||||
<string name="notification_action_shuffle">خلط</string>
|
||||
<string name="notification_action_repeat">كرّر</string>
|
||||
<string name="notification_action_repeat">تكرار</string>
|
||||
<string name="notification_actions_at_most_three">يمكنك تحديد ثلاثة إجراءات كحد أقصى لإظهارها في الإشعار المضغوط!</string>
|
||||
<string name="notification_actions_summary">قم بتحرير كل إشعار أدناه من خلال النقر عليه. حدد ما يصل إلى ثلاثة منها لتظهر في الإشعار المضغوط باستخدام مربعات الاختيار الموجودة على اليمين</string>
|
||||
<string name="notification_action_4_title">زر الإجراء الخامس</string>
|
||||
@ -632,13 +631,13 @@
|
||||
<string name="show_description_title">إظهار الوصف</string>
|
||||
<string name="open_with">فتح مع</string>
|
||||
<string name="paid_content">يتوفر هذا المحتوى فقط للمستخدمين الذين قاموا بالدفع، لذلك لا يمكن بثه أو تنزيله عبر NewPipe.</string>
|
||||
<string name="youtube_music_premium_content">يتوفر هذا الفيديو فقط لأعضاء YouTube Music Premium ، لذلك لا يمكن بثه أو تنزيله من قبل NewPipe.</string>
|
||||
<string name="youtube_music_premium_content">يتوفر هذا الفيديو فقط لأعضاء YouTube Music Premium، لذلك لا يمكن بثه أو تنزيله من قبل NewPipe.</string>
|
||||
<string name="private_content">هذا المحتوى خاص، لذلك لا يمكن دفقه أو تنزيله بواسطة NewPipe.</string>
|
||||
<string name="soundcloud_go_plus_content">هذا هو مسار SoundCloud Go+ ، على الأقل في بلدك ، لذلك لا يمكن بثها أو تنزيلها من قبل NewPipe.</string>
|
||||
<string name="soundcloud_go_plus_content">هذا مسار SoundCloud Go + ، على الأقل في بلدك ، لذلك لا يمكن دفقه أو تنزيله بواسطة NewPipe.</string>
|
||||
<string name="georestricted_content">هذا المحتوى غير متوفر في بلدك.</string>
|
||||
<string name="crash_the_app">اغلق التطبيق قسريا</string>
|
||||
<string name="restricted_video_no_stream">هذا الفيديو مقيد بالفئة العمرية.
|
||||
\nنظرا لسياسات YouTube الجديدة المتعلقة بمقاطع الفيديو المقيدة بالفئة العمرية، لا يمكن ل NewPipe الوصول إلى أي من مقاطع الفيديو الخاصة بها وبالتالي يتعذر تشغيلها.</string>
|
||||
\nنظرا لسياسات YouTube الجديدة المتعلقة بمقاطع الفيديو المقيدة بالفئة العمرية، لا يمكن ل NewPipe الوصول إلى أي من مقاطع الفيديو الخاصة بها وبالتالي لا يمكن تشغيلها.</string>
|
||||
<string name="radio">إذاعة</string>
|
||||
<string name="featured">المميزة</string>
|
||||
<string name="recaptcha_solve">حل</string>
|
||||
@ -675,10 +674,10 @@
|
||||
\nهل تريد إلغاء الاشتراك من هذه القناة؟</string>
|
||||
<string name="feed_load_error_account_info">تعذر تحميل تغذية ل \'%s\'.</string>
|
||||
<string name="feed_load_error">خطأ في تحميل الخلاصة</string>
|
||||
<string name="downloads_storage_use_saf_summary_api_29">بدءًا من Android 10 ، يتم دعم \"Storage Access Framework\" فقط</string>
|
||||
<string name="downloads_storage_use_saf_summary_api_29">بدءًا من Android 10، يتم دعم \"Storage Access Framework\" فقط</string>
|
||||
<string name="downloads_storage_use_saf_summary_api_19">لا يتم دعم \"Storage Access Framework\" على Android KitKat والإصدارات الأقدم</string>
|
||||
<string name="downloads_storage_ask_summary_no_saf_notice">سيتم سؤالك عن مكان حفظ كل تنزيل</string>
|
||||
<string name="no_dir_yet">لم يتم تعيين مجلد التحميل ، الرجاء اختيار مجلد التحميل الافتراضي الآن</string>
|
||||
<string name="no_dir_yet">لم يتم تعيين مجلد التحميل، الرجاء اختيار مجلد التحميل الافتراضي الآن</string>
|
||||
<string name="off">إيقاف</string>
|
||||
<string name="on">تشغيل</string>
|
||||
<string name="tablet_mode_title">وضع الجهاز اللوحي</string>
|
||||
@ -722,9 +721,19 @@
|
||||
<string name="manual_update_description">التحقق يدويا من وجود إصدارات جديدة</string>
|
||||
<string name="checking_updates_toast">جاري التحقق من وجود تحديثات…</string>
|
||||
<string name="feed_new_items">عناصر تغذية جديدة</string>
|
||||
<string name="report_player_errors_title">الإبلاغ عن أخطاء المشغل</string>
|
||||
<string name="show_crash_the_player_summary">إظهار خيار تعطل عند استخدام المشغل</string>
|
||||
<string name="show_crash_the_player_title">إظهار \"تعطل المشغل\"</string>
|
||||
<string name="crash_the_player">تحطيم المشغل</string>
|
||||
<string name="report_player_errors_summary">رفع تقرير لأخطاء المشغل بالتفصيل الكامل بدلا من إظهار رسالة اشعار قصيرة الأجل (مفيدة لتشخيص المشاكل)</string>
|
||||
<string name="error_report_channel_name">إشعار الإبلاغ عن الأخطاء</string>
|
||||
<string name="error_report_channel_description">التنبيهات المتعلقة بالإبلاغ عن الأخطاء</string>
|
||||
<string name="error_report_notification_title">واجه NewPipe خطأ، اضغط للتقرير</string>
|
||||
<string name="error_report_notification_toast">حدث خطأ، انظر للإشعار</string>
|
||||
<string name="create_error_notification">قم بإنشاء تنبيه بالخطأ</string>
|
||||
<string name="no_appropriate_file_manager_message">لم يتم العثور على مدير ملفات مناسب لهذا الإجراء.
|
||||
\nالرجاء تثبيت مدير ملفات أو محاولة تعطيل \"%s\" في إعدادات التنزيل.</string>
|
||||
<string name="show_error_snackbar">إظهار خطأ snackbar</string>
|
||||
<string name="no_appropriate_file_manager_message_android_10">لم يتم العثور على مدير ملفات مناسب لهذا الإجراء.
|
||||
\nالرجاء تثبيت مدير ملفات متوافق مع Storage Access Framework.</string>
|
||||
<string name="background_player_already_playing_toast">يتم تشغيله في الخلفية</string>
|
||||
<string name="detail_pinned_comment_view_description">تعليق مثبت</string>
|
||||
</resources>
|
@ -458,7 +458,6 @@
|
||||
<string name="detail_drag_description">Arrastra pa reordenar</string>
|
||||
<string name="detail_uploader_thumbnail_view_description">Avatar del xubidor</string>
|
||||
<string name="detail_thumbnail_view_description">Reproducción d\'un videu, duración:</string>
|
||||
<string name="list_thumbnail_view_description">Miniatura del videu</string>
|
||||
<string name="your_comment">Un comentariu (n\'inglés):</string>
|
||||
<string name="what_happened_headline">Qué pasó:</string>
|
||||
<string name="error_snackbar_action">Informar</string>
|
||||
|
@ -289,7 +289,6 @@
|
||||
<string name="detail_likes_img_view_description">Layklar</string>
|
||||
<string name="detail_uploader_thumbnail_view_description">Yuklovchining avatar eskizi</string>
|
||||
<string name="detail_thumbnail_view_description">Videoni ijro etish muddati, davomiyligi:</string>
|
||||
<string name="list_thumbnail_view_description">Videoni oldindan ko\'rish uchun eskiz</string>
|
||||
<string name="error_details_headline">Detallar:</string>
|
||||
<string name="your_comment">Sizning sharhingiz (ingliz tilida):</string>
|
||||
<string name="info_labels">Nima: \\n So\'rov: \\nTarkib tili: \\nTarkib mamlakati: \\nIlova tili: \\ nXizmat: \\ nGMT vaqti: \\ nPaket: \\ nVersion: \\ nOS versiyasi:</string>
|
||||
|
@ -33,7 +33,7 @@
|
||||
<plurals name="videos">
|
||||
<item quantity="other">%s 个视频</item>
|
||||
</plurals>
|
||||
<string name="disabled">停用</string>
|
||||
<string name="disabled">已停用</string>
|
||||
<string name="controls_background_title">后台播放</string>
|
||||
<string name="show_search_suggestions_title">显示搜索建议</string>
|
||||
<string name="subscribe_button_title">订阅</string>
|
||||
@ -58,7 +58,7 @@
|
||||
<string name="settings_category_updates_title">更新</string>
|
||||
<string name="file_deleted">文件已删除</string>
|
||||
<string name="subscribers_count_not_available">无法得知订阅人数</string>
|
||||
<string name="updates_setting_description">发布新版本时,通知我升级应用</string>
|
||||
<string name="updates_setting_description">有新版本时,显示通知提示更新应用</string>
|
||||
<string name="grid">网格</string>
|
||||
<string name="app_update_notification_content_title">NewPipe 可更新!</string>
|
||||
<string name="error_http_unsupported_range">服务器不接受多线程下载, 使用 @string/msg_threads = 1 重试</string>
|
||||
@ -148,14 +148,13 @@
|
||||
<string name="app_ui_crash">App/UI 崩溃</string>
|
||||
<string name="sorry_string">抱歉, 这本不该发生。</string>
|
||||
<string name="error_report_button_text">使用电子邮件反馈错误</string>
|
||||
<string name="error_snackbar_message">抱歉, 发生了一些错误。</string>
|
||||
<string name="error_snackbar_message">抱歉,发生了一些错误。</string>
|
||||
<string name="error_snackbar_action">反馈</string>
|
||||
<string name="what_device_headline">信息:</string>
|
||||
<string name="what_happened_headline">发生了什么:</string>
|
||||
<string name="info_labels">详情:\\n请求:\\n内容语言:\\n内容国家:\\n客户端语言:\\n服务:\\nGMT时间:\\n包名:\\n版本:\\n操作系统版本:</string>
|
||||
<string name="your_comment">您的附加说明(请用英文):</string>
|
||||
<string name="error_details_headline">详细信息:</string>
|
||||
<string name="list_thumbnail_view_description">视频预览缩略图</string>
|
||||
<string name="detail_thumbnail_view_description">播放视频,时长:</string>
|
||||
<string name="detail_uploader_thumbnail_view_description">视频上传者的头像缩略图</string>
|
||||
<string name="short_billion">十亿</string>
|
||||
@ -175,7 +174,7 @@
|
||||
<string name="use_external_video_player_summary">部分分辨率下没有音频</string>
|
||||
<string name="show_search_suggestions_summary">选择搜索时显示的建议</string>
|
||||
<string name="best_resolution">最佳分辨率</string>
|
||||
<string name="app_description">开源且小巧的 Android 媒体播放器。</string>
|
||||
<string name="app_description">自由且小巧的 Android 媒体播放器。</string>
|
||||
<string name="view_on_github">在 GitHub 上查看</string>
|
||||
<string name="app_license_title">NewPipe 的许可证</string>
|
||||
<string name="contribution_encouragement">你是否想过要翻译、设计、清理或重构代码——我们始终欢迎你来贡献!</string>
|
||||
@ -195,7 +194,7 @@
|
||||
<string name="title_activity_history">历史记录</string>
|
||||
<string name="action_history">历史记录</string>
|
||||
<string name="notification_channel_name">NewPipe 通知</string>
|
||||
<string name="notification_channel_description">NewPipe 在后台播放和悬浮窗播放时在通知栏中显示通知</string>
|
||||
<string name="notification_channel_description">NewPipe 播放器的通知</string>
|
||||
<string name="default_video_format_title">默认视频格式</string>
|
||||
<string name="settings_category_player_behavior_title">行为</string>
|
||||
<string name="empty_subscription_feed_subtitle">空空如也</string>
|
||||
@ -250,7 +249,7 @@
|
||||
<string name="import_complete_toast">导入成功</string>
|
||||
<string name="no_valid_zip_file">没有有效的 ZIP 文件</string>
|
||||
<string name="could_not_import_all_files">警告:无法导入所有文件。</string>
|
||||
<string name="override_current_data">此操作会<b>覆盖当前设置</b>。</string>
|
||||
<string name="override_current_data">此操作会覆盖当前设置。</string>
|
||||
<string name="show_info">显示信息</string>
|
||||
<string name="tab_bookmarks">收藏</string>
|
||||
<string name="title_last_played">最近观看</string>
|
||||
@ -273,8 +272,8 @@
|
||||
<string name="resize_zoom">缩放画面</string>
|
||||
<string name="settings_category_debug_title">调试</string>
|
||||
<string name="caption_auto_generated">自动生成</string>
|
||||
<string name="enable_leak_canary_summary">『内存泄漏监视』可能导致应用在『核心转储』时无响应</string>
|
||||
<string name="enable_disposed_exceptions_title">报告『提前结束Android生命周期』错误</string>
|
||||
<string name="enable_leak_canary_summary">内存泄漏监测可能会导致应用在堆转储时无响应</string>
|
||||
<string name="enable_disposed_exceptions_title">报告超出生命周期的错误</string>
|
||||
<string name="enable_disposed_exceptions_summary">强制报告处理后的未送达的 Activity 或 Fragment 生命周期之外的 Rx 异常</string>
|
||||
<string name="use_inexact_seek_title">使用快速寻址(不精确)</string>
|
||||
<string name="use_inexact_seek_summary">快速寻址定位允许播放器以较低精确度为代价换取更快的寻址定位速度。此功能不适用于以 5、15 或 25 秒为隔的寻址定位</string>
|
||||
@ -301,8 +300,8 @@
|
||||
\n3. 单击“包含所有数据”,然后单击“取消全选”,然后仅选择“订阅”并单击“确定”
|
||||
\n4. 点击“下一步”,然后点击“创建导出作业”
|
||||
\n5. 出现“下载”按钮后点击它
|
||||
\n6. 单击下面的导入文件并选择下载的 zip 文件
|
||||
\n7.【如果 zip 导入失败】解压 .csv文件(通常在“YouTube和YouTube Music/subscriptions/subscriptions.csv”下),点击下方的导入文件,选择解压出来的 csv 文件</string>
|
||||
\n6. 单击下面的导入文件并选择下载的 .zip 文件
|
||||
\n7.(如果 .zip 导入失败)解压 .csv文件(通常在“YouTube和YouTube Music/subscriptions/subscriptions.csv”下),点击下方的导入文件,选择解压出来的 csv 文件</string>
|
||||
<string name="import_soundcloud_instructions">通过输入网址或你的 ID 导入 SoundCloud 配置文件:
|
||||
\n
|
||||
\n1. 在浏览器中启用“电脑模式“(该网站未适配移动设备);
|
||||
@ -333,7 +332,7 @@
|
||||
<string name="clear_search_history_summary">清空搜索历史关键词</string>
|
||||
<string name="delete_search_history_alert">是否删除全部搜索历史?</string>
|
||||
<string name="search_history_deleted">搜索历史已删除</string>
|
||||
<string name="app_license">NewPipe 是版权自由软件:您可以随时使用、研究共享和改进它。您可以根据自由软件基金会发布的 GNU 通用公共许可证 GPLv3 或(由您选择的)任何更高版本的许可证重新分发或修改该许可证。</string>
|
||||
<string name="app_license">NewPipe 是 Copyleft 的自由软件:您可以随时使用、研究共享和改进它。您可以根据自由软件基金会发布的 GNU 通用公共许可证 GPLv3 或(由您选择的)任何更高版本的许可证重新分发或修改该许可证。</string>
|
||||
<string name="import_settings">是否要导入设置?</string>
|
||||
<string name="privacy_policy_title">NewPipe 隐私政策</string>
|
||||
<string name="privacy_policy_encouragement">NewPipe 项目非常重视您的隐私。因此,未经您的同意,应用程序不会收集任何数据。
|
||||
@ -359,7 +358,7 @@
|
||||
<string name="brightness_gesture_control_summary">使用手势控制播放器的亮度</string>
|
||||
<string name="content_language_title">视频默认语言</string>
|
||||
<string name="app_update_notification_channel_name">应用更新通知</string>
|
||||
<string name="app_update_notification_channel_description">当 NewPipe 有新版本时发送通知</string>
|
||||
<string name="app_update_notification_channel_description">NewPipe 新版本的通知</string>
|
||||
<string name="download_to_sdcard_error_title">外置存储不可用</string>
|
||||
<string name="download_to_sdcard_error_message">无法下载到外部 SD 卡,修改下载文件夹位置?</string>
|
||||
<string name="saved_tabs_invalid_json">读取已保存标签时发生错误,因此使用默认标签</string>
|
||||
@ -522,7 +521,7 @@
|
||||
<string name="remove_watched_popup_title">移除看过的视频?</string>
|
||||
<string name="remove_watched">移除看过的视频</string>
|
||||
<string name="show_original_time_ago_summary">来自服务的原始文本将在串流项目中可见</string>
|
||||
<string name="show_original_time_ago_title">显示原始时间</string>
|
||||
<string name="show_original_time_ago_title">在项目上显示原始时间</string>
|
||||
<string name="youtube_restricted_mode_enabled_title">启用 YouTube“受限模式”</string>
|
||||
<string name="feed_group_show_only_ungrouped_subscriptions">仅显示未分组订阅</string>
|
||||
<string name="playlist_page_summary">播放列表页</string>
|
||||
@ -662,9 +661,19 @@
|
||||
<string name="checking_updates_toast">检查更新中…</string>
|
||||
<string name="manual_update_title">检查更新</string>
|
||||
<string name="feed_new_items">新订阅源条目</string>
|
||||
<string name="report_player_errors_summary">完整报告播放器错误,而不是弹出一个临时Toast(对诊断应用很有用)</string>
|
||||
<string name="show_crash_the_player_title">显示\"使播放器崩溃\"</string>
|
||||
<string name="show_crash_the_player_summary">在使用播放器时显示一个崩溃选项</string>
|
||||
<string name="crash_the_player">使播放器崩溃</string>
|
||||
<string name="report_player_errors_title">报告播放器错误</string>
|
||||
<string name="error_report_channel_name">错误报告通知</string>
|
||||
<string name="error_report_channel_description">提示报告错误的通知</string>
|
||||
<string name="error_report_notification_toast">发生错误,详见通知</string>
|
||||
<string name="show_error_snackbar">显示错误警示SnackBar</string>
|
||||
<string name="create_error_notification">创建一条错误通知</string>
|
||||
<string name="no_appropriate_file_manager_message">找不到适合此操作的文件管理器。
|
||||
\n请安装一文件管理器或尝试在下载设置中禁用“%s”。</string>
|
||||
<string name="no_appropriate_file_manager_message_android_10">找不到适合此操作的文件管理器。
|
||||
\n请安装与存储访问框架(SAF)兼容的文件管理器。</string>
|
||||
<string name="error_report_notification_title">NewPipe 遇到了一个错误,点击此处报告此错误</string>
|
||||
<string name="background_player_already_playing_toast">已经在后台播放</string>
|
||||
<string name="detail_pinned_comment_view_description">置顶评论</string>
|
||||
</resources>
|
@ -153,7 +153,6 @@
|
||||
<string name="info_labels">Што:\\nЗапыт:\\nМова кантэнту:\\nСэрвіс:\\nЧас па Грынвічы:\\nПакет:\\nВерсія:\\nВерсія АС:</string>
|
||||
<string name="your_comment">Ваш каментар (English):</string>
|
||||
<string name="error_details_headline">Падрабязнасці:</string>
|
||||
<string name="list_thumbnail_view_description">Мініяцюра відэа-прэв\'ю</string>
|
||||
<string name="detail_thumbnail_view_description">Мініяцюра відэа-прэв\'ю</string>
|
||||
<string name="detail_uploader_thumbnail_view_description">Мініяцюра аватара карыстальніка</string>
|
||||
<string name="detail_likes_img_view_description">Спадабалася</string>
|
||||
|
@ -213,7 +213,6 @@
|
||||
<string name="video_streams_empty">Не са намерени видео стриймове</string>
|
||||
<string name="audio_streams_empty">Не са намерени аудио стриймове</string>
|
||||
<string name="info_labels">Какво:\\nЗаявка:\\nЕзик на съдържанието:\\nУслуга:\\nВреме по GMT:\\nПакет:\\nВерсия:\\nОС версия:</string>
|
||||
<string name="list_thumbnail_view_description">Миниатюра на видео</string>
|
||||
<string name="detail_drag_description">Пренареди чрез плъзгане</string>
|
||||
<string name="start">Начало</string>
|
||||
<string name="rename">Преименувай</string>
|
||||
|
@ -77,7 +77,6 @@
|
||||
<string name="your_comment">তোমার মন্তব্য (ইংরেজিতে):</string>
|
||||
<string name="error_details_headline">বর্ণনা:</string>
|
||||
<!-- Content descriptions (for better accessibility) -->
|
||||
<string name="list_thumbnail_view_description">ভিডিও প্রাকদর্শন থাম্বনেইল</string>
|
||||
<string name="detail_thumbnail_view_description">ভিডিও প্রাকদর্শন, সময়ঃ</string>
|
||||
<string name="detail_uploader_thumbnail_view_description">আপলোডারের ইউজারপিক থাম্বনেইল</string>
|
||||
<string name="detail_likes_img_view_description">পছন্দ হয়েছে</string>
|
||||
|
@ -31,7 +31,6 @@
|
||||
<string name="detail_likes_img_view_description">পছন্দ হয়েছে</string>
|
||||
<string name="detail_uploader_thumbnail_view_description">আপলোডারের ইউজারপিক থাম্বনেইল</string>
|
||||
<string name="detail_thumbnail_view_description">ভিডিও প্রাকদর্শন, সময়ঃ</string>
|
||||
<string name="list_thumbnail_view_description">ভিডিও প্রাকদর্শন থাম্বনেইল</string>
|
||||
<string name="error_details_headline">বর্ণনা:</string>
|
||||
<string name="your_comment">আপনার মন্তব্য (ইংরেজিতে):</string>
|
||||
<string name="info_labels">কি:\\nঅনুরোধ:\\nকন্টেন্ট ভাষা:\\nসার্ভিস:\\nসময়(GMT এ):\\nপ্যাকেজ:\\nসংস্করণ:\\nওএস সংস্করণ:\\nআইপি পরিসর:</string>
|
||||
|
@ -116,7 +116,6 @@
|
||||
<string name="detail_likes_img_view_description">পছন্দ</string>
|
||||
<string name="detail_uploader_thumbnail_view_description">আপলোডারের অবয়বের প্রতিচ্ছবি</string>
|
||||
<string name="detail_thumbnail_view_description">ভিডিও চালাও, সময়ঃ</string>
|
||||
<string name="list_thumbnail_view_description">ভিডিও প্রাকদর্শন প্রতিচ্ছবি</string>
|
||||
<string name="error_details_headline">বর্ণনা:</string>
|
||||
<string name="your_comment">তোমার মন্তব্য (ইংরেজিতে):</string>
|
||||
<string name="info_labels">কি:\\nঅনুরোধ:\\nকন্টেন্ট ভাষা:\\nসার্ভিস:\\nসময়(GMT এ):\\nপ্যাকেজ:\\nসংস্করণ:\\nওএস সংস্করণ:\\nআইপি পরিসর:</string>
|
||||
|
@ -126,7 +126,7 @@
|
||||
<string name="thumbnail_cache_wipe_complete_notice">S\'ha eliminat la memòria cau d\'imatges</string>
|
||||
<string name="metadata_cache_wipe_title">Elimina les metadades de la memòria cau</string>
|
||||
<string name="metadata_cache_wipe_complete_notice">S\'ha esborrat la memòria cau de metadades</string>
|
||||
<string name="auto_queue_title">Afegeix vídeos relacionats a la cua</string>
|
||||
<string name="auto_queue_title">Posa a la cua el següent flux</string>
|
||||
<string name="show_search_suggestions_title">Suggeriments de cerca</string>
|
||||
<string name="show_search_suggestions_summary">Mostra suggeriments durant la cerca</string>
|
||||
<string name="enable_search_history_title">Historial de cerca</string>
|
||||
@ -136,8 +136,8 @@
|
||||
<string name="content_language_title">Llengua per defecte dels continguts</string>
|
||||
<string name="background_player_playing_toast">S\'està reproduint en rerefons</string>
|
||||
<string name="popup_playing_toast">S\'està reproduint en mode emergent</string>
|
||||
<string name="notification_channel_name">Notificació del NewPipe</string>
|
||||
<string name="notification_channel_description">Notificacions dels reproductors en rerefons o emergents del NewPipe</string>
|
||||
<string name="notification_channel_name">Notificació de NewPipe</string>
|
||||
<string name="notification_channel_description">Notificacions per al reproductor de NewPipe</string>
|
||||
<string name="could_not_load_thumbnails">No s\'han pogut carregar totes les miniatures</string>
|
||||
<string name="youtube_signature_deobfuscation_error">No s\'ha pogut desxifrar la signatura de l\'URL del vídeo</string>
|
||||
<string name="parsing_error">No s\'ha pogut processar el lloc web</string>
|
||||
@ -161,7 +161,6 @@
|
||||
<string name="what_happened_headline">Què ha passat:</string>
|
||||
<string name="your_comment">Comentari (en anglès):</string>
|
||||
<string name="error_details_headline">Detalls:</string>
|
||||
<string name="list_thumbnail_view_description">Miniatura de previsualització del vídeo</string>
|
||||
<string name="detail_thumbnail_view_description">Reprodueix el vídeo, duració:</string>
|
||||
<string name="detail_uploader_thumbnail_view_description">Miniatura de l\'avatar del propietari</string>
|
||||
<string name="detail_likes_img_view_description">M\'agrada</string>
|
||||
@ -271,7 +270,7 @@
|
||||
<string name="unhook_checkbox">Desvincula (pot causar deformació)</string>
|
||||
<string name="metadata_cache_wipe_summary">Elimina totes les dades de llocs web de la memòria cau</string>
|
||||
<string name="auto_queue_summary">Acaba de reproduir la cua (sense repetició) quan s\'hi afegeixi un vídeo relacionat</string>
|
||||
<string name="show_hold_to_append_title">Mostra els missatges d\'ajuda</string>
|
||||
<string name="show_hold_to_append_title">Mostra l\'indicador «Mantenir per posar a la cua»</string>
|
||||
<string name="show_hold_to_append_summary">Mostra un missatge d\'ajuda quan el botó de mode en segon pla o emergent estigui premut a la pàgina de detalls d\'un vídeo</string>
|
||||
<string name="info_labels">Què ha passat:\\nPetició:\\nIdioma del contingut:\\nPaís del contingut:\\nLlengua de l\'aplicació:\\nServei:\\nHora GMT:\\nPaquet:\\nVersió:\\nVersió del SO:</string>
|
||||
<string name="preferred_open_action_settings_title">Acció d\'obertura preferida</string>
|
||||
@ -560,7 +559,7 @@
|
||||
<string name="show_age_restricted_content_summary">Mostra contingut que podria ser inadequat pels infants</string>
|
||||
<string name="settings_category_notification_title">Notificació</string>
|
||||
<string name="unsupported_url_dialog_message">No s\'ha pogut reconèixer l\'adreça URL. Obrir-la amb una altra aplicació\?</string>
|
||||
<string name="auto_queue_toggle">Cua automàtica</string>
|
||||
<string name="auto_queue_toggle">Posa a la cua automàticament</string>
|
||||
<string name="show_meta_info_summary">Desactiveu-ho per deixar de mostrar les metadades, que contenen informació addicional sobre el creador del directe, el contingut o una sol·licitud de cerca</string>
|
||||
<string name="show_meta_info_title">Mostra les metadades</string>
|
||||
<string name="clear_queue_confirmation_description">La cua de reproducció activa serà sobreescrita</string>
|
||||
@ -653,4 +652,39 @@
|
||||
<string name="start_main_player_fullscreen_title">Inicia el reproductor principal en pantalla completa</string>
|
||||
<string name="main_page_content_swipe_remove">Llisqueu els elements per eliminar-los</string>
|
||||
<string name="start_main_player_fullscreen_summary">Si la rotació automàtica està bloquejada, no inicieu vídeos al mini reproductor, sinó que aneu directament al mode de pantalla completa. Podeu accedir igualment al mini reproductor sortint de pantalla completa</string>
|
||||
<string name="background_player_already_playing_toast">Ja s\'està reproduint en segon pla</string>
|
||||
<string name="error_report_channel_name">Notificació d\'informe d\'error</string>
|
||||
<string name="crash_the_player">Tancar abruptament el reproductor</string>
|
||||
<string name="manual_update_title">Comprovar si hi ha actualitzacions</string>
|
||||
<string name="manual_update_description">Comprovar manualment si hi ha noves versions</string>
|
||||
<plurals name="download_finished_notification">
|
||||
<item quantity="one">Baixada finalitzada</item>
|
||||
<item quantity="other">%s baixades finalitzades</item>
|
||||
</plurals>
|
||||
<string name="seekbar_preview_thumbnail_title">Vista prèvia de les miniatures de la barra de cerca</string>
|
||||
<string name="no_appropriate_file_manager_message_android_10">No s\'ha trobat cap gestor de fitxers adequat per a aquesta acció.
|
||||
\nInstal·leu un gestor de fitxers compatible amb l\'entorn d\'accés d\'emmagatzematge.</string>
|
||||
<string name="no_appropriate_file_manager_message">No s\'ha trobat cap gestor de fitxers adequat per a aquesta acció.
|
||||
\nInstal·leu un gestor de fitxers o intenteu desactivar «%s» als paràmetres de baixada.</string>
|
||||
<string name="error_report_notification_toast">S\'ha produït un error, consulteu la notificació</string>
|
||||
<string name="enqueued_next">Afegit el següent vídeo a la cua</string>
|
||||
<string name="error_report_notification_title">NewPipe ha trobat un error, toca per informar</string>
|
||||
<string name="enqueue_next_stream">Posa a la cua el següent vídeo</string>
|
||||
<string name="create_error_notification">Crear una notificació d\'error</string>
|
||||
<string name="error_show_channel_details">Error en mostrar els detalls del canal</string>
|
||||
<string name="error_report_channel_description">Notificacions per informar d\'errors</string>
|
||||
<string name="show_error_snackbar">Mostra una barra d\'errors</string>
|
||||
<string name="checking_updates_toast">S\'estan comprovant les actualitzacions…</string>
|
||||
<plurals name="deleted_downloads_toast">
|
||||
<item quantity="one">S\'ha suprimit %1$s baixada</item>
|
||||
<item quantity="other">S\'han suprimit %1$s baixades</item>
|
||||
</plurals>
|
||||
<string name="downloads_storage_use_saf_summary_api_19">El \"Sistema d\'Accés a l\'Emmagatzematge\" no està implementat a Android KitKat i a versions anteriors</string>
|
||||
<string name="downloads_storage_use_saf_summary_api_29">A partir de l\'Android 10 només s\'admet el \"Sistema d\'Accés a l\'Emmagatzematge\"</string>
|
||||
<string name="feed_new_items">Elements de feed nous</string>
|
||||
<string name="feed_load_error_fast_unknown">El mode d\'alimentació ràpida no proporciona més informació sobre això.</string>
|
||||
<string name="detail_pinned_comment_view_description">Comentari fixat</string>
|
||||
<string name="show_crash_the_player_title">Mostrar \"tancar de forma violenta el reproductor\"</string>
|
||||
<string name="show_crash_the_player_summary">Mostra una opció de fallada quan s\'utilitza el reproductor</string>
|
||||
<string name="show_image_indicators_summary">Mostra les cintes de color Picasso a la part superior de les imatges que indiquen la seva font: vermell per a la xarxa, blau per al disc i verd per a la memòria</string>
|
||||
</resources>
|
@ -45,7 +45,7 @@
|
||||
<string name="clear_search_history_title">سڕینەوەی مێژووی گەڕان</string>
|
||||
<string name="msg_error">هەڵە</string>
|
||||
<string name="override_current_data">ئەمە لەسەر ڕێکخستنەکانی ئێستات جێگیر دەبێت.</string>
|
||||
<string name="notification_channel_name">ئاگانامەکانی نیوپایپ</string>
|
||||
<string name="notification_channel_name">پەیامەکانی نیوپایپ</string>
|
||||
<string name="donation_encouragement">نیوپایپ لەلایەن چەند خۆبەخشێکەوە دروستکراوە کە کاتهكانی خۆیان پێ بەخشیووە تاکو باشترین خزمەتگوزاریت پێشکەش بکەن. هیچ نەبێت بە کڕینی کوپێک قاوە یارمەتی گەشەپێدەرەکانمان بدە بۆ ئەوەی کاتی زیاتر تەرخان بکەین بۆ بەرەوپێشبردنی نیوپایپ.</string>
|
||||
<string name="short_billion">ملیار</string>
|
||||
<string name="show_search_suggestions_title">گەڕانی پێشنیارکراوەکان</string>
|
||||
@ -177,7 +177,6 @@
|
||||
<string name="auto_queue_title">خستنه نۆبهتی-خۆكاری پهخشی دواتر</string>
|
||||
<string name="external_player_unsupported_link_type">لێدهره دەرەکییەکان پشتگیری ئەم جۆرە بەستەرانە ناکەن</string>
|
||||
<string name="permission_denied">کردار ڕەتکرایەوە لەلایەن سیستەمەوە</string>
|
||||
<string name="list_thumbnail_view_description">زووبینینی وێنۆچکەی ڤیدیۆ</string>
|
||||
<string name="controls_popup_title">پهنجهره</string>
|
||||
<string name="black_theme_title">ڕهش</string>
|
||||
<string name="default_popup_resolution_title">قهبارهی بنەڕەتی پهنجهره</string>
|
||||
@ -238,9 +237,9 @@
|
||||
<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>
|
||||
<string name="app_update_notification_channel_description">پەیامەکانی وەشانە نوێیەکانی نیوپایپ</string>
|
||||
<string name="show_higher_resolutions_summary">تەنها چەند مۆبایلێک پشتگیری لێدانی ڤیدیۆی 2K/4K دەکەن</string>
|
||||
<string name="notification_channel_description">ئاگانامەکانی پاشبنەمای نیوپایپ و لێدانهكانی پەنجەرە</string>
|
||||
<string name="notification_channel_description">پەیامەکانی لێدەری نیوپایپ</string>
|
||||
<string name="feed_use_dedicated_fetch_method_summary">لەهەندێ خزمەتگوزاریدا بەردەستە، بەزۆری خێراترینە بەڵام ڕەنگە هەندێک لە بابەتەکان زانیارییەکانیان ناتەواو بێت (وەک نەبوونی ماوە، جۆری بابەت ، نەبوونی پەخش)</string>
|
||||
<plurals name="seconds">
|
||||
<item quantity="one">%d چرکە</item>
|
||||
@ -276,7 +275,7 @@
|
||||
<string name="video_streams_empty">هیچ پەخشێکی ڤیدیۆیی نەدۆزرایەوە</string>
|
||||
<string name="import_from">هاوردە لە</string>
|
||||
<string name="subtitle_activity_recaptcha">كرتە بکە لەسەر ”كرا” کاتێك کە چارەسەرکرا</string>
|
||||
<string name="app_update_notification_channel_name">ئاگانامەی نوێکاری بەرنامە</string>
|
||||
<string name="app_update_notification_channel_name">پەیامی نوێکردنەوەی بەرنامە</string>
|
||||
<string name="could_not_load_thumbnails">ناتوانرێت هەموو وێنۆچکەکان باربکرێن</string>
|
||||
<string name="previous_export">هەناردەی پێشووتر</string>
|
||||
<string name="no_valid_zip_file">فایلی ZIP دروست نییە</string>
|
||||
@ -395,7 +394,7 @@
|
||||
<string name="bookmark_playlist">نیشانهكردنی خشتهلێدان</string>
|
||||
<string name="file_name_empty_error">ناوی فایل ناکرێت بەتاڵ بێت</string>
|
||||
<string name="youtube_signature_deobfuscation_error">ناتوانرێت واژووی بەستەری ڤیدیۆ بخوێنرێتەوە</string>
|
||||
<string name="updates_setting_description">پیشاندانی ئاگانامەیەک بۆ ئامادەبوونی بهرنامه لەکاتی بەردەست بوونی وەشانی نوێ</string>
|
||||
<string name="updates_setting_description">پیشاندانی پەیامێک بۆ ئامادەبوونی بهرنامه لەکاتی بەردەست بوونی وەشانی نوێ</string>
|
||||
<string name="enable_watch_history_title">مێژووی سەیرکردن</string>
|
||||
<string name="enable_disposed_exceptions_title">سکاڵا لەسەر کێشەکان</string>
|
||||
<string name="playlist_no_uploader">خۆکار-دانراو (هیچ بەرزکەرەوەیەک نەدۆزرایەوە)</string>
|
||||
@ -543,7 +542,7 @@
|
||||
<string name="error_report_open_github_notice">تكایه پشكنینێك بكه كه ئاخۆ كێشهیهك ههیه باسی كڕاشهكهت بكات. لهكاتی سازدانی پلیتی لێكچوو ، كات له ئێمه دهگریت كه ئێمه سهرقاڵی چارهسهركردنی ههمان كێشه دهكهیت.</string>
|
||||
<string name="error_report_open_issue_button_text">سكاڵا لەسەر GitHub</string>
|
||||
<string name="copy_for_github">لهبهرگرتنهوهی سكاڵای جۆركراو</string>
|
||||
<string name="settings_category_notification_title">ئاگانامە</string>
|
||||
<string name="settings_category_notification_title">پەیام</string>
|
||||
<string name="unsupported_url_dialog_message">ناتوانرێت بهستهرهكه بناسرێتەوە. بە بەرنامەیەکی دیكه بکرێتەوە؟</string>
|
||||
<string name="auto_queue_toggle">خستنه نۆبهتی-خۆكاری</string>
|
||||
<string name="clear_queue_confirmation_description">نۆبهتهكه لە لێدەری چالاکەوە جێگۆڕکێی دەکرێت</string>
|
||||
@ -553,14 +552,14 @@
|
||||
<string name="notification_action_buffering">نێوانگری</string>
|
||||
<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_at_most_three">دەتوانیت تا سێ كردار دیار بكهیت تا پیشان بدرێن له پەیامەکەدا!</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>
|
||||
<string name="notification_action_1_title">دووهم كرداری دوگمه</string>
|
||||
<string name="notification_action_0_title">یهكهم كرداری دوگمه</string>
|
||||
<string name="notification_scale_to_square_image_summary">وێنۆچكهی ڤیدیۆ پێوانه دهكرێتهوه له ئاگانامهكاندا له ڕهههندی 16:9 هوه بۆ ڕهههندی 1:1</string>
|
||||
<string name="notification_scale_to_square_image_summary">وێنۆچكهی ڤیدیۆ پێوانه دهكرێتهوه له پەیامەکاندا له ڕهههندی 16:9 هوه بۆ ڕهههندی 1:1</string>
|
||||
<string name="notification_scale_to_square_image_title">پێوانەكردنی وێنۆچكه بۆ ڕهههندی 1:1</string>
|
||||
<string name="search_showing_result_for">پیشاندانی ئەنجامەکانی: %s</string>
|
||||
<string name="open_with">كردنهوه له</string>
|
||||
@ -569,8 +568,8 @@
|
||||
<string name="show_description_summary">ناكارایبكه بۆ شاردنهوهی دیسکریپشن لهسهر ڤیدیۆ و زانیاری زیاتر</string>
|
||||
<string name="show_description_title">پیشاندانی دیسکریپشن</string>
|
||||
<string name="night_theme_title">ڕووكاری شهو</string>
|
||||
<string name="notification_colorize_summary">ئهندرۆید ڕهنگی ئاگانامه دڵخواز دهكات بهپێی ڕهنگی سهرهكی وێنۆچكهكه ( ڕهچاوی ئهوه بكه كه ئهم تایبهتمهندییه ههموو ئامێرێك ناگرێتهوه )</string>
|
||||
<string name="notification_colorize_title">ڕهنگكردنی ئاگانامه</string>
|
||||
<string name="notification_colorize_summary">ئهندرۆید ڕهنگی پەیام دڵخواز دهكات بهپێی ڕهنگی سهرهكی وێنۆچكهكه ( ڕهچاوی ئهوه بكه كه ئهم تایبهتمهندییه ههموو ئامێرێك ناگرێتهوه )</string>
|
||||
<string name="notification_colorize_title">ڕهنگكردنی پەیام</string>
|
||||
<string name="youtube_restricted_mode_enabled_summary">یوتوب ”دۆخی قهدهغهكراو” پێشكهش دهكات كه بابەتە نهشیاوهكان دهشارێتهوه</string>
|
||||
<string name="msg_calculating_hash">ئهژماركردنی هاش</string>
|
||||
<string name="comments_are_disabled">لێدوانهكان ناكاراكراون</string>
|
||||
@ -582,8 +581,8 @@
|
||||
<string name="clear_cookie_summary">خاوێنكردنهوی ئهو شهكرۆكانهی كه له نیوپایپ كۆگاكراون ، ئهمهش لهدوای شیكار كردنی reCAPTCHA</string>
|
||||
<string name="recaptcha_cookies_cleared">شهكرۆكهكانی reCAPTCHA خاوێنكرانهوه</string>
|
||||
<string name="clear_cookie_title">سڕینهوهی شهكرۆكهكانی reCAPTCHA</string>
|
||||
<string name="hash_channel_description">ئاگانامهكانی ئهنجامدانی هاشكردنی ڤیدیۆ</string>
|
||||
<string name="hash_channel_name">ئاگانامهی هاشی ڤیدیۆ</string>
|
||||
<string name="hash_channel_description">پەیامەکانی ئهنجامدانی هاش كردنی ڤیدیۆ</string>
|
||||
<string name="hash_channel_name">پەیامی هاش ڤیدیۆ</string>
|
||||
<string name="description_select_disable">ناکاراکردنی دیار کردنی نوسینی نێو دیسکریبشن</string>
|
||||
<string name="description_select_enable">کاراکردنی دیار کردنی نوسینی نێو دیسکریبشن</string>
|
||||
<string name="description_select_note">لە ئێستادا دەتوانیت نوسینی نێو دیسکریپشن دیار بکەیت. بەڵام ڕەچاوی ئەوە بکە کە ئەو پەڕەیە ڕەنگە تێکبچێت و لینکەکان کرتەیان لەسەر نەکرێت لە دۆخی دیار کردندا.</string>
|
||||
@ -622,7 +621,7 @@
|
||||
<string name="no_app_to_open_intent">هیچ بهرنامهیهكی نێو مۆبایلهكهت ناتوانێت ئهمه بكاتهوه</string>
|
||||
<string name="chapters">بهشهكان</string>
|
||||
<string name="recent">دواین</string>
|
||||
<string name="show_thumbnail_summary">وێنۆچكهكه بۆ پاشبنهمای ڕوونماداخراو و ئاگانامهكان بهكاربهێنرێن</string>
|
||||
<string name="show_thumbnail_summary">وێنۆچكهكه بۆ پاشبنهمای ڕوونماداخراو و پەیامەکان بهكاردەهێنرێن</string>
|
||||
<string name="show_thumbnail_title">پیشاندانی وێنۆچكه</string>
|
||||
<string name="feed_toggle_show_played_items">تەماشاکراوەکان پیشان بدرێن</string>
|
||||
<string name="downloads_storage_ask_summary_no_saf_notice">بۆ دابهزاندنی ههر بابهتێك پرست پێ دهكرێت لهبارهی شوێنی دابهزاندنیان</string>
|
||||
@ -669,4 +668,18 @@
|
||||
<string name="enqueue_next_stream">لە نۆبەت دانان بۆ دواتر</string>
|
||||
<string name="enqueued_next">لە نۆبەت دانرا بۆ دواتر</string>
|
||||
<string name="processing_may_take_a_moment">جێبەجێ دەکرێت... ڕەنگە ساتێک بخایەنێت</string>
|
||||
<string name="checking_updates_toast">دەپشکنرێت بۆ نوێکردنەوە…</string>
|
||||
<string name="manual_update_description">پشکنینی خۆیی بۆ وەشانی نوێ</string>
|
||||
<string name="show_crash_the_player_summary">بژاردەی کڕاش کردن پیشان دەدات لەکاتی بەکارهێنانی لێدەرەکە</string>
|
||||
<string name="error_report_notification_toast">کێشەیەک ڕوویدا ، پەیامەکە ببینە</string>
|
||||
<string name="error_report_notification_title">نیوپایپ تووشی کێشەیەک بوو ، کرتە بکە بۆ سکاڵاکردن</string>
|
||||
<string name="show_crash_the_player_title">پیشاندانی ”کڕاش کردنی لێدەرەکە“</string>
|
||||
<string name="create_error_notification">سازاندنی پەیامی کێشەیەک</string>
|
||||
<string name="manual_update_title">پشکنین بۆ نوێکردنەوە</string>
|
||||
<string name="background_player_already_playing_toast">وا لە پاشبنەمادا لێدەدرێت</string>
|
||||
<string name="error_report_channel_name">کێشە لە سکاڵا کردنی پەیام</string>
|
||||
<string name="error_report_channel_description">پەیامەکانی سکاڵاکردن لە کێشەکان</string>
|
||||
<string name="feed_new_items">بابەتە نوێیەکانی فیید</string>
|
||||
<string name="detail_pinned_comment_view_description">لێدوانی هەڵواسراو</string>
|
||||
<string name="crash_the_player">کڕاش کردنی لێدەر</string>
|
||||
</resources>
|
@ -42,7 +42,6 @@
|
||||
<string name="youtube_signature_deobfuscation_error">Nebylo možné dekódovat URL videa</string>
|
||||
<string name="parsing_error">Nebylo možné analyzovat stránku</string>
|
||||
<string name="content_not_available">Obsah není k dispozici</string>
|
||||
<string name="list_thumbnail_view_description">Náhled videa</string>
|
||||
<string name="detail_thumbnail_view_description">Přehrát video, délka:</string>
|
||||
<string name="detail_uploader_thumbnail_view_description">Náhled avataru uploadera</string>
|
||||
<string name="detail_likes_img_view_description">To se mi líbí</string>
|
||||
@ -124,7 +123,7 @@
|
||||
<string name="best_resolution">Nejlepší rozlišení</string>
|
||||
<string name="undo">Vrátit</string>
|
||||
<string name="notification_channel_name">Oznámení NewPipe</string>
|
||||
<string name="notification_channel_description">Oznámení pro NewPipe přehrávače v pozadí a v okně</string>
|
||||
<string name="notification_channel_description">Oznámení pro NewPipe přehrávač</string>
|
||||
<string name="search_no_results">Žádné výsledky</string>
|
||||
<string name="empty_subscription_feed_subtitle">Je tu sranda jak v márnici</string>
|
||||
<string name="short_billion">mld.</string>
|
||||
@ -165,7 +164,7 @@
|
||||
<string name="contribution_title">Podílet se</string>
|
||||
<string name="title_activity_history">Historie</string>
|
||||
<string name="action_history">Historie</string>
|
||||
<string name="show_hold_to_append_title">Zobrazovat tip \"Podržet pro přidání\"</string>
|
||||
<string name="show_hold_to_append_title">Zobrazit tip \"Podržet pro vložení do fronty\"</string>
|
||||
<string name="show_hold_to_append_summary">Ukázat tip po stisku na pozadí nebo na popup tlačítko v \"Podrobnostech\" o videu</string>
|
||||
<string name="play_all">Přehrát vše</string>
|
||||
<string name="player_stream_failure">Tento stream nelze přehrát</string>
|
||||
@ -259,7 +258,7 @@
|
||||
<string name="metadata_cache_wipe_title">Vymazat metadata v mezipaměti</string>
|
||||
<string name="metadata_cache_wipe_summary">Odebrat všechna data uložená v mezipaměti</string>
|
||||
<string name="metadata_cache_wipe_complete_notice">Mezipaměť metadat vymazána</string>
|
||||
<string name="auto_queue_title">Automatická fronta dalšího streamu</string>
|
||||
<string name="auto_queue_title">Další stream automaticky vložit do fronty</string>
|
||||
<string name="auto_queue_summary">Pokračovat konečnou (neopakující se) frontu playbacku připojením souvisejícího streamu</string>
|
||||
<string name="file">Soubor</string>
|
||||
<string name="invalid_directory">Neexistující složka</string>
|
||||
@ -276,15 +275,15 @@
|
||||
<string name="previous_export">Předchozí export</string>
|
||||
<string name="subscriptions_import_unsuccessful">Odběry nelze importovat</string>
|
||||
<string name="subscriptions_export_unsuccessful">Odběry nelze exportovat</string>
|
||||
<string name="import_youtube_instructions">Importovat YouTube odběry stáhnutím exportního souboru:
|
||||
<string name="import_youtube_instructions">Importovat YouTube odběry z Google:
|
||||
\n
|
||||
\n1. Přejděte na tuto URL adresu: %1$s
|
||||
\n2. Na vyžádání se přihlašte
|
||||
\n3. Klikněte na \"Jsou zahrnuta všechna data z YouTube\" pak na \"Zrušit výběr všech\" a pak vyberte jen \"odběry\" a klikněte na OK
|
||||
\n3. Klikněte na \"Jsou zahrnuta všechna data\", \"Zrušit výběr všech\" a pak vyberte jen \"odběry\" a klikněte na OK
|
||||
\n4. Klikněte na \"Další krok\" a pak na \"Vytvořit export\"
|
||||
\n5. Vyčkejte než se objeví tlačítko \"Stáhnout\" a klikněte na něj
|
||||
\n6. Klikněte IMPORTOVAT SOUBOR a vyberte stažený .zip soubor z předchozího kroku
|
||||
\n7. [Pokud se import .zip souboru nezdaří] Rozbalte soubor subscriptions.csv (měl by se nacházet v adresáři \"YouTube and YouTube Music/subscriptions/subscriptions.csv\"), stiskněte IMPORTOVAT SOUBOR níže a vyberte rozbalený soubor subscriptions.csv</string>
|
||||
\n7. [Pokud se import .zip souboru nezdaří] Rozbalte soubor .csv (měl by to být soubor \"YouTube and YouTube Music/subscriptions/subscriptions.csv\"), stiskněte IMPORTOVAT SOUBOR níže a vyberte rozbalený csv soubor</string>
|
||||
<string name="import_soundcloud_instructions">Importovat SoundCloud profil zadáním URL adresy nebo vašeho ID:
|
||||
\n
|
||||
\n1. Ve svém prohlížeči povolte \"režim pro PC\" (pro mobilní zařízení není stránka dostupná)
|
||||
@ -347,7 +346,7 @@
|
||||
<string name="settings_category_updates_title">Aktualizace</string>
|
||||
<string name="file_deleted">Soubor smazán</string>
|
||||
<string name="app_update_notification_channel_name">Oznámení o aktualizaci aplikace</string>
|
||||
<string name="app_update_notification_channel_description">Oznámení o nové verzi NewPipe</string>
|
||||
<string name="app_update_notification_channel_description">Oznámení o nových verzích NewPipe</string>
|
||||
<string name="download_to_sdcard_error_title">Externí úložiště není k dispozici</string>
|
||||
<string name="saved_tabs_invalid_json">Nelze načíst uložené karty, takže se použijí výchozí karty</string>
|
||||
<string name="restore_defaults">Obnovit do výchozího nastavení</string>
|
||||
@ -682,4 +681,23 @@
|
||||
<string name="main_page_content_swipe_remove">Tažením položky odstraníte</string>
|
||||
<string name="start_main_player_fullscreen_title">Spustit hlavní přehrávač na celé obrazovce</string>
|
||||
<string name="processing_may_take_a_moment">Zpracovávám... může trvat moment</string>
|
||||
<string name="manual_update_description">Ručně zkontrolovat zda je k dispozici nová verze</string>
|
||||
<string name="manual_update_title">Kontrola aktualizací</string>
|
||||
<string name="error_report_notification_title">NewPipe narazil na problém, klikněte pro nahlášení</string>
|
||||
<string name="error_report_notification_toast">Došlo k chybě, více v oznámení</string>
|
||||
<string name="create_error_notification">Vytvořit oznámení o chybě</string>
|
||||
<string name="checking_updates_toast">Kontrola aktualizací…</string>
|
||||
<string name="show_crash_the_player_title">Ukázat \"Zřícení přehávače\"</string>
|
||||
<string name="background_player_already_playing_toast">Hraje již v pozadí</string>
|
||||
<string name="feed_new_items">Nové položky feedů</string>
|
||||
<string name="no_appropriate_file_manager_message_android_10">Pro tuto akci nebyl nalezen žádný vhodný správce souborů.
|
||||
\nProsím, nainstalujte správce souborů kompatibilní se Storage Access Framework.</string>
|
||||
<string name="error_report_channel_name">Oznámení o hlášení chyb</string>
|
||||
<string name="error_report_channel_description">Oznámení za účelem hlášení chyb</string>
|
||||
<string name="no_appropriate_file_manager_message">Pro tuto akci nebyl nalezen žádný vhodný správce souborů.
|
||||
\nProsím, nainstalujte správce souborů nebo zkuste vypnout %s v nastaveních stahování.</string>
|
||||
<string name="show_crash_the_player_summary">Ukáže volbu pro zřícení během používání přehrávače</string>
|
||||
<string name="show_error_snackbar">Ukázat krátké oznámení o chybě</string>
|
||||
<string name="detail_pinned_comment_view_description">Připnutý komentář</string>
|
||||
<string name="crash_the_player">Zřícení přehrávače</string>
|
||||
</resources>
|
@ -1,8 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="main_bg_subtitle">Tryk søg for at komme i gang</string>
|
||||
<string name="main_bg_subtitle">Tryk på forstørrelsesglasset for at komme i gang.</string>
|
||||
<string name="upload_date_text">Udgivet %1$s</string>
|
||||
<string name="no_player_found">Ingen streamafspiller blev fundet. Vil du installere VLC\?</string>
|
||||
<string name="no_player_found">Ingen streamafspiller blev fundet. Installer 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>
|
||||
@ -171,7 +171,6 @@
|
||||
<string name="what_happened_headline">Hvad skete der:</string>
|
||||
<string name="your_comment">Din kommentar (på engelsk):</string>
|
||||
<string name="error_details_headline">Detaljer:</string>
|
||||
<string name="list_thumbnail_view_description">Videominiaturebillede</string>
|
||||
<string name="detail_thumbnail_view_description">Videominiaturebillede</string>
|
||||
<string name="detail_uploader_thumbnail_view_description">Uploaders profilbillede</string>
|
||||
<string name="detail_likes_img_view_description">Synes godt om</string>
|
||||
|
@ -26,7 +26,6 @@
|
||||
<string name="unsupported_url">Nicht unterstützte URL</string>
|
||||
<string name="settings_category_video_audio_title">Video und Audio</string>
|
||||
<string name="content_language_title">Bevorzugte Sprache des Inhalts</string>
|
||||
<string name="list_thumbnail_view_description">Video-Vorschaubild</string>
|
||||
<string name="detail_thumbnail_view_description">Video abspielen, Dauer:</string>
|
||||
<string name="detail_uploader_thumbnail_view_description">Avatarbild des Benutzers</string>
|
||||
<string name="detail_dislikes_img_view_description">Gefällt mir nicht</string>
|
||||
@ -140,7 +139,7 @@
|
||||
<string name="subscription_update_failed">Abonnement konnte nicht aktualisiert werden</string>
|
||||
<string name="resume_on_audio_focus_gain_summary">Nach Unterbrechungen (z.B. Telefonaten) Wiedergabe fortsetzen</string>
|
||||
<string name="notification_channel_name">NewPipe-Benachrichtigung</string>
|
||||
<string name="notification_channel_description">Benachrichtigungen für NewPipe-Hintergrund- und Pop-up Wiedergabe</string>
|
||||
<string name="notification_channel_description">Benachrichtigungen für den NewPipe-Player</string>
|
||||
<string name="settings_category_player_behavior_title">Verhalten</string>
|
||||
<string name="settings_category_history_title">Verlauf und Cache</string>
|
||||
<string name="undo">Rückgängig machen</string>
|
||||
@ -345,8 +344,8 @@
|
||||
<string name="brightness_gesture_control_summary">Gesten verwenden, um die Helligkeit einzustellen</string>
|
||||
<string name="settings_category_updates_title">Aktualisierungen</string>
|
||||
<string name="file_deleted">Datei gelöscht</string>
|
||||
<string name="app_update_notification_channel_name">Aktualisierungsbenachrichtigung</string>
|
||||
<string name="app_update_notification_channel_description">Benachrichtigung bei neuer NewPipe-Version</string>
|
||||
<string name="app_update_notification_channel_name">Benachrichtigung über App-Update</string>
|
||||
<string name="app_update_notification_channel_description">Benachrichtigungen über neue NewPipe-Versionen</string>
|
||||
<string name="download_to_sdcard_error_title">Kein externer Speicher verfügbar</string>
|
||||
<string name="download_to_sdcard_error_message">Herunterladen auf externe SD-Karte ist nicht möglich. Downloadordner zurücksetzen\?</string>
|
||||
<string name="saved_tabs_invalid_json">Konnte gespeicherte Tabs nicht lesen, daher werden die Voreinstellungen genutzt</string>
|
||||
@ -371,7 +370,7 @@
|
||||
<string name="paused">pausiert</string>
|
||||
<string name="queued">eingereiht</string>
|
||||
<string name="post_processing">Nachbearbeitung</string>
|
||||
<string name="enqueue">Warteschlange</string>
|
||||
<string name="enqueue">In Wiedergabe einreihen</string>
|
||||
<string name="permission_denied">System verweigert den Zugriff</string>
|
||||
<string name="download_failed">Herunterladen fehlgeschlagen</string>
|
||||
<string name="generate_unique_name">Eindeutigen Namen erzeugen</string>
|
||||
@ -583,8 +582,8 @@
|
||||
<string name="show_thumbnail_summary">Vorschaubild für Sperrbildschirmhintergrund und Benachrichtigungen verwenden</string>
|
||||
<string name="show_thumbnail_title">Vorschaubild anzeigen</string>
|
||||
<string name="msg_calculating_hash">Hash wird berechnet</string>
|
||||
<string name="hash_channel_description">Benachrichtigungen für den Video-hashing Fortschritt</string>
|
||||
<string name="hash_channel_name">Video Hash Benachrichtigung</string>
|
||||
<string name="hash_channel_description">Benachrichtigungen über den Hashing-Fortschritt von Videos</string>
|
||||
<string name="hash_channel_name">Video-Hash Benachrichtigung</string>
|
||||
<string name="recent">Letzte</string>
|
||||
<string name="show_meta_info_title">Metadaten anzeigen</string>
|
||||
<string name="show_meta_info_summary">Deaktiviere diese Option, um Meta-Infofelder mit zusätzlichen Informationen zum Stream-Ersteller, zum Stream-Inhalt oder zu einer Suchanforderung auszublenden</string>
|
||||
@ -676,7 +675,17 @@
|
||||
<string name="feed_new_items">Neue Feed-Elemente</string>
|
||||
<string name="show_crash_the_player_title">\"Absturz des Players\" anzeigen</string>
|
||||
<string name="crash_the_player">Absturz des Players</string>
|
||||
<string name="report_player_errors_summary">Meldet Playerfehler mit allen Details, anstatt eine kurzlebige Popupmeldung anzuzeigen (nützlich für die Diagnose von Problemen)</string>
|
||||
<string name="show_crash_the_player_summary">Zeigt eine Absturzoption an, wenn der Player verwendet wird</string>
|
||||
<string name="report_player_errors_title">Playerfehler melden</string>
|
||||
<string name="error_report_channel_name">Benachrichtigung über Fehlerberichte</string>
|
||||
<string name="error_report_channel_description">Benachrichtigungen zur Meldung von Fehlern</string>
|
||||
<string name="error_report_notification_title">Bei NewPipe ist ein Fehler aufgetreten. Zum Melden antippen</string>
|
||||
<string name="error_report_notification_toast">Ein Fehler ist aufgetreten, siehe die Benachrichtigung</string>
|
||||
<string name="create_error_notification">Eine Fehlermeldung erstellen</string>
|
||||
<string name="show_error_snackbar">Fehler-Kurzmeldung anzeigen</string>
|
||||
<string name="no_appropriate_file_manager_message">Es wurde kein geeigneter Dateimanager für diese Aktion gefunden.
|
||||
\nBitte installiere einen Dateimanager oder versuche, \'%s\' in den Downloadeinstellungen zu deaktivieren.</string>
|
||||
<string name="no_appropriate_file_manager_message_android_10">Es wurde kein geeigneter Dateimanager für diese Aktion gefunden.
|
||||
\nBitte installiere einen Storage Access Framework kompatiblen Dateimanager.</string>
|
||||
<string name="background_player_already_playing_toast">Wird bereits im Hintergrund abgespielt</string>
|
||||
<string name="detail_pinned_comment_view_description">Angehefteter Kommentar</string>
|
||||
</resources>
|
@ -37,7 +37,6 @@
|
||||
<string name="settings_category_appearance_title">Εμφάνιση</string>
|
||||
<string name="background_player_playing_toast">Αναπαραγωγή στο παρασκήνιο</string>
|
||||
<string name="network_error">Σφάλμα δικτύου</string>
|
||||
<string name="list_thumbnail_view_description">Μικρογραφία προεπισκόπησης βίντεο</string>
|
||||
<string name="detail_thumbnail_view_description">Αναπαραγωγή βίντεο, διάρκεια:</string>
|
||||
<string name="detail_uploader_thumbnail_view_description">Μικρογραφία εικόνας προφίλ του χρήστη</string>
|
||||
<string name="detail_likes_img_view_description">Like</string>
|
||||
@ -131,7 +130,7 @@
|
||||
<string name="just_once">Μόνο μία φορά</string>
|
||||
<string name="file">Αρχείο</string>
|
||||
<string name="notification_channel_name">Ειδοποίηση NewPipe</string>
|
||||
<string name="notification_channel_description">Ειδοποιήσεις αναπαραγωγής παρασκηνίου και αναδυόμενου παραθύρου</string>
|
||||
<string name="notification_channel_description">Ειδοποιήσεις του αναπαραγωγέα του NewPipe</string>
|
||||
<string name="unknown_content">[Άγνωστο]</string>
|
||||
<string name="switch_to_background">Αλλαγή σε Παρασκήνιο</string>
|
||||
<string name="switch_to_popup">Αλλαγή σε Αναδυόμενο Παράθυρο</string>
|
||||
@ -344,7 +343,7 @@
|
||||
<string name="settings_category_updates_title">Ενημερώσεις</string>
|
||||
<string name="events">Συμβάντα</string>
|
||||
<string name="file_deleted">Το αρχείο διαγράφηκε</string>
|
||||
<string name="app_update_notification_channel_name">Ειδοποίηση Ενημέρωσης Εφαρμογής</string>
|
||||
<string name="app_update_notification_channel_name">Ειδοποίηση ενημέρωσης εφαρμογής</string>
|
||||
<string name="app_update_notification_channel_description">Ειδοποίηση για νεότερη έκδοση του NewPipe</string>
|
||||
<string name="download_to_sdcard_error_title">Εξωτερική μνήμη αποθήκευσης μη διαθέσιμη</string>
|
||||
<string name="download_to_sdcard_error_message">Η αποθήκευση στην SD κάρτα δεν είναι δυνατή. Επαναφορά στην αρχική τοποθεσία λήψης;</string>
|
||||
@ -367,7 +366,7 @@
|
||||
<string name="paused">σε παύση</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>
|
||||
@ -676,7 +675,17 @@
|
||||
<string name="feed_new_items">Νέα αντικείμενα τροφοδοσίας</string>
|
||||
<string name="show_crash_the_player_title">Εμφάνιση «κατάρρευσης αναπαραγωγέα»</string>
|
||||
<string name="show_crash_the_player_summary">Εμφανίζει μια επιλογή κατάρρευσης κατά τη χρήση του αναπαραγωγέα</string>
|
||||
<string name="report_player_errors_title">Αναφορά σφαλμάτων αναπαραγωγέα</string>
|
||||
<string name="report_player_errors_summary">Αναφορά σφαλμάτων αναπαραγωγέα με λεπτομέρειες αντί για ένα σύντομο μήνυμα (χρήσιμο για διάγνωση προβλημάτων)</string>
|
||||
<string name="crash_the_player">Κατάρρευση αναπαραγωγέα</string>
|
||||
<string name="error_report_channel_name">Ειδοποίηση αναφοράς σφάλματος</string>
|
||||
<string name="error_report_channel_description">Ειδοποιήσεις για την αναφορά σφαλμάτων</string>
|
||||
<string name="error_report_notification_toast">Συνέβη ένα σφάλμα. Δείτε την ειδοποίηση</string>
|
||||
<string name="create_error_notification">Δημιουργία ειδοποίησης σφάλματος</string>
|
||||
<string name="no_appropriate_file_manager_message">Δε βρέθηκε κατάλληλος διαχειριστής αρχείων για αυτή την ενέργεια.
|
||||
\nΕγκαταστήστε έναν ή δοκιμάστε να απενεργοποιήσετε το \'%s\' στις ρυθμίσεις λήψεων.</string>
|
||||
<string name="no_appropriate_file_manager_message_android_10">Δε βρέθηκε κατάλληλος διαχειριστής αρχείων για αυτή την ενέργεια.
|
||||
\nΕγκαταστήστε έναν συμβατό με το Πλαίσιο Πρόσβασης Αποθήκευσης.</string>
|
||||
<string name="error_report_notification_title">Το NewPipe παρουσίασε ένα σφάλμα. Πατήστε για αναφορά</string>
|
||||
<string name="show_error_snackbar">Εμφάνιση μιας snackbar σφάλματος</string>
|
||||
<string name="background_player_already_playing_toast">Αναπαράγεται ήδη στο παρασκήνιο</string>
|
||||
<string name="detail_pinned_comment_view_description">Καρφιτσωμένο σχόλιο</string>
|
||||
</resources>
|
2
app/src/main/res/values-enm/strings.xml
Normal file
2
app/src/main/res/values-enm/strings.xml
Normal file
@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources></resources>
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user