mirror of
https://github.com/TeamNewPipe/NewPipe
synced 2026-01-14 21:48:00 +00:00
Compare commits
222 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e0ba9b3902 | ||
|
|
f11b5ae7a1 | ||
|
|
7baeb6eca7 | ||
|
|
4e8519a1b9 | ||
|
|
e6fffc0d5b | ||
|
|
4c5fc7fa7c | ||
|
|
b633108a4c | ||
|
|
3d42da5ff5 | ||
|
|
1b869199f4 | ||
|
|
f3cd2f6c9d | ||
|
|
2e3e7f9bf2 | ||
|
|
92327dd9e3 | ||
|
|
d40b432f46 | ||
|
|
5b3137093f | ||
|
|
4fc9f2e5fd | ||
|
|
ce592f4baf | ||
|
|
2b3edcf2d1 | ||
|
|
f165f97bd9 | ||
|
|
4ec572372e | ||
|
|
672eb34049 | ||
|
|
a0b042091b | ||
|
|
b753705a84 | ||
|
|
f48ff610a3 | ||
|
|
93aed9f34c | ||
|
|
3cf94382e6 | ||
|
|
f52cb3bbe0 | ||
|
|
d45182cb5c | ||
|
|
22847c6c92 | ||
|
|
a70c51b71c | ||
|
|
02d417476e | ||
|
|
bc3139e5f9 | ||
|
|
c1f7b2653c | ||
|
|
72dbb9441e | ||
|
|
bbc13756f3 | ||
|
|
ba0876b43b | ||
|
|
c0d41661e8 | ||
|
|
b2e2551e33 | ||
|
|
ac371e6fb4 | ||
|
|
a225ac5deb | ||
|
|
920695f90a | ||
|
|
49fc57eee9 | ||
|
|
b61d44aaa6 | ||
|
|
f36fd2f7b2 | ||
|
|
7e26748dc4 | ||
|
|
ba6fdecbae | ||
|
|
f791e83380 | ||
|
|
dd7f914b8d | ||
|
|
7667b2ce59 | ||
|
|
8272b2508b | ||
|
|
70354eb73e | ||
|
|
63083ac0c3 | ||
|
|
9346f9b0f3 | ||
|
|
605e5d265c | ||
|
|
25456b15e7 | ||
|
|
ebbe7ef944 | ||
|
|
60a272e70a | ||
|
|
672fcb9ce3 | ||
|
|
870d50ebcd | ||
|
|
b62b3e91a0 | ||
|
|
b022d90303 | ||
|
|
02af529551 | ||
|
|
dd9cc619ed | ||
|
|
75c9e959de | ||
|
|
fb8afec1bf | ||
|
|
a2887034a6 | ||
|
|
7eb5aa1bc5 | ||
|
|
08ebd7d39a | ||
|
|
9ea263f72e | ||
|
|
e4a2d2f3c1 | ||
|
|
892b4a15f6 | ||
|
|
fda0a550fd | ||
|
|
638825cdff | ||
|
|
6a1d81fcf3 | ||
|
|
8afd44a72f | ||
|
|
22c5135740 | ||
|
|
4d51ebc37a | ||
|
|
433c6dc33b | ||
|
|
ed4fdadd4d | ||
|
|
298e96b821 | ||
|
|
9006667b4d | ||
|
|
abbf71982d | ||
|
|
57110717d3 | ||
|
|
c3b5444281 | ||
|
|
7a542975ca | ||
|
|
490aff5846 | ||
|
|
1dfc036ead | ||
|
|
360d6b998c | ||
|
|
be7307cf39 | ||
|
|
12096ab050 | ||
|
|
225f23ce02 | ||
|
|
9c15ee7285 | ||
|
|
8dd617fc6b | ||
|
|
ae8e72f34b | ||
|
|
fc52a6e871 | ||
|
|
722b47b86f | ||
|
|
3a09039b93 | ||
|
|
669a35bc78 | ||
|
|
81fa0c1558 | ||
|
|
ed408b2094 | ||
|
|
3bc661f583 | ||
|
|
cf9b482be2 | ||
|
|
1d935b46f9 | ||
|
|
520ac2e935 | ||
|
|
c6316abbce | ||
|
|
2dfe837c35 | ||
|
|
3c2ea7697c | ||
|
|
faa7a91764 | ||
|
|
f629a4d206 | ||
|
|
4b7c37e919 | ||
|
|
a4c9732916 | ||
|
|
f8f2dfce4b | ||
|
|
5284072b8d | ||
|
|
e603dddc54 | ||
|
|
15691ba41a | ||
|
|
a555aab3e7 | ||
|
|
88f1c3a808 | ||
|
|
0e6668636d | ||
|
|
d0f4d8b132 | ||
|
|
cfdcb92fa3 | ||
|
|
039bd5d413 | ||
|
|
5ffba55b4a | ||
|
|
57ca281c80 | ||
|
|
46f74b908a | ||
|
|
703f1550d8 | ||
|
|
8bfd380b89 | ||
|
|
43e91ae4ae | ||
|
|
023a2c1d9c | ||
|
|
d931d058d9 | ||
|
|
a825253b7f | ||
|
|
d9086300f3 | ||
|
|
f18a7c91ca | ||
|
|
556aad0114 | ||
|
|
05f6ea6401 | ||
|
|
43d0543b9f | ||
|
|
e95637f7b7 | ||
|
|
4cd7c42b9e | ||
|
|
0787d62254 | ||
|
|
b061423847 | ||
|
|
dbd90299bd | ||
|
|
1faf1b261c | ||
|
|
c6ead351c0 | ||
|
|
bbcfdf2969 | ||
|
|
36e72d5a41 | ||
|
|
f8297a8a9b | ||
|
|
a4503eb609 | ||
|
|
a1cb3e59d6 | ||
|
|
ef94458249 | ||
|
|
1b05c404d5 | ||
|
|
5de455bb86 | ||
|
|
acdfee5c25 | ||
|
|
a6d6ed6474 | ||
|
|
87e7d95966 | ||
|
|
d37ee1e0dc | ||
|
|
1d33e7ab49 | ||
|
|
2027b743b4 | ||
|
|
7e27e73532 | ||
|
|
3705a1adad | ||
|
|
793b88a7d4 | ||
|
|
2928df0cc9 | ||
|
|
4f5e772157 | ||
|
|
f7a0b9951e | ||
|
|
44128f9145 | ||
|
|
6eaff5ca6a | ||
|
|
c0664c1cb6 | ||
|
|
e229e5355d | ||
|
|
52189fc5df | ||
|
|
314964c5f9 | ||
|
|
fcef783bbb | ||
|
|
9c5ac069d7 | ||
|
|
160f9df64e | ||
|
|
bdbb9bead2 | ||
|
|
e4dfce9ee2 | ||
|
|
6fbb601802 | ||
|
|
bf029ddd9f | ||
|
|
af5f0c042a | ||
|
|
b566355c4f | ||
|
|
5c31dff72d | ||
|
|
d69672e113 | ||
|
|
a209e87c69 | ||
|
|
71610a365f | ||
|
|
44860f2ea7 | ||
|
|
967bdf8f08 | ||
|
|
02aa6fcab0 | ||
|
|
712985ced1 | ||
|
|
0683dafa55 | ||
|
|
6f1958d398 | ||
|
|
85fbd2560d | ||
|
|
65f2730261 | ||
|
|
21bcadeecb | ||
|
|
bd0427c79f | ||
|
|
241054fd26 | ||
|
|
d8888e3495 | ||
|
|
137d9e6d6e | ||
|
|
d0cbd1e663 | ||
|
|
da51e1ed72 | ||
|
|
76803bfcb1 | ||
|
|
c248741c00 | ||
|
|
759a078ce0 | ||
|
|
a536311d56 | ||
|
|
4d50a66e40 | ||
|
|
e6c56cacc6 | ||
|
|
c3b9465aa3 | ||
|
|
5f3b8bea52 | ||
|
|
0e4c8ea8af | ||
|
|
f9ab23bb4a | ||
|
|
9f8b2264a2 | ||
|
|
52cc3f10c1 | ||
|
|
1d61bb58f5 | ||
|
|
51c60e5261 | ||
|
|
12e46e0a36 | ||
|
|
f8caed139a | ||
|
|
26c39381a8 | ||
|
|
a4742ad9e9 | ||
|
|
32dffb577c | ||
|
|
9458b9f37d | ||
|
|
79102a20d2 | ||
|
|
0782410a14 | ||
|
|
f5d015e8f9 | ||
|
|
f00cffd17e | ||
|
|
7fdb6e1425 | ||
|
|
621f049a5c | ||
|
|
eb6968fb3f |
2
.github/ISSUE_TEMPLATE/bug_report.md
vendored
2
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -57,7 +57,7 @@ Oh no, a bug! It happens. Thanks for reporting an issue with NewPipe. To make it
|
||||
|
||||
|
||||
|
||||
<!-- Please fill this out when you do not provide a log generate by NewPipe -->
|
||||
<!-- Please fill this section if you did not provide a log generated by NewPipe -->
|
||||
|
||||
### Device info
|
||||
|
||||
|
||||
34
.github/ISSUE_TEMPLATE/feature_request.md
vendored
34
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@@ -5,10 +5,9 @@ labels: enhancement
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
<!-- IF YOU DON'T FILL IN THE TEMPLATE PROPERLY, YOUR ISSUE IS LIABLE TO BE CLOSED. If you feel tired/lazy right now, open your issue some other time. We'll wait. -->
|
||||
<!-- IF YOU DON'T FILL IN THE TEMPLATE PROPERLY, YOUR ISSUE IS LIABLE TO BE CLOSED. If you are currently unable to do so for any reason, open your issue some other time. We'll wait. -->
|
||||
|
||||
|
||||
<!-- The comments between these brackets won't show up in the submitted issue (as you can see in the Preview). -->
|
||||
<!-- The comments between these brackets won't show up in the submitted issue (as you can see in the Preview tab). -->
|
||||
|
||||
### Checklist
|
||||
<!-- This checklist is COMPULSORY. The first box has been checked for you to show you how it is done. -->
|
||||
@@ -17,30 +16,9 @@ assignees: ''
|
||||
- [ ] I have read the contribution guidelines given at https://github.com/TeamNewPipe/NewPipe/blob/HEAD/.github/CONTRIBUTING.md.
|
||||
- [ ] This issue contains only one feature request. I will open one issue for every feature I want to request.
|
||||
|
||||
|
||||
#### Describe the feature you want
|
||||
<!-- A clear and concise description of what you wish should happen.
|
||||
Example: *I think it would be nice if you add feature Y which makes X possible.*
|
||||
|
||||
Optionally, also describe alternatives you've considered.
|
||||
Example: *Z is also a good alternative. Not as good as Y, but at least...* or *I considered Z, but that didn't turn out to be a good idea because...* -->
|
||||
#### What feature do you want?
|
||||
<!-- Explain how you want the app's look or behavior to change to suit your needs. -->
|
||||
|
||||
|
||||
|
||||
#### Is your feature request related to a problem? Please describe it
|
||||
<!-- A clear and concise description of what the problem is. Maybe the developers and the community could brainstorm and come up with a better solution to your problem. If they exist, link to related Issues and/or PRs for developers to keep track easier.
|
||||
Example: *I want to do X, but there is no way to do it.* -->
|
||||
|
||||
|
||||
|
||||
#### Additional context
|
||||
<!-- Add any other context, like screenshots, about the feature request here.
|
||||
Example: *Here's a photo of my cat!* -->
|
||||
|
||||
|
||||
|
||||
#### How will you/everyone benefit from this feature?
|
||||
<!-- Convince us! How does it change your NewPipe experience and/or your life?
|
||||
The better this paragraph is, the more likely a developer will think about working on it.
|
||||
Example: *This feature will help us colonize the galaxy! -->
|
||||
|
||||
#### Why do you want this feature?
|
||||
<!-- Describe any problem or limitation you come across while using the app which would be solved by this feature. -->
|
||||
|
||||
44
.github/workflows/ci.yml
vendored
44
.github/workflows/ci.yml
vendored
@@ -1,6 +1,7 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
pull_request:
|
||||
branches:
|
||||
- dev
|
||||
@@ -32,24 +33,15 @@ jobs:
|
||||
if: github.event_name == 'pull_request'
|
||||
run: git checkout -B ${{ github.head_ref }}
|
||||
|
||||
- name: set up JDK 8
|
||||
- name: set up JDK 11
|
||||
uses: actions/setup-java@v2
|
||||
with:
|
||||
java-version: 8
|
||||
distribution: "adopt"
|
||||
|
||||
- name: Cache Gradle dependencies
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ~/.gradle/caches
|
||||
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }}
|
||||
restore-keys: ${{ runner.os }}-gradle
|
||||
java-version: 11
|
||||
distribution: "temurin"
|
||||
cache: 'gradle'
|
||||
|
||||
- name: Check if kotlin files are formatted correctly
|
||||
run: ./gradlew runKtlint
|
||||
|
||||
- name: Build debug APK and run jvm tests
|
||||
run: ./gradlew assembleDebug lintDebug testDebugUnitTest --stacktrace
|
||||
run: ./gradlew assembleDebug lintDebug testDebugUnitTest --stacktrace -DskipFormatKtlint
|
||||
|
||||
- name: Upload APK
|
||||
uses: actions/upload-artifact@v2
|
||||
@@ -67,18 +59,12 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: set up JDK 8
|
||||
- name: set up JDK 11
|
||||
uses: actions/setup-java@v2
|
||||
with:
|
||||
java-version: 8
|
||||
distribution: "adopt"
|
||||
|
||||
- name: Cache Gradle dependencies
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ~/.gradle/caches
|
||||
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }}
|
||||
restore-keys: ${{ runner.os }}-gradle
|
||||
java-version: 11
|
||||
distribution: "temurin"
|
||||
cache: 'gradle'
|
||||
|
||||
- name: Run android tests
|
||||
uses: reactivecircus/android-emulator-runner@v2
|
||||
@@ -99,7 +85,8 @@ jobs:
|
||||
# uses: actions/setup-java@v2
|
||||
# with:
|
||||
# java-version: 11 # Sonar requires JDK 11
|
||||
# distribution: "adopt"
|
||||
# distribution: "temurin"
|
||||
# cache: 'gradle'
|
||||
|
||||
# - name: Cache SonarCloud packages
|
||||
# uses: actions/cache@v2
|
||||
@@ -108,13 +95,6 @@ jobs:
|
||||
# key: ${{ runner.os }}-sonar
|
||||
# restore-keys: ${{ runner.os }}-sonar
|
||||
|
||||
# - name: Cache Gradle packages
|
||||
# uses: actions/cache@v2
|
||||
# with:
|
||||
# path: ~/.gradle/caches
|
||||
# key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }}
|
||||
# restore-keys: ${{ runner.os }}-gradle
|
||||
|
||||
# - name: Build and analyze
|
||||
# env:
|
||||
# GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any
|
||||
|
||||
20
.github/workflows/no-response.yml
vendored
Normal file
20
.github/workflows/no-response.yml
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
name: No Response
|
||||
|
||||
# Both `issue_comment` and `scheduled` event types are required for this Action
|
||||
# to work properly.
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
schedule:
|
||||
# Run daily at midnight.
|
||||
- cron: '0 0 * * *'
|
||||
|
||||
jobs:
|
||||
noResponse:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: lee-dohm/no-response@v0.5.0
|
||||
with:
|
||||
token: ${{ github.token }}
|
||||
daysUntilClose: 14
|
||||
responseRequiredLabel: waiting-for-author
|
||||
125
README.es.md
125
README.es.md
@@ -1,30 +1,30 @@
|
||||
<p align="center"><a href="https://newpipe.net"><img src="assets/new_pipe_icon_5.png" width="150"></a></p>
|
||||
<h2 align="center"><b>NewPipe</b></h2>
|
||||
<h4 align="center">Una interfaz de streaming lijera y libre para Android.</h4>
|
||||
<h4 align="center">Una interfaz de streaming ligera y libre para Android.</h4>
|
||||
|
||||
<p align="center"><a href="https://f-droid.org/packages/org.schabi.newpipe/"><img src="https://f-droid.org/wiki/images/0/06/F-Droid-button_get-it-on.png"></a></p>
|
||||
<p align="center"><a href="https://f-droid.org/packages/org.schabi.newpipe/"><img src="https://fdroid.gitlab.io/artwork/badge/get-it-on-es.svg"></a></p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/TeamNewPipe/NewPipe/releases" alt="GitHub release"><img src="https://img.shields.io/badge/Lanzamiento-v0.20.11-blue.svg" ></a>
|
||||
<a href="https://www.gnu.org/licenses/gpl-3.0" alt="License: GPLv3"><img src="https://img.shields.io/badge/Licencia-GPL%20v3-blue.svg"></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/es/" alt="Estado de la traducción"><img src="https://hosted.weblate.org/widgets/newpipe/es/svg-badge.svg"></a>
|
||||
<a href="https://web.libera.chat/#newpipe" alt="IRC channel: #newpipe"><img src="https://img.shields.io/badge/Canal%20de%20IRC%20-%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://github.com/TeamNewPipe/NewPipe/releases" alt="Lanzamientos GitHub"><img src="https://img.shields.io/github/release/TeamNewPipe/NewPipe.svg" ></a>
|
||||
<a href="https://www.gnu.org/licenses/gpl-3.0" alt="Licencia: GPLv3"><img src="https://img.shields.io/badge/Licencia-GPL%20v3-blue.svg"></a>
|
||||
<a href="https://github.com/TeamNewPipe/NewPipe/actions" alt="Estado del Build"><img src="https://github.com/TeamNewPipe/NewPipe/workflows/CI/badge.svg?branch=dev&event=push"></a>
|
||||
<a href="https://hosted.weblate.org/engage/newpipe/es/" alt="Estado de la Traducción"><img src="https://hosted.weblate.org/widgets/newpipe/es/svg-badge.svg"></a>
|
||||
<a href="https://web.libera.chat/#newpipe" alt="Canal de IRC: #newpipe"><img src="https://img.shields.io/badge/Canal%20de%20IRC%20-%23newpipe-brightgreen.svg"></a>
|
||||
<a href="https://www.bountysource.com/teams/newpipe" alt="Recompensas en Bountysource"><img src="https://img.shields.io/bountysource/team/newpipe/activity.svg?colorB=cd201f"></a>
|
||||
</p>
|
||||
<hr>
|
||||
|
||||
<p align="center"><a href="#capturas-de-pantalla">Capturas de pantalla</a> • <a href="#descripción">Descripción</a> • <a href="#características">Características</a> • <a href="#installación-y-actualizaciones">Installación y actualizaciones</a> • <a href="#contribución">Contribución</a> • <a href="#donar">Donar</a> • <a href="#licencias">Licencias</a></p>
|
||||
<p align="center"><a href="https://newpipe.net">Sitio web</a> • <a href="https://newpipe.net/blog/">Blog</a> • <a href="https://newpipe.net/FAQ/">Preguntas Frecuentes</a> • <a href="https://newpipe.net/press/">Prensa</a></p>
|
||||
<p align="center"><a href="#capturas-de-pantalla">Capturas de Pantalla</a> • <a href="#descripción">Descripción</a> • <a href="#características">Características</a> • <a href="#instalación-y-actualizaciones">Instalación y Actualizaciones</a> • <a href="#contribución">Contribución</a> • <a href="#donar">Donar</a> • <a href="#licencia">Licencia</a></p>
|
||||
<p align="center"><a href="https://newpipe.net">Sitio Web</a> • <a href="https://newpipe.net/blog/">Blog</a> • <a href="https://newpipe.net/FAQ/">Preguntas Frecuentes</a> • <a href="https://newpipe.net/press/">Prensa</a></p>
|
||||
<hr>
|
||||
|
||||
*Lea esto en otros idiomas: [English](README.md), [한국어](README.ko.md), [Soomaali](README.so.md), [Português Brasil](README.pt_BR.md), [日本語](README.ja.md), [Română](README.ro.md), [Türkçe](README.tr.md).*
|
||||
|
||||
<b>AVISO: ESTA ES UNA VERSIÓN BETA, POR LO TANTO, PUEDE ENCONTRAR BUGS (ERRORES). SI ENCUENTRA UNO, ABRA UN ISSUE A TRAVÉS DE NUESTRO REPOSITORIO GITHUB.</b>
|
||||
<b>AVISO: ESTA ES UNA VERSIÓN BETA, POR LO TANTO, PUEDE ENCONTRAR BUGS. SI ENCUENTRA UNO ABRA UN ISSUE A TRAVÉS DE NUESTRO REPOSITORIO DE GITHUB.</b>
|
||||
|
||||
<b>COLOCAR NEWPIPE O CUALQUIER FORK (BIFURCACIÓN) REALIZADO DE ELLO EN GOOGLE PLAY STORE VIOLA SUS TÉRMINOS Y CONDICIONES.</b>
|
||||
<b>COLOCAR NEWPIPE O CUALQUIER FORK DE NEWPIPE EN LA GOOGLE PLAY STORE VIOLARÁ SUS TÉRMINOS Y CONDICIONES.</b>
|
||||
|
||||
## Capturas de pantalla
|
||||
## Capturas de Pantalla
|
||||
|
||||
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_01.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_01.png)
|
||||
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_02.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_02.png)
|
||||
@@ -40,39 +40,43 @@
|
||||
[<img src="fastlane/metadata/android/en-US/images/tenInchScreenshots/shot_12.png" width=405>](fastlane/metadata/android/en-US/images/tenInchScreenshots/shot_12.png)
|
||||
|
||||
## Descripción
|
||||
NewPipe no usa ninguna librería de framework de Google, ni la API de YouTube. Los sitios web solamente se analizan para extraer la información requerida, asi que esta app se puede usar sin los servicios de Google instalados. Además, no se necesita una cuenta de YouTube para usar NewPipe, lo cual es un software libre de copyleft.
|
||||
|
||||
NewPipe no usa ninguna librería del framework de Google, ni la API de YouTube. Los sitios web solamente se analizan para extraer la información requerida, por lo que esta app se puede usar sin los servicios de Google instalados. Además, no se necesita una cuenta de YouTube para usar NewPipe, lo cual es un software libre de copyleft.
|
||||
|
||||
### Características
|
||||
|
||||
* Buscar videos
|
||||
* No requiere inicio de sesión
|
||||
* Mostrar información general sobre videos
|
||||
* Mirar videos de YouTube
|
||||
* Escuchar audio de YouTube
|
||||
* Modo popup (reproductor flotante)
|
||||
* Elegir reproductor para mirar el video
|
||||
* Modo de solo audio en videos de YouTube
|
||||
* Modo pop-up (reproductor flotante)
|
||||
* Elegir un reproductor de video externo para mirar videos
|
||||
* Descargar videos
|
||||
* Descargar solamente audio
|
||||
* Abrir video en Kodi
|
||||
* Descargar solo audio
|
||||
* Abrir videos en Kodi
|
||||
* Mostrar videos próximos/relacionados
|
||||
* Buscar a través de YouTube en un idioma específico
|
||||
* Mirar/Bloquear materiales restringidas por edad.
|
||||
* Mirar/Bloquear videos restringidos por edad
|
||||
* Mostrar información general sobre canales
|
||||
* Buscar canales
|
||||
* Buscar de canales
|
||||
* Mirar videos de un canal
|
||||
* Apoyo Orbot/Tor (todavía no directamente)
|
||||
* Apoyo 1080p/2K/4K
|
||||
* Ver historias
|
||||
* Subscribirse a canales
|
||||
* Buscar historias
|
||||
* Buscar/mirar listas de reproducción
|
||||
* Mirar listas de reproducción en fila
|
||||
* Poner videos en fila
|
||||
* Listas locales de reproducción
|
||||
* Soporte Orbot/Tor (todavía no directamente)
|
||||
* Soporte para videos en 1080p/2K/4K
|
||||
* Historial de videos vistos
|
||||
* Suscripción a canales
|
||||
* Historial de búsquedas
|
||||
* Buscar/Mirar listas de reproducción
|
||||
* Mirar listas de reproducción en cola
|
||||
* Poner videos en cola
|
||||
* Listas de reproducción locales
|
||||
* Subtítulos
|
||||
* Apoyo de medios en directo
|
||||
* Soporte para transmisiones en vivo
|
||||
* Mostrar comentarios
|
||||
|
||||
### Servicios apoyados
|
||||
NewPipe apoya varios servicios. Nuestras [documentaciones](https://teamnewpipe.github.io/documentation/) proveen más información en como se puede agregar un servicio nuevo a la app y el extractor. Por favor contáctenos si pretende agregar uno nuevo. Actualmente los servicios apoyados son:
|
||||
### Servicios Soportados
|
||||
|
||||
NewPipe soporta varios servicios. Nuestras [documentaciones](https://teamnewpipe.github.io/documentation/) ofrecen más información sobre cómo se puede agregar un servicio nuevo a la app y al extractor. Por favor ponte en contacto con nosotros si tienes pensado agregar uno nuevo. Actualmente los servicios soportados son:
|
||||
|
||||
* YouTube
|
||||
* SoundCloud \[beta\]
|
||||
@@ -80,61 +84,60 @@ NewPipe apoya varios servicios. Nuestras [documentaciones](https://teamnewpipe.g
|
||||
* PeerTube instances \[beta\]
|
||||
* Bandcamp \[beta\]
|
||||
|
||||
<!-- Brecha escondida para mantener compatibles los enlaces viejos. -->
|
||||
<span id="actualizaciones"></span>
|
||||
## Instalación y Actualizaciones
|
||||
|
||||
## Installación y actualizaciones
|
||||
Se puede instalar NewPipe usando uno de los métodos siguientes:
|
||||
1. Agregar nuestro repositorio personalizado a F-Droid e instalarlo desde allí. Las instrucciones están aquí: https://newpipe.net/FAQ/tutorials/install-add-fdroid-repo/
|
||||
2. Descargar el archivo APK del enlace [Github Releases](https://github.com/TeamNewPipe/NewPipe/releases) e instalarlo.
|
||||
3. Actualizar a través de F-Droid. Este es el método más lento para obtener la actualización, como F-Droid debe reconocer cambios, construir el APK aparte, firmarlo con una clave, y finalmente empujar la actualización a los usuarios.
|
||||
4. Construir un APK de depuración por si mismo. Este es el modo más rápido para realizar nuevas características en su dispositivo, pero es mucho más complicado, asi que recomendamos uno de los otros métodos.
|
||||
|
||||
Recomendamos el método 1 para la mayoría de usuarios. Los APKs instalados usando método 1 o 2 son compatibles el uno con el otro, pero no con las instalaciones usando método 3. Esta es debida a la misma clave digital (la nuestra), siendo utilizado en los métodos 1 y 2, pero una clave digital diferente (la de F-Droid) siendo utilizado en el método 3. Construir un APK de depuración usando método 4 excluye una clave enteramente. Firmando con claves digitales ayuda a asegurar de que un
|
||||
usuario no esté engañado para instalar una actualización maliciosa a una app.
|
||||
1. Agregando nuestro repositorio personalizado a F-Droid e instalarlo desde allí. Las instrucciones están [aquí](https://newpipe.net/FAQ/tutorials/install-add-fdroid-repo/).
|
||||
2. Descargando el archivo APK de [aquí](https://github.com/TeamNewPipe/NewPipe/releases) y posteriormente instalarlo.
|
||||
3. Usando el repositorio oficial de F-Droid. Este es el método más lento para obtener actualizaciones, ya que F-Droid debe reconocer los cambios, construir el APK aparte, firmarlo con una clave, y finalmente publicar la actualización.
|
||||
4. Construyendo la app usted mismo. Este es el modo más rápido para obtener nuevas características en su dispositivo, pero es mucho más complicado, así que recomendamos uno de los otros métodos.
|
||||
|
||||
Mientras tanto, si quiere cambiar los fuentes por alguna razón (por ejemplo, la funcionalidad del nucleo de NewPipe se rompe y F-Droid aun no tiene la actualización), recomendamos el siguiente procedimiento:
|
||||
1. Repaldear sus datos a través de Ajustes > Contenido > Exporta base de datos para guardar su historia, subscripciones, y listas de reproducción
|
||||
2. Desinstalar NewPipe
|
||||
3. Descargar el APK del nuevo fuente e instalarlo.
|
||||
4. Importar los datos del paso 1 a través de Ajustes > Contenido > Importa base de datos.
|
||||
Recomendamos el método 1 para la mayoría de usuarios. Los APKs instalados usando método 1 y 2 son compatibles entre sí, pero no lo son con los instalados usando el método 3. Esto es debido a la clave de firmado, ya que los métodos 1 y 2 usan la misma clave (la nuestra), pero el método 3 usa una clave diferente (la de F-Droid). El método 4 excluye totalmente una clave de firmado. Las claves de firmado aseguran que el usuario no esté siendo engañado para instalar/actualizar una APK maliciosa.
|
||||
|
||||
Además, si quiere cambiar el método de instalación por alguna razón (por ejemplo: la funcionalidad del núcleo de NewPipe se rompe o F-Droid aún no publica la actualización), recomendamos el siguiente procedimiento:
|
||||
1. Respalde su información a través de Ajustes > Contenido > Exportar base de datos; esto guardará su historial (videos vistos y búsquedas), suscripciones, listas de reproducción y ajustes.
|
||||
2. Desinstale NewPipe.
|
||||
3. Descargue el APK con un método distinto e instálelo.
|
||||
4. Importe la información (la base de datos extraída del paso 1) a través de Ajustes > Contenido > Importar base de datos. Tenga en cuenta que esta opción superpondrá su historial actual (tanto de vídeos como de búsquedas), sus listas de reproducción y (opcionalmente) sus configuraciones.
|
||||
|
||||
## Contribución
|
||||
Si tiene ideas, traducciónes, cambios de diseño, limpieza de código, o cambios grandes de código, su ayuda es siempre bienvenida.
|
||||
Cuanto más realizamos, mejor se pone la aplicación!
|
||||
|
||||
Si tiene ideas, traducciones, cambios de diseño, limpieza de código o cambios grandes de código, su ayuda es siempre bienvenida. ¡Cuanto más hagamos, NewPipe será mucho mejor!
|
||||
|
||||
Si quiere involucrarse, fíjese en nuestras [notas de contribución](.github/CONTRIBUTING.md).
|
||||
|
||||
<a href="https://hosted.weblate.org/engage/newpipe/es/">
|
||||
<img src="https://hosted.weblate.org/widgets/newpipe/es/287x66-grey.png" alt="Estado de la traducción" />
|
||||
<img src="https://hosted.weblate.org/widgets/newpipe/es/287x66-grey.png" alt="Estado de la Traducción" />
|
||||
</a>
|
||||
|
||||
## Donar
|
||||
Si le gusta el NewPipe estaremos felices con una donación. O puede enviar bitcoin o donar a través de Bountysource o Liberapay. Para obtener más información sobre como donar a NewPipe, por favor visita nuestro [sitio web](https://newpipe.net/donate).
|
||||
Si te gusta NewPipe, estaremos felices con una donación. Puede enviar bitcoin o donar a través de Bountysource o Liberapay. Visita nuestro [sitio web](https://newpipe.net/donate) para más información.
|
||||
|
||||
<table>
|
||||
<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><img src="assets/bitcoin_qr_code.png" alt="Código QR del Bitcoin" width="100px"></td>
|
||||
<td><samp>16A9J59ahMRqkLSZjhYj33n9j3fMztFxnh</samp></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="https://liberapay.com/TeamNewPipe/"><img src="https://upload.wikimedia.org/wikipedia/commons/2/27/Liberapay_logo_v2_white-on-yellow.svg" alt="Liberapay" width="80px" ></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/"><img src="assets/liberapay_qr_code.png" alt="Visita NewPipe en liberapay.com" width="100px"></a></td>
|
||||
<td><a href="https://liberapay.com/TeamNewPipe/donate"><img src="assets/liberapay_donate_button.svg" alt="Dona vía Liberapay" height="35px"></a></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>
|
||||
<td><a href="https://www.bountysource.com/teams/newpipe"><img src="assets/bountysource_qr_code.png" alt="Visita NewPipe en 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="Revisa cuántas recompensas puedes obtener."></a></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
## Política de privacidad
|
||||
El proyecto NewPipe tiene como objetivo proveer una experience privada y anónima para usar servicios de medios web.
|
||||
Por lo tanto, la app no colecciona ningunos datos sin su consentimiento. La politica de privacidad de NewPipe explica en detalle los datos enviados y almacenados cuando envia un informe de error, o comentario en nuestro blog. Puede encontrar el documento [aqui](https://newpipe.net/legal/privacy/).
|
||||
## Política de Privacidad
|
||||
|
||||
El proyecto NewPipe tiene como objetivo ofrecer una experience privada y anónima al usar servicios web multimedia. Por lo tanto, la app no recoleta ningún tipo de información sin su consentimiento. La politica de privacidad de NewPipe explica en detalle qué información es enviada y almacenada cuando envía un informe de error o comenta en [nuestro blog](https://newpipe.net/blog/). Puede encontrar el documento [aquí](https://newpipe.net/legal/privacy/).
|
||||
|
||||
## Licencia
|
||||
[](https://www.gnu.org/licenses/gpl-3.0.en.html)
|
||||
|
||||
NewPipe es Software Libre: Puede usar, estudiar, compartir, y mejorarlo a su voluntad. Especificamente puede redistribuir y/o modificarlo bajo los términos de la [GNU General Public License](https://www.gnu.org/licenses/gpl.html) como publicado por la Free Software Foundation, o versión 3 de la licencia, o (en su opción) cualquier versión posterior.
|
||||
[](https://www.gnu.org/licenses/gpl-3.0.html)
|
||||
|
||||
NewPipe es Software Libre: Puede usarlo, estudiarlo, compartirlo y mejorarlo a su voluntad. Más específicamente, puede redistribuirlo y/o modificarlo bajo los términos de la [GNU General Public License](https://www.gnu.org/licenses/gpl.html) publicada por la Free Software Foundation tanto si usa la versión 3 o posterior de la licencia.
|
||||
|
||||
@@ -89,7 +89,7 @@ NewPipe 코드의 변경이 있을 때(기능 추가 또는 버그 수정으로
|
||||
따라서 우리는 다른 방법들 중 하나를 사용하는 것을 추천합니다.
|
||||
2. 우리의 커스텀 저장소를 F-Droid에 추가하고 우리가 릴리즈를 게시하는 대로 저곳에서 릴리즈를 설치할 수 있습니다.
|
||||
이에 대한 설명서는 이곳에서 확인할 수 있습니다: https://newpipe.net/FAQ/tutorials/install-add-fdroid-repo/
|
||||
3. 우리가 릴리즈를 게시하는 대로 [Github Releases](https://github.com/TeamNewPipe/NewPipe/releases)에서 APK를 다운받고 이것을 설치할 수 있습니다.
|
||||
3. 우리가 릴리즈를 게시하는 대로 [GitHub Releases](https://github.com/TeamNewPipe/NewPipe/releases)에서 APK를 다운받고 이것을 설치할 수 있습니다.
|
||||
4. F-Droid를 통해 업데이트 할 수 있습니다. F-Droid는 변화를 인식하고, 스스로 APK를 생성하고, 이것에 서명하고, 사용자들에서 업데이트를 전달해야만 하기 때문에,
|
||||
이것은 업데이트를 받는 가장 느린 방법입니다.
|
||||
|
||||
|
||||
@@ -89,7 +89,7 @@ NewPipe supports multiple services. Our [docs](https://teamnewpipe.github.io/doc
|
||||
## Installation and updates
|
||||
You can install NewPipe using one of the following methods:
|
||||
1. Add our custom repo to F-Droid and install it from there. The instructions are here: https://newpipe.net/FAQ/tutorials/install-add-fdroid-repo/
|
||||
2. Download the APK from [Github Releases](https://github.com/TeamNewPipe/NewPipe/releases) and install it.
|
||||
2. Download the APK from [GitHub Releases](https://github.com/TeamNewPipe/NewPipe/releases) and install it.
|
||||
3. Update via F-Droid. This is the slowest method of getting updates, as F-Droid must recognize changes, build the APK itself, sign it, then push the update to users.
|
||||
4. Build a debug APK yourself. This is the fastest way to get new features on your device, but is much more complicated, so we recommend using one of the other methods.
|
||||
|
||||
|
||||
@@ -87,7 +87,7 @@ O NewPipe suporta vários serviços. Nosso [documentação](https://teamnewpipe.
|
||||
Quando uma alteração no código NewPipe (devido à adição de recursos ou fixação de bugs), eventualmente ocorrerá uma versão. Estes estão no formato x.xx.x . A fim de obter esta nova versão, você pode:
|
||||
1. Construa um APK de depuração você mesmo. Esta é a maneira mais rápida de obter novos recursos em seu dispositivo, mas é muito mais complicado, por isso recomendamos usar um dos outros métodos.
|
||||
2. Adicione nosso repo personalizado ao F-Droid e instale-o a partir daí assim que publicarmos um lançamento. As instruções estão aqui.: https://newpipe.net/FAQ/tutorials/install-add-fdroid-repo/
|
||||
3. Baixe o APK do [Github Releases](https://github.com/TeamNewPipe/NewPipe/releases) e instalá-lo assim que publicarmos um lançamento.
|
||||
3. Baixe o APK do [GitHub Releases](https://github.com/TeamNewPipe/NewPipe/releases) e instalá-lo assim que publicarmos um lançamento.
|
||||
4. Atualização via F-droid. Este é o método mais lento para obter atualizações, pois o F-Droid deve reconhecer alterações, construir o próprio APK, assiná-lo e, em seguida, enviar a atualização para os usuários.
|
||||
|
||||
Recomendamos o método 2 para a maioria dos usuários. Os APKs instalados usando o método 2 ou 3 são compatíveis entre si, mas não com aqueles instalados usando o método 4. Isso se deve à mesma chave de assinatura (nossa) sendo usada para 2 e 3, mas uma chave de assinatura diferente (F-Droid's) está sendo usada para 4. Construir um APK depuração usando o método 1 exclui totalmente uma chave. Assinar chaves ajudam a garantir que um usuário não seja enganado para instalar uma atualização maliciosa em um aplicativo.
|
||||
|
||||
@@ -89,7 +89,7 @@ NewPipe suportă servicii multiple. [Documentele](https://teamnewpipe.github.io/
|
||||
## Instalare şi actualizări
|
||||
Puteţi instala NewPipe folosind una dintre următoarele metode:
|
||||
1. Adăugaţi depozitul nostru F-droid personalizat. Instrucţiunile sunt aici: https://newpipe.net/FAQ/tutorials/install-add-fdroid-repo/
|
||||
2. Descărcaţi APK-ul din [Github Releases](https://github.com/TeamNewPipe/NewPipe/releases) şi instalaţi-l.
|
||||
2. Descărcaţi APK-ul din [GitHub Releases](https://github.com/TeamNewPipe/NewPipe/releases) şi instalaţi-l.
|
||||
3. Actualizaţi via F-Droid. Aceasta este cea mai lentă metodă de a obţine actualizări, deoarece F-Droid trebuie să recunoască schimbările, să constriască APK-ul, să îl semneze, iar apoi să îl trimită utilizatorilor.
|
||||
4. Construiţi un APK de depanare. Aceasta este cea mai rapidă metodă de a primi funcţii noi, dar este mult mai complicată, aşa că vă recomandăm să folosiţi una dintre celelalte metode.
|
||||
|
||||
|
||||
@@ -86,7 +86,7 @@ NewPipe wuxuu taageeraa adeegyo badan. [warqadan](https://teamnewpipe.github.io/
|
||||
Marka koodhka NewPipe isbadal ku dhaco (wax cusub oo lagusoo kordhiyay ama cilad bixin), ugu dambayn waxaa lasii daayaa mid cusub (Siidayn). Siidaynta qaabkeedu waa x.xx.x . Si aad midka cusub u hesho, waxaad samayn kartaa:
|
||||
1. Inaad mid cusub (APK) adigu dhisato. Tani waa mida ugu dagdag badan eed waxyaabaha cusub ku heli karto, laakiin way adagtahay, sidaa darteed waxaan soojeedinaynaa inaad isticmaasho qababka kale.
|
||||
2. Ku dar qayb gaar ah xaganaga F-Droid oo xagaas kaga shub isla markay siidayn soobaxdo. Hagitaanka xagan ka eeg: https://newpipe.net/FAQ/tutorials/install-add-fdroid-repo/
|
||||
3. Kasoo dajiso APK-ga xaga [Siidaynta Github](https://github.com/TeamNewPipe/NewPipe/releases) oo ku shubo isla markay siidayn soobaxdo.
|
||||
3. Kasoo dajiso APK-ga xaga [Siidaynta GitHub](https://github.com/TeamNewPipe/NewPipe/releases) oo ku shubo isla markay siidayn soobaxdo.
|
||||
4. Ka cusboonaysii xaga F-Droid. Tani waa mida ugu daahitaanka badan, sababtoo ah F-Droid waxay fiirin isbadalka waxayna iyadu dhisi mid (app), sixiixi, kadibna ay cusboonaysiinta usiidayn isticmaalayaasha.
|
||||
|
||||
Waxaan usoojeedinaynaa isticmaaalka qaabka 2 dadka badankood. APK-yada loogu shubo qaabka 2 ama 3 way isqaadan karaan, laakiin isma qaadan karaan kuwa loogu shubay qaabka 4. Sababtuna waxaa weeye furaha sixiixa oo iskumid ah (kaanaga weeye) oo loo isticmaalay 2 iyo 3, laakiin furo sixiixeed ka duwan (midka F-Droid) oo loo isticmaalay 4. Dhisida APK ayadoo la isticmaalayo qaabka 1 waxay gabi ahaanba ka reebtaa wax fure ah. Furayaasha sixiixa waxay xaqiijiyaan in isticmaalaha aan lagu khaldin inuu ku shubto cusboonaysiin khalad ah (wax lasoo dhexraaciyay) app-ka.
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
|
||||
<b>GOOGLE PLAY STORE'A NEWPIPE VEYA BAŞKA BİR KOPYASINI KOYMAK, PLAY STORE ŞARTLARINI VE KOŞULLARINI İHLAL EDER.</b>
|
||||
|
||||
## Ekran fotoğrafları
|
||||
## Ekran görüntüleri
|
||||
|
||||
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_01.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_01.png)
|
||||
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_02.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_02.png)
|
||||
@@ -88,7 +88,7 @@ NewPipe birden fazla hizmeti destekler. Uygulamaya ve ayıklayıcıya yeni bir h
|
||||
## Kurulum ve güncellemeler
|
||||
Aşağıdaki yöntemlerden birini kullanarak NewPipe'ı kurabilirsiniz:
|
||||
1. Özel depomuzu F-Droid'e ekleyin ve oradan yükleyin. Kılavuzu şurada bulabilirsiniz: https://newpipe.net/FAQ/tutorials/install-add-fdroid-repo/
|
||||
2. APK'yı [Github sürümlerinden](https://github.com/TeamNewPipe/NewPipe/releases) indirin ve kurun.
|
||||
2. APK'yı [GitHub sürümlerinden](https://github.com/TeamNewPipe/NewPipe/releases) indirin ve kurun.
|
||||
3. F-Droid ile güncelleyin. Bu, güncellemeleri almanın en yavaş yöntemidir, çünkü F-Droid değişiklikleri tanımalı, APK'yı kendisi oluşturmalı, imzalamalı ve ardından güncellemeyi kullanıcılara dağıtmalıdır.
|
||||
4. Kendiniz bir APK derleyin. Bu yöntem, cihazınızda yeni özellikler edinmenin en hızlı yoludur, ancak çok daha karmaşıktır, bu nedenle diğer yöntemlerden birini kullanmanızı öneririz.
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ plugins {
|
||||
|
||||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'kotlin-android'
|
||||
apply plugin: 'kotlin-android-extensions'
|
||||
apply plugin: 'kotlin-parcelize'
|
||||
apply plugin: 'kotlin-kapt'
|
||||
apply plugin: 'checkstyle'
|
||||
|
||||
@@ -17,8 +17,8 @@ android {
|
||||
resValue "string", "app_name", "NewPipe"
|
||||
minSdkVersion 19
|
||||
targetSdkVersion 29
|
||||
versionCode 975
|
||||
versionName "0.21.9"
|
||||
versionCode 977
|
||||
versionName "0.21.11"
|
||||
|
||||
multiDexEnabled true
|
||||
|
||||
@@ -54,6 +54,11 @@ android {
|
||||
// debug build. This seems to be a Gradle bug, therefore
|
||||
// TODO: update Gradle version
|
||||
release {
|
||||
if (System.properties.containsKey('packageSuffix')) {
|
||||
applicationIdSuffix System.getProperty('packageSuffix')
|
||||
resValue "string", "app_name", "NewPipe " + System.getProperty('packageSuffix')
|
||||
archivesBaseName = 'NewPipe_' + System.getProperty('packageSuffix')
|
||||
}
|
||||
minifyEnabled true
|
||||
shrinkResources false // disabled to fix F-Droid's reproducible build
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||
@@ -84,11 +89,6 @@ android {
|
||||
jvmTarget = JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
// Required and used only by groupie
|
||||
androidExtensions {
|
||||
experimental = true
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
androidTest.assets.srcDirs += files("$projectDir/schemas".toString())
|
||||
}
|
||||
@@ -107,7 +107,7 @@ ext {
|
||||
icepickVersion = '3.2.0'
|
||||
exoPlayerVersion = '2.12.3'
|
||||
googleAutoServiceVersion = '1.0'
|
||||
groupieVersion = '2.8.1'
|
||||
groupieVersion = '2.9.0'
|
||||
markwonVersion = '4.6.2'
|
||||
|
||||
leakCanaryVersion = '2.5'
|
||||
@@ -165,7 +165,10 @@ task formatKtlint(type: JavaExec) {
|
||||
}
|
||||
|
||||
afterEvaluate {
|
||||
preDebugBuild.dependsOn formatKtlint, runCheckstyle, runKtlint
|
||||
if (!System.properties.containsKey('skipFormatKtlint')) {
|
||||
preDebugBuild.dependsOn formatKtlint
|
||||
}
|
||||
preDebugBuild.dependsOn runCheckstyle, runKtlint
|
||||
}
|
||||
|
||||
sonarqube {
|
||||
@@ -186,7 +189,7 @@ dependencies {
|
||||
// name and the commit hash with the commit hash of the (pushed) commit you want to test
|
||||
// This works thanks to JitPack: https://jitpack.io/
|
||||
implementation 'com.github.TeamNewPipe:nanojson:1d9e1aea9049fc9f85e68b43ba39fe7be1c1f751'
|
||||
implementation 'com.github.TeamNewPipe:NewPipeExtractor:v0.21.9'
|
||||
implementation 'com.github.TeamNewPipe:NewPipeExtractor:v0.21.11'
|
||||
|
||||
/** Checkstyle **/
|
||||
checkstyle "com.puppycrawl.tools:checkstyle:${checkstyleVersion}"
|
||||
@@ -243,7 +246,8 @@ dependencies {
|
||||
// Circular ImageView
|
||||
implementation "de.hdodenhof:circleimageview:3.1.0"
|
||||
// Image loading
|
||||
implementation "com.nostra13.universalimageloader:universal-image-loader:1.9.5"
|
||||
//noinspection GradleDependency --> 2.8 is the last version, not 2.71828!
|
||||
implementation "com.squareup.picasso:picasso:2.8"
|
||||
|
||||
// Markdown library for Android
|
||||
implementation "io.noties.markwon:core:${markwonVersion}"
|
||||
@@ -255,6 +259,9 @@ dependencies {
|
||||
// Crash reporting
|
||||
implementation "ch.acra:acra-core:5.7.0"
|
||||
|
||||
// Properly restarting
|
||||
implementation 'com.jakewharton:process-phoenix:2.1.2'
|
||||
|
||||
// Reactive extensions for Java VM
|
||||
implementation "io.reactivex.rxjava3:rxjava:3.0.7"
|
||||
implementation "io.reactivex.rxjava3:rxandroid:3.0.0"
|
||||
|
||||
713
app/schemas/org.schabi.newpipe.database.AppDatabase/4.json
Normal file
713
app/schemas/org.schabi.newpipe.database.AppDatabase/4.json
Normal file
@@ -0,0 +1,713 @@
|
||||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 4,
|
||||
"identityHash": "d8070091972a7011bce18aed62f80b90",
|
||||
"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)",
|
||||
"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
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"columnNames": [
|
||||
"uid"
|
||||
],
|
||||
"autoGenerate": true
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_subscriptions_service_id_url",
|
||||
"unique": true,
|
||||
"columnNames": [
|
||||
"service_id",
|
||||
"url"
|
||||
],
|
||||
"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}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `creation_date` INTEGER, `service_id` INTEGER NOT NULL, `search` TEXT)",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"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
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"columnNames": [
|
||||
"id"
|
||||
],
|
||||
"autoGenerate": true
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_search_history_search",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"search"
|
||||
],
|
||||
"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": {
|
||||
"columnNames": [
|
||||
"uid"
|
||||
],
|
||||
"autoGenerate": true
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_streams_service_id_url",
|
||||
"unique": true,
|
||||
"columnNames": [
|
||||
"service_id",
|
||||
"url"
|
||||
],
|
||||
"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": {
|
||||
"columnNames": [
|
||||
"stream_id",
|
||||
"access_date"
|
||||
],
|
||||
"autoGenerate": false
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_stream_history_stream_id",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"stream_id"
|
||||
],
|
||||
"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": {
|
||||
"columnNames": [
|
||||
"stream_id"
|
||||
],
|
||||
"autoGenerate": false
|
||||
},
|
||||
"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, `thumbnail_url` TEXT)",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "uid",
|
||||
"columnName": "uid",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "name",
|
||||
"columnName": "name",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "thumbnailUrl",
|
||||
"columnName": "thumbnail_url",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"columnNames": [
|
||||
"uid"
|
||||
],
|
||||
"autoGenerate": true
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_playlists_name",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"name"
|
||||
],
|
||||
"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": {
|
||||
"columnNames": [
|
||||
"playlist_id",
|
||||
"join_index"
|
||||
],
|
||||
"autoGenerate": false
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_playlist_stream_join_playlist_id_join_index",
|
||||
"unique": true,
|
||||
"columnNames": [
|
||||
"playlist_id",
|
||||
"join_index"
|
||||
],
|
||||
"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"
|
||||
],
|
||||
"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": {
|
||||
"columnNames": [
|
||||
"uid"
|
||||
],
|
||||
"autoGenerate": true
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_remote_playlists_name",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"name"
|
||||
],
|
||||
"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"
|
||||
],
|
||||
"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": {
|
||||
"columnNames": [
|
||||
"stream_id",
|
||||
"subscription_id"
|
||||
],
|
||||
"autoGenerate": false
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_feed_subscription_id",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"subscription_id"
|
||||
],
|
||||
"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": {
|
||||
"columnNames": [
|
||||
"uid"
|
||||
],
|
||||
"autoGenerate": true
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_feed_group_sort_order",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"sort_order"
|
||||
],
|
||||
"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": {
|
||||
"columnNames": [
|
||||
"group_id",
|
||||
"subscription_id"
|
||||
],
|
||||
"autoGenerate": false
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_feed_group_subscription_join_subscription_id",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"subscription_id"
|
||||
],
|
||||
"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": {
|
||||
"columnNames": [
|
||||
"subscription_id"
|
||||
],
|
||||
"autoGenerate": false
|
||||
},
|
||||
"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, 'd8070091972a7011bce18aed62f80b90')"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,134 +0,0 @@
|
||||
package org.schabi.newpipe.database
|
||||
|
||||
import android.content.ContentValues
|
||||
import android.database.sqlite.SQLiteDatabase
|
||||
import androidx.room.Room
|
||||
import androidx.room.testing.MigrationTestHelper
|
||||
import androidx.sqlite.db.framework.FrameworkSQLiteOpenHelperFactory
|
||||
import androidx.test.core.app.ApplicationProvider
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.schabi.newpipe.extractor.stream.StreamType
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class AppDatabaseTest {
|
||||
companion object {
|
||||
private const val DEFAULT_SERVICE_ID = 0
|
||||
private const val DEFAULT_URL = "https://www.youtube.com/watch?v=cDphUib5iG4"
|
||||
private const val DEFAULT_TITLE = "Test Title"
|
||||
private val DEFAULT_TYPE = StreamType.VIDEO_STREAM
|
||||
private const val DEFAULT_DURATION = 480L
|
||||
private const val DEFAULT_UPLOADER_NAME = "Uploader Test"
|
||||
private const val DEFAULT_THUMBNAIL = "https://example.com/example.jpg"
|
||||
|
||||
private const val DEFAULT_SECOND_SERVICE_ID = 0
|
||||
private const val DEFAULT_SECOND_URL = "https://www.youtube.com/watch?v=ncQU6iBn5Fc"
|
||||
}
|
||||
|
||||
@get:Rule
|
||||
val testHelper = MigrationTestHelper(
|
||||
InstrumentationRegistry.getInstrumentation(),
|
||||
AppDatabase::class.java.canonicalName, FrameworkSQLiteOpenHelperFactory()
|
||||
)
|
||||
|
||||
@Test
|
||||
fun migrateDatabaseFrom2to3() {
|
||||
val databaseInV2 = testHelper.createDatabase(AppDatabase.DATABASE_NAME, Migrations.DB_VER_2)
|
||||
|
||||
databaseInV2.run {
|
||||
insert(
|
||||
"streams", SQLiteDatabase.CONFLICT_FAIL,
|
||||
ContentValues().apply {
|
||||
// put("uid", null)
|
||||
put("service_id", DEFAULT_SERVICE_ID)
|
||||
put("url", DEFAULT_URL)
|
||||
put("title", DEFAULT_TITLE)
|
||||
put("stream_type", DEFAULT_TYPE.name)
|
||||
put("duration", DEFAULT_DURATION)
|
||||
put("uploader", DEFAULT_UPLOADER_NAME)
|
||||
put("thumbnail_url", DEFAULT_THUMBNAIL)
|
||||
}
|
||||
)
|
||||
insert(
|
||||
"streams", SQLiteDatabase.CONFLICT_FAIL,
|
||||
ContentValues().apply {
|
||||
// put("uid", null)
|
||||
put("service_id", DEFAULT_SECOND_SERVICE_ID)
|
||||
put("url", DEFAULT_SECOND_URL)
|
||||
// put("title", null)
|
||||
// put("stream_type", null)
|
||||
// put("duration", null)
|
||||
// put("uploader", null)
|
||||
// put("thumbnail_url", null)
|
||||
}
|
||||
)
|
||||
insert(
|
||||
"streams", SQLiteDatabase.CONFLICT_FAIL,
|
||||
ContentValues().apply {
|
||||
// put("uid", null)
|
||||
put("service_id", DEFAULT_SERVICE_ID)
|
||||
// put("url", null)
|
||||
// put("title", null)
|
||||
// put("stream_type", null)
|
||||
// put("duration", null)
|
||||
// put("uploader", null)
|
||||
// put("thumbnail_url", null)
|
||||
}
|
||||
)
|
||||
close()
|
||||
}
|
||||
|
||||
testHelper.runMigrationsAndValidate(
|
||||
AppDatabase.DATABASE_NAME, Migrations.DB_VER_3,
|
||||
true, Migrations.MIGRATION_2_3
|
||||
)
|
||||
|
||||
val migratedDatabaseV3 = getMigratedDatabase()
|
||||
val listFromDB = migratedDatabaseV3.streamDAO().all.blockingFirst()
|
||||
|
||||
// Only expect 2, the one with the null url will be ignored
|
||||
assertEquals(2, listFromDB.size)
|
||||
|
||||
val streamFromMigratedDatabase = listFromDB[0]
|
||||
assertEquals(DEFAULT_SERVICE_ID, streamFromMigratedDatabase.serviceId)
|
||||
assertEquals(DEFAULT_URL, streamFromMigratedDatabase.url)
|
||||
assertEquals(DEFAULT_TITLE, streamFromMigratedDatabase.title)
|
||||
assertEquals(DEFAULT_TYPE, streamFromMigratedDatabase.streamType)
|
||||
assertEquals(DEFAULT_DURATION, streamFromMigratedDatabase.duration)
|
||||
assertEquals(DEFAULT_UPLOADER_NAME, streamFromMigratedDatabase.uploader)
|
||||
assertEquals(DEFAULT_THUMBNAIL, streamFromMigratedDatabase.thumbnailUrl)
|
||||
assertNull(streamFromMigratedDatabase.viewCount)
|
||||
assertNull(streamFromMigratedDatabase.textualUploadDate)
|
||||
assertNull(streamFromMigratedDatabase.uploadDate)
|
||||
assertNull(streamFromMigratedDatabase.isUploadDateApproximation)
|
||||
|
||||
val secondStreamFromMigratedDatabase = listFromDB[1]
|
||||
assertEquals(DEFAULT_SECOND_SERVICE_ID, secondStreamFromMigratedDatabase.serviceId)
|
||||
assertEquals(DEFAULT_SECOND_URL, secondStreamFromMigratedDatabase.url)
|
||||
assertEquals("", secondStreamFromMigratedDatabase.title)
|
||||
// Should fallback to VIDEO_STREAM
|
||||
assertEquals(StreamType.VIDEO_STREAM, secondStreamFromMigratedDatabase.streamType)
|
||||
assertEquals(0, secondStreamFromMigratedDatabase.duration)
|
||||
assertEquals("", secondStreamFromMigratedDatabase.uploader)
|
||||
assertEquals("", secondStreamFromMigratedDatabase.thumbnailUrl)
|
||||
assertNull(secondStreamFromMigratedDatabase.viewCount)
|
||||
assertNull(secondStreamFromMigratedDatabase.textualUploadDate)
|
||||
assertNull(secondStreamFromMigratedDatabase.uploadDate)
|
||||
assertNull(secondStreamFromMigratedDatabase.isUploadDateApproximation)
|
||||
}
|
||||
|
||||
private fun getMigratedDatabase(): AppDatabase {
|
||||
val database: AppDatabase = Room.databaseBuilder(
|
||||
ApplicationProvider.getApplicationContext(),
|
||||
AppDatabase::class.java, AppDatabase.DATABASE_NAME
|
||||
)
|
||||
.build()
|
||||
testHelper.closeWhenFinished(database)
|
||||
return database
|
||||
}
|
||||
}
|
||||
@@ -43,9 +43,11 @@ class LocalPlaylistManagerTest {
|
||||
|
||||
@Test
|
||||
fun createPlaylist() {
|
||||
val NEWPIPE_URL = "https://newpipe.net/"
|
||||
val stream = StreamEntity(
|
||||
serviceId = 1, url = "https://newpipe.net/", title = "title",
|
||||
streamType = StreamType.VIDEO_STREAM, duration = 1, uploader = "uploader"
|
||||
serviceId = 1, url = NEWPIPE_URL, title = "title",
|
||||
streamType = StreamType.VIDEO_STREAM, duration = 1, uploader = "uploader",
|
||||
uploaderUrl = NEWPIPE_URL
|
||||
)
|
||||
|
||||
val result = manager.createPlaylist("name", listOf(stream))
|
||||
@@ -69,12 +71,14 @@ class LocalPlaylistManagerTest {
|
||||
fun createPlaylist_nonExistentStreamsAreUpserted() {
|
||||
val stream = StreamEntity(
|
||||
serviceId = 1, url = "https://newpipe.net/", title = "title",
|
||||
streamType = StreamType.VIDEO_STREAM, duration = 1, uploader = "uploader"
|
||||
streamType = StreamType.VIDEO_STREAM, duration = 1, uploader = "uploader",
|
||||
uploaderUrl = "https://newpipe.net/"
|
||||
)
|
||||
database.streamDAO().insert(stream)
|
||||
val upserted = StreamEntity(
|
||||
serviceId = 1, url = "https://newpipe.net/2", title = "title2",
|
||||
streamType = StreamType.VIDEO_STREAM, duration = 1, uploader = "uploader"
|
||||
streamType = StreamType.VIDEO_STREAM, duration = 1, uploader = "uploader",
|
||||
uploaderUrl = "https://newpipe.net/"
|
||||
)
|
||||
|
||||
val result = manager.createPlaylist("name", listOf(stream, upserted))
|
||||
|
||||
@@ -5,6 +5,7 @@ import android.os.Bundle;
|
||||
import androidx.preference.Preference;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.util.PicassoHelper;
|
||||
|
||||
import leakcanary.LeakCanary;
|
||||
|
||||
@@ -15,10 +16,13 @@ public class DebugSettingsFragment extends BasePreferenceFragment {
|
||||
|
||||
final Preference showMemoryLeaksPreference
|
||||
= findPreference(getString(R.string.show_memory_leaks_key));
|
||||
final Preference showImageIndicatorsPreference
|
||||
= findPreference(getString(R.string.show_image_indicators_key));
|
||||
final Preference crashTheAppPreference
|
||||
= findPreference(getString(R.string.crash_the_app_key));
|
||||
|
||||
assert showMemoryLeaksPreference != null;
|
||||
assert showImageIndicatorsPreference != null;
|
||||
assert crashTheAppPreference != null;
|
||||
|
||||
showMemoryLeaksPreference.setOnPreferenceClickListener(preference -> {
|
||||
@@ -26,6 +30,11 @@ public class DebugSettingsFragment extends BasePreferenceFragment {
|
||||
return true;
|
||||
});
|
||||
|
||||
showImageIndicatorsPreference.setOnPreferenceChangeListener((preference, newValue) -> {
|
||||
PicassoHelper.setIndicatorsEnabled((Boolean) newValue);
|
||||
return true;
|
||||
});
|
||||
|
||||
crashTheAppPreference.setOnPreferenceClickListener(preference -> {
|
||||
throw new RuntimeException();
|
||||
});
|
||||
|
||||
@@ -146,6 +146,7 @@
|
||||
<data android:pathPrefix="/embed/" />
|
||||
<data android:pathPrefix="/watch" />
|
||||
<data android:pathPrefix="/attribution_link" />
|
||||
<data android:pathPrefix="/shorts/" />
|
||||
<!-- channel prefix -->
|
||||
<data android:pathPrefix="/channel/" />
|
||||
<data android:pathPrefix="/user/" />
|
||||
@@ -224,6 +225,7 @@
|
||||
|
||||
<data android:scheme="http" />
|
||||
<data android:scheme="https" />
|
||||
<data android:host="tubus.eduvid.org" />
|
||||
<data android:host="invidio.us" />
|
||||
<data android:host="dev.invidio.us" />
|
||||
<data android:host="www.invidio.us" />
|
||||
@@ -309,6 +311,7 @@
|
||||
<data android:scheme="http" />
|
||||
<data android:scheme="https" />
|
||||
|
||||
<data android:host="eduvid.org" />
|
||||
<data android:host="framatube.org" />
|
||||
<data android:host="media.assassinate-you.net" />
|
||||
<data android:host="peertube.co.uk" />
|
||||
@@ -358,6 +361,9 @@
|
||||
<service
|
||||
android:name=".RouterActivity$FetcherService"
|
||||
android:exported="false" />
|
||||
<service
|
||||
android:name=".CheckForNewAppVersion"
|
||||
android:exported="false" />
|
||||
|
||||
<!-- opting out of sending metrics to Google in Android System WebView -->
|
||||
<meta-data android:name="android.webkit.WebView.MetricsOptOut" android:value="true" />
|
||||
|
||||
@@ -5,15 +5,12 @@ import android.content.SharedPreferences;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.app.NotificationChannelCompat;
|
||||
import androidx.core.app.NotificationManagerCompat;
|
||||
import androidx.multidex.MultiDexApplication;
|
||||
import androidx.preference.PreferenceManager;
|
||||
|
||||
import com.nostra13.universalimageloader.cache.memory.impl.LRULimitedMemoryCache;
|
||||
import com.nostra13.universalimageloader.core.ImageLoader;
|
||||
import com.nostra13.universalimageloader.core.ImageLoaderConfiguration;
|
||||
import com.jakewharton.processphoenix.ProcessPhoenix;
|
||||
|
||||
import org.acra.ACRA;
|
||||
import org.acra.config.ACRAConfigurationException;
|
||||
@@ -28,6 +25,7 @@ import org.schabi.newpipe.extractor.downloader.Downloader;
|
||||
import org.schabi.newpipe.ktx.ExceptionUtils;
|
||||
import org.schabi.newpipe.settings.NewPipeSettings;
|
||||
import org.schabi.newpipe.util.Localization;
|
||||
import org.schabi.newpipe.util.PicassoHelper;
|
||||
import org.schabi.newpipe.util.ServiceHelper;
|
||||
import org.schabi.newpipe.util.StateSaver;
|
||||
|
||||
@@ -38,7 +36,6 @@ import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import io.reactivex.rxjava3.disposables.Disposable;
|
||||
import io.reactivex.rxjava3.exceptions.CompositeException;
|
||||
import io.reactivex.rxjava3.exceptions.MissingBackpressureException;
|
||||
import io.reactivex.rxjava3.exceptions.OnErrorNotImplementedException;
|
||||
@@ -46,6 +43,8 @@ import io.reactivex.rxjava3.exceptions.UndeliverableException;
|
||||
import io.reactivex.rxjava3.functions.Consumer;
|
||||
import io.reactivex.rxjava3.plugins.RxJavaPlugins;
|
||||
|
||||
import static org.schabi.newpipe.CheckForNewAppVersion.startNewVersionCheckService;
|
||||
|
||||
/*
|
||||
* Copyright (C) Hans-Christoph Steiner 2016 <hans@eds.org>
|
||||
* App.java is part of NewPipe.
|
||||
@@ -65,12 +64,9 @@ import io.reactivex.rxjava3.plugins.RxJavaPlugins;
|
||||
*/
|
||||
|
||||
public class App extends MultiDexApplication {
|
||||
protected static final String TAG = App.class.toString();
|
||||
private static App app;
|
||||
public static final String PACKAGE_NAME = BuildConfig.APPLICATION_ID;
|
||||
|
||||
@Nullable
|
||||
private Disposable disposable = null;
|
||||
private static final String TAG = App.class.toString();
|
||||
private static App app;
|
||||
|
||||
@NonNull
|
||||
public static App getApp() {
|
||||
@@ -89,6 +85,12 @@ public class App extends MultiDexApplication {
|
||||
|
||||
app = this;
|
||||
|
||||
if (ProcessPhoenix.isPhoenixProcess(this)) {
|
||||
Log.i(TAG, "This is a phoenix process! "
|
||||
+ "Aborting initialization of App[onCreate]");
|
||||
return;
|
||||
}
|
||||
|
||||
// Initialize settings first because others inits can use its values
|
||||
NewPipeSettings.initSettings(this);
|
||||
|
||||
@@ -103,20 +105,23 @@ public class App extends MultiDexApplication {
|
||||
ServiceHelper.initServices(this);
|
||||
|
||||
// Initialize image loader
|
||||
ImageLoader.getInstance().init(getImageLoaderConfigurations(10, 50));
|
||||
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
|
||||
PicassoHelper.init(this);
|
||||
PicassoHelper.setShouldLoadImages(
|
||||
prefs.getBoolean(getString(R.string.download_thumbnail_key), true));
|
||||
PicassoHelper.setIndicatorsEnabled(MainActivity.DEBUG
|
||||
&& prefs.getBoolean(getString(R.string.show_image_indicators_key), false));
|
||||
|
||||
configureRxJavaErrorHandler();
|
||||
|
||||
// Check for new version
|
||||
disposable = CheckForNewAppVersion.checkNewVersion(this);
|
||||
startNewVersionCheckService();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTerminate() {
|
||||
if (disposable != null) {
|
||||
disposable.dispose();
|
||||
}
|
||||
super.onTerminate();
|
||||
PicassoHelper.terminate();
|
||||
}
|
||||
|
||||
protected Downloader getDownloader() {
|
||||
@@ -201,15 +206,6 @@ public class App extends MultiDexApplication {
|
||||
});
|
||||
}
|
||||
|
||||
private ImageLoaderConfiguration getImageLoaderConfigurations(final int memoryCacheSizeMb,
|
||||
final int diskCacheSizeMb) {
|
||||
return new ImageLoaderConfiguration.Builder(this)
|
||||
.memoryCache(new LRULimitedMemoryCache(memoryCacheSizeMb * 1024 * 1024))
|
||||
.diskCacheSize(diskCacheSizeMb * 1024 * 1024)
|
||||
.imageDownloader(new ImageDownloader(getApplicationContext()))
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Called in {@link #attachBaseContext(Context)} after calling the {@code super} method.
|
||||
* Should be overridden if MultiDex is enabled, since it has to be initialized before ACRA.
|
||||
|
||||
@@ -10,16 +10,13 @@ import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
|
||||
import com.nostra13.universalimageloader.core.ImageLoader;
|
||||
|
||||
import icepick.Icepick;
|
||||
import icepick.State;
|
||||
import leakcanary.AppWatcher;
|
||||
|
||||
public abstract class BaseFragment extends Fragment {
|
||||
public static final ImageLoader IMAGE_LOADER = ImageLoader.getInstance();
|
||||
protected final String TAG = getClass().getSimpleName() + "@" + Integer.toHexString(hashCode());
|
||||
protected final boolean DEBUG = MainActivity.DEBUG;
|
||||
protected static final boolean DEBUG = MainActivity.DEBUG;
|
||||
protected AppCompatActivity activity;
|
||||
//These values are used for controlling fragments when they are part of the frontpage
|
||||
@State
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
package org.schabi.newpipe;
|
||||
|
||||
import android.app.Application;
|
||||
import android.app.IntentService;
|
||||
import android.app.PendingIntent;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.pm.PackageInfo;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.content.pm.Signature;
|
||||
import android.net.ConnectivityManager;
|
||||
@@ -16,6 +16,7 @@ import androidx.annotation.Nullable;
|
||||
import androidx.core.app.NotificationCompat;
|
||||
import androidx.core.app.NotificationManagerCompat;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.core.content.pm.PackageInfoCompat;
|
||||
import androidx.preference.PreferenceManager;
|
||||
|
||||
import com.grack.nanojson.JsonObject;
|
||||
@@ -25,8 +26,11 @@ import com.grack.nanojson.JsonParserException;
|
||||
import org.schabi.newpipe.error.ErrorActivity;
|
||||
import org.schabi.newpipe.error.ErrorInfo;
|
||||
import org.schabi.newpipe.error.UserAction;
|
||||
import org.schabi.newpipe.extractor.downloader.Response;
|
||||
import org.schabi.newpipe.extractor.exceptions.ReCaptchaException;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
@@ -34,14 +38,12 @@ import java.security.cert.CertificateEncodingException;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.security.cert.CertificateFactory;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.List;
|
||||
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.rxjava3.core.Maybe;
|
||||
import io.reactivex.rxjava3.disposables.Disposable;
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers;
|
||||
|
||||
public final class CheckForNewAppVersion {
|
||||
private CheckForNewAppVersion() { }
|
||||
public final class CheckForNewAppVersion extends IntentService {
|
||||
public CheckForNewAppVersion() {
|
||||
super("CheckForNewAppVersion");
|
||||
}
|
||||
|
||||
private static final boolean DEBUG = MainActivity.DEBUG;
|
||||
private static final String TAG = CheckForNewAppVersion.class.getSimpleName();
|
||||
@@ -58,20 +60,22 @@ public final class CheckForNewAppVersion {
|
||||
*/
|
||||
@NonNull
|
||||
private static String getCertificateSHA1Fingerprint(@NonNull final Application application) {
|
||||
final PackageInfo packageInfo;
|
||||
final List<Signature> signatures;
|
||||
try {
|
||||
packageInfo = application.getPackageManager().getPackageInfo(
|
||||
application.getPackageName(), PackageManager.GET_SIGNATURES);
|
||||
signatures = PackageInfoCompat.getSignatures(application.getPackageManager(),
|
||||
application.getPackageName());
|
||||
} catch (final PackageManager.NameNotFoundException e) {
|
||||
ErrorActivity.reportError(application, new ErrorInfo(e,
|
||||
UserAction.CHECK_FOR_NEW_APP_VERSION, "Could not find package info"));
|
||||
return "";
|
||||
}
|
||||
if (signatures.isEmpty()) {
|
||||
return "";
|
||||
}
|
||||
|
||||
final X509Certificate c;
|
||||
try {
|
||||
final Signature[] signatures = packageInfo.signatures;
|
||||
final byte[] cert = signatures[0].toByteArray();
|
||||
final byte[] cert = signatures.get(0).toByteArray();
|
||||
final InputStream input = new ByteArrayInputStream(cert);
|
||||
final CertificateFactory cf = CertificateFactory.getInstance("X509");
|
||||
c = (X509Certificate) cf.generateCertificate(input);
|
||||
@@ -165,78 +169,87 @@ public final class CheckForNewAppVersion {
|
||||
return getCertificateSHA1Fingerprint(app).equals(GITHUB_APK_SHA1);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static Disposable checkNewVersion(@NonNull final App app) {
|
||||
private void checkNewVersion() throws IOException, ReCaptchaException {
|
||||
final App app = App.getApp();
|
||||
|
||||
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(app);
|
||||
final NewVersionManager manager = new NewVersionManager();
|
||||
|
||||
// Check if user has enabled/disabled update checking
|
||||
// and if the current apk is a github one or not.
|
||||
if (!prefs.getBoolean(app.getString(R.string.update_app_key), true) || !isGithubApk(app)) {
|
||||
return null;
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the last request has happened a certain time ago
|
||||
// to reduce the number of API requests.
|
||||
final long expiry = prefs.getLong(app.getString(R.string.update_expiry_key), 0);
|
||||
if (!manager.isExpired(expiry)) {
|
||||
return null;
|
||||
return;
|
||||
}
|
||||
|
||||
return Maybe
|
||||
.fromCallable(() -> {
|
||||
if (!isConnected(app)) {
|
||||
return null;
|
||||
}
|
||||
// Make a network request to get latest NewPipe data.
|
||||
final Response response = DownloaderImpl.getInstance().get(NEWPIPE_API_URL);
|
||||
handleResponse(response, manager, prefs, app);
|
||||
}
|
||||
|
||||
// Make a network request to get latest NewPipe data.
|
||||
return DownloaderImpl.getInstance().get(NEWPIPE_API_URL);
|
||||
})
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
response -> {
|
||||
try {
|
||||
// Store a timestamp which needs to be exceeded,
|
||||
// before a new request to the API is made.
|
||||
final long newExpiry = manager
|
||||
.coerceExpiry(response.getHeader("expires"));
|
||||
prefs.edit()
|
||||
.putLong(app.getString(R.string.update_expiry_key), newExpiry)
|
||||
.apply();
|
||||
} catch (final Exception e) {
|
||||
if (DEBUG) {
|
||||
Log.w(TAG, "Could not extract and save new expiry date", e);
|
||||
}
|
||||
}
|
||||
private void handleResponse(@NonNull final Response response,
|
||||
@NonNull final NewVersionManager manager,
|
||||
@NonNull final SharedPreferences prefs,
|
||||
@NonNull final App app) {
|
||||
try {
|
||||
// Store a timestamp which needs to be exceeded,
|
||||
// before a new request to the API is made.
|
||||
final long newExpiry = manager
|
||||
.coerceExpiry(response.getHeader("expires"));
|
||||
prefs.edit()
|
||||
.putLong(app.getString(R.string.update_expiry_key), newExpiry)
|
||||
.apply();
|
||||
} catch (final Exception e) {
|
||||
if (DEBUG) {
|
||||
Log.w(TAG, "Could not extract and save new expiry date", e);
|
||||
}
|
||||
}
|
||||
|
||||
// Parse the json from the response.
|
||||
try {
|
||||
final JsonObject githubStableObject = JsonParser.object()
|
||||
.from(response.responseBody()).getObject("flavors")
|
||||
.getObject("github").getObject("stable");
|
||||
// Parse the json from the response.
|
||||
try {
|
||||
final JsonObject githubStableObject = JsonParser.object()
|
||||
.from(response.responseBody()).getObject("flavors")
|
||||
.getObject("github").getObject("stable");
|
||||
|
||||
final String versionName = githubStableObject
|
||||
.getString("version");
|
||||
final int versionCode = githubStableObject
|
||||
.getInt("version_code");
|
||||
final String apkLocationUrl = githubStableObject
|
||||
.getString("apk");
|
||||
final String versionName = githubStableObject
|
||||
.getString("version");
|
||||
final int versionCode = githubStableObject
|
||||
.getInt("version_code");
|
||||
final String apkLocationUrl = githubStableObject
|
||||
.getString("apk");
|
||||
|
||||
compareAppVersionAndShowNotification(app, versionName,
|
||||
apkLocationUrl, versionCode);
|
||||
} catch (final JsonParserException e) {
|
||||
// Most likely something is wrong in data received from NEWPIPE_API_URL.
|
||||
// Do not alarm user and fail silently.
|
||||
if (DEBUG) {
|
||||
Log.w(TAG, "Could not get NewPipe API: invalid json", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void startNewVersionCheckService() {
|
||||
final Intent intent = new Intent(App.getApp().getApplicationContext(),
|
||||
CheckForNewAppVersion.class);
|
||||
App.getApp().startService(intent);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onHandleIntent(@Nullable final Intent intent) {
|
||||
try {
|
||||
checkNewVersion();
|
||||
} catch (final IOException e) {
|
||||
Log.w(TAG, "Could not fetch NewPipe API: probably network problem", e);
|
||||
} catch (final ReCaptchaException e) {
|
||||
Log.e(TAG, "ReCaptchaException should never happen here.", e);
|
||||
}
|
||||
|
||||
compareAppVersionAndShowNotification(app, versionName,
|
||||
apkLocationUrl, versionCode);
|
||||
} catch (final JsonParserException e) {
|
||||
// connectivity problems, do not alarm user and fail silently
|
||||
if (DEBUG) {
|
||||
Log.w(TAG, "Could not get NewPipe API: invalid json", e);
|
||||
}
|
||||
}
|
||||
},
|
||||
e -> {
|
||||
// connectivity problems, do not alarm user and fail silently
|
||||
if (DEBUG) {
|
||||
Log.w(TAG, "Could not get NewPipe API: network problem", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,6 @@ import org.schabi.newpipe.util.InfoCache;
|
||||
import org.schabi.newpipe.util.TLSSocketFactoryCompat;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.security.KeyManagementException;
|
||||
import java.security.KeyStore;
|
||||
import java.security.KeyStoreException;
|
||||
@@ -194,36 +193,6 @@ public final class DownloaderImpl extends Downloader {
|
||||
}
|
||||
}
|
||||
|
||||
public InputStream stream(final String siteUrl) throws IOException {
|
||||
try {
|
||||
final okhttp3.Request.Builder requestBuilder = new okhttp3.Request.Builder()
|
||||
.method("GET", null).url(siteUrl)
|
||||
.addHeader("User-Agent", USER_AGENT);
|
||||
|
||||
final String cookies = getCookies(siteUrl);
|
||||
if (!cookies.isEmpty()) {
|
||||
requestBuilder.addHeader("Cookie", cookies);
|
||||
}
|
||||
|
||||
final okhttp3.Request request = requestBuilder.build();
|
||||
final okhttp3.Response response = client.newCall(request).execute();
|
||||
final ResponseBody body = response.body();
|
||||
|
||||
if (response.code() == 429) {
|
||||
throw new ReCaptchaException("reCaptcha Challenge requested", siteUrl);
|
||||
}
|
||||
|
||||
if (body == null) {
|
||||
response.close();
|
||||
return null;
|
||||
}
|
||||
|
||||
return body.byteStream();
|
||||
} catch (final ReCaptchaException e) {
|
||||
throw new IOException(e.getMessage(), e.getCause());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Response execute(@NonNull final Request request)
|
||||
throws IOException, ReCaptchaException {
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
package org.schabi.newpipe;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.res.Resources;
|
||||
|
||||
import androidx.preference.PreferenceManager;
|
||||
|
||||
import com.nostra13.universalimageloader.core.download.BaseImageDownloader;
|
||||
|
||||
import org.schabi.newpipe.extractor.NewPipe;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
public class ImageDownloader extends BaseImageDownloader {
|
||||
private final Resources resources;
|
||||
private final SharedPreferences preferences;
|
||||
private final String downloadThumbnailKey;
|
||||
|
||||
public ImageDownloader(final Context context) {
|
||||
super(context);
|
||||
this.resources = context.getResources();
|
||||
this.preferences = PreferenceManager.getDefaultSharedPreferences(context);
|
||||
this.downloadThumbnailKey = context.getString(R.string.download_thumbnail_key);
|
||||
}
|
||||
|
||||
private boolean isDownloadingThumbnail() {
|
||||
return preferences.getBoolean(downloadThumbnailKey, true);
|
||||
}
|
||||
|
||||
@SuppressLint("ResourceType")
|
||||
@Override
|
||||
public InputStream getStream(final String imageUri, final Object extra) throws IOException {
|
||||
if (isDownloadingThumbnail()) {
|
||||
return super.getStream(imageUri, extra);
|
||||
} else {
|
||||
return resources.openRawResource(R.drawable.dummy_thumbnail_dark);
|
||||
}
|
||||
}
|
||||
|
||||
protected InputStream getStreamFromNetwork(final String imageUri, final Object extra)
|
||||
throws IOException {
|
||||
final DownloaderImpl downloader = (DownloaderImpl) NewPipe.getDownloader();
|
||||
return downloader.stream(imageUri);
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import org.schabi.newpipe.database.AppDatabase;
|
||||
import static org.schabi.newpipe.database.AppDatabase.DATABASE_NAME;
|
||||
import static org.schabi.newpipe.database.Migrations.MIGRATION_1_2;
|
||||
import static org.schabi.newpipe.database.Migrations.MIGRATION_2_3;
|
||||
import static org.schabi.newpipe.database.Migrations.MIGRATION_3_4;
|
||||
|
||||
public final class NewPipeDatabase {
|
||||
private static volatile AppDatabase databaseInstance;
|
||||
@@ -22,7 +23,7 @@ public final class NewPipeDatabase {
|
||||
private static AppDatabase getDatabase(final Context context) {
|
||||
return Room
|
||||
.databaseBuilder(context.getApplicationContext(), AppDatabase.class, DATABASE_NAME)
|
||||
.addMigrations(MIGRATION_1_2, MIGRATION_2_3)
|
||||
.addMigrations(MIGRATION_1_2, MIGRATION_2_3, MIGRATION_3_4)
|
||||
.build();
|
||||
}
|
||||
|
||||
|
||||
70
app/src/main/java/org/schabi/newpipe/QueueItemMenuUtil.java
Normal file
70
app/src/main/java/org/schabi/newpipe/QueueItemMenuUtil.java
Normal file
@@ -0,0 +1,70 @@
|
||||
package org.schabi.newpipe;
|
||||
|
||||
import static org.schabi.newpipe.util.external_communication.ShareUtils.shareText;
|
||||
|
||||
import android.content.Context;
|
||||
import android.view.ContextThemeWrapper;
|
||||
import android.view.View;
|
||||
import android.widget.PopupMenu;
|
||||
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
|
||||
import org.schabi.newpipe.local.dialog.PlaylistAppendDialog;
|
||||
import org.schabi.newpipe.local.dialog.PlaylistCreationDialog;
|
||||
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
||||
import org.schabi.newpipe.player.playqueue.PlayQueueItem;
|
||||
import org.schabi.newpipe.util.NavigationHelper;
|
||||
|
||||
import java.util.Collections;
|
||||
|
||||
public final class QueueItemMenuUtil {
|
||||
public static void openPopupMenu(final PlayQueue playQueue,
|
||||
final PlayQueueItem item,
|
||||
final View view,
|
||||
final boolean hideDetails,
|
||||
final FragmentManager fragmentManager,
|
||||
final Context context) {
|
||||
final ContextThemeWrapper themeWrapper =
|
||||
new ContextThemeWrapper(context, R.style.DarkPopupMenu);
|
||||
|
||||
final PopupMenu popupMenu = new PopupMenu(themeWrapper, view);
|
||||
popupMenu.inflate(R.menu.menu_play_queue_item);
|
||||
|
||||
if (hideDetails) {
|
||||
popupMenu.getMenu().findItem(R.id.menu_item_details).setVisible(false);
|
||||
}
|
||||
|
||||
popupMenu.setOnMenuItemClickListener(menuItem -> {
|
||||
switch (menuItem.getItemId()) {
|
||||
case R.id.menu_item_remove:
|
||||
final int index = playQueue.indexOf(item);
|
||||
playQueue.remove(index);
|
||||
return true;
|
||||
case R.id.menu_item_details:
|
||||
// playQueue is null since we don't want any queue change
|
||||
NavigationHelper.openVideoDetail(context, item.getServiceId(),
|
||||
item.getUrl(), item.getTitle(), null,
|
||||
false);
|
||||
return true;
|
||||
case R.id.menu_item_append_playlist:
|
||||
final PlaylistAppendDialog d = PlaylistAppendDialog.fromPlayQueueItems(
|
||||
Collections.singletonList(item)
|
||||
);
|
||||
PlaylistAppendDialog.onPlaylistFound(context,
|
||||
() -> d.show(fragmentManager, "QueueItemMenuUtil@append_playlist"),
|
||||
() -> PlaylistCreationDialog.newInstance(d)
|
||||
.show(fragmentManager, "QueueItemMenuUtil@append_playlist"));
|
||||
return true;
|
||||
case R.id.menu_item_share:
|
||||
shareText(context, item.getTitle(), item.getUrl(),
|
||||
item.getThumbnailUrl());
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
popupMenu.show();
|
||||
}
|
||||
|
||||
private QueueItemMenuUtil() { }
|
||||
}
|
||||
@@ -162,10 +162,18 @@ class AboutActivity : AppCompatActivity() {
|
||||
"OkHttp", "2019", "Square, Inc.",
|
||||
"https://square.github.io/okhttp/", StandardLicenses.APACHE2
|
||||
),
|
||||
SoftwareComponent(
|
||||
"Picasso", "2013", "Square, Inc.",
|
||||
"https://square.github.io/picasso/", StandardLicenses.APACHE2
|
||||
),
|
||||
SoftwareComponent(
|
||||
"PrettyTime", "2012 - 2020", "Lincoln Baxter, III",
|
||||
"https://github.com/ocpsoft/prettytime", StandardLicenses.APACHE2
|
||||
),
|
||||
SoftwareComponent(
|
||||
"ProcessPhoenix", "2015", "Jake Wharton",
|
||||
"https://github.com/JakeWharton/ProcessPhoenix", StandardLicenses.APACHE2
|
||||
),
|
||||
SoftwareComponent(
|
||||
"RxAndroid", "2015", "The RxAndroid authors",
|
||||
"https://github.com/ReactiveX/RxAndroid", StandardLicenses.APACHE2
|
||||
@@ -177,11 +185,6 @@ class AboutActivity : AppCompatActivity() {
|
||||
SoftwareComponent(
|
||||
"RxJava", "2016 - 2020", "RxJava Contributors",
|
||||
"https://github.com/ReactiveX/RxJava", StandardLicenses.APACHE2
|
||||
),
|
||||
SoftwareComponent(
|
||||
"Universal Image Loader", "2011 - 2015", "Sergey Tarasevich",
|
||||
"https://github.com/nostra13/Android-Universal-Image-Loader",
|
||||
StandardLicenses.APACHE2
|
||||
)
|
||||
)
|
||||
private const val POS_ABOUT = 0
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package org.schabi.newpipe.about
|
||||
|
||||
import android.os.Parcelable
|
||||
import kotlinx.android.parcel.Parcelize
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import java.io.Serializable
|
||||
|
||||
/**
|
||||
|
||||
@@ -108,7 +108,7 @@ object LicenseFragmentHelper {
|
||||
alert.setView(webView)
|
||||
Localization.assureCorrectAppLanguage(context)
|
||||
alert.setNegativeButton(
|
||||
context.getString(R.string.finish)
|
||||
context.getString(R.string.ok)
|
||||
) { dialog, _ -> dialog.dismiss() }
|
||||
alert.show()
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package org.schabi.newpipe.about
|
||||
|
||||
import android.os.Parcelable
|
||||
import kotlinx.android.parcel.Parcelize
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Parcelize
|
||||
class SoftwareComponent
|
||||
|
||||
@@ -27,7 +27,7 @@ import org.schabi.newpipe.database.stream.model.StreamStateEntity;
|
||||
import org.schabi.newpipe.database.subscription.SubscriptionDAO;
|
||||
import org.schabi.newpipe.database.subscription.SubscriptionEntity;
|
||||
|
||||
import static org.schabi.newpipe.database.Migrations.DB_VER_3;
|
||||
import static org.schabi.newpipe.database.Migrations.DB_VER_4;
|
||||
|
||||
@TypeConverters({Converters.class})
|
||||
@Database(
|
||||
@@ -38,7 +38,7 @@ import static org.schabi.newpipe.database.Migrations.DB_VER_3;
|
||||
FeedEntity.class, FeedGroupEntity.class, FeedGroupSubscriptionEntity.class,
|
||||
FeedLastUpdatedEntity.class
|
||||
},
|
||||
version = DB_VER_3
|
||||
version = DB_VER_4
|
||||
)
|
||||
public abstract class AppDatabase extends RoomDatabase {
|
||||
public static final String DATABASE_NAME = "newpipe.db";
|
||||
|
||||
@@ -9,9 +9,19 @@ import androidx.sqlite.db.SupportSQLiteDatabase;
|
||||
import org.schabi.newpipe.MainActivity;
|
||||
|
||||
public final class Migrations {
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
// Test new migrations manually by importing a database from daily usage //
|
||||
// and checking if the migration works (Use the Database Inspector //
|
||||
// https://developer.android.com/studio/inspect/database). //
|
||||
// If you add a migration point it out in the pull request, so that //
|
||||
// others remember to test it themselves. //
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
public static final int DB_VER_1 = 1;
|
||||
public static final int DB_VER_2 = 2;
|
||||
public static final int DB_VER_3 = 3;
|
||||
public static final int DB_VER_4 = 4;
|
||||
|
||||
private static final String TAG = Migrations.class.getName();
|
||||
public static final boolean DEBUG = MainActivity.DEBUG;
|
||||
@@ -160,5 +170,14 @@ public final class Migrations {
|
||||
}
|
||||
};
|
||||
|
||||
public static final Migration MIGRATION_3_4 = new Migration(DB_VER_3, DB_VER_4) {
|
||||
@Override
|
||||
public void migrate(@NonNull final SupportSQLiteDatabase database) {
|
||||
database.execSQL(
|
||||
"ALTER TABLE streams ADD COLUMN uploader_url TEXT"
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
private Migrations() { }
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ data class PlaylistStreamEntry(
|
||||
val item = StreamInfoItem(streamEntity.serviceId, streamEntity.url, streamEntity.title, streamEntity.streamType)
|
||||
item.duration = streamEntity.duration
|
||||
item.uploaderName = streamEntity.uploader
|
||||
item.uploaderUrl = streamEntity.uploaderUrl
|
||||
item.thumbnailUrl = streamEntity.thumbnailUrl
|
||||
|
||||
return item
|
||||
|
||||
@@ -29,6 +29,7 @@ class StreamStatisticsEntry(
|
||||
val item = StreamInfoItem(streamEntity.serviceId, streamEntity.url, streamEntity.title, streamEntity.streamType)
|
||||
item.duration = streamEntity.duration
|
||||
item.uploaderName = streamEntity.uploader
|
||||
item.uploaderUrl = streamEntity.uploaderUrl
|
||||
item.thumbnailUrl = streamEntity.thumbnailUrl
|
||||
|
||||
return item
|
||||
|
||||
@@ -6,6 +6,7 @@ import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.Query
|
||||
import androidx.room.Transaction
|
||||
import io.reactivex.rxjava3.core.Completable
|
||||
import io.reactivex.rxjava3.core.Flowable
|
||||
import org.schabi.newpipe.database.BasicDAO
|
||||
import org.schabi.newpipe.database.stream.model.StreamEntity
|
||||
@@ -29,6 +30,9 @@ abstract class StreamDAO : BasicDAO<StreamEntity> {
|
||||
@Query("SELECT * FROM streams WHERE url = :url AND service_id = :serviceId")
|
||||
abstract fun getStream(serviceId: Long, url: String): Flowable<List<StreamEntity>>
|
||||
|
||||
@Query("UPDATE streams SET uploader_url = :uploaderUrl WHERE url = :url AND service_id = :serviceId")
|
||||
abstract fun setUploaderUrl(serviceId: Long, url: String, uploaderUrl: String): Completable
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
||||
internal abstract fun silentInsertInternal(stream: StreamEntity): Long
|
||||
|
||||
|
||||
@@ -45,6 +45,9 @@ data class StreamEntity(
|
||||
@ColumnInfo(name = STREAM_UPLOADER)
|
||||
var uploader: String,
|
||||
|
||||
@ColumnInfo(name = STREAM_UPLOADER_URL)
|
||||
var uploaderUrl: String? = null,
|
||||
|
||||
@ColumnInfo(name = STREAM_THUMBNAIL_URL)
|
||||
var thumbnailUrl: String? = null,
|
||||
|
||||
@@ -64,7 +67,7 @@ data class StreamEntity(
|
||||
constructor(item: StreamInfoItem) : this(
|
||||
serviceId = item.serviceId, url = item.url, title = item.name,
|
||||
streamType = item.streamType, duration = item.duration, uploader = item.uploaderName,
|
||||
thumbnailUrl = item.thumbnailUrl, viewCount = item.viewCount,
|
||||
uploaderUrl = item.uploaderUrl, thumbnailUrl = item.thumbnailUrl, viewCount = item.viewCount,
|
||||
textualUploadDate = item.textualUploadDate, uploadDate = item.uploadDate?.offsetDateTime(),
|
||||
isUploadDateApproximation = item.uploadDate?.isApproximation
|
||||
)
|
||||
@@ -73,7 +76,7 @@ data class StreamEntity(
|
||||
constructor(info: StreamInfo) : this(
|
||||
serviceId = info.serviceId, url = info.url, title = info.name,
|
||||
streamType = info.streamType, duration = info.duration, uploader = info.uploaderName,
|
||||
thumbnailUrl = info.thumbnailUrl, viewCount = info.viewCount,
|
||||
uploaderUrl = info.uploaderUrl, thumbnailUrl = info.thumbnailUrl, viewCount = info.viewCount,
|
||||
textualUploadDate = info.textualUploadDate, uploadDate = info.uploadDate?.offsetDateTime(),
|
||||
isUploadDateApproximation = info.uploadDate?.isApproximation
|
||||
)
|
||||
@@ -82,13 +85,14 @@ data class StreamEntity(
|
||||
constructor(item: PlayQueueItem) : this(
|
||||
serviceId = item.serviceId, url = item.url, title = item.title,
|
||||
streamType = item.streamType, duration = item.duration, uploader = item.uploader,
|
||||
thumbnailUrl = item.thumbnailUrl
|
||||
uploaderUrl = item.uploaderUrl, thumbnailUrl = item.thumbnailUrl
|
||||
)
|
||||
|
||||
fun toStreamInfoItem(): StreamInfoItem {
|
||||
val item = StreamInfoItem(serviceId, url, title, streamType)
|
||||
item.duration = duration
|
||||
item.uploaderName = uploader
|
||||
item.uploaderUrl = uploaderUrl
|
||||
item.thumbnailUrl = thumbnailUrl
|
||||
|
||||
if (viewCount != null) item.viewCount = viewCount as Long
|
||||
@@ -109,6 +113,7 @@ data class StreamEntity(
|
||||
const val STREAM_TYPE = "stream_type"
|
||||
const val STREAM_DURATION = "duration"
|
||||
const val STREAM_UPLOADER = "uploader"
|
||||
const val STREAM_UPLOADER_URL = "uploader_url"
|
||||
const val STREAM_THUMBNAIL_URL = "thumbnail_url"
|
||||
|
||||
const val STREAM_VIEWS = "view_count"
|
||||
|
||||
@@ -681,7 +681,7 @@ public class DownloadDialog extends DialogFragment
|
||||
new AlertDialog.Builder(context)
|
||||
.setTitle(R.string.general_error)
|
||||
.setMessage(msg)
|
||||
.setNegativeButton(getString(R.string.finish), null)
|
||||
.setNegativeButton(getString(R.string.ok), null)
|
||||
.create()
|
||||
.show();
|
||||
}
|
||||
@@ -864,7 +864,7 @@ public class DownloadDialog extends DialogFragment
|
||||
final AlertDialog.Builder askDialog = new AlertDialog.Builder(context)
|
||||
.setTitle(R.string.download_dialog_title)
|
||||
.setMessage(msgBody)
|
||||
.setNegativeButton(android.R.string.cancel, null);
|
||||
.setNegativeButton(R.string.cancel, null);
|
||||
final StoredFileHelper finalStorage = storage;
|
||||
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ package org.schabi.newpipe.error
|
||||
|
||||
import android.os.Parcelable
|
||||
import androidx.annotation.StringRes
|
||||
import kotlinx.android.parcel.Parcelize
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import org.schabi.newpipe.R
|
||||
import org.schabi.newpipe.extractor.Info
|
||||
import org.schabi.newpipe.extractor.NewPipe
|
||||
|
||||
@@ -6,6 +6,8 @@ import android.util.Log
|
||||
import android.view.View
|
||||
import android.widget.Button
|
||||
import android.widget.TextView
|
||||
import androidx.annotation.Nullable
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.fragment.app.Fragment
|
||||
import com.jakewharton.rxbinding4.view.clicks
|
||||
@@ -37,22 +39,39 @@ class ErrorPanelHelper(
|
||||
onRetry: Runnable
|
||||
) {
|
||||
private val context: Context = rootView.context!!
|
||||
|
||||
private val errorPanelRoot: View = rootView.findViewById(R.id.error_panel)
|
||||
private val errorTextView: TextView = errorPanelRoot.findViewById(R.id.error_message_view)
|
||||
private val errorServiceInfoTextView: TextView = errorPanelRoot.findViewById(R.id.error_message_service_info_view)
|
||||
private val errorServiceExplenationTextView: TextView = errorPanelRoot.findViewById(R.id.error_message_service_explenation_view)
|
||||
private val errorButtonAction: Button = errorPanelRoot.findViewById(R.id.error_button_action)
|
||||
private val errorButtonRetry: Button = errorPanelRoot.findViewById(R.id.error_button_retry)
|
||||
|
||||
// the only element that is visible by default
|
||||
private val errorTextView: TextView =
|
||||
errorPanelRoot.findViewById(R.id.error_message_view)
|
||||
private val errorServiceInfoTextView: TextView =
|
||||
errorPanelRoot.findViewById(R.id.error_message_service_info_view)
|
||||
private val errorServiceExplanationTextView: TextView =
|
||||
errorPanelRoot.findViewById(R.id.error_message_service_explanation_view)
|
||||
private val errorActionButton: Button =
|
||||
errorPanelRoot.findViewById(R.id.error_action_button)
|
||||
private val errorRetryButton: Button =
|
||||
errorPanelRoot.findViewById(R.id.error_retry_button)
|
||||
|
||||
private var errorDisposable: Disposable? = null
|
||||
|
||||
init {
|
||||
errorDisposable = errorButtonRetry.clicks()
|
||||
errorDisposable = errorRetryButton.clicks()
|
||||
.debounce(300, TimeUnit.MILLISECONDS)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe { onRetry.run() }
|
||||
}
|
||||
|
||||
private fun ensureDefaultVisibility() {
|
||||
errorTextView.isVisible = true
|
||||
|
||||
errorServiceInfoTextView.isVisible = false
|
||||
errorServiceExplanationTextView.isVisible = false
|
||||
errorActionButton.isVisible = false
|
||||
errorRetryButton.isVisible = false
|
||||
}
|
||||
|
||||
fun showError(errorInfo: ErrorInfo) {
|
||||
|
||||
if (errorInfo.throwable != null && errorInfo.throwable!!.isInterruptedCaused) {
|
||||
@@ -62,10 +81,14 @@ class ErrorPanelHelper(
|
||||
return
|
||||
}
|
||||
|
||||
errorButtonAction.isVisible = true
|
||||
ensureDefaultVisibility()
|
||||
|
||||
if (errorInfo.throwable is ReCaptchaException) {
|
||||
errorButtonAction.setText(R.string.recaptcha_solve)
|
||||
errorButtonAction.setOnClickListener {
|
||||
errorTextView.setText(R.string.recaptcha_request_toast)
|
||||
|
||||
showAndSetErrorButtonAction(
|
||||
R.string.recaptcha_solve
|
||||
) {
|
||||
// Starting ReCaptcha Challenge Activity
|
||||
val intent = Intent(context, ReCaptchaActivity::class.java)
|
||||
intent.putExtra(
|
||||
@@ -73,78 +96,70 @@ class ErrorPanelHelper(
|
||||
(errorInfo.throwable as ReCaptchaException).url
|
||||
)
|
||||
fragment.startActivityForResult(intent, ReCaptchaActivity.RECAPTCHA_REQUEST)
|
||||
errorButtonAction.setOnClickListener(null)
|
||||
errorActionButton.setOnClickListener(null)
|
||||
}
|
||||
errorTextView.setText(R.string.recaptcha_request_toast)
|
||||
// additional info is only provided by AccountTerminatedException
|
||||
errorServiceInfoTextView.isVisible = false
|
||||
errorServiceExplenationTextView.isVisible = false
|
||||
errorButtonRetry.isVisible = true
|
||||
|
||||
errorRetryButton.isVisible = true
|
||||
} else if (errorInfo.throwable is AccountTerminatedException) {
|
||||
errorButtonRetry.isVisible = false
|
||||
errorButtonAction.isVisible = false
|
||||
errorTextView.setText(R.string.account_terminated)
|
||||
|
||||
if (!isNullOrEmpty((errorInfo.throwable as AccountTerminatedException).message)) {
|
||||
errorServiceInfoTextView.setText(
|
||||
context.resources.getString(
|
||||
R.string.service_provides_reason,
|
||||
NewPipe.getNameOfService(ServiceHelper.getSelectedServiceId(context))
|
||||
)
|
||||
)
|
||||
errorServiceExplenationTextView.setText(
|
||||
(errorInfo.throwable as AccountTerminatedException).message
|
||||
errorServiceInfoTextView.text = context.resources.getString(
|
||||
R.string.service_provides_reason,
|
||||
NewPipe.getNameOfService(ServiceHelper.getSelectedServiceId(context))
|
||||
)
|
||||
errorServiceInfoTextView.isVisible = true
|
||||
errorServiceExplenationTextView.isVisible = true
|
||||
} else {
|
||||
errorServiceInfoTextView.isVisible = false
|
||||
errorServiceExplenationTextView.isVisible = false
|
||||
|
||||
errorServiceExplanationTextView.text =
|
||||
(errorInfo.throwable as AccountTerminatedException).message
|
||||
errorServiceExplanationTextView.isVisible = true
|
||||
}
|
||||
} else {
|
||||
errorButtonAction.setText(R.string.error_snackbar_action)
|
||||
errorButtonAction.setOnClickListener {
|
||||
showAndSetErrorButtonAction(
|
||||
R.string.error_snackbar_action
|
||||
) {
|
||||
ErrorActivity.reportError(context, errorInfo)
|
||||
}
|
||||
|
||||
// additional info is only provided by AccountTerminatedException
|
||||
errorServiceInfoTextView.isVisible = false
|
||||
errorServiceExplenationTextView.isVisible = false
|
||||
errorTextView.setText(getExceptionDescription(errorInfo.throwable))
|
||||
|
||||
// hide retry button by default, then show only if not unavailable/unsupported content
|
||||
errorButtonRetry.isVisible = false
|
||||
errorTextView.setText(
|
||||
when (errorInfo.throwable) {
|
||||
is AgeRestrictedContentException -> R.string.restricted_video_no_stream
|
||||
is GeographicRestrictionException -> R.string.georestricted_content
|
||||
is PaidContentException -> R.string.paid_content
|
||||
is PrivateContentException -> R.string.private_content
|
||||
is SoundCloudGoPlusContentException -> R.string.soundcloud_go_plus_content
|
||||
is YoutubeMusicPremiumContentException -> R.string.youtube_music_premium_content
|
||||
is ContentNotAvailableException -> R.string.content_not_available
|
||||
is ContentNotSupportedException -> R.string.content_not_supported
|
||||
else -> {
|
||||
// show retry button only for content which is not unavailable or unsupported
|
||||
errorButtonRetry.isVisible = true
|
||||
if (errorInfo.throwable != null && errorInfo.throwable!!.isNetworkRelated) {
|
||||
R.string.network_error
|
||||
} else {
|
||||
R.string.error_snackbar_message
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
if (errorInfo.throwable !is ContentNotAvailableException &&
|
||||
errorInfo.throwable !is ContentNotSupportedException
|
||||
) {
|
||||
// show retry button only for content which is not unavailable or unsupported
|
||||
errorRetryButton.isVisible = true
|
||||
}
|
||||
}
|
||||
errorPanelRoot.animate(true, 300)
|
||||
|
||||
setRootVisible()
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows the errorButtonAction, sets a text into it and sets the click listener.
|
||||
*/
|
||||
private fun showAndSetErrorButtonAction(
|
||||
@StringRes resid: Int,
|
||||
@Nullable listener: View.OnClickListener
|
||||
) {
|
||||
errorActionButton.isVisible = true
|
||||
errorActionButton.setText(resid)
|
||||
errorActionButton.setOnClickListener(listener)
|
||||
}
|
||||
|
||||
fun showTextError(errorString: String) {
|
||||
errorButtonAction.isVisible = false
|
||||
errorButtonRetry.isVisible = false
|
||||
ensureDefaultVisibility()
|
||||
|
||||
errorTextView.text = errorString
|
||||
|
||||
setRootVisible()
|
||||
}
|
||||
|
||||
private fun setRootVisible() {
|
||||
errorPanelRoot.animate(true, 300)
|
||||
}
|
||||
|
||||
fun hide() {
|
||||
errorButtonAction.setOnClickListener(null)
|
||||
errorActionButton.setOnClickListener(null)
|
||||
errorPanelRoot.animate(false, 150)
|
||||
}
|
||||
|
||||
@@ -153,13 +168,35 @@ class ErrorPanelHelper(
|
||||
}
|
||||
|
||||
fun dispose() {
|
||||
errorButtonAction.setOnClickListener(null)
|
||||
errorButtonRetry.setOnClickListener(null)
|
||||
errorActionButton.setOnClickListener(null)
|
||||
errorRetryButton.setOnClickListener(null)
|
||||
errorDisposable?.dispose()
|
||||
}
|
||||
|
||||
companion object {
|
||||
val TAG: String = ErrorPanelHelper::class.simpleName!!
|
||||
val DEBUG: Boolean = MainActivity.DEBUG
|
||||
|
||||
@StringRes
|
||||
public fun getExceptionDescription(throwable: Throwable?): Int {
|
||||
return when (throwable) {
|
||||
is AgeRestrictedContentException -> R.string.restricted_video_no_stream
|
||||
is GeographicRestrictionException -> R.string.georestricted_content
|
||||
is PaidContentException -> R.string.paid_content
|
||||
is PrivateContentException -> R.string.private_content
|
||||
is SoundCloudGoPlusContentException -> R.string.soundcloud_go_plus_content
|
||||
is YoutubeMusicPremiumContentException -> R.string.youtube_music_premium_content
|
||||
is ContentNotAvailableException -> R.string.content_not_available
|
||||
is ContentNotSupportedException -> R.string.content_not_supported
|
||||
else -> {
|
||||
// show retry button only for content which is not unavailable or unsupported
|
||||
if (throwable != null && throwable.isNetworkRelated) {
|
||||
R.string.network_error
|
||||
} else {
|
||||
R.string.error_snackbar_message
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package org.schabi.newpipe.error;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.Build;
|
||||
@@ -66,6 +67,7 @@ public class ReCaptchaActivity extends AppCompatActivity {
|
||||
private ActivityRecaptchaBinding recaptchaBinding;
|
||||
private String foundCookies = "";
|
||||
|
||||
@SuppressLint("SetJavaScriptEnabled")
|
||||
@Override
|
||||
protected void onCreate(final Bundle savedInstanceState) {
|
||||
ThemeHelper.setTheme(this);
|
||||
|
||||
@@ -48,9 +48,7 @@ import com.google.android.exoplayer2.PlaybackParameters;
|
||||
import com.google.android.material.appbar.AppBarLayout;
|
||||
import com.google.android.material.bottomsheet.BottomSheetBehavior;
|
||||
import com.google.android.material.tabs.TabLayout;
|
||||
import com.nostra13.universalimageloader.core.assist.FailReason;
|
||||
import com.nostra13.universalimageloader.core.listener.ImageLoadingListener;
|
||||
import com.nostra13.universalimageloader.core.listener.SimpleImageLoadingListener;
|
||||
import com.squareup.picasso.Callback;
|
||||
|
||||
import org.schabi.newpipe.App;
|
||||
import org.schabi.newpipe.R;
|
||||
@@ -79,6 +77,7 @@ import org.schabi.newpipe.local.dialog.PlaylistAppendDialog;
|
||||
import org.schabi.newpipe.local.dialog.PlaylistCreationDialog;
|
||||
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
||||
import org.schabi.newpipe.player.MainPlayer;
|
||||
import org.schabi.newpipe.player.MainPlayer.PlayerType;
|
||||
import org.schabi.newpipe.player.Player;
|
||||
import org.schabi.newpipe.player.event.OnKeyDownListener;
|
||||
import org.schabi.newpipe.player.event.PlayerServiceExtendedEventListener;
|
||||
@@ -90,14 +89,14 @@ import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
|
||||
import org.schabi.newpipe.util.Constants;
|
||||
import org.schabi.newpipe.util.DeviceUtils;
|
||||
import org.schabi.newpipe.util.ExtractorHelper;
|
||||
import org.schabi.newpipe.util.ImageDisplayConstants;
|
||||
import org.schabi.newpipe.util.external_communication.KoreUtils;
|
||||
import org.schabi.newpipe.util.ListHelper;
|
||||
import org.schabi.newpipe.util.Localization;
|
||||
import org.schabi.newpipe.util.NavigationHelper;
|
||||
import org.schabi.newpipe.util.PermissionHelper;
|
||||
import org.schabi.newpipe.util.external_communication.ShareUtils;
|
||||
import org.schabi.newpipe.util.PicassoHelper;
|
||||
import org.schabi.newpipe.util.ThemeHelper;
|
||||
import org.schabi.newpipe.util.external_communication.KoreUtils;
|
||||
import org.schabi.newpipe.util.external_communication.ShareUtils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
@@ -151,6 +150,8 @@ public final class VideoDetailFragment
|
||||
private static final String DESCRIPTION_TAB_TAG = "DESCRIPTION TAB";
|
||||
private static final String EMPTY_TAB_TAG = "EMPTY TAB";
|
||||
|
||||
private static final String PICASSO_VIDEO_DETAILS_TAG = "PICASSO_VIDEO_DETAILS_TAG";
|
||||
|
||||
// tabs
|
||||
private boolean showComments;
|
||||
private boolean showRelatedItems;
|
||||
@@ -201,7 +202,7 @@ public final class VideoDetailFragment
|
||||
@Nullable
|
||||
private MainPlayer playerService;
|
||||
private Player player;
|
||||
private PlayerHolder playerHolder = PlayerHolder.getInstance();
|
||||
private final PlayerHolder playerHolder = PlayerHolder.getInstance();
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Service management
|
||||
@@ -220,7 +221,7 @@ public final class VideoDetailFragment
|
||||
return;
|
||||
}
|
||||
|
||||
if (isLandscape()) {
|
||||
if (DeviceUtils.isLandscape(requireContext())) {
|
||||
// If the video is playing but orientation changed
|
||||
// let's make the video in fullscreen again
|
||||
checkLandscape();
|
||||
@@ -241,7 +242,7 @@ public final class VideoDetailFragment
|
||||
&& isAutoplayEnabled()
|
||||
&& player.getParentActivity() == null)) {
|
||||
autoPlayEnabled = true; // forcefully start playing
|
||||
openVideoPlayer();
|
||||
openVideoPlayerAutoFullscreen();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -423,7 +424,7 @@ public final class VideoDetailFragment
|
||||
showRelatedItems = sharedPreferences.getBoolean(key, true);
|
||||
tabSettingsChanged = true;
|
||||
} else if (key.equals(getString(R.string.show_description_key))) {
|
||||
showComments = sharedPreferences.getBoolean(key, true);
|
||||
showDescription = sharedPreferences.getBoolean(key, true);
|
||||
tabSettingsChanged = true;
|
||||
}
|
||||
}
|
||||
@@ -499,7 +500,7 @@ public final class VideoDetailFragment
|
||||
break;
|
||||
case R.id.detail_thumbnail_root_layout:
|
||||
autoPlayEnabled = true; // forcefully start playing
|
||||
openVideoPlayer();
|
||||
openVideoPlayerAutoFullscreen();
|
||||
break;
|
||||
case R.id.detail_title_root_layout:
|
||||
toggleTitleAndSecondaryControls();
|
||||
@@ -516,7 +517,7 @@ public final class VideoDetailFragment
|
||||
showSystemUi();
|
||||
} else {
|
||||
autoPlayEnabled = true; // forcefully start playing
|
||||
openVideoPlayer();
|
||||
openVideoPlayer(false);
|
||||
}
|
||||
|
||||
setOverlayPlayPauseImage(isPlayerAvailable() && player.isPlaying());
|
||||
@@ -686,33 +687,24 @@ public final class VideoDetailFragment
|
||||
}
|
||||
|
||||
private void initThumbnailViews(@NonNull final StreamInfo info) {
|
||||
binding.detailThumbnailImageView.setImageResource(R.drawable.dummy_thumbnail_dark);
|
||||
PicassoHelper.loadThumbnail(info.getThumbnailUrl()).tag(PICASSO_VIDEO_DETAILS_TAG)
|
||||
.into(binding.detailThumbnailImageView, new Callback() {
|
||||
@Override
|
||||
public void onSuccess() {
|
||||
// nothing to do, the image was loaded correctly into the thumbnail
|
||||
}
|
||||
|
||||
if (!isEmpty(info.getThumbnailUrl())) {
|
||||
final ImageLoadingListener onFailListener = new SimpleImageLoadingListener() {
|
||||
@Override
|
||||
public void onLoadingFailed(final String imageUri, final View view,
|
||||
final FailReason failReason) {
|
||||
showSnackBarError(new ErrorInfo(failReason.getCause(), UserAction.LOAD_IMAGE,
|
||||
imageUri, info));
|
||||
}
|
||||
};
|
||||
@Override
|
||||
public void onError(final Exception e) {
|
||||
showSnackBarError(new ErrorInfo(e, UserAction.LOAD_IMAGE,
|
||||
info.getThumbnailUrl(), info));
|
||||
}
|
||||
});
|
||||
|
||||
IMAGE_LOADER.displayImage(info.getThumbnailUrl(), binding.detailThumbnailImageView,
|
||||
ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS, onFailListener);
|
||||
}
|
||||
|
||||
if (!isEmpty(info.getSubChannelAvatarUrl())) {
|
||||
IMAGE_LOADER.displayImage(info.getSubChannelAvatarUrl(),
|
||||
binding.detailSubChannelThumbnailView,
|
||||
ImageDisplayConstants.DISPLAY_AVATAR_OPTIONS);
|
||||
}
|
||||
|
||||
if (!isEmpty(info.getUploaderAvatarUrl())) {
|
||||
IMAGE_LOADER.displayImage(info.getUploaderAvatarUrl(),
|
||||
binding.detailUploaderThumbnailView,
|
||||
ImageDisplayConstants.DISPLAY_AVATAR_OPTIONS);
|
||||
}
|
||||
PicassoHelper.loadAvatar(info.getSubChannelAvatarUrl()).tag(PICASSO_VIDEO_DETAILS_TAG)
|
||||
.into(binding.detailSubChannelThumbnailView);
|
||||
PicassoHelper.loadAvatar(info.getUploaderAvatarUrl()).tag(PICASSO_VIDEO_DETAILS_TAG)
|
||||
.into(binding.detailUploaderThumbnailView);
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
@@ -751,27 +743,26 @@ public final class VideoDetailFragment
|
||||
&& player.getPlayQueue() != null
|
||||
&& player.videoPlayerSelected()
|
||||
&& player.getPlayQueue().previous()) {
|
||||
return true;
|
||||
return true; // no code here, as previous() was used in the if
|
||||
}
|
||||
|
||||
// That means that we are on the start of the stack,
|
||||
// return false to let the MainActivity handle the onBack
|
||||
if (stack.size() <= 1) {
|
||||
restoreDefaultOrientation();
|
||||
|
||||
return false;
|
||||
return false; // let MainActivity handle the onBack (e.g. to minimize the mini player)
|
||||
}
|
||||
|
||||
// Remove top
|
||||
stack.pop();
|
||||
// Get stack item from the new top
|
||||
assert stack.peek() != null;
|
||||
setupFromHistoryItem(stack.peek());
|
||||
setupFromHistoryItem(Objects.requireNonNull(stack.peek()));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void setupFromHistoryItem(final StackItem item) {
|
||||
setAutoPlay(false);
|
||||
hideMainPlayer();
|
||||
hideMainPlayerOnLoadingNewStream();
|
||||
|
||||
setInitialData(item.getServiceId(), item.getUrl(),
|
||||
item.getTitle() == null ? "" : item.getTitle(), item.getPlayQueue());
|
||||
@@ -891,7 +882,7 @@ public final class VideoDetailFragment
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(result -> {
|
||||
isLoading.set(false);
|
||||
hideMainPlayer();
|
||||
hideMainPlayerOnLoadingNewStream();
|
||||
if (result.getAgeLimit() != NO_AGE_LIMIT && !prefs.getBoolean(
|
||||
getString(R.string.show_age_restricted_content), false)) {
|
||||
hideAgeRestrictedContent();
|
||||
@@ -906,8 +897,9 @@ public final class VideoDetailFragment
|
||||
stack.push(new StackItem(serviceId, url, title, playQueue));
|
||||
}
|
||||
}
|
||||
|
||||
if (isAutoplayEnabled()) {
|
||||
openVideoPlayer();
|
||||
openVideoPlayerAutoFullscreen();
|
||||
}
|
||||
}
|
||||
}, throwable -> showError(new ErrorInfo(throwable, UserAction.REQUESTED_STREAM,
|
||||
@@ -1104,15 +1096,37 @@ public final class VideoDetailFragment
|
||||
toggleFullscreenIfInFullscreenMode();
|
||||
|
||||
final PlayQueue queue = setupPlayQueueForIntent(append);
|
||||
if (append) {
|
||||
NavigationHelper.enqueueOnPopupPlayer(activity, queue, false);
|
||||
if (append) { //resumePlayback: false
|
||||
NavigationHelper.enqueueOnPlayer(activity, queue, PlayerType.POPUP);
|
||||
} else {
|
||||
replaceQueueIfUserConfirms(() -> NavigationHelper
|
||||
.playOnPopupPlayer(activity, queue, true));
|
||||
}
|
||||
}
|
||||
|
||||
public void openVideoPlayer() {
|
||||
/**
|
||||
* Opens the video player, in fullscreen if needed. In order to open fullscreen, the activity
|
||||
* is toggled to landscape orientation (which will then cause fullscreen mode).
|
||||
*
|
||||
* @param directlyFullscreenIfApplicable whether to open fullscreen if we are not already
|
||||
* in landscape and screen orientation is locked
|
||||
*/
|
||||
public void openVideoPlayer(final boolean directlyFullscreenIfApplicable) {
|
||||
if (directlyFullscreenIfApplicable
|
||||
&& !DeviceUtils.isLandscape(requireContext())
|
||||
&& PlayerHelper.globalScreenOrientationLocked(requireContext())) {
|
||||
// Make sure the bottom sheet turns out expanded. When this code kicks in the bottom
|
||||
// sheet could not have fully expanded yet, and thus be in the STATE_SETTLING state.
|
||||
// When the activity is rotated, and its state is saved and then restored, the bottom
|
||||
// sheet would forget what it was doing, since even if STATE_SETTLING is restored, it
|
||||
// doesn't tell which state it was settling to, and thus the bottom sheet settles to
|
||||
// STATE_COLLAPSED. This can be solved by manually setting the state that will be
|
||||
// restored (i.e. bottomSheetState) to STATE_EXPANDED.
|
||||
bottomSheetState = BottomSheetBehavior.STATE_EXPANDED;
|
||||
// toggle landscape in order to open directly in fullscreen
|
||||
onScreenRotationButtonClicked();
|
||||
}
|
||||
|
||||
if (PreferenceManager.getDefaultSharedPreferences(activity)
|
||||
.getBoolean(this.getString(R.string.use_external_video_player_key), false)) {
|
||||
showExternalPlaybackDialog();
|
||||
@@ -1121,6 +1135,18 @@ public final class VideoDetailFragment
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If the option to start directly fullscreen is enabled, calls
|
||||
* {@link #openVideoPlayer(boolean)} with {@code directlyFullscreenIfApplicable = true}, so that
|
||||
* if the user is not already in landscape and he has screen orientation locked the activity
|
||||
* rotates and fullscreen starts. Otherwise, if the option to start directly fullscreen is
|
||||
* disabled, calls {@link #openVideoPlayer(boolean)} with {@code directlyFullscreenIfApplicable
|
||||
* = false}, hence preventing it from going directly fullscreen.
|
||||
*/
|
||||
public void openVideoPlayerAutoFullscreen() {
|
||||
openVideoPlayer(PlayerHelper.isStartMainPlayerFullscreenEnabled(requireContext()));
|
||||
}
|
||||
|
||||
private void openNormalBackgroundPlayer(final boolean append) {
|
||||
// See UI changes while remote playQueue changes
|
||||
if (!isPlayerAvailable()) {
|
||||
@@ -1129,7 +1155,7 @@ public final class VideoDetailFragment
|
||||
|
||||
final PlayQueue queue = setupPlayQueueForIntent(append);
|
||||
if (append) {
|
||||
NavigationHelper.enqueueOnBackgroundPlayer(activity, queue, false);
|
||||
NavigationHelper.enqueueOnPlayer(activity, queue, PlayerType.AUDIO);
|
||||
} else {
|
||||
replaceQueueIfUserConfirms(() -> NavigationHelper
|
||||
.playOnBackgroundPlayer(activity, queue, true));
|
||||
@@ -1154,12 +1180,19 @@ public final class VideoDetailFragment
|
||||
}
|
||||
addVideoPlayerView();
|
||||
|
||||
final Intent playerIntent = NavigationHelper
|
||||
.getPlayerIntent(requireContext(), MainPlayer.class, queue, true, autoPlayEnabled);
|
||||
final Intent playerIntent = NavigationHelper.getPlayerIntent(requireContext(),
|
||||
MainPlayer.class, queue, autoPlayEnabled);
|
||||
ContextCompat.startForegroundService(activity, playerIntent);
|
||||
}
|
||||
|
||||
private void hideMainPlayer() {
|
||||
/**
|
||||
* When the video detail fragment is already showing details for a video and the user opens a
|
||||
* new one, the video detail fragment changes all of its old data to the new stream, so if there
|
||||
* is a video player currently open it should be hidden. This method does exactly that. If
|
||||
* autoplay is enabled, the underlying player is not stopped completely, since it is going to
|
||||
* be reused in a few milliseconds and the flickering would be annoying.
|
||||
*/
|
||||
private void hideMainPlayerOnLoadingNewStream() {
|
||||
if (!isPlayerServiceAvailable()
|
||||
|| playerService.getView() == null
|
||||
|| !player.videoPlayerSelected()) {
|
||||
@@ -1167,8 +1200,12 @@ public final class VideoDetailFragment
|
||||
}
|
||||
|
||||
removeVideoPlayerView();
|
||||
playerService.stop(isAutoplayEnabled());
|
||||
playerService.getView().setVisibility(View.GONE);
|
||||
if (isAutoplayEnabled()) {
|
||||
playerService.stopForImmediateReusing();
|
||||
playerService.getView().setVisibility(View.GONE);
|
||||
} else {
|
||||
playerHolder.stopService();
|
||||
}
|
||||
}
|
||||
|
||||
private PlayQueue setupPlayQueueForIntent(final boolean append) {
|
||||
@@ -1261,7 +1298,7 @@ public final class VideoDetailFragment
|
||||
final DisplayMetrics metrics = getResources().getDisplayMetrics();
|
||||
|
||||
if (getView() != null) {
|
||||
final int height = (isInMultiWindow()
|
||||
final int height = (DeviceUtils.isInMultiWindow(activity)
|
||||
? requireView()
|
||||
: activity.getWindow().getDecorView()).getHeight();
|
||||
setHeightThumbnail(height, metrics);
|
||||
@@ -1284,7 +1321,7 @@ public final class VideoDetailFragment
|
||||
requireView().getViewTreeObserver().removeOnPreDrawListener(preDrawListener);
|
||||
|
||||
if (isPlayerAvailable() && player.isFullscreen()) {
|
||||
final int height = (isInMultiWindow()
|
||||
final int height = (DeviceUtils.isInMultiWindow(activity)
|
||||
? requireView()
|
||||
: activity.getWindow().getDecorView()).getHeight();
|
||||
// Height is zero when the view is not yet displayed like after orientation change
|
||||
@@ -1395,17 +1432,15 @@ public final class VideoDetailFragment
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
private void restoreDefaultOrientation() {
|
||||
if (!isPlayerAvailable() || !player.videoPlayerSelected() || activity == null) {
|
||||
return;
|
||||
if (isPlayerAvailable() && player.videoPlayerSelected()) {
|
||||
toggleFullscreenIfInFullscreenMode();
|
||||
}
|
||||
|
||||
toggleFullscreenIfInFullscreenMode();
|
||||
|
||||
// This will show systemUI and pause the player.
|
||||
// User can tap on Play button and video will be in fullscreen mode again
|
||||
// Note for tablet: trying to avoid orientation changes since it's not easy
|
||||
// to physically rotate the tablet every time
|
||||
if (!DeviceUtils.isTablet(activity)) {
|
||||
if (activity != null && !DeviceUtils.isTablet(activity)) {
|
||||
activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED);
|
||||
}
|
||||
}
|
||||
@@ -1446,8 +1481,7 @@ public final class VideoDetailFragment
|
||||
}
|
||||
}
|
||||
|
||||
IMAGE_LOADER.cancelDisplayTask(binding.detailThumbnailImageView);
|
||||
IMAGE_LOADER.cancelDisplayTask(binding.detailSubChannelThumbnailView);
|
||||
PicassoHelper.cancelTag(PICASSO_VIDEO_DETAILS_TAG);
|
||||
binding.detailThumbnailImageView.setImageBitmap(null);
|
||||
binding.detailSubChannelThumbnailView.setImageBitmap(null);
|
||||
}
|
||||
@@ -1818,7 +1852,7 @@ public final class VideoDetailFragment
|
||||
|| error.type == ExoPlaybackException.TYPE_UNEXPECTED) {
|
||||
// Properly exit from fullscreen
|
||||
toggleFullscreenIfInFullscreenMode();
|
||||
hideMainPlayer();
|
||||
hideMainPlayerOnLoadingNewStream();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1874,13 +1908,14 @@ public final class VideoDetailFragment
|
||||
// from landscape to portrait every time.
|
||||
// Just turn on fullscreen mode in landscape orientation
|
||||
// or portrait & unlocked global orientation
|
||||
final boolean isLandscape = DeviceUtils.isLandscape(requireContext());
|
||||
if (DeviceUtils.isTablet(activity)
|
||||
&& (!globalScreenOrientationLocked(activity) || isLandscape())) {
|
||||
&& (!globalScreenOrientationLocked(activity) || isLandscape)) {
|
||||
player.toggleFullscreen();
|
||||
return;
|
||||
}
|
||||
|
||||
final int newOrientation = isLandscape()
|
||||
final int newOrientation = isLandscape
|
||||
? ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
|
||||
: ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE;
|
||||
|
||||
@@ -1952,15 +1987,17 @@ public final class VideoDetailFragment
|
||||
| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
|
||||
| View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
|
||||
| View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY;
|
||||
|
||||
// In multiWindow mode status bar is not transparent for devices with cutout
|
||||
// if I include this flag. So without it is better in this case
|
||||
if (!isInMultiWindow()) {
|
||||
final boolean isInMultiWindow = DeviceUtils.isInMultiWindow(activity);
|
||||
if (!isInMultiWindow) {
|
||||
visibility |= View.SYSTEM_UI_FLAG_FULLSCREEN;
|
||||
}
|
||||
activity.getWindow().getDecorView().setSystemUiVisibility(visibility);
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP
|
||||
&& (isInMultiWindow() || (isPlayerAvailable() && player.isFullscreen()))) {
|
||||
&& (isInMultiWindow || (isPlayerAvailable() && player.isFullscreen()))) {
|
||||
activity.getWindow().setStatusBarColor(Color.TRANSPARENT);
|
||||
activity.getWindow().setNavigationBarColor(Color.TRANSPARENT);
|
||||
}
|
||||
@@ -2032,15 +2069,6 @@ public final class VideoDetailFragment
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isLandscape() {
|
||||
return getResources().getDisplayMetrics().heightPixels < getResources()
|
||||
.getDisplayMetrics().widthPixels;
|
||||
}
|
||||
|
||||
private boolean isInMultiWindow() {
|
||||
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && activity.isInMultiWindowMode();
|
||||
}
|
||||
|
||||
/*
|
||||
* Means that the player fragment was swiped away via BottomSheetLayout
|
||||
* and is empty but ready for any new actions. See cleanUp()
|
||||
@@ -2080,8 +2108,8 @@ public final class VideoDetailFragment
|
||||
private void showClearingQueueConfirmation(final Runnable onAllow) {
|
||||
new AlertDialog.Builder(activity)
|
||||
.setTitle(R.string.clear_queue_confirmation_description)
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.setPositiveButton(android.R.string.yes, (dialog, which) -> {
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.setPositiveButton(R.string.ok, (dialog, which) -> {
|
||||
onAllow.run();
|
||||
dialog.dismiss();
|
||||
}).show();
|
||||
@@ -2096,7 +2124,7 @@ public final class VideoDetailFragment
|
||||
resolutions[i] = sortedVideoStreams.get(i).getResolution();
|
||||
}
|
||||
final AlertDialog.Builder builder = new AlertDialog.Builder(activity)
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.setNeutralButton(R.string.open_in_browser, (dialog, i) ->
|
||||
ShareUtils.openUrlInBrowser(requireActivity(), url)
|
||||
);
|
||||
@@ -2223,7 +2251,7 @@ public final class VideoDetailFragment
|
||||
setOverlayElementsClickable(false);
|
||||
hideSystemUiIfNeeded();
|
||||
// Conditions when the player should be expanded to fullscreen
|
||||
if (isLandscape()
|
||||
if (DeviceUtils.isLandscape(requireContext())
|
||||
&& isPlayerAvailable()
|
||||
&& player.isPlaying()
|
||||
&& !player.isFullscreen()
|
||||
@@ -2278,10 +2306,8 @@ public final class VideoDetailFragment
|
||||
binding.overlayTitleTextView.setText(isEmpty(overlayTitle) ? "" : overlayTitle);
|
||||
binding.overlayChannelTextView.setText(isEmpty(uploader) ? "" : uploader);
|
||||
binding.overlayThumbnail.setImageResource(R.drawable.dummy_thumbnail_dark);
|
||||
if (!isEmpty(thumbnailUrl)) {
|
||||
IMAGE_LOADER.displayImage(thumbnailUrl, binding.overlayThumbnail,
|
||||
ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS, null);
|
||||
}
|
||||
PicassoHelper.loadThumbnail(thumbnailUrl).tag(PICASSO_VIDEO_DETAILS_TAG)
|
||||
.into(binding.overlayThumbnail);
|
||||
}
|
||||
|
||||
private void setOverlayPlayPauseImage(final boolean playerIsPlaying) {
|
||||
|
||||
@@ -350,12 +350,16 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I>
|
||||
if (context == null || context.getResources() == null || activity == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final List<StreamDialogEntry> entries = new ArrayList<>();
|
||||
|
||||
if (PlayerHolder.getInstance().getType() != null) {
|
||||
if (PlayerHolder.getInstance().isPlayerOpen()) {
|
||||
entries.add(StreamDialogEntry.enqueue);
|
||||
|
||||
if (PlayerHolder.getInstance().getQueueSize() > 1) {
|
||||
entries.add(StreamDialogEntry.enqueue_next);
|
||||
}
|
||||
}
|
||||
|
||||
if (item.getStreamType() == StreamType.AUDIO_STREAM) {
|
||||
entries.addAll(Arrays.asList(
|
||||
StreamDialogEntry.start_here_on_background,
|
||||
|
||||
@@ -37,13 +37,14 @@ import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||
import org.schabi.newpipe.fragments.list.BaseListInfoFragment;
|
||||
import org.schabi.newpipe.ktx.AnimationType;
|
||||
import org.schabi.newpipe.local.subscription.SubscriptionManager;
|
||||
import org.schabi.newpipe.player.MainPlayer.PlayerType;
|
||||
import org.schabi.newpipe.player.playqueue.ChannelPlayQueue;
|
||||
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
||||
import org.schabi.newpipe.util.ExtractorHelper;
|
||||
import org.schabi.newpipe.util.ImageDisplayConstants;
|
||||
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.PicassoHelper;
|
||||
import org.schabi.newpipe.util.ThemeHelper;
|
||||
|
||||
import java.util.ArrayList;
|
||||
@@ -66,7 +67,10 @@ import static org.schabi.newpipe.ktx.ViewUtils.animateBackgroundColor;
|
||||
|
||||
public class ChannelFragment extends BaseListInfoFragment<ChannelInfo>
|
||||
implements View.OnClickListener {
|
||||
|
||||
private static final int BUTTON_DEBOUNCE_INTERVAL = 100;
|
||||
private static final String PICASSO_CHANNEL_TAG = "PICASSO_CHANNEL_TAG";
|
||||
|
||||
private final CompositeDisposable disposables = new CompositeDisposable();
|
||||
private Disposable subscribeButtonMonitor;
|
||||
|
||||
@@ -421,10 +425,7 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo>
|
||||
@Override
|
||||
public void showLoading() {
|
||||
super.showLoading();
|
||||
|
||||
IMAGE_LOADER.cancelDisplayTask(headerBinding.channelBannerImage);
|
||||
IMAGE_LOADER.cancelDisplayTask(headerBinding.channelAvatarView);
|
||||
IMAGE_LOADER.cancelDisplayTask(headerBinding.subChannelAvatarView);
|
||||
PicassoHelper.cancelTag(PICASSO_CHANNEL_TAG);
|
||||
animate(headerBinding.channelSubscribeButton, false, 100);
|
||||
}
|
||||
|
||||
@@ -433,13 +434,12 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo>
|
||||
super.handleResult(result);
|
||||
|
||||
headerBinding.getRoot().setVisibility(View.VISIBLE);
|
||||
IMAGE_LOADER.displayImage(result.getBannerUrl(), headerBinding.channelBannerImage,
|
||||
ImageDisplayConstants.DISPLAY_BANNER_OPTIONS);
|
||||
IMAGE_LOADER.displayImage(result.getAvatarUrl(), headerBinding.channelAvatarView,
|
||||
ImageDisplayConstants.DISPLAY_AVATAR_OPTIONS);
|
||||
IMAGE_LOADER.displayImage(result.getParentChannelAvatarUrl(),
|
||||
headerBinding.subChannelAvatarView,
|
||||
ImageDisplayConstants.DISPLAY_AVATAR_OPTIONS);
|
||||
PicassoHelper.loadBanner(result.getBannerUrl()).tag(PICASSO_CHANNEL_TAG)
|
||||
.into(headerBinding.channelBannerImage);
|
||||
PicassoHelper.loadAvatar(result.getAvatarUrl()).tag(PICASSO_CHANNEL_TAG)
|
||||
.into(headerBinding.channelAvatarView);
|
||||
PicassoHelper.loadAvatar(result.getParentChannelAvatarUrl()).tag(PICASSO_CHANNEL_TAG)
|
||||
.into(headerBinding.subChannelAvatarView);
|
||||
|
||||
headerBinding.channelSubscriberView.setVisibility(View.VISIBLE);
|
||||
if (result.getSubscriberCount() >= 0) {
|
||||
@@ -496,12 +496,12 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo>
|
||||
.playOnBackgroundPlayer(activity, getPlayQueue(), false));
|
||||
|
||||
playlistControlBinding.playlistCtrlPlayPopupButton.setOnLongClickListener(view -> {
|
||||
NavigationHelper.enqueueOnPopupPlayer(activity, getPlayQueue(), true);
|
||||
NavigationHelper.enqueueOnPlayer(activity, getPlayQueue(), PlayerType.POPUP);
|
||||
return true;
|
||||
});
|
||||
|
||||
playlistControlBinding.playlistCtrlPlayBgButton.setOnLongClickListener(view -> {
|
||||
NavigationHelper.enqueueOnBackgroundPlayer(activity, getPlayQueue(), true);
|
||||
NavigationHelper.enqueueOnPlayer(activity, getPlayQueue(), PlayerType.AUDIO);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -37,11 +37,12 @@ import org.schabi.newpipe.extractor.stream.StreamType;
|
||||
import org.schabi.newpipe.fragments.list.BaseListInfoFragment;
|
||||
import org.schabi.newpipe.info_list.InfoItemDialog;
|
||||
import org.schabi.newpipe.local.playlist.RemotePlaylistManager;
|
||||
import org.schabi.newpipe.player.MainPlayer.PlayerType;
|
||||
import org.schabi.newpipe.player.helper.PlayerHolder;
|
||||
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
||||
import org.schabi.newpipe.player.playqueue.PlaylistPlayQueue;
|
||||
import org.schabi.newpipe.util.ExtractorHelper;
|
||||
import org.schabi.newpipe.util.ImageDisplayConstants;
|
||||
import org.schabi.newpipe.util.PicassoHelper;
|
||||
import org.schabi.newpipe.util.external_communication.KoreUtils;
|
||||
import org.schabi.newpipe.util.Localization;
|
||||
import org.schabi.newpipe.util.NavigationHelper;
|
||||
@@ -64,12 +65,16 @@ import static org.schabi.newpipe.ktx.ViewUtils.animate;
|
||||
import static org.schabi.newpipe.ktx.ViewUtils.animateHideRecyclerViewAllowingScrolling;
|
||||
|
||||
public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
|
||||
|
||||
private static final String PICASSO_PLAYLIST_TAG = "PICASSO_PLAYLIST_TAG";
|
||||
|
||||
private CompositeDisposable disposables;
|
||||
private Subscription bookmarkReactor;
|
||||
private AtomicBoolean isBookmarkButtonReady;
|
||||
|
||||
private RemotePlaylistManager remotePlaylistManager;
|
||||
private PlaylistRemoteEntity playlistEntity;
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Views
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
@@ -144,9 +149,14 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
|
||||
|
||||
final ArrayList<StreamDialogEntry> entries = new ArrayList<>();
|
||||
|
||||
if (PlayerHolder.getInstance().getType() != null) {
|
||||
if (PlayerHolder.getInstance().isPlayerOpen()) {
|
||||
entries.add(StreamDialogEntry.enqueue);
|
||||
|
||||
if (PlayerHolder.getInstance().getQueueSize() > 1) {
|
||||
entries.add(StreamDialogEntry.enqueue_next);
|
||||
}
|
||||
}
|
||||
|
||||
if (item.getStreamType() == StreamType.AUDIO_STREAM) {
|
||||
entries.addAll(Arrays.asList(
|
||||
StreamDialogEntry.start_here_on_background,
|
||||
@@ -274,7 +284,7 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
|
||||
animate(headerBinding.getRoot(), false, 200);
|
||||
animateHideRecyclerViewAllowingScrolling(itemsList);
|
||||
|
||||
IMAGE_LOADER.cancelDisplayTask(headerBinding.uploaderAvatarView);
|
||||
PicassoHelper.cancelTag(PICASSO_PLAYLIST_TAG);
|
||||
animate(headerBinding.uploaderLayout, false, 200);
|
||||
}
|
||||
|
||||
@@ -317,8 +327,8 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
|
||||
R.drawable.ic_radio)
|
||||
);
|
||||
} else {
|
||||
IMAGE_LOADER.displayImage(avatarUrl, headerBinding.uploaderAvatarView,
|
||||
ImageDisplayConstants.DISPLAY_AVATAR_OPTIONS);
|
||||
PicassoHelper.loadAvatar(avatarUrl).tag(PICASSO_PLAYLIST_TAG)
|
||||
.into(headerBinding.uploaderAvatarView);
|
||||
}
|
||||
|
||||
headerBinding.playlistStreamCount.setText(Localization
|
||||
@@ -343,12 +353,12 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
|
||||
NavigationHelper.playOnBackgroundPlayer(activity, getPlayQueue(), false));
|
||||
|
||||
playlistControlBinding.playlistCtrlPlayPopupButton.setOnLongClickListener(view -> {
|
||||
NavigationHelper.enqueueOnPopupPlayer(activity, getPlayQueue(), true);
|
||||
NavigationHelper.enqueueOnPlayer(activity, getPlayQueue(), PlayerType.POPUP);
|
||||
return true;
|
||||
});
|
||||
|
||||
playlistControlBinding.playlistCtrlPlayBgButton.setOnLongClickListener(view -> {
|
||||
NavigationHelper.enqueueOnBackgroundPlayer(activity, getPlayQueue(), true);
|
||||
NavigationHelper.enqueueOnPlayer(activity, getPlayQueue(), PlayerType.AUDIO);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -57,6 +57,7 @@ import org.schabi.newpipe.fragments.list.BaseListFragment;
|
||||
import org.schabi.newpipe.ktx.AnimationType;
|
||||
import org.schabi.newpipe.ktx.ExceptionUtils;
|
||||
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
||||
import org.schabi.newpipe.settings.NewPipeSettings;
|
||||
import org.schabi.newpipe.util.Constants;
|
||||
import org.schabi.newpipe.util.DeviceUtils;
|
||||
import org.schabi.newpipe.util.ExtractorHelper;
|
||||
@@ -65,16 +66,19 @@ import org.schabi.newpipe.util.ServiceHelper;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Queue;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import icepick.State;
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.rxjava3.core.Flowable;
|
||||
import io.reactivex.rxjava3.core.Observable;
|
||||
import io.reactivex.rxjava3.core.Single;
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
||||
import io.reactivex.rxjava3.disposables.Disposable;
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers;
|
||||
@@ -143,7 +147,8 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
||||
@Nullable private Map<Integer, String> menuItemToFilterName = null;
|
||||
private StreamingService service;
|
||||
private Page nextPage;
|
||||
private boolean isSuggestionsEnabled = true;
|
||||
private boolean showLocalSuggestions = true;
|
||||
private boolean showRemoteSuggestions = true;
|
||||
|
||||
private Disposable searchDisposable;
|
||||
private Disposable suggestionDisposable;
|
||||
@@ -194,26 +199,14 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
||||
public void onAttach(@NonNull final Context context) {
|
||||
super.onAttach(context);
|
||||
|
||||
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(activity);
|
||||
showLocalSuggestions = NewPipeSettings.showLocalSearchSuggestions(activity, prefs);
|
||||
showRemoteSuggestions = NewPipeSettings.showRemoteSearchSuggestions(activity, prefs);
|
||||
|
||||
suggestionListAdapter = new SuggestionListAdapter(activity);
|
||||
final SharedPreferences preferences
|
||||
= PreferenceManager.getDefaultSharedPreferences(activity);
|
||||
final boolean isSearchHistoryEnabled = preferences
|
||||
.getBoolean(getString(R.string.enable_search_history_key), true);
|
||||
suggestionListAdapter.setShowSuggestionHistory(isSearchHistoryEnabled);
|
||||
|
||||
historyRecordManager = new HistoryRecordManager(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(final Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
final SharedPreferences preferences
|
||||
= PreferenceManager.getDefaultSharedPreferences(activity);
|
||||
isSuggestionsEnabled = preferences
|
||||
.getBoolean(getString(R.string.show_search_suggestions_key), true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(final LayoutInflater inflater, @Nullable final ViewGroup container,
|
||||
@Nullable final Bundle savedInstanceState) {
|
||||
@@ -222,6 +215,7 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
||||
|
||||
@Override
|
||||
public void onViewCreated(@NonNull final View rootView, final Bundle savedInstanceState) {
|
||||
searchBinding = FragmentSearchBinding.bind(rootView);
|
||||
super.onViewCreated(rootView, savedInstanceState);
|
||||
showSearchOnStart();
|
||||
initSearchListeners();
|
||||
@@ -348,7 +342,6 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
||||
@Override
|
||||
protected void initViews(final View rootView, final Bundle savedInstanceState) {
|
||||
super.initViews(rootView, savedInstanceState);
|
||||
searchBinding = FragmentSearchBinding.bind(rootView);
|
||||
|
||||
searchBinding.suggestionsList.setAdapter(suggestionListAdapter);
|
||||
new ItemTouchHelper(new ItemTouchHelper.Callback() {
|
||||
@@ -554,7 +547,7 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "onClick() called with: v = [" + v + "]");
|
||||
}
|
||||
if (isSuggestionsEnabled && !isErrorPanelVisible()) {
|
||||
if ((showLocalSuggestions || showRemoteSuggestions) && !isErrorPanelVisible()) {
|
||||
showSuggestionsPanel();
|
||||
}
|
||||
if (DeviceUtils.isTv(getContext())) {
|
||||
@@ -567,7 +560,8 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
||||
Log.d(TAG, "onFocusChange() called with: "
|
||||
+ "v = [" + v + "], hasFocus = [" + hasFocus + "]");
|
||||
}
|
||||
if (isSuggestionsEnabled && hasFocus && !isErrorPanelVisible()) {
|
||||
if ((showLocalSuggestions || showRemoteSuggestions)
|
||||
&& hasFocus && !isErrorPanelVisible()) {
|
||||
showSuggestionsPanel();
|
||||
}
|
||||
});
|
||||
@@ -743,6 +737,34 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
private Observable<List<SuggestionItem>> getLocalSuggestionsObservable(
|
||||
final String query, final int similarQueryLimit) {
|
||||
return historyRecordManager
|
||||
.getRelatedSearches(query, similarQueryLimit, 25)
|
||||
.toObservable()
|
||||
.map(searchHistoryEntries -> {
|
||||
final Set<SuggestionItem> result = new HashSet<>(); // remove duplicates
|
||||
for (final SearchHistoryEntry entry : searchHistoryEntries) {
|
||||
result.add(new SuggestionItem(true, entry.getSearch()));
|
||||
}
|
||||
return new ArrayList<>(result);
|
||||
});
|
||||
}
|
||||
|
||||
private Observable<List<SuggestionItem>> getRemoteSuggestionsObservable(final String query) {
|
||||
return ExtractorHelper
|
||||
.suggestionsFor(serviceId, query)
|
||||
.toObservable()
|
||||
.map(strings -> {
|
||||
final List<SuggestionItem> result = new ArrayList<>();
|
||||
for (final String entry : strings) {
|
||||
result.add(new SuggestionItem(false, entry));
|
||||
}
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
private void initSuggestionObserver() {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "initSuggestionObserver() called");
|
||||
@@ -753,73 +775,53 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
||||
|
||||
suggestionDisposable = suggestionPublisher
|
||||
.debounce(SUGGESTIONS_DEBOUNCE, TimeUnit.MILLISECONDS)
|
||||
.startWithItem(searchString != null
|
||||
? searchString
|
||||
: "")
|
||||
.filter(ss -> isSuggestionsEnabled)
|
||||
.startWithItem(searchString == null ? "" : searchString)
|
||||
.switchMap(query -> {
|
||||
final Flowable<List<SearchHistoryEntry>> flowable = historyRecordManager
|
||||
.getRelatedSearches(query, 3, 25);
|
||||
final Observable<List<SuggestionItem>> local = flowable.toObservable()
|
||||
.map(searchHistoryEntries -> {
|
||||
final List<SuggestionItem> result = new ArrayList<>();
|
||||
for (final SearchHistoryEntry entry : searchHistoryEntries) {
|
||||
result.add(new SuggestionItem(true, entry.getSearch()));
|
||||
}
|
||||
return result;
|
||||
});
|
||||
// Only show remote suggestions if they are enabled in settings and
|
||||
// the query length is at least THRESHOLD_NETWORK_SUGGESTION
|
||||
final boolean shallShowRemoteSuggestionsNow = showRemoteSuggestions
|
||||
&& query.length() >= THRESHOLD_NETWORK_SUGGESTION;
|
||||
|
||||
if (query.length() < THRESHOLD_NETWORK_SUGGESTION) {
|
||||
// Only pass through if the query length
|
||||
// is equal or greater than THRESHOLD_NETWORK_SUGGESTION
|
||||
return local.materialize();
|
||||
if (showLocalSuggestions && shallShowRemoteSuggestionsNow) {
|
||||
return Observable.zip(
|
||||
getLocalSuggestionsObservable(query, 3),
|
||||
getRemoteSuggestionsObservable(query),
|
||||
(local, remote) -> {
|
||||
remote.removeIf(remoteItem -> local.stream().anyMatch(
|
||||
localItem -> localItem.equals(remoteItem)));
|
||||
local.addAll(remote);
|
||||
return local;
|
||||
})
|
||||
.materialize();
|
||||
} else if (showLocalSuggestions) {
|
||||
return getLocalSuggestionsObservable(query, 25)
|
||||
.materialize();
|
||||
} else if (shallShowRemoteSuggestionsNow) {
|
||||
return getRemoteSuggestionsObservable(query)
|
||||
.materialize();
|
||||
} else {
|
||||
return Single.fromCallable(Collections::<SuggestionItem>emptyList)
|
||||
.toObservable()
|
||||
.materialize();
|
||||
}
|
||||
|
||||
final Observable<List<SuggestionItem>> network = ExtractorHelper
|
||||
.suggestionsFor(serviceId, query)
|
||||
.onErrorReturn(throwable -> {
|
||||
if (!ExceptionUtils.isNetworkRelated(throwable)) {
|
||||
showSnackBarError(new ErrorInfo(throwable,
|
||||
UserAction.GET_SUGGESTIONS, searchString, serviceId));
|
||||
}
|
||||
return new ArrayList<>();
|
||||
})
|
||||
.toObservable()
|
||||
.map(strings -> {
|
||||
final List<SuggestionItem> result = new ArrayList<>();
|
||||
for (final String entry : strings) {
|
||||
result.add(new SuggestionItem(false, entry));
|
||||
}
|
||||
return result;
|
||||
});
|
||||
|
||||
return Observable.zip(local, network, (localResult, networkResult) -> {
|
||||
final List<SuggestionItem> result = new ArrayList<>();
|
||||
if (localResult.size() > 0) {
|
||||
result.addAll(localResult);
|
||||
}
|
||||
|
||||
// Remove duplicates
|
||||
networkResult.removeIf(networkItem ->
|
||||
localResult.stream().anyMatch(localItem ->
|
||||
localItem.query.equals(networkItem.query)));
|
||||
|
||||
if (networkResult.size() > 0) {
|
||||
result.addAll(networkResult);
|
||||
}
|
||||
return result;
|
||||
}).materialize();
|
||||
})
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(listNotification -> {
|
||||
if (listNotification.isOnNext()) {
|
||||
handleSuggestions(listNotification.getValue());
|
||||
} else if (listNotification.isOnError()) {
|
||||
showError(new ErrorInfo(listNotification.getError(),
|
||||
UserAction.GET_SUGGESTIONS, searchString, serviceId));
|
||||
}
|
||||
});
|
||||
.subscribe(
|
||||
listNotification -> {
|
||||
if (listNotification.isOnNext()) {
|
||||
if (listNotification.getValue() != null) {
|
||||
handleSuggestions(listNotification.getValue());
|
||||
}
|
||||
} else if (listNotification.isOnError()
|
||||
&& listNotification.getError() != null
|
||||
&& !ExceptionUtils.isInterruptedCaused(
|
||||
listNotification.getError())) {
|
||||
showSnackBarError(new ErrorInfo(listNotification.getError(),
|
||||
UserAction.GET_SUGGESTIONS, searchString, serviceId));
|
||||
}
|
||||
}, throwable -> showSnackBarError(new ErrorInfo(
|
||||
throwable, UserAction.GET_SUGGESTIONS, searchString, serviceId)));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package org.schabi.newpipe.fragments.list.search;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
public class SuggestionItem {
|
||||
final boolean fromHistory;
|
||||
public final String query;
|
||||
@@ -9,6 +11,20 @@ public class SuggestionItem {
|
||||
this.query = query;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(final Object o) {
|
||||
if (o instanceof SuggestionItem) {
|
||||
return query.equals(((SuggestionItem) o).query);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return query.hashCode();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String toString() {
|
||||
return "[" + fromHistory + "→" + query + "]";
|
||||
|
||||
@@ -19,7 +19,6 @@ public class SuggestionListAdapter
|
||||
private final ArrayList<SuggestionItem> items = new ArrayList<>();
|
||||
private final Context context;
|
||||
private OnSuggestionItemSelected listener;
|
||||
private boolean showSuggestionHistory = true;
|
||||
|
||||
public SuggestionListAdapter(final Context context) {
|
||||
this.context = context;
|
||||
@@ -27,16 +26,7 @@ public class SuggestionListAdapter
|
||||
|
||||
public void setItems(final List<SuggestionItem> items) {
|
||||
this.items.clear();
|
||||
if (showSuggestionHistory) {
|
||||
this.items.addAll(items);
|
||||
} else {
|
||||
// remove history items if history is disabled
|
||||
for (final SuggestionItem item : items) {
|
||||
if (!item.fromHistory) {
|
||||
this.items.add(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
this.items.addAll(items);
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
@@ -44,10 +34,6 @@ public class SuggestionListAdapter
|
||||
this.listener = listener;
|
||||
}
|
||||
|
||||
public void setShowSuggestionHistory(final boolean v) {
|
||||
showSuggestionHistory = v;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SuggestionItemHolder onCreateViewHolder(final ViewGroup parent, final int viewType) {
|
||||
return new SuggestionItemHolder(LayoutInflater.from(context)
|
||||
|
||||
@@ -6,8 +6,6 @@ import android.view.ViewGroup;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.nostra13.universalimageloader.core.ImageLoader;
|
||||
|
||||
import org.schabi.newpipe.extractor.InfoItem;
|
||||
import org.schabi.newpipe.extractor.channel.ChannelInfoItem;
|
||||
import org.schabi.newpipe.extractor.comments.CommentsInfoItem;
|
||||
@@ -51,7 +49,6 @@ import org.schabi.newpipe.util.OnClickGesture;
|
||||
|
||||
public class InfoItemBuilder {
|
||||
private final Context context;
|
||||
private final ImageLoader imageLoader = ImageLoader.getInstance();
|
||||
|
||||
private OnClickGesture<StreamInfoItem> onStreamSelectedListener;
|
||||
private OnClickGesture<ChannelInfoItem> onChannelSelectedListener;
|
||||
@@ -101,10 +98,6 @@ public class InfoItemBuilder {
|
||||
return context;
|
||||
}
|
||||
|
||||
public ImageLoader getImageLoader() {
|
||||
return imageLoader;
|
||||
}
|
||||
|
||||
public OnClickGesture<StreamInfoItem> getOnStreamSelectedListener() {
|
||||
return onStreamSelectedListener;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
package org.schabi.newpipe.info_list
|
||||
|
||||
import android.util.Log
|
||||
import com.xwray.groupie.GroupAdapter
|
||||
import com.xwray.groupie.GroupieViewHolder
|
||||
import com.xwray.groupie.GroupieAdapter
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfo
|
||||
import kotlin.math.max
|
||||
|
||||
@@ -11,7 +10,7 @@ import kotlin.math.max
|
||||
*/
|
||||
class StreamSegmentAdapter(
|
||||
private val listener: StreamSegmentListener
|
||||
) : GroupAdapter<GroupieViewHolder>() {
|
||||
) : GroupieAdapter() {
|
||||
|
||||
var currentIndex: Int = 0
|
||||
private set
|
||||
|
||||
@@ -3,13 +3,12 @@ package org.schabi.newpipe.info_list
|
||||
import android.view.View
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import com.nostra13.universalimageloader.core.ImageLoader
|
||||
import com.xwray.groupie.GroupieViewHolder
|
||||
import com.xwray.groupie.Item
|
||||
import org.schabi.newpipe.R
|
||||
import org.schabi.newpipe.extractor.stream.StreamSegment
|
||||
import org.schabi.newpipe.util.ImageDisplayConstants
|
||||
import org.schabi.newpipe.util.Localization
|
||||
import org.schabi.newpipe.util.PicassoHelper
|
||||
|
||||
class StreamSegmentItem(
|
||||
private val item: StreamSegment,
|
||||
@@ -24,10 +23,8 @@ class StreamSegmentItem(
|
||||
|
||||
override fun bind(viewHolder: GroupieViewHolder, position: Int) {
|
||||
item.previewUrl?.let {
|
||||
ImageLoader.getInstance().displayImage(
|
||||
it, viewHolder.root.findViewById<ImageView>(R.id.previewImage),
|
||||
ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS
|
||||
)
|
||||
PicassoHelper.loadThumbnail(it)
|
||||
.into(viewHolder.root.findViewById<ImageView>(R.id.previewImage))
|
||||
}
|
||||
viewHolder.root.findViewById<TextView>(R.id.textViewTitle).text = item.title
|
||||
if (item.channelName == null) {
|
||||
|
||||
@@ -8,7 +8,7 @@ import org.schabi.newpipe.extractor.InfoItem;
|
||||
import org.schabi.newpipe.extractor.channel.ChannelInfoItem;
|
||||
import org.schabi.newpipe.info_list.InfoItemBuilder;
|
||||
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
||||
import org.schabi.newpipe.util.ImageDisplayConstants;
|
||||
import org.schabi.newpipe.util.PicassoHelper;
|
||||
import org.schabi.newpipe.util.Localization;
|
||||
|
||||
import de.hdodenhof.circleimageview.CircleImageView;
|
||||
@@ -43,10 +43,7 @@ public class ChannelMiniInfoItemHolder extends InfoItemHolder {
|
||||
itemTitleView.setText(item.getName());
|
||||
itemAdditionalDetailView.setText(getDetailLine(item));
|
||||
|
||||
itemBuilder.getImageLoader()
|
||||
.displayImage(item.getThumbnailUrl(),
|
||||
itemThumbnailView,
|
||||
ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS);
|
||||
PicassoHelper.loadThumbnail(item.getThumbnailUrl()).into(itemThumbnailView);
|
||||
|
||||
itemView.setOnClickListener(view -> {
|
||||
if (itemBuilder.getOnChannelSelectedListener() != null) {
|
||||
|
||||
@@ -1,17 +1,16 @@
|
||||
package org.schabi.newpipe.info_list.holder;
|
||||
|
||||
import android.content.SharedPreferences;
|
||||
import android.text.TextUtils;
|
||||
import android.text.method.LinkMovementMethod;
|
||||
import android.text.style.URLSpan;
|
||||
import android.text.util.Linkify;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.RelativeLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.preference.PreferenceManager;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.error.ErrorActivity;
|
||||
@@ -21,30 +20,29 @@ import org.schabi.newpipe.info_list.InfoItemBuilder;
|
||||
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
||||
import org.schabi.newpipe.util.CommentTextOnTouchListener;
|
||||
import org.schabi.newpipe.util.DeviceUtils;
|
||||
import org.schabi.newpipe.util.ImageDisplayConstants;
|
||||
import org.schabi.newpipe.util.Localization;
|
||||
import org.schabi.newpipe.util.NavigationHelper;
|
||||
import org.schabi.newpipe.util.external_communication.TimestampExtractor;
|
||||
import org.schabi.newpipe.util.external_communication.ShareUtils;
|
||||
import org.schabi.newpipe.util.PicassoHelper;
|
||||
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import de.hdodenhof.circleimageview.CircleImageView;
|
||||
|
||||
public class CommentsMiniInfoItemHolder extends InfoItemHolder {
|
||||
private static final String TAG = "CommentsMiniIIHolder";
|
||||
|
||||
private static final int COMMENT_DEFAULT_LINES = 2;
|
||||
private static final int COMMENT_EXPANDED_LINES = 1000;
|
||||
private static final Pattern PATTERN = Pattern.compile("(\\d+:)?(\\d+)?:(\\d+)");
|
||||
private final String downloadThumbnailKey;
|
||||
|
||||
private final int commentHorizontalPadding;
|
||||
private final int commentVerticalPadding;
|
||||
|
||||
private SharedPreferences preferences = null;
|
||||
private final RelativeLayout itemRoot;
|
||||
public final CircleImageView itemThumbnailView;
|
||||
private final TextView itemContentView;
|
||||
private final TextView itemLikesCountView;
|
||||
private final TextView itemDislikesCountView;
|
||||
private final TextView itemPublishedTime;
|
||||
|
||||
private String commentText;
|
||||
@@ -53,20 +51,21 @@ public class CommentsMiniInfoItemHolder extends InfoItemHolder {
|
||||
private final Linkify.TransformFilter timestampLink = new Linkify.TransformFilter() {
|
||||
@Override
|
||||
public String transformUrl(final Matcher match, final String url) {
|
||||
int timestamp = 0;
|
||||
final String hours = match.group(1);
|
||||
final String minutes = match.group(2);
|
||||
final String seconds = match.group(3);
|
||||
if (hours != null) {
|
||||
timestamp += (Integer.parseInt(hours.replace(":", "")) * 3600);
|
||||
try {
|
||||
final TimestampExtractor.TimestampMatchDTO timestampMatchDTO =
|
||||
TimestampExtractor.getTimestampFromMatcher(match, commentText);
|
||||
|
||||
if (timestampMatchDTO == null) {
|
||||
return url;
|
||||
}
|
||||
|
||||
return streamUrl + url.replace(
|
||||
match.group(0),
|
||||
"#timestamp=" + timestampMatchDTO.seconds());
|
||||
} catch (final Exception ex) {
|
||||
Log.e(TAG, "Unable to process url='" + url + "' as timestampLink", ex);
|
||||
return url;
|
||||
}
|
||||
if (minutes != null) {
|
||||
timestamp += (Integer.parseInt(minutes.replace(":", "")) * 60);
|
||||
}
|
||||
if (seconds != null) {
|
||||
timestamp += (Integer.parseInt(seconds));
|
||||
}
|
||||
return streamUrl + url.replace(match.group(0), "#timestamp=" + timestamp);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -77,13 +76,9 @@ public class CommentsMiniInfoItemHolder extends InfoItemHolder {
|
||||
itemRoot = itemView.findViewById(R.id.itemRoot);
|
||||
itemThumbnailView = itemView.findViewById(R.id.itemThumbnailView);
|
||||
itemLikesCountView = itemView.findViewById(R.id.detail_thumbs_up_count_view);
|
||||
itemDislikesCountView = itemView.findViewById(R.id.detail_thumbs_down_count_view);
|
||||
itemPublishedTime = itemView.findViewById(R.id.itemPublishedTime);
|
||||
itemContentView = itemView.findViewById(R.id.itemCommentContentView);
|
||||
|
||||
downloadThumbnailKey = infoItemBuilder.getContext().
|
||||
getString(R.string.download_thumbnail_key);
|
||||
|
||||
commentHorizontalPadding = (int) infoItemBuilder.getContext()
|
||||
.getResources().getDimension(R.dimen.comments_horizontal_padding);
|
||||
commentVerticalPadding = (int) infoItemBuilder.getContext()
|
||||
@@ -103,14 +98,8 @@ public class CommentsMiniInfoItemHolder extends InfoItemHolder {
|
||||
}
|
||||
final CommentsInfoItem item = (CommentsInfoItem) infoItem;
|
||||
|
||||
preferences = PreferenceManager.getDefaultSharedPreferences(itemBuilder.getContext());
|
||||
|
||||
itemBuilder.getImageLoader()
|
||||
.displayImage(item.getUploaderAvatarUrl(),
|
||||
itemThumbnailView,
|
||||
ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS);
|
||||
|
||||
if (preferences.getBoolean(downloadThumbnailKey, true)) {
|
||||
PicassoHelper.loadAvatar(item.getUploaderAvatarUrl()).into(itemThumbnailView);
|
||||
if (PicassoHelper.getShouldLoadImages()) {
|
||||
itemThumbnailView.setVisibility(View.VISIBLE);
|
||||
itemRoot.setPadding(commentVerticalPadding, commentVerticalPadding,
|
||||
commentVerticalPadding, commentVerticalPadding);
|
||||
@@ -254,7 +243,14 @@ public class CommentsMiniInfoItemHolder extends InfoItemHolder {
|
||||
}
|
||||
|
||||
private void linkify() {
|
||||
Linkify.addLinks(itemContentView, Linkify.WEB_URLS);
|
||||
Linkify.addLinks(itemContentView, PATTERN, null, null, timestampLink);
|
||||
Linkify.addLinks(
|
||||
itemContentView,
|
||||
Linkify.WEB_URLS);
|
||||
Linkify.addLinks(
|
||||
itemContentView,
|
||||
TimestampExtractor.TIMESTAMPS_PATTERN,
|
||||
null,
|
||||
null,
|
||||
timestampLink);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import org.schabi.newpipe.extractor.InfoItem;
|
||||
import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem;
|
||||
import org.schabi.newpipe.info_list.InfoItemBuilder;
|
||||
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
||||
import org.schabi.newpipe.util.ImageDisplayConstants;
|
||||
import org.schabi.newpipe.util.PicassoHelper;
|
||||
import org.schabi.newpipe.util.Localization;
|
||||
|
||||
public class PlaylistMiniInfoItemHolder extends InfoItemHolder {
|
||||
@@ -46,9 +46,7 @@ public class PlaylistMiniInfoItemHolder extends InfoItemHolder {
|
||||
.localizeStreamCountMini(itemStreamCountView.getContext(), item.getStreamCount()));
|
||||
itemUploaderView.setText(item.getUploaderName());
|
||||
|
||||
itemBuilder.getImageLoader()
|
||||
.displayImage(item.getThumbnailUrl(), itemThumbnailView,
|
||||
ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS);
|
||||
PicassoHelper.loadPlaylistThumbnail(item.getThumbnailUrl()).into(itemThumbnailView);
|
||||
|
||||
itemView.setOnClickListener(view -> {
|
||||
if (itemBuilder.getOnPlaylistSelectedListener() != null) {
|
||||
|
||||
@@ -15,7 +15,7 @@ import org.schabi.newpipe.extractor.stream.StreamType;
|
||||
import org.schabi.newpipe.info_list.InfoItemBuilder;
|
||||
import org.schabi.newpipe.ktx.ViewUtils;
|
||||
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
||||
import org.schabi.newpipe.util.ImageDisplayConstants;
|
||||
import org.schabi.newpipe.util.PicassoHelper;
|
||||
import org.schabi.newpipe.util.Localization;
|
||||
import org.schabi.newpipe.views.AnimatedProgressBar;
|
||||
|
||||
@@ -83,10 +83,7 @@ public class StreamMiniInfoItemHolder extends InfoItemHolder {
|
||||
}
|
||||
|
||||
// Default thumbnail is shown on error, while loading and if the url is empty
|
||||
itemBuilder.getImageLoader()
|
||||
.displayImage(item.getThumbnailUrl(),
|
||||
itemThumbnailView,
|
||||
ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS);
|
||||
PicassoHelper.loadThumbnail(item.getThumbnailUrl()).into(itemThumbnailView);
|
||||
|
||||
itemView.setOnClickListener(view -> {
|
||||
if (itemBuilder.getOnStreamSelectedListener() != null) {
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
package org.schabi.newpipe.local;
|
||||
|
||||
import android.content.Context;
|
||||
import android.widget.ImageView;
|
||||
|
||||
import com.nostra13.universalimageloader.core.DisplayImageOptions;
|
||||
import com.nostra13.universalimageloader.core.ImageLoader;
|
||||
|
||||
import org.schabi.newpipe.database.LocalItem;
|
||||
import org.schabi.newpipe.util.OnClickGesture;
|
||||
@@ -31,7 +27,6 @@ import org.schabi.newpipe.util.OnClickGesture;
|
||||
|
||||
public class LocalItemBuilder {
|
||||
private final Context context;
|
||||
private final ImageLoader imageLoader = ImageLoader.getInstance();
|
||||
|
||||
private OnClickGesture<LocalItem> onSelectedListener;
|
||||
|
||||
@@ -43,11 +38,6 @@ public class LocalItemBuilder {
|
||||
return context;
|
||||
}
|
||||
|
||||
public void displayImage(final String url, final ImageView view,
|
||||
final DisplayImageOptions options) {
|
||||
imageLoader.displayImage(url, view, options);
|
||||
}
|
||||
|
||||
public OnClickGesture<LocalItem> getOnItemSelectedListener() {
|
||||
return onSelectedListener;
|
||||
}
|
||||
|
||||
@@ -40,8 +40,7 @@ import androidx.core.view.isVisible
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.preference.PreferenceManager
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import com.xwray.groupie.GroupAdapter
|
||||
import com.xwray.groupie.GroupieViewHolder
|
||||
import com.xwray.groupie.GroupieAdapter
|
||||
import com.xwray.groupie.Item
|
||||
import com.xwray.groupie.OnItemClickListener
|
||||
import com.xwray.groupie.OnItemLongClickListener
|
||||
@@ -91,7 +90,7 @@ class FeedFragment : BaseStateFragment<FeedState>() {
|
||||
private var groupName = ""
|
||||
private var oldestSubscriptionUpdate: OffsetDateTime? = null
|
||||
|
||||
private lateinit var groupAdapter: GroupAdapter<GroupieViewHolder>
|
||||
private lateinit var groupAdapter: GroupieAdapter
|
||||
@State @JvmField var showPlayedItems: Boolean = true
|
||||
|
||||
private var onSettingsChangeListener: SharedPreferences.OnSharedPreferenceChangeListener? = null
|
||||
@@ -131,7 +130,7 @@ class FeedFragment : BaseStateFragment<FeedState>() {
|
||||
viewModel = ViewModelProvider(this, factory).get(FeedViewModel::class.java)
|
||||
viewModel.stateLiveData.observe(viewLifecycleOwner, { it?.let(::handleResult) })
|
||||
|
||||
groupAdapter = GroupAdapter<GroupieViewHolder>().apply {
|
||||
groupAdapter = GroupieAdapter().apply {
|
||||
setOnItemClickListener(listenerStreamItem)
|
||||
setOnItemLongClickListener(listenerStreamItem)
|
||||
}
|
||||
@@ -206,7 +205,7 @@ class FeedFragment : BaseStateFragment<FeedState>() {
|
||||
putBoolean(getString(R.string.feed_use_dedicated_fetch_method_key), !usingDedicatedMethod)
|
||||
}
|
||||
}
|
||||
.setPositiveButton(resources.getString(R.string.finish), null)
|
||||
.setPositiveButton(resources.getString(R.string.ok), null)
|
||||
.create()
|
||||
.show()
|
||||
return true
|
||||
@@ -326,9 +325,14 @@ class FeedFragment : BaseStateFragment<FeedState>() {
|
||||
if (context == null || context.resources == null || activity == null) return
|
||||
|
||||
val entries = ArrayList<StreamDialogEntry>()
|
||||
if (PlayerHolder.getInstance().getType() != null) {
|
||||
if (PlayerHolder.getInstance().isPlayerOpen) {
|
||||
entries.add(StreamDialogEntry.enqueue)
|
||||
|
||||
if (PlayerHolder.getInstance().queueSize > 1) {
|
||||
entries.add(StreamDialogEntry.enqueue_next)
|
||||
}
|
||||
}
|
||||
|
||||
if (item.streamType == StreamType.AUDIO_STREAM) {
|
||||
entries.addAll(
|
||||
listOf(
|
||||
@@ -362,6 +366,7 @@ class FeedFragment : BaseStateFragment<FeedState>() {
|
||||
StreamDialogEntry.mark_as_watched
|
||||
)
|
||||
}
|
||||
entries.add(StreamDialogEntry.show_channel_details)
|
||||
|
||||
StreamDialogEntry.setEnabledEntries(entries)
|
||||
InfoItemDialog(activity, item, StreamDialogEntry.getCommands(context)) { _, which ->
|
||||
|
||||
@@ -5,7 +5,6 @@ import android.text.TextUtils
|
||||
import android.view.View
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.preference.PreferenceManager
|
||||
import com.nostra13.universalimageloader.core.ImageLoader
|
||||
import com.xwray.groupie.viewbinding.BindableItem
|
||||
import org.schabi.newpipe.MainActivity
|
||||
import org.schabi.newpipe.R
|
||||
@@ -16,8 +15,9 @@ import org.schabi.newpipe.extractor.stream.StreamType.AUDIO_LIVE_STREAM
|
||||
import org.schabi.newpipe.extractor.stream.StreamType.AUDIO_STREAM
|
||||
import org.schabi.newpipe.extractor.stream.StreamType.LIVE_STREAM
|
||||
import org.schabi.newpipe.extractor.stream.StreamType.VIDEO_STREAM
|
||||
import org.schabi.newpipe.util.ImageDisplayConstants
|
||||
import org.schabi.newpipe.util.Localization
|
||||
import org.schabi.newpipe.util.PicassoHelper
|
||||
import org.schabi.newpipe.util.StreamTypeUtil
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
data class StreamItem(
|
||||
@@ -59,8 +59,6 @@ data class StreamItem(
|
||||
viewBinding.itemVideoTitleView.text = stream.title
|
||||
viewBinding.itemUploaderView.text = stream.uploader
|
||||
|
||||
val isLiveStream = stream.streamType == LIVE_STREAM || stream.streamType == AUDIO_LIVE_STREAM
|
||||
|
||||
if (stream.duration > 0) {
|
||||
viewBinding.itemDurationView.text = Localization.getDurationString(stream.duration)
|
||||
viewBinding.itemDurationView.setBackgroundColor(
|
||||
@@ -78,7 +76,7 @@ data class StreamItem(
|
||||
} else {
|
||||
viewBinding.itemProgressView.visibility = View.GONE
|
||||
}
|
||||
} else if (isLiveStream) {
|
||||
} else if (StreamTypeUtil.isLiveStream(stream.streamType)) {
|
||||
viewBinding.itemDurationView.setText(R.string.duration_live)
|
||||
viewBinding.itemDurationView.setBackgroundColor(
|
||||
ContextCompat.getColor(
|
||||
@@ -93,10 +91,7 @@ data class StreamItem(
|
||||
viewBinding.itemProgressView.visibility = View.GONE
|
||||
}
|
||||
|
||||
ImageLoader.getInstance().displayImage(
|
||||
stream.thumbnailUrl, viewBinding.itemThumbnailView,
|
||||
ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS
|
||||
)
|
||||
PicassoHelper.loadThumbnail(stream.thumbnailUrl).into(viewBinding.itemThumbnailView)
|
||||
|
||||
if (itemVersion != ItemVersion.MINI) {
|
||||
viewBinding.itemAdditionalDetails.text =
|
||||
|
||||
@@ -300,6 +300,12 @@ class FeedLoadService : Service() {
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe { _, throwable ->
|
||||
// There seems to be a bug in the kotlin plugin as it tells you when
|
||||
// building that this can't be null:
|
||||
// "Condition 'throwable != null' is always 'true'"
|
||||
// However it can indeed be null
|
||||
// The suppression may be removed in further versions
|
||||
@Suppress("SENSELESS_COMPARISON")
|
||||
if (throwable != null) {
|
||||
Log.e(TAG, "Error while storing result", throwable)
|
||||
handleError(throwable)
|
||||
|
||||
@@ -53,8 +53,6 @@ import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
||||
import io.reactivex.rxjava3.disposables.Disposable;
|
||||
|
||||
import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty;
|
||||
|
||||
public class StatisticsPlaylistFragment
|
||||
extends BaseLocalListFragment<List<StreamStatisticsEntry>, Void> {
|
||||
private final CompositeDisposable disposables = new CompositeDisposable();
|
||||
@@ -340,9 +338,14 @@ public class StatisticsPlaylistFragment
|
||||
|
||||
final ArrayList<StreamDialogEntry> entries = new ArrayList<>();
|
||||
|
||||
if (PlayerHolder.getInstance().getType() != null) {
|
||||
if (PlayerHolder.getInstance().isPlayerOpen()) {
|
||||
entries.add(StreamDialogEntry.enqueue);
|
||||
|
||||
if (PlayerHolder.getInstance().getQueueSize() > 1) {
|
||||
entries.add(StreamDialogEntry.enqueue_next);
|
||||
}
|
||||
}
|
||||
|
||||
if (infoItem.getStreamType() == StreamType.AUDIO_STREAM) {
|
||||
entries.addAll(Arrays.asList(
|
||||
StreamDialogEntry.start_here_on_background,
|
||||
@@ -363,10 +366,7 @@ public class StatisticsPlaylistFragment
|
||||
if (KoreUtils.shouldShowPlayWithKodi(context, infoItem.getServiceId())) {
|
||||
entries.add(StreamDialogEntry.play_with_kodi);
|
||||
}
|
||||
|
||||
if (!isNullOrEmpty(infoItem.getUploaderUrl())) {
|
||||
entries.add(StreamDialogEntry.show_channel_details);
|
||||
}
|
||||
entries.add(StreamDialogEntry.show_channel_details);
|
||||
|
||||
StreamDialogEntry.setEnabledEntries(entries);
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import org.schabi.newpipe.database.LocalItem;
|
||||
import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry;
|
||||
import org.schabi.newpipe.local.LocalItemBuilder;
|
||||
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
||||
import org.schabi.newpipe.util.ImageDisplayConstants;
|
||||
import org.schabi.newpipe.util.PicassoHelper;
|
||||
import org.schabi.newpipe.util.Localization;
|
||||
|
||||
import java.time.format.DateTimeFormatter;
|
||||
@@ -36,8 +36,7 @@ public class LocalPlaylistItemHolder extends PlaylistItemHolder {
|
||||
itemStreamCountView.getContext(), item.streamCount));
|
||||
itemUploaderView.setVisibility(View.INVISIBLE);
|
||||
|
||||
itemBuilder.displayImage(item.thumbnailUrl, itemThumbnailView,
|
||||
ImageDisplayConstants.DISPLAY_PLAYLIST_OPTIONS);
|
||||
PicassoHelper.loadPlaylistThumbnail(item.thumbnailUrl).into(itemThumbnailView);
|
||||
|
||||
super.updateFromItem(localItem, historyRecordManager, dateTimeFormatter);
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ import org.schabi.newpipe.extractor.NewPipe;
|
||||
import org.schabi.newpipe.ktx.ViewUtils;
|
||||
import org.schabi.newpipe.local.LocalItemBuilder;
|
||||
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
||||
import org.schabi.newpipe.util.ImageDisplayConstants;
|
||||
import org.schabi.newpipe.util.PicassoHelper;
|
||||
import org.schabi.newpipe.util.Localization;
|
||||
import org.schabi.newpipe.views.AnimatedProgressBar;
|
||||
|
||||
@@ -81,8 +81,8 @@ public class LocalPlaylistStreamItemHolder extends LocalItemHolder {
|
||||
}
|
||||
|
||||
// Default thumbnail is shown on error, while loading and if the url is empty
|
||||
itemBuilder.displayImage(item.getStreamEntity().getThumbnailUrl(), itemThumbnailView,
|
||||
ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS);
|
||||
PicassoHelper.loadThumbnail(item.getStreamEntity().getThumbnailUrl())
|
||||
.into(itemThumbnailView);
|
||||
|
||||
itemView.setOnClickListener(view -> {
|
||||
if (itemBuilder.getOnItemSelectedListener() != null) {
|
||||
|
||||
@@ -15,7 +15,7 @@ import org.schabi.newpipe.extractor.NewPipe;
|
||||
import org.schabi.newpipe.ktx.ViewUtils;
|
||||
import org.schabi.newpipe.local.LocalItemBuilder;
|
||||
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
||||
import org.schabi.newpipe.util.ImageDisplayConstants;
|
||||
import org.schabi.newpipe.util.PicassoHelper;
|
||||
import org.schabi.newpipe.util.Localization;
|
||||
import org.schabi.newpipe.views.AnimatedProgressBar;
|
||||
|
||||
@@ -114,8 +114,8 @@ public class LocalStatisticStreamItemHolder extends LocalItemHolder {
|
||||
}
|
||||
|
||||
// Default thumbnail is shown on error, while loading and if the url is empty
|
||||
itemBuilder.displayImage(item.getStreamEntity().getThumbnailUrl(), itemThumbnailView,
|
||||
ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS);
|
||||
PicassoHelper.loadThumbnail(item.getStreamEntity().getThumbnailUrl())
|
||||
.into(itemThumbnailView);
|
||||
|
||||
itemView.setOnClickListener(view -> {
|
||||
if (itemBuilder.getOnItemSelectedListener() != null) {
|
||||
|
||||
@@ -8,7 +8,7 @@ import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity;
|
||||
import org.schabi.newpipe.extractor.NewPipe;
|
||||
import org.schabi.newpipe.local.LocalItemBuilder;
|
||||
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
||||
import org.schabi.newpipe.util.ImageDisplayConstants;
|
||||
import org.schabi.newpipe.util.PicassoHelper;
|
||||
import org.schabi.newpipe.util.Localization;
|
||||
|
||||
import java.time.format.DateTimeFormatter;
|
||||
@@ -44,9 +44,7 @@ public class RemotePlaylistItemHolder extends PlaylistItemHolder {
|
||||
itemUploaderView.setText(NewPipe.getNameOfService(item.getServiceId()));
|
||||
}
|
||||
|
||||
|
||||
itemBuilder.displayImage(item.getThumbnailUrl(), itemThumbnailView,
|
||||
ImageDisplayConstants.DISPLAY_PLAYLIST_OPTIONS);
|
||||
PicassoHelper.loadPlaylistThumbnail(item.getThumbnailUrl()).into(itemThumbnailView);
|
||||
|
||||
super.updateFromItem(localItem, historyRecordManager, dateTimeFormatter);
|
||||
}
|
||||
|
||||
@@ -42,6 +42,7 @@ import org.schabi.newpipe.extractor.stream.StreamType;
|
||||
import org.schabi.newpipe.info_list.InfoItemDialog;
|
||||
import org.schabi.newpipe.local.BaseLocalListFragment;
|
||||
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
||||
import org.schabi.newpipe.player.MainPlayer.PlayerType;
|
||||
import org.schabi.newpipe.player.helper.PlayerHolder;
|
||||
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
||||
import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
|
||||
@@ -67,7 +68,6 @@ import io.reactivex.rxjava3.disposables.Disposable;
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers;
|
||||
import io.reactivex.rxjava3.subjects.PublishSubject;
|
||||
|
||||
import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty;
|
||||
import static org.schabi.newpipe.ktx.ViewUtils.animate;
|
||||
import static org.schabi.newpipe.util.ThemeHelper.shouldUseGridLayout;
|
||||
|
||||
@@ -494,12 +494,12 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
|
||||
NavigationHelper.playOnBackgroundPlayer(activity, getPlayQueue(), false));
|
||||
|
||||
playlistControlBinding.playlistCtrlPlayPopupButton.setOnLongClickListener(view -> {
|
||||
NavigationHelper.enqueueOnPopupPlayer(activity, getPlayQueue(), true);
|
||||
NavigationHelper.enqueueOnPlayer(activity, getPlayQueue(), PlayerType.POPUP);
|
||||
return true;
|
||||
});
|
||||
|
||||
playlistControlBinding.playlistCtrlPlayBgButton.setOnLongClickListener(view -> {
|
||||
NavigationHelper.enqueueOnBackgroundPlayer(activity, getPlayQueue(), true);
|
||||
NavigationHelper.enqueueOnPlayer(activity, getPlayQueue(), PlayerType.AUDIO);
|
||||
return true;
|
||||
});
|
||||
|
||||
@@ -753,8 +753,12 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
|
||||
|
||||
final ArrayList<StreamDialogEntry> entries = new ArrayList<>();
|
||||
|
||||
if (PlayerHolder.getInstance().getType() != null) {
|
||||
if (PlayerHolder.getInstance().isPlayerOpen()) {
|
||||
entries.add(StreamDialogEntry.enqueue);
|
||||
|
||||
if (PlayerHolder.getInstance().getQueueSize() > 1) {
|
||||
entries.add(StreamDialogEntry.enqueue_next);
|
||||
}
|
||||
}
|
||||
if (infoItem.getStreamType() == StreamType.AUDIO_STREAM) {
|
||||
entries.addAll(Arrays.asList(
|
||||
@@ -778,10 +782,7 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
|
||||
if (KoreUtils.shouldShowPlayWithKodi(context, infoItem.getServiceId())) {
|
||||
entries.add(StreamDialogEntry.play_with_kodi);
|
||||
}
|
||||
|
||||
if (!isNullOrEmpty(infoItem.getUploaderUrl())) {
|
||||
entries.add(StreamDialogEntry.show_channel_details);
|
||||
}
|
||||
entries.add(StreamDialogEntry.show_channel_details);
|
||||
|
||||
StreamDialogEntry.setEnabledEntries(entries);
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ enum class FeedGroupIcon(
|
||||
COMPUTER(5, R.drawable.ic_computer),
|
||||
GAMING(6, R.drawable.ic_videogame_asset),
|
||||
SPORTS(7, R.drawable.ic_directions_bike),
|
||||
NEWS(8, R.drawable.ic_megaphone),
|
||||
NEWS(8, R.drawable.ic_campaign),
|
||||
FAVORITES(9, R.drawable.ic_favorite),
|
||||
CAR(10, R.drawable.ic_directions_car),
|
||||
MOTORCYCLE(11, R.drawable.ic_motorcycle),
|
||||
|
||||
@@ -40,7 +40,7 @@ public class ImportConfirmationDialog extends DialogFragment {
|
||||
.setMessage(R.string.import_network_expensive_warning)
|
||||
.setCancelable(true)
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.setPositiveButton(R.string.finish, (dialogInterface, i) -> {
|
||||
.setPositiveButton(R.string.ok, (dialogInterface, i) -> {
|
||||
if (resultServiceIntent != null && getContext() != null) {
|
||||
getContext().startService(resultServiceIntent);
|
||||
}
|
||||
|
||||
@@ -179,7 +179,7 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
|
||||
}
|
||||
|
||||
private fun onImportPreviousSelected() {
|
||||
requestImportLauncher.launch(StoredFileHelper.getPicker(activity))
|
||||
requestImportLauncher.launch(StoredFileHelper.getPicker(activity, JSON_MIME_TYPE))
|
||||
}
|
||||
|
||||
private fun onExportSelected() {
|
||||
@@ -187,7 +187,7 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
|
||||
val exportName = "newpipe_subscriptions_$date.json"
|
||||
|
||||
requestExportLauncher.launch(
|
||||
StoredFileHelper.getNewPicker(activity, exportName, "application/json", null)
|
||||
StoredFileHelper.getNewPicker(activity, exportName, JSON_MIME_TYPE, null)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -195,7 +195,7 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
|
||||
FeedGroupReorderDialog().show(parentFragmentManager, null)
|
||||
}
|
||||
|
||||
fun requestExportResult(result: ActivityResult) {
|
||||
private fun requestExportResult(result: ActivityResult) {
|
||||
if (result.data != null && result.resultCode == Activity.RESULT_OK) {
|
||||
activity.startService(
|
||||
Intent(activity, SubscriptionsExportService::class.java)
|
||||
@@ -204,7 +204,7 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
|
||||
}
|
||||
}
|
||||
|
||||
fun requestImportResult(result: ActivityResult) {
|
||||
private fun requestImportResult(result: ActivityResult) {
|
||||
if (result.data != null && result.resultCode == Activity.RESULT_OK) {
|
||||
ImportConfirmationDialog.show(
|
||||
this,
|
||||
@@ -407,4 +407,8 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
|
||||
super.hideLoading()
|
||||
binding.itemsList.animate(true, 200)
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val JSON_MIME_TYPE = "application/json"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -177,7 +177,8 @@ public class SubscriptionsImportFragment extends BaseFragment {
|
||||
}
|
||||
|
||||
public void onImportFile() {
|
||||
requestImportFileLauncher.launch(StoredFileHelper.getPicker(activity));
|
||||
// leave */* mime type to support all services with different mime types and file extensions
|
||||
requestImportFileLauncher.launch(StoredFileHelper.getPicker(activity, "*/*"));
|
||||
}
|
||||
|
||||
private void requestImportFileResult(final ActivityResult result) {
|
||||
|
||||
@@ -21,8 +21,7 @@ import androidx.lifecycle.Observer
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.xwray.groupie.GroupAdapter
|
||||
import com.xwray.groupie.GroupieViewHolder
|
||||
import com.xwray.groupie.GroupieAdapter
|
||||
import com.xwray.groupie.OnItemClickListener
|
||||
import com.xwray.groupie.Section
|
||||
import icepick.Icepick
|
||||
@@ -78,7 +77,7 @@ class FeedGroupDialog : DialogFragment(), BackPressable {
|
||||
|
||||
private val subscriptionMainSection = Section()
|
||||
private val subscriptionEmptyFooter = Section()
|
||||
private lateinit var subscriptionGroupAdapter: GroupAdapter<GroupieViewHolder>
|
||||
private lateinit var subscriptionGroupAdapter: GroupieAdapter
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
@@ -143,23 +142,17 @@ class FeedGroupDialog : DialogFragment(), BackPressable {
|
||||
).get(FeedGroupDialogViewModel::class.java)
|
||||
|
||||
viewModel.groupLiveData.observe(viewLifecycleOwner, Observer(::handleGroup))
|
||||
viewModel.subscriptionsLiveData.observe(
|
||||
viewLifecycleOwner,
|
||||
Observer {
|
||||
setupSubscriptionPicker(it.first, it.second)
|
||||
viewModel.subscriptionsLiveData.observe(viewLifecycleOwner) {
|
||||
setupSubscriptionPicker(it.first, it.second)
|
||||
}
|
||||
viewModel.dialogEventLiveData.observe(viewLifecycleOwner) {
|
||||
when (it) {
|
||||
ProcessingEvent -> disableInput()
|
||||
SuccessEvent -> dismiss()
|
||||
}
|
||||
)
|
||||
viewModel.dialogEventLiveData.observe(
|
||||
viewLifecycleOwner,
|
||||
Observer {
|
||||
when (it) {
|
||||
ProcessingEvent -> disableInput()
|
||||
SuccessEvent -> dismiss()
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
subscriptionGroupAdapter = GroupAdapter<GroupieViewHolder>().apply {
|
||||
subscriptionGroupAdapter = GroupieAdapter().apply {
|
||||
add(subscriptionMainSection)
|
||||
add(subscriptionEmptyFooter)
|
||||
spanCount = 4
|
||||
@@ -385,7 +378,7 @@ class FeedGroupDialog : DialogFragment(), BackPressable {
|
||||
}
|
||||
|
||||
private fun setupIconPicker() {
|
||||
val groupAdapter = GroupAdapter<GroupieViewHolder>()
|
||||
val groupAdapter = GroupieAdapter()
|
||||
groupAdapter.addAll(FeedGroupIcon.values().map { PickerIconItem(it) })
|
||||
|
||||
feedGroupCreateBinding.iconSelector.apply {
|
||||
@@ -437,7 +430,7 @@ class FeedGroupDialog : DialogFragment(), BackPressable {
|
||||
feedGroupCreateBinding.confirmButton.setText(
|
||||
when {
|
||||
currentScreen == InitialScreen && groupId == NO_GROUP_SELECTED -> R.string.create
|
||||
else -> android.R.string.ok
|
||||
else -> R.string.ok
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -11,8 +11,7 @@ import androidx.recyclerview.widget.ItemTouchHelper
|
||||
import androidx.recyclerview.widget.ItemTouchHelper.SimpleCallback
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.xwray.groupie.GroupAdapter
|
||||
import com.xwray.groupie.GroupieViewHolder
|
||||
import com.xwray.groupie.GroupieAdapter
|
||||
import com.xwray.groupie.TouchCallback
|
||||
import icepick.Icepick
|
||||
import icepick.State
|
||||
@@ -38,7 +37,7 @@ class FeedGroupReorderDialog : DialogFragment() {
|
||||
@State
|
||||
@JvmField
|
||||
var groupOrderedIdList = ArrayList<Long>()
|
||||
private val groupAdapter = GroupAdapter<GroupieViewHolder>()
|
||||
private val groupAdapter = GroupieAdapter()
|
||||
private val itemTouchHelper = ItemTouchHelper(getItemTouchCallback())
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
|
||||
@@ -3,14 +3,13 @@ package org.schabi.newpipe.local.subscription.item
|
||||
import android.content.Context
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import com.nostra13.universalimageloader.core.ImageLoader
|
||||
import com.xwray.groupie.GroupieViewHolder
|
||||
import com.xwray.groupie.Item
|
||||
import org.schabi.newpipe.R
|
||||
import org.schabi.newpipe.extractor.channel.ChannelInfoItem
|
||||
import org.schabi.newpipe.util.ImageDisplayConstants
|
||||
import org.schabi.newpipe.util.Localization
|
||||
import org.schabi.newpipe.util.OnClickGesture
|
||||
import org.schabi.newpipe.util.PicassoHelper
|
||||
|
||||
class ChannelItem(
|
||||
private val infoItem: ChannelInfoItem,
|
||||
@@ -40,10 +39,7 @@ class ChannelItem(
|
||||
itemChannelDescriptionView.text = infoItem.description
|
||||
}
|
||||
|
||||
ImageLoader.getInstance().displayImage(
|
||||
infoItem.thumbnailUrl, itemThumbnailView,
|
||||
ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS
|
||||
)
|
||||
PicassoHelper.loadThumbnail(infoItem.thumbnailUrl).into(itemThumbnailView)
|
||||
|
||||
gesturesListener?.run {
|
||||
viewHolder.root.setOnClickListener { selected(infoItem) }
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
package org.schabi.newpipe.local.subscription.item
|
||||
|
||||
import android.view.View
|
||||
import android.view.View.OnClickListener
|
||||
import com.xwray.groupie.viewbinding.BindableItem
|
||||
import org.schabi.newpipe.R
|
||||
import org.schabi.newpipe.databinding.HeaderItemBinding
|
||||
|
||||
class HeaderItem(
|
||||
val title: String,
|
||||
private val onClickListener: (() -> Unit)? = null
|
||||
) : BindableItem<HeaderItemBinding>() {
|
||||
override fun getLayout(): Int = R.layout.header_item
|
||||
|
||||
override fun bind(viewBinding: HeaderItemBinding, position: Int) {
|
||||
viewBinding.headerTitle.text = title
|
||||
|
||||
val listener = onClickListener?.let { OnClickListener { onClickListener.invoke() } }
|
||||
viewBinding.root.setOnClickListener(listener)
|
||||
}
|
||||
|
||||
override fun initializeViewBinding(view: View) = HeaderItemBinding.bind(view)
|
||||
}
|
||||
@@ -3,7 +3,6 @@ package org.schabi.newpipe.local.subscription.item
|
||||
import android.view.View
|
||||
import androidx.core.view.isGone
|
||||
import androidx.core.view.isVisible
|
||||
import com.nostra13.universalimageloader.core.ImageLoader
|
||||
import com.xwray.groupie.viewbinding.BindableItem
|
||||
import com.xwray.groupie.viewbinding.GroupieViewHolder
|
||||
import org.schabi.newpipe.R
|
||||
@@ -11,7 +10,7 @@ import org.schabi.newpipe.database.subscription.SubscriptionEntity
|
||||
import org.schabi.newpipe.databinding.PickerSubscriptionItemBinding
|
||||
import org.schabi.newpipe.ktx.AnimationType
|
||||
import org.schabi.newpipe.ktx.animate
|
||||
import org.schabi.newpipe.util.ImageDisplayConstants
|
||||
import org.schabi.newpipe.util.PicassoHelper
|
||||
|
||||
data class PickerSubscriptionItem(
|
||||
val subscriptionEntity: SubscriptionEntity,
|
||||
@@ -22,11 +21,7 @@ data class PickerSubscriptionItem(
|
||||
override fun getSpanSize(spanCount: Int, position: Int): Int = 1
|
||||
|
||||
override fun bind(viewBinding: PickerSubscriptionItemBinding, position: Int) {
|
||||
ImageLoader.getInstance().displayImage(
|
||||
subscriptionEntity.avatarUrl,
|
||||
viewBinding.thumbnailView, ImageDisplayConstants.DISPLAY_AVATAR_OPTIONS
|
||||
)
|
||||
|
||||
PicassoHelper.loadAvatar(subscriptionEntity.avatarUrl).into(viewBinding.thumbnailView)
|
||||
viewBinding.titleView.text = subscriptionEntity.name
|
||||
viewBinding.selectedHighlight.isVisible = isSelected
|
||||
}
|
||||
|
||||
@@ -19,6 +19,9 @@
|
||||
|
||||
package org.schabi.newpipe.local.subscription.services;
|
||||
|
||||
import static org.schabi.newpipe.MainActivity.DEBUG;
|
||||
import static org.schabi.newpipe.streams.io.StoredFileHelper.DEFAULT_MIME;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.text.TextUtils;
|
||||
@@ -46,6 +49,7 @@ import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.rxjava3.core.Flowable;
|
||||
@@ -54,9 +58,6 @@ import io.reactivex.rxjava3.functions.Consumer;
|
||||
import io.reactivex.rxjava3.functions.Function;
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers;
|
||||
|
||||
import static org.schabi.newpipe.MainActivity.DEBUG;
|
||||
import static org.schabi.newpipe.streams.io.StoredFileHelper.DEFAULT_MIME;
|
||||
|
||||
public class SubscriptionsImportService extends BaseImportExportService {
|
||||
public static final int CHANNEL_URL_MODE = 0;
|
||||
public static final int INPUT_STREAM_MODE = 1;
|
||||
@@ -89,6 +90,8 @@ public class SubscriptionsImportService extends BaseImportExportService {
|
||||
private String channelUrl;
|
||||
@Nullable
|
||||
private InputStream inputStream;
|
||||
@Nullable
|
||||
private String inputStreamType;
|
||||
|
||||
@Override
|
||||
public int onStartCommand(final Intent intent, final int flags, final int startId) {
|
||||
@@ -111,8 +114,20 @@ public class SubscriptionsImportService extends BaseImportExportService {
|
||||
}
|
||||
|
||||
try {
|
||||
inputStream = new SharpInputStream(
|
||||
new StoredFileHelper(this, uri, DEFAULT_MIME).getStream());
|
||||
final StoredFileHelper fileHelper = new StoredFileHelper(this, uri, DEFAULT_MIME);
|
||||
inputStream = new SharpInputStream(fileHelper.getStream());
|
||||
inputStreamType = fileHelper.getType();
|
||||
|
||||
if (inputStreamType == null || inputStreamType.equals(DEFAULT_MIME)) {
|
||||
// mime type could not be determined, just take file extension
|
||||
final String name = fileHelper.getName();
|
||||
final int pointIndex = name.lastIndexOf('.');
|
||||
if (pointIndex == -1 || pointIndex >= name.length() - 1) {
|
||||
inputStreamType = DEFAULT_MIME; // no extension, will fail in the extractor
|
||||
} else {
|
||||
inputStreamType = name.substring(pointIndex + 1);
|
||||
}
|
||||
}
|
||||
} catch (final IOException e) {
|
||||
handleError(e);
|
||||
return START_NOT_STICKY;
|
||||
@@ -248,9 +263,9 @@ public class SubscriptionsImportService extends BaseImportExportService {
|
||||
final Throwable error = notification.getError();
|
||||
final Throwable cause = error.getCause();
|
||||
if (error instanceof IOException) {
|
||||
throw (IOException) error;
|
||||
throw error;
|
||||
} else if (cause instanceof IOException) {
|
||||
throw (IOException) cause;
|
||||
throw cause;
|
||||
} else if (ExceptionUtils.isNetworkRelated(error)) {
|
||||
throw new IOException(error);
|
||||
}
|
||||
@@ -280,9 +295,12 @@ public class SubscriptionsImportService extends BaseImportExportService {
|
||||
}
|
||||
|
||||
private Flowable<List<SubscriptionItem>> importFromInputStream() {
|
||||
Objects.requireNonNull(inputStream);
|
||||
Objects.requireNonNull(inputStreamType);
|
||||
|
||||
return Flowable.fromCallable(() -> NewPipe.getService(currentServiceId)
|
||||
.getSubscriptionExtractor()
|
||||
.fromInputStream(inputStream));
|
||||
.fromInputStream(inputStream, inputStreamType));
|
||||
}
|
||||
|
||||
private Flowable<List<SubscriptionItem>> importFromPreviousExport() {
|
||||
|
||||
@@ -24,7 +24,6 @@ import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Binder;
|
||||
import android.os.IBinder;
|
||||
import android.util.DisplayMetrics;
|
||||
import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
@@ -36,6 +35,7 @@ import androidx.core.content.ContextCompat;
|
||||
|
||||
import org.schabi.newpipe.App;
|
||||
import org.schabi.newpipe.databinding.PlayerBinding;
|
||||
import org.schabi.newpipe.util.DeviceUtils;
|
||||
import org.schabi.newpipe.util.ThemeHelper;
|
||||
|
||||
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
|
||||
@@ -133,32 +133,29 @@ public final class MainPlayer extends Service {
|
||||
return START_NOT_STICKY;
|
||||
}
|
||||
|
||||
public void stop(final boolean autoplayEnabled) {
|
||||
public void stopForImmediateReusing() {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "stop() called");
|
||||
Log.d(TAG, "stopForImmediateReusing() called");
|
||||
}
|
||||
|
||||
if (!player.exoPlayerIsNull()) {
|
||||
player.saveWasPlaying();
|
||||
|
||||
// Releases wifi & cpu, disables keepScreenOn, etc.
|
||||
if (!autoplayEnabled) {
|
||||
player.pause();
|
||||
}
|
||||
// We can't just pause the player here because it will make transition
|
||||
// from one stream to a new stream not smooth
|
||||
player.smoothStopPlayer();
|
||||
player.setRecovery();
|
||||
|
||||
// Android TV will handle back button in case controls will be visible
|
||||
// (one more additional unneeded click while the player is hidden)
|
||||
player.hideControls(0, 0);
|
||||
player.closeItemsList();
|
||||
|
||||
// Notification shows information about old stream but if a user selects
|
||||
// a stream from backStack it's not actual anymore
|
||||
// So we should hide the notification at all.
|
||||
// When autoplay enabled such notification flashing is annoying so skip this case
|
||||
if (!autoplayEnabled) {
|
||||
NotificationUtil.getInstance().cancelNotificationAndStopForeground(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -222,11 +219,8 @@ public final class MainPlayer extends Service {
|
||||
boolean isLandscape() {
|
||||
// DisplayMetrics from activity context knows about MultiWindow feature
|
||||
// while DisplayMetrics from app context doesn't
|
||||
final DisplayMetrics metrics = (player != null
|
||||
&& player.getParentActivity() != null
|
||||
? player.getParentActivity().getResources()
|
||||
: getResources()).getDisplayMetrics();
|
||||
return metrics.heightPixels < metrics.widthPixels;
|
||||
return DeviceUtils.isLandscape(player != null && player.getParentActivity() != null
|
||||
? player.getParentActivity() : this);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
package org.schabi.newpipe.player;
|
||||
|
||||
import static org.schabi.newpipe.player.helper.PlayerHelper.formatSpeed;
|
||||
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
|
||||
import static org.schabi.newpipe.util.external_communication.ShareUtils.shareText;
|
||||
|
||||
import android.content.ComponentName;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
@@ -16,7 +12,6 @@ import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.PopupMenu;
|
||||
import android.widget.SeekBar;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
@@ -47,16 +42,18 @@ import org.schabi.newpipe.util.PermissionHelper;
|
||||
import org.schabi.newpipe.util.ServiceHelper;
|
||||
import org.schabi.newpipe.util.ThemeHelper;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import static org.schabi.newpipe.QueueItemMenuUtil.openPopupMenu;
|
||||
import static org.schabi.newpipe.player.helper.PlayerHelper.formatSpeed;
|
||||
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
|
||||
|
||||
public final class PlayQueueActivity extends AppCompatActivity
|
||||
implements PlayerEventListener, SeekBar.OnSeekBarChangeListener,
|
||||
View.OnClickListener, PlaybackParameterDialog.Callback {
|
||||
|
||||
private static final String TAG = PlayQueueActivity.class.getSimpleName();
|
||||
|
||||
private static final int RECYCLER_ITEM_POPUP_MENU_GROUP_ID = 47;
|
||||
private static final int SMOOTH_SCROLL_MAXIMUM_DISTANCE = 80;
|
||||
|
||||
protected Player player;
|
||||
@@ -279,49 +276,6 @@ public final class PlayQueueActivity extends AppCompatActivity
|
||||
queueControlBinding.controlShuffle.setOnClickListener(this);
|
||||
}
|
||||
|
||||
private void buildItemPopupMenu(final PlayQueueItem item, final View view) {
|
||||
final PopupMenu popupMenu = new PopupMenu(this, view);
|
||||
final MenuItem remove = popupMenu.getMenu().add(RECYCLER_ITEM_POPUP_MENU_GROUP_ID, 0,
|
||||
Menu.NONE, R.string.play_queue_remove);
|
||||
remove.setOnMenuItemClickListener(menuItem -> {
|
||||
if (player == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final int index = player.getPlayQueue().indexOf(item);
|
||||
if (index != -1) {
|
||||
player.getPlayQueue().remove(index);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
final MenuItem detail = popupMenu.getMenu().add(RECYCLER_ITEM_POPUP_MENU_GROUP_ID, 1,
|
||||
Menu.NONE, R.string.play_queue_stream_detail);
|
||||
detail.setOnMenuItemClickListener(menuItem -> {
|
||||
// playQueue is null since we don't want any queue change
|
||||
NavigationHelper.openVideoDetail(this, item.getServiceId(), item.getUrl(),
|
||||
item.getTitle(), null, false);
|
||||
return true;
|
||||
});
|
||||
|
||||
final MenuItem append = popupMenu.getMenu().add(RECYCLER_ITEM_POPUP_MENU_GROUP_ID, 2,
|
||||
Menu.NONE, R.string.append_playlist);
|
||||
append.setOnMenuItemClickListener(menuItem -> {
|
||||
openPlaylistAppendDialog(Collections.singletonList(item));
|
||||
return true;
|
||||
});
|
||||
|
||||
final MenuItem share = popupMenu.getMenu().add(RECYCLER_ITEM_POPUP_MENU_GROUP_ID, 3,
|
||||
Menu.NONE, R.string.share);
|
||||
share.setOnMenuItemClickListener(menuItem -> {
|
||||
shareText(getApplicationContext(), item.getTitle(), item.getUrl(),
|
||||
item.getThumbnailUrl());
|
||||
return true;
|
||||
});
|
||||
|
||||
popupMenu.show();
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
// Component Helpers
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
@@ -369,13 +323,9 @@ public final class PlayQueueActivity extends AppCompatActivity
|
||||
|
||||
@Override
|
||||
public void held(final PlayQueueItem item, final View view) {
|
||||
if (player == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final int index = player.getPlayQueue().indexOf(item);
|
||||
if (index != -1) {
|
||||
buildItemPopupMenu(item, view);
|
||||
if (player != null && player.getPlayQueue().indexOf(item) != -1) {
|
||||
openPopupMenu(player.getPlayQueue(), item, view, false,
|
||||
getSupportFragmentManager(), PlayQueueActivity.this);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,53 @@
|
||||
package org.schabi.newpipe.player;
|
||||
|
||||
import static com.google.android.exoplayer2.Player.DISCONTINUITY_REASON_AD_INSERTION;
|
||||
import static com.google.android.exoplayer2.Player.DISCONTINUITY_REASON_INTERNAL;
|
||||
import static com.google.android.exoplayer2.Player.DISCONTINUITY_REASON_PERIOD_TRANSITION;
|
||||
import static com.google.android.exoplayer2.Player.DISCONTINUITY_REASON_SEEK;
|
||||
import static com.google.android.exoplayer2.Player.DISCONTINUITY_REASON_SEEK_ADJUSTMENT;
|
||||
import static com.google.android.exoplayer2.Player.DiscontinuityReason;
|
||||
import static com.google.android.exoplayer2.Player.EventListener;
|
||||
import static com.google.android.exoplayer2.Player.REPEAT_MODE_ALL;
|
||||
import static com.google.android.exoplayer2.Player.REPEAT_MODE_OFF;
|
||||
import static com.google.android.exoplayer2.Player.REPEAT_MODE_ONE;
|
||||
import static com.google.android.exoplayer2.Player.RepeatMode;
|
||||
import static org.schabi.newpipe.QueueItemMenuUtil.openPopupMenu;
|
||||
import static org.schabi.newpipe.extractor.ServiceList.YouTube;
|
||||
import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty;
|
||||
import static org.schabi.newpipe.ktx.ViewUtils.animate;
|
||||
import static org.schabi.newpipe.ktx.ViewUtils.animateRotation;
|
||||
import static org.schabi.newpipe.player.MainPlayer.ACTION_CLOSE;
|
||||
import static org.schabi.newpipe.player.MainPlayer.ACTION_FAST_FORWARD;
|
||||
import static org.schabi.newpipe.player.MainPlayer.ACTION_FAST_REWIND;
|
||||
import static org.schabi.newpipe.player.MainPlayer.ACTION_PLAY_NEXT;
|
||||
import static org.schabi.newpipe.player.MainPlayer.ACTION_PLAY_PAUSE;
|
||||
import static org.schabi.newpipe.player.MainPlayer.ACTION_PLAY_PREVIOUS;
|
||||
import static org.schabi.newpipe.player.MainPlayer.ACTION_RECREATE_NOTIFICATION;
|
||||
import static org.schabi.newpipe.player.MainPlayer.ACTION_REPEAT;
|
||||
import static org.schabi.newpipe.player.MainPlayer.ACTION_SHUFFLE;
|
||||
import static org.schabi.newpipe.player.helper.PlayerHelper.MinimizeMode.MINIMIZE_ON_EXIT_MODE_BACKGROUND;
|
||||
import static org.schabi.newpipe.player.helper.PlayerHelper.MinimizeMode.MINIMIZE_ON_EXIT_MODE_NONE;
|
||||
import static org.schabi.newpipe.player.helper.PlayerHelper.MinimizeMode.MINIMIZE_ON_EXIT_MODE_POPUP;
|
||||
import static org.schabi.newpipe.player.helper.PlayerHelper.buildCloseOverlayLayoutParams;
|
||||
import static org.schabi.newpipe.player.helper.PlayerHelper.formatSpeed;
|
||||
import static org.schabi.newpipe.player.helper.PlayerHelper.getMinimizeOnExitAction;
|
||||
import static org.schabi.newpipe.player.helper.PlayerHelper.getMinimumVideoHeight;
|
||||
import static org.schabi.newpipe.player.helper.PlayerHelper.getTimeString;
|
||||
import static org.schabi.newpipe.player.helper.PlayerHelper.globalScreenOrientationLocked;
|
||||
import static org.schabi.newpipe.player.helper.PlayerHelper.isPlaybackResumeEnabled;
|
||||
import static org.schabi.newpipe.player.helper.PlayerHelper.nextRepeatMode;
|
||||
import static org.schabi.newpipe.player.helper.PlayerHelper.nextResizeModeAndSaveToPrefs;
|
||||
import static org.schabi.newpipe.player.helper.PlayerHelper.retrievePlaybackParametersFromPrefs;
|
||||
import static org.schabi.newpipe.player.helper.PlayerHelper.retrievePlayerTypeFromIntent;
|
||||
import static org.schabi.newpipe.player.helper.PlayerHelper.retrievePopupLayoutParamsFromPrefs;
|
||||
import static org.schabi.newpipe.player.helper.PlayerHelper.retrieveSeekDurationFromPreferences;
|
||||
import static org.schabi.newpipe.player.helper.PlayerHelper.savePlaybackParametersToPrefs;
|
||||
import static org.schabi.newpipe.util.ListHelper.getPopupResolutionIndex;
|
||||
import static org.schabi.newpipe.util.ListHelper.getResolutionIndex;
|
||||
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
|
||||
import static org.schabi.newpipe.util.Localization.containsCaseInsensitive;
|
||||
import static java.util.concurrent.TimeUnit.MILLISECONDS;
|
||||
|
||||
import android.animation.Animator;
|
||||
import android.animation.AnimatorListenerAdapter;
|
||||
import android.animation.ObjectAnimator;
|
||||
@@ -18,6 +66,7 @@ import android.graphics.BitmapFactory;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.PorterDuff;
|
||||
import android.graphics.PorterDuffColorFilter;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.media.AudioManager;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
@@ -82,9 +131,8 @@ import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
import com.google.android.exoplayer2.video.VideoListener;
|
||||
import com.google.android.material.floatingactionbutton.FloatingActionButton;
|
||||
import com.nostra13.universalimageloader.core.ImageLoader;
|
||||
import com.nostra13.universalimageloader.core.assist.FailReason;
|
||||
import com.nostra13.universalimageloader.core.listener.ImageLoadingListener;
|
||||
import com.squareup.picasso.Picasso;
|
||||
import com.squareup.picasso.Target;
|
||||
|
||||
import org.schabi.newpipe.DownloaderImpl;
|
||||
import org.schabi.newpipe.MainActivity;
|
||||
@@ -94,7 +142,6 @@ import org.schabi.newpipe.databinding.PlayerPopupCloseOverlayBinding;
|
||||
import org.schabi.newpipe.extractor.MediaFormat;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||
import org.schabi.newpipe.extractor.stream.StreamSegment;
|
||||
import org.schabi.newpipe.extractor.stream.StreamType;
|
||||
import org.schabi.newpipe.extractor.stream.VideoStream;
|
||||
import org.schabi.newpipe.fragments.OnScrollBelowItemsListener;
|
||||
import org.schabi.newpipe.fragments.detail.VideoDetailFragment;
|
||||
@@ -127,10 +174,11 @@ import org.schabi.newpipe.player.resolver.MediaSourceTag;
|
||||
import org.schabi.newpipe.player.resolver.VideoPlaybackResolver;
|
||||
import org.schabi.newpipe.player.seekbarpreview.SeekbarPreviewThumbnailHelper;
|
||||
import org.schabi.newpipe.player.seekbarpreview.SeekbarPreviewThumbnailHolder;
|
||||
import org.schabi.newpipe.util.StreamTypeUtil;
|
||||
import org.schabi.newpipe.util.DeviceUtils;
|
||||
import org.schabi.newpipe.util.ImageDisplayConstants;
|
||||
import org.schabi.newpipe.util.ListHelper;
|
||||
import org.schabi.newpipe.util.NavigationHelper;
|
||||
import org.schabi.newpipe.util.PicassoHelper;
|
||||
import org.schabi.newpipe.util.SerializedCache;
|
||||
import org.schabi.newpipe.util.external_communication.KoreUtils;
|
||||
import org.schabi.newpipe.util.external_communication.ShareUtils;
|
||||
@@ -140,6 +188,7 @@ import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.rxjava3.core.Observable;
|
||||
@@ -147,56 +196,9 @@ import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
||||
import io.reactivex.rxjava3.disposables.Disposable;
|
||||
import io.reactivex.rxjava3.disposables.SerialDisposable;
|
||||
|
||||
import static com.google.android.exoplayer2.Player.DISCONTINUITY_REASON_AD_INSERTION;
|
||||
import static com.google.android.exoplayer2.Player.DISCONTINUITY_REASON_INTERNAL;
|
||||
import static com.google.android.exoplayer2.Player.DISCONTINUITY_REASON_PERIOD_TRANSITION;
|
||||
import static com.google.android.exoplayer2.Player.DISCONTINUITY_REASON_SEEK;
|
||||
import static com.google.android.exoplayer2.Player.DISCONTINUITY_REASON_SEEK_ADJUSTMENT;
|
||||
import static com.google.android.exoplayer2.Player.DiscontinuityReason;
|
||||
import static com.google.android.exoplayer2.Player.EventListener;
|
||||
import static com.google.android.exoplayer2.Player.REPEAT_MODE_ALL;
|
||||
import static com.google.android.exoplayer2.Player.REPEAT_MODE_OFF;
|
||||
import static com.google.android.exoplayer2.Player.REPEAT_MODE_ONE;
|
||||
import static com.google.android.exoplayer2.Player.RepeatMode;
|
||||
import static java.util.concurrent.TimeUnit.MILLISECONDS;
|
||||
import static org.schabi.newpipe.extractor.ServiceList.YouTube;
|
||||
import static org.schabi.newpipe.ktx.ViewUtils.animate;
|
||||
import static org.schabi.newpipe.ktx.ViewUtils.animateRotation;
|
||||
import static org.schabi.newpipe.player.MainPlayer.ACTION_CLOSE;
|
||||
import static org.schabi.newpipe.player.MainPlayer.ACTION_FAST_FORWARD;
|
||||
import static org.schabi.newpipe.player.MainPlayer.ACTION_FAST_REWIND;
|
||||
import static org.schabi.newpipe.player.MainPlayer.ACTION_PLAY_NEXT;
|
||||
import static org.schabi.newpipe.player.MainPlayer.ACTION_PLAY_PAUSE;
|
||||
import static org.schabi.newpipe.player.MainPlayer.ACTION_PLAY_PREVIOUS;
|
||||
import static org.schabi.newpipe.player.MainPlayer.ACTION_RECREATE_NOTIFICATION;
|
||||
import static org.schabi.newpipe.player.MainPlayer.ACTION_REPEAT;
|
||||
import static org.schabi.newpipe.player.MainPlayer.ACTION_SHUFFLE;
|
||||
import static org.schabi.newpipe.player.helper.PlayerHelper.MinimizeMode.MINIMIZE_ON_EXIT_MODE_BACKGROUND;
|
||||
import static org.schabi.newpipe.player.helper.PlayerHelper.MinimizeMode.MINIMIZE_ON_EXIT_MODE_NONE;
|
||||
import static org.schabi.newpipe.player.helper.PlayerHelper.MinimizeMode.MINIMIZE_ON_EXIT_MODE_POPUP;
|
||||
import static org.schabi.newpipe.player.helper.PlayerHelper.buildCloseOverlayLayoutParams;
|
||||
import static org.schabi.newpipe.player.helper.PlayerHelper.formatSpeed;
|
||||
import static org.schabi.newpipe.player.helper.PlayerHelper.getMinimizeOnExitAction;
|
||||
import static org.schabi.newpipe.player.helper.PlayerHelper.getMinimumVideoHeight;
|
||||
import static org.schabi.newpipe.player.helper.PlayerHelper.getTimeString;
|
||||
import static org.schabi.newpipe.player.helper.PlayerHelper.globalScreenOrientationLocked;
|
||||
import static org.schabi.newpipe.player.helper.PlayerHelper.isPlaybackResumeEnabled;
|
||||
import static org.schabi.newpipe.player.helper.PlayerHelper.nextRepeatMode;
|
||||
import static org.schabi.newpipe.player.helper.PlayerHelper.nextResizeModeAndSaveToPrefs;
|
||||
import static org.schabi.newpipe.player.helper.PlayerHelper.retrievePlaybackParametersFromPrefs;
|
||||
import static org.schabi.newpipe.player.helper.PlayerHelper.retrievePlayerTypeFromIntent;
|
||||
import static org.schabi.newpipe.player.helper.PlayerHelper.retrievePopupLayoutParamsFromPrefs;
|
||||
import static org.schabi.newpipe.player.helper.PlayerHelper.retrieveSeekDurationFromPreferences;
|
||||
import static org.schabi.newpipe.player.helper.PlayerHelper.savePlaybackParametersToPrefs;
|
||||
import static org.schabi.newpipe.util.ListHelper.getPopupResolutionIndex;
|
||||
import static org.schabi.newpipe.util.ListHelper.getResolutionIndex;
|
||||
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
|
||||
import static org.schabi.newpipe.util.Localization.containsCaseInsensitive;
|
||||
|
||||
public final class Player implements
|
||||
EventListener,
|
||||
PlaybackListener,
|
||||
ImageLoadingListener,
|
||||
VideoListener,
|
||||
SeekBar.OnSeekBarChangeListener,
|
||||
View.OnClickListener,
|
||||
@@ -225,10 +227,10 @@ public final class Player implements
|
||||
public static final String REPEAT_MODE = "repeat_mode";
|
||||
public static final String PLAYBACK_QUALITY = "playback_quality";
|
||||
public static final String PLAY_QUEUE_KEY = "play_queue_key";
|
||||
public static final String APPEND_ONLY = "append_only";
|
||||
public static final String ENQUEUE = "enqueue";
|
||||
public static final String ENQUEUE_NEXT = "enqueue_next";
|
||||
public static final String RESUME_PLAYBACK = "resume_playback";
|
||||
public static final String PLAY_WHEN_READY = "play_when_ready";
|
||||
public static final String SELECT_ON_APPEND = "select_on_append";
|
||||
public static final String PLAYER_TYPE = "player_type";
|
||||
public static final String IS_MUTED = "is_muted";
|
||||
|
||||
@@ -237,7 +239,7 @@ public final class Player implements
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
public static final int PLAY_PREV_ACTIVATION_LIMIT_MILLIS = 5000; // 5 seconds
|
||||
public static final int PROGRESS_LOOP_INTERVAL_MILLIS = 500; // 500 millis
|
||||
public static final int PROGRESS_LOOP_INTERVAL_MILLIS = 1000; // 1 second
|
||||
public static final int DEFAULT_CONTROLS_DURATION = 300; // 300 millis
|
||||
public static final int DEFAULT_CONTROLS_HIDE_TIME = 2000; // 2 Seconds
|
||||
public static final int DPAD_CONTROLS_HIDE_TIME = 7000; // 7 Seconds
|
||||
@@ -390,7 +392,7 @@ public final class Player implements
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Constructor
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
//region
|
||||
//region Constructor
|
||||
|
||||
public Player(@NonNull final MainPlayer service) {
|
||||
this.service = service;
|
||||
@@ -437,7 +439,7 @@ public final class Player implements
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Setup and initialization
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
//region
|
||||
//region Setup and initialization
|
||||
|
||||
public void setupFromView(@NonNull final PlayerBinding playerBinding) {
|
||||
initViews(playerBinding);
|
||||
@@ -585,7 +587,7 @@ public final class Player implements
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Playback initialization via intent
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
//region
|
||||
//region Playback initialization via intent
|
||||
|
||||
public void handleIntent(@NonNull final Intent intent) {
|
||||
// fail fast if no play queue was provided
|
||||
@@ -607,19 +609,22 @@ public final class Player implements
|
||||
setPlaybackQuality(intent.getStringExtra(PLAYBACK_QUALITY));
|
||||
}
|
||||
|
||||
// Resolve append intents
|
||||
if (intent.getBooleanExtra(APPEND_ONLY, false) && playQueue != null) {
|
||||
final int sizeBeforeAppend = playQueue.size();
|
||||
// Resolve enqueue intents
|
||||
if (intent.getBooleanExtra(ENQUEUE, false) && playQueue != null) {
|
||||
playQueue.append(newQueue.getStreams());
|
||||
return;
|
||||
|
||||
if ((intent.getBooleanExtra(SELECT_ON_APPEND, false)
|
||||
|| currentState == STATE_COMPLETED) && newQueue.getStreams().size() > 0) {
|
||||
playQueue.setIndex(sizeBeforeAppend);
|
||||
}
|
||||
|
||||
// Resolve enqueue next intents
|
||||
} else if (intent.getBooleanExtra(ENQUEUE_NEXT, false) && playQueue != null) {
|
||||
final int currentIndex = playQueue.getIndex();
|
||||
playQueue.append(newQueue.getStreams());
|
||||
playQueue.move(playQueue.size() - 1, currentIndex + 1);
|
||||
return;
|
||||
}
|
||||
|
||||
// needed for tablets, check the function for a better explanation
|
||||
directlyOpenFullscreenIfNeeded();
|
||||
|
||||
final PlaybackParameters savedParameters = retrievePlaybackParametersFromPrefs(this);
|
||||
final float playbackSpeed = savedParameters.speed;
|
||||
final float playbackPitch = savedParameters.pitch;
|
||||
@@ -671,6 +676,7 @@ public final class Player implements
|
||||
&& isPlaybackResumeEnabled(this)
|
||||
&& !samePlayQueue
|
||||
&& !newQueue.isEmpty()
|
||||
&& newQueue.getItem() != null
|
||||
&& newQueue.getItem().getRecoveryPosition() == PlayQueueItem.RECOVERY_UNSET) {
|
||||
databaseUpdateDisposable.add(recordManager.loadStreamState(newQueue.getItem())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
@@ -742,6 +748,22 @@ public final class Player implements
|
||||
NavigationHelper.sendPlayerStartedEvent(context);
|
||||
}
|
||||
|
||||
/**
|
||||
* Open fullscreen on tablets where the option to have the main player start automatically in
|
||||
* fullscreen mode is on. Rotating the device to landscape is already done in {@link
|
||||
* VideoDetailFragment#openVideoPlayer(boolean)} when the thumbnail is clicked, and that's
|
||||
* enough for phones, but not for tablets since the mini player can be also shown in landscape.
|
||||
*/
|
||||
private void directlyOpenFullscreenIfNeeded() {
|
||||
if (fragmentListener != null
|
||||
&& PlayerHelper.isStartMainPlayerFullscreenEnabled(service)
|
||||
&& DeviceUtils.isTablet(service)
|
||||
&& videoPlayerSelected()
|
||||
&& PlayerHelper.globalScreenOrientationLocked(service)) {
|
||||
fragmentListener.onScreenRotationButtonClicked();
|
||||
}
|
||||
}
|
||||
|
||||
private void initPlayback(@NonNull final PlayQueue queue,
|
||||
@RepeatMode final int repeatMode,
|
||||
final float playbackSpeed,
|
||||
@@ -774,7 +796,7 @@ public final class Player implements
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Destroy and recovery
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
//region
|
||||
//region Destroy and recovery
|
||||
|
||||
private void destroyPlayer() {
|
||||
if (DEBUG) {
|
||||
@@ -820,7 +842,7 @@ public final class Player implements
|
||||
|
||||
databaseUpdateDisposable.clear();
|
||||
progressUpdateDisposable.set(null);
|
||||
ImageLoader.getInstance().stop();
|
||||
PicassoHelper.cancelTag(PicassoHelper.PLAYER_THUMBNAIL_TAG); // cancel thumbnail loading
|
||||
|
||||
if (binding != null) {
|
||||
binding.endScreen.setImageBitmap(null);
|
||||
@@ -883,7 +905,7 @@ public final class Player implements
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Player type specific setup
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
//region
|
||||
//region Player type specific setup
|
||||
|
||||
private void initVideoPlayer() {
|
||||
// restore last resize mode
|
||||
@@ -945,7 +967,7 @@ public final class Player implements
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Elements visibility and size: popup and main players have different look
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
//region
|
||||
//region Elements visibility and size: popup and main players have different look
|
||||
|
||||
/**
|
||||
* This method ensures that popup and main players have different look.
|
||||
@@ -1059,7 +1081,7 @@ public final class Player implements
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Broadcast receiver
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
//region
|
||||
//region Broadcast receiver
|
||||
|
||||
private void setupBroadcastReceiver() {
|
||||
if (DEBUG) {
|
||||
@@ -1211,18 +1233,49 @@ public final class Player implements
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Thumbnail loading
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
//region
|
||||
//region Thumbnail loading
|
||||
|
||||
private void initThumbnail(final String url) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "Thumbnail - initThumbnail() called");
|
||||
Log.d(TAG, "Thumbnail - initThumbnail() called with url = ["
|
||||
+ (url == null ? "null" : url) + "]");
|
||||
}
|
||||
if (url == null || url.isEmpty()) {
|
||||
if (isNullOrEmpty(url)) {
|
||||
return;
|
||||
}
|
||||
ImageLoader.getInstance().resume();
|
||||
ImageLoader.getInstance()
|
||||
.loadImage(url, ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS, this);
|
||||
|
||||
// scale down the notification thumbnail for performance
|
||||
PicassoHelper.loadScaledDownThumbnail(context, url).into(new Target() {
|
||||
@Override
|
||||
public void onBitmapLoaded(final Bitmap bitmap, final Picasso.LoadedFrom from) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "Thumbnail - onLoadingComplete() called with: url = [" + url
|
||||
+ "], " + "loadedImage = [" + bitmap + " -> " + bitmap.getWidth() + "x"
|
||||
+ bitmap.getHeight() + "], from = [" + from + "]");
|
||||
}
|
||||
|
||||
currentThumbnail = bitmap;
|
||||
NotificationUtil.getInstance()
|
||||
.createNotificationIfNeededAndUpdate(Player.this, false);
|
||||
// there is a new thumbnail, so changed the end screen thumbnail, too.
|
||||
updateEndScreenThumbnail();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBitmapFailed(final Exception e, final Drawable errorDrawable) {
|
||||
Log.e(TAG, "Thumbnail - onBitmapFailed() called with: url = [" + url + "]", e);
|
||||
currentThumbnail = null;
|
||||
NotificationUtil.getInstance()
|
||||
.createNotificationIfNeededAndUpdate(Player.this, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPrepareLoad(final Drawable placeHolderDrawable) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "Thumbnail - onLoadingStarted() called with: url = [" + url + "]");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1296,61 +1349,6 @@ public final class Player implements
|
||||
return Math.min(currentThumbnail.getHeight(), screenHeight);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadingStarted(final String imageUri, final View view) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "Thumbnail - onLoadingStarted() called on: "
|
||||
+ "imageUri = [" + imageUri + "], view = [" + view + "]");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadingFailed(final String imageUri, final View view,
|
||||
final FailReason failReason) {
|
||||
Log.e(TAG, "Thumbnail - onLoadingFailed() called on imageUri = [" + imageUri + "]",
|
||||
failReason.getCause());
|
||||
currentThumbnail = null;
|
||||
NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadingComplete(final String imageUri, final View view,
|
||||
final Bitmap loadedImage) {
|
||||
// scale down the notification thumbnail for performance
|
||||
final float notificationThumbnailWidth = Math.min(
|
||||
context.getResources().getDimension(R.dimen.player_notification_thumbnail_width),
|
||||
loadedImage.getWidth());
|
||||
currentThumbnail = Bitmap.createScaledBitmap(
|
||||
loadedImage,
|
||||
(int) notificationThumbnailWidth,
|
||||
(int) (loadedImage.getHeight()
|
||||
/ (loadedImage.getWidth() / notificationThumbnailWidth)),
|
||||
true);
|
||||
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "Thumbnail - onLoadingComplete() called with: "
|
||||
+ "imageUri = [" + imageUri + "], view = [" + view + "], "
|
||||
+ "loadedImage = [" + loadedImage + "], "
|
||||
+ loadedImage.getWidth() + "x" + loadedImage.getHeight()
|
||||
+ ", scaled notification width = " + notificationThumbnailWidth);
|
||||
}
|
||||
|
||||
NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, false);
|
||||
|
||||
// there is a new thumbnail, thus the end screen thumbnail needs to be changed, too.
|
||||
updateEndScreenThumbnail();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadingCancelled(final String imageUri, final View view) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "Thumbnail - onLoadingCancelled() called with: "
|
||||
+ "imageUri = [" + imageUri + "], view = [" + view + "]");
|
||||
}
|
||||
currentThumbnail = null;
|
||||
NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, false);
|
||||
}
|
||||
//endregion
|
||||
|
||||
|
||||
@@ -1358,7 +1356,7 @@ public final class Player implements
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Popup player utils
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
//region
|
||||
//region Popup player utils
|
||||
|
||||
/**
|
||||
* Check if {@link #popupLayoutParams}' position is within a arbitrary boundary
|
||||
@@ -1533,7 +1531,7 @@ public final class Player implements
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Playback parameters
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
//region
|
||||
//region Playback parameters
|
||||
|
||||
public float getPlaybackSpeed() {
|
||||
return getPlaybackParameters().speed;
|
||||
@@ -1586,7 +1584,7 @@ public final class Player implements
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Progress loop and updates
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
//region
|
||||
//region Progress loop and updates
|
||||
|
||||
private void onUpdateProgress(final int currentProgress,
|
||||
final int duration,
|
||||
@@ -1596,14 +1594,10 @@ public final class Player implements
|
||||
}
|
||||
|
||||
if (duration != binding.playbackSeekBar.getMax()) {
|
||||
binding.playbackEndTime.setText(getTimeString(duration));
|
||||
binding.playbackSeekBar.setMax(duration);
|
||||
setVideoDurationToControls(duration);
|
||||
}
|
||||
if (currentState != STATE_PAUSED) {
|
||||
if (currentState != STATE_PAUSED_SEEK) {
|
||||
binding.playbackSeekBar.setProgress(currentProgress);
|
||||
}
|
||||
binding.playbackCurrentTime.setText(getTimeString(currentProgress));
|
||||
updatePlayBackElementsCurrentDuration(currentProgress);
|
||||
}
|
||||
if (simpleExoPlayer.isLoading() || bufferPercent > 90) {
|
||||
binding.playbackSeekBar.setSecondaryProgress(
|
||||
@@ -1626,12 +1620,6 @@ public final class Player implements
|
||||
if (isQueueVisible) {
|
||||
updateQueueTime(currentProgress);
|
||||
}
|
||||
|
||||
final boolean showThumbnail = prefs.getBoolean(
|
||||
context.getString(R.string.show_thumbnail_key), true);
|
||||
// setMetadata only updates the metadata when any of the metadata keys are null
|
||||
mediaSessionManager.setMetadata(getVideoTitle(), getUploaderName(),
|
||||
showThumbnail ? getThumbnail() : null, duration);
|
||||
}
|
||||
|
||||
private void startProgressLoop() {
|
||||
@@ -1656,10 +1644,10 @@ public final class Player implements
|
||||
// TODO: revert #6307 when introducing proper HLS support
|
||||
final int duration;
|
||||
if (currentItem != null
|
||||
&& currentItem.getStreamType() != StreamType.AUDIO_LIVE_STREAM
|
||||
&& currentItem.getStreamType() != StreamType.LIVE_STREAM) {
|
||||
&& !StreamTypeUtil.isLiveStream(currentItem.getStreamType())
|
||||
) {
|
||||
// convert seconds to milliseconds
|
||||
duration = (int) (currentItem.getDuration() * 1000);
|
||||
duration = (int) (currentItem.getDuration() * 1000);
|
||||
} else {
|
||||
duration = (int) simpleExoPlayer.getDuration();
|
||||
}
|
||||
@@ -1802,7 +1790,7 @@ public final class Player implements
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Controls showing / hiding
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
//region
|
||||
//region Controls showing / hiding
|
||||
|
||||
public boolean isControlsVisible() {
|
||||
return binding != null && binding.playbackControlRoot.getVisibility() == View.VISIBLE;
|
||||
@@ -1972,7 +1960,7 @@ public final class Player implements
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Playback states
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
//region
|
||||
//region Playback states
|
||||
|
||||
@Override // exoplayer listener
|
||||
public void onPlayerStateChanged(final boolean playWhenReady, final int playbackState) {
|
||||
@@ -2097,8 +2085,8 @@ public final class Player implements
|
||||
Log.d(TAG, "onPrepared() called with: playWhenReady = [" + playWhenReady + "]");
|
||||
}
|
||||
|
||||
binding.playbackSeekBar.setMax((int) simpleExoPlayer.getDuration());
|
||||
binding.playbackEndTime.setText(getTimeString((int) simpleExoPlayer.getDuration()));
|
||||
setVideoDurationToControls((int) simpleExoPlayer.getDuration());
|
||||
|
||||
binding.playbackSpeed.setText(formatSpeed(getPlaybackSpeed()));
|
||||
|
||||
if (playWhenReady) {
|
||||
@@ -2257,6 +2245,9 @@ public final class Player implements
|
||||
stopProgressLoop();
|
||||
}
|
||||
|
||||
// When a (short) video ends the elements have to display the correct values - see #6180
|
||||
updatePlayBackElementsCurrentDuration(binding.playbackSeekBar.getMax());
|
||||
|
||||
showControls(500);
|
||||
animate(binding.currentDisplaySeek, false, 200, AnimationType.SCALE_AND_ALPHA);
|
||||
binding.loadingPanel.setVisibility(View.GONE);
|
||||
@@ -2293,7 +2284,7 @@ public final class Player implements
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Repeat and shuffle
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
//region
|
||||
//region Repeat and shuffle
|
||||
|
||||
public void onRepeatClicked() {
|
||||
if (DEBUG) {
|
||||
@@ -2330,7 +2321,7 @@ public final class Player implements
|
||||
Log.d(TAG, "ExoPlayer - onRepeatModeChanged() called with: "
|
||||
+ "repeatMode = [" + repeatMode + "]");
|
||||
}
|
||||
setRepeatModeButton(((AppCompatImageButton) binding.repeatButton), repeatMode);
|
||||
setRepeatModeButton(binding.repeatButton, repeatMode);
|
||||
onShuffleOrRepeatModeChanged();
|
||||
}
|
||||
|
||||
@@ -2382,7 +2373,7 @@ public final class Player implements
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Mute / Unmute
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
//region
|
||||
//region Mute / Unmute
|
||||
|
||||
public void onMuteUnmuteButtonClicked() {
|
||||
if (DEBUG) {
|
||||
@@ -2408,7 +2399,7 @@ public final class Player implements
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// ExoPlayer listeners (that didn't fit in other categories)
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
//region
|
||||
//region ExoPlayer listeners (that didn't fit in other categories)
|
||||
|
||||
@Override
|
||||
public void onTimelineChanged(@NonNull final Timeline timeline, final int reason) {
|
||||
@@ -2496,7 +2487,7 @@ public final class Player implements
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Errors
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
//region
|
||||
//region Errors
|
||||
/**
|
||||
* Process exceptions produced by {@link com.google.android.exoplayer2.ExoPlayer ExoPlayer}.
|
||||
* <p>There are multiple types of errors:</p>
|
||||
@@ -2597,7 +2588,19 @@ public final class Player implements
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Playback position and seek
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
//region
|
||||
//region Playback position and seek
|
||||
|
||||
/**
|
||||
* Sets the current duration into the corresponding elements.
|
||||
* @param currentProgress
|
||||
*/
|
||||
private void updatePlayBackElementsCurrentDuration(final int currentProgress) {
|
||||
// Don't set seekbar progress while user is seeking
|
||||
if (currentState != STATE_PAUSED_SEEK) {
|
||||
binding.playbackSeekBar.setProgress(currentProgress);
|
||||
}
|
||||
binding.playbackCurrentTime.setText(getTimeString(currentProgress));
|
||||
}
|
||||
|
||||
@Override // own playback listener (this is a getter)
|
||||
public boolean isApproachingPlaybackEdge(final long timeToEndMillis) {
|
||||
@@ -2740,6 +2743,20 @@ public final class Player implements
|
||||
simpleExoPlayer.seekToDefaultPosition();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the video duration time into all control components (e.g. seekbar).
|
||||
* @param duration
|
||||
*/
|
||||
private void setVideoDurationToControls(final int duration) {
|
||||
binding.playbackEndTime.setText(getTimeString(duration));
|
||||
|
||||
binding.playbackSeekBar.setMax(duration);
|
||||
// This is important for Android TVs otherwise it would apply the default from
|
||||
// setMax/Min methods which is (max - min) / 20
|
||||
binding.playbackSeekBar.setKeyProgressIncrement(
|
||||
PlayerHelper.retrieveSeekDurationFromPreferences(this));
|
||||
}
|
||||
//endregion
|
||||
|
||||
|
||||
@@ -2747,7 +2764,7 @@ public final class Player implements
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Player actions (play, pause, previous, fast-forward, ...)
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
//region
|
||||
//region Player actions (play, pause, previous, fast-forward, ...)
|
||||
|
||||
public void play() {
|
||||
if (DEBUG) {
|
||||
@@ -2789,7 +2806,9 @@ public final class Player implements
|
||||
Log.d(TAG, "onPlayPause() called");
|
||||
}
|
||||
|
||||
if (getPlayWhenReady()) {
|
||||
if (getPlayWhenReady()
|
||||
// When state is completed (replay button is shown) then (re)play and do not pause
|
||||
&& currentState != STATE_COMPLETED) {
|
||||
pause();
|
||||
} else {
|
||||
play();
|
||||
@@ -2855,7 +2874,7 @@ public final class Player implements
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// StreamInfo history: views and progress
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
//region
|
||||
//region StreamInfo history: views and progress
|
||||
|
||||
private void registerStreamViewed() {
|
||||
if (currentMetadata != null) {
|
||||
@@ -2913,7 +2932,7 @@ public final class Player implements
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Metadata
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
//region
|
||||
//region Metadata
|
||||
|
||||
private void onMetadataChanged(@NonNull final MediaSourceTag tag) {
|
||||
final StreamInfo info = tag.getMetadata();
|
||||
@@ -2934,6 +2953,18 @@ public final class Player implements
|
||||
tag.getMetadata().getPreviewFrames());
|
||||
|
||||
NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, false);
|
||||
|
||||
final boolean showThumbnail = prefs.getBoolean(
|
||||
context.getString(R.string.show_thumbnail_key), true);
|
||||
mediaSessionManager.setMetadata(
|
||||
getVideoTitle(),
|
||||
getUploaderName(),
|
||||
showThumbnail ? Optional.ofNullable(getThumbnail()) : Optional.empty(),
|
||||
StreamTypeUtil.isLiveStream(tag.getMetadata().getStreamType())
|
||||
? -1
|
||||
: tag.getMetadata().getDuration()
|
||||
);
|
||||
|
||||
notifyMetadataUpdateToListeners();
|
||||
|
||||
if (areSegmentsVisible) {
|
||||
@@ -3011,9 +3042,11 @@ public final class Player implements
|
||||
|
||||
@Nullable
|
||||
public Bitmap getThumbnail() {
|
||||
return currentThumbnail == null
|
||||
? BitmapFactory.decodeResource(context.getResources(), R.drawable.dummy_thumbnail)
|
||||
: currentThumbnail;
|
||||
if (currentThumbnail == null) {
|
||||
currentThumbnail = BitmapFactory.decodeResource(
|
||||
context.getResources(), R.drawable.dummy_thumbnail);
|
||||
}
|
||||
return currentThumbnail;
|
||||
}
|
||||
//endregion
|
||||
|
||||
@@ -3022,7 +3055,7 @@ public final class Player implements
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Play queue, segments and streams
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
//region
|
||||
//region Play queue, segments and streams
|
||||
|
||||
private void maybeAutoQueueNextStream(@NonNull final MediaSourceTag metadata) {
|
||||
if (playQueue == null || playQueue.getIndex() != playQueue.size() - 1
|
||||
@@ -3177,7 +3210,7 @@ public final class Player implements
|
||||
private StreamSegmentAdapter.StreamSegmentListener getStreamSegmentListener() {
|
||||
return (item, seconds) -> {
|
||||
segmentAdapter.selectSegment(item);
|
||||
seekTo(seconds * 1000);
|
||||
seekTo(seconds * 1000L);
|
||||
triggerProgressUpdate();
|
||||
};
|
||||
}
|
||||
@@ -3187,7 +3220,7 @@ public final class Player implements
|
||||
final List<StreamSegment> segments = currentMetadata.getMetadata().getStreamSegments();
|
||||
|
||||
for (int i = 0; i < segments.size(); i++) {
|
||||
if (segments.get(i).getStartTimeSeconds() * 1000 > playbackPosition) {
|
||||
if (segments.get(i).getStartTimeSeconds() * 1000L > playbackPosition) {
|
||||
break;
|
||||
}
|
||||
nearestPosition++;
|
||||
@@ -3222,9 +3255,9 @@ public final class Player implements
|
||||
|
||||
@Override
|
||||
public void held(final PlayQueueItem item, final View view) {
|
||||
final int index = playQueue.indexOf(item);
|
||||
if (index != -1) {
|
||||
playQueue.remove(index);
|
||||
if (playQueue.indexOf(item) != -1) {
|
||||
openPopupMenu(playQueue, item, view, true,
|
||||
getParentActivity().getSupportFragmentManager(), context);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3338,7 +3371,7 @@ public final class Player implements
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Popup menus ("popup" means that they pop up, not that they belong to the popup player)
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
//region
|
||||
//region Popup menus ("popup" means that they pop up, not that they belong to the popup player)
|
||||
|
||||
private void buildQualityMenu() {
|
||||
if (qualityPopupMenu == null) {
|
||||
@@ -3541,7 +3574,7 @@ public final class Player implements
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Captions (text tracks)
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
//region
|
||||
//region Captions (text tracks)
|
||||
|
||||
private void setupSubtitleView() {
|
||||
final float captionScale = PlayerHelper.getCaptionScale(context);
|
||||
@@ -3620,7 +3653,7 @@ public final class Player implements
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Click listeners
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
//region
|
||||
//region Click listeners
|
||||
|
||||
@Override
|
||||
public void onClick(final View v) {
|
||||
@@ -3808,7 +3841,7 @@ public final class Player implements
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Video size, resize, orientation, fullscreen
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
//region
|
||||
//region Video size, resize, orientation, fullscreen
|
||||
|
||||
private void setupScreenRotationButton() {
|
||||
binding.screenRotationButton.setVisibility(videoPlayerSelected()
|
||||
@@ -3863,11 +3896,9 @@ public final class Player implements
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "toggleFullscreen() called");
|
||||
}
|
||||
if (popupPlayerSelected() || exoPlayerIsNull() || currentMetadata == null
|
||||
|| fragmentListener == null) {
|
||||
if (popupPlayerSelected() || exoPlayerIsNull() || fragmentListener == null) {
|
||||
return;
|
||||
}
|
||||
//changeState(STATE_BLOCKED); TODO check what this does
|
||||
|
||||
isFullscreen = !isFullscreen;
|
||||
if (!isFullscreen) {
|
||||
@@ -3915,7 +3946,7 @@ public final class Player implements
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Gestures
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
//region
|
||||
//region Gestures
|
||||
|
||||
@SuppressWarnings("checkstyle:ParameterNumber")
|
||||
private void onLayoutChange(final View view, final int l, final int t, final int r, final int b,
|
||||
@@ -3979,7 +4010,7 @@ public final class Player implements
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Activity / fragment binding
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
//region
|
||||
//region Activity / fragment binding
|
||||
|
||||
public void setFragmentListener(final PlayerServiceEventListener listener) {
|
||||
fragmentListener = listener;
|
||||
@@ -4118,7 +4149,7 @@ public final class Player implements
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Getters
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
//region
|
||||
//region Getters
|
||||
|
||||
public int getCurrentState() {
|
||||
return currentState;
|
||||
@@ -4304,6 +4335,7 @@ public final class Player implements
|
||||
// SurfaceHolderCallback helpers
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
//region SurfaceHolderCallback helpers
|
||||
|
||||
private void setupVideoSurface() {
|
||||
// make sure there is nothing left over from previous calls
|
||||
cleanupVideoSurface();
|
||||
@@ -4331,5 +4363,5 @@ public final class Player implements
|
||||
}
|
||||
}
|
||||
}
|
||||
//endregion SurfaceHolderCallback helpers
|
||||
//endregion
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ import androidx.media.AudioManagerCompat;
|
||||
|
||||
import com.google.android.exoplayer2.SimpleExoPlayer;
|
||||
import com.google.android.exoplayer2.analytics.AnalyticsListener;
|
||||
import com.google.android.exoplayer2.decoder.DecoderCounters;
|
||||
|
||||
public class AudioReactor implements AudioManager.OnAudioFocusChangeListener, AnalyticsListener {
|
||||
|
||||
@@ -50,6 +51,7 @@ public class AudioReactor implements AudioManager.OnAudioFocusChangeListener, An
|
||||
public void dispose() {
|
||||
abandonAudioFocus();
|
||||
player.removeAnalyticsListener(this);
|
||||
notifyAudioSessionUpdate(false, player.getAudioSessionId());
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
@@ -149,11 +151,21 @@ public class AudioReactor implements AudioManager.OnAudioFocusChangeListener, An
|
||||
|
||||
@Override
|
||||
public void onAudioSessionId(final EventTime eventTime, final int audioSessionId) {
|
||||
notifyAudioSessionUpdate(true, audioSessionId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAudioDisabled(final EventTime eventTime, final DecoderCounters counters) {
|
||||
notifyAudioSessionUpdate(false, player.getAudioSessionId());
|
||||
}
|
||||
|
||||
private void notifyAudioSessionUpdate(final boolean active, final int audioSessionId) {
|
||||
if (!PlayerHelper.isUsingDSP()) {
|
||||
return;
|
||||
}
|
||||
|
||||
final Intent intent = new Intent(AudioEffect.ACTION_OPEN_AUDIO_EFFECT_CONTROL_SESSION);
|
||||
final Intent intent = new Intent(active
|
||||
? AudioEffect.ACTION_OPEN_AUDIO_EFFECT_CONTROL_SESSION
|
||||
: AudioEffect.ACTION_CLOSE_AUDIO_EFFECT_CONTROL_SESSION);
|
||||
intent.putExtra(AudioEffect.EXTRA_AUDIO_SESSION, audioSessionId);
|
||||
intent.putExtra(AudioEffect.EXTRA_PACKAGE_NAME, context.getPackageName());
|
||||
context.sendBroadcast(intent);
|
||||
|
||||
@@ -20,18 +20,16 @@ public class LoadController implements LoadControl {
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
public LoadController() {
|
||||
this(PlayerHelper.getPlaybackStartBufferMs(),
|
||||
PlayerHelper.getPlaybackMinimumBufferMs(),
|
||||
PlayerHelper.getPlaybackOptimalBufferMs());
|
||||
this(PlayerHelper.getPlaybackStartBufferMs());
|
||||
}
|
||||
|
||||
private LoadController(final int initialPlaybackBufferMs,
|
||||
final int minimumPlaybackBufferMs,
|
||||
final int optimalPlaybackBufferMs) {
|
||||
private LoadController(final int initialPlaybackBufferMs) {
|
||||
this.initialPlaybackBufferUs = initialPlaybackBufferMs * 1000;
|
||||
|
||||
final DefaultLoadControl.Builder builder = new DefaultLoadControl.Builder();
|
||||
builder.setBufferDurationsMs(minimumPlaybackBufferMs, optimalPlaybackBufferMs,
|
||||
builder.setBufferDurationsMs(
|
||||
DefaultLoadControl.DEFAULT_MIN_BUFFER_MS,
|
||||
DefaultLoadControl.DEFAULT_MAX_BUFFER_MS,
|
||||
initialPlaybackBufferMs,
|
||||
DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS);
|
||||
internalLoadControl = builder.build();
|
||||
|
||||
@@ -21,6 +21,8 @@ import org.schabi.newpipe.player.mediasession.MediaSessionCallback;
|
||||
import org.schabi.newpipe.player.mediasession.PlayQueueNavigator;
|
||||
import org.schabi.newpipe.player.mediasession.PlayQueuePlaybackController;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
public class MediaSessionManager {
|
||||
private static final String TAG = MediaSessionManager.class.getSimpleName();
|
||||
public static final boolean DEBUG = MainActivity.DEBUG;
|
||||
@@ -30,6 +32,9 @@ public class MediaSessionManager {
|
||||
@NonNull
|
||||
private final MediaSessionConnector sessionConnector;
|
||||
|
||||
private int lastTitleHashCode;
|
||||
private int lastArtistHashCode;
|
||||
private long lastDuration;
|
||||
private int lastAlbumArtHashCode;
|
||||
|
||||
public MediaSessionManager(@NonNull final Context context,
|
||||
@@ -65,47 +70,127 @@ public class MediaSessionManager {
|
||||
return mediaSession.getSessionToken();
|
||||
}
|
||||
|
||||
public void setMetadata(final String title,
|
||||
final String artist,
|
||||
final Bitmap albumArt,
|
||||
final long duration) {
|
||||
if (albumArt == null || !mediaSession.isActive()) {
|
||||
/**
|
||||
* sets the Metadata - if required.
|
||||
*
|
||||
* @param title {@link MediaMetadataCompat#METADATA_KEY_TITLE}
|
||||
* @param artist {@link MediaMetadataCompat#METADATA_KEY_ARTIST}
|
||||
* @param optAlbumArt {@link MediaMetadataCompat#METADATA_KEY_ALBUM_ART}
|
||||
* @param duration {@link MediaMetadataCompat#METADATA_KEY_DURATION}
|
||||
* - should be a negative value for unknown durations, e.g. for livestreams
|
||||
*/
|
||||
public void setMetadata(@NonNull final String title,
|
||||
@NonNull final String artist,
|
||||
@NonNull final Optional<Bitmap> optAlbumArt,
|
||||
final long duration
|
||||
) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "setMetadata called:"
|
||||
+ " t: " + title
|
||||
+ " a: " + artist
|
||||
+ " thumb: " + (
|
||||
optAlbumArt.isPresent()
|
||||
? optAlbumArt.get().hashCode()
|
||||
: "<none>")
|
||||
+ " d: " + duration);
|
||||
}
|
||||
|
||||
if (!mediaSession.isActive()) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "setMetadata: mediaSession not active - exiting");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!checkIfMetadataShouldBeSet(title, artist, optAlbumArt, duration)) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "setMetadata: No update required - exiting");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (DEBUG) {
|
||||
if (getMetadataAlbumArt() == null) {
|
||||
Log.d(TAG, "N_getMetadataAlbumArt: thumb == null");
|
||||
}
|
||||
if (getMetadataTitle() == null) {
|
||||
Log.d(TAG, "N_getMetadataTitle: title == null");
|
||||
}
|
||||
if (getMetadataArtist() == null) {
|
||||
Log.d(TAG, "N_getMetadataArtist: artist == null");
|
||||
}
|
||||
if (getMetadataDuration() <= 1) {
|
||||
Log.d(TAG, "N_getMetadataDuration: duration <= 1; " + getMetadataDuration());
|
||||
}
|
||||
Log.d(TAG, "setMetadata: N_Metadata update:"
|
||||
+ " t: " + title
|
||||
+ " a: " + artist
|
||||
+ " thumb: " + (
|
||||
optAlbumArt.isPresent()
|
||||
? optAlbumArt.get().hashCode()
|
||||
: "<none>")
|
||||
+ " d: " + duration);
|
||||
}
|
||||
|
||||
if (getMetadataAlbumArt() == null || getMetadataTitle() == null
|
||||
|| getMetadataArtist() == null || getMetadataDuration() <= 1
|
||||
|| albumArt.hashCode() != lastAlbumArtHashCode) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "setMetadata: N_Metadata update: t: " + title + " a: " + artist
|
||||
+ " thumb: " + albumArt.hashCode() + " d: " + duration);
|
||||
}
|
||||
final MediaMetadataCompat.Builder builder = new MediaMetadataCompat.Builder()
|
||||
.putString(MediaMetadataCompat.METADATA_KEY_TITLE, title)
|
||||
.putString(MediaMetadataCompat.METADATA_KEY_ARTIST, artist)
|
||||
.putLong(MediaMetadataCompat.METADATA_KEY_DURATION, duration);
|
||||
|
||||
mediaSession.setMetadata(new MediaMetadataCompat.Builder()
|
||||
.putString(MediaMetadataCompat.METADATA_KEY_TITLE, title)
|
||||
.putString(MediaMetadataCompat.METADATA_KEY_ARTIST, artist)
|
||||
.putBitmap(MediaMetadataCompat.METADATA_KEY_ALBUM_ART, albumArt)
|
||||
.putBitmap(MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON, albumArt)
|
||||
.putLong(MediaMetadataCompat.METADATA_KEY_DURATION, duration).build());
|
||||
lastAlbumArtHashCode = albumArt.hashCode();
|
||||
if (optAlbumArt.isPresent()) {
|
||||
builder.putBitmap(MediaMetadataCompat.METADATA_KEY_ALBUM_ART, optAlbumArt.get());
|
||||
builder.putBitmap(MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON, optAlbumArt.get());
|
||||
}
|
||||
|
||||
mediaSession.setMetadata(builder.build());
|
||||
|
||||
lastTitleHashCode = title.hashCode();
|
||||
lastArtistHashCode = artist.hashCode();
|
||||
lastDuration = duration;
|
||||
if (optAlbumArt.isPresent()) {
|
||||
lastAlbumArtHashCode = optAlbumArt.get().hashCode();
|
||||
}
|
||||
}
|
||||
|
||||
private boolean checkIfMetadataShouldBeSet(
|
||||
@NonNull final String title,
|
||||
@NonNull final String artist,
|
||||
@NonNull final Optional<Bitmap> optAlbumArt,
|
||||
final long duration
|
||||
) {
|
||||
// Check if the values have changed since the last time
|
||||
if (title.hashCode() != lastTitleHashCode
|
||||
|| artist.hashCode() != lastArtistHashCode
|
||||
|| duration != lastDuration
|
||||
|| (optAlbumArt.isPresent() && optAlbumArt.get().hashCode() != lastAlbumArtHashCode)
|
||||
) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG,
|
||||
"checkIfMetadataShouldBeSet: true - reason: changed values since last");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if the currently set metadata is valid
|
||||
if (getMetadataTitle() == null
|
||||
|| getMetadataArtist() == null
|
||||
// Note that the duration can be <= 0 for live streams
|
||||
) {
|
||||
if (DEBUG) {
|
||||
if (getMetadataTitle() == null) {
|
||||
Log.d(TAG,
|
||||
"N_getMetadataTitle: title == null");
|
||||
} else if (getMetadataArtist() == null) {
|
||||
Log.d(TAG,
|
||||
"N_getMetadataArtist: artist == null");
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// If we got an album art check if the current set AlbumArt is null
|
||||
if (optAlbumArt.isPresent() && getMetadataAlbumArt() == null) {
|
||||
if (DEBUG) {
|
||||
if (getMetadataAlbumArt() == null) {
|
||||
Log.d(TAG, "N_getMetadataAlbumArt: thumb == null");
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Default - no update required
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
private Bitmap getMetadataAlbumArt() {
|
||||
return mediaSession.getController().getMetadata()
|
||||
.getBitmap(MediaMetadataCompat.METADATA_KEY_ALBUM_ART);
|
||||
@@ -121,11 +206,6 @@ public class MediaSessionManager {
|
||||
.getString(MediaMetadataCompat.METADATA_KEY_ARTIST);
|
||||
}
|
||||
|
||||
private long getMetadataDuration() {
|
||||
return mediaSession.getController().getMetadata()
|
||||
.getLong(MediaMetadataCompat.METADATA_KEY_DURATION);
|
||||
}
|
||||
|
||||
/**
|
||||
* Should be called on player destruction to prevent leakage.
|
||||
*/
|
||||
|
||||
@@ -164,7 +164,7 @@ public class PlaybackParameterDialog extends DialogFragment {
|
||||
setPlaybackParameters(initialTempo, initialPitch, initialSkipSilence))
|
||||
.setNeutralButton(R.string.playback_reset, (dialogInterface, i) ->
|
||||
setPlaybackParameters(DEFAULT_TEMPO, DEFAULT_PITCH, DEFAULT_SKIP_SILENCE))
|
||||
.setPositiveButton(R.string.finish, (dialogInterface, i) ->
|
||||
.setPositiveButton(R.string.ok, (dialogInterface, i) ->
|
||||
setCurrentPlaybackParameters());
|
||||
|
||||
return dialogBuilder.create();
|
||||
|
||||
@@ -239,6 +239,11 @@ public final class PlayerHelper {
|
||||
.getBoolean(context.getString(R.string.brightness_gesture_control_key), true);
|
||||
}
|
||||
|
||||
public static boolean isStartMainPlayerFullscreenEnabled(@NonNull final Context context) {
|
||||
return getPreferences(context)
|
||||
.getBoolean(context.getString(R.string.start_main_player_fullscreen_key), false);
|
||||
}
|
||||
|
||||
public static boolean isAutoQueueEnabled(@NonNull final Context context) {
|
||||
return getPreferences(context)
|
||||
.getBoolean(context.getString(R.string.auto_queue_key), false);
|
||||
@@ -307,22 +312,6 @@ public final class PlayerHelper {
|
||||
return 500;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the minimum number of milliseconds the player always buffers to
|
||||
* after starting playback.
|
||||
*/
|
||||
public static int getPlaybackMinimumBufferMs() {
|
||||
return 25000;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the maximum/optimal number of milliseconds the player will buffer to once the buffer
|
||||
* hits the point of {@link #getPlaybackMinimumBufferMs()}.
|
||||
*/
|
||||
public static int getPlaybackOptimalBufferMs() {
|
||||
return 60000;
|
||||
}
|
||||
|
||||
public static TrackSelection.Factory getQualitySelector() {
|
||||
return new AdaptiveTrackSelection.Factory(
|
||||
1000,
|
||||
|
||||
@@ -70,6 +70,10 @@ public final class PlayerHolder {
|
||||
return player != null;
|
||||
}
|
||||
|
||||
public int getQueueSize() {
|
||||
return isPlayerOpen() ? player.getPlayQueue().size() : 0;
|
||||
}
|
||||
|
||||
public void setListener(@Nullable final PlayerServiceExtendedEventListener newListener) {
|
||||
listener = newListener;
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ public class PlayQueueItem implements Serializable {
|
||||
private final String thumbnailUrl;
|
||||
@NonNull
|
||||
private final String uploader;
|
||||
private final String uploaderUrl;
|
||||
@NonNull
|
||||
private final StreamType streamType;
|
||||
|
||||
@@ -37,7 +38,8 @@ public class PlayQueueItem implements Serializable {
|
||||
|
||||
PlayQueueItem(@NonNull final StreamInfo info) {
|
||||
this(info.getName(), info.getUrl(), info.getServiceId(), info.getDuration(),
|
||||
info.getThumbnailUrl(), info.getUploaderName(), info.getStreamType());
|
||||
info.getThumbnailUrl(), info.getUploaderName(),
|
||||
info.getUploaderUrl(), info.getStreamType());
|
||||
|
||||
if (info.getStartPosition() > 0) {
|
||||
setRecoveryPosition(info.getStartPosition() * 1000);
|
||||
@@ -46,19 +48,21 @@ public class PlayQueueItem implements Serializable {
|
||||
|
||||
PlayQueueItem(@NonNull final StreamInfoItem item) {
|
||||
this(item.getName(), item.getUrl(), item.getServiceId(), item.getDuration(),
|
||||
item.getThumbnailUrl(), item.getUploaderName(), item.getStreamType());
|
||||
item.getThumbnailUrl(), item.getUploaderName(),
|
||||
item.getUploaderUrl(), item.getStreamType());
|
||||
}
|
||||
|
||||
private PlayQueueItem(@Nullable final String name, @Nullable final String url,
|
||||
final int serviceId, final long duration,
|
||||
@Nullable final String thumbnailUrl, @Nullable final String uploader,
|
||||
@NonNull final StreamType streamType) {
|
||||
final String uploaderUrl, @NonNull final StreamType streamType) {
|
||||
this.title = name != null ? name : EMPTY_STRING;
|
||||
this.url = url != null ? url : EMPTY_STRING;
|
||||
this.serviceId = serviceId;
|
||||
this.duration = duration;
|
||||
this.thumbnailUrl = thumbnailUrl != null ? thumbnailUrl : EMPTY_STRING;
|
||||
this.uploader = uploader != null ? uploader : EMPTY_STRING;
|
||||
this.uploaderUrl = uploaderUrl;
|
||||
this.streamType = streamType;
|
||||
|
||||
this.recoveryPosition = RECOVERY_UNSET;
|
||||
@@ -92,6 +96,10 @@ public class PlayQueueItem implements Serializable {
|
||||
return uploader;
|
||||
}
|
||||
|
||||
public String getUploaderUrl() {
|
||||
return uploaderUrl;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public StreamType getStreamType() {
|
||||
return streamType;
|
||||
|
||||
@@ -5,11 +5,9 @@ import android.text.TextUtils;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
|
||||
import com.nostra13.universalimageloader.core.ImageLoader;
|
||||
|
||||
import org.schabi.newpipe.extractor.NewPipe;
|
||||
import org.schabi.newpipe.util.ImageDisplayConstants;
|
||||
import org.schabi.newpipe.util.Localization;
|
||||
import org.schabi.newpipe.util.PicassoHelper;
|
||||
|
||||
public class PlayQueueItemBuilder {
|
||||
private static final String TAG = PlayQueueItemBuilder.class.toString();
|
||||
@@ -35,8 +33,7 @@ public class PlayQueueItemBuilder {
|
||||
holder.itemDurationView.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
ImageLoader.getInstance().displayImage(item.getThumbnailUrl(), holder.itemThumbnailView,
|
||||
ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS);
|
||||
PicassoHelper.loadThumbnail(item.getThumbnailUrl()).into(holder.itemThumbnailView);
|
||||
|
||||
holder.itemRoot.setOnClickListener(view -> {
|
||||
if (onItemClickListener != null) {
|
||||
|
||||
@@ -14,6 +14,7 @@ import com.google.android.exoplayer2.util.Util;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||
import org.schabi.newpipe.extractor.stream.StreamType;
|
||||
import org.schabi.newpipe.player.helper.PlayerDataSource;
|
||||
import org.schabi.newpipe.util.StreamTypeUtil;
|
||||
|
||||
public interface PlaybackResolver extends Resolver<StreamInfo, MediaSource> {
|
||||
|
||||
@@ -21,7 +22,7 @@ public interface PlaybackResolver extends Resolver<StreamInfo, MediaSource> {
|
||||
default MediaSource maybeBuildLiveMediaSource(@NonNull final PlayerDataSource dataSource,
|
||||
@NonNull final StreamInfo info) {
|
||||
final StreamType streamType = info.getStreamType();
|
||||
if (!(streamType == StreamType.AUDIO_LIVE_STREAM || streamType == StreamType.LIVE_STREAM)) {
|
||||
if (!StreamTypeUtil.isLiveStream(streamType)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
package org.schabi.newpipe.player.seekbarpreview;
|
||||
|
||||
import static org.schabi.newpipe.player.seekbarpreview.SeekbarPreviewThumbnailHelper.SeekbarPreviewThumbnailType;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Bitmap;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.google.common.base.Stopwatch;
|
||||
import com.nostra13.universalimageloader.core.ImageLoader;
|
||||
|
||||
import org.schabi.newpipe.extractor.stream.Frameset;
|
||||
import org.schabi.newpipe.util.ImageDisplayConstants;
|
||||
import org.schabi.newpipe.util.PicassoHelper;
|
||||
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
@@ -21,11 +23,8 @@ import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import static org.schabi.newpipe.player.seekbarpreview.SeekbarPreviewThumbnailHelper.SeekbarPreviewThumbnailType;
|
||||
|
||||
public class SeekbarPreviewThumbnailHolder {
|
||||
|
||||
// This has to be <= 23 chars on devices running Android 7 or lower (API <= 25)
|
||||
@@ -174,6 +173,7 @@ public class SeekbarPreviewThumbnailHolder {
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private Bitmap getBitMapFrom(final String url) {
|
||||
if (url == null) {
|
||||
Log.w(TAG, "url is null; This should never happen");
|
||||
@@ -182,24 +182,11 @@ public class SeekbarPreviewThumbnailHolder {
|
||||
|
||||
final Stopwatch sw = Log.isLoggable(TAG, Log.DEBUG) ? Stopwatch.createStarted() : null;
|
||||
try {
|
||||
final SyncImageLoadingListener syncImageLoadingListener =
|
||||
new SyncImageLoadingListener();
|
||||
|
||||
Log.d(TAG, "Downloading bitmap for seekbarPreview from '" + url + "'");
|
||||
|
||||
// Ensure that everything is running
|
||||
ImageLoader.getInstance().resume();
|
||||
// Load the image
|
||||
// Impl-Note:
|
||||
// Gets the bitmap within the timeout of 15 seconds imposed by default by OkHttpClient
|
||||
// Ensure that your are not running on the main-Thread this will otherwise hang
|
||||
ImageLoader.getInstance().loadImage(
|
||||
url,
|
||||
ImageDisplayConstants.DISPLAY_SEEKBAR_PREVIEW_OPTIONS,
|
||||
syncImageLoadingListener);
|
||||
|
||||
// Get the bitmap within the timeout
|
||||
final Bitmap bitmap =
|
||||
syncImageLoadingListener.waitForBitmapOrThrow(30, TimeUnit.SECONDS);
|
||||
final Bitmap bitmap = PicassoHelper.loadSeekbarThumbnailPreview(url).get();
|
||||
|
||||
if (sw != null) {
|
||||
Log.d(TAG,
|
||||
|
||||
@@ -1,87 +0,0 @@
|
||||
package org.schabi.newpipe.player.seekbarpreview;
|
||||
|
||||
import android.graphics.Bitmap;
|
||||
import android.view.View;
|
||||
|
||||
import com.nostra13.universalimageloader.core.assist.FailReason;
|
||||
import com.nostra13.universalimageloader.core.listener.SimpleImageLoadingListener;
|
||||
|
||||
import java.util.concurrent.CancellationException;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
||||
/**
|
||||
* Listener for synchronously downloading of an image/bitmap.
|
||||
*/
|
||||
public class SyncImageLoadingListener extends SimpleImageLoadingListener {
|
||||
|
||||
private final CountDownLatch countDownLatch = new CountDownLatch(1);
|
||||
|
||||
private Bitmap bitmap;
|
||||
private boolean cancelled = false;
|
||||
private FailReason failReason = null;
|
||||
|
||||
@SuppressWarnings("checkstyle:HiddenField")
|
||||
@Override
|
||||
public void onLoadingFailed(
|
||||
final String imageUri,
|
||||
final View view,
|
||||
final FailReason failReason) {
|
||||
|
||||
this.failReason = failReason;
|
||||
countDownLatch.countDown();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadingComplete(
|
||||
final String imageUri,
|
||||
final View view,
|
||||
final Bitmap loadedImage) {
|
||||
|
||||
bitmap = loadedImage;
|
||||
countDownLatch.countDown();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadingCancelled(final String imageUri, final View view) {
|
||||
cancelled = true;
|
||||
countDownLatch.countDown();
|
||||
}
|
||||
|
||||
public Bitmap waitForBitmapOrThrow(final long timeout, final TimeUnit timeUnit)
|
||||
throws InterruptedException, TimeoutException {
|
||||
|
||||
// Wait for the download to finish
|
||||
if (!countDownLatch.await(timeout, timeUnit)) {
|
||||
throw new TimeoutException("Couldn't get the image in time");
|
||||
}
|
||||
|
||||
if (isCancelled()) {
|
||||
throw new CancellationException("Download of image was cancelled");
|
||||
}
|
||||
|
||||
if (getFailReason() != null) {
|
||||
throw new RuntimeException("Failed to download image" + getFailReason().getType(),
|
||||
getFailReason().getCause());
|
||||
}
|
||||
|
||||
if (getBitmap() == null) {
|
||||
throw new NullPointerException("Bitmap is null");
|
||||
}
|
||||
|
||||
return getBitmap();
|
||||
}
|
||||
|
||||
public Bitmap getBitmap() {
|
||||
return bitmap;
|
||||
}
|
||||
|
||||
public boolean isCancelled() {
|
||||
return cancelled;
|
||||
}
|
||||
|
||||
public FailReason getFailReason() {
|
||||
return failReason;
|
||||
}
|
||||
}
|
||||
@@ -17,8 +17,6 @@ import androidx.core.content.ContextCompat;
|
||||
import androidx.preference.Preference;
|
||||
import androidx.preference.PreferenceManager;
|
||||
|
||||
import com.nostra13.universalimageloader.core.ImageLoader;
|
||||
|
||||
import org.schabi.newpipe.DownloaderImpl;
|
||||
import org.schabi.newpipe.NewPipeDatabase;
|
||||
import org.schabi.newpipe.R;
|
||||
@@ -29,6 +27,7 @@ import org.schabi.newpipe.extractor.localization.ContentCountry;
|
||||
import org.schabi.newpipe.extractor.localization.Localization;
|
||||
import org.schabi.newpipe.streams.io.StoredFileHelper;
|
||||
import org.schabi.newpipe.util.NavigationHelper;
|
||||
import org.schabi.newpipe.util.PicassoHelper;
|
||||
import org.schabi.newpipe.util.ZipHelper;
|
||||
|
||||
import java.io.File;
|
||||
@@ -50,7 +49,6 @@ public class ContentSettingsFragment extends BasePreferenceFragment {
|
||||
private ContentSettingsManager manager;
|
||||
|
||||
private String importExportDataPathKey;
|
||||
private String thumbnailLoadToggleKey;
|
||||
private String youtubeRestrictedModeEnabledKey;
|
||||
|
||||
private Localization initialSelectedLocalization;
|
||||
@@ -69,7 +67,6 @@ public class ContentSettingsFragment extends BasePreferenceFragment {
|
||||
manager.deleteSettingsFile();
|
||||
|
||||
importExportDataPathKey = getString(R.string.import_export_data_path);
|
||||
thumbnailLoadToggleKey = getString(R.string.download_thumbnail_key);
|
||||
youtubeRestrictedModeEnabledKey = getString(R.string.youtube_restricted_mode_enabled);
|
||||
|
||||
addPreferencesFromResource(R.xml.content_settings);
|
||||
@@ -77,7 +74,8 @@ public class ContentSettingsFragment extends BasePreferenceFragment {
|
||||
final Preference importDataPreference = requirePreference(R.string.import_data);
|
||||
importDataPreference.setOnPreferenceClickListener((Preference p) -> {
|
||||
requestImportPathLauncher.launch(
|
||||
StoredFileHelper.getPicker(requireContext(), getImportExportDataUri()));
|
||||
StoredFileHelper.getPicker(requireContext(),
|
||||
ZIP_MIME_TYPE, getImportExportDataUri()));
|
||||
return true;
|
||||
});
|
||||
|
||||
@@ -95,8 +93,7 @@ public class ContentSettingsFragment extends BasePreferenceFragment {
|
||||
.getPreferredLocalization(requireContext());
|
||||
initialSelectedContentCountry = org.schabi.newpipe.util.Localization
|
||||
.getPreferredContentCountry(requireContext());
|
||||
initialLanguage = PreferenceManager
|
||||
.getDefaultSharedPreferences(requireContext()).getString("app_language_key", "en");
|
||||
initialLanguage = defaultPreferences.getString(getString(R.string.app_language_key), "en");
|
||||
|
||||
final Preference clearCookiePref = requirePreference(R.string.clear_cookie_key);
|
||||
clearCookiePref.setOnPreferenceClickListener(preference -> {
|
||||
@@ -112,20 +109,24 @@ public class ContentSettingsFragment extends BasePreferenceFragment {
|
||||
if (defaultPreferences.getString(getString(R.string.recaptcha_cookies_key), "").isEmpty()) {
|
||||
clearCookiePref.setVisible(false);
|
||||
}
|
||||
|
||||
findPreference(getString(R.string.download_thumbnail_key)).setOnPreferenceChangeListener(
|
||||
(preference, newValue) -> {
|
||||
PicassoHelper.setShouldLoadImages((Boolean) newValue);
|
||||
try {
|
||||
PicassoHelper.clearCache(preference.getContext());
|
||||
Toast.makeText(preference.getContext(),
|
||||
R.string.thumbnail_cache_wipe_complete_notice, Toast.LENGTH_SHORT)
|
||||
.show();
|
||||
} catch (final IOException e) {
|
||||
Log.e(TAG, "Unable to clear Picasso cache", e);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onPreferenceTreeClick(final Preference preference) {
|
||||
if (preference.getKey().equals(thumbnailLoadToggleKey)) {
|
||||
final ImageLoader imageLoader = ImageLoader.getInstance();
|
||||
imageLoader.stop();
|
||||
imageLoader.clearDiskCache();
|
||||
imageLoader.clearMemoryCache();
|
||||
imageLoader.resume();
|
||||
Toast.makeText(preference.getContext(), R.string.thumbnail_cache_wipe_complete_notice,
|
||||
Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
|
||||
if (preference.getKey().equals(youtubeRestrictedModeEnabledKey)) {
|
||||
final Context context = getContext();
|
||||
if (context != null) {
|
||||
@@ -146,8 +147,8 @@ public class ContentSettingsFragment extends BasePreferenceFragment {
|
||||
.getPreferredLocalization(requireContext());
|
||||
final ContentCountry selectedContentCountry = org.schabi.newpipe.util.Localization
|
||||
.getPreferredContentCountry(requireContext());
|
||||
final String selectedLanguage = PreferenceManager
|
||||
.getDefaultSharedPreferences(requireContext()).getString("app_language_key", "en");
|
||||
final String selectedLanguage =
|
||||
defaultPreferences.getString(getString(R.string.app_language_key), "en");
|
||||
|
||||
if (!selectedLocalization.equals(initialSelectedLocalization)
|
||||
|| !selectedContentCountry.equals(initialSelectedContentCountry)
|
||||
@@ -183,7 +184,7 @@ public class ContentSettingsFragment extends BasePreferenceFragment {
|
||||
|
||||
new AlertDialog.Builder(requireActivity())
|
||||
.setMessage(R.string.override_current_data)
|
||||
.setPositiveButton(R.string.finish, (d, id) ->
|
||||
.setPositiveButton(R.string.ok, (d, id) ->
|
||||
importDatabase(file, lastImportDataUri))
|
||||
.setNegativeButton(R.string.cancel, (d, id) ->
|
||||
d.cancel())
|
||||
@@ -231,11 +232,11 @@ public class ContentSettingsFragment extends BasePreferenceFragment {
|
||||
final AlertDialog.Builder alert = new AlertDialog.Builder(requireContext());
|
||||
alert.setTitle(R.string.import_settings);
|
||||
|
||||
alert.setNegativeButton(android.R.string.no, (dialog, which) -> {
|
||||
alert.setNegativeButton(R.string.cancel, (dialog, which) -> {
|
||||
dialog.dismiss();
|
||||
finishImport(importDataUri);
|
||||
});
|
||||
alert.setPositiveButton(getString(R.string.finish), (dialog, which) -> {
|
||||
alert.setPositiveButton(R.string.ok, (dialog, which) -> {
|
||||
dialog.dismiss();
|
||||
manager.loadSharedPreferences(PreferenceManager
|
||||
.getDefaultSharedPreferences(requireContext()));
|
||||
|
||||
@@ -91,6 +91,11 @@ class ContentSettingsManager(private val fileLocator: NewPipeFileLocator) {
|
||||
is String -> {
|
||||
preferenceEditor.putString(key, value)
|
||||
}
|
||||
is Set<*> -> {
|
||||
// There are currently only Sets with type String possible
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
preferenceEditor.putStringSet(key, value as Set<String>?)
|
||||
}
|
||||
}
|
||||
}
|
||||
preferenceEditor.commit()
|
||||
|
||||
@@ -179,7 +179,7 @@ public class DownloadSettingsFragment extends BasePreferenceFragment {
|
||||
final AlertDialog.Builder msg = new AlertDialog.Builder(ctx);
|
||||
msg.setTitle(title);
|
||||
msg.setMessage(message);
|
||||
msg.setPositiveButton(getString(R.string.finish), null);
|
||||
msg.setPositiveButton(getString(R.string.ok), null);
|
||||
msg.show();
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import android.os.Build;
|
||||
import android.os.Environment;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.StringRes;
|
||||
import androidx.preference.PreferenceManager;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
@@ -59,6 +60,10 @@ public final class NewPipeSettings {
|
||||
isFirstRun = true;
|
||||
}
|
||||
|
||||
// first run migrations, then setDefaultValues, since the latter requires the correct types
|
||||
SettingMigrations.initMigrations(context, isFirstRun);
|
||||
|
||||
// readAgain is true so that if new settings are added their default value is set
|
||||
PreferenceManager.setDefaultValues(context, R.xml.main_settings, true);
|
||||
PreferenceManager.setDefaultValues(context, R.xml.video_audio_settings, true);
|
||||
PreferenceManager.setDefaultValues(context, R.xml.download_settings, true);
|
||||
@@ -71,8 +76,6 @@ public final class NewPipeSettings {
|
||||
|
||||
saveDefaultVideoDownloadDirectory(context);
|
||||
saveDefaultAudioDownloadDirectory(context);
|
||||
|
||||
SettingMigrations.initMigrations(context, isFirstRun);
|
||||
}
|
||||
|
||||
static void saveDefaultVideoDownloadDirectory(final Context context) {
|
||||
@@ -124,4 +127,29 @@ public final class NewPipeSettings {
|
||||
|
||||
return prefs.getBoolean(key, true);
|
||||
}
|
||||
|
||||
private static boolean showSearchSuggestions(final Context context,
|
||||
final SharedPreferences sharedPreferences,
|
||||
@StringRes final int key) {
|
||||
final Set<String> enabledSearchSuggestions = sharedPreferences.getStringSet(
|
||||
context.getString(R.string.show_search_suggestions_key), null);
|
||||
|
||||
if (enabledSearchSuggestions == null) {
|
||||
return true; // defaults to true
|
||||
} else {
|
||||
return enabledSearchSuggestions.contains(context.getString(key));
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean showLocalSearchSuggestions(final Context context,
|
||||
final SharedPreferences sharedPreferences) {
|
||||
return showSearchSuggestions(context, sharedPreferences,
|
||||
R.string.show_local_search_suggestions_key);
|
||||
}
|
||||
|
||||
public static boolean showRemoteSearchSuggestions(final Context context,
|
||||
final SharedPreferences sharedPreferences) {
|
||||
return showSearchSuggestions(context, sharedPreferences,
|
||||
R.string.show_remote_search_suggestions_key);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -218,7 +218,7 @@ public class PeertubeInstanceListFragment extends Fragment {
|
||||
.setIcon(R.drawable.place_holder_peertube)
|
||||
.setView(dialogBinding.getRoot())
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.setPositiveButton(R.string.finish, (dialog1, which) -> {
|
||||
.setPositiveButton(R.string.ok, (dialog1, which) -> {
|
||||
final String url = dialogBinding.dialogEditText.getText().toString();
|
||||
addInstance(url);
|
||||
})
|
||||
|
||||
@@ -14,13 +14,11 @@ import androidx.fragment.app.DialogFragment;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.nostra13.universalimageloader.core.DisplayImageOptions;
|
||||
import com.nostra13.universalimageloader.core.ImageLoader;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.database.subscription.SubscriptionEntity;
|
||||
import org.schabi.newpipe.error.ErrorActivity;
|
||||
import org.schabi.newpipe.local.subscription.SubscriptionManager;
|
||||
import org.schabi.newpipe.util.PicassoHelper;
|
||||
import org.schabi.newpipe.util.ThemeHelper;
|
||||
|
||||
import java.util.List;
|
||||
@@ -54,13 +52,6 @@ import io.reactivex.rxjava3.schedulers.Schedulers;
|
||||
*/
|
||||
|
||||
public class SelectChannelFragment extends DialogFragment {
|
||||
/**
|
||||
* This contains the base display options for images.
|
||||
*/
|
||||
private static final DisplayImageOptions DISPLAY_IMAGE_OPTIONS
|
||||
= new DisplayImageOptions.Builder().cacheInMemory(true).build();
|
||||
|
||||
private final ImageLoader imageLoader = ImageLoader.getInstance();
|
||||
|
||||
private OnSelectedListener onSelectedListener = null;
|
||||
private OnCancelListener onCancelListener = null;
|
||||
@@ -199,8 +190,7 @@ public class SelectChannelFragment extends DialogFragment {
|
||||
final SubscriptionEntity entry = subscriptions.get(position);
|
||||
holder.titleView.setText(entry.getName());
|
||||
holder.view.setOnClickListener(view -> clickedItem(position));
|
||||
imageLoader.displayImage(entry.getAvatarUrl(), holder.thumbnailView,
|
||||
DISPLAY_IMAGE_OPTIONS);
|
||||
PicassoHelper.loadAvatar(entry.getAvatarUrl()).into(holder.thumbnailView);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -14,9 +14,6 @@ import androidx.fragment.app.DialogFragment;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.nostra13.universalimageloader.core.DisplayImageOptions;
|
||||
import com.nostra13.universalimageloader.core.ImageLoader;
|
||||
|
||||
import org.schabi.newpipe.NewPipeDatabase;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.database.AppDatabase;
|
||||
@@ -29,6 +26,7 @@ import org.schabi.newpipe.error.ErrorInfo;
|
||||
import org.schabi.newpipe.error.UserAction;
|
||||
import org.schabi.newpipe.local.playlist.LocalPlaylistManager;
|
||||
import org.schabi.newpipe.local.playlist.RemotePlaylistManager;
|
||||
import org.schabi.newpipe.util.PicassoHelper;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Vector;
|
||||
@@ -38,13 +36,6 @@ import io.reactivex.rxjava3.core.Flowable;
|
||||
import io.reactivex.rxjava3.disposables.Disposable;
|
||||
|
||||
public class SelectPlaylistFragment extends DialogFragment {
|
||||
/**
|
||||
* This contains the base display options for images.
|
||||
*/
|
||||
private static final DisplayImageOptions DISPLAY_IMAGE_OPTIONS
|
||||
= new DisplayImageOptions.Builder().cacheInMemory(true).build();
|
||||
|
||||
private final ImageLoader imageLoader = ImageLoader.getInstance();
|
||||
|
||||
private OnSelectedListener onSelectedListener = null;
|
||||
|
||||
@@ -170,16 +161,15 @@ public class SelectPlaylistFragment extends DialogFragment {
|
||||
|
||||
holder.titleView.setText(entry.name);
|
||||
holder.view.setOnClickListener(view -> clickedItem(position));
|
||||
imageLoader.displayImage(entry.thumbnailUrl, holder.thumbnailView,
|
||||
DISPLAY_IMAGE_OPTIONS);
|
||||
PicassoHelper.loadPlaylistThumbnail(entry.thumbnailUrl).into(holder.thumbnailView);
|
||||
|
||||
} else if (selectedItem instanceof PlaylistRemoteEntity) {
|
||||
final PlaylistRemoteEntity entry = ((PlaylistRemoteEntity) selectedItem);
|
||||
|
||||
holder.titleView.setText(entry.getName());
|
||||
holder.view.setOnClickListener(view -> clickedItem(position));
|
||||
imageLoader.displayImage(entry.getThumbnailUrl(), holder.thumbnailView,
|
||||
DISPLAY_IMAGE_OPTIONS);
|
||||
PicassoHelper.loadPlaylistThumbnail(entry.getThumbnailUrl())
|
||||
.into(holder.thumbnailView);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -13,14 +13,22 @@ import org.schabi.newpipe.error.ErrorInfo;
|
||||
import org.schabi.newpipe.error.UserAction;
|
||||
import org.schabi.newpipe.util.DeviceUtils;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.schabi.newpipe.MainActivity.DEBUG;
|
||||
|
||||
/**
|
||||
* In order to add a migration, follow these steps, given P is the previous version:<br>
|
||||
* - in the class body add a new {@code MIGRATION_P_P+1 = new Migration(P, P+1) { ... }} and put in
|
||||
* the {@code migrate()} method the code that need to be run when migrating from P to P+1<br>
|
||||
* - add {@code MIGRATION_P_P+1} at the end of {@link SettingMigrations#SETTING_MIGRATIONS}<br>
|
||||
* - increment {@link SettingMigrations#VERSION}'s value by 1 (so it should become P+1)
|
||||
*/
|
||||
public final class SettingMigrations {
|
||||
|
||||
private static final String TAG = SettingMigrations.class.toString();
|
||||
/**
|
||||
* Version number for preferences. Must be incremented every time a migration is necessary.
|
||||
*/
|
||||
public static final int VERSION = 3;
|
||||
private static SharedPreferences sp;
|
||||
|
||||
public static final Migration MIGRATION_0_1 = new Migration(0, 1) {
|
||||
@@ -72,6 +80,35 @@ public final class SettingMigrations {
|
||||
}
|
||||
};
|
||||
|
||||
public static final Migration MIGRATION_3_4 = new Migration(3, 4) {
|
||||
@Override
|
||||
protected void migrate(final Context context) {
|
||||
// Pull request #3546 added support for choosing the type of search suggestions to
|
||||
// show, replacing the on-off switch used before, so migrate the previous user choice
|
||||
|
||||
final String showSearchSuggestionsKey =
|
||||
context.getString(R.string.show_search_suggestions_key);
|
||||
|
||||
boolean addAllSearchSuggestionTypes;
|
||||
try {
|
||||
addAllSearchSuggestionTypes = sp.getBoolean(showSearchSuggestionsKey, true);
|
||||
} catch (final ClassCastException e) {
|
||||
// just in case it was not a boolean for some reason, let's consider it a "true"
|
||||
addAllSearchSuggestionTypes = true;
|
||||
}
|
||||
|
||||
final Set<String> showSearchSuggestionsValueList = new HashSet<>();
|
||||
if (addAllSearchSuggestionTypes) {
|
||||
// if the preference was true, all suggestions will be shown, otherwise none
|
||||
Collections.addAll(showSearchSuggestionsValueList, context.getResources()
|
||||
.getStringArray(R.array.show_search_suggestions_value_list));
|
||||
}
|
||||
|
||||
sp.edit().putStringSet(
|
||||
showSearchSuggestionsKey, showSearchSuggestionsValueList).apply();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* List of all implemented migrations.
|
||||
* <p>
|
||||
@@ -81,9 +118,15 @@ public final class SettingMigrations {
|
||||
private static final Migration[] SETTING_MIGRATIONS = {
|
||||
MIGRATION_0_1,
|
||||
MIGRATION_1_2,
|
||||
MIGRATION_2_3
|
||||
MIGRATION_2_3,
|
||||
MIGRATION_3_4,
|
||||
};
|
||||
|
||||
/**
|
||||
* Version number for preferences. Must be incremented every time a migration is necessary.
|
||||
*/
|
||||
public static final int VERSION = 4;
|
||||
|
||||
|
||||
public static void initMigrations(final Context context, final boolean isFirstRun) {
|
||||
// setup migrations and check if there is something to do
|
||||
|
||||
@@ -6,11 +6,21 @@ import androidx.preference.Preference;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
|
||||
import static org.schabi.newpipe.CheckForNewAppVersion.startNewVersionCheckService;
|
||||
|
||||
public class UpdateSettingsFragment extends BasePreferenceFragment {
|
||||
private final Preference.OnPreferenceChangeListener updatePreferenceChange
|
||||
= (preference, newValue) -> {
|
||||
= (preference, checkForUpdates) -> {
|
||||
defaultPreferences.edit()
|
||||
.putBoolean(getString(R.string.update_app_key), (boolean) newValue).apply();
|
||||
.putBoolean(getString(R.string.update_app_key), (boolean) checkForUpdates).apply();
|
||||
|
||||
if ((boolean) checkForUpdates) {
|
||||
// Search for updates immediately when update checks are enabled.
|
||||
// Reset the expire time. This is necessary to check for an update immediately.
|
||||
defaultPreferences.edit()
|
||||
.putLong(getString(R.string.update_expiry_key), 0).apply();
|
||||
startNewVersionCheckService();
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
|
||||
@@ -459,11 +459,12 @@ public class StoredFileHelper implements Serializable {
|
||||
return !str1.equals(str2);
|
||||
}
|
||||
|
||||
public static Intent getPicker(@NonNull final Context ctx) {
|
||||
public static Intent getPicker(@NonNull final Context ctx,
|
||||
@NonNull final String mimeType) {
|
||||
if (NewPipeSettings.useStorageAccessFramework(ctx)) {
|
||||
return new Intent(Intent.ACTION_OPEN_DOCUMENT)
|
||||
.putExtra("android.content.extra.SHOW_ADVANCED", true)
|
||||
.setType("*/*")
|
||||
.setType(mimeType)
|
||||
.addCategory(Intent.CATEGORY_OPENABLE)
|
||||
.addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION
|
||||
| StoredDirectoryHelper.PERMISSION_FLAGS);
|
||||
@@ -477,8 +478,10 @@ public class StoredFileHelper implements Serializable {
|
||||
}
|
||||
}
|
||||
|
||||
public static Intent getPicker(@NonNull final Context ctx, @Nullable final Uri initialPath) {
|
||||
return applyInitialPathToPickerIntent(ctx, getPicker(ctx), initialPath, null);
|
||||
public static Intent getPicker(@NonNull final Context ctx,
|
||||
@NonNull final String mimeType,
|
||||
@Nullable final Uri initialPath) {
|
||||
return applyInitialPathToPickerIntent(ctx, getPicker(ctx, mimeType), initialPath, null);
|
||||
}
|
||||
|
||||
public static Intent getNewPicker(@NonNull final Context ctx,
|
||||
|
||||
@@ -11,6 +11,7 @@ import android.view.KeyEvent;
|
||||
|
||||
import androidx.annotation.Dimension;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.preference.PreferenceManager;
|
||||
|
||||
@@ -32,6 +33,9 @@ public final class DeviceUtils {
|
||||
// Zephir TS43UHD-2
|
||||
private static final boolean CVT_MT5886_EU_1G = Build.VERSION.SDK_INT == 24
|
||||
&& Build.DEVICE.equals("cvt_mt5886_eu_1g");
|
||||
// Hilife TV
|
||||
private static final boolean REALTEKATV = Build.VERSION.SDK_INT == 25
|
||||
&& Build.DEVICE.equals("RealtekATV");
|
||||
|
||||
private DeviceUtils() {
|
||||
}
|
||||
@@ -128,6 +132,16 @@ public final class DeviceUtils {
|
||||
public static boolean shouldSupportMediaTunneling() {
|
||||
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP
|
||||
&& !HI3798MV200
|
||||
&& !CVT_MT5886_EU_1G;
|
||||
&& !CVT_MT5886_EU_1G
|
||||
&& !REALTEKATV;
|
||||
}
|
||||
|
||||
public static boolean isLandscape(final Context context) {
|
||||
return context.getResources().getDisplayMetrics().heightPixels < context.getResources()
|
||||
.getDisplayMetrics().widthPixels;
|
||||
}
|
||||
|
||||
public static boolean isInMultiWindow(final AppCompatActivity activity) {
|
||||
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && activity.isInMultiWindowMode();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,65 +0,0 @@
|
||||
package org.schabi.newpipe.util;
|
||||
|
||||
import android.graphics.Bitmap;
|
||||
|
||||
import com.nostra13.universalimageloader.core.DisplayImageOptions;
|
||||
import com.nostra13.universalimageloader.core.assist.ImageScaleType;
|
||||
import com.nostra13.universalimageloader.core.display.FadeInBitmapDisplayer;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
|
||||
public final class ImageDisplayConstants {
|
||||
private static final int BITMAP_FADE_IN_DURATION_MILLIS = 250;
|
||||
|
||||
/**
|
||||
* This constant contains the base display options.
|
||||
*/
|
||||
private static final DisplayImageOptions BASE_DISPLAY_IMAGE_OPTIONS =
|
||||
new DisplayImageOptions.Builder()
|
||||
.cacheInMemory(true)
|
||||
.cacheOnDisk(true)
|
||||
.resetViewBeforeLoading(true)
|
||||
.bitmapConfig(Bitmap.Config.RGB_565)
|
||||
.imageScaleType(ImageScaleType.EXACTLY)
|
||||
.displayer(new FadeInBitmapDisplayer(BITMAP_FADE_IN_DURATION_MILLIS))
|
||||
.build();
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// DisplayImageOptions default configurations
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
public static final DisplayImageOptions DISPLAY_AVATAR_OPTIONS =
|
||||
new DisplayImageOptions.Builder()
|
||||
.cloneFrom(BASE_DISPLAY_IMAGE_OPTIONS)
|
||||
.showImageForEmptyUri(R.drawable.buddy)
|
||||
.showImageOnFail(R.drawable.buddy)
|
||||
.build();
|
||||
|
||||
public static final DisplayImageOptions DISPLAY_THUMBNAIL_OPTIONS =
|
||||
new DisplayImageOptions.Builder()
|
||||
.cloneFrom(BASE_DISPLAY_IMAGE_OPTIONS)
|
||||
.showImageForEmptyUri(R.drawable.dummy_thumbnail)
|
||||
.showImageOnFail(R.drawable.dummy_thumbnail)
|
||||
.build();
|
||||
|
||||
public static final DisplayImageOptions DISPLAY_BANNER_OPTIONS =
|
||||
new DisplayImageOptions.Builder()
|
||||
.cloneFrom(BASE_DISPLAY_IMAGE_OPTIONS)
|
||||
.showImageForEmptyUri(R.drawable.channel_banner)
|
||||
.showImageOnFail(R.drawable.channel_banner)
|
||||
.build();
|
||||
|
||||
public static final DisplayImageOptions DISPLAY_PLAYLIST_OPTIONS =
|
||||
new DisplayImageOptions.Builder()
|
||||
.cloneFrom(BASE_DISPLAY_IMAGE_OPTIONS)
|
||||
.showImageForEmptyUri(R.drawable.dummy_thumbnail_playlist)
|
||||
.showImageOnFail(R.drawable.dummy_thumbnail_playlist)
|
||||
.build();
|
||||
|
||||
public static final DisplayImageOptions DISPLAY_SEEKBAR_PREVIEW_OPTIONS =
|
||||
new DisplayImageOptions.Builder()
|
||||
.cloneFrom(BASE_DISPLAY_IMAGE_OPTIONS)
|
||||
.build();
|
||||
|
||||
private ImageDisplayConstants() { }
|
||||
}
|
||||
@@ -226,6 +226,16 @@ public final class Localization {
|
||||
shortCount(context, subscriberCount));
|
||||
}
|
||||
|
||||
public static String downloadCount(final Context context, final int downloadCount) {
|
||||
return getQuantity(context, R.plurals.download_finished_notification, 0,
|
||||
downloadCount, shortCount(context, downloadCount));
|
||||
}
|
||||
|
||||
public static String deletedDownloadCount(final Context context, final int deletedCount) {
|
||||
return getQuantity(context, R.plurals.deleted_downloads_toast, 0,
|
||||
deletedCount, shortCount(context, deletedCount));
|
||||
}
|
||||
|
||||
private static String getQuantity(final Context context, @PluralsRes final int pluralId,
|
||||
@StringRes final int zeroCaseStringId, final long count,
|
||||
final String formattedCount) {
|
||||
|
||||
@@ -18,8 +18,6 @@ import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
import androidx.fragment.app.FragmentTransaction;
|
||||
|
||||
import com.nostra13.universalimageloader.core.ImageLoader;
|
||||
|
||||
import org.schabi.newpipe.MainActivity;
|
||||
import org.schabi.newpipe.NewPipeDatabase;
|
||||
import org.schabi.newpipe.R;
|
||||
@@ -47,6 +45,7 @@ import org.schabi.newpipe.local.playlist.LocalPlaylistFragment;
|
||||
import org.schabi.newpipe.local.subscription.SubscriptionFragment;
|
||||
import org.schabi.newpipe.local.subscription.SubscriptionsImportFragment;
|
||||
import org.schabi.newpipe.player.MainPlayer;
|
||||
import org.schabi.newpipe.player.MainPlayer.PlayerType;
|
||||
import org.schabi.newpipe.player.PlayQueueActivity;
|
||||
import org.schabi.newpipe.player.Player;
|
||||
import org.schabi.newpipe.player.helper.PlayerHelper;
|
||||
@@ -60,22 +59,25 @@ import java.util.ArrayList;
|
||||
|
||||
import static org.schabi.newpipe.util.external_communication.ShareUtils.installApp;
|
||||
|
||||
import com.jakewharton.processphoenix.ProcessPhoenix;
|
||||
|
||||
public final class NavigationHelper {
|
||||
public static final String MAIN_FRAGMENT_TAG = "main_fragment_tag";
|
||||
public static final String SEARCH_FRAGMENT_TAG = "search_fragment_tag";
|
||||
|
||||
private static final String TAG = NavigationHelper.class.getSimpleName();
|
||||
|
||||
private NavigationHelper() {
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Players
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
/* INTENT */
|
||||
@NonNull
|
||||
public static <T> Intent getPlayerIntent(@NonNull final Context context,
|
||||
@NonNull final Class<T> targetClazz,
|
||||
@Nullable final PlayQueue playQueue,
|
||||
final boolean resumePlayback) {
|
||||
@Nullable final PlayQueue playQueue) {
|
||||
final Intent intent = new Intent(context, targetClazz);
|
||||
|
||||
if (playQueue != null) {
|
||||
@@ -84,7 +86,6 @@ public final class NavigationHelper {
|
||||
intent.putExtra(Player.PLAY_QUEUE_KEY, cacheKey);
|
||||
}
|
||||
}
|
||||
intent.putExtra(Player.RESUME_PLAYBACK, resumePlayback);
|
||||
intent.putExtra(Player.PLAYER_TYPE, MainPlayer.PlayerType.VIDEO.ordinal());
|
||||
|
||||
return intent;
|
||||
@@ -94,23 +95,28 @@ public final class NavigationHelper {
|
||||
public static <T> Intent getPlayerIntent(@NonNull final Context context,
|
||||
@NonNull final Class<T> targetClazz,
|
||||
@Nullable final PlayQueue playQueue,
|
||||
final boolean resumePlayback,
|
||||
final boolean playWhenReady) {
|
||||
return getPlayerIntent(context, targetClazz, playQueue, resumePlayback)
|
||||
return getPlayerIntent(context, targetClazz, playQueue)
|
||||
.putExtra(Player.PLAY_WHEN_READY, playWhenReady);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public static <T> Intent getPlayerEnqueueIntent(@NonNull final Context context,
|
||||
@NonNull final Class<T> targetClazz,
|
||||
@Nullable final PlayQueue playQueue,
|
||||
final boolean selectOnAppend,
|
||||
final boolean resumePlayback) {
|
||||
return getPlayerIntent(context, targetClazz, playQueue, resumePlayback)
|
||||
.putExtra(Player.APPEND_ONLY, true)
|
||||
.putExtra(Player.SELECT_ON_APPEND, selectOnAppend);
|
||||
@Nullable final PlayQueue playQueue) {
|
||||
return getPlayerIntent(context, targetClazz, playQueue)
|
||||
.putExtra(Player.ENQUEUE, true);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public static <T> Intent getPlayerEnqueueNextIntent(@NonNull final Context context,
|
||||
@NonNull final Class<T> targetClazz,
|
||||
@Nullable final PlayQueue playQueue) {
|
||||
return getPlayerIntent(context, targetClazz, playQueue)
|
||||
.putExtra(Player.ENQUEUE_NEXT, true);
|
||||
}
|
||||
|
||||
/* PLAY */
|
||||
public static void playOnMainPlayer(final AppCompatActivity activity,
|
||||
@NonNull final PlayQueue playQueue) {
|
||||
final PlayQueueItem item = playQueue.getItem();
|
||||
@@ -156,56 +162,38 @@ public final class NavigationHelper {
|
||||
ContextCompat.startForegroundService(context, intent);
|
||||
}
|
||||
|
||||
public static void enqueueOnVideoPlayer(final Context context, final PlayQueue queue,
|
||||
final boolean resumePlayback) {
|
||||
enqueueOnVideoPlayer(context, queue, false, resumePlayback);
|
||||
}
|
||||
|
||||
public static void enqueueOnVideoPlayer(final Context context, final PlayQueue queue,
|
||||
final boolean selectOnAppend,
|
||||
final boolean resumePlayback) {
|
||||
|
||||
/* ENQUEUE */
|
||||
public static void enqueueOnPlayer(final Context context,
|
||||
final PlayQueue queue,
|
||||
final PlayerType playerType) {
|
||||
Toast.makeText(context, R.string.enqueued, Toast.LENGTH_SHORT).show();
|
||||
final Intent intent = getPlayerEnqueueIntent(
|
||||
context, MainPlayer.class, queue, selectOnAppend, resumePlayback);
|
||||
final Intent intent = getPlayerEnqueueIntent(context, MainPlayer.class, queue);
|
||||
|
||||
intent.putExtra(Player.PLAYER_TYPE, MainPlayer.PlayerType.VIDEO.ordinal());
|
||||
intent.putExtra(Player.PLAYER_TYPE, playerType.ordinal());
|
||||
ContextCompat.startForegroundService(context, intent);
|
||||
}
|
||||
|
||||
public static void enqueueOnPopupPlayer(final Context context, final PlayQueue queue,
|
||||
final boolean resumePlayback) {
|
||||
enqueueOnPopupPlayer(context, queue, false, resumePlayback);
|
||||
}
|
||||
|
||||
public static void enqueueOnPopupPlayer(final Context context, final PlayQueue queue,
|
||||
final boolean selectOnAppend,
|
||||
final boolean resumePlayback) {
|
||||
if (!PermissionHelper.isPopupEnabled(context)) {
|
||||
PermissionHelper.showPopupEnablementToast(context);
|
||||
return;
|
||||
public static void enqueueOnPlayer(final Context context, final PlayQueue queue) {
|
||||
PlayerType playerType = PlayerHolder.getInstance().getType();
|
||||
if (!PlayerHolder.getInstance().isPlayerOpen()) {
|
||||
Log.e(TAG, "Enqueueing but no player is open; defaulting to background player");
|
||||
playerType = MainPlayer.PlayerType.AUDIO;
|
||||
}
|
||||
|
||||
Toast.makeText(context, R.string.enqueued, Toast.LENGTH_SHORT).show();
|
||||
final Intent intent = getPlayerEnqueueIntent(
|
||||
context, MainPlayer.class, queue, selectOnAppend, resumePlayback);
|
||||
intent.putExtra(Player.PLAYER_TYPE, MainPlayer.PlayerType.POPUP.ordinal());
|
||||
ContextCompat.startForegroundService(context, intent);
|
||||
enqueueOnPlayer(context, queue, playerType);
|
||||
}
|
||||
|
||||
public static void enqueueOnBackgroundPlayer(final Context context, final PlayQueue queue,
|
||||
final boolean resumePlayback) {
|
||||
enqueueOnBackgroundPlayer(context, queue, false, resumePlayback);
|
||||
}
|
||||
/* ENQUEUE NEXT */
|
||||
public static void enqueueNextOnPlayer(final Context context, final PlayQueue queue) {
|
||||
PlayerType playerType = PlayerHolder.getInstance().getType();
|
||||
if (!PlayerHolder.getInstance().isPlayerOpen()) {
|
||||
Log.e(TAG, "Enqueueing next but no player is open; defaulting to background player");
|
||||
playerType = MainPlayer.PlayerType.AUDIO;
|
||||
}
|
||||
Toast.makeText(context, R.string.enqueued_next, Toast.LENGTH_SHORT).show();
|
||||
final Intent intent = getPlayerEnqueueNextIntent(context, MainPlayer.class, queue);
|
||||
|
||||
public static void enqueueOnBackgroundPlayer(final Context context,
|
||||
final PlayQueue queue,
|
||||
final boolean selectOnAppend,
|
||||
final boolean resumePlayback) {
|
||||
Toast.makeText(context, R.string.enqueued, Toast.LENGTH_SHORT).show();
|
||||
final Intent intent = getPlayerEnqueueIntent(
|
||||
context, MainPlayer.class, queue, selectOnAppend, resumePlayback);
|
||||
intent.putExtra(Player.PLAYER_TYPE, MainPlayer.PlayerType.AUDIO.ordinal());
|
||||
intent.putExtra(Player.PLAYER_TYPE, playerType.ordinal());
|
||||
ContextCompat.startForegroundService(context, intent);
|
||||
}
|
||||
|
||||
@@ -259,10 +247,9 @@ public final class NavigationHelper {
|
||||
if (context instanceof Activity) {
|
||||
new AlertDialog.Builder(context)
|
||||
.setMessage(R.string.no_player_found)
|
||||
.setPositiveButton(R.string.install, (dialog, which) -> {
|
||||
ShareUtils.openUrlInBrowser(context,
|
||||
context.getString(R.string.fdroid_vlc_url), false);
|
||||
})
|
||||
.setPositiveButton(R.string.install,
|
||||
(dialog, which) -> ShareUtils.openUrlInBrowser(context,
|
||||
context.getString(R.string.fdroid_vlc_url), false))
|
||||
.setNegativeButton(R.string.cancel, (dialog, which)
|
||||
-> Log.i("NavigationHelper", "You unlocked a secret unicorn."))
|
||||
.show();
|
||||
@@ -284,8 +271,6 @@ public final class NavigationHelper {
|
||||
}
|
||||
|
||||
public static void gotoMainFragment(final FragmentManager fragmentManager) {
|
||||
ImageLoader.getInstance().clearMemoryCache();
|
||||
|
||||
final boolean popped = fragmentManager.popBackStackImmediate(MAIN_FRAGMENT_TAG, 0);
|
||||
if (!popped) {
|
||||
openMainFragment(fragmentManager);
|
||||
@@ -351,7 +336,7 @@ public final class NavigationHelper {
|
||||
|
||||
final boolean autoPlay;
|
||||
@Nullable final MainPlayer.PlayerType playerType = PlayerHolder.getInstance().getType();
|
||||
if (playerType == null) {
|
||||
if (!PlayerHolder.getInstance().isPlayerOpen()) {
|
||||
// no player open
|
||||
autoPlay = PlayerHelper.isAutoplayAllowedByUser(context);
|
||||
} else if (switchingPlayers) {
|
||||
@@ -365,13 +350,15 @@ public final class NavigationHelper {
|
||||
autoPlay = false;
|
||||
}
|
||||
|
||||
final RunnableWithVideoDetailFragment onVideoDetailFragmentReady = (detailFragment) -> {
|
||||
final RunnableWithVideoDetailFragment onVideoDetailFragmentReady = detailFragment -> {
|
||||
expandMainPlayer(detailFragment.requireActivity());
|
||||
detailFragment.setAutoPlay(autoPlay);
|
||||
if (switchingPlayers) {
|
||||
// Situation when user switches from players to main player. All needed data is
|
||||
// here, we can start watching (assuming newQueue equals playQueue).
|
||||
detailFragment.openVideoPlayer();
|
||||
// Starting directly in fullscreen if the previous player type was popup.
|
||||
detailFragment.openVideoPlayer(playerType == MainPlayer.PlayerType.POPUP
|
||||
|| PlayerHelper.isStartMainPlayerFullscreenEnabled(context));
|
||||
} else {
|
||||
detailFragment.selectAndLoadVideo(serviceId, url, title, playQueue);
|
||||
}
|
||||
@@ -610,8 +597,7 @@ public final class NavigationHelper {
|
||||
*/
|
||||
public static void restartApp(final Activity activity) {
|
||||
NewPipeDatabase.close();
|
||||
activity.finishAffinity();
|
||||
final Intent intent = new Intent(activity, MainActivity.class);
|
||||
activity.startActivity(intent);
|
||||
|
||||
ProcessPhoenix.triggerRebirth(activity.getApplicationContext());
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user