mirror of
https://github.com/TeamNewPipe/NewPipe
synced 2025-09-24 20:20:51 +02:00
Compare commits
132 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
50f3131f1a | ||
![]() |
fcaebc838e | ||
![]() |
cde32a8aed | ||
![]() |
ec3efea05a | ||
![]() |
5ac71e0579 | ||
![]() |
d04ecbcb0a | ||
![]() |
e4987d9a59 | ||
![]() |
155c6e94a3 | ||
![]() |
4e285a4e70 | ||
![]() |
9c00e681bb | ||
![]() |
160891592b | ||
![]() |
085d1e0d38 | ||
![]() |
4ee1cd5826 | ||
![]() |
dc7fce86a5 | ||
![]() |
10c9661369 | ||
![]() |
3901ffca17 | ||
![]() |
cbd3308da6 | ||
![]() |
0ad6b3b88e | ||
![]() |
4e87f5aabc | ||
![]() |
2019af831a | ||
![]() |
1e076ea63d | ||
![]() |
4863084fa2 | ||
![]() |
7ba79171c7 | ||
![]() |
e3c2aea3cc | ||
![]() |
21c9530e8b | ||
![]() |
036196a487 | ||
![]() |
73855cacb7 | ||
![]() |
8dad6d7e1c | ||
![]() |
e5ffa2aa09 | ||
![]() |
8445c381c5 | ||
![]() |
fa46b7bf85 | ||
![]() |
7ce2250d85 | ||
![]() |
ef20d9b91a | ||
![]() |
fbee310261 | ||
![]() |
7d6bf4b0ca | ||
![]() |
210834fbe9 | ||
![]() |
a59660f421 | ||
![]() |
be5af0b777 | ||
![]() |
75e5fe7d27 | ||
![]() |
2985258074 | ||
![]() |
911ac65d1e | ||
![]() |
d2967f514b | ||
![]() |
a68c6a2cfc | ||
![]() |
733f6aae85 | ||
![]() |
1daece3bee | ||
![]() |
adddd48c1d | ||
![]() |
8c870cd3ca | ||
![]() |
bd5eda92a7 | ||
![]() |
cf09cef6d8 | ||
![]() |
b3f9f8275d | ||
![]() |
9597d474d0 | ||
![]() |
e6f2e9791c | ||
![]() |
31b1370270 | ||
![]() |
064a4ce798 | ||
![]() |
ac5843edb0 | ||
![]() |
a1f64e4774 | ||
![]() |
21d2ae709f | ||
![]() |
c5e509f069 | ||
![]() |
761c0ff9ac | ||
![]() |
ce8289e753 | ||
![]() |
2dd4f8b04a | ||
![]() |
b4615f7655 | ||
![]() |
fcaa787060 | ||
![]() |
23c1fc3544 | ||
![]() |
a4037a8268 | ||
![]() |
61ee1c61df | ||
![]() |
69f95f4148 | ||
![]() |
212a413e93 | ||
![]() |
de4b5a8f0f | ||
![]() |
1228ce277f | ||
![]() |
bd6fdd625a | ||
![]() |
7de17ad949 | ||
![]() |
7ab11a8379 | ||
![]() |
70e0085596 | ||
![]() |
f9ccc19df5 | ||
![]() |
5c69568c7f | ||
![]() |
1d69bd48be | ||
![]() |
5b435c586e | ||
![]() |
71e46d1eca | ||
![]() |
238aff7c31 | ||
![]() |
a1435bd566 | ||
![]() |
59d8c570b7 | ||
![]() |
8f34f69397 | ||
![]() |
47af21d248 | ||
![]() |
c2a3c1cb8f | ||
![]() |
1e2d76a686 | ||
![]() |
34468c16ad | ||
![]() |
b84c6b4b32 | ||
![]() |
8395cf8d5a | ||
![]() |
c2bf7f09ce | ||
![]() |
c2762d3b5e | ||
![]() |
01d996a5c0 | ||
![]() |
50739277c4 | ||
![]() |
0fef4e6e2e | ||
![]() |
218012558a | ||
![]() |
e40e86500b | ||
![]() |
6f0942ac6e | ||
![]() |
a67927c29c | ||
![]() |
7e50eed95e | ||
![]() |
173b6c3f00 | ||
![]() |
7646c683b5 | ||
![]() |
047fe21c14 | ||
![]() |
b59a601b52 | ||
![]() |
bb495f567c | ||
![]() |
aa1db617d5 | ||
![]() |
ec5cfe0019 | ||
![]() |
fd5626e9e2 | ||
![]() |
53bf3420e7 | ||
![]() |
127a27315e | ||
![]() |
671441bdf8 | ||
![]() |
8ea98b64aa | ||
![]() |
4904b48f5c | ||
![]() |
a311519314 | ||
![]() |
1dc146322c | ||
![]() |
0f551baf37 | ||
![]() |
b9190eddfe | ||
![]() |
44dada9e60 | ||
![]() |
1b8c517e3e | ||
![]() |
20602889be | ||
![]() |
4b06536582 | ||
![]() |
621b38c98b | ||
![]() |
321cf8bf7d | ||
![]() |
762cdc812c | ||
![]() |
dae5aa38a8 | ||
![]() |
7d42e50f5b | ||
![]() |
a4c083e7f9 | ||
![]() |
e4f202834c | ||
![]() |
6e0c380409 | ||
![]() |
4cdf6eda2c | ||
![]() |
652d50173e | ||
![]() |
248ca5ee12 | ||
![]() |
2e771cd65a |
@@ -17,7 +17,7 @@
|
|||||||
<p align="center"><a href="https://newpipe.net">Website</a> • <a href="https://newpipe.net/blog/">Blog</a> • <a href="https://newpipe.net/FAQ/">FAQ</a> • <a href="https://newpipe.net/press/">Press</a></p>
|
<p align="center"><a href="https://newpipe.net">Website</a> • <a href="https://newpipe.net/blog/">Blog</a> • <a href="https://newpipe.net/FAQ/">FAQ</a> • <a href="https://newpipe.net/press/">Press</a></p>
|
||||||
<hr>
|
<hr>
|
||||||
|
|
||||||
*Read this in other languages: [English](README.md), [Español](doc/README.es.md), [한국어](doc/README.ko.md), [Soomaali](doc/README.so.md), [Português Brasil](doc/README.pt_BR.md), [Polski](doc/README.pl.md), [日本語](doc/README.ja.md), [Română](doc/README.ro.md), [Türkçe](doc/README.tr.md), [正體中文](doc/README.zh_TW.md).*
|
*Read this in other languages: [English](README.md), [Español](doc/README.es.md), [हिन्दी](doc/README.hi.md), [한국어](doc/README.ko.md), [Soomaali](doc/README.so.md), [Português Brasil](doc/README.pt_BR.md), [Polski](doc/README.pl.md), [日本語](doc/README.ja.md), [Română](doc/README.ro.md), [Türkçe](doc/README.tr.md), [正體中文](doc/README.zh_TW.md).*
|
||||||
|
|
||||||
<b>WARNING: THIS IS A BETA VERSION, THEREFORE YOU MAY ENCOUNTER BUGS. IF YOU DO, OPEN AN ISSUE VIA OUR GITHUB REPOSITORY.</b>
|
<b>WARNING: THIS IS A BETA VERSION, THEREFORE YOU MAY ENCOUNTER BUGS. IF YOU DO, OPEN AN ISSUE VIA OUR GITHUB REPOSITORY.</b>
|
||||||
|
|
||||||
|
@@ -16,8 +16,8 @@ android {
|
|||||||
resValue "string", "app_name", "NewPipe"
|
resValue "string", "app_name", "NewPipe"
|
||||||
minSdk 19
|
minSdk 19
|
||||||
targetSdk 29
|
targetSdk 29
|
||||||
versionCode 986
|
versionCode 988
|
||||||
versionName "0.23.0"
|
versionName "0.23.2"
|
||||||
|
|
||||||
multiDexEnabled true
|
multiDexEnabled true
|
||||||
|
|
||||||
@@ -107,7 +107,7 @@ ext {
|
|||||||
icepickVersion = '3.2.0'
|
icepickVersion = '3.2.0'
|
||||||
exoPlayerVersion = '2.17.1'
|
exoPlayerVersion = '2.17.1'
|
||||||
googleAutoServiceVersion = '1.0.1'
|
googleAutoServiceVersion = '1.0.1'
|
||||||
groupieVersion = '2.10.0'
|
groupieVersion = '2.10.1'
|
||||||
markwonVersion = '4.6.2'
|
markwonVersion = '4.6.2'
|
||||||
|
|
||||||
leakCanaryVersion = '2.5'
|
leakCanaryVersion = '2.5'
|
||||||
@@ -190,7 +190,7 @@ dependencies {
|
|||||||
// name and the commit hash with the commit hash of the (pushed) commit you want to test
|
// name and the commit hash with the commit hash of the (pushed) commit you want to test
|
||||||
// This works thanks to JitPack: https://jitpack.io/
|
// This works thanks to JitPack: https://jitpack.io/
|
||||||
implementation 'com.github.TeamNewPipe:nanojson:1d9e1aea9049fc9f85e68b43ba39fe7be1c1f751'
|
implementation 'com.github.TeamNewPipe:nanojson:1d9e1aea9049fc9f85e68b43ba39fe7be1c1f751'
|
||||||
implementation 'com.github.TeamNewPipe:NewPipeExtractor:ac1c22d81c65b7b0c5427f4e1989f5256d617f32'
|
implementation 'com.github.TeamNewPipe:NewPipeExtractor:76aad92fa54524f20c3338ab568c9cd6b50c9d33'
|
||||||
|
|
||||||
/** Checkstyle **/
|
/** Checkstyle **/
|
||||||
checkstyle "com.puppycrawl.tools:checkstyle:${checkstyleVersion}"
|
checkstyle "com.puppycrawl.tools:checkstyle:${checkstyleVersion}"
|
||||||
@@ -221,10 +221,9 @@ dependencies {
|
|||||||
// https://developer.android.com/jetpack/androidx/releases/viewpager2#1.1.0-alpha01
|
// https://developer.android.com/jetpack/androidx/releases/viewpager2#1.1.0-alpha01
|
||||||
implementation 'androidx.viewpager2:viewpager2:1.1.0-beta01'
|
implementation 'androidx.viewpager2:viewpager2:1.1.0-beta01'
|
||||||
implementation 'androidx.webkit:webkit:1.4.0'
|
implementation 'androidx.webkit:webkit:1.4.0'
|
||||||
implementation 'com.google.android.material:material:1.5.0'
|
|
||||||
implementation "androidx.work:work-runtime:${androidxWorkVersion}"
|
|
||||||
implementation "androidx.work:work-runtime-ktx:${androidxWorkVersion}"
|
implementation "androidx.work:work-runtime-ktx:${androidxWorkVersion}"
|
||||||
implementation "androidx.work:work-rxjava3:${androidxWorkVersion}"
|
implementation "androidx.work:work-rxjava3:${androidxWorkVersion}"
|
||||||
|
implementation 'com.google.android.material:material:1.5.0'
|
||||||
|
|
||||||
/** Third-party libraries **/
|
/** Third-party libraries **/
|
||||||
// Instance state boilerplate elimination
|
// Instance state boilerplate elimination
|
||||||
@@ -262,7 +261,7 @@ dependencies {
|
|||||||
implementation "com.nononsenseapps:filepicker:4.2.1"
|
implementation "com.nononsenseapps:filepicker:4.2.1"
|
||||||
|
|
||||||
// Crash reporting
|
// Crash reporting
|
||||||
implementation "ch.acra:acra-core:5.8.4"
|
implementation "ch.acra:acra-core:5.9.3"
|
||||||
|
|
||||||
// Properly restarting
|
// Properly restarting
|
||||||
implementation 'com.jakewharton:process-phoenix:2.1.2'
|
implementation 'com.jakewharton:process-phoenix:2.1.2'
|
||||||
|
@@ -91,7 +91,12 @@ class StreamItemAdapterTest {
|
|||||||
context,
|
context,
|
||||||
StreamItemAdapter.StreamSizeWrapper(
|
StreamItemAdapter.StreamSizeWrapper(
|
||||||
(0 until 5).map {
|
(0 until 5).map {
|
||||||
SubtitlesStream(MediaFormat.SRT, "pt-BR", "https://example.com", false)
|
SubtitlesStream.Builder()
|
||||||
|
.setContent("https://example.com", true)
|
||||||
|
.setMediaFormat(MediaFormat.SRT)
|
||||||
|
.setLanguageCode("pt-BR")
|
||||||
|
.setAutoGenerated(false)
|
||||||
|
.build()
|
||||||
},
|
},
|
||||||
context
|
context
|
||||||
),
|
),
|
||||||
@@ -108,7 +113,14 @@ class StreamItemAdapterTest {
|
|||||||
val adapter = StreamItemAdapter<AudioStream, Stream>(
|
val adapter = StreamItemAdapter<AudioStream, Stream>(
|
||||||
context,
|
context,
|
||||||
StreamItemAdapter.StreamSizeWrapper(
|
StreamItemAdapter.StreamSizeWrapper(
|
||||||
(0 until 5).map { AudioStream("https://example.com/$it", MediaFormat.OPUS, 192) },
|
(0 until 5).map {
|
||||||
|
AudioStream.Builder()
|
||||||
|
.setId(Stream.ID_UNKNOWN)
|
||||||
|
.setContent("https://example.com/$it", true)
|
||||||
|
.setMediaFormat(MediaFormat.OPUS)
|
||||||
|
.setAverageBitrate(192)
|
||||||
|
.build()
|
||||||
|
},
|
||||||
context
|
context
|
||||||
),
|
),
|
||||||
null
|
null
|
||||||
@@ -126,7 +138,13 @@ class StreamItemAdapterTest {
|
|||||||
private fun getVideoStreams(vararg videoOnly: Boolean) =
|
private fun getVideoStreams(vararg videoOnly: Boolean) =
|
||||||
StreamItemAdapter.StreamSizeWrapper(
|
StreamItemAdapter.StreamSizeWrapper(
|
||||||
videoOnly.map {
|
videoOnly.map {
|
||||||
VideoStream("https://example.com", MediaFormat.MPEG_4, "720p", it)
|
VideoStream.Builder()
|
||||||
|
.setId(Stream.ID_UNKNOWN)
|
||||||
|
.setContent("https://example.com", true)
|
||||||
|
.setMediaFormat(MediaFormat.MPEG_4)
|
||||||
|
.setResolution("720p")
|
||||||
|
.setIsVideoOnly(it)
|
||||||
|
.build()
|
||||||
},
|
},
|
||||||
context
|
context
|
||||||
)
|
)
|
||||||
@@ -138,8 +156,16 @@ class StreamItemAdapterTest {
|
|||||||
private fun getAudioStreams(vararg shouldBeValid: Boolean) =
|
private fun getAudioStreams(vararg shouldBeValid: Boolean) =
|
||||||
getSecondaryStreamsFromList(
|
getSecondaryStreamsFromList(
|
||||||
shouldBeValid.map {
|
shouldBeValid.map {
|
||||||
if (it) AudioStream("https://example.com", MediaFormat.OPUS, 192)
|
if (it) {
|
||||||
else null
|
AudioStream.Builder()
|
||||||
|
.setId(Stream.ID_UNKNOWN)
|
||||||
|
.setContent("https://example.com", true)
|
||||||
|
.setMediaFormat(MediaFormat.OPUS)
|
||||||
|
.setAverageBitrate(192)
|
||||||
|
.build()
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@@ -205,7 +205,7 @@ public class App extends MultiDexApplication {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
final CoreConfigurationBuilder acraConfig = new CoreConfigurationBuilder(this)
|
final CoreConfigurationBuilder acraConfig = new CoreConfigurationBuilder()
|
||||||
.withBuildConfigClass(BuildConfig.class);
|
.withBuildConfigClass(BuildConfig.class);
|
||||||
ACRA.init(this, acraConfig);
|
ACRA.init(this, acraConfig);
|
||||||
}
|
}
|
||||||
|
@@ -43,7 +43,7 @@ import static org.schabi.newpipe.MainActivity.DEBUG;
|
|||||||
|
|
||||||
public final class DownloaderImpl extends Downloader {
|
public final class DownloaderImpl extends Downloader {
|
||||||
public static final String USER_AGENT
|
public static final String USER_AGENT
|
||||||
= "Mozilla/5.0 (Windows NT 10.0; rv:78.0) Gecko/20100101 Firefox/78.0";
|
= "Mozilla/5.0 (Windows NT 10.0; rv:91.0) Gecko/20100101 Firefox/91.0";
|
||||||
public static final String YOUTUBE_RESTRICTED_MODE_COOKIE_KEY
|
public static final String YOUTUBE_RESTRICTED_MODE_COOKIE_KEY
|
||||||
= "youtube_restricted_mode_key";
|
= "youtube_restricted_mode_key";
|
||||||
public static final String YOUTUBE_RESTRICTED_MODE_COOKIE = "PREF=f2=8000000";
|
public static final String YOUTUBE_RESTRICTED_MODE_COOKIE = "PREF=f2=8000000";
|
||||||
|
File diff suppressed because it is too large
Load Diff
@@ -4,7 +4,6 @@ import androidx.annotation.NonNull;
|
|||||||
import androidx.room.ColumnInfo;
|
import androidx.room.ColumnInfo;
|
||||||
import androidx.room.Entity;
|
import androidx.room.Entity;
|
||||||
import androidx.room.ForeignKey;
|
import androidx.room.ForeignKey;
|
||||||
import androidx.room.Ignore;
|
|
||||||
import androidx.room.Index;
|
import androidx.room.Index;
|
||||||
|
|
||||||
import org.schabi.newpipe.database.stream.model.StreamEntity;
|
import org.schabi.newpipe.database.stream.model.StreamEntity;
|
||||||
@@ -42,18 +41,19 @@ public class StreamHistoryEntity {
|
|||||||
@ColumnInfo(name = STREAM_REPEAT_COUNT)
|
@ColumnInfo(name = STREAM_REPEAT_COUNT)
|
||||||
private long repeatCount;
|
private long repeatCount;
|
||||||
|
|
||||||
public StreamHistoryEntity(final long streamUid, @NonNull final OffsetDateTime accessDate,
|
/**
|
||||||
|
* @param streamUid the stream id this history item will refer to
|
||||||
|
* @param accessDate the last time the stream was accessed
|
||||||
|
* @param repeatCount the total number of views this stream received
|
||||||
|
*/
|
||||||
|
public StreamHistoryEntity(final long streamUid,
|
||||||
|
@NonNull final OffsetDateTime accessDate,
|
||||||
final long repeatCount) {
|
final long repeatCount) {
|
||||||
this.streamUid = streamUid;
|
this.streamUid = streamUid;
|
||||||
this.accessDate = accessDate;
|
this.accessDate = accessDate;
|
||||||
this.repeatCount = repeatCount;
|
this.repeatCount = repeatCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Ignore
|
|
||||||
public StreamHistoryEntity(final long streamUid, @NonNull final OffsetDateTime accessDate) {
|
|
||||||
this(streamUid, accessDate, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
public long getStreamUid() {
|
public long getStreamUid() {
|
||||||
return streamUid;
|
return streamUid;
|
||||||
}
|
}
|
||||||
|
@@ -12,8 +12,7 @@ import org.schabi.newpipe.database.BasicDAO
|
|||||||
import org.schabi.newpipe.database.stream.model.StreamEntity
|
import org.schabi.newpipe.database.stream.model.StreamEntity
|
||||||
import org.schabi.newpipe.database.stream.model.StreamEntity.Companion.STREAM_ID
|
import org.schabi.newpipe.database.stream.model.StreamEntity.Companion.STREAM_ID
|
||||||
import org.schabi.newpipe.extractor.stream.StreamType
|
import org.schabi.newpipe.extractor.stream.StreamType
|
||||||
import org.schabi.newpipe.extractor.stream.StreamType.AUDIO_LIVE_STREAM
|
import org.schabi.newpipe.util.StreamTypeUtil
|
||||||
import org.schabi.newpipe.extractor.stream.StreamType.LIVE_STREAM
|
|
||||||
import java.time.OffsetDateTime
|
import java.time.OffsetDateTime
|
||||||
|
|
||||||
@Dao
|
@Dao
|
||||||
@@ -91,8 +90,7 @@ abstract class StreamDAO : BasicDAO<StreamEntity> {
|
|||||||
?: throw IllegalStateException("Stream cannot be null just after insertion.")
|
?: throw IllegalStateException("Stream cannot be null just after insertion.")
|
||||||
newerStream.uid = existentMinimalStream.uid
|
newerStream.uid = existentMinimalStream.uid
|
||||||
|
|
||||||
val isNewerStreamLive = newerStream.streamType == AUDIO_LIVE_STREAM || newerStream.streamType == LIVE_STREAM
|
if (!StreamTypeUtil.isLiveStream(newerStream.streamType)) {
|
||||||
if (!isNewerStreamLive) {
|
|
||||||
|
|
||||||
// Use the existent upload date if the newer stream does not have a better precision
|
// Use the existent upload date if the newer stream does not have a better precision
|
||||||
// (i.e. is an approximation). This is done to prevent unnecessary changes.
|
// (i.e. is an approximation). This is done to prevent unnecessary changes.
|
||||||
|
File diff suppressed because it is too large
Load Diff
@@ -7,13 +7,13 @@ import kotlinx.parcelize.IgnoredOnParcel
|
|||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
import org.schabi.newpipe.R
|
import org.schabi.newpipe.R
|
||||||
import org.schabi.newpipe.extractor.Info
|
import org.schabi.newpipe.extractor.Info
|
||||||
import org.schabi.newpipe.extractor.NewPipe
|
|
||||||
import org.schabi.newpipe.extractor.exceptions.AccountTerminatedException
|
import org.schabi.newpipe.extractor.exceptions.AccountTerminatedException
|
||||||
import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException
|
import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException
|
||||||
import org.schabi.newpipe.extractor.exceptions.ContentNotSupportedException
|
import org.schabi.newpipe.extractor.exceptions.ContentNotSupportedException
|
||||||
import org.schabi.newpipe.extractor.exceptions.ExtractionException
|
import org.schabi.newpipe.extractor.exceptions.ExtractionException
|
||||||
import org.schabi.newpipe.extractor.services.youtube.extractors.YoutubeStreamExtractor.DeobfuscateException
|
import org.schabi.newpipe.extractor.services.youtube.extractors.YoutubeStreamExtractor.DeobfuscateException
|
||||||
import org.schabi.newpipe.ktx.isNetworkRelated
|
import org.schabi.newpipe.ktx.isNetworkRelated
|
||||||
|
import org.schabi.newpipe.util.ServiceHelper
|
||||||
import java.io.PrintWriter
|
import java.io.PrintWriter
|
||||||
import java.io.StringWriter
|
import java.io.StringWriter
|
||||||
|
|
||||||
@@ -65,7 +65,7 @@ class ErrorInfo(
|
|||||||
constructor(throwable: Throwable, userAction: UserAction, request: String) :
|
constructor(throwable: Throwable, userAction: UserAction, request: String) :
|
||||||
this(throwable, userAction, SERVICE_NONE, request)
|
this(throwable, userAction, SERVICE_NONE, request)
|
||||||
constructor(throwable: Throwable, userAction: UserAction, request: String, serviceId: Int) :
|
constructor(throwable: Throwable, userAction: UserAction, request: String, serviceId: Int) :
|
||||||
this(throwable, userAction, NewPipe.getNameOfService(serviceId), request)
|
this(throwable, userAction, ServiceHelper.getNameOfServiceById(serviceId), request)
|
||||||
constructor(throwable: Throwable, userAction: UserAction, request: String, info: Info?) :
|
constructor(throwable: Throwable, userAction: UserAction, request: String, info: Info?) :
|
||||||
this(throwable, userAction, getInfoServiceName(info), request)
|
this(throwable, userAction, getInfoServiceName(info), request)
|
||||||
|
|
||||||
@@ -73,7 +73,7 @@ class ErrorInfo(
|
|||||||
constructor(throwable: List<Throwable>, userAction: UserAction, request: String) :
|
constructor(throwable: List<Throwable>, userAction: UserAction, request: String) :
|
||||||
this(throwable, userAction, SERVICE_NONE, request)
|
this(throwable, userAction, SERVICE_NONE, request)
|
||||||
constructor(throwable: List<Throwable>, userAction: UserAction, request: String, serviceId: Int) :
|
constructor(throwable: List<Throwable>, userAction: UserAction, request: String, serviceId: Int) :
|
||||||
this(throwable, userAction, NewPipe.getNameOfService(serviceId), request)
|
this(throwable, userAction, ServiceHelper.getNameOfServiceById(serviceId), request)
|
||||||
constructor(throwable: List<Throwable>, userAction: UserAction, request: String, info: Info?) :
|
constructor(throwable: List<Throwable>, userAction: UserAction, request: String, info: Info?) :
|
||||||
this(throwable, userAction, getInfoServiceName(info), request)
|
this(throwable, userAction, getInfoServiceName(info), request)
|
||||||
|
|
||||||
@@ -95,7 +95,7 @@ class ErrorInfo(
|
|||||||
Array(throwable.size) { i -> getStackTrace(throwable[i]) }
|
Array(throwable.size) { i -> getStackTrace(throwable[i]) }
|
||||||
|
|
||||||
private fun getInfoServiceName(info: Info?) =
|
private fun getInfoServiceName(info: Info?) =
|
||||||
if (info == null) SERVICE_NONE else NewPipe.getNameOfService(info.serviceId)
|
if (info == null) SERVICE_NONE else ServiceHelper.getNameOfServiceById(info.serviceId)
|
||||||
|
|
||||||
@StringRes
|
@StringRes
|
||||||
private fun getMessageStringId(
|
private fun getMessageStringId(
|
||||||
|
@@ -15,7 +15,6 @@ import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
|||||||
import io.reactivex.rxjava3.disposables.Disposable
|
import io.reactivex.rxjava3.disposables.Disposable
|
||||||
import org.schabi.newpipe.MainActivity
|
import org.schabi.newpipe.MainActivity
|
||||||
import org.schabi.newpipe.R
|
import org.schabi.newpipe.R
|
||||||
import org.schabi.newpipe.extractor.NewPipe
|
|
||||||
import org.schabi.newpipe.extractor.exceptions.AccountTerminatedException
|
import org.schabi.newpipe.extractor.exceptions.AccountTerminatedException
|
||||||
import org.schabi.newpipe.extractor.exceptions.AgeRestrictedContentException
|
import org.schabi.newpipe.extractor.exceptions.AgeRestrictedContentException
|
||||||
import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException
|
import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException
|
||||||
@@ -106,7 +105,7 @@ class ErrorPanelHelper(
|
|||||||
if (!isNullOrEmpty((errorInfo.throwable as AccountTerminatedException).message)) {
|
if (!isNullOrEmpty((errorInfo.throwable as AccountTerminatedException).message)) {
|
||||||
errorServiceInfoTextView.text = context.resources.getString(
|
errorServiceInfoTextView.text = context.resources.getString(
|
||||||
R.string.service_provides_reason,
|
R.string.service_provides_reason,
|
||||||
NewPipe.getNameOfService(ServiceHelper.getSelectedServiceId(context))
|
ServiceHelper.getSelectedService(context)?.serviceInfo?.name ?: "<unknown>"
|
||||||
)
|
)
|
||||||
errorServiceInfoTextView.isVisible = true
|
errorServiceInfoTextView.isVisible = true
|
||||||
|
|
||||||
|
@@ -31,6 +31,7 @@ import android.view.WindowManager;
|
|||||||
import android.view.animation.DecelerateInterpolator;
|
import android.view.animation.DecelerateInterpolator;
|
||||||
import android.widget.FrameLayout;
|
import android.widget.FrameLayout;
|
||||||
import android.widget.RelativeLayout;
|
import android.widget.RelativeLayout;
|
||||||
|
import android.widget.Toast;
|
||||||
|
|
||||||
import androidx.annotation.AttrRes;
|
import androidx.annotation.AttrRes;
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
@@ -94,6 +95,7 @@ import org.schabi.newpipe.util.Localization;
|
|||||||
import org.schabi.newpipe.util.NavigationHelper;
|
import org.schabi.newpipe.util.NavigationHelper;
|
||||||
import org.schabi.newpipe.util.PermissionHelper;
|
import org.schabi.newpipe.util.PermissionHelper;
|
||||||
import org.schabi.newpipe.util.PicassoHelper;
|
import org.schabi.newpipe.util.PicassoHelper;
|
||||||
|
import org.schabi.newpipe.util.StreamTypeUtil;
|
||||||
import org.schabi.newpipe.util.ThemeHelper;
|
import org.schabi.newpipe.util.ThemeHelper;
|
||||||
import org.schabi.newpipe.util.external_communication.KoreUtils;
|
import org.schabi.newpipe.util.external_communication.KoreUtils;
|
||||||
import org.schabi.newpipe.util.external_communication.ShareUtils;
|
import org.schabi.newpipe.util.external_communication.ShareUtils;
|
||||||
@@ -121,6 +123,7 @@ import static org.schabi.newpipe.player.helper.PlayerHelper.globalScreenOrientat
|
|||||||
import static org.schabi.newpipe.player.helper.PlayerHelper.isClearingQueueConfirmationRequired;
|
import static org.schabi.newpipe.player.helper.PlayerHelper.isClearingQueueConfirmationRequired;
|
||||||
import static org.schabi.newpipe.player.playqueue.PlayQueueItem.RECOVERY_UNSET;
|
import static org.schabi.newpipe.player.playqueue.PlayQueueItem.RECOVERY_UNSET;
|
||||||
import static org.schabi.newpipe.util.ExtractorHelper.showMetaInfoInTextView;
|
import static org.schabi.newpipe.util.ExtractorHelper.showMetaInfoInTextView;
|
||||||
|
import static org.schabi.newpipe.util.ListHelper.getUrlAndNonTorrentStreams;
|
||||||
|
|
||||||
public final class VideoDetailFragment
|
public final class VideoDetailFragment
|
||||||
extends BaseStateFragment<StreamInfo>
|
extends BaseStateFragment<StreamInfo>
|
||||||
@@ -186,8 +189,6 @@ public final class VideoDetailFragment
|
|||||||
@Nullable
|
@Nullable
|
||||||
private Disposable positionSubscriber = null;
|
private Disposable positionSubscriber = null;
|
||||||
|
|
||||||
private List<VideoStream> sortedVideoStreams;
|
|
||||||
private int selectedVideoStreamIndex = -1;
|
|
||||||
private BottomSheetBehavior<FrameLayout> bottomSheetBehavior;
|
private BottomSheetBehavior<FrameLayout> bottomSheetBehavior;
|
||||||
private BroadcastReceiver broadcastReceiver;
|
private BroadcastReceiver broadcastReceiver;
|
||||||
|
|
||||||
@@ -663,8 +664,7 @@ public final class VideoDetailFragment
|
|||||||
binding.detailControlsCrashThePlayer.setOnClickListener(
|
binding.detailControlsCrashThePlayer.setOnClickListener(
|
||||||
v -> VideoDetailPlayerCrasher.onCrashThePlayer(
|
v -> VideoDetailPlayerCrasher.onCrashThePlayer(
|
||||||
this.getContext(),
|
this.getContext(),
|
||||||
this.player,
|
this.player)
|
||||||
getLayoutInflater())
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1093,9 +1093,6 @@ public final class VideoDetailFragment
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void openBackgroundPlayer(final boolean append) {
|
private void openBackgroundPlayer(final boolean append) {
|
||||||
final AudioStream audioStream = currentInfo.getAudioStreams()
|
|
||||||
.get(ListHelper.getDefaultAudioFormat(activity, currentInfo.getAudioStreams()));
|
|
||||||
|
|
||||||
final boolean useExternalAudioPlayer = PreferenceManager
|
final boolean useExternalAudioPlayer = PreferenceManager
|
||||||
.getDefaultSharedPreferences(activity)
|
.getDefaultSharedPreferences(activity)
|
||||||
.getBoolean(activity.getString(R.string.use_external_audio_player_key), false);
|
.getBoolean(activity.getString(R.string.use_external_audio_player_key), false);
|
||||||
@@ -1110,7 +1107,17 @@ public final class VideoDetailFragment
|
|||||||
if (!useExternalAudioPlayer) {
|
if (!useExternalAudioPlayer) {
|
||||||
openNormalBackgroundPlayer(append);
|
openNormalBackgroundPlayer(append);
|
||||||
} else {
|
} else {
|
||||||
startOnExternalPlayer(activity, currentInfo, audioStream);
|
final List<AudioStream> audioStreams = getUrlAndNonTorrentStreams(
|
||||||
|
currentInfo.getAudioStreams());
|
||||||
|
final int index = ListHelper.getDefaultAudioFormat(activity, audioStreams);
|
||||||
|
|
||||||
|
if (index == -1) {
|
||||||
|
Toast.makeText(activity, R.string.no_audio_streams_available_for_external_players,
|
||||||
|
Toast.LENGTH_SHORT).show();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
startOnExternalPlayer(activity, currentInfo, audioStreams.get(index));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1613,14 +1620,6 @@ public final class VideoDetailFragment
|
|||||||
binding.detailToggleSecondaryControlsView.setVisibility(View.VISIBLE);
|
binding.detailToggleSecondaryControlsView.setVisibility(View.VISIBLE);
|
||||||
binding.detailSecondaryControlPanel.setVisibility(View.GONE);
|
binding.detailSecondaryControlPanel.setVisibility(View.GONE);
|
||||||
|
|
||||||
sortedVideoStreams = ListHelper.getSortedStreamVideosList(
|
|
||||||
activity,
|
|
||||||
info.getVideoStreams(),
|
|
||||||
info.getVideoOnlyStreams(),
|
|
||||||
false,
|
|
||||||
false);
|
|
||||||
selectedVideoStreamIndex = ListHelper
|
|
||||||
.getDefaultResolutionIndex(activity, sortedVideoStreams);
|
|
||||||
updateProgressInfo(info);
|
updateProgressInfo(info);
|
||||||
initThumbnailViews(info);
|
initThumbnailViews(info);
|
||||||
showMetaInfoInTextView(info.getMetaInfo(), binding.detailMetaInfoTextView,
|
showMetaInfoInTextView(info.getMetaInfo(), binding.detailMetaInfoTextView,
|
||||||
@@ -1646,8 +1645,8 @@ public final class VideoDetailFragment
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.detailControlsDownload.setVisibility(info.getStreamType() == StreamType.LIVE_STREAM
|
binding.detailControlsDownload.setVisibility(
|
||||||
|| info.getStreamType() == StreamType.AUDIO_LIVE_STREAM ? View.GONE : View.VISIBLE);
|
StreamTypeUtil.isLiveStream(info.getStreamType()) ? View.GONE : View.VISIBLE);
|
||||||
binding.detailControlsBackground.setVisibility(info.getAudioStreams().isEmpty()
|
binding.detailControlsBackground.setVisibility(info.getAudioStreams().isEmpty()
|
||||||
? View.GONE : View.VISIBLE);
|
? View.GONE : View.VISIBLE);
|
||||||
|
|
||||||
@@ -1688,12 +1687,7 @@ public final class VideoDetailFragment
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final DownloadDialog downloadDialog = DownloadDialog.newInstance(currentInfo);
|
final DownloadDialog downloadDialog = new DownloadDialog(activity, currentInfo);
|
||||||
downloadDialog.setVideoStreams(sortedVideoStreams);
|
|
||||||
downloadDialog.setAudioStreams(currentInfo.getAudioStreams());
|
|
||||||
downloadDialog.setSelectedVideoStream(selectedVideoStreamIndex);
|
|
||||||
downloadDialog.setSubtitleStreams(currentInfo.getSubtitles());
|
|
||||||
|
|
||||||
downloadDialog.show(activity.getSupportFragmentManager(), "downloadDialog");
|
downloadDialog.show(activity.getSupportFragmentManager(), "downloadDialog");
|
||||||
} catch (final Exception e) {
|
} catch (final Exception e) {
|
||||||
ErrorUtil.showSnackbar(activity, new ErrorInfo(e, UserAction.DOWNLOAD_OPEN_DIALOG,
|
ErrorUtil.showSnackbar(activity, new ErrorInfo(e, UserAction.DOWNLOAD_OPEN_DIALOG,
|
||||||
@@ -1723,8 +1717,7 @@ public final class VideoDetailFragment
|
|||||||
binding.detailPositionView.setVisibility(View.GONE);
|
binding.detailPositionView.setVisibility(View.GONE);
|
||||||
// TODO: Remove this check when separation of concerns is done.
|
// TODO: Remove this check when separation of concerns is done.
|
||||||
// (live streams weren't getting updated because they are mixed)
|
// (live streams weren't getting updated because they are mixed)
|
||||||
if (!info.getStreamType().equals(StreamType.LIVE_STREAM)
|
if (!StreamTypeUtil.isLiveStream(info.getStreamType())) {
|
||||||
&& !info.getStreamType().equals(StreamType.AUDIO_LIVE_STREAM)) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -2152,25 +2145,52 @@ public final class VideoDetailFragment
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void showExternalPlaybackDialog() {
|
private void showExternalPlaybackDialog() {
|
||||||
if (sortedVideoStreams == null) {
|
if (currentInfo == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final CharSequence[] resolutions = new CharSequence[sortedVideoStreams.size()];
|
|
||||||
for (int i = 0; i < sortedVideoStreams.size(); i++) {
|
final AlertDialog.Builder builder = new AlertDialog.Builder(activity);
|
||||||
resolutions[i] = sortedVideoStreams.get(i).getResolution();
|
builder.setTitle(R.string.select_quality_external_players);
|
||||||
}
|
builder.setNeutralButton(R.string.open_in_browser, (dialog, i) ->
|
||||||
final AlertDialog.Builder builder = new AlertDialog.Builder(activity)
|
ShareUtils.openUrlInBrowser(requireActivity(), url));
|
||||||
.setNegativeButton(R.string.cancel, null)
|
|
||||||
.setNeutralButton(R.string.open_in_browser, (dialog, i) ->
|
final List<VideoStream> videoStreamsForExternalPlayers =
|
||||||
ShareUtils.openUrlInBrowser(requireActivity(), url)
|
ListHelper.getSortedStreamVideosList(
|
||||||
|
activity,
|
||||||
|
getUrlAndNonTorrentStreams(currentInfo.getVideoStreams()),
|
||||||
|
getUrlAndNonTorrentStreams(currentInfo.getVideoOnlyStreams()),
|
||||||
|
false,
|
||||||
|
false
|
||||||
);
|
);
|
||||||
// Maybe there are no video streams available, show just `open in browser` button
|
|
||||||
if (resolutions.length > 0) {
|
if (videoStreamsForExternalPlayers.isEmpty()) {
|
||||||
builder.setSingleChoiceItems(resolutions, selectedVideoStreamIndex, (dialog, i) -> {
|
builder.setMessage(R.string.no_video_streams_available_for_external_players);
|
||||||
dialog.dismiss();
|
builder.setPositiveButton(R.string.ok, null);
|
||||||
startOnExternalPlayer(activity, currentInfo, sortedVideoStreams.get(i));
|
|
||||||
}
|
} else {
|
||||||
);
|
final int selectedVideoStreamIndexForExternalPlayers =
|
||||||
|
ListHelper.getDefaultResolutionIndex(activity, videoStreamsForExternalPlayers);
|
||||||
|
final CharSequence[] resolutions =
|
||||||
|
new CharSequence[videoStreamsForExternalPlayers.size()];
|
||||||
|
|
||||||
|
for (int i = 0; i < videoStreamsForExternalPlayers.size(); i++) {
|
||||||
|
resolutions[i] = videoStreamsForExternalPlayers.get(i).getResolution();
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.setSingleChoiceItems(resolutions, selectedVideoStreamIndexForExternalPlayers,
|
||||||
|
null);
|
||||||
|
builder.setNegativeButton(R.string.cancel, null);
|
||||||
|
builder.setPositiveButton(R.string.ok, (dialog, i) -> {
|
||||||
|
final int index = ((AlertDialog) dialog).getListView().getCheckedItemPosition();
|
||||||
|
// We don't have to manage the index validity because if there is no stream
|
||||||
|
// available for external players, this code will be not executed and if there is
|
||||||
|
// no stream which matches the default resolution, 0 is returned by
|
||||||
|
// ListHelper.getDefaultResolutionIndex.
|
||||||
|
// The index cannot be outside the bounds of the list as its always between 0 and
|
||||||
|
// the list size - 1, .
|
||||||
|
startOnExternalPlayer(activity, currentInfo,
|
||||||
|
videoStreamsForExternalPlayers.get(index));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
builder.show();
|
builder.show();
|
||||||
}
|
}
|
||||||
|
@@ -1,5 +1,9 @@
|
|||||||
package org.schabi.newpipe.fragments.detail;
|
package org.schabi.newpipe.fragments.detail;
|
||||||
|
|
||||||
|
import static com.google.android.exoplayer2.PlaybackException.ERROR_CODE_BEHIND_LIVE_WINDOW;
|
||||||
|
import static com.google.android.exoplayer2.PlaybackException.ERROR_CODE_DECODING_FAILED;
|
||||||
|
import static com.google.android.exoplayer2.PlaybackException.ERROR_CODE_UNSPECIFIED;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import android.view.ContextThemeWrapper;
|
import android.view.ContextThemeWrapper;
|
||||||
@@ -29,10 +33,6 @@ import java.util.LinkedHashMap;
|
|||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.function.Supplier;
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
import static com.google.android.exoplayer2.PlaybackException.ERROR_CODE_BEHIND_LIVE_WINDOW;
|
|
||||||
import static com.google.android.exoplayer2.PlaybackException.ERROR_CODE_DECODING_FAILED;
|
|
||||||
import static com.google.android.exoplayer2.PlaybackException.ERROR_CODE_UNSPECIFIED;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Outsourced logic for crashing the player in the {@link VideoDetailFragment}.
|
* Outsourced logic for crashing the player in the {@link VideoDetailFragment}.
|
||||||
*/
|
*/
|
||||||
@@ -97,8 +97,7 @@ public final class VideoDetailPlayerCrasher {
|
|||||||
|
|
||||||
public static void onCrashThePlayer(
|
public static void onCrashThePlayer(
|
||||||
@NonNull final Context context,
|
@NonNull final Context context,
|
||||||
@Nullable final Player player,
|
@Nullable final Player player
|
||||||
@NonNull final LayoutInflater layoutInflater
|
|
||||||
) {
|
) {
|
||||||
if (player == null) {
|
if (player == null) {
|
||||||
Log.d(TAG, "Player is not available");
|
Log.d(TAG, "Player is not available");
|
||||||
@@ -109,16 +108,15 @@ public final class VideoDetailPlayerCrasher {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// -- Build the dialog/UI --
|
// -- Build the dialog/UI --
|
||||||
|
|
||||||
final Context themeWrapperContext = getThemeWrapperContext(context);
|
final Context themeWrapperContext = getThemeWrapperContext(context);
|
||||||
|
|
||||||
final LayoutInflater inflater = LayoutInflater.from(themeWrapperContext);
|
final LayoutInflater inflater = LayoutInflater.from(themeWrapperContext);
|
||||||
final RadioGroup radioGroup = SingleChoiceDialogViewBinding.inflate(layoutInflater)
|
|
||||||
.list;
|
|
||||||
|
|
||||||
final AlertDialog alertDialog = new AlertDialog.Builder(getThemeWrapperContext(context))
|
final SingleChoiceDialogViewBinding binding =
|
||||||
|
SingleChoiceDialogViewBinding.inflate(inflater);
|
||||||
|
|
||||||
|
final AlertDialog alertDialog = new AlertDialog.Builder(themeWrapperContext)
|
||||||
.setTitle("Choose an exception")
|
.setTitle("Choose an exception")
|
||||||
.setView(radioGroup)
|
.setView(binding.getRoot())
|
||||||
.setCancelable(true)
|
.setCancelable(true)
|
||||||
.setNegativeButton(R.string.cancel, null)
|
.setNegativeButton(R.string.cancel, null)
|
||||||
.create();
|
.create();
|
||||||
@@ -136,11 +134,9 @@ public final class VideoDetailPlayerCrasher {
|
|||||||
);
|
);
|
||||||
radioButton.setOnClickListener(v -> {
|
radioButton.setOnClickListener(v -> {
|
||||||
tryCrashPlayerWith(player, entry.getValue().get());
|
tryCrashPlayerWith(player, entry.getValue().get());
|
||||||
if (alertDialog != null) {
|
alertDialog.cancel();
|
||||||
alertDialog.cancel();
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
radioGroup.addView(radioButton);
|
binding.list.addView(radioButton);
|
||||||
}
|
}
|
||||||
|
|
||||||
alertDialog.show();
|
alertDialog.show();
|
||||||
|
@@ -77,6 +77,8 @@ public class ChannelFragment extends BaseListInfoFragment<StreamInfoItem, Channe
|
|||||||
private final CompositeDisposable disposables = new CompositeDisposable();
|
private final CompositeDisposable disposables = new CompositeDisposable();
|
||||||
private Disposable subscribeButtonMonitor;
|
private Disposable subscribeButtonMonitor;
|
||||||
|
|
||||||
|
private boolean channelContentNotSupported = false;
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
// Views
|
// Views
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
@@ -130,6 +132,7 @@ public class ChannelFragment extends BaseListInfoFragment<StreamInfoItem, Channe
|
|||||||
public void onViewCreated(@NonNull final View rootView, final Bundle savedInstanceState) {
|
public void onViewCreated(@NonNull final View rootView, final Bundle savedInstanceState) {
|
||||||
super.onViewCreated(rootView, savedInstanceState);
|
super.onViewCreated(rootView, savedInstanceState);
|
||||||
channelBinding = FragmentChannelBinding.bind(rootView);
|
channelBinding = FragmentChannelBinding.bind(rootView);
|
||||||
|
showContentNotSupportedIfNeeded();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -524,9 +527,12 @@ public class ChannelFragment extends BaseListInfoFragment<StreamInfoItem, Channe
|
|||||||
playlistControlBinding.getRoot().setVisibility(View.GONE);
|
playlistControlBinding.getRoot().setVisibility(View.GONE);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
channelContentNotSupported = false;
|
||||||
for (final Throwable throwable : result.getErrors()) {
|
for (final Throwable throwable : result.getErrors()) {
|
||||||
if (throwable instanceof ContentNotSupportedException) {
|
if (throwable instanceof ContentNotSupportedException) {
|
||||||
showContentNotSupported();
|
channelContentNotSupported = true;
|
||||||
|
showContentNotSupportedIfNeeded();
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -558,7 +564,13 @@ public class ChannelFragment extends BaseListInfoFragment<StreamInfoItem, Channe
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private void showContentNotSupported() {
|
private void showContentNotSupportedIfNeeded() {
|
||||||
|
// channelBinding might not be initialized when handleResult() is called
|
||||||
|
// (e.g. after rotating the screen, #6696)
|
||||||
|
if (!channelContentNotSupported || channelBinding == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
channelBinding.errorContentNotSupported.setVisibility(View.VISIBLE);
|
channelBinding.errorContentNotSupported.setVisibility(View.VISIBLE);
|
||||||
channelBinding.channelKaomoji.setText("(︶︹︺)");
|
channelBinding.channelKaomoji.setText("(︶︹︺)");
|
||||||
channelBinding.channelKaomoji.setTextSize(TypedValue.COMPLEX_UNIT_SP, 45f);
|
channelBinding.channelKaomoji.setTextSize(TypedValue.COMPLEX_UNIT_SP, 45f);
|
||||||
|
@@ -4,7 +4,6 @@ import static org.schabi.newpipe.ktx.ViewUtils.animate;
|
|||||||
import static org.schabi.newpipe.ktx.ViewUtils.animateHideRecyclerViewAllowingScrolling;
|
import static org.schabi.newpipe.ktx.ViewUtils.animateHideRecyclerViewAllowingScrolling;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.res.ColorStateList;
|
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
@@ -18,7 +17,6 @@ import android.view.ViewGroup;
|
|||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.appcompat.content.res.AppCompatResources;
|
import androidx.appcompat.content.res.AppCompatResources;
|
||||||
import androidx.core.content.ContextCompat;
|
|
||||||
|
|
||||||
import com.google.android.material.shape.CornerFamily;
|
import com.google.android.material.shape.CornerFamily;
|
||||||
import com.google.android.material.shape.ShapeAppearanceModel;
|
import com.google.android.material.shape.ShapeAppearanceModel;
|
||||||
@@ -28,6 +26,7 @@ import org.reactivestreams.Subscription;
|
|||||||
import org.schabi.newpipe.NewPipeDatabase;
|
import org.schabi.newpipe.NewPipeDatabase;
|
||||||
import org.schabi.newpipe.R;
|
import org.schabi.newpipe.R;
|
||||||
import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity;
|
import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity;
|
||||||
|
import org.schabi.newpipe.database.stream.model.StreamEntity;
|
||||||
import org.schabi.newpipe.databinding.PlaylistControlBinding;
|
import org.schabi.newpipe.databinding.PlaylistControlBinding;
|
||||||
import org.schabi.newpipe.databinding.PlaylistHeaderBinding;
|
import org.schabi.newpipe.databinding.PlaylistHeaderBinding;
|
||||||
import org.schabi.newpipe.error.ErrorInfo;
|
import org.schabi.newpipe.error.ErrorInfo;
|
||||||
@@ -41,6 +40,8 @@ import org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper;
|
|||||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||||
import org.schabi.newpipe.fragments.list.BaseListInfoFragment;
|
import org.schabi.newpipe.fragments.list.BaseListInfoFragment;
|
||||||
import org.schabi.newpipe.info_list.dialog.InfoItemDialog;
|
import org.schabi.newpipe.info_list.dialog.InfoItemDialog;
|
||||||
|
import org.schabi.newpipe.info_list.dialog.StreamDialogDefaultEntry;
|
||||||
|
import org.schabi.newpipe.local.dialog.PlaylistDialog;
|
||||||
import org.schabi.newpipe.local.playlist.RemotePlaylistManager;
|
import org.schabi.newpipe.local.playlist.RemotePlaylistManager;
|
||||||
import org.schabi.newpipe.player.MainPlayer.PlayerType;
|
import org.schabi.newpipe.player.MainPlayer.PlayerType;
|
||||||
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
||||||
@@ -49,13 +50,13 @@ import org.schabi.newpipe.util.ExtractorHelper;
|
|||||||
import org.schabi.newpipe.util.Localization;
|
import org.schabi.newpipe.util.Localization;
|
||||||
import org.schabi.newpipe.util.NavigationHelper;
|
import org.schabi.newpipe.util.NavigationHelper;
|
||||||
import org.schabi.newpipe.util.PicassoHelper;
|
import org.schabi.newpipe.util.PicassoHelper;
|
||||||
import org.schabi.newpipe.info_list.dialog.StreamDialogDefaultEntry;
|
|
||||||
import org.schabi.newpipe.util.external_communication.ShareUtils;
|
import org.schabi.newpipe.util.external_communication.ShareUtils;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
import java.util.function.Supplier;
|
import java.util.function.Supplier;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||||
import io.reactivex.rxjava3.core.Flowable;
|
import io.reactivex.rxjava3.core.Flowable;
|
||||||
@@ -237,6 +238,17 @@ public class PlaylistFragment extends BaseListInfoFragment<StreamInfoItem, Playl
|
|||||||
case R.id.menu_item_bookmark:
|
case R.id.menu_item_bookmark:
|
||||||
onBookmarkClicked();
|
onBookmarkClicked();
|
||||||
break;
|
break;
|
||||||
|
case R.id.menu_item_append_playlist:
|
||||||
|
disposables.add(PlaylistDialog.createCorrespondingDialog(
|
||||||
|
getContext(),
|
||||||
|
getPlayQueue()
|
||||||
|
.getStreams()
|
||||||
|
.stream()
|
||||||
|
.map(StreamEntity::new)
|
||||||
|
.collect(Collectors.toList()),
|
||||||
|
dialog -> dialog.show(getFM(), TAG)
|
||||||
|
));
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
return super.onOptionsItemSelected(item);
|
return super.onOptionsItemSelected(item);
|
||||||
}
|
}
|
||||||
@@ -293,10 +305,8 @@ public class PlaylistFragment extends BaseListInfoFragment<StreamInfoItem, Playl
|
|||||||
.setAllCorners(CornerFamily.ROUNDED, 0f)
|
.setAllCorners(CornerFamily.ROUNDED, 0f)
|
||||||
.build(); // this turns the image back into a square
|
.build(); // this turns the image back into a square
|
||||||
headerBinding.uploaderAvatarView.setShapeAppearanceModel(model);
|
headerBinding.uploaderAvatarView.setShapeAppearanceModel(model);
|
||||||
headerBinding.uploaderAvatarView.setStrokeColor(
|
headerBinding.uploaderAvatarView.setStrokeColor(AppCompatResources
|
||||||
ColorStateList.valueOf(ContextCompat.getColor(
|
.getColorStateList(requireContext(), R.color.transparent_background_color));
|
||||||
requireContext(), R.color.transparent_background_color))
|
|
||||||
);
|
|
||||||
headerBinding.uploaderAvatarView.setImageDrawable(
|
headerBinding.uploaderAvatarView.setImageDrawable(
|
||||||
AppCompatResources.getDrawable(requireContext(),
|
AppCompatResources.getDrawable(requireContext(),
|
||||||
R.drawable.ic_radio)
|
R.drawable.ic_radio)
|
||||||
|
@@ -24,6 +24,7 @@ import org.schabi.newpipe.extractor.InfoItem;
|
|||||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||||
import org.schabi.newpipe.extractor.stream.StreamType;
|
import org.schabi.newpipe.extractor.stream.StreamType;
|
||||||
import org.schabi.newpipe.player.helper.PlayerHolder;
|
import org.schabi.newpipe.player.helper.PlayerHolder;
|
||||||
|
import org.schabi.newpipe.util.StreamTypeUtil;
|
||||||
import org.schabi.newpipe.util.external_communication.KoreUtils;
|
import org.schabi.newpipe.util.external_communication.KoreUtils;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
@@ -269,8 +270,7 @@ public final class InfoItemDialog {
|
|||||||
*/
|
*/
|
||||||
public Builder addStartHereEntries() {
|
public Builder addStartHereEntries() {
|
||||||
addEntry(StreamDialogDefaultEntry.START_HERE_ON_BACKGROUND);
|
addEntry(StreamDialogDefaultEntry.START_HERE_ON_BACKGROUND);
|
||||||
if (infoItem.getStreamType() != StreamType.AUDIO_STREAM
|
if (!StreamTypeUtil.isAudio(infoItem.getStreamType())) {
|
||||||
&& infoItem.getStreamType() != StreamType.AUDIO_LIVE_STREAM) {
|
|
||||||
addEntry(StreamDialogDefaultEntry.START_HERE_ON_POPUP);
|
addEntry(StreamDialogDefaultEntry.START_HERE_ON_POPUP);
|
||||||
}
|
}
|
||||||
return this;
|
return this;
|
||||||
@@ -285,9 +285,7 @@ public final class InfoItemDialog {
|
|||||||
final boolean isWatchHistoryEnabled = PreferenceManager
|
final boolean isWatchHistoryEnabled = PreferenceManager
|
||||||
.getDefaultSharedPreferences(context)
|
.getDefaultSharedPreferences(context)
|
||||||
.getBoolean(context.getString(R.string.enable_watch_history_key), false);
|
.getBoolean(context.getString(R.string.enable_watch_history_key), false);
|
||||||
if (isWatchHistoryEnabled
|
if (isWatchHistoryEnabled && !StreamTypeUtil.isLiveStream(infoItem.getStreamType())) {
|
||||||
&& infoItem.getStreamType() != StreamType.LIVE_STREAM
|
|
||||||
&& infoItem.getStreamType() != StreamType.AUDIO_LIVE_STREAM) {
|
|
||||||
addEntry(StreamDialogDefaultEntry.MARK_AS_WATCHED);
|
addEntry(StreamDialogDefaultEntry.MARK_AS_WATCHED);
|
||||||
}
|
}
|
||||||
return this;
|
return this;
|
||||||
|
@@ -11,12 +11,12 @@ import org.schabi.newpipe.R;
|
|||||||
import org.schabi.newpipe.database.stream.model.StreamStateEntity;
|
import org.schabi.newpipe.database.stream.model.StreamStateEntity;
|
||||||
import org.schabi.newpipe.extractor.InfoItem;
|
import org.schabi.newpipe.extractor.InfoItem;
|
||||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||||
import org.schabi.newpipe.extractor.stream.StreamType;
|
|
||||||
import org.schabi.newpipe.info_list.InfoItemBuilder;
|
import org.schabi.newpipe.info_list.InfoItemBuilder;
|
||||||
import org.schabi.newpipe.ktx.ViewUtils;
|
import org.schabi.newpipe.ktx.ViewUtils;
|
||||||
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
||||||
import org.schabi.newpipe.util.PicassoHelper;
|
import org.schabi.newpipe.util.PicassoHelper;
|
||||||
import org.schabi.newpipe.util.Localization;
|
import org.schabi.newpipe.util.Localization;
|
||||||
|
import org.schabi.newpipe.util.StreamTypeUtil;
|
||||||
import org.schabi.newpipe.views.AnimatedProgressBar;
|
import org.schabi.newpipe.views.AnimatedProgressBar;
|
||||||
|
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
@@ -70,8 +70,7 @@ public class StreamMiniInfoItemHolder extends InfoItemHolder {
|
|||||||
} else {
|
} else {
|
||||||
itemProgressView.setVisibility(View.GONE);
|
itemProgressView.setVisibility(View.GONE);
|
||||||
}
|
}
|
||||||
} else if (item.getStreamType() == StreamType.LIVE_STREAM
|
} else if (StreamTypeUtil.isLiveStream(item.getStreamType())) {
|
||||||
|| item.getStreamType() == StreamType.AUDIO_LIVE_STREAM) {
|
|
||||||
itemDurationView.setText(R.string.duration_live);
|
itemDurationView.setText(R.string.duration_live);
|
||||||
itemDurationView.setBackgroundColor(ContextCompat.getColor(itemBuilder.getContext(),
|
itemDurationView.setBackgroundColor(ContextCompat.getColor(itemBuilder.getContext(),
|
||||||
R.color.live_duration_background_color));
|
R.color.live_duration_background_color));
|
||||||
@@ -96,9 +95,10 @@ public class StreamMiniInfoItemHolder extends InfoItemHolder {
|
|||||||
case VIDEO_STREAM:
|
case VIDEO_STREAM:
|
||||||
case LIVE_STREAM:
|
case LIVE_STREAM:
|
||||||
case AUDIO_LIVE_STREAM:
|
case AUDIO_LIVE_STREAM:
|
||||||
|
case POST_LIVE_STREAM:
|
||||||
|
case POST_LIVE_AUDIO_STREAM:
|
||||||
enableLongClick(item);
|
enableLongClick(item);
|
||||||
break;
|
break;
|
||||||
case FILE:
|
|
||||||
case NONE:
|
case NONE:
|
||||||
default:
|
default:
|
||||||
disableLongClick();
|
disableLongClick();
|
||||||
@@ -114,7 +114,7 @@ public class StreamMiniInfoItemHolder extends InfoItemHolder {
|
|||||||
final StreamStateEntity state
|
final StreamStateEntity state
|
||||||
= historyRecordManager.loadStreamState(infoItem).blockingGet()[0];
|
= historyRecordManager.loadStreamState(infoItem).blockingGet()[0];
|
||||||
if (state != null && item.getDuration() > 0
|
if (state != null && item.getDuration() > 0
|
||||||
&& item.getStreamType() != StreamType.LIVE_STREAM) {
|
&& !StreamTypeUtil.isLiveStream(item.getStreamType())) {
|
||||||
itemProgressView.setMax((int) item.getDuration());
|
itemProgressView.setMax((int) item.getDuration());
|
||||||
if (itemProgressView.getVisibility() == View.VISIBLE) {
|
if (itemProgressView.getVisibility() == View.VISIBLE) {
|
||||||
itemProgressView.setProgressAnimated((int) TimeUnit.MILLISECONDS
|
itemProgressView.setProgressAnimated((int) TimeUnit.MILLISECONDS
|
||||||
|
@@ -300,14 +300,7 @@ private fun View.animateLightSlideAndAlpha(enterOrExit: Boolean, duration: Long,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun View.slideUp(
|
@JvmOverloads
|
||||||
duration: Long,
|
|
||||||
delay: Long,
|
|
||||||
@FloatRange(from = 0.0, to = 1.0) translationPercent: Float
|
|
||||||
) {
|
|
||||||
slideUp(duration, delay, translationPercent, null)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun View.slideUp(
|
fun View.slideUp(
|
||||||
duration: Long,
|
duration: Long,
|
||||||
delay: Long = 0L,
|
delay: Long = 0L,
|
||||||
|
@@ -25,7 +25,6 @@ import android.content.Context
|
|||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
import android.graphics.Typeface
|
import android.graphics.Typeface
|
||||||
import android.graphics.drawable.Drawable
|
|
||||||
import android.graphics.drawable.LayerDrawable
|
import android.graphics.drawable.LayerDrawable
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
@@ -37,7 +36,6 @@ import android.view.MenuItem
|
|||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.Button
|
import android.widget.Button
|
||||||
import androidx.annotation.AttrRes
|
|
||||||
import androidx.annotation.Nullable
|
import androidx.annotation.Nullable
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.appcompat.content.res.AppCompatResources
|
import androidx.appcompat.content.res.AppCompatResources
|
||||||
@@ -80,6 +78,7 @@ import org.schabi.newpipe.util.DeviceUtils
|
|||||||
import org.schabi.newpipe.util.Localization
|
import org.schabi.newpipe.util.Localization
|
||||||
import org.schabi.newpipe.util.NavigationHelper
|
import org.schabi.newpipe.util.NavigationHelper
|
||||||
import org.schabi.newpipe.util.ThemeHelper.getGridSpanCountStreams
|
import org.schabi.newpipe.util.ThemeHelper.getGridSpanCountStreams
|
||||||
|
import org.schabi.newpipe.util.ThemeHelper.resolveDrawable
|
||||||
import org.schabi.newpipe.util.ThemeHelper.shouldUseGridLayout
|
import org.schabi.newpipe.util.ThemeHelper.shouldUseGridLayout
|
||||||
import java.time.OffsetDateTime
|
import java.time.OffsetDateTime
|
||||||
import java.util.function.Consumer
|
import java.util.function.Consumer
|
||||||
@@ -579,19 +578,6 @@ class FeedFragment : BaseStateFragment<FeedState>() {
|
|||||||
lastNewItemsCount = highlightCount
|
lastNewItemsCount = highlightCount
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun resolveDrawable(context: Context, @AttrRes attrResId: Int): Drawable? {
|
|
||||||
return androidx.core.content.ContextCompat.getDrawable(
|
|
||||||
context,
|
|
||||||
android.util.TypedValue().apply {
|
|
||||||
context.theme.resolveAttribute(
|
|
||||||
attrResId,
|
|
||||||
this,
|
|
||||||
true
|
|
||||||
)
|
|
||||||
}.resourceId
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun showNewItemsLoaded() {
|
private fun showNewItemsLoaded() {
|
||||||
tryGetNewItemsLoadedButton()?.clearAnimation()
|
tryGetNewItemsLoadedButton()?.clearAnimation()
|
||||||
tryGetNewItemsLoadedButton()
|
tryGetNewItemsLoadedButton()
|
||||||
|
@@ -14,6 +14,8 @@ import org.schabi.newpipe.databinding.ListStreamItemBinding
|
|||||||
import org.schabi.newpipe.extractor.stream.StreamType.AUDIO_LIVE_STREAM
|
import org.schabi.newpipe.extractor.stream.StreamType.AUDIO_LIVE_STREAM
|
||||||
import org.schabi.newpipe.extractor.stream.StreamType.AUDIO_STREAM
|
import org.schabi.newpipe.extractor.stream.StreamType.AUDIO_STREAM
|
||||||
import org.schabi.newpipe.extractor.stream.StreamType.LIVE_STREAM
|
import org.schabi.newpipe.extractor.stream.StreamType.LIVE_STREAM
|
||||||
|
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.extractor.stream.StreamType.VIDEO_STREAM
|
||||||
import org.schabi.newpipe.util.Localization
|
import org.schabi.newpipe.util.Localization
|
||||||
import org.schabi.newpipe.util.PicassoHelper
|
import org.schabi.newpipe.util.PicassoHelper
|
||||||
@@ -109,7 +111,7 @@ data class StreamItem(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun isLongClickable() = when (stream.streamType) {
|
override fun isLongClickable() = when (stream.streamType) {
|
||||||
AUDIO_STREAM, VIDEO_STREAM, LIVE_STREAM, AUDIO_LIVE_STREAM -> true
|
AUDIO_STREAM, VIDEO_STREAM, LIVE_STREAM, AUDIO_LIVE_STREAM, POST_LIVE_STREAM, POST_LIVE_AUDIO_STREAM -> true
|
||||||
else -> false
|
else -> false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -128,13 +128,11 @@ public class HistoryRecordManager {
|
|||||||
|
|
||||||
// Add a history entry
|
// Add a history entry
|
||||||
final StreamHistoryEntity latestEntry = streamHistoryTable.getLatestEntry(streamId);
|
final StreamHistoryEntity latestEntry = streamHistoryTable.getLatestEntry(streamId);
|
||||||
if (latestEntry != null) {
|
if (latestEntry == null) {
|
||||||
streamHistoryTable.delete(latestEntry);
|
// never actually viewed: add history entry but with 0 views
|
||||||
latestEntry.setAccessDate(currentTime);
|
return streamHistoryTable.insert(new StreamHistoryEntity(streamId, currentTime, 0));
|
||||||
latestEntry.setRepeatCount(latestEntry.getRepeatCount() + 1);
|
|
||||||
return streamHistoryTable.insert(latestEntry);
|
|
||||||
} else {
|
} else {
|
||||||
return streamHistoryTable.insert(new StreamHistoryEntity(streamId, currentTime));
|
return 0L;
|
||||||
}
|
}
|
||||||
})).subscribeOn(Schedulers.io());
|
})).subscribeOn(Schedulers.io());
|
||||||
}
|
}
|
||||||
@@ -155,7 +153,8 @@ public class HistoryRecordManager {
|
|||||||
latestEntry.setRepeatCount(latestEntry.getRepeatCount() + 1);
|
latestEntry.setRepeatCount(latestEntry.getRepeatCount() + 1);
|
||||||
return streamHistoryTable.insert(latestEntry);
|
return streamHistoryTable.insert(latestEntry);
|
||||||
} else {
|
} else {
|
||||||
return streamHistoryTable.insert(new StreamHistoryEntity(streamId, currentTime));
|
// just viewed for the first time: set 1 view
|
||||||
|
return streamHistoryTable.insert(new StreamHistoryEntity(streamId, currentTime, 1));
|
||||||
}
|
}
|
||||||
})).subscribeOn(Schedulers.io());
|
})).subscribeOn(Schedulers.io());
|
||||||
}
|
}
|
||||||
|
@@ -11,12 +11,12 @@ import androidx.core.content.ContextCompat;
|
|||||||
import org.schabi.newpipe.R;
|
import org.schabi.newpipe.R;
|
||||||
import org.schabi.newpipe.database.LocalItem;
|
import org.schabi.newpipe.database.LocalItem;
|
||||||
import org.schabi.newpipe.database.playlist.PlaylistStreamEntry;
|
import org.schabi.newpipe.database.playlist.PlaylistStreamEntry;
|
||||||
import org.schabi.newpipe.extractor.NewPipe;
|
|
||||||
import org.schabi.newpipe.ktx.ViewUtils;
|
import org.schabi.newpipe.ktx.ViewUtils;
|
||||||
import org.schabi.newpipe.local.LocalItemBuilder;
|
import org.schabi.newpipe.local.LocalItemBuilder;
|
||||||
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
||||||
import org.schabi.newpipe.util.PicassoHelper;
|
|
||||||
import org.schabi.newpipe.util.Localization;
|
import org.schabi.newpipe.util.Localization;
|
||||||
|
import org.schabi.newpipe.util.PicassoHelper;
|
||||||
|
import org.schabi.newpipe.util.ServiceHelper;
|
||||||
import org.schabi.newpipe.views.AnimatedProgressBar;
|
import org.schabi.newpipe.views.AnimatedProgressBar;
|
||||||
|
|
||||||
import java.time.format.DateTimeFormatter;
|
import java.time.format.DateTimeFormatter;
|
||||||
@@ -59,7 +59,7 @@ public class LocalPlaylistStreamItemHolder extends LocalItemHolder {
|
|||||||
itemVideoTitleView.setText(item.getStreamEntity().getTitle());
|
itemVideoTitleView.setText(item.getStreamEntity().getTitle());
|
||||||
itemAdditionalDetailsView.setText(Localization
|
itemAdditionalDetailsView.setText(Localization
|
||||||
.concatenateStrings(item.getStreamEntity().getUploader(),
|
.concatenateStrings(item.getStreamEntity().getUploader(),
|
||||||
NewPipe.getNameOfService(item.getStreamEntity().getServiceId())));
|
ServiceHelper.getNameOfServiceById(item.getStreamEntity().getServiceId())));
|
||||||
|
|
||||||
if (item.getStreamEntity().getDuration() > 0) {
|
if (item.getStreamEntity().getDuration() > 0) {
|
||||||
itemDurationView.setText(Localization
|
itemDurationView.setText(Localization
|
||||||
|
@@ -11,12 +11,12 @@ import androidx.core.content.ContextCompat;
|
|||||||
import org.schabi.newpipe.R;
|
import org.schabi.newpipe.R;
|
||||||
import org.schabi.newpipe.database.LocalItem;
|
import org.schabi.newpipe.database.LocalItem;
|
||||||
import org.schabi.newpipe.database.stream.StreamStatisticsEntry;
|
import org.schabi.newpipe.database.stream.StreamStatisticsEntry;
|
||||||
import org.schabi.newpipe.extractor.NewPipe;
|
|
||||||
import org.schabi.newpipe.ktx.ViewUtils;
|
import org.schabi.newpipe.ktx.ViewUtils;
|
||||||
import org.schabi.newpipe.local.LocalItemBuilder;
|
import org.schabi.newpipe.local.LocalItemBuilder;
|
||||||
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
||||||
import org.schabi.newpipe.util.PicassoHelper;
|
|
||||||
import org.schabi.newpipe.util.Localization;
|
import org.schabi.newpipe.util.Localization;
|
||||||
|
import org.schabi.newpipe.util.PicassoHelper;
|
||||||
|
import org.schabi.newpipe.util.ServiceHelper;
|
||||||
import org.schabi.newpipe.views.AnimatedProgressBar;
|
import org.schabi.newpipe.views.AnimatedProgressBar;
|
||||||
|
|
||||||
import java.time.format.DateTimeFormatter;
|
import java.time.format.DateTimeFormatter;
|
||||||
@@ -70,11 +70,12 @@ public class LocalStatisticStreamItemHolder extends LocalItemHolder {
|
|||||||
|
|
||||||
private String getStreamInfoDetailLine(final StreamStatisticsEntry entry,
|
private String getStreamInfoDetailLine(final StreamStatisticsEntry entry,
|
||||||
final DateTimeFormatter dateTimeFormatter) {
|
final DateTimeFormatter dateTimeFormatter) {
|
||||||
final String watchCount = Localization
|
return Localization.concatenateStrings(
|
||||||
.shortViewCount(itemBuilder.getContext(), entry.getWatchCount());
|
// watchCount
|
||||||
final String uploadDate = dateTimeFormatter.format(entry.getLatestAccessDate());
|
Localization.shortViewCount(itemBuilder.getContext(), entry.getWatchCount()),
|
||||||
final String serviceName = NewPipe.getNameOfService(entry.getStreamEntity().getServiceId());
|
dateTimeFormatter.format(entry.getLatestAccessDate()),
|
||||||
return Localization.concatenateStrings(watchCount, uploadDate, serviceName);
|
// serviceName
|
||||||
|
ServiceHelper.getNameOfServiceById(entry.getStreamEntity().getServiceId()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@@ -5,11 +5,11 @@ import android.view.ViewGroup;
|
|||||||
|
|
||||||
import org.schabi.newpipe.database.LocalItem;
|
import org.schabi.newpipe.database.LocalItem;
|
||||||
import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity;
|
import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity;
|
||||||
import org.schabi.newpipe.extractor.NewPipe;
|
|
||||||
import org.schabi.newpipe.local.LocalItemBuilder;
|
import org.schabi.newpipe.local.LocalItemBuilder;
|
||||||
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
||||||
import org.schabi.newpipe.util.PicassoHelper;
|
|
||||||
import org.schabi.newpipe.util.Localization;
|
import org.schabi.newpipe.util.Localization;
|
||||||
|
import org.schabi.newpipe.util.PicassoHelper;
|
||||||
|
import org.schabi.newpipe.util.ServiceHelper;
|
||||||
|
|
||||||
import java.time.format.DateTimeFormatter;
|
import java.time.format.DateTimeFormatter;
|
||||||
|
|
||||||
@@ -39,9 +39,9 @@ public class RemotePlaylistItemHolder extends PlaylistItemHolder {
|
|||||||
// Here is where the uploader name is set in the bookmarked playlists library
|
// Here is where the uploader name is set in the bookmarked playlists library
|
||||||
if (!TextUtils.isEmpty(item.getUploader())) {
|
if (!TextUtils.isEmpty(item.getUploader())) {
|
||||||
itemUploaderView.setText(Localization.concatenateStrings(item.getUploader(),
|
itemUploaderView.setText(Localization.concatenateStrings(item.getUploader(),
|
||||||
NewPipe.getNameOfService(item.getServiceId())));
|
ServiceHelper.getNameOfServiceById(item.getServiceId())));
|
||||||
} else {
|
} else {
|
||||||
itemUploaderView.setText(NewPipe.getNameOfService(item.getServiceId()));
|
itemUploaderView.setText(ServiceHelper.getNameOfServiceById(item.getServiceId()));
|
||||||
}
|
}
|
||||||
|
|
||||||
PicassoHelper.loadPlaylistThumbnail(item.getThumbnailUrl()).into(itemThumbnailView);
|
PicassoHelper.loadPlaylistThumbnail(item.getThumbnailUrl()).into(itemThumbnailView);
|
||||||
|
@@ -419,9 +419,11 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
|
|||||||
final PlaylistStreamEntry playlistItem = playlistIter.next();
|
final PlaylistStreamEntry playlistItem = playlistIter.next();
|
||||||
final int indexInHistory = Collections.binarySearch(historyStreamIds,
|
final int indexInHistory = Collections.binarySearch(historyStreamIds,
|
||||||
playlistItem.getStreamId());
|
playlistItem.getStreamId());
|
||||||
|
final StreamStateEntity streamStateEntity = streamStatesIter.next();
|
||||||
|
final long duration = playlistItem.toStreamInfoItem().getDuration();
|
||||||
|
|
||||||
final boolean hasState = streamStatesIter.next() != null;
|
if (indexInHistory < 0 || (streamStateEntity != null
|
||||||
if (indexInHistory < 0 || hasState) {
|
&& !streamStateEntity.isFinished(duration))) {
|
||||||
notWatchedItems.add(playlistItem);
|
notWatchedItems.add(playlistItem);
|
||||||
} else if (!thumbnailVideoRemoved
|
} else if (!thumbnailVideoRemoved
|
||||||
&& playlistManager.getPlaylistThumbnail(playlistId)
|
&& playlistManager.getPlaylistThumbnail(playlistId)
|
||||||
|
@@ -1,24 +1,24 @@
|
|||||||
package org.schabi.newpipe.local.subscription
|
package org.schabi.newpipe.local.subscription
|
||||||
|
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.content.BroadcastReceiver
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.DialogInterface
|
import android.content.DialogInterface
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.IntentFilter
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.Menu
|
import android.view.Menu
|
||||||
import android.view.MenuInflater
|
import android.view.MenuInflater
|
||||||
|
import android.view.MenuItem
|
||||||
|
import android.view.SubMenu
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.activity.result.ActivityResult
|
import androidx.activity.result.ActivityResult
|
||||||
import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult
|
import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult
|
||||||
|
import androidx.annotation.StringRes
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.lifecycle.ViewModelProvider
|
import androidx.lifecycle.ViewModelProvider
|
||||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager
|
|
||||||
import androidx.recyclerview.widget.GridLayoutManager
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
import com.xwray.groupie.Group
|
import com.xwray.groupie.Group
|
||||||
import com.xwray.groupie.GroupAdapter
|
import com.xwray.groupie.GroupAdapter
|
||||||
@@ -34,6 +34,7 @@ import org.schabi.newpipe.databinding.FeedItemCarouselBinding
|
|||||||
import org.schabi.newpipe.databinding.FragmentSubscriptionBinding
|
import org.schabi.newpipe.databinding.FragmentSubscriptionBinding
|
||||||
import org.schabi.newpipe.error.ErrorInfo
|
import org.schabi.newpipe.error.ErrorInfo
|
||||||
import org.schabi.newpipe.error.UserAction
|
import org.schabi.newpipe.error.UserAction
|
||||||
|
import org.schabi.newpipe.extractor.ServiceList
|
||||||
import org.schabi.newpipe.extractor.channel.ChannelInfoItem
|
import org.schabi.newpipe.extractor.channel.ChannelInfoItem
|
||||||
import org.schabi.newpipe.fragments.BaseStateFragment
|
import org.schabi.newpipe.fragments.BaseStateFragment
|
||||||
import org.schabi.newpipe.ktx.animate
|
import org.schabi.newpipe.ktx.animate
|
||||||
@@ -45,13 +46,10 @@ import org.schabi.newpipe.local.subscription.item.EmptyPlaceholderItem
|
|||||||
import org.schabi.newpipe.local.subscription.item.FeedGroupAddItem
|
import org.schabi.newpipe.local.subscription.item.FeedGroupAddItem
|
||||||
import org.schabi.newpipe.local.subscription.item.FeedGroupCardItem
|
import org.schabi.newpipe.local.subscription.item.FeedGroupCardItem
|
||||||
import org.schabi.newpipe.local.subscription.item.FeedGroupCarouselItem
|
import org.schabi.newpipe.local.subscription.item.FeedGroupCarouselItem
|
||||||
import org.schabi.newpipe.local.subscription.item.FeedImportExportItem
|
|
||||||
import org.schabi.newpipe.local.subscription.item.HeaderWithMenuItem
|
import org.schabi.newpipe.local.subscription.item.HeaderWithMenuItem
|
||||||
import org.schabi.newpipe.local.subscription.item.HeaderWithMenuItem.Companion.PAYLOAD_UPDATE_VISIBILITY_MENU_ITEM
|
import org.schabi.newpipe.local.subscription.item.HeaderWithMenuItem.Companion.PAYLOAD_UPDATE_VISIBILITY_MENU_ITEM
|
||||||
import org.schabi.newpipe.local.subscription.services.SubscriptionsExportService
|
import org.schabi.newpipe.local.subscription.services.SubscriptionsExportService
|
||||||
import org.schabi.newpipe.local.subscription.services.SubscriptionsExportService.EXPORT_COMPLETE_ACTION
|
|
||||||
import org.schabi.newpipe.local.subscription.services.SubscriptionsImportService
|
import org.schabi.newpipe.local.subscription.services.SubscriptionsImportService
|
||||||
import org.schabi.newpipe.local.subscription.services.SubscriptionsImportService.IMPORT_COMPLETE_ACTION
|
|
||||||
import org.schabi.newpipe.local.subscription.services.SubscriptionsImportService.KEY_MODE
|
import org.schabi.newpipe.local.subscription.services.SubscriptionsImportService.KEY_MODE
|
||||||
import org.schabi.newpipe.local.subscription.services.SubscriptionsImportService.KEY_VALUE
|
import org.schabi.newpipe.local.subscription.services.SubscriptionsImportService.KEY_VALUE
|
||||||
import org.schabi.newpipe.local.subscription.services.SubscriptionsImportService.PREVIOUS_EXPORT_MODE
|
import org.schabi.newpipe.local.subscription.services.SubscriptionsImportService.PREVIOUS_EXPORT_MODE
|
||||||
@@ -59,6 +57,7 @@ import org.schabi.newpipe.streams.io.NoFileManagerSafeGuard
|
|||||||
import org.schabi.newpipe.streams.io.StoredFileHelper
|
import org.schabi.newpipe.streams.io.StoredFileHelper
|
||||||
import org.schabi.newpipe.util.NavigationHelper
|
import org.schabi.newpipe.util.NavigationHelper
|
||||||
import org.schabi.newpipe.util.OnClickGesture
|
import org.schabi.newpipe.util.OnClickGesture
|
||||||
|
import org.schabi.newpipe.util.ServiceHelper
|
||||||
import org.schabi.newpipe.util.ThemeHelper.getGridSpanCountChannels
|
import org.schabi.newpipe.util.ThemeHelper.getGridSpanCountChannels
|
||||||
import org.schabi.newpipe.util.ThemeHelper.shouldUseGridLayout
|
import org.schabi.newpipe.util.ThemeHelper.shouldUseGridLayout
|
||||||
import org.schabi.newpipe.util.external_communication.ShareUtils
|
import org.schabi.newpipe.util.external_communication.ShareUtils
|
||||||
@@ -74,12 +73,9 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
|
|||||||
private lateinit var subscriptionManager: SubscriptionManager
|
private lateinit var subscriptionManager: SubscriptionManager
|
||||||
private val disposables: CompositeDisposable = CompositeDisposable()
|
private val disposables: CompositeDisposable = CompositeDisposable()
|
||||||
|
|
||||||
private var subscriptionBroadcastReceiver: BroadcastReceiver? = null
|
|
||||||
|
|
||||||
private val groupAdapter = GroupAdapter<GroupieViewHolder<FeedItemCarouselBinding>>()
|
private val groupAdapter = GroupAdapter<GroupieViewHolder<FeedItemCarouselBinding>>()
|
||||||
private val feedGroupsSection = Section()
|
private val feedGroupsSection = Section()
|
||||||
private var feedGroupsCarousel: FeedGroupCarouselItem? = null
|
private var feedGroupsCarousel: FeedGroupCarouselItem? = null
|
||||||
private lateinit var importExportItem: FeedImportExportItem
|
|
||||||
private lateinit var feedGroupsSortMenuItem: HeaderWithMenuItem
|
private lateinit var feedGroupsSortMenuItem: HeaderWithMenuItem
|
||||||
private val subscriptionsSection = Section()
|
private val subscriptionsSection = Section()
|
||||||
|
|
||||||
@@ -91,12 +87,10 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
|
|||||||
@State
|
@State
|
||||||
@JvmField
|
@JvmField
|
||||||
var itemsListState: Parcelable? = null
|
var itemsListState: Parcelable? = null
|
||||||
|
|
||||||
@State
|
@State
|
||||||
@JvmField
|
@JvmField
|
||||||
var feedGroupsListState: Parcelable? = null
|
var feedGroupsListState: Parcelable? = null
|
||||||
@State
|
|
||||||
@JvmField
|
|
||||||
var importExportItemExpandedState: Boolean? = null
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
setHasOptionsMenu(true)
|
setHasOptionsMenu(true)
|
||||||
@@ -120,20 +114,10 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
|
|||||||
return inflater.inflate(R.layout.fragment_subscription, container, false)
|
return inflater.inflate(R.layout.fragment_subscription, container, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onResume() {
|
|
||||||
super.onResume()
|
|
||||||
setupBroadcastReceiver()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onPause() {
|
override fun onPause() {
|
||||||
super.onPause()
|
super.onPause()
|
||||||
itemsListState = binding.itemsList.layoutManager?.onSaveInstanceState()
|
itemsListState = binding.itemsList.layoutManager?.onSaveInstanceState()
|
||||||
feedGroupsListState = feedGroupsCarousel?.onSaveInstanceState()
|
feedGroupsListState = feedGroupsCarousel?.onSaveInstanceState()
|
||||||
importExportItemExpandedState = importExportItem.isExpanded
|
|
||||||
|
|
||||||
if (subscriptionBroadcastReceiver != null && activity != null) {
|
|
||||||
LocalBroadcastManager.getInstance(activity).unregisterReceiver(subscriptionBroadcastReceiver!!)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
@@ -150,28 +134,61 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
|
|||||||
|
|
||||||
activity.supportActionBar?.setDisplayShowTitleEnabled(true)
|
activity.supportActionBar?.setDisplayShowTitleEnabled(true)
|
||||||
activity.supportActionBar?.setTitle(R.string.tab_subscriptions)
|
activity.supportActionBar?.setTitle(R.string.tab_subscriptions)
|
||||||
|
|
||||||
|
buildImportExportMenu(menu)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setupBroadcastReceiver() {
|
private fun buildImportExportMenu(menu: Menu) {
|
||||||
if (activity == null) return
|
// -- Import --
|
||||||
|
val importSubMenu = menu.addSubMenu(R.string.import_from)
|
||||||
|
|
||||||
if (subscriptionBroadcastReceiver != null) {
|
addMenuItemToSubmenu(importSubMenu, R.string.previous_export) { onImportPreviousSelected() }
|
||||||
LocalBroadcastManager.getInstance(activity).unregisterReceiver(subscriptionBroadcastReceiver!!)
|
.setIcon(R.drawable.ic_backup)
|
||||||
}
|
|
||||||
|
|
||||||
val filters = IntentFilter()
|
for (service in ServiceList.all()) {
|
||||||
filters.addAction(EXPORT_COMPLETE_ACTION)
|
val subscriptionExtractor = service.subscriptionExtractor ?: continue
|
||||||
filters.addAction(IMPORT_COMPLETE_ACTION)
|
|
||||||
subscriptionBroadcastReceiver = object : BroadcastReceiver() {
|
val supportedSources = subscriptionExtractor.supportedSources
|
||||||
override fun onReceive(context: Context, intent: Intent) {
|
if (supportedSources.isEmpty()) continue
|
||||||
_binding?.itemsList?.post {
|
|
||||||
importExportItem.isExpanded = false
|
addMenuItemToSubmenu(importSubMenu, service.serviceInfo.name) {
|
||||||
importExportItem.notifyChanged(FeedImportExportItem.REFRESH_EXPANDED_STATUS)
|
onImportFromServiceSelected(service.serviceId)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
.setIcon(ServiceHelper.getIcon(service.serviceId))
|
||||||
}
|
}
|
||||||
|
|
||||||
LocalBroadcastManager.getInstance(activity).registerReceiver(subscriptionBroadcastReceiver!!, filters)
|
// -- Export --
|
||||||
|
val exportSubMenu = menu.addSubMenu(R.string.export_to)
|
||||||
|
|
||||||
|
addMenuItemToSubmenu(exportSubMenu, R.string.file) { onExportSelected() }
|
||||||
|
.setIcon(R.drawable.ic_save)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun addMenuItemToSubmenu(
|
||||||
|
subMenu: SubMenu,
|
||||||
|
@StringRes title: Int,
|
||||||
|
onClick: Runnable
|
||||||
|
): MenuItem {
|
||||||
|
return setClickListenerToMenuItem(subMenu.add(title), onClick)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun addMenuItemToSubmenu(
|
||||||
|
subMenu: SubMenu,
|
||||||
|
title: String,
|
||||||
|
onClick: Runnable
|
||||||
|
): MenuItem {
|
||||||
|
return setClickListenerToMenuItem(subMenu.add(title), onClick)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setClickListenerToMenuItem(
|
||||||
|
menuItem: MenuItem,
|
||||||
|
onClick: Runnable
|
||||||
|
): MenuItem {
|
||||||
|
menuItem.setOnMenuItemClickListener { _ ->
|
||||||
|
onClick.run()
|
||||||
|
true
|
||||||
|
}
|
||||||
|
return menuItem
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onImportFromServiceSelected(serviceId: Int) {
|
private fun onImportFromServiceSelected(serviceId: Int) {
|
||||||
@@ -263,13 +280,14 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
|
|||||||
subscriptionsSection.setPlaceholder(EmptyPlaceholderItem())
|
subscriptionsSection.setPlaceholder(EmptyPlaceholderItem())
|
||||||
subscriptionsSection.setHideWhenEmpty(true)
|
subscriptionsSection.setHideWhenEmpty(true)
|
||||||
|
|
||||||
importExportItem = FeedImportExportItem(
|
groupAdapter.add(
|
||||||
{ onImportPreviousSelected() },
|
Section(
|
||||||
{ onImportFromServiceSelected(it) },
|
HeaderWithMenuItem(
|
||||||
{ onExportSelected() },
|
getString(R.string.tab_subscriptions)
|
||||||
importExportItemExpandedState ?: false
|
),
|
||||||
|
listOf(subscriptionsSection)
|
||||||
|
)
|
||||||
)
|
)
|
||||||
groupAdapter.add(Section(importExportItem, listOf(subscriptionsSection)))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun initViews(rootView: View, savedInstanceState: Bundle?) {
|
override fun initViews(rootView: View, savedInstanceState: Bundle?) {
|
||||||
@@ -371,13 +389,6 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
|
|||||||
subscriptionsSection.update(result.subscriptions)
|
subscriptionsSection.update(result.subscriptions)
|
||||||
subscriptionsSection.setHideWhenEmpty(false)
|
subscriptionsSection.setHideWhenEmpty(false)
|
||||||
|
|
||||||
if (result.subscriptions.isEmpty() && importExportItemExpandedState == null) {
|
|
||||||
binding.itemsList.post {
|
|
||||||
importExportItem.isExpanded = true
|
|
||||||
importExportItem.notifyChanged(FeedImportExportItem.REFRESH_EXPANDED_STATUS)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (itemsListState != null) {
|
if (itemsListState != null) {
|
||||||
binding.itemsList.layoutManager?.onRestoreInstanceState(itemsListState)
|
binding.itemsList.layoutManager?.onRestoreInstanceState(itemsListState)
|
||||||
itemsListState = null
|
itemsListState = null
|
||||||
|
@@ -1,5 +1,11 @@
|
|||||||
package org.schabi.newpipe.local.subscription;
|
package org.schabi.newpipe.local.subscription;
|
||||||
|
|
||||||
|
import static org.schabi.newpipe.extractor.subscription.SubscriptionExtractor.ContentSource.CHANNEL_URL;
|
||||||
|
import static org.schabi.newpipe.local.subscription.services.SubscriptionsImportService.CHANNEL_URL_MODE;
|
||||||
|
import static org.schabi.newpipe.local.subscription.services.SubscriptionsImportService.INPUT_STREAM_MODE;
|
||||||
|
import static org.schabi.newpipe.local.subscription.services.SubscriptionsImportService.KEY_MODE;
|
||||||
|
import static org.schabi.newpipe.local.subscription.services.SubscriptionsImportService.KEY_VALUE;
|
||||||
|
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
@@ -40,12 +46,6 @@ import java.util.List;
|
|||||||
|
|
||||||
import icepick.State;
|
import icepick.State;
|
||||||
|
|
||||||
import static org.schabi.newpipe.extractor.subscription.SubscriptionExtractor.ContentSource.CHANNEL_URL;
|
|
||||||
import static org.schabi.newpipe.local.subscription.services.SubscriptionsImportService.CHANNEL_URL_MODE;
|
|
||||||
import static org.schabi.newpipe.local.subscription.services.SubscriptionsImportService.INPUT_STREAM_MODE;
|
|
||||||
import static org.schabi.newpipe.local.subscription.services.SubscriptionsImportService.KEY_MODE;
|
|
||||||
import static org.schabi.newpipe.local.subscription.services.SubscriptionsImportService.KEY_VALUE;
|
|
||||||
|
|
||||||
public class SubscriptionsImportFragment extends BaseFragment {
|
public class SubscriptionsImportFragment extends BaseFragment {
|
||||||
@State
|
@State
|
||||||
int currentServiceId = Constants.NO_SERVICE_ID;
|
int currentServiceId = Constants.NO_SERVICE_ID;
|
||||||
@@ -89,7 +89,7 @@ public class SubscriptionsImportFragment extends BaseFragment {
|
|||||||
if (supportedSources.isEmpty() && currentServiceId != Constants.NO_SERVICE_ID) {
|
if (supportedSources.isEmpty() && currentServiceId != Constants.NO_SERVICE_ID) {
|
||||||
ErrorUtil.showSnackbar(activity,
|
ErrorUtil.showSnackbar(activity,
|
||||||
new ErrorInfo(new String[]{}, UserAction.SUBSCRIPTION_IMPORT_EXPORT,
|
new ErrorInfo(new String[]{}, UserAction.SUBSCRIPTION_IMPORT_EXPORT,
|
||||||
NewPipe.getNameOfService(currentServiceId),
|
ServiceHelper.getNameOfServiceById(currentServiceId),
|
||||||
"Service does not support importing subscriptions",
|
"Service does not support importing subscriptions",
|
||||||
R.string.general_error));
|
R.string.general_error));
|
||||||
activity.finish();
|
activity.finish();
|
||||||
|
@@ -1,7 +1,6 @@
|
|||||||
package org.schabi.newpipe.local.subscription.dialog
|
package org.schabi.newpipe.local.subscription.dialog
|
||||||
|
|
||||||
import android.app.Dialog
|
import android.app.Dialog
|
||||||
import android.content.res.ColorStateList
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
@@ -9,7 +8,7 @@ import android.view.View
|
|||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.view.inputmethod.InputMethodManager
|
import android.view.inputmethod.InputMethodManager
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.appcompat.content.res.AppCompatResources
|
||||||
import androidx.core.content.getSystemService
|
import androidx.core.content.getSystemService
|
||||||
import androidx.core.os.bundleOf
|
import androidx.core.os.bundleOf
|
||||||
import androidx.core.view.isGone
|
import androidx.core.view.isGone
|
||||||
@@ -127,7 +126,7 @@ class FeedGroupDialog : DialogFragment(), BackPressable {
|
|||||||
|
|
||||||
if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.LOLLIPOP) {
|
if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.LOLLIPOP) {
|
||||||
// KitKat doesn't apply container's theme to <include> content
|
// KitKat doesn't apply container's theme to <include> content
|
||||||
val contrastColor = ColorStateList.valueOf(ContextCompat.getColor(requireContext(), R.color.contrastColor))
|
val contrastColor = AppCompatResources.getColorStateList(requireContext(), R.color.contrastColor)
|
||||||
searchLayoutBinding.toolbarSearchEditText.setTextColor(contrastColor)
|
searchLayoutBinding.toolbarSearchEditText.setTextColor(contrastColor)
|
||||||
searchLayoutBinding.toolbarSearchEditText.setHintTextColor(contrastColor.withAlpha(128))
|
searchLayoutBinding.toolbarSearchEditText.setHintTextColor(contrastColor.withAlpha(128))
|
||||||
ImageViewCompat.setImageTintList(searchLayoutBinding.toolbarSearchClearIcon, contrastColor)
|
ImageViewCompat.setImageTintList(searchLayoutBinding.toolbarSearchClearIcon, contrastColor)
|
||||||
|
@@ -1,122 +0,0 @@
|
|||||||
package org.schabi.newpipe.local.subscription.item
|
|
||||||
|
|
||||||
import android.graphics.Color
|
|
||||||
import android.graphics.PorterDuff
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import android.widget.ImageView
|
|
||||||
import android.widget.TextView
|
|
||||||
import androidx.annotation.DrawableRes
|
|
||||||
import com.xwray.groupie.viewbinding.BindableItem
|
|
||||||
import com.xwray.groupie.viewbinding.GroupieViewHolder
|
|
||||||
import org.schabi.newpipe.R
|
|
||||||
import org.schabi.newpipe.databinding.FeedImportExportGroupBinding
|
|
||||||
import org.schabi.newpipe.extractor.NewPipe
|
|
||||||
import org.schabi.newpipe.extractor.exceptions.ExtractionException
|
|
||||||
import org.schabi.newpipe.ktx.animateRotation
|
|
||||||
import org.schabi.newpipe.util.ServiceHelper
|
|
||||||
import org.schabi.newpipe.util.ThemeHelper
|
|
||||||
import org.schabi.newpipe.views.CollapsibleView
|
|
||||||
|
|
||||||
class FeedImportExportItem(
|
|
||||||
val onImportPreviousSelected: () -> Unit,
|
|
||||||
val onImportFromServiceSelected: (Int) -> Unit,
|
|
||||||
val onExportSelected: () -> Unit,
|
|
||||||
var isExpanded: Boolean = false
|
|
||||||
) : BindableItem<FeedImportExportGroupBinding>() {
|
|
||||||
companion object {
|
|
||||||
const val REFRESH_EXPANDED_STATUS = 123
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun bind(viewBinding: FeedImportExportGroupBinding, position: Int, payloads: MutableList<Any>) {
|
|
||||||
if (payloads.contains(REFRESH_EXPANDED_STATUS)) {
|
|
||||||
viewBinding.importExportOptions.apply { if (isExpanded) expand() else collapse() }
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
super.bind(viewBinding, position, payloads)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getLayout(): Int = R.layout.feed_import_export_group
|
|
||||||
|
|
||||||
override fun bind(viewBinding: FeedImportExportGroupBinding, position: Int) {
|
|
||||||
if (viewBinding.importFromOptions.childCount == 0) setupImportFromItems(viewBinding.importFromOptions)
|
|
||||||
if (viewBinding.exportToOptions.childCount == 0) setupExportToItems(viewBinding.exportToOptions)
|
|
||||||
|
|
||||||
expandIconListener?.let { viewBinding.importExportOptions.removeListener(it) }
|
|
||||||
expandIconListener = CollapsibleView.StateListener { newState ->
|
|
||||||
viewBinding.importExportExpandIcon.animateRotation(
|
|
||||||
250, if (newState == CollapsibleView.COLLAPSED) 0 else 180
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
viewBinding.importExportOptions.currentState = if (isExpanded) CollapsibleView.EXPANDED else CollapsibleView.COLLAPSED
|
|
||||||
viewBinding.importExportExpandIcon.rotation = if (isExpanded) 180F else 0F
|
|
||||||
viewBinding.importExportOptions.ready()
|
|
||||||
|
|
||||||
viewBinding.importExportOptions.addListener(expandIconListener)
|
|
||||||
viewBinding.importExport.setOnClickListener {
|
|
||||||
viewBinding.importExportOptions.switchState()
|
|
||||||
isExpanded = viewBinding.importExportOptions.currentState == CollapsibleView.EXPANDED
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun unbind(viewHolder: GroupieViewHolder<FeedImportExportGroupBinding>) {
|
|
||||||
super.unbind(viewHolder)
|
|
||||||
expandIconListener?.let { viewHolder.binding.importExportOptions.removeListener(it) }
|
|
||||||
expandIconListener = null
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun initializeViewBinding(view: View) = FeedImportExportGroupBinding.bind(view)
|
|
||||||
|
|
||||||
private var expandIconListener: CollapsibleView.StateListener? = null
|
|
||||||
|
|
||||||
private fun addItemView(title: String, @DrawableRes icon: Int, container: ViewGroup): View {
|
|
||||||
val itemRoot = View.inflate(container.context, R.layout.subscription_import_export_item, null)
|
|
||||||
val titleView = itemRoot.findViewById<TextView>(android.R.id.text1)
|
|
||||||
val iconView = itemRoot.findViewById<ImageView>(android.R.id.icon1)
|
|
||||||
|
|
||||||
titleView.text = title
|
|
||||||
iconView.setImageResource(icon)
|
|
||||||
|
|
||||||
container.addView(itemRoot)
|
|
||||||
return itemRoot
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun setupImportFromItems(listHolder: ViewGroup) {
|
|
||||||
val previousBackupItem = addItemView(
|
|
||||||
listHolder.context.getString(R.string.previous_export),
|
|
||||||
R.drawable.ic_backup, listHolder
|
|
||||||
)
|
|
||||||
previousBackupItem.setOnClickListener { onImportPreviousSelected() }
|
|
||||||
|
|
||||||
val iconColor = if (ThemeHelper.isLightThemeSelected(listHolder.context)) Color.BLACK else Color.WHITE
|
|
||||||
val services = listHolder.context.resources.getStringArray(R.array.service_list)
|
|
||||||
for (serviceName in services) {
|
|
||||||
try {
|
|
||||||
val service = NewPipe.getService(serviceName)
|
|
||||||
|
|
||||||
val subscriptionExtractor = service.subscriptionExtractor ?: continue
|
|
||||||
|
|
||||||
val supportedSources = subscriptionExtractor.supportedSources
|
|
||||||
if (supportedSources.isEmpty()) continue
|
|
||||||
|
|
||||||
val itemView = addItemView(serviceName, ServiceHelper.getIcon(service.serviceId), listHolder)
|
|
||||||
val iconView = itemView.findViewById<ImageView>(android.R.id.icon1)
|
|
||||||
iconView.setColorFilter(iconColor, PorterDuff.Mode.SRC_IN)
|
|
||||||
|
|
||||||
itemView.setOnClickListener { onImportFromServiceSelected(service.serviceId) }
|
|
||||||
} catch (e: ExtractionException) {
|
|
||||||
throw RuntimeException("Services array contains an entry that it's not a valid service name ($serviceName)", e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun setupExportToItems(listHolder: ViewGroup) {
|
|
||||||
val previousBackupItem = addItemView(
|
|
||||||
listHolder.context.getString(R.string.file),
|
|
||||||
R.drawable.ic_save, listHolder
|
|
||||||
)
|
|
||||||
previousBackupItem.setOnClickListener { onExportSelected() }
|
|
||||||
}
|
|
||||||
}
|
|
@@ -97,7 +97,10 @@ public final class PlayQueueActivity extends AppCompatActivity
|
|||||||
getMenuInflater().inflate(R.menu.menu_play_queue, m);
|
getMenuInflater().inflate(R.menu.menu_play_queue, m);
|
||||||
getMenuInflater().inflate(R.menu.menu_play_queue_bg, m);
|
getMenuInflater().inflate(R.menu.menu_play_queue_bg, m);
|
||||||
onMaybeMuteChanged();
|
onMaybeMuteChanged();
|
||||||
onPlaybackParameterChanged(player.getPlaybackParameters());
|
// to avoid null reference
|
||||||
|
if (player != null) {
|
||||||
|
onPlaybackParameterChanged(player.getPlaybackParameters());
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -88,6 +88,7 @@ import android.provider.Settings;
|
|||||||
import android.util.DisplayMetrics;
|
import android.util.DisplayMetrics;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import android.util.TypedValue;
|
import android.util.TypedValue;
|
||||||
|
import android.view.GestureDetector;
|
||||||
import android.view.Gravity;
|
import android.view.Gravity;
|
||||||
import android.view.KeyEvent;
|
import android.view.KeyEvent;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
@@ -118,7 +119,6 @@ import androidx.appcompat.widget.PopupMenu;
|
|||||||
import androidx.collection.ArraySet;
|
import androidx.collection.ArraySet;
|
||||||
import androidx.core.content.ContextCompat;
|
import androidx.core.content.ContextCompat;
|
||||||
import androidx.core.graphics.Insets;
|
import androidx.core.graphics.Insets;
|
||||||
import androidx.core.view.GestureDetectorCompat;
|
|
||||||
import androidx.core.view.ViewCompat;
|
import androidx.core.view.ViewCompat;
|
||||||
import androidx.core.view.WindowInsetsCompat;
|
import androidx.core.view.WindowInsetsCompat;
|
||||||
import androidx.fragment.app.FragmentManager;
|
import androidx.fragment.app.FragmentManager;
|
||||||
@@ -150,7 +150,6 @@ import com.google.android.material.floatingactionbutton.FloatingActionButton;
|
|||||||
import com.squareup.picasso.Picasso;
|
import com.squareup.picasso.Picasso;
|
||||||
import com.squareup.picasso.Target;
|
import com.squareup.picasso.Target;
|
||||||
|
|
||||||
import org.schabi.newpipe.DownloaderImpl;
|
|
||||||
import org.schabi.newpipe.MainActivity;
|
import org.schabi.newpipe.MainActivity;
|
||||||
import org.schabi.newpipe.R;
|
import org.schabi.newpipe.R;
|
||||||
import org.schabi.newpipe.database.stream.model.StreamEntity;
|
import org.schabi.newpipe.database.stream.model.StreamEntity;
|
||||||
@@ -387,7 +386,7 @@ public final class Player implements
|
|||||||
private static final float MAX_GESTURE_LENGTH = 0.75f;
|
private static final float MAX_GESTURE_LENGTH = 0.75f;
|
||||||
|
|
||||||
private int maxGestureLength; // scaled
|
private int maxGestureLength; // scaled
|
||||||
private GestureDetectorCompat gestureDetector;
|
private GestureDetector gestureDetector;
|
||||||
private PlayerGestureListener playerGestureListener;
|
private PlayerGestureListener playerGestureListener;
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
@@ -429,7 +428,7 @@ public final class Player implements
|
|||||||
setupBroadcastReceiver();
|
setupBroadcastReceiver();
|
||||||
|
|
||||||
trackSelector = new DefaultTrackSelector(context, PlayerHelper.getQualitySelector());
|
trackSelector = new DefaultTrackSelector(context, PlayerHelper.getQualitySelector());
|
||||||
final PlayerDataSource dataSource = new PlayerDataSource(context, DownloaderImpl.USER_AGENT,
|
final PlayerDataSource dataSource = new PlayerDataSource(context,
|
||||||
new DefaultBandwidthMeter.Builder(context).build());
|
new DefaultBandwidthMeter.Builder(context).build());
|
||||||
loadController = new LoadController();
|
loadController = new LoadController();
|
||||||
renderFactory = new DefaultRenderersFactory(context);
|
renderFactory = new DefaultRenderersFactory(context);
|
||||||
@@ -555,7 +554,7 @@ public final class Player implements
|
|||||||
binding.playbackLiveSync.setOnClickListener(this);
|
binding.playbackLiveSync.setOnClickListener(this);
|
||||||
|
|
||||||
playerGestureListener = new PlayerGestureListener(this, service);
|
playerGestureListener = new PlayerGestureListener(this, service);
|
||||||
gestureDetector = new GestureDetectorCompat(context, playerGestureListener);
|
gestureDetector = new GestureDetector(context, playerGestureListener);
|
||||||
binding.getRoot().setOnTouchListener(playerGestureListener);
|
binding.getRoot().setOnTouchListener(playerGestureListener);
|
||||||
|
|
||||||
binding.queueButton.setOnClickListener(v -> onQueueClicked());
|
binding.queueButton.setOnClickListener(v -> onQueueClicked());
|
||||||
@@ -1744,24 +1743,9 @@ public final class Player implements
|
|||||||
if (exoPlayerIsNull()) {
|
if (exoPlayerIsNull()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Use duration of currentItem for non-live streams,
|
|
||||||
// because HLS streams are fragmented
|
onUpdateProgress(Math.max((int) simpleExoPlayer.getCurrentPosition(), 0),
|
||||||
// and thus the whole duration is not available to the player
|
(int) simpleExoPlayer.getDuration(), simpleExoPlayer.getBufferedPercentage());
|
||||||
// TODO: revert #6307 when introducing proper HLS support
|
|
||||||
final int duration;
|
|
||||||
if (currentItem != null
|
|
||||||
&& !StreamTypeUtil.isLiveStream(currentItem.getStreamType())
|
|
||||||
) {
|
|
||||||
// convert seconds to milliseconds
|
|
||||||
duration = (int) (currentItem.getDuration() * 1000);
|
|
||||||
} else {
|
|
||||||
duration = (int) simpleExoPlayer.getDuration();
|
|
||||||
}
|
|
||||||
onUpdateProgress(
|
|
||||||
Math.max((int) simpleExoPlayer.getCurrentPosition(), 0),
|
|
||||||
duration,
|
|
||||||
simpleExoPlayer.getBufferedPercentage()
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private Disposable getProgressUpdateDisposable() {
|
private Disposable getProgressUpdateDisposable() {
|
||||||
@@ -2501,22 +2485,31 @@ public final class Player implements
|
|||||||
Listener.super.onEvents(player, events);
|
Listener.super.onEvents(player, events);
|
||||||
MediaItemTag.from(player.getCurrentMediaItem()).ifPresent(tag -> {
|
MediaItemTag.from(player.getCurrentMediaItem()).ifPresent(tag -> {
|
||||||
if (tag == currentMetadata) {
|
if (tag == currentMetadata) {
|
||||||
return;
|
return; // we still have the same metadata, no need to do anything
|
||||||
}
|
}
|
||||||
|
final StreamInfo previousInfo = Optional.ofNullable(currentMetadata)
|
||||||
|
.flatMap(MediaItemTag::getMaybeStreamInfo).orElse(null);
|
||||||
currentMetadata = tag;
|
currentMetadata = tag;
|
||||||
if (!tag.getErrors().isEmpty()) {
|
|
||||||
|
if (!currentMetadata.getErrors().isEmpty()) {
|
||||||
|
// new errors might have been added even if previousInfo == tag.getMaybeStreamInfo()
|
||||||
final ErrorInfo errorInfo = new ErrorInfo(
|
final ErrorInfo errorInfo = new ErrorInfo(
|
||||||
tag.getErrors().get(0),
|
currentMetadata.getErrors(),
|
||||||
UserAction.PLAY_STREAM,
|
UserAction.PLAY_STREAM,
|
||||||
"Loading failed for [" + tag.getTitle() + "]: " + tag.getStreamUrl(),
|
"Loading failed for [" + currentMetadata.getTitle()
|
||||||
tag.getServiceId());
|
+ "]: " + currentMetadata.getStreamUrl(),
|
||||||
|
currentMetadata.getServiceId());
|
||||||
ErrorUtil.createNotification(context, errorInfo);
|
ErrorUtil.createNotification(context, errorInfo);
|
||||||
}
|
}
|
||||||
tag.getMaybeStreamInfo().ifPresent(info -> {
|
|
||||||
|
currentMetadata.getMaybeStreamInfo().ifPresent(info -> {
|
||||||
if (DEBUG) {
|
if (DEBUG) {
|
||||||
Log.d(TAG, "ExoPlayer - onEvents() update stream info: " + info.getName());
|
Log.d(TAG, "ExoPlayer - onEvents() update stream info: " + info.getName());
|
||||||
}
|
}
|
||||||
updateMetadataWith(info);
|
if (previousInfo == null || !previousInfo.getUrl().equals(info.getUrl())) {
|
||||||
|
// only update with the new stream info if it has actually changed
|
||||||
|
updateMetadataWith(info);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -3399,6 +3392,7 @@ public final class Player implements
|
|||||||
|
|
||||||
switch (info.getStreamType()) {
|
switch (info.getStreamType()) {
|
||||||
case AUDIO_STREAM:
|
case AUDIO_STREAM:
|
||||||
|
case POST_LIVE_AUDIO_STREAM:
|
||||||
binding.surfaceView.setVisibility(View.GONE);
|
binding.surfaceView.setVisibility(View.GONE);
|
||||||
binding.endScreen.setVisibility(View.VISIBLE);
|
binding.endScreen.setVisibility(View.VISIBLE);
|
||||||
binding.playbackEndTime.setVisibility(View.VISIBLE);
|
binding.playbackEndTime.setVisibility(View.VISIBLE);
|
||||||
@@ -3417,6 +3411,7 @@ public final class Player implements
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case VIDEO_STREAM:
|
case VIDEO_STREAM:
|
||||||
|
case POST_LIVE_STREAM:
|
||||||
if (currentMetadata == null
|
if (currentMetadata == null
|
||||||
|| !currentMetadata.getMaybeQuality().isPresent()
|
|| !currentMetadata.getMaybeQuality().isPresent()
|
||||||
|| (info.getVideoStreams().isEmpty()
|
|| (info.getVideoStreams().isEmpty()
|
||||||
@@ -3484,10 +3479,10 @@ public final class Player implements
|
|||||||
for (int i = 0; i < availableStreams.size(); i++) {
|
for (int i = 0; i < availableStreams.size(); i++) {
|
||||||
final VideoStream videoStream = availableStreams.get(i);
|
final VideoStream videoStream = availableStreams.get(i);
|
||||||
qualityPopupMenu.getMenu().add(POPUP_MENU_ID_QUALITY, i, Menu.NONE, MediaFormat
|
qualityPopupMenu.getMenu().add(POPUP_MENU_ID_QUALITY, i, Menu.NONE, MediaFormat
|
||||||
.getNameById(videoStream.getFormatId()) + " " + videoStream.resolution);
|
.getNameById(videoStream.getFormatId()) + " " + videoStream.getResolution());
|
||||||
}
|
}
|
||||||
if (getSelectedVideoStream() != null) {
|
if (getSelectedVideoStream() != null) {
|
||||||
binding.qualityTextView.setText(getSelectedVideoStream().resolution);
|
binding.qualityTextView.setText(getSelectedVideoStream().getResolution());
|
||||||
}
|
}
|
||||||
qualityPopupMenu.setOnMenuItemClickListener(this);
|
qualityPopupMenu.setOnMenuItemClickListener(this);
|
||||||
qualityPopupMenu.setOnDismissListener(this);
|
qualityPopupMenu.setOnDismissListener(this);
|
||||||
@@ -3605,7 +3600,7 @@ public final class Player implements
|
|||||||
}
|
}
|
||||||
|
|
||||||
saveStreamProgressState(); //TODO added, check if good
|
saveStreamProgressState(); //TODO added, check if good
|
||||||
final String newResolution = availableStreams.get(menuItemIndex).resolution;
|
final String newResolution = availableStreams.get(menuItemIndex).getResolution();
|
||||||
setRecovery();
|
setRecovery();
|
||||||
setPlaybackQuality(newResolution);
|
setPlaybackQuality(newResolution);
|
||||||
reloadPlayQueueManager();
|
reloadPlayQueueManager();
|
||||||
@@ -3633,7 +3628,7 @@ public final class Player implements
|
|||||||
}
|
}
|
||||||
isSomePopupMenuVisible = false; //TODO check if this works
|
isSomePopupMenuVisible = false; //TODO check if this works
|
||||||
if (getSelectedVideoStream() != null) {
|
if (getSelectedVideoStream() != null) {
|
||||||
binding.qualityTextView.setText(getSelectedVideoStream().resolution);
|
binding.qualityTextView.setText(getSelectedVideoStream().getResolution());
|
||||||
}
|
}
|
||||||
if (isPlaying()) {
|
if (isPlaying()) {
|
||||||
hideControls(DEFAULT_CONTROLS_DURATION, 0);
|
hideControls(DEFAULT_CONTROLS_DURATION, 0);
|
||||||
@@ -4248,9 +4243,7 @@ public final class Player implements
|
|||||||
if (playQueueManagerReloadingNeeded(sourceType, info, getVideoRendererIndex())) {
|
if (playQueueManagerReloadingNeeded(sourceType, info, getVideoRendererIndex())) {
|
||||||
reloadPlayQueueManager();
|
reloadPlayQueueManager();
|
||||||
} else {
|
} else {
|
||||||
final StreamType streamType = info.getStreamType();
|
if (StreamTypeUtil.isAudio(info.getStreamType())) {
|
||||||
if (streamType == StreamType.AUDIO_STREAM
|
|
||||||
|| streamType == StreamType.AUDIO_LIVE_STREAM) {
|
|
||||||
// Nothing to do more than setting the recovery position
|
// Nothing to do more than setting the recovery position
|
||||||
setRecovery();
|
setRecovery();
|
||||||
return;
|
return;
|
||||||
@@ -4285,13 +4278,15 @@ public final class Player implements
|
|||||||
* the content is not an audio content, but also if none of the following cases is met:
|
* the content is not an audio content, but also if none of the following cases is met:
|
||||||
*
|
*
|
||||||
* <ul>
|
* <ul>
|
||||||
* <li>the content is an {@link StreamType#AUDIO_STREAM audio stream} or an
|
* <li>the content is an {@link StreamType#AUDIO_STREAM audio stream}, an
|
||||||
* {@link StreamType#AUDIO_LIVE_STREAM audio live stream};</li>
|
* {@link StreamType#AUDIO_LIVE_STREAM audio live stream}, or a
|
||||||
|
* {@link StreamType#POST_LIVE_AUDIO_STREAM ended audio live stream};</li>
|
||||||
* <li>the content is a {@link StreamType#LIVE_STREAM live stream} and the source type is a
|
* <li>the content is a {@link StreamType#LIVE_STREAM live stream} and the source type is a
|
||||||
* {@link SourceType#LIVE_STREAM live source};</li>
|
* {@link SourceType#LIVE_STREAM live source};</li>
|
||||||
* <li>the content's source is {@link SourceType#VIDEO_WITH_SEPARATED_AUDIO a video stream
|
* <li>the content's source is {@link SourceType#VIDEO_WITH_SEPARATED_AUDIO a video stream
|
||||||
* with a separated audio source} or has no audio-only streams available <b>and</b> is a
|
* with a separated audio source} or has no audio-only streams available <b>and</b> is a
|
||||||
* {@link StreamType#LIVE_STREAM live stream} or a
|
* {@link StreamType#VIDEO_STREAM video stream}, an
|
||||||
|
* {@link StreamType#POST_LIVE_STREAM ended live stream}, or a
|
||||||
* {@link StreamType#LIVE_STREAM live stream}.
|
* {@link StreamType#LIVE_STREAM live stream}.
|
||||||
* </li>
|
* </li>
|
||||||
* </ul>
|
* </ul>
|
||||||
@@ -4307,18 +4302,17 @@ public final class Player implements
|
|||||||
@NonNull final StreamInfo streamInfo,
|
@NonNull final StreamInfo streamInfo,
|
||||||
final int videoRendererIndex) {
|
final int videoRendererIndex) {
|
||||||
final StreamType streamType = streamInfo.getStreamType();
|
final StreamType streamType = streamInfo.getStreamType();
|
||||||
|
final boolean isStreamTypeAudio = StreamTypeUtil.isAudio(streamType);
|
||||||
|
|
||||||
if (videoRendererIndex == RENDERER_UNAVAILABLE && streamType != StreamType.AUDIO_STREAM
|
if (videoRendererIndex == RENDERER_UNAVAILABLE && !isStreamTypeAudio) {
|
||||||
&& streamType != StreamType.AUDIO_LIVE_STREAM) {
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// The content is an audio stream, an audio live stream, or a live stream with a live
|
// The content is an audio stream, an audio live stream, or a live stream with a live
|
||||||
// source: it's not needed to reload the play queue manager because the stream source will
|
// source: it's not needed to reload the play queue manager because the stream source will
|
||||||
// be the same
|
// be the same
|
||||||
if ((streamType == StreamType.AUDIO_STREAM || streamType == StreamType.AUDIO_LIVE_STREAM)
|
if (isStreamTypeAudio || (streamType == StreamType.LIVE_STREAM
|
||||||
|| (streamType == StreamType.LIVE_STREAM
|
&& sourceType == SourceType.LIVE_STREAM)) {
|
||||||
&& sourceType == SourceType.LIVE_STREAM)) {
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4331,8 +4325,8 @@ public final class Player implements
|
|||||||
|| (sourceType == SourceType.VIDEO_WITH_AUDIO_OR_AUDIO_ONLY
|
|| (sourceType == SourceType.VIDEO_WITH_AUDIO_OR_AUDIO_ONLY
|
||||||
&& isNullOrEmpty(streamInfo.getAudioStreams()))) {
|
&& isNullOrEmpty(streamInfo.getAudioStreams()))) {
|
||||||
// It's not needed to reload the play queue manager only if the content's stream type
|
// It's not needed to reload the play queue manager only if the content's stream type
|
||||||
// is a video stream or a live stream
|
// is a video stream, a live stream or an ended live stream
|
||||||
return streamType != StreamType.VIDEO_STREAM && streamType != StreamType.LIVE_STREAM;
|
return !StreamTypeUtil.isVideo(streamType);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Other cases: the play queue manager reload is needed
|
// Other cases: the play queue manager reload is needed
|
||||||
@@ -4428,7 +4422,7 @@ public final class Player implements
|
|||||||
return audioReactor;
|
return audioReactor;
|
||||||
}
|
}
|
||||||
|
|
||||||
public GestureDetectorCompat getGestureDetector() {
|
public GestureDetector getGestureDetector() {
|
||||||
return gestureDetector;
|
return gestureDetector;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -0,0 +1,136 @@
|
|||||||
|
package org.schabi.newpipe.player.datasource;
|
||||||
|
|
||||||
|
import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
import com.google.android.exoplayer2.C;
|
||||||
|
import com.google.android.exoplayer2.source.hls.HlsDataSourceFactory;
|
||||||
|
import com.google.android.exoplayer2.upstream.ByteArrayDataSource;
|
||||||
|
import com.google.android.exoplayer2.upstream.DataSource;
|
||||||
|
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A {@link HlsDataSourceFactory} which allows playback of non-URI media HLS playlists for
|
||||||
|
* {@link com.google.android.exoplayer2.source.hls.HlsMediaSource HlsMediaSource}s.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* If media requests are relative, the URI from which the manifest comes from (either the
|
||||||
|
* manifest URI (preferred) or the master URI (if applicable)) must be returned, otherwise the
|
||||||
|
* content will be not playable, as it will be an invalid URL, or it may be treat as something
|
||||||
|
* unexpected, for instance as a file for
|
||||||
|
* {@link com.google.android.exoplayer2.upstream.DefaultDataSource DefaultDataSource}s.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* See {@link #createDataSource(int)} for changes and implementation details.
|
||||||
|
* </p>
|
||||||
|
*/
|
||||||
|
public final class NonUriHlsDataSourceFactory implements HlsDataSourceFactory {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builder class of {@link NonUriHlsDataSourceFactory} instances.
|
||||||
|
*/
|
||||||
|
public static final class Builder {
|
||||||
|
private DataSource.Factory dataSourceFactory;
|
||||||
|
private String playlistString;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the {@link DataSource.Factory} which will be used to create non manifest contents
|
||||||
|
* {@link DataSource}s.
|
||||||
|
*
|
||||||
|
* @param dataSourceFactoryForNonManifestContents the {@link DataSource.Factory} which will
|
||||||
|
* be used to create non manifest contents
|
||||||
|
* {@link DataSource}s, which cannot be null
|
||||||
|
*/
|
||||||
|
public void setDataSourceFactory(
|
||||||
|
@NonNull final DataSource.Factory dataSourceFactoryForNonManifestContents) {
|
||||||
|
this.dataSourceFactory = dataSourceFactoryForNonManifestContents;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the HLS playlist which will be used for manifests requests.
|
||||||
|
*
|
||||||
|
* @param hlsPlaylistString the string which correspond to the response of the HLS
|
||||||
|
* manifest, which cannot be null or empty
|
||||||
|
*/
|
||||||
|
public void setPlaylistString(@NonNull final String hlsPlaylistString) {
|
||||||
|
this.playlistString = hlsPlaylistString;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new {@link NonUriHlsDataSourceFactory} with the given data source factory and
|
||||||
|
* the given HLS playlist.
|
||||||
|
*
|
||||||
|
* @return a {@link NonUriHlsDataSourceFactory}
|
||||||
|
* @throws IllegalArgumentException if the data source factory is null or if the HLS
|
||||||
|
* playlist string set is null or empty
|
||||||
|
*/
|
||||||
|
@NonNull
|
||||||
|
public NonUriHlsDataSourceFactory build() {
|
||||||
|
if (dataSourceFactory == null) {
|
||||||
|
throw new IllegalArgumentException(
|
||||||
|
"No DataSource.Factory valid instance has been specified.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isNullOrEmpty(playlistString)) {
|
||||||
|
throw new IllegalArgumentException("No HLS valid playlist has been specified.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return new NonUriHlsDataSourceFactory(dataSourceFactory,
|
||||||
|
playlistString.getBytes(StandardCharsets.UTF_8));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private final DataSource.Factory dataSourceFactory;
|
||||||
|
private final byte[] playlistStringByteArray;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a {@link NonUriHlsDataSourceFactory} instance.
|
||||||
|
*
|
||||||
|
* @param dataSourceFactory the {@link DataSource.Factory} which will be used to build
|
||||||
|
* non manifests {@link DataSource}s, which must not be null
|
||||||
|
* @param playlistStringByteArray a byte array of the HLS playlist, which must not be null
|
||||||
|
*/
|
||||||
|
private NonUriHlsDataSourceFactory(@NonNull final DataSource.Factory dataSourceFactory,
|
||||||
|
@NonNull final byte[] playlistStringByteArray) {
|
||||||
|
this.dataSourceFactory = dataSourceFactory;
|
||||||
|
this.playlistStringByteArray = playlistStringByteArray;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a {@link DataSource} for the given data type.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* Contrary to {@link com.google.android.exoplayer2.source.hls.DefaultHlsDataSourceFactory
|
||||||
|
* ExoPlayer's default implementation}, this implementation is not always using the
|
||||||
|
* {@link DataSource.Factory} passed to the
|
||||||
|
* {@link com.google.android.exoplayer2.source.hls.HlsMediaSource.Factory
|
||||||
|
* HlsMediaSource.Factory} constructor, only when it's not
|
||||||
|
* {@link C#DATA_TYPE_MANIFEST the manifest type}.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* This change allow playback of non-URI HLS contents, when the manifest is not a master
|
||||||
|
* manifest/playlist (otherwise, endless loops should be encountered because the
|
||||||
|
* {@link DataSource}s created for media playlists should use the master playlist response
|
||||||
|
* instead).
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @param dataType the data type for which the {@link DataSource} will be used, which is one of
|
||||||
|
* {@link C} {@code .DATA_TYPE_*} constants
|
||||||
|
* @return a {@link DataSource} for the given data type
|
||||||
|
*/
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public DataSource createDataSource(final int dataType) {
|
||||||
|
// The manifest is already downloaded and provided with playlistStringByteArray, so we
|
||||||
|
// don't need to download it again and we can use a ByteArrayDataSource instead
|
||||||
|
if (dataType == C.DATA_TYPE_MANIFEST) {
|
||||||
|
return new ByteArrayDataSource(playlistStringByteArray);
|
||||||
|
}
|
||||||
|
|
||||||
|
return dataSourceFactory.createDataSource();
|
||||||
|
}
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user