1
0
mirror of https://github.com/TeamNewPipe/NewPipe synced 2024-12-23 08:30:44 +00:00

fetch and merge

This commit is contained in:
martin 2022-02-04 15:34:00 +01:00
commit 47798febed
208 changed files with 5335 additions and 1780 deletions

1
.github/FUNDING.yml vendored
View File

@ -1 +1,2 @@
liberapay: TeamNewPipe
custom: 'https://newpipe.net/donate/'

View File

@ -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
@ -74,6 +75,13 @@ jobs:
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
steps:

4
.gitignore vendored
View File

@ -8,8 +8,8 @@ captures/
*~
.weblate
*.class
**/debug/
**/release/
app/debug/
app/release/
# vscode / eclipse files
*.classpath

View File

@ -17,7 +17,7 @@
<p align="center"><a href="https://newpipe.net">Website</a> &bull; <a href="https://newpipe.net/blog/">Blog</a> &bull; <a href="https://newpipe.net/FAQ/">FAQ</a> &bull; <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>

View File

@ -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() {

View File

@ -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"),
)
}
}

View File

@ -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()

View File

@ -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();
}
}

View File

@ -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;
});
}
}

View File

@ -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;
}
}

View File

@ -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:
SaveUploaderUrlHelper.saveUploaderUrlIfNeeded(context, item,
// 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());
uploaderUrl -> NavigationHelper.openChannelFragmentUsingIntent(
context, item.getServiceId(), uploaderUrl, item.getUploader()
));
return true;
case R.id.menu_item_share:
shareText(context, item.getTitle(), item.getUrl(),

View File

@ -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

View File

@ -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());
}
}

View File

@ -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"
}
}

View File

@ -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,14 +2216,22 @@ public final class VideoDetailFragment
mainFragment.setDescendantFocusability(afterDescendants);
toolbar.setDescendantFocusability(afterDescendants);
((ViewGroup) requireView()).setDescendantFocusability(blockDescendants);
// 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);
// Only focus the player if it not already has focus
if (!binding.getRoot().hasFocus()) {
binding.detailThumbnailRootLayout.requestFocus();
}
}
}
/**
* When the mini player exists the view underneath it is not touchable.

View File

@ -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();

View File

@ -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) {

View File

@ -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);
}
}

View File

@ -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)

View File

@ -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

View File

@ -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,6 +2177,9 @@ public final class Player implements
stopProgressLoop();
}
// 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);
@ -2189,7 +2191,7 @@ public final class Player implements
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;

View File

@ -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
// ///////////////////////////////////////////////////////////////////

View File

@ -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);
}

View File

@ -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() {

View File

@ -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) {

View File

@ -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) {

View File

@ -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);

View File

@ -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();
}
}

View File

@ -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);

View File

@ -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

View File

@ -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();
}
}

View File

@ -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))

View File

@ -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<>();

View File

@ -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
}

View File

@ -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());
}
}
}

View File

@ -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);

View File

@ -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();

View File

@ -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;
}
}
}

View File

@ -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());
}
}

View File

@ -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;
}
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -0,0 +1,7 @@
package org.schabi.newpipe.settings.preferencesearch;
import androidx.annotation.NonNull;
public interface PreferenceSearchResultListener {
void onSearchResultClicked(@NonNull PreferenceSearchItem result);
}

View File

@ -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();
}
}

View File

@ -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;

View File

@ -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);
});
}
/*//////////////////////////////////////////////////////////////////////////

View 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();
}
}

View File

@ -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();
}
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();
}
final Intent intent = getPlayerIntent(context, MainPlayer.class, queue, resumePlayback);
intent.putExtra(Player.PLAYER_TYPE, MainPlayer.PlayerType.AUDIO.ordinal());
ContextCompat.startForegroundService(context, intent);

View File

@ -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);
}
}

View File

@ -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));
}
}
}

View File

@ -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)
}
}

View File

@ -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
}
}

View 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
})
}
}
}

View 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>

View 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>

View 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>

View File

@ -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"

View File

@ -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>

View File

@ -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"

View File

@ -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"

View File

@ -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" />

View File

@ -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"

View File

@ -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" />

View File

@ -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" />

View File

@ -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" />

View File

@ -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" />

View File

@ -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"

View File

@ -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"

View File

@ -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" />

View File

@ -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" />

View File

@ -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" />

View File

@ -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

View File

@ -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"

View File

@ -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>

View 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>

View 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>

View File

@ -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" />

View File

@ -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>

View File

@ -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>

View File

@ -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>

View 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

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View 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