Merge branch 'dev' into refactor
This commit is contained in:
commit
394a7f68cd
@ -6,40 +6,13 @@
|
||||
root = true
|
||||
|
||||
[*.{kt,kts}]
|
||||
ktlint_code_style = android_studio
|
||||
# https://pinterest.github.io/ktlint/latest/rules/standard/#function-naming
|
||||
ktlint_function_naming_ignore_when_annotated_with = Composable
|
||||
ktlint_standard_annotation = disabled
|
||||
ktlint_standard_argument-list-wrapping = disabled
|
||||
ktlint_standard_backing-property-naming = disabled
|
||||
ktlint_standard_blank-line-before-declaration = disabled
|
||||
ktlint_standard_blank-line-between-when-conditions = disabled
|
||||
ktlint_standard_chain-method-continuation = disabled
|
||||
|
||||
ktlint_standard_class-signature = disabled
|
||||
ktlint_standard_comment-wrapping = disabled
|
||||
ktlint_standard_enum-wrapping = disabled
|
||||
ktlint_standard_function-expression-body = disabled
|
||||
ktlint_standard_function-literal = disabled
|
||||
ktlint_standard_function-signature = disabled
|
||||
ktlint_standard_indent = disabled
|
||||
ktlint_standard_kdoc = disabled
|
||||
ktlint_standard_max-line-length = disabled
|
||||
ktlint_standard_mixed-condition-operators = disabled
|
||||
ktlint_standard_multiline-expression-wrapping = disabled
|
||||
ktlint_standard_multiline-if-else = disabled
|
||||
ktlint_standard_no-blank-line-in-list = disabled
|
||||
ktlint_standard_no-consecutive-comments = disabled
|
||||
ktlint_standard_no-empty-first-line-in-class-body = disabled
|
||||
ktlint_standard_no-empty-first-line-in-method-block = disabled
|
||||
ktlint_standard_no-line-break-after-else = disabled
|
||||
ktlint_standard_no-semi = disabled
|
||||
ktlint_standard_no-single-line-block-comment = disabled
|
||||
ktlint_standard_package-name = disabled
|
||||
ktlint_standard_parameter-list-wrapping = disabled
|
||||
ktlint_standard_property-naming = disabled
|
||||
ktlint_standard_spacing-between-declarations-with-annotations = disabled
|
||||
ktlint_standard_spacing-between-declarations-with-comments = disabled
|
||||
ktlint_standard_statement-wrapping = disabled
|
||||
ktlint_standard_string-template-indent = disabled
|
||||
ktlint_standard_trailing-comma-on-call-site = disabled
|
||||
ktlint_standard_trailing-comma-on-declaration-site = disabled
|
||||
ktlint_standard_try-catch-finally-spacing = disabled
|
||||
ktlint_standard_when-entry-bracing = disabled
|
||||
|
||||
4
.github/workflows/backport-pr.yml
vendored
4
.github/workflows/backport-pr.yml
vendored
@ -25,9 +25,11 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Get backport metadata
|
||||
# the target branch is the first argument after `/backport`
|
||||
env:
|
||||
COMMENT_BODY: ${{ github.event.comment.body }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
body="${{ github.event.comment.body }}"
|
||||
body="$COMMENT_BODY"
|
||||
|
||||
line=${body%%$'\n'*} # Get the first line
|
||||
if [[ $line =~ ^/backport[[:space:]]+([^[:space:]]+) ]]; then
|
||||
|
||||
@ -47,9 +47,9 @@ android {
|
||||
minSdk = 23
|
||||
targetSdk = 35
|
||||
|
||||
versionCode = System.getProperty("versionCodeOverride")?.toInt() ?: 1005
|
||||
versionCode = System.getProperty("versionCodeOverride")?.toInt() ?: 1006
|
||||
|
||||
versionName = "0.28.0"
|
||||
versionName = "0.28.1"
|
||||
System.getProperty("versionNameSuffix")?.let { versionNameSuffix = it }
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
@ -140,6 +140,13 @@ ksp {
|
||||
// Custom dependency configuration for ktlint
|
||||
val ktlint by configurations.creating
|
||||
|
||||
// https://checkstyle.org/#JRE_and_JDK
|
||||
tasks.withType<Checkstyle>().configureEach {
|
||||
javaLauncher = javaToolchains.launcherFor {
|
||||
languageVersion = JavaLanguageVersion.of(21)
|
||||
}
|
||||
}
|
||||
|
||||
checkstyle {
|
||||
configDirectory = rootProject.file("checkstyle")
|
||||
isIgnoreFailures = false
|
||||
|
||||
5
app/proguard-rules.pro
vendored
5
app/proguard-rules.pro
vendored
@ -16,6 +16,11 @@
|
||||
-dontwarn javax.script.**
|
||||
-keep class jdk.dynalink.** { *; }
|
||||
-dontwarn jdk.dynalink.**
|
||||
# Rules for jsoup
|
||||
# Ignore intended-to-be-optional re2j classes - only needed if using re2j for jsoup regex
|
||||
# jsoup safely falls back to JDK regex if re2j not on classpath, but has concrete re2j refs
|
||||
# See https://github.com/jhy/jsoup/issues/2459 - may be resolved in future, then this may be removed
|
||||
-dontwarn com.google.re2j.**
|
||||
|
||||
## Rules for ExoPlayer
|
||||
-keep class com.google.android.exoplayer2.** { *; }
|
||||
|
||||
@ -176,28 +176,32 @@ class DatabaseMigrationTest {
|
||||
|
||||
databaseInV7.run {
|
||||
insert(
|
||||
"search_history", SQLiteDatabase.CONFLICT_FAIL,
|
||||
"search_history",
|
||||
SQLiteDatabase.CONFLICT_FAIL,
|
||||
ContentValues().apply {
|
||||
put("service_id", serviceId)
|
||||
put("search", defaultSearch1)
|
||||
}
|
||||
)
|
||||
insert(
|
||||
"search_history", SQLiteDatabase.CONFLICT_FAIL,
|
||||
"search_history",
|
||||
SQLiteDatabase.CONFLICT_FAIL,
|
||||
ContentValues().apply {
|
||||
put("service_id", serviceId)
|
||||
put("search", defaultSearch2)
|
||||
}
|
||||
)
|
||||
insert(
|
||||
"search_history", SQLiteDatabase.CONFLICT_FAIL,
|
||||
"search_history",
|
||||
SQLiteDatabase.CONFLICT_FAIL,
|
||||
ContentValues().apply {
|
||||
put("service_id", otherServiceId)
|
||||
put("search", defaultSearch1)
|
||||
}
|
||||
)
|
||||
insert(
|
||||
"search_history", SQLiteDatabase.CONFLICT_FAIL,
|
||||
"search_history",
|
||||
SQLiteDatabase.CONFLICT_FAIL,
|
||||
ContentValues().apply {
|
||||
put("service_id", otherServiceId)
|
||||
put("search", defaultSearch2)
|
||||
@ -207,13 +211,17 @@ class DatabaseMigrationTest {
|
||||
}
|
||||
|
||||
testHelper.runMigrationsAndValidate(
|
||||
AppDatabase.DATABASE_NAME, Migrations.DB_VER_8,
|
||||
true, Migrations.MIGRATION_7_8
|
||||
AppDatabase.DATABASE_NAME,
|
||||
Migrations.DB_VER_8,
|
||||
true,
|
||||
Migrations.MIGRATION_7_8
|
||||
)
|
||||
|
||||
testHelper.runMigrationsAndValidate(
|
||||
AppDatabase.DATABASE_NAME, Migrations.DB_VER_9,
|
||||
true, Migrations.MIGRATION_8_9
|
||||
AppDatabase.DATABASE_NAME,
|
||||
Migrations.DB_VER_9,
|
||||
true,
|
||||
Migrations.MIGRATION_8_9
|
||||
)
|
||||
|
||||
val migratedDatabaseV8 = getMigratedDatabase()
|
||||
@ -235,7 +243,8 @@ class DatabaseMigrationTest {
|
||||
val remoteUid2: Long
|
||||
databaseInV8.run {
|
||||
localUid1 = insert(
|
||||
"playlists", SQLiteDatabase.CONFLICT_FAIL,
|
||||
"playlists",
|
||||
SQLiteDatabase.CONFLICT_FAIL,
|
||||
ContentValues().apply {
|
||||
put("name", DEFAULT_NAME + "1")
|
||||
put("is_thumbnail_permanent", false)
|
||||
@ -243,7 +252,8 @@ class DatabaseMigrationTest {
|
||||
}
|
||||
)
|
||||
localUid2 = insert(
|
||||
"playlists", SQLiteDatabase.CONFLICT_FAIL,
|
||||
"playlists",
|
||||
SQLiteDatabase.CONFLICT_FAIL,
|
||||
ContentValues().apply {
|
||||
put("name", DEFAULT_NAME + "2")
|
||||
put("is_thumbnail_permanent", false)
|
||||
@ -251,25 +261,29 @@ class DatabaseMigrationTest {
|
||||
}
|
||||
)
|
||||
delete(
|
||||
"playlists", "uid = ?",
|
||||
"playlists",
|
||||
"uid = ?",
|
||||
Array(1) { localUid1 }
|
||||
)
|
||||
remoteUid1 = insert(
|
||||
"remote_playlists", SQLiteDatabase.CONFLICT_FAIL,
|
||||
"remote_playlists",
|
||||
SQLiteDatabase.CONFLICT_FAIL,
|
||||
ContentValues().apply {
|
||||
put("service_id", DEFAULT_SERVICE_ID)
|
||||
put("url", DEFAULT_URL)
|
||||
}
|
||||
)
|
||||
remoteUid2 = insert(
|
||||
"remote_playlists", SQLiteDatabase.CONFLICT_FAIL,
|
||||
"remote_playlists",
|
||||
SQLiteDatabase.CONFLICT_FAIL,
|
||||
ContentValues().apply {
|
||||
put("service_id", DEFAULT_SECOND_SERVICE_ID)
|
||||
put("url", DEFAULT_SECOND_URL)
|
||||
}
|
||||
)
|
||||
delete(
|
||||
"remote_playlists", "uid = ?",
|
||||
"remote_playlists",
|
||||
"uid = ?",
|
||||
Array(1) { remoteUid2 }
|
||||
)
|
||||
close()
|
||||
|
||||
@ -4,6 +4,8 @@ import android.content.Context
|
||||
import androidx.room.Room
|
||||
import androidx.test.core.app.ApplicationProvider
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import java.io.IOException
|
||||
import java.time.OffsetDateTime
|
||||
import org.junit.After
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertNotNull
|
||||
@ -20,9 +22,6 @@ import org.schabi.newpipe.database.subscription.SubscriptionEntity
|
||||
import org.schabi.newpipe.extractor.ServiceList
|
||||
import org.schabi.newpipe.extractor.channel.ChannelInfo
|
||||
import org.schabi.newpipe.extractor.stream.StreamType
|
||||
import java.io.IOException
|
||||
import java.time.OffsetDateTime
|
||||
import kotlin.streams.toList
|
||||
|
||||
class FeedDAOTest {
|
||||
private lateinit var db: AppDatabase
|
||||
@ -41,14 +40,21 @@ class FeedDAOTest {
|
||||
private val stream7 = StreamEntity(7, serviceId, "https://youtube.com/watch?v=7", "stream 7", StreamType.VIDEO_STREAM, 1000, "channel-4", "https://youtube.com/channel/4", "https://i.ytimg.com/vi/1/hqdefault.jpg", 100, "2023-08-10", OffsetDateTime.parse("2023-08-10T00:00:00Z"))
|
||||
|
||||
private val allStreams = listOf(
|
||||
stream1, stream2, stream3, stream4, stream5, stream6, stream7
|
||||
stream1,
|
||||
stream2,
|
||||
stream3,
|
||||
stream4,
|
||||
stream5,
|
||||
stream6,
|
||||
stream7
|
||||
)
|
||||
|
||||
@Before
|
||||
fun createDb() {
|
||||
val context = ApplicationProvider.getApplicationContext<Context>()
|
||||
db = Room.inMemoryDatabaseBuilder(
|
||||
context, AppDatabase::class.java
|
||||
context,
|
||||
AppDatabase::class.java
|
||||
).build()
|
||||
feedDAO = db.feedDAO()
|
||||
streamDAO = db.streamDAO()
|
||||
@ -65,7 +71,10 @@ class FeedDAOTest {
|
||||
fun testUnlinkStreamsOlderThan_KeepOne() {
|
||||
setupUnlinkDelete("2023-08-15T00:00:00Z")
|
||||
val streams = feedDAO.getStreams(
|
||||
FeedGroupEntity.GROUP_ALL_ID, includePlayed = true, includePartiallyPlayed = true, null
|
||||
FeedGroupEntity.GROUP_ALL_ID,
|
||||
includePlayed = true,
|
||||
includePartiallyPlayed = true,
|
||||
null
|
||||
)
|
||||
.blockingGet()
|
||||
val allowedStreams = listOf(stream3, stream5, stream6, stream7)
|
||||
@ -76,7 +85,10 @@ class FeedDAOTest {
|
||||
fun testUnlinkStreamsOlderThan_KeepMultiple() {
|
||||
setupUnlinkDelete("2023-08-01T00:00:00Z")
|
||||
val streams = feedDAO.getStreams(
|
||||
FeedGroupEntity.GROUP_ALL_ID, includePlayed = true, includePartiallyPlayed = true, null
|
||||
FeedGroupEntity.GROUP_ALL_ID,
|
||||
includePlayed = true,
|
||||
includePartiallyPlayed = true,
|
||||
null
|
||||
)
|
||||
.blockingGet()
|
||||
val allowedStreams = listOf(stream3, stream4, stream5, stream6, stream7)
|
||||
@ -112,7 +124,7 @@ class FeedDAOTest {
|
||||
SubscriptionEntity.from(ChannelInfo(serviceId, "1", "https://youtube.com/channel/1", "https://youtube.com/channel/1", "channel-1")),
|
||||
SubscriptionEntity.from(ChannelInfo(serviceId, "2", "https://youtube.com/channel/2", "https://youtube.com/channel/2", "channel-2")),
|
||||
SubscriptionEntity.from(ChannelInfo(serviceId, "3", "https://youtube.com/channel/3", "https://youtube.com/channel/3", "channel-3")),
|
||||
SubscriptionEntity.from(ChannelInfo(serviceId, "4", "https://youtube.com/channel/4", "https://youtube.com/channel/4", "channel-4")),
|
||||
SubscriptionEntity.from(ChannelInfo(serviceId, "4", "https://youtube.com/channel/4", "https://youtube.com/channel/4", "channel-4"))
|
||||
)
|
||||
)
|
||||
feedDAO.insertAll(
|
||||
@ -123,7 +135,7 @@ class FeedDAOTest {
|
||||
FeedEntity(4, 2),
|
||||
FeedEntity(5, 2),
|
||||
FeedEntity(6, 3),
|
||||
FeedEntity(7, 4),
|
||||
FeedEntity(7, 4)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@ -1,6 +1,9 @@
|
||||
package org.schabi.newpipe.local.history
|
||||
|
||||
import androidx.test.core.app.ApplicationProvider
|
||||
import java.time.LocalDateTime
|
||||
import java.time.OffsetDateTime
|
||||
import java.time.ZoneOffset
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.junit.After
|
||||
import org.junit.Assert.assertEquals
|
||||
@ -11,9 +14,6 @@ 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.time.ZoneOffset
|
||||
|
||||
class HistoryRecordManagerTest {
|
||||
|
||||
@ -54,7 +54,7 @@ class HistoryRecordManagerTest {
|
||||
SearchHistoryEntry(creationDate = time.minusSeconds(1), serviceId = 0, search = "A"),
|
||||
SearchHistoryEntry(creationDate = time.minusSeconds(2), serviceId = 2, search = "A"),
|
||||
SearchHistoryEntry(creationDate = time.minusSeconds(3), serviceId = 1, search = "B"),
|
||||
SearchHistoryEntry(creationDate = time.minusSeconds(4), serviceId = 0, search = "B"),
|
||||
SearchHistoryEntry(creationDate = time.minusSeconds(4), serviceId = 0, search = "B")
|
||||
)
|
||||
|
||||
// make sure all 4 were inserted
|
||||
@ -85,7 +85,7 @@ class HistoryRecordManagerTest {
|
||||
val entries = listOf(
|
||||
SearchHistoryEntry(creationDate = time.minusSeconds(1), serviceId = 1, search = "A"),
|
||||
SearchHistoryEntry(creationDate = time.minusSeconds(2), serviceId = 2, search = "B"),
|
||||
SearchHistoryEntry(creationDate = time.minusSeconds(3), serviceId = 0, search = "C"),
|
||||
SearchHistoryEntry(creationDate = time.minusSeconds(3), serviceId = 0, search = "C")
|
||||
)
|
||||
|
||||
// make sure all 3 were inserted
|
||||
@ -98,7 +98,6 @@ class HistoryRecordManagerTest {
|
||||
}
|
||||
|
||||
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
|
||||
@ -121,7 +120,7 @@ class HistoryRecordManagerTest {
|
||||
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
|
||||
RELATED_SEARCHES_ENTRIES[2].search // BA
|
||||
)
|
||||
}
|
||||
|
||||
@ -136,7 +135,7 @@ class HistoryRecordManagerTest {
|
||||
SearchHistoryEntry(creationDate = time.minusSeconds(4), serviceId = 3, search = "A"),
|
||||
SearchHistoryEntry(creationDate = time.minusSeconds(3), serviceId = 3, search = "A"),
|
||||
SearchHistoryEntry(creationDate = time.minusSeconds(2), serviceId = 0, search = "A"),
|
||||
SearchHistoryEntry(creationDate = time.minusSeconds(1), serviceId = 2, search = "AA"),
|
||||
SearchHistoryEntry(creationDate = time.minusSeconds(1), serviceId = 2, search = "AA")
|
||||
)
|
||||
insertShuffledRelatedSearches(relatedSearches)
|
||||
|
||||
@ -153,7 +152,7 @@ class HistoryRecordManagerTest {
|
||||
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
|
||||
RELATED_SEARCHES_ENTRIES[1].search // BA
|
||||
)
|
||||
|
||||
// also make sure that the string comparison is case insensitive
|
||||
@ -171,7 +170,7 @@ class HistoryRecordManagerTest {
|
||||
SearchHistoryEntry(creationDate = time.minusSeconds(4), serviceId = 3, search = "A"),
|
||||
SearchHistoryEntry(creationDate = time.minusSeconds(2), serviceId = 0, search = "B"),
|
||||
SearchHistoryEntry(creationDate = time.minusSeconds(3), serviceId = 2, search = "AA"),
|
||||
SearchHistoryEntry(creationDate = time.minusSeconds(1), serviceId = 1, search = "A"),
|
||||
SearchHistoryEntry(creationDate = time.minusSeconds(1), serviceId = 1, search = "A")
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -33,8 +33,12 @@ class LocalPlaylistManagerTest {
|
||||
fun createPlaylist() {
|
||||
val NEWPIPE_URL = "https://newpipe.net/"
|
||||
val stream = StreamEntity(
|
||||
serviceId = 1, url = NEWPIPE_URL, title = "title",
|
||||
streamType = StreamType.VIDEO_STREAM, duration = 1, uploader = "uploader",
|
||||
serviceId = 1,
|
||||
url = NEWPIPE_URL,
|
||||
title = "title",
|
||||
streamType = StreamType.VIDEO_STREAM,
|
||||
duration = 1,
|
||||
uploader = "uploader",
|
||||
uploaderUrl = NEWPIPE_URL
|
||||
)
|
||||
|
||||
@ -58,14 +62,22 @@ class LocalPlaylistManagerTest {
|
||||
@Test()
|
||||
fun createPlaylist_nonExistentStreamsAreUpserted() {
|
||||
val stream = StreamEntity(
|
||||
serviceId = 1, url = "https://newpipe.net/", title = "title",
|
||||
streamType = StreamType.VIDEO_STREAM, duration = 1, uploader = "uploader",
|
||||
serviceId = 1,
|
||||
url = "https://newpipe.net/",
|
||||
title = "title",
|
||||
streamType = StreamType.VIDEO_STREAM,
|
||||
duration = 1,
|
||||
uploader = "uploader",
|
||||
uploaderUrl = "https://newpipe.net/"
|
||||
)
|
||||
database.streamDAO().insert(stream)
|
||||
val upserted = StreamEntity(
|
||||
serviceId = 1, url = "https://newpipe.net/2", title = "title2",
|
||||
streamType = StreamType.VIDEO_STREAM, duration = 1, uploader = "uploader",
|
||||
serviceId = 1,
|
||||
url = "https://newpipe.net/2",
|
||||
title = "title2",
|
||||
streamType = StreamType.VIDEO_STREAM,
|
||||
duration = 1,
|
||||
uploader = "uploader",
|
||||
uploaderUrl = "https://newpipe.net/"
|
||||
)
|
||||
|
||||
|
||||
@ -17,21 +17,20 @@ class TrampolineSchedulerRule : TestRule {
|
||||
|
||||
private val scheduler = Schedulers.trampoline()
|
||||
|
||||
override fun apply(base: Statement, description: Description): Statement =
|
||||
object : Statement() {
|
||||
override fun evaluate() {
|
||||
try {
|
||||
RxJavaPlugins.setComputationSchedulerHandler { scheduler }
|
||||
RxJavaPlugins.setIoSchedulerHandler { scheduler }
|
||||
RxJavaPlugins.setNewThreadSchedulerHandler { scheduler }
|
||||
RxJavaPlugins.setSingleSchedulerHandler { scheduler }
|
||||
RxAndroidPlugins.setInitMainThreadSchedulerHandler { scheduler }
|
||||
override fun apply(base: Statement, description: Description): Statement = object : Statement() {
|
||||
override fun evaluate() {
|
||||
try {
|
||||
RxJavaPlugins.setComputationSchedulerHandler { scheduler }
|
||||
RxJavaPlugins.setIoSchedulerHandler { scheduler }
|
||||
RxJavaPlugins.setNewThreadSchedulerHandler { scheduler }
|
||||
RxJavaPlugins.setSingleSchedulerHandler { scheduler }
|
||||
RxAndroidPlugins.setInitMainThreadSchedulerHandler { scheduler }
|
||||
|
||||
base.evaluate()
|
||||
} finally {
|
||||
RxJavaPlugins.reset()
|
||||
RxAndroidPlugins.reset()
|
||||
}
|
||||
base.evaluate()
|
||||
} finally {
|
||||
RxJavaPlugins.reset()
|
||||
RxAndroidPlugins.reset()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -156,41 +156,51 @@ class StreamItemAdapterTest {
|
||||
|
||||
helper.assertInvalidResponse(getResponse(mapOf(Pair("content-length", "mp3"))), 0)
|
||||
helper.assertInvalidResponse(
|
||||
getResponse(mapOf(Pair("Content-Disposition", "filename=\"train.png\""))), 1
|
||||
getResponse(mapOf(Pair("Content-Disposition", "filename=\"train.png\""))),
|
||||
1
|
||||
)
|
||||
helper.assertInvalidResponse(
|
||||
getResponse(mapOf(Pair("Content-Disposition", "form-data; name=\"data.csv\""))), 2
|
||||
getResponse(mapOf(Pair("Content-Disposition", "form-data; name=\"data.csv\""))),
|
||||
2
|
||||
)
|
||||
helper.assertInvalidResponse(
|
||||
getResponse(mapOf(Pair("Content-Disposition", "form-data; filename=\"data.csv\""))), 3
|
||||
getResponse(mapOf(Pair("Content-Disposition", "form-data; filename=\"data.csv\""))),
|
||||
3
|
||||
)
|
||||
helper.assertInvalidResponse(
|
||||
getResponse(mapOf(Pair("Content-Disposition", "form-data; name=\"fieldName\"; filename*=\"filename.jpg\""))), 4
|
||||
getResponse(mapOf(Pair("Content-Disposition", "form-data; name=\"fieldName\"; filename*=\"filename.jpg\""))),
|
||||
4
|
||||
)
|
||||
|
||||
helper.assertValidResponse(
|
||||
getResponse(mapOf(Pair("Content-Disposition", "filename=\"train.ogg\""))),
|
||||
5, MediaFormat.OGG
|
||||
5,
|
||||
MediaFormat.OGG
|
||||
)
|
||||
helper.assertValidResponse(
|
||||
getResponse(mapOf(Pair("Content-Disposition", "some-form-data; filename=\"audio.flac\""))),
|
||||
6, MediaFormat.FLAC
|
||||
6,
|
||||
MediaFormat.FLAC
|
||||
)
|
||||
helper.assertValidResponse(
|
||||
getResponse(mapOf(Pair("Content-Disposition", "form-data; name=\"audio.aiff\"; filename=\"audio.aiff\""))),
|
||||
7, MediaFormat.AIFF
|
||||
7,
|
||||
MediaFormat.AIFF
|
||||
)
|
||||
helper.assertValidResponse(
|
||||
getResponse(mapOf(Pair("Content-Disposition", "form-data; name=\"alien?\"; filename*=UTF-8''%CE%B1%CE%BB%CE%B9%CF%B5%CE%BD.m4a"))),
|
||||
8, MediaFormat.M4A
|
||||
8,
|
||||
MediaFormat.M4A
|
||||
)
|
||||
helper.assertValidResponse(
|
||||
getResponse(mapOf(Pair("Content-Disposition", "form-data; name=\"audio.mp3\"; filename=\"audio.opus\"; filename*=UTF-8''alien.opus"))),
|
||||
9, MediaFormat.OPUS
|
||||
9,
|
||||
MediaFormat.OPUS
|
||||
)
|
||||
helper.assertValidResponse(
|
||||
getResponse(mapOf(Pair("Content-Disposition", "form-data; name=\"audio.mp3\"; filename=\"audio.opus\"; filename*=\"UTF-8''alien.opus\""))),
|
||||
10, MediaFormat.OPUS
|
||||
10,
|
||||
MediaFormat.OPUS
|
||||
)
|
||||
}
|
||||
|
||||
@ -213,16 +223,24 @@ class StreamItemAdapterTest {
|
||||
helper.assertInvalidResponse(getResponse(mapOf()), 7)
|
||||
|
||||
helper.assertValidResponse(
|
||||
getResponse(mapOf(Pair("Content-Type", "audio/flac"))), 8, MediaFormat.FLAC
|
||||
getResponse(mapOf(Pair("Content-Type", "audio/flac"))),
|
||||
8,
|
||||
MediaFormat.FLAC
|
||||
)
|
||||
helper.assertValidResponse(
|
||||
getResponse(mapOf(Pair("Content-Type", "audio/wav"))), 9, MediaFormat.WAV
|
||||
getResponse(mapOf(Pair("Content-Type", "audio/wav"))),
|
||||
9,
|
||||
MediaFormat.WAV
|
||||
)
|
||||
helper.assertValidResponse(
|
||||
getResponse(mapOf(Pair("Content-Type", "audio/opus"))), 10, MediaFormat.OPUS
|
||||
getResponse(mapOf(Pair("Content-Type", "audio/opus"))),
|
||||
10,
|
||||
MediaFormat.OPUS
|
||||
)
|
||||
helper.assertValidResponse(
|
||||
getResponse(mapOf(Pair("Content-Type", "audio/aiff"))), 11, MediaFormat.AIFF
|
||||
getResponse(mapOf(Pair("Content-Type", "audio/aiff"))),
|
||||
11,
|
||||
MediaFormat.AIFF
|
||||
)
|
||||
}
|
||||
|
||||
@ -230,39 +248,37 @@ class StreamItemAdapterTest {
|
||||
* @return a list of video streams, in which their video only property mirrors the provided
|
||||
* [videoOnly] vararg.
|
||||
*/
|
||||
private fun getVideoStreams(vararg videoOnly: Boolean) =
|
||||
StreamItemAdapter.StreamInfoWrapper(
|
||||
videoOnly.map {
|
||||
VideoStream.Builder()
|
||||
.setId(Stream.ID_UNKNOWN)
|
||||
.setContent("https://example.com", true)
|
||||
.setMediaFormat(MediaFormat.MPEG_4)
|
||||
.setResolution("720p")
|
||||
.setIsVideoOnly(it)
|
||||
.build()
|
||||
},
|
||||
context
|
||||
)
|
||||
private fun getVideoStreams(vararg videoOnly: Boolean) = StreamInfoWrapper(
|
||||
videoOnly.map {
|
||||
VideoStream.Builder()
|
||||
.setId(Stream.ID_UNKNOWN)
|
||||
.setContent("https://example.com", true)
|
||||
.setMediaFormat(MediaFormat.MPEG_4)
|
||||
.setResolution("720p")
|
||||
.setIsVideoOnly(it)
|
||||
.build()
|
||||
},
|
||||
context
|
||||
)
|
||||
|
||||
/**
|
||||
* @return a list of audio streams, containing valid and null elements mirroring the provided
|
||||
* [shouldBeValid] vararg.
|
||||
*/
|
||||
private fun getAudioStreams(vararg shouldBeValid: Boolean) =
|
||||
getSecondaryStreamsFromList(
|
||||
shouldBeValid.map {
|
||||
if (it) {
|
||||
AudioStream.Builder()
|
||||
.setId(Stream.ID_UNKNOWN)
|
||||
.setContent("https://example.com", true)
|
||||
.setMediaFormat(MediaFormat.OPUS)
|
||||
.setAverageBitrate(192)
|
||||
.build()
|
||||
} else {
|
||||
null
|
||||
}
|
||||
private fun getAudioStreams(vararg shouldBeValid: Boolean) = getSecondaryStreamsFromList(
|
||||
shouldBeValid.map {
|
||||
if (it) {
|
||||
AudioStream.Builder()
|
||||
.setId(Stream.ID_UNKNOWN)
|
||||
.setContent("https://example.com", true)
|
||||
.setMediaFormat(MediaFormat.OPUS)
|
||||
.setAverageBitrate(192)
|
||||
.build()
|
||||
} else {
|
||||
null
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
private fun getIncompleteAudioStreams(size: Int): List<AudioStream> {
|
||||
val list = ArrayList<AudioStream>(size)
|
||||
@ -292,7 +308,7 @@ class StreamItemAdapterTest {
|
||||
Assert.assertEquals(
|
||||
"normal visibility (pos=[$position]) is not correct",
|
||||
findViewById<View>(R.id.wo_sound_icon).visibility,
|
||||
normalVisibility,
|
||||
normalVisibility
|
||||
)
|
||||
}
|
||||
spinner.adapter.getDropDownView(position, null, spinner).run {
|
||||
@ -307,18 +323,17 @@ class StreamItemAdapterTest {
|
||||
/**
|
||||
* Helper function that builds a secondary stream list.
|
||||
*/
|
||||
private fun <T : Stream> getSecondaryStreamsFromList(streams: List<T?>) =
|
||||
SparseArrayCompat<SecondaryStreamHelper<T>?>(streams.size).apply {
|
||||
streams.forEachIndexed { index, stream ->
|
||||
val secondaryStreamHelper: SecondaryStreamHelper<T>? = stream?.let {
|
||||
SecondaryStreamHelper(
|
||||
StreamItemAdapter.StreamInfoWrapper(streams, context),
|
||||
it
|
||||
)
|
||||
}
|
||||
put(index, secondaryStreamHelper)
|
||||
private fun <T : Stream> getSecondaryStreamsFromList(streams: List<T?>) = SparseArrayCompat<SecondaryStreamHelper<T>?>(streams.size).apply {
|
||||
streams.forEachIndexed { index, stream ->
|
||||
val secondaryStreamHelper: SecondaryStreamHelper<T>? = stream?.let {
|
||||
SecondaryStreamHelper(
|
||||
StreamItemAdapter.StreamInfoWrapper(streams, context),
|
||||
it
|
||||
)
|
||||
}
|
||||
put(index, secondaryStreamHelper)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getResponse(headers: Map<String, String>): Response {
|
||||
val listHeaders = HashMap<String, List<String>>()
|
||||
@ -345,7 +360,8 @@ class StreamItemAdapterTest {
|
||||
index: Int
|
||||
) {
|
||||
assertFalse(
|
||||
"invalid header returns valid value", retrieveMediaFormat(streams[index], response)
|
||||
"invalid header returns valid value",
|
||||
retrieveMediaFormat(streams[index], response)
|
||||
)
|
||||
assertNull("Media format extracted although stated otherwise", wrapper.getFormat(index))
|
||||
}
|
||||
@ -359,7 +375,8 @@ class StreamItemAdapterTest {
|
||||
format: MediaFormat
|
||||
) {
|
||||
assertTrue(
|
||||
"header was not recognized", retrieveMediaFormat(streams[index], response)
|
||||
"header was not recognized",
|
||||
retrieveMediaFormat(streams[index], response)
|
||||
)
|
||||
assertEquals("Wrong media format extracted", format, wrapper.getFormat(index))
|
||||
}
|
||||
|
||||
@ -166,9 +166,7 @@ public final class DownloaderImpl extends Downloader {
|
||||
|
||||
String responseBodyToReturn = null;
|
||||
try (ResponseBody body = response.body()) {
|
||||
if (body != null) {
|
||||
responseBodyToReturn = body.string();
|
||||
}
|
||||
responseBodyToReturn = body.string();
|
||||
}
|
||||
|
||||
final String latestUrl = response.request().url().toString();
|
||||
|
||||
@ -8,6 +8,7 @@ package org.schabi.newpipe
|
||||
|
||||
import android.content.Context
|
||||
import androidx.room.Room.databaseBuilder
|
||||
import kotlin.concurrent.Volatile
|
||||
import org.schabi.newpipe.database.AppDatabase
|
||||
import org.schabi.newpipe.database.Migrations.MIGRATION_1_2
|
||||
import org.schabi.newpipe.database.Migrations.MIGRATION_2_3
|
||||
@ -17,7 +18,6 @@ import org.schabi.newpipe.database.Migrations.MIGRATION_5_6
|
||||
import org.schabi.newpipe.database.Migrations.MIGRATION_6_7
|
||||
import org.schabi.newpipe.database.Migrations.MIGRATION_7_8
|
||||
import org.schabi.newpipe.database.Migrations.MIGRATION_8_9
|
||||
import kotlin.concurrent.Volatile
|
||||
|
||||
object NewPipeDatabase {
|
||||
|
||||
|
||||
@ -18,10 +18,10 @@ import androidx.work.WorkerParameters
|
||||
import androidx.work.workDataOf
|
||||
import com.grack.nanojson.JsonParser
|
||||
import com.grack.nanojson.JsonParserException
|
||||
import java.io.IOException
|
||||
import org.schabi.newpipe.extractor.downloader.Response
|
||||
import org.schabi.newpipe.extractor.exceptions.ReCaptchaException
|
||||
import org.schabi.newpipe.util.ReleaseVersionUtil
|
||||
import java.io.IOException
|
||||
|
||||
class NewVersionWorker(
|
||||
context: Context,
|
||||
@ -46,7 +46,8 @@ class NewVersionWorker(
|
||||
// Show toast stating that the app is up-to-date if the update check was manual.
|
||||
ContextCompat.getMainExecutor(applicationContext).execute {
|
||||
Toast.makeText(
|
||||
applicationContext, R.string.app_update_unavailable_toast,
|
||||
applicationContext,
|
||||
R.string.app_update_unavailable_toast,
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
@ -58,7 +59,11 @@ class NewVersionWorker(
|
||||
val intent = Intent(Intent.ACTION_VIEW, apkLocationUrl?.toUri())
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
val pendingIntent = PendingIntentCompat.getActivity(
|
||||
applicationContext, 0, intent, 0, false
|
||||
applicationContext,
|
||||
0,
|
||||
intent,
|
||||
0,
|
||||
false
|
||||
)
|
||||
val channelId = applicationContext.getString(R.string.app_update_notification_channel_id)
|
||||
val notificationBuilder = NotificationCompat.Builder(applicationContext, channelId)
|
||||
@ -71,7 +76,8 @@ class NewVersionWorker(
|
||||
)
|
||||
.setContentText(
|
||||
applicationContext.getString(
|
||||
R.string.app_update_available_notification_text, versionName
|
||||
R.string.app_update_available_notification_text,
|
||||
versionName
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
package org.schabi.newpipe.database
|
||||
|
||||
import androidx.room.TypeConverter
|
||||
import org.schabi.newpipe.extractor.stream.StreamType
|
||||
import org.schabi.newpipe.local.subscription.FeedGroupIcon
|
||||
import java.time.Instant
|
||||
import java.time.OffsetDateTime
|
||||
import java.time.ZoneOffset
|
||||
import org.schabi.newpipe.extractor.stream.StreamType
|
||||
import org.schabi.newpipe.local.subscription.FeedGroupIcon
|
||||
|
||||
class Converters {
|
||||
/**
|
||||
|
||||
@ -14,6 +14,6 @@ interface LocalItem {
|
||||
PLAYLIST_REMOTE_ITEM,
|
||||
|
||||
PLAYLIST_STREAM_ITEM,
|
||||
STATISTIC_STREAM_ITEM,
|
||||
STATISTIC_STREAM_ITEM
|
||||
}
|
||||
}
|
||||
|
||||
@ -8,7 +8,6 @@ package org.schabi.newpipe.database
|
||||
|
||||
import android.util.Log
|
||||
import androidx.room.migration.Migration
|
||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||
import org.schabi.newpipe.MainActivity
|
||||
|
||||
object Migrations {
|
||||
|
||||
@ -8,6 +8,7 @@ import androidx.room.Transaction
|
||||
import androidx.room.Update
|
||||
import io.reactivex.rxjava3.core.Flowable
|
||||
import io.reactivex.rxjava3.core.Maybe
|
||||
import java.time.OffsetDateTime
|
||||
import org.schabi.newpipe.database.feed.model.FeedEntity
|
||||
import org.schabi.newpipe.database.feed.model.FeedGroupEntity
|
||||
import org.schabi.newpipe.database.feed.model.FeedLastUpdatedEntity
|
||||
@ -15,7 +16,6 @@ import org.schabi.newpipe.database.stream.StreamWithState
|
||||
import org.schabi.newpipe.database.stream.model.StreamStateEntity
|
||||
import org.schabi.newpipe.database.subscription.NotificationMode
|
||||
import org.schabi.newpipe.database.subscription.SubscriptionEntity
|
||||
import java.time.OffsetDateTime
|
||||
|
||||
@Dao
|
||||
abstract class FeedDAO {
|
||||
|
||||
@ -19,13 +19,17 @@ import org.schabi.newpipe.database.subscription.SubscriptionEntity
|
||||
entity = StreamEntity::class,
|
||||
parentColumns = [StreamEntity.STREAM_ID],
|
||||
childColumns = [STREAM_ID],
|
||||
onDelete = ForeignKey.CASCADE, onUpdate = ForeignKey.CASCADE, deferred = true
|
||||
onDelete = ForeignKey.CASCADE,
|
||||
onUpdate = ForeignKey.CASCADE,
|
||||
deferred = true
|
||||
),
|
||||
ForeignKey(
|
||||
entity = SubscriptionEntity::class,
|
||||
parentColumns = [SubscriptionEntity.SUBSCRIPTION_UID],
|
||||
childColumns = [SUBSCRIPTION_ID],
|
||||
onDelete = ForeignKey.CASCADE, onUpdate = ForeignKey.CASCADE, deferred = true
|
||||
onDelete = ForeignKey.CASCADE,
|
||||
onUpdate = ForeignKey.CASCADE,
|
||||
deferred = true
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
@ -18,14 +18,18 @@ import org.schabi.newpipe.database.subscription.SubscriptionEntity
|
||||
entity = FeedGroupEntity::class,
|
||||
parentColumns = [FeedGroupEntity.ID],
|
||||
childColumns = [GROUP_ID],
|
||||
onDelete = ForeignKey.CASCADE, onUpdate = ForeignKey.CASCADE, deferred = true
|
||||
onDelete = ForeignKey.CASCADE,
|
||||
onUpdate = ForeignKey.CASCADE,
|
||||
deferred = true
|
||||
),
|
||||
|
||||
ForeignKey(
|
||||
entity = SubscriptionEntity::class,
|
||||
parentColumns = [SubscriptionEntity.SUBSCRIPTION_UID],
|
||||
childColumns = [SUBSCRIPTION_ID],
|
||||
onDelete = ForeignKey.CASCADE, onUpdate = ForeignKey.CASCADE, deferred = true
|
||||
onDelete = ForeignKey.CASCADE,
|
||||
onUpdate = ForeignKey.CASCADE,
|
||||
deferred = true
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
@ -4,10 +4,10 @@ import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.ForeignKey
|
||||
import androidx.room.PrimaryKey
|
||||
import java.time.OffsetDateTime
|
||||
import org.schabi.newpipe.database.feed.model.FeedLastUpdatedEntity.Companion.FEED_LAST_UPDATED_TABLE
|
||||
import org.schabi.newpipe.database.feed.model.FeedLastUpdatedEntity.Companion.SUBSCRIPTION_ID
|
||||
import org.schabi.newpipe.database.subscription.SubscriptionEntity
|
||||
import java.time.OffsetDateTime
|
||||
|
||||
@Entity(
|
||||
tableName = FEED_LAST_UPDATED_TABLE,
|
||||
@ -16,7 +16,9 @@ import java.time.OffsetDateTime
|
||||
entity = SubscriptionEntity::class,
|
||||
parentColumns = [SubscriptionEntity.SUBSCRIPTION_UID],
|
||||
childColumns = [SUBSCRIPTION_ID],
|
||||
onDelete = ForeignKey.CASCADE, onUpdate = ForeignKey.CASCADE, deferred = true
|
||||
onDelete = ForeignKey.CASCADE,
|
||||
onUpdate = ForeignKey.CASCADE,
|
||||
deferred = true
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
@ -29,7 +29,7 @@ data class SearchHistoryEntry @JvmOverloads constructor(
|
||||
|
||||
@ColumnInfo(name = ID)
|
||||
@PrimaryKey(autoGenerate = true)
|
||||
val id: Long = 0,
|
||||
val id: Long = 0
|
||||
) {
|
||||
|
||||
@Ignore
|
||||
|
||||
@ -11,12 +11,12 @@ import androidx.room.Entity
|
||||
import androidx.room.ForeignKey
|
||||
import androidx.room.ForeignKey.Companion.CASCADE
|
||||
import androidx.room.Index
|
||||
import java.time.OffsetDateTime
|
||||
import org.schabi.newpipe.database.history.model.StreamHistoryEntity.Companion.JOIN_STREAM_ID
|
||||
import org.schabi.newpipe.database.history.model.StreamHistoryEntity.Companion.STREAM_ACCESS_DATE
|
||||
import org.schabi.newpipe.database.history.model.StreamHistoryEntity.Companion.STREAM_HISTORY_TABLE
|
||||
import org.schabi.newpipe.database.stream.model.StreamEntity
|
||||
import org.schabi.newpipe.database.stream.model.StreamEntity.Companion.STREAM_ID
|
||||
import java.time.OffsetDateTime
|
||||
|
||||
/**
|
||||
* @param streamUid the stream id this history item will refer to
|
||||
|
||||
@ -2,10 +2,10 @@ package org.schabi.newpipe.database.history.model
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Embedded
|
||||
import java.time.OffsetDateTime
|
||||
import org.schabi.newpipe.database.stream.model.StreamEntity
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem
|
||||
import org.schabi.newpipe.util.image.ImageStrategy
|
||||
import java.time.OffsetDateTime
|
||||
|
||||
data class StreamHistoryEntry(
|
||||
@Embedded
|
||||
@ -30,16 +30,15 @@ data class StreamHistoryEntry(
|
||||
accessDate.isEqual(other.accessDate)
|
||||
}
|
||||
|
||||
fun toStreamInfoItem(): StreamInfoItem =
|
||||
StreamInfoItem(
|
||||
streamEntity.serviceId,
|
||||
streamEntity.url,
|
||||
streamEntity.title,
|
||||
streamEntity.streamType,
|
||||
).apply {
|
||||
duration = streamEntity.duration
|
||||
uploaderName = streamEntity.uploader
|
||||
uploaderUrl = streamEntity.uploaderUrl
|
||||
thumbnails = ImageStrategy.dbUrlToImageList(streamEntity.thumbnailUrl)
|
||||
}
|
||||
fun toStreamInfoItem(): StreamInfoItem = StreamInfoItem(
|
||||
streamEntity.serviceId,
|
||||
streamEntity.url,
|
||||
streamEntity.title,
|
||||
streamEntity.streamType
|
||||
).apply {
|
||||
duration = streamEntity.duration
|
||||
uploaderName = streamEntity.uploader
|
||||
uploaderUrl = streamEntity.uploaderUrl
|
||||
thumbnails = ImageStrategy.dbUrlToImageList(streamEntity.thumbnailUrl)
|
||||
}
|
||||
}
|
||||
|
||||
@ -68,6 +68,11 @@ interface PlaylistStreamDAO : BasicDAO<PlaylistStreamEntity> {
|
||||
)
|
||||
fun getOrderedStreamsOf(playlistId: Long): Flowable<MutableList<PlaylistStreamEntry>>
|
||||
|
||||
// If a playlist has no streams, there won’t be any rows in the **playlist_stream_join** table
|
||||
// that have a foreign key to that playlist. Thus, the **playlist_id** will not have a
|
||||
// corresponding value in any rows of the join table. So, if you group by the **playlist_id**,
|
||||
// only playlists that contain videos are grouped and displayed. Look at #9642 #13055
|
||||
|
||||
@Transaction
|
||||
@Query(
|
||||
"""
|
||||
@ -103,6 +108,11 @@ interface PlaylistStreamDAO : BasicDAO<PlaylistStreamEntity> {
|
||||
)
|
||||
fun getStreamsWithoutDuplicates(playlistId: Long): Flowable<MutableList<PlaylistStreamEntry>>
|
||||
|
||||
// If a playlist has no streams, there won’t be any rows in the **playlist_stream_join** table
|
||||
// that have a foreign key to that playlist. Thus, the **playlist_id** will not have a
|
||||
// corresponding value in any rows of the join table. So, if you group by the **playlist_id**,
|
||||
// only playlists that contain videos are grouped and displayed. Look at #9642 #13055
|
||||
|
||||
@Transaction
|
||||
@Query(
|
||||
"""
|
||||
@ -118,7 +128,7 @@ interface PlaylistStreamDAO : BasicDAO<PlaylistStreamEntity> {
|
||||
LEFT JOIN streams
|
||||
ON streams.uid = stream_id AND :streamUrl = :streamUrl
|
||||
|
||||
GROUP BY playlist_id
|
||||
GROUP BY playlists.uid
|
||||
ORDER BY display_index, name
|
||||
"""
|
||||
)
|
||||
|
||||
@ -37,7 +37,7 @@ data class PlaylistEntity @JvmOverloads constructor(
|
||||
name = item.orderingName,
|
||||
isThumbnailPermanent = item.isThumbnailPermanent!!,
|
||||
thumbnailStreamId = item.thumbnailStreamId!!,
|
||||
displayIndex = item.displayIndex!!,
|
||||
displayIndex = item.displayIndex!!
|
||||
)
|
||||
|
||||
companion object {
|
||||
|
||||
@ -9,13 +9,13 @@ package org.schabi.newpipe.database.stream
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Embedded
|
||||
import androidx.room.Ignore
|
||||
import java.time.OffsetDateTime
|
||||
import org.schabi.newpipe.database.LocalItem
|
||||
import org.schabi.newpipe.database.history.model.StreamHistoryEntity
|
||||
import org.schabi.newpipe.database.stream.model.StreamEntity
|
||||
import org.schabi.newpipe.database.stream.model.StreamStateEntity.Companion.STREAM_PROGRESS_MILLIS
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem
|
||||
import org.schabi.newpipe.util.image.ImageStrategy
|
||||
import java.time.OffsetDateTime
|
||||
|
||||
data class StreamStatisticsEntry(
|
||||
@Embedded
|
||||
|
||||
@ -9,12 +9,12 @@ import androidx.room.Transaction
|
||||
import io.reactivex.rxjava3.core.Completable
|
||||
import io.reactivex.rxjava3.core.Flowable
|
||||
import io.reactivex.rxjava3.core.Maybe
|
||||
import java.time.OffsetDateTime
|
||||
import org.schabi.newpipe.database.BasicDAO
|
||||
import org.schabi.newpipe.database.stream.model.StreamEntity
|
||||
import org.schabi.newpipe.database.stream.model.StreamEntity.Companion.STREAM_ID
|
||||
import org.schabi.newpipe.extractor.stream.StreamType
|
||||
import org.schabi.newpipe.util.StreamTypeUtil
|
||||
import java.time.OffsetDateTime
|
||||
|
||||
@Dao
|
||||
abstract class StreamDAO : BasicDAO<StreamEntity> {
|
||||
@ -92,7 +92,6 @@ abstract class StreamDAO : BasicDAO<StreamEntity> {
|
||||
newerStream.uid = existentMinimalStream.uid
|
||||
|
||||
if (!StreamTypeUtil.isLiveStream(newerStream.streamType)) {
|
||||
|
||||
// Use the existent upload date if the newer stream does not have a better precision
|
||||
// (i.e. is an approximation). This is done to prevent unnecessary changes.
|
||||
val hasBetterPrecision =
|
||||
|
||||
@ -5,6 +5,8 @@ import androidx.room.Entity
|
||||
import androidx.room.Ignore
|
||||
import androidx.room.Index
|
||||
import androidx.room.PrimaryKey
|
||||
import java.io.Serializable
|
||||
import java.time.OffsetDateTime
|
||||
import org.schabi.newpipe.database.stream.model.StreamEntity.Companion.STREAM_SERVICE_ID
|
||||
import org.schabi.newpipe.database.stream.model.StreamEntity.Companion.STREAM_TABLE
|
||||
import org.schabi.newpipe.database.stream.model.StreamEntity.Companion.STREAM_URL
|
||||
@ -14,8 +16,6 @@ import org.schabi.newpipe.extractor.stream.StreamInfoItem
|
||||
import org.schabi.newpipe.extractor.stream.StreamType
|
||||
import org.schabi.newpipe.player.playqueue.PlayQueueItem
|
||||
import org.schabi.newpipe.util.image.ImageStrategy
|
||||
import java.io.Serializable
|
||||
import java.time.OffsetDateTime
|
||||
|
||||
@Entity(
|
||||
tableName = STREAM_TABLE,
|
||||
@ -86,8 +86,12 @@ data class StreamEntity(
|
||||
|
||||
@Ignore
|
||||
constructor(item: PlayQueueItem) : this(
|
||||
serviceId = item.serviceId, url = item.url, title = item.title,
|
||||
streamType = item.streamType, duration = item.duration, uploader = item.uploader,
|
||||
serviceId = item.serviceId,
|
||||
url = item.url,
|
||||
title = item.title,
|
||||
streamType = item.streamType,
|
||||
duration = item.duration,
|
||||
uploader = item.uploader,
|
||||
uploaderUrl = item.uploaderUrl,
|
||||
thumbnailUrl = ImageStrategy.imageListToDbUrl(item.thumbnails)
|
||||
)
|
||||
|
||||
@ -11,16 +11,16 @@ import androidx.fragment.app.Fragment
|
||||
import com.jakewharton.rxbinding4.view.clicks
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.disposables.Disposable
|
||||
import java.util.concurrent.TimeUnit
|
||||
import org.schabi.newpipe.MainActivity
|
||||
import org.schabi.newpipe.R
|
||||
import org.schabi.newpipe.ktx.animate
|
||||
import org.schabi.newpipe.util.external_communication.ShareUtils
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class ErrorPanelHelper(
|
||||
private val fragment: Fragment,
|
||||
rootView: View,
|
||||
onRetry: Runnable?,
|
||||
onRetry: Runnable?
|
||||
) {
|
||||
private val context: Context = rootView.context!!
|
||||
|
||||
|
||||
@ -46,7 +46,7 @@ class ErrorUtil {
|
||||
@JvmStatic
|
||||
fun openActivity(context: Context, errorInfo: ErrorInfo) {
|
||||
if (PreferenceManager.getDefaultSharedPreferences(context)
|
||||
.getBoolean(MainActivity.KEY_IS_IN_BACKGROUND, true)
|
||||
.getBoolean(MainActivity.KEY_IS_IN_BACKGROUND, true)
|
||||
) {
|
||||
createNotification(context, errorInfo)
|
||||
} else {
|
||||
|
||||
@ -40,5 +40,5 @@ enum class UserAction(val message: String) {
|
||||
OPEN_INFO_ITEM_DIALOG("open info item dialog"),
|
||||
GETTING_MAIN_SCREEN_TAB("getting main screen tab"),
|
||||
PLAY_ON_POPUP("play on popup"),
|
||||
SUBSCRIPTIONS("loading subscriptions");
|
||||
SUBSCRIPTIONS("loading subscriptions")
|
||||
}
|
||||
|
||||
@ -13,14 +13,17 @@ enum class ItemViewMode {
|
||||
* Default mode.
|
||||
*/
|
||||
AUTO,
|
||||
|
||||
/**
|
||||
* Full width list item with thumb on the left and two line title & uploader in right.
|
||||
*/
|
||||
LIST,
|
||||
|
||||
/**
|
||||
* Grid mode places two cards per row.
|
||||
*/
|
||||
GRID,
|
||||
|
||||
/**
|
||||
* A full width card in phone - portrait.
|
||||
*/
|
||||
|
||||
@ -2,8 +2,8 @@ package org.schabi.newpipe.info_list
|
||||
|
||||
import android.util.Log
|
||||
import com.xwray.groupie.GroupieAdapter
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfo
|
||||
import kotlin.math.max
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfo
|
||||
|
||||
/**
|
||||
* Custom RecyclerView.Adapter/GroupieAdapter for [StreamSegmentItem] for handling selection state.
|
||||
|
||||
@ -41,14 +41,16 @@ fun View.animate(
|
||||
execOnEnd: Runnable? = null
|
||||
) {
|
||||
if (DEBUG) {
|
||||
val id = try {
|
||||
resources.getResourceEntryName(id)
|
||||
} catch (e: Exception) {
|
||||
id.toString()
|
||||
}
|
||||
val id = runCatching { resources.getResourceEntryName(id) }.getOrDefault(id.toString())
|
||||
val msg = String.format(
|
||||
"%8s → [%s:%s] [%s %s:%s] execOnEnd=%s", enterOrExit,
|
||||
javaClass.simpleName, id, animationType, duration, delay, execOnEnd
|
||||
"%8s → [%s:%s] [%s %s:%s] execOnEnd=%s",
|
||||
enterOrExit,
|
||||
javaClass.simpleName,
|
||||
id,
|
||||
animationType,
|
||||
duration,
|
||||
delay,
|
||||
execOnEnd
|
||||
)
|
||||
Log.d(TAG, "animate(): $msg")
|
||||
}
|
||||
@ -291,5 +293,9 @@ private class HideAndExecOnEndListener(private val view: View, execOnEnd: Runnab
|
||||
}
|
||||
|
||||
enum class AnimationType {
|
||||
ALPHA, SCALE_AND_ALPHA, LIGHT_SCALE_AND_ALPHA, SLIDE_AND_ALPHA, LIGHT_SLIDE_AND_ALPHA
|
||||
ALPHA,
|
||||
SCALE_AND_ALPHA,
|
||||
LIGHT_SCALE_AND_ALPHA,
|
||||
SLIDE_AND_ALPHA,
|
||||
LIGHT_SLIDE_AND_ALPHA
|
||||
}
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
package org.schabi.newpipe.local.bookmark;
|
||||
|
||||
import static org.schabi.newpipe.local.bookmark.MergedPlaylistManager.getMergedOrderedPlaylists;
|
||||
import static org.schabi.newpipe.util.ThemeHelper.shouldUseGridLayout;
|
||||
|
||||
import android.content.DialogInterface;
|
||||
import android.os.Bundle;
|
||||
@ -422,10 +423,11 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL
|
||||
}
|
||||
|
||||
private ItemTouchHelper.SimpleCallback getItemTouchCallback() {
|
||||
// if adding grid layout, also include ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT
|
||||
// with an `if (shouldUseGridLayout()) ...`
|
||||
return new ItemTouchHelper.SimpleCallback(ItemTouchHelper.UP | ItemTouchHelper.DOWN,
|
||||
ItemTouchHelper.ACTION_STATE_IDLE) {
|
||||
int directions = ItemTouchHelper.UP | ItemTouchHelper.DOWN;
|
||||
if (shouldUseGridLayout(requireContext())) {
|
||||
directions |= ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT;
|
||||
}
|
||||
return new ItemTouchHelper.SimpleCallback(directions, ItemTouchHelper.ACTION_STATE_IDLE) {
|
||||
@Override
|
||||
public int interpolateOutOfBoundsScroll(@NonNull final RecyclerView recyclerView,
|
||||
final int viewSize,
|
||||
|
||||
@ -7,6 +7,9 @@ import io.reactivex.rxjava3.core.Completable
|
||||
import io.reactivex.rxjava3.core.Flowable
|
||||
import io.reactivex.rxjava3.core.Maybe
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import java.time.LocalDate
|
||||
import java.time.OffsetDateTime
|
||||
import java.time.ZoneOffset
|
||||
import org.schabi.newpipe.MainActivity.DEBUG
|
||||
import org.schabi.newpipe.NewPipeDatabase
|
||||
import org.schabi.newpipe.database.feed.model.FeedEntity
|
||||
@ -18,9 +21,6 @@ import org.schabi.newpipe.database.subscription.NotificationMode
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem
|
||||
import org.schabi.newpipe.extractor.stream.StreamType
|
||||
import org.schabi.newpipe.local.subscription.FeedGroupIcon
|
||||
import java.time.LocalDate
|
||||
import java.time.OffsetDateTime
|
||||
import java.time.ZoneOffset
|
||||
|
||||
class FeedDatabaseManager(context: Context) {
|
||||
private val database = NewPipeDatabase.getInstance(context)
|
||||
@ -85,14 +85,13 @@ class FeedDatabaseManager(context: Context) {
|
||||
items: List<StreamInfoItem>,
|
||||
oldestAllowedDate: OffsetDateTime = FEED_OLDEST_ALLOWED_DATE
|
||||
) {
|
||||
val itemsToInsert = ArrayList<StreamInfoItem>()
|
||||
loop@ for (streamItem in items) {
|
||||
val uploadDate = streamItem.uploadDate
|
||||
val itemsToInsert = items.mapNotNull { stream ->
|
||||
val uploadDate = stream.uploadDate
|
||||
|
||||
itemsToInsert += when {
|
||||
uploadDate == null && streamItem.streamType == StreamType.LIVE_STREAM -> streamItem
|
||||
uploadDate != null && uploadDate.offsetDateTime() >= oldestAllowedDate -> streamItem
|
||||
else -> continue@loop
|
||||
when {
|
||||
uploadDate == null && stream.streamType == StreamType.LIVE_STREAM -> stream
|
||||
uploadDate != null && uploadDate.offsetDateTime() >= oldestAllowedDate -> stream
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -53,6 +53,8 @@ import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import java.time.OffsetDateTime
|
||||
import java.util.function.Consumer
|
||||
import org.schabi.newpipe.NewPipeDatabase
|
||||
import org.schabi.newpipe.R
|
||||
import org.schabi.newpipe.database.feed.model.FeedGroupEntity
|
||||
@ -82,8 +84,6 @@ import org.schabi.newpipe.util.ThemeHelper.getGridSpanCountStreams
|
||||
import org.schabi.newpipe.util.ThemeHelper.getItemViewMode
|
||||
import org.schabi.newpipe.util.ThemeHelper.resolveDrawable
|
||||
import org.schabi.newpipe.util.ThemeHelper.shouldUseGridLayout
|
||||
import java.time.OffsetDateTime
|
||||
import java.util.function.Consumer
|
||||
|
||||
class FeedFragment : BaseStateFragment<FeedState>() {
|
||||
private var _feedBinding: FragmentFeedBinding? = null
|
||||
@ -92,7 +92,10 @@ class FeedFragment : BaseStateFragment<FeedState>() {
|
||||
private val disposables = CompositeDisposable()
|
||||
|
||||
private lateinit var viewModel: FeedViewModel
|
||||
@State @JvmField var listState: Parcelable? = null
|
||||
|
||||
@State
|
||||
@JvmField
|
||||
var listState: Parcelable? = null
|
||||
|
||||
private var groupId = FeedGroupEntity.GROUP_ALL_ID
|
||||
private var groupName = ""
|
||||
@ -151,7 +154,6 @@ class FeedFragment : BaseStateFragment<FeedState>() {
|
||||
if (newState == RecyclerView.SCROLL_STATE_IDLE &&
|
||||
!recyclerView.canScrollVertically(-1)
|
||||
) {
|
||||
|
||||
if (tryGetNewItemsLoadedButton()?.isVisible == true) {
|
||||
hideNewItemsLoaded(true)
|
||||
}
|
||||
@ -392,8 +394,13 @@ class FeedFragment : BaseStateFragment<FeedState>() {
|
||||
if (item is StreamItem && !isRefreshing) {
|
||||
val stream = item.streamWithState.stream
|
||||
NavigationHelper.openVideoDetailFragment(
|
||||
requireContext(), fm,
|
||||
stream.serviceId, stream.url, stream.title, null, false
|
||||
requireContext(),
|
||||
fm,
|
||||
stream.serviceId,
|
||||
stream.url,
|
||||
stream.title,
|
||||
null,
|
||||
false
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -505,7 +512,8 @@ class FeedFragment : BaseStateFragment<FeedState>() {
|
||||
) {
|
||||
val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
|
||||
val isFastFeedModeEnabled = sharedPreferences.getBoolean(
|
||||
getString(R.string.feed_use_dedicated_fetch_method_key), false
|
||||
getString(R.string.feed_use_dedicated_fetch_method_key),
|
||||
false
|
||||
)
|
||||
|
||||
val builder = AlertDialog.Builder(requireContext())
|
||||
@ -540,7 +548,8 @@ class FeedFragment : BaseStateFragment<FeedState>() {
|
||||
private fun updateRelativeTimeViews() {
|
||||
updateRefreshViewState()
|
||||
groupAdapter.notifyItemRangeChanged(
|
||||
0, groupAdapter.itemCount,
|
||||
0,
|
||||
groupAdapter.itemCount,
|
||||
StreamItem.UPDATE_RELATIVE_TIME
|
||||
)
|
||||
}
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
package org.schabi.newpipe.local.feed
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
import org.schabi.newpipe.local.feed.item.StreamItem
|
||||
import java.time.OffsetDateTime
|
||||
import org.schabi.newpipe.local.feed.item.StreamItem
|
||||
|
||||
sealed class FeedState {
|
||||
data class ProgressState(
|
||||
|
||||
@ -14,6 +14,8 @@ import io.reactivex.rxjava3.core.Flowable
|
||||
import io.reactivex.rxjava3.functions.Function6
|
||||
import io.reactivex.rxjava3.processors.BehaviorProcessor
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import java.time.OffsetDateTime
|
||||
import java.util.concurrent.TimeUnit
|
||||
import org.schabi.newpipe.App
|
||||
import org.schabi.newpipe.R
|
||||
import org.schabi.newpipe.database.feed.model.FeedGroupEntity
|
||||
@ -25,8 +27,6 @@ import org.schabi.newpipe.local.feed.service.FeedEventManager.Event.IdleEvent
|
||||
import org.schabi.newpipe.local.feed.service.FeedEventManager.Event.ProgressEvent
|
||||
import org.schabi.newpipe.local.feed.service.FeedEventManager.Event.SuccessResultEvent
|
||||
import org.schabi.newpipe.util.DEFAULT_THROTTLE_TIMEOUT
|
||||
import java.time.OffsetDateTime
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class FeedViewModel(
|
||||
private val application: Application,
|
||||
@ -64,8 +64,14 @@ class FeedViewModel(
|
||||
feedDatabaseManager.notLoadedCount(groupId),
|
||||
feedDatabaseManager.oldestSubscriptionUpdate(groupId),
|
||||
|
||||
Function6 { t1: FeedEventManager.Event, t2: Boolean, t3: Boolean, t4: Boolean,
|
||||
t5: Long, t6: List<OffsetDateTime?> ->
|
||||
Function6 {
|
||||
t1: FeedEventManager.Event,
|
||||
t2: Boolean,
|
||||
t3: Boolean,
|
||||
t4: Boolean,
|
||||
t5: Long,
|
||||
t6: List<OffsetDateTime?>
|
||||
->
|
||||
return@Function6 CombineResultEventHolder(t1, t2, t3, t4, t5, t6.firstOrNull())
|
||||
}
|
||||
)
|
||||
@ -73,12 +79,13 @@ class FeedViewModel(
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(Schedulers.io())
|
||||
.map { (event, showPlayedItems, showPartiallyPlayedItems, showFutureItems, notLoadedCount, oldestUpdate) ->
|
||||
val streamItems = if (event is SuccessResultEvent || event is IdleEvent)
|
||||
val streamItems = if (event is SuccessResultEvent || event is IdleEvent) {
|
||||
feedDatabaseManager
|
||||
.getStreams(groupId, showPlayedItems, showPartiallyPlayedItems, showFutureItems)
|
||||
.blockingGet(arrayListOf())
|
||||
else
|
||||
} else {
|
||||
arrayListOf()
|
||||
}
|
||||
|
||||
CombineResultDataHolder(event, streamItems, notLoadedCount, oldestUpdate)
|
||||
}
|
||||
@ -150,17 +157,14 @@ class FeedViewModel(
|
||||
fun getShowFutureItemsFromPreferences() = getShowFutureItemsFromPreferences(application)
|
||||
|
||||
companion object {
|
||||
private fun getShowPlayedItemsFromPreferences(context: Context) =
|
||||
PreferenceManager.getDefaultSharedPreferences(context)
|
||||
.getBoolean(context.getString(R.string.feed_show_watched_items_key), true)
|
||||
private fun getShowPlayedItemsFromPreferences(context: Context) = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
.getBoolean(context.getString(R.string.feed_show_watched_items_key), true)
|
||||
|
||||
private fun getShowPartiallyPlayedItemsFromPreferences(context: Context) =
|
||||
PreferenceManager.getDefaultSharedPreferences(context)
|
||||
.getBoolean(context.getString(R.string.feed_show_partially_watched_items_key), true)
|
||||
private fun getShowPartiallyPlayedItemsFromPreferences(context: Context) = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
.getBoolean(context.getString(R.string.feed_show_partially_watched_items_key), true)
|
||||
|
||||
private fun getShowFutureItemsFromPreferences(context: Context) =
|
||||
PreferenceManager.getDefaultSharedPreferences(context)
|
||||
.getBoolean(context.getString(R.string.feed_show_future_items_key), true)
|
||||
private fun getShowFutureItemsFromPreferences(context: Context) = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
.getBoolean(context.getString(R.string.feed_show_future_items_key), true)
|
||||
|
||||
fun getFactory(context: Context, groupId: Long) = viewModelFactory {
|
||||
initializer {
|
||||
|
||||
@ -14,6 +14,7 @@ import androidx.core.app.NotificationManagerCompat
|
||||
import androidx.core.app.PendingIntentCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.content.getSystemService
|
||||
import androidx.core.net.toUri
|
||||
import androidx.preference.PreferenceManager
|
||||
import org.schabi.newpipe.R
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem
|
||||
@ -37,7 +38,9 @@ class NotificationHelper(val context: Context) {
|
||||
fun displayNewStreamsNotifications(data: FeedUpdateInfo) {
|
||||
val newStreams = data.newStreams
|
||||
val summary = context.resources.getQuantityString(
|
||||
R.plurals.new_streams, newStreams.size, newStreams.size
|
||||
R.plurals.new_streams,
|
||||
newStreams.size,
|
||||
newStreams.size
|
||||
)
|
||||
val summaryBuilder = NotificationCompat.Builder(
|
||||
context,
|
||||
@ -146,8 +149,7 @@ class NotificationHelper(val context: Context) {
|
||||
val manager = context.getSystemService<NotificationManager>()!!
|
||||
val enabled = manager.areNotificationsEnabled()
|
||||
val channel = manager.getNotificationChannel(channelId)
|
||||
val importance = channel?.importance
|
||||
enabled && channel != null && importance != NotificationManager.IMPORTANCE_NONE
|
||||
enabled && channel?.importance != NotificationManager.IMPORTANCE_NONE
|
||||
} else {
|
||||
NotificationManagerCompat.from(context).areNotificationsEnabled()
|
||||
}
|
||||
@ -177,7 +179,7 @@ class NotificationHelper(val context: Context) {
|
||||
context.startActivity(intent)
|
||||
} else {
|
||||
val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
|
||||
intent.data = Uri.parse("package:" + context.packageName)
|
||||
intent.data = "package:${context.packageName}".toUri()
|
||||
context.startActivity(intent)
|
||||
}
|
||||
}
|
||||
|
||||
@ -16,6 +16,7 @@ import androidx.work.WorkerParameters
|
||||
import androidx.work.rxjava3.RxWorker
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import java.util.concurrent.TimeUnit
|
||||
import org.schabi.newpipe.App
|
||||
import org.schabi.newpipe.R
|
||||
import org.schabi.newpipe.error.ErrorInfo
|
||||
@ -23,7 +24,6 @@ import org.schabi.newpipe.error.ErrorUtil
|
||||
import org.schabi.newpipe.error.UserAction
|
||||
import org.schabi.newpipe.local.feed.service.FeedLoadManager
|
||||
import org.schabi.newpipe.local.feed.service.FeedLoadService
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
/*
|
||||
* Worker which checks for new streams of subscribed channels
|
||||
@ -31,7 +31,7 @@ import java.util.concurrent.TimeUnit
|
||||
*/
|
||||
class NotificationWorker(
|
||||
appContext: Context,
|
||||
workerParams: WorkerParameters,
|
||||
workerParams: WorkerParameters
|
||||
) : RxWorker(appContext, workerParams) {
|
||||
|
||||
private val notificationHelper by lazy {
|
||||
@ -95,9 +95,8 @@ class NotificationWorker(
|
||||
private val TAG = NotificationWorker::class.java.simpleName
|
||||
private const val WORK_TAG = App.PACKAGE_NAME + "_streams_notifications"
|
||||
|
||||
private fun areNotificationsEnabled(context: Context) =
|
||||
NotificationHelper.areNewStreamsNotificationsEnabled(context) &&
|
||||
NotificationHelper.areNotificationsEnabledOnDevice(context)
|
||||
private fun areNotificationsEnabled(context: Context) = NotificationHelper.areNewStreamsNotificationsEnabled(context) &&
|
||||
NotificationHelper.areNotificationsEnabledOnDevice(context)
|
||||
|
||||
/**
|
||||
* Schedules a task for the [NotificationWorker]
|
||||
|
||||
@ -2,8 +2,9 @@ package org.schabi.newpipe.local.feed.notifications
|
||||
|
||||
import android.content.Context
|
||||
import androidx.preference.PreferenceManager
|
||||
import org.schabi.newpipe.R
|
||||
import java.util.concurrent.TimeUnit
|
||||
import org.schabi.newpipe.R
|
||||
import org.schabi.newpipe.ktx.getStringSafe
|
||||
|
||||
/**
|
||||
* Information for the Scheduler which checks for new streams.
|
||||
@ -20,11 +21,9 @@ data class ScheduleOptions(
|
||||
val preferences = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
return ScheduleOptions(
|
||||
interval = TimeUnit.SECONDS.toMillis(
|
||||
preferences.getString(
|
||||
preferences.getStringSafe(
|
||||
context.getString(R.string.streams_notifications_interval_key),
|
||||
null
|
||||
)?.toLongOrNull() ?: context.getString(
|
||||
R.string.streams_notifications_interval_default
|
||||
context.getString(R.string.streams_notifications_interval_default)
|
||||
).toLong()
|
||||
),
|
||||
isRequireNonMeteredNetwork = preferences.getString(
|
||||
|
||||
@ -3,8 +3,8 @@ package org.schabi.newpipe.local.feed.service
|
||||
import androidx.annotation.StringRes
|
||||
import io.reactivex.rxjava3.core.Flowable
|
||||
import io.reactivex.rxjava3.processors.BehaviorProcessor
|
||||
import org.schabi.newpipe.local.feed.service.FeedEventManager.Event.IdleEvent
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import org.schabi.newpipe.local.feed.service.FeedEventManager.Event.IdleEvent
|
||||
|
||||
object FeedEventManager {
|
||||
private var processor: BehaviorProcessor<Event> = BehaviorProcessor.create()
|
||||
|
||||
@ -11,6 +11,10 @@ import io.reactivex.rxjava3.core.Single
|
||||
import io.reactivex.rxjava3.functions.Consumer
|
||||
import io.reactivex.rxjava3.processors.PublishProcessor
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import java.time.OffsetDateTime
|
||||
import java.time.ZoneOffset
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
import org.schabi.newpipe.R
|
||||
import org.schabi.newpipe.database.feed.model.FeedGroupEntity
|
||||
import org.schabi.newpipe.database.subscription.NotificationMode
|
||||
@ -27,10 +31,6 @@ import org.schabi.newpipe.util.ChannelTabHelper
|
||||
import org.schabi.newpipe.util.ExtractorHelper.getChannelInfo
|
||||
import org.schabi.newpipe.util.ExtractorHelper.getChannelTab
|
||||
import org.schabi.newpipe.util.ExtractorHelper.getMoreChannelTabItems
|
||||
import java.time.OffsetDateTime
|
||||
import java.time.ZoneOffset
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
|
||||
class FeedLoadManager(private val context: Context) {
|
||||
|
||||
@ -60,7 +60,7 @@ class FeedLoadManager(private val context: Context) {
|
||||
*/
|
||||
fun startLoading(
|
||||
groupId: Long = FeedGroupEntity.GROUP_ALL_ID,
|
||||
ignoreOutdatedThreshold: Boolean = false,
|
||||
ignoreOutdatedThreshold: Boolean = false
|
||||
): Single<List<Notification<FeedUpdateInfo>>> {
|
||||
val defaultSharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
val useFeedExtractor = defaultSharedPreferences.getBoolean(
|
||||
@ -85,9 +85,12 @@ class FeedLoadManager(private val context: Context) {
|
||||
FeedGroupEntity.GROUP_ALL_ID -> feedDatabaseManager.outdatedSubscriptions(
|
||||
outdatedThreshold
|
||||
)
|
||||
|
||||
GROUP_NOTIFICATION_ENABLED -> feedDatabaseManager.outdatedSubscriptionsWithNotificationMode(
|
||||
outdatedThreshold, NotificationMode.ENABLED
|
||||
outdatedThreshold,
|
||||
NotificationMode.ENABLED
|
||||
)
|
||||
|
||||
else -> feedDatabaseManager.outdatedSubscriptionsForGroup(groupId, outdatedThreshold)
|
||||
}
|
||||
|
||||
@ -186,7 +189,8 @@ class FeedLoadManager(private val context: Context) {
|
||||
|
||||
val channelInfo = getChannelInfo(
|
||||
subscriptionEntity.serviceId,
|
||||
subscriptionEntity.url, true
|
||||
subscriptionEntity.url,
|
||||
true
|
||||
)
|
||||
.onErrorReturn(storeOriginalErrorAndRethrow)
|
||||
.blockingGet()
|
||||
@ -216,7 +220,8 @@ class FeedLoadManager(private val context: Context) {
|
||||
) {
|
||||
val infoItemsPage = getMoreChannelTabItems(
|
||||
subscriptionEntity.serviceId,
|
||||
linkHandler, channelTabInfo.nextPage
|
||||
linkHandler,
|
||||
channelTabInfo.nextPage
|
||||
)
|
||||
.blockingGet()
|
||||
|
||||
@ -234,7 +239,7 @@ class FeedLoadManager(private val context: Context) {
|
||||
subscriptionEntity,
|
||||
originalInfo!!,
|
||||
streams!!,
|
||||
errors,
|
||||
errors
|
||||
)
|
||||
)
|
||||
} catch (e: Throwable) {
|
||||
@ -305,6 +310,7 @@ class FeedLoadManager(private val context: Context) {
|
||||
feedDatabaseManager.markAsOutdated(info.uid)
|
||||
}
|
||||
}
|
||||
|
||||
notification.isOnError -> {
|
||||
val error = notification.error
|
||||
feedResultsHolder.addError(error!!)
|
||||
|
||||
@ -36,13 +36,13 @@ import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.core.Flowable
|
||||
import io.reactivex.rxjava3.disposables.Disposable
|
||||
import io.reactivex.rxjava3.functions.Function
|
||||
import java.util.concurrent.TimeUnit
|
||||
import org.schabi.newpipe.App
|
||||
import org.schabi.newpipe.MainActivity.DEBUG
|
||||
import org.schabi.newpipe.R
|
||||
import org.schabi.newpipe.database.feed.model.FeedGroupEntity
|
||||
import org.schabi.newpipe.local.feed.service.FeedEventManager.Event.ErrorResultEvent
|
||||
import org.schabi.newpipe.local.feed.service.FeedEventManager.postEvent
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class FeedLoadService : Service() {
|
||||
companion object {
|
||||
@ -94,7 +94,8 @@ class FeedLoadService : Service() {
|
||||
.doOnSubscribe {
|
||||
startForeground(NOTIFICATION_ID, notificationBuilder.build())
|
||||
}
|
||||
.subscribe { _, error: Throwable? -> // explicitly mark error as nullable
|
||||
.subscribe { _, error: Throwable? ->
|
||||
// explicitly mark error as nullable
|
||||
if (error != null) {
|
||||
Log.e(TAG, "Error while storing result", error)
|
||||
handleError(error)
|
||||
|
||||
@ -3,5 +3,5 @@ package org.schabi.newpipe.local.feed.service
|
||||
data class FeedLoadState(
|
||||
val updateDescription: String,
|
||||
val maxProgress: Int,
|
||||
val currentProgress: Int,
|
||||
val currentProgress: Int
|
||||
)
|
||||
|
||||
@ -25,13 +25,13 @@ data class FeedUpdateInfo(
|
||||
val description: String?,
|
||||
val subscriberCount: Long?,
|
||||
val streams: List<StreamInfoItem>,
|
||||
val errors: List<Throwable>,
|
||||
val errors: List<Throwable>
|
||||
) {
|
||||
constructor(
|
||||
subscription: SubscriptionEntity,
|
||||
info: Info,
|
||||
streams: List<StreamInfoItem>,
|
||||
errors: List<Throwable>,
|
||||
errors: List<Throwable>
|
||||
) : this(
|
||||
uid = subscription.uid,
|
||||
notificationMode = subscription.notificationMode,
|
||||
@ -46,7 +46,7 @@ data class FeedUpdateInfo(
|
||||
description = (info as? ChannelInfo)?.description,
|
||||
subscriberCount = (info as? ChannelInfo)?.subscriberCount,
|
||||
streams = streams,
|
||||
errors = errors,
|
||||
errors = errors
|
||||
)
|
||||
|
||||
/**
|
||||
|
||||
@ -44,7 +44,6 @@ private fun exportJustUrls(playlist: List<PlaylistStreamEntry>): String {
|
||||
}
|
||||
|
||||
private fun exportAsYoutubeTempPlaylist(playlist: List<PlaylistStreamEntry>): String {
|
||||
|
||||
val videoIDs = playlist.asReversed().asSequence()
|
||||
.mapNotNull { getYouTubeId(it.streamEntity.url) }
|
||||
.take(50) // YouTube limitation: temp playlists can't have more than 50 items
|
||||
@ -64,6 +63,5 @@ private val linkHandler: YoutubeStreamLinkHandlerFactory = YoutubeStreamLinkHand
|
||||
* @return the video id
|
||||
*/
|
||||
private fun getYouTubeId(url: String): String? {
|
||||
|
||||
return try { linkHandler.getId(url) } catch (e: ParsingException) { null }
|
||||
return runCatching { linkHandler.getId(url) }.getOrNull()
|
||||
}
|
||||
|
||||
@ -768,11 +768,17 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
|
||||
final boolean isSwapped = itemListAdapter.swapItems(sourceIndex, targetIndex);
|
||||
if (isSwapped) {
|
||||
debounceSaver.setHasChangesToSave();
|
||||
saveImmediate();
|
||||
}
|
||||
return isSwapped;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearView(@NonNull final RecyclerView recyclerView,
|
||||
@NonNull final RecyclerView.ViewHolder viewHolder) {
|
||||
super.clearView(recyclerView, viewHolder);
|
||||
saveImmediate();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isLongPressDragEnabled() {
|
||||
return false;
|
||||
|
||||
@ -26,6 +26,9 @@ import com.xwray.groupie.GroupAdapter
|
||||
import com.xwray.groupie.Section
|
||||
import com.xwray.groupie.viewbinding.GroupieViewHolder
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
import org.schabi.newpipe.R
|
||||
import org.schabi.newpipe.database.feed.model.FeedGroupEntity.Companion.GROUP_ALL_ID
|
||||
import org.schabi.newpipe.databinding.DialogTitleBinding
|
||||
@ -59,9 +62,6 @@ import org.schabi.newpipe.util.OnClickGesture
|
||||
import org.schabi.newpipe.util.ServiceHelper
|
||||
import org.schabi.newpipe.util.ThemeHelper.getGridSpanCountChannels
|
||||
import org.schabi.newpipe.util.external_communication.ShareUtils
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
|
||||
class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
|
||||
private var _binding: FragmentSubscriptionBinding? = null
|
||||
@ -272,10 +272,13 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
|
||||
when (item) {
|
||||
is FeedGroupCardItem ->
|
||||
NavigationHelper.openFeedFragment(fm, item.groupId, item.name)
|
||||
|
||||
is FeedGroupCardGridItem ->
|
||||
NavigationHelper.openFeedFragment(fm, item.groupId, item.name)
|
||||
|
||||
is FeedGroupAddNewItem ->
|
||||
FeedGroupDialog.newInstance().show(fm, null)
|
||||
|
||||
is FeedGroupAddNewGridItem ->
|
||||
FeedGroupDialog.newInstance().show(fm, null)
|
||||
}
|
||||
@ -290,6 +293,7 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
|
||||
when (item) {
|
||||
is FeedGroupCardItem ->
|
||||
FeedGroupDialog.newInstance(item.groupId).show(fm, null)
|
||||
|
||||
is FeedGroupCardGridItem ->
|
||||
FeedGroupDialog.newInstance(item.groupId).show(fm, null)
|
||||
}
|
||||
@ -305,7 +309,7 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
|
||||
title = getString(R.string.feed_groups_header_title),
|
||||
onSortClicked = ::openReorderDialog,
|
||||
onToggleListViewModeClicked = ::toggleListViewMode,
|
||||
listViewMode = viewModel.getListViewMode(),
|
||||
listViewMode = viewModel.getListViewMode()
|
||||
)
|
||||
|
||||
add(Section(feedGroupsSortMenuItem, listOf(feedGroupsCarousel)))
|
||||
@ -338,9 +342,14 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
|
||||
val actions = DialogInterface.OnClickListener { _, i ->
|
||||
when (i) {
|
||||
0 -> ShareUtils.shareText(
|
||||
requireContext(), selectedItem.name, selectedItem.url, selectedItem.thumbnails
|
||||
requireContext(),
|
||||
selectedItem.name,
|
||||
selectedItem.url,
|
||||
selectedItem.thumbnails
|
||||
)
|
||||
|
||||
1 -> ShareUtils.openUrlInBrowser(requireContext(), selectedItem.url)
|
||||
|
||||
2 -> deleteChannel(selectedItem)
|
||||
}
|
||||
}
|
||||
@ -370,7 +379,9 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
|
||||
private val listenerChannelItem = object : OnClickGesture<ChannelInfoItem> {
|
||||
override fun selected(selectedItem: ChannelInfoItem) = NavigationHelper.openChannelFragment(
|
||||
fm,
|
||||
selectedItem.serviceId, selectedItem.url, selectedItem.name
|
||||
selectedItem.serviceId,
|
||||
selectedItem.url,
|
||||
selectedItem.name
|
||||
)
|
||||
|
||||
override fun held(selectedItem: ChannelInfoItem) = showLongTapDialog(selectedItem)
|
||||
@ -400,6 +411,7 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
|
||||
itemsListState = null
|
||||
}
|
||||
}
|
||||
|
||||
is SubscriptionState.ErrorState -> {
|
||||
result.error?.let {
|
||||
showError(ErrorInfo(result.error, UserAction.SOMETHING_ELSE, "Subscriptions"))
|
||||
|
||||
@ -36,13 +36,16 @@ class SubscriptionManager(context: Context) {
|
||||
filterQuery.isNotEmpty() -> {
|
||||
return if (showOnlyUngrouped) {
|
||||
subscriptionTable.getSubscriptionsOnlyUngroupedFiltered(
|
||||
currentGroupId, filterQuery
|
||||
currentGroupId,
|
||||
filterQuery
|
||||
)
|
||||
} else {
|
||||
subscriptionTable.getSubscriptionsFiltered(filterQuery)
|
||||
}
|
||||
}
|
||||
|
||||
showOnlyUngrouped -> subscriptionTable.getSubscriptionsOnlyUngrouped(currentGroupId)
|
||||
|
||||
else -> subscriptionTable.getAll()
|
||||
}
|
||||
}
|
||||
@ -59,19 +62,18 @@ class SubscriptionManager(context: Context) {
|
||||
}
|
||||
}
|
||||
|
||||
fun updateChannelInfo(info: ChannelInfo): Completable =
|
||||
subscriptionTable.getSubscription(info.serviceId, info.url)
|
||||
.flatMapCompletable {
|
||||
Completable.fromRunnable {
|
||||
it.apply {
|
||||
name = info.name
|
||||
avatarUrl = ImageStrategy.imageListToDbUrl(info.avatars)
|
||||
description = info.description
|
||||
subscriberCount = info.subscriberCount
|
||||
}
|
||||
subscriptionTable.update(it)
|
||||
fun updateChannelInfo(info: ChannelInfo): Completable = subscriptionTable.getSubscription(info.serviceId, info.url)
|
||||
.flatMapCompletable {
|
||||
Completable.fromRunnable {
|
||||
it.apply {
|
||||
name = info.name
|
||||
avatarUrl = ImageStrategy.imageListToDbUrl(info.avatars)
|
||||
description = info.description
|
||||
subscriberCount = info.subscriberCount
|
||||
}
|
||||
subscriptionTable.update(it)
|
||||
}
|
||||
}
|
||||
|
||||
fun updateNotificationMode(serviceId: Int, url: String, @NotificationMode mode: Int): Completable {
|
||||
return subscriptionTable().getSubscription(serviceId, url)
|
||||
|
||||
@ -9,6 +9,7 @@ import com.xwray.groupie.Group
|
||||
import io.reactivex.rxjava3.core.Flowable
|
||||
import io.reactivex.rxjava3.processors.BehaviorProcessor
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import java.util.concurrent.TimeUnit
|
||||
import org.schabi.newpipe.info_list.ItemViewMode
|
||||
import org.schabi.newpipe.local.feed.FeedDatabaseManager
|
||||
import org.schabi.newpipe.local.subscription.item.ChannelItem
|
||||
@ -16,7 +17,6 @@ import org.schabi.newpipe.local.subscription.item.FeedGroupCardGridItem
|
||||
import org.schabi.newpipe.local.subscription.item.FeedGroupCardItem
|
||||
import org.schabi.newpipe.util.DEFAULT_THROTTLE_TIMEOUT
|
||||
import org.schabi.newpipe.util.ThemeHelper.getItemViewMode
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class SubscriptionViewModel(application: Application) : AndroidViewModel(application) {
|
||||
private var feedDatabaseManager: FeedDatabaseManager = FeedDatabaseManager(application)
|
||||
|
||||
@ -23,6 +23,7 @@ import com.livefront.bridge.Bridge
|
||||
import com.xwray.groupie.GroupieAdapter
|
||||
import com.xwray.groupie.OnItemClickListener
|
||||
import com.xwray.groupie.Section
|
||||
import java.io.Serializable
|
||||
import org.schabi.newpipe.R
|
||||
import org.schabi.newpipe.database.feed.model.FeedGroupEntity
|
||||
import org.schabi.newpipe.databinding.DialogFeedGroupCreateBinding
|
||||
@ -40,7 +41,6 @@ import org.schabi.newpipe.local.subscription.item.PickerIconItem
|
||||
import org.schabi.newpipe.local.subscription.item.PickerSubscriptionItem
|
||||
import org.schabi.newpipe.util.DeviceUtils
|
||||
import org.schabi.newpipe.util.ThemeHelper
|
||||
import java.io.Serializable
|
||||
|
||||
class FeedGroupDialog : DialogFragment(), BackPressable {
|
||||
private var _feedGroupCreateBinding: DialogFeedGroupCreateBinding? = null
|
||||
@ -61,16 +61,41 @@ class FeedGroupDialog : DialogFragment(), BackPressable {
|
||||
data object DeleteScreen : ScreenState()
|
||||
}
|
||||
|
||||
@State @JvmField var selectedIcon: FeedGroupIcon? = null
|
||||
@State @JvmField var selectedSubscriptions: HashSet<Long> = HashSet()
|
||||
@State @JvmField var wasSubscriptionSelectionChanged: Boolean = false
|
||||
@State @JvmField var currentScreen: ScreenState = InitialScreen
|
||||
@State
|
||||
@JvmField
|
||||
var selectedIcon: FeedGroupIcon? = null
|
||||
|
||||
@State @JvmField var subscriptionsListState: Parcelable? = null
|
||||
@State @JvmField var iconsListState: Parcelable? = null
|
||||
@State @JvmField var wasSearchSubscriptionsVisible = false
|
||||
@State @JvmField var subscriptionsCurrentSearchQuery = ""
|
||||
@State @JvmField var subscriptionsShowOnlyUngrouped = false
|
||||
@State
|
||||
@JvmField
|
||||
var selectedSubscriptions: HashSet<Long> = HashSet()
|
||||
|
||||
@State
|
||||
@JvmField
|
||||
var wasSubscriptionSelectionChanged: Boolean = false
|
||||
|
||||
@State
|
||||
@JvmField
|
||||
var currentScreen: ScreenState = InitialScreen
|
||||
|
||||
@State
|
||||
@JvmField
|
||||
var subscriptionsListState: Parcelable? = null
|
||||
|
||||
@State
|
||||
@JvmField
|
||||
var iconsListState: Parcelable? = null
|
||||
|
||||
@State
|
||||
@JvmField
|
||||
var wasSearchSubscriptionsVisible = false
|
||||
|
||||
@State
|
||||
@JvmField
|
||||
var subscriptionsCurrentSearchQuery = ""
|
||||
|
||||
@State
|
||||
@JvmField
|
||||
var subscriptionsShowOnlyUngrouped = false
|
||||
|
||||
private val subscriptionMainSection = Section()
|
||||
private val subscriptionEmptyFooter = Section()
|
||||
@ -154,8 +179,10 @@ class FeedGroupDialog : DialogFragment(), BackPressable {
|
||||
itemAnimator = null
|
||||
adapter = subscriptionGroupAdapter
|
||||
layoutManager = GridLayoutManager(
|
||||
requireContext(), subscriptionGroupAdapter.spanCount,
|
||||
RecyclerView.VERTICAL, false
|
||||
requireContext(),
|
||||
subscriptionGroupAdapter.spanCount,
|
||||
RecyclerView.VERTICAL,
|
||||
false
|
||||
).apply {
|
||||
spanSizeLookup = subscriptionGroupAdapter.spanSizeLookup
|
||||
}
|
||||
@ -363,7 +390,8 @@ class FeedGroupDialog : DialogFragment(), BackPressable {
|
||||
val selectedCount = this.selectedSubscriptions.size
|
||||
val selectedCountText = resources.getQuantityString(
|
||||
R.plurals.feed_group_dialog_selection_count,
|
||||
selectedCount, selectedCount
|
||||
selectedCount,
|
||||
selectedCount
|
||||
)
|
||||
feedGroupCreateBinding.selectedSubscriptionCountView.text = selectedCountText
|
||||
feedGroupCreateBinding.subscriptionsHeaderInfo.text = selectedCountText
|
||||
|
||||
@ -55,7 +55,8 @@ class FeedGroupDialogViewModel(
|
||||
|
||||
private var subscriptionsDisposable = Flowable
|
||||
.combineLatest(
|
||||
subscriptionsFlowable, feedDatabaseManager.subscriptionIdsForGroup(groupId)
|
||||
subscriptionsFlowable,
|
||||
feedDatabaseManager.subscriptionIdsForGroup(groupId)
|
||||
) { t1: List<PickerSubscriptionItem>, t2: List<Long> -> t1 to t2.toSet() }
|
||||
.subscribeOn(Schedulers.io())
|
||||
.subscribe(mutableSubscriptionsLiveData::postValue)
|
||||
@ -125,7 +126,10 @@ class FeedGroupDialogViewModel(
|
||||
) = viewModelFactory {
|
||||
initializer {
|
||||
FeedGroupDialogViewModel(
|
||||
context.applicationContext, groupId, initialQuery, initialShowOnlyUngrouped
|
||||
context.applicationContext,
|
||||
groupId,
|
||||
initialQuery,
|
||||
initialShowOnlyUngrouped
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -15,6 +15,7 @@ import com.evernote.android.state.State
|
||||
import com.livefront.bridge.Bridge
|
||||
import com.xwray.groupie.GroupieAdapter
|
||||
import com.xwray.groupie.TouchCallback
|
||||
import java.util.Collections
|
||||
import org.schabi.newpipe.R
|
||||
import org.schabi.newpipe.database.feed.model.FeedGroupEntity
|
||||
import org.schabi.newpipe.databinding.DialogFeedGroupReorderBinding
|
||||
@ -22,7 +23,6 @@ import org.schabi.newpipe.local.subscription.dialog.FeedGroupReorderDialogViewMo
|
||||
import org.schabi.newpipe.local.subscription.dialog.FeedGroupReorderDialogViewModel.DialogEvent.SuccessEvent
|
||||
import org.schabi.newpipe.local.subscription.item.FeedGroupReorderItem
|
||||
import org.schabi.newpipe.util.ThemeHelper
|
||||
import java.util.Collections
|
||||
|
||||
class FeedGroupReorderDialog : DialogFragment() {
|
||||
private var _binding: DialogFeedGroupReorderBinding? = null
|
||||
|
||||
@ -43,7 +43,10 @@ class ChannelItem(
|
||||
|
||||
gesturesListener?.run {
|
||||
viewHolder.root.setOnClickListener { selected(infoItem) }
|
||||
viewHolder.root.setOnLongClickListener { held(infoItem); true }
|
||||
viewHolder.root.setOnLongClickListener {
|
||||
held(infoItem)
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -10,7 +10,7 @@ import org.schabi.newpipe.local.subscription.FeedGroupIcon
|
||||
data class FeedGroupCardGridItem(
|
||||
val groupId: Long = FeedGroupEntity.GROUP_ALL_ID,
|
||||
val name: String,
|
||||
val icon: FeedGroupIcon,
|
||||
val icon: FeedGroupIcon
|
||||
) : BindableItem<FeedGroupCardGridItemBinding>() {
|
||||
constructor (feedGroupEntity: FeedGroupEntity) : this(feedGroupEntity.uid, feedGroupEntity.name, feedGroupEntity.icon)
|
||||
|
||||
|
||||
@ -18,7 +18,7 @@ import org.schabi.newpipe.player.ui.VideoPlayerUi
|
||||
* and provides some abstract methods to make it easier separating the logic from the UI.
|
||||
*/
|
||||
abstract class BasePlayerGestureListener(
|
||||
private val playerUi: VideoPlayerUi,
|
||||
private val playerUi: VideoPlayerUi
|
||||
) : GestureDetector.SimpleOnGestureListener(), View.OnTouchListener {
|
||||
|
||||
protected val player: Player = playerUi.player
|
||||
@ -86,8 +86,9 @@ abstract class BasePlayerGestureListener(
|
||||
// ///////////////////////////////////////////////////////////////////
|
||||
|
||||
override fun onDown(e: MotionEvent): Boolean {
|
||||
if (DEBUG)
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "onDown called with e = [$e]")
|
||||
}
|
||||
|
||||
if (isDoubleTapping && isDoubleTapEnabled) {
|
||||
doubleTapControls?.onDoubleTapProgressDown(getDisplayPortion(e))
|
||||
@ -108,8 +109,9 @@ abstract class BasePlayerGestureListener(
|
||||
}
|
||||
|
||||
override fun onDoubleTap(e: MotionEvent): Boolean {
|
||||
if (DEBUG)
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "onDoubleTap called with e = [$e]")
|
||||
}
|
||||
|
||||
onDoubleTap(e, getDisplayPortion(e))
|
||||
return true
|
||||
@ -136,8 +138,9 @@ abstract class BasePlayerGestureListener(
|
||||
|
||||
private fun startMultiDoubleTap(e: MotionEvent) {
|
||||
if (!isDoubleTapping) {
|
||||
if (DEBUG)
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "startMultiDoubleTap called with e = [$e]")
|
||||
}
|
||||
|
||||
keepInDoubleTapMode()
|
||||
doubleTapControls?.onDoubleTapStarted(getDisplayPortion(e))
|
||||
@ -145,8 +148,9 @@ abstract class BasePlayerGestureListener(
|
||||
}
|
||||
|
||||
fun keepInDoubleTapMode() {
|
||||
if (DEBUG)
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "keepInDoubleTapMode called")
|
||||
}
|
||||
|
||||
isDoubleTapping = true
|
||||
doubleTapHandler.removeCallbacksAndMessages(DOUBLE_TAP)
|
||||
@ -161,8 +165,9 @@ abstract class BasePlayerGestureListener(
|
||||
}
|
||||
|
||||
fun endMultiDoubleTap() {
|
||||
if (DEBUG)
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "endMultiDoubleTap called")
|
||||
}
|
||||
|
||||
isDoubleTapping = false
|
||||
doubleTapHandler.removeCallbacksAndMessages(DOUBLE_TAP)
|
||||
|
||||
@ -1,5 +1,9 @@
|
||||
package org.schabi.newpipe.player.gesture
|
||||
|
||||
enum class DisplayPortion {
|
||||
LEFT, MIDDLE, RIGHT, LEFT_HALF, RIGHT_HALF
|
||||
LEFT,
|
||||
MIDDLE,
|
||||
RIGHT,
|
||||
LEFT_HALF,
|
||||
RIGHT_HALF
|
||||
}
|
||||
|
||||
@ -8,6 +8,7 @@ import android.widget.ProgressBar
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.appcompat.content.res.AppCompatResources
|
||||
import androidx.core.view.isVisible
|
||||
import kotlin.math.abs
|
||||
import org.schabi.newpipe.MainActivity
|
||||
import org.schabi.newpipe.R
|
||||
import org.schabi.newpipe.ktx.AnimationType
|
||||
@ -17,7 +18,6 @@ import org.schabi.newpipe.player.helper.AudioReactor
|
||||
import org.schabi.newpipe.player.helper.PlayerHelper
|
||||
import org.schabi.newpipe.player.ui.MainPlayerUi
|
||||
import org.schabi.newpipe.util.ThemeHelper.getAndroidDimenPx
|
||||
import kotlin.math.abs
|
||||
|
||||
/**
|
||||
* GestureListener for the player
|
||||
@ -42,24 +42,29 @@ class MainPlayerGestureListener(
|
||||
v.parent?.requestDisallowInterceptTouchEvent(playerUi.isFullscreen)
|
||||
true
|
||||
}
|
||||
|
||||
MotionEvent.ACTION_UP -> {
|
||||
v.parent?.requestDisallowInterceptTouchEvent(false)
|
||||
false
|
||||
}
|
||||
|
||||
else -> true
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSingleTapConfirmed(e: MotionEvent): Boolean {
|
||||
if (DEBUG)
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "onSingleTapConfirmed() called with: e = [$e]")
|
||||
}
|
||||
|
||||
if (isDoubleTapping)
|
||||
if (isDoubleTapping) {
|
||||
return true
|
||||
}
|
||||
super.onSingleTapConfirmed(e)
|
||||
|
||||
if (player.currentState != Player.STATE_BLOCKED)
|
||||
if (player.currentState != Player.STATE_BLOCKED) {
|
||||
onSingleTap()
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
@ -195,6 +200,7 @@ class MainPlayerGestureListener(
|
||||
when (PlayerHelper.getActionForRightGestureSide(player.context)) {
|
||||
player.context.getString(R.string.volume_control_key) ->
|
||||
onScrollVolume(distanceY)
|
||||
|
||||
player.context.getString(R.string.brightness_control_key) ->
|
||||
onScrollBrightness(distanceY)
|
||||
}
|
||||
@ -202,6 +208,7 @@ class MainPlayerGestureListener(
|
||||
when (PlayerHelper.getActionForLeftGestureSide(player.context)) {
|
||||
player.context.getString(R.string.volume_control_key) ->
|
||||
onScrollVolume(distanceY)
|
||||
|
||||
player.context.getString(R.string.brightness_control_key) ->
|
||||
onScrollBrightness(distanceY)
|
||||
}
|
||||
|
||||
@ -5,17 +5,17 @@ import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import android.view.ViewConfiguration
|
||||
import androidx.core.view.isVisible
|
||||
import org.schabi.newpipe.MainActivity
|
||||
import org.schabi.newpipe.ktx.AnimationType
|
||||
import org.schabi.newpipe.ktx.animate
|
||||
import org.schabi.newpipe.player.ui.PopupPlayerUi
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.hypot
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
import org.schabi.newpipe.MainActivity
|
||||
import org.schabi.newpipe.ktx.AnimationType
|
||||
import org.schabi.newpipe.ktx.animate
|
||||
import org.schabi.newpipe.player.ui.PopupPlayerUi
|
||||
|
||||
class PopupPlayerGestureListener(
|
||||
private val playerUi: PopupPlayerUi,
|
||||
private val playerUi: PopupPlayerUi
|
||||
) : BasePlayerGestureListener(playerUi) {
|
||||
|
||||
private var isMoving = false
|
||||
@ -205,13 +205,16 @@ class PopupPlayerGestureListener(
|
||||
}
|
||||
|
||||
override fun onSingleTapConfirmed(e: MotionEvent): Boolean {
|
||||
if (DEBUG)
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "onSingleTapConfirmed() called with: e = [$e]")
|
||||
}
|
||||
|
||||
if (isDoubleTapping)
|
||||
if (isDoubleTapping) {
|
||||
return true
|
||||
if (player.exoPlayerIsNull())
|
||||
}
|
||||
if (player.exoPlayerIsNull()) {
|
||||
return false
|
||||
}
|
||||
|
||||
onSingleTap()
|
||||
return true
|
||||
|
||||
@ -13,6 +13,8 @@ import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import io.reactivex.rxjava3.disposables.Disposable
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import java.util.function.BiConsumer
|
||||
import java.util.function.Consumer
|
||||
import org.schabi.newpipe.MainActivity
|
||||
import org.schabi.newpipe.NewPipeDatabase
|
||||
import org.schabi.newpipe.R
|
||||
@ -30,8 +32,6 @@ import org.schabi.newpipe.util.ChannelTabHelper
|
||||
import org.schabi.newpipe.util.ExtractorHelper
|
||||
import org.schabi.newpipe.util.Localization
|
||||
import org.schabi.newpipe.util.NavigationHelper
|
||||
import java.util.function.BiConsumer
|
||||
import java.util.function.Consumer
|
||||
|
||||
/**
|
||||
* This class is used to cleanly separate the Service implementation (in
|
||||
@ -51,7 +51,7 @@ class MediaBrowserPlaybackPreparer(
|
||||
private val context: Context,
|
||||
private val setMediaSessionError: BiConsumer<String, Int>, // error string, error code
|
||||
private val clearMediaSessionError: Runnable,
|
||||
private val onPrepare: Consumer<Boolean>,
|
||||
private val onPrepare: Consumer<Boolean>
|
||||
) : PlaybackPreparer {
|
||||
private val database = NewPipeDatabase.getInstance(context)
|
||||
private var disposable: Disposable? = null
|
||||
@ -146,7 +146,7 @@ class MediaBrowserPlaybackPreparer(
|
||||
throw parseError(mediaId)
|
||||
}
|
||||
|
||||
return when (/*val uriType = */path.removeAt(0)) {
|
||||
return when (path.removeAt(0)) {
|
||||
ID_BOOKMARKS -> extractPlayQueueFromPlaylistMediaId(
|
||||
mediaId,
|
||||
path,
|
||||
@ -172,7 +172,7 @@ class MediaBrowserPlaybackPreparer(
|
||||
private fun extractPlayQueueFromPlaylistMediaId(
|
||||
mediaId: String,
|
||||
path: MutableList<String>,
|
||||
url: String?,
|
||||
url: String?
|
||||
): Single<PlayQueue> {
|
||||
if (path.isEmpty()) {
|
||||
throw parseError(mediaId)
|
||||
@ -185,10 +185,11 @@ class MediaBrowserPlaybackPreparer(
|
||||
}
|
||||
val playlistId = path[0].toLong()
|
||||
val index = path[1].toInt()
|
||||
return if (playlistType == ID_LOCAL)
|
||||
return if (playlistType == ID_LOCAL) {
|
||||
extractLocalPlayQueue(playlistId, index)
|
||||
else
|
||||
} else {
|
||||
extractRemotePlayQueue(playlistId, index)
|
||||
}
|
||||
}
|
||||
|
||||
ID_URL -> {
|
||||
@ -208,7 +209,7 @@ class MediaBrowserPlaybackPreparer(
|
||||
@Throws(ContentNotAvailableException::class)
|
||||
private fun extractPlayQueueFromHistoryMediaId(
|
||||
mediaId: String,
|
||||
path: List<String>,
|
||||
path: List<String>
|
||||
): Single<PlayQueue> {
|
||||
if (path.size != 1) {
|
||||
throw parseError(mediaId)
|
||||
@ -229,14 +230,14 @@ class MediaBrowserPlaybackPreparer(
|
||||
private fun extractPlayQueueFromInfoItemMediaId(
|
||||
mediaId: String,
|
||||
path: List<String>,
|
||||
url: String,
|
||||
url: String
|
||||
): Single<PlayQueue> {
|
||||
if (path.size != 2) {
|
||||
throw parseError(mediaId)
|
||||
}
|
||||
|
||||
val serviceId = path[1].toInt()
|
||||
return when (/*val infoItemType = */infoItemTypeFromString(path[0])) {
|
||||
return when (infoItemTypeFromString(path[0])) {
|
||||
InfoType.STREAM -> ExtractorHelper.getStreamInfo(serviceId, url, false)
|
||||
.map { SinglePlayQueue(it) }
|
||||
|
||||
|
||||
@ -30,9 +30,9 @@ import android.support.v4.media.session.MediaSessionCompat
|
||||
import android.util.Log
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import androidx.media.MediaBrowserServiceCompat
|
||||
import org.schabi.newpipe.BuildConfig
|
||||
import java.security.MessageDigest
|
||||
import java.security.NoSuchAlgorithmException
|
||||
import org.schabi.newpipe.BuildConfig
|
||||
|
||||
/**
|
||||
* Validates that the calling package is authorized to browse a [MediaBrowserServiceCompat].
|
||||
@ -94,18 +94,22 @@ internal class PackageValidator(context: Context) {
|
||||
val isCallerKnown = when {
|
||||
// If it's our own app making the call, allow it.
|
||||
callingUid == Process.myUid() -> true
|
||||
|
||||
// If the system is making the call, allow it.
|
||||
callingUid == Process.SYSTEM_UID -> true
|
||||
|
||||
// If the app was signed by the same certificate as the platform itself, also allow it.
|
||||
callerSignature == platformSignature -> true
|
||||
/**
|
||||
|
||||
/*
|
||||
* [MEDIA_CONTENT_CONTROL] permission is only available to system applications, and
|
||||
* while it isn't required to allow these apps to connect to a
|
||||
* [MediaBrowserServiceCompat], allowing this ensures optimal compatability with apps
|
||||
* such as Android TV and the Google Assistant.
|
||||
*/
|
||||
callerPackageInfo.permissions.contains(MEDIA_CONTENT_CONTROL) -> true
|
||||
/**
|
||||
|
||||
/*
|
||||
* If the calling app has a notification listener it is able to retrieve notifications
|
||||
* and can connect to an active [MediaSessionCompat].
|
||||
*
|
||||
@ -169,11 +173,10 @@ internal class PackageValidator(context: Context) {
|
||||
*/
|
||||
@Suppress("deprecation")
|
||||
@SuppressLint("PackageManagerGetSignatures")
|
||||
private fun getPackageInfo(callingPackage: String): PackageInfo? =
|
||||
packageManager.getPackageInfo(
|
||||
callingPackage,
|
||||
PackageManager.GET_SIGNATURES or PackageManager.GET_PERMISSIONS
|
||||
)
|
||||
private fun getPackageInfo(callingPackage: String): PackageInfo? = packageManager.getPackageInfo(
|
||||
callingPackage,
|
||||
PackageManager.GET_SIGNATURES or PackageManager.GET_PERMISSIONS
|
||||
)
|
||||
|
||||
/**
|
||||
* Gets the signature of a given package's [PackageInfo].
|
||||
@ -185,23 +188,21 @@ internal class PackageValidator(context: Context) {
|
||||
* returns `null` as the signature.
|
||||
*/
|
||||
@Suppress("deprecation")
|
||||
private fun getSignature(packageInfo: PackageInfo): String? =
|
||||
if (packageInfo.signatures == null || packageInfo.signatures!!.size != 1) {
|
||||
// Security best practices dictate that an app should be signed with exactly one (1)
|
||||
// signature. Because of this, if there are multiple signatures, reject it.
|
||||
null
|
||||
} else {
|
||||
val certificate = packageInfo.signatures!![0].toByteArray()
|
||||
getSignatureSha256(certificate)
|
||||
}
|
||||
private fun getSignature(packageInfo: PackageInfo): String? = if (packageInfo.signatures == null || packageInfo.signatures!!.size != 1) {
|
||||
// Security best practices dictate that an app should be signed with exactly one (1)
|
||||
// signature. Because of this, if there are multiple signatures, reject it.
|
||||
null
|
||||
} else {
|
||||
val certificate = packageInfo.signatures!![0].toByteArray()
|
||||
getSignatureSha256(certificate)
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the Android platform signing key signature. This key is never null.
|
||||
*/
|
||||
private fun getSystemSignature(): String =
|
||||
getPackageInfo(ANDROID_PLATFORM)?.let { platformInfo ->
|
||||
getSignature(platformInfo)
|
||||
} ?: throw IllegalStateException("Platform signature not found")
|
||||
private fun getSystemSignature(): String = getPackageInfo(ANDROID_PLATFORM)?.let { platformInfo ->
|
||||
getSignature(platformInfo)
|
||||
} ?: throw IllegalStateException("Platform signature not found")
|
||||
|
||||
/**
|
||||
* Creates a SHA-256 signature given a certificate byte array.
|
||||
|
||||
@ -3,7 +3,7 @@ package org.schabi.newpipe.player.playqueue
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.core.BackpressureStrategy
|
||||
import io.reactivex.rxjava3.core.Flowable
|
||||
import io.reactivex.rxjava3.subjects.BehaviorSubject
|
||||
import io.reactivex.rxjava3.subjects.PublishSubject
|
||||
import org.schabi.newpipe.player.playqueue.PlayQueueEvent.AppendEvent
|
||||
import org.schabi.newpipe.player.playqueue.PlayQueueEvent.ErrorEvent
|
||||
import org.schabi.newpipe.player.playqueue.PlayQueueEvent.InitEvent
|
||||
@ -36,7 +36,7 @@ abstract class PlayQueue internal constructor(
|
||||
private var streams = startWith.toMutableList()
|
||||
|
||||
@Transient
|
||||
private var eventBroadcast: BehaviorSubject<PlayQueueEvent>? = null
|
||||
private var eventBroadcast: PublishSubject<PlayQueueEvent>? = null
|
||||
|
||||
/**
|
||||
* Returns the play queue's update broadcast.
|
||||
@ -68,7 +68,7 @@ abstract class PlayQueue internal constructor(
|
||||
* Also starts a self reporter for logging if debug mode is enabled.
|
||||
*/
|
||||
fun init() {
|
||||
eventBroadcast = BehaviorSubject.create()
|
||||
eventBroadcast = PublishSubject.create()
|
||||
|
||||
broadcastReceiver =
|
||||
eventBroadcast!!
|
||||
|
||||
@ -10,6 +10,7 @@ import kotlin.io.path.div
|
||||
class BackupFileLocator(context: Context) {
|
||||
companion object {
|
||||
const val FILE_NAME_DB = "newpipe.db"
|
||||
|
||||
@Deprecated(
|
||||
"Serializing preferences with Java's ObjectOutputStream is vulnerable to injections",
|
||||
replaceWith = ReplaceWith("FILE_NAME_JSON_PREFS")
|
||||
|
||||
@ -45,7 +45,7 @@ class NotificationModeConfigFragment : Fragment() {
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
_binding = FragmentChannelsNotificationsBinding.inflate(inflater, container, false)
|
||||
return binding.root
|
||||
@ -90,6 +90,7 @@ class NotificationModeConfigFragment : Fragment() {
|
||||
toggleAll()
|
||||
true
|
||||
}
|
||||
|
||||
else -> super.onOptionsItemSelected(item)
|
||||
}
|
||||
}
|
||||
|
||||
@ -7,9 +7,9 @@ package org.schabi.newpipe.util
|
||||
|
||||
import android.content.Context
|
||||
import androidx.preference.PreferenceManager
|
||||
import java.util.regex.Matcher
|
||||
import org.schabi.newpipe.R
|
||||
import org.schabi.newpipe.ktx.getStringSafe
|
||||
import java.util.regex.Matcher
|
||||
|
||||
object FilenameUtils {
|
||||
private const val CHARSET_MOST_SPECIAL = "[\\n\\r|?*<\":\\\\>/']+"
|
||||
@ -31,10 +31,12 @@ object FilenameUtils {
|
||||
val defaultCharset = context.getString(R.string.default_file_charset_value)
|
||||
|
||||
val replacementChar = sharedPreferences.getStringSafe(
|
||||
context.getString(R.string.settings_file_replacement_character_key), "_"
|
||||
context.getString(R.string.settings_file_replacement_character_key),
|
||||
"_"
|
||||
)
|
||||
val selectedCharset = sharedPreferences.getStringSafe(
|
||||
context.getString(R.string.settings_file_charset_key), ""
|
||||
context.getString(R.string.settings_file_charset_key),
|
||||
""
|
||||
).ifEmpty { defaultCharset }
|
||||
|
||||
val charset = when (selectedCharset) {
|
||||
|
||||
@ -806,7 +806,7 @@ public final class ListHelper {
|
||||
final Locale preferredLanguage = Localization.getPreferredLocale(context);
|
||||
final boolean preferOriginalAudio =
|
||||
preferences.getBoolean(context.getString(R.string.prefer_original_audio_key),
|
||||
false);
|
||||
true);
|
||||
final boolean preferDescriptiveAudio =
|
||||
preferences.getBoolean(context.getString(R.string.prefer_descriptive_audio_key),
|
||||
false);
|
||||
|
||||
@ -2,13 +2,13 @@ package org.schabi.newpipe.util
|
||||
|
||||
import android.content.pm.PackageManager
|
||||
import androidx.core.content.pm.PackageInfoCompat
|
||||
import java.time.Instant
|
||||
import java.time.ZonedDateTime
|
||||
import java.time.format.DateTimeFormatter
|
||||
import org.schabi.newpipe.App
|
||||
import org.schabi.newpipe.error.ErrorInfo
|
||||
import org.schabi.newpipe.error.ErrorUtil.Companion.createNotification
|
||||
import org.schabi.newpipe.error.UserAction
|
||||
import java.time.Instant
|
||||
import java.time.ZonedDateTime
|
||||
import java.time.format.DateTimeFormatter
|
||||
|
||||
object ReleaseVersionUtil {
|
||||
// Public key of the certificate that is used in NewPipe release versions
|
||||
@ -26,7 +26,8 @@ object ReleaseVersionUtil {
|
||||
PackageInfoCompat.hasSignatures(app.packageManager, app.packageName, certificates, false)
|
||||
} catch (e: PackageManager.NameNotFoundException) {
|
||||
createNotification(
|
||||
app, ErrorInfo(e, UserAction.CHECK_FOR_NEW_APP_VERSION, "Could not find package info")
|
||||
app,
|
||||
ErrorInfo(e, UserAction.CHECK_FOR_NEW_APP_VERSION, "Could not find package info")
|
||||
)
|
||||
false
|
||||
}
|
||||
|
||||
@ -5,9 +5,9 @@
|
||||
|
||||
package org.schabi.newpipe.util.image
|
||||
|
||||
import kotlin.math.abs
|
||||
import org.schabi.newpipe.extractor.Image
|
||||
import org.schabi.newpipe.extractor.Image.ResolutionLevel
|
||||
import kotlin.math.abs
|
||||
|
||||
object ImageStrategy {
|
||||
// when preferredImageQuality is LOW or MEDIUM, images are sorted by how close their preferred
|
||||
@ -68,7 +68,7 @@ object ImageStrategy {
|
||||
val initialComparator =
|
||||
Comparator // the first step splits the images into groups of resolution levels
|
||||
.comparingInt { i: Image ->
|
||||
return@comparingInt when (i.estimatedResolutionLevel) {
|
||||
return@comparingInt when (i.estimatedResolutionLevel) {
|
||||
// avoid unknowns as much as possible
|
||||
ResolutionLevel.UNKNOWN -> 3
|
||||
|
||||
@ -92,6 +92,7 @@ object ImageStrategy {
|
||||
// the same number for those.
|
||||
val finalComparator = when (nonNoneQuality) {
|
||||
PreferredImageQuality.NONE -> initialComparator
|
||||
|
||||
PreferredImageQuality.LOW -> initialComparator.thenComparingDouble { image ->
|
||||
val pixelCount = estimatePixelCount(image, widthOverHeight)
|
||||
abs(pixelCount - BEST_LOW_H * BEST_LOW_H * widthOverHeight)
|
||||
|
||||
@ -6,8 +6,9 @@ class PoTokenException(message: String) : Exception(message)
|
||||
class BadWebViewException(message: String) : Exception(message)
|
||||
|
||||
fun buildExceptionForJsError(error: String): Exception {
|
||||
return if (error.contains("SyntaxError"))
|
||||
return if (error.contains("SyntaxError")) {
|
||||
BadWebViewException(error)
|
||||
else
|
||||
} else {
|
||||
PoTokenException(error)
|
||||
}
|
||||
}
|
||||
|
||||
@ -37,7 +37,9 @@ object PoTokenProviderImpl : PoTokenProvider {
|
||||
webViewBadImpl = true
|
||||
return null
|
||||
}
|
||||
|
||||
null -> throw e
|
||||
|
||||
else -> throw cause // includes PoTokenException
|
||||
}
|
||||
}
|
||||
@ -58,7 +60,6 @@ object PoTokenProviderImpl : PoTokenProvider {
|
||||
webPoTokenGenerator!!.isExpired()
|
||||
|
||||
if (shouldRecreate) {
|
||||
|
||||
val innertubeClientRequestInfo = InnertubeClientRequestInfo.ofWebClient()
|
||||
innertubeClientRequestInfo.clientInfo.clientVersion =
|
||||
YoutubeParsingHelper.getClientVersion()
|
||||
|
||||
@ -16,14 +16,14 @@ import io.reactivex.rxjava3.core.Single
|
||||
import io.reactivex.rxjava3.core.SingleEmitter
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import java.time.Instant
|
||||
import org.schabi.newpipe.BuildConfig
|
||||
import org.schabi.newpipe.DownloaderImpl
|
||||
import java.time.Instant
|
||||
|
||||
class PoTokenWebView private constructor(
|
||||
context: Context,
|
||||
// to be used exactly once only during initialization!
|
||||
private val generatorEmitter: SingleEmitter<PoTokenGenerator>,
|
||||
private val generatorEmitter: SingleEmitter<PoTokenGenerator>
|
||||
) : PoTokenGenerator {
|
||||
private val webView = WebView(context)
|
||||
private val disposables = CompositeDisposable() // used only during initialization
|
||||
@ -93,7 +93,7 @@ class PoTokenWebView private constructor(
|
||||
),
|
||||
"text/html",
|
||||
"utf-8",
|
||||
null,
|
||||
null
|
||||
)
|
||||
},
|
||||
this::onInitializationErrorCloseAndCancel
|
||||
@ -113,7 +113,7 @@ class PoTokenWebView private constructor(
|
||||
|
||||
makeBotguardServiceRequest(
|
||||
"https://www.youtube.com/api/jnn/v1/Create",
|
||||
"[ \"$REQUEST_KEY\" ]",
|
||||
"[ \"$REQUEST_KEY\" ]"
|
||||
) { responseBody ->
|
||||
val parsedChallengeData = parseChallengeData(responseBody)
|
||||
webView.evaluateJavascript(
|
||||
@ -156,7 +156,7 @@ class PoTokenWebView private constructor(
|
||||
}
|
||||
makeBotguardServiceRequest(
|
||||
"https://www.youtube.com/api/jnn/v1/GenerateIT",
|
||||
"[ \"$REQUEST_KEY\", \"$botguardResponse\" ]",
|
||||
"[ \"$REQUEST_KEY\", \"$botguardResponse\" ]"
|
||||
) { responseBody ->
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.d(TAG, "GenerateIT response: $responseBody")
|
||||
@ -179,16 +179,15 @@ class PoTokenWebView private constructor(
|
||||
//endregion
|
||||
|
||||
//region Obtaining poTokens
|
||||
override fun generatePoToken(identifier: String): Single<String> =
|
||||
Single.create { emitter ->
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.d(TAG, "generatePoToken() called with identifier $identifier")
|
||||
}
|
||||
runOnMainThread(emitter) {
|
||||
addPoTokenEmitter(identifier, emitter)
|
||||
val u8Identifier = stringToU8(identifier)
|
||||
webView.evaluateJavascript(
|
||||
"""try {
|
||||
override fun generatePoToken(identifier: String): Single<String> = Single.create { emitter ->
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.d(TAG, "generatePoToken() called with identifier $identifier")
|
||||
}
|
||||
runOnMainThread(emitter) {
|
||||
addPoTokenEmitter(identifier, emitter)
|
||||
val u8Identifier = stringToU8(identifier)
|
||||
webView.evaluateJavascript(
|
||||
"""try {
|
||||
identifier = "$identifier"
|
||||
u8Identifier = $u8Identifier
|
||||
poTokenU8 = obtainPoToken(webPoSignalOutput, integrityToken, u8Identifier)
|
||||
@ -200,10 +199,10 @@ class PoTokenWebView private constructor(
|
||||
$JS_INTERFACE.onObtainPoTokenResult(identifier, poTokenU8String)
|
||||
} catch (error) {
|
||||
$JS_INTERFACE.onObtainPoTokenError(identifier, error + "\n" + error.stack)
|
||||
}""",
|
||||
) {}
|
||||
}
|
||||
}"""
|
||||
) {}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called by the JavaScript snippet from [generatePoToken] when an error occurs in calling the
|
||||
@ -245,6 +244,7 @@ class PoTokenWebView private constructor(
|
||||
//endregion
|
||||
|
||||
//region Handling multiple emitters
|
||||
|
||||
/**
|
||||
* Adds the ([identifier], [emitter]) pair to the [poTokenEmitters] list. This makes it so that
|
||||
* multiple poToken requests can be generated invparallel, and the results will be notified to
|
||||
@ -283,6 +283,7 @@ class PoTokenWebView private constructor(
|
||||
//endregion
|
||||
|
||||
//region Utils
|
||||
|
||||
/**
|
||||
* Makes a POST request to [url] with the given [data] by setting the correct headers. Calls
|
||||
* [onInitializationErrorCloseAndCancel] in case of any network errors and also if the response
|
||||
@ -294,7 +295,7 @@ class PoTokenWebView private constructor(
|
||||
private fun makeBotguardServiceRequest(
|
||||
url: String,
|
||||
data: String,
|
||||
handleResponseBody: (String) -> Unit,
|
||||
handleResponseBody: (String) -> Unit
|
||||
) {
|
||||
disposables.add(
|
||||
Single.fromCallable {
|
||||
@ -306,7 +307,7 @@ class PoTokenWebView private constructor(
|
||||
"Accept" to listOf("application/json"),
|
||||
"Content-Type" to listOf("application/json+protobuf"),
|
||||
"x-goog-api-key" to listOf(GOOGLE_API_KEY),
|
||||
"x-user-agent" to listOf("grpc-web-javascript/0.1"),
|
||||
"x-user-agent" to listOf("grpc-web-javascript/0.1")
|
||||
),
|
||||
data.toByteArray()
|
||||
)
|
||||
@ -363,6 +364,7 @@ class PoTokenWebView private constructor(
|
||||
|
||||
companion object : PoTokenGenerator.Factory {
|
||||
private val TAG = PoTokenWebView::class.simpleName
|
||||
|
||||
// Public API key used by BotGuard, which has been got by looking at BotGuard requests
|
||||
private const val GOOGLE_API_KEY = "AIzaSyDyT5W0Jh49F30Pqqtyfdf7pDLFKLJoAnw" // NOSONAR
|
||||
private const val REQUEST_KEY = "O43z0dpjhgX20SCx4KAo"
|
||||
@ -370,14 +372,13 @@ class PoTokenWebView private constructor(
|
||||
"AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.3"
|
||||
private const val JS_INTERFACE = "PoTokenWebView"
|
||||
|
||||
override fun newPoTokenGenerator(context: Context): Single<PoTokenGenerator> =
|
||||
Single.create { emitter ->
|
||||
runOnMainThread(emitter) {
|
||||
val potWv = PoTokenWebView(context, emitter)
|
||||
potWv.loadHtmlAndObtainBotguard(context)
|
||||
emitter.setDisposable(potWv.disposables)
|
||||
}
|
||||
override fun newPoTokenGenerator(context: Context): Single<PoTokenGenerator> = Single.create { emitter ->
|
||||
runOnMainThread(emitter) {
|
||||
val potWv = PoTokenWebView(context, emitter)
|
||||
potWv.loadHtmlAndObtainBotguard(context)
|
||||
emitter.setDisposable(potWv.disposables)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs [runnable] on the main thread using `Handler(Looper.getMainLooper()).post()`, and
|
||||
@ -385,7 +386,7 @@ class PoTokenWebView private constructor(
|
||||
*/
|
||||
private fun runOnMainThread(
|
||||
emitterIfPostFails: SingleEmitter<out Any>,
|
||||
runnable: Runnable,
|
||||
runnable: Runnable
|
||||
) {
|
||||
if (!Handler(Looper.getMainLooper()).post(runnable)) {
|
||||
emitterIfPostFails.onError(PoTokenException("Could not run on main thread"))
|
||||
|
||||
@ -53,8 +53,10 @@ class TimestampLongPressClickableSpan(
|
||||
when (relatedInfoService) {
|
||||
ServiceList.YouTube ->
|
||||
return relatedStreamUrl + "&t=" + timestampMatchDTO.seconds()
|
||||
|
||||
ServiceList.SoundCloud, ServiceList.MediaCCC ->
|
||||
return relatedStreamUrl + "#t=" + timestampMatchDTO.seconds()
|
||||
|
||||
ServiceList.PeerTube ->
|
||||
return relatedStreamUrl + "?start=" + timestampMatchDTO.seconds()
|
||||
}
|
||||
|
||||
@ -1,249 +0,0 @@
|
||||
/*
|
||||
* Copyright 2018 Mauricio Colli <mauriciocolli@outlook.com>
|
||||
* CollapsibleView.java is part of NewPipe
|
||||
*
|
||||
* License: GPL-3.0+
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.schabi.newpipe.views;
|
||||
|
||||
import static org.schabi.newpipe.MainActivity.DEBUG;
|
||||
import static java.lang.annotation.RetentionPolicy.SOURCE;
|
||||
|
||||
import android.animation.ValueAnimator;
|
||||
import android.content.Context;
|
||||
import android.os.Parcelable;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.Log;
|
||||
import android.widget.LinearLayout;
|
||||
|
||||
import androidx.annotation.IntDef;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.evernote.android.state.State;
|
||||
import com.livefront.bridge.Bridge;
|
||||
|
||||
import org.schabi.newpipe.ktx.ViewUtils;
|
||||
|
||||
import java.lang.annotation.Retention;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* A view that can be fully collapsed and expanded.
|
||||
*/
|
||||
public class CollapsibleView extends LinearLayout {
|
||||
private static final String TAG = CollapsibleView.class.getSimpleName();
|
||||
|
||||
private static final int ANIMATION_DURATION = 420;
|
||||
|
||||
public static final int COLLAPSED = 0;
|
||||
public static final int EXPANDED = 1;
|
||||
|
||||
@State
|
||||
@ViewMode
|
||||
int currentState = COLLAPSED;
|
||||
private boolean readyToChangeState;
|
||||
|
||||
private int targetHeight = -1;
|
||||
private ValueAnimator currentAnimator;
|
||||
private final List<StateListener> listeners = new ArrayList<>();
|
||||
|
||||
public CollapsibleView(final Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
public CollapsibleView(final Context context, @Nullable final AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
}
|
||||
|
||||
public CollapsibleView(final Context context, @Nullable final AttributeSet attrs,
|
||||
final int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
}
|
||||
|
||||
public CollapsibleView(final Context context, final AttributeSet attrs, final int defStyleAttr,
|
||||
final int defStyleRes) {
|
||||
super(context, attrs, defStyleAttr, defStyleRes);
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Collapse/expand logic
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
/**
|
||||
* This method recalculates the height of this view so it <b>must</b> be called when
|
||||
* some child changes (e.g. add new views, change text).
|
||||
*/
|
||||
public void ready() {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, getDebugLogString("ready() called"));
|
||||
}
|
||||
|
||||
measure(MeasureSpec.makeMeasureSpec(getWidth(), MeasureSpec.AT_MOST),
|
||||
MeasureSpec.UNSPECIFIED);
|
||||
targetHeight = getMeasuredHeight();
|
||||
|
||||
getLayoutParams().height = currentState == COLLAPSED ? 0 : targetHeight;
|
||||
requestLayout();
|
||||
broadcastState();
|
||||
|
||||
readyToChangeState = true;
|
||||
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, getDebugLogString("ready() *after* measuring"));
|
||||
}
|
||||
}
|
||||
|
||||
public void collapse() {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, getDebugLogString("collapse() called"));
|
||||
}
|
||||
|
||||
if (!readyToChangeState) {
|
||||
return;
|
||||
}
|
||||
|
||||
final int height = getHeight();
|
||||
if (height == 0) {
|
||||
setCurrentState(COLLAPSED);
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentAnimator != null && currentAnimator.isRunning()) {
|
||||
currentAnimator.cancel();
|
||||
}
|
||||
currentAnimator = ViewUtils.animateHeight(this, ANIMATION_DURATION, 0);
|
||||
|
||||
setCurrentState(COLLAPSED);
|
||||
}
|
||||
|
||||
public void expand() {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, getDebugLogString("expand() called"));
|
||||
}
|
||||
|
||||
if (!readyToChangeState) {
|
||||
return;
|
||||
}
|
||||
|
||||
final int height = getHeight();
|
||||
if (height == this.targetHeight) {
|
||||
setCurrentState(EXPANDED);
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentAnimator != null && currentAnimator.isRunning()) {
|
||||
currentAnimator.cancel();
|
||||
}
|
||||
currentAnimator = ViewUtils.animateHeight(this, ANIMATION_DURATION, this.targetHeight);
|
||||
setCurrentState(EXPANDED);
|
||||
}
|
||||
|
||||
public void switchState() {
|
||||
if (!readyToChangeState) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentState == COLLAPSED) {
|
||||
expand();
|
||||
} else {
|
||||
collapse();
|
||||
}
|
||||
}
|
||||
|
||||
@ViewMode
|
||||
public int getCurrentState() {
|
||||
return currentState;
|
||||
}
|
||||
|
||||
public void setCurrentState(@ViewMode final int currentState) {
|
||||
this.currentState = currentState;
|
||||
broadcastState();
|
||||
}
|
||||
|
||||
public void broadcastState() {
|
||||
for (final StateListener listener : listeners) {
|
||||
listener.onStateChanged(currentState);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a listener which will be listening for changes in this view (i.e. collapsed or expanded).
|
||||
* @param listener {@link StateListener} to be added
|
||||
*/
|
||||
public void addListener(final StateListener listener) {
|
||||
if (listeners.contains(listener)) {
|
||||
throw new IllegalStateException("Trying to add the same listener multiple times");
|
||||
}
|
||||
|
||||
listeners.add(listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a listener so it doesn't receive more state changes.
|
||||
* @param listener {@link StateListener} to be removed
|
||||
*/
|
||||
public void removeListener(final StateListener listener) {
|
||||
listeners.remove(listener);
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// State Saving
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public Parcelable onSaveInstanceState() {
|
||||
return Bridge.saveInstanceState(this, super.onSaveInstanceState());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRestoreInstanceState(final Parcelable state) {
|
||||
super.onRestoreInstanceState(Bridge.restoreInstanceState(this, state));
|
||||
|
||||
ready();
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Internal
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
public String getDebugLogString(final String description) {
|
||||
return String.format("%-100s → %s",
|
||||
description, "readyToChangeState = [" + readyToChangeState + "], "
|
||||
+ "currentState = [" + currentState + "], "
|
||||
+ "targetHeight = [" + targetHeight + "], "
|
||||
+ "mW x mH = [" + getMeasuredWidth() + "x" + getMeasuredHeight() + "], "
|
||||
+ "W x H = [" + getWidth() + "x" + getHeight() + "]");
|
||||
}
|
||||
|
||||
@Retention(SOURCE)
|
||||
@IntDef({COLLAPSED, EXPANDED})
|
||||
public @interface ViewMode { }
|
||||
|
||||
/**
|
||||
* Simple interface used for listening state changes of the {@link CollapsibleView}.
|
||||
*/
|
||||
public interface StateListener {
|
||||
/**
|
||||
* Called when the state changes.
|
||||
*
|
||||
* @param newState the state that the {@link CollapsibleView} transitioned to,<br/>
|
||||
* it's an integer being either {@link #COLLAPSED} or {@link #EXPANDED}
|
||||
*/
|
||||
void onStateChanged(@ViewMode int newState);
|
||||
}
|
||||
}
|
||||
@ -52,8 +52,9 @@ class PlayerFastSeekOverlay(context: Context, attrs: AttributeSet?) :
|
||||
private var initTap: Boolean = false
|
||||
|
||||
override fun onDoubleTapStarted(portion: DisplayPortion) {
|
||||
if (DEBUG)
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "onDoubleTapStarted called with portion = [$portion]")
|
||||
}
|
||||
|
||||
initTap = false
|
||||
|
||||
@ -64,7 +65,7 @@ class PlayerFastSeekOverlay(context: Context, attrs: AttributeSet?) :
|
||||
val shouldForward: Boolean =
|
||||
performListener?.getFastSeekDirection(portion)?.directionAsBoolean ?: return
|
||||
|
||||
if (DEBUG)
|
||||
if (DEBUG) {
|
||||
Log.d(
|
||||
TAG,
|
||||
"onDoubleTapProgressDown called with " +
|
||||
@ -72,6 +73,7 @@ class PlayerFastSeekOverlay(context: Context, attrs: AttributeSet?) :
|
||||
"wasForwarding = [$wasForwarding], " +
|
||||
"initTap = [$initTap], "
|
||||
)
|
||||
}
|
||||
|
||||
/*
|
||||
* Check if a initial tap occurred or if direction was switched
|
||||
@ -97,8 +99,9 @@ class PlayerFastSeekOverlay(context: Context, attrs: AttributeSet?) :
|
||||
}
|
||||
|
||||
override fun onDoubleTapFinished() {
|
||||
if (DEBUG)
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "onDoubleTapFinished called with initTap = [$initTap]")
|
||||
}
|
||||
|
||||
if (initTap) performListener?.onDoubleTapEnd()
|
||||
initTap = false
|
||||
@ -112,8 +115,10 @@ class PlayerFastSeekOverlay(context: Context, attrs: AttributeSet?) :
|
||||
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.id,
|
||||
if (forward) END else START,
|
||||
PARENT_ID,
|
||||
if (forward) END else START
|
||||
)
|
||||
secondsView.startAnimation()
|
||||
applyTo(rootConstraintLayout)
|
||||
@ -123,6 +128,7 @@ class PlayerFastSeekOverlay(context: Context, attrs: AttributeSet?) :
|
||||
interface PerformListener {
|
||||
fun onDoubleTap()
|
||||
fun onDoubleTapEnd()
|
||||
|
||||
/**
|
||||
* Determines if the playback should forward/rewind or do nothing.
|
||||
*/
|
||||
@ -132,7 +138,7 @@ class PlayerFastSeekOverlay(context: Context, attrs: AttributeSet?) :
|
||||
enum class FastSeekDirection(val directionAsBoolean: Boolean?) {
|
||||
NONE(null),
|
||||
FORWARD(true),
|
||||
BACKWARD(false);
|
||||
BACKWARD(false)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -29,7 +29,9 @@ class SecondsView(context: Context, attrs: AttributeSet?) : LinearLayout(context
|
||||
var seconds: Int = 0
|
||||
set(value) {
|
||||
binding.tvSeconds.text = context.resources.getQuantityString(
|
||||
R.plurals.seconds, value, value
|
||||
R.plurals.seconds,
|
||||
value,
|
||||
value
|
||||
)
|
||||
field = value
|
||||
}
|
||||
|
||||
@ -1,13 +1,13 @@
|
||||
package us.shandian.giga.get
|
||||
|
||||
import android.os.Parcelable
|
||||
import java.io.Serializable
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import org.schabi.newpipe.extractor.MediaFormat
|
||||
import org.schabi.newpipe.extractor.stream.AudioStream
|
||||
import org.schabi.newpipe.extractor.stream.Stream
|
||||
import org.schabi.newpipe.extractor.stream.SubtitlesStream
|
||||
import org.schabi.newpipe.extractor.stream.VideoStream
|
||||
import java.io.Serializable
|
||||
|
||||
@Parcelize
|
||||
class MissionRecoveryInfo(
|
||||
@ -25,16 +25,19 @@ class MissionRecoveryInfo(
|
||||
isDesired2 = false
|
||||
kind = 'a'
|
||||
}
|
||||
|
||||
is VideoStream -> {
|
||||
desired = stream.getResolution()
|
||||
isDesired2 = stream.isVideoOnly()
|
||||
kind = 'v'
|
||||
}
|
||||
|
||||
is SubtitlesStream -> {
|
||||
desired = stream.languageTag
|
||||
isDesired2 = stream.isAutoGenerated
|
||||
kind = 's'
|
||||
}
|
||||
|
||||
else -> throw RuntimeException("Unknown stream kind")
|
||||
}
|
||||
}
|
||||
@ -48,14 +51,17 @@ class MissionRecoveryInfo(
|
||||
str.append("audio")
|
||||
info = "bitrate=$desiredBitrate"
|
||||
}
|
||||
|
||||
'v' -> {
|
||||
str.append("video")
|
||||
info = "quality=$desired videoOnly=$isDesired2"
|
||||
}
|
||||
|
||||
's' -> {
|
||||
str.append("subtitles")
|
||||
info = "language=$desired autoGenerated=$isDesired2"
|
||||
}
|
||||
|
||||
else -> {
|
||||
info = ""
|
||||
str.append("other")
|
||||
|
||||
@ -583,7 +583,7 @@
|
||||
<string name="peertube_instance_add_fail">Serveri təsdiqləmək mümkün olmadı</string>
|
||||
<string name="peertube_instance_url_help">%s-də bəyəndiyiniz serverləri tapın</string>
|
||||
<string name="show_hold_to_append_summary">Video \"Təfsilatlar\" səhifəsində fon və ya ani görüntü düyməsin basarkən ipucu göstər</string>
|
||||
<string name="caption_setting_description">Oynadıcı titr mətn miqyasını və arxa fon üslublarını dəyişdir. Effektiv olması üçün tətbiqi yenidən başlatmaq tələb olunur</string>
|
||||
<string name="caption_setting_description">Oynadıcı titr mətn miqyasını və arxa plan üslublarını dəyişdir. Effektiv olması üçün tətbiqi yenidən başlatmaq tələb olunur</string>
|
||||
<string name="error_occurred_detail">Xəta baş verdi: %1$s</string>
|
||||
<string name="invalid_file">Fayl mövcud deyil, yaxud oxumaq və ya yazmaq icazəsi yoxdur</string>
|
||||
<string name="parsing_error">Veb saytı təhlil etmək alınmadı</string>
|
||||
|
||||
@ -831,4 +831,5 @@
|
||||
<string name="trending_movies">Трэнды – фільмы і перадачы</string>
|
||||
<string name="unsupported_content_in_country">Гэты кантэнт недаступны для цяперашняй краіны кантэнту.\n\nЯе можна змяніць праз «Налады > Кантэнт > Прадвызначаная краіна кантэнту».</string>
|
||||
<string name="migration_info_7_8_message">21 ліпеня 2025 года YouTube спыніў падтрымку аб\'яднанай старонкі трэндаў. NewPipe замяніў старонку трэндаў на трэнды трансляцый.\n\nТаксама можна выбраць іншыя старонкі трэндаў праз «Налады > Кантэнт > Змесціва галоўнай старонкі».</string>
|
||||
<string name="migration_info_7_8_title">Аб\'яднаныя трэнды YouTube выдалены</string>
|
||||
</resources>
|
||||
|
||||
@ -307,4 +307,8 @@
|
||||
<string name="notification_scale_to_square_image_summary">বিজ্ঞপ্তিতে প্রদর্শিত ভিডিও থাম্বনেল 16:9 থেকে 1:1 অনুপাতের করুন (বিকৃতি দেখা যেতে পারে)</string>
|
||||
<string name="notification_action_shuffle">অদলবদল</string>
|
||||
<string name="notification_action_nothing">কিছু না</string>
|
||||
<string name="yes">হ্যাঁ</string>
|
||||
<string name="no">না</string>
|
||||
<string name="search_with_service_name">সার্চ</string>
|
||||
<string name="search_with_service_name_and_filter">খুঁজুন</string>
|
||||
</resources>
|
||||
|
||||
@ -835,7 +835,7 @@
|
||||
<string name="trending_movies">Beliebte Filme und Shows</string>
|
||||
<string name="trending_music">Beliebte Musik</string>
|
||||
<string name="trending_podcasts">Beliebte Podcasts</string>
|
||||
<string name="migration_info_7_8_title">YouTube hat den geteilten Feed entfernt</string>
|
||||
<string name="migration_info_7_8_title">YouTube hat die kombinierten „beliebten Seiten“ entfernt</string>
|
||||
<string name="migration_info_7_8_message">YouTube hat die kombinierte Trending-Seite ab dem 21. Juli 2025 eingestellt. NewPipe hat die Standard-Trending-Seite durch die Trending-Livestreams ersetzt.\n\nDu kannst auch verschiedene Trendseiten unter „Einstellungen > Inhalt > Inhalt der Hauptseite“ auswählen.</string>
|
||||
<string name="permission_display_over_apps_message">Um den Pop-up-Player zu verwenden, bitte in den folgenden Android-Einstellungen %1$s auswählen und %2$s aktivieren.</string>
|
||||
<string name="permission_display_over_apps_permission_name">„Über anderen Apps einblenden“</string>
|
||||
|
||||
@ -74,7 +74,7 @@
|
||||
<string name="enable_watch_history_summary">देखे गए वीडियोज़ की सूची रखें</string>
|
||||
<string name="resume_on_audio_focus_gain_title">प्लेबैक फिर से शुरू करें</string>
|
||||
<string name="resume_on_audio_focus_gain_summary">रुकावटें (जैसे कि फ़ोन कॉल) खत्म होने के बाद वीडियो प्ले जारी रखें</string>
|
||||
<string name="show_next_and_similar_title">\'अगले\' और \'सबंधित\' वीडियो दिखाएं</string>
|
||||
<string name="show_next_and_similar_title">\'अगला\' और \'संबंधित\' वीडियो दिखाएं</string>
|
||||
<string name="show_hold_to_append_title">\"कतार में जोड़ने के लिए स्पर्श बनाये रखें\" दिखाएं</string>
|
||||
<string name="show_hold_to_append_summary">जब बैकग्राउंड और पॉपअप बटन वीडियो के विवरण पन्ने में दबाई जाए तो सलाह दिखाएं</string>
|
||||
<string name="unsupported_url">असमर्थित URL</string>
|
||||
@ -93,7 +93,7 @@
|
||||
<string name="disabled">बंद किया</string>
|
||||
<string name="clear">साफ करें</string>
|
||||
<string name="best_resolution">उत्तम रिजॉल्युशन</string>
|
||||
<string name="undo">वापिस</string>
|
||||
<string name="undo">अन-डू करें</string>
|
||||
<string name="play_all">सभी प्ले करें</string>
|
||||
<string name="notification_channel_name">न्यूपाइप की नोटीफिकेशन</string>
|
||||
<string name="notification_channel_description">न्यूपाइप के प्लेयर के लिए नोटीफिकेशन</string>
|
||||
@ -104,7 +104,7 @@
|
||||
<string name="parsing_error">वैबसाइट parse नहीं हो सकी</string>
|
||||
<string name="content_not_available">विषय वस्तु उपलब्ध नहीं है</string>
|
||||
<string name="could_not_setup_download_menu">डाउनलोड मेनू स्थापित नहीं किया जा सका</string>
|
||||
<string name="app_ui_crash">APP/UI करैश हो गई</string>
|
||||
<string name="app_ui_crash">ऐप/UI करैश हो गई</string>
|
||||
<string name="player_stream_failure">इस वीडियो को चलाने में असफल हुए</string>
|
||||
<string name="player_unrecoverable_failure">अनचाही वीडियो प्लेयर त्रुटी आयी है</string>
|
||||
<string name="player_recoverable_failure">वीडियो प्लेयर त्रुटी से ठीक हो रहा है</string>
|
||||
@ -391,7 +391,7 @@
|
||||
<string name="clear_playback_states_title">प्लेबैक स्थानों को मिटाएं</string>
|
||||
<string name="clear_playback_states_summary">सारे प्लेबैक स्थानों को मिटाता है</string>
|
||||
<string name="delete_playback_states_alert">सारे प्लेबैक स्थानों को मिटाएं\?</string>
|
||||
<string name="enable_disposed_exceptions_summary">निपटान के बाद खंड या गतिविधि जीवन चक्र के बाहर अविभाज्य आरएक्स अपवादों की रिपोर्टिंग को बलपूर्वक लागू करें</string>
|
||||
<string name="enable_disposed_exceptions_summary">हैंडलिंग के बाद फ्रैगमेंट या एक्टिविटी लूप के बाहर अनहैंडल्ड Rx एक्सेप्शन की रिपोर्टिंग को बलपूर्वक लागू करें</string>
|
||||
<string name="import_soundcloud_instructions">साउंडक्लाउड प्रोफाइल निर्यात करने के लिए आईडी या युआरएल दीजिये:
|
||||
\n
|
||||
\n1. अपने वेब ब्राउज़र में \"डेस्कटॉप मोड\" चालू करें (वेबसाइट मोबाइल उपकरणों के लिए उपलब्ध नहीं है)
|
||||
@ -406,7 +406,7 @@
|
||||
<string name="grid">ग्रिड</string>
|
||||
<string name="auto">ऑटो</string>
|
||||
<string name="show_error">त्रुटि दिखाएं</string>
|
||||
<string name="error_http_unsupported_range">सर्वर मल्टी थ्रेडेड डाउनलोड स्वीकार नहीं करता, पुनः कोशिश करे @string/msg_threads = 1 के साथ</string>
|
||||
<string name="error_http_unsupported_range">सर्वर मल्टी थ्रेडेड डाउनलोड स्वीकार नहीं करता, @string/msg_threads = 1 के साथ पुनः कोशिश करें</string>
|
||||
<string name="downloads_storage_use_saf_summary">\'स्टोरेज एक्सेस फ्रेमवर्क\' आपको बाहरी एसडी कार्ड पर डाउनलोड करने देता है</string>
|
||||
<string name="drawer_header_description">सेवा चुनें, वर्तमान चुनाव :</string>
|
||||
<string name="default_kiosk_page_summary">डिफ़ॉल्ट कियोस्क</string>
|
||||
@ -502,7 +502,7 @@
|
||||
<string name="infinite_videos">अनगिनत विडीओज़</string>
|
||||
<string name="more_than_100_videos">100+ विडीओज़</string>
|
||||
<string name="description_tab_description">विवरण</string>
|
||||
<string name="related_items_tab_description">संबंधित स्ट्रीमस</string>
|
||||
<string name="related_items_tab_description">संबंधित आइटम्स</string>
|
||||
<string name="comments_tab_description">टिप्पणियाँ</string>
|
||||
<string name="error_report_open_github_notice">कृपया जांचें लें कि क्या आपके क्रैश पर चर्चा करने वाला मुद्दा पहले से मौजूद है। डुप्लिकेट टिकट बनाते समय, आप हमसे समय लेते हैं जो हम वास्तविक बग को ठीक करने के लिए खर्च कर सकते हैं।</string>
|
||||
<string name="error_report_open_issue_button_text">गिटहब पर रिपोर्ट करें</string>
|
||||
@ -826,4 +826,26 @@
|
||||
<string name="feed_group_page_summary">चैनल समूह पेज</string>
|
||||
<string name="channel_tab_likes">पसंद</string>
|
||||
<string name="share_playlist_as_youtube_temporary_playlist">यूट्यूब अस्थायी प्लेलिस्ट के रूप में साझा करें</string>
|
||||
<string name="entry_deleted">एंटरी मिटा दी गई</string>
|
||||
<string name="delete_file">फाईल डिलीट करें</string>
|
||||
<string name="delete_entry">एंटरी मिटाऐं</string>
|
||||
<string name="short_thousand">%sहज़ार</string>
|
||||
<string name="permission_display_over_apps_message">पॉपअप प्लेयर इस्तेमाल करने के लिए, कृपया नीचे दिए गए Android सेटिंग्स मेनू में %1$s चुनें और %2$s चालू करें।</string>
|
||||
<string name="permission_display_over_apps_permission_name">“अन्य ऐप्स पर डिस्प्ले की अनुमति दें”</string>
|
||||
<string name="short_million">%sमिलीअन</string>
|
||||
<string name="short_billion">%sअरब</string>
|
||||
<string name="account_terminated_service_provides_reason">अकाउंट बंद कर दिया गया\n\n%1$s यह कारण बताता है: %2$s</string>
|
||||
<string name="migration_info_6_7_title">साउंडक्लाउड टॉप 50 पेज हटा दिया गया</string>
|
||||
<string name="migration_info_6_7_message">साउंडक्लाउड ने ओरिजिनल टॉप 50 चार्ट बंद कर दिए हैं। इससे जुड़ा टैब आपके मेन पेज से हटा दिया गया है।</string>
|
||||
<string name="migration_info_7_8_title">YouTube कंबाइंड ट्रेंडिंग हटा दी गई</string>
|
||||
<string name="migration_info_7_8_message">YouTube ने 21 जुलाई 2025 से कंबाइंड ट्रेंडिंग पेज बंद कर दिया है। NewPipe ने डिफ़ॉल्ट ट्रेंडिंग पेज को ट्रेंडिंग लाइवस्ट्रीम से बदल दिया है।\n\nआप \"सेटिंग्स > कंटेंट > मेन पेज कंटेंट\" में अलग-अलग ट्रेंडिंग पेज भी चुन सकते हैं।</string>
|
||||
<string name="trending_gaming">गेमिंग ट्रेंडस</string>
|
||||
<string name="trending_podcasts">ट्रेंडिंग पॉडकास्ट</string>
|
||||
<string name="trending_movies">ट्रेंडिंग फिल्में और शो</string>
|
||||
<string name="trending_music">ट्रेंडिंग संगीत</string>
|
||||
<string name="player_http_403">पले करते समय सर्वर से HTTP error 403 मिला, शायद स्ट्रीमिंग URL एक्सपायर होने या IP बैन की वजह से हुआ</string>
|
||||
<string name="player_http_invalid_status">पले करते समय सर्वर से HTTP error %1$s मिला</string>
|
||||
<string name="youtube_player_http_403">पले करते समय सर्वर से HTTP error 403 मिला, जो शायद IP बैन या स्ट्रीमिंग URL डीओबफस्केशन की दिक्कतों की वजह से हुआ है</string>
|
||||
<string name="sign_in_confirm_not_bot_error">%1$s ने डेटा देने से मना कर दिया, और यह कन्फर्म करने के लिए लॉगिन मांगा कि रिक्वेस्ट करने वाला बोट नहीं है।\n\nहो सकता है कि %1$s ने आपके IP को कुछ समय के लिए बैन कर दिया हो, आप कुछ समय इंतज़ार कर सकते हैं या किसी दूसरे IP पर स्विच कर सकते हैं (जैसे VPN ऑन/ऑफ करके, या WiFi से मोबाइल डेटा पर स्विच करके)।</string>
|
||||
<string name="unsupported_content_in_country">यह कंटेंट अभी चुने गए देश के कंटेंट के लिए उपलब्ध नहीं है।\n\n\"सेटिंग्स > कंटेंट > डिफ़ॉल्ट कंटेंट देश\" से अपना चुनाव बदलें।</string>
|
||||
</resources>
|
||||
|
||||
@ -816,7 +816,7 @@
|
||||
<string name="entry_deleted">Bejegyzés törölve</string>
|
||||
<string name="account_terminated_service_provides_reason">Fiók megszüntetve\n\n%1$s az alábbi ok miatt: %2$s</string>
|
||||
<string name="player_http_403">A lejátszás közben a kiszolgáló 403-as HTTP-hibát adott vissza, valószínűleg a közvetítési hivatkozás érvényessége lejárt vagy a IP-tiltás miatt</string>
|
||||
<string name="player_http_invalid_status">HTTP-hiba %1$s érkezett a kiszolgáltól a lejátszás közben</string>
|
||||
<string name="player_http_invalid_status">HTTP-hiba (%1$s) érkezett a kiszolgálótól a lejátszás során</string>
|
||||
<string name="youtube_player_http_403">HTTP 403-as hiba érkezett a kiszolgálótól a lejátszás közben, valószínűleg IP-tiltás vagy a közvetítési hivatkozás feloldási problémák miatt</string>
|
||||
<string name="sign_in_confirm_not_bot_error">%1$s visszautasította az adatok szolgáltatását, és bejelentkezést kér annak megerősítésére, hogy a kérés nem robot által érkezik.\n\nElőfordulhat, hogy az IP-címét ideiglenesen letiltotta %1$s, várhat egy keveset, vagy váltson egy másik IP-címre (például VPN be-/kikapcsolásával, vagy Wi-Fi-ről mobiladat-forgalomra váltva).</string>
|
||||
<string name="unsupported_content_in_country">Ez a tartalom a jelenleg kiválasztott tartalom országában nem elérhető.\n\nVáltoztassa meg a „Beállítások > Tartalom >Tartalom alapértelmezett országa” menüpontban.</string>
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="main_bg_subtitle">Pigia la lente per inziaa.</string>
|
||||
<string name="channels">Canai</string>
|
||||
</resources>
|
||||
|
||||
@ -29,7 +29,7 @@
|
||||
<string name="fragment_feed_title">ਨਵਾਂ ਕੀ ਹੈ</string>
|
||||
<string name="controls_background_title">ਬੈਕਗ੍ਰਾਊਂਡ</string>
|
||||
<string name="controls_popup_title">ਪੌਪ-ਅਪ</string>
|
||||
<string name="controls_add_to_playlist_title">ਵਿੱਚ ਸ਼ਾਮਿਲ ਕਰੋ</string>
|
||||
<string name="controls_add_to_playlist_title">ਦੇ ਵਿੱਚ ਜੋੜ੍ਹੋ</string>
|
||||
<string name="download_path_title">ਵੀਡੀਓ ਲਈ ਡਾਊਨਲੋਡ ਫ਼ੋਲਡਰ</string>
|
||||
<string name="download_path_summary">ਡਾਊਨਲੋਡ ਕੀਤੀਆਂ ਵੀਡੀਓ ਫ਼ਾਈਲਾਂ ਇੱਥੇ ਜਮ੍ਹਾਂ ਹੁੰਦੀਆਂ ਹਨ</string>
|
||||
<string name="download_path_dialog_title">ਵੀਡੀਓ ਫ਼ਾਈਲਾਂ ਲਈ ਡਾਊਨਲੋਡ ਫ਼ੋਲਡਰ ਚੁਣੋ</string>
|
||||
@ -93,7 +93,7 @@
|
||||
<string name="disabled">ਬੰਦ ਕੀਤਾ</string>
|
||||
<string name="clear">ਸਾਫ ਕਰੋ</string>
|
||||
<string name="best_resolution">ਵਧੀਆ ਰੈਜ਼ੋਲਿਊਸ਼ਨ</string>
|
||||
<string name="undo">ਵਾਪਿਸ</string>
|
||||
<string name="undo">ਅਣ-ਕੀਤਾ ਕਰੋ</string>
|
||||
<string name="play_all">ਸਾਰੇ ਚਲਾਓ</string>
|
||||
<string name="always">ਹਮੇਸ਼ਾਂ</string>
|
||||
<string name="just_once">ਸਿਰਫ਼ ਇਸ ਬਾਰ</string>
|
||||
@ -184,8 +184,7 @@
|
||||
<string name="msg_wait">ਕ੍ਰਿਪਾ ਕਰਕੇ ਉਡੀਕ ਕਰੋ…</string>
|
||||
<string name="msg_copied">ਕਲਿਪ-ਬੋਰਡ ਵਿੱਚ ਕਾਪੀ ਹੋ ਗਿਆ ਹੈ</string>
|
||||
<string name="no_available_dir">ਬਾਅਦ ਵਿੱਚ ਸੈਟਿੰਗਾਂ ਵਿਚੋਂ ਇੱਕ ਡਾਊਨਲੋਡ ਫੋਲਡਰ ਨੂੰ ਚੁਣੋ</string>
|
||||
<string name="msg_popup_permission">ਪੌਪ-ਅਪ ਮੋਡ ਵਿੱਚ ਖੋਲ੍ਹਣ ਵਾਸਤੇ
|
||||
\nਇਸ ਇਜਾਜ਼ਤ ਦੀ ਲੋੜ ਹੈ</string>
|
||||
<string name="msg_popup_permission">ਪੌਪ-ਅਪ ਮੋਡ ਵਿੱਚ ਖੋਲ੍ਹਣ ਵਾਸਤੇ\nਇਸ ਇਜਾਜ਼ਤ ਦੀ ਲੋੜ ਹੈ</string>
|
||||
<string name="one_item_deleted">1 ਆਈਟਮ ਮਿਟਾਈ ਗਈ।</string>
|
||||
<string name="title_activity_recaptcha">ReCaptcha ਚੁਣੌਤੀ</string>
|
||||
<string name="recaptcha_request_toast">ReCaptcha ਚੁਣੌਤੀ ਲਈ ਬੇਨਤੀ</string>
|
||||
@ -278,36 +277,19 @@
|
||||
<string name="previous_export">ਪਿੱਛਲਾ ਐਕਸਪੋਰਟ</string>
|
||||
<string name="subscriptions_import_unsuccessful">ਸਬਸਕ੍ਰਿਪਸ਼ਨਾਂ ਇੰਪੋਰਟ ਨਹੀਂ ਹੋ ਸਕੀਆਂ</string>
|
||||
<string name="subscriptions_export_unsuccessful">ਸਬਸਕ੍ਰਿਪਸ਼ਨਾਂ ਐਕਸਪੋਰਟ ਨਹੀਂ ਹੋ ਸਕੀਆਂ</string>
|
||||
<string name="import_youtube_instructions">ਗੂਗਲ ਟੇਕਅਊਟ ਤੋਂ ਯੂਟਿਊਬ ਸਬਸਕ੍ਰਿਪਸ਼ਨਾਂ ਇੰਪੋਰਟ ਕਰਨ ਲਈ ਐਕਸਪੋਰਟ ਫਾਈਲ ਡਾਊਨਲੋਡ ਕਰੋ:
|
||||
\n
|
||||
\n1. ਇਸ URL ਤੇ ਜਾਓ: %1$s
|
||||
\n2. ਮੰਗਣ ਤੇ ਆਪਣੇ ਖਾਤੇ \'ਚ ਲਾਗ-ਇਨ ਕਰੋ
|
||||
\n3. ਕਲਿੱਕ ਕਰੋ \" All data incuded\" ਤੇ, ਫੇਰ \"Deselect all\" ਤੇ ਫੇਰ ਸਿਰਫ \"subscriprion\" ਚੁਣੋ ਅਤੇ \"OK\" ਕਰੋ
|
||||
\n4. \"Next step\" ਤੇ ਕਲਿੱਕ ਕਰੋ ਤੇ ਫੇਰ \"create export\" ਤੇ
|
||||
\n5. ਡਾਊਨਲੋਡ ਬਟਨ ਦਿਖਾਈ ਦੇਣ ਤੇ ਇਸ ਤੇ ਕਲਿੱਕ ਕਰੋ।ਇੱਕ ਡਾਉਨਲੋਡ ਸ਼ੁਰੂ ਹੋਣੀ ਚਾਹੀਦੀ ਹੈ (ਇਹੀ ਐਕਸਪੋਰਟ ਫਾਈਲ ਹੈ)
|
||||
\n6. ਥੱਲੇ ਇੰਪੋਰਟ ਫਾਈਲ ਤੇ ਕਲਿੱਕ ਕਰੋ ਤੇ ਡਾਊਨਲੋਡ ਕੀਤੀ .zip ਫਾਈਲ ਚੁਣੋ
|
||||
\n7. [ਜੇ .zip ਤੋਂ ਐਕਸਪੋਰਟ ਫੇਲ ਹੋ ਜਾਂਦੀ ਹੈ] ਤਾਂ .csv ਫਾਈਲ ਐਕਸਟਰੈਕਟ ਕਰੋ (ਆਮ ਤੌਰ ਤੇ \"YouTube and YouTube Music/subscriptions/subscriptions.csv\"), ਥੱਲੇ ਦਿੱਤੇ ਇੰਪੋਰਟ ਫਾਈਲ ਤੇ ਕਲਿੱਕ ਕਰਕੇ ਐਕਸਟਰੈਕਟ ਕੀਤੀ csv ਫਾਈਲ ਚੁਣੋ</string>
|
||||
<string name="import_soundcloud_instructions">URL ਜਾਂ ਆਪਣੀ ID ਟਾਈਪ ਕਰਕੇ ਸਾਉੰਡ ਕਲਾਉਡ ਪ੍ਰੋਫਾਈਲ ਇੰਪੋਰਟ ਕਰੋ:
|
||||
\n
|
||||
\n1. ਇੱਕ ਵੈਬ-ਬ੍ਰਾਊਜ਼ਰ ਵਿੱਚ \"ਡੈਸਕਟਾਪ ਮੋਡ\" ਨੂੰ ਚਾਲੂ ਕਰੋ (ਸਾਈਟ ਮੋਬਾਈਲ ਉਪਕਰਣਾਂ ਲਈ ਉਪਲਬਧ ਨਹੀਂ ਹੈ)
|
||||
\n2. ਇਸ URL ਤੇ ਜਾਓ: %1$s
|
||||
\n3. ਆਪਣੇ ਖਾਤੇ ਚ ਲੌਗ-ਇਨ ਕਰੋ
|
||||
\n4. ਨਿਰਦੇਸ਼ਤ ਕੀਤੇ ਗਏ ਪ੍ਰੋਫਾਈਲ URL ਨੂੰ ਕਾਪੀ ਕਰੋ.</string>
|
||||
<string name="import_youtube_instructions">ਗੂਗਲ ਟੇਕਆਊਟ ਤੋਂ ਯੂਟਿਊਬ ਸਬਸਕ੍ਰਿਪਸ਼ਨਾਂ ਇੰਪੋਰਟ ਕਰਨ ਲਈ ਐਕਸਪੋਰਟ ਫਾਈਲ ਡਾਊਨਲੋਡ ਕਰੋ:\n\n1. ਇਸ URL ਤੇ ਜਾਓ: %1$s\n2. ਮੰਗਣ ਤੇ ਆਪਣੇ ਖਾਤੇ \'ਚ ਲਾਗ-ਇਨ ਕਰੋ\n3. ਕਲਿੱਕ ਕਰੋ \" All data incuded\" ਤੇ, ਫੇਰ \"Deselect all\" ਤੇ ਫੇਰ ਸਿਰਫ \"subscriprion\" ਚੁਣੋ ਅਤੇ \"OK\" ਕਰੋ\n4. \"Next step\" ਤੇ ਕਲਿੱਕ ਕਰੋ ਅਤੇ ਫੇਰ \"create export\" ਤੇ\n5. ਡਾਊਨਲੋਡ ਬਟਨ ਦਿਖਾਈ ਦੇਣ ਤੇ ਇਸ ਤੇ ਕਲਿੱਕ ਕਰੋ। ਇੱਕ ਡਾਉਨਲੋਡ ਸ਼ੁਰੂ ਹੋਣੀ ਚਾਹੀਦੀ ਹੈ (ਇਹੀ ਐਕਸਪੋਰਟ ਫਾਈਲ ਹੈ)\n6. ਥੱਲੇ ਇੰਪੋਰਟ ਫਾਈਲ ਤੇ ਕਲਿੱਕ ਕਰੋ ਤੇ ਡਾਊਨਲੋਡ ਕੀਤੀ .zip ਫਾਈਲ ਚੁਣੋ\n7. [ਜੇ .zip ਤੋਂ ਐਕਸਪੋਰਟ ਫੇਲ ਹੋ ਜਾਂਦੀ ਹੈ] ਤਾਂ .csv ਫਾਈਲ ਐਕਸਟਰੈਕਟ ਕਰੋ (ਆਮ ਤੌਰ ਤੇ \"YouTube and YouTube Music/subscriptions/subscriptions.csv\"), ਥੱਲੇ ਦਿੱਤੇ ਇੰਪੋਰਟ ਫਾਈਲ ਤੇ ਕਲਿੱਕ ਕਰਕੇ ਐਕਸਟਰੈਕਟ ਕੀਤੀ csv ਫਾਈਲ ਚੁਣੋ</string>
|
||||
<string name="import_soundcloud_instructions">URL ਜਾਂ ਆਪਣੀ ID ਟਾਈਪ ਕਰਕੇ ਸਾਉੰਡ ਕਲਾਉਡ ਪ੍ਰੋਫਾਈਲ ਇੰਪੋਰਟ ਕਰੋ: \n \n1. ਇੱਕ ਵੈਬ-ਬ੍ਰਾਊਜ਼ਰ ਵਿੱਚ \"ਡੈਸਕਟਾਪ ਮੋਡ\" ਨੂੰ ਚਾਲੂ ਕਰੋ (ਸਾਈਟ ਮੋਬਾਈਲ ਉਪਕਰਣਾਂ ਲਈ ਉਪਲਬਧ ਨਹੀਂ ਹੈ) \n2. ਇਸ URL ਤੇ ਜਾਓ: %1$s \n3. ਆਪਣੇ ਖਾਤੇ ਚ ਲੌਗ-ਇਨ ਕਰੋ \n4. ਨਿਰਦੇਸ਼ਤ ਕੀਤੇ ਗਏ ਪ੍ਰੋਫਾਈਲ URL ਨੂੰ ਕਾਪੀ ਕਰੋ।</string>
|
||||
<string name="import_soundcloud_instructions_hint">ਤੁਹਾਡੀ ਆਈਡੀ, soundcloud.com/ਤੁਹਾਡੀ ਆਈਡੀ</string>
|
||||
<string name="import_network_expensive_warning">ਯਾਦ ਰੱਖੋ ਕਿ ਇਸ ਕਾਰਜ ਨਾਲ ਡਾਟਾ ਖਪਤ ਹੋ ਸਕਦਾ ਹੈ।
|
||||
\n
|
||||
\nਕੀ ਤੁਸੀਂ ਜਾਰੀ ਰੱਖਣਾ ਚਾਹੁੰਦੇ ਹੋ\?</string>
|
||||
<string name="import_network_expensive_warning">ਯਾਦ ਰੱਖੋ ਕਿ ਇਸ ਕਾਰਜ ਨਾਲ ਡਾਟਾ ਖਪਤ ਹੋ ਸਕਦਾ ਹੈ।\n\nਕੀ ਤੁਸੀਂ ਜਾਰੀ ਰੱਖਣਾ ਚਾਹੁੰਦੇ ਹੋ?</string>
|
||||
<string name="playback_speed_control">ਪਲੇਅਬੈਕ ਸਪੀਡ ਕੰਟਰੋਲ</string>
|
||||
<string name="playback_tempo">ਤਾਲ</string>
|
||||
<string name="playback_pitch">ਪਿੱਚ</string>
|
||||
<string name="unhook_checkbox">ਅਲਹਿਦਾ ਕਰੋ (ਵਿਗਾੜ ਪੈ ਸਕਦਾ ਹੈ)</string>
|
||||
<string name="import_settings">ਕੀ ਤੁਸੀਂ ਸੈਟਿੰਗਾਂ ਨੂੰ ਵੀ ਇੰਪੋਰਟ ਕਰਨਾ ਚਾਹੁੰਦੇ ਹੋ\?</string>
|
||||
<string name="privacy_policy_title">ਨਿਊਪਾਈਪ ਦੀ ਗੋਪਨੀਯਤਾ ਨੀਤੀ</string>
|
||||
<string name="privacy_policy_encouragement">ਨਿਊਪਾਈਪ ਪ੍ਰੋਜੈਕਟ ਤੁਹਾਡੀ ਗੋਪਨੀਯਤਾ ਨੂੰ ਬਹੁਤ ਗੰਭੀਰਤਾ ਨਾਲ ਲੈਂਦਾ ਹੈ। ਇਸ ਲਈ ਐਪ ਤੁਹਾਡੀ ਸਹਿਮਤੀ ਤੋਂ ਬਿਨਾਂ ਕੋਈ ਵੀ ਡਾਟਾ ਇੱਕਠਾ ਨਹੀਂ ਕਰਦਾ।
|
||||
\nਨਿਊਪਾਈਪ ਦੀ ਗੋਪਨੀਯਤਾ ਨੀਤੀ ਵਿਸਥਾਰ ਵਿੱਚ ਦੱਸਦੀ ਹੈ ਕਿ ਜਦੋਂ ਤੁਸੀਂ ਕਰੈਸ਼ ਰਿਪੋਰਟ ਭੇਜਦੇ ਹੋ ਤਾਂ ਕਿਹੜਾ ਡਾਟਾ ਭੇਜਿਆ ਜਾਂ ਸਟੋਰ ਕੀਤਾ ਜਾਂਦਾ ਹੈ।</string>
|
||||
<string name="privacy_policy_encouragement">ਨਿਊਪਾਈਪ ਪ੍ਰੋਜੈਕਟ ਤੁਹਾਡੀ ਗੋਪਨੀਯਤਾ ਨੂੰ ਬਹੁਤ ਗੰਭੀਰਤਾ ਨਾਲ ਲੈਂਦਾ ਹੈ। ਇਸ ਲਈ ਐਪ ਤੁਹਾਡੀ ਸਹਿਮਤੀ ਤੋਂ ਬਿਨਾਂ ਕੋਈ ਵੀ ਡਾਟਾ ਇੱਕਠਾ ਨਹੀਂ ਕਰਦਾ।\nਨਿਊਪਾਈਪ ਦੀ ਗੋਪਨੀਯਤਾ ਨੀਤੀ ਵਿਸਥਾਰ ਵਿੱਚ ਦੱਸਦੀ ਹੈ ਕਿ ਜਦੋਂ ਤੁਸੀਂ ਕਰੈਸ਼ ਰਿਪੋਰਟ ਭੇਜਦੇ ਹੋ ਤਾਂ ਕਿਹੜਾ ਡਾਟਾ ਭੇਜਿਆ ਜਾਂ ਸਟੋਰ ਕੀਤਾ ਜਾਂਦਾ ਹੈ।</string>
|
||||
<string name="read_privacy_policy">ਗੋਪਨੀਯਤਾ ਨੀਤੀ ਪੜ੍ਹੋ</string>
|
||||
<string name="start_accept_privacy_policy">ਯੂਰਪੀਅਨ ਜਨਰਲ ਡੇਟਾ ਪ੍ਰੋਟੈਕਸ਼ਨ ਰੈਗੂਲੇਸ਼ਨ (ਜੀਡੀਪੀਆਰ) ਦੀ ਪਾਲਣਾ ਕਰਨ ਲਈ, ਅਸੀਂ ਤੁਹਾਡਾ ਧਿਆਨ ਨਿਊਪਾਈਪ ਦੀ ਗੋਪਨੀਯਤਾ ਨੀਤੀ ਵੱਲ ਖਿੱਚਦੇ ਹਾਂ। ਕਿਰਪਾ ਕਰਕੇ ਇਸਨੂੰ ਧਿਆਨ ਨਾਲ ਪੜ੍ਹੋ।
|
||||
\nਸਾਨੂੰ ਨੁਕਸ ਰਿਪੋਰਟ ਭੇਜਣ ਲਈ ਤੁਹਾਨੂੰ ਇਸ ਨੂੰ ਸਵੀਕਾਰ ਕਰਨਾ ਹੋਵੇਗਾ।</string>
|
||||
<string name="start_accept_privacy_policy">ਯੂਰਪੀਅਨ ਜਨਰਲ ਡੇਟਾ ਪ੍ਰੋਟੈਕਸ਼ਨ ਰੈਗੂਲੇਸ਼ਨ (ਜੀਡੀਪੀਆਰ) ਦੀ ਪਾਲਣਾ ਕਰਨ ਲਈ, ਅਸੀਂ ਤੁਹਾਡਾ ਧਿਆਨ ਨਿਊਪਾਈਪ ਦੀ ਗੋਪਨੀਯਤਾ ਨੀਤੀ ਵੱਲ ਖਿੱਚਦੇ ਹਾਂ। ਕਿਰਪਾ ਕਰਕੇ ਇਸਨੂੰ ਧਿਆਨ ਨਾਲ ਪੜ੍ਹੋ।\nਸਾਨੂੰ ਨੁਕਸ ਰਿਪੋਰਟ ਭੇਜਣ ਲਈ ਤੁਹਾਨੂੰ ਇਸ ਨੂੰ ਸਵੀਕਾਰ ਕਰਨਾ ਹੋਵੇਗਾ।</string>
|
||||
<string name="accept">ਸਵੀਕਾਰ ਕਰੋ</string>
|
||||
<string name="decline">ਅਸਵੀਕਾਰ</string>
|
||||
<string name="limit_data_usage_none_description">ਕੋਈ ਸੀਮਾ ਨਹੀਂ</string>
|
||||
@ -512,8 +494,7 @@
|
||||
<item quantity="other">%d ਸਕਿੰਟ</item>
|
||||
</plurals>
|
||||
<string name="remove_watched_popup_yes_and_partially_watched_videos">ਹਾਂ, ਅਤੇ ਅੱਧ-ਪਚੱਧੀਆਂ ਵੇਖੀਆਂ ਹੋਈਆਂ ਵੀ</string>
|
||||
<string name="remove_watched_popup_warning">ਪਲੇਲਿਸਟ ਵਿੱਚ ਸ਼ਾਮਿਲ, ਪਹਿਲਾਂ ਚਾਹੇ ਬਾਅਦ ਵਿੱਚ ਵੇਖੇ ਜਾ ਚੁੱਕੇ ਵੀਡੀਓ ਹਟਾ ਦਿੱਤੇ ਜਾਣਗੇ।
|
||||
\nਕੀ ਵਾਕਿਆ ਹੀ ਤੁਸੀਂ ਇਹਨਾਂ ਨੂੰ ਹਟਾਉਣਾ ਚਾਹੁੰਦੇ ਹੋ? ਇਸ ਕਾਰਵਾਈ ਨੂੰ ਵਾਪਸ ਨਹੀਂ ਮੋੜਿਆ ਜਾ ਸਕਣਾ!</string>
|
||||
<string name="remove_watched_popup_warning">ਪਲੇਲਿਸਟ ਵਿੱਚ ਸ਼ਾਮਿਲ ਪਹਿਲਾਂ ਤੇ ਬਾਅਦ ਵਿੱਚ ਵੇਖੇ ਜਾ ਚੁੱਕੇ ਵੀਡੀਓ ਹਟਾ ਦਿੱਤੇ ਜਾਣਗੇ। \nਕੀ ਵਾਕਿਆ ਹੀ ਤੁਸੀਂ ਇਹਨਾਂ ਨੂੰ ਹਟਾਉਣਾ ਚਾਹੁੰਦੇ ਹੋ? ਇਸ ਕਾਰਵਾਈ ਨੂੰ ਵਾਪਸ ਨਹੀਂ ਕੀਤਾ ਜਾ ਸਕਣਾ!</string>
|
||||
<string name="remove_watched_popup_title">ਵੇਖੇ ਹੋਏ ਵੀਡੀਓ ਹਟਾ ਦੇਈਏ\?</string>
|
||||
<string name="remove_watched">ਵੇਖੇ ਹੋਏ ਨੂੰ ਹਟਾਓ</string>
|
||||
<string name="systems_language">ਸਿਸਟਮ ਡਿਫ਼ਾਲਟ</string>
|
||||
@ -554,7 +535,7 @@
|
||||
<string name="no_one_listening">ਕੋਈ ਸਰੋਤਾ ਨਹੀਂ ਸੁਣ ਰਿਹਾ</string>
|
||||
<string name="no_one_watching">ਕੋਈ ਦਰਸ਼ਕ ਨਹੀਂ ਵੇਖ ਰਿਹਾ</string>
|
||||
<string name="description_tab_description">ਵੇਰਵਾ</string>
|
||||
<string name="related_items_tab_description">ਸਬੰਧਤ ਨਗ</string>
|
||||
<string name="related_items_tab_description">ਸਬੰਧਤ ਆਈਟਮਾਂ</string>
|
||||
<string name="comments_tab_description">ਟਿੱਪਣੀਆਂ</string>
|
||||
<string name="error_report_open_issue_button_text">ਗਿਟਹੱਬ \'ਤੇ ਜਾ ਕੇ ਇਤਲਾਹ ਦਿਓ</string>
|
||||
<string name="permission_display_over_apps">ਦੂਜੀਆਂ ਐਪਾਂ ਦੇ ਉੱਤੇ ਵਿਖਾਉਣ ਦੀ ਇਜਾਜ਼ਤ ਦਿਓ</string>
|
||||
@ -820,8 +801,8 @@
|
||||
<string name="audio_track_type_secondary">ਸੈਕੰਡਰੀ</string>
|
||||
<string name="share_playlist_as_youtube_temporary_playlist">ਅਸਥਾਈ ਯੂਟਿਊਬ ਪਲੇਲਿਸਟ ਵਜੋਂ ਸਾਂਝਾ ਕਰੋ</string>
|
||||
<string name="tab_bookmarks_short">ਪਲੇਲਿਸਟਾਂ</string>
|
||||
<string name="search_with_service_name">%1$s ਦੀ ਖੋਜ ਕਰੋ</string>
|
||||
<string name="search_with_service_name_and_filter">%1$s (%2$s) ٪1$s ਦੀ ਖੋਜ ਕਰੋ</string>
|
||||
<string name="search_with_service_name">%1$s ਖੋਜੋ</string>
|
||||
<string name="search_with_service_name_and_filter">%1$s (%2$s) ਖੋਜੋ</string>
|
||||
<string name="select_a_feed_group">ਫੀਡ ਗਰੁੱਪ ਚੁਣੋ</string>
|
||||
<string name="no_feed_group_created_yet">ਅਜੇ ਤੱਕ ਕੋਈ ਫੀਡ ਗਰੁੱਪ ਨਹੀਂ ਬਣਾਇਆ ਗਿਆ</string>
|
||||
<string name="feed_group_page_summary">ਚੈਨਲ ਗਰੁੱਪ ਪੰਨਾ</string>
|
||||
@ -830,4 +811,22 @@
|
||||
<string name="delete_entry">ਐਂਟਰੀ ਮਿਟਾਓ</string>
|
||||
<string name="account_terminated_service_provides_reason">ਖ਼ਾਤਾ ਬੰਦ ਕੀਤਾ ਗਿਆ\n\n%1$s ਇਹ ਕਾਰਨ ਪ੍ਰਦਾਨ ਕਰਦਾ ਹੈ: %2$s</string>
|
||||
<string name="entry_deleted">ਐਂਟਰੀ ਮਿਟਾ ਦਿੱਤੀ ਗਈ</string>
|
||||
<string name="permission_display_over_apps_message">ਪੌਪਅੱਪ ਪਲੇਅਰ ਦੀ ਵਰਤੋਂ ਕਰਨ ਲਈ, ਕਿਰਪਾ ਕਰਕੇ ਹੇਠਾਂ ਦਿੱਤੇ Android ਸੈਟਿੰਗ ਮੀਨੂ ਵਿੱਚ %1$s ਚੁਣੋ ਅਤੇ %2$s ਨੂੰ ਇਨੇਬਲ ਕਰੋ।</string>
|
||||
<string name="permission_display_over_apps_permission_name">\"ਹੋਰ ਐਪਾਂ ਉੱਤੇ ਡਿਸਪਲੇ ਦੀ ਆਗਿਆ ਦਿਓ\"</string>
|
||||
<string name="short_thousand">%sਹਜ਼ਾਰ</string>
|
||||
<string name="short_million">%sਮਿਲੀਅਨ</string>
|
||||
<string name="short_billion">%sਅਰਬ</string>
|
||||
<string name="migration_info_6_7_title">SoundCloud ਟੌਪ 50 ਪੰਨਾ ਹਟਾ ਦਿੱਤਾ ਗਿਆ</string>
|
||||
<string name="migration_info_6_7_message">SoundCloud ਨੇ ਮੂਲ ਟੌਪ 50 ਚਾਰਟਾਂ ਨੂੰ ਬੰਦ ਕਰ ਦਿੱਤਾ ਹੈ। ਸੰਬੰਧਿਤ ਟੈਬ ਨੂੰ ਤੁਹਾਡੇ ਮੁੱਖ ਪੰਨੇ ਤੋਂ ਹਟਾ ਦਿੱਤਾ ਗਿਆ ਹੈ।</string>
|
||||
<string name="migration_info_7_8_title">YouTube ਸੰਯੁਕਤ ਰੁਝਾਨ ਹਟਾਇਆ ਗਿਆ</string>
|
||||
<string name="migration_info_7_8_message">YouTube ਨੇ 21 ਜੁਲਾਈ 2025 ਤੋਂ ਸੰਯੁਕਤ \"ਰੁਝਾਨ ਵਿੱਚ\" ਪੰਨੇ ਨੂੰ ਬੰਦ ਕਰ ਦਿੱਤਾ ਹੈ। NewPipe ਨੇ ਡਿਫ਼ਾਲਟ \"ਰੁਝਾਨ ਵਿੱਚ\" ਪੰਨੇ ਨੂੰ ਟ੍ਰੈਂਡਿੰਗ ਲਾਈਵਸਟ੍ਰੀਮਾਂ ਨਾਲ ਬਦਲ ਦਿੱਤਾ ਹੈ।\n\nਤੁਸੀਂ \"ਸੈਟਿੰਗਾਂ > ਸਮੱਗਰੀ > ਮੁੱਖ ਪੰਨੇ ਦੀ ਸਮੱਗਰੀ\" ਵਿੱਚ ਵੱਖ-ਵੱਖ ਟ੍ਰੈਂਡਿੰਗ ਪੰਨਿਆਂ ਨੂੰ ਵੀ ਚੁਣ ਸਕਦੇ ਹੋ।</string>
|
||||
<string name="trending_gaming">ਗੇਮਿੰਗ ਟ੍ਰੈਂਡਸ</string>
|
||||
<string name="trending_podcasts">ਟ੍ਰੈਂਡਿੰਗ ਪੌਡਕਾਸਟ</string>
|
||||
<string name="trending_movies">ਟਰੈਂਡਿੰਗ ਫ਼ਿਲਮਾਂ ਅਤੇ ਸ਼ੋਅ</string>
|
||||
<string name="trending_music">ਟਰੈਂਡਿੰਗ ਸੰਗੀਤ</string>
|
||||
<string name="player_http_403">ਪਲੇਅ ਕਰਦੇ ਸਮੇਂ ਸਰਵਰ ਤੋਂ HTTP error 403 ਪ੍ਰਾਪਤ ਹੋਇਆ, ਜੋ ਸ਼ਾਇਦ ਸਟ੍ਰੀਮਿੰਗ URL ਦੀ ਮਿਆਦ ਪੁੱਗਣ ਜਾਂ IP ਦੀ ਪਾਬੰਦੀ ਕਾਰਨ ਹੋਈ ਹੈ</string>
|
||||
<string name="player_http_invalid_status">ਚਲਾਉਣ ਦੌਰਾਨ ਸਰਵਰ ਤੋਂ HTTP error %1$s ਪ੍ਰਾਪਤ ਹੋਇਆ</string>
|
||||
<string name="youtube_player_http_403">ਪਲੇਅ ਕਰਦੇ ਸਮੇਂ ਸਰਵਰ ਤੋਂ HTTP error 403 ਪ੍ਰਾਪਤ ਹੋਇਆ, ਜੋ ਸ਼ਾਇਦ IP ਬੈਨ ਜਾਂ ਸਟ੍ਰੀਮਿੰਗ URL ਡੀਔਬਫਸਕੇਸ਼ਨ ਸਮੱਸਿਆਵਾਂ ਕਾਰਨ ਹੋਈ ਹੈ</string>
|
||||
<string name="sign_in_confirm_not_bot_error">%1$s ਨੇ ਡੇਟਾ ਪ੍ਰਦਾਨ ਕਰਨ ਤੋਂ ਇਨਕਾਰ ਕਰ ਦਿੱਤਾ, ਅਤੇ ਇਹ ਪੁਸ਼ਟੀ ਕਰਨ ਲਈ ਲੌਗਇਨ ਕਰਨ ਲਈ ਕਿਹਾ ਕਿ ਬੇਨਤੀਕਰਤਾ ਬੋਟ ਨਹੀਂ ਹੈ।\n\nਹੋ ਸਕਦਾ ਹੈ ਕਿ %1$s ਨੇ ਤੁਹਾਡੇ IP ਨੂੰ ਅਸਥਾਈ ਤੌਰ \'ਤੇ ਪਾਬੰਦੀ ਲਗਾਈ ਹੋਵੇ, ਤੁਸੀਂ ਕੁਝ ਸਮਾਂ ਉਡੀਕ ਕਰ ਸਕਦੇ ਹੋ ਜਾਂ ਕਿਸੇ ਵੱਖਰੇ IP \'ਤੇ ਸਵਿੱਚ ਕਰ ਸਕਦੇ ਹੋ (ਉਦਾਹਰਣ ਵਜੋਂ VPN ਨੂੰ ਚਾਲੂ/ਬੰਦ ਕਰਕੇ, ਜਾਂ WiFi ਤੋਂ ਮੋਬਾਈਲ ਡੇਟਾ \'ਤੇ ਸਵਿੱਚ ਕਰਕੇ)।</string>
|
||||
<string name="unsupported_content_in_country">ਇਹ ਸਮੱਗਰੀ ਵਰਤਮਾਨ ਵਿੱਚ ਚੁਣੇ ਗਏ ਦੇਸ਼ ਦੀ ਸਮੱਗਰੀ ਲਈ ਉਪਲੱਬਧ ਨਹੀਂ ਹੈ।\n\n\"ਸੈਟਿੰਗਾਂ > ਸਮੱਗਰੀ > ਡਿਫ਼ਾਲਟ ਸਮੱਗਰੀ ਦੇਸ਼\" ਤੋਂ ਆਪਣੀ ਚੋਣ ਬਦਲੋ।</string>
|
||||
</resources>
|
||||
|
||||
@ -45,7 +45,7 @@
|
||||
<string name="detail_uploader_thumbnail_view_description">Náhľad avataru uploadera</string>
|
||||
<string name="detail_likes_img_view_description">Lajky</string>
|
||||
<string name="detail_dislikes_img_view_description">Dislajky</string>
|
||||
<string name="main_bg_subtitle">Začnite klepnutím na lupu.</string>
|
||||
<string name="main_bg_subtitle">Začnite ťuknutím na lupu.</string>
|
||||
<string name="content">Obsah</string>
|
||||
<string name="show_age_restricted_content_title">Zobraziť vekovo obmedzený obsah</string>
|
||||
<string name="duration_live">Naživo</string>
|
||||
@ -74,7 +74,7 @@
|
||||
<string name="msg_wait">Čakajte prosím…</string>
|
||||
<string name="msg_copied">Skopírované do schránky</string>
|
||||
<string name="no_available_dir">Priečinok na sťahovanie zadefinujte prosím neskôr v nastaveniach</string>
|
||||
<string name="downloads">Sťahované súbory</string>
|
||||
<string name="downloads">Stiahnuté súbory</string>
|
||||
<string name="downloads_title">Stiahnuté</string>
|
||||
<string name="error_report_title">Hlásenie o chybe</string>
|
||||
<string name="app_ui_crash">Aplikácia/UP zlyhalo</string>
|
||||
@ -208,7 +208,7 @@
|
||||
<string name="no_valid_zip_file">Neplatný ZIP súbor</string>
|
||||
<string name="could_not_import_all_files">Upozornenie: Nemožno importovať všetky súbory.</string>
|
||||
<string name="override_current_data">Toto prepíše vaše aktuálne nastavenie.</string>
|
||||
<string name="trending">Trendy</string>
|
||||
<string name="trending">Populárne</string>
|
||||
<string name="top_50">Top 50</string>
|
||||
<string name="new_and_hot">Nové a horúce</string>
|
||||
<string name="play_queue_remove">Odstrániť</string>
|
||||
@ -473,7 +473,7 @@
|
||||
<item quantity="other">%d dní</item>
|
||||
</plurals>
|
||||
<string name="feed_groups_header_title">Skupiny kanálov</string>
|
||||
<string name="feed_oldest_subscription_update">Zdroj naposledy aktualizovaný: %s</string>
|
||||
<string name="feed_oldest_subscription_update">Zdroj aktualizovaný: %s</string>
|
||||
<string name="feed_subscription_not_loaded_count">Nenačítané: %d</string>
|
||||
<string name="feed_notification_loading">Načítavanie zdroja…</string>
|
||||
<string name="feed_processing_message">Spracovávanie zdroja…</string>
|
||||
@ -843,10 +843,10 @@
|
||||
<string name="migration_info_6_7_message">SoundCloud prestal používať pôvodnú Top 50. Daná stránka bola odstránená z hlavnej stránky.</string>
|
||||
<string name="migration_info_7_8_title">Odstránené kombinované trendy na YouTube</string>
|
||||
<string name="migration_info_7_8_message">YouTube ukončil prevádzku kombinovanej stránky s trendmi k 21. júlu 2025. NewPipe nahradil predvolenú stránku s trendmi stránkou s trendovými živými prenosmi.\n\nV nastaveniach „Nastavenia > Obsah > Obsah hlavnej stránky“ môžete vybrať aj iné stránky s trendmi.</string>
|
||||
<string name="trending_gaming">Ttendy v hrách</string>
|
||||
<string name="trending_podcasts">Trendové podcasty</string>
|
||||
<string name="trending_movies">Trendové filmy a seriály</string>
|
||||
<string name="trending_music">Trendová hudba</string>
|
||||
<string name="trending_gaming">Populárne hry</string>
|
||||
<string name="trending_podcasts">Populárne podcasty</string>
|
||||
<string name="trending_movies">Populárne filmy a seriály</string>
|
||||
<string name="trending_music">Populárna hudba</string>
|
||||
<string name="short_thousand">%stis.</string>
|
||||
<string name="short_million">%smil.</string>
|
||||
<string name="short_billion">%smld.</string>
|
||||
|
||||
@ -170,4 +170,19 @@
|
||||
<string name="dismiss">ሰሩዞ</string>
|
||||
<string name="rename">ስም ቀያር</string>
|
||||
<string name="msg_error">ስሕተት</string>
|
||||
<string name="metadata_tags">መፍለዪ</string>
|
||||
<string name="metadata_privacy_public">ህዝባዊ</string>
|
||||
<string name="metadata_privacy">ብሕትነት</string>
|
||||
<string name="tab_licenses">ፍቓድታት</string>
|
||||
<string name="read_full_license">ፍቓድ ኣንብብ</string>
|
||||
<string name="metadata_category">ምድብ</string>
|
||||
<string name="metadata_licence">ፍቓድ</string>
|
||||
<string name="settings_category_updates_title">እዋናዊታት</string>
|
||||
<string name="metadata_avatars">ኣቫታራት</string>
|
||||
<string name="metadata_banners">ባነራት</string>
|
||||
<string name="metadata_privacy_unlisted">ዘይተዘርዘረ</string>
|
||||
<string name="metadata_privacy_private">ብሕታዊ</string>
|
||||
<string name="metadata_privacy_internal">ውሽጣዊ</string>
|
||||
<string name="metadata_subscribers">ተኸታተልቲ</string>
|
||||
<string name="open_website_license">መርበብ-ቦታ ክፈት</string>
|
||||
</resources>
|
||||
|
||||
@ -62,7 +62,7 @@
|
||||
app:useSimpleSummaryProvider="true" />
|
||||
|
||||
<SwitchPreferenceCompat
|
||||
android:defaultValue="false"
|
||||
android:defaultValue="true"
|
||||
android:key="@string/prefer_original_audio_key"
|
||||
android:summary="@string/prefer_original_audio_summary"
|
||||
android:title="@string/prefer_original_audio_title"
|
||||
|
||||
@ -1,14 +1,14 @@
|
||||
package org.schabi.newpipe
|
||||
|
||||
import java.time.Instant
|
||||
import java.time.ZoneId
|
||||
import java.time.format.DateTimeFormatter
|
||||
import kotlin.math.abs
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
import org.schabi.newpipe.util.ReleaseVersionUtil.coerceUpdateCheckExpiry
|
||||
import org.schabi.newpipe.util.ReleaseVersionUtil.isLastUpdateCheckExpired
|
||||
import java.time.Instant
|
||||
import java.time.ZoneId
|
||||
import java.time.format.DateTimeFormatter
|
||||
import kotlin.math.abs
|
||||
|
||||
class NewVersionManagerTest {
|
||||
|
||||
|
||||
@ -1,12 +1,12 @@
|
||||
package org.schabi.newpipe.ktx
|
||||
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
import java.io.IOException
|
||||
import java.io.InterruptedIOException
|
||||
import java.net.SocketException
|
||||
import javax.net.ssl.SSLException
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
|
||||
class ThrowableExtensionsTest {
|
||||
@Test fun `assignable causes`() {
|
||||
|
||||
@ -15,7 +15,8 @@ class FeedGroupIconTest {
|
||||
|
||||
assertEquals(
|
||||
"Gap between ids detected (current item: ${currentIcon.name} - ${currentIcon.id} → should be: $shouldBeId)",
|
||||
shouldBeId, currentIcon.id
|
||||
shouldBeId,
|
||||
currentIcon.id
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,6 +2,10 @@ package org.schabi.newpipe.settings
|
||||
|
||||
import android.content.SharedPreferences
|
||||
import com.grack.nanojson.JsonParser
|
||||
import java.io.File
|
||||
import java.io.ObjectInputStream
|
||||
import java.nio.file.Files
|
||||
import java.util.zip.ZipFile
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertThrows
|
||||
@ -23,6 +27,7 @@ import org.schabi.newpipe.settings.export.BackupFileLocator
|
||||
import org.schabi.newpipe.settings.export.ImportExportManager
|
||||
import org.schabi.newpipe.streams.io.StoredFileHelper
|
||||
import us.shandian.giga.io.FileStream
|
||||
<<<<<<< HEAD
|
||||
import java.io.File
|
||||
import java.io.ObjectInputStream
|
||||
import java.nio.file.Paths
|
||||
@ -34,6 +39,8 @@ import kotlin.io.path.div
|
||||
import kotlin.io.path.exists
|
||||
import kotlin.io.path.fileSize
|
||||
import kotlin.io.path.inputStream
|
||||
=======
|
||||
>>>>>>> dev
|
||||
|
||||
@RunWith(MockitoJUnitRunner::class)
|
||||
class ImportExportManagerTest {
|
||||
|
||||
@ -1,12 +1,12 @@
|
||||
package org.schabi.newpipe.util
|
||||
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
import org.ocpsoft.prettytime.PrettyTime
|
||||
import java.time.LocalDate
|
||||
import java.time.OffsetDateTime
|
||||
import java.time.ZoneOffset
|
||||
import java.util.Locale
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
import org.ocpsoft.prettytime.PrettyTime
|
||||
|
||||
class LocalizationTest {
|
||||
@Test(expected = NullPointerException::class)
|
||||
|
||||
@ -1,8 +0,0 @@
|
||||
### Подобрения
|
||||
- Импорт/ Експорт на настройки #1333
|
||||
- Редуциране на надхвърляне (подобрение на производителността) #1371
|
||||
- Малки подобрения в кода #1375
|
||||
- Добавяне на всичко за GDPR #1420
|
||||
|
||||
### Поправени
|
||||
- Изтегляния: Поправен срив при зареждане на неприключени изтегляния от .giga файлове #1407
|
||||
@ -1,17 +1,17 @@
|
||||
Novinky
|
||||
• Přidána podpora pro Android Auto.
|
||||
• Možnost nastavit skupiny kanálů jako záložky na hlavní obrazovce.
|
||||
• [YouTube] Sdílení jako dočasný seznam skladeb.
|
||||
• [SoundCloud] Záložka Oblíbené kanály
|
||||
Nově
|
||||
• Podpora pro Android Auto
|
||||
• Možnost nastavit skupiny zdrojů jako záložky
|
||||
• [YouTube] Sdílení jako dočasný playlist
|
||||
• [SoundCloud] Záložka Oblíbené u kanálů
|
||||
|
||||
Vylepšeno
|
||||
• Lepší nápověda pro vyhledávací lištu
|
||||
• Zobrazení data stažení v sekci Stažené soubory
|
||||
• Použití jazyka Android 13 pro jednotlivé aplikace
|
||||
• Lepší našeptávač vyhledávače
|
||||
• Zobrazení data stažení ve Stažených
|
||||
• Použití individuálního jazyka
|
||||
|
||||
Opraveno
|
||||
• Oprava chybných barev textu v tmavém režimu
|
||||
• [YouTube] Oprava seznamů skladeb, které nenačtou více než 100 položek
|
||||
• [YouTube] Oprava nenačtení více než 100 položek v playlistech
|
||||
• [YouTube] Oprava chybějících doporučených videí
|
||||
• Oprava pádů v zobrazení seznamu historie
|
||||
• Oprava časových značek v odpovědích na komentáře
|
||||
• Oprava pádů v Historii
|
||||
• Oprava časů v odpovědích
|
||||
|
||||
@ -4,13 +4,9 @@ Pokus o obnovení čekajících stahování, pokud to jde
|
||||
Možnost odstranění stahování bez smazání souboru
|
||||
Oprávnění Zobrazení přes ostatní aplikace: zobrazení vysvětlení pro Android > R
|
||||
Podpora odkazů on.soundcloud
|
||||
Spousta malých vylepšení a optimalizací
|
||||
|
||||
# Opravy
|
||||
Oprava formátování pro verze Androidu nižší než 7
|
||||
Oprava falešných oznámení
|
||||
Opravy souborů titulků SRT
|
||||
Oprava spousty pádů
|
||||
|
||||
# Vývoj
|
||||
Interní modernizace kódu
|
||||
|
||||
@ -11,6 +11,3 @@ Kurzformatierung für Android-Versionen unter 7
|
||||
Geisterbenachrichtigungen
|
||||
SRT-Untertiteldateien
|
||||
Zahlreiche Abstürze
|
||||
|
||||
# Entwicklung
|
||||
Modernisierung des internen Codes
|
||||
|
||||
@ -1,16 +1,15 @@
|
||||
Neu:
|
||||
Neu
|
||||
• Benachrichtigungen für neue Streams
|
||||
• Nahtloser Übergang zwischen Hintergrund- und Videoplayer
|
||||
• Änderung der Tonhöhe um Halbtöne
|
||||
• Ändern der Tonhöhe um Halbtöne
|
||||
• Warteschlange des Hauptplayers an Wiedergabeliste anfügen
|
||||
|
||||
Verbessert:
|
||||
• Geschwindigkeit/Tonhöhenschrittgröße speichern
|
||||
Verbessert
|
||||
• Speichern der Geschwindigkeit/Tonhöhenschrittweite
|
||||
• Anfängliche lange Videoplayer-Pufferung verringert
|
||||
• Player-UI für Android TV
|
||||
• Löschbestätigung für alle heruntergeladenen Dateien
|
||||
• Löschbestätigung aller heruntergeladenen Dateien
|
||||
|
||||
Behoben:
|
||||
• Medienschaltfläche blendet die Steuerelemente des Players nicht aus
|
||||
• Rücksetzung der Wiedergabe bei Änderung des Playertyps
|
||||
• Drehung des Wiedergabelisten-Dialogs
|
||||
Behoben
|
||||
• Medienschaltfläche blendet Player-Steuerelemente nicht aus
|
||||
• …
|
||||
|
||||
@ -1,12 +1,12 @@
|
||||
Neu:
|
||||
• Unterstützung anderer Übertragungsmethoden als progressives HTTP: schnellere Ladezeit der Wiedergabe, Korrekturen für PeerTube und SoundCloud, Wiedergabe von kürzlich beendeten YouTube-Livestreams
|
||||
• Schaltfläche um entfernte Wiedergabeliste einer lokalen Wiedergabeliste hinzuzufügen
|
||||
• Bildvorschau im Android 10+ Teilen-Dialog
|
||||
Neu
|
||||
• Unterstützung anderer Übertragungsmethoden als progressives HTTP: schnellere Ladezeit bei Wiedergabe, Fehlerbehebungen für PeerTube/SoundCloud, Wiedergabe kürzlich beendeter YouTube-Livestreams
|
||||
• Schaltfläche, um Remote-Wiedergabeliste einer Lokalen hinzuzufügen
|
||||
• Bildvorschau im Teilen-Dialog von Android 10+
|
||||
|
||||
Verbessert:
|
||||
Verbessert
|
||||
• Wiedergabewerte-Dialog
|
||||
• Import/Export-Schaltflächen für Abonnements in das Drei-Punkte-Menü verschoben
|
||||
• Import/Export-Schaltflächen für Abos in Drei-Punkte-Menü verschoben
|
||||
|
||||
Behoben:
|
||||
• Entfernung von vollständig angesehenen Videos aus der Wiedergabeliste
|
||||
• Freigabemenü-Design und „Zur Wiedergabeliste hinzufügen“-Eintrag
|
||||
Behoben
|
||||
• Entfernen vollständig angesehener Videos aus Wiedergabeliste
|
||||
• …
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user