mirror of
https://github.com/TeamNewPipe/NewPipe.git
synced 2024-11-24 20:15:16 +01:00
Merge branch 'dev' into pr9236
This commit is contained in:
commit
37f7fa7ef4
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:
|
||||||
|
17
.github/changed-lines-count-labeler.yml
vendored
Normal file
17
.github/changed-lines-count-labeler.yml
vendored
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
# Add 'size/small' label to any changes with less than 50 lines
|
||||||
|
size/small:
|
||||||
|
max: 49
|
||||||
|
|
||||||
|
# Add 'size/medium' label to any changes between 50 and 249 lines
|
||||||
|
size/medium:
|
||||||
|
min: 50
|
||||||
|
max: 249
|
||||||
|
|
||||||
|
# Add 'size/large' label to any changes between 250 and 749 lines
|
||||||
|
size/large:
|
||||||
|
min: 250
|
||||||
|
max: 749
|
||||||
|
|
||||||
|
# Add 'size/giant' label to any changes for more than 749 lines
|
||||||
|
size/giant:
|
||||||
|
min: 750
|
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: |
|
||||||
|
18
.github/workflows/pr-labeler.yml
vendored
Normal file
18
.github/workflows/pr-labeler.yml
vendored
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
name: "PR size labeler"
|
||||||
|
on: [pull_request_target]
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
pull-requests: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
changed-lines-count-labeler:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
name: Automatically labelling pull requests based on the changed lines count
|
||||||
|
permissions:
|
||||||
|
pull-requests: write
|
||||||
|
steps:
|
||||||
|
- name: Set a label
|
||||||
|
uses: TeamNewPipe/changed-lines-count-labeler@main
|
||||||
|
with:
|
||||||
|
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
configuration-path: .github/changed-lines-count-labeler.yml
|
21
README.md
21
README.md
@ -13,18 +13,19 @@
|
|||||||
<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://www.bountysource.com/teams/newpipe" alt="Bountysource bounties"><img src="https://img.shields.io/bountysource/team/newpipe/activity.svg?colorB=cd201f"></a>
|
<a href="https://matrix.to/#/#newpipe: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>
|
||||||
<p align="center"><a href="https://newpipe.net">Website</a> • <a href="https://newpipe.net/blog/">Blog</a> • <a href="https://newpipe.net/FAQ/">FAQ</a> • <a href="https://newpipe.net/press/">Press</a></p>
|
<p align="center"><a href="https://newpipe.net">Website</a> • <a href="https://newpipe.net/blog/">Blog</a> • <a href="https://newpipe.net/FAQ/">FAQ</a> • <a href="https://newpipe.net/press/">Press</a></p>
|
||||||
<hr>
|
<hr>
|
||||||
|
|
||||||
*Read this document in other languages: [Deutsch](doc/README.de.md), [English](README.md), [Español](doc/README.es.md), [Français](doc/README.fr.md), [हिन्दी](doc/README.hi.md), [Italiano](doc/README.it.md), [한국어](doc/README.ko.md), [Português Brasil](doc/README.pt_BR.md), [Polski](doc/README.pl.md), [ਪੰਜਾਬੀ ](doc/README.pa.md), [日本語](doc/README.ja.md), [Română](doc/README.ro.md), [Soomaali](doc/README.so.md), [Türkçe](doc/README.tr.md), [正體中文](doc/README.zh_TW.md), [অসমীয়া](doc/README.asm.md)*
|
*Read this document in other languages: [Deutsch](doc/README.de.md), [English](README.md), [Español](doc/README.es.md), [Français](doc/README.fr.md), [हिन्दी](doc/README.hi.md), [Italiano](doc/README.it.md), [한국어](doc/README.ko.md), [Português Brasil](doc/README.pt_BR.md), [Polski](doc/README.pl.md), [ਪੰਜਾਬੀ ](doc/README.pa.md), [日本語](doc/README.ja.md), [Română](doc/README.ro.md), [Soomaali](doc/README.so.md), [Türkçe](doc/README.tr.md), [正體中文](doc/README.zh_TW.md), [অসমীয়া](doc/README.asm.md), [Српски](doc/README.sr.md)*
|
||||||
|
|
||||||
<b>WARNING: THIS APP IS IN BETA, SO YOU MAY ENCOUNTER BUGS. IF YOU DO, OPEN AN ISSUE IN OUR GITHUB REPOSITORY BY FILLING OUT THE ISSUE TEMPLATE.</b>
|
> [!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
|
||||||
|
|
||||||
@ -126,16 +127,6 @@ If you like NewPipe, you're welcome to send a donation. We prefer Liberapay, as
|
|||||||
<td><a href="https://liberapay.com/TeamNewPipe/"><img src="assets/liberapay_qr_code.png" alt="Visit NewPipe at liberapay.com" width="100px"></a></td>
|
<td><a href="https://liberapay.com/TeamNewPipe/"><img src="assets/liberapay_qr_code.png" alt="Visit NewPipe at liberapay.com" width="100px"></a></td>
|
||||||
<td><a href="https://liberapay.com/TeamNewPipe/donate"><img src="assets/liberapay_donate_button.svg" alt="Donate via Liberapay" height="35px"></a></td>
|
<td><a href="https://liberapay.com/TeamNewPipe/donate"><img src="assets/liberapay_donate_button.svg" alt="Donate via Liberapay" height="35px"></a></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
|
||||||
<td><img src="https://bitcoin.org/img/icons/logotop.svg" alt="Bitcoin"></td>
|
|
||||||
<td><img src="assets/bitcoin_qr_code.png" alt="Bitcoin QR code" width="100px"></td>
|
|
||||||
<td><samp>16A9J59ahMRqkLSZjhYj33n9j3fMztFxnh</samp></td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td><a href="https://www.bountysource.com/teams/newpipe"><img src="https://upload.wikimedia.org/wikipedia/commons/thumb/2/22/Bountysource.png/320px-Bountysource.png" alt="Bountysource" width="190px"></a></td>
|
|
||||||
<td><a href="https://www.bountysource.com/teams/newpipe"><img src="assets/bountysource_qr_code.png" alt="Visit NewPipe at bountysource.com" width="100px"></a></td>
|
|
||||||
<td><a href="https://www.bountysource.com/teams/newpipe/issues"><img src="https://img.shields.io/bountysource/team/newpipe/activity.svg?colorB=cd201f" height="30px" alt="Check out how many bounties you can earn."></a></td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
## Privacy Policy
|
## Privacy Policy
|
||||||
|
@ -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 994
|
versionCode 996
|
||||||
versionName "0.25.2"
|
versionName "0.26.1"
|
||||||
|
|
||||||
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:95a3cc0a173bba28c179f9f9503b1010ec6bff21'
|
implementation 'com.github.Stypox:NewPipeExtractor:aaf3231fc75d7b4177549fec4aa7e672bfe84015'
|
||||||
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')"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
@ -8,10 +8,12 @@ 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.extractor.ServiceList
|
||||||
import org.schabi.newpipe.extractor.stream.StreamType
|
import org.schabi.newpipe.extractor.stream.StreamType
|
||||||
|
|
||||||
@RunWith(AndroidJUnit4::class)
|
@RunWith(AndroidJUnit4::class)
|
||||||
@ -106,6 +108,13 @@ class DatabaseMigrationTest {
|
|||||||
Migrations.MIGRATION_6_7
|
Migrations.MIGRATION_6_7
|
||||||
)
|
)
|
||||||
|
|
||||||
|
testHelper.runMigrationsAndValidate(
|
||||||
|
AppDatabase.DATABASE_NAME,
|
||||||
|
Migrations.DB_VER_8,
|
||||||
|
true,
|
||||||
|
Migrations.MIGRATION_7_8
|
||||||
|
)
|
||||||
|
|
||||||
val migratedDatabaseV3 = getMigratedDatabase()
|
val migratedDatabaseV3 = getMigratedDatabase()
|
||||||
val listFromDB = migratedDatabaseV3.streamDAO().all.blockingFirst()
|
val listFromDB = migratedDatabaseV3.streamDAO().all.blockingFirst()
|
||||||
|
|
||||||
@ -140,6 +149,64 @@ 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
|
||||||
|
)
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
private fun getMigratedDatabase(): AppDatabase {
|
private fun getMigratedDatabase(): AppDatabase {
|
||||||
val database: AppDatabase = Room.databaseBuilder(
|
val database: AppDatabase = Room.databaseBuilder(
|
||||||
ApplicationProvider.getApplicationContext(),
|
ApplicationProvider.getApplicationContext(),
|
||||||
|
@ -0,0 +1,130 @@
|
|||||||
|
package org.schabi.newpipe.database
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.room.Room
|
||||||
|
import androidx.test.core.app.ApplicationProvider
|
||||||
|
import io.reactivex.rxjava3.core.Single
|
||||||
|
import org.junit.After
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Assert.assertNotNull
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Test
|
||||||
|
import org.schabi.newpipe.database.feed.dao.FeedDAO
|
||||||
|
import org.schabi.newpipe.database.feed.model.FeedEntity
|
||||||
|
import org.schabi.newpipe.database.feed.model.FeedGroupEntity
|
||||||
|
import org.schabi.newpipe.database.stream.StreamWithState
|
||||||
|
import org.schabi.newpipe.database.stream.dao.StreamDAO
|
||||||
|
import org.schabi.newpipe.database.stream.model.StreamEntity
|
||||||
|
import org.schabi.newpipe.database.subscription.SubscriptionDAO
|
||||||
|
import org.schabi.newpipe.database.subscription.SubscriptionEntity
|
||||||
|
import org.schabi.newpipe.extractor.ServiceList
|
||||||
|
import org.schabi.newpipe.extractor.channel.ChannelInfo
|
||||||
|
import org.schabi.newpipe.extractor.stream.StreamType
|
||||||
|
import java.io.IOException
|
||||||
|
import java.time.OffsetDateTime
|
||||||
|
import kotlin.streams.toList
|
||||||
|
|
||||||
|
class FeedDAOTest {
|
||||||
|
private lateinit var db: AppDatabase
|
||||||
|
private lateinit var feedDAO: FeedDAO
|
||||||
|
private lateinit var streamDAO: StreamDAO
|
||||||
|
private lateinit var subscriptionDAO: SubscriptionDAO
|
||||||
|
|
||||||
|
private val serviceId = ServiceList.YouTube.serviceId
|
||||||
|
|
||||||
|
private val stream1 = StreamEntity(1, serviceId, "https://youtube.com/watch?v=1", "stream 1", StreamType.VIDEO_STREAM, 1000, "channel-1", "https://youtube.com/channel/1", "https://i.ytimg.com/vi/1/hqdefault.jpg", 100, "2023-01-01", OffsetDateTime.parse("2023-01-01T00:00:00Z"))
|
||||||
|
private val stream2 = StreamEntity(2, serviceId, "https://youtube.com/watch?v=2", "stream 2", StreamType.VIDEO_STREAM, 1000, "channel-1", "https://youtube.com/channel/1", "https://i.ytimg.com/vi/1/hqdefault.jpg", 100, "2023-01-02", OffsetDateTime.parse("2023-01-02T00:00:00Z"))
|
||||||
|
private val stream3 = StreamEntity(3, serviceId, "https://youtube.com/watch?v=3", "stream 3", StreamType.LIVE_STREAM, 1000, "channel-1", "https://youtube.com/channel/1", "https://i.ytimg.com/vi/1/hqdefault.jpg", 100, "2023-01-03", OffsetDateTime.parse("2023-01-03T00:00:00Z"))
|
||||||
|
private val stream4 = StreamEntity(4, serviceId, "https://youtube.com/watch?v=4", "stream 4", StreamType.VIDEO_STREAM, 1000, "channel-2", "https://youtube.com/channel/2", "https://i.ytimg.com/vi/1/hqdefault.jpg", 100, "2023-08-10", OffsetDateTime.parse("2023-08-10T00:00:00Z"))
|
||||||
|
private val stream5 = StreamEntity(5, serviceId, "https://youtube.com/watch?v=5", "stream 5", StreamType.VIDEO_STREAM, 1000, "channel-2", "https://youtube.com/channel/2", "https://i.ytimg.com/vi/1/hqdefault.jpg", 100, "2023-08-20", OffsetDateTime.parse("2023-08-20T00:00:00Z"))
|
||||||
|
private val stream6 = StreamEntity(6, serviceId, "https://youtube.com/watch?v=6", "stream 6", StreamType.VIDEO_STREAM, 1000, "channel-3", "https://youtube.com/channel/3", "https://i.ytimg.com/vi/1/hqdefault.jpg", 100, "2023-09-01", OffsetDateTime.parse("2023-09-01T00:00:00Z"))
|
||||||
|
private val stream7 = StreamEntity(7, serviceId, "https://youtube.com/watch?v=7", "stream 7", StreamType.VIDEO_STREAM, 1000, "channel-4", "https://youtube.com/channel/4", "https://i.ytimg.com/vi/1/hqdefault.jpg", 100, "2023-08-10", OffsetDateTime.parse("2023-08-10T00:00:00Z"))
|
||||||
|
|
||||||
|
private val allStreams = listOf(
|
||||||
|
stream1, stream2, stream3, stream4, stream5, stream6, stream7
|
||||||
|
)
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun createDb() {
|
||||||
|
val context = ApplicationProvider.getApplicationContext<Context>()
|
||||||
|
db = Room.inMemoryDatabaseBuilder(
|
||||||
|
context, AppDatabase::class.java
|
||||||
|
).build()
|
||||||
|
feedDAO = db.feedDAO()
|
||||||
|
streamDAO = db.streamDAO()
|
||||||
|
subscriptionDAO = db.subscriptionDAO()
|
||||||
|
}
|
||||||
|
|
||||||
|
@After
|
||||||
|
@Throws(IOException::class)
|
||||||
|
fun closeDb() {
|
||||||
|
db.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testUnlinkStreamsOlderThan_KeepOne() {
|
||||||
|
setupUnlinkDelete("2023-08-15T00:00:00Z")
|
||||||
|
val streams = feedDAO.getStreams(
|
||||||
|
FeedGroupEntity.GROUP_ALL_ID, includePlayed = true, includePartiallyPlayed = true, null
|
||||||
|
)
|
||||||
|
.blockingGet()
|
||||||
|
val allowedStreams = listOf(stream3, stream5, stream6, stream7)
|
||||||
|
assertEqual(streams, allowedStreams)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testUnlinkStreamsOlderThan_KeepMultiple() {
|
||||||
|
setupUnlinkDelete("2023-08-01T00:00:00Z")
|
||||||
|
val streams = feedDAO.getStreams(
|
||||||
|
FeedGroupEntity.GROUP_ALL_ID, includePlayed = true, includePartiallyPlayed = true, null
|
||||||
|
)
|
||||||
|
.blockingGet()
|
||||||
|
val allowedStreams = listOf(stream3, stream4, stream5, stream6, stream7)
|
||||||
|
assertEqual(streams, allowedStreams)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun assertEqual(streams: List<StreamWithState>?, allowedStreams: List<StreamEntity>) {
|
||||||
|
assertNotNull(streams)
|
||||||
|
assertEquals(
|
||||||
|
allowedStreams,
|
||||||
|
streams!!
|
||||||
|
.map { it.stream }
|
||||||
|
.sortedBy { it.uid }
|
||||||
|
.toList()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setupUnlinkDelete(time: String) {
|
||||||
|
clearAndFillTables()
|
||||||
|
Single.fromCallable {
|
||||||
|
feedDAO.unlinkStreamsOlderThan(OffsetDateTime.parse(time))
|
||||||
|
}.blockingSubscribe()
|
||||||
|
Single.fromCallable {
|
||||||
|
streamDAO.deleteOrphans()
|
||||||
|
}.blockingSubscribe()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun clearAndFillTables() {
|
||||||
|
db.clearAllTables()
|
||||||
|
streamDAO.insertAll(allStreams)
|
||||||
|
subscriptionDAO.insertAll(
|
||||||
|
listOf(
|
||||||
|
SubscriptionEntity.from(ChannelInfo(serviceId, "1", "https://youtube.com/channel/1", "https://youtube.com/channel/1", "channel-1")),
|
||||||
|
SubscriptionEntity.from(ChannelInfo(serviceId, "2", "https://youtube.com/channel/2", "https://youtube.com/channel/2", "channel-2")),
|
||||||
|
SubscriptionEntity.from(ChannelInfo(serviceId, "3", "https://youtube.com/channel/3", "https://youtube.com/channel/3", "channel-3")),
|
||||||
|
SubscriptionEntity.from(ChannelInfo(serviceId, "4", "https://youtube.com/channel/4", "https://youtube.com/channel/4", "channel-4")),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
feedDAO.insertAll(
|
||||||
|
listOf(
|
||||||
|
FeedEntity(1, 1),
|
||||||
|
FeedEntity(2, 1),
|
||||||
|
FeedEntity(3, 1),
|
||||||
|
FeedEntity(4, 2),
|
||||||
|
FeedEntity(5, 2),
|
||||||
|
FeedEntity(6, 3),
|
||||||
|
FeedEntity(7, 4),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
@ -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) {
|
||||||
|
@ -20,9 +20,11 @@ import org.schabi.newpipe.extractor.downloader.Downloader;
|
|||||||
import org.schabi.newpipe.ktx.ExceptionUtils;
|
import org.schabi.newpipe.ktx.ExceptionUtils;
|
||||||
import org.schabi.newpipe.settings.NewPipeSettings;
|
import org.schabi.newpipe.settings.NewPipeSettings;
|
||||||
import org.schabi.newpipe.util.Localization;
|
import org.schabi.newpipe.util.Localization;
|
||||||
import org.schabi.newpipe.util.PicassoHelper;
|
import org.schabi.newpipe.util.image.ImageStrategy;
|
||||||
|
import org.schabi.newpipe.util.image.PicassoHelper;
|
||||||
import org.schabi.newpipe.util.ServiceHelper;
|
import org.schabi.newpipe.util.ServiceHelper;
|
||||||
import org.schabi.newpipe.util.StateSaver;
|
import org.schabi.newpipe.util.StateSaver;
|
||||||
|
import org.schabi.newpipe.util.image.PreferredImageQuality;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InterruptedIOException;
|
import java.io.InterruptedIOException;
|
||||||
@ -99,8 +101,9 @@ public class App extends Application {
|
|||||||
// Initialize image loader
|
// Initialize image loader
|
||||||
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
|
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
|
||||||
PicassoHelper.init(this);
|
PicassoHelper.init(this);
|
||||||
PicassoHelper.setShouldLoadImages(
|
ImageStrategy.setPreferredImageQuality(PreferredImageQuality.fromPreferenceKey(this,
|
||||||
prefs.getBoolean(getString(R.string.download_thumbnail_key), true));
|
prefs.getString(getString(R.string.image_quality_key),
|
||||||
|
getString(R.string.image_quality_default))));
|
||||||
PicassoHelper.setIndicatorsEnabled(MainActivity.DEBUG
|
PicassoHelper.setIndicatorsEnabled(MainActivity.DEBUG
|
||||||
&& prefs.getBoolean(getString(R.string.show_image_indicators_key), false));
|
&& prefs.getBoolean(getString(R.string.show_image_indicators_key), false));
|
||||||
|
|
||||||
|
@ -120,9 +120,20 @@ public abstract class BaseFragment extends Fragment {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds the root fragment by looping through all of the parent fragments. The root fragment
|
||||||
|
* is supposed to be {@link org.schabi.newpipe.fragments.MainFragment}, and is the fragment that
|
||||||
|
* handles keeping the backstack of opened fragments in NewPipe, and also the player bottom
|
||||||
|
* sheet. This function therefore returns the fragment manager of said fragment.
|
||||||
|
*
|
||||||
|
* @return the fragment manager of the root fragment, i.e.
|
||||||
|
* {@link org.schabi.newpipe.fragments.MainFragment}
|
||||||
|
*/
|
||||||
protected FragmentManager getFM() {
|
protected FragmentManager getFM() {
|
||||||
return getParentFragment() == null
|
Fragment current = this;
|
||||||
? getFragmentManager()
|
while (current.getParentFragment() != null) {
|
||||||
: getParentFragment().getFragmentManager();
|
current = current.getParentFragment();
|
||||||
|
}
|
||||||
|
return current.getFragmentManager();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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,11 +66,13 @@ 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;
|
||||||
@ -546,14 +550,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 +640,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 +846,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,7 @@ 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 android.content.Context;
|
import android.content.Context;
|
||||||
import android.database.Cursor;
|
import android.database.Cursor;
|
||||||
@ -27,7 +28,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)
|
||||||
.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)
|
||||||
}
|
}
|
||||||
@ -120,13 +118,13 @@ class NewVersionWorker(
|
|||||||
|
|
||||||
// Parse the json from the response.
|
// Parse the json from the response.
|
||||||
try {
|
try {
|
||||||
val githubStableObject = JsonParser.`object`()
|
val newpipeVersionInfo = JsonParser.`object`()
|
||||||
.from(response.responseBody()).getObject("flavors")
|
.from(response.responseBody()).getObject("flavors")
|
||||||
.getObject("github").getObject("stable")
|
.getObject("newpipe")
|
||||||
|
|
||||||
val versionName = githubStableObject.getString("version")
|
val versionName = newpipeVersionInfo.getString("version")
|
||||||
val versionCode = githubStableObject.getInt("version_code")
|
val versionCode = newpipeVersionInfo.getInt("version_code")
|
||||||
val apkLocationUrl = githubStableObject.getString("apk")
|
val apkLocationUrl = newpipeVersionInfo.getString("apk")
|
||||||
compareAppVersionAndShowNotification(versionName, apkLocationUrl, versionCode)
|
compareAppVersionAndShowNotification(versionName, apkLocationUrl, versionCode)
|
||||||
} catch (e: JsonParserException) {
|
} catch (e: JsonParserException) {
|
||||||
// Most likely something is wrong in data received from NEWPIPE_API_URL.
|
// Most likely something is wrong in data received from NEWPIPE_API_URL.
|
||||||
|
@ -75,7 +75,7 @@ public final class QueueItemMenuUtil {
|
|||||||
return true;
|
return true;
|
||||||
case R.id.menu_item_share:
|
case R.id.menu_item_share:
|
||||||
shareText(context, item.getTitle(), item.getUrl(),
|
shareText(context, item.getTitle(), item.getUrl(),
|
||||||
item.getThumbnailUrl());
|
item.getThumbnails());
|
||||||
return true;
|
return true;
|
||||||
case R.id.menu_item_download:
|
case R.id.menu_item_download:
|
||||||
fetchStreamInfoAndSaveToDatabase(context, item.getServiceId(), item.getUrl(),
|
fetchStreamInfoAndSaveToDatabase(context, item.getServiceId(), item.getUrl(),
|
||||||
|
@ -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
|
||||||
|
@ -1,30 +1,40 @@
|
|||||||
package org.schabi.newpipe.about
|
package org.schabi.newpipe.about
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.util.Base64
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
import android.webkit.WebView
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.core.os.bundleOf
|
import androidx.core.os.bundleOf
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
|
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||||
|
import io.reactivex.rxjava3.core.Observable
|
||||||
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||||
|
import io.reactivex.rxjava3.disposables.Disposable
|
||||||
|
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||||
|
import org.schabi.newpipe.BuildConfig
|
||||||
import org.schabi.newpipe.R
|
import org.schabi.newpipe.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.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 activeLicense: License? = 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)!!
|
||||||
activeLicense = savedInstanceState?.getSerializable(LICENSE_KEY) as? License
|
.sortedBy { it.name } // Sort components by name
|
||||||
// Sort components by name
|
activeSoftwareComponent = savedInstanceState?.getSerializable(SOFTWARE_COMPONENT_KEY) as? SoftwareComponent
|
||||||
softwareComponents.sortBy { it.name }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
@ -39,9 +49,8 @@ class LicenseFragment : Fragment() {
|
|||||||
): View {
|
): View {
|
||||||
val binding = FragmentLicensesBinding.inflate(inflater, container, false)
|
val binding = FragmentLicensesBinding.inflate(inflater, container, false)
|
||||||
binding.licensesAppReadLicense.setOnClickListener {
|
binding.licensesAppReadLicense.setOnClickListener {
|
||||||
activeLicense = StandardLicenses.GPL3
|
|
||||||
compositeDisposable.add(
|
compositeDisposable.add(
|
||||||
showLicense(activity, StandardLicenses.GPL3)
|
showLicense(NEWPIPE_SOFTWARE_COMPONENT)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
for (component in softwareComponents) {
|
for (component in softwareComponents) {
|
||||||
@ -57,27 +66,72 @@ class LicenseFragment : Fragment() {
|
|||||||
val root: View = componentBinding.root
|
val root: View = componentBinding.root
|
||||||
root.tag = component
|
root.tag = component
|
||||||
root.setOnClickListener {
|
root.setOnClickListener {
|
||||||
activeLicense = component.license
|
|
||||||
compositeDisposable.add(
|
compositeDisposable.add(
|
||||||
showLicense(activity, component)
|
showLicense(component)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
binding.licensesSoftwareComponents.addView(root)
|
binding.licensesSoftwareComponents.addView(root)
|
||||||
registerForContextMenu(root)
|
registerForContextMenu(root)
|
||||||
}
|
}
|
||||||
activeLicense?.let { compositeDisposable.add(showLicense(activity, it)) }
|
activeSoftwareComponent?.let { compositeDisposable.add(showLicense(it)) }
|
||||||
return binding.root
|
return binding.root
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onSaveInstanceState(savedInstanceState: Bundle) {
|
override fun onSaveInstanceState(savedInstanceState: Bundle) {
|
||||||
super.onSaveInstanceState(savedInstanceState)
|
super.onSaveInstanceState(savedInstanceState)
|
||||||
activeLicense?.let { savedInstanceState.putSerializable(LICENSE_KEY, it) }
|
activeSoftwareComponent?.let { savedInstanceState.putSerializable(SOFTWARE_COMPONENT_KEY, it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showLicense(
|
||||||
|
softwareComponent: SoftwareComponent
|
||||||
|
): Disposable {
|
||||||
|
return if (context == null) {
|
||||||
|
Disposable.empty()
|
||||||
|
} else {
|
||||||
|
val context = requireContext()
|
||||||
|
activeSoftwareComponent = softwareComponent
|
||||||
|
Observable.fromCallable { getFormattedLicense(context, softwareComponent.license) }
|
||||||
|
.subscribeOn(Schedulers.io())
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribe { formattedLicense ->
|
||||||
|
val webViewData = Base64.encodeToString(
|
||||||
|
formattedLicense.toByteArray(), Base64.NO_PADDING
|
||||||
|
)
|
||||||
|
val webView = WebView(context)
|
||||||
|
webView.loadData(webViewData, "text/html; charset=UTF-8", "base64")
|
||||||
|
|
||||||
|
Localization.assureCorrectAppLanguage(context)
|
||||||
|
val builder = AlertDialog.Builder(requireContext())
|
||||||
|
.setTitle(softwareComponent.name)
|
||||||
|
.setView(webView)
|
||||||
|
.setOnCancelListener { activeSoftwareComponent = null }
|
||||||
|
.setOnDismissListener { activeSoftwareComponent = null }
|
||||||
|
.setPositiveButton(R.string.done) { dialog, _ -> dialog.dismiss() }
|
||||||
|
|
||||||
|
if (softwareComponent != NEWPIPE_SOFTWARE_COMPONENT) {
|
||||||
|
builder.setNeutralButton(R.string.open_website_license) { _, _ ->
|
||||||
|
ShareUtils.openUrlInApp(requireContext(), softwareComponent.link)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.show()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val ARG_COMPONENTS = "components"
|
private const val ARG_COMPONENTS = "components"
|
||||||
private const val LICENSE_KEY = "ACTIVE_LICENSE"
|
private const val SOFTWARE_COMPONENT_KEY = "ACTIVE_SOFTWARE_COMPONENT"
|
||||||
fun newInstance(softwareComponents: Array<SoftwareComponent>): LicenseFragment {
|
private val NEWPIPE_SOFTWARE_COMPONENT = SoftwareComponent(
|
||||||
|
"NewPipe",
|
||||||
|
"2014-2023",
|
||||||
|
"Team NewPipe",
|
||||||
|
"https://newpipe.net/",
|
||||||
|
StandardLicenses.GPL3,
|
||||||
|
BuildConfig.VERSION_NAME
|
||||||
|
)
|
||||||
|
|
||||||
|
fun newInstance(softwareComponents: ArrayList<SoftwareComponent>): LicenseFragment {
|
||||||
val fragment = LicenseFragment()
|
val fragment = LicenseFragment()
|
||||||
fragment.arguments = bundleOf(ARG_COMPONENTS to softwareComponents)
|
fragment.arguments = bundleOf(ARG_COMPONENTS to softwareComponents)
|
||||||
return fragment
|
return fragment
|
||||||
|
@ -1,17 +1,8 @@
|
|||||||
package org.schabi.newpipe.about
|
package org.schabi.newpipe.about
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.util.Base64
|
|
||||||
import android.webkit.WebView
|
|
||||||
import androidx.appcompat.app.AlertDialog
|
|
||||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
|
||||||
import io.reactivex.rxjava3.core.Observable
|
|
||||||
import io.reactivex.rxjava3.disposables.Disposable
|
|
||||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
|
||||||
import org.schabi.newpipe.R
|
import org.schabi.newpipe.R
|
||||||
import org.schabi.newpipe.util.Localization
|
|
||||||
import org.schabi.newpipe.util.ThemeHelper
|
import org.schabi.newpipe.util.ThemeHelper
|
||||||
import org.schabi.newpipe.util.external_communication.ShareUtils
|
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -20,7 +11,7 @@ import java.io.IOException
|
|||||||
* @return String which contains a HTML formatted license page
|
* @return String which contains a HTML formatted license page
|
||||||
* styled according to the context's theme
|
* styled according to the context's theme
|
||||||
*/
|
*/
|
||||||
private fun getFormattedLicense(context: Context, license: License): String {
|
fun getFormattedLicense(context: Context, license: License): String {
|
||||||
try {
|
try {
|
||||||
return context.assets.open(license.filename).bufferedReader().use { it.readText() }
|
return context.assets.open(license.filename).bufferedReader().use { it.readText() }
|
||||||
// split the HTML file and insert the stylesheet into the HEAD of the file
|
// split the HTML file and insert the stylesheet into the HEAD of the file
|
||||||
@ -34,7 +25,7 @@ private fun getFormattedLicense(context: Context, license: License): String {
|
|||||||
* @param context the Android context
|
* @param context the Android context
|
||||||
* @return String which is a CSS stylesheet according to the context's theme
|
* @return String which is a CSS stylesheet according to the context's theme
|
||||||
*/
|
*/
|
||||||
private fun getLicenseStylesheet(context: Context): String {
|
fun getLicenseStylesheet(context: Context): String {
|
||||||
val isLightTheme = ThemeHelper.isLightThemeSelected(context)
|
val isLightTheme = ThemeHelper.isLightThemeSelected(context)
|
||||||
val licenseBackgroundColor = getHexRGBColor(
|
val licenseBackgroundColor = getHexRGBColor(
|
||||||
context, if (isLightTheme) R.color.light_license_background_color else R.color.dark_license_background_color
|
context, if (isLightTheme) R.color.light_license_background_color else R.color.dark_license_background_color
|
||||||
@ -56,48 +47,6 @@ private fun getLicenseStylesheet(context: Context): String {
|
|||||||
* @param color the color number from R.color
|
* @param color the color number from R.color
|
||||||
* @return a six characters long String with hexadecimal RGB values
|
* @return a six characters long String with hexadecimal RGB values
|
||||||
*/
|
*/
|
||||||
private fun getHexRGBColor(context: Context, color: Int): String {
|
fun getHexRGBColor(context: Context, color: Int): String {
|
||||||
return context.getString(color).substring(3)
|
return context.getString(color).substring(3)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun showLicense(context: Context?, component: SoftwareComponent): Disposable {
|
|
||||||
return showLicense(context, component.license) {
|
|
||||||
setPositiveButton(R.string.dismiss) { dialog, _ ->
|
|
||||||
dialog.dismiss()
|
|
||||||
}
|
|
||||||
setNeutralButton(R.string.open_website_license) { _, _ ->
|
|
||||||
ShareUtils.openUrlInApp(context!!, component.link)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun showLicense(context: Context?, license: License) = showLicense(context, license) {
|
|
||||||
setPositiveButton(R.string.ok) { dialog, _ -> dialog.dismiss() }
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun showLicense(
|
|
||||||
context: Context?,
|
|
||||||
license: License,
|
|
||||||
block: AlertDialog.Builder.() -> AlertDialog.Builder
|
|
||||||
): Disposable {
|
|
||||||
return if (context == null) {
|
|
||||||
Disposable.empty()
|
|
||||||
} else {
|
|
||||||
Observable.fromCallable { getFormattedLicense(context, license) }
|
|
||||||
.subscribeOn(Schedulers.io())
|
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
|
||||||
.subscribe { formattedLicense ->
|
|
||||||
val webViewData =
|
|
||||||
Base64.encodeToString(formattedLicense.toByteArray(), Base64.NO_PADDING)
|
|
||||||
val webView = WebView(context)
|
|
||||||
webView.loadData(webViewData, "text/html; charset=UTF-8", "base64")
|
|
||||||
|
|
||||||
Localization.assureCorrectAppLanguage(context)
|
|
||||||
AlertDialog.Builder(context)
|
|
||||||
.setTitle(license.name)
|
|
||||||
.setView(webView)
|
|
||||||
.block()
|
|
||||||
.show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -2,6 +2,7 @@ package org.schabi.newpipe.about
|
|||||||
|
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
|
import java.io.Serializable
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
class SoftwareComponent
|
class SoftwareComponent
|
||||||
@ -13,4 +14,4 @@ constructor(
|
|||||||
val link: String,
|
val link: String,
|
||||||
val license: License,
|
val license: License,
|
||||||
val version: String? = null
|
val version: String? = null
|
||||||
) : Parcelable
|
) : Parcelable, Serializable
|
||||||
|
@ -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_8;
|
||||||
|
|
||||||
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_8
|
||||||
)
|
)
|
||||||
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,7 @@ 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;
|
||||||
|
|
||||||
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;
|
||||||
@ -235,6 +236,15 @@ 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)");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
private Migrations() {
|
private Migrations() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -93,18 +93,30 @@ abstract class FeedDAO {
|
|||||||
uploadDateBefore: OffsetDateTime?
|
uploadDateBefore: OffsetDateTime?
|
||||||
): Maybe<List<StreamWithState>>
|
): Maybe<List<StreamWithState>>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove links to streams that are older than the given date
|
||||||
|
* **but keep at least one stream per uploader**.
|
||||||
|
*
|
||||||
|
* One stream per uploader is kept because it is needed as reference
|
||||||
|
* when fetching new streams to check if they are new or not.
|
||||||
|
* @param offsetDateTime the newest date to keep, older streams are removed
|
||||||
|
*/
|
||||||
@Query(
|
@Query(
|
||||||
"""
|
"""
|
||||||
DELETE FROM feed WHERE
|
DELETE FROM feed
|
||||||
|
WHERE feed.stream_id IN (SELECT uid from (
|
||||||
feed.stream_id IN (
|
SELECT s.uid,
|
||||||
SELECT s.uid FROM streams s
|
(SELECT MAX(upload_date)
|
||||||
|
FROM streams s1
|
||||||
INNER JOIN feed f
|
INNER JOIN feed f1
|
||||||
ON s.uid = f.stream_id
|
ON s1.uid = f1.stream_id
|
||||||
|
WHERE f1.subscription_id = f.subscription_id) max_upload_date
|
||||||
WHERE s.upload_date < :offsetDateTime
|
FROM streams s
|
||||||
)
|
INNER JOIN feed f
|
||||||
|
ON s.uid = f.stream_id
|
||||||
|
|
||||||
|
WHERE s.upload_date < :offsetDateTime
|
||||||
|
AND s.upload_date <> max_upload_date))
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
abstract fun unlinkStreamsOlderThan(offsetDateTime: OffsetDateTime)
|
abstract fun unlinkStreamsOlderThan(offsetDateTime: OffsetDateTime)
|
||||||
|
@ -7,6 +7,7 @@ import org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity
|
|||||||
import org.schabi.newpipe.database.stream.model.StreamEntity
|
import org.schabi.newpipe.database.stream.model.StreamEntity
|
||||||
import org.schabi.newpipe.database.stream.model.StreamStateEntity
|
import org.schabi.newpipe.database.stream.model.StreamStateEntity
|
||||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem
|
import org.schabi.newpipe.extractor.stream.StreamInfoItem
|
||||||
|
import org.schabi.newpipe.util.image.ImageStrategy
|
||||||
|
|
||||||
data class PlaylistStreamEntry(
|
data class PlaylistStreamEntry(
|
||||||
@Embedded
|
@Embedded
|
||||||
@ -28,7 +29,7 @@ data class PlaylistStreamEntry(
|
|||||||
item.duration = streamEntity.duration
|
item.duration = streamEntity.duration
|
||||||
item.uploaderName = streamEntity.uploader
|
item.uploaderName = streamEntity.uploader
|
||||||
item.uploaderUrl = streamEntity.uploaderUrl
|
item.uploaderUrl = streamEntity.uploaderUrl
|
||||||
item.thumbnailUrl = streamEntity.thumbnailUrl
|
item.thumbnails = ImageStrategy.dbUrlToImageList(streamEntity.thumbnailUrl)
|
||||||
|
|
||||||
return item
|
return item
|
||||||
}
|
}
|
||||||
|
@ -11,6 +11,7 @@ import androidx.room.PrimaryKey;
|
|||||||
import org.schabi.newpipe.database.playlist.PlaylistLocalItem;
|
import org.schabi.newpipe.database.playlist.PlaylistLocalItem;
|
||||||
import org.schabi.newpipe.extractor.playlist.PlaylistInfo;
|
import org.schabi.newpipe.extractor.playlist.PlaylistInfo;
|
||||||
import org.schabi.newpipe.util.Constants;
|
import org.schabi.newpipe.util.Constants;
|
||||||
|
import org.schabi.newpipe.util.image.ImageStrategy;
|
||||||
|
|
||||||
import static org.schabi.newpipe.database.LocalItem.LocalItemType.PLAYLIST_REMOTE_ITEM;
|
import static org.schabi.newpipe.database.LocalItem.LocalItemType.PLAYLIST_REMOTE_ITEM;
|
||||||
import static org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity.REMOTE_PLAYLIST_NAME;
|
import static org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity.REMOTE_PLAYLIST_NAME;
|
||||||
@ -69,8 +70,9 @@ public class PlaylistRemoteEntity implements PlaylistLocalItem {
|
|||||||
@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(),
|
||||||
info.getThumbnailUrl() == null
|
// use uploader avatar when no thumbnail is available
|
||||||
? info.getUploaderAvatarUrl() : info.getThumbnailUrl(),
|
ImageStrategy.imageListToDbUrl(info.getThumbnails().isEmpty()
|
||||||
|
? info.getUploaderAvatars() : info.getThumbnails()),
|
||||||
info.getUploaderName(), info.getStreamCount());
|
info.getUploaderName(), info.getStreamCount());
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -84,7 +86,10 @@ public class PlaylistRemoteEntity implements PlaylistLocalItem {
|
|||||||
&& getStreamCount() == info.getStreamCount()
|
&& getStreamCount() == info.getStreamCount()
|
||||||
&& TextUtils.equals(getName(), info.getName())
|
&& TextUtils.equals(getName(), info.getName())
|
||||||
&& TextUtils.equals(getUrl(), info.getUrl())
|
&& TextUtils.equals(getUrl(), info.getUrl())
|
||||||
&& TextUtils.equals(getThumbnailUrl(), info.getThumbnailUrl())
|
// we want to update the local playlist data even when either the remote thumbnail
|
||||||
|
// URL changes, or the preferred image quality setting is changed by the user
|
||||||
|
&& TextUtils.equals(getThumbnailUrl(),
|
||||||
|
ImageStrategy.imageListToDbUrl(info.getThumbnails()))
|
||||||
&& TextUtils.equals(getUploader(), info.getUploaderName());
|
&& TextUtils.equals(getUploader(), info.getUploaderName());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -7,6 +7,7 @@ import org.schabi.newpipe.database.history.model.StreamHistoryEntity
|
|||||||
import org.schabi.newpipe.database.stream.model.StreamEntity
|
import org.schabi.newpipe.database.stream.model.StreamEntity
|
||||||
import org.schabi.newpipe.database.stream.model.StreamStateEntity.STREAM_PROGRESS_MILLIS
|
import org.schabi.newpipe.database.stream.model.StreamStateEntity.STREAM_PROGRESS_MILLIS
|
||||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem
|
import org.schabi.newpipe.extractor.stream.StreamInfoItem
|
||||||
|
import org.schabi.newpipe.util.image.ImageStrategy
|
||||||
import java.time.OffsetDateTime
|
import java.time.OffsetDateTime
|
||||||
|
|
||||||
class StreamStatisticsEntry(
|
class StreamStatisticsEntry(
|
||||||
@ -30,7 +31,7 @@ class StreamStatisticsEntry(
|
|||||||
item.duration = streamEntity.duration
|
item.duration = streamEntity.duration
|
||||||
item.uploaderName = streamEntity.uploader
|
item.uploaderName = streamEntity.uploader
|
||||||
item.uploaderUrl = streamEntity.uploaderUrl
|
item.uploaderUrl = streamEntity.uploaderUrl
|
||||||
item.thumbnailUrl = streamEntity.thumbnailUrl
|
item.thumbnails = ImageStrategy.dbUrlToImageList(streamEntity.thumbnailUrl)
|
||||||
|
|
||||||
return item
|
return item
|
||||||
}
|
}
|
||||||
|
@ -13,6 +13,7 @@ import org.schabi.newpipe.extractor.stream.StreamInfo
|
|||||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem
|
import org.schabi.newpipe.extractor.stream.StreamInfoItem
|
||||||
import org.schabi.newpipe.extractor.stream.StreamType
|
import org.schabi.newpipe.extractor.stream.StreamType
|
||||||
import org.schabi.newpipe.player.playqueue.PlayQueueItem
|
import org.schabi.newpipe.player.playqueue.PlayQueueItem
|
||||||
|
import org.schabi.newpipe.util.image.ImageStrategy
|
||||||
import java.io.Serializable
|
import java.io.Serializable
|
||||||
import java.time.OffsetDateTime
|
import java.time.OffsetDateTime
|
||||||
|
|
||||||
@ -67,7 +68,8 @@ data class StreamEntity(
|
|||||||
constructor(item: StreamInfoItem) : this(
|
constructor(item: StreamInfoItem) : this(
|
||||||
serviceId = item.serviceId, url = item.url, title = item.name,
|
serviceId = item.serviceId, url = item.url, title = item.name,
|
||||||
streamType = item.streamType, duration = item.duration, uploader = item.uploaderName,
|
streamType = item.streamType, duration = item.duration, uploader = item.uploaderName,
|
||||||
uploaderUrl = item.uploaderUrl, thumbnailUrl = item.thumbnailUrl, viewCount = item.viewCount,
|
uploaderUrl = item.uploaderUrl,
|
||||||
|
thumbnailUrl = ImageStrategy.imageListToDbUrl(item.thumbnails), viewCount = item.viewCount,
|
||||||
textualUploadDate = item.textualUploadDate, uploadDate = item.uploadDate?.offsetDateTime(),
|
textualUploadDate = item.textualUploadDate, uploadDate = item.uploadDate?.offsetDateTime(),
|
||||||
isUploadDateApproximation = item.uploadDate?.isApproximation
|
isUploadDateApproximation = item.uploadDate?.isApproximation
|
||||||
)
|
)
|
||||||
@ -76,7 +78,8 @@ data class StreamEntity(
|
|||||||
constructor(info: StreamInfo) : this(
|
constructor(info: StreamInfo) : this(
|
||||||
serviceId = info.serviceId, url = info.url, title = info.name,
|
serviceId = info.serviceId, url = info.url, title = info.name,
|
||||||
streamType = info.streamType, duration = info.duration, uploader = info.uploaderName,
|
streamType = info.streamType, duration = info.duration, uploader = info.uploaderName,
|
||||||
uploaderUrl = info.uploaderUrl, thumbnailUrl = info.thumbnailUrl, viewCount = info.viewCount,
|
uploaderUrl = info.uploaderUrl,
|
||||||
|
thumbnailUrl = ImageStrategy.imageListToDbUrl(info.thumbnails), viewCount = info.viewCount,
|
||||||
textualUploadDate = info.textualUploadDate, uploadDate = info.uploadDate?.offsetDateTime(),
|
textualUploadDate = info.textualUploadDate, uploadDate = info.uploadDate?.offsetDateTime(),
|
||||||
isUploadDateApproximation = info.uploadDate?.isApproximation
|
isUploadDateApproximation = info.uploadDate?.isApproximation
|
||||||
)
|
)
|
||||||
@ -85,7 +88,8 @@ data class StreamEntity(
|
|||||||
constructor(item: PlayQueueItem) : this(
|
constructor(item: PlayQueueItem) : this(
|
||||||
serviceId = item.serviceId, url = item.url, title = item.title,
|
serviceId = item.serviceId, url = item.url, title = item.title,
|
||||||
streamType = item.streamType, duration = item.duration, uploader = item.uploader,
|
streamType = item.streamType, duration = item.duration, uploader = item.uploader,
|
||||||
uploaderUrl = item.uploaderUrl, thumbnailUrl = item.thumbnailUrl
|
uploaderUrl = item.uploaderUrl,
|
||||||
|
thumbnailUrl = ImageStrategy.imageListToDbUrl(item.thumbnails)
|
||||||
)
|
)
|
||||||
|
|
||||||
fun toStreamInfoItem(): StreamInfoItem {
|
fun toStreamInfoItem(): StreamInfoItem {
|
||||||
@ -93,7 +97,7 @@ data class StreamEntity(
|
|||||||
item.duration = duration
|
item.duration = duration
|
||||||
item.uploaderName = uploader
|
item.uploaderName = uploader
|
||||||
item.uploaderUrl = uploaderUrl
|
item.uploaderUrl = uploaderUrl
|
||||||
item.thumbnailUrl = thumbnailUrl
|
item.thumbnails = ImageStrategy.dbUrlToImageList(thumbnailUrl)
|
||||||
|
|
||||||
if (viewCount != null) item.viewCount = viewCount as Long
|
if (viewCount != null) item.viewCount = viewCount as Long
|
||||||
item.textualUploadDate = textualUploadDate
|
item.textualUploadDate = textualUploadDate
|
||||||
|
@ -10,6 +10,7 @@ import androidx.room.PrimaryKey;
|
|||||||
import org.schabi.newpipe.extractor.channel.ChannelInfo;
|
import org.schabi.newpipe.extractor.channel.ChannelInfo;
|
||||||
import org.schabi.newpipe.extractor.channel.ChannelInfoItem;
|
import org.schabi.newpipe.extractor.channel.ChannelInfoItem;
|
||||||
import org.schabi.newpipe.util.Constants;
|
import org.schabi.newpipe.util.Constants;
|
||||||
|
import org.schabi.newpipe.util.image.ImageStrategy;
|
||||||
|
|
||||||
import static org.schabi.newpipe.database.subscription.SubscriptionEntity.SUBSCRIPTION_SERVICE_ID;
|
import static org.schabi.newpipe.database.subscription.SubscriptionEntity.SUBSCRIPTION_SERVICE_ID;
|
||||||
import static org.schabi.newpipe.database.subscription.SubscriptionEntity.SUBSCRIPTION_TABLE;
|
import static org.schabi.newpipe.database.subscription.SubscriptionEntity.SUBSCRIPTION_TABLE;
|
||||||
@ -57,8 +58,8 @@ public class SubscriptionEntity {
|
|||||||
final SubscriptionEntity result = new SubscriptionEntity();
|
final SubscriptionEntity result = new SubscriptionEntity();
|
||||||
result.setServiceId(info.getServiceId());
|
result.setServiceId(info.getServiceId());
|
||||||
result.setUrl(info.getUrl());
|
result.setUrl(info.getUrl());
|
||||||
result.setData(info.getName(), info.getAvatarUrl(), info.getDescription(),
|
result.setData(info.getName(), ImageStrategy.imageListToDbUrl(info.getAvatars()),
|
||||||
info.getSubscriberCount());
|
info.getDescription(), info.getSubscriberCount());
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -138,7 +139,7 @@ public class SubscriptionEntity {
|
|||||||
@Ignore
|
@Ignore
|
||||||
public ChannelInfoItem toChannelInfoItem() {
|
public ChannelInfoItem toChannelInfoItem() {
|
||||||
final ChannelInfoItem item = new ChannelInfoItem(getServiceId(), getUrl(), getName());
|
final ChannelInfoItem item = new ChannelInfoItem(getServiceId(), getUrl(), getName());
|
||||||
item.setThumbnailUrl(getAvatarUrl());
|
item.setThumbnails(ImageStrategy.dbUrlToImageList(getAvatarUrl()));
|
||||||
item.setSubscriberCount(getSubscriberCount());
|
item.setSubscriberCount(getSubscriberCount());
|
||||||
item.setDescription(getDescription());
|
item.setDescription(getDescription());
|
||||||
return item;
|
return item;
|
||||||
|
@ -16,6 +16,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 +75,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;
|
||||||
@ -146,7 +148,6 @@ public class DownloadDialog extends DialogFragment
|
|||||||
registerForActivityResult(
|
registerForActivityResult(
|
||||||
new StartActivityForResult(), this::requestDownloadPickVideoFolderResult);
|
new StartActivityForResult(), this::requestDownloadPickVideoFolderResult);
|
||||||
|
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
// Instance creation
|
// Instance creation
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
@ -267,8 +268,8 @@ public class DownloadDialog extends DialogFragment
|
|||||||
if (!videoStreams.get(i).isVideoOnly()) {
|
if (!videoStreams.get(i).isVideoOnly()) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
final AudioStream audioStream = SecondaryStreamHelper
|
final AudioStream audioStream = SecondaryStreamHelper.getAudioStreamFor(
|
||||||
.getAudioStreamFor(audioStreams.getStreamsList(), videoStreams.get(i));
|
context, audioStreams.getStreamsList(), videoStreams.get(i));
|
||||||
|
|
||||||
if (audioStream != null) {
|
if (audioStream != null) {
|
||||||
secondaryStreams.append(i, new SecondaryStreamHelper<>(audioStreams, audioStream));
|
secondaryStreams.append(i, new SecondaryStreamHelper<>(audioStreams, audioStream));
|
||||||
@ -564,7 +565,6 @@ public class DownloadDialog extends DialogFragment
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
// Listeners
|
// Listeners
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
@ -783,6 +783,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 +795,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 +808,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 +818,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 +874,22 @@ public class DownloadDialog extends DialogFragment
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check for free memory space (for api 24 and up)
|
||||||
|
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) {
|
||||||
|
final long freeSpace = mainStorage.getFreeMemory();
|
||||||
|
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 +1072,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 +1137,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 +1147,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();
|
||||||
|
@ -11,7 +11,6 @@ import org.schabi.newpipe.extractor.exceptions.AccountTerminatedException
|
|||||||
import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException
|
import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException
|
||||||
import org.schabi.newpipe.extractor.exceptions.ContentNotSupportedException
|
import org.schabi.newpipe.extractor.exceptions.ContentNotSupportedException
|
||||||
import org.schabi.newpipe.extractor.exceptions.ExtractionException
|
import org.schabi.newpipe.extractor.exceptions.ExtractionException
|
||||||
import org.schabi.newpipe.extractor.services.youtube.extractors.YoutubeStreamExtractor.DeobfuscateException
|
|
||||||
import org.schabi.newpipe.ktx.isNetworkRelated
|
import org.schabi.newpipe.ktx.isNetworkRelated
|
||||||
import org.schabi.newpipe.util.ServiceHelper
|
import org.schabi.newpipe.util.ServiceHelper
|
||||||
|
|
||||||
@ -96,7 +95,6 @@ class ErrorInfo(
|
|||||||
throwable is ContentNotAvailableException -> R.string.content_not_available
|
throwable is ContentNotAvailableException -> R.string.content_not_available
|
||||||
throwable != null && throwable.isNetworkRelated -> R.string.network_error
|
throwable != null && throwable.isNetworkRelated -> R.string.network_error
|
||||||
throwable is ContentNotSupportedException -> R.string.content_not_supported
|
throwable is ContentNotSupportedException -> R.string.content_not_supported
|
||||||
throwable is DeobfuscateException -> R.string.youtube_signature_deobfuscation_error
|
|
||||||
throwable is ExtractionException -> R.string.parsing_error
|
throwable is ExtractionException -> R.string.parsing_error
|
||||||
throwable is ExoPlaybackException -> {
|
throwable is ExoPlaybackException -> {
|
||||||
when (throwable.type) {
|
when (throwable.type) {
|
||||||
|
@ -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"),
|
||||||
|
@ -38,6 +38,7 @@ import org.schabi.newpipe.R;
|
|||||||
import org.schabi.newpipe.databinding.FragmentMainBinding;
|
import org.schabi.newpipe.databinding.FragmentMainBinding;
|
||||||
import org.schabi.newpipe.error.ErrorUtil;
|
import org.schabi.newpipe.error.ErrorUtil;
|
||||||
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||||
|
import org.schabi.newpipe.local.playlist.LocalPlaylistFragment;
|
||||||
import org.schabi.newpipe.settings.tabs.Tab;
|
import org.schabi.newpipe.settings.tabs.Tab;
|
||||||
import org.schabi.newpipe.settings.tabs.TabsManager;
|
import org.schabi.newpipe.settings.tabs.TabsManager;
|
||||||
import org.schabi.newpipe.util.NavigationHelper;
|
import org.schabi.newpipe.util.NavigationHelper;
|
||||||
@ -193,7 +194,6 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte
|
|||||||
}
|
}
|
||||||
|
|
||||||
binding.pager.setAdapter(null);
|
binding.pager.setAdapter(null);
|
||||||
binding.pager.setOffscreenPageLimit(tabsList.size());
|
|
||||||
binding.pager.setAdapter(pagerAdapter);
|
binding.pager.setAdapter(pagerAdapter);
|
||||||
|
|
||||||
updateTabsIconAndDescription();
|
updateTabsIconAndDescription();
|
||||||
@ -217,6 +217,12 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte
|
|||||||
setTitle(tabsList.get(tabPosition).getTabName(requireContext()));
|
setTitle(tabsList.get(tabPosition).getTabName(requireContext()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void commitPlaylistTabs() {
|
||||||
|
pagerAdapter.getLocalPlaylistFragments()
|
||||||
|
.stream()
|
||||||
|
.forEach(LocalPlaylistFragment::commitChanges);
|
||||||
|
}
|
||||||
|
|
||||||
private void updateTabLayoutPosition() {
|
private void updateTabLayoutPosition() {
|
||||||
final ScrollableTabLayout tabLayout = binding.mainTabLayout;
|
final ScrollableTabLayout tabLayout = binding.mainTabLayout;
|
||||||
final ViewPager viewPager = binding.pager;
|
final ViewPager viewPager = binding.pager;
|
||||||
@ -268,10 +274,18 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte
|
|||||||
updateTitleForTab(tab.getPosition());
|
updateTitleForTab(tab.getPosition());
|
||||||
}
|
}
|
||||||
|
|
||||||
private static final class SelectedTabsPagerAdapter
|
public static final class SelectedTabsPagerAdapter
|
||||||
extends FragmentStatePagerAdapterMenuWorkaround {
|
extends FragmentStatePagerAdapterMenuWorkaround {
|
||||||
private final Context context;
|
private final Context context;
|
||||||
private final List<Tab> internalTabsList;
|
private final List<Tab> internalTabsList;
|
||||||
|
/**
|
||||||
|
* Keep reference to LocalPlaylistFragments, because their data can be modified by the user
|
||||||
|
* during runtime and changes are not committed immediately. However, in some cases,
|
||||||
|
* the changes need to be committed immediately by calling
|
||||||
|
* {@link LocalPlaylistFragment#commitChanges()}.
|
||||||
|
* The fragments are removed when {@link LocalPlaylistFragment#onDestroy()} is called.
|
||||||
|
*/
|
||||||
|
private final List<LocalPlaylistFragment> localPlaylistFragments = new ArrayList<>();
|
||||||
|
|
||||||
private SelectedTabsPagerAdapter(final Context context,
|
private SelectedTabsPagerAdapter(final Context context,
|
||||||
final FragmentManager fragmentManager,
|
final FragmentManager fragmentManager,
|
||||||
@ -298,9 +312,17 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte
|
|||||||
((BaseFragment) fragment).useAsFrontPage(true);
|
((BaseFragment) fragment).useAsFrontPage(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (fragment instanceof LocalPlaylistFragment) {
|
||||||
|
localPlaylistFragments.add((LocalPlaylistFragment) fragment);
|
||||||
|
}
|
||||||
|
|
||||||
return fragment;
|
return fragment;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public List<LocalPlaylistFragment> getLocalPlaylistFragments() {
|
||||||
|
return localPlaylistFragments;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getItemPosition(@NonNull final Object object) {
|
public int getItemPosition(@NonNull final Object object) {
|
||||||
// Causes adapter to reload all Fragments when
|
// Causes adapter to reload all Fragments when
|
||||||
|
@ -4,7 +4,13 @@ import static android.text.TextUtils.isEmpty;
|
|||||||
import static org.schabi.newpipe.extractor.utils.Utils.isBlank;
|
import static org.schabi.newpipe.extractor.utils.Utils.isBlank;
|
||||||
import static org.schabi.newpipe.util.text.TextLinkifier.SET_LINK_MOVEMENT_METHOD;
|
import static org.schabi.newpipe.util.text.TextLinkifier.SET_LINK_MOVEMENT_METHOD;
|
||||||
|
|
||||||
|
import android.graphics.Typeface;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
import android.text.SpannableStringBuilder;
|
||||||
|
import android.text.Spanned;
|
||||||
|
import android.text.method.LinkMovementMethod;
|
||||||
|
import android.text.style.ClickableSpan;
|
||||||
|
import android.text.style.StyleSpan;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
@ -23,10 +29,12 @@ import org.schabi.newpipe.R;
|
|||||||
import org.schabi.newpipe.databinding.FragmentDescriptionBinding;
|
import org.schabi.newpipe.databinding.FragmentDescriptionBinding;
|
||||||
import org.schabi.newpipe.databinding.ItemMetadataBinding;
|
import org.schabi.newpipe.databinding.ItemMetadataBinding;
|
||||||
import org.schabi.newpipe.databinding.ItemMetadataTagsBinding;
|
import org.schabi.newpipe.databinding.ItemMetadataTagsBinding;
|
||||||
|
import org.schabi.newpipe.extractor.Image;
|
||||||
import org.schabi.newpipe.extractor.StreamingService;
|
import org.schabi.newpipe.extractor.StreamingService;
|
||||||
import org.schabi.newpipe.extractor.stream.Description;
|
import org.schabi.newpipe.extractor.stream.Description;
|
||||||
import org.schabi.newpipe.util.NavigationHelper;
|
import org.schabi.newpipe.util.NavigationHelper;
|
||||||
import org.schabi.newpipe.util.external_communication.ShareUtils;
|
import org.schabi.newpipe.util.external_communication.ShareUtils;
|
||||||
|
import org.schabi.newpipe.util.image.ImageStrategy;
|
||||||
import org.schabi.newpipe.util.text.TextLinkifier;
|
import org.schabi.newpipe.util.text.TextLinkifier;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@ -176,6 +184,74 @@ public abstract class BaseDescriptionFragment extends BaseFragment {
|
|||||||
layout.addView(itemBinding.getRoot());
|
layout.addView(itemBinding.getRoot());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String imageSizeToText(final int heightOrWidth) {
|
||||||
|
if (heightOrWidth < 0) {
|
||||||
|
return getString(R.string.question_mark);
|
||||||
|
} else {
|
||||||
|
return String.valueOf(heightOrWidth);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void addImagesMetadataItem(final LayoutInflater inflater,
|
||||||
|
final LinearLayout layout,
|
||||||
|
@StringRes final int type,
|
||||||
|
final List<Image> images) {
|
||||||
|
final String preferredImageUrl = ImageStrategy.choosePreferredImage(images);
|
||||||
|
if (preferredImageUrl == null) {
|
||||||
|
return; // null will be returned in case there is no image
|
||||||
|
}
|
||||||
|
|
||||||
|
final ItemMetadataBinding itemBinding =
|
||||||
|
ItemMetadataBinding.inflate(inflater, layout, false);
|
||||||
|
itemBinding.metadataTypeView.setText(type);
|
||||||
|
|
||||||
|
final SpannableStringBuilder urls = new SpannableStringBuilder();
|
||||||
|
for (final Image image : images) {
|
||||||
|
if (urls.length() != 0) {
|
||||||
|
urls.append(", ");
|
||||||
|
}
|
||||||
|
final int entryBegin = urls.length();
|
||||||
|
|
||||||
|
if (image.getHeight() != Image.HEIGHT_UNKNOWN
|
||||||
|
|| image.getWidth() != Image.WIDTH_UNKNOWN
|
||||||
|
// if even the resolution level is unknown, ?x? will be shown
|
||||||
|
|| image.getEstimatedResolutionLevel() == Image.ResolutionLevel.UNKNOWN) {
|
||||||
|
urls.append(imageSizeToText(image.getHeight()));
|
||||||
|
urls.append('x');
|
||||||
|
urls.append(imageSizeToText(image.getWidth()));
|
||||||
|
} else {
|
||||||
|
switch (image.getEstimatedResolutionLevel()) {
|
||||||
|
case LOW:
|
||||||
|
urls.append(getString(R.string.image_quality_low));
|
||||||
|
break;
|
||||||
|
default: // unreachable, Image.ResolutionLevel.UNKNOWN is already filtered out
|
||||||
|
case MEDIUM:
|
||||||
|
urls.append(getString(R.string.image_quality_medium));
|
||||||
|
break;
|
||||||
|
case HIGH:
|
||||||
|
urls.append(getString(R.string.image_quality_high));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
urls.setSpan(new ClickableSpan() {
|
||||||
|
@Override
|
||||||
|
public void onClick(@NonNull final View widget) {
|
||||||
|
ShareUtils.openUrlInBrowser(requireContext(), image.getUrl());
|
||||||
|
}
|
||||||
|
}, entryBegin, urls.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||||
|
|
||||||
|
if (preferredImageUrl.equals(image.getUrl())) {
|
||||||
|
urls.setSpan(new StyleSpan(Typeface.BOLD), entryBegin, urls.length(),
|
||||||
|
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
itemBinding.metadataContentView.setText(urls);
|
||||||
|
itemBinding.metadataContentView.setMovementMethod(LinkMovementMethod.getInstance());
|
||||||
|
layout.addView(itemBinding.getRoot());
|
||||||
|
}
|
||||||
|
|
||||||
private void addTagsMetadataItem(final LayoutInflater inflater, final LinearLayout layout) {
|
private void addTagsMetadataItem(final LayoutInflater inflater, final LinearLayout layout) {
|
||||||
final List<String> tags = getTags();
|
final List<String> tags = getTags();
|
||||||
|
|
||||||
|
@ -112,8 +112,13 @@ public class DescriptionFragment extends BaseDescriptionFragment {
|
|||||||
streamInfo.getSupportInfo());
|
streamInfo.getSupportInfo());
|
||||||
addMetadataItem(inflater, layout, true, R.string.metadata_host,
|
addMetadataItem(inflater, layout, true, R.string.metadata_host,
|
||||||
streamInfo.getHost());
|
streamInfo.getHost());
|
||||||
addMetadataItem(inflater, layout, true, R.string.metadata_thumbnail_url,
|
|
||||||
streamInfo.getThumbnailUrl());
|
addImagesMetadataItem(inflater, layout, R.string.metadata_thumbnails,
|
||||||
|
streamInfo.getThumbnails());
|
||||||
|
addImagesMetadataItem(inflater, layout, R.string.metadata_uploader_avatars,
|
||||||
|
streamInfo.getUploaderAvatars());
|
||||||
|
addImagesMetadataItem(inflater, layout, R.string.metadata_subchannel_avatars,
|
||||||
|
streamInfo.getSubChannelAvatars());
|
||||||
}
|
}
|
||||||
|
|
||||||
private void addPrivacyMetadataItem(final LayoutInflater inflater, final LinearLayout layout) {
|
private void addPrivacyMetadataItem(final LayoutInflater inflater, final LinearLayout layout) {
|
||||||
|
@ -24,7 +24,6 @@ import android.content.pm.ActivityInfo;
|
|||||||
import android.database.ContentObserver;
|
import android.database.ContentObserver;
|
||||||
import android.graphics.Color;
|
import android.graphics.Color;
|
||||||
import android.graphics.Rect;
|
import android.graphics.Rect;
|
||||||
import android.graphics.drawable.Drawable;
|
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
@ -54,6 +53,7 @@ import androidx.appcompat.content.res.AppCompatResources;
|
|||||||
import androidx.appcompat.widget.Toolbar;
|
import androidx.appcompat.widget.Toolbar;
|
||||||
import androidx.coordinatorlayout.widget.CoordinatorLayout;
|
import androidx.coordinatorlayout.widget.CoordinatorLayout;
|
||||||
import androidx.core.content.ContextCompat;
|
import androidx.core.content.ContextCompat;
|
||||||
|
import androidx.fragment.app.Fragment;
|
||||||
import androidx.preference.PreferenceManager;
|
import androidx.preference.PreferenceManager;
|
||||||
|
|
||||||
import com.google.android.exoplayer2.PlaybackException;
|
import com.google.android.exoplayer2.PlaybackException;
|
||||||
@ -71,8 +71,9 @@ import org.schabi.newpipe.error.ErrorInfo;
|
|||||||
import org.schabi.newpipe.error.ErrorUtil;
|
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.InfoItem;
|
import org.schabi.newpipe.extractor.Image;
|
||||||
import org.schabi.newpipe.extractor.NewPipe;
|
import org.schabi.newpipe.extractor.NewPipe;
|
||||||
|
import org.schabi.newpipe.extractor.comments.CommentsInfoItem;
|
||||||
import org.schabi.newpipe.extractor.exceptions.ContentNotSupportedException;
|
import org.schabi.newpipe.extractor.exceptions.ContentNotSupportedException;
|
||||||
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||||
import org.schabi.newpipe.extractor.stream.AudioStream;
|
import org.schabi.newpipe.extractor.stream.AudioStream;
|
||||||
@ -83,11 +84,13 @@ import org.schabi.newpipe.extractor.stream.VideoStream;
|
|||||||
import org.schabi.newpipe.fragments.BackPressable;
|
import org.schabi.newpipe.fragments.BackPressable;
|
||||||
import org.schabi.newpipe.fragments.BaseStateFragment;
|
import org.schabi.newpipe.fragments.BaseStateFragment;
|
||||||
import org.schabi.newpipe.fragments.EmptyFragment;
|
import org.schabi.newpipe.fragments.EmptyFragment;
|
||||||
|
import org.schabi.newpipe.fragments.MainFragment;
|
||||||
import org.schabi.newpipe.fragments.list.comments.CommentsFragment;
|
import org.schabi.newpipe.fragments.list.comments.CommentsFragment;
|
||||||
import org.schabi.newpipe.fragments.list.videos.RelatedItemsFragment;
|
import org.schabi.newpipe.fragments.list.videos.RelatedItemsFragment;
|
||||||
import org.schabi.newpipe.ktx.AnimationType;
|
import org.schabi.newpipe.ktx.AnimationType;
|
||||||
import org.schabi.newpipe.local.dialog.PlaylistDialog;
|
import org.schabi.newpipe.local.dialog.PlaylistDialog;
|
||||||
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
||||||
|
import org.schabi.newpipe.local.playlist.LocalPlaylistFragment;
|
||||||
import org.schabi.newpipe.player.Player;
|
import org.schabi.newpipe.player.Player;
|
||||||
import org.schabi.newpipe.player.PlayerService;
|
import org.schabi.newpipe.player.PlayerService;
|
||||||
import org.schabi.newpipe.player.PlayerType;
|
import org.schabi.newpipe.player.PlayerType;
|
||||||
@ -103,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.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;
|
||||||
@ -471,10 +475,23 @@ public final class VideoDetailFragment
|
|||||||
|
|
||||||
binding.detailControlsBackground.setOnClickListener(v -> openBackgroundPlayer(false));
|
binding.detailControlsBackground.setOnClickListener(v -> openBackgroundPlayer(false));
|
||||||
binding.detailControlsPopup.setOnClickListener(v -> openPopupPlayer(false));
|
binding.detailControlsPopup.setOnClickListener(v -> openPopupPlayer(false));
|
||||||
binding.detailControlsPlaylistAppend.setOnClickListener(makeOnClickListener(info ->
|
binding.detailControlsPlaylistAppend.setOnClickListener(makeOnClickListener(info -> {
|
||||||
|
if (getFM() != null && currentInfo != null) {
|
||||||
|
final Fragment fragment = getParentFragmentManager().
|
||||||
|
findFragmentById(R.id.fragment_holder);
|
||||||
|
|
||||||
|
// commit previous pending changes to database
|
||||||
|
if (fragment instanceof LocalPlaylistFragment) {
|
||||||
|
((LocalPlaylistFragment) fragment).commitChanges();
|
||||||
|
} else if (fragment instanceof MainFragment) {
|
||||||
|
((MainFragment) fragment).commitPlaylistTabs();
|
||||||
|
}
|
||||||
|
|
||||||
disposables.add(PlaylistDialog.createCorrespondingDialog(requireContext(),
|
disposables.add(PlaylistDialog.createCorrespondingDialog(requireContext(),
|
||||||
List.of(new StreamEntity(info)),
|
List.of(new StreamEntity(info)),
|
||||||
dialog -> dialog.show(getParentFragmentManager(), TAG)))));
|
dialog -> dialog.show(getParentFragmentManager(), TAG)));
|
||||||
|
}
|
||||||
|
}));
|
||||||
binding.detailControlsDownload.setOnClickListener(v -> {
|
binding.detailControlsDownload.setOnClickListener(v -> {
|
||||||
if (PermissionHelper.checkStoragePermissions(activity,
|
if (PermissionHelper.checkStoragePermissions(activity,
|
||||||
PermissionHelper.DOWNLOAD_DIALOG_REQUEST_CODE)) {
|
PermissionHelper.DOWNLOAD_DIALOG_REQUEST_CODE)) {
|
||||||
@ -483,7 +500,7 @@ public final class VideoDetailFragment
|
|||||||
});
|
});
|
||||||
binding.detailControlsShare.setOnClickListener(makeOnClickListener(info ->
|
binding.detailControlsShare.setOnClickListener(makeOnClickListener(info ->
|
||||||
ShareUtils.shareText(requireContext(), info.getName(), info.getUrl(),
|
ShareUtils.shareText(requireContext(), info.getName(), info.getUrl(),
|
||||||
info.getThumbnailUrl())));
|
info.getThumbnails())));
|
||||||
binding.detailControlsOpenInBrowser.setOnClickListener(makeOnClickListener(info ->
|
binding.detailControlsOpenInBrowser.setOnClickListener(makeOnClickListener(info ->
|
||||||
ShareUtils.openUrlInBrowser(requireContext(), info.getUrl())));
|
ShareUtils.openUrlInBrowser(requireContext(), info.getUrl())));
|
||||||
binding.detailControlsPlayWithKodi.setOnClickListener(makeOnClickListener(info ->
|
binding.detailControlsPlayWithKodi.setOnClickListener(makeOnClickListener(info ->
|
||||||
@ -723,7 +740,7 @@ public final class VideoDetailFragment
|
|||||||
final boolean isPlayerStopped = !isPlayerAvailable() || player.isStopped();
|
final boolean isPlayerStopped = !isPlayerAvailable() || player.isStopped();
|
||||||
if (playQueueItem != null && isPlayerStopped) {
|
if (playQueueItem != null && isPlayerStopped) {
|
||||||
updateOverlayData(playQueueItem.getTitle(),
|
updateOverlayData(playQueueItem.getTitle(),
|
||||||
playQueueItem.getUploader(), playQueueItem.getThumbnailUrl());
|
playQueueItem.getUploader(), playQueueItem.getThumbnails());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -996,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
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
@ -1414,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);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1465,11 +1496,6 @@ public final class VideoDetailFragment
|
|||||||
displayUploaderAsSubChannel(info);
|
displayUploaderAsSubChannel(info);
|
||||||
}
|
}
|
||||||
|
|
||||||
final Drawable buddyDrawable =
|
|
||||||
AppCompatResources.getDrawable(activity, R.drawable.placeholder_person);
|
|
||||||
binding.detailSubChannelThumbnailView.setImageDrawable(buddyDrawable);
|
|
||||||
binding.detailUploaderThumbnailView.setImageDrawable(buddyDrawable);
|
|
||||||
|
|
||||||
if (info.getViewCount() >= 0) {
|
if (info.getViewCount() >= 0) {
|
||||||
if (info.getStreamType().equals(StreamType.AUDIO_LIVE_STREAM)) {
|
if (info.getStreamType().equals(StreamType.AUDIO_LIVE_STREAM)) {
|
||||||
binding.detailViewCountView.setText(Localization.listeningCount(activity,
|
binding.detailViewCountView.setText(Localization.listeningCount(activity,
|
||||||
@ -1536,13 +1562,13 @@ public final class VideoDetailFragment
|
|||||||
binding.detailSecondaryControlPanel.setVisibility(View.GONE);
|
binding.detailSecondaryControlPanel.setVisibility(View.GONE);
|
||||||
|
|
||||||
checkUpdateProgressInfo(info);
|
checkUpdateProgressInfo(info);
|
||||||
PicassoHelper.loadDetailsThumbnail(info.getThumbnailUrl()).tag(PICASSO_VIDEO_DETAILS_TAG)
|
PicassoHelper.loadDetailsThumbnail(info.getThumbnails()).tag(PICASSO_VIDEO_DETAILS_TAG)
|
||||||
.into(binding.detailThumbnailImageView);
|
.into(binding.detailThumbnailImageView);
|
||||||
showMetaInfoInTextView(info.getMetaInfo(), binding.detailMetaInfoTextView,
|
showMetaInfoInTextView(info.getMetaInfo(), binding.detailMetaInfoTextView,
|
||||||
binding.detailMetaInfoSeparator, disposables);
|
binding.detailMetaInfoSeparator, disposables);
|
||||||
|
|
||||||
if (!isPlayerAvailable() || player.isStopped()) {
|
if (!isPlayerAvailable() || player.isStopped()) {
|
||||||
updateOverlayData(info.getName(), info.getUploaderName(), info.getThumbnailUrl());
|
updateOverlayData(info.getName(), info.getUploaderName(), info.getThumbnails());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!info.getErrors().isEmpty()) {
|
if (!info.getErrors().isEmpty()) {
|
||||||
@ -1587,7 +1613,7 @@ public final class VideoDetailFragment
|
|||||||
binding.detailUploaderTextView.setVisibility(View.GONE);
|
binding.detailUploaderTextView.setVisibility(View.GONE);
|
||||||
}
|
}
|
||||||
|
|
||||||
PicassoHelper.loadAvatar(info.getUploaderAvatarUrl()).tag(PICASSO_VIDEO_DETAILS_TAG)
|
PicassoHelper.loadAvatar(info.getUploaderAvatars()).tag(PICASSO_VIDEO_DETAILS_TAG)
|
||||||
.into(binding.detailSubChannelThumbnailView);
|
.into(binding.detailSubChannelThumbnailView);
|
||||||
binding.detailSubChannelThumbnailView.setVisibility(View.VISIBLE);
|
binding.detailSubChannelThumbnailView.setVisibility(View.VISIBLE);
|
||||||
binding.detailUploaderThumbnailView.setVisibility(View.GONE);
|
binding.detailUploaderThumbnailView.setVisibility(View.GONE);
|
||||||
@ -1619,10 +1645,10 @@ public final class VideoDetailFragment
|
|||||||
binding.detailUploaderTextView.setVisibility(View.GONE);
|
binding.detailUploaderTextView.setVisibility(View.GONE);
|
||||||
}
|
}
|
||||||
|
|
||||||
PicassoHelper.loadAvatar(info.getSubChannelAvatarUrl()).tag(PICASSO_VIDEO_DETAILS_TAG)
|
PicassoHelper.loadAvatar(info.getSubChannelAvatars()).tag(PICASSO_VIDEO_DETAILS_TAG)
|
||||||
.into(binding.detailSubChannelThumbnailView);
|
.into(binding.detailSubChannelThumbnailView);
|
||||||
binding.detailSubChannelThumbnailView.setVisibility(View.VISIBLE);
|
binding.detailSubChannelThumbnailView.setVisibility(View.VISIBLE);
|
||||||
PicassoHelper.loadAvatar(info.getUploaderAvatarUrl()).tag(PICASSO_VIDEO_DETAILS_TAG)
|
PicassoHelper.loadAvatar(info.getUploaderAvatars()).tag(PICASSO_VIDEO_DETAILS_TAG)
|
||||||
.into(binding.detailUploaderThumbnailView);
|
.into(binding.detailUploaderThumbnailView);
|
||||||
binding.detailUploaderThumbnailView.setVisibility(View.VISIBLE);
|
binding.detailUploaderThumbnailView.setVisibility(View.VISIBLE);
|
||||||
}
|
}
|
||||||
@ -1797,7 +1823,7 @@ public final class VideoDetailFragment
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
updateOverlayData(info.getName(), info.getUploaderName(), info.getThumbnailUrl());
|
updateOverlayData(info.getName(), info.getUploaderName(), info.getThumbnails());
|
||||||
if (currentInfo != null && info.getUrl().equals(currentInfo.getUrl())) {
|
if (currentInfo != null && info.getUrl().equals(currentInfo.getUrl())) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -1826,7 +1852,7 @@ public final class VideoDetailFragment
|
|||||||
if (currentInfo != null) {
|
if (currentInfo != null) {
|
||||||
updateOverlayData(currentInfo.getName(),
|
updateOverlayData(currentInfo.getName(),
|
||||||
currentInfo.getUploaderName(),
|
currentInfo.getUploaderName(),
|
||||||
currentInfo.getThumbnailUrl());
|
currentInfo.getThumbnails());
|
||||||
}
|
}
|
||||||
updateOverlayPlayQueueButtonVisibility();
|
updateOverlayPlayQueueButtonVisibility();
|
||||||
}
|
}
|
||||||
@ -2191,7 +2217,7 @@ public final class VideoDetailFragment
|
|||||||
playerHolder.stopService();
|
playerHolder.stopService();
|
||||||
setInitialData(0, null, "", null);
|
setInitialData(0, null, "", null);
|
||||||
currentInfo = null;
|
currentInfo = null;
|
||||||
updateOverlayData(null, null, null);
|
updateOverlayData(null, null, List.of());
|
||||||
}
|
}
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
@ -2373,11 +2399,11 @@ public final class VideoDetailFragment
|
|||||||
|
|
||||||
private void updateOverlayData(@Nullable final String overlayTitle,
|
private void updateOverlayData(@Nullable final String overlayTitle,
|
||||||
@Nullable final String uploader,
|
@Nullable final String uploader,
|
||||||
@Nullable final String thumbnailUrl) {
|
@NonNull final List<Image> thumbnails) {
|
||||||
binding.overlayTitleTextView.setText(isEmpty(overlayTitle) ? "" : overlayTitle);
|
binding.overlayTitleTextView.setText(isEmpty(overlayTitle) ? "" : overlayTitle);
|
||||||
binding.overlayChannelTextView.setText(isEmpty(uploader) ? "" : uploader);
|
binding.overlayChannelTextView.setText(isEmpty(uploader) ? "" : uploader);
|
||||||
binding.overlayThumbnail.setImageDrawable(null);
|
binding.overlayThumbnail.setImageDrawable(null);
|
||||||
PicassoHelper.loadDetailsThumbnail(thumbnailUrl).tag(PICASSO_VIDEO_DETAILS_TAG)
|
PicassoHelper.loadDetailsThumbnail(thumbnails).tag(PICASSO_VIDEO_DETAILS_TAG)
|
||||||
.into(binding.overlayThumbnail);
|
.into(binding.overlayThumbnail);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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();
|
||||||
|
@ -99,9 +99,9 @@ public class ChannelAboutFragment extends BaseDescriptionFragment {
|
|||||||
Localization.localizeNumber(context, channelInfo.getSubscriberCount()));
|
Localization.localizeNumber(context, channelInfo.getSubscriberCount()));
|
||||||
}
|
}
|
||||||
|
|
||||||
addMetadataItem(inflater, layout, true, R.string.metadata_avatar_url,
|
addImagesMetadataItem(inflater, layout, R.string.metadata_avatars,
|
||||||
channelInfo.getAvatarUrl());
|
channelInfo.getAvatars());
|
||||||
addMetadataItem(inflater, layout, true, R.string.metadata_banner_url,
|
addImagesMetadataItem(inflater, layout, R.string.metadata_banners,
|
||||||
channelInfo.getBannerUrl());
|
channelInfo.getBanners());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
package org.schabi.newpipe.fragments.list.channel;
|
package org.schabi.newpipe.fragments.list.channel;
|
||||||
|
|
||||||
import static org.schabi.newpipe.extractor.utils.Utils.isBlank;
|
|
||||||
import static org.schabi.newpipe.ktx.TextViewUtils.animateTextColor;
|
import static org.schabi.newpipe.ktx.TextViewUtils.animateTextColor;
|
||||||
import static org.schabi.newpipe.ktx.ViewUtils.animate;
|
import static org.schabi.newpipe.ktx.ViewUtils.animate;
|
||||||
import static org.schabi.newpipe.ktx.ViewUtils.animateBackgroundColor;
|
import static org.schabi.newpipe.ktx.ViewUtils.animateBackgroundColor;
|
||||||
@ -49,8 +48,9 @@ import org.schabi.newpipe.util.Constants;
|
|||||||
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.PicassoHelper;
|
|
||||||
import org.schabi.newpipe.util.StateSaver;
|
import org.schabi.newpipe.util.StateSaver;
|
||||||
|
import org.schabi.newpipe.util.image.ImageStrategy;
|
||||||
|
import org.schabi.newpipe.util.image.PicassoHelper;
|
||||||
import org.schabi.newpipe.util.ThemeHelper;
|
import org.schabi.newpipe.util.ThemeHelper;
|
||||||
import org.schabi.newpipe.util.external_communication.ShareUtils;
|
import org.schabi.newpipe.util.external_communication.ShareUtils;
|
||||||
|
|
||||||
@ -148,7 +148,7 @@ public class ChannelFragment extends BaseStateFragment<ChannelInfo>
|
|||||||
|
|
||||||
setTitle(name);
|
setTitle(name);
|
||||||
binding.channelTitleView.setText(name);
|
binding.channelTitleView.setText(name);
|
||||||
if (!PicassoHelper.getShouldLoadImages()) {
|
if (!ImageStrategy.shouldLoadImages()) {
|
||||||
// do not waste space for the banner if it is not going to be loaded
|
// do not waste space for the banner if it is not going to be loaded
|
||||||
binding.channelBannerImage.setImageDrawable(null);
|
binding.channelBannerImage.setImageDrawable(null);
|
||||||
}
|
}
|
||||||
@ -234,7 +234,7 @@ public class ChannelFragment extends BaseStateFragment<ChannelInfo>
|
|||||||
case R.id.menu_item_share:
|
case R.id.menu_item_share:
|
||||||
if (currentInfo != null) {
|
if (currentInfo != null) {
|
||||||
ShareUtils.shareText(requireContext(), name, currentInfo.getOriginalUrl(),
|
ShareUtils.shareText(requireContext(), name, currentInfo.getOriginalUrl(),
|
||||||
currentInfo.getAvatarUrl());
|
currentInfo.getAvatars());
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
@ -355,7 +355,7 @@ public class ChannelFragment extends BaseStateFragment<ChannelInfo>
|
|||||||
channel.setServiceId(info.getServiceId());
|
channel.setServiceId(info.getServiceId());
|
||||||
channel.setUrl(info.getUrl());
|
channel.setUrl(info.getUrl());
|
||||||
channel.setData(info.getName(),
|
channel.setData(info.getName(),
|
||||||
info.getAvatarUrl(),
|
ImageStrategy.imageListToDbUrl(info.getAvatars()),
|
||||||
info.getDescription(),
|
info.getDescription(),
|
||||||
info.getSubscriberCount());
|
info.getSubscriberCount());
|
||||||
channelSubscription = null;
|
channelSubscription = null;
|
||||||
@ -579,17 +579,17 @@ public class ChannelFragment extends BaseStateFragment<ChannelInfo>
|
|||||||
currentInfo = result;
|
currentInfo = result;
|
||||||
setInitialData(result.getServiceId(), result.getOriginalUrl(), result.getName());
|
setInitialData(result.getServiceId(), result.getOriginalUrl(), result.getName());
|
||||||
|
|
||||||
if (PicassoHelper.getShouldLoadImages() && !isBlank(result.getBannerUrl())) {
|
if (ImageStrategy.shouldLoadImages() && !result.getBanners().isEmpty()) {
|
||||||
PicassoHelper.loadBanner(result.getBannerUrl()).tag(PICASSO_CHANNEL_TAG)
|
PicassoHelper.loadBanner(result.getBanners()).tag(PICASSO_CHANNEL_TAG)
|
||||||
.into(binding.channelBannerImage);
|
.into(binding.channelBannerImage);
|
||||||
} else {
|
} else {
|
||||||
// do not waste space for the banner, if the user disabled images or there is not one
|
// do not waste space for the banner, if the user disabled images or there is not one
|
||||||
binding.channelBannerImage.setImageDrawable(null);
|
binding.channelBannerImage.setImageDrawable(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
PicassoHelper.loadAvatar(result.getAvatarUrl()).tag(PICASSO_CHANNEL_TAG)
|
PicassoHelper.loadAvatar(result.getAvatars()).tag(PICASSO_CHANNEL_TAG)
|
||||||
.into(binding.channelAvatarView);
|
.into(binding.channelAvatarView);
|
||||||
PicassoHelper.loadAvatar(result.getParentChannelAvatarUrl()).tag(PICASSO_CHANNEL_TAG)
|
PicassoHelper.loadAvatar(result.getParentChannelAvatars()).tag(PICASSO_CHANNEL_TAG)
|
||||||
.into(binding.subChannelAvatarView);
|
.into(binding.subChannelAvatarView);
|
||||||
|
|
||||||
binding.channelTitleView.setText(result.getName());
|
binding.channelTitleView.setText(result.getName());
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package org.schabi.newpipe.fragments.list.channel;
|
package org.schabi.newpipe.fragments.list.channel;
|
||||||
|
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
import android.util.Log;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
@ -14,7 +15,10 @@ import org.schabi.newpipe.error.UserAction;
|
|||||||
import org.schabi.newpipe.extractor.InfoItem;
|
import org.schabi.newpipe.extractor.InfoItem;
|
||||||
import org.schabi.newpipe.extractor.ListExtractor;
|
import org.schabi.newpipe.extractor.ListExtractor;
|
||||||
import org.schabi.newpipe.extractor.channel.tabs.ChannelTabInfo;
|
import org.schabi.newpipe.extractor.channel.tabs.ChannelTabInfo;
|
||||||
|
import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
||||||
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler;
|
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler;
|
||||||
|
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandlerFactory;
|
||||||
|
import org.schabi.newpipe.extractor.linkhandler.ReadyChannelTabListLinkHandler;
|
||||||
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.fragments.list.playlist.PlaylistControlViewHolder;
|
import org.schabi.newpipe.fragments.list.playlist.PlaylistControlViewHolder;
|
||||||
@ -114,6 +118,30 @@ public class ChannelTabFragment extends BaseListInfoFragment<InfoItem, ChannelTa
|
|||||||
public void handleResult(@NonNull final ChannelTabInfo result) {
|
public void handleResult(@NonNull final ChannelTabInfo result) {
|
||||||
super.handleResult(result);
|
super.handleResult(result);
|
||||||
|
|
||||||
|
// FIXME this is a really hacky workaround, to avoid storing useless data in the fragment
|
||||||
|
// state. The problem is, `ReadyChannelTabListLinkHandler` might contain raw JSON data that
|
||||||
|
// uses a lot of memory (e.g. ~800KB for YouTube). While 800KB doesn't seem much, if
|
||||||
|
// you combine just a couple of channel tab fragments you easily go over the 1MB
|
||||||
|
// save&restore transaction limit, and get `TransactionTooLargeException`s. A proper
|
||||||
|
// solution would require rethinking about `ReadyChannelTabListLinkHandler`s.
|
||||||
|
if (tabHandler instanceof ReadyChannelTabListLinkHandler) {
|
||||||
|
try {
|
||||||
|
// once `handleResult` is called, the parsed data was already saved to cache, so
|
||||||
|
// we can discard any raw data in ReadyChannelTabListLinkHandler and create a
|
||||||
|
// link handler with identical properties, but without any raw data
|
||||||
|
final ListLinkHandlerFactory channelTabLHFactory = result.getService()
|
||||||
|
.getChannelTabLHFactory();
|
||||||
|
if (channelTabLHFactory != null) {
|
||||||
|
// some services do not not have a ChannelTabLHFactory
|
||||||
|
tabHandler = channelTabLHFactory.fromQuery(tabHandler.getId(),
|
||||||
|
tabHandler.getContentFilters(), tabHandler.getSortFilter());
|
||||||
|
}
|
||||||
|
} catch (final ParsingException e) {
|
||||||
|
// silently ignore the error, as the app can continue to function normally
|
||||||
|
Log.w(TAG, "Could not recreate channel tab handler", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (playlistControlBinding != null) {
|
if (playlistControlBinding != null) {
|
||||||
// PlaylistControls should be visible only if there is some item in
|
// PlaylistControls should be visible only if there is some item in
|
||||||
// infoListAdapter other than header
|
// infoListAdapter other than header
|
||||||
|
@ -0,0 +1,168 @@
|
|||||||
|
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 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();
|
||||||
|
|
||||||
|
private 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.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;
|
||||||
@ -234,7 +238,7 @@ public class PlaylistFragment extends BaseListInfoFragment<StreamInfoItem, Playl
|
|||||||
break;
|
break;
|
||||||
case R.id.menu_item_share:
|
case R.id.menu_item_share:
|
||||||
ShareUtils.shareText(requireContext(), name, url,
|
ShareUtils.shareText(requireContext(), name, url,
|
||||||
currentInfo == null ? null : currentInfo.getThumbnailUrl());
|
currentInfo == null ? List.of() : currentInfo.getThumbnails());
|
||||||
break;
|
break;
|
||||||
case R.id.menu_item_bookmark:
|
case R.id.menu_item_bookmark:
|
||||||
onBookmarkClicked();
|
onBookmarkClicked();
|
||||||
@ -299,7 +303,6 @@ public class PlaylistFragment extends BaseListInfoFragment<StreamInfoItem, Playl
|
|||||||
|
|
||||||
playlistControlBinding.getRoot().setVisibility(View.VISIBLE);
|
playlistControlBinding.getRoot().setVisibility(View.VISIBLE);
|
||||||
|
|
||||||
final String avatarUrl = result.getUploaderAvatarUrl();
|
|
||||||
if (result.getServiceId() == ServiceList.YouTube.getServiceId()
|
if (result.getServiceId() == ServiceList.YouTube.getServiceId()
|
||||||
&& (YoutubeParsingHelper.isYoutubeMixId(result.getId())
|
&& (YoutubeParsingHelper.isYoutubeMixId(result.getId())
|
||||||
|| YoutubeParsingHelper.isYoutubeMusicMixId(result.getId()))) {
|
|| YoutubeParsingHelper.isYoutubeMusicMixId(result.getId()))) {
|
||||||
@ -315,13 +318,36 @@ public class PlaylistFragment extends BaseListInfoFragment<StreamInfoItem, Playl
|
|||||||
R.drawable.ic_radio)
|
R.drawable.ic_radio)
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
PicassoHelper.loadAvatar(avatarUrl).tag(PICASSO_PLAYLIST_TAG)
|
PicassoHelper.loadAvatar(result.getUploaderAvatars()).tag(PICASSO_PLAYLIST_TAG)
|
||||||
.into(headerBinding.uploaderAvatarView);
|
.into(headerBinding.uploaderAvatarView);
|
||||||
}
|
}
|
||||||
|
|
||||||
headerBinding.playlistStreamCount.setText(Localization
|
headerBinding.playlistStreamCount.setText(Localization
|
||||||
.localizeStreamCount(getContext(), result.getStreamCount()));
|
.localizeStreamCount(getContext(), result.getStreamCount()));
|
||||||
|
|
||||||
|
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,
|
||||||
result.getUrl(), result));
|
result.getUrl(), result));
|
||||||
|
@ -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()));
|
||||||
}
|
}
|
||||||
|
@ -8,7 +8,7 @@ import com.xwray.groupie.Item
|
|||||||
import org.schabi.newpipe.R
|
import org.schabi.newpipe.R
|
||||||
import org.schabi.newpipe.extractor.stream.StreamSegment
|
import org.schabi.newpipe.extractor.stream.StreamSegment
|
||||||
import org.schabi.newpipe.util.Localization
|
import org.schabi.newpipe.util.Localization
|
||||||
import org.schabi.newpipe.util.PicassoHelper
|
import org.schabi.newpipe.util.image.PicassoHelper
|
||||||
|
|
||||||
class StreamSegmentItem(
|
class StreamSegmentItem(
|
||||||
private val item: StreamSegment,
|
private val item: StreamSegment,
|
||||||
|
@ -104,7 +104,7 @@ public enum StreamDialogDefaultEntry {
|
|||||||
|
|
||||||
SHARE(R.string.share, (fragment, item) ->
|
SHARE(R.string.share, (fragment, item) ->
|
||||||
ShareUtils.shareText(fragment.requireContext(), item.getName(), item.getUrl(),
|
ShareUtils.shareText(fragment.requireContext(), item.getName(), item.getUrl(),
|
||||||
item.getThumbnailUrl())),
|
item.getThumbnails())),
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Opens a {@link DownloadDialog} after fetching some stream info.
|
* Opens a {@link DownloadDialog} after fetching some stream info.
|
||||||
|
@ -13,7 +13,7 @@ import org.schabi.newpipe.extractor.channel.ChannelInfoItem;
|
|||||||
import org.schabi.newpipe.extractor.utils.Utils;
|
import org.schabi.newpipe.extractor.utils.Utils;
|
||||||
import org.schabi.newpipe.info_list.InfoItemBuilder;
|
import org.schabi.newpipe.info_list.InfoItemBuilder;
|
||||||
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
||||||
import org.schabi.newpipe.util.PicassoHelper;
|
import org.schabi.newpipe.util.image.PicassoHelper;
|
||||||
import org.schabi.newpipe.util.Localization;
|
import org.schabi.newpipe.util.Localization;
|
||||||
|
|
||||||
public class ChannelMiniInfoItemHolder extends InfoItemHolder {
|
public class ChannelMiniInfoItemHolder extends InfoItemHolder {
|
||||||
@ -56,7 +56,7 @@ public class ChannelMiniInfoItemHolder extends InfoItemHolder {
|
|||||||
itemAdditionalDetailView.setText(getDetailLine(item));
|
itemAdditionalDetailView.setText(getDetailLine(item));
|
||||||
}
|
}
|
||||||
|
|
||||||
PicassoHelper.loadAvatar(item.getThumbnailUrl()).into(itemThumbnailView);
|
PicassoHelper.loadAvatar(item.getThumbnails()).into(itemThumbnailView);
|
||||||
|
|
||||||
itemView.setOnClickListener(view -> {
|
itemView.setOnClickListener(view -> {
|
||||||
if (itemBuilder.getOnChannelSelectedListener() != null) {
|
if (itemBuilder.getOnChannelSelectedListener() != null) {
|
||||||
|
@ -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,279 +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.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.getUploaderAvatarUrl()).into(itemThumbnailView);
|
|
||||||
if (PicassoHelper.getShouldLoadImages()) {
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -9,7 +9,7 @@ import org.schabi.newpipe.extractor.InfoItem;
|
|||||||
import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem;
|
import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem;
|
||||||
import org.schabi.newpipe.info_list.InfoItemBuilder;
|
import org.schabi.newpipe.info_list.InfoItemBuilder;
|
||||||
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
||||||
import org.schabi.newpipe.util.PicassoHelper;
|
import org.schabi.newpipe.util.image.PicassoHelper;
|
||||||
import org.schabi.newpipe.util.Localization;
|
import org.schabi.newpipe.util.Localization;
|
||||||
|
|
||||||
public class PlaylistMiniInfoItemHolder extends InfoItemHolder {
|
public class PlaylistMiniInfoItemHolder extends InfoItemHolder {
|
||||||
@ -46,7 +46,7 @@ public class PlaylistMiniInfoItemHolder extends InfoItemHolder {
|
|||||||
.localizeStreamCountMini(itemStreamCountView.getContext(), item.getStreamCount()));
|
.localizeStreamCountMini(itemStreamCountView.getContext(), item.getStreamCount()));
|
||||||
itemUploaderView.setText(item.getUploaderName());
|
itemUploaderView.setText(item.getUploaderName());
|
||||||
|
|
||||||
PicassoHelper.loadPlaylistThumbnail(item.getThumbnailUrl()).into(itemThumbnailView);
|
PicassoHelper.loadPlaylistThumbnail(item.getThumbnails()).into(itemThumbnailView);
|
||||||
|
|
||||||
itemView.setOnClickListener(view -> {
|
itemView.setOnClickListener(view -> {
|
||||||
if (itemBuilder.getOnPlaylistSelectedListener() != null) {
|
if (itemBuilder.getOnPlaylistSelectedListener() != null) {
|
||||||
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -16,7 +16,7 @@ import org.schabi.newpipe.ktx.ViewUtils;
|
|||||||
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
||||||
import org.schabi.newpipe.util.DependentPreferenceHelper;
|
import org.schabi.newpipe.util.DependentPreferenceHelper;
|
||||||
import org.schabi.newpipe.util.Localization;
|
import org.schabi.newpipe.util.Localization;
|
||||||
import org.schabi.newpipe.util.PicassoHelper;
|
import org.schabi.newpipe.util.image.PicassoHelper;
|
||||||
import org.schabi.newpipe.util.StreamTypeUtil;
|
import org.schabi.newpipe.util.StreamTypeUtil;
|
||||||
import org.schabi.newpipe.views.AnimatedProgressBar;
|
import org.schabi.newpipe.views.AnimatedProgressBar;
|
||||||
|
|
||||||
@ -87,7 +87,7 @@ public class StreamMiniInfoItemHolder extends InfoItemHolder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Default thumbnail is shown on error, while loading and if the url is empty
|
// Default thumbnail is shown on error, while loading and if the url is empty
|
||||||
PicassoHelper.loadThumbnail(item.getThumbnailUrl()).into(itemThumbnailView);
|
PicassoHelper.loadThumbnail(item.getThumbnails()).into(itemThumbnailView);
|
||||||
|
|
||||||
itemView.setOnClickListener(view -> {
|
itemView.setOnClickListener(view -> {
|
||||||
if (itemBuilder.getOnStreamSelectedListener() != null) {
|
if (itemBuilder.getOnStreamSelectedListener() != null) {
|
||||||
|
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)
|
||||||
|
}
|
@ -607,9 +607,13 @@ class FeedFragment : BaseStateFragment<FeedState>() {
|
|||||||
execOnEnd = {
|
execOnEnd = {
|
||||||
// Disabled animations would result in immediately hiding the button
|
// Disabled animations would result in immediately hiding the button
|
||||||
// after it showed up
|
// after it showed up
|
||||||
if (DeviceUtils.hasAnimationsAnimatorDurationEnabled(context)) {
|
// Context can be null in some cases, so we have to make sure it is not null in
|
||||||
// Hide the new items-"popup" after 10s
|
// order to avoid a NullPointerException
|
||||||
hideNewItemsLoaded(true, 10000)
|
context?.let {
|
||||||
|
if (DeviceUtils.hasAnimationsAnimatorDurationEnabled(it)) {
|
||||||
|
// Hide the new items button after 10s
|
||||||
|
hideNewItemsLoaded(true, 10000)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
@ -18,8 +18,8 @@ import org.schabi.newpipe.extractor.stream.StreamType.POST_LIVE_AUDIO_STREAM
|
|||||||
import org.schabi.newpipe.extractor.stream.StreamType.POST_LIVE_STREAM
|
import org.schabi.newpipe.extractor.stream.StreamType.POST_LIVE_STREAM
|
||||||
import org.schabi.newpipe.extractor.stream.StreamType.VIDEO_STREAM
|
import org.schabi.newpipe.extractor.stream.StreamType.VIDEO_STREAM
|
||||||
import org.schabi.newpipe.util.Localization
|
import org.schabi.newpipe.util.Localization
|
||||||
import org.schabi.newpipe.util.PicassoHelper
|
|
||||||
import org.schabi.newpipe.util.StreamTypeUtil
|
import org.schabi.newpipe.util.StreamTypeUtil
|
||||||
|
import org.schabi.newpipe.util.image.PicassoHelper
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
import java.util.function.Consumer
|
import java.util.function.Consumer
|
||||||
|
|
||||||
|
@ -22,7 +22,7 @@ import org.schabi.newpipe.R
|
|||||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem
|
import org.schabi.newpipe.extractor.stream.StreamInfoItem
|
||||||
import org.schabi.newpipe.local.feed.service.FeedUpdateInfo
|
import org.schabi.newpipe.local.feed.service.FeedUpdateInfo
|
||||||
import org.schabi.newpipe.util.NavigationHelper
|
import org.schabi.newpipe.util.NavigationHelper
|
||||||
import org.schabi.newpipe.util.PicassoHelper
|
import org.schabi.newpipe.util.image.PicassoHelper
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Helper for everything related to show notifications about new streams to the user.
|
* Helper for everything related to show notifications about new streams to the user.
|
||||||
@ -58,7 +58,7 @@ class NotificationHelper(val context: Context) {
|
|||||||
.setAutoCancel(true)
|
.setAutoCancel(true)
|
||||||
.setCategory(NotificationCompat.CATEGORY_SOCIAL)
|
.setCategory(NotificationCompat.CATEGORY_SOCIAL)
|
||||||
.setGroupSummary(true)
|
.setGroupSummary(true)
|
||||||
.setGroup(data.originalInfo.url)
|
.setGroup(data.url)
|
||||||
.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_SUMMARY)
|
.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_SUMMARY)
|
||||||
|
|
||||||
// Build a summary notification for Android versions < 7.0
|
// Build a summary notification for Android versions < 7.0
|
||||||
@ -73,7 +73,7 @@ class NotificationHelper(val context: Context) {
|
|||||||
context,
|
context,
|
||||||
data.pseudoId,
|
data.pseudoId,
|
||||||
NavigationHelper
|
NavigationHelper
|
||||||
.getChannelIntent(context, data.originalInfo.serviceId, data.originalInfo.url)
|
.getChannelIntent(context, data.serviceId, data.url)
|
||||||
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK),
|
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK),
|
||||||
0,
|
0,
|
||||||
false
|
false
|
||||||
@ -88,7 +88,7 @@ class NotificationHelper(val context: Context) {
|
|||||||
|
|
||||||
// Show individual stream notifications, set channel icon only if there is actually
|
// Show individual stream notifications, set channel icon only if there is actually
|
||||||
// one
|
// one
|
||||||
showStreamNotifications(newStreams, data.originalInfo.serviceId, bitmap)
|
showStreamNotifications(newStreams, data.serviceId, bitmap)
|
||||||
// Show summary notification
|
// Show summary notification
|
||||||
manager.notify(data.pseudoId, summaryBuilder.build())
|
manager.notify(data.pseudoId, summaryBuilder.build())
|
||||||
|
|
||||||
@ -97,7 +97,7 @@ class NotificationHelper(val context: Context) {
|
|||||||
|
|
||||||
override fun onBitmapFailed(e: Exception, errorDrawable: Drawable) {
|
override fun onBitmapFailed(e: Exception, errorDrawable: Drawable) {
|
||||||
// Show individual stream notifications
|
// Show individual stream notifications
|
||||||
showStreamNotifications(newStreams, data.originalInfo.serviceId, null)
|
showStreamNotifications(newStreams, data.serviceId, null)
|
||||||
// Show summary notification
|
// Show summary notification
|
||||||
manager.notify(data.pseudoId, summaryBuilder.build())
|
manager.notify(data.pseudoId, summaryBuilder.build())
|
||||||
iconLoadingTargets.remove(this) // allow it to be garbage-collected
|
iconLoadingTargets.remove(this) // allow it to be garbage-collected
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
@ -277,14 +277,14 @@ class FeedLoadManager(private val context: Context) {
|
|||||||
notification.value!!.newStreams = filterNewStreams(info.streams)
|
notification.value!!.newStreams = filterNewStreams(info.streams)
|
||||||
|
|
||||||
feedDatabaseManager.upsertAll(info.uid, info.streams)
|
feedDatabaseManager.upsertAll(info.uid, info.streams)
|
||||||
subscriptionManager.updateFromInfo(info.uid, info.originalInfo)
|
subscriptionManager.updateFromInfo(info)
|
||||||
|
|
||||||
if (info.errors.isNotEmpty()) {
|
if (info.errors.isNotEmpty()) {
|
||||||
feedResultsHolder.addErrors(
|
feedResultsHolder.addErrors(
|
||||||
info.errors.map {
|
info.errors.map {
|
||||||
FeedLoadService.RequestException(
|
FeedLoadService.RequestException(
|
||||||
info.uid,
|
info.uid,
|
||||||
"${info.originalInfo.serviceId}:${info.originalInfo.url}",
|
"${info.serviceId}:${info.url}",
|
||||||
it
|
it
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -3,29 +3,48 @@ package org.schabi.newpipe.local.feed.service
|
|||||||
import org.schabi.newpipe.database.subscription.NotificationMode
|
import org.schabi.newpipe.database.subscription.NotificationMode
|
||||||
import org.schabi.newpipe.database.subscription.SubscriptionEntity
|
import org.schabi.newpipe.database.subscription.SubscriptionEntity
|
||||||
import org.schabi.newpipe.extractor.Info
|
import org.schabi.newpipe.extractor.Info
|
||||||
|
import org.schabi.newpipe.extractor.channel.ChannelInfo
|
||||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem
|
import org.schabi.newpipe.extractor.stream.StreamInfoItem
|
||||||
|
import org.schabi.newpipe.util.image.ImageStrategy
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Instances of this class might stay around in memory for some time while fetching the feed,
|
||||||
|
* because of [FeedLoadManager.BUFFER_COUNT_BEFORE_INSERT]. Therefore this class should contain
|
||||||
|
* as little data as possible to avoid out of memory errors. In particular, avoid storing whole
|
||||||
|
* [ChannelInfo] objects, as they might contain raw JSON info in ready channel tabs link handlers.
|
||||||
|
*/
|
||||||
data class FeedUpdateInfo(
|
data class FeedUpdateInfo(
|
||||||
val uid: Long,
|
val uid: Long,
|
||||||
@NotificationMode
|
@NotificationMode
|
||||||
val notificationMode: Int,
|
val notificationMode: Int,
|
||||||
val name: String,
|
val name: String,
|
||||||
val avatarUrl: String,
|
val avatarUrl: String,
|
||||||
val originalInfo: Info,
|
val url: String,
|
||||||
|
val serviceId: Int,
|
||||||
|
// description and subscriberCount are null if the constructor info is from the fast feed method
|
||||||
|
val description: String?,
|
||||||
|
val subscriberCount: Long?,
|
||||||
val streams: List<StreamInfoItem>,
|
val streams: List<StreamInfoItem>,
|
||||||
val errors: List<Throwable>,
|
val errors: List<Throwable>,
|
||||||
) {
|
) {
|
||||||
constructor(
|
constructor(
|
||||||
subscription: SubscriptionEntity,
|
subscription: SubscriptionEntity,
|
||||||
originalInfo: Info,
|
info: Info,
|
||||||
streams: List<StreamInfoItem>,
|
streams: List<StreamInfoItem>,
|
||||||
errors: List<Throwable>,
|
errors: List<Throwable>,
|
||||||
) : this(
|
) : this(
|
||||||
uid = subscription.uid,
|
uid = subscription.uid,
|
||||||
notificationMode = subscription.notificationMode,
|
notificationMode = subscription.notificationMode,
|
||||||
name = subscription.name,
|
name = info.name,
|
||||||
avatarUrl = subscription.avatarUrl,
|
avatarUrl = (info as? ChannelInfo)?.avatars?.let {
|
||||||
originalInfo = originalInfo,
|
// if the newly fetched info is not from fast feed, then it contains updated avatars
|
||||||
|
ImageStrategy.imageListToDbUrl(it)
|
||||||
|
} ?: subscription.avatarUrl,
|
||||||
|
url = info.url,
|
||||||
|
serviceId = info.serviceId,
|
||||||
|
// there is no description and subscriberCount in the fast feed
|
||||||
|
description = (info as? ChannelInfo)?.description,
|
||||||
|
subscriberCount = (info as? ChannelInfo)?.subscriberCount,
|
||||||
streams = streams,
|
streams = streams,
|
||||||
errors = errors,
|
errors = errors,
|
||||||
)
|
)
|
||||||
@ -34,7 +53,7 @@ data class FeedUpdateInfo(
|
|||||||
* Integer id, can be used as notification id, etc.
|
* Integer id, can be used as notification id, etc.
|
||||||
*/
|
*/
|
||||||
val pseudoId: Int
|
val pseudoId: Int
|
||||||
get() = originalInfo.url.hashCode()
|
get() = url.hashCode()
|
||||||
|
|
||||||
lateinit var newStreams: List<StreamInfoItem>
|
lateinit var newStreams: List<StreamInfoItem>
|
||||||
}
|
}
|
||||||
|
@ -8,7 +8,7 @@ import org.schabi.newpipe.database.playlist.PlaylistDuplicatesEntry;
|
|||||||
import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry;
|
import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry;
|
||||||
import org.schabi.newpipe.local.LocalItemBuilder;
|
import org.schabi.newpipe.local.LocalItemBuilder;
|
||||||
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
||||||
import org.schabi.newpipe.util.PicassoHelper;
|
import org.schabi.newpipe.util.image.PicassoHelper;
|
||||||
import org.schabi.newpipe.util.Localization;
|
import org.schabi.newpipe.util.Localization;
|
||||||
|
|
||||||
import java.time.format.DateTimeFormatter;
|
import java.time.format.DateTimeFormatter;
|
||||||
|
@ -16,7 +16,7 @@ import org.schabi.newpipe.local.LocalItemBuilder;
|
|||||||
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
||||||
import org.schabi.newpipe.util.DependentPreferenceHelper;
|
import org.schabi.newpipe.util.DependentPreferenceHelper;
|
||||||
import org.schabi.newpipe.util.Localization;
|
import org.schabi.newpipe.util.Localization;
|
||||||
import org.schabi.newpipe.util.PicassoHelper;
|
import org.schabi.newpipe.util.image.PicassoHelper;
|
||||||
import org.schabi.newpipe.util.ServiceHelper;
|
import org.schabi.newpipe.util.ServiceHelper;
|
||||||
import org.schabi.newpipe.views.AnimatedProgressBar;
|
import org.schabi.newpipe.views.AnimatedProgressBar;
|
||||||
|
|
||||||
|
@ -16,7 +16,7 @@ import org.schabi.newpipe.local.LocalItemBuilder;
|
|||||||
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
||||||
import org.schabi.newpipe.util.DependentPreferenceHelper;
|
import org.schabi.newpipe.util.DependentPreferenceHelper;
|
||||||
import org.schabi.newpipe.util.Localization;
|
import org.schabi.newpipe.util.Localization;
|
||||||
import org.schabi.newpipe.util.PicassoHelper;
|
import org.schabi.newpipe.util.image.PicassoHelper;
|
||||||
import org.schabi.newpipe.util.ServiceHelper;
|
import org.schabi.newpipe.util.ServiceHelper;
|
||||||
import org.schabi.newpipe.views.AnimatedProgressBar;
|
import org.schabi.newpipe.views.AnimatedProgressBar;
|
||||||
|
|
||||||
|
@ -8,7 +8,7 @@ import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity;
|
|||||||
import org.schabi.newpipe.local.LocalItemBuilder;
|
import org.schabi.newpipe.local.LocalItemBuilder;
|
||||||
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
||||||
import org.schabi.newpipe.util.Localization;
|
import org.schabi.newpipe.util.Localization;
|
||||||
import org.schabi.newpipe.util.PicassoHelper;
|
import org.schabi.newpipe.util.image.PicassoHelper;
|
||||||
import org.schabi.newpipe.util.ServiceHelper;
|
import org.schabi.newpipe.util.ServiceHelper;
|
||||||
|
|
||||||
import java.time.format.DateTimeFormatter;
|
import java.time.format.DateTimeFormatter;
|
||||||
|
@ -41,6 +41,7 @@ import org.schabi.newpipe.databinding.PlaylistControlBinding;
|
|||||||
import org.schabi.newpipe.error.ErrorInfo;
|
import org.schabi.newpipe.error.ErrorInfo;
|
||||||
import org.schabi.newpipe.error.UserAction;
|
import org.schabi.newpipe.error.UserAction;
|
||||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||||
|
import org.schabi.newpipe.fragments.MainFragment;
|
||||||
import org.schabi.newpipe.fragments.list.playlist.PlaylistControlViewHolder;
|
import org.schabi.newpipe.fragments.list.playlist.PlaylistControlViewHolder;
|
||||||
import org.schabi.newpipe.info_list.dialog.InfoItemDialog;
|
import org.schabi.newpipe.info_list.dialog.InfoItemDialog;
|
||||||
import org.schabi.newpipe.info_list.dialog.StreamDialogDefaultEntry;
|
import org.schabi.newpipe.info_list.dialog.StreamDialogDefaultEntry;
|
||||||
@ -51,8 +52,8 @@ import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
|
|||||||
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;
|
||||||
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 java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
@ -71,7 +72,7 @@ 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 {
|
||||||
// Save the list 10 seconds after the last change occurred
|
/** Save the list 10 seconds after the last change occurred. */
|
||||||
private static final long SAVE_DEBOUNCE_MILLIS = 10000;
|
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
|
||||||
@ -92,13 +93,20 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
|
|||||||
private PublishSubject<Long> debouncedSaveSignal;
|
private PublishSubject<Long> debouncedSaveSignal;
|
||||||
private CompositeDisposable disposables;
|
private CompositeDisposable disposables;
|
||||||
|
|
||||||
/* Has the playlist been fully loaded from db */
|
/** Whether the playlist has been fully loaded from db. */
|
||||||
private AtomicBoolean isLoadingComplete;
|
private AtomicBoolean isLoadingComplete;
|
||||||
/* Has the playlist been modified (e.g. items reordered or deleted) */
|
/** Whether the playlist has been modified (e.g. items reordered or deleted) */
|
||||||
private AtomicBoolean isModified;
|
private AtomicBoolean isModified;
|
||||||
/* Flag to prevent simultaneous rewrites of the playlist */
|
/** Flag to prevent simultaneous rewrites of the playlist. */
|
||||||
private boolean isRewritingPlaylist = false;
|
private boolean isRewritingPlaylist = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The pager adapter that the fragment is created from when it is used as frontpage, i.e.
|
||||||
|
* {@link #useAsFrontPage} is {@link true}.
|
||||||
|
*/
|
||||||
|
@Nullable
|
||||||
|
private MainFragment.SelectedTabsPagerAdapter tabsPagerAdapter = null;
|
||||||
|
|
||||||
public static LocalPlaylistFragment getInstance(final long playlistId, final String name) {
|
public static LocalPlaylistFragment getInstance(final long playlistId, final String name) {
|
||||||
final LocalPlaylistFragment instance = new LocalPlaylistFragment();
|
final LocalPlaylistFragment instance = new LocalPlaylistFragment();
|
||||||
instance.setInitialData(playlistId, name);
|
instance.setInitialData(playlistId, name);
|
||||||
@ -158,6 +166,17 @@ 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();
|
||||||
@ -291,6 +310,9 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
|
|||||||
if (disposables != null) {
|
if (disposables != null) {
|
||||||
disposables.dispose();
|
disposables.dispose();
|
||||||
}
|
}
|
||||||
|
if (tabsPagerAdapter != null) {
|
||||||
|
tabsPagerAdapter.getLocalPlaylistFragments().remove(this);
|
||||||
|
}
|
||||||
|
|
||||||
debouncedSaveSignal = null;
|
debouncedSaveSignal = null;
|
||||||
playlistManager = null;
|
playlistManager = null;
|
||||||
@ -346,7 +368,7 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
|
|||||||
@Override
|
@Override
|
||||||
public boolean onOptionsItemSelected(final MenuItem item) {
|
public boolean onOptionsItemSelected(final MenuItem item) {
|
||||||
if (item.getItemId() == R.id.menu_item_share_playlist) {
|
if (item.getItemId() == R.id.menu_item_share_playlist) {
|
||||||
sharePlaylist();
|
createShareConfirmationDialog();
|
||||||
} else if (item.getItemId() == R.id.menu_item_rename_playlist) {
|
} else if (item.getItemId() == R.id.menu_item_rename_playlist) {
|
||||||
createRenameDialog();
|
createRenameDialog();
|
||||||
} else if (item.getItemId() == R.id.menu_item_remove_watched) {
|
} else if (item.getItemId() == R.id.menu_item_remove_watched) {
|
||||||
@ -374,16 +396,33 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Share the playlist as a newline-separated list of stream URLs.
|
* Shares the playlist as a list of stream URLs if {@code shouldSharePlaylistDetails} is
|
||||||
|
* set to {@code false}. Shares the playlist name along with a list of video titles and URLs
|
||||||
|
* if {@code shouldSharePlaylistDetails} is set to {@code true}.
|
||||||
|
*
|
||||||
|
* @param shouldSharePlaylistDetails Whether the playlist details should be included in the
|
||||||
|
* shared content.
|
||||||
*/
|
*/
|
||||||
public void sharePlaylist() {
|
private void sharePlaylist(final boolean shouldSharePlaylistDetails) {
|
||||||
|
final Context context = requireContext();
|
||||||
|
|
||||||
disposables.add(playlistManager.getPlaylistStreams(playlistId)
|
disposables.add(playlistManager.getPlaylistStreams(playlistId)
|
||||||
.flatMapSingle(playlist -> Single.just(playlist.stream()
|
.flatMapSingle(playlist -> Single.just(playlist.stream()
|
||||||
.map(PlaylistStreamEntry::getStreamEntity)
|
.map(PlaylistStreamEntry::getStreamEntity)
|
||||||
.map(StreamEntity::getUrl)
|
.map(streamEntity -> {
|
||||||
|
if (shouldSharePlaylistDetails) {
|
||||||
|
return context.getString(R.string.video_details_list_item,
|
||||||
|
streamEntity.getTitle(), streamEntity.getUrl());
|
||||||
|
} else {
|
||||||
|
return streamEntity.getUrl();
|
||||||
|
}
|
||||||
|
})
|
||||||
.collect(Collectors.joining("\n"))))
|
.collect(Collectors.joining("\n"))))
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.subscribe(urlsText -> ShareUtils.shareText(requireContext(), name, urlsText),
|
.subscribe(urlsText -> ShareUtils.shareText(
|
||||||
|
context, name, shouldSharePlaylistDetails
|
||||||
|
? context.getString(R.string.share_playlist_content_details,
|
||||||
|
name, urlsText) : urlsText),
|
||||||
throwable -> showUiErrorSnackbar(this, "Sharing playlist", throwable)));
|
throwable -> showUiErrorSnackbar(this, "Sharing playlist", throwable)));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -841,5 +880,29 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
|
|||||||
}
|
}
|
||||||
return new SinglePlayQueue(streamInfoItems, index);
|
return new SinglePlayQueue(streamInfoItems, index);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a dialog to confirm whether the user wants to share the playlist
|
||||||
|
* with the playlist details or just the list of stream URLs.
|
||||||
|
* After the user has made a choice, the playlist is shared.
|
||||||
|
*/
|
||||||
|
private void createShareConfirmationDialog() {
|
||||||
|
new AlertDialog.Builder(requireContext())
|
||||||
|
.setTitle(R.string.share_playlist)
|
||||||
|
.setMessage(R.string.share_playlist_with_titles_message)
|
||||||
|
.setCancelable(true)
|
||||||
|
.setPositiveButton(R.string.share_playlist_with_titles, (dialog, which) ->
|
||||||
|
sharePlaylist(/* shouldSharePlaylistDetails= */ true)
|
||||||
|
)
|
||||||
|
.setNegativeButton(R.string.share_playlist_with_list, (dialog, which) ->
|
||||||
|
sharePlaylist(/* shouldSharePlaylistDetails= */ false)
|
||||||
|
)
|
||||||
|
.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTabsPagerAdapter(
|
||||||
|
@Nullable final MainFragment.SelectedTabsPagerAdapter tabsPagerAdapter) {
|
||||||
|
this.tabsPagerAdapter = tabsPagerAdapter;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -341,8 +341,7 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
|
|||||||
val actions = DialogInterface.OnClickListener { _, i ->
|
val actions = DialogInterface.OnClickListener { _, i ->
|
||||||
when (i) {
|
when (i) {
|
||||||
0 -> ShareUtils.shareText(
|
0 -> ShareUtils.shareText(
|
||||||
requireContext(), selectedItem.name, selectedItem.url,
|
requireContext(), selectedItem.name, selectedItem.url, selectedItem.thumbnails
|
||||||
selectedItem.thumbnailUrl
|
|
||||||
)
|
)
|
||||||
1 -> ShareUtils.openUrlInBrowser(requireContext(), selectedItem.url)
|
1 -> ShareUtils.openUrlInBrowser(requireContext(), selectedItem.url)
|
||||||
2 -> deleteChannel(selectedItem)
|
2 -> deleteChannel(selectedItem)
|
||||||
|
@ -12,13 +12,13 @@ import org.schabi.newpipe.database.stream.model.StreamEntity
|
|||||||
import org.schabi.newpipe.database.subscription.NotificationMode
|
import org.schabi.newpipe.database.subscription.NotificationMode
|
||||||
import org.schabi.newpipe.database.subscription.SubscriptionDAO
|
import org.schabi.newpipe.database.subscription.SubscriptionDAO
|
||||||
import org.schabi.newpipe.database.subscription.SubscriptionEntity
|
import org.schabi.newpipe.database.subscription.SubscriptionEntity
|
||||||
import org.schabi.newpipe.extractor.Info
|
|
||||||
import org.schabi.newpipe.extractor.channel.ChannelInfo
|
import org.schabi.newpipe.extractor.channel.ChannelInfo
|
||||||
import org.schabi.newpipe.extractor.channel.tabs.ChannelTabInfo
|
import org.schabi.newpipe.extractor.channel.tabs.ChannelTabInfo
|
||||||
import org.schabi.newpipe.extractor.feed.FeedInfo
|
|
||||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem
|
import org.schabi.newpipe.extractor.stream.StreamInfoItem
|
||||||
import org.schabi.newpipe.local.feed.FeedDatabaseManager
|
import org.schabi.newpipe.local.feed.FeedDatabaseManager
|
||||||
|
import org.schabi.newpipe.local.feed.service.FeedUpdateInfo
|
||||||
import org.schabi.newpipe.util.ExtractorHelper
|
import org.schabi.newpipe.util.ExtractorHelper
|
||||||
|
import org.schabi.newpipe.util.image.ImageStrategy
|
||||||
|
|
||||||
class SubscriptionManager(context: Context) {
|
class SubscriptionManager(context: Context) {
|
||||||
private val database = NewPipeDatabase.getInstance(context)
|
private val database = NewPipeDatabase.getInstance(context)
|
||||||
@ -71,7 +71,12 @@ class SubscriptionManager(context: Context) {
|
|||||||
subscriptionTable.getSubscription(info.serviceId, info.url)
|
subscriptionTable.getSubscription(info.serviceId, info.url)
|
||||||
.flatMapCompletable {
|
.flatMapCompletable {
|
||||||
Completable.fromRunnable {
|
Completable.fromRunnable {
|
||||||
it.setData(info.name, info.avatarUrl, info.description, info.subscriberCount)
|
it.setData(
|
||||||
|
info.name,
|
||||||
|
ImageStrategy.imageListToDbUrl(info.avatars),
|
||||||
|
info.description,
|
||||||
|
info.subscriberCount
|
||||||
|
)
|
||||||
subscriptionTable.update(it)
|
subscriptionTable.update(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -91,19 +96,15 @@ class SubscriptionManager(context: Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateFromInfo(subscriptionId: Long, info: Info) {
|
fun updateFromInfo(info: FeedUpdateInfo) {
|
||||||
val subscriptionEntity = subscriptionTable.getSubscription(subscriptionId)
|
val subscriptionEntity = subscriptionTable.getSubscription(info.uid)
|
||||||
|
|
||||||
if (info is FeedInfo) {
|
subscriptionEntity.name = info.name
|
||||||
subscriptionEntity.name = info.name
|
subscriptionEntity.avatarUrl = info.avatarUrl
|
||||||
} else if (info is ChannelInfo) {
|
|
||||||
subscriptionEntity.setData(
|
// these two fields are null if the feed info was fetched using the fast feed method
|
||||||
info.name,
|
info.description?.let { subscriptionEntity.description = it }
|
||||||
info.avatarUrl,
|
info.subscriberCount?.let { subscriptionEntity.subscriberCount = it }
|
||||||
info.description,
|
|
||||||
info.subscriberCount
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
subscriptionTable.update(subscriptionEntity)
|
subscriptionTable.update(subscriptionEntity)
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
|
@ -9,7 +9,7 @@ import org.schabi.newpipe.R
|
|||||||
import org.schabi.newpipe.extractor.channel.ChannelInfoItem
|
import org.schabi.newpipe.extractor.channel.ChannelInfoItem
|
||||||
import org.schabi.newpipe.util.Localization
|
import org.schabi.newpipe.util.Localization
|
||||||
import org.schabi.newpipe.util.OnClickGesture
|
import org.schabi.newpipe.util.OnClickGesture
|
||||||
import org.schabi.newpipe.util.PicassoHelper
|
import org.schabi.newpipe.util.image.PicassoHelper
|
||||||
|
|
||||||
class ChannelItem(
|
class ChannelItem(
|
||||||
private val infoItem: ChannelInfoItem,
|
private val infoItem: ChannelInfoItem,
|
||||||
@ -39,7 +39,7 @@ class ChannelItem(
|
|||||||
itemChannelDescriptionView.text = infoItem.description
|
itemChannelDescriptionView.text = infoItem.description
|
||||||
}
|
}
|
||||||
|
|
||||||
PicassoHelper.loadAvatar(infoItem.thumbnailUrl).into(itemThumbnailView)
|
PicassoHelper.loadAvatar(infoItem.thumbnails).into(itemThumbnailView)
|
||||||
|
|
||||||
gesturesListener?.run {
|
gesturesListener?.run {
|
||||||
viewHolder.root.setOnClickListener { selected(infoItem) }
|
viewHolder.root.setOnClickListener { selected(infoItem) }
|
||||||
|
@ -10,7 +10,7 @@ import org.schabi.newpipe.database.subscription.SubscriptionEntity
|
|||||||
import org.schabi.newpipe.databinding.PickerSubscriptionItemBinding
|
import org.schabi.newpipe.databinding.PickerSubscriptionItemBinding
|
||||||
import org.schabi.newpipe.ktx.AnimationType
|
import org.schabi.newpipe.ktx.AnimationType
|
||||||
import org.schabi.newpipe.ktx.animate
|
import org.schabi.newpipe.ktx.animate
|
||||||
import org.schabi.newpipe.util.PicassoHelper
|
import org.schabi.newpipe.util.image.PicassoHelper
|
||||||
|
|
||||||
data class PickerSubscriptionItem(
|
data class PickerSubscriptionItem(
|
||||||
val subscriptionEntity: SubscriptionEntity,
|
val subscriptionEntity: SubscriptionEntity,
|
||||||
|
@ -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"),
|
||||||
|
@ -619,11 +619,13 @@ public final class PlayQueueActivity extends AppCompatActivity
|
|||||||
|
|
||||||
final MenuItem audioTrackSelector = menu.findItem(R.id.action_audio_track);
|
final MenuItem audioTrackSelector = menu.findItem(R.id.action_audio_track);
|
||||||
final List<AudioStream> availableStreams =
|
final List<AudioStream> availableStreams =
|
||||||
Optional.ofNullable(player.getCurrentMetadata())
|
Optional.ofNullable(player)
|
||||||
|
.map(Player::getCurrentMetadata)
|
||||||
.flatMap(MediaItemTag::getMaybeAudioTrack)
|
.flatMap(MediaItemTag::getMaybeAudioTrack)
|
||||||
.map(MediaItemTag.AudioTrack::getAudioStreams)
|
.map(MediaItemTag.AudioTrack::getAudioStreams)
|
||||||
.orElse(null);
|
.orElse(null);
|
||||||
final Optional<AudioStream> selectedAudioStream = player.getSelectedAudioStream();
|
final Optional<AudioStream> selectedAudioStream = Optional.ofNullable(player)
|
||||||
|
.flatMap(Player::getSelectedAudioStream);
|
||||||
|
|
||||||
if (availableStreams == null || availableStreams.size() < 2
|
if (availableStreams == null || availableStreams.size() < 2
|
||||||
|| selectedAudioStream.isEmpty()) {
|
|| selectedAudioStream.isEmpty()) {
|
||||||
|
@ -87,6 +87,7 @@ import org.schabi.newpipe.error.ErrorInfo;
|
|||||||
import org.schabi.newpipe.error.ErrorUtil;
|
import org.schabi.newpipe.error.ErrorUtil;
|
||||||
import org.schabi.newpipe.error.UserAction;
|
import org.schabi.newpipe.error.UserAction;
|
||||||
import org.schabi.newpipe.extractor.stream.AudioStream;
|
import org.schabi.newpipe.extractor.stream.AudioStream;
|
||||||
|
import org.schabi.newpipe.extractor.Image;
|
||||||
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||||
import org.schabi.newpipe.extractor.stream.StreamType;
|
import org.schabi.newpipe.extractor.stream.StreamType;
|
||||||
import org.schabi.newpipe.extractor.stream.VideoStream;
|
import org.schabi.newpipe.extractor.stream.VideoStream;
|
||||||
@ -117,7 +118,7 @@ import org.schabi.newpipe.player.ui.VideoPlayerUi;
|
|||||||
import org.schabi.newpipe.util.DependentPreferenceHelper;
|
import org.schabi.newpipe.util.DependentPreferenceHelper;
|
||||||
import org.schabi.newpipe.util.ListHelper;
|
import org.schabi.newpipe.util.ListHelper;
|
||||||
import org.schabi.newpipe.util.NavigationHelper;
|
import org.schabi.newpipe.util.NavigationHelper;
|
||||||
import org.schabi.newpipe.util.PicassoHelper;
|
import org.schabi.newpipe.util.image.PicassoHelper;
|
||||||
import org.schabi.newpipe.util.SerializedCache;
|
import org.schabi.newpipe.util.SerializedCache;
|
||||||
import org.schabi.newpipe.util.StreamTypeUtil;
|
import org.schabi.newpipe.util.StreamTypeUtil;
|
||||||
|
|
||||||
@ -805,10 +806,10 @@ public final class Player implements PlaybackListener, Listener {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private void loadCurrentThumbnail(final String url) {
|
private void loadCurrentThumbnail(final List<Image> thumbnails) {
|
||||||
if (DEBUG) {
|
if (DEBUG) {
|
||||||
Log.d(TAG, "Thumbnail - loadCurrentThumbnail() called with url = ["
|
Log.d(TAG, "Thumbnail - loadCurrentThumbnail() called with thumbnails = ["
|
||||||
+ (url == null ? "null" : url) + "]");
|
+ thumbnails.size() + "]");
|
||||||
}
|
}
|
||||||
|
|
||||||
// first cancel any previous loading
|
// first cancel any previous loading
|
||||||
@ -817,12 +818,12 @@ public final class Player implements PlaybackListener, Listener {
|
|||||||
// Unset currentThumbnail, since it is now outdated. This ensures it is not used in media
|
// Unset currentThumbnail, since it is now outdated. This ensures it is not used in media
|
||||||
// session metadata while the new thumbnail is being loaded by Picasso.
|
// session metadata while the new thumbnail is being loaded by Picasso.
|
||||||
onThumbnailLoaded(null);
|
onThumbnailLoaded(null);
|
||||||
if (isNullOrEmpty(url)) {
|
if (thumbnails.isEmpty()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// scale down the notification thumbnail for performance
|
// scale down the notification thumbnail for performance
|
||||||
PicassoHelper.loadScaledDownThumbnail(context, url)
|
PicassoHelper.loadScaledDownThumbnail(context, thumbnails)
|
||||||
.tag(PICASSO_PLAYER_THUMBNAIL_TAG)
|
.tag(PICASSO_PLAYER_THUMBNAIL_TAG)
|
||||||
.into(currentThumbnailTarget);
|
.into(currentThumbnailTarget);
|
||||||
}
|
}
|
||||||
@ -1792,7 +1793,7 @@ public final class Player implements PlaybackListener, Listener {
|
|||||||
|
|
||||||
maybeAutoQueueNextStream(info);
|
maybeAutoQueueNextStream(info);
|
||||||
|
|
||||||
loadCurrentThumbnail(info.getThumbnailUrl());
|
loadCurrentThumbnail(info.getThumbnails());
|
||||||
registerStreamViewed();
|
registerStreamViewed();
|
||||||
|
|
||||||
notifyMetadataUpdateToListeners();
|
notifyMetadataUpdateToListeners();
|
||||||
|
@ -29,6 +29,7 @@ import android.os.IBinder;
|
|||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
import org.schabi.newpipe.player.mediasession.MediaSessionPlayerUi;
|
import org.schabi.newpipe.player.mediasession.MediaSessionPlayerUi;
|
||||||
|
import org.schabi.newpipe.player.notification.NotificationPlayerUi;
|
||||||
import org.schabi.newpipe.util.ThemeHelper;
|
import org.schabi.newpipe.util.ThemeHelper;
|
||||||
|
|
||||||
import java.lang.ref.WeakReference;
|
import java.lang.ref.WeakReference;
|
||||||
@ -59,6 +60,14 @@ public final class PlayerService extends Service {
|
|||||||
ThemeHelper.setTheme(this);
|
ThemeHelper.setTheme(this);
|
||||||
|
|
||||||
player = new Player(this);
|
player = new Player(this);
|
||||||
|
/*
|
||||||
|
Create the player notification and start immediately the service in foreground,
|
||||||
|
otherwise if nothing is played or initializing the player and its components (especially
|
||||||
|
loading stream metadata) takes a lot of time, the app would crash on Android 8+ as the
|
||||||
|
service would never be put in the foreground while we said to the system we would do so
|
||||||
|
*/
|
||||||
|
player.UIs().get(NotificationPlayerUi.class)
|
||||||
|
.ifPresent(NotificationPlayerUi::createNotificationAndStartForeground);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -68,16 +77,38 @@ public final class PlayerService extends Service {
|
|||||||
+ "], flags = [" + flags + "], startId = [" + startId + "]");
|
+ "], flags = [" + flags + "], startId = [" + startId + "]");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Be sure that the player notification is set and the service is started in foreground,
|
||||||
|
otherwise, the app may crash on Android 8+ as the service would never be put in the
|
||||||
|
foreground while we said to the system we would do so
|
||||||
|
The service is always requested to be started in foreground, so always creating a
|
||||||
|
notification if there is no one already and starting the service in foreground should
|
||||||
|
not create any issues
|
||||||
|
If the service is already started in foreground, requesting it to be started shouldn't
|
||||||
|
do anything
|
||||||
|
*/
|
||||||
|
if (player != null) {
|
||||||
|
player.UIs().get(NotificationPlayerUi.class)
|
||||||
|
.ifPresent(NotificationPlayerUi::createNotificationAndStartForeground);
|
||||||
|
}
|
||||||
|
|
||||||
if (Intent.ACTION_MEDIA_BUTTON.equals(intent.getAction())
|
if (Intent.ACTION_MEDIA_BUTTON.equals(intent.getAction())
|
||||||
&& player.getPlayQueue() == null) {
|
&& (player == null || player.getPlayQueue() == null)) {
|
||||||
// No need to process media button's actions if the player is not working, otherwise the
|
/*
|
||||||
// player service would strangely start with nothing to play
|
No need to process media button's actions if the player is not working, otherwise
|
||||||
|
the player service would strangely start with nothing to play
|
||||||
|
Stop the service in this case, which will be removed from the foreground and its
|
||||||
|
notification cancelled in its destruction
|
||||||
|
*/
|
||||||
|
stopSelf();
|
||||||
return START_NOT_STICKY;
|
return START_NOT_STICKY;
|
||||||
}
|
}
|
||||||
|
|
||||||
player.handleIntent(intent);
|
if (player != null) {
|
||||||
player.UIs().get(MediaSessionPlayerUi.class)
|
player.handleIntent(intent);
|
||||||
.ifPresent(ui -> ui.handleMediaButtonIntent(intent));
|
player.UIs().get(MediaSessionPlayerUi.class)
|
||||||
|
.ifPresent(ui -> ui.handleMediaButtonIntent(intent));
|
||||||
|
}
|
||||||
|
|
||||||
return START_NOT_STICKY;
|
return START_NOT_STICKY;
|
||||||
}
|
}
|
||||||
@ -87,7 +118,7 @@ public final class PlayerService extends Service {
|
|||||||
Log.d(TAG, "stopForImmediateReusing() called");
|
Log.d(TAG, "stopForImmediateReusing() called");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!player.exoPlayerIsNull()) {
|
if (player != null && !player.exoPlayerIsNull()) {
|
||||||
// Releases wifi & cpu, disables keepScreenOn, etc.
|
// Releases wifi & cpu, disables keepScreenOn, etc.
|
||||||
// We can't just pause the player here because it will make transition
|
// We can't just pause the player here because it will make transition
|
||||||
// from one stream to a new stream not smooth
|
// from one stream to a new stream not smooth
|
||||||
@ -98,7 +129,7 @@ public final class PlayerService extends Service {
|
|||||||
@Override
|
@Override
|
||||||
public void onTaskRemoved(final Intent rootIntent) {
|
public void onTaskRemoved(final Intent rootIntent) {
|
||||||
super.onTaskRemoved(rootIntent);
|
super.onTaskRemoved(rootIntent);
|
||||||
if (!player.videoPlayerSelected()) {
|
if (player != null && !player.videoPlayerSelected()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
onDestroy();
|
onDestroy();
|
||||||
|
@ -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)
|
||||||
|
@ -3,6 +3,7 @@ package org.schabi.newpipe.player.mediaitem;
|
|||||||
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||||
import org.schabi.newpipe.extractor.stream.StreamType;
|
import org.schabi.newpipe.extractor.stream.StreamType;
|
||||||
import org.schabi.newpipe.player.playqueue.PlayQueueItem;
|
import org.schabi.newpipe.player.playqueue.PlayQueueItem;
|
||||||
|
import org.schabi.newpipe.util.image.ImageStrategy;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
@ -74,7 +75,7 @@ public final class ExceptionTag implements MediaItemTag {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getThumbnailUrl() {
|
public String getThumbnailUrl() {
|
||||||
return item.getThumbnailUrl();
|
return ImageStrategy.choosePreferredImage(item.getThumbnails());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -81,8 +81,9 @@ public interface MediaItemTag {
|
|||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
default MediaItem asMediaItem() {
|
default MediaItem asMediaItem() {
|
||||||
|
final String thumbnailUrl = getThumbnailUrl();
|
||||||
final MediaMetadata mediaMetadata = new MediaMetadata.Builder()
|
final MediaMetadata mediaMetadata = new MediaMetadata.Builder()
|
||||||
.setArtworkUri(Uri.parse(getThumbnailUrl()))
|
.setArtworkUri(thumbnailUrl == null ? null : Uri.parse(thumbnailUrl))
|
||||||
.setArtist(getUploaderName())
|
.setArtist(getUploaderName())
|
||||||
.setDescription(getTitle())
|
.setDescription(getTitle())
|
||||||
.setDisplayTitle(getTitle())
|
.setDisplayTitle(getTitle())
|
||||||
|
@ -6,6 +6,7 @@ import org.schabi.newpipe.extractor.stream.AudioStream;
|
|||||||
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||||
import org.schabi.newpipe.extractor.stream.StreamType;
|
import org.schabi.newpipe.extractor.stream.StreamType;
|
||||||
import org.schabi.newpipe.extractor.stream.VideoStream;
|
import org.schabi.newpipe.extractor.stream.VideoStream;
|
||||||
|
import org.schabi.newpipe.util.image.ImageStrategy;
|
||||||
|
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@ -95,7 +96,7 @@ public final class StreamInfoTag implements MediaItemTag {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getThumbnailUrl() {
|
public String getThumbnailUrl() {
|
||||||
return streamInfo.getThumbnailUrl();
|
return ImageStrategy.choosePreferredImage(streamInfo.getThumbnails());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -20,6 +20,7 @@ import com.google.android.exoplayer2.util.Util;
|
|||||||
import org.schabi.newpipe.player.Player;
|
import org.schabi.newpipe.player.Player;
|
||||||
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
||||||
import org.schabi.newpipe.player.playqueue.PlayQueueItem;
|
import org.schabi.newpipe.player.playqueue.PlayQueueItem;
|
||||||
|
import org.schabi.newpipe.util.image.ImageStrategy;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
@ -137,9 +138,12 @@ public class PlayQueueNavigator implements MediaSessionConnector.QueueNavigator
|
|||||||
.putLong(MediaMetadataCompat.METADATA_KEY_NUM_TRACKS, player.getPlayQueue().size());
|
.putLong(MediaMetadataCompat.METADATA_KEY_NUM_TRACKS, player.getPlayQueue().size());
|
||||||
descBuilder.setExtras(additionalMetadata);
|
descBuilder.setExtras(additionalMetadata);
|
||||||
|
|
||||||
final Uri thumbnailUri = Uri.parse(item.getThumbnailUrl());
|
try {
|
||||||
if (thumbnailUri != null) {
|
descBuilder.setIconUri(Uri.parse(
|
||||||
descBuilder.setIconUri(thumbnailUri);
|
ImageStrategy.choosePreferredImage(item.getThumbnails())));
|
||||||
|
} catch (final Throwable e) {
|
||||||
|
// no thumbnail available at all, or the user disabled image loading,
|
||||||
|
// or the obtained url is not a valid `Uri`
|
||||||
}
|
}
|
||||||
|
|
||||||
return descBuilder.build();
|
return descBuilder.build();
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -17,7 +17,6 @@ import org.schabi.newpipe.player.helper.PlayerHelper;
|
|||||||
import org.schabi.newpipe.player.ui.PlayerUi;
|
import org.schabi.newpipe.player.ui.PlayerUi;
|
||||||
|
|
||||||
public final class NotificationPlayerUi extends PlayerUi {
|
public final class NotificationPlayerUi extends PlayerUi {
|
||||||
private boolean foregroundNotificationAlreadyCreated = false;
|
|
||||||
private final NotificationUtil notificationUtil;
|
private final NotificationUtil notificationUtil;
|
||||||
|
|
||||||
public NotificationPlayerUi(@NonNull final Player player) {
|
public NotificationPlayerUi(@NonNull final Player player) {
|
||||||
@ -25,15 +24,6 @@ public final class NotificationPlayerUi extends PlayerUi {
|
|||||||
notificationUtil = new NotificationUtil(player);
|
notificationUtil = new NotificationUtil(player);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public void initPlayer() {
|
|
||||||
super.initPlayer();
|
|
||||||
if (!foregroundNotificationAlreadyCreated) {
|
|
||||||
notificationUtil.createNotificationAndStartForeground();
|
|
||||||
foregroundNotificationAlreadyCreated = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void destroy() {
|
public void destroy() {
|
||||||
super.destroy();
|
super.destroy();
|
||||||
@ -122,4 +112,8 @@ public final class NotificationPlayerUi extends PlayerUi {
|
|||||||
super.onPlayQueueEdited();
|
super.onPlayQueueEdited();
|
||||||
notificationUtil.createNotificationIfNeededAndUpdate(false);
|
notificationUtil.createNotificationIfNeededAndUpdate(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void createNotificationAndStartForeground() {
|
||||||
|
notificationUtil.createNotificationAndStartForeground();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3,12 +3,14 @@ package org.schabi.newpipe.player.playqueue;
|
|||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
|
import org.schabi.newpipe.extractor.Image;
|
||||||
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||||
import org.schabi.newpipe.extractor.stream.StreamType;
|
import org.schabi.newpipe.extractor.stream.StreamType;
|
||||||
import org.schabi.newpipe.util.ExtractorHelper;
|
import org.schabi.newpipe.util.ExtractorHelper;
|
||||||
|
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
import io.reactivex.rxjava3.core.Single;
|
import io.reactivex.rxjava3.core.Single;
|
||||||
import io.reactivex.rxjava3.schedulers.Schedulers;
|
import io.reactivex.rxjava3.schedulers.Schedulers;
|
||||||
@ -24,7 +26,7 @@ public class PlayQueueItem implements Serializable {
|
|||||||
private final int serviceId;
|
private final int serviceId;
|
||||||
private final long duration;
|
private final long duration;
|
||||||
@NonNull
|
@NonNull
|
||||||
private final String thumbnailUrl;
|
private final List<Image> thumbnails;
|
||||||
@NonNull
|
@NonNull
|
||||||
private final String uploader;
|
private final String uploader;
|
||||||
private final String uploaderUrl;
|
private final String uploaderUrl;
|
||||||
@ -38,7 +40,7 @@ public class PlayQueueItem implements Serializable {
|
|||||||
|
|
||||||
PlayQueueItem(@NonNull final StreamInfo info) {
|
PlayQueueItem(@NonNull final StreamInfo info) {
|
||||||
this(info.getName(), info.getUrl(), info.getServiceId(), info.getDuration(),
|
this(info.getName(), info.getUrl(), info.getServiceId(), info.getDuration(),
|
||||||
info.getThumbnailUrl(), info.getUploaderName(),
|
info.getThumbnails(), info.getUploaderName(),
|
||||||
info.getUploaderUrl(), info.getStreamType());
|
info.getUploaderUrl(), info.getStreamType());
|
||||||
|
|
||||||
if (info.getStartPosition() > 0) {
|
if (info.getStartPosition() > 0) {
|
||||||
@ -48,20 +50,20 @@ public class PlayQueueItem implements Serializable {
|
|||||||
|
|
||||||
PlayQueueItem(@NonNull final StreamInfoItem item) {
|
PlayQueueItem(@NonNull final StreamInfoItem item) {
|
||||||
this(item.getName(), item.getUrl(), item.getServiceId(), item.getDuration(),
|
this(item.getName(), item.getUrl(), item.getServiceId(), item.getDuration(),
|
||||||
item.getThumbnailUrl(), item.getUploaderName(),
|
item.getThumbnails(), item.getUploaderName(),
|
||||||
item.getUploaderUrl(), item.getStreamType());
|
item.getUploaderUrl(), item.getStreamType());
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings("ParameterNumber")
|
@SuppressWarnings("ParameterNumber")
|
||||||
private PlayQueueItem(@Nullable final String name, @Nullable final String url,
|
private PlayQueueItem(@Nullable final String name, @Nullable final String url,
|
||||||
final int serviceId, final long duration,
|
final int serviceId, final long duration,
|
||||||
@Nullable final String thumbnailUrl, @Nullable final String uploader,
|
final List<Image> thumbnails, @Nullable final String uploader,
|
||||||
final String uploaderUrl, @NonNull final StreamType streamType) {
|
final String uploaderUrl, @NonNull final StreamType streamType) {
|
||||||
this.title = name != null ? name : EMPTY_STRING;
|
this.title = name != null ? name : EMPTY_STRING;
|
||||||
this.url = url != null ? url : EMPTY_STRING;
|
this.url = url != null ? url : EMPTY_STRING;
|
||||||
this.serviceId = serviceId;
|
this.serviceId = serviceId;
|
||||||
this.duration = duration;
|
this.duration = duration;
|
||||||
this.thumbnailUrl = thumbnailUrl != null ? thumbnailUrl : EMPTY_STRING;
|
this.thumbnails = thumbnails;
|
||||||
this.uploader = uploader != null ? uploader : EMPTY_STRING;
|
this.uploader = uploader != null ? uploader : EMPTY_STRING;
|
||||||
this.uploaderUrl = uploaderUrl;
|
this.uploaderUrl = uploaderUrl;
|
||||||
this.streamType = streamType;
|
this.streamType = streamType;
|
||||||
@ -88,8 +90,8 @@ public class PlayQueueItem implements Serializable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
public String getThumbnailUrl() {
|
public List<Image> getThumbnails() {
|
||||||
return thumbnailUrl;
|
return thumbnails;
|
||||||
}
|
}
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
|
@ -6,7 +6,7 @@ import android.view.MotionEvent;
|
|||||||
import android.view.View;
|
import android.view.View;
|
||||||
|
|
||||||
import org.schabi.newpipe.util.Localization;
|
import org.schabi.newpipe.util.Localization;
|
||||||
import org.schabi.newpipe.util.PicassoHelper;
|
import org.schabi.newpipe.util.image.PicassoHelper;
|
||||||
import org.schabi.newpipe.util.ServiceHelper;
|
import org.schabi.newpipe.util.ServiceHelper;
|
||||||
|
|
||||||
public class PlayQueueItemBuilder {
|
public class PlayQueueItemBuilder {
|
||||||
@ -33,7 +33,7 @@ public class PlayQueueItemBuilder {
|
|||||||
holder.itemDurationView.setVisibility(View.GONE);
|
holder.itemDurationView.setVisibility(View.GONE);
|
||||||
}
|
}
|
||||||
|
|
||||||
PicassoHelper.loadThumbnail(item.getThumbnailUrl()).into(holder.itemThumbnailView);
|
PicassoHelper.loadThumbnail(item.getThumbnails()).into(holder.itemThumbnailView);
|
||||||
|
|
||||||
holder.itemRoot.setOnClickListener(view -> {
|
holder.itemRoot.setOnClickListener(view -> {
|
||||||
if (onItemClickListener != null) {
|
if (onItemClickListener != null) {
|
||||||
|
@ -14,7 +14,7 @@ import androidx.collection.SparseArrayCompat;
|
|||||||
import com.google.common.base.Stopwatch;
|
import com.google.common.base.Stopwatch;
|
||||||
|
|
||||||
import org.schabi.newpipe.extractor.stream.Frameset;
|
import org.schabi.newpipe.extractor.stream.Frameset;
|
||||||
import org.schabi.newpipe.util.PicassoHelper;
|
import org.schabi.newpipe.util.image.PicassoHelper;
|
||||||
|
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
@ -740,7 +740,7 @@ public final class MainPlayerUi extends VideoPlayerUi implements View.OnLayoutCh
|
|||||||
String videoUrl = player.getVideoUrl();
|
String videoUrl = player.getVideoUrl();
|
||||||
videoUrl += ("&t=" + seconds);
|
videoUrl += ("&t=" + seconds);
|
||||||
ShareUtils.shareText(context, currentItem.getTitle(),
|
ShareUtils.shareText(context, currentItem.getTitle(),
|
||||||
videoUrl, currentItem.getThumbnailUrl());
|
videoUrl, currentItem.getThumbnails());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user