mirror of
https://github.com/TeamNewPipe/NewPipe.git
synced 2024-11-25 04:22:30 +01:00
Merge pull request #10470 from TeamNewPipe/release-0.26.0
Release 0.26.0
This commit is contained in:
commit
67629938d6
2
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
2
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@ -14,7 +14,7 @@ body:
|
|||||||
attributes:
|
attributes:
|
||||||
label: "Checklist"
|
label: "Checklist"
|
||||||
options:
|
options:
|
||||||
- label: "I am able to reproduce the bug with the [latest version](https://github.com/TeamNewPipe/NewPipe/releases/latest)."
|
- label: "I am able to reproduce the bug with the latest version given here: [CLICK THIS LINK](https://github.com/TeamNewPipe/NewPipe/releases/latest)."
|
||||||
required: true
|
required: true
|
||||||
- label: "I made sure that there are *no existing issues* - [open](https://github.com/TeamNewPipe/NewPipe/issues) or [closed](https://github.com/TeamNewPipe/NewPipe/issues?q=is%3Aissue+is%3Aclosed) - which I could contribute my information to."
|
- label: "I made sure that there are *no existing issues* - [open](https://github.com/TeamNewPipe/NewPipe/issues) or [closed](https://github.com/TeamNewPipe/NewPipe/issues?q=is%3Aissue+is%3Aclosed) - which I could contribute my information to."
|
||||||
required: true
|
required: true
|
||||||
|
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
|
14
README.md
14
README.md
@ -13,14 +13,14 @@
|
|||||||
<a href="https://github.com/TeamNewPipe/NewPipe/actions" alt="Build Status"><img src="https://github.com/TeamNewPipe/NewPipe/workflows/CI/badge.svg?branch=dev&event=push"></a>
|
<a href="https://github.com/TeamNewPipe/NewPipe/actions" alt="Build Status"><img src="https://github.com/TeamNewPipe/NewPipe/workflows/CI/badge.svg?branch=dev&event=push"></a>
|
||||||
<a href="https://hosted.weblate.org/engage/newpipe/" alt="Translation Status"><img src="https://hosted.weblate.org/widgets/newpipe/-/svg-badge.svg"></a>
|
<a href="https://hosted.weblate.org/engage/newpipe/" alt="Translation Status"><img src="https://hosted.weblate.org/widgets/newpipe/-/svg-badge.svg"></a>
|
||||||
<a href="https://web.libera.chat/#newpipe" alt="IRC channel: #newpipe"><img src="https://img.shields.io/badge/IRC%20chat-%23newpipe-brightgreen.svg"></a>
|
<a href="https://web.libera.chat/#newpipe" alt="IRC channel: #newpipe"><img src="https://img.shields.io/badge/IRC%20chat-%23newpipe-brightgreen.svg"></a>
|
||||||
<a href="https://www.bountysource.com/teams/newpipe" alt="Bountysource bounties"><img src="https://img.shields.io/bountysource/team/newpipe/activity.svg?colorB=cd201f"></a>
|
<a href="https://matrix.to/#/#newpipe:libera.chat" alt="Matrix channel: #newpipe"><img src="https://img.shields.io/badge/Matrix%20chat-%23newpipe-blue"></a>
|
||||||
</p>
|
</p>
|
||||||
<hr>
|
<hr>
|
||||||
<p align="center"><a href="#screenshots">Screenshots</a> • <a href="#supported-services">Supported Services</a> • <a href="#description">Description</a> • <a href="#features">Features</a> • <a href="#installation-and-updates">Installation and updates</a> • <a href="#contribution">Contribution</a> • <a href="#donate">Donate</a> • <a href="#license">License</a></p>
|
<p align="center"><a href="#screenshots">Screenshots</a> • <a href="#supported-services">Supported Services</a> • <a href="#description">Description</a> • <a href="#features">Features</a> • <a href="#installation-and-updates">Installation and updates</a> • <a href="#contribution">Contribution</a> • <a href="#donate">Donate</a> • <a href="#license">License</a></p>
|
||||||
<p align="center"><a href="https://newpipe.net">Website</a> • <a href="https://newpipe.net/blog/">Blog</a> • <a href="https://newpipe.net/FAQ/">FAQ</a> • <a href="https://newpipe.net/press/">Press</a></p>
|
<p align="center"><a href="https://newpipe.net">Website</a> • <a href="https://newpipe.net/blog/">Blog</a> • <a href="https://newpipe.net/FAQ/">FAQ</a> • <a href="https://newpipe.net/press/">Press</a></p>
|
||||||
<hr>
|
<hr>
|
||||||
|
|
||||||
*Read this document in other languages: [Deutsch](doc/README.de.md), [English](README.md), [Español](doc/README.es.md), [Français](doc/README.fr.md), [हिन्दी](doc/README.hi.md), [Italiano](doc/README.it.md), [한국어](doc/README.ko.md), [Português Brasil](doc/README.pt_BR.md), [Polski](doc/README.pl.md), [ਪੰਜਾਬੀ ](doc/README.pa.md), [日本語](doc/README.ja.md), [Română](doc/README.ro.md), [Soomaali](doc/README.so.md), [Türkçe](doc/README.tr.md), [正體中文](doc/README.zh_TW.md), [অসমীয়া](doc/README.asm.md)*
|
*Read this document in other languages: [Deutsch](doc/README.de.md), [English](README.md), [Español](doc/README.es.md), [Français](doc/README.fr.md), [हिन्दी](doc/README.hi.md), [Italiano](doc/README.it.md), [한국어](doc/README.ko.md), [Português Brasil](doc/README.pt_BR.md), [Polski](doc/README.pl.md), [ਪੰਜਾਬੀ ](doc/README.pa.md), [日本語](doc/README.ja.md), [Română](doc/README.ro.md), [Soomaali](doc/README.so.md), [Türkçe](doc/README.tr.md), [正體中文](doc/README.zh_TW.md), [অসমীয়া](doc/README.asm.md), [Српски](doc/README.sr.md)*
|
||||||
|
|
||||||
<b>WARNING: THIS APP IS IN BETA, SO YOU MAY ENCOUNTER BUGS. IF YOU DO, OPEN AN ISSUE IN OUR GITHUB REPOSITORY BY FILLING OUT THE ISSUE TEMPLATE.</b>
|
<b>WARNING: THIS APP IS IN BETA, SO YOU MAY ENCOUNTER BUGS. IF YOU DO, OPEN AN ISSUE IN OUR GITHUB REPOSITORY BY FILLING OUT THE ISSUE TEMPLATE.</b>
|
||||||
|
|
||||||
@ -126,16 +126,6 @@ If you like NewPipe, you're welcome to send a donation. We prefer Liberapay, as
|
|||||||
<td><a href="https://liberapay.com/TeamNewPipe/"><img src="assets/liberapay_qr_code.png" alt="Visit NewPipe at liberapay.com" width="100px"></a></td>
|
<td><a href="https://liberapay.com/TeamNewPipe/"><img src="assets/liberapay_qr_code.png" alt="Visit NewPipe at liberapay.com" width="100px"></a></td>
|
||||||
<td><a href="https://liberapay.com/TeamNewPipe/donate"><img src="assets/liberapay_donate_button.svg" alt="Donate via Liberapay" height="35px"></a></td>
|
<td><a href="https://liberapay.com/TeamNewPipe/donate"><img src="assets/liberapay_donate_button.svg" alt="Donate via Liberapay" height="35px"></a></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
|
||||||
<td><img src="https://bitcoin.org/img/icons/logotop.svg" alt="Bitcoin"></td>
|
|
||||||
<td><img src="assets/bitcoin_qr_code.png" alt="Bitcoin QR code" width="100px"></td>
|
|
||||||
<td><samp>16A9J59ahMRqkLSZjhYj33n9j3fMztFxnh</samp></td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td><a href="https://www.bountysource.com/teams/newpipe"><img src="https://upload.wikimedia.org/wikipedia/commons/thumb/2/22/Bountysource.png/320px-Bountysource.png" alt="Bountysource" width="190px"></a></td>
|
|
||||||
<td><a href="https://www.bountysource.com/teams/newpipe"><img src="assets/bountysource_qr_code.png" alt="Visit NewPipe at bountysource.com" width="100px"></a></td>
|
|
||||||
<td><a href="https://www.bountysource.com/teams/newpipe/issues"><img src="https://img.shields.io/bountysource/team/newpipe/activity.svg?colorB=cd201f" height="30px" alt="Check out how many bounties you can earn."></a></td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
## Privacy Policy
|
## Privacy Policy
|
||||||
|
@ -20,8 +20,8 @@ android {
|
|||||||
resValue "string", "app_name", "NewPipe"
|
resValue "string", "app_name", "NewPipe"
|
||||||
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')
|
||||||
@ -115,7 +112,7 @@ ext {
|
|||||||
|
|
||||||
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'
|
||||||
|
|
||||||
@ -192,7 +189,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.3'
|
||||||
|
|
||||||
/** 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 +197,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,7 +205,7 @@ 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.5.1'
|
||||||
@ -232,7 +229,7 @@ dependencies {
|
|||||||
implementation 'androidx.viewpager2:viewpager2:1.1.0-beta01'
|
implementation 'androidx.viewpager2:viewpager2:1.1.0-beta01'
|
||||||
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.9.0'
|
||||||
|
|
||||||
/** Third-party libraries **/
|
/** Third-party libraries **/
|
||||||
// Instance state boilerplate elimination
|
// Instance state boilerplate elimination
|
||||||
|
@ -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))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -219,14 +219,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 +306,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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -120,13 +120,13 @@ class NewVersionWorker(
|
|||||||
|
|
||||||
// Parse the json from the response.
|
// Parse the json from the response.
|
||||||
try {
|
try {
|
||||||
val githubStableObject = JsonParser.`object`()
|
val newpipeVersionInfo = JsonParser.`object`()
|
||||||
.from(response.responseBody()).getObject("flavors")
|
.from(response.responseBody()).getObject("flavors")
|
||||||
.getObject("github").getObject("stable")
|
.getObject("newpipe")
|
||||||
|
|
||||||
val versionName = githubStableObject.getString("version")
|
val versionName = newpipeVersionInfo.getString("version")
|
||||||
val versionCode = githubStableObject.getInt("version_code")
|
val versionCode = newpipeVersionInfo.getInt("version_code")
|
||||||
val apkLocationUrl = githubStableObject.getString("apk")
|
val apkLocationUrl = newpipeVersionInfo.getString("apk")
|
||||||
compareAppVersionAndShowNotification(versionName, apkLocationUrl, versionCode)
|
compareAppVersionAndShowNotification(versionName, apkLocationUrl, versionCode)
|
||||||
} catch (e: JsonParserException) {
|
} catch (e: JsonParserException) {
|
||||||
// Most likely something is wrong in data received from NEWPIPE_API_URL.
|
// Most likely something is wrong in data received from NEWPIPE_API_URL.
|
||||||
|
@ -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 {
|
||||||
|
@ -1,28 +1,38 @@
|
|||||||
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.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: Array<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?.getParcelableArray(ARG_COMPONENTS) as Array<SoftwareComponent>
|
||||||
activeLicense = savedInstanceState?.getSerializable(LICENSE_KEY) as? License
|
activeSoftwareComponent = savedInstanceState?.getSerializable(SOFTWARE_COMPONENT_KEY) as? SoftwareComponent
|
||||||
// Sort components by name
|
// Sort components by name
|
||||||
softwareComponents.sortBy { it.name }
|
softwareComponents.sortBy { it.name }
|
||||||
}
|
}
|
||||||
@ -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,26 +66,70 @@ 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"
|
||||||
|
private val NEWPIPE_SOFTWARE_COMPONENT = SoftwareComponent(
|
||||||
|
"NewPipe",
|
||||||
|
"2014-2023",
|
||||||
|
"Team NewPipe",
|
||||||
|
"https://newpipe.net/",
|
||||||
|
StandardLicenses.GPL3,
|
||||||
|
BuildConfig.VERSION_NAME
|
||||||
|
)
|
||||||
fun newInstance(softwareComponents: Array<SoftwareComponent>): LicenseFragment {
|
fun newInstance(softwareComponents: Array<SoftwareComponent>): LicenseFragment {
|
||||||
val fragment = LicenseFragment()
|
val fragment = LicenseFragment()
|
||||||
fragment.arguments = bundleOf(ARG_COMPONENTS to softwareComponents)
|
fragment.arguments = bundleOf(ARG_COMPONENTS to softwareComponents)
|
||||||
|
@ -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
|
||||||
|
@ -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 (
|
||||||
feed.stream_id IN (
|
SELECT s.uid,
|
||||||
SELECT s.uid FROM streams s
|
(SELECT MAX(upload_date)
|
||||||
|
FROM streams s1
|
||||||
INNER JOIN feed f
|
INNER JOIN feed f1
|
||||||
ON s.uid = f.stream_id
|
ON s1.uid = f1.stream_id
|
||||||
|
WHERE f1.subscription_id = f.subscription_id) max_upload_date
|
||||||
WHERE s.upload_date < :offsetDateTime
|
FROM streams s
|
||||||
)
|
INNER JOIN feed f
|
||||||
|
ON s.uid = f.stream_id
|
||||||
|
|
||||||
|
WHERE s.upload_date < :offsetDateTime
|
||||||
|
AND s.upload_date <> max_upload_date))
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
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;
|
||||||
|
@ -67,7 +67,7 @@ import org.schabi.newpipe.util.PermissionHelper;
|
|||||||
import org.schabi.newpipe.util.SecondaryStreamHelper;
|
import org.schabi.newpipe.util.SecondaryStreamHelper;
|
||||||
import org.schabi.newpipe.util.SimpleOnSeekBarChangeListener;
|
import org.schabi.newpipe.util.SimpleOnSeekBarChangeListener;
|
||||||
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;
|
||||||
@ -97,9 +97,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
|
||||||
@ -187,8 +187,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);
|
||||||
@ -258,17 +258,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));
|
||||||
@ -396,7 +396,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) {
|
||||||
@ -406,7 +406,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) {
|
||||||
@ -416,7 +416,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) {
|
||||||
@ -724,9 +724,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);
|
||||||
}
|
}
|
||||||
@ -766,7 +766,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)
|
||||||
@ -799,7 +799,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:
|
||||||
@ -808,7 +808,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:
|
||||||
@ -820,9 +820,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:
|
||||||
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
@ -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) {
|
||||||
|
@ -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,6 +71,7 @@ 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.exceptions.ContentNotSupportedException;
|
import org.schabi.newpipe.extractor.exceptions.ContentNotSupportedException;
|
||||||
@ -83,11 +84,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;
|
||||||
@ -107,11 +110,12 @@ 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.ThemeHelper;
|
import org.schabi.newpipe.util.ThemeHelper;
|
||||||
import org.schabi.newpipe.util.external_communication.KoreUtils;
|
import org.schabi.newpipe.util.external_communication.KoreUtils;
|
||||||
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.Iterator;
|
import java.util.Iterator;
|
||||||
@ -470,10 +474,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)) {
|
||||||
@ -482,7 +499,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 ->
|
||||||
@ -535,9 +552,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)));
|
||||||
|
|
||||||
@ -620,8 +639,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));
|
||||||
@ -721,7 +739,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());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1463,11 +1481,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,
|
||||||
@ -1534,13 +1547,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()) {
|
||||||
@ -1585,7 +1598,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);
|
||||||
@ -1617,10 +1630,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);
|
||||||
}
|
}
|
||||||
@ -1795,7 +1808,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;
|
||||||
}
|
}
|
||||||
@ -1824,7 +1837,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();
|
||||||
}
|
}
|
||||||
@ -2189,7 +2202,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());
|
||||||
}
|
}
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
@ -2371,11 +2384,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;
|
||||||
@ -231,11 +233,7 @@ public abstract class BaseListInfoFragment<I extends InfoItem, L extends ListInf
|
|||||||
showListFooter(hasMoreItems());
|
showListFooter(hasMoreItems());
|
||||||
} 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 +250,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);
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
||||||
|
@ -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) {
|
||||||
|
@ -31,7 +31,8 @@ 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;
|
||||||
@ -97,8 +98,8 @@ public class CommentsMiniInfoItemHolder extends InfoItemHolder {
|
|||||||
}
|
}
|
||||||
final CommentsInfoItem item = (CommentsInfoItem) infoItem;
|
final CommentsInfoItem item = (CommentsInfoItem) infoItem;
|
||||||
|
|
||||||
PicassoHelper.loadAvatar(item.getUploaderAvatarUrl()).into(itemThumbnailView);
|
PicassoHelper.loadAvatar(item.getUploaderAvatars()).into(itemThumbnailView);
|
||||||
if (PicassoHelper.getShouldLoadImages()) {
|
if (ImageStrategy.shouldLoadImages()) {
|
||||||
itemThumbnailView.setVisibility(View.VISIBLE);
|
itemThumbnailView.setVisibility(View.VISIBLE);
|
||||||
itemRoot.setPadding(commentVerticalPadding, commentVerticalPadding,
|
itemRoot.setPadding(commentVerticalPadding, commentVerticalPadding,
|
||||||
commentVerticalPadding, commentVerticalPadding);
|
commentVerticalPadding, commentVerticalPadding);
|
||||||
|
@ -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) {
|
||||||
|
@ -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) {
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
||||||
|
@ -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,
|
||||||
|
@ -26,6 +26,7 @@ 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;
|
||||||
@ -38,6 +39,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 +50,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;
|
||||||
|
|
||||||
@ -199,12 +202,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 +233,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 +264,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 +286,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());
|
||||||
}
|
}
|
||||||
|
@ -16,6 +16,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;
|
||||||
@ -531,18 +532,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;
|
||||||
@ -585,11 +587,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));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -619,11 +619,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()) {
|
||||||
|
@ -87,6 +87,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;
|
||||||
@ -117,7 +118,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.StreamTypeUtil;
|
import org.schabi.newpipe.util.StreamTypeUtil;
|
||||||
|
|
||||||
@ -805,10 +806,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
|
||||||
@ -817,12 +818,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);
|
||||||
}
|
}
|
||||||
@ -1082,7 +1083,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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1223,6 +1224,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();
|
||||||
}
|
}
|
||||||
@ -1620,7 +1626,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) {
|
||||||
@ -1785,7 +1793,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
|
||||||
|
@ -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
|
||||||
@ -235,14 +235,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 +253,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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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,12 +3,14 @@ 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;
|
||||||
import org.schabi.newpipe.util.ExtractorHelper;
|
import org.schabi.newpipe.util.ExtractorHelper;
|
||||||
|
|
||||||
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;
|
||||||
@ -24,7 +26,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;
|
||||||
@ -38,7 +40,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) {
|
||||||
@ -48,20 +50,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;
|
||||||
@ -88,8 +90,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) {
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
package org.schabi.newpipe.player.playqueue;
|
package org.schabi.newpipe.player.playqueue;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
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;
|
||||||
|
|
||||||
@ -20,11 +22,11 @@ public final class SinglePlayQueue extends PlayQueue {
|
|||||||
getItem().setRecoveryPosition(startPosition);
|
getItem().setRecoveryPosition(startPosition);
|
||||||
}
|
}
|
||||||
|
|
||||||
public SinglePlayQueue(final List<StreamInfoItem> items, final int index) {
|
public SinglePlayQueue(@NonNull final List<StreamInfoItem> items, final int index) {
|
||||||
super(index, playQueueItemsOf(items));
|
super(index, playQueueItemsOf(items));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static List<PlayQueueItem> playQueueItemsOf(final List<StreamInfoItem> items) {
|
private static List<PlayQueueItem> playQueueItemsOf(@NonNull final List<StreamInfoItem> items) {
|
||||||
final List<PlayQueueItem> playQueueItems = new ArrayList<>(items.size());
|
final List<PlayQueueItem> playQueueItems = new ArrayList<>(items.size());
|
||||||
for (final StreamInfoItem item : items) {
|
for (final StreamInfoItem item : items) {
|
||||||
playQueueItems.add(new PlayQueueItem(item));
|
playQueueItems.add(new PlayQueueItem(item));
|
||||||
@ -39,5 +41,7 @@ public final class SinglePlayQueue extends PlayQueue {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void fetch() {
|
public void fetch() {
|
||||||
|
// Item was already passed in constructor.
|
||||||
|
// No further items need to be fetched as this is a PlayQueue with only one item
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -14,7 +14,7 @@ import androidx.collection.SparseArrayCompat;
|
|||||||
import com.google.common.base.Stopwatch;
|
import com.google.common.base.Stopwatch;
|
||||||
|
|
||||||
import org.schabi.newpipe.extractor.stream.Frameset;
|
import org.schabi.newpipe.extractor.stream.Frameset;
|
||||||
import org.schabi.newpipe.util.PicassoHelper;
|
import org.schabi.newpipe.util.image.PicassoHelper;
|
||||||
|
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
@ -434,7 +434,7 @@ public final class MainPlayerUi extends VideoPlayerUi implements View.OnLayoutCh
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
final boolean showQueue = playQueue.getStreams().size() > 1;
|
final boolean showQueue = !playQueue.getStreams().isEmpty();
|
||||||
final boolean showSegment = !player.getCurrentStreamInfo()
|
final boolean showSegment = !player.getCurrentStreamInfo()
|
||||||
.map(StreamInfo::getStreamSegments)
|
.map(StreamInfo::getStreamSegments)
|
||||||
.map(List::isEmpty)
|
.map(List::isEmpty)
|
||||||
@ -740,7 +740,7 @@ public final class MainPlayerUi extends VideoPlayerUi implements View.OnLayoutCh
|
|||||||
String videoUrl = player.getVideoUrl();
|
String videoUrl = player.getVideoUrl();
|
||||||
videoUrl += ("&t=" + seconds);
|
videoUrl += ("&t=" + seconds);
|
||||||
ShareUtils.shareText(context, currentItem.getTitle(),
|
ShareUtils.shareText(context, currentItem.getTitle(),
|
||||||
videoUrl, currentItem.getThumbnailUrl());
|
videoUrl, currentItem.getThumbnails());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -41,6 +41,7 @@ import androidx.annotation.NonNull;
|
|||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.appcompat.content.res.AppCompatResources;
|
import androidx.appcompat.content.res.AppCompatResources;
|
||||||
import androidx.appcompat.view.ContextThemeWrapper;
|
import androidx.appcompat.view.ContextThemeWrapper;
|
||||||
|
import androidx.appcompat.widget.AppCompatImageButton;
|
||||||
import androidx.appcompat.widget.PopupMenu;
|
import androidx.appcompat.widget.PopupMenu;
|
||||||
import androidx.core.graphics.BitmapCompat;
|
import androidx.core.graphics.BitmapCompat;
|
||||||
import androidx.core.graphics.Insets;
|
import androidx.core.graphics.Insets;
|
||||||
@ -103,6 +104,9 @@ public abstract class VideoPlayerUi extends PlayerUi implements SeekBar.OnSeekBa
|
|||||||
// other constants (TODO remove playback speeds and use normal menu for popup, too)
|
// other constants (TODO remove playback speeds and use normal menu for popup, too)
|
||||||
private static final float[] PLAYBACK_SPEEDS = {0.5f, 0.75f, 1.0f, 1.25f, 1.5f, 1.75f, 2.0f};
|
private static final float[] PLAYBACK_SPEEDS = {0.5f, 0.75f, 1.0f, 1.25f, 1.5f, 1.75f, 2.0f};
|
||||||
|
|
||||||
|
private enum PlayButtonAction {
|
||||||
|
PLAY, PAUSE, REPLAY
|
||||||
|
}
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
// Views
|
// Views
|
||||||
@ -222,7 +226,7 @@ public abstract class VideoPlayerUi extends PlayerUi implements SeekBar.OnSeekBa
|
|||||||
final PlayQueueItem currentItem = player.getCurrentItem();
|
final PlayQueueItem currentItem = player.getCurrentItem();
|
||||||
if (currentItem != null) {
|
if (currentItem != null) {
|
||||||
ShareUtils.shareText(context, currentItem.getTitle(),
|
ShareUtils.shareText(context, currentItem.getTitle(),
|
||||||
player.getVideoUrlAtCurrentTime(), currentItem.getThumbnailUrl());
|
player.getVideoUrlAtCurrentTime(), currentItem.getThumbnails());
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
binding.share.setOnLongClickListener(v -> {
|
binding.share.setOnLongClickListener(v -> {
|
||||||
@ -755,6 +759,29 @@ public abstract class VideoPlayerUi extends PlayerUi implements SeekBar.OnSeekBa
|
|||||||
// only MainPlayerUi can be in fullscreen, so overridden there
|
// only MainPlayerUi can be in fullscreen, so overridden there
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the play/pause button ({@link R.id.playPauseButton}) to reflect the action
|
||||||
|
* that will be performed when the button is clicked..
|
||||||
|
* @param action the action that is performed when the play/pause button is clicked
|
||||||
|
*/
|
||||||
|
private void updatePlayPauseButton(final PlayButtonAction action) {
|
||||||
|
final AppCompatImageButton button = binding.playPauseButton;
|
||||||
|
switch (action) {
|
||||||
|
case PLAY:
|
||||||
|
button.setContentDescription(context.getString(R.string.play));
|
||||||
|
button.setImageResource(R.drawable.ic_play_arrow);
|
||||||
|
break;
|
||||||
|
case PAUSE:
|
||||||
|
button.setContentDescription(context.getString(R.string.pause));
|
||||||
|
button.setImageResource(R.drawable.ic_pause);
|
||||||
|
break;
|
||||||
|
case REPLAY:
|
||||||
|
button.setContentDescription(context.getString(R.string.replay));
|
||||||
|
button.setImageResource(R.drawable.ic_replay);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
//endregion
|
//endregion
|
||||||
|
|
||||||
|
|
||||||
@ -785,7 +812,7 @@ public abstract class VideoPlayerUi extends PlayerUi implements SeekBar.OnSeekBa
|
|||||||
animate(binding.loadingPanel, true, 0);
|
animate(binding.loadingPanel, true, 0);
|
||||||
animate(binding.surfaceForeground, true, 100);
|
animate(binding.surfaceForeground, true, 100);
|
||||||
|
|
||||||
binding.playPauseButton.setImageResource(R.drawable.ic_play_arrow);
|
updatePlayPauseButton(PlayButtonAction.PLAY);
|
||||||
animatePlayButtons(false, 100);
|
animatePlayButtons(false, 100);
|
||||||
binding.getRoot().setKeepScreenOn(false);
|
binding.getRoot().setKeepScreenOn(false);
|
||||||
}
|
}
|
||||||
@ -806,7 +833,7 @@ public abstract class VideoPlayerUi extends PlayerUi implements SeekBar.OnSeekBa
|
|||||||
|
|
||||||
animate(binding.playPauseButton, false, 80, AnimationType.SCALE_AND_ALPHA, 0,
|
animate(binding.playPauseButton, false, 80, AnimationType.SCALE_AND_ALPHA, 0,
|
||||||
() -> {
|
() -> {
|
||||||
binding.playPauseButton.setImageResource(R.drawable.ic_pause);
|
updatePlayPauseButton(PlayButtonAction.PAUSE);
|
||||||
animatePlayButtons(true, 200);
|
animatePlayButtons(true, 200);
|
||||||
if (!isAnyListViewOpen()) {
|
if (!isAnyListViewOpen()) {
|
||||||
binding.playPauseButton.requestFocus();
|
binding.playPauseButton.requestFocus();
|
||||||
@ -836,7 +863,7 @@ public abstract class VideoPlayerUi extends PlayerUi implements SeekBar.OnSeekBa
|
|||||||
|
|
||||||
animate(binding.playPauseButton, false, 80, AnimationType.SCALE_AND_ALPHA, 0,
|
animate(binding.playPauseButton, false, 80, AnimationType.SCALE_AND_ALPHA, 0,
|
||||||
() -> {
|
() -> {
|
||||||
binding.playPauseButton.setImageResource(R.drawable.ic_play_arrow);
|
updatePlayPauseButton(PlayButtonAction.PLAY);
|
||||||
animatePlayButtons(true, 200);
|
animatePlayButtons(true, 200);
|
||||||
if (!isAnyListViewOpen()) {
|
if (!isAnyListViewOpen()) {
|
||||||
binding.playPauseButton.requestFocus();
|
binding.playPauseButton.requestFocus();
|
||||||
@ -860,7 +887,7 @@ public abstract class VideoPlayerUi extends PlayerUi implements SeekBar.OnSeekBa
|
|||||||
|
|
||||||
animate(binding.playPauseButton, false, 0, AnimationType.SCALE_AND_ALPHA, 0,
|
animate(binding.playPauseButton, false, 0, AnimationType.SCALE_AND_ALPHA, 0,
|
||||||
() -> {
|
() -> {
|
||||||
binding.playPauseButton.setImageResource(R.drawable.ic_replay);
|
updatePlayPauseButton(PlayButtonAction.REPLAY);
|
||||||
animatePlayButtons(true, DEFAULT_CONTROLS_DURATION);
|
animatePlayButtons(true, DEFAULT_CONTROLS_DURATION);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -31,8 +31,10 @@ import org.schabi.newpipe.extractor.localization.Localization;
|
|||||||
import org.schabi.newpipe.streams.io.NoFileManagerSafeGuard;
|
import org.schabi.newpipe.streams.io.NoFileManagerSafeGuard;
|
||||||
import org.schabi.newpipe.streams.io.StoredFileHelper;
|
import org.schabi.newpipe.streams.io.StoredFileHelper;
|
||||||
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.ZipHelper;
|
import org.schabi.newpipe.util.ZipHelper;
|
||||||
|
import org.schabi.newpipe.util.image.PreferredImageQuality;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
@ -105,9 +107,11 @@ public class ContentSettingsFragment extends BasePreferenceFragment {
|
|||||||
.getPreferredContentCountry(requireContext());
|
.getPreferredContentCountry(requireContext());
|
||||||
initialLanguage = defaultPreferences.getString(getString(R.string.app_language_key), "en");
|
initialLanguage = defaultPreferences.getString(getString(R.string.app_language_key), "en");
|
||||||
|
|
||||||
findPreference(getString(R.string.download_thumbnail_key)).setOnPreferenceChangeListener(
|
final Preference imageQualityPreference = requirePreference(R.string.image_quality_key);
|
||||||
|
imageQualityPreference.setOnPreferenceChangeListener(
|
||||||
(preference, newValue) -> {
|
(preference, newValue) -> {
|
||||||
PicassoHelper.setShouldLoadImages((Boolean) newValue);
|
ImageStrategy.setPreferredImageQuality(PreferredImageQuality
|
||||||
|
.fromPreferenceKey(requireContext(), (String) newValue));
|
||||||
try {
|
try {
|
||||||
PicassoHelper.clearCache(preference.getContext());
|
PicassoHelper.clearCache(preference.getContext());
|
||||||
Toast.makeText(preference.getContext(),
|
Toast.makeText(preference.getContext(),
|
||||||
|
@ -2,6 +2,7 @@ package org.schabi.newpipe.settings
|
|||||||
|
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import org.schabi.newpipe.MainActivity.DEBUG
|
||||||
import org.schabi.newpipe.streams.io.SharpOutputStream
|
import org.schabi.newpipe.streams.io.SharpOutputStream
|
||||||
import org.schabi.newpipe.streams.io.StoredFileHelper
|
import org.schabi.newpipe.streams.io.StoredFileHelper
|
||||||
import org.schabi.newpipe.util.ZipHelper
|
import org.schabi.newpipe.util.ZipHelper
|
||||||
@ -32,7 +33,9 @@ class ContentSettingsManager(private val fileLocator: NewPipeFileLocator) {
|
|||||||
output.flush()
|
output.flush()
|
||||||
}
|
}
|
||||||
} catch (e: IOException) {
|
} catch (e: IOException) {
|
||||||
Log.e(TAG, "Unable to exportDatabase", e)
|
if (DEBUG) {
|
||||||
|
Log.e(TAG, "Unable to exportDatabase", e)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ZipHelper.addFileToZip(outZip, fileLocator.settings.path, "newpipe.settings")
|
ZipHelper.addFileToZip(outZip, fileLocator.settings.path, "newpipe.settings")
|
||||||
@ -105,9 +108,13 @@ class ContentSettingsManager(private val fileLocator: NewPipeFileLocator) {
|
|||||||
preferenceEditor.commit()
|
preferenceEditor.commit()
|
||||||
}
|
}
|
||||||
} catch (e: IOException) {
|
} catch (e: IOException) {
|
||||||
Log.e(TAG, "Unable to loadSharedPreferences", e)
|
if (DEBUG) {
|
||||||
|
Log.e(TAG, "Unable to loadSharedPreferences", e)
|
||||||
|
}
|
||||||
} catch (e: ClassNotFoundException) {
|
} catch (e: ClassNotFoundException) {
|
||||||
Log.e(TAG, "Unable to loadSharedPreferences", e)
|
if (DEBUG) {
|
||||||
|
Log.e(TAG, "Unable to loadSharedPreferences", e)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -10,7 +10,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.local.feed.notifications.NotificationWorker;
|
import org.schabi.newpipe.local.feed.notifications.NotificationWorker;
|
||||||
import org.schabi.newpipe.util.PicassoHelper;
|
import org.schabi.newpipe.util.image.PicassoHelper;
|
||||||
|
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
|
@ -44,21 +44,11 @@ public final class NewPipeSettings {
|
|||||||
private NewPipeSettings() { }
|
private NewPipeSettings() { }
|
||||||
|
|
||||||
public static void initSettings(final Context context) {
|
public static void initSettings(final Context context) {
|
||||||
// check if there are entries in the prefs to determine whether this is the first app run
|
// check if the last used preference version is set
|
||||||
Boolean isFirstRun = null;
|
// to determine whether this is the first app run
|
||||||
final Set<String> prefsKeys = PreferenceManager.getDefaultSharedPreferences(context)
|
final int lastUsedPrefVersion = PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
.getAll().keySet();
|
.getInt(context.getString(R.string.last_used_preferences_version), -1);
|
||||||
for (final String key: prefsKeys) {
|
final boolean isFirstRun = lastUsedPrefVersion == -1;
|
||||||
// ACRA stores some info in the prefs during app initialization
|
|
||||||
// which happens before this method is called. Therefore ignore ACRA-related keys.
|
|
||||||
if (!key.toLowerCase().startsWith("acra")) {
|
|
||||||
isFirstRun = false;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (isFirstRun == null) {
|
|
||||||
isFirstRun = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// first run migrations, then setDefaultValues, since the latter requires the correct types
|
// first run migrations, then setDefaultValues, since the latter requires the correct types
|
||||||
SettingMigrations.runMigrationsIfNeeded(context, isFirstRun);
|
SettingMigrations.runMigrationsIfNeeded(context, isFirstRun);
|
||||||
|
@ -19,7 +19,7 @@ import org.schabi.newpipe.R;
|
|||||||
import org.schabi.newpipe.database.subscription.SubscriptionEntity;
|
import org.schabi.newpipe.database.subscription.SubscriptionEntity;
|
||||||
import org.schabi.newpipe.error.ErrorUtil;
|
import org.schabi.newpipe.error.ErrorUtil;
|
||||||
import org.schabi.newpipe.local.subscription.SubscriptionManager;
|
import org.schabi.newpipe.local.subscription.SubscriptionManager;
|
||||||
import org.schabi.newpipe.util.PicassoHelper;
|
import org.schabi.newpipe.util.image.PicassoHelper;
|
||||||
import org.schabi.newpipe.util.ThemeHelper;
|
import org.schabi.newpipe.util.ThemeHelper;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
@ -25,7 +25,7 @@ import org.schabi.newpipe.error.ErrorUtil;
|
|||||||
import org.schabi.newpipe.error.UserAction;
|
import org.schabi.newpipe.error.UserAction;
|
||||||
import org.schabi.newpipe.local.playlist.LocalPlaylistManager;
|
import org.schabi.newpipe.local.playlist.LocalPlaylistManager;
|
||||||
import org.schabi.newpipe.local.playlist.RemotePlaylistManager;
|
import org.schabi.newpipe.local.playlist.RemotePlaylistManager;
|
||||||
import org.schabi.newpipe.util.PicassoHelper;
|
import org.schabi.newpipe.util.image.PicassoHelper;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Vector;
|
import java.util.Vector;
|
||||||
|
@ -128,6 +128,20 @@ public final class SettingMigrations {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
public static final Migration MIGRATION_5_6 = new Migration(5, 6) {
|
||||||
|
@Override
|
||||||
|
protected void migrate(@NonNull final Context context) {
|
||||||
|
final boolean loadImages = sp.getBoolean("download_thumbnail_key", true);
|
||||||
|
|
||||||
|
sp.edit()
|
||||||
|
.putString(context.getString(R.string.image_quality_key),
|
||||||
|
context.getString(loadImages
|
||||||
|
? R.string.image_quality_default
|
||||||
|
: R.string.image_quality_none_key))
|
||||||
|
.apply();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* List of all implemented migrations.
|
* List of all implemented migrations.
|
||||||
* <p>
|
* <p>
|
||||||
@ -140,12 +154,13 @@ public final class SettingMigrations {
|
|||||||
MIGRATION_2_3,
|
MIGRATION_2_3,
|
||||||
MIGRATION_3_4,
|
MIGRATION_3_4,
|
||||||
MIGRATION_4_5,
|
MIGRATION_4_5,
|
||||||
|
MIGRATION_5_6,
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Version number for preferences. Must be incremented every time a migration is necessary.
|
* Version number for preferences. Must be incremented every time a migration is necessary.
|
||||||
*/
|
*/
|
||||||
private static final int VERSION = 5;
|
private static final int VERSION = 6;
|
||||||
|
|
||||||
|
|
||||||
public static void runMigrationsIfNeeded(@NonNull final Context context,
|
public static void runMigrationsIfNeeded(@NonNull final Context context,
|
||||||
|
@ -13,6 +13,7 @@ import androidx.preference.ListPreference;
|
|||||||
import com.google.android.material.snackbar.Snackbar;
|
import com.google.android.material.snackbar.Snackbar;
|
||||||
|
|
||||||
import org.schabi.newpipe.R;
|
import org.schabi.newpipe.R;
|
||||||
|
import org.schabi.newpipe.util.ListHelper;
|
||||||
import org.schabi.newpipe.util.PermissionHelper;
|
import org.schabi.newpipe.util.PermissionHelper;
|
||||||
|
|
||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
@ -26,7 +27,7 @@ public class VideoAudioSettingsFragment extends BasePreferenceFragment {
|
|||||||
addPreferencesFromResourceRegistry();
|
addPreferencesFromResourceRegistry();
|
||||||
|
|
||||||
updateSeekOptions();
|
updateSeekOptions();
|
||||||
|
updateResolutionOptions();
|
||||||
listener = (sharedPreferences, key) -> {
|
listener = (sharedPreferences, key) -> {
|
||||||
|
|
||||||
// on M and above, if user chooses to minimise to popup player on exit
|
// on M and above, if user chooses to minimise to popup player on exit
|
||||||
@ -48,10 +49,84 @@ public class VideoAudioSettingsFragment extends BasePreferenceFragment {
|
|||||||
}
|
}
|
||||||
} else if (getString(R.string.use_inexact_seek_key).equals(key)) {
|
} else if (getString(R.string.use_inexact_seek_key).equals(key)) {
|
||||||
updateSeekOptions();
|
updateSeekOptions();
|
||||||
|
} else if (getString(R.string.show_higher_resolutions_key).equals(key)) {
|
||||||
|
updateResolutionOptions();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update default resolution, default popup resolution & mobile data resolution options.
|
||||||
|
* <br />
|
||||||
|
* Show high resolutions when "Show higher resolution" option is enabled.
|
||||||
|
* Set default resolution to "best resolution" when "Show higher resolution" option
|
||||||
|
* is disabled.
|
||||||
|
*/
|
||||||
|
private void updateResolutionOptions() {
|
||||||
|
final Resources resources = getResources();
|
||||||
|
final boolean showHigherResolutions = getPreferenceManager().getSharedPreferences()
|
||||||
|
.getBoolean(resources.getString(R.string.show_higher_resolutions_key), false);
|
||||||
|
|
||||||
|
// get sorted resolution lists
|
||||||
|
final List<String> resolutionListDescriptions = ListHelper.getSortedResolutionList(
|
||||||
|
resources,
|
||||||
|
R.array.resolution_list_description,
|
||||||
|
R.array.high_resolution_list_descriptions,
|
||||||
|
showHigherResolutions);
|
||||||
|
final List<String> resolutionListValues = ListHelper.getSortedResolutionList(
|
||||||
|
resources,
|
||||||
|
R.array.resolution_list_values,
|
||||||
|
R.array.high_resolution_list_values,
|
||||||
|
showHigherResolutions);
|
||||||
|
final List<String> limitDataUsageResolutionValues = ListHelper.getSortedResolutionList(
|
||||||
|
resources,
|
||||||
|
R.array.limit_data_usage_values_list,
|
||||||
|
R.array.high_resolution_limit_data_usage_values_list,
|
||||||
|
showHigherResolutions);
|
||||||
|
final List<String> limitDataUsageResolutionDescriptions = ListHelper
|
||||||
|
.getSortedResolutionList(resources,
|
||||||
|
R.array.limit_data_usage_description_list,
|
||||||
|
R.array.high_resolution_list_descriptions,
|
||||||
|
showHigherResolutions);
|
||||||
|
|
||||||
|
// get resolution preferences
|
||||||
|
final ListPreference defaultResolution = findPreference(
|
||||||
|
getString(R.string.default_resolution_key));
|
||||||
|
final ListPreference defaultPopupResolution = findPreference(
|
||||||
|
getString(R.string.default_popup_resolution_key));
|
||||||
|
final ListPreference mobileDataResolution = findPreference(
|
||||||
|
getString(R.string.limit_mobile_data_usage_key));
|
||||||
|
|
||||||
|
// update resolution preferences with new resolutions, entries & values for each
|
||||||
|
defaultResolution.setEntries(resolutionListDescriptions.toArray(new String[0]));
|
||||||
|
defaultResolution.setEntryValues(resolutionListValues.toArray(new String[0]));
|
||||||
|
defaultPopupResolution.setEntries(resolutionListDescriptions.toArray(new String[0]));
|
||||||
|
defaultPopupResolution.setEntryValues(resolutionListValues.toArray(new String[0]));
|
||||||
|
mobileDataResolution.setEntries(
|
||||||
|
limitDataUsageResolutionDescriptions.toArray(new String[0]));
|
||||||
|
mobileDataResolution.setEntryValues(limitDataUsageResolutionValues.toArray(new String[0]));
|
||||||
|
|
||||||
|
// if "Show higher resolution" option is disabled,
|
||||||
|
// set default resolution to "best resolution"
|
||||||
|
if (!showHigherResolutions) {
|
||||||
|
if (ListHelper.isHighResolutionSelected(defaultResolution.getValue(),
|
||||||
|
R.array.high_resolution_list_values,
|
||||||
|
resources)) {
|
||||||
|
defaultResolution.setValueIndex(0);
|
||||||
|
}
|
||||||
|
if (ListHelper.isHighResolutionSelected(defaultPopupResolution.getValue(),
|
||||||
|
R.array.high_resolution_list_values,
|
||||||
|
resources)) {
|
||||||
|
defaultPopupResolution.setValueIndex(0);
|
||||||
|
}
|
||||||
|
if (ListHelper.isHighResolutionSelected(mobileDataResolution.getValue(),
|
||||||
|
R.array.high_resolution_limit_data_usage_values_list,
|
||||||
|
resources)) {
|
||||||
|
mobileDataResolution.setValueIndex(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update fast-forward/-rewind seek duration options
|
* Update fast-forward/-rewind seek duration options
|
||||||
* according to language and inexact seek setting.
|
* according to language and inexact seek setting.
|
||||||
|
@ -73,10 +73,8 @@ public final class TabsManager {
|
|||||||
|
|
||||||
private SharedPreferences.OnSharedPreferenceChangeListener getPreferenceChangeListener() {
|
private SharedPreferences.OnSharedPreferenceChangeListener getPreferenceChangeListener() {
|
||||||
return (sp, key) -> {
|
return (sp, key) -> {
|
||||||
if (savedTabsKey.equals(key)) {
|
if (savedTabsKey.equals(key) && savedTabsChangeListener != null) {
|
||||||
if (savedTabsChangeListener != null) {
|
savedTabsChangeListener.onTabsChanged();
|
||||||
savedTabsChangeListener.onTabsChanged();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,7 @@ import android.content.Intent;
|
|||||||
import android.database.Cursor;
|
import android.database.Cursor;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.provider.DocumentsContract;
|
import android.provider.DocumentsContract;
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
@ -14,21 +15,27 @@ import androidx.documentfile.provider.DocumentFile;
|
|||||||
import org.schabi.newpipe.settings.NewPipeSettings;
|
import org.schabi.newpipe.settings.NewPipeSettings;
|
||||||
import org.schabi.newpipe.util.FilePickerActivityHelper;
|
import org.schabi.newpipe.util.FilePickerActivityHelper;
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.Paths;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
import static android.provider.DocumentsContract.Document.COLUMN_DISPLAY_NAME;
|
import static android.provider.DocumentsContract.Document.COLUMN_DISPLAY_NAME;
|
||||||
import static android.provider.DocumentsContract.Root.COLUMN_DOCUMENT_ID;
|
import static android.provider.DocumentsContract.Root.COLUMN_DOCUMENT_ID;
|
||||||
import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty;
|
import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty;
|
||||||
|
|
||||||
public class StoredDirectoryHelper {
|
public class StoredDirectoryHelper {
|
||||||
|
private static final String TAG = StoredDirectoryHelper.class.getSimpleName();
|
||||||
public static final int PERMISSION_FLAGS = Intent.FLAG_GRANT_READ_URI_PERMISSION
|
public static final int PERMISSION_FLAGS = Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||||
| Intent.FLAG_GRANT_WRITE_URI_PERMISSION;
|
| Intent.FLAG_GRANT_WRITE_URI_PERMISSION;
|
||||||
|
|
||||||
private File ioTree;
|
private Path ioTree;
|
||||||
private DocumentFile docTree;
|
private DocumentFile docTree;
|
||||||
|
|
||||||
private Context context;
|
private Context context;
|
||||||
@ -40,7 +47,7 @@ public class StoredDirectoryHelper {
|
|||||||
this.tag = tag;
|
this.tag = tag;
|
||||||
|
|
||||||
if (ContentResolver.SCHEME_FILE.equalsIgnoreCase(path.getScheme())) {
|
if (ContentResolver.SCHEME_FILE.equalsIgnoreCase(path.getScheme())) {
|
||||||
this.ioTree = new File(URI.create(path.toString()));
|
ioTree = Paths.get(URI.create(path.toString()));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -64,13 +71,17 @@ public class StoredDirectoryHelper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public StoredFileHelper createUniqueFile(final String name, final String mime) {
|
public StoredFileHelper createUniqueFile(final String name, final String mime) {
|
||||||
final ArrayList<String> matches = new ArrayList<>();
|
final List<String> matches = new ArrayList<>();
|
||||||
final String[] filename = splitFilename(name);
|
final String[] filename = splitFilename(name);
|
||||||
final String lcFilename = filename[0].toLowerCase();
|
final String lcFileName = filename[0].toLowerCase();
|
||||||
|
|
||||||
if (docTree == null) {
|
if (docTree == null) {
|
||||||
for (final File file : ioTree.listFiles()) {
|
try (Stream<Path> stream = Files.list(ioTree)) {
|
||||||
addIfStartWith(matches, lcFilename, file.getName());
|
matches.addAll(stream.map(path -> path.getFileName().toString().toLowerCase())
|
||||||
|
.filter(fileName -> fileName.startsWith(lcFileName))
|
||||||
|
.collect(Collectors.toList()));
|
||||||
|
} catch (final IOException e) {
|
||||||
|
Log.e(TAG, "Exception while traversing " + ioTree, e);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// warning: SAF file listing is very slow
|
// warning: SAF file listing is very slow
|
||||||
@ -82,37 +93,37 @@ public class StoredDirectoryHelper {
|
|||||||
final ContentResolver cr = context.getContentResolver();
|
final ContentResolver cr = context.getContentResolver();
|
||||||
|
|
||||||
try (Cursor cursor = cr.query(docTreeChildren, projection, selection,
|
try (Cursor cursor = cr.query(docTreeChildren, projection, selection,
|
||||||
new String[]{lcFilename}, null)) {
|
new String[]{lcFileName}, null)) {
|
||||||
if (cursor != null) {
|
if (cursor != null) {
|
||||||
while (cursor.moveToNext()) {
|
while (cursor.moveToNext()) {
|
||||||
addIfStartWith(matches, lcFilename, cursor.getString(0));
|
addIfStartWith(matches, lcFileName, cursor.getString(0));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (matches.size() < 1) {
|
if (matches.isEmpty()) {
|
||||||
return createFile(name, mime, true);
|
return createFile(name, mime, true);
|
||||||
} else {
|
}
|
||||||
// check if the filename is in use
|
|
||||||
String lcName = name.toLowerCase();
|
|
||||||
for (final String testName : matches) {
|
|
||||||
if (testName.equals(lcName)) {
|
|
||||||
lcName = null;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// check if not in use
|
// check if the filename is in use
|
||||||
if (lcName != null) {
|
String lcName = name.toLowerCase();
|
||||||
return createFile(name, mime, true);
|
for (final String testName : matches) {
|
||||||
|
if (testName.equals(lcName)) {
|
||||||
|
lcName = null;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// create file if filename not in use
|
||||||
|
if (lcName != null) {
|
||||||
|
return createFile(name, mime, true);
|
||||||
|
}
|
||||||
|
|
||||||
Collections.sort(matches, String::compareTo);
|
Collections.sort(matches, String::compareTo);
|
||||||
|
|
||||||
for (int i = 1; i < 1000; i++) {
|
for (int i = 1; i < 1000; i++) {
|
||||||
if (Collections.binarySearch(matches, makeFileName(lcFilename, i, filename[1])) < 0) {
|
if (Collections.binarySearch(matches, makeFileName(lcFileName, i, filename[1])) < 0) {
|
||||||
return createFile(makeFileName(filename[0], i, filename[1]), mime, true);
|
return createFile(makeFileName(filename[0], i, filename[1]), mime, true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -141,11 +152,11 @@ public class StoredDirectoryHelper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public Uri getUri() {
|
public Uri getUri() {
|
||||||
return docTree == null ? Uri.fromFile(ioTree) : docTree.getUri();
|
return docTree == null ? Uri.fromFile(ioTree.toFile()) : docTree.getUri();
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean exists() {
|
public boolean exists() {
|
||||||
return docTree == null ? ioTree.exists() : docTree.exists();
|
return docTree == null ? Files.exists(ioTree) : docTree.exists();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -159,8 +170,8 @@ public class StoredDirectoryHelper {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Only using Java I/O. Creates the directory named by this abstract pathname, including any
|
* Only using Java I/O. Creates the directory named by this abstract pathname, including any
|
||||||
* necessary but nonexistent parent directories. Note that if this
|
* necessary but nonexistent parent directories.
|
||||||
* operation fails it may have succeeded in creating some of the necessary
|
* Note that if this operation fails it may have succeeded in creating some of the necessary
|
||||||
* parent directories.
|
* parent directories.
|
||||||
*
|
*
|
||||||
* @return <code>true</code> if and only if the directory was created,
|
* @return <code>true</code> if and only if the directory was created,
|
||||||
@ -169,7 +180,12 @@ public class StoredDirectoryHelper {
|
|||||||
*/
|
*/
|
||||||
public boolean mkdirs() {
|
public boolean mkdirs() {
|
||||||
if (docTree == null) {
|
if (docTree == null) {
|
||||||
return ioTree.exists() || ioTree.mkdirs();
|
try {
|
||||||
|
Files.createDirectories(ioTree);
|
||||||
|
} catch (final IOException e) {
|
||||||
|
Log.e(TAG, "Error while creating directories at " + ioTree, e);
|
||||||
|
}
|
||||||
|
return Files.exists(ioTree);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (docTree.exists()) {
|
if (docTree.exists()) {
|
||||||
@ -206,8 +222,8 @@ public class StoredDirectoryHelper {
|
|||||||
|
|
||||||
public Uri findFile(final String filename) {
|
public Uri findFile(final String filename) {
|
||||||
if (docTree == null) {
|
if (docTree == null) {
|
||||||
final File res = new File(ioTree, filename);
|
final Path res = ioTree.resolve(filename);
|
||||||
return res.exists() ? Uri.fromFile(res) : null;
|
return Files.exists(res) ? Uri.fromFile(res.toFile()) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
final DocumentFile res = findFileSAFHelper(context, docTree, filename);
|
final DocumentFile res = findFileSAFHelper(context, docTree, filename);
|
||||||
@ -215,7 +231,7 @@ public class StoredDirectoryHelper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public boolean canWrite() {
|
public boolean canWrite() {
|
||||||
return docTree == null ? ioTree.canWrite() : docTree.canWrite();
|
return docTree == null ? Files.isWritable(ioTree) : docTree.canWrite();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -230,14 +246,14 @@ public class StoredDirectoryHelper {
|
|||||||
@NonNull
|
@NonNull
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return (docTree == null ? Uri.fromFile(ioTree) : docTree.getUri()).toString();
|
return (docTree == null ? Uri.fromFile(ioTree.toFile()) : docTree.getUri()).toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
////////////////////
|
////////////////////
|
||||||
// Utils
|
// Utils
|
||||||
///////////////////
|
///////////////////
|
||||||
|
|
||||||
private static void addIfStartWith(final ArrayList<String> list, @NonNull final String base,
|
private static void addIfStartWith(final List<String> list, @NonNull final String base,
|
||||||
final String str) {
|
final String str) {
|
||||||
if (isNullOrEmpty(str)) {
|
if (isNullOrEmpty(str)) {
|
||||||
return;
|
return;
|
||||||
@ -248,6 +264,12 @@ public class StoredDirectoryHelper {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Splits the filename into the name and extension.
|
||||||
|
*
|
||||||
|
* @param filename The filename to split
|
||||||
|
* @return A String array with the name at index 0 and extension at index 1
|
||||||
|
*/
|
||||||
private static String[] splitFilename(@NonNull final String filename) {
|
private static String[] splitFilename(@NonNull final String filename) {
|
||||||
final int dotIndex = filename.lastIndexOf('.');
|
final int dotIndex = filename.lastIndexOf('.');
|
||||||
|
|
||||||
@ -259,7 +281,7 @@ public class StoredDirectoryHelper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static String makeFileName(final String name, final int idx, final String ext) {
|
private static String makeFileName(final String name, final int idx, final String ext) {
|
||||||
return name.concat(" (").concat(String.valueOf(idx)).concat(")").concat(ext);
|
return name + "(" + idx + ")" + ext;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -23,6 +23,9 @@ import java.io.File;
|
|||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.Paths;
|
||||||
|
|
||||||
import us.shandian.giga.io.FileStream;
|
import us.shandian.giga.io.FileStream;
|
||||||
import us.shandian.giga.io.FileStreamSAF;
|
import us.shandian.giga.io.FileStreamSAF;
|
||||||
@ -36,7 +39,7 @@ public class StoredFileHelper implements Serializable {
|
|||||||
|
|
||||||
private transient DocumentFile docFile;
|
private transient DocumentFile docFile;
|
||||||
private transient DocumentFile docTree;
|
private transient DocumentFile docTree;
|
||||||
private transient File ioFile;
|
private transient Path ioPath;
|
||||||
private transient Context context;
|
private transient Context context;
|
||||||
|
|
||||||
protected String source;
|
protected String source;
|
||||||
@ -49,7 +52,8 @@ public class StoredFileHelper implements Serializable {
|
|||||||
|
|
||||||
public StoredFileHelper(final Context context, final Uri uri, final String mime) {
|
public StoredFileHelper(final Context context, final Uri uri, final String mime) {
|
||||||
if (FilePickerActivityHelper.isOwnFileUri(context, uri)) {
|
if (FilePickerActivityHelper.isOwnFileUri(context, uri)) {
|
||||||
ioFile = Utils.getFileForUri(uri);
|
final File ioFile = Utils.getFileForUri(uri);
|
||||||
|
ioPath = ioFile.toPath();
|
||||||
source = Uri.fromFile(ioFile).toString();
|
source = Uri.fromFile(ioFile).toString();
|
||||||
} else {
|
} else {
|
||||||
docFile = DocumentFile.fromSingleUri(context, uri);
|
docFile = DocumentFile.fromSingleUri(context, uri);
|
||||||
@ -100,26 +104,18 @@ public class StoredFileHelper implements Serializable {
|
|||||||
this.srcType = this.docFile.getType();
|
this.srcType = this.docFile.getType();
|
||||||
}
|
}
|
||||||
|
|
||||||
StoredFileHelper(final File location, final String filename, final String mime)
|
StoredFileHelper(final Path location, final String filename, final String mime)
|
||||||
throws IOException {
|
throws IOException {
|
||||||
this.ioFile = new File(location, filename);
|
ioPath = location.resolve(filename);
|
||||||
|
|
||||||
if (this.ioFile.exists()) {
|
Files.deleteIfExists(ioPath);
|
||||||
if (!this.ioFile.isFile() && !this.ioFile.delete()) {
|
Files.createFile(ioPath);
|
||||||
throw new IOException("The filename is already in use by non-file entity "
|
|
||||||
+ "and cannot overwrite it");
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (!this.ioFile.createNewFile()) {
|
|
||||||
throw new IOException("Cannot create the file");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.source = Uri.fromFile(this.ioFile).toString();
|
source = Uri.fromFile(ioPath.toFile()).toString();
|
||||||
this.sourceTree = Uri.fromFile(location).toString();
|
sourceTree = Uri.fromFile(location.toFile()).toString();
|
||||||
|
|
||||||
this.srcName = ioFile.getName();
|
srcName = ioPath.getFileName().toString();
|
||||||
this.srcType = mime;
|
srcType = mime;
|
||||||
}
|
}
|
||||||
|
|
||||||
public StoredFileHelper(final Context context, @Nullable final Uri parent,
|
public StoredFileHelper(final Context context, @Nullable final Uri parent,
|
||||||
@ -129,12 +125,12 @@ public class StoredFileHelper implements Serializable {
|
|||||||
|
|
||||||
if (path.getScheme() == null
|
if (path.getScheme() == null
|
||||||
|| path.getScheme().equalsIgnoreCase(ContentResolver.SCHEME_FILE)) {
|
|| path.getScheme().equalsIgnoreCase(ContentResolver.SCHEME_FILE)) {
|
||||||
this.ioFile = new File(URI.create(this.source));
|
this.ioPath = Paths.get(URI.create(this.source));
|
||||||
} else {
|
} else {
|
||||||
final DocumentFile file = DocumentFile.fromSingleUri(context, path);
|
final DocumentFile file = DocumentFile.fromSingleUri(context, path);
|
||||||
|
|
||||||
if (file == null) {
|
if (file == null) {
|
||||||
throw new RuntimeException("SAF not available");
|
throw new IOException("SAF not available");
|
||||||
}
|
}
|
||||||
|
|
||||||
this.context = context;
|
this.context = context;
|
||||||
@ -187,7 +183,7 @@ public class StoredFileHelper implements Serializable {
|
|||||||
assertValid();
|
assertValid();
|
||||||
|
|
||||||
if (docFile == null) {
|
if (docFile == null) {
|
||||||
return new FileStream(ioFile);
|
return new FileStream(ioPath.toFile());
|
||||||
} else {
|
} else {
|
||||||
return new FileStreamSAF(context.getContentResolver(), docFile.getUri());
|
return new FileStreamSAF(context.getContentResolver(), docFile.getUri());
|
||||||
}
|
}
|
||||||
@ -211,7 +207,7 @@ public class StoredFileHelper implements Serializable {
|
|||||||
public Uri getUri() {
|
public Uri getUri() {
|
||||||
assertValid();
|
assertValid();
|
||||||
|
|
||||||
return docFile == null ? Uri.fromFile(ioFile) : docFile.getUri();
|
return docFile == null ? Uri.fromFile(ioPath.toFile()) : docFile.getUri();
|
||||||
}
|
}
|
||||||
|
|
||||||
public Uri getParentUri() {
|
public Uri getParentUri() {
|
||||||
@ -233,7 +229,12 @@ public class StoredFileHelper implements Serializable {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (docFile == null) {
|
if (docFile == null) {
|
||||||
return ioFile.delete();
|
try {
|
||||||
|
return Files.deleteIfExists(ioPath);
|
||||||
|
} catch (final IOException e) {
|
||||||
|
Log.e(TAG, "Exception while deleting " + ioPath, e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final boolean res = docFile.delete();
|
final boolean res = docFile.delete();
|
||||||
@ -252,21 +253,30 @@ public class StoredFileHelper implements Serializable {
|
|||||||
public long length() {
|
public long length() {
|
||||||
assertValid();
|
assertValid();
|
||||||
|
|
||||||
return docFile == null ? ioFile.length() : docFile.length();
|
if (docFile == null) {
|
||||||
|
try {
|
||||||
|
return Files.size(ioPath);
|
||||||
|
} catch (final IOException e) {
|
||||||
|
Log.e(TAG, "Exception while getting the size of " + ioPath, e);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return docFile.length();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean canWrite() {
|
public boolean canWrite() {
|
||||||
if (source == null) {
|
if (source == null) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return docFile == null ? ioFile.canWrite() : docFile.canWrite();
|
return docFile == null ? Files.isWritable(ioPath) : docFile.canWrite();
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getName() {
|
public String getName() {
|
||||||
if (source == null) {
|
if (source == null) {
|
||||||
return srcName;
|
return srcName;
|
||||||
} else if (docFile == null) {
|
} else if (docFile == null) {
|
||||||
return ioFile.getName();
|
return ioPath.getFileName().toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
final String name = docFile.getName();
|
final String name = docFile.getName();
|
||||||
@ -287,12 +297,11 @@ public class StoredFileHelper implements Serializable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public boolean existsAsFile() {
|
public boolean existsAsFile() {
|
||||||
if (source == null || (docFile == null && ioFile == null)) {
|
if (source == null || (docFile == null && ioPath == null)) {
|
||||||
if (DEBUG) {
|
if (DEBUG) {
|
||||||
Log.d(TAG, "existsAsFile called but something is null: source = ["
|
Log.d(TAG, "existsAsFile called but something is null: source = ["
|
||||||
+ (source == null ? "null => storage is invalid" : source)
|
+ (source == null ? "null => storage is invalid" : source)
|
||||||
+ "], docFile = [" + (docFile == null ? "null" : docFile)
|
+ "], docFile = [" + docFile + "], ioPath = [" + ioPath + "]");
|
||||||
+ "], ioFile = [" + (ioFile == null ? "null" : ioFile) + "]");
|
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -300,7 +309,7 @@ public class StoredFileHelper implements Serializable {
|
|||||||
// WARNING: DocumentFile.exists() and DocumentFile.isFile() methods are slow
|
// WARNING: DocumentFile.exists() and DocumentFile.isFile() methods are slow
|
||||||
// docFile.isVirtual() means it is non-physical?
|
// docFile.isVirtual() means it is non-physical?
|
||||||
return docFile == null
|
return docFile == null
|
||||||
? (ioFile.exists() && ioFile.isFile())
|
? Files.isRegularFile(ioPath)
|
||||||
: (docFile.exists() && docFile.isFile());
|
: (docFile.exists() && docFile.isFile());
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -310,8 +319,10 @@ public class StoredFileHelper implements Serializable {
|
|||||||
|
|
||||||
if (docFile == null) {
|
if (docFile == null) {
|
||||||
try {
|
try {
|
||||||
result = ioFile.createNewFile();
|
Files.createFile(ioPath);
|
||||||
|
result = true;
|
||||||
} catch (final IOException e) {
|
} catch (final IOException e) {
|
||||||
|
Log.e(TAG, "Exception while creating " + ioPath, e);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
} else if (docTree == null) {
|
} else if (docTree == null) {
|
||||||
@ -332,7 +343,8 @@ public class StoredFileHelper implements Serializable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (result) {
|
if (result) {
|
||||||
source = (docFile == null ? Uri.fromFile(ioFile) : docFile.getUri()).toString();
|
source = (docFile == null ? Uri.fromFile(ioPath.toFile()) : docFile.getUri())
|
||||||
|
.toString();
|
||||||
srcName = getName();
|
srcName = getName();
|
||||||
srcType = getType();
|
srcType = getType();
|
||||||
}
|
}
|
||||||
@ -352,7 +364,7 @@ public class StoredFileHelper implements Serializable {
|
|||||||
|
|
||||||
docTree = null;
|
docTree = null;
|
||||||
docFile = null;
|
docFile = null;
|
||||||
ioFile = null;
|
ioPath = null;
|
||||||
context = null;
|
context = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -383,7 +395,7 @@ public class StoredFileHelper implements Serializable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (this.isDirect()) {
|
if (this.isDirect()) {
|
||||||
return this.ioFile.getPath().equalsIgnoreCase(storage.ioFile.getPath());
|
return this.ioPath.equals(storage.ioPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
return DocumentsContract.getDocumentId(this.docFile.getUri())
|
return DocumentsContract.getDocumentId(this.docFile.getUri())
|
||||||
|
@ -13,7 +13,7 @@ import androidx.annotation.Nullable;
|
|||||||
|
|
||||||
import org.schabi.newpipe.R;
|
import org.schabi.newpipe.R;
|
||||||
import org.schabi.newpipe.extractor.stream.AudioStream;
|
import org.schabi.newpipe.extractor.stream.AudioStream;
|
||||||
import org.schabi.newpipe.util.StreamItemAdapter.StreamSizeWrapper;
|
import org.schabi.newpipe.util.StreamItemAdapter.StreamInfoWrapper;
|
||||||
|
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@ -75,15 +75,15 @@ public class AudioTrackAdapter extends BaseAdapter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static class AudioTracksWrapper implements Serializable {
|
public static class AudioTracksWrapper implements Serializable {
|
||||||
private final List<StreamSizeWrapper<AudioStream>> tracksList;
|
private final List<StreamInfoWrapper<AudioStream>> tracksList;
|
||||||
|
|
||||||
public AudioTracksWrapper(@NonNull final List<List<AudioStream>> groupedAudioStreams,
|
public AudioTracksWrapper(@NonNull final List<List<AudioStream>> groupedAudioStreams,
|
||||||
@Nullable final Context context) {
|
@Nullable final Context context) {
|
||||||
this.tracksList = groupedAudioStreams.stream().map(streams ->
|
this.tracksList = groupedAudioStreams.stream().map(streams ->
|
||||||
new StreamSizeWrapper<>(streams, context)).collect(Collectors.toList());
|
new StreamInfoWrapper<>(streams, context)).collect(Collectors.toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<StreamSizeWrapper<AudioStream>> getTracksList() {
|
public List<StreamInfoWrapper<AudioStream>> getTracksList() {
|
||||||
return tracksList;
|
return tracksList;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
151
app/src/main/java/org/schabi/newpipe/util/ChannelTabHelper.java
Normal file
151
app/src/main/java/org/schabi/newpipe/util/ChannelTabHelper.java
Normal file
@ -0,0 +1,151 @@
|
|||||||
|
package org.schabi.newpipe.util;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.SharedPreferences;
|
||||||
|
|
||||||
|
import androidx.annotation.StringRes;
|
||||||
|
|
||||||
|
import org.schabi.newpipe.R;
|
||||||
|
import org.schabi.newpipe.extractor.channel.tabs.ChannelTabs;
|
||||||
|
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
public final class ChannelTabHelper {
|
||||||
|
private ChannelTabHelper() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param tab the channel tab to check
|
||||||
|
* @return whether the tab should contain (playable) streams or not
|
||||||
|
*/
|
||||||
|
public static boolean isStreamsTab(final String tab) {
|
||||||
|
switch (tab) {
|
||||||
|
case ChannelTabs.VIDEOS:
|
||||||
|
case ChannelTabs.TRACKS:
|
||||||
|
case ChannelTabs.SHORTS:
|
||||||
|
case ChannelTabs.LIVESTREAMS:
|
||||||
|
return true;
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param tab the channel tab link handler to check
|
||||||
|
* @return whether the tab should contain (playable) streams or not
|
||||||
|
*/
|
||||||
|
public static boolean isStreamsTab(final ListLinkHandler tab) {
|
||||||
|
final List<String> contentFilters = tab.getContentFilters();
|
||||||
|
if (contentFilters.isEmpty()) {
|
||||||
|
return false; // this should never happen, but check just to be sure
|
||||||
|
} else {
|
||||||
|
return isStreamsTab(contentFilters.get(0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@StringRes
|
||||||
|
private static int getShowTabKey(final String tab) {
|
||||||
|
switch (tab) {
|
||||||
|
case ChannelTabs.VIDEOS:
|
||||||
|
return R.string.show_channel_tabs_videos;
|
||||||
|
case ChannelTabs.TRACKS:
|
||||||
|
return R.string.show_channel_tabs_tracks;
|
||||||
|
case ChannelTabs.SHORTS:
|
||||||
|
return R.string.show_channel_tabs_shorts;
|
||||||
|
case ChannelTabs.LIVESTREAMS:
|
||||||
|
return R.string.show_channel_tabs_livestreams;
|
||||||
|
case ChannelTabs.CHANNELS:
|
||||||
|
return R.string.show_channel_tabs_channels;
|
||||||
|
case ChannelTabs.PLAYLISTS:
|
||||||
|
return R.string.show_channel_tabs_playlists;
|
||||||
|
case ChannelTabs.ALBUMS:
|
||||||
|
return R.string.show_channel_tabs_albums;
|
||||||
|
default:
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@StringRes
|
||||||
|
private static int getFetchFeedTabKey(final String tab) {
|
||||||
|
switch (tab) {
|
||||||
|
case ChannelTabs.VIDEOS:
|
||||||
|
return R.string.fetch_channel_tabs_videos;
|
||||||
|
case ChannelTabs.TRACKS:
|
||||||
|
return R.string.fetch_channel_tabs_tracks;
|
||||||
|
case ChannelTabs.SHORTS:
|
||||||
|
return R.string.fetch_channel_tabs_shorts;
|
||||||
|
case ChannelTabs.LIVESTREAMS:
|
||||||
|
return R.string.fetch_channel_tabs_livestreams;
|
||||||
|
default:
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@StringRes
|
||||||
|
public static int getTranslationKey(final String tab) {
|
||||||
|
switch (tab) {
|
||||||
|
case ChannelTabs.VIDEOS:
|
||||||
|
return R.string.channel_tab_videos;
|
||||||
|
case ChannelTabs.TRACKS:
|
||||||
|
return R.string.channel_tab_tracks;
|
||||||
|
case ChannelTabs.SHORTS:
|
||||||
|
return R.string.channel_tab_shorts;
|
||||||
|
case ChannelTabs.LIVESTREAMS:
|
||||||
|
return R.string.channel_tab_livestreams;
|
||||||
|
case ChannelTabs.CHANNELS:
|
||||||
|
return R.string.channel_tab_channels;
|
||||||
|
case ChannelTabs.PLAYLISTS:
|
||||||
|
return R.string.channel_tab_playlists;
|
||||||
|
case ChannelTabs.ALBUMS:
|
||||||
|
return R.string.channel_tab_albums;
|
||||||
|
default:
|
||||||
|
return R.string.unknown_content;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean showChannelTab(final Context context,
|
||||||
|
final SharedPreferences sharedPreferences,
|
||||||
|
@StringRes final int key) {
|
||||||
|
final Set<String> enabledTabs = sharedPreferences.getStringSet(
|
||||||
|
context.getString(R.string.show_channel_tabs_key), null);
|
||||||
|
if (enabledTabs == null) {
|
||||||
|
return true; // default to true
|
||||||
|
} else {
|
||||||
|
return enabledTabs.contains(context.getString(key));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean showChannelTab(final Context context,
|
||||||
|
final SharedPreferences sharedPreferences,
|
||||||
|
final String tab) {
|
||||||
|
final int key = ChannelTabHelper.getShowTabKey(tab);
|
||||||
|
if (key == -1) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return showChannelTab(context, sharedPreferences, key);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean fetchFeedChannelTab(final Context context,
|
||||||
|
final SharedPreferences sharedPreferences,
|
||||||
|
final ListLinkHandler tab) {
|
||||||
|
final List<String> contentFilters = tab.getContentFilters();
|
||||||
|
if (contentFilters.isEmpty()) {
|
||||||
|
return false; // this should never happen, but check just to be sure
|
||||||
|
}
|
||||||
|
|
||||||
|
final int key = ChannelTabHelper.getFetchFeedTabKey(contentFilters.get(0));
|
||||||
|
if (key == -1) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
final Set<String> enabledTabs = sharedPreferences.getStringSet(
|
||||||
|
context.getString(R.string.feed_fetch_channel_tabs_key), null);
|
||||||
|
if (enabledTabs == null) {
|
||||||
|
return true; // default to true
|
||||||
|
} else {
|
||||||
|
return enabledTabs.contains(context.getString(key));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -36,17 +36,15 @@ import org.schabi.newpipe.R;
|
|||||||
import org.schabi.newpipe.extractor.Info;
|
import org.schabi.newpipe.extractor.Info;
|
||||||
import org.schabi.newpipe.extractor.InfoItem;
|
import org.schabi.newpipe.extractor.InfoItem;
|
||||||
import org.schabi.newpipe.extractor.ListExtractor.InfoItemsPage;
|
import org.schabi.newpipe.extractor.ListExtractor.InfoItemsPage;
|
||||||
import org.schabi.newpipe.extractor.ListInfo;
|
|
||||||
import org.schabi.newpipe.extractor.MetaInfo;
|
import org.schabi.newpipe.extractor.MetaInfo;
|
||||||
import org.schabi.newpipe.extractor.NewPipe;
|
import org.schabi.newpipe.extractor.NewPipe;
|
||||||
import org.schabi.newpipe.extractor.Page;
|
import org.schabi.newpipe.extractor.Page;
|
||||||
import org.schabi.newpipe.extractor.StreamingService;
|
|
||||||
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.comments.CommentsInfo;
|
import org.schabi.newpipe.extractor.comments.CommentsInfo;
|
||||||
import org.schabi.newpipe.extractor.comments.CommentsInfoItem;
|
import org.schabi.newpipe.extractor.comments.CommentsInfoItem;
|
||||||
import org.schabi.newpipe.extractor.feed.FeedExtractor;
|
|
||||||
import org.schabi.newpipe.extractor.feed.FeedInfo;
|
|
||||||
import org.schabi.newpipe.extractor.kiosk.KioskInfo;
|
import org.schabi.newpipe.extractor.kiosk.KioskInfo;
|
||||||
|
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.search.SearchInfo;
|
import org.schabi.newpipe.extractor.search.SearchInfo;
|
||||||
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||||
@ -127,28 +125,24 @@ public final class ExtractorHelper {
|
|||||||
ChannelInfo.getInfo(NewPipe.getService(serviceId), url)));
|
ChannelInfo.getInfo(NewPipe.getService(serviceId), url)));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Single<InfoItemsPage<StreamInfoItem>> getMoreChannelItems(final int serviceId,
|
public static Single<ChannelTabInfo> getChannelTab(final int serviceId,
|
||||||
final String url,
|
final ListLinkHandler listLinkHandler,
|
||||||
final Page nextPage) {
|
final boolean forceLoad) {
|
||||||
checkServiceId(serviceId);
|
checkServiceId(serviceId);
|
||||||
return Single.fromCallable(() ->
|
return checkCache(forceLoad, serviceId,
|
||||||
ChannelInfo.getMoreItems(NewPipe.getService(serviceId), url, nextPage));
|
listLinkHandler.getUrl(), InfoItem.InfoType.CHANNEL,
|
||||||
|
Single.fromCallable(() ->
|
||||||
|
ChannelTabInfo.getInfo(NewPipe.getService(serviceId), listLinkHandler)));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Single<ListInfo<StreamInfoItem>> getFeedInfoFallbackToChannelInfo(
|
public static Single<InfoItemsPage<InfoItem>> getMoreChannelTabItems(
|
||||||
final int serviceId, final String url) {
|
final int serviceId,
|
||||||
final Maybe<ListInfo<StreamInfoItem>> maybeFeedInfo = Maybe.fromCallable(() -> {
|
final ListLinkHandler listLinkHandler,
|
||||||
final StreamingService service = NewPipe.getService(serviceId);
|
final Page nextPage) {
|
||||||
final FeedExtractor feedExtractor = service.getFeedExtractor(url);
|
checkServiceId(serviceId);
|
||||||
|
return Single.fromCallable(() ->
|
||||||
if (feedExtractor == null) {
|
ChannelTabInfo.getMoreItems(NewPipe.getService(serviceId),
|
||||||
return null;
|
listLinkHandler, nextPage));
|
||||||
}
|
|
||||||
|
|
||||||
return FeedInfo.getInfo(feedExtractor);
|
|
||||||
});
|
|
||||||
|
|
||||||
return maybeFeedInfo.switchIfEmpty(getChannelInfo(serviceId, url, true));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Single<CommentsInfo> getCommentsInfo(final int serviceId, final String url,
|
public static Single<CommentsInfo> getCommentsInfo(final int serviceId, final String url,
|
||||||
@ -229,7 +223,7 @@ public final class ExtractorHelper {
|
|||||||
load = actualLoadFromNetwork;
|
load = actualLoadFromNetwork;
|
||||||
} else {
|
} else {
|
||||||
load = Maybe.concat(ExtractorHelper.loadFromCache(serviceId, url, infoType),
|
load = Maybe.concat(ExtractorHelper.loadFromCache(serviceId, url, infoType),
|
||||||
actualLoadFromNetwork.toMaybe())
|
actualLoadFromNetwork.toMaybe())
|
||||||
.firstElement() // Take the first valid
|
.firstElement() // Take the first valid
|
||||||
.toSingle();
|
.toSingle();
|
||||||
}
|
}
|
||||||
@ -240,10 +234,10 @@ public final class ExtractorHelper {
|
|||||||
/**
|
/**
|
||||||
* Default implementation uses the {@link InfoCache} to get cached results.
|
* Default implementation uses the {@link InfoCache} to get cached results.
|
||||||
*
|
*
|
||||||
* @param <I> the item type's class that extends {@link Info}
|
* @param <I> the item type's class that extends {@link Info}
|
||||||
* @param serviceId the service to load from
|
* @param serviceId the service to load from
|
||||||
* @param url the URL to load
|
* @param url the URL to load
|
||||||
* @param infoType the {@link InfoItem.InfoType} of the item
|
* @param infoType the {@link InfoItem.InfoType} of the item
|
||||||
* @return a {@link Single} that loads the item
|
* @return a {@link Single} that loads the item
|
||||||
*/
|
*/
|
||||||
private static <I extends Info> Maybe<I> loadFromCache(final int serviceId, final String url,
|
private static <I extends Info> Maybe<I> loadFromCache(final int serviceId, final String url,
|
||||||
@ -274,11 +268,12 @@ public final class ExtractorHelper {
|
|||||||
* Formats the text contained in the meta info list as HTML and puts it into the text view,
|
* Formats the text contained in the meta info list as HTML and puts it into the text view,
|
||||||
* while also making the separator visible. If the list is null or empty, or the user chose not
|
* while also making the separator visible. If the list is null or empty, or the user chose not
|
||||||
* to see meta information, both the text view and the separator are hidden
|
* to see meta information, both the text view and the separator are hidden
|
||||||
* @param metaInfos a list of meta information, can be null or empty
|
*
|
||||||
* @param metaInfoTextView the text view in which to show the formatted HTML
|
* @param metaInfos a list of meta information, can be null or empty
|
||||||
|
* @param metaInfoTextView the text view in which to show the formatted HTML
|
||||||
* @param metaInfoSeparator another view to be shown or hidden accordingly to the text view
|
* @param metaInfoSeparator another view to be shown or hidden accordingly to the text view
|
||||||
* @param disposables disposables created by the method are added here and their lifecycle
|
* @param disposables disposables created by the method are added here and their lifecycle
|
||||||
* should be handled by the calling class
|
* should be handled by the calling class
|
||||||
*/
|
*/
|
||||||
public static void showMetaInfoInTextView(@Nullable final List<MetaInfo> metaInfos,
|
public static void showMetaInfoInTextView(@Nullable final List<MetaInfo> metaInfos,
|
||||||
final TextView metaInfoTextView,
|
final TextView metaInfoTextView,
|
||||||
@ -287,7 +282,7 @@ public final class ExtractorHelper {
|
|||||||
final Context context = metaInfoTextView.getContext();
|
final Context context = metaInfoTextView.getContext();
|
||||||
if (metaInfos == null || metaInfos.isEmpty()
|
if (metaInfos == null || metaInfos.isEmpty()
|
||||||
|| !PreferenceManager.getDefaultSharedPreferences(context).getBoolean(
|
|| !PreferenceManager.getDefaultSharedPreferences(context).getBoolean(
|
||||||
context.getString(R.string.show_meta_info_key), true)) {
|
context.getString(R.string.show_meta_info_key), true)) {
|
||||||
metaInfoTextView.setVisibility(View.GONE);
|
metaInfoTextView.setVisibility(View.GONE);
|
||||||
metaInfoSeparator.setVisibility(View.GONE);
|
metaInfoSeparator.setVisibility(View.GONE);
|
||||||
|
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user