Merge https://github.com/TeamNewPipe/NewPipe into sponsorblock
Signed-off-by: baalajimaestro <me@baalajimaestro.me>
This commit is contained in:
commit
155e66d07f
358 changed files with 11595 additions and 3955 deletions
17
.github/changed-lines-count-labeler.yml
vendored
Normal file
17
.github/changed-lines-count-labeler.yml
vendored
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
# Add 'size/small' label to any changes with less than 50 lines
|
||||||
|
size/small:
|
||||||
|
max: 49
|
||||||
|
|
||||||
|
# Add 'size/medium' label to any changes between 50 and 249 lines
|
||||||
|
size/medium:
|
||||||
|
min: 50
|
||||||
|
max: 249
|
||||||
|
|
||||||
|
# Add 'size/large' label to any changes between 250 and 749 lines
|
||||||
|
size/large:
|
||||||
|
min: 250
|
||||||
|
max: 749
|
||||||
|
|
||||||
|
# Add 'size/giant' label to any changes for more than 749 lines
|
||||||
|
size/giant:
|
||||||
|
min: 750
|
4
.github/workflows/image-minimizer.js
vendored
4
.github/workflows/image-minimizer.js
vendored
|
@ -86,7 +86,7 @@ module.exports = async ({github, context}) => {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Asnyc replace function from https://stackoverflow.com/a/48032528
|
// Async replace function from https://stackoverflow.com/a/48032528
|
||||||
async function replaceAsync(str, regex, asyncFn) {
|
async function replaceAsync(str, regex, asyncFn) {
|
||||||
const promises = [];
|
const promises = [];
|
||||||
str.replace(regex, (match, ...args) => {
|
str.replace(regex, (match, ...args) => {
|
||||||
|
@ -138,7 +138,7 @@ module.exports = async ({github, context}) => {
|
||||||
if (shouldModify) {
|
if (shouldModify) {
|
||||||
wasMatchModified = true;
|
wasMatchModified = true;
|
||||||
console.log(`Modifying match '${match}'`);
|
console.log(`Modifying match '${match}'`);
|
||||||
return `<img alt="${g1}" src="${g2}" width=${Math.min(600, (IMG_MAX_HEIGHT_PX * probeAspectRatio).toFixed(0))} />`;
|
return `<img alt="${g1}" src="${g2}" width=${Math.min(600, Math.floor(IMG_MAX_HEIGHT_PX * probeAspectRatio))} />`;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`Match '${match}' is ok/will not be modified`);
|
console.log(`Match '${match}' is ok/will not be modified`);
|
||||||
|
|
18
.github/workflows/pr-labeler.yml
vendored
Normal file
18
.github/workflows/pr-labeler.yml
vendored
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
name: "PR size labeler"
|
||||||
|
on: [pull_request]
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
pull-requests: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
changed-lines-count-labeler:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
name: Automatically labelling pull requests based on the changed lines count
|
||||||
|
permissions:
|
||||||
|
pull-requests: write
|
||||||
|
steps:
|
||||||
|
- name: Set a label
|
||||||
|
uses: TeamNewPipe/changed-lines-count-labeler@main
|
||||||
|
with:
|
||||||
|
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
configuration-path: .github/changed-lines-count-labeler.yml
|
|
@ -12,7 +12,7 @@ plugins {
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdk 33
|
compileSdk 34
|
||||||
namespace 'org.schabi.newpipe'
|
namespace 'org.schabi.newpipe'
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
|
@ -20,8 +20,8 @@ android {
|
||||||
resValue "string", "app_name", "NewPipe SponsorBlock"
|
resValue "string", "app_name", "NewPipe SponsorBlock"
|
||||||
minSdk 21
|
minSdk 21
|
||||||
targetSdk 33
|
targetSdk 33
|
||||||
versionCode 994
|
versionCode 995
|
||||||
versionName "0.25.2"
|
versionName "0.26.0"
|
||||||
|
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
|
|
||||||
|
@ -50,9 +50,6 @@ android {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Keep the release build type at the end of the list to override 'archivesBaseName' of
|
|
||||||
// debug build. This seems to be a Gradle bug, therefore
|
|
||||||
// TODO: update Gradle version
|
|
||||||
release {
|
release {
|
||||||
if (System.properties.containsKey('packageSuffix')) {
|
if (System.properties.containsKey('packageSuffix')) {
|
||||||
applicationIdSuffix System.getProperty('packageSuffix')
|
applicationIdSuffix System.getProperty('packageSuffix')
|
||||||
|
@ -101,7 +98,9 @@ android {
|
||||||
resources {
|
resources {
|
||||||
// remove two files which belong to jsoup
|
// remove two files which belong to jsoup
|
||||||
// no idea how they ended up in the META-INF dir...
|
// no idea how they ended up in the META-INF dir...
|
||||||
excludes += ['META-INF/README.md', 'META-INF/CHANGES']
|
excludes += ['META-INF/README.md', 'META-INF/CHANGES',
|
||||||
|
// 'COPYRIGHT' belongs to RxJava...
|
||||||
|
'META-INF/COPYRIGHT']
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -109,19 +108,18 @@ android {
|
||||||
ext {
|
ext {
|
||||||
checkstyleVersion = '10.12.1'
|
checkstyleVersion = '10.12.1'
|
||||||
|
|
||||||
androidxLifecycleVersion = '2.5.1'
|
androidxLifecycleVersion = '2.6.2'
|
||||||
androidxRoomVersion = '2.5.2'
|
androidxRoomVersion = '2.6.1'
|
||||||
androidxWorkVersion = '2.7.1'
|
androidxWorkVersion = '2.8.1'
|
||||||
|
|
||||||
icepickVersion = '3.2.0'
|
icepickVersion = '3.2.0'
|
||||||
exoPlayerVersion = '2.18.7'
|
exoPlayerVersion = '2.18.7'
|
||||||
googleAutoServiceVersion = '1.0.1'
|
googleAutoServiceVersion = '1.1.1'
|
||||||
groupieVersion = '2.10.1'
|
groupieVersion = '2.10.1'
|
||||||
markwonVersion = '4.6.2'
|
markwonVersion = '4.6.2'
|
||||||
|
|
||||||
leakCanaryVersion = '2.12'
|
leakCanaryVersion = '2.12'
|
||||||
stethoVersion = '1.6.0'
|
stethoVersion = '1.6.0'
|
||||||
mockitoVersion = '4.0.0'
|
|
||||||
}
|
}
|
||||||
|
|
||||||
configurations {
|
configurations {
|
||||||
|
@ -136,7 +134,7 @@ checkstyle {
|
||||||
toolVersion = checkstyleVersion
|
toolVersion = checkstyleVersion
|
||||||
}
|
}
|
||||||
|
|
||||||
task runCheckstyle(type: Checkstyle) {
|
tasks.register('runCheckstyle', Checkstyle) {
|
||||||
source 'src'
|
source 'src'
|
||||||
include '**/*.java'
|
include '**/*.java'
|
||||||
exclude '**/gen/**'
|
exclude '**/gen/**'
|
||||||
|
@ -157,7 +155,7 @@ task runCheckstyle(type: Checkstyle) {
|
||||||
def outputDir = "${project.buildDir}/reports/ktlint/"
|
def outputDir = "${project.buildDir}/reports/ktlint/"
|
||||||
def inputFiles = project.fileTree(dir: "src", include: "**/*.kt")
|
def inputFiles = project.fileTree(dir: "src", include: "**/*.kt")
|
||||||
|
|
||||||
task runKtlint(type: JavaExec) {
|
tasks.register('runKtlint', JavaExec) {
|
||||||
inputs.files(inputFiles)
|
inputs.files(inputFiles)
|
||||||
outputs.dir(outputDir)
|
outputs.dir(outputDir)
|
||||||
getMainClass().set("com.pinterest.ktlint.Main")
|
getMainClass().set("com.pinterest.ktlint.Main")
|
||||||
|
@ -166,7 +164,7 @@ task runKtlint(type: JavaExec) {
|
||||||
jvmArgs("--add-opens", "java.base/java.lang=ALL-UNNAMED")
|
jvmArgs("--add-opens", "java.base/java.lang=ALL-UNNAMED")
|
||||||
}
|
}
|
||||||
|
|
||||||
task formatKtlint(type: JavaExec) {
|
tasks.register('formatKtlint', JavaExec) {
|
||||||
inputs.files(inputFiles)
|
inputs.files(inputFiles)
|
||||||
outputs.dir(outputDir)
|
outputs.dir(outputDir)
|
||||||
getMainClass().set("com.pinterest.ktlint.Main")
|
getMainClass().set("com.pinterest.ktlint.Main")
|
||||||
|
@ -192,7 +190,7 @@ sonar {
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
/** Desugaring **/
|
/** Desugaring **/
|
||||||
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.3'
|
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs_nio:2.0.4'
|
||||||
|
|
||||||
/** NewPipe libraries **/
|
/** NewPipe libraries **/
|
||||||
// You can use a local version by uncommenting a few lines in settings.gradle
|
// You can use a local version by uncommenting a few lines in settings.gradle
|
||||||
|
@ -200,7 +198,7 @@ dependencies {
|
||||||
// name and the commit hash with the commit hash of the (pushed) commit you want to test
|
// name and the commit hash with the commit hash of the (pushed) commit you want to test
|
||||||
// This works thanks to JitPack: https://jitpack.io/
|
// This works thanks to JitPack: https://jitpack.io/
|
||||||
implementation 'com.github.TeamNewPipe:nanojson:1d9e1aea9049fc9f85e68b43ba39fe7be1c1f751'
|
implementation 'com.github.TeamNewPipe:nanojson:1d9e1aea9049fc9f85e68b43ba39fe7be1c1f751'
|
||||||
implementation 'com.github.TeamNewPipe:NewPipeExtractor:v0.22.7'
|
implementation 'com.github.TeamNewPipe:NewPipeExtractor:v0.23.1'
|
||||||
implementation 'com.github.TeamNewPipe:NoNonsense-FilePicker:5.0.0'
|
implementation 'com.github.TeamNewPipe:NoNonsense-FilePicker:5.0.0'
|
||||||
|
|
||||||
/** Checkstyle **/
|
/** Checkstyle **/
|
||||||
|
@ -208,31 +206,31 @@ dependencies {
|
||||||
ktlint 'com.pinterest:ktlint:0.45.2'
|
ktlint 'com.pinterest:ktlint:0.45.2'
|
||||||
|
|
||||||
/** Kotlin **/
|
/** Kotlin **/
|
||||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:${kotlin_version}"
|
implementation "org.jetbrains.kotlin:kotlin-stdlib:${kotlin_version}"
|
||||||
|
|
||||||
/** AndroidX **/
|
/** AndroidX **/
|
||||||
implementation 'androidx.appcompat:appcompat:1.5.1'
|
implementation 'androidx.appcompat:appcompat:1.6.1'
|
||||||
implementation 'androidx.cardview:cardview:1.0.0'
|
implementation 'androidx.cardview:cardview:1.0.0'
|
||||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
|
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
|
||||||
implementation 'androidx.core:core-ktx:1.10.0'
|
implementation 'androidx.core:core-ktx:1.12.0'
|
||||||
implementation 'androidx.documentfile:documentfile:1.0.1'
|
implementation 'androidx.documentfile:documentfile:1.0.1'
|
||||||
implementation 'androidx.fragment:fragment-ktx:1.4.1'
|
implementation 'androidx.fragment:fragment-ktx:1.6.2'
|
||||||
implementation "androidx.lifecycle:lifecycle-livedata-ktx:${androidxLifecycleVersion}"
|
implementation "androidx.lifecycle:lifecycle-livedata-ktx:${androidxLifecycleVersion}"
|
||||||
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:${androidxLifecycleVersion}"
|
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:${androidxLifecycleVersion}"
|
||||||
implementation 'androidx.localbroadcastmanager:localbroadcastmanager:1.1.0'
|
implementation 'androidx.localbroadcastmanager:localbroadcastmanager:1.1.0'
|
||||||
implementation 'androidx.media:media:1.6.0'
|
implementation 'androidx.media:media:1.7.0'
|
||||||
implementation 'androidx.preference:preference:1.2.0'
|
implementation 'androidx.preference:preference:1.2.1'
|
||||||
implementation 'androidx.recyclerview:recyclerview:1.2.1'
|
implementation 'androidx.recyclerview:recyclerview:1.3.2'
|
||||||
implementation "androidx.room:room-runtime:${androidxRoomVersion}"
|
implementation "androidx.room:room-runtime:${androidxRoomVersion}"
|
||||||
implementation "androidx.room:room-rxjava3:${androidxRoomVersion}"
|
implementation "androidx.room:room-rxjava3:${androidxRoomVersion}"
|
||||||
kapt "androidx.room:room-compiler:${androidxRoomVersion}"
|
kapt "androidx.room:room-compiler:${androidxRoomVersion}"
|
||||||
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
|
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
|
||||||
// Newer version specified to prevent accessibility regressions with RecyclerView, see:
|
// Newer version specified to prevent accessibility regressions with RecyclerView, see:
|
||||||
// https://developer.android.com/jetpack/androidx/releases/viewpager2#1.1.0-alpha01
|
// https://developer.android.com/jetpack/androidx/releases/viewpager2#1.1.0-alpha01
|
||||||
implementation 'androidx.viewpager2:viewpager2:1.1.0-beta01'
|
implementation 'androidx.viewpager2:viewpager2:1.1.0-beta02'
|
||||||
implementation "androidx.work:work-runtime-ktx:${androidxWorkVersion}"
|
implementation "androidx.work:work-runtime-ktx:${androidxWorkVersion}"
|
||||||
implementation "androidx.work:work-rxjava3:${androidxWorkVersion}"
|
implementation "androidx.work:work-rxjava3:${androidxWorkVersion}"
|
||||||
implementation 'com.google.android.material:material:1.6.1'
|
implementation 'com.google.android.material:material:1.11.0'
|
||||||
|
|
||||||
/** Third-party libraries **/
|
/** Third-party libraries **/
|
||||||
// Instance state boilerplate elimination
|
// Instance state boilerplate elimination
|
||||||
|
@ -240,13 +238,10 @@ dependencies {
|
||||||
kapt "frankiesardo:icepick-processor:${icepickVersion}"
|
kapt "frankiesardo:icepick-processor:${icepickVersion}"
|
||||||
|
|
||||||
// HTML parser
|
// HTML parser
|
||||||
implementation "org.jsoup:jsoup:1.16.1"
|
implementation "org.jsoup:jsoup:1.16.2"
|
||||||
|
|
||||||
// HTTP client
|
// HTTP client
|
||||||
implementation "com.squareup.okhttp3:okhttp:4.11.0"
|
implementation "com.squareup.okhttp3:okhttp:4.12.0"
|
||||||
// okhttp3:4.11.0 introduces a vulnerability from com.squareup.okio:okio@3.3.0,
|
|
||||||
// remove com.squareup.okio:okio when updating okhttp
|
|
||||||
implementation "com.squareup.okio:okio:3.4.0"
|
|
||||||
|
|
||||||
// Media player
|
// Media player
|
||||||
implementation "com.google.android.exoplayer:exoplayer-core:${exoPlayerVersion}"
|
implementation "com.google.android.exoplayer:exoplayer-core:${exoPlayerVersion}"
|
||||||
|
@ -275,19 +270,19 @@ dependencies {
|
||||||
implementation "io.noties.markwon:linkify:${markwonVersion}"
|
implementation "io.noties.markwon:linkify:${markwonVersion}"
|
||||||
|
|
||||||
// Crash reporting
|
// Crash reporting
|
||||||
implementation "ch.acra:acra-core:5.10.1"
|
implementation "ch.acra:acra-core:5.11.3"
|
||||||
|
|
||||||
// Properly restarting
|
// Properly restarting
|
||||||
implementation 'com.jakewharton:process-phoenix:2.1.2'
|
implementation 'com.jakewharton:process-phoenix:2.1.2'
|
||||||
|
|
||||||
// Reactive extensions for Java VM
|
// Reactive extensions for Java VM
|
||||||
implementation "io.reactivex.rxjava3:rxjava:3.1.6"
|
implementation "io.reactivex.rxjava3:rxjava:3.1.8"
|
||||||
implementation "io.reactivex.rxjava3:rxandroid:3.0.2"
|
implementation "io.reactivex.rxjava3:rxandroid:3.0.2"
|
||||||
// RxJava binding APIs for Android UI widgets
|
// RxJava binding APIs for Android UI widgets
|
||||||
implementation "com.jakewharton.rxbinding4:rxbinding:4.0.0"
|
implementation "com.jakewharton.rxbinding4:rxbinding:4.0.0"
|
||||||
|
|
||||||
// Date and time formatting
|
// Date and time formatting
|
||||||
implementation "org.ocpsoft.prettytime:prettytime:5.0.6.Final"
|
implementation "org.ocpsoft.prettytime:prettytime:5.0.7.Final"
|
||||||
|
|
||||||
/** Debugging **/
|
/** Debugging **/
|
||||||
// Memory leak detection
|
// Memory leak detection
|
||||||
|
@ -300,13 +295,12 @@ dependencies {
|
||||||
|
|
||||||
/** Testing **/
|
/** Testing **/
|
||||||
testImplementation 'junit:junit:4.13.2'
|
testImplementation 'junit:junit:4.13.2'
|
||||||
testImplementation "org.mockito:mockito-core:${mockitoVersion}"
|
testImplementation 'org.mockito:mockito-core:5.6.0'
|
||||||
testImplementation "org.mockito:mockito-inline:${mockitoVersion}"
|
|
||||||
|
|
||||||
androidTestImplementation "androidx.test.ext:junit:1.1.5"
|
androidTestImplementation "androidx.test.ext:junit:1.1.5"
|
||||||
androidTestImplementation "androidx.test:runner:1.5.2"
|
androidTestImplementation "androidx.test:runner:1.5.2"
|
||||||
androidTestImplementation "androidx.room:room-testing:${androidxRoomVersion}"
|
androidTestImplementation "androidx.room:room-testing:${androidxRoomVersion}"
|
||||||
androidTestImplementation "org.assertj:assertj-core:3.23.1"
|
androidTestImplementation "org.assertj:assertj-core:3.24.2"
|
||||||
}
|
}
|
||||||
|
|
||||||
static String getGitWorkingBranch() {
|
static String getGitWorkingBranch() {
|
||||||
|
|
|
@ -0,0 +1,124 @@
|
||||||
|
package org.schabi.newpipe.database
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.room.Room
|
||||||
|
import androidx.test.core.app.ApplicationProvider
|
||||||
|
import io.reactivex.rxjava3.core.Single
|
||||||
|
import org.junit.After
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Assert.assertNotNull
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Test
|
||||||
|
import org.schabi.newpipe.database.feed.dao.FeedDAO
|
||||||
|
import org.schabi.newpipe.database.feed.model.FeedEntity
|
||||||
|
import org.schabi.newpipe.database.feed.model.FeedGroupEntity
|
||||||
|
import org.schabi.newpipe.database.stream.StreamWithState
|
||||||
|
import org.schabi.newpipe.database.stream.dao.StreamDAO
|
||||||
|
import org.schabi.newpipe.database.stream.model.StreamEntity
|
||||||
|
import org.schabi.newpipe.database.subscription.SubscriptionDAO
|
||||||
|
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
|
||||||
|
private lateinit var feedDAO: FeedDAO
|
||||||
|
private lateinit var streamDAO: StreamDAO
|
||||||
|
private lateinit var subscriptionDAO: SubscriptionDAO
|
||||||
|
|
||||||
|
private val serviceId = ServiceList.YouTube.serviceId
|
||||||
|
|
||||||
|
private val stream1 = StreamEntity(1, serviceId, "https://youtube.com/watch?v=1", "stream 1", StreamType.VIDEO_STREAM, 1000, "channel-1", "https://youtube.com/channel/1", "https://i.ytimg.com/vi/1/hqdefault.jpg", 100, "2023-01-01", OffsetDateTime.parse("2023-01-01T00:00:00Z"))
|
||||||
|
private val stream2 = StreamEntity(2, serviceId, "https://youtube.com/watch?v=2", "stream 2", StreamType.VIDEO_STREAM, 1000, "channel-1", "https://youtube.com/channel/1", "https://i.ytimg.com/vi/1/hqdefault.jpg", 100, "2023-01-02", OffsetDateTime.parse("2023-01-02T00:00:00Z"))
|
||||||
|
private val stream3 = StreamEntity(3, serviceId, "https://youtube.com/watch?v=3", "stream 3", StreamType.LIVE_STREAM, 1000, "channel-1", "https://youtube.com/channel/1", "https://i.ytimg.com/vi/1/hqdefault.jpg", 100, "2023-01-03", OffsetDateTime.parse("2023-01-03T00:00:00Z"))
|
||||||
|
private val stream4 = StreamEntity(4, serviceId, "https://youtube.com/watch?v=4", "stream 4", StreamType.VIDEO_STREAM, 1000, "channel-2", "https://youtube.com/channel/2", "https://i.ytimg.com/vi/1/hqdefault.jpg", 100, "2023-08-10", OffsetDateTime.parse("2023-08-10T00:00:00Z"))
|
||||||
|
private val stream5 = StreamEntity(5, serviceId, "https://youtube.com/watch?v=5", "stream 5", StreamType.VIDEO_STREAM, 1000, "channel-2", "https://youtube.com/channel/2", "https://i.ytimg.com/vi/1/hqdefault.jpg", 100, "2023-08-20", OffsetDateTime.parse("2023-08-20T00:00:00Z"))
|
||||||
|
private val stream6 = StreamEntity(6, serviceId, "https://youtube.com/watch?v=6", "stream 6", StreamType.VIDEO_STREAM, 1000, "channel-3", "https://youtube.com/channel/3", "https://i.ytimg.com/vi/1/hqdefault.jpg", 100, "2023-09-01", OffsetDateTime.parse("2023-09-01T00:00:00Z"))
|
||||||
|
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
|
||||||
|
)
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun createDb() {
|
||||||
|
val context = ApplicationProvider.getApplicationContext<Context>()
|
||||||
|
db = Room.inMemoryDatabaseBuilder(
|
||||||
|
context, AppDatabase::class.java
|
||||||
|
).build()
|
||||||
|
feedDAO = db.feedDAO()
|
||||||
|
streamDAO = db.streamDAO()
|
||||||
|
subscriptionDAO = db.subscriptionDAO()
|
||||||
|
}
|
||||||
|
|
||||||
|
@After
|
||||||
|
@Throws(IOException::class)
|
||||||
|
fun closeDb() {
|
||||||
|
db.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testUnlinkStreamsOlderThan_KeepOne() {
|
||||||
|
setupUnlinkDelete("2023-08-15T00:00:00Z")
|
||||||
|
val streams = feedDAO.getStreams(
|
||||||
|
FeedGroupEntity.GROUP_ALL_ID, includePlayed = true, includePartiallyPlayed = true, null
|
||||||
|
)
|
||||||
|
.blockingGet()
|
||||||
|
val allowedStreams = listOf(stream3, stream5, stream6, stream7)
|
||||||
|
assertEqual(streams, allowedStreams)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testUnlinkStreamsOlderThan_KeepMultiple() {
|
||||||
|
setupUnlinkDelete("2023-08-01T00:00:00Z")
|
||||||
|
val streams = feedDAO.getStreams(
|
||||||
|
FeedGroupEntity.GROUP_ALL_ID, includePlayed = true, includePartiallyPlayed = true, null
|
||||||
|
)
|
||||||
|
.blockingGet()
|
||||||
|
val allowedStreams = listOf(stream3, stream4, stream5, stream6, stream7)
|
||||||
|
assertEqual(streams, allowedStreams)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun assertEqual(streams: List<StreamWithState>?, allowedStreams: List<StreamEntity>) {
|
||||||
|
assertNotNull(streams)
|
||||||
|
assertEquals(allowedStreams, streams!!.stream().map { it.stream }.toList().sortedBy { it.uid })
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setupUnlinkDelete(time: String) {
|
||||||
|
clearAndFillTables()
|
||||||
|
Single.fromCallable {
|
||||||
|
feedDAO.unlinkStreamsOlderThan(OffsetDateTime.parse(time))
|
||||||
|
}.blockingSubscribe()
|
||||||
|
Single.fromCallable {
|
||||||
|
streamDAO.deleteOrphans()
|
||||||
|
}.blockingSubscribe()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun clearAndFillTables() {
|
||||||
|
db.clearAllTables()
|
||||||
|
streamDAO.insertAll(allStreams)
|
||||||
|
subscriptionDAO.insertAll(
|
||||||
|
listOf(
|
||||||
|
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")),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
feedDAO.insertAll(
|
||||||
|
listOf(
|
||||||
|
FeedEntity(1, 1),
|
||||||
|
FeedEntity(2, 1),
|
||||||
|
FeedEntity(3, 1),
|
||||||
|
FeedEntity(4, 2),
|
||||||
|
FeedEntity(5, 2),
|
||||||
|
FeedEntity(6, 3),
|
||||||
|
FeedEntity(7, 4),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -10,19 +10,13 @@ import org.junit.Rule;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.schabi.newpipe.database.AppDatabase;
|
import org.schabi.newpipe.database.AppDatabase;
|
||||||
import org.schabi.newpipe.database.feed.model.FeedGroupEntity;
|
import org.schabi.newpipe.database.feed.model.FeedGroupEntity;
|
||||||
import org.schabi.newpipe.database.stream.model.StreamEntity;
|
|
||||||
import org.schabi.newpipe.database.subscription.SubscriptionEntity;
|
import org.schabi.newpipe.database.subscription.SubscriptionEntity;
|
||||||
import org.schabi.newpipe.extractor.channel.ChannelInfo;
|
import org.schabi.newpipe.extractor.channel.ChannelInfo;
|
||||||
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||||
import org.schabi.newpipe.extractor.localization.DateWrapper;
|
|
||||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
|
||||||
import org.schabi.newpipe.extractor.stream.StreamType;
|
|
||||||
import org.schabi.newpipe.testUtil.TestDatabase;
|
import org.schabi.newpipe.testUtil.TestDatabase;
|
||||||
import org.schabi.newpipe.testUtil.TrampolineSchedulerRule;
|
import org.schabi.newpipe.testUtil.TrampolineSchedulerRule;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.time.OffsetDateTime;
|
|
||||||
import java.util.Comparator;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
public class SubscriptionManagerTest {
|
public class SubscriptionManagerTest {
|
||||||
|
@ -58,7 +52,7 @@ public class SubscriptionManagerTest {
|
||||||
final ChannelInfo info = ChannelInfo.getInfo("https://www.youtube.com/c/3blue1brown");
|
final ChannelInfo info = ChannelInfo.getInfo("https://www.youtube.com/c/3blue1brown");
|
||||||
final SubscriptionEntity subscription = SubscriptionEntity.from(info);
|
final SubscriptionEntity subscription = SubscriptionEntity.from(info);
|
||||||
|
|
||||||
manager.insertSubscription(subscription, info);
|
manager.insertSubscription(subscription);
|
||||||
final SubscriptionEntity readSubscription = getAssertOneSubscriptionEntity();
|
final SubscriptionEntity readSubscription = getAssertOneSubscriptionEntity();
|
||||||
|
|
||||||
// the uid has changed, since the uid is chosen upon inserting, but the rest should match
|
// the uid has changed, since the uid is chosen upon inserting, but the rest should match
|
||||||
|
@ -76,7 +70,7 @@ public class SubscriptionManagerTest {
|
||||||
final SubscriptionEntity subscription = SubscriptionEntity.from(info);
|
final SubscriptionEntity subscription = SubscriptionEntity.from(info);
|
||||||
subscription.setNotificationMode(0);
|
subscription.setNotificationMode(0);
|
||||||
|
|
||||||
manager.insertSubscription(subscription, info);
|
manager.insertSubscription(subscription);
|
||||||
manager.updateNotificationMode(subscription.getServiceId(), subscription.getUrl(), 1)
|
manager.updateNotificationMode(subscription.getServiceId(), subscription.getUrl(), 1)
|
||||||
.blockingAwait();
|
.blockingAwait();
|
||||||
final SubscriptionEntity anotherSubscription = getAssertOneSubscriptionEntity();
|
final SubscriptionEntity anotherSubscription = getAssertOneSubscriptionEntity();
|
||||||
|
@ -85,35 +79,4 @@ public class SubscriptionManagerTest {
|
||||||
assertEquals(subscription.getUrl(), anotherSubscription.getUrl());
|
assertEquals(subscription.getUrl(), anotherSubscription.getUrl());
|
||||||
assertEquals(1, anotherSubscription.getNotificationMode());
|
assertEquals(1, anotherSubscription.getNotificationMode());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testRememberRecentStreams() throws ExtractionException, IOException {
|
|
||||||
final ChannelInfo info = ChannelInfo.getInfo("https://www.youtube.com/c/Polyphia");
|
|
||||||
final List<StreamInfoItem> relatedItems = List.of(
|
|
||||||
new StreamInfoItem(0, "a", "b", StreamType.VIDEO_STREAM),
|
|
||||||
new StreamInfoItem(1, "c", "d", StreamType.AUDIO_STREAM),
|
|
||||||
new StreamInfoItem(2, "e", "f", StreamType.AUDIO_LIVE_STREAM),
|
|
||||||
new StreamInfoItem(3, "g", "h", StreamType.LIVE_STREAM));
|
|
||||||
relatedItems.forEach(item -> {
|
|
||||||
// these two fields must be non-null for the insert to succeed
|
|
||||||
item.setUploaderUrl(info.getUrl());
|
|
||||||
item.setUploaderName(info.getName());
|
|
||||||
// the upload date must not be too much in the past for the item to actually be inserted
|
|
||||||
item.setUploadDate(new DateWrapper(OffsetDateTime.now()));
|
|
||||||
});
|
|
||||||
info.setRelatedItems(relatedItems);
|
|
||||||
final SubscriptionEntity subscription = SubscriptionEntity.from(info);
|
|
||||||
|
|
||||||
manager.insertSubscription(subscription, info);
|
|
||||||
final List<StreamEntity> streams = database.streamDAO().getAll().blockingFirst();
|
|
||||||
|
|
||||||
assertEquals(4, streams.size());
|
|
||||||
streams.sort(Comparator.comparing(StreamEntity::getServiceId));
|
|
||||||
for (int i = 0; i < 4; i++) {
|
|
||||||
assertEquals(relatedItems.get(0).getServiceId(), streams.get(0).getServiceId());
|
|
||||||
assertEquals(relatedItems.get(0).getUrl(), streams.get(0).getUrl());
|
|
||||||
assertEquals(relatedItems.get(0).getName(), streams.get(0).getTitle());
|
|
||||||
assertEquals(relatedItems.get(0).getStreamType(), streams.get(0).getStreamType());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,15 +12,21 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
import androidx.test.filters.MediumTest
|
import androidx.test.filters.MediumTest
|
||||||
import androidx.test.internal.runner.junit4.statement.UiThreadStatement
|
import androidx.test.internal.runner.junit4.statement.UiThreadStatement
|
||||||
import org.junit.Assert
|
import org.junit.Assert
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Assert.assertFalse
|
||||||
|
import org.junit.Assert.assertNull
|
||||||
|
import org.junit.Assert.assertTrue
|
||||||
import org.junit.Before
|
import org.junit.Before
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
import org.schabi.newpipe.R
|
import org.schabi.newpipe.R
|
||||||
import org.schabi.newpipe.extractor.MediaFormat
|
import org.schabi.newpipe.extractor.MediaFormat
|
||||||
|
import org.schabi.newpipe.extractor.downloader.Response
|
||||||
import org.schabi.newpipe.extractor.stream.AudioStream
|
import org.schabi.newpipe.extractor.stream.AudioStream
|
||||||
import org.schabi.newpipe.extractor.stream.Stream
|
import org.schabi.newpipe.extractor.stream.Stream
|
||||||
import org.schabi.newpipe.extractor.stream.SubtitlesStream
|
import org.schabi.newpipe.extractor.stream.SubtitlesStream
|
||||||
import org.schabi.newpipe.extractor.stream.VideoStream
|
import org.schabi.newpipe.extractor.stream.VideoStream
|
||||||
|
import org.schabi.newpipe.util.StreamItemAdapter.StreamInfoWrapper
|
||||||
|
|
||||||
@MediumTest
|
@MediumTest
|
||||||
@RunWith(AndroidJUnit4::class)
|
@RunWith(AndroidJUnit4::class)
|
||||||
|
@ -84,7 +90,7 @@ class StreamItemAdapterTest {
|
||||||
@Test
|
@Test
|
||||||
fun subtitleStreams_noIcon() {
|
fun subtitleStreams_noIcon() {
|
||||||
val adapter = StreamItemAdapter<SubtitlesStream, Stream>(
|
val adapter = StreamItemAdapter<SubtitlesStream, Stream>(
|
||||||
StreamItemAdapter.StreamSizeWrapper(
|
StreamItemAdapter.StreamInfoWrapper(
|
||||||
(0 until 5).map {
|
(0 until 5).map {
|
||||||
SubtitlesStream.Builder()
|
SubtitlesStream.Builder()
|
||||||
.setContent("https://example.com", true)
|
.setContent("https://example.com", true)
|
||||||
|
@ -105,7 +111,7 @@ class StreamItemAdapterTest {
|
||||||
@Test
|
@Test
|
||||||
fun audioStreams_noIcon() {
|
fun audioStreams_noIcon() {
|
||||||
val adapter = StreamItemAdapter<AudioStream, Stream>(
|
val adapter = StreamItemAdapter<AudioStream, Stream>(
|
||||||
StreamItemAdapter.StreamSizeWrapper(
|
StreamItemAdapter.StreamInfoWrapper(
|
||||||
(0 until 5).map {
|
(0 until 5).map {
|
||||||
AudioStream.Builder()
|
AudioStream.Builder()
|
||||||
.setId(Stream.ID_UNKNOWN)
|
.setId(Stream.ID_UNKNOWN)
|
||||||
|
@ -123,12 +129,109 @@ class StreamItemAdapterTest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun retrieveMediaFormatFromFileTypeHeaders() {
|
||||||
|
val streams = getIncompleteAudioStreams(5)
|
||||||
|
val wrapper = StreamInfoWrapper(streams, context)
|
||||||
|
val retrieveMediaFormat = { stream: AudioStream, response: Response ->
|
||||||
|
StreamInfoWrapper.retrieveMediaFormatFromFileTypeHeaders(stream, wrapper, response)
|
||||||
|
}
|
||||||
|
val helper = AssertionHelper(streams, wrapper, retrieveMediaFormat)
|
||||||
|
|
||||||
|
helper.assertInvalidResponse(getResponse(mapOf(Pair("content-length", "mp3"))), 0)
|
||||||
|
helper.assertInvalidResponse(getResponse(mapOf(Pair("file-type", "mp0"))), 1)
|
||||||
|
|
||||||
|
helper.assertValidResponse(getResponse(mapOf(Pair("x-amz-meta-file-type", "aiff"))), 2, MediaFormat.AIFF)
|
||||||
|
helper.assertValidResponse(getResponse(mapOf(Pair("file-type", "mp3"))), 3, MediaFormat.MP3)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun retrieveMediaFormatFromContentDispositionHeader() {
|
||||||
|
val streams = getIncompleteAudioStreams(11)
|
||||||
|
val wrapper = StreamInfoWrapper(streams, context)
|
||||||
|
val retrieveMediaFormat = { stream: AudioStream, response: Response ->
|
||||||
|
StreamInfoWrapper.retrieveMediaFormatFromContentDispositionHeader(stream, wrapper, response)
|
||||||
|
}
|
||||||
|
val helper = AssertionHelper(streams, wrapper, retrieveMediaFormat)
|
||||||
|
|
||||||
|
helper.assertInvalidResponse(getResponse(mapOf(Pair("content-length", "mp3"))), 0)
|
||||||
|
helper.assertInvalidResponse(
|
||||||
|
getResponse(mapOf(Pair("Content-Disposition", "filename=\"train.png\""))), 1
|
||||||
|
)
|
||||||
|
helper.assertInvalidResponse(
|
||||||
|
getResponse(mapOf(Pair("Content-Disposition", "form-data; name=\"data.csv\""))), 2
|
||||||
|
)
|
||||||
|
helper.assertInvalidResponse(
|
||||||
|
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
|
||||||
|
)
|
||||||
|
|
||||||
|
helper.assertValidResponse(
|
||||||
|
getResponse(mapOf(Pair("Content-Disposition", "filename=\"train.ogg\""))),
|
||||||
|
5, MediaFormat.OGG
|
||||||
|
)
|
||||||
|
helper.assertValidResponse(
|
||||||
|
getResponse(mapOf(Pair("Content-Disposition", "some-form-data; filename=\"audio.flac\""))),
|
||||||
|
6, MediaFormat.FLAC
|
||||||
|
)
|
||||||
|
helper.assertValidResponse(
|
||||||
|
getResponse(mapOf(Pair("Content-Disposition", "form-data; name=\"audio.aiff\"; filename=\"audio.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
|
||||||
|
)
|
||||||
|
helper.assertValidResponse(
|
||||||
|
getResponse(mapOf(Pair("Content-Disposition", "form-data; name=\"audio.mp3\"; filename=\"audio.opus\"; filename*=UTF-8''alien.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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun retrieveMediaFormatFromContentTypeHeader() {
|
||||||
|
val streams = getIncompleteAudioStreams(12)
|
||||||
|
val wrapper = StreamInfoWrapper(streams, context)
|
||||||
|
val retrieveMediaFormat = { stream: AudioStream, response: Response ->
|
||||||
|
StreamInfoWrapper.retrieveMediaFormatFromContentTypeHeader(stream, wrapper, response)
|
||||||
|
}
|
||||||
|
val helper = AssertionHelper(streams, wrapper, retrieveMediaFormat)
|
||||||
|
|
||||||
|
helper.assertInvalidResponse(getResponse(mapOf(Pair("content-length", "984501"))), 0)
|
||||||
|
helper.assertInvalidResponse(getResponse(mapOf(Pair("Content-Type", "audio/xyz"))), 1)
|
||||||
|
helper.assertInvalidResponse(getResponse(mapOf(Pair("Content-Type", "mp3"))), 2)
|
||||||
|
helper.assertInvalidResponse(getResponse(mapOf(Pair("Content-Type", "mp3"))), 3)
|
||||||
|
helper.assertInvalidResponse(getResponse(mapOf(Pair("Content-Type", "audio/mpeg"))), 4)
|
||||||
|
helper.assertInvalidResponse(getResponse(mapOf(Pair("Content-Type", "audio/aif"))), 5)
|
||||||
|
helper.assertInvalidResponse(getResponse(mapOf(Pair("Content-Type", "whatever"))), 6)
|
||||||
|
helper.assertInvalidResponse(getResponse(mapOf()), 7)
|
||||||
|
|
||||||
|
helper.assertValidResponse(
|
||||||
|
getResponse(mapOf(Pair("Content-Type", "audio/flac"))), 8, MediaFormat.FLAC
|
||||||
|
)
|
||||||
|
helper.assertValidResponse(
|
||||||
|
getResponse(mapOf(Pair("Content-Type", "audio/wav"))), 9, MediaFormat.WAV
|
||||||
|
)
|
||||||
|
helper.assertValidResponse(
|
||||||
|
getResponse(mapOf(Pair("Content-Type", "audio/opus"))), 10, MediaFormat.OPUS
|
||||||
|
)
|
||||||
|
helper.assertValidResponse(
|
||||||
|
getResponse(mapOf(Pair("Content-Type", "audio/aiff"))), 11, MediaFormat.AIFF
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return a list of video streams, in which their video only property mirrors the provided
|
* @return a list of video streams, in which their video only property mirrors the provided
|
||||||
* [videoOnly] vararg.
|
* [videoOnly] vararg.
|
||||||
*/
|
*/
|
||||||
private fun getVideoStreams(vararg videoOnly: Boolean) =
|
private fun getVideoStreams(vararg videoOnly: Boolean) =
|
||||||
StreamItemAdapter.StreamSizeWrapper(
|
StreamItemAdapter.StreamInfoWrapper(
|
||||||
videoOnly.map {
|
videoOnly.map {
|
||||||
VideoStream.Builder()
|
VideoStream.Builder()
|
||||||
.setId(Stream.ID_UNKNOWN)
|
.setId(Stream.ID_UNKNOWN)
|
||||||
|
@ -161,6 +264,19 @@ class StreamItemAdapterTest {
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
private fun getIncompleteAudioStreams(size: Int): List<AudioStream> {
|
||||||
|
val list = ArrayList<AudioStream>(size)
|
||||||
|
for (i in 1..size) {
|
||||||
|
list.add(
|
||||||
|
AudioStream.Builder()
|
||||||
|
.setId(Stream.ID_UNKNOWN)
|
||||||
|
.setContent("https://example.com/$i", true)
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return list
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks whether the item at [position] in the [spinner] has the correct icon visibility when
|
* Checks whether the item at [position] in the [spinner] has the correct icon visibility when
|
||||||
* it is shown in normal mode (selected) and in dropdown mode (user is choosing one of a list).
|
* it is shown in normal mode (selected) and in dropdown mode (user is choosing one of a list).
|
||||||
|
@ -196,11 +312,56 @@ class StreamItemAdapterTest {
|
||||||
streams.forEachIndexed { index, stream ->
|
streams.forEachIndexed { index, stream ->
|
||||||
val secondaryStreamHelper: SecondaryStreamHelper<T>? = stream?.let {
|
val secondaryStreamHelper: SecondaryStreamHelper<T>? = stream?.let {
|
||||||
SecondaryStreamHelper(
|
SecondaryStreamHelper(
|
||||||
StreamItemAdapter.StreamSizeWrapper(streams, context),
|
StreamItemAdapter.StreamInfoWrapper(streams, context),
|
||||||
it
|
it
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
put(index, secondaryStreamHelper)
|
put(index, secondaryStreamHelper)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun getResponse(headers: Map<String, String>): Response {
|
||||||
|
val listHeaders = HashMap<String, List<String>>()
|
||||||
|
headers.forEach { entry ->
|
||||||
|
listHeaders[entry.key] = listOf(entry.value)
|
||||||
|
}
|
||||||
|
return Response(200, null, listHeaders, "", "")
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper class for assertion related to extractions of [MediaFormat]s.
|
||||||
|
*/
|
||||||
|
class AssertionHelper<T : Stream>(
|
||||||
|
private val streams: List<T>,
|
||||||
|
private val wrapper: StreamInfoWrapper<T>,
|
||||||
|
private val retrieveMediaFormat: (stream: T, response: Response) -> Boolean
|
||||||
|
) {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Assert that an invalid response does not result in wrongly extracted [MediaFormat].
|
||||||
|
*/
|
||||||
|
fun assertInvalidResponse(
|
||||||
|
response: Response,
|
||||||
|
index: Int
|
||||||
|
) {
|
||||||
|
assertFalse(
|
||||||
|
"invalid header returns valid value", retrieveMediaFormat(streams[index], response)
|
||||||
|
)
|
||||||
|
assertNull("Media format extracted although stated otherwise", wrapper.getFormat(index))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Assert that a valid response results in correctly extracted and handled [MediaFormat].
|
||||||
|
*/
|
||||||
|
fun assertValidResponse(
|
||||||
|
response: Response,
|
||||||
|
index: Int,
|
||||||
|
format: MediaFormat
|
||||||
|
) {
|
||||||
|
assertTrue(
|
||||||
|
"header was not recognized", retrieveMediaFormat(streams[index], response)
|
||||||
|
)
|
||||||
|
assertEquals("Wrong media format extracted", format, wrapper.getFormat(index))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,6 +25,7 @@ import android.view.ViewGroup;
|
||||||
import androidx.annotation.IntDef;
|
import androidx.annotation.IntDef;
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.core.os.BundleCompat;
|
||||||
import androidx.lifecycle.Lifecycle;
|
import androidx.lifecycle.Lifecycle;
|
||||||
import androidx.viewpager.widget.PagerAdapter;
|
import androidx.viewpager.widget.PagerAdapter;
|
||||||
|
|
||||||
|
@ -284,7 +285,7 @@ public abstract class FragmentStatePagerAdapterMenuWorkaround extends PagerAdapt
|
||||||
Bundle state = null;
|
Bundle state = null;
|
||||||
if (!mSavedState.isEmpty()) {
|
if (!mSavedState.isEmpty()) {
|
||||||
state = new Bundle();
|
state = new Bundle();
|
||||||
state.putParcelableArray("states", mSavedState.toArray(new Fragment.SavedState[0]));
|
state.putParcelableArrayList("states", mSavedState);
|
||||||
}
|
}
|
||||||
for (int i = 0; i < mFragments.size(); i++) {
|
for (int i = 0; i < mFragments.size(); i++) {
|
||||||
final Fragment f = mFragments.get(i);
|
final Fragment f = mFragments.get(i);
|
||||||
|
@ -311,13 +312,12 @@ public abstract class FragmentStatePagerAdapterMenuWorkaround extends PagerAdapt
|
||||||
if (state != null) {
|
if (state != null) {
|
||||||
final Bundle bundle = (Bundle) state;
|
final Bundle bundle = (Bundle) state;
|
||||||
bundle.setClassLoader(loader);
|
bundle.setClassLoader(loader);
|
||||||
final Parcelable[] fss = bundle.getParcelableArray("states");
|
final var states = BundleCompat.getParcelableArrayList(bundle, "states",
|
||||||
|
Fragment.SavedState.class);
|
||||||
mSavedState.clear();
|
mSavedState.clear();
|
||||||
mFragments.clear();
|
mFragments.clear();
|
||||||
if (fss != null) {
|
if (states != null) {
|
||||||
for (final Parcelable parcelable : fss) {
|
mSavedState.addAll(states);
|
||||||
mSavedState.add((Fragment.SavedState) parcelable);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
final Iterable<String> keys = bundle.keySet();
|
final Iterable<String> keys = bundle.keySet();
|
||||||
for (final String key : keys) {
|
for (final String key : keys) {
|
||||||
|
|
|
@ -20,9 +20,11 @@ import org.schabi.newpipe.extractor.downloader.Downloader;
|
||||||
import org.schabi.newpipe.ktx.ExceptionUtils;
|
import org.schabi.newpipe.ktx.ExceptionUtils;
|
||||||
import org.schabi.newpipe.settings.NewPipeSettings;
|
import org.schabi.newpipe.settings.NewPipeSettings;
|
||||||
import org.schabi.newpipe.util.Localization;
|
import org.schabi.newpipe.util.Localization;
|
||||||
import org.schabi.newpipe.util.PicassoHelper;
|
import org.schabi.newpipe.util.image.ImageStrategy;
|
||||||
|
import org.schabi.newpipe.util.image.PicassoHelper;
|
||||||
import org.schabi.newpipe.util.ServiceHelper;
|
import org.schabi.newpipe.util.ServiceHelper;
|
||||||
import org.schabi.newpipe.util.StateSaver;
|
import org.schabi.newpipe.util.StateSaver;
|
||||||
|
import org.schabi.newpipe.util.image.PreferredImageQuality;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InterruptedIOException;
|
import java.io.InterruptedIOException;
|
||||||
|
@ -99,8 +101,9 @@ public class App extends Application {
|
||||||
// Initialize image loader
|
// Initialize image loader
|
||||||
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
|
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
|
||||||
PicassoHelper.init(this);
|
PicassoHelper.init(this);
|
||||||
PicassoHelper.setShouldLoadImages(
|
ImageStrategy.setPreferredImageQuality(PreferredImageQuality.fromPreferenceKey(this,
|
||||||
prefs.getBoolean(getString(R.string.download_thumbnail_key), true));
|
prefs.getString(getString(R.string.image_quality_key),
|
||||||
|
getString(R.string.image_quality_default))));
|
||||||
PicassoHelper.setIndicatorsEnabled(MainActivity.DEBUG
|
PicassoHelper.setIndicatorsEnabled(MainActivity.DEBUG
|
||||||
&& prefs.getBoolean(getString(R.string.show_image_indicators_key), false));
|
&& prefs.getBoolean(getString(R.string.show_image_indicators_key), false));
|
||||||
|
|
||||||
|
|
|
@ -80,9 +80,29 @@ public abstract class BaseFragment extends Fragment {
|
||||||
// Init
|
// Init
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method is called in {@link #onViewCreated(View, Bundle)} to initialize the views.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* {@link #initListeners()} is called after this method to initialize the corresponding
|
||||||
|
* listeners.
|
||||||
|
* </p>
|
||||||
|
* @param rootView The inflated view for this fragment
|
||||||
|
* (provided by {@link #onViewCreated(View, Bundle)})
|
||||||
|
* @param savedInstanceState The saved state of this fragment
|
||||||
|
* (provided by {@link #onViewCreated(View, Bundle)})
|
||||||
|
*/
|
||||||
protected void initViews(final View rootView, final Bundle savedInstanceState) {
|
protected void initViews(final View rootView, final Bundle savedInstanceState) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the listeners for this fragment.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* This method is called after {@link #initViews(View, Bundle)}
|
||||||
|
* in {@link #onViewCreated(View, Bundle)}.
|
||||||
|
* </p>
|
||||||
|
*/
|
||||||
protected void initListeners() {
|
protected void initListeners() {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -100,9 +120,20 @@ public abstract class BaseFragment extends Fragment {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds the root fragment by looping through all of the parent fragments. The root fragment
|
||||||
|
* is supposed to be {@link org.schabi.newpipe.fragments.MainFragment}, and is the fragment that
|
||||||
|
* handles keeping the backstack of opened fragments in NewPipe, and also the player bottom
|
||||||
|
* sheet. This function therefore returns the fragment manager of said fragment.
|
||||||
|
*
|
||||||
|
* @return the fragment manager of the root fragment, i.e.
|
||||||
|
* {@link org.schabi.newpipe.fragments.MainFragment}
|
||||||
|
*/
|
||||||
protected FragmentManager getFM() {
|
protected FragmentManager getFM() {
|
||||||
return getParentFragment() == null
|
Fragment current = this;
|
||||||
? getFragmentManager()
|
while (current.getParentFragment() != null) {
|
||||||
: getParentFragment().getFragmentManager();
|
current = current.getParentFragment();
|
||||||
|
}
|
||||||
|
return current.getFragmentManager();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -44,6 +44,7 @@ import android.widget.FrameLayout;
|
||||||
import android.widget.Spinner;
|
import android.widget.Spinner;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
import androidx.appcompat.app.ActionBar;
|
import androidx.appcompat.app.ActionBar;
|
||||||
import androidx.appcompat.app.ActionBarDrawerToggle;
|
import androidx.appcompat.app.ActionBarDrawerToggle;
|
||||||
import androidx.appcompat.app.AppCompatActivity;
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
|
@ -51,6 +52,7 @@ import androidx.core.app.ActivityCompat;
|
||||||
import androidx.core.view.GravityCompat;
|
import androidx.core.view.GravityCompat;
|
||||||
import androidx.drawerlayout.widget.DrawerLayout;
|
import androidx.drawerlayout.widget.DrawerLayout;
|
||||||
import androidx.fragment.app.Fragment;
|
import androidx.fragment.app.Fragment;
|
||||||
|
import androidx.fragment.app.FragmentContainerView;
|
||||||
import androidx.fragment.app.FragmentManager;
|
import androidx.fragment.app.FragmentManager;
|
||||||
import androidx.preference.PreferenceManager;
|
import androidx.preference.PreferenceManager;
|
||||||
|
|
||||||
|
@ -64,11 +66,13 @@ import org.schabi.newpipe.databinding.ToolbarLayoutBinding;
|
||||||
import org.schabi.newpipe.error.ErrorUtil;
|
import org.schabi.newpipe.error.ErrorUtil;
|
||||||
import org.schabi.newpipe.extractor.NewPipe;
|
import org.schabi.newpipe.extractor.NewPipe;
|
||||||
import org.schabi.newpipe.extractor.StreamingService;
|
import org.schabi.newpipe.extractor.StreamingService;
|
||||||
|
import org.schabi.newpipe.extractor.comments.CommentsInfoItem;
|
||||||
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||||
import org.schabi.newpipe.extractor.services.peertube.PeertubeInstance;
|
import org.schabi.newpipe.extractor.services.peertube.PeertubeInstance;
|
||||||
import org.schabi.newpipe.fragments.BackPressable;
|
import org.schabi.newpipe.fragments.BackPressable;
|
||||||
import org.schabi.newpipe.fragments.MainFragment;
|
import org.schabi.newpipe.fragments.MainFragment;
|
||||||
import org.schabi.newpipe.fragments.detail.VideoDetailFragment;
|
import org.schabi.newpipe.fragments.detail.VideoDetailFragment;
|
||||||
|
import org.schabi.newpipe.fragments.list.comments.CommentRepliesFragment;
|
||||||
import org.schabi.newpipe.fragments.list.search.SearchFragment;
|
import org.schabi.newpipe.fragments.list.search.SearchFragment;
|
||||||
import org.schabi.newpipe.local.feed.notifications.NotificationWorker;
|
import org.schabi.newpipe.local.feed.notifications.NotificationWorker;
|
||||||
import org.schabi.newpipe.player.Player;
|
import org.schabi.newpipe.player.Player;
|
||||||
|
@ -219,14 +223,14 @@ public class MainActivity extends AppCompatActivity {
|
||||||
final int currentServiceId = ServiceHelper.getSelectedServiceId(this);
|
final int currentServiceId = ServiceHelper.getSelectedServiceId(this);
|
||||||
final StreamingService service = NewPipe.getService(currentServiceId);
|
final StreamingService service = NewPipe.getService(currentServiceId);
|
||||||
|
|
||||||
int kioskId = 0;
|
int kioskMenuItemId = 0;
|
||||||
|
|
||||||
for (final String ks : service.getKioskList().getAvailableKiosks()) {
|
for (final String ks : service.getKioskList().getAvailableKiosks()) {
|
||||||
drawerLayoutBinding.navigation.getMenu()
|
drawerLayoutBinding.navigation.getMenu()
|
||||||
.add(R.id.menu_tabs_group, kioskId, 0, KioskTranslator
|
.add(R.id.menu_tabs_group, kioskMenuItemId, 0, KioskTranslator
|
||||||
.getTranslatedKioskName(ks, this))
|
.getTranslatedKioskName(ks, this))
|
||||||
.setIcon(KioskTranslator.getKioskIcon(ks));
|
.setIcon(KioskTranslator.getKioskIcon(ks));
|
||||||
kioskId++;
|
kioskMenuItemId++;
|
||||||
}
|
}
|
||||||
|
|
||||||
drawerLayoutBinding.navigation.getMenu()
|
drawerLayoutBinding.navigation.getMenu()
|
||||||
|
@ -306,20 +310,16 @@ public class MainActivity extends AppCompatActivity {
|
||||||
NavigationHelper.openStatisticFragment(getSupportFragmentManager());
|
NavigationHelper.openStatisticFragment(getSupportFragmentManager());
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
final int currentServiceId = ServiceHelper.getSelectedServiceId(this);
|
final StreamingService currentService = ServiceHelper.getSelectedService(this);
|
||||||
final StreamingService service = NewPipe.getService(currentServiceId);
|
int kioskMenuItemId = 0;
|
||||||
String serviceName = "";
|
for (final String kioskId : currentService.getKioskList().getAvailableKiosks()) {
|
||||||
|
if (kioskMenuItemId == item.getItemId()) {
|
||||||
int kioskId = 0;
|
NavigationHelper.openKioskFragment(getSupportFragmentManager(),
|
||||||
for (final String ks : service.getKioskList().getAvailableKiosks()) {
|
currentService.getServiceId(), kioskId);
|
||||||
if (kioskId == item.getItemId()) {
|
break;
|
||||||
serviceName = ks;
|
|
||||||
}
|
}
|
||||||
kioskId++;
|
kioskMenuItemId++;
|
||||||
}
|
}
|
||||||
|
|
||||||
NavigationHelper.openKioskFragment(getSupportFragmentManager(), currentServiceId,
|
|
||||||
serviceName);
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -550,14 +550,21 @@ public class MainActivity extends AppCompatActivity {
|
||||||
// interacts with a fragment inside fragment_holder so all back presses should be
|
// interacts with a fragment inside fragment_holder so all back presses should be
|
||||||
// handled by it
|
// handled by it
|
||||||
if (bottomSheetHiddenOrCollapsed()) {
|
if (bottomSheetHiddenOrCollapsed()) {
|
||||||
final Fragment fragment = getSupportFragmentManager()
|
final FragmentManager fm = getSupportFragmentManager();
|
||||||
.findFragmentById(R.id.fragment_holder);
|
final Fragment fragment = fm.findFragmentById(R.id.fragment_holder);
|
||||||
// If current fragment implements BackPressable (i.e. can/wanna handle back press)
|
// If current fragment implements BackPressable (i.e. can/wanna handle back press)
|
||||||
// delegate the back press to it
|
// delegate the back press to it
|
||||||
if (fragment instanceof BackPressable) {
|
if (fragment instanceof BackPressable) {
|
||||||
if (((BackPressable) fragment).onBackPressed()) {
|
if (((BackPressable) fragment).onBackPressed()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
} else if (fragment instanceof CommentRepliesFragment) {
|
||||||
|
// expand DetailsFragment if CommentRepliesFragment was opened
|
||||||
|
// to show the top level comments again
|
||||||
|
// Expand DetailsFragment if CommentRepliesFragment was opened
|
||||||
|
// and no other CommentRepliesFragments are on top of the back stack
|
||||||
|
// to show the top level comments again.
|
||||||
|
openDetailFragmentFromCommentReplies(fm, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
|
@ -633,10 +640,17 @@ public class MainActivity extends AppCompatActivity {
|
||||||
* </pre>
|
* </pre>
|
||||||
*/
|
*/
|
||||||
private void onHomeButtonPressed() {
|
private void onHomeButtonPressed() {
|
||||||
// If search fragment wasn't found in the backstack...
|
final FragmentManager fm = getSupportFragmentManager();
|
||||||
if (!NavigationHelper.tryGotoSearchFragment(getSupportFragmentManager())) {
|
final Fragment fragment = fm.findFragmentById(R.id.fragment_holder);
|
||||||
// ...go to the main fragment
|
|
||||||
NavigationHelper.gotoMainFragment(getSupportFragmentManager());
|
if (fragment instanceof CommentRepliesFragment) {
|
||||||
|
// Expand DetailsFragment if CommentRepliesFragment was opened
|
||||||
|
// and no other CommentRepliesFragments are on top of the back stack
|
||||||
|
// to show the top level comments again.
|
||||||
|
openDetailFragmentFromCommentReplies(fm, true);
|
||||||
|
} else if (!NavigationHelper.tryGotoSearchFragment(fm)) {
|
||||||
|
// If search fragment wasn't found in the backstack go to the main fragment
|
||||||
|
NavigationHelper.gotoMainFragment(fm);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -832,6 +846,68 @@ public class MainActivity extends AppCompatActivity {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void openDetailFragmentFromCommentReplies(
|
||||||
|
@NonNull final FragmentManager fm,
|
||||||
|
final boolean popBackStack
|
||||||
|
) {
|
||||||
|
// obtain the name of the fragment under the replies fragment that's going to be popped
|
||||||
|
@Nullable final String fragmentUnderEntryName;
|
||||||
|
if (fm.getBackStackEntryCount() < 2) {
|
||||||
|
fragmentUnderEntryName = null;
|
||||||
|
} else {
|
||||||
|
fragmentUnderEntryName = fm.getBackStackEntryAt(fm.getBackStackEntryCount() - 2)
|
||||||
|
.getName();
|
||||||
|
}
|
||||||
|
|
||||||
|
// the root comment is the comment for which the user opened the replies page
|
||||||
|
@Nullable final CommentRepliesFragment repliesFragment =
|
||||||
|
(CommentRepliesFragment) fm.findFragmentByTag(CommentRepliesFragment.TAG);
|
||||||
|
@Nullable final CommentsInfoItem rootComment =
|
||||||
|
repliesFragment == null ? null : repliesFragment.getCommentsInfoItem();
|
||||||
|
|
||||||
|
// sometimes this function pops the backstack, other times it's handled by the system
|
||||||
|
if (popBackStack) {
|
||||||
|
fm.popBackStackImmediate();
|
||||||
|
}
|
||||||
|
|
||||||
|
// only expand the bottom sheet back if there are no more nested comment replies fragments
|
||||||
|
// stacked under the one that is currently being popped
|
||||||
|
if (CommentRepliesFragment.TAG.equals(fragmentUnderEntryName)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final BottomSheetBehavior<FragmentContainerView> behavior = BottomSheetBehavior
|
||||||
|
.from(mainBinding.fragmentPlayerHolder);
|
||||||
|
// do not return to the comment if the details fragment was closed
|
||||||
|
if (behavior.getState() == BottomSheetBehavior.STATE_HIDDEN) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// scroll to the root comment once the bottom sheet expansion animation is finished
|
||||||
|
behavior.addBottomSheetCallback(new BottomSheetBehavior.BottomSheetCallback() {
|
||||||
|
@Override
|
||||||
|
public void onStateChanged(@NonNull final View bottomSheet,
|
||||||
|
final int newState) {
|
||||||
|
if (newState == BottomSheetBehavior.STATE_EXPANDED) {
|
||||||
|
final Fragment detailFragment = fm.findFragmentById(
|
||||||
|
R.id.fragment_player_holder);
|
||||||
|
if (detailFragment instanceof VideoDetailFragment && rootComment != null) {
|
||||||
|
// should always be the case
|
||||||
|
((VideoDetailFragment) detailFragment).scrollToComment(rootComment);
|
||||||
|
}
|
||||||
|
behavior.removeBottomSheetCallback(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onSlide(@NonNull final View bottomSheet, final float slideOffset) {
|
||||||
|
// not needed, listener is removed once the sheet is expanded
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
behavior.setState(BottomSheetBehavior.STATE_EXPANDED);
|
||||||
|
}
|
||||||
|
|
||||||
private boolean bottomSheetHiddenOrCollapsed() {
|
private boolean bottomSheetHiddenOrCollapsed() {
|
||||||
final BottomSheetBehavior<FrameLayout> bottomSheetBehavior =
|
final BottomSheetBehavior<FrameLayout> bottomSheetBehavior =
|
||||||
BottomSheetBehavior.from(mainBinding.fragmentPlayerHolder);
|
BottomSheetBehavior.from(mainBinding.fragmentPlayerHolder);
|
||||||
|
|
|
@ -75,7 +75,7 @@ public final class QueueItemMenuUtil {
|
||||||
return true;
|
return true;
|
||||||
case R.id.menu_item_share:
|
case R.id.menu_item_share:
|
||||||
shareText(context, item.getTitle(), item.getUrl(),
|
shareText(context, item.getTitle(), item.getUrl(),
|
||||||
item.getThumbnailUrl());
|
item.getThumbnails());
|
||||||
return true;
|
return true;
|
||||||
case R.id.menu_item_download:
|
case R.id.menu_item_download:
|
||||||
fetchStreamInfoAndSaveToDatabase(context, item.getServiceId(), item.getUrl(),
|
fetchStreamInfoAndSaveToDatabase(context, item.getServiceId(), item.getUrl(),
|
||||||
|
|
|
@ -45,6 +45,7 @@ import org.schabi.newpipe.database.stream.model.StreamEntity;
|
||||||
import org.schabi.newpipe.databinding.ListRadioIconItemBinding;
|
import org.schabi.newpipe.databinding.ListRadioIconItemBinding;
|
||||||
import org.schabi.newpipe.databinding.SingleChoiceDialogViewBinding;
|
import org.schabi.newpipe.databinding.SingleChoiceDialogViewBinding;
|
||||||
import org.schabi.newpipe.download.DownloadDialog;
|
import org.schabi.newpipe.download.DownloadDialog;
|
||||||
|
import org.schabi.newpipe.download.LoadingDialog;
|
||||||
import org.schabi.newpipe.error.ErrorInfo;
|
import org.schabi.newpipe.error.ErrorInfo;
|
||||||
import org.schabi.newpipe.error.ErrorUtil;
|
import org.schabi.newpipe.error.ErrorUtil;
|
||||||
import org.schabi.newpipe.error.ReCaptchaActivity;
|
import org.schabi.newpipe.error.ReCaptchaActivity;
|
||||||
|
@ -64,6 +65,7 @@ import org.schabi.newpipe.extractor.exceptions.PrivateContentException;
|
||||||
import org.schabi.newpipe.extractor.exceptions.ReCaptchaException;
|
import org.schabi.newpipe.extractor.exceptions.ReCaptchaException;
|
||||||
import org.schabi.newpipe.extractor.exceptions.SoundCloudGoPlusContentException;
|
import org.schabi.newpipe.extractor.exceptions.SoundCloudGoPlusContentException;
|
||||||
import org.schabi.newpipe.extractor.exceptions.YoutubeMusicPremiumContentException;
|
import org.schabi.newpipe.extractor.exceptions.YoutubeMusicPremiumContentException;
|
||||||
|
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler;
|
||||||
import org.schabi.newpipe.extractor.playlist.PlaylistInfo;
|
import org.schabi.newpipe.extractor.playlist.PlaylistInfo;
|
||||||
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||||
import org.schabi.newpipe.ktx.ExceptionUtils;
|
import org.schabi.newpipe.ktx.ExceptionUtils;
|
||||||
|
@ -71,10 +73,11 @@ import org.schabi.newpipe.local.dialog.PlaylistDialog;
|
||||||
import org.schabi.newpipe.player.PlayerType;
|
import org.schabi.newpipe.player.PlayerType;
|
||||||
import org.schabi.newpipe.player.helper.PlayerHelper;
|
import org.schabi.newpipe.player.helper.PlayerHelper;
|
||||||
import org.schabi.newpipe.player.helper.PlayerHolder;
|
import org.schabi.newpipe.player.helper.PlayerHolder;
|
||||||
import org.schabi.newpipe.player.playqueue.ChannelPlayQueue;
|
import org.schabi.newpipe.player.playqueue.ChannelTabPlayQueue;
|
||||||
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
||||||
import org.schabi.newpipe.player.playqueue.PlaylistPlayQueue;
|
import org.schabi.newpipe.player.playqueue.PlaylistPlayQueue;
|
||||||
import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
|
import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
|
||||||
|
import org.schabi.newpipe.util.ChannelTabHelper;
|
||||||
import org.schabi.newpipe.util.Constants;
|
import org.schabi.newpipe.util.Constants;
|
||||||
import org.schabi.newpipe.util.DeviceUtils;
|
import org.schabi.newpipe.util.DeviceUtils;
|
||||||
import org.schabi.newpipe.util.ExtractorHelper;
|
import org.schabi.newpipe.util.ExtractorHelper;
|
||||||
|
@ -789,10 +792,10 @@ public class RouterActivity extends AppCompatActivity {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}, () -> {
|
}, () ->
|
||||||
// this branch is executed if there is no activity context
|
// this branch is executed if there is no activity context
|
||||||
inFlight(false);
|
inFlight(false)
|
||||||
});
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
<T> Single<T> pleaseWait(final Single<T> single) {
|
<T> Single<T> pleaseWait(final Single<T> single) {
|
||||||
|
@ -812,19 +815,24 @@ public class RouterActivity extends AppCompatActivity {
|
||||||
@SuppressLint("CheckResult")
|
@SuppressLint("CheckResult")
|
||||||
private void openDownloadDialog(final int currentServiceId, final String currentUrl) {
|
private void openDownloadDialog(final int currentServiceId, final String currentUrl) {
|
||||||
inFlight(true);
|
inFlight(true);
|
||||||
|
final LoadingDialog loadingDialog = new LoadingDialog(R.string.loading_metadata_title);
|
||||||
|
loadingDialog.show(getParentFragmentManager(), "loadingDialog");
|
||||||
disposables.add(ExtractorHelper.getStreamInfo(currentServiceId, currentUrl, true)
|
disposables.add(ExtractorHelper.getStreamInfo(currentServiceId, currentUrl, true)
|
||||||
.subscribeOn(Schedulers.io())
|
.subscribeOn(Schedulers.io())
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.compose(this::pleaseWait)
|
.compose(this::pleaseWait)
|
||||||
.subscribe(result ->
|
.subscribe(result ->
|
||||||
runOnVisible(ctx -> {
|
runOnVisible(ctx -> {
|
||||||
|
loadingDialog.dismiss();
|
||||||
final FragmentManager fm = ctx.getSupportFragmentManager();
|
final FragmentManager fm = ctx.getSupportFragmentManager();
|
||||||
final DownloadDialog downloadDialog = new DownloadDialog(ctx, result);
|
final DownloadDialog downloadDialog = new DownloadDialog(ctx, result);
|
||||||
// dismiss listener to be handled by FragmentManager
|
// dismiss listener to be handled by FragmentManager
|
||||||
downloadDialog.show(fm, "downloadDialog");
|
downloadDialog.show(fm, "downloadDialog");
|
||||||
}
|
}
|
||||||
), throwable -> runOnVisible(ctx ->
|
), throwable -> runOnVisible(ctx -> {
|
||||||
((RouterActivity) ctx).showUnsupportedUrlDialog(currentUrl))));
|
loadingDialog.dismiss();
|
||||||
|
((RouterActivity) ctx).showUnsupportedUrlDialog(currentUrl);
|
||||||
|
})));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void openAddToPlaylistDialog(final int currentServiceId, final String currentUrl) {
|
private void openAddToPlaylistDialog(final int currentServiceId, final String currentUrl) {
|
||||||
|
@ -1016,7 +1024,16 @@ public class RouterActivity extends AppCompatActivity {
|
||||||
}
|
}
|
||||||
playQueue = new SinglePlayQueue((StreamInfo) info);
|
playQueue = new SinglePlayQueue((StreamInfo) info);
|
||||||
} else if (info instanceof ChannelInfo) {
|
} else if (info instanceof ChannelInfo) {
|
||||||
playQueue = new ChannelPlayQueue((ChannelInfo) info);
|
final Optional<ListLinkHandler> playableTab = ((ChannelInfo) info).getTabs()
|
||||||
|
.stream()
|
||||||
|
.filter(ChannelTabHelper::isStreamsTab)
|
||||||
|
.findFirst();
|
||||||
|
|
||||||
|
if (playableTab.isPresent()) {
|
||||||
|
playQueue = new ChannelTabPlayQueue(info.getServiceId(), playableTab.get());
|
||||||
|
} else {
|
||||||
|
return; // there is no playable tab
|
||||||
|
}
|
||||||
} else if (info instanceof PlaylistInfo) {
|
} else if (info instanceof PlaylistInfo) {
|
||||||
playQueue = new PlaylistPlayQueue((PlaylistInfo) info);
|
playQueue = new PlaylistPlayQueue((PlaylistInfo) info);
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -116,7 +116,7 @@ class AboutActivity : AppCompatActivity() {
|
||||||
/**
|
/**
|
||||||
* List of all software components.
|
* List of all software components.
|
||||||
*/
|
*/
|
||||||
private val SOFTWARE_COMPONENTS = arrayOf(
|
private val SOFTWARE_COMPONENTS = arrayListOf(
|
||||||
SoftwareComponent(
|
SoftwareComponent(
|
||||||
"ACRA", "2013", "Kevin Gaudin",
|
"ACRA", "2013", "Kevin Gaudin",
|
||||||
"https://github.com/ACRA/acra", StandardLicenses.APACHE2
|
"https://github.com/ACRA/acra", StandardLicenses.APACHE2
|
||||||
|
|
|
@ -1,30 +1,40 @@
|
||||||
package org.schabi.newpipe.about
|
package org.schabi.newpipe.about
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.util.Base64
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
import android.webkit.WebView
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.core.os.bundleOf
|
import androidx.core.os.bundleOf
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
|
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||||
|
import io.reactivex.rxjava3.core.Observable
|
||||||
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||||
|
import io.reactivex.rxjava3.disposables.Disposable
|
||||||
|
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||||
|
import org.schabi.newpipe.BuildConfig
|
||||||
import org.schabi.newpipe.R
|
import org.schabi.newpipe.R
|
||||||
import org.schabi.newpipe.databinding.FragmentLicensesBinding
|
import org.schabi.newpipe.databinding.FragmentLicensesBinding
|
||||||
import org.schabi.newpipe.databinding.ItemSoftwareComponentBinding
|
import org.schabi.newpipe.databinding.ItemSoftwareComponentBinding
|
||||||
|
import org.schabi.newpipe.ktx.parcelableArrayList
|
||||||
|
import org.schabi.newpipe.util.Localization
|
||||||
|
import org.schabi.newpipe.util.external_communication.ShareUtils
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fragment containing the software licenses.
|
* Fragment containing the software licenses.
|
||||||
*/
|
*/
|
||||||
class LicenseFragment : Fragment() {
|
class LicenseFragment : Fragment() {
|
||||||
private lateinit var softwareComponents: Array<SoftwareComponent>
|
private lateinit var softwareComponents: List<SoftwareComponent>
|
||||||
private var activeLicense: License? = null
|
private var activeSoftwareComponent: SoftwareComponent? = null
|
||||||
private val compositeDisposable = CompositeDisposable()
|
private val compositeDisposable = CompositeDisposable()
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
softwareComponents = arguments?.getParcelableArray(ARG_COMPONENTS) as Array<SoftwareComponent>
|
softwareComponents = arguments?.parcelableArrayList<SoftwareComponent>(ARG_COMPONENTS)!!
|
||||||
activeLicense = savedInstanceState?.getSerializable(LICENSE_KEY) as? License
|
.sortedBy { it.name } // Sort components by name
|
||||||
// Sort components by name
|
activeSoftwareComponent = savedInstanceState?.getSerializable(SOFTWARE_COMPONENT_KEY) as? SoftwareComponent
|
||||||
softwareComponents.sortBy { it.name }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
|
@ -39,9 +49,8 @@ class LicenseFragment : Fragment() {
|
||||||
): View {
|
): View {
|
||||||
val binding = FragmentLicensesBinding.inflate(inflater, container, false)
|
val binding = FragmentLicensesBinding.inflate(inflater, container, false)
|
||||||
binding.licensesAppReadLicense.setOnClickListener {
|
binding.licensesAppReadLicense.setOnClickListener {
|
||||||
activeLicense = StandardLicenses.GPL3
|
|
||||||
compositeDisposable.add(
|
compositeDisposable.add(
|
||||||
showLicense(activity, StandardLicenses.GPL3)
|
showLicense(NEWPIPE_SOFTWARE_COMPONENT)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
for (component in softwareComponents) {
|
for (component in softwareComponents) {
|
||||||
|
@ -57,27 +66,72 @@ class LicenseFragment : Fragment() {
|
||||||
val root: View = componentBinding.root
|
val root: View = componentBinding.root
|
||||||
root.tag = component
|
root.tag = component
|
||||||
root.setOnClickListener {
|
root.setOnClickListener {
|
||||||
activeLicense = component.license
|
|
||||||
compositeDisposable.add(
|
compositeDisposable.add(
|
||||||
showLicense(activity, component)
|
showLicense(component)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
binding.licensesSoftwareComponents.addView(root)
|
binding.licensesSoftwareComponents.addView(root)
|
||||||
registerForContextMenu(root)
|
registerForContextMenu(root)
|
||||||
}
|
}
|
||||||
activeLicense?.let { compositeDisposable.add(showLicense(activity, it)) }
|
activeSoftwareComponent?.let { compositeDisposable.add(showLicense(it)) }
|
||||||
return binding.root
|
return binding.root
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onSaveInstanceState(savedInstanceState: Bundle) {
|
override fun onSaveInstanceState(savedInstanceState: Bundle) {
|
||||||
super.onSaveInstanceState(savedInstanceState)
|
super.onSaveInstanceState(savedInstanceState)
|
||||||
activeLicense?.let { savedInstanceState.putSerializable(LICENSE_KEY, it) }
|
activeSoftwareComponent?.let { savedInstanceState.putSerializable(SOFTWARE_COMPONENT_KEY, it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showLicense(
|
||||||
|
softwareComponent: SoftwareComponent
|
||||||
|
): Disposable {
|
||||||
|
return if (context == null) {
|
||||||
|
Disposable.empty()
|
||||||
|
} else {
|
||||||
|
val context = requireContext()
|
||||||
|
activeSoftwareComponent = softwareComponent
|
||||||
|
Observable.fromCallable { getFormattedLicense(context, softwareComponent.license) }
|
||||||
|
.subscribeOn(Schedulers.io())
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribe { formattedLicense ->
|
||||||
|
val webViewData = Base64.encodeToString(
|
||||||
|
formattedLicense.toByteArray(), Base64.NO_PADDING
|
||||||
|
)
|
||||||
|
val webView = WebView(context)
|
||||||
|
webView.loadData(webViewData, "text/html; charset=UTF-8", "base64")
|
||||||
|
|
||||||
|
Localization.assureCorrectAppLanguage(context)
|
||||||
|
val builder = AlertDialog.Builder(requireContext())
|
||||||
|
.setTitle(softwareComponent.name)
|
||||||
|
.setView(webView)
|
||||||
|
.setOnCancelListener { activeSoftwareComponent = null }
|
||||||
|
.setOnDismissListener { activeSoftwareComponent = null }
|
||||||
|
.setPositiveButton(R.string.done) { dialog, _ -> dialog.dismiss() }
|
||||||
|
|
||||||
|
if (softwareComponent != NEWPIPE_SOFTWARE_COMPONENT) {
|
||||||
|
builder.setNeutralButton(R.string.open_website_license) { _, _ ->
|
||||||
|
ShareUtils.openUrlInApp(requireContext(), softwareComponent.link)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.show()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val ARG_COMPONENTS = "components"
|
private const val ARG_COMPONENTS = "components"
|
||||||
private const val LICENSE_KEY = "ACTIVE_LICENSE"
|
private const val SOFTWARE_COMPONENT_KEY = "ACTIVE_SOFTWARE_COMPONENT"
|
||||||
fun newInstance(softwareComponents: Array<SoftwareComponent>): LicenseFragment {
|
private val NEWPIPE_SOFTWARE_COMPONENT = SoftwareComponent(
|
||||||
|
"NewPipe",
|
||||||
|
"2014-2023",
|
||||||
|
"Team NewPipe",
|
||||||
|
"https://newpipe.net/",
|
||||||
|
StandardLicenses.GPL3,
|
||||||
|
BuildConfig.VERSION_NAME
|
||||||
|
)
|
||||||
|
|
||||||
|
fun newInstance(softwareComponents: ArrayList<SoftwareComponent>): LicenseFragment {
|
||||||
val fragment = LicenseFragment()
|
val fragment = LicenseFragment()
|
||||||
fragment.arguments = bundleOf(ARG_COMPONENTS to softwareComponents)
|
fragment.arguments = bundleOf(ARG_COMPONENTS to softwareComponents)
|
||||||
return fragment
|
return fragment
|
||||||
|
|
|
@ -1,17 +1,8 @@
|
||||||
package org.schabi.newpipe.about
|
package org.schabi.newpipe.about
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.util.Base64
|
|
||||||
import android.webkit.WebView
|
|
||||||
import androidx.appcompat.app.AlertDialog
|
|
||||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
|
||||||
import io.reactivex.rxjava3.core.Observable
|
|
||||||
import io.reactivex.rxjava3.disposables.Disposable
|
|
||||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
|
||||||
import org.schabi.newpipe.R
|
import org.schabi.newpipe.R
|
||||||
import org.schabi.newpipe.util.Localization
|
|
||||||
import org.schabi.newpipe.util.ThemeHelper
|
import org.schabi.newpipe.util.ThemeHelper
|
||||||
import org.schabi.newpipe.util.external_communication.ShareUtils
|
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -20,7 +11,7 @@ import java.io.IOException
|
||||||
* @return String which contains a HTML formatted license page
|
* @return String which contains a HTML formatted license page
|
||||||
* styled according to the context's theme
|
* styled according to the context's theme
|
||||||
*/
|
*/
|
||||||
private fun getFormattedLicense(context: Context, license: License): String {
|
fun getFormattedLicense(context: Context, license: License): String {
|
||||||
try {
|
try {
|
||||||
return context.assets.open(license.filename).bufferedReader().use { it.readText() }
|
return context.assets.open(license.filename).bufferedReader().use { it.readText() }
|
||||||
// split the HTML file and insert the stylesheet into the HEAD of the file
|
// split the HTML file and insert the stylesheet into the HEAD of the file
|
||||||
|
@ -34,7 +25,7 @@ private fun getFormattedLicense(context: Context, license: License): String {
|
||||||
* @param context the Android context
|
* @param context the Android context
|
||||||
* @return String which is a CSS stylesheet according to the context's theme
|
* @return String which is a CSS stylesheet according to the context's theme
|
||||||
*/
|
*/
|
||||||
private fun getLicenseStylesheet(context: Context): String {
|
fun getLicenseStylesheet(context: Context): String {
|
||||||
val isLightTheme = ThemeHelper.isLightThemeSelected(context)
|
val isLightTheme = ThemeHelper.isLightThemeSelected(context)
|
||||||
val licenseBackgroundColor = getHexRGBColor(
|
val licenseBackgroundColor = getHexRGBColor(
|
||||||
context, if (isLightTheme) R.color.light_license_background_color else R.color.dark_license_background_color
|
context, if (isLightTheme) R.color.light_license_background_color else R.color.dark_license_background_color
|
||||||
|
@ -56,48 +47,6 @@ private fun getLicenseStylesheet(context: Context): String {
|
||||||
* @param color the color number from R.color
|
* @param color the color number from R.color
|
||||||
* @return a six characters long String with hexadecimal RGB values
|
* @return a six characters long String with hexadecimal RGB values
|
||||||
*/
|
*/
|
||||||
private fun getHexRGBColor(context: Context, color: Int): String {
|
fun getHexRGBColor(context: Context, color: Int): String {
|
||||||
return context.getString(color).substring(3)
|
return context.getString(color).substring(3)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun showLicense(context: Context?, component: SoftwareComponent): Disposable {
|
|
||||||
return showLicense(context, component.license) {
|
|
||||||
setPositiveButton(R.string.dismiss) { dialog, _ ->
|
|
||||||
dialog.dismiss()
|
|
||||||
}
|
|
||||||
setNeutralButton(R.string.open_website_license) { _, _ ->
|
|
||||||
ShareUtils.openUrlInApp(context!!, component.link)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun showLicense(context: Context?, license: License) = showLicense(context, license) {
|
|
||||||
setPositiveButton(R.string.ok) { dialog, _ -> dialog.dismiss() }
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun showLicense(
|
|
||||||
context: Context?,
|
|
||||||
license: License,
|
|
||||||
block: AlertDialog.Builder.() -> AlertDialog.Builder
|
|
||||||
): Disposable {
|
|
||||||
return if (context == null) {
|
|
||||||
Disposable.empty()
|
|
||||||
} else {
|
|
||||||
Observable.fromCallable { getFormattedLicense(context, license) }
|
|
||||||
.subscribeOn(Schedulers.io())
|
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
|
||||||
.subscribe { formattedLicense ->
|
|
||||||
val webViewData =
|
|
||||||
Base64.encodeToString(formattedLicense.toByteArray(), Base64.NO_PADDING)
|
|
||||||
val webView = WebView(context)
|
|
||||||
webView.loadData(webViewData, "text/html; charset=UTF-8", "base64")
|
|
||||||
|
|
||||||
Localization.assureCorrectAppLanguage(context)
|
|
||||||
AlertDialog.Builder(context)
|
|
||||||
.setTitle(license.name)
|
|
||||||
.setView(webView)
|
|
||||||
.block()
|
|
||||||
.show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -2,6 +2,7 @@ package org.schabi.newpipe.about
|
||||||
|
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
|
import java.io.Serializable
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
class SoftwareComponent
|
class SoftwareComponent
|
||||||
|
@ -13,4 +14,4 @@ constructor(
|
||||||
val link: String,
|
val link: String,
|
||||||
val license: License,
|
val license: License,
|
||||||
val version: String? = null
|
val version: String? = null
|
||||||
) : Parcelable
|
) : Parcelable, Serializable
|
||||||
|
|
|
@ -7,7 +7,7 @@ import java.time.Instant
|
||||||
import java.time.OffsetDateTime
|
import java.time.OffsetDateTime
|
||||||
import java.time.ZoneOffset
|
import java.time.ZoneOffset
|
||||||
|
|
||||||
object Converters {
|
class Converters {
|
||||||
/**
|
/**
|
||||||
* Convert a long value to a [OffsetDateTime].
|
* Convert a long value to a [OffsetDateTime].
|
||||||
*
|
*
|
||||||
|
@ -47,6 +47,6 @@ object Converters {
|
||||||
|
|
||||||
@TypeConverter
|
@TypeConverter
|
||||||
fun feedGroupIconOf(id: Int): FeedGroupIcon {
|
fun feedGroupIconOf(id: Int): FeedGroupIcon {
|
||||||
return FeedGroupIcon.values().first { it.id == id }
|
return FeedGroupIcon.entries.first { it.id == id }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -93,18 +93,30 @@ abstract class FeedDAO {
|
||||||
uploadDateBefore: OffsetDateTime?
|
uploadDateBefore: OffsetDateTime?
|
||||||
): Maybe<List<StreamWithState>>
|
): Maybe<List<StreamWithState>>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove links to streams that are older than the given date
|
||||||
|
* **but keep at least one stream per uploader**.
|
||||||
|
*
|
||||||
|
* One stream per uploader is kept because it is needed as reference
|
||||||
|
* when fetching new streams to check if they are new or not.
|
||||||
|
* @param offsetDateTime the newest date to keep, older streams are removed
|
||||||
|
*/
|
||||||
@Query(
|
@Query(
|
||||||
"""
|
"""
|
||||||
DELETE FROM feed WHERE
|
DELETE FROM feed
|
||||||
|
WHERE feed.stream_id IN (SELECT uid from (
|
||||||
|
SELECT s.uid,
|
||||||
|
(SELECT MAX(upload_date)
|
||||||
|
FROM streams s1
|
||||||
|
INNER JOIN feed f1
|
||||||
|
ON s1.uid = f1.stream_id
|
||||||
|
WHERE f1.subscription_id = f.subscription_id) max_upload_date
|
||||||
|
FROM streams s
|
||||||
|
INNER JOIN feed f
|
||||||
|
ON s.uid = f.stream_id
|
||||||
|
|
||||||
feed.stream_id IN (
|
WHERE s.upload_date < :offsetDateTime
|
||||||
SELECT s.uid FROM streams s
|
AND s.upload_date <> max_upload_date))
|
||||||
|
|
||||||
INNER JOIN feed f
|
|
||||||
ON s.uid = f.stream_id
|
|
||||||
|
|
||||||
WHERE s.upload_date < :offsetDateTime
|
|
||||||
)
|
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
abstract fun unlinkStreamsOlderThan(offsetDateTime: OffsetDateTime)
|
abstract fun unlinkStreamsOlderThan(offsetDateTime: OffsetDateTime)
|
||||||
|
|
|
@ -7,6 +7,7 @@ import org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity
|
||||||
import org.schabi.newpipe.database.stream.model.StreamEntity
|
import org.schabi.newpipe.database.stream.model.StreamEntity
|
||||||
import org.schabi.newpipe.database.stream.model.StreamStateEntity
|
import org.schabi.newpipe.database.stream.model.StreamStateEntity
|
||||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem
|
import org.schabi.newpipe.extractor.stream.StreamInfoItem
|
||||||
|
import org.schabi.newpipe.util.image.ImageStrategy
|
||||||
|
|
||||||
data class PlaylistStreamEntry(
|
data class PlaylistStreamEntry(
|
||||||
@Embedded
|
@Embedded
|
||||||
|
@ -28,7 +29,7 @@ data class PlaylistStreamEntry(
|
||||||
item.duration = streamEntity.duration
|
item.duration = streamEntity.duration
|
||||||
item.uploaderName = streamEntity.uploader
|
item.uploaderName = streamEntity.uploader
|
||||||
item.uploaderUrl = streamEntity.uploaderUrl
|
item.uploaderUrl = streamEntity.uploaderUrl
|
||||||
item.thumbnailUrl = streamEntity.thumbnailUrl
|
item.thumbnails = ImageStrategy.dbUrlToImageList(streamEntity.thumbnailUrl)
|
||||||
|
|
||||||
return item
|
return item
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,6 +11,7 @@ import androidx.room.PrimaryKey;
|
||||||
import org.schabi.newpipe.database.playlist.PlaylistLocalItem;
|
import org.schabi.newpipe.database.playlist.PlaylistLocalItem;
|
||||||
import org.schabi.newpipe.extractor.playlist.PlaylistInfo;
|
import org.schabi.newpipe.extractor.playlist.PlaylistInfo;
|
||||||
import org.schabi.newpipe.util.Constants;
|
import org.schabi.newpipe.util.Constants;
|
||||||
|
import org.schabi.newpipe.util.image.ImageStrategy;
|
||||||
|
|
||||||
import static org.schabi.newpipe.database.LocalItem.LocalItemType.PLAYLIST_REMOTE_ITEM;
|
import static org.schabi.newpipe.database.LocalItem.LocalItemType.PLAYLIST_REMOTE_ITEM;
|
||||||
import static org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity.REMOTE_PLAYLIST_NAME;
|
import static org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity.REMOTE_PLAYLIST_NAME;
|
||||||
|
@ -69,8 +70,9 @@ public class PlaylistRemoteEntity implements PlaylistLocalItem {
|
||||||
@Ignore
|
@Ignore
|
||||||
public PlaylistRemoteEntity(final PlaylistInfo info) {
|
public PlaylistRemoteEntity(final PlaylistInfo info) {
|
||||||
this(info.getServiceId(), info.getName(), info.getUrl(),
|
this(info.getServiceId(), info.getName(), info.getUrl(),
|
||||||
info.getThumbnailUrl() == null
|
// use uploader avatar when no thumbnail is available
|
||||||
? info.getUploaderAvatarUrl() : info.getThumbnailUrl(),
|
ImageStrategy.imageListToDbUrl(info.getThumbnails().isEmpty()
|
||||||
|
? info.getUploaderAvatars() : info.getThumbnails()),
|
||||||
info.getUploaderName(), info.getStreamCount());
|
info.getUploaderName(), info.getStreamCount());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -84,7 +86,10 @@ public class PlaylistRemoteEntity implements PlaylistLocalItem {
|
||||||
&& getStreamCount() == info.getStreamCount()
|
&& getStreamCount() == info.getStreamCount()
|
||||||
&& TextUtils.equals(getName(), info.getName())
|
&& TextUtils.equals(getName(), info.getName())
|
||||||
&& TextUtils.equals(getUrl(), info.getUrl())
|
&& TextUtils.equals(getUrl(), info.getUrl())
|
||||||
&& TextUtils.equals(getThumbnailUrl(), info.getThumbnailUrl())
|
// we want to update the local playlist data even when either the remote thumbnail
|
||||||
|
// URL changes, or the preferred image quality setting is changed by the user
|
||||||
|
&& TextUtils.equals(getThumbnailUrl(),
|
||||||
|
ImageStrategy.imageListToDbUrl(info.getThumbnails()))
|
||||||
&& TextUtils.equals(getUploader(), info.getUploaderName());
|
&& TextUtils.equals(getUploader(), info.getUploaderName());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -7,6 +7,7 @@ import org.schabi.newpipe.database.history.model.StreamHistoryEntity
|
||||||
import org.schabi.newpipe.database.stream.model.StreamEntity
|
import org.schabi.newpipe.database.stream.model.StreamEntity
|
||||||
import org.schabi.newpipe.database.stream.model.StreamStateEntity.STREAM_PROGRESS_MILLIS
|
import org.schabi.newpipe.database.stream.model.StreamStateEntity.STREAM_PROGRESS_MILLIS
|
||||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem
|
import org.schabi.newpipe.extractor.stream.StreamInfoItem
|
||||||
|
import org.schabi.newpipe.util.image.ImageStrategy
|
||||||
import java.time.OffsetDateTime
|
import java.time.OffsetDateTime
|
||||||
|
|
||||||
class StreamStatisticsEntry(
|
class StreamStatisticsEntry(
|
||||||
|
@ -30,7 +31,7 @@ class StreamStatisticsEntry(
|
||||||
item.duration = streamEntity.duration
|
item.duration = streamEntity.duration
|
||||||
item.uploaderName = streamEntity.uploader
|
item.uploaderName = streamEntity.uploader
|
||||||
item.uploaderUrl = streamEntity.uploaderUrl
|
item.uploaderUrl = streamEntity.uploaderUrl
|
||||||
item.thumbnailUrl = streamEntity.thumbnailUrl
|
item.thumbnails = ImageStrategy.dbUrlToImageList(streamEntity.thumbnailUrl)
|
||||||
|
|
||||||
return item
|
return item
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,6 +13,7 @@ import org.schabi.newpipe.extractor.stream.StreamInfo
|
||||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem
|
import org.schabi.newpipe.extractor.stream.StreamInfoItem
|
||||||
import org.schabi.newpipe.extractor.stream.StreamType
|
import org.schabi.newpipe.extractor.stream.StreamType
|
||||||
import org.schabi.newpipe.player.playqueue.PlayQueueItem
|
import org.schabi.newpipe.player.playqueue.PlayQueueItem
|
||||||
|
import org.schabi.newpipe.util.image.ImageStrategy
|
||||||
import java.io.Serializable
|
import java.io.Serializable
|
||||||
import java.time.OffsetDateTime
|
import java.time.OffsetDateTime
|
||||||
|
|
||||||
|
@ -67,7 +68,8 @@ data class StreamEntity(
|
||||||
constructor(item: StreamInfoItem) : this(
|
constructor(item: StreamInfoItem) : this(
|
||||||
serviceId = item.serviceId, url = item.url, title = item.name,
|
serviceId = item.serviceId, url = item.url, title = item.name,
|
||||||
streamType = item.streamType, duration = item.duration, uploader = item.uploaderName,
|
streamType = item.streamType, duration = item.duration, uploader = item.uploaderName,
|
||||||
uploaderUrl = item.uploaderUrl, thumbnailUrl = item.thumbnailUrl, viewCount = item.viewCount,
|
uploaderUrl = item.uploaderUrl,
|
||||||
|
thumbnailUrl = ImageStrategy.imageListToDbUrl(item.thumbnails), viewCount = item.viewCount,
|
||||||
textualUploadDate = item.textualUploadDate, uploadDate = item.uploadDate?.offsetDateTime(),
|
textualUploadDate = item.textualUploadDate, uploadDate = item.uploadDate?.offsetDateTime(),
|
||||||
isUploadDateApproximation = item.uploadDate?.isApproximation
|
isUploadDateApproximation = item.uploadDate?.isApproximation
|
||||||
)
|
)
|
||||||
|
@ -76,7 +78,8 @@ data class StreamEntity(
|
||||||
constructor(info: StreamInfo) : this(
|
constructor(info: StreamInfo) : this(
|
||||||
serviceId = info.serviceId, url = info.url, title = info.name,
|
serviceId = info.serviceId, url = info.url, title = info.name,
|
||||||
streamType = info.streamType, duration = info.duration, uploader = info.uploaderName,
|
streamType = info.streamType, duration = info.duration, uploader = info.uploaderName,
|
||||||
uploaderUrl = info.uploaderUrl, thumbnailUrl = info.thumbnailUrl, viewCount = info.viewCount,
|
uploaderUrl = info.uploaderUrl,
|
||||||
|
thumbnailUrl = ImageStrategy.imageListToDbUrl(info.thumbnails), viewCount = info.viewCount,
|
||||||
textualUploadDate = info.textualUploadDate, uploadDate = info.uploadDate?.offsetDateTime(),
|
textualUploadDate = info.textualUploadDate, uploadDate = info.uploadDate?.offsetDateTime(),
|
||||||
isUploadDateApproximation = info.uploadDate?.isApproximation
|
isUploadDateApproximation = info.uploadDate?.isApproximation
|
||||||
)
|
)
|
||||||
|
@ -85,7 +88,8 @@ data class StreamEntity(
|
||||||
constructor(item: PlayQueueItem) : this(
|
constructor(item: PlayQueueItem) : this(
|
||||||
serviceId = item.serviceId, url = item.url, title = item.title,
|
serviceId = item.serviceId, url = item.url, title = item.title,
|
||||||
streamType = item.streamType, duration = item.duration, uploader = item.uploader,
|
streamType = item.streamType, duration = item.duration, uploader = item.uploader,
|
||||||
uploaderUrl = item.uploaderUrl, thumbnailUrl = item.thumbnailUrl
|
uploaderUrl = item.uploaderUrl,
|
||||||
|
thumbnailUrl = ImageStrategy.imageListToDbUrl(item.thumbnails)
|
||||||
)
|
)
|
||||||
|
|
||||||
fun toStreamInfoItem(): StreamInfoItem {
|
fun toStreamInfoItem(): StreamInfoItem {
|
||||||
|
@ -93,7 +97,7 @@ data class StreamEntity(
|
||||||
item.duration = duration
|
item.duration = duration
|
||||||
item.uploaderName = uploader
|
item.uploaderName = uploader
|
||||||
item.uploaderUrl = uploaderUrl
|
item.uploaderUrl = uploaderUrl
|
||||||
item.thumbnailUrl = thumbnailUrl
|
item.thumbnails = ImageStrategy.dbUrlToImageList(thumbnailUrl)
|
||||||
|
|
||||||
if (viewCount != null) item.viewCount = viewCount as Long
|
if (viewCount != null) item.viewCount = viewCount as Long
|
||||||
item.textualUploadDate = textualUploadDate
|
item.textualUploadDate = textualUploadDate
|
||||||
|
|
|
@ -10,6 +10,7 @@ import androidx.room.PrimaryKey;
|
||||||
import org.schabi.newpipe.extractor.channel.ChannelInfo;
|
import org.schabi.newpipe.extractor.channel.ChannelInfo;
|
||||||
import org.schabi.newpipe.extractor.channel.ChannelInfoItem;
|
import org.schabi.newpipe.extractor.channel.ChannelInfoItem;
|
||||||
import org.schabi.newpipe.util.Constants;
|
import org.schabi.newpipe.util.Constants;
|
||||||
|
import org.schabi.newpipe.util.image.ImageStrategy;
|
||||||
|
|
||||||
import static org.schabi.newpipe.database.subscription.SubscriptionEntity.SUBSCRIPTION_SERVICE_ID;
|
import static org.schabi.newpipe.database.subscription.SubscriptionEntity.SUBSCRIPTION_SERVICE_ID;
|
||||||
import static org.schabi.newpipe.database.subscription.SubscriptionEntity.SUBSCRIPTION_TABLE;
|
import static org.schabi.newpipe.database.subscription.SubscriptionEntity.SUBSCRIPTION_TABLE;
|
||||||
|
@ -57,8 +58,8 @@ public class SubscriptionEntity {
|
||||||
final SubscriptionEntity result = new SubscriptionEntity();
|
final SubscriptionEntity result = new SubscriptionEntity();
|
||||||
result.setServiceId(info.getServiceId());
|
result.setServiceId(info.getServiceId());
|
||||||
result.setUrl(info.getUrl());
|
result.setUrl(info.getUrl());
|
||||||
result.setData(info.getName(), info.getAvatarUrl(), info.getDescription(),
|
result.setData(info.getName(), ImageStrategy.imageListToDbUrl(info.getAvatars()),
|
||||||
info.getSubscriberCount());
|
info.getDescription(), info.getSubscriberCount());
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -138,7 +139,7 @@ public class SubscriptionEntity {
|
||||||
@Ignore
|
@Ignore
|
||||||
public ChannelInfoItem toChannelInfoItem() {
|
public ChannelInfoItem toChannelInfoItem() {
|
||||||
final ChannelInfoItem item = new ChannelInfoItem(getServiceId(), getUrl(), getName());
|
final ChannelInfoItem item = new ChannelInfoItem(getServiceId(), getUrl(), getName());
|
||||||
item.setThumbnailUrl(getAvatarUrl());
|
item.setThumbnails(ImageStrategy.dbUrlToImageList(getAvatarUrl()));
|
||||||
item.setSubscriberCount(getSubscriberCount());
|
item.setSubscriberCount(getSubscriberCount());
|
||||||
item.setDescription(getDescription());
|
item.setDescription(getDescription());
|
||||||
return item;
|
return item;
|
||||||
|
|
|
@ -69,7 +69,7 @@ import org.schabi.newpipe.util.SecondaryStreamHelper;
|
||||||
import org.schabi.newpipe.util.SimpleOnSeekBarChangeListener;
|
import org.schabi.newpipe.util.SimpleOnSeekBarChangeListener;
|
||||||
import org.schabi.newpipe.util.SponsorBlockUtils;
|
import org.schabi.newpipe.util.SponsorBlockUtils;
|
||||||
import org.schabi.newpipe.util.StreamItemAdapter;
|
import org.schabi.newpipe.util.StreamItemAdapter;
|
||||||
import org.schabi.newpipe.util.StreamItemAdapter.StreamSizeWrapper;
|
import org.schabi.newpipe.util.StreamItemAdapter.StreamInfoWrapper;
|
||||||
import org.schabi.newpipe.util.AudioTrackAdapter;
|
import org.schabi.newpipe.util.AudioTrackAdapter;
|
||||||
import org.schabi.newpipe.util.AudioTrackAdapter.AudioTracksWrapper;
|
import org.schabi.newpipe.util.AudioTrackAdapter.AudioTracksWrapper;
|
||||||
import org.schabi.newpipe.util.ThemeHelper;
|
import org.schabi.newpipe.util.ThemeHelper;
|
||||||
|
@ -77,6 +77,7 @@ import org.schabi.newpipe.util.VideoSegment;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
@ -103,9 +104,9 @@ public class DownloadDialog extends DialogFragment
|
||||||
@State
|
@State
|
||||||
StreamInfo currentInfo;
|
StreamInfo currentInfo;
|
||||||
@State
|
@State
|
||||||
StreamSizeWrapper<VideoStream> wrappedVideoStreams;
|
StreamInfoWrapper<VideoStream> wrappedVideoStreams;
|
||||||
@State
|
@State
|
||||||
StreamSizeWrapper<SubtitlesStream> wrappedSubtitleStreams;
|
StreamInfoWrapper<SubtitlesStream> wrappedSubtitleStreams;
|
||||||
@State
|
@State
|
||||||
AudioTracksWrapper wrappedAudioTracks;
|
AudioTracksWrapper wrappedAudioTracks;
|
||||||
@State
|
@State
|
||||||
|
@ -195,8 +196,8 @@ public class DownloadDialog extends DialogFragment
|
||||||
wrappedAudioTracks.size() > 1
|
wrappedAudioTracks.size() > 1
|
||||||
);
|
);
|
||||||
|
|
||||||
this.wrappedVideoStreams = new StreamSizeWrapper<>(videoStreams, context);
|
this.wrappedVideoStreams = new StreamInfoWrapper<>(videoStreams, context);
|
||||||
this.wrappedSubtitleStreams = new StreamSizeWrapper<>(
|
this.wrappedSubtitleStreams = new StreamInfoWrapper<>(
|
||||||
getStreamsOfSpecifiedDelivery(info.getSubtitles(), PROGRESSIVE_HTTP), context);
|
getStreamsOfSpecifiedDelivery(info.getSubtitles(), PROGRESSIVE_HTTP), context);
|
||||||
|
|
||||||
this.selectedVideoIndex = ListHelper.getDefaultResolutionIndex(context, videoStreams);
|
this.selectedVideoIndex = ListHelper.getDefaultResolutionIndex(context, videoStreams);
|
||||||
|
@ -270,17 +271,17 @@ public class DownloadDialog extends DialogFragment
|
||||||
* Update the displayed video streams based on the selected audio track.
|
* Update the displayed video streams based on the selected audio track.
|
||||||
*/
|
*/
|
||||||
private void updateSecondaryStreams() {
|
private void updateSecondaryStreams() {
|
||||||
final StreamSizeWrapper<AudioStream> audioStreams = getWrappedAudioStreams();
|
final StreamInfoWrapper<AudioStream> audioStreams = getWrappedAudioStreams();
|
||||||
final var secondaryStreams = new SparseArrayCompat<SecondaryStreamHelper<AudioStream>>(4);
|
final var secondaryStreams = new SparseArrayCompat<SecondaryStreamHelper<AudioStream>>(4);
|
||||||
final List<VideoStream> videoStreams = wrappedVideoStreams.getStreamsList();
|
final List<VideoStream> videoStreams = wrappedVideoStreams.getStreamsList();
|
||||||
wrappedVideoStreams.resetSizes();
|
wrappedVideoStreams.resetInfo();
|
||||||
|
|
||||||
for (int i = 0; i < videoStreams.size(); i++) {
|
for (int i = 0; i < videoStreams.size(); i++) {
|
||||||
if (!videoStreams.get(i).isVideoOnly()) {
|
if (!videoStreams.get(i).isVideoOnly()) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
final AudioStream audioStream = SecondaryStreamHelper
|
final AudioStream audioStream = SecondaryStreamHelper.getAudioStreamFor(
|
||||||
.getAudioStreamFor(audioStreams.getStreamsList(), videoStreams.get(i));
|
context, audioStreams.getStreamsList(), videoStreams.get(i));
|
||||||
|
|
||||||
if (audioStream != null) {
|
if (audioStream != null) {
|
||||||
secondaryStreams.append(i, new SecondaryStreamHelper<>(audioStreams, audioStream));
|
secondaryStreams.append(i, new SecondaryStreamHelper<>(audioStreams, audioStream));
|
||||||
|
@ -410,7 +411,7 @@ public class DownloadDialog extends DialogFragment
|
||||||
|
|
||||||
private void fetchStreamsSize() {
|
private void fetchStreamsSize() {
|
||||||
disposables.clear();
|
disposables.clear();
|
||||||
disposables.add(StreamSizeWrapper.fetchSizeForWrapper(wrappedVideoStreams)
|
disposables.add(StreamInfoWrapper.fetchMoreInfoForWrapper(wrappedVideoStreams)
|
||||||
.subscribe(result -> {
|
.subscribe(result -> {
|
||||||
if (dialogBinding.videoAudioGroup.getCheckedRadioButtonId()
|
if (dialogBinding.videoAudioGroup.getCheckedRadioButtonId()
|
||||||
== R.id.video_button) {
|
== R.id.video_button) {
|
||||||
|
@ -420,7 +421,7 @@ public class DownloadDialog extends DialogFragment
|
||||||
new ErrorInfo(throwable, UserAction.DOWNLOAD_OPEN_DIALOG,
|
new ErrorInfo(throwable, UserAction.DOWNLOAD_OPEN_DIALOG,
|
||||||
"Downloading video stream size",
|
"Downloading video stream size",
|
||||||
currentInfo.getServiceId()))));
|
currentInfo.getServiceId()))));
|
||||||
disposables.add(StreamSizeWrapper.fetchSizeForWrapper(getWrappedAudioStreams())
|
disposables.add(StreamInfoWrapper.fetchMoreInfoForWrapper(getWrappedAudioStreams())
|
||||||
.subscribe(result -> {
|
.subscribe(result -> {
|
||||||
if (dialogBinding.videoAudioGroup.getCheckedRadioButtonId()
|
if (dialogBinding.videoAudioGroup.getCheckedRadioButtonId()
|
||||||
== R.id.audio_button) {
|
== R.id.audio_button) {
|
||||||
|
@ -430,7 +431,7 @@ public class DownloadDialog extends DialogFragment
|
||||||
new ErrorInfo(throwable, UserAction.DOWNLOAD_OPEN_DIALOG,
|
new ErrorInfo(throwable, UserAction.DOWNLOAD_OPEN_DIALOG,
|
||||||
"Downloading audio stream size",
|
"Downloading audio stream size",
|
||||||
currentInfo.getServiceId()))));
|
currentInfo.getServiceId()))));
|
||||||
disposables.add(StreamSizeWrapper.fetchSizeForWrapper(wrappedSubtitleStreams)
|
disposables.add(StreamInfoWrapper.fetchMoreInfoForWrapper(wrappedSubtitleStreams)
|
||||||
.subscribe(result -> {
|
.subscribe(result -> {
|
||||||
if (dialogBinding.videoAudioGroup.getCheckedRadioButtonId()
|
if (dialogBinding.videoAudioGroup.getCheckedRadioButtonId()
|
||||||
== R.id.subtitle_button) {
|
== R.id.subtitle_button) {
|
||||||
|
@ -738,9 +739,9 @@ public class DownloadDialog extends DialogFragment
|
||||||
dialogBinding.subtitleButton.setEnabled(enabled);
|
dialogBinding.subtitleButton.setEnabled(enabled);
|
||||||
}
|
}
|
||||||
|
|
||||||
private StreamSizeWrapper<AudioStream> getWrappedAudioStreams() {
|
private StreamInfoWrapper<AudioStream> getWrappedAudioStreams() {
|
||||||
if (selectedAudioTrackIndex < 0 || selectedAudioTrackIndex > wrappedAudioTracks.size()) {
|
if (selectedAudioTrackIndex < 0 || selectedAudioTrackIndex > wrappedAudioTracks.size()) {
|
||||||
return StreamSizeWrapper.empty();
|
return StreamInfoWrapper.empty();
|
||||||
}
|
}
|
||||||
return wrappedAudioTracks.getTracksList().get(selectedAudioTrackIndex);
|
return wrappedAudioTracks.getTracksList().get(selectedAudioTrackIndex);
|
||||||
}
|
}
|
||||||
|
@ -780,7 +781,7 @@ public class DownloadDialog extends DialogFragment
|
||||||
}
|
}
|
||||||
|
|
||||||
private void showFailedDialog(@StringRes final int msg) {
|
private void showFailedDialog(@StringRes final int msg) {
|
||||||
assureCorrectAppLanguage(getContext());
|
assureCorrectAppLanguage(requireContext());
|
||||||
new AlertDialog.Builder(context)
|
new AlertDialog.Builder(context)
|
||||||
.setTitle(R.string.general_error)
|
.setTitle(R.string.general_error)
|
||||||
.setMessage(msg)
|
.setMessage(msg)
|
||||||
|
@ -813,7 +814,7 @@ public class DownloadDialog extends DialogFragment
|
||||||
filenameTmp += "opus";
|
filenameTmp += "opus";
|
||||||
} else if (format != null) {
|
} else if (format != null) {
|
||||||
mimeTmp = format.mimeType;
|
mimeTmp = format.mimeType;
|
||||||
filenameTmp += format.suffix;
|
filenameTmp += format.getSuffix();
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case R.id.video_button:
|
case R.id.video_button:
|
||||||
|
@ -822,7 +823,7 @@ public class DownloadDialog extends DialogFragment
|
||||||
format = videoStreamsAdapter.getItem(selectedVideoIndex).getFormat();
|
format = videoStreamsAdapter.getItem(selectedVideoIndex).getFormat();
|
||||||
if (format != null) {
|
if (format != null) {
|
||||||
mimeTmp = format.mimeType;
|
mimeTmp = format.mimeType;
|
||||||
filenameTmp += format.suffix;
|
filenameTmp += format.getSuffix();
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case R.id.subtitle_button:
|
case R.id.subtitle_button:
|
||||||
|
@ -834,9 +835,9 @@ public class DownloadDialog extends DialogFragment
|
||||||
}
|
}
|
||||||
|
|
||||||
if (format == MediaFormat.TTML) {
|
if (format == MediaFormat.TTML) {
|
||||||
filenameTmp += MediaFormat.SRT.suffix;
|
filenameTmp += MediaFormat.SRT.getSuffix();
|
||||||
} else if (format != null) {
|
} else if (format != null) {
|
||||||
filenameTmp += format.suffix;
|
filenameTmp += format.getSuffix();
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
|
@ -1066,7 +1067,7 @@ public class DownloadDialog extends DialogFragment
|
||||||
final char kind;
|
final char kind;
|
||||||
int threads = dialogBinding.threads.getProgress() + 1;
|
int threads = dialogBinding.threads.getProgress() + 1;
|
||||||
final String[] urls;
|
final String[] urls;
|
||||||
final MissionRecoveryInfo[] recoveryInfo;
|
final List<MissionRecoveryInfo> recoveryInfo;
|
||||||
String psName = null;
|
String psName = null;
|
||||||
String[] psArgs = null;
|
String[] psArgs = null;
|
||||||
long nearLength = 0;
|
long nearLength = 0;
|
||||||
|
@ -1131,9 +1132,7 @@ public class DownloadDialog extends DialogFragment
|
||||||
urls = new String[] {
|
urls = new String[] {
|
||||||
selectedStream.getContent()
|
selectedStream.getContent()
|
||||||
};
|
};
|
||||||
recoveryInfo = new MissionRecoveryInfo[] {
|
recoveryInfo = List.of(new MissionRecoveryInfo(selectedStream));
|
||||||
new MissionRecoveryInfo(selectedStream)
|
|
||||||
};
|
|
||||||
} else {
|
} else {
|
||||||
if (secondaryStream.getDeliveryMethod() != PROGRESSIVE_HTTP) {
|
if (secondaryStream.getDeliveryMethod() != PROGRESSIVE_HTTP) {
|
||||||
throw new IllegalArgumentException("Unsupported stream delivery format"
|
throw new IllegalArgumentException("Unsupported stream delivery format"
|
||||||
|
@ -1143,13 +1142,14 @@ public class DownloadDialog extends DialogFragment
|
||||||
urls = new String[] {
|
urls = new String[] {
|
||||||
selectedStream.getContent(), secondaryStream.getContent()
|
selectedStream.getContent(), secondaryStream.getContent()
|
||||||
};
|
};
|
||||||
recoveryInfo = new MissionRecoveryInfo[] {new MissionRecoveryInfo(selectedStream),
|
recoveryInfo = List.of(
|
||||||
new MissionRecoveryInfo(secondaryStream)};
|
new MissionRecoveryInfo(selectedStream),
|
||||||
|
new MissionRecoveryInfo(secondaryStream)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
DownloadManagerService.startMission(context, urls, storage, kind, threads,
|
DownloadManagerService.startMission(context, urls, storage, kind, threads,
|
||||||
currentInfo.getUrl(), psName, psArgs, nearLength, recoveryInfo,
|
currentInfo.getUrl(), psName, psArgs, nearLength, new ArrayList<>(recoveryInfo), segments);
|
||||||
segments);
|
|
||||||
|
|
||||||
Toast.makeText(context, getString(R.string.download_has_started),
|
Toast.makeText(context, getString(R.string.download_has_started),
|
||||||
Toast.LENGTH_SHORT).show();
|
Toast.LENGTH_SHORT).show();
|
||||||
|
|
|
@ -0,0 +1,87 @@
|
||||||
|
package org.schabi.newpipe.download;
|
||||||
|
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.util.Log;
|
||||||
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.annotation.StringRes;
|
||||||
|
import androidx.appcompat.widget.Toolbar;
|
||||||
|
import androidx.fragment.app.DialogFragment;
|
||||||
|
|
||||||
|
import org.schabi.newpipe.MainActivity;
|
||||||
|
import org.schabi.newpipe.R;
|
||||||
|
import org.schabi.newpipe.databinding.DownloadLoadingDialogBinding;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class contains a dialog which shows a loading indicator and has a customizable title.
|
||||||
|
*/
|
||||||
|
public class LoadingDialog extends DialogFragment {
|
||||||
|
private static final String TAG = "LoadingDialog";
|
||||||
|
private static final boolean DEBUG = MainActivity.DEBUG;
|
||||||
|
private DownloadLoadingDialogBinding dialogLoadingBinding;
|
||||||
|
private final @StringRes int title;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new LoadingDialog.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* The dialog contains a loading indicator and has a customizable title.
|
||||||
|
* <br/>
|
||||||
|
* Use {@code show()} to display the dialog to the user.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @param title an informative title shown in the dialog's toolbar
|
||||||
|
*/
|
||||||
|
public LoadingDialog(final @StringRes int title) {
|
||||||
|
this.title = title;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreate(@Nullable final Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
if (DEBUG) {
|
||||||
|
Log.d(TAG, "onCreate() called with: "
|
||||||
|
+ "savedInstanceState = [" + savedInstanceState + "]");
|
||||||
|
}
|
||||||
|
this.setCancelable(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public View onCreateView(
|
||||||
|
@NonNull final LayoutInflater inflater,
|
||||||
|
final ViewGroup container,
|
||||||
|
final Bundle savedInstanceState) {
|
||||||
|
if (DEBUG) {
|
||||||
|
Log.d(TAG, "onCreateView() called with: "
|
||||||
|
+ "inflater = [" + inflater + "], container = [" + container + "], "
|
||||||
|
+ "savedInstanceState = [" + savedInstanceState + "]");
|
||||||
|
}
|
||||||
|
return inflater.inflate(R.layout.download_loading_dialog, container);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onViewCreated(@NonNull final View view, @Nullable final Bundle savedInstanceState) {
|
||||||
|
super.onViewCreated(view, savedInstanceState);
|
||||||
|
dialogLoadingBinding = DownloadLoadingDialogBinding.bind(view);
|
||||||
|
initToolbar(dialogLoadingBinding.toolbarLayout.toolbar);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void initToolbar(final Toolbar toolbar) {
|
||||||
|
if (DEBUG) {
|
||||||
|
Log.d(TAG, "initToolbar() called with: toolbar = [" + toolbar + "]");
|
||||||
|
}
|
||||||
|
toolbar.setTitle(requireContext().getString(title));
|
||||||
|
toolbar.setNavigationOnClickListener(v -> dismiss());
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDestroyView() {
|
||||||
|
dialogLoadingBinding = null;
|
||||||
|
super.onDestroyView();
|
||||||
|
}
|
||||||
|
}
|
|
@ -17,6 +17,7 @@ import androidx.annotation.Nullable;
|
||||||
import androidx.appcompat.app.ActionBar;
|
import androidx.appcompat.app.ActionBar;
|
||||||
import androidx.appcompat.app.AlertDialog;
|
import androidx.appcompat.app.AlertDialog;
|
||||||
import androidx.appcompat.app.AppCompatActivity;
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
|
import androidx.core.content.IntentCompat;
|
||||||
|
|
||||||
import com.grack.nanojson.JsonWriter;
|
import com.grack.nanojson.JsonWriter;
|
||||||
|
|
||||||
|
@ -105,7 +106,7 @@ public class ErrorActivity extends AppCompatActivity {
|
||||||
actionBar.setDisplayShowTitleEnabled(true);
|
actionBar.setDisplayShowTitleEnabled(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
errorInfo = intent.getParcelableExtra(ERROR_INFO);
|
errorInfo = IntentCompat.getParcelableExtra(intent, ERROR_INFO, ErrorInfo.class);
|
||||||
|
|
||||||
// important add guru meditation
|
// important add guru meditation
|
||||||
addGuruMeditation();
|
addGuruMeditation();
|
||||||
|
|
|
@ -11,7 +11,6 @@ import org.schabi.newpipe.extractor.exceptions.AccountTerminatedException
|
||||||
import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException
|
import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException
|
||||||
import org.schabi.newpipe.extractor.exceptions.ContentNotSupportedException
|
import org.schabi.newpipe.extractor.exceptions.ContentNotSupportedException
|
||||||
import org.schabi.newpipe.extractor.exceptions.ExtractionException
|
import org.schabi.newpipe.extractor.exceptions.ExtractionException
|
||||||
import org.schabi.newpipe.extractor.services.youtube.extractors.YoutubeStreamExtractor.DeobfuscateException
|
|
||||||
import org.schabi.newpipe.ktx.isNetworkRelated
|
import org.schabi.newpipe.ktx.isNetworkRelated
|
||||||
import org.schabi.newpipe.util.ServiceHelper
|
import org.schabi.newpipe.util.ServiceHelper
|
||||||
|
|
||||||
|
@ -96,7 +95,6 @@ class ErrorInfo(
|
||||||
throwable is ContentNotAvailableException -> R.string.content_not_available
|
throwable is ContentNotAvailableException -> R.string.content_not_available
|
||||||
throwable != null && throwable.isNetworkRelated -> R.string.network_error
|
throwable != null && throwable.isNetworkRelated -> R.string.network_error
|
||||||
throwable is ContentNotSupportedException -> R.string.content_not_supported
|
throwable is ContentNotSupportedException -> R.string.content_not_supported
|
||||||
throwable is DeobfuscateException -> R.string.youtube_signature_deobfuscation_error
|
|
||||||
throwable is ExtractionException -> R.string.parsing_error
|
throwable is ExtractionException -> R.string.parsing_error
|
||||||
throwable is ExoPlaybackException -> {
|
throwable is ExoPlaybackException -> {
|
||||||
when (throwable.type) {
|
when (throwable.type) {
|
||||||
|
|
|
@ -19,6 +19,7 @@ public enum UserAction {
|
||||||
REQUESTED_PLAYLIST("requested playlist"),
|
REQUESTED_PLAYLIST("requested playlist"),
|
||||||
REQUESTED_KIOSK("requested kiosk"),
|
REQUESTED_KIOSK("requested kiosk"),
|
||||||
REQUESTED_COMMENTS("requested comments"),
|
REQUESTED_COMMENTS("requested comments"),
|
||||||
|
REQUESTED_COMMENT_REPLIES("requested comment replies"),
|
||||||
REQUESTED_FEED("requested feed"),
|
REQUESTED_FEED("requested feed"),
|
||||||
REQUESTED_BOOKMARK("bookmark"),
|
REQUESTED_BOOKMARK("bookmark"),
|
||||||
DELETE_FROM_HISTORY("delete from history"),
|
DELETE_FROM_HISTORY("delete from history"),
|
||||||
|
|
|
@ -1,12 +1,16 @@
|
||||||
package org.schabi.newpipe.fragments;
|
package org.schabi.newpipe.fragments;
|
||||||
|
|
||||||
|
import static org.schabi.newpipe.ktx.ViewUtils.animate;
|
||||||
|
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.widget.ProgressBar;
|
import android.widget.ProgressBar;
|
||||||
|
import android.widget.TextView;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.annotation.StringRes;
|
||||||
import androidx.fragment.app.Fragment;
|
import androidx.fragment.app.Fragment;
|
||||||
|
|
||||||
import org.schabi.newpipe.BaseFragment;
|
import org.schabi.newpipe.BaseFragment;
|
||||||
|
@ -20,15 +24,15 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
|
||||||
import icepick.State;
|
import icepick.State;
|
||||||
|
|
||||||
import static org.schabi.newpipe.ktx.ViewUtils.animate;
|
|
||||||
|
|
||||||
public abstract class BaseStateFragment<I> extends BaseFragment implements ViewContract<I> {
|
public abstract class BaseStateFragment<I> extends BaseFragment implements ViewContract<I> {
|
||||||
@State
|
@State
|
||||||
protected AtomicBoolean wasLoading = new AtomicBoolean();
|
protected AtomicBoolean wasLoading = new AtomicBoolean();
|
||||||
protected AtomicBoolean isLoading = new AtomicBoolean();
|
protected AtomicBoolean isLoading = new AtomicBoolean();
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
private View emptyStateView;
|
protected View emptyStateView;
|
||||||
|
@Nullable
|
||||||
|
protected TextView emptyStateMessageView;
|
||||||
@Nullable
|
@Nullable
|
||||||
private ProgressBar loadingProgressBar;
|
private ProgressBar loadingProgressBar;
|
||||||
|
|
||||||
|
@ -65,6 +69,7 @@ public abstract class BaseStateFragment<I> extends BaseFragment implements ViewC
|
||||||
protected void initViews(final View rootView, final Bundle savedInstanceState) {
|
protected void initViews(final View rootView, final Bundle savedInstanceState) {
|
||||||
super.initViews(rootView, savedInstanceState);
|
super.initViews(rootView, savedInstanceState);
|
||||||
emptyStateView = rootView.findViewById(R.id.empty_state_view);
|
emptyStateView = rootView.findViewById(R.id.empty_state_view);
|
||||||
|
emptyStateMessageView = rootView.findViewById(R.id.empty_state_message);
|
||||||
loadingProgressBar = rootView.findViewById(R.id.loading_progress_bar);
|
loadingProgressBar = rootView.findViewById(R.id.loading_progress_bar);
|
||||||
errorPanelHelper = new ErrorPanelHelper(this, rootView, this::onRetryButtonClicked);
|
errorPanelHelper = new ErrorPanelHelper(this, rootView, this::onRetryButtonClicked);
|
||||||
}
|
}
|
||||||
|
@ -75,6 +80,8 @@ public abstract class BaseStateFragment<I> extends BaseFragment implements ViewC
|
||||||
if (errorPanelHelper != null) {
|
if (errorPanelHelper != null) {
|
||||||
errorPanelHelper.dispose();
|
errorPanelHelper.dispose();
|
||||||
}
|
}
|
||||||
|
emptyStateView = null;
|
||||||
|
emptyStateMessageView = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void onRetryButtonClicked() {
|
protected void onRetryButtonClicked() {
|
||||||
|
@ -189,6 +196,12 @@ public abstract class BaseStateFragment<I> extends BaseFragment implements ViewC
|
||||||
errorPanelHelper.showTextError(errorString);
|
errorPanelHelper.showTextError(errorString);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected void setEmptyStateMessage(@StringRes final int text) {
|
||||||
|
if (emptyStateMessageView != null) {
|
||||||
|
emptyStateMessageView.setText(text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public final void hideErrorPanel() {
|
public final void hideErrorPanel() {
|
||||||
errorPanelHelper.hide();
|
errorPanelHelper.hide();
|
||||||
lastPanelError = null;
|
lastPanelError = null;
|
||||||
|
|
|
@ -38,6 +38,7 @@ import org.schabi.newpipe.R;
|
||||||
import org.schabi.newpipe.databinding.FragmentMainBinding;
|
import org.schabi.newpipe.databinding.FragmentMainBinding;
|
||||||
import org.schabi.newpipe.error.ErrorUtil;
|
import org.schabi.newpipe.error.ErrorUtil;
|
||||||
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||||
|
import org.schabi.newpipe.local.playlist.LocalPlaylistFragment;
|
||||||
import org.schabi.newpipe.settings.tabs.Tab;
|
import org.schabi.newpipe.settings.tabs.Tab;
|
||||||
import org.schabi.newpipe.settings.tabs.TabsManager;
|
import org.schabi.newpipe.settings.tabs.TabsManager;
|
||||||
import org.schabi.newpipe.util.NavigationHelper;
|
import org.schabi.newpipe.util.NavigationHelper;
|
||||||
|
@ -139,6 +140,12 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDestroyView() {
|
||||||
|
super.onDestroyView();
|
||||||
|
binding = null;
|
||||||
|
}
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
// Menu
|
// Menu
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
@ -187,7 +194,6 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.pager.setAdapter(null);
|
binding.pager.setAdapter(null);
|
||||||
binding.pager.setOffscreenPageLimit(tabsList.size());
|
|
||||||
binding.pager.setAdapter(pagerAdapter);
|
binding.pager.setAdapter(pagerAdapter);
|
||||||
|
|
||||||
updateTabsIconAndDescription();
|
updateTabsIconAndDescription();
|
||||||
|
@ -211,6 +217,12 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte
|
||||||
setTitle(tabsList.get(tabPosition).getTabName(requireContext()));
|
setTitle(tabsList.get(tabPosition).getTabName(requireContext()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void commitPlaylistTabs() {
|
||||||
|
pagerAdapter.getLocalPlaylistFragments()
|
||||||
|
.stream()
|
||||||
|
.forEach(LocalPlaylistFragment::commitChanges);
|
||||||
|
}
|
||||||
|
|
||||||
private void updateTabLayoutPosition() {
|
private void updateTabLayoutPosition() {
|
||||||
final ScrollableTabLayout tabLayout = binding.mainTabLayout;
|
final ScrollableTabLayout tabLayout = binding.mainTabLayout;
|
||||||
final ViewPager viewPager = binding.pager;
|
final ViewPager viewPager = binding.pager;
|
||||||
|
@ -262,10 +274,18 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte
|
||||||
updateTitleForTab(tab.getPosition());
|
updateTitleForTab(tab.getPosition());
|
||||||
}
|
}
|
||||||
|
|
||||||
private static final class SelectedTabsPagerAdapter
|
public static final class SelectedTabsPagerAdapter
|
||||||
extends FragmentStatePagerAdapterMenuWorkaround {
|
extends FragmentStatePagerAdapterMenuWorkaround {
|
||||||
private final Context context;
|
private final Context context;
|
||||||
private final List<Tab> internalTabsList;
|
private final List<Tab> internalTabsList;
|
||||||
|
/**
|
||||||
|
* Keep reference to LocalPlaylistFragments, because their data can be modified by the user
|
||||||
|
* during runtime and changes are not committed immediately. However, in some cases,
|
||||||
|
* the changes need to be committed immediately by calling
|
||||||
|
* {@link LocalPlaylistFragment#commitChanges()}.
|
||||||
|
* The fragments are removed when {@link LocalPlaylistFragment#onDestroy()} is called.
|
||||||
|
*/
|
||||||
|
private final List<LocalPlaylistFragment> localPlaylistFragments = new ArrayList<>();
|
||||||
|
|
||||||
private SelectedTabsPagerAdapter(final Context context,
|
private SelectedTabsPagerAdapter(final Context context,
|
||||||
final FragmentManager fragmentManager,
|
final FragmentManager fragmentManager,
|
||||||
|
@ -292,9 +312,17 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte
|
||||||
((BaseFragment) fragment).useAsFrontPage(true);
|
((BaseFragment) fragment).useAsFrontPage(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (fragment instanceof LocalPlaylistFragment) {
|
||||||
|
localPlaylistFragments.add((LocalPlaylistFragment) fragment);
|
||||||
|
}
|
||||||
|
|
||||||
return fragment;
|
return fragment;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public List<LocalPlaylistFragment> getLocalPlaylistFragments() {
|
||||||
|
return localPlaylistFragments;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getItemPosition(@NonNull final Object object) {
|
public int getItemPosition(@NonNull final Object object) {
|
||||||
// Causes adapter to reload all Fragments when
|
// Causes adapter to reload all Fragments when
|
||||||
|
|
|
@ -0,0 +1,285 @@
|
||||||
|
package org.schabi.newpipe.fragments.detail;
|
||||||
|
|
||||||
|
import static android.text.TextUtils.isEmpty;
|
||||||
|
import static org.schabi.newpipe.extractor.utils.Utils.isBlank;
|
||||||
|
import static org.schabi.newpipe.util.text.TextLinkifier.SET_LINK_MOVEMENT_METHOD;
|
||||||
|
|
||||||
|
import android.graphics.Typeface;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.text.SpannableStringBuilder;
|
||||||
|
import android.text.Spanned;
|
||||||
|
import android.text.method.LinkMovementMethod;
|
||||||
|
import android.text.style.ClickableSpan;
|
||||||
|
import android.text.style.StyleSpan;
|
||||||
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.widget.LinearLayout;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.annotation.StringRes;
|
||||||
|
import androidx.appcompat.widget.TooltipCompat;
|
||||||
|
import androidx.core.text.HtmlCompat;
|
||||||
|
|
||||||
|
import com.google.android.material.chip.Chip;
|
||||||
|
|
||||||
|
import org.schabi.newpipe.BaseFragment;
|
||||||
|
import org.schabi.newpipe.R;
|
||||||
|
import org.schabi.newpipe.databinding.FragmentDescriptionBinding;
|
||||||
|
import org.schabi.newpipe.databinding.ItemMetadataBinding;
|
||||||
|
import org.schabi.newpipe.databinding.ItemMetadataTagsBinding;
|
||||||
|
import org.schabi.newpipe.extractor.Image;
|
||||||
|
import org.schabi.newpipe.extractor.StreamingService;
|
||||||
|
import org.schabi.newpipe.extractor.stream.Description;
|
||||||
|
import org.schabi.newpipe.util.NavigationHelper;
|
||||||
|
import org.schabi.newpipe.util.external_communication.ShareUtils;
|
||||||
|
import org.schabi.newpipe.util.image.ImageStrategy;
|
||||||
|
import org.schabi.newpipe.util.text.TextLinkifier;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
||||||
|
|
||||||
|
public abstract class BaseDescriptionFragment extends BaseFragment {
|
||||||
|
private final CompositeDisposable descriptionDisposables = new CompositeDisposable();
|
||||||
|
protected FragmentDescriptionBinding binding;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public View onCreateView(@NonNull final LayoutInflater inflater,
|
||||||
|
@Nullable final ViewGroup container,
|
||||||
|
@Nullable final Bundle savedInstanceState) {
|
||||||
|
binding = FragmentDescriptionBinding.inflate(inflater, container, false);
|
||||||
|
setupDescription();
|
||||||
|
setupMetadata(inflater, binding.detailMetadataLayout);
|
||||||
|
addTagsMetadataItem(inflater, binding.detailMetadataLayout);
|
||||||
|
return binding.getRoot();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDestroy() {
|
||||||
|
descriptionDisposables.clear();
|
||||||
|
super.onDestroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the description to display.
|
||||||
|
* @return description object
|
||||||
|
*/
|
||||||
|
@Nullable
|
||||||
|
protected abstract Description getDescription();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the streaming service. Used for generating description links.
|
||||||
|
* @return streaming service
|
||||||
|
*/
|
||||||
|
@Nullable
|
||||||
|
protected abstract StreamingService getService();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the streaming service ID. Used for tag links.
|
||||||
|
* @return service ID
|
||||||
|
*/
|
||||||
|
protected abstract int getServiceId();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the URL of the described video or audio, used to generate description links.
|
||||||
|
* @return stream URL
|
||||||
|
*/
|
||||||
|
@Nullable
|
||||||
|
protected abstract String getStreamUrl();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the list of tags to display below the description.
|
||||||
|
* @return tag list
|
||||||
|
*/
|
||||||
|
@Nullable
|
||||||
|
public abstract List<String> getTags();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add additional metadata to display.
|
||||||
|
* @param inflater LayoutInflater
|
||||||
|
* @param layout detailMetadataLayout
|
||||||
|
*/
|
||||||
|
protected abstract void setupMetadata(LayoutInflater inflater, LinearLayout layout);
|
||||||
|
|
||||||
|
private void setupDescription() {
|
||||||
|
final Description description = getDescription();
|
||||||
|
if (description == null || isEmpty(description.getContent())
|
||||||
|
|| description == Description.EMPTY_DESCRIPTION) {
|
||||||
|
binding.detailDescriptionView.setVisibility(View.GONE);
|
||||||
|
binding.detailSelectDescriptionButton.setVisibility(View.GONE);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// start with disabled state. This also loads description content (!)
|
||||||
|
disableDescriptionSelection();
|
||||||
|
|
||||||
|
binding.detailSelectDescriptionButton.setOnClickListener(v -> {
|
||||||
|
if (binding.detailDescriptionNoteView.getVisibility() == View.VISIBLE) {
|
||||||
|
disableDescriptionSelection();
|
||||||
|
} else {
|
||||||
|
// enable selection only when button is clicked to prevent flickering
|
||||||
|
enableDescriptionSelection();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void enableDescriptionSelection() {
|
||||||
|
binding.detailDescriptionNoteView.setVisibility(View.VISIBLE);
|
||||||
|
binding.detailDescriptionView.setTextIsSelectable(true);
|
||||||
|
|
||||||
|
final String buttonLabel = getString(R.string.description_select_disable);
|
||||||
|
binding.detailSelectDescriptionButton.setContentDescription(buttonLabel);
|
||||||
|
TooltipCompat.setTooltipText(binding.detailSelectDescriptionButton, buttonLabel);
|
||||||
|
binding.detailSelectDescriptionButton.setImageResource(R.drawable.ic_close);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void disableDescriptionSelection() {
|
||||||
|
// show description content again, otherwise some links are not clickable
|
||||||
|
final Description description = getDescription();
|
||||||
|
if (description != null) {
|
||||||
|
TextLinkifier.fromDescription(binding.detailDescriptionView,
|
||||||
|
description, HtmlCompat.FROM_HTML_MODE_LEGACY,
|
||||||
|
getService(), getStreamUrl(),
|
||||||
|
descriptionDisposables, SET_LINK_MOVEMENT_METHOD);
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.detailDescriptionNoteView.setVisibility(View.GONE);
|
||||||
|
binding.detailDescriptionView.setTextIsSelectable(false);
|
||||||
|
|
||||||
|
final String buttonLabel = getString(R.string.description_select_enable);
|
||||||
|
binding.detailSelectDescriptionButton.setContentDescription(buttonLabel);
|
||||||
|
TooltipCompat.setTooltipText(binding.detailSelectDescriptionButton, buttonLabel);
|
||||||
|
binding.detailSelectDescriptionButton.setImageResource(R.drawable.ic_select_all);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void addMetadataItem(final LayoutInflater inflater,
|
||||||
|
final LinearLayout layout,
|
||||||
|
final boolean linkifyContent,
|
||||||
|
@StringRes final int type,
|
||||||
|
@Nullable final String content) {
|
||||||
|
if (isBlank(content)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final ItemMetadataBinding itemBinding =
|
||||||
|
ItemMetadataBinding.inflate(inflater, layout, false);
|
||||||
|
|
||||||
|
itemBinding.metadataTypeView.setText(type);
|
||||||
|
itemBinding.metadataTypeView.setOnLongClickListener(v -> {
|
||||||
|
ShareUtils.copyToClipboard(requireContext(), content);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (linkifyContent) {
|
||||||
|
TextLinkifier.fromPlainText(itemBinding.metadataContentView, content, null, null,
|
||||||
|
descriptionDisposables, SET_LINK_MOVEMENT_METHOD);
|
||||||
|
} else {
|
||||||
|
itemBinding.metadataContentView.setText(content);
|
||||||
|
}
|
||||||
|
|
||||||
|
itemBinding.metadataContentView.setClickable(true);
|
||||||
|
|
||||||
|
layout.addView(itemBinding.getRoot());
|
||||||
|
}
|
||||||
|
|
||||||
|
private String imageSizeToText(final int heightOrWidth) {
|
||||||
|
if (heightOrWidth < 0) {
|
||||||
|
return getString(R.string.question_mark);
|
||||||
|
} else {
|
||||||
|
return String.valueOf(heightOrWidth);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void addImagesMetadataItem(final LayoutInflater inflater,
|
||||||
|
final LinearLayout layout,
|
||||||
|
@StringRes final int type,
|
||||||
|
final List<Image> images) {
|
||||||
|
final String preferredImageUrl = ImageStrategy.choosePreferredImage(images);
|
||||||
|
if (preferredImageUrl == null) {
|
||||||
|
return; // null will be returned in case there is no image
|
||||||
|
}
|
||||||
|
|
||||||
|
final ItemMetadataBinding itemBinding =
|
||||||
|
ItemMetadataBinding.inflate(inflater, layout, false);
|
||||||
|
itemBinding.metadataTypeView.setText(type);
|
||||||
|
|
||||||
|
final SpannableStringBuilder urls = new SpannableStringBuilder();
|
||||||
|
for (final Image image : images) {
|
||||||
|
if (urls.length() != 0) {
|
||||||
|
urls.append(", ");
|
||||||
|
}
|
||||||
|
final int entryBegin = urls.length();
|
||||||
|
|
||||||
|
if (image.getHeight() != Image.HEIGHT_UNKNOWN
|
||||||
|
|| image.getWidth() != Image.WIDTH_UNKNOWN
|
||||||
|
// if even the resolution level is unknown, ?x? will be shown
|
||||||
|
|| image.getEstimatedResolutionLevel() == Image.ResolutionLevel.UNKNOWN) {
|
||||||
|
urls.append(imageSizeToText(image.getHeight()));
|
||||||
|
urls.append('x');
|
||||||
|
urls.append(imageSizeToText(image.getWidth()));
|
||||||
|
} else {
|
||||||
|
switch (image.getEstimatedResolutionLevel()) {
|
||||||
|
case LOW:
|
||||||
|
urls.append(getString(R.string.image_quality_low));
|
||||||
|
break;
|
||||||
|
default: // unreachable, Image.ResolutionLevel.UNKNOWN is already filtered out
|
||||||
|
case MEDIUM:
|
||||||
|
urls.append(getString(R.string.image_quality_medium));
|
||||||
|
break;
|
||||||
|
case HIGH:
|
||||||
|
urls.append(getString(R.string.image_quality_high));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
urls.setSpan(new ClickableSpan() {
|
||||||
|
@Override
|
||||||
|
public void onClick(@NonNull final View widget) {
|
||||||
|
ShareUtils.openUrlInBrowser(requireContext(), image.getUrl());
|
||||||
|
}
|
||||||
|
}, entryBegin, urls.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||||
|
|
||||||
|
if (preferredImageUrl.equals(image.getUrl())) {
|
||||||
|
urls.setSpan(new StyleSpan(Typeface.BOLD), entryBegin, urls.length(),
|
||||||
|
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
itemBinding.metadataContentView.setText(urls);
|
||||||
|
itemBinding.metadataContentView.setMovementMethod(LinkMovementMethod.getInstance());
|
||||||
|
layout.addView(itemBinding.getRoot());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addTagsMetadataItem(final LayoutInflater inflater, final LinearLayout layout) {
|
||||||
|
final List<String> tags = getTags();
|
||||||
|
|
||||||
|
if (tags != null && !tags.isEmpty()) {
|
||||||
|
final var itemBinding = ItemMetadataTagsBinding.inflate(inflater, layout, false);
|
||||||
|
|
||||||
|
tags.stream().sorted(String.CASE_INSENSITIVE_ORDER).forEach(tag -> {
|
||||||
|
final Chip chip = (Chip) inflater.inflate(R.layout.chip,
|
||||||
|
itemBinding.metadataTagsChips, false);
|
||||||
|
chip.setText(tag);
|
||||||
|
chip.setOnClickListener(this::onTagClick);
|
||||||
|
chip.setOnLongClickListener(this::onTagLongClick);
|
||||||
|
itemBinding.metadataTagsChips.addView(chip);
|
||||||
|
});
|
||||||
|
|
||||||
|
layout.addView(itemBinding.getRoot());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onTagClick(final View chip) {
|
||||||
|
if (getParentFragment() != null) {
|
||||||
|
NavigationHelper.openSearchFragment(getParentFragment().getParentFragmentManager(),
|
||||||
|
getServiceId(), ((Chip) chip).getText().toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean onTagLongClick(final View chip) {
|
||||||
|
ShareUtils.copyToClipboard(requireContext(), ((Chip) chip).getText().toString());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,46 +1,29 @@
|
||||||
package org.schabi.newpipe.fragments.detail;
|
package org.schabi.newpipe.fragments.detail;
|
||||||
|
|
||||||
import static android.text.TextUtils.isEmpty;
|
|
||||||
import static org.schabi.newpipe.extractor.stream.StreamExtractor.NO_AGE_LIMIT;
|
import static org.schabi.newpipe.extractor.stream.StreamExtractor.NO_AGE_LIMIT;
|
||||||
import static org.schabi.newpipe.extractor.utils.Utils.isBlank;
|
|
||||||
import static org.schabi.newpipe.util.Localization.getAppLocale;
|
import static org.schabi.newpipe.util.Localization.getAppLocale;
|
||||||
import static org.schabi.newpipe.util.text.TextLinkifier.SET_LINK_MOVEMENT_METHOD;
|
|
||||||
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
|
||||||
import android.widget.LinearLayout;
|
import android.widget.LinearLayout;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.annotation.StringRes;
|
import androidx.annotation.StringRes;
|
||||||
import androidx.appcompat.widget.TooltipCompat;
|
|
||||||
import androidx.core.text.HtmlCompat;
|
|
||||||
|
|
||||||
import com.google.android.material.chip.Chip;
|
|
||||||
|
|
||||||
import org.schabi.newpipe.BaseFragment;
|
|
||||||
import org.schabi.newpipe.R;
|
import org.schabi.newpipe.R;
|
||||||
import org.schabi.newpipe.databinding.FragmentDescriptionBinding;
|
import org.schabi.newpipe.extractor.StreamingService;
|
||||||
import org.schabi.newpipe.databinding.ItemMetadataBinding;
|
|
||||||
import org.schabi.newpipe.databinding.ItemMetadataTagsBinding;
|
|
||||||
import org.schabi.newpipe.extractor.stream.Description;
|
import org.schabi.newpipe.extractor.stream.Description;
|
||||||
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||||
import org.schabi.newpipe.util.Localization;
|
import org.schabi.newpipe.util.Localization;
|
||||||
import org.schabi.newpipe.util.NavigationHelper;
|
|
||||||
import org.schabi.newpipe.util.external_communication.ShareUtils;
|
import java.util.List;
|
||||||
import org.schabi.newpipe.util.text.TextLinkifier;
|
|
||||||
|
|
||||||
import icepick.State;
|
import icepick.State;
|
||||||
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
|
||||||
|
|
||||||
public class DescriptionFragment extends BaseFragment {
|
public class DescriptionFragment extends BaseDescriptionFragment {
|
||||||
|
|
||||||
@State
|
@State
|
||||||
StreamInfo streamInfo = null;
|
StreamInfo streamInfo = null;
|
||||||
final CompositeDisposable descriptionDisposables = new CompositeDisposable();
|
|
||||||
FragmentDescriptionBinding binding;
|
|
||||||
|
|
||||||
public DescriptionFragment() {
|
public DescriptionFragment() {
|
||||||
}
|
}
|
||||||
|
@ -49,86 +32,64 @@ public class DescriptionFragment extends BaseFragment {
|
||||||
this.streamInfo = streamInfo;
|
this.streamInfo = streamInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
@Override
|
@Override
|
||||||
public View onCreateView(@NonNull final LayoutInflater inflater,
|
protected Description getDescription() {
|
||||||
@Nullable final ViewGroup container,
|
if (streamInfo == null) {
|
||||||
@Nullable final Bundle savedInstanceState) {
|
return null;
|
||||||
binding = FragmentDescriptionBinding.inflate(inflater, container, false);
|
|
||||||
if (streamInfo != null) {
|
|
||||||
setupUploadDate();
|
|
||||||
setupDescription();
|
|
||||||
setupMetadata(inflater, binding.detailMetadataLayout);
|
|
||||||
}
|
}
|
||||||
return binding.getRoot();
|
return streamInfo.getDescription();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
@Override
|
||||||
|
protected StreamingService getService() {
|
||||||
|
if (streamInfo == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return streamInfo.getService();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onDestroy() {
|
protected int getServiceId() {
|
||||||
descriptionDisposables.clear();
|
if (streamInfo == null) {
|
||||||
super.onDestroy();
|
return -1;
|
||||||
|
}
|
||||||
|
return streamInfo.getServiceId();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
@Override
|
||||||
|
protected String getStreamUrl() {
|
||||||
|
if (streamInfo == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return streamInfo.getUrl();
|
||||||
|
}
|
||||||
|
|
||||||
private void setupUploadDate() {
|
@Nullable
|
||||||
if (streamInfo.getUploadDate() != null) {
|
@Override
|
||||||
|
public List<String> getTags() {
|
||||||
|
if (streamInfo == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return streamInfo.getTags();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void setupMetadata(final LayoutInflater inflater,
|
||||||
|
final LinearLayout layout) {
|
||||||
|
if (streamInfo != null && streamInfo.getUploadDate() != null) {
|
||||||
binding.detailUploadDateView.setText(Localization
|
binding.detailUploadDateView.setText(Localization
|
||||||
.localizeUploadDate(activity, streamInfo.getUploadDate().offsetDateTime()));
|
.localizeUploadDate(activity, streamInfo.getUploadDate().offsetDateTime()));
|
||||||
} else {
|
} else {
|
||||||
binding.detailUploadDateView.setVisibility(View.GONE);
|
binding.detailUploadDateView.setVisibility(View.GONE);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
if (streamInfo == null) {
|
||||||
private void setupDescription() {
|
|
||||||
final Description description = streamInfo.getDescription();
|
|
||||||
if (description == null || isEmpty(description.getContent())
|
|
||||||
|| description == Description.EMPTY_DESCRIPTION) {
|
|
||||||
binding.detailDescriptionView.setVisibility(View.GONE);
|
|
||||||
binding.detailSelectDescriptionButton.setVisibility(View.GONE);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// start with disabled state. This also loads description content (!)
|
|
||||||
disableDescriptionSelection();
|
|
||||||
|
|
||||||
binding.detailSelectDescriptionButton.setOnClickListener(v -> {
|
|
||||||
if (binding.detailDescriptionNoteView.getVisibility() == View.VISIBLE) {
|
|
||||||
disableDescriptionSelection();
|
|
||||||
} else {
|
|
||||||
// enable selection only when button is clicked to prevent flickering
|
|
||||||
enableDescriptionSelection();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private void enableDescriptionSelection() {
|
|
||||||
binding.detailDescriptionNoteView.setVisibility(View.VISIBLE);
|
|
||||||
binding.detailDescriptionView.setTextIsSelectable(true);
|
|
||||||
|
|
||||||
final String buttonLabel = getString(R.string.description_select_disable);
|
|
||||||
binding.detailSelectDescriptionButton.setContentDescription(buttonLabel);
|
|
||||||
TooltipCompat.setTooltipText(binding.detailSelectDescriptionButton, buttonLabel);
|
|
||||||
binding.detailSelectDescriptionButton.setImageResource(R.drawable.ic_close);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void disableDescriptionSelection() {
|
|
||||||
// show description content again, otherwise some links are not clickable
|
|
||||||
TextLinkifier.fromDescription(binding.detailDescriptionView,
|
|
||||||
streamInfo.getDescription(), HtmlCompat.FROM_HTML_MODE_LEGACY,
|
|
||||||
streamInfo.getService(), streamInfo.getUrl(),
|
|
||||||
descriptionDisposables, SET_LINK_MOVEMENT_METHOD);
|
|
||||||
|
|
||||||
binding.detailDescriptionNoteView.setVisibility(View.GONE);
|
|
||||||
binding.detailDescriptionView.setTextIsSelectable(false);
|
|
||||||
|
|
||||||
final String buttonLabel = getString(R.string.description_select_enable);
|
|
||||||
binding.detailSelectDescriptionButton.setContentDescription(buttonLabel);
|
|
||||||
TooltipCompat.setTooltipText(binding.detailSelectDescriptionButton, buttonLabel);
|
|
||||||
binding.detailSelectDescriptionButton.setImageResource(R.drawable.ic_select_all);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void setupMetadata(final LayoutInflater inflater,
|
|
||||||
final LinearLayout layout) {
|
|
||||||
addMetadataItem(inflater, layout, false, R.string.metadata_category,
|
addMetadataItem(inflater, layout, false, R.string.metadata_category,
|
||||||
streamInfo.getCategory());
|
streamInfo.getCategory());
|
||||||
|
|
||||||
|
@ -151,69 +112,13 @@ public class DescriptionFragment extends BaseFragment {
|
||||||
streamInfo.getSupportInfo());
|
streamInfo.getSupportInfo());
|
||||||
addMetadataItem(inflater, layout, true, R.string.metadata_host,
|
addMetadataItem(inflater, layout, true, R.string.metadata_host,
|
||||||
streamInfo.getHost());
|
streamInfo.getHost());
|
||||||
addMetadataItem(inflater, layout, true, R.string.metadata_thumbnail_url,
|
|
||||||
streamInfo.getThumbnailUrl());
|
|
||||||
|
|
||||||
addTagsMetadataItem(inflater, layout);
|
addImagesMetadataItem(inflater, layout, R.string.metadata_thumbnails,
|
||||||
}
|
streamInfo.getThumbnails());
|
||||||
|
addImagesMetadataItem(inflater, layout, R.string.metadata_uploader_avatars,
|
||||||
private void addMetadataItem(final LayoutInflater inflater,
|
streamInfo.getUploaderAvatars());
|
||||||
final LinearLayout layout,
|
addImagesMetadataItem(inflater, layout, R.string.metadata_subchannel_avatars,
|
||||||
final boolean linkifyContent,
|
streamInfo.getSubChannelAvatars());
|
||||||
@StringRes final int type,
|
|
||||||
@Nullable final String content) {
|
|
||||||
if (isBlank(content)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final ItemMetadataBinding itemBinding =
|
|
||||||
ItemMetadataBinding.inflate(inflater, layout, false);
|
|
||||||
|
|
||||||
itemBinding.metadataTypeView.setText(type);
|
|
||||||
itemBinding.metadataTypeView.setOnLongClickListener(v -> {
|
|
||||||
ShareUtils.copyToClipboard(requireContext(), content);
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (linkifyContent) {
|
|
||||||
TextLinkifier.fromPlainText(itemBinding.metadataContentView, content, null, null,
|
|
||||||
descriptionDisposables, SET_LINK_MOVEMENT_METHOD);
|
|
||||||
} else {
|
|
||||||
itemBinding.metadataContentView.setText(content);
|
|
||||||
}
|
|
||||||
|
|
||||||
itemBinding.metadataContentView.setClickable(true);
|
|
||||||
|
|
||||||
layout.addView(itemBinding.getRoot());
|
|
||||||
}
|
|
||||||
|
|
||||||
private void addTagsMetadataItem(final LayoutInflater inflater, final LinearLayout layout) {
|
|
||||||
if (streamInfo.getTags() != null && !streamInfo.getTags().isEmpty()) {
|
|
||||||
final var itemBinding = ItemMetadataTagsBinding.inflate(inflater, layout, false);
|
|
||||||
|
|
||||||
streamInfo.getTags().stream().sorted(String.CASE_INSENSITIVE_ORDER).forEach(tag -> {
|
|
||||||
final Chip chip = (Chip) inflater.inflate(R.layout.chip,
|
|
||||||
itemBinding.metadataTagsChips, false);
|
|
||||||
chip.setText(tag);
|
|
||||||
chip.setOnClickListener(this::onTagClick);
|
|
||||||
chip.setOnLongClickListener(this::onTagLongClick);
|
|
||||||
itemBinding.metadataTagsChips.addView(chip);
|
|
||||||
});
|
|
||||||
|
|
||||||
layout.addView(itemBinding.getRoot());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void onTagClick(final View chip) {
|
|
||||||
if (getParentFragment() != null) {
|
|
||||||
NavigationHelper.openSearchFragment(getParentFragment().getParentFragmentManager(),
|
|
||||||
streamInfo.getServiceId(), ((Chip) chip).getText().toString());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean onTagLongClick(final View chip) {
|
|
||||||
ShareUtils.copyToClipboard(requireContext(), ((Chip) chip).getText().toString());
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void addPrivacyMetadataItem(final LayoutInflater inflater, final LinearLayout layout) {
|
private void addPrivacyMetadataItem(final LayoutInflater inflater, final LinearLayout layout) {
|
||||||
|
|
|
@ -24,7 +24,6 @@ import android.content.pm.ActivityInfo;
|
||||||
import android.database.ContentObserver;
|
import android.database.ContentObserver;
|
||||||
import android.graphics.Color;
|
import android.graphics.Color;
|
||||||
import android.graphics.Rect;
|
import android.graphics.Rect;
|
||||||
import android.graphics.drawable.Drawable;
|
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
@ -54,6 +53,7 @@ import androidx.appcompat.content.res.AppCompatResources;
|
||||||
import androidx.appcompat.widget.Toolbar;
|
import androidx.appcompat.widget.Toolbar;
|
||||||
import androidx.coordinatorlayout.widget.CoordinatorLayout;
|
import androidx.coordinatorlayout.widget.CoordinatorLayout;
|
||||||
import androidx.core.content.ContextCompat;
|
import androidx.core.content.ContextCompat;
|
||||||
|
import androidx.fragment.app.Fragment;
|
||||||
import androidx.preference.PreferenceManager;
|
import androidx.preference.PreferenceManager;
|
||||||
|
|
||||||
import com.google.android.exoplayer2.PlaybackException;
|
import com.google.android.exoplayer2.PlaybackException;
|
||||||
|
@ -71,8 +71,10 @@ import org.schabi.newpipe.error.ErrorInfo;
|
||||||
import org.schabi.newpipe.error.ErrorUtil;
|
import org.schabi.newpipe.error.ErrorUtil;
|
||||||
import org.schabi.newpipe.error.ReCaptchaActivity;
|
import org.schabi.newpipe.error.ReCaptchaActivity;
|
||||||
import org.schabi.newpipe.error.UserAction;
|
import org.schabi.newpipe.error.UserAction;
|
||||||
|
import org.schabi.newpipe.extractor.Image;
|
||||||
import org.schabi.newpipe.extractor.InfoItem;
|
import org.schabi.newpipe.extractor.InfoItem;
|
||||||
import org.schabi.newpipe.extractor.NewPipe;
|
import org.schabi.newpipe.extractor.NewPipe;
|
||||||
|
import org.schabi.newpipe.extractor.comments.CommentsInfoItem;
|
||||||
import org.schabi.newpipe.extractor.exceptions.ContentNotSupportedException;
|
import org.schabi.newpipe.extractor.exceptions.ContentNotSupportedException;
|
||||||
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||||
import org.schabi.newpipe.extractor.stream.AudioStream;
|
import org.schabi.newpipe.extractor.stream.AudioStream;
|
||||||
|
@ -83,11 +85,13 @@ import org.schabi.newpipe.extractor.stream.VideoStream;
|
||||||
import org.schabi.newpipe.fragments.BackPressable;
|
import org.schabi.newpipe.fragments.BackPressable;
|
||||||
import org.schabi.newpipe.fragments.BaseStateFragment;
|
import org.schabi.newpipe.fragments.BaseStateFragment;
|
||||||
import org.schabi.newpipe.fragments.EmptyFragment;
|
import org.schabi.newpipe.fragments.EmptyFragment;
|
||||||
|
import org.schabi.newpipe.fragments.MainFragment;
|
||||||
import org.schabi.newpipe.fragments.list.comments.CommentsFragment;
|
import org.schabi.newpipe.fragments.list.comments.CommentsFragment;
|
||||||
import org.schabi.newpipe.fragments.list.videos.RelatedItemsFragment;
|
import org.schabi.newpipe.fragments.list.videos.RelatedItemsFragment;
|
||||||
import org.schabi.newpipe.ktx.AnimationType;
|
import org.schabi.newpipe.ktx.AnimationType;
|
||||||
import org.schabi.newpipe.local.dialog.PlaylistDialog;
|
import org.schabi.newpipe.local.dialog.PlaylistDialog;
|
||||||
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
||||||
|
import org.schabi.newpipe.local.playlist.LocalPlaylistFragment;
|
||||||
import org.schabi.newpipe.player.Player;
|
import org.schabi.newpipe.player.Player;
|
||||||
import org.schabi.newpipe.player.PlayerService;
|
import org.schabi.newpipe.player.PlayerService;
|
||||||
import org.schabi.newpipe.player.PlayerType;
|
import org.schabi.newpipe.player.PlayerType;
|
||||||
|
@ -109,10 +113,11 @@ import org.schabi.newpipe.util.ListHelper;
|
||||||
import org.schabi.newpipe.util.Localization;
|
import org.schabi.newpipe.util.Localization;
|
||||||
import org.schabi.newpipe.util.NavigationHelper;
|
import org.schabi.newpipe.util.NavigationHelper;
|
||||||
import org.schabi.newpipe.util.PermissionHelper;
|
import org.schabi.newpipe.util.PermissionHelper;
|
||||||
import org.schabi.newpipe.util.PicassoHelper;
|
import org.schabi.newpipe.util.image.PicassoHelper;
|
||||||
import org.schabi.newpipe.util.StreamTypeUtil;
|
import org.schabi.newpipe.util.StreamTypeUtil;
|
||||||
import org.schabi.newpipe.util.external_communication.ShareUtils;
|
import org.schabi.newpipe.util.external_communication.ShareUtils;
|
||||||
import org.schabi.newpipe.util.ThemeHelper;
|
import org.schabi.newpipe.util.ThemeHelper;
|
||||||
|
import org.schabi.newpipe.util.PlayButtonHelper;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Iterator;
|
import java.util.Iterator;
|
||||||
|
@ -471,10 +476,23 @@ public final class VideoDetailFragment
|
||||||
|
|
||||||
binding.detailControlsBackground.setOnClickListener(v -> openBackgroundPlayer(false));
|
binding.detailControlsBackground.setOnClickListener(v -> openBackgroundPlayer(false));
|
||||||
binding.detailControlsPopup.setOnClickListener(v -> openPopupPlayer(false));
|
binding.detailControlsPopup.setOnClickListener(v -> openPopupPlayer(false));
|
||||||
binding.detailControlsPlaylistAppend.setOnClickListener(makeOnClickListener(info ->
|
binding.detailControlsPlaylistAppend.setOnClickListener(makeOnClickListener(info -> {
|
||||||
|
if (getFM() != null && currentInfo != null) {
|
||||||
|
final Fragment fragment = getParentFragmentManager().
|
||||||
|
findFragmentById(R.id.fragment_holder);
|
||||||
|
|
||||||
|
// commit previous pending changes to database
|
||||||
|
if (fragment instanceof LocalPlaylistFragment) {
|
||||||
|
((LocalPlaylistFragment) fragment).commitChanges();
|
||||||
|
} else if (fragment instanceof MainFragment) {
|
||||||
|
((MainFragment) fragment).commitPlaylistTabs();
|
||||||
|
}
|
||||||
|
|
||||||
disposables.add(PlaylistDialog.createCorrespondingDialog(requireContext(),
|
disposables.add(PlaylistDialog.createCorrespondingDialog(requireContext(),
|
||||||
List.of(new StreamEntity(info)),
|
List.of(new StreamEntity(info)),
|
||||||
dialog -> dialog.show(getParentFragmentManager(), TAG)))));
|
dialog -> dialog.show(getParentFragmentManager(), TAG)));
|
||||||
|
}
|
||||||
|
}));
|
||||||
binding.detailControlsDownload.setOnClickListener(v -> {
|
binding.detailControlsDownload.setOnClickListener(v -> {
|
||||||
if (PermissionHelper.checkStoragePermissions(activity,
|
if (PermissionHelper.checkStoragePermissions(activity,
|
||||||
PermissionHelper.DOWNLOAD_DIALOG_REQUEST_CODE)) {
|
PermissionHelper.DOWNLOAD_DIALOG_REQUEST_CODE)) {
|
||||||
|
@ -483,7 +501,7 @@ public final class VideoDetailFragment
|
||||||
});
|
});
|
||||||
binding.detailControlsShare.setOnClickListener(makeOnClickListener(info ->
|
binding.detailControlsShare.setOnClickListener(makeOnClickListener(info ->
|
||||||
ShareUtils.shareText(requireContext(), info.getName(), info.getUrl(),
|
ShareUtils.shareText(requireContext(), info.getName(), info.getUrl(),
|
||||||
info.getThumbnailUrl())));
|
info.getThumbnails())));
|
||||||
binding.detailControlsOpenInBrowser.setOnClickListener(makeOnClickListener(info ->
|
binding.detailControlsOpenInBrowser.setOnClickListener(makeOnClickListener(info ->
|
||||||
ShareUtils.openUrlInBrowser(requireContext(), info.getUrl())));
|
ShareUtils.openUrlInBrowser(requireContext(), info.getUrl())));
|
||||||
binding.detailControlsPlayWithKodi.setOnClickListener(makeOnClickListener(info ->
|
binding.detailControlsPlayWithKodi.setOnClickListener(makeOnClickListener(info ->
|
||||||
|
@ -536,9 +554,11 @@ public final class VideoDetailFragment
|
||||||
}));
|
}));
|
||||||
|
|
||||||
binding.detailControlsBackground.setOnLongClickListener(makeOnLongClickListener(info ->
|
binding.detailControlsBackground.setOnLongClickListener(makeOnLongClickListener(info ->
|
||||||
openBackgroundPlayer(true)));
|
openBackgroundPlayer(true)
|
||||||
|
));
|
||||||
binding.detailControlsPopup.setOnLongClickListener(makeOnLongClickListener(info ->
|
binding.detailControlsPopup.setOnLongClickListener(makeOnLongClickListener(info ->
|
||||||
openPopupPlayer(true)));
|
openPopupPlayer(true)
|
||||||
|
));
|
||||||
binding.detailControlsDownload.setOnLongClickListener(makeOnLongClickListener(info ->
|
binding.detailControlsDownload.setOnLongClickListener(makeOnLongClickListener(info ->
|
||||||
NavigationHelper.openDownloads(activity)));
|
NavigationHelper.openDownloads(activity)));
|
||||||
|
|
||||||
|
@ -621,8 +641,7 @@ public final class VideoDetailFragment
|
||||||
|
|
||||||
final View.OnTouchListener controlsTouchListener = (view, motionEvent) -> {
|
final View.OnTouchListener controlsTouchListener = (view, motionEvent) -> {
|
||||||
if (motionEvent.getAction() == MotionEvent.ACTION_DOWN
|
if (motionEvent.getAction() == MotionEvent.ACTION_DOWN
|
||||||
&& PreferenceManager.getDefaultSharedPreferences(activity)
|
&& PlayButtonHelper.shouldShowHoldToAppendTip(activity)) {
|
||||||
.getBoolean(getString(R.string.show_hold_to_append_key), true)) {
|
|
||||||
|
|
||||||
animate(binding.touchAppendDetail, true, 250, AnimationType.ALPHA, 0, () ->
|
animate(binding.touchAppendDetail, true, 250, AnimationType.ALPHA, 0, () ->
|
||||||
animate(binding.touchAppendDetail, false, 1500, AnimationType.ALPHA, 1000));
|
animate(binding.touchAppendDetail, false, 1500, AnimationType.ALPHA, 1000));
|
||||||
|
@ -722,7 +741,7 @@ public final class VideoDetailFragment
|
||||||
final boolean isPlayerStopped = !isPlayerAvailable() || player.isStopped();
|
final boolean isPlayerStopped = !isPlayerAvailable() || player.isStopped();
|
||||||
if (playQueueItem != null && isPlayerStopped) {
|
if (playQueueItem != null && isPlayerStopped) {
|
||||||
updateOverlayData(playQueueItem.getTitle(),
|
updateOverlayData(playQueueItem.getTitle(),
|
||||||
playQueueItem.getUploader(), playQueueItem.getThumbnailUrl());
|
playQueueItem.getUploader(), playQueueItem.getThumbnails());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -995,6 +1014,20 @@ public final class VideoDetailFragment
|
||||||
updateTabLayoutVisibility();
|
updateTabLayoutVisibility();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void scrollToComment(final CommentsInfoItem comment) {
|
||||||
|
final int commentsTabPos = pageAdapter.getItemPositionByTitle(COMMENTS_TAB_TAG);
|
||||||
|
final Fragment fragment = pageAdapter.getItem(commentsTabPos);
|
||||||
|
if (!(fragment instanceof CommentsFragment)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// unexpand the app bar only if scrolling to the comment succeeded
|
||||||
|
if (((CommentsFragment) fragment).scrollToComment(comment)) {
|
||||||
|
binding.appBarLayout.setExpanded(false, false);
|
||||||
|
binding.viewPager.setCurrentItem(commentsTabPos, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
// Play Utils
|
// Play Utils
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
@ -1464,11 +1497,6 @@ public final class VideoDetailFragment
|
||||||
displayUploaderAsSubChannel(info);
|
displayUploaderAsSubChannel(info);
|
||||||
}
|
}
|
||||||
|
|
||||||
final Drawable buddyDrawable =
|
|
||||||
AppCompatResources.getDrawable(activity, R.drawable.placeholder_person);
|
|
||||||
binding.detailSubChannelThumbnailView.setImageDrawable(buddyDrawable);
|
|
||||||
binding.detailUploaderThumbnailView.setImageDrawable(buddyDrawable);
|
|
||||||
|
|
||||||
if (info.getViewCount() >= 0) {
|
if (info.getViewCount() >= 0) {
|
||||||
if (info.getStreamType().equals(StreamType.AUDIO_LIVE_STREAM)) {
|
if (info.getStreamType().equals(StreamType.AUDIO_LIVE_STREAM)) {
|
||||||
binding.detailViewCountView.setText(Localization.listeningCount(activity,
|
binding.detailViewCountView.setText(Localization.listeningCount(activity,
|
||||||
|
@ -1555,13 +1583,13 @@ public final class VideoDetailFragment
|
||||||
binding.detailSecondaryControlPanel.setVisibility(View.GONE);
|
binding.detailSecondaryControlPanel.setVisibility(View.GONE);
|
||||||
|
|
||||||
checkUpdateProgressInfo(info);
|
checkUpdateProgressInfo(info);
|
||||||
PicassoHelper.loadDetailsThumbnail(info.getThumbnailUrl()).tag(PICASSO_VIDEO_DETAILS_TAG)
|
PicassoHelper.loadDetailsThumbnail(info.getThumbnails()).tag(PICASSO_VIDEO_DETAILS_TAG)
|
||||||
.into(binding.detailThumbnailImageView);
|
.into(binding.detailThumbnailImageView);
|
||||||
showMetaInfoInTextView(info.getMetaInfo(), binding.detailMetaInfoTextView,
|
showMetaInfoInTextView(info.getMetaInfo(), binding.detailMetaInfoTextView,
|
||||||
binding.detailMetaInfoSeparator, disposables);
|
binding.detailMetaInfoSeparator, disposables);
|
||||||
|
|
||||||
if (!isPlayerAvailable() || player.isStopped()) {
|
if (!isPlayerAvailable() || player.isStopped()) {
|
||||||
updateOverlayData(info.getName(), info.getUploaderName(), info.getThumbnailUrl());
|
updateOverlayData(info.getName(), info.getUploaderName(), info.getThumbnails());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!info.getErrors().isEmpty()) {
|
if (!info.getErrors().isEmpty()) {
|
||||||
|
@ -1606,7 +1634,7 @@ public final class VideoDetailFragment
|
||||||
binding.detailUploaderTextView.setVisibility(View.GONE);
|
binding.detailUploaderTextView.setVisibility(View.GONE);
|
||||||
}
|
}
|
||||||
|
|
||||||
PicassoHelper.loadAvatar(info.getUploaderAvatarUrl()).tag(PICASSO_VIDEO_DETAILS_TAG)
|
PicassoHelper.loadAvatar(info.getUploaderAvatars()).tag(PICASSO_VIDEO_DETAILS_TAG)
|
||||||
.into(binding.detailSubChannelThumbnailView);
|
.into(binding.detailSubChannelThumbnailView);
|
||||||
binding.detailSubChannelThumbnailView.setVisibility(View.VISIBLE);
|
binding.detailSubChannelThumbnailView.setVisibility(View.VISIBLE);
|
||||||
binding.detailUploaderThumbnailView.setVisibility(View.GONE);
|
binding.detailUploaderThumbnailView.setVisibility(View.GONE);
|
||||||
|
@ -1638,10 +1666,10 @@ public final class VideoDetailFragment
|
||||||
binding.detailUploaderTextView.setVisibility(View.GONE);
|
binding.detailUploaderTextView.setVisibility(View.GONE);
|
||||||
}
|
}
|
||||||
|
|
||||||
PicassoHelper.loadAvatar(info.getSubChannelAvatarUrl()).tag(PICASSO_VIDEO_DETAILS_TAG)
|
PicassoHelper.loadAvatar(info.getSubChannelAvatars()).tag(PICASSO_VIDEO_DETAILS_TAG)
|
||||||
.into(binding.detailSubChannelThumbnailView);
|
.into(binding.detailSubChannelThumbnailView);
|
||||||
binding.detailSubChannelThumbnailView.setVisibility(View.VISIBLE);
|
binding.detailSubChannelThumbnailView.setVisibility(View.VISIBLE);
|
||||||
PicassoHelper.loadAvatar(info.getUploaderAvatarUrl()).tag(PICASSO_VIDEO_DETAILS_TAG)
|
PicassoHelper.loadAvatar(info.getUploaderAvatars()).tag(PICASSO_VIDEO_DETAILS_TAG)
|
||||||
.into(binding.detailUploaderThumbnailView);
|
.into(binding.detailUploaderThumbnailView);
|
||||||
binding.detailUploaderThumbnailView.setVisibility(View.VISIBLE);
|
binding.detailUploaderThumbnailView.setVisibility(View.VISIBLE);
|
||||||
}
|
}
|
||||||
|
@ -1816,7 +1844,7 @@ public final class VideoDetailFragment
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
updateOverlayData(info.getName(), info.getUploaderName(), info.getThumbnailUrl());
|
updateOverlayData(info.getName(), info.getUploaderName(), info.getThumbnails());
|
||||||
if (currentInfo != null && info.getUrl().equals(currentInfo.getUrl())) {
|
if (currentInfo != null && info.getUrl().equals(currentInfo.getUrl())) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -1845,7 +1873,7 @@ public final class VideoDetailFragment
|
||||||
if (currentInfo != null) {
|
if (currentInfo != null) {
|
||||||
updateOverlayData(currentInfo.getName(),
|
updateOverlayData(currentInfo.getName(),
|
||||||
currentInfo.getUploaderName(),
|
currentInfo.getUploaderName(),
|
||||||
currentInfo.getThumbnailUrl());
|
currentInfo.getThumbnails());
|
||||||
}
|
}
|
||||||
updateOverlayPlayQueueButtonVisibility();
|
updateOverlayPlayQueueButtonVisibility();
|
||||||
}
|
}
|
||||||
|
@ -2210,7 +2238,7 @@ public final class VideoDetailFragment
|
||||||
playerHolder.stopService();
|
playerHolder.stopService();
|
||||||
setInitialData(0, null, "", null);
|
setInitialData(0, null, "", null);
|
||||||
currentInfo = null;
|
currentInfo = null;
|
||||||
updateOverlayData(null, null, null);
|
updateOverlayData(null, null, List.of());
|
||||||
}
|
}
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
|
@ -2392,11 +2420,11 @@ public final class VideoDetailFragment
|
||||||
|
|
||||||
private void updateOverlayData(@Nullable final String overlayTitle,
|
private void updateOverlayData(@Nullable final String overlayTitle,
|
||||||
@Nullable final String uploader,
|
@Nullable final String uploader,
|
||||||
@Nullable final String thumbnailUrl) {
|
@NonNull final List<Image> thumbnails) {
|
||||||
binding.overlayTitleTextView.setText(isEmpty(overlayTitle) ? "" : overlayTitle);
|
binding.overlayTitleTextView.setText(isEmpty(overlayTitle) ? "" : overlayTitle);
|
||||||
binding.overlayChannelTextView.setText(isEmpty(uploader) ? "" : uploader);
|
binding.overlayChannelTextView.setText(isEmpty(uploader) ? "" : uploader);
|
||||||
binding.overlayThumbnail.setImageDrawable(null);
|
binding.overlayThumbnail.setImageDrawable(null);
|
||||||
PicassoHelper.loadDetailsThumbnail(thumbnailUrl).tag(PICASSO_VIDEO_DETAILS_TAG)
|
PicassoHelper.loadDetailsThumbnail(thumbnails).tag(PICASSO_VIDEO_DETAILS_TAG)
|
||||||
.into(binding.overlayThumbnail);
|
.into(binding.overlayThumbnail);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
package org.schabi.newpipe.fragments.list;
|
package org.schabi.newpipe.fragments.list;
|
||||||
|
|
||||||
|
import static org.schabi.newpipe.extractor.ServiceList.SoundCloud;
|
||||||
|
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
@ -7,13 +9,13 @@ import android.view.View;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
import org.schabi.newpipe.R;
|
||||||
import org.schabi.newpipe.error.ErrorInfo;
|
import org.schabi.newpipe.error.ErrorInfo;
|
||||||
import org.schabi.newpipe.error.UserAction;
|
import org.schabi.newpipe.error.UserAction;
|
||||||
import org.schabi.newpipe.extractor.InfoItem;
|
import org.schabi.newpipe.extractor.InfoItem;
|
||||||
import org.schabi.newpipe.extractor.ListExtractor;
|
import org.schabi.newpipe.extractor.ListExtractor;
|
||||||
import org.schabi.newpipe.extractor.ListInfo;
|
import org.schabi.newpipe.extractor.ListInfo;
|
||||||
import org.schabi.newpipe.extractor.Page;
|
import org.schabi.newpipe.extractor.Page;
|
||||||
import org.schabi.newpipe.extractor.channel.ChannelInfo;
|
|
||||||
import org.schabi.newpipe.extractor.exceptions.ContentNotSupportedException;
|
import org.schabi.newpipe.extractor.exceptions.ContentNotSupportedException;
|
||||||
import org.schabi.newpipe.util.Constants;
|
import org.schabi.newpipe.util.Constants;
|
||||||
import org.schabi.newpipe.views.NewPipeRecyclerView;
|
import org.schabi.newpipe.views.NewPipeRecyclerView;
|
||||||
|
@ -229,13 +231,11 @@ public abstract class BaseListInfoFragment<I extends InfoItem, L extends ListInf
|
||||||
if (!result.getRelatedItems().isEmpty()) {
|
if (!result.getRelatedItems().isEmpty()) {
|
||||||
infoListAdapter.addInfoItemList(result.getRelatedItems());
|
infoListAdapter.addInfoItemList(result.getRelatedItems());
|
||||||
showListFooter(hasMoreItems());
|
showListFooter(hasMoreItems());
|
||||||
|
} else if (hasMoreItems()) {
|
||||||
|
loadMoreItems();
|
||||||
} else {
|
} else {
|
||||||
infoListAdapter.clearStreamItemList();
|
infoListAdapter.clearStreamItemList();
|
||||||
// showEmptyState should be called only if there is no item as
|
showEmptyState();
|
||||||
// well as no header in infoListAdapter
|
|
||||||
if (!(result instanceof ChannelInfo && infoListAdapter.getItemCount() == 1)) {
|
|
||||||
showEmptyState();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -252,6 +252,20 @@ public abstract class BaseListInfoFragment<I extends InfoItem, L extends ListInf
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void showEmptyState() {
|
||||||
|
// show "no streams" for SoundCloud; otherwise "no videos"
|
||||||
|
// showing "no live streams" is handled in KioskFragment
|
||||||
|
if (emptyStateView != null) {
|
||||||
|
if (currentInfo.getService() == SoundCloud) {
|
||||||
|
setEmptyStateMessage(R.string.no_streams);
|
||||||
|
} else {
|
||||||
|
setEmptyStateMessage(R.string.no_videos);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
super.showEmptyState();
|
||||||
|
}
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
// Utils
|
// Utils
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
|
@ -0,0 +1,107 @@
|
||||||
|
package org.schabi.newpipe.fragments.list.channel;
|
||||||
|
|
||||||
|
import static org.schabi.newpipe.extractor.stream.StreamExtractor.UNKNOWN_SUBSCRIBER_COUNT;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.View;
|
||||||
|
import android.widget.LinearLayout;
|
||||||
|
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
|
import org.schabi.newpipe.R;
|
||||||
|
import org.schabi.newpipe.extractor.StreamingService;
|
||||||
|
import org.schabi.newpipe.extractor.channel.ChannelInfo;
|
||||||
|
import org.schabi.newpipe.extractor.stream.Description;
|
||||||
|
import org.schabi.newpipe.fragments.detail.BaseDescriptionFragment;
|
||||||
|
import org.schabi.newpipe.util.DeviceUtils;
|
||||||
|
import org.schabi.newpipe.util.Localization;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import icepick.State;
|
||||||
|
|
||||||
|
public class ChannelAboutFragment extends BaseDescriptionFragment {
|
||||||
|
@State
|
||||||
|
protected ChannelInfo channelInfo;
|
||||||
|
|
||||||
|
public static ChannelAboutFragment getInstance(final ChannelInfo channelInfo) {
|
||||||
|
final ChannelAboutFragment fragment = new ChannelAboutFragment();
|
||||||
|
fragment.channelInfo = channelInfo;
|
||||||
|
return fragment;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ChannelAboutFragment() {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void initViews(final View rootView, final Bundle savedInstanceState) {
|
||||||
|
super.initViews(rootView, savedInstanceState);
|
||||||
|
binding.constraintLayout.setPadding(0, DeviceUtils.dpToPx(8, requireContext()), 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
@Override
|
||||||
|
protected Description getDescription() {
|
||||||
|
if (channelInfo == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return new Description(channelInfo.getDescription(), Description.PLAIN_TEXT);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
@Override
|
||||||
|
protected StreamingService getService() {
|
||||||
|
if (channelInfo == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return channelInfo.getService();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected int getServiceId() {
|
||||||
|
if (channelInfo == null) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
return channelInfo.getServiceId();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
@Override
|
||||||
|
protected String getStreamUrl() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
@Override
|
||||||
|
public List<String> getTags() {
|
||||||
|
if (channelInfo == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return channelInfo.getTags();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void setupMetadata(final LayoutInflater inflater,
|
||||||
|
final LinearLayout layout) {
|
||||||
|
// There is no upload date available for channels, so hide the relevant UI element
|
||||||
|
binding.detailUploadDateView.setVisibility(View.GONE);
|
||||||
|
|
||||||
|
if (channelInfo == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final Context context = getContext();
|
||||||
|
if (channelInfo.getSubscriberCount() != UNKNOWN_SUBSCRIBER_COUNT) {
|
||||||
|
addMetadataItem(inflater, layout, false, R.string.metadata_subscribers,
|
||||||
|
Localization.localizeNumber(context, channelInfo.getSubscriberCount()));
|
||||||
|
}
|
||||||
|
|
||||||
|
addImagesMetadataItem(inflater, layout, R.string.metadata_avatars,
|
||||||
|
channelInfo.getAvatars());
|
||||||
|
addImagesMetadataItem(inflater, layout, R.string.metadata_banners,
|
||||||
|
channelInfo.getBanners());
|
||||||
|
}
|
||||||
|
}
|
|
@ -5,6 +5,7 @@ import static org.schabi.newpipe.ktx.ViewUtils.animate;
|
||||||
import static org.schabi.newpipe.ktx.ViewUtils.animateBackgroundColor;
|
import static org.schabi.newpipe.ktx.ViewUtils.animateBackgroundColor;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
import android.content.SharedPreferences;
|
||||||
import android.graphics.Color;
|
import android.graphics.Color;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
|
@ -16,51 +17,50 @@ import android.view.MenuInflater;
|
||||||
import android.view.MenuItem;
|
import android.view.MenuItem;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
import android.widget.Button;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.appcompat.app.ActionBar;
|
|
||||||
import androidx.core.content.ContextCompat;
|
import androidx.core.content.ContextCompat;
|
||||||
|
import androidx.core.graphics.ColorUtils;
|
||||||
|
import androidx.preference.PreferenceManager;
|
||||||
|
|
||||||
import com.google.android.material.snackbar.Snackbar;
|
import com.google.android.material.snackbar.Snackbar;
|
||||||
|
import com.google.android.material.tabs.TabLayout;
|
||||||
import com.jakewharton.rxbinding4.view.RxView;
|
import com.jakewharton.rxbinding4.view.RxView;
|
||||||
|
|
||||||
import org.schabi.newpipe.R;
|
import org.schabi.newpipe.R;
|
||||||
import org.schabi.newpipe.database.subscription.NotificationMode;
|
import org.schabi.newpipe.database.subscription.NotificationMode;
|
||||||
import org.schabi.newpipe.database.subscription.SubscriptionEntity;
|
import org.schabi.newpipe.database.subscription.SubscriptionEntity;
|
||||||
import org.schabi.newpipe.databinding.ChannelHeaderBinding;
|
|
||||||
import org.schabi.newpipe.databinding.FragmentChannelBinding;
|
import org.schabi.newpipe.databinding.FragmentChannelBinding;
|
||||||
import org.schabi.newpipe.databinding.PlaylistControlBinding;
|
|
||||||
import org.schabi.newpipe.error.ErrorInfo;
|
import org.schabi.newpipe.error.ErrorInfo;
|
||||||
import org.schabi.newpipe.error.ErrorUtil;
|
import org.schabi.newpipe.error.ErrorUtil;
|
||||||
import org.schabi.newpipe.error.UserAction;
|
import org.schabi.newpipe.error.UserAction;
|
||||||
import org.schabi.newpipe.extractor.ListExtractor;
|
|
||||||
import org.schabi.newpipe.extractor.channel.ChannelInfo;
|
import org.schabi.newpipe.extractor.channel.ChannelInfo;
|
||||||
import org.schabi.newpipe.extractor.exceptions.ContentNotSupportedException;
|
import org.schabi.newpipe.extractor.exceptions.ContentNotSupportedException;
|
||||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler;
|
||||||
import org.schabi.newpipe.fragments.list.BaseListInfoFragment;
|
import org.schabi.newpipe.fragments.BaseStateFragment;
|
||||||
|
import org.schabi.newpipe.fragments.detail.TabAdapter;
|
||||||
import org.schabi.newpipe.ktx.AnimationType;
|
import org.schabi.newpipe.ktx.AnimationType;
|
||||||
import org.schabi.newpipe.local.subscription.SubscriptionManager;
|
|
||||||
import org.schabi.newpipe.local.feed.notifications.NotificationHelper;
|
import org.schabi.newpipe.local.feed.notifications.NotificationHelper;
|
||||||
import org.schabi.newpipe.player.PlayerType;
|
import org.schabi.newpipe.local.subscription.SubscriptionManager;
|
||||||
import org.schabi.newpipe.player.playqueue.ChannelPlayQueue;
|
import org.schabi.newpipe.util.ChannelTabHelper;
|
||||||
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
import org.schabi.newpipe.util.Constants;
|
||||||
import org.schabi.newpipe.util.ExtractorHelper;
|
import org.schabi.newpipe.util.ExtractorHelper;
|
||||||
import org.schabi.newpipe.util.Localization;
|
import org.schabi.newpipe.util.Localization;
|
||||||
import org.schabi.newpipe.util.NavigationHelper;
|
import org.schabi.newpipe.util.NavigationHelper;
|
||||||
import org.schabi.newpipe.util.PicassoHelper;
|
import org.schabi.newpipe.util.StateSaver;
|
||||||
|
import org.schabi.newpipe.util.image.ImageStrategy;
|
||||||
|
import org.schabi.newpipe.util.image.PicassoHelper;
|
||||||
import org.schabi.newpipe.util.ThemeHelper;
|
import org.schabi.newpipe.util.ThemeHelper;
|
||||||
import org.schabi.newpipe.util.external_communication.ShareUtils;
|
import org.schabi.newpipe.util.external_communication.ShareUtils;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Queue;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
import java.util.function.Supplier;
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
|
import icepick.State;
|
||||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||||
import io.reactivex.rxjava3.core.Observable;
|
import io.reactivex.rxjava3.core.Observable;
|
||||||
import io.reactivex.rxjava3.core.Single;
|
|
||||||
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
||||||
import io.reactivex.rxjava3.disposables.Disposable;
|
import io.reactivex.rxjava3.disposables.Disposable;
|
||||||
import io.reactivex.rxjava3.functions.Action;
|
import io.reactivex.rxjava3.functions.Action;
|
||||||
|
@ -68,29 +68,37 @@ import io.reactivex.rxjava3.functions.Consumer;
|
||||||
import io.reactivex.rxjava3.functions.Function;
|
import io.reactivex.rxjava3.functions.Function;
|
||||||
import io.reactivex.rxjava3.schedulers.Schedulers;
|
import io.reactivex.rxjava3.schedulers.Schedulers;
|
||||||
|
|
||||||
public class ChannelFragment extends BaseListInfoFragment<StreamInfoItem, ChannelInfo>
|
public class ChannelFragment extends BaseStateFragment<ChannelInfo>
|
||||||
implements View.OnClickListener {
|
implements StateSaver.WriteRead {
|
||||||
|
|
||||||
private static final int BUTTON_DEBOUNCE_INTERVAL = 100;
|
private static final int BUTTON_DEBOUNCE_INTERVAL = 100;
|
||||||
private static final String PICASSO_CHANNEL_TAG = "PICASSO_CHANNEL_TAG";
|
private static final String PICASSO_CHANNEL_TAG = "PICASSO_CHANNEL_TAG";
|
||||||
|
|
||||||
|
@State
|
||||||
|
protected int serviceId = Constants.NO_SERVICE_ID;
|
||||||
|
@State
|
||||||
|
protected String name;
|
||||||
|
@State
|
||||||
|
protected String url;
|
||||||
|
|
||||||
|
private ChannelInfo currentInfo;
|
||||||
|
private Disposable currentWorker;
|
||||||
private final CompositeDisposable disposables = new CompositeDisposable();
|
private final CompositeDisposable disposables = new CompositeDisposable();
|
||||||
private Disposable subscribeButtonMonitor;
|
private Disposable subscribeButtonMonitor;
|
||||||
|
private SubscriptionManager subscriptionManager;
|
||||||
|
private int lastTab;
|
||||||
private boolean channelContentNotSupported = false;
|
private boolean channelContentNotSupported = false;
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
// Views
|
// Views
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
private SubscriptionManager subscriptionManager;
|
private FragmentChannelBinding binding;
|
||||||
|
private TabAdapter tabAdapter;
|
||||||
private FragmentChannelBinding channelBinding;
|
|
||||||
private ChannelHeaderBinding headerBinding;
|
|
||||||
private PlaylistControlBinding playlistControlBinding;
|
|
||||||
|
|
||||||
private MenuItem menuRssButton;
|
private MenuItem menuRssButton;
|
||||||
private MenuItem menuNotifyButton;
|
private MenuItem menuNotifyButton;
|
||||||
|
private SubscriptionEntity channelSubscription;
|
||||||
|
|
||||||
public static ChannelFragment getInstance(final int serviceId, final String url,
|
public static ChannelFragment getInstance(final int serviceId, final String url,
|
||||||
final String name) {
|
final String name) {
|
||||||
|
@ -99,22 +107,23 @@ public class ChannelFragment extends BaseListInfoFragment<StreamInfoItem, Channe
|
||||||
return instance;
|
return instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
public ChannelFragment() {
|
private void setInitialData(final int sid, final String u, final String title) {
|
||||||
super(UserAction.REQUESTED_CHANNEL);
|
this.serviceId = sid;
|
||||||
|
this.url = u;
|
||||||
|
this.name = !TextUtils.isEmpty(title) ? title : "";
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onResume() {
|
|
||||||
super.onResume();
|
|
||||||
if (activity != null && useAsFrontPage) {
|
|
||||||
setTitle(currentInfo != null ? currentInfo.getName() : name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
// LifeCycle
|
// LifeCycle
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreate(final Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
setHasOptionsMenu(true);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onAttach(@NonNull final Context context) {
|
public void onAttach(@NonNull final Context context) {
|
||||||
super.onAttach(context);
|
super.onAttach(context);
|
||||||
|
@ -125,49 +134,58 @@ public class ChannelFragment extends BaseListInfoFragment<StreamInfoItem, Channe
|
||||||
public View onCreateView(@NonNull final LayoutInflater inflater,
|
public View onCreateView(@NonNull final LayoutInflater inflater,
|
||||||
@Nullable final ViewGroup container,
|
@Nullable final ViewGroup container,
|
||||||
@Nullable final Bundle savedInstanceState) {
|
@Nullable final Bundle savedInstanceState) {
|
||||||
return inflater.inflate(R.layout.fragment_channel, container, false);
|
binding = FragmentChannelBinding.inflate(inflater, container, false);
|
||||||
|
return binding.getRoot();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override // called from onViewCreated in BaseFragment.onViewCreated
|
||||||
public void onViewCreated(@NonNull final View rootView, final Bundle savedInstanceState) {
|
protected void initViews(final View rootView, final Bundle savedInstanceState) {
|
||||||
super.onViewCreated(rootView, savedInstanceState);
|
super.initViews(rootView, savedInstanceState);
|
||||||
channelBinding = FragmentChannelBinding.bind(rootView);
|
|
||||||
showContentNotSupportedIfNeeded();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
tabAdapter = new TabAdapter(getChildFragmentManager());
|
||||||
public void onDestroy() {
|
binding.viewPager.setAdapter(tabAdapter);
|
||||||
super.onDestroy();
|
binding.tabLayout.setupWithViewPager(binding.viewPager);
|
||||||
disposables.clear();
|
|
||||||
if (subscribeButtonMonitor != null) {
|
setTitle(name);
|
||||||
subscribeButtonMonitor.dispose();
|
binding.channelTitleView.setText(name);
|
||||||
|
if (!ImageStrategy.shouldLoadImages()) {
|
||||||
|
// do not waste space for the banner if it is not going to be loaded
|
||||||
|
binding.channelBannerImage.setImageDrawable(null);
|
||||||
}
|
}
|
||||||
channelBinding = null;
|
|
||||||
headerBinding = null;
|
|
||||||
playlistControlBinding = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
|
||||||
// Init
|
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected Supplier<View> getListHeaderSupplier() {
|
|
||||||
headerBinding = ChannelHeaderBinding
|
|
||||||
.inflate(activity.getLayoutInflater(), itemsList, false);
|
|
||||||
playlistControlBinding = headerBinding.playlistControl;
|
|
||||||
|
|
||||||
return headerBinding::getRoot;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void initListeners() {
|
protected void initListeners() {
|
||||||
super.initListeners();
|
super.initListeners();
|
||||||
|
|
||||||
headerBinding.subChannelTitleView.setOnClickListener(this);
|
final View.OnClickListener openSubChannel = v -> {
|
||||||
headerBinding.subChannelAvatarView.setOnClickListener(this);
|
if (!TextUtils.isEmpty(currentInfo.getParentChannelUrl())) {
|
||||||
|
try {
|
||||||
|
NavigationHelper.openChannelFragment(getFM(), currentInfo.getServiceId(),
|
||||||
|
currentInfo.getParentChannelUrl(),
|
||||||
|
currentInfo.getParentChannelName());
|
||||||
|
} catch (final Exception e) {
|
||||||
|
ErrorUtil.showUiErrorSnackbar(this, "Opening channel fragment", e);
|
||||||
|
}
|
||||||
|
} else if (DEBUG) {
|
||||||
|
Log.i(TAG, "Can't open parent channel because we got no channel URL");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
binding.subChannelAvatarView.setOnClickListener(openSubChannel);
|
||||||
|
binding.subChannelTitleView.setOnClickListener(openSubChannel);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDestroy() {
|
||||||
|
super.onDestroy();
|
||||||
|
if (currentWorker != null) {
|
||||||
|
currentWorker.dispose();
|
||||||
|
}
|
||||||
|
disposables.clear();
|
||||||
|
binding = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
// Menu
|
// Menu
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
@ -176,32 +194,33 @@ public class ChannelFragment extends BaseListInfoFragment<StreamInfoItem, Channe
|
||||||
public void onCreateOptionsMenu(@NonNull final Menu menu,
|
public void onCreateOptionsMenu(@NonNull final Menu menu,
|
||||||
@NonNull final MenuInflater inflater) {
|
@NonNull final MenuInflater inflater) {
|
||||||
super.onCreateOptionsMenu(menu, inflater);
|
super.onCreateOptionsMenu(menu, inflater);
|
||||||
final ActionBar supportActionBar = activity.getSupportActionBar();
|
inflater.inflate(R.menu.menu_channel, menu);
|
||||||
if (useAsFrontPage && supportActionBar != null) {
|
|
||||||
supportActionBar.setDisplayHomeAsUpEnabled(false);
|
|
||||||
} else {
|
|
||||||
inflater.inflate(R.menu.menu_channel, menu);
|
|
||||||
|
|
||||||
if (DEBUG) {
|
if (DEBUG) {
|
||||||
Log.d(TAG, "onCreateOptionsMenu() called with: "
|
Log.d(TAG, "onCreateOptionsMenu() called with: "
|
||||||
+ "menu = [" + menu + "], inflater = [" + inflater + "]");
|
+ "menu = [" + menu + "], inflater = [" + inflater + "]");
|
||||||
}
|
|
||||||
menuRssButton = menu.findItem(R.id.menu_item_rss);
|
|
||||||
menuNotifyButton = menu.findItem(R.id.menu_item_notify);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onOptionsItemSelected(final MenuItem item) {
|
public void onPrepareOptionsMenu(@NonNull final Menu menu) {
|
||||||
|
super.onPrepareOptionsMenu(menu);
|
||||||
|
menuRssButton = menu.findItem(R.id.menu_item_rss);
|
||||||
|
menuNotifyButton = menu.findItem(R.id.menu_item_notify);
|
||||||
|
updateNotifyButton(channelSubscription);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onOptionsItemSelected(@NonNull final MenuItem item) {
|
||||||
switch (item.getItemId()) {
|
switch (item.getItemId()) {
|
||||||
case R.id.action_settings:
|
|
||||||
NavigationHelper.openSettings(requireContext());
|
|
||||||
break;
|
|
||||||
case R.id.menu_item_notify:
|
case R.id.menu_item_notify:
|
||||||
final boolean value = !item.isChecked();
|
final boolean value = !item.isChecked();
|
||||||
item.setEnabled(false);
|
item.setEnabled(false);
|
||||||
setNotify(value);
|
setNotify(value);
|
||||||
break;
|
break;
|
||||||
|
case R.id.action_settings:
|
||||||
|
NavigationHelper.openSettings(requireContext());
|
||||||
|
break;
|
||||||
case R.id.menu_item_rss:
|
case R.id.menu_item_rss:
|
||||||
if (currentInfo != null) {
|
if (currentInfo != null) {
|
||||||
ShareUtils.openUrlInApp(requireContext(), currentInfo.getFeedUrl());
|
ShareUtils.openUrlInApp(requireContext(), currentInfo.getFeedUrl());
|
||||||
|
@ -215,7 +234,7 @@ public class ChannelFragment extends BaseListInfoFragment<StreamInfoItem, Channe
|
||||||
case R.id.menu_item_share:
|
case R.id.menu_item_share:
|
||||||
if (currentInfo != null) {
|
if (currentInfo != null) {
|
||||||
ShareUtils.shareText(requireContext(), name, currentInfo.getOriginalUrl(),
|
ShareUtils.shareText(requireContext(), name, currentInfo.getOriginalUrl(),
|
||||||
currentInfo.getAvatarUrl());
|
currentInfo.getAvatars());
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
|
@ -224,13 +243,14 @@ public class ChannelFragment extends BaseListInfoFragment<StreamInfoItem, Channe
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
// Channel Subscription
|
// Channel Subscription
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
private void monitorSubscription(final ChannelInfo info) {
|
private void monitorSubscription(final ChannelInfo info) {
|
||||||
final Consumer<Throwable> onError = (Throwable throwable) -> {
|
final Consumer<Throwable> onError = (Throwable throwable) -> {
|
||||||
animate(headerBinding.channelSubscribeButton, false, 100);
|
animate(binding.channelSubscribeButton, false, 100);
|
||||||
showSnackBarError(new ErrorInfo(throwable, UserAction.SUBSCRIPTION_GET,
|
showSnackBarError(new ErrorInfo(throwable, UserAction.SUBSCRIPTION_GET,
|
||||||
"Get subscription status", currentInfo));
|
"Get subscription status", currentInfo));
|
||||||
};
|
};
|
||||||
|
@ -263,10 +283,9 @@ public class ChannelFragment extends BaseListInfoFragment<StreamInfoItem, Channe
|
||||||
}, onError));
|
}, onError));
|
||||||
}
|
}
|
||||||
|
|
||||||
private Function<Object, Object> mapOnSubscribe(final SubscriptionEntity subscription,
|
private Function<Object, Object> mapOnSubscribe(final SubscriptionEntity subscription) {
|
||||||
final ChannelInfo info) {
|
|
||||||
return (@NonNull Object o) -> {
|
return (@NonNull Object o) -> {
|
||||||
subscriptionManager.insertSubscription(subscription, info);
|
subscriptionManager.insertSubscription(subscription);
|
||||||
return o;
|
return o;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -298,8 +317,7 @@ public class ChannelFragment extends BaseListInfoFragment<StreamInfoItem, Channe
|
||||||
.subscribe(onComplete, onError));
|
.subscribe(onComplete, onError));
|
||||||
}
|
}
|
||||||
|
|
||||||
private Disposable monitorSubscribeButton(final Button subscribeButton,
|
private Disposable monitorSubscribeButton(final Function<Object, Object> action) {
|
||||||
final Function<Object, Object> action) {
|
|
||||||
final Consumer<Object> onNext = (@NonNull Object o) -> {
|
final Consumer<Object> onNext = (@NonNull Object o) -> {
|
||||||
if (DEBUG) {
|
if (DEBUG) {
|
||||||
Log.d(TAG, "Changed subscription status to this channel!");
|
Log.d(TAG, "Changed subscription status to this channel!");
|
||||||
|
@ -311,7 +329,7 @@ public class ChannelFragment extends BaseListInfoFragment<StreamInfoItem, Channe
|
||||||
"Changing subscription for " + currentInfo.getUrl(), currentInfo));
|
"Changing subscription for " + currentInfo.getUrl(), currentInfo));
|
||||||
|
|
||||||
/* Emit clicks from main thread unto io thread */
|
/* Emit clicks from main thread unto io thread */
|
||||||
return RxView.clicks(subscribeButton)
|
return RxView.clicks(binding.channelSubscribeButton)
|
||||||
.subscribeOn(AndroidSchedulers.mainThread())
|
.subscribeOn(AndroidSchedulers.mainThread())
|
||||||
.observeOn(Schedulers.io())
|
.observeOn(Schedulers.io())
|
||||||
.debounce(BUTTON_DEBOUNCE_INTERVAL, TimeUnit.MILLISECONDS) // Ignore rapid clicks
|
.debounce(BUTTON_DEBOUNCE_INTERVAL, TimeUnit.MILLISECONDS) // Ignore rapid clicks
|
||||||
|
@ -337,20 +355,20 @@ public class ChannelFragment extends BaseListInfoFragment<StreamInfoItem, Channe
|
||||||
channel.setServiceId(info.getServiceId());
|
channel.setServiceId(info.getServiceId());
|
||||||
channel.setUrl(info.getUrl());
|
channel.setUrl(info.getUrl());
|
||||||
channel.setData(info.getName(),
|
channel.setData(info.getName(),
|
||||||
info.getAvatarUrl(),
|
ImageStrategy.imageListToDbUrl(info.getAvatars()),
|
||||||
info.getDescription(),
|
info.getDescription(),
|
||||||
info.getSubscriberCount());
|
info.getSubscriberCount());
|
||||||
|
channelSubscription = null;
|
||||||
updateNotifyButton(null);
|
updateNotifyButton(null);
|
||||||
subscribeButtonMonitor = monitorSubscribeButton(
|
subscribeButtonMonitor = monitorSubscribeButton(mapOnSubscribe(channel));
|
||||||
headerBinding.channelSubscribeButton, mapOnSubscribe(channel, info));
|
|
||||||
} else {
|
} else {
|
||||||
if (DEBUG) {
|
if (DEBUG) {
|
||||||
Log.d(TAG, "Found subscription to this channel!");
|
Log.d(TAG, "Found subscription to this channel!");
|
||||||
}
|
}
|
||||||
final SubscriptionEntity subscription = subscriptionEntities.get(0);
|
channelSubscription = subscriptionEntities.get(0);
|
||||||
updateNotifyButton(subscription);
|
updateNotifyButton(channelSubscription);
|
||||||
subscribeButtonMonitor = monitorSubscribeButton(
|
subscribeButtonMonitor =
|
||||||
headerBinding.channelSubscribeButton, mapOnUnsubscribe(subscription));
|
monitorSubscribeButton(mapOnUnsubscribe(channelSubscription));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -361,34 +379,33 @@ public class ChannelFragment extends BaseListInfoFragment<StreamInfoItem, Channe
|
||||||
+ "isSubscribed = [" + isSubscribed + "]");
|
+ "isSubscribed = [" + isSubscribed + "]");
|
||||||
}
|
}
|
||||||
|
|
||||||
final boolean isButtonVisible = headerBinding.channelSubscribeButton.getVisibility()
|
final boolean isButtonVisible = binding.channelSubscribeButton.getVisibility()
|
||||||
== View.VISIBLE;
|
== View.VISIBLE;
|
||||||
final int backgroundDuration = isButtonVisible ? 300 : 0;
|
final int backgroundDuration = isButtonVisible ? 300 : 0;
|
||||||
final int textDuration = isButtonVisible ? 200 : 0;
|
final int textDuration = isButtonVisible ? 200 : 0;
|
||||||
|
|
||||||
final int subscribeBackground = ThemeHelper
|
|
||||||
.resolveColorFromAttr(activity, R.attr.colorPrimary);
|
|
||||||
final int subscribeText = ContextCompat.getColor(activity, R.color.subscribe_text_color);
|
|
||||||
final int subscribedBackground = ContextCompat
|
final int subscribedBackground = ContextCompat
|
||||||
.getColor(activity, R.color.subscribed_background_color);
|
.getColor(activity, R.color.subscribed_background_color);
|
||||||
final int subscribedText = ContextCompat.getColor(activity, R.color.subscribed_text_color);
|
final int subscribedText = ContextCompat.getColor(activity, R.color.subscribed_text_color);
|
||||||
|
final int subscribeBackground = ColorUtils.blendARGB(ThemeHelper
|
||||||
|
.resolveColorFromAttr(activity, R.attr.colorPrimary), subscribedBackground, 0.35f);
|
||||||
|
final int subscribeText = ContextCompat.getColor(activity, R.color.subscribe_text_color);
|
||||||
|
|
||||||
if (!isSubscribed) {
|
if (isSubscribed) {
|
||||||
headerBinding.channelSubscribeButton.setText(R.string.subscribe_button_title);
|
binding.channelSubscribeButton.setText(R.string.subscribed_button_title);
|
||||||
animateBackgroundColor(headerBinding.channelSubscribeButton, backgroundDuration,
|
animateBackgroundColor(binding.channelSubscribeButton, backgroundDuration,
|
||||||
subscribedBackground, subscribeBackground);
|
|
||||||
animateTextColor(headerBinding.channelSubscribeButton, textDuration, subscribedText,
|
|
||||||
subscribeText);
|
|
||||||
} else {
|
|
||||||
headerBinding.channelSubscribeButton.setText(R.string.subscribed_button_title);
|
|
||||||
animateBackgroundColor(headerBinding.channelSubscribeButton, backgroundDuration,
|
|
||||||
subscribeBackground, subscribedBackground);
|
subscribeBackground, subscribedBackground);
|
||||||
animateTextColor(headerBinding.channelSubscribeButton, textDuration, subscribeText,
|
animateTextColor(binding.channelSubscribeButton, textDuration, subscribeText,
|
||||||
subscribedText);
|
subscribedText);
|
||||||
|
} else {
|
||||||
|
binding.channelSubscribeButton.setText(R.string.subscribe_button_title);
|
||||||
|
animateBackgroundColor(binding.channelSubscribeButton, backgroundDuration,
|
||||||
|
subscribedBackground, subscribeBackground);
|
||||||
|
animateTextColor(binding.channelSubscribeButton, textDuration, subscribedText,
|
||||||
|
subscribeText);
|
||||||
}
|
}
|
||||||
|
|
||||||
animate(headerBinding.channelSubscribeButton, true, 100,
|
animate(binding.channelSubscribeButton, true, 100, AnimationType.LIGHT_SCALE_AND_ALPHA);
|
||||||
AnimationType.LIGHT_SCALE_AND_ALPHA);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateNotifyButton(@Nullable final SubscriptionEntity subscription) {
|
private void updateNotifyButton(@Nullable final SubscriptionEntity subscription) {
|
||||||
|
@ -424,108 +441,179 @@ public class ChannelFragment extends BaseListInfoFragment<StreamInfoItem, Channe
|
||||||
* Show a snackbar with the option to enable notifications on new streams for this channel.
|
* Show a snackbar with the option to enable notifications on new streams for this channel.
|
||||||
*/
|
*/
|
||||||
private void showNotifySnackbar() {
|
private void showNotifySnackbar() {
|
||||||
Snackbar.make(itemsList, R.string.you_successfully_subscribed, Snackbar.LENGTH_LONG)
|
Snackbar.make(binding.getRoot(), R.string.you_successfully_subscribed, Snackbar.LENGTH_LONG)
|
||||||
.setAction(R.string.get_notified, v -> setNotify(true))
|
.setAction(R.string.get_notified, v -> setNotify(true))
|
||||||
.setActionTextColor(Color.YELLOW)
|
.setActionTextColor(Color.YELLOW)
|
||||||
.show();
|
.show();
|
||||||
}
|
}
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
|
||||||
// Load and handle
|
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected Single<ListExtractor.InfoItemsPage<StreamInfoItem>> loadMoreItemsLogic() {
|
|
||||||
return ExtractorHelper.getMoreChannelItems(serviceId, url, currentNextPage);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected Single<ChannelInfo> loadResult(final boolean forceLoad) {
|
|
||||||
return ExtractorHelper.getChannelInfo(serviceId, url, forceLoad);
|
|
||||||
}
|
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
// OnClick
|
// Init
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
@Override
|
private void updateTabs() {
|
||||||
public void onClick(final View v) {
|
tabAdapter.clearAllItems();
|
||||||
if (isLoading.get() || currentInfo == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (v.getId()) {
|
if (currentInfo != null && !channelContentNotSupported) {
|
||||||
case R.id.sub_channel_avatar_view:
|
final Context context = requireContext();
|
||||||
case R.id.sub_channel_title_view:
|
final SharedPreferences preferences = PreferenceManager
|
||||||
if (!TextUtils.isEmpty(currentInfo.getParentChannelUrl())) {
|
.getDefaultSharedPreferences(context);
|
||||||
try {
|
|
||||||
NavigationHelper.openChannelFragment(getFM(), currentInfo.getServiceId(),
|
for (final ListLinkHandler linkHandler : currentInfo.getTabs()) {
|
||||||
currentInfo.getParentChannelUrl(),
|
final String tab = linkHandler.getContentFilters().get(0);
|
||||||
currentInfo.getParentChannelName());
|
if (ChannelTabHelper.showChannelTab(context, preferences, tab)) {
|
||||||
} catch (final Exception e) {
|
final ChannelTabFragment channelTabFragment =
|
||||||
ErrorUtil.showUiErrorSnackbar(this, "Opening channel fragment", e);
|
ChannelTabFragment.getInstance(serviceId, linkHandler, name);
|
||||||
}
|
channelTabFragment.useAsFrontPage(useAsFrontPage);
|
||||||
} else if (DEBUG) {
|
tabAdapter.addFragment(channelTabFragment,
|
||||||
Log.i(TAG, "Can't open parent channel because we got no channel URL");
|
context.getString(ChannelTabHelper.getTranslationKey(tab)));
|
||||||
}
|
}
|
||||||
break;
|
}
|
||||||
|
|
||||||
|
if (ChannelTabHelper.showChannelTab(
|
||||||
|
context, preferences, R.string.show_channel_tabs_about)) {
|
||||||
|
tabAdapter.addFragment(
|
||||||
|
ChannelAboutFragment.getInstance(currentInfo),
|
||||||
|
context.getString(R.string.channel_tab_about));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tabAdapter.notifyDataSetUpdate();
|
||||||
|
|
||||||
|
for (int i = 0; i < tabAdapter.getCount(); i++) {
|
||||||
|
binding.tabLayout.getTabAt(i).setText(tabAdapter.getItemTitle(i));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restore previously selected tab
|
||||||
|
final TabLayout.Tab ltab = binding.tabLayout.getTabAt(lastTab);
|
||||||
|
if (ltab != null) {
|
||||||
|
binding.tabLayout.selectTab(ltab);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
|
// State Saving
|
||||||
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String generateSuffix() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void writeTo(final Queue<Object> objectsToSave) {
|
||||||
|
objectsToSave.add(currentInfo);
|
||||||
|
objectsToSave.add(binding == null ? 0 : binding.tabLayout.getSelectedTabPosition());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void readFrom(@NonNull final Queue<Object> savedObjects) {
|
||||||
|
currentInfo = (ChannelInfo) savedObjects.poll();
|
||||||
|
lastTab = (Integer) savedObjects.poll();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onSaveInstanceState(final @NonNull Bundle outState) {
|
||||||
|
super.onSaveInstanceState(outState);
|
||||||
|
if (binding != null) {
|
||||||
|
outState.putInt("LastTab", binding.tabLayout.getSelectedTabPosition());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onRestoreInstanceState(@NonNull final Bundle savedInstanceState) {
|
||||||
|
super.onRestoreInstanceState(savedInstanceState);
|
||||||
|
lastTab = savedInstanceState.getInt("LastTab", 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
// Contract
|
// Contract
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void doInitialLoadLogic() {
|
||||||
|
if (currentInfo == null) {
|
||||||
|
startLoading(false);
|
||||||
|
} else {
|
||||||
|
handleResult(currentInfo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void startLoading(final boolean forceLoad) {
|
||||||
|
super.startLoading(forceLoad);
|
||||||
|
|
||||||
|
currentInfo = null;
|
||||||
|
updateTabs();
|
||||||
|
if (currentWorker != null) {
|
||||||
|
currentWorker.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
runWorker(forceLoad);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void runWorker(final boolean forceLoad) {
|
||||||
|
currentWorker = ExtractorHelper.getChannelInfo(serviceId, url, forceLoad)
|
||||||
|
.subscribeOn(Schedulers.io())
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribe(result -> {
|
||||||
|
isLoading.set(false);
|
||||||
|
handleResult(result);
|
||||||
|
}, throwable -> showError(new ErrorInfo(throwable, UserAction.REQUESTED_CHANNEL,
|
||||||
|
url == null ? "No URL" : url, serviceId)));
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void showLoading() {
|
public void showLoading() {
|
||||||
super.showLoading();
|
super.showLoading();
|
||||||
PicassoHelper.cancelTag(PICASSO_CHANNEL_TAG);
|
PicassoHelper.cancelTag(PICASSO_CHANNEL_TAG);
|
||||||
animate(headerBinding.channelSubscribeButton, false, 100);
|
animate(binding.channelSubscribeButton, false, 100);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void handleResult(@NonNull final ChannelInfo result) {
|
public void handleResult(@NonNull final ChannelInfo result) {
|
||||||
super.handleResult(result);
|
super.handleResult(result);
|
||||||
|
currentInfo = result;
|
||||||
|
setInitialData(result.getServiceId(), result.getOriginalUrl(), result.getName());
|
||||||
|
|
||||||
headerBinding.getRoot().setVisibility(View.VISIBLE);
|
if (ImageStrategy.shouldLoadImages() && !result.getBanners().isEmpty()) {
|
||||||
PicassoHelper.loadBanner(result.getBannerUrl()).tag(PICASSO_CHANNEL_TAG)
|
PicassoHelper.loadBanner(result.getBanners()).tag(PICASSO_CHANNEL_TAG)
|
||||||
.into(headerBinding.channelBannerImage);
|
.into(binding.channelBannerImage);
|
||||||
PicassoHelper.loadAvatar(result.getAvatarUrl()).tag(PICASSO_CHANNEL_TAG)
|
} else {
|
||||||
.into(headerBinding.channelAvatarView);
|
// do not waste space for the banner, if the user disabled images or there is not one
|
||||||
PicassoHelper.loadAvatar(result.getParentChannelAvatarUrl()).tag(PICASSO_CHANNEL_TAG)
|
binding.channelBannerImage.setImageDrawable(null);
|
||||||
.into(headerBinding.subChannelAvatarView);
|
}
|
||||||
|
|
||||||
headerBinding.channelSubscriberView.setVisibility(View.VISIBLE);
|
PicassoHelper.loadAvatar(result.getAvatars()).tag(PICASSO_CHANNEL_TAG)
|
||||||
|
.into(binding.channelAvatarView);
|
||||||
|
PicassoHelper.loadAvatar(result.getParentChannelAvatars()).tag(PICASSO_CHANNEL_TAG)
|
||||||
|
.into(binding.subChannelAvatarView);
|
||||||
|
|
||||||
|
binding.channelTitleView.setText(result.getName());
|
||||||
|
binding.channelSubscriberView.setVisibility(View.VISIBLE);
|
||||||
if (result.getSubscriberCount() >= 0) {
|
if (result.getSubscriberCount() >= 0) {
|
||||||
headerBinding.channelSubscriberView.setText(Localization
|
binding.channelSubscriberView.setText(Localization
|
||||||
.shortSubscriberCount(activity, result.getSubscriberCount()));
|
.shortSubscriberCount(activity, result.getSubscriberCount()));
|
||||||
} else {
|
} else {
|
||||||
headerBinding.channelSubscriberView.setText(R.string.subscribers_count_not_available);
|
binding.channelSubscriberView.setText(R.string.subscribers_count_not_available);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!TextUtils.isEmpty(currentInfo.getParentChannelName())) {
|
if (!TextUtils.isEmpty(currentInfo.getParentChannelName())) {
|
||||||
headerBinding.subChannelTitleView.setText(String.format(
|
binding.subChannelTitleView.setText(String.format(
|
||||||
getString(R.string.channel_created_by),
|
getString(R.string.channel_created_by),
|
||||||
currentInfo.getParentChannelName())
|
currentInfo.getParentChannelName())
|
||||||
);
|
);
|
||||||
headerBinding.subChannelTitleView.setVisibility(View.VISIBLE);
|
binding.subChannelTitleView.setVisibility(View.VISIBLE);
|
||||||
headerBinding.subChannelAvatarView.setVisibility(View.VISIBLE);
|
binding.subChannelAvatarView.setVisibility(View.VISIBLE);
|
||||||
} else {
|
|
||||||
headerBinding.subChannelTitleView.setVisibility(View.GONE);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (menuRssButton != null) {
|
if (menuRssButton != null) {
|
||||||
menuRssButton.setVisible(!TextUtils.isEmpty(result.getFeedUrl()));
|
menuRssButton.setVisible(!TextUtils.isEmpty(result.getFeedUrl()));
|
||||||
}
|
}
|
||||||
|
|
||||||
// PlaylistControls should be visible only if there is some item in
|
|
||||||
// infoListAdapter other than header
|
|
||||||
if (infoListAdapter.getItemCount() != 1) {
|
|
||||||
playlistControlBinding.getRoot().setVisibility(View.VISIBLE);
|
|
||||||
} else {
|
|
||||||
playlistControlBinding.getRoot().setVisibility(View.GONE);
|
|
||||||
}
|
|
||||||
|
|
||||||
channelContentNotSupported = false;
|
channelContentNotSupported = false;
|
||||||
for (final Throwable throwable : result.getErrors()) {
|
for (final Throwable throwable : result.getErrors()) {
|
||||||
if (throwable instanceof ContentNotSupportedException) {
|
if (throwable instanceof ContentNotSupportedException) {
|
||||||
|
@ -539,62 +627,21 @@ public class ChannelFragment extends BaseListInfoFragment<StreamInfoItem, Channe
|
||||||
if (subscribeButtonMonitor != null) {
|
if (subscribeButtonMonitor != null) {
|
||||||
subscribeButtonMonitor.dispose();
|
subscribeButtonMonitor.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateTabs();
|
||||||
updateSubscription(result);
|
updateSubscription(result);
|
||||||
monitorSubscription(result);
|
monitorSubscription(result);
|
||||||
|
|
||||||
playlistControlBinding.playlistCtrlPlayAllButton
|
|
||||||
.setOnClickListener(view -> NavigationHelper
|
|
||||||
.playOnMainPlayer(activity, getPlayQueue()));
|
|
||||||
playlistControlBinding.playlistCtrlPlayPopupButton
|
|
||||||
.setOnClickListener(view -> NavigationHelper
|
|
||||||
.playOnPopupPlayer(activity, getPlayQueue(), false));
|
|
||||||
playlistControlBinding.playlistCtrlPlayBgButton
|
|
||||||
.setOnClickListener(view -> NavigationHelper
|
|
||||||
.playOnBackgroundPlayer(activity, getPlayQueue(), false));
|
|
||||||
|
|
||||||
playlistControlBinding.playlistCtrlPlayPopupButton.setOnLongClickListener(view -> {
|
|
||||||
NavigationHelper.enqueueOnPlayer(activity, getPlayQueue(), PlayerType.POPUP);
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
|
|
||||||
playlistControlBinding.playlistCtrlPlayBgButton.setOnLongClickListener(view -> {
|
|
||||||
NavigationHelper.enqueueOnPlayer(activity, getPlayQueue(), PlayerType.AUDIO);
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void showContentNotSupportedIfNeeded() {
|
private void showContentNotSupportedIfNeeded() {
|
||||||
// channelBinding might not be initialized when handleResult() is called
|
// channelBinding might not be initialized when handleResult() is called
|
||||||
// (e.g. after rotating the screen, #6696)
|
// (e.g. after rotating the screen, #6696)
|
||||||
if (!channelContentNotSupported || channelBinding == null) {
|
if (!channelContentNotSupported || binding == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
channelBinding.errorContentNotSupported.setVisibility(View.VISIBLE);
|
binding.errorContentNotSupported.setVisibility(View.VISIBLE);
|
||||||
channelBinding.channelKaomoji.setText("(︶︹︺)");
|
binding.channelKaomoji.setText("(︶︹︺)");
|
||||||
channelBinding.channelKaomoji.setTextSize(TypedValue.COMPLEX_UNIT_SP, 45f);
|
binding.channelKaomoji.setTextSize(TypedValue.COMPLEX_UNIT_SP, 45f);
|
||||||
channelBinding.channelNoVideos.setVisibility(View.GONE);
|
|
||||||
}
|
|
||||||
|
|
||||||
private PlayQueue getPlayQueue() {
|
|
||||||
final List<StreamInfoItem> streamItems = infoListAdapter.getItemsList().stream()
|
|
||||||
.filter(StreamInfoItem.class::isInstance)
|
|
||||||
.map(StreamInfoItem.class::cast)
|
|
||||||
.collect(Collectors.toList());
|
|
||||||
|
|
||||||
return new ChannelPlayQueue(currentInfo.getServiceId(), currentInfo.getUrl(),
|
|
||||||
currentInfo.getNextPage(), streamItems, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
|
||||||
// Utils
|
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void setTitle(final String title) {
|
|
||||||
super.setTitle(title);
|
|
||||||
if (!useAsFrontPage) {
|
|
||||||
headerBinding.channelTitleView.setText(title);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,164 @@
|
||||||
|
package org.schabi.newpipe.fragments.list.channel;
|
||||||
|
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.util.Log;
|
||||||
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
|
import org.schabi.newpipe.R;
|
||||||
|
import org.schabi.newpipe.databinding.PlaylistControlBinding;
|
||||||
|
import org.schabi.newpipe.error.UserAction;
|
||||||
|
import org.schabi.newpipe.extractor.InfoItem;
|
||||||
|
import org.schabi.newpipe.extractor.ListExtractor;
|
||||||
|
import org.schabi.newpipe.extractor.channel.tabs.ChannelTabInfo;
|
||||||
|
import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
||||||
|
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler;
|
||||||
|
import org.schabi.newpipe.extractor.linkhandler.ReadyChannelTabListLinkHandler;
|
||||||
|
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||||
|
import org.schabi.newpipe.fragments.list.BaseListInfoFragment;
|
||||||
|
import org.schabi.newpipe.fragments.list.playlist.PlaylistControlViewHolder;
|
||||||
|
import org.schabi.newpipe.player.playqueue.ChannelTabPlayQueue;
|
||||||
|
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
||||||
|
import org.schabi.newpipe.util.ChannelTabHelper;
|
||||||
|
import org.schabi.newpipe.util.ExtractorHelper;
|
||||||
|
import org.schabi.newpipe.util.PlayButtonHelper;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.function.Supplier;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import icepick.State;
|
||||||
|
import io.reactivex.rxjava3.core.Single;
|
||||||
|
|
||||||
|
public class ChannelTabFragment extends BaseListInfoFragment<InfoItem, ChannelTabInfo>
|
||||||
|
implements PlaylistControlViewHolder {
|
||||||
|
|
||||||
|
// states must be protected and not private for IcePick being able to access them
|
||||||
|
@State
|
||||||
|
protected ListLinkHandler tabHandler;
|
||||||
|
@State
|
||||||
|
protected String channelName;
|
||||||
|
|
||||||
|
private PlaylistControlBinding playlistControlBinding;
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public static ChannelTabFragment getInstance(final int serviceId,
|
||||||
|
final ListLinkHandler tabHandler,
|
||||||
|
final String channelName) {
|
||||||
|
final ChannelTabFragment instance = new ChannelTabFragment();
|
||||||
|
instance.serviceId = serviceId;
|
||||||
|
instance.tabHandler = tabHandler;
|
||||||
|
instance.channelName = channelName;
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ChannelTabFragment() {
|
||||||
|
super(UserAction.REQUESTED_CHANNEL);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
|
// LifeCycle
|
||||||
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreate(final Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
setHasOptionsMenu(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public View onCreateView(@NonNull final LayoutInflater inflater,
|
||||||
|
@Nullable final ViewGroup container,
|
||||||
|
@Nullable final Bundle savedInstanceState) {
|
||||||
|
return inflater.inflate(R.layout.fragment_channel_tab, container, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDestroyView() {
|
||||||
|
super.onDestroyView();
|
||||||
|
playlistControlBinding = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Supplier<View> getListHeaderSupplier() {
|
||||||
|
if (ChannelTabHelper.isStreamsTab(tabHandler)) {
|
||||||
|
playlistControlBinding = PlaylistControlBinding
|
||||||
|
.inflate(activity.getLayoutInflater(), itemsList, false);
|
||||||
|
return playlistControlBinding::getRoot;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Single<ChannelTabInfo> loadResult(final boolean forceLoad) {
|
||||||
|
return ExtractorHelper.getChannelTab(serviceId, tabHandler, forceLoad);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Single<ListExtractor.InfoItemsPage<InfoItem>> loadMoreItemsLogic() {
|
||||||
|
return ExtractorHelper.getMoreChannelTabItems(serviceId, tabHandler, currentNextPage);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setTitle(final String title) {
|
||||||
|
// The channel name is displayed as title in the toolbar.
|
||||||
|
// The title is always a description of the content of the tab fragment.
|
||||||
|
// It should be unique for each channel because multiple channel tabs
|
||||||
|
// can be added to the main page. Therefore, the channel name is used.
|
||||||
|
// Using the title variable would cause the title to be the same for all channel tabs.
|
||||||
|
super.setTitle(channelName);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void handleResult(@NonNull final ChannelTabInfo result) {
|
||||||
|
super.handleResult(result);
|
||||||
|
|
||||||
|
// FIXME this is a really hacky workaround, to avoid storing useless data in the fragment
|
||||||
|
// state. The problem is, `ReadyChannelTabListLinkHandler` might contain raw JSON data that
|
||||||
|
// uses a lot of memory (e.g. ~800KB for YouTube). While 800KB doesn't seem much, if
|
||||||
|
// you combine just a couple of channel tab fragments you easily go over the 1MB
|
||||||
|
// save&restore transaction limit, and get `TransactionTooLargeException`s. A proper
|
||||||
|
// solution would require rethinking about `ReadyChannelTabListLinkHandler`s.
|
||||||
|
if (tabHandler instanceof ReadyChannelTabListLinkHandler) {
|
||||||
|
try {
|
||||||
|
// once `handleResult` is called, the parsed data was already saved to cache, so
|
||||||
|
// we can discard any raw data in ReadyChannelTabListLinkHandler and create a
|
||||||
|
// link handler with identical properties, but without any raw data
|
||||||
|
tabHandler = result.getService()
|
||||||
|
.getChannelTabLHFactory()
|
||||||
|
.fromQuery(tabHandler.getId(), tabHandler.getContentFilters(),
|
||||||
|
tabHandler.getSortFilter());
|
||||||
|
} catch (final ParsingException e) {
|
||||||
|
// silently ignore the error, as the app can continue to function normally
|
||||||
|
Log.w(TAG, "Could not recreate channel tab handler", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (playlistControlBinding != null) {
|
||||||
|
// PlaylistControls should be visible only if there is some item in
|
||||||
|
// infoListAdapter other than header
|
||||||
|
if (infoListAdapter.getItemCount() > 1) {
|
||||||
|
playlistControlBinding.getRoot().setVisibility(View.VISIBLE);
|
||||||
|
} else {
|
||||||
|
playlistControlBinding.getRoot().setVisibility(View.GONE);
|
||||||
|
}
|
||||||
|
|
||||||
|
PlayButtonHelper.initPlaylistControlClickListener(
|
||||||
|
activity, playlistControlBinding, this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public PlayQueue getPlayQueue() {
|
||||||
|
final List<StreamInfoItem> streamItems = infoListAdapter.getItemsList().stream()
|
||||||
|
.filter(StreamInfoItem.class::isInstance)
|
||||||
|
.map(StreamInfoItem.class::cast)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
return new ChannelTabPlayQueue(currentInfo.getServiceId(), tabHandler,
|
||||||
|
currentInfo.getNextPage(), streamItems, 0);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,168 @@
|
||||||
|
package org.schabi.newpipe.fragments.list.comments;
|
||||||
|
|
||||||
|
import static org.schabi.newpipe.util.ServiceHelper.getServiceById;
|
||||||
|
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.constraintlayout.widget.ConstraintLayout;
|
||||||
|
import androidx.core.text.HtmlCompat;
|
||||||
|
|
||||||
|
import org.schabi.newpipe.R;
|
||||||
|
import org.schabi.newpipe.databinding.CommentRepliesHeaderBinding;
|
||||||
|
import org.schabi.newpipe.error.UserAction;
|
||||||
|
import org.schabi.newpipe.extractor.ListExtractor;
|
||||||
|
import org.schabi.newpipe.extractor.comments.CommentsInfoItem;
|
||||||
|
import org.schabi.newpipe.fragments.list.BaseListInfoFragment;
|
||||||
|
import org.schabi.newpipe.info_list.ItemViewMode;
|
||||||
|
import org.schabi.newpipe.util.DeviceUtils;
|
||||||
|
import org.schabi.newpipe.util.ExtractorHelper;
|
||||||
|
import org.schabi.newpipe.util.Localization;
|
||||||
|
import org.schabi.newpipe.util.NavigationHelper;
|
||||||
|
import org.schabi.newpipe.util.image.ImageStrategy;
|
||||||
|
import org.schabi.newpipe.util.image.PicassoHelper;
|
||||||
|
import org.schabi.newpipe.util.text.TextLinkifier;
|
||||||
|
|
||||||
|
import java.util.Queue;
|
||||||
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
|
import io.reactivex.rxjava3.core.Single;
|
||||||
|
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
||||||
|
|
||||||
|
public final class CommentRepliesFragment
|
||||||
|
extends BaseListInfoFragment<CommentsInfoItem, CommentRepliesInfo> {
|
||||||
|
|
||||||
|
public static final String TAG = CommentRepliesFragment.class.getSimpleName();
|
||||||
|
|
||||||
|
private CommentsInfoItem commentsInfoItem; // the comment to show replies of
|
||||||
|
private final CompositeDisposable disposables = new CompositeDisposable();
|
||||||
|
|
||||||
|
|
||||||
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
|
// Constructors and lifecycle
|
||||||
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
|
// only called by the Android framework, after which readFrom is called and restores all data
|
||||||
|
public CommentRepliesFragment() {
|
||||||
|
super(UserAction.REQUESTED_COMMENT_REPLIES);
|
||||||
|
}
|
||||||
|
|
||||||
|
public CommentRepliesFragment(@NonNull final CommentsInfoItem commentsInfoItem) {
|
||||||
|
this();
|
||||||
|
this.commentsInfoItem = commentsInfoItem;
|
||||||
|
// setting "" as title since the title will be properly set right after
|
||||||
|
setInitialData(commentsInfoItem.getServiceId(), commentsInfoItem.getUrl(), "");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
@Override
|
||||||
|
public View onCreateView(@NonNull final LayoutInflater inflater,
|
||||||
|
@Nullable final ViewGroup container,
|
||||||
|
@Nullable final Bundle savedInstanceState) {
|
||||||
|
return inflater.inflate(R.layout.fragment_comments, container, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDestroyView() {
|
||||||
|
disposables.clear();
|
||||||
|
super.onDestroyView();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Supplier<View> getListHeaderSupplier() {
|
||||||
|
return () -> {
|
||||||
|
final CommentRepliesHeaderBinding binding = CommentRepliesHeaderBinding
|
||||||
|
.inflate(activity.getLayoutInflater(), itemsList, false);
|
||||||
|
final CommentsInfoItem item = commentsInfoItem;
|
||||||
|
|
||||||
|
// load the author avatar
|
||||||
|
PicassoHelper.loadAvatar(item.getUploaderAvatars()).into(binding.authorAvatar);
|
||||||
|
binding.authorAvatar.setVisibility(ImageStrategy.shouldLoadImages()
|
||||||
|
? View.VISIBLE : View.GONE);
|
||||||
|
|
||||||
|
// setup author name and comment date
|
||||||
|
binding.authorName.setText(item.getUploaderName());
|
||||||
|
binding.uploadDate.setText(Localization.relativeTimeOrTextual(
|
||||||
|
getContext(), item.getUploadDate(), item.getTextualUploadDate()));
|
||||||
|
binding.authorTouchArea.setOnClickListener(
|
||||||
|
v -> NavigationHelper.openCommentAuthorIfPresent(requireActivity(), item));
|
||||||
|
|
||||||
|
// setup like count, hearted and pinned
|
||||||
|
binding.thumbsUpCount.setText(
|
||||||
|
Localization.likeCount(requireContext(), item.getLikeCount()));
|
||||||
|
// for heartImage goneMarginEnd was used, but there is no way to tell ConstraintLayout
|
||||||
|
// not to use a different margin only when both the next two views are gone
|
||||||
|
((ConstraintLayout.LayoutParams) binding.thumbsUpCount.getLayoutParams())
|
||||||
|
.setMarginEnd(DeviceUtils.dpToPx(
|
||||||
|
(item.isHeartedByUploader() || item.isPinned() ? 8 : 16),
|
||||||
|
requireContext()));
|
||||||
|
binding.heartImage.setVisibility(item.isHeartedByUploader() ? View.VISIBLE : View.GONE);
|
||||||
|
binding.pinnedImage.setVisibility(item.isPinned() ? View.VISIBLE : View.GONE);
|
||||||
|
|
||||||
|
// setup comment content
|
||||||
|
TextLinkifier.fromDescription(binding.commentContent, item.getCommentText(),
|
||||||
|
HtmlCompat.FROM_HTML_MODE_LEGACY, getServiceById(item.getServiceId()),
|
||||||
|
item.getUrl(), disposables, null);
|
||||||
|
|
||||||
|
return binding.getRoot();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
|
// State saving
|
||||||
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void writeTo(final Queue<Object> objectsToSave) {
|
||||||
|
super.writeTo(objectsToSave);
|
||||||
|
objectsToSave.add(commentsInfoItem);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void readFrom(@NonNull final Queue<Object> savedObjects) throws Exception {
|
||||||
|
super.readFrom(savedObjects);
|
||||||
|
commentsInfoItem = (CommentsInfoItem) savedObjects.poll();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
|
// Data loading
|
||||||
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Single<CommentRepliesInfo> loadResult(final boolean forceLoad) {
|
||||||
|
return Single.fromCallable(() -> new CommentRepliesInfo(commentsInfoItem,
|
||||||
|
// the reply count string will be shown as the activity title
|
||||||
|
Localization.replyCount(requireContext(), commentsInfoItem.getReplyCount())));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Single<ListExtractor.InfoItemsPage<CommentsInfoItem>> loadMoreItemsLogic() {
|
||||||
|
// commentsInfoItem.getUrl() should contain the url of the original
|
||||||
|
// ListInfo<CommentsInfoItem>, which should be the stream url
|
||||||
|
return ExtractorHelper.getMoreCommentItems(
|
||||||
|
serviceId, commentsInfoItem.getUrl(), currentNextPage);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
|
// Utils
|
||||||
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected ItemViewMode getItemViewMode() {
|
||||||
|
return ItemViewMode.LIST;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return the comment to which the replies are shown
|
||||||
|
*/
|
||||||
|
public CommentsInfoItem getCommentsInfoItem() {
|
||||||
|
return commentsInfoItem;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,22 @@
|
||||||
|
package org.schabi.newpipe.fragments.list.comments;
|
||||||
|
|
||||||
|
import org.schabi.newpipe.extractor.ListInfo;
|
||||||
|
import org.schabi.newpipe.extractor.comments.CommentsInfoItem;
|
||||||
|
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
|
||||||
|
public final class CommentRepliesInfo extends ListInfo<CommentsInfoItem> {
|
||||||
|
/**
|
||||||
|
* This class is used to wrap the comment replies page into a ListInfo object.
|
||||||
|
*
|
||||||
|
* @param comment the comment from which to get replies
|
||||||
|
* @param name will be shown as the fragment title
|
||||||
|
*/
|
||||||
|
public CommentRepliesInfo(final CommentsInfoItem comment, final String name) {
|
||||||
|
super(comment.getServiceId(),
|
||||||
|
new ListLinkHandler("", "", "", Collections.emptyList(), null), name);
|
||||||
|
setNextPage(comment.getReplies());
|
||||||
|
setRelatedItems(Collections.emptyList()); // since it must be non-null
|
||||||
|
}
|
||||||
|
}
|
|
@ -110,4 +110,14 @@ public class CommentsFragment extends BaseListInfoFragment<CommentsInfoItem, Com
|
||||||
protected ItemViewMode getItemViewMode() {
|
protected ItemViewMode getItemViewMode() {
|
||||||
return ItemViewMode.LIST;
|
return ItemViewMode.LIST;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean scrollToComment(final CommentsInfoItem comment) {
|
||||||
|
final int position = infoListAdapter.getItemsList().indexOf(comment);
|
||||||
|
if (position < 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
itemsList.scrollToPosition(position);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,11 +16,13 @@ import org.schabi.newpipe.error.ErrorInfo;
|
||||||
import org.schabi.newpipe.error.UserAction;
|
import org.schabi.newpipe.error.UserAction;
|
||||||
import org.schabi.newpipe.extractor.ListExtractor;
|
import org.schabi.newpipe.extractor.ListExtractor;
|
||||||
import org.schabi.newpipe.extractor.NewPipe;
|
import org.schabi.newpipe.extractor.NewPipe;
|
||||||
|
import org.schabi.newpipe.extractor.ServiceList;
|
||||||
import org.schabi.newpipe.extractor.StreamingService;
|
import org.schabi.newpipe.extractor.StreamingService;
|
||||||
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||||
import org.schabi.newpipe.extractor.kiosk.KioskInfo;
|
import org.schabi.newpipe.extractor.kiosk.KioskInfo;
|
||||||
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandlerFactory;
|
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandlerFactory;
|
||||||
import org.schabi.newpipe.extractor.localization.ContentCountry;
|
import org.schabi.newpipe.extractor.localization.ContentCountry;
|
||||||
|
import org.schabi.newpipe.extractor.services.media_ccc.extractors.MediaCCCLiveStreamKiosk;
|
||||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||||
import org.schabi.newpipe.fragments.list.BaseListInfoFragment;
|
import org.schabi.newpipe.fragments.list.BaseListInfoFragment;
|
||||||
import org.schabi.newpipe.util.ExtractorHelper;
|
import org.schabi.newpipe.util.ExtractorHelper;
|
||||||
|
@ -161,4 +163,14 @@ public class KioskFragment extends BaseListInfoFragment<StreamInfoItem, KioskInf
|
||||||
name = kioskTranslatedName;
|
name = kioskTranslatedName;
|
||||||
setTitle(kioskTranslatedName);
|
setTitle(kioskTranslatedName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void showEmptyState() {
|
||||||
|
// show "no live streams" for live stream kiosk
|
||||||
|
super.showEmptyState();
|
||||||
|
if (MediaCCCLiveStreamKiosk.KIOSK_ID.equals(currentInfo.getId())
|
||||||
|
&& ServiceList.MediaCCC.getServiceId() == currentInfo.getServiceId()) {
|
||||||
|
setEmptyStateMessage(R.string.no_live_streams);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
package org.schabi.newpipe.fragments.list.playlist;
|
||||||
|
|
||||||
|
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface for {@code R.layout.playlist_control} view holders
|
||||||
|
* to give access to the play queue.
|
||||||
|
*/
|
||||||
|
public interface PlaylistControlViewHolder {
|
||||||
|
PlayQueue getPlayQueue();
|
||||||
|
}
|
|
@ -43,14 +43,14 @@ import org.schabi.newpipe.info_list.dialog.InfoItemDialog;
|
||||||
import org.schabi.newpipe.info_list.dialog.StreamDialogDefaultEntry;
|
import org.schabi.newpipe.info_list.dialog.StreamDialogDefaultEntry;
|
||||||
import org.schabi.newpipe.local.dialog.PlaylistDialog;
|
import org.schabi.newpipe.local.dialog.PlaylistDialog;
|
||||||
import org.schabi.newpipe.local.playlist.RemotePlaylistManager;
|
import org.schabi.newpipe.local.playlist.RemotePlaylistManager;
|
||||||
import org.schabi.newpipe.player.PlayerType;
|
|
||||||
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
||||||
import org.schabi.newpipe.player.playqueue.PlaylistPlayQueue;
|
import org.schabi.newpipe.player.playqueue.PlaylistPlayQueue;
|
||||||
import org.schabi.newpipe.util.ExtractorHelper;
|
import org.schabi.newpipe.util.ExtractorHelper;
|
||||||
import org.schabi.newpipe.util.Localization;
|
import org.schabi.newpipe.util.Localization;
|
||||||
import org.schabi.newpipe.util.NavigationHelper;
|
import org.schabi.newpipe.util.NavigationHelper;
|
||||||
import org.schabi.newpipe.util.PicassoHelper;
|
import org.schabi.newpipe.util.image.PicassoHelper;
|
||||||
import org.schabi.newpipe.util.external_communication.ShareUtils;
|
import org.schabi.newpipe.util.external_communication.ShareUtils;
|
||||||
|
import org.schabi.newpipe.util.PlayButtonHelper;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
@ -64,7 +64,8 @@ import io.reactivex.rxjava3.core.Single;
|
||||||
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
||||||
import io.reactivex.rxjava3.disposables.Disposable;
|
import io.reactivex.rxjava3.disposables.Disposable;
|
||||||
|
|
||||||
public class PlaylistFragment extends BaseListInfoFragment<StreamInfoItem, PlaylistInfo> {
|
public class PlaylistFragment extends BaseListInfoFragment<StreamInfoItem, PlaylistInfo>
|
||||||
|
implements PlaylistControlViewHolder {
|
||||||
|
|
||||||
private static final String PICASSO_PLAYLIST_TAG = "PICASSO_PLAYLIST_TAG";
|
private static final String PICASSO_PLAYLIST_TAG = "PICASSO_PLAYLIST_TAG";
|
||||||
|
|
||||||
|
@ -233,7 +234,7 @@ public class PlaylistFragment extends BaseListInfoFragment<StreamInfoItem, Playl
|
||||||
break;
|
break;
|
||||||
case R.id.menu_item_share:
|
case R.id.menu_item_share:
|
||||||
ShareUtils.shareText(requireContext(), name, url,
|
ShareUtils.shareText(requireContext(), name, url,
|
||||||
currentInfo == null ? null : currentInfo.getThumbnailUrl());
|
currentInfo == null ? List.of() : currentInfo.getThumbnails());
|
||||||
break;
|
break;
|
||||||
case R.id.menu_item_bookmark:
|
case R.id.menu_item_bookmark:
|
||||||
onBookmarkClicked();
|
onBookmarkClicked();
|
||||||
|
@ -298,7 +299,6 @@ public class PlaylistFragment extends BaseListInfoFragment<StreamInfoItem, Playl
|
||||||
|
|
||||||
playlistControlBinding.getRoot().setVisibility(View.VISIBLE);
|
playlistControlBinding.getRoot().setVisibility(View.VISIBLE);
|
||||||
|
|
||||||
final String avatarUrl = result.getUploaderAvatarUrl();
|
|
||||||
if (result.getServiceId() == ServiceList.YouTube.getServiceId()
|
if (result.getServiceId() == ServiceList.YouTube.getServiceId()
|
||||||
&& (YoutubeParsingHelper.isYoutubeMixId(result.getId())
|
&& (YoutubeParsingHelper.isYoutubeMixId(result.getId())
|
||||||
|| YoutubeParsingHelper.isYoutubeMusicMixId(result.getId()))) {
|
|| YoutubeParsingHelper.isYoutubeMusicMixId(result.getId()))) {
|
||||||
|
@ -314,7 +314,7 @@ public class PlaylistFragment extends BaseListInfoFragment<StreamInfoItem, Playl
|
||||||
R.drawable.ic_radio)
|
R.drawable.ic_radio)
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
PicassoHelper.loadAvatar(avatarUrl).tag(PICASSO_PLAYLIST_TAG)
|
PicassoHelper.loadAvatar(result.getUploaderAvatars()).tag(PICASSO_PLAYLIST_TAG)
|
||||||
.into(headerBinding.uploaderAvatarView);
|
.into(headerBinding.uploaderAvatarView);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -332,25 +332,10 @@ public class PlaylistFragment extends BaseListInfoFragment<StreamInfoItem, Playl
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.subscribe(getPlaylistBookmarkSubscriber());
|
.subscribe(getPlaylistBookmarkSubscriber());
|
||||||
|
|
||||||
playlistControlBinding.playlistCtrlPlayAllButton.setOnClickListener(view ->
|
PlayButtonHelper.initPlaylistControlClickListener(activity, playlistControlBinding, this);
|
||||||
NavigationHelper.playOnMainPlayer(activity, getPlayQueue()));
|
|
||||||
playlistControlBinding.playlistCtrlPlayPopupButton.setOnClickListener(view ->
|
|
||||||
NavigationHelper.playOnPopupPlayer(activity, getPlayQueue(), false));
|
|
||||||
playlistControlBinding.playlistCtrlPlayBgButton.setOnClickListener(view ->
|
|
||||||
NavigationHelper.playOnBackgroundPlayer(activity, getPlayQueue(), false));
|
|
||||||
|
|
||||||
playlistControlBinding.playlistCtrlPlayPopupButton.setOnLongClickListener(view -> {
|
|
||||||
NavigationHelper.enqueueOnPlayer(activity, getPlayQueue(), PlayerType.POPUP);
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
|
|
||||||
playlistControlBinding.playlistCtrlPlayBgButton.setOnLongClickListener(view -> {
|
|
||||||
NavigationHelper.enqueueOnPlayer(activity, getPlayQueue(), PlayerType.AUDIO);
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private PlayQueue getPlayQueue() {
|
public PlayQueue getPlayQueue() {
|
||||||
return getPlayQueue(0);
|
return getPlayQueue(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -167,6 +167,10 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
||||||
|
|
||||||
/*////////////////////////////////////////////////////////////////////////*/
|
/*////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TextWatcher to remove rich-text formatting on the search EditText when pasting content
|
||||||
|
* from the clipboard.
|
||||||
|
*/
|
||||||
private TextWatcher textWatcher;
|
private TextWatcher textWatcher;
|
||||||
|
|
||||||
public static SearchFragment getInstance(final int serviceId, final String searchString) {
|
public static SearchFragment getInstance(final int serviceId, final String searchString) {
|
||||||
|
@ -583,11 +587,13 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
||||||
@Override
|
@Override
|
||||||
public void beforeTextChanged(final CharSequence s, final int start,
|
public void beforeTextChanged(final CharSequence s, final int start,
|
||||||
final int count, final int after) {
|
final int count, final int after) {
|
||||||
|
// Do nothing, old text is already clean
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onTextChanged(final CharSequence s, final int start,
|
public void onTextChanged(final CharSequence s, final int start,
|
||||||
final int before, final int count) {
|
final int before, final int count) {
|
||||||
|
// Changes are handled in afterTextChanged; CharSequence cannot be changed here.
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -21,18 +21,17 @@ import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||||
import org.schabi.newpipe.fragments.list.BaseListInfoFragment;
|
import org.schabi.newpipe.fragments.list.BaseListInfoFragment;
|
||||||
import org.schabi.newpipe.info_list.ItemViewMode;
|
import org.schabi.newpipe.info_list.ItemViewMode;
|
||||||
import org.schabi.newpipe.ktx.ViewUtils;
|
import org.schabi.newpipe.ktx.ViewUtils;
|
||||||
import org.schabi.newpipe.util.RelatedItemInfo;
|
|
||||||
|
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
import java.util.function.Supplier;
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
import io.reactivex.rxjava3.core.Single;
|
import io.reactivex.rxjava3.core.Single;
|
||||||
|
|
||||||
public class RelatedItemsFragment extends BaseListInfoFragment<InfoItem, RelatedItemInfo>
|
public class RelatedItemsFragment extends BaseListInfoFragment<InfoItem, RelatedItemsInfo>
|
||||||
implements SharedPreferences.OnSharedPreferenceChangeListener {
|
implements SharedPreferences.OnSharedPreferenceChangeListener {
|
||||||
private static final String INFO_KEY = "related_info_key";
|
private static final String INFO_KEY = "related_info_key";
|
||||||
|
|
||||||
private RelatedItemInfo relatedItemInfo;
|
private RelatedItemsInfo relatedItemsInfo;
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
// Views
|
// Views
|
||||||
|
@ -69,7 +68,7 @@ public class RelatedItemsFragment extends BaseListInfoFragment<InfoItem, Related
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected Supplier<View> getListHeaderSupplier() {
|
protected Supplier<View> getListHeaderSupplier() {
|
||||||
if (relatedItemInfo == null || relatedItemInfo.getRelatedItems() == null) {
|
if (relatedItemsInfo == null || relatedItemsInfo.getRelatedItems() == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -97,8 +96,8 @@ public class RelatedItemsFragment extends BaseListInfoFragment<InfoItem, Related
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected Single<RelatedItemInfo> loadResult(final boolean forceLoad) {
|
protected Single<RelatedItemsInfo> loadResult(final boolean forceLoad) {
|
||||||
return Single.fromCallable(() -> relatedItemInfo);
|
return Single.fromCallable(() -> relatedItemsInfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -110,7 +109,7 @@ public class RelatedItemsFragment extends BaseListInfoFragment<InfoItem, Related
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void handleResult(@NonNull final RelatedItemInfo result) {
|
public void handleResult(@NonNull final RelatedItemsInfo result) {
|
||||||
super.handleResult(result);
|
super.handleResult(result);
|
||||||
|
|
||||||
if (headerBinding != null) {
|
if (headerBinding != null) {
|
||||||
|
@ -137,23 +136,23 @@ public class RelatedItemsFragment extends BaseListInfoFragment<InfoItem, Related
|
||||||
|
|
||||||
private void setInitialData(final StreamInfo info) {
|
private void setInitialData(final StreamInfo info) {
|
||||||
super.setInitialData(info.getServiceId(), info.getUrl(), info.getName());
|
super.setInitialData(info.getServiceId(), info.getUrl(), info.getName());
|
||||||
if (this.relatedItemInfo == null) {
|
if (this.relatedItemsInfo == null) {
|
||||||
this.relatedItemInfo = RelatedItemInfo.getInfo(info);
|
this.relatedItemsInfo = new RelatedItemsInfo(info);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onSaveInstanceState(@NonNull final Bundle outState) {
|
public void onSaveInstanceState(@NonNull final Bundle outState) {
|
||||||
super.onSaveInstanceState(outState);
|
super.onSaveInstanceState(outState);
|
||||||
outState.putSerializable(INFO_KEY, relatedItemInfo);
|
outState.putSerializable(INFO_KEY, relatedItemsInfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onRestoreInstanceState(@NonNull final Bundle savedState) {
|
protected void onRestoreInstanceState(@NonNull final Bundle savedState) {
|
||||||
super.onRestoreInstanceState(savedState);
|
super.onRestoreInstanceState(savedState);
|
||||||
final Serializable serializable = savedState.getSerializable(INFO_KEY);
|
final Serializable serializable = savedState.getSerializable(INFO_KEY);
|
||||||
if (serializable instanceof RelatedItemInfo) {
|
if (serializable instanceof RelatedItemsInfo) {
|
||||||
this.relatedItemInfo = (RelatedItemInfo) serializable;
|
this.relatedItemsInfo = (RelatedItemsInfo) serializable;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,22 @@
|
||||||
|
package org.schabi.newpipe.fragments.list.videos;
|
||||||
|
|
||||||
|
import org.schabi.newpipe.extractor.InfoItem;
|
||||||
|
import org.schabi.newpipe.extractor.ListInfo;
|
||||||
|
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler;
|
||||||
|
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
|
||||||
|
public final class RelatedItemsInfo extends ListInfo<InfoItem> {
|
||||||
|
/**
|
||||||
|
* This class is used to wrap the related items of a StreamInfo into a ListInfo object.
|
||||||
|
*
|
||||||
|
* @param info the stream info from which to get related items
|
||||||
|
*/
|
||||||
|
public RelatedItemsInfo(final StreamInfo info) {
|
||||||
|
super(info.getServiceId(), new ListLinkHandler(info.getOriginalUrl(), info.getUrl(),
|
||||||
|
info.getId(), Collections.emptyList(), null), info.getName());
|
||||||
|
setRelatedItems(new ArrayList<>(info.getRelatedItems()));
|
||||||
|
}
|
||||||
|
}
|
|
@ -13,8 +13,7 @@ import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem;
|
||||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||||
import org.schabi.newpipe.info_list.holder.ChannelInfoItemHolder;
|
import org.schabi.newpipe.info_list.holder.ChannelInfoItemHolder;
|
||||||
import org.schabi.newpipe.info_list.holder.ChannelMiniInfoItemHolder;
|
import org.schabi.newpipe.info_list.holder.ChannelMiniInfoItemHolder;
|
||||||
import org.schabi.newpipe.info_list.holder.CommentsInfoItemHolder;
|
import org.schabi.newpipe.info_list.holder.CommentInfoItemHolder;
|
||||||
import org.schabi.newpipe.info_list.holder.CommentsMiniInfoItemHolder;
|
|
||||||
import org.schabi.newpipe.info_list.holder.InfoItemHolder;
|
import org.schabi.newpipe.info_list.holder.InfoItemHolder;
|
||||||
import org.schabi.newpipe.info_list.holder.PlaylistInfoItemHolder;
|
import org.schabi.newpipe.info_list.holder.PlaylistInfoItemHolder;
|
||||||
import org.schabi.newpipe.info_list.holder.PlaylistMiniInfoItemHolder;
|
import org.schabi.newpipe.info_list.holder.PlaylistMiniInfoItemHolder;
|
||||||
|
@ -87,8 +86,7 @@ public class InfoItemBuilder {
|
||||||
return useMiniVariant ? new PlaylistMiniInfoItemHolder(this, parent)
|
return useMiniVariant ? new PlaylistMiniInfoItemHolder(this, parent)
|
||||||
: new PlaylistInfoItemHolder(this, parent);
|
: new PlaylistInfoItemHolder(this, parent);
|
||||||
case COMMENT:
|
case COMMENT:
|
||||||
return useMiniVariant ? new CommentsMiniInfoItemHolder(this, parent)
|
return new CommentInfoItemHolder(this, parent);
|
||||||
: new CommentsInfoItemHolder(this, parent);
|
|
||||||
default:
|
default:
|
||||||
throw new RuntimeException("InfoType not expected = " + infoType.name());
|
throw new RuntimeException("InfoType not expected = " + infoType.name());
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,8 +21,7 @@ import org.schabi.newpipe.info_list.holder.ChannelCardInfoItemHolder;
|
||||||
import org.schabi.newpipe.info_list.holder.ChannelGridInfoItemHolder;
|
import org.schabi.newpipe.info_list.holder.ChannelGridInfoItemHolder;
|
||||||
import org.schabi.newpipe.info_list.holder.ChannelInfoItemHolder;
|
import org.schabi.newpipe.info_list.holder.ChannelInfoItemHolder;
|
||||||
import org.schabi.newpipe.info_list.holder.ChannelMiniInfoItemHolder;
|
import org.schabi.newpipe.info_list.holder.ChannelMiniInfoItemHolder;
|
||||||
import org.schabi.newpipe.info_list.holder.CommentsInfoItemHolder;
|
import org.schabi.newpipe.info_list.holder.CommentInfoItemHolder;
|
||||||
import org.schabi.newpipe.info_list.holder.CommentsMiniInfoItemHolder;
|
|
||||||
import org.schabi.newpipe.info_list.holder.InfoItemHolder;
|
import org.schabi.newpipe.info_list.holder.InfoItemHolder;
|
||||||
import org.schabi.newpipe.info_list.holder.PlaylistCardInfoItemHolder;
|
import org.schabi.newpipe.info_list.holder.PlaylistCardInfoItemHolder;
|
||||||
import org.schabi.newpipe.info_list.holder.PlaylistGridInfoItemHolder;
|
import org.schabi.newpipe.info_list.holder.PlaylistGridInfoItemHolder;
|
||||||
|
@ -79,8 +78,7 @@ public class InfoListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolde
|
||||||
private static final int PLAYLIST_HOLDER_TYPE = 0x301;
|
private static final int PLAYLIST_HOLDER_TYPE = 0x301;
|
||||||
private static final int GRID_PLAYLIST_HOLDER_TYPE = 0x302;
|
private static final int GRID_PLAYLIST_HOLDER_TYPE = 0x302;
|
||||||
private static final int CARD_PLAYLIST_HOLDER_TYPE = 0x303;
|
private static final int CARD_PLAYLIST_HOLDER_TYPE = 0x303;
|
||||||
private static final int MINI_COMMENT_HOLDER_TYPE = 0x400;
|
private static final int COMMENT_HOLDER_TYPE = 0x400;
|
||||||
private static final int COMMENT_HOLDER_TYPE = 0x401;
|
|
||||||
|
|
||||||
private final LayoutInflater layoutInflater;
|
private final LayoutInflater layoutInflater;
|
||||||
private final InfoItemBuilder infoItemBuilder;
|
private final InfoItemBuilder infoItemBuilder;
|
||||||
|
@ -271,7 +269,7 @@ public class InfoListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolde
|
||||||
return PLAYLIST_HOLDER_TYPE;
|
return PLAYLIST_HOLDER_TYPE;
|
||||||
}
|
}
|
||||||
case COMMENT:
|
case COMMENT:
|
||||||
return useMiniVariant ? MINI_COMMENT_HOLDER_TYPE : COMMENT_HOLDER_TYPE;
|
return COMMENT_HOLDER_TYPE;
|
||||||
default:
|
default:
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
@ -320,10 +318,8 @@ public class InfoListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolde
|
||||||
return new PlaylistGridInfoItemHolder(infoItemBuilder, parent);
|
return new PlaylistGridInfoItemHolder(infoItemBuilder, parent);
|
||||||
case CARD_PLAYLIST_HOLDER_TYPE:
|
case CARD_PLAYLIST_HOLDER_TYPE:
|
||||||
return new PlaylistCardInfoItemHolder(infoItemBuilder, parent);
|
return new PlaylistCardInfoItemHolder(infoItemBuilder, parent);
|
||||||
case MINI_COMMENT_HOLDER_TYPE:
|
|
||||||
return new CommentsMiniInfoItemHolder(infoItemBuilder, parent);
|
|
||||||
case COMMENT_HOLDER_TYPE:
|
case COMMENT_HOLDER_TYPE:
|
||||||
return new CommentsInfoItemHolder(infoItemBuilder, parent);
|
return new CommentInfoItemHolder(infoItemBuilder, parent);
|
||||||
default:
|
default:
|
||||||
return new FallbackViewHolder(new View(parent.getContext()));
|
return new FallbackViewHolder(new View(parent.getContext()));
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,7 +8,7 @@ import com.xwray.groupie.Item
|
||||||
import org.schabi.newpipe.R
|
import org.schabi.newpipe.R
|
||||||
import org.schabi.newpipe.extractor.stream.StreamSegment
|
import org.schabi.newpipe.extractor.stream.StreamSegment
|
||||||
import org.schabi.newpipe.util.Localization
|
import org.schabi.newpipe.util.Localization
|
||||||
import org.schabi.newpipe.util.PicassoHelper
|
import org.schabi.newpipe.util.image.PicassoHelper
|
||||||
|
|
||||||
class StreamSegmentItem(
|
class StreamSegmentItem(
|
||||||
private val item: StreamSegment,
|
private val item: StreamSegment,
|
||||||
|
|
|
@ -104,7 +104,7 @@ public enum StreamDialogDefaultEntry {
|
||||||
|
|
||||||
SHARE(R.string.share, (fragment, item) ->
|
SHARE(R.string.share, (fragment, item) ->
|
||||||
ShareUtils.shareText(fragment.requireContext(), item.getName(), item.getUrl(),
|
ShareUtils.shareText(fragment.requireContext(), item.getName(), item.getUrl(),
|
||||||
item.getThumbnailUrl())),
|
item.getThumbnails())),
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Opens a {@link DownloadDialog} after fetching some stream info.
|
* Opens a {@link DownloadDialog} after fetching some stream info.
|
||||||
|
|
|
@ -13,7 +13,7 @@ import org.schabi.newpipe.extractor.channel.ChannelInfoItem;
|
||||||
import org.schabi.newpipe.extractor.utils.Utils;
|
import org.schabi.newpipe.extractor.utils.Utils;
|
||||||
import org.schabi.newpipe.info_list.InfoItemBuilder;
|
import org.schabi.newpipe.info_list.InfoItemBuilder;
|
||||||
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
||||||
import org.schabi.newpipe.util.PicassoHelper;
|
import org.schabi.newpipe.util.image.PicassoHelper;
|
||||||
import org.schabi.newpipe.util.Localization;
|
import org.schabi.newpipe.util.Localization;
|
||||||
|
|
||||||
public class ChannelMiniInfoItemHolder extends InfoItemHolder {
|
public class ChannelMiniInfoItemHolder extends InfoItemHolder {
|
||||||
|
@ -56,7 +56,7 @@ public class ChannelMiniInfoItemHolder extends InfoItemHolder {
|
||||||
itemAdditionalDetailView.setText(getDetailLine(item));
|
itemAdditionalDetailView.setText(getDetailLine(item));
|
||||||
}
|
}
|
||||||
|
|
||||||
PicassoHelper.loadAvatar(item.getThumbnailUrl()).into(itemThumbnailView);
|
PicassoHelper.loadAvatar(item.getThumbnails()).into(itemThumbnailView);
|
||||||
|
|
||||||
itemView.setOnClickListener(view -> {
|
itemView.setOnClickListener(view -> {
|
||||||
if (itemBuilder.getOnChannelSelectedListener() != null) {
|
if (itemBuilder.getOnChannelSelectedListener() != null) {
|
||||||
|
|
|
@ -1,37 +1,36 @@
|
||||||
package org.schabi.newpipe.info_list.holder;
|
package org.schabi.newpipe.info_list.holder;
|
||||||
|
|
||||||
import static android.text.TextUtils.isEmpty;
|
import static android.text.TextUtils.isEmpty;
|
||||||
|
import static org.schabi.newpipe.util.ServiceHelper.getServiceById;
|
||||||
|
|
||||||
import android.graphics.Paint;
|
import android.graphics.Paint;
|
||||||
import android.text.Layout;
|
import android.text.Layout;
|
||||||
import android.text.method.LinkMovementMethod;
|
import android.text.method.LinkMovementMethod;
|
||||||
import android.text.style.URLSpan;
|
import android.text.style.URLSpan;
|
||||||
import android.util.Log;
|
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
|
import android.widget.Button;
|
||||||
import android.widget.ImageView;
|
import android.widget.ImageView;
|
||||||
import android.widget.RelativeLayout;
|
import android.widget.RelativeLayout;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.appcompat.app.AppCompatActivity;
|
|
||||||
import androidx.core.text.HtmlCompat;
|
import androidx.core.text.HtmlCompat;
|
||||||
|
import androidx.fragment.app.FragmentActivity;
|
||||||
|
|
||||||
import org.schabi.newpipe.R;
|
import org.schabi.newpipe.R;
|
||||||
import org.schabi.newpipe.error.ErrorUtil;
|
|
||||||
import org.schabi.newpipe.extractor.InfoItem;
|
import org.schabi.newpipe.extractor.InfoItem;
|
||||||
import org.schabi.newpipe.extractor.NewPipe;
|
|
||||||
import org.schabi.newpipe.extractor.ServiceList;
|
|
||||||
import org.schabi.newpipe.extractor.StreamingService;
|
import org.schabi.newpipe.extractor.StreamingService;
|
||||||
import org.schabi.newpipe.extractor.comments.CommentsInfoItem;
|
import org.schabi.newpipe.extractor.comments.CommentsInfoItem;
|
||||||
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
|
||||||
import org.schabi.newpipe.extractor.stream.Description;
|
import org.schabi.newpipe.extractor.stream.Description;
|
||||||
import org.schabi.newpipe.info_list.InfoItemBuilder;
|
import org.schabi.newpipe.info_list.InfoItemBuilder;
|
||||||
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
||||||
import org.schabi.newpipe.util.DeviceUtils;
|
import org.schabi.newpipe.util.DeviceUtils;
|
||||||
import org.schabi.newpipe.util.Localization;
|
import org.schabi.newpipe.util.Localization;
|
||||||
import org.schabi.newpipe.util.NavigationHelper;
|
import org.schabi.newpipe.util.NavigationHelper;
|
||||||
import org.schabi.newpipe.util.PicassoHelper;
|
import org.schabi.newpipe.util.image.ImageStrategy;
|
||||||
|
import org.schabi.newpipe.util.image.PicassoHelper;
|
||||||
import org.schabi.newpipe.util.external_communication.ShareUtils;
|
import org.schabi.newpipe.util.external_communication.ShareUtils;
|
||||||
import org.schabi.newpipe.util.text.CommentTextOnTouchListener;
|
import org.schabi.newpipe.util.text.CommentTextOnTouchListener;
|
||||||
import org.schabi.newpipe.util.text.TextLinkifier;
|
import org.schabi.newpipe.util.text.TextLinkifier;
|
||||||
|
@ -40,8 +39,7 @@ import java.util.function.Consumer;
|
||||||
|
|
||||||
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
||||||
|
|
||||||
public class CommentsMiniInfoItemHolder extends InfoItemHolder {
|
public class CommentInfoItemHolder extends InfoItemHolder {
|
||||||
private static final String TAG = "CommentsMiniIIHolder";
|
|
||||||
private static final String ELLIPSIS = "…";
|
private static final String ELLIPSIS = "…";
|
||||||
|
|
||||||
private static final int COMMENT_DEFAULT_LINES = 2;
|
private static final int COMMENT_DEFAULT_LINES = 2;
|
||||||
|
@ -56,23 +54,34 @@ public class CommentsMiniInfoItemHolder extends InfoItemHolder {
|
||||||
private final RelativeLayout itemRoot;
|
private final RelativeLayout itemRoot;
|
||||||
private final ImageView itemThumbnailView;
|
private final ImageView itemThumbnailView;
|
||||||
private final TextView itemContentView;
|
private final TextView itemContentView;
|
||||||
|
private final ImageView itemThumbsUpView;
|
||||||
private final TextView itemLikesCountView;
|
private final TextView itemLikesCountView;
|
||||||
private final TextView itemPublishedTime;
|
private final TextView itemTitleView;
|
||||||
|
private final ImageView itemHeartView;
|
||||||
|
private final ImageView itemPinnedView;
|
||||||
|
private final Button repliesButton;
|
||||||
|
|
||||||
private final CompositeDisposable disposables = new CompositeDisposable();
|
private final CompositeDisposable disposables = new CompositeDisposable();
|
||||||
@Nullable private Description commentText;
|
@Nullable
|
||||||
@Nullable private StreamingService streamService;
|
private Description commentText;
|
||||||
@Nullable private String streamUrl;
|
@Nullable
|
||||||
|
private StreamingService streamService;
|
||||||
|
@Nullable
|
||||||
|
private String streamUrl;
|
||||||
|
|
||||||
CommentsMiniInfoItemHolder(final InfoItemBuilder infoItemBuilder, final int layoutId,
|
public CommentInfoItemHolder(final InfoItemBuilder infoItemBuilder,
|
||||||
final ViewGroup parent) {
|
final ViewGroup parent) {
|
||||||
super(infoItemBuilder, layoutId, parent);
|
super(infoItemBuilder, R.layout.list_comment_item, parent);
|
||||||
|
|
||||||
itemRoot = itemView.findViewById(R.id.itemRoot);
|
itemRoot = itemView.findViewById(R.id.itemRoot);
|
||||||
itemThumbnailView = itemView.findViewById(R.id.itemThumbnailView);
|
itemThumbnailView = itemView.findViewById(R.id.itemThumbnailView);
|
||||||
itemLikesCountView = itemView.findViewById(R.id.detail_thumbs_up_count_view);
|
|
||||||
itemPublishedTime = itemView.findViewById(R.id.itemPublishedTime);
|
|
||||||
itemContentView = itemView.findViewById(R.id.itemCommentContentView);
|
itemContentView = itemView.findViewById(R.id.itemCommentContentView);
|
||||||
|
itemThumbsUpView = itemView.findViewById(R.id.detail_thumbs_up_img_view);
|
||||||
|
itemLikesCountView = itemView.findViewById(R.id.detail_thumbs_up_count_view);
|
||||||
|
itemTitleView = itemView.findViewById(R.id.itemTitleView);
|
||||||
|
itemHeartView = itemView.findViewById(R.id.detail_heart_image_view);
|
||||||
|
itemPinnedView = itemView.findViewById(R.id.detail_pinned_view);
|
||||||
|
repliesButton = itemView.findViewById(R.id.replies_button);
|
||||||
|
|
||||||
commentHorizontalPadding = (int) infoItemBuilder.getContext()
|
commentHorizontalPadding = (int) infoItemBuilder.getContext()
|
||||||
.getResources().getDimension(R.dimen.comments_horizontal_padding);
|
.getResources().getDimension(R.dimen.comments_horizontal_padding);
|
||||||
|
@ -84,11 +93,6 @@ public class CommentsMiniInfoItemHolder extends InfoItemHolder {
|
||||||
ellipsisWidthPx = paintAtContentSize.measureText(ELLIPSIS);
|
ellipsisWidthPx = paintAtContentSize.measureText(ELLIPSIS);
|
||||||
}
|
}
|
||||||
|
|
||||||
public CommentsMiniInfoItemHolder(final InfoItemBuilder infoItemBuilder,
|
|
||||||
final ViewGroup parent) {
|
|
||||||
this(infoItemBuilder, R.layout.list_comments_mini_item, parent);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void updateFromItem(final InfoItem infoItem,
|
public void updateFromItem(final InfoItem infoItem,
|
||||||
final HistoryRecordManager historyRecordManager) {
|
final HistoryRecordManager historyRecordManager) {
|
||||||
|
@ -97,8 +101,10 @@ public class CommentsMiniInfoItemHolder extends InfoItemHolder {
|
||||||
}
|
}
|
||||||
final CommentsInfoItem item = (CommentsInfoItem) infoItem;
|
final CommentsInfoItem item = (CommentsInfoItem) infoItem;
|
||||||
|
|
||||||
PicassoHelper.loadAvatar(item.getUploaderAvatarUrl()).into(itemThumbnailView);
|
|
||||||
if (PicassoHelper.getShouldLoadImages()) {
|
// load the author avatar
|
||||||
|
PicassoHelper.loadAvatar(item.getUploaderAvatars()).into(itemThumbnailView);
|
||||||
|
if (ImageStrategy.shouldLoadImages()) {
|
||||||
itemThumbnailView.setVisibility(View.VISIBLE);
|
itemThumbnailView.setVisibility(View.VISIBLE);
|
||||||
itemRoot.setPadding(commentVerticalPadding, commentVerticalPadding,
|
itemRoot.setPadding(commentVerticalPadding, commentVerticalPadding,
|
||||||
commentVerticalPadding, commentVerticalPadding);
|
commentVerticalPadding, commentVerticalPadding);
|
||||||
|
@ -107,18 +113,33 @@ public class CommentsMiniInfoItemHolder extends InfoItemHolder {
|
||||||
itemRoot.setPadding(commentHorizontalPadding, commentVerticalPadding,
|
itemRoot.setPadding(commentHorizontalPadding, commentVerticalPadding,
|
||||||
commentHorizontalPadding, commentVerticalPadding);
|
commentHorizontalPadding, commentVerticalPadding);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
itemThumbnailView.setOnClickListener(view -> openCommentAuthor(item));
|
itemThumbnailView.setOnClickListener(view -> openCommentAuthor(item));
|
||||||
|
|
||||||
try {
|
|
||||||
streamService = NewPipe.getService(item.getServiceId());
|
// setup the top row, with pinned icon, author name and comment date
|
||||||
} catch (final ExtractionException e) {
|
itemPinnedView.setVisibility(item.isPinned() ? View.VISIBLE : View.GONE);
|
||||||
// should never happen
|
itemTitleView.setText(Localization.concatenateStrings(item.getUploaderName(),
|
||||||
ErrorUtil.showUiErrorSnackbar(itemBuilder.getContext(), "Getting StreamingService", e);
|
Localization.relativeTimeOrTextual(itemBuilder.getContext(), item.getUploadDate(),
|
||||||
Log.w(TAG, "Cannot obtain service from comment service id, defaulting to YouTube", e);
|
item.getTextualUploadDate())));
|
||||||
streamService = ServiceList.YouTube;
|
|
||||||
}
|
|
||||||
|
// setup bottom row, with likes, heart and replies button
|
||||||
|
itemLikesCountView.setText(
|
||||||
|
Localization.likeCount(itemBuilder.getContext(), item.getLikeCount()));
|
||||||
|
|
||||||
|
itemHeartView.setVisibility(item.isHeartedByUploader() ? View.VISIBLE : View.GONE);
|
||||||
|
|
||||||
|
final boolean hasReplies = item.getReplies() != null;
|
||||||
|
repliesButton.setOnClickListener(hasReplies ? v -> openCommentReplies(item) : null);
|
||||||
|
repliesButton.setVisibility(hasReplies ? View.VISIBLE : View.GONE);
|
||||||
|
repliesButton.setText(hasReplies
|
||||||
|
? Localization.replyCount(itemBuilder.getContext(), item.getReplyCount()) : "");
|
||||||
|
((RelativeLayout.LayoutParams) itemThumbsUpView.getLayoutParams()).topMargin =
|
||||||
|
hasReplies ? 0 : DeviceUtils.dpToPx(6, itemBuilder.getContext());
|
||||||
|
|
||||||
|
|
||||||
|
// setup comment content and click listeners to expand/ellipsize it
|
||||||
|
streamService = getServiceById(item.getServiceId());
|
||||||
streamUrl = item.getUrl();
|
streamUrl = item.getUrl();
|
||||||
commentText = item.getCommentText();
|
commentText = item.getCommentText();
|
||||||
ellipsize();
|
ellipsize();
|
||||||
|
@ -126,22 +147,6 @@ public class CommentsMiniInfoItemHolder extends InfoItemHolder {
|
||||||
//noinspection ClickableViewAccessibility
|
//noinspection ClickableViewAccessibility
|
||||||
itemContentView.setOnTouchListener(CommentTextOnTouchListener.INSTANCE);
|
itemContentView.setOnTouchListener(CommentTextOnTouchListener.INSTANCE);
|
||||||
|
|
||||||
if (item.getLikeCount() >= 0) {
|
|
||||||
itemLikesCountView.setText(
|
|
||||||
Localization.shortCount(
|
|
||||||
itemBuilder.getContext(),
|
|
||||||
item.getLikeCount()));
|
|
||||||
} else {
|
|
||||||
itemLikesCountView.setText("-");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (item.getUploadDate() != null) {
|
|
||||||
itemPublishedTime.setText(Localization.relativeTime(item.getUploadDate()
|
|
||||||
.offsetDateTime()));
|
|
||||||
} else {
|
|
||||||
itemPublishedTime.setText(item.getTextualUploadDate());
|
|
||||||
}
|
|
||||||
|
|
||||||
itemView.setOnClickListener(view -> {
|
itemView.setOnClickListener(view -> {
|
||||||
toggleEllipsize();
|
toggleEllipsize();
|
||||||
if (itemBuilder.getOnCommentsSelectedListener() != null) {
|
if (itemBuilder.getOnCommentsSelectedListener() != null) {
|
||||||
|
@ -149,7 +154,6 @@ public class CommentsMiniInfoItemHolder extends InfoItemHolder {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
itemView.setOnLongClickListener(view -> {
|
itemView.setOnLongClickListener(view -> {
|
||||||
if (DeviceUtils.isTv(itemBuilder.getContext())) {
|
if (DeviceUtils.isTv(itemBuilder.getContext())) {
|
||||||
openCommentAuthor(item);
|
openCommentAuthor(item);
|
||||||
|
@ -163,20 +167,14 @@ public class CommentsMiniInfoItemHolder extends InfoItemHolder {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private void openCommentAuthor(final CommentsInfoItem item) {
|
private void openCommentAuthor(@NonNull final CommentsInfoItem item) {
|
||||||
if (isEmpty(item.getUploaderUrl())) {
|
NavigationHelper.openCommentAuthorIfPresent((FragmentActivity) itemBuilder.getContext(),
|
||||||
return;
|
item);
|
||||||
}
|
}
|
||||||
final AppCompatActivity activity = (AppCompatActivity) itemBuilder.getContext();
|
|
||||||
try {
|
private void openCommentReplies(@NonNull final CommentsInfoItem item) {
|
||||||
NavigationHelper.openChannelFragment(
|
NavigationHelper.openCommentRepliesFragment((FragmentActivity) itemBuilder.getContext(),
|
||||||
activity.getSupportFragmentManager(),
|
item);
|
||||||
item.getServiceId(),
|
|
||||||
item.getUploaderUrl(),
|
|
||||||
item.getUploaderName());
|
|
||||||
} catch (final Exception e) {
|
|
||||||
ErrorUtil.showUiErrorSnackbar(activity, "Opening channel fragment", e);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void allowLinkFocus() {
|
private void allowLinkFocus() {
|
|
@ -1,63 +0,0 @@
|
||||||
package org.schabi.newpipe.info_list.holder;
|
|
||||||
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.ViewGroup;
|
|
||||||
import android.widget.ImageView;
|
|
||||||
import android.widget.TextView;
|
|
||||||
|
|
||||||
import org.schabi.newpipe.R;
|
|
||||||
import org.schabi.newpipe.extractor.InfoItem;
|
|
||||||
import org.schabi.newpipe.extractor.comments.CommentsInfoItem;
|
|
||||||
import org.schabi.newpipe.info_list.InfoItemBuilder;
|
|
||||||
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Created by Christian Schabesberger on 12.02.17.
|
|
||||||
*
|
|
||||||
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
|
|
||||||
* ChannelInfoItemHolder .java is part of NewPipe.
|
|
||||||
*
|
|
||||||
* NewPipe is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* NewPipe is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
public class CommentsInfoItemHolder extends CommentsMiniInfoItemHolder {
|
|
||||||
public final TextView itemTitleView;
|
|
||||||
private final ImageView itemHeartView;
|
|
||||||
private final ImageView itemPinnedView;
|
|
||||||
|
|
||||||
public CommentsInfoItemHolder(final InfoItemBuilder infoItemBuilder, final ViewGroup parent) {
|
|
||||||
super(infoItemBuilder, R.layout.list_comments_item, parent);
|
|
||||||
|
|
||||||
itemTitleView = itemView.findViewById(R.id.itemTitleView);
|
|
||||||
itemHeartView = itemView.findViewById(R.id.detail_heart_image_view);
|
|
||||||
itemPinnedView = itemView.findViewById(R.id.detail_pinned_view);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void updateFromItem(final InfoItem infoItem,
|
|
||||||
final HistoryRecordManager historyRecordManager) {
|
|
||||||
super.updateFromItem(infoItem, historyRecordManager);
|
|
||||||
|
|
||||||
if (!(infoItem instanceof CommentsInfoItem)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
final CommentsInfoItem item = (CommentsInfoItem) infoItem;
|
|
||||||
|
|
||||||
itemTitleView.setText(item.getUploaderName());
|
|
||||||
|
|
||||||
itemHeartView.setVisibility(item.isHeartedByUploader() ? View.VISIBLE : View.GONE);
|
|
||||||
|
|
||||||
itemPinnedView.setVisibility(item.isPinned() ? View.VISIBLE : View.GONE);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -9,7 +9,7 @@ import org.schabi.newpipe.extractor.InfoItem;
|
||||||
import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem;
|
import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem;
|
||||||
import org.schabi.newpipe.info_list.InfoItemBuilder;
|
import org.schabi.newpipe.info_list.InfoItemBuilder;
|
||||||
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
||||||
import org.schabi.newpipe.util.PicassoHelper;
|
import org.schabi.newpipe.util.image.PicassoHelper;
|
||||||
import org.schabi.newpipe.util.Localization;
|
import org.schabi.newpipe.util.Localization;
|
||||||
|
|
||||||
public class PlaylistMiniInfoItemHolder extends InfoItemHolder {
|
public class PlaylistMiniInfoItemHolder extends InfoItemHolder {
|
||||||
|
@ -46,7 +46,7 @@ public class PlaylistMiniInfoItemHolder extends InfoItemHolder {
|
||||||
.localizeStreamCountMini(itemStreamCountView.getContext(), item.getStreamCount()));
|
.localizeStreamCountMini(itemStreamCountView.getContext(), item.getStreamCount()));
|
||||||
itemUploaderView.setText(item.getUploaderName());
|
itemUploaderView.setText(item.getUploaderName());
|
||||||
|
|
||||||
PicassoHelper.loadPlaylistThumbnail(item.getThumbnailUrl()).into(itemThumbnailView);
|
PicassoHelper.loadPlaylistThumbnail(item.getThumbnails()).into(itemThumbnailView);
|
||||||
|
|
||||||
itemView.setOnClickListener(view -> {
|
itemView.setOnClickListener(view -> {
|
||||||
if (itemBuilder.getOnPlaylistSelectedListener() != null) {
|
if (itemBuilder.getOnPlaylistSelectedListener() != null) {
|
||||||
|
|
|
@ -12,10 +12,6 @@ import org.schabi.newpipe.info_list.InfoItemBuilder;
|
||||||
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
||||||
import org.schabi.newpipe.util.Localization;
|
import org.schabi.newpipe.util.Localization;
|
||||||
|
|
||||||
import androidx.preference.PreferenceManager;
|
|
||||||
|
|
||||||
import static org.schabi.newpipe.MainActivity.DEBUG;
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Created by Christian Schabesberger on 01.08.16.
|
* Created by Christian Schabesberger on 01.08.16.
|
||||||
* <p>
|
* <p>
|
||||||
|
@ -81,7 +77,9 @@ public class StreamInfoItemHolder extends StreamMiniInfoItemHolder {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final String uploadDate = getFormattedRelativeUploadDate(infoItem);
|
final String uploadDate = Localization.relativeTimeOrTextual(itemBuilder.getContext(),
|
||||||
|
infoItem.getUploadDate(),
|
||||||
|
infoItem.getTextualUploadDate());
|
||||||
if (!TextUtils.isEmpty(uploadDate)) {
|
if (!TextUtils.isEmpty(uploadDate)) {
|
||||||
if (viewsAndDate.isEmpty()) {
|
if (viewsAndDate.isEmpty()) {
|
||||||
return uploadDate;
|
return uploadDate;
|
||||||
|
@ -92,20 +90,4 @@ public class StreamInfoItemHolder extends StreamMiniInfoItemHolder {
|
||||||
|
|
||||||
return viewsAndDate;
|
return viewsAndDate;
|
||||||
}
|
}
|
||||||
|
|
||||||
private String getFormattedRelativeUploadDate(final StreamInfoItem infoItem) {
|
|
||||||
if (infoItem.getUploadDate() != null) {
|
|
||||||
String formattedRelativeTime = Localization
|
|
||||||
.relativeTime(infoItem.getUploadDate().offsetDateTime());
|
|
||||||
|
|
||||||
if (DEBUG && PreferenceManager.getDefaultSharedPreferences(itemBuilder.getContext())
|
|
||||||
.getBoolean(itemBuilder.getContext()
|
|
||||||
.getString(R.string.show_original_time_ago_key), false)) {
|
|
||||||
formattedRelativeTime += " (" + infoItem.getTextualUploadDate() + ")";
|
|
||||||
}
|
|
||||||
return formattedRelativeTime;
|
|
||||||
} else {
|
|
||||||
return infoItem.getTextualUploadDate();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,7 +16,7 @@ import org.schabi.newpipe.ktx.ViewUtils;
|
||||||
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
||||||
import org.schabi.newpipe.util.DependentPreferenceHelper;
|
import org.schabi.newpipe.util.DependentPreferenceHelper;
|
||||||
import org.schabi.newpipe.util.Localization;
|
import org.schabi.newpipe.util.Localization;
|
||||||
import org.schabi.newpipe.util.PicassoHelper;
|
import org.schabi.newpipe.util.image.PicassoHelper;
|
||||||
import org.schabi.newpipe.util.StreamTypeUtil;
|
import org.schabi.newpipe.util.StreamTypeUtil;
|
||||||
import org.schabi.newpipe.views.AnimatedProgressBar;
|
import org.schabi.newpipe.views.AnimatedProgressBar;
|
||||||
|
|
||||||
|
@ -87,7 +87,7 @@ public class StreamMiniInfoItemHolder extends InfoItemHolder {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Default thumbnail is shown on error, while loading and if the url is empty
|
// Default thumbnail is shown on error, while loading and if the url is empty
|
||||||
PicassoHelper.loadThumbnail(item.getThumbnailUrl()).into(itemThumbnailView);
|
PicassoHelper.loadThumbnail(item.getThumbnails()).into(itemThumbnailView);
|
||||||
|
|
||||||
itemView.setOnClickListener(view -> {
|
itemView.setOnClickListener(view -> {
|
||||||
if (itemBuilder.getOnStreamSelectedListener() != null) {
|
if (itemBuilder.getOnStreamSelectedListener() != null) {
|
||||||
|
|
9
app/src/main/java/org/schabi/newpipe/ktx/Bundle.kt
Normal file
9
app/src/main/java/org/schabi/newpipe/ktx/Bundle.kt
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
package org.schabi.newpipe.ktx
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.os.Parcelable
|
||||||
|
import androidx.core.os.BundleCompat
|
||||||
|
|
||||||
|
inline fun <reified T : Parcelable> Bundle.parcelableArrayList(key: String?): ArrayList<T>? {
|
||||||
|
return BundleCompat.getParcelableArrayList(this, key, T::class.java)
|
||||||
|
}
|
|
@ -91,11 +91,6 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL
|
||||||
// Fragment LifeCycle - Views
|
// Fragment LifeCycle - Views
|
||||||
///////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void initViews(final View rootView, final Bundle savedInstanceState) {
|
|
||||||
super.initViews(rootView, savedInstanceState);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void initListeners() {
|
protected void initListeners() {
|
||||||
super.initListeners();
|
super.initListeners();
|
||||||
|
|
|
@ -38,7 +38,6 @@ import android.view.ViewGroup
|
||||||
import android.widget.Button
|
import android.widget.Button
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.core.content.edit
|
import androidx.core.content.edit
|
||||||
import androidx.core.math.MathUtils
|
|
||||||
import androidx.core.os.bundleOf
|
import androidx.core.os.bundleOf
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import androidx.lifecycle.ViewModelProvider
|
import androidx.lifecycle.ViewModelProvider
|
||||||
|
@ -60,6 +59,7 @@ import org.schabi.newpipe.database.feed.model.FeedGroupEntity
|
||||||
import org.schabi.newpipe.database.subscription.SubscriptionEntity
|
import org.schabi.newpipe.database.subscription.SubscriptionEntity
|
||||||
import org.schabi.newpipe.databinding.FragmentFeedBinding
|
import org.schabi.newpipe.databinding.FragmentFeedBinding
|
||||||
import org.schabi.newpipe.error.ErrorInfo
|
import org.schabi.newpipe.error.ErrorInfo
|
||||||
|
import org.schabi.newpipe.error.ErrorUtil
|
||||||
import org.schabi.newpipe.error.UserAction
|
import org.schabi.newpipe.error.UserAction
|
||||||
import org.schabi.newpipe.extractor.exceptions.AccountTerminatedException
|
import org.schabi.newpipe.extractor.exceptions.AccountTerminatedException
|
||||||
import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException
|
import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException
|
||||||
|
@ -453,24 +453,33 @@ class FeedFragment : BaseStateFragment<FeedState>() {
|
||||||
if (t is FeedLoadService.RequestException &&
|
if (t is FeedLoadService.RequestException &&
|
||||||
t.cause is ContentNotAvailableException
|
t.cause is ContentNotAvailableException
|
||||||
) {
|
) {
|
||||||
Single.fromCallable {
|
disposables.add(
|
||||||
NewPipeDatabase.getInstance(requireContext()).subscriptionDAO()
|
Single.fromCallable {
|
||||||
.getSubscription(t.subscriptionId)
|
NewPipeDatabase.getInstance(requireContext()).subscriptionDAO()
|
||||||
}.subscribeOn(Schedulers.io())
|
.getSubscription(t.subscriptionId)
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
}
|
||||||
.subscribe(
|
.subscribeOn(Schedulers.io())
|
||||||
{ subscriptionEntity ->
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
handleFeedNotAvailable(
|
.subscribe(
|
||||||
subscriptionEntity,
|
{ subscriptionEntity ->
|
||||||
t.cause,
|
handleFeedNotAvailable(
|
||||||
errors.subList(i + 1, errors.size)
|
subscriptionEntity,
|
||||||
)
|
t.cause,
|
||||||
},
|
errors.subList(i + 1, errors.size)
|
||||||
{ throwable -> Log.e(TAG, "Unable to process", throwable) }
|
)
|
||||||
)
|
},
|
||||||
return // this will be called on the remaining errors by handleFeedNotAvailable()
|
{ throwable -> Log.e(TAG, "Unable to process", throwable) }
|
||||||
|
)
|
||||||
|
)
|
||||||
|
// this will be called on the remaining errors by handleFeedNotAvailable()
|
||||||
|
return@handleItemsErrors
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (errors.isNotEmpty()) {
|
||||||
|
// if no error was a ContentNotAvailableException, show a general error snackbar
|
||||||
|
ErrorUtil.showSnackbar(this, ErrorInfo(errors, UserAction.REQUESTED_FEED, ""))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleFeedNotAvailable(
|
private fun handleFeedNotAvailable(
|
||||||
|
@ -579,7 +588,7 @@ class FeedFragment : BaseStateFragment<FeedState>() {
|
||||||
// state until the user scrolls them out of the visible area which causes a update/bind-call
|
// state until the user scrolls them out of the visible area which causes a update/bind-call
|
||||||
groupAdapter.notifyItemRangeChanged(
|
groupAdapter.notifyItemRangeChanged(
|
||||||
0,
|
0,
|
||||||
MathUtils.clamp(highlightCount, lastNewItemsCount, groupAdapter.itemCount)
|
highlightCount.coerceIn(lastNewItemsCount, groupAdapter.itemCount)
|
||||||
)
|
)
|
||||||
|
|
||||||
if (highlightCount > 0) {
|
if (highlightCount > 0) {
|
||||||
|
@ -598,9 +607,13 @@ class FeedFragment : BaseStateFragment<FeedState>() {
|
||||||
execOnEnd = {
|
execOnEnd = {
|
||||||
// Disabled animations would result in immediately hiding the button
|
// Disabled animations would result in immediately hiding the button
|
||||||
// after it showed up
|
// after it showed up
|
||||||
if (DeviceUtils.hasAnimationsAnimatorDurationEnabled(context)) {
|
// Context can be null in some cases, so we have to make sure it is not null in
|
||||||
// Hide the new items-"popup" after 10s
|
// order to avoid a NullPointerException
|
||||||
hideNewItemsLoaded(true, 10000)
|
context?.let {
|
||||||
|
if (DeviceUtils.hasAnimationsAnimatorDurationEnabled(it)) {
|
||||||
|
// Hide the new items button after 10s
|
||||||
|
hideNewItemsLoaded(true, 10000)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
|
@ -13,9 +13,9 @@ sealed class FeedState {
|
||||||
|
|
||||||
data class LoadedState(
|
data class LoadedState(
|
||||||
val items: List<StreamItem>,
|
val items: List<StreamItem>,
|
||||||
val oldestUpdate: OffsetDateTime? = null,
|
val oldestUpdate: OffsetDateTime?,
|
||||||
val notLoadedCount: Long,
|
val notLoadedCount: Long,
|
||||||
val itemsErrors: List<Throwable> = emptyList()
|
val itemsErrors: List<Throwable>
|
||||||
) : FeedState()
|
) : FeedState()
|
||||||
|
|
||||||
data class ErrorState(
|
data class ErrorState(
|
||||||
|
|
|
@ -86,7 +86,7 @@ class FeedViewModel(
|
||||||
.subscribe { (event, listFromDB, notLoadedCount, oldestUpdate) ->
|
.subscribe { (event, listFromDB, notLoadedCount, oldestUpdate) ->
|
||||||
mutableStateLiveData.postValue(
|
mutableStateLiveData.postValue(
|
||||||
when (event) {
|
when (event) {
|
||||||
is IdleEvent -> FeedState.LoadedState(listFromDB.map { e -> StreamItem(e) }, oldestUpdate, notLoadedCount)
|
is IdleEvent -> FeedState.LoadedState(listFromDB.map { e -> StreamItem(e) }, oldestUpdate, notLoadedCount, listOf())
|
||||||
is ProgressEvent -> FeedState.ProgressState(event.currentProgress, event.maxProgress, event.progressMessage)
|
is ProgressEvent -> FeedState.ProgressState(event.currentProgress, event.maxProgress, event.progressMessage)
|
||||||
is SuccessResultEvent -> FeedState.LoadedState(listFromDB.map { e -> StreamItem(e) }, oldestUpdate, notLoadedCount, event.itemsErrors)
|
is SuccessResultEvent -> FeedState.LoadedState(listFromDB.map { e -> StreamItem(e) }, oldestUpdate, notLoadedCount, event.itemsErrors)
|
||||||
is ErrorResultEvent -> FeedState.ErrorState(event.error)
|
is ErrorResultEvent -> FeedState.ErrorState(event.error)
|
||||||
|
|
|
@ -18,8 +18,8 @@ import org.schabi.newpipe.extractor.stream.StreamType.POST_LIVE_AUDIO_STREAM
|
||||||
import org.schabi.newpipe.extractor.stream.StreamType.POST_LIVE_STREAM
|
import org.schabi.newpipe.extractor.stream.StreamType.POST_LIVE_STREAM
|
||||||
import org.schabi.newpipe.extractor.stream.StreamType.VIDEO_STREAM
|
import org.schabi.newpipe.extractor.stream.StreamType.VIDEO_STREAM
|
||||||
import org.schabi.newpipe.util.Localization
|
import org.schabi.newpipe.util.Localization
|
||||||
import org.schabi.newpipe.util.PicassoHelper
|
|
||||||
import org.schabi.newpipe.util.StreamTypeUtil
|
import org.schabi.newpipe.util.StreamTypeUtil
|
||||||
|
import org.schabi.newpipe.util.image.PicassoHelper
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
import java.util.function.Consumer
|
import java.util.function.Consumer
|
||||||
|
|
||||||
|
|
|
@ -22,7 +22,7 @@ import org.schabi.newpipe.R
|
||||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem
|
import org.schabi.newpipe.extractor.stream.StreamInfoItem
|
||||||
import org.schabi.newpipe.local.feed.service.FeedUpdateInfo
|
import org.schabi.newpipe.local.feed.service.FeedUpdateInfo
|
||||||
import org.schabi.newpipe.util.NavigationHelper
|
import org.schabi.newpipe.util.NavigationHelper
|
||||||
import org.schabi.newpipe.util.PicassoHelper
|
import org.schabi.newpipe.util.image.PicassoHelper
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Helper for everything related to show notifications about new streams to the user.
|
* Helper for everything related to show notifications about new streams to the user.
|
||||||
|
@ -58,7 +58,7 @@ class NotificationHelper(val context: Context) {
|
||||||
.setAutoCancel(true)
|
.setAutoCancel(true)
|
||||||
.setCategory(NotificationCompat.CATEGORY_SOCIAL)
|
.setCategory(NotificationCompat.CATEGORY_SOCIAL)
|
||||||
.setGroupSummary(true)
|
.setGroupSummary(true)
|
||||||
.setGroup(data.listInfo.url)
|
.setGroup(data.url)
|
||||||
.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_SUMMARY)
|
.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_SUMMARY)
|
||||||
|
|
||||||
// Build a summary notification for Android versions < 7.0
|
// Build a summary notification for Android versions < 7.0
|
||||||
|
@ -73,7 +73,7 @@ class NotificationHelper(val context: Context) {
|
||||||
context,
|
context,
|
||||||
data.pseudoId,
|
data.pseudoId,
|
||||||
NavigationHelper
|
NavigationHelper
|
||||||
.getChannelIntent(context, data.listInfo.serviceId, data.listInfo.url)
|
.getChannelIntent(context, data.serviceId, data.url)
|
||||||
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK),
|
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK),
|
||||||
0,
|
0,
|
||||||
false
|
false
|
||||||
|
@ -88,7 +88,7 @@ class NotificationHelper(val context: Context) {
|
||||||
|
|
||||||
// Show individual stream notifications, set channel icon only if there is actually
|
// Show individual stream notifications, set channel icon only if there is actually
|
||||||
// one
|
// one
|
||||||
showStreamNotifications(newStreams, data.listInfo.serviceId, bitmap)
|
showStreamNotifications(newStreams, data.serviceId, bitmap)
|
||||||
// Show summary notification
|
// Show summary notification
|
||||||
manager.notify(data.pseudoId, summaryBuilder.build())
|
manager.notify(data.pseudoId, summaryBuilder.build())
|
||||||
|
|
||||||
|
@ -97,7 +97,7 @@ class NotificationHelper(val context: Context) {
|
||||||
|
|
||||||
override fun onBitmapFailed(e: Exception, errorDrawable: Drawable) {
|
override fun onBitmapFailed(e: Exception, errorDrawable: Drawable) {
|
||||||
// Show individual stream notifications
|
// Show individual stream notifications
|
||||||
showStreamNotifications(newStreams, data.listInfo.serviceId, null)
|
showStreamNotifications(newStreams, data.serviceId, null)
|
||||||
// Show summary notification
|
// Show summary notification
|
||||||
manager.notify(data.pseudoId, summaryBuilder.build())
|
manager.notify(data.pseudoId, summaryBuilder.build())
|
||||||
iconLoadingTargets.remove(this) // allow it to be garbage-collected
|
iconLoadingTargets.remove(this) // allow it to be garbage-collected
|
||||||
|
|
|
@ -137,7 +137,7 @@ class NotificationWorker(
|
||||||
.enqueueUniquePeriodicWork(
|
.enqueueUniquePeriodicWork(
|
||||||
WORK_TAG,
|
WORK_TAG,
|
||||||
if (force) {
|
if (force) {
|
||||||
ExistingPeriodicWorkPolicy.REPLACE
|
ExistingPeriodicWorkPolicy.CANCEL_AND_REENQUEUE
|
||||||
} else {
|
} else {
|
||||||
ExistingPeriodicWorkPolicy.KEEP
|
ExistingPeriodicWorkPolicy.KEEP
|
||||||
},
|
},
|
||||||
|
|
|
@ -26,7 +26,7 @@ object FeedEventManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
sealed class Event {
|
sealed class Event {
|
||||||
object IdleEvent : Event()
|
data object IdleEvent : Event()
|
||||||
data class ProgressEvent(val currentProgress: Int = -1, val maxProgress: Int = -1, @StringRes val progressMessage: Int = 0) : Event() {
|
data class ProgressEvent(val currentProgress: Int = -1, val maxProgress: Int = -1, @StringRes val progressMessage: Int = 0) : Event() {
|
||||||
constructor(@StringRes progressMessage: Int) : this(-1, -1, progressMessage)
|
constructor(@StringRes progressMessage: Int) : this(-1, -1, progressMessage)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package org.schabi.newpipe.local.feed.service
|
package org.schabi.newpipe.local.feed.service
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.content.SharedPreferences
|
||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||||
import io.reactivex.rxjava3.core.Completable
|
import io.reactivex.rxjava3.core.Completable
|
||||||
|
@ -13,11 +14,17 @@ import io.reactivex.rxjava3.schedulers.Schedulers
|
||||||
import org.schabi.newpipe.R
|
import org.schabi.newpipe.R
|
||||||
import org.schabi.newpipe.database.feed.model.FeedGroupEntity
|
import org.schabi.newpipe.database.feed.model.FeedGroupEntity
|
||||||
import org.schabi.newpipe.database.subscription.NotificationMode
|
import org.schabi.newpipe.database.subscription.NotificationMode
|
||||||
import org.schabi.newpipe.extractor.ListInfo
|
import org.schabi.newpipe.database.subscription.SubscriptionEntity
|
||||||
|
import org.schabi.newpipe.extractor.Info
|
||||||
|
import org.schabi.newpipe.extractor.NewPipe
|
||||||
|
import org.schabi.newpipe.extractor.feed.FeedInfo
|
||||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem
|
import org.schabi.newpipe.extractor.stream.StreamInfoItem
|
||||||
import org.schabi.newpipe.local.feed.FeedDatabaseManager
|
import org.schabi.newpipe.local.feed.FeedDatabaseManager
|
||||||
import org.schabi.newpipe.local.subscription.SubscriptionManager
|
import org.schabi.newpipe.local.subscription.SubscriptionManager
|
||||||
import org.schabi.newpipe.util.ExtractorHelper
|
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.OffsetDateTime
|
||||||
import java.time.ZoneOffset
|
import java.time.ZoneOffset
|
||||||
import java.util.concurrent.atomic.AtomicBoolean
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
|
@ -75,7 +82,9 @@ class FeedLoadManager(private val context: Context) {
|
||||||
* subscriptions which have not been updated within the feed updated threshold
|
* subscriptions which have not been updated within the feed updated threshold
|
||||||
*/
|
*/
|
||||||
val outdatedSubscriptions = when (groupId) {
|
val outdatedSubscriptions = when (groupId) {
|
||||||
FeedGroupEntity.GROUP_ALL_ID -> feedDatabaseManager.outdatedSubscriptions(outdatedThreshold)
|
FeedGroupEntity.GROUP_ALL_ID -> feedDatabaseManager.outdatedSubscriptions(
|
||||||
|
outdatedThreshold
|
||||||
|
)
|
||||||
GROUP_NOTIFICATION_ENABLED -> feedDatabaseManager.outdatedSubscriptionsWithNotificationMode(
|
GROUP_NOTIFICATION_ENABLED -> feedDatabaseManager.outdatedSubscriptionsWithNotificationMode(
|
||||||
outdatedThreshold, NotificationMode.ENABLED
|
outdatedThreshold, NotificationMode.ENABLED
|
||||||
)
|
)
|
||||||
|
@ -101,52 +110,7 @@ class FeedLoadManager(private val context: Context) {
|
||||||
.runOn(Schedulers.io(), PARALLEL_EXTRACTIONS * 2)
|
.runOn(Schedulers.io(), PARALLEL_EXTRACTIONS * 2)
|
||||||
.filter { !cancelSignal.get() }
|
.filter { !cancelSignal.get() }
|
||||||
.map { subscriptionEntity ->
|
.map { subscriptionEntity ->
|
||||||
var error: Throwable? = null
|
loadStreams(subscriptionEntity, useFeedExtractor, defaultSharedPreferences)
|
||||||
try {
|
|
||||||
// check for and load new streams
|
|
||||||
// either by using the dedicated feed method or by getting the channel info
|
|
||||||
val listInfo = if (useFeedExtractor) {
|
|
||||||
ExtractorHelper
|
|
||||||
.getFeedInfoFallbackToChannelInfo(
|
|
||||||
subscriptionEntity.serviceId,
|
|
||||||
subscriptionEntity.url
|
|
||||||
)
|
|
||||||
.onErrorReturn {
|
|
||||||
error = it // store error, otherwise wrapped into RuntimeException
|
|
||||||
throw it
|
|
||||||
}
|
|
||||||
.blockingGet()
|
|
||||||
} else {
|
|
||||||
ExtractorHelper
|
|
||||||
.getChannelInfo(
|
|
||||||
subscriptionEntity.serviceId,
|
|
||||||
subscriptionEntity.url,
|
|
||||||
true
|
|
||||||
)
|
|
||||||
.onErrorReturn {
|
|
||||||
error = it // store error, otherwise wrapped into RuntimeException
|
|
||||||
throw it
|
|
||||||
}
|
|
||||||
.blockingGet()
|
|
||||||
} as ListInfo<StreamInfoItem>
|
|
||||||
|
|
||||||
return@map Notification.createOnNext(
|
|
||||||
FeedUpdateInfo(
|
|
||||||
subscriptionEntity,
|
|
||||||
listInfo
|
|
||||||
)
|
|
||||||
)
|
|
||||||
} catch (e: Throwable) {
|
|
||||||
if (error == null) {
|
|
||||||
// do this to prevent blockingGet() from wrapping into RuntimeException
|
|
||||||
error = e
|
|
||||||
}
|
|
||||||
|
|
||||||
val request = "${subscriptionEntity.serviceId}:${subscriptionEntity.url}"
|
|
||||||
val wrapper =
|
|
||||||
FeedLoadService.RequestException(subscriptionEntity.uid, request, error!!)
|
|
||||||
return@map Notification.createOnError<FeedUpdateInfo>(wrapper)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.sequential()
|
.sequential()
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
@ -164,7 +128,112 @@ class FeedLoadManager(private val context: Context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun broadcastProgress() {
|
private fun broadcastProgress() {
|
||||||
FeedEventManager.postEvent(FeedEventManager.Event.ProgressEvent(currentProgress.get(), maxProgress.get()))
|
FeedEventManager.postEvent(
|
||||||
|
FeedEventManager.Event.ProgressEvent(
|
||||||
|
currentProgress.get(),
|
||||||
|
maxProgress.get()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun loadStreams(
|
||||||
|
subscriptionEntity: SubscriptionEntity,
|
||||||
|
useFeedExtractor: Boolean,
|
||||||
|
defaultSharedPreferences: SharedPreferences
|
||||||
|
): Notification<FeedUpdateInfo> {
|
||||||
|
var error: Throwable? = null
|
||||||
|
val storeOriginalErrorAndRethrow = { e: Throwable ->
|
||||||
|
// keep original to prevent blockingGet() from wrapping it into RuntimeException
|
||||||
|
error = e
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// check for and load new streams
|
||||||
|
// either by using the dedicated feed method or by getting the channel info
|
||||||
|
var originalInfo: Info? = null
|
||||||
|
var streams: List<StreamInfoItem>? = null
|
||||||
|
val errors = ArrayList<Throwable>()
|
||||||
|
|
||||||
|
if (useFeedExtractor) {
|
||||||
|
NewPipe.getService(subscriptionEntity.serviceId)
|
||||||
|
.getFeedExtractor(subscriptionEntity.url)
|
||||||
|
?.also { feedExtractor ->
|
||||||
|
// the user wants to use a feed extractor and there is one, use it
|
||||||
|
val feedInfo = FeedInfo.getInfo(feedExtractor)
|
||||||
|
errors.addAll(feedInfo.errors)
|
||||||
|
originalInfo = feedInfo
|
||||||
|
streams = feedInfo.relatedItems
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (originalInfo == null) {
|
||||||
|
// use the normal channel tabs extractor if either the user wants it, or
|
||||||
|
// the current service does not have a dedicated feed extractor
|
||||||
|
|
||||||
|
val channelInfo = getChannelInfo(
|
||||||
|
subscriptionEntity.serviceId,
|
||||||
|
subscriptionEntity.url, true
|
||||||
|
)
|
||||||
|
.onErrorReturn(storeOriginalErrorAndRethrow)
|
||||||
|
.blockingGet()
|
||||||
|
errors.addAll(channelInfo.errors)
|
||||||
|
originalInfo = channelInfo
|
||||||
|
|
||||||
|
streams = channelInfo.tabs
|
||||||
|
.filter { tab ->
|
||||||
|
ChannelTabHelper.fetchFeedChannelTab(
|
||||||
|
context,
|
||||||
|
defaultSharedPreferences,
|
||||||
|
tab
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.map {
|
||||||
|
Pair(
|
||||||
|
getChannelTab(subscriptionEntity.serviceId, it, true)
|
||||||
|
.onErrorReturn(storeOriginalErrorAndRethrow)
|
||||||
|
.blockingGet(),
|
||||||
|
it
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.flatMap { (channelTabInfo, linkHandler) ->
|
||||||
|
errors.addAll(channelTabInfo.errors)
|
||||||
|
if (channelTabInfo.relatedItems.isEmpty() &&
|
||||||
|
channelTabInfo.nextPage != null
|
||||||
|
) {
|
||||||
|
val infoItemsPage = getMoreChannelTabItems(
|
||||||
|
subscriptionEntity.serviceId,
|
||||||
|
linkHandler, channelTabInfo.nextPage
|
||||||
|
)
|
||||||
|
.blockingGet()
|
||||||
|
|
||||||
|
errors.addAll(infoItemsPage.errors)
|
||||||
|
return@flatMap infoItemsPage.items
|
||||||
|
} else {
|
||||||
|
return@flatMap channelTabInfo.relatedItems
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.filterIsInstance<StreamInfoItem>()
|
||||||
|
}
|
||||||
|
|
||||||
|
return Notification.createOnNext(
|
||||||
|
FeedUpdateInfo(
|
||||||
|
subscriptionEntity,
|
||||||
|
originalInfo!!,
|
||||||
|
streams!!,
|
||||||
|
errors,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
val request = "${subscriptionEntity.serviceId}:${subscriptionEntity.url}"
|
||||||
|
val wrapper = FeedLoadService.RequestException(
|
||||||
|
subscriptionEntity.uid,
|
||||||
|
request,
|
||||||
|
// do this to prevent blockingGet() from wrapping into RuntimeException
|
||||||
|
error ?: e
|
||||||
|
)
|
||||||
|
return Notification.createOnError(wrapper)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -203,24 +272,24 @@ class FeedLoadManager(private val context: Context) {
|
||||||
for (notification in list) {
|
for (notification in list) {
|
||||||
when {
|
when {
|
||||||
notification.isOnNext -> {
|
notification.isOnNext -> {
|
||||||
val subscriptionId = notification.value!!.uid
|
val info = notification.value!!
|
||||||
val info = notification.value!!.listInfo
|
|
||||||
|
|
||||||
notification.value!!.newStreams = filterNewStreams(
|
notification.value!!.newStreams = filterNewStreams(info.streams)
|
||||||
notification.value!!.listInfo.relatedItems
|
|
||||||
)
|
|
||||||
|
|
||||||
feedDatabaseManager.upsertAll(subscriptionId, info.relatedItems)
|
feedDatabaseManager.upsertAll(info.uid, info.streams)
|
||||||
subscriptionManager.updateFromInfo(subscriptionId, info)
|
subscriptionManager.updateFromInfo(info)
|
||||||
|
|
||||||
if (info.errors.isNotEmpty()) {
|
if (info.errors.isNotEmpty()) {
|
||||||
feedResultsHolder.addErrors(
|
feedResultsHolder.addErrors(
|
||||||
FeedLoadService.RequestException.wrapList(
|
info.errors.map {
|
||||||
subscriptionId,
|
FeedLoadService.RequestException(
|
||||||
info
|
info.uid,
|
||||||
)
|
"${info.serviceId}:${info.url}",
|
||||||
|
it
|
||||||
|
)
|
||||||
|
}
|
||||||
)
|
)
|
||||||
feedDatabaseManager.markAsOutdated(subscriptionId)
|
feedDatabaseManager.markAsOutdated(info.uid)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
notification.isOnError -> {
|
notification.isOnError -> {
|
||||||
|
|
|
@ -39,8 +39,6 @@ import org.schabi.newpipe.App
|
||||||
import org.schabi.newpipe.MainActivity.DEBUG
|
import org.schabi.newpipe.MainActivity.DEBUG
|
||||||
import org.schabi.newpipe.R
|
import org.schabi.newpipe.R
|
||||||
import org.schabi.newpipe.database.feed.model.FeedGroupEntity
|
import org.schabi.newpipe.database.feed.model.FeedGroupEntity
|
||||||
import org.schabi.newpipe.extractor.ListInfo
|
|
||||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem
|
|
||||||
import org.schabi.newpipe.local.feed.service.FeedEventManager.Event.ErrorResultEvent
|
import org.schabi.newpipe.local.feed.service.FeedEventManager.Event.ErrorResultEvent
|
||||||
import org.schabi.newpipe.local.feed.service.FeedEventManager.postEvent
|
import org.schabi.newpipe.local.feed.service.FeedEventManager.postEvent
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
@ -126,17 +124,7 @@ class FeedLoadService : Service() {
|
||||||
// Loading & Handling
|
// Loading & Handling
|
||||||
// /////////////////////////////////////////////////////////////////////////
|
// /////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
class RequestException(val subscriptionId: Long, message: String, cause: Throwable) : Exception(message, cause) {
|
class RequestException(val subscriptionId: Long, message: String, cause: Throwable) : Exception(message, cause)
|
||||||
companion object {
|
|
||||||
fun wrapList(subscriptionId: Long, info: ListInfo<StreamInfoItem>): List<Throwable> {
|
|
||||||
val toReturn = ArrayList<Throwable>(info.errors.size)
|
|
||||||
info.errors.mapTo(toReturn) {
|
|
||||||
RequestException(subscriptionId, info.serviceId.toString() + ":" + info.url, it)
|
|
||||||
}
|
|
||||||
return toReturn
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// /////////////////////////////////////////////////////////////////////////
|
// /////////////////////////////////////////////////////////////////////////
|
||||||
// Notification
|
// Notification
|
||||||
|
|
|
@ -2,33 +2,58 @@ package org.schabi.newpipe.local.feed.service
|
||||||
|
|
||||||
import org.schabi.newpipe.database.subscription.NotificationMode
|
import org.schabi.newpipe.database.subscription.NotificationMode
|
||||||
import org.schabi.newpipe.database.subscription.SubscriptionEntity
|
import org.schabi.newpipe.database.subscription.SubscriptionEntity
|
||||||
import org.schabi.newpipe.extractor.ListInfo
|
import org.schabi.newpipe.extractor.Info
|
||||||
|
import org.schabi.newpipe.extractor.channel.ChannelInfo
|
||||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem
|
import org.schabi.newpipe.extractor.stream.StreamInfoItem
|
||||||
|
import org.schabi.newpipe.util.image.ImageStrategy
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Instances of this class might stay around in memory for some time while fetching the feed,
|
||||||
|
* because of [FeedLoadManager.BUFFER_COUNT_BEFORE_INSERT]. Therefore this class should contain
|
||||||
|
* as little data as possible to avoid out of memory errors. In particular, avoid storing whole
|
||||||
|
* [ChannelInfo] objects, as they might contain raw JSON info in ready channel tabs link handlers.
|
||||||
|
*/
|
||||||
data class FeedUpdateInfo(
|
data class FeedUpdateInfo(
|
||||||
val uid: Long,
|
val uid: Long,
|
||||||
@NotificationMode
|
@NotificationMode
|
||||||
val notificationMode: Int,
|
val notificationMode: Int,
|
||||||
val name: String,
|
val name: String,
|
||||||
val avatarUrl: String,
|
val avatarUrl: String,
|
||||||
val listInfo: ListInfo<StreamInfoItem>,
|
val url: String,
|
||||||
|
val serviceId: Int,
|
||||||
|
// description and subscriberCount are null if the constructor info is from the fast feed method
|
||||||
|
val description: String?,
|
||||||
|
val subscriberCount: Long?,
|
||||||
|
val streams: List<StreamInfoItem>,
|
||||||
|
val errors: List<Throwable>,
|
||||||
) {
|
) {
|
||||||
constructor(
|
constructor(
|
||||||
subscription: SubscriptionEntity,
|
subscription: SubscriptionEntity,
|
||||||
listInfo: ListInfo<StreamInfoItem>,
|
info: Info,
|
||||||
|
streams: List<StreamInfoItem>,
|
||||||
|
errors: List<Throwable>,
|
||||||
) : this(
|
) : this(
|
||||||
uid = subscription.uid,
|
uid = subscription.uid,
|
||||||
notificationMode = subscription.notificationMode,
|
notificationMode = subscription.notificationMode,
|
||||||
name = subscription.name,
|
name = info.name,
|
||||||
avatarUrl = subscription.avatarUrl,
|
avatarUrl = (info as? ChannelInfo)?.avatars?.let {
|
||||||
listInfo = listInfo,
|
// if the newly fetched info is not from fast feed, then it contains updated avatars
|
||||||
|
ImageStrategy.imageListToDbUrl(it)
|
||||||
|
} ?: subscription.avatarUrl,
|
||||||
|
url = info.url,
|
||||||
|
serviceId = info.serviceId,
|
||||||
|
// there is no description and subscriberCount in the fast feed
|
||||||
|
description = (info as? ChannelInfo)?.description,
|
||||||
|
subscriberCount = (info as? ChannelInfo)?.subscriberCount,
|
||||||
|
streams = streams,
|
||||||
|
errors = errors,
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Integer id, can be used as notification id, etc.
|
* Integer id, can be used as notification id, etc.
|
||||||
*/
|
*/
|
||||||
val pseudoId: Int
|
val pseudoId: Int
|
||||||
get() = listInfo.url.hashCode()
|
get() = url.hashCode()
|
||||||
|
|
||||||
lateinit var newStreams: List<StreamInfoItem>
|
lateinit var newStreams: List<StreamInfoItem>
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,14 +28,16 @@ import org.schabi.newpipe.databinding.StatisticPlaylistControlBinding;
|
||||||
import org.schabi.newpipe.error.ErrorInfo;
|
import org.schabi.newpipe.error.ErrorInfo;
|
||||||
import org.schabi.newpipe.error.UserAction;
|
import org.schabi.newpipe.error.UserAction;
|
||||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||||
|
import org.schabi.newpipe.fragments.list.playlist.PlaylistControlViewHolder;
|
||||||
import org.schabi.newpipe.info_list.dialog.InfoItemDialog;
|
import org.schabi.newpipe.info_list.dialog.InfoItemDialog;
|
||||||
|
import org.schabi.newpipe.info_list.dialog.StreamDialogDefaultEntry;
|
||||||
import org.schabi.newpipe.local.BaseLocalListFragment;
|
import org.schabi.newpipe.local.BaseLocalListFragment;
|
||||||
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
||||||
import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
|
import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
|
||||||
import org.schabi.newpipe.settings.HistorySettingsFragment;
|
import org.schabi.newpipe.settings.HistorySettingsFragment;
|
||||||
import org.schabi.newpipe.util.NavigationHelper;
|
import org.schabi.newpipe.util.NavigationHelper;
|
||||||
import org.schabi.newpipe.util.OnClickGesture;
|
import org.schabi.newpipe.util.OnClickGesture;
|
||||||
import org.schabi.newpipe.info_list.dialog.StreamDialogDefaultEntry;
|
import org.schabi.newpipe.util.PlayButtonHelper;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
|
@ -49,7 +51,8 @@ import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
||||||
import io.reactivex.rxjava3.disposables.Disposable;
|
import io.reactivex.rxjava3.disposables.Disposable;
|
||||||
|
|
||||||
public class StatisticsPlaylistFragment
|
public class StatisticsPlaylistFragment
|
||||||
extends BaseLocalListFragment<List<StreamStatisticsEntry>, Void> {
|
extends BaseLocalListFragment<List<StreamStatisticsEntry>, Void>
|
||||||
|
implements PlaylistControlViewHolder {
|
||||||
private final CompositeDisposable disposables = new CompositeDisposable();
|
private final CompositeDisposable disposables = new CompositeDisposable();
|
||||||
@State
|
@State
|
||||||
Parcelable itemsListState;
|
Parcelable itemsListState;
|
||||||
|
@ -195,14 +198,9 @@ public class StatisticsPlaylistFragment
|
||||||
if (itemListAdapter != null) {
|
if (itemListAdapter != null) {
|
||||||
itemListAdapter.unsetSelectedListener();
|
itemListAdapter.unsetSelectedListener();
|
||||||
}
|
}
|
||||||
if (playlistControlBinding != null) {
|
|
||||||
playlistControlBinding.playlistCtrlPlayBgButton.setOnClickListener(null);
|
|
||||||
playlistControlBinding.playlistCtrlPlayAllButton.setOnClickListener(null);
|
|
||||||
playlistControlBinding.playlistCtrlPlayPopupButton.setOnClickListener(null);
|
|
||||||
|
|
||||||
headerBinding = null;
|
headerBinding = null;
|
||||||
playlistControlBinding = null;
|
playlistControlBinding = null;
|
||||||
}
|
|
||||||
|
|
||||||
if (databaseSubscription != null) {
|
if (databaseSubscription != null) {
|
||||||
databaseSubscription.cancel();
|
databaseSubscription.cancel();
|
||||||
|
@ -276,12 +274,8 @@ public class StatisticsPlaylistFragment
|
||||||
itemsListState = null;
|
itemsListState = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
playlistControlBinding.playlistCtrlPlayAllButton.setOnClickListener(view ->
|
PlayButtonHelper.initPlaylistControlClickListener(activity, playlistControlBinding, this);
|
||||||
NavigationHelper.playOnMainPlayer(activity, getPlayQueue()));
|
|
||||||
playlistControlBinding.playlistCtrlPlayPopupButton.setOnClickListener(view ->
|
|
||||||
NavigationHelper.playOnPopupPlayer(activity, getPlayQueue(), false));
|
|
||||||
playlistControlBinding.playlistCtrlPlayBgButton.setOnClickListener(view ->
|
|
||||||
NavigationHelper.playOnBackgroundPlayer(activity, getPlayQueue(), false));
|
|
||||||
headerBinding.sortButton.setOnClickListener(view -> toggleSortMode());
|
headerBinding.sortButton.setOnClickListener(view -> toggleSortMode());
|
||||||
|
|
||||||
hideLoading();
|
hideLoading();
|
||||||
|
@ -374,7 +368,7 @@ public class StatisticsPlaylistFragment
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private PlayQueue getPlayQueue() {
|
public PlayQueue getPlayQueue() {
|
||||||
return getPlayQueue(0);
|
return getPlayQueue(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -8,7 +8,7 @@ import org.schabi.newpipe.database.playlist.PlaylistDuplicatesEntry;
|
||||||
import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry;
|
import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry;
|
||||||
import org.schabi.newpipe.local.LocalItemBuilder;
|
import org.schabi.newpipe.local.LocalItemBuilder;
|
||||||
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
||||||
import org.schabi.newpipe.util.PicassoHelper;
|
import org.schabi.newpipe.util.image.PicassoHelper;
|
||||||
import org.schabi.newpipe.util.Localization;
|
import org.schabi.newpipe.util.Localization;
|
||||||
|
|
||||||
import java.time.format.DateTimeFormatter;
|
import java.time.format.DateTimeFormatter;
|
||||||
|
|
|
@ -16,7 +16,7 @@ import org.schabi.newpipe.local.LocalItemBuilder;
|
||||||
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
||||||
import org.schabi.newpipe.util.DependentPreferenceHelper;
|
import org.schabi.newpipe.util.DependentPreferenceHelper;
|
||||||
import org.schabi.newpipe.util.Localization;
|
import org.schabi.newpipe.util.Localization;
|
||||||
import org.schabi.newpipe.util.PicassoHelper;
|
import org.schabi.newpipe.util.image.PicassoHelper;
|
||||||
import org.schabi.newpipe.util.ServiceHelper;
|
import org.schabi.newpipe.util.ServiceHelper;
|
||||||
import org.schabi.newpipe.views.AnimatedProgressBar;
|
import org.schabi.newpipe.views.AnimatedProgressBar;
|
||||||
|
|
||||||
|
|
|
@ -16,7 +16,7 @@ import org.schabi.newpipe.local.LocalItemBuilder;
|
||||||
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
||||||
import org.schabi.newpipe.util.DependentPreferenceHelper;
|
import org.schabi.newpipe.util.DependentPreferenceHelper;
|
||||||
import org.schabi.newpipe.util.Localization;
|
import org.schabi.newpipe.util.Localization;
|
||||||
import org.schabi.newpipe.util.PicassoHelper;
|
import org.schabi.newpipe.util.image.PicassoHelper;
|
||||||
import org.schabi.newpipe.util.ServiceHelper;
|
import org.schabi.newpipe.util.ServiceHelper;
|
||||||
import org.schabi.newpipe.views.AnimatedProgressBar;
|
import org.schabi.newpipe.views.AnimatedProgressBar;
|
||||||
|
|
||||||
|
|
|
@ -8,7 +8,7 @@ import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity;
|
||||||
import org.schabi.newpipe.local.LocalItemBuilder;
|
import org.schabi.newpipe.local.LocalItemBuilder;
|
||||||
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
||||||
import org.schabi.newpipe.util.Localization;
|
import org.schabi.newpipe.util.Localization;
|
||||||
import org.schabi.newpipe.util.PicassoHelper;
|
import org.schabi.newpipe.util.image.PicassoHelper;
|
||||||
import org.schabi.newpipe.util.ServiceHelper;
|
import org.schabi.newpipe.util.ServiceHelper;
|
||||||
|
|
||||||
import java.time.format.DateTimeFormatter;
|
import java.time.format.DateTimeFormatter;
|
||||||
|
|
|
@ -22,7 +22,6 @@ import android.widget.Toast;
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.appcompat.app.AlertDialog;
|
import androidx.appcompat.app.AlertDialog;
|
||||||
import androidx.preference.PreferenceManager;
|
|
||||||
import androidx.recyclerview.widget.ItemTouchHelper;
|
import androidx.recyclerview.widget.ItemTouchHelper;
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
import androidx.viewbinding.ViewBinding;
|
import androidx.viewbinding.ViewBinding;
|
||||||
|
@ -42,16 +41,18 @@ import org.schabi.newpipe.databinding.PlaylistControlBinding;
|
||||||
import org.schabi.newpipe.error.ErrorInfo;
|
import org.schabi.newpipe.error.ErrorInfo;
|
||||||
import org.schabi.newpipe.error.UserAction;
|
import org.schabi.newpipe.error.UserAction;
|
||||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||||
|
import org.schabi.newpipe.fragments.MainFragment;
|
||||||
|
import org.schabi.newpipe.fragments.list.playlist.PlaylistControlViewHolder;
|
||||||
import org.schabi.newpipe.info_list.dialog.InfoItemDialog;
|
import org.schabi.newpipe.info_list.dialog.InfoItemDialog;
|
||||||
import org.schabi.newpipe.info_list.dialog.StreamDialogDefaultEntry;
|
import org.schabi.newpipe.info_list.dialog.StreamDialogDefaultEntry;
|
||||||
import org.schabi.newpipe.local.BaseLocalListFragment;
|
import org.schabi.newpipe.local.BaseLocalListFragment;
|
||||||
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
||||||
import org.schabi.newpipe.player.PlayerType;
|
|
||||||
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
||||||
import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
|
import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
|
||||||
import org.schabi.newpipe.util.Localization;
|
import org.schabi.newpipe.util.Localization;
|
||||||
import org.schabi.newpipe.util.NavigationHelper;
|
import org.schabi.newpipe.util.NavigationHelper;
|
||||||
import org.schabi.newpipe.util.OnClickGesture;
|
import org.schabi.newpipe.util.OnClickGesture;
|
||||||
|
import org.schabi.newpipe.util.PlayButtonHelper;
|
||||||
import org.schabi.newpipe.util.external_communication.ShareUtils;
|
import org.schabi.newpipe.util.external_communication.ShareUtils;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
@ -69,8 +70,9 @@ import io.reactivex.rxjava3.disposables.Disposable;
|
||||||
import io.reactivex.rxjava3.schedulers.Schedulers;
|
import io.reactivex.rxjava3.schedulers.Schedulers;
|
||||||
import io.reactivex.rxjava3.subjects.PublishSubject;
|
import io.reactivex.rxjava3.subjects.PublishSubject;
|
||||||
|
|
||||||
public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistStreamEntry>, Void> {
|
public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistStreamEntry>, Void>
|
||||||
// Save the list 10 seconds after the last change occurred
|
implements PlaylistControlViewHolder {
|
||||||
|
/** Save the list 10 seconds after the last change occurred. */
|
||||||
private static final long SAVE_DEBOUNCE_MILLIS = 10000;
|
private static final long SAVE_DEBOUNCE_MILLIS = 10000;
|
||||||
private static final int MINIMUM_INITIAL_DRAG_VELOCITY = 12;
|
private static final int MINIMUM_INITIAL_DRAG_VELOCITY = 12;
|
||||||
@State
|
@State
|
||||||
|
@ -91,13 +93,20 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
|
||||||
private PublishSubject<Long> debouncedSaveSignal;
|
private PublishSubject<Long> debouncedSaveSignal;
|
||||||
private CompositeDisposable disposables;
|
private CompositeDisposable disposables;
|
||||||
|
|
||||||
/* Has the playlist been fully loaded from db */
|
/** Whether the playlist has been fully loaded from db. */
|
||||||
private AtomicBoolean isLoadingComplete;
|
private AtomicBoolean isLoadingComplete;
|
||||||
/* Has the playlist been modified (e.g. items reordered or deleted) */
|
/** Whether the playlist has been modified (e.g. items reordered or deleted) */
|
||||||
private AtomicBoolean isModified;
|
private AtomicBoolean isModified;
|
||||||
/* Flag to prevent simultaneous rewrites of the playlist */
|
/** Flag to prevent simultaneous rewrites of the playlist. */
|
||||||
private boolean isRewritingPlaylist = false;
|
private boolean isRewritingPlaylist = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The pager adapter that the fragment is created from when it is used as frontpage, i.e.
|
||||||
|
* {@link #useAsFrontPage} is {@link true}.
|
||||||
|
*/
|
||||||
|
@Nullable
|
||||||
|
private MainFragment.SelectedTabsPagerAdapter tabsPagerAdapter = null;
|
||||||
|
|
||||||
public static LocalPlaylistFragment getInstance(final long playlistId, final String name) {
|
public static LocalPlaylistFragment getInstance(final long playlistId, final String name) {
|
||||||
final LocalPlaylistFragment instance = new LocalPlaylistFragment();
|
final LocalPlaylistFragment instance = new LocalPlaylistFragment();
|
||||||
instance.setInitialData(playlistId, name);
|
instance.setInitialData(playlistId, name);
|
||||||
|
@ -157,6 +166,17 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
|
||||||
return headerBinding;
|
return headerBinding;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <p>Commit changes immediately if the playlist has been modified.</p>
|
||||||
|
* Delete operations and other modifications will be committed to ensure that the database
|
||||||
|
* is up to date, e.g. when the user adds the just deleted stream from another fragment.
|
||||||
|
*/
|
||||||
|
public void commitChanges() {
|
||||||
|
if (isModified != null && isModified.get()) {
|
||||||
|
saveImmediate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void initListeners() {
|
protected void initListeners() {
|
||||||
super.initListeners();
|
super.initListeners();
|
||||||
|
@ -265,14 +285,10 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
|
||||||
if (itemListAdapter != null) {
|
if (itemListAdapter != null) {
|
||||||
itemListAdapter.unsetSelectedListener();
|
itemListAdapter.unsetSelectedListener();
|
||||||
}
|
}
|
||||||
if (playlistControlBinding != null) {
|
|
||||||
playlistControlBinding.playlistCtrlPlayBgButton.setOnClickListener(null);
|
|
||||||
playlistControlBinding.playlistCtrlPlayAllButton.setOnClickListener(null);
|
|
||||||
playlistControlBinding.playlistCtrlPlayPopupButton.setOnClickListener(null);
|
|
||||||
|
|
||||||
headerBinding = null;
|
headerBinding = null;
|
||||||
playlistControlBinding = null;
|
playlistControlBinding = null;
|
||||||
}
|
|
||||||
|
|
||||||
if (databaseSubscription != null) {
|
if (databaseSubscription != null) {
|
||||||
databaseSubscription.cancel();
|
databaseSubscription.cancel();
|
||||||
|
@ -294,6 +310,9 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
|
||||||
if (disposables != null) {
|
if (disposables != null) {
|
||||||
disposables.dispose();
|
disposables.dispose();
|
||||||
}
|
}
|
||||||
|
if (tabsPagerAdapter != null) {
|
||||||
|
tabsPagerAdapter.getLocalPlaylistFragments().remove(this);
|
||||||
|
}
|
||||||
|
|
||||||
debouncedSaveSignal = null;
|
debouncedSaveSignal = null;
|
||||||
playlistManager = null;
|
playlistManager = null;
|
||||||
|
@ -349,7 +368,7 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
|
||||||
@Override
|
@Override
|
||||||
public boolean onOptionsItemSelected(final MenuItem item) {
|
public boolean onOptionsItemSelected(final MenuItem item) {
|
||||||
if (item.getItemId() == R.id.menu_item_share_playlist) {
|
if (item.getItemId() == R.id.menu_item_share_playlist) {
|
||||||
sharePlaylist();
|
createShareConfirmationDialog();
|
||||||
} else if (item.getItemId() == R.id.menu_item_rename_playlist) {
|
} else if (item.getItemId() == R.id.menu_item_rename_playlist) {
|
||||||
createRenameDialog();
|
createRenameDialog();
|
||||||
} else if (item.getItemId() == R.id.menu_item_remove_watched) {
|
} else if (item.getItemId() == R.id.menu_item_remove_watched) {
|
||||||
|
@ -377,16 +396,33 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Share the playlist as a newline-separated list of stream URLs.
|
* Shares the playlist as a list of stream URLs if {@code shouldSharePlaylistDetails} is
|
||||||
|
* set to {@code false}. Shares the playlist name along with a list of video titles and URLs
|
||||||
|
* if {@code shouldSharePlaylistDetails} is set to {@code true}.
|
||||||
|
*
|
||||||
|
* @param shouldSharePlaylistDetails Whether the playlist details should be included in the
|
||||||
|
* shared content.
|
||||||
*/
|
*/
|
||||||
public void sharePlaylist() {
|
private void sharePlaylist(final boolean shouldSharePlaylistDetails) {
|
||||||
|
final Context context = requireContext();
|
||||||
|
|
||||||
disposables.add(playlistManager.getPlaylistStreams(playlistId)
|
disposables.add(playlistManager.getPlaylistStreams(playlistId)
|
||||||
.flatMapSingle(playlist -> Single.just(playlist.stream()
|
.flatMapSingle(playlist -> Single.just(playlist.stream()
|
||||||
.map(PlaylistStreamEntry::getStreamEntity)
|
.map(PlaylistStreamEntry::getStreamEntity)
|
||||||
.map(StreamEntity::getUrl)
|
.map(streamEntity -> {
|
||||||
|
if (shouldSharePlaylistDetails) {
|
||||||
|
return context.getString(R.string.video_details_list_item,
|
||||||
|
streamEntity.getTitle(), streamEntity.getUrl());
|
||||||
|
} else {
|
||||||
|
return streamEntity.getUrl();
|
||||||
|
}
|
||||||
|
})
|
||||||
.collect(Collectors.joining("\n"))))
|
.collect(Collectors.joining("\n"))))
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.subscribe(urlsText -> ShareUtils.shareText(requireContext(), name, urlsText),
|
.subscribe(urlsText -> ShareUtils.shareText(
|
||||||
|
context, name, shouldSharePlaylistDetails
|
||||||
|
? context.getString(R.string.share_playlist_content_details,
|
||||||
|
name, urlsText) : urlsText),
|
||||||
throwable -> showUiErrorSnackbar(this, "Sharing playlist", throwable)));
|
throwable -> showUiErrorSnackbar(this, "Sharing playlist", throwable)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -498,38 +534,11 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
|
||||||
}
|
}
|
||||||
setVideoCount(itemListAdapter.getItemsList().size());
|
setVideoCount(itemListAdapter.getItemsList().size());
|
||||||
|
|
||||||
playlistControlBinding.playlistCtrlPlayAllButton.setOnClickListener(view -> {
|
PlayButtonHelper.initPlaylistControlClickListener(activity, playlistControlBinding, this);
|
||||||
NavigationHelper.playOnMainPlayer(activity, getPlayQueue());
|
|
||||||
showHoldToAppendTipIfNeeded();
|
|
||||||
});
|
|
||||||
playlistControlBinding.playlistCtrlPlayPopupButton.setOnClickListener(view -> {
|
|
||||||
NavigationHelper.playOnPopupPlayer(activity, getPlayQueue(), false);
|
|
||||||
showHoldToAppendTipIfNeeded();
|
|
||||||
});
|
|
||||||
playlistControlBinding.playlistCtrlPlayBgButton.setOnClickListener(view -> {
|
|
||||||
NavigationHelper.playOnBackgroundPlayer(activity, getPlayQueue(), false);
|
|
||||||
showHoldToAppendTipIfNeeded();
|
|
||||||
});
|
|
||||||
playlistControlBinding.playlistCtrlPlayPopupButton.setOnLongClickListener(view -> {
|
|
||||||
NavigationHelper.enqueueOnPlayer(activity, getPlayQueue(), PlayerType.POPUP);
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
|
|
||||||
playlistControlBinding.playlistCtrlPlayBgButton.setOnLongClickListener(view -> {
|
|
||||||
NavigationHelper.enqueueOnPlayer(activity, getPlayQueue(), PlayerType.AUDIO);
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
|
|
||||||
hideLoading();
|
hideLoading();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void showHoldToAppendTipIfNeeded() {
|
|
||||||
if (PreferenceManager.getDefaultSharedPreferences(activity)
|
|
||||||
.getBoolean(getString(R.string.show_hold_to_append_key), true)) {
|
|
||||||
Toast.makeText(activity, R.string.hold_to_append, Toast.LENGTH_SHORT).show();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////
|
||||||
// Fragment Error Handling
|
// Fragment Error Handling
|
||||||
///////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////
|
||||||
|
@ -853,7 +862,7 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private PlayQueue getPlayQueue() {
|
public PlayQueue getPlayQueue() {
|
||||||
return getPlayQueue(0);
|
return getPlayQueue(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -871,5 +880,29 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
|
||||||
}
|
}
|
||||||
return new SinglePlayQueue(streamInfoItems, index);
|
return new SinglePlayQueue(streamInfoItems, index);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a dialog to confirm whether the user wants to share the playlist
|
||||||
|
* with the playlist details or just the list of stream URLs.
|
||||||
|
* After the user has made a choice, the playlist is shared.
|
||||||
|
*/
|
||||||
|
private void createShareConfirmationDialog() {
|
||||||
|
new AlertDialog.Builder(requireContext())
|
||||||
|
.setTitle(R.string.share_playlist)
|
||||||
|
.setMessage(R.string.share_playlist_with_titles_message)
|
||||||
|
.setCancelable(true)
|
||||||
|
.setPositiveButton(R.string.share_playlist_with_titles, (dialog, which) ->
|
||||||
|
sharePlaylist(/* shouldSharePlaylistDetails= */ true)
|
||||||
|
)
|
||||||
|
.setNegativeButton(R.string.share_playlist_with_list, (dialog, which) ->
|
||||||
|
sharePlaylist(/* shouldSharePlaylistDetails= */ false)
|
||||||
|
)
|
||||||
|
.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTabsPagerAdapter(
|
||||||
|
@Nullable final MainFragment.SelectedTabsPagerAdapter tabsPagerAdapter) {
|
||||||
|
this.tabsPagerAdapter = tabsPagerAdapter;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -115,6 +115,11 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
|
||||||
feedGroupsCarouselState = feedGroupsCarousel.onSaveInstanceState()
|
feedGroupsCarouselState = feedGroupsCarousel.onSaveInstanceState()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onDestroyView() {
|
||||||
|
super.onDestroyView()
|
||||||
|
_binding = null
|
||||||
|
}
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
disposables.dispose()
|
disposables.dispose()
|
||||||
|
@ -336,8 +341,7 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
|
||||||
val actions = DialogInterface.OnClickListener { _, i ->
|
val actions = DialogInterface.OnClickListener { _, i ->
|
||||||
when (i) {
|
when (i) {
|
||||||
0 -> ShareUtils.shareText(
|
0 -> ShareUtils.shareText(
|
||||||
requireContext(), selectedItem.name, selectedItem.url,
|
requireContext(), selectedItem.name, selectedItem.url, selectedItem.thumbnails
|
||||||
selectedItem.thumbnailUrl
|
|
||||||
)
|
)
|
||||||
1 -> ShareUtils.openUrlInBrowser(requireContext(), selectedItem.url)
|
1 -> ShareUtils.openUrlInBrowser(requireContext(), selectedItem.url)
|
||||||
2 -> deleteChannel(selectedItem)
|
2 -> deleteChannel(selectedItem)
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package org.schabi.newpipe.local.subscription
|
package org.schabi.newpipe.local.subscription
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.util.Pair
|
||||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||||
import io.reactivex.rxjava3.core.Completable
|
import io.reactivex.rxjava3.core.Completable
|
||||||
import io.reactivex.rxjava3.core.Flowable
|
import io.reactivex.rxjava3.core.Flowable
|
||||||
|
@ -11,12 +12,13 @@ import org.schabi.newpipe.database.stream.model.StreamEntity
|
||||||
import org.schabi.newpipe.database.subscription.NotificationMode
|
import org.schabi.newpipe.database.subscription.NotificationMode
|
||||||
import org.schabi.newpipe.database.subscription.SubscriptionDAO
|
import org.schabi.newpipe.database.subscription.SubscriptionDAO
|
||||||
import org.schabi.newpipe.database.subscription.SubscriptionEntity
|
import org.schabi.newpipe.database.subscription.SubscriptionEntity
|
||||||
import org.schabi.newpipe.extractor.ListInfo
|
|
||||||
import org.schabi.newpipe.extractor.channel.ChannelInfo
|
import org.schabi.newpipe.extractor.channel.ChannelInfo
|
||||||
import org.schabi.newpipe.extractor.feed.FeedInfo
|
import org.schabi.newpipe.extractor.channel.tabs.ChannelTabInfo
|
||||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem
|
import org.schabi.newpipe.extractor.stream.StreamInfoItem
|
||||||
import org.schabi.newpipe.local.feed.FeedDatabaseManager
|
import org.schabi.newpipe.local.feed.FeedDatabaseManager
|
||||||
|
import org.schabi.newpipe.local.feed.service.FeedUpdateInfo
|
||||||
import org.schabi.newpipe.util.ExtractorHelper
|
import org.schabi.newpipe.util.ExtractorHelper
|
||||||
|
import org.schabi.newpipe.util.image.ImageStrategy
|
||||||
|
|
||||||
class SubscriptionManager(context: Context) {
|
class SubscriptionManager(context: Context) {
|
||||||
private val database = NewPipeDatabase.getInstance(context)
|
private val database = NewPipeDatabase.getInstance(context)
|
||||||
|
@ -46,28 +48,38 @@ class SubscriptionManager(context: Context) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun upsertAll(infoList: List<ChannelInfo>): List<SubscriptionEntity> {
|
fun upsertAll(infoList: List<Pair<ChannelInfo, List<ChannelTabInfo>>>): List<SubscriptionEntity> {
|
||||||
val listEntities = subscriptionTable.upsertAll(
|
val listEntities = subscriptionTable.upsertAll(
|
||||||
infoList.map { SubscriptionEntity.from(it) }
|
infoList.map { SubscriptionEntity.from(it.first) }
|
||||||
)
|
)
|
||||||
|
|
||||||
database.runInTransaction {
|
database.runInTransaction {
|
||||||
infoList.forEachIndexed { index, info ->
|
infoList.forEachIndexed { index, info ->
|
||||||
feedDatabaseManager.upsertAll(listEntities[index].uid, info.relatedItems)
|
info.second.forEach {
|
||||||
|
feedDatabaseManager.upsertAll(
|
||||||
|
listEntities[index].uid,
|
||||||
|
it.relatedItems.filterIsInstance<StreamInfoItem>()
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return listEntities
|
return listEntities
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateChannelInfo(info: ChannelInfo): Completable = subscriptionTable.getSubscription(info.serviceId, info.url)
|
fun updateChannelInfo(info: ChannelInfo): Completable =
|
||||||
.flatMapCompletable {
|
subscriptionTable.getSubscription(info.serviceId, info.url)
|
||||||
Completable.fromRunnable {
|
.flatMapCompletable {
|
||||||
it.setData(info.name, info.avatarUrl, info.description, info.subscriberCount)
|
Completable.fromRunnable {
|
||||||
subscriptionTable.update(it)
|
it.setData(
|
||||||
feedDatabaseManager.upsertAll(it.uid, info.relatedItems)
|
info.name,
|
||||||
|
ImageStrategy.imageListToDbUrl(info.avatars),
|
||||||
|
info.description,
|
||||||
|
info.subscriberCount
|
||||||
|
)
|
||||||
|
subscriptionTable.update(it)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
fun updateNotificationMode(serviceId: Int, url: String, @NotificationMode mode: Int): Completable {
|
fun updateNotificationMode(serviceId: Int, url: String, @NotificationMode mode: Int): Completable {
|
||||||
return subscriptionTable().getSubscription(serviceId, url)
|
return subscriptionTable().getSubscription(serviceId, url)
|
||||||
|
@ -84,19 +96,15 @@ class SubscriptionManager(context: Context) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateFromInfo(subscriptionId: Long, info: ListInfo<StreamInfoItem>) {
|
fun updateFromInfo(info: FeedUpdateInfo) {
|
||||||
val subscriptionEntity = subscriptionTable.getSubscription(subscriptionId)
|
val subscriptionEntity = subscriptionTable.getSubscription(info.uid)
|
||||||
|
|
||||||
if (info is FeedInfo) {
|
subscriptionEntity.name = info.name
|
||||||
subscriptionEntity.name = info.name
|
subscriptionEntity.avatarUrl = info.avatarUrl
|
||||||
} else if (info is ChannelInfo) {
|
|
||||||
subscriptionEntity.setData(
|
// these two fields are null if the feed info was fetched using the fast feed method
|
||||||
info.name,
|
info.description?.let { subscriptionEntity.description = it }
|
||||||
info.avatarUrl,
|
info.subscriberCount?.let { subscriptionEntity.subscriberCount = it }
|
||||||
info.description,
|
|
||||||
info.subscriberCount
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
subscriptionTable.update(subscriptionEntity)
|
subscriptionTable.update(subscriptionEntity)
|
||||||
}
|
}
|
||||||
|
@ -107,11 +115,8 @@ class SubscriptionManager(context: Context) {
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
}
|
}
|
||||||
|
|
||||||
fun insertSubscription(subscriptionEntity: SubscriptionEntity, info: ChannelInfo) {
|
fun insertSubscription(subscriptionEntity: SubscriptionEntity) {
|
||||||
database.runInTransaction {
|
subscriptionTable.insert(subscriptionEntity)
|
||||||
val subscriptionId = subscriptionTable.insert(subscriptionEntity)
|
|
||||||
feedDatabaseManager.upsertAll(subscriptionId, info.relatedItems)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun deleteSubscription(subscriptionEntity: SubscriptionEntity) {
|
fun deleteSubscription(subscriptionEntity: SubscriptionEntity) {
|
||||||
|
@ -125,7 +130,10 @@ class SubscriptionManager(context: Context) {
|
||||||
*/
|
*/
|
||||||
private fun rememberAllStreams(subscription: SubscriptionEntity): Completable {
|
private fun rememberAllStreams(subscription: SubscriptionEntity): Completable {
|
||||||
return ExtractorHelper.getChannelInfo(subscription.serviceId, subscription.url, false)
|
return ExtractorHelper.getChannelInfo(subscription.serviceId, subscription.url, false)
|
||||||
.map { channel -> channel.relatedItems.map { stream -> StreamEntity(stream) } }
|
.flatMap { info ->
|
||||||
|
ExtractorHelper.getChannelTab(subscription.serviceId, info.tabs.first(), false)
|
||||||
|
}
|
||||||
|
.map { channel -> channel.relatedItems.filterIsInstance<StreamInfoItem>().map { stream -> StreamEntity(stream) } }
|
||||||
.flatMapCompletable { entities ->
|
.flatMapCompletable { entities ->
|
||||||
Completable.fromAction {
|
Completable.fromAction {
|
||||||
database.streamDAO().upsertAll(entities)
|
database.streamDAO().upsertAll(entities)
|
||||||
|
|
|
@ -55,10 +55,10 @@ class FeedGroupDialog : DialogFragment(), BackPressable {
|
||||||
private var groupSortOrder: Long = -1
|
private var groupSortOrder: Long = -1
|
||||||
|
|
||||||
sealed class ScreenState : Serializable {
|
sealed class ScreenState : Serializable {
|
||||||
object InitialScreen : ScreenState()
|
data object InitialScreen : ScreenState()
|
||||||
object IconPickerScreen : ScreenState()
|
data object IconPickerScreen : ScreenState()
|
||||||
object SubscriptionsPickerScreen : ScreenState()
|
data object SubscriptionsPickerScreen : ScreenState()
|
||||||
object DeleteScreen : ScreenState()
|
data object DeleteScreen : ScreenState()
|
||||||
}
|
}
|
||||||
|
|
||||||
@State @JvmField var selectedIcon: FeedGroupIcon? = null
|
@State @JvmField var selectedIcon: FeedGroupIcon? = null
|
||||||
|
@ -370,7 +370,7 @@ class FeedGroupDialog : DialogFragment(), BackPressable {
|
||||||
|
|
||||||
private fun setupIconPicker() {
|
private fun setupIconPicker() {
|
||||||
val groupAdapter = GroupieAdapter()
|
val groupAdapter = GroupieAdapter()
|
||||||
groupAdapter.addAll(FeedGroupIcon.values().map { PickerIconItem(it) })
|
groupAdapter.addAll(FeedGroupIcon.entries.map { PickerIconItem(it) })
|
||||||
|
|
||||||
feedGroupCreateBinding.iconSelector.apply {
|
feedGroupCreateBinding.iconSelector.apply {
|
||||||
layoutManager = GridLayoutManager(requireContext(), 7, RecyclerView.VERTICAL, false)
|
layoutManager = GridLayoutManager(requireContext(), 7, RecyclerView.VERTICAL, false)
|
||||||
|
|
|
@ -110,8 +110,8 @@ class FeedGroupDialogViewModel(
|
||||||
}
|
}
|
||||||
|
|
||||||
sealed class DialogEvent {
|
sealed class DialogEvent {
|
||||||
object ProcessingEvent : DialogEvent()
|
data object ProcessingEvent : DialogEvent()
|
||||||
object SuccessEvent : DialogEvent()
|
data object SuccessEvent : DialogEvent()
|
||||||
}
|
}
|
||||||
|
|
||||||
data class Filter(val query: String, val showOnlyUngrouped: Boolean)
|
data class Filter(val query: String, val showOnlyUngrouped: Boolean)
|
||||||
|
|
|
@ -9,7 +9,7 @@ import org.schabi.newpipe.R
|
||||||
import org.schabi.newpipe.extractor.channel.ChannelInfoItem
|
import org.schabi.newpipe.extractor.channel.ChannelInfoItem
|
||||||
import org.schabi.newpipe.util.Localization
|
import org.schabi.newpipe.util.Localization
|
||||||
import org.schabi.newpipe.util.OnClickGesture
|
import org.schabi.newpipe.util.OnClickGesture
|
||||||
import org.schabi.newpipe.util.PicassoHelper
|
import org.schabi.newpipe.util.image.PicassoHelper
|
||||||
|
|
||||||
class ChannelItem(
|
class ChannelItem(
|
||||||
private val infoItem: ChannelInfoItem,
|
private val infoItem: ChannelInfoItem,
|
||||||
|
@ -39,7 +39,7 @@ class ChannelItem(
|
||||||
itemChannelDescriptionView.text = infoItem.description
|
itemChannelDescriptionView.text = infoItem.description
|
||||||
}
|
}
|
||||||
|
|
||||||
PicassoHelper.loadAvatar(infoItem.thumbnailUrl).into(itemThumbnailView)
|
PicassoHelper.loadAvatar(infoItem.thumbnails).into(itemThumbnailView)
|
||||||
|
|
||||||
gesturesListener?.run {
|
gesturesListener?.run {
|
||||||
viewHolder.root.setOnClickListener { selected(infoItem) }
|
viewHolder.root.setOnClickListener { selected(infoItem) }
|
||||||
|
|
|
@ -10,7 +10,7 @@ import org.schabi.newpipe.database.subscription.SubscriptionEntity
|
||||||
import org.schabi.newpipe.databinding.PickerSubscriptionItemBinding
|
import org.schabi.newpipe.databinding.PickerSubscriptionItemBinding
|
||||||
import org.schabi.newpipe.ktx.AnimationType
|
import org.schabi.newpipe.ktx.AnimationType
|
||||||
import org.schabi.newpipe.ktx.animate
|
import org.schabi.newpipe.ktx.animate
|
||||||
import org.schabi.newpipe.util.PicassoHelper
|
import org.schabi.newpipe.util.image.PicassoHelper
|
||||||
|
|
||||||
data class PickerSubscriptionItem(
|
data class PickerSubscriptionItem(
|
||||||
val subscriptionEntity: SubscriptionEntity,
|
val subscriptionEntity: SubscriptionEntity,
|
||||||
|
|
|
@ -25,6 +25,7 @@ import android.content.Intent;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
|
import androidx.core.content.IntentCompat;
|
||||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
|
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
|
||||||
|
|
||||||
import org.reactivestreams.Subscriber;
|
import org.reactivestreams.Subscriber;
|
||||||
|
@ -65,7 +66,7 @@ public class SubscriptionsExportService extends BaseImportExportService {
|
||||||
return START_NOT_STICKY;
|
return START_NOT_STICKY;
|
||||||
}
|
}
|
||||||
|
|
||||||
final Uri path = intent.getParcelableExtra(KEY_FILE_PATH);
|
final Uri path = IntentCompat.getParcelableExtra(intent, KEY_FILE_PATH, Uri.class);
|
||||||
if (path == null) {
|
if (path == null) {
|
||||||
stopAndReportError(new IllegalStateException(
|
stopAndReportError(new IllegalStateException(
|
||||||
"Exporting to a file, but the path is null"),
|
"Exporting to a file, but the path is null"),
|
||||||
|
|
|
@ -26,9 +26,11 @@ import android.content.Intent;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
import android.util.Pair;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.core.content.IntentCompat;
|
||||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
|
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
|
||||||
|
|
||||||
import org.reactivestreams.Subscriber;
|
import org.reactivestreams.Subscriber;
|
||||||
|
@ -38,6 +40,7 @@ import org.schabi.newpipe.R;
|
||||||
import org.schabi.newpipe.database.subscription.SubscriptionEntity;
|
import org.schabi.newpipe.database.subscription.SubscriptionEntity;
|
||||||
import org.schabi.newpipe.extractor.NewPipe;
|
import org.schabi.newpipe.extractor.NewPipe;
|
||||||
import org.schabi.newpipe.extractor.channel.ChannelInfo;
|
import org.schabi.newpipe.extractor.channel.ChannelInfo;
|
||||||
|
import org.schabi.newpipe.extractor.channel.tabs.ChannelTabInfo;
|
||||||
import org.schabi.newpipe.extractor.subscription.SubscriptionItem;
|
import org.schabi.newpipe.extractor.subscription.SubscriptionItem;
|
||||||
import org.schabi.newpipe.ktx.ExceptionUtils;
|
import org.schabi.newpipe.ktx.ExceptionUtils;
|
||||||
import org.schabi.newpipe.streams.io.SharpInputStream;
|
import org.schabi.newpipe.streams.io.SharpInputStream;
|
||||||
|
@ -48,6 +51,7 @@ import org.schabi.newpipe.util.ExtractorHelper;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
|
||||||
|
@ -105,7 +109,7 @@ public class SubscriptionsImportService extends BaseImportExportService {
|
||||||
if (currentMode == CHANNEL_URL_MODE) {
|
if (currentMode == CHANNEL_URL_MODE) {
|
||||||
channelUrl = intent.getStringExtra(KEY_VALUE);
|
channelUrl = intent.getStringExtra(KEY_VALUE);
|
||||||
} else {
|
} else {
|
||||||
final Uri uri = intent.getParcelableExtra(KEY_VALUE);
|
final Uri uri = IntentCompat.getParcelableExtra(intent, KEY_VALUE, Uri.class);
|
||||||
if (uri == null) {
|
if (uri == null) {
|
||||||
stopAndReportError(new IllegalStateException(
|
stopAndReportError(new IllegalStateException(
|
||||||
"Importing from input stream, but file path is null"),
|
"Importing from input stream, but file path is null"),
|
||||||
|
@ -199,12 +203,19 @@ public class SubscriptionsImportService extends BaseImportExportService {
|
||||||
|
|
||||||
.parallel(PARALLEL_EXTRACTIONS)
|
.parallel(PARALLEL_EXTRACTIONS)
|
||||||
.runOn(Schedulers.io())
|
.runOn(Schedulers.io())
|
||||||
.map((Function<SubscriptionItem, Notification<ChannelInfo>>) subscriptionItem -> {
|
.map((Function<SubscriptionItem, Notification<Pair<ChannelInfo,
|
||||||
|
List<ChannelTabInfo>>>>) subscriptionItem -> {
|
||||||
try {
|
try {
|
||||||
return Notification.createOnNext(ExtractorHelper
|
final ChannelInfo channelInfo = ExtractorHelper
|
||||||
.getChannelInfo(subscriptionItem.getServiceId(),
|
.getChannelInfo(subscriptionItem.getServiceId(),
|
||||||
subscriptionItem.getUrl(), true)
|
subscriptionItem.getUrl(), true)
|
||||||
.blockingGet());
|
.blockingGet();
|
||||||
|
return Notification.createOnNext(new Pair<>(channelInfo,
|
||||||
|
Collections.singletonList(
|
||||||
|
ExtractorHelper.getChannelTab(
|
||||||
|
subscriptionItem.getServiceId(),
|
||||||
|
channelInfo.getTabs().get(0), true).blockingGet()
|
||||||
|
)));
|
||||||
} catch (final Throwable e) {
|
} catch (final Throwable e) {
|
||||||
return Notification.createOnError(e);
|
return Notification.createOnError(e);
|
||||||
}
|
}
|
||||||
|
@ -223,7 +234,7 @@ public class SubscriptionsImportService extends BaseImportExportService {
|
||||||
}
|
}
|
||||||
|
|
||||||
private Subscriber<List<SubscriptionEntity>> getSubscriber() {
|
private Subscriber<List<SubscriptionEntity>> getSubscriber() {
|
||||||
return new Subscriber<List<SubscriptionEntity>>() {
|
return new Subscriber<>() {
|
||||||
@Override
|
@Override
|
||||||
public void onSubscribe(final Subscription s) {
|
public void onSubscribe(final Subscription s) {
|
||||||
subscription = s;
|
subscription = s;
|
||||||
|
@ -254,10 +265,11 @@ public class SubscriptionsImportService extends BaseImportExportService {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private Consumer<Notification<ChannelInfo>> getNotificationsConsumer() {
|
private Consumer<Notification<Pair<ChannelInfo,
|
||||||
|
List<ChannelTabInfo>>>> getNotificationsConsumer() {
|
||||||
return notification -> {
|
return notification -> {
|
||||||
if (notification.isOnNext()) {
|
if (notification.isOnNext()) {
|
||||||
final String name = notification.getValue().getName();
|
final String name = notification.getValue().first.getName();
|
||||||
eventListener.onItemCompleted(!TextUtils.isEmpty(name) ? name : "");
|
eventListener.onItemCompleted(!TextUtils.isEmpty(name) ? name : "");
|
||||||
} else if (notification.isOnError()) {
|
} else if (notification.isOnError()) {
|
||||||
final Throwable error = notification.getError();
|
final Throwable error = notification.getError();
|
||||||
|
@ -275,10 +287,12 @@ public class SubscriptionsImportService extends BaseImportExportService {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private Function<List<Notification<ChannelInfo>>, List<SubscriptionEntity>> upsertBatch() {
|
private Function<List<Notification<Pair<ChannelInfo, List<ChannelTabInfo>>>>,
|
||||||
|
List<SubscriptionEntity>> upsertBatch() {
|
||||||
return notificationList -> {
|
return notificationList -> {
|
||||||
final List<ChannelInfo> infoList = new ArrayList<>(notificationList.size());
|
final List<Pair<ChannelInfo, List<ChannelTabInfo>>> infoList =
|
||||||
for (final Notification<ChannelInfo> n : notificationList) {
|
new ArrayList<>(notificationList.size());
|
||||||
|
for (final Notification<Pair<ChannelInfo, List<ChannelTabInfo>>> n : notificationList) {
|
||||||
if (n.isOnNext()) {
|
if (n.isOnNext()) {
|
||||||
infoList.add(n.getValue());
|
infoList.add(n.getValue());
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,6 +18,7 @@ import android.view.MenuItem;
|
||||||
import android.view.SubMenu;
|
import android.view.SubMenu;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
|
import android.widget.ImageButton;
|
||||||
import android.widget.SeekBar;
|
import android.widget.SeekBar;
|
||||||
|
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
@ -542,18 +543,19 @@ public final class PlayQueueActivity extends AppCompatActivity
|
||||||
////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
private void onStateChanged(final int state) {
|
private void onStateChanged(final int state) {
|
||||||
|
final ImageButton playPauseButton = queueControlBinding.controlPlayPause;
|
||||||
switch (state) {
|
switch (state) {
|
||||||
case Player.STATE_PAUSED:
|
case Player.STATE_PAUSED:
|
||||||
queueControlBinding.controlPlayPause
|
playPauseButton.setImageResource(R.drawable.ic_play_arrow);
|
||||||
.setImageResource(R.drawable.ic_play_arrow);
|
playPauseButton.setContentDescription(getString(R.string.play));
|
||||||
break;
|
break;
|
||||||
case Player.STATE_PLAYING:
|
case Player.STATE_PLAYING:
|
||||||
queueControlBinding.controlPlayPause
|
playPauseButton.setImageResource(R.drawable.ic_pause);
|
||||||
.setImageResource(R.drawable.ic_pause);
|
playPauseButton.setContentDescription(getString(R.string.pause));
|
||||||
break;
|
break;
|
||||||
case Player.STATE_COMPLETED:
|
case Player.STATE_COMPLETED:
|
||||||
queueControlBinding.controlPlayPause
|
playPauseButton.setImageResource(R.drawable.ic_replay);
|
||||||
.setImageResource(R.drawable.ic_replay);
|
playPauseButton.setContentDescription(getString(R.string.replay));
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
|
@ -596,11 +598,9 @@ public final class PlayQueueActivity extends AppCompatActivity
|
||||||
}
|
}
|
||||||
|
|
||||||
private void onPlaybackParameterChanged(@Nullable final PlaybackParameters parameters) {
|
private void onPlaybackParameterChanged(@Nullable final PlaybackParameters parameters) {
|
||||||
if (parameters != null) {
|
if (parameters != null && menu != null && player != null) {
|
||||||
if (menu != null && player != null) {
|
final MenuItem item = menu.findItem(R.id.action_playback_speed);
|
||||||
final MenuItem item = menu.findItem(R.id.action_playback_speed);
|
item.setTitle(formatSpeed(parameters.speed));
|
||||||
item.setTitle(formatSpeed(parameters.speed));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -630,11 +630,13 @@ public final class PlayQueueActivity extends AppCompatActivity
|
||||||
|
|
||||||
final MenuItem audioTrackSelector = menu.findItem(R.id.action_audio_track);
|
final MenuItem audioTrackSelector = menu.findItem(R.id.action_audio_track);
|
||||||
final List<AudioStream> availableStreams =
|
final List<AudioStream> availableStreams =
|
||||||
Optional.ofNullable(player.getCurrentMetadata())
|
Optional.ofNullable(player)
|
||||||
|
.map(Player::getCurrentMetadata)
|
||||||
.flatMap(MediaItemTag::getMaybeAudioTrack)
|
.flatMap(MediaItemTag::getMaybeAudioTrack)
|
||||||
.map(MediaItemTag.AudioTrack::getAudioStreams)
|
.map(MediaItemTag.AudioTrack::getAudioStreams)
|
||||||
.orElse(null);
|
.orElse(null);
|
||||||
final Optional<AudioStream> selectedAudioStream = player.getSelectedAudioStream();
|
final Optional<AudioStream> selectedAudioStream = Optional.ofNullable(player)
|
||||||
|
.flatMap(Player::getSelectedAudioStream);
|
||||||
|
|
||||||
if (availableStreams == null || availableStreams.size() < 2
|
if (availableStreams == null || availableStreams.size() < 2
|
||||||
|| selectedAudioStream.isEmpty()) {
|
|| selectedAudioStream.isEmpty()) {
|
||||||
|
|
|
@ -89,6 +89,7 @@ import org.schabi.newpipe.error.ErrorInfo;
|
||||||
import org.schabi.newpipe.error.ErrorUtil;
|
import org.schabi.newpipe.error.ErrorUtil;
|
||||||
import org.schabi.newpipe.error.UserAction;
|
import org.schabi.newpipe.error.UserAction;
|
||||||
import org.schabi.newpipe.extractor.stream.AudioStream;
|
import org.schabi.newpipe.extractor.stream.AudioStream;
|
||||||
|
import org.schabi.newpipe.extractor.Image;
|
||||||
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||||
import org.schabi.newpipe.extractor.stream.StreamType;
|
import org.schabi.newpipe.extractor.stream.StreamType;
|
||||||
import org.schabi.newpipe.extractor.stream.VideoStream;
|
import org.schabi.newpipe.extractor.stream.VideoStream;
|
||||||
|
@ -119,7 +120,7 @@ import org.schabi.newpipe.player.ui.VideoPlayerUi;
|
||||||
import org.schabi.newpipe.util.DependentPreferenceHelper;
|
import org.schabi.newpipe.util.DependentPreferenceHelper;
|
||||||
import org.schabi.newpipe.util.ListHelper;
|
import org.schabi.newpipe.util.ListHelper;
|
||||||
import org.schabi.newpipe.util.NavigationHelper;
|
import org.schabi.newpipe.util.NavigationHelper;
|
||||||
import org.schabi.newpipe.util.PicassoHelper;
|
import org.schabi.newpipe.util.image.PicassoHelper;
|
||||||
import org.schabi.newpipe.util.SerializedCache;
|
import org.schabi.newpipe.util.SerializedCache;
|
||||||
import org.schabi.newpipe.util.SponsorBlockMode;
|
import org.schabi.newpipe.util.SponsorBlockMode;
|
||||||
import org.schabi.newpipe.util.StreamTypeUtil;
|
import org.schabi.newpipe.util.StreamTypeUtil;
|
||||||
|
@ -818,10 +819,10 @@ public final class Player implements PlaybackListener, Listener {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private void loadCurrentThumbnail(final String url) {
|
private void loadCurrentThumbnail(final List<Image> thumbnails) {
|
||||||
if (DEBUG) {
|
if (DEBUG) {
|
||||||
Log.d(TAG, "Thumbnail - loadCurrentThumbnail() called with url = ["
|
Log.d(TAG, "Thumbnail - loadCurrentThumbnail() called with thumbnails = ["
|
||||||
+ (url == null ? "null" : url) + "]");
|
+ thumbnails.size() + "]");
|
||||||
}
|
}
|
||||||
|
|
||||||
// first cancel any previous loading
|
// first cancel any previous loading
|
||||||
|
@ -830,12 +831,12 @@ public final class Player implements PlaybackListener, Listener {
|
||||||
// Unset currentThumbnail, since it is now outdated. This ensures it is not used in media
|
// Unset currentThumbnail, since it is now outdated. This ensures it is not used in media
|
||||||
// session metadata while the new thumbnail is being loaded by Picasso.
|
// session metadata while the new thumbnail is being loaded by Picasso.
|
||||||
onThumbnailLoaded(null);
|
onThumbnailLoaded(null);
|
||||||
if (isNullOrEmpty(url)) {
|
if (thumbnails.isEmpty()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// scale down the notification thumbnail for performance
|
// scale down the notification thumbnail for performance
|
||||||
PicassoHelper.loadScaledDownThumbnail(context, url)
|
PicassoHelper.loadScaledDownThumbnail(context, thumbnails)
|
||||||
.tag(PICASSO_PLAYER_THUMBNAIL_TAG)
|
.tag(PICASSO_PLAYER_THUMBNAIL_TAG)
|
||||||
.into(currentThumbnailTarget);
|
.into(currentThumbnailTarget);
|
||||||
}
|
}
|
||||||
|
@ -1180,7 +1181,7 @@ public final class Player implements PlaybackListener, Listener {
|
||||||
|
|
||||||
UIs.call(PlayerUi::onPrepared);
|
UIs.call(PlayerUi::onPrepared);
|
||||||
|
|
||||||
if (playWhenReady) {
|
if (playWhenReady && !isMuted()) {
|
||||||
audioReactor.requestAudioFocus();
|
audioReactor.requestAudioFocus();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1321,6 +1322,11 @@ public final class Player implements PlaybackListener, Listener {
|
||||||
public void toggleMute() {
|
public void toggleMute() {
|
||||||
final boolean wasMuted = isMuted();
|
final boolean wasMuted = isMuted();
|
||||||
simpleExoPlayer.setVolume(wasMuted ? 1 : 0);
|
simpleExoPlayer.setVolume(wasMuted ? 1 : 0);
|
||||||
|
if (wasMuted) {
|
||||||
|
audioReactor.requestAudioFocus();
|
||||||
|
} else {
|
||||||
|
audioReactor.abandonAudioFocus();
|
||||||
|
}
|
||||||
UIs.call(playerUi -> playerUi.onMuteUnmuteChanged(!wasMuted));
|
UIs.call(playerUi -> playerUi.onMuteUnmuteChanged(!wasMuted));
|
||||||
notifyPlaybackUpdateToListeners();
|
notifyPlaybackUpdateToListeners();
|
||||||
}
|
}
|
||||||
|
@ -1718,7 +1724,9 @@ public final class Player implements PlaybackListener, Listener {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
audioReactor.requestAudioFocus();
|
if (!isMuted()) {
|
||||||
|
audioReactor.requestAudioFocus();
|
||||||
|
}
|
||||||
|
|
||||||
if (currentState == STATE_COMPLETED) {
|
if (currentState == STATE_COMPLETED) {
|
||||||
if (playQueue.getIndex() == 0) {
|
if (playQueue.getIndex() == 0) {
|
||||||
|
@ -1883,7 +1891,7 @@ public final class Player implements PlaybackListener, Listener {
|
||||||
|
|
||||||
maybeAutoQueueNextStream(info);
|
maybeAutoQueueNextStream(info);
|
||||||
|
|
||||||
loadCurrentThumbnail(info.getThumbnailUrl());
|
loadCurrentThumbnail(info.getThumbnails());
|
||||||
registerStreamViewed();
|
registerStreamViewed();
|
||||||
|
|
||||||
notifyMetadataUpdateToListeners();
|
notifyMetadataUpdateToListeners();
|
||||||
|
|
|
@ -29,6 +29,7 @@ import android.os.IBinder;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
import org.schabi.newpipe.player.mediasession.MediaSessionPlayerUi;
|
import org.schabi.newpipe.player.mediasession.MediaSessionPlayerUi;
|
||||||
|
import org.schabi.newpipe.player.notification.NotificationPlayerUi;
|
||||||
import org.schabi.newpipe.util.ThemeHelper;
|
import org.schabi.newpipe.util.ThemeHelper;
|
||||||
|
|
||||||
import java.lang.ref.WeakReference;
|
import java.lang.ref.WeakReference;
|
||||||
|
@ -59,6 +60,14 @@ public final class PlayerService extends Service {
|
||||||
ThemeHelper.setTheme(this);
|
ThemeHelper.setTheme(this);
|
||||||
|
|
||||||
player = new Player(this);
|
player = new Player(this);
|
||||||
|
/*
|
||||||
|
Create the player notification and start immediately the service in foreground,
|
||||||
|
otherwise if nothing is played or initializing the player and its components (especially
|
||||||
|
loading stream metadata) takes a lot of time, the app would crash on Android 8+ as the
|
||||||
|
service would never be put in the foreground while we said to the system we would do so
|
||||||
|
*/
|
||||||
|
player.UIs().get(NotificationPlayerUi.class)
|
||||||
|
.ifPresent(NotificationPlayerUi::createNotificationAndStartForeground);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -68,16 +77,38 @@ public final class PlayerService extends Service {
|
||||||
+ "], flags = [" + flags + "], startId = [" + startId + "]");
|
+ "], flags = [" + flags + "], startId = [" + startId + "]");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Be sure that the player notification is set and the service is started in foreground,
|
||||||
|
otherwise, the app may crash on Android 8+ as the service would never be put in the
|
||||||
|
foreground while we said to the system we would do so
|
||||||
|
The service is always requested to be started in foreground, so always creating a
|
||||||
|
notification if there is no one already and starting the service in foreground should
|
||||||
|
not create any issues
|
||||||
|
If the service is already started in foreground, requesting it to be started shouldn't
|
||||||
|
do anything
|
||||||
|
*/
|
||||||
|
if (player != null) {
|
||||||
|
player.UIs().get(NotificationPlayerUi.class)
|
||||||
|
.ifPresent(NotificationPlayerUi::createNotificationAndStartForeground);
|
||||||
|
}
|
||||||
|
|
||||||
if (Intent.ACTION_MEDIA_BUTTON.equals(intent.getAction())
|
if (Intent.ACTION_MEDIA_BUTTON.equals(intent.getAction())
|
||||||
&& player.getPlayQueue() == null) {
|
&& (player == null || player.getPlayQueue() == null)) {
|
||||||
// No need to process media button's actions if the player is not working, otherwise the
|
/*
|
||||||
// player service would strangely start with nothing to play
|
No need to process media button's actions if the player is not working, otherwise
|
||||||
|
the player service would strangely start with nothing to play
|
||||||
|
Stop the service in this case, which will be removed from the foreground and its
|
||||||
|
notification cancelled in its destruction
|
||||||
|
*/
|
||||||
|
stopSelf();
|
||||||
return START_NOT_STICKY;
|
return START_NOT_STICKY;
|
||||||
}
|
}
|
||||||
|
|
||||||
player.handleIntent(intent);
|
if (player != null) {
|
||||||
player.UIs().get(MediaSessionPlayerUi.class)
|
player.handleIntent(intent);
|
||||||
.ifPresent(ui -> ui.handleMediaButtonIntent(intent));
|
player.UIs().get(MediaSessionPlayerUi.class)
|
||||||
|
.ifPresent(ui -> ui.handleMediaButtonIntent(intent));
|
||||||
|
}
|
||||||
|
|
||||||
return START_NOT_STICKY;
|
return START_NOT_STICKY;
|
||||||
}
|
}
|
||||||
|
@ -87,7 +118,7 @@ public final class PlayerService extends Service {
|
||||||
Log.d(TAG, "stopForImmediateReusing() called");
|
Log.d(TAG, "stopForImmediateReusing() called");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!player.exoPlayerIsNull()) {
|
if (player != null && !player.exoPlayerIsNull()) {
|
||||||
// Releases wifi & cpu, disables keepScreenOn, etc.
|
// Releases wifi & cpu, disables keepScreenOn, etc.
|
||||||
// We can't just pause the player here because it will make transition
|
// We can't just pause the player here because it will make transition
|
||||||
// from one stream to a new stream not smooth
|
// from one stream to a new stream not smooth
|
||||||
|
@ -98,7 +129,7 @@ public final class PlayerService extends Service {
|
||||||
@Override
|
@Override
|
||||||
public void onTaskRemoved(final Intent rootIntent) {
|
public void onTaskRemoved(final Intent rootIntent) {
|
||||||
super.onTaskRemoved(rootIntent);
|
super.onTaskRemoved(rootIntent);
|
||||||
if (!player.videoPlayerSelected()) {
|
if (player != null && !player.videoPlayerSelected()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
onDestroy();
|
onDestroy();
|
||||||
|
|
|
@ -7,7 +7,6 @@ import android.view.View.OnTouchListener
|
||||||
import android.widget.ProgressBar
|
import android.widget.ProgressBar
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.appcompat.content.res.AppCompatResources
|
import androidx.appcompat.content.res.AppCompatResources
|
||||||
import androidx.core.math.MathUtils
|
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import org.schabi.newpipe.MainActivity
|
import org.schabi.newpipe.MainActivity
|
||||||
import org.schabi.newpipe.R
|
import org.schabi.newpipe.R
|
||||||
|
@ -113,7 +112,7 @@ class MainPlayerGestureListener(
|
||||||
|
|
||||||
// Update progress bar
|
// Update progress bar
|
||||||
val oldBrightness = layoutParams.screenBrightness
|
val oldBrightness = layoutParams.screenBrightness
|
||||||
bar.progress = (bar.max * MathUtils.clamp(oldBrightness, 0f, 1f)).toInt()
|
bar.progress = (bar.max * oldBrightness.coerceIn(0f, 1f)).toInt()
|
||||||
bar.incrementProgressBy(distanceY.toInt())
|
bar.incrementProgressBy(distanceY.toInt())
|
||||||
|
|
||||||
// Update brightness
|
// Update brightness
|
||||||
|
@ -161,13 +160,12 @@ class MainPlayerGestureListener(
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onScroll(
|
override fun onScroll(
|
||||||
initialEvent: MotionEvent,
|
initialEvent: MotionEvent?,
|
||||||
movingEvent: MotionEvent,
|
movingEvent: MotionEvent,
|
||||||
distanceX: Float,
|
distanceX: Float,
|
||||||
distanceY: Float
|
distanceY: Float
|
||||||
): Boolean {
|
): Boolean {
|
||||||
|
if (initialEvent == null || !playerUi.isFullscreen) {
|
||||||
if (!playerUi.isFullscreen) {
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,7 @@ import android.util.Log
|
||||||
import android.view.MotionEvent
|
import android.view.MotionEvent
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewConfiguration
|
import android.view.ViewConfiguration
|
||||||
import androidx.core.math.MathUtils
|
import androidx.core.view.isVisible
|
||||||
import org.schabi.newpipe.MainActivity
|
import org.schabi.newpipe.MainActivity
|
||||||
import org.schabi.newpipe.ktx.AnimationType
|
import org.schabi.newpipe.ktx.AnimationType
|
||||||
import org.schabi.newpipe.ktx.animate
|
import org.schabi.newpipe.ktx.animate
|
||||||
|
@ -167,7 +167,7 @@ class PopupPlayerGestureListener(
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onFling(
|
override fun onFling(
|
||||||
e1: MotionEvent,
|
e1: MotionEvent?,
|
||||||
e2: MotionEvent,
|
e2: MotionEvent,
|
||||||
velocityX: Float,
|
velocityX: Float,
|
||||||
velocityY: Float
|
velocityY: Float
|
||||||
|
@ -218,11 +218,14 @@ class PopupPlayerGestureListener(
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onScroll(
|
override fun onScroll(
|
||||||
initialEvent: MotionEvent,
|
initialEvent: MotionEvent?,
|
||||||
movingEvent: MotionEvent,
|
movingEvent: MotionEvent,
|
||||||
distanceX: Float,
|
distanceX: Float,
|
||||||
distanceY: Float
|
distanceY: Float
|
||||||
): Boolean {
|
): Boolean {
|
||||||
|
if (initialEvent == null) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
if (isResizing) {
|
if (isResizing) {
|
||||||
return super.onScroll(initialEvent, movingEvent, distanceX, distanceY)
|
return super.onScroll(initialEvent, movingEvent, distanceX, distanceY)
|
||||||
|
@ -235,14 +238,16 @@ class PopupPlayerGestureListener(
|
||||||
isMoving = true
|
isMoving = true
|
||||||
|
|
||||||
val diffX = (movingEvent.rawX - initialEvent.rawX)
|
val diffX = (movingEvent.rawX - initialEvent.rawX)
|
||||||
val posX = MathUtils.clamp(
|
val posX = (initialPopupX + diffX).coerceIn(
|
||||||
initialPopupX + diffX,
|
0f,
|
||||||
0f, (playerUi.screenWidth - playerUi.popupLayoutParams.width).toFloat()
|
(playerUi.screenWidth - playerUi.popupLayoutParams.width).toFloat()
|
||||||
|
.coerceAtLeast(0f)
|
||||||
)
|
)
|
||||||
val diffY = (movingEvent.rawY - initialEvent.rawY)
|
val diffY = (movingEvent.rawY - initialEvent.rawY)
|
||||||
val posY = MathUtils.clamp(
|
val posY = (initialPopupY + diffY).coerceIn(
|
||||||
initialPopupY + diffY,
|
0f,
|
||||||
0f, (playerUi.screenHeight - playerUi.popupLayoutParams.height).toFloat()
|
(playerUi.screenHeight - playerUi.popupLayoutParams.height).toFloat()
|
||||||
|
.coerceAtLeast(0f)
|
||||||
)
|
)
|
||||||
|
|
||||||
playerUi.popupLayoutParams.x = posX.toInt()
|
playerUi.popupLayoutParams.x = posX.toInt()
|
||||||
|
@ -251,8 +256,7 @@ class PopupPlayerGestureListener(
|
||||||
// -- Determine if the ClosingOverlayView (red X) has to be shown or hidden --
|
// -- Determine if the ClosingOverlayView (red X) has to be shown or hidden --
|
||||||
val showClosingOverlayView: Boolean = playerUi.isInsideClosingRadius(movingEvent)
|
val showClosingOverlayView: Boolean = playerUi.isInsideClosingRadius(movingEvent)
|
||||||
// Check if an view is in expected state and if not animate it into the correct state
|
// Check if an view is in expected state and if not animate it into the correct state
|
||||||
val expectedVisibility = if (showClosingOverlayView) View.VISIBLE else View.GONE
|
if (binding.closingOverlay.isVisible != showClosingOverlayView) {
|
||||||
if (binding.closingOverlay.visibility != expectedVisibility) {
|
|
||||||
binding.closingOverlay.animate(showClosingOverlayView, 200)
|
binding.closingOverlay.animate(showClosingOverlayView, 200)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,6 +3,7 @@ package org.schabi.newpipe.player.mediaitem;
|
||||||
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||||
import org.schabi.newpipe.extractor.stream.StreamType;
|
import org.schabi.newpipe.extractor.stream.StreamType;
|
||||||
import org.schabi.newpipe.player.playqueue.PlayQueueItem;
|
import org.schabi.newpipe.player.playqueue.PlayQueueItem;
|
||||||
|
import org.schabi.newpipe.util.image.ImageStrategy;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
@ -74,7 +75,7 @@ public final class ExceptionTag implements MediaItemTag {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getThumbnailUrl() {
|
public String getThumbnailUrl() {
|
||||||
return item.getThumbnailUrl();
|
return ImageStrategy.choosePreferredImage(item.getThumbnails());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -81,8 +81,9 @@ public interface MediaItemTag {
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
default MediaItem asMediaItem() {
|
default MediaItem asMediaItem() {
|
||||||
|
final String thumbnailUrl = getThumbnailUrl();
|
||||||
final MediaMetadata mediaMetadata = new MediaMetadata.Builder()
|
final MediaMetadata mediaMetadata = new MediaMetadata.Builder()
|
||||||
.setArtworkUri(Uri.parse(getThumbnailUrl()))
|
.setArtworkUri(thumbnailUrl == null ? null : Uri.parse(thumbnailUrl))
|
||||||
.setArtist(getUploaderName())
|
.setArtist(getUploaderName())
|
||||||
.setDescription(getTitle())
|
.setDescription(getTitle())
|
||||||
.setDisplayTitle(getTitle())
|
.setDisplayTitle(getTitle())
|
||||||
|
|
|
@ -6,6 +6,7 @@ import org.schabi.newpipe.extractor.stream.AudioStream;
|
||||||
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||||
import org.schabi.newpipe.extractor.stream.StreamType;
|
import org.schabi.newpipe.extractor.stream.StreamType;
|
||||||
import org.schabi.newpipe.extractor.stream.VideoStream;
|
import org.schabi.newpipe.extractor.stream.VideoStream;
|
||||||
|
import org.schabi.newpipe.util.image.ImageStrategy;
|
||||||
|
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
@ -95,7 +96,7 @@ public final class StreamInfoTag implements MediaItemTag {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getThumbnailUrl() {
|
public String getThumbnailUrl() {
|
||||||
return streamInfo.getThumbnailUrl();
|
return ImageStrategy.choosePreferredImage(streamInfo.getThumbnails());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -20,6 +20,7 @@ import com.google.android.exoplayer2.util.Util;
|
||||||
import org.schabi.newpipe.player.Player;
|
import org.schabi.newpipe.player.Player;
|
||||||
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
||||||
import org.schabi.newpipe.player.playqueue.PlayQueueItem;
|
import org.schabi.newpipe.player.playqueue.PlayQueueItem;
|
||||||
|
import org.schabi.newpipe.util.image.ImageStrategy;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
|
@ -137,9 +138,12 @@ public class PlayQueueNavigator implements MediaSessionConnector.QueueNavigator
|
||||||
.putLong(MediaMetadataCompat.METADATA_KEY_NUM_TRACKS, player.getPlayQueue().size());
|
.putLong(MediaMetadataCompat.METADATA_KEY_NUM_TRACKS, player.getPlayQueue().size());
|
||||||
descBuilder.setExtras(additionalMetadata);
|
descBuilder.setExtras(additionalMetadata);
|
||||||
|
|
||||||
final Uri thumbnailUri = Uri.parse(item.getThumbnailUrl());
|
try {
|
||||||
if (thumbnailUri != null) {
|
descBuilder.setIconUri(Uri.parse(
|
||||||
descBuilder.setIconUri(thumbnailUri);
|
ImageStrategy.choosePreferredImage(item.getThumbnails())));
|
||||||
|
} catch (final Throwable e) {
|
||||||
|
// no thumbnail available at all, or the user disabled image loading,
|
||||||
|
// or the obtained url is not a valid `Uri`
|
||||||
}
|
}
|
||||||
|
|
||||||
return descBuilder.build();
|
return descBuilder.build();
|
||||||
|
|
|
@ -17,7 +17,6 @@ import org.schabi.newpipe.player.helper.PlayerHelper;
|
||||||
import org.schabi.newpipe.player.ui.PlayerUi;
|
import org.schabi.newpipe.player.ui.PlayerUi;
|
||||||
|
|
||||||
public final class NotificationPlayerUi extends PlayerUi {
|
public final class NotificationPlayerUi extends PlayerUi {
|
||||||
private boolean foregroundNotificationAlreadyCreated = false;
|
|
||||||
private final NotificationUtil notificationUtil;
|
private final NotificationUtil notificationUtil;
|
||||||
|
|
||||||
public NotificationPlayerUi(@NonNull final Player player) {
|
public NotificationPlayerUi(@NonNull final Player player) {
|
||||||
|
@ -25,15 +24,6 @@ public final class NotificationPlayerUi extends PlayerUi {
|
||||||
notificationUtil = new NotificationUtil(player);
|
notificationUtil = new NotificationUtil(player);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public void initPlayer() {
|
|
||||||
super.initPlayer();
|
|
||||||
if (!foregroundNotificationAlreadyCreated) {
|
|
||||||
notificationUtil.createNotificationAndStartForeground();
|
|
||||||
foregroundNotificationAlreadyCreated = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void destroy() {
|
public void destroy() {
|
||||||
super.destroy();
|
super.destroy();
|
||||||
|
@ -122,4 +112,8 @@ public final class NotificationPlayerUi extends PlayerUi {
|
||||||
super.onPlayQueueEdited();
|
super.onPlayQueueEdited();
|
||||||
notificationUtil.createNotificationIfNeededAndUpdate(false);
|
notificationUtil.createNotificationIfNeededAndUpdate(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void createNotificationAndStartForeground() {
|
||||||
|
notificationUtil.createNotificationAndStartForeground();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -364,7 +364,7 @@ public final class NotificationUtil {
|
||||||
final Bitmap thumbnail = player.getThumbnail();
|
final Bitmap thumbnail = player.getThumbnail();
|
||||||
if (thumbnail == null || !showThumbnail) {
|
if (thumbnail == null || !showThumbnail) {
|
||||||
// since the builder is reused, make sure the thumbnail is unset if there is not one
|
// since the builder is reused, make sure the thumbnail is unset if there is not one
|
||||||
builder.setLargeIcon(null);
|
builder.setLargeIcon((Bitmap) null);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,7 @@ import android.util.Log;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
import org.schabi.newpipe.extractor.InfoItem;
|
||||||
import org.schabi.newpipe.extractor.ListExtractor;
|
import org.schabi.newpipe.extractor.ListExtractor;
|
||||||
import org.schabi.newpipe.extractor.ListInfo;
|
import org.schabi.newpipe.extractor.ListInfo;
|
||||||
import org.schabi.newpipe.extractor.Page;
|
import org.schabi.newpipe.extractor.Page;
|
||||||
|
@ -15,7 +16,7 @@ import java.util.stream.Collectors;
|
||||||
import io.reactivex.rxjava3.core.SingleObserver;
|
import io.reactivex.rxjava3.core.SingleObserver;
|
||||||
import io.reactivex.rxjava3.disposables.Disposable;
|
import io.reactivex.rxjava3.disposables.Disposable;
|
||||||
|
|
||||||
abstract class AbstractInfoPlayQueue<T extends ListInfo<StreamInfoItem>>
|
abstract class AbstractInfoPlayQueue<T extends ListInfo<? extends InfoItem>>
|
||||||
extends PlayQueue {
|
extends PlayQueue {
|
||||||
boolean isInitial;
|
boolean isInitial;
|
||||||
private boolean isComplete;
|
private boolean isComplete;
|
||||||
|
@ -27,7 +28,13 @@ abstract class AbstractInfoPlayQueue<T extends ListInfo<StreamInfoItem>>
|
||||||
private transient Disposable fetchReactor;
|
private transient Disposable fetchReactor;
|
||||||
|
|
||||||
protected AbstractInfoPlayQueue(final T info) {
|
protected AbstractInfoPlayQueue(final T info) {
|
||||||
this(info.getServiceId(), info.getUrl(), info.getNextPage(), info.getRelatedItems(), 0);
|
this(info.getServiceId(), info.getUrl(), info.getNextPage(),
|
||||||
|
info.getRelatedItems()
|
||||||
|
.stream()
|
||||||
|
.filter(StreamInfoItem.class::isInstance)
|
||||||
|
.map(StreamInfoItem.class::cast)
|
||||||
|
.collect(Collectors.toList()),
|
||||||
|
0);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected AbstractInfoPlayQueue(final int serviceId,
|
protected AbstractInfoPlayQueue(final int serviceId,
|
||||||
|
@ -72,7 +79,11 @@ abstract class AbstractInfoPlayQueue<T extends ListInfo<StreamInfoItem>>
|
||||||
}
|
}
|
||||||
nextPage = result.getNextPage();
|
nextPage = result.getNextPage();
|
||||||
|
|
||||||
append(extractListItems(result.getRelatedItems()));
|
append(extractListItems(result.getRelatedItems()
|
||||||
|
.stream()
|
||||||
|
.filter(StreamInfoItem.class::isInstance)
|
||||||
|
.map(StreamInfoItem.class::cast)
|
||||||
|
.collect(Collectors.toList())));
|
||||||
|
|
||||||
fetchReactor.dispose();
|
fetchReactor.dispose();
|
||||||
fetchReactor = null;
|
fetchReactor = null;
|
||||||
|
@ -87,7 +98,7 @@ abstract class AbstractInfoPlayQueue<T extends ListInfo<StreamInfoItem>>
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
SingleObserver<ListExtractor.InfoItemsPage<StreamInfoItem>> getNextPageObserver() {
|
SingleObserver<ListExtractor.InfoItemsPage<? extends InfoItem>> getNextPageObserver() {
|
||||||
return new SingleObserver<>() {
|
return new SingleObserver<>() {
|
||||||
@Override
|
@Override
|
||||||
public void onSubscribe(@NonNull final Disposable d) {
|
public void onSubscribe(@NonNull final Disposable d) {
|
||||||
|
@ -101,13 +112,17 @@ abstract class AbstractInfoPlayQueue<T extends ListInfo<StreamInfoItem>>
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onSuccess(
|
public void onSuccess(
|
||||||
@NonNull final ListExtractor.InfoItemsPage<StreamInfoItem> result) {
|
@NonNull final ListExtractor.InfoItemsPage<? extends InfoItem> result) {
|
||||||
if (!result.hasNextPage()) {
|
if (!result.hasNextPage()) {
|
||||||
isComplete = true;
|
isComplete = true;
|
||||||
}
|
}
|
||||||
nextPage = result.getNextPage();
|
nextPage = result.getNextPage();
|
||||||
|
|
||||||
append(extractListItems(result.getItems()));
|
append(extractListItems(result.getItems()
|
||||||
|
.stream()
|
||||||
|
.filter(StreamInfoItem.class::isInstance)
|
||||||
|
.map(StreamInfoItem.class::cast)
|
||||||
|
.collect(Collectors.toList())));
|
||||||
|
|
||||||
fetchReactor.dispose();
|
fetchReactor.dispose();
|
||||||
fetchReactor = null;
|
fetchReactor = null;
|
||||||
|
|
|
@ -1,47 +0,0 @@
|
||||||
package org.schabi.newpipe.player.playqueue;
|
|
||||||
|
|
||||||
|
|
||||||
import org.schabi.newpipe.extractor.Page;
|
|
||||||
import org.schabi.newpipe.extractor.channel.ChannelInfo;
|
|
||||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
|
||||||
import org.schabi.newpipe.util.ExtractorHelper;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
|
||||||
import io.reactivex.rxjava3.schedulers.Schedulers;
|
|
||||||
|
|
||||||
public final class ChannelPlayQueue extends AbstractInfoPlayQueue<ChannelInfo> {
|
|
||||||
|
|
||||||
public ChannelPlayQueue(final ChannelInfo info) {
|
|
||||||
super(info);
|
|
||||||
}
|
|
||||||
|
|
||||||
public ChannelPlayQueue(final int serviceId,
|
|
||||||
final String url,
|
|
||||||
final Page nextPage,
|
|
||||||
final List<StreamInfoItem> streams,
|
|
||||||
final int index) {
|
|
||||||
super(serviceId, url, nextPage, streams, index);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected String getTag() {
|
|
||||||
return "ChannelPlayQueue@" + Integer.toHexString(hashCode());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void fetch() {
|
|
||||||
if (this.isInitial) {
|
|
||||||
ExtractorHelper.getChannelInfo(this.serviceId, this.baseUrl, false)
|
|
||||||
.subscribeOn(Schedulers.io())
|
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
|
||||||
.subscribe(getHeadListObserver());
|
|
||||||
} else {
|
|
||||||
ExtractorHelper.getMoreChannelItems(this.serviceId, this.baseUrl, this.nextPage)
|
|
||||||
.subscribeOn(Schedulers.io())
|
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
|
||||||
.subscribe(getNextPageObserver());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,53 @@
|
||||||
|
package org.schabi.newpipe.player.playqueue;
|
||||||
|
|
||||||
|
|
||||||
|
import org.schabi.newpipe.extractor.Page;
|
||||||
|
import org.schabi.newpipe.extractor.channel.tabs.ChannelTabInfo;
|
||||||
|
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler;
|
||||||
|
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||||
|
import org.schabi.newpipe.util.ExtractorHelper;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||||
|
import io.reactivex.rxjava3.schedulers.Schedulers;
|
||||||
|
|
||||||
|
public final class ChannelTabPlayQueue extends AbstractInfoPlayQueue<ChannelTabInfo> {
|
||||||
|
|
||||||
|
final ListLinkHandler linkHandler;
|
||||||
|
|
||||||
|
public ChannelTabPlayQueue(final int serviceId,
|
||||||
|
final ListLinkHandler linkHandler,
|
||||||
|
final Page nextPage,
|
||||||
|
final List<StreamInfoItem> streams,
|
||||||
|
final int index) {
|
||||||
|
super(serviceId, linkHandler.getUrl(), nextPage, streams, index);
|
||||||
|
this.linkHandler = linkHandler;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ChannelTabPlayQueue(final int serviceId,
|
||||||
|
final ListLinkHandler linkHandler) {
|
||||||
|
this(serviceId, linkHandler, null, Collections.emptyList(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected String getTag() {
|
||||||
|
return "ChannelTabPlayQueue@" + Integer.toHexString(hashCode());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void fetch() {
|
||||||
|
if (isInitial) {
|
||||||
|
ExtractorHelper.getChannelTab(this.serviceId, this.linkHandler, false)
|
||||||
|
.subscribeOn(Schedulers.io())
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribe(getHeadListObserver());
|
||||||
|
} else {
|
||||||
|
ExtractorHelper.getMoreChannelTabItems(this.serviceId, this.linkHandler, this.nextPage)
|
||||||
|
.subscribeOn(Schedulers.io())
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribe(getNextPageObserver());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -539,7 +539,8 @@ public abstract class PlayQueue implements Serializable {
|
||||||
|
|
||||||
public boolean equalStreamsAndIndex(@Nullable final PlayQueue other) {
|
public boolean equalStreamsAndIndex(@Nullable final PlayQueue other) {
|
||||||
if (equalStreams(other)) {
|
if (equalStreams(other)) {
|
||||||
return other.getIndex() == getIndex();
|
//noinspection ConstantConditions
|
||||||
|
return other.getIndex() == getIndex(); //NOSONAR: other is not null
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,7 @@ package org.schabi.newpipe.player.playqueue;
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
|
import org.schabi.newpipe.extractor.Image;
|
||||||
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||||
import org.schabi.newpipe.extractor.stream.StreamType;
|
import org.schabi.newpipe.extractor.stream.StreamType;
|
||||||
|
@ -10,6 +11,7 @@ import org.schabi.newpipe.util.ExtractorHelper;
|
||||||
import org.schabi.newpipe.util.VideoSegment;
|
import org.schabi.newpipe.util.VideoSegment;
|
||||||
|
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
import io.reactivex.rxjava3.core.Single;
|
import io.reactivex.rxjava3.core.Single;
|
||||||
import io.reactivex.rxjava3.schedulers.Schedulers;
|
import io.reactivex.rxjava3.schedulers.Schedulers;
|
||||||
|
@ -25,7 +27,7 @@ public class PlayQueueItem implements Serializable {
|
||||||
private final int serviceId;
|
private final int serviceId;
|
||||||
private final long duration;
|
private final long duration;
|
||||||
@NonNull
|
@NonNull
|
||||||
private final String thumbnailUrl;
|
private final List<Image> thumbnails;
|
||||||
@NonNull
|
@NonNull
|
||||||
private final String uploader;
|
private final String uploader;
|
||||||
private final String uploaderUrl;
|
private final String uploaderUrl;
|
||||||
|
@ -41,7 +43,7 @@ public class PlayQueueItem implements Serializable {
|
||||||
|
|
||||||
PlayQueueItem(@NonNull final StreamInfo info) {
|
PlayQueueItem(@NonNull final StreamInfo info) {
|
||||||
this(info.getName(), info.getUrl(), info.getServiceId(), info.getDuration(),
|
this(info.getName(), info.getUrl(), info.getServiceId(), info.getDuration(),
|
||||||
info.getThumbnailUrl(), info.getUploaderName(),
|
info.getThumbnails(), info.getUploaderName(),
|
||||||
info.getUploaderUrl(), info.getStreamType());
|
info.getUploaderUrl(), info.getStreamType());
|
||||||
|
|
||||||
if (info.getStartPosition() > 0) {
|
if (info.getStartPosition() > 0) {
|
||||||
|
@ -51,20 +53,20 @@ public class PlayQueueItem implements Serializable {
|
||||||
|
|
||||||
PlayQueueItem(@NonNull final StreamInfoItem item) {
|
PlayQueueItem(@NonNull final StreamInfoItem item) {
|
||||||
this(item.getName(), item.getUrl(), item.getServiceId(), item.getDuration(),
|
this(item.getName(), item.getUrl(), item.getServiceId(), item.getDuration(),
|
||||||
item.getThumbnailUrl(), item.getUploaderName(),
|
item.getThumbnails(), item.getUploaderName(),
|
||||||
item.getUploaderUrl(), item.getStreamType());
|
item.getUploaderUrl(), item.getStreamType());
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings("ParameterNumber")
|
@SuppressWarnings("ParameterNumber")
|
||||||
private PlayQueueItem(@Nullable final String name, @Nullable final String url,
|
private PlayQueueItem(@Nullable final String name, @Nullable final String url,
|
||||||
final int serviceId, final long duration,
|
final int serviceId, final long duration,
|
||||||
@Nullable final String thumbnailUrl, @Nullable final String uploader,
|
final List<Image> thumbnails, @Nullable final String uploader,
|
||||||
final String uploaderUrl, @NonNull final StreamType streamType) {
|
final String uploaderUrl, @NonNull final StreamType streamType) {
|
||||||
this.title = name != null ? name : EMPTY_STRING;
|
this.title = name != null ? name : EMPTY_STRING;
|
||||||
this.url = url != null ? url : EMPTY_STRING;
|
this.url = url != null ? url : EMPTY_STRING;
|
||||||
this.serviceId = serviceId;
|
this.serviceId = serviceId;
|
||||||
this.duration = duration;
|
this.duration = duration;
|
||||||
this.thumbnailUrl = thumbnailUrl != null ? thumbnailUrl : EMPTY_STRING;
|
this.thumbnails = thumbnails;
|
||||||
this.uploader = uploader != null ? uploader : EMPTY_STRING;
|
this.uploader = uploader != null ? uploader : EMPTY_STRING;
|
||||||
this.uploaderUrl = uploaderUrl;
|
this.uploaderUrl = uploaderUrl;
|
||||||
this.streamType = streamType;
|
this.streamType = streamType;
|
||||||
|
@ -91,8 +93,8 @@ public class PlayQueueItem implements Serializable {
|
||||||
}
|
}
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
public String getThumbnailUrl() {
|
public List<Image> getThumbnails() {
|
||||||
return thumbnailUrl;
|
return thumbnails;
|
||||||
}
|
}
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
|
|
|
@ -6,7 +6,7 @@ import android.view.MotionEvent;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
|
|
||||||
import org.schabi.newpipe.util.Localization;
|
import org.schabi.newpipe.util.Localization;
|
||||||
import org.schabi.newpipe.util.PicassoHelper;
|
import org.schabi.newpipe.util.image.PicassoHelper;
|
||||||
import org.schabi.newpipe.util.ServiceHelper;
|
import org.schabi.newpipe.util.ServiceHelper;
|
||||||
|
|
||||||
public class PlayQueueItemBuilder {
|
public class PlayQueueItemBuilder {
|
||||||
|
@ -33,7 +33,7 @@ public class PlayQueueItemBuilder {
|
||||||
holder.itemDurationView.setVisibility(View.GONE);
|
holder.itemDurationView.setVisibility(View.GONE);
|
||||||
}
|
}
|
||||||
|
|
||||||
PicassoHelper.loadThumbnail(item.getThumbnailUrl()).into(holder.itemThumbnailView);
|
PicassoHelper.loadThumbnail(item.getThumbnails()).into(holder.itemThumbnailView);
|
||||||
|
|
||||||
holder.itemRoot.setOnClickListener(view -> {
|
holder.itemRoot.setOnClickListener(view -> {
|
||||||
if (onItemClickListener != null) {
|
if (onItemClickListener != null) {
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue