Merge pull request #10929 from TeamNewPipe/release-0.27.0
Release v0.27.0 (997)
This commit is contained in:
commit
a557ac3c7b
334 changed files with 8911 additions and 2854 deletions
3
.github/DISCUSSION_TEMPLATE/questions.yml
vendored
3
.github/DISCUSSION_TEMPLATE/questions.yml
vendored
|
@ -1,6 +1,3 @@
|
||||||
name: Question
|
|
||||||
description: Ask about anything NewPipe-related
|
|
||||||
labels: [question]
|
|
||||||
body:
|
body:
|
||||||
- type: markdown
|
- type: markdown
|
||||||
attributes:
|
attributes:
|
||||||
|
|
20
.github/workflows/ci.yml
vendored
20
.github/workflows/ci.yml
vendored
|
@ -36,8 +36,8 @@ jobs:
|
||||||
contents: read
|
contents: read
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
- uses: gradle/wrapper-validation-action@v1
|
- uses: gradle/wrapper-validation-action@v2
|
||||||
|
|
||||||
- name: create and checkout branch
|
- name: create and checkout branch
|
||||||
# push events already checked out the branch
|
# push events already checked out the branch
|
||||||
|
@ -47,7 +47,7 @@ jobs:
|
||||||
run: git checkout -B "$BRANCH"
|
run: git checkout -B "$BRANCH"
|
||||||
|
|
||||||
- name: set up JDK 17
|
- name: set up JDK 17
|
||||||
uses: actions/setup-java@v3
|
uses: actions/setup-java@v4
|
||||||
with:
|
with:
|
||||||
java-version: 17
|
java-version: 17
|
||||||
distribution: "temurin"
|
distribution: "temurin"
|
||||||
|
@ -57,7 +57,7 @@ jobs:
|
||||||
run: ./gradlew assembleDebug lintDebug testDebugUnitTest --stacktrace -DskipFormatKtlint
|
run: ./gradlew assembleDebug lintDebug testDebugUnitTest --stacktrace -DskipFormatKtlint
|
||||||
|
|
||||||
- name: Upload APK
|
- name: Upload APK
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: app
|
name: app
|
||||||
path: app/build/outputs/apk/debug/*.apk
|
path: app/build/outputs/apk/debug/*.apk
|
||||||
|
@ -80,10 +80,10 @@ jobs:
|
||||||
contents: read
|
contents: read
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: set up JDK 17
|
- name: set up JDK 17
|
||||||
uses: actions/setup-java@v3
|
uses: actions/setup-java@v4
|
||||||
with:
|
with:
|
||||||
java-version: 17
|
java-version: 17
|
||||||
distribution: "temurin"
|
distribution: "temurin"
|
||||||
|
@ -98,7 +98,7 @@ jobs:
|
||||||
script: ./gradlew connectedCheck --stacktrace
|
script: ./gradlew connectedCheck --stacktrace
|
||||||
|
|
||||||
- name: Upload test report when tests fail # because the printed out stacktrace (console) is too short, see also #7553
|
- name: Upload test report when tests fail # because the printed out stacktrace (console) is too short, see also #7553
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v4
|
||||||
if: failure()
|
if: failure()
|
||||||
with:
|
with:
|
||||||
name: android-test-report-api${{ matrix.api-level }}
|
name: android-test-report-api${{ matrix.api-level }}
|
||||||
|
@ -111,19 +111,19 @@ jobs:
|
||||||
contents: read
|
contents: read
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis
|
fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis
|
||||||
|
|
||||||
- name: Set up JDK 17
|
- name: Set up JDK 17
|
||||||
uses: actions/setup-java@v3
|
uses: actions/setup-java@v4
|
||||||
with:
|
with:
|
||||||
java-version: 17
|
java-version: 17
|
||||||
distribution: "temurin"
|
distribution: "temurin"
|
||||||
cache: 'gradle'
|
cache: 'gradle'
|
||||||
|
|
||||||
- name: Cache SonarCloud packages
|
- name: Cache SonarCloud packages
|
||||||
uses: actions/cache@v3
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
path: ~/.sonar/cache
|
path: ~/.sonar/cache
|
||||||
key: ${{ runner.os }}-sonar
|
key: ${{ runner.os }}-sonar
|
||||||
|
|
6
.github/workflows/image-minimizer.yml
vendored
6
.github/workflows/image-minimizer.yml
vendored
|
@ -17,9 +17,9 @@ jobs:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- uses: actions/setup-node@v3
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 16
|
node-version: 16
|
||||||
|
|
||||||
|
@ -27,7 +27,7 @@ jobs:
|
||||||
run: npm i probe-image-size@7.2.3 --ignore-scripts
|
run: npm i probe-image-size@7.2.3 --ignore-scripts
|
||||||
|
|
||||||
- name: Minimize simple images
|
- name: Minimize simple images
|
||||||
uses: actions/github-script@v6
|
uses: actions/github-script@v7
|
||||||
timeout-minutes: 3
|
timeout-minutes: 3
|
||||||
with:
|
with:
|
||||||
script: |
|
script: |
|
||||||
|
|
2
.github/workflows/pr-labeler.yml
vendored
2
.github/workflows/pr-labeler.yml
vendored
|
@ -1,5 +1,5 @@
|
||||||
name: "PR size labeler"
|
name: "PR size labeler"
|
||||||
on: [pull_request]
|
on: [pull_request_target]
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
pull-requests: write
|
pull-requests: write
|
||||||
|
|
|
@ -13,7 +13,7 @@
|
||||||
<a href="https://github.com/TeamNewPipe/NewPipe/actions" alt="Build Status"><img src="https://github.com/TeamNewPipe/NewPipe/workflows/CI/badge.svg?branch=dev&event=push"></a>
|
<a href="https://github.com/TeamNewPipe/NewPipe/actions" alt="Build Status"><img src="https://github.com/TeamNewPipe/NewPipe/workflows/CI/badge.svg?branch=dev&event=push"></a>
|
||||||
<a href="https://hosted.weblate.org/engage/newpipe/" alt="Translation Status"><img src="https://hosted.weblate.org/widgets/newpipe/-/svg-badge.svg"></a>
|
<a href="https://hosted.weblate.org/engage/newpipe/" alt="Translation Status"><img src="https://hosted.weblate.org/widgets/newpipe/-/svg-badge.svg"></a>
|
||||||
<a href="https://web.libera.chat/#newpipe" alt="IRC channel: #newpipe"><img src="https://img.shields.io/badge/IRC%20chat-%23newpipe-brightgreen.svg"></a>
|
<a href="https://web.libera.chat/#newpipe" alt="IRC channel: #newpipe"><img src="https://img.shields.io/badge/IRC%20chat-%23newpipe-brightgreen.svg"></a>
|
||||||
<a href="https://matrix.to/#/#newpipe:libera.chat" alt="Matrix channel: #newpipe"><img src="https://img.shields.io/badge/Matrix%20chat-%23newpipe-blue"></a>
|
<a href="https://matrix.to/#/#newpipe:matrix.newpipe-ev.de" alt="Matrix channel: #newpipe"><img src="https://img.shields.io/badge/Matrix%20chat-%23newpipe-blue"></a>
|
||||||
</p>
|
</p>
|
||||||
<hr>
|
<hr>
|
||||||
<p align="center"><a href="#screenshots">Screenshots</a> • <a href="#supported-services">Supported Services</a> • <a href="#description">Description</a> • <a href="#features">Features</a> • <a href="#installation-and-updates">Installation and updates</a> • <a href="#contribution">Contribution</a> • <a href="#donate">Donate</a> • <a href="#license">License</a></p>
|
<p align="center"><a href="#screenshots">Screenshots</a> • <a href="#supported-services">Supported Services</a> • <a href="#description">Description</a> • <a href="#features">Features</a> • <a href="#installation-and-updates">Installation and updates</a> • <a href="#contribution">Contribution</a> • <a href="#donate">Donate</a> • <a href="#license">License</a></p>
|
||||||
|
@ -22,9 +22,10 @@
|
||||||
|
|
||||||
*Read this document in other languages: [Deutsch](doc/README.de.md), [English](README.md), [Español](doc/README.es.md), [Français](doc/README.fr.md), [हिन्दी](doc/README.hi.md), [Italiano](doc/README.it.md), [한국어](doc/README.ko.md), [Português Brasil](doc/README.pt_BR.md), [Polski](doc/README.pl.md), [ਪੰਜਾਬੀ ](doc/README.pa.md), [日本語](doc/README.ja.md), [Română](doc/README.ro.md), [Soomaali](doc/README.so.md), [Türkçe](doc/README.tr.md), [正體中文](doc/README.zh_TW.md), [অসমীয়া](doc/README.asm.md), [Српски](doc/README.sr.md)*
|
*Read this document in other languages: [Deutsch](doc/README.de.md), [English](README.md), [Español](doc/README.es.md), [Français](doc/README.fr.md), [हिन्दी](doc/README.hi.md), [Italiano](doc/README.it.md), [한국어](doc/README.ko.md), [Português Brasil](doc/README.pt_BR.md), [Polski](doc/README.pl.md), [ਪੰਜਾਬੀ ](doc/README.pa.md), [日本語](doc/README.ja.md), [Română](doc/README.ro.md), [Soomaali](doc/README.so.md), [Türkçe](doc/README.tr.md), [正體中文](doc/README.zh_TW.md), [অসমীয়া](doc/README.asm.md), [Српски](doc/README.sr.md)*
|
||||||
|
|
||||||
<b>WARNING: THIS APP IS IN BETA, SO YOU MAY ENCOUNTER BUGS. IF YOU DO, OPEN AN ISSUE IN OUR GITHUB REPOSITORY BY FILLING OUT THE ISSUE TEMPLATE.</b>
|
> [!warning]
|
||||||
|
> <b>THIS APP IS IN BETA, SO YOU MAY ENCOUNTER BUGS. IF YOU DO, OPEN AN ISSUE IN OUR GITHUB REPOSITORY BY FILLING OUT THE ISSUE TEMPLATE.</b>
|
||||||
<b>PUTTING NEWPIPE, OR ANY FORK OF IT, INTO THE GOOGLE PLAY STORE VIOLATES THEIR TERMS AND CONDITIONS.</b>
|
>
|
||||||
|
> <b>PUTTING NEWPIPE, OR ANY FORK OF IT, INTO THE GOOGLE PLAY STORE VIOLATES THEIR TERMS AND CONDITIONS.</b>
|
||||||
|
|
||||||
## Screenshots
|
## Screenshots
|
||||||
|
|
||||||
|
|
|
@ -12,7 +12,7 @@ plugins {
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdk 33
|
compileSdk 34
|
||||||
namespace 'org.schabi.newpipe'
|
namespace 'org.schabi.newpipe'
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
|
@ -20,8 +20,8 @@ android {
|
||||||
resValue "string", "app_name", "NewPipe"
|
resValue "string", "app_name", "NewPipe"
|
||||||
minSdk 21
|
minSdk 21
|
||||||
targetSdk 33
|
targetSdk 33
|
||||||
versionCode 996
|
versionCode 997
|
||||||
versionName "0.26.1"
|
versionName "0.27.0"
|
||||||
|
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
|
|
||||||
|
@ -98,7 +98,9 @@ android {
|
||||||
resources {
|
resources {
|
||||||
// remove two files which belong to jsoup
|
// remove two files which belong to jsoup
|
||||||
// no idea how they ended up in the META-INF dir...
|
// no idea how they ended up in the META-INF dir...
|
||||||
excludes += ['META-INF/README.md', 'META-INF/CHANGES']
|
excludes += ['META-INF/README.md', 'META-INF/CHANGES',
|
||||||
|
// 'COPYRIGHT' belongs to RxJava...
|
||||||
|
'META-INF/COPYRIGHT']
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -106,9 +108,9 @@ android {
|
||||||
ext {
|
ext {
|
||||||
checkstyleVersion = '10.12.1'
|
checkstyleVersion = '10.12.1'
|
||||||
|
|
||||||
androidxLifecycleVersion = '2.5.1'
|
androidxLifecycleVersion = '2.6.2'
|
||||||
androidxRoomVersion = '2.5.2'
|
androidxRoomVersion = '2.6.1'
|
||||||
androidxWorkVersion = '2.7.1'
|
androidxWorkVersion = '2.8.1'
|
||||||
|
|
||||||
icepickVersion = '3.2.0'
|
icepickVersion = '3.2.0'
|
||||||
exoPlayerVersion = '2.18.7'
|
exoPlayerVersion = '2.18.7'
|
||||||
|
@ -118,7 +120,6 @@ ext {
|
||||||
|
|
||||||
leakCanaryVersion = '2.12'
|
leakCanaryVersion = '2.12'
|
||||||
stethoVersion = '1.6.0'
|
stethoVersion = '1.6.0'
|
||||||
mockitoVersion = '4.0.0'
|
|
||||||
}
|
}
|
||||||
|
|
||||||
configurations {
|
configurations {
|
||||||
|
@ -133,7 +134,7 @@ checkstyle {
|
||||||
toolVersion = checkstyleVersion
|
toolVersion = checkstyleVersion
|
||||||
}
|
}
|
||||||
|
|
||||||
task runCheckstyle(type: Checkstyle) {
|
tasks.register('runCheckstyle', Checkstyle) {
|
||||||
source 'src'
|
source 'src'
|
||||||
include '**/*.java'
|
include '**/*.java'
|
||||||
exclude '**/gen/**'
|
exclude '**/gen/**'
|
||||||
|
@ -154,7 +155,7 @@ task runCheckstyle(type: Checkstyle) {
|
||||||
def outputDir = "${project.buildDir}/reports/ktlint/"
|
def outputDir = "${project.buildDir}/reports/ktlint/"
|
||||||
def inputFiles = project.fileTree(dir: "src", include: "**/*.kt")
|
def inputFiles = project.fileTree(dir: "src", include: "**/*.kt")
|
||||||
|
|
||||||
task runKtlint(type: JavaExec) {
|
tasks.register('runKtlint', JavaExec) {
|
||||||
inputs.files(inputFiles)
|
inputs.files(inputFiles)
|
||||||
outputs.dir(outputDir)
|
outputs.dir(outputDir)
|
||||||
getMainClass().set("com.pinterest.ktlint.Main")
|
getMainClass().set("com.pinterest.ktlint.Main")
|
||||||
|
@ -163,7 +164,7 @@ task runKtlint(type: JavaExec) {
|
||||||
jvmArgs("--add-opens", "java.base/java.lang=ALL-UNNAMED")
|
jvmArgs("--add-opens", "java.base/java.lang=ALL-UNNAMED")
|
||||||
}
|
}
|
||||||
|
|
||||||
task formatKtlint(type: JavaExec) {
|
tasks.register('formatKtlint', JavaExec) {
|
||||||
inputs.files(inputFiles)
|
inputs.files(inputFiles)
|
||||||
outputs.dir(outputDir)
|
outputs.dir(outputDir)
|
||||||
getMainClass().set("com.pinterest.ktlint.Main")
|
getMainClass().set("com.pinterest.ktlint.Main")
|
||||||
|
@ -189,7 +190,7 @@ sonar {
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
/** Desugaring **/
|
/** Desugaring **/
|
||||||
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs_nio:2.0.3'
|
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs_nio:2.0.4'
|
||||||
|
|
||||||
/** NewPipe libraries **/
|
/** NewPipe libraries **/
|
||||||
// You can use a local version by uncommenting a few lines in settings.gradle
|
// You can use a local version by uncommenting a few lines in settings.gradle
|
||||||
|
@ -197,7 +198,7 @@ dependencies {
|
||||||
// name and the commit hash with the commit hash of the (pushed) commit you want to test
|
// name and the commit hash with the commit hash of the (pushed) commit you want to test
|
||||||
// This works thanks to JitPack: https://jitpack.io/
|
// This works thanks to JitPack: https://jitpack.io/
|
||||||
implementation 'com.github.TeamNewPipe:nanojson:1d9e1aea9049fc9f85e68b43ba39fe7be1c1f751'
|
implementation 'com.github.TeamNewPipe:nanojson:1d9e1aea9049fc9f85e68b43ba39fe7be1c1f751'
|
||||||
implementation 'com.github.TeamNewPipe:NewPipeExtractor:v0.23.1'
|
implementation 'com.github.TeamNewPipe:NewPipeExtractor:fbe9e6223aceac8d6f6b352afaed4cb61aed1c79'
|
||||||
implementation 'com.github.TeamNewPipe:NoNonsense-FilePicker:5.0.0'
|
implementation 'com.github.TeamNewPipe:NoNonsense-FilePicker:5.0.0'
|
||||||
|
|
||||||
/** Checkstyle **/
|
/** Checkstyle **/
|
||||||
|
@ -208,28 +209,28 @@ dependencies {
|
||||||
implementation "org.jetbrains.kotlin:kotlin-stdlib:${kotlin_version}"
|
implementation "org.jetbrains.kotlin:kotlin-stdlib:${kotlin_version}"
|
||||||
|
|
||||||
/** AndroidX **/
|
/** AndroidX **/
|
||||||
implementation 'androidx.appcompat:appcompat:1.5.1'
|
implementation 'androidx.appcompat:appcompat:1.6.1'
|
||||||
implementation 'androidx.cardview:cardview:1.0.0'
|
implementation 'androidx.cardview:cardview:1.0.0'
|
||||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
|
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
|
||||||
implementation 'androidx.core:core-ktx:1.10.0'
|
implementation 'androidx.core:core-ktx:1.12.0'
|
||||||
implementation 'androidx.documentfile:documentfile:1.0.1'
|
implementation 'androidx.documentfile:documentfile:1.0.1'
|
||||||
implementation 'androidx.fragment:fragment-ktx:1.4.1'
|
implementation 'androidx.fragment:fragment-ktx:1.6.2'
|
||||||
implementation "androidx.lifecycle:lifecycle-livedata-ktx:${androidxLifecycleVersion}"
|
implementation "androidx.lifecycle:lifecycle-livedata-ktx:${androidxLifecycleVersion}"
|
||||||
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:${androidxLifecycleVersion}"
|
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:${androidxLifecycleVersion}"
|
||||||
implementation 'androidx.localbroadcastmanager:localbroadcastmanager:1.1.0'
|
implementation 'androidx.localbroadcastmanager:localbroadcastmanager:1.1.0'
|
||||||
implementation 'androidx.media:media:1.6.0'
|
implementation 'androidx.media:media:1.7.0'
|
||||||
implementation 'androidx.preference:preference:1.2.0'
|
implementation 'androidx.preference:preference:1.2.1'
|
||||||
implementation 'androidx.recyclerview:recyclerview:1.2.1'
|
implementation 'androidx.recyclerview:recyclerview:1.3.2'
|
||||||
implementation "androidx.room:room-runtime:${androidxRoomVersion}"
|
implementation "androidx.room:room-runtime:${androidxRoomVersion}"
|
||||||
implementation "androidx.room:room-rxjava3:${androidxRoomVersion}"
|
implementation "androidx.room:room-rxjava3:${androidxRoomVersion}"
|
||||||
kapt "androidx.room:room-compiler:${androidxRoomVersion}"
|
kapt "androidx.room:room-compiler:${androidxRoomVersion}"
|
||||||
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
|
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
|
||||||
// Newer version specified to prevent accessibility regressions with RecyclerView, see:
|
// Newer version specified to prevent accessibility regressions with RecyclerView, see:
|
||||||
// https://developer.android.com/jetpack/androidx/releases/viewpager2#1.1.0-alpha01
|
// https://developer.android.com/jetpack/androidx/releases/viewpager2#1.1.0-alpha01
|
||||||
implementation 'androidx.viewpager2:viewpager2:1.1.0-beta01'
|
implementation 'androidx.viewpager2:viewpager2:1.1.0-beta02'
|
||||||
implementation "androidx.work:work-runtime-ktx:${androidxWorkVersion}"
|
implementation "androidx.work:work-runtime-ktx:${androidxWorkVersion}"
|
||||||
implementation "androidx.work:work-rxjava3:${androidxWorkVersion}"
|
implementation "androidx.work:work-rxjava3:${androidxWorkVersion}"
|
||||||
implementation 'com.google.android.material:material:1.9.0'
|
implementation 'com.google.android.material:material:1.11.0'
|
||||||
|
|
||||||
/** Third-party libraries **/
|
/** Third-party libraries **/
|
||||||
// Instance state boilerplate elimination
|
// Instance state boilerplate elimination
|
||||||
|
@ -237,13 +238,10 @@ dependencies {
|
||||||
kapt "frankiesardo:icepick-processor:${icepickVersion}"
|
kapt "frankiesardo:icepick-processor:${icepickVersion}"
|
||||||
|
|
||||||
// HTML parser
|
// HTML parser
|
||||||
implementation "org.jsoup:jsoup:1.16.1"
|
implementation "org.jsoup:jsoup:1.17.2"
|
||||||
|
|
||||||
// HTTP client
|
// HTTP client
|
||||||
implementation "com.squareup.okhttp3:okhttp:4.11.0"
|
implementation "com.squareup.okhttp3:okhttp:4.12.0"
|
||||||
// okhttp3:4.11.0 introduces a vulnerability from com.squareup.okio:okio@3.3.0,
|
|
||||||
// remove com.squareup.okio:okio when updating okhttp
|
|
||||||
implementation "com.squareup.okio:okio:3.4.0"
|
|
||||||
|
|
||||||
// Media player
|
// Media player
|
||||||
implementation "com.google.android.exoplayer:exoplayer-core:${exoPlayerVersion}"
|
implementation "com.google.android.exoplayer:exoplayer-core:${exoPlayerVersion}"
|
||||||
|
@ -272,19 +270,19 @@ dependencies {
|
||||||
implementation "io.noties.markwon:linkify:${markwonVersion}"
|
implementation "io.noties.markwon:linkify:${markwonVersion}"
|
||||||
|
|
||||||
// Crash reporting
|
// Crash reporting
|
||||||
implementation "ch.acra:acra-core:5.10.1"
|
implementation "ch.acra:acra-core:5.11.3"
|
||||||
|
|
||||||
// Properly restarting
|
// Properly restarting
|
||||||
implementation 'com.jakewharton:process-phoenix:2.1.2'
|
implementation 'com.jakewharton:process-phoenix:2.1.2'
|
||||||
|
|
||||||
// Reactive extensions for Java VM
|
// Reactive extensions for Java VM
|
||||||
implementation "io.reactivex.rxjava3:rxjava:3.1.6"
|
implementation "io.reactivex.rxjava3:rxjava:3.1.8"
|
||||||
implementation "io.reactivex.rxjava3:rxandroid:3.0.2"
|
implementation "io.reactivex.rxjava3:rxandroid:3.0.2"
|
||||||
// RxJava binding APIs for Android UI widgets
|
// RxJava binding APIs for Android UI widgets
|
||||||
implementation "com.jakewharton.rxbinding4:rxbinding:4.0.0"
|
implementation "com.jakewharton.rxbinding4:rxbinding:4.0.0"
|
||||||
|
|
||||||
// Date and time formatting
|
// Date and time formatting
|
||||||
implementation "org.ocpsoft.prettytime:prettytime:5.0.6.Final"
|
implementation "org.ocpsoft.prettytime:prettytime:5.0.7.Final"
|
||||||
|
|
||||||
/** Debugging **/
|
/** Debugging **/
|
||||||
// Memory leak detection
|
// Memory leak detection
|
||||||
|
@ -297,13 +295,12 @@ dependencies {
|
||||||
|
|
||||||
/** Testing **/
|
/** Testing **/
|
||||||
testImplementation 'junit:junit:4.13.2'
|
testImplementation 'junit:junit:4.13.2'
|
||||||
testImplementation "org.mockito:mockito-core:${mockitoVersion}"
|
testImplementation 'org.mockito:mockito-core:5.6.0'
|
||||||
testImplementation "org.mockito:mockito-inline:${mockitoVersion}"
|
|
||||||
|
|
||||||
androidTestImplementation "androidx.test.ext:junit:1.1.5"
|
androidTestImplementation "androidx.test.ext:junit:1.1.5"
|
||||||
androidTestImplementation "androidx.test:runner:1.5.2"
|
androidTestImplementation "androidx.test:runner:1.5.2"
|
||||||
androidTestImplementation "androidx.room:room-testing:${androidxRoomVersion}"
|
androidTestImplementation "androidx.room:room-testing:${androidxRoomVersion}"
|
||||||
androidTestImplementation "org.assertj:assertj-core:3.23.1"
|
androidTestImplementation "org.assertj:assertj-core:3.24.2"
|
||||||
}
|
}
|
||||||
|
|
||||||
static String getGitWorkingBranch() {
|
static String getGitWorkingBranch() {
|
||||||
|
|
737
app/schemas/org.schabi.newpipe.database.AppDatabase/8.json
Normal file
737
app/schemas/org.schabi.newpipe.database.AppDatabase/8.json
Normal file
|
@ -0,0 +1,737 @@
|
||||||
|
{
|
||||||
|
"formatVersion": 1,
|
||||||
|
"database": {
|
||||||
|
"version": 8,
|
||||||
|
"identityHash": "012fc8e7ad3333f1597347f34e76a513",
|
||||||
|
"entities": [
|
||||||
|
{
|
||||||
|
"tableName": "subscriptions",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `service_id` INTEGER NOT NULL, `url` TEXT, `name` TEXT, `avatar_url` TEXT, `subscriber_count` INTEGER, `description` TEXT, `notification_mode` INTEGER NOT NULL)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "uid",
|
||||||
|
"columnName": "uid",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "serviceId",
|
||||||
|
"columnName": "service_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "url",
|
||||||
|
"columnName": "url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "name",
|
||||||
|
"columnName": "name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "avatarUrl",
|
||||||
|
"columnName": "avatar_url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "subscriberCount",
|
||||||
|
"columnName": "subscriber_count",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "description",
|
||||||
|
"columnName": "description",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "notificationMode",
|
||||||
|
"columnName": "notification_mode",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": true,
|
||||||
|
"columnNames": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_subscriptions_service_id_url",
|
||||||
|
"unique": true,
|
||||||
|
"columnNames": [
|
||||||
|
"service_id",
|
||||||
|
"url"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_subscriptions_service_id_url` ON `${TABLE_NAME}` (`service_id`, `url`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "search_history",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`creation_date` INTEGER, `service_id` INTEGER NOT NULL, `search` TEXT, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "creationDate",
|
||||||
|
"columnName": "creation_date",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "serviceId",
|
||||||
|
"columnName": "service_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "search",
|
||||||
|
"columnName": "search",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": true,
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_search_history_search",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"search"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_search_history_search` ON `${TABLE_NAME}` (`search`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "streams",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `service_id` INTEGER NOT NULL, `url` TEXT NOT NULL, `title` TEXT NOT NULL, `stream_type` TEXT NOT NULL, `duration` INTEGER NOT NULL, `uploader` TEXT NOT NULL, `uploader_url` TEXT, `thumbnail_url` TEXT, `view_count` INTEGER, `textual_upload_date` TEXT, `upload_date` INTEGER, `is_upload_date_approximation` INTEGER)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "uid",
|
||||||
|
"columnName": "uid",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "serviceId",
|
||||||
|
"columnName": "service_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "url",
|
||||||
|
"columnName": "url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "title",
|
||||||
|
"columnName": "title",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "streamType",
|
||||||
|
"columnName": "stream_type",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "duration",
|
||||||
|
"columnName": "duration",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "uploader",
|
||||||
|
"columnName": "uploader",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "uploaderUrl",
|
||||||
|
"columnName": "uploader_url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "thumbnailUrl",
|
||||||
|
"columnName": "thumbnail_url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "viewCount",
|
||||||
|
"columnName": "view_count",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "textualUploadDate",
|
||||||
|
"columnName": "textual_upload_date",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "uploadDate",
|
||||||
|
"columnName": "upload_date",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "isUploadDateApproximation",
|
||||||
|
"columnName": "is_upload_date_approximation",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": true,
|
||||||
|
"columnNames": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_streams_service_id_url",
|
||||||
|
"unique": true,
|
||||||
|
"columnNames": [
|
||||||
|
"service_id",
|
||||||
|
"url"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_streams_service_id_url` ON `${TABLE_NAME}` (`service_id`, `url`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "stream_history",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`stream_id` INTEGER NOT NULL, `access_date` INTEGER NOT NULL, `repeat_count` INTEGER NOT NULL, PRIMARY KEY(`stream_id`, `access_date`), FOREIGN KEY(`stream_id`) REFERENCES `streams`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE )",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "streamUid",
|
||||||
|
"columnName": "stream_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "accessDate",
|
||||||
|
"columnName": "access_date",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "repeatCount",
|
||||||
|
"columnName": "repeat_count",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"stream_id",
|
||||||
|
"access_date"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_stream_history_stream_id",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"stream_id"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_stream_history_stream_id` ON `${TABLE_NAME}` (`stream_id`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "streams",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"stream_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "stream_state",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`stream_id` INTEGER NOT NULL, `progress_time` INTEGER NOT NULL, PRIMARY KEY(`stream_id`), FOREIGN KEY(`stream_id`) REFERENCES `streams`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE )",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "streamUid",
|
||||||
|
"columnName": "stream_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "progressMillis",
|
||||||
|
"columnName": "progress_time",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"stream_id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "streams",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"stream_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "playlists",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT, `is_thumbnail_permanent` INTEGER NOT NULL, `thumbnail_stream_id` INTEGER NOT NULL)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "uid",
|
||||||
|
"columnName": "uid",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "name",
|
||||||
|
"columnName": "name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "isThumbnailPermanent",
|
||||||
|
"columnName": "is_thumbnail_permanent",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "thumbnailStreamId",
|
||||||
|
"columnName": "thumbnail_stream_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": true,
|
||||||
|
"columnNames": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_playlists_name",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"name"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_playlists_name` ON `${TABLE_NAME}` (`name`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "playlist_stream_join",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`playlist_id` INTEGER NOT NULL, `stream_id` INTEGER NOT NULL, `join_index` INTEGER NOT NULL, PRIMARY KEY(`playlist_id`, `join_index`), FOREIGN KEY(`playlist_id`) REFERENCES `playlists`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, FOREIGN KEY(`stream_id`) REFERENCES `streams`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "playlistUid",
|
||||||
|
"columnName": "playlist_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "streamUid",
|
||||||
|
"columnName": "stream_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "index",
|
||||||
|
"columnName": "join_index",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"playlist_id",
|
||||||
|
"join_index"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_playlist_stream_join_playlist_id_join_index",
|
||||||
|
"unique": true,
|
||||||
|
"columnNames": [
|
||||||
|
"playlist_id",
|
||||||
|
"join_index"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_playlist_stream_join_playlist_id_join_index` ON `${TABLE_NAME}` (`playlist_id`, `join_index`)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "index_playlist_stream_join_stream_id",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"stream_id"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_playlist_stream_join_stream_id` ON `${TABLE_NAME}` (`stream_id`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "playlists",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"playlist_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"table": "streams",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"stream_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "remote_playlists",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `service_id` INTEGER NOT NULL, `name` TEXT, `url` TEXT, `thumbnail_url` TEXT, `uploader` TEXT, `stream_count` INTEGER)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "uid",
|
||||||
|
"columnName": "uid",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "serviceId",
|
||||||
|
"columnName": "service_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "name",
|
||||||
|
"columnName": "name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "url",
|
||||||
|
"columnName": "url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "thumbnailUrl",
|
||||||
|
"columnName": "thumbnail_url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "uploader",
|
||||||
|
"columnName": "uploader",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "streamCount",
|
||||||
|
"columnName": "stream_count",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": true,
|
||||||
|
"columnNames": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_remote_playlists_name",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"name"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_remote_playlists_name` ON `${TABLE_NAME}` (`name`)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "index_remote_playlists_service_id_url",
|
||||||
|
"unique": true,
|
||||||
|
"columnNames": [
|
||||||
|
"service_id",
|
||||||
|
"url"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_remote_playlists_service_id_url` ON `${TABLE_NAME}` (`service_id`, `url`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "feed",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`stream_id` INTEGER NOT NULL, `subscription_id` INTEGER NOT NULL, PRIMARY KEY(`stream_id`, `subscription_id`), FOREIGN KEY(`stream_id`) REFERENCES `streams`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, FOREIGN KEY(`subscription_id`) REFERENCES `subscriptions`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "streamId",
|
||||||
|
"columnName": "stream_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "subscriptionId",
|
||||||
|
"columnName": "subscription_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"stream_id",
|
||||||
|
"subscription_id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_feed_subscription_id",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"subscription_id"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_feed_subscription_id` ON `${TABLE_NAME}` (`subscription_id`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "streams",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"stream_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"table": "subscriptions",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"subscription_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "feed_group",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `icon_id` INTEGER NOT NULL, `sort_order` INTEGER NOT NULL)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "uid",
|
||||||
|
"columnName": "uid",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "name",
|
||||||
|
"columnName": "name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "icon",
|
||||||
|
"columnName": "icon_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "sortOrder",
|
||||||
|
"columnName": "sort_order",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": true,
|
||||||
|
"columnNames": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_feed_group_sort_order",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"sort_order"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_feed_group_sort_order` ON `${TABLE_NAME}` (`sort_order`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "feed_group_subscription_join",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`group_id` INTEGER NOT NULL, `subscription_id` INTEGER NOT NULL, PRIMARY KEY(`group_id`, `subscription_id`), FOREIGN KEY(`group_id`) REFERENCES `feed_group`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, FOREIGN KEY(`subscription_id`) REFERENCES `subscriptions`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "feedGroupId",
|
||||||
|
"columnName": "group_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "subscriptionId",
|
||||||
|
"columnName": "subscription_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"group_id",
|
||||||
|
"subscription_id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_feed_group_subscription_join_subscription_id",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"subscription_id"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_feed_group_subscription_join_subscription_id` ON `${TABLE_NAME}` (`subscription_id`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "feed_group",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"group_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"table": "subscriptions",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"subscription_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "feed_last_updated",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`subscription_id` INTEGER NOT NULL, `last_updated` INTEGER, PRIMARY KEY(`subscription_id`), FOREIGN KEY(`subscription_id`) REFERENCES `subscriptions`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "subscriptionId",
|
||||||
|
"columnName": "subscription_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastUpdated",
|
||||||
|
"columnName": "last_updated",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"subscription_id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "subscriptions",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"subscription_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"views": [],
|
||||||
|
"setupQueries": [
|
||||||
|
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||||
|
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '012fc8e7ad3333f1597347f34e76a513')"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
730
app/schemas/org.schabi.newpipe.database.AppDatabase/9.json
Normal file
730
app/schemas/org.schabi.newpipe.database.AppDatabase/9.json
Normal file
|
@ -0,0 +1,730 @@
|
||||||
|
{
|
||||||
|
"formatVersion": 1,
|
||||||
|
"database": {
|
||||||
|
"version": 9,
|
||||||
|
"identityHash": "7591e8039faa74d8c0517dc867af9d3e",
|
||||||
|
"entities": [
|
||||||
|
{
|
||||||
|
"tableName": "subscriptions",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `service_id` INTEGER NOT NULL, `url` TEXT, `name` TEXT, `avatar_url` TEXT, `subscriber_count` INTEGER, `description` TEXT, `notification_mode` INTEGER NOT NULL)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "uid",
|
||||||
|
"columnName": "uid",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "serviceId",
|
||||||
|
"columnName": "service_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "url",
|
||||||
|
"columnName": "url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "name",
|
||||||
|
"columnName": "name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "avatarUrl",
|
||||||
|
"columnName": "avatar_url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "subscriberCount",
|
||||||
|
"columnName": "subscriber_count",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "description",
|
||||||
|
"columnName": "description",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "notificationMode",
|
||||||
|
"columnName": "notification_mode",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": true,
|
||||||
|
"columnNames": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_subscriptions_service_id_url",
|
||||||
|
"unique": true,
|
||||||
|
"columnNames": [
|
||||||
|
"service_id",
|
||||||
|
"url"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_subscriptions_service_id_url` ON `${TABLE_NAME}` (`service_id`, `url`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "search_history",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`creation_date` INTEGER, `service_id` INTEGER NOT NULL, `search` TEXT, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "creationDate",
|
||||||
|
"columnName": "creation_date",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "serviceId",
|
||||||
|
"columnName": "service_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "search",
|
||||||
|
"columnName": "search",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": true,
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_search_history_search",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"search"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_search_history_search` ON `${TABLE_NAME}` (`search`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "streams",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `service_id` INTEGER NOT NULL, `url` TEXT NOT NULL, `title` TEXT NOT NULL, `stream_type` TEXT NOT NULL, `duration` INTEGER NOT NULL, `uploader` TEXT NOT NULL, `uploader_url` TEXT, `thumbnail_url` TEXT, `view_count` INTEGER, `textual_upload_date` TEXT, `upload_date` INTEGER, `is_upload_date_approximation` INTEGER)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "uid",
|
||||||
|
"columnName": "uid",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "serviceId",
|
||||||
|
"columnName": "service_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "url",
|
||||||
|
"columnName": "url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "title",
|
||||||
|
"columnName": "title",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "streamType",
|
||||||
|
"columnName": "stream_type",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "duration",
|
||||||
|
"columnName": "duration",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "uploader",
|
||||||
|
"columnName": "uploader",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "uploaderUrl",
|
||||||
|
"columnName": "uploader_url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "thumbnailUrl",
|
||||||
|
"columnName": "thumbnail_url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "viewCount",
|
||||||
|
"columnName": "view_count",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "textualUploadDate",
|
||||||
|
"columnName": "textual_upload_date",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "uploadDate",
|
||||||
|
"columnName": "upload_date",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "isUploadDateApproximation",
|
||||||
|
"columnName": "is_upload_date_approximation",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": true,
|
||||||
|
"columnNames": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_streams_service_id_url",
|
||||||
|
"unique": true,
|
||||||
|
"columnNames": [
|
||||||
|
"service_id",
|
||||||
|
"url"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_streams_service_id_url` ON `${TABLE_NAME}` (`service_id`, `url`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "stream_history",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`stream_id` INTEGER NOT NULL, `access_date` INTEGER NOT NULL, `repeat_count` INTEGER NOT NULL, PRIMARY KEY(`stream_id`, `access_date`), FOREIGN KEY(`stream_id`) REFERENCES `streams`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE )",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "streamUid",
|
||||||
|
"columnName": "stream_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "accessDate",
|
||||||
|
"columnName": "access_date",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "repeatCount",
|
||||||
|
"columnName": "repeat_count",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"stream_id",
|
||||||
|
"access_date"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_stream_history_stream_id",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"stream_id"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_stream_history_stream_id` ON `${TABLE_NAME}` (`stream_id`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "streams",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"stream_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "stream_state",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`stream_id` INTEGER NOT NULL, `progress_time` INTEGER NOT NULL, PRIMARY KEY(`stream_id`), FOREIGN KEY(`stream_id`) REFERENCES `streams`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE )",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "streamUid",
|
||||||
|
"columnName": "stream_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "progressMillis",
|
||||||
|
"columnName": "progress_time",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"stream_id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "streams",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"stream_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "playlists",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT, `is_thumbnail_permanent` INTEGER NOT NULL, `thumbnail_stream_id` INTEGER NOT NULL, `display_index` INTEGER NOT NULL)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "uid",
|
||||||
|
"columnName": "uid",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "name",
|
||||||
|
"columnName": "name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "isThumbnailPermanent",
|
||||||
|
"columnName": "is_thumbnail_permanent",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "thumbnailStreamId",
|
||||||
|
"columnName": "thumbnail_stream_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "displayIndex",
|
||||||
|
"columnName": "display_index",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": true,
|
||||||
|
"columnNames": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "playlist_stream_join",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`playlist_id` INTEGER NOT NULL, `stream_id` INTEGER NOT NULL, `join_index` INTEGER NOT NULL, PRIMARY KEY(`playlist_id`, `join_index`), FOREIGN KEY(`playlist_id`) REFERENCES `playlists`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, FOREIGN KEY(`stream_id`) REFERENCES `streams`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "playlistUid",
|
||||||
|
"columnName": "playlist_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "streamUid",
|
||||||
|
"columnName": "stream_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "index",
|
||||||
|
"columnName": "join_index",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"playlist_id",
|
||||||
|
"join_index"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_playlist_stream_join_playlist_id_join_index",
|
||||||
|
"unique": true,
|
||||||
|
"columnNames": [
|
||||||
|
"playlist_id",
|
||||||
|
"join_index"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_playlist_stream_join_playlist_id_join_index` ON `${TABLE_NAME}` (`playlist_id`, `join_index`)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "index_playlist_stream_join_stream_id",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"stream_id"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_playlist_stream_join_stream_id` ON `${TABLE_NAME}` (`stream_id`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "playlists",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"playlist_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"table": "streams",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"stream_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "remote_playlists",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `service_id` INTEGER NOT NULL, `name` TEXT, `url` TEXT, `thumbnail_url` TEXT, `uploader` TEXT, `display_index` INTEGER NOT NULL, `stream_count` INTEGER)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "uid",
|
||||||
|
"columnName": "uid",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "serviceId",
|
||||||
|
"columnName": "service_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "name",
|
||||||
|
"columnName": "name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "url",
|
||||||
|
"columnName": "url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "thumbnailUrl",
|
||||||
|
"columnName": "thumbnail_url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "uploader",
|
||||||
|
"columnName": "uploader",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "displayIndex",
|
||||||
|
"columnName": "display_index",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "streamCount",
|
||||||
|
"columnName": "stream_count",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": true,
|
||||||
|
"columnNames": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_remote_playlists_service_id_url",
|
||||||
|
"unique": true,
|
||||||
|
"columnNames": [
|
||||||
|
"service_id",
|
||||||
|
"url"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_remote_playlists_service_id_url` ON `${TABLE_NAME}` (`service_id`, `url`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "feed",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`stream_id` INTEGER NOT NULL, `subscription_id` INTEGER NOT NULL, PRIMARY KEY(`stream_id`, `subscription_id`), FOREIGN KEY(`stream_id`) REFERENCES `streams`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, FOREIGN KEY(`subscription_id`) REFERENCES `subscriptions`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "streamId",
|
||||||
|
"columnName": "stream_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "subscriptionId",
|
||||||
|
"columnName": "subscription_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"stream_id",
|
||||||
|
"subscription_id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_feed_subscription_id",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"subscription_id"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_feed_subscription_id` ON `${TABLE_NAME}` (`subscription_id`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "streams",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"stream_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"table": "subscriptions",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"subscription_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "feed_group",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `icon_id` INTEGER NOT NULL, `sort_order` INTEGER NOT NULL)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "uid",
|
||||||
|
"columnName": "uid",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "name",
|
||||||
|
"columnName": "name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "icon",
|
||||||
|
"columnName": "icon_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "sortOrder",
|
||||||
|
"columnName": "sort_order",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": true,
|
||||||
|
"columnNames": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_feed_group_sort_order",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"sort_order"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_feed_group_sort_order` ON `${TABLE_NAME}` (`sort_order`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "feed_group_subscription_join",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`group_id` INTEGER NOT NULL, `subscription_id` INTEGER NOT NULL, PRIMARY KEY(`group_id`, `subscription_id`), FOREIGN KEY(`group_id`) REFERENCES `feed_group`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, FOREIGN KEY(`subscription_id`) REFERENCES `subscriptions`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "feedGroupId",
|
||||||
|
"columnName": "group_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "subscriptionId",
|
||||||
|
"columnName": "subscription_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"group_id",
|
||||||
|
"subscription_id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_feed_group_subscription_join_subscription_id",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"subscription_id"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_feed_group_subscription_join_subscription_id` ON `${TABLE_NAME}` (`subscription_id`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "feed_group",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"group_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"table": "subscriptions",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"subscription_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "feed_last_updated",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`subscription_id` INTEGER NOT NULL, `last_updated` INTEGER, PRIMARY KEY(`subscription_id`), FOREIGN KEY(`subscription_id`) REFERENCES `subscriptions`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "subscriptionId",
|
||||||
|
"columnName": "subscription_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastUpdated",
|
||||||
|
"columnName": "last_updated",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"subscription_id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "subscriptions",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"subscription_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"views": [],
|
||||||
|
"setupQueries": [
|
||||||
|
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||||
|
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '7591e8039faa74d8c0517dc867af9d3e')"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
|
@ -8,10 +8,14 @@ import androidx.test.core.app.ApplicationProvider
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
import androidx.test.platform.app.InstrumentationRegistry
|
import androidx.test.platform.app.InstrumentationRegistry
|
||||||
import org.junit.Assert.assertEquals
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Assert.assertNotEquals
|
||||||
import org.junit.Assert.assertNull
|
import org.junit.Assert.assertNull
|
||||||
import org.junit.Rule
|
import org.junit.Rule
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
|
import org.schabi.newpipe.database.playlist.model.PlaylistEntity
|
||||||
|
import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity
|
||||||
|
import org.schabi.newpipe.extractor.ServiceList
|
||||||
import org.schabi.newpipe.extractor.stream.StreamType
|
import org.schabi.newpipe.extractor.stream.StreamType
|
||||||
|
|
||||||
@RunWith(AndroidJUnit4::class)
|
@RunWith(AndroidJUnit4::class)
|
||||||
|
@ -20,13 +24,17 @@ class DatabaseMigrationTest {
|
||||||
private const val DEFAULT_SERVICE_ID = 0
|
private const val DEFAULT_SERVICE_ID = 0
|
||||||
private const val DEFAULT_URL = "https://www.youtube.com/watch?v=cDphUib5iG4"
|
private const val DEFAULT_URL = "https://www.youtube.com/watch?v=cDphUib5iG4"
|
||||||
private const val DEFAULT_TITLE = "Test Title"
|
private const val DEFAULT_TITLE = "Test Title"
|
||||||
|
private const val DEFAULT_NAME = "Test Name"
|
||||||
private val DEFAULT_TYPE = StreamType.VIDEO_STREAM
|
private val DEFAULT_TYPE = StreamType.VIDEO_STREAM
|
||||||
private const val DEFAULT_DURATION = 480L
|
private const val DEFAULT_DURATION = 480L
|
||||||
private const val DEFAULT_UPLOADER_NAME = "Uploader Test"
|
private const val DEFAULT_UPLOADER_NAME = "Uploader Test"
|
||||||
private const val DEFAULT_THUMBNAIL = "https://example.com/example.jpg"
|
private const val DEFAULT_THUMBNAIL = "https://example.com/example.jpg"
|
||||||
|
|
||||||
private const val DEFAULT_SECOND_SERVICE_ID = 0
|
private const val DEFAULT_SECOND_SERVICE_ID = 1
|
||||||
private const val DEFAULT_SECOND_URL = "https://www.youtube.com/watch?v=ncQU6iBn5Fc"
|
private const val DEFAULT_SECOND_URL = "https://www.youtube.com/watch?v=ncQU6iBn5Fc"
|
||||||
|
|
||||||
|
private const val DEFAULT_THIRD_SERVICE_ID = 2
|
||||||
|
private const val DEFAULT_THIRD_URL = "https://www.youtube.com/watch?v=dQw4w9WgXcQ"
|
||||||
}
|
}
|
||||||
|
|
||||||
@get:Rule
|
@get:Rule
|
||||||
|
@ -106,6 +114,20 @@ class DatabaseMigrationTest {
|
||||||
Migrations.MIGRATION_6_7
|
Migrations.MIGRATION_6_7
|
||||||
)
|
)
|
||||||
|
|
||||||
|
testHelper.runMigrationsAndValidate(
|
||||||
|
AppDatabase.DATABASE_NAME,
|
||||||
|
Migrations.DB_VER_8,
|
||||||
|
true,
|
||||||
|
Migrations.MIGRATION_7_8
|
||||||
|
)
|
||||||
|
|
||||||
|
testHelper.runMigrationsAndValidate(
|
||||||
|
AppDatabase.DATABASE_NAME,
|
||||||
|
Migrations.DB_VER_9,
|
||||||
|
true,
|
||||||
|
Migrations.MIGRATION_8_9
|
||||||
|
)
|
||||||
|
|
||||||
val migratedDatabaseV3 = getMigratedDatabase()
|
val migratedDatabaseV3 = getMigratedDatabase()
|
||||||
val listFromDB = migratedDatabaseV3.streamDAO().all.blockingFirst()
|
val listFromDB = migratedDatabaseV3.streamDAO().all.blockingFirst()
|
||||||
|
|
||||||
|
@ -140,6 +162,157 @@ class DatabaseMigrationTest {
|
||||||
assertNull(secondStreamFromMigratedDatabase.isUploadDateApproximation)
|
assertNull(secondStreamFromMigratedDatabase.isUploadDateApproximation)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun migrateDatabaseFrom7to8() {
|
||||||
|
val databaseInV7 = testHelper.createDatabase(AppDatabase.DATABASE_NAME, Migrations.DB_VER_7)
|
||||||
|
|
||||||
|
val defaultSearch1 = " abc "
|
||||||
|
val defaultSearch2 = " abc"
|
||||||
|
|
||||||
|
val serviceId = DEFAULT_SERVICE_ID // YouTube
|
||||||
|
// Use id different to YouTube because two searches with the same query
|
||||||
|
// but different service are considered not equal.
|
||||||
|
val otherServiceId = ServiceList.SoundCloud.serviceId
|
||||||
|
|
||||||
|
databaseInV7.run {
|
||||||
|
insert(
|
||||||
|
"search_history", SQLiteDatabase.CONFLICT_FAIL,
|
||||||
|
ContentValues().apply {
|
||||||
|
put("service_id", serviceId)
|
||||||
|
put("search", defaultSearch1)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
insert(
|
||||||
|
"search_history", SQLiteDatabase.CONFLICT_FAIL,
|
||||||
|
ContentValues().apply {
|
||||||
|
put("service_id", serviceId)
|
||||||
|
put("search", defaultSearch2)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
insert(
|
||||||
|
"search_history", SQLiteDatabase.CONFLICT_FAIL,
|
||||||
|
ContentValues().apply {
|
||||||
|
put("service_id", otherServiceId)
|
||||||
|
put("search", defaultSearch1)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
insert(
|
||||||
|
"search_history", SQLiteDatabase.CONFLICT_FAIL,
|
||||||
|
ContentValues().apply {
|
||||||
|
put("service_id", otherServiceId)
|
||||||
|
put("search", defaultSearch2)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
close()
|
||||||
|
}
|
||||||
|
|
||||||
|
testHelper.runMigrationsAndValidate(
|
||||||
|
AppDatabase.DATABASE_NAME, Migrations.DB_VER_8,
|
||||||
|
true, Migrations.MIGRATION_7_8
|
||||||
|
)
|
||||||
|
|
||||||
|
testHelper.runMigrationsAndValidate(
|
||||||
|
AppDatabase.DATABASE_NAME, Migrations.DB_VER_9,
|
||||||
|
true, Migrations.MIGRATION_8_9
|
||||||
|
)
|
||||||
|
|
||||||
|
val migratedDatabaseV8 = getMigratedDatabase()
|
||||||
|
val listFromDB = migratedDatabaseV8.searchHistoryDAO().all.blockingFirst()
|
||||||
|
|
||||||
|
assertEquals(2, listFromDB.size)
|
||||||
|
assertEquals("abc", listFromDB[0].search)
|
||||||
|
assertEquals("abc", listFromDB[1].search)
|
||||||
|
assertNotEquals(listFromDB[0].serviceId, listFromDB[1].serviceId)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun migrateDatabaseFrom8to9() {
|
||||||
|
val databaseInV8 = testHelper.createDatabase(AppDatabase.DATABASE_NAME, Migrations.DB_VER_8)
|
||||||
|
|
||||||
|
val localUid1: Long
|
||||||
|
val localUid2: Long
|
||||||
|
val remoteUid1: Long
|
||||||
|
val remoteUid2: Long
|
||||||
|
databaseInV8.run {
|
||||||
|
localUid1 = insert(
|
||||||
|
"playlists", SQLiteDatabase.CONFLICT_FAIL,
|
||||||
|
ContentValues().apply {
|
||||||
|
put("name", DEFAULT_NAME + "1")
|
||||||
|
put("is_thumbnail_permanent", false)
|
||||||
|
put("thumbnail_stream_id", -1)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
localUid2 = insert(
|
||||||
|
"playlists", SQLiteDatabase.CONFLICT_FAIL,
|
||||||
|
ContentValues().apply {
|
||||||
|
put("name", DEFAULT_NAME + "2")
|
||||||
|
put("is_thumbnail_permanent", false)
|
||||||
|
put("thumbnail_stream_id", -1)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
delete(
|
||||||
|
"playlists", "uid = ?",
|
||||||
|
Array(1) { localUid1 }
|
||||||
|
)
|
||||||
|
remoteUid1 = insert(
|
||||||
|
"remote_playlists", SQLiteDatabase.CONFLICT_FAIL,
|
||||||
|
ContentValues().apply {
|
||||||
|
put("service_id", DEFAULT_SERVICE_ID)
|
||||||
|
put("url", DEFAULT_URL)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
remoteUid2 = insert(
|
||||||
|
"remote_playlists", SQLiteDatabase.CONFLICT_FAIL,
|
||||||
|
ContentValues().apply {
|
||||||
|
put("service_id", DEFAULT_SECOND_SERVICE_ID)
|
||||||
|
put("url", DEFAULT_SECOND_URL)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
delete(
|
||||||
|
"remote_playlists", "uid = ?",
|
||||||
|
Array(1) { remoteUid2 }
|
||||||
|
)
|
||||||
|
close()
|
||||||
|
}
|
||||||
|
|
||||||
|
testHelper.runMigrationsAndValidate(
|
||||||
|
AppDatabase.DATABASE_NAME,
|
||||||
|
Migrations.DB_VER_9,
|
||||||
|
true,
|
||||||
|
Migrations.MIGRATION_8_9
|
||||||
|
)
|
||||||
|
|
||||||
|
val migratedDatabaseV9 = getMigratedDatabase()
|
||||||
|
var localListFromDB = migratedDatabaseV9.playlistDAO().all.blockingFirst()
|
||||||
|
var remoteListFromDB = migratedDatabaseV9.playlistRemoteDAO().all.blockingFirst()
|
||||||
|
|
||||||
|
assertEquals(1, localListFromDB.size)
|
||||||
|
assertEquals(localUid2, localListFromDB[0].uid)
|
||||||
|
assertEquals(-1, localListFromDB[0].displayIndex)
|
||||||
|
assertEquals(1, remoteListFromDB.size)
|
||||||
|
assertEquals(remoteUid1, remoteListFromDB[0].uid)
|
||||||
|
assertEquals(-1, remoteListFromDB[0].displayIndex)
|
||||||
|
|
||||||
|
val localUid3 = migratedDatabaseV9.playlistDAO().insert(
|
||||||
|
PlaylistEntity(DEFAULT_NAME + "3", false, -1, -1)
|
||||||
|
)
|
||||||
|
val remoteUid3 = migratedDatabaseV9.playlistRemoteDAO().insert(
|
||||||
|
PlaylistRemoteEntity(
|
||||||
|
DEFAULT_THIRD_SERVICE_ID, DEFAULT_NAME, DEFAULT_THIRD_URL,
|
||||||
|
DEFAULT_THUMBNAIL, DEFAULT_UPLOADER_NAME, -1, 10
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
localListFromDB = migratedDatabaseV9.playlistDAO().all.blockingFirst()
|
||||||
|
remoteListFromDB = migratedDatabaseV9.playlistRemoteDAO().all.blockingFirst()
|
||||||
|
assertEquals(2, localListFromDB.size)
|
||||||
|
assertEquals(localUid3, localListFromDB[1].uid)
|
||||||
|
assertEquals(-1, localListFromDB[1].displayIndex)
|
||||||
|
assertEquals(2, remoteListFromDB.size)
|
||||||
|
assertEquals(remoteUid3, remoteListFromDB[1].uid)
|
||||||
|
assertEquals(-1, remoteListFromDB[1].displayIndex)
|
||||||
|
}
|
||||||
|
|
||||||
private fun getMigratedDatabase(): AppDatabase {
|
private fun getMigratedDatabase(): AppDatabase {
|
||||||
val database: AppDatabase = Room.databaseBuilder(
|
val database: AppDatabase = Room.databaseBuilder(
|
||||||
ApplicationProvider.getApplicationContext(),
|
ApplicationProvider.getApplicationContext(),
|
||||||
|
|
|
@ -85,7 +85,13 @@ class FeedDAOTest {
|
||||||
|
|
||||||
private fun assertEqual(streams: List<StreamWithState>?, allowedStreams: List<StreamEntity>) {
|
private fun assertEqual(streams: List<StreamWithState>?, allowedStreams: List<StreamEntity>) {
|
||||||
assertNotNull(streams)
|
assertNotNull(streams)
|
||||||
assertEquals(allowedStreams, streams!!.stream().map { it.stream }.toList().sortedBy { it.uid })
|
assertEquals(
|
||||||
|
allowedStreams,
|
||||||
|
streams!!
|
||||||
|
.map { it.stream }
|
||||||
|
.sortedBy { it.uid }
|
||||||
|
.toList()
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setupUnlinkDelete(time: String) {
|
private fun setupUnlinkDelete(time: String) {
|
||||||
|
|
|
@ -25,6 +25,7 @@ import android.view.ViewGroup;
|
||||||
import androidx.annotation.IntDef;
|
import androidx.annotation.IntDef;
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.core.os.BundleCompat;
|
||||||
import androidx.lifecycle.Lifecycle;
|
import androidx.lifecycle.Lifecycle;
|
||||||
import androidx.viewpager.widget.PagerAdapter;
|
import androidx.viewpager.widget.PagerAdapter;
|
||||||
|
|
||||||
|
@ -284,7 +285,7 @@ public abstract class FragmentStatePagerAdapterMenuWorkaround extends PagerAdapt
|
||||||
Bundle state = null;
|
Bundle state = null;
|
||||||
if (!mSavedState.isEmpty()) {
|
if (!mSavedState.isEmpty()) {
|
||||||
state = new Bundle();
|
state = new Bundle();
|
||||||
state.putParcelableArray("states", mSavedState.toArray(new Fragment.SavedState[0]));
|
state.putParcelableArrayList("states", mSavedState);
|
||||||
}
|
}
|
||||||
for (int i = 0; i < mFragments.size(); i++) {
|
for (int i = 0; i < mFragments.size(); i++) {
|
||||||
final Fragment f = mFragments.get(i);
|
final Fragment f = mFragments.get(i);
|
||||||
|
@ -311,13 +312,12 @@ public abstract class FragmentStatePagerAdapterMenuWorkaround extends PagerAdapt
|
||||||
if (state != null) {
|
if (state != null) {
|
||||||
final Bundle bundle = (Bundle) state;
|
final Bundle bundle = (Bundle) state;
|
||||||
bundle.setClassLoader(loader);
|
bundle.setClassLoader(loader);
|
||||||
final Parcelable[] fss = bundle.getParcelableArray("states");
|
final var states = BundleCompat.getParcelableArrayList(bundle, "states",
|
||||||
|
Fragment.SavedState.class);
|
||||||
mSavedState.clear();
|
mSavedState.clear();
|
||||||
mFragments.clear();
|
mFragments.clear();
|
||||||
if (fss != null) {
|
if (states != null) {
|
||||||
for (final Parcelable parcelable : fss) {
|
mSavedState.addAll(states);
|
||||||
mSavedState.add((Fragment.SavedState) parcelable);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
final Iterable<String> keys = bundle.keySet();
|
final Iterable<String> keys = bundle.keySet();
|
||||||
for (final String key : keys) {
|
for (final String key : keys) {
|
||||||
|
|
|
@ -60,6 +60,8 @@ import io.reactivex.rxjava3.plugins.RxJavaPlugins;
|
||||||
public class App extends Application {
|
public class App extends Application {
|
||||||
public static final String PACKAGE_NAME = BuildConfig.APPLICATION_ID;
|
public static final String PACKAGE_NAME = BuildConfig.APPLICATION_ID;
|
||||||
private static final String TAG = App.class.toString();
|
private static final String TAG = App.class.toString();
|
||||||
|
|
||||||
|
private boolean isFirstRun = false;
|
||||||
private static App app;
|
private static App app;
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
|
@ -85,7 +87,13 @@ public class App extends Application {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize settings first because others inits can use its values
|
// 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);
|
NewPipeSettings.initSettings(this);
|
||||||
|
|
||||||
NewPipe.init(getDownloader(),
|
NewPipe.init(getDownloader(),
|
||||||
|
@ -255,4 +263,7 @@ public class App extends Application {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean isFirstRun() {
|
||||||
|
return isFirstRun;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -44,6 +44,7 @@ import android.widget.FrameLayout;
|
||||||
import android.widget.Spinner;
|
import android.widget.Spinner;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
import androidx.appcompat.app.ActionBar;
|
import androidx.appcompat.app.ActionBar;
|
||||||
import androidx.appcompat.app.ActionBarDrawerToggle;
|
import androidx.appcompat.app.ActionBarDrawerToggle;
|
||||||
import androidx.appcompat.app.AppCompatActivity;
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
|
@ -51,6 +52,7 @@ import androidx.core.app.ActivityCompat;
|
||||||
import androidx.core.view.GravityCompat;
|
import androidx.core.view.GravityCompat;
|
||||||
import androidx.drawerlayout.widget.DrawerLayout;
|
import androidx.drawerlayout.widget.DrawerLayout;
|
||||||
import androidx.fragment.app.Fragment;
|
import androidx.fragment.app.Fragment;
|
||||||
|
import androidx.fragment.app.FragmentContainerView;
|
||||||
import androidx.fragment.app.FragmentManager;
|
import androidx.fragment.app.FragmentManager;
|
||||||
import androidx.preference.PreferenceManager;
|
import androidx.preference.PreferenceManager;
|
||||||
|
|
||||||
|
@ -64,17 +66,20 @@ import org.schabi.newpipe.databinding.ToolbarLayoutBinding;
|
||||||
import org.schabi.newpipe.error.ErrorUtil;
|
import org.schabi.newpipe.error.ErrorUtil;
|
||||||
import org.schabi.newpipe.extractor.NewPipe;
|
import org.schabi.newpipe.extractor.NewPipe;
|
||||||
import org.schabi.newpipe.extractor.StreamingService;
|
import org.schabi.newpipe.extractor.StreamingService;
|
||||||
|
import org.schabi.newpipe.extractor.comments.CommentsInfoItem;
|
||||||
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||||
import org.schabi.newpipe.extractor.services.peertube.PeertubeInstance;
|
import org.schabi.newpipe.extractor.services.peertube.PeertubeInstance;
|
||||||
import org.schabi.newpipe.fragments.BackPressable;
|
import org.schabi.newpipe.fragments.BackPressable;
|
||||||
import org.schabi.newpipe.fragments.MainFragment;
|
import org.schabi.newpipe.fragments.MainFragment;
|
||||||
import org.schabi.newpipe.fragments.detail.VideoDetailFragment;
|
import org.schabi.newpipe.fragments.detail.VideoDetailFragment;
|
||||||
|
import org.schabi.newpipe.fragments.list.comments.CommentRepliesFragment;
|
||||||
import org.schabi.newpipe.fragments.list.search.SearchFragment;
|
import org.schabi.newpipe.fragments.list.search.SearchFragment;
|
||||||
import org.schabi.newpipe.local.feed.notifications.NotificationWorker;
|
import org.schabi.newpipe.local.feed.notifications.NotificationWorker;
|
||||||
import org.schabi.newpipe.player.Player;
|
import org.schabi.newpipe.player.Player;
|
||||||
import org.schabi.newpipe.player.event.OnKeyDownListener;
|
import org.schabi.newpipe.player.event.OnKeyDownListener;
|
||||||
import org.schabi.newpipe.player.helper.PlayerHolder;
|
import org.schabi.newpipe.player.helper.PlayerHolder;
|
||||||
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
||||||
|
import org.schabi.newpipe.settings.UpdateSettingsFragment;
|
||||||
import org.schabi.newpipe.util.Constants;
|
import org.schabi.newpipe.util.Constants;
|
||||||
import org.schabi.newpipe.util.DeviceUtils;
|
import org.schabi.newpipe.util.DeviceUtils;
|
||||||
import org.schabi.newpipe.util.KioskTranslator;
|
import org.schabi.newpipe.util.KioskTranslator;
|
||||||
|
@ -82,6 +87,7 @@ import org.schabi.newpipe.util.Localization;
|
||||||
import org.schabi.newpipe.util.NavigationHelper;
|
import org.schabi.newpipe.util.NavigationHelper;
|
||||||
import org.schabi.newpipe.util.PeertubeHelper;
|
import org.schabi.newpipe.util.PeertubeHelper;
|
||||||
import org.schabi.newpipe.util.PermissionHelper;
|
import org.schabi.newpipe.util.PermissionHelper;
|
||||||
|
import org.schabi.newpipe.util.ReleaseVersionUtil;
|
||||||
import org.schabi.newpipe.util.SerializedCache;
|
import org.schabi.newpipe.util.SerializedCache;
|
||||||
import org.schabi.newpipe.util.ServiceHelper;
|
import org.schabi.newpipe.util.ServiceHelper;
|
||||||
import org.schabi.newpipe.util.StateSaver;
|
import org.schabi.newpipe.util.StateSaver;
|
||||||
|
@ -163,6 +169,11 @@ public class MainActivity extends AppCompatActivity {
|
||||||
// if this is enabled by the user.
|
// if this is enabled by the user.
|
||||||
NotificationWorker.initialize(this);
|
NotificationWorker.initialize(this);
|
||||||
}
|
}
|
||||||
|
if (!UpdateSettingsFragment.wasUserAskedForConsent(this)
|
||||||
|
&& !App.getApp().isFirstRun()
|
||||||
|
&& ReleaseVersionUtil.INSTANCE.isReleaseApk()) {
|
||||||
|
UpdateSettingsFragment.askForConsentToUpdateChecks(this);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -172,7 +183,8 @@ public class MainActivity extends AppCompatActivity {
|
||||||
final App app = App.getApp();
|
final App app = App.getApp();
|
||||||
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(app);
|
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(app);
|
||||||
|
|
||||||
if (prefs.getBoolean(app.getString(R.string.update_app_key), true)) {
|
if (prefs.getBoolean(app.getString(R.string.update_app_key), false)
|
||||||
|
&& prefs.getBoolean(app.getString(R.string.update_check_consent_key), false)) {
|
||||||
// Start the worker which is checking all conditions
|
// Start the worker which is checking all conditions
|
||||||
// and eventually searching for a new version.
|
// and eventually searching for a new version.
|
||||||
NewVersionWorker.enqueueNewVersionCheckingWork(app, false);
|
NewVersionWorker.enqueueNewVersionCheckingWork(app, false);
|
||||||
|
@ -546,14 +558,21 @@ public class MainActivity extends AppCompatActivity {
|
||||||
// interacts with a fragment inside fragment_holder so all back presses should be
|
// interacts with a fragment inside fragment_holder so all back presses should be
|
||||||
// handled by it
|
// handled by it
|
||||||
if (bottomSheetHiddenOrCollapsed()) {
|
if (bottomSheetHiddenOrCollapsed()) {
|
||||||
final Fragment fragment = getSupportFragmentManager()
|
final FragmentManager fm = getSupportFragmentManager();
|
||||||
.findFragmentById(R.id.fragment_holder);
|
final Fragment fragment = fm.findFragmentById(R.id.fragment_holder);
|
||||||
// If current fragment implements BackPressable (i.e. can/wanna handle back press)
|
// If current fragment implements BackPressable (i.e. can/wanna handle back press)
|
||||||
// delegate the back press to it
|
// delegate the back press to it
|
||||||
if (fragment instanceof BackPressable) {
|
if (fragment instanceof BackPressable) {
|
||||||
if (((BackPressable) fragment).onBackPressed()) {
|
if (((BackPressable) fragment).onBackPressed()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
} else if (fragment instanceof CommentRepliesFragment) {
|
||||||
|
// expand DetailsFragment if CommentRepliesFragment was opened
|
||||||
|
// to show the top level comments again
|
||||||
|
// Expand DetailsFragment if CommentRepliesFragment was opened
|
||||||
|
// and no other CommentRepliesFragments are on top of the back stack
|
||||||
|
// to show the top level comments again.
|
||||||
|
openDetailFragmentFromCommentReplies(fm, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
|
@ -629,10 +648,17 @@ public class MainActivity extends AppCompatActivity {
|
||||||
* </pre>
|
* </pre>
|
||||||
*/
|
*/
|
||||||
private void onHomeButtonPressed() {
|
private void onHomeButtonPressed() {
|
||||||
// If search fragment wasn't found in the backstack...
|
final FragmentManager fm = getSupportFragmentManager();
|
||||||
if (!NavigationHelper.tryGotoSearchFragment(getSupportFragmentManager())) {
|
final Fragment fragment = fm.findFragmentById(R.id.fragment_holder);
|
||||||
// ...go to the main fragment
|
|
||||||
NavigationHelper.gotoMainFragment(getSupportFragmentManager());
|
if (fragment instanceof CommentRepliesFragment) {
|
||||||
|
// Expand DetailsFragment if CommentRepliesFragment was opened
|
||||||
|
// and no other CommentRepliesFragments are on top of the back stack
|
||||||
|
// to show the top level comments again.
|
||||||
|
openDetailFragmentFromCommentReplies(fm, true);
|
||||||
|
} else if (!NavigationHelper.tryGotoSearchFragment(fm)) {
|
||||||
|
// If search fragment wasn't found in the backstack go to the main fragment
|
||||||
|
NavigationHelper.gotoMainFragment(fm);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -828,6 +854,68 @@ public class MainActivity extends AppCompatActivity {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void openDetailFragmentFromCommentReplies(
|
||||||
|
@NonNull final FragmentManager fm,
|
||||||
|
final boolean popBackStack
|
||||||
|
) {
|
||||||
|
// obtain the name of the fragment under the replies fragment that's going to be popped
|
||||||
|
@Nullable final String fragmentUnderEntryName;
|
||||||
|
if (fm.getBackStackEntryCount() < 2) {
|
||||||
|
fragmentUnderEntryName = null;
|
||||||
|
} else {
|
||||||
|
fragmentUnderEntryName = fm.getBackStackEntryAt(fm.getBackStackEntryCount() - 2)
|
||||||
|
.getName();
|
||||||
|
}
|
||||||
|
|
||||||
|
// the root comment is the comment for which the user opened the replies page
|
||||||
|
@Nullable final CommentRepliesFragment repliesFragment =
|
||||||
|
(CommentRepliesFragment) fm.findFragmentByTag(CommentRepliesFragment.TAG);
|
||||||
|
@Nullable final CommentsInfoItem rootComment =
|
||||||
|
repliesFragment == null ? null : repliesFragment.getCommentsInfoItem();
|
||||||
|
|
||||||
|
// sometimes this function pops the backstack, other times it's handled by the system
|
||||||
|
if (popBackStack) {
|
||||||
|
fm.popBackStackImmediate();
|
||||||
|
}
|
||||||
|
|
||||||
|
// only expand the bottom sheet back if there are no more nested comment replies fragments
|
||||||
|
// stacked under the one that is currently being popped
|
||||||
|
if (CommentRepliesFragment.TAG.equals(fragmentUnderEntryName)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final BottomSheetBehavior<FragmentContainerView> behavior = BottomSheetBehavior
|
||||||
|
.from(mainBinding.fragmentPlayerHolder);
|
||||||
|
// do not return to the comment if the details fragment was closed
|
||||||
|
if (behavior.getState() == BottomSheetBehavior.STATE_HIDDEN) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// scroll to the root comment once the bottom sheet expansion animation is finished
|
||||||
|
behavior.addBottomSheetCallback(new BottomSheetBehavior.BottomSheetCallback() {
|
||||||
|
@Override
|
||||||
|
public void onStateChanged(@NonNull final View bottomSheet,
|
||||||
|
final int newState) {
|
||||||
|
if (newState == BottomSheetBehavior.STATE_EXPANDED) {
|
||||||
|
final Fragment detailFragment = fm.findFragmentById(
|
||||||
|
R.id.fragment_player_holder);
|
||||||
|
if (detailFragment instanceof VideoDetailFragment && rootComment != null) {
|
||||||
|
// should always be the case
|
||||||
|
((VideoDetailFragment) detailFragment).scrollToComment(rootComment);
|
||||||
|
}
|
||||||
|
behavior.removeBottomSheetCallback(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onSlide(@NonNull final View bottomSheet, final float slideOffset) {
|
||||||
|
// not needed, listener is removed once the sheet is expanded
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
behavior.setState(BottomSheetBehavior.STATE_EXPANDED);
|
||||||
|
}
|
||||||
|
|
||||||
private boolean bottomSheetHiddenOrCollapsed() {
|
private boolean bottomSheetHiddenOrCollapsed() {
|
||||||
final BottomSheetBehavior<FrameLayout> bottomSheetBehavior =
|
final BottomSheetBehavior<FrameLayout> bottomSheetBehavior =
|
||||||
BottomSheetBehavior.from(mainBinding.fragmentPlayerHolder);
|
BottomSheetBehavior.from(mainBinding.fragmentPlayerHolder);
|
||||||
|
|
|
@ -7,6 +7,8 @@ import static org.schabi.newpipe.database.Migrations.MIGRATION_3_4;
|
||||||
import static org.schabi.newpipe.database.Migrations.MIGRATION_4_5;
|
import static org.schabi.newpipe.database.Migrations.MIGRATION_4_5;
|
||||||
import static org.schabi.newpipe.database.Migrations.MIGRATION_5_6;
|
import static org.schabi.newpipe.database.Migrations.MIGRATION_5_6;
|
||||||
import static org.schabi.newpipe.database.Migrations.MIGRATION_6_7;
|
import static org.schabi.newpipe.database.Migrations.MIGRATION_6_7;
|
||||||
|
import static org.schabi.newpipe.database.Migrations.MIGRATION_7_8;
|
||||||
|
import static org.schabi.newpipe.database.Migrations.MIGRATION_8_9;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.database.Cursor;
|
import android.database.Cursor;
|
||||||
|
@ -27,7 +29,7 @@ public final class NewPipeDatabase {
|
||||||
return Room
|
return Room
|
||||||
.databaseBuilder(context.getApplicationContext(), AppDatabase.class, DATABASE_NAME)
|
.databaseBuilder(context.getApplicationContext(), AppDatabase.class, DATABASE_NAME)
|
||||||
.addMigrations(MIGRATION_1_2, MIGRATION_2_3, MIGRATION_3_4, MIGRATION_4_5,
|
.addMigrations(MIGRATION_1_2, MIGRATION_2_3, MIGRATION_3_4, MIGRATION_4_5,
|
||||||
MIGRATION_5_6, MIGRATION_6_7)
|
MIGRATION_5_6, MIGRATION_6_7, MIGRATION_7_8, MIGRATION_8_9)
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -20,9 +20,7 @@ import com.grack.nanojson.JsonParser
|
||||||
import com.grack.nanojson.JsonParserException
|
import com.grack.nanojson.JsonParserException
|
||||||
import org.schabi.newpipe.extractor.downloader.Response
|
import org.schabi.newpipe.extractor.downloader.Response
|
||||||
import org.schabi.newpipe.extractor.exceptions.ReCaptchaException
|
import org.schabi.newpipe.extractor.exceptions.ReCaptchaException
|
||||||
import org.schabi.newpipe.util.ReleaseVersionUtil.coerceUpdateCheckExpiry
|
import org.schabi.newpipe.util.ReleaseVersionUtil
|
||||||
import org.schabi.newpipe.util.ReleaseVersionUtil.isLastUpdateCheckExpired
|
|
||||||
import org.schabi.newpipe.util.ReleaseVersionUtil.isReleaseApk
|
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
|
|
||||||
class NewVersionWorker(
|
class NewVersionWorker(
|
||||||
|
@ -84,7 +82,7 @@ class NewVersionWorker(
|
||||||
@Throws(IOException::class, ReCaptchaException::class)
|
@Throws(IOException::class, ReCaptchaException::class)
|
||||||
private fun checkNewVersion() {
|
private fun checkNewVersion() {
|
||||||
// Check if the current apk is a github one or not.
|
// Check if the current apk is a github one or not.
|
||||||
if (!isReleaseApk()) {
|
if (!ReleaseVersionUtil.isReleaseApk) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -93,7 +91,7 @@ class NewVersionWorker(
|
||||||
// Check if the last request has happened a certain time ago
|
// Check if the last request has happened a certain time ago
|
||||||
// to reduce the number of API requests.
|
// to reduce the number of API requests.
|
||||||
val expiry = prefs.getLong(applicationContext.getString(R.string.update_expiry_key), 0)
|
val expiry = prefs.getLong(applicationContext.getString(R.string.update_expiry_key), 0)
|
||||||
if (!isLastUpdateCheckExpired(expiry)) {
|
if (!ReleaseVersionUtil.isLastUpdateCheckExpired(expiry)) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -108,7 +106,7 @@ class NewVersionWorker(
|
||||||
try {
|
try {
|
||||||
// Store a timestamp which needs to be exceeded,
|
// Store a timestamp which needs to be exceeded,
|
||||||
// before a new request to the API is made.
|
// before a new request to the API is made.
|
||||||
val newExpiry = coerceUpdateCheckExpiry(response.getHeader("expires"))
|
val newExpiry = ReleaseVersionUtil.coerceUpdateCheckExpiry(response.getHeader("expires"))
|
||||||
prefs.edit {
|
prefs.edit {
|
||||||
putLong(applicationContext.getString(R.string.update_expiry_key), newExpiry)
|
putLong(applicationContext.getString(R.string.update_expiry_key), newExpiry)
|
||||||
}
|
}
|
||||||
|
|
|
@ -116,7 +116,7 @@ class AboutActivity : AppCompatActivity() {
|
||||||
/**
|
/**
|
||||||
* List of all software components.
|
* List of all software components.
|
||||||
*/
|
*/
|
||||||
private val SOFTWARE_COMPONENTS = arrayOf(
|
private val SOFTWARE_COMPONENTS = arrayListOf(
|
||||||
SoftwareComponent(
|
SoftwareComponent(
|
||||||
"ACRA", "2013", "Kevin Gaudin",
|
"ACRA", "2013", "Kevin Gaudin",
|
||||||
"https://github.com/ACRA/acra", StandardLicenses.APACHE2
|
"https://github.com/ACRA/acra", StandardLicenses.APACHE2
|
||||||
|
|
|
@ -18,6 +18,7 @@ import org.schabi.newpipe.BuildConfig
|
||||||
import org.schabi.newpipe.R
|
import org.schabi.newpipe.R
|
||||||
import org.schabi.newpipe.databinding.FragmentLicensesBinding
|
import org.schabi.newpipe.databinding.FragmentLicensesBinding
|
||||||
import org.schabi.newpipe.databinding.ItemSoftwareComponentBinding
|
import org.schabi.newpipe.databinding.ItemSoftwareComponentBinding
|
||||||
|
import org.schabi.newpipe.ktx.parcelableArrayList
|
||||||
import org.schabi.newpipe.util.Localization
|
import org.schabi.newpipe.util.Localization
|
||||||
import org.schabi.newpipe.util.external_communication.ShareUtils
|
import org.schabi.newpipe.util.external_communication.ShareUtils
|
||||||
|
|
||||||
|
@ -25,16 +26,15 @@ import org.schabi.newpipe.util.external_communication.ShareUtils
|
||||||
* Fragment containing the software licenses.
|
* Fragment containing the software licenses.
|
||||||
*/
|
*/
|
||||||
class LicenseFragment : Fragment() {
|
class LicenseFragment : Fragment() {
|
||||||
private lateinit var softwareComponents: Array<SoftwareComponent>
|
private lateinit var softwareComponents: List<SoftwareComponent>
|
||||||
private var activeSoftwareComponent: SoftwareComponent? = null
|
private var activeSoftwareComponent: SoftwareComponent? = null
|
||||||
private val compositeDisposable = CompositeDisposable()
|
private val compositeDisposable = CompositeDisposable()
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
softwareComponents = arguments?.getParcelableArray(ARG_COMPONENTS) as Array<SoftwareComponent>
|
softwareComponents = arguments?.parcelableArrayList<SoftwareComponent>(ARG_COMPONENTS)!!
|
||||||
|
.sortedBy { it.name } // Sort components by name
|
||||||
activeSoftwareComponent = savedInstanceState?.getSerializable(SOFTWARE_COMPONENT_KEY) as? SoftwareComponent
|
activeSoftwareComponent = savedInstanceState?.getSerializable(SOFTWARE_COMPONENT_KEY) as? SoftwareComponent
|
||||||
// Sort components by name
|
|
||||||
softwareComponents.sortBy { it.name }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
|
@ -130,7 +130,8 @@ class LicenseFragment : Fragment() {
|
||||||
StandardLicenses.GPL3,
|
StandardLicenses.GPL3,
|
||||||
BuildConfig.VERSION_NAME
|
BuildConfig.VERSION_NAME
|
||||||
)
|
)
|
||||||
fun newInstance(softwareComponents: Array<SoftwareComponent>): LicenseFragment {
|
|
||||||
|
fun newInstance(softwareComponents: ArrayList<SoftwareComponent>): LicenseFragment {
|
||||||
val fragment = LicenseFragment()
|
val fragment = LicenseFragment()
|
||||||
fragment.arguments = bundleOf(ARG_COMPONENTS to softwareComponents)
|
fragment.arguments = bundleOf(ARG_COMPONENTS to softwareComponents)
|
||||||
return fragment
|
return fragment
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
package org.schabi.newpipe.database;
|
package org.schabi.newpipe.database;
|
||||||
|
|
||||||
import static org.schabi.newpipe.database.Migrations.DB_VER_7;
|
import static org.schabi.newpipe.database.Migrations.DB_VER_9;
|
||||||
|
|
||||||
import androidx.room.Database;
|
import androidx.room.Database;
|
||||||
import androidx.room.RoomDatabase;
|
import androidx.room.RoomDatabase;
|
||||||
|
@ -38,7 +38,7 @@ import org.schabi.newpipe.database.subscription.SubscriptionEntity;
|
||||||
FeedEntity.class, FeedGroupEntity.class, FeedGroupSubscriptionEntity.class,
|
FeedEntity.class, FeedGroupEntity.class, FeedGroupSubscriptionEntity.class,
|
||||||
FeedLastUpdatedEntity.class
|
FeedLastUpdatedEntity.class
|
||||||
},
|
},
|
||||||
version = DB_VER_7
|
version = DB_VER_9
|
||||||
)
|
)
|
||||||
public abstract class AppDatabase extends RoomDatabase {
|
public abstract class AppDatabase extends RoomDatabase {
|
||||||
public static final String DATABASE_NAME = "newpipe.db";
|
public static final String DATABASE_NAME = "newpipe.db";
|
||||||
|
|
|
@ -7,7 +7,7 @@ import java.time.Instant
|
||||||
import java.time.OffsetDateTime
|
import java.time.OffsetDateTime
|
||||||
import java.time.ZoneOffset
|
import java.time.ZoneOffset
|
||||||
|
|
||||||
object Converters {
|
class Converters {
|
||||||
/**
|
/**
|
||||||
* Convert a long value to a [OffsetDateTime].
|
* Convert a long value to a [OffsetDateTime].
|
||||||
*
|
*
|
||||||
|
@ -47,6 +47,6 @@ object Converters {
|
||||||
|
|
||||||
@TypeConverter
|
@TypeConverter
|
||||||
fun feedGroupIconOf(id: Int): FeedGroupIcon {
|
fun feedGroupIconOf(id: Int): FeedGroupIcon {
|
||||||
return FeedGroupIcon.values().first { it.id == id }
|
return FeedGroupIcon.entries.first { it.id == id }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,6 +25,8 @@ public final class Migrations {
|
||||||
public static final int DB_VER_5 = 5;
|
public static final int DB_VER_5 = 5;
|
||||||
public static final int DB_VER_6 = 6;
|
public static final int DB_VER_6 = 6;
|
||||||
public static final int DB_VER_7 = 7;
|
public static final int DB_VER_7 = 7;
|
||||||
|
public static final int DB_VER_8 = 8;
|
||||||
|
public static final int DB_VER_9 = 9;
|
||||||
|
|
||||||
private static final String TAG = Migrations.class.getName();
|
private static final String TAG = Migrations.class.getName();
|
||||||
public static final boolean DEBUG = MainActivity.DEBUG;
|
public static final boolean DEBUG = MainActivity.DEBUG;
|
||||||
|
@ -186,7 +188,7 @@ public final class Migrations {
|
||||||
@Override
|
@Override
|
||||||
public void migrate(@NonNull final SupportSQLiteDatabase database) {
|
public void migrate(@NonNull final SupportSQLiteDatabase database) {
|
||||||
database.execSQL("ALTER TABLE `subscriptions` ADD COLUMN `notification_mode` "
|
database.execSQL("ALTER TABLE `subscriptions` ADD COLUMN `notification_mode` "
|
||||||
+ "INTEGER NOT NULL DEFAULT 0");
|
+ "INTEGER NOT NULL DEFAULT 0");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -235,6 +237,71 @@ public final class Migrations {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
public static final Migration MIGRATION_7_8 = new Migration(DB_VER_7, DB_VER_8) {
|
||||||
|
@Override
|
||||||
|
public void migrate(@NonNull final SupportSQLiteDatabase database) {
|
||||||
|
database.execSQL("DELETE FROM search_history WHERE id NOT IN (SELECT id FROM (SELECT "
|
||||||
|
+ "MIN(id) as id FROM search_history GROUP BY trim(search), service_id ) tmp)");
|
||||||
|
database.execSQL("UPDATE search_history SET search = trim(search)");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
public static final Migration MIGRATION_8_9 = new Migration(DB_VER_8, DB_VER_9) {
|
||||||
|
@Override
|
||||||
|
public void migrate(@NonNull final SupportSQLiteDatabase database) {
|
||||||
|
try {
|
||||||
|
database.beginTransaction();
|
||||||
|
|
||||||
|
// Update playlists.
|
||||||
|
// Create a temp table to initialize display_index.
|
||||||
|
database.execSQL("CREATE TABLE `playlists_tmp` "
|
||||||
|
+ "(`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, "
|
||||||
|
+ "`name` TEXT, `is_thumbnail_permanent` INTEGER NOT NULL, "
|
||||||
|
+ "`thumbnail_stream_id` INTEGER NOT NULL, "
|
||||||
|
+ "`display_index` INTEGER NOT NULL)");
|
||||||
|
database.execSQL("INSERT INTO `playlists_tmp` "
|
||||||
|
+ "(`uid`, `name`, `is_thumbnail_permanent`, `thumbnail_stream_id`, "
|
||||||
|
+ "`display_index`) "
|
||||||
|
+ "SELECT `uid`, `name`, `is_thumbnail_permanent`, `thumbnail_stream_id`, "
|
||||||
|
+ "-1 "
|
||||||
|
+ "FROM `playlists`");
|
||||||
|
|
||||||
|
// Replace the old table, note that this also removes the index on the name which
|
||||||
|
// we don't need anymore.
|
||||||
|
database.execSQL("DROP TABLE `playlists`");
|
||||||
|
database.execSQL("ALTER TABLE `playlists_tmp` RENAME TO `playlists`");
|
||||||
|
|
||||||
|
|
||||||
|
// Update remote_playlists.
|
||||||
|
// Create a temp table to initialize display_index.
|
||||||
|
database.execSQL("CREATE TABLE `remote_playlists_tmp` "
|
||||||
|
+ "(`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, "
|
||||||
|
+ "`service_id` INTEGER NOT NULL, `name` TEXT, `url` TEXT, "
|
||||||
|
+ "`thumbnail_url` TEXT, `uploader` TEXT, "
|
||||||
|
+ "`display_index` INTEGER NOT NULL,"
|
||||||
|
+ "`stream_count` INTEGER)");
|
||||||
|
database.execSQL("INSERT INTO `remote_playlists_tmp` (`uid`, `service_id`, "
|
||||||
|
+ "`name`, `url`, `thumbnail_url`, `uploader`, `display_index`, "
|
||||||
|
+ "`stream_count`)"
|
||||||
|
+ "SELECT `uid`, `service_id`, `name`, `url`, `thumbnail_url`, `uploader`, "
|
||||||
|
+ "-1, `stream_count` FROM `remote_playlists`");
|
||||||
|
|
||||||
|
// Replace the old table, note that this also removes the index on the name which
|
||||||
|
// we don't need anymore.
|
||||||
|
database.execSQL("DROP TABLE `remote_playlists`");
|
||||||
|
database.execSQL("ALTER TABLE `remote_playlists_tmp` RENAME TO `remote_playlists`");
|
||||||
|
|
||||||
|
// Create index on the new table.
|
||||||
|
database.execSQL("CREATE UNIQUE INDEX `index_remote_playlists_service_id_url` "
|
||||||
|
+ "ON `remote_playlists` (`service_id`, `url`)");
|
||||||
|
|
||||||
|
database.setTransactionSuccessful();
|
||||||
|
} finally {
|
||||||
|
database.endTransaction();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
private Migrations() {
|
private Migrations() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,12 +13,17 @@ public class PlaylistDuplicatesEntry extends PlaylistMetadataEntry {
|
||||||
@ColumnInfo(name = PLAYLIST_TIMES_STREAM_IS_CONTAINED)
|
@ColumnInfo(name = PLAYLIST_TIMES_STREAM_IS_CONTAINED)
|
||||||
public final long timesStreamIsContained;
|
public final long timesStreamIsContained;
|
||||||
|
|
||||||
|
@SuppressWarnings("checkstyle:ParameterNumber")
|
||||||
public PlaylistDuplicatesEntry(final long uid,
|
public PlaylistDuplicatesEntry(final long uid,
|
||||||
final String name,
|
final String name,
|
||||||
final String thumbnailUrl,
|
final String thumbnailUrl,
|
||||||
|
final boolean isThumbnailPermanent,
|
||||||
|
final long thumbnailStreamId,
|
||||||
|
final long displayIndex,
|
||||||
final long streamCount,
|
final long streamCount,
|
||||||
final long timesStreamIsContained) {
|
final long timesStreamIsContained) {
|
||||||
super(uid, name, thumbnailUrl, streamCount);
|
super(uid, name, thumbnailUrl, isThumbnailPermanent, thumbnailStreamId, displayIndex,
|
||||||
|
streamCount);
|
||||||
this.timesStreamIsContained = timesStreamIsContained;
|
this.timesStreamIsContained = timesStreamIsContained;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,22 +1,13 @@
|
||||||
package org.schabi.newpipe.database.playlist;
|
package org.schabi.newpipe.database.playlist;
|
||||||
|
|
||||||
import org.schabi.newpipe.database.LocalItem;
|
import org.schabi.newpipe.database.LocalItem;
|
||||||
import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity;
|
|
||||||
|
|
||||||
import java.util.Comparator;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
import java.util.stream.Stream;
|
|
||||||
|
|
||||||
public interface PlaylistLocalItem extends LocalItem {
|
public interface PlaylistLocalItem extends LocalItem {
|
||||||
String getOrderingName();
|
String getOrderingName();
|
||||||
|
|
||||||
static List<PlaylistLocalItem> merge(
|
long getDisplayIndex();
|
||||||
final List<PlaylistMetadataEntry> localPlaylists,
|
|
||||||
final List<PlaylistRemoteEntity> remotePlaylists) {
|
long getUid();
|
||||||
return Stream.concat(localPlaylists.stream(), remotePlaylists.stream())
|
|
||||||
.sorted(Comparator.comparing(PlaylistLocalItem::getOrderingName,
|
void setDisplayIndex(long displayIndex);
|
||||||
Comparator.nullsLast(String.CASE_INSENSITIVE_ORDER)))
|
|
||||||
.collect(Collectors.toList());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,27 +2,40 @@ package org.schabi.newpipe.database.playlist;
|
||||||
|
|
||||||
import androidx.room.ColumnInfo;
|
import androidx.room.ColumnInfo;
|
||||||
|
|
||||||
|
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_DISPLAY_INDEX;
|
||||||
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_ID;
|
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_ID;
|
||||||
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_NAME;
|
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_NAME;
|
||||||
|
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_THUMBNAIL_PERMANENT;
|
||||||
|
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_THUMBNAIL_STREAM_ID;
|
||||||
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_THUMBNAIL_URL;
|
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_THUMBNAIL_URL;
|
||||||
|
|
||||||
public class PlaylistMetadataEntry implements PlaylistLocalItem {
|
public class PlaylistMetadataEntry implements PlaylistLocalItem {
|
||||||
public static final String PLAYLIST_STREAM_COUNT = "streamCount";
|
public static final String PLAYLIST_STREAM_COUNT = "streamCount";
|
||||||
|
|
||||||
@ColumnInfo(name = PLAYLIST_ID)
|
@ColumnInfo(name = PLAYLIST_ID)
|
||||||
public final long uid;
|
private final long uid;
|
||||||
@ColumnInfo(name = PLAYLIST_NAME)
|
@ColumnInfo(name = PLAYLIST_NAME)
|
||||||
public final String name;
|
public final String name;
|
||||||
|
@ColumnInfo(name = PLAYLIST_THUMBNAIL_PERMANENT)
|
||||||
|
private final boolean isThumbnailPermanent;
|
||||||
|
@ColumnInfo(name = PLAYLIST_THUMBNAIL_STREAM_ID)
|
||||||
|
private final long thumbnailStreamId;
|
||||||
@ColumnInfo(name = PLAYLIST_THUMBNAIL_URL)
|
@ColumnInfo(name = PLAYLIST_THUMBNAIL_URL)
|
||||||
public final String thumbnailUrl;
|
public final String thumbnailUrl;
|
||||||
|
@ColumnInfo(name = PLAYLIST_DISPLAY_INDEX)
|
||||||
|
private long displayIndex;
|
||||||
@ColumnInfo(name = PLAYLIST_STREAM_COUNT)
|
@ColumnInfo(name = PLAYLIST_STREAM_COUNT)
|
||||||
public final long streamCount;
|
public final long streamCount;
|
||||||
|
|
||||||
public PlaylistMetadataEntry(final long uid, final String name, final String thumbnailUrl,
|
public PlaylistMetadataEntry(final long uid, final String name, final String thumbnailUrl,
|
||||||
final long streamCount) {
|
final boolean isThumbnailPermanent, final long thumbnailStreamId,
|
||||||
|
final long displayIndex, final long streamCount) {
|
||||||
this.uid = uid;
|
this.uid = uid;
|
||||||
this.name = name;
|
this.name = name;
|
||||||
this.thumbnailUrl = thumbnailUrl;
|
this.thumbnailUrl = thumbnailUrl;
|
||||||
|
this.isThumbnailPermanent = isThumbnailPermanent;
|
||||||
|
this.thumbnailStreamId = thumbnailStreamId;
|
||||||
|
this.displayIndex = displayIndex;
|
||||||
this.streamCount = streamCount;
|
this.streamCount = streamCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -35,4 +48,27 @@ public class PlaylistMetadataEntry implements PlaylistLocalItem {
|
||||||
public String getOrderingName() {
|
public String getOrderingName() {
|
||||||
return name;
|
return name;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean isThumbnailPermanent() {
|
||||||
|
return isThumbnailPermanent;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getThumbnailStreamId() {
|
||||||
|
return thumbnailStreamId;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long getDisplayIndex() {
|
||||||
|
return displayIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long getUid() {
|
||||||
|
return uid;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setDisplayIndex(final long displayIndex) {
|
||||||
|
this.displayIndex = displayIndex;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@ package org.schabi.newpipe.database.playlist.dao;
|
||||||
|
|
||||||
import androidx.room.Dao;
|
import androidx.room.Dao;
|
||||||
import androidx.room.Query;
|
import androidx.room.Query;
|
||||||
|
import androidx.room.Transaction;
|
||||||
|
|
||||||
import org.schabi.newpipe.database.BasicDAO;
|
import org.schabi.newpipe.database.BasicDAO;
|
||||||
import org.schabi.newpipe.database.playlist.model.PlaylistEntity;
|
import org.schabi.newpipe.database.playlist.model.PlaylistEntity;
|
||||||
|
@ -36,4 +37,17 @@ public interface PlaylistDAO extends BasicDAO<PlaylistEntity> {
|
||||||
|
|
||||||
@Query("SELECT COUNT(*) FROM " + PLAYLIST_TABLE)
|
@Query("SELECT COUNT(*) FROM " + PLAYLIST_TABLE)
|
||||||
Flowable<Long> getCount();
|
Flowable<Long> getCount();
|
||||||
|
|
||||||
|
@Transaction
|
||||||
|
default long upsertPlaylist(final PlaylistEntity playlist) {
|
||||||
|
final long playlistId = playlist.getUid();
|
||||||
|
|
||||||
|
if (playlistId == -1) {
|
||||||
|
// This situation is probably impossible.
|
||||||
|
return insert(playlist);
|
||||||
|
} else {
|
||||||
|
update(playlist);
|
||||||
|
return playlistId;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,6 +11,7 @@ import java.util.List;
|
||||||
|
|
||||||
import io.reactivex.rxjava3.core.Flowable;
|
import io.reactivex.rxjava3.core.Flowable;
|
||||||
|
|
||||||
|
import static org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity.REMOTE_PLAYLIST_DISPLAY_INDEX;
|
||||||
import static org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity.REMOTE_PLAYLIST_ID;
|
import static org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity.REMOTE_PLAYLIST_ID;
|
||||||
import static org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity.REMOTE_PLAYLIST_SERVICE_ID;
|
import static org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity.REMOTE_PLAYLIST_SERVICE_ID;
|
||||||
import static org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity.REMOTE_PLAYLIST_TABLE;
|
import static org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity.REMOTE_PLAYLIST_TABLE;
|
||||||
|
@ -31,10 +32,18 @@ public interface PlaylistRemoteDAO extends BasicDAO<PlaylistRemoteEntity> {
|
||||||
+ " WHERE " + REMOTE_PLAYLIST_SERVICE_ID + " = :serviceId")
|
+ " WHERE " + REMOTE_PLAYLIST_SERVICE_ID + " = :serviceId")
|
||||||
Flowable<List<PlaylistRemoteEntity>> listByService(int serviceId);
|
Flowable<List<PlaylistRemoteEntity>> listByService(int serviceId);
|
||||||
|
|
||||||
|
@Query("SELECT * FROM " + REMOTE_PLAYLIST_TABLE + " WHERE "
|
||||||
|
+ REMOTE_PLAYLIST_ID + " = :playlistId")
|
||||||
|
Flowable<List<PlaylistRemoteEntity>> getPlaylist(long playlistId);
|
||||||
|
|
||||||
@Query("SELECT * FROM " + REMOTE_PLAYLIST_TABLE + " WHERE "
|
@Query("SELECT * FROM " + REMOTE_PLAYLIST_TABLE + " WHERE "
|
||||||
+ REMOTE_PLAYLIST_URL + " = :url AND " + REMOTE_PLAYLIST_SERVICE_ID + " = :serviceId")
|
+ REMOTE_PLAYLIST_URL + " = :url AND " + REMOTE_PLAYLIST_SERVICE_ID + " = :serviceId")
|
||||||
Flowable<List<PlaylistRemoteEntity>> getPlaylist(long serviceId, String url);
|
Flowable<List<PlaylistRemoteEntity>> getPlaylist(long serviceId, String url);
|
||||||
|
|
||||||
|
@Query("SELECT * FROM " + REMOTE_PLAYLIST_TABLE
|
||||||
|
+ " ORDER BY " + REMOTE_PLAYLIST_DISPLAY_INDEX)
|
||||||
|
Flowable<List<PlaylistRemoteEntity>> getPlaylists();
|
||||||
|
|
||||||
@Query("SELECT " + REMOTE_PLAYLIST_ID + " FROM " + REMOTE_PLAYLIST_TABLE
|
@Query("SELECT " + REMOTE_PLAYLIST_ID + " FROM " + REMOTE_PLAYLIST_TABLE
|
||||||
+ " WHERE " + REMOTE_PLAYLIST_URL + " = :url "
|
+ " WHERE " + REMOTE_PLAYLIST_URL + " = :url "
|
||||||
+ "AND " + REMOTE_PLAYLIST_SERVICE_ID + " = :serviceId")
|
+ "AND " + REMOTE_PLAYLIST_SERVICE_ID + " = :serviceId")
|
||||||
|
|
|
@ -18,10 +18,12 @@ import io.reactivex.rxjava3.core.Flowable;
|
||||||
|
|
||||||
import static org.schabi.newpipe.database.playlist.PlaylistDuplicatesEntry.PLAYLIST_TIMES_STREAM_IS_CONTAINED;
|
import static org.schabi.newpipe.database.playlist.PlaylistDuplicatesEntry.PLAYLIST_TIMES_STREAM_IS_CONTAINED;
|
||||||
import static org.schabi.newpipe.database.playlist.PlaylistMetadataEntry.PLAYLIST_STREAM_COUNT;
|
import static org.schabi.newpipe.database.playlist.PlaylistMetadataEntry.PLAYLIST_STREAM_COUNT;
|
||||||
|
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_DISPLAY_INDEX;
|
||||||
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.DEFAULT_THUMBNAIL;
|
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.DEFAULT_THUMBNAIL;
|
||||||
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_ID;
|
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_ID;
|
||||||
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_NAME;
|
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_NAME;
|
||||||
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_TABLE;
|
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_TABLE;
|
||||||
|
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_THUMBNAIL_PERMANENT;
|
||||||
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_THUMBNAIL_STREAM_ID;
|
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_THUMBNAIL_STREAM_ID;
|
||||||
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_THUMBNAIL_URL;
|
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_THUMBNAIL_URL;
|
||||||
import static org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity.JOIN_INDEX;
|
import static org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity.JOIN_INDEX;
|
||||||
|
@ -91,7 +93,9 @@ public interface PlaylistStreamDAO extends BasicDAO<PlaylistStreamEntity> {
|
||||||
Flowable<List<PlaylistStreamEntry>> getOrderedStreamsOf(long playlistId);
|
Flowable<List<PlaylistStreamEntry>> getOrderedStreamsOf(long playlistId);
|
||||||
|
|
||||||
@Transaction
|
@Transaction
|
||||||
@Query("SELECT " + PLAYLIST_ID + ", " + PLAYLIST_NAME + ","
|
@Query("SELECT " + PLAYLIST_ID + ", " + PLAYLIST_NAME + ", "
|
||||||
|
+ PLAYLIST_THUMBNAIL_PERMANENT + ", " + PLAYLIST_THUMBNAIL_STREAM_ID + ", "
|
||||||
|
+ PLAYLIST_DISPLAY_INDEX + ", "
|
||||||
|
|
||||||
+ " CASE WHEN " + PLAYLIST_THUMBNAIL_STREAM_ID + " = "
|
+ " CASE WHEN " + PLAYLIST_THUMBNAIL_STREAM_ID + " = "
|
||||||
+ PlaylistEntity.DEFAULT_THUMBNAIL_ID + " THEN " + "'" + DEFAULT_THUMBNAIL + "'"
|
+ PlaylistEntity.DEFAULT_THUMBNAIL_ID + " THEN " + "'" + DEFAULT_THUMBNAIL + "'"
|
||||||
|
@ -105,7 +109,7 @@ public interface PlaylistStreamDAO extends BasicDAO<PlaylistStreamEntity> {
|
||||||
+ " LEFT JOIN " + PLAYLIST_STREAM_JOIN_TABLE
|
+ " LEFT JOIN " + PLAYLIST_STREAM_JOIN_TABLE
|
||||||
+ " ON " + PLAYLIST_TABLE + "." + PLAYLIST_ID + " = " + JOIN_PLAYLIST_ID
|
+ " ON " + PLAYLIST_TABLE + "." + PLAYLIST_ID + " = " + JOIN_PLAYLIST_ID
|
||||||
+ " GROUP BY " + PLAYLIST_ID
|
+ " GROUP BY " + PLAYLIST_ID
|
||||||
+ " ORDER BY " + PLAYLIST_NAME + " COLLATE NOCASE ASC")
|
+ " ORDER BY " + PLAYLIST_DISPLAY_INDEX)
|
||||||
Flowable<List<PlaylistMetadataEntry>> getPlaylistMetadata();
|
Flowable<List<PlaylistMetadataEntry>> getPlaylistMetadata();
|
||||||
|
|
||||||
@RewriteQueriesToDropUnusedColumns
|
@RewriteQueriesToDropUnusedColumns
|
||||||
|
@ -126,8 +130,9 @@ public interface PlaylistStreamDAO extends BasicDAO<PlaylistStreamEntity> {
|
||||||
Flowable<List<PlaylistStreamEntry>> getStreamsWithoutDuplicates(long playlistId);
|
Flowable<List<PlaylistStreamEntry>> getStreamsWithoutDuplicates(long playlistId);
|
||||||
|
|
||||||
@Transaction
|
@Transaction
|
||||||
@Query("SELECT " + PLAYLIST_TABLE + "." + PLAYLIST_ID + ", "
|
@Query("SELECT " + PLAYLIST_TABLE + "." + PLAYLIST_ID + ", " + PLAYLIST_NAME + ", "
|
||||||
+ PLAYLIST_NAME + ", "
|
+ PLAYLIST_THUMBNAIL_PERMANENT + ", " + PLAYLIST_THUMBNAIL_STREAM_ID + ", "
|
||||||
|
+ PLAYLIST_DISPLAY_INDEX + ", "
|
||||||
|
|
||||||
+ " CASE WHEN " + PLAYLIST_THUMBNAIL_STREAM_ID + " = "
|
+ " CASE WHEN " + PLAYLIST_THUMBNAIL_STREAM_ID + " = "
|
||||||
+ PlaylistEntity.DEFAULT_THUMBNAIL_ID + " THEN " + "'" + DEFAULT_THUMBNAIL + "'"
|
+ PlaylistEntity.DEFAULT_THUMBNAIL_ID + " THEN " + "'" + DEFAULT_THUMBNAIL + "'"
|
||||||
|
@ -149,6 +154,6 @@ public interface PlaylistStreamDAO extends BasicDAO<PlaylistStreamEntity> {
|
||||||
+ " AND :streamUrl = :streamUrl"
|
+ " AND :streamUrl = :streamUrl"
|
||||||
|
|
||||||
+ " GROUP BY " + JOIN_PLAYLIST_ID
|
+ " GROUP BY " + JOIN_PLAYLIST_ID
|
||||||
+ " ORDER BY " + PLAYLIST_NAME + " COLLATE NOCASE ASC")
|
+ " ORDER BY " + PLAYLIST_DISPLAY_INDEX)
|
||||||
Flowable<List<PlaylistDuplicatesEntry>> getPlaylistDuplicatesMetadata(String streamUrl);
|
Flowable<List<PlaylistDuplicatesEntry>> getPlaylistDuplicatesMetadata(String streamUrl);
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,16 +2,15 @@ package org.schabi.newpipe.database.playlist.model;
|
||||||
|
|
||||||
import androidx.room.ColumnInfo;
|
import androidx.room.ColumnInfo;
|
||||||
import androidx.room.Entity;
|
import androidx.room.Entity;
|
||||||
import androidx.room.Index;
|
import androidx.room.Ignore;
|
||||||
import androidx.room.PrimaryKey;
|
import androidx.room.PrimaryKey;
|
||||||
|
|
||||||
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_NAME;
|
|
||||||
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_TABLE;
|
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_TABLE;
|
||||||
|
|
||||||
import org.schabi.newpipe.R;
|
import org.schabi.newpipe.R;
|
||||||
|
import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry;
|
||||||
|
|
||||||
@Entity(tableName = PLAYLIST_TABLE,
|
@Entity(tableName = PLAYLIST_TABLE)
|
||||||
indices = {@Index(value = {PLAYLIST_NAME})})
|
|
||||||
public class PlaylistEntity {
|
public class PlaylistEntity {
|
||||||
|
|
||||||
public static final String DEFAULT_THUMBNAIL = "drawable://"
|
public static final String DEFAULT_THUMBNAIL = "drawable://"
|
||||||
|
@ -22,6 +21,7 @@ public class PlaylistEntity {
|
||||||
public static final String PLAYLIST_ID = "uid";
|
public static final String PLAYLIST_ID = "uid";
|
||||||
public static final String PLAYLIST_NAME = "name";
|
public static final String PLAYLIST_NAME = "name";
|
||||||
public static final String PLAYLIST_THUMBNAIL_URL = "thumbnail_url";
|
public static final String PLAYLIST_THUMBNAIL_URL = "thumbnail_url";
|
||||||
|
public static final String PLAYLIST_DISPLAY_INDEX = "display_index";
|
||||||
public static final String PLAYLIST_THUMBNAIL_PERMANENT = "is_thumbnail_permanent";
|
public static final String PLAYLIST_THUMBNAIL_PERMANENT = "is_thumbnail_permanent";
|
||||||
public static final String PLAYLIST_THUMBNAIL_STREAM_ID = "thumbnail_stream_id";
|
public static final String PLAYLIST_THUMBNAIL_STREAM_ID = "thumbnail_stream_id";
|
||||||
|
|
||||||
|
@ -38,11 +38,24 @@ public class PlaylistEntity {
|
||||||
@ColumnInfo(name = PLAYLIST_THUMBNAIL_STREAM_ID)
|
@ColumnInfo(name = PLAYLIST_THUMBNAIL_STREAM_ID)
|
||||||
private long thumbnailStreamId;
|
private long thumbnailStreamId;
|
||||||
|
|
||||||
|
@ColumnInfo(name = PLAYLIST_DISPLAY_INDEX)
|
||||||
|
private long displayIndex;
|
||||||
|
|
||||||
public PlaylistEntity(final String name, final boolean isThumbnailPermanent,
|
public PlaylistEntity(final String name, final boolean isThumbnailPermanent,
|
||||||
final long thumbnailStreamId) {
|
final long thumbnailStreamId, final long displayIndex) {
|
||||||
this.name = name;
|
this.name = name;
|
||||||
this.isThumbnailPermanent = isThumbnailPermanent;
|
this.isThumbnailPermanent = isThumbnailPermanent;
|
||||||
this.thumbnailStreamId = thumbnailStreamId;
|
this.thumbnailStreamId = thumbnailStreamId;
|
||||||
|
this.displayIndex = displayIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Ignore
|
||||||
|
public PlaylistEntity(final PlaylistMetadataEntry item) {
|
||||||
|
this.uid = item.getUid();
|
||||||
|
this.name = item.name;
|
||||||
|
this.isThumbnailPermanent = item.isThumbnailPermanent();
|
||||||
|
this.thumbnailStreamId = item.getThumbnailStreamId();
|
||||||
|
this.displayIndex = item.getDisplayIndex();
|
||||||
}
|
}
|
||||||
|
|
||||||
public long getUid() {
|
public long getUid() {
|
||||||
|
@ -77,4 +90,11 @@ public class PlaylistEntity {
|
||||||
this.isThumbnailPermanent = isThumbnailSet;
|
this.isThumbnailPermanent = isThumbnailSet;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public long getDisplayIndex() {
|
||||||
|
return displayIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setDisplayIndex(final long displayIndex) {
|
||||||
|
this.displayIndex = displayIndex;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,7 +21,6 @@ import static org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity.RE
|
||||||
|
|
||||||
@Entity(tableName = REMOTE_PLAYLIST_TABLE,
|
@Entity(tableName = REMOTE_PLAYLIST_TABLE,
|
||||||
indices = {
|
indices = {
|
||||||
@Index(value = {REMOTE_PLAYLIST_NAME}),
|
|
||||||
@Index(value = {REMOTE_PLAYLIST_SERVICE_ID, REMOTE_PLAYLIST_URL}, unique = true)
|
@Index(value = {REMOTE_PLAYLIST_SERVICE_ID, REMOTE_PLAYLIST_URL}, unique = true)
|
||||||
})
|
})
|
||||||
public class PlaylistRemoteEntity implements PlaylistLocalItem {
|
public class PlaylistRemoteEntity implements PlaylistLocalItem {
|
||||||
|
@ -32,6 +31,7 @@ public class PlaylistRemoteEntity implements PlaylistLocalItem {
|
||||||
public static final String REMOTE_PLAYLIST_URL = "url";
|
public static final String REMOTE_PLAYLIST_URL = "url";
|
||||||
public static final String REMOTE_PLAYLIST_THUMBNAIL_URL = "thumbnail_url";
|
public static final String REMOTE_PLAYLIST_THUMBNAIL_URL = "thumbnail_url";
|
||||||
public static final String REMOTE_PLAYLIST_UPLOADER_NAME = "uploader";
|
public static final String REMOTE_PLAYLIST_UPLOADER_NAME = "uploader";
|
||||||
|
public static final String REMOTE_PLAYLIST_DISPLAY_INDEX = "display_index";
|
||||||
public static final String REMOTE_PLAYLIST_STREAM_COUNT = "stream_count";
|
public static final String REMOTE_PLAYLIST_STREAM_COUNT = "stream_count";
|
||||||
|
|
||||||
@PrimaryKey(autoGenerate = true)
|
@PrimaryKey(autoGenerate = true)
|
||||||
|
@ -53,6 +53,9 @@ public class PlaylistRemoteEntity implements PlaylistLocalItem {
|
||||||
@ColumnInfo(name = REMOTE_PLAYLIST_UPLOADER_NAME)
|
@ColumnInfo(name = REMOTE_PLAYLIST_UPLOADER_NAME)
|
||||||
private String uploader;
|
private String uploader;
|
||||||
|
|
||||||
|
@ColumnInfo(name = REMOTE_PLAYLIST_DISPLAY_INDEX)
|
||||||
|
private long displayIndex = -1; // Make sure the new item is on the top
|
||||||
|
|
||||||
@ColumnInfo(name = REMOTE_PLAYLIST_STREAM_COUNT)
|
@ColumnInfo(name = REMOTE_PLAYLIST_STREAM_COUNT)
|
||||||
private Long streamCount;
|
private Long streamCount;
|
||||||
|
|
||||||
|
@ -67,6 +70,19 @@ public class PlaylistRemoteEntity implements PlaylistLocalItem {
|
||||||
this.streamCount = streamCount;
|
this.streamCount = streamCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Ignore
|
||||||
|
public PlaylistRemoteEntity(final int serviceId, final String name, final String url,
|
||||||
|
final String thumbnailUrl, final String uploader,
|
||||||
|
final long displayIndex, final Long streamCount) {
|
||||||
|
this.serviceId = serviceId;
|
||||||
|
this.name = name;
|
||||||
|
this.url = url;
|
||||||
|
this.thumbnailUrl = thumbnailUrl;
|
||||||
|
this.uploader = uploader;
|
||||||
|
this.displayIndex = displayIndex;
|
||||||
|
this.streamCount = streamCount;
|
||||||
|
}
|
||||||
|
|
||||||
@Ignore
|
@Ignore
|
||||||
public PlaylistRemoteEntity(final PlaylistInfo info) {
|
public PlaylistRemoteEntity(final PlaylistInfo info) {
|
||||||
this(info.getServiceId(), info.getName(), info.getUrl(),
|
this(info.getServiceId(), info.getName(), info.getUrl(),
|
||||||
|
@ -93,6 +109,7 @@ public class PlaylistRemoteEntity implements PlaylistLocalItem {
|
||||||
&& TextUtils.equals(getUploader(), info.getUploaderName());
|
&& TextUtils.equals(getUploader(), info.getUploaderName());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
public long getUid() {
|
public long getUid() {
|
||||||
return uid;
|
return uid;
|
||||||
}
|
}
|
||||||
|
@ -141,6 +158,16 @@ public class PlaylistRemoteEntity implements PlaylistLocalItem {
|
||||||
this.uploader = uploader;
|
this.uploader = uploader;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long getDisplayIndex() {
|
||||||
|
return displayIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setDisplayIndex(final long displayIndex) {
|
||||||
|
this.displayIndex = displayIndex;
|
||||||
|
}
|
||||||
|
|
||||||
public Long getStreamCount() {
|
public Long getStreamCount() {
|
||||||
return streamCount;
|
return streamCount;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package org.schabi.newpipe.database.subscription;
|
package org.schabi.newpipe.database.subscription;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
import androidx.room.ColumnInfo;
|
import androidx.room.ColumnInfo;
|
||||||
import androidx.room.Entity;
|
import androidx.room.Entity;
|
||||||
import androidx.room.Ignore;
|
import androidx.room.Ignore;
|
||||||
|
@ -95,11 +96,12 @@ public class SubscriptionEntity {
|
||||||
this.name = name;
|
this.name = name;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
public String getAvatarUrl() {
|
public String getAvatarUrl() {
|
||||||
return avatarUrl;
|
return avatarUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setAvatarUrl(final String avatarUrl) {
|
public void setAvatarUrl(@Nullable final String avatarUrl) {
|
||||||
this.avatarUrl = avatarUrl;
|
this.avatarUrl = avatarUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -7,8 +7,6 @@ import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
import android.content.ComponentName;
|
import android.content.ComponentName;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.DialogInterface;
|
|
||||||
import android.content.DialogInterface.OnDismissListener;
|
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.ServiceConnection;
|
import android.content.ServiceConnection;
|
||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
|
@ -16,6 +14,7 @@ import android.net.Uri;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.os.Environment;
|
import android.os.Environment;
|
||||||
import android.os.IBinder;
|
import android.os.IBinder;
|
||||||
|
import android.provider.Settings;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
|
@ -74,6 +73,7 @@ import org.schabi.newpipe.util.ThemeHelper;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
@ -111,14 +111,11 @@ public class DownloadDialog extends DialogFragment
|
||||||
@State
|
@State
|
||||||
int selectedSubtitleIndex = 0; // default to the first item
|
int selectedSubtitleIndex = 0; // default to the first item
|
||||||
|
|
||||||
@Nullable
|
|
||||||
private OnDismissListener onDismissListener = null;
|
|
||||||
|
|
||||||
private StoredDirectoryHelper mainStorageAudio = null;
|
private StoredDirectoryHelper mainStorageAudio = null;
|
||||||
private StoredDirectoryHelper mainStorageVideo = null;
|
private StoredDirectoryHelper mainStorageVideo = null;
|
||||||
private DownloadManager downloadManager = null;
|
private DownloadManager downloadManager = null;
|
||||||
private ActionMenuItemView okButton = null;
|
private ActionMenuItemView okButton = null;
|
||||||
private Context context;
|
private Context context = null;
|
||||||
private boolean askForSavePath;
|
private boolean askForSavePath;
|
||||||
|
|
||||||
private AudioTrackAdapter audioTrackAdapter;
|
private AudioTrackAdapter audioTrackAdapter;
|
||||||
|
@ -146,7 +143,6 @@ public class DownloadDialog extends DialogFragment
|
||||||
registerForActivityResult(
|
registerForActivityResult(
|
||||||
new StartActivityForResult(), this::requestDownloadPickVideoFolderResult);
|
new StartActivityForResult(), this::requestDownloadPickVideoFolderResult);
|
||||||
|
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
// Instance creation
|
// Instance creation
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
@ -194,13 +190,6 @@ public class DownloadDialog extends DialogFragment
|
||||||
this.selectedVideoIndex = ListHelper.getDefaultResolutionIndex(context, videoStreams);
|
this.selectedVideoIndex = ListHelper.getDefaultResolutionIndex(context, videoStreams);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @param onDismissListener the listener to call in {@link #onDismiss(DialogInterface)}
|
|
||||||
*/
|
|
||||||
public void setOnDismissListener(@Nullable final OnDismissListener onDismissListener) {
|
|
||||||
this.onDismissListener = onDismissListener;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
// Android lifecycle
|
// Android lifecycle
|
||||||
|
@ -220,6 +209,8 @@ public class DownloadDialog extends DialogFragment
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// context will remain null if dismiss() was called above, allowing to check whether the
|
||||||
|
// dialog is being dismissed in onViewCreated()
|
||||||
context = getContext();
|
context = getContext();
|
||||||
|
|
||||||
setStyle(STYLE_NO_TITLE, ThemeHelper.getDialogTheme(context));
|
setStyle(STYLE_NO_TITLE, ThemeHelper.getDialogTheme(context));
|
||||||
|
@ -304,6 +295,9 @@ public class DownloadDialog extends DialogFragment
|
||||||
@Nullable final Bundle savedInstanceState) {
|
@Nullable final Bundle savedInstanceState) {
|
||||||
super.onViewCreated(view, savedInstanceState);
|
super.onViewCreated(view, savedInstanceState);
|
||||||
dialogBinding = DownloadDialogBinding.bind(view);
|
dialogBinding = DownloadDialogBinding.bind(view);
|
||||||
|
if (context == null) {
|
||||||
|
return; // the dialog is being dismissed, see the call to dismiss() in onCreate()
|
||||||
|
}
|
||||||
|
|
||||||
dialogBinding.fileName.setText(FilenameUtils.createFilename(getContext(),
|
dialogBinding.fileName.setText(FilenameUtils.createFilename(getContext(),
|
||||||
currentInfo.getName()));
|
currentInfo.getName()));
|
||||||
|
@ -363,14 +357,6 @@ public class DownloadDialog extends DialogFragment
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onDismiss(@NonNull final DialogInterface dialog) {
|
|
||||||
super.onDismiss(dialog);
|
|
||||||
if (onDismissListener != null) {
|
|
||||||
onDismissListener.onDismiss(dialog);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onDestroy() {
|
public void onDestroy() {
|
||||||
super.onDestroy();
|
super.onDestroy();
|
||||||
|
@ -564,7 +550,6 @@ public class DownloadDialog extends DialogFragment
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
// Listeners
|
// Listeners
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
@ -783,6 +768,7 @@ public class DownloadDialog extends DialogFragment
|
||||||
final StoredDirectoryHelper mainStorage;
|
final StoredDirectoryHelper mainStorage;
|
||||||
final MediaFormat format;
|
final MediaFormat format;
|
||||||
final String selectedMediaType;
|
final String selectedMediaType;
|
||||||
|
final long size;
|
||||||
|
|
||||||
// first, build the filename and get the output folder (if possible)
|
// first, build the filename and get the output folder (if possible)
|
||||||
// later, run a very very very large file checking logic
|
// later, run a very very very large file checking logic
|
||||||
|
@ -794,6 +780,7 @@ public class DownloadDialog extends DialogFragment
|
||||||
selectedMediaType = getString(R.string.last_download_type_audio_key);
|
selectedMediaType = getString(R.string.last_download_type_audio_key);
|
||||||
mainStorage = mainStorageAudio;
|
mainStorage = mainStorageAudio;
|
||||||
format = audioStreamsAdapter.getItem(selectedAudioIndex).getFormat();
|
format = audioStreamsAdapter.getItem(selectedAudioIndex).getFormat();
|
||||||
|
size = getWrappedAudioStreams().getSizeInBytes(selectedAudioIndex);
|
||||||
if (format == MediaFormat.WEBMA_OPUS) {
|
if (format == MediaFormat.WEBMA_OPUS) {
|
||||||
mimeTmp = "audio/ogg";
|
mimeTmp = "audio/ogg";
|
||||||
filenameTmp += "opus";
|
filenameTmp += "opus";
|
||||||
|
@ -806,6 +793,7 @@ public class DownloadDialog extends DialogFragment
|
||||||
selectedMediaType = getString(R.string.last_download_type_video_key);
|
selectedMediaType = getString(R.string.last_download_type_video_key);
|
||||||
mainStorage = mainStorageVideo;
|
mainStorage = mainStorageVideo;
|
||||||
format = videoStreamsAdapter.getItem(selectedVideoIndex).getFormat();
|
format = videoStreamsAdapter.getItem(selectedVideoIndex).getFormat();
|
||||||
|
size = wrappedVideoStreams.getSizeInBytes(selectedVideoIndex);
|
||||||
if (format != null) {
|
if (format != null) {
|
||||||
mimeTmp = format.mimeType;
|
mimeTmp = format.mimeType;
|
||||||
filenameTmp += format.getSuffix();
|
filenameTmp += format.getSuffix();
|
||||||
|
@ -815,6 +803,7 @@ public class DownloadDialog extends DialogFragment
|
||||||
selectedMediaType = getString(R.string.last_download_type_subtitle_key);
|
selectedMediaType = getString(R.string.last_download_type_subtitle_key);
|
||||||
mainStorage = mainStorageVideo; // subtitle & video files go together
|
mainStorage = mainStorageVideo; // subtitle & video files go together
|
||||||
format = subtitleStreamsAdapter.getItem(selectedSubtitleIndex).getFormat();
|
format = subtitleStreamsAdapter.getItem(selectedSubtitleIndex).getFormat();
|
||||||
|
size = wrappedSubtitleStreams.getSizeInBytes(selectedSubtitleIndex);
|
||||||
if (format != null) {
|
if (format != null) {
|
||||||
mimeTmp = format.mimeType;
|
mimeTmp = format.mimeType;
|
||||||
}
|
}
|
||||||
|
@ -870,6 +859,21 @@ public class DownloadDialog extends DialogFragment
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check for free storage space
|
||||||
|
final long freeSpace = mainStorage.getFreeStorageSpace();
|
||||||
|
if (freeSpace <= size) {
|
||||||
|
Toast.makeText(context, getString(R.
|
||||||
|
string.error_insufficient_storage), Toast.LENGTH_LONG).show();
|
||||||
|
// move the user to storage setting tab
|
||||||
|
final Intent storageSettingsIntent = new Intent(Settings.
|
||||||
|
ACTION_INTERNAL_STORAGE_SETTINGS);
|
||||||
|
if (storageSettingsIntent.resolveActivity(context.getPackageManager())
|
||||||
|
!= null) {
|
||||||
|
startActivity(storageSettingsIntent);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// check for existing file with the same name
|
// check for existing file with the same name
|
||||||
checkSelectedDownload(mainStorage, mainStorage.findFile(filenameTmp), filenameTmp,
|
checkSelectedDownload(mainStorage, mainStorage.findFile(filenameTmp), filenameTmp,
|
||||||
mimeTmp);
|
mimeTmp);
|
||||||
|
@ -1052,7 +1056,7 @@ public class DownloadDialog extends DialogFragment
|
||||||
final char kind;
|
final char kind;
|
||||||
int threads = dialogBinding.threads.getProgress() + 1;
|
int threads = dialogBinding.threads.getProgress() + 1;
|
||||||
final String[] urls;
|
final String[] urls;
|
||||||
final MissionRecoveryInfo[] recoveryInfo;
|
final List<MissionRecoveryInfo> recoveryInfo;
|
||||||
String psName = null;
|
String psName = null;
|
||||||
String[] psArgs = null;
|
String[] psArgs = null;
|
||||||
long nearLength = 0;
|
long nearLength = 0;
|
||||||
|
@ -1117,9 +1121,7 @@ public class DownloadDialog extends DialogFragment
|
||||||
urls = new String[] {
|
urls = new String[] {
|
||||||
selectedStream.getContent()
|
selectedStream.getContent()
|
||||||
};
|
};
|
||||||
recoveryInfo = new MissionRecoveryInfo[] {
|
recoveryInfo = List.of(new MissionRecoveryInfo(selectedStream));
|
||||||
new MissionRecoveryInfo(selectedStream)
|
|
||||||
};
|
|
||||||
} else {
|
} else {
|
||||||
if (secondaryStream.getDeliveryMethod() != PROGRESSIVE_HTTP) {
|
if (secondaryStream.getDeliveryMethod() != PROGRESSIVE_HTTP) {
|
||||||
throw new IllegalArgumentException("Unsupported stream delivery format"
|
throw new IllegalArgumentException("Unsupported stream delivery format"
|
||||||
|
@ -1129,12 +1131,14 @@ public class DownloadDialog extends DialogFragment
|
||||||
urls = new String[] {
|
urls = new String[] {
|
||||||
selectedStream.getContent(), secondaryStream.getContent()
|
selectedStream.getContent(), secondaryStream.getContent()
|
||||||
};
|
};
|
||||||
recoveryInfo = new MissionRecoveryInfo[] {new MissionRecoveryInfo(selectedStream),
|
recoveryInfo = List.of(
|
||||||
new MissionRecoveryInfo(secondaryStream)};
|
new MissionRecoveryInfo(selectedStream),
|
||||||
|
new MissionRecoveryInfo(secondaryStream)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
DownloadManagerService.startMission(context, urls, storage, kind, threads,
|
DownloadManagerService.startMission(context, urls, storage, kind, threads,
|
||||||
currentInfo.getUrl(), psName, psArgs, nearLength, recoveryInfo);
|
currentInfo.getUrl(), psName, psArgs, nearLength, new ArrayList<>(recoveryInfo));
|
||||||
|
|
||||||
Toast.makeText(context, getString(R.string.download_has_started),
|
Toast.makeText(context, getString(R.string.download_has_started),
|
||||||
Toast.LENGTH_SHORT).show();
|
Toast.LENGTH_SHORT).show();
|
||||||
|
|
|
@ -17,6 +17,7 @@ import androidx.annotation.Nullable;
|
||||||
import androidx.appcompat.app.ActionBar;
|
import androidx.appcompat.app.ActionBar;
|
||||||
import androidx.appcompat.app.AlertDialog;
|
import androidx.appcompat.app.AlertDialog;
|
||||||
import androidx.appcompat.app.AppCompatActivity;
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
|
import androidx.core.content.IntentCompat;
|
||||||
|
|
||||||
import com.grack.nanojson.JsonWriter;
|
import com.grack.nanojson.JsonWriter;
|
||||||
|
|
||||||
|
@ -105,7 +106,7 @@ public class ErrorActivity extends AppCompatActivity {
|
||||||
actionBar.setDisplayShowTitleEnabled(true);
|
actionBar.setDisplayShowTitleEnabled(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
errorInfo = intent.getParcelableExtra(ERROR_INFO);
|
errorInfo = IntentCompat.getParcelableExtra(intent, ERROR_INFO, ErrorInfo.class);
|
||||||
|
|
||||||
// important add guru meditation
|
// important add guru meditation
|
||||||
addGuruMeditation();
|
addGuruMeditation();
|
||||||
|
|
|
@ -19,6 +19,7 @@ public enum UserAction {
|
||||||
REQUESTED_PLAYLIST("requested playlist"),
|
REQUESTED_PLAYLIST("requested playlist"),
|
||||||
REQUESTED_KIOSK("requested kiosk"),
|
REQUESTED_KIOSK("requested kiosk"),
|
||||||
REQUESTED_COMMENTS("requested comments"),
|
REQUESTED_COMMENTS("requested comments"),
|
||||||
|
REQUESTED_COMMENT_REPLIES("requested comment replies"),
|
||||||
REQUESTED_FEED("requested feed"),
|
REQUESTED_FEED("requested feed"),
|
||||||
REQUESTED_BOOKMARK("bookmark"),
|
REQUESTED_BOOKMARK("bookmark"),
|
||||||
DELETE_FROM_HISTORY("delete from history"),
|
DELETE_FROM_HISTORY("delete from history"),
|
||||||
|
|
|
@ -220,7 +220,7 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte
|
||||||
public void commitPlaylistTabs() {
|
public void commitPlaylistTabs() {
|
||||||
pagerAdapter.getLocalPlaylistFragments()
|
pagerAdapter.getLocalPlaylistFragments()
|
||||||
.stream()
|
.stream()
|
||||||
.forEach(LocalPlaylistFragment::commitChanges);
|
.forEach(LocalPlaylistFragment::saveImmediate);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateTabLayoutPosition() {
|
private void updateTabLayoutPosition() {
|
||||||
|
@ -282,7 +282,7 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte
|
||||||
* Keep reference to LocalPlaylistFragments, because their data can be modified by the user
|
* Keep reference to LocalPlaylistFragments, because their data can be modified by the user
|
||||||
* during runtime and changes are not committed immediately. However, in some cases,
|
* during runtime and changes are not committed immediately. However, in some cases,
|
||||||
* the changes need to be committed immediately by calling
|
* the changes need to be committed immediately by calling
|
||||||
* {@link LocalPlaylistFragment#commitChanges()}.
|
* {@link LocalPlaylistFragment#saveImmediate()}.
|
||||||
* The fragments are removed when {@link LocalPlaylistFragment#onDestroy()} is called.
|
* The fragments are removed when {@link LocalPlaylistFragment#onDestroy()} is called.
|
||||||
*/
|
*/
|
||||||
private final List<LocalPlaylistFragment> localPlaylistFragments = new ArrayList<>();
|
private final List<LocalPlaylistFragment> localPlaylistFragments = new ArrayList<>();
|
||||||
|
|
|
@ -64,7 +64,7 @@ public abstract class BaseDescriptionFragment extends BaseFragment {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the description to display.
|
* Get the description to display.
|
||||||
* @return description object
|
* @return description object, if available
|
||||||
*/
|
*/
|
||||||
@Nullable
|
@Nullable
|
||||||
protected abstract Description getDescription();
|
protected abstract Description getDescription();
|
||||||
|
@ -73,7 +73,7 @@ public abstract class BaseDescriptionFragment extends BaseFragment {
|
||||||
* Get the streaming service. Used for generating description links.
|
* Get the streaming service. Used for generating description links.
|
||||||
* @return streaming service
|
* @return streaming service
|
||||||
*/
|
*/
|
||||||
@Nullable
|
@NonNull
|
||||||
protected abstract StreamingService getService();
|
protected abstract StreamingService getService();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -93,7 +93,7 @@ public abstract class BaseDescriptionFragment extends BaseFragment {
|
||||||
* Get the list of tags to display below the description.
|
* Get the list of tags to display below the description.
|
||||||
* @return tag list
|
* @return tag list
|
||||||
*/
|
*/
|
||||||
@Nullable
|
@NonNull
|
||||||
public abstract List<String> getTags();
|
public abstract List<String> getTags();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -158,7 +158,7 @@ public abstract class BaseDescriptionFragment extends BaseFragment {
|
||||||
final LinearLayout layout,
|
final LinearLayout layout,
|
||||||
final boolean linkifyContent,
|
final boolean linkifyContent,
|
||||||
@StringRes final int type,
|
@StringRes final int type,
|
||||||
@Nullable final String content) {
|
@NonNull final String content) {
|
||||||
if (isBlank(content)) {
|
if (isBlank(content)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -221,16 +221,12 @@ public abstract class BaseDescriptionFragment extends BaseFragment {
|
||||||
urls.append(imageSizeToText(image.getWidth()));
|
urls.append(imageSizeToText(image.getWidth()));
|
||||||
} else {
|
} else {
|
||||||
switch (image.getEstimatedResolutionLevel()) {
|
switch (image.getEstimatedResolutionLevel()) {
|
||||||
case LOW:
|
case LOW -> urls.append(getString(R.string.image_quality_low));
|
||||||
urls.append(getString(R.string.image_quality_low));
|
case MEDIUM -> urls.append(getString(R.string.image_quality_medium));
|
||||||
break;
|
case HIGH -> urls.append(getString(R.string.image_quality_high));
|
||||||
default: // unreachable, Image.ResolutionLevel.UNKNOWN is already filtered out
|
default -> {
|
||||||
case MEDIUM:
|
// unreachable, Image.ResolutionLevel.UNKNOWN is already filtered out
|
||||||
urls.append(getString(R.string.image_quality_medium));
|
}
|
||||||
break;
|
|
||||||
case HIGH:
|
|
||||||
urls.append(getString(R.string.image_quality_high));
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -255,7 +251,7 @@ public abstract class BaseDescriptionFragment extends BaseFragment {
|
||||||
private void addTagsMetadataItem(final LayoutInflater inflater, final LinearLayout layout) {
|
private void addTagsMetadataItem(final LayoutInflater inflater, final LinearLayout layout) {
|
||||||
final List<String> tags = getTags();
|
final List<String> tags = getTags();
|
||||||
|
|
||||||
if (tags != null && !tags.isEmpty()) {
|
if (!tags.isEmpty()) {
|
||||||
final var itemBinding = ItemMetadataTagsBinding.inflate(inflater, layout, false);
|
final var itemBinding = ItemMetadataTagsBinding.inflate(inflater, layout, false);
|
||||||
|
|
||||||
tags.stream().sorted(String.CASE_INSENSITIVE_ORDER).forEach(tag -> {
|
tags.stream().sorted(String.CASE_INSENSITIVE_ORDER).forEach(tag -> {
|
||||||
|
|
|
@ -7,6 +7,7 @@ import android.view.LayoutInflater;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.widget.LinearLayout;
|
import android.widget.LinearLayout;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.annotation.StringRes;
|
import androidx.annotation.StringRes;
|
||||||
|
|
||||||
|
@ -23,56 +24,43 @@ import icepick.State;
|
||||||
public class DescriptionFragment extends BaseDescriptionFragment {
|
public class DescriptionFragment extends BaseDescriptionFragment {
|
||||||
|
|
||||||
@State
|
@State
|
||||||
StreamInfo streamInfo = null;
|
StreamInfo streamInfo;
|
||||||
|
|
||||||
public DescriptionFragment() {
|
|
||||||
}
|
|
||||||
|
|
||||||
public DescriptionFragment(final StreamInfo streamInfo) {
|
public DescriptionFragment(final StreamInfo streamInfo) {
|
||||||
this.streamInfo = streamInfo;
|
this.streamInfo = streamInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nullable
|
public DescriptionFragment() {
|
||||||
@Override
|
// keep empty constructor for IcePick when resuming fragment from memory
|
||||||
protected Description getDescription() {
|
|
||||||
if (streamInfo == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return streamInfo.getDescription();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
@Override
|
@Override
|
||||||
|
protected Description getDescription() {
|
||||||
|
return streamInfo.getDescription();
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
protected StreamingService getService() {
|
protected StreamingService getService() {
|
||||||
if (streamInfo == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return streamInfo.getService();
|
return streamInfo.getService();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected int getServiceId() {
|
protected int getServiceId() {
|
||||||
if (streamInfo == null) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
return streamInfo.getServiceId();
|
return streamInfo.getServiceId();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nullable
|
@NonNull
|
||||||
@Override
|
@Override
|
||||||
protected String getStreamUrl() {
|
protected String getStreamUrl() {
|
||||||
if (streamInfo == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return streamInfo.getUrl();
|
return streamInfo.getUrl();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nullable
|
@NonNull
|
||||||
@Override
|
@Override
|
||||||
public List<String> getTags() {
|
public List<String> getTags() {
|
||||||
if (streamInfo == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return streamInfo.getTags();
|
return streamInfo.getTags();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -72,8 +72,8 @@ import org.schabi.newpipe.error.ErrorUtil;
|
||||||
import org.schabi.newpipe.error.ReCaptchaActivity;
|
import org.schabi.newpipe.error.ReCaptchaActivity;
|
||||||
import org.schabi.newpipe.error.UserAction;
|
import org.schabi.newpipe.error.UserAction;
|
||||||
import org.schabi.newpipe.extractor.Image;
|
import org.schabi.newpipe.extractor.Image;
|
||||||
import org.schabi.newpipe.extractor.InfoItem;
|
|
||||||
import org.schabi.newpipe.extractor.NewPipe;
|
import org.schabi.newpipe.extractor.NewPipe;
|
||||||
|
import org.schabi.newpipe.extractor.comments.CommentsInfoItem;
|
||||||
import org.schabi.newpipe.extractor.exceptions.ContentNotSupportedException;
|
import org.schabi.newpipe.extractor.exceptions.ContentNotSupportedException;
|
||||||
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||||
import org.schabi.newpipe.extractor.stream.AudioStream;
|
import org.schabi.newpipe.extractor.stream.AudioStream;
|
||||||
|
@ -106,16 +106,17 @@ import org.schabi.newpipe.player.ui.VideoPlayerUi;
|
||||||
import org.schabi.newpipe.util.Constants;
|
import org.schabi.newpipe.util.Constants;
|
||||||
import org.schabi.newpipe.util.DeviceUtils;
|
import org.schabi.newpipe.util.DeviceUtils;
|
||||||
import org.schabi.newpipe.util.ExtractorHelper;
|
import org.schabi.newpipe.util.ExtractorHelper;
|
||||||
|
import org.schabi.newpipe.util.InfoCache;
|
||||||
import org.schabi.newpipe.util.ListHelper;
|
import org.schabi.newpipe.util.ListHelper;
|
||||||
import org.schabi.newpipe.util.Localization;
|
import org.schabi.newpipe.util.Localization;
|
||||||
import org.schabi.newpipe.util.NavigationHelper;
|
import org.schabi.newpipe.util.NavigationHelper;
|
||||||
import org.schabi.newpipe.util.PermissionHelper;
|
import org.schabi.newpipe.util.PermissionHelper;
|
||||||
import org.schabi.newpipe.util.image.PicassoHelper;
|
import org.schabi.newpipe.util.PlayButtonHelper;
|
||||||
import org.schabi.newpipe.util.StreamTypeUtil;
|
import org.schabi.newpipe.util.StreamTypeUtil;
|
||||||
import org.schabi.newpipe.util.ThemeHelper;
|
import org.schabi.newpipe.util.ThemeHelper;
|
||||||
import org.schabi.newpipe.util.external_communication.KoreUtils;
|
import org.schabi.newpipe.util.external_communication.KoreUtils;
|
||||||
import org.schabi.newpipe.util.external_communication.ShareUtils;
|
import org.schabi.newpipe.util.external_communication.ShareUtils;
|
||||||
import org.schabi.newpipe.util.PlayButtonHelper;
|
import org.schabi.newpipe.util.image.PicassoHelper;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Iterator;
|
import java.util.Iterator;
|
||||||
|
@ -481,7 +482,7 @@ public final class VideoDetailFragment
|
||||||
|
|
||||||
// commit previous pending changes to database
|
// commit previous pending changes to database
|
||||||
if (fragment instanceof LocalPlaylistFragment) {
|
if (fragment instanceof LocalPlaylistFragment) {
|
||||||
((LocalPlaylistFragment) fragment).commitChanges();
|
((LocalPlaylistFragment) fragment).saveImmediate();
|
||||||
} else if (fragment instanceof MainFragment) {
|
} else if (fragment instanceof MainFragment) {
|
||||||
((MainFragment) fragment).commitPlaylistTabs();
|
((MainFragment) fragment).commitPlaylistTabs();
|
||||||
}
|
}
|
||||||
|
@ -1012,6 +1013,20 @@ public final class VideoDetailFragment
|
||||||
updateTabLayoutVisibility();
|
updateTabLayoutVisibility();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void scrollToComment(final CommentsInfoItem comment) {
|
||||||
|
final int commentsTabPos = pageAdapter.getItemPositionByTitle(COMMENTS_TAB_TAG);
|
||||||
|
final Fragment fragment = pageAdapter.getItem(commentsTabPos);
|
||||||
|
if (!(fragment instanceof CommentsFragment)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// unexpand the app bar only if scrolling to the comment succeeded
|
||||||
|
if (((CommentsFragment) fragment).scrollToComment(comment)) {
|
||||||
|
binding.appBarLayout.setExpanded(false, false);
|
||||||
|
binding.viewPager.setCurrentItem(commentsTabPos, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
// Play Utils
|
// Play Utils
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
@ -1430,7 +1445,7 @@ public final class VideoDetailFragment
|
||||||
super.showLoading();
|
super.showLoading();
|
||||||
|
|
||||||
//if data is already cached, transition from VISIBLE -> INVISIBLE -> VISIBLE is not required
|
//if data is already cached, transition from VISIBLE -> INVISIBLE -> VISIBLE is not required
|
||||||
if (!ExtractorHelper.isCached(serviceId, url, InfoItem.InfoType.STREAM)) {
|
if (!ExtractorHelper.isCached(serviceId, url, InfoCache.Type.STREAM)) {
|
||||||
binding.detailContentRootHiding.setVisibility(View.INVISIBLE);
|
binding.detailContentRootHiding.setVisibility(View.INVISIBLE);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -231,6 +231,8 @@ public abstract class BaseListInfoFragment<I extends InfoItem, L extends ListInf
|
||||||
if (!result.getRelatedItems().isEmpty()) {
|
if (!result.getRelatedItems().isEmpty()) {
|
||||||
infoListAdapter.addInfoItemList(result.getRelatedItems());
|
infoListAdapter.addInfoItemList(result.getRelatedItems());
|
||||||
showListFooter(hasMoreItems());
|
showListFooter(hasMoreItems());
|
||||||
|
} else if (hasMoreItems()) {
|
||||||
|
loadMoreItems();
|
||||||
} else {
|
} else {
|
||||||
infoListAdapter.clearStreamItemList();
|
infoListAdapter.clearStreamItemList();
|
||||||
showEmptyState();
|
showEmptyState();
|
||||||
|
|
|
@ -2,12 +2,12 @@ package org.schabi.newpipe.fragments.list.channel;
|
||||||
|
|
||||||
import static org.schabi.newpipe.extractor.stream.StreamExtractor.UNKNOWN_SUBSCRIBER_COUNT;
|
import static org.schabi.newpipe.extractor.stream.StreamExtractor.UNKNOWN_SUBSCRIBER_COUNT;
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.widget.LinearLayout;
|
import android.widget.LinearLayout;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
import org.schabi.newpipe.R;
|
import org.schabi.newpipe.R;
|
||||||
|
@ -26,14 +26,12 @@ public class ChannelAboutFragment extends BaseDescriptionFragment {
|
||||||
@State
|
@State
|
||||||
protected ChannelInfo channelInfo;
|
protected ChannelInfo channelInfo;
|
||||||
|
|
||||||
public static ChannelAboutFragment getInstance(final ChannelInfo channelInfo) {
|
ChannelAboutFragment(@NonNull final ChannelInfo channelInfo) {
|
||||||
final ChannelAboutFragment fragment = new ChannelAboutFragment();
|
this.channelInfo = channelInfo;
|
||||||
fragment.channelInfo = channelInfo;
|
|
||||||
return fragment;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public ChannelAboutFragment() {
|
public ChannelAboutFragment() {
|
||||||
super();
|
// keep empty constructor for IcePick when resuming fragment from memory
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -45,26 +43,17 @@ public class ChannelAboutFragment extends BaseDescriptionFragment {
|
||||||
@Nullable
|
@Nullable
|
||||||
@Override
|
@Override
|
||||||
protected Description getDescription() {
|
protected Description getDescription() {
|
||||||
if (channelInfo == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return new Description(channelInfo.getDescription(), Description.PLAIN_TEXT);
|
return new Description(channelInfo.getDescription(), Description.PLAIN_TEXT);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nullable
|
@NonNull
|
||||||
@Override
|
@Override
|
||||||
protected StreamingService getService() {
|
protected StreamingService getService() {
|
||||||
if (channelInfo == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return channelInfo.getService();
|
return channelInfo.getService();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected int getServiceId() {
|
protected int getServiceId() {
|
||||||
if (channelInfo == null) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
return channelInfo.getServiceId();
|
return channelInfo.getServiceId();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -74,12 +63,9 @@ public class ChannelAboutFragment extends BaseDescriptionFragment {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nullable
|
@NonNull
|
||||||
@Override
|
@Override
|
||||||
public List<String> getTags() {
|
public List<String> getTags() {
|
||||||
if (channelInfo == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return channelInfo.getTags();
|
return channelInfo.getTags();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -93,10 +79,11 @@ public class ChannelAboutFragment extends BaseDescriptionFragment {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
final Context context = getContext();
|
|
||||||
if (channelInfo.getSubscriberCount() != UNKNOWN_SUBSCRIBER_COUNT) {
|
if (channelInfo.getSubscriberCount() != UNKNOWN_SUBSCRIBER_COUNT) {
|
||||||
addMetadataItem(inflater, layout, false, R.string.metadata_subscribers,
|
addMetadataItem(inflater, layout, false, R.string.metadata_subscribers,
|
||||||
Localization.localizeNumber(context, channelInfo.getSubscriberCount()));
|
Localization.localizeNumber(
|
||||||
|
requireContext(),
|
||||||
|
channelInfo.getSubscriberCount()));
|
||||||
}
|
}
|
||||||
|
|
||||||
addImagesMetadataItem(inflater, layout, R.string.metadata_avatars,
|
addImagesMetadataItem(inflater, layout, R.string.metadata_avatars,
|
||||||
|
|
|
@ -474,7 +474,7 @@ public class ChannelFragment extends BaseStateFragment<ChannelInfo>
|
||||||
if (ChannelTabHelper.showChannelTab(
|
if (ChannelTabHelper.showChannelTab(
|
||||||
context, preferences, R.string.show_channel_tabs_about)) {
|
context, preferences, R.string.show_channel_tabs_about)) {
|
||||||
tabAdapter.addFragment(
|
tabAdapter.addFragment(
|
||||||
ChannelAboutFragment.getInstance(currentInfo),
|
new ChannelAboutFragment(currentInfo),
|
||||||
context.getString(R.string.channel_tab_about));
|
context.getString(R.string.channel_tab_about));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,170 @@
|
||||||
|
package org.schabi.newpipe.fragments.list.comments;
|
||||||
|
|
||||||
|
import static org.schabi.newpipe.util.ServiceHelper.getServiceById;
|
||||||
|
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.constraintlayout.widget.ConstraintLayout;
|
||||||
|
import androidx.core.text.HtmlCompat;
|
||||||
|
|
||||||
|
import org.schabi.newpipe.R;
|
||||||
|
import org.schabi.newpipe.databinding.CommentRepliesHeaderBinding;
|
||||||
|
import org.schabi.newpipe.error.UserAction;
|
||||||
|
import org.schabi.newpipe.extractor.ListExtractor;
|
||||||
|
import org.schabi.newpipe.extractor.comments.CommentsInfoItem;
|
||||||
|
import org.schabi.newpipe.fragments.list.BaseListInfoFragment;
|
||||||
|
import org.schabi.newpipe.info_list.ItemViewMode;
|
||||||
|
import org.schabi.newpipe.util.DeviceUtils;
|
||||||
|
import org.schabi.newpipe.util.ExtractorHelper;
|
||||||
|
import org.schabi.newpipe.util.Localization;
|
||||||
|
import org.schabi.newpipe.util.NavigationHelper;
|
||||||
|
import org.schabi.newpipe.util.image.ImageStrategy;
|
||||||
|
import org.schabi.newpipe.util.image.PicassoHelper;
|
||||||
|
import org.schabi.newpipe.util.text.TextLinkifier;
|
||||||
|
|
||||||
|
import java.util.Queue;
|
||||||
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
|
import icepick.State;
|
||||||
|
import io.reactivex.rxjava3.core.Single;
|
||||||
|
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
||||||
|
|
||||||
|
public final class CommentRepliesFragment
|
||||||
|
extends BaseListInfoFragment<CommentsInfoItem, CommentRepliesInfo> {
|
||||||
|
|
||||||
|
public static final String TAG = CommentRepliesFragment.class.getSimpleName();
|
||||||
|
|
||||||
|
@State
|
||||||
|
CommentsInfoItem commentsInfoItem; // the comment to show replies of
|
||||||
|
private final CompositeDisposable disposables = new CompositeDisposable();
|
||||||
|
|
||||||
|
|
||||||
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
|
// Constructors and lifecycle
|
||||||
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
|
// only called by the Android framework, after which readFrom is called and restores all data
|
||||||
|
public CommentRepliesFragment() {
|
||||||
|
super(UserAction.REQUESTED_COMMENT_REPLIES);
|
||||||
|
}
|
||||||
|
|
||||||
|
public CommentRepliesFragment(@NonNull final CommentsInfoItem commentsInfoItem) {
|
||||||
|
this();
|
||||||
|
this.commentsInfoItem = commentsInfoItem;
|
||||||
|
// setting "" as title since the title will be properly set right after
|
||||||
|
setInitialData(commentsInfoItem.getServiceId(), commentsInfoItem.getUrl(), "");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
@Override
|
||||||
|
public View onCreateView(@NonNull final LayoutInflater inflater,
|
||||||
|
@Nullable final ViewGroup container,
|
||||||
|
@Nullable final Bundle savedInstanceState) {
|
||||||
|
return inflater.inflate(R.layout.fragment_comments, container, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDestroyView() {
|
||||||
|
disposables.clear();
|
||||||
|
super.onDestroyView();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Supplier<View> getListHeaderSupplier() {
|
||||||
|
return () -> {
|
||||||
|
final CommentRepliesHeaderBinding binding = CommentRepliesHeaderBinding
|
||||||
|
.inflate(activity.getLayoutInflater(), itemsList, false);
|
||||||
|
final CommentsInfoItem item = commentsInfoItem;
|
||||||
|
|
||||||
|
// load the author avatar
|
||||||
|
PicassoHelper.loadAvatar(item.getUploaderAvatars()).into(binding.authorAvatar);
|
||||||
|
binding.authorAvatar.setVisibility(ImageStrategy.shouldLoadImages()
|
||||||
|
? View.VISIBLE : View.GONE);
|
||||||
|
|
||||||
|
// setup author name and comment date
|
||||||
|
binding.authorName.setText(item.getUploaderName());
|
||||||
|
binding.uploadDate.setText(Localization.relativeTimeOrTextual(
|
||||||
|
getContext(), item.getUploadDate(), item.getTextualUploadDate()));
|
||||||
|
binding.authorTouchArea.setOnClickListener(
|
||||||
|
v -> NavigationHelper.openCommentAuthorIfPresent(requireActivity(), item));
|
||||||
|
|
||||||
|
// setup like count, hearted and pinned
|
||||||
|
binding.thumbsUpCount.setText(
|
||||||
|
Localization.likeCount(requireContext(), item.getLikeCount()));
|
||||||
|
// for heartImage goneMarginEnd was used, but there is no way to tell ConstraintLayout
|
||||||
|
// not to use a different margin only when both the next two views are gone
|
||||||
|
((ConstraintLayout.LayoutParams) binding.thumbsUpCount.getLayoutParams())
|
||||||
|
.setMarginEnd(DeviceUtils.dpToPx(
|
||||||
|
(item.isHeartedByUploader() || item.isPinned() ? 8 : 16),
|
||||||
|
requireContext()));
|
||||||
|
binding.heartImage.setVisibility(item.isHeartedByUploader() ? View.VISIBLE : View.GONE);
|
||||||
|
binding.pinnedImage.setVisibility(item.isPinned() ? View.VISIBLE : View.GONE);
|
||||||
|
|
||||||
|
// setup comment content
|
||||||
|
TextLinkifier.fromDescription(binding.commentContent, item.getCommentText(),
|
||||||
|
HtmlCompat.FROM_HTML_MODE_LEGACY, getServiceById(item.getServiceId()),
|
||||||
|
item.getUrl(), disposables, null);
|
||||||
|
|
||||||
|
return binding.getRoot();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
|
// State saving
|
||||||
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void writeTo(final Queue<Object> objectsToSave) {
|
||||||
|
super.writeTo(objectsToSave);
|
||||||
|
objectsToSave.add(commentsInfoItem);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void readFrom(@NonNull final Queue<Object> savedObjects) throws Exception {
|
||||||
|
super.readFrom(savedObjects);
|
||||||
|
commentsInfoItem = (CommentsInfoItem) savedObjects.poll();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
|
// Data loading
|
||||||
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Single<CommentRepliesInfo> loadResult(final boolean forceLoad) {
|
||||||
|
return Single.fromCallable(() -> new CommentRepliesInfo(commentsInfoItem,
|
||||||
|
// the reply count string will be shown as the activity title
|
||||||
|
Localization.replyCount(requireContext(), commentsInfoItem.getReplyCount())));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Single<ListExtractor.InfoItemsPage<CommentsInfoItem>> loadMoreItemsLogic() {
|
||||||
|
// commentsInfoItem.getUrl() should contain the url of the original
|
||||||
|
// ListInfo<CommentsInfoItem>, which should be the stream url
|
||||||
|
return ExtractorHelper.getMoreCommentItems(
|
||||||
|
serviceId, commentsInfoItem.getUrl(), currentNextPage);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
|
// Utils
|
||||||
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected ItemViewMode getItemViewMode() {
|
||||||
|
return ItemViewMode.LIST;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return the comment to which the replies are shown
|
||||||
|
*/
|
||||||
|
public CommentsInfoItem getCommentsInfoItem() {
|
||||||
|
return commentsInfoItem;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,22 @@
|
||||||
|
package org.schabi.newpipe.fragments.list.comments;
|
||||||
|
|
||||||
|
import org.schabi.newpipe.extractor.ListInfo;
|
||||||
|
import org.schabi.newpipe.extractor.comments.CommentsInfoItem;
|
||||||
|
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
|
||||||
|
public final class CommentRepliesInfo extends ListInfo<CommentsInfoItem> {
|
||||||
|
/**
|
||||||
|
* This class is used to wrap the comment replies page into a ListInfo object.
|
||||||
|
*
|
||||||
|
* @param comment the comment from which to get replies
|
||||||
|
* @param name will be shown as the fragment title
|
||||||
|
*/
|
||||||
|
public CommentRepliesInfo(final CommentsInfoItem comment, final String name) {
|
||||||
|
super(comment.getServiceId(),
|
||||||
|
new ListLinkHandler("", "", "", Collections.emptyList(), null), name);
|
||||||
|
setNextPage(comment.getReplies());
|
||||||
|
setRelatedItems(Collections.emptyList()); // since it must be non-null
|
||||||
|
}
|
||||||
|
}
|
|
@ -110,4 +110,14 @@ public class CommentsFragment extends BaseListInfoFragment<CommentsInfoItem, Com
|
||||||
protected ItemViewMode getItemViewMode() {
|
protected ItemViewMode getItemViewMode() {
|
||||||
return ItemViewMode.LIST;
|
return ItemViewMode.LIST;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean scrollToComment(final CommentsInfoItem comment) {
|
||||||
|
final int position = infoListAdapter.getItemsList().indexOf(comment);
|
||||||
|
if (position < 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
itemsList.scrollToPosition(position);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
package org.schabi.newpipe.fragments.list.playlist;
|
package org.schabi.newpipe.fragments.list.playlist;
|
||||||
|
|
||||||
|
import static org.schabi.newpipe.extractor.utils.Utils.isBlank;
|
||||||
import static org.schabi.newpipe.ktx.ViewUtils.animate;
|
import static org.schabi.newpipe.ktx.ViewUtils.animate;
|
||||||
import static org.schabi.newpipe.ktx.ViewUtils.animateHideRecyclerViewAllowingScrolling;
|
import static org.schabi.newpipe.ktx.ViewUtils.animateHideRecyclerViewAllowingScrolling;
|
||||||
|
import static org.schabi.newpipe.util.ServiceHelper.getServiceById;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
@ -37,6 +39,7 @@ import org.schabi.newpipe.extractor.ListExtractor;
|
||||||
import org.schabi.newpipe.extractor.ServiceList;
|
import org.schabi.newpipe.extractor.ServiceList;
|
||||||
import org.schabi.newpipe.extractor.playlist.PlaylistInfo;
|
import org.schabi.newpipe.extractor.playlist.PlaylistInfo;
|
||||||
import org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper;
|
import org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper;
|
||||||
|
import org.schabi.newpipe.extractor.stream.Description;
|
||||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||||
import org.schabi.newpipe.fragments.list.BaseListInfoFragment;
|
import org.schabi.newpipe.fragments.list.BaseListInfoFragment;
|
||||||
import org.schabi.newpipe.info_list.dialog.InfoItemDialog;
|
import org.schabi.newpipe.info_list.dialog.InfoItemDialog;
|
||||||
|
@ -48,9 +51,10 @@ import org.schabi.newpipe.player.playqueue.PlaylistPlayQueue;
|
||||||
import org.schabi.newpipe.util.ExtractorHelper;
|
import org.schabi.newpipe.util.ExtractorHelper;
|
||||||
import org.schabi.newpipe.util.Localization;
|
import org.schabi.newpipe.util.Localization;
|
||||||
import org.schabi.newpipe.util.NavigationHelper;
|
import org.schabi.newpipe.util.NavigationHelper;
|
||||||
import org.schabi.newpipe.util.image.PicassoHelper;
|
|
||||||
import org.schabi.newpipe.util.external_communication.ShareUtils;
|
|
||||||
import org.schabi.newpipe.util.PlayButtonHelper;
|
import org.schabi.newpipe.util.PlayButtonHelper;
|
||||||
|
import org.schabi.newpipe.util.external_communication.ShareUtils;
|
||||||
|
import org.schabi.newpipe.util.image.PicassoHelper;
|
||||||
|
import org.schabi.newpipe.util.text.TextEllipsizer;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
@ -85,6 +89,9 @@ public class PlaylistFragment extends BaseListInfoFragment<StreamInfoItem, Playl
|
||||||
|
|
||||||
private MenuItem playlistBookmarkButton;
|
private MenuItem playlistBookmarkButton;
|
||||||
|
|
||||||
|
private long streamCount;
|
||||||
|
private long playlistOverallDurationSeconds;
|
||||||
|
|
||||||
public static PlaylistFragment getInstance(final int serviceId, final String url,
|
public static PlaylistFragment getInstance(final int serviceId, final String url,
|
||||||
final String name) {
|
final String name) {
|
||||||
final PlaylistFragment instance = new PlaylistFragment();
|
final PlaylistFragment instance = new PlaylistFragment();
|
||||||
|
@ -273,6 +280,12 @@ public class PlaylistFragment extends BaseListInfoFragment<StreamInfoItem, Playl
|
||||||
animate(headerBinding.uploaderLayout, false, 200);
|
animate(headerBinding.uploaderLayout, false, 200);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void handleNextItems(final ListExtractor.InfoItemsPage result) {
|
||||||
|
super.handleNextItems(result);
|
||||||
|
setStreamCountAndOverallDuration(result.getItems(), !result.hasNextPage());
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void handleResult(@NonNull final PlaylistInfo result) {
|
public void handleResult(@NonNull final PlaylistInfo result) {
|
||||||
super.handleResult(result);
|
super.handleResult(result);
|
||||||
|
@ -318,8 +331,31 @@ public class PlaylistFragment extends BaseListInfoFragment<StreamInfoItem, Playl
|
||||||
.into(headerBinding.uploaderAvatarView);
|
.into(headerBinding.uploaderAvatarView);
|
||||||
}
|
}
|
||||||
|
|
||||||
headerBinding.playlistStreamCount.setText(Localization
|
streamCount = result.getStreamCount();
|
||||||
.localizeStreamCount(getContext(), result.getStreamCount()));
|
setStreamCountAndOverallDuration(result.getRelatedItems(), !result.hasNextPage());
|
||||||
|
|
||||||
|
final Description description = result.getDescription();
|
||||||
|
if (description != null && description != Description.EMPTY_DESCRIPTION
|
||||||
|
&& !isBlank(description.getContent())) {
|
||||||
|
final TextEllipsizer ellipsizer = new TextEllipsizer(
|
||||||
|
headerBinding.playlistDescription, 5, getServiceById(result.getServiceId()));
|
||||||
|
ellipsizer.setStateChangeListener(isEllipsized ->
|
||||||
|
headerBinding.playlistDescriptionReadMore.setText(
|
||||||
|
Boolean.TRUE.equals(isEllipsized) ? R.string.show_more : R.string.show_less
|
||||||
|
));
|
||||||
|
ellipsizer.setOnContentChanged(canBeEllipsized -> {
|
||||||
|
headerBinding.playlistDescriptionReadMore.setVisibility(
|
||||||
|
Boolean.TRUE.equals(canBeEllipsized) ? View.VISIBLE : View.GONE);
|
||||||
|
if (Boolean.TRUE.equals(canBeEllipsized)) {
|
||||||
|
ellipsizer.ellipsize();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
ellipsizer.setContent(description);
|
||||||
|
headerBinding.playlistDescriptionReadMore.setOnClickListener(v -> ellipsizer.toggle());
|
||||||
|
} else {
|
||||||
|
headerBinding.playlistDescription.setVisibility(View.GONE);
|
||||||
|
headerBinding.playlistDescriptionReadMore.setVisibility(View.GONE);
|
||||||
|
}
|
||||||
|
|
||||||
if (!result.getErrors().isEmpty()) {
|
if (!result.getErrors().isEmpty()) {
|
||||||
showSnackBarError(new ErrorInfo(result.getErrors(), UserAction.REQUESTED_PLAYLIST,
|
showSnackBarError(new ErrorInfo(result.getErrors(), UserAction.REQUESTED_PLAYLIST,
|
||||||
|
@ -459,4 +495,20 @@ public class PlaylistFragment extends BaseListInfoFragment<StreamInfoItem, Playl
|
||||||
playlistBookmarkButton.setIcon(drawable);
|
playlistBookmarkButton.setIcon(drawable);
|
||||||
playlistBookmarkButton.setTitle(titleRes);
|
playlistBookmarkButton.setTitle(titleRes);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void setStreamCountAndOverallDuration(final List<StreamInfoItem> list,
|
||||||
|
final boolean isDurationComplete) {
|
||||||
|
if (activity != null && headerBinding != null) {
|
||||||
|
playlistOverallDurationSeconds += list.stream()
|
||||||
|
.mapToLong(x -> x.getDuration())
|
||||||
|
.sum();
|
||||||
|
headerBinding.playlistStreamCount.setText(
|
||||||
|
Localization.concatenateStrings(
|
||||||
|
Localization.localizeStreamCount(activity, streamCount),
|
||||||
|
Localization.getDurationString(playlistOverallDurationSeconds,
|
||||||
|
isDurationComplete, true))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package org.schabi.newpipe.fragments.list.search;
|
package org.schabi.newpipe.fragments.list.search;
|
||||||
|
|
||||||
import static androidx.recyclerview.widget.ItemTouchHelper.Callback.makeMovementFlags;
|
import static androidx.recyclerview.widget.ItemTouchHelper.Callback.makeMovementFlags;
|
||||||
|
import static org.schabi.newpipe.extractor.utils.Utils.isBlank;
|
||||||
import static org.schabi.newpipe.ktx.ViewUtils.animate;
|
import static org.schabi.newpipe.ktx.ViewUtils.animate;
|
||||||
import static org.schabi.newpipe.util.ExtractorHelper.showMetaInfoInTextView;
|
import static org.schabi.newpipe.util.ExtractorHelper.showMetaInfoInTextView;
|
||||||
import static java.util.Arrays.asList;
|
import static java.util.Arrays.asList;
|
||||||
|
@ -389,7 +390,7 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
||||||
@Override
|
@Override
|
||||||
public void onSaveInstanceState(@NonNull final Bundle bundle) {
|
public void onSaveInstanceState(@NonNull final Bundle bundle) {
|
||||||
searchString = searchEditText != null
|
searchString = searchEditText != null
|
||||||
? searchEditText.getText().toString()
|
? getSearchEditString().trim()
|
||||||
: searchString;
|
: searchString;
|
||||||
super.onSaveInstanceState(bundle);
|
super.onSaveInstanceState(bundle);
|
||||||
}
|
}
|
||||||
|
@ -400,11 +401,11 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void reloadContent() {
|
public void reloadContent() {
|
||||||
if (!TextUtils.isEmpty(searchString)
|
if (!TextUtils.isEmpty(searchString) || (searchEditText != null
|
||||||
|| (searchEditText != null && !TextUtils.isEmpty(searchEditText.getText()))) {
|
&& !isSearchEditBlank())) {
|
||||||
search(!TextUtils.isEmpty(searchString)
|
search(!TextUtils.isEmpty(searchString)
|
||||||
? searchString
|
? searchString
|
||||||
: searchEditText.getText().toString(), this.contentFilter, "");
|
: getSearchEditString(), this.contentFilter, "");
|
||||||
} else {
|
} else {
|
||||||
if (searchEditText != null) {
|
if (searchEditText != null) {
|
||||||
searchEditText.setText("");
|
searchEditText.setText("");
|
||||||
|
@ -498,7 +499,8 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
||||||
}
|
}
|
||||||
searchEditText.setText(searchString);
|
searchEditText.setText(searchString);
|
||||||
|
|
||||||
if (TextUtils.isEmpty(searchString) || TextUtils.isEmpty(searchEditText.getText())) {
|
if (TextUtils.isEmpty(searchString)
|
||||||
|
|| isSearchEditBlank()) {
|
||||||
searchToolbarContainer.setTranslationX(100);
|
searchToolbarContainer.setTranslationX(100);
|
||||||
searchToolbarContainer.setAlpha(0.0f);
|
searchToolbarContainer.setAlpha(0.0f);
|
||||||
searchToolbarContainer.setVisibility(View.VISIBLE);
|
searchToolbarContainer.setVisibility(View.VISIBLE);
|
||||||
|
@ -522,7 +524,7 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
||||||
if (DEBUG) {
|
if (DEBUG) {
|
||||||
Log.d(TAG, "onClick() called with: v = [" + v + "]");
|
Log.d(TAG, "onClick() called with: v = [" + v + "]");
|
||||||
}
|
}
|
||||||
if (TextUtils.isEmpty(searchEditText.getText())) {
|
if (isSearchEditBlank()) {
|
||||||
NavigationHelper.gotoMainFragment(getFM());
|
NavigationHelper.gotoMainFragment(getFM());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -603,7 +605,7 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
||||||
s.removeSpan(span);
|
s.removeSpan(span);
|
||||||
}
|
}
|
||||||
|
|
||||||
final String newText = searchEditText.getText().toString();
|
final String newText = getSearchEditString().trim();
|
||||||
suggestionPublisher.onNext(newText);
|
suggestionPublisher.onNext(newText);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -619,7 +621,8 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
||||||
} else if (event != null
|
} else if (event != null
|
||||||
&& (event.getKeyCode() == KeyEvent.KEYCODE_ENTER
|
&& (event.getKeyCode() == KeyEvent.KEYCODE_ENTER
|
||||||
|| event.getAction() == EditorInfo.IME_ACTION_SEARCH)) {
|
|| event.getAction() == EditorInfo.IME_ACTION_SEARCH)) {
|
||||||
search(searchEditText.getText().toString(), new String[0], "");
|
searchEditText.setText(getSearchEditString().trim());
|
||||||
|
search(getSearchEditString(), new String[0], "");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
|
@ -694,7 +697,7 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.subscribe(
|
.subscribe(
|
||||||
howManyDeleted -> suggestionPublisher
|
howManyDeleted -> suggestionPublisher
|
||||||
.onNext(searchEditText.getText().toString()),
|
.onNext(getSearchEditString()),
|
||||||
throwable -> showSnackBarError(new ErrorInfo(throwable,
|
throwable -> showSnackBarError(new ErrorInfo(throwable,
|
||||||
UserAction.DELETE_FROM_HISTORY,
|
UserAction.DELETE_FROM_HISTORY,
|
||||||
"Deleting item failed")));
|
"Deleting item failed")));
|
||||||
|
@ -723,9 +726,9 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
||||||
.getRelatedSearches(query, similarQueryLimit, 25)
|
.getRelatedSearches(query, similarQueryLimit, 25)
|
||||||
.toObservable()
|
.toObservable()
|
||||||
.map(searchHistoryEntries ->
|
.map(searchHistoryEntries ->
|
||||||
searchHistoryEntries.stream()
|
searchHistoryEntries.stream()
|
||||||
.map(entry -> new SuggestionItem(true, entry))
|
.map(entry -> new SuggestionItem(true, entry))
|
||||||
.collect(Collectors.toList()));
|
.collect(Collectors.toList()));
|
||||||
}
|
}
|
||||||
|
|
||||||
private Observable<List<SuggestionItem>> getRemoteSuggestionsObservable(final String query) {
|
private Observable<List<SuggestionItem>> getRemoteSuggestionsObservable(final String query) {
|
||||||
|
@ -792,12 +795,12 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
||||||
} else if (listNotification.isOnError()
|
} else if (listNotification.isOnError()
|
||||||
&& listNotification.getError() != null
|
&& listNotification.getError() != null
|
||||||
&& !ExceptionUtils.isInterruptedCaused(
|
&& !ExceptionUtils.isInterruptedCaused(
|
||||||
listNotification.getError())) {
|
listNotification.getError())) {
|
||||||
showSnackBarError(new ErrorInfo(listNotification.getError(),
|
showSnackBarError(new ErrorInfo(listNotification.getError(),
|
||||||
UserAction.GET_SUGGESTIONS, searchString, serviceId));
|
UserAction.GET_SUGGESTIONS, searchString, serviceId));
|
||||||
}
|
}
|
||||||
}, throwable -> showSnackBarError(new ErrorInfo(
|
}, throwable -> showSnackBarError(new ErrorInfo(
|
||||||
throwable, UserAction.GET_SUGGESTIONS, searchString, serviceId)));
|
throwable, UserAction.GET_SUGGESTIONS, searchString, serviceId)));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -805,7 +808,13 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
||||||
// no-op
|
// no-op
|
||||||
}
|
}
|
||||||
|
|
||||||
private void search(final String theSearchString,
|
/**
|
||||||
|
* Perform a search.
|
||||||
|
* @param theSearchString the trimmed search string
|
||||||
|
* @param theContentFilter the content filter to use. FIXME: unused param
|
||||||
|
* @param theSortFilter FIXME: unused param
|
||||||
|
*/
|
||||||
|
private void search(@NonNull final String theSearchString,
|
||||||
final String[] theContentFilter,
|
final String[] theContentFilter,
|
||||||
final String theSortFilter) {
|
final String theSortFilter) {
|
||||||
if (DEBUG) {
|
if (DEBUG) {
|
||||||
|
@ -815,25 +824,26 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if theSearchString is a URL which can be opened by NewPipe directly
|
||||||
|
// and open it if possible.
|
||||||
try {
|
try {
|
||||||
final StreamingService streamingService = NewPipe.getServiceByUrl(theSearchString);
|
final StreamingService streamingService = NewPipe.getServiceByUrl(theSearchString);
|
||||||
if (streamingService != null) {
|
showLoading();
|
||||||
showLoading();
|
disposables.add(Observable
|
||||||
disposables.add(Observable
|
.fromCallable(() -> NavigationHelper.getIntentByLink(activity,
|
||||||
.fromCallable(() -> NavigationHelper.getIntentByLink(activity,
|
streamingService, theSearchString))
|
||||||
streamingService, theSearchString))
|
.subscribeOn(Schedulers.io())
|
||||||
.subscribeOn(Schedulers.io())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.subscribe(intent -> {
|
||||||
.subscribe(intent -> {
|
getFM().popBackStackImmediate();
|
||||||
getFM().popBackStackImmediate();
|
activity.startActivity(intent);
|
||||||
activity.startActivity(intent);
|
}, throwable -> showTextError(getString(R.string.unsupported_url))));
|
||||||
}, throwable -> showTextError(getString(R.string.unsupported_url))));
|
return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
} catch (final Exception ignored) {
|
} catch (final Exception ignored) {
|
||||||
// Exception occurred, it's not a url
|
// Exception occurred, it's not a url
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// prepare search
|
||||||
lastSearchedString = this.searchString;
|
lastSearchedString = this.searchString;
|
||||||
this.searchString = theSearchString;
|
this.searchString = theSearchString;
|
||||||
infoListAdapter.clearStreamItemList();
|
infoListAdapter.clearStreamItemList();
|
||||||
|
@ -842,13 +852,17 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
||||||
searchBinding.searchMetaInfoSeparator, disposables);
|
searchBinding.searchMetaInfoSeparator, disposables);
|
||||||
hideKeyboardSearch();
|
hideKeyboardSearch();
|
||||||
|
|
||||||
|
// store search query if search history is enabled
|
||||||
disposables.add(historyRecordManager.onSearched(serviceId, theSearchString)
|
disposables.add(historyRecordManager.onSearched(serviceId, theSearchString)
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.subscribe(
|
.subscribe(
|
||||||
ignored -> { },
|
ignored -> {
|
||||||
|
},
|
||||||
throwable -> showSnackBarError(new ErrorInfo(throwable, UserAction.SEARCHED,
|
throwable -> showSnackBarError(new ErrorInfo(throwable, UserAction.SEARCHED,
|
||||||
theSearchString, serviceId))
|
theSearchString, serviceId))
|
||||||
));
|
));
|
||||||
|
|
||||||
|
// load search results
|
||||||
suggestionPublisher.onNext(theSearchString);
|
suggestionPublisher.onNext(theSearchString);
|
||||||
startLoading(false);
|
startLoading(false);
|
||||||
}
|
}
|
||||||
|
@ -938,6 +952,14 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
||||||
sortFilter = theSortFilter;
|
sortFilter = theSortFilter;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String getSearchEditString() {
|
||||||
|
return searchEditText.getText().toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isSearchEditBlank() {
|
||||||
|
return isBlank(getSearchEditString());
|
||||||
|
}
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
// Suggestion Results
|
// Suggestion Results
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
@ -979,6 +1001,9 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
||||||
}
|
}
|
||||||
|
|
||||||
searchSuggestion = result.getSearchSuggestion();
|
searchSuggestion = result.getSearchSuggestion();
|
||||||
|
if (searchSuggestion != null) {
|
||||||
|
searchSuggestion = searchSuggestion.trim();
|
||||||
|
}
|
||||||
isCorrectedSearch = result.isCorrectedSearch();
|
isCorrectedSearch = result.isCorrectedSearch();
|
||||||
|
|
||||||
// List<MetaInfo> cannot be bundled without creating some containers
|
// List<MetaInfo> cannot be bundled without creating some containers
|
||||||
|
@ -1080,7 +1105,7 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.subscribe(
|
.subscribe(
|
||||||
howManyDeleted -> suggestionPublisher
|
howManyDeleted -> suggestionPublisher
|
||||||
.onNext(searchEditText.getText().toString()),
|
.onNext(getSearchEditString()),
|
||||||
throwable -> showSnackBarError(new ErrorInfo(throwable,
|
throwable -> showSnackBarError(new ErrorInfo(throwable,
|
||||||
UserAction.DELETE_FROM_HISTORY, "Deleting item failed")));
|
UserAction.DELETE_FROM_HISTORY, "Deleting item failed")));
|
||||||
disposables.add(onDelete);
|
disposables.add(onDelete);
|
||||||
|
|
|
@ -21,18 +21,17 @@ import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||||
import org.schabi.newpipe.fragments.list.BaseListInfoFragment;
|
import org.schabi.newpipe.fragments.list.BaseListInfoFragment;
|
||||||
import org.schabi.newpipe.info_list.ItemViewMode;
|
import org.schabi.newpipe.info_list.ItemViewMode;
|
||||||
import org.schabi.newpipe.ktx.ViewUtils;
|
import org.schabi.newpipe.ktx.ViewUtils;
|
||||||
import org.schabi.newpipe.util.RelatedItemInfo;
|
|
||||||
|
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
import java.util.function.Supplier;
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
import io.reactivex.rxjava3.core.Single;
|
import io.reactivex.rxjava3.core.Single;
|
||||||
|
|
||||||
public class RelatedItemsFragment extends BaseListInfoFragment<InfoItem, RelatedItemInfo>
|
public class RelatedItemsFragment extends BaseListInfoFragment<InfoItem, RelatedItemsInfo>
|
||||||
implements SharedPreferences.OnSharedPreferenceChangeListener {
|
implements SharedPreferences.OnSharedPreferenceChangeListener {
|
||||||
private static final String INFO_KEY = "related_info_key";
|
private static final String INFO_KEY = "related_info_key";
|
||||||
|
|
||||||
private RelatedItemInfo relatedItemInfo;
|
private RelatedItemsInfo relatedItemsInfo;
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
// Views
|
// Views
|
||||||
|
@ -69,7 +68,7 @@ public class RelatedItemsFragment extends BaseListInfoFragment<InfoItem, Related
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected Supplier<View> getListHeaderSupplier() {
|
protected Supplier<View> getListHeaderSupplier() {
|
||||||
if (relatedItemInfo == null || relatedItemInfo.getRelatedItems() == null) {
|
if (relatedItemsInfo == null || relatedItemsInfo.getRelatedItems() == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -97,8 +96,8 @@ public class RelatedItemsFragment extends BaseListInfoFragment<InfoItem, Related
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected Single<RelatedItemInfo> loadResult(final boolean forceLoad) {
|
protected Single<RelatedItemsInfo> loadResult(final boolean forceLoad) {
|
||||||
return Single.fromCallable(() -> relatedItemInfo);
|
return Single.fromCallable(() -> relatedItemsInfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -110,7 +109,7 @@ public class RelatedItemsFragment extends BaseListInfoFragment<InfoItem, Related
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void handleResult(@NonNull final RelatedItemInfo result) {
|
public void handleResult(@NonNull final RelatedItemsInfo result) {
|
||||||
super.handleResult(result);
|
super.handleResult(result);
|
||||||
|
|
||||||
if (headerBinding != null) {
|
if (headerBinding != null) {
|
||||||
|
@ -137,23 +136,23 @@ public class RelatedItemsFragment extends BaseListInfoFragment<InfoItem, Related
|
||||||
|
|
||||||
private void setInitialData(final StreamInfo info) {
|
private void setInitialData(final StreamInfo info) {
|
||||||
super.setInitialData(info.getServiceId(), info.getUrl(), info.getName());
|
super.setInitialData(info.getServiceId(), info.getUrl(), info.getName());
|
||||||
if (this.relatedItemInfo == null) {
|
if (this.relatedItemsInfo == null) {
|
||||||
this.relatedItemInfo = RelatedItemInfo.getInfo(info);
|
this.relatedItemsInfo = new RelatedItemsInfo(info);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onSaveInstanceState(@NonNull final Bundle outState) {
|
public void onSaveInstanceState(@NonNull final Bundle outState) {
|
||||||
super.onSaveInstanceState(outState);
|
super.onSaveInstanceState(outState);
|
||||||
outState.putSerializable(INFO_KEY, relatedItemInfo);
|
outState.putSerializable(INFO_KEY, relatedItemsInfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onRestoreInstanceState(@NonNull final Bundle savedState) {
|
protected void onRestoreInstanceState(@NonNull final Bundle savedState) {
|
||||||
super.onRestoreInstanceState(savedState);
|
super.onRestoreInstanceState(savedState);
|
||||||
final Serializable serializable = savedState.getSerializable(INFO_KEY);
|
final Serializable serializable = savedState.getSerializable(INFO_KEY);
|
||||||
if (serializable instanceof RelatedItemInfo) {
|
if (serializable instanceof RelatedItemsInfo) {
|
||||||
this.relatedItemInfo = (RelatedItemInfo) serializable;
|
this.relatedItemsInfo = (RelatedItemsInfo) serializable;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,22 @@
|
||||||
|
package org.schabi.newpipe.fragments.list.videos;
|
||||||
|
|
||||||
|
import org.schabi.newpipe.extractor.InfoItem;
|
||||||
|
import org.schabi.newpipe.extractor.ListInfo;
|
||||||
|
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler;
|
||||||
|
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
|
||||||
|
public final class RelatedItemsInfo extends ListInfo<InfoItem> {
|
||||||
|
/**
|
||||||
|
* This class is used to wrap the related items of a StreamInfo into a ListInfo object.
|
||||||
|
*
|
||||||
|
* @param info the stream info from which to get related items
|
||||||
|
*/
|
||||||
|
public RelatedItemsInfo(final StreamInfo info) {
|
||||||
|
super(info.getServiceId(), new ListLinkHandler(info.getOriginalUrl(), info.getUrl(),
|
||||||
|
info.getId(), Collections.emptyList(), null), info.getName());
|
||||||
|
setRelatedItems(new ArrayList<>(info.getRelatedItems()));
|
||||||
|
}
|
||||||
|
}
|
|
@ -13,8 +13,7 @@ import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem;
|
||||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||||
import org.schabi.newpipe.info_list.holder.ChannelInfoItemHolder;
|
import org.schabi.newpipe.info_list.holder.ChannelInfoItemHolder;
|
||||||
import org.schabi.newpipe.info_list.holder.ChannelMiniInfoItemHolder;
|
import org.schabi.newpipe.info_list.holder.ChannelMiniInfoItemHolder;
|
||||||
import org.schabi.newpipe.info_list.holder.CommentsInfoItemHolder;
|
import org.schabi.newpipe.info_list.holder.CommentInfoItemHolder;
|
||||||
import org.schabi.newpipe.info_list.holder.CommentsMiniInfoItemHolder;
|
|
||||||
import org.schabi.newpipe.info_list.holder.InfoItemHolder;
|
import org.schabi.newpipe.info_list.holder.InfoItemHolder;
|
||||||
import org.schabi.newpipe.info_list.holder.PlaylistInfoItemHolder;
|
import org.schabi.newpipe.info_list.holder.PlaylistInfoItemHolder;
|
||||||
import org.schabi.newpipe.info_list.holder.PlaylistMiniInfoItemHolder;
|
import org.schabi.newpipe.info_list.holder.PlaylistMiniInfoItemHolder;
|
||||||
|
@ -87,8 +86,7 @@ public class InfoItemBuilder {
|
||||||
return useMiniVariant ? new PlaylistMiniInfoItemHolder(this, parent)
|
return useMiniVariant ? new PlaylistMiniInfoItemHolder(this, parent)
|
||||||
: new PlaylistInfoItemHolder(this, parent);
|
: new PlaylistInfoItemHolder(this, parent);
|
||||||
case COMMENT:
|
case COMMENT:
|
||||||
return useMiniVariant ? new CommentsMiniInfoItemHolder(this, parent)
|
return new CommentInfoItemHolder(this, parent);
|
||||||
: new CommentsInfoItemHolder(this, parent);
|
|
||||||
default:
|
default:
|
||||||
throw new RuntimeException("InfoType not expected = " + infoType.name());
|
throw new RuntimeException("InfoType not expected = " + infoType.name());
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,8 +21,7 @@ import org.schabi.newpipe.info_list.holder.ChannelCardInfoItemHolder;
|
||||||
import org.schabi.newpipe.info_list.holder.ChannelGridInfoItemHolder;
|
import org.schabi.newpipe.info_list.holder.ChannelGridInfoItemHolder;
|
||||||
import org.schabi.newpipe.info_list.holder.ChannelInfoItemHolder;
|
import org.schabi.newpipe.info_list.holder.ChannelInfoItemHolder;
|
||||||
import org.schabi.newpipe.info_list.holder.ChannelMiniInfoItemHolder;
|
import org.schabi.newpipe.info_list.holder.ChannelMiniInfoItemHolder;
|
||||||
import org.schabi.newpipe.info_list.holder.CommentsInfoItemHolder;
|
import org.schabi.newpipe.info_list.holder.CommentInfoItemHolder;
|
||||||
import org.schabi.newpipe.info_list.holder.CommentsMiniInfoItemHolder;
|
|
||||||
import org.schabi.newpipe.info_list.holder.InfoItemHolder;
|
import org.schabi.newpipe.info_list.holder.InfoItemHolder;
|
||||||
import org.schabi.newpipe.info_list.holder.PlaylistCardInfoItemHolder;
|
import org.schabi.newpipe.info_list.holder.PlaylistCardInfoItemHolder;
|
||||||
import org.schabi.newpipe.info_list.holder.PlaylistGridInfoItemHolder;
|
import org.schabi.newpipe.info_list.holder.PlaylistGridInfoItemHolder;
|
||||||
|
@ -79,8 +78,7 @@ public class InfoListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolde
|
||||||
private static final int PLAYLIST_HOLDER_TYPE = 0x301;
|
private static final int PLAYLIST_HOLDER_TYPE = 0x301;
|
||||||
private static final int GRID_PLAYLIST_HOLDER_TYPE = 0x302;
|
private static final int GRID_PLAYLIST_HOLDER_TYPE = 0x302;
|
||||||
private static final int CARD_PLAYLIST_HOLDER_TYPE = 0x303;
|
private static final int CARD_PLAYLIST_HOLDER_TYPE = 0x303;
|
||||||
private static final int MINI_COMMENT_HOLDER_TYPE = 0x400;
|
private static final int COMMENT_HOLDER_TYPE = 0x400;
|
||||||
private static final int COMMENT_HOLDER_TYPE = 0x401;
|
|
||||||
|
|
||||||
private final LayoutInflater layoutInflater;
|
private final LayoutInflater layoutInflater;
|
||||||
private final InfoItemBuilder infoItemBuilder;
|
private final InfoItemBuilder infoItemBuilder;
|
||||||
|
@ -271,7 +269,7 @@ public class InfoListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolde
|
||||||
return PLAYLIST_HOLDER_TYPE;
|
return PLAYLIST_HOLDER_TYPE;
|
||||||
}
|
}
|
||||||
case COMMENT:
|
case COMMENT:
|
||||||
return useMiniVariant ? MINI_COMMENT_HOLDER_TYPE : COMMENT_HOLDER_TYPE;
|
return COMMENT_HOLDER_TYPE;
|
||||||
default:
|
default:
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
@ -320,10 +318,8 @@ public class InfoListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolde
|
||||||
return new PlaylistGridInfoItemHolder(infoItemBuilder, parent);
|
return new PlaylistGridInfoItemHolder(infoItemBuilder, parent);
|
||||||
case CARD_PLAYLIST_HOLDER_TYPE:
|
case CARD_PLAYLIST_HOLDER_TYPE:
|
||||||
return new PlaylistCardInfoItemHolder(infoItemBuilder, parent);
|
return new PlaylistCardInfoItemHolder(infoItemBuilder, parent);
|
||||||
case MINI_COMMENT_HOLDER_TYPE:
|
|
||||||
return new CommentsMiniInfoItemHolder(infoItemBuilder, parent);
|
|
||||||
case COMMENT_HOLDER_TYPE:
|
case COMMENT_HOLDER_TYPE:
|
||||||
return new CommentsInfoItemHolder(infoItemBuilder, parent);
|
return new CommentInfoItemHolder(infoItemBuilder, parent);
|
||||||
default:
|
default:
|
||||||
return new FallbackViewHolder(new View(parent.getContext()));
|
return new FallbackViewHolder(new View(parent.getContext()));
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,188 @@
|
||||||
|
package org.schabi.newpipe.info_list.holder;
|
||||||
|
|
||||||
|
import static org.schabi.newpipe.util.ServiceHelper.getServiceById;
|
||||||
|
|
||||||
|
import android.text.method.LinkMovementMethod;
|
||||||
|
import android.text.style.URLSpan;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.widget.Button;
|
||||||
|
import android.widget.ImageView;
|
||||||
|
import android.widget.RelativeLayout;
|
||||||
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.fragment.app.FragmentActivity;
|
||||||
|
|
||||||
|
import org.schabi.newpipe.R;
|
||||||
|
import org.schabi.newpipe.extractor.InfoItem;
|
||||||
|
import org.schabi.newpipe.extractor.comments.CommentsInfoItem;
|
||||||
|
import org.schabi.newpipe.info_list.InfoItemBuilder;
|
||||||
|
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
||||||
|
import org.schabi.newpipe.util.DeviceUtils;
|
||||||
|
import org.schabi.newpipe.util.Localization;
|
||||||
|
import org.schabi.newpipe.util.NavigationHelper;
|
||||||
|
import org.schabi.newpipe.util.external_communication.ShareUtils;
|
||||||
|
import org.schabi.newpipe.util.image.ImageStrategy;
|
||||||
|
import org.schabi.newpipe.util.image.PicassoHelper;
|
||||||
|
import org.schabi.newpipe.util.text.CommentTextOnTouchListener;
|
||||||
|
import org.schabi.newpipe.util.text.TextEllipsizer;
|
||||||
|
|
||||||
|
public class CommentInfoItemHolder extends InfoItemHolder {
|
||||||
|
|
||||||
|
private static final int COMMENT_DEFAULT_LINES = 2;
|
||||||
|
private final int commentHorizontalPadding;
|
||||||
|
private final int commentVerticalPadding;
|
||||||
|
|
||||||
|
private final RelativeLayout itemRoot;
|
||||||
|
private final ImageView itemThumbnailView;
|
||||||
|
private final TextView itemContentView;
|
||||||
|
private final ImageView itemThumbsUpView;
|
||||||
|
private final TextView itemLikesCountView;
|
||||||
|
private final TextView itemTitleView;
|
||||||
|
private final ImageView itemHeartView;
|
||||||
|
private final ImageView itemPinnedView;
|
||||||
|
private final Button repliesButton;
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
private final TextEllipsizer textEllipsizer;
|
||||||
|
|
||||||
|
public CommentInfoItemHolder(final InfoItemBuilder infoItemBuilder,
|
||||||
|
final ViewGroup parent) {
|
||||||
|
super(infoItemBuilder, R.layout.list_comment_item, parent);
|
||||||
|
|
||||||
|
itemRoot = itemView.findViewById(R.id.itemRoot);
|
||||||
|
itemThumbnailView = itemView.findViewById(R.id.itemThumbnailView);
|
||||||
|
itemContentView = itemView.findViewById(R.id.itemCommentContentView);
|
||||||
|
itemThumbsUpView = itemView.findViewById(R.id.detail_thumbs_up_img_view);
|
||||||
|
itemLikesCountView = itemView.findViewById(R.id.detail_thumbs_up_count_view);
|
||||||
|
itemTitleView = itemView.findViewById(R.id.itemTitleView);
|
||||||
|
itemHeartView = itemView.findViewById(R.id.detail_heart_image_view);
|
||||||
|
itemPinnedView = itemView.findViewById(R.id.detail_pinned_view);
|
||||||
|
repliesButton = itemView.findViewById(R.id.replies_button);
|
||||||
|
|
||||||
|
commentHorizontalPadding = (int) infoItemBuilder.getContext()
|
||||||
|
.getResources().getDimension(R.dimen.comments_horizontal_padding);
|
||||||
|
commentVerticalPadding = (int) infoItemBuilder.getContext()
|
||||||
|
.getResources().getDimension(R.dimen.comments_vertical_padding);
|
||||||
|
|
||||||
|
textEllipsizer = new TextEllipsizer(itemContentView, COMMENT_DEFAULT_LINES, null);
|
||||||
|
textEllipsizer.setStateChangeListener(isEllipsized -> {
|
||||||
|
if (Boolean.TRUE.equals(isEllipsized)) {
|
||||||
|
denyLinkFocus();
|
||||||
|
} else {
|
||||||
|
determineMovementMethod();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void updateFromItem(final InfoItem infoItem,
|
||||||
|
final HistoryRecordManager historyRecordManager) {
|
||||||
|
if (!(infoItem instanceof CommentsInfoItem)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final CommentsInfoItem item = (CommentsInfoItem) infoItem;
|
||||||
|
|
||||||
|
|
||||||
|
// load the author avatar
|
||||||
|
PicassoHelper.loadAvatar(item.getUploaderAvatars()).into(itemThumbnailView);
|
||||||
|
if (ImageStrategy.shouldLoadImages()) {
|
||||||
|
itemThumbnailView.setVisibility(View.VISIBLE);
|
||||||
|
itemRoot.setPadding(commentVerticalPadding, commentVerticalPadding,
|
||||||
|
commentVerticalPadding, commentVerticalPadding);
|
||||||
|
} else {
|
||||||
|
itemThumbnailView.setVisibility(View.GONE);
|
||||||
|
itemRoot.setPadding(commentHorizontalPadding, commentVerticalPadding,
|
||||||
|
commentHorizontalPadding, commentVerticalPadding);
|
||||||
|
}
|
||||||
|
itemThumbnailView.setOnClickListener(view -> openCommentAuthor(item));
|
||||||
|
|
||||||
|
|
||||||
|
// setup the top row, with pinned icon, author name and comment date
|
||||||
|
itemPinnedView.setVisibility(item.isPinned() ? View.VISIBLE : View.GONE);
|
||||||
|
itemTitleView.setText(Localization.concatenateStrings(item.getUploaderName(),
|
||||||
|
Localization.relativeTimeOrTextual(itemBuilder.getContext(), item.getUploadDate(),
|
||||||
|
item.getTextualUploadDate())));
|
||||||
|
|
||||||
|
|
||||||
|
// setup bottom row, with likes, heart and replies button
|
||||||
|
itemLikesCountView.setText(
|
||||||
|
Localization.likeCount(itemBuilder.getContext(), item.getLikeCount()));
|
||||||
|
|
||||||
|
itemHeartView.setVisibility(item.isHeartedByUploader() ? View.VISIBLE : View.GONE);
|
||||||
|
|
||||||
|
final boolean hasReplies = item.getReplies() != null;
|
||||||
|
repliesButton.setOnClickListener(hasReplies ? v -> openCommentReplies(item) : null);
|
||||||
|
repliesButton.setVisibility(hasReplies ? View.VISIBLE : View.GONE);
|
||||||
|
repliesButton.setText(hasReplies
|
||||||
|
? Localization.replyCount(itemBuilder.getContext(), item.getReplyCount()) : "");
|
||||||
|
((RelativeLayout.LayoutParams) itemThumbsUpView.getLayoutParams()).topMargin =
|
||||||
|
hasReplies ? 0 : DeviceUtils.dpToPx(6, itemBuilder.getContext());
|
||||||
|
|
||||||
|
|
||||||
|
// setup comment content and click listeners to expand/ellipsize it
|
||||||
|
textEllipsizer.setStreamingService(getServiceById(item.getServiceId()));
|
||||||
|
textEllipsizer.setStreamUrl(item.getUrl());
|
||||||
|
textEllipsizer.setContent(item.getCommentText());
|
||||||
|
textEllipsizer.ellipsize();
|
||||||
|
|
||||||
|
//noinspection ClickableViewAccessibility
|
||||||
|
itemContentView.setOnTouchListener(CommentTextOnTouchListener.INSTANCE);
|
||||||
|
|
||||||
|
itemView.setOnClickListener(view -> {
|
||||||
|
textEllipsizer.toggle();
|
||||||
|
if (itemBuilder.getOnCommentsSelectedListener() != null) {
|
||||||
|
itemBuilder.getOnCommentsSelectedListener().selected(item);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
itemView.setOnLongClickListener(view -> {
|
||||||
|
if (DeviceUtils.isTv(itemBuilder.getContext())) {
|
||||||
|
openCommentAuthor(item);
|
||||||
|
} else {
|
||||||
|
final CharSequence text = itemContentView.getText();
|
||||||
|
if (text != null) {
|
||||||
|
ShareUtils.copyToClipboard(itemBuilder.getContext(), text.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void openCommentAuthor(@NonNull final CommentsInfoItem item) {
|
||||||
|
NavigationHelper.openCommentAuthorIfPresent((FragmentActivity) itemBuilder.getContext(),
|
||||||
|
item);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void openCommentReplies(@NonNull final CommentsInfoItem item) {
|
||||||
|
NavigationHelper.openCommentRepliesFragment((FragmentActivity) itemBuilder.getContext(),
|
||||||
|
item);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void allowLinkFocus() {
|
||||||
|
itemContentView.setMovementMethod(LinkMovementMethod.getInstance());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void denyLinkFocus() {
|
||||||
|
itemContentView.setMovementMethod(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean shouldFocusLinks() {
|
||||||
|
if (itemView.isInTouchMode()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
final URLSpan[] urls = itemContentView.getUrls();
|
||||||
|
|
||||||
|
return urls != null && urls.length != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void determineMovementMethod() {
|
||||||
|
if (shouldFocusLinks()) {
|
||||||
|
allowLinkFocus();
|
||||||
|
} else {
|
||||||
|
denyLinkFocus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,63 +0,0 @@
|
||||||
package org.schabi.newpipe.info_list.holder;
|
|
||||||
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.ViewGroup;
|
|
||||||
import android.widget.ImageView;
|
|
||||||
import android.widget.TextView;
|
|
||||||
|
|
||||||
import org.schabi.newpipe.R;
|
|
||||||
import org.schabi.newpipe.extractor.InfoItem;
|
|
||||||
import org.schabi.newpipe.extractor.comments.CommentsInfoItem;
|
|
||||||
import org.schabi.newpipe.info_list.InfoItemBuilder;
|
|
||||||
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Created by Christian Schabesberger on 12.02.17.
|
|
||||||
*
|
|
||||||
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
|
|
||||||
* ChannelInfoItemHolder .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 CommentsInfoItemHolder extends CommentsMiniInfoItemHolder {
|
|
||||||
public final TextView itemTitleView;
|
|
||||||
private final ImageView itemHeartView;
|
|
||||||
private final ImageView itemPinnedView;
|
|
||||||
|
|
||||||
public CommentsInfoItemHolder(final InfoItemBuilder infoItemBuilder, final ViewGroup parent) {
|
|
||||||
super(infoItemBuilder, R.layout.list_comments_item, parent);
|
|
||||||
|
|
||||||
itemTitleView = itemView.findViewById(R.id.itemTitleView);
|
|
||||||
itemHeartView = itemView.findViewById(R.id.detail_heart_image_view);
|
|
||||||
itemPinnedView = itemView.findViewById(R.id.detail_pinned_view);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void updateFromItem(final InfoItem infoItem,
|
|
||||||
final HistoryRecordManager historyRecordManager) {
|
|
||||||
super.updateFromItem(infoItem, historyRecordManager);
|
|
||||||
|
|
||||||
if (!(infoItem instanceof CommentsInfoItem)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
final CommentsInfoItem item = (CommentsInfoItem) infoItem;
|
|
||||||
|
|
||||||
itemTitleView.setText(item.getUploaderName());
|
|
||||||
|
|
||||||
itemHeartView.setVisibility(item.isHeartedByUploader() ? View.VISIBLE : View.GONE);
|
|
||||||
|
|
||||||
itemPinnedView.setVisibility(item.isPinned() ? View.VISIBLE : View.GONE);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,280 +0,0 @@
|
||||||
package org.schabi.newpipe.info_list.holder;
|
|
||||||
|
|
||||||
import static android.text.TextUtils.isEmpty;
|
|
||||||
|
|
||||||
import android.graphics.Paint;
|
|
||||||
import android.text.Layout;
|
|
||||||
import android.text.method.LinkMovementMethod;
|
|
||||||
import android.text.style.URLSpan;
|
|
||||||
import android.util.Log;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.ViewGroup;
|
|
||||||
import android.widget.ImageView;
|
|
||||||
import android.widget.RelativeLayout;
|
|
||||||
import android.widget.TextView;
|
|
||||||
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import androidx.appcompat.app.AppCompatActivity;
|
|
||||||
import androidx.core.text.HtmlCompat;
|
|
||||||
|
|
||||||
import org.schabi.newpipe.R;
|
|
||||||
import org.schabi.newpipe.error.ErrorUtil;
|
|
||||||
import org.schabi.newpipe.extractor.InfoItem;
|
|
||||||
import org.schabi.newpipe.extractor.NewPipe;
|
|
||||||
import org.schabi.newpipe.extractor.ServiceList;
|
|
||||||
import org.schabi.newpipe.extractor.StreamingService;
|
|
||||||
import org.schabi.newpipe.extractor.comments.CommentsInfoItem;
|
|
||||||
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
|
||||||
import org.schabi.newpipe.extractor.stream.Description;
|
|
||||||
import org.schabi.newpipe.info_list.InfoItemBuilder;
|
|
||||||
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
|
||||||
import org.schabi.newpipe.util.DeviceUtils;
|
|
||||||
import org.schabi.newpipe.util.Localization;
|
|
||||||
import org.schabi.newpipe.util.NavigationHelper;
|
|
||||||
import org.schabi.newpipe.util.image.ImageStrategy;
|
|
||||||
import org.schabi.newpipe.util.image.PicassoHelper;
|
|
||||||
import org.schabi.newpipe.util.external_communication.ShareUtils;
|
|
||||||
import org.schabi.newpipe.util.text.CommentTextOnTouchListener;
|
|
||||||
import org.schabi.newpipe.util.text.TextLinkifier;
|
|
||||||
|
|
||||||
import java.util.function.Consumer;
|
|
||||||
|
|
||||||
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
|
||||||
|
|
||||||
public class CommentsMiniInfoItemHolder extends InfoItemHolder {
|
|
||||||
private static final String TAG = "CommentsMiniIIHolder";
|
|
||||||
private static final String ELLIPSIS = "…";
|
|
||||||
|
|
||||||
private static final int COMMENT_DEFAULT_LINES = 2;
|
|
||||||
private static final int COMMENT_EXPANDED_LINES = 1000;
|
|
||||||
|
|
||||||
private final int commentHorizontalPadding;
|
|
||||||
private final int commentVerticalPadding;
|
|
||||||
|
|
||||||
private final Paint paintAtContentSize;
|
|
||||||
private final float ellipsisWidthPx;
|
|
||||||
|
|
||||||
private final RelativeLayout itemRoot;
|
|
||||||
private final ImageView itemThumbnailView;
|
|
||||||
private final TextView itemContentView;
|
|
||||||
private final TextView itemLikesCountView;
|
|
||||||
private final TextView itemPublishedTime;
|
|
||||||
|
|
||||||
private final CompositeDisposable disposables = new CompositeDisposable();
|
|
||||||
@Nullable private Description commentText;
|
|
||||||
@Nullable private StreamingService streamService;
|
|
||||||
@Nullable private String streamUrl;
|
|
||||||
|
|
||||||
CommentsMiniInfoItemHolder(final InfoItemBuilder infoItemBuilder, final int layoutId,
|
|
||||||
final ViewGroup parent) {
|
|
||||||
super(infoItemBuilder, layoutId, parent);
|
|
||||||
|
|
||||||
itemRoot = itemView.findViewById(R.id.itemRoot);
|
|
||||||
itemThumbnailView = itemView.findViewById(R.id.itemThumbnailView);
|
|
||||||
itemLikesCountView = itemView.findViewById(R.id.detail_thumbs_up_count_view);
|
|
||||||
itemPublishedTime = itemView.findViewById(R.id.itemPublishedTime);
|
|
||||||
itemContentView = itemView.findViewById(R.id.itemCommentContentView);
|
|
||||||
|
|
||||||
commentHorizontalPadding = (int) infoItemBuilder.getContext()
|
|
||||||
.getResources().getDimension(R.dimen.comments_horizontal_padding);
|
|
||||||
commentVerticalPadding = (int) infoItemBuilder.getContext()
|
|
||||||
.getResources().getDimension(R.dimen.comments_vertical_padding);
|
|
||||||
|
|
||||||
paintAtContentSize = new Paint();
|
|
||||||
paintAtContentSize.setTextSize(itemContentView.getTextSize());
|
|
||||||
ellipsisWidthPx = paintAtContentSize.measureText(ELLIPSIS);
|
|
||||||
}
|
|
||||||
|
|
||||||
public CommentsMiniInfoItemHolder(final InfoItemBuilder infoItemBuilder,
|
|
||||||
final ViewGroup parent) {
|
|
||||||
this(infoItemBuilder, R.layout.list_comments_mini_item, parent);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void updateFromItem(final InfoItem infoItem,
|
|
||||||
final HistoryRecordManager historyRecordManager) {
|
|
||||||
if (!(infoItem instanceof CommentsInfoItem)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
final CommentsInfoItem item = (CommentsInfoItem) infoItem;
|
|
||||||
|
|
||||||
PicassoHelper.loadAvatar(item.getUploaderAvatars()).into(itemThumbnailView);
|
|
||||||
if (ImageStrategy.shouldLoadImages()) {
|
|
||||||
itemThumbnailView.setVisibility(View.VISIBLE);
|
|
||||||
itemRoot.setPadding(commentVerticalPadding, commentVerticalPadding,
|
|
||||||
commentVerticalPadding, commentVerticalPadding);
|
|
||||||
} else {
|
|
||||||
itemThumbnailView.setVisibility(View.GONE);
|
|
||||||
itemRoot.setPadding(commentHorizontalPadding, commentVerticalPadding,
|
|
||||||
commentHorizontalPadding, commentVerticalPadding);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
itemThumbnailView.setOnClickListener(view -> openCommentAuthor(item));
|
|
||||||
|
|
||||||
try {
|
|
||||||
streamService = NewPipe.getService(item.getServiceId());
|
|
||||||
} catch (final ExtractionException e) {
|
|
||||||
// should never happen
|
|
||||||
ErrorUtil.showUiErrorSnackbar(itemBuilder.getContext(), "Getting StreamingService", e);
|
|
||||||
Log.w(TAG, "Cannot obtain service from comment service id, defaulting to YouTube", e);
|
|
||||||
streamService = ServiceList.YouTube;
|
|
||||||
}
|
|
||||||
streamUrl = item.getUrl();
|
|
||||||
commentText = item.getCommentText();
|
|
||||||
ellipsize();
|
|
||||||
|
|
||||||
//noinspection ClickableViewAccessibility
|
|
||||||
itemContentView.setOnTouchListener(CommentTextOnTouchListener.INSTANCE);
|
|
||||||
|
|
||||||
if (item.getLikeCount() >= 0) {
|
|
||||||
itemLikesCountView.setText(
|
|
||||||
Localization.shortCount(
|
|
||||||
itemBuilder.getContext(),
|
|
||||||
item.getLikeCount()));
|
|
||||||
} else {
|
|
||||||
itemLikesCountView.setText("-");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (item.getUploadDate() != null) {
|
|
||||||
itemPublishedTime.setText(Localization.relativeTime(item.getUploadDate()
|
|
||||||
.offsetDateTime()));
|
|
||||||
} else {
|
|
||||||
itemPublishedTime.setText(item.getTextualUploadDate());
|
|
||||||
}
|
|
||||||
|
|
||||||
itemView.setOnClickListener(view -> {
|
|
||||||
toggleEllipsize();
|
|
||||||
if (itemBuilder.getOnCommentsSelectedListener() != null) {
|
|
||||||
itemBuilder.getOnCommentsSelectedListener().selected(item);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
itemView.setOnLongClickListener(view -> {
|
|
||||||
if (DeviceUtils.isTv(itemBuilder.getContext())) {
|
|
||||||
openCommentAuthor(item);
|
|
||||||
} else {
|
|
||||||
final CharSequence text = itemContentView.getText();
|
|
||||||
if (text != null) {
|
|
||||||
ShareUtils.copyToClipboard(itemBuilder.getContext(), text.toString());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private void openCommentAuthor(final CommentsInfoItem item) {
|
|
||||||
if (isEmpty(item.getUploaderUrl())) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
final AppCompatActivity activity = (AppCompatActivity) itemBuilder.getContext();
|
|
||||||
try {
|
|
||||||
NavigationHelper.openChannelFragment(
|
|
||||||
activity.getSupportFragmentManager(),
|
|
||||||
item.getServiceId(),
|
|
||||||
item.getUploaderUrl(),
|
|
||||||
item.getUploaderName());
|
|
||||||
} catch (final Exception e) {
|
|
||||||
ErrorUtil.showUiErrorSnackbar(activity, "Opening channel fragment", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void allowLinkFocus() {
|
|
||||||
itemContentView.setMovementMethod(LinkMovementMethod.getInstance());
|
|
||||||
}
|
|
||||||
|
|
||||||
private void denyLinkFocus() {
|
|
||||||
itemContentView.setMovementMethod(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean shouldFocusLinks() {
|
|
||||||
if (itemView.isInTouchMode()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
final URLSpan[] urls = itemContentView.getUrls();
|
|
||||||
|
|
||||||
return urls != null && urls.length != 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void determineMovementMethod() {
|
|
||||||
if (shouldFocusLinks()) {
|
|
||||||
allowLinkFocus();
|
|
||||||
} else {
|
|
||||||
denyLinkFocus();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ellipsize() {
|
|
||||||
itemContentView.setMaxLines(COMMENT_EXPANDED_LINES);
|
|
||||||
linkifyCommentContentView(v -> {
|
|
||||||
boolean hasEllipsis = false;
|
|
||||||
|
|
||||||
final CharSequence charSeqText = itemContentView.getText();
|
|
||||||
if (charSeqText != null && itemContentView.getLineCount() > COMMENT_DEFAULT_LINES) {
|
|
||||||
// Note that converting to String removes spans (i.e. links), but that's something
|
|
||||||
// we actually want since when the text is ellipsized we want all clicks on the
|
|
||||||
// comment to expand the comment, not to open links.
|
|
||||||
final String text = charSeqText.toString();
|
|
||||||
|
|
||||||
final Layout layout = itemContentView.getLayout();
|
|
||||||
final float lineWidth = layout.getLineWidth(COMMENT_DEFAULT_LINES - 1);
|
|
||||||
final float layoutWidth = layout.getWidth();
|
|
||||||
final int lineStart = layout.getLineStart(COMMENT_DEFAULT_LINES - 1);
|
|
||||||
final int lineEnd = layout.getLineEnd(COMMENT_DEFAULT_LINES - 1);
|
|
||||||
|
|
||||||
// remove characters up until there is enough space for the ellipsis
|
|
||||||
// (also summing 2 more pixels, just to be sure to avoid float rounding errors)
|
|
||||||
int end = lineEnd;
|
|
||||||
float removedCharactersWidth = 0.0f;
|
|
||||||
while (lineWidth - removedCharactersWidth + ellipsisWidthPx + 2.0f > layoutWidth
|
|
||||||
&& end >= lineStart) {
|
|
||||||
end -= 1;
|
|
||||||
// recalculate each time to account for ligatures or other similar things
|
|
||||||
removedCharactersWidth = paintAtContentSize.measureText(
|
|
||||||
text.substring(end, lineEnd));
|
|
||||||
}
|
|
||||||
|
|
||||||
// remove trailing spaces and newlines
|
|
||||||
while (end > 0 && Character.isWhitespace(text.charAt(end - 1))) {
|
|
||||||
end -= 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
final String newVal = text.substring(0, end) + ELLIPSIS;
|
|
||||||
itemContentView.setText(newVal);
|
|
||||||
hasEllipsis = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
itemContentView.setMaxLines(COMMENT_DEFAULT_LINES);
|
|
||||||
if (hasEllipsis) {
|
|
||||||
denyLinkFocus();
|
|
||||||
} else {
|
|
||||||
determineMovementMethod();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private void toggleEllipsize() {
|
|
||||||
final CharSequence text = itemContentView.getText();
|
|
||||||
if (!isEmpty(text) && text.charAt(text.length() - 1) == ELLIPSIS.charAt(0)) {
|
|
||||||
expand();
|
|
||||||
} else if (itemContentView.getLineCount() > COMMENT_DEFAULT_LINES) {
|
|
||||||
ellipsize();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void expand() {
|
|
||||||
itemContentView.setMaxLines(COMMENT_EXPANDED_LINES);
|
|
||||||
linkifyCommentContentView(v -> determineMovementMethod());
|
|
||||||
}
|
|
||||||
|
|
||||||
private void linkifyCommentContentView(@Nullable final Consumer<TextView> onCompletion) {
|
|
||||||
disposables.clear();
|
|
||||||
if (commentText != null) {
|
|
||||||
TextLinkifier.fromDescription(itemContentView, commentText,
|
|
||||||
HtmlCompat.FROM_HTML_MODE_LEGACY, streamService, streamUrl, disposables,
|
|
||||||
onCompletion);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -12,10 +12,6 @@ import org.schabi.newpipe.info_list.InfoItemBuilder;
|
||||||
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
||||||
import org.schabi.newpipe.util.Localization;
|
import org.schabi.newpipe.util.Localization;
|
||||||
|
|
||||||
import androidx.preference.PreferenceManager;
|
|
||||||
|
|
||||||
import static org.schabi.newpipe.MainActivity.DEBUG;
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Created by Christian Schabesberger on 01.08.16.
|
* Created by Christian Schabesberger on 01.08.16.
|
||||||
* <p>
|
* <p>
|
||||||
|
@ -81,7 +77,9 @@ public class StreamInfoItemHolder extends StreamMiniInfoItemHolder {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final String uploadDate = getFormattedRelativeUploadDate(infoItem);
|
final String uploadDate = Localization.relativeTimeOrTextual(itemBuilder.getContext(),
|
||||||
|
infoItem.getUploadDate(),
|
||||||
|
infoItem.getTextualUploadDate());
|
||||||
if (!TextUtils.isEmpty(uploadDate)) {
|
if (!TextUtils.isEmpty(uploadDate)) {
|
||||||
if (viewsAndDate.isEmpty()) {
|
if (viewsAndDate.isEmpty()) {
|
||||||
return uploadDate;
|
return uploadDate;
|
||||||
|
@ -92,20 +90,4 @@ public class StreamInfoItemHolder extends StreamMiniInfoItemHolder {
|
||||||
|
|
||||||
return viewsAndDate;
|
return viewsAndDate;
|
||||||
}
|
}
|
||||||
|
|
||||||
private String getFormattedRelativeUploadDate(final StreamInfoItem infoItem) {
|
|
||||||
if (infoItem.getUploadDate() != null) {
|
|
||||||
String formattedRelativeTime = Localization
|
|
||||||
.relativeTime(infoItem.getUploadDate().offsetDateTime());
|
|
||||||
|
|
||||||
if (DEBUG && PreferenceManager.getDefaultSharedPreferences(itemBuilder.getContext())
|
|
||||||
.getBoolean(itemBuilder.getContext()
|
|
||||||
.getString(R.string.show_original_time_ago_key), false)) {
|
|
||||||
formattedRelativeTime += " (" + infoItem.getTextualUploadDate() + ")";
|
|
||||||
}
|
|
||||||
return formattedRelativeTime;
|
|
||||||
} else {
|
|
||||||
return infoItem.getTextualUploadDate();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
9
app/src/main/java/org/schabi/newpipe/ktx/Bundle.kt
Normal file
9
app/src/main/java/org/schabi/newpipe/ktx/Bundle.kt
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
package org.schabi.newpipe.ktx
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.os.Parcelable
|
||||||
|
import androidx.core.os.BundleCompat
|
||||||
|
|
||||||
|
inline fun <reified T : Parcelable> Bundle.parcelableArrayList(key: String?): ArrayList<T>? {
|
||||||
|
return BundleCompat.getParcelableArrayList(this, key, T::class.java)
|
||||||
|
}
|
|
@ -14,6 +14,7 @@ import org.schabi.newpipe.database.LocalItem;
|
||||||
import org.schabi.newpipe.database.stream.model.StreamStateEntity;
|
import org.schabi.newpipe.database.stream.model.StreamStateEntity;
|
||||||
import org.schabi.newpipe.info_list.ItemViewMode;
|
import org.schabi.newpipe.info_list.ItemViewMode;
|
||||||
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
||||||
|
import org.schabi.newpipe.local.holder.LocalBookmarkPlaylistItemHolder;
|
||||||
import org.schabi.newpipe.local.holder.LocalItemHolder;
|
import org.schabi.newpipe.local.holder.LocalItemHolder;
|
||||||
import org.schabi.newpipe.local.holder.LocalPlaylistCardItemHolder;
|
import org.schabi.newpipe.local.holder.LocalPlaylistCardItemHolder;
|
||||||
import org.schabi.newpipe.local.holder.LocalPlaylistGridItemHolder;
|
import org.schabi.newpipe.local.holder.LocalPlaylistGridItemHolder;
|
||||||
|
@ -24,6 +25,7 @@ import org.schabi.newpipe.local.holder.LocalPlaylistStreamItemHolder;
|
||||||
import org.schabi.newpipe.local.holder.LocalStatisticStreamCardItemHolder;
|
import org.schabi.newpipe.local.holder.LocalStatisticStreamCardItemHolder;
|
||||||
import org.schabi.newpipe.local.holder.LocalStatisticStreamGridItemHolder;
|
import org.schabi.newpipe.local.holder.LocalStatisticStreamGridItemHolder;
|
||||||
import org.schabi.newpipe.local.holder.LocalStatisticStreamItemHolder;
|
import org.schabi.newpipe.local.holder.LocalStatisticStreamItemHolder;
|
||||||
|
import org.schabi.newpipe.local.holder.RemoteBookmarkPlaylistItemHolder;
|
||||||
import org.schabi.newpipe.local.holder.RemotePlaylistCardItemHolder;
|
import org.schabi.newpipe.local.holder.RemotePlaylistCardItemHolder;
|
||||||
import org.schabi.newpipe.local.holder.RemotePlaylistGridItemHolder;
|
import org.schabi.newpipe.local.holder.RemotePlaylistGridItemHolder;
|
||||||
import org.schabi.newpipe.local.holder.RemotePlaylistItemHolder;
|
import org.schabi.newpipe.local.holder.RemotePlaylistItemHolder;
|
||||||
|
@ -73,10 +75,12 @@ public class LocalItemListAdapter extends RecyclerView.Adapter<RecyclerView.View
|
||||||
private static final int LOCAL_PLAYLIST_HOLDER_TYPE = 0x2000;
|
private static final int LOCAL_PLAYLIST_HOLDER_TYPE = 0x2000;
|
||||||
private static final int LOCAL_PLAYLIST_GRID_HOLDER_TYPE = 0x2001;
|
private static final int LOCAL_PLAYLIST_GRID_HOLDER_TYPE = 0x2001;
|
||||||
private static final int LOCAL_PLAYLIST_CARD_HOLDER_TYPE = 0x2002;
|
private static final int LOCAL_PLAYLIST_CARD_HOLDER_TYPE = 0x2002;
|
||||||
|
private static final int LOCAL_BOOKMARK_PLAYLIST_HOLDER_TYPE = 0x2003;
|
||||||
|
|
||||||
private static final int REMOTE_PLAYLIST_HOLDER_TYPE = 0x3000;
|
private static final int REMOTE_PLAYLIST_HOLDER_TYPE = 0x3000;
|
||||||
private static final int REMOTE_PLAYLIST_GRID_HOLDER_TYPE = 0x3001;
|
private static final int REMOTE_PLAYLIST_GRID_HOLDER_TYPE = 0x3001;
|
||||||
private static final int REMOTE_PLAYLIST_CARD_HOLDER_TYPE = 0x3002;
|
private static final int REMOTE_PLAYLIST_CARD_HOLDER_TYPE = 0x3002;
|
||||||
|
private static final int REMOTE_BOOKMARK_PLAYLIST_HOLDER_TYPE = 0x3003;
|
||||||
|
|
||||||
private final LocalItemBuilder localItemBuilder;
|
private final LocalItemBuilder localItemBuilder;
|
||||||
private final ArrayList<LocalItem> localItems;
|
private final ArrayList<LocalItem> localItems;
|
||||||
|
@ -87,6 +91,7 @@ public class LocalItemListAdapter extends RecyclerView.Adapter<RecyclerView.View
|
||||||
private View header = null;
|
private View header = null;
|
||||||
private View footer = null;
|
private View footer = null;
|
||||||
private ItemViewMode itemViewMode = ItemViewMode.LIST;
|
private ItemViewMode itemViewMode = ItemViewMode.LIST;
|
||||||
|
private boolean useItemHandle = false;
|
||||||
|
|
||||||
public LocalItemListAdapter(final Context context) {
|
public LocalItemListAdapter(final Context context) {
|
||||||
recordManager = new HistoryRecordManager(context);
|
recordManager = new HistoryRecordManager(context);
|
||||||
|
@ -180,6 +185,10 @@ public class LocalItemListAdapter extends RecyclerView.Adapter<RecyclerView.View
|
||||||
this.itemViewMode = itemViewMode;
|
this.itemViewMode = itemViewMode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setUseItemHandle(final boolean useItemHandle) {
|
||||||
|
this.useItemHandle = useItemHandle;
|
||||||
|
}
|
||||||
|
|
||||||
public void setHeader(final View header) {
|
public void setHeader(final View header) {
|
||||||
final boolean changed = header != this.header;
|
final boolean changed = header != this.header;
|
||||||
this.header = header;
|
this.header = header;
|
||||||
|
@ -257,7 +266,9 @@ public class LocalItemListAdapter extends RecyclerView.Adapter<RecyclerView.View
|
||||||
final LocalItem item = localItems.get(position);
|
final LocalItem item = localItems.get(position);
|
||||||
switch (item.getLocalItemType()) {
|
switch (item.getLocalItemType()) {
|
||||||
case PLAYLIST_LOCAL_ITEM:
|
case PLAYLIST_LOCAL_ITEM:
|
||||||
if (itemViewMode == ItemViewMode.CARD) {
|
if (useItemHandle) {
|
||||||
|
return LOCAL_BOOKMARK_PLAYLIST_HOLDER_TYPE;
|
||||||
|
} else if (itemViewMode == ItemViewMode.CARD) {
|
||||||
return LOCAL_PLAYLIST_CARD_HOLDER_TYPE;
|
return LOCAL_PLAYLIST_CARD_HOLDER_TYPE;
|
||||||
} else if (itemViewMode == ItemViewMode.GRID) {
|
} else if (itemViewMode == ItemViewMode.GRID) {
|
||||||
return LOCAL_PLAYLIST_GRID_HOLDER_TYPE;
|
return LOCAL_PLAYLIST_GRID_HOLDER_TYPE;
|
||||||
|
@ -265,7 +276,9 @@ public class LocalItemListAdapter extends RecyclerView.Adapter<RecyclerView.View
|
||||||
return LOCAL_PLAYLIST_HOLDER_TYPE;
|
return LOCAL_PLAYLIST_HOLDER_TYPE;
|
||||||
}
|
}
|
||||||
case PLAYLIST_REMOTE_ITEM:
|
case PLAYLIST_REMOTE_ITEM:
|
||||||
if (itemViewMode == ItemViewMode.CARD) {
|
if (useItemHandle) {
|
||||||
|
return REMOTE_BOOKMARK_PLAYLIST_HOLDER_TYPE;
|
||||||
|
} else if (itemViewMode == ItemViewMode.CARD) {
|
||||||
return REMOTE_PLAYLIST_CARD_HOLDER_TYPE;
|
return REMOTE_PLAYLIST_CARD_HOLDER_TYPE;
|
||||||
} else if (itemViewMode == ItemViewMode.GRID) {
|
} else if (itemViewMode == ItemViewMode.GRID) {
|
||||||
return REMOTE_PLAYLIST_GRID_HOLDER_TYPE;
|
return REMOTE_PLAYLIST_GRID_HOLDER_TYPE;
|
||||||
|
@ -314,12 +327,16 @@ public class LocalItemListAdapter extends RecyclerView.Adapter<RecyclerView.View
|
||||||
return new LocalPlaylistGridItemHolder(localItemBuilder, parent);
|
return new LocalPlaylistGridItemHolder(localItemBuilder, parent);
|
||||||
case LOCAL_PLAYLIST_CARD_HOLDER_TYPE:
|
case LOCAL_PLAYLIST_CARD_HOLDER_TYPE:
|
||||||
return new LocalPlaylistCardItemHolder(localItemBuilder, parent);
|
return new LocalPlaylistCardItemHolder(localItemBuilder, parent);
|
||||||
|
case LOCAL_BOOKMARK_PLAYLIST_HOLDER_TYPE:
|
||||||
|
return new LocalBookmarkPlaylistItemHolder(localItemBuilder, parent);
|
||||||
case REMOTE_PLAYLIST_HOLDER_TYPE:
|
case REMOTE_PLAYLIST_HOLDER_TYPE:
|
||||||
return new RemotePlaylistItemHolder(localItemBuilder, parent);
|
return new RemotePlaylistItemHolder(localItemBuilder, parent);
|
||||||
case REMOTE_PLAYLIST_GRID_HOLDER_TYPE:
|
case REMOTE_PLAYLIST_GRID_HOLDER_TYPE:
|
||||||
return new RemotePlaylistGridItemHolder(localItemBuilder, parent);
|
return new RemotePlaylistGridItemHolder(localItemBuilder, parent);
|
||||||
case REMOTE_PLAYLIST_CARD_HOLDER_TYPE:
|
case REMOTE_PLAYLIST_CARD_HOLDER_TYPE:
|
||||||
return new RemotePlaylistCardItemHolder(localItemBuilder, parent);
|
return new RemotePlaylistCardItemHolder(localItemBuilder, parent);
|
||||||
|
case REMOTE_BOOKMARK_PLAYLIST_HOLDER_TYPE:
|
||||||
|
return new RemoteBookmarkPlaylistItemHolder(localItemBuilder, parent);
|
||||||
case STREAM_PLAYLIST_HOLDER_TYPE:
|
case STREAM_PLAYLIST_HOLDER_TYPE:
|
||||||
return new LocalPlaylistStreamItemHolder(localItemBuilder, parent);
|
return new LocalPlaylistStreamItemHolder(localItemBuilder, parent);
|
||||||
case STREAM_PLAYLIST_GRID_HOLDER_TYPE:
|
case STREAM_PLAYLIST_GRID_HOLDER_TYPE:
|
||||||
|
|
|
@ -1,10 +1,13 @@
|
||||||
package org.schabi.newpipe.local.bookmark;
|
package org.schabi.newpipe.local.bookmark;
|
||||||
|
|
||||||
|
import static org.schabi.newpipe.local.bookmark.MergedPlaylistManager.getMergedOrderedPlaylists;
|
||||||
|
|
||||||
import android.content.DialogInterface;
|
import android.content.DialogInterface;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.os.Parcelable;
|
import android.os.Parcelable;
|
||||||
import android.text.InputType;
|
import android.text.InputType;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
import android.util.Pair;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
|
@ -13,6 +16,8 @@ import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.appcompat.app.AlertDialog;
|
import androidx.appcompat.app.AlertDialog;
|
||||||
import androidx.fragment.app.FragmentManager;
|
import androidx.fragment.app.FragmentManager;
|
||||||
|
import androidx.recyclerview.widget.ItemTouchHelper;
|
||||||
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
|
|
||||||
import org.reactivestreams.Subscriber;
|
import org.reactivestreams.Subscriber;
|
||||||
import org.reactivestreams.Subscription;
|
import org.reactivestreams.Subscription;
|
||||||
|
@ -27,29 +32,45 @@ import org.schabi.newpipe.databinding.DialogEditTextBinding;
|
||||||
import org.schabi.newpipe.error.ErrorInfo;
|
import org.schabi.newpipe.error.ErrorInfo;
|
||||||
import org.schabi.newpipe.error.UserAction;
|
import org.schabi.newpipe.error.UserAction;
|
||||||
import org.schabi.newpipe.local.BaseLocalListFragment;
|
import org.schabi.newpipe.local.BaseLocalListFragment;
|
||||||
|
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.LocalPlaylistManager;
|
||||||
import org.schabi.newpipe.local.playlist.RemotePlaylistManager;
|
import org.schabi.newpipe.local.playlist.RemotePlaylistManager;
|
||||||
|
import org.schabi.newpipe.util.debounce.DebounceSavable;
|
||||||
|
import org.schabi.newpipe.util.debounce.DebounceSaver;
|
||||||
import org.schabi.newpipe.util.NavigationHelper;
|
import org.schabi.newpipe.util.NavigationHelper;
|
||||||
import org.schabi.newpipe.util.OnClickGesture;
|
import org.schabi.newpipe.util.OnClickGesture;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
|
||||||
import icepick.State;
|
import icepick.State;
|
||||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||||
import io.reactivex.rxjava3.core.Flowable;
|
|
||||||
import io.reactivex.rxjava3.core.Single;
|
|
||||||
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
||||||
import io.reactivex.rxjava3.disposables.Disposable;
|
import io.reactivex.rxjava3.disposables.Disposable;
|
||||||
|
|
||||||
public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistLocalItem>, Void> {
|
public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistLocalItem>, Void>
|
||||||
|
implements DebounceSavable {
|
||||||
|
|
||||||
|
private static final int MINIMUM_INITIAL_DRAG_VELOCITY = 12;
|
||||||
@State
|
@State
|
||||||
protected Parcelable itemsListState;
|
Parcelable itemsListState;
|
||||||
|
|
||||||
private Subscription databaseSubscription;
|
private Subscription databaseSubscription;
|
||||||
private CompositeDisposable disposables = new CompositeDisposable();
|
private CompositeDisposable disposables = new CompositeDisposable();
|
||||||
private LocalPlaylistManager localPlaylistManager;
|
private LocalPlaylistManager localPlaylistManager;
|
||||||
private RemotePlaylistManager remotePlaylistManager;
|
private RemotePlaylistManager remotePlaylistManager;
|
||||||
|
private ItemTouchHelper itemTouchHelper;
|
||||||
|
|
||||||
|
/* Have the bookmarked playlists been fully loaded from db */
|
||||||
|
private AtomicBoolean isLoadingComplete;
|
||||||
|
|
||||||
|
/* Gives enough time to avoid interrupting user sorting operations */
|
||||||
|
@Nullable
|
||||||
|
private DebounceSaver debounceSaver;
|
||||||
|
|
||||||
|
private List<Pair<Long, LocalItem.LocalItemType>> deletedItems;
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////
|
||||||
// Fragment LifeCycle - Creation
|
// Fragment LifeCycle - Creation
|
||||||
|
@ -65,6 +86,11 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL
|
||||||
localPlaylistManager = new LocalPlaylistManager(database);
|
localPlaylistManager = new LocalPlaylistManager(database);
|
||||||
remotePlaylistManager = new RemotePlaylistManager(database);
|
remotePlaylistManager = new RemotePlaylistManager(database);
|
||||||
disposables = new CompositeDisposable();
|
disposables = new CompositeDisposable();
|
||||||
|
|
||||||
|
isLoadingComplete = new AtomicBoolean();
|
||||||
|
debounceSaver = new DebounceSaver(3000, this);
|
||||||
|
|
||||||
|
deletedItems = new ArrayList<>();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
|
@ -91,10 +117,20 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL
|
||||||
// Fragment LifeCycle - Views
|
// Fragment LifeCycle - Views
|
||||||
///////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void initViews(final View rootView, final Bundle savedInstanceState) {
|
||||||
|
super.initViews(rootView, savedInstanceState);
|
||||||
|
|
||||||
|
itemListAdapter.setUseItemHandle(true);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void initListeners() {
|
protected void initListeners() {
|
||||||
super.initListeners();
|
super.initListeners();
|
||||||
|
|
||||||
|
itemTouchHelper = new ItemTouchHelper(getItemTouchCallback());
|
||||||
|
itemTouchHelper.attachToRecyclerView(itemsList);
|
||||||
|
|
||||||
itemListAdapter.setSelectedListener(new OnClickGesture<>() {
|
itemListAdapter.setSelectedListener(new OnClickGesture<>() {
|
||||||
@Override
|
@Override
|
||||||
public void selected(final LocalItem selectedItem) {
|
public void selected(final LocalItem selectedItem) {
|
||||||
|
@ -102,7 +138,7 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL
|
||||||
|
|
||||||
if (selectedItem instanceof PlaylistMetadataEntry) {
|
if (selectedItem instanceof PlaylistMetadataEntry) {
|
||||||
final PlaylistMetadataEntry entry = ((PlaylistMetadataEntry) selectedItem);
|
final PlaylistMetadataEntry entry = ((PlaylistMetadataEntry) selectedItem);
|
||||||
NavigationHelper.openLocalPlaylistFragment(fragmentManager, entry.uid,
|
NavigationHelper.openLocalPlaylistFragment(fragmentManager, entry.getUid(),
|
||||||
entry.name);
|
entry.name);
|
||||||
|
|
||||||
} else if (selectedItem instanceof PlaylistRemoteEntity) {
|
} else if (selectedItem instanceof PlaylistRemoteEntity) {
|
||||||
|
@ -123,6 +159,14 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL
|
||||||
showRemoteDeleteDialog((PlaylistRemoteEntity) selectedItem);
|
showRemoteDeleteDialog((PlaylistRemoteEntity) selectedItem);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void drag(final LocalItem selectedItem,
|
||||||
|
final RecyclerView.ViewHolder viewHolder) {
|
||||||
|
if (itemTouchHelper != null) {
|
||||||
|
itemTouchHelper.startDrag(viewHolder);
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -134,8 +178,13 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL
|
||||||
public void startLoading(final boolean forceLoad) {
|
public void startLoading(final boolean forceLoad) {
|
||||||
super.startLoading(forceLoad);
|
super.startLoading(forceLoad);
|
||||||
|
|
||||||
Flowable.combineLatest(localPlaylistManager.getPlaylists(),
|
if (debounceSaver != null) {
|
||||||
remotePlaylistManager.getPlaylists(), PlaylistLocalItem::merge)
|
disposables.add(debounceSaver.getDebouncedSaver());
|
||||||
|
debounceSaver.setNoChangesToSave();
|
||||||
|
}
|
||||||
|
isLoadingComplete.set(false);
|
||||||
|
|
||||||
|
getMergedOrderedPlaylists(localPlaylistManager, remotePlaylistManager)
|
||||||
.onBackpressureLatest()
|
.onBackpressureLatest()
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.subscribe(getPlaylistsSubscriber());
|
.subscribe(getPlaylistsSubscriber());
|
||||||
|
@ -149,6 +198,9 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL
|
||||||
public void onPause() {
|
public void onPause() {
|
||||||
super.onPause();
|
super.onPause();
|
||||||
itemsListState = itemsList.getLayoutManager().onSaveInstanceState();
|
itemsListState = itemsList.getLayoutManager().onSaveInstanceState();
|
||||||
|
|
||||||
|
// Save on exit
|
||||||
|
saveImmediate();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -163,19 +215,27 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL
|
||||||
}
|
}
|
||||||
|
|
||||||
databaseSubscription = null;
|
databaseSubscription = null;
|
||||||
|
itemTouchHelper = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onDestroy() {
|
public void onDestroy() {
|
||||||
super.onDestroy();
|
super.onDestroy();
|
||||||
|
if (debounceSaver != null) {
|
||||||
|
debounceSaver.getDebouncedSaveSignal().onComplete();
|
||||||
|
}
|
||||||
if (disposables != null) {
|
if (disposables != null) {
|
||||||
disposables.dispose();
|
disposables.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
debounceSaver = null;
|
||||||
disposables = null;
|
disposables = null;
|
||||||
localPlaylistManager = null;
|
localPlaylistManager = null;
|
||||||
remotePlaylistManager = null;
|
remotePlaylistManager = null;
|
||||||
itemsListState = null;
|
itemsListState = null;
|
||||||
|
|
||||||
|
isLoadingComplete = null;
|
||||||
|
deletedItems = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////
|
||||||
|
@ -183,10 +243,12 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL
|
||||||
///////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
private Subscriber<List<PlaylistLocalItem>> getPlaylistsSubscriber() {
|
private Subscriber<List<PlaylistLocalItem>> getPlaylistsSubscriber() {
|
||||||
return new Subscriber<List<PlaylistLocalItem>>() {
|
return new Subscriber<>() {
|
||||||
@Override
|
@Override
|
||||||
public void onSubscribe(final Subscription s) {
|
public void onSubscribe(final Subscription s) {
|
||||||
showLoading();
|
showLoading();
|
||||||
|
isLoadingComplete.set(false);
|
||||||
|
|
||||||
if (databaseSubscription != null) {
|
if (databaseSubscription != null) {
|
||||||
databaseSubscription.cancel();
|
databaseSubscription.cancel();
|
||||||
}
|
}
|
||||||
|
@ -196,7 +258,10 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onNext(final List<PlaylistLocalItem> subscriptions) {
|
public void onNext(final List<PlaylistLocalItem> subscriptions) {
|
||||||
handleResult(subscriptions);
|
if (debounceSaver == null || !debounceSaver.getIsModified()) {
|
||||||
|
handleResult(subscriptions);
|
||||||
|
isLoadingComplete.set(true);
|
||||||
|
}
|
||||||
if (databaseSubscription != null) {
|
if (databaseSubscription != null) {
|
||||||
databaseSubscription.request(1);
|
databaseSubscription.request(1);
|
||||||
}
|
}
|
||||||
|
@ -209,7 +274,8 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onComplete() { }
|
public void onComplete() {
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -244,12 +310,183 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
|
// Playlist Metadata Manipulation
|
||||||
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
|
private void changeLocalPlaylistName(final long id, final String name) {
|
||||||
|
if (localPlaylistManager == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (DEBUG) {
|
||||||
|
Log.d(TAG, "Updating playlist id=[" + id + "] "
|
||||||
|
+ "with new name=[" + name + "] items");
|
||||||
|
}
|
||||||
|
|
||||||
|
final Disposable disposable = localPlaylistManager.renamePlaylist(id, name)
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribe(longs -> { /*Do nothing on success*/ }, throwable -> showError(
|
||||||
|
new ErrorInfo(throwable,
|
||||||
|
UserAction.REQUESTED_BOOKMARK,
|
||||||
|
"Changing playlist name")));
|
||||||
|
disposables.add(disposable);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void deleteItem(final PlaylistLocalItem item) {
|
||||||
|
if (itemListAdapter == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
itemListAdapter.removeItem(item);
|
||||||
|
|
||||||
|
if (item instanceof PlaylistMetadataEntry) {
|
||||||
|
deletedItems.add(new Pair<>(item.getUid(),
|
||||||
|
LocalItem.LocalItemType.PLAYLIST_LOCAL_ITEM));
|
||||||
|
} else if (item instanceof PlaylistRemoteEntity) {
|
||||||
|
deletedItems.add(new Pair<>(item.getUid(),
|
||||||
|
LocalItem.LocalItemType.PLAYLIST_REMOTE_ITEM));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (debounceSaver != null) {
|
||||||
|
debounceSaver.setHasChangesToSave();
|
||||||
|
saveImmediate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void saveImmediate() {
|
||||||
|
if (itemListAdapter == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// List must be loaded and modified in order to save
|
||||||
|
if (isLoadingComplete == null || debounceSaver == null
|
||||||
|
|| !isLoadingComplete.get() || !debounceSaver.getIsModified()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final List<LocalItem> items = itemListAdapter.getItemsList();
|
||||||
|
final List<PlaylistMetadataEntry> localItemsUpdate = new ArrayList<>();
|
||||||
|
final List<Long> localItemsDeleteUid = new ArrayList<>();
|
||||||
|
final List<PlaylistRemoteEntity> remoteItemsUpdate = new ArrayList<>();
|
||||||
|
final List<Long> remoteItemsDeleteUid = new ArrayList<>();
|
||||||
|
|
||||||
|
// Calculate display index
|
||||||
|
for (int i = 0; i < items.size(); i++) {
|
||||||
|
final LocalItem item = items.get(i);
|
||||||
|
|
||||||
|
if (item instanceof PlaylistMetadataEntry
|
||||||
|
&& ((PlaylistMetadataEntry) item).getDisplayIndex() != i) {
|
||||||
|
((PlaylistMetadataEntry) item).setDisplayIndex(i);
|
||||||
|
localItemsUpdate.add((PlaylistMetadataEntry) item);
|
||||||
|
} else if (item instanceof PlaylistRemoteEntity
|
||||||
|
&& ((PlaylistRemoteEntity) item).getDisplayIndex() != i) {
|
||||||
|
((PlaylistRemoteEntity) item).setDisplayIndex(i);
|
||||||
|
remoteItemsUpdate.add((PlaylistRemoteEntity) item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find deleted items
|
||||||
|
for (final Pair<Long, LocalItem.LocalItemType> item : deletedItems) {
|
||||||
|
if (item.second.equals(LocalItem.LocalItemType.PLAYLIST_LOCAL_ITEM)) {
|
||||||
|
localItemsDeleteUid.add(item.first);
|
||||||
|
} else if (item.second.equals(LocalItem.LocalItemType.PLAYLIST_REMOTE_ITEM)) {
|
||||||
|
remoteItemsDeleteUid.add(item.first);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
deletedItems.clear();
|
||||||
|
|
||||||
|
// 1. Update local playlists
|
||||||
|
// 2. Update remote playlists
|
||||||
|
// 3. Set NoChangesToSave
|
||||||
|
disposables.add(localPlaylistManager.updatePlaylists(localItemsUpdate, localItemsDeleteUid)
|
||||||
|
.mergeWith(remotePlaylistManager.updatePlaylists(
|
||||||
|
remoteItemsUpdate, remoteItemsDeleteUid))
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribe(() -> {
|
||||||
|
if (debounceSaver != null) {
|
||||||
|
debounceSaver.setNoChangesToSave();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
throwable -> showError(new ErrorInfo(throwable,
|
||||||
|
UserAction.REQUESTED_BOOKMARK, "Saving playlist"))
|
||||||
|
));
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private ItemTouchHelper.SimpleCallback getItemTouchCallback() {
|
||||||
|
// if adding grid layout, also include ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT
|
||||||
|
// with an `if (shouldUseGridLayout()) ...`
|
||||||
|
return new ItemTouchHelper.SimpleCallback(ItemTouchHelper.UP | ItemTouchHelper.DOWN,
|
||||||
|
ItemTouchHelper.ACTION_STATE_IDLE) {
|
||||||
|
@Override
|
||||||
|
public int interpolateOutOfBoundsScroll(@NonNull final RecyclerView recyclerView,
|
||||||
|
final int viewSize,
|
||||||
|
final int viewSizeOutOfBounds,
|
||||||
|
final int totalSize,
|
||||||
|
final long msSinceStartScroll) {
|
||||||
|
final int standardSpeed = super.interpolateOutOfBoundsScroll(recyclerView,
|
||||||
|
viewSize, viewSizeOutOfBounds, totalSize, msSinceStartScroll);
|
||||||
|
final int minimumAbsVelocity = Math.max(MINIMUM_INITIAL_DRAG_VELOCITY,
|
||||||
|
Math.abs(standardSpeed));
|
||||||
|
return minimumAbsVelocity * (int) Math.signum(viewSizeOutOfBounds);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onMove(@NonNull final RecyclerView recyclerView,
|
||||||
|
@NonNull final RecyclerView.ViewHolder source,
|
||||||
|
@NonNull final RecyclerView.ViewHolder target) {
|
||||||
|
|
||||||
|
// Allow swap LocalBookmarkPlaylistItemHolder and RemoteBookmarkPlaylistItemHolder.
|
||||||
|
if (itemListAdapter == null
|
||||||
|
|| source.getItemViewType() != target.getItemViewType()
|
||||||
|
&& !(
|
||||||
|
(
|
||||||
|
(source instanceof LocalBookmarkPlaylistItemHolder)
|
||||||
|
|| (source instanceof RemoteBookmarkPlaylistItemHolder)
|
||||||
|
)
|
||||||
|
&& (
|
||||||
|
(target instanceof LocalBookmarkPlaylistItemHolder)
|
||||||
|
|| (target instanceof RemoteBookmarkPlaylistItemHolder)
|
||||||
|
))
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
final int sourceIndex = source.getBindingAdapterPosition();
|
||||||
|
final int targetIndex = target.getBindingAdapterPosition();
|
||||||
|
final boolean isSwapped = itemListAdapter.swapItems(sourceIndex, targetIndex);
|
||||||
|
if (isSwapped && debounceSaver != null) {
|
||||||
|
debounceSaver.setHasChangesToSave();
|
||||||
|
}
|
||||||
|
return isSwapped;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isLongPressDragEnabled() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isItemViewSwipeEnabled() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onSwiped(@NonNull final RecyclerView.ViewHolder viewHolder,
|
||||||
|
final int swipeDir) {
|
||||||
|
// Do nothing.
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////
|
||||||
// Utils
|
// Utils
|
||||||
///////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
private void showRemoteDeleteDialog(final PlaylistRemoteEntity item) {
|
private void showRemoteDeleteDialog(final PlaylistRemoteEntity item) {
|
||||||
showDeleteDialog(item.getName(), remotePlaylistManager.deletePlaylist(item.getUid()));
|
showDeleteDialog(item.getName(), item);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void showLocalDialog(final PlaylistMetadataEntry selectedItem) {
|
private void showLocalDialog(final PlaylistMetadataEntry selectedItem) {
|
||||||
|
@ -257,7 +494,7 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL
|
||||||
final String delete = getString(R.string.delete);
|
final String delete = getString(R.string.delete);
|
||||||
final String unsetThumbnail = getString(R.string.unset_playlist_thumbnail);
|
final String unsetThumbnail = getString(R.string.unset_playlist_thumbnail);
|
||||||
final boolean isThumbnailPermanent = localPlaylistManager
|
final boolean isThumbnailPermanent = localPlaylistManager
|
||||||
.getIsPlaylistThumbnailPermanent(selectedItem.uid);
|
.getIsPlaylistThumbnailPermanent(selectedItem.getUid());
|
||||||
|
|
||||||
final ArrayList<String> items = new ArrayList<>();
|
final ArrayList<String> items = new ArrayList<>();
|
||||||
items.add(rename);
|
items.add(rename);
|
||||||
|
@ -270,13 +507,12 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL
|
||||||
if (items.get(index).equals(rename)) {
|
if (items.get(index).equals(rename)) {
|
||||||
showRenameDialog(selectedItem);
|
showRenameDialog(selectedItem);
|
||||||
} else if (items.get(index).equals(delete)) {
|
} else if (items.get(index).equals(delete)) {
|
||||||
showDeleteDialog(selectedItem.name,
|
showDeleteDialog(selectedItem.name, selectedItem);
|
||||||
localPlaylistManager.deletePlaylist(selectedItem.uid));
|
|
||||||
} else if (isThumbnailPermanent && items.get(index).equals(unsetThumbnail)) {
|
} else if (isThumbnailPermanent && items.get(index).equals(unsetThumbnail)) {
|
||||||
final long thumbnailStreamId = localPlaylistManager
|
final long thumbnailStreamId = localPlaylistManager
|
||||||
.getAutomaticPlaylistThumbnailStreamId(selectedItem.uid);
|
.getAutomaticPlaylistThumbnailStreamId(selectedItem.getUid());
|
||||||
localPlaylistManager
|
localPlaylistManager
|
||||||
.changePlaylistThumbnail(selectedItem.uid, thumbnailStreamId, false)
|
.changePlaylistThumbnail(selectedItem.getUid(), thumbnailStreamId, false)
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.subscribe();
|
.subscribe();
|
||||||
}
|
}
|
||||||
|
@ -298,13 +534,13 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL
|
||||||
.setView(dialogBinding.getRoot())
|
.setView(dialogBinding.getRoot())
|
||||||
.setPositiveButton(R.string.rename_playlist, (dialog, which) ->
|
.setPositiveButton(R.string.rename_playlist, (dialog, which) ->
|
||||||
changeLocalPlaylistName(
|
changeLocalPlaylistName(
|
||||||
selectedItem.uid,
|
selectedItem.getUid(),
|
||||||
dialogBinding.dialogEditText.getText().toString()))
|
dialogBinding.dialogEditText.getText().toString()))
|
||||||
.setNegativeButton(R.string.cancel, null)
|
.setNegativeButton(R.string.cancel, null)
|
||||||
.show();
|
.show();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void showDeleteDialog(final String name, final Single<Integer> deleteReactor) {
|
private void showDeleteDialog(final String name, final PlaylistLocalItem item) {
|
||||||
if (activity == null || disposables == null) {
|
if (activity == null || disposables == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -313,35 +549,8 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL
|
||||||
.setTitle(name)
|
.setTitle(name)
|
||||||
.setMessage(R.string.delete_playlist_prompt)
|
.setMessage(R.string.delete_playlist_prompt)
|
||||||
.setCancelable(true)
|
.setCancelable(true)
|
||||||
.setPositiveButton(R.string.delete, (dialog, i) ->
|
.setPositiveButton(R.string.delete, (dialog, i) -> deleteItem(item))
|
||||||
disposables.add(deleteReactor
|
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
|
||||||
.subscribe(ignored -> { /*Do nothing on success*/ }, throwable ->
|
|
||||||
showError(new ErrorInfo(throwable,
|
|
||||||
UserAction.REQUESTED_BOOKMARK,
|
|
||||||
"Deleting playlist")))))
|
|
||||||
.setNegativeButton(R.string.cancel, null)
|
.setNegativeButton(R.string.cancel, null)
|
||||||
.show();
|
.show();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void changeLocalPlaylistName(final long id, final String name) {
|
|
||||||
if (localPlaylistManager == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (DEBUG) {
|
|
||||||
Log.d(TAG, "Updating playlist id=[" + id + "] "
|
|
||||||
+ "with new name=[" + name + "] items");
|
|
||||||
}
|
|
||||||
|
|
||||||
localPlaylistManager.renamePlaylist(id, name);
|
|
||||||
final Disposable disposable = localPlaylistManager.renamePlaylist(id, name)
|
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
|
||||||
.subscribe(longs -> { /*Do nothing on success*/ }, throwable -> showError(
|
|
||||||
new ErrorInfo(throwable,
|
|
||||||
UserAction.REQUESTED_BOOKMARK,
|
|
||||||
"Changing playlist name")));
|
|
||||||
disposables.add(disposable);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,95 @@
|
||||||
|
package org.schabi.newpipe.local.bookmark;
|
||||||
|
|
||||||
|
import org.schabi.newpipe.database.playlist.PlaylistLocalItem;
|
||||||
|
import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry;
|
||||||
|
import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity;
|
||||||
|
import org.schabi.newpipe.local.playlist.LocalPlaylistManager;
|
||||||
|
import org.schabi.newpipe.local.playlist.RemotePlaylistManager;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import io.reactivex.rxjava3.core.Flowable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Takes care of remote and local playlists at once, hence "merged".
|
||||||
|
*/
|
||||||
|
public final class MergedPlaylistManager {
|
||||||
|
|
||||||
|
private MergedPlaylistManager() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Flowable<List<PlaylistLocalItem>> getMergedOrderedPlaylists(
|
||||||
|
final LocalPlaylistManager localPlaylistManager,
|
||||||
|
final RemotePlaylistManager remotePlaylistManager) {
|
||||||
|
return Flowable.combineLatest(
|
||||||
|
localPlaylistManager.getPlaylists(),
|
||||||
|
remotePlaylistManager.getPlaylists(),
|
||||||
|
MergedPlaylistManager::merge
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Merge localPlaylists and remotePlaylists by the display index.
|
||||||
|
* If two items have the same display index, sort them in {@code CASE_INSENSITIVE_ORDER}.
|
||||||
|
*
|
||||||
|
* @param localPlaylists local playlists, already sorted by display index
|
||||||
|
* @param remotePlaylists remote playlists, already sorted by display index
|
||||||
|
* @return merged playlists
|
||||||
|
*/
|
||||||
|
public static List<PlaylistLocalItem> merge(
|
||||||
|
final List<PlaylistMetadataEntry> localPlaylists,
|
||||||
|
final List<PlaylistRemoteEntity> remotePlaylists) {
|
||||||
|
|
||||||
|
// This algorithm is similar to the merge operation in merge sort.
|
||||||
|
final List<PlaylistLocalItem> result = new ArrayList<>(
|
||||||
|
localPlaylists.size() + remotePlaylists.size());
|
||||||
|
final List<PlaylistLocalItem> itemsWithSameIndex = new ArrayList<>();
|
||||||
|
|
||||||
|
int i = 0;
|
||||||
|
int j = 0;
|
||||||
|
while (i < localPlaylists.size()) {
|
||||||
|
while (j < remotePlaylists.size()) {
|
||||||
|
if (remotePlaylists.get(j).getDisplayIndex()
|
||||||
|
<= localPlaylists.get(i).getDisplayIndex()) {
|
||||||
|
addItem(result, remotePlaylists.get(j), itemsWithSameIndex);
|
||||||
|
j++;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
addItem(result, localPlaylists.get(i), itemsWithSameIndex);
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
while (j < remotePlaylists.size()) {
|
||||||
|
addItem(result, remotePlaylists.get(j), itemsWithSameIndex);
|
||||||
|
j++;
|
||||||
|
}
|
||||||
|
addItemsWithSameIndex(result, itemsWithSameIndex);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void addItem(final List<PlaylistLocalItem> result,
|
||||||
|
final PlaylistLocalItem item,
|
||||||
|
final List<PlaylistLocalItem> itemsWithSameIndex) {
|
||||||
|
if (!itemsWithSameIndex.isEmpty()
|
||||||
|
&& itemsWithSameIndex.get(0).getDisplayIndex() != item.getDisplayIndex()) {
|
||||||
|
// The new item has a different display index, add previous items with same
|
||||||
|
// index to the result.
|
||||||
|
addItemsWithSameIndex(result, itemsWithSameIndex);
|
||||||
|
itemsWithSameIndex.clear();
|
||||||
|
}
|
||||||
|
itemsWithSameIndex.add(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void addItemsWithSameIndex(final List<PlaylistLocalItem> result,
|
||||||
|
final List<PlaylistLocalItem> itemsWithSameIndex) {
|
||||||
|
Collections.sort(itemsWithSameIndex,
|
||||||
|
Comparator.comparing(PlaylistLocalItem::getOrderingName,
|
||||||
|
Comparator.nullsLast(String.CASE_INSENSITIVE_ORDER)));
|
||||||
|
result.addAll(itemsWithSameIndex);
|
||||||
|
}
|
||||||
|
}
|
|
@ -155,14 +155,14 @@ public final class PlaylistAppendDialog extends PlaylistDialog {
|
||||||
|
|
||||||
final Toast successToast = Toast.makeText(getContext(), toastText, Toast.LENGTH_SHORT);
|
final Toast successToast = Toast.makeText(getContext(), toastText, Toast.LENGTH_SHORT);
|
||||||
|
|
||||||
playlistDisposables.add(manager.appendToPlaylist(playlist.uid, streams)
|
playlistDisposables.add(manager.appendToPlaylist(playlist.getUid(), streams)
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.subscribe(ignored -> {
|
.subscribe(ignored -> {
|
||||||
successToast.show();
|
successToast.show();
|
||||||
|
|
||||||
if (playlist.thumbnailUrl.equals(PlaylistEntity.DEFAULT_THUMBNAIL)) {
|
if (playlist.thumbnailUrl.equals(PlaylistEntity.DEFAULT_THUMBNAIL)) {
|
||||||
playlistDisposables.add(manager
|
playlistDisposables.add(manager
|
||||||
.changePlaylistThumbnail(playlist.uid, streams.get(0).getUid(),
|
.changePlaylistThumbnail(playlist.getUid(), streams.get(0).getUid(),
|
||||||
false)
|
false)
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.subscribe(ignore -> successToast.show()));
|
.subscribe(ignore -> successToast.show()));
|
||||||
|
|
|
@ -137,7 +137,7 @@ class NotificationWorker(
|
||||||
.enqueueUniquePeriodicWork(
|
.enqueueUniquePeriodicWork(
|
||||||
WORK_TAG,
|
WORK_TAG,
|
||||||
if (force) {
|
if (force) {
|
||||||
ExistingPeriodicWorkPolicy.REPLACE
|
ExistingPeriodicWorkPolicy.CANCEL_AND_REENQUEUE
|
||||||
} else {
|
} else {
|
||||||
ExistingPeriodicWorkPolicy.KEEP
|
ExistingPeriodicWorkPolicy.KEEP
|
||||||
},
|
},
|
||||||
|
|
|
@ -26,7 +26,7 @@ object FeedEventManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
sealed class Event {
|
sealed class Event {
|
||||||
object IdleEvent : Event()
|
data object IdleEvent : Event()
|
||||||
data class ProgressEvent(val currentProgress: Int = -1, val maxProgress: Int = -1, @StringRes val progressMessage: Int = 0) : Event() {
|
data class ProgressEvent(val currentProgress: Int = -1, val maxProgress: Int = -1, @StringRes val progressMessage: Int = 0) : Event() {
|
||||||
constructor(@StringRes progressMessage: Int) : this(-1, -1, progressMessage)
|
constructor(@StringRes progressMessage: Int) : this(-1, -1, progressMessage)
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,7 +18,7 @@ data class FeedUpdateInfo(
|
||||||
@NotificationMode
|
@NotificationMode
|
||||||
val notificationMode: Int,
|
val notificationMode: Int,
|
||||||
val name: String,
|
val name: String,
|
||||||
val avatarUrl: String,
|
val avatarUrl: String?,
|
||||||
val url: String,
|
val url: String,
|
||||||
val serviceId: Int,
|
val serviceId: Int,
|
||||||
// description and subscriberCount are null if the constructor info is from the fast feed method
|
// description and subscriberCount are null if the constructor info is from the fast feed method
|
||||||
|
|
|
@ -0,0 +1,54 @@
|
||||||
|
package org.schabi.newpipe.local.holder;
|
||||||
|
|
||||||
|
import android.view.MotionEvent;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
|
||||||
|
import org.schabi.newpipe.R;
|
||||||
|
import org.schabi.newpipe.database.LocalItem;
|
||||||
|
import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry;
|
||||||
|
import org.schabi.newpipe.local.LocalItemBuilder;
|
||||||
|
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
||||||
|
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
|
||||||
|
public class LocalBookmarkPlaylistItemHolder extends LocalPlaylistItemHolder {
|
||||||
|
private final View itemHandleView;
|
||||||
|
|
||||||
|
public LocalBookmarkPlaylistItemHolder(final LocalItemBuilder infoItemBuilder,
|
||||||
|
final ViewGroup parent) {
|
||||||
|
this(infoItemBuilder, R.layout.list_playlist_bookmark_item, parent);
|
||||||
|
}
|
||||||
|
|
||||||
|
LocalBookmarkPlaylistItemHolder(final LocalItemBuilder infoItemBuilder, final int layoutId,
|
||||||
|
final ViewGroup parent) {
|
||||||
|
super(infoItemBuilder, layoutId, parent);
|
||||||
|
itemHandleView = itemView.findViewById(R.id.itemHandle);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void updateFromItem(final LocalItem localItem,
|
||||||
|
final HistoryRecordManager historyRecordManager,
|
||||||
|
final DateTimeFormatter dateTimeFormatter) {
|
||||||
|
if (!(localItem instanceof PlaylistMetadataEntry)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final PlaylistMetadataEntry item = (PlaylistMetadataEntry) localItem;
|
||||||
|
|
||||||
|
itemHandleView.setOnTouchListener(getOnTouchListener(item));
|
||||||
|
|
||||||
|
super.updateFromItem(localItem, historyRecordManager, dateTimeFormatter);
|
||||||
|
}
|
||||||
|
|
||||||
|
private View.OnTouchListener getOnTouchListener(final PlaylistMetadataEntry item) {
|
||||||
|
return (view, motionEvent) -> {
|
||||||
|
view.performClick();
|
||||||
|
if (itemBuilder != null && itemBuilder.getOnItemSelectedListener() != null
|
||||||
|
&& motionEvent.getActionMasked() == MotionEvent.ACTION_DOWN) {
|
||||||
|
itemBuilder.getOnItemSelectedListener().drag(item,
|
||||||
|
LocalBookmarkPlaylistItemHolder.this);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,54 @@
|
||||||
|
package org.schabi.newpipe.local.holder;
|
||||||
|
|
||||||
|
import android.view.MotionEvent;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
|
||||||
|
import org.schabi.newpipe.R;
|
||||||
|
import org.schabi.newpipe.database.LocalItem;
|
||||||
|
import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity;
|
||||||
|
import org.schabi.newpipe.local.LocalItemBuilder;
|
||||||
|
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
||||||
|
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
|
||||||
|
public class RemoteBookmarkPlaylistItemHolder extends RemotePlaylistItemHolder {
|
||||||
|
private final View itemHandleView;
|
||||||
|
|
||||||
|
public RemoteBookmarkPlaylistItemHolder(final LocalItemBuilder infoItemBuilder,
|
||||||
|
final ViewGroup parent) {
|
||||||
|
this(infoItemBuilder, R.layout.list_playlist_bookmark_item, parent);
|
||||||
|
}
|
||||||
|
|
||||||
|
RemoteBookmarkPlaylistItemHolder(final LocalItemBuilder infoItemBuilder, final int layoutId,
|
||||||
|
final ViewGroup parent) {
|
||||||
|
super(infoItemBuilder, layoutId, parent);
|
||||||
|
itemHandleView = itemView.findViewById(R.id.itemHandle);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void updateFromItem(final LocalItem localItem,
|
||||||
|
final HistoryRecordManager historyRecordManager,
|
||||||
|
final DateTimeFormatter dateTimeFormatter) {
|
||||||
|
if (!(localItem instanceof PlaylistRemoteEntity)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final PlaylistRemoteEntity item = (PlaylistRemoteEntity) localItem;
|
||||||
|
|
||||||
|
itemHandleView.setOnTouchListener(getOnTouchListener(item));
|
||||||
|
|
||||||
|
super.updateFromItem(localItem, historyRecordManager, dateTimeFormatter);
|
||||||
|
}
|
||||||
|
|
||||||
|
private View.OnTouchListener getOnTouchListener(final PlaylistRemoteEntity item) {
|
||||||
|
return (view, motionEvent) -> {
|
||||||
|
view.performClick();
|
||||||
|
if (itemBuilder != null && itemBuilder.getOnItemSelectedListener() != null
|
||||||
|
&& motionEvent.getActionMasked() == MotionEvent.ACTION_DOWN) {
|
||||||
|
itemBuilder.getOnItemSelectedListener().drag(item,
|
||||||
|
RemoteBookmarkPlaylistItemHolder.this);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
|
@ -14,6 +14,7 @@ import org.schabi.newpipe.util.ServiceHelper;
|
||||||
import java.time.format.DateTimeFormatter;
|
import java.time.format.DateTimeFormatter;
|
||||||
|
|
||||||
public class RemotePlaylistItemHolder extends PlaylistItemHolder {
|
public class RemotePlaylistItemHolder extends PlaylistItemHolder {
|
||||||
|
|
||||||
public RemotePlaylistItemHolder(final LocalItemBuilder infoItemBuilder,
|
public RemotePlaylistItemHolder(final LocalItemBuilder infoItemBuilder,
|
||||||
final ViewGroup parent) {
|
final ViewGroup parent) {
|
||||||
super(infoItemBuilder, parent);
|
super(infoItemBuilder, parent);
|
||||||
|
|
|
@ -49,6 +49,8 @@ import org.schabi.newpipe.local.BaseLocalListFragment;
|
||||||
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
||||||
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
||||||
import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
|
import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
|
||||||
|
import org.schabi.newpipe.util.debounce.DebounceSavable;
|
||||||
|
import org.schabi.newpipe.util.debounce.DebounceSaver;
|
||||||
import org.schabi.newpipe.util.Localization;
|
import org.schabi.newpipe.util.Localization;
|
||||||
import org.schabi.newpipe.util.NavigationHelper;
|
import org.schabi.newpipe.util.NavigationHelper;
|
||||||
import org.schabi.newpipe.util.OnClickGesture;
|
import org.schabi.newpipe.util.OnClickGesture;
|
||||||
|
@ -58,7 +60,6 @@ import org.schabi.newpipe.util.external_communication.ShareUtils;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.concurrent.TimeUnit;
|
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
@ -68,12 +69,10 @@ import io.reactivex.rxjava3.core.Single;
|
||||||
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
||||||
import io.reactivex.rxjava3.disposables.Disposable;
|
import io.reactivex.rxjava3.disposables.Disposable;
|
||||||
import io.reactivex.rxjava3.schedulers.Schedulers;
|
import io.reactivex.rxjava3.schedulers.Schedulers;
|
||||||
import io.reactivex.rxjava3.subjects.PublishSubject;
|
|
||||||
|
|
||||||
public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistStreamEntry>, Void>
|
public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistStreamEntry>, Void>
|
||||||
implements PlaylistControlViewHolder {
|
implements PlaylistControlViewHolder, DebounceSavable {
|
||||||
/** Save the list 10 seconds after the last change occurred. */
|
|
||||||
private static final long SAVE_DEBOUNCE_MILLIS = 10000;
|
|
||||||
private static final int MINIMUM_INITIAL_DRAG_VELOCITY = 12;
|
private static final int MINIMUM_INITIAL_DRAG_VELOCITY = 12;
|
||||||
@State
|
@State
|
||||||
protected Long playlistId;
|
protected Long playlistId;
|
||||||
|
@ -90,13 +89,12 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
|
||||||
private LocalPlaylistManager playlistManager;
|
private LocalPlaylistManager playlistManager;
|
||||||
private Subscription databaseSubscription;
|
private Subscription databaseSubscription;
|
||||||
|
|
||||||
private PublishSubject<Long> debouncedSaveSignal;
|
|
||||||
private CompositeDisposable disposables;
|
private CompositeDisposable disposables;
|
||||||
|
|
||||||
/** Whether the playlist has been fully loaded from db. */
|
/** Whether the playlist has been fully loaded from db. */
|
||||||
private AtomicBoolean isLoadingComplete;
|
private AtomicBoolean isLoadingComplete;
|
||||||
/** Whether the playlist has been modified (e.g. items reordered or deleted) */
|
/** Used to debounce saving playlist edits to disk. */
|
||||||
private AtomicBoolean isModified;
|
private DebounceSaver debounceSaver;
|
||||||
/** Flag to prevent simultaneous rewrites of the playlist. */
|
/** Flag to prevent simultaneous rewrites of the playlist. */
|
||||||
private boolean isRewritingPlaylist = false;
|
private boolean isRewritingPlaylist = false;
|
||||||
|
|
||||||
|
@ -121,12 +119,11 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
|
||||||
public void onCreate(final Bundle savedInstanceState) {
|
public void onCreate(final Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
playlistManager = new LocalPlaylistManager(NewPipeDatabase.getInstance(requireContext()));
|
playlistManager = new LocalPlaylistManager(NewPipeDatabase.getInstance(requireContext()));
|
||||||
debouncedSaveSignal = PublishSubject.create();
|
|
||||||
|
|
||||||
disposables = new CompositeDisposable();
|
disposables = new CompositeDisposable();
|
||||||
|
|
||||||
isLoadingComplete = new AtomicBoolean();
|
isLoadingComplete = new AtomicBoolean();
|
||||||
isModified = new AtomicBoolean();
|
debounceSaver = new DebounceSaver(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -166,17 +163,6 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
|
||||||
return headerBinding;
|
return headerBinding;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* <p>Commit changes immediately if the playlist has been modified.</p>
|
|
||||||
* Delete operations and other modifications will be committed to ensure that the database
|
|
||||||
* is up to date, e.g. when the user adds the just deleted stream from another fragment.
|
|
||||||
*/
|
|
||||||
public void commitChanges() {
|
|
||||||
if (isModified != null && isModified.get()) {
|
|
||||||
saveImmediate();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void initListeners() {
|
protected void initListeners() {
|
||||||
super.initListeners();
|
super.initListeners();
|
||||||
|
@ -243,10 +229,13 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
|
||||||
if (disposables != null) {
|
if (disposables != null) {
|
||||||
disposables.clear();
|
disposables.clear();
|
||||||
}
|
}
|
||||||
disposables.add(getDebouncedSaver());
|
|
||||||
|
if (debounceSaver != null) {
|
||||||
|
disposables.add(debounceSaver.getDebouncedSaver());
|
||||||
|
debounceSaver.setNoChangesToSave();
|
||||||
|
}
|
||||||
|
|
||||||
isLoadingComplete.set(false);
|
isLoadingComplete.set(false);
|
||||||
isModified.set(false);
|
|
||||||
|
|
||||||
playlistManager.getPlaylistStreams(playlistId)
|
playlistManager.getPlaylistStreams(playlistId)
|
||||||
.onBackpressureLatest()
|
.onBackpressureLatest()
|
||||||
|
@ -304,8 +293,8 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
|
||||||
@Override
|
@Override
|
||||||
public void onDestroy() {
|
public void onDestroy() {
|
||||||
super.onDestroy();
|
super.onDestroy();
|
||||||
if (debouncedSaveSignal != null) {
|
if (debounceSaver != null) {
|
||||||
debouncedSaveSignal.onComplete();
|
debounceSaver.getDebouncedSaveSignal().onComplete();
|
||||||
}
|
}
|
||||||
if (disposables != null) {
|
if (disposables != null) {
|
||||||
disposables.dispose();
|
disposables.dispose();
|
||||||
|
@ -314,12 +303,11 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
|
||||||
tabsPagerAdapter.getLocalPlaylistFragments().remove(this);
|
tabsPagerAdapter.getLocalPlaylistFragments().remove(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
debouncedSaveSignal = null;
|
debounceSaver = null;
|
||||||
playlistManager = null;
|
playlistManager = null;
|
||||||
disposables = null;
|
disposables = null;
|
||||||
|
|
||||||
isLoadingComplete = null;
|
isLoadingComplete = null;
|
||||||
isModified = null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////
|
||||||
|
@ -343,7 +331,7 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
|
||||||
@Override
|
@Override
|
||||||
public void onNext(final List<PlaylistStreamEntry> streams) {
|
public void onNext(final List<PlaylistStreamEntry> streams) {
|
||||||
// Skip handling the result after it has been modified
|
// Skip handling the result after it has been modified
|
||||||
if (isModified == null || !isModified.get()) {
|
if (debounceSaver == null || !debounceSaver.getIsModified()) {
|
||||||
handleResult(streams);
|
handleResult(streams);
|
||||||
isLoadingComplete.set(true);
|
isLoadingComplete.set(true);
|
||||||
}
|
}
|
||||||
|
@ -495,14 +483,14 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
|
||||||
|
|
||||||
itemListAdapter.clearStreamItemList();
|
itemListAdapter.clearStreamItemList();
|
||||||
itemListAdapter.addItems(itemsToKeep);
|
itemListAdapter.addItems(itemsToKeep);
|
||||||
saveChanges();
|
debounceSaver.setHasChangesToSave();
|
||||||
|
|
||||||
if (thumbnailVideoRemoved) {
|
if (thumbnailVideoRemoved) {
|
||||||
updateThumbnailUrl();
|
updateThumbnailUrl();
|
||||||
}
|
}
|
||||||
|
|
||||||
final long videoCount = itemListAdapter.getItemsList().size();
|
final long videoCount = itemListAdapter.getItemsList().size();
|
||||||
setVideoCount(videoCount);
|
setStreamCountAndOverallDuration(itemListAdapter.getItemsList());
|
||||||
if (videoCount == 0) {
|
if (videoCount == 0) {
|
||||||
showEmptyState();
|
showEmptyState();
|
||||||
}
|
}
|
||||||
|
@ -532,7 +520,7 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
|
||||||
itemsList.getLayoutManager().onRestoreInstanceState(itemsListState);
|
itemsList.getLayoutManager().onRestoreInstanceState(itemsListState);
|
||||||
itemsListState = null;
|
itemsListState = null;
|
||||||
}
|
}
|
||||||
setVideoCount(itemListAdapter.getItemsList().size());
|
setStreamCountAndOverallDuration(itemListAdapter.getItemsList());
|
||||||
|
|
||||||
PlayButtonHelper.initPlaylistControlClickListener(activity, playlistControlBinding, this);
|
PlayButtonHelper.initPlaylistControlClickListener(activity, playlistControlBinding, this);
|
||||||
|
|
||||||
|
@ -665,8 +653,8 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
|
||||||
.subscribe(itemsToKeep -> {
|
.subscribe(itemsToKeep -> {
|
||||||
itemListAdapter.clearStreamItemList();
|
itemListAdapter.clearStreamItemList();
|
||||||
itemListAdapter.addItems(itemsToKeep);
|
itemListAdapter.addItems(itemsToKeep);
|
||||||
setVideoCount(itemListAdapter.getItemsList().size());
|
setStreamCountAndOverallDuration(itemListAdapter.getItemsList());
|
||||||
saveChanges();
|
debounceSaver.setHasChangesToSave();
|
||||||
|
|
||||||
hideLoading();
|
hideLoading();
|
||||||
isRewritingPlaylist = false;
|
isRewritingPlaylist = false;
|
||||||
|
@ -684,42 +672,24 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
|
||||||
updateThumbnailUrl();
|
updateThumbnailUrl();
|
||||||
}
|
}
|
||||||
|
|
||||||
setVideoCount(itemListAdapter.getItemsList().size());
|
setStreamCountAndOverallDuration(itemListAdapter.getItemsList());
|
||||||
saveChanges();
|
debounceSaver.setHasChangesToSave();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void saveChanges() {
|
/**
|
||||||
if (isModified == null || debouncedSaveSignal == null) {
|
* <p>Commit changes immediately if the playlist has been modified.</p>
|
||||||
return;
|
* Delete operations and other modifications will be committed to ensure that the database
|
||||||
}
|
* is up to date, e.g. when the user adds the just deleted stream from another fragment.
|
||||||
|
*/
|
||||||
isModified.set(true);
|
@Override
|
||||||
debouncedSaveSignal.onNext(System.currentTimeMillis());
|
public void saveImmediate() {
|
||||||
}
|
|
||||||
|
|
||||||
private Disposable getDebouncedSaver() {
|
|
||||||
if (debouncedSaveSignal == null) {
|
|
||||||
return Disposable.empty();
|
|
||||||
}
|
|
||||||
|
|
||||||
return debouncedSaveSignal
|
|
||||||
.debounce(SAVE_DEBOUNCE_MILLIS, TimeUnit.MILLISECONDS)
|
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
|
||||||
.subscribe(ignored -> saveImmediate(), throwable ->
|
|
||||||
showError(new ErrorInfo(throwable, UserAction.SOMETHING_ELSE,
|
|
||||||
"Debounced saver")));
|
|
||||||
}
|
|
||||||
|
|
||||||
private void saveImmediate() {
|
|
||||||
if (playlistManager == null || itemListAdapter == null) {
|
if (playlistManager == null || itemListAdapter == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// List must be loaded and modified in order to save
|
// List must be loaded and modified in order to save
|
||||||
if (isLoadingComplete == null || isModified == null
|
if (isLoadingComplete == null || debounceSaver == null
|
||||||
|| !isLoadingComplete.get() || !isModified.get()) {
|
|| !isLoadingComplete.get() || !debounceSaver.getIsModified()) {
|
||||||
Log.w(TAG, "Attempting to save playlist when local playlist "
|
|
||||||
+ "is not loaded or not modified: playlist id=[" + playlistId + "]");
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -740,8 +710,8 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.subscribe(
|
.subscribe(
|
||||||
() -> {
|
() -> {
|
||||||
if (isModified != null) {
|
if (debounceSaver != null) {
|
||||||
isModified.set(false);
|
debounceSaver.setNoChangesToSave();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
throwable -> showError(new ErrorInfo(throwable,
|
throwable -> showError(new ErrorInfo(throwable,
|
||||||
|
@ -784,7 +754,7 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
|
||||||
final int targetIndex = target.getBindingAdapterPosition();
|
final int targetIndex = target.getBindingAdapterPosition();
|
||||||
final boolean isSwapped = itemListAdapter.swapItems(sourceIndex, targetIndex);
|
final boolean isSwapped = itemListAdapter.swapItems(sourceIndex, targetIndex);
|
||||||
if (isSwapped) {
|
if (isSwapped) {
|
||||||
saveChanges();
|
debounceSaver.setHasChangesToSave();
|
||||||
}
|
}
|
||||||
return isSwapped;
|
return isSwapped;
|
||||||
}
|
}
|
||||||
|
@ -855,10 +825,21 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
|
||||||
this.name = !TextUtils.isEmpty(title) ? title : "";
|
this.name = !TextUtils.isEmpty(title) ? title : "";
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setVideoCount(final long count) {
|
private void setStreamCountAndOverallDuration(final ArrayList<LocalItem> itemsList) {
|
||||||
if (activity != null && headerBinding != null) {
|
if (activity != null && headerBinding != null) {
|
||||||
headerBinding.playlistStreamCount.setText(Localization
|
final long streamCount = itemsList.size();
|
||||||
.localizeStreamCount(activity, count));
|
final long playlistOverallDurationSeconds = itemsList.stream()
|
||||||
|
.filter(PlaylistStreamEntry.class::isInstance)
|
||||||
|
.map(PlaylistStreamEntry.class::cast)
|
||||||
|
.map(PlaylistStreamEntry::getStreamEntity)
|
||||||
|
.mapToLong(StreamEntity::getDuration)
|
||||||
|
.sum();
|
||||||
|
headerBinding.playlistStreamCount.setText(
|
||||||
|
Localization.concatenateStrings(
|
||||||
|
Localization.localizeStreamCount(activity, streamCount),
|
||||||
|
Localization.getDurationString(playlistOverallDurationSeconds,
|
||||||
|
true, true))
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -19,7 +19,6 @@ import java.util.List;
|
||||||
import io.reactivex.rxjava3.core.Completable;
|
import io.reactivex.rxjava3.core.Completable;
|
||||||
import io.reactivex.rxjava3.core.Flowable;
|
import io.reactivex.rxjava3.core.Flowable;
|
||||||
import io.reactivex.rxjava3.core.Maybe;
|
import io.reactivex.rxjava3.core.Maybe;
|
||||||
import io.reactivex.rxjava3.core.Single;
|
|
||||||
import io.reactivex.rxjava3.schedulers.Schedulers;
|
import io.reactivex.rxjava3.schedulers.Schedulers;
|
||||||
|
|
||||||
public class LocalPlaylistManager {
|
public class LocalPlaylistManager {
|
||||||
|
@ -43,10 +42,13 @@ public class LocalPlaylistManager {
|
||||||
return Maybe.empty();
|
return Maybe.empty();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Save to the database directly.
|
||||||
|
// Make sure the new playlist is always on the top of bookmark.
|
||||||
|
// The index will be reassigned to non-negative number in BookmarkFragment.
|
||||||
return Maybe.fromCallable(() -> database.runInTransaction(() -> {
|
return Maybe.fromCallable(() -> database.runInTransaction(() -> {
|
||||||
final List<Long> streamIds = streamTable.upsertAll(streams);
|
final List<Long> streamIds = streamTable.upsertAll(streams);
|
||||||
final PlaylistEntity newPlaylist = new PlaylistEntity(name, false,
|
final PlaylistEntity newPlaylist = new PlaylistEntity(name, false,
|
||||||
streamIds.get(0));
|
streamIds.get(0), -1);
|
||||||
|
|
||||||
return insertJoinEntities(playlistTable.insert(newPlaylist),
|
return insertJoinEntities(playlistTable.insert(newPlaylist),
|
||||||
streamIds, 0);
|
streamIds, 0);
|
||||||
|
@ -89,8 +91,20 @@ public class LocalPlaylistManager {
|
||||||
})).subscribeOn(Schedulers.io());
|
})).subscribeOn(Schedulers.io());
|
||||||
}
|
}
|
||||||
|
|
||||||
public Flowable<List<PlaylistMetadataEntry>> getPlaylists() {
|
public Completable updatePlaylists(final List<PlaylistMetadataEntry> updateItems,
|
||||||
return playlistStreamTable.getPlaylistMetadata().subscribeOn(Schedulers.io());
|
final List<Long> deletedItems) {
|
||||||
|
final List<PlaylistEntity> items = new ArrayList<>(updateItems.size());
|
||||||
|
for (final PlaylistMetadataEntry item : updateItems) {
|
||||||
|
items.add(new PlaylistEntity(item));
|
||||||
|
}
|
||||||
|
return Completable.fromRunnable(() -> database.runInTransaction(() -> {
|
||||||
|
for (final Long uid : deletedItems) {
|
||||||
|
playlistTable.deletePlaylist(uid);
|
||||||
|
}
|
||||||
|
for (final PlaylistEntity item : items) {
|
||||||
|
playlistTable.upsertPlaylist(item);
|
||||||
|
}
|
||||||
|
})).subscribeOn(Schedulers.io());
|
||||||
}
|
}
|
||||||
|
|
||||||
public Flowable<List<PlaylistStreamEntry>> getDistinctPlaylistStreams(final long playlistId) {
|
public Flowable<List<PlaylistStreamEntry>> getDistinctPlaylistStreams(final long playlistId) {
|
||||||
|
@ -110,13 +124,12 @@ public class LocalPlaylistManager {
|
||||||
.subscribeOn(Schedulers.io());
|
.subscribeOn(Schedulers.io());
|
||||||
}
|
}
|
||||||
|
|
||||||
public Flowable<List<PlaylistStreamEntry>> getPlaylistStreams(final long playlistId) {
|
public Flowable<List<PlaylistMetadataEntry>> getPlaylists() {
|
||||||
return playlistStreamTable.getOrderedStreamsOf(playlistId).subscribeOn(Schedulers.io());
|
return playlistStreamTable.getPlaylistMetadata().subscribeOn(Schedulers.io());
|
||||||
}
|
}
|
||||||
|
|
||||||
public Single<Integer> deletePlaylist(final long playlistId) {
|
public Flowable<List<PlaylistStreamEntry>> getPlaylistStreams(final long playlistId) {
|
||||||
return Single.fromCallable(() -> playlistTable.deletePlaylist(playlistId))
|
return playlistStreamTable.getOrderedStreamsOf(playlistId).subscribeOn(Schedulers.io());
|
||||||
.subscribeOn(Schedulers.io());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public Maybe<Integer> renamePlaylist(final long playlistId, final String name) {
|
public Maybe<Integer> renamePlaylist(final long playlistId, final String name) {
|
||||||
|
|
|
@ -7,20 +7,23 @@ import org.schabi.newpipe.extractor.playlist.PlaylistInfo;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
import io.reactivex.rxjava3.core.Completable;
|
||||||
import io.reactivex.rxjava3.core.Flowable;
|
import io.reactivex.rxjava3.core.Flowable;
|
||||||
import io.reactivex.rxjava3.core.Single;
|
import io.reactivex.rxjava3.core.Single;
|
||||||
import io.reactivex.rxjava3.schedulers.Schedulers;
|
import io.reactivex.rxjava3.schedulers.Schedulers;
|
||||||
|
|
||||||
public class RemotePlaylistManager {
|
public class RemotePlaylistManager {
|
||||||
|
|
||||||
|
private final AppDatabase database;
|
||||||
private final PlaylistRemoteDAO playlistRemoteTable;
|
private final PlaylistRemoteDAO playlistRemoteTable;
|
||||||
|
|
||||||
public RemotePlaylistManager(final AppDatabase db) {
|
public RemotePlaylistManager(final AppDatabase db) {
|
||||||
|
database = db;
|
||||||
playlistRemoteTable = db.playlistRemoteDAO();
|
playlistRemoteTable = db.playlistRemoteDAO();
|
||||||
}
|
}
|
||||||
|
|
||||||
public Flowable<List<PlaylistRemoteEntity>> getPlaylists() {
|
public Flowable<List<PlaylistRemoteEntity>> getPlaylists() {
|
||||||
return playlistRemoteTable.getAll().subscribeOn(Schedulers.io());
|
return playlistRemoteTable.getPlaylists().subscribeOn(Schedulers.io());
|
||||||
}
|
}
|
||||||
|
|
||||||
public Flowable<List<PlaylistRemoteEntity>> getPlaylist(final PlaylistInfo info) {
|
public Flowable<List<PlaylistRemoteEntity>> getPlaylist(final PlaylistInfo info) {
|
||||||
|
@ -33,6 +36,18 @@ public class RemotePlaylistManager {
|
||||||
.subscribeOn(Schedulers.io());
|
.subscribeOn(Schedulers.io());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Completable updatePlaylists(final List<PlaylistRemoteEntity> updateItems,
|
||||||
|
final List<Long> deletedItems) {
|
||||||
|
return Completable.fromRunnable(() -> database.runInTransaction(() -> {
|
||||||
|
for (final Long uid: deletedItems) {
|
||||||
|
playlistRemoteTable.deletePlaylist(uid);
|
||||||
|
}
|
||||||
|
for (final PlaylistRemoteEntity item: updateItems) {
|
||||||
|
playlistRemoteTable.upsert(item);
|
||||||
|
}
|
||||||
|
})).subscribeOn(Schedulers.io());
|
||||||
|
}
|
||||||
|
|
||||||
public Single<Long> onBookmark(final PlaylistInfo playlistInfo) {
|
public Single<Long> onBookmark(final PlaylistInfo playlistInfo) {
|
||||||
return Single.fromCallable(() -> {
|
return Single.fromCallable(() -> {
|
||||||
final PlaylistRemoteEntity playlist = new PlaylistRemoteEntity(playlistInfo);
|
final PlaylistRemoteEntity playlist = new PlaylistRemoteEntity(playlistInfo);
|
||||||
|
|
|
@ -100,7 +100,9 @@ class SubscriptionManager(context: Context) {
|
||||||
val subscriptionEntity = subscriptionTable.getSubscription(info.uid)
|
val subscriptionEntity = subscriptionTable.getSubscription(info.uid)
|
||||||
|
|
||||||
subscriptionEntity.name = info.name
|
subscriptionEntity.name = info.name
|
||||||
subscriptionEntity.avatarUrl = info.avatarUrl
|
|
||||||
|
// some services do not provide an avatar URL
|
||||||
|
info.avatarUrl?.let { subscriptionEntity.avatarUrl = it }
|
||||||
|
|
||||||
// these two fields are null if the feed info was fetched using the fast feed method
|
// these two fields are null if the feed info was fetched using the fast feed method
|
||||||
info.description?.let { subscriptionEntity.description = it }
|
info.description?.let { subscriptionEntity.description = it }
|
||||||
|
|
|
@ -55,10 +55,10 @@ class FeedGroupDialog : DialogFragment(), BackPressable {
|
||||||
private var groupSortOrder: Long = -1
|
private var groupSortOrder: Long = -1
|
||||||
|
|
||||||
sealed class ScreenState : Serializable {
|
sealed class ScreenState : Serializable {
|
||||||
object InitialScreen : ScreenState()
|
data object InitialScreen : ScreenState()
|
||||||
object IconPickerScreen : ScreenState()
|
data object IconPickerScreen : ScreenState()
|
||||||
object SubscriptionsPickerScreen : ScreenState()
|
data object SubscriptionsPickerScreen : ScreenState()
|
||||||
object DeleteScreen : ScreenState()
|
data object DeleteScreen : ScreenState()
|
||||||
}
|
}
|
||||||
|
|
||||||
@State @JvmField var selectedIcon: FeedGroupIcon? = null
|
@State @JvmField var selectedIcon: FeedGroupIcon? = null
|
||||||
|
@ -370,7 +370,7 @@ class FeedGroupDialog : DialogFragment(), BackPressable {
|
||||||
|
|
||||||
private fun setupIconPicker() {
|
private fun setupIconPicker() {
|
||||||
val groupAdapter = GroupieAdapter()
|
val groupAdapter = GroupieAdapter()
|
||||||
groupAdapter.addAll(FeedGroupIcon.values().map { PickerIconItem(it) })
|
groupAdapter.addAll(FeedGroupIcon.entries.map { PickerIconItem(it) })
|
||||||
|
|
||||||
feedGroupCreateBinding.iconSelector.apply {
|
feedGroupCreateBinding.iconSelector.apply {
|
||||||
layoutManager = GridLayoutManager(requireContext(), 7, RecyclerView.VERTICAL, false)
|
layoutManager = GridLayoutManager(requireContext(), 7, RecyclerView.VERTICAL, false)
|
||||||
|
|
|
@ -110,8 +110,8 @@ class FeedGroupDialogViewModel(
|
||||||
}
|
}
|
||||||
|
|
||||||
sealed class DialogEvent {
|
sealed class DialogEvent {
|
||||||
object ProcessingEvent : DialogEvent()
|
data object ProcessingEvent : DialogEvent()
|
||||||
object SuccessEvent : DialogEvent()
|
data object SuccessEvent : DialogEvent()
|
||||||
}
|
}
|
||||||
|
|
||||||
data class Filter(val query: String, val showOnlyUngrouped: Boolean)
|
data class Filter(val query: String, val showOnlyUngrouped: Boolean)
|
||||||
|
|
|
@ -25,6 +25,7 @@ import android.content.Intent;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
|
import androidx.core.content.IntentCompat;
|
||||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
|
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
|
||||||
|
|
||||||
import org.reactivestreams.Subscriber;
|
import org.reactivestreams.Subscriber;
|
||||||
|
@ -65,7 +66,7 @@ public class SubscriptionsExportService extends BaseImportExportService {
|
||||||
return START_NOT_STICKY;
|
return START_NOT_STICKY;
|
||||||
}
|
}
|
||||||
|
|
||||||
final Uri path = intent.getParcelableExtra(KEY_FILE_PATH);
|
final Uri path = IntentCompat.getParcelableExtra(intent, KEY_FILE_PATH, Uri.class);
|
||||||
if (path == null) {
|
if (path == null) {
|
||||||
stopAndReportError(new IllegalStateException(
|
stopAndReportError(new IllegalStateException(
|
||||||
"Exporting to a file, but the path is null"),
|
"Exporting to a file, but the path is null"),
|
||||||
|
|
|
@ -30,6 +30,7 @@ import android.util.Pair;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.core.content.IntentCompat;
|
||||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
|
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
|
||||||
|
|
||||||
import org.reactivestreams.Subscriber;
|
import org.reactivestreams.Subscriber;
|
||||||
|
@ -108,7 +109,7 @@ public class SubscriptionsImportService extends BaseImportExportService {
|
||||||
if (currentMode == CHANNEL_URL_MODE) {
|
if (currentMode == CHANNEL_URL_MODE) {
|
||||||
channelUrl = intent.getStringExtra(KEY_VALUE);
|
channelUrl = intent.getStringExtra(KEY_VALUE);
|
||||||
} else {
|
} else {
|
||||||
final Uri uri = intent.getParcelableExtra(KEY_VALUE);
|
final Uri uri = IntentCompat.getParcelableExtra(intent, KEY_VALUE, Uri.class);
|
||||||
if (uri == null) {
|
if (uri == null) {
|
||||||
stopAndReportError(new IllegalStateException(
|
stopAndReportError(new IllegalStateException(
|
||||||
"Importing from input stream, but file path is null"),
|
"Importing from input stream, but file path is null"),
|
||||||
|
|
|
@ -160,13 +160,12 @@ class MainPlayerGestureListener(
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onScroll(
|
override fun onScroll(
|
||||||
initialEvent: MotionEvent,
|
initialEvent: MotionEvent?,
|
||||||
movingEvent: MotionEvent,
|
movingEvent: MotionEvent,
|
||||||
distanceX: Float,
|
distanceX: Float,
|
||||||
distanceY: Float
|
distanceY: Float
|
||||||
): Boolean {
|
): Boolean {
|
||||||
|
if (initialEvent == null || !playerUi.isFullscreen) {
|
||||||
if (!playerUi.isFullscreen) {
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -167,7 +167,7 @@ class PopupPlayerGestureListener(
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onFling(
|
override fun onFling(
|
||||||
e1: MotionEvent,
|
e1: MotionEvent?,
|
||||||
e2: MotionEvent,
|
e2: MotionEvent,
|
||||||
velocityX: Float,
|
velocityX: Float,
|
||||||
velocityY: Float
|
velocityY: Float
|
||||||
|
@ -218,11 +218,14 @@ class PopupPlayerGestureListener(
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onScroll(
|
override fun onScroll(
|
||||||
initialEvent: MotionEvent,
|
initialEvent: MotionEvent?,
|
||||||
movingEvent: MotionEvent,
|
movingEvent: MotionEvent,
|
||||||
distanceX: Float,
|
distanceX: Float,
|
||||||
distanceY: Float
|
distanceY: Float
|
||||||
): Boolean {
|
): Boolean {
|
||||||
|
if (initialEvent == null) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
if (isResizing) {
|
if (isResizing) {
|
||||||
return super.onScroll(initialEvent, movingEvent, distanceX, distanceY)
|
return super.onScroll(initialEvent, movingEvent, distanceX, distanceY)
|
||||||
|
|
|
@ -1,10 +1,12 @@
|
||||||
package org.schabi.newpipe.player.mediasession;
|
package org.schabi.newpipe.player.mediasession;
|
||||||
|
|
||||||
import static org.schabi.newpipe.MainActivity.DEBUG;
|
import static org.schabi.newpipe.MainActivity.DEBUG;
|
||||||
|
import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_RECREATE_NOTIFICATION;
|
||||||
|
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
import android.graphics.Bitmap;
|
import android.graphics.Bitmap;
|
||||||
|
import android.os.Build;
|
||||||
import android.support.v4.media.MediaMetadataCompat;
|
import android.support.v4.media.MediaMetadataCompat;
|
||||||
import android.support.v4.media.session.MediaSessionCompat;
|
import android.support.v4.media.session.MediaSessionCompat;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
@ -14,15 +16,23 @@ import androidx.annotation.Nullable;
|
||||||
import androidx.media.session.MediaButtonReceiver;
|
import androidx.media.session.MediaButtonReceiver;
|
||||||
|
|
||||||
import com.google.android.exoplayer2.ForwardingPlayer;
|
import com.google.android.exoplayer2.ForwardingPlayer;
|
||||||
|
import com.google.android.exoplayer2.Player.RepeatMode;
|
||||||
import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector;
|
import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector;
|
||||||
|
|
||||||
import org.schabi.newpipe.R;
|
import org.schabi.newpipe.R;
|
||||||
|
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||||
import org.schabi.newpipe.player.Player;
|
import org.schabi.newpipe.player.Player;
|
||||||
|
import org.schabi.newpipe.player.notification.NotificationActionData;
|
||||||
|
import org.schabi.newpipe.player.notification.NotificationConstants;
|
||||||
import org.schabi.newpipe.player.ui.PlayerUi;
|
import org.schabi.newpipe.player.ui.PlayerUi;
|
||||||
import org.schabi.newpipe.player.ui.VideoPlayerUi;
|
import org.schabi.newpipe.player.ui.VideoPlayerUi;
|
||||||
import org.schabi.newpipe.util.StreamTypeUtil;
|
import org.schabi.newpipe.util.StreamTypeUtil;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import java.util.stream.IntStream;
|
||||||
|
|
||||||
public class MediaSessionPlayerUi extends PlayerUi
|
public class MediaSessionPlayerUi extends PlayerUi
|
||||||
implements SharedPreferences.OnSharedPreferenceChangeListener {
|
implements SharedPreferences.OnSharedPreferenceChangeListener {
|
||||||
|
@ -34,6 +44,10 @@ public class MediaSessionPlayerUi extends PlayerUi
|
||||||
private final String ignoreHardwareMediaButtonsKey;
|
private final String ignoreHardwareMediaButtonsKey;
|
||||||
private boolean shouldIgnoreHardwareMediaButtons = false;
|
private boolean shouldIgnoreHardwareMediaButtons = false;
|
||||||
|
|
||||||
|
// used to check whether any notification action changed, before sending costly updates
|
||||||
|
private List<NotificationActionData> prevNotificationActions = List.of();
|
||||||
|
|
||||||
|
|
||||||
public MediaSessionPlayerUi(@NonNull final Player player) {
|
public MediaSessionPlayerUi(@NonNull final Player player) {
|
||||||
super(player);
|
super(player);
|
||||||
ignoreHardwareMediaButtonsKey =
|
ignoreHardwareMediaButtonsKey =
|
||||||
|
@ -63,6 +77,10 @@ public class MediaSessionPlayerUi extends PlayerUi
|
||||||
|
|
||||||
sessionConnector.setMetadataDeduplicationEnabled(true);
|
sessionConnector.setMetadataDeduplicationEnabled(true);
|
||||||
sessionConnector.setMediaMetadataProvider(exoPlayer -> buildMediaMetadata());
|
sessionConnector.setMediaMetadataProvider(exoPlayer -> buildMediaMetadata());
|
||||||
|
|
||||||
|
// force updating media session actions by resetting the previous ones
|
||||||
|
prevNotificationActions = List.of();
|
||||||
|
updateMediaSessionActions();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -80,6 +98,7 @@ public class MediaSessionPlayerUi extends PlayerUi
|
||||||
mediaSession.release();
|
mediaSession.release();
|
||||||
mediaSession = null;
|
mediaSession = null;
|
||||||
}
|
}
|
||||||
|
prevNotificationActions = List.of();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -163,4 +182,109 @@ public class MediaSessionPlayerUi extends PlayerUi
|
||||||
|
|
||||||
return builder.build();
|
return builder.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private void updateMediaSessionActions() {
|
||||||
|
// On Android 13+ (or Android T or API 33+) the actions in the player notification can't be
|
||||||
|
// controlled directly anymore, but are instead derived from custom media session actions.
|
||||||
|
// However the system allows customizing only two of these actions, since the other three
|
||||||
|
// are fixed to play-pause-buffering, previous, next.
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
|
||||||
|
// Although setting media session actions on older android versions doesn't seem to
|
||||||
|
// cause any trouble, it also doesn't seem to do anything, so we don't do anything to
|
||||||
|
// save battery. Check out NotificationUtil.updateActions() to see what happens on
|
||||||
|
// older android versions.
|
||||||
|
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)
|
||||||
|
.map(i -> player.getPrefs().getInt(
|
||||||
|
player.getContext().getString(NotificationConstants.SLOT_PREF_KEYS[i]),
|
||||||
|
NotificationConstants.SLOT_DEFAULTS[i]))
|
||||||
|
.mapToObj(action -> NotificationActionData
|
||||||
|
.fromNotificationActionEnum(player, action))
|
||||||
|
.filter(Objects::nonNull)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
// avoid costly notification actions update, if nothing changed from last time
|
||||||
|
if (!newNotificationActions.equals(prevNotificationActions)) {
|
||||||
|
prevNotificationActions = newNotificationActions;
|
||||||
|
sessionConnector.setCustomActionProviders(
|
||||||
|
newNotificationActions.stream()
|
||||||
|
.map(data -> new SessionConnectorActionProvider(data, context))
|
||||||
|
.toArray(SessionConnectorActionProvider[]::new));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onBlocked() {
|
||||||
|
super.onBlocked();
|
||||||
|
updateMediaSessionActions();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onPlaying() {
|
||||||
|
super.onPlaying();
|
||||||
|
updateMediaSessionActions();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onBuffering() {
|
||||||
|
super.onBuffering();
|
||||||
|
updateMediaSessionActions();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onPaused() {
|
||||||
|
super.onPaused();
|
||||||
|
updateMediaSessionActions();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onPausedSeek() {
|
||||||
|
super.onPausedSeek();
|
||||||
|
updateMediaSessionActions();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCompleted() {
|
||||||
|
super.onCompleted();
|
||||||
|
updateMediaSessionActions();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onRepeatModeChanged(@RepeatMode final int repeatMode) {
|
||||||
|
super.onRepeatModeChanged(repeatMode);
|
||||||
|
updateMediaSessionActions();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onShuffleModeEnabledChanged(final boolean shuffleModeEnabled) {
|
||||||
|
super.onShuffleModeEnabledChanged(shuffleModeEnabled);
|
||||||
|
updateMediaSessionActions();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onBroadcastReceived(final Intent intent) {
|
||||||
|
super.onBroadcastReceived(intent);
|
||||||
|
if (ACTION_RECREATE_NOTIFICATION.equals(intent.getAction())) {
|
||||||
|
// the notification actions changed
|
||||||
|
updateMediaSessionActions();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onMetadataChanged(@NonNull final StreamInfo info) {
|
||||||
|
super.onMetadataChanged(info);
|
||||||
|
updateMediaSessionActions();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onPlayQueueEdited() {
|
||||||
|
super.onPlayQueueEdited();
|
||||||
|
updateMediaSessionActions();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,47 @@
|
||||||
|
package org.schabi.newpipe.player.mediasession;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.support.v4.media.session.PlaybackStateCompat;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
|
import com.google.android.exoplayer2.Player;
|
||||||
|
import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector;
|
||||||
|
|
||||||
|
import org.schabi.newpipe.player.notification.NotificationActionData;
|
||||||
|
|
||||||
|
import java.lang.ref.WeakReference;
|
||||||
|
|
||||||
|
public class SessionConnectorActionProvider implements MediaSessionConnector.CustomActionProvider {
|
||||||
|
|
||||||
|
private final NotificationActionData data;
|
||||||
|
@NonNull
|
||||||
|
private final WeakReference<Context> context;
|
||||||
|
|
||||||
|
public SessionConnectorActionProvider(final NotificationActionData notificationActionData,
|
||||||
|
@NonNull final Context context) {
|
||||||
|
this.data = notificationActionData;
|
||||||
|
this.context = new WeakReference<>(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCustomAction(@NonNull final Player player,
|
||||||
|
@NonNull final String action,
|
||||||
|
@Nullable final Bundle extras) {
|
||||||
|
final Context actualContext = context.get();
|
||||||
|
if (actualContext != null) {
|
||||||
|
actualContext.sendBroadcast(new Intent(action));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
@Override
|
||||||
|
public PlaybackStateCompat.CustomAction getCustomAction(@NonNull final Player player) {
|
||||||
|
return new PlaybackStateCompat.CustomAction.Builder(
|
||||||
|
data.action(), data.name(), data.icon()
|
||||||
|
).build();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,187 @@
|
||||||
|
package org.schabi.newpipe.player.notification;
|
||||||
|
|
||||||
|
import static com.google.android.exoplayer2.Player.REPEAT_MODE_ALL;
|
||||||
|
import static com.google.android.exoplayer2.Player.REPEAT_MODE_ONE;
|
||||||
|
import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_CLOSE;
|
||||||
|
import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_FAST_FORWARD;
|
||||||
|
import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_FAST_REWIND;
|
||||||
|
import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_PLAY_NEXT;
|
||||||
|
import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_PLAY_PAUSE;
|
||||||
|
import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_PLAY_PREVIOUS;
|
||||||
|
import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_REPEAT;
|
||||||
|
import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_SHUFFLE;
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint;
|
||||||
|
import android.content.Context;
|
||||||
|
|
||||||
|
import androidx.annotation.DrawableRes;
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
|
import org.schabi.newpipe.R;
|
||||||
|
import org.schabi.newpipe.player.Player;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
public final class NotificationActionData {
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
private final String action;
|
||||||
|
@NonNull
|
||||||
|
private final String name;
|
||||||
|
@DrawableRes
|
||||||
|
private final int icon;
|
||||||
|
|
||||||
|
|
||||||
|
public NotificationActionData(@NonNull final String action, @NonNull final String name,
|
||||||
|
@DrawableRes final int icon) {
|
||||||
|
this.action = action;
|
||||||
|
this.name = name;
|
||||||
|
this.icon = icon;
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public String action() {
|
||||||
|
return action;
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public String name() {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
@DrawableRes
|
||||||
|
public int icon() {
|
||||||
|
return icon;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@SuppressLint("PrivateResource") // we currently use Exoplayer's internal strings and icons
|
||||||
|
@Nullable
|
||||||
|
public static NotificationActionData fromNotificationActionEnum(
|
||||||
|
@NonNull final Player player,
|
||||||
|
@NotificationConstants.Action final int selectedAction
|
||||||
|
) {
|
||||||
|
|
||||||
|
final int baseActionIcon = NotificationConstants.ACTION_ICONS[selectedAction];
|
||||||
|
final Context ctx = player.getContext();
|
||||||
|
|
||||||
|
switch (selectedAction) {
|
||||||
|
case NotificationConstants.PREVIOUS:
|
||||||
|
return new NotificationActionData(ACTION_PLAY_PREVIOUS,
|
||||||
|
ctx.getString(R.string.exo_controls_previous_description), baseActionIcon);
|
||||||
|
|
||||||
|
case NotificationConstants.NEXT:
|
||||||
|
return new NotificationActionData(ACTION_PLAY_NEXT,
|
||||||
|
ctx.getString(R.string.exo_controls_next_description), baseActionIcon);
|
||||||
|
|
||||||
|
case NotificationConstants.REWIND:
|
||||||
|
return new NotificationActionData(ACTION_FAST_REWIND,
|
||||||
|
ctx.getString(R.string.exo_controls_rewind_description), baseActionIcon);
|
||||||
|
|
||||||
|
case NotificationConstants.FORWARD:
|
||||||
|
return new NotificationActionData(ACTION_FAST_FORWARD,
|
||||||
|
ctx.getString(R.string.exo_controls_fastforward_description),
|
||||||
|
baseActionIcon);
|
||||||
|
|
||||||
|
case NotificationConstants.SMART_REWIND_PREVIOUS:
|
||||||
|
if (player.getPlayQueue() != null && player.getPlayQueue().size() > 1) {
|
||||||
|
return new NotificationActionData(ACTION_PLAY_PREVIOUS,
|
||||||
|
ctx.getString(R.string.exo_controls_previous_description),
|
||||||
|
R.drawable.exo_notification_previous);
|
||||||
|
} else {
|
||||||
|
return new NotificationActionData(ACTION_FAST_REWIND,
|
||||||
|
ctx.getString(R.string.exo_controls_rewind_description),
|
||||||
|
R.drawable.exo_controls_rewind);
|
||||||
|
}
|
||||||
|
|
||||||
|
case NotificationConstants.SMART_FORWARD_NEXT:
|
||||||
|
if (player.getPlayQueue() != null && player.getPlayQueue().size() > 1) {
|
||||||
|
return new NotificationActionData(ACTION_PLAY_NEXT,
|
||||||
|
ctx.getString(R.string.exo_controls_next_description),
|
||||||
|
R.drawable.exo_notification_next);
|
||||||
|
} else {
|
||||||
|
return new NotificationActionData(ACTION_FAST_FORWARD,
|
||||||
|
ctx.getString(R.string.exo_controls_fastforward_description),
|
||||||
|
R.drawable.exo_controls_fastforward);
|
||||||
|
}
|
||||||
|
|
||||||
|
case NotificationConstants.PLAY_PAUSE_BUFFERING:
|
||||||
|
if (player.getCurrentState() == Player.STATE_PREFLIGHT
|
||||||
|
|| player.getCurrentState() == Player.STATE_BLOCKED
|
||||||
|
|| player.getCurrentState() == Player.STATE_BUFFERING) {
|
||||||
|
return new NotificationActionData(ACTION_PLAY_PAUSE,
|
||||||
|
ctx.getString(R.string.notification_action_buffering),
|
||||||
|
R.drawable.ic_hourglass_top);
|
||||||
|
}
|
||||||
|
|
||||||
|
// fallthrough
|
||||||
|
case NotificationConstants.PLAY_PAUSE:
|
||||||
|
if (player.getCurrentState() == Player.STATE_COMPLETED) {
|
||||||
|
return new NotificationActionData(ACTION_PLAY_PAUSE,
|
||||||
|
ctx.getString(R.string.exo_controls_pause_description),
|
||||||
|
R.drawable.ic_replay);
|
||||||
|
} else if (player.isPlaying()
|
||||||
|
|| player.getCurrentState() == Player.STATE_PREFLIGHT
|
||||||
|
|| player.getCurrentState() == Player.STATE_BLOCKED
|
||||||
|
|| player.getCurrentState() == Player.STATE_BUFFERING) {
|
||||||
|
return new NotificationActionData(ACTION_PLAY_PAUSE,
|
||||||
|
ctx.getString(R.string.exo_controls_pause_description),
|
||||||
|
R.drawable.exo_notification_pause);
|
||||||
|
} else {
|
||||||
|
return new NotificationActionData(ACTION_PLAY_PAUSE,
|
||||||
|
ctx.getString(R.string.exo_controls_play_description),
|
||||||
|
R.drawable.exo_notification_play);
|
||||||
|
}
|
||||||
|
|
||||||
|
case NotificationConstants.REPEAT:
|
||||||
|
if (player.getRepeatMode() == REPEAT_MODE_ALL) {
|
||||||
|
return new NotificationActionData(ACTION_REPEAT,
|
||||||
|
ctx.getString(R.string.exo_controls_repeat_all_description),
|
||||||
|
R.drawable.exo_media_action_repeat_all);
|
||||||
|
} else if (player.getRepeatMode() == REPEAT_MODE_ONE) {
|
||||||
|
return new NotificationActionData(ACTION_REPEAT,
|
||||||
|
ctx.getString(R.string.exo_controls_repeat_one_description),
|
||||||
|
R.drawable.exo_media_action_repeat_one);
|
||||||
|
} else /* player.getRepeatMode() == REPEAT_MODE_OFF */ {
|
||||||
|
return new NotificationActionData(ACTION_REPEAT,
|
||||||
|
ctx.getString(R.string.exo_controls_repeat_off_description),
|
||||||
|
R.drawable.exo_media_action_repeat_off);
|
||||||
|
}
|
||||||
|
|
||||||
|
case NotificationConstants.SHUFFLE:
|
||||||
|
if (player.getPlayQueue() != null && player.getPlayQueue().isShuffled()) {
|
||||||
|
return new NotificationActionData(ACTION_SHUFFLE,
|
||||||
|
ctx.getString(R.string.exo_controls_shuffle_on_description),
|
||||||
|
R.drawable.exo_controls_shuffle_on);
|
||||||
|
} else {
|
||||||
|
return new NotificationActionData(ACTION_SHUFFLE,
|
||||||
|
ctx.getString(R.string.exo_controls_shuffle_off_description),
|
||||||
|
R.drawable.exo_controls_shuffle_off);
|
||||||
|
}
|
||||||
|
|
||||||
|
case NotificationConstants.CLOSE:
|
||||||
|
return new NotificationActionData(ACTION_CLOSE, ctx.getString(R.string.close),
|
||||||
|
R.drawable.ic_close);
|
||||||
|
|
||||||
|
case NotificationConstants.NOTHING:
|
||||||
|
default:
|
||||||
|
// do nothing
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(@Nullable final Object obj) {
|
||||||
|
return (obj instanceof NotificationActionData other)
|
||||||
|
&& this.action.equals(other.action)
|
||||||
|
&& this.name.equals(other.name)
|
||||||
|
&& this.icon == other.icon;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return Objects.hash(action, name, icon);
|
||||||
|
}
|
||||||
|
}
|
|
@ -13,7 +13,7 @@ import org.schabi.newpipe.util.Localization;
|
||||||
|
|
||||||
import java.lang.annotation.Retention;
|
import java.lang.annotation.Retention;
|
||||||
import java.lang.annotation.RetentionPolicy;
|
import java.lang.annotation.RetentionPolicy;
|
||||||
import java.util.ArrayList;
|
import java.util.Collection;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.SortedSet;
|
import java.util.SortedSet;
|
||||||
import java.util.TreeSet;
|
import java.util.TreeSet;
|
||||||
|
@ -65,10 +65,16 @@ public final class NotificationConstants {
|
||||||
public static final int CLOSE = 11;
|
public static final int CLOSE = 11;
|
||||||
|
|
||||||
@Retention(RetentionPolicy.SOURCE)
|
@Retention(RetentionPolicy.SOURCE)
|
||||||
@IntDef({NOTHING, PREVIOUS, NEXT, REWIND, FORWARD, SMART_REWIND_PREVIOUS, SMART_FORWARD_NEXT,
|
@IntDef({NOTHING, PREVIOUS, NEXT, REWIND, FORWARD,
|
||||||
PLAY_PAUSE, PLAY_PAUSE_BUFFERING, REPEAT, SHUFFLE, CLOSE})
|
SMART_REWIND_PREVIOUS, SMART_FORWARD_NEXT, PLAY_PAUSE, PLAY_PAUSE_BUFFERING, REPEAT,
|
||||||
|
SHUFFLE, CLOSE})
|
||||||
public @interface Action { }
|
public @interface Action { }
|
||||||
|
|
||||||
|
@Action
|
||||||
|
public static final int[] ALL_ACTIONS = {NOTHING, PREVIOUS, NEXT, REWIND, FORWARD,
|
||||||
|
SMART_REWIND_PREVIOUS, SMART_FORWARD_NEXT, PLAY_PAUSE, PLAY_PAUSE_BUFFERING, REPEAT,
|
||||||
|
SHUFFLE, CLOSE};
|
||||||
|
|
||||||
@DrawableRes
|
@DrawableRes
|
||||||
public static final int[] ACTION_ICONS = {
|
public static final int[] ACTION_ICONS = {
|
||||||
0,
|
0,
|
||||||
|
@ -95,16 +101,6 @@ public final class NotificationConstants {
|
||||||
CLOSE,
|
CLOSE,
|
||||||
};
|
};
|
||||||
|
|
||||||
@Action
|
|
||||||
public static final int[][] SLOT_ALLOWED_ACTIONS = {
|
|
||||||
new int[] {PREVIOUS, REWIND, SMART_REWIND_PREVIOUS},
|
|
||||||
new int[] {REWIND, PLAY_PAUSE, PLAY_PAUSE_BUFFERING},
|
|
||||||
new int[] {NEXT, FORWARD, SMART_FORWARD_NEXT, PLAY_PAUSE, PLAY_PAUSE_BUFFERING},
|
|
||||||
new int[] {NOTHING, PREVIOUS, NEXT, REWIND, FORWARD, SMART_REWIND_PREVIOUS,
|
|
||||||
SMART_FORWARD_NEXT, REPEAT, SHUFFLE, CLOSE},
|
|
||||||
new int[] {NOTHING, NEXT, FORWARD, SMART_FORWARD_NEXT, REPEAT, SHUFFLE, CLOSE},
|
|
||||||
};
|
|
||||||
|
|
||||||
public static final int[] SLOT_PREF_KEYS = {
|
public static final int[] SLOT_PREF_KEYS = {
|
||||||
R.string.notification_slot_0_key,
|
R.string.notification_slot_0_key,
|
||||||
R.string.notification_slot_1_key,
|
R.string.notification_slot_1_key,
|
||||||
|
@ -165,14 +161,11 @@ public final class NotificationConstants {
|
||||||
/**
|
/**
|
||||||
* @param context the context to use
|
* @param context the context to use
|
||||||
* @param sharedPreferences the shared preferences to query values from
|
* @param sharedPreferences the shared preferences to query values from
|
||||||
* @param slotCount remove indices >= than this value (set to {@code 5} to do nothing, or make
|
|
||||||
* it lower if there are slots with empty actions)
|
|
||||||
* @return a sorted list of the indices of the slots to use as compact slots
|
* @return a sorted list of the indices of the slots to use as compact slots
|
||||||
*/
|
*/
|
||||||
public static List<Integer> getCompactSlotsFromPreferences(
|
public static Collection<Integer> getCompactSlotsFromPreferences(
|
||||||
@NonNull final Context context,
|
@NonNull final Context context,
|
||||||
final SharedPreferences sharedPreferences,
|
final SharedPreferences sharedPreferences) {
|
||||||
final int slotCount) {
|
|
||||||
final SortedSet<Integer> compactSlots = new TreeSet<>();
|
final SortedSet<Integer> compactSlots = new TreeSet<>();
|
||||||
for (int i = 0; i < 3; i++) {
|
for (int i = 0; i < 3; i++) {
|
||||||
final int compactSlot = sharedPreferences.getInt(
|
final int compactSlot = sharedPreferences.getInt(
|
||||||
|
@ -180,14 +173,14 @@ public final class NotificationConstants {
|
||||||
|
|
||||||
if (compactSlot == Integer.MAX_VALUE) {
|
if (compactSlot == Integer.MAX_VALUE) {
|
||||||
// settings not yet populated, return default values
|
// settings not yet populated, return default values
|
||||||
return new ArrayList<>(SLOT_COMPACT_DEFAULTS);
|
return SLOT_COMPACT_DEFAULTS;
|
||||||
}
|
}
|
||||||
|
|
||||||
// a negative value (-1) is set when the user does not want a particular compact slot
|
if (compactSlot >= 0) {
|
||||||
if (compactSlot >= 0 && compactSlot < slotCount) {
|
// compact slot is < 0 if there are less than 3 checked checkboxes
|
||||||
compactSlots.add(compactSlot);
|
compactSlots.add(compactSlot);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return new ArrayList<>(compactSlots);
|
return compactSlots;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,16 +1,19 @@
|
||||||
package org.schabi.newpipe.player.notification;
|
package org.schabi.newpipe.player.notification;
|
||||||
|
|
||||||
|
import static android.app.PendingIntent.FLAG_UPDATE_CURRENT;
|
||||||
|
import static androidx.media.app.NotificationCompat.MediaStyle;
|
||||||
|
import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_CLOSE;
|
||||||
|
|
||||||
import android.annotation.SuppressLint;
|
import android.annotation.SuppressLint;
|
||||||
|
import android.app.PendingIntent;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.pm.ServiceInfo;
|
import android.content.pm.ServiceInfo;
|
||||||
import android.graphics.Bitmap;
|
import android.graphics.Bitmap;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
import androidx.annotation.DrawableRes;
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.annotation.StringRes;
|
|
||||||
import androidx.core.app.NotificationCompat;
|
import androidx.core.app.NotificationCompat;
|
||||||
import androidx.core.app.NotificationManagerCompat;
|
import androidx.core.app.NotificationManagerCompat;
|
||||||
import androidx.core.app.PendingIntentCompat;
|
import androidx.core.app.PendingIntentCompat;
|
||||||
|
@ -23,23 +26,12 @@ import org.schabi.newpipe.player.Player;
|
||||||
import org.schabi.newpipe.player.mediasession.MediaSessionPlayerUi;
|
import org.schabi.newpipe.player.mediasession.MediaSessionPlayerUi;
|
||||||
import org.schabi.newpipe.util.NavigationHelper;
|
import org.schabi.newpipe.util.NavigationHelper;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collection;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
import static android.app.PendingIntent.FLAG_UPDATE_CURRENT;
|
|
||||||
import static androidx.media.app.NotificationCompat.MediaStyle;
|
|
||||||
import static com.google.android.exoplayer2.Player.REPEAT_MODE_ALL;
|
|
||||||
import static com.google.android.exoplayer2.Player.REPEAT_MODE_ONE;
|
|
||||||
import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_CLOSE;
|
|
||||||
import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_FAST_FORWARD;
|
|
||||||
import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_FAST_REWIND;
|
|
||||||
import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_PLAY_NEXT;
|
|
||||||
import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_PLAY_PAUSE;
|
|
||||||
import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_PLAY_PREVIOUS;
|
|
||||||
import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_REPEAT;
|
|
||||||
import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_SHUFFLE;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This is a utility class for player notifications.
|
* This is a utility class for player notifications.
|
||||||
*/
|
*/
|
||||||
|
@ -100,29 +92,21 @@ public final class NotificationUtil {
|
||||||
final NotificationCompat.Builder builder =
|
final NotificationCompat.Builder builder =
|
||||||
new NotificationCompat.Builder(player.getContext(),
|
new NotificationCompat.Builder(player.getContext(),
|
||||||
player.getContext().getString(R.string.notification_channel_id));
|
player.getContext().getString(R.string.notification_channel_id));
|
||||||
|
final MediaStyle mediaStyle = new MediaStyle();
|
||||||
|
|
||||||
initializeNotificationSlots();
|
// setup media style (compact notification slots and media session)
|
||||||
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
|
||||||
// count the number of real slots, to make sure compact slots indices are not out of bound
|
// notification actions are ignored on Android 13+, and are replaced by code in
|
||||||
int nonNothingSlotCount = 5;
|
// MediaSessionPlayerUi
|
||||||
if (notificationSlots[3] == NotificationConstants.NOTHING) {
|
final int[] compactSlots = initializeNotificationSlots();
|
||||||
--nonNothingSlotCount;
|
mediaStyle.setShowActionsInCompactView(compactSlots);
|
||||||
}
|
}
|
||||||
if (notificationSlots[4] == NotificationConstants.NOTHING) {
|
|
||||||
--nonNothingSlotCount;
|
|
||||||
}
|
|
||||||
|
|
||||||
// build the compact slot indices array (need code to convert from Integer... because Java)
|
|
||||||
final List<Integer> compactSlotList = NotificationConstants.getCompactSlotsFromPreferences(
|
|
||||||
player.getContext(), player.getPrefs(), nonNothingSlotCount);
|
|
||||||
final int[] compactSlots = compactSlotList.stream().mapToInt(Integer::intValue).toArray();
|
|
||||||
|
|
||||||
final MediaStyle mediaStyle = new MediaStyle().setShowActionsInCompactView(compactSlots);
|
|
||||||
player.UIs()
|
player.UIs()
|
||||||
.get(MediaSessionPlayerUi.class)
|
.get(MediaSessionPlayerUi.class)
|
||||||
.flatMap(MediaSessionPlayerUi::getSessionToken)
|
.flatMap(MediaSessionPlayerUi::getSessionToken)
|
||||||
.ifPresent(mediaStyle::setMediaSession);
|
.ifPresent(mediaStyle::setMediaSession);
|
||||||
|
|
||||||
|
// setup notification builder
|
||||||
builder.setStyle(mediaStyle)
|
builder.setStyle(mediaStyle)
|
||||||
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
||||||
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
|
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
|
||||||
|
@ -157,7 +141,11 @@ public final class NotificationUtil {
|
||||||
notificationBuilder.setContentText(player.getUploaderName());
|
notificationBuilder.setContentText(player.getUploaderName());
|
||||||
notificationBuilder.setTicker(player.getVideoTitle());
|
notificationBuilder.setTicker(player.getVideoTitle());
|
||||||
|
|
||||||
updateActions(notificationBuilder);
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
|
||||||
|
// notification actions are ignored on Android 13+, and are replaced by code in
|
||||||
|
// MediaSessionPlayerUi
|
||||||
|
updateActions(notificationBuilder);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -209,12 +197,35 @@ public final class NotificationUtil {
|
||||||
// ACTIONS
|
// ACTIONS
|
||||||
/////////////////////////////////////////////////////
|
/////////////////////////////////////////////////////
|
||||||
|
|
||||||
private void initializeNotificationSlots() {
|
/**
|
||||||
|
* The compact slots array from settings contains indices from 0 to 4, each referring to one of
|
||||||
|
* the five actions configurable by the user. However, if the user sets an action to "Nothing",
|
||||||
|
* then all of the actions coming after will have a "settings index" different than the index
|
||||||
|
* of the corresponding action when sent to the system.
|
||||||
|
*
|
||||||
|
* @return the indices of compact slots referred to the list of non-nothing actions that will be
|
||||||
|
* sent to the system
|
||||||
|
*/
|
||||||
|
private int[] initializeNotificationSlots() {
|
||||||
|
final Collection<Integer> settingsCompactSlots = NotificationConstants
|
||||||
|
.getCompactSlotsFromPreferences(player.getContext(), player.getPrefs());
|
||||||
|
final List<Integer> adjustedCompactSlots = new ArrayList<>();
|
||||||
|
|
||||||
|
int nonNothingIndex = 0;
|
||||||
for (int i = 0; i < 5; ++i) {
|
for (int i = 0; i < 5; ++i) {
|
||||||
notificationSlots[i] = player.getPrefs().getInt(
|
notificationSlots[i] = player.getPrefs().getInt(
|
||||||
player.getContext().getString(NotificationConstants.SLOT_PREF_KEYS[i]),
|
player.getContext().getString(NotificationConstants.SLOT_PREF_KEYS[i]),
|
||||||
NotificationConstants.SLOT_DEFAULTS[i]);
|
NotificationConstants.SLOT_DEFAULTS[i]);
|
||||||
|
|
||||||
|
if (notificationSlots[i] != NotificationConstants.NOTHING) {
|
||||||
|
if (settingsCompactSlots.contains(i)) {
|
||||||
|
adjustedCompactSlots.add(nonNothingIndex);
|
||||||
|
}
|
||||||
|
nonNothingIndex += 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return adjustedCompactSlots.stream().mapToInt(Integer::intValue).toArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("RestrictedApi")
|
@SuppressLint("RestrictedApi")
|
||||||
|
@ -227,115 +238,15 @@ public final class NotificationUtil {
|
||||||
|
|
||||||
private void addAction(final NotificationCompat.Builder builder,
|
private void addAction(final NotificationCompat.Builder builder,
|
||||||
@NotificationConstants.Action final int slot) {
|
@NotificationConstants.Action final int slot) {
|
||||||
final NotificationCompat.Action action = getAction(slot);
|
@Nullable final NotificationActionData data =
|
||||||
if (action != null) {
|
NotificationActionData.fromNotificationActionEnum(player, slot);
|
||||||
builder.addAction(action);
|
if (data == null) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
@Nullable
|
final PendingIntent intent = PendingIntentCompat.getBroadcast(player.getContext(),
|
||||||
private NotificationCompat.Action getAction(
|
NOTIFICATION_ID, new Intent(data.action()), FLAG_UPDATE_CURRENT, false);
|
||||||
@NotificationConstants.Action final int selectedAction) {
|
builder.addAction(new NotificationCompat.Action(data.icon(), data.name(), intent));
|
||||||
final int baseActionIcon = NotificationConstants.ACTION_ICONS[selectedAction];
|
|
||||||
switch (selectedAction) {
|
|
||||||
case NotificationConstants.PREVIOUS:
|
|
||||||
return getAction(baseActionIcon,
|
|
||||||
R.string.exo_controls_previous_description, ACTION_PLAY_PREVIOUS);
|
|
||||||
|
|
||||||
case NotificationConstants.NEXT:
|
|
||||||
return getAction(baseActionIcon,
|
|
||||||
R.string.exo_controls_next_description, ACTION_PLAY_NEXT);
|
|
||||||
|
|
||||||
case NotificationConstants.REWIND:
|
|
||||||
return getAction(baseActionIcon,
|
|
||||||
R.string.exo_controls_rewind_description, ACTION_FAST_REWIND);
|
|
||||||
|
|
||||||
case NotificationConstants.FORWARD:
|
|
||||||
return getAction(baseActionIcon,
|
|
||||||
R.string.exo_controls_fastforward_description, ACTION_FAST_FORWARD);
|
|
||||||
|
|
||||||
case NotificationConstants.SMART_REWIND_PREVIOUS:
|
|
||||||
if (player.getPlayQueue() != null && player.getPlayQueue().size() > 1) {
|
|
||||||
return getAction(R.drawable.exo_notification_previous,
|
|
||||||
R.string.exo_controls_previous_description, ACTION_PLAY_PREVIOUS);
|
|
||||||
} else {
|
|
||||||
return getAction(R.drawable.exo_controls_rewind,
|
|
||||||
R.string.exo_controls_rewind_description, ACTION_FAST_REWIND);
|
|
||||||
}
|
|
||||||
|
|
||||||
case NotificationConstants.SMART_FORWARD_NEXT:
|
|
||||||
if (player.getPlayQueue() != null && player.getPlayQueue().size() > 1) {
|
|
||||||
return getAction(R.drawable.exo_notification_next,
|
|
||||||
R.string.exo_controls_next_description, ACTION_PLAY_NEXT);
|
|
||||||
} else {
|
|
||||||
return getAction(R.drawable.exo_controls_fastforward,
|
|
||||||
R.string.exo_controls_fastforward_description, ACTION_FAST_FORWARD);
|
|
||||||
}
|
|
||||||
|
|
||||||
case NotificationConstants.PLAY_PAUSE_BUFFERING:
|
|
||||||
if (player.getCurrentState() == Player.STATE_PREFLIGHT
|
|
||||||
|| player.getCurrentState() == Player.STATE_BLOCKED
|
|
||||||
|| player.getCurrentState() == Player.STATE_BUFFERING) {
|
|
||||||
// null intent -> show hourglass icon that does nothing when clicked
|
|
||||||
return new NotificationCompat.Action(R.drawable.ic_hourglass_top,
|
|
||||||
player.getContext().getString(R.string.notification_action_buffering),
|
|
||||||
null);
|
|
||||||
}
|
|
||||||
|
|
||||||
// fallthrough
|
|
||||||
case NotificationConstants.PLAY_PAUSE:
|
|
||||||
if (player.getCurrentState() == Player.STATE_COMPLETED) {
|
|
||||||
return getAction(R.drawable.ic_replay,
|
|
||||||
R.string.exo_controls_pause_description, ACTION_PLAY_PAUSE);
|
|
||||||
} else if (player.isPlaying()
|
|
||||||
|| player.getCurrentState() == Player.STATE_PREFLIGHT
|
|
||||||
|| player.getCurrentState() == Player.STATE_BLOCKED
|
|
||||||
|| player.getCurrentState() == Player.STATE_BUFFERING) {
|
|
||||||
return getAction(R.drawable.exo_notification_pause,
|
|
||||||
R.string.exo_controls_pause_description, ACTION_PLAY_PAUSE);
|
|
||||||
} else {
|
|
||||||
return getAction(R.drawable.exo_notification_play,
|
|
||||||
R.string.exo_controls_play_description, ACTION_PLAY_PAUSE);
|
|
||||||
}
|
|
||||||
|
|
||||||
case NotificationConstants.REPEAT:
|
|
||||||
if (player.getRepeatMode() == REPEAT_MODE_ALL) {
|
|
||||||
return getAction(R.drawable.exo_media_action_repeat_all,
|
|
||||||
R.string.exo_controls_repeat_all_description, ACTION_REPEAT);
|
|
||||||
} else if (player.getRepeatMode() == REPEAT_MODE_ONE) {
|
|
||||||
return getAction(R.drawable.exo_media_action_repeat_one,
|
|
||||||
R.string.exo_controls_repeat_one_description, ACTION_REPEAT);
|
|
||||||
} else /* player.getRepeatMode() == REPEAT_MODE_OFF */ {
|
|
||||||
return getAction(R.drawable.exo_media_action_repeat_off,
|
|
||||||
R.string.exo_controls_repeat_off_description, ACTION_REPEAT);
|
|
||||||
}
|
|
||||||
|
|
||||||
case NotificationConstants.SHUFFLE:
|
|
||||||
if (player.getPlayQueue() != null && player.getPlayQueue().isShuffled()) {
|
|
||||||
return getAction(R.drawable.exo_controls_shuffle_on,
|
|
||||||
R.string.exo_controls_shuffle_on_description, ACTION_SHUFFLE);
|
|
||||||
} else {
|
|
||||||
return getAction(R.drawable.exo_controls_shuffle_off,
|
|
||||||
R.string.exo_controls_shuffle_off_description, ACTION_SHUFFLE);
|
|
||||||
}
|
|
||||||
|
|
||||||
case NotificationConstants.CLOSE:
|
|
||||||
return getAction(R.drawable.ic_close,
|
|
||||||
R.string.close, ACTION_CLOSE);
|
|
||||||
|
|
||||||
case NotificationConstants.NOTHING:
|
|
||||||
default:
|
|
||||||
// do nothing
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private NotificationCompat.Action getAction(@DrawableRes final int drawable,
|
|
||||||
@StringRes final int title,
|
|
||||||
final String intentAction) {
|
|
||||||
return new NotificationCompat.Action(drawable, player.getContext().getString(title),
|
|
||||||
PendingIntentCompat.getBroadcast(player.getContext(), NOTIFICATION_ID,
|
|
||||||
new Intent(intentAction), FLAG_UPDATE_CURRENT, false));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private Intent getIntentForNotification() {
|
private Intent getIntentForNotification() {
|
||||||
|
@ -364,7 +275,7 @@ public final class NotificationUtil {
|
||||||
final Bitmap thumbnail = player.getThumbnail();
|
final Bitmap thumbnail = player.getThumbnail();
|
||||||
if (thumbnail == null || !showThumbnail) {
|
if (thumbnail == null || !showThumbnail) {
|
||||||
// since the builder is reused, make sure the thumbnail is unset if there is not one
|
// since the builder is reused, make sure the thumbnail is unset if there is not one
|
||||||
builder.setLargeIcon(null);
|
builder.setLargeIcon((Bitmap) null);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,271 @@
|
||||||
|
package org.schabi.newpipe.settings;
|
||||||
|
|
||||||
|
import static org.schabi.newpipe.extractor.utils.Utils.isBlank;
|
||||||
|
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
|
import android.app.AlertDialog;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.content.SharedPreferences;
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.widget.Toast;
|
||||||
|
|
||||||
|
import androidx.activity.result.ActivityResult;
|
||||||
|
import androidx.activity.result.ActivityResultLauncher;
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts;
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.core.content.ContextCompat;
|
||||||
|
import androidx.preference.Preference;
|
||||||
|
import androidx.preference.PreferenceManager;
|
||||||
|
|
||||||
|
import org.schabi.newpipe.NewPipeDatabase;
|
||||||
|
import org.schabi.newpipe.R;
|
||||||
|
import org.schabi.newpipe.error.ErrorUtil;
|
||||||
|
import org.schabi.newpipe.streams.io.NoFileManagerSafeGuard;
|
||||||
|
import org.schabi.newpipe.streams.io.StoredFileHelper;
|
||||||
|
import org.schabi.newpipe.util.NavigationHelper;
|
||||||
|
import org.schabi.newpipe.util.ZipHelper;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.text.SimpleDateFormat;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
public class BackupRestoreSettingsFragment extends BasePreferenceFragment {
|
||||||
|
|
||||||
|
private static final String ZIP_MIME_TYPE = "application/zip";
|
||||||
|
|
||||||
|
private final SimpleDateFormat exportDateFormat =
|
||||||
|
new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US);
|
||||||
|
private ContentSettingsManager manager;
|
||||||
|
private String importExportDataPathKey;
|
||||||
|
private final ActivityResultLauncher<Intent> requestImportPathLauncher =
|
||||||
|
registerForActivityResult(new ActivityResultContracts.StartActivityForResult(),
|
||||||
|
this::requestImportPathResult);
|
||||||
|
private final ActivityResultLauncher<Intent> requestExportPathLauncher =
|
||||||
|
registerForActivityResult(new ActivityResultContracts.StartActivityForResult(),
|
||||||
|
this::requestExportPathResult);
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreatePreferences(@Nullable final Bundle savedInstanceState,
|
||||||
|
@Nullable final String rootKey) {
|
||||||
|
final File homeDir = ContextCompat.getDataDir(requireContext());
|
||||||
|
Objects.requireNonNull(homeDir);
|
||||||
|
manager = new ContentSettingsManager(new NewPipeFileLocator(homeDir));
|
||||||
|
manager.deleteSettingsFile();
|
||||||
|
|
||||||
|
importExportDataPathKey = getString(R.string.import_export_data_path);
|
||||||
|
|
||||||
|
|
||||||
|
addPreferencesFromResourceRegistry();
|
||||||
|
|
||||||
|
final Preference importDataPreference = requirePreference(R.string.import_data);
|
||||||
|
importDataPreference.setOnPreferenceClickListener((Preference p) -> {
|
||||||
|
NoFileManagerSafeGuard.launchSafe(
|
||||||
|
requestImportPathLauncher,
|
||||||
|
StoredFileHelper.getPicker(requireContext(),
|
||||||
|
ZIP_MIME_TYPE, getImportExportDataUri()),
|
||||||
|
TAG,
|
||||||
|
getContext()
|
||||||
|
);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
final Preference exportDataPreference = requirePreference(R.string.export_data);
|
||||||
|
exportDataPreference.setOnPreferenceClickListener((final Preference p) -> {
|
||||||
|
NoFileManagerSafeGuard.launchSafe(
|
||||||
|
requestExportPathLauncher,
|
||||||
|
StoredFileHelper.getNewPicker(requireContext(),
|
||||||
|
"NewPipeData-" + exportDateFormat.format(new Date()) + ".zip",
|
||||||
|
ZIP_MIME_TYPE, getImportExportDataUri()),
|
||||||
|
TAG,
|
||||||
|
getContext()
|
||||||
|
);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
final Preference resetSettings = findPreference(getString(R.string.reset_settings));
|
||||||
|
// Resets all settings by deleting shared preference and restarting the app
|
||||||
|
// A dialogue will pop up to confirm if user intends to reset all settings
|
||||||
|
assert resetSettings != null;
|
||||||
|
resetSettings.setOnPreferenceClickListener(preference -> {
|
||||||
|
// Show Alert Dialogue
|
||||||
|
final AlertDialog.Builder builder = new AlertDialog.Builder(getContext());
|
||||||
|
builder.setMessage(R.string.reset_all_settings);
|
||||||
|
builder.setCancelable(true);
|
||||||
|
builder.setPositiveButton(R.string.ok, (dialogInterface, i) -> {
|
||||||
|
// Deletes all shared preferences xml files.
|
||||||
|
final SharedPreferences sharedPreferences =
|
||||||
|
PreferenceManager.getDefaultSharedPreferences(requireContext());
|
||||||
|
sharedPreferences.edit().clear().apply();
|
||||||
|
// Restarts the app
|
||||||
|
if (getActivity() == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
NavigationHelper.restartApp(getActivity());
|
||||||
|
});
|
||||||
|
builder.setNegativeButton(R.string.cancel, (dialogInterface, i) -> {
|
||||||
|
});
|
||||||
|
final AlertDialog alertDialog = builder.create();
|
||||||
|
alertDialog.show();
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void requestExportPathResult(final ActivityResult result) {
|
||||||
|
assureCorrectAppLanguage(requireContext());
|
||||||
|
if (result.getResultCode() == Activity.RESULT_OK && result.getData() != null) {
|
||||||
|
// will be saved only on success
|
||||||
|
final Uri lastExportDataUri = result.getData().getData();
|
||||||
|
|
||||||
|
final StoredFileHelper file = new StoredFileHelper(
|
||||||
|
requireContext(), result.getData().getData(), ZIP_MIME_TYPE);
|
||||||
|
|
||||||
|
exportDatabase(file, lastExportDataUri);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void requestImportPathResult(final ActivityResult result) {
|
||||||
|
assureCorrectAppLanguage(requireContext());
|
||||||
|
if (result.getResultCode() == Activity.RESULT_OK && result.getData() != null) {
|
||||||
|
// will be saved only on success
|
||||||
|
final Uri lastImportDataUri = result.getData().getData();
|
||||||
|
|
||||||
|
final StoredFileHelper file = new StoredFileHelper(
|
||||||
|
requireContext(), result.getData().getData(), ZIP_MIME_TYPE);
|
||||||
|
|
||||||
|
new androidx.appcompat.app.AlertDialog.Builder(requireActivity())
|
||||||
|
.setMessage(R.string.override_current_data)
|
||||||
|
.setPositiveButton(R.string.ok, (d, id) ->
|
||||||
|
importDatabase(file, lastImportDataUri))
|
||||||
|
.setNegativeButton(R.string.cancel, (d, id) ->
|
||||||
|
d.cancel())
|
||||||
|
.show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void exportDatabase(final StoredFileHelper file, final Uri exportDataUri) {
|
||||||
|
try {
|
||||||
|
//checkpoint before export
|
||||||
|
NewPipeDatabase.checkpoint();
|
||||||
|
|
||||||
|
final SharedPreferences preferences = PreferenceManager
|
||||||
|
.getDefaultSharedPreferences(requireContext());
|
||||||
|
manager.exportDatabase(preferences, file);
|
||||||
|
|
||||||
|
saveLastImportExportDataUri(exportDataUri); // save export path only on success
|
||||||
|
Toast.makeText(requireContext(), R.string.export_complete_toast, Toast.LENGTH_SHORT)
|
||||||
|
.show();
|
||||||
|
} catch (final Exception e) {
|
||||||
|
ErrorUtil.showUiErrorSnackbar(this, "Exporting database", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void importDatabase(final StoredFileHelper file, final Uri importDataUri) {
|
||||||
|
// check if file is supported
|
||||||
|
if (!ZipHelper.isValidZipFile(file)) {
|
||||||
|
Toast.makeText(requireContext(), R.string.no_valid_zip_file, Toast.LENGTH_SHORT)
|
||||||
|
.show();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!manager.ensureDbDirectoryExists()) {
|
||||||
|
throw new IOException("Could not create databases dir");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!manager.extractDb(file)) {
|
||||||
|
Toast.makeText(requireContext(), R.string.could_not_import_all_files,
|
||||||
|
Toast.LENGTH_LONG)
|
||||||
|
.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
// if settings file exist, ask if it should be imported.
|
||||||
|
if (manager.extractSettings(file)) {
|
||||||
|
new androidx.appcompat.app.AlertDialog.Builder(requireContext())
|
||||||
|
.setTitle(R.string.import_settings)
|
||||||
|
.setNegativeButton(R.string.cancel, (dialog, which) -> {
|
||||||
|
dialog.dismiss();
|
||||||
|
finishImport(importDataUri);
|
||||||
|
})
|
||||||
|
.setPositiveButton(R.string.ok, (dialog, which) -> {
|
||||||
|
dialog.dismiss();
|
||||||
|
final Context context = requireContext();
|
||||||
|
final SharedPreferences prefs = PreferenceManager
|
||||||
|
.getDefaultSharedPreferences(context);
|
||||||
|
manager.loadSharedPreferences(prefs);
|
||||||
|
cleanImport(context, prefs);
|
||||||
|
finishImport(importDataUri);
|
||||||
|
})
|
||||||
|
.show();
|
||||||
|
} else {
|
||||||
|
finishImport(importDataUri);
|
||||||
|
}
|
||||||
|
} catch (final Exception e) {
|
||||||
|
ErrorUtil.showUiErrorSnackbar(this, "Importing database", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove settings that are not supposed to be imported on different devices
|
||||||
|
* and reset them to default values.
|
||||||
|
* @param context the context used for the import
|
||||||
|
* @param prefs the preferences used while running the import
|
||||||
|
*/
|
||||||
|
private void cleanImport(@NonNull final Context context,
|
||||||
|
@NonNull final SharedPreferences prefs) {
|
||||||
|
// Check if media tunnelling needs to be disabled automatically,
|
||||||
|
// if it was disabled automatically in the imported preferences.
|
||||||
|
final String tunnelingKey = context.getString(R.string.disable_media_tunneling_key);
|
||||||
|
final String automaticTunnelingKey =
|
||||||
|
context.getString(R.string.disabled_media_tunneling_automatically_key);
|
||||||
|
// R.string.disable_media_tunneling_key should always be true
|
||||||
|
// if R.string.disabled_media_tunneling_automatically_key equals 1,
|
||||||
|
// but we double check here just to be sure and to avoid regressions
|
||||||
|
// caused by possible later modification of the media tunneling functionality.
|
||||||
|
// R.string.disabled_media_tunneling_automatically_key == 0:
|
||||||
|
// automatic value overridden by user in settings
|
||||||
|
// R.string.disabled_media_tunneling_automatically_key == -1: not set
|
||||||
|
final boolean wasMediaTunnelingDisabledAutomatically =
|
||||||
|
prefs.getInt(automaticTunnelingKey, -1) == 1
|
||||||
|
&& prefs.getBoolean(tunnelingKey, false);
|
||||||
|
if (wasMediaTunnelingDisabledAutomatically) {
|
||||||
|
prefs.edit()
|
||||||
|
.putInt(automaticTunnelingKey, -1)
|
||||||
|
.putBoolean(tunnelingKey, false)
|
||||||
|
.apply();
|
||||||
|
NewPipeSettings.setMediaTunneling(context);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save import path and restart system.
|
||||||
|
*
|
||||||
|
* @param importDataUri The import path to save
|
||||||
|
*/
|
||||||
|
private void finishImport(final Uri importDataUri) {
|
||||||
|
// save import path only on success
|
||||||
|
saveLastImportExportDataUri(importDataUri);
|
||||||
|
// restart app to properly load db
|
||||||
|
NavigationHelper.restartApp(requireActivity());
|
||||||
|
}
|
||||||
|
|
||||||
|
private Uri getImportExportDataUri() {
|
||||||
|
final String path = defaultPreferences.getString(importExportDataPathKey, null);
|
||||||
|
return isBlank(path) ? null : Uri.parse(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void saveLastImportExportDataUri(final Uri importExportDataUri) {
|
||||||
|
final SharedPreferences.Editor editor = defaultPreferences.edit()
|
||||||
|
.putString(importExportDataPathKey, importExportDataUri.toString());
|
||||||
|
editor.apply();
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,106 +1,36 @@
|
||||||
package org.schabi.newpipe.settings;
|
package org.schabi.newpipe.settings;
|
||||||
|
|
||||||
import static org.schabi.newpipe.extractor.utils.Utils.isBlank;
|
|
||||||
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
|
|
||||||
|
|
||||||
import android.app.Activity;
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
|
||||||
import android.content.SharedPreferences;
|
|
||||||
import android.net.Uri;
|
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
|
|
||||||
import androidx.activity.result.ActivityResult;
|
|
||||||
import androidx.activity.result.ActivityResultLauncher;
|
|
||||||
import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult;
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.appcompat.app.AlertDialog;
|
|
||||||
import androidx.core.content.ContextCompat;
|
|
||||||
import androidx.preference.Preference;
|
import androidx.preference.Preference;
|
||||||
import androidx.preference.PreferenceManager;
|
|
||||||
|
|
||||||
import org.schabi.newpipe.DownloaderImpl;
|
import org.schabi.newpipe.DownloaderImpl;
|
||||||
import org.schabi.newpipe.NewPipeDatabase;
|
|
||||||
import org.schabi.newpipe.R;
|
import org.schabi.newpipe.R;
|
||||||
import org.schabi.newpipe.error.ErrorUtil;
|
|
||||||
import org.schabi.newpipe.extractor.NewPipe;
|
import org.schabi.newpipe.extractor.NewPipe;
|
||||||
import org.schabi.newpipe.extractor.localization.ContentCountry;
|
import org.schabi.newpipe.extractor.localization.ContentCountry;
|
||||||
import org.schabi.newpipe.extractor.localization.Localization;
|
import org.schabi.newpipe.extractor.localization.Localization;
|
||||||
import org.schabi.newpipe.streams.io.NoFileManagerSafeGuard;
|
|
||||||
import org.schabi.newpipe.streams.io.StoredFileHelper;
|
|
||||||
import org.schabi.newpipe.util.NavigationHelper;
|
|
||||||
import org.schabi.newpipe.util.image.ImageStrategy;
|
import org.schabi.newpipe.util.image.ImageStrategy;
|
||||||
import org.schabi.newpipe.util.image.PicassoHelper;
|
import org.schabi.newpipe.util.image.PicassoHelper;
|
||||||
import org.schabi.newpipe.util.ZipHelper;
|
|
||||||
import org.schabi.newpipe.util.image.PreferredImageQuality;
|
import org.schabi.newpipe.util.image.PreferredImageQuality;
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.text.SimpleDateFormat;
|
|
||||||
import java.util.Date;
|
|
||||||
import java.util.Locale;
|
|
||||||
import java.util.Objects;
|
|
||||||
|
|
||||||
public class ContentSettingsFragment extends BasePreferenceFragment {
|
public class ContentSettingsFragment extends BasePreferenceFragment {
|
||||||
private static final String ZIP_MIME_TYPE = "application/zip";
|
|
||||||
|
|
||||||
private final SimpleDateFormat exportDateFormat =
|
|
||||||
new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US);
|
|
||||||
|
|
||||||
private ContentSettingsManager manager;
|
|
||||||
|
|
||||||
private String importExportDataPathKey;
|
|
||||||
private String youtubeRestrictedModeEnabledKey;
|
private String youtubeRestrictedModeEnabledKey;
|
||||||
|
|
||||||
private Localization initialSelectedLocalization;
|
private Localization initialSelectedLocalization;
|
||||||
private ContentCountry initialSelectedContentCountry;
|
private ContentCountry initialSelectedContentCountry;
|
||||||
private String initialLanguage;
|
private String initialLanguage;
|
||||||
private final ActivityResultLauncher<Intent> requestImportPathLauncher =
|
|
||||||
registerForActivityResult(new StartActivityForResult(), this::requestImportPathResult);
|
|
||||||
private final ActivityResultLauncher<Intent> requestExportPathLauncher =
|
|
||||||
registerForActivityResult(new StartActivityForResult(), this::requestExportPathResult);
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCreatePreferences(final Bundle savedInstanceState, final String rootKey) {
|
public void onCreatePreferences(final Bundle savedInstanceState, final String rootKey) {
|
||||||
final File homeDir = ContextCompat.getDataDir(requireContext());
|
|
||||||
Objects.requireNonNull(homeDir);
|
|
||||||
manager = new ContentSettingsManager(new NewPipeFileLocator(homeDir));
|
|
||||||
manager.deleteSettingsFile();
|
|
||||||
|
|
||||||
importExportDataPathKey = getString(R.string.import_export_data_path);
|
|
||||||
youtubeRestrictedModeEnabledKey = getString(R.string.youtube_restricted_mode_enabled);
|
youtubeRestrictedModeEnabledKey = getString(R.string.youtube_restricted_mode_enabled);
|
||||||
|
|
||||||
addPreferencesFromResourceRegistry();
|
addPreferencesFromResourceRegistry();
|
||||||
|
|
||||||
final Preference importDataPreference = requirePreference(R.string.import_data);
|
|
||||||
importDataPreference.setOnPreferenceClickListener((Preference p) -> {
|
|
||||||
NoFileManagerSafeGuard.launchSafe(
|
|
||||||
requestImportPathLauncher,
|
|
||||||
StoredFileHelper.getPicker(requireContext(),
|
|
||||||
ZIP_MIME_TYPE, getImportExportDataUri()),
|
|
||||||
TAG,
|
|
||||||
getContext()
|
|
||||||
);
|
|
||||||
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
|
|
||||||
final Preference exportDataPreference = requirePreference(R.string.export_data);
|
|
||||||
exportDataPreference.setOnPreferenceClickListener((final Preference p) -> {
|
|
||||||
NoFileManagerSafeGuard.launchSafe(
|
|
||||||
requestExportPathLauncher,
|
|
||||||
StoredFileHelper.getNewPicker(requireContext(),
|
|
||||||
"NewPipeData-" + exportDateFormat.format(new Date()) + ".zip",
|
|
||||||
ZIP_MIME_TYPE, getImportExportDataUri()),
|
|
||||||
TAG,
|
|
||||||
getContext()
|
|
||||||
);
|
|
||||||
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
|
|
||||||
initialSelectedLocalization = org.schabi.newpipe.util.Localization
|
initialSelectedLocalization = org.schabi.newpipe.util.Localization
|
||||||
.getPreferredLocalization(requireContext());
|
.getPreferredLocalization(requireContext());
|
||||||
initialSelectedContentCountry = org.schabi.newpipe.util.Localization
|
initialSelectedContentCountry = org.schabi.newpipe.util.Localization
|
||||||
|
@ -158,151 +88,4 @@ public class ContentSettingsFragment extends BasePreferenceFragment {
|
||||||
NewPipe.setupLocalization(selectedLocalization, selectedContentCountry);
|
NewPipe.setupLocalization(selectedLocalization, selectedContentCountry);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void requestExportPathResult(final ActivityResult result) {
|
|
||||||
assureCorrectAppLanguage(getContext());
|
|
||||||
if (result.getResultCode() == Activity.RESULT_OK && result.getData() != null) {
|
|
||||||
// will be saved only on success
|
|
||||||
final Uri lastExportDataUri = result.getData().getData();
|
|
||||||
|
|
||||||
final StoredFileHelper file =
|
|
||||||
new StoredFileHelper(getContext(), result.getData().getData(), ZIP_MIME_TYPE);
|
|
||||||
|
|
||||||
exportDatabase(file, lastExportDataUri);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void requestImportPathResult(final ActivityResult result) {
|
|
||||||
assureCorrectAppLanguage(getContext());
|
|
||||||
if (result.getResultCode() == Activity.RESULT_OK && result.getData() != null) {
|
|
||||||
// will be saved only on success
|
|
||||||
final Uri lastImportDataUri = result.getData().getData();
|
|
||||||
|
|
||||||
final StoredFileHelper file =
|
|
||||||
new StoredFileHelper(getContext(), result.getData().getData(), ZIP_MIME_TYPE);
|
|
||||||
|
|
||||||
new AlertDialog.Builder(requireActivity())
|
|
||||||
.setMessage(R.string.override_current_data)
|
|
||||||
.setPositiveButton(R.string.ok, (d, id) ->
|
|
||||||
importDatabase(file, lastImportDataUri))
|
|
||||||
.setNegativeButton(R.string.cancel, (d, id) ->
|
|
||||||
d.cancel())
|
|
||||||
.show();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void exportDatabase(final StoredFileHelper file, final Uri exportDataUri) {
|
|
||||||
try {
|
|
||||||
//checkpoint before export
|
|
||||||
NewPipeDatabase.checkpoint();
|
|
||||||
|
|
||||||
final SharedPreferences preferences = PreferenceManager
|
|
||||||
.getDefaultSharedPreferences(requireContext());
|
|
||||||
manager.exportDatabase(preferences, file);
|
|
||||||
|
|
||||||
saveLastImportExportDataUri(exportDataUri); // save export path only on success
|
|
||||||
Toast.makeText(getContext(), R.string.export_complete_toast, Toast.LENGTH_SHORT).show();
|
|
||||||
} catch (final Exception e) {
|
|
||||||
ErrorUtil.showUiErrorSnackbar(this, "Exporting database", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void importDatabase(final StoredFileHelper file, final Uri importDataUri) {
|
|
||||||
// check if file is supported
|
|
||||||
if (!ZipHelper.isValidZipFile(file)) {
|
|
||||||
Toast.makeText(getContext(), R.string.no_valid_zip_file, Toast.LENGTH_SHORT)
|
|
||||||
.show();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (!manager.ensureDbDirectoryExists()) {
|
|
||||||
throw new IOException("Could not create databases dir");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!manager.extractDb(file)) {
|
|
||||||
Toast.makeText(getContext(), R.string.could_not_import_all_files, Toast.LENGTH_LONG)
|
|
||||||
.show();
|
|
||||||
}
|
|
||||||
|
|
||||||
// if settings file exist, ask if it should be imported.
|
|
||||||
if (manager.extractSettings(file)) {
|
|
||||||
new AlertDialog.Builder(requireContext())
|
|
||||||
.setTitle(R.string.import_settings)
|
|
||||||
.setNegativeButton(R.string.cancel, (dialog, which) -> {
|
|
||||||
dialog.dismiss();
|
|
||||||
finishImport(importDataUri);
|
|
||||||
})
|
|
||||||
.setPositiveButton(R.string.ok, (dialog, which) -> {
|
|
||||||
dialog.dismiss();
|
|
||||||
final Context context = requireContext();
|
|
||||||
final SharedPreferences prefs = PreferenceManager
|
|
||||||
.getDefaultSharedPreferences(context);
|
|
||||||
manager.loadSharedPreferences(prefs);
|
|
||||||
cleanImport(context, prefs);
|
|
||||||
finishImport(importDataUri);
|
|
||||||
})
|
|
||||||
.show();
|
|
||||||
} else {
|
|
||||||
finishImport(importDataUri);
|
|
||||||
}
|
|
||||||
} catch (final Exception e) {
|
|
||||||
ErrorUtil.showUiErrorSnackbar(this, "Importing database", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove settings that are not supposed to be imported on different devices
|
|
||||||
* and reset them to default values.
|
|
||||||
* @param context the context used for the import
|
|
||||||
* @param prefs the preferences used while running the import
|
|
||||||
*/
|
|
||||||
private void cleanImport(@NonNull final Context context,
|
|
||||||
@NonNull final SharedPreferences prefs) {
|
|
||||||
// Check if media tunnelling needs to be disabled automatically,
|
|
||||||
// if it was disabled automatically in the imported preferences.
|
|
||||||
final String tunnelingKey = context.getString(R.string.disable_media_tunneling_key);
|
|
||||||
final String automaticTunnelingKey =
|
|
||||||
context.getString(R.string.disabled_media_tunneling_automatically_key);
|
|
||||||
// R.string.disable_media_tunneling_key should always be true
|
|
||||||
// if R.string.disabled_media_tunneling_automatically_key equals 1,
|
|
||||||
// but we double check here just to be sure and to avoid regressions
|
|
||||||
// caused by possible later modification of the media tunneling functionality.
|
|
||||||
// R.string.disabled_media_tunneling_automatically_key == 0:
|
|
||||||
// automatic value overridden by user in settings
|
|
||||||
// R.string.disabled_media_tunneling_automatically_key == -1: not set
|
|
||||||
final boolean wasMediaTunnelingDisabledAutomatically =
|
|
||||||
prefs.getInt(automaticTunnelingKey, -1) == 1
|
|
||||||
&& prefs.getBoolean(tunnelingKey, false);
|
|
||||||
if (wasMediaTunnelingDisabledAutomatically) {
|
|
||||||
prefs.edit()
|
|
||||||
.putInt(automaticTunnelingKey, -1)
|
|
||||||
.putBoolean(tunnelingKey, false)
|
|
||||||
.apply();
|
|
||||||
NewPipeSettings.setMediaTunneling(context);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Save import path and restart system.
|
|
||||||
*
|
|
||||||
* @param importDataUri The import path to save
|
|
||||||
*/
|
|
||||||
private void finishImport(final Uri importDataUri) {
|
|
||||||
// save import path only on success
|
|
||||||
saveLastImportExportDataUri(importDataUri);
|
|
||||||
// restart app to properly load db
|
|
||||||
NavigationHelper.restartApp(requireActivity());
|
|
||||||
}
|
|
||||||
|
|
||||||
private Uri getImportExportDataUri() {
|
|
||||||
final String path = defaultPreferences.getString(importExportDataPathKey, null);
|
|
||||||
return isBlank(path) ? null : Uri.parse(path);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void saveLastImportExportDataUri(final Uri importExportDataUri) {
|
|
||||||
final SharedPreferences.Editor editor = defaultPreferences.edit()
|
|
||||||
.putString(importExportDataPathKey, importExportDataUri.toString());
|
|
||||||
editor.apply();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,7 +23,7 @@ public class MainSettingsFragment extends BasePreferenceFragment {
|
||||||
setHasOptionsMenu(true); // Otherwise onCreateOptionsMenu is not called
|
setHasOptionsMenu(true); // Otherwise onCreateOptionsMenu is not called
|
||||||
|
|
||||||
// Check if the app is updatable
|
// Check if the app is updatable
|
||||||
if (!ReleaseVersionUtil.isReleaseApk()) {
|
if (!ReleaseVersionUtil.INSTANCE.isReleaseApk()) {
|
||||||
getPreferenceScreen().removePreference(
|
getPreferenceScreen().removePreference(
|
||||||
findPreference(getString(R.string.update_pref_screen_key)));
|
findPreference(getString(R.string.update_pref_screen_key)));
|
||||||
|
|
||||||
|
|
|
@ -11,6 +11,7 @@ import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.StringRes;
|
import androidx.annotation.StringRes;
|
||||||
import androidx.preference.PreferenceManager;
|
import androidx.preference.PreferenceManager;
|
||||||
|
|
||||||
|
import org.schabi.newpipe.App;
|
||||||
import org.schabi.newpipe.R;
|
import org.schabi.newpipe.R;
|
||||||
import org.schabi.newpipe.util.DeviceUtils;
|
import org.schabi.newpipe.util.DeviceUtils;
|
||||||
|
|
||||||
|
@ -44,14 +45,8 @@ public final class NewPipeSettings {
|
||||||
private NewPipeSettings() { }
|
private NewPipeSettings() { }
|
||||||
|
|
||||||
public static void initSettings(final Context context) {
|
public static void initSettings(final Context context) {
|
||||||
// check if the last used preference version is set
|
|
||||||
// to determine whether this is the first app run
|
|
||||||
final int lastUsedPrefVersion = PreferenceManager.getDefaultSharedPreferences(context)
|
|
||||||
.getInt(context.getString(R.string.last_used_preferences_version), -1);
|
|
||||||
final boolean isFirstRun = lastUsedPrefVersion == -1;
|
|
||||||
|
|
||||||
// first run migrations, then setDefaultValues, since the latter requires the correct types
|
// first run migrations, then setDefaultValues, since the latter requires the correct types
|
||||||
SettingMigrations.runMigrationsIfNeeded(context, isFirstRun);
|
SettingMigrations.runMigrationsIfNeeded(context);
|
||||||
|
|
||||||
// readAgain is true so that if new settings are added their default value is set
|
// readAgain is true so that if new settings are added their default value is set
|
||||||
PreferenceManager.setDefaultValues(context, R.xml.main_settings, true);
|
PreferenceManager.setDefaultValues(context, R.xml.main_settings, true);
|
||||||
|
@ -63,11 +58,12 @@ public final class NewPipeSettings {
|
||||||
PreferenceManager.setDefaultValues(context, R.xml.player_notification_settings, true);
|
PreferenceManager.setDefaultValues(context, R.xml.player_notification_settings, true);
|
||||||
PreferenceManager.setDefaultValues(context, R.xml.update_settings, true);
|
PreferenceManager.setDefaultValues(context, R.xml.update_settings, true);
|
||||||
PreferenceManager.setDefaultValues(context, R.xml.debug_settings, true);
|
PreferenceManager.setDefaultValues(context, R.xml.debug_settings, true);
|
||||||
|
PreferenceManager.setDefaultValues(context, R.xml.backup_restore_settings, true);
|
||||||
|
|
||||||
saveDefaultVideoDownloadDirectory(context);
|
saveDefaultVideoDownloadDirectory(context);
|
||||||
saveDefaultAudioDownloadDirectory(context);
|
saveDefaultAudioDownloadDirectory(context);
|
||||||
|
|
||||||
disableMediaTunnelingIfNecessary(context, isFirstRun);
|
disableMediaTunnelingIfNecessary(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void saveDefaultVideoDownloadDirectory(final Context context) {
|
static void saveDefaultVideoDownloadDirectory(final Context context) {
|
||||||
|
@ -145,8 +141,7 @@ public final class NewPipeSettings {
|
||||||
R.string.show_remote_search_suggestions_key);
|
R.string.show_remote_search_suggestions_key);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void disableMediaTunnelingIfNecessary(@NonNull final Context context,
|
private static void disableMediaTunnelingIfNecessary(@NonNull final Context context) {
|
||||||
final boolean isFirstRun) {
|
|
||||||
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
||||||
final String disabledTunnelingKey = context.getString(R.string.disable_media_tunneling_key);
|
final String disabledTunnelingKey = context.getString(R.string.disable_media_tunneling_key);
|
||||||
final String disabledTunnelingAutomaticallyKey =
|
final String disabledTunnelingAutomaticallyKey =
|
||||||
|
@ -161,7 +156,7 @@ public final class NewPipeSettings {
|
||||||
prefs.getInt(disabledTunnelingAutomaticallyKey, -1) == 0
|
prefs.getInt(disabledTunnelingAutomaticallyKey, -1) == 0
|
||||||
&& !prefs.getBoolean(disabledTunnelingKey, false);
|
&& !prefs.getBoolean(disabledTunnelingKey, false);
|
||||||
|
|
||||||
if (Boolean.TRUE.equals(isFirstRun)
|
if (App.getApp().isFirstRun()
|
||||||
|| (wasDeviceBlacklistUpdated && !wasMediaTunnelingEnabledByUser)) {
|
|| (wasDeviceBlacklistUpdated && !wasMediaTunnelingEnabledByUser)) {
|
||||||
setMediaTunneling(context);
|
setMediaTunneling(context);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
package org.schabi.newpipe.settings;
|
package org.schabi.newpipe.settings;
|
||||||
|
|
||||||
|
import static org.schabi.newpipe.local.bookmark.MergedPlaylistManager.getMergedOrderedPlaylists;
|
||||||
|
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
|
@ -31,7 +33,6 @@ import java.util.List;
|
||||||
import java.util.Vector;
|
import java.util.Vector;
|
||||||
|
|
||||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||||
import io.reactivex.rxjava3.core.Flowable;
|
|
||||||
import io.reactivex.rxjava3.disposables.Disposable;
|
import io.reactivex.rxjava3.disposables.Disposable;
|
||||||
|
|
||||||
public class SelectPlaylistFragment extends DialogFragment {
|
public class SelectPlaylistFragment extends DialogFragment {
|
||||||
|
@ -90,8 +91,7 @@ public class SelectPlaylistFragment extends DialogFragment {
|
||||||
final LocalPlaylistManager localPlaylistManager = new LocalPlaylistManager(database);
|
final LocalPlaylistManager localPlaylistManager = new LocalPlaylistManager(database);
|
||||||
final RemotePlaylistManager remotePlaylistManager = new RemotePlaylistManager(database);
|
final RemotePlaylistManager remotePlaylistManager = new RemotePlaylistManager(database);
|
||||||
|
|
||||||
disposable = Flowable.combineLatest(localPlaylistManager.getPlaylists(),
|
disposable = getMergedOrderedPlaylists(localPlaylistManager, remotePlaylistManager)
|
||||||
remotePlaylistManager.getPlaylists(), PlaylistLocalItem::merge)
|
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.subscribe(this::displayPlaylists, this::onError);
|
.subscribe(this::displayPlaylists, this::onError);
|
||||||
}
|
}
|
||||||
|
@ -118,7 +118,7 @@ public class SelectPlaylistFragment extends DialogFragment {
|
||||||
|
|
||||||
if (selectedItem instanceof PlaylistMetadataEntry) {
|
if (selectedItem instanceof PlaylistMetadataEntry) {
|
||||||
final PlaylistMetadataEntry entry = ((PlaylistMetadataEntry) selectedItem);
|
final PlaylistMetadataEntry entry = ((PlaylistMetadataEntry) selectedItem);
|
||||||
onSelectedListener.onLocalPlaylistSelected(entry.uid, entry.name);
|
onSelectedListener.onLocalPlaylistSelected(entry.getUid(), entry.name);
|
||||||
|
|
||||||
} else if (selectedItem instanceof PlaylistRemoteEntity) {
|
} else if (selectedItem instanceof PlaylistRemoteEntity) {
|
||||||
final PlaylistRemoteEntity entry = ((PlaylistRemoteEntity) selectedItem);
|
final PlaylistRemoteEntity entry = ((PlaylistRemoteEntity) selectedItem);
|
||||||
|
|
|
@ -7,6 +7,7 @@ import android.util.Log;
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.preference.PreferenceManager;
|
import androidx.preference.PreferenceManager;
|
||||||
|
|
||||||
|
import org.schabi.newpipe.App;
|
||||||
import org.schabi.newpipe.R;
|
import org.schabi.newpipe.R;
|
||||||
import org.schabi.newpipe.error.ErrorInfo;
|
import org.schabi.newpipe.error.ErrorInfo;
|
||||||
import org.schabi.newpipe.error.ErrorUtil;
|
import org.schabi.newpipe.error.ErrorUtil;
|
||||||
|
@ -163,15 +164,14 @@ public final class SettingMigrations {
|
||||||
private static final int VERSION = 6;
|
private static final int VERSION = 6;
|
||||||
|
|
||||||
|
|
||||||
public static void runMigrationsIfNeeded(@NonNull final Context context,
|
public static void runMigrationsIfNeeded(@NonNull final Context context) {
|
||||||
final boolean isFirstRun) {
|
|
||||||
// setup migrations and check if there is something to do
|
// setup migrations and check if there is something to do
|
||||||
sp = PreferenceManager.getDefaultSharedPreferences(context);
|
sp = PreferenceManager.getDefaultSharedPreferences(context);
|
||||||
final String lastPrefVersionKey = context.getString(R.string.last_used_preferences_version);
|
final String lastPrefVersionKey = context.getString(R.string.last_used_preferences_version);
|
||||||
final int lastPrefVersion = sp.getInt(lastPrefVersionKey, 0);
|
final int lastPrefVersion = sp.getInt(lastPrefVersionKey, 0);
|
||||||
|
|
||||||
// no migration to run, already up to date
|
// no migration to run, already up to date
|
||||||
if (isFirstRun) {
|
if (App.getApp().isFirstRun()) {
|
||||||
sp.edit().putInt(lastPrefVersionKey, VERSION).apply();
|
sp.edit().putInt(lastPrefVersionKey, VERSION).apply();
|
||||||
return;
|
return;
|
||||||
} else if (lastPrefVersion == VERSION) {
|
} else if (lastPrefVersion == VERSION) {
|
||||||
|
|
|
@ -266,7 +266,7 @@ public class SettingsActivity extends AppCompatActivity implements
|
||||||
*/
|
*/
|
||||||
private void ensureSearchRepresentsApplicationState() {
|
private void ensureSearchRepresentsApplicationState() {
|
||||||
// Check if the update settings are available
|
// Check if the update settings are available
|
||||||
if (!ReleaseVersionUtil.isReleaseApk()) {
|
if (!ReleaseVersionUtil.INSTANCE.isReleaseApk()) {
|
||||||
SettingsResourceRegistry.getInstance()
|
SettingsResourceRegistry.getInstance()
|
||||||
.getEntryByPreferencesResId(R.xml.update_settings)
|
.getEntryByPreferencesResId(R.xml.update_settings)
|
||||||
.setSearchable(false);
|
.setSearchable(false);
|
||||||
|
|
|
@ -41,6 +41,7 @@ public final class SettingsResourceRegistry {
|
||||||
add(UpdateSettingsFragment.class, R.xml.update_settings);
|
add(UpdateSettingsFragment.class, R.xml.update_settings);
|
||||||
add(VideoAudioSettingsFragment.class, R.xml.video_audio_settings);
|
add(VideoAudioSettingsFragment.class, R.xml.video_audio_settings);
|
||||||
add(ExoPlayerSettingsFragment.class, R.xml.exoplayer_settings);
|
add(ExoPlayerSettingsFragment.class, R.xml.exoplayer_settings);
|
||||||
|
add(BackupRestoreSettingsFragment.class, R.xml.backup_restore_settings);
|
||||||
}
|
}
|
||||||
|
|
||||||
private SettingRegistryEntry add(
|
private SettingRegistryEntry add(
|
||||||
|
|
|
@ -1,9 +1,12 @@
|
||||||
package org.schabi.newpipe.settings;
|
package org.schabi.newpipe.settings;
|
||||||
|
|
||||||
|
import android.app.AlertDialog;
|
||||||
|
import android.content.Context;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
|
|
||||||
import androidx.preference.Preference;
|
import androidx.preference.Preference;
|
||||||
|
import androidx.preference.PreferenceManager;
|
||||||
|
|
||||||
import org.schabi.newpipe.NewVersionWorker;
|
import org.schabi.newpipe.NewVersionWorker;
|
||||||
import org.schabi.newpipe.R;
|
import org.schabi.newpipe.R;
|
||||||
|
@ -36,4 +39,38 @@ public class UpdateSettingsFragment extends BasePreferenceFragment {
|
||||||
findPreference(getString(R.string.manual_update_key))
|
findPreference(getString(R.string.manual_update_key))
|
||||||
.setOnPreferenceClickListener(manualUpdateClick);
|
.setOnPreferenceClickListener(manualUpdateClick);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void askForConsentToUpdateChecks(final Context context) {
|
||||||
|
new AlertDialog.Builder(context)
|
||||||
|
.setTitle(context.getString(R.string.check_for_updates))
|
||||||
|
.setMessage(context.getString(R.string.auto_update_check_description))
|
||||||
|
.setPositiveButton(context.getString(R.string.yes), (d, w) -> {
|
||||||
|
d.dismiss();
|
||||||
|
setAutoUpdateCheckEnabled(context, true);
|
||||||
|
})
|
||||||
|
.setNegativeButton(R.string.no, (d, w) -> {
|
||||||
|
d.dismiss();
|
||||||
|
// set explicitly to false, since the default is true on previous versions
|
||||||
|
setAutoUpdateCheckEnabled(context, false);
|
||||||
|
})
|
||||||
|
.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void setAutoUpdateCheckEnabled(final Context context, final boolean enabled) {
|
||||||
|
PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
|
.edit()
|
||||||
|
.putBoolean(context.getString(R.string.update_app_key), enabled)
|
||||||
|
.putBoolean(context.getString(R.string.update_check_consent_key), true)
|
||||||
|
.apply();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the user was asked for consent to automatically check for app updates.
|
||||||
|
* @param context
|
||||||
|
* @return true if the user was asked for consent, false otherwise
|
||||||
|
*/
|
||||||
|
public static boolean wasUserAskedForConsent(final Context context) {
|
||||||
|
return PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
|
.getBoolean(context.getString(R.string.update_check_consent_key), false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,35 +5,22 @@ import static org.schabi.newpipe.player.notification.NotificationConstants.ACTIO
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
import android.content.res.ColorStateList;
|
import android.os.Build;
|
||||||
import android.util.AttributeSet;
|
import android.util.AttributeSet;
|
||||||
import android.view.LayoutInflater;
|
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
|
||||||
import android.widget.CheckBox;
|
import android.widget.CheckBox;
|
||||||
import android.widget.ImageView;
|
|
||||||
import android.widget.RadioButton;
|
|
||||||
import android.widget.RadioGroup;
|
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import androidx.appcompat.app.AlertDialog;
|
|
||||||
import androidx.appcompat.content.res.AppCompatResources;
|
|
||||||
import androidx.core.widget.TextViewCompat;
|
|
||||||
import androidx.preference.Preference;
|
import androidx.preference.Preference;
|
||||||
import androidx.preference.PreferenceViewHolder;
|
import androidx.preference.PreferenceViewHolder;
|
||||||
|
|
||||||
import org.schabi.newpipe.App;
|
import org.schabi.newpipe.App;
|
||||||
import org.schabi.newpipe.R;
|
import org.schabi.newpipe.R;
|
||||||
import org.schabi.newpipe.databinding.ListRadioIconItemBinding;
|
|
||||||
import org.schabi.newpipe.databinding.SingleChoiceDialogViewBinding;
|
|
||||||
import org.schabi.newpipe.player.notification.NotificationConstants;
|
import org.schabi.newpipe.player.notification.NotificationConstants;
|
||||||
import org.schabi.newpipe.util.DeviceUtils;
|
|
||||||
import org.schabi.newpipe.util.ThemeHelper;
|
|
||||||
import org.schabi.newpipe.views.FocusOverlayView;
|
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.stream.IntStream;
|
import java.util.stream.IntStream;
|
||||||
|
|
||||||
|
@ -45,8 +32,9 @@ public class NotificationActionsPreference extends Preference {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Nullable private NotificationSlot[] notificationSlots = null;
|
private NotificationSlot[] notificationSlots;
|
||||||
@Nullable private List<Integer> compactSlots = null;
|
private List<Integer> compactSlots;
|
||||||
|
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////
|
||||||
// Lifecycle
|
// Lifecycle
|
||||||
|
@ -56,6 +44,11 @@ public class NotificationActionsPreference extends Preference {
|
||||||
public void onBindViewHolder(@NonNull final PreferenceViewHolder holder) {
|
public void onBindViewHolder(@NonNull final PreferenceViewHolder holder) {
|
||||||
super.onBindViewHolder(holder);
|
super.onBindViewHolder(holder);
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
|
((TextView) holder.itemView.findViewById(R.id.summary))
|
||||||
|
.setText(R.string.notification_actions_summary_android13);
|
||||||
|
}
|
||||||
|
|
||||||
holder.itemView.setClickable(false);
|
holder.itemView.setClickable(false);
|
||||||
setupActions(holder.itemView);
|
setupActions(holder.itemView);
|
||||||
}
|
}
|
||||||
|
@ -75,13 +68,29 @@ public class NotificationActionsPreference extends Preference {
|
||||||
////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
private void setupActions(@NonNull final View view) {
|
private void setupActions(@NonNull final View view) {
|
||||||
compactSlots = NotificationConstants.getCompactSlotsFromPreferences(getContext(),
|
compactSlots = new ArrayList<>(NotificationConstants.getCompactSlotsFromPreferences(
|
||||||
getSharedPreferences(), 5);
|
getContext(), getSharedPreferences()));
|
||||||
notificationSlots = IntStream.range(0, 5)
|
notificationSlots = IntStream.range(0, 5)
|
||||||
.mapToObj(i -> new NotificationSlot(i, view))
|
.mapToObj(i -> new NotificationSlot(getContext(), getSharedPreferences(), i, view,
|
||||||
|
compactSlots.contains(i), this::onToggleCompactSlot))
|
||||||
.toArray(NotificationSlot[]::new);
|
.toArray(NotificationSlot[]::new);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void onToggleCompactSlot(final int i, final CheckBox checkBox) {
|
||||||
|
if (checkBox.isChecked()) {
|
||||||
|
compactSlots.remove((Integer) i);
|
||||||
|
} else if (compactSlots.size() < 3) {
|
||||||
|
compactSlots.add(i);
|
||||||
|
} else {
|
||||||
|
Toast.makeText(getContext(),
|
||||||
|
R.string.notification_actions_at_most_three,
|
||||||
|
Toast.LENGTH_SHORT).show();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
checkBox.toggle();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////
|
||||||
// Saving
|
// Saving
|
||||||
|
@ -99,143 +108,10 @@ public class NotificationActionsPreference extends Preference {
|
||||||
|
|
||||||
for (int i = 0; i < 5; i++) {
|
for (int i = 0; i < 5; i++) {
|
||||||
editor.putInt(getContext().getString(NotificationConstants.SLOT_PREF_KEYS[i]),
|
editor.putInt(getContext().getString(NotificationConstants.SLOT_PREF_KEYS[i]),
|
||||||
notificationSlots[i].selectedAction);
|
notificationSlots[i].getSelectedAction());
|
||||||
}
|
}
|
||||||
|
|
||||||
editor.apply();
|
editor.apply();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////
|
|
||||||
// Notification action
|
|
||||||
////////////////////////////////////////////////////////////////////////////
|
|
||||||
|
|
||||||
private static final int[] SLOT_ITEMS = {
|
|
||||||
R.id.notificationAction0,
|
|
||||||
R.id.notificationAction1,
|
|
||||||
R.id.notificationAction2,
|
|
||||||
R.id.notificationAction3,
|
|
||||||
R.id.notificationAction4,
|
|
||||||
};
|
|
||||||
|
|
||||||
private static final int[] SLOT_TITLES = {
|
|
||||||
R.string.notification_action_0_title,
|
|
||||||
R.string.notification_action_1_title,
|
|
||||||
R.string.notification_action_2_title,
|
|
||||||
R.string.notification_action_3_title,
|
|
||||||
R.string.notification_action_4_title,
|
|
||||||
};
|
|
||||||
|
|
||||||
private class NotificationSlot {
|
|
||||||
|
|
||||||
final int i;
|
|
||||||
@NotificationConstants.Action int selectedAction;
|
|
||||||
|
|
||||||
ImageView icon;
|
|
||||||
TextView summary;
|
|
||||||
|
|
||||||
NotificationSlot(final int actionIndex, final View parentView) {
|
|
||||||
this.i = actionIndex;
|
|
||||||
|
|
||||||
final View view = parentView.findViewById(SLOT_ITEMS[i]);
|
|
||||||
setupSelectedAction(view);
|
|
||||||
setupTitle(view);
|
|
||||||
setupCheckbox(view);
|
|
||||||
}
|
|
||||||
|
|
||||||
void setupTitle(final View view) {
|
|
||||||
((TextView) view.findViewById(R.id.notificationActionTitle))
|
|
||||||
.setText(SLOT_TITLES[i]);
|
|
||||||
view.findViewById(R.id.notificationActionClickableArea).setOnClickListener(
|
|
||||||
v -> openActionChooserDialog());
|
|
||||||
}
|
|
||||||
|
|
||||||
void setupCheckbox(final View view) {
|
|
||||||
final CheckBox compactSlotCheckBox = view.findViewById(R.id.notificationActionCheckBox);
|
|
||||||
compactSlotCheckBox.setChecked(compactSlots.contains(i));
|
|
||||||
view.findViewById(R.id.notificationActionCheckBoxClickableArea).setOnClickListener(
|
|
||||||
v -> {
|
|
||||||
if (compactSlotCheckBox.isChecked()) {
|
|
||||||
compactSlots.remove((Integer) i);
|
|
||||||
} else if (compactSlots.size() < 3) {
|
|
||||||
compactSlots.add(i);
|
|
||||||
} else {
|
|
||||||
Toast.makeText(getContext(),
|
|
||||||
R.string.notification_actions_at_most_three,
|
|
||||||
Toast.LENGTH_SHORT).show();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
compactSlotCheckBox.toggle();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
void setupSelectedAction(final View view) {
|
|
||||||
icon = view.findViewById(R.id.notificationActionIcon);
|
|
||||||
summary = view.findViewById(R.id.notificationActionSummary);
|
|
||||||
selectedAction = getSharedPreferences().getInt(
|
|
||||||
getContext().getString(NotificationConstants.SLOT_PREF_KEYS[i]),
|
|
||||||
NotificationConstants.SLOT_DEFAULTS[i]);
|
|
||||||
updateInfo();
|
|
||||||
}
|
|
||||||
|
|
||||||
void updateInfo() {
|
|
||||||
if (NotificationConstants.ACTION_ICONS[selectedAction] == 0) {
|
|
||||||
icon.setImageDrawable(null);
|
|
||||||
} else {
|
|
||||||
icon.setImageDrawable(AppCompatResources.getDrawable(getContext(),
|
|
||||||
NotificationConstants.ACTION_ICONS[selectedAction]));
|
|
||||||
}
|
|
||||||
|
|
||||||
summary.setText(NotificationConstants.getActionName(getContext(), selectedAction));
|
|
||||||
}
|
|
||||||
|
|
||||||
void openActionChooserDialog() {
|
|
||||||
final LayoutInflater inflater = LayoutInflater.from(getContext());
|
|
||||||
final SingleChoiceDialogViewBinding binding =
|
|
||||||
SingleChoiceDialogViewBinding.inflate(inflater);
|
|
||||||
|
|
||||||
final AlertDialog alertDialog = new AlertDialog.Builder(getContext())
|
|
||||||
.setTitle(SLOT_TITLES[i])
|
|
||||||
.setView(binding.getRoot())
|
|
||||||
.setCancelable(true)
|
|
||||||
.create();
|
|
||||||
|
|
||||||
final View.OnClickListener radioButtonsClickListener = v -> {
|
|
||||||
selectedAction = NotificationConstants.SLOT_ALLOWED_ACTIONS[i][v.getId()];
|
|
||||||
updateInfo();
|
|
||||||
alertDialog.dismiss();
|
|
||||||
};
|
|
||||||
|
|
||||||
for (int id = 0; id < NotificationConstants.SLOT_ALLOWED_ACTIONS[i].length; ++id) {
|
|
||||||
final int action = NotificationConstants.SLOT_ALLOWED_ACTIONS[i][id];
|
|
||||||
final RadioButton radioButton = ListRadioIconItemBinding.inflate(inflater)
|
|
||||||
.getRoot();
|
|
||||||
|
|
||||||
// if present set action icon with correct color
|
|
||||||
final int iconId = NotificationConstants.ACTION_ICONS[action];
|
|
||||||
if (iconId != 0) {
|
|
||||||
radioButton.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, iconId, 0);
|
|
||||||
|
|
||||||
final var color = ColorStateList.valueOf(ThemeHelper
|
|
||||||
.resolveColorFromAttr(getContext(), android.R.attr.textColorPrimary));
|
|
||||||
TextViewCompat.setCompoundDrawableTintList(radioButton, color);
|
|
||||||
}
|
|
||||||
|
|
||||||
radioButton.setText(NotificationConstants.getActionName(getContext(), action));
|
|
||||||
radioButton.setChecked(action == selectedAction);
|
|
||||||
radioButton.setId(id);
|
|
||||||
radioButton.setLayoutParams(new RadioGroup.LayoutParams(
|
|
||||||
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT));
|
|
||||||
radioButton.setOnClickListener(radioButtonsClickListener);
|
|
||||||
binding.list.addView(radioButton);
|
|
||||||
}
|
|
||||||
alertDialog.show();
|
|
||||||
|
|
||||||
if (DeviceUtils.isTv(getContext())) {
|
|
||||||
FocusOverlayView.setupFocusObserver(alertDialog);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,172 @@
|
||||||
|
package org.schabi.newpipe.settings.custom;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.SharedPreferences;
|
||||||
|
import android.content.res.ColorStateList;
|
||||||
|
import android.os.Build;
|
||||||
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.widget.CheckBox;
|
||||||
|
import android.widget.ImageView;
|
||||||
|
import android.widget.RadioButton;
|
||||||
|
import android.widget.RadioGroup;
|
||||||
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import androidx.appcompat.app.AlertDialog;
|
||||||
|
import androidx.appcompat.content.res.AppCompatResources;
|
||||||
|
import androidx.core.widget.TextViewCompat;
|
||||||
|
|
||||||
|
import org.schabi.newpipe.R;
|
||||||
|
import org.schabi.newpipe.databinding.ListRadioIconItemBinding;
|
||||||
|
import org.schabi.newpipe.databinding.SingleChoiceDialogViewBinding;
|
||||||
|
import org.schabi.newpipe.player.notification.NotificationConstants;
|
||||||
|
import org.schabi.newpipe.util.DeviceUtils;
|
||||||
|
import org.schabi.newpipe.util.ThemeHelper;
|
||||||
|
import org.schabi.newpipe.views.FocusOverlayView;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.function.BiConsumer;
|
||||||
|
|
||||||
|
class NotificationSlot {
|
||||||
|
|
||||||
|
private static final int[] SLOT_ITEMS = {
|
||||||
|
R.id.notificationAction0,
|
||||||
|
R.id.notificationAction1,
|
||||||
|
R.id.notificationAction2,
|
||||||
|
R.id.notificationAction3,
|
||||||
|
R.id.notificationAction4,
|
||||||
|
};
|
||||||
|
|
||||||
|
private static final int[] SLOT_TITLES = {
|
||||||
|
R.string.notification_action_0_title,
|
||||||
|
R.string.notification_action_1_title,
|
||||||
|
R.string.notification_action_2_title,
|
||||||
|
R.string.notification_action_3_title,
|
||||||
|
R.string.notification_action_4_title,
|
||||||
|
};
|
||||||
|
|
||||||
|
private final int i;
|
||||||
|
private @NotificationConstants.Action int selectedAction;
|
||||||
|
private final Context context;
|
||||||
|
private final BiConsumer<Integer, CheckBox> onToggleCompactSlot;
|
||||||
|
|
||||||
|
private ImageView icon;
|
||||||
|
private TextView summary;
|
||||||
|
|
||||||
|
NotificationSlot(final Context context,
|
||||||
|
final SharedPreferences prefs,
|
||||||
|
final int actionIndex,
|
||||||
|
final View parentView,
|
||||||
|
final boolean isCompactSlotChecked,
|
||||||
|
final BiConsumer<Integer, CheckBox> onToggleCompactSlot) {
|
||||||
|
this.context = context;
|
||||||
|
this.i = actionIndex;
|
||||||
|
this.onToggleCompactSlot = onToggleCompactSlot;
|
||||||
|
|
||||||
|
selectedAction = Objects.requireNonNull(prefs).getInt(
|
||||||
|
context.getString(NotificationConstants.SLOT_PREF_KEYS[i]),
|
||||||
|
NotificationConstants.SLOT_DEFAULTS[i]);
|
||||||
|
final View view = parentView.findViewById(SLOT_ITEMS[i]);
|
||||||
|
|
||||||
|
// only show the last two notification slots on Android 13+
|
||||||
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU || i >= 3) {
|
||||||
|
setupSelectedAction(view);
|
||||||
|
setupTitle(view);
|
||||||
|
setupCheckbox(view, isCompactSlotChecked);
|
||||||
|
} else {
|
||||||
|
view.setVisibility(View.GONE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void setupTitle(final View view) {
|
||||||
|
((TextView) view.findViewById(R.id.notificationActionTitle))
|
||||||
|
.setText(SLOT_TITLES[i]);
|
||||||
|
view.findViewById(R.id.notificationActionClickableArea).setOnClickListener(
|
||||||
|
v -> openActionChooserDialog());
|
||||||
|
}
|
||||||
|
|
||||||
|
void setupCheckbox(final View view, final boolean isCompactSlotChecked) {
|
||||||
|
final CheckBox compactSlotCheckBox = view.findViewById(R.id.notificationActionCheckBox);
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
|
// there are no compact slots to customize on Android 13+
|
||||||
|
compactSlotCheckBox.setVisibility(View.GONE);
|
||||||
|
view.findViewById(R.id.notificationActionCheckBoxClickableArea)
|
||||||
|
.setVisibility(View.GONE);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
compactSlotCheckBox.setChecked(isCompactSlotChecked);
|
||||||
|
view.findViewById(R.id.notificationActionCheckBoxClickableArea).setOnClickListener(
|
||||||
|
v -> onToggleCompactSlot.accept(i, compactSlotCheckBox));
|
||||||
|
}
|
||||||
|
|
||||||
|
void setupSelectedAction(final View view) {
|
||||||
|
icon = view.findViewById(R.id.notificationActionIcon);
|
||||||
|
summary = view.findViewById(R.id.notificationActionSummary);
|
||||||
|
updateInfo();
|
||||||
|
}
|
||||||
|
|
||||||
|
void updateInfo() {
|
||||||
|
if (NotificationConstants.ACTION_ICONS[selectedAction] == 0) {
|
||||||
|
icon.setImageDrawable(null);
|
||||||
|
} else {
|
||||||
|
icon.setImageDrawable(AppCompatResources.getDrawable(context,
|
||||||
|
NotificationConstants.ACTION_ICONS[selectedAction]));
|
||||||
|
}
|
||||||
|
|
||||||
|
summary.setText(NotificationConstants.getActionName(context, selectedAction));
|
||||||
|
}
|
||||||
|
|
||||||
|
void openActionChooserDialog() {
|
||||||
|
final LayoutInflater inflater = LayoutInflater.from(context);
|
||||||
|
final SingleChoiceDialogViewBinding binding =
|
||||||
|
SingleChoiceDialogViewBinding.inflate(inflater);
|
||||||
|
|
||||||
|
final AlertDialog alertDialog = new AlertDialog.Builder(context)
|
||||||
|
.setTitle(SLOT_TITLES[i])
|
||||||
|
.setView(binding.getRoot())
|
||||||
|
.setCancelable(true)
|
||||||
|
.create();
|
||||||
|
|
||||||
|
final View.OnClickListener radioButtonsClickListener = v -> {
|
||||||
|
selectedAction = NotificationConstants.ALL_ACTIONS[v.getId()];
|
||||||
|
updateInfo();
|
||||||
|
alertDialog.dismiss();
|
||||||
|
};
|
||||||
|
|
||||||
|
for (int id = 0; id < NotificationConstants.ALL_ACTIONS.length; ++id) {
|
||||||
|
final int action = NotificationConstants.ALL_ACTIONS[id];
|
||||||
|
final RadioButton radioButton = ListRadioIconItemBinding.inflate(inflater)
|
||||||
|
.getRoot();
|
||||||
|
|
||||||
|
// if present set action icon with correct color
|
||||||
|
final int iconId = NotificationConstants.ACTION_ICONS[action];
|
||||||
|
if (iconId != 0) {
|
||||||
|
radioButton.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, iconId, 0);
|
||||||
|
|
||||||
|
final var color = ColorStateList.valueOf(ThemeHelper
|
||||||
|
.resolveColorFromAttr(context, android.R.attr.textColorPrimary));
|
||||||
|
TextViewCompat.setCompoundDrawableTintList(radioButton, color);
|
||||||
|
}
|
||||||
|
|
||||||
|
radioButton.setText(NotificationConstants.getActionName(context, action));
|
||||||
|
radioButton.setChecked(action == selectedAction);
|
||||||
|
radioButton.setId(id);
|
||||||
|
radioButton.setLayoutParams(new RadioGroup.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT));
|
||||||
|
radioButton.setOnClickListener(radioButtonsClickListener);
|
||||||
|
binding.list.addView(radioButton);
|
||||||
|
}
|
||||||
|
alertDialog.show();
|
||||||
|
|
||||||
|
if (DeviceUtils.isTv(context)) {
|
||||||
|
FocusOverlayView.setupFocusObserver(alertDialog);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@NotificationConstants.Action
|
||||||
|
public int getSelectedAction() {
|
||||||
|
return selectedAction;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,11 +1,18 @@
|
||||||
package org.schabi.newpipe.streams.io;
|
package org.schabi.newpipe.streams.io;
|
||||||
|
|
||||||
|
import static android.provider.DocumentsContract.Document.COLUMN_DISPLAY_NAME;
|
||||||
|
import static android.provider.DocumentsContract.Root.COLUMN_DOCUMENT_ID;
|
||||||
|
import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty;
|
||||||
|
|
||||||
import android.content.ContentResolver;
|
import android.content.ContentResolver;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.database.Cursor;
|
import android.database.Cursor;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
|
import android.os.ParcelFileDescriptor;
|
||||||
import android.provider.DocumentsContract;
|
import android.provider.DocumentsContract;
|
||||||
|
import android.system.Os;
|
||||||
|
import android.system.StructStatVfs;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
@ -15,6 +22,7 @@ import androidx.documentfile.provider.DocumentFile;
|
||||||
import org.schabi.newpipe.settings.NewPipeSettings;
|
import org.schabi.newpipe.settings.NewPipeSettings;
|
||||||
import org.schabi.newpipe.util.FilePickerActivityHelper;
|
import org.schabi.newpipe.util.FilePickerActivityHelper;
|
||||||
|
|
||||||
|
import java.io.FileDescriptor;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
|
@ -26,10 +34,6 @@ import java.util.List;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
import java.util.stream.Stream;
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
import static android.provider.DocumentsContract.Document.COLUMN_DISPLAY_NAME;
|
|
||||||
import static android.provider.DocumentsContract.Root.COLUMN_DOCUMENT_ID;
|
|
||||||
import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty;
|
|
||||||
|
|
||||||
public class StoredDirectoryHelper {
|
public class StoredDirectoryHelper {
|
||||||
private static final String TAG = StoredDirectoryHelper.class.getSimpleName();
|
private static final String TAG = StoredDirectoryHelper.class.getSimpleName();
|
||||||
public static final int PERMISSION_FLAGS = Intent.FLAG_GRANT_READ_URI_PERMISSION
|
public static final int PERMISSION_FLAGS = Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||||
|
@ -38,6 +42,10 @@ public class StoredDirectoryHelper {
|
||||||
private Path ioTree;
|
private Path ioTree;
|
||||||
private DocumentFile docTree;
|
private DocumentFile docTree;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Context is `null` for non-SAF files, i.e. files that use `ioTree`.
|
||||||
|
*/
|
||||||
|
@Nullable
|
||||||
private Context context;
|
private Context context;
|
||||||
|
|
||||||
private final String tag;
|
private final String tag;
|
||||||
|
@ -168,6 +176,46 @@ public class StoredDirectoryHelper {
|
||||||
return docTree == null;
|
return docTree == null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get free memory of the storage partition this file belongs to (root of the directory).
|
||||||
|
* See <a href="https://stackoverflow.com/q/31171838">StackOverflow</a> and
|
||||||
|
* <a href="https://pubs.opengroup.org/onlinepubs/9699919799/functions/fstatvfs.html">
|
||||||
|
* {@code statvfs()} and {@code fstatvfs()} docs</a>
|
||||||
|
*
|
||||||
|
* @return amount of free memory in the volume of current directory (bytes), or {@link
|
||||||
|
* Long#MAX_VALUE} if an error occurred
|
||||||
|
*/
|
||||||
|
public long getFreeStorageSpace() {
|
||||||
|
try {
|
||||||
|
final StructStatVfs stat;
|
||||||
|
|
||||||
|
if (ioTree != null) {
|
||||||
|
// non-SAF file, use statvfs with the path directly (also, `context` would be null
|
||||||
|
// for non-SAF files, so we wouldn't be able to call `getContentResolver` anyway)
|
||||||
|
stat = Os.statvfs(ioTree.toString());
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// SAF file, we can't get a path directly, so obtain a file descriptor first
|
||||||
|
// and then use fstatvfs with the file descriptor
|
||||||
|
try (ParcelFileDescriptor parcelFileDescriptor =
|
||||||
|
context.getContentResolver().openFileDescriptor(getUri(), "r")) {
|
||||||
|
if (parcelFileDescriptor == null) {
|
||||||
|
return Long.MAX_VALUE;
|
||||||
|
}
|
||||||
|
final FileDescriptor fileDescriptor = parcelFileDescriptor.getFileDescriptor();
|
||||||
|
stat = Os.fstatvfs(fileDescriptor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// this is the same formula used inside the FsStat class
|
||||||
|
return stat.f_bavail * stat.f_frsize;
|
||||||
|
} catch (final Throwable e) {
|
||||||
|
// ignore any error
|
||||||
|
Log.e(TAG, "Could not get free storage space", e);
|
||||||
|
return Long.MAX_VALUE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Only using Java I/O. Creates the directory named by this abstract pathname, including any
|
* Only using Java I/O. Creates the directory named by this abstract pathname, including any
|
||||||
* necessary but nonexistent parent directories.
|
* necessary but nonexistent parent directories.
|
||||||
|
|
|
@ -27,6 +27,7 @@ import android.util.Log;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.core.text.HtmlCompat;
|
import androidx.core.text.HtmlCompat;
|
||||||
import androidx.preference.PreferenceManager;
|
import androidx.preference.PreferenceManager;
|
||||||
|
@ -113,14 +114,14 @@ public final class ExtractorHelper {
|
||||||
public static Single<StreamInfo> getStreamInfo(final int serviceId, final String url,
|
public static Single<StreamInfo> getStreamInfo(final int serviceId, final String url,
|
||||||
final boolean forceLoad) {
|
final boolean forceLoad) {
|
||||||
checkServiceId(serviceId);
|
checkServiceId(serviceId);
|
||||||
return checkCache(forceLoad, serviceId, url, InfoItem.InfoType.STREAM,
|
return checkCache(forceLoad, serviceId, url, InfoCache.Type.STREAM,
|
||||||
Single.fromCallable(() -> StreamInfo.getInfo(NewPipe.getService(serviceId), url)));
|
Single.fromCallable(() -> StreamInfo.getInfo(NewPipe.getService(serviceId), url)));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Single<ChannelInfo> getChannelInfo(final int serviceId, final String url,
|
public static Single<ChannelInfo> getChannelInfo(final int serviceId, final String url,
|
||||||
final boolean forceLoad) {
|
final boolean forceLoad) {
|
||||||
checkServiceId(serviceId);
|
checkServiceId(serviceId);
|
||||||
return checkCache(forceLoad, serviceId, url, InfoItem.InfoType.CHANNEL,
|
return checkCache(forceLoad, serviceId, url, InfoCache.Type.CHANNEL,
|
||||||
Single.fromCallable(() ->
|
Single.fromCallable(() ->
|
||||||
ChannelInfo.getInfo(NewPipe.getService(serviceId), url)));
|
ChannelInfo.getInfo(NewPipe.getService(serviceId), url)));
|
||||||
}
|
}
|
||||||
|
@ -130,7 +131,7 @@ public final class ExtractorHelper {
|
||||||
final boolean forceLoad) {
|
final boolean forceLoad) {
|
||||||
checkServiceId(serviceId);
|
checkServiceId(serviceId);
|
||||||
return checkCache(forceLoad, serviceId,
|
return checkCache(forceLoad, serviceId,
|
||||||
listLinkHandler.getUrl(), InfoItem.InfoType.CHANNEL,
|
listLinkHandler.getUrl(), InfoCache.Type.CHANNEL_TAB,
|
||||||
Single.fromCallable(() ->
|
Single.fromCallable(() ->
|
||||||
ChannelTabInfo.getInfo(NewPipe.getService(serviceId), listLinkHandler)));
|
ChannelTabInfo.getInfo(NewPipe.getService(serviceId), listLinkHandler)));
|
||||||
}
|
}
|
||||||
|
@ -145,10 +146,11 @@ public final class ExtractorHelper {
|
||||||
listLinkHandler, nextPage));
|
listLinkHandler, nextPage));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Single<CommentsInfo> getCommentsInfo(final int serviceId, final String url,
|
public static Single<CommentsInfo> getCommentsInfo(final int serviceId,
|
||||||
|
final String url,
|
||||||
final boolean forceLoad) {
|
final boolean forceLoad) {
|
||||||
checkServiceId(serviceId);
|
checkServiceId(serviceId);
|
||||||
return checkCache(forceLoad, serviceId, url, InfoItem.InfoType.COMMENT,
|
return checkCache(forceLoad, serviceId, url, InfoCache.Type.COMMENTS,
|
||||||
Single.fromCallable(() ->
|
Single.fromCallable(() ->
|
||||||
CommentsInfo.getInfo(NewPipe.getService(serviceId), url)));
|
CommentsInfo.getInfo(NewPipe.getService(serviceId), url)));
|
||||||
}
|
}
|
||||||
|
@ -162,11 +164,20 @@ public final class ExtractorHelper {
|
||||||
CommentsInfo.getMoreItems(NewPipe.getService(serviceId), info, nextPage));
|
CommentsInfo.getMoreItems(NewPipe.getService(serviceId), info, nextPage));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static Single<InfoItemsPage<CommentsInfoItem>> getMoreCommentItems(
|
||||||
|
final int serviceId,
|
||||||
|
final String url,
|
||||||
|
final Page nextPage) {
|
||||||
|
checkServiceId(serviceId);
|
||||||
|
return Single.fromCallable(() ->
|
||||||
|
CommentsInfo.getMoreItems(NewPipe.getService(serviceId), url, nextPage));
|
||||||
|
}
|
||||||
|
|
||||||
public static Single<PlaylistInfo> getPlaylistInfo(final int serviceId,
|
public static Single<PlaylistInfo> getPlaylistInfo(final int serviceId,
|
||||||
final String url,
|
final String url,
|
||||||
final boolean forceLoad) {
|
final boolean forceLoad) {
|
||||||
checkServiceId(serviceId);
|
checkServiceId(serviceId);
|
||||||
return checkCache(forceLoad, serviceId, url, InfoItem.InfoType.PLAYLIST,
|
return checkCache(forceLoad, serviceId, url, InfoCache.Type.PLAYLIST,
|
||||||
Single.fromCallable(() ->
|
Single.fromCallable(() ->
|
||||||
PlaylistInfo.getInfo(NewPipe.getService(serviceId), url)));
|
PlaylistInfo.getInfo(NewPipe.getService(serviceId), url)));
|
||||||
}
|
}
|
||||||
|
@ -179,9 +190,10 @@ public final class ExtractorHelper {
|
||||||
PlaylistInfo.getMoreItems(NewPipe.getService(serviceId), url, nextPage));
|
PlaylistInfo.getMoreItems(NewPipe.getService(serviceId), url, nextPage));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Single<KioskInfo> getKioskInfo(final int serviceId, final String url,
|
public static Single<KioskInfo> getKioskInfo(final int serviceId,
|
||||||
|
final String url,
|
||||||
final boolean forceLoad) {
|
final boolean forceLoad) {
|
||||||
return checkCache(forceLoad, serviceId, url, InfoItem.InfoType.PLAYLIST,
|
return checkCache(forceLoad, serviceId, url, InfoCache.Type.KIOSK,
|
||||||
Single.fromCallable(() -> KioskInfo.getInfo(NewPipe.getService(serviceId), url)));
|
Single.fromCallable(() -> KioskInfo.getInfo(NewPipe.getService(serviceId), url)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -193,7 +205,7 @@ public final class ExtractorHelper {
|
||||||
}
|
}
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
// Utils
|
// Cache
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -205,24 +217,25 @@ public final class ExtractorHelper {
|
||||||
* @param forceLoad whether to force loading from the network instead of from the cache
|
* @param forceLoad whether to force loading from the network instead of from the cache
|
||||||
* @param serviceId the service to load from
|
* @param serviceId the service to load from
|
||||||
* @param url the URL to load
|
* @param url the URL to load
|
||||||
* @param infoType the {@link InfoItem.InfoType} of the item
|
* @param cacheType the {@link InfoCache.Type} of the item
|
||||||
* @param loadFromNetwork the {@link Single} to load the item from the network
|
* @param loadFromNetwork the {@link Single} to load the item from the network
|
||||||
* @return a {@link Single} that loads the item
|
* @return a {@link Single} that loads the item
|
||||||
*/
|
*/
|
||||||
private static <I extends Info> Single<I> checkCache(final boolean forceLoad,
|
private static <I extends Info> Single<I> checkCache(final boolean forceLoad,
|
||||||
final int serviceId, final String url,
|
final int serviceId,
|
||||||
final InfoItem.InfoType infoType,
|
@NonNull final String url,
|
||||||
final Single<I> loadFromNetwork) {
|
@NonNull final InfoCache.Type cacheType,
|
||||||
|
@NonNull final Single<I> loadFromNetwork) {
|
||||||
checkServiceId(serviceId);
|
checkServiceId(serviceId);
|
||||||
final Single<I> actualLoadFromNetwork = loadFromNetwork
|
final Single<I> actualLoadFromNetwork = loadFromNetwork
|
||||||
.doOnSuccess(info -> CACHE.putInfo(serviceId, url, info, infoType));
|
.doOnSuccess(info -> CACHE.putInfo(serviceId, url, info, cacheType));
|
||||||
|
|
||||||
final Single<I> load;
|
final Single<I> load;
|
||||||
if (forceLoad) {
|
if (forceLoad) {
|
||||||
CACHE.removeInfo(serviceId, url, infoType);
|
CACHE.removeInfo(serviceId, url, cacheType);
|
||||||
load = actualLoadFromNetwork;
|
load = actualLoadFromNetwork;
|
||||||
} else {
|
} else {
|
||||||
load = Maybe.concat(ExtractorHelper.loadFromCache(serviceId, url, infoType),
|
load = Maybe.concat(ExtractorHelper.loadFromCache(serviceId, url, cacheType),
|
||||||
actualLoadFromNetwork.toMaybe())
|
actualLoadFromNetwork.toMaybe())
|
||||||
.firstElement() // Take the first valid
|
.firstElement() // Take the first valid
|
||||||
.toSingle();
|
.toSingle();
|
||||||
|
@ -237,15 +250,17 @@ public final class ExtractorHelper {
|
||||||
* @param <I> the item type's class that extends {@link Info}
|
* @param <I> the item type's class that extends {@link Info}
|
||||||
* @param serviceId the service to load from
|
* @param serviceId the service to load from
|
||||||
* @param url the URL to load
|
* @param url the URL to load
|
||||||
* @param infoType the {@link InfoItem.InfoType} of the item
|
* @param cacheType the {@link InfoCache.Type} of the item
|
||||||
* @return a {@link Single} that loads the item
|
* @return a {@link Single} that loads the item
|
||||||
*/
|
*/
|
||||||
private static <I extends Info> Maybe<I> loadFromCache(final int serviceId, final String url,
|
private static <I extends Info> Maybe<I> loadFromCache(
|
||||||
final InfoItem.InfoType infoType) {
|
final int serviceId,
|
||||||
|
@NonNull final String url,
|
||||||
|
@NonNull final InfoCache.Type cacheType) {
|
||||||
checkServiceId(serviceId);
|
checkServiceId(serviceId);
|
||||||
return Maybe.defer(() -> {
|
return Maybe.defer(() -> {
|
||||||
//noinspection unchecked
|
//noinspection unchecked
|
||||||
final I info = (I) CACHE.getFromKey(serviceId, url, infoType);
|
final I info = (I) CACHE.getFromKey(serviceId, url, cacheType);
|
||||||
if (MainActivity.DEBUG) {
|
if (MainActivity.DEBUG) {
|
||||||
Log.d(TAG, "loadFromCache() called, info > " + info);
|
Log.d(TAG, "loadFromCache() called, info > " + info);
|
||||||
}
|
}
|
||||||
|
@ -259,11 +274,17 @@ public final class ExtractorHelper {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public static boolean isCached(final int serviceId, final String url,
|
public static boolean isCached(final int serviceId,
|
||||||
final InfoItem.InfoType infoType) {
|
@NonNull final String url,
|
||||||
return null != loadFromCache(serviceId, url, infoType).blockingGet();
|
@NonNull final InfoCache.Type cacheType) {
|
||||||
|
return null != loadFromCache(serviceId, url, cacheType).blockingGet();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
|
// Utils
|
||||||
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Formats the text contained in the meta info list as HTML and puts it into the text view,
|
* Formats the text contained in the meta info list as HTML and puts it into the text view,
|
||||||
* while also making the separator visible. If the list is null or empty, or the user chose not
|
* while also making the separator visible. If the list is null or empty, or the user chose not
|
||||||
|
|
|
@ -27,7 +27,6 @@ import androidx.collection.LruCache;
|
||||||
|
|
||||||
import org.schabi.newpipe.MainActivity;
|
import org.schabi.newpipe.MainActivity;
|
||||||
import org.schabi.newpipe.extractor.Info;
|
import org.schabi.newpipe.extractor.Info;
|
||||||
import org.schabi.newpipe.extractor.InfoItem;
|
|
||||||
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
|
@ -48,14 +47,27 @@ public final class InfoCache {
|
||||||
// no instance
|
// no instance
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Identifies the type of {@link Info} to put into the cache.
|
||||||
|
*/
|
||||||
|
public enum Type {
|
||||||
|
STREAM,
|
||||||
|
CHANNEL,
|
||||||
|
CHANNEL_TAB,
|
||||||
|
COMMENTS,
|
||||||
|
PLAYLIST,
|
||||||
|
KIOSK,
|
||||||
|
}
|
||||||
|
|
||||||
public static InfoCache getInstance() {
|
public static InfoCache getInstance() {
|
||||||
return INSTANCE;
|
return INSTANCE;
|
||||||
}
|
}
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
private static String keyOf(final int serviceId, @NonNull final String url,
|
private static String keyOf(final int serviceId,
|
||||||
@NonNull final InfoItem.InfoType infoType) {
|
@NonNull final String url,
|
||||||
return serviceId + url + infoType.toString();
|
@NonNull final Type cacheType) {
|
||||||
|
return serviceId + ":" + cacheType.ordinal() + ":" + url;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void removeStaleCache() {
|
private static void removeStaleCache() {
|
||||||
|
@ -83,19 +95,22 @@ public final class InfoCache {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
public Info getFromKey(final int serviceId, @NonNull final String url,
|
public Info getFromKey(final int serviceId,
|
||||||
@NonNull final InfoItem.InfoType infoType) {
|
@NonNull final String url,
|
||||||
|
@NonNull final Type cacheType) {
|
||||||
if (DEBUG) {
|
if (DEBUG) {
|
||||||
Log.d(TAG, "getFromKey() called with: "
|
Log.d(TAG, "getFromKey() called with: "
|
||||||
+ "serviceId = [" + serviceId + "], url = [" + url + "]");
|
+ "serviceId = [" + serviceId + "], url = [" + url + "]");
|
||||||
}
|
}
|
||||||
synchronized (LRU_CACHE) {
|
synchronized (LRU_CACHE) {
|
||||||
return getInfo(keyOf(serviceId, url, infoType));
|
return getInfo(keyOf(serviceId, url, cacheType));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void putInfo(final int serviceId, @NonNull final String url, @NonNull final Info info,
|
public void putInfo(final int serviceId,
|
||||||
@NonNull final InfoItem.InfoType infoType) {
|
@NonNull final String url,
|
||||||
|
@NonNull final Info info,
|
||||||
|
@NonNull final Type cacheType) {
|
||||||
if (DEBUG) {
|
if (DEBUG) {
|
||||||
Log.d(TAG, "putInfo() called with: info = [" + info + "]");
|
Log.d(TAG, "putInfo() called with: info = [" + info + "]");
|
||||||
}
|
}
|
||||||
|
@ -103,18 +118,19 @@ public final class InfoCache {
|
||||||
final long expirationMillis = ServiceHelper.getCacheExpirationMillis(info.getServiceId());
|
final long expirationMillis = ServiceHelper.getCacheExpirationMillis(info.getServiceId());
|
||||||
synchronized (LRU_CACHE) {
|
synchronized (LRU_CACHE) {
|
||||||
final CacheData data = new CacheData(info, expirationMillis);
|
final CacheData data = new CacheData(info, expirationMillis);
|
||||||
LRU_CACHE.put(keyOf(serviceId, url, infoType), data);
|
LRU_CACHE.put(keyOf(serviceId, url, cacheType), data);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void removeInfo(final int serviceId, @NonNull final String url,
|
public void removeInfo(final int serviceId,
|
||||||
@NonNull final InfoItem.InfoType infoType) {
|
@NonNull final String url,
|
||||||
|
@NonNull final Type cacheType) {
|
||||||
if (DEBUG) {
|
if (DEBUG) {
|
||||||
Log.d(TAG, "removeInfo() called with: "
|
Log.d(TAG, "removeInfo() called with: "
|
||||||
+ "serviceId = [" + serviceId + "], url = [" + url + "]");
|
+ "serviceId = [" + serviceId + "], url = [" + url + "]");
|
||||||
}
|
}
|
||||||
synchronized (LRU_CACHE) {
|
synchronized (LRU_CACHE) {
|
||||||
LRU_CACHE.remove(keyOf(serviceId, url, infoType));
|
LRU_CACHE.remove(keyOf(serviceId, url, cacheType));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -643,6 +643,7 @@ public final class ListHelper {
|
||||||
context.getString(R.string.best_resolution_key), defaultFormat, videoStreams);
|
context.getString(R.string.best_resolution_key), defaultFormat, videoStreams);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
private static MediaFormat getDefaultFormat(@NonNull final Context context,
|
private static MediaFormat getDefaultFormat(@NonNull final Context context,
|
||||||
@StringRes final int defaultFormatKey,
|
@StringRes final int defaultFormatKey,
|
||||||
@StringRes final int defaultFormatValueKey) {
|
@StringRes final int defaultFormatValueKey) {
|
||||||
|
@ -651,18 +652,14 @@ public final class ListHelper {
|
||||||
|
|
||||||
final String defaultFormat = context.getString(defaultFormatValueKey);
|
final String defaultFormat = context.getString(defaultFormatValueKey);
|
||||||
final String defaultFormatString = preferences.getString(
|
final String defaultFormatString = preferences.getString(
|
||||||
context.getString(defaultFormatKey), defaultFormat);
|
context.getString(defaultFormatKey),
|
||||||
|
defaultFormat
|
||||||
|
);
|
||||||
|
|
||||||
MediaFormat defaultMediaFormat = getMediaFormatFromKey(context, defaultFormatString);
|
return getMediaFormatFromKey(context, defaultFormatString);
|
||||||
if (defaultMediaFormat == null) {
|
|
||||||
preferences.edit().putString(context.getString(defaultFormatKey), defaultFormat)
|
|
||||||
.apply();
|
|
||||||
defaultMediaFormat = getMediaFormatFromKey(context, defaultFormat);
|
|
||||||
}
|
|
||||||
|
|
||||||
return defaultMediaFormat;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
private static MediaFormat getMediaFormatFromKey(@NonNull final Context context,
|
private static MediaFormat getMediaFormatFromKey(@NonNull final Context context,
|
||||||
@NonNull final String formatKey) {
|
@NonNull final String formatKey) {
|
||||||
MediaFormat format = null;
|
MediaFormat format = null;
|
||||||
|
@ -877,6 +874,7 @@ public final class ListHelper {
|
||||||
|
|
||||||
return Comparator.comparing(AudioStream::getAudioLocale, Comparator.nullsLast(
|
return Comparator.comparing(AudioStream::getAudioLocale, Comparator.nullsLast(
|
||||||
Comparator.comparing(locale -> locale.getDisplayName(appLoc))))
|
Comparator.comparing(locale -> locale.getDisplayName(appLoc))))
|
||||||
.thenComparing(AudioStream::getAudioTrackType);
|
.thenComparing(AudioStream::getAudioTrackType, Comparator.nullsLast(
|
||||||
|
Comparator.naturalOrder()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
package org.schabi.newpipe.util;
|
package org.schabi.newpipe.util;
|
||||||
|
|
||||||
|
import static org.schabi.newpipe.MainActivity.DEBUG;
|
||||||
|
|
||||||
import android.annotation.SuppressLint;
|
import android.annotation.SuppressLint;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
|
@ -22,6 +24,7 @@ import org.ocpsoft.prettytime.units.Decade;
|
||||||
import org.schabi.newpipe.R;
|
import org.schabi.newpipe.R;
|
||||||
import org.schabi.newpipe.extractor.ListExtractor;
|
import org.schabi.newpipe.extractor.ListExtractor;
|
||||||
import org.schabi.newpipe.extractor.localization.ContentCountry;
|
import org.schabi.newpipe.extractor.localization.ContentCountry;
|
||||||
|
import org.schabi.newpipe.extractor.localization.DateWrapper;
|
||||||
import org.schabi.newpipe.extractor.stream.AudioStream;
|
import org.schabi.newpipe.extractor.stream.AudioStream;
|
||||||
import org.schabi.newpipe.extractor.stream.AudioTrackType;
|
import org.schabi.newpipe.extractor.stream.AudioTrackType;
|
||||||
|
|
||||||
|
@ -82,7 +85,7 @@ public final class Localization {
|
||||||
.fromLocale(getPreferredLocale(context));
|
.fromLocale(getPreferredLocale(context));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static ContentCountry getPreferredContentCountry(final Context context) {
|
public static ContentCountry getPreferredContentCountry(@NonNull final Context context) {
|
||||||
final String contentCountry = PreferenceManager.getDefaultSharedPreferences(context)
|
final String contentCountry = PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
.getString(context.getString(R.string.content_country_key),
|
.getString(context.getString(R.string.content_country_key),
|
||||||
context.getString(R.string.default_localization_key));
|
context.getString(R.string.default_localization_key));
|
||||||
|
@ -92,41 +95,43 @@ public final class Localization {
|
||||||
return new ContentCountry(contentCountry);
|
return new ContentCountry(contentCountry);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Locale getPreferredLocale(final Context context) {
|
public static Locale getPreferredLocale(@NonNull final Context context) {
|
||||||
return getLocaleFromPrefs(context, R.string.content_language_key);
|
return getLocaleFromPrefs(context, R.string.content_language_key);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Locale getAppLocale(final Context context) {
|
public static Locale getAppLocale(@NonNull final Context context) {
|
||||||
return getLocaleFromPrefs(context, R.string.app_language_key);
|
return getLocaleFromPrefs(context, R.string.app_language_key);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String localizeNumber(final Context context, final long number) {
|
public static String localizeNumber(@NonNull final Context context, final long number) {
|
||||||
return localizeNumber(context, (double) number);
|
return localizeNumber(context, (double) number);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String localizeNumber(final Context context, final double number) {
|
public static String localizeNumber(@NonNull final Context context, final double number) {
|
||||||
final NumberFormat nf = NumberFormat.getInstance(getAppLocale(context));
|
final NumberFormat nf = NumberFormat.getInstance(getAppLocale(context));
|
||||||
return nf.format(number);
|
return nf.format(number);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String formatDate(final OffsetDateTime offsetDateTime, final Context context) {
|
public static String formatDate(@NonNull final Context context,
|
||||||
|
@NonNull final OffsetDateTime offsetDateTime) {
|
||||||
return DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM)
|
return DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM)
|
||||||
.withLocale(getAppLocale(context)).format(offsetDateTime
|
.withLocale(getAppLocale(context)).format(offsetDateTime
|
||||||
.atZoneSameInstant(ZoneId.systemDefault()));
|
.atZoneSameInstant(ZoneId.systemDefault()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("StringFormatInvalid")
|
@SuppressLint("StringFormatInvalid")
|
||||||
public static String localizeUploadDate(final Context context,
|
public static String localizeUploadDate(@NonNull final Context context,
|
||||||
final OffsetDateTime offsetDateTime) {
|
@NonNull final OffsetDateTime offsetDateTime) {
|
||||||
return context.getString(R.string.upload_date_text, formatDate(offsetDateTime, context));
|
return context.getString(R.string.upload_date_text, formatDate(context, offsetDateTime));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String localizeViewCount(final Context context, final long viewCount) {
|
public static String localizeViewCount(@NonNull final Context context, final long viewCount) {
|
||||||
return getQuantity(context, R.plurals.views, R.string.no_views, viewCount,
|
return getQuantity(context, R.plurals.views, R.string.no_views, viewCount,
|
||||||
localizeNumber(context, viewCount));
|
localizeNumber(context, viewCount));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String localizeStreamCount(final Context context, final long streamCount) {
|
public static String localizeStreamCount(@NonNull final Context context,
|
||||||
|
final long streamCount) {
|
||||||
switch ((int) streamCount) {
|
switch ((int) streamCount) {
|
||||||
case (int) ListExtractor.ITEM_COUNT_UNKNOWN:
|
case (int) ListExtractor.ITEM_COUNT_UNKNOWN:
|
||||||
return "";
|
return "";
|
||||||
|
@ -140,7 +145,8 @@ public final class Localization {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String localizeStreamCountMini(final Context context, final long streamCount) {
|
public static String localizeStreamCountMini(@NonNull final Context context,
|
||||||
|
final long streamCount) {
|
||||||
switch ((int) streamCount) {
|
switch ((int) streamCount) {
|
||||||
case (int) ListExtractor.ITEM_COUNT_UNKNOWN:
|
case (int) ListExtractor.ITEM_COUNT_UNKNOWN:
|
||||||
return "";
|
return "";
|
||||||
|
@ -153,12 +159,13 @@ public final class Localization {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String localizeWatchingCount(final Context context, final long watchingCount) {
|
public static String localizeWatchingCount(@NonNull final Context context,
|
||||||
|
final long watchingCount) {
|
||||||
return getQuantity(context, R.plurals.watching, R.string.no_one_watching, watchingCount,
|
return getQuantity(context, R.plurals.watching, R.string.no_one_watching, watchingCount,
|
||||||
localizeNumber(context, watchingCount));
|
localizeNumber(context, watchingCount));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String shortCount(final Context context, final long count) {
|
public static String shortCount(@NonNull final Context context, final long count) {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||||
return CompactDecimalFormat.getInstance(getAppLocale(context),
|
return CompactDecimalFormat.getInstance(getAppLocale(context),
|
||||||
CompactDecimalFormat.CompactStyle.SHORT).format(count);
|
CompactDecimalFormat.CompactStyle.SHORT).format(count);
|
||||||
|
@ -179,37 +186,79 @@ public final class Localization {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String listeningCount(final Context context, final long listeningCount) {
|
public static String listeningCount(@NonNull final Context context, final long listeningCount) {
|
||||||
return getQuantity(context, R.plurals.listening, R.string.no_one_listening, listeningCount,
|
return getQuantity(context, R.plurals.listening, R.string.no_one_listening, listeningCount,
|
||||||
shortCount(context, listeningCount));
|
shortCount(context, listeningCount));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String shortWatchingCount(final Context context, final long watchingCount) {
|
public static String shortWatchingCount(@NonNull final Context context,
|
||||||
|
final long watchingCount) {
|
||||||
return getQuantity(context, R.plurals.watching, R.string.no_one_watching, watchingCount,
|
return getQuantity(context, R.plurals.watching, R.string.no_one_watching, watchingCount,
|
||||||
shortCount(context, watchingCount));
|
shortCount(context, watchingCount));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String shortViewCount(final Context context, final long viewCount) {
|
public static String shortViewCount(@NonNull final Context context, final long viewCount) {
|
||||||
return getQuantity(context, R.plurals.views, R.string.no_views, viewCount,
|
return getQuantity(context, R.plurals.views, R.string.no_views, viewCount,
|
||||||
shortCount(context, viewCount));
|
shortCount(context, viewCount));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String shortSubscriberCount(final Context context, final long subscriberCount) {
|
public static String shortSubscriberCount(@NonNull final Context context,
|
||||||
|
final long subscriberCount) {
|
||||||
return getQuantity(context, R.plurals.subscribers, R.string.no_subscribers, subscriberCount,
|
return getQuantity(context, R.plurals.subscribers, R.string.no_subscribers, subscriberCount,
|
||||||
shortCount(context, subscriberCount));
|
shortCount(context, subscriberCount));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String downloadCount(final Context context, final int downloadCount) {
|
public static String downloadCount(@NonNull final Context context, final int downloadCount) {
|
||||||
return getQuantity(context, R.plurals.download_finished_notification, 0,
|
return getQuantity(context, R.plurals.download_finished_notification, 0,
|
||||||
downloadCount, shortCount(context, downloadCount));
|
downloadCount, shortCount(context, downloadCount));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String deletedDownloadCount(final Context context, final int deletedCount) {
|
public static String deletedDownloadCount(@NonNull final Context context,
|
||||||
|
final int deletedCount) {
|
||||||
return getQuantity(context, R.plurals.deleted_downloads_toast, 0,
|
return getQuantity(context, R.plurals.deleted_downloads_toast, 0,
|
||||||
deletedCount, shortCount(context, deletedCount));
|
deletedCount, shortCount(context, deletedCount));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static String replyCount(@NonNull final Context context, final int replyCount) {
|
||||||
|
return getQuantity(context, R.plurals.replies, 0, replyCount,
|
||||||
|
String.valueOf(replyCount));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param context the Android context
|
||||||
|
* @param likeCount the like count, possibly negative if unknown
|
||||||
|
* @return if {@code likeCount} is smaller than {@code 0}, the string {@code "-"}, otherwise
|
||||||
|
* the result of calling {@link #shortCount(Context, long)} on the like count
|
||||||
|
*/
|
||||||
|
public static String likeCount(@NonNull final Context context, final int likeCount) {
|
||||||
|
if (likeCount < 0) {
|
||||||
|
return "-";
|
||||||
|
} else {
|
||||||
|
return shortCount(context, likeCount);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a readable text for a duration in the format {@code days:hours:minutes:seconds}.
|
||||||
|
* Prepended zeros are removed.
|
||||||
|
* @param duration the duration in seconds
|
||||||
|
* @return a formatted duration String or {@code 0:00} if the duration is zero.
|
||||||
|
*/
|
||||||
public static String getDurationString(final long duration) {
|
public static String getDurationString(final long duration) {
|
||||||
|
return getDurationString(duration, true, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a readable text for a duration in the format {@code days:hours:minutes:seconds+}.
|
||||||
|
* Prepended zeros are removed. If the given duration is incomplete, a plus is appended to the
|
||||||
|
* duration string.
|
||||||
|
* @param duration the duration in seconds
|
||||||
|
* @param isDurationComplete whether the given duration is complete or whether info is missing
|
||||||
|
* @param showDurationPrefix whether the duration-prefix shall be shown
|
||||||
|
* @return a formatted duration String or {@code 0:00} if the duration is zero.
|
||||||
|
*/
|
||||||
|
public static String getDurationString(final long duration, final boolean isDurationComplete,
|
||||||
|
final boolean showDurationPrefix) {
|
||||||
final String output;
|
final String output;
|
||||||
|
|
||||||
final long days = duration / (24 * 60 * 60L); /* greater than a day */
|
final long days = duration / (24 * 60 * 60L); /* greater than a day */
|
||||||
|
@ -227,7 +276,9 @@ public final class Localization {
|
||||||
} else {
|
} else {
|
||||||
output = String.format(Locale.US, "%d:%02d", minutes, seconds);
|
output = String.format(Locale.US, "%d:%02d", minutes, seconds);
|
||||||
}
|
}
|
||||||
return output;
|
final String durationPrefix = showDurationPrefix ? "⏱ " : "";
|
||||||
|
final String durationPostfix = isDurationComplete ? "" : "+";
|
||||||
|
return durationPrefix + output + durationPostfix;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -241,7 +292,8 @@ public final class Localization {
|
||||||
* @return duration in a human readable string.
|
* @return duration in a human readable string.
|
||||||
*/
|
*/
|
||||||
@NonNull
|
@NonNull
|
||||||
public static String localizeDuration(final Context context, final int durationInSecs) {
|
public static String localizeDuration(@NonNull final Context context,
|
||||||
|
final int durationInSecs) {
|
||||||
if (durationInSecs < 0) {
|
if (durationInSecs < 0) {
|
||||||
throw new IllegalArgumentException("duration can not be negative");
|
throw new IllegalArgumentException("duration can not be negative");
|
||||||
}
|
}
|
||||||
|
@ -278,7 +330,7 @@ public final class Localization {
|
||||||
* @param track an {@link AudioStream} of the track
|
* @param track an {@link AudioStream} of the track
|
||||||
* @return the localized name of the audio track
|
* @return the localized name of the audio track
|
||||||
*/
|
*/
|
||||||
public static String audioTrackName(final Context context, final AudioStream track) {
|
public static String audioTrackName(@NonNull final Context context, final AudioStream track) {
|
||||||
final String name;
|
final String name;
|
||||||
if (track.getAudioLocale() != null) {
|
if (track.getAudioLocale() != null) {
|
||||||
name = track.getAudioLocale().getDisplayLanguage(getAppLocale(context));
|
name = track.getAudioLocale().getDisplayLanguage(getAppLocale(context));
|
||||||
|
@ -298,7 +350,8 @@ public final class Localization {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
private static String audioTrackType(final Context context, final AudioTrackType trackType) {
|
private static String audioTrackType(@NonNull final Context context,
|
||||||
|
final AudioTrackType trackType) {
|
||||||
switch (trackType) {
|
switch (trackType) {
|
||||||
case ORIGINAL:
|
case ORIGINAL:
|
||||||
return context.getString(R.string.audio_track_type_original);
|
return context.getString(R.string.audio_track_type_original);
|
||||||
|
@ -314,20 +367,45 @@ public final class Localization {
|
||||||
// Pretty Time
|
// Pretty Time
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
public static void initPrettyTime(final PrettyTime time) {
|
public static void initPrettyTime(@NonNull final PrettyTime time) {
|
||||||
prettyTime = time;
|
prettyTime = time;
|
||||||
// Do not use decades as YouTube doesn't either.
|
// Do not use decades as YouTube doesn't either.
|
||||||
prettyTime.removeUnit(Decade.class);
|
prettyTime.removeUnit(Decade.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static PrettyTime resolvePrettyTime(final Context context) {
|
public static PrettyTime resolvePrettyTime(@NonNull final Context context) {
|
||||||
return new PrettyTime(getAppLocale(context));
|
return new PrettyTime(getAppLocale(context));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String relativeTime(final OffsetDateTime offsetDateTime) {
|
public static String relativeTime(@NonNull final OffsetDateTime offsetDateTime) {
|
||||||
return prettyTime.formatUnrounded(offsetDateTime);
|
return prettyTime.formatUnrounded(offsetDateTime);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param context the Android context; if {@code null} then even if in debug mode and the
|
||||||
|
* setting is enabled, {@code textual} will not be shown next to {@code parsed}
|
||||||
|
* @param parsed the textual date or time ago parsed by NewPipeExtractor, or {@code null} if
|
||||||
|
* the extractor could not parse it
|
||||||
|
* @param textual the original textual date or time ago string as provided by services
|
||||||
|
* @return {@link #relativeTime(OffsetDateTime)} is used if {@code parsed != null}, otherwise
|
||||||
|
* {@code textual} is returned. If in debug mode, {@code context != null},
|
||||||
|
* {@code parsed != null} and the relevant setting is enabled, {@code textual} will
|
||||||
|
* be appended to the returned string for debugging purposes.
|
||||||
|
*/
|
||||||
|
public static String relativeTimeOrTextual(@Nullable final Context context,
|
||||||
|
@Nullable final DateWrapper parsed,
|
||||||
|
final String textual) {
|
||||||
|
if (parsed == null) {
|
||||||
|
return textual;
|
||||||
|
} else if (DEBUG && context != null && PreferenceManager
|
||||||
|
.getDefaultSharedPreferences(context)
|
||||||
|
.getBoolean(context.getString(R.string.show_original_time_ago_key), false)) {
|
||||||
|
return relativeTime(parsed.offsetDateTime()) + " (" + textual + ")";
|
||||||
|
} else {
|
||||||
|
return relativeTime(parsed.offsetDateTime());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static void assureCorrectAppLanguage(final Context c) {
|
public static void assureCorrectAppLanguage(final Context c) {
|
||||||
final Resources res = c.getResources();
|
final Resources res = c.getResources();
|
||||||
final DisplayMetrics dm = res.getDisplayMetrics();
|
final DisplayMetrics dm = res.getDisplayMetrics();
|
||||||
|
@ -336,7 +414,8 @@ public final class Localization {
|
||||||
res.updateConfiguration(conf, dm);
|
res.updateConfiguration(conf, dm);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Locale getLocaleFromPrefs(final Context context, @StringRes final int prefKey) {
|
private static Locale getLocaleFromPrefs(@NonNull final Context context,
|
||||||
|
@StringRes final int prefKey) {
|
||||||
final SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(context);
|
final SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(context);
|
||||||
final String defaultKey = context.getString(R.string.default_localization_key);
|
final String defaultKey = context.getString(R.string.default_localization_key);
|
||||||
final String languageCode = sp.getString(context.getString(prefKey), defaultKey);
|
final String languageCode = sp.getString(context.getString(prefKey), defaultKey);
|
||||||
|
@ -352,8 +431,10 @@ public final class Localization {
|
||||||
return new BigDecimal(value).setScale(1, RoundingMode.HALF_UP).doubleValue();
|
return new BigDecimal(value).setScale(1, RoundingMode.HALF_UP).doubleValue();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static String getQuantity(final Context context, @PluralsRes final int pluralId,
|
private static String getQuantity(@NonNull final Context context,
|
||||||
@StringRes final int zeroCaseStringId, final long count,
|
@PluralsRes final int pluralId,
|
||||||
|
@StringRes final int zeroCaseStringId,
|
||||||
|
final long count,
|
||||||
final String formattedCount) {
|
final String formattedCount) {
|
||||||
if (count == 0) {
|
if (count == 0) {
|
||||||
return context.getString(zeroCaseStringId);
|
return context.getString(zeroCaseStringId);
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
package org.schabi.newpipe.util;
|
package org.schabi.newpipe.util;
|
||||||
|
|
||||||
|
import static android.text.TextUtils.isEmpty;
|
||||||
import static org.schabi.newpipe.util.ListHelper.getUrlAndNonTorrentStreams;
|
import static org.schabi.newpipe.util.ListHelper.getUrlAndNonTorrentStreams;
|
||||||
|
|
||||||
import android.annotation.SuppressLint;
|
import android.annotation.SuppressLint;
|
||||||
|
@ -17,6 +18,7 @@ import androidx.appcompat.app.AlertDialog;
|
||||||
import androidx.appcompat.app.AppCompatActivity;
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
import androidx.core.content.ContextCompat;
|
import androidx.core.content.ContextCompat;
|
||||||
import androidx.fragment.app.Fragment;
|
import androidx.fragment.app.Fragment;
|
||||||
|
import androidx.fragment.app.FragmentActivity;
|
||||||
import androidx.fragment.app.FragmentManager;
|
import androidx.fragment.app.FragmentManager;
|
||||||
import androidx.fragment.app.FragmentTransaction;
|
import androidx.fragment.app.FragmentTransaction;
|
||||||
|
|
||||||
|
@ -29,8 +31,10 @@ import org.schabi.newpipe.RouterActivity;
|
||||||
import org.schabi.newpipe.about.AboutActivity;
|
import org.schabi.newpipe.about.AboutActivity;
|
||||||
import org.schabi.newpipe.database.feed.model.FeedGroupEntity;
|
import org.schabi.newpipe.database.feed.model.FeedGroupEntity;
|
||||||
import org.schabi.newpipe.download.DownloadActivity;
|
import org.schabi.newpipe.download.DownloadActivity;
|
||||||
|
import org.schabi.newpipe.error.ErrorUtil;
|
||||||
import org.schabi.newpipe.extractor.NewPipe;
|
import org.schabi.newpipe.extractor.NewPipe;
|
||||||
import org.schabi.newpipe.extractor.StreamingService;
|
import org.schabi.newpipe.extractor.StreamingService;
|
||||||
|
import org.schabi.newpipe.extractor.comments.CommentsInfoItem;
|
||||||
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||||
import org.schabi.newpipe.extractor.stream.AudioStream;
|
import org.schabi.newpipe.extractor.stream.AudioStream;
|
||||||
import org.schabi.newpipe.extractor.stream.DeliveryMethod;
|
import org.schabi.newpipe.extractor.stream.DeliveryMethod;
|
||||||
|
@ -41,6 +45,7 @@ import org.schabi.newpipe.extractor.stream.VideoStream;
|
||||||
import org.schabi.newpipe.fragments.MainFragment;
|
import org.schabi.newpipe.fragments.MainFragment;
|
||||||
import org.schabi.newpipe.fragments.detail.VideoDetailFragment;
|
import org.schabi.newpipe.fragments.detail.VideoDetailFragment;
|
||||||
import org.schabi.newpipe.fragments.list.channel.ChannelFragment;
|
import org.schabi.newpipe.fragments.list.channel.ChannelFragment;
|
||||||
|
import org.schabi.newpipe.fragments.list.comments.CommentRepliesFragment;
|
||||||
import org.schabi.newpipe.fragments.list.kiosk.KioskFragment;
|
import org.schabi.newpipe.fragments.list.kiosk.KioskFragment;
|
||||||
import org.schabi.newpipe.fragments.list.playlist.PlaylistFragment;
|
import org.schabi.newpipe.fragments.list.playlist.PlaylistFragment;
|
||||||
import org.schabi.newpipe.fragments.list.search.SearchFragment;
|
import org.schabi.newpipe.fragments.list.search.SearchFragment;
|
||||||
|
@ -476,6 +481,35 @@ public final class NavigationHelper {
|
||||||
item.getServiceId(), uploaderUrl, item.getUploaderName());
|
item.getServiceId(), uploaderUrl, item.getUploaderName());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens the comment author channel fragment, if the {@link CommentsInfoItem#getUploaderUrl()}
|
||||||
|
* of {@code comment} is non-null. Shows a UI-error snackbar if something goes wrong.
|
||||||
|
*
|
||||||
|
* @param activity the activity with the fragment manager and in which to show the snackbar
|
||||||
|
* @param comment the comment whose uploader/author will be opened
|
||||||
|
*/
|
||||||
|
public static void openCommentAuthorIfPresent(@NonNull final FragmentActivity activity,
|
||||||
|
@NonNull final CommentsInfoItem comment) {
|
||||||
|
if (isEmpty(comment.getUploaderUrl())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
openChannelFragment(activity.getSupportFragmentManager(), comment.getServiceId(),
|
||||||
|
comment.getUploaderUrl(), comment.getUploaderName());
|
||||||
|
} catch (final Exception e) {
|
||||||
|
ErrorUtil.showUiErrorSnackbar(activity, "Opening channel fragment", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void openCommentRepliesFragment(@NonNull final FragmentActivity activity,
|
||||||
|
@NonNull final CommentsInfoItem comment) {
|
||||||
|
defaultTransaction(activity.getSupportFragmentManager())
|
||||||
|
.replace(R.id.fragment_holder, new CommentRepliesFragment(comment),
|
||||||
|
CommentRepliesFragment.TAG)
|
||||||
|
.addToBackStack(CommentRepliesFragment.TAG)
|
||||||
|
.commit();
|
||||||
|
}
|
||||||
|
|
||||||
public static void openPlaylistFragment(final FragmentManager fragmentManager,
|
public static void openPlaylistFragment(final FragmentManager fragmentManager,
|
||||||
final int serviceId, final String url,
|
final int serviceId, final String url,
|
||||||
@NonNull final String name) {
|
@NonNull final String name) {
|
||||||
|
|
|
@ -1,27 +0,0 @@
|
||||||
package org.schabi.newpipe.util;
|
|
||||||
|
|
||||||
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;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
public class RelatedItemInfo extends ListInfo<InfoItem> {
|
|
||||||
public RelatedItemInfo(final int serviceId, final ListLinkHandler listUrlIdHandler,
|
|
||||||
final String name) {
|
|
||||||
super(serviceId, listUrlIdHandler, name);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static RelatedItemInfo getInfo(final StreamInfo info) {
|
|
||||||
final ListLinkHandler handler = new ListLinkHandler(
|
|
||||||
info.getOriginalUrl(), info.getUrl(), info.getId(), Collections.emptyList(), null);
|
|
||||||
final RelatedItemInfo relatedItemInfo = new RelatedItemInfo(
|
|
||||||
info.getServiceId(), handler, info.getName());
|
|
||||||
final List<InfoItem> relatedItems = new ArrayList<>(info.getRelatedItems());
|
|
||||||
relatedItemInfo.setRelatedItems(relatedItems);
|
|
||||||
return relatedItemInfo;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,97 +1,39 @@
|
||||||
package org.schabi.newpipe.util
|
package org.schabi.newpipe.util
|
||||||
|
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.content.pm.Signature
|
|
||||||
import androidx.core.content.pm.PackageInfoCompat
|
import androidx.core.content.pm.PackageInfoCompat
|
||||||
import org.schabi.newpipe.App
|
import org.schabi.newpipe.App
|
||||||
import org.schabi.newpipe.error.ErrorInfo
|
import org.schabi.newpipe.error.ErrorInfo
|
||||||
import org.schabi.newpipe.error.ErrorUtil.Companion.createNotification
|
import org.schabi.newpipe.error.ErrorUtil.Companion.createNotification
|
||||||
import org.schabi.newpipe.error.UserAction
|
import org.schabi.newpipe.error.UserAction
|
||||||
import java.security.MessageDigest
|
|
||||||
import java.security.NoSuchAlgorithmException
|
|
||||||
import java.security.cert.CertificateEncodingException
|
|
||||||
import java.security.cert.CertificateException
|
|
||||||
import java.security.cert.CertificateFactory
|
|
||||||
import java.security.cert.X509Certificate
|
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
import java.time.ZonedDateTime
|
import java.time.ZonedDateTime
|
||||||
import java.time.format.DateTimeFormatter
|
import java.time.format.DateTimeFormatter
|
||||||
|
|
||||||
object ReleaseVersionUtil {
|
object ReleaseVersionUtil {
|
||||||
// Public key of the certificate that is used in NewPipe release versions
|
// Public key of the certificate that is used in NewPipe release versions
|
||||||
private const val RELEASE_CERT_PUBLIC_KEY_SHA1 =
|
private const val RELEASE_CERT_PUBLIC_KEY_SHA256 =
|
||||||
"B0:2E:90:7C:1C:D6:FC:57:C3:35:F0:88:D0:8F:50:5F:94:E4:D2:15"
|
"cb84069bd68116bafae5ee4ee5b08a567aa6d898404e7cb12f9e756df5cf5cab"
|
||||||
|
|
||||||
@JvmStatic
|
@OptIn(ExperimentalStdlibApi::class)
|
||||||
fun isReleaseApk(): Boolean {
|
val isReleaseApk by lazy {
|
||||||
return certificateSHA1Fingerprint == RELEASE_CERT_PUBLIC_KEY_SHA1
|
@Suppress("NewApi")
|
||||||
}
|
val certificates = mapOf(
|
||||||
|
RELEASE_CERT_PUBLIC_KEY_SHA256.hexToByteArray() to PackageManager.CERT_INPUT_SHA256
|
||||||
/**
|
|
||||||
* Method to get the APK's SHA1 key. See https://stackoverflow.com/questions/9293019/#22506133.
|
|
||||||
*
|
|
||||||
* @return String with the APK's SHA1 fingerprint in hexadecimal
|
|
||||||
*/
|
|
||||||
private val certificateSHA1Fingerprint: String
|
|
||||||
get() {
|
|
||||||
val app = App.getApp()
|
|
||||||
val signatures: List<Signature> = try {
|
|
||||||
PackageInfoCompat.getSignatures(app.packageManager, app.packageName)
|
|
||||||
} catch (e: PackageManager.NameNotFoundException) {
|
|
||||||
showRequestError(app, e, "Could not find package info")
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
if (signatures.isEmpty()) {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
val x509cert = try {
|
|
||||||
val cf = CertificateFactory.getInstance("X509")
|
|
||||||
cf.generateCertificate(signatures[0].toByteArray().inputStream()) as X509Certificate
|
|
||||||
} catch (e: CertificateException) {
|
|
||||||
showRequestError(app, e, "Certificate error")
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
return try {
|
|
||||||
val md = MessageDigest.getInstance("SHA1")
|
|
||||||
val publicKey = md.digest(x509cert.encoded)
|
|
||||||
byte2HexFormatted(publicKey)
|
|
||||||
} catch (e: NoSuchAlgorithmException) {
|
|
||||||
showRequestError(app, e, "Could not retrieve SHA1 key")
|
|
||||||
""
|
|
||||||
} catch (e: CertificateEncodingException) {
|
|
||||||
showRequestError(app, e, "Could not retrieve SHA1 key")
|
|
||||||
""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun byte2HexFormatted(arr: ByteArray): String {
|
|
||||||
val str = StringBuilder(arr.size * 2)
|
|
||||||
for (i in arr.indices) {
|
|
||||||
var h = Integer.toHexString(arr[i].toInt())
|
|
||||||
val l = h.length
|
|
||||||
if (l == 1) {
|
|
||||||
h = "0$h"
|
|
||||||
}
|
|
||||||
if (l > 2) {
|
|
||||||
h = h.substring(l - 2, l)
|
|
||||||
}
|
|
||||||
str.append(h.uppercase())
|
|
||||||
if (i < arr.size - 1) {
|
|
||||||
str.append(':')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return str.toString()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun showRequestError(app: App, e: Exception, request: String) {
|
|
||||||
createNotification(
|
|
||||||
app, ErrorInfo(e, UserAction.CHECK_FOR_NEW_APP_VERSION, request)
|
|
||||||
)
|
)
|
||||||
|
val app = App.getApp()
|
||||||
|
try {
|
||||||
|
PackageInfoCompat.hasSignatures(app.packageManager, app.packageName, certificates, false)
|
||||||
|
} catch (e: PackageManager.NameNotFoundException) {
|
||||||
|
createNotification(
|
||||||
|
app, ErrorInfo(e, UserAction.CHECK_FOR_NEW_APP_VERSION, "Could not find package info")
|
||||||
|
)
|
||||||
|
false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun isLastUpdateCheckExpired(expiry: Long): Boolean {
|
fun isLastUpdateCheckExpired(expiry: Long): Boolean {
|
||||||
return Instant.ofEpochSecond(expiry).isBefore(Instant.now())
|
return Instant.ofEpochSecond(expiry) < Instant.now()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -11,7 +11,6 @@ import org.schabi.newpipe.extractor.stream.Stream;
|
||||||
import org.schabi.newpipe.extractor.stream.VideoStream;
|
import org.schabi.newpipe.extractor.stream.VideoStream;
|
||||||
import org.schabi.newpipe.util.StreamItemAdapter.StreamInfoWrapper;
|
import org.schabi.newpipe.util.StreamItemAdapter.StreamInfoWrapper;
|
||||||
|
|
||||||
import java.util.Comparator;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
public class SecondaryStreamHelper<T extends Stream> {
|
public class SecondaryStreamHelper<T extends Stream> {
|
||||||
|
@ -43,42 +42,27 @@ public class SecondaryStreamHelper<T extends Stream> {
|
||||||
@NonNull final List<AudioStream> audioStreams,
|
@NonNull final List<AudioStream> audioStreams,
|
||||||
@NonNull final VideoStream videoStream) {
|
@NonNull final VideoStream videoStream) {
|
||||||
final MediaFormat mediaFormat = videoStream.getFormat();
|
final MediaFormat mediaFormat = videoStream.getFormat();
|
||||||
if (mediaFormat == null) {
|
|
||||||
|
if (mediaFormat == MediaFormat.WEBM) {
|
||||||
|
return audioStreams
|
||||||
|
.stream()
|
||||||
|
.filter(audioStream -> audioStream.getFormat() == MediaFormat.WEBMA
|
||||||
|
|| audioStream.getFormat() == MediaFormat.WEBMA_OPUS)
|
||||||
|
.max(ListHelper.getAudioFormatComparator(MediaFormat.WEBMA,
|
||||||
|
ListHelper.isLimitingDataUsage(context)))
|
||||||
|
.orElse(null);
|
||||||
|
|
||||||
|
} else if (mediaFormat == MediaFormat.MPEG_4) {
|
||||||
|
return audioStreams
|
||||||
|
.stream()
|
||||||
|
.filter(audioStream -> audioStream.getFormat() == MediaFormat.M4A)
|
||||||
|
.max(ListHelper.getAudioFormatComparator(MediaFormat.M4A,
|
||||||
|
ListHelper.isLimitingDataUsage(context)))
|
||||||
|
.orElse(null);
|
||||||
|
|
||||||
|
} else {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (mediaFormat) {
|
|
||||||
case WEBM:
|
|
||||||
case MPEG_4: // Is MPEG-4 DASH?
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
final boolean m4v = mediaFormat == MediaFormat.MPEG_4;
|
|
||||||
final boolean isLimitingDataUsage = ListHelper.isLimitingDataUsage(context);
|
|
||||||
|
|
||||||
Comparator<AudioStream> comparator = ListHelper.getAudioFormatComparator(
|
|
||||||
m4v ? MediaFormat.M4A : MediaFormat.WEBMA, isLimitingDataUsage);
|
|
||||||
int preferredAudioStreamIndex = ListHelper.getAudioIndexByHighestRank(
|
|
||||||
audioStreams, comparator);
|
|
||||||
|
|
||||||
if (preferredAudioStreamIndex == -1) {
|
|
||||||
if (m4v) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
comparator = ListHelper.getAudioFormatComparator(
|
|
||||||
MediaFormat.WEBMA_OPUS, isLimitingDataUsage);
|
|
||||||
preferredAudioStreamIndex = ListHelper.getAudioIndexByHighestRank(
|
|
||||||
audioStreams, comparator);
|
|
||||||
|
|
||||||
if (preferredAudioStreamIndex == -1) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return audioStreams.get(preferredAudioStreamIndex);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public T getStream() {
|
public T getStream() {
|
||||||
|
|
|
@ -144,6 +144,19 @@ public final class ServiceHelper {
|
||||||
.orElse("<unknown>");
|
.orElse("<unknown>");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param serviceId the id of the service
|
||||||
|
* @return the service corresponding to the provided id
|
||||||
|
* @throws java.util.NoSuchElementException if there is no service with the provided id
|
||||||
|
*/
|
||||||
|
@NonNull
|
||||||
|
public static StreamingService getServiceById(final int serviceId) {
|
||||||
|
return ServiceList.all().stream()
|
||||||
|
.filter(s -> s.getServiceId() == serviceId)
|
||||||
|
.findFirst()
|
||||||
|
.orElseThrow();
|
||||||
|
}
|
||||||
|
|
||||||
public static void setSelectedServiceId(final Context context, final int serviceId) {
|
public static void setSelectedServiceId(final Context context, final int serviceId) {
|
||||||
String serviceName;
|
String serviceName;
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -27,6 +27,7 @@ import android.util.Log;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.core.os.BundleCompat;
|
||||||
|
|
||||||
import org.schabi.newpipe.BuildConfig;
|
import org.schabi.newpipe.BuildConfig;
|
||||||
import org.schabi.newpipe.MainActivity;
|
import org.schabi.newpipe.MainActivity;
|
||||||
|
@ -82,7 +83,8 @@ public final class StateSaver {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
final SavedState savedState = outState.getParcelable(KEY_SAVED_STATE);
|
final SavedState savedState = BundleCompat.getParcelable(
|
||||||
|
outState, KEY_SAVED_STATE, SavedState.class);
|
||||||
if (savedState == null) {
|
if (savedState == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue