mirror of
https://github.com/TeamNewPipe/NewPipe
synced 2025-01-04 22:40:32 +00:00
Merge branch 'refactor' into Playlist-Compose
# Conflicts: # app/build.gradle # app/src/main/java/org/schabi/newpipe/MainActivity.java # app/src/main/java/org/schabi/newpipe/error/ReCaptchaActivity.java # app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java # app/src/main/java/org/schabi/newpipe/fragments/list/comments/CommentsFragment.kt # app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistFragment.java # app/src/main/java/org/schabi/newpipe/ktx/Bundle.kt # app/src/main/java/org/schabi/newpipe/local/bookmark/BookmarkFragment.java # app/src/main/java/org/schabi/newpipe/paging/CommentsSource.kt # app/src/main/java/org/schabi/newpipe/ui/components/common/DescriptionText.kt # app/src/main/java/org/schabi/newpipe/ui/components/common/LoadingIndicator.kt # app/src/main/java/org/schabi/newpipe/viewmodels/CommentsViewModel.kt # build.gradle
This commit is contained in:
commit
52a2accea9
6
.github/CONTRIBUTING.md
vendored
6
.github/CONTRIBUTING.md
vendored
@ -79,6 +79,6 @@ The [ktlint](https://github.com/pinterest/ktlint) plugin does the same job as ch
|
||||
|
||||
## Communication
|
||||
|
||||
* The #newpipe channel on Libera Chat (`ircs://irc.libera.chat:6697/newpipe`) has the core team and other developers in it. [Click here for webchat](https://web.libera.chat/#newpipe)!
|
||||
* You can also use a Matrix account to join the NewPipe channel at [#newpipe:libera.chat](https://matrix.to/#/#newpipe:libera.chat). Some convenient clients, available both for phone and desktop, are listed at that link.
|
||||
* You can post your suggestions, changes, ideas etc. on either GitHub or IRC.
|
||||
* You can use a Matrix account to join the NewPipe channel at [#newpipe:matrix.newpipe-ev.de](https://matrix.to/#/#newpipe:matrix.newpipe-ev.de). Some convenient clients, available both for phone and desktop, are listed at that link.
|
||||
* Alternatively, the #newpipe channel on Libera Chat (`ircs://irc.libera.chat:6697/newpipe`) can also be joined, as it is bridged to the Matrix room. [Click here for webchat](https://web.libera.chat/#newpipe)!
|
||||
* You can post your suggestions, changes, ideas etc. on either GitHub or Matrix (including via IRC).
|
||||
|
6
.github/ISSUE_TEMPLATE/config.yml
vendored
6
.github/ISSUE_TEMPLATE/config.yml
vendored
@ -3,9 +3,9 @@ contact_links:
|
||||
- name: ❓ Question
|
||||
url: https://github.com/TeamNewPipe/NewPipe/discussions/new?category=questions
|
||||
about: Ask about anything NewPipe-related
|
||||
- name: 💬 Matrix
|
||||
url: https://matrix.to/#/#newpipe:matrix.newpipe-ev.de
|
||||
about: Chat with us via Matrix for quick Q/A
|
||||
- name: 💬 IRC
|
||||
url: https://web.libera.chat/#newpipe
|
||||
about: Chat with us via IRC for quick Q/A
|
||||
- name: 💬 Matrix
|
||||
url: https://matrix.to/#/#newpipe:libera.chat
|
||||
about: Chat with us via Matrix for quick Q/A
|
||||
|
12
.github/workflows/ci.yml
vendored
12
.github/workflows/ci.yml
vendored
@ -47,10 +47,10 @@ jobs:
|
||||
BRANCH: ${{ github.head_ref }}
|
||||
run: git checkout -B "$BRANCH"
|
||||
|
||||
- name: set up JDK 17
|
||||
- name: set up JDK
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
java-version: 17
|
||||
java-version: 21
|
||||
distribution: "temurin"
|
||||
cache: 'gradle'
|
||||
|
||||
@ -88,10 +88,10 @@ jobs:
|
||||
sudo udevadm control --reload-rules
|
||||
sudo udevadm trigger --name-match=kvm
|
||||
|
||||
- name: set up JDK 17
|
||||
- name: set up JDK
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
java-version: 17
|
||||
java-version: 21
|
||||
distribution: "temurin"
|
||||
cache: 'gradle'
|
||||
|
||||
@ -121,10 +121,10 @@ jobs:
|
||||
with:
|
||||
fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis
|
||||
|
||||
- name: Set up JDK 17
|
||||
- name: Set up JDK
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
java-version: 17
|
||||
java-version: 21
|
||||
distribution: "temurin"
|
||||
cache: 'gradle'
|
||||
|
||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -10,6 +10,7 @@ captures/
|
||||
*.class
|
||||
app/debug/
|
||||
app/release/
|
||||
.kotlin/
|
||||
|
||||
# vscode / eclipse files
|
||||
*.classpath
|
||||
|
21
.idea/icon.svg
generated
Normal file
21
.idea/icon.svg
generated
Normal file
@ -0,0 +1,21 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" x="0px" y="0px"
|
||||
viewBox="0 0 100 100" style="enable-background:new 0 0 100 100;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#CD201F;}
|
||||
.st1{fill:#FFFFFF;}
|
||||
</style>
|
||||
<g id="Alapkör">
|
||||
<circle id="XMLID_23_" class="st0" cx="50" cy="50" r="50"/>
|
||||
</g>
|
||||
<g id="Elemek">
|
||||
<path id="XMLID_19_" class="st1" d="M47,28.2c-9-5.3-15.3-9-15.3-9v61.7c0,0,30.4-18,52.3-30.9C72.1,43,57.7,34.5,47,28.2z"/>
|
||||
</g>
|
||||
<g id="Fedő">
|
||||
<path id="XMLID_5_" class="st0" d="M48.4,40.1c-4.1-2.4-7-4.1-7-4.1V64c0,0,13.9-8.2,23.8-14C59.8,46.8,53.3,42.9,48.4,40.1z"/>
|
||||
<rect id="XMLID_4_" x="41.4" y="55.6" class="st0" width="6.2" height="21"/>
|
||||
</g>
|
||||
<g id="Vonalak">
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 850 B |
225
app/build.gradle
225
app/build.gradle
@ -1,15 +1,18 @@
|
||||
import com.android.tools.profgen.ArtProfileKt
|
||||
import com.android.tools.profgen.ArtProfileSerializer
|
||||
import com.android.tools.profgen.DexFile
|
||||
import com.mikepenz.aboutlibraries.plugin.DuplicateMode
|
||||
|
||||
plugins {
|
||||
id "com.android.application"
|
||||
id "kotlin-android"
|
||||
id "kotlin-kapt"
|
||||
id "kotlin-parcelize"
|
||||
id "checkstyle"
|
||||
id "org.sonarqube" version "4.0.0.2929"
|
||||
id "org.jetbrains.kotlin.plugin.compose" version "${kotlin_version}"
|
||||
alias libs.plugins.android.application
|
||||
alias libs.plugins.kotlin.android
|
||||
alias libs.plugins.kotlin.compose
|
||||
alias libs.plugins.kotlin.kapt
|
||||
alias libs.plugins.kotlin.parcelize
|
||||
alias libs.plugins.checkstyle
|
||||
alias libs.plugins.sonarqube
|
||||
alias libs.plugins.hilt
|
||||
alias libs.plugins.aboutlibraries
|
||||
}
|
||||
|
||||
android {
|
||||
@ -21,8 +24,8 @@ android {
|
||||
resValue "string", "app_name", "NewPipe"
|
||||
minSdk 21
|
||||
targetSdk 33
|
||||
versionCode 997
|
||||
versionName "0.27.0"
|
||||
versionCode 999
|
||||
versionName "0.27.2"
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
|
||||
@ -94,6 +97,7 @@ android {
|
||||
buildFeatures {
|
||||
viewBinding true
|
||||
compose true
|
||||
buildConfig true
|
||||
}
|
||||
|
||||
packagingOptions {
|
||||
@ -107,23 +111,6 @@ android {
|
||||
}
|
||||
}
|
||||
|
||||
ext {
|
||||
checkstyleVersion = '10.12.1'
|
||||
|
||||
androidxLifecycleVersion = '2.6.2'
|
||||
androidxRoomVersion = '2.6.1'
|
||||
androidxWorkVersion = '2.8.1'
|
||||
|
||||
icepickVersion = '3.2.0'
|
||||
exoPlayerVersion = '2.18.7'
|
||||
googleAutoServiceVersion = '1.1.1'
|
||||
groupieVersion = '2.10.1'
|
||||
markwonVersion = '4.6.2'
|
||||
|
||||
leakCanaryVersion = '2.12'
|
||||
stethoVersion = '1.6.0'
|
||||
}
|
||||
|
||||
configurations {
|
||||
checkstyle
|
||||
ktlint
|
||||
@ -133,7 +120,7 @@ checkstyle {
|
||||
getConfigDirectory().set(rootProject.file("checkstyle"))
|
||||
ignoreFailures false
|
||||
showViolations true
|
||||
toolVersion = checkstyleVersion
|
||||
toolVersion = libs.versions.checkstyle.get()
|
||||
}
|
||||
|
||||
tasks.register('runCheckstyle', Checkstyle) {
|
||||
@ -175,11 +162,13 @@ tasks.register('formatKtlint', JavaExec) {
|
||||
jvmArgs("--add-opens", "java.base/java.lang=ALL-UNNAMED")
|
||||
}
|
||||
|
||||
apply from: 'check-dependencies.gradle'
|
||||
|
||||
afterEvaluate {
|
||||
if (!System.properties.containsKey('skipFormatKtlint')) {
|
||||
preDebugBuild.dependsOn formatKtlint
|
||||
}
|
||||
preDebugBuild.dependsOn runCheckstyle, runKtlint
|
||||
preDebugBuild.dependsOn runCheckstyle, runKtlint, checkDependenciesOrder
|
||||
}
|
||||
|
||||
sonar {
|
||||
@ -190,132 +179,154 @@ 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 {
|
||||
/** Desugaring **/
|
||||
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs_nio:2.0.4'
|
||||
coreLibraryDesugaring libs.desugar.jdk.libs.nio
|
||||
|
||||
/** NewPipe libraries **/
|
||||
// You can use a local version by uncommenting a few lines in settings.gradle
|
||||
// Or you can use a commit you pushed to GitHub by just replacing TeamNewPipe with your GitHub
|
||||
// name and the commit hash with the commit hash of the (pushed) commit you want to test
|
||||
// This works thanks to JitPack: https://jitpack.io/
|
||||
implementation 'com.github.TeamNewPipe:nanojson:1d9e1aea9049fc9f85e68b43ba39fe7be1c1f751'
|
||||
implementation 'com.github.TeamNewPipe:NewPipeExtractor:v0.24.2'
|
||||
implementation 'com.github.TeamNewPipe:NoNonsense-FilePicker:5.0.0'
|
||||
implementation libs.teamnewpipe.nanojson
|
||||
implementation libs.teamnewpipe.newpipe.extractor
|
||||
implementation libs.teamnewpipe.nononsense.filepicker
|
||||
|
||||
/** Checkstyle **/
|
||||
checkstyle "com.puppycrawl.tools:checkstyle:${checkstyleVersion}"
|
||||
ktlint 'com.pinterest:ktlint:0.45.2'
|
||||
checkstyle libs.tools.checkstyle
|
||||
ktlint libs.tools.ktlint
|
||||
|
||||
/** Kotlin **/
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib:${kotlin_version}"
|
||||
implementation libs.kotlin.stdlib
|
||||
|
||||
/** AndroidX **/
|
||||
implementation 'androidx.appcompat:appcompat:1.6.1'
|
||||
implementation 'androidx.cardview:cardview:1.0.0'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
|
||||
implementation 'androidx.core:core-ktx:1.12.0'
|
||||
implementation 'androidx.documentfile:documentfile:1.0.1'
|
||||
implementation 'androidx.fragment:fragment-ktx:1.6.2'
|
||||
implementation "androidx.lifecycle:lifecycle-livedata-ktx:${androidxLifecycleVersion}"
|
||||
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:${androidxLifecycleVersion}"
|
||||
implementation 'androidx.localbroadcastmanager:localbroadcastmanager:1.1.0'
|
||||
implementation 'androidx.media:media:1.7.0'
|
||||
implementation 'androidx.preference:preference:1.2.1'
|
||||
implementation 'androidx.recyclerview:recyclerview:1.3.2'
|
||||
implementation "androidx.room:room-runtime:${androidxRoomVersion}"
|
||||
implementation "androidx.room:room-rxjava3:${androidxRoomVersion}"
|
||||
kapt "androidx.room:room-compiler:${androidxRoomVersion}"
|
||||
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
|
||||
// Newer version specified to prevent accessibility regressions with RecyclerView, see:
|
||||
// https://developer.android.com/jetpack/androidx/releases/viewpager2#1.1.0-alpha01
|
||||
implementation 'androidx.viewpager2:viewpager2:1.1.0-beta02'
|
||||
implementation "androidx.work:work-runtime-ktx:${androidxWorkVersion}"
|
||||
implementation "androidx.work:work-rxjava3:${androidxWorkVersion}"
|
||||
implementation 'com.google.android.material:material:1.11.0'
|
||||
implementation libs.androidx.appcompat
|
||||
implementation libs.androidx.cardview
|
||||
implementation libs.androidx.constraintlayout
|
||||
implementation libs.androidx.core.ktx
|
||||
implementation libs.androidx.documentfile
|
||||
implementation libs.androidx.fragment.compose
|
||||
implementation libs.androidx.lifecycle.livedata
|
||||
implementation libs.androidx.lifecycle.viewmodel
|
||||
implementation libs.androidx.localbroadcastmanager
|
||||
implementation libs.androidx.media
|
||||
implementation libs.androidx.preference
|
||||
implementation libs.androidx.recyclerview
|
||||
implementation libs.androidx.room.runtime
|
||||
implementation libs.androidx.room.rxjava3
|
||||
kapt libs.androidx.room.compiler
|
||||
implementation libs.androidx.swiperefreshlayout
|
||||
implementation libs.androidx.work.runtime
|
||||
implementation libs.androidx.work.rxjava3
|
||||
implementation libs.androidx.material
|
||||
|
||||
/** Third-party libraries **/
|
||||
// Instance state boilerplate elimination
|
||||
implementation "frankiesardo:icepick:${icepickVersion}"
|
||||
kapt "frankiesardo:icepick-processor:${icepickVersion}"
|
||||
implementation libs.livefront.bridge
|
||||
implementation libs.android.state
|
||||
kapt libs.android.state.processor
|
||||
|
||||
// HTML parser
|
||||
implementation "org.jsoup:jsoup:1.17.2"
|
||||
implementation libs.jsoup
|
||||
|
||||
// HTTP client
|
||||
implementation "com.squareup.okhttp3:okhttp:4.12.0"
|
||||
implementation libs.okhttp
|
||||
|
||||
// Media player
|
||||
implementation "com.google.android.exoplayer:exoplayer-core:${exoPlayerVersion}"
|
||||
implementation "com.google.android.exoplayer:exoplayer-dash:${exoPlayerVersion}"
|
||||
implementation "com.google.android.exoplayer:exoplayer-database:${exoPlayerVersion}"
|
||||
implementation "com.google.android.exoplayer:exoplayer-datasource:${exoPlayerVersion}"
|
||||
implementation "com.google.android.exoplayer:exoplayer-hls:${exoPlayerVersion}"
|
||||
implementation "com.google.android.exoplayer:exoplayer-smoothstreaming:${exoPlayerVersion}"
|
||||
implementation "com.google.android.exoplayer:exoplayer-ui:${exoPlayerVersion}"
|
||||
implementation "com.google.android.exoplayer:extension-mediasession:${exoPlayerVersion}"
|
||||
implementation libs.exoplayer.core
|
||||
implementation libs.exoplayer.dash
|
||||
implementation libs.exoplayer.database
|
||||
implementation libs.exoplayer.datasource
|
||||
implementation libs.exoplayer.hls
|
||||
implementation libs.exoplayer.smoothstreaming
|
||||
implementation libs.exoplayer.ui
|
||||
implementation libs.extension.mediasession
|
||||
|
||||
// Metadata generator for service descriptors
|
||||
compileOnly "com.google.auto.service:auto-service-annotations:${googleAutoServiceVersion}"
|
||||
kapt "com.google.auto.service:auto-service:${googleAutoServiceVersion}"
|
||||
compileOnly libs.auto.service
|
||||
kapt libs.auto.service.kapt
|
||||
|
||||
// Manager for complex RecyclerView layouts
|
||||
implementation "com.github.lisawray.groupie:groupie:${groupieVersion}"
|
||||
implementation "com.github.lisawray.groupie:groupie-viewbinding:${groupieVersion}"
|
||||
implementation libs.lisawray.groupie
|
||||
implementation libs.lisawray.groupie.viewbinding
|
||||
|
||||
// Image loading
|
||||
implementation 'io.coil-kt:coil-compose:2.7.0'
|
||||
implementation libs.coil.compose
|
||||
implementation libs.coil.network.okhttp
|
||||
|
||||
// Markdown library for Android
|
||||
implementation "io.noties.markwon:core:${markwonVersion}"
|
||||
implementation "io.noties.markwon:linkify:${markwonVersion}"
|
||||
implementation libs.markwon.core
|
||||
implementation libs.markwon.linkify
|
||||
|
||||
// Crash reporting
|
||||
implementation "ch.acra:acra-core:5.11.3"
|
||||
implementation libs.acra.core
|
||||
|
||||
// Properly restarting
|
||||
implementation 'com.jakewharton:process-phoenix:2.1.2'
|
||||
implementation libs.process.phoenix
|
||||
|
||||
// Reactive extensions for Java VM
|
||||
implementation "io.reactivex.rxjava3:rxjava:3.1.8"
|
||||
implementation "io.reactivex.rxjava3:rxandroid:3.0.2"
|
||||
implementation libs.rxjava3.rxjava
|
||||
implementation libs.rxjava3.rxandroid
|
||||
// RxJava binding APIs for Android UI widgets
|
||||
implementation "com.jakewharton.rxbinding4:rxbinding:4.0.0"
|
||||
implementation libs.rxbinding4.rxbinding
|
||||
|
||||
// Date and time formatting
|
||||
implementation "org.ocpsoft.prettytime:prettytime:5.0.8.Final"
|
||||
implementation libs.prettytime
|
||||
|
||||
// Jetpack Compose
|
||||
implementation(platform('androidx.compose:compose-bom:2024.06.00'))
|
||||
implementation 'androidx.compose.material3:material3:1.3.0-beta05'
|
||||
implementation 'androidx.compose.material3.adaptive:adaptive:1.0.0-beta04'
|
||||
implementation 'androidx.activity:activity-compose'
|
||||
implementation 'androidx.compose.ui:ui-tooling-preview'
|
||||
implementation 'androidx.compose.ui:ui-text:1.7.0-beta06' // Needed for parsing HTML to AnnotatedString
|
||||
implementation 'androidx.lifecycle:lifecycle-viewmodel-compose'
|
||||
implementation 'androidx.paging:paging-compose:3.3.1'
|
||||
implementation 'com.github.nanihadesuka:LazyColumnScrollbar:2.2.0'
|
||||
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 **/
|
||||
// Memory leak detection
|
||||
debugImplementation "com.squareup.leakcanary:leakcanary-object-watcher-android:${leakCanaryVersion}"
|
||||
debugImplementation "com.squareup.leakcanary:plumber-android:${leakCanaryVersion}"
|
||||
debugImplementation "com.squareup.leakcanary:leakcanary-android-core:${leakCanaryVersion}"
|
||||
debugImplementation libs.leakcanary.object.watcher
|
||||
debugImplementation libs.leakcanary.plumber.android
|
||||
debugImplementation libs.leakcanary.android.core
|
||||
// Debug bridge for Android
|
||||
debugImplementation "com.facebook.stetho:stetho:${stethoVersion}"
|
||||
debugImplementation "com.facebook.stetho:stetho-okhttp3:${stethoVersion}"
|
||||
debugImplementation libs.stetho
|
||||
debugImplementation libs.stetho.okhttp3
|
||||
|
||||
// Jetpack Compose
|
||||
debugImplementation 'androidx.compose.ui:ui-tooling'
|
||||
debugImplementation libs.androidx.compose.ui.tooling
|
||||
|
||||
/** Testing **/
|
||||
testImplementation 'junit:junit:4.13.2'
|
||||
testImplementation 'org.mockito:mockito-core:5.6.0'
|
||||
testImplementation libs.junit
|
||||
testImplementation libs.mockito.core
|
||||
|
||||
androidTestImplementation "androidx.test.ext:junit:1.1.5"
|
||||
androidTestImplementation "androidx.test:runner:1.5.2"
|
||||
androidTestImplementation "androidx.room:room-testing:${androidxRoomVersion}"
|
||||
androidTestImplementation "org.assertj:assertj-core:3.24.2"
|
||||
androidTestImplementation libs.androidx.junit
|
||||
androidTestImplementation libs.androidx.runner
|
||||
androidTestImplementation libs.androidx.room.testing
|
||||
androidTestImplementation libs.assertj.core
|
||||
}
|
||||
|
||||
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"))
|
||||
}
|
||||
}
|
||||
}
|
10
app/proguard-rules.pro
vendored
10
app/proguard-rules.pro
vendored
@ -7,20 +7,12 @@
|
||||
-keep class org.schabi.newpipe.extractor.timeago.patterns.** { *; }
|
||||
-keep class org.mozilla.javascript.** { *; }
|
||||
-keep class org.mozilla.classfile.ClassFileWriter
|
||||
-dontwarn org.mozilla.javascript.JavaToJSONConverters
|
||||
-dontwarn org.mozilla.javascript.tools.**
|
||||
|
||||
## Rules for ExoPlayer
|
||||
-keep class com.google.android.exoplayer2.** { *; }
|
||||
|
||||
## Rules for Icepick. Copy pasted from https://github.com/frankiesardo/icepick
|
||||
-dontwarn icepick.**
|
||||
-keep class icepick.** { *; }
|
||||
-keep class **$$Icepick { *; }
|
||||
-keepclasseswithmembernames class * {
|
||||
@icepick.* <fields>;
|
||||
}
|
||||
-keepnames class * { @icepick.State *;}
|
||||
|
||||
## Rules for OkHttp. Copy pasted from https://github.com/square/okhttp
|
||||
-dontwarn okhttp3.**
|
||||
-dontwarn okio.**
|
||||
|
@ -77,6 +77,11 @@
|
||||
android:exported="false"
|
||||
android:label="@string/settings" />
|
||||
|
||||
<activity
|
||||
android:name=".settings.SettingsV2Activity"
|
||||
android:exported="true"
|
||||
android:label="@string/settings" />
|
||||
|
||||
<activity
|
||||
android:name=".about.AboutActivity"
|
||||
android:exported="false"
|
||||
|
@ -1,275 +0,0 @@
|
||||
package org.schabi.newpipe;
|
||||
|
||||
import android.app.ActivityManager;
|
||||
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.core.content.ContextCompat;
|
||||
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.ktx.ExceptionUtils;
|
||||
import org.schabi.newpipe.settings.NewPipeSettings;
|
||||
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 java.io.IOException;
|
||||
import java.io.InterruptedIOException;
|
||||
import java.net.SocketException;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
import coil.ImageLoader;
|
||||
import coil.ImageLoaderFactory;
|
||||
import coil.util.DebugLogger;
|
||||
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 implements ImageLoaderFactory {
|
||||
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()));
|
||||
|
||||
StateSaver.init(this);
|
||||
initNotificationChannels();
|
||||
|
||||
ServiceHelper.initServices(this);
|
||||
|
||||
// Initialize image loader
|
||||
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
|
||||
ImageStrategy.setPreferredImageQuality(PreferredImageQuality.fromPreferenceKey(this,
|
||||
prefs.getString(getString(R.string.image_quality_key),
|
||||
getString(R.string.image_quality_default))));
|
||||
|
||||
configureRxJavaErrorHandler();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public ImageLoader newImageLoader() {
|
||||
return new ImageLoader.Builder(this)
|
||||
.allowRgb565(ContextCompat.getSystemService(this, ActivityManager.class)
|
||||
.isLowRamDevice())
|
||||
.logger(BuildConfig.DEBUG ? new DebugLogger() : null)
|
||||
.crossfade(true)
|
||||
.build();
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
286
app/src/main/java/org/schabi/newpipe/App.kt
Normal file
286
app/src/main/java/org/schabi/newpipe/App.kt
Normal file
@ -0,0 +1,286 @@
|
||||
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.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 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()
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
@ -10,8 +10,9 @@ import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
|
||||
import icepick.Icepick;
|
||||
import icepick.State;
|
||||
import com.evernote.android.state.State;
|
||||
import com.livefront.bridge.Bridge;
|
||||
|
||||
|
||||
public abstract class BaseFragment extends Fragment {
|
||||
protected final String TAG = getClass().getSimpleName() + "@" + Integer.toHexString(hashCode());
|
||||
@ -48,7 +49,7 @@ public abstract class BaseFragment extends Fragment {
|
||||
+ "savedInstanceState = [" + savedInstanceState + "]");
|
||||
}
|
||||
super.onCreate(savedInstanceState);
|
||||
Icepick.restoreInstanceState(this, savedInstanceState);
|
||||
Bridge.restoreInstanceState(this, savedInstanceState);
|
||||
if (savedInstanceState != null) {
|
||||
onRestoreInstanceState(savedInstanceState);
|
||||
}
|
||||
@ -70,7 +71,7 @@ public abstract class BaseFragment extends Fragment {
|
||||
@Override
|
||||
public void onSaveInstanceState(@NonNull final Bundle outState) {
|
||||
super.onSaveInstanceState(outState);
|
||||
Icepick.saveInstanceState(this, outState);
|
||||
Bridge.saveInstanceState(this, outState);
|
||||
}
|
||||
|
||||
protected void onRestoreInstanceState(@NonNull final Bundle savedInstanceState) {
|
||||
|
@ -48,6 +48,11 @@ public final class DownloaderImpl extends Downloader {
|
||||
this.mCookies = new HashMap<>();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public OkHttpClient getClient() {
|
||||
return client;
|
||||
}
|
||||
|
||||
/**
|
||||
* It's recommended to call exactly once in the entire lifetime of the application.
|
||||
*
|
||||
|
@ -166,7 +166,7 @@ public class MainActivity extends AppCompatActivity {
|
||||
NotificationWorker.initialize(this);
|
||||
}
|
||||
if (!UpdateSettingsFragment.wasUserAskedForConsent(this)
|
||||
&& !App.getApp().isFirstRun()
|
||||
&& !App.getInstance().isFirstRun()
|
||||
&& ReleaseVersionUtil.INSTANCE.isReleaseApk()) {
|
||||
UpdateSettingsFragment.askForConsentToUpdateChecks(this);
|
||||
}
|
||||
@ -176,7 +176,7 @@ public class MainActivity extends AppCompatActivity {
|
||||
protected void onPostCreate(final Bundle savedInstanceState) {
|
||||
super.onPostCreate(savedInstanceState);
|
||||
|
||||
final App app = App.getApp();
|
||||
final App app = App.getInstance();
|
||||
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(app);
|
||||
|
||||
if (prefs.getBoolean(app.getString(R.string.update_app_key), false)
|
||||
@ -553,28 +553,27 @@ public class MainActivity extends AppCompatActivity {
|
||||
// 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
|
||||
// handled by it
|
||||
final var fragmentManager = getSupportFragmentManager();
|
||||
|
||||
if (bottomSheetHiddenOrCollapsed()) {
|
||||
final var fm = getSupportFragmentManager();
|
||||
final var fragment = fm.findFragmentById(R.id.fragment_holder);
|
||||
final var fragment = fragmentManager.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 backPressable && backPressable.onBackPressed()) {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
final var fragmentPlayer = getSupportFragmentManager()
|
||||
.findFragmentById(R.id.fragment_player_holder);
|
||||
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 (fragmentPlayer instanceof BackPressable backPressable
|
||||
&& !backPressable.onBackPressed()) {
|
||||
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();
|
||||
} else {
|
||||
super.onBackPressed();
|
||||
|
@ -41,6 +41,9 @@ import androidx.lifecycle.Lifecycle;
|
||||
import androidx.lifecycle.LifecycleOwner;
|
||||
import androidx.preference.PreferenceManager;
|
||||
|
||||
import com.evernote.android.state.State;
|
||||
import com.livefront.bridge.Bridge;
|
||||
|
||||
import org.schabi.newpipe.database.stream.model.StreamEntity;
|
||||
import org.schabi.newpipe.databinding.ListRadioIconItemBinding;
|
||||
import org.schabi.newpipe.databinding.SingleChoiceDialogViewBinding;
|
||||
@ -98,8 +101,6 @@ import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import icepick.Icepick;
|
||||
import icepick.State;
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.rxjava3.core.Observable;
|
||||
import io.reactivex.rxjava3.core.Single;
|
||||
@ -152,7 +153,7 @@ public class RouterActivity extends AppCompatActivity {
|
||||
getWindow().setAttributes(params);
|
||||
|
||||
super.onCreate(savedInstanceState);
|
||||
Icepick.restoreInstanceState(this, savedInstanceState);
|
||||
Bridge.restoreInstanceState(this, savedInstanceState);
|
||||
|
||||
// FragmentManager will take care to recreate (Playlist|Download)Dialog when screen rotates
|
||||
// We used to .setOnDismissListener(dialog -> finish()); when creating these DialogFragments
|
||||
@ -197,7 +198,7 @@ public class RouterActivity extends AppCompatActivity {
|
||||
@Override
|
||||
protected void onSaveInstanceState(@NonNull final Bundle outState) {
|
||||
super.onSaveInstanceState(outState);
|
||||
Icepick.saveInstanceState(this, outState);
|
||||
Bridge.saveInstanceState(this, outState);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -1,199 +1,31 @@
|
||||
package org.schabi.newpipe.about
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.Button
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import androidx.viewpager2.adapter.FragmentStateAdapter
|
||||
import com.google.android.material.tabs.TabLayoutMediator
|
||||
import org.schabi.newpipe.BuildConfig
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import org.schabi.newpipe.R
|
||||
import org.schabi.newpipe.databinding.ActivityAboutBinding
|
||||
import org.schabi.newpipe.databinding.FragmentAboutBinding
|
||||
import org.schabi.newpipe.ui.components.common.ScaffoldWithToolbar
|
||||
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.ThemeHelper
|
||||
import org.schabi.newpipe.util.external_communication.ShareUtils
|
||||
|
||||
class AboutActivity : AppCompatActivity() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
Localization.assureCorrectAppLanguage(this)
|
||||
enableEdgeToEdge()
|
||||
super.onCreate(savedInstanceState)
|
||||
ThemeHelper.setTheme(this)
|
||||
title = getString(R.string.title_activity_about)
|
||||
|
||||
val aboutBinding = ActivityAboutBinding.inflate(layoutInflater)
|
||||
setContentView(aboutBinding.root)
|
||||
setSupportActionBar(aboutBinding.aboutToolbar)
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
|
||||
// Create the adapter that will return a fragment for each of the three
|
||||
// primary sections of the activity.
|
||||
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))
|
||||
setContent {
|
||||
AppTheme {
|
||||
ScaffoldWithToolbar(
|
||||
title = stringResource(R.string.title_activity_about),
|
||||
onBackClick = { onBackPressedDispatcher.onBackPressed() }
|
||||
) { padding ->
|
||||
AboutScreen(padding)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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(
|
||||
"Icepick", "2015", "Frankie Sardo",
|
||||
"https://github.com/frankiesardo/icepick", StandardLicenses.EPL1
|
||||
),
|
||||
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(
|
||||
"Coil", "2023", "Coil Contributors",
|
||||
"https://coil-kt.github.io/coil/", 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 io.reactivex.rxjava3.core.Completable
|
||||
import io.reactivex.rxjava3.core.Flowable
|
||||
import io.reactivex.rxjava3.core.Maybe
|
||||
import org.schabi.newpipe.database.BasicDAO
|
||||
import org.schabi.newpipe.database.stream.model.StreamEntity
|
||||
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>>
|
||||
|
||||
@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")
|
||||
abstract fun setUploaderUrl(serviceId: Long, url: String, uploaderUrl: String): Completable
|
||||
|
@ -1,5 +1,8 @@
|
||||
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.Insert;
|
||||
import androidx.room.OnConflictStrategy;
|
||||
@ -12,9 +15,7 @@ import org.schabi.newpipe.database.stream.model.StreamStateEntity;
|
||||
import java.util.List;
|
||||
|
||||
import io.reactivex.rxjava3.core.Flowable;
|
||||
|
||||
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 io.reactivex.rxjava3.core.Maybe;
|
||||
|
||||
@Dao
|
||||
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")
|
||||
Flowable<List<StreamStateEntity>> getState(long streamId);
|
||||
Maybe<StreamStateEntity> getState(long streamId);
|
||||
|
||||
@Query("DELETE FROM " + STREAM_STATE_TABLE + " WHERE " + JOIN_STREAM_ID + " = :streamId")
|
||||
int deleteState(long streamId);
|
||||
|
@ -39,6 +39,8 @@ import androidx.documentfile.provider.DocumentFile;
|
||||
import androidx.fragment.app.DialogFragment;
|
||||
import androidx.preference.PreferenceManager;
|
||||
|
||||
import com.evernote.android.state.State;
|
||||
import com.livefront.bridge.Bridge;
|
||||
import com.nononsenseapps.filepicker.Utils;
|
||||
|
||||
import org.schabi.newpipe.MainActivity;
|
||||
@ -59,6 +61,8 @@ import org.schabi.newpipe.settings.NewPipeSettings;
|
||||
import org.schabi.newpipe.streams.io.NoFileManagerSafeGuard;
|
||||
import org.schabi.newpipe.streams.io.StoredDirectoryHelper;
|
||||
import org.schabi.newpipe.streams.io.StoredFileHelper;
|
||||
import org.schabi.newpipe.util.AudioTrackAdapter;
|
||||
import org.schabi.newpipe.util.AudioTrackAdapter.AudioTracksWrapper;
|
||||
import org.schabi.newpipe.util.FilePickerActivityHelper;
|
||||
import org.schabi.newpipe.util.FilenameUtils;
|
||||
import org.schabi.newpipe.util.ListHelper;
|
||||
@ -67,8 +71,6 @@ import org.schabi.newpipe.util.SecondaryStreamHelper;
|
||||
import org.schabi.newpipe.util.SimpleOnSeekBarChangeListener;
|
||||
import org.schabi.newpipe.util.StreamItemAdapter;
|
||||
import org.schabi.newpipe.util.StreamItemAdapter.StreamInfoWrapper;
|
||||
import org.schabi.newpipe.util.AudioTrackAdapter;
|
||||
import org.schabi.newpipe.util.AudioTrackAdapter.AudioTracksWrapper;
|
||||
import org.schabi.newpipe.util.ThemeHelper;
|
||||
|
||||
import java.io.File;
|
||||
@ -79,8 +81,6 @@ import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
|
||||
import icepick.Icepick;
|
||||
import icepick.State;
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
||||
import us.shandian.giga.get.MissionRecoveryInfo;
|
||||
import us.shandian.giga.postprocessing.Postprocessing;
|
||||
@ -214,7 +214,7 @@ public class DownloadDialog extends DialogFragment
|
||||
context = getContext();
|
||||
|
||||
setStyle(STYLE_NO_TITLE, ThemeHelper.getDialogTheme(context));
|
||||
Icepick.restoreInstanceState(this, savedInstanceState);
|
||||
Bridge.restoreInstanceState(this, savedInstanceState);
|
||||
|
||||
this.audioTrackAdapter = new AudioTrackAdapter(wrappedAudioTracks);
|
||||
this.subtitleStreamsAdapter = new StreamItemAdapter<>(wrappedSubtitleStreams);
|
||||
@ -372,7 +372,7 @@ public class DownloadDialog extends DialogFragment
|
||||
@Override
|
||||
public void onSaveInstanceState(@NonNull final Bundle outState) {
|
||||
super.onSaveInstanceState(outState);
|
||||
Icepick.saveInstanceState(this, outState);
|
||||
Bridge.saveInstanceState(this, outState);
|
||||
}
|
||||
|
||||
|
||||
|
@ -2,7 +2,6 @@ package org.schabi.newpipe.error;
|
||||
|
||||
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
@ -13,7 +12,6 @@ import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
@ -22,7 +20,6 @@ import androidx.core.content.IntentCompat;
|
||||
import com.grack.nanojson.JsonWriter;
|
||||
|
||||
import org.schabi.newpipe.BuildConfig;
|
||||
import org.schabi.newpipe.MainActivity;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.databinding.ActivityErrorBinding;
|
||||
import org.schabi.newpipe.util.Localization;
|
||||
@ -187,25 +184,6 @@ public class ErrorActivity extends AppCompatActivity {
|
||||
.collect(Collectors.joining(separator + "\n", separator + "\n", separator));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the checked activity.
|
||||
*
|
||||
* @param returnActivity the activity to return to
|
||||
* @return the casted return activity or null
|
||||
*/
|
||||
@Nullable
|
||||
static Class<? extends Activity> getReturnActivity(final Class<?> returnActivity) {
|
||||
Class<? extends Activity> checkedReturnActivity = null;
|
||||
if (returnActivity != null) {
|
||||
if (Activity.class.isAssignableFrom(returnActivity)) {
|
||||
checkedReturnActivity = returnActivity.asSubclass(Activity.class);
|
||||
} else {
|
||||
checkedReturnActivity = MainActivity.class;
|
||||
}
|
||||
}
|
||||
return checkedReturnActivity;
|
||||
}
|
||||
|
||||
private void buildInfo(final ErrorInfo info) {
|
||||
String text = "";
|
||||
|
||||
|
@ -188,7 +188,7 @@ public class ReCaptchaActivity extends AppCompatActivity {
|
||||
handleCookies(Utils.decodeUrlUtf8(url.substring(abuseStart + 13, abuseEnd)));
|
||||
} catch (final StringIndexOutOfBoundsException e) {
|
||||
if (MainActivity.DEBUG) {
|
||||
Log.d(TAG, "handleCookiesFromUrl: invalid google abuse starting at "
|
||||
Log.e(TAG, "handleCookiesFromUrl: invalid google abuse starting at "
|
||||
+ abuseStart + " and ending at " + abuseEnd + " for url " + url, e);
|
||||
}
|
||||
}
|
||||
|
@ -13,6 +13,8 @@ import androidx.annotation.Nullable;
|
||||
import androidx.annotation.StringRes;
|
||||
import androidx.fragment.app.Fragment;
|
||||
|
||||
import com.evernote.android.state.State;
|
||||
|
||||
import org.schabi.newpipe.BaseFragment;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.error.ErrorInfo;
|
||||
@ -22,8 +24,6 @@ import org.schabi.newpipe.util.InfoCache;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
import icepick.State;
|
||||
|
||||
public abstract class BaseStateFragment<I> extends BaseFragment implements ViewContract<I> {
|
||||
@State
|
||||
protected AtomicBoolean wasLoading = new AtomicBoolean();
|
||||
@ -134,6 +134,7 @@ public abstract class BaseStateFragment<I> extends BaseFragment implements ViewC
|
||||
hideErrorPanel();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void showEmptyState() {
|
||||
isLoading.set(false);
|
||||
if (emptyStateView != null) {
|
||||
|
@ -6,9 +6,11 @@ import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.compose.ui.platform.ComposeView;
|
||||
|
||||
import org.schabi.newpipe.BaseFragment;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.ui.emptystate.EmptyStateUtil;
|
||||
|
||||
public class EmptyFragment extends BaseFragment {
|
||||
private static final String SHOW_MESSAGE = "SHOW_MESSAGE";
|
||||
@ -26,8 +28,10 @@ public class EmptyFragment extends BaseFragment {
|
||||
final Bundle savedInstanceState) {
|
||||
final boolean showMessage = getArguments().getBoolean(SHOW_MESSAGE);
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
@ -11,6 +11,8 @@ import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.StringRes;
|
||||
|
||||
import com.evernote.android.state.State;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.extractor.StreamingService;
|
||||
import org.schabi.newpipe.extractor.stream.Description;
|
||||
@ -19,8 +21,6 @@ import org.schabi.newpipe.util.Localization;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import icepick.State;
|
||||
|
||||
public class DescriptionFragment extends BaseDescriptionFragment {
|
||||
|
||||
@State
|
||||
@ -31,7 +31,7 @@ public class DescriptionFragment extends BaseDescriptionFragment {
|
||||
}
|
||||
|
||||
public DescriptionFragment() {
|
||||
// keep empty constructor for IcePick when resuming fragment from memory
|
||||
// keep empty constructor for State when resuming fragment from memory
|
||||
}
|
||||
|
||||
|
||||
|
@ -56,6 +56,7 @@ import androidx.core.content.ContextCompat;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.preference.PreferenceManager;
|
||||
|
||||
import com.evernote.android.state.State;
|
||||
import com.google.android.exoplayer2.PlaybackException;
|
||||
import com.google.android.exoplayer2.PlaybackParameters;
|
||||
import com.google.android.material.appbar.AppBarLayout;
|
||||
@ -73,7 +74,6 @@ import org.schabi.newpipe.error.ReCaptchaActivity;
|
||||
import org.schabi.newpipe.error.UserAction;
|
||||
import org.schabi.newpipe.extractor.Image;
|
||||
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.ExtractionException;
|
||||
import org.schabi.newpipe.extractor.stream.AudioStream;
|
||||
@ -127,8 +127,7 @@ import java.util.Optional;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import coil.util.CoilUtils;
|
||||
import icepick.State;
|
||||
import coil3.util.CoilUtils;
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
||||
import io.reactivex.rxjava3.disposables.Disposable;
|
||||
@ -1011,19 +1010,6 @@ public final class VideoDetailFragment
|
||||
updateTabLayoutVisibility();
|
||||
}
|
||||
|
||||
public void scrollToComment(final CommentsInfoItem comment) {
|
||||
final int commentsTabPos = pageAdapter.getItemPositionByTitle(COMMENTS_TAB_TAG);
|
||||
final var fragment = pageAdapter.getItem(commentsTabPos);
|
||||
|
||||
// TODO: Implement the scrolling with Compose.
|
||||
// unexpand the app bar only if scrolling to the comment succeeded
|
||||
// if (fragment instanceof CommentsFragment commentsFragment &&
|
||||
// commentsFragment.scrollToComment(comment)) {
|
||||
// binding.appBarLayout.setExpanded(false, false);
|
||||
// binding.viewPager.setCurrentItem(commentsTabPos, false);
|
||||
// }
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Play Utils
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
@ -9,6 +9,8 @@ import android.view.View;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.evernote.android.state.State;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.error.ErrorInfo;
|
||||
import org.schabi.newpipe.error.UserAction;
|
||||
@ -24,7 +26,6 @@ import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Queue;
|
||||
|
||||
import icepick.State;
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.rxjava3.core.Single;
|
||||
import io.reactivex.rxjava3.disposables.Disposable;
|
||||
@ -143,7 +144,7 @@ public abstract class BaseListInfoFragment<I extends InfoItem, L extends ListInf
|
||||
currentWorker = loadResult(forceLoad)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe((@NonNull L result) -> {
|
||||
.subscribe((@NonNull final L result) -> {
|
||||
isLoading.set(false);
|
||||
currentInfo = result;
|
||||
currentNextPage = result.getNextPage();
|
||||
|
@ -10,6 +10,8 @@ import android.widget.LinearLayout;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.evernote.android.state.State;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.extractor.StreamingService;
|
||||
import org.schabi.newpipe.extractor.channel.ChannelInfo;
|
||||
@ -20,8 +22,6 @@ import org.schabi.newpipe.util.Localization;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import icepick.State;
|
||||
|
||||
public class ChannelAboutFragment extends BaseDescriptionFragment {
|
||||
@State
|
||||
protected ChannelInfo channelInfo;
|
||||
@ -31,7 +31,7 @@ public class ChannelAboutFragment extends BaseDescriptionFragment {
|
||||
}
|
||||
|
||||
public ChannelAboutFragment() {
|
||||
// keep empty constructor for IcePick when resuming fragment from memory
|
||||
// keep empty constructor for State when resuming fragment from memory
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -10,7 +10,6 @@ import android.graphics.Color;
|
||||
import android.os.Bundle;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
import android.util.TypedValue;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
@ -25,6 +24,7 @@ import androidx.core.graphics.ColorUtils;
|
||||
import androidx.core.view.MenuProvider;
|
||||
import androidx.preference.PreferenceManager;
|
||||
|
||||
import com.evernote.android.state.State;
|
||||
import com.google.android.material.snackbar.Snackbar;
|
||||
import com.google.android.material.tabs.TabLayout;
|
||||
import com.jakewharton.rxbinding4.view.RxView;
|
||||
@ -44,6 +44,8 @@ import org.schabi.newpipe.fragments.detail.TabAdapter;
|
||||
import org.schabi.newpipe.ktx.AnimationType;
|
||||
import org.schabi.newpipe.local.feed.notifications.NotificationHelper;
|
||||
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.Constants;
|
||||
import org.schabi.newpipe.util.ExtractorHelper;
|
||||
@ -59,8 +61,7 @@ import java.util.List;
|
||||
import java.util.Queue;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import coil.util.CoilUtils;
|
||||
import icepick.State;
|
||||
import coil3.util.CoilUtils;
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.rxjava3.core.Observable;
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
||||
@ -199,6 +200,11 @@ public class ChannelFragment extends BaseStateFragment<ChannelInfo>
|
||||
protected void initViews(final View rootView, final Bundle savedInstanceState) {
|
||||
super.initViews(rootView, savedInstanceState);
|
||||
|
||||
EmptyStateUtil.setEmptyStateComposable(
|
||||
binding.emptyStateView,
|
||||
EmptyStateSpec.Companion.getContentNotSupported()
|
||||
);
|
||||
|
||||
tabAdapter = new TabAdapter(getChildFragmentManager());
|
||||
binding.viewPager.setAdapter(tabAdapter);
|
||||
binding.tabLayout.setupWithViewPager(binding.viewPager);
|
||||
@ -249,7 +255,7 @@ public class ChannelFragment extends BaseStateFragment<ChannelInfo>
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
private void monitorSubscription(final ChannelInfo info) {
|
||||
final Consumer<Throwable> onError = (Throwable throwable) -> {
|
||||
final Consumer<Throwable> onError = (final Throwable throwable) -> {
|
||||
animate(binding.channelSubscribeButton, false, 100);
|
||||
showSnackBarError(new ErrorInfo(throwable, UserAction.SUBSCRIPTION_GET,
|
||||
"Get subscription status", currentInfo));
|
||||
@ -284,14 +290,14 @@ public class ChannelFragment extends BaseStateFragment<ChannelInfo>
|
||||
}
|
||||
|
||||
private Function<Object, Object> mapOnSubscribe(final SubscriptionEntity subscription) {
|
||||
return (@NonNull Object o) -> {
|
||||
return (@NonNull final Object o) -> {
|
||||
subscriptionManager.insertSubscription(subscription);
|
||||
return o;
|
||||
};
|
||||
}
|
||||
|
||||
private Function<Object, Object> mapOnUnsubscribe(final SubscriptionEntity subscription) {
|
||||
return (@NonNull Object o) -> {
|
||||
return (@NonNull final Object o) -> {
|
||||
subscriptionManager.deleteSubscription(subscription);
|
||||
return o;
|
||||
};
|
||||
@ -318,7 +324,7 @@ public class ChannelFragment extends BaseStateFragment<ChannelInfo>
|
||||
}
|
||||
|
||||
private Disposable monitorSubscribeButton(final Function<Object, Object> action) {
|
||||
final Consumer<Object> onNext = (@NonNull Object o) -> {
|
||||
final Consumer<Object> onNext = (@NonNull final Object o) -> {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "Changed subscription status to this channel!");
|
||||
}
|
||||
@ -338,7 +344,7 @@ public class ChannelFragment extends BaseStateFragment<ChannelInfo>
|
||||
}
|
||||
|
||||
private Consumer<List<SubscriptionEntity>> getSubscribeUpdateMonitor(final ChannelInfo info) {
|
||||
return (List<SubscriptionEntity> subscriptionEntities) -> {
|
||||
return (final List<SubscriptionEntity> subscriptionEntities) -> {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "subscriptionManager.subscriptionTable.doOnNext() called with: "
|
||||
+ "subscriptionEntities = [" + subscriptionEntities + "]");
|
||||
@ -645,8 +651,6 @@ public class ChannelFragment extends BaseStateFragment<ChannelInfo>
|
||||
return;
|
||||
}
|
||||
|
||||
binding.errorContentNotSupported.setVisibility(View.VISIBLE);
|
||||
binding.channelKaomoji.setText("(︶︹︺)");
|
||||
binding.channelKaomoji.setTextSize(TypedValue.COMPLEX_UNIT_SP, 45f);
|
||||
binding.emptyStateView.setVisibility(View.VISIBLE);
|
||||
}
|
||||
}
|
||||
|
@ -9,6 +9,8 @@ import android.view.ViewGroup;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.evernote.android.state.State;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.databinding.PlaylistControlBinding;
|
||||
import org.schabi.newpipe.error.UserAction;
|
||||
@ -24,6 +26,7 @@ import org.schabi.newpipe.fragments.list.BaseListInfoFragment;
|
||||
import org.schabi.newpipe.fragments.list.playlist.PlaylistControlViewHolder;
|
||||
import org.schabi.newpipe.player.playqueue.ChannelTabPlayQueue;
|
||||
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
||||
import org.schabi.newpipe.ui.emptystate.EmptyStateUtil;
|
||||
import org.schabi.newpipe.util.ChannelTabHelper;
|
||||
import org.schabi.newpipe.util.ExtractorHelper;
|
||||
import org.schabi.newpipe.util.PlayButtonHelper;
|
||||
@ -32,13 +35,12 @@ import java.util.List;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import icepick.State;
|
||||
import io.reactivex.rxjava3.core.Single;
|
||||
|
||||
public class ChannelTabFragment extends BaseListInfoFragment<InfoItem, ChannelTabInfo>
|
||||
implements PlaylistControlViewHolder {
|
||||
|
||||
// states must be protected and not private for IcePick being able to access them
|
||||
// states must be protected and not private for State being able to access them
|
||||
@State
|
||||
protected ListLinkHandler tabHandler;
|
||||
@State
|
||||
@ -78,6 +80,12 @@ public class ChannelTabFragment extends BaseListInfoFragment<InfoItem, ChannelTa
|
||||
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
|
||||
public void onDestroyView() {
|
||||
super.onDestroyView();
|
||||
@ -156,6 +164,7 @@ public class ChannelTabFragment extends BaseListInfoFragment<InfoItem, ChannelTa
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public PlayQueue getPlayQueue() {
|
||||
final List<StreamInfoItem> streamItems = infoListAdapter.getItemsList().stream()
|
||||
.filter(StreamInfoItem.class::isInstance)
|
||||
|
@ -3,28 +3,25 @@ package org.schabi.newpipe.fragments.list.comments
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import androidx.compose.ui.platform.ComposeView
|
||||
import androidx.compose.ui.platform.ViewCompositionStrategy
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import org.schabi.newpipe.ui.components.comment.CommentSection
|
||||
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
|
||||
import org.schabi.newpipe.viewmodels.CommentsViewModel
|
||||
|
||||
class CommentsFragment : Fragment() {
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
) = ComposeView(requireContext()).apply {
|
||||
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
|
||||
setContent {
|
||||
val viewModel = viewModel<CommentsViewModel>()
|
||||
AppTheme {
|
||||
CommentSection(commentsFlow = viewModel.comments)
|
||||
) = content {
|
||||
AppTheme {
|
||||
Surface(color = MaterialTheme.colorScheme.background) {
|
||||
CommentSection()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -11,6 +11,8 @@ import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
|
||||
import com.evernote.android.state.State;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.error.ErrorInfo;
|
||||
import org.schabi.newpipe.error.UserAction;
|
||||
@ -29,7 +31,6 @@ import org.schabi.newpipe.util.ExtractorHelper;
|
||||
import org.schabi.newpipe.util.KioskTranslator;
|
||||
import org.schabi.newpipe.util.Localization;
|
||||
|
||||
import icepick.State;
|
||||
import io.reactivex.rxjava3.core.Single;
|
||||
|
||||
/**
|
||||
|
@ -3,10 +3,9 @@ package org.schabi.newpipe.fragments.list.playlist
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import androidx.compose.ui.platform.ComposeView
|
||||
import androidx.compose.ui.platform.ViewCompositionStrategy
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.compose.content
|
||||
import org.schabi.newpipe.ui.screens.PlaylistScreen
|
||||
import org.schabi.newpipe.ui.theme.AppTheme
|
||||
import org.schabi.newpipe.util.KEY_SERVICE_ID
|
||||
@ -17,12 +16,9 @@ class PlaylistFragment : Fragment() {
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?,
|
||||
) = ComposeView(requireContext()).apply {
|
||||
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
|
||||
setContent {
|
||||
AppTheme {
|
||||
PlaylistScreen()
|
||||
}
|
||||
) = content {
|
||||
AppTheme {
|
||||
PlaylistScreen()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -40,6 +40,8 @@ import androidx.preference.PreferenceManager;
|
||||
import androidx.recyclerview.widget.ItemTouchHelper;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.evernote.android.state.State;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.databinding.FragmentSearchBinding;
|
||||
import org.schabi.newpipe.error.ErrorInfo;
|
||||
@ -62,6 +64,8 @@ import org.schabi.newpipe.ktx.AnimationType;
|
||||
import org.schabi.newpipe.ktx.ExceptionUtils;
|
||||
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
||||
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.DeviceUtils;
|
||||
import org.schabi.newpipe.util.ExtractorHelper;
|
||||
@ -77,7 +81,6 @@ import java.util.Queue;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import icepick.State;
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.rxjava3.core.Observable;
|
||||
import io.reactivex.rxjava3.core.Single;
|
||||
@ -343,6 +346,10 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
||||
protected void initViews(final View rootView, final Bundle savedInstanceState) {
|
||||
super.initViews(rootView, savedInstanceState);
|
||||
|
||||
EmptyStateUtil.setEmptyStateComposable(
|
||||
searchBinding.emptyStateView,
|
||||
EmptyStateSpec.Companion.getNoSearchResult());
|
||||
|
||||
searchBinding.suggestionsList.setAdapter(suggestionListAdapter);
|
||||
// animations are just strange and useless, since the suggestions keep changing too much
|
||||
searchBinding.suggestionsList.setItemAnimator(null);
|
||||
@ -550,7 +557,7 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
||||
}
|
||||
});
|
||||
|
||||
searchEditText.setOnFocusChangeListener((View v, boolean hasFocus) -> {
|
||||
searchEditText.setOnFocusChangeListener((final View v, final boolean hasFocus) -> {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "onFocusChange() called with: "
|
||||
+ "v = [" + v + "], hasFocus = [" + hasFocus + "]");
|
||||
@ -611,7 +618,7 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
||||
};
|
||||
searchEditText.addTextChangedListener(textWatcher);
|
||||
searchEditText.setOnEditorActionListener(
|
||||
(TextView v, int actionId, KeyEvent event) -> {
|
||||
(final TextView v, final int actionId, final KeyEvent event) -> {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "onEditorAction() called with: v = [" + v + "], "
|
||||
+ "actionId = [" + actionId + "], event = [" + event + "]");
|
||||
|
@ -1,176 +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.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.fragments.list.BaseListInfoFragment;
|
||||
import org.schabi.newpipe.info_list.ItemViewMode;
|
||||
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;
|
||||
}
|
||||
}
|
@ -0,0 +1,36 @@
|
||||
package org.schabi.newpipe.fragments.list.videos
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
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(color = MaterialTheme.colorScheme.background) {
|
||||
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()));
|
||||
}
|
||||
}
|
@ -4,6 +4,10 @@ package org.schabi.newpipe.info_list;
|
||||
* Item view mode for streams & playlist listing screens.
|
||||
*/
|
||||
public enum ItemViewMode {
|
||||
/**
|
||||
* View mode is automatically determined based on the device configuration.
|
||||
*/
|
||||
AUTO,
|
||||
/**
|
||||
* Full width list item with thumb on the left and two line title & uploader in right.
|
||||
*/
|
||||
|
@ -346,7 +346,7 @@ public final class InfoItemDialog {
|
||||
|
||||
public static void reportErrorDuringInitialization(final Throwable throwable,
|
||||
final InfoItem item) {
|
||||
ErrorUtil.showSnackbar(App.getApp().getBaseContext(), new ErrorInfo(
|
||||
ErrorUtil.showSnackbar(App.getInstance().getBaseContext(), new ErrorInfo(
|
||||
throwable,
|
||||
UserAction.OPEN_INFO_ITEM_DIALOG,
|
||||
"none",
|
||||
|
@ -41,10 +41,11 @@ import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||
* </p>
|
||||
*/
|
||||
public enum StreamDialogDefaultEntry {
|
||||
SHOW_CHANNEL_DETAILS(R.string.show_channel_details, (fragment, item) ->
|
||||
fetchUploaderUrlIfSparse(fragment.requireContext(), item.getServiceId(), item.getUrl(),
|
||||
item.getUploaderUrl(), url -> openChannelFragment(fragment, item, url))
|
||||
),
|
||||
SHOW_CHANNEL_DETAILS(R.string.show_channel_details, (fragment, item) -> {
|
||||
final var activity = fragment.requireActivity();
|
||||
fetchUploaderUrlIfSparse(activity, item.getServiceId(), item.getUrl(),
|
||||
item.getUploaderUrl(), url -> openChannelFragment(activity, item, url));
|
||||
}),
|
||||
|
||||
/**
|
||||
* Enqueues the stream automatically to the current PlayerType.
|
||||
|
@ -64,8 +64,7 @@ public class StreamMiniInfoItemHolder extends InfoItemHolder {
|
||||
StreamStateEntity state2 = null;
|
||||
if (DependentPreferenceHelper
|
||||
.getPositionsInListsEnabled(itemProgressView.getContext())) {
|
||||
state2 = historyRecordManager.loadStreamState(infoItem)
|
||||
.blockingGet()[0];
|
||||
state2 = historyRecordManager.loadStreamState(infoItem).blockingGet();
|
||||
}
|
||||
if (state2 != null) {
|
||||
itemProgressView.setVisibility(View.VISIBLE);
|
||||
@ -120,7 +119,7 @@ public class StreamMiniInfoItemHolder extends InfoItemHolder {
|
||||
if (DependentPreferenceHelper.getPositionsInListsEnabled(itemProgressView.getContext())) {
|
||||
state = historyRecordManager
|
||||
.loadStreamState(infoItem)
|
||||
.blockingGet()[0];
|
||||
.blockingGet();
|
||||
}
|
||||
if (state != null && item.getDuration() > 0
|
||||
&& !StreamTypeUtil.isLiveStream(item.getStreamType())) {
|
||||
|
@ -1,25 +1,9 @@
|
||||
package org.schabi.newpipe.ktx
|
||||
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.Parcelable
|
||||
import androidx.core.os.BundleCompat
|
||||
import java.io.Serializable
|
||||
import kotlin.reflect.safeCast
|
||||
|
||||
inline fun <reified T : Parcelable> Bundle.parcelableArrayList(key: String?): ArrayList<T>? {
|
||||
return BundleCompat.getParcelableArrayList(this, key, T::class.java)
|
||||
}
|
||||
|
||||
inline fun <reified T : Serializable> Bundle.serializable(key: String?): T? {
|
||||
return getSerializable(this, key, T::class.java)
|
||||
}
|
||||
|
||||
fun <T : Serializable> getSerializable(bundle: Bundle, key: String?, clazz: Class<T>): T? {
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||
bundle.getSerializable(key, clazz)
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
clazz.kotlin.safeCast(bundle.getSerializable(key))
|
||||
}
|
||||
return BundleCompat.getSerializable(this, key, T::class.java)
|
||||
}
|
||||
|
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")
|
||||
}
|
||||
}
|
@ -18,6 +18,8 @@ import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.recyclerview.widget.ItemTouchHelper;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.evernote.android.state.State;
|
||||
|
||||
import org.reactivestreams.Subscriber;
|
||||
import org.reactivestreams.Subscription;
|
||||
import org.schabi.newpipe.NewPipeDatabase;
|
||||
@ -35,6 +37,8 @@ import org.schabi.newpipe.local.holder.LocalBookmarkPlaylistItemHolder;
|
||||
import org.schabi.newpipe.local.holder.RemoteBookmarkPlaylistItemHolder;
|
||||
import org.schabi.newpipe.local.playlist.LocalPlaylistManager;
|
||||
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.OnClickGesture;
|
||||
import org.schabi.newpipe.util.debounce.DebounceSavable;
|
||||
@ -44,7 +48,6 @@ import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
import icepick.State;
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
||||
import io.reactivex.rxjava3.disposables.Disposable;
|
||||
@ -121,6 +124,10 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL
|
||||
super.initViews(rootView, savedInstanceState);
|
||||
|
||||
itemListAdapter.setUseItemHandle(true);
|
||||
EmptyStateUtil.setEmptyStateComposable(
|
||||
rootView.findViewById(R.id.empty_state_view),
|
||||
EmptyStateSpec.Companion.getNoBookmarkedPlaylist()
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -44,11 +44,11 @@ import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.preference.PreferenceManager
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.evernote.android.state.State
|
||||
import com.xwray.groupie.GroupieAdapter
|
||||
import com.xwray.groupie.Item
|
||||
import com.xwray.groupie.OnItemClickListener
|
||||
import com.xwray.groupie.OnItemLongClickListener
|
||||
import icepick.State
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||
@ -74,6 +74,7 @@ import org.schabi.newpipe.ktx.slideUp
|
||||
import org.schabi.newpipe.local.feed.item.StreamItem
|
||||
import org.schabi.newpipe.local.feed.service.FeedLoadService
|
||||
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.Localization
|
||||
import org.schabi.newpipe.util.NavigationHelper
|
||||
@ -132,6 +133,7 @@ class FeedFragment : BaseStateFragment<FeedState>() {
|
||||
override fun onViewCreated(rootView: View, savedInstanceState: Bundle?) {
|
||||
// super.onViewCreated() calls initListeners() which require the binding to be initialized
|
||||
_feedBinding = FragmentFeedBinding.bind(rootView)
|
||||
feedBinding.emptyStateView.setEmptyStateComposable()
|
||||
super.onViewCreated(rootView, savedInstanceState)
|
||||
|
||||
val factory = FeedViewModel.getFactory(requireContext(), groupId)
|
||||
|
@ -165,7 +165,7 @@ class FeedViewModel(
|
||||
fun getFactory(context: Context, groupId: Long) = viewModelFactory {
|
||||
initializer {
|
||||
FeedViewModel(
|
||||
App.getApp(),
|
||||
App.instance,
|
||||
groupId,
|
||||
// Read initial value from preferences
|
||||
getShowPlayedItemsFromPreferences(context.applicationContext),
|
||||
|
@ -18,10 +18,13 @@ package org.schabi.newpipe.local.history;
|
||||
* 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.SharedPreferences;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.collection.LongLongPair;
|
||||
import androidx.preference.PreferenceManager;
|
||||
|
||||
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.local.feed.FeedViewModel;
|
||||
import org.schabi.newpipe.player.playqueue.PlayQueueItem;
|
||||
import org.schabi.newpipe.util.ExtractorHelper;
|
||||
|
||||
import java.time.OffsetDateTime;
|
||||
import java.time.ZoneOffset;
|
||||
@ -91,47 +93,39 @@ public class HistoryRecordManager {
|
||||
* @param info the item to mark as watched
|
||||
* @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()) {
|
||||
return Maybe.empty();
|
||||
return Completable.complete();
|
||||
}
|
||||
|
||||
final OffsetDateTime currentTime = OffsetDateTime.now(ZoneOffset.UTC);
|
||||
return Maybe.fromCallable(() -> database.runInTransaction(() -> {
|
||||
final long streamId;
|
||||
final long duration;
|
||||
// Duration will not exist if the item was loaded with fast mode, so fetch it if empty
|
||||
if (info.getDuration() < 0) {
|
||||
final StreamInfo completeInfo = ExtractorHelper.getStreamInfo(
|
||||
info.getServiceId(),
|
||||
info.getUrl(),
|
||||
false
|
||||
final var remoteInfo = getStreamInfo(info.getServiceId(), info.getUrl(), false)
|
||||
.map(item ->
|
||||
new LongLongPair(item.getDuration(), streamTable.upsert(new StreamEntity(item))));
|
||||
|
||||
return Single.just(info)
|
||||
.filter(item -> item.getDuration() >= 0)
|
||||
.map(item ->
|
||||
new LongLongPair(item.getDuration(), streamTable.upsert(new StreamEntity(item)))
|
||||
)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.blockingGet();
|
||||
duration = completeInfo.getDuration();
|
||||
streamId = streamTable.upsert(new StreamEntity(completeInfo));
|
||||
} else {
|
||||
duration = info.getDuration();
|
||||
streamId = streamTable.upsert(new StreamEntity(info));
|
||||
}
|
||||
.switchIfEmpty(remoteInfo)
|
||||
.flatMapCompletable(pair -> Completable.fromRunnable(() -> {
|
||||
final long duration = pair.getFirst();
|
||||
final long streamId = pair.getSecond();
|
||||
|
||||
// Update the stream progress to the full duration of the video
|
||||
final StreamStateEntity entity = new StreamStateEntity(
|
||||
streamId,
|
||||
duration * 1000
|
||||
);
|
||||
streamStateTable.upsert(entity);
|
||||
// Update the stream progress to the full duration of the video
|
||||
final var entity = new StreamStateEntity(streamId, duration * 1000);
|
||||
streamStateTable.upsert(entity);
|
||||
|
||||
// Add a history entry
|
||||
final StreamHistoryEntity latestEntry = streamHistoryTable.getLatestEntry(streamId);
|
||||
if (latestEntry == null) {
|
||||
// never actually viewed: add history entry but with 0 views
|
||||
return streamHistoryTable.insert(new StreamHistoryEntity(streamId, currentTime, 0));
|
||||
} else {
|
||||
return 0L;
|
||||
}
|
||||
})).subscribeOn(Schedulers.io());
|
||||
// Add a history entry
|
||||
final var latestEntry = streamHistoryTable.getLatestEntry(streamId);
|
||||
if (latestEntry == null) {
|
||||
final var currentTime = OffsetDateTime.now(ZoneOffset.UTC);
|
||||
// never actually viewed: add history entry but with 0 views
|
||||
final var entry = new StreamHistoryEntity(streamId, currentTime, 0);
|
||||
streamHistoryTable.insert(entry);
|
||||
}
|
||||
}))
|
||||
.subscribeOn(Schedulers.io());
|
||||
}
|
||||
|
||||
public Maybe<Long> onViewed(final StreamInfo info) {
|
||||
@ -221,7 +215,7 @@ public class HistoryRecordManager {
|
||||
public Flowable<List<String>> getRelatedSearches(final String query,
|
||||
final int similarQueryLimit,
|
||||
final int uniqueQueryLimit) {
|
||||
return query.length() > 0
|
||||
return !query.isEmpty()
|
||||
? searchHistoryTable.getSimilarEntries(query, similarQueryLimit)
|
||||
: searchHistoryTable.getUniqueEntries(uniqueQueryLimit);
|
||||
}
|
||||
@ -236,47 +230,31 @@ public class HistoryRecordManager {
|
||||
|
||||
public Maybe<StreamStateEntity> loadStreamState(final PlayQueueItem queueItem) {
|
||||
return queueItem.getStream()
|
||||
.map(info -> streamTable.upsert(new StreamEntity(info)))
|
||||
.flatMapPublisher(streamStateTable::getState)
|
||||
.firstElement()
|
||||
.flatMap(list -> list.isEmpty() ? Maybe.empty() : Maybe.just(list.get(0)))
|
||||
.flatMapMaybe(this::loadStreamState)
|
||||
.filter(state -> state.isValid(queueItem.getDuration()))
|
||||
.subscribeOn(Schedulers.io());
|
||||
}
|
||||
|
||||
public Maybe<StreamStateEntity> loadStreamState(final StreamInfo info) {
|
||||
return Single.fromCallable(() -> streamTable.upsert(new StreamEntity(info)))
|
||||
.flatMapPublisher(streamStateTable::getState)
|
||||
.firstElement()
|
||||
.flatMap(list -> list.isEmpty() ? Maybe.empty() : Maybe.just(list.get(0)))
|
||||
.filter(state -> state.isValid(info.getDuration()))
|
||||
.flatMapMaybe(streamStateTable::getState)
|
||||
.subscribeOn(Schedulers.io());
|
||||
}
|
||||
|
||||
public Completable saveStreamState(@NonNull final StreamInfo info, final long progressMillis) {
|
||||
return Completable.fromAction(() -> database.runInTransaction(() -> {
|
||||
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())) {
|
||||
streamStateTable.upsert(state);
|
||||
}
|
||||
})).subscribeOn(Schedulers.io());
|
||||
}
|
||||
|
||||
public Single<StreamStateEntity[]> loadStreamState(final InfoItem info) {
|
||||
return Single.fromCallable(() -> {
|
||||
final List<StreamEntity> entities = streamTable
|
||||
.getStream(info.getServiceId(), info.getUrl()).blockingFirst();
|
||||
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 Maybe<StreamStateEntity> loadStreamState(final InfoItem info) {
|
||||
return streamTable.getStream(info.getServiceId(), info.getUrl())
|
||||
.flatMap(entity -> streamStateTable.getState(entity.getUid()))
|
||||
.subscribeOn(Schedulers.io());
|
||||
}
|
||||
|
||||
public Single<List<StreamStateEntity>> loadLocalStreamStateBatch(
|
||||
@ -295,13 +273,7 @@ public class HistoryRecordManager {
|
||||
result.add(null);
|
||||
continue;
|
||||
}
|
||||
final List<StreamStateEntity> states = streamStateTable.getState(streamId)
|
||||
.blockingFirst();
|
||||
if (states.isEmpty()) {
|
||||
result.add(null);
|
||||
} else {
|
||||
result.add(states.get(0));
|
||||
}
|
||||
result.add(streamStateTable.getState(streamId).blockingGet());
|
||||
}
|
||||
return result;
|
||||
}).subscribeOn(Schedulers.io());
|
||||
|
@ -15,6 +15,7 @@ import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.viewbinding.ViewBinding;
|
||||
|
||||
import com.evernote.android.state.State;
|
||||
import com.google.android.material.snackbar.Snackbar;
|
||||
|
||||
import org.reactivestreams.Subscriber;
|
||||
@ -45,7 +46,6 @@ import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
import icepick.State;
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
||||
import io.reactivex.rxjava3.disposables.Disposable;
|
||||
@ -368,6 +368,7 @@ public class StatisticsPlaylistFragment
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public PlayQueue getPlayQueue() {
|
||||
return getPlayQueue(0);
|
||||
}
|
||||
|
@ -26,6 +26,8 @@ import androidx.recyclerview.widget.ItemTouchHelper;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import androidx.viewbinding.ViewBinding;
|
||||
|
||||
import com.evernote.android.state.State;
|
||||
|
||||
import org.reactivestreams.Subscriber;
|
||||
import org.reactivestreams.Subscription;
|
||||
import org.schabi.newpipe.NewPipeDatabase;
|
||||
@ -49,12 +51,12 @@ import org.schabi.newpipe.local.BaseLocalListFragment;
|
||||
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
||||
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
||||
import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
|
||||
import org.schabi.newpipe.util.debounce.DebounceSavable;
|
||||
import org.schabi.newpipe.util.debounce.DebounceSaver;
|
||||
import org.schabi.newpipe.util.Localization;
|
||||
import org.schabi.newpipe.util.NavigationHelper;
|
||||
import org.schabi.newpipe.util.OnClickGesture;
|
||||
import org.schabi.newpipe.util.PlayButtonHelper;
|
||||
import org.schabi.newpipe.util.debounce.DebounceSavable;
|
||||
import org.schabi.newpipe.util.debounce.DebounceSaver;
|
||||
import org.schabi.newpipe.util.external_communication.ShareUtils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
@ -63,7 +65,6 @@ import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import icepick.State;
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.rxjava3.core.Single;
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
||||
@ -843,6 +844,7 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public PlayQueue getPlayQueue() {
|
||||
return getPlayQueue(0);
|
||||
}
|
||||
|
@ -1,5 +1,7 @@
|
||||
package org.schabi.newpipe.local.subscription;
|
||||
|
||||
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
|
||||
|
||||
import android.app.Dialog;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
@ -10,13 +12,11 @@ import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.fragment.app.DialogFragment;
|
||||
import androidx.fragment.app.Fragment;
|
||||
|
||||
import com.evernote.android.state.State;
|
||||
import com.livefront.bridge.Bridge;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
|
||||
import icepick.Icepick;
|
||||
import icepick.State;
|
||||
|
||||
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
|
||||
|
||||
public class ImportConfirmationDialog extends DialogFragment {
|
||||
@State
|
||||
protected Intent resultServiceIntent;
|
||||
@ -57,12 +57,12 @@ public class ImportConfirmationDialog extends DialogFragment {
|
||||
throw new IllegalStateException("Result intent is null");
|
||||
}
|
||||
|
||||
Icepick.restoreInstanceState(this, savedInstanceState);
|
||||
Bridge.restoreInstanceState(this, savedInstanceState);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSaveInstanceState(@NonNull final Bundle outState) {
|
||||
super.onSaveInstanceState(outState);
|
||||
Icepick.saveInstanceState(this, outState);
|
||||
Bridge.saveInstanceState(this, outState);
|
||||
}
|
||||
}
|
||||
|
@ -20,11 +20,11 @@ import androidx.annotation.StringRes
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import com.evernote.android.state.State
|
||||
import com.xwray.groupie.Group
|
||||
import com.xwray.groupie.GroupAdapter
|
||||
import com.xwray.groupie.Section
|
||||
import com.xwray.groupie.viewbinding.GroupieViewHolder
|
||||
import icepick.State
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||
import org.schabi.newpipe.R
|
||||
import org.schabi.newpipe.database.feed.model.FeedGroupEntity.Companion.GROUP_ALL_ID
|
||||
@ -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.streams.io.NoFileManagerSafeGuard
|
||||
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.OnClickGesture
|
||||
import org.schabi.newpipe.util.ServiceHelper
|
||||
@ -257,6 +258,8 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
|
||||
binding.itemsList.adapter = groupAdapter
|
||||
binding.itemsList.itemAnimator = null
|
||||
|
||||
binding.emptyStateView.setEmptyStateComposable()
|
||||
|
||||
viewModel = ViewModelProvider(this)[SubscriptionViewModel::class.java]
|
||||
viewModel.stateLiveData.observe(viewLifecycleOwner) { it?.let(this::handleResult) }
|
||||
viewModel.feedGroupsLiveData.observe(viewLifecycleOwner) {
|
||||
|
@ -27,6 +27,8 @@ import androidx.annotation.StringRes;
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
import androidx.core.text.util.LinkifyCompat;
|
||||
|
||||
import com.evernote.android.state.State;
|
||||
|
||||
import org.schabi.newpipe.BaseFragment;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.error.ErrorInfo;
|
||||
@ -44,8 +46,6 @@ import org.schabi.newpipe.util.ServiceHelper;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import icepick.State;
|
||||
|
||||
public class SubscriptionsImportFragment extends BaseFragment {
|
||||
@State
|
||||
int currentServiceId = Constants.NO_SERVICE_ID;
|
||||
|
@ -18,11 +18,11 @@ import androidx.lifecycle.Observer
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.evernote.android.state.State
|
||||
import com.livefront.bridge.Bridge
|
||||
import com.xwray.groupie.GroupieAdapter
|
||||
import com.xwray.groupie.OnItemClickListener
|
||||
import com.xwray.groupie.Section
|
||||
import icepick.Icepick
|
||||
import icepick.State
|
||||
import org.schabi.newpipe.R
|
||||
import org.schabi.newpipe.database.feed.model.FeedGroupEntity
|
||||
import org.schabi.newpipe.databinding.DialogFeedGroupCreateBinding
|
||||
@ -78,7 +78,7 @@ class FeedGroupDialog : DialogFragment(), BackPressable {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
Icepick.restoreInstanceState(this, savedInstanceState)
|
||||
Bridge.restoreInstanceState(this, savedInstanceState)
|
||||
|
||||
setStyle(STYLE_NO_TITLE, ThemeHelper.getMinWidthDialogTheme(requireContext()))
|
||||
groupId = arguments?.getLong(KEY_GROUP_ID, NO_GROUP_SELECTED) ?: NO_GROUP_SELECTED
|
||||
@ -115,7 +115,7 @@ class FeedGroupDialog : DialogFragment(), BackPressable {
|
||||
iconsListState = feedGroupCreateBinding.iconSelector.layoutManager?.onSaveInstanceState()
|
||||
subscriptionsListState = feedGroupCreateBinding.subscriptionsSelectorList.layoutManager?.onSaveInstanceState()
|
||||
|
||||
Icepick.saveInstanceState(this, outState)
|
||||
Bridge.saveInstanceState(this, outState)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
|
@ -11,10 +11,10 @@ import androidx.recyclerview.widget.ItemTouchHelper
|
||||
import androidx.recyclerview.widget.ItemTouchHelper.SimpleCallback
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.evernote.android.state.State
|
||||
import com.livefront.bridge.Bridge
|
||||
import com.xwray.groupie.GroupieAdapter
|
||||
import com.xwray.groupie.TouchCallback
|
||||
import icepick.Icepick
|
||||
import icepick.State
|
||||
import org.schabi.newpipe.R
|
||||
import org.schabi.newpipe.database.feed.model.FeedGroupEntity
|
||||
import org.schabi.newpipe.databinding.DialogFeedGroupReorderBinding
|
||||
@ -23,10 +23,6 @@ import org.schabi.newpipe.local.subscription.dialog.FeedGroupReorderDialogViewMo
|
||||
import org.schabi.newpipe.local.subscription.item.FeedGroupReorderItem
|
||||
import org.schabi.newpipe.util.ThemeHelper
|
||||
import java.util.Collections
|
||||
import kotlin.collections.ArrayList
|
||||
import kotlin.collections.List
|
||||
import kotlin.collections.map
|
||||
import kotlin.collections.sortedBy
|
||||
|
||||
class FeedGroupReorderDialog : DialogFragment() {
|
||||
private var _binding: DialogFeedGroupReorderBinding? = null
|
||||
@ -42,7 +38,7 @@ class FeedGroupReorderDialog : DialogFragment() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
Icepick.restoreInstanceState(this, savedInstanceState)
|
||||
Bridge.restoreInstanceState(this, savedInstanceState)
|
||||
|
||||
setStyle(STYLE_NO_TITLE, ThemeHelper.getMinWidthDialogTheme(requireContext()))
|
||||
}
|
||||
@ -80,7 +76,7 @@ class FeedGroupReorderDialog : DialogFragment() {
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
super.onSaveInstanceState(outState)
|
||||
Icepick.saveInstanceState(this, outState)
|
||||
Bridge.saveInstanceState(this, outState)
|
||||
}
|
||||
|
||||
private fun handleGroups(list: List<FeedGroupEntity>) {
|
||||
|
@ -3,14 +3,18 @@ package org.schabi.newpipe.local.subscription.item
|
||||
import android.view.View
|
||||
import com.xwray.groupie.viewbinding.BindableItem
|
||||
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
|
||||
*/
|
||||
class ImportSubscriptionsHintPlaceholderItem : BindableItem<ListEmptyViewBinding>() {
|
||||
class ImportSubscriptionsHintPlaceholderItem : BindableItem<ListEmptyViewSubscriptionsBinding>() {
|
||||
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 initializeViewBinding(view: View) = ListEmptyViewBinding.bind(view)
|
||||
override fun initializeViewBinding(view: View) = ListEmptyViewSubscriptionsBinding.bind(view)
|
||||
}
|
||||
|
@ -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
|
||||
}
|
@ -8,38 +8,23 @@ 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.util.NO_SERVICE_ID
|
||||
import org.schabi.newpipe.ui.components.video.comment.CommentInfo
|
||||
|
||||
class CommentsSource(
|
||||
serviceId: Int,
|
||||
private val url: String?,
|
||||
private val repliesPage: Page?
|
||||
) : PagingSource<Page, CommentsInfoItem>() {
|
||||
init {
|
||||
require(serviceId != NO_SERVICE_ID) { "serviceId is NO_SERVICE_ID" }
|
||||
}
|
||||
private val service = NewPipe.getService(serviceId)
|
||||
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> {
|
||||
// repliesPage is non-null only when used to load the comment replies
|
||||
val nextKey = params.key ?: repliesPage
|
||||
|
||||
return withContext(Dispatchers.IO) {
|
||||
nextKey?.let {
|
||||
val info = CommentsInfo.getMoreItems(service, url, it)
|
||||
LoadResult.Page(info.items, null, info.nextPage)
|
||||
} ?: run {
|
||||
val info = CommentsInfo.getInfo(service, url)
|
||||
if (info.isCommentsDisabled) {
|
||||
LoadResult.Error(CommentsDisabledException())
|
||||
} else {
|
||||
LoadResult.Page(info.relatedItems, null, info.nextPage)
|
||||
}
|
||||
// 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
|
||||
}
|
||||
|
||||
class CommentsDisabledException : RuntimeException()
|
||||
|
@ -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.Localization.assureCorrectAppLanguage;
|
||||
import static java.util.concurrent.TimeUnit.MILLISECONDS;
|
||||
import static coil3.Image_androidKt.toBitmap;
|
||||
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
@ -53,14 +54,12 @@ import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.content.SharedPreferences;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.media.AudioManager;
|
||||
import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.graphics.drawable.DrawableKt;
|
||||
import androidx.core.math.MathUtils;
|
||||
import androidx.preference.PreferenceManager;
|
||||
|
||||
@ -125,7 +124,7 @@ import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.IntStream;
|
||||
|
||||
import coil.target.Target;
|
||||
import coil3.target.Target;
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.rxjava3.core.Observable;
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
||||
@ -193,7 +192,7 @@ public final class Player implements PlaybackListener, Listener {
|
||||
@Nullable
|
||||
private Bitmap currentThumbnail;
|
||||
@Nullable
|
||||
private coil.request.Disposable thumbnailDisposable;
|
||||
private coil3.request.Disposable thumbnailDisposable;
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Player
|
||||
@ -789,27 +788,26 @@ public final class Player implements PlaybackListener, Listener {
|
||||
// scale down the notification thumbnail for performance
|
||||
final var thumbnailTarget = new Target() {
|
||||
@Override
|
||||
public void onError(@Nullable final Drawable error) {
|
||||
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);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStart(@Nullable final Drawable placeholder) {
|
||||
public void onStart(@Nullable final coil3.Image placeholder) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "Thumbnail - onStart() called");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSuccess(@NonNull final Drawable result) {
|
||||
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(DrawableKt.toBitmapOrNull(result, result.getIntrinsicWidth(),
|
||||
result.getIntrinsicHeight(), null));
|
||||
onThumbnailLoaded(toBitmap(result));
|
||||
}
|
||||
};
|
||||
thumbnailDisposable = CoilHelper.INSTANCE
|
||||
|
@ -24,6 +24,9 @@ import androidx.core.math.MathUtils;
|
||||
import androidx.fragment.app.DialogFragment;
|
||||
import androidx.preference.PreferenceManager;
|
||||
|
||||
import com.evernote.android.state.State;
|
||||
import com.livefront.bridge.Bridge;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.databinding.DialogPlaybackParameterBinding;
|
||||
import org.schabi.newpipe.player.ui.VideoPlayerUi;
|
||||
@ -37,9 +40,6 @@ import java.util.function.DoubleConsumer;
|
||||
import java.util.function.DoubleFunction;
|
||||
import java.util.function.DoubleSupplier;
|
||||
|
||||
import icepick.Icepick;
|
||||
import icepick.State;
|
||||
|
||||
public class PlaybackParameterDialog extends DialogFragment {
|
||||
private static final String TAG = "PlaybackParameterDialog";
|
||||
|
||||
@ -135,7 +135,7 @@ public class PlaybackParameterDialog extends DialogFragment {
|
||||
@Override
|
||||
public void onSaveInstanceState(@NonNull final Bundle outState) {
|
||||
super.onSaveInstanceState(outState);
|
||||
Icepick.saveInstanceState(this, outState);
|
||||
Bridge.saveInstanceState(this, outState);
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
@ -146,7 +146,7 @@ public class PlaybackParameterDialog extends DialogFragment {
|
||||
@Override
|
||||
public Dialog onCreateDialog(@Nullable final Bundle savedInstanceState) {
|
||||
assureCorrectAppLanguage(getContext());
|
||||
Icepick.restoreInstanceState(this, savedInstanceState);
|
||||
Bridge.restoreInstanceState(this, savedInstanceState);
|
||||
|
||||
binding = DialogPlaybackParameterBinding.inflate(getLayoutInflater());
|
||||
initUI();
|
||||
|
@ -16,8 +16,8 @@ import com.google.android.exoplayer2.PlaybackParameters;
|
||||
import org.schabi.newpipe.App;
|
||||
import org.schabi.newpipe.MainActivity;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||
import org.schabi.newpipe.player.PlayerService;
|
||||
import org.schabi.newpipe.player.Player;
|
||||
import org.schabi.newpipe.player.PlayerService;
|
||||
import org.schabi.newpipe.player.PlayerType;
|
||||
import org.schabi.newpipe.player.event.PlayerServiceEventListener;
|
||||
import org.schabi.newpipe.player.event.PlayerServiceExtendedEventListener;
|
||||
@ -116,7 +116,7 @@ public final class PlayerHolder {
|
||||
// helper to handle context in common place as using the same
|
||||
// context to bind/unbind a service is crucial
|
||||
private Context getCommonContext() {
|
||||
return App.getApp();
|
||||
return App.getInstance();
|
||||
}
|
||||
|
||||
public void startService(final boolean playAfterConnect,
|
||||
|
@ -38,7 +38,9 @@ public class MediaSessionPlayerUi extends PlayerUi
|
||||
implements SharedPreferences.OnSharedPreferenceChangeListener {
|
||||
private static final String TAG = "MediaSessUi";
|
||||
|
||||
@Nullable
|
||||
private MediaSessionCompat mediaSession;
|
||||
@Nullable
|
||||
private MediaSessionConnector sessionConnector;
|
||||
|
||||
private final String ignoreHardwareMediaButtonsKey;
|
||||
@ -198,6 +200,11 @@ public class MediaSessionPlayerUi extends PlayerUi
|
||||
return;
|
||||
}
|
||||
|
||||
if (sessionConnector == null) {
|
||||
// sessionConnector will be null after destroyPlayer is called
|
||||
return;
|
||||
}
|
||||
|
||||
// only use the fourth and fifth actions (the settings page also shows only the last 2 on
|
||||
// Android 13+)
|
||||
final List<NotificationActionData> newNotificationActions = IntStream.of(3, 4)
|
||||
|
@ -179,7 +179,7 @@ public class SeekbarPreviewThumbnailHolder {
|
||||
|
||||
// Gets the bitmap within the timeout of 15 seconds imposed by default by OkHttpClient
|
||||
// Ensure that you are not running on the main thread, otherwise this will hang
|
||||
final var bitmap = CoilHelper.INSTANCE.loadBitmapBlocking(App.getApp(), url);
|
||||
final var bitmap = CoilHelper.INSTANCE.loadBitmapBlocking(App.getInstance(), url);
|
||||
|
||||
if (sw != null) {
|
||||
Log.d(TAG, "Download of bitmap for seekbarPreview from '" + url + "' took "
|
||||
|
@ -15,7 +15,7 @@ import org.schabi.newpipe.extractor.localization.Localization;
|
||||
import org.schabi.newpipe.util.image.ImageStrategy;
|
||||
import org.schabi.newpipe.util.image.PreferredImageQuality;
|
||||
|
||||
import coil.Coil;
|
||||
import coil3.SingletonImageLoader;
|
||||
|
||||
public class ContentSettingsFragment extends BasePreferenceFragment {
|
||||
private String youtubeRestrictedModeEnabledKey;
|
||||
@ -41,7 +41,7 @@ public class ContentSettingsFragment extends BasePreferenceFragment {
|
||||
(preference, newValue) -> {
|
||||
ImageStrategy.setPreferredImageQuality(PreferredImageQuality
|
||||
.fromPreferenceKey(requireContext(), (String) newValue));
|
||||
final var loader = Coil.imageLoader(preference.getContext());
|
||||
final var loader = SingletonImageLoader.get(preference.getContext());
|
||||
loader.getMemoryCache().clear();
|
||||
loader.getDiskCache().clear();
|
||||
Toast.makeText(preference.getContext(),
|
||||
|
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
|
||||
)
|
||||
}
|
||||
}
|
@ -156,7 +156,7 @@ public final class NewPipeSettings {
|
||||
prefs.getInt(disabledTunnelingAutomaticallyKey, -1) == 0
|
||||
&& !prefs.getBoolean(disabledTunnelingKey, false);
|
||||
|
||||
if (App.getApp().isFirstRun()
|
||||
if (App.getInstance().isFirstRun()
|
||||
|| (wasDeviceBlacklistUpdated && !wasMediaTunnelingEnabledByUser)) {
|
||||
setMediaTunneling(context);
|
||||
}
|
||||
|
@ -11,6 +11,7 @@ import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.compose.ui.platform.ComposeView;
|
||||
import androidx.fragment.app.DialogFragment;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
@ -19,6 +20,8 @@ import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.database.subscription.SubscriptionEntity;
|
||||
import org.schabi.newpipe.error.ErrorUtil;
|
||||
import org.schabi.newpipe.local.subscription.SubscriptionManager;
|
||||
import org.schabi.newpipe.ui.emptystate.EmptyStateSpec;
|
||||
import org.schabi.newpipe.ui.emptystate.EmptyStateUtil;
|
||||
import org.schabi.newpipe.util.ThemeHelper;
|
||||
import org.schabi.newpipe.util.image.CoilHelper;
|
||||
|
||||
@ -57,7 +60,7 @@ public class SelectChannelFragment extends DialogFragment {
|
||||
private OnCancelListener onCancelListener = null;
|
||||
|
||||
private ProgressBar progressBar;
|
||||
private TextView emptyView;
|
||||
private ComposeView emptyView;
|
||||
private RecyclerView recyclerView;
|
||||
|
||||
private List<SubscriptionEntity> subscriptions = new Vector<>();
|
||||
@ -91,6 +94,9 @@ public class SelectChannelFragment extends DialogFragment {
|
||||
|
||||
progressBar = v.findViewById(R.id.progressBar);
|
||||
emptyView = v.findViewById(R.id.empty_state_view);
|
||||
|
||||
EmptyStateUtil.setEmptyStateComposable(emptyView,
|
||||
EmptyStateSpec.Companion.getNoSubscriptions());
|
||||
progressBar.setVisibility(View.VISIBLE);
|
||||
recyclerView.setVisibility(View.GONE);
|
||||
emptyView.setVisibility(View.GONE);
|
||||
|
@ -11,6 +11,7 @@ import android.widget.ProgressBar;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.compose.ui.platform.ComposeView;
|
||||
import androidx.fragment.app.DialogFragment;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
@ -27,6 +28,8 @@ import org.schabi.newpipe.error.ErrorUtil;
|
||||
import org.schabi.newpipe.error.UserAction;
|
||||
import org.schabi.newpipe.local.playlist.LocalPlaylistManager;
|
||||
import org.schabi.newpipe.local.playlist.RemotePlaylistManager;
|
||||
import org.schabi.newpipe.ui.emptystate.EmptyStateSpec;
|
||||
import org.schabi.newpipe.ui.emptystate.EmptyStateUtil;
|
||||
import org.schabi.newpipe.util.image.CoilHelper;
|
||||
|
||||
import java.util.List;
|
||||
@ -40,7 +43,7 @@ public class SelectPlaylistFragment extends DialogFragment {
|
||||
private OnSelectedListener onSelectedListener = null;
|
||||
|
||||
private ProgressBar progressBar;
|
||||
private TextView emptyView;
|
||||
private ComposeView emptyView;
|
||||
private RecyclerView recyclerView;
|
||||
private Disposable disposable = null;
|
||||
|
||||
@ -62,6 +65,8 @@ public class SelectPlaylistFragment extends DialogFragment {
|
||||
recyclerView = v.findViewById(R.id.items_list);
|
||||
emptyView = v.findViewById(R.id.empty_state_view);
|
||||
|
||||
EmptyStateUtil.setEmptyStateComposable(emptyView,
|
||||
EmptyStateSpec.Companion.getNoBookmarkedPlaylist());
|
||||
recyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
|
||||
final SelectPlaylistAdapter playlistAdapter = new SelectPlaylistAdapter();
|
||||
recyclerView.setAdapter(playlistAdapter);
|
||||
|
@ -1,5 +1,7 @@
|
||||
package org.schabi.newpipe.settings;
|
||||
|
||||
import static org.schabi.newpipe.MainActivity.DEBUG;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.util.Log;
|
||||
@ -18,8 +20,6 @@ import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
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 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);
|
||||
|
||||
// no migration to run, already up to date
|
||||
if (App.getApp().isFirstRun()) {
|
||||
if (App.getInstance().isFirstRun()) {
|
||||
sp.edit().putInt(lastPrefVersionKey, VERSION).apply();
|
||||
return;
|
||||
} else if (lastPrefVersion == VERSION) {
|
||||
|
@ -21,7 +21,9 @@ import androidx.fragment.app.FragmentManager;
|
||||
import androidx.preference.Preference;
|
||||
import androidx.preference.PreferenceFragmentCompat;
|
||||
|
||||
import com.evernote.android.state.State;
|
||||
import com.jakewharton.rxbinding4.widget.RxTextView;
|
||||
import com.livefront.bridge.Bridge;
|
||||
|
||||
import org.schabi.newpipe.MainActivity;
|
||||
import org.schabi.newpipe.R;
|
||||
@ -41,9 +43,6 @@ import org.schabi.newpipe.views.FocusOverlayView;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import icepick.Icepick;
|
||||
import icepick.State;
|
||||
|
||||
/*
|
||||
* Created by Christian Schabesberger on 31.08.15.
|
||||
*
|
||||
@ -93,7 +92,7 @@ public class SettingsActivity extends AppCompatActivity implements
|
||||
assureCorrectAppLanguage(this);
|
||||
|
||||
super.onCreate(savedInstanceBundle);
|
||||
Icepick.restoreInstanceState(this, savedInstanceBundle);
|
||||
Bridge.restoreInstanceState(this, savedInstanceBundle);
|
||||
final boolean restored = savedInstanceBundle != null;
|
||||
|
||||
final SettingsLayoutBinding settingsLayoutBinding =
|
||||
@ -125,7 +124,7 @@ public class SettingsActivity extends AppCompatActivity implements
|
||||
@Override
|
||||
protected void onSaveInstanceState(@NonNull final Bundle outState) {
|
||||
super.onSaveInstanceState(outState);
|
||||
Icepick.saveInstanceState(this, outState);
|
||||
Bridge.saveInstanceState(this, outState);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -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)
|
||||
}
|
@ -11,6 +11,8 @@ import androidx.fragment.app.Fragment;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
|
||||
import org.schabi.newpipe.databinding.SettingsPreferencesearchFragmentBinding;
|
||||
import org.schabi.newpipe.ui.emptystate.EmptyStateSpec;
|
||||
import org.schabi.newpipe.ui.emptystate.EmptyStateUtil;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@ -39,6 +41,9 @@ public class PreferenceSearchFragment extends Fragment {
|
||||
binding = SettingsPreferencesearchFragmentBinding.inflate(inflater, container, false);
|
||||
|
||||
binding.searchResults.setLayoutManager(new LinearLayoutManager(getContext()));
|
||||
EmptyStateUtil.setEmptyStateComposable(
|
||||
binding.emptyStateView,
|
||||
EmptyStateSpec.Companion.getNoSearchMaxSizeResult());
|
||||
|
||||
adapter = new PreferenceSearchAdapter();
|
||||
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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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?,
|
||||
)
|
||||
}
|
@ -40,12 +40,12 @@ import androidx.fragment.app.FragmentActivity
|
||||
import androidx.paging.Pager
|
||||
import androidx.paging.PagingConfig
|
||||
import androidx.paging.cachedIn
|
||||
import coil.compose.AsyncImage
|
||||
import coil3.compose.AsyncImage
|
||||
import org.schabi.newpipe.R
|
||||
import org.schabi.newpipe.extractor.Page
|
||||
import org.schabi.newpipe.extractor.comments.CommentsInfoItem
|
||||
import org.schabi.newpipe.extractor.stream.Description
|
||||
import org.schabi.newpipe.paging.CommentsSource
|
||||
import org.schabi.newpipe.paging.CommentRepliesSource
|
||||
import org.schabi.newpipe.ui.components.common.DescriptionText
|
||||
import org.schabi.newpipe.ui.theme.AppTheme
|
||||
import org.schabi.newpipe.util.Localization
|
||||
@ -147,7 +147,7 @@ fun Comment(comment: CommentsInfoItem) {
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
val flow = remember {
|
||||
Pager(PagingConfig(pageSize = 20, enablePlaceholders = false)) {
|
||||
CommentsSource(comment.serviceId, comment.url, comment.replies)
|
||||
CommentRepliesSource(comment)
|
||||
}.flow
|
||||
.cachedIn(coroutineScope)
|
||||
}
|
||||
|
@ -23,7 +23,7 @@ import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import coil.compose.AsyncImage
|
||||
import coil3.compose.AsyncImage
|
||||
import org.schabi.newpipe.R
|
||||
import org.schabi.newpipe.extractor.comments.CommentsInfoItem
|
||||
import org.schabi.newpipe.extractor.stream.Description
|
||||
|
@ -1,28 +1,21 @@
|
||||
package org.schabi.newpipe.ui.components.comment
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.wrapContentSize
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.paging.LoadState
|
||||
import androidx.paging.LoadStates
|
||||
import androidx.paging.PagingData
|
||||
@ -30,11 +23,11 @@ import androidx.paging.compose.collectAsLazyPagingItems
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import my.nanihadesuka.compose.LazyColumnScrollbar
|
||||
import org.schabi.newpipe.R
|
||||
import org.schabi.newpipe.extractor.comments.CommentsInfoItem
|
||||
import org.schabi.newpipe.extractor.stream.Description
|
||||
import org.schabi.newpipe.paging.CommentsDisabledException
|
||||
import org.schabi.newpipe.ui.components.common.LoadingIndicator
|
||||
import org.schabi.newpipe.ui.emptystate.EmptyStateComposable
|
||||
import org.schabi.newpipe.ui.emptystate.EmptyStateSpec
|
||||
import org.schabi.newpipe.ui.theme.AppTheme
|
||||
|
||||
@Composable
|
||||
@ -62,7 +55,7 @@ fun CommentSection(
|
||||
if (refresh is LoadState.Loading) {
|
||||
LoadingIndicator(modifier = Modifier.padding(top = 8.dp))
|
||||
} else {
|
||||
NoCommentsMessage((refresh as? LoadState.Error)?.error)
|
||||
EmptyStateComposable(EmptyStateSpec.NoComments)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@ -75,25 +68,6 @@ fun CommentSection(
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun NoCommentsMessage(error: Throwable?) {
|
||||
val message = if (error is CommentsDisabledException) {
|
||||
R.string.comments_are_disabled
|
||||
} else {
|
||||
R.string.no_comments
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.wrapContentSize(Alignment.Center),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Text(text = "(╯°-°)╯", fontSize = 35.sp)
|
||||
Text(text = stringResource(id = message), fontSize = 24.sp)
|
||||
}
|
||||
}
|
||||
|
||||
private class CommentDataProvider : PreviewParameterProvider<PagingData<CommentsInfoItem>> {
|
||||
private val notLoading = LoadState.NotLoading(true)
|
||||
|
||||
@ -107,11 +81,6 @@ private class CommentDataProvider : PreviewParameterProvider<PagingData<Comments
|
||||
)
|
||||
}
|
||||
),
|
||||
// Comments disabled
|
||||
PagingData.from(
|
||||
listOf<CommentsInfoItem>(),
|
||||
LoadStates(LoadState.Error(CommentsDisabledException()), notLoading, notLoading)
|
||||
),
|
||||
// No comments
|
||||
PagingData.from(
|
||||
listOf<CommentsInfoItem>(),
|
||||
|
@ -6,7 +6,6 @@ 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.ParagraphStyle
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.TextLayoutResult
|
||||
import androidx.compose.ui.text.TextLinkStyles
|
||||
@ -23,24 +22,27 @@ fun DescriptionText(
|
||||
overflow: TextOverflow = TextOverflow.Clip,
|
||||
maxLines: Int = Int.MAX_VALUE,
|
||||
onTextLayout: (TextLayoutResult) -> Unit = {},
|
||||
style: TextStyle = LocalTextStyle.current
|
||||
style: TextStyle = LocalTextStyle.current,
|
||||
) {
|
||||
Text(
|
||||
modifier = modifier,
|
||||
text = rememberParsedDescription(description),
|
||||
maxLines = maxLines,
|
||||
onTextLayout = onTextLayout,
|
||||
style = style,
|
||||
overflow = overflow
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun rememberParsedDescription(description: Description): AnnotatedString {
|
||||
// TODO: Handle links and hashtags, Markdown.
|
||||
val parsedDescription = remember(description) {
|
||||
return remember(description) {
|
||||
if (description.type == Description.HTML) {
|
||||
val styles = TextLinkStyles(SpanStyle(textDecoration = TextDecoration.Underline))
|
||||
AnnotatedString.fromHtml(description.content, styles)
|
||||
} else {
|
||||
AnnotatedString(description.content, ParagraphStyle())
|
||||
AnnotatedString(description.content)
|
||||
}
|
||||
}
|
||||
|
||||
Text(
|
||||
modifier = modifier,
|
||||
text = parsedDescription,
|
||||
maxLines = maxLines,
|
||||
style = style,
|
||||
overflow = overflow,
|
||||
onTextLayout = onTextLayout
|
||||
)
|
||||
}
|
||||
|
@ -11,9 +11,7 @@ import androidx.compose.ui.Modifier
|
||||
@Composable
|
||||
fun LoadingIndicator(modifier: Modifier = Modifier) {
|
||||
CircularProgressIndicator(
|
||||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
.wrapContentSize(Alignment.Center),
|
||||
modifier = modifier.fillMaxSize().wrapContentSize(Alignment.Center),
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
trackColor = MaterialTheme.colorScheme.surfaceVariant,
|
||||
)
|
||||
|
@ -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,30 @@
|
||||
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.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
|
||||
) {
|
||||
my.nanihadesuka.compose.LazyColumnScrollbar(
|
||||
state = state,
|
||||
modifier = modifier,
|
||||
settings = settings,
|
||||
indicatorContent = indicatorContent,
|
||||
content = content,
|
||||
)
|
||||
}
|
@ -0,0 +1,148 @@
|
||||
package org.schabi.newpipe.ui.components.items
|
||||
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyListScope
|
||||
import androidx.compose.foundation.lazy.grid.GridCells
|
||||
import androidx.compose.foundation.lazy.grid.LazyGridScope
|
||||
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
||||
import androidx.compose.foundation.lazy.grid.rememberLazyGridState
|
||||
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.compose.ui.unit.dp
|
||||
import androidx.paging.compose.LazyPagingItems
|
||||
import androidx.preference.PreferenceManager
|
||||
import androidx.window.core.layout.WindowWidthSizeClass
|
||||
import my.nanihadesuka.compose.LazyVerticalGridScrollbar
|
||||
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.common.defaultThemedScrollbarSettings
|
||||
import org.schabi.newpipe.ui.components.items.playlist.PlaylistListItem
|
||||
import org.schabi.newpipe.ui.components.items.stream.StreamCardItem
|
||||
import org.schabi.newpipe.ui.components.items.stream.StreamGridItem
|
||||
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: LazyPagingItems<out InfoItem>,
|
||||
mode: ItemViewMode = determineItemViewMode(),
|
||||
gridHeader: LazyGridScope.() -> Unit = {},
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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) {
|
||||
val gridState = rememberLazyGridState()
|
||||
|
||||
LazyVerticalGridScrollbar(state = gridState, settings = defaultThemedScrollbarSettings()) {
|
||||
val windowSizeClass = currentWindowAdaptiveInfo().windowSizeClass
|
||||
val isCompact = windowSizeClass.windowWidthSizeClass == WindowWidthSizeClass.COMPACT
|
||||
val minSize = if (isCompact) 150.dp else 250.dp
|
||||
|
||||
LazyVerticalGrid(state = gridState, columns = GridCells.Adaptive(minSize)) {
|
||||
gridHeader()
|
||||
|
||||
items(items.itemCount) {
|
||||
val item = items[it]!!
|
||||
|
||||
// TODO: Handle channel and playlist items.
|
||||
if (item is StreamInfoItem) {
|
||||
val isSelected = selectedStream == item
|
||||
|
||||
StreamGridItem(item, showProgress, isSelected, isCompact, onClick, onLongClick, onDismissPopup)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
val state = rememberLazyListState()
|
||||
|
||||
LazyColumnThemedScrollbar(state = state) {
|
||||
LazyColumn(modifier = nestedScrollModifier, state = state) {
|
||||
listHeader()
|
||||
|
||||
items(items.itemCount) {
|
||||
val item = items[it]!!
|
||||
|
||||
// TODO: Handle channel items.
|
||||
if (item is StreamInfoItem) {
|
||||
val isSelected = selectedStream == item
|
||||
|
||||
if (mode == ItemViewMode.CARD) {
|
||||
StreamCardItem(item, showProgress, isSelected, onClick, onLongClick, onDismissPopup)
|
||||
} else {
|
||||
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(color = MaterialTheme.colorScheme.background) {
|
||||
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)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package org.schabi.newpipe.ui.components.stream
|
||||
package org.schabi.newpipe.ui.components.items.stream
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
@ -26,7 +26,8 @@ import org.schabi.newpipe.ui.theme.AppTheme
|
||||
@Composable
|
||||
fun StreamCardItem(
|
||||
stream: StreamInfoItem,
|
||||
isSelected: Boolean = false,
|
||||
showProgress: Boolean,
|
||||
isSelected: Boolean,
|
||||
onClick: (StreamInfoItem) -> Unit = {},
|
||||
onLongClick: (StreamInfoItem) -> Unit = {},
|
||||
onDismissPopup: () -> Unit = {}
|
||||
@ -42,6 +43,7 @@ fun StreamCardItem(
|
||||
) {
|
||||
StreamThumbnail(
|
||||
stream = stream,
|
||||
showProgress = showProgress,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
contentScale = ContentScale.FillWidth
|
||||
)
|
||||
@ -68,9 +70,7 @@ fun StreamCardItem(
|
||||
}
|
||||
}
|
||||
|
||||
if (isSelected) {
|
||||
StreamMenu(stream, onDismissPopup)
|
||||
}
|
||||
StreamMenu(stream, isSelected, onDismissPopup)
|
||||
}
|
||||
}
|
||||
|
||||
@ -82,7 +82,7 @@ private fun StreamCardItemPreview(
|
||||
) {
|
||||
AppTheme {
|
||||
Surface(color = MaterialTheme.colorScheme.background) {
|
||||
StreamCardItem(stream)
|
||||
StreamCardItem(stream, showProgress = false, isSelected = false)
|
||||
}
|
||||
}
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package org.schabi.newpipe.ui.components.stream
|
||||
package org.schabi.newpipe.ui.components.items.stream
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
@ -24,6 +24,7 @@ import org.schabi.newpipe.ui.theme.AppTheme
|
||||
@Composable
|
||||
fun StreamGridItem(
|
||||
stream: StreamInfoItem,
|
||||
showProgress: Boolean,
|
||||
isSelected: Boolean = false,
|
||||
isMini: Boolean = false,
|
||||
onClick: (StreamInfoItem) -> Unit = {},
|
||||
@ -41,7 +42,11 @@ fun StreamGridItem(
|
||||
) {
|
||||
val size = if (isMini) DpSize(150.dp, 85.dp) else DpSize(246.dp, 138.dp)
|
||||
|
||||
StreamThumbnail(stream = stream, modifier = Modifier.size(size))
|
||||
StreamThumbnail(
|
||||
stream = stream,
|
||||
showProgress = showProgress,
|
||||
modifier = Modifier.size(size)
|
||||
)
|
||||
|
||||
Text(
|
||||
text = stream.name,
|
||||
@ -58,9 +63,7 @@ fun StreamGridItem(
|
||||
)
|
||||
}
|
||||
|
||||
if (isSelected) {
|
||||
StreamMenu(stream, onDismissPopup)
|
||||
}
|
||||
StreamMenu(stream, isSelected, onDismissPopup)
|
||||
}
|
||||
}
|
||||
|
||||
@ -72,7 +75,7 @@ private fun StreamGridItemPreview(
|
||||
) {
|
||||
AppTheme {
|
||||
Surface(color = MaterialTheme.colorScheme.background) {
|
||||
StreamGridItem(stream)
|
||||
StreamGridItem(stream, showProgress = false)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -85,7 +88,7 @@ private fun StreamMiniGridItemPreview(
|
||||
) {
|
||||
AppTheme {
|
||||
Surface(color = MaterialTheme.colorScheme.background) {
|
||||
StreamGridItem(stream, isMini = true)
|
||||
StreamGridItem(stream, showProgress = false, isMini = true)
|
||||
}
|
||||
}
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package org.schabi.newpipe.ui.components.stream
|
||||
package org.schabi.newpipe.ui.components.items.stream
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
@ -27,26 +27,27 @@ import org.schabi.newpipe.ui.theme.AppTheme
|
||||
@Composable
|
||||
fun StreamListItem(
|
||||
stream: StreamInfoItem,
|
||||
isSelected: Boolean = false,
|
||||
showProgress: Boolean,
|
||||
isSelected: Boolean,
|
||||
onClick: (StreamInfoItem) -> Unit = {},
|
||||
onLongClick: (StreamInfoItem) -> Unit = {},
|
||||
onDismissPopup: () -> Unit = {}
|
||||
) {
|
||||
Box {
|
||||
// Box serves as an anchor for the dropdown menu
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.combinedClickable(onLongClick = { onLongClick(stream) }, onClick = { onClick(stream) })
|
||||
.fillMaxWidth()
|
||||
.padding(12.dp)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.combinedClickable(
|
||||
onLongClick = { onLongClick(stream) },
|
||||
onClick = { onClick(stream) }
|
||||
)
|
||||
.fillMaxWidth()
|
||||
.padding(12.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
StreamThumbnail(
|
||||
stream = stream,
|
||||
modifier = Modifier.size(width = 98.dp, height = 55.dp)
|
||||
showProgress = showProgress,
|
||||
modifier = Modifier.size(width = 140.dp, height = 78.dp)
|
||||
)
|
||||
|
||||
Column {
|
||||
@ -54,7 +55,7 @@ fun StreamListItem(
|
||||
text = stream.name,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
maxLines = 1
|
||||
maxLines = 2
|
||||
)
|
||||
|
||||
Text(text = stream.uploaderName.orEmpty(), style = MaterialTheme.typography.bodySmall)
|
||||
@ -66,9 +67,7 @@ fun StreamListItem(
|
||||
}
|
||||
}
|
||||
|
||||
if (isSelected) {
|
||||
StreamMenu(stream, onDismissPopup)
|
||||
}
|
||||
StreamMenu(stream, isSelected, onDismissPopup)
|
||||
}
|
||||
}
|
||||
|
||||
@ -80,7 +79,7 @@ private fun StreamListItemPreview(
|
||||
) {
|
||||
AppTheme {
|
||||
Surface(color = MaterialTheme.colorScheme.background) {
|
||||
StreamListItem(stream)
|
||||
StreamListItem(stream, showProgress = false, isSelected = false)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,153 @@
|
||||
package org.schabi.newpipe.ui.components.items.stream
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import org.schabi.newpipe.R
|
||||
import org.schabi.newpipe.database.stream.model.StreamEntity
|
||||
import org.schabi.newpipe.download.DownloadDialog
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem
|
||||
import org.schabi.newpipe.ktx.findFragmentActivity
|
||||
import org.schabi.newpipe.local.dialog.PlaylistAppendDialog
|
||||
import org.schabi.newpipe.local.dialog.PlaylistDialog
|
||||
import org.schabi.newpipe.player.helper.PlayerHolder
|
||||
import org.schabi.newpipe.util.NavigationHelper
|
||||
import org.schabi.newpipe.util.SparseItemUtil
|
||||
import org.schabi.newpipe.util.external_communication.ShareUtils
|
||||
import org.schabi.newpipe.viewmodels.StreamViewModel
|
||||
|
||||
@Composable
|
||||
fun StreamMenu(
|
||||
stream: StreamInfoItem,
|
||||
expanded: Boolean,
|
||||
onDismissRequest: () -> Unit
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val streamViewModel = viewModel<StreamViewModel>()
|
||||
val playerHolder = PlayerHolder.getInstance()
|
||||
|
||||
DropdownMenu(expanded = expanded, onDismissRequest = onDismissRequest) {
|
||||
if (playerHolder.isPlayQueueReady) {
|
||||
StreamMenuItem(
|
||||
text = R.string.enqueue_stream,
|
||||
onClick = {
|
||||
onDismissRequest()
|
||||
SparseItemUtil.fetchItemInfoIfSparse(context, stream) {
|
||||
NavigationHelper.enqueueOnPlayer(context, it)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
if (playerHolder.queuePosition < playerHolder.queueSize - 1) {
|
||||
StreamMenuItem(
|
||||
text = R.string.enqueue_next_stream,
|
||||
onClick = {
|
||||
onDismissRequest()
|
||||
SparseItemUtil.fetchItemInfoIfSparse(context, stream) {
|
||||
NavigationHelper.enqueueNextOnPlayer(context, it)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
StreamMenuItem(
|
||||
text = R.string.start_here_on_background,
|
||||
onClick = {
|
||||
onDismissRequest()
|
||||
SparseItemUtil.fetchItemInfoIfSparse(context, stream) {
|
||||
NavigationHelper.playOnBackgroundPlayer(context, it, true)
|
||||
}
|
||||
}
|
||||
)
|
||||
StreamMenuItem(
|
||||
text = R.string.start_here_on_popup,
|
||||
onClick = {
|
||||
onDismissRequest()
|
||||
SparseItemUtil.fetchItemInfoIfSparse(context, stream) {
|
||||
NavigationHelper.playOnPopupPlayer(context, it, true)
|
||||
}
|
||||
}
|
||||
)
|
||||
StreamMenuItem(
|
||||
text = R.string.download,
|
||||
onClick = {
|
||||
onDismissRequest()
|
||||
SparseItemUtil.fetchStreamInfoAndSaveToDatabase(
|
||||
context, stream.serviceId, stream.url
|
||||
) { info ->
|
||||
// TODO: Use an AlertDialog composable instead.
|
||||
val downloadDialog = DownloadDialog(context, info)
|
||||
val fragmentManager = context.findFragmentActivity().supportFragmentManager
|
||||
downloadDialog.show(fragmentManager, "downloadDialog")
|
||||
}
|
||||
}
|
||||
)
|
||||
StreamMenuItem(
|
||||
text = R.string.add_to_playlist,
|
||||
onClick = {
|
||||
onDismissRequest()
|
||||
val list = listOf(StreamEntity(stream))
|
||||
PlaylistDialog.createCorrespondingDialog(context, list) { dialog ->
|
||||
val tag = if (dialog is PlaylistAppendDialog) "append" else "create"
|
||||
dialog.show(
|
||||
context.findFragmentActivity().supportFragmentManager,
|
||||
"StreamDialogEntry@${tag}_playlist"
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
StreamMenuItem(
|
||||
text = R.string.share,
|
||||
onClick = {
|
||||
onDismissRequest()
|
||||
ShareUtils.shareText(context, stream.name, stream.url, stream.thumbnails)
|
||||
}
|
||||
)
|
||||
StreamMenuItem(
|
||||
text = R.string.open_in_browser,
|
||||
onClick = {
|
||||
onDismissRequest()
|
||||
ShareUtils.openUrlInBrowser(context, stream.url)
|
||||
}
|
||||
)
|
||||
StreamMenuItem(
|
||||
text = R.string.mark_as_watched,
|
||||
onClick = {
|
||||
onDismissRequest()
|
||||
streamViewModel.markAsWatched(stream)
|
||||
}
|
||||
)
|
||||
StreamMenuItem(
|
||||
text = R.string.show_channel_details,
|
||||
onClick = {
|
||||
onDismissRequest()
|
||||
SparseItemUtil.fetchUploaderUrlIfSparse(
|
||||
context, stream.serviceId, stream.url, stream.uploaderUrl
|
||||
) { url ->
|
||||
val activity = context.findFragmentActivity()
|
||||
NavigationHelper.openChannelFragment(activity, stream, url)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun StreamMenuItem(
|
||||
@StringRes text: Int,
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
DropdownMenuItem(
|
||||
text = {
|
||||
Text(text = stringResource(text), color = MaterialTheme.colorScheme.onBackground)
|
||||
},
|
||||
onClick = onClick
|
||||
)
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user