Merge pull request #13121 from theimpulson/forwardport
Merge dev into refactor
This commit is contained in:
commit
50b9a7b7f6
@ -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)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@ -6,6 +6,8 @@ import android.os.Parcelable
|
||||
import androidx.test.core.app.ApplicationProvider
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.filters.LargeTest
|
||||
import java.io.IOException
|
||||
import java.net.SocketTimeoutException
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertNull
|
||||
@ -16,8 +18,6 @@ import org.schabi.newpipe.R
|
||||
import org.schabi.newpipe.extractor.ServiceList
|
||||
import org.schabi.newpipe.extractor.exceptions.ParsingException
|
||||
import org.schabi.newpipe.extractor.exceptions.ReCaptchaException
|
||||
import java.io.IOException
|
||||
import java.net.SocketTimeoutException
|
||||
|
||||
/**
|
||||
* Instrumented tests for {@link ErrorInfo}.
|
||||
|
||||
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -7,6 +7,7 @@ import androidx.compose.ui.test.junit4.createAndroidComposeRule
|
||||
import androidx.compose.ui.test.onNodeWithText
|
||||
import androidx.compose.ui.test.performClick
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import java.net.UnknownHostException
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
@ -16,7 +17,6 @@ import org.schabi.newpipe.error.UserAction
|
||||
import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException
|
||||
import org.schabi.newpipe.extractor.exceptions.ReCaptchaException
|
||||
import org.schabi.newpipe.ui.theme.AppTheme
|
||||
import java.net.UnknownHostException
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class ErrorPanelTest {
|
||||
|
||||
@ -34,6 +34,7 @@ import androidx.paging.LoadState
|
||||
import androidx.paging.LoadStates
|
||||
import androidx.paging.PagingData
|
||||
import androidx.paging.compose.collectAsLazyPagingItems
|
||||
import java.net.UnknownHostException
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
@ -54,7 +55,6 @@ import org.schabi.newpipe.ui.emptystate.EmptyStateComposable
|
||||
import org.schabi.newpipe.ui.emptystate.EmptyStateSpec
|
||||
import org.schabi.newpipe.ui.theme.AppTheme
|
||||
import org.schabi.newpipe.viewmodels.util.Resource
|
||||
import java.net.UnknownHostException
|
||||
|
||||
class CommentSectionInstrumentedTest {
|
||||
|
||||
@ -277,6 +277,7 @@ private fun TestCommentSection(
|
||||
is Resource.Loading -> item {
|
||||
LoadingIndicator(modifier = Modifier.padding(top = 8.dp))
|
||||
}
|
||||
|
||||
is Resource.Success -> {
|
||||
val commentInfo = (uiState as Resource.Success<CommentInfo>).data
|
||||
val count = commentInfo.commentCount
|
||||
@ -290,6 +291,7 @@ private fun TestCommentSection(
|
||||
.heightIn(min = 128.dp)
|
||||
)
|
||||
}
|
||||
|
||||
count == 0 -> item {
|
||||
EmptyStateComposable(
|
||||
spec = EmptyStateSpec.NoComments,
|
||||
@ -298,6 +300,7 @@ private fun TestCommentSection(
|
||||
.heightIn(min = 128.dp)
|
||||
)
|
||||
}
|
||||
|
||||
else -> {
|
||||
if (count >= 0) {
|
||||
item {
|
||||
@ -314,6 +317,7 @@ private fun TestCommentSection(
|
||||
is LoadState.Loading -> item {
|
||||
LoadingIndicator(modifier = Modifier.padding(top = 8.dp))
|
||||
}
|
||||
|
||||
is LoadState.Error -> item {
|
||||
Box(
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
@ -329,6 +333,7 @@ private fun TestCommentSection(
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
else -> items(comments.itemCount) { index ->
|
||||
Comment(comment = comments[index]!!) {}
|
||||
}
|
||||
@ -336,6 +341,7 @@ private fun TestCommentSection(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
is Resource.Error -> item {
|
||||
Box(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
|
||||
@ -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))
|
||||
}
|
||||
|
||||
@ -22,6 +22,9 @@ import io.reactivex.rxjava3.exceptions.OnErrorNotImplementedException
|
||||
import io.reactivex.rxjava3.exceptions.UndeliverableException
|
||||
import io.reactivex.rxjava3.functions.Consumer
|
||||
import io.reactivex.rxjava3.plugins.RxJavaPlugins
|
||||
import java.io.IOException
|
||||
import java.io.InterruptedIOException
|
||||
import java.net.SocketException
|
||||
import org.acra.ACRA.init
|
||||
import org.acra.ACRA.isACRASenderServiceProcess
|
||||
import org.acra.config.CoreConfigurationBuilder
|
||||
@ -38,9 +41,6 @@ import org.schabi.newpipe.util.StateSaver
|
||||
import org.schabi.newpipe.util.image.ImageStrategy
|
||||
import org.schabi.newpipe.util.image.PreferredImageQuality
|
||||
import org.schabi.newpipe.util.potoken.PoTokenProviderImpl
|
||||
import java.io.IOException
|
||||
import java.io.InterruptedIOException
|
||||
import java.net.SocketException
|
||||
|
||||
/*
|
||||
* Copyright (C) Hans-Christoph Steiner 2016 <hans@eds.org>
|
||||
@ -101,7 +101,7 @@ open class App :
|
||||
NewPipe.init(
|
||||
getDownloader(),
|
||||
Localization.getPreferredLocalization(this),
|
||||
Localization.getPreferredContentCountry(this),
|
||||
Localization.getPreferredContentCountry(this)
|
||||
)
|
||||
Localization.initPrettyTime(Localization.resolvePrettyTime())
|
||||
|
||||
@ -118,9 +118,9 @@ open class App :
|
||||
this,
|
||||
prefs.getString(
|
||||
getString(R.string.image_quality_key),
|
||||
getString(R.string.image_quality_default),
|
||||
),
|
||||
),
|
||||
getString(R.string.image_quality_default)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
configureRxJavaErrorHandler()
|
||||
@ -128,15 +128,14 @@ open class App :
|
||||
YoutubeStreamExtractor.setPoTokenProvider(PoTokenProviderImpl)
|
||||
}
|
||||
|
||||
override fun newImageLoader(context: Context): ImageLoader =
|
||||
ImageLoader
|
||||
.Builder(this)
|
||||
.logger(if (BuildConfig.DEBUG) DebugLogger() else null)
|
||||
.allowRgb565(getSystemService<ActivityManager>()!!.isLowRamDevice)
|
||||
.crossfade(true)
|
||||
.components {
|
||||
add(OkHttpNetworkFetcherFactory(callFactory = DownloaderImpl.getInstance().client))
|
||||
}.build()
|
||||
override fun newImageLoader(context: Context): ImageLoader = ImageLoader
|
||||
.Builder(this)
|
||||
.logger(if (BuildConfig.DEBUG) DebugLogger() else null)
|
||||
.allowRgb565(getSystemService<ActivityManager>()!!.isLowRamDevice)
|
||||
.crossfade(true)
|
||||
.components {
|
||||
add(OkHttpNetworkFetcherFactory(callFactory = DownloaderImpl.getInstance().client))
|
||||
}.build()
|
||||
|
||||
protected open fun getDownloader(): Downloader {
|
||||
val downloader = DownloaderImpl.init(null)
|
||||
@ -190,7 +189,7 @@ open class App :
|
||||
IOException::class.java,
|
||||
SocketException::class.java, // blocking code disposed
|
||||
InterruptedException::class.java,
|
||||
InterruptedIOException::class.java,
|
||||
InterruptedIOException::class.java
|
||||
)
|
||||
}
|
||||
|
||||
@ -204,7 +203,7 @@ open class App :
|
||||
OnErrorNotImplementedException::class.java,
|
||||
MissingBackpressureException::class.java,
|
||||
// bug in operator
|
||||
IllegalStateException::class.java,
|
||||
IllegalStateException::class.java
|
||||
)
|
||||
}
|
||||
|
||||
@ -215,7 +214,7 @@ open class App :
|
||||
.uncaughtExceptionHandler
|
||||
.uncaughtException(Thread.currentThread(), throwable)
|
||||
}
|
||||
},
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@ -241,7 +240,7 @@ open class App :
|
||||
NotificationChannelCompat
|
||||
.Builder(
|
||||
getString(R.string.notification_channel_id),
|
||||
NotificationManagerCompat.IMPORTANCE_LOW,
|
||||
NotificationManagerCompat.IMPORTANCE_LOW
|
||||
).setName(getString(R.string.notification_channel_name))
|
||||
.setDescription(getString(R.string.notification_channel_description))
|
||||
.build()
|
||||
@ -249,7 +248,7 @@ open class App :
|
||||
NotificationChannelCompat
|
||||
.Builder(
|
||||
getString(R.string.app_update_notification_channel_id),
|
||||
NotificationManagerCompat.IMPORTANCE_LOW,
|
||||
NotificationManagerCompat.IMPORTANCE_LOW
|
||||
).setName(getString(R.string.app_update_notification_channel_name))
|
||||
.setDescription(getString(R.string.app_update_notification_channel_description))
|
||||
.build()
|
||||
@ -257,7 +256,7 @@ open class App :
|
||||
NotificationChannelCompat
|
||||
.Builder(
|
||||
getString(R.string.hash_channel_id),
|
||||
NotificationManagerCompat.IMPORTANCE_HIGH,
|
||||
NotificationManagerCompat.IMPORTANCE_HIGH
|
||||
).setName(getString(R.string.hash_channel_name))
|
||||
.setDescription(getString(R.string.hash_channel_description))
|
||||
.build()
|
||||
@ -265,7 +264,7 @@ open class App :
|
||||
NotificationChannelCompat
|
||||
.Builder(
|
||||
getString(R.string.error_report_channel_id),
|
||||
NotificationManagerCompat.IMPORTANCE_LOW,
|
||||
NotificationManagerCompat.IMPORTANCE_LOW
|
||||
).setName(getString(R.string.error_report_channel_name))
|
||||
.setDescription(getString(R.string.error_report_channel_description))
|
||||
.build()
|
||||
@ -273,7 +272,7 @@ open class App :
|
||||
NotificationChannelCompat
|
||||
.Builder(
|
||||
getString(R.string.streams_notification_channel_id),
|
||||
NotificationManagerCompat.IMPORTANCE_DEFAULT,
|
||||
NotificationManagerCompat.IMPORTANCE_DEFAULT
|
||||
).setName(getString(R.string.streams_notification_channel_name))
|
||||
.setDescription(getString(R.string.streams_notification_channel_description))
|
||||
.build()
|
||||
|
||||
@ -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)
|
||||
)
|
||||
|
||||
@ -6,6 +6,7 @@ import androidx.annotation.StringRes
|
||||
import com.google.android.exoplayer2.ExoPlaybackException
|
||||
import com.google.android.exoplayer2.upstream.HttpDataSource
|
||||
import com.google.android.exoplayer2.upstream.Loader
|
||||
import java.net.UnknownHostException
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import org.schabi.newpipe.R
|
||||
import org.schabi.newpipe.extractor.Info
|
||||
@ -28,7 +29,6 @@ import org.schabi.newpipe.ktx.isNetworkRelated
|
||||
import org.schabi.newpipe.player.mediasource.FailedMediaSource
|
||||
import org.schabi.newpipe.player.resolver.PlaybackResolver
|
||||
import org.schabi.newpipe.util.Localization
|
||||
import java.net.UnknownHostException
|
||||
|
||||
/**
|
||||
* An error has occurred in the app. This class contains plain old parcelable data that can be used
|
||||
@ -59,7 +59,7 @@ class ErrorInfo private constructor(
|
||||
* If present, this resource can alternatively be opened in browser (useful if NewPipe is
|
||||
* badly broken).
|
||||
*/
|
||||
val openInBrowserUrl: String?,
|
||||
val openInBrowserUrl: String?
|
||||
) : Parcelable {
|
||||
|
||||
@JvmOverloads
|
||||
@ -68,7 +68,7 @@ class ErrorInfo private constructor(
|
||||
userAction: UserAction,
|
||||
request: String,
|
||||
serviceId: Int? = null,
|
||||
openInBrowserUrl: String? = null,
|
||||
openInBrowserUrl: String? = null
|
||||
) : this(
|
||||
throwableToStringList(throwable),
|
||||
userAction,
|
||||
@ -78,7 +78,7 @@ class ErrorInfo private constructor(
|
||||
isReportable(throwable),
|
||||
isRetryable(throwable),
|
||||
(throwable as? ReCaptchaException)?.url,
|
||||
openInBrowserUrl,
|
||||
openInBrowserUrl
|
||||
)
|
||||
|
||||
@JvmOverloads
|
||||
@ -87,7 +87,7 @@ class ErrorInfo private constructor(
|
||||
userAction: UserAction,
|
||||
request: String,
|
||||
serviceId: Int? = null,
|
||||
openInBrowserUrl: String? = null,
|
||||
openInBrowserUrl: String? = null
|
||||
) : this(
|
||||
throwableListToStringList(throwables),
|
||||
userAction,
|
||||
@ -97,7 +97,7 @@ class ErrorInfo private constructor(
|
||||
throwables.any(::isReportable),
|
||||
throwables.isEmpty() || throwables.any(::isRetryable),
|
||||
throwables.firstNotNullOfOrNull { it as? ReCaptchaException }?.url,
|
||||
openInBrowserUrl,
|
||||
openInBrowserUrl
|
||||
)
|
||||
|
||||
// constructor to manually build ErrorInfo when no throwable is available
|
||||
@ -118,7 +118,7 @@ class ErrorInfo private constructor(
|
||||
throwable: Throwable,
|
||||
userAction: UserAction,
|
||||
request: String,
|
||||
info: Info?,
|
||||
info: Info?
|
||||
) :
|
||||
this(throwable, userAction, request, info?.serviceId, info?.url)
|
||||
|
||||
@ -127,7 +127,7 @@ class ErrorInfo private constructor(
|
||||
throwables: List<Throwable>,
|
||||
userAction: UserAction,
|
||||
request: String,
|
||||
info: Info?,
|
||||
info: Info?
|
||||
) :
|
||||
this(throwables, userAction, request, info?.serviceId, info?.url)
|
||||
|
||||
@ -144,7 +144,7 @@ class ErrorInfo private constructor(
|
||||
class ErrorMessage(
|
||||
@StringRes
|
||||
private val stringRes: Int,
|
||||
private vararg val formatArgs: String,
|
||||
private vararg val formatArgs: String
|
||||
) : Parcelable {
|
||||
fun getString(context: Context): String {
|
||||
// use Localization.compatGetString() just in case context is not AppCompatActivity
|
||||
@ -158,21 +158,19 @@ class ErrorInfo private constructor(
|
||||
|
||||
const val SERVICE_NONE = "<unknown_service>"
|
||||
|
||||
private fun getServiceName(serviceId: Int?) =
|
||||
// not using getNameOfServiceById since we want to accept a nullable serviceId and we
|
||||
private fun getServiceName(serviceId: Int?) = // not using getNameOfServiceById since we want to accept a nullable serviceId and we
|
||||
// want to default to SERVICE_NONE
|
||||
ServiceList.all()?.firstOrNull { it.serviceId == serviceId }?.serviceInfo?.name
|
||||
?: SERVICE_NONE
|
||||
|
||||
fun throwableToStringList(throwable: Throwable) = arrayOf(throwable.stackTraceToString())
|
||||
|
||||
fun throwableListToStringList(throwableList: List<Throwable>) =
|
||||
throwableList.map { it.stackTraceToString() }.toTypedArray()
|
||||
fun throwableListToStringList(throwableList: List<Throwable>) = throwableList.map { it.stackTraceToString() }.toTypedArray()
|
||||
|
||||
fun getMessage(
|
||||
throwable: Throwable?,
|
||||
action: UserAction?,
|
||||
serviceId: Int?,
|
||||
serviceId: Int?
|
||||
): ErrorMessage {
|
||||
return when {
|
||||
// player exceptions
|
||||
@ -191,18 +189,24 @@ class ErrorInfo private constructor(
|
||||
ErrorMessage(R.string.player_http_invalid_status, cause.responseCode.toString())
|
||||
}
|
||||
}
|
||||
|
||||
cause is Loader.UnexpectedLoaderException && cause.cause is ExtractionException ->
|
||||
getMessage(throwable, action, serviceId)
|
||||
|
||||
throwable.type == ExoPlaybackException.TYPE_SOURCE ->
|
||||
ErrorMessage(R.string.player_stream_failure)
|
||||
|
||||
throwable.type == ExoPlaybackException.TYPE_UNEXPECTED ->
|
||||
ErrorMessage(R.string.player_recoverable_failure)
|
||||
|
||||
else ->
|
||||
ErrorMessage(R.string.player_unrecoverable_failure)
|
||||
}
|
||||
}
|
||||
|
||||
throwable is FailedMediaSource.FailedMediaSourceException ->
|
||||
getMessage(throwable.cause, action, serviceId)
|
||||
|
||||
throwable is PlaybackResolver.ResolverException ->
|
||||
ErrorMessage(R.string.player_stream_failure)
|
||||
|
||||
@ -218,34 +222,46 @@ class ErrorInfo private constructor(
|
||||
)
|
||||
}
|
||||
?: ErrorMessage(R.string.account_terminated)
|
||||
|
||||
throwable is AgeRestrictedContentException ->
|
||||
ErrorMessage(R.string.restricted_video_no_stream)
|
||||
|
||||
throwable is GeographicRestrictionException ->
|
||||
ErrorMessage(R.string.georestricted_content)
|
||||
|
||||
throwable is PaidContentException ->
|
||||
ErrorMessage(R.string.paid_content)
|
||||
|
||||
throwable is PrivateContentException ->
|
||||
ErrorMessage(R.string.private_content)
|
||||
|
||||
throwable is SoundCloudGoPlusContentException ->
|
||||
ErrorMessage(R.string.soundcloud_go_plus_content)
|
||||
|
||||
throwable is UnsupportedContentInCountryException ->
|
||||
ErrorMessage(R.string.unsupported_content_in_country)
|
||||
|
||||
throwable is YoutubeMusicPremiumContentException ->
|
||||
ErrorMessage(R.string.youtube_music_premium_content)
|
||||
|
||||
throwable is SignInConfirmNotBotException ->
|
||||
ErrorMessage(R.string.sign_in_confirm_not_bot_error, getServiceName(serviceId))
|
||||
|
||||
throwable is ContentNotAvailableException ->
|
||||
ErrorMessage(R.string.content_not_available)
|
||||
|
||||
// other extractor exceptions
|
||||
throwable is ContentNotSupportedException ->
|
||||
ErrorMessage(R.string.content_not_supported)
|
||||
|
||||
// ReCaptchas will be handled in a special way anyway
|
||||
throwable is ReCaptchaException ->
|
||||
ErrorMessage(R.string.recaptcha_request_toast)
|
||||
|
||||
// test this at the end as many exceptions could be a subclass of IOException
|
||||
throwable != null && throwable.isNetworkRelated ->
|
||||
ErrorMessage(R.string.network_error)
|
||||
|
||||
// an extraction exception unrelated to the network
|
||||
// is likely an issue with parsing the website
|
||||
throwable is ExtractionException ->
|
||||
@ -254,16 +270,22 @@ class ErrorInfo private constructor(
|
||||
// user actions (in case the exception is null or unrecognizable)
|
||||
action == UserAction.UI_ERROR ->
|
||||
ErrorMessage(R.string.app_ui_crash)
|
||||
|
||||
action == UserAction.REQUESTED_COMMENTS ->
|
||||
ErrorMessage(R.string.error_unable_to_load_comments)
|
||||
|
||||
action == UserAction.SUBSCRIPTION_CHANGE ->
|
||||
ErrorMessage(R.string.subscription_change_failed)
|
||||
|
||||
action == UserAction.SUBSCRIPTION_UPDATE ->
|
||||
ErrorMessage(R.string.subscription_update_failed)
|
||||
|
||||
action == UserAction.LOAD_IMAGE ->
|
||||
ErrorMessage(R.string.could_not_load_thumbnails)
|
||||
|
||||
action == UserAction.DOWNLOAD_OPEN_DIALOG ->
|
||||
ErrorMessage(R.string.could_not_setup_download_menu)
|
||||
|
||||
else ->
|
||||
ErrorMessage(R.string.error_snackbar_message)
|
||||
}
|
||||
@ -274,18 +296,23 @@ class ErrorInfo private constructor(
|
||||
// we don't have an exception, so this is a manually built error, which likely
|
||||
// indicates that it's important and is thus reportable
|
||||
null -> true
|
||||
|
||||
// a recaptcha was detected, and the user needs to solve it, there is no use in
|
||||
// letting users report it
|
||||
is ReCaptchaException -> false
|
||||
|
||||
// the service explicitly said that content is not available (e.g. age restrictions,
|
||||
// video deleted, etc.), there is no use in letting users report it
|
||||
is ContentNotAvailableException -> false
|
||||
|
||||
// we know the content is not supported, no need to let the user report it
|
||||
is ContentNotSupportedException -> false
|
||||
|
||||
// happens often when there is no internet connection; we don't use
|
||||
// `throwable.isNetworkRelated` since any `IOException` would make that function
|
||||
// return true, but not all `IOException`s are network related
|
||||
is UnknownHostException -> false
|
||||
|
||||
// by default, this is an unexpected exception, which the user could report
|
||||
else -> true
|
||||
}
|
||||
@ -295,8 +322,10 @@ class ErrorInfo private constructor(
|
||||
return when (throwable) {
|
||||
// we know the content is not available, retrying won't help
|
||||
is ContentNotAvailableException -> false
|
||||
|
||||
// we know the content is not supported, retrying won't help
|
||||
is ContentNotSupportedException -> false
|
||||
|
||||
// by default (including if throwable is null), enable retrying (though the retry
|
||||
// button will be shown only if a way to perform the retry is implemented)
|
||||
else -> true
|
||||
|
||||
@ -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")
|
||||
}
|
||||
|
||||
@ -56,6 +56,11 @@ import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||
import io.reactivex.rxjava3.disposables.Disposable
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import java.util.LinkedList
|
||||
import java.util.concurrent.TimeUnit
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
import org.schabi.newpipe.App
|
||||
import org.schabi.newpipe.R
|
||||
import org.schabi.newpipe.database.stream.model.StreamEntity
|
||||
@ -115,11 +120,6 @@ import org.schabi.newpipe.util.ThemeHelper
|
||||
import org.schabi.newpipe.util.external_communication.KoreUtils
|
||||
import org.schabi.newpipe.util.external_communication.ShareUtils
|
||||
import org.schabi.newpipe.util.image.CoilHelper
|
||||
import java.util.LinkedList
|
||||
import java.util.concurrent.TimeUnit
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
|
||||
class VideoDetailFragment :
|
||||
BaseStateFragment<StreamInfo>(),
|
||||
@ -128,15 +128,29 @@ class VideoDetailFragment :
|
||||
OnKeyDownListener {
|
||||
|
||||
// stream info
|
||||
@JvmField @State var serviceId: Int = NO_SERVICE_ID
|
||||
@JvmField @State var title: String = ""
|
||||
@JvmField @State var url: String? = null
|
||||
@JvmField
|
||||
@State
|
||||
var serviceId: Int = NO_SERVICE_ID
|
||||
|
||||
@JvmField
|
||||
@State
|
||||
var title: String = ""
|
||||
|
||||
@JvmField
|
||||
@State
|
||||
var url: String? = null
|
||||
private var currentInfo: StreamInfo? = null
|
||||
|
||||
// player objects
|
||||
private var playQueue: PlayQueue? = null
|
||||
@JvmField @State var autoPlayEnabled: Boolean = true
|
||||
@JvmField @State var originalOrientation: Int = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
|
||||
|
||||
@JvmField
|
||||
@State
|
||||
var autoPlayEnabled: Boolean = true
|
||||
|
||||
@JvmField
|
||||
@State
|
||||
var originalOrientation: Int = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
|
||||
private var playerService: PlayerService? = null
|
||||
private var player: Player? = null
|
||||
|
||||
@ -152,7 +166,9 @@ class VideoDetailFragment :
|
||||
private var showRelatedItems = false
|
||||
private var showDescription = false
|
||||
private lateinit var selectedTabTag: String
|
||||
|
||||
@AttrRes val tabIcons = ArrayList<Int>()
|
||||
|
||||
@StringRes val tabContentDescriptions = ArrayList<Int>()
|
||||
private var tabSettingsChanged = false
|
||||
private var lastAppBarVerticalOffset = Int.Companion.MAX_VALUE // prevents useless updates
|
||||
@ -172,8 +188,13 @@ class VideoDetailFragment :
|
||||
}
|
||||
|
||||
// bottom sheet
|
||||
@JvmField @State var bottomSheetState: Int = BottomSheetBehavior.STATE_EXPANDED
|
||||
@JvmField @State var lastStableBottomSheetState: Int = BottomSheetBehavior.STATE_EXPANDED
|
||||
@JvmField
|
||||
@State
|
||||
var bottomSheetState: Int = BottomSheetBehavior.STATE_EXPANDED
|
||||
|
||||
@JvmField
|
||||
@State
|
||||
var lastStableBottomSheetState: Int = BottomSheetBehavior.STATE_EXPANDED
|
||||
private lateinit var bottomSheetBehavior: BottomSheetBehavior<FrameLayout?>
|
||||
private lateinit var bottomSheetCallback: BottomSheetCallback
|
||||
private lateinit var broadcastReceiver: BroadcastReceiver
|
||||
@ -244,7 +265,8 @@ class VideoDetailFragment :
|
||||
showRelatedItems = prefs.getBoolean(getString(R.string.show_next_video_key), true)
|
||||
showDescription = prefs.getBoolean(getString(R.string.show_description_key), true)
|
||||
selectedTabTag = prefs.getString(
|
||||
getString(R.string.stream_info_selected_tab_key), COMMENTS_TAB_TAG
|
||||
getString(R.string.stream_info_selected_tab_key),
|
||||
COMMENTS_TAB_TAG
|
||||
)!!
|
||||
prefs.registerOnSharedPreferenceChangeListener(preferenceChangeListener)
|
||||
|
||||
@ -258,7 +280,8 @@ class VideoDetailFragment :
|
||||
}
|
||||
}
|
||||
activity.contentResolver.registerContentObserver(
|
||||
Settings.System.getUriFor(Settings.System.ACCELEROMETER_ROTATION), false,
|
||||
Settings.System.getUriFor(Settings.System.ACCELEROMETER_ROTATION),
|
||||
false,
|
||||
settingsContentObserver!!
|
||||
)
|
||||
}
|
||||
@ -357,7 +380,13 @@ class VideoDetailFragment :
|
||||
if (requestCode == ReCaptchaActivity.RECAPTCHA_REQUEST) {
|
||||
if (resultCode == Activity.RESULT_OK) {
|
||||
NavigationHelper.openVideoDetailFragment(
|
||||
requireContext(), getFM(), serviceId, url, title, null, false
|
||||
requireContext(),
|
||||
getFM(),
|
||||
serviceId,
|
||||
url,
|
||||
title,
|
||||
null,
|
||||
false
|
||||
)
|
||||
} else {
|
||||
Log.e(TAG, "ReCaptcha failed")
|
||||
@ -562,7 +591,7 @@ class VideoDetailFragment :
|
||||
KoreUtils.shouldShowPlayWithKodi(requireContext(), serviceId)
|
||||
binding.detailControlsCrashThePlayer.isVisible =
|
||||
DEBUG && PreferenceManager.getDefaultSharedPreferences(requireContext())
|
||||
.getBoolean(getString(R.string.show_crash_the_player_key), false)
|
||||
.getBoolean(getString(R.string.show_crash_the_player_key), false)
|
||||
|
||||
accommodateForTvAndDesktopMode()
|
||||
}
|
||||
@ -845,7 +874,9 @@ class VideoDetailFragment :
|
||||
private fun updateTabs(info: StreamInfo) {
|
||||
if (showRelatedItems) {
|
||||
when (val relatedItemsLayout = binding.relatedItemsLayout) {
|
||||
null -> pageAdapter.updateItem(RELATED_TAB_TAG, getInstance(info)) // phone
|
||||
// phone
|
||||
null -> pageAdapter.updateItem(RELATED_TAB_TAG, getInstance(info))
|
||||
|
||||
else -> { // tablet + TV
|
||||
getChildFragmentManager().beginTransaction()
|
||||
.replace(R.id.relatedItemsLayout, getInstance(info))
|
||||
@ -896,7 +927,9 @@ class VideoDetailFragment :
|
||||
val viewPagerVisibleHeight = height - pagerHitRect.top
|
||||
// see TabLayout.DEFAULT_HEIGHT, which is equal to 48dp
|
||||
val tabLayoutHeight = TypedValue.applyDimension(
|
||||
TypedValue.COMPLEX_UNIT_DIP, 48f, resources.displayMetrics
|
||||
TypedValue.COMPLEX_UNIT_DIP,
|
||||
48f,
|
||||
resources.displayMetrics
|
||||
)
|
||||
|
||||
if (viewPagerVisibleHeight > tabLayoutHeight * 2) {
|
||||
@ -997,7 +1030,7 @@ class VideoDetailFragment :
|
||||
}
|
||||
|
||||
if (PreferenceManager.getDefaultSharedPreferences(activity)
|
||||
.getBoolean(this.getString(R.string.use_external_video_player_key), false)
|
||||
.getBoolean(this.getString(R.string.use_external_video_player_key), false)
|
||||
) {
|
||||
showExternalVideoPlaybackDialog()
|
||||
} else {
|
||||
@ -1046,7 +1079,10 @@ class VideoDetailFragment :
|
||||
tryAddVideoPlayerView()
|
||||
|
||||
val playerIntent = NavigationHelper.getPlayerIntent(
|
||||
requireContext(), PlayerService::class.java, queue, PlayerIntentType.AllOthers
|
||||
requireContext(),
|
||||
PlayerService::class.java,
|
||||
queue,
|
||||
PlayerIntentType.AllOthers
|
||||
)
|
||||
.putExtra(Player.PLAY_WHEN_READY, autoPlayEnabled)
|
||||
.putExtra(Player.RESUME_PLAYBACK, true)
|
||||
@ -1102,7 +1138,10 @@ class VideoDetailFragment :
|
||||
selectedStream: Stream
|
||||
) {
|
||||
NavigationHelper.playOnExternalPlayer(
|
||||
context, info.name, info.subChannelName, selectedStream
|
||||
context,
|
||||
info.name,
|
||||
info.subChannelName,
|
||||
selectedStream
|
||||
)
|
||||
|
||||
val recordManager = HistoryRecordManager(requireContext())
|
||||
@ -1173,10 +1212,11 @@ class VideoDetailFragment :
|
||||
|
||||
private val preDrawListener: OnPreDrawListener = OnPreDrawListener {
|
||||
view?.let { view ->
|
||||
val decorView = if (DeviceUtils.isInMultiWindow(activity))
|
||||
val decorView = if (DeviceUtils.isInMultiWindow(activity)) {
|
||||
view
|
||||
else
|
||||
} else {
|
||||
activity.window.decorView
|
||||
}
|
||||
setHeightThumbnail(decorView.height, resources.displayMetrics)
|
||||
view.getViewTreeObserver().removeOnPreDrawListener(preDrawListener)
|
||||
}
|
||||
@ -1196,10 +1236,11 @@ class VideoDetailFragment :
|
||||
|
||||
if (this.isFullscreen) {
|
||||
val height = (
|
||||
if (DeviceUtils.isInMultiWindow(activity))
|
||||
if (DeviceUtils.isInMultiWindow(activity)) {
|
||||
requireView()
|
||||
else
|
||||
} else {
|
||||
activity.window.decorView
|
||||
}
|
||||
).height
|
||||
// Height is zero when the view is not yet displayed like after orientation change
|
||||
if (height != 0) {
|
||||
@ -1210,10 +1251,11 @@ class VideoDetailFragment :
|
||||
} else {
|
||||
val isPortrait = metrics.heightPixels > metrics.widthPixels
|
||||
val height = (
|
||||
if (isPortrait)
|
||||
if (isPortrait) {
|
||||
metrics.widthPixels / (16.0f / 9.0f)
|
||||
else
|
||||
} else {
|
||||
metrics.heightPixels / 2.0f
|
||||
}
|
||||
).toInt()
|
||||
setHeightThumbnail(height, metrics)
|
||||
}
|
||||
@ -1288,7 +1330,9 @@ class VideoDetailFragment :
|
||||
override fun onReceive(context: Context?, intent: Intent) {
|
||||
when (intent.action) {
|
||||
ACTION_SHOW_MAIN_PLAYER -> bottomSheetBehavior.setState(BottomSheetBehavior.STATE_EXPANDED)
|
||||
|
||||
ACTION_HIDE_MAIN_PLAYER -> bottomSheetBehavior.setState(BottomSheetBehavior.STATE_HIDDEN)
|
||||
|
||||
ACTION_PLAYER_STARTED -> {
|
||||
// If the state is not hidden we don't need to show the mini player
|
||||
if (bottomSheetBehavior.getState() == BottomSheetBehavior.STATE_HIDDEN) {
|
||||
@ -1446,8 +1490,10 @@ class VideoDetailFragment :
|
||||
checkUpdateProgressInfo(info)
|
||||
CoilHelper.loadDetailsThumbnail(binding.detailThumbnailImageView, info.thumbnails)
|
||||
ExtractorHelper.showMetaInfoInTextView(
|
||||
info.metaInfo, binding.detailMetaInfoTextView,
|
||||
binding.detailMetaInfoSeparator, disposables
|
||||
info.metaInfo,
|
||||
binding.detailMetaInfoTextView,
|
||||
binding.detailMetaInfoSeparator,
|
||||
disposables
|
||||
)
|
||||
|
||||
if (playerIsStopped) {
|
||||
@ -1561,7 +1607,9 @@ class VideoDetailFragment :
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
{ state -> updatePlaybackProgress(state.progressMillis, info.duration * 1000) },
|
||||
{ throwable -> /* impossible due to the onErrorComplete() */ },
|
||||
{ throwable ->
|
||||
/* impossible due to the onErrorComplete() */
|
||||
},
|
||||
{
|
||||
/* onComplete */
|
||||
binding.positionView.visibility = View.GONE
|
||||
@ -1607,7 +1655,7 @@ class VideoDetailFragment :
|
||||
Log.d(
|
||||
TAG,
|
||||
"onQueueUpdate() called with: serviceId = [$serviceId], url = [${
|
||||
url}], name = [$title], playQueue = [$playQueue]"
|
||||
url}], name = [$title], playQueue = [$playQueue]"
|
||||
)
|
||||
}
|
||||
|
||||
@ -1787,7 +1835,8 @@ class VideoDetailFragment :
|
||||
activity.window.decorView.systemUiVisibility = 0
|
||||
activity.window.clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN)
|
||||
activity.window.statusBarColor = ThemeHelper.resolveColorFromAttr(
|
||||
requireContext(), android.R.attr.colorPrimary
|
||||
requireContext(),
|
||||
android.R.attr.colorPrimary
|
||||
)
|
||||
}
|
||||
|
||||
@ -2013,7 +2062,8 @@ class VideoDetailFragment :
|
||||
|
||||
if (audioTracks.isEmpty()) {
|
||||
Toast.makeText(
|
||||
activity, R.string.no_audio_streams_available_for_external_players,
|
||||
activity,
|
||||
R.string.no_audio_streams_available_for_external_players,
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
} else if (audioTracks.size == 1) {
|
||||
@ -2053,6 +2103,7 @@ class VideoDetailFragment :
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Bottom mini player
|
||||
////////////////////////////////////////////////////////////////////////// */
|
||||
|
||||
/**
|
||||
* That's for Android TV support. Move focus from main fragment to the player or back
|
||||
* based on what is currently selected
|
||||
@ -2311,6 +2362,7 @@ class VideoDetailFragment :
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// OwnStack
|
||||
////////////////////////////////////////////////////////////////////////// */
|
||||
|
||||
/**
|
||||
* Stack that contains the "navigation history".<br></br>
|
||||
* The peek is the current video.
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -37,7 +37,10 @@ class StreamSegmentItem(
|
||||
viewBinding.textViewStartSeconds.text =
|
||||
Localization.getDurationString(item.startTimeSeconds.toLong())
|
||||
viewBinding.root.setOnClickListener { onClick.onItemClick(this, item.startTimeSeconds) }
|
||||
viewBinding.root.setOnLongClickListener { onClick.onItemLongClick(this, item.startTimeSeconds); true }
|
||||
viewBinding.root.setOnLongClickListener {
|
||||
onClick.onItemLongClick(this, item.startTimeSeconds)
|
||||
true
|
||||
}
|
||||
viewBinding.root.isSelected = isSelected
|
||||
}
|
||||
|
||||
|
||||
@ -9,5 +9,5 @@ inline fun Bitmap.scale(
|
||||
width: Int,
|
||||
height: Int,
|
||||
srcRect: Rect? = null,
|
||||
scaleInLinearSpace: Boolean = true,
|
||||
scaleInLinearSpace: Boolean = true
|
||||
) = BitmapCompat.createScaledBitmap(this, width, height, srcRect, scaleInLinearSpace)
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -6,6 +6,8 @@ import android.view.View
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.preference.PreferenceManager
|
||||
import com.xwray.groupie.viewbinding.BindableItem
|
||||
import java.util.concurrent.TimeUnit
|
||||
import java.util.function.Consumer
|
||||
import org.schabi.newpipe.MainActivity
|
||||
import org.schabi.newpipe.R
|
||||
import org.schabi.newpipe.database.stream.StreamWithState
|
||||
@ -20,8 +22,6 @@ import org.schabi.newpipe.extractor.stream.StreamType.VIDEO_STREAM
|
||||
import org.schabi.newpipe.util.Localization
|
||||
import org.schabi.newpipe.util.StreamTypeUtil
|
||||
import org.schabi.newpipe.util.image.CoilHelper
|
||||
import java.util.concurrent.TimeUnit
|
||||
import java.util.function.Consumer
|
||||
|
||||
data class StreamItem(
|
||||
val streamWithState: StreamWithState,
|
||||
@ -132,6 +132,7 @@ data class StreamItem(
|
||||
viewsAndDate.isEmpty() -> uploadDate!!
|
||||
else -> Localization.concatenateStrings(viewsAndDate, uploadDate)
|
||||
}
|
||||
|
||||
else -> viewsAndDate
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
@ -231,7 +231,8 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
|
||||
val data = result.data?.dataString
|
||||
if (data != null && result.resultCode == Activity.RESULT_OK) {
|
||||
ImportConfirmationDialog.show(
|
||||
this, SubscriptionImportInput.PreviousExportMode(data)
|
||||
this,
|
||||
SubscriptionImportInput.PreviousExportMode(data)
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -272,10 +273,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 +294,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 +310,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 +343,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 +380,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 +412,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)
|
||||
|
||||
|
||||
@ -19,13 +19,13 @@
|
||||
|
||||
package org.schabi.newpipe.local.subscription.workers
|
||||
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
import kotlinx.serialization.ExperimentalSerializationApi
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.decodeFromStream
|
||||
import kotlinx.serialization.json.encodeToStream
|
||||
import org.schabi.newpipe.extractor.subscription.SubscriptionExtractor.InvalidSourceException
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
|
||||
/**
|
||||
* A JSON implementation capable of importing and exporting subscriptions, it has the advantage
|
||||
@ -65,7 +65,7 @@ object ImportExportJsonHelper {
|
||||
@JvmStatic
|
||||
fun writeTo(
|
||||
items: List<SubscriptionItem>,
|
||||
out: OutputStream,
|
||||
out: OutputStream
|
||||
) {
|
||||
json.encodeToStream(SubscriptionData(items), out)
|
||||
}
|
||||
|
||||
@ -25,7 +25,7 @@ import org.schabi.newpipe.R
|
||||
|
||||
class SubscriptionExportWorker(
|
||||
appContext: Context,
|
||||
params: WorkerParameters,
|
||||
params: WorkerParameters
|
||||
) : CoroutineWorker(appContext, params) {
|
||||
// This is needed for API levels < 31 (Android S).
|
||||
override suspend fun getForegroundInfo(): ForegroundInfo {
|
||||
@ -102,7 +102,7 @@ class SubscriptionExportWorker(
|
||||
|
||||
fun schedule(
|
||||
context: Context,
|
||||
uri: Uri,
|
||||
uri: Uri
|
||||
) {
|
||||
val data = workDataOf(EXPORT_PATH to uri.toString())
|
||||
val workRequest =
|
||||
|
||||
@ -31,7 +31,7 @@ import org.schabi.newpipe.util.ExtractorHelper
|
||||
|
||||
class SubscriptionImportWorker(
|
||||
appContext: Context,
|
||||
params: WorkerParameters,
|
||||
params: WorkerParameters
|
||||
) : CoroutineWorker(appContext, params) {
|
||||
// This is needed for API levels < 31 (Android S).
|
||||
override suspend fun getForegroundInfo(): ForegroundInfo {
|
||||
@ -139,7 +139,7 @@ class SubscriptionImportWorker(
|
||||
title: String,
|
||||
text: String?,
|
||||
currentProgress: Int,
|
||||
maxProgress: Int,
|
||||
maxProgress: Int
|
||||
): ForegroundInfo {
|
||||
val notification =
|
||||
NotificationCompat
|
||||
@ -154,7 +154,7 @@ class SubscriptionImportWorker(
|
||||
.addAction(
|
||||
R.drawable.ic_close,
|
||||
applicationContext.getString(R.string.cancel),
|
||||
WorkManager.getInstance(applicationContext).createCancelPendingIntent(id),
|
||||
WorkManager.getInstance(applicationContext).createCancelPendingIntent(id)
|
||||
).apply {
|
||||
if (currentProgress > 0 && maxProgress > 0) {
|
||||
val progressText = "$currentProgress/$maxProgress"
|
||||
@ -187,8 +187,10 @@ class SubscriptionImportWorker(
|
||||
sealed class SubscriptionImportInput : Parcelable {
|
||||
@Parcelize
|
||||
data class ChannelUrlMode(val serviceId: Int, val url: String) : SubscriptionImportInput()
|
||||
|
||||
@Parcelize
|
||||
data class InputStreamMode(val serviceId: Int, val url: String) : SubscriptionImportInput()
|
||||
|
||||
@Parcelize
|
||||
data class PreviousExportMode(val url: String) : SubscriptionImportInput()
|
||||
|
||||
@ -218,6 +220,7 @@ sealed class SubscriptionImportInput : Parcelable {
|
||||
val url = data.getString("url")!!
|
||||
return ChannelUrlMode(serviceId, url)
|
||||
}
|
||||
|
||||
INPUT_STREAM_MODE -> {
|
||||
val serviceId = data.getInt("service_id", -1)
|
||||
if (serviceId == -1) {
|
||||
@ -226,10 +229,12 @@ sealed class SubscriptionImportInput : Parcelable {
|
||||
val url = data.getString("url")!!
|
||||
return InputStreamMode(serviceId, url)
|
||||
}
|
||||
|
||||
PREVIOUS_EXPORT_MODE -> {
|
||||
val url = data.getString("url")!!
|
||||
return PreviousExportMode(url)
|
||||
}
|
||||
|
||||
else -> throw IllegalArgumentException("Unknown mode: $mode")
|
||||
}
|
||||
}
|
||||
|
||||
@ -10,7 +10,7 @@ import org.schabi.newpipe.extractor.comments.CommentsInfo
|
||||
import org.schabi.newpipe.extractor.comments.CommentsInfoItem
|
||||
|
||||
class CommentRepliesSource(
|
||||
private val commentInfo: CommentsInfoItem,
|
||||
private val commentInfo: CommentsInfoItem
|
||||
) : PagingSource<Page, CommentsInfoItem>() {
|
||||
private val service = NewPipe.getService(commentInfo.serviceId)
|
||||
|
||||
|
||||
@ -29,6 +29,8 @@ import android.util.Log
|
||||
import androidx.core.app.ServiceCompat
|
||||
import androidx.media.MediaBrowserServiceCompat
|
||||
import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector
|
||||
import java.lang.ref.WeakReference
|
||||
import java.util.function.Consumer
|
||||
import org.schabi.newpipe.ktx.toDebugString
|
||||
import org.schabi.newpipe.player.mediabrowser.MediaBrowserImpl
|
||||
import org.schabi.newpipe.player.mediabrowser.MediaBrowserPlaybackPreparer
|
||||
@ -36,8 +38,6 @@ import org.schabi.newpipe.player.mediasession.MediaSessionPlayerUi
|
||||
import org.schabi.newpipe.player.notification.NotificationPlayerUi
|
||||
import org.schabi.newpipe.player.notification.NotificationUtil
|
||||
import org.schabi.newpipe.util.ThemeHelper
|
||||
import java.lang.ref.WeakReference
|
||||
import java.util.function.Consumer
|
||||
|
||||
/**
|
||||
* One service for all players.
|
||||
@ -110,7 +110,7 @@ class PlayerService : MediaBrowserServiceCompat() {
|
||||
Log.d(
|
||||
TAG,
|
||||
"onStartCommand() called with: intent = [$intent], extras = [${
|
||||
intent.extras.toDebugString()}], flags = [$flags], startId = [$startId]"
|
||||
intent.extras.toDebugString()}], flags = [$flags], startId = [$startId]"
|
||||
)
|
||||
}
|
||||
|
||||
@ -251,7 +251,7 @@ class PlayerService : MediaBrowserServiceCompat() {
|
||||
Log.d(
|
||||
TAG,
|
||||
"onBind() called with: intent = [$intent], extras = [${
|
||||
intent.extras.toDebugString()}]"
|
||||
intent.extras.toDebugString()}]"
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -43,13 +43,13 @@ object PlayerHolder {
|
||||
private val playQueue: PlayQueue?
|
||||
get() = this.player?.playQueue
|
||||
|
||||
/**
|
||||
* Returns the current [PlayerType] of the [PlayerService] service,
|
||||
* otherwise `null` if no service is running.
|
||||
*
|
||||
* @return Current PlayerType
|
||||
*/
|
||||
val type: PlayerType?
|
||||
/**
|
||||
* Returns the current [PlayerType] of the [PlayerService] service,
|
||||
* otherwise `null` if no service is running.
|
||||
*
|
||||
* @return Current PlayerType
|
||||
*/
|
||||
get() = this.player?.playerType
|
||||
|
||||
val isPlaying: Boolean
|
||||
@ -58,12 +58,12 @@ object PlayerHolder {
|
||||
val isPlayerOpen: Boolean
|
||||
get() = this.player != null
|
||||
|
||||
/**
|
||||
* Use this method to only allow the user to manipulate the play queue (e.g. by enqueueing via
|
||||
* the stream long press menu) when there actually is a play queue to manipulate.
|
||||
* @return true only if the player is open and its play queue is ready (i.e. it is not null)
|
||||
*/
|
||||
val isPlayQueueReady: Boolean
|
||||
/**
|
||||
* Use this method to only allow the user to manipulate the play queue (e.g. by enqueueing via
|
||||
* the stream long press menu) when there actually is a play queue to manipulate.
|
||||
* @return true only if the player is open and its play queue is ready (i.e. it is not null)
|
||||
*/
|
||||
get() = this.playQueue != null
|
||||
|
||||
val queueSize: Int
|
||||
|
||||
@ -47,7 +47,7 @@ import org.schabi.newpipe.util.image.ImageStrategy
|
||||
*/
|
||||
class MediaBrowserImpl(
|
||||
private val context: Context,
|
||||
notifyChildrenChanged: (parentId: String) -> Unit,
|
||||
notifyChildrenChanged: (parentId: String) -> Unit
|
||||
) {
|
||||
private val packageValidator = PackageValidator(context)
|
||||
private val database = NewPipeDatabase.getInstance(context)
|
||||
@ -87,7 +87,8 @@ class MediaBrowserImpl(
|
||||
|
||||
val extras = Bundle()
|
||||
extras.putBoolean(
|
||||
MediaConstants.BROWSER_SERVICE_EXTRAS_KEY_SEARCH_SUPPORTED, true
|
||||
MediaConstants.BROWSER_SERVICE_EXTRAS_KEY_SEARCH_SUPPORTED,
|
||||
true
|
||||
)
|
||||
return MediaBrowserServiceCompat.BrowserRoot(ID_ROOT, extras)
|
||||
}
|
||||
@ -135,7 +136,7 @@ class MediaBrowserImpl(
|
||||
)
|
||||
}
|
||||
|
||||
when (/*val uriType = */path.removeAt(0)) {
|
||||
when (path.removeAt(0)) {
|
||||
ID_BOOKMARKS -> {
|
||||
if (path.isEmpty()) {
|
||||
return populateBookmarks()
|
||||
@ -206,7 +207,7 @@ class MediaBrowserImpl(
|
||||
|
||||
return MediaBrowserCompat.MediaItem(
|
||||
builder.build(),
|
||||
MediaBrowserCompat.MediaItem.FLAG_BROWSABLE,
|
||||
MediaBrowserCompat.MediaItem.FLAG_BROWSABLE
|
||||
)
|
||||
}
|
||||
|
||||
@ -260,7 +261,7 @@ class MediaBrowserImpl(
|
||||
private fun createLocalPlaylistStreamMediaItem(
|
||||
playlistId: Long,
|
||||
item: PlaylistStreamEntry,
|
||||
index: Int,
|
||||
index: Int
|
||||
): MediaBrowserCompat.MediaItem {
|
||||
val builder = MediaDescriptionCompat.Builder()
|
||||
.setMediaId(createMediaIdForPlaylistIndex(false, playlistId, index))
|
||||
@ -277,7 +278,7 @@ class MediaBrowserImpl(
|
||||
private fun createRemotePlaylistStreamMediaItem(
|
||||
playlistId: Long,
|
||||
item: StreamInfoItem,
|
||||
index: Int,
|
||||
index: Int
|
||||
): MediaBrowserCompat.MediaItem {
|
||||
val builder = MediaDescriptionCompat.Builder()
|
||||
builder.setMediaId(createMediaIdForPlaylistIndex(true, playlistId, index))
|
||||
@ -294,7 +295,7 @@ class MediaBrowserImpl(
|
||||
private fun createMediaIdForPlaylistIndex(
|
||||
isRemote: Boolean,
|
||||
playlistId: Long,
|
||||
index: Int,
|
||||
index: Int
|
||||
): String {
|
||||
return buildLocalPlaylistItemMediaId(isRemote, playlistId)
|
||||
.appendPath(index.toString())
|
||||
|
||||
@ -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,10 @@ 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 java.io.Serializable
|
||||
import java.util.Collections
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
import org.schabi.newpipe.player.playqueue.PlayQueueEvent.AppendEvent
|
||||
import org.schabi.newpipe.player.playqueue.PlayQueueEvent.ErrorEvent
|
||||
import org.schabi.newpipe.player.playqueue.PlayQueueEvent.InitEvent
|
||||
@ -12,9 +15,6 @@ import org.schabi.newpipe.player.playqueue.PlayQueueEvent.RecoveryEvent
|
||||
import org.schabi.newpipe.player.playqueue.PlayQueueEvent.RemoveEvent
|
||||
import org.schabi.newpipe.player.playqueue.PlayQueueEvent.ReorderEvent
|
||||
import org.schabi.newpipe.player.playqueue.PlayQueueEvent.SelectEvent
|
||||
import java.io.Serializable
|
||||
import java.util.Collections
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
|
||||
/**
|
||||
* PlayQueue is responsible for keeping track of a list of streams and the index of
|
||||
@ -28,7 +28,7 @@ import java.util.concurrent.atomic.AtomicInteger
|
||||
*/
|
||||
abstract class PlayQueue internal constructor(
|
||||
index: Int,
|
||||
startWith: List<PlayQueueItem>,
|
||||
startWith: List<PlayQueueItem>
|
||||
) : Serializable {
|
||||
private val queueIndex = AtomicInteger(index)
|
||||
private val history = mutableListOf<PlayQueueItem>()
|
||||
@ -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!!
|
||||
@ -105,22 +105,20 @@ abstract class PlayQueue internal constructor(
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Readonly ops
|
||||
////////////////////////////////////////////////////////////////////////// */
|
||||
|
||||
/**
|
||||
* Changes the current playing index to a new index.
|
||||
*
|
||||
* This method is guarded using in a circular manner for index exceeding the play queue size.
|
||||
*
|
||||
* Will emit a [SelectEvent] if the index is not the current playing index.
|
||||
*
|
||||
* @param index the index to be set
|
||||
* @return the current index that should be played
|
||||
*/
|
||||
@set:Synchronized
|
||||
var index: Int = 0
|
||||
/**
|
||||
* @return the current index that should be played
|
||||
*/
|
||||
get() = queueIndex.get()
|
||||
|
||||
/**
|
||||
* Changes the current playing index to a new index.
|
||||
*
|
||||
* This method is guarded using in a circular manner for index exceeding the play queue size.
|
||||
*
|
||||
* Will emit a [SelectEvent] if the index is not the current playing index.
|
||||
*
|
||||
* @param index the index to be set
|
||||
*/
|
||||
set(index) {
|
||||
val oldIndex = field
|
||||
|
||||
@ -340,7 +338,7 @@ abstract class PlayQueue internal constructor(
|
||||
@Synchronized
|
||||
fun move(
|
||||
source: Int,
|
||||
target: Int,
|
||||
target: Int
|
||||
) {
|
||||
if (source < 0 || target < 0) {
|
||||
return
|
||||
@ -375,7 +373,7 @@ abstract class PlayQueue internal constructor(
|
||||
@Synchronized
|
||||
fun setRecovery(
|
||||
index: Int,
|
||||
position: Long,
|
||||
position: Long
|
||||
) {
|
||||
streams.getOrNull(index)?.let {
|
||||
it.recoveryPosition = position
|
||||
|
||||
@ -2,13 +2,13 @@ package org.schabi.newpipe.player.playqueue
|
||||
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import java.io.Serializable
|
||||
import java.util.Objects
|
||||
import org.schabi.newpipe.extractor.Image
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfo
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem
|
||||
import org.schabi.newpipe.extractor.stream.StreamType
|
||||
import org.schabi.newpipe.util.ExtractorHelper
|
||||
import java.io.Serializable
|
||||
import java.util.Objects
|
||||
|
||||
class PlayQueueItem private constructor(
|
||||
val title: String,
|
||||
@ -18,7 +18,7 @@ class PlayQueueItem private constructor(
|
||||
val thumbnails: List<Image>,
|
||||
val uploader: String,
|
||||
val uploaderUrl: String?,
|
||||
val streamType: StreamType,
|
||||
val streamType: StreamType
|
||||
) : Serializable {
|
||||
//
|
||||
// ////////////////////////////////////////////////////////////////////// */
|
||||
@ -40,7 +40,7 @@ class PlayQueueItem private constructor(
|
||||
info.thumbnails,
|
||||
info.uploaderName.orEmpty(),
|
||||
info.uploaderUrl,
|
||||
info.streamType,
|
||||
info.streamType
|
||||
) {
|
||||
if (info.startPosition > 0) {
|
||||
this.recoveryPosition = info.startPosition * 1000
|
||||
@ -55,7 +55,7 @@ class PlayQueueItem private constructor(
|
||||
item.thumbnails,
|
||||
item.uploaderName.orEmpty(),
|
||||
item.uploaderUrl,
|
||||
item.streamType,
|
||||
item.streamType
|
||||
)
|
||||
|
||||
val stream: Single<StreamInfo>
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
package org.schabi.newpipe.player.ui
|
||||
|
||||
import org.schabi.newpipe.util.GuardedByMutex
|
||||
import kotlin.reflect.KClass
|
||||
import kotlin.reflect.safeCast
|
||||
import org.schabi.newpipe.util.GuardedByMutex
|
||||
|
||||
/**
|
||||
* Creates a [PlayerUiList] starting with the provided player uis. The provided player uis
|
||||
@ -78,22 +78,20 @@ class PlayerUiList(vararg initialPlayerUis: PlayerUi) {
|
||||
* @param T the class type parameter
|
||||
* @return the first player UI of the required type found in the list, or null
|
||||
</T> */
|
||||
fun <T : PlayerUi> get(playerUiType: KClass<T>): T? =
|
||||
playerUis.runWithLockSync {
|
||||
for (ui in lockData) {
|
||||
if (playerUiType.isInstance(ui)) {
|
||||
// try all UIs before returning null
|
||||
playerUiType.safeCast(ui)?.let { return@runWithLockSync it }
|
||||
}
|
||||
fun <T : PlayerUi> get(playerUiType: KClass<T>): T? = playerUis.runWithLockSync {
|
||||
for (ui in lockData) {
|
||||
if (playerUiType.isInstance(ui)) {
|
||||
// try all UIs before returning null
|
||||
playerUiType.safeCast(ui)?.let { return@runWithLockSync it }
|
||||
}
|
||||
return@runWithLockSync null
|
||||
}
|
||||
return@runWithLockSync null
|
||||
}
|
||||
|
||||
/**
|
||||
* See [get] above
|
||||
*/
|
||||
fun <T : PlayerUi> get(playerUiType: Class<T>): T? =
|
||||
get(playerUiType.kotlin)
|
||||
fun <T : PlayerUi> get(playerUiType: Class<T>): T? = get(playerUiType.kotlin)
|
||||
|
||||
/**
|
||||
* Calls the provided consumer on all player UIs in the list, in order of addition.
|
||||
|
||||
@ -13,7 +13,6 @@ import org.schabi.newpipe.ui.theme.SizeTokens
|
||||
|
||||
@Composable
|
||||
fun DebugScreen(viewModel: SettingsViewModel, modifier: Modifier = Modifier) {
|
||||
|
||||
val settingsLayoutRedesign by viewModel.settingsLayoutRedesign.collectAsState()
|
||||
|
||||
Column(modifier = modifier) {
|
||||
|
||||
@ -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")
|
||||
|
||||
@ -5,15 +5,15 @@ import com.grack.nanojson.JsonArray
|
||||
import com.grack.nanojson.JsonParser
|
||||
import com.grack.nanojson.JsonParserException
|
||||
import com.grack.nanojson.JsonWriter
|
||||
import org.schabi.newpipe.streams.io.SharpOutputStream
|
||||
import org.schabi.newpipe.streams.io.StoredFileHelper
|
||||
import org.schabi.newpipe.util.ZipHelper
|
||||
import java.io.FileNotFoundException
|
||||
import java.io.IOException
|
||||
import java.io.ObjectOutputStream
|
||||
import java.util.zip.ZipOutputStream
|
||||
import kotlin.io.path.createParentDirectories
|
||||
import kotlin.io.path.deleteIfExists
|
||||
import org.schabi.newpipe.streams.io.SharpOutputStream
|
||||
import org.schabi.newpipe.streams.io.StoredFileHelper
|
||||
import org.schabi.newpipe.util.ZipHelper
|
||||
|
||||
class ImportExportManager(private val fileLocator: BackupFileLocator) {
|
||||
companion object {
|
||||
@ -117,10 +117,15 @@ class ImportExportManager(private val fileLocator: BackupFileLocator) {
|
||||
for ((key, value) in entries) {
|
||||
when (value) {
|
||||
is Boolean -> editor.putBoolean(key, value)
|
||||
|
||||
is Float -> editor.putFloat(key, value)
|
||||
|
||||
is Int -> editor.putInt(key, value)
|
||||
|
||||
is Long -> editor.putLong(key, value)
|
||||
|
||||
is String -> editor.putString(key, value)
|
||||
|
||||
is Set<*> -> {
|
||||
// There are currently only Sets with type String possible
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
@ -154,10 +159,15 @@ class ImportExportManager(private val fileLocator: BackupFileLocator) {
|
||||
for ((key, value) in jsonObject) {
|
||||
when (value) {
|
||||
is Boolean -> editor.putBoolean(key, value)
|
||||
|
||||
is Float -> editor.putFloat(key, value)
|
||||
|
||||
is Int -> editor.putInt(key, value)
|
||||
|
||||
is Long -> editor.putLong(key, value)
|
||||
|
||||
is String -> editor.putString(key, value)
|
||||
|
||||
is JsonArray -> {
|
||||
editor.putStringSet(key, value.mapNotNull { e -> e as? String }.toSet())
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -6,11 +6,11 @@ import android.content.SharedPreferences
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import org.schabi.newpipe.R
|
||||
import org.schabi.newpipe.util.Localization
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class SettingsViewModel @Inject constructor(
|
||||
@ -18,7 +18,7 @@ class SettingsViewModel @Inject constructor(
|
||||
private val preferenceManager: SharedPreferences
|
||||
) : AndroidViewModel(context.applicationContext as Application) {
|
||||
|
||||
private var _settingsLayoutRedesignPref: Boolean
|
||||
private var settingsLayoutRedesignPref: Boolean
|
||||
get() = preferenceManager.getBoolean(
|
||||
Localization.compatGetString(getApplication(), R.string.settings_layout_redesign_key),
|
||||
false
|
||||
@ -30,11 +30,11 @@ class SettingsViewModel @Inject constructor(
|
||||
).apply()
|
||||
}
|
||||
private val _settingsLayoutRedesign: MutableStateFlow<Boolean> =
|
||||
MutableStateFlow(_settingsLayoutRedesignPref)
|
||||
MutableStateFlow(settingsLayoutRedesignPref)
|
||||
val settingsLayoutRedesign = _settingsLayoutRedesign.asStateFlow()
|
||||
|
||||
fun toggleSettingsLayoutRedesign(newState: Boolean) {
|
||||
_settingsLayoutRedesign.value = newState
|
||||
_settingsLayoutRedesignPref = newState
|
||||
settingsLayoutRedesignPref = newState
|
||||
}
|
||||
}
|
||||
|
||||
@ -36,14 +36,14 @@ fun SwitchPreference(
|
||||
text = stringResource(id = title),
|
||||
modifier = Modifier.padding(SizeTokens.SpacingExtraSmall),
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
textAlign = TextAlign.Start,
|
||||
textAlign = TextAlign.Start
|
||||
)
|
||||
summary?.let {
|
||||
Text(
|
||||
text = stringResource(id = summary),
|
||||
modifier = Modifier.padding(SizeTokens.SpacingExtraSmall),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
textAlign = TextAlign.Start,
|
||||
textAlign = TextAlign.Start
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -28,7 +28,7 @@ fun TextPreference(
|
||||
@StringRes title: Int,
|
||||
@DrawableRes icon: Int? = null,
|
||||
@StringRes summary: Int? = null,
|
||||
onClick: () -> Unit,
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
@ -51,14 +51,14 @@ fun TextPreference(
|
||||
text = stringResource(id = title),
|
||||
modifier = Modifier.padding(SizeTokens.SpacingExtraSmall),
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
textAlign = TextAlign.Start,
|
||||
textAlign = TextAlign.Start
|
||||
)
|
||||
summary?.let {
|
||||
Text(
|
||||
text = stringResource(id = summary),
|
||||
modifier = Modifier.padding(SizeTokens.SpacingExtraSmall),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
textAlign = TextAlign.Start,
|
||||
textAlign = TextAlign.Start
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -37,7 +37,8 @@ fun TextAction(text: String, modifier: Modifier = Modifier) {
|
||||
@Composable
|
||||
fun NavigationIcon() {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back",
|
||||
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
||||
contentDescription = "Back",
|
||||
modifier = Modifier.padding(horizontal = SizeTokens.SpacingExtraSmall)
|
||||
)
|
||||
}
|
||||
|
||||
@ -37,20 +37,28 @@ import org.schabi.newpipe.util.image.NewPipeSquircleIcon
|
||||
private val ABOUT_ITEMS = listOf(
|
||||
AboutData(R.string.faq_title, R.string.faq_description, R.string.faq, R.string.faq_url),
|
||||
AboutData(
|
||||
R.string.contribution_title, R.string.contribution_encouragement,
|
||||
R.string.view_on_github, R.string.github_url
|
||||
R.string.contribution_title,
|
||||
R.string.contribution_encouragement,
|
||||
R.string.view_on_github,
|
||||
R.string.github_url
|
||||
),
|
||||
AboutData(
|
||||
R.string.donation_title, R.string.donation_encouragement, R.string.give_back,
|
||||
R.string.donation_title,
|
||||
R.string.donation_encouragement,
|
||||
R.string.give_back,
|
||||
R.string.donation_url
|
||||
),
|
||||
AboutData(
|
||||
R.string.website_title, R.string.website_encouragement, R.string.open_in_browser,
|
||||
R.string.website_title,
|
||||
R.string.website_encouragement,
|
||||
R.string.open_in_browser,
|
||||
R.string.website_url
|
||||
),
|
||||
AboutData(
|
||||
R.string.privacy_policy_title, R.string.privacy_policy_encouragement,
|
||||
R.string.read_privacy_policy, R.string.privacy_policy_url
|
||||
R.string.privacy_policy_title,
|
||||
R.string.privacy_policy_encouragement,
|
||||
R.string.read_privacy_policy,
|
||||
R.string.privacy_policy_url
|
||||
)
|
||||
)
|
||||
|
||||
@ -86,23 +94,23 @@ fun AboutTab() {
|
||||
Image(
|
||||
imageVector = NewPipeSquircleIcon,
|
||||
contentDescription = stringResource(R.string.app_name),
|
||||
modifier = Modifier.size(64.dp),
|
||||
modifier = Modifier.size(64.dp)
|
||||
)
|
||||
Spacer(Modifier.height(4.dp))
|
||||
Text(
|
||||
text = stringResource(R.string.app_name),
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
textAlign = TextAlign.Center,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
Text(
|
||||
text = BuildConfig.VERSION_NAME,
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
textAlign = TextAlign.Center,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
Text(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
text = stringResource(R.string.app_description),
|
||||
textAlign = TextAlign.Center,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
}
|
||||
|
||||
@ -120,7 +128,7 @@ fun AboutTab() {
|
||||
@NonRestartableComposable
|
||||
private fun AboutItem(
|
||||
@PreviewParameter(AboutDataProvider::class) aboutData: AboutData,
|
||||
modifier: Modifier = Modifier,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Column(modifier = modifier) {
|
||||
Text(
|
||||
|
||||
@ -39,7 +39,7 @@ import org.schabi.newpipe.util.external_communication.ShareUtils
|
||||
fun Library(
|
||||
@PreviewParameter(LibraryProvider::class) library: Library,
|
||||
showLicenseDialog: (licenseFilename: String) -> Unit,
|
||||
descriptionMaxLines: Int,
|
||||
descriptionMaxLines: Int
|
||||
) {
|
||||
val spdxLicense = library.licenses.firstOrNull()?.spdxId?.takeIf { it.isNotBlank() }
|
||||
val licenseAssetPath = spdxLicense?.let { SPDX_ID_TO_ASSET_PATH[it] }
|
||||
@ -63,14 +63,14 @@ fun Library(
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Text(
|
||||
text = library.name,
|
||||
modifier = Modifier.weight(0.75f),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
val version = library.artifactVersion
|
||||
if (!version.isNullOrBlank()) {
|
||||
@ -85,7 +85,7 @@ fun Library(
|
||||
}.padding(start = 8.dp),
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -95,7 +95,7 @@ fun Library(
|
||||
text = author,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
val description = library.description
|
||||
@ -105,14 +105,14 @@ fun Library(
|
||||
text = description,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
maxLines = descriptionMaxLines,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
if (library.licenses.isNotEmpty()) {
|
||||
FlowRow(
|
||||
modifier = Modifier.padding(top = 6.dp, bottom = 4.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
library.licenses.forEach {
|
||||
Badge {
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
/**
|
||||
/*
|
||||
* The library definitions for most libraries are autogenerated by the AboutLibraries plugin.
|
||||
* This file is only for TeamNewPipe-related libraries.
|
||||
*/
|
||||
@ -22,12 +22,12 @@ val SPDX_ID_TO_ASSET_PATH = mapOf(
|
||||
"GPL-3.0-only" to "gpl_3.html",
|
||||
"GPL-3.0-or-later" to "gpl_3.html",
|
||||
"MIT" to "mit.html",
|
||||
"MPL-2.0" to "mpl2.html",
|
||||
"MPL-2.0" to "mpl2.html"
|
||||
)
|
||||
|
||||
fun getFirstPartyLibraries(
|
||||
context: Context,
|
||||
teamNewPipeLibraries: List<Library>,
|
||||
teamNewPipeLibraries: List<Library>
|
||||
): List<Library> {
|
||||
val gpl3 = setOf(
|
||||
License(
|
||||
@ -36,7 +36,7 @@ fun getFirstPartyLibraries(
|
||||
year = null,
|
||||
spdxId = "GPL-3.0-or-later",
|
||||
licenseContent = null,
|
||||
hash = "GPL-3.0-or-later",
|
||||
hash = "GPL-3.0-or-later"
|
||||
)
|
||||
).toImmutableSet()
|
||||
|
||||
@ -58,7 +58,7 @@ fun getFirstPartyLibraries(
|
||||
).toImmutableList(),
|
||||
organization = null,
|
||||
scm = Scm(null, null, context.getString(R.string.github_url)),
|
||||
licenses = gpl3,
|
||||
licenses = gpl3
|
||||
),
|
||||
Library(
|
||||
uniqueId = npeId,
|
||||
@ -74,15 +74,15 @@ fun getFirstPartyLibraries(
|
||||
).toImmutableList(),
|
||||
organization = null,
|
||||
scm = Scm(null, null, context.getString(R.string.newpipe_extractor_github_url)),
|
||||
licenses = gpl3,
|
||||
),
|
||||
licenses = gpl3
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
fun getAdditionalThirdPartyLibraries(
|
||||
context: Context,
|
||||
teamNewPipeLibraries: List<Library>,
|
||||
licenses: ImmutableSet<License>,
|
||||
licenses: ImmutableSet<License>
|
||||
): List<Library> {
|
||||
val apache2 = licenses.firstOrNull { it.spdxId == "Apache-2.0" }
|
||||
val mit = licenses.firstOrNull { it.spdxId == "MIT" }
|
||||
@ -103,7 +103,7 @@ fun getAdditionalThirdPartyLibraries(
|
||||
developers = listOf(
|
||||
Developer(
|
||||
name = "Jonas Kalderstam",
|
||||
organisationUrl = "https://github.com/spacecowboy/NoNonsense-FilePicker",
|
||||
organisationUrl = "https://github.com/spacecowboy/NoNonsense-FilePicker"
|
||||
),
|
||||
Developer(
|
||||
name = context.getString(R.string.team_newpipe),
|
||||
@ -112,7 +112,7 @@ fun getAdditionalThirdPartyLibraries(
|
||||
).toImmutableList(),
|
||||
organization = null,
|
||||
scm = Scm(null, null, "https://github.com/TeamNewPipe/NoNonsense-FilePicker"),
|
||||
licenses = listOfNotNull(mpl2).toImmutableSet(),
|
||||
licenses = listOfNotNull(mpl2).toImmutableSet()
|
||||
),
|
||||
Library(
|
||||
uniqueId = nanojsonId,
|
||||
@ -123,16 +123,16 @@ fun getAdditionalThirdPartyLibraries(
|
||||
developers = listOf(
|
||||
Developer(
|
||||
name = "mmastrac",
|
||||
organisationUrl = "https://github.com/mmastrac/nanojson",
|
||||
organisationUrl = "https://github.com/mmastrac/nanojson"
|
||||
),
|
||||
Developer(
|
||||
name = context.getString(R.string.team_newpipe),
|
||||
organisationUrl = context.getString(R.string.website_url)
|
||||
),
|
||||
)
|
||||
).toImmutableList(),
|
||||
organization = null,
|
||||
scm = Scm(null, null, "https://github.com/TeamNewPipe/nanojson"),
|
||||
licenses = listOfNotNull(mit, apache2).toImmutableSet()
|
||||
),
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@ -40,7 +40,7 @@ fun LicenseDialog(licenseHtml: AnnotatedString, onDismissRequest: () -> Unit) {
|
||||
} else {
|
||||
Text(
|
||||
text = licenseHtml,
|
||||
modifier = Modifier.padding(horizontal = 12.dp),
|
||||
modifier = Modifier.padding(horizontal = 12.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -32,7 +32,7 @@ fun LicenseTab(viewModel: LicenseTabViewModel = viewModel()) {
|
||||
|
||||
LazyColumnThemedScrollbar(state = lazyListState) {
|
||||
LazyColumn(
|
||||
state = lazyListState,
|
||||
state = lazyListState
|
||||
) {
|
||||
item {
|
||||
Text(
|
||||
@ -43,7 +43,7 @@ fun LicenseTab(viewModel: LicenseTabViewModel = viewModel()) {
|
||||
top = 16.dp,
|
||||
end = 16.dp,
|
||||
bottom = 8.dp
|
||||
),
|
||||
)
|
||||
)
|
||||
}
|
||||
item {
|
||||
@ -54,7 +54,7 @@ fun LicenseTab(viewModel: LicenseTabViewModel = viewModel()) {
|
||||
start = 16.dp,
|
||||
end = 16.dp,
|
||||
bottom = 8.dp
|
||||
),
|
||||
)
|
||||
)
|
||||
}
|
||||
if (state.firstPartyLibraries == null) {
|
||||
@ -67,7 +67,7 @@ fun LicenseTab(viewModel: LicenseTabViewModel = viewModel()) {
|
||||
Library(
|
||||
library = library,
|
||||
showLicenseDialog = viewModel::showLicenseDialog,
|
||||
descriptionMaxLines = Int.MAX_VALUE,
|
||||
descriptionMaxLines = Int.MAX_VALUE
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -82,7 +82,7 @@ fun LicenseTab(viewModel: LicenseTabViewModel = viewModel()) {
|
||||
top = 16.dp,
|
||||
end = 16.dp,
|
||||
bottom = 8.dp
|
||||
),
|
||||
)
|
||||
)
|
||||
}
|
||||
if (state.thirdPartyLibraries == null) {
|
||||
@ -95,7 +95,7 @@ fun LicenseTab(viewModel: LicenseTabViewModel = viewModel()) {
|
||||
Library(
|
||||
library = library,
|
||||
showLicenseDialog = viewModel::showLicenseDialog,
|
||||
descriptionMaxLines = 2,
|
||||
descriptionMaxLines = 2
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -45,7 +45,7 @@ class LicenseTabViewModel : ViewModel() {
|
||||
_state.update {
|
||||
it.copy(
|
||||
firstPartyLibraries = firstParty,
|
||||
thirdPartyLibraries = allThirdParty,
|
||||
thirdPartyLibraries = allThirdParty
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -77,6 +77,6 @@ class LicenseTabViewModel : ViewModel() {
|
||||
val firstPartyLibraries: List<Library>?,
|
||||
val thirdPartyLibraries: List<Library>?,
|
||||
// null if dialog closed, empty if loading, otherwise license HTML content
|
||||
val licenseDialogHtml: AnnotatedString?,
|
||||
val licenseDialogHtml: AnnotatedString?
|
||||
)
|
||||
}
|
||||
|
||||
@ -29,7 +29,7 @@ import org.schabi.newpipe.util.external_communication.ShareUtils
|
||||
fun ErrorPanel(
|
||||
errorInfo: ErrorInfo,
|
||||
modifier: Modifier = Modifier,
|
||||
onRetry: (() -> Unit)? = null,
|
||||
onRetry: (() -> Unit)? = null
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val isPreview = LocalInspectionMode.current
|
||||
@ -42,7 +42,7 @@ fun ErrorPanel(
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
modifier = modifier,
|
||||
modifier = modifier
|
||||
) {
|
||||
Text(
|
||||
text = messageText,
|
||||
@ -97,9 +97,9 @@ private fun ErrorPanelPreview() {
|
||||
throwable = ReCaptchaException("An error", "https://example.com"),
|
||||
userAction = UserAction.REQUESTED_STREAM,
|
||||
request = "Preview request",
|
||||
openInBrowserUrl = "https://example.com",
|
||||
openInBrowserUrl = "https://example.com"
|
||||
),
|
||||
onRetry = {},
|
||||
onRetry = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -34,7 +34,7 @@ fun ScaffoldWithToolbar(
|
||||
scrolledContainerColor = MaterialTheme.colorScheme.primaryContainer,
|
||||
navigationIconContentColor = MaterialTheme.colorScheme.onPrimaryContainer,
|
||||
titleContentColor = MaterialTheme.colorScheme.onPrimaryContainer,
|
||||
actionIconContentColor = MaterialTheme.colorScheme.onPrimaryContainer,
|
||||
actionIconContentColor = MaterialTheme.colorScheme.onPrimaryContainer
|
||||
),
|
||||
navigationIcon = {
|
||||
IconButton(onClick = onBackClick) {
|
||||
|
||||
@ -10,7 +10,7 @@ import my.nanihadesuka.compose.ScrollbarSettings
|
||||
@Composable
|
||||
fun defaultThemedScrollbarSettings(): ScrollbarSettings = ScrollbarSettings.Default.copy(
|
||||
thumbUnselectedColor = MaterialTheme.colorScheme.primary,
|
||||
thumbSelectedColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.75f),
|
||||
thumbSelectedColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.75f)
|
||||
)
|
||||
|
||||
@Composable
|
||||
@ -26,6 +26,6 @@ fun LazyColumnThemedScrollbar(
|
||||
modifier = modifier,
|
||||
settings = settings,
|
||||
indicatorContent = indicatorContent,
|
||||
content = content,
|
||||
content = content
|
||||
)
|
||||
}
|
||||
|
||||
@ -21,7 +21,7 @@ import org.schabi.newpipe.ui.theme.SizeTokens.SpacingSmall
|
||||
fun ServiceColoredButton(
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
content: @Composable() RowScope.() -> Unit,
|
||||
content: @Composable RowScope.() -> Unit
|
||||
) {
|
||||
Button(
|
||||
onClick = onClick,
|
||||
@ -33,9 +33,9 @@ fun ServiceColoredButton(
|
||||
contentPadding = PaddingValues(horizontal = SpacingMedium, vertical = SpacingSmall),
|
||||
shape = RectangleShape,
|
||||
elevation = ButtonDefaults.buttonElevation(
|
||||
defaultElevation = 8.dp,
|
||||
defaultElevation = 8.dp
|
||||
|
||||
),
|
||||
)
|
||||
) {
|
||||
content()
|
||||
}
|
||||
|
||||
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