mirror of
https://github.com/TeamNewPipe/NewPipe
synced 2024-11-05 01:26:23 +00: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:
|
||||
label: "Checklist"
|
||||
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
|
||||
- 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
|
||||
|
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) {
|
||||
const promises = [];
|
||||
str.replace(regex, (match, ...args) => {
|
||||
@ -138,7 +138,7 @@ module.exports = async ({github, context}) => {
|
||||
if (shouldModify) {
|
||||
wasMatchModified = true;
|
||||
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`);
|
||||
|
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://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://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>
|
||||
<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="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>
|
||||
|
||||
*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>
|
||||
|
||||
@ -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/donate"><img src="assets/liberapay_donate_button.svg" alt="Donate via Liberapay" height="35px"></a></td>
|
||||
</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>
|
||||
|
||||
## Privacy Policy
|
||||
|
@ -20,8 +20,8 @@ android {
|
||||
resValue "string", "app_name", "NewPipe"
|
||||
minSdk 21
|
||||
targetSdk 33
|
||||
versionCode 994
|
||||
versionName "0.25.2"
|
||||
versionCode 995
|
||||
versionName "0.26.0"
|
||||
|
||||
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 {
|
||||
if (System.properties.containsKey('packageSuffix')) {
|
||||
applicationIdSuffix System.getProperty('packageSuffix')
|
||||
@ -115,7 +112,7 @@ ext {
|
||||
|
||||
icepickVersion = '3.2.0'
|
||||
exoPlayerVersion = '2.18.7'
|
||||
googleAutoServiceVersion = '1.0.1'
|
||||
googleAutoServiceVersion = '1.1.1'
|
||||
groupieVersion = '2.10.1'
|
||||
markwonVersion = '4.6.2'
|
||||
|
||||
@ -192,7 +189,7 @@ sonar {
|
||||
|
||||
dependencies {
|
||||
/** Desugaring **/
|
||||
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.3'
|
||||
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs_nio:2.0.3'
|
||||
|
||||
/** NewPipe libraries **/
|
||||
// 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
|
||||
// This works thanks to JitPack: https://jitpack.io/
|
||||
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'
|
||||
|
||||
/** Checkstyle **/
|
||||
@ -208,7 +205,7 @@ dependencies {
|
||||
ktlint 'com.pinterest:ktlint:0.45.2'
|
||||
|
||||
/** Kotlin **/
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:${kotlin_version}"
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib:${kotlin_version}"
|
||||
|
||||
/** AndroidX **/
|
||||
implementation 'androidx.appcompat:appcompat:1.5.1'
|
||||
@ -232,7 +229,7 @@ dependencies {
|
||||
implementation 'androidx.viewpager2:viewpager2:1.1.0-beta01'
|
||||
implementation "androidx.work:work-runtime-ktx:${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 **/
|
||||
// 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.schabi.newpipe.database.AppDatabase;
|
||||
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.extractor.channel.ChannelInfo;
|
||||
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.TrampolineSchedulerRule;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
|
||||
public class SubscriptionManagerTest {
|
||||
@ -58,7 +52,7 @@ public class SubscriptionManagerTest {
|
||||
final ChannelInfo info = ChannelInfo.getInfo("https://www.youtube.com/c/3blue1brown");
|
||||
final SubscriptionEntity subscription = SubscriptionEntity.from(info);
|
||||
|
||||
manager.insertSubscription(subscription, info);
|
||||
manager.insertSubscription(subscription);
|
||||
final SubscriptionEntity readSubscription = getAssertOneSubscriptionEntity();
|
||||
|
||||
// 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);
|
||||
subscription.setNotificationMode(0);
|
||||
|
||||
manager.insertSubscription(subscription, info);
|
||||
manager.insertSubscription(subscription);
|
||||
manager.updateNotificationMode(subscription.getServiceId(), subscription.getUrl(), 1)
|
||||
.blockingAwait();
|
||||
final SubscriptionEntity anotherSubscription = getAssertOneSubscriptionEntity();
|
||||
@ -85,35 +79,4 @@ public class SubscriptionManagerTest {
|
||||
assertEquals(subscription.getUrl(), anotherSubscription.getUrl());
|
||||
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.internal.runner.junit4.statement.UiThreadStatement
|
||||
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.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.schabi.newpipe.R
|
||||
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.Stream
|
||||
import org.schabi.newpipe.extractor.stream.SubtitlesStream
|
||||
import org.schabi.newpipe.extractor.stream.VideoStream
|
||||
import org.schabi.newpipe.util.StreamItemAdapter.StreamInfoWrapper
|
||||
|
||||
@MediumTest
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
@ -84,7 +90,7 @@ class StreamItemAdapterTest {
|
||||
@Test
|
||||
fun subtitleStreams_noIcon() {
|
||||
val adapter = StreamItemAdapter<SubtitlesStream, Stream>(
|
||||
StreamItemAdapter.StreamSizeWrapper(
|
||||
StreamItemAdapter.StreamInfoWrapper(
|
||||
(0 until 5).map {
|
||||
SubtitlesStream.Builder()
|
||||
.setContent("https://example.com", true)
|
||||
@ -105,7 +111,7 @@ class StreamItemAdapterTest {
|
||||
@Test
|
||||
fun audioStreams_noIcon() {
|
||||
val adapter = StreamItemAdapter<AudioStream, Stream>(
|
||||
StreamItemAdapter.StreamSizeWrapper(
|
||||
StreamItemAdapter.StreamInfoWrapper(
|
||||
(0 until 5).map {
|
||||
AudioStream.Builder()
|
||||
.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
|
||||
* [videoOnly] vararg.
|
||||
*/
|
||||
private fun getVideoStreams(vararg videoOnly: Boolean) =
|
||||
StreamItemAdapter.StreamSizeWrapper(
|
||||
StreamItemAdapter.StreamInfoWrapper(
|
||||
videoOnly.map {
|
||||
VideoStream.Builder()
|
||||
.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
|
||||
* 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 ->
|
||||
val secondaryStreamHelper: SecondaryStreamHelper<T>? = stream?.let {
|
||||
SecondaryStreamHelper(
|
||||
StreamItemAdapter.StreamSizeWrapper(streams, context),
|
||||
StreamItemAdapter.StreamInfoWrapper(streams, context),
|
||||
it
|
||||
)
|
||||
}
|
||||
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.settings.NewPipeSettings;
|
||||
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.StateSaver;
|
||||
import org.schabi.newpipe.util.image.PreferredImageQuality;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InterruptedIOException;
|
||||
@ -99,8 +101,9 @@ public class App extends Application {
|
||||
// Initialize image loader
|
||||
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
|
||||
PicassoHelper.init(this);
|
||||
PicassoHelper.setShouldLoadImages(
|
||||
prefs.getBoolean(getString(R.string.download_thumbnail_key), true));
|
||||
ImageStrategy.setPreferredImageQuality(PreferredImageQuality.fromPreferenceKey(this,
|
||||
prefs.getString(getString(R.string.image_quality_key),
|
||||
getString(R.string.image_quality_default))));
|
||||
PicassoHelper.setIndicatorsEnabled(MainActivity.DEBUG
|
||||
&& prefs.getBoolean(getString(R.string.show_image_indicators_key), false));
|
||||
|
||||
|
@ -80,9 +80,29 @@ public abstract class BaseFragment extends Fragment {
|
||||
// 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) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 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() {
|
||||
}
|
||||
|
||||
@ -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() {
|
||||
return getParentFragment() == null
|
||||
? getFragmentManager()
|
||||
: getParentFragment().getFragmentManager();
|
||||
Fragment current = this;
|
||||
while (current.getParentFragment() != null) {
|
||||
current = current.getParentFragment();
|
||||
}
|
||||
return current.getFragmentManager();
|
||||
}
|
||||
}
|
||||
|
@ -219,14 +219,14 @@ public class MainActivity extends AppCompatActivity {
|
||||
final int currentServiceId = ServiceHelper.getSelectedServiceId(this);
|
||||
final StreamingService service = NewPipe.getService(currentServiceId);
|
||||
|
||||
int kioskId = 0;
|
||||
int kioskMenuItemId = 0;
|
||||
|
||||
for (final String ks : service.getKioskList().getAvailableKiosks()) {
|
||||
drawerLayoutBinding.navigation.getMenu()
|
||||
.add(R.id.menu_tabs_group, kioskId, 0, KioskTranslator
|
||||
.add(R.id.menu_tabs_group, kioskMenuItemId, 0, KioskTranslator
|
||||
.getTranslatedKioskName(ks, this))
|
||||
.setIcon(KioskTranslator.getKioskIcon(ks));
|
||||
kioskId++;
|
||||
kioskMenuItemId++;
|
||||
}
|
||||
|
||||
drawerLayoutBinding.navigation.getMenu()
|
||||
@ -306,20 +306,16 @@ public class MainActivity extends AppCompatActivity {
|
||||
NavigationHelper.openStatisticFragment(getSupportFragmentManager());
|
||||
break;
|
||||
default:
|
||||
final int currentServiceId = ServiceHelper.getSelectedServiceId(this);
|
||||
final StreamingService service = NewPipe.getService(currentServiceId);
|
||||
String serviceName = "";
|
||||
|
||||
int kioskId = 0;
|
||||
for (final String ks : service.getKioskList().getAvailableKiosks()) {
|
||||
if (kioskId == item.getItemId()) {
|
||||
serviceName = ks;
|
||||
final StreamingService currentService = ServiceHelper.getSelectedService(this);
|
||||
int kioskMenuItemId = 0;
|
||||
for (final String kioskId : currentService.getKioskList().getAvailableKiosks()) {
|
||||
if (kioskMenuItemId == item.getItemId()) {
|
||||
NavigationHelper.openKioskFragment(getSupportFragmentManager(),
|
||||
currentService.getServiceId(), kioskId);
|
||||
break;
|
||||
}
|
||||
kioskId++;
|
||||
kioskMenuItemId++;
|
||||
}
|
||||
|
||||
NavigationHelper.openKioskFragment(getSupportFragmentManager(), currentServiceId,
|
||||
serviceName);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
@ -120,13 +120,13 @@ class NewVersionWorker(
|
||||
|
||||
// Parse the json from the response.
|
||||
try {
|
||||
val githubStableObject = JsonParser.`object`()
|
||||
val newpipeVersionInfo = JsonParser.`object`()
|
||||
.from(response.responseBody()).getObject("flavors")
|
||||
.getObject("github").getObject("stable")
|
||||
.getObject("newpipe")
|
||||
|
||||
val versionName = githubStableObject.getString("version")
|
||||
val versionCode = githubStableObject.getInt("version_code")
|
||||
val apkLocationUrl = githubStableObject.getString("apk")
|
||||
val versionName = newpipeVersionInfo.getString("version")
|
||||
val versionCode = newpipeVersionInfo.getInt("version_code")
|
||||
val apkLocationUrl = newpipeVersionInfo.getString("apk")
|
||||
compareAppVersionAndShowNotification(versionName, apkLocationUrl, versionCode)
|
||||
} catch (e: JsonParserException) {
|
||||
// Most likely something is wrong in data received from NEWPIPE_API_URL.
|
||||
|
@ -75,7 +75,7 @@ public final class QueueItemMenuUtil {
|
||||
return true;
|
||||
case R.id.menu_item_share:
|
||||
shareText(context, item.getTitle(), item.getUrl(),
|
||||
item.getThumbnailUrl());
|
||||
item.getThumbnails());
|
||||
return true;
|
||||
case R.id.menu_item_download:
|
||||
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.SingleChoiceDialogViewBinding;
|
||||
import org.schabi.newpipe.download.DownloadDialog;
|
||||
import org.schabi.newpipe.download.LoadingDialog;
|
||||
import org.schabi.newpipe.error.ErrorInfo;
|
||||
import org.schabi.newpipe.error.ErrorUtil;
|
||||
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.SoundCloudGoPlusContentException;
|
||||
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.stream.StreamInfo;
|
||||
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.helper.PlayerHelper;
|
||||
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.PlaylistPlayQueue;
|
||||
import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
|
||||
import org.schabi.newpipe.util.ChannelTabHelper;
|
||||
import org.schabi.newpipe.util.Constants;
|
||||
import org.schabi.newpipe.util.DeviceUtils;
|
||||
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
|
||||
inFlight(false);
|
||||
});
|
||||
inFlight(false)
|
||||
);
|
||||
}
|
||||
|
||||
<T> Single<T> pleaseWait(final Single<T> single) {
|
||||
@ -812,19 +815,24 @@ public class RouterActivity extends AppCompatActivity {
|
||||
@SuppressLint("CheckResult")
|
||||
private void openDownloadDialog(final int currentServiceId, final String currentUrl) {
|
||||
inFlight(true);
|
||||
final LoadingDialog loadingDialog = new LoadingDialog(R.string.loading_metadata_title);
|
||||
loadingDialog.show(getParentFragmentManager(), "loadingDialog");
|
||||
disposables.add(ExtractorHelper.getStreamInfo(currentServiceId, currentUrl, true)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.compose(this::pleaseWait)
|
||||
.subscribe(result ->
|
||||
runOnVisible(ctx -> {
|
||||
loadingDialog.dismiss();
|
||||
final FragmentManager fm = ctx.getSupportFragmentManager();
|
||||
final DownloadDialog downloadDialog = new DownloadDialog(ctx, result);
|
||||
// dismiss listener to be handled by FragmentManager
|
||||
downloadDialog.show(fm, "downloadDialog");
|
||||
}
|
||||
), throwable -> runOnVisible(ctx ->
|
||||
((RouterActivity) ctx).showUnsupportedUrlDialog(currentUrl))));
|
||||
), throwable -> runOnVisible(ctx -> {
|
||||
loadingDialog.dismiss();
|
||||
((RouterActivity) ctx).showUnsupportedUrlDialog(currentUrl);
|
||||
})));
|
||||
}
|
||||
|
||||
private void openAddToPlaylistDialog(final int currentServiceId, final String currentUrl) {
|
||||
@ -1016,7 +1024,16 @@ public class RouterActivity extends AppCompatActivity {
|
||||
}
|
||||
playQueue = new SinglePlayQueue((StreamInfo) info);
|
||||
} 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) {
|
||||
playQueue = new PlaylistPlayQueue((PlaylistInfo) info);
|
||||
} else {
|
||||
|
@ -1,28 +1,38 @@
|
||||
package org.schabi.newpipe.about
|
||||
|
||||
import android.os.Bundle
|
||||
import android.util.Base64
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.webkit.WebView
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.core.os.bundleOf
|
||||
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.Disposable
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import org.schabi.newpipe.BuildConfig
|
||||
import org.schabi.newpipe.R
|
||||
import org.schabi.newpipe.databinding.FragmentLicensesBinding
|
||||
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.
|
||||
*/
|
||||
class LicenseFragment : Fragment() {
|
||||
private lateinit var softwareComponents: Array<SoftwareComponent>
|
||||
private var activeLicense: License? = null
|
||||
private var activeSoftwareComponent: SoftwareComponent? = null
|
||||
private val compositeDisposable = CompositeDisposable()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
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
|
||||
softwareComponents.sortBy { it.name }
|
||||
}
|
||||
@ -39,9 +49,8 @@ class LicenseFragment : Fragment() {
|
||||
): View {
|
||||
val binding = FragmentLicensesBinding.inflate(inflater, container, false)
|
||||
binding.licensesAppReadLicense.setOnClickListener {
|
||||
activeLicense = StandardLicenses.GPL3
|
||||
compositeDisposable.add(
|
||||
showLicense(activity, StandardLicenses.GPL3)
|
||||
showLicense(NEWPIPE_SOFTWARE_COMPONENT)
|
||||
)
|
||||
}
|
||||
for (component in softwareComponents) {
|
||||
@ -57,26 +66,70 @@ class LicenseFragment : Fragment() {
|
||||
val root: View = componentBinding.root
|
||||
root.tag = component
|
||||
root.setOnClickListener {
|
||||
activeLicense = component.license
|
||||
compositeDisposable.add(
|
||||
showLicense(activity, component)
|
||||
showLicense(component)
|
||||
)
|
||||
}
|
||||
binding.licensesSoftwareComponents.addView(root)
|
||||
registerForContextMenu(root)
|
||||
}
|
||||
activeLicense?.let { compositeDisposable.add(showLicense(activity, it)) }
|
||||
activeSoftwareComponent?.let { compositeDisposable.add(showLicense(it)) }
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(savedInstanceState: Bundle) {
|
||||
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 {
|
||||
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 {
|
||||
val fragment = LicenseFragment()
|
||||
fragment.arguments = bundleOf(ARG_COMPONENTS to softwareComponents)
|
||||
|
@ -1,17 +1,8 @@
|
||||
package org.schabi.newpipe.about
|
||||
|
||||
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.util.Localization
|
||||
import org.schabi.newpipe.util.ThemeHelper
|
||||
import org.schabi.newpipe.util.external_communication.ShareUtils
|
||||
import java.io.IOException
|
||||
|
||||
/**
|
||||
@ -20,7 +11,7 @@ import java.io.IOException
|
||||
* @return String which contains a HTML formatted license page
|
||||
* styled according to the context's theme
|
||||
*/
|
||||
private fun getFormattedLicense(context: Context, license: License): String {
|
||||
fun getFormattedLicense(context: Context, license: License): String {
|
||||
try {
|
||||
return context.assets.open(license.filename).bufferedReader().use { it.readText() }
|
||||
// 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
|
||||
* @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 licenseBackgroundColor = getHexRGBColor(
|
||||
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
|
||||
* @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)
|
||||
}
|
||||
|
||||
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 kotlinx.parcelize.Parcelize
|
||||
import java.io.Serializable
|
||||
|
||||
@Parcelize
|
||||
class SoftwareComponent
|
||||
@ -13,4 +14,4 @@ constructor(
|
||||
val link: String,
|
||||
val license: License,
|
||||
val version: String? = null
|
||||
) : Parcelable
|
||||
) : Parcelable, Serializable
|
||||
|
@ -93,18 +93,30 @@ abstract class FeedDAO {
|
||||
uploadDateBefore: OffsetDateTime?
|
||||
): 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(
|
||||
"""
|
||||
DELETE FROM feed WHERE
|
||||
|
||||
feed.stream_id IN (
|
||||
SELECT s.uid FROM streams s
|
||||
|
||||
INNER JOIN feed f
|
||||
ON s.uid = f.stream_id
|
||||
|
||||
WHERE s.upload_date < :offsetDateTime
|
||||
)
|
||||
DELETE FROM feed
|
||||
WHERE feed.stream_id IN (SELECT uid from (
|
||||
SELECT s.uid,
|
||||
(SELECT MAX(upload_date)
|
||||
FROM streams s1
|
||||
INNER JOIN feed f1
|
||||
ON s1.uid = f1.stream_id
|
||||
WHERE f1.subscription_id = f.subscription_id) max_upload_date
|
||||
FROM streams s
|
||||
INNER JOIN feed f
|
||||
ON s.uid = f.stream_id
|
||||
|
||||
WHERE s.upload_date < :offsetDateTime
|
||||
AND s.upload_date <> max_upload_date))
|
||||
"""
|
||||
)
|
||||
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.StreamStateEntity
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem
|
||||
import org.schabi.newpipe.util.image.ImageStrategy
|
||||
|
||||
data class PlaylistStreamEntry(
|
||||
@Embedded
|
||||
@ -28,7 +29,7 @@ data class PlaylistStreamEntry(
|
||||
item.duration = streamEntity.duration
|
||||
item.uploaderName = streamEntity.uploader
|
||||
item.uploaderUrl = streamEntity.uploaderUrl
|
||||
item.thumbnailUrl = streamEntity.thumbnailUrl
|
||||
item.thumbnails = ImageStrategy.dbUrlToImageList(streamEntity.thumbnailUrl)
|
||||
|
||||
return item
|
||||
}
|
||||
|
@ -11,6 +11,7 @@ import androidx.room.PrimaryKey;
|
||||
import org.schabi.newpipe.database.playlist.PlaylistLocalItem;
|
||||
import org.schabi.newpipe.extractor.playlist.PlaylistInfo;
|
||||
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.playlist.model.PlaylistRemoteEntity.REMOTE_PLAYLIST_NAME;
|
||||
@ -69,8 +70,9 @@ public class PlaylistRemoteEntity implements PlaylistLocalItem {
|
||||
@Ignore
|
||||
public PlaylistRemoteEntity(final PlaylistInfo info) {
|
||||
this(info.getServiceId(), info.getName(), info.getUrl(),
|
||||
info.getThumbnailUrl() == null
|
||||
? info.getUploaderAvatarUrl() : info.getThumbnailUrl(),
|
||||
// use uploader avatar when no thumbnail is available
|
||||
ImageStrategy.imageListToDbUrl(info.getThumbnails().isEmpty()
|
||||
? info.getUploaderAvatars() : info.getThumbnails()),
|
||||
info.getUploaderName(), info.getStreamCount());
|
||||
}
|
||||
|
||||
@ -84,7 +86,10 @@ public class PlaylistRemoteEntity implements PlaylistLocalItem {
|
||||
&& getStreamCount() == info.getStreamCount()
|
||||
&& TextUtils.equals(getName(), info.getName())
|
||||
&& 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());
|
||||
}
|
||||
|
||||
|
@ -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.StreamStateEntity.STREAM_PROGRESS_MILLIS
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem
|
||||
import org.schabi.newpipe.util.image.ImageStrategy
|
||||
import java.time.OffsetDateTime
|
||||
|
||||
class StreamStatisticsEntry(
|
||||
@ -30,7 +31,7 @@ class StreamStatisticsEntry(
|
||||
item.duration = streamEntity.duration
|
||||
item.uploaderName = streamEntity.uploader
|
||||
item.uploaderUrl = streamEntity.uploaderUrl
|
||||
item.thumbnailUrl = streamEntity.thumbnailUrl
|
||||
item.thumbnails = ImageStrategy.dbUrlToImageList(streamEntity.thumbnailUrl)
|
||||
|
||||
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.StreamType
|
||||
import org.schabi.newpipe.player.playqueue.PlayQueueItem
|
||||
import org.schabi.newpipe.util.image.ImageStrategy
|
||||
import java.io.Serializable
|
||||
import java.time.OffsetDateTime
|
||||
|
||||
@ -67,7 +68,8 @@ data class StreamEntity(
|
||||
constructor(item: StreamInfoItem) : this(
|
||||
serviceId = item.serviceId, url = item.url, title = item.name,
|
||||
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(),
|
||||
isUploadDateApproximation = item.uploadDate?.isApproximation
|
||||
)
|
||||
@ -76,7 +78,8 @@ data class StreamEntity(
|
||||
constructor(info: StreamInfo) : this(
|
||||
serviceId = info.serviceId, url = info.url, title = info.name,
|
||||
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(),
|
||||
isUploadDateApproximation = info.uploadDate?.isApproximation
|
||||
)
|
||||
@ -85,7 +88,8 @@ data class StreamEntity(
|
||||
constructor(item: PlayQueueItem) : this(
|
||||
serviceId = item.serviceId, url = item.url, title = item.title,
|
||||
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 {
|
||||
@ -93,7 +97,7 @@ data class StreamEntity(
|
||||
item.duration = duration
|
||||
item.uploaderName = uploader
|
||||
item.uploaderUrl = uploaderUrl
|
||||
item.thumbnailUrl = thumbnailUrl
|
||||
item.thumbnails = ImageStrategy.dbUrlToImageList(thumbnailUrl)
|
||||
|
||||
if (viewCount != null) item.viewCount = viewCount as Long
|
||||
item.textualUploadDate = textualUploadDate
|
||||
|
@ -10,6 +10,7 @@ import androidx.room.PrimaryKey;
|
||||
import org.schabi.newpipe.extractor.channel.ChannelInfo;
|
||||
import org.schabi.newpipe.extractor.channel.ChannelInfoItem;
|
||||
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_TABLE;
|
||||
@ -57,8 +58,8 @@ public class SubscriptionEntity {
|
||||
final SubscriptionEntity result = new SubscriptionEntity();
|
||||
result.setServiceId(info.getServiceId());
|
||||
result.setUrl(info.getUrl());
|
||||
result.setData(info.getName(), info.getAvatarUrl(), info.getDescription(),
|
||||
info.getSubscriberCount());
|
||||
result.setData(info.getName(), ImageStrategy.imageListToDbUrl(info.getAvatars()),
|
||||
info.getDescription(), info.getSubscriberCount());
|
||||
return result;
|
||||
}
|
||||
|
||||
@ -138,7 +139,7 @@ public class SubscriptionEntity {
|
||||
@Ignore
|
||||
public ChannelInfoItem toChannelInfoItem() {
|
||||
final ChannelInfoItem item = new ChannelInfoItem(getServiceId(), getUrl(), getName());
|
||||
item.setThumbnailUrl(getAvatarUrl());
|
||||
item.setThumbnails(ImageStrategy.dbUrlToImageList(getAvatarUrl()));
|
||||
item.setSubscriberCount(getSubscriberCount());
|
||||
item.setDescription(getDescription());
|
||||
return item;
|
||||
|
@ -67,7 +67,7 @@ import org.schabi.newpipe.util.PermissionHelper;
|
||||
import org.schabi.newpipe.util.SecondaryStreamHelper;
|
||||
import org.schabi.newpipe.util.SimpleOnSeekBarChangeListener;
|
||||
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.AudioTracksWrapper;
|
||||
import org.schabi.newpipe.util.ThemeHelper;
|
||||
@ -97,9 +97,9 @@ public class DownloadDialog extends DialogFragment
|
||||
@State
|
||||
StreamInfo currentInfo;
|
||||
@State
|
||||
StreamSizeWrapper<VideoStream> wrappedVideoStreams;
|
||||
StreamInfoWrapper<VideoStream> wrappedVideoStreams;
|
||||
@State
|
||||
StreamSizeWrapper<SubtitlesStream> wrappedSubtitleStreams;
|
||||
StreamInfoWrapper<SubtitlesStream> wrappedSubtitleStreams;
|
||||
@State
|
||||
AudioTracksWrapper wrappedAudioTracks;
|
||||
@State
|
||||
@ -187,8 +187,8 @@ public class DownloadDialog extends DialogFragment
|
||||
wrappedAudioTracks.size() > 1
|
||||
);
|
||||
|
||||
this.wrappedVideoStreams = new StreamSizeWrapper<>(videoStreams, context);
|
||||
this.wrappedSubtitleStreams = new StreamSizeWrapper<>(
|
||||
this.wrappedVideoStreams = new StreamInfoWrapper<>(videoStreams, context);
|
||||
this.wrappedSubtitleStreams = new StreamInfoWrapper<>(
|
||||
getStreamsOfSpecifiedDelivery(info.getSubtitles(), PROGRESSIVE_HTTP), context);
|
||||
|
||||
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.
|
||||
*/
|
||||
private void updateSecondaryStreams() {
|
||||
final StreamSizeWrapper<AudioStream> audioStreams = getWrappedAudioStreams();
|
||||
final StreamInfoWrapper<AudioStream> audioStreams = getWrappedAudioStreams();
|
||||
final var secondaryStreams = new SparseArrayCompat<SecondaryStreamHelper<AudioStream>>(4);
|
||||
final List<VideoStream> videoStreams = wrappedVideoStreams.getStreamsList();
|
||||
wrappedVideoStreams.resetSizes();
|
||||
wrappedVideoStreams.resetInfo();
|
||||
|
||||
for (int i = 0; i < videoStreams.size(); i++) {
|
||||
if (!videoStreams.get(i).isVideoOnly()) {
|
||||
continue;
|
||||
}
|
||||
final AudioStream audioStream = SecondaryStreamHelper
|
||||
.getAudioStreamFor(audioStreams.getStreamsList(), videoStreams.get(i));
|
||||
final AudioStream audioStream = SecondaryStreamHelper.getAudioStreamFor(
|
||||
context, audioStreams.getStreamsList(), videoStreams.get(i));
|
||||
|
||||
if (audioStream != null) {
|
||||
secondaryStreams.append(i, new SecondaryStreamHelper<>(audioStreams, audioStream));
|
||||
@ -396,7 +396,7 @@ public class DownloadDialog extends DialogFragment
|
||||
|
||||
private void fetchStreamsSize() {
|
||||
disposables.clear();
|
||||
disposables.add(StreamSizeWrapper.fetchSizeForWrapper(wrappedVideoStreams)
|
||||
disposables.add(StreamInfoWrapper.fetchMoreInfoForWrapper(wrappedVideoStreams)
|
||||
.subscribe(result -> {
|
||||
if (dialogBinding.videoAudioGroup.getCheckedRadioButtonId()
|
||||
== R.id.video_button) {
|
||||
@ -406,7 +406,7 @@ public class DownloadDialog extends DialogFragment
|
||||
new ErrorInfo(throwable, UserAction.DOWNLOAD_OPEN_DIALOG,
|
||||
"Downloading video stream size",
|
||||
currentInfo.getServiceId()))));
|
||||
disposables.add(StreamSizeWrapper.fetchSizeForWrapper(getWrappedAudioStreams())
|
||||
disposables.add(StreamInfoWrapper.fetchMoreInfoForWrapper(getWrappedAudioStreams())
|
||||
.subscribe(result -> {
|
||||
if (dialogBinding.videoAudioGroup.getCheckedRadioButtonId()
|
||||
== R.id.audio_button) {
|
||||
@ -416,7 +416,7 @@ public class DownloadDialog extends DialogFragment
|
||||
new ErrorInfo(throwable, UserAction.DOWNLOAD_OPEN_DIALOG,
|
||||
"Downloading audio stream size",
|
||||
currentInfo.getServiceId()))));
|
||||
disposables.add(StreamSizeWrapper.fetchSizeForWrapper(wrappedSubtitleStreams)
|
||||
disposables.add(StreamInfoWrapper.fetchMoreInfoForWrapper(wrappedSubtitleStreams)
|
||||
.subscribe(result -> {
|
||||
if (dialogBinding.videoAudioGroup.getCheckedRadioButtonId()
|
||||
== R.id.subtitle_button) {
|
||||
@ -724,9 +724,9 @@ public class DownloadDialog extends DialogFragment
|
||||
dialogBinding.subtitleButton.setEnabled(enabled);
|
||||
}
|
||||
|
||||
private StreamSizeWrapper<AudioStream> getWrappedAudioStreams() {
|
||||
private StreamInfoWrapper<AudioStream> getWrappedAudioStreams() {
|
||||
if (selectedAudioTrackIndex < 0 || selectedAudioTrackIndex > wrappedAudioTracks.size()) {
|
||||
return StreamSizeWrapper.empty();
|
||||
return StreamInfoWrapper.empty();
|
||||
}
|
||||
return wrappedAudioTracks.getTracksList().get(selectedAudioTrackIndex);
|
||||
}
|
||||
@ -766,7 +766,7 @@ public class DownloadDialog extends DialogFragment
|
||||
}
|
||||
|
||||
private void showFailedDialog(@StringRes final int msg) {
|
||||
assureCorrectAppLanguage(getContext());
|
||||
assureCorrectAppLanguage(requireContext());
|
||||
new AlertDialog.Builder(context)
|
||||
.setTitle(R.string.general_error)
|
||||
.setMessage(msg)
|
||||
@ -799,7 +799,7 @@ public class DownloadDialog extends DialogFragment
|
||||
filenameTmp += "opus";
|
||||
} else if (format != null) {
|
||||
mimeTmp = format.mimeType;
|
||||
filenameTmp += format.suffix;
|
||||
filenameTmp += format.getSuffix();
|
||||
}
|
||||
break;
|
||||
case R.id.video_button:
|
||||
@ -808,7 +808,7 @@ public class DownloadDialog extends DialogFragment
|
||||
format = videoStreamsAdapter.getItem(selectedVideoIndex).getFormat();
|
||||
if (format != null) {
|
||||
mimeTmp = format.mimeType;
|
||||
filenameTmp += format.suffix;
|
||||
filenameTmp += format.getSuffix();
|
||||
}
|
||||
break;
|
||||
case R.id.subtitle_button:
|
||||
@ -820,9 +820,9 @@ public class DownloadDialog extends DialogFragment
|
||||
}
|
||||
|
||||
if (format == MediaFormat.TTML) {
|
||||
filenameTmp += MediaFormat.SRT.suffix;
|
||||
filenameTmp += MediaFormat.SRT.getSuffix();
|
||||
} else if (format != null) {
|
||||
filenameTmp += format.suffix;
|
||||
filenameTmp += format.getSuffix();
|
||||
}
|
||||
break;
|
||||
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.ContentNotSupportedException
|
||||
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.util.ServiceHelper
|
||||
|
||||
@ -96,7 +95,6 @@ class ErrorInfo(
|
||||
throwable is ContentNotAvailableException -> R.string.content_not_available
|
||||
throwable != null && throwable.isNetworkRelated -> R.string.network_error
|
||||
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 ExoPlaybackException -> {
|
||||
when (throwable.type) {
|
||||
|
@ -1,12 +1,16 @@
|
||||
package org.schabi.newpipe.fragments;
|
||||
|
||||
import static org.schabi.newpipe.ktx.ViewUtils.animate;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
import android.widget.ProgressBar;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.StringRes;
|
||||
import androidx.fragment.app.Fragment;
|
||||
|
||||
import org.schabi.newpipe.BaseFragment;
|
||||
@ -20,15 +24,15 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
import icepick.State;
|
||||
|
||||
import static org.schabi.newpipe.ktx.ViewUtils.animate;
|
||||
|
||||
public abstract class BaseStateFragment<I> extends BaseFragment implements ViewContract<I> {
|
||||
@State
|
||||
protected AtomicBoolean wasLoading = new AtomicBoolean();
|
||||
protected AtomicBoolean isLoading = new AtomicBoolean();
|
||||
|
||||
@Nullable
|
||||
private View emptyStateView;
|
||||
protected View emptyStateView;
|
||||
@Nullable
|
||||
protected TextView emptyStateMessageView;
|
||||
@Nullable
|
||||
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) {
|
||||
super.initViews(rootView, savedInstanceState);
|
||||
emptyStateView = rootView.findViewById(R.id.empty_state_view);
|
||||
emptyStateMessageView = rootView.findViewById(R.id.empty_state_message);
|
||||
loadingProgressBar = rootView.findViewById(R.id.loading_progress_bar);
|
||||
errorPanelHelper = new ErrorPanelHelper(this, rootView, this::onRetryButtonClicked);
|
||||
}
|
||||
@ -75,6 +80,8 @@ public abstract class BaseStateFragment<I> extends BaseFragment implements ViewC
|
||||
if (errorPanelHelper != null) {
|
||||
errorPanelHelper.dispose();
|
||||
}
|
||||
emptyStateView = null;
|
||||
emptyStateMessageView = null;
|
||||
}
|
||||
|
||||
protected void onRetryButtonClicked() {
|
||||
@ -189,6 +196,12 @@ public abstract class BaseStateFragment<I> extends BaseFragment implements ViewC
|
||||
errorPanelHelper.showTextError(errorString);
|
||||
}
|
||||
|
||||
protected void setEmptyStateMessage(@StringRes final int text) {
|
||||
if (emptyStateMessageView != null) {
|
||||
emptyStateMessageView.setText(text);
|
||||
}
|
||||
}
|
||||
|
||||
public final void hideErrorPanel() {
|
||||
errorPanelHelper.hide();
|
||||
lastPanelError = null;
|
||||
|
@ -38,6 +38,7 @@ import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.databinding.FragmentMainBinding;
|
||||
import org.schabi.newpipe.error.ErrorUtil;
|
||||
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.TabsManager;
|
||||
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
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
@ -187,7 +194,6 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte
|
||||
}
|
||||
|
||||
binding.pager.setAdapter(null);
|
||||
binding.pager.setOffscreenPageLimit(tabsList.size());
|
||||
binding.pager.setAdapter(pagerAdapter);
|
||||
|
||||
updateTabsIconAndDescription();
|
||||
@ -211,6 +217,12 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte
|
||||
setTitle(tabsList.get(tabPosition).getTabName(requireContext()));
|
||||
}
|
||||
|
||||
public void commitPlaylistTabs() {
|
||||
pagerAdapter.getLocalPlaylistFragments()
|
||||
.stream()
|
||||
.forEach(LocalPlaylistFragment::commitChanges);
|
||||
}
|
||||
|
||||
private void updateTabLayoutPosition() {
|
||||
final ScrollableTabLayout tabLayout = binding.mainTabLayout;
|
||||
final ViewPager viewPager = binding.pager;
|
||||
@ -262,10 +274,18 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte
|
||||
updateTitleForTab(tab.getPosition());
|
||||
}
|
||||
|
||||
private static final class SelectedTabsPagerAdapter
|
||||
public static final class SelectedTabsPagerAdapter
|
||||
extends FragmentStatePagerAdapterMenuWorkaround {
|
||||
private final Context context;
|
||||
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,
|
||||
final FragmentManager fragmentManager,
|
||||
@ -292,9 +312,17 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte
|
||||
((BaseFragment) fragment).useAsFrontPage(true);
|
||||
}
|
||||
|
||||
if (fragment instanceof LocalPlaylistFragment) {
|
||||
localPlaylistFragments.add((LocalPlaylistFragment) fragment);
|
||||
}
|
||||
|
||||
return fragment;
|
||||
}
|
||||
|
||||
public List<LocalPlaylistFragment> getLocalPlaylistFragments() {
|
||||
return localPlaylistFragments;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemPosition(@NonNull final Object object) {
|
||||
// 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;
|
||||
|
||||
import static android.text.TextUtils.isEmpty;
|
||||
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.text.TextLinkifier.SET_LINK_MOVEMENT_METHOD;
|
||||
|
||||
import android.os.Bundle;
|
||||
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.StreamingService;
|
||||
import org.schabi.newpipe.extractor.stream.Description;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||
import org.schabi.newpipe.util.Localization;
|
||||
import org.schabi.newpipe.util.NavigationHelper;
|
||||
import org.schabi.newpipe.util.external_communication.ShareUtils;
|
||||
import org.schabi.newpipe.util.text.TextLinkifier;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import icepick.State;
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
||||
|
||||
public class DescriptionFragment extends BaseFragment {
|
||||
public class DescriptionFragment extends BaseDescriptionFragment {
|
||||
|
||||
@State
|
||||
StreamInfo streamInfo = null;
|
||||
final CompositeDisposable descriptionDisposables = new CompositeDisposable();
|
||||
FragmentDescriptionBinding binding;
|
||||
|
||||
public DescriptionFragment() {
|
||||
}
|
||||
@ -49,86 +32,64 @@ public class DescriptionFragment extends BaseFragment {
|
||||
this.streamInfo = streamInfo;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public View onCreateView(@NonNull final LayoutInflater inflater,
|
||||
@Nullable final ViewGroup container,
|
||||
@Nullable final Bundle savedInstanceState) {
|
||||
binding = FragmentDescriptionBinding.inflate(inflater, container, false);
|
||||
if (streamInfo != null) {
|
||||
setupUploadDate();
|
||||
setupDescription();
|
||||
setupMetadata(inflater, binding.detailMetadataLayout);
|
||||
protected Description getDescription() {
|
||||
if (streamInfo == null) {
|
||||
return null;
|
||||
}
|
||||
return binding.getRoot();
|
||||
return streamInfo.getDescription();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
protected StreamingService getService() {
|
||||
if (streamInfo == null) {
|
||||
return null;
|
||||
}
|
||||
return streamInfo.getService();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
descriptionDisposables.clear();
|
||||
super.onDestroy();
|
||||
protected int getServiceId() {
|
||||
if (streamInfo == null) {
|
||||
return -1;
|
||||
}
|
||||
return streamInfo.getServiceId();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
protected String getStreamUrl() {
|
||||
if (streamInfo == null) {
|
||||
return null;
|
||||
}
|
||||
return streamInfo.getUrl();
|
||||
}
|
||||
|
||||
private void setupUploadDate() {
|
||||
if (streamInfo.getUploadDate() != null) {
|
||||
@Nullable
|
||||
@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
|
||||
.localizeUploadDate(activity, streamInfo.getUploadDate().offsetDateTime()));
|
||||
} else {
|
||||
binding.detailUploadDateView.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
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);
|
||||
if (streamInfo == null) {
|
||||
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,
|
||||
streamInfo.getCategory());
|
||||
|
||||
@ -151,69 +112,13 @@ public class DescriptionFragment extends BaseFragment {
|
||||
streamInfo.getSupportInfo());
|
||||
addMetadataItem(inflater, layout, true, R.string.metadata_host,
|
||||
streamInfo.getHost());
|
||||
addMetadataItem(inflater, layout, true, R.string.metadata_thumbnail_url,
|
||||
streamInfo.getThumbnailUrl());
|
||||
|
||||
addTagsMetadataItem(inflater, layout);
|
||||
}
|
||||
|
||||
private 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 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;
|
||||
addImagesMetadataItem(inflater, layout, R.string.metadata_thumbnails,
|
||||
streamInfo.getThumbnails());
|
||||
addImagesMetadataItem(inflater, layout, R.string.metadata_uploader_avatars,
|
||||
streamInfo.getUploaderAvatars());
|
||||
addImagesMetadataItem(inflater, layout, R.string.metadata_subchannel_avatars,
|
||||
streamInfo.getSubChannelAvatars());
|
||||
}
|
||||
|
||||
private void addPrivacyMetadataItem(final LayoutInflater inflater, final LinearLayout layout) {
|
||||
|
@ -24,7 +24,6 @@ import android.content.pm.ActivityInfo;
|
||||
import android.database.ContentObserver;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
@ -54,6 +53,7 @@ import androidx.appcompat.content.res.AppCompatResources;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.coordinatorlayout.widget.CoordinatorLayout;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.preference.PreferenceManager;
|
||||
|
||||
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.ReCaptchaActivity;
|
||||
import org.schabi.newpipe.error.UserAction;
|
||||
import org.schabi.newpipe.extractor.Image;
|
||||
import org.schabi.newpipe.extractor.InfoItem;
|
||||
import org.schabi.newpipe.extractor.NewPipe;
|
||||
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.BaseStateFragment;
|
||||
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.videos.RelatedItemsFragment;
|
||||
import org.schabi.newpipe.ktx.AnimationType;
|
||||
import org.schabi.newpipe.local.dialog.PlaylistDialog;
|
||||
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.PlayerService;
|
||||
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.NavigationHelper;
|
||||
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.ThemeHelper;
|
||||
import org.schabi.newpipe.util.external_communication.KoreUtils;
|
||||
import org.schabi.newpipe.util.external_communication.ShareUtils;
|
||||
import org.schabi.newpipe.util.PlayButtonHelper;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
@ -470,10 +474,23 @@ public final class VideoDetailFragment
|
||||
|
||||
binding.detailControlsBackground.setOnClickListener(v -> openBackgroundPlayer(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(),
|
||||
List.of(new StreamEntity(info)),
|
||||
dialog -> dialog.show(getParentFragmentManager(), TAG)))));
|
||||
dialog -> dialog.show(getParentFragmentManager(), TAG)));
|
||||
}
|
||||
}));
|
||||
binding.detailControlsDownload.setOnClickListener(v -> {
|
||||
if (PermissionHelper.checkStoragePermissions(activity,
|
||||
PermissionHelper.DOWNLOAD_DIALOG_REQUEST_CODE)) {
|
||||
@ -482,7 +499,7 @@ public final class VideoDetailFragment
|
||||
});
|
||||
binding.detailControlsShare.setOnClickListener(makeOnClickListener(info ->
|
||||
ShareUtils.shareText(requireContext(), info.getName(), info.getUrl(),
|
||||
info.getThumbnailUrl())));
|
||||
info.getThumbnails())));
|
||||
binding.detailControlsOpenInBrowser.setOnClickListener(makeOnClickListener(info ->
|
||||
ShareUtils.openUrlInBrowser(requireContext(), info.getUrl())));
|
||||
binding.detailControlsPlayWithKodi.setOnClickListener(makeOnClickListener(info ->
|
||||
@ -535,9 +552,11 @@ public final class VideoDetailFragment
|
||||
}));
|
||||
|
||||
binding.detailControlsBackground.setOnLongClickListener(makeOnLongClickListener(info ->
|
||||
openBackgroundPlayer(true)));
|
||||
openBackgroundPlayer(true)
|
||||
));
|
||||
binding.detailControlsPopup.setOnLongClickListener(makeOnLongClickListener(info ->
|
||||
openPopupPlayer(true)));
|
||||
openPopupPlayer(true)
|
||||
));
|
||||
binding.detailControlsDownload.setOnLongClickListener(makeOnLongClickListener(info ->
|
||||
NavigationHelper.openDownloads(activity)));
|
||||
|
||||
@ -620,8 +639,7 @@ public final class VideoDetailFragment
|
||||
|
||||
final View.OnTouchListener controlsTouchListener = (view, motionEvent) -> {
|
||||
if (motionEvent.getAction() == MotionEvent.ACTION_DOWN
|
||||
&& PreferenceManager.getDefaultSharedPreferences(activity)
|
||||
.getBoolean(getString(R.string.show_hold_to_append_key), true)) {
|
||||
&& PlayButtonHelper.shouldShowHoldToAppendTip(activity)) {
|
||||
|
||||
animate(binding.touchAppendDetail, true, 250, AnimationType.ALPHA, 0, () ->
|
||||
animate(binding.touchAppendDetail, false, 1500, AnimationType.ALPHA, 1000));
|
||||
@ -721,7 +739,7 @@ public final class VideoDetailFragment
|
||||
final boolean isPlayerStopped = !isPlayerAvailable() || player.isStopped();
|
||||
if (playQueueItem != null && isPlayerStopped) {
|
||||
updateOverlayData(playQueueItem.getTitle(),
|
||||
playQueueItem.getUploader(), playQueueItem.getThumbnailUrl());
|
||||
playQueueItem.getUploader(), playQueueItem.getThumbnails());
|
||||
}
|
||||
}
|
||||
|
||||
@ -1463,11 +1481,6 @@ public final class VideoDetailFragment
|
||||
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.getStreamType().equals(StreamType.AUDIO_LIVE_STREAM)) {
|
||||
binding.detailViewCountView.setText(Localization.listeningCount(activity,
|
||||
@ -1534,13 +1547,13 @@ public final class VideoDetailFragment
|
||||
binding.detailSecondaryControlPanel.setVisibility(View.GONE);
|
||||
|
||||
checkUpdateProgressInfo(info);
|
||||
PicassoHelper.loadDetailsThumbnail(info.getThumbnailUrl()).tag(PICASSO_VIDEO_DETAILS_TAG)
|
||||
PicassoHelper.loadDetailsThumbnail(info.getThumbnails()).tag(PICASSO_VIDEO_DETAILS_TAG)
|
||||
.into(binding.detailThumbnailImageView);
|
||||
showMetaInfoInTextView(info.getMetaInfo(), binding.detailMetaInfoTextView,
|
||||
binding.detailMetaInfoSeparator, disposables);
|
||||
|
||||
if (!isPlayerAvailable() || player.isStopped()) {
|
||||
updateOverlayData(info.getName(), info.getUploaderName(), info.getThumbnailUrl());
|
||||
updateOverlayData(info.getName(), info.getUploaderName(), info.getThumbnails());
|
||||
}
|
||||
|
||||
if (!info.getErrors().isEmpty()) {
|
||||
@ -1585,7 +1598,7 @@ public final class VideoDetailFragment
|
||||
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);
|
||||
binding.detailSubChannelThumbnailView.setVisibility(View.VISIBLE);
|
||||
binding.detailUploaderThumbnailView.setVisibility(View.GONE);
|
||||
@ -1617,10 +1630,10 @@ public final class VideoDetailFragment
|
||||
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);
|
||||
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);
|
||||
binding.detailUploaderThumbnailView.setVisibility(View.VISIBLE);
|
||||
}
|
||||
@ -1795,7 +1808,7 @@ public final class VideoDetailFragment
|
||||
return;
|
||||
}
|
||||
|
||||
updateOverlayData(info.getName(), info.getUploaderName(), info.getThumbnailUrl());
|
||||
updateOverlayData(info.getName(), info.getUploaderName(), info.getThumbnails());
|
||||
if (currentInfo != null && info.getUrl().equals(currentInfo.getUrl())) {
|
||||
return;
|
||||
}
|
||||
@ -1824,7 +1837,7 @@ public final class VideoDetailFragment
|
||||
if (currentInfo != null) {
|
||||
updateOverlayData(currentInfo.getName(),
|
||||
currentInfo.getUploaderName(),
|
||||
currentInfo.getThumbnailUrl());
|
||||
currentInfo.getThumbnails());
|
||||
}
|
||||
updateOverlayPlayQueueButtonVisibility();
|
||||
}
|
||||
@ -2189,7 +2202,7 @@ public final class VideoDetailFragment
|
||||
playerHolder.stopService();
|
||||
setInitialData(0, null, "", 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,
|
||||
@Nullable final String uploader,
|
||||
@Nullable final String thumbnailUrl) {
|
||||
@NonNull final List<Image> thumbnails) {
|
||||
binding.overlayTitleTextView.setText(isEmpty(overlayTitle) ? "" : overlayTitle);
|
||||
binding.overlayChannelTextView.setText(isEmpty(uploader) ? "" : uploader);
|
||||
binding.overlayThumbnail.setImageDrawable(null);
|
||||
PicassoHelper.loadDetailsThumbnail(thumbnailUrl).tag(PICASSO_VIDEO_DETAILS_TAG)
|
||||
PicassoHelper.loadDetailsThumbnail(thumbnails).tag(PICASSO_VIDEO_DETAILS_TAG)
|
||||
.into(binding.overlayThumbnail);
|
||||
}
|
||||
|
||||
|
@ -1,5 +1,7 @@
|
||||
package org.schabi.newpipe.fragments.list;
|
||||
|
||||
import static org.schabi.newpipe.extractor.ServiceList.SoundCloud;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
@ -7,13 +9,13 @@ import android.view.View;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.error.ErrorInfo;
|
||||
import org.schabi.newpipe.error.UserAction;
|
||||
import org.schabi.newpipe.extractor.InfoItem;
|
||||
import org.schabi.newpipe.extractor.ListExtractor;
|
||||
import org.schabi.newpipe.extractor.ListInfo;
|
||||
import org.schabi.newpipe.extractor.Page;
|
||||
import org.schabi.newpipe.extractor.channel.ChannelInfo;
|
||||
import org.schabi.newpipe.extractor.exceptions.ContentNotSupportedException;
|
||||
import org.schabi.newpipe.util.Constants;
|
||||
import org.schabi.newpipe.views.NewPipeRecyclerView;
|
||||
@ -231,11 +233,7 @@ public abstract class BaseListInfoFragment<I extends InfoItem, L extends ListInf
|
||||
showListFooter(hasMoreItems());
|
||||
} else {
|
||||
infoListAdapter.clearStreamItemList();
|
||||
// showEmptyState should be called only if there is no item as
|
||||
// well as no header in infoListAdapter
|
||||
if (!(result instanceof ChannelInfo && infoListAdapter.getItemCount() == 1)) {
|
||||
showEmptyState();
|
||||
}
|
||||
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
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
@ -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 android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.graphics.Color;
|
||||
import android.os.Bundle;
|
||||
import android.text.TextUtils;
|
||||
@ -16,51 +17,50 @@ import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.Button;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
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.tabs.TabLayout;
|
||||
import com.jakewharton.rxbinding4.view.RxView;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.database.subscription.NotificationMode;
|
||||
import org.schabi.newpipe.database.subscription.SubscriptionEntity;
|
||||
import org.schabi.newpipe.databinding.ChannelHeaderBinding;
|
||||
import org.schabi.newpipe.databinding.FragmentChannelBinding;
|
||||
import org.schabi.newpipe.databinding.PlaylistControlBinding;
|
||||
import org.schabi.newpipe.error.ErrorInfo;
|
||||
import org.schabi.newpipe.error.ErrorUtil;
|
||||
import org.schabi.newpipe.error.UserAction;
|
||||
import org.schabi.newpipe.extractor.ListExtractor;
|
||||
import org.schabi.newpipe.extractor.channel.ChannelInfo;
|
||||
import org.schabi.newpipe.extractor.exceptions.ContentNotSupportedException;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||
import org.schabi.newpipe.fragments.list.BaseListInfoFragment;
|
||||
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler;
|
||||
import org.schabi.newpipe.fragments.BaseStateFragment;
|
||||
import org.schabi.newpipe.fragments.detail.TabAdapter;
|
||||
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.player.PlayerType;
|
||||
import org.schabi.newpipe.player.playqueue.ChannelPlayQueue;
|
||||
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
||||
import org.schabi.newpipe.local.subscription.SubscriptionManager;
|
||||
import org.schabi.newpipe.util.ChannelTabHelper;
|
||||
import org.schabi.newpipe.util.Constants;
|
||||
import org.schabi.newpipe.util.ExtractorHelper;
|
||||
import org.schabi.newpipe.util.Localization;
|
||||
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.external_communication.ShareUtils;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Queue;
|
||||
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.core.Observable;
|
||||
import io.reactivex.rxjava3.core.Single;
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
||||
import io.reactivex.rxjava3.disposables.Disposable;
|
||||
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.schedulers.Schedulers;
|
||||
|
||||
public class ChannelFragment extends BaseListInfoFragment<StreamInfoItem, ChannelInfo>
|
||||
implements View.OnClickListener {
|
||||
public class ChannelFragment extends BaseStateFragment<ChannelInfo>
|
||||
implements StateSaver.WriteRead {
|
||||
|
||||
private static final int BUTTON_DEBOUNCE_INTERVAL = 100;
|
||||
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 Disposable subscribeButtonMonitor;
|
||||
|
||||
private SubscriptionManager subscriptionManager;
|
||||
private int lastTab;
|
||||
private boolean channelContentNotSupported = false;
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Views
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
private SubscriptionManager subscriptionManager;
|
||||
|
||||
private FragmentChannelBinding channelBinding;
|
||||
private ChannelHeaderBinding headerBinding;
|
||||
private PlaylistControlBinding playlistControlBinding;
|
||||
private FragmentChannelBinding binding;
|
||||
private TabAdapter tabAdapter;
|
||||
|
||||
private MenuItem menuRssButton;
|
||||
private MenuItem menuNotifyButton;
|
||||
private SubscriptionEntity channelSubscription;
|
||||
|
||||
public static ChannelFragment getInstance(final int serviceId, final String url,
|
||||
final String name) {
|
||||
@ -99,22 +107,23 @@ public class ChannelFragment extends BaseListInfoFragment<StreamInfoItem, Channe
|
||||
return instance;
|
||||
}
|
||||
|
||||
public ChannelFragment() {
|
||||
super(UserAction.REQUESTED_CHANNEL);
|
||||
private void setInitialData(final int sid, final String u, final String title) {
|
||||
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
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public void onCreate(final Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setHasOptionsMenu(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAttach(@NonNull final Context context) {
|
||||
super.onAttach(context);
|
||||
@ -125,49 +134,58 @@ public class ChannelFragment extends BaseListInfoFragment<StreamInfoItem, Channe
|
||||
public View onCreateView(@NonNull final LayoutInflater inflater,
|
||||
@Nullable final ViewGroup container,
|
||||
@Nullable final Bundle savedInstanceState) {
|
||||
return inflater.inflate(R.layout.fragment_channel, container, false);
|
||||
binding = FragmentChannelBinding.inflate(inflater, container, false);
|
||||
return binding.getRoot();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(@NonNull final View rootView, final Bundle savedInstanceState) {
|
||||
super.onViewCreated(rootView, savedInstanceState);
|
||||
channelBinding = FragmentChannelBinding.bind(rootView);
|
||||
showContentNotSupportedIfNeeded();
|
||||
}
|
||||
@Override // called from onViewCreated in BaseFragment.onViewCreated
|
||||
protected void initViews(final View rootView, final Bundle savedInstanceState) {
|
||||
super.initViews(rootView, savedInstanceState);
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
disposables.clear();
|
||||
if (subscribeButtonMonitor != null) {
|
||||
subscribeButtonMonitor.dispose();
|
||||
tabAdapter = new TabAdapter(getChildFragmentManager());
|
||||
binding.viewPager.setAdapter(tabAdapter);
|
||||
binding.tabLayout.setupWithViewPager(binding.viewPager);
|
||||
|
||||
setTitle(name);
|
||||
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
|
||||
protected void initListeners() {
|
||||
super.initListeners();
|
||||
|
||||
headerBinding.subChannelTitleView.setOnClickListener(this);
|
||||
headerBinding.subChannelAvatarView.setOnClickListener(this);
|
||||
final View.OnClickListener openSubChannel = v -> {
|
||||
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
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
@ -176,32 +194,33 @@ public class ChannelFragment extends BaseListInfoFragment<StreamInfoItem, Channe
|
||||
public void onCreateOptionsMenu(@NonNull final Menu menu,
|
||||
@NonNull final MenuInflater inflater) {
|
||||
super.onCreateOptionsMenu(menu, inflater);
|
||||
final ActionBar supportActionBar = activity.getSupportActionBar();
|
||||
if (useAsFrontPage && supportActionBar != null) {
|
||||
supportActionBar.setDisplayHomeAsUpEnabled(false);
|
||||
} else {
|
||||
inflater.inflate(R.menu.menu_channel, menu);
|
||||
inflater.inflate(R.menu.menu_channel, menu);
|
||||
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "onCreateOptionsMenu() called with: "
|
||||
+ "menu = [" + menu + "], inflater = [" + inflater + "]");
|
||||
}
|
||||
menuRssButton = menu.findItem(R.id.menu_item_rss);
|
||||
menuNotifyButton = menu.findItem(R.id.menu_item_notify);
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "onCreateOptionsMenu() called with: "
|
||||
+ "menu = [" + menu + "], inflater = [" + inflater + "]");
|
||||
}
|
||||
}
|
||||
|
||||
@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()) {
|
||||
case R.id.action_settings:
|
||||
NavigationHelper.openSettings(requireContext());
|
||||
break;
|
||||
case R.id.menu_item_notify:
|
||||
final boolean value = !item.isChecked();
|
||||
item.setEnabled(false);
|
||||
setNotify(value);
|
||||
break;
|
||||
case R.id.action_settings:
|
||||
NavigationHelper.openSettings(requireContext());
|
||||
break;
|
||||
case R.id.menu_item_rss:
|
||||
if (currentInfo != null) {
|
||||
ShareUtils.openUrlInApp(requireContext(), currentInfo.getFeedUrl());
|
||||
@ -215,7 +234,7 @@ public class ChannelFragment extends BaseListInfoFragment<StreamInfoItem, Channe
|
||||
case R.id.menu_item_share:
|
||||
if (currentInfo != null) {
|
||||
ShareUtils.shareText(requireContext(), name, currentInfo.getOriginalUrl(),
|
||||
currentInfo.getAvatarUrl());
|
||||
currentInfo.getAvatars());
|
||||
}
|
||||
break;
|
||||
default:
|
||||
@ -224,13 +243,14 @@ public class ChannelFragment extends BaseListInfoFragment<StreamInfoItem, Channe
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Channel Subscription
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
private void monitorSubscription(final ChannelInfo info) {
|
||||
final Consumer<Throwable> onError = (Throwable throwable) -> {
|
||||
animate(headerBinding.channelSubscribeButton, false, 100);
|
||||
animate(binding.channelSubscribeButton, false, 100);
|
||||
showSnackBarError(new ErrorInfo(throwable, UserAction.SUBSCRIPTION_GET,
|
||||
"Get subscription status", currentInfo));
|
||||
};
|
||||
@ -263,10 +283,9 @@ public class ChannelFragment extends BaseListInfoFragment<StreamInfoItem, Channe
|
||||
}, onError));
|
||||
}
|
||||
|
||||
private Function<Object, Object> mapOnSubscribe(final SubscriptionEntity subscription,
|
||||
final ChannelInfo info) {
|
||||
private Function<Object, Object> mapOnSubscribe(final SubscriptionEntity subscription) {
|
||||
return (@NonNull Object o) -> {
|
||||
subscriptionManager.insertSubscription(subscription, info);
|
||||
subscriptionManager.insertSubscription(subscription);
|
||||
return o;
|
||||
};
|
||||
}
|
||||
@ -298,8 +317,7 @@ public class ChannelFragment extends BaseListInfoFragment<StreamInfoItem, Channe
|
||||
.subscribe(onComplete, onError));
|
||||
}
|
||||
|
||||
private Disposable monitorSubscribeButton(final Button subscribeButton,
|
||||
final Function<Object, Object> action) {
|
||||
private Disposable monitorSubscribeButton(final Function<Object, Object> action) {
|
||||
final Consumer<Object> onNext = (@NonNull Object o) -> {
|
||||
if (DEBUG) {
|
||||
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));
|
||||
|
||||
/* Emit clicks from main thread unto io thread */
|
||||
return RxView.clicks(subscribeButton)
|
||||
return RxView.clicks(binding.channelSubscribeButton)
|
||||
.subscribeOn(AndroidSchedulers.mainThread())
|
||||
.observeOn(Schedulers.io())
|
||||
.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.setUrl(info.getUrl());
|
||||
channel.setData(info.getName(),
|
||||
info.getAvatarUrl(),
|
||||
ImageStrategy.imageListToDbUrl(info.getAvatars()),
|
||||
info.getDescription(),
|
||||
info.getSubscriberCount());
|
||||
channelSubscription = null;
|
||||
updateNotifyButton(null);
|
||||
subscribeButtonMonitor = monitorSubscribeButton(
|
||||
headerBinding.channelSubscribeButton, mapOnSubscribe(channel, info));
|
||||
subscribeButtonMonitor = monitorSubscribeButton(mapOnSubscribe(channel));
|
||||
} else {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "Found subscription to this channel!");
|
||||
}
|
||||
final SubscriptionEntity subscription = subscriptionEntities.get(0);
|
||||
updateNotifyButton(subscription);
|
||||
subscribeButtonMonitor = monitorSubscribeButton(
|
||||
headerBinding.channelSubscribeButton, mapOnUnsubscribe(subscription));
|
||||
channelSubscription = subscriptionEntities.get(0);
|
||||
updateNotifyButton(channelSubscription);
|
||||
subscribeButtonMonitor =
|
||||
monitorSubscribeButton(mapOnUnsubscribe(channelSubscription));
|
||||
}
|
||||
};
|
||||
}
|
||||
@ -361,34 +379,33 @@ public class ChannelFragment extends BaseListInfoFragment<StreamInfoItem, Channe
|
||||
+ "isSubscribed = [" + isSubscribed + "]");
|
||||
}
|
||||
|
||||
final boolean isButtonVisible = headerBinding.channelSubscribeButton.getVisibility()
|
||||
final boolean isButtonVisible = binding.channelSubscribeButton.getVisibility()
|
||||
== View.VISIBLE;
|
||||
final int backgroundDuration = isButtonVisible ? 300 : 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
|
||||
.getColor(activity, R.color.subscribed_background_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) {
|
||||
headerBinding.channelSubscribeButton.setText(R.string.subscribe_button_title);
|
||||
animateBackgroundColor(headerBinding.channelSubscribeButton, backgroundDuration,
|
||||
subscribedBackground, subscribeBackground);
|
||||
animateTextColor(headerBinding.channelSubscribeButton, textDuration, subscribedText,
|
||||
subscribeText);
|
||||
} else {
|
||||
headerBinding.channelSubscribeButton.setText(R.string.subscribed_button_title);
|
||||
animateBackgroundColor(headerBinding.channelSubscribeButton, backgroundDuration,
|
||||
if (isSubscribed) {
|
||||
binding.channelSubscribeButton.setText(R.string.subscribed_button_title);
|
||||
animateBackgroundColor(binding.channelSubscribeButton, backgroundDuration,
|
||||
subscribeBackground, subscribedBackground);
|
||||
animateTextColor(headerBinding.channelSubscribeButton, textDuration, subscribeText,
|
||||
animateTextColor(binding.channelSubscribeButton, textDuration, subscribeText,
|
||||
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,
|
||||
AnimationType.LIGHT_SCALE_AND_ALPHA);
|
||||
animate(binding.channelSubscribeButton, true, 100, AnimationType.LIGHT_SCALE_AND_ALPHA);
|
||||
}
|
||||
|
||||
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.
|
||||
*/
|
||||
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))
|
||||
.setActionTextColor(Color.YELLOW)
|
||||
.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
|
||||
public void onClick(final View v) {
|
||||
if (isLoading.get() || currentInfo == null) {
|
||||
return;
|
||||
}
|
||||
private void updateTabs() {
|
||||
tabAdapter.clearAllItems();
|
||||
|
||||
switch (v.getId()) {
|
||||
case R.id.sub_channel_avatar_view:
|
||||
case R.id.sub_channel_title_view:
|
||||
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");
|
||||
if (currentInfo != null && !channelContentNotSupported) {
|
||||
final Context context = requireContext();
|
||||
final SharedPreferences preferences = PreferenceManager
|
||||
.getDefaultSharedPreferences(context);
|
||||
|
||||
for (final ListLinkHandler linkHandler : currentInfo.getTabs()) {
|
||||
final String tab = linkHandler.getContentFilters().get(0);
|
||||
if (ChannelTabHelper.showChannelTab(context, preferences, tab)) {
|
||||
final ChannelTabFragment channelTabFragment =
|
||||
ChannelTabFragment.getInstance(serviceId, linkHandler, name);
|
||||
channelTabFragment.useAsFrontPage(useAsFrontPage);
|
||||
tabAdapter.addFragment(channelTabFragment,
|
||||
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
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@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
|
||||
public void showLoading() {
|
||||
super.showLoading();
|
||||
PicassoHelper.cancelTag(PICASSO_CHANNEL_TAG);
|
||||
animate(headerBinding.channelSubscribeButton, false, 100);
|
||||
animate(binding.channelSubscribeButton, false, 100);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleResult(@NonNull final ChannelInfo result) {
|
||||
super.handleResult(result);
|
||||
currentInfo = result;
|
||||
setInitialData(result.getServiceId(), result.getOriginalUrl(), result.getName());
|
||||
|
||||
headerBinding.getRoot().setVisibility(View.VISIBLE);
|
||||
PicassoHelper.loadBanner(result.getBannerUrl()).tag(PICASSO_CHANNEL_TAG)
|
||||
.into(headerBinding.channelBannerImage);
|
||||
PicassoHelper.loadAvatar(result.getAvatarUrl()).tag(PICASSO_CHANNEL_TAG)
|
||||
.into(headerBinding.channelAvatarView);
|
||||
PicassoHelper.loadAvatar(result.getParentChannelAvatarUrl()).tag(PICASSO_CHANNEL_TAG)
|
||||
.into(headerBinding.subChannelAvatarView);
|
||||
if (ImageStrategy.shouldLoadImages() && !result.getBanners().isEmpty()) {
|
||||
PicassoHelper.loadBanner(result.getBanners()).tag(PICASSO_CHANNEL_TAG)
|
||||
.into(binding.channelBannerImage);
|
||||
} else {
|
||||
// do not waste space for the banner, if the user disabled images or there is not one
|
||||
binding.channelBannerImage.setImageDrawable(null);
|
||||
}
|
||||
|
||||
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) {
|
||||
headerBinding.channelSubscriberView.setText(Localization
|
||||
binding.channelSubscriberView.setText(Localization
|
||||
.shortSubscriberCount(activity, result.getSubscriberCount()));
|
||||
} else {
|
||||
headerBinding.channelSubscriberView.setText(R.string.subscribers_count_not_available);
|
||||
binding.channelSubscriberView.setText(R.string.subscribers_count_not_available);
|
||||
}
|
||||
|
||||
if (!TextUtils.isEmpty(currentInfo.getParentChannelName())) {
|
||||
headerBinding.subChannelTitleView.setText(String.format(
|
||||
binding.subChannelTitleView.setText(String.format(
|
||||
getString(R.string.channel_created_by),
|
||||
currentInfo.getParentChannelName())
|
||||
);
|
||||
headerBinding.subChannelTitleView.setVisibility(View.VISIBLE);
|
||||
headerBinding.subChannelAvatarView.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
headerBinding.subChannelTitleView.setVisibility(View.GONE);
|
||||
binding.subChannelTitleView.setVisibility(View.VISIBLE);
|
||||
binding.subChannelAvatarView.setVisibility(View.VISIBLE);
|
||||
}
|
||||
|
||||
if (menuRssButton != null) {
|
||||
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;
|
||||
for (final Throwable throwable : result.getErrors()) {
|
||||
if (throwable instanceof ContentNotSupportedException) {
|
||||
@ -539,62 +627,21 @@ public class ChannelFragment extends BaseListInfoFragment<StreamInfoItem, Channe
|
||||
if (subscribeButtonMonitor != null) {
|
||||
subscribeButtonMonitor.dispose();
|
||||
}
|
||||
|
||||
updateTabs();
|
||||
updateSubscription(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() {
|
||||
// channelBinding might not be initialized when handleResult() is called
|
||||
// (e.g. after rotating the screen, #6696)
|
||||
if (!channelContentNotSupported || channelBinding == null) {
|
||||
if (!channelContentNotSupported || binding == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
channelBinding.errorContentNotSupported.setVisibility(View.VISIBLE);
|
||||
channelBinding.channelKaomoji.setText("(︶︹︺)");
|
||||
channelBinding.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);
|
||||
}
|
||||
binding.errorContentNotSupported.setVisibility(View.VISIBLE);
|
||||
binding.channelKaomoji.setText("(︶︹︺)");
|
||||
binding.channelKaomoji.setTextSize(TypedValue.COMPLEX_UNIT_SP, 45f);
|
||||
}
|
||||
}
|
||||
|
@ -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.extractor.ListExtractor;
|
||||
import org.schabi.newpipe.extractor.NewPipe;
|
||||
import org.schabi.newpipe.extractor.ServiceList;
|
||||
import org.schabi.newpipe.extractor.StreamingService;
|
||||
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||
import org.schabi.newpipe.extractor.kiosk.KioskInfo;
|
||||
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandlerFactory;
|
||||
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.fragments.list.BaseListInfoFragment;
|
||||
import org.schabi.newpipe.util.ExtractorHelper;
|
||||
@ -161,4 +163,14 @@ public class KioskFragment extends BaseListInfoFragment<StreamInfoItem, KioskInf
|
||||
name = 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.local.dialog.PlaylistDialog;
|
||||
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.PlaylistPlayQueue;
|
||||
import org.schabi.newpipe.util.ExtractorHelper;
|
||||
import org.schabi.newpipe.util.Localization;
|
||||
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.PlayButtonHelper;
|
||||
|
||||
import java.util.ArrayList;
|
||||
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.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";
|
||||
|
||||
@ -233,7 +234,7 @@ public class PlaylistFragment extends BaseListInfoFragment<StreamInfoItem, Playl
|
||||
break;
|
||||
case R.id.menu_item_share:
|
||||
ShareUtils.shareText(requireContext(), name, url,
|
||||
currentInfo == null ? null : currentInfo.getThumbnailUrl());
|
||||
currentInfo == null ? List.of() : currentInfo.getThumbnails());
|
||||
break;
|
||||
case R.id.menu_item_bookmark:
|
||||
onBookmarkClicked();
|
||||
@ -298,7 +299,6 @@ public class PlaylistFragment extends BaseListInfoFragment<StreamInfoItem, Playl
|
||||
|
||||
playlistControlBinding.getRoot().setVisibility(View.VISIBLE);
|
||||
|
||||
final String avatarUrl = result.getUploaderAvatarUrl();
|
||||
if (result.getServiceId() == ServiceList.YouTube.getServiceId()
|
||||
&& (YoutubeParsingHelper.isYoutubeMixId(result.getId())
|
||||
|| YoutubeParsingHelper.isYoutubeMusicMixId(result.getId()))) {
|
||||
@ -314,7 +314,7 @@ public class PlaylistFragment extends BaseListInfoFragment<StreamInfoItem, Playl
|
||||
R.drawable.ic_radio)
|
||||
);
|
||||
} else {
|
||||
PicassoHelper.loadAvatar(avatarUrl).tag(PICASSO_PLAYLIST_TAG)
|
||||
PicassoHelper.loadAvatar(result.getUploaderAvatars()).tag(PICASSO_PLAYLIST_TAG)
|
||||
.into(headerBinding.uploaderAvatarView);
|
||||
}
|
||||
|
||||
@ -332,25 +332,10 @@ public class PlaylistFragment extends BaseListInfoFragment<StreamInfoItem, Playl
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(getPlaylistBookmarkSubscriber());
|
||||
|
||||
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;
|
||||
});
|
||||
PlayButtonHelper.initPlaylistControlClickListener(activity, playlistControlBinding, this);
|
||||
}
|
||||
|
||||
private PlayQueue getPlayQueue() {
|
||||
public PlayQueue getPlayQueue() {
|
||||
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;
|
||||
|
||||
public static SearchFragment getInstance(final int serviceId, final String searchString) {
|
||||
@ -583,11 +587,13 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
||||
@Override
|
||||
public void beforeTextChanged(final CharSequence s, final int start,
|
||||
final int count, final int after) {
|
||||
// Do nothing, old text is already clean
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTextChanged(final CharSequence s, final int start,
|
||||
final int before, final int count) {
|
||||
// Changes are handled in afterTextChanged; CharSequence cannot be changed here.
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -8,7 +8,7 @@ import com.xwray.groupie.Item
|
||||
import org.schabi.newpipe.R
|
||||
import org.schabi.newpipe.extractor.stream.StreamSegment
|
||||
import org.schabi.newpipe.util.Localization
|
||||
import org.schabi.newpipe.util.PicassoHelper
|
||||
import org.schabi.newpipe.util.image.PicassoHelper
|
||||
|
||||
class StreamSegmentItem(
|
||||
private val item: StreamSegment,
|
||||
|
@ -104,7 +104,7 @@ public enum StreamDialogDefaultEntry {
|
||||
|
||||
SHARE(R.string.share, (fragment, item) ->
|
||||
ShareUtils.shareText(fragment.requireContext(), item.getName(), item.getUrl(),
|
||||
item.getThumbnailUrl())),
|
||||
item.getThumbnails())),
|
||||
|
||||
/**
|
||||
* 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.info_list.InfoItemBuilder;
|
||||
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;
|
||||
|
||||
public class ChannelMiniInfoItemHolder extends InfoItemHolder {
|
||||
@ -56,7 +56,7 @@ public class ChannelMiniInfoItemHolder extends InfoItemHolder {
|
||||
itemAdditionalDetailView.setText(getDetailLine(item));
|
||||
}
|
||||
|
||||
PicassoHelper.loadAvatar(item.getThumbnailUrl()).into(itemThumbnailView);
|
||||
PicassoHelper.loadAvatar(item.getThumbnails()).into(itemThumbnailView);
|
||||
|
||||
itemView.setOnClickListener(view -> {
|
||||
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.Localization;
|
||||
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.text.CommentTextOnTouchListener;
|
||||
import org.schabi.newpipe.util.text.TextLinkifier;
|
||||
@ -97,8 +98,8 @@ public class CommentsMiniInfoItemHolder extends InfoItemHolder {
|
||||
}
|
||||
final CommentsInfoItem item = (CommentsInfoItem) infoItem;
|
||||
|
||||
PicassoHelper.loadAvatar(item.getUploaderAvatarUrl()).into(itemThumbnailView);
|
||||
if (PicassoHelper.getShouldLoadImages()) {
|
||||
PicassoHelper.loadAvatar(item.getUploaderAvatars()).into(itemThumbnailView);
|
||||
if (ImageStrategy.shouldLoadImages()) {
|
||||
itemThumbnailView.setVisibility(View.VISIBLE);
|
||||
itemRoot.setPadding(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.info_list.InfoItemBuilder;
|
||||
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;
|
||||
|
||||
public class PlaylistMiniInfoItemHolder extends InfoItemHolder {
|
||||
@ -46,7 +46,7 @@ public class PlaylistMiniInfoItemHolder extends InfoItemHolder {
|
||||
.localizeStreamCountMini(itemStreamCountView.getContext(), item.getStreamCount()));
|
||||
itemUploaderView.setText(item.getUploaderName());
|
||||
|
||||
PicassoHelper.loadPlaylistThumbnail(item.getThumbnailUrl()).into(itemThumbnailView);
|
||||
PicassoHelper.loadPlaylistThumbnail(item.getThumbnails()).into(itemThumbnailView);
|
||||
|
||||
itemView.setOnClickListener(view -> {
|
||||
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.util.DependentPreferenceHelper;
|
||||
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.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
|
||||
PicassoHelper.loadThumbnail(item.getThumbnailUrl()).into(itemThumbnailView);
|
||||
PicassoHelper.loadThumbnail(item.getThumbnails()).into(itemThumbnailView);
|
||||
|
||||
itemView.setOnClickListener(view -> {
|
||||
if (itemBuilder.getOnStreamSelectedListener() != null) {
|
||||
|
@ -91,11 +91,6 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL
|
||||
// Fragment LifeCycle - Views
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@Override
|
||||
protected void initViews(final View rootView, final Bundle savedInstanceState) {
|
||||
super.initViews(rootView, savedInstanceState);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void initListeners() {
|
||||
super.initListeners();
|
||||
|
@ -38,7 +38,6 @@ import android.view.ViewGroup
|
||||
import android.widget.Button
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.core.content.edit
|
||||
import androidx.core.math.MathUtils
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.core.view.isVisible
|
||||
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.databinding.FragmentFeedBinding
|
||||
import org.schabi.newpipe.error.ErrorInfo
|
||||
import org.schabi.newpipe.error.ErrorUtil
|
||||
import org.schabi.newpipe.error.UserAction
|
||||
import org.schabi.newpipe.extractor.exceptions.AccountTerminatedException
|
||||
import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException
|
||||
@ -453,24 +453,33 @@ class FeedFragment : BaseStateFragment<FeedState>() {
|
||||
if (t is FeedLoadService.RequestException &&
|
||||
t.cause is ContentNotAvailableException
|
||||
) {
|
||||
Single.fromCallable {
|
||||
NewPipeDatabase.getInstance(requireContext()).subscriptionDAO()
|
||||
.getSubscription(t.subscriptionId)
|
||||
}.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
{ subscriptionEntity ->
|
||||
handleFeedNotAvailable(
|
||||
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()
|
||||
disposables.add(
|
||||
Single.fromCallable {
|
||||
NewPipeDatabase.getInstance(requireContext()).subscriptionDAO()
|
||||
.getSubscription(t.subscriptionId)
|
||||
}
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
{ subscriptionEntity ->
|
||||
handleFeedNotAvailable(
|
||||
subscriptionEntity,
|
||||
t.cause,
|
||||
errors.subList(i + 1, errors.size)
|
||||
)
|
||||
},
|
||||
{ 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(
|
||||
@ -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
|
||||
groupAdapter.notifyItemRangeChanged(
|
||||
0,
|
||||
MathUtils.clamp(highlightCount, lastNewItemsCount, groupAdapter.itemCount)
|
||||
highlightCount.coerceIn(lastNewItemsCount, groupAdapter.itemCount)
|
||||
)
|
||||
|
||||
if (highlightCount > 0) {
|
||||
@ -598,9 +607,13 @@ class FeedFragment : BaseStateFragment<FeedState>() {
|
||||
execOnEnd = {
|
||||
// Disabled animations would result in immediately hiding the button
|
||||
// after it showed up
|
||||
if (DeviceUtils.hasAnimationsAnimatorDurationEnabled(context)) {
|
||||
// Hide the new items-"popup" after 10s
|
||||
hideNewItemsLoaded(true, 10000)
|
||||
// Context can be null in some cases, so we have to make sure it is not null in
|
||||
// order to avoid a NullPointerException
|
||||
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(
|
||||
val items: List<StreamItem>,
|
||||
val oldestUpdate: OffsetDateTime? = null,
|
||||
val oldestUpdate: OffsetDateTime?,
|
||||
val notLoadedCount: Long,
|
||||
val itemsErrors: List<Throwable> = emptyList()
|
||||
val itemsErrors: List<Throwable>
|
||||
) : FeedState()
|
||||
|
||||
data class ErrorState(
|
||||
|
@ -86,7 +86,7 @@ class FeedViewModel(
|
||||
.subscribe { (event, listFromDB, notLoadedCount, oldestUpdate) ->
|
||||
mutableStateLiveData.postValue(
|
||||
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 SuccessResultEvent -> FeedState.LoadedState(listFromDB.map { e -> StreamItem(e) }, oldestUpdate, notLoadedCount, event.itemsErrors)
|
||||
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.VIDEO_STREAM
|
||||
import org.schabi.newpipe.util.Localization
|
||||
import org.schabi.newpipe.util.PicassoHelper
|
||||
import org.schabi.newpipe.util.StreamTypeUtil
|
||||
import org.schabi.newpipe.util.image.PicassoHelper
|
||||
import java.util.concurrent.TimeUnit
|
||||
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.local.feed.service.FeedUpdateInfo
|
||||
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.
|
||||
@ -58,7 +58,7 @@ class NotificationHelper(val context: Context) {
|
||||
.setAutoCancel(true)
|
||||
.setCategory(NotificationCompat.CATEGORY_SOCIAL)
|
||||
.setGroupSummary(true)
|
||||
.setGroup(data.listInfo.url)
|
||||
.setGroup(data.url)
|
||||
.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_SUMMARY)
|
||||
|
||||
// Build a summary notification for Android versions < 7.0
|
||||
@ -73,7 +73,7 @@ class NotificationHelper(val context: Context) {
|
||||
context,
|
||||
data.pseudoId,
|
||||
NavigationHelper
|
||||
.getChannelIntent(context, data.listInfo.serviceId, data.listInfo.url)
|
||||
.getChannelIntent(context, data.serviceId, data.url)
|
||||
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK),
|
||||
0,
|
||||
false
|
||||
@ -88,7 +88,7 @@ class NotificationHelper(val context: Context) {
|
||||
|
||||
// Show individual stream notifications, set channel icon only if there is actually
|
||||
// one
|
||||
showStreamNotifications(newStreams, data.listInfo.serviceId, bitmap)
|
||||
showStreamNotifications(newStreams, data.serviceId, bitmap)
|
||||
// Show summary notification
|
||||
manager.notify(data.pseudoId, summaryBuilder.build())
|
||||
|
||||
@ -97,7 +97,7 @@ class NotificationHelper(val context: Context) {
|
||||
|
||||
override fun onBitmapFailed(e: Exception, errorDrawable: Drawable) {
|
||||
// Show individual stream notifications
|
||||
showStreamNotifications(newStreams, data.listInfo.serviceId, null)
|
||||
showStreamNotifications(newStreams, data.serviceId, null)
|
||||
// Show summary notification
|
||||
manager.notify(data.pseudoId, summaryBuilder.build())
|
||||
iconLoadingTargets.remove(this) // allow it to be garbage-collected
|
||||
|
@ -1,6 +1,7 @@
|
||||
package org.schabi.newpipe.local.feed.service
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import androidx.preference.PreferenceManager
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
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.database.feed.model.FeedGroupEntity
|
||||
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.local.feed.FeedDatabaseManager
|
||||
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.ZoneOffset
|
||||
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
|
||||
*/
|
||||
val outdatedSubscriptions = when (groupId) {
|
||||
FeedGroupEntity.GROUP_ALL_ID -> feedDatabaseManager.outdatedSubscriptions(outdatedThreshold)
|
||||
FeedGroupEntity.GROUP_ALL_ID -> feedDatabaseManager.outdatedSubscriptions(
|
||||
outdatedThreshold
|
||||
)
|
||||
GROUP_NOTIFICATION_ENABLED -> feedDatabaseManager.outdatedSubscriptionsWithNotificationMode(
|
||||
outdatedThreshold, NotificationMode.ENABLED
|
||||
)
|
||||
@ -101,52 +110,7 @@ class FeedLoadManager(private val context: Context) {
|
||||
.runOn(Schedulers.io(), PARALLEL_EXTRACTIONS * 2)
|
||||
.filter { !cancelSignal.get() }
|
||||
.map { subscriptionEntity ->
|
||||
var error: Throwable? = null
|
||||
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)
|
||||
}
|
||||
loadStreams(subscriptionEntity, useFeedExtractor, defaultSharedPreferences)
|
||||
}
|
||||
.sequential()
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
@ -164,7 +128,112 @@ class FeedLoadManager(private val context: Context) {
|
||||
}
|
||||
|
||||
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) {
|
||||
when {
|
||||
notification.isOnNext -> {
|
||||
val subscriptionId = notification.value!!.uid
|
||||
val info = notification.value!!.listInfo
|
||||
val info = notification.value!!
|
||||
|
||||
notification.value!!.newStreams = filterNewStreams(
|
||||
notification.value!!.listInfo.relatedItems
|
||||
)
|
||||
notification.value!!.newStreams = filterNewStreams(info.streams)
|
||||
|
||||
feedDatabaseManager.upsertAll(subscriptionId, info.relatedItems)
|
||||
subscriptionManager.updateFromInfo(subscriptionId, info)
|
||||
feedDatabaseManager.upsertAll(info.uid, info.streams)
|
||||
subscriptionManager.updateFromInfo(info)
|
||||
|
||||
if (info.errors.isNotEmpty()) {
|
||||
feedResultsHolder.addErrors(
|
||||
FeedLoadService.RequestException.wrapList(
|
||||
subscriptionId,
|
||||
info
|
||||
)
|
||||
info.errors.map {
|
||||
FeedLoadService.RequestException(
|
||||
info.uid,
|
||||
"${info.serviceId}:${info.url}",
|
||||
it
|
||||
)
|
||||
}
|
||||
)
|
||||
feedDatabaseManager.markAsOutdated(subscriptionId)
|
||||
feedDatabaseManager.markAsOutdated(info.uid)
|
||||
}
|
||||
}
|
||||
notification.isOnError -> {
|
||||
|
@ -39,8 +39,6 @@ import org.schabi.newpipe.App
|
||||
import org.schabi.newpipe.MainActivity.DEBUG
|
||||
import org.schabi.newpipe.R
|
||||
import org.schabi.newpipe.database.feed.model.FeedGroupEntity
|
||||
import org.schabi.newpipe.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.postEvent
|
||||
import java.util.concurrent.TimeUnit
|
||||
@ -126,17 +124,7 @@ class FeedLoadService : Service() {
|
||||
// Loading & Handling
|
||||
// /////////////////////////////////////////////////////////////////////////
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
class RequestException(val subscriptionId: Long, message: String, cause: Throwable) : Exception(message, cause)
|
||||
|
||||
// /////////////////////////////////////////////////////////////////////////
|
||||
// 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.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.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(
|
||||
val uid: Long,
|
||||
@NotificationMode
|
||||
val notificationMode: Int,
|
||||
val name: 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(
|
||||
subscription: SubscriptionEntity,
|
||||
listInfo: ListInfo<StreamInfoItem>,
|
||||
info: Info,
|
||||
streams: List<StreamInfoItem>,
|
||||
errors: List<Throwable>,
|
||||
) : this(
|
||||
uid = subscription.uid,
|
||||
notificationMode = subscription.notificationMode,
|
||||
name = subscription.name,
|
||||
avatarUrl = subscription.avatarUrl,
|
||||
listInfo = listInfo,
|
||||
name = info.name,
|
||||
avatarUrl = (info as? ChannelInfo)?.avatars?.let {
|
||||
// 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.
|
||||
*/
|
||||
val pseudoId: Int
|
||||
get() = listInfo.url.hashCode()
|
||||
get() = url.hashCode()
|
||||
|
||||
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.UserAction;
|
||||
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.StreamDialogDefaultEntry;
|
||||
import org.schabi.newpipe.local.BaseLocalListFragment;
|
||||
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
||||
import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
|
||||
import org.schabi.newpipe.settings.HistorySettingsFragment;
|
||||
import org.schabi.newpipe.util.NavigationHelper;
|
||||
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.Collections;
|
||||
@ -49,7 +51,8 @@ import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
||||
import io.reactivex.rxjava3.disposables.Disposable;
|
||||
|
||||
public class StatisticsPlaylistFragment
|
||||
extends BaseLocalListFragment<List<StreamStatisticsEntry>, Void> {
|
||||
extends BaseLocalListFragment<List<StreamStatisticsEntry>, Void>
|
||||
implements PlaylistControlViewHolder {
|
||||
private final CompositeDisposable disposables = new CompositeDisposable();
|
||||
@State
|
||||
Parcelable itemsListState;
|
||||
@ -195,14 +198,9 @@ public class StatisticsPlaylistFragment
|
||||
if (itemListAdapter != null) {
|
||||
itemListAdapter.unsetSelectedListener();
|
||||
}
|
||||
if (playlistControlBinding != null) {
|
||||
playlistControlBinding.playlistCtrlPlayBgButton.setOnClickListener(null);
|
||||
playlistControlBinding.playlistCtrlPlayAllButton.setOnClickListener(null);
|
||||
playlistControlBinding.playlistCtrlPlayPopupButton.setOnClickListener(null);
|
||||
|
||||
headerBinding = null;
|
||||
playlistControlBinding = null;
|
||||
}
|
||||
headerBinding = null;
|
||||
playlistControlBinding = null;
|
||||
|
||||
if (databaseSubscription != null) {
|
||||
databaseSubscription.cancel();
|
||||
@ -276,12 +274,8 @@ public class StatisticsPlaylistFragment
|
||||
itemsListState = null;
|
||||
}
|
||||
|
||||
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));
|
||||
PlayButtonHelper.initPlaylistControlClickListener(activity, playlistControlBinding, this);
|
||||
|
||||
headerBinding.sortButton.setOnClickListener(view -> toggleSortMode());
|
||||
|
||||
hideLoading();
|
||||
@ -374,7 +368,7 @@ public class StatisticsPlaylistFragment
|
||||
}
|
||||
}
|
||||
|
||||
private PlayQueue getPlayQueue() {
|
||||
public PlayQueue getPlayQueue() {
|
||||
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.local.LocalItemBuilder;
|
||||
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 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.util.DependentPreferenceHelper;
|
||||
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.views.AnimatedProgressBar;
|
||||
|
||||
|
@ -16,7 +16,7 @@ import org.schabi.newpipe.local.LocalItemBuilder;
|
||||
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
||||
import org.schabi.newpipe.util.DependentPreferenceHelper;
|
||||
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.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.history.HistoryRecordManager;
|
||||
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 java.time.format.DateTimeFormatter;
|
||||
|
@ -22,7 +22,6 @@ import android.widget.Toast;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.preference.PreferenceManager;
|
||||
import androidx.recyclerview.widget.ItemTouchHelper;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
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.UserAction;
|
||||
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.StreamDialogDefaultEntry;
|
||||
import org.schabi.newpipe.local.BaseLocalListFragment;
|
||||
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.SinglePlayQueue;
|
||||
import org.schabi.newpipe.util.Localization;
|
||||
import org.schabi.newpipe.util.NavigationHelper;
|
||||
import org.schabi.newpipe.util.OnClickGesture;
|
||||
import org.schabi.newpipe.util.PlayButtonHelper;
|
||||
import org.schabi.newpipe.util.external_communication.ShareUtils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
@ -69,8 +70,9 @@ import io.reactivex.rxjava3.disposables.Disposable;
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers;
|
||||
import io.reactivex.rxjava3.subjects.PublishSubject;
|
||||
|
||||
public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistStreamEntry>, Void> {
|
||||
// Save the list 10 seconds after the last change occurred
|
||||
public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistStreamEntry>, Void>
|
||||
implements PlaylistControlViewHolder {
|
||||
/** Save the list 10 seconds after the last change occurred. */
|
||||
private static final long SAVE_DEBOUNCE_MILLIS = 10000;
|
||||
private static final int MINIMUM_INITIAL_DRAG_VELOCITY = 12;
|
||||
@State
|
||||
@ -91,13 +93,20 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
|
||||
private PublishSubject<Long> debouncedSaveSignal;
|
||||
private CompositeDisposable disposables;
|
||||
|
||||
/* Has the playlist been fully loaded from db */
|
||||
/** Whether the playlist has been fully loaded from db. */
|
||||
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;
|
||||
/* Flag to prevent simultaneous rewrites of the playlist */
|
||||
/** Flag to prevent simultaneous rewrites of the playlist. */
|
||||
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) {
|
||||
final LocalPlaylistFragment instance = new LocalPlaylistFragment();
|
||||
instance.setInitialData(playlistId, name);
|
||||
@ -157,6 +166,17 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
|
||||
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
|
||||
protected void initListeners() {
|
||||
super.initListeners();
|
||||
@ -265,14 +285,10 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
|
||||
if (itemListAdapter != null) {
|
||||
itemListAdapter.unsetSelectedListener();
|
||||
}
|
||||
if (playlistControlBinding != null) {
|
||||
playlistControlBinding.playlistCtrlPlayBgButton.setOnClickListener(null);
|
||||
playlistControlBinding.playlistCtrlPlayAllButton.setOnClickListener(null);
|
||||
playlistControlBinding.playlistCtrlPlayPopupButton.setOnClickListener(null);
|
||||
|
||||
headerBinding = null;
|
||||
playlistControlBinding = null;
|
||||
}
|
||||
headerBinding = null;
|
||||
playlistControlBinding = null;
|
||||
|
||||
|
||||
if (databaseSubscription != null) {
|
||||
databaseSubscription.cancel();
|
||||
@ -294,6 +310,9 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
|
||||
if (disposables != null) {
|
||||
disposables.dispose();
|
||||
}
|
||||
if (tabsPagerAdapter != null) {
|
||||
tabsPagerAdapter.getLocalPlaylistFragments().remove(this);
|
||||
}
|
||||
|
||||
debouncedSaveSignal = null;
|
||||
playlistManager = null;
|
||||
@ -349,7 +368,7 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(final MenuItem item) {
|
||||
if (item.getItemId() == R.id.menu_item_share_playlist) {
|
||||
sharePlaylist();
|
||||
createShareConfirmationDialog();
|
||||
} else if (item.getItemId() == R.id.menu_item_rename_playlist) {
|
||||
createRenameDialog();
|
||||
} 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)
|
||||
.flatMapSingle(playlist -> Single.just(playlist.stream()
|
||||
.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"))))
|
||||
.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)));
|
||||
}
|
||||
|
||||
@ -498,38 +534,11 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
|
||||
}
|
||||
setVideoCount(itemListAdapter.getItemsList().size());
|
||||
|
||||
playlistControlBinding.playlistCtrlPlayAllButton.setOnClickListener(view -> {
|
||||
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;
|
||||
});
|
||||
PlayButtonHelper.initPlaylistControlClickListener(activity, playlistControlBinding, this);
|
||||
|
||||
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
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
@ -853,7 +862,7 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
|
||||
}
|
||||
}
|
||||
|
||||
private PlayQueue getPlayQueue() {
|
||||
public PlayQueue getPlayQueue() {
|
||||
return getPlayQueue(0);
|
||||
}
|
||||
|
||||
@ -871,5 +880,29 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
|
||||
}
|
||||
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()
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
super.onDestroyView()
|
||||
_binding = null
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
disposables.dispose()
|
||||
@ -336,8 +341,7 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
|
||||
val actions = DialogInterface.OnClickListener { _, i ->
|
||||
when (i) {
|
||||
0 -> ShareUtils.shareText(
|
||||
requireContext(), selectedItem.name, selectedItem.url,
|
||||
selectedItem.thumbnailUrl
|
||||
requireContext(), selectedItem.name, selectedItem.url, selectedItem.thumbnails
|
||||
)
|
||||
1 -> ShareUtils.openUrlInBrowser(requireContext(), selectedItem.url)
|
||||
2 -> deleteChannel(selectedItem)
|
||||
|
@ -1,6 +1,7 @@
|
||||
package org.schabi.newpipe.local.subscription
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Pair
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.core.Completable
|
||||
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.SubscriptionDAO
|
||||
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.feed.FeedInfo
|
||||
import org.schabi.newpipe.extractor.channel.tabs.ChannelTabInfo
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem
|
||||
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.image.ImageStrategy
|
||||
|
||||
class SubscriptionManager(context: 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(
|
||||
infoList.map { SubscriptionEntity.from(it) }
|
||||
infoList.map { SubscriptionEntity.from(it.first) }
|
||||
)
|
||||
|
||||
database.runInTransaction {
|
||||
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
|
||||
}
|
||||
|
||||
fun updateChannelInfo(info: ChannelInfo): Completable = subscriptionTable.getSubscription(info.serviceId, info.url)
|
||||
.flatMapCompletable {
|
||||
Completable.fromRunnable {
|
||||
it.setData(info.name, info.avatarUrl, info.description, info.subscriberCount)
|
||||
subscriptionTable.update(it)
|
||||
feedDatabaseManager.upsertAll(it.uid, info.relatedItems)
|
||||
fun updateChannelInfo(info: ChannelInfo): Completable =
|
||||
subscriptionTable.getSubscription(info.serviceId, info.url)
|
||||
.flatMapCompletable {
|
||||
Completable.fromRunnable {
|
||||
it.setData(
|
||||
info.name,
|
||||
ImageStrategy.imageListToDbUrl(info.avatars),
|
||||
info.description,
|
||||
info.subscriberCount
|
||||
)
|
||||
subscriptionTable.update(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun updateNotificationMode(serviceId: Int, url: String, @NotificationMode mode: Int): Completable {
|
||||
return subscriptionTable().getSubscription(serviceId, url)
|
||||
@ -84,19 +96,15 @@ class SubscriptionManager(context: Context) {
|
||||
}
|
||||
}
|
||||
|
||||
fun updateFromInfo(subscriptionId: Long, info: ListInfo<StreamInfoItem>) {
|
||||
val subscriptionEntity = subscriptionTable.getSubscription(subscriptionId)
|
||||
fun updateFromInfo(info: FeedUpdateInfo) {
|
||||
val subscriptionEntity = subscriptionTable.getSubscription(info.uid)
|
||||
|
||||
if (info is FeedInfo) {
|
||||
subscriptionEntity.name = info.name
|
||||
} else if (info is ChannelInfo) {
|
||||
subscriptionEntity.setData(
|
||||
info.name,
|
||||
info.avatarUrl,
|
||||
info.description,
|
||||
info.subscriberCount
|
||||
)
|
||||
}
|
||||
subscriptionEntity.name = info.name
|
||||
subscriptionEntity.avatarUrl = info.avatarUrl
|
||||
|
||||
// these two fields are null if the feed info was fetched using the fast feed method
|
||||
info.description?.let { subscriptionEntity.description = it }
|
||||
info.subscriberCount?.let { subscriptionEntity.subscriberCount = it }
|
||||
|
||||
subscriptionTable.update(subscriptionEntity)
|
||||
}
|
||||
@ -107,11 +115,8 @@ class SubscriptionManager(context: Context) {
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
}
|
||||
|
||||
fun insertSubscription(subscriptionEntity: SubscriptionEntity, info: ChannelInfo) {
|
||||
database.runInTransaction {
|
||||
val subscriptionId = subscriptionTable.insert(subscriptionEntity)
|
||||
feedDatabaseManager.upsertAll(subscriptionId, info.relatedItems)
|
||||
}
|
||||
fun insertSubscription(subscriptionEntity: SubscriptionEntity) {
|
||||
subscriptionTable.insert(subscriptionEntity)
|
||||
}
|
||||
|
||||
fun deleteSubscription(subscriptionEntity: SubscriptionEntity) {
|
||||
@ -125,7 +130,10 @@ class SubscriptionManager(context: Context) {
|
||||
*/
|
||||
private fun rememberAllStreams(subscription: SubscriptionEntity): Completable {
|
||||
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 ->
|
||||
Completable.fromAction {
|
||||
database.streamDAO().upsertAll(entities)
|
||||
|
@ -9,7 +9,7 @@ import org.schabi.newpipe.R
|
||||
import org.schabi.newpipe.extractor.channel.ChannelInfoItem
|
||||
import org.schabi.newpipe.util.Localization
|
||||
import org.schabi.newpipe.util.OnClickGesture
|
||||
import org.schabi.newpipe.util.PicassoHelper
|
||||
import org.schabi.newpipe.util.image.PicassoHelper
|
||||
|
||||
class ChannelItem(
|
||||
private val infoItem: ChannelInfoItem,
|
||||
@ -39,7 +39,7 @@ class ChannelItem(
|
||||
itemChannelDescriptionView.text = infoItem.description
|
||||
}
|
||||
|
||||
PicassoHelper.loadAvatar(infoItem.thumbnailUrl).into(itemThumbnailView)
|
||||
PicassoHelper.loadAvatar(infoItem.thumbnails).into(itemThumbnailView)
|
||||
|
||||
gesturesListener?.run {
|
||||
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.ktx.AnimationType
|
||||
import org.schabi.newpipe.ktx.animate
|
||||
import org.schabi.newpipe.util.PicassoHelper
|
||||
import org.schabi.newpipe.util.image.PicassoHelper
|
||||
|
||||
data class PickerSubscriptionItem(
|
||||
val subscriptionEntity: SubscriptionEntity,
|
||||
|
@ -26,6 +26,7 @@ import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
import android.util.Pair;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
@ -38,6 +39,7 @@ import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.database.subscription.SubscriptionEntity;
|
||||
import org.schabi.newpipe.extractor.NewPipe;
|
||||
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.ktx.ExceptionUtils;
|
||||
import org.schabi.newpipe.streams.io.SharpInputStream;
|
||||
@ -48,6 +50,7 @@ import org.schabi.newpipe.util.ExtractorHelper;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
@ -199,12 +202,19 @@ public class SubscriptionsImportService extends BaseImportExportService {
|
||||
|
||||
.parallel(PARALLEL_EXTRACTIONS)
|
||||
.runOn(Schedulers.io())
|
||||
.map((Function<SubscriptionItem, Notification<ChannelInfo>>) subscriptionItem -> {
|
||||
.map((Function<SubscriptionItem, Notification<Pair<ChannelInfo,
|
||||
List<ChannelTabInfo>>>>) subscriptionItem -> {
|
||||
try {
|
||||
return Notification.createOnNext(ExtractorHelper
|
||||
final ChannelInfo channelInfo = ExtractorHelper
|
||||
.getChannelInfo(subscriptionItem.getServiceId(),
|
||||
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) {
|
||||
return Notification.createOnError(e);
|
||||
}
|
||||
@ -223,7 +233,7 @@ public class SubscriptionsImportService extends BaseImportExportService {
|
||||
}
|
||||
|
||||
private Subscriber<List<SubscriptionEntity>> getSubscriber() {
|
||||
return new Subscriber<List<SubscriptionEntity>>() {
|
||||
return new Subscriber<>() {
|
||||
@Override
|
||||
public void onSubscribe(final 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 -> {
|
||||
if (notification.isOnNext()) {
|
||||
final String name = notification.getValue().getName();
|
||||
final String name = notification.getValue().first.getName();
|
||||
eventListener.onItemCompleted(!TextUtils.isEmpty(name) ? name : "");
|
||||
} else if (notification.isOnError()) {
|
||||
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 -> {
|
||||
final List<ChannelInfo> infoList = new ArrayList<>(notificationList.size());
|
||||
for (final Notification<ChannelInfo> n : notificationList) {
|
||||
final List<Pair<ChannelInfo, List<ChannelTabInfo>>> infoList =
|
||||
new ArrayList<>(notificationList.size());
|
||||
for (final Notification<Pair<ChannelInfo, List<ChannelTabInfo>>> n : notificationList) {
|
||||
if (n.isOnNext()) {
|
||||
infoList.add(n.getValue());
|
||||
}
|
||||
|
@ -16,6 +16,7 @@ import android.view.MenuItem;
|
||||
import android.view.SubMenu;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageButton;
|
||||
import android.widget.SeekBar;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
@ -531,18 +532,19 @@ public final class PlayQueueActivity extends AppCompatActivity
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
private void onStateChanged(final int state) {
|
||||
final ImageButton playPauseButton = queueControlBinding.controlPlayPause;
|
||||
switch (state) {
|
||||
case Player.STATE_PAUSED:
|
||||
queueControlBinding.controlPlayPause
|
||||
.setImageResource(R.drawable.ic_play_arrow);
|
||||
playPauseButton.setImageResource(R.drawable.ic_play_arrow);
|
||||
playPauseButton.setContentDescription(getString(R.string.play));
|
||||
break;
|
||||
case Player.STATE_PLAYING:
|
||||
queueControlBinding.controlPlayPause
|
||||
.setImageResource(R.drawable.ic_pause);
|
||||
playPauseButton.setImageResource(R.drawable.ic_pause);
|
||||
playPauseButton.setContentDescription(getString(R.string.pause));
|
||||
break;
|
||||
case Player.STATE_COMPLETED:
|
||||
queueControlBinding.controlPlayPause
|
||||
.setImageResource(R.drawable.ic_replay);
|
||||
playPauseButton.setImageResource(R.drawable.ic_replay);
|
||||
playPauseButton.setContentDescription(getString(R.string.replay));
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
@ -585,11 +587,9 @@ public final class PlayQueueActivity extends AppCompatActivity
|
||||
}
|
||||
|
||||
private void onPlaybackParameterChanged(@Nullable final PlaybackParameters parameters) {
|
||||
if (parameters != null) {
|
||||
if (menu != null && player != null) {
|
||||
final MenuItem item = menu.findItem(R.id.action_playback_speed);
|
||||
item.setTitle(formatSpeed(parameters.speed));
|
||||
}
|
||||
if (parameters != null && menu != null && player != null) {
|
||||
final MenuItem item = menu.findItem(R.id.action_playback_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 List<AudioStream> availableStreams =
|
||||
Optional.ofNullable(player.getCurrentMetadata())
|
||||
Optional.ofNullable(player)
|
||||
.map(Player::getCurrentMetadata)
|
||||
.flatMap(MediaItemTag::getMaybeAudioTrack)
|
||||
.map(MediaItemTag.AudioTrack::getAudioStreams)
|
||||
.orElse(null);
|
||||
final Optional<AudioStream> selectedAudioStream = player.getSelectedAudioStream();
|
||||
final Optional<AudioStream> selectedAudioStream = Optional.ofNullable(player)
|
||||
.flatMap(Player::getSelectedAudioStream);
|
||||
|
||||
if (availableStreams == null || availableStreams.size() < 2
|
||||
|| selectedAudioStream.isEmpty()) {
|
||||
|
@ -87,6 +87,7 @@ import org.schabi.newpipe.error.ErrorInfo;
|
||||
import org.schabi.newpipe.error.ErrorUtil;
|
||||
import org.schabi.newpipe.error.UserAction;
|
||||
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.StreamType;
|
||||
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.ListHelper;
|
||||
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.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) {
|
||||
Log.d(TAG, "Thumbnail - loadCurrentThumbnail() called with url = ["
|
||||
+ (url == null ? "null" : url) + "]");
|
||||
Log.d(TAG, "Thumbnail - loadCurrentThumbnail() called with thumbnails = ["
|
||||
+ thumbnails.size() + "]");
|
||||
}
|
||||
|
||||
// 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
|
||||
// session metadata while the new thumbnail is being loaded by Picasso.
|
||||
onThumbnailLoaded(null);
|
||||
if (isNullOrEmpty(url)) {
|
||||
if (thumbnails.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// scale down the notification thumbnail for performance
|
||||
PicassoHelper.loadScaledDownThumbnail(context, url)
|
||||
PicassoHelper.loadScaledDownThumbnail(context, thumbnails)
|
||||
.tag(PICASSO_PLAYER_THUMBNAIL_TAG)
|
||||
.into(currentThumbnailTarget);
|
||||
}
|
||||
@ -1082,7 +1083,7 @@ public final class Player implements PlaybackListener, Listener {
|
||||
|
||||
UIs.call(PlayerUi::onPrepared);
|
||||
|
||||
if (playWhenReady) {
|
||||
if (playWhenReady && !isMuted()) {
|
||||
audioReactor.requestAudioFocus();
|
||||
}
|
||||
}
|
||||
@ -1223,6 +1224,11 @@ public final class Player implements PlaybackListener, Listener {
|
||||
public void toggleMute() {
|
||||
final boolean wasMuted = isMuted();
|
||||
simpleExoPlayer.setVolume(wasMuted ? 1 : 0);
|
||||
if (wasMuted) {
|
||||
audioReactor.requestAudioFocus();
|
||||
} else {
|
||||
audioReactor.abandonAudioFocus();
|
||||
}
|
||||
UIs.call(playerUi -> playerUi.onMuteUnmuteChanged(!wasMuted));
|
||||
notifyPlaybackUpdateToListeners();
|
||||
}
|
||||
@ -1620,7 +1626,9 @@ public final class Player implements PlaybackListener, Listener {
|
||||
return;
|
||||
}
|
||||
|
||||
audioReactor.requestAudioFocus();
|
||||
if (!isMuted()) {
|
||||
audioReactor.requestAudioFocus();
|
||||
}
|
||||
|
||||
if (currentState == STATE_COMPLETED) {
|
||||
if (playQueue.getIndex() == 0) {
|
||||
@ -1785,7 +1793,7 @@ public final class Player implements PlaybackListener, Listener {
|
||||
|
||||
maybeAutoQueueNextStream(info);
|
||||
|
||||
loadCurrentThumbnail(info.getThumbnailUrl());
|
||||
loadCurrentThumbnail(info.getThumbnails());
|
||||
registerStreamViewed();
|
||||
|
||||
notifyMetadataUpdateToListeners();
|
||||
|
@ -29,6 +29,7 @@ import android.os.IBinder;
|
||||
import android.util.Log;
|
||||
|
||||
import org.schabi.newpipe.player.mediasession.MediaSessionPlayerUi;
|
||||
import org.schabi.newpipe.player.notification.NotificationPlayerUi;
|
||||
import org.schabi.newpipe.util.ThemeHelper;
|
||||
|
||||
import java.lang.ref.WeakReference;
|
||||
@ -59,6 +60,14 @@ public final class PlayerService extends Service {
|
||||
ThemeHelper.setTheme(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
|
||||
@ -68,16 +77,38 @@ public final class PlayerService extends Service {
|
||||
+ "], 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())
|
||||
&& 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
|
||||
&& (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
|
||||
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;
|
||||
}
|
||||
|
||||
player.handleIntent(intent);
|
||||
player.UIs().get(MediaSessionPlayerUi.class)
|
||||
.ifPresent(ui -> ui.handleMediaButtonIntent(intent));
|
||||
if (player != null) {
|
||||
player.handleIntent(intent);
|
||||
player.UIs().get(MediaSessionPlayerUi.class)
|
||||
.ifPresent(ui -> ui.handleMediaButtonIntent(intent));
|
||||
}
|
||||
|
||||
return START_NOT_STICKY;
|
||||
}
|
||||
@ -87,7 +118,7 @@ public final class PlayerService extends Service {
|
||||
Log.d(TAG, "stopForImmediateReusing() called");
|
||||
}
|
||||
|
||||
if (!player.exoPlayerIsNull()) {
|
||||
if (player != null && !player.exoPlayerIsNull()) {
|
||||
// Releases wifi & cpu, disables keepScreenOn, etc.
|
||||
// We can't just pause the player here because it will make transition
|
||||
// from one stream to a new stream not smooth
|
||||
@ -98,7 +129,7 @@ public final class PlayerService extends Service {
|
||||
@Override
|
||||
public void onTaskRemoved(final Intent rootIntent) {
|
||||
super.onTaskRemoved(rootIntent);
|
||||
if (!player.videoPlayerSelected()) {
|
||||
if (player != null && !player.videoPlayerSelected()) {
|
||||
return;
|
||||
}
|
||||
onDestroy();
|
||||
|
@ -7,7 +7,6 @@ import android.view.View.OnTouchListener
|
||||
import android.widget.ProgressBar
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.appcompat.content.res.AppCompatResources
|
||||
import androidx.core.math.MathUtils
|
||||
import androidx.core.view.isVisible
|
||||
import org.schabi.newpipe.MainActivity
|
||||
import org.schabi.newpipe.R
|
||||
@ -113,7 +112,7 @@ class MainPlayerGestureListener(
|
||||
|
||||
// Update progress bar
|
||||
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())
|
||||
|
||||
// Update brightness
|
||||
|
@ -4,7 +4,7 @@ import android.util.Log
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import android.view.ViewConfiguration
|
||||
import androidx.core.math.MathUtils
|
||||
import androidx.core.view.isVisible
|
||||
import org.schabi.newpipe.MainActivity
|
||||
import org.schabi.newpipe.ktx.AnimationType
|
||||
import org.schabi.newpipe.ktx.animate
|
||||
@ -235,14 +235,16 @@ class PopupPlayerGestureListener(
|
||||
isMoving = true
|
||||
|
||||
val diffX = (movingEvent.rawX - initialEvent.rawX)
|
||||
val posX = MathUtils.clamp(
|
||||
initialPopupX + diffX,
|
||||
0f, (playerUi.screenWidth - playerUi.popupLayoutParams.width).toFloat()
|
||||
val posX = (initialPopupX + diffX).coerceIn(
|
||||
0f,
|
||||
(playerUi.screenWidth - playerUi.popupLayoutParams.width).toFloat()
|
||||
.coerceAtLeast(0f)
|
||||
)
|
||||
val diffY = (movingEvent.rawY - initialEvent.rawY)
|
||||
val posY = MathUtils.clamp(
|
||||
initialPopupY + diffY,
|
||||
0f, (playerUi.screenHeight - playerUi.popupLayoutParams.height).toFloat()
|
||||
val posY = (initialPopupY + diffY).coerceIn(
|
||||
0f,
|
||||
(playerUi.screenHeight - playerUi.popupLayoutParams.height).toFloat()
|
||||
.coerceAtLeast(0f)
|
||||
)
|
||||
|
||||
playerUi.popupLayoutParams.x = posX.toInt()
|
||||
@ -251,8 +253,7 @@ class PopupPlayerGestureListener(
|
||||
// -- Determine if the ClosingOverlayView (red X) has to be shown or hidden --
|
||||
val showClosingOverlayView: Boolean = playerUi.isInsideClosingRadius(movingEvent)
|
||||
// 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.visibility != expectedVisibility) {
|
||||
if (binding.closingOverlay.isVisible != showClosingOverlayView) {
|
||||
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.StreamType;
|
||||
import org.schabi.newpipe.player.playqueue.PlayQueueItem;
|
||||
import org.schabi.newpipe.util.image.ImageStrategy;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
@ -74,7 +75,7 @@ public final class ExceptionTag implements MediaItemTag {
|
||||
|
||||
@Override
|
||||
public String getThumbnailUrl() {
|
||||
return item.getThumbnailUrl();
|
||||
return ImageStrategy.choosePreferredImage(item.getThumbnails());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -81,8 +81,9 @@ public interface MediaItemTag {
|
||||
|
||||
@NonNull
|
||||
default MediaItem asMediaItem() {
|
||||
final String thumbnailUrl = getThumbnailUrl();
|
||||
final MediaMetadata mediaMetadata = new MediaMetadata.Builder()
|
||||
.setArtworkUri(Uri.parse(getThumbnailUrl()))
|
||||
.setArtworkUri(thumbnailUrl == null ? null : Uri.parse(thumbnailUrl))
|
||||
.setArtist(getUploaderName())
|
||||
.setDescription(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.StreamType;
|
||||
import org.schabi.newpipe.extractor.stream.VideoStream;
|
||||
import org.schabi.newpipe.util.image.ImageStrategy;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
@ -95,7 +96,7 @@ public final class StreamInfoTag implements MediaItemTag {
|
||||
|
||||
@Override
|
||||
public String getThumbnailUrl() {
|
||||
return streamInfo.getThumbnailUrl();
|
||||
return ImageStrategy.choosePreferredImage(streamInfo.getThumbnails());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -20,6 +20,7 @@ import com.google.android.exoplayer2.util.Util;
|
||||
import org.schabi.newpipe.player.Player;
|
||||
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
||||
import org.schabi.newpipe.player.playqueue.PlayQueueItem;
|
||||
import org.schabi.newpipe.util.image.ImageStrategy;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
@ -137,9 +138,12 @@ public class PlayQueueNavigator implements MediaSessionConnector.QueueNavigator
|
||||
.putLong(MediaMetadataCompat.METADATA_KEY_NUM_TRACKS, player.getPlayQueue().size());
|
||||
descBuilder.setExtras(additionalMetadata);
|
||||
|
||||
final Uri thumbnailUri = Uri.parse(item.getThumbnailUrl());
|
||||
if (thumbnailUri != null) {
|
||||
descBuilder.setIconUri(thumbnailUri);
|
||||
try {
|
||||
descBuilder.setIconUri(Uri.parse(
|
||||
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();
|
||||
|
@ -17,7 +17,6 @@ import org.schabi.newpipe.player.helper.PlayerHelper;
|
||||
import org.schabi.newpipe.player.ui.PlayerUi;
|
||||
|
||||
public final class NotificationPlayerUi extends PlayerUi {
|
||||
private boolean foregroundNotificationAlreadyCreated = false;
|
||||
private final NotificationUtil notificationUtil;
|
||||
|
||||
public NotificationPlayerUi(@NonNull final Player player) {
|
||||
@ -25,15 +24,6 @@ public final class NotificationPlayerUi extends PlayerUi {
|
||||
notificationUtil = new NotificationUtil(player);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initPlayer() {
|
||||
super.initPlayer();
|
||||
if (!foregroundNotificationAlreadyCreated) {
|
||||
notificationUtil.createNotificationAndStartForeground();
|
||||
foregroundNotificationAlreadyCreated = true;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroy() {
|
||||
super.destroy();
|
||||
@ -122,4 +112,8 @@ public final class NotificationPlayerUi extends PlayerUi {
|
||||
super.onPlayQueueEdited();
|
||||
notificationUtil.createNotificationIfNeededAndUpdate(false);
|
||||
}
|
||||
|
||||
public void createNotificationAndStartForeground() {
|
||||
notificationUtil.createNotificationAndStartForeground();
|
||||
}
|
||||
}
|
||||
|
@ -4,6 +4,7 @@ import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.schabi.newpipe.extractor.InfoItem;
|
||||
import org.schabi.newpipe.extractor.ListExtractor;
|
||||
import org.schabi.newpipe.extractor.ListInfo;
|
||||
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.disposables.Disposable;
|
||||
|
||||
abstract class AbstractInfoPlayQueue<T extends ListInfo<StreamInfoItem>>
|
||||
abstract class AbstractInfoPlayQueue<T extends ListInfo<? extends InfoItem>>
|
||||
extends PlayQueue {
|
||||
boolean isInitial;
|
||||
private boolean isComplete;
|
||||
@ -27,7 +28,13 @@ abstract class AbstractInfoPlayQueue<T extends ListInfo<StreamInfoItem>>
|
||||
private transient Disposable fetchReactor;
|
||||
|
||||
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,
|
||||
@ -72,7 +79,11 @@ abstract class AbstractInfoPlayQueue<T extends ListInfo<StreamInfoItem>>
|
||||
}
|
||||
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 = 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<>() {
|
||||
@Override
|
||||
public void onSubscribe(@NonNull final Disposable d) {
|
||||
@ -101,13 +112,17 @@ abstract class AbstractInfoPlayQueue<T extends ListInfo<StreamInfoItem>>
|
||||
|
||||
@Override
|
||||
public void onSuccess(
|
||||
@NonNull final ListExtractor.InfoItemsPage<StreamInfoItem> result) {
|
||||
@NonNull final ListExtractor.InfoItemsPage<? extends InfoItem> result) {
|
||||
if (!result.hasNextPage()) {
|
||||
isComplete = true;
|
||||
}
|
||||
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 = 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) {
|
||||
if (equalStreams(other)) {
|
||||
return other.getIndex() == getIndex();
|
||||
//noinspection ConstantConditions
|
||||
return other.getIndex() == getIndex(); //NOSONAR: other is not null
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
@ -3,12 +3,14 @@ package org.schabi.newpipe.player.playqueue;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.schabi.newpipe.extractor.Image;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||
import org.schabi.newpipe.extractor.stream.StreamType;
|
||||
import org.schabi.newpipe.util.ExtractorHelper;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.List;
|
||||
|
||||
import io.reactivex.rxjava3.core.Single;
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers;
|
||||
@ -24,7 +26,7 @@ public class PlayQueueItem implements Serializable {
|
||||
private final int serviceId;
|
||||
private final long duration;
|
||||
@NonNull
|
||||
private final String thumbnailUrl;
|
||||
private final List<Image> thumbnails;
|
||||
@NonNull
|
||||
private final String uploader;
|
||||
private final String uploaderUrl;
|
||||
@ -38,7 +40,7 @@ public class PlayQueueItem implements Serializable {
|
||||
|
||||
PlayQueueItem(@NonNull final StreamInfo info) {
|
||||
this(info.getName(), info.getUrl(), info.getServiceId(), info.getDuration(),
|
||||
info.getThumbnailUrl(), info.getUploaderName(),
|
||||
info.getThumbnails(), info.getUploaderName(),
|
||||
info.getUploaderUrl(), info.getStreamType());
|
||||
|
||||
if (info.getStartPosition() > 0) {
|
||||
@ -48,20 +50,20 @@ public class PlayQueueItem implements Serializable {
|
||||
|
||||
PlayQueueItem(@NonNull final StreamInfoItem item) {
|
||||
this(item.getName(), item.getUrl(), item.getServiceId(), item.getDuration(),
|
||||
item.getThumbnailUrl(), item.getUploaderName(),
|
||||
item.getThumbnails(), item.getUploaderName(),
|
||||
item.getUploaderUrl(), item.getStreamType());
|
||||
}
|
||||
|
||||
@SuppressWarnings("ParameterNumber")
|
||||
private PlayQueueItem(@Nullable final String name, @Nullable final String url,
|
||||
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) {
|
||||
this.title = name != null ? name : EMPTY_STRING;
|
||||
this.url = url != null ? url : EMPTY_STRING;
|
||||
this.serviceId = serviceId;
|
||||
this.duration = duration;
|
||||
this.thumbnailUrl = thumbnailUrl != null ? thumbnailUrl : EMPTY_STRING;
|
||||
this.thumbnails = thumbnails;
|
||||
this.uploader = uploader != null ? uploader : EMPTY_STRING;
|
||||
this.uploaderUrl = uploaderUrl;
|
||||
this.streamType = streamType;
|
||||
@ -88,8 +90,8 @@ public class PlayQueueItem implements Serializable {
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public String getThumbnailUrl() {
|
||||
return thumbnailUrl;
|
||||
public List<Image> getThumbnails() {
|
||||
return thumbnails;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
|
@ -6,7 +6,7 @@ import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
|
||||
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;
|
||||
|
||||
public class PlayQueueItemBuilder {
|
||||
@ -33,7 +33,7 @@ public class PlayQueueItemBuilder {
|
||||
holder.itemDurationView.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
PicassoHelper.loadThumbnail(item.getThumbnailUrl()).into(holder.itemThumbnailView);
|
||||
PicassoHelper.loadThumbnail(item.getThumbnails()).into(holder.itemThumbnailView);
|
||||
|
||||
holder.itemRoot.setOnClickListener(view -> {
|
||||
if (onItemClickListener != null) {
|
||||
|
@ -1,5 +1,7 @@
|
||||
package org.schabi.newpipe.player.playqueue;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||
|
||||
@ -20,11 +22,11 @@ public final class SinglePlayQueue extends PlayQueue {
|
||||
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));
|
||||
}
|
||||
|
||||
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());
|
||||
for (final StreamInfoItem item : items) {
|
||||
playQueueItems.add(new PlayQueueItem(item));
|
||||
@ -39,5 +41,7 @@ public final class SinglePlayQueue extends PlayQueue {
|
||||
|
||||
@Override
|
||||
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 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.List;
|
||||
|
@ -434,7 +434,7 @@ public final class MainPlayerUi extends VideoPlayerUi implements View.OnLayoutCh
|
||||
return;
|
||||
}
|
||||
|
||||
final boolean showQueue = playQueue.getStreams().size() > 1;
|
||||
final boolean showQueue = !playQueue.getStreams().isEmpty();
|
||||
final boolean showSegment = !player.getCurrentStreamInfo()
|
||||
.map(StreamInfo::getStreamSegments)
|
||||
.map(List::isEmpty)
|
||||
@ -740,7 +740,7 @@ public final class MainPlayerUi extends VideoPlayerUi implements View.OnLayoutCh
|
||||
String videoUrl = player.getVideoUrl();
|
||||
videoUrl += ("&t=" + seconds);
|
||||
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.appcompat.content.res.AppCompatResources;
|
||||
import androidx.appcompat.view.ContextThemeWrapper;
|
||||
import androidx.appcompat.widget.AppCompatImageButton;
|
||||
import androidx.appcompat.widget.PopupMenu;
|
||||
import androidx.core.graphics.BitmapCompat;
|
||||
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)
|
||||
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
|
||||
@ -222,7 +226,7 @@ public abstract class VideoPlayerUi extends PlayerUi implements SeekBar.OnSeekBa
|
||||
final PlayQueueItem currentItem = player.getCurrentItem();
|
||||
if (currentItem != null) {
|
||||
ShareUtils.shareText(context, currentItem.getTitle(),
|
||||
player.getVideoUrlAtCurrentTime(), currentItem.getThumbnailUrl());
|
||||
player.getVideoUrlAtCurrentTime(), currentItem.getThumbnails());
|
||||
}
|
||||
}));
|
||||
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
|
||||
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
|
||||
|
||||
|
||||
@ -785,7 +812,7 @@ public abstract class VideoPlayerUi extends PlayerUi implements SeekBar.OnSeekBa
|
||||
animate(binding.loadingPanel, true, 0);
|
||||
animate(binding.surfaceForeground, true, 100);
|
||||
|
||||
binding.playPauseButton.setImageResource(R.drawable.ic_play_arrow);
|
||||
updatePlayPauseButton(PlayButtonAction.PLAY);
|
||||
animatePlayButtons(false, 100);
|
||||
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,
|
||||
() -> {
|
||||
binding.playPauseButton.setImageResource(R.drawable.ic_pause);
|
||||
updatePlayPauseButton(PlayButtonAction.PAUSE);
|
||||
animatePlayButtons(true, 200);
|
||||
if (!isAnyListViewOpen()) {
|
||||
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,
|
||||
() -> {
|
||||
binding.playPauseButton.setImageResource(R.drawable.ic_play_arrow);
|
||||
updatePlayPauseButton(PlayButtonAction.PLAY);
|
||||
animatePlayButtons(true, 200);
|
||||
if (!isAnyListViewOpen()) {
|
||||
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,
|
||||
() -> {
|
||||
binding.playPauseButton.setImageResource(R.drawable.ic_replay);
|
||||
updatePlayPauseButton(PlayButtonAction.REPLAY);
|
||||
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.StoredFileHelper;
|
||||
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.image.PreferredImageQuality;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
@ -105,9 +107,11 @@ public class ContentSettingsFragment extends BasePreferenceFragment {
|
||||
.getPreferredContentCountry(requireContext());
|
||||
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) -> {
|
||||
PicassoHelper.setShouldLoadImages((Boolean) newValue);
|
||||
ImageStrategy.setPreferredImageQuality(PreferredImageQuality
|
||||
.fromPreferenceKey(requireContext(), (String) newValue));
|
||||
try {
|
||||
PicassoHelper.clearCache(preference.getContext());
|
||||
Toast.makeText(preference.getContext(),
|
||||
|
@ -2,6 +2,7 @@ package org.schabi.newpipe.settings
|
||||
|
||||
import android.content.SharedPreferences
|
||||
import android.util.Log
|
||||
import org.schabi.newpipe.MainActivity.DEBUG
|
||||
import org.schabi.newpipe.streams.io.SharpOutputStream
|
||||
import org.schabi.newpipe.streams.io.StoredFileHelper
|
||||
import org.schabi.newpipe.util.ZipHelper
|
||||
@ -32,7 +33,9 @@ class ContentSettingsManager(private val fileLocator: NewPipeFileLocator) {
|
||||
output.flush()
|
||||
}
|
||||
} 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")
|
||||
@ -105,9 +108,13 @@ class ContentSettingsManager(private val fileLocator: NewPipeFileLocator) {
|
||||
preferenceEditor.commit()
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
Log.e(TAG, "Unable to loadSharedPreferences", e)
|
||||
if (DEBUG) {
|
||||
Log.e(TAG, "Unable to loadSharedPreferences", e)
|
||||
}
|
||||
} 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.UserAction;
|
||||
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;
|
||||
|
||||
|
@ -44,21 +44,11 @@ public final class NewPipeSettings {
|
||||
private NewPipeSettings() { }
|
||||
|
||||
public static void initSettings(final Context context) {
|
||||
// check if there are entries in the prefs to determine whether this is the first app run
|
||||
Boolean isFirstRun = null;
|
||||
final Set<String> prefsKeys = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
.getAll().keySet();
|
||||
for (final String key: prefsKeys) {
|
||||
// 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;
|
||||
}
|
||||
// check if the last used preference version is set
|
||||
// to determine whether this is the first app run
|
||||
final int lastUsedPrefVersion = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
.getInt(context.getString(R.string.last_used_preferences_version), -1);
|
||||
final boolean isFirstRun = lastUsedPrefVersion == -1;
|
||||
|
||||
// first run migrations, then setDefaultValues, since the latter requires the correct types
|
||||
SettingMigrations.runMigrationsIfNeeded(context, isFirstRun);
|
||||
|
@ -19,7 +19,7 @@ import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.database.subscription.SubscriptionEntity;
|
||||
import org.schabi.newpipe.error.ErrorUtil;
|
||||
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 java.util.List;
|
||||
|
@ -25,7 +25,7 @@ import org.schabi.newpipe.error.ErrorUtil;
|
||||
import org.schabi.newpipe.error.UserAction;
|
||||
import org.schabi.newpipe.local.playlist.LocalPlaylistManager;
|
||||
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.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.
|
||||
* <p>
|
||||
@ -140,12 +154,13 @@ public final class SettingMigrations {
|
||||
MIGRATION_2_3,
|
||||
MIGRATION_3_4,
|
||||
MIGRATION_4_5,
|
||||
MIGRATION_5_6,
|
||||
};
|
||||
|
||||
/**
|
||||
* 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,
|
||||
|
@ -13,6 +13,7 @@ import androidx.preference.ListPreference;
|
||||
import com.google.android.material.snackbar.Snackbar;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.util.ListHelper;
|
||||
import org.schabi.newpipe.util.PermissionHelper;
|
||||
|
||||
import java.util.LinkedList;
|
||||
@ -26,7 +27,7 @@ public class VideoAudioSettingsFragment extends BasePreferenceFragment {
|
||||
addPreferencesFromResourceRegistry();
|
||||
|
||||
updateSeekOptions();
|
||||
|
||||
updateResolutionOptions();
|
||||
listener = (sharedPreferences, key) -> {
|
||||
|
||||
// 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)) {
|
||||
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
|
||||
* according to language and inexact seek setting.
|
||||
|
@ -73,10 +73,8 @@ public final class TabsManager {
|
||||
|
||||
private SharedPreferences.OnSharedPreferenceChangeListener getPreferenceChangeListener() {
|
||||
return (sp, key) -> {
|
||||
if (savedTabsKey.equals(key)) {
|
||||
if (savedTabsChangeListener != null) {
|
||||
savedTabsChangeListener.onTabsChanged();
|
||||
}
|
||||
if (savedTabsKey.equals(key) && savedTabsChangeListener != null) {
|
||||
savedTabsChangeListener.onTabsChanged();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ import android.content.Intent;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.provider.DocumentsContract;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
@ -14,21 +15,27 @@ import androidx.documentfile.provider.DocumentFile;
|
||||
import org.schabi.newpipe.settings.NewPipeSettings;
|
||||
import org.schabi.newpipe.util.FilePickerActivityHelper;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
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.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.Root.COLUMN_DOCUMENT_ID;
|
||||
import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty;
|
||||
|
||||
public class StoredDirectoryHelper {
|
||||
private static final String TAG = StoredDirectoryHelper.class.getSimpleName();
|
||||
public static final int PERMISSION_FLAGS = Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||
| Intent.FLAG_GRANT_WRITE_URI_PERMISSION;
|
||||
|
||||
private File ioTree;
|
||||
private Path ioTree;
|
||||
private DocumentFile docTree;
|
||||
|
||||
private Context context;
|
||||
@ -40,7 +47,7 @@ public class StoredDirectoryHelper {
|
||||
this.tag = tag;
|
||||
|
||||
if (ContentResolver.SCHEME_FILE.equalsIgnoreCase(path.getScheme())) {
|
||||
this.ioTree = new File(URI.create(path.toString()));
|
||||
ioTree = Paths.get(URI.create(path.toString()));
|
||||
return;
|
||||
}
|
||||
|
||||
@ -64,13 +71,17 @@ public class StoredDirectoryHelper {
|
||||
}
|
||||
|
||||
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 lcFilename = filename[0].toLowerCase();
|
||||
final String lcFileName = filename[0].toLowerCase();
|
||||
|
||||
if (docTree == null) {
|
||||
for (final File file : ioTree.listFiles()) {
|
||||
addIfStartWith(matches, lcFilename, file.getName());
|
||||
try (Stream<Path> stream = Files.list(ioTree)) {
|
||||
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 {
|
||||
// warning: SAF file listing is very slow
|
||||
@ -82,37 +93,37 @@ public class StoredDirectoryHelper {
|
||||
final ContentResolver cr = context.getContentResolver();
|
||||
|
||||
try (Cursor cursor = cr.query(docTreeChildren, projection, selection,
|
||||
new String[]{lcFilename}, null)) {
|
||||
new String[]{lcFileName}, null)) {
|
||||
if (cursor != null) {
|
||||
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);
|
||||
} 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
|
||||
if (lcName != null) {
|
||||
return createFile(name, mime, true);
|
||||
// check if the filename is in use
|
||||
String lcName = name.toLowerCase();
|
||||
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);
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
@ -141,11 +152,11 @@ public class StoredDirectoryHelper {
|
||||
}
|
||||
|
||||
public Uri getUri() {
|
||||
return docTree == null ? Uri.fromFile(ioTree) : docTree.getUri();
|
||||
return docTree == null ? Uri.fromFile(ioTree.toFile()) : docTree.getUri();
|
||||
}
|
||||
|
||||
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
|
||||
* necessary but nonexistent parent directories. Note that if this
|
||||
* operation fails it may have succeeded in creating some of the necessary
|
||||
* necessary but nonexistent parent directories.
|
||||
* Note that if this operation fails it may have succeeded in creating some of the necessary
|
||||
* parent directories.
|
||||
*
|
||||
* @return <code>true</code> if and only if the directory was created,
|
||||
@ -169,7 +180,12 @@ public class StoredDirectoryHelper {
|
||||
*/
|
||||
public boolean mkdirs() {
|
||||
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()) {
|
||||
@ -206,8 +222,8 @@ public class StoredDirectoryHelper {
|
||||
|
||||
public Uri findFile(final String filename) {
|
||||
if (docTree == null) {
|
||||
final File res = new File(ioTree, filename);
|
||||
return res.exists() ? Uri.fromFile(res) : null;
|
||||
final Path res = ioTree.resolve(filename);
|
||||
return Files.exists(res) ? Uri.fromFile(res.toFile()) : null;
|
||||
}
|
||||
|
||||
final DocumentFile res = findFileSAFHelper(context, docTree, filename);
|
||||
@ -215,7 +231,7 @@ public class StoredDirectoryHelper {
|
||||
}
|
||||
|
||||
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
|
||||
@Override
|
||||
public String toString() {
|
||||
return (docTree == null ? Uri.fromFile(ioTree) : docTree.getUri()).toString();
|
||||
return (docTree == null ? Uri.fromFile(ioTree.toFile()) : docTree.getUri()).toString();
|
||||
}
|
||||
|
||||
////////////////////
|
||||
// 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) {
|
||||
if (isNullOrEmpty(str)) {
|
||||
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) {
|
||||
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) {
|
||||
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.Serializable;
|
||||
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.FileStreamSAF;
|
||||
@ -36,7 +39,7 @@ public class StoredFileHelper implements Serializable {
|
||||
|
||||
private transient DocumentFile docFile;
|
||||
private transient DocumentFile docTree;
|
||||
private transient File ioFile;
|
||||
private transient Path ioPath;
|
||||
private transient Context context;
|
||||
|
||||
protected String source;
|
||||
@ -49,7 +52,8 @@ public class StoredFileHelper implements Serializable {
|
||||
|
||||
public StoredFileHelper(final Context context, final Uri uri, final String mime) {
|
||||
if (FilePickerActivityHelper.isOwnFileUri(context, uri)) {
|
||||
ioFile = Utils.getFileForUri(uri);
|
||||
final File ioFile = Utils.getFileForUri(uri);
|
||||
ioPath = ioFile.toPath();
|
||||
source = Uri.fromFile(ioFile).toString();
|
||||
} else {
|
||||
docFile = DocumentFile.fromSingleUri(context, uri);
|
||||
@ -100,26 +104,18 @@ public class StoredFileHelper implements Serializable {
|
||||
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 {
|
||||
this.ioFile = new File(location, filename);
|
||||
ioPath = location.resolve(filename);
|
||||
|
||||
if (this.ioFile.exists()) {
|
||||
if (!this.ioFile.isFile() && !this.ioFile.delete()) {
|
||||
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");
|
||||
}
|
||||
}
|
||||
Files.deleteIfExists(ioPath);
|
||||
Files.createFile(ioPath);
|
||||
|
||||
this.source = Uri.fromFile(this.ioFile).toString();
|
||||
this.sourceTree = Uri.fromFile(location).toString();
|
||||
source = Uri.fromFile(ioPath.toFile()).toString();
|
||||
sourceTree = Uri.fromFile(location.toFile()).toString();
|
||||
|
||||
this.srcName = ioFile.getName();
|
||||
this.srcType = mime;
|
||||
srcName = ioPath.getFileName().toString();
|
||||
srcType = mime;
|
||||
}
|
||||
|
||||
public StoredFileHelper(final Context context, @Nullable final Uri parent,
|
||||
@ -129,12 +125,12 @@ public class StoredFileHelper implements Serializable {
|
||||
|
||||
if (path.getScheme() == null
|
||||
|| path.getScheme().equalsIgnoreCase(ContentResolver.SCHEME_FILE)) {
|
||||
this.ioFile = new File(URI.create(this.source));
|
||||
this.ioPath = Paths.get(URI.create(this.source));
|
||||
} else {
|
||||
final DocumentFile file = DocumentFile.fromSingleUri(context, path);
|
||||
|
||||
if (file == null) {
|
||||
throw new RuntimeException("SAF not available");
|
||||
throw new IOException("SAF not available");
|
||||
}
|
||||
|
||||
this.context = context;
|
||||
@ -187,7 +183,7 @@ public class StoredFileHelper implements Serializable {
|
||||
assertValid();
|
||||
|
||||
if (docFile == null) {
|
||||
return new FileStream(ioFile);
|
||||
return new FileStream(ioPath.toFile());
|
||||
} else {
|
||||
return new FileStreamSAF(context.getContentResolver(), docFile.getUri());
|
||||
}
|
||||
@ -211,7 +207,7 @@ public class StoredFileHelper implements Serializable {
|
||||
public Uri getUri() {
|
||||
assertValid();
|
||||
|
||||
return docFile == null ? Uri.fromFile(ioFile) : docFile.getUri();
|
||||
return docFile == null ? Uri.fromFile(ioPath.toFile()) : docFile.getUri();
|
||||
}
|
||||
|
||||
public Uri getParentUri() {
|
||||
@ -233,7 +229,12 @@ public class StoredFileHelper implements Serializable {
|
||||
return true;
|
||||
}
|
||||
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();
|
||||
@ -252,21 +253,30 @@ public class StoredFileHelper implements Serializable {
|
||||
public long length() {
|
||||
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() {
|
||||
if (source == null) {
|
||||
return false;
|
||||
}
|
||||
return docFile == null ? ioFile.canWrite() : docFile.canWrite();
|
||||
return docFile == null ? Files.isWritable(ioPath) : docFile.canWrite();
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
if (source == null) {
|
||||
return srcName;
|
||||
} else if (docFile == null) {
|
||||
return ioFile.getName();
|
||||
return ioPath.getFileName().toString();
|
||||
}
|
||||
|
||||
final String name = docFile.getName();
|
||||
@ -287,12 +297,11 @@ public class StoredFileHelper implements Serializable {
|
||||
}
|
||||
|
||||
public boolean existsAsFile() {
|
||||
if (source == null || (docFile == null && ioFile == null)) {
|
||||
if (source == null || (docFile == null && ioPath == null)) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "existsAsFile called but something is null: source = ["
|
||||
+ (source == null ? "null => storage is invalid" : source)
|
||||
+ "], docFile = [" + (docFile == null ? "null" : docFile)
|
||||
+ "], ioFile = [" + (ioFile == null ? "null" : ioFile) + "]");
|
||||
+ "], docFile = [" + docFile + "], ioPath = [" + ioPath + "]");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@ -300,7 +309,7 @@ public class StoredFileHelper implements Serializable {
|
||||
// WARNING: DocumentFile.exists() and DocumentFile.isFile() methods are slow
|
||||
// docFile.isVirtual() means it is non-physical?
|
||||
return docFile == null
|
||||
? (ioFile.exists() && ioFile.isFile())
|
||||
? Files.isRegularFile(ioPath)
|
||||
: (docFile.exists() && docFile.isFile());
|
||||
}
|
||||
|
||||
@ -310,8 +319,10 @@ public class StoredFileHelper implements Serializable {
|
||||
|
||||
if (docFile == null) {
|
||||
try {
|
||||
result = ioFile.createNewFile();
|
||||
Files.createFile(ioPath);
|
||||
result = true;
|
||||
} catch (final IOException e) {
|
||||
Log.e(TAG, "Exception while creating " + ioPath, e);
|
||||
return false;
|
||||
}
|
||||
} else if (docTree == null) {
|
||||
@ -332,7 +343,8 @@ public class StoredFileHelper implements Serializable {
|
||||
}
|
||||
|
||||
if (result) {
|
||||
source = (docFile == null ? Uri.fromFile(ioFile) : docFile.getUri()).toString();
|
||||
source = (docFile == null ? Uri.fromFile(ioPath.toFile()) : docFile.getUri())
|
||||
.toString();
|
||||
srcName = getName();
|
||||
srcType = getType();
|
||||
}
|
||||
@ -352,7 +364,7 @@ public class StoredFileHelper implements Serializable {
|
||||
|
||||
docTree = null;
|
||||
docFile = null;
|
||||
ioFile = null;
|
||||
ioPath = null;
|
||||
context = null;
|
||||
}
|
||||
|
||||
@ -383,7 +395,7 @@ public class StoredFileHelper implements Serializable {
|
||||
}
|
||||
|
||||
if (this.isDirect()) {
|
||||
return this.ioFile.getPath().equalsIgnoreCase(storage.ioFile.getPath());
|
||||
return this.ioPath.equals(storage.ioPath);
|
||||
}
|
||||
|
||||
return DocumentsContract.getDocumentId(this.docFile.getUri())
|
||||
|
@ -13,7 +13,7 @@ import androidx.annotation.Nullable;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
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.util.List;
|
||||
@ -75,15 +75,15 @@ public class AudioTrackAdapter extends BaseAdapter {
|
||||
}
|
||||
|
||||
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,
|
||||
@Nullable final Context context) {
|
||||
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;
|
||||
}
|
||||
|
||||
|
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.InfoItem;
|
||||
import org.schabi.newpipe.extractor.ListExtractor.InfoItemsPage;
|
||||
import org.schabi.newpipe.extractor.ListInfo;
|
||||
import org.schabi.newpipe.extractor.MetaInfo;
|
||||
import org.schabi.newpipe.extractor.NewPipe;
|
||||
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.tabs.ChannelTabInfo;
|
||||
import org.schabi.newpipe.extractor.comments.CommentsInfo;
|
||||
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.linkhandler.ListLinkHandler;
|
||||
import org.schabi.newpipe.extractor.playlist.PlaylistInfo;
|
||||
import org.schabi.newpipe.extractor.search.SearchInfo;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||
@ -127,28 +125,24 @@ public final class ExtractorHelper {
|
||||
ChannelInfo.getInfo(NewPipe.getService(serviceId), url)));
|
||||
}
|
||||
|
||||
public static Single<InfoItemsPage<StreamInfoItem>> getMoreChannelItems(final int serviceId,
|
||||
final String url,
|
||||
final Page nextPage) {
|
||||
public static Single<ChannelTabInfo> getChannelTab(final int serviceId,
|
||||
final ListLinkHandler listLinkHandler,
|
||||
final boolean forceLoad) {
|
||||
checkServiceId(serviceId);
|
||||
return Single.fromCallable(() ->
|
||||
ChannelInfo.getMoreItems(NewPipe.getService(serviceId), url, nextPage));
|
||||
return checkCache(forceLoad, serviceId,
|
||||
listLinkHandler.getUrl(), InfoItem.InfoType.CHANNEL,
|
||||
Single.fromCallable(() ->
|
||||
ChannelTabInfo.getInfo(NewPipe.getService(serviceId), listLinkHandler)));
|
||||
}
|
||||
|
||||
public static Single<ListInfo<StreamInfoItem>> getFeedInfoFallbackToChannelInfo(
|
||||
final int serviceId, final String url) {
|
||||
final Maybe<ListInfo<StreamInfoItem>> maybeFeedInfo = Maybe.fromCallable(() -> {
|
||||
final StreamingService service = NewPipe.getService(serviceId);
|
||||
final FeedExtractor feedExtractor = service.getFeedExtractor(url);
|
||||
|
||||
if (feedExtractor == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return FeedInfo.getInfo(feedExtractor);
|
||||
});
|
||||
|
||||
return maybeFeedInfo.switchIfEmpty(getChannelInfo(serviceId, url, true));
|
||||
public static Single<InfoItemsPage<InfoItem>> getMoreChannelTabItems(
|
||||
final int serviceId,
|
||||
final ListLinkHandler listLinkHandler,
|
||||
final Page nextPage) {
|
||||
checkServiceId(serviceId);
|
||||
return Single.fromCallable(() ->
|
||||
ChannelTabInfo.getMoreItems(NewPipe.getService(serviceId),
|
||||
listLinkHandler, nextPage));
|
||||
}
|
||||
|
||||
public static Single<CommentsInfo> getCommentsInfo(final int serviceId, final String url,
|
||||
@ -229,7 +223,7 @@ public final class ExtractorHelper {
|
||||
load = actualLoadFromNetwork;
|
||||
} else {
|
||||
load = Maybe.concat(ExtractorHelper.loadFromCache(serviceId, url, infoType),
|
||||
actualLoadFromNetwork.toMaybe())
|
||||
actualLoadFromNetwork.toMaybe())
|
||||
.firstElement() // Take the first valid
|
||||
.toSingle();
|
||||
}
|
||||
@ -240,10 +234,10 @@ public final class ExtractorHelper {
|
||||
/**
|
||||
* Default implementation uses the {@link InfoCache} to get cached results.
|
||||
*
|
||||
* @param <I> the item type's class that extends {@link Info}
|
||||
* @param serviceId the service to load from
|
||||
* @param url the URL to load
|
||||
* @param infoType the {@link InfoItem.InfoType} of the item
|
||||
* @param <I> the item type's class that extends {@link Info}
|
||||
* @param serviceId the service to load from
|
||||
* @param url the URL to load
|
||||
* @param infoType the {@link InfoItem.InfoType} of the item
|
||||
* @return a {@link Single} that loads the item
|
||||
*/
|
||||
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,
|
||||
* 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
|
||||
* @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 disposables disposables created by the method are added here and their lifecycle
|
||||
* should be handled by the calling class
|
||||
* @param disposables disposables created by the method are added here and their lifecycle
|
||||
* should be handled by the calling class
|
||||
*/
|
||||
public static void showMetaInfoInTextView(@Nullable final List<MetaInfo> metaInfos,
|
||||
final TextView metaInfoTextView,
|
||||
@ -287,7 +282,7 @@ public final class ExtractorHelper {
|
||||
final Context context = metaInfoTextView.getContext();
|
||||
if (metaInfos == null || metaInfos.isEmpty()
|
||||
|| !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);
|
||||
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