Merge pull request #13121 from theimpulson/forwardport

Merge dev into refactor
This commit is contained in:
Tobi 2026-01-28 08:43:36 -08:00 committed by GitHub
commit 50b9a7b7f6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
230 changed files with 1957 additions and 1786 deletions

View File

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

View File

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

View File

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

View File

@ -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.** { *; }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -14,6 +14,6 @@ interface LocalItem {
PLAYLIST_REMOTE_ITEM,
PLAYLIST_STREAM_ITEM,
STATISTIC_STREAM_ITEM,
STATISTIC_STREAM_ITEM
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -29,7 +29,7 @@ data class SearchHistoryEntry @JvmOverloads constructor(
@ColumnInfo(name = ID)
@PrimaryKey(autoGenerate = true)
val id: Long = 0,
val id: Long = 0
) {
@Ignore

View File

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

View File

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

View File

@ -68,6 +68,11 @@ interface PlaylistStreamDAO : BasicDAO<PlaylistStreamEntity> {
)
fun getOrderedStreamsOf(playlistId: Long): Flowable<MutableList<PlaylistStreamEntry>>
// If a playlist has no streams, there wont 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 wont 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
"""
)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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