mirror of
https://github.com/TeamNewPipe/NewPipe
synced 2026-02-05 17:50:18 +00:00
Compare commits
199 Commits
dev
...
player-cla
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
31ade7cd30 | ||
|
|
4e1b0e0555 | ||
|
|
0aa71a58ed | ||
|
|
7585cc2e73 | ||
|
|
803fd52859 | ||
|
|
ab8a9ae11c | ||
|
|
83486402df | ||
|
|
a5813f256a | ||
|
|
0a885492b6 | ||
|
|
731efc2124 | ||
|
|
a8da9946d1 | ||
|
|
3d069cdf5b | ||
|
|
eccedc0ab0 | ||
|
|
7cecda5713 | ||
|
|
1d94fd1582 | ||
|
|
c9542ad6fd | ||
|
|
7615f79aca | ||
|
|
47299c9184 | ||
|
|
6486f2de56 | ||
|
|
c1bdffd917 | ||
|
|
99aae7eb28 | ||
|
|
fd99c5e461 | ||
|
|
407d2d768d | ||
|
|
b109e4d3cc | ||
|
|
137ade24ff | ||
|
|
b78e0b2da8 | ||
|
|
3e6e980362 | ||
|
|
1890fbb19a | ||
|
|
efb3aa530d | ||
|
|
ce919215fb | ||
|
|
6a4aaba431 | ||
|
|
83d93e16e7 | ||
|
|
8d15a141b1 | ||
|
|
a78bed700a | ||
|
|
ef3c76645f | ||
|
|
d4ed18bf08 | ||
|
|
fbafdeb2ca | ||
|
|
781040efaa | ||
|
|
1547b50b4e | ||
|
|
3f7ef49979 | ||
|
|
dab0148a78 | ||
|
|
c0c08a4f63 | ||
|
|
aaf337421d | ||
|
|
79a0edacd7 | ||
|
|
d56eef6ece | ||
|
|
72f054a4fa | ||
|
|
172c3c92ac | ||
|
|
137ef3fee4 | ||
|
|
e49156fb11 | ||
|
|
de5d45849f | ||
|
|
a25034b898 | ||
|
|
ae9e82b2c1 | ||
|
|
a70b38a8e5 | ||
|
|
08f3dba42c | ||
|
|
0cff3a6ecd | ||
|
|
9b78e49e45 | ||
|
|
e6eea8f851 | ||
|
|
4e55f1bee6 | ||
|
|
cff3834fde | ||
|
|
c8b01a06b0 | ||
|
|
414b1a8344 | ||
|
|
404d9f3fac | ||
|
|
55e4014036 | ||
|
|
1cd5563b27 | ||
|
|
1abced992b | ||
|
|
46b9243661 | ||
|
|
ad72b2cb31 | ||
|
|
8b79d0ee29 | ||
|
|
295f719b77 | ||
|
|
b584353f4d | ||
|
|
d73314b4dd | ||
|
|
9f4a33c7a8 | ||
|
|
3a9540b042 | ||
|
|
ca855cbca0 | ||
|
|
6a98b1dac7 | ||
|
|
7d4a2836fc | ||
|
|
6ea715a18d | ||
|
|
a56debfce6 | ||
|
|
226b6de34f | ||
|
|
13585ca0be | ||
|
|
62ab9bd740 | ||
|
|
fdf36cbad6 | ||
|
|
aea2b7c7f3 | ||
|
|
37d1c784fa | ||
|
|
cea149f852 | ||
|
|
a92a28517e | ||
|
|
800961c3d7 | ||
|
|
9d8a79b0bd | ||
|
|
ef56dea817 | ||
|
|
23b3835af0 | ||
|
|
412e1d602a | ||
|
|
802a094154 | ||
|
|
e6b1341246 | ||
|
|
36ede243e3 | ||
|
|
bac9f7eebf | ||
|
|
8ada566bf1 | ||
|
|
5bd4ed77df | ||
|
|
97652ac015 | ||
|
|
6dd24033a4 | ||
|
|
4de3ef20be | ||
|
|
702f74291d | ||
|
|
d8759993a9 | ||
|
|
7787eafd3a | ||
|
|
4f4136c6e9 | ||
|
|
b399030e19 | ||
|
|
0991461d04 | ||
|
|
49bcf2c41b | ||
|
|
c00c6c460c | ||
|
|
4c4fe3f511 | ||
|
|
db485c3d77 | ||
|
|
c0388d948b | ||
|
|
43bbddcc26 | ||
|
|
6471b64ab6 | ||
|
|
b9fcf0dff8 | ||
|
|
3177ca6e8a | ||
|
|
5017f4f05a | ||
|
|
823d4a041f | ||
|
|
62d4044d6c | ||
|
|
3785404618 | ||
|
|
c98ad62163 | ||
|
|
4cac111b66 | ||
|
|
941b8eb194 | ||
|
|
b1add13bfd | ||
|
|
5fffee2c7d | ||
|
|
f9340ae604 | ||
|
|
d3a6991fd4 | ||
|
|
b0bfd4a807 | ||
|
|
3641698379 | ||
|
|
2836191fb3 | ||
|
|
0df6c7fc2c | ||
|
|
b1ebd3ecd9 | ||
|
|
4758244cf5 | ||
|
|
294b9cf347 | ||
|
|
6d05af484e | ||
|
|
e082bca5e0 | ||
|
|
f9dae9078e | ||
|
|
e955beeef1 | ||
|
|
eaac7f3f85 | ||
|
|
ea414f57d4 | ||
|
|
f984b26626 | ||
|
|
edab9a6a1f | ||
|
|
4740e3be86 | ||
|
|
e639b02fed | ||
|
|
ac1ca1412d | ||
|
|
d131d3399a | ||
|
|
1009dc4d4e | ||
|
|
42cb914616 | ||
|
|
e72da94eb1 | ||
|
|
c5d94a5b60 | ||
|
|
02c5f2607a | ||
|
|
369a46f8fe | ||
|
|
909d214002 | ||
|
|
5e7e14ee4d | ||
|
|
b092fe2c76 | ||
|
|
b9dd7078ad | ||
|
|
93310955f2 | ||
|
|
9c52e039ee | ||
|
|
be037e0756 | ||
|
|
5bfb0449cf | ||
|
|
0ec81c9e52 | ||
|
|
5841eaa6d7 | ||
|
|
e92ba8f5d1 | ||
|
|
1908e18dc4 | ||
|
|
e30d5e4305 | ||
|
|
11bb2495ba | ||
|
|
341cc37ce7 | ||
|
|
1620668966 | ||
|
|
56c80ce6dd | ||
|
|
8ce9a7e43c | ||
|
|
e05d97732e | ||
|
|
644a345b55 | ||
|
|
bda961a04c | ||
|
|
ba2efded76 | ||
|
|
b05b98ca61 | ||
|
|
7a7f81ac7f | ||
|
|
6e6c171dd7 | ||
|
|
8a41c8cf66 | ||
|
|
05271d95a9 | ||
|
|
9d04a73c85 | ||
|
|
d336f4cef2 | ||
|
|
4ec7532126 | ||
|
|
da83646303 | ||
|
|
5062d38b65 | ||
|
|
82b492c050 | ||
|
|
73e3a69aaf | ||
|
|
348a79f91d | ||
|
|
c4ada7ff6e | ||
|
|
39d0691c7e | ||
|
|
71361de8ee | ||
|
|
8aa2590fd3 | ||
|
|
e3b7bf467e | ||
|
|
f74402bc94 | ||
|
|
4d3b4a7b20 | ||
|
|
e6302cc868 | ||
|
|
844b4edf48 | ||
|
|
92a7f22d3c | ||
|
|
03167a1e9c | ||
|
|
d479f29e9b | ||
|
|
1af798b04b |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -10,6 +10,7 @@ captures/
|
|||||||
*.class
|
*.class
|
||||||
app/debug/
|
app/debug/
|
||||||
app/release/
|
app/release/
|
||||||
|
.kotlin/
|
||||||
|
|
||||||
# vscode / eclipse files
|
# vscode / eclipse files
|
||||||
*.classpath
|
*.classpath
|
||||||
|
|||||||
222
app/build.gradle
222
app/build.gradle
@@ -1,14 +1,18 @@
|
|||||||
import com.android.tools.profgen.ArtProfileKt
|
import com.android.tools.profgen.ArtProfileKt
|
||||||
import com.android.tools.profgen.ArtProfileSerializer
|
import com.android.tools.profgen.ArtProfileSerializer
|
||||||
import com.android.tools.profgen.DexFile
|
import com.android.tools.profgen.DexFile
|
||||||
|
import com.mikepenz.aboutlibraries.plugin.DuplicateMode
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
id "com.android.application"
|
alias libs.plugins.android.application
|
||||||
id "kotlin-android"
|
alias libs.plugins.kotlin.android
|
||||||
id "kotlin-kapt"
|
alias libs.plugins.kotlin.compose
|
||||||
id "kotlin-parcelize"
|
alias libs.plugins.kotlin.kapt
|
||||||
id "checkstyle"
|
alias libs.plugins.kotlin.parcelize
|
||||||
id "org.sonarqube" version "4.0.0.2929"
|
alias libs.plugins.checkstyle
|
||||||
|
alias libs.plugins.sonarqube
|
||||||
|
alias libs.plugins.hilt
|
||||||
|
alias libs.plugins.aboutlibraries
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
@@ -103,6 +107,7 @@ android {
|
|||||||
|
|
||||||
buildFeatures {
|
buildFeatures {
|
||||||
viewBinding true
|
viewBinding true
|
||||||
|
compose true
|
||||||
buildConfig true
|
buildConfig true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -117,23 +122,6 @@ android {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ext {
|
|
||||||
checkstyleVersion = '10.12.1'
|
|
||||||
|
|
||||||
androidxLifecycleVersion = '2.6.2'
|
|
||||||
androidxRoomVersion = '2.6.1'
|
|
||||||
androidxWorkVersion = '2.8.1'
|
|
||||||
|
|
||||||
stateSaverVersion = '1.4.1'
|
|
||||||
exoPlayerVersion = '2.18.7'
|
|
||||||
googleAutoServiceVersion = '1.1.1'
|
|
||||||
groupieVersion = '2.10.1'
|
|
||||||
markwonVersion = '4.6.2'
|
|
||||||
|
|
||||||
leakCanaryVersion = '2.12'
|
|
||||||
stethoVersion = '1.6.0'
|
|
||||||
}
|
|
||||||
|
|
||||||
configurations {
|
configurations {
|
||||||
checkstyle
|
checkstyle
|
||||||
ktlint
|
ktlint
|
||||||
@@ -143,7 +131,7 @@ checkstyle {
|
|||||||
getConfigDirectory().set(rootProject.file("checkstyle"))
|
getConfigDirectory().set(rootProject.file("checkstyle"))
|
||||||
ignoreFailures false
|
ignoreFailures false
|
||||||
showViolations true
|
showViolations true
|
||||||
toolVersion = checkstyleVersion
|
toolVersion = libs.versions.checkstyle.get()
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.register('runCheckstyle', Checkstyle) {
|
tasks.register('runCheckstyle', Checkstyle) {
|
||||||
@@ -185,11 +173,13 @@ tasks.register('formatKtlint', JavaExec) {
|
|||||||
jvmArgs("--add-opens", "java.base/java.lang=ALL-UNNAMED")
|
jvmArgs("--add-opens", "java.base/java.lang=ALL-UNNAMED")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
apply from: 'check-dependencies.gradle'
|
||||||
|
|
||||||
afterEvaluate {
|
afterEvaluate {
|
||||||
if (!System.properties.containsKey('skipFormatKtlint')) {
|
if (!System.properties.containsKey('skipFormatKtlint')) {
|
||||||
preDebugBuild.dependsOn formatKtlint
|
preDebugBuild.dependsOn formatKtlint
|
||||||
}
|
}
|
||||||
preDebugBuild.dependsOn runCheckstyle, runKtlint
|
preDebugBuild.dependsOn runCheckstyle, runKtlint, checkDependenciesOrder
|
||||||
}
|
}
|
||||||
|
|
||||||
sonar {
|
sonar {
|
||||||
@@ -200,125 +190,155 @@ sonar {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
kapt {
|
||||||
|
correctErrorTypes true
|
||||||
|
}
|
||||||
|
|
||||||
|
aboutLibraries {
|
||||||
|
// note: offline mode prevents the plugin from fetching licenses at build time, which would be
|
||||||
|
// harmful for reproducible builds
|
||||||
|
offlineMode = true
|
||||||
|
duplicationMode = DuplicateMode.MERGE
|
||||||
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
/** Desugaring **/
|
/** Desugaring **/
|
||||||
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs_nio:2.0.4'
|
coreLibraryDesugaring libs.desugar.jdk.libs.nio
|
||||||
|
|
||||||
/** NewPipe libraries **/
|
/** NewPipe libraries **/
|
||||||
// You can use a local version by uncommenting a few lines in settings.gradle
|
implementation libs.teamnewpipe.nanojson
|
||||||
// Or you can use a commit you pushed to GitHub by just replacing TeamNewPipe with your GitHub
|
implementation libs.teamnewpipe.newpipe.extractor
|
||||||
// name and the commit hash with the commit hash of the (pushed) commit you want to test
|
implementation libs.teamnewpipe.nononsense.filepicker
|
||||||
// This works thanks to JitPack: https://jitpack.io/
|
|
||||||
implementation 'com.github.TeamNewPipe:nanojson:1d9e1aea9049fc9f85e68b43ba39fe7be1c1f751'
|
|
||||||
// WORKAROUND: if you get errors with the NewPipeExtractor dependency, replace `v0.24.3` with
|
|
||||||
// the corresponding commit hash, since JitPack sometimes deletes artifacts.
|
|
||||||
// If there’s already a git hash, just add more of it to the end (or remove a letter)
|
|
||||||
// to cause jitpack to regenerate the artifact.
|
|
||||||
implementation 'com.github.TeamNewPipe.NewPipeExtractor:NewPipeExtractor:v0.24.6'
|
|
||||||
implementation 'com.github.TeamNewPipe:NoNonsense-FilePicker:5.0.0'
|
|
||||||
|
|
||||||
/** Checkstyle **/
|
/** Checkstyle **/
|
||||||
checkstyle "com.puppycrawl.tools:checkstyle:${checkstyleVersion}"
|
checkstyle libs.tools.checkstyle
|
||||||
ktlint 'com.pinterest:ktlint:0.45.2'
|
ktlint libs.tools.ktlint
|
||||||
|
|
||||||
/** Kotlin **/
|
/** Kotlin **/
|
||||||
implementation "org.jetbrains.kotlin:kotlin-stdlib:${kotlin_version}"
|
implementation libs.kotlin.stdlib
|
||||||
|
|
||||||
/** AndroidX **/
|
/** AndroidX **/
|
||||||
implementation 'androidx.appcompat:appcompat:1.6.1'
|
implementation libs.androidx.appcompat
|
||||||
implementation 'androidx.cardview:cardview:1.0.0'
|
implementation libs.androidx.cardview
|
||||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
|
implementation libs.androidx.constraintlayout
|
||||||
implementation 'androidx.core:core-ktx:1.12.0'
|
implementation libs.androidx.core.ktx
|
||||||
implementation 'androidx.documentfile:documentfile:1.0.1'
|
implementation libs.androidx.documentfile
|
||||||
implementation 'androidx.fragment:fragment-ktx:1.6.2'
|
implementation libs.androidx.fragment.compose
|
||||||
implementation "androidx.lifecycle:lifecycle-livedata-ktx:${androidxLifecycleVersion}"
|
implementation libs.androidx.lifecycle.livedata
|
||||||
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:${androidxLifecycleVersion}"
|
implementation libs.androidx.lifecycle.viewmodel
|
||||||
implementation 'androidx.localbroadcastmanager:localbroadcastmanager:1.1.0'
|
implementation libs.androidx.localbroadcastmanager
|
||||||
implementation 'androidx.media:media:1.7.0'
|
implementation libs.androidx.media
|
||||||
implementation 'androidx.preference:preference:1.2.1'
|
implementation libs.androidx.preference
|
||||||
implementation 'androidx.recyclerview:recyclerview:1.3.2'
|
implementation libs.androidx.recyclerview
|
||||||
implementation "androidx.room:room-runtime:${androidxRoomVersion}"
|
implementation libs.androidx.room.runtime
|
||||||
implementation "androidx.room:room-rxjava3:${androidxRoomVersion}"
|
implementation libs.androidx.room.rxjava3
|
||||||
kapt "androidx.room:room-compiler:${androidxRoomVersion}"
|
kapt libs.androidx.room.compiler
|
||||||
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
|
implementation libs.androidx.swiperefreshlayout
|
||||||
// Newer version specified to prevent accessibility regressions with RecyclerView, see:
|
implementation libs.androidx.work.runtime
|
||||||
// https://developer.android.com/jetpack/androidx/releases/viewpager2#1.1.0-alpha01
|
implementation libs.androidx.work.rxjava3
|
||||||
implementation 'androidx.viewpager2:viewpager2:1.1.0-beta02'
|
implementation libs.androidx.material
|
||||||
implementation "androidx.work:work-runtime-ktx:${androidxWorkVersion}"
|
implementation libs.androidx.webkit
|
||||||
implementation "androidx.work:work-rxjava3:${androidxWorkVersion}"
|
|
||||||
implementation 'com.google.android.material:material:1.11.0'
|
|
||||||
implementation "androidx.webkit:webkit:1.9.0"
|
|
||||||
|
|
||||||
/** Third-party libraries **/
|
/** Third-party libraries **/
|
||||||
// Instance state boilerplate elimination
|
// Instance state boilerplate elimination
|
||||||
implementation 'com.github.livefront:bridge:v2.0.2'
|
implementation libs.livefront.bridge
|
||||||
implementation "com.evernote:android-state:$stateSaverVersion"
|
implementation libs.android.state
|
||||||
kapt "com.evernote:android-state-processor:$stateSaverVersion"
|
kapt libs.android.state.processor
|
||||||
|
|
||||||
// HTML parser
|
// HTML parser
|
||||||
implementation "org.jsoup:jsoup:1.17.2"
|
implementation libs.jsoup
|
||||||
|
|
||||||
// HTTP client
|
// HTTP client
|
||||||
implementation "com.squareup.okhttp3:okhttp:4.12.0"
|
implementation libs.okhttp
|
||||||
|
|
||||||
// Media player
|
// Media player
|
||||||
implementation "com.google.android.exoplayer:exoplayer-core:${exoPlayerVersion}"
|
implementation libs.exoplayer.core
|
||||||
implementation "com.google.android.exoplayer:exoplayer-dash:${exoPlayerVersion}"
|
implementation libs.exoplayer.dash
|
||||||
implementation "com.google.android.exoplayer:exoplayer-database:${exoPlayerVersion}"
|
implementation libs.exoplayer.database
|
||||||
implementation "com.google.android.exoplayer:exoplayer-datasource:${exoPlayerVersion}"
|
implementation libs.exoplayer.datasource
|
||||||
implementation "com.google.android.exoplayer:exoplayer-hls:${exoPlayerVersion}"
|
implementation libs.exoplayer.hls
|
||||||
implementation "com.google.android.exoplayer:exoplayer-smoothstreaming:${exoPlayerVersion}"
|
implementation libs.exoplayer.smoothstreaming
|
||||||
implementation "com.google.android.exoplayer:exoplayer-ui:${exoPlayerVersion}"
|
implementation libs.exoplayer.ui
|
||||||
implementation "com.google.android.exoplayer:extension-mediasession:${exoPlayerVersion}"
|
implementation libs.extension.mediasession
|
||||||
|
|
||||||
// Metadata generator for service descriptors
|
// Metadata generator for service descriptors
|
||||||
compileOnly "com.google.auto.service:auto-service-annotations:${googleAutoServiceVersion}"
|
compileOnly libs.auto.service
|
||||||
kapt "com.google.auto.service:auto-service:${googleAutoServiceVersion}"
|
kapt libs.auto.service.kapt
|
||||||
|
|
||||||
// Manager for complex RecyclerView layouts
|
// Manager for complex RecyclerView layouts
|
||||||
implementation "com.github.lisawray.groupie:groupie:${groupieVersion}"
|
implementation libs.lisawray.groupie
|
||||||
implementation "com.github.lisawray.groupie:groupie-viewbinding:${groupieVersion}"
|
implementation libs.lisawray.groupie.viewbinding
|
||||||
|
|
||||||
// Image loading
|
// Image loading
|
||||||
//noinspection GradleDependency --> 2.8 is the last version, not 2.71828!
|
implementation libs.coil.compose
|
||||||
implementation "com.squareup.picasso:picasso:2.8"
|
implementation libs.coil.network.okhttp
|
||||||
|
|
||||||
// Markdown library for Android
|
// Markdown library for Android
|
||||||
implementation "io.noties.markwon:core:${markwonVersion}"
|
implementation libs.markwon.core
|
||||||
implementation "io.noties.markwon:linkify:${markwonVersion}"
|
implementation libs.markwon.linkify
|
||||||
|
|
||||||
// Crash reporting
|
// Crash reporting
|
||||||
implementation "ch.acra:acra-core:5.11.3"
|
implementation libs.acra.core
|
||||||
|
|
||||||
// Properly restarting
|
// Properly restarting
|
||||||
implementation 'com.jakewharton:process-phoenix:2.1.2'
|
implementation libs.process.phoenix
|
||||||
|
|
||||||
// Reactive extensions for Java VM
|
// Reactive extensions for Java VM
|
||||||
implementation "io.reactivex.rxjava3:rxjava:3.1.8"
|
implementation libs.rxjava3.rxjava
|
||||||
implementation "io.reactivex.rxjava3:rxandroid:3.0.2"
|
implementation libs.rxjava3.rxandroid
|
||||||
// RxJava binding APIs for Android UI widgets
|
// RxJava binding APIs for Android UI widgets
|
||||||
implementation "com.jakewharton.rxbinding4:rxbinding:4.0.0"
|
implementation libs.rxbinding4.rxbinding
|
||||||
|
|
||||||
// Date and time formatting
|
// Date and time formatting
|
||||||
implementation "org.ocpsoft.prettytime:prettytime:5.0.8.Final"
|
implementation libs.prettytime
|
||||||
|
|
||||||
|
// Jetpack Compose
|
||||||
|
implementation(platform(libs.androidx.compose.bom))
|
||||||
|
implementation libs.androidx.compose.material3
|
||||||
|
implementation libs.androidx.compose.adaptive
|
||||||
|
implementation libs.androidx.activity.compose
|
||||||
|
implementation libs.androidx.compose.ui.tooling.preview
|
||||||
|
implementation libs.androidx.lifecycle.viewmodel.compose
|
||||||
|
implementation libs.androidx.compose.ui.text // Needed for parsing HTML to AnnotatedString
|
||||||
|
implementation libs.androidx.compose.material.icons.extended
|
||||||
|
|
||||||
|
// Jetpack Compose related dependencies
|
||||||
|
implementation libs.androidx.paging.compose
|
||||||
|
implementation libs.androidx.navigation.compose
|
||||||
|
|
||||||
|
// Coroutines interop
|
||||||
|
implementation libs.kotlinx.coroutines.rx3
|
||||||
|
|
||||||
|
// Library loading for About screen
|
||||||
|
implementation libs.aboutlibraries.compose.m3
|
||||||
|
|
||||||
|
// Hilt
|
||||||
|
implementation libs.hilt.android
|
||||||
|
kapt(libs.hilt.compiler)
|
||||||
|
|
||||||
|
// Scroll
|
||||||
|
implementation libs.lazycolumnscrollbar
|
||||||
|
|
||||||
/** Debugging **/
|
/** Debugging **/
|
||||||
// Memory leak detection
|
// Memory leak detection
|
||||||
debugImplementation "com.squareup.leakcanary:leakcanary-object-watcher-android:${leakCanaryVersion}"
|
debugImplementation libs.leakcanary.object.watcher
|
||||||
debugImplementation "com.squareup.leakcanary:plumber-android:${leakCanaryVersion}"
|
debugImplementation libs.leakcanary.plumber.android
|
||||||
debugImplementation "com.squareup.leakcanary:leakcanary-android-core:${leakCanaryVersion}"
|
debugImplementation libs.leakcanary.android.core
|
||||||
// Debug bridge for Android
|
// Debug bridge for Android
|
||||||
debugImplementation "com.facebook.stetho:stetho:${stethoVersion}"
|
debugImplementation libs.stetho
|
||||||
debugImplementation "com.facebook.stetho:stetho-okhttp3:${stethoVersion}"
|
debugImplementation libs.stetho.okhttp3
|
||||||
|
|
||||||
|
// Jetpack Compose
|
||||||
|
debugImplementation libs.androidx.compose.ui.tooling
|
||||||
|
|
||||||
/** Testing **/
|
/** Testing **/
|
||||||
testImplementation 'junit:junit:4.13.2'
|
testImplementation libs.junit
|
||||||
testImplementation 'org.mockito:mockito-core:5.6.0'
|
testImplementation libs.mockito.core
|
||||||
|
|
||||||
androidTestImplementation "androidx.test.ext:junit:1.1.5"
|
androidTestImplementation libs.androidx.junit
|
||||||
androidTestImplementation "androidx.test:runner:1.5.2"
|
androidTestImplementation libs.androidx.runner
|
||||||
androidTestImplementation "androidx.room:room-testing:${androidxRoomVersion}"
|
androidTestImplementation libs.androidx.room.testing
|
||||||
androidTestImplementation "org.assertj:assertj-core:3.24.2"
|
androidTestImplementation libs.assertj.core
|
||||||
}
|
}
|
||||||
|
|
||||||
static String getGitWorkingBranch() {
|
static String getGitWorkingBranch() {
|
||||||
|
|||||||
48
app/check-dependencies.gradle
Normal file
48
app/check-dependencies.gradle
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
tasks.register('checkDependenciesOrder') {
|
||||||
|
group = 'verification'
|
||||||
|
description = 'Checks that each section in libs.versions.toml is sorted alphabetically'
|
||||||
|
|
||||||
|
def tomlFile = file('../gradle/libs.versions.toml')
|
||||||
|
|
||||||
|
doLast {
|
||||||
|
if (!tomlFile.exists()) {
|
||||||
|
throw new GradleException('TOML file not found')
|
||||||
|
}
|
||||||
|
|
||||||
|
def lines = tomlFile.readLines()
|
||||||
|
def nonSortedBlocks = []
|
||||||
|
def currentBlock = []
|
||||||
|
def prevLine = ''
|
||||||
|
def prevIndex = 0
|
||||||
|
|
||||||
|
lines.eachWithIndex { line, lineIndex ->
|
||||||
|
if (line.trim() && !line.startsWith('#')) {
|
||||||
|
if (line.startsWith('[')) {
|
||||||
|
prevLine = ''
|
||||||
|
} else {
|
||||||
|
def currIndex = lineIndex + 1
|
||||||
|
if (prevLine > line) {
|
||||||
|
if (currentBlock && currentBlock[-1] == "${prevIndex}: ${prevLine}") {
|
||||||
|
currentBlock.add("${currIndex}: ${line}")
|
||||||
|
} else {
|
||||||
|
if (!currentBlock.isEmpty()) {
|
||||||
|
nonSortedBlocks.add(currentBlock)
|
||||||
|
currentBlock = []
|
||||||
|
}
|
||||||
|
currentBlock.add("${prevIndex}: ${prevLine}")
|
||||||
|
currentBlock.add("${currIndex}: ${line}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
prevLine = line
|
||||||
|
prevIndex = lineIndex + 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!currentBlock.isEmpty()) {
|
||||||
|
nonSortedBlocks.add(currentBlock)
|
||||||
|
throw new GradleException("The following lines were not sorted:\n" +
|
||||||
|
nonSortedBlocks.collect { it.join("\n") }.join("\n\n"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -80,6 +80,11 @@
|
|||||||
android:exported="false"
|
android:exported="false"
|
||||||
android:label="@string/settings" />
|
android:label="@string/settings" />
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name=".settings.SettingsV2Activity"
|
||||||
|
android:exported="true"
|
||||||
|
android:label="@string/settings" />
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".about.AboutActivity"
|
android:name=".about.AboutActivity"
|
||||||
android:exported="false"
|
android:exported="false"
|
||||||
|
|||||||
@@ -1,275 +0,0 @@
|
|||||||
package org.schabi.newpipe;
|
|
||||||
|
|
||||||
import android.app.Application;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.SharedPreferences;
|
|
||||||
import android.util.Log;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.core.app.NotificationChannelCompat;
|
|
||||||
import androidx.core.app.NotificationManagerCompat;
|
|
||||||
import androidx.preference.PreferenceManager;
|
|
||||||
|
|
||||||
import com.jakewharton.processphoenix.ProcessPhoenix;
|
|
||||||
|
|
||||||
import org.acra.ACRA;
|
|
||||||
import org.acra.config.CoreConfigurationBuilder;
|
|
||||||
import org.schabi.newpipe.error.ReCaptchaActivity;
|
|
||||||
import org.schabi.newpipe.extractor.NewPipe;
|
|
||||||
import org.schabi.newpipe.extractor.downloader.Downloader;
|
|
||||||
import org.schabi.newpipe.extractor.services.youtube.extractors.YoutubeStreamExtractor;
|
|
||||||
import org.schabi.newpipe.ktx.ExceptionUtils;
|
|
||||||
import org.schabi.newpipe.settings.NewPipeSettings;
|
|
||||||
import org.schabi.newpipe.util.BridgeStateSaverInitializer;
|
|
||||||
import org.schabi.newpipe.util.Localization;
|
|
||||||
import org.schabi.newpipe.util.ServiceHelper;
|
|
||||||
import org.schabi.newpipe.util.StateSaver;
|
|
||||||
import org.schabi.newpipe.util.image.ImageStrategy;
|
|
||||||
import org.schabi.newpipe.util.image.PicassoHelper;
|
|
||||||
import org.schabi.newpipe.util.image.PreferredImageQuality;
|
|
||||||
import org.schabi.newpipe.util.potoken.PoTokenProviderImpl;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InterruptedIOException;
|
|
||||||
import java.net.SocketException;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Objects;
|
|
||||||
|
|
||||||
import io.reactivex.rxjava3.exceptions.CompositeException;
|
|
||||||
import io.reactivex.rxjava3.exceptions.MissingBackpressureException;
|
|
||||||
import io.reactivex.rxjava3.exceptions.OnErrorNotImplementedException;
|
|
||||||
import io.reactivex.rxjava3.exceptions.UndeliverableException;
|
|
||||||
import io.reactivex.rxjava3.functions.Consumer;
|
|
||||||
import io.reactivex.rxjava3.plugins.RxJavaPlugins;
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Copyright (C) Hans-Christoph Steiner 2016 <hans@eds.org>
|
|
||||||
* App.java is part of NewPipe.
|
|
||||||
*
|
|
||||||
* NewPipe is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* NewPipe is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
public class App extends Application {
|
|
||||||
public static final String PACKAGE_NAME = BuildConfig.APPLICATION_ID;
|
|
||||||
private static final String TAG = App.class.toString();
|
|
||||||
|
|
||||||
private boolean isFirstRun = false;
|
|
||||||
private static App app;
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
public static App getApp() {
|
|
||||||
return app;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void attachBaseContext(final Context base) {
|
|
||||||
super.attachBaseContext(base);
|
|
||||||
initACRA();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onCreate() {
|
|
||||||
super.onCreate();
|
|
||||||
|
|
||||||
app = this;
|
|
||||||
|
|
||||||
if (ProcessPhoenix.isPhoenixProcess(this)) {
|
|
||||||
Log.i(TAG, "This is a phoenix process! "
|
|
||||||
+ "Aborting initialization of App[onCreate]");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// check if the last used preference version is set
|
|
||||||
// to determine whether this is the first app run
|
|
||||||
final int lastUsedPrefVersion = PreferenceManager.getDefaultSharedPreferences(this)
|
|
||||||
.getInt(getString(R.string.last_used_preferences_version), -1);
|
|
||||||
isFirstRun = lastUsedPrefVersion == -1;
|
|
||||||
|
|
||||||
// Initialize settings first because other initializations can use its values
|
|
||||||
NewPipeSettings.initSettings(this);
|
|
||||||
|
|
||||||
NewPipe.init(getDownloader(),
|
|
||||||
Localization.getPreferredLocalization(this),
|
|
||||||
Localization.getPreferredContentCountry(this));
|
|
||||||
Localization.initPrettyTime(Localization.resolvePrettyTime(getApplicationContext()));
|
|
||||||
|
|
||||||
BridgeStateSaverInitializer.init(this);
|
|
||||||
StateSaver.init(this);
|
|
||||||
initNotificationChannels();
|
|
||||||
|
|
||||||
ServiceHelper.initServices(this);
|
|
||||||
|
|
||||||
// Initialize image loader
|
|
||||||
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
|
|
||||||
PicassoHelper.init(this);
|
|
||||||
ImageStrategy.setPreferredImageQuality(PreferredImageQuality.fromPreferenceKey(this,
|
|
||||||
prefs.getString(getString(R.string.image_quality_key),
|
|
||||||
getString(R.string.image_quality_default))));
|
|
||||||
PicassoHelper.setIndicatorsEnabled(MainActivity.DEBUG
|
|
||||||
&& prefs.getBoolean(getString(R.string.show_image_indicators_key), false));
|
|
||||||
|
|
||||||
configureRxJavaErrorHandler();
|
|
||||||
|
|
||||||
YoutubeStreamExtractor.setPoTokenProvider(PoTokenProviderImpl.INSTANCE);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onTerminate() {
|
|
||||||
super.onTerminate();
|
|
||||||
PicassoHelper.terminate();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected Downloader getDownloader() {
|
|
||||||
final DownloaderImpl downloader = DownloaderImpl.init(null);
|
|
||||||
setCookiesToDownloader(downloader);
|
|
||||||
return downloader;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void setCookiesToDownloader(final DownloaderImpl downloader) {
|
|
||||||
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(
|
|
||||||
getApplicationContext());
|
|
||||||
final String key = getApplicationContext().getString(R.string.recaptcha_cookies_key);
|
|
||||||
downloader.setCookie(ReCaptchaActivity.RECAPTCHA_COOKIES_KEY, prefs.getString(key, null));
|
|
||||||
downloader.updateYoutubeRestrictedModeCookies(getApplicationContext());
|
|
||||||
}
|
|
||||||
|
|
||||||
private void configureRxJavaErrorHandler() {
|
|
||||||
// https://github.com/ReactiveX/RxJava/wiki/What's-different-in-2.0#error-handling
|
|
||||||
RxJavaPlugins.setErrorHandler(new Consumer<Throwable>() {
|
|
||||||
@Override
|
|
||||||
public void accept(@NonNull final Throwable throwable) {
|
|
||||||
Log.e(TAG, "RxJavaPlugins.ErrorHandler called with -> : "
|
|
||||||
+ "throwable = [" + throwable.getClass().getName() + "]");
|
|
||||||
|
|
||||||
final Throwable actualThrowable;
|
|
||||||
if (throwable instanceof UndeliverableException) {
|
|
||||||
// As UndeliverableException is a wrapper,
|
|
||||||
// get the cause of it to get the "real" exception
|
|
||||||
actualThrowable = Objects.requireNonNull(throwable.getCause());
|
|
||||||
} else {
|
|
||||||
actualThrowable = throwable;
|
|
||||||
}
|
|
||||||
|
|
||||||
final List<Throwable> errors;
|
|
||||||
if (actualThrowable instanceof CompositeException) {
|
|
||||||
errors = ((CompositeException) actualThrowable).getExceptions();
|
|
||||||
} else {
|
|
||||||
errors = List.of(actualThrowable);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (final Throwable error : errors) {
|
|
||||||
if (isThrowableIgnored(error)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (isThrowableCritical(error)) {
|
|
||||||
reportException(error);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Out-of-lifecycle exceptions should only be reported if a debug user wishes so,
|
|
||||||
// When exception is not reported, log it
|
|
||||||
if (isDisposedRxExceptionsReported()) {
|
|
||||||
reportException(actualThrowable);
|
|
||||||
} else {
|
|
||||||
Log.e(TAG, "RxJavaPlugin: Undeliverable Exception received: ", actualThrowable);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isThrowableIgnored(@NonNull final Throwable throwable) {
|
|
||||||
// Don't crash the application over a simple network problem
|
|
||||||
return ExceptionUtils.hasAssignableCause(throwable,
|
|
||||||
// network api cancellation
|
|
||||||
IOException.class, SocketException.class,
|
|
||||||
// blocking code disposed
|
|
||||||
InterruptedException.class, InterruptedIOException.class);
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isThrowableCritical(@NonNull final Throwable throwable) {
|
|
||||||
// Though these exceptions cannot be ignored
|
|
||||||
return ExceptionUtils.hasAssignableCause(throwable,
|
|
||||||
NullPointerException.class, IllegalArgumentException.class, // bug in app
|
|
||||||
OnErrorNotImplementedException.class, MissingBackpressureException.class,
|
|
||||||
IllegalStateException.class); // bug in operator
|
|
||||||
}
|
|
||||||
|
|
||||||
private void reportException(@NonNull final Throwable throwable) {
|
|
||||||
// Throw uncaught exception that will trigger the report system
|
|
||||||
Thread.currentThread().getUncaughtExceptionHandler()
|
|
||||||
.uncaughtException(Thread.currentThread(), throwable);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called in {@link #attachBaseContext(Context)} after calling the {@code super} method.
|
|
||||||
* Should be overridden if MultiDex is enabled, since it has to be initialized before ACRA.
|
|
||||||
*/
|
|
||||||
protected void initACRA() {
|
|
||||||
if (ACRA.isACRASenderServiceProcess()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final CoreConfigurationBuilder acraConfig = new CoreConfigurationBuilder()
|
|
||||||
.withBuildConfigClass(BuildConfig.class);
|
|
||||||
ACRA.init(this, acraConfig);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void initNotificationChannels() {
|
|
||||||
// Keep the importance below DEFAULT to avoid making noise on every notification update for
|
|
||||||
// the main and update channels
|
|
||||||
final List<NotificationChannelCompat> notificationChannelCompats = List.of(
|
|
||||||
new NotificationChannelCompat.Builder(getString(R.string.notification_channel_id),
|
|
||||||
NotificationManagerCompat.IMPORTANCE_LOW)
|
|
||||||
.setName(getString(R.string.notification_channel_name))
|
|
||||||
.setDescription(getString(R.string.notification_channel_description))
|
|
||||||
.build(),
|
|
||||||
new NotificationChannelCompat
|
|
||||||
.Builder(getString(R.string.app_update_notification_channel_id),
|
|
||||||
NotificationManagerCompat.IMPORTANCE_LOW)
|
|
||||||
.setName(getString(R.string.app_update_notification_channel_name))
|
|
||||||
.setDescription(
|
|
||||||
getString(R.string.app_update_notification_channel_description))
|
|
||||||
.build(),
|
|
||||||
new NotificationChannelCompat.Builder(getString(R.string.hash_channel_id),
|
|
||||||
NotificationManagerCompat.IMPORTANCE_HIGH)
|
|
||||||
.setName(getString(R.string.hash_channel_name))
|
|
||||||
.setDescription(getString(R.string.hash_channel_description))
|
|
||||||
.build(),
|
|
||||||
new NotificationChannelCompat.Builder(getString(R.string.error_report_channel_id),
|
|
||||||
NotificationManagerCompat.IMPORTANCE_LOW)
|
|
||||||
.setName(getString(R.string.error_report_channel_name))
|
|
||||||
.setDescription(getString(R.string.error_report_channel_description))
|
|
||||||
.build(),
|
|
||||||
new NotificationChannelCompat
|
|
||||||
.Builder(getString(R.string.streams_notification_channel_id),
|
|
||||||
NotificationManagerCompat.IMPORTANCE_DEFAULT)
|
|
||||||
.setName(getString(R.string.streams_notification_channel_name))
|
|
||||||
.setDescription(
|
|
||||||
getString(R.string.streams_notification_channel_description))
|
|
||||||
.build()
|
|
||||||
);
|
|
||||||
|
|
||||||
final NotificationManagerCompat notificationManager = NotificationManagerCompat.from(this);
|
|
||||||
notificationManager.createNotificationChannelsCompat(notificationChannelCompats);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected boolean isDisposedRxExceptionsReported() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isFirstRun() {
|
|
||||||
return isFirstRun;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
290
app/src/main/java/org/schabi/newpipe/App.kt
Normal file
290
app/src/main/java/org/schabi/newpipe/App.kt
Normal file
@@ -0,0 +1,290 @@
|
|||||||
|
package org.schabi.newpipe
|
||||||
|
|
||||||
|
import android.app.ActivityManager
|
||||||
|
import android.app.Application
|
||||||
|
import android.content.Context
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.core.app.NotificationChannelCompat
|
||||||
|
import androidx.core.app.NotificationManagerCompat
|
||||||
|
import androidx.core.content.getSystemService
|
||||||
|
import androidx.preference.PreferenceManager
|
||||||
|
import coil3.ImageLoader
|
||||||
|
import coil3.SingletonImageLoader
|
||||||
|
import coil3.network.okhttp.OkHttpNetworkFetcherFactory
|
||||||
|
import coil3.request.allowRgb565
|
||||||
|
import coil3.request.crossfade
|
||||||
|
import coil3.util.DebugLogger
|
||||||
|
import com.jakewharton.processphoenix.ProcessPhoenix
|
||||||
|
import dagger.hilt.android.HiltAndroidApp
|
||||||
|
import io.reactivex.rxjava3.exceptions.CompositeException
|
||||||
|
import io.reactivex.rxjava3.exceptions.MissingBackpressureException
|
||||||
|
import io.reactivex.rxjava3.exceptions.OnErrorNotImplementedException
|
||||||
|
import io.reactivex.rxjava3.exceptions.UndeliverableException
|
||||||
|
import io.reactivex.rxjava3.functions.Consumer
|
||||||
|
import io.reactivex.rxjava3.plugins.RxJavaPlugins
|
||||||
|
import org.acra.ACRA.init
|
||||||
|
import org.acra.ACRA.isACRASenderServiceProcess
|
||||||
|
import org.acra.config.CoreConfigurationBuilder
|
||||||
|
import org.schabi.newpipe.error.ReCaptchaActivity
|
||||||
|
import org.schabi.newpipe.extractor.NewPipe
|
||||||
|
import org.schabi.newpipe.extractor.downloader.Downloader
|
||||||
|
import org.schabi.newpipe.extractor.services.youtube.extractors.YoutubeStreamExtractor
|
||||||
|
import org.schabi.newpipe.ktx.hasAssignableCause
|
||||||
|
import org.schabi.newpipe.settings.NewPipeSettings
|
||||||
|
import org.schabi.newpipe.util.BridgeStateSaverInitializer
|
||||||
|
import org.schabi.newpipe.util.Localization
|
||||||
|
import org.schabi.newpipe.util.ServiceHelper
|
||||||
|
import org.schabi.newpipe.util.StateSaver
|
||||||
|
import org.schabi.newpipe.util.image.ImageStrategy
|
||||||
|
import org.schabi.newpipe.util.image.PreferredImageQuality
|
||||||
|
import org.schabi.newpipe.util.potoken.PoTokenProviderImpl
|
||||||
|
import java.io.IOException
|
||||||
|
import java.io.InterruptedIOException
|
||||||
|
import java.net.SocketException
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Copyright (C) Hans-Christoph Steiner 2016 <hans@eds.org>
|
||||||
|
* App.kt is part of NewPipe.
|
||||||
|
*
|
||||||
|
* NewPipe is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* NewPipe is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
@HiltAndroidApp
|
||||||
|
open class App :
|
||||||
|
Application(),
|
||||||
|
SingletonImageLoader.Factory {
|
||||||
|
var isFirstRun = false
|
||||||
|
private set
|
||||||
|
|
||||||
|
override fun attachBaseContext(base: Context?) {
|
||||||
|
super.attachBaseContext(base)
|
||||||
|
initACRA()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreate() {
|
||||||
|
super.onCreate()
|
||||||
|
|
||||||
|
instance = this
|
||||||
|
|
||||||
|
if (ProcessPhoenix.isPhoenixProcess(this)) {
|
||||||
|
Log.i(TAG, "This is a phoenix process! Aborting initialization of App[onCreate]")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if the last used preference version is set
|
||||||
|
// to determine whether this is the first app run
|
||||||
|
val lastUsedPrefVersion =
|
||||||
|
PreferenceManager
|
||||||
|
.getDefaultSharedPreferences(this)
|
||||||
|
.getInt(getString(R.string.last_used_preferences_version), -1)
|
||||||
|
isFirstRun = lastUsedPrefVersion == -1
|
||||||
|
|
||||||
|
// Initialize settings first because other initializations can use its values
|
||||||
|
NewPipeSettings.initSettings(this)
|
||||||
|
|
||||||
|
NewPipe.init(
|
||||||
|
getDownloader(),
|
||||||
|
Localization.getPreferredLocalization(this),
|
||||||
|
Localization.getPreferredContentCountry(this),
|
||||||
|
)
|
||||||
|
Localization.initPrettyTime(Localization.resolvePrettyTime(this))
|
||||||
|
|
||||||
|
BridgeStateSaverInitializer.init(this)
|
||||||
|
StateSaver.init(this)
|
||||||
|
initNotificationChannels()
|
||||||
|
|
||||||
|
ServiceHelper.initServices(this)
|
||||||
|
|
||||||
|
// Initialize image loader
|
||||||
|
val prefs = PreferenceManager.getDefaultSharedPreferences(this)
|
||||||
|
ImageStrategy.setPreferredImageQuality(
|
||||||
|
PreferredImageQuality.fromPreferenceKey(
|
||||||
|
this,
|
||||||
|
prefs.getString(
|
||||||
|
getString(R.string.image_quality_key),
|
||||||
|
getString(R.string.image_quality_default),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
configureRxJavaErrorHandler()
|
||||||
|
|
||||||
|
YoutubeStreamExtractor.setPoTokenProvider(PoTokenProviderImpl)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun newImageLoader(context: Context): ImageLoader =
|
||||||
|
ImageLoader
|
||||||
|
.Builder(this)
|
||||||
|
.logger(if (BuildConfig.DEBUG) DebugLogger() else null)
|
||||||
|
.allowRgb565(getSystemService<ActivityManager>()!!.isLowRamDevice)
|
||||||
|
.crossfade(true)
|
||||||
|
.components {
|
||||||
|
add(OkHttpNetworkFetcherFactory(callFactory = DownloaderImpl.getInstance().client))
|
||||||
|
}.build()
|
||||||
|
|
||||||
|
protected open fun getDownloader(): Downloader {
|
||||||
|
val downloader = DownloaderImpl.init(null)
|
||||||
|
setCookiesToDownloader(downloader)
|
||||||
|
return downloader
|
||||||
|
}
|
||||||
|
|
||||||
|
protected fun setCookiesToDownloader(downloader: DownloaderImpl) {
|
||||||
|
val prefs = PreferenceManager.getDefaultSharedPreferences(this)
|
||||||
|
val key = getString(R.string.recaptcha_cookies_key)
|
||||||
|
downloader.setCookie(ReCaptchaActivity.RECAPTCHA_COOKIES_KEY, prefs.getString(key, null))
|
||||||
|
downloader.updateYoutubeRestrictedModeCookies(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun configureRxJavaErrorHandler() {
|
||||||
|
// https://github.com/ReactiveX/RxJava/wiki/What's-different-in-2.0#error-handling
|
||||||
|
RxJavaPlugins.setErrorHandler(
|
||||||
|
object : Consumer<Throwable> {
|
||||||
|
override fun accept(throwable: Throwable) {
|
||||||
|
Log.e(TAG, "RxJavaPlugins.ErrorHandler called with -> : throwable = [${throwable.javaClass.getName()}]")
|
||||||
|
|
||||||
|
// As UndeliverableException is a wrapper,
|
||||||
|
// get the cause of it to get the "real" exception
|
||||||
|
val actualThrowable = (throwable as? UndeliverableException)?.cause ?: throwable
|
||||||
|
|
||||||
|
val errors = (actualThrowable as? CompositeException)?.exceptions ?: listOf(actualThrowable)
|
||||||
|
|
||||||
|
for (error in errors) {
|
||||||
|
if (isThrowableIgnored(error)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (isThrowableCritical(error)) {
|
||||||
|
reportException(error)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Out-of-lifecycle exceptions should only be reported if a debug user wishes so,
|
||||||
|
// When exception is not reported, log it
|
||||||
|
if (isDisposedRxExceptionsReported()) {
|
||||||
|
reportException(actualThrowable)
|
||||||
|
} else {
|
||||||
|
Log.e(TAG, "RxJavaPlugin: Undeliverable Exception received: ", actualThrowable)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isThrowableIgnored(throwable: Throwable): Boolean {
|
||||||
|
// Don't crash the application over a simple network problem
|
||||||
|
return throwable // network api cancellation
|
||||||
|
.hasAssignableCause(
|
||||||
|
IOException::class.java,
|
||||||
|
SocketException::class.java, // blocking code disposed
|
||||||
|
InterruptedException::class.java,
|
||||||
|
InterruptedIOException::class.java,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isThrowableCritical(throwable: Throwable): Boolean {
|
||||||
|
// Though these exceptions cannot be ignored
|
||||||
|
return throwable
|
||||||
|
.hasAssignableCause(
|
||||||
|
// bug in app
|
||||||
|
NullPointerException::class.java,
|
||||||
|
IllegalArgumentException::class.java,
|
||||||
|
OnErrorNotImplementedException::class.java,
|
||||||
|
MissingBackpressureException::class.java,
|
||||||
|
// bug in operator
|
||||||
|
IllegalStateException::class.java,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun reportException(throwable: Throwable) {
|
||||||
|
// Throw uncaught exception that will trigger the report system
|
||||||
|
Thread
|
||||||
|
.currentThread()
|
||||||
|
.uncaughtExceptionHandler
|
||||||
|
.uncaughtException(Thread.currentThread(), throwable)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called in [.attachBaseContext] after calling the `super` method.
|
||||||
|
* Should be overridden if MultiDex is enabled, since it has to be initialized before ACRA.
|
||||||
|
*/
|
||||||
|
protected fun initACRA() {
|
||||||
|
if (isACRASenderServiceProcess()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val acraConfig =
|
||||||
|
CoreConfigurationBuilder()
|
||||||
|
.withBuildConfigClass(BuildConfig::class.java)
|
||||||
|
init(this, acraConfig)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initNotificationChannels() {
|
||||||
|
// Keep the importance below DEFAULT to avoid making noise on every notification update for
|
||||||
|
// the main and update channels
|
||||||
|
val mainChannel =
|
||||||
|
NotificationChannelCompat
|
||||||
|
.Builder(
|
||||||
|
getString(R.string.notification_channel_id),
|
||||||
|
NotificationManagerCompat.IMPORTANCE_LOW,
|
||||||
|
).setName(getString(R.string.notification_channel_name))
|
||||||
|
.setDescription(getString(R.string.notification_channel_description))
|
||||||
|
.build()
|
||||||
|
val appUpdateChannel =
|
||||||
|
NotificationChannelCompat
|
||||||
|
.Builder(
|
||||||
|
getString(R.string.app_update_notification_channel_id),
|
||||||
|
NotificationManagerCompat.IMPORTANCE_LOW,
|
||||||
|
).setName(getString(R.string.app_update_notification_channel_name))
|
||||||
|
.setDescription(getString(R.string.app_update_notification_channel_description))
|
||||||
|
.build()
|
||||||
|
val hashChannel =
|
||||||
|
NotificationChannelCompat
|
||||||
|
.Builder(
|
||||||
|
getString(R.string.hash_channel_id),
|
||||||
|
NotificationManagerCompat.IMPORTANCE_HIGH,
|
||||||
|
).setName(getString(R.string.hash_channel_name))
|
||||||
|
.setDescription(getString(R.string.hash_channel_description))
|
||||||
|
.build()
|
||||||
|
val errorReportChannel =
|
||||||
|
NotificationChannelCompat
|
||||||
|
.Builder(
|
||||||
|
getString(R.string.error_report_channel_id),
|
||||||
|
NotificationManagerCompat.IMPORTANCE_LOW,
|
||||||
|
).setName(getString(R.string.error_report_channel_name))
|
||||||
|
.setDescription(getString(R.string.error_report_channel_description))
|
||||||
|
.build()
|
||||||
|
val newStreamChannel =
|
||||||
|
NotificationChannelCompat
|
||||||
|
.Builder(
|
||||||
|
getString(R.string.streams_notification_channel_id),
|
||||||
|
NotificationManagerCompat.IMPORTANCE_DEFAULT,
|
||||||
|
).setName(getString(R.string.streams_notification_channel_name))
|
||||||
|
.setDescription(getString(R.string.streams_notification_channel_description))
|
||||||
|
.build()
|
||||||
|
|
||||||
|
val channels = listOf(mainChannel, appUpdateChannel, hashChannel, errorReportChannel, newStreamChannel)
|
||||||
|
|
||||||
|
NotificationManagerCompat.from(this).createNotificationChannelsCompat(channels)
|
||||||
|
}
|
||||||
|
|
||||||
|
protected open fun isDisposedRxExceptionsReported(): Boolean = false
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val PACKAGE_NAME: String = BuildConfig.APPLICATION_ID
|
||||||
|
private val TAG = App::class.java.toString()
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
lateinit var instance: App
|
||||||
|
private set
|
||||||
|
}
|
||||||
|
}
|
||||||
22
app/src/main/java/org/schabi/newpipe/AppModule.kt
Normal file
22
app/src/main/java/org/schabi/newpipe/AppModule.kt
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
package org.schabi.newpipe
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.SharedPreferences
|
||||||
|
import androidx.preference.PreferenceManager
|
||||||
|
import dagger.Module
|
||||||
|
import dagger.Provides
|
||||||
|
import dagger.hilt.InstallIn
|
||||||
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
|
import dagger.hilt.components.SingletonComponent
|
||||||
|
import javax.inject.Singleton
|
||||||
|
|
||||||
|
@Module
|
||||||
|
@InstallIn(SingletonComponent::class)
|
||||||
|
class AppModule {
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
fun providesSharedPreference(@ApplicationContext context: Context): SharedPreferences {
|
||||||
|
return PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -48,6 +48,11 @@ public final class DownloaderImpl extends Downloader {
|
|||||||
this.mCookies = new HashMap<>();
|
this.mCookies = new HashMap<>();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public OkHttpClient getClient() {
|
||||||
|
return client;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* It's recommended to call exactly once in the entire lifetime of the application.
|
* It's recommended to call exactly once in the entire lifetime of the application.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -45,7 +45,6 @@ import android.widget.FrameLayout;
|
|||||||
import android.widget.Spinner;
|
import android.widget.Spinner;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import androidx.appcompat.app.ActionBar;
|
import androidx.appcompat.app.ActionBar;
|
||||||
import androidx.appcompat.app.ActionBarDrawerToggle;
|
import androidx.appcompat.app.ActionBarDrawerToggle;
|
||||||
import androidx.appcompat.app.AppCompatActivity;
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
@@ -53,7 +52,6 @@ import androidx.core.app.ActivityCompat;
|
|||||||
import androidx.core.view.GravityCompat;
|
import androidx.core.view.GravityCompat;
|
||||||
import androidx.drawerlayout.widget.DrawerLayout;
|
import androidx.drawerlayout.widget.DrawerLayout;
|
||||||
import androidx.fragment.app.Fragment;
|
import androidx.fragment.app.Fragment;
|
||||||
import androidx.fragment.app.FragmentContainerView;
|
|
||||||
import androidx.fragment.app.FragmentManager;
|
import androidx.fragment.app.FragmentManager;
|
||||||
import androidx.preference.PreferenceManager;
|
import androidx.preference.PreferenceManager;
|
||||||
|
|
||||||
@@ -67,13 +65,11 @@ import org.schabi.newpipe.databinding.ToolbarLayoutBinding;
|
|||||||
import org.schabi.newpipe.error.ErrorUtil;
|
import org.schabi.newpipe.error.ErrorUtil;
|
||||||
import org.schabi.newpipe.extractor.NewPipe;
|
import org.schabi.newpipe.extractor.NewPipe;
|
||||||
import org.schabi.newpipe.extractor.StreamingService;
|
import org.schabi.newpipe.extractor.StreamingService;
|
||||||
import org.schabi.newpipe.extractor.comments.CommentsInfoItem;
|
|
||||||
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||||
import org.schabi.newpipe.extractor.services.peertube.PeertubeInstance;
|
import org.schabi.newpipe.extractor.services.peertube.PeertubeInstance;
|
||||||
import org.schabi.newpipe.fragments.BackPressable;
|
import org.schabi.newpipe.fragments.BackPressable;
|
||||||
import org.schabi.newpipe.fragments.MainFragment;
|
import org.schabi.newpipe.fragments.MainFragment;
|
||||||
import org.schabi.newpipe.fragments.detail.VideoDetailFragment;
|
import org.schabi.newpipe.fragments.detail.VideoDetailFragment;
|
||||||
import org.schabi.newpipe.fragments.list.comments.CommentRepliesFragment;
|
|
||||||
import org.schabi.newpipe.fragments.list.search.SearchFragment;
|
import org.schabi.newpipe.fragments.list.search.SearchFragment;
|
||||||
import org.schabi.newpipe.local.feed.notifications.NotificationWorker;
|
import org.schabi.newpipe.local.feed.notifications.NotificationWorker;
|
||||||
import org.schabi.newpipe.player.Player;
|
import org.schabi.newpipe.player.Player;
|
||||||
@@ -186,7 +182,7 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
NotificationWorker.initialize(this);
|
NotificationWorker.initialize(this);
|
||||||
}
|
}
|
||||||
if (!UpdateSettingsFragment.wasUserAskedForConsent(this)
|
if (!UpdateSettingsFragment.wasUserAskedForConsent(this)
|
||||||
&& !App.getApp().isFirstRun()
|
&& !App.getInstance().isFirstRun()
|
||||||
&& ReleaseVersionUtil.INSTANCE.isReleaseApk()) {
|
&& ReleaseVersionUtil.INSTANCE.isReleaseApk()) {
|
||||||
UpdateSettingsFragment.askForConsentToUpdateChecks(this);
|
UpdateSettingsFragment.askForConsentToUpdateChecks(this);
|
||||||
}
|
}
|
||||||
@@ -198,7 +194,7 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
protected void onPostCreate(final Bundle savedInstanceState) {
|
protected void onPostCreate(final Bundle savedInstanceState) {
|
||||||
super.onPostCreate(savedInstanceState);
|
super.onPostCreate(savedInstanceState);
|
||||||
|
|
||||||
final App app = App.getApp();
|
final App app = App.getInstance();
|
||||||
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(app);
|
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(app);
|
||||||
|
|
||||||
if (prefs.getBoolean(app.getString(R.string.update_app_key), false)
|
if (prefs.getBoolean(app.getString(R.string.update_app_key), false)
|
||||||
@@ -582,39 +578,27 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
// In case bottomSheet is not visible on the screen or collapsed we can assume that the user
|
// In case bottomSheet is not visible on the screen or collapsed we can assume that the user
|
||||||
// interacts with a fragment inside fragment_holder so all back presses should be
|
// interacts with a fragment inside fragment_holder so all back presses should be
|
||||||
// handled by it
|
// handled by it
|
||||||
if (bottomSheetHiddenOrCollapsed()) {
|
final var fragmentManager = getSupportFragmentManager();
|
||||||
final FragmentManager fm = getSupportFragmentManager();
|
|
||||||
final Fragment fragment = fm.findFragmentById(R.id.fragment_holder);
|
|
||||||
// If current fragment implements BackPressable (i.e. can/wanna handle back press)
|
|
||||||
// delegate the back press to it
|
|
||||||
if (fragment instanceof BackPressable) {
|
|
||||||
if (((BackPressable) fragment).onBackPressed()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
} else if (fragment instanceof CommentRepliesFragment) {
|
|
||||||
// expand DetailsFragment if CommentRepliesFragment was opened
|
|
||||||
// to show the top level comments again
|
|
||||||
// Expand DetailsFragment if CommentRepliesFragment was opened
|
|
||||||
// and no other CommentRepliesFragments are on top of the back stack
|
|
||||||
// to show the top level comments again.
|
|
||||||
openDetailFragmentFromCommentReplies(fm, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
} else {
|
if (bottomSheetHiddenOrCollapsed()) {
|
||||||
final Fragment fragmentPlayer = getSupportFragmentManager()
|
final var fragment = fragmentManager.findFragmentById(R.id.fragment_holder);
|
||||||
.findFragmentById(R.id.fragment_player_holder);
|
|
||||||
// If current fragment implements BackPressable (i.e. can/wanna handle back press)
|
// If current fragment implements BackPressable (i.e. can/wanna handle back press)
|
||||||
// delegate the back press to it
|
// delegate the back press to it
|
||||||
if (fragmentPlayer instanceof BackPressable) {
|
if (fragment instanceof BackPressable backPressable && backPressable.onBackPressed()) {
|
||||||
if (!((BackPressable) fragmentPlayer).onBackPressed()) {
|
|
||||||
BottomSheetBehavior.from(mainBinding.fragmentPlayerHolder)
|
|
||||||
.setState(BottomSheetBehavior.STATE_COLLAPSED);
|
|
||||||
}
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
final var player = fragmentManager.findFragmentById(R.id.fragment_player_holder);
|
||||||
|
// If current fragment implements BackPressable (i.e. can/wanna handle back press)
|
||||||
|
// delegate the back press to it
|
||||||
|
if (player instanceof BackPressable backPressable && !backPressable.onBackPressed()) {
|
||||||
|
BottomSheetBehavior.from(mainBinding.fragmentPlayerHolder)
|
||||||
|
.setState(BottomSheetBehavior.STATE_COLLAPSED);
|
||||||
|
}
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (getSupportFragmentManager().getBackStackEntryCount() == 1) {
|
if (fragmentManager.getBackStackEntryCount() == 1) {
|
||||||
finish();
|
finish();
|
||||||
} else {
|
} else {
|
||||||
super.onBackPressed();
|
super.onBackPressed();
|
||||||
@@ -673,15 +657,9 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
* </pre>
|
* </pre>
|
||||||
*/
|
*/
|
||||||
private void onHomeButtonPressed() {
|
private void onHomeButtonPressed() {
|
||||||
final FragmentManager fm = getSupportFragmentManager();
|
final var fm = getSupportFragmentManager();
|
||||||
final Fragment fragment = fm.findFragmentById(R.id.fragment_holder);
|
|
||||||
|
|
||||||
if (fragment instanceof CommentRepliesFragment) {
|
if (!NavigationHelper.tryGotoSearchFragment(fm)) {
|
||||||
// Expand DetailsFragment if CommentRepliesFragment was opened
|
|
||||||
// and no other CommentRepliesFragments are on top of the back stack
|
|
||||||
// to show the top level comments again.
|
|
||||||
openDetailFragmentFromCommentReplies(fm, true);
|
|
||||||
} else if (!NavigationHelper.tryGotoSearchFragment(fm)) {
|
|
||||||
// If search fragment wasn't found in the backstack go to the main fragment
|
// If search fragment wasn't found in the backstack go to the main fragment
|
||||||
NavigationHelper.gotoMainFragment(fm);
|
NavigationHelper.gotoMainFragment(fm);
|
||||||
}
|
}
|
||||||
@@ -884,68 +862,6 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void openDetailFragmentFromCommentReplies(
|
|
||||||
@NonNull final FragmentManager fm,
|
|
||||||
final boolean popBackStack
|
|
||||||
) {
|
|
||||||
// obtain the name of the fragment under the replies fragment that's going to be popped
|
|
||||||
@Nullable final String fragmentUnderEntryName;
|
|
||||||
if (fm.getBackStackEntryCount() < 2) {
|
|
||||||
fragmentUnderEntryName = null;
|
|
||||||
} else {
|
|
||||||
fragmentUnderEntryName = fm.getBackStackEntryAt(fm.getBackStackEntryCount() - 2)
|
|
||||||
.getName();
|
|
||||||
}
|
|
||||||
|
|
||||||
// the root comment is the comment for which the user opened the replies page
|
|
||||||
@Nullable final CommentRepliesFragment repliesFragment =
|
|
||||||
(CommentRepliesFragment) fm.findFragmentByTag(CommentRepliesFragment.TAG);
|
|
||||||
@Nullable final CommentsInfoItem rootComment =
|
|
||||||
repliesFragment == null ? null : repliesFragment.getCommentsInfoItem();
|
|
||||||
|
|
||||||
// sometimes this function pops the backstack, other times it's handled by the system
|
|
||||||
if (popBackStack) {
|
|
||||||
fm.popBackStackImmediate();
|
|
||||||
}
|
|
||||||
|
|
||||||
// only expand the bottom sheet back if there are no more nested comment replies fragments
|
|
||||||
// stacked under the one that is currently being popped
|
|
||||||
if (CommentRepliesFragment.TAG.equals(fragmentUnderEntryName)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final BottomSheetBehavior<FragmentContainerView> behavior = BottomSheetBehavior
|
|
||||||
.from(mainBinding.fragmentPlayerHolder);
|
|
||||||
// do not return to the comment if the details fragment was closed
|
|
||||||
if (behavior.getState() == BottomSheetBehavior.STATE_HIDDEN) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// scroll to the root comment once the bottom sheet expansion animation is finished
|
|
||||||
behavior.addBottomSheetCallback(new BottomSheetBehavior.BottomSheetCallback() {
|
|
||||||
@Override
|
|
||||||
public void onStateChanged(@NonNull final View bottomSheet,
|
|
||||||
final int newState) {
|
|
||||||
if (newState == BottomSheetBehavior.STATE_EXPANDED) {
|
|
||||||
final Fragment detailFragment = fm.findFragmentById(
|
|
||||||
R.id.fragment_player_holder);
|
|
||||||
if (detailFragment instanceof VideoDetailFragment && rootComment != null) {
|
|
||||||
// should always be the case
|
|
||||||
((VideoDetailFragment) detailFragment).scrollToComment(rootComment);
|
|
||||||
}
|
|
||||||
behavior.removeBottomSheetCallback(this);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onSlide(@NonNull final View bottomSheet, final float slideOffset) {
|
|
||||||
// not needed, listener is removed once the sheet is expanded
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
behavior.setState(BottomSheetBehavior.STATE_EXPANDED);
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean bottomSheetHiddenOrCollapsed() {
|
private boolean bottomSheetHiddenOrCollapsed() {
|
||||||
final BottomSheetBehavior<FrameLayout> bottomSheetBehavior =
|
final BottomSheetBehavior<FrameLayout> bottomSheetBehavior =
|
||||||
BottomSheetBehavior.from(mainBinding.fragmentPlayerHolder);
|
BottomSheetBehavior.from(mainBinding.fragmentPlayerHolder);
|
||||||
|
|||||||
@@ -1,203 +1,31 @@
|
|||||||
package org.schabi.newpipe.about
|
package org.schabi.newpipe.about
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
import androidx.activity.compose.setContent
|
||||||
import android.view.MenuItem
|
import androidx.activity.enableEdgeToEdge
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import android.widget.Button
|
|
||||||
import androidx.annotation.StringRes
|
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.fragment.app.FragmentActivity
|
|
||||||
import androidx.viewpager2.adapter.FragmentStateAdapter
|
|
||||||
import com.google.android.material.tabs.TabLayoutMediator
|
|
||||||
import org.schabi.newpipe.BuildConfig
|
|
||||||
import org.schabi.newpipe.R
|
import org.schabi.newpipe.R
|
||||||
import org.schabi.newpipe.databinding.ActivityAboutBinding
|
import org.schabi.newpipe.ui.components.common.ScaffoldWithToolbar
|
||||||
import org.schabi.newpipe.databinding.FragmentAboutBinding
|
import org.schabi.newpipe.ui.screens.AboutScreen
|
||||||
|
import org.schabi.newpipe.ui.theme.AppTheme
|
||||||
import org.schabi.newpipe.util.Localization
|
import org.schabi.newpipe.util.Localization
|
||||||
import org.schabi.newpipe.util.ThemeHelper
|
|
||||||
import org.schabi.newpipe.util.external_communication.ShareUtils
|
|
||||||
|
|
||||||
class AboutActivity : AppCompatActivity() {
|
class AboutActivity : AppCompatActivity() {
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
Localization.assureCorrectAppLanguage(this)
|
Localization.assureCorrectAppLanguage(this)
|
||||||
|
enableEdgeToEdge()
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
ThemeHelper.setTheme(this)
|
|
||||||
title = getString(R.string.title_activity_about)
|
|
||||||
|
|
||||||
val aboutBinding = ActivityAboutBinding.inflate(layoutInflater)
|
setContent {
|
||||||
setContentView(aboutBinding.root)
|
AppTheme {
|
||||||
setSupportActionBar(aboutBinding.aboutToolbar)
|
ScaffoldWithToolbar(
|
||||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
title = stringResource(R.string.title_activity_about),
|
||||||
|
onBackClick = { onBackPressedDispatcher.onBackPressed() }
|
||||||
// Create the adapter that will return a fragment for each of the three
|
) { padding ->
|
||||||
// primary sections of the activity.
|
AboutScreen(padding)
|
||||||
val mAboutStateAdapter = AboutStateAdapter(this)
|
}
|
||||||
// Set up the ViewPager with the sections adapter.
|
|
||||||
aboutBinding.aboutViewPager2.adapter = mAboutStateAdapter
|
|
||||||
TabLayoutMediator(
|
|
||||||
aboutBinding.aboutTabLayout,
|
|
||||||
aboutBinding.aboutViewPager2
|
|
||||||
) { tab, position ->
|
|
||||||
tab.setText(mAboutStateAdapter.getPageTitle(position))
|
|
||||||
}.attach()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
|
||||||
if (item.itemId == android.R.id.home) {
|
|
||||||
finish()
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return super.onOptionsItemSelected(item)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A placeholder fragment containing a simple view.
|
|
||||||
*/
|
|
||||||
class AboutFragment : Fragment() {
|
|
||||||
private fun Button.openLink(@StringRes url: Int) {
|
|
||||||
setOnClickListener {
|
|
||||||
ShareUtils.openUrlInApp(context, requireContext().getString(url))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateView(
|
|
||||||
inflater: LayoutInflater,
|
|
||||||
container: ViewGroup?,
|
|
||||||
savedInstanceState: Bundle?
|
|
||||||
): View {
|
|
||||||
FragmentAboutBinding.inflate(inflater, container, false).apply {
|
|
||||||
aboutAppVersion.text = BuildConfig.VERSION_NAME
|
|
||||||
aboutGithubLink.openLink(R.string.github_url)
|
|
||||||
aboutDonationLink.openLink(R.string.donation_url)
|
|
||||||
aboutWebsiteLink.openLink(R.string.website_url)
|
|
||||||
aboutPrivacyPolicyLink.openLink(R.string.privacy_policy_url)
|
|
||||||
faqLink.openLink(R.string.faq_url)
|
|
||||||
return root
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A [FragmentStateAdapter] that returns a fragment corresponding to
|
|
||||||
* one of the sections/tabs/pages.
|
|
||||||
*/
|
|
||||||
private class AboutStateAdapter(fa: FragmentActivity) : FragmentStateAdapter(fa) {
|
|
||||||
private val posAbout = 0
|
|
||||||
private val posLicense = 1
|
|
||||||
private val totalCount = 2
|
|
||||||
|
|
||||||
override fun createFragment(position: Int): Fragment {
|
|
||||||
return when (position) {
|
|
||||||
posAbout -> AboutFragment()
|
|
||||||
posLicense -> LicenseFragment.newInstance(SOFTWARE_COMPONENTS)
|
|
||||||
else -> throw IllegalArgumentException("Unknown position for ViewPager2")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getItemCount(): Int {
|
|
||||||
// Show 2 total pages.
|
|
||||||
return totalCount
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getPageTitle(position: Int): Int {
|
|
||||||
return when (position) {
|
|
||||||
posAbout -> R.string.tab_about
|
|
||||||
posLicense -> R.string.tab_licenses
|
|
||||||
else -> throw IllegalArgumentException("Unknown position for ViewPager2")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
/**
|
|
||||||
* List of all software components.
|
|
||||||
*/
|
|
||||||
private val SOFTWARE_COMPONENTS = arrayListOf(
|
|
||||||
SoftwareComponent(
|
|
||||||
"ACRA", "2013", "Kevin Gaudin",
|
|
||||||
"https://github.com/ACRA/acra", StandardLicenses.APACHE2
|
|
||||||
),
|
|
||||||
SoftwareComponent(
|
|
||||||
"AndroidX", "2005 - 2011", "The Android Open Source Project",
|
|
||||||
"https://developer.android.com/jetpack", StandardLicenses.APACHE2
|
|
||||||
),
|
|
||||||
SoftwareComponent(
|
|
||||||
"ExoPlayer", "2014 - 2020", "Google, Inc.",
|
|
||||||
"https://github.com/google/ExoPlayer", StandardLicenses.APACHE2
|
|
||||||
),
|
|
||||||
SoftwareComponent(
|
|
||||||
"GigaGet", "2014 - 2015", "Peter Cai",
|
|
||||||
"https://github.com/PaperAirplane-Dev-Team/GigaGet", StandardLicenses.GPL3
|
|
||||||
),
|
|
||||||
SoftwareComponent(
|
|
||||||
"Groupie", "2016", "Lisa Wray",
|
|
||||||
"https://github.com/lisawray/groupie", StandardLicenses.MIT
|
|
||||||
),
|
|
||||||
SoftwareComponent(
|
|
||||||
"Android-State", "2018", "Evernote",
|
|
||||||
"https://github.com/Evernote/android-state", StandardLicenses.EPL1
|
|
||||||
),
|
|
||||||
SoftwareComponent(
|
|
||||||
"Bridge", "2021", "Livefront",
|
|
||||||
"https://github.com/livefront/bridge", StandardLicenses.APACHE2
|
|
||||||
),
|
|
||||||
SoftwareComponent(
|
|
||||||
"Jsoup", "2009 - 2020", "Jonathan Hedley",
|
|
||||||
"https://github.com/jhy/jsoup", StandardLicenses.MIT
|
|
||||||
),
|
|
||||||
SoftwareComponent(
|
|
||||||
"Markwon", "2019", "Dimitry Ivanov",
|
|
||||||
"https://github.com/noties/Markwon", StandardLicenses.APACHE2
|
|
||||||
),
|
|
||||||
SoftwareComponent(
|
|
||||||
"Material Components for Android", "2016 - 2020", "Google, Inc.",
|
|
||||||
"https://github.com/material-components/material-components-android",
|
|
||||||
StandardLicenses.APACHE2
|
|
||||||
),
|
|
||||||
SoftwareComponent(
|
|
||||||
"NewPipe Extractor", "2017 - 2020", "Christian Schabesberger",
|
|
||||||
"https://github.com/TeamNewPipe/NewPipeExtractor", StandardLicenses.GPL3
|
|
||||||
),
|
|
||||||
SoftwareComponent(
|
|
||||||
"NoNonsense-FilePicker", "2016", "Jonas Kalderstam",
|
|
||||||
"https://github.com/spacecowboy/NoNonsense-FilePicker", StandardLicenses.MPL2
|
|
||||||
),
|
|
||||||
SoftwareComponent(
|
|
||||||
"OkHttp", "2019", "Square, Inc.",
|
|
||||||
"https://square.github.io/okhttp/", StandardLicenses.APACHE2
|
|
||||||
),
|
|
||||||
SoftwareComponent(
|
|
||||||
"Picasso", "2013", "Square, Inc.",
|
|
||||||
"https://square.github.io/picasso/", StandardLicenses.APACHE2
|
|
||||||
),
|
|
||||||
SoftwareComponent(
|
|
||||||
"PrettyTime", "2012 - 2020", "Lincoln Baxter, III",
|
|
||||||
"https://github.com/ocpsoft/prettytime", StandardLicenses.APACHE2
|
|
||||||
),
|
|
||||||
SoftwareComponent(
|
|
||||||
"ProcessPhoenix", "2015", "Jake Wharton",
|
|
||||||
"https://github.com/JakeWharton/ProcessPhoenix", StandardLicenses.APACHE2
|
|
||||||
),
|
|
||||||
SoftwareComponent(
|
|
||||||
"RxAndroid", "2015", "The RxAndroid authors",
|
|
||||||
"https://github.com/ReactiveX/RxAndroid", StandardLicenses.APACHE2
|
|
||||||
),
|
|
||||||
SoftwareComponent(
|
|
||||||
"RxBinding", "2015", "Jake Wharton",
|
|
||||||
"https://github.com/JakeWharton/RxBinding", StandardLicenses.APACHE2
|
|
||||||
),
|
|
||||||
SoftwareComponent(
|
|
||||||
"RxJava", "2016 - 2020", "RxJava Contributors",
|
|
||||||
"https://github.com/ReactiveX/RxJava", StandardLicenses.APACHE2
|
|
||||||
),
|
|
||||||
SoftwareComponent(
|
|
||||||
"SearchPreference", "2018", "ByteHamster",
|
|
||||||
"https://github.com/ByteHamster/SearchPreference", StandardLicenses.MIT
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
package org.schabi.newpipe.about
|
|
||||||
|
|
||||||
import android.os.Parcelable
|
|
||||||
import kotlinx.parcelize.Parcelize
|
|
||||||
import java.io.Serializable
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Class for storing information about a software license.
|
|
||||||
*/
|
|
||||||
@Parcelize
|
|
||||||
class License(val name: String, val abbreviation: String, val filename: String) : Parcelable, Serializable
|
|
||||||
@@ -1,140 +0,0 @@
|
|||||||
package org.schabi.newpipe.about
|
|
||||||
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.util.Base64
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import android.webkit.WebView
|
|
||||||
import androidx.appcompat.app.AlertDialog
|
|
||||||
import androidx.core.os.bundleOf
|
|
||||||
import androidx.fragment.app.Fragment
|
|
||||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
|
||||||
import io.reactivex.rxjava3.core.Observable
|
|
||||||
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
|
||||||
import io.reactivex.rxjava3.disposables.Disposable
|
|
||||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
|
||||||
import org.schabi.newpipe.BuildConfig
|
|
||||||
import org.schabi.newpipe.R
|
|
||||||
import org.schabi.newpipe.databinding.FragmentLicensesBinding
|
|
||||||
import org.schabi.newpipe.databinding.ItemSoftwareComponentBinding
|
|
||||||
import org.schabi.newpipe.ktx.parcelableArrayList
|
|
||||||
import org.schabi.newpipe.util.Localization
|
|
||||||
import org.schabi.newpipe.util.external_communication.ShareUtils
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fragment containing the software licenses.
|
|
||||||
*/
|
|
||||||
class LicenseFragment : Fragment() {
|
|
||||||
private lateinit var softwareComponents: List<SoftwareComponent>
|
|
||||||
private var activeSoftwareComponent: SoftwareComponent? = null
|
|
||||||
private val compositeDisposable = CompositeDisposable()
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
softwareComponents = arguments?.parcelableArrayList<SoftwareComponent>(ARG_COMPONENTS)!!
|
|
||||||
.sortedBy { it.name } // Sort components by name
|
|
||||||
activeSoftwareComponent = savedInstanceState?.getSerializable(SOFTWARE_COMPONENT_KEY) as? SoftwareComponent
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDestroy() {
|
|
||||||
compositeDisposable.dispose()
|
|
||||||
super.onDestroy()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateView(
|
|
||||||
inflater: LayoutInflater,
|
|
||||||
container: ViewGroup?,
|
|
||||||
savedInstanceState: Bundle?
|
|
||||||
): View {
|
|
||||||
val binding = FragmentLicensesBinding.inflate(inflater, container, false)
|
|
||||||
binding.licensesAppReadLicense.setOnClickListener {
|
|
||||||
compositeDisposable.add(
|
|
||||||
showLicense(NEWPIPE_SOFTWARE_COMPONENT)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
for (component in softwareComponents) {
|
|
||||||
val componentBinding = ItemSoftwareComponentBinding
|
|
||||||
.inflate(inflater, container, false)
|
|
||||||
componentBinding.name.text = component.name
|
|
||||||
componentBinding.copyright.text = getString(
|
|
||||||
R.string.copyright,
|
|
||||||
component.years,
|
|
||||||
component.copyrightOwner,
|
|
||||||
component.license.abbreviation
|
|
||||||
)
|
|
||||||
val root: View = componentBinding.root
|
|
||||||
root.tag = component
|
|
||||||
root.setOnClickListener {
|
|
||||||
compositeDisposable.add(
|
|
||||||
showLicense(component)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
binding.licensesSoftwareComponents.addView(root)
|
|
||||||
registerForContextMenu(root)
|
|
||||||
}
|
|
||||||
activeSoftwareComponent?.let { compositeDisposable.add(showLicense(it)) }
|
|
||||||
return binding.root
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onSaveInstanceState(savedInstanceState: Bundle) {
|
|
||||||
super.onSaveInstanceState(savedInstanceState)
|
|
||||||
activeSoftwareComponent?.let { savedInstanceState.putSerializable(SOFTWARE_COMPONENT_KEY, it) }
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun showLicense(
|
|
||||||
softwareComponent: SoftwareComponent
|
|
||||||
): Disposable {
|
|
||||||
return if (context == null) {
|
|
||||||
Disposable.empty()
|
|
||||||
} else {
|
|
||||||
val context = requireContext()
|
|
||||||
activeSoftwareComponent = softwareComponent
|
|
||||||
Observable.fromCallable { getFormattedLicense(context, softwareComponent.license) }
|
|
||||||
.subscribeOn(Schedulers.io())
|
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
|
||||||
.subscribe { formattedLicense ->
|
|
||||||
val webViewData = Base64.encodeToString(
|
|
||||||
formattedLicense.toByteArray(), Base64.NO_PADDING
|
|
||||||
)
|
|
||||||
val webView = WebView(context)
|
|
||||||
webView.loadData(webViewData, "text/html; charset=UTF-8", "base64")
|
|
||||||
|
|
||||||
Localization.assureCorrectAppLanguage(context)
|
|
||||||
val builder = AlertDialog.Builder(requireContext())
|
|
||||||
.setTitle(softwareComponent.name)
|
|
||||||
.setView(webView)
|
|
||||||
.setOnCancelListener { activeSoftwareComponent = null }
|
|
||||||
.setOnDismissListener { activeSoftwareComponent = null }
|
|
||||||
.setPositiveButton(R.string.done) { dialog, _ -> dialog.dismiss() }
|
|
||||||
|
|
||||||
if (softwareComponent != NEWPIPE_SOFTWARE_COMPONENT) {
|
|
||||||
builder.setNeutralButton(R.string.open_website_license) { _, _ ->
|
|
||||||
ShareUtils.openUrlInApp(requireContext(), softwareComponent.link)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
builder.show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private const val ARG_COMPONENTS = "components"
|
|
||||||
private const val SOFTWARE_COMPONENT_KEY = "ACTIVE_SOFTWARE_COMPONENT"
|
|
||||||
private val NEWPIPE_SOFTWARE_COMPONENT = SoftwareComponent(
|
|
||||||
"NewPipe",
|
|
||||||
"2014-2023",
|
|
||||||
"Team NewPipe",
|
|
||||||
"https://newpipe.net/",
|
|
||||||
StandardLicenses.GPL3,
|
|
||||||
BuildConfig.VERSION_NAME
|
|
||||||
)
|
|
||||||
|
|
||||||
fun newInstance(softwareComponents: ArrayList<SoftwareComponent>): LicenseFragment {
|
|
||||||
val fragment = LicenseFragment()
|
|
||||||
fragment.arguments = bundleOf(ARG_COMPONENTS to softwareComponents)
|
|
||||||
return fragment
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
package org.schabi.newpipe.about
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import org.schabi.newpipe.R
|
|
||||||
import org.schabi.newpipe.util.ThemeHelper
|
|
||||||
import java.io.IOException
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param context the context to use
|
|
||||||
* @param license the license
|
|
||||||
* @return String which contains a HTML formatted license page
|
|
||||||
* styled according to the context's theme
|
|
||||||
*/
|
|
||||||
fun getFormattedLicense(context: Context, license: License): String {
|
|
||||||
try {
|
|
||||||
return context.assets.open(license.filename).bufferedReader().use { it.readText() }
|
|
||||||
// split the HTML file and insert the stylesheet into the HEAD of the file
|
|
||||||
.replace("</head>", "<style>${getLicenseStylesheet(context)}</style></head>")
|
|
||||||
} catch (e: IOException) {
|
|
||||||
throw IllegalArgumentException("Could not get license file: ${license.filename}", e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param context the Android context
|
|
||||||
* @return String which is a CSS stylesheet according to the context's theme
|
|
||||||
*/
|
|
||||||
fun getLicenseStylesheet(context: Context): String {
|
|
||||||
val isLightTheme = ThemeHelper.isLightThemeSelected(context)
|
|
||||||
val licenseBackgroundColor = getHexRGBColor(
|
|
||||||
context, if (isLightTheme) R.color.light_license_background_color else R.color.dark_license_background_color
|
|
||||||
)
|
|
||||||
val licenseTextColor = getHexRGBColor(
|
|
||||||
context, if (isLightTheme) R.color.light_license_text_color else R.color.dark_license_text_color
|
|
||||||
)
|
|
||||||
val youtubePrimaryColor = getHexRGBColor(
|
|
||||||
context, if (isLightTheme) R.color.light_youtube_primary_color else R.color.dark_youtube_primary_color
|
|
||||||
)
|
|
||||||
return "body{padding:12px 15px;margin:0;background:#$licenseBackgroundColor;color:#$licenseTextColor}" +
|
|
||||||
"a[href]{color:#$youtubePrimaryColor}pre{white-space:pre-wrap}"
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Cast R.color to a hexadecimal color value.
|
|
||||||
*
|
|
||||||
* @param context the context to use
|
|
||||||
* @param color the color number from R.color
|
|
||||||
* @return a six characters long String with hexadecimal RGB values
|
|
||||||
*/
|
|
||||||
fun getHexRGBColor(context: Context, color: Int): String {
|
|
||||||
return context.getString(color).substring(3)
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
package org.schabi.newpipe.about
|
|
||||||
|
|
||||||
import android.os.Parcelable
|
|
||||||
import kotlinx.parcelize.Parcelize
|
|
||||||
import java.io.Serializable
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
class SoftwareComponent
|
|
||||||
@JvmOverloads
|
|
||||||
constructor(
|
|
||||||
val name: String,
|
|
||||||
val years: String,
|
|
||||||
val copyrightOwner: String,
|
|
||||||
val link: String,
|
|
||||||
val license: License,
|
|
||||||
val version: String? = null
|
|
||||||
) : Parcelable, Serializable
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
package org.schabi.newpipe.about
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Class containing information about standard software licenses.
|
|
||||||
*/
|
|
||||||
object StandardLicenses {
|
|
||||||
@JvmField
|
|
||||||
val GPL3 = License("GNU General Public License, Version 3.0", "GPLv3", "gpl_3.html")
|
|
||||||
|
|
||||||
@JvmField
|
|
||||||
val APACHE2 = License("Apache License, Version 2.0", "ALv2", "apache2.html")
|
|
||||||
|
|
||||||
@JvmField
|
|
||||||
val MPL2 = License("Mozilla Public License, Version 2.0", "MPL 2.0", "mpl2.html")
|
|
||||||
|
|
||||||
@JvmField
|
|
||||||
val MIT = License("MIT License", "MIT", "mit.html")
|
|
||||||
|
|
||||||
@JvmField
|
|
||||||
val EPL1 = License("Eclipse Public License, Version 1.0", "EPL 1.0", "epl1.html")
|
|
||||||
}
|
|
||||||
@@ -8,6 +8,7 @@ import androidx.room.Query
|
|||||||
import androidx.room.Transaction
|
import androidx.room.Transaction
|
||||||
import io.reactivex.rxjava3.core.Completable
|
import io.reactivex.rxjava3.core.Completable
|
||||||
import io.reactivex.rxjava3.core.Flowable
|
import io.reactivex.rxjava3.core.Flowable
|
||||||
|
import io.reactivex.rxjava3.core.Maybe
|
||||||
import org.schabi.newpipe.database.BasicDAO
|
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
|
||||||
@@ -27,7 +28,7 @@ abstract class StreamDAO : BasicDAO<StreamEntity> {
|
|||||||
abstract override fun listByService(serviceId: Int): Flowable<List<StreamEntity>>
|
abstract override fun listByService(serviceId: Int): Flowable<List<StreamEntity>>
|
||||||
|
|
||||||
@Query("SELECT * FROM streams WHERE url = :url AND service_id = :serviceId")
|
@Query("SELECT * FROM streams WHERE url = :url AND service_id = :serviceId")
|
||||||
abstract fun getStream(serviceId: Long, url: String): Flowable<List<StreamEntity>>
|
abstract fun getStream(serviceId: Long, url: String): Maybe<StreamEntity>
|
||||||
|
|
||||||
@Query("UPDATE streams SET uploader_url = :uploaderUrl WHERE url = :url AND service_id = :serviceId")
|
@Query("UPDATE streams SET uploader_url = :uploaderUrl WHERE url = :url AND service_id = :serviceId")
|
||||||
abstract fun setUploaderUrl(serviceId: Long, url: String, uploaderUrl: String): Completable
|
abstract fun setUploaderUrl(serviceId: Long, url: String, uploaderUrl: String): Completable
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
package org.schabi.newpipe.database.stream.dao;
|
package org.schabi.newpipe.database.stream.dao;
|
||||||
|
|
||||||
|
import static org.schabi.newpipe.database.stream.model.StreamStateEntity.JOIN_STREAM_ID;
|
||||||
|
import static org.schabi.newpipe.database.stream.model.StreamStateEntity.STREAM_STATE_TABLE;
|
||||||
|
|
||||||
import androidx.room.Dao;
|
import androidx.room.Dao;
|
||||||
import androidx.room.Insert;
|
import androidx.room.Insert;
|
||||||
import androidx.room.OnConflictStrategy;
|
import androidx.room.OnConflictStrategy;
|
||||||
@@ -12,9 +15,7 @@ import org.schabi.newpipe.database.stream.model.StreamStateEntity;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import io.reactivex.rxjava3.core.Flowable;
|
import io.reactivex.rxjava3.core.Flowable;
|
||||||
|
import io.reactivex.rxjava3.core.Maybe;
|
||||||
import static org.schabi.newpipe.database.stream.model.StreamStateEntity.JOIN_STREAM_ID;
|
|
||||||
import static org.schabi.newpipe.database.stream.model.StreamStateEntity.STREAM_STATE_TABLE;
|
|
||||||
|
|
||||||
@Dao
|
@Dao
|
||||||
public interface StreamStateDAO extends BasicDAO<StreamStateEntity> {
|
public interface StreamStateDAO extends BasicDAO<StreamStateEntity> {
|
||||||
@@ -32,7 +33,7 @@ public interface StreamStateDAO extends BasicDAO<StreamStateEntity> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Query("SELECT * FROM " + STREAM_STATE_TABLE + " WHERE " + JOIN_STREAM_ID + " = :streamId")
|
@Query("SELECT * FROM " + STREAM_STATE_TABLE + " WHERE " + JOIN_STREAM_ID + " = :streamId")
|
||||||
Flowable<List<StreamStateEntity>> getState(long streamId);
|
Maybe<StreamStateEntity> getState(long streamId);
|
||||||
|
|
||||||
@Query("DELETE FROM " + STREAM_STATE_TABLE + " WHERE " + JOIN_STREAM_ID + " = :streamId")
|
@Query("DELETE FROM " + STREAM_STATE_TABLE + " WHERE " + JOIN_STREAM_ID + " = :streamId")
|
||||||
int deleteState(long streamId);
|
int deleteState(long streamId);
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import com.google.auto.service.AutoService;
|
|||||||
import org.acra.config.CoreConfiguration;
|
import org.acra.config.CoreConfiguration;
|
||||||
import org.acra.sender.ReportSender;
|
import org.acra.sender.ReportSender;
|
||||||
import org.acra.sender.ReportSenderFactory;
|
import org.acra.sender.ReportSenderFactory;
|
||||||
import org.schabi.newpipe.App;
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Created by Christian Schabesberger on 13.09.16.
|
* Created by Christian Schabesberger on 13.09.16.
|
||||||
|
|||||||
@@ -6,9 +6,11 @@ import android.view.View;
|
|||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
|
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.compose.ui.platform.ComposeView;
|
||||||
|
|
||||||
import org.schabi.newpipe.BaseFragment;
|
import org.schabi.newpipe.BaseFragment;
|
||||||
import org.schabi.newpipe.R;
|
import org.schabi.newpipe.R;
|
||||||
|
import org.schabi.newpipe.ui.emptystate.EmptyStateUtil;
|
||||||
|
|
||||||
public class EmptyFragment extends BaseFragment {
|
public class EmptyFragment extends BaseFragment {
|
||||||
private static final String SHOW_MESSAGE = "SHOW_MESSAGE";
|
private static final String SHOW_MESSAGE = "SHOW_MESSAGE";
|
||||||
@@ -26,8 +28,10 @@ public class EmptyFragment extends BaseFragment {
|
|||||||
final Bundle savedInstanceState) {
|
final Bundle savedInstanceState) {
|
||||||
final boolean showMessage = getArguments().getBoolean(SHOW_MESSAGE);
|
final boolean showMessage = getArguments().getBoolean(SHOW_MESSAGE);
|
||||||
final View view = inflater.inflate(R.layout.fragment_empty, container, false);
|
final View view = inflater.inflate(R.layout.fragment_empty, container, false);
|
||||||
view.findViewById(R.id.empty_state_view).setVisibility(
|
|
||||||
showMessage ? View.VISIBLE : View.GONE);
|
final ComposeView composeView = view.findViewById(R.id.empty_state_view);
|
||||||
|
EmptyStateUtil.setEmptyStateComposable(composeView);
|
||||||
|
composeView.setVisibility(showMessage ? View.VISIBLE : View.GONE);
|
||||||
return view;
|
return view;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -74,7 +74,6 @@ import org.schabi.newpipe.error.ReCaptchaActivity;
|
|||||||
import org.schabi.newpipe.error.UserAction;
|
import org.schabi.newpipe.error.UserAction;
|
||||||
import org.schabi.newpipe.extractor.Image;
|
import org.schabi.newpipe.extractor.Image;
|
||||||
import org.schabi.newpipe.extractor.NewPipe;
|
import org.schabi.newpipe.extractor.NewPipe;
|
||||||
import org.schabi.newpipe.extractor.comments.CommentsInfoItem;
|
|
||||||
import org.schabi.newpipe.extractor.exceptions.ContentNotSupportedException;
|
import org.schabi.newpipe.extractor.exceptions.ContentNotSupportedException;
|
||||||
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||||
import org.schabi.newpipe.extractor.stream.AudioStream;
|
import org.schabi.newpipe.extractor.stream.AudioStream;
|
||||||
@@ -117,7 +116,7 @@ import org.schabi.newpipe.util.StreamTypeUtil;
|
|||||||
import org.schabi.newpipe.util.ThemeHelper;
|
import org.schabi.newpipe.util.ThemeHelper;
|
||||||
import org.schabi.newpipe.util.external_communication.KoreUtils;
|
import org.schabi.newpipe.util.external_communication.KoreUtils;
|
||||||
import org.schabi.newpipe.util.external_communication.ShareUtils;
|
import org.schabi.newpipe.util.external_communication.ShareUtils;
|
||||||
import org.schabi.newpipe.util.image.PicassoHelper;
|
import org.schabi.newpipe.util.image.CoilHelper;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Iterator;
|
import java.util.Iterator;
|
||||||
@@ -128,6 +127,7 @@ import java.util.Optional;
|
|||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
|
import coil3.util.CoilUtils;
|
||||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||||
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
||||||
import io.reactivex.rxjava3.disposables.Disposable;
|
import io.reactivex.rxjava3.disposables.Disposable;
|
||||||
@@ -159,8 +159,6 @@ public final class VideoDetailFragment
|
|||||||
private static final String DESCRIPTION_TAB_TAG = "DESCRIPTION TAB";
|
private static final String DESCRIPTION_TAB_TAG = "DESCRIPTION TAB";
|
||||||
private static final String EMPTY_TAB_TAG = "EMPTY TAB";
|
private static final String EMPTY_TAB_TAG = "EMPTY TAB";
|
||||||
|
|
||||||
private static final String PICASSO_VIDEO_DETAILS_TAG = "PICASSO_VIDEO_DETAILS_TAG";
|
|
||||||
|
|
||||||
// tabs
|
// tabs
|
||||||
private boolean showComments;
|
private boolean showComments;
|
||||||
private boolean showRelatedItems;
|
private boolean showRelatedItems;
|
||||||
@@ -190,21 +188,21 @@ public final class VideoDetailFragment
|
|||||||
};
|
};
|
||||||
|
|
||||||
@State
|
@State
|
||||||
protected int serviceId = Constants.NO_SERVICE_ID;
|
int serviceId = Constants.NO_SERVICE_ID;
|
||||||
@State
|
@State
|
||||||
@NonNull
|
@NonNull
|
||||||
protected String title = "";
|
String title = "";
|
||||||
@State
|
@State
|
||||||
@Nullable
|
@Nullable
|
||||||
protected String url = null;
|
String url = null;
|
||||||
@Nullable
|
@Nullable
|
||||||
protected PlayQueue playQueue = null;
|
private PlayQueue playQueue = null;
|
||||||
@State
|
@State
|
||||||
int bottomSheetState = BottomSheetBehavior.STATE_EXPANDED;
|
int bottomSheetState = BottomSheetBehavior.STATE_EXPANDED;
|
||||||
@State
|
@State
|
||||||
int lastStableBottomSheetState = BottomSheetBehavior.STATE_EXPANDED;
|
int lastStableBottomSheetState = BottomSheetBehavior.STATE_EXPANDED;
|
||||||
@State
|
@State
|
||||||
protected boolean autoPlayEnabled = true;
|
boolean autoPlayEnabled = true;
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
private StreamInfo currentInfo = null;
|
private StreamInfo currentInfo = null;
|
||||||
@@ -248,7 +246,7 @@ public final class VideoDetailFragment
|
|||||||
// It will do nothing if the player is not in fullscreen mode
|
// It will do nothing if the player is not in fullscreen mode
|
||||||
hideSystemUiIfNeeded();
|
hideSystemUiIfNeeded();
|
||||||
|
|
||||||
final Optional<MainPlayerUi> playerUi = player.UIs().get(MainPlayerUi.class);
|
final Optional<MainPlayerUi> playerUi = player.UIs().getOpt(MainPlayerUi.class);
|
||||||
if (!player.videoPlayerSelected() && !playAfterConnect) {
|
if (!player.videoPlayerSelected() && !playAfterConnect) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -440,18 +438,15 @@ public final class VideoDetailFragment
|
|||||||
@Override
|
@Override
|
||||||
public void onActivityResult(final int requestCode, final int resultCode, final Intent data) {
|
public void onActivityResult(final int requestCode, final int resultCode, final Intent data) {
|
||||||
super.onActivityResult(requestCode, resultCode, data);
|
super.onActivityResult(requestCode, resultCode, data);
|
||||||
switch (requestCode) {
|
if (requestCode == ReCaptchaActivity.RECAPTCHA_REQUEST) {
|
||||||
case ReCaptchaActivity.RECAPTCHA_REQUEST:
|
if (resultCode == Activity.RESULT_OK) {
|
||||||
if (resultCode == Activity.RESULT_OK) {
|
NavigationHelper.openVideoDetailFragment(requireContext(), getFM(),
|
||||||
NavigationHelper.openVideoDetailFragment(requireContext(), getFM(),
|
serviceId, url, title, null, false);
|
||||||
serviceId, url, title, null, false);
|
} else {
|
||||||
} else {
|
Log.e(TAG, "ReCaptcha failed");
|
||||||
Log.e(TAG, "ReCaptcha failed");
|
}
|
||||||
}
|
} else {
|
||||||
break;
|
Log.e(TAG, "Request code from activity not supported [" + requestCode + "]");
|
||||||
default:
|
|
||||||
Log.e(TAG, "Request code from activity not supported [" + requestCode + "]");
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -531,7 +526,7 @@ public final class VideoDetailFragment
|
|||||||
binding.overlayPlayPauseButton.setOnClickListener(v -> {
|
binding.overlayPlayPauseButton.setOnClickListener(v -> {
|
||||||
if (playerIsNotStopped()) {
|
if (playerIsNotStopped()) {
|
||||||
player.playPause();
|
player.playPause();
|
||||||
player.UIs().get(VideoPlayerUi.class).ifPresent(ui -> ui.hideControls(0, 0));
|
player.UIs().getOpt(VideoPlayerUi.class).ifPresent(ui -> ui.hideControls(0, 0));
|
||||||
showSystemUi();
|
showSystemUi();
|
||||||
} else {
|
} else {
|
||||||
autoPlayEnabled = true; // forcefully start playing
|
autoPlayEnabled = true; // forcefully start playing
|
||||||
@@ -690,7 +685,7 @@ public final class VideoDetailFragment
|
|||||||
@Override
|
@Override
|
||||||
public boolean onKeyDown(final int keyCode) {
|
public boolean onKeyDown(final int keyCode) {
|
||||||
return isPlayerAvailable()
|
return isPlayerAvailable()
|
||||||
&& player.UIs().get(VideoPlayerUi.class)
|
&& player.UIs().getOpt(VideoPlayerUi.class)
|
||||||
.map(playerUi -> playerUi.onKeyDown(keyCode)).orElse(false);
|
.map(playerUi -> playerUi.onKeyDown(keyCode)).orElse(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -817,25 +812,17 @@ public final class VideoDetailFragment
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void prepareAndLoadInfo() {
|
private void prepareAndLoadInfo() {
|
||||||
scrollToTop();
|
scrollToTop();
|
||||||
startLoading(false);
|
startLoading(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void startLoading(final boolean forceLoad) {
|
public void startLoading(final boolean forceLoad) {
|
||||||
super.startLoading(forceLoad);
|
startLoading(forceLoad, null);
|
||||||
|
|
||||||
initTabs();
|
|
||||||
currentInfo = null;
|
|
||||||
if (currentWorker != null) {
|
|
||||||
currentWorker.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
runWorker(forceLoad, stack.isEmpty());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void startLoading(final boolean forceLoad, final boolean addToBackStack) {
|
private void startLoading(final boolean forceLoad, final @Nullable Boolean addToBackStack) {
|
||||||
super.startLoading(forceLoad);
|
super.startLoading(forceLoad);
|
||||||
|
|
||||||
initTabs();
|
initTabs();
|
||||||
@@ -844,7 +831,7 @@ public final class VideoDetailFragment
|
|||||||
currentWorker.dispose();
|
currentWorker.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
runWorker(forceLoad, addToBackStack);
|
runWorker(forceLoad, addToBackStack != null ? addToBackStack : stack.isEmpty());
|
||||||
}
|
}
|
||||||
|
|
||||||
private void runWorker(final boolean forceLoad, final boolean addToBackStack) {
|
private void runWorker(final boolean forceLoad, final boolean addToBackStack) {
|
||||||
@@ -892,8 +879,7 @@ public final class VideoDetailFragment
|
|||||||
tabContentDescriptions.clear();
|
tabContentDescriptions.clear();
|
||||||
|
|
||||||
if (shouldShowComments()) {
|
if (shouldShowComments()) {
|
||||||
pageAdapter.addFragment(
|
pageAdapter.addFragment(CommentsFragment.getInstance(serviceId, url), COMMENTS_TAB_TAG);
|
||||||
CommentsFragment.getInstance(serviceId, url, title), COMMENTS_TAB_TAG);
|
|
||||||
tabIcons.add(R.drawable.ic_comment);
|
tabIcons.add(R.drawable.ic_comment);
|
||||||
tabContentDescriptions.add(R.string.comments_tab_description);
|
tabContentDescriptions.add(R.string.comments_tab_description);
|
||||||
}
|
}
|
||||||
@@ -1023,20 +1009,6 @@ public final class VideoDetailFragment
|
|||||||
updateTabLayoutVisibility();
|
updateTabLayoutVisibility();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void scrollToComment(final CommentsInfoItem comment) {
|
|
||||||
final int commentsTabPos = pageAdapter.getItemPositionByTitle(COMMENTS_TAB_TAG);
|
|
||||||
final Fragment fragment = pageAdapter.getItem(commentsTabPos);
|
|
||||||
if (!(fragment instanceof CommentsFragment)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// unexpand the app bar only if scrolling to the comment succeeded
|
|
||||||
if (((CommentsFragment) fragment).scrollToComment(comment)) {
|
|
||||||
binding.appBarLayout.setExpanded(false, false);
|
|
||||||
binding.viewPager.setCurrentItem(commentsTabPos, false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
// Play Utils
|
// Play Utils
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
@@ -1045,7 +1017,7 @@ public final class VideoDetailFragment
|
|||||||
// If a user watched video inside fullscreen mode and than chose another player
|
// If a user watched video inside fullscreen mode and than chose another player
|
||||||
// return to non-fullscreen mode
|
// return to non-fullscreen mode
|
||||||
if (isPlayerAvailable()) {
|
if (isPlayerAvailable()) {
|
||||||
player.UIs().get(MainPlayerUi.class).ifPresent(playerUi -> {
|
player.UIs().getOpt(MainPlayerUi.class).ifPresent(playerUi -> {
|
||||||
if (playerUi.isFullscreen()) {
|
if (playerUi.isFullscreen()) {
|
||||||
playerUi.toggleFullscreen();
|
playerUi.toggleFullscreen();
|
||||||
}
|
}
|
||||||
@@ -1155,7 +1127,7 @@ public final class VideoDetailFragment
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void openMainPlayer() {
|
private void openMainPlayer() {
|
||||||
if (!isPlayerServiceAvailable()) {
|
if (noPlayerServiceAvailable()) {
|
||||||
playerHolder.startService(autoPlayEnabled, this);
|
playerHolder.startService(autoPlayEnabled, this);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1180,7 +1152,7 @@ public final class VideoDetailFragment
|
|||||||
*/
|
*/
|
||||||
private void hideMainPlayerOnLoadingNewStream() {
|
private void hideMainPlayerOnLoadingNewStream() {
|
||||||
final var root = getRoot();
|
final var root = getRoot();
|
||||||
if (!isPlayerServiceAvailable() || root.isEmpty() || !player.videoPlayerSelected()) {
|
if (noPlayerServiceAvailable() || root.isEmpty() || !player.videoPlayerSelected()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1261,7 +1233,7 @@ public final class VideoDetailFragment
|
|||||||
// setup the surface view height, so that it fits the video correctly
|
// setup the surface view height, so that it fits the video correctly
|
||||||
setHeightThumbnail();
|
setHeightThumbnail();
|
||||||
|
|
||||||
player.UIs().get(MainPlayerUi.class).ifPresent(playerUi -> {
|
player.UIs().getOpt(MainPlayerUi.class).ifPresent(playerUi -> {
|
||||||
// sometimes binding would be null here, even though getView() != null above u.u
|
// sometimes binding would be null here, even though getView() != null above u.u
|
||||||
if (binding != null) {
|
if (binding != null) {
|
||||||
// prevent from re-adding a view multiple times
|
// prevent from re-adding a view multiple times
|
||||||
@@ -1277,7 +1249,7 @@ public final class VideoDetailFragment
|
|||||||
makeDefaultHeightForVideoPlaceholder();
|
makeDefaultHeightForVideoPlaceholder();
|
||||||
|
|
||||||
if (player != null) {
|
if (player != null) {
|
||||||
player.UIs().get(VideoPlayerUi.class).ifPresent(VideoPlayerUi::removeViewFromParent);
|
player.UIs().getOpt(VideoPlayerUi.class).ifPresent(VideoPlayerUi::removeViewFromParent);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1344,7 +1316,7 @@ public final class VideoDetailFragment
|
|||||||
binding.detailThumbnailImageView.setMinimumHeight(newHeight);
|
binding.detailThumbnailImageView.setMinimumHeight(newHeight);
|
||||||
if (isPlayerAvailable()) {
|
if (isPlayerAvailable()) {
|
||||||
final int maxHeight = (int) (metrics.heightPixels * MAX_PLAYER_HEIGHT);
|
final int maxHeight = (int) (metrics.heightPixels * MAX_PLAYER_HEIGHT);
|
||||||
player.UIs().get(VideoPlayerUi.class).ifPresent(ui ->
|
player.UIs().getOpt(VideoPlayerUi.class).ifPresent(ui ->
|
||||||
ui.getBinding().surfaceView.setHeights(newHeight,
|
ui.getBinding().surfaceView.setHeights(newHeight,
|
||||||
ui.isFullscreen() ? newHeight : maxHeight));
|
ui.isFullscreen() ? newHeight : maxHeight));
|
||||||
}
|
}
|
||||||
@@ -1354,23 +1326,23 @@ public final class VideoDetailFragment
|
|||||||
binding.detailContentRootHiding.setVisibility(View.VISIBLE);
|
binding.detailContentRootHiding.setVisibility(View.VISIBLE);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void setInitialData(final int newServiceId,
|
private void setInitialData(final int newServiceId,
|
||||||
@Nullable final String newUrl,
|
@Nullable final String newUrl,
|
||||||
@NonNull final String newTitle,
|
@NonNull final String newTitle,
|
||||||
@Nullable final PlayQueue newPlayQueue) {
|
@Nullable final PlayQueue newPlayQueue) {
|
||||||
this.serviceId = newServiceId;
|
this.serviceId = newServiceId;
|
||||||
this.url = newUrl;
|
this.url = newUrl;
|
||||||
this.title = newTitle;
|
this.title = newTitle;
|
||||||
this.playQueue = newPlayQueue;
|
this.playQueue = newPlayQueue;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setErrorImage(final int imageResource) {
|
private void setErrorImage() {
|
||||||
if (binding == null || activity == null) {
|
if (binding == null || activity == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.detailThumbnailImageView.setImageDrawable(
|
binding.detailThumbnailImageView.setImageDrawable(
|
||||||
AppCompatResources.getDrawable(requireContext(), imageResource));
|
AppCompatResources.getDrawable(requireContext(), R.drawable.not_available_monkey));
|
||||||
animate(binding.detailThumbnailImageView, false, 0, AnimationType.ALPHA,
|
animate(binding.detailThumbnailImageView, false, 0, AnimationType.ALPHA,
|
||||||
0, () -> animate(binding.detailThumbnailImageView, true, 500));
|
0, () -> animate(binding.detailThumbnailImageView, true, 500));
|
||||||
}
|
}
|
||||||
@@ -1378,7 +1350,7 @@ public final class VideoDetailFragment
|
|||||||
@Override
|
@Override
|
||||||
public void handleError() {
|
public void handleError() {
|
||||||
super.handleError();
|
super.handleError();
|
||||||
setErrorImage(R.drawable.not_available_monkey);
|
setErrorImage();
|
||||||
|
|
||||||
if (binding.relatedItemsLayout != null) { // hide related streams for tablets
|
if (binding.relatedItemsLayout != null) { // hide related streams for tablets
|
||||||
binding.relatedItemsLayout.setVisibility(View.INVISIBLE);
|
binding.relatedItemsLayout.setVisibility(View.INVISIBLE);
|
||||||
@@ -1481,7 +1453,11 @@ public final class VideoDetailFragment
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
PicassoHelper.cancelTag(PICASSO_VIDEO_DETAILS_TAG);
|
CoilUtils.dispose(binding.detailThumbnailImageView);
|
||||||
|
CoilUtils.dispose(binding.detailSubChannelThumbnailView);
|
||||||
|
CoilUtils.dispose(binding.overlayThumbnail);
|
||||||
|
CoilUtils.dispose(binding.detailUploaderThumbnailView);
|
||||||
|
|
||||||
binding.detailThumbnailImageView.setImageBitmap(null);
|
binding.detailThumbnailImageView.setImageBitmap(null);
|
||||||
binding.detailSubChannelThumbnailView.setImageBitmap(null);
|
binding.detailSubChannelThumbnailView.setImageBitmap(null);
|
||||||
}
|
}
|
||||||
@@ -1572,8 +1548,8 @@ public final class VideoDetailFragment
|
|||||||
binding.detailSecondaryControlPanel.setVisibility(View.GONE);
|
binding.detailSecondaryControlPanel.setVisibility(View.GONE);
|
||||||
|
|
||||||
checkUpdateProgressInfo(info);
|
checkUpdateProgressInfo(info);
|
||||||
PicassoHelper.loadDetailsThumbnail(info.getThumbnails()).tag(PICASSO_VIDEO_DETAILS_TAG)
|
CoilHelper.INSTANCE.loadDetailsThumbnail(binding.detailThumbnailImageView,
|
||||||
.into(binding.detailThumbnailImageView);
|
info.getThumbnails());
|
||||||
showMetaInfoInTextView(info.getMetaInfo(), binding.detailMetaInfoTextView,
|
showMetaInfoInTextView(info.getMetaInfo(), binding.detailMetaInfoTextView,
|
||||||
binding.detailMetaInfoSeparator, disposables);
|
binding.detailMetaInfoSeparator, disposables);
|
||||||
|
|
||||||
@@ -1623,8 +1599,8 @@ public final class VideoDetailFragment
|
|||||||
binding.detailUploaderTextView.setVisibility(View.GONE);
|
binding.detailUploaderTextView.setVisibility(View.GONE);
|
||||||
}
|
}
|
||||||
|
|
||||||
PicassoHelper.loadAvatar(info.getUploaderAvatars()).tag(PICASSO_VIDEO_DETAILS_TAG)
|
CoilHelper.INSTANCE.loadAvatar(binding.detailSubChannelThumbnailView,
|
||||||
.into(binding.detailSubChannelThumbnailView);
|
info.getUploaderAvatars());
|
||||||
binding.detailSubChannelThumbnailView.setVisibility(View.VISIBLE);
|
binding.detailSubChannelThumbnailView.setVisibility(View.VISIBLE);
|
||||||
binding.detailUploaderThumbnailView.setVisibility(View.GONE);
|
binding.detailUploaderThumbnailView.setVisibility(View.GONE);
|
||||||
}
|
}
|
||||||
@@ -1655,11 +1631,11 @@ public final class VideoDetailFragment
|
|||||||
binding.detailUploaderTextView.setVisibility(View.GONE);
|
binding.detailUploaderTextView.setVisibility(View.GONE);
|
||||||
}
|
}
|
||||||
|
|
||||||
PicassoHelper.loadAvatar(info.getSubChannelAvatars()).tag(PICASSO_VIDEO_DETAILS_TAG)
|
CoilHelper.INSTANCE.loadAvatar(binding.detailSubChannelThumbnailView,
|
||||||
.into(binding.detailSubChannelThumbnailView);
|
info.getSubChannelAvatars());
|
||||||
binding.detailSubChannelThumbnailView.setVisibility(View.VISIBLE);
|
binding.detailSubChannelThumbnailView.setVisibility(View.VISIBLE);
|
||||||
PicassoHelper.loadAvatar(info.getUploaderAvatars()).tag(PICASSO_VIDEO_DETAILS_TAG)
|
CoilHelper.INSTANCE.loadAvatar(binding.detailUploaderThumbnailView,
|
||||||
.into(binding.detailUploaderThumbnailView);
|
info.getUploaderAvatars());
|
||||||
binding.detailUploaderThumbnailView.setVisibility(View.VISIBLE);
|
binding.detailUploaderThumbnailView.setVisibility(View.VISIBLE);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1789,16 +1765,14 @@ public final class VideoDetailFragment
|
|||||||
final PlaybackParameters parameters) {
|
final PlaybackParameters parameters) {
|
||||||
setOverlayPlayPauseImage(player != null && player.isPlaying());
|
setOverlayPlayPauseImage(player != null && player.isPlaying());
|
||||||
|
|
||||||
switch (state) {
|
if (state == Player.STATE_PLAYING) {
|
||||||
case Player.STATE_PLAYING:
|
if (binding.positionView.getAlpha() != 1.0f
|
||||||
if (binding.positionView.getAlpha() != 1.0f
|
&& player.getPlayQueue() != null
|
||||||
&& player.getPlayQueue() != null
|
&& player.getPlayQueue().getItem() != null
|
||||||
&& player.getPlayQueue().getItem() != null
|
&& player.getPlayQueue().getItem().getUrl().equals(url)) {
|
||||||
&& player.getPlayQueue().getItem().getUrl().equals(url)) {
|
animate(binding.positionView, true, 100);
|
||||||
animate(binding.positionView, true, 100);
|
animate(binding.detailPositionView, true, 100);
|
||||||
animate(binding.detailPositionView, true, 100);
|
}
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1874,7 +1848,7 @@ public final class VideoDetailFragment
|
|||||||
public void onFullscreenStateChanged(final boolean fullscreen) {
|
public void onFullscreenStateChanged(final boolean fullscreen) {
|
||||||
setupBrightness();
|
setupBrightness();
|
||||||
if (!isPlayerAndPlayerServiceAvailable()
|
if (!isPlayerAndPlayerServiceAvailable()
|
||||||
|| player.UIs().get(MainPlayerUi.class).isEmpty()
|
|| player.UIs().getOpt(MainPlayerUi.class).isEmpty()
|
||||||
|| getRoot().map(View::getParent).isEmpty()) {
|
|| getRoot().map(View::getParent).isEmpty()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1903,7 +1877,7 @@ public final class VideoDetailFragment
|
|||||||
final boolean isLandscape = DeviceUtils.isLandscape(requireContext());
|
final boolean isLandscape = DeviceUtils.isLandscape(requireContext());
|
||||||
if (DeviceUtils.isTablet(activity)
|
if (DeviceUtils.isTablet(activity)
|
||||||
&& (!globalScreenOrientationLocked(activity) || isLandscape)) {
|
&& (!globalScreenOrientationLocked(activity) || isLandscape)) {
|
||||||
player.UIs().get(MainPlayerUi.class).ifPresent(MainPlayerUi::toggleFullscreen);
|
player.UIs().getOpt(MainPlayerUi.class).ifPresent(MainPlayerUi::toggleFullscreen);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2003,7 +1977,7 @@ public final class VideoDetailFragment
|
|||||||
}
|
}
|
||||||
|
|
||||||
private boolean isFullscreen() {
|
private boolean isFullscreen() {
|
||||||
return isPlayerAvailable() && player.UIs().get(VideoPlayerUi.class)
|
return isPlayerAvailable() && player.UIs().getOpt(VideoPlayerUi.class)
|
||||||
.map(VideoPlayerUi::isFullscreen).orElse(false);
|
.map(VideoPlayerUi::isFullscreen).orElse(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2080,7 +2054,7 @@ public final class VideoDetailFragment
|
|||||||
setAutoPlay(true);
|
setAutoPlay(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
player.UIs().get(MainPlayerUi.class).ifPresent(MainPlayerUi::checkLandscape);
|
player.UIs().getOpt(MainPlayerUi.class).ifPresent(MainPlayerUi::checkLandscape);
|
||||||
// Let's give a user time to look at video information page if video is not playing
|
// Let's give a user time to look at video information page if video is not playing
|
||||||
if (globalScreenOrientationLocked(activity) && !player.isPlaying()) {
|
if (globalScreenOrientationLocked(activity) && !player.isPlaying()) {
|
||||||
player.play();
|
player.play();
|
||||||
@@ -2345,7 +2319,7 @@ public final class VideoDetailFragment
|
|||||||
&& player.isPlaying()
|
&& player.isPlaying()
|
||||||
&& !isFullscreen()
|
&& !isFullscreen()
|
||||||
&& !DeviceUtils.isTablet(activity)) {
|
&& !DeviceUtils.isTablet(activity)) {
|
||||||
player.UIs().get(MainPlayerUi.class)
|
player.UIs().getOpt(MainPlayerUi.class)
|
||||||
.ifPresent(MainPlayerUi::toggleFullscreen);
|
.ifPresent(MainPlayerUi::toggleFullscreen);
|
||||||
}
|
}
|
||||||
setOverlayLook(binding.appBarLayout, behavior, 1);
|
setOverlayLook(binding.appBarLayout, behavior, 1);
|
||||||
@@ -2359,7 +2333,7 @@ public final class VideoDetailFragment
|
|||||||
// Re-enable clicks
|
// Re-enable clicks
|
||||||
setOverlayElementsClickable(true);
|
setOverlayElementsClickable(true);
|
||||||
if (isPlayerAvailable()) {
|
if (isPlayerAvailable()) {
|
||||||
player.UIs().get(MainPlayerUi.class)
|
player.UIs().getOpt(MainPlayerUi.class)
|
||||||
.ifPresent(MainPlayerUi::closeItemsList);
|
.ifPresent(MainPlayerUi::closeItemsList);
|
||||||
}
|
}
|
||||||
setOverlayLook(binding.appBarLayout, behavior, 0);
|
setOverlayLook(binding.appBarLayout, behavior, 0);
|
||||||
@@ -2370,7 +2344,7 @@ public final class VideoDetailFragment
|
|||||||
showSystemUi();
|
showSystemUi();
|
||||||
}
|
}
|
||||||
if (isPlayerAvailable()) {
|
if (isPlayerAvailable()) {
|
||||||
player.UIs().get(MainPlayerUi.class).ifPresent(ui -> {
|
player.UIs().getOpt(MainPlayerUi.class).ifPresent(ui -> {
|
||||||
if (ui.isControlsVisible()) {
|
if (ui.isControlsVisible()) {
|
||||||
ui.hideControls(0, 0);
|
ui.hideControls(0, 0);
|
||||||
}
|
}
|
||||||
@@ -2416,8 +2390,7 @@ public final class VideoDetailFragment
|
|||||||
binding.overlayTitleTextView.setText(isEmpty(overlayTitle) ? "" : overlayTitle);
|
binding.overlayTitleTextView.setText(isEmpty(overlayTitle) ? "" : overlayTitle);
|
||||||
binding.overlayChannelTextView.setText(isEmpty(uploader) ? "" : uploader);
|
binding.overlayChannelTextView.setText(isEmpty(uploader) ? "" : uploader);
|
||||||
binding.overlayThumbnail.setImageDrawable(null);
|
binding.overlayThumbnail.setImageDrawable(null);
|
||||||
PicassoHelper.loadDetailsThumbnail(thumbnails).tag(PICASSO_VIDEO_DETAILS_TAG)
|
CoilHelper.INSTANCE.loadDetailsThumbnail(binding.overlayThumbnail, thumbnails);
|
||||||
.into(binding.overlayThumbnail);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setOverlayPlayPauseImage(final boolean playerIsPlaying) {
|
private void setOverlayPlayPauseImage(final boolean playerIsPlaying) {
|
||||||
@@ -2458,8 +2431,8 @@ public final class VideoDetailFragment
|
|||||||
return player != null;
|
return player != null;
|
||||||
}
|
}
|
||||||
|
|
||||||
boolean isPlayerServiceAvailable() {
|
boolean noPlayerServiceAvailable() {
|
||||||
return playerService != null;
|
return playerService == null;
|
||||||
}
|
}
|
||||||
|
|
||||||
boolean isPlayerAndPlayerServiceAvailable() {
|
boolean isPlayerAndPlayerServiceAvailable() {
|
||||||
@@ -2468,7 +2441,7 @@ public final class VideoDetailFragment
|
|||||||
|
|
||||||
public Optional<View> getRoot() {
|
public Optional<View> getRoot() {
|
||||||
return Optional.ofNullable(player)
|
return Optional.ofNullable(player)
|
||||||
.flatMap(player1 -> player1.UIs().get(VideoPlayerUi.class))
|
.flatMap(player1 -> player1.UIs().getOpt(VideoPlayerUi.class))
|
||||||
.map(playerUi -> playerUi.getBinding().getRoot());
|
.map(playerUi -> playerUi.getBinding().getRoot());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import android.graphics.Color;
|
|||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import android.util.TypedValue;
|
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.Menu;
|
import android.view.Menu;
|
||||||
import android.view.MenuInflater;
|
import android.view.MenuInflater;
|
||||||
@@ -45,6 +44,8 @@ import org.schabi.newpipe.fragments.detail.TabAdapter;
|
|||||||
import org.schabi.newpipe.ktx.AnimationType;
|
import org.schabi.newpipe.ktx.AnimationType;
|
||||||
import org.schabi.newpipe.local.feed.notifications.NotificationHelper;
|
import org.schabi.newpipe.local.feed.notifications.NotificationHelper;
|
||||||
import org.schabi.newpipe.local.subscription.SubscriptionManager;
|
import org.schabi.newpipe.local.subscription.SubscriptionManager;
|
||||||
|
import org.schabi.newpipe.ui.emptystate.EmptyStateSpec;
|
||||||
|
import org.schabi.newpipe.ui.emptystate.EmptyStateUtil;
|
||||||
import org.schabi.newpipe.util.ChannelTabHelper;
|
import org.schabi.newpipe.util.ChannelTabHelper;
|
||||||
import org.schabi.newpipe.util.Constants;
|
import org.schabi.newpipe.util.Constants;
|
||||||
import org.schabi.newpipe.util.ExtractorHelper;
|
import org.schabi.newpipe.util.ExtractorHelper;
|
||||||
@@ -53,13 +54,14 @@ import org.schabi.newpipe.util.NavigationHelper;
|
|||||||
import org.schabi.newpipe.util.StateSaver;
|
import org.schabi.newpipe.util.StateSaver;
|
||||||
import org.schabi.newpipe.util.ThemeHelper;
|
import org.schabi.newpipe.util.ThemeHelper;
|
||||||
import org.schabi.newpipe.util.external_communication.ShareUtils;
|
import org.schabi.newpipe.util.external_communication.ShareUtils;
|
||||||
|
import org.schabi.newpipe.util.image.CoilHelper;
|
||||||
import org.schabi.newpipe.util.image.ImageStrategy;
|
import org.schabi.newpipe.util.image.ImageStrategy;
|
||||||
import org.schabi.newpipe.util.image.PicassoHelper;
|
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Queue;
|
import java.util.Queue;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
import coil3.util.CoilUtils;
|
||||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||||
import io.reactivex.rxjava3.core.Observable;
|
import io.reactivex.rxjava3.core.Observable;
|
||||||
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
||||||
@@ -73,7 +75,6 @@ public class ChannelFragment extends BaseStateFragment<ChannelInfo>
|
|||||||
implements StateSaver.WriteRead {
|
implements StateSaver.WriteRead {
|
||||||
|
|
||||||
private static final int BUTTON_DEBOUNCE_INTERVAL = 100;
|
private static final int BUTTON_DEBOUNCE_INTERVAL = 100;
|
||||||
private static final String PICASSO_CHANNEL_TAG = "PICASSO_CHANNEL_TAG";
|
|
||||||
|
|
||||||
@State
|
@State
|
||||||
protected int serviceId = Constants.NO_SERVICE_ID;
|
protected int serviceId = Constants.NO_SERVICE_ID;
|
||||||
@@ -199,6 +200,11 @@ public class ChannelFragment extends BaseStateFragment<ChannelInfo>
|
|||||||
protected void initViews(final View rootView, final Bundle savedInstanceState) {
|
protected void initViews(final View rootView, final Bundle savedInstanceState) {
|
||||||
super.initViews(rootView, savedInstanceState);
|
super.initViews(rootView, savedInstanceState);
|
||||||
|
|
||||||
|
EmptyStateUtil.setEmptyStateComposable(
|
||||||
|
binding.emptyStateView,
|
||||||
|
EmptyStateSpec.Companion.getContentNotSupported()
|
||||||
|
);
|
||||||
|
|
||||||
tabAdapter = new TabAdapter(getChildFragmentManager());
|
tabAdapter = new TabAdapter(getChildFragmentManager());
|
||||||
binding.viewPager.setAdapter(tabAdapter);
|
binding.viewPager.setAdapter(tabAdapter);
|
||||||
binding.tabLayout.setupWithViewPager(binding.viewPager);
|
binding.tabLayout.setupWithViewPager(binding.viewPager);
|
||||||
@@ -583,7 +589,9 @@ public class ChannelFragment extends BaseStateFragment<ChannelInfo>
|
|||||||
@Override
|
@Override
|
||||||
public void showLoading() {
|
public void showLoading() {
|
||||||
super.showLoading();
|
super.showLoading();
|
||||||
PicassoHelper.cancelTag(PICASSO_CHANNEL_TAG);
|
CoilUtils.dispose(binding.channelAvatarView);
|
||||||
|
CoilUtils.dispose(binding.channelBannerImage);
|
||||||
|
CoilUtils.dispose(binding.subChannelAvatarView);
|
||||||
animate(binding.channelSubscribeButton, false, 100);
|
animate(binding.channelSubscribeButton, false, 100);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -594,17 +602,15 @@ public class ChannelFragment extends BaseStateFragment<ChannelInfo>
|
|||||||
setInitialData(result.getServiceId(), result.getOriginalUrl(), result.getName());
|
setInitialData(result.getServiceId(), result.getOriginalUrl(), result.getName());
|
||||||
|
|
||||||
if (ImageStrategy.shouldLoadImages() && !result.getBanners().isEmpty()) {
|
if (ImageStrategy.shouldLoadImages() && !result.getBanners().isEmpty()) {
|
||||||
PicassoHelper.loadBanner(result.getBanners()).tag(PICASSO_CHANNEL_TAG)
|
CoilHelper.INSTANCE.loadBanner(binding.channelBannerImage, result.getBanners());
|
||||||
.into(binding.channelBannerImage);
|
|
||||||
} else {
|
} else {
|
||||||
// do not waste space for the banner, if the user disabled images or there is not one
|
// do not waste space for the banner, if the user disabled images or there is not one
|
||||||
binding.channelBannerImage.setImageDrawable(null);
|
binding.channelBannerImage.setImageDrawable(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
PicassoHelper.loadAvatar(result.getAvatars()).tag(PICASSO_CHANNEL_TAG)
|
CoilHelper.INSTANCE.loadAvatar(binding.channelAvatarView, result.getAvatars());
|
||||||
.into(binding.channelAvatarView);
|
CoilHelper.INSTANCE.loadAvatar(binding.subChannelAvatarView,
|
||||||
PicassoHelper.loadAvatar(result.getParentChannelAvatars()).tag(PICASSO_CHANNEL_TAG)
|
result.getParentChannelAvatars());
|
||||||
.into(binding.subChannelAvatarView);
|
|
||||||
|
|
||||||
binding.channelTitleView.setText(result.getName());
|
binding.channelTitleView.setText(result.getName());
|
||||||
binding.channelSubscriberView.setVisibility(View.VISIBLE);
|
binding.channelSubscriberView.setVisibility(View.VISIBLE);
|
||||||
@@ -652,8 +658,6 @@ public class ChannelFragment extends BaseStateFragment<ChannelInfo>
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.errorContentNotSupported.setVisibility(View.VISIBLE);
|
binding.emptyStateView.setVisibility(View.VISIBLE);
|
||||||
binding.channelKaomoji.setText("(︶︹︺)");
|
|
||||||
binding.channelKaomoji.setTextSize(TypedValue.COMPLEX_UNIT_SP, 45f);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import org.schabi.newpipe.fragments.list.BaseListInfoFragment;
|
|||||||
import org.schabi.newpipe.fragments.list.playlist.PlaylistControlViewHolder;
|
import org.schabi.newpipe.fragments.list.playlist.PlaylistControlViewHolder;
|
||||||
import org.schabi.newpipe.player.playqueue.ChannelTabPlayQueue;
|
import org.schabi.newpipe.player.playqueue.ChannelTabPlayQueue;
|
||||||
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
||||||
|
import org.schabi.newpipe.ui.emptystate.EmptyStateUtil;
|
||||||
import org.schabi.newpipe.util.ChannelTabHelper;
|
import org.schabi.newpipe.util.ChannelTabHelper;
|
||||||
import org.schabi.newpipe.util.ExtractorHelper;
|
import org.schabi.newpipe.util.ExtractorHelper;
|
||||||
import org.schabi.newpipe.util.PlayButtonHelper;
|
import org.schabi.newpipe.util.PlayButtonHelper;
|
||||||
@@ -79,6 +80,12 @@ public class ChannelTabFragment extends BaseListInfoFragment<InfoItem, ChannelTa
|
|||||||
return inflater.inflate(R.layout.fragment_channel_tab, container, false);
|
return inflater.inflate(R.layout.fragment_channel_tab, container, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onViewCreated(@NonNull final View rootView, final Bundle savedInstanceState) {
|
||||||
|
super.onViewCreated(rootView, savedInstanceState);
|
||||||
|
EmptyStateUtil.setEmptyStateComposable(rootView.findViewById(R.id.empty_state_view));
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onDestroyView() {
|
public void onDestroyView() {
|
||||||
super.onDestroyView();
|
super.onDestroyView();
|
||||||
|
|||||||
@@ -1,172 +0,0 @@
|
|||||||
package org.schabi.newpipe.fragments.list.comments;
|
|
||||||
|
|
||||||
import static org.schabi.newpipe.util.ServiceHelper.getServiceById;
|
|
||||||
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.view.LayoutInflater;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.ViewGroup;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import androidx.constraintlayout.widget.ConstraintLayout;
|
|
||||||
import androidx.core.text.HtmlCompat;
|
|
||||||
|
|
||||||
import com.evernote.android.state.State;
|
|
||||||
|
|
||||||
import org.schabi.newpipe.R;
|
|
||||||
import org.schabi.newpipe.databinding.CommentRepliesHeaderBinding;
|
|
||||||
import org.schabi.newpipe.error.UserAction;
|
|
||||||
import org.schabi.newpipe.extractor.ListExtractor;
|
|
||||||
import org.schabi.newpipe.extractor.comments.CommentsInfoItem;
|
|
||||||
import org.schabi.newpipe.fragments.list.BaseListInfoFragment;
|
|
||||||
import org.schabi.newpipe.info_list.ItemViewMode;
|
|
||||||
import org.schabi.newpipe.util.DeviceUtils;
|
|
||||||
import org.schabi.newpipe.util.ExtractorHelper;
|
|
||||||
import org.schabi.newpipe.util.Localization;
|
|
||||||
import org.schabi.newpipe.util.NavigationHelper;
|
|
||||||
import org.schabi.newpipe.util.image.ImageStrategy;
|
|
||||||
import org.schabi.newpipe.util.image.PicassoHelper;
|
|
||||||
import org.schabi.newpipe.util.text.TextLinkifier;
|
|
||||||
import org.schabi.newpipe.util.text.LongPressLinkMovementMethod;
|
|
||||||
|
|
||||||
import java.util.Queue;
|
|
||||||
import java.util.function.Supplier;
|
|
||||||
|
|
||||||
import io.reactivex.rxjava3.core.Single;
|
|
||||||
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
|
||||||
|
|
||||||
public final class CommentRepliesFragment
|
|
||||||
extends BaseListInfoFragment<CommentsInfoItem, CommentRepliesInfo> {
|
|
||||||
|
|
||||||
public static final String TAG = CommentRepliesFragment.class.getSimpleName();
|
|
||||||
|
|
||||||
@State
|
|
||||||
CommentsInfoItem commentsInfoItem; // the comment to show replies of
|
|
||||||
private final CompositeDisposable disposables = new CompositeDisposable();
|
|
||||||
|
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
|
||||||
// Constructors and lifecycle
|
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
|
||||||
|
|
||||||
// only called by the Android framework, after which readFrom is called and restores all data
|
|
||||||
public CommentRepliesFragment() {
|
|
||||||
super(UserAction.REQUESTED_COMMENT_REPLIES);
|
|
||||||
}
|
|
||||||
|
|
||||||
public CommentRepliesFragment(@NonNull final CommentsInfoItem commentsInfoItem) {
|
|
||||||
this();
|
|
||||||
this.commentsInfoItem = commentsInfoItem;
|
|
||||||
// setting "" as title since the title will be properly set right after
|
|
||||||
setInitialData(commentsInfoItem.getServiceId(), commentsInfoItem.getUrl(), "");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
@Override
|
|
||||||
public View onCreateView(@NonNull final LayoutInflater inflater,
|
|
||||||
@Nullable final ViewGroup container,
|
|
||||||
@Nullable final Bundle savedInstanceState) {
|
|
||||||
return inflater.inflate(R.layout.fragment_comments, container, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onDestroyView() {
|
|
||||||
disposables.clear();
|
|
||||||
super.onDestroyView();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected Supplier<View> getListHeaderSupplier() {
|
|
||||||
return () -> {
|
|
||||||
final CommentRepliesHeaderBinding binding = CommentRepliesHeaderBinding
|
|
||||||
.inflate(activity.getLayoutInflater(), itemsList, false);
|
|
||||||
final CommentsInfoItem item = commentsInfoItem;
|
|
||||||
|
|
||||||
// load the author avatar
|
|
||||||
PicassoHelper.loadAvatar(item.getUploaderAvatars()).into(binding.authorAvatar);
|
|
||||||
binding.authorAvatar.setVisibility(ImageStrategy.shouldLoadImages()
|
|
||||||
? View.VISIBLE : View.GONE);
|
|
||||||
|
|
||||||
// setup author name and comment date
|
|
||||||
binding.authorName.setText(item.getUploaderName());
|
|
||||||
binding.uploadDate.setText(Localization.relativeTimeOrTextual(
|
|
||||||
getContext(), item.getUploadDate(), item.getTextualUploadDate()));
|
|
||||||
binding.authorTouchArea.setOnClickListener(
|
|
||||||
v -> NavigationHelper.openCommentAuthorIfPresent(requireActivity(), item));
|
|
||||||
|
|
||||||
// setup like count, hearted and pinned
|
|
||||||
binding.thumbsUpCount.setText(
|
|
||||||
Localization.likeCount(requireContext(), item.getLikeCount()));
|
|
||||||
// for heartImage goneMarginEnd was used, but there is no way to tell ConstraintLayout
|
|
||||||
// not to use a different margin only when both the next two views are gone
|
|
||||||
((ConstraintLayout.LayoutParams) binding.thumbsUpCount.getLayoutParams())
|
|
||||||
.setMarginEnd(DeviceUtils.dpToPx(
|
|
||||||
(item.isHeartedByUploader() || item.isPinned() ? 8 : 16),
|
|
||||||
requireContext()));
|
|
||||||
binding.heartImage.setVisibility(item.isHeartedByUploader() ? View.VISIBLE : View.GONE);
|
|
||||||
binding.pinnedImage.setVisibility(item.isPinned() ? View.VISIBLE : View.GONE);
|
|
||||||
|
|
||||||
// setup comment content
|
|
||||||
TextLinkifier.fromDescription(binding.commentContent, item.getCommentText(),
|
|
||||||
HtmlCompat.FROM_HTML_MODE_LEGACY, getServiceById(item.getServiceId()),
|
|
||||||
item.getUrl(), disposables, null);
|
|
||||||
binding.commentContent.setMovementMethod(LongPressLinkMovementMethod.getInstance());
|
|
||||||
return binding.getRoot();
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
|
||||||
// State saving
|
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void writeTo(final Queue<Object> objectsToSave) {
|
|
||||||
super.writeTo(objectsToSave);
|
|
||||||
objectsToSave.add(commentsInfoItem);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void readFrom(@NonNull final Queue<Object> savedObjects) throws Exception {
|
|
||||||
super.readFrom(savedObjects);
|
|
||||||
commentsInfoItem = (CommentsInfoItem) savedObjects.poll();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
|
||||||
// Data loading
|
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected Single<CommentRepliesInfo> loadResult(final boolean forceLoad) {
|
|
||||||
return Single.fromCallable(() -> new CommentRepliesInfo(commentsInfoItem,
|
|
||||||
// the reply count string will be shown as the activity title
|
|
||||||
Localization.replyCount(requireContext(), commentsInfoItem.getReplyCount())));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected Single<ListExtractor.InfoItemsPage<CommentsInfoItem>> loadMoreItemsLogic() {
|
|
||||||
// commentsInfoItem.getUrl() should contain the url of the original
|
|
||||||
// ListInfo<CommentsInfoItem>, which should be the stream url
|
|
||||||
return ExtractorHelper.getMoreCommentItems(
|
|
||||||
serviceId, commentsInfoItem.getUrl(), currentNextPage);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
|
||||||
// Utils
|
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected ItemViewMode getItemViewMode() {
|
|
||||||
return ItemViewMode.LIST;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return the comment to which the replies are shown
|
|
||||||
*/
|
|
||||||
public CommentsInfoItem getCommentsInfoItem() {
|
|
||||||
return commentsInfoItem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
package org.schabi.newpipe.fragments.list.comments;
|
|
||||||
|
|
||||||
import org.schabi.newpipe.extractor.ListInfo;
|
|
||||||
import org.schabi.newpipe.extractor.comments.CommentsInfoItem;
|
|
||||||
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler;
|
|
||||||
|
|
||||||
import java.util.Collections;
|
|
||||||
|
|
||||||
public final class CommentRepliesInfo extends ListInfo<CommentsInfoItem> {
|
|
||||||
/**
|
|
||||||
* This class is used to wrap the comment replies page into a ListInfo object.
|
|
||||||
*
|
|
||||||
* @param comment the comment from which to get replies
|
|
||||||
* @param name will be shown as the fragment title
|
|
||||||
*/
|
|
||||||
public CommentRepliesInfo(final CommentsInfoItem comment, final String name) {
|
|
||||||
super(comment.getServiceId(),
|
|
||||||
new ListLinkHandler("", "", "", Collections.emptyList(), null), name);
|
|
||||||
setNextPage(comment.getReplies());
|
|
||||||
setRelatedItems(Collections.emptyList()); // since it must be non-null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,123 +0,0 @@
|
|||||||
package org.schabi.newpipe.fragments.list.comments;
|
|
||||||
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.view.LayoutInflater;
|
|
||||||
import android.view.Menu;
|
|
||||||
import android.view.MenuInflater;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.ViewGroup;
|
|
||||||
import android.widget.TextView;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
|
|
||||||
import org.schabi.newpipe.R;
|
|
||||||
import org.schabi.newpipe.error.UserAction;
|
|
||||||
import org.schabi.newpipe.extractor.ListExtractor;
|
|
||||||
import org.schabi.newpipe.extractor.comments.CommentsInfo;
|
|
||||||
import org.schabi.newpipe.extractor.comments.CommentsInfoItem;
|
|
||||||
import org.schabi.newpipe.fragments.list.BaseListInfoFragment;
|
|
||||||
import org.schabi.newpipe.info_list.ItemViewMode;
|
|
||||||
import org.schabi.newpipe.ktx.ViewUtils;
|
|
||||||
import org.schabi.newpipe.util.ExtractorHelper;
|
|
||||||
|
|
||||||
import io.reactivex.rxjava3.core.Single;
|
|
||||||
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
|
||||||
|
|
||||||
public class CommentsFragment extends BaseListInfoFragment<CommentsInfoItem, CommentsInfo> {
|
|
||||||
private final CompositeDisposable disposables = new CompositeDisposable();
|
|
||||||
|
|
||||||
private TextView emptyStateDesc;
|
|
||||||
|
|
||||||
public static CommentsFragment getInstance(final int serviceId, final String url,
|
|
||||||
final String name) {
|
|
||||||
final CommentsFragment instance = new CommentsFragment();
|
|
||||||
instance.setInitialData(serviceId, url, name);
|
|
||||||
return instance;
|
|
||||||
}
|
|
||||||
|
|
||||||
public CommentsFragment() {
|
|
||||||
super(UserAction.REQUESTED_COMMENTS);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void initViews(final View rootView, final Bundle savedInstanceState) {
|
|
||||||
super.initViews(rootView, savedInstanceState);
|
|
||||||
|
|
||||||
emptyStateDesc = rootView.findViewById(R.id.empty_state_desc);
|
|
||||||
}
|
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
|
||||||
// LifeCycle
|
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public View onCreateView(@NonNull final LayoutInflater inflater,
|
|
||||||
@Nullable final ViewGroup container,
|
|
||||||
@Nullable final Bundle savedInstanceState) {
|
|
||||||
return inflater.inflate(R.layout.fragment_comments, container, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onDestroy() {
|
|
||||||
super.onDestroy();
|
|
||||||
disposables.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
|
||||||
// Load and handle
|
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected Single<ListExtractor.InfoItemsPage<CommentsInfoItem>> loadMoreItemsLogic() {
|
|
||||||
return ExtractorHelper.getMoreCommentItems(serviceId, currentInfo, currentNextPage);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected Single<CommentsInfo> loadResult(final boolean forceLoad) {
|
|
||||||
return ExtractorHelper.getCommentsInfo(serviceId, url, forceLoad);
|
|
||||||
}
|
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
|
||||||
// Contract
|
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void handleResult(@NonNull final CommentsInfo result) {
|
|
||||||
super.handleResult(result);
|
|
||||||
|
|
||||||
emptyStateDesc.setText(
|
|
||||||
result.isCommentsDisabled()
|
|
||||||
? R.string.comments_are_disabled
|
|
||||||
: R.string.no_comments);
|
|
||||||
|
|
||||||
ViewUtils.slideUp(requireView(), 120, 150, 0.06f);
|
|
||||||
disposables.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
|
||||||
// Utils
|
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void setTitle(final String title) { }
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onCreateOptionsMenu(@NonNull final Menu menu,
|
|
||||||
@NonNull final MenuInflater inflater) { }
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected ItemViewMode getItemViewMode() {
|
|
||||||
return ItemViewMode.LIST;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean scrollToComment(final CommentsInfoItem comment) {
|
|
||||||
final int position = infoListAdapter.getItemsList().indexOf(comment);
|
|
||||||
if (position < 0) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
itemsList.scrollToPosition(position);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
package org.schabi.newpipe.fragments.list.comments
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.compose.material3.Surface
|
||||||
|
import androidx.core.os.bundleOf
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import androidx.fragment.compose.content
|
||||||
|
import org.schabi.newpipe.ui.components.video.comment.CommentSection
|
||||||
|
import org.schabi.newpipe.ui.theme.AppTheme
|
||||||
|
import org.schabi.newpipe.util.KEY_SERVICE_ID
|
||||||
|
import org.schabi.newpipe.util.KEY_URL
|
||||||
|
|
||||||
|
class CommentsFragment : Fragment() {
|
||||||
|
override fun onCreateView(
|
||||||
|
inflater: LayoutInflater,
|
||||||
|
container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?
|
||||||
|
) = content {
|
||||||
|
AppTheme {
|
||||||
|
Surface {
|
||||||
|
CommentSection()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
@JvmStatic
|
||||||
|
fun getInstance(serviceId: Int, url: String?) = CommentsFragment().apply {
|
||||||
|
arguments = bundleOf(KEY_SERVICE_ID to serviceId, KEY_URL to url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -53,7 +53,7 @@ import org.schabi.newpipe.util.Localization;
|
|||||||
import org.schabi.newpipe.util.NavigationHelper;
|
import org.schabi.newpipe.util.NavigationHelper;
|
||||||
import org.schabi.newpipe.util.PlayButtonHelper;
|
import org.schabi.newpipe.util.PlayButtonHelper;
|
||||||
import org.schabi.newpipe.util.external_communication.ShareUtils;
|
import org.schabi.newpipe.util.external_communication.ShareUtils;
|
||||||
import org.schabi.newpipe.util.image.PicassoHelper;
|
import org.schabi.newpipe.util.image.CoilHelper;
|
||||||
import org.schabi.newpipe.util.text.TextEllipsizer;
|
import org.schabi.newpipe.util.text.TextEllipsizer;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
@@ -62,6 +62,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
|||||||
import java.util.function.Supplier;
|
import java.util.function.Supplier;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import coil3.util.CoilUtils;
|
||||||
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;
|
||||||
import io.reactivex.rxjava3.core.Single;
|
import io.reactivex.rxjava3.core.Single;
|
||||||
@@ -71,8 +72,6 @@ import io.reactivex.rxjava3.disposables.Disposable;
|
|||||||
public class PlaylistFragment extends BaseListInfoFragment<StreamInfoItem, PlaylistInfo>
|
public class PlaylistFragment extends BaseListInfoFragment<StreamInfoItem, PlaylistInfo>
|
||||||
implements PlaylistControlViewHolder {
|
implements PlaylistControlViewHolder {
|
||||||
|
|
||||||
private static final String PICASSO_PLAYLIST_TAG = "PICASSO_PLAYLIST_TAG";
|
|
||||||
|
|
||||||
private CompositeDisposable disposables;
|
private CompositeDisposable disposables;
|
||||||
private Subscription bookmarkReactor;
|
private Subscription bookmarkReactor;
|
||||||
private AtomicBoolean isBookmarkButtonReady;
|
private AtomicBoolean isBookmarkButtonReady;
|
||||||
@@ -276,7 +275,7 @@ public class PlaylistFragment extends BaseListInfoFragment<StreamInfoItem, Playl
|
|||||||
animate(headerBinding.getRoot(), false, 200);
|
animate(headerBinding.getRoot(), false, 200);
|
||||||
animateHideRecyclerViewAllowingScrolling(itemsList);
|
animateHideRecyclerViewAllowingScrolling(itemsList);
|
||||||
|
|
||||||
PicassoHelper.cancelTag(PICASSO_PLAYLIST_TAG);
|
CoilUtils.dispose(headerBinding.uploaderAvatarView);
|
||||||
animate(headerBinding.uploaderLayout, false, 200);
|
animate(headerBinding.uploaderLayout, false, 200);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -327,8 +326,8 @@ public class PlaylistFragment extends BaseListInfoFragment<StreamInfoItem, Playl
|
|||||||
R.drawable.ic_radio)
|
R.drawable.ic_radio)
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
PicassoHelper.loadAvatar(result.getUploaderAvatars()).tag(PICASSO_PLAYLIST_TAG)
|
CoilHelper.INSTANCE.loadAvatar(headerBinding.uploaderAvatarView,
|
||||||
.into(headerBinding.uploaderAvatarView);
|
result.getUploaderAvatars());
|
||||||
}
|
}
|
||||||
|
|
||||||
streamCount = result.getStreamCount();
|
streamCount = result.getStreamCount();
|
||||||
|
|||||||
@@ -64,6 +64,8 @@ import org.schabi.newpipe.ktx.AnimationType;
|
|||||||
import org.schabi.newpipe.ktx.ExceptionUtils;
|
import org.schabi.newpipe.ktx.ExceptionUtils;
|
||||||
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
||||||
import org.schabi.newpipe.settings.NewPipeSettings;
|
import org.schabi.newpipe.settings.NewPipeSettings;
|
||||||
|
import org.schabi.newpipe.ui.emptystate.EmptyStateSpec;
|
||||||
|
import org.schabi.newpipe.ui.emptystate.EmptyStateUtil;
|
||||||
import org.schabi.newpipe.util.Constants;
|
import org.schabi.newpipe.util.Constants;
|
||||||
import org.schabi.newpipe.util.DeviceUtils;
|
import org.schabi.newpipe.util.DeviceUtils;
|
||||||
import org.schabi.newpipe.util.ExtractorHelper;
|
import org.schabi.newpipe.util.ExtractorHelper;
|
||||||
@@ -344,6 +346,10 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
|||||||
protected void initViews(final View rootView, final Bundle savedInstanceState) {
|
protected void initViews(final View rootView, final Bundle savedInstanceState) {
|
||||||
super.initViews(rootView, savedInstanceState);
|
super.initViews(rootView, savedInstanceState);
|
||||||
|
|
||||||
|
EmptyStateUtil.setEmptyStateComposable(
|
||||||
|
searchBinding.emptyStateView,
|
||||||
|
EmptyStateSpec.Companion.getNoSearchResult());
|
||||||
|
|
||||||
searchBinding.suggestionsList.setAdapter(suggestionListAdapter);
|
searchBinding.suggestionsList.setAdapter(suggestionListAdapter);
|
||||||
// animations are just strange and useless, since the suggestions keep changing too much
|
// animations are just strange and useless, since the suggestions keep changing too much
|
||||||
searchBinding.suggestionsList.setItemAnimator(null);
|
searchBinding.suggestionsList.setItemAnimator(null);
|
||||||
|
|||||||
@@ -1,202 +0,0 @@
|
|||||||
package org.schabi.newpipe.fragments.list.videos;
|
|
||||||
|
|
||||||
import android.content.SharedPreferences;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.view.LayoutInflater;
|
|
||||||
import android.view.Menu;
|
|
||||||
import android.view.MenuInflater;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.ViewGroup;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import androidx.fragment.app.Fragment;
|
|
||||||
import androidx.preference.PreferenceManager;
|
|
||||||
|
|
||||||
import org.schabi.newpipe.R;
|
|
||||||
import org.schabi.newpipe.databinding.RelatedItemsHeaderBinding;
|
|
||||||
import org.schabi.newpipe.error.UserAction;
|
|
||||||
import org.schabi.newpipe.extractor.InfoItem;
|
|
||||||
import org.schabi.newpipe.extractor.ListExtractor;
|
|
||||||
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
|
||||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
|
||||||
import org.schabi.newpipe.fragments.list.BaseListInfoFragment;
|
|
||||||
import org.schabi.newpipe.info_list.ItemViewMode;
|
|
||||||
import org.schabi.newpipe.info_list.dialog.InfoItemDialog;
|
|
||||||
import org.schabi.newpipe.ktx.ViewUtils;
|
|
||||||
|
|
||||||
import java.io.Serializable;
|
|
||||||
import java.util.function.Supplier;
|
|
||||||
|
|
||||||
import io.reactivex.rxjava3.core.Single;
|
|
||||||
|
|
||||||
public class RelatedItemsFragment extends BaseListInfoFragment<InfoItem, RelatedItemsInfo>
|
|
||||||
implements SharedPreferences.OnSharedPreferenceChangeListener {
|
|
||||||
private static final String INFO_KEY = "related_info_key";
|
|
||||||
|
|
||||||
private RelatedItemsInfo relatedItemsInfo;
|
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
|
||||||
// Views
|
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
|
||||||
|
|
||||||
private RelatedItemsHeaderBinding headerBinding;
|
|
||||||
|
|
||||||
public static RelatedItemsFragment getInstance(final StreamInfo info) {
|
|
||||||
final RelatedItemsFragment instance = new RelatedItemsFragment();
|
|
||||||
instance.setInitialData(info);
|
|
||||||
return instance;
|
|
||||||
}
|
|
||||||
|
|
||||||
public RelatedItemsFragment() {
|
|
||||||
super(UserAction.REQUESTED_STREAM);
|
|
||||||
}
|
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
|
||||||
// LifeCycle
|
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public View onCreateView(@NonNull final LayoutInflater inflater,
|
|
||||||
@Nullable final ViewGroup container,
|
|
||||||
@Nullable final Bundle savedInstanceState) {
|
|
||||||
return inflater.inflate(R.layout.fragment_related_items, container, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onDestroyView() {
|
|
||||||
headerBinding = null;
|
|
||||||
super.onDestroyView();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected Supplier<View> getListHeaderSupplier() {
|
|
||||||
if (relatedItemsInfo == null || relatedItemsInfo.getRelatedItems() == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
headerBinding = RelatedItemsHeaderBinding
|
|
||||||
.inflate(activity.getLayoutInflater(), itemsList, false);
|
|
||||||
|
|
||||||
final SharedPreferences pref = PreferenceManager
|
|
||||||
.getDefaultSharedPreferences(requireContext());
|
|
||||||
final boolean autoplay = pref.getBoolean(getString(R.string.auto_queue_key), false);
|
|
||||||
headerBinding.autoplaySwitch.setChecked(autoplay);
|
|
||||||
headerBinding.autoplaySwitch.setOnCheckedChangeListener((compoundButton, b) ->
|
|
||||||
PreferenceManager.getDefaultSharedPreferences(requireContext()).edit()
|
|
||||||
.putBoolean(getString(R.string.auto_queue_key), b).apply());
|
|
||||||
|
|
||||||
return headerBinding::getRoot;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected Single<ListExtractor.InfoItemsPage<InfoItem>> loadMoreItemsLogic() {
|
|
||||||
return Single.fromCallable(ListExtractor.InfoItemsPage::emptyPage);
|
|
||||||
}
|
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
|
||||||
// Contract
|
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected Single<RelatedItemsInfo> loadResult(final boolean forceLoad) {
|
|
||||||
return Single.fromCallable(() -> relatedItemsInfo);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void showLoading() {
|
|
||||||
super.showLoading();
|
|
||||||
if (headerBinding != null) {
|
|
||||||
headerBinding.getRoot().setVisibility(View.INVISIBLE);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void handleResult(@NonNull final RelatedItemsInfo result) {
|
|
||||||
super.handleResult(result);
|
|
||||||
|
|
||||||
if (headerBinding != null) {
|
|
||||||
headerBinding.getRoot().setVisibility(View.VISIBLE);
|
|
||||||
}
|
|
||||||
ViewUtils.slideUp(requireView(), 120, 96, 0.06f);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
|
||||||
// Utils
|
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void setTitle(final String title) {
|
|
||||||
// Nothing to do - override parent
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onCreateOptionsMenu(@NonNull final Menu menu,
|
|
||||||
@NonNull final MenuInflater inflater) {
|
|
||||||
// Nothing to do - override parent
|
|
||||||
}
|
|
||||||
|
|
||||||
private void setInitialData(final StreamInfo info) {
|
|
||||||
super.setInitialData(info.getServiceId(), info.getUrl(), info.getName());
|
|
||||||
if (this.relatedItemsInfo == null) {
|
|
||||||
this.relatedItemsInfo = new RelatedItemsInfo(info);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onSaveInstanceState(@NonNull final Bundle outState) {
|
|
||||||
super.onSaveInstanceState(outState);
|
|
||||||
outState.putSerializable(INFO_KEY, relatedItemsInfo);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onRestoreInstanceState(@NonNull final Bundle savedState) {
|
|
||||||
super.onRestoreInstanceState(savedState);
|
|
||||||
final Serializable serializable = savedState.getSerializable(INFO_KEY);
|
|
||||||
if (serializable instanceof RelatedItemsInfo) {
|
|
||||||
this.relatedItemsInfo = (RelatedItemsInfo) serializable;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onSharedPreferenceChanged(final SharedPreferences sharedPreferences,
|
|
||||||
final String key) {
|
|
||||||
if (headerBinding != null && getString(R.string.auto_queue_key).equals(key)) {
|
|
||||||
headerBinding.autoplaySwitch.setChecked(sharedPreferences.getBoolean(key, false));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected ItemViewMode getItemViewMode() {
|
|
||||||
ItemViewMode mode = super.getItemViewMode();
|
|
||||||
// Only list mode is supported. Either List or card will be used.
|
|
||||||
if (mode != ItemViewMode.LIST && mode != ItemViewMode.CARD) {
|
|
||||||
mode = ItemViewMode.LIST;
|
|
||||||
}
|
|
||||||
return mode;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void showInfoItemDialog(final StreamInfoItem item) {
|
|
||||||
// Try and attach the InfoItemDialog to the parent fragment of the RelatedItemsFragment
|
|
||||||
// so that its context is not lost when the RelatedItemsFragment is reinitialized,
|
|
||||||
// e.g. when a new stream is loaded in a parent VideoDetailFragment.
|
|
||||||
final Fragment parentFragment = getParentFragment();
|
|
||||||
if (parentFragment != null) {
|
|
||||||
try {
|
|
||||||
new InfoItemDialog.Builder(
|
|
||||||
parentFragment.getActivity(),
|
|
||||||
parentFragment.getContext(),
|
|
||||||
parentFragment,
|
|
||||||
item
|
|
||||||
).create().show();
|
|
||||||
} catch (final IllegalArgumentException e) {
|
|
||||||
InfoItemDialog.Builder.reportErrorDuringInitialization(e, item);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
super.showInfoItemDialog(item);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
package org.schabi.newpipe.fragments.list.videos
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.compose.material3.Surface
|
||||||
|
import androidx.core.os.bundleOf
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import androidx.fragment.compose.content
|
||||||
|
import org.schabi.newpipe.extractor.stream.StreamInfo
|
||||||
|
import org.schabi.newpipe.ktx.serializable
|
||||||
|
import org.schabi.newpipe.ui.components.video.RelatedItems
|
||||||
|
import org.schabi.newpipe.ui.theme.AppTheme
|
||||||
|
import org.schabi.newpipe.util.KEY_INFO
|
||||||
|
|
||||||
|
class RelatedItemsFragment : Fragment() {
|
||||||
|
override fun onCreateView(
|
||||||
|
inflater: LayoutInflater,
|
||||||
|
container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?
|
||||||
|
) = content {
|
||||||
|
AppTheme {
|
||||||
|
Surface {
|
||||||
|
RelatedItems(requireArguments().serializable<StreamInfo>(KEY_INFO)!!)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
@JvmStatic
|
||||||
|
fun getInstance(info: StreamInfo) = RelatedItemsFragment().apply {
|
||||||
|
arguments = bundleOf(KEY_INFO to info)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
package org.schabi.newpipe.fragments.list.videos;
|
|
||||||
|
|
||||||
import org.schabi.newpipe.extractor.InfoItem;
|
|
||||||
import org.schabi.newpipe.extractor.ListInfo;
|
|
||||||
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler;
|
|
||||||
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Collections;
|
|
||||||
|
|
||||||
public final class RelatedItemsInfo extends ListInfo<InfoItem> {
|
|
||||||
/**
|
|
||||||
* This class is used to wrap the related items of a StreamInfo into a ListInfo object.
|
|
||||||
*
|
|
||||||
* @param info the stream info from which to get related items
|
|
||||||
*/
|
|
||||||
public RelatedItemsInfo(final StreamInfo info) {
|
|
||||||
super(info.getServiceId(), new ListLinkHandler(info.getOriginalUrl(), info.getUrl(),
|
|
||||||
info.getId(), Collections.emptyList(), null), info.getName());
|
|
||||||
setRelatedItems(new ArrayList<>(info.getRelatedItems()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -13,7 +13,6 @@ import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem;
|
|||||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||||
import org.schabi.newpipe.info_list.holder.ChannelInfoItemHolder;
|
import org.schabi.newpipe.info_list.holder.ChannelInfoItemHolder;
|
||||||
import org.schabi.newpipe.info_list.holder.ChannelMiniInfoItemHolder;
|
import org.schabi.newpipe.info_list.holder.ChannelMiniInfoItemHolder;
|
||||||
import org.schabi.newpipe.info_list.holder.CommentInfoItemHolder;
|
|
||||||
import org.schabi.newpipe.info_list.holder.InfoItemHolder;
|
import org.schabi.newpipe.info_list.holder.InfoItemHolder;
|
||||||
import org.schabi.newpipe.info_list.holder.PlaylistInfoItemHolder;
|
import org.schabi.newpipe.info_list.holder.PlaylistInfoItemHolder;
|
||||||
import org.schabi.newpipe.info_list.holder.PlaylistMiniInfoItemHolder;
|
import org.schabi.newpipe.info_list.holder.PlaylistMiniInfoItemHolder;
|
||||||
@@ -75,21 +74,16 @@ public class InfoItemBuilder {
|
|||||||
private InfoItemHolder holderFromInfoType(@NonNull final ViewGroup parent,
|
private InfoItemHolder holderFromInfoType(@NonNull final ViewGroup parent,
|
||||||
@NonNull final InfoItem.InfoType infoType,
|
@NonNull final InfoItem.InfoType infoType,
|
||||||
final boolean useMiniVariant) {
|
final boolean useMiniVariant) {
|
||||||
switch (infoType) {
|
return switch (infoType) {
|
||||||
case STREAM:
|
case STREAM -> useMiniVariant ? new StreamMiniInfoItemHolder(this, parent)
|
||||||
return useMiniVariant ? new StreamMiniInfoItemHolder(this, parent)
|
: new StreamInfoItemHolder(this, parent);
|
||||||
: new StreamInfoItemHolder(this, parent);
|
case CHANNEL -> useMiniVariant ? new ChannelMiniInfoItemHolder(this, parent)
|
||||||
case CHANNEL:
|
: new ChannelInfoItemHolder(this, parent);
|
||||||
return useMiniVariant ? new ChannelMiniInfoItemHolder(this, parent)
|
case PLAYLIST -> useMiniVariant ? new PlaylistMiniInfoItemHolder(this, parent)
|
||||||
: new ChannelInfoItemHolder(this, parent);
|
: new PlaylistInfoItemHolder(this, parent);
|
||||||
case PLAYLIST:
|
case COMMENT ->
|
||||||
return useMiniVariant ? new PlaylistMiniInfoItemHolder(this, parent)
|
throw new IllegalArgumentException("Comments should be rendered using Compose");
|
||||||
: new PlaylistInfoItemHolder(this, parent);
|
};
|
||||||
case COMMENT:
|
|
||||||
return new CommentInfoItemHolder(this, parent);
|
|
||||||
default:
|
|
||||||
throw new RuntimeException("InfoType not expected = " + infoType.name());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public Context getContext() {
|
public Context getContext() {
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ import org.schabi.newpipe.info_list.holder.ChannelCardInfoItemHolder;
|
|||||||
import org.schabi.newpipe.info_list.holder.ChannelGridInfoItemHolder;
|
import org.schabi.newpipe.info_list.holder.ChannelGridInfoItemHolder;
|
||||||
import org.schabi.newpipe.info_list.holder.ChannelInfoItemHolder;
|
import org.schabi.newpipe.info_list.holder.ChannelInfoItemHolder;
|
||||||
import org.schabi.newpipe.info_list.holder.ChannelMiniInfoItemHolder;
|
import org.schabi.newpipe.info_list.holder.ChannelMiniInfoItemHolder;
|
||||||
import org.schabi.newpipe.info_list.holder.CommentInfoItemHolder;
|
|
||||||
import org.schabi.newpipe.info_list.holder.InfoItemHolder;
|
import org.schabi.newpipe.info_list.holder.InfoItemHolder;
|
||||||
import org.schabi.newpipe.info_list.holder.PlaylistCardInfoItemHolder;
|
import org.schabi.newpipe.info_list.holder.PlaylistCardInfoItemHolder;
|
||||||
import org.schabi.newpipe.info_list.holder.PlaylistGridInfoItemHolder;
|
import org.schabi.newpipe.info_list.holder.PlaylistGridInfoItemHolder;
|
||||||
@@ -283,46 +282,32 @@ public class InfoListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolde
|
|||||||
Log.d(TAG, "onCreateViewHolder() called with: "
|
Log.d(TAG, "onCreateViewHolder() called with: "
|
||||||
+ "parent = [" + parent + "], type = [" + type + "]");
|
+ "parent = [" + parent + "], type = [" + type + "]");
|
||||||
}
|
}
|
||||||
switch (type) {
|
return switch (type) {
|
||||||
// #4475 and #3368
|
// #4475 and #3368
|
||||||
// Always create a new instance otherwise the same instance
|
// Always create a new instance otherwise the same instance
|
||||||
// is sometimes reused which causes a crash
|
// is sometimes reused which causes a crash
|
||||||
case HEADER_TYPE:
|
case HEADER_TYPE -> new HFHolder(headerSupplier.get());
|
||||||
return new HFHolder(headerSupplier.get());
|
case FOOTER_TYPE -> new HFHolder(PignateFooterBinding
|
||||||
case FOOTER_TYPE:
|
.inflate(layoutInflater, parent, false)
|
||||||
return new HFHolder(PignateFooterBinding
|
.getRoot()
|
||||||
.inflate(layoutInflater, parent, false)
|
);
|
||||||
.getRoot()
|
case MINI_STREAM_HOLDER_TYPE -> new StreamMiniInfoItemHolder(infoItemBuilder, parent);
|
||||||
);
|
case STREAM_HOLDER_TYPE -> new StreamInfoItemHolder(infoItemBuilder, parent);
|
||||||
case MINI_STREAM_HOLDER_TYPE:
|
case GRID_STREAM_HOLDER_TYPE -> new StreamGridInfoItemHolder(infoItemBuilder, parent);
|
||||||
return new StreamMiniInfoItemHolder(infoItemBuilder, parent);
|
case CARD_STREAM_HOLDER_TYPE -> new StreamCardInfoItemHolder(infoItemBuilder, parent);
|
||||||
case STREAM_HOLDER_TYPE:
|
case MINI_CHANNEL_HOLDER_TYPE -> new ChannelMiniInfoItemHolder(infoItemBuilder, parent);
|
||||||
return new StreamInfoItemHolder(infoItemBuilder, parent);
|
case CHANNEL_HOLDER_TYPE -> new ChannelInfoItemHolder(infoItemBuilder, parent);
|
||||||
case GRID_STREAM_HOLDER_TYPE:
|
case CARD_CHANNEL_HOLDER_TYPE -> new ChannelCardInfoItemHolder(infoItemBuilder, parent);
|
||||||
return new StreamGridInfoItemHolder(infoItemBuilder, parent);
|
case GRID_CHANNEL_HOLDER_TYPE -> new ChannelGridInfoItemHolder(infoItemBuilder, parent);
|
||||||
case CARD_STREAM_HOLDER_TYPE:
|
case MINI_PLAYLIST_HOLDER_TYPE ->
|
||||||
return new StreamCardInfoItemHolder(infoItemBuilder, parent);
|
new PlaylistMiniInfoItemHolder(infoItemBuilder, parent);
|
||||||
case MINI_CHANNEL_HOLDER_TYPE:
|
case PLAYLIST_HOLDER_TYPE -> new PlaylistInfoItemHolder(infoItemBuilder, parent);
|
||||||
return new ChannelMiniInfoItemHolder(infoItemBuilder, parent);
|
case GRID_PLAYLIST_HOLDER_TYPE ->
|
||||||
case CHANNEL_HOLDER_TYPE:
|
new PlaylistGridInfoItemHolder(infoItemBuilder, parent);
|
||||||
return new ChannelInfoItemHolder(infoItemBuilder, parent);
|
case CARD_PLAYLIST_HOLDER_TYPE ->
|
||||||
case CARD_CHANNEL_HOLDER_TYPE:
|
new PlaylistCardInfoItemHolder(infoItemBuilder, parent);
|
||||||
return new ChannelCardInfoItemHolder(infoItemBuilder, parent);
|
default -> new FallbackViewHolder(new View(parent.getContext()));
|
||||||
case GRID_CHANNEL_HOLDER_TYPE:
|
};
|
||||||
return new ChannelGridInfoItemHolder(infoItemBuilder, parent);
|
|
||||||
case MINI_PLAYLIST_HOLDER_TYPE:
|
|
||||||
return new PlaylistMiniInfoItemHolder(infoItemBuilder, parent);
|
|
||||||
case PLAYLIST_HOLDER_TYPE:
|
|
||||||
return new PlaylistInfoItemHolder(infoItemBuilder, parent);
|
|
||||||
case GRID_PLAYLIST_HOLDER_TYPE:
|
|
||||||
return new PlaylistGridInfoItemHolder(infoItemBuilder, parent);
|
|
||||||
case CARD_PLAYLIST_HOLDER_TYPE:
|
|
||||||
return new PlaylistCardInfoItemHolder(infoItemBuilder, parent);
|
|
||||||
case COMMENT_HOLDER_TYPE:
|
|
||||||
return new CommentInfoItemHolder(infoItemBuilder, parent);
|
|
||||||
default:
|
|
||||||
return new FallbackViewHolder(new View(parent.getContext()));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -1,19 +1,18 @@
|
|||||||
package org.schabi.newpipe.info_list
|
package org.schabi.newpipe.info_list
|
||||||
|
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.ImageView
|
import com.xwray.groupie.viewbinding.BindableItem
|
||||||
import android.widget.TextView
|
import com.xwray.groupie.viewbinding.GroupieViewHolder
|
||||||
import com.xwray.groupie.GroupieViewHolder
|
|
||||||
import com.xwray.groupie.Item
|
|
||||||
import org.schabi.newpipe.R
|
import org.schabi.newpipe.R
|
||||||
|
import org.schabi.newpipe.databinding.ItemStreamSegmentBinding
|
||||||
import org.schabi.newpipe.extractor.stream.StreamSegment
|
import org.schabi.newpipe.extractor.stream.StreamSegment
|
||||||
import org.schabi.newpipe.util.Localization
|
import org.schabi.newpipe.util.Localization
|
||||||
import org.schabi.newpipe.util.image.PicassoHelper
|
import org.schabi.newpipe.util.image.CoilHelper
|
||||||
|
|
||||||
class StreamSegmentItem(
|
class StreamSegmentItem(
|
||||||
private val item: StreamSegment,
|
private val item: StreamSegment,
|
||||||
private val onClick: StreamSegmentAdapter.StreamSegmentListener
|
private val onClick: StreamSegmentAdapter.StreamSegmentListener
|
||||||
) : Item<GroupieViewHolder>() {
|
) : BindableItem<ItemStreamSegmentBinding>() {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val PAYLOAD_SELECT = 1
|
const val PAYLOAD_SELECT = 1
|
||||||
@@ -21,31 +20,32 @@ class StreamSegmentItem(
|
|||||||
|
|
||||||
var isSelected = false
|
var isSelected = false
|
||||||
|
|
||||||
override fun bind(viewHolder: GroupieViewHolder, position: Int) {
|
override fun bind(viewBinding: ItemStreamSegmentBinding, position: Int) {
|
||||||
item.previewUrl?.let {
|
CoilHelper.loadThumbnail(viewBinding.previewImage, item.previewUrl)
|
||||||
PicassoHelper.loadThumbnail(it)
|
viewBinding.textViewTitle.text = item.title
|
||||||
.into(viewHolder.root.findViewById<ImageView>(R.id.previewImage))
|
|
||||||
}
|
|
||||||
viewHolder.root.findViewById<TextView>(R.id.textViewTitle).text = item.title
|
|
||||||
if (item.channelName == null) {
|
if (item.channelName == null) {
|
||||||
viewHolder.root.findViewById<TextView>(R.id.textViewChannel).visibility = View.GONE
|
viewBinding.textViewChannel.visibility = View.GONE
|
||||||
// When the channel name is displayed there is less space
|
// When the channel name is displayed there is less space
|
||||||
// and thus the segment title needs to be only one line height.
|
// and thus the segment title needs to be only one line height.
|
||||||
// But when there is no channel name displayed, the title can be two lines long.
|
// But when there is no channel name displayed, the title can be two lines long.
|
||||||
// The default maxLines value is set to 1 to display all elements in the AS preview,
|
// The default maxLines value is set to 1 to display all elements in the AS preview,
|
||||||
viewHolder.root.findViewById<TextView>(R.id.textViewTitle).maxLines = 2
|
viewBinding.textViewTitle.maxLines = 2
|
||||||
} else {
|
} else {
|
||||||
viewHolder.root.findViewById<TextView>(R.id.textViewChannel).text = item.channelName
|
viewBinding.textViewChannel.text = item.channelName
|
||||||
viewHolder.root.findViewById<TextView>(R.id.textViewChannel).visibility = View.VISIBLE
|
viewBinding.textViewChannel.visibility = View.VISIBLE
|
||||||
}
|
}
|
||||||
viewHolder.root.findViewById<TextView>(R.id.textViewStartSeconds).text =
|
viewBinding.textViewStartSeconds.text =
|
||||||
Localization.getDurationString(item.startTimeSeconds.toLong())
|
Localization.getDurationString(item.startTimeSeconds.toLong())
|
||||||
viewHolder.root.setOnClickListener { onClick.onItemClick(this, item.startTimeSeconds) }
|
viewBinding.root.setOnClickListener { onClick.onItemClick(this, item.startTimeSeconds) }
|
||||||
viewHolder.root.setOnLongClickListener { onClick.onItemLongClick(this, item.startTimeSeconds); true }
|
viewBinding.root.setOnLongClickListener { onClick.onItemLongClick(this, item.startTimeSeconds); true }
|
||||||
viewHolder.root.isSelected = isSelected
|
viewBinding.root.isSelected = isSelected
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun bind(viewHolder: GroupieViewHolder, position: Int, payloads: MutableList<Any>) {
|
override fun bind(
|
||||||
|
viewHolder: GroupieViewHolder<ItemStreamSegmentBinding>,
|
||||||
|
position: Int,
|
||||||
|
payloads: MutableList<Any>
|
||||||
|
) {
|
||||||
if (payloads.contains(PAYLOAD_SELECT)) {
|
if (payloads.contains(PAYLOAD_SELECT)) {
|
||||||
viewHolder.root.isSelected = isSelected
|
viewHolder.root.isSelected = isSelected
|
||||||
return
|
return
|
||||||
@@ -54,4 +54,6 @@ class StreamSegmentItem(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun getLayout() = R.layout.item_stream_segment
|
override fun getLayout() = R.layout.item_stream_segment
|
||||||
|
|
||||||
|
override fun initializeViewBinding(view: View) = ItemStreamSegmentBinding.bind(view)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -346,7 +346,7 @@ public final class InfoItemDialog {
|
|||||||
|
|
||||||
public static void reportErrorDuringInitialization(final Throwable throwable,
|
public static void reportErrorDuringInitialization(final Throwable throwable,
|
||||||
final InfoItem item) {
|
final InfoItem item) {
|
||||||
ErrorUtil.showSnackbar(App.getApp().getBaseContext(), new ErrorInfo(
|
ErrorUtil.showSnackbar(App.getInstance().getBaseContext(), new ErrorInfo(
|
||||||
throwable,
|
throwable,
|
||||||
UserAction.OPEN_INFO_ITEM_DIALOG,
|
UserAction.OPEN_INFO_ITEM_DIALOG,
|
||||||
"none",
|
"none",
|
||||||
|
|||||||
@@ -41,10 +41,11 @@ import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
|||||||
* </p>
|
* </p>
|
||||||
*/
|
*/
|
||||||
public enum StreamDialogDefaultEntry {
|
public enum StreamDialogDefaultEntry {
|
||||||
SHOW_CHANNEL_DETAILS(R.string.show_channel_details, (fragment, item) ->
|
SHOW_CHANNEL_DETAILS(R.string.show_channel_details, (fragment, item) -> {
|
||||||
fetchUploaderUrlIfSparse(fragment.requireContext(), item.getServiceId(), item.getUrl(),
|
final var activity = fragment.requireActivity();
|
||||||
item.getUploaderUrl(), url -> openChannelFragment(fragment, item, url))
|
fetchUploaderUrlIfSparse(activity, item.getServiceId(), item.getUrl(),
|
||||||
),
|
item.getUploaderUrl(), url -> openChannelFragment(activity, item, url));
|
||||||
|
}),
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Enqueues the stream automatically to the current PlayerType.
|
* Enqueues the stream automatically to the current PlayerType.
|
||||||
|
|||||||
@@ -13,8 +13,8 @@ import org.schabi.newpipe.extractor.channel.ChannelInfoItem;
|
|||||||
import org.schabi.newpipe.extractor.utils.Utils;
|
import org.schabi.newpipe.extractor.utils.Utils;
|
||||||
import org.schabi.newpipe.info_list.InfoItemBuilder;
|
import org.schabi.newpipe.info_list.InfoItemBuilder;
|
||||||
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
||||||
import org.schabi.newpipe.util.image.PicassoHelper;
|
|
||||||
import org.schabi.newpipe.util.Localization;
|
import org.schabi.newpipe.util.Localization;
|
||||||
|
import org.schabi.newpipe.util.image.CoilHelper;
|
||||||
|
|
||||||
public class ChannelMiniInfoItemHolder extends InfoItemHolder {
|
public class ChannelMiniInfoItemHolder extends InfoItemHolder {
|
||||||
private final ImageView itemThumbnailView;
|
private final ImageView itemThumbnailView;
|
||||||
@@ -56,7 +56,7 @@ public class ChannelMiniInfoItemHolder extends InfoItemHolder {
|
|||||||
itemAdditionalDetailView.setText(getDetailLine(item));
|
itemAdditionalDetailView.setText(getDetailLine(item));
|
||||||
}
|
}
|
||||||
|
|
||||||
PicassoHelper.loadAvatar(item.getThumbnails()).into(itemThumbnailView);
|
CoilHelper.INSTANCE.loadAvatar(itemThumbnailView, item.getThumbnails());
|
||||||
|
|
||||||
itemView.setOnClickListener(view -> {
|
itemView.setOnClickListener(view -> {
|
||||||
if (itemBuilder.getOnChannelSelectedListener() != null) {
|
if (itemBuilder.getOnChannelSelectedListener() != null) {
|
||||||
|
|||||||
@@ -1,212 +0,0 @@
|
|||||||
package org.schabi.newpipe.info_list.holder;
|
|
||||||
|
|
||||||
import static org.schabi.newpipe.util.ServiceHelper.getServiceById;
|
|
||||||
import static org.schabi.newpipe.util.text.TouchUtils.getOffsetForHorizontalLine;
|
|
||||||
|
|
||||||
import android.text.Spanned;
|
|
||||||
import android.text.method.LinkMovementMethod;
|
|
||||||
import android.text.style.ClickableSpan;
|
|
||||||
import android.text.style.URLSpan;
|
|
||||||
import android.view.MotionEvent;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.ViewGroup;
|
|
||||||
import android.widget.Button;
|
|
||||||
import android.widget.ImageView;
|
|
||||||
import android.widget.RelativeLayout;
|
|
||||||
import android.widget.TextView;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.fragment.app.FragmentActivity;
|
|
||||||
|
|
||||||
import org.schabi.newpipe.R;
|
|
||||||
import org.schabi.newpipe.extractor.InfoItem;
|
|
||||||
import org.schabi.newpipe.extractor.comments.CommentsInfoItem;
|
|
||||||
import org.schabi.newpipe.info_list.InfoItemBuilder;
|
|
||||||
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
|
||||||
import org.schabi.newpipe.util.DeviceUtils;
|
|
||||||
import org.schabi.newpipe.util.Localization;
|
|
||||||
import org.schabi.newpipe.util.NavigationHelper;
|
|
||||||
import org.schabi.newpipe.util.external_communication.ShareUtils;
|
|
||||||
import org.schabi.newpipe.util.image.ImageStrategy;
|
|
||||||
import org.schabi.newpipe.util.image.PicassoHelper;
|
|
||||||
import org.schabi.newpipe.util.text.TextEllipsizer;
|
|
||||||
|
|
||||||
public class CommentInfoItemHolder extends InfoItemHolder {
|
|
||||||
|
|
||||||
private static final int COMMENT_DEFAULT_LINES = 2;
|
|
||||||
private final int commentHorizontalPadding;
|
|
||||||
private final int commentVerticalPadding;
|
|
||||||
|
|
||||||
private final RelativeLayout itemRoot;
|
|
||||||
private final ImageView itemThumbnailView;
|
|
||||||
private final TextView itemContentView;
|
|
||||||
private final ImageView itemThumbsUpView;
|
|
||||||
private final TextView itemLikesCountView;
|
|
||||||
private final TextView itemTitleView;
|
|
||||||
private final ImageView itemHeartView;
|
|
||||||
private final ImageView itemPinnedView;
|
|
||||||
private final Button repliesButton;
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
private final TextEllipsizer textEllipsizer;
|
|
||||||
|
|
||||||
public CommentInfoItemHolder(final InfoItemBuilder infoItemBuilder,
|
|
||||||
final ViewGroup parent) {
|
|
||||||
super(infoItemBuilder, R.layout.list_comment_item, parent);
|
|
||||||
|
|
||||||
itemRoot = itemView.findViewById(R.id.itemRoot);
|
|
||||||
itemThumbnailView = itemView.findViewById(R.id.itemThumbnailView);
|
|
||||||
itemContentView = itemView.findViewById(R.id.itemCommentContentView);
|
|
||||||
itemThumbsUpView = itemView.findViewById(R.id.detail_thumbs_up_img_view);
|
|
||||||
itemLikesCountView = itemView.findViewById(R.id.detail_thumbs_up_count_view);
|
|
||||||
itemTitleView = itemView.findViewById(R.id.itemTitleView);
|
|
||||||
itemHeartView = itemView.findViewById(R.id.detail_heart_image_view);
|
|
||||||
itemPinnedView = itemView.findViewById(R.id.detail_pinned_view);
|
|
||||||
repliesButton = itemView.findViewById(R.id.replies_button);
|
|
||||||
|
|
||||||
commentHorizontalPadding = (int) infoItemBuilder.getContext()
|
|
||||||
.getResources().getDimension(R.dimen.comments_horizontal_padding);
|
|
||||||
commentVerticalPadding = (int) infoItemBuilder.getContext()
|
|
||||||
.getResources().getDimension(R.dimen.comments_vertical_padding);
|
|
||||||
|
|
||||||
textEllipsizer = new TextEllipsizer(itemContentView, COMMENT_DEFAULT_LINES, null);
|
|
||||||
textEllipsizer.setStateChangeListener(isEllipsized -> {
|
|
||||||
if (Boolean.TRUE.equals(isEllipsized)) {
|
|
||||||
denyLinkFocus();
|
|
||||||
} else {
|
|
||||||
determineMovementMethod();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void updateFromItem(final InfoItem infoItem,
|
|
||||||
final HistoryRecordManager historyRecordManager) {
|
|
||||||
if (!(infoItem instanceof CommentsInfoItem)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
final CommentsInfoItem item = (CommentsInfoItem) infoItem;
|
|
||||||
|
|
||||||
|
|
||||||
// load the author avatar
|
|
||||||
PicassoHelper.loadAvatar(item.getUploaderAvatars()).into(itemThumbnailView);
|
|
||||||
if (ImageStrategy.shouldLoadImages()) {
|
|
||||||
itemThumbnailView.setVisibility(View.VISIBLE);
|
|
||||||
itemRoot.setPadding(commentVerticalPadding, commentVerticalPadding,
|
|
||||||
commentVerticalPadding, commentVerticalPadding);
|
|
||||||
} else {
|
|
||||||
itemThumbnailView.setVisibility(View.GONE);
|
|
||||||
itemRoot.setPadding(commentHorizontalPadding, commentVerticalPadding,
|
|
||||||
commentHorizontalPadding, commentVerticalPadding);
|
|
||||||
}
|
|
||||||
itemThumbnailView.setOnClickListener(view -> openCommentAuthor(item));
|
|
||||||
|
|
||||||
// setup the top row, with pinned icon, author name and comment date
|
|
||||||
itemPinnedView.setVisibility(item.isPinned() ? View.VISIBLE : View.GONE);
|
|
||||||
final String uploaderName = Localization.localizeUserName(item.getUploaderName());
|
|
||||||
itemTitleView.setText(Localization.concatenateStrings(
|
|
||||||
uploaderName,
|
|
||||||
Localization.relativeTimeOrTextual(
|
|
||||||
itemBuilder.getContext(),
|
|
||||||
item.getUploadDate(),
|
|
||||||
item.getTextualUploadDate())));
|
|
||||||
|
|
||||||
// setup bottom row, with likes, heart and replies button
|
|
||||||
itemLikesCountView.setText(
|
|
||||||
Localization.likeCount(itemBuilder.getContext(), item.getLikeCount()));
|
|
||||||
|
|
||||||
itemHeartView.setVisibility(item.isHeartedByUploader() ? View.VISIBLE : View.GONE);
|
|
||||||
|
|
||||||
final boolean hasReplies = item.getReplies() != null;
|
|
||||||
repliesButton.setOnClickListener(hasReplies ? v -> openCommentReplies(item) : null);
|
|
||||||
repliesButton.setVisibility(hasReplies ? View.VISIBLE : View.GONE);
|
|
||||||
repliesButton.setText(hasReplies
|
|
||||||
? Localization.replyCount(itemBuilder.getContext(), item.getReplyCount()) : "");
|
|
||||||
((RelativeLayout.LayoutParams) itemThumbsUpView.getLayoutParams()).topMargin =
|
|
||||||
hasReplies ? 0 : DeviceUtils.dpToPx(6, itemBuilder.getContext());
|
|
||||||
|
|
||||||
|
|
||||||
// setup comment content and click listeners to expand/ellipsize it
|
|
||||||
textEllipsizer.setStreamingService(getServiceById(item.getServiceId()));
|
|
||||||
textEllipsizer.setStreamUrl(item.getUrl());
|
|
||||||
textEllipsizer.setContent(item.getCommentText());
|
|
||||||
textEllipsizer.ellipsize();
|
|
||||||
|
|
||||||
//noinspection ClickableViewAccessibility
|
|
||||||
itemContentView.setOnTouchListener((v, event) -> {
|
|
||||||
final CharSequence text = itemContentView.getText();
|
|
||||||
if (text instanceof Spanned buffer) {
|
|
||||||
final int action = event.getAction();
|
|
||||||
|
|
||||||
if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_DOWN) {
|
|
||||||
final int offset = getOffsetForHorizontalLine(itemContentView, event);
|
|
||||||
final var links = buffer.getSpans(offset, offset, ClickableSpan.class);
|
|
||||||
|
|
||||||
if (links.length != 0) {
|
|
||||||
if (action == MotionEvent.ACTION_UP) {
|
|
||||||
links[0].onClick(itemContentView);
|
|
||||||
}
|
|
||||||
// we handle events that intersect links, so return true
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
|
|
||||||
itemView.setOnClickListener(view -> {
|
|
||||||
textEllipsizer.toggle();
|
|
||||||
if (itemBuilder.getOnCommentsSelectedListener() != null) {
|
|
||||||
itemBuilder.getOnCommentsSelectedListener().selected(item);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
itemView.setOnLongClickListener(view -> {
|
|
||||||
if (DeviceUtils.isTv(itemBuilder.getContext())) {
|
|
||||||
openCommentAuthor(item);
|
|
||||||
} else {
|
|
||||||
final CharSequence text = itemContentView.getText();
|
|
||||||
if (text != null) {
|
|
||||||
ShareUtils.copyToClipboard(itemBuilder.getContext(), text.toString());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private void openCommentAuthor(@NonNull final CommentsInfoItem item) {
|
|
||||||
NavigationHelper.openCommentAuthorIfPresent((FragmentActivity) itemBuilder.getContext(),
|
|
||||||
item);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void openCommentReplies(@NonNull final CommentsInfoItem item) {
|
|
||||||
NavigationHelper.openCommentRepliesFragment((FragmentActivity) itemBuilder.getContext(),
|
|
||||||
item);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void allowLinkFocus() {
|
|
||||||
itemContentView.setMovementMethod(LinkMovementMethod.getInstance());
|
|
||||||
}
|
|
||||||
|
|
||||||
private void denyLinkFocus() {
|
|
||||||
itemContentView.setMovementMethod(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean shouldFocusLinks() {
|
|
||||||
if (itemView.isInTouchMode()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
final URLSpan[] urls = itemContentView.getUrls();
|
|
||||||
|
|
||||||
return urls != null && urls.length != 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void determineMovementMethod() {
|
|
||||||
if (shouldFocusLinks()) {
|
|
||||||
allowLinkFocus();
|
|
||||||
} else {
|
|
||||||
denyLinkFocus();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -9,8 +9,8 @@ import org.schabi.newpipe.extractor.InfoItem;
|
|||||||
import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem;
|
import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem;
|
||||||
import org.schabi.newpipe.info_list.InfoItemBuilder;
|
import org.schabi.newpipe.info_list.InfoItemBuilder;
|
||||||
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
||||||
import org.schabi.newpipe.util.image.PicassoHelper;
|
|
||||||
import org.schabi.newpipe.util.Localization;
|
import org.schabi.newpipe.util.Localization;
|
||||||
|
import org.schabi.newpipe.util.image.CoilHelper;
|
||||||
|
|
||||||
public class PlaylistMiniInfoItemHolder extends InfoItemHolder {
|
public class PlaylistMiniInfoItemHolder extends InfoItemHolder {
|
||||||
public final ImageView itemThumbnailView;
|
public final ImageView itemThumbnailView;
|
||||||
@@ -46,7 +46,7 @@ public class PlaylistMiniInfoItemHolder extends InfoItemHolder {
|
|||||||
.localizeStreamCountMini(itemStreamCountView.getContext(), item.getStreamCount()));
|
.localizeStreamCountMini(itemStreamCountView.getContext(), item.getStreamCount()));
|
||||||
itemUploaderView.setText(item.getUploaderName());
|
itemUploaderView.setText(item.getUploaderName());
|
||||||
|
|
||||||
PicassoHelper.loadPlaylistThumbnail(item.getThumbnails()).into(itemThumbnailView);
|
CoilHelper.INSTANCE.loadPlaylistThumbnail(itemThumbnailView, item.getThumbnails());
|
||||||
|
|
||||||
itemView.setOnClickListener(view -> {
|
itemView.setOnClickListener(view -> {
|
||||||
if (itemBuilder.getOnPlaylistSelectedListener() != null) {
|
if (itemBuilder.getOnPlaylistSelectedListener() != null) {
|
||||||
|
|||||||
@@ -16,8 +16,8 @@ import org.schabi.newpipe.ktx.ViewUtils;
|
|||||||
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
||||||
import org.schabi.newpipe.util.DependentPreferenceHelper;
|
import org.schabi.newpipe.util.DependentPreferenceHelper;
|
||||||
import org.schabi.newpipe.util.Localization;
|
import org.schabi.newpipe.util.Localization;
|
||||||
import org.schabi.newpipe.util.image.PicassoHelper;
|
|
||||||
import org.schabi.newpipe.util.StreamTypeUtil;
|
import org.schabi.newpipe.util.StreamTypeUtil;
|
||||||
|
import org.schabi.newpipe.util.image.CoilHelper;
|
||||||
import org.schabi.newpipe.views.AnimatedProgressBar;
|
import org.schabi.newpipe.views.AnimatedProgressBar;
|
||||||
|
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
@@ -64,8 +64,7 @@ public class StreamMiniInfoItemHolder extends InfoItemHolder {
|
|||||||
StreamStateEntity state2 = null;
|
StreamStateEntity state2 = null;
|
||||||
if (DependentPreferenceHelper
|
if (DependentPreferenceHelper
|
||||||
.getPositionsInListsEnabled(itemProgressView.getContext())) {
|
.getPositionsInListsEnabled(itemProgressView.getContext())) {
|
||||||
state2 = historyRecordManager.loadStreamState(infoItem)
|
state2 = historyRecordManager.loadStreamState(infoItem).blockingGet();
|
||||||
.blockingGet()[0];
|
|
||||||
}
|
}
|
||||||
if (state2 != null) {
|
if (state2 != null) {
|
||||||
itemProgressView.setVisibility(View.VISIBLE);
|
itemProgressView.setVisibility(View.VISIBLE);
|
||||||
@@ -87,7 +86,7 @@ public class StreamMiniInfoItemHolder extends InfoItemHolder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Default thumbnail is shown on error, while loading and if the url is empty
|
// Default thumbnail is shown on error, while loading and if the url is empty
|
||||||
PicassoHelper.loadThumbnail(item.getThumbnails()).into(itemThumbnailView);
|
CoilHelper.INSTANCE.loadThumbnail(itemThumbnailView, item.getThumbnails());
|
||||||
|
|
||||||
itemView.setOnClickListener(view -> {
|
itemView.setOnClickListener(view -> {
|
||||||
if (itemBuilder.getOnStreamSelectedListener() != null) {
|
if (itemBuilder.getOnStreamSelectedListener() != null) {
|
||||||
@@ -120,7 +119,7 @@ public class StreamMiniInfoItemHolder extends InfoItemHolder {
|
|||||||
if (DependentPreferenceHelper.getPositionsInListsEnabled(itemProgressView.getContext())) {
|
if (DependentPreferenceHelper.getPositionsInListsEnabled(itemProgressView.getContext())) {
|
||||||
state = historyRecordManager
|
state = historyRecordManager
|
||||||
.loadStreamState(infoItem)
|
.loadStreamState(infoItem)
|
||||||
.blockingGet()[0];
|
.blockingGet();
|
||||||
}
|
}
|
||||||
if (state != null && item.getDuration() > 0
|
if (state != null && item.getDuration() > 0
|
||||||
&& !StreamTypeUtil.isLiveStream(item.getStreamType())) {
|
&& !StreamTypeUtil.isLiveStream(item.getStreamType())) {
|
||||||
|
|||||||
13
app/src/main/java/org/schabi/newpipe/ktx/Bitmap.kt
Normal file
13
app/src/main/java/org/schabi/newpipe/ktx/Bitmap.kt
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
package org.schabi.newpipe.ktx
|
||||||
|
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.graphics.Rect
|
||||||
|
import androidx.core.graphics.BitmapCompat
|
||||||
|
|
||||||
|
@Suppress("NOTHING_TO_INLINE")
|
||||||
|
inline fun Bitmap.scale(
|
||||||
|
width: Int,
|
||||||
|
height: Int,
|
||||||
|
srcRect: Rect? = null,
|
||||||
|
scaleInLinearSpace: Boolean = true,
|
||||||
|
) = BitmapCompat.createScaledBitmap(this, width, height, srcRect, scaleInLinearSpace)
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
package org.schabi.newpipe.ktx
|
package org.schabi.newpipe.ktx
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.Parcelable
|
|
||||||
import androidx.core.os.BundleCompat
|
import androidx.core.os.BundleCompat
|
||||||
|
import java.io.Serializable
|
||||||
|
|
||||||
inline fun <reified T : Parcelable> Bundle.parcelableArrayList(key: String?): ArrayList<T>? {
|
inline fun <reified T : Serializable> Bundle.serializable(key: String?): T? {
|
||||||
return BundleCompat.getParcelableArrayList(this, key, T::class.java)
|
return BundleCompat.getSerializable(this, key, T::class.java)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Bundle?.toDebugString(): String {
|
fun Bundle?.toDebugString(): String {
|
||||||
|
|||||||
13
app/src/main/java/org/schabi/newpipe/ktx/Context.kt
Normal file
13
app/src/main/java/org/schabi/newpipe/ktx/Context.kt
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
package org.schabi.newpipe.ktx
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.ContextWrapper
|
||||||
|
import androidx.fragment.app.FragmentActivity
|
||||||
|
|
||||||
|
tailrec fun Context.findFragmentActivity(): FragmentActivity {
|
||||||
|
return when (this) {
|
||||||
|
is FragmentActivity -> this
|
||||||
|
is ContextWrapper -> baseContext.findFragmentActivity()
|
||||||
|
else -> throw IllegalStateException("Unable to find FragmentActivity")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -38,6 +38,8 @@ import org.schabi.newpipe.local.holder.LocalBookmarkPlaylistItemHolder;
|
|||||||
import org.schabi.newpipe.local.holder.RemoteBookmarkPlaylistItemHolder;
|
import org.schabi.newpipe.local.holder.RemoteBookmarkPlaylistItemHolder;
|
||||||
import org.schabi.newpipe.local.playlist.LocalPlaylistManager;
|
import org.schabi.newpipe.local.playlist.LocalPlaylistManager;
|
||||||
import org.schabi.newpipe.local.playlist.RemotePlaylistManager;
|
import org.schabi.newpipe.local.playlist.RemotePlaylistManager;
|
||||||
|
import org.schabi.newpipe.ui.emptystate.EmptyStateSpec;
|
||||||
|
import org.schabi.newpipe.ui.emptystate.EmptyStateUtil;
|
||||||
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.debounce.DebounceSavable;
|
import org.schabi.newpipe.util.debounce.DebounceSavable;
|
||||||
@@ -123,6 +125,10 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL
|
|||||||
super.initViews(rootView, savedInstanceState);
|
super.initViews(rootView, savedInstanceState);
|
||||||
|
|
||||||
itemListAdapter.setUseItemHandle(true);
|
itemListAdapter.setUseItemHandle(true);
|
||||||
|
EmptyStateUtil.setEmptyStateComposable(
|
||||||
|
rootView.findViewById(R.id.empty_state_view),
|
||||||
|
EmptyStateSpec.Companion.getNoBookmarkedPlaylist()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -74,6 +74,7 @@ import org.schabi.newpipe.ktx.slideUp
|
|||||||
import org.schabi.newpipe.local.feed.item.StreamItem
|
import org.schabi.newpipe.local.feed.item.StreamItem
|
||||||
import org.schabi.newpipe.local.feed.service.FeedLoadService
|
import org.schabi.newpipe.local.feed.service.FeedLoadService
|
||||||
import org.schabi.newpipe.local.subscription.SubscriptionManager
|
import org.schabi.newpipe.local.subscription.SubscriptionManager
|
||||||
|
import org.schabi.newpipe.ui.emptystate.setEmptyStateComposable
|
||||||
import org.schabi.newpipe.util.DeviceUtils
|
import org.schabi.newpipe.util.DeviceUtils
|
||||||
import org.schabi.newpipe.util.Localization
|
import org.schabi.newpipe.util.Localization
|
||||||
import org.schabi.newpipe.util.NavigationHelper
|
import org.schabi.newpipe.util.NavigationHelper
|
||||||
@@ -132,6 +133,7 @@ class FeedFragment : BaseStateFragment<FeedState>() {
|
|||||||
override fun onViewCreated(rootView: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(rootView: View, savedInstanceState: Bundle?) {
|
||||||
// super.onViewCreated() calls initListeners() which require the binding to be initialized
|
// super.onViewCreated() calls initListeners() which require the binding to be initialized
|
||||||
_feedBinding = FragmentFeedBinding.bind(rootView)
|
_feedBinding = FragmentFeedBinding.bind(rootView)
|
||||||
|
feedBinding.emptyStateView.setEmptyStateComposable()
|
||||||
super.onViewCreated(rootView, savedInstanceState)
|
super.onViewCreated(rootView, savedInstanceState)
|
||||||
|
|
||||||
val factory = FeedViewModel.getFactory(requireContext(), groupId)
|
val factory = FeedViewModel.getFactory(requireContext(), groupId)
|
||||||
@@ -202,6 +204,7 @@ class FeedFragment : BaseStateFragment<FeedState>() {
|
|||||||
// Menu
|
// Menu
|
||||||
// /////////////////////////////////////////////////////////////////////////
|
// /////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
@Deprecated("Deprecated in Java")
|
||||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||||
super.onCreateOptionsMenu(menu, inflater)
|
super.onCreateOptionsMenu(menu, inflater)
|
||||||
|
|
||||||
@@ -212,6 +215,7 @@ class FeedFragment : BaseStateFragment<FeedState>() {
|
|||||||
inflater.inflate(R.menu.menu_feed_fragment, menu)
|
inflater.inflate(R.menu.menu_feed_fragment, menu)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Deprecated("Deprecated in Java")
|
||||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||||
if (item.itemId == R.id.menu_item_feed_help) {
|
if (item.itemId == R.id.menu_item_feed_help) {
|
||||||
val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
|
val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
|
||||||
@@ -253,7 +257,7 @@ class FeedFragment : BaseStateFragment<FeedState>() {
|
|||||||
viewModel.getShowFutureItemsFromPreferences()
|
viewModel.getShowFutureItemsFromPreferences()
|
||||||
)
|
)
|
||||||
|
|
||||||
AlertDialog.Builder(context!!)
|
AlertDialog.Builder(requireContext())
|
||||||
.setTitle(R.string.feed_hide_streams_title)
|
.setTitle(R.string.feed_hide_streams_title)
|
||||||
.setMultiChoiceItems(dialogItems, checkedDialogItems) { _, which, isChecked ->
|
.setMultiChoiceItems(dialogItems, checkedDialogItems) { _, which, isChecked ->
|
||||||
checkedDialogItems[which] = isChecked
|
checkedDialogItems[which] = isChecked
|
||||||
@@ -267,6 +271,7 @@ class FeedFragment : BaseStateFragment<FeedState>() {
|
|||||||
.show()
|
.show()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Deprecated("Deprecated in Java")
|
||||||
override fun onDestroyOptionsMenu() {
|
override fun onDestroyOptionsMenu() {
|
||||||
super.onDestroyOptionsMenu()
|
super.onDestroyOptionsMenu()
|
||||||
activity?.supportActionBar?.subtitle = null
|
activity?.supportActionBar?.subtitle = null
|
||||||
|
|||||||
@@ -165,7 +165,7 @@ class FeedViewModel(
|
|||||||
fun getFactory(context: Context, groupId: Long) = viewModelFactory {
|
fun getFactory(context: Context, groupId: Long) = viewModelFactory {
|
||||||
initializer {
|
initializer {
|
||||||
FeedViewModel(
|
FeedViewModel(
|
||||||
App.getApp(),
|
App.instance,
|
||||||
groupId,
|
groupId,
|
||||||
// Read initial value from preferences
|
// Read initial value from preferences
|
||||||
getShowPlayedItemsFromPreferences(context.applicationContext),
|
getShowPlayedItemsFromPreferences(context.applicationContext),
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ 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.StreamTypeUtil
|
import org.schabi.newpipe.util.StreamTypeUtil
|
||||||
import org.schabi.newpipe.util.image.PicassoHelper
|
import org.schabi.newpipe.util.image.CoilHelper
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
import java.util.function.Consumer
|
import java.util.function.Consumer
|
||||||
|
|
||||||
@@ -101,7 +101,7 @@ data class StreamItem(
|
|||||||
viewBinding.itemProgressView.visibility = View.GONE
|
viewBinding.itemProgressView.visibility = View.GONE
|
||||||
}
|
}
|
||||||
|
|
||||||
PicassoHelper.loadThumbnail(stream.thumbnailUrl).into(viewBinding.itemThumbnailView)
|
CoilHelper.loadThumbnail(viewBinding.itemThumbnailView, stream.thumbnailUrl)
|
||||||
|
|
||||||
if (itemVersion != ItemVersion.MINI) {
|
if (itemVersion != ItemVersion.MINI) {
|
||||||
viewBinding.itemAdditionalDetails.text =
|
viewBinding.itemAdditionalDetails.text =
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import android.app.PendingIntent
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
import android.graphics.drawable.Drawable
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.provider.Settings
|
import android.provider.Settings
|
||||||
@@ -16,20 +15,17 @@ import androidx.core.app.PendingIntentCompat
|
|||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.core.content.getSystemService
|
import androidx.core.content.getSystemService
|
||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
import com.squareup.picasso.Picasso
|
|
||||||
import com.squareup.picasso.Target
|
|
||||||
import org.schabi.newpipe.R
|
import org.schabi.newpipe.R
|
||||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem
|
import org.schabi.newpipe.extractor.stream.StreamInfoItem
|
||||||
import org.schabi.newpipe.local.feed.service.FeedUpdateInfo
|
import org.schabi.newpipe.local.feed.service.FeedUpdateInfo
|
||||||
import org.schabi.newpipe.util.NavigationHelper
|
import org.schabi.newpipe.util.NavigationHelper
|
||||||
import org.schabi.newpipe.util.image.PicassoHelper
|
import org.schabi.newpipe.util.image.CoilHelper
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Helper for everything related to show notifications about new streams to the user.
|
* Helper for everything related to show notifications about new streams to the user.
|
||||||
*/
|
*/
|
||||||
class NotificationHelper(val context: Context) {
|
class NotificationHelper(val context: Context) {
|
||||||
private val manager = NotificationManagerCompat.from(context)
|
private val manager = NotificationManagerCompat.from(context)
|
||||||
private val iconLoadingTargets = ArrayList<Target>()
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Show notifications for new streams from a single channel. The individual notifications are
|
* Show notifications for new streams from a single channel. The individual notifications are
|
||||||
@@ -68,61 +64,31 @@ class NotificationHelper(val context: Context) {
|
|||||||
summaryBuilder.setStyle(style)
|
summaryBuilder.setStyle(style)
|
||||||
|
|
||||||
// open the channel page when clicking on the summary notification
|
// open the channel page when clicking on the summary notification
|
||||||
|
val intent = NavigationHelper
|
||||||
|
.getChannelIntent(context, data.serviceId, data.url)
|
||||||
|
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||||
summaryBuilder.setContentIntent(
|
summaryBuilder.setContentIntent(
|
||||||
PendingIntentCompat.getActivity(
|
PendingIntentCompat.getActivity(context, data.pseudoId, intent, 0, false)
|
||||||
context,
|
|
||||||
data.pseudoId,
|
|
||||||
NavigationHelper
|
|
||||||
.getChannelIntent(context, data.serviceId, data.url)
|
|
||||||
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK),
|
|
||||||
0,
|
|
||||||
false
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// a Target is like a listener for image loading events
|
val avatarIcon =
|
||||||
val target = object : Target {
|
CoilHelper.loadBitmapBlocking(context, data.avatarUrl, R.drawable.ic_newpipe_triangle_white)
|
||||||
override fun onBitmapLoaded(bitmap: Bitmap, from: Picasso.LoadedFrom) {
|
|
||||||
// set channel icon only if there is actually one (for Android versions < 7.0)
|
|
||||||
summaryBuilder.setLargeIcon(bitmap)
|
|
||||||
|
|
||||||
// Show individual stream notifications, set channel icon only if there is actually
|
summaryBuilder.setLargeIcon(avatarIcon)
|
||||||
// one
|
|
||||||
showStreamNotifications(newStreams, data.serviceId, data.url, bitmap)
|
|
||||||
// Show summary notification
|
|
||||||
manager.notify(data.pseudoId, summaryBuilder.build())
|
|
||||||
|
|
||||||
iconLoadingTargets.remove(this) // allow it to be garbage-collected
|
// Show individual stream notifications, set channel icon only if there is actually one
|
||||||
}
|
showStreamNotifications(newStreams, data.serviceId, avatarIcon)
|
||||||
|
// Show summary notification
|
||||||
override fun onBitmapFailed(e: Exception, errorDrawable: Drawable) {
|
manager.notify(data.pseudoId, summaryBuilder.build())
|
||||||
// Show individual stream notifications
|
|
||||||
showStreamNotifications(newStreams, data.serviceId, data.url, null)
|
|
||||||
// Show summary notification
|
|
||||||
manager.notify(data.pseudoId, summaryBuilder.build())
|
|
||||||
iconLoadingTargets.remove(this) // allow it to be garbage-collected
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onPrepareLoad(placeHolderDrawable: Drawable) {
|
|
||||||
// Nothing to do
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// add the target to the list to hold a strong reference and prevent it from being garbage
|
|
||||||
// collected, since Picasso only holds weak references to targets
|
|
||||||
iconLoadingTargets.add(target)
|
|
||||||
|
|
||||||
PicassoHelper.loadNotificationIcon(data.avatarUrl).into(target)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun showStreamNotifications(
|
private fun showStreamNotifications(
|
||||||
newStreams: List<StreamInfoItem>,
|
newStreams: List<StreamInfoItem>,
|
||||||
serviceId: Int,
|
serviceId: Int,
|
||||||
channelUrl: String,
|
|
||||||
channelIcon: Bitmap?
|
channelIcon: Bitmap?
|
||||||
) {
|
) {
|
||||||
for (stream in newStreams) {
|
for (stream in newStreams) {
|
||||||
val notification = createStreamNotification(stream, serviceId, channelUrl, channelIcon)
|
val notification = createStreamNotification(stream, serviceId, channelIcon)
|
||||||
manager.notify(stream.url.hashCode(), notification)
|
manager.notify(stream.url.hashCode(), notification)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -130,7 +96,6 @@ class NotificationHelper(val context: Context) {
|
|||||||
private fun createStreamNotification(
|
private fun createStreamNotification(
|
||||||
item: StreamInfoItem,
|
item: StreamInfoItem,
|
||||||
serviceId: Int,
|
serviceId: Int,
|
||||||
channelUrl: String,
|
|
||||||
channelIcon: Bitmap?
|
channelIcon: Bitmap?
|
||||||
): Notification {
|
): Notification {
|
||||||
return NotificationCompat.Builder(
|
return NotificationCompat.Builder(
|
||||||
@@ -141,7 +106,7 @@ class NotificationHelper(val context: Context) {
|
|||||||
.setLargeIcon(channelIcon)
|
.setLargeIcon(channelIcon)
|
||||||
.setContentTitle(item.name)
|
.setContentTitle(item.name)
|
||||||
.setContentText(item.uploaderName)
|
.setContentText(item.uploaderName)
|
||||||
.setGroup(channelUrl)
|
.setGroup(item.uploaderUrl)
|
||||||
.setColor(ContextCompat.getColor(context, R.color.ic_launcher_background))
|
.setColor(ContextCompat.getColor(context, R.color.ic_launcher_background))
|
||||||
.setColorized(true)
|
.setColorized(true)
|
||||||
.setAutoCancel(true)
|
.setAutoCancel(true)
|
||||||
|
|||||||
@@ -18,10 +18,13 @@ package org.schabi.newpipe.local.history;
|
|||||||
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import static org.schabi.newpipe.util.ExtractorHelper.getStreamInfo;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.collection.LongLongPair;
|
||||||
import androidx.preference.PreferenceManager;
|
import androidx.preference.PreferenceManager;
|
||||||
|
|
||||||
import org.schabi.newpipe.NewPipeDatabase;
|
import org.schabi.newpipe.NewPipeDatabase;
|
||||||
@@ -45,7 +48,6 @@ import org.schabi.newpipe.extractor.stream.StreamInfo;
|
|||||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||||
import org.schabi.newpipe.local.feed.FeedViewModel;
|
import org.schabi.newpipe.local.feed.FeedViewModel;
|
||||||
import org.schabi.newpipe.player.playqueue.PlayQueueItem;
|
import org.schabi.newpipe.player.playqueue.PlayQueueItem;
|
||||||
import org.schabi.newpipe.util.ExtractorHelper;
|
|
||||||
|
|
||||||
import java.time.OffsetDateTime;
|
import java.time.OffsetDateTime;
|
||||||
import java.time.ZoneOffset;
|
import java.time.ZoneOffset;
|
||||||
@@ -91,47 +93,39 @@ public class HistoryRecordManager {
|
|||||||
* @param info the item to mark as watched
|
* @param info the item to mark as watched
|
||||||
* @return a Maybe containing the ID of the item if successful
|
* @return a Maybe containing the ID of the item if successful
|
||||||
*/
|
*/
|
||||||
public Maybe<Long> markAsWatched(final StreamInfoItem info) {
|
public Completable markAsWatched(final StreamInfoItem info) {
|
||||||
if (!isStreamHistoryEnabled()) {
|
if (!isStreamHistoryEnabled()) {
|
||||||
return Maybe.empty();
|
return Completable.complete();
|
||||||
}
|
}
|
||||||
|
|
||||||
final OffsetDateTime currentTime = OffsetDateTime.now(ZoneOffset.UTC);
|
final var remoteInfo = getStreamInfo(info.getServiceId(), info.getUrl(), false)
|
||||||
return Maybe.fromCallable(() -> database.runInTransaction(() -> {
|
.map(item ->
|
||||||
final long streamId;
|
new LongLongPair(item.getDuration(), streamTable.upsert(new StreamEntity(item))));
|
||||||
final long duration;
|
|
||||||
// Duration will not exist if the item was loaded with fast mode, so fetch it if empty
|
return Single.just(info)
|
||||||
if (info.getDuration() < 0) {
|
.filter(item -> item.getDuration() >= 0)
|
||||||
final StreamInfo completeInfo = ExtractorHelper.getStreamInfo(
|
.map(item ->
|
||||||
info.getServiceId(),
|
new LongLongPair(item.getDuration(), streamTable.upsert(new StreamEntity(item)))
|
||||||
info.getUrl(),
|
|
||||||
false
|
|
||||||
)
|
)
|
||||||
.subscribeOn(Schedulers.io())
|
.switchIfEmpty(remoteInfo)
|
||||||
.blockingGet();
|
.flatMapCompletable(pair -> Completable.fromRunnable(() -> {
|
||||||
duration = completeInfo.getDuration();
|
final long duration = pair.getFirst();
|
||||||
streamId = streamTable.upsert(new StreamEntity(completeInfo));
|
final long streamId = pair.getSecond();
|
||||||
} else {
|
|
||||||
duration = info.getDuration();
|
|
||||||
streamId = streamTable.upsert(new StreamEntity(info));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update the stream progress to the full duration of the video
|
// Update the stream progress to the full duration of the video
|
||||||
final StreamStateEntity entity = new StreamStateEntity(
|
final var entity = new StreamStateEntity(streamId, duration * 1000);
|
||||||
streamId,
|
streamStateTable.upsert(entity);
|
||||||
duration * 1000
|
|
||||||
);
|
|
||||||
streamStateTable.upsert(entity);
|
|
||||||
|
|
||||||
// Add a history entry
|
// Add a history entry
|
||||||
final StreamHistoryEntity latestEntry = streamHistoryTable.getLatestEntry(streamId);
|
final var latestEntry = streamHistoryTable.getLatestEntry(streamId);
|
||||||
if (latestEntry == null) {
|
if (latestEntry == null) {
|
||||||
// never actually viewed: add history entry but with 0 views
|
final var currentTime = OffsetDateTime.now(ZoneOffset.UTC);
|
||||||
return streamHistoryTable.insert(new StreamHistoryEntity(streamId, currentTime, 0));
|
// never actually viewed: add history entry but with 0 views
|
||||||
} else {
|
final var entry = new StreamHistoryEntity(streamId, currentTime, 0);
|
||||||
return 0L;
|
streamHistoryTable.insert(entry);
|
||||||
}
|
}
|
||||||
})).subscribeOn(Schedulers.io());
|
}))
|
||||||
|
.subscribeOn(Schedulers.io());
|
||||||
}
|
}
|
||||||
|
|
||||||
public Maybe<Long> onViewed(final StreamInfo info) {
|
public Maybe<Long> onViewed(final StreamInfo info) {
|
||||||
@@ -221,7 +215,7 @@ public class HistoryRecordManager {
|
|||||||
public Flowable<List<String>> getRelatedSearches(final String query,
|
public Flowable<List<String>> getRelatedSearches(final String query,
|
||||||
final int similarQueryLimit,
|
final int similarQueryLimit,
|
||||||
final int uniqueQueryLimit) {
|
final int uniqueQueryLimit) {
|
||||||
return query.length() > 0
|
return !query.isEmpty()
|
||||||
? searchHistoryTable.getSimilarEntries(query, similarQueryLimit)
|
? searchHistoryTable.getSimilarEntries(query, similarQueryLimit)
|
||||||
: searchHistoryTable.getUniqueEntries(uniqueQueryLimit);
|
: searchHistoryTable.getUniqueEntries(uniqueQueryLimit);
|
||||||
}
|
}
|
||||||
@@ -236,47 +230,31 @@ public class HistoryRecordManager {
|
|||||||
|
|
||||||
public Maybe<StreamStateEntity> loadStreamState(final PlayQueueItem queueItem) {
|
public Maybe<StreamStateEntity> loadStreamState(final PlayQueueItem queueItem) {
|
||||||
return queueItem.getStream()
|
return queueItem.getStream()
|
||||||
.map(info -> streamTable.upsert(new StreamEntity(info)))
|
.flatMapMaybe(this::loadStreamState)
|
||||||
.flatMapPublisher(streamStateTable::getState)
|
|
||||||
.firstElement()
|
|
||||||
.flatMap(list -> list.isEmpty() ? Maybe.empty() : Maybe.just(list.get(0)))
|
|
||||||
.filter(state -> state.isValid(queueItem.getDuration()))
|
.filter(state -> state.isValid(queueItem.getDuration()))
|
||||||
.subscribeOn(Schedulers.io());
|
.subscribeOn(Schedulers.io());
|
||||||
}
|
}
|
||||||
|
|
||||||
public Maybe<StreamStateEntity> loadStreamState(final StreamInfo info) {
|
public Maybe<StreamStateEntity> loadStreamState(final StreamInfo info) {
|
||||||
return Single.fromCallable(() -> streamTable.upsert(new StreamEntity(info)))
|
return Single.fromCallable(() -> streamTable.upsert(new StreamEntity(info)))
|
||||||
.flatMapPublisher(streamStateTable::getState)
|
.flatMapMaybe(streamStateTable::getState)
|
||||||
.firstElement()
|
|
||||||
.flatMap(list -> list.isEmpty() ? Maybe.empty() : Maybe.just(list.get(0)))
|
|
||||||
.filter(state -> state.isValid(info.getDuration()))
|
|
||||||
.subscribeOn(Schedulers.io());
|
.subscribeOn(Schedulers.io());
|
||||||
}
|
}
|
||||||
|
|
||||||
public Completable saveStreamState(@NonNull final StreamInfo info, final long progressMillis) {
|
public Completable saveStreamState(@NonNull final StreamInfo info, final long progressMillis) {
|
||||||
return Completable.fromAction(() -> database.runInTransaction(() -> {
|
return Completable.fromAction(() -> database.runInTransaction(() -> {
|
||||||
final long streamId = streamTable.upsert(new StreamEntity(info));
|
final long streamId = streamTable.upsert(new StreamEntity(info));
|
||||||
final StreamStateEntity state = new StreamStateEntity(streamId, progressMillis);
|
final var state = new StreamStateEntity(streamId, progressMillis);
|
||||||
if (state.isValid(info.getDuration())) {
|
if (state.isValid(info.getDuration())) {
|
||||||
streamStateTable.upsert(state);
|
streamStateTable.upsert(state);
|
||||||
}
|
}
|
||||||
})).subscribeOn(Schedulers.io());
|
})).subscribeOn(Schedulers.io());
|
||||||
}
|
}
|
||||||
|
|
||||||
public Single<StreamStateEntity[]> loadStreamState(final InfoItem info) {
|
public Maybe<StreamStateEntity> loadStreamState(final InfoItem info) {
|
||||||
return Single.fromCallable(() -> {
|
return streamTable.getStream(info.getServiceId(), info.getUrl())
|
||||||
final List<StreamEntity> entities = streamTable
|
.flatMap(entity -> streamStateTable.getState(entity.getUid()))
|
||||||
.getStream(info.getServiceId(), info.getUrl()).blockingFirst();
|
.subscribeOn(Schedulers.io());
|
||||||
if (entities.isEmpty()) {
|
|
||||||
return new StreamStateEntity[]{null};
|
|
||||||
}
|
|
||||||
final List<StreamStateEntity> states = streamStateTable
|
|
||||||
.getState(entities.get(0).getUid()).blockingFirst();
|
|
||||||
if (states.isEmpty()) {
|
|
||||||
return new StreamStateEntity[]{null};
|
|
||||||
}
|
|
||||||
return new StreamStateEntity[]{states.get(0)};
|
|
||||||
}).subscribeOn(Schedulers.io());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public Single<List<StreamStateEntity>> loadLocalStreamStateBatch(
|
public Single<List<StreamStateEntity>> loadLocalStreamStateBatch(
|
||||||
@@ -295,13 +273,7 @@ public class HistoryRecordManager {
|
|||||||
result.add(null);
|
result.add(null);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
final List<StreamStateEntity> states = streamStateTable.getState(streamId)
|
result.add(streamStateTable.getState(streamId).blockingGet());
|
||||||
.blockingFirst();
|
|
||||||
if (states.isEmpty()) {
|
|
||||||
result.add(null);
|
|
||||||
} else {
|
|
||||||
result.add(states.get(0));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}).subscribeOn(Schedulers.io());
|
}).subscribeOn(Schedulers.io());
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ import org.schabi.newpipe.database.playlist.PlaylistDuplicatesEntry;
|
|||||||
import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry;
|
import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry;
|
||||||
import org.schabi.newpipe.local.LocalItemBuilder;
|
import org.schabi.newpipe.local.LocalItemBuilder;
|
||||||
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
||||||
import org.schabi.newpipe.util.image.PicassoHelper;
|
|
||||||
import org.schabi.newpipe.util.Localization;
|
import org.schabi.newpipe.util.Localization;
|
||||||
|
import org.schabi.newpipe.util.image.CoilHelper;
|
||||||
|
|
||||||
import java.time.format.DateTimeFormatter;
|
import java.time.format.DateTimeFormatter;
|
||||||
|
|
||||||
@@ -30,17 +30,16 @@ public class LocalPlaylistItemHolder extends PlaylistItemHolder {
|
|||||||
public void updateFromItem(final LocalItem localItem,
|
public void updateFromItem(final LocalItem localItem,
|
||||||
final HistoryRecordManager historyRecordManager,
|
final HistoryRecordManager historyRecordManager,
|
||||||
final DateTimeFormatter dateTimeFormatter) {
|
final DateTimeFormatter dateTimeFormatter) {
|
||||||
if (!(localItem instanceof PlaylistMetadataEntry)) {
|
if (!(localItem instanceof PlaylistMetadataEntry item)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final PlaylistMetadataEntry item = (PlaylistMetadataEntry) localItem;
|
|
||||||
|
|
||||||
itemTitleView.setText(item.name);
|
itemTitleView.setText(item.name);
|
||||||
itemStreamCountView.setText(Localization.localizeStreamCountMini(
|
itemStreamCountView.setText(Localization.localizeStreamCountMini(
|
||||||
itemStreamCountView.getContext(), item.streamCount));
|
itemStreamCountView.getContext(), item.streamCount));
|
||||||
itemUploaderView.setVisibility(View.INVISIBLE);
|
itemUploaderView.setVisibility(View.INVISIBLE);
|
||||||
|
|
||||||
PicassoHelper.loadPlaylistThumbnail(item.thumbnailUrl).into(itemThumbnailView);
|
CoilHelper.INSTANCE.loadPlaylistThumbnail(itemThumbnailView, item.thumbnailUrl);
|
||||||
|
|
||||||
if (item instanceof PlaylistDuplicatesEntry
|
if (item instanceof PlaylistDuplicatesEntry
|
||||||
&& ((PlaylistDuplicatesEntry) item).timesStreamIsContained > 0) {
|
&& ((PlaylistDuplicatesEntry) item).timesStreamIsContained > 0) {
|
||||||
|
|||||||
@@ -16,8 +16,8 @@ import org.schabi.newpipe.local.LocalItemBuilder;
|
|||||||
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
||||||
import org.schabi.newpipe.util.DependentPreferenceHelper;
|
import org.schabi.newpipe.util.DependentPreferenceHelper;
|
||||||
import org.schabi.newpipe.util.Localization;
|
import org.schabi.newpipe.util.Localization;
|
||||||
import org.schabi.newpipe.util.image.PicassoHelper;
|
|
||||||
import org.schabi.newpipe.util.ServiceHelper;
|
import org.schabi.newpipe.util.ServiceHelper;
|
||||||
|
import org.schabi.newpipe.util.image.CoilHelper;
|
||||||
import org.schabi.newpipe.views.AnimatedProgressBar;
|
import org.schabi.newpipe.views.AnimatedProgressBar;
|
||||||
|
|
||||||
import java.time.format.DateTimeFormatter;
|
import java.time.format.DateTimeFormatter;
|
||||||
@@ -83,8 +83,8 @@ public class LocalPlaylistStreamItemHolder extends LocalItemHolder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Default thumbnail is shown on error, while loading and if the url is empty
|
// Default thumbnail is shown on error, while loading and if the url is empty
|
||||||
PicassoHelper.loadThumbnail(item.getStreamEntity().getThumbnailUrl())
|
CoilHelper.INSTANCE.loadThumbnail(itemThumbnailView,
|
||||||
.into(itemThumbnailView);
|
item.getStreamEntity().getThumbnailUrl());
|
||||||
|
|
||||||
itemView.setOnClickListener(view -> {
|
itemView.setOnClickListener(view -> {
|
||||||
if (itemBuilder.getOnItemSelectedListener() != null) {
|
if (itemBuilder.getOnItemSelectedListener() != null) {
|
||||||
|
|||||||
@@ -16,8 +16,8 @@ import org.schabi.newpipe.local.LocalItemBuilder;
|
|||||||
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
||||||
import org.schabi.newpipe.util.DependentPreferenceHelper;
|
import org.schabi.newpipe.util.DependentPreferenceHelper;
|
||||||
import org.schabi.newpipe.util.Localization;
|
import org.schabi.newpipe.util.Localization;
|
||||||
import org.schabi.newpipe.util.image.PicassoHelper;
|
|
||||||
import org.schabi.newpipe.util.ServiceHelper;
|
import org.schabi.newpipe.util.ServiceHelper;
|
||||||
|
import org.schabi.newpipe.util.image.CoilHelper;
|
||||||
import org.schabi.newpipe.views.AnimatedProgressBar;
|
import org.schabi.newpipe.views.AnimatedProgressBar;
|
||||||
|
|
||||||
import java.time.format.DateTimeFormatter;
|
import java.time.format.DateTimeFormatter;
|
||||||
@@ -117,8 +117,8 @@ public class LocalStatisticStreamItemHolder extends LocalItemHolder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Default thumbnail is shown on error, while loading and if the url is empty
|
// Default thumbnail is shown on error, while loading and if the url is empty
|
||||||
PicassoHelper.loadThumbnail(item.getStreamEntity().getThumbnailUrl())
|
CoilHelper.INSTANCE.loadThumbnail(itemThumbnailView,
|
||||||
.into(itemThumbnailView);
|
item.getStreamEntity().getThumbnailUrl());
|
||||||
|
|
||||||
itemView.setOnClickListener(view -> {
|
itemView.setOnClickListener(view -> {
|
||||||
if (itemBuilder.getOnItemSelectedListener() != null) {
|
if (itemBuilder.getOnItemSelectedListener() != null) {
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity;
|
|||||||
import org.schabi.newpipe.local.LocalItemBuilder;
|
import org.schabi.newpipe.local.LocalItemBuilder;
|
||||||
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
||||||
import org.schabi.newpipe.util.Localization;
|
import org.schabi.newpipe.util.Localization;
|
||||||
import org.schabi.newpipe.util.image.PicassoHelper;
|
|
||||||
import org.schabi.newpipe.util.ServiceHelper;
|
import org.schabi.newpipe.util.ServiceHelper;
|
||||||
|
import org.schabi.newpipe.util.image.CoilHelper;
|
||||||
|
|
||||||
import java.time.format.DateTimeFormatter;
|
import java.time.format.DateTimeFormatter;
|
||||||
|
|
||||||
@@ -29,10 +29,9 @@ public class RemotePlaylistItemHolder extends PlaylistItemHolder {
|
|||||||
public void updateFromItem(final LocalItem localItem,
|
public void updateFromItem(final LocalItem localItem,
|
||||||
final HistoryRecordManager historyRecordManager,
|
final HistoryRecordManager historyRecordManager,
|
||||||
final DateTimeFormatter dateTimeFormatter) {
|
final DateTimeFormatter dateTimeFormatter) {
|
||||||
if (!(localItem instanceof PlaylistRemoteEntity)) {
|
if (!(localItem instanceof PlaylistRemoteEntity item)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final PlaylistRemoteEntity item = (PlaylistRemoteEntity) localItem;
|
|
||||||
|
|
||||||
itemTitleView.setText(item.getName());
|
itemTitleView.setText(item.getName());
|
||||||
itemStreamCountView.setText(Localization.localizeStreamCountMini(
|
itemStreamCountView.setText(Localization.localizeStreamCountMini(
|
||||||
@@ -45,7 +44,7 @@ public class RemotePlaylistItemHolder extends PlaylistItemHolder {
|
|||||||
itemUploaderView.setText(ServiceHelper.getNameOfServiceById(item.getServiceId()));
|
itemUploaderView.setText(ServiceHelper.getNameOfServiceById(item.getServiceId()));
|
||||||
}
|
}
|
||||||
|
|
||||||
PicassoHelper.loadPlaylistThumbnail(item.getThumbnailUrl()).into(itemThumbnailView);
|
CoilHelper.INSTANCE.loadPlaylistThumbnail(itemThumbnailView, item.getThumbnailUrl());
|
||||||
|
|
||||||
super.updateFromItem(localItem, historyRecordManager, dateTimeFormatter);
|
super.updateFromItem(localItem, historyRecordManager, dateTimeFormatter);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ import org.schabi.newpipe.local.subscription.services.SubscriptionsImportService
|
|||||||
import org.schabi.newpipe.local.subscription.services.SubscriptionsImportService.PREVIOUS_EXPORT_MODE
|
import org.schabi.newpipe.local.subscription.services.SubscriptionsImportService.PREVIOUS_EXPORT_MODE
|
||||||
import org.schabi.newpipe.streams.io.NoFileManagerSafeGuard
|
import org.schabi.newpipe.streams.io.NoFileManagerSafeGuard
|
||||||
import org.schabi.newpipe.streams.io.StoredFileHelper
|
import org.schabi.newpipe.streams.io.StoredFileHelper
|
||||||
|
import org.schabi.newpipe.ui.emptystate.setEmptyStateComposable
|
||||||
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.ServiceHelper
|
||||||
@@ -129,6 +130,7 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
|
|||||||
// Menu
|
// Menu
|
||||||
// ////////////////////////////////////////////////////////////////////////
|
// ////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
@Deprecated("Deprecated in Java")
|
||||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||||
super.onCreateOptionsMenu(menu, inflater)
|
super.onCreateOptionsMenu(menu, inflater)
|
||||||
|
|
||||||
@@ -256,6 +258,8 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
|
|||||||
binding.itemsList.adapter = groupAdapter
|
binding.itemsList.adapter = groupAdapter
|
||||||
binding.itemsList.itemAnimator = null
|
binding.itemsList.itemAnimator = null
|
||||||
|
|
||||||
|
binding.emptyStateView.setEmptyStateComposable()
|
||||||
|
|
||||||
viewModel = ViewModelProvider(this)[SubscriptionViewModel::class.java]
|
viewModel = ViewModelProvider(this)[SubscriptionViewModel::class.java]
|
||||||
viewModel.stateLiveData.observe(viewLifecycleOwner) { it?.let(this::handleResult) }
|
viewModel.stateLiveData.observe(viewLifecycleOwner) { it?.let(this::handleResult) }
|
||||||
viewModel.feedGroupsLiveData.observe(viewLifecycleOwner) {
|
viewModel.feedGroupsLiveData.observe(viewLifecycleOwner) {
|
||||||
|
|||||||
@@ -94,6 +94,7 @@ class FeedGroupDialog : DialogFragment(), BackPressable {
|
|||||||
|
|
||||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
return object : Dialog(requireActivity(), theme) {
|
return object : Dialog(requireActivity(), theme) {
|
||||||
|
@Deprecated("Deprecated in Java")
|
||||||
override fun onBackPressed() {
|
override fun onBackPressed() {
|
||||||
if (!this@FeedGroupDialog.onBackPressed()) {
|
if (!this@FeedGroupDialog.onBackPressed()) {
|
||||||
super.onBackPressed()
|
super.onBackPressed()
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import org.schabi.newpipe.R
|
|||||||
import org.schabi.newpipe.extractor.channel.ChannelInfoItem
|
import org.schabi.newpipe.extractor.channel.ChannelInfoItem
|
||||||
import org.schabi.newpipe.util.Localization
|
import org.schabi.newpipe.util.Localization
|
||||||
import org.schabi.newpipe.util.OnClickGesture
|
import org.schabi.newpipe.util.OnClickGesture
|
||||||
import org.schabi.newpipe.util.image.PicassoHelper
|
import org.schabi.newpipe.util.image.CoilHelper
|
||||||
|
|
||||||
class ChannelItem(
|
class ChannelItem(
|
||||||
private val infoItem: ChannelInfoItem,
|
private val infoItem: ChannelInfoItem,
|
||||||
@@ -39,7 +39,7 @@ class ChannelItem(
|
|||||||
itemChannelDescriptionView.text = infoItem.description
|
itemChannelDescriptionView.text = infoItem.description
|
||||||
}
|
}
|
||||||
|
|
||||||
PicassoHelper.loadAvatar(infoItem.thumbnails).into(itemThumbnailView)
|
CoilHelper.loadAvatar(itemThumbnailView, infoItem.thumbnails)
|
||||||
|
|
||||||
gesturesListener?.run {
|
gesturesListener?.run {
|
||||||
viewHolder.root.setOnClickListener { selected(infoItem) }
|
viewHolder.root.setOnClickListener { selected(infoItem) }
|
||||||
|
|||||||
@@ -3,14 +3,18 @@ package org.schabi.newpipe.local.subscription.item
|
|||||||
import android.view.View
|
import android.view.View
|
||||||
import com.xwray.groupie.viewbinding.BindableItem
|
import com.xwray.groupie.viewbinding.BindableItem
|
||||||
import org.schabi.newpipe.R
|
import org.schabi.newpipe.R
|
||||||
import org.schabi.newpipe.databinding.ListEmptyViewBinding
|
import org.schabi.newpipe.databinding.ListEmptyViewSubscriptionsBinding
|
||||||
|
import org.schabi.newpipe.ui.emptystate.EmptyStateSpec
|
||||||
|
import org.schabi.newpipe.ui.emptystate.setEmptyStateComposable
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* When there are no subscriptions, show a hint to the user about how to import subscriptions
|
* When there are no subscriptions, show a hint to the user about how to import subscriptions
|
||||||
*/
|
*/
|
||||||
class ImportSubscriptionsHintPlaceholderItem : BindableItem<ListEmptyViewBinding>() {
|
class ImportSubscriptionsHintPlaceholderItem : BindableItem<ListEmptyViewSubscriptionsBinding>() {
|
||||||
override fun getLayout(): Int = R.layout.list_empty_view_subscriptions
|
override fun getLayout(): Int = R.layout.list_empty_view_subscriptions
|
||||||
override fun bind(viewBinding: ListEmptyViewBinding, position: Int) {}
|
override fun bind(viewBinding: ListEmptyViewSubscriptionsBinding, position: Int) {
|
||||||
|
viewBinding.root.setEmptyStateComposable(EmptyStateSpec.NoSubscriptionsHint)
|
||||||
|
}
|
||||||
override fun getSpanSize(spanCount: Int, position: Int): Int = spanCount
|
override fun getSpanSize(spanCount: Int, position: Int): Int = spanCount
|
||||||
override fun initializeViewBinding(view: View) = ListEmptyViewBinding.bind(view)
|
override fun initializeViewBinding(view: View) = ListEmptyViewSubscriptionsBinding.bind(view)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import org.schabi.newpipe.database.subscription.SubscriptionEntity
|
|||||||
import org.schabi.newpipe.databinding.PickerSubscriptionItemBinding
|
import org.schabi.newpipe.databinding.PickerSubscriptionItemBinding
|
||||||
import org.schabi.newpipe.ktx.AnimationType
|
import org.schabi.newpipe.ktx.AnimationType
|
||||||
import org.schabi.newpipe.ktx.animate
|
import org.schabi.newpipe.ktx.animate
|
||||||
import org.schabi.newpipe.util.image.PicassoHelper
|
import org.schabi.newpipe.util.image.CoilHelper
|
||||||
|
|
||||||
data class PickerSubscriptionItem(
|
data class PickerSubscriptionItem(
|
||||||
val subscriptionEntity: SubscriptionEntity,
|
val subscriptionEntity: SubscriptionEntity,
|
||||||
@@ -21,7 +21,7 @@ data class PickerSubscriptionItem(
|
|||||||
override fun getSpanSize(spanCount: Int, position: Int): Int = 1
|
override fun getSpanSize(spanCount: Int, position: Int): Int = 1
|
||||||
|
|
||||||
override fun bind(viewBinding: PickerSubscriptionItemBinding, position: Int) {
|
override fun bind(viewBinding: PickerSubscriptionItemBinding, position: Int) {
|
||||||
PicassoHelper.loadAvatar(subscriptionEntity.avatarUrl).into(viewBinding.thumbnailView)
|
CoilHelper.loadAvatar(viewBinding.thumbnailView, subscriptionEntity.avatarUrl)
|
||||||
viewBinding.titleView.text = subscriptionEntity.name
|
viewBinding.titleView.text = subscriptionEntity.name
|
||||||
viewBinding.selectedHighlight.isVisible = isSelected
|
viewBinding.selectedHighlight.isVisible = isSelected
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package org.schabi.newpipe.paging
|
||||||
|
|
||||||
|
import androidx.paging.PagingSource
|
||||||
|
import androidx.paging.PagingState
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import org.schabi.newpipe.extractor.NewPipe
|
||||||
|
import org.schabi.newpipe.extractor.Page
|
||||||
|
import org.schabi.newpipe.extractor.comments.CommentsInfo
|
||||||
|
import org.schabi.newpipe.extractor.comments.CommentsInfoItem
|
||||||
|
|
||||||
|
class CommentRepliesSource(
|
||||||
|
private val commentInfo: CommentsInfoItem,
|
||||||
|
) : PagingSource<Page, CommentsInfoItem>() {
|
||||||
|
private val service = NewPipe.getService(commentInfo.serviceId)
|
||||||
|
|
||||||
|
override suspend fun load(params: LoadParams<Page>): LoadResult<Page, CommentsInfoItem> {
|
||||||
|
// params.key is null the first time load() is called, and we need to return the first page
|
||||||
|
val repliesPage = params.key ?: commentInfo.replies
|
||||||
|
val info = withContext(Dispatchers.IO) {
|
||||||
|
CommentsInfo.getMoreItems(service, commentInfo.url, repliesPage)
|
||||||
|
}
|
||||||
|
return LoadResult.Page(info.items, null, info.nextPage)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getRefreshKey(state: PagingState<Page, CommentsInfoItem>) = null
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
package org.schabi.newpipe.paging
|
||||||
|
|
||||||
|
import androidx.paging.PagingSource
|
||||||
|
import androidx.paging.PagingState
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import org.schabi.newpipe.extractor.NewPipe
|
||||||
|
import org.schabi.newpipe.extractor.Page
|
||||||
|
import org.schabi.newpipe.extractor.comments.CommentsInfo
|
||||||
|
import org.schabi.newpipe.extractor.comments.CommentsInfoItem
|
||||||
|
import org.schabi.newpipe.ui.components.video.comment.CommentInfo
|
||||||
|
|
||||||
|
class CommentsSource(private val commentInfo: CommentInfo) : PagingSource<Page, CommentsInfoItem>() {
|
||||||
|
private val service = NewPipe.getService(commentInfo.serviceId)
|
||||||
|
|
||||||
|
override suspend fun load(params: LoadParams<Page>): LoadResult<Page, CommentsInfoItem> {
|
||||||
|
// params.key is null the first time the load() function is called, so we need to return the
|
||||||
|
// first batch of already-loaded comments
|
||||||
|
if (params.key == null) {
|
||||||
|
return LoadResult.Page(commentInfo.comments, null, commentInfo.nextPage)
|
||||||
|
} else {
|
||||||
|
val info = withContext(Dispatchers.IO) {
|
||||||
|
CommentsInfo.getMoreItems(service, commentInfo.url, params.key)
|
||||||
|
}
|
||||||
|
return LoadResult.Page(info.items, null, info.nextPage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getRefreshKey(state: PagingState<Page, CommentsInfoItem>) = null
|
||||||
|
}
|
||||||
@@ -46,6 +46,7 @@ import static org.schabi.newpipe.util.ListHelper.getPopupResolutionIndex;
|
|||||||
import static org.schabi.newpipe.util.ListHelper.getResolutionIndex;
|
import static org.schabi.newpipe.util.ListHelper.getResolutionIndex;
|
||||||
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
|
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
|
||||||
import static java.util.concurrent.TimeUnit.MILLISECONDS;
|
import static java.util.concurrent.TimeUnit.MILLISECONDS;
|
||||||
|
import static coil3.Image_androidKt.toBitmap;
|
||||||
|
|
||||||
import android.content.BroadcastReceiver;
|
import android.content.BroadcastReceiver;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
@@ -53,7 +54,6 @@ import android.content.Intent;
|
|||||||
import android.content.IntentFilter;
|
import android.content.IntentFilter;
|
||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
import android.graphics.Bitmap;
|
import android.graphics.Bitmap;
|
||||||
import android.graphics.drawable.Drawable;
|
|
||||||
import android.media.AudioManager;
|
import android.media.AudioManager;
|
||||||
import android.support.v4.media.session.MediaSessionCompat;
|
import android.support.v4.media.session.MediaSessionCompat;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
@@ -79,8 +79,6 @@ import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
|
|||||||
import com.google.android.exoplayer2.trackselection.MappingTrackSelector;
|
import com.google.android.exoplayer2.trackselection.MappingTrackSelector;
|
||||||
import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter;
|
import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter;
|
||||||
import com.google.android.exoplayer2.video.VideoSize;
|
import com.google.android.exoplayer2.video.VideoSize;
|
||||||
import com.squareup.picasso.Picasso;
|
|
||||||
import com.squareup.picasso.Target;
|
|
||||||
|
|
||||||
import org.schabi.newpipe.MainActivity;
|
import org.schabi.newpipe.MainActivity;
|
||||||
import org.schabi.newpipe.R;
|
import org.schabi.newpipe.R;
|
||||||
@@ -88,8 +86,8 @@ import org.schabi.newpipe.databinding.PlayerBinding;
|
|||||||
import org.schabi.newpipe.error.ErrorInfo;
|
import org.schabi.newpipe.error.ErrorInfo;
|
||||||
import org.schabi.newpipe.error.ErrorUtil;
|
import org.schabi.newpipe.error.ErrorUtil;
|
||||||
import org.schabi.newpipe.error.UserAction;
|
import org.schabi.newpipe.error.UserAction;
|
||||||
import org.schabi.newpipe.extractor.stream.AudioStream;
|
|
||||||
import org.schabi.newpipe.extractor.Image;
|
import org.schabi.newpipe.extractor.Image;
|
||||||
|
import org.schabi.newpipe.extractor.stream.AudioStream;
|
||||||
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||||
import org.schabi.newpipe.extractor.stream.StreamType;
|
import org.schabi.newpipe.extractor.stream.StreamType;
|
||||||
import org.schabi.newpipe.extractor.stream.VideoStream;
|
import org.schabi.newpipe.extractor.stream.VideoStream;
|
||||||
@@ -120,14 +118,15 @@ import org.schabi.newpipe.player.ui.VideoPlayerUi;
|
|||||||
import org.schabi.newpipe.util.DependentPreferenceHelper;
|
import org.schabi.newpipe.util.DependentPreferenceHelper;
|
||||||
import org.schabi.newpipe.util.ListHelper;
|
import org.schabi.newpipe.util.ListHelper;
|
||||||
import org.schabi.newpipe.util.NavigationHelper;
|
import org.schabi.newpipe.util.NavigationHelper;
|
||||||
import org.schabi.newpipe.util.image.PicassoHelper;
|
|
||||||
import org.schabi.newpipe.util.SerializedCache;
|
import org.schabi.newpipe.util.SerializedCache;
|
||||||
import org.schabi.newpipe.util.StreamTypeUtil;
|
import org.schabi.newpipe.util.StreamTypeUtil;
|
||||||
|
import org.schabi.newpipe.util.image.CoilHelper;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.stream.IntStream;
|
import java.util.stream.IntStream;
|
||||||
|
|
||||||
|
import coil3.target.Target;
|
||||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||||
import io.reactivex.rxjava3.core.Observable;
|
import io.reactivex.rxjava3.core.Observable;
|
||||||
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
||||||
@@ -176,7 +175,6 @@ public final class Player implements PlaybackListener, Listener {
|
|||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
public static final int RENDERER_UNAVAILABLE = -1;
|
public static final int RENDERER_UNAVAILABLE = -1;
|
||||||
private static final String PICASSO_PLAYER_THUMBNAIL_TAG = "PICASSO_PLAYER_THUMBNAIL_TAG";
|
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
// Playback
|
// Playback
|
||||||
@@ -195,6 +193,8 @@ public final class Player implements PlaybackListener, Listener {
|
|||||||
private MediaItemTag currentMetadata;
|
private MediaItemTag currentMetadata;
|
||||||
@Nullable
|
@Nullable
|
||||||
private Bitmap currentThumbnail;
|
private Bitmap currentThumbnail;
|
||||||
|
@Nullable
|
||||||
|
private coil3.request.Disposable thumbnailDisposable;
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
// Player
|
// Player
|
||||||
@@ -248,12 +248,6 @@ public final class Player implements PlaybackListener, Listener {
|
|||||||
@NonNull
|
@NonNull
|
||||||
private final CompositeDisposable databaseUpdateDisposable = new CompositeDisposable();
|
private final CompositeDisposable databaseUpdateDisposable = new CompositeDisposable();
|
||||||
|
|
||||||
// This is the only listener we need for thumbnail loading, since there is always at most only
|
|
||||||
// one thumbnail being loaded at a time. This field is also here to maintain a strong reference,
|
|
||||||
// which would otherwise be garbage collected since Picasso holds weak references to targets.
|
|
||||||
@NonNull
|
|
||||||
private final Target currentThumbnailTarget;
|
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
// Utils
|
// Utils
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
@@ -306,8 +300,6 @@ public final class Player implements PlaybackListener, Listener {
|
|||||||
videoResolver = new VideoPlaybackResolver(context, dataSource, getQualityResolver());
|
videoResolver = new VideoPlaybackResolver(context, dataSource, getQualityResolver());
|
||||||
audioResolver = new AudioPlaybackResolver(context, dataSource);
|
audioResolver = new AudioPlaybackResolver(context, dataSource);
|
||||||
|
|
||||||
currentThumbnailTarget = getCurrentThumbnailTarget();
|
|
||||||
|
|
||||||
// The UIs added here should always be present. They will be initialized when the player
|
// The UIs added here should always be present. They will be initialized when the player
|
||||||
// reaches the initialization step. Make sure the media session ui is before the
|
// reaches the initialization step. Make sure the media session ui is before the
|
||||||
// notification ui in the UIs list, since the notification depends on the media session in
|
// notification ui in the UIs list, since the notification depends on the media session in
|
||||||
@@ -481,14 +473,15 @@ public final class Player implements PlaybackListener, Listener {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void initUIsForCurrentPlayerType() {
|
private void initUIsForCurrentPlayerType() {
|
||||||
if ((UIs.get(MainPlayerUi.class).isPresent() && playerType == PlayerType.MAIN)
|
if ((UIs.getOpt(MainPlayerUi.class).isPresent() && playerType == PlayerType.MAIN)
|
||||||
|| (UIs.get(PopupPlayerUi.class).isPresent() && playerType == PlayerType.POPUP)) {
|
|| (UIs.getOpt(PopupPlayerUi.class).isPresent()
|
||||||
|
&& playerType == PlayerType.POPUP)) {
|
||||||
// correct UI already in place
|
// correct UI already in place
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// try to reuse binding if possible
|
// try to reuse binding if possible
|
||||||
final PlayerBinding binding = UIs.get(VideoPlayerUi.class).map(VideoPlayerUi::getBinding)
|
final PlayerBinding binding = UIs.getOpt(VideoPlayerUi.class).map(VideoPlayerUi::getBinding)
|
||||||
.orElseGet(() -> {
|
.orElseGet(() -> {
|
||||||
if (playerType == PlayerType.AUDIO) {
|
if (playerType == PlayerType.AUDIO) {
|
||||||
return null;
|
return null;
|
||||||
@@ -499,15 +492,15 @@ public final class Player implements PlaybackListener, Listener {
|
|||||||
|
|
||||||
switch (playerType) {
|
switch (playerType) {
|
||||||
case MAIN:
|
case MAIN:
|
||||||
UIs.destroyAll(PopupPlayerUi.class);
|
UIs.destroyAllOfType(PopupPlayerUi.class);
|
||||||
UIs.addAndPrepare(new MainPlayerUi(this, binding));
|
UIs.addAndPrepare(new MainPlayerUi(this, binding));
|
||||||
break;
|
break;
|
||||||
case POPUP:
|
case POPUP:
|
||||||
UIs.destroyAll(MainPlayerUi.class);
|
UIs.destroyAllOfType(MainPlayerUi.class);
|
||||||
UIs.addAndPrepare(new PopupPlayerUi(this, binding));
|
UIs.addAndPrepare(new PopupPlayerUi(this, binding));
|
||||||
break;
|
break;
|
||||||
case AUDIO:
|
case AUDIO:
|
||||||
UIs.destroyAll(VideoPlayerUi.class);
|
UIs.destroyAllOfType(VideoPlayerUi.class);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -598,9 +591,15 @@ public final class Player implements PlaybackListener, Listener {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void destroy() {
|
|
||||||
|
/**
|
||||||
|
* Shut down this player.
|
||||||
|
* Saves the stream progress, sets recovery.
|
||||||
|
* Then destroys the player in all UIs and destroys the UIs as well.
|
||||||
|
*/
|
||||||
|
public void saveAndShutdown() {
|
||||||
if (DEBUG) {
|
if (DEBUG) {
|
||||||
Log.d(TAG, "destroy() called");
|
Log.d(TAG, "saveAndShutdown() called");
|
||||||
}
|
}
|
||||||
|
|
||||||
saveStreamProgressState();
|
saveStreamProgressState();
|
||||||
@@ -612,9 +611,8 @@ public final class Player implements PlaybackListener, Listener {
|
|||||||
|
|
||||||
databaseUpdateDisposable.clear();
|
databaseUpdateDisposable.clear();
|
||||||
progressUpdateDisposable.set(null);
|
progressUpdateDisposable.set(null);
|
||||||
cancelLoadingCurrentThumbnail();
|
|
||||||
|
|
||||||
UIs.destroyAll(Object.class); // destroy every UI: obviously every UI extends Object
|
UIs.destroyAllOfType(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setRecovery() {
|
public void setRecovery() {
|
||||||
@@ -786,67 +784,57 @@ public final class Player implements PlaybackListener, Listener {
|
|||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
//region Thumbnail loading
|
//region Thumbnail loading
|
||||||
|
|
||||||
private Target getCurrentThumbnailTarget() {
|
|
||||||
// a Picasso target is just a listener for thumbnail loading events
|
|
||||||
return new Target() {
|
|
||||||
@Override
|
|
||||||
public void onBitmapLoaded(final Bitmap bitmap, final Picasso.LoadedFrom from) {
|
|
||||||
if (DEBUG) {
|
|
||||||
Log.d(TAG, "Thumbnail - onBitmapLoaded() called with: bitmap = [" + bitmap
|
|
||||||
+ " -> " + bitmap.getWidth() + "x" + bitmap.getHeight() + "], from = ["
|
|
||||||
+ from + "]");
|
|
||||||
}
|
|
||||||
// there is a new thumbnail, so e.g. the end screen thumbnail needs to change, too.
|
|
||||||
onThumbnailLoaded(bitmap);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onBitmapFailed(final Exception e, final Drawable errorDrawable) {
|
|
||||||
Log.e(TAG, "Thumbnail - onBitmapFailed() called", e);
|
|
||||||
// there is a new thumbnail, so e.g. the end screen thumbnail needs to change, too.
|
|
||||||
onThumbnailLoaded(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onPrepareLoad(final Drawable placeHolderDrawable) {
|
|
||||||
if (DEBUG) {
|
|
||||||
Log.d(TAG, "Thumbnail - onPrepareLoad() called");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private void loadCurrentThumbnail(final List<Image> thumbnails) {
|
private void loadCurrentThumbnail(final List<Image> thumbnails) {
|
||||||
if (DEBUG) {
|
if (DEBUG) {
|
||||||
Log.d(TAG, "Thumbnail - loadCurrentThumbnail() called with thumbnails = ["
|
Log.d(TAG, "Thumbnail - loadCurrentThumbnail() called with thumbnails = ["
|
||||||
+ thumbnails.size() + "]");
|
+ thumbnails.size() + "]");
|
||||||
}
|
}
|
||||||
|
|
||||||
// first cancel any previous loading
|
// Cancel any ongoing image loading
|
||||||
cancelLoadingCurrentThumbnail();
|
if (thumbnailDisposable != null) {
|
||||||
|
thumbnailDisposable.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
// Unset currentThumbnail, since it is now outdated. This ensures it is not used in media
|
// Unset currentThumbnail, since it is now outdated. This ensures it is not used in media
|
||||||
// session metadata while the new thumbnail is being loaded by Picasso.
|
// session metadata while the new thumbnail is being loaded by Coil.
|
||||||
onThumbnailLoaded(null);
|
onThumbnailLoaded(null);
|
||||||
if (thumbnails.isEmpty()) {
|
if (thumbnails.isEmpty()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// scale down the notification thumbnail for performance
|
// scale down the notification thumbnail for performance
|
||||||
PicassoHelper.loadScaledDownThumbnail(context, thumbnails)
|
final var thumbnailTarget = new Target() {
|
||||||
.tag(PICASSO_PLAYER_THUMBNAIL_TAG)
|
@Override
|
||||||
.into(currentThumbnailTarget);
|
public void onError(@Nullable final coil3.Image error) {
|
||||||
}
|
Log.e(TAG, "Thumbnail - onError() called");
|
||||||
|
// there is a new thumbnail, so e.g. the end screen thumbnail needs to change, too.
|
||||||
|
onThumbnailLoaded(null);
|
||||||
|
}
|
||||||
|
|
||||||
private void cancelLoadingCurrentThumbnail() {
|
@Override
|
||||||
// cancel the Picasso job associated with the player thumbnail, if any
|
public void onStart(@Nullable final coil3.Image placeholder) {
|
||||||
PicassoHelper.cancelTag(PICASSO_PLAYER_THUMBNAIL_TAG);
|
if (DEBUG) {
|
||||||
|
Log.d(TAG, "Thumbnail - onStart() called");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onSuccess(@NonNull final coil3.Image result) {
|
||||||
|
if (DEBUG) {
|
||||||
|
Log.d(TAG, "Thumbnail - onSuccess() called with: drawable = [" + result + "]");
|
||||||
|
}
|
||||||
|
// there is a new thumbnail, so e.g. the end screen thumbnail needs to change, too.
|
||||||
|
onThumbnailLoaded(toBitmap(result));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
thumbnailDisposable = CoilHelper.INSTANCE
|
||||||
|
.loadScaledDownThumbnail(context, thumbnails, thumbnailTarget);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void onThumbnailLoaded(@Nullable final Bitmap bitmap) {
|
private void onThumbnailLoaded(@Nullable final Bitmap bitmap) {
|
||||||
// Avoid useless thumbnail updates, if the thumbnail has not actually changed. Based on the
|
// Avoid useless thumbnail updates, if the thumbnail has not actually changed. Based on the
|
||||||
// thumbnail loading code, this if would be skipped only when both bitmaps are `null`, since
|
// thumbnail loading code, this if would be skipped only when both bitmaps are `null`, since
|
||||||
// onThumbnailLoaded won't be called twice with the same nonnull bitmap by Picasso's target.
|
// onThumbnailLoaded won't be called twice with the same nonnull bitmap by Coil's target.
|
||||||
if (currentThumbnail != bitmap) {
|
if (currentThumbnail != bitmap) {
|
||||||
currentThumbnail = bitmap;
|
currentThumbnail = bitmap;
|
||||||
UIs.call(playerUi -> playerUi.onThumbnailLoaded(bitmap));
|
UIs.call(playerUi -> playerUi.onThumbnailLoaded(bitmap));
|
||||||
@@ -2013,6 +2001,10 @@ public final class Player implements PlaybackListener, Listener {
|
|||||||
triggerProgressUpdate();
|
triggerProgressUpdate();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove the listener, if it was set.
|
||||||
|
* @param listener listener to remove
|
||||||
|
* */
|
||||||
public void removeFragmentListener(final PlayerServiceEventListener listener) {
|
public void removeFragmentListener(final PlayerServiceEventListener listener) {
|
||||||
if (fragmentListener == listener) {
|
if (fragmentListener == listener) {
|
||||||
fragmentListener = null;
|
fragmentListener = null;
|
||||||
@@ -2027,6 +2019,10 @@ public final class Player implements PlaybackListener, Listener {
|
|||||||
triggerProgressUpdate();
|
triggerProgressUpdate();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove the listener, if it was set.
|
||||||
|
* @param listener listener to remove
|
||||||
|
* */
|
||||||
void removeActivityListener(final PlayerEventListener listener) {
|
void removeActivityListener(final PlayerEventListener listener) {
|
||||||
if (activityListener == listener) {
|
if (activityListener == listener) {
|
||||||
activityListener = null;
|
activityListener = null;
|
||||||
|
|||||||
@@ -16,103 +16,101 @@
|
|||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
package org.schabi.newpipe.player
|
||||||
|
|
||||||
package org.schabi.newpipe.player;
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
|
import android.os.Binder
|
||||||
|
import android.os.Bundle
|
||||||
import android.content.Context;
|
import android.os.IBinder
|
||||||
import android.content.Intent;
|
import android.support.v4.media.MediaBrowserCompat
|
||||||
import android.os.Binder;
|
import android.support.v4.media.session.MediaSessionCompat
|
||||||
import android.os.Bundle;
|
import android.util.Log
|
||||||
import android.os.IBinder;
|
import androidx.core.app.ServiceCompat
|
||||||
import android.support.v4.media.MediaBrowserCompat;
|
import androidx.media.MediaBrowserServiceCompat
|
||||||
import android.support.v4.media.session.MediaSessionCompat;
|
import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector
|
||||||
import android.util.Log;
|
import org.schabi.newpipe.ktx.toDebugString
|
||||||
|
import org.schabi.newpipe.player.mediabrowser.MediaBrowserImpl
|
||||||
import androidx.annotation.NonNull;
|
import org.schabi.newpipe.player.mediabrowser.MediaBrowserPlaybackPreparer
|
||||||
import androidx.annotation.Nullable;
|
import org.schabi.newpipe.player.mediasession.MediaSessionPlayerUi
|
||||||
import androidx.core.app.ServiceCompat;
|
import org.schabi.newpipe.player.notification.NotificationPlayerUi
|
||||||
import androidx.media.MediaBrowserServiceCompat;
|
import org.schabi.newpipe.util.Localization
|
||||||
|
import org.schabi.newpipe.util.ThemeHelper
|
||||||
import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector;
|
import java.lang.ref.WeakReference
|
||||||
|
import java.util.function.BiConsumer
|
||||||
import org.schabi.newpipe.ktx.BundleKt;
|
import java.util.function.Consumer
|
||||||
import org.schabi.newpipe.player.mediabrowser.MediaBrowserImpl;
|
|
||||||
import org.schabi.newpipe.player.mediabrowser.MediaBrowserPlaybackPreparer;
|
|
||||||
import org.schabi.newpipe.player.mediasession.MediaSessionPlayerUi;
|
|
||||||
import org.schabi.newpipe.player.notification.NotificationPlayerUi;
|
|
||||||
import org.schabi.newpipe.util.ThemeHelper;
|
|
||||||
|
|
||||||
import java.lang.ref.WeakReference;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.function.Consumer;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* One service for all players.
|
* One service for all players.
|
||||||
*/
|
*/
|
||||||
public final class PlayerService extends MediaBrowserServiceCompat {
|
class PlayerService : MediaBrowserServiceCompat() {
|
||||||
private static final String TAG = PlayerService.class.getSimpleName();
|
|
||||||
private static final boolean DEBUG = Player.DEBUG;
|
|
||||||
|
|
||||||
public static final String SHOULD_START_FOREGROUND_EXTRA = "should_start_foreground_extra";
|
|
||||||
public static final String BIND_PLAYER_HOLDER_ACTION = "bind_player_holder_action";
|
|
||||||
|
|
||||||
// These objects are used to cleanly separate the Service implementation (in this file) and the
|
// These objects are used to cleanly separate the Service implementation (in this file) and the
|
||||||
// media browser and playback preparer implementations. At the moment the playback preparer is
|
// media browser and playback preparer implementations. At the moment the playback preparer is
|
||||||
// only used in conjunction with the media browser.
|
// only used in conjunction with the media browser.
|
||||||
private MediaBrowserImpl mediaBrowserImpl;
|
private var mediaBrowserImpl: MediaBrowserImpl? = null
|
||||||
private MediaBrowserPlaybackPreparer mediaBrowserPlaybackPreparer;
|
private var mediaBrowserPlaybackPreparer: MediaBrowserPlaybackPreparer? = null
|
||||||
|
|
||||||
// these are instantiated in onCreate() as per
|
// these are instantiated in onCreate() as per
|
||||||
// https://developer.android.com/training/cars/media#browser_workflow
|
// https://developer.android.com/training/cars/media#browser_workflow
|
||||||
private MediaSessionCompat mediaSession;
|
private var mediaSession: MediaSessionCompat? = null
|
||||||
private MediaSessionConnector sessionConnector;
|
private var sessionConnector: MediaSessionConnector? = null
|
||||||
|
|
||||||
@Nullable
|
|
||||||
private Player player;
|
|
||||||
|
|
||||||
private final IBinder mBinder = new PlayerService.LocalBinder(this);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The parameter taken by this {@link Consumer} can be null to indicate the player is being
|
* @return the current active player instance. May be null, since the player service can outlive
|
||||||
|
* the player e.g. to respond to Android Auto media browser queries.
|
||||||
|
*/
|
||||||
|
var player: Player? = null
|
||||||
|
private set
|
||||||
|
|
||||||
|
private val mBinder: IBinder = LocalBinder(this)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The parameter taken by this [Consumer] can be null to indicate the player is being
|
||||||
* stopped.
|
* stopped.
|
||||||
*/
|
*/
|
||||||
@Nullable
|
private var onPlayerStartedOrStopped: Consumer<Player?>? = null
|
||||||
private Consumer<Player> onPlayerStartedOrStopped = null;
|
|
||||||
|
|
||||||
|
|
||||||
//region Service lifecycle
|
//region Service lifecycle
|
||||||
@Override
|
override fun onCreate() {
|
||||||
public void onCreate() {
|
super.onCreate()
|
||||||
super.onCreate();
|
|
||||||
|
|
||||||
if (DEBUG) {
|
if (DEBUG) {
|
||||||
Log.d(TAG, "onCreate() called");
|
Log.d(TAG, "onCreate() called")
|
||||||
}
|
}
|
||||||
assureCorrectAppLanguage(this);
|
Localization.assureCorrectAppLanguage(this)
|
||||||
ThemeHelper.setTheme(this);
|
ThemeHelper.setTheme(this)
|
||||||
|
|
||||||
mediaBrowserImpl = new MediaBrowserImpl(this, this::notifyChildrenChanged);
|
mediaBrowserImpl = MediaBrowserImpl(
|
||||||
|
this,
|
||||||
|
Consumer { parentId: String? ->
|
||||||
|
this.notifyChildrenChanged(
|
||||||
|
parentId!!
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
// see https://developer.android.com/training/cars/media#browser_workflow
|
// see https://developer.android.com/training/cars/media#browser_workflow
|
||||||
mediaSession = new MediaSessionCompat(this, "MediaSessionPlayerServ");
|
mediaSession = MediaSessionCompat(this, "MediaSessionPlayerServ")
|
||||||
setSessionToken(mediaSession.getSessionToken());
|
setSessionToken(mediaSession!!.sessionToken)
|
||||||
sessionConnector = new MediaSessionConnector(mediaSession);
|
sessionConnector = MediaSessionConnector(mediaSession!!)
|
||||||
sessionConnector.setMetadataDeduplicationEnabled(true);
|
sessionConnector!!.setMetadataDeduplicationEnabled(true)
|
||||||
|
|
||||||
mediaBrowserPlaybackPreparer = new MediaBrowserPlaybackPreparer(
|
mediaBrowserPlaybackPreparer = MediaBrowserPlaybackPreparer(
|
||||||
this,
|
this,
|
||||||
sessionConnector::setCustomErrorMessage,
|
BiConsumer { message: String?, code: Int? ->
|
||||||
() -> sessionConnector.setCustomErrorMessage(null),
|
sessionConnector!!.setCustomErrorMessage(
|
||||||
(playWhenReady) -> {
|
message,
|
||||||
if (player != null) {
|
code!!
|
||||||
player.onPrepare();
|
)
|
||||||
}
|
},
|
||||||
|
Runnable { sessionConnector!!.setCustomErrorMessage(null) },
|
||||||
|
Consumer { playWhenReady: Boolean? ->
|
||||||
|
if (player != null) {
|
||||||
|
player!!.onPrepare()
|
||||||
}
|
}
|
||||||
);
|
}
|
||||||
sessionConnector.setPlaybackPreparer(mediaBrowserPlaybackPreparer);
|
)
|
||||||
|
sessionConnector!!.setPlaybackPreparer(mediaBrowserPlaybackPreparer)
|
||||||
|
|
||||||
// Note: you might be tempted to create the player instance and call startForeground here,
|
// Note: you might be tempted to create the player instance and call startForeground here,
|
||||||
// but be aware that the Android system might start the service just to perform media
|
// but be aware that the Android system might start the service just to perform media
|
||||||
@@ -123,22 +121,26 @@ public final class PlayerService extends MediaBrowserServiceCompat {
|
|||||||
// useless notification.
|
// useless notification.
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
|
||||||
public int onStartCommand(final Intent intent, final int flags, final int startId) {
|
|
||||||
if (DEBUG) {
|
if (DEBUG) {
|
||||||
Log.d(TAG, "onStartCommand() called with: intent = [" + intent
|
Log.d(
|
||||||
+ "], extras = [" + BundleKt.toDebugString(intent.getExtras())
|
TAG,
|
||||||
+ "], flags = [" + flags + "], startId = [" + startId + "]");
|
(
|
||||||
|
"onStartCommand() called with: intent = [" + intent +
|
||||||
|
"], extras = [" + intent.extras.toDebugString() +
|
||||||
|
"], flags = [" + flags + "], startId = [" + startId + "]"
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// All internal NewPipe intents used to interact with the player, that are sent to the
|
// All internal NewPipe intents used to interact with the player, that are sent to the
|
||||||
// PlayerService using startForegroundService(), will have SHOULD_START_FOREGROUND_EXTRA,
|
// PlayerService using startForegroundService(), will have SHOULD_START_FOREGROUND_EXTRA,
|
||||||
// to ensure startForeground() is called (otherwise Android will force-crash the app).
|
// to ensure startForeground() is called (otherwise Android will force-crash the app).
|
||||||
if (intent.getBooleanExtra(SHOULD_START_FOREGROUND_EXTRA, false)) {
|
if (intent.getBooleanExtra(SHOULD_START_FOREGROUND_EXTRA, false)) {
|
||||||
final boolean playerWasNull = (player == null);
|
val playerWasNull = (player == null)
|
||||||
if (playerWasNull) {
|
if (playerWasNull) {
|
||||||
// make sure the player exists, in case the service was resumed
|
// make sure the player exists, in case the service was resumed
|
||||||
player = new Player(this, mediaSession, sessionConnector);
|
player = Player(this, mediaSession!!, sessionConnector!!)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Be sure that the player notification is set and the service is started in foreground,
|
// Be sure that the player notification is set and the service is started in foreground,
|
||||||
@@ -148,107 +150,107 @@ public final class PlayerService extends MediaBrowserServiceCompat {
|
|||||||
// no one already and starting the service in foreground should not create any issues.
|
// no one already and starting the service in foreground should not create any issues.
|
||||||
// If the service is already started in foreground, requesting it to be started
|
// If the service is already started in foreground, requesting it to be started
|
||||||
// shouldn't do anything.
|
// shouldn't do anything.
|
||||||
player.UIs().get(NotificationPlayerUi.class)
|
player!!.UIs().get(NotificationPlayerUi::class.java)
|
||||||
.ifPresent(NotificationPlayerUi::createNotificationAndStartForeground);
|
?.createNotificationAndStartForeground()
|
||||||
|
|
||||||
if (playerWasNull && onPlayerStartedOrStopped != null) {
|
if (playerWasNull && onPlayerStartedOrStopped != null) {
|
||||||
// notify that a new player was created (but do it after creating the foreground
|
// notify that a new player was created (but do it after creating the foreground
|
||||||
// notification just to make sure we don't incur, due to slowness, in
|
// notification just to make sure we don't incur, due to slowness, in
|
||||||
// "Context.startForegroundService() did not then call Service.startForeground()")
|
// "Context.startForegroundService() did not then call Service.startForeground()")
|
||||||
onPlayerStartedOrStopped.accept(player);
|
onPlayerStartedOrStopped!!.accept(player)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Intent.ACTION_MEDIA_BUTTON.equals(intent.getAction())
|
if (Intent.ACTION_MEDIA_BUTTON == intent.action &&
|
||||||
&& (player == null || player.getPlayQueue() == null)) {
|
(player == null || player!!.playQueue == null)
|
||||||
|
) {
|
||||||
/*
|
/*
|
||||||
No need to process media button's actions if the player is not working, otherwise
|
No need to process media button's actions if the player is not working, otherwise
|
||||||
the player service would strangely start with nothing to play
|
the player service would strangely start with nothing to play
|
||||||
Stop the service in this case, which will be removed from the foreground and its
|
Stop the service in this case, which will be removed from the foreground and its
|
||||||
notification cancelled in its destruction
|
notification cancelled in its destruction
|
||||||
*/
|
*/
|
||||||
destroyPlayerAndStopService();
|
destroyPlayerAndStopService()
|
||||||
return START_NOT_STICKY;
|
return START_NOT_STICKY
|
||||||
}
|
}
|
||||||
|
|
||||||
if (player != null) {
|
val p = player
|
||||||
player.handleIntent(intent);
|
if (p != null) {
|
||||||
player.UIs().get(MediaSessionPlayerUi.class)
|
p.handleIntent(intent)
|
||||||
.ifPresent(ui -> ui.handleMediaButtonIntent(intent));
|
p.UIs().get(MediaSessionPlayerUi::class.java)
|
||||||
|
?.handleMediaButtonIntent(intent)
|
||||||
}
|
}
|
||||||
|
|
||||||
return START_NOT_STICKY;
|
return START_NOT_STICKY
|
||||||
}
|
}
|
||||||
|
|
||||||
public void stopForImmediateReusing() {
|
fun stopForImmediateReusing() {
|
||||||
if (DEBUG) {
|
if (DEBUG) {
|
||||||
Log.d(TAG, "stopForImmediateReusing() called");
|
Log.d(TAG, "stopForImmediateReusing() called")
|
||||||
}
|
}
|
||||||
|
|
||||||
if (player != null && !player.exoPlayerIsNull()) {
|
if (player != null && !player!!.exoPlayerIsNull()) {
|
||||||
// Releases wifi & cpu, disables keepScreenOn, etc.
|
// Releases wifi & cpu, disables keepScreenOn, etc.
|
||||||
// We can't just pause the player here because it will make transition
|
// We can't just pause the player here because it will make transition
|
||||||
// from one stream to a new stream not smooth
|
// from one stream to a new stream not smooth
|
||||||
player.smoothStopForImmediateReusing();
|
player!!.smoothStopForImmediateReusing()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
override fun onTaskRemoved(rootIntent: Intent?) {
|
||||||
public void onTaskRemoved(final Intent rootIntent) {
|
super.onTaskRemoved(rootIntent)
|
||||||
super.onTaskRemoved(rootIntent);
|
if (player != null && !player!!.videoPlayerSelected()) {
|
||||||
if (player != null && !player.videoPlayerSelected()) {
|
return
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
onDestroy();
|
onDestroy()
|
||||||
// Unload from memory completely
|
// Unload from memory completely
|
||||||
Runtime.getRuntime().halt(0);
|
Runtime.getRuntime().halt(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
override fun onDestroy() {
|
||||||
public void onDestroy() {
|
|
||||||
if (DEBUG) {
|
if (DEBUG) {
|
||||||
Log.d(TAG, "destroy() called");
|
Log.d(TAG, "destroy() called")
|
||||||
}
|
}
|
||||||
super.onDestroy();
|
super.onDestroy()
|
||||||
|
|
||||||
cleanup();
|
cleanup()
|
||||||
|
|
||||||
mediaBrowserPlaybackPreparer.dispose();
|
mediaBrowserPlaybackPreparer!!.dispose()
|
||||||
mediaSession.release();
|
mediaSession!!.release()
|
||||||
mediaBrowserImpl.dispose();
|
mediaBrowserImpl!!.dispose()
|
||||||
}
|
}
|
||||||
|
|
||||||
private void cleanup() {
|
private fun cleanup() {
|
||||||
if (player != null) {
|
if (player != null) {
|
||||||
if (onPlayerStartedOrStopped != null) {
|
if (onPlayerStartedOrStopped != null) {
|
||||||
// notify that the player is being destroyed
|
// notify that the player is being destroyed
|
||||||
onPlayerStartedOrStopped.accept(null);
|
onPlayerStartedOrStopped!!.accept(null)
|
||||||
}
|
}
|
||||||
player.destroy();
|
player!!.saveAndShutdown()
|
||||||
player = null;
|
player = null
|
||||||
}
|
}
|
||||||
|
|
||||||
// Should already be handled by MediaSessionPlayerUi, but just to be sure.
|
// Should already be handled by MediaSessionPlayerUi, but just to be sure.
|
||||||
mediaSession.setActive(false);
|
mediaSession!!.setActive(false)
|
||||||
|
|
||||||
// Should already be handled by NotificationUtil.cancelNotificationAndStopForeground() in
|
// Should already be handled by NotificationUtil.cancelNotificationAndStopForeground() in
|
||||||
// NotificationPlayerUi, but let's make sure that the foreground service is stopped.
|
// NotificationPlayerUi, but let's make sure that the foreground service is stopped.
|
||||||
ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE);
|
ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Destroys the player and allows the player instance to be garbage collected. Sets the media
|
* Destroys the player and allows the player instance to be garbage collected. Sets the media
|
||||||
* session to inactive. Stops the foreground service and removes the player notification
|
* session to inactive. Stops the foreground service and removes the player notification
|
||||||
* associated with it. Tries to stop the {@link PlayerService} completely, but this step will
|
* associated with it. Tries to stop the [PlayerService] completely, but this step will
|
||||||
* have no effect in case some service connection still uses the service (e.g. the Android Auto
|
* have no effect in case some service connection still uses the service (e.g. the Android Auto
|
||||||
* system accesses the media browser even when no player is running).
|
* system accesses the media browser even when no player is running).
|
||||||
*/
|
*/
|
||||||
public void destroyPlayerAndStopService() {
|
fun destroyPlayerAndStopService() {
|
||||||
if (DEBUG) {
|
if (DEBUG) {
|
||||||
Log.d(TAG, "destroyPlayerAndStopService() called");
|
Log.d(TAG, "destroyPlayerAndStopService() called")
|
||||||
}
|
}
|
||||||
|
|
||||||
cleanup();
|
cleanup()
|
||||||
|
|
||||||
// This only really stops the service if there are no other service connections (see docs):
|
// This only really stops the service if there are no other service connections (see docs):
|
||||||
// for example the (Android Auto) media browser binder will block stopService().
|
// for example the (Android Auto) media browser binder will block stopService().
|
||||||
@@ -256,95 +258,96 @@ public final class PlayerService extends MediaBrowserServiceCompat {
|
|||||||
// If we were to call stopSelf(), then the service would be surely stopped (regardless of
|
// If we were to call stopSelf(), then the service would be surely stopped (regardless of
|
||||||
// other service connections), but this would be a waste of resources since the service
|
// other service connections), but this would be a waste of resources since the service
|
||||||
// would be immediately restarted by those same connections to perform the queries.
|
// would be immediately restarted by those same connections to perform the queries.
|
||||||
stopService(new Intent(this, PlayerService.class));
|
stopService(Intent(this, PlayerService::class.java))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
override fun attachBaseContext(base: Context?) {
|
||||||
protected void attachBaseContext(final Context base) {
|
super.attachBaseContext(AudioServiceLeakFix.preventLeakOf(base))
|
||||||
super.attachBaseContext(AudioServiceLeakFix.preventLeakOf(base));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//endregion
|
//endregion
|
||||||
|
|
||||||
//region Bind
|
//region Bind
|
||||||
@Override
|
override fun onBind(intent: Intent): IBinder? {
|
||||||
public IBinder onBind(final Intent intent) {
|
|
||||||
if (DEBUG) {
|
if (DEBUG) {
|
||||||
Log.d(TAG, "onBind() called with: intent = [" + intent
|
Log.d(
|
||||||
+ "], extras = [" + BundleKt.toDebugString(intent.getExtras()) + "]");
|
TAG,
|
||||||
|
(
|
||||||
|
"onBind() called with: intent = [" + intent +
|
||||||
|
"], extras = [" + intent.extras.toDebugString() + "]"
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (BIND_PLAYER_HOLDER_ACTION.equals(intent.getAction())) {
|
if (BIND_PLAYER_HOLDER_ACTION == intent.action) {
|
||||||
// Note that this binder might be reused multiple times while the service is alive, even
|
// Note that this binder might be reused multiple times while the service is alive, even
|
||||||
// after unbind() has been called: https://stackoverflow.com/a/8794930 .
|
// after unbind() has been called: https://stackoverflow.com/a/8794930 .
|
||||||
return mBinder;
|
return mBinder
|
||||||
|
} else if (SERVICE_INTERFACE == intent.action) {
|
||||||
} else if (MediaBrowserServiceCompat.SERVICE_INTERFACE.equals(intent.getAction())) {
|
|
||||||
// MediaBrowserService also uses its own binder, so for actions related to the media
|
// MediaBrowserService also uses its own binder, so for actions related to the media
|
||||||
// browser service, pass the onBind to the superclass.
|
// browser service, pass the onBind to the superclass.
|
||||||
return super.onBind(intent);
|
return super.onBind(intent)
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
// This is an unknown request, avoid returning any binder to not leak objects.
|
// This is an unknown request, avoid returning any binder to not leak objects.
|
||||||
return null;
|
return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class LocalBinder extends Binder {
|
class LocalBinder internal constructor(playerService: PlayerService) : Binder() {
|
||||||
private final WeakReference<PlayerService> playerService;
|
private val playerService: WeakReference<PlayerService?>
|
||||||
|
|
||||||
LocalBinder(final PlayerService playerService) {
|
init {
|
||||||
this.playerService = new WeakReference<>(playerService);
|
this.playerService = WeakReference<PlayerService?>(playerService)
|
||||||
}
|
}
|
||||||
|
|
||||||
public PlayerService getService() {
|
val service: PlayerService?
|
||||||
return playerService.get();
|
get() = playerService.get()
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return the current active player instance. May be null, since the player service can outlive
|
|
||||||
* the player e.g. to respond to Android Auto media browser queries.
|
|
||||||
*/
|
|
||||||
@Nullable
|
|
||||||
public Player getPlayer() {
|
|
||||||
return player;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the listener that will be called when the player is started or stopped. If a
|
* Sets the listener that will be called when the player is started or stopped. If a
|
||||||
* {@code null} listener is passed, then the current listener will be unset. The parameter taken
|
* `null` listener is passed, then the current listener will be unset. The parameter taken
|
||||||
* by the {@link Consumer} can be null to indicate that the player is stopping.
|
* by the [Consumer] can be null to indicate that the player is stopping.
|
||||||
* @param listener the listener to set or unset
|
* @param listener the listener to set or unset
|
||||||
*/
|
*/
|
||||||
public void setPlayerListener(@Nullable final Consumer<Player> listener) {
|
fun setPlayerListener(listener: Consumer<Player?>?) {
|
||||||
this.onPlayerStartedOrStopped = listener;
|
this.onPlayerStartedOrStopped = listener
|
||||||
if (listener != null) {
|
if (listener != null) {
|
||||||
// if there is no player, then `null` will be sent here, to ensure the state is synced
|
// if there is no player, then `null` will be sent here, to ensure the state is synced
|
||||||
listener.accept(player);
|
listener.accept(player)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
//endregion
|
|
||||||
|
|
||||||
|
//endregion
|
||||||
//region Media browser
|
//region Media browser
|
||||||
@Override
|
override fun onGetRoot(
|
||||||
public BrowserRoot onGetRoot(@NonNull final String clientPackageName,
|
clientPackageName: String,
|
||||||
final int clientUid,
|
clientUid: Int,
|
||||||
@Nullable final Bundle rootHints) {
|
rootHints: Bundle?
|
||||||
|
): BrowserRoot {
|
||||||
// TODO check if the accessing package has permission to view data
|
// TODO check if the accessing package has permission to view data
|
||||||
return mediaBrowserImpl.onGetRoot(clientPackageName, clientUid, rootHints);
|
return mediaBrowserImpl!!.onGetRoot(clientPackageName, clientUid, rootHints)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
override fun onLoadChildren(
|
||||||
public void onLoadChildren(@NonNull final String parentId,
|
parentId: String,
|
||||||
@NonNull final Result<List<MediaBrowserCompat.MediaItem>> result) {
|
result: Result<List<MediaBrowserCompat.MediaItem>>
|
||||||
mediaBrowserImpl.onLoadChildren(parentId, result);
|
) {
|
||||||
|
mediaBrowserImpl!!.onLoadChildren(parentId, result)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
override fun onSearch(
|
||||||
public void onSearch(@NonNull final String query,
|
query: String,
|
||||||
final Bundle extras,
|
extras: Bundle?,
|
||||||
@NonNull final Result<List<MediaBrowserCompat.MediaItem>> result) {
|
result: Result<List<MediaBrowserCompat.MediaItem>>
|
||||||
mediaBrowserImpl.onSearch(query, result);
|
) {
|
||||||
|
mediaBrowserImpl!!.onSearch(query, result)
|
||||||
|
} //endregion
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val TAG: String = PlayerService::class.java.getSimpleName()
|
||||||
|
private val DEBUG = Player.DEBUG
|
||||||
|
|
||||||
|
const val SHOULD_START_FOREGROUND_EXTRA: String = "should_start_foreground_extra"
|
||||||
|
const val BIND_PLAYER_HOLDER_ACTION: String = "bind_player_holder_action"
|
||||||
}
|
}
|
||||||
//endregion
|
|
||||||
}
|
}
|
||||||
@@ -5,6 +5,7 @@ import com.google.android.exoplayer2.PlaybackParameters;
|
|||||||
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||||
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
||||||
|
|
||||||
|
/** Player-specific events like queue or progress updates. */
|
||||||
public interface PlayerEventListener {
|
public interface PlayerEventListener {
|
||||||
void onQueueUpdate(PlayQueue queue);
|
void onQueueUpdate(PlayQueue queue);
|
||||||
void onPlaybackUpdate(int state, int repeatMode, boolean shuffled,
|
void onPlaybackUpdate(int state, int repeatMode, boolean shuffled,
|
||||||
|
|||||||
@@ -2,6 +2,9 @@ package org.schabi.newpipe.player.event;
|
|||||||
|
|
||||||
import com.google.android.exoplayer2.PlaybackException;
|
import com.google.android.exoplayer2.PlaybackException;
|
||||||
|
|
||||||
|
/** {@link org.schabi.newpipe.player.event.PlayerEventListener} that also gets called for
|
||||||
|
* application-specific events like screen rotation or UI changes.
|
||||||
|
*/
|
||||||
public interface PlayerServiceEventListener extends PlayerEventListener {
|
public interface PlayerServiceEventListener extends PlayerEventListener {
|
||||||
void onViewCreated();
|
void onViewCreated();
|
||||||
|
|
||||||
|
|||||||
@@ -117,9 +117,17 @@ public final class PlayerHolder {
|
|||||||
// helper to handle context in common place as using the same
|
// helper to handle context in common place as using the same
|
||||||
// context to bind/unbind a service is crucial
|
// context to bind/unbind a service is crucial
|
||||||
private Context getCommonContext() {
|
private Context getCommonContext() {
|
||||||
return App.getApp();
|
return App.getInstance();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Connect to (and if needed start) the {@link PlayerService}
|
||||||
|
* and bind {@link PlayerServiceConnection} to it.
|
||||||
|
* If the service is already started, only set the listener.
|
||||||
|
* @param playAfterConnect If this holder’s service was already started,
|
||||||
|
* start playing immediately
|
||||||
|
* @param newListener set this listener
|
||||||
|
* */
|
||||||
public void startService(final boolean playAfterConnect,
|
public void startService(final boolean playAfterConnect,
|
||||||
final PlayerServiceExtendedEventListener newListener) {
|
final PlayerServiceExtendedEventListener newListener) {
|
||||||
if (DEBUG) {
|
if (DEBUG) {
|
||||||
|
|||||||
@@ -138,7 +138,7 @@ public class MediaSessionPlayerUi extends PlayerUi
|
|||||||
public void play() {
|
public void play() {
|
||||||
player.play();
|
player.play();
|
||||||
// hide the player controls even if the play command came from the media session
|
// hide the player controls even if the play command came from the media session
|
||||||
player.UIs().get(VideoPlayerUi.class).ifPresent(ui -> ui.hideControls(0, 0));
|
player.UIs().getOpt(VideoPlayerUi.class).ifPresent(ui -> ui.hideControls(0, 0));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -102,7 +102,7 @@ public final class NotificationUtil {
|
|||||||
mediaStyle.setShowActionsInCompactView(compactSlots);
|
mediaStyle.setShowActionsInCompactView(compactSlots);
|
||||||
}
|
}
|
||||||
player.UIs()
|
player.UIs()
|
||||||
.get(MediaSessionPlayerUi.class)
|
.getOpt(MediaSessionPlayerUi.class)
|
||||||
.flatMap(MediaSessionPlayerUi::getSessionToken)
|
.flatMap(MediaSessionPlayerUi::getSessionToken)
|
||||||
.ifPresent(mediaStyle::setMediaSession);
|
.ifPresent(mediaStyle::setMediaSession);
|
||||||
|
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ import android.view.MotionEvent;
|
|||||||
import android.view.View;
|
import android.view.View;
|
||||||
|
|
||||||
import org.schabi.newpipe.util.Localization;
|
import org.schabi.newpipe.util.Localization;
|
||||||
import org.schabi.newpipe.util.image.PicassoHelper;
|
|
||||||
import org.schabi.newpipe.util.ServiceHelper;
|
import org.schabi.newpipe.util.ServiceHelper;
|
||||||
|
import org.schabi.newpipe.util.image.CoilHelper;
|
||||||
|
|
||||||
public class PlayQueueItemBuilder {
|
public class PlayQueueItemBuilder {
|
||||||
private static final String TAG = PlayQueueItemBuilder.class.toString();
|
private static final String TAG = PlayQueueItemBuilder.class.toString();
|
||||||
@@ -33,7 +33,7 @@ public class PlayQueueItemBuilder {
|
|||||||
holder.itemDurationView.setVisibility(View.GONE);
|
holder.itemDurationView.setVisibility(View.GONE);
|
||||||
}
|
}
|
||||||
|
|
||||||
PicassoHelper.loadThumbnail(item.getThumbnails()).into(holder.itemThumbnailView);
|
CoilHelper.INSTANCE.loadThumbnail(holder.itemThumbnailView, item.getThumbnails());
|
||||||
|
|
||||||
holder.itemRoot.setOnClickListener(view -> {
|
holder.itemRoot.setOnClickListener(view -> {
|
||||||
if (onItemClickListener != null) {
|
if (onItemClickListener != null) {
|
||||||
|
|||||||
@@ -13,8 +13,9 @@ import androidx.collection.SparseArrayCompat;
|
|||||||
|
|
||||||
import com.google.common.base.Stopwatch;
|
import com.google.common.base.Stopwatch;
|
||||||
|
|
||||||
|
import org.schabi.newpipe.App;
|
||||||
import org.schabi.newpipe.extractor.stream.Frameset;
|
import org.schabi.newpipe.extractor.stream.Frameset;
|
||||||
import org.schabi.newpipe.util.image.PicassoHelper;
|
import org.schabi.newpipe.util.image.CoilHelper;
|
||||||
|
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -207,8 +208,8 @@ public class SeekbarPreviewThumbnailHolder {
|
|||||||
Log.d(TAG, "Downloading bitmap for seekbarPreview from '" + url + "'");
|
Log.d(TAG, "Downloading bitmap for seekbarPreview from '" + url + "'");
|
||||||
|
|
||||||
// Gets the bitmap within the timeout of 15 seconds imposed by default by OkHttpClient
|
// Gets the bitmap within the timeout of 15 seconds imposed by default by OkHttpClient
|
||||||
// Ensure that your are not running on the main-Thread this will otherwise hang
|
// Ensure that you are not running on the main thread, otherwise this will hang
|
||||||
final Bitmap bitmap = PicassoHelper.loadSeekbarThumbnailPreview(url).get();
|
final var bitmap = CoilHelper.INSTANCE.loadBitmapBlocking(App.getInstance(), url);
|
||||||
|
|
||||||
if (sw != null) {
|
if (sw != null) {
|
||||||
Log.d(TAG, "Download of bitmap for seekbarPreview from '" + url + "' took "
|
Log.d(TAG, "Download of bitmap for seekbarPreview from '" + url + "' took "
|
||||||
|
|||||||
@@ -1,90 +0,0 @@
|
|||||||
package org.schabi.newpipe.player.ui;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Optional;
|
|
||||||
import java.util.function.Consumer;
|
|
||||||
|
|
||||||
public final class PlayerUiList {
|
|
||||||
final List<PlayerUi> playerUis = new ArrayList<>();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a {@link PlayerUiList} starting with the provided player uis. The provided player uis
|
|
||||||
* will not be prepared like those passed to {@link #addAndPrepare(PlayerUi)}, because when
|
|
||||||
* the {@link PlayerUiList} constructor is called, the player is still not running and it
|
|
||||||
* wouldn't make sense to initialize uis then. Instead the player will initialize them by doing
|
|
||||||
* proper calls to {@link #call(Consumer)}.
|
|
||||||
*
|
|
||||||
* @param initialPlayerUis the player uis this list should start with; the order will be kept
|
|
||||||
*/
|
|
||||||
public PlayerUiList(final PlayerUi... initialPlayerUis) {
|
|
||||||
playerUis.addAll(List.of(initialPlayerUis));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adds the provided player ui to the list and calls on it the initialization functions that
|
|
||||||
* apply based on the current player state. The preparation step needs to be done since when UIs
|
|
||||||
* are removed and re-added, the player will not call e.g. initPlayer again since the exoplayer
|
|
||||||
* is already initialized, but we need to notify the newly built UI that the player is ready
|
|
||||||
* nonetheless.
|
|
||||||
* @param playerUi the player ui to prepare and add to the list; its {@link
|
|
||||||
* PlayerUi#getPlayer()} will be used to query information about the player
|
|
||||||
* state
|
|
||||||
*/
|
|
||||||
public void addAndPrepare(final PlayerUi playerUi) {
|
|
||||||
if (playerUi.getPlayer().getFragmentListener().isPresent()) {
|
|
||||||
// make sure UIs know whether a service is connected or not
|
|
||||||
playerUi.onFragmentListenerSet();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!playerUi.getPlayer().exoPlayerIsNull()) {
|
|
||||||
playerUi.initPlayer();
|
|
||||||
if (playerUi.getPlayer().getPlayQueue() != null) {
|
|
||||||
playerUi.initPlayback();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
playerUis.add(playerUi);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Destroys all matching player UIs and removes them from the list.
|
|
||||||
* @param playerUiType the class of the player UI to destroy; the {@link
|
|
||||||
* Class#isInstance(Object)} method will be used, so even subclasses will be
|
|
||||||
* destroyed and removed
|
|
||||||
* @param <T> the class type parameter
|
|
||||||
*/
|
|
||||||
public <T> void destroyAll(final Class<T> playerUiType) {
|
|
||||||
playerUis.stream()
|
|
||||||
.filter(playerUiType::isInstance)
|
|
||||||
.forEach(playerUi -> {
|
|
||||||
playerUi.destroyPlayer();
|
|
||||||
playerUi.destroy();
|
|
||||||
});
|
|
||||||
playerUis.removeIf(playerUiType::isInstance);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param playerUiType the class of the player UI to return; the {@link
|
|
||||||
* Class#isInstance(Object)} method will be used, so even subclasses could
|
|
||||||
* be returned
|
|
||||||
* @param <T> the class type parameter
|
|
||||||
* @return the first player UI of the required type found in the list, or an empty {@link
|
|
||||||
* Optional} otherwise
|
|
||||||
*/
|
|
||||||
public <T> Optional<T> get(final Class<T> playerUiType) {
|
|
||||||
return playerUis.stream()
|
|
||||||
.filter(playerUiType::isInstance)
|
|
||||||
.map(playerUiType::cast)
|
|
||||||
.findFirst();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Calls the provided consumer on all player UIs in the list, in order of addition.
|
|
||||||
* @param consumer the consumer to call with player UIs
|
|
||||||
*/
|
|
||||||
public void call(final Consumer<PlayerUi> consumer) {
|
|
||||||
//noinspection SimplifyStreamApiCallChains
|
|
||||||
playerUis.stream().forEachOrdered(consumer);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
124
app/src/main/java/org/schabi/newpipe/player/ui/PlayerUiList.kt
Normal file
124
app/src/main/java/org/schabi/newpipe/player/ui/PlayerUiList.kt
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
package org.schabi.newpipe.player.ui
|
||||||
|
|
||||||
|
import org.schabi.newpipe.util.GuardedByMutex
|
||||||
|
import java.util.Optional
|
||||||
|
|
||||||
|
class PlayerUiList(vararg initialPlayerUis: PlayerUi) {
|
||||||
|
private var playerUis = GuardedByMutex(mutableListOf<PlayerUi>())
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a [PlayerUiList] starting with the provided player uis. The provided player uis
|
||||||
|
* will not be prepared like those passed to [.addAndPrepare], because when
|
||||||
|
* the [PlayerUiList] constructor is called, the player is still not running and it
|
||||||
|
* wouldn't make sense to initialize uis then. Instead the player will initialize them by doing
|
||||||
|
* proper calls to [.call].
|
||||||
|
*
|
||||||
|
* @param initialPlayerUis the player uis this list should start with; the order will be kept
|
||||||
|
*/
|
||||||
|
init {
|
||||||
|
playerUis.runWithLockSync {
|
||||||
|
lockData.addAll(listOf(*initialPlayerUis))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds the provided player ui to the list and calls on it the initialization functions that
|
||||||
|
* apply based on the current player state. The preparation step needs to be done since when UIs
|
||||||
|
* are removed and re-added, the player will not call e.g. initPlayer again since the exoplayer
|
||||||
|
* is already initialized, but we need to notify the newly built UI that the player is ready
|
||||||
|
* nonetheless.
|
||||||
|
* @param playerUi the player ui to prepare and add to the list; its [PlayerUi.getPlayer]
|
||||||
|
* will be used to query information about the player state
|
||||||
|
*/
|
||||||
|
fun addAndPrepare(playerUi: PlayerUi) {
|
||||||
|
if (playerUi.getPlayer().fragmentListener.isPresent) {
|
||||||
|
// make sure UIs know whether a service is connected or not
|
||||||
|
playerUi.onFragmentListenerSet()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!playerUi.getPlayer().exoPlayerIsNull()) {
|
||||||
|
playerUi.initPlayer()
|
||||||
|
if (playerUi.getPlayer().playQueue != null) {
|
||||||
|
playerUi.initPlayback()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
playerUis.runWithLockSync {
|
||||||
|
lockData.add(playerUi)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Destroys all matching player UIs and removes them from the list.
|
||||||
|
* @param playerUiType the class of the player UI to destroy, everything if `null`.
|
||||||
|
* The [Class.isInstance] method will be used, so even subclasses will be
|
||||||
|
* destroyed and removed
|
||||||
|
* @param T the class type parameter </T>
|
||||||
|
* */
|
||||||
|
fun <T : PlayerUi> destroyAllOfType(playerUiType: Class<T>? = null) {
|
||||||
|
val toDestroy = mutableListOf<PlayerUi>()
|
||||||
|
|
||||||
|
// short blocking removal from class to prevent interfering from other threads
|
||||||
|
playerUis.runWithLockSync {
|
||||||
|
val new = mutableListOf<PlayerUi>()
|
||||||
|
for (ui in lockData) {
|
||||||
|
if (playerUiType == null || playerUiType.isInstance(ui)) {
|
||||||
|
toDestroy.add(ui)
|
||||||
|
} else {
|
||||||
|
new.add(ui)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lockData = new
|
||||||
|
}
|
||||||
|
// then actually destroy the UIs
|
||||||
|
for (ui in toDestroy) {
|
||||||
|
ui.destroyPlayer()
|
||||||
|
ui.destroy()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param playerUiType the class of the player UI to return;
|
||||||
|
* the [Class.isInstance] method will be used, so even subclasses could be returned
|
||||||
|
* @param T the class type parameter
|
||||||
|
* @return the first player UI of the required type found in the list, or null
|
||||||
|
</T> */
|
||||||
|
fun <T : PlayerUi> get(playerUiType: Class<T>): T? =
|
||||||
|
playerUis.runWithLockSync {
|
||||||
|
for (ui in lockData) {
|
||||||
|
if (playerUiType.isInstance(ui)) {
|
||||||
|
when (val r = playerUiType.cast(ui)) {
|
||||||
|
// try all UIs before returning null
|
||||||
|
null -> continue
|
||||||
|
else -> return@runWithLockSync r
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return@runWithLockSync null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param playerUiType the class of the player UI to return;
|
||||||
|
* the [Class.isInstance] method will be used, so even subclasses could be returned
|
||||||
|
* @param T the class type parameter
|
||||||
|
* @return the first player UI of the required type found in the list, or an empty
|
||||||
|
* [Optional] otherwise
|
||||||
|
</T> */
|
||||||
|
@Deprecated("use get", ReplaceWith("get(playerUiType)"))
|
||||||
|
fun <T : PlayerUi> getOpt(playerUiType: Class<T>): Optional<T> =
|
||||||
|
Optional.ofNullable(get(playerUiType))
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calls the provided consumer on all player UIs in the list, in order of addition.
|
||||||
|
* @param consumer the consumer to call with player UIs
|
||||||
|
*/
|
||||||
|
fun call(consumer: java.util.function.Consumer<PlayerUi>) {
|
||||||
|
// copy the list out of the mutex before calling the consumer which might block
|
||||||
|
val new = playerUis.runWithLockSync {
|
||||||
|
lockData.toMutableList()
|
||||||
|
}
|
||||||
|
for (ui in new) {
|
||||||
|
consumer.accept(ui)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,6 +16,7 @@ import static org.schabi.newpipe.player.helper.PlayerHelper.getTimeString;
|
|||||||
import static org.schabi.newpipe.player.helper.PlayerHelper.nextResizeModeAndSaveToPrefs;
|
import static org.schabi.newpipe.player.helper.PlayerHelper.nextResizeModeAndSaveToPrefs;
|
||||||
import static org.schabi.newpipe.player.helper.PlayerHelper.retrieveSeekDurationFromPreferences;
|
import static org.schabi.newpipe.player.helper.PlayerHelper.retrieveSeekDurationFromPreferences;
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.res.Resources;
|
import android.content.res.Resources;
|
||||||
import android.graphics.Bitmap;
|
import android.graphics.Bitmap;
|
||||||
@@ -761,7 +762,7 @@ public abstract class VideoPlayerUi extends PlayerUi implements SeekBar.OnSeekBa
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update the play/pause button ({@link R.id.playPauseButton}) to reflect the action
|
* Update the play/pause button (`R.id.playPauseButton`) to reflect the action
|
||||||
* that will be performed when the button is clicked..
|
* that will be performed when the button is clicked..
|
||||||
* @param action the action that is performed when the play/pause button is clicked
|
* @param action the action that is performed when the play/pause button is clicked
|
||||||
*/
|
*/
|
||||||
@@ -947,6 +948,8 @@ public abstract class VideoPlayerUi extends PlayerUi implements SeekBar.OnSeekBa
|
|||||||
player.toggleShuffleModeEnabled();
|
player.toggleShuffleModeEnabled();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: don’t reference internal exoplayer2 resources
|
||||||
|
@SuppressLint("PrivateResource")
|
||||||
@Override
|
@Override
|
||||||
public void onRepeatModeChanged(@RepeatMode final int repeatMode) {
|
public void onRepeatModeChanged(@RepeatMode final int repeatMode) {
|
||||||
super.onRepeatModeChanged(repeatMode);
|
super.onRepeatModeChanged(repeatMode);
|
||||||
|
|||||||
@@ -18,12 +18,12 @@ import org.schabi.newpipe.extractor.NewPipe;
|
|||||||
import org.schabi.newpipe.extractor.localization.ContentCountry;
|
import org.schabi.newpipe.extractor.localization.ContentCountry;
|
||||||
import org.schabi.newpipe.extractor.localization.Localization;
|
import org.schabi.newpipe.extractor.localization.Localization;
|
||||||
import org.schabi.newpipe.util.image.ImageStrategy;
|
import org.schabi.newpipe.util.image.ImageStrategy;
|
||||||
import org.schabi.newpipe.util.image.PicassoHelper;
|
|
||||||
import org.schabi.newpipe.util.image.PreferredImageQuality;
|
import org.schabi.newpipe.util.image.PreferredImageQuality;
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
|
|
||||||
|
import coil3.SingletonImageLoader;
|
||||||
|
|
||||||
public class ContentSettingsFragment extends BasePreferenceFragment {
|
public class ContentSettingsFragment extends BasePreferenceFragment {
|
||||||
private String youtubeRestrictedModeEnabledKey;
|
private String youtubeRestrictedModeEnabledKey;
|
||||||
|
|
||||||
@@ -62,14 +62,13 @@ public class ContentSettingsFragment extends BasePreferenceFragment {
|
|||||||
(preference, newValue) -> {
|
(preference, newValue) -> {
|
||||||
ImageStrategy.setPreferredImageQuality(PreferredImageQuality
|
ImageStrategy.setPreferredImageQuality(PreferredImageQuality
|
||||||
.fromPreferenceKey(requireContext(), (String) newValue));
|
.fromPreferenceKey(requireContext(), (String) newValue));
|
||||||
try {
|
final var loader = SingletonImageLoader.get(preference.getContext());
|
||||||
PicassoHelper.clearCache(preference.getContext());
|
loader.getMemoryCache().clear();
|
||||||
Toast.makeText(preference.getContext(),
|
loader.getDiskCache().clear();
|
||||||
R.string.thumbnail_cache_wipe_complete_notice, Toast.LENGTH_SHORT)
|
Toast.makeText(preference.getContext(),
|
||||||
.show();
|
R.string.thumbnail_cache_wipe_complete_notice, Toast.LENGTH_SHORT)
|
||||||
} catch (final IOException e) {
|
.show();
|
||||||
Log.e(TAG, "Unable to clear Picasso cache", e);
|
|
||||||
}
|
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
27
app/src/main/java/org/schabi/newpipe/settings/DebugScreen.kt
Normal file
27
app/src/main/java/org/schabi/newpipe/settings/DebugScreen.kt
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
package org.schabi.newpipe.settings
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import org.schabi.newpipe.R
|
||||||
|
import org.schabi.newpipe.settings.viewmodel.SettingsViewModel
|
||||||
|
import org.schabi.newpipe.ui.SwitchPreference
|
||||||
|
import org.schabi.newpipe.ui.theme.SizeTokens
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun DebugScreen(viewModel: SettingsViewModel, modifier: Modifier = Modifier) {
|
||||||
|
|
||||||
|
val settingsLayoutRedesign by viewModel.settingsLayoutRedesign.collectAsState()
|
||||||
|
|
||||||
|
Column(modifier = modifier) {
|
||||||
|
SwitchPreference(
|
||||||
|
modifier = Modifier.padding(SizeTokens.SpacingExtraSmall),
|
||||||
|
R.string.settings_layout_redesign,
|
||||||
|
settingsLayoutRedesign,
|
||||||
|
viewModel::toggleSettingsLayoutRedesign
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,7 +10,6 @@ import org.schabi.newpipe.error.ErrorInfo;
|
|||||||
import org.schabi.newpipe.error.ErrorUtil;
|
import org.schabi.newpipe.error.ErrorUtil;
|
||||||
import org.schabi.newpipe.error.UserAction;
|
import org.schabi.newpipe.error.UserAction;
|
||||||
import org.schabi.newpipe.local.feed.notifications.NotificationWorker;
|
import org.schabi.newpipe.local.feed.notifications.NotificationWorker;
|
||||||
import org.schabi.newpipe.util.image.PicassoHelper;
|
|
||||||
|
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
@@ -25,8 +24,6 @@ public class DebugSettingsFragment extends BasePreferenceFragment {
|
|||||||
findPreference(getString(R.string.allow_heap_dumping_key));
|
findPreference(getString(R.string.allow_heap_dumping_key));
|
||||||
final Preference showMemoryLeaksPreference =
|
final Preference showMemoryLeaksPreference =
|
||||||
findPreference(getString(R.string.show_memory_leaks_key));
|
findPreference(getString(R.string.show_memory_leaks_key));
|
||||||
final Preference showImageIndicatorsPreference =
|
|
||||||
findPreference(getString(R.string.show_image_indicators_key));
|
|
||||||
final Preference checkNewStreamsPreference =
|
final Preference checkNewStreamsPreference =
|
||||||
findPreference(getString(R.string.check_new_streams_key));
|
findPreference(getString(R.string.check_new_streams_key));
|
||||||
final Preference crashTheAppPreference =
|
final Preference crashTheAppPreference =
|
||||||
@@ -38,7 +35,6 @@ public class DebugSettingsFragment extends BasePreferenceFragment {
|
|||||||
|
|
||||||
assert allowHeapDumpingPreference != null;
|
assert allowHeapDumpingPreference != null;
|
||||||
assert showMemoryLeaksPreference != null;
|
assert showMemoryLeaksPreference != null;
|
||||||
assert showImageIndicatorsPreference != null;
|
|
||||||
assert checkNewStreamsPreference != null;
|
assert checkNewStreamsPreference != null;
|
||||||
assert crashTheAppPreference != null;
|
assert crashTheAppPreference != null;
|
||||||
assert showErrorSnackbarPreference != null;
|
assert showErrorSnackbarPreference != null;
|
||||||
@@ -61,11 +57,6 @@ public class DebugSettingsFragment extends BasePreferenceFragment {
|
|||||||
showMemoryLeaksPreference.setSummary(R.string.leak_canary_not_available);
|
showMemoryLeaksPreference.setSummary(R.string.leak_canary_not_available);
|
||||||
}
|
}
|
||||||
|
|
||||||
showImageIndicatorsPreference.setOnPreferenceChangeListener((preference, newValue) -> {
|
|
||||||
PicassoHelper.setIndicatorsEnabled((Boolean) newValue);
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
|
|
||||||
checkNewStreamsPreference.setOnPreferenceClickListener(preference -> {
|
checkNewStreamsPreference.setOnPreferenceClickListener(preference -> {
|
||||||
NotificationWorker.runNow(preference.getContext());
|
NotificationWorker.runNow(preference.getContext());
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -156,7 +156,7 @@ public final class NewPipeSettings {
|
|||||||
prefs.getInt(disabledTunnelingAutomaticallyKey, -1) == 0
|
prefs.getInt(disabledTunnelingAutomaticallyKey, -1) == 0
|
||||||
&& !prefs.getBoolean(disabledTunnelingKey, false);
|
&& !prefs.getBoolean(disabledTunnelingKey, false);
|
||||||
|
|
||||||
if (App.getApp().isFirstRun()
|
if (App.getInstance().isFirstRun()
|
||||||
|| (wasDeviceBlacklistUpdated && !wasMediaTunnelingEnabledByUser)) {
|
|| (wasDeviceBlacklistUpdated && !wasMediaTunnelingEnabledByUser)) {
|
||||||
setMediaTunneling(context);
|
setMediaTunneling(context);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import android.widget.TextView;
|
|||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.compose.ui.platform.ComposeView;
|
||||||
import androidx.fragment.app.DialogFragment;
|
import androidx.fragment.app.DialogFragment;
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
@@ -19,8 +20,10 @@ import org.schabi.newpipe.R;
|
|||||||
import org.schabi.newpipe.database.subscription.SubscriptionEntity;
|
import org.schabi.newpipe.database.subscription.SubscriptionEntity;
|
||||||
import org.schabi.newpipe.error.ErrorUtil;
|
import org.schabi.newpipe.error.ErrorUtil;
|
||||||
import org.schabi.newpipe.local.subscription.SubscriptionManager;
|
import org.schabi.newpipe.local.subscription.SubscriptionManager;
|
||||||
import org.schabi.newpipe.util.image.PicassoHelper;
|
import org.schabi.newpipe.ui.emptystate.EmptyStateSpec;
|
||||||
|
import org.schabi.newpipe.ui.emptystate.EmptyStateUtil;
|
||||||
import org.schabi.newpipe.util.ThemeHelper;
|
import org.schabi.newpipe.util.ThemeHelper;
|
||||||
|
import org.schabi.newpipe.util.image.CoilHelper;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Vector;
|
import java.util.Vector;
|
||||||
@@ -57,7 +60,7 @@ public class SelectChannelFragment extends DialogFragment {
|
|||||||
private OnCancelListener onCancelListener = null;
|
private OnCancelListener onCancelListener = null;
|
||||||
|
|
||||||
private ProgressBar progressBar;
|
private ProgressBar progressBar;
|
||||||
private TextView emptyView;
|
private ComposeView emptyView;
|
||||||
private RecyclerView recyclerView;
|
private RecyclerView recyclerView;
|
||||||
|
|
||||||
private List<SubscriptionEntity> subscriptions = new Vector<>();
|
private List<SubscriptionEntity> subscriptions = new Vector<>();
|
||||||
@@ -91,6 +94,9 @@ public class SelectChannelFragment extends DialogFragment {
|
|||||||
|
|
||||||
progressBar = v.findViewById(R.id.progressBar);
|
progressBar = v.findViewById(R.id.progressBar);
|
||||||
emptyView = v.findViewById(R.id.empty_state_view);
|
emptyView = v.findViewById(R.id.empty_state_view);
|
||||||
|
|
||||||
|
EmptyStateUtil.setEmptyStateComposable(emptyView,
|
||||||
|
EmptyStateSpec.Companion.getNoSubscriptions());
|
||||||
progressBar.setVisibility(View.VISIBLE);
|
progressBar.setVisibility(View.VISIBLE);
|
||||||
recyclerView.setVisibility(View.GONE);
|
recyclerView.setVisibility(View.GONE);
|
||||||
emptyView.setVisibility(View.GONE);
|
emptyView.setVisibility(View.GONE);
|
||||||
@@ -190,7 +196,7 @@ public class SelectChannelFragment extends DialogFragment {
|
|||||||
final SubscriptionEntity entry = subscriptions.get(position);
|
final SubscriptionEntity entry = subscriptions.get(position);
|
||||||
holder.titleView.setText(entry.getName());
|
holder.titleView.setText(entry.getName());
|
||||||
holder.view.setOnClickListener(view -> clickedItem(position));
|
holder.view.setOnClickListener(view -> clickedItem(position));
|
||||||
PicassoHelper.loadAvatar(entry.getAvatarUrl()).into(holder.thumbnailView);
|
CoilHelper.INSTANCE.loadAvatar(holder.thumbnailView, entry.getAvatarUrl());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import android.widget.ProgressBar;
|
|||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.compose.ui.platform.ComposeView;
|
||||||
import androidx.fragment.app.DialogFragment;
|
import androidx.fragment.app.DialogFragment;
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
@@ -27,7 +28,9 @@ import org.schabi.newpipe.error.ErrorUtil;
|
|||||||
import org.schabi.newpipe.error.UserAction;
|
import org.schabi.newpipe.error.UserAction;
|
||||||
import org.schabi.newpipe.local.playlist.LocalPlaylistManager;
|
import org.schabi.newpipe.local.playlist.LocalPlaylistManager;
|
||||||
import org.schabi.newpipe.local.playlist.RemotePlaylistManager;
|
import org.schabi.newpipe.local.playlist.RemotePlaylistManager;
|
||||||
import org.schabi.newpipe.util.image.PicassoHelper;
|
import org.schabi.newpipe.ui.emptystate.EmptyStateSpec;
|
||||||
|
import org.schabi.newpipe.ui.emptystate.EmptyStateUtil;
|
||||||
|
import org.schabi.newpipe.util.image.CoilHelper;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Vector;
|
import java.util.Vector;
|
||||||
@@ -40,7 +43,7 @@ public class SelectPlaylistFragment extends DialogFragment {
|
|||||||
private OnSelectedListener onSelectedListener = null;
|
private OnSelectedListener onSelectedListener = null;
|
||||||
|
|
||||||
private ProgressBar progressBar;
|
private ProgressBar progressBar;
|
||||||
private TextView emptyView;
|
private ComposeView emptyView;
|
||||||
private RecyclerView recyclerView;
|
private RecyclerView recyclerView;
|
||||||
private Disposable disposable = null;
|
private Disposable disposable = null;
|
||||||
|
|
||||||
@@ -62,6 +65,8 @@ public class SelectPlaylistFragment extends DialogFragment {
|
|||||||
recyclerView = v.findViewById(R.id.items_list);
|
recyclerView = v.findViewById(R.id.items_list);
|
||||||
emptyView = v.findViewById(R.id.empty_state_view);
|
emptyView = v.findViewById(R.id.empty_state_view);
|
||||||
|
|
||||||
|
EmptyStateUtil.setEmptyStateComposable(emptyView,
|
||||||
|
EmptyStateSpec.Companion.getNoBookmarkedPlaylist());
|
||||||
recyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
|
recyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
|
||||||
final SelectPlaylistAdapter playlistAdapter = new SelectPlaylistAdapter();
|
final SelectPlaylistAdapter playlistAdapter = new SelectPlaylistAdapter();
|
||||||
recyclerView.setAdapter(playlistAdapter);
|
recyclerView.setAdapter(playlistAdapter);
|
||||||
@@ -154,20 +159,15 @@ public class SelectPlaylistFragment extends DialogFragment {
|
|||||||
final int position) {
|
final int position) {
|
||||||
final PlaylistLocalItem selectedItem = playlists.get(position);
|
final PlaylistLocalItem selectedItem = playlists.get(position);
|
||||||
|
|
||||||
if (selectedItem instanceof PlaylistMetadataEntry) {
|
if (selectedItem instanceof PlaylistMetadataEntry entry) {
|
||||||
final PlaylistMetadataEntry entry = ((PlaylistMetadataEntry) selectedItem);
|
|
||||||
|
|
||||||
holder.titleView.setText(entry.name);
|
holder.titleView.setText(entry.name);
|
||||||
holder.view.setOnClickListener(view -> clickedItem(position));
|
holder.view.setOnClickListener(view -> clickedItem(position));
|
||||||
PicassoHelper.loadPlaylistThumbnail(entry.thumbnailUrl).into(holder.thumbnailView);
|
CoilHelper.INSTANCE.loadPlaylistThumbnail(holder.thumbnailView, entry.thumbnailUrl);
|
||||||
|
} else if (selectedItem instanceof PlaylistRemoteEntity entry) {
|
||||||
} else if (selectedItem instanceof PlaylistRemoteEntity) {
|
|
||||||
final PlaylistRemoteEntity entry = ((PlaylistRemoteEntity) selectedItem);
|
|
||||||
|
|
||||||
holder.titleView.setText(entry.getName());
|
holder.titleView.setText(entry.getName());
|
||||||
holder.view.setOnClickListener(view -> clickedItem(position));
|
holder.view.setOnClickListener(view -> clickedItem(position));
|
||||||
PicassoHelper.loadPlaylistThumbnail(entry.getThumbnailUrl())
|
CoilHelper.INSTANCE.loadPlaylistThumbnail(holder.thumbnailView,
|
||||||
.into(holder.thumbnailView);
|
entry.getThumbnailUrl());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
package org.schabi.newpipe.settings;
|
package org.schabi.newpipe.settings;
|
||||||
|
|
||||||
|
import static org.schabi.newpipe.MainActivity.DEBUG;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
@@ -18,8 +20,6 @@ import java.util.Collections;
|
|||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
import static org.schabi.newpipe.MainActivity.DEBUG;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* In order to add a migration, follow these steps, given P is the previous version:<br>
|
* In order to add a migration, follow these steps, given P is the previous version:<br>
|
||||||
* - in the class body add a new {@code MIGRATION_P_P+1 = new Migration(P, P+1) { ... }} and put in
|
* - in the class body add a new {@code MIGRATION_P_P+1 = new Migration(P, P+1) { ... }} and put in
|
||||||
@@ -171,7 +171,7 @@ public final class SettingMigrations {
|
|||||||
final int lastPrefVersion = sp.getInt(lastPrefVersionKey, 0);
|
final int lastPrefVersion = sp.getInt(lastPrefVersionKey, 0);
|
||||||
|
|
||||||
// no migration to run, already up to date
|
// no migration to run, already up to date
|
||||||
if (App.getApp().isFirstRun()) {
|
if (App.getInstance().isFirstRun()) {
|
||||||
sp.edit().putInt(lastPrefVersionKey, VERSION).apply();
|
sp.edit().putInt(lastPrefVersionKey, VERSION).apply();
|
||||||
return;
|
return;
|
||||||
} else if (lastPrefVersion == VERSION) {
|
} else if (lastPrefVersion == VERSION) {
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package org.schabi.newpipe.settings
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.material3.HorizontalDivider
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import org.schabi.newpipe.R
|
||||||
|
import org.schabi.newpipe.ui.TextPreference
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SettingsScreen(
|
||||||
|
onSelectSettingOption: (SettingsScreenKey) -> Unit,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
Column(modifier = modifier) {
|
||||||
|
TextPreference(
|
||||||
|
title = R.string.settings_category_debug_title,
|
||||||
|
onClick = { onSelectSettingOption(SettingsScreenKey.DEBUG) }
|
||||||
|
)
|
||||||
|
HorizontalDivider(color = Color.Black)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
package org.schabi.newpipe.settings
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import androidx.activity.ComponentActivity
|
||||||
|
import androidx.activity.compose.setContent
|
||||||
|
import androidx.activity.viewModels
|
||||||
|
import androidx.annotation.StringRes
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableIntStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.navigation.compose.NavHost
|
||||||
|
import androidx.navigation.compose.composable
|
||||||
|
import androidx.navigation.compose.rememberNavController
|
||||||
|
import androidx.navigation.navArgument
|
||||||
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
|
import org.schabi.newpipe.R
|
||||||
|
import org.schabi.newpipe.settings.viewmodel.SettingsViewModel
|
||||||
|
import org.schabi.newpipe.ui.Toolbar
|
||||||
|
import org.schabi.newpipe.ui.theme.AppTheme
|
||||||
|
|
||||||
|
const val SCREEN_TITLE_KEY = "SCREEN_TITLE_KEY"
|
||||||
|
|
||||||
|
@AndroidEntryPoint
|
||||||
|
class SettingsV2Activity : ComponentActivity() {
|
||||||
|
|
||||||
|
private val settingsViewModel: SettingsViewModel by viewModels()
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
setContent {
|
||||||
|
val navController = rememberNavController()
|
||||||
|
var screenTitle by remember { mutableIntStateOf(SettingsScreenKey.ROOT.screenTitle) }
|
||||||
|
navController.addOnDestinationChangedListener { _, _, arguments ->
|
||||||
|
screenTitle =
|
||||||
|
arguments?.getInt(SCREEN_TITLE_KEY) ?: SettingsScreenKey.ROOT.screenTitle
|
||||||
|
}
|
||||||
|
|
||||||
|
AppTheme {
|
||||||
|
Scaffold(topBar = {
|
||||||
|
Toolbar(
|
||||||
|
title = stringResource(id = screenTitle),
|
||||||
|
hasSearch = true,
|
||||||
|
onSearchQueryChange = null // TODO: Add suggestions logic
|
||||||
|
)
|
||||||
|
}) { padding ->
|
||||||
|
NavHost(
|
||||||
|
navController = navController,
|
||||||
|
startDestination = SettingsScreenKey.ROOT.name,
|
||||||
|
modifier = Modifier.padding(padding)
|
||||||
|
) {
|
||||||
|
composable(
|
||||||
|
SettingsScreenKey.ROOT.name,
|
||||||
|
listOf(createScreenTitleArg(SettingsScreenKey.ROOT.screenTitle))
|
||||||
|
) {
|
||||||
|
SettingsScreen(onSelectSettingOption = { screen ->
|
||||||
|
navController.navigate(screen.name)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
composable(
|
||||||
|
SettingsScreenKey.DEBUG.name,
|
||||||
|
listOf(createScreenTitleArg(SettingsScreenKey.DEBUG.screenTitle))
|
||||||
|
) {
|
||||||
|
DebugScreen(settingsViewModel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun createScreenTitleArg(@StringRes screenTitle: Int) = navArgument(SCREEN_TITLE_KEY) {
|
||||||
|
defaultValue = screenTitle
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class SettingsScreenKey(@StringRes val screenTitle: Int) {
|
||||||
|
ROOT(R.string.settings),
|
||||||
|
DEBUG(R.string.settings_category_debug_title)
|
||||||
|
}
|
||||||
@@ -77,11 +77,13 @@ class NotificationModeConfigFragment : Fragment() {
|
|||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Deprecated("Deprecated in Java")
|
||||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||||
super.onCreateOptionsMenu(menu, inflater)
|
super.onCreateOptionsMenu(menu, inflater)
|
||||||
inflater.inflate(R.menu.menu_notifications_channels, menu)
|
inflater.inflate(R.menu.menu_notifications_channels, menu)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Deprecated("Deprecated in Java")
|
||||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||||
return when (item.itemId) {
|
return when (item.itemId) {
|
||||||
R.id.action_toggle_all -> {
|
R.id.action_toggle_all -> {
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ import androidx.fragment.app.Fragment;
|
|||||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||||
|
|
||||||
import org.schabi.newpipe.databinding.SettingsPreferencesearchFragmentBinding;
|
import org.schabi.newpipe.databinding.SettingsPreferencesearchFragmentBinding;
|
||||||
|
import org.schabi.newpipe.ui.emptystate.EmptyStateSpec;
|
||||||
|
import org.schabi.newpipe.ui.emptystate.EmptyStateUtil;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@@ -39,6 +41,9 @@ public class PreferenceSearchFragment extends Fragment {
|
|||||||
binding = SettingsPreferencesearchFragmentBinding.inflate(inflater, container, false);
|
binding = SettingsPreferencesearchFragmentBinding.inflate(inflater, container, false);
|
||||||
|
|
||||||
binding.searchResults.setLayoutManager(new LinearLayoutManager(getContext()));
|
binding.searchResults.setLayoutManager(new LinearLayoutManager(getContext()));
|
||||||
|
EmptyStateUtil.setEmptyStateComposable(
|
||||||
|
binding.emptyStateView,
|
||||||
|
EmptyStateSpec.Companion.getNoSearchMaxSizeResult());
|
||||||
|
|
||||||
adapter = new PreferenceSearchAdapter();
|
adapter = new PreferenceSearchAdapter();
|
||||||
adapter.setOnItemClickListener(this::onItemClicked);
|
adapter.setOnItemClickListener(this::onItemClicked);
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
package org.schabi.newpipe.settings.viewmodel
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.SharedPreferences
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.lifecycle.AndroidViewModel
|
||||||
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import org.schabi.newpipe.R
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@HiltViewModel
|
||||||
|
class SettingsViewModel @Inject constructor(
|
||||||
|
@ApplicationContext context: Context,
|
||||||
|
private val preferenceManager: SharedPreferences
|
||||||
|
) : AndroidViewModel(context.applicationContext as Application) {
|
||||||
|
|
||||||
|
private var _settingsLayoutRedesignPref: Boolean
|
||||||
|
get() = preferenceManager.getBoolean(
|
||||||
|
ContextCompat.getString(getApplication(), R.string.settings_layout_redesign_key), false
|
||||||
|
)
|
||||||
|
set(value) {
|
||||||
|
preferenceManager.edit().putBoolean(
|
||||||
|
ContextCompat.getString(getApplication(), R.string.settings_layout_redesign_key),
|
||||||
|
value
|
||||||
|
).apply()
|
||||||
|
}
|
||||||
|
private val _settingsLayoutRedesign: MutableStateFlow<Boolean> =
|
||||||
|
MutableStateFlow(_settingsLayoutRedesignPref)
|
||||||
|
val settingsLayoutRedesign = _settingsLayoutRedesign.asStateFlow()
|
||||||
|
|
||||||
|
fun toggleSettingsLayoutRedesign(newState: Boolean) {
|
||||||
|
_settingsLayoutRedesign.value = newState
|
||||||
|
_settingsLayoutRedesignPref = newState
|
||||||
|
}
|
||||||
|
}
|
||||||
53
app/src/main/java/org/schabi/newpipe/ui/SwitchPreference.kt
Normal file
53
app/src/main/java/org/schabi/newpipe/ui/SwitchPreference.kt
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
package org.schabi.newpipe.ui
|
||||||
|
|
||||||
|
import androidx.annotation.StringRes
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Switch
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import org.schabi.newpipe.ui.theme.SizeTokens
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SwitchPreference(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
@StringRes title: Int,
|
||||||
|
isChecked: Boolean,
|
||||||
|
onCheckedChange: (Boolean) -> Unit,
|
||||||
|
@StringRes summary: Int? = null
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
modifier = modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
Column {
|
||||||
|
Text(
|
||||||
|
text = stringResource(id = title),
|
||||||
|
modifier = Modifier.padding(SizeTokens.SpacingExtraSmall),
|
||||||
|
style = MaterialTheme.typography.titleSmall,
|
||||||
|
textAlign = TextAlign.Start,
|
||||||
|
)
|
||||||
|
summary?.let {
|
||||||
|
Text(
|
||||||
|
text = stringResource(id = summary),
|
||||||
|
modifier = Modifier.padding(SizeTokens.SpacingExtraSmall),
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
textAlign = TextAlign.Start,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.width(SizeTokens.SpacingSmall))
|
||||||
|
Switch(checked = isChecked, onCheckedChange = onCheckedChange)
|
||||||
|
}
|
||||||
|
}
|
||||||
66
app/src/main/java/org/schabi/newpipe/ui/TextPreference.kt
Normal file
66
app/src/main/java/org/schabi/newpipe/ui/TextPreference.kt
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
package org.schabi.newpipe.ui
|
||||||
|
|
||||||
|
import androidx.annotation.DrawableRes
|
||||||
|
import androidx.annotation.StringRes
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.defaultMinSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import org.schabi.newpipe.ui.theme.SizeTokens
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun TextPreference(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
@StringRes title: Int,
|
||||||
|
@DrawableRes icon: Int? = null,
|
||||||
|
@StringRes summary: Int? = null,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(SizeTokens.SpacingSmall)
|
||||||
|
.defaultMinSize(minHeight = SizeTokens.SpaceMinSize)
|
||||||
|
.clickable { onClick() }
|
||||||
|
) {
|
||||||
|
icon?.let {
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(id = icon),
|
||||||
|
contentDescription = "icon for $title preference"
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(SizeTokens.SpacingSmall))
|
||||||
|
}
|
||||||
|
Column {
|
||||||
|
Text(
|
||||||
|
text = stringResource(id = title),
|
||||||
|
modifier = Modifier.padding(SizeTokens.SpacingExtraSmall),
|
||||||
|
style = MaterialTheme.typography.titleSmall,
|
||||||
|
textAlign = TextAlign.Start,
|
||||||
|
)
|
||||||
|
summary?.let {
|
||||||
|
Text(
|
||||||
|
text = stringResource(id = summary),
|
||||||
|
modifier = Modifier.padding(SizeTokens.SpacingExtraSmall),
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
textAlign = TextAlign.Start,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
129
app/src/main/java/org/schabi/newpipe/ui/Toolbar.kt
Normal file
129
app/src/main/java/org/schabi/newpipe/ui/Toolbar.kt
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
package org.schabi.newpipe.ui
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.RowScope
|
||||||
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.SearchBar
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TopAppBar
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import org.schabi.newpipe.R
|
||||||
|
import org.schabi.newpipe.ui.theme.AppTheme
|
||||||
|
import org.schabi.newpipe.ui.theme.SizeTokens
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun TextAction(text: String, modifier: Modifier = Modifier) {
|
||||||
|
Text(text = text, color = MaterialTheme.colorScheme.onSurface, modifier = modifier)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun NavigationIcon() {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back",
|
||||||
|
modifier = Modifier.padding(horizontal = SizeTokens.SpacingExtraSmall)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SearchSuggestionItem(text: String) {
|
||||||
|
// TODO: Add more components here to display all the required details of a search suggestion item.
|
||||||
|
Text(text = text)
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun Toolbar(
|
||||||
|
title: String,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
hasNavigationIcon: Boolean = true,
|
||||||
|
hasSearch: Boolean = false,
|
||||||
|
onSearchQueryChange: ((String) -> List<String>)? = null,
|
||||||
|
actions: @Composable RowScope.() -> Unit = {}
|
||||||
|
) {
|
||||||
|
var isSearchActive by remember { mutableStateOf(false) }
|
||||||
|
var query by remember { mutableStateOf("") }
|
||||||
|
|
||||||
|
Column {
|
||||||
|
TopAppBar(
|
||||||
|
title = { Text(text = title) },
|
||||||
|
modifier = modifier,
|
||||||
|
navigationIcon = { if (hasNavigationIcon) NavigationIcon() },
|
||||||
|
actions = {
|
||||||
|
actions()
|
||||||
|
if (hasSearch) {
|
||||||
|
IconButton(onClick = { isSearchActive = true }) {
|
||||||
|
Icon(
|
||||||
|
painterResource(id = R.drawable.ic_search),
|
||||||
|
contentDescription = stringResource(id = R.string.search),
|
||||||
|
tint = MaterialTheme.colorScheme.onSurface
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
if (isSearchActive) {
|
||||||
|
SearchBar(
|
||||||
|
query = query,
|
||||||
|
onQueryChange = { query = it },
|
||||||
|
onSearch = {},
|
||||||
|
placeholder = {
|
||||||
|
Text(text = stringResource(id = R.string.search))
|
||||||
|
},
|
||||||
|
active = true,
|
||||||
|
onActiveChange = {
|
||||||
|
isSearchActive = it
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
onSearchQueryChange?.invoke(query)?.takeIf { it.isNotEmpty() }
|
||||||
|
?.map { suggestionText -> SearchSuggestionItem(text = suggestionText) }
|
||||||
|
?: run {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxHeight()
|
||||||
|
.fillMaxWidth(),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Column {
|
||||||
|
Text(text = "╰(°●°╰)")
|
||||||
|
Text(text = stringResource(id = R.string.search_no_results))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
fun ToolbarPreview() {
|
||||||
|
AppTheme {
|
||||||
|
Toolbar(
|
||||||
|
title = "Title",
|
||||||
|
hasSearch = true,
|
||||||
|
onSearchQueryChange = { emptyList() },
|
||||||
|
actions = {
|
||||||
|
TextAction(text = "Action1")
|
||||||
|
TextAction(text = "Action2")
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,147 @@
|
|||||||
|
package org.schabi.newpipe.ui.components.about
|
||||||
|
|
||||||
|
import androidx.annotation.StringRes
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.wrapContentSize
|
||||||
|
import androidx.compose.foundation.layout.wrapContentWidth
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextButton
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.NonRestartableComposable
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
|
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.core.content.ContextCompat.getDrawable
|
||||||
|
import coil3.compose.AsyncImage
|
||||||
|
import my.nanihadesuka.compose.ColumnScrollbar
|
||||||
|
import org.schabi.newpipe.BuildConfig
|
||||||
|
import org.schabi.newpipe.R
|
||||||
|
import org.schabi.newpipe.ui.components.common.defaultThemedScrollbarSettings
|
||||||
|
import org.schabi.newpipe.util.external_communication.ShareUtils
|
||||||
|
|
||||||
|
private val ABOUT_ITEMS = listOf(
|
||||||
|
AboutData(R.string.faq_title, R.string.faq_description, R.string.faq, R.string.faq_url),
|
||||||
|
AboutData(
|
||||||
|
R.string.contribution_title, R.string.contribution_encouragement,
|
||||||
|
R.string.view_on_github, R.string.github_url
|
||||||
|
),
|
||||||
|
AboutData(
|
||||||
|
R.string.donation_title, R.string.donation_encouragement, R.string.give_back,
|
||||||
|
R.string.donation_url
|
||||||
|
),
|
||||||
|
AboutData(
|
||||||
|
R.string.website_title, R.string.website_encouragement, R.string.open_in_browser,
|
||||||
|
R.string.website_url
|
||||||
|
),
|
||||||
|
AboutData(
|
||||||
|
R.string.privacy_policy_title, R.string.privacy_policy_encouragement,
|
||||||
|
R.string.read_privacy_policy, R.string.privacy_policy_url
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
private class AboutData(
|
||||||
|
@StringRes val title: Int,
|
||||||
|
@StringRes val description: Int,
|
||||||
|
@StringRes val buttonText: Int,
|
||||||
|
@StringRes val url: Int
|
||||||
|
)
|
||||||
|
|
||||||
|
private class AboutDataProvider : CollectionPreviewParameterProvider<AboutData>(ABOUT_ITEMS)
|
||||||
|
|
||||||
|
@Preview(backgroundColor = 0xFFFFFFFF, showBackground = true)
|
||||||
|
@Composable
|
||||||
|
@NonRestartableComposable
|
||||||
|
fun AboutTab() {
|
||||||
|
val scrollState = rememberScrollState()
|
||||||
|
|
||||||
|
ColumnScrollbar(state = scrollState, settings = defaultThemedScrollbarSettings()) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.verticalScroll(scrollState),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(16.dp)
|
||||||
|
.fillMaxWidth()
|
||||||
|
.wrapContentSize(Alignment.Center),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
|
// note: the preview
|
||||||
|
val context = LocalContext.current
|
||||||
|
val launcherDrawable = remember { getDrawable(context, R.mipmap.ic_launcher) }
|
||||||
|
AsyncImage(
|
||||||
|
model = launcherDrawable,
|
||||||
|
contentDescription = stringResource(R.string.app_name),
|
||||||
|
)
|
||||||
|
Spacer(Modifier.height(4.dp))
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.app_name),
|
||||||
|
style = MaterialTheme.typography.titleLarge,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = BuildConfig.VERSION_NAME,
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
text = stringResource(R.string.app_description),
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
for (item in ABOUT_ITEMS) {
|
||||||
|
AboutItem(item, Modifier.padding(horizontal = 16.dp))
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(Modifier.height(8.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview(backgroundColor = 0xFFFFFFFF, showBackground = true)
|
||||||
|
@Composable
|
||||||
|
@NonRestartableComposable
|
||||||
|
private fun AboutItem(
|
||||||
|
@PreviewParameter(AboutDataProvider::class) aboutData: AboutData,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
Column(modifier = modifier) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(aboutData.title),
|
||||||
|
style = MaterialTheme.typography.titleMedium
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = stringResource(aboutData.description),
|
||||||
|
style = MaterialTheme.typography.bodyMedium
|
||||||
|
)
|
||||||
|
|
||||||
|
val context = LocalContext.current
|
||||||
|
TextButton(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.wrapContentWidth(Alignment.End),
|
||||||
|
onClick = { ShareUtils.openUrlInApp(context, context.getString(aboutData.url)) }
|
||||||
|
) {
|
||||||
|
Text(text = stringResource(aboutData.buttonText))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,186 @@
|
|||||||
|
@file:OptIn(ExperimentalLayoutApi::class)
|
||||||
|
|
||||||
|
package org.schabi.newpipe.ui.components.about
|
||||||
|
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||||
|
import androidx.compose.foundation.layout.FlowRow
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.material3.Badge
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
|
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
|
||||||
|
import androidx.compose.ui.tooling.preview.datasource.LoremIpsum
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.mikepenz.aboutlibraries.entity.Developer
|
||||||
|
import com.mikepenz.aboutlibraries.entity.Library
|
||||||
|
import com.mikepenz.aboutlibraries.entity.License
|
||||||
|
import com.mikepenz.aboutlibraries.entity.Organization
|
||||||
|
import com.mikepenz.aboutlibraries.entity.Scm
|
||||||
|
import com.mikepenz.aboutlibraries.ui.compose.m3.util.author
|
||||||
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
|
import kotlinx.collections.immutable.toImmutableSet
|
||||||
|
import org.schabi.newpipe.ui.theme.AppTheme
|
||||||
|
import org.schabi.newpipe.util.external_communication.ShareUtils
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun Library(
|
||||||
|
@PreviewParameter(LibraryProvider::class) library: Library,
|
||||||
|
showLicenseDialog: (licenseFilename: String) -> Unit,
|
||||||
|
descriptionMaxLines: Int,
|
||||||
|
) {
|
||||||
|
val spdxLicense = library.licenses.firstOrNull()?.spdxId?.takeIf { it.isNotBlank() }
|
||||||
|
val licenseAssetPath = spdxLicense?.let { SPDX_ID_TO_ASSET_PATH[it] }
|
||||||
|
val context = LocalContext.current
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = (
|
||||||
|
if (licenseAssetPath != null) {
|
||||||
|
Modifier.clickable {
|
||||||
|
showLicenseDialog(licenseAssetPath)
|
||||||
|
}
|
||||||
|
} else if (spdxLicense != null) {
|
||||||
|
Modifier.clickable {
|
||||||
|
ShareUtils.openUrlInBrowser(context, "https://spdx.org/licenses/$spdxLicense.html")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Modifier
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.padding(vertical = 8.dp, horizontal = 16.dp)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = library.name,
|
||||||
|
modifier = Modifier.weight(0.75f),
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
)
|
||||||
|
val version = library.artifactVersion
|
||||||
|
if (!version.isNullOrBlank()) {
|
||||||
|
Text(
|
||||||
|
version,
|
||||||
|
modifier = if (version.length > 12) {
|
||||||
|
// limit the version size if it's too many characters (can happen e.g. if
|
||||||
|
// the version is a commit hash)
|
||||||
|
Modifier.weight(0.25f)
|
||||||
|
} else {
|
||||||
|
Modifier
|
||||||
|
}.padding(start = 8.dp),
|
||||||
|
style = MaterialTheme.typography.labelMedium,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val author = library.author
|
||||||
|
if (author.isNotBlank()) {
|
||||||
|
Text(
|
||||||
|
text = author,
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
val description = library.description
|
||||||
|
if (!description.isNullOrBlank() && description != library.name) {
|
||||||
|
Spacer(Modifier.height(3.dp))
|
||||||
|
Text(
|
||||||
|
text = description,
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
maxLines = descriptionMaxLines,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (library.licenses.isNotEmpty()) {
|
||||||
|
FlowRow(
|
||||||
|
modifier = Modifier.padding(top = 6.dp, bottom = 4.dp),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||||
|
) {
|
||||||
|
library.licenses.forEach {
|
||||||
|
Badge {
|
||||||
|
Text(text = it.spdxId?.takeIf { it.isNotBlank() } ?: it.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview(showBackground = true, backgroundColor = 0xFFFFFFFF)
|
||||||
|
@Composable
|
||||||
|
private fun LibraryPreview(@PreviewParameter(LibraryProvider::class) library: Library) {
|
||||||
|
AppTheme {
|
||||||
|
Library(library, {}, 2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class LibraryProvider : CollectionPreviewParameterProvider<Library>(
|
||||||
|
listOf(
|
||||||
|
Library(
|
||||||
|
uniqueId = "org.schabi.newpipe.extractor",
|
||||||
|
artifactVersion = "v0.24.3",
|
||||||
|
name = "NewPipeExtractor",
|
||||||
|
description = "NewPipe Extractor is a library for extracting things from streaming sites. It is a core component of NewPipe, but could be used independently.",
|
||||||
|
website = "https://newpipe.net",
|
||||||
|
developers = listOf(Developer("TeamNewPipe", "https://newpipe.net")).toImmutableList(),
|
||||||
|
organization = Organization("TeamNewPipe", "https://newpipe.net"),
|
||||||
|
scm = Scm(null, null, "https://github.com/TeamNewPipe/NewPipeExtractor"),
|
||||||
|
licenses = setOf(
|
||||||
|
License(
|
||||||
|
name = "GNU General Public License v3.0",
|
||||||
|
url = "https://api.github.com/licenses/gpl-3.0",
|
||||||
|
year = null,
|
||||||
|
spdxId = "GPL-3.0-only",
|
||||||
|
licenseContent = LoremIpsum().values.first(),
|
||||||
|
hash = "1234"
|
||||||
|
),
|
||||||
|
License(
|
||||||
|
name = "GNU General Public License v3.0",
|
||||||
|
url = "https://api.github.com/licenses/gpl-3.0",
|
||||||
|
year = null,
|
||||||
|
spdxId = "GPL-3.0-only",
|
||||||
|
licenseContent = LoremIpsum().values.first(),
|
||||||
|
hash = "4321"
|
||||||
|
)
|
||||||
|
).toImmutableSet()
|
||||||
|
),
|
||||||
|
Library(
|
||||||
|
uniqueId = "org.schabi.newpipe.extractor",
|
||||||
|
artifactVersion = "v0.24.3",
|
||||||
|
name = "NewPipeExtractor",
|
||||||
|
description = "NewPipe Extractor is a library for extracting things from streaming sites. It is a core component of NewPipe, but could be used independently.",
|
||||||
|
website = null,
|
||||||
|
developers = listOf<Developer>().toImmutableList(),
|
||||||
|
organization = null,
|
||||||
|
scm = null,
|
||||||
|
licenses = setOf(
|
||||||
|
License(
|
||||||
|
name = "GNU General Public License v3.0",
|
||||||
|
url = "https://api.github.com/licenses/gpl-3.0",
|
||||||
|
year = null,
|
||||||
|
spdxId = "GPL-3.0-only",
|
||||||
|
licenseContent = LoremIpsum().values.first(),
|
||||||
|
hash = "1234"
|
||||||
|
)
|
||||||
|
).toImmutableSet()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
@@ -0,0 +1,138 @@
|
|||||||
|
/**
|
||||||
|
* The library definitions for most libraries are autogenerated by the AboutLibraries plugin.
|
||||||
|
* This file is only for TeamNewPipe-related libraries.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.schabi.newpipe.ui.components.about
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import com.mikepenz.aboutlibraries.entity.Developer
|
||||||
|
import com.mikepenz.aboutlibraries.entity.Library
|
||||||
|
import com.mikepenz.aboutlibraries.entity.License
|
||||||
|
import com.mikepenz.aboutlibraries.entity.Scm
|
||||||
|
import kotlinx.collections.immutable.ImmutableSet
|
||||||
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
|
import kotlinx.collections.immutable.toImmutableSet
|
||||||
|
import org.schabi.newpipe.BuildConfig
|
||||||
|
import org.schabi.newpipe.R
|
||||||
|
|
||||||
|
val SPDX_ID_TO_ASSET_PATH = mapOf(
|
||||||
|
"Apache-2.0" to "apache2.html",
|
||||||
|
"EPL-1.0" to "epl1.html",
|
||||||
|
"GPL-3.0-only" to "gpl_3.html",
|
||||||
|
"GPL-3.0-or-later" to "gpl_3.html",
|
||||||
|
"MIT" to "mit.html",
|
||||||
|
"MPL-2.0" to "mpl2.html",
|
||||||
|
)
|
||||||
|
|
||||||
|
fun getFirstPartyLibraries(
|
||||||
|
context: Context,
|
||||||
|
teamNewPipeLibraries: List<Library>,
|
||||||
|
): List<Library> {
|
||||||
|
val gpl3 = setOf(
|
||||||
|
License(
|
||||||
|
name = "GNU General Public License v3.0",
|
||||||
|
url = "https://www.gnu.org/licenses/gpl-3.0.txt",
|
||||||
|
year = null,
|
||||||
|
spdxId = "GPL-3.0-or-later",
|
||||||
|
licenseContent = null,
|
||||||
|
hash = "GPL-3.0-or-later",
|
||||||
|
)
|
||||||
|
).toImmutableSet()
|
||||||
|
|
||||||
|
val npeId = "com.github.TeamNewPipe:NewPipeExtractor"
|
||||||
|
val npe = teamNewPipeLibraries.firstOrNull { it.uniqueId == npeId }
|
||||||
|
|
||||||
|
return listOf(
|
||||||
|
Library(
|
||||||
|
uniqueId = BuildConfig.APPLICATION_ID,
|
||||||
|
artifactVersion = BuildConfig.VERSION_NAME,
|
||||||
|
name = context.getString(R.string.app_name),
|
||||||
|
description = context.getString(R.string.app_description),
|
||||||
|
website = context.getString(R.string.website_url),
|
||||||
|
developers = listOf(
|
||||||
|
Developer(
|
||||||
|
name = context.getString(R.string.team_newpipe),
|
||||||
|
organisationUrl = context.getString(R.string.website_url)
|
||||||
|
)
|
||||||
|
).toImmutableList(),
|
||||||
|
organization = null,
|
||||||
|
scm = Scm(null, null, context.getString(R.string.github_url)),
|
||||||
|
licenses = gpl3,
|
||||||
|
),
|
||||||
|
Library(
|
||||||
|
uniqueId = npeId,
|
||||||
|
artifactVersion = npe?.artifactVersion,
|
||||||
|
name = context.getString(R.string.newpipe_extractor),
|
||||||
|
description = context.getString(R.string.newpipe_extractor_description),
|
||||||
|
website = context.getString(R.string.newpipe_extractor_github_url),
|
||||||
|
developers = listOf(
|
||||||
|
Developer(
|
||||||
|
name = context.getString(R.string.team_newpipe),
|
||||||
|
organisationUrl = context.getString(R.string.website_url)
|
||||||
|
)
|
||||||
|
).toImmutableList(),
|
||||||
|
organization = null,
|
||||||
|
scm = Scm(null, null, context.getString(R.string.newpipe_extractor_github_url)),
|
||||||
|
licenses = gpl3,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getAdditionalThirdPartyLibraries(
|
||||||
|
context: Context,
|
||||||
|
teamNewPipeLibraries: List<Library>,
|
||||||
|
licenses: ImmutableSet<License>,
|
||||||
|
): List<Library> {
|
||||||
|
val apache2 = licenses.firstOrNull { it.spdxId == "Apache-2.0" }
|
||||||
|
val mit = licenses.firstOrNull { it.spdxId == "MIT" }
|
||||||
|
val mpl2 = licenses.firstOrNull { it.spdxId == "MPL-2.0" }
|
||||||
|
|
||||||
|
val nanojsonId = "com.github.TeamNewPipe:nanojson"
|
||||||
|
val nanojson = teamNewPipeLibraries.firstOrNull { it.uniqueId == nanojsonId }
|
||||||
|
val nnfpId = "com.github.TeamNewPipe:NoNonsense-FilePicker"
|
||||||
|
val nnfp = teamNewPipeLibraries.firstOrNull { it.uniqueId == nnfpId }
|
||||||
|
|
||||||
|
return listOf(
|
||||||
|
Library(
|
||||||
|
uniqueId = nnfpId,
|
||||||
|
artifactVersion = nnfp?.artifactVersion,
|
||||||
|
name = "NoNonsense-FilePicker",
|
||||||
|
description = "A file/directory-picker for Android.",
|
||||||
|
website = "https://github.com/TeamNewPipe/NoNonsense-FilePicker",
|
||||||
|
developers = listOf(
|
||||||
|
Developer(
|
||||||
|
name = "Jonas Kalderstam",
|
||||||
|
organisationUrl = "https://github.com/spacecowboy/NoNonsense-FilePicker",
|
||||||
|
),
|
||||||
|
Developer(
|
||||||
|
name = context.getString(R.string.team_newpipe),
|
||||||
|
organisationUrl = context.getString(R.string.website_url)
|
||||||
|
)
|
||||||
|
).toImmutableList(),
|
||||||
|
organization = null,
|
||||||
|
scm = Scm(null, null, "https://github.com/TeamNewPipe/NoNonsense-FilePicker"),
|
||||||
|
licenses = listOfNotNull(mpl2).toImmutableSet(),
|
||||||
|
),
|
||||||
|
Library(
|
||||||
|
uniqueId = nanojsonId,
|
||||||
|
artifactVersion = nanojson?.artifactVersion,
|
||||||
|
name = "nanojson",
|
||||||
|
description = "nanojson is a tiny, fast, and compliant JSON parser and writer for Java.",
|
||||||
|
website = "https://github.com/TeamNewPipe/nanojson",
|
||||||
|
developers = listOf(
|
||||||
|
Developer(
|
||||||
|
name = "mmastrac",
|
||||||
|
organisationUrl = "https://github.com/mmastrac/nanojson",
|
||||||
|
),
|
||||||
|
Developer(
|
||||||
|
name = context.getString(R.string.team_newpipe),
|
||||||
|
organisationUrl = context.getString(R.string.website_url)
|
||||||
|
),
|
||||||
|
).toImmutableList(),
|
||||||
|
organization = null,
|
||||||
|
scm = Scm(null, null, "https://github.com/TeamNewPipe/nanojson"),
|
||||||
|
licenses = listOfNotNull(mit, apache2).toImmutableSet()
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
@file:OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
|
||||||
|
package org.schabi.newpipe.ui.components.about
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.LocalContentColor
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.ModalBottomSheet
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.contentColorFor
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.text.AnnotatedString
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import org.schabi.newpipe.ui.components.common.LazyColumnThemedScrollbar
|
||||||
|
import org.schabi.newpipe.ui.components.common.LoadingIndicator
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun LicenseDialog(licenseHtml: AnnotatedString, onDismissRequest: () -> Unit) {
|
||||||
|
val lazyListState = rememberLazyListState()
|
||||||
|
|
||||||
|
ModalBottomSheet(onDismissRequest) {
|
||||||
|
CompositionLocalProvider(
|
||||||
|
// contentColorFor(MaterialTheme.colorScheme.containerColor), i.e. ModalBottomSheet's
|
||||||
|
// default background color, does not resolve correctly, so need to manually set the
|
||||||
|
// content color for MaterialTheme.colorScheme.background instead
|
||||||
|
LocalContentColor provides contentColorFor(MaterialTheme.colorScheme.background)
|
||||||
|
) {
|
||||||
|
LazyColumnThemedScrollbar(state = lazyListState) {
|
||||||
|
LazyColumn(
|
||||||
|
state = lazyListState
|
||||||
|
) {
|
||||||
|
item {
|
||||||
|
if (licenseHtml.isEmpty()) {
|
||||||
|
LoadingIndicator(modifier = Modifier.padding(32.dp))
|
||||||
|
} else {
|
||||||
|
Text(
|
||||||
|
text = licenseHtml,
|
||||||
|
modifier = Modifier.padding(horizontal = 12.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
package org.schabi.newpipe.ui.components.about
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.NonRestartableComposable
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
|
import org.schabi.newpipe.R
|
||||||
|
import org.schabi.newpipe.ui.components.common.LazyColumnThemedScrollbar
|
||||||
|
import org.schabi.newpipe.ui.components.common.LoadingIndicator
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
@NonRestartableComposable
|
||||||
|
fun LicenseTab(viewModel: LicenseTabViewModel = viewModel()) {
|
||||||
|
val lazyListState = rememberLazyListState()
|
||||||
|
val stateFlow = viewModel.state.collectAsState()
|
||||||
|
val state = stateFlow.value
|
||||||
|
|
||||||
|
if (state.licenseDialogHtml != null) {
|
||||||
|
LicenseDialog(
|
||||||
|
licenseHtml = state.licenseDialogHtml,
|
||||||
|
onDismissRequest = { viewModel.closeLicenseDialog() }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
LazyColumnThemedScrollbar(state = lazyListState) {
|
||||||
|
LazyColumn(
|
||||||
|
state = lazyListState,
|
||||||
|
) {
|
||||||
|
item {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.app_license_title),
|
||||||
|
style = MaterialTheme.typography.titleLarge,
|
||||||
|
modifier = Modifier.padding(
|
||||||
|
start = 16.dp,
|
||||||
|
top = 16.dp,
|
||||||
|
end = 16.dp,
|
||||||
|
bottom = 8.dp
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
item {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.app_license),
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
modifier = Modifier.padding(
|
||||||
|
start = 16.dp,
|
||||||
|
end = 16.dp,
|
||||||
|
bottom = 8.dp
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (state.firstPartyLibraries == null) {
|
||||||
|
item {
|
||||||
|
LoadingIndicator(modifier = Modifier.padding(32.dp))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (library in state.firstPartyLibraries) {
|
||||||
|
item {
|
||||||
|
Library(
|
||||||
|
library = library,
|
||||||
|
showLicenseDialog = viewModel::showLicenseDialog,
|
||||||
|
descriptionMaxLines = Int.MAX_VALUE,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
item {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.title_licenses),
|
||||||
|
style = MaterialTheme.typography.titleLarge,
|
||||||
|
modifier = Modifier.padding(
|
||||||
|
start = 16.dp,
|
||||||
|
top = 16.dp,
|
||||||
|
end = 16.dp,
|
||||||
|
bottom = 8.dp
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (state.thirdPartyLibraries == null) {
|
||||||
|
item {
|
||||||
|
LoadingIndicator(modifier = Modifier.padding(32.dp))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (library in state.thirdPartyLibraries) {
|
||||||
|
item {
|
||||||
|
Library(
|
||||||
|
library = library,
|
||||||
|
showLicenseDialog = viewModel::showLicenseDialog,
|
||||||
|
descriptionMaxLines = 2,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
package org.schabi.newpipe.ui.components.about
|
||||||
|
|
||||||
|
import androidx.compose.ui.text.AnnotatedString
|
||||||
|
import androidx.compose.ui.text.fromHtml
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import com.mikepenz.aboutlibraries.Libs
|
||||||
|
import com.mikepenz.aboutlibraries.entity.Library
|
||||||
|
import com.mikepenz.aboutlibraries.util.withContext
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.update
|
||||||
|
import kotlinx.coroutines.isActive
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import org.schabi.newpipe.App
|
||||||
|
|
||||||
|
class LicenseTabViewModel : ViewModel() {
|
||||||
|
private val _state = MutableStateFlow(LicenseTabState(null, null, null))
|
||||||
|
val state: StateFlow<LicenseTabState> = _state
|
||||||
|
private var licenseLoadJob: Job? = null
|
||||||
|
|
||||||
|
init {
|
||||||
|
viewModelScope.launch {
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
loadLibraries()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun loadLibraries() {
|
||||||
|
val context = App.instance
|
||||||
|
val libs = Libs.Builder().withContext(context).build()
|
||||||
|
val (teamNewPipeLibraries, thirdParty) = libs.libraries
|
||||||
|
.toMutableList()
|
||||||
|
.partition { it.uniqueId.startsWith("com.github.TeamNewPipe") }
|
||||||
|
|
||||||
|
val firstParty = getFirstPartyLibraries(context, teamNewPipeLibraries)
|
||||||
|
val allThirdParty =
|
||||||
|
getAdditionalThirdPartyLibraries(context, teamNewPipeLibraries, libs.licenses) +
|
||||||
|
thirdParty
|
||||||
|
|
||||||
|
_state.update {
|
||||||
|
it.copy(
|
||||||
|
firstPartyLibraries = firstParty,
|
||||||
|
thirdPartyLibraries = allThirdParty,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun showLicenseDialog(filename: String) {
|
||||||
|
licenseLoadJob?.cancel()
|
||||||
|
_state.update { it.copy(licenseDialogHtml = AnnotatedString("")) }
|
||||||
|
licenseLoadJob = viewModelScope.launch {
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
val text = App.instance.assets.open(filename).bufferedReader().use { it.readText() }
|
||||||
|
val parsedHtml = AnnotatedString.fromHtml(text)
|
||||||
|
_state.update {
|
||||||
|
if (it.licenseDialogHtml != null && isActive) {
|
||||||
|
it.copy(licenseDialogHtml = parsedHtml)
|
||||||
|
} else {
|
||||||
|
it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun closeLicenseDialog() {
|
||||||
|
licenseLoadJob?.cancel()
|
||||||
|
_state.update { it.copy(licenseDialogHtml = null) }
|
||||||
|
}
|
||||||
|
|
||||||
|
data class LicenseTabState(
|
||||||
|
val firstPartyLibraries: List<Library>?,
|
||||||
|
val thirdPartyLibraries: List<Library>?,
|
||||||
|
// null if dialog closed, empty if loading, otherwise license HTML content
|
||||||
|
val licenseDialogHtml: AnnotatedString?,
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
package org.schabi.newpipe.ui.components.common
|
||||||
|
|
||||||
|
import androidx.compose.material3.LocalTextStyle
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.text.AnnotatedString
|
||||||
|
import androidx.compose.ui.text.SpanStyle
|
||||||
|
import androidx.compose.ui.text.TextLinkStyles
|
||||||
|
import androidx.compose.ui.text.TextStyle
|
||||||
|
import androidx.compose.ui.text.fromHtml
|
||||||
|
import androidx.compose.ui.text.style.TextDecoration
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import org.schabi.newpipe.extractor.stream.Description
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun DescriptionText(
|
||||||
|
description: Description,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
overflow: TextOverflow = TextOverflow.Clip,
|
||||||
|
maxLines: Int = Int.MAX_VALUE,
|
||||||
|
style: TextStyle = LocalTextStyle.current
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
modifier = modifier,
|
||||||
|
text = rememberParsedDescription(description),
|
||||||
|
maxLines = maxLines,
|
||||||
|
style = style,
|
||||||
|
overflow = overflow
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun rememberParsedDescription(description: Description): AnnotatedString {
|
||||||
|
// TODO: Handle links and hashtags, Markdown.
|
||||||
|
return remember(description) {
|
||||||
|
if (description.type == Description.HTML) {
|
||||||
|
val styles = TextLinkStyles(SpanStyle(textDecoration = TextDecoration.Underline))
|
||||||
|
AnnotatedString.fromHtml(description.content, styles)
|
||||||
|
} else {
|
||||||
|
AnnotatedString(description.content)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package org.schabi.newpipe.ui.components.common
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.wrapContentSize
|
||||||
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun LoadingIndicator(modifier: Modifier = Modifier) {
|
||||||
|
CircularProgressIndicator(modifier = modifier.fillMaxSize().wrapContentSize(Alignment.Center))
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
package org.schabi.newpipe.ui.components.common
|
||||||
|
|
||||||
|
import android.content.res.Configuration
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
import androidx.compose.foundation.layout.RowScope
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TopAppBar
|
||||||
|
import androidx.compose.material3.TopAppBarDefaults
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun ScaffoldWithToolbar(
|
||||||
|
title: String,
|
||||||
|
onBackClick: () -> Unit,
|
||||||
|
actions: @Composable RowScope.() -> Unit = {},
|
||||||
|
content: @Composable (PaddingValues) -> Unit
|
||||||
|
) {
|
||||||
|
Scaffold(
|
||||||
|
topBar = {
|
||||||
|
TopAppBar(
|
||||||
|
title = { Text(text = title) },
|
||||||
|
// TODO decide whether to use default colors instead
|
||||||
|
colors = TopAppBarDefaults.topAppBarColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.primaryContainer,
|
||||||
|
scrolledContainerColor = MaterialTheme.colorScheme.primaryContainer,
|
||||||
|
navigationIconContentColor = MaterialTheme.colorScheme.onPrimaryContainer,
|
||||||
|
titleContentColor = MaterialTheme.colorScheme.onPrimaryContainer,
|
||||||
|
actionIconContentColor = MaterialTheme.colorScheme.onPrimaryContainer,
|
||||||
|
),
|
||||||
|
navigationIcon = {
|
||||||
|
IconButton(onClick = onBackClick) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
||||||
|
contentDescription = null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
actions = actions
|
||||||
|
)
|
||||||
|
},
|
||||||
|
content = content
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview(name = "Light mode", uiMode = Configuration.UI_MODE_NIGHT_NO)
|
||||||
|
@Preview(name = "Dark mode", uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||||
|
@Composable
|
||||||
|
private fun ScaffoldWithToolbarPreview() {
|
||||||
|
ScaffoldWithToolbar(
|
||||||
|
title = "Example",
|
||||||
|
onBackClick = {},
|
||||||
|
content = {}
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
package org.schabi.newpipe.ui.components.common
|
||||||
|
|
||||||
|
import androidx.compose.foundation.lazy.LazyListState
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import my.nanihadesuka.compose.LazyColumnScrollbar
|
||||||
|
import my.nanihadesuka.compose.ScrollbarSettings
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun defaultThemedScrollbarSettings(): ScrollbarSettings = ScrollbarSettings.Default.copy(
|
||||||
|
thumbUnselectedColor = MaterialTheme.colorScheme.primary,
|
||||||
|
thumbSelectedColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.75f),
|
||||||
|
)
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun LazyColumnThemedScrollbar(
|
||||||
|
state: LazyListState,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
settings: ScrollbarSettings = defaultThemedScrollbarSettings(),
|
||||||
|
indicatorContent: (@Composable (index: Int, isThumbSelected: Boolean) -> Unit)? = null,
|
||||||
|
content: @Composable () -> Unit
|
||||||
|
) {
|
||||||
|
LazyColumnScrollbar(
|
||||||
|
state = state,
|
||||||
|
modifier = modifier,
|
||||||
|
settings = settings,
|
||||||
|
indicatorContent = indicatorContent,
|
||||||
|
content = content,
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
package org.schabi.newpipe.ui.components.items
|
||||||
|
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.LazyListScope
|
||||||
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
|
import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.platform.rememberNestedScrollInteropConnection
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.preference.PreferenceManager
|
||||||
|
import androidx.window.core.layout.WindowWidthSizeClass
|
||||||
|
import org.schabi.newpipe.R
|
||||||
|
import org.schabi.newpipe.extractor.InfoItem
|
||||||
|
import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem
|
||||||
|
import org.schabi.newpipe.extractor.stream.StreamInfoItem
|
||||||
|
import org.schabi.newpipe.info_list.ItemViewMode
|
||||||
|
import org.schabi.newpipe.ktx.findFragmentActivity
|
||||||
|
import org.schabi.newpipe.ui.components.common.LazyColumnThemedScrollbar
|
||||||
|
import org.schabi.newpipe.ui.components.items.playlist.PlaylistListItem
|
||||||
|
import org.schabi.newpipe.ui.components.items.stream.StreamListItem
|
||||||
|
import org.schabi.newpipe.util.DependentPreferenceHelper
|
||||||
|
import org.schabi.newpipe.util.NavigationHelper
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ItemList(
|
||||||
|
items: List<InfoItem>,
|
||||||
|
mode: ItemViewMode = determineItemViewMode(),
|
||||||
|
listHeader: LazyListScope.() -> Unit = {}
|
||||||
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val onClick = remember {
|
||||||
|
{ item: InfoItem ->
|
||||||
|
val fragmentManager = context.findFragmentActivity().supportFragmentManager
|
||||||
|
if (item is StreamInfoItem) {
|
||||||
|
NavigationHelper.openVideoDetailFragment(
|
||||||
|
context, fragmentManager, item.serviceId, item.url, item.name, null, false
|
||||||
|
)
|
||||||
|
} else if (item is PlaylistInfoItem) {
|
||||||
|
NavigationHelper.openPlaylistFragment(
|
||||||
|
fragmentManager, item.serviceId, item.url, item.name
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle long clicks for stream items
|
||||||
|
// TODO: Adjust the menu display depending on where it was triggered
|
||||||
|
var selectedStream by remember { mutableStateOf<StreamInfoItem?>(null) }
|
||||||
|
val onLongClick = remember {
|
||||||
|
{ stream: StreamInfoItem ->
|
||||||
|
selectedStream = stream
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val onDismissPopup = remember {
|
||||||
|
{
|
||||||
|
selectedStream = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val showProgress = DependentPreferenceHelper.getPositionsInListsEnabled(context)
|
||||||
|
val nestedScrollModifier = Modifier.nestedScroll(rememberNestedScrollInteropConnection())
|
||||||
|
|
||||||
|
if (mode == ItemViewMode.GRID) {
|
||||||
|
// TODO: Implement grid layout using LazyVerticalGrid and LazyVerticalGridScrollbar.
|
||||||
|
} else {
|
||||||
|
val state = rememberLazyListState()
|
||||||
|
|
||||||
|
LazyColumnThemedScrollbar(state = state) {
|
||||||
|
LazyColumn(modifier = nestedScrollModifier, state = state) {
|
||||||
|
listHeader()
|
||||||
|
|
||||||
|
items(items.size) {
|
||||||
|
val item = items[it]
|
||||||
|
|
||||||
|
if (item is StreamInfoItem) {
|
||||||
|
val isSelected = selectedStream == item
|
||||||
|
StreamListItem(
|
||||||
|
item, showProgress, isSelected, onClick, onLongClick, onDismissPopup
|
||||||
|
)
|
||||||
|
} else if (item is PlaylistInfoItem) {
|
||||||
|
PlaylistListItem(item, onClick)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun determineItemViewMode(): ItemViewMode {
|
||||||
|
val prefValue = PreferenceManager.getDefaultSharedPreferences(LocalContext.current)
|
||||||
|
.getString(stringResource(R.string.list_view_mode_key), null)
|
||||||
|
val viewMode = prefValue?.let { ItemViewMode.valueOf(it.uppercase()) } ?: ItemViewMode.AUTO
|
||||||
|
|
||||||
|
return when (viewMode) {
|
||||||
|
ItemViewMode.AUTO -> {
|
||||||
|
// Evaluate whether to use Grid based on screen real estate.
|
||||||
|
val windowSizeClass = currentWindowAdaptiveInfo().windowSizeClass
|
||||||
|
if (windowSizeClass.windowWidthSizeClass == WindowWidthSizeClass.EXPANDED) {
|
||||||
|
ItemViewMode.GRID
|
||||||
|
} else {
|
||||||
|
ItemViewMode.LIST
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> viewMode
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
package org.schabi.newpipe.ui.components.items.playlist
|
||||||
|
|
||||||
|
import android.content.res.Configuration
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Surface
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import org.schabi.newpipe.extractor.InfoItem
|
||||||
|
import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem
|
||||||
|
import org.schabi.newpipe.ui.theme.AppTheme
|
||||||
|
import org.schabi.newpipe.util.NO_SERVICE_ID
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun PlaylistListItem(
|
||||||
|
playlist: PlaylistInfoItem,
|
||||||
|
onClick: (InfoItem) -> Unit = {},
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.clickable { onClick(playlist) }
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(12.dp),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
PlaylistThumbnail(
|
||||||
|
playlist = playlist,
|
||||||
|
modifier = Modifier.size(width = 140.dp, height = 78.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
Column {
|
||||||
|
Text(
|
||||||
|
text = playlist.name,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
style = MaterialTheme.typography.titleSmall,
|
||||||
|
maxLines = 2
|
||||||
|
)
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = playlist.uploaderName.orEmpty(),
|
||||||
|
style = MaterialTheme.typography.bodySmall
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview(name = "Light mode", uiMode = Configuration.UI_MODE_NIGHT_NO)
|
||||||
|
@Preview(name = "Dark mode", uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||||
|
@Composable
|
||||||
|
private fun PlaylistListItemPreview() {
|
||||||
|
val playlist = PlaylistInfoItem(NO_SERVICE_ID, "", "Playlist")
|
||||||
|
playlist.uploaderName = "Uploader"
|
||||||
|
|
||||||
|
AppTheme {
|
||||||
|
Surface {
|
||||||
|
PlaylistListItem(playlist)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
package org.schabi.newpipe.ui.components.items.playlist
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.automirrored.filled.PlaylistPlay
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.layout.ContentScale
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import coil3.compose.AsyncImage
|
||||||
|
import org.schabi.newpipe.R
|
||||||
|
import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem
|
||||||
|
import org.schabi.newpipe.util.Localization
|
||||||
|
import org.schabi.newpipe.util.image.ImageStrategy
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun PlaylistThumbnail(
|
||||||
|
playlist: PlaylistInfoItem,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
contentScale: ContentScale = ContentScale.Fit
|
||||||
|
) {
|
||||||
|
Box(contentAlignment = Alignment.BottomEnd) {
|
||||||
|
AsyncImage(
|
||||||
|
model = ImageStrategy.choosePreferredImage(playlist.thumbnails),
|
||||||
|
contentDescription = null,
|
||||||
|
placeholder = painterResource(R.drawable.placeholder_thumbnail_playlist),
|
||||||
|
error = painterResource(R.drawable.placeholder_thumbnail_playlist),
|
||||||
|
contentScale = contentScale,
|
||||||
|
modifier = modifier
|
||||||
|
)
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(2.dp)
|
||||||
|
.background(Color.Black.copy(alpha = 0.5f))
|
||||||
|
.padding(2.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.AutoMirrored.Default.PlaylistPlay,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = Color.White,
|
||||||
|
modifier = Modifier.size(18.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
val context = LocalContext.current
|
||||||
|
Text(
|
||||||
|
text = Localization.localizeStreamCountMini(context, playlist.streamCount),
|
||||||
|
color = Color.White,
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
modifier = Modifier.padding(start = 4.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user