mirror of
https://github.com/TeamNewPipe/NewPipe
synced 2026-01-14 10:42:40 +00:00
Compare commits
386 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
600e156c4c | ||
|
|
464d0e50b0 | ||
|
|
7411c54f9e | ||
|
|
7c74deb700 | ||
|
|
e63165e80f | ||
|
|
e2bc9dfacd | ||
|
|
48789dbab7 | ||
|
|
196f0f0475 | ||
|
|
085b59f2e1 | ||
|
|
2fdc2664ff | ||
|
|
a33a5c5527 | ||
|
|
a7c0f37904 | ||
|
|
4889ab3462 | ||
|
|
3923deeaad | ||
|
|
80d6fff0ca | ||
|
|
fe43b4da39 | ||
|
|
67afd05e22 | ||
|
|
0fcaf20221 | ||
|
|
0277b94b37 | ||
|
|
c7efa8c4f1 | ||
|
|
bf6645e829 | ||
|
|
ea1b42510c | ||
|
|
72818ffa42 | ||
|
|
985308bf0c | ||
|
|
86381696f4 | ||
|
|
08b960cc6e | ||
|
|
731c65cd59 | ||
|
|
a85e8a29ff | ||
|
|
22b2f52f8c | ||
|
|
a713ce2126 | ||
|
|
4fac3cf304 | ||
|
|
74e20a8c52 | ||
|
|
8cf4ba25f5 | ||
|
|
feb65cf8f3 | ||
|
|
93592d23f4 | ||
|
|
338a4837bc | ||
|
|
8e19fe535c | ||
|
|
2aeccc0c5c | ||
|
|
8db1234a59 | ||
|
|
80fb351ad3 | ||
|
|
523f85d4d1 | ||
|
|
bfff500915 | ||
|
|
3e83bb0d95 | ||
|
|
bdaee25e61 | ||
|
|
71d3227791 | ||
|
|
a28aa6a8c4 | ||
|
|
292e103073 | ||
|
|
39a3f03e79 | ||
|
|
404a6c12a6 | ||
|
|
8271409afe | ||
|
|
985f659026 | ||
|
|
7c36cbffd0 | ||
|
|
285ea4e3fd | ||
|
|
8ce18647f1 | ||
|
|
aee0478235 | ||
|
|
c3cf1d81c2 | ||
|
|
c2b6cec37d | ||
|
|
b265cabc22 | ||
|
|
463dd8ea74 | ||
|
|
0263125e11 | ||
|
|
1fc8e4c148 | ||
|
|
c43bca6007 | ||
|
|
4c31636d19 | ||
|
|
3a61ab59f2 | ||
|
|
1db3c57ef0 | ||
|
|
eeaf3496d5 | ||
|
|
70f421b787 | ||
|
|
86fa629591 | ||
|
|
553b80164b | ||
|
|
8518933ca8 | ||
|
|
ea53b7d4ad | ||
|
|
37a96d063f | ||
|
|
b360920472 | ||
|
|
9e1744f904 | ||
|
|
85a468bda9 | ||
|
|
7955ef8105 | ||
|
|
80cd41893b | ||
|
|
9b09f2ad71 | ||
|
|
c45d9559c4 | ||
|
|
f0d978b4c6 | ||
|
|
8734f4bbe3 | ||
|
|
9f03280075 | ||
|
|
427ac4ef35 | ||
|
|
d9c4495e8e | ||
|
|
d09070b61d | ||
|
|
d6855a6b50 | ||
|
|
9be970a4c4 | ||
|
|
b236bb407b | ||
|
|
8978187c64 | ||
|
|
eba0b07782 | ||
|
|
1f77e00df4 | ||
|
|
41c70cc85d | ||
|
|
5bc0a8fba1 | ||
|
|
687020e595 | ||
|
|
8c75b96c38 | ||
|
|
6f7a01bc53 | ||
|
|
5383a0af0b | ||
|
|
3e50466024 | ||
|
|
45b703daf6 | ||
|
|
f1e1f6424a | ||
|
|
460f031cef | ||
|
|
25aaf4e48b | ||
|
|
a26baa3061 | ||
|
|
138513d790 | ||
|
|
1e5dc01825 | ||
|
|
8c4b1b967d | ||
|
|
7faa107547 | ||
|
|
85ccc2384f | ||
|
|
469a5b1974 | ||
|
|
80161c36c6 | ||
|
|
aea912f499 | ||
|
|
156d7139fa | ||
|
|
0ad3d0247d | ||
|
|
092f9170cc | ||
|
|
b820e9a888 | ||
|
|
b9cd55188e | ||
|
|
ebd45dfae3 | ||
|
|
40195b2d98 | ||
|
|
0b0305eaed | ||
|
|
950997ea66 | ||
|
|
be4beb41b6 | ||
|
|
c55f87c962 | ||
|
|
bdc85b435c | ||
|
|
522d6d8b01 | ||
|
|
e98838ad7e | ||
|
|
3829565ea0 | ||
|
|
c16a8dacd0 | ||
|
|
02db971b7c | ||
|
|
0d522aae6c | ||
|
|
fdb0f01b38 | ||
|
|
376cba696e | ||
|
|
cade272501 | ||
|
|
4f828fbe00 | ||
|
|
3d348c63d9 | ||
|
|
fe10c19956 | ||
|
|
9ada979484 | ||
|
|
2926cb7682 | ||
|
|
8c15cc1c17 | ||
|
|
bea4fb6ae6 | ||
|
|
cafc64534b | ||
|
|
327fc742d3 | ||
|
|
0c5df29417 | ||
|
|
cce896e900 | ||
|
|
50c0f9e622 | ||
|
|
10ec67854e | ||
|
|
e327f7ba2c | ||
|
|
9a65f02d5b | ||
|
|
d1fc9c5880 | ||
|
|
3c9ae68314 | ||
|
|
5e7c2c11f6 | ||
|
|
053b6ab8c6 | ||
|
|
5814743d59 | ||
|
|
fa7613b8d1 | ||
|
|
d3d05d613d | ||
|
|
23b5cd5b72 | ||
|
|
d4a33603ab | ||
|
|
39724de6e6 | ||
|
|
156adaa1a0 | ||
|
|
3868243c2a | ||
|
|
243f539439 | ||
|
|
71d92c8d1b | ||
|
|
e840d42fb9 | ||
|
|
750c4ffbd3 | ||
|
|
d043a4f410 | ||
|
|
4c3ba0fe3d | ||
|
|
a314f55a17 | ||
|
|
78a9811fe3 | ||
|
|
6277639ded | ||
|
|
d1c807487a | ||
|
|
fe92abde0e | ||
|
|
098c954ef1 | ||
|
|
01396923f1 | ||
|
|
e0de66b1be | ||
|
|
77675b361f | ||
|
|
e2dd058430 | ||
|
|
a188125982 | ||
|
|
9e5f079cf2 | ||
|
|
51a948bfcf | ||
|
|
9d27d49c1f | ||
|
|
761f6568fa | ||
|
|
ee94b296ae | ||
|
|
b387946d34 | ||
|
|
46afe5153f | ||
|
|
68be87724a | ||
|
|
8c9f2af855 | ||
|
|
594f0b10ba | ||
|
|
79e98db3bd | ||
|
|
a57fd69fb4 | ||
|
|
b73eb9438d | ||
|
|
920e560b4b | ||
|
|
0d33f8b460 | ||
|
|
5b58850c31 | ||
|
|
17746f35f9 | ||
|
|
ca0b211854 | ||
|
|
54cb26ff03 | ||
|
|
a7ff73dbfd | ||
|
|
815dd0f706 | ||
|
|
7455dc93ac | ||
|
|
337662bd40 | ||
|
|
91305771bc | ||
|
|
98ed80d305 | ||
|
|
5313e1861a | ||
|
|
d8665366ef | ||
|
|
c216f29fb0 | ||
|
|
302fde6004 | ||
|
|
14ddf37988 | ||
|
|
87568b6590 | ||
|
|
37aa41afae | ||
|
|
41968918bb | ||
|
|
8fd48a88be | ||
|
|
10c35f354e | ||
|
|
9ee7740fcc | ||
|
|
94b086de20 | ||
|
|
c90696e67e | ||
|
|
8378789f6a | ||
|
|
059bb7622d | ||
|
|
cece83328a | ||
|
|
4a12b0ab2d | ||
|
|
f6e2dd1480 | ||
|
|
f04b5fd42f | ||
|
|
5994cd8ea2 | ||
|
|
83f33a7d1b | ||
|
|
f80e1bd214 | ||
|
|
97672f06de | ||
|
|
6039484a02 | ||
|
|
7682ebd245 | ||
|
|
7c581ec108 | ||
|
|
910d22daa6 | ||
|
|
979102a2d9 | ||
|
|
98be89a20a | ||
|
|
0264383ad2 | ||
|
|
e2ea217bc5 | ||
|
|
fa75c79d34 | ||
|
|
0c86a4e608 | ||
|
|
031585be3f | ||
|
|
92a87a5ed2 | ||
|
|
4c26e597e4 | ||
|
|
5108bf1742 | ||
|
|
6215faa06c | ||
|
|
fee1fed0a1 | ||
|
|
50dcf308a2 | ||
|
|
486e720e00 | ||
|
|
a6c09e2dac | ||
|
|
43e4dc8170 | ||
|
|
5c4d72ec42 | ||
|
|
114806db55 | ||
|
|
0ff7170ab1 | ||
|
|
6b2f084cda | ||
|
|
907106156f | ||
|
|
50a026183d | ||
|
|
716d795970 | ||
|
|
fcfdcd1025 | ||
|
|
af119db1d7 | ||
|
|
122e80fae9 | ||
|
|
8fceffd6fd | ||
|
|
f778c48923 | ||
|
|
19cd3a17df | ||
|
|
ea91a62c89 | ||
|
|
cef791ba1b | ||
|
|
f78a7fa630 | ||
|
|
ac59382b84 | ||
|
|
68175c1cf0 | ||
|
|
aeca8dc5b2 | ||
|
|
e75ef086af | ||
|
|
14a2171035 | ||
|
|
0cdd866393 | ||
|
|
24c1cfbf72 | ||
|
|
31899d2ab9 | ||
|
|
16c44f3a30 | ||
|
|
1b4bde4e78 | ||
|
|
ff9ae57f39 | ||
|
|
71add5a7c2 | ||
|
|
ce2719d77e | ||
|
|
8193a0df63 | ||
|
|
48a5107296 | ||
|
|
ebd589c9cb | ||
|
|
1f15368b7b | ||
|
|
8fe1a76ec6 | ||
|
|
83faf119a9 | ||
|
|
0a05534c84 | ||
|
|
137fbb34d9 | ||
|
|
d45ce19b04 | ||
|
|
7153506ddb | ||
|
|
0483d3ff32 | ||
|
|
7e784ce9a7 | ||
|
|
8343d9cc18 | ||
|
|
db0ecd92ca | ||
|
|
b5140cfecd | ||
|
|
36aea35a92 | ||
|
|
1984436b41 | ||
|
|
8ba2f5f964 | ||
|
|
c923d35a1f | ||
|
|
c550779472 | ||
|
|
90d3c9ced0 | ||
|
|
43cbc09f1f | ||
|
|
feea084c60 | ||
|
|
d403a83a24 | ||
|
|
35fc27cfb0 | ||
|
|
81742565a4 | ||
|
|
923d0b7c80 | ||
|
|
d416465371 | ||
|
|
2586c543d3 | ||
|
|
c32bc26328 | ||
|
|
4074c71b6a | ||
|
|
a7493d1039 | ||
|
|
d7cab6a8d8 | ||
|
|
eddc12693a | ||
|
|
ced3898499 | ||
|
|
318a5df109 | ||
|
|
b2e9981313 | ||
|
|
5f092e37f9 | ||
|
|
81bbef04dc | ||
|
|
74f43639ad | ||
|
|
fc342bd458 | ||
|
|
adfbf5b49f | ||
|
|
2cb7bb84f7 | ||
|
|
84d1792e7f | ||
|
|
531859ac60 | ||
|
|
b5bf0d7e1d | ||
|
|
bf071d65d7 | ||
|
|
e4aa7a90c7 | ||
|
|
6d4e3c5633 | ||
|
|
19f9b4f502 | ||
|
|
2b8837609b | ||
|
|
f3dbb19364 | ||
|
|
0a831ec84e | ||
|
|
0c656abb8e | ||
|
|
7a7a90bf79 | ||
|
|
908dff3931 | ||
|
|
a786cff036 | ||
|
|
28802805f8 | ||
|
|
f59099395f | ||
|
|
0fe3fe7594 | ||
|
|
467dacd35a | ||
|
|
173150591d | ||
|
|
e4d94b1a4e | ||
|
|
75e34a5a8e | ||
|
|
d6121c8e21 | ||
|
|
b4d77df1be | ||
|
|
e6021465f6 | ||
|
|
22ec70e94d | ||
|
|
a1a70a94a8 | ||
|
|
a65ed7e914 | ||
|
|
4545b8e92d | ||
|
|
ba0c0fb109 | ||
|
|
18d530021c | ||
|
|
31bb70e333 | ||
|
|
a919a039e5 | ||
|
|
aacb1f46a8 | ||
|
|
96862cbcb3 | ||
|
|
10f79e1307 | ||
|
|
e0ee3dce40 | ||
|
|
13e7d2e7ac | ||
|
|
a7723373a0 | ||
|
|
7e469ead45 | ||
|
|
5397a4e410 | ||
|
|
99b59f0126 | ||
|
|
d46c7eb8fe | ||
|
|
e4a1fc9d95 | ||
|
|
276f50a944 | ||
|
|
40fcd93312 | ||
|
|
807e4d4af9 | ||
|
|
480348f11a | ||
|
|
30613b7064 | ||
|
|
79189dcc83 | ||
|
|
c2210330b6 | ||
|
|
917f459569 | ||
|
|
0ced9ba799 | ||
|
|
5e95277d7c | ||
|
|
efb417dba7 | ||
|
|
5c2d4c4d9d | ||
|
|
e8bd9920fd | ||
|
|
69ed531a5c | ||
|
|
b967d7c148 | ||
|
|
90150c42ed | ||
|
|
8e2fd9ccce | ||
|
|
567ffad41d | ||
|
|
a00ac6b9ca | ||
|
|
ac86fe80c8 | ||
|
|
b30e025bda | ||
|
|
5f3eb4871a | ||
|
|
9a223532c5 | ||
|
|
f341f43427 | ||
|
|
f7f00293cc | ||
|
|
c0ff1e86b9 | ||
|
|
b822c5a039 |
2
.github/ISSUE_TEMPLATE/bug_report.md
vendored
2
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -15,7 +15,7 @@ Oh no, a bug! It happens. Thanks for reporting an issue with NewPipe. To make it
|
||||
<!-- The comments between these brackets won't show up in the submitted issue (as you can see in the Preview). -->
|
||||
|
||||
### Checklist
|
||||
<!-- The first box has been checked for you to show you how it is done. -->
|
||||
<!-- This checklist is COMPULSORY. The first box has been checked for you to show you how it is done. -->
|
||||
|
||||
- [x] I am using the latest version - x.xx.x <!-- Check https://github.com/TeamNewPipe/NewPipe/releases -->
|
||||
- [ ] I checked, but didn't find any duplicates (open OR closed) of this issue in the repo. <!-- Seriously, check. O_O -->
|
||||
|
||||
7
.github/ISSUE_TEMPLATE/config.yml
vendored
7
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -1 +1,8 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: 💬 IRC
|
||||
url: https://webchat.freenode.net/#newpipe
|
||||
about: Chat with us via IRC for quick Q/A
|
||||
- name: 💬 Matrix
|
||||
url: https://matrix.to/#/#freenode_#newpipe:matrix.org
|
||||
about: Chat with us via Matrix for quick Q/A
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/feature_request.md
vendored
2
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@@ -11,7 +11,7 @@ assignees: ''
|
||||
<!-- The comments between these brackets won't show up in the submitted issue (as you can see in the Preview). -->
|
||||
|
||||
### Checklist
|
||||
<!-- The first box has been checked for you to show you how it is done. -->
|
||||
<!-- This checklist is COMPULSORY. The first box has been checked for you to show you how it is done. -->
|
||||
|
||||
- [x] I checked, but didn't find any duplicates (open OR closed) of this issue in the repo. <!-- Seriously, check. O_O -->
|
||||
- [ ] I have read the contribution guidelines given at https://github.com/TeamNewPipe/NewPipe/blob/HEAD/.github/CONTRIBUTING.md.
|
||||
|
||||
3
.github/PULL_REQUEST_TEMPLATE.md
vendored
3
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -22,7 +22,8 @@
|
||||
|
||||
#### APK testing
|
||||
<!-- Use a new, meaningfully named branch. The name is used as a suffix for the app ID to allow installing and testing multiple versions of NewPipe, e.g. "commentfix", if your PR implements a bugfix for comments. (No names like "patch-0" and "feature-1".) -->
|
||||
debug.zip
|
||||
<!-- Remove the following line if you directly link the APK created by the CI pipeline. Directly linking is preferred if you need to let users test.-->
|
||||
On the website the APK can be found by going to the "Checks" tab below the title and then on "artifacts" on the right.
|
||||
|
||||
#### Due diligence
|
||||
- [ ] I read the [contribution guidelines](https://github.com/TeamNewPipe/NewPipe/blob/HEAD/.github/CONTRIBUTING.md).
|
||||
|
||||
73
.github/workflows/ci.yml
vendored
Normal file
73
.github/workflows/ci.yml
vendored
Normal file
@@ -0,0 +1,73 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- dev
|
||||
push:
|
||||
branches:
|
||||
- dev
|
||||
- master
|
||||
|
||||
jobs:
|
||||
build-and-test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: create and checkout branch
|
||||
# push events already checked out the branch
|
||||
if: github.event_name == 'pull_request'
|
||||
run: git checkout -B ${{ github.head_ref }}
|
||||
|
||||
- name: set up JDK 1.8
|
||||
uses: actions/setup-java@v1.4.3
|
||||
with:
|
||||
java-version: 1.8
|
||||
|
||||
- name: Cache Gradle dependencies
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ~/.gradle/caches
|
||||
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }}
|
||||
restore-keys: ${{ runner.os }}-gradle
|
||||
|
||||
- name: Build debug APK and run Tests
|
||||
run: ./gradlew assembleDebug lintDebug testDebugUnitTest --stacktrace
|
||||
|
||||
- name: Upload APK
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: app
|
||||
path: app/build/outputs/apk/debug/*.apk
|
||||
# sonar:
|
||||
# runs-on: ubuntu-latest
|
||||
# steps:
|
||||
# - uses: actions/checkout@v2
|
||||
# with:
|
||||
# fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis
|
||||
|
||||
# - name: Set up JDK 11
|
||||
# uses: actions/setup-java@v1.4.3
|
||||
# with:
|
||||
# java-version: 11 # Sonar requires JDK 11
|
||||
|
||||
# - name: Cache SonarCloud packages
|
||||
# uses: actions/cache@v2
|
||||
# with:
|
||||
# path: ~/.sonar/cache
|
||||
# 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
|
||||
# SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
||||
# run: ./gradlew build sonarqube --info
|
||||
0
.gitmodules
vendored
0
.gitmodules
vendored
18
.travis.yml
18
.travis.yml
@@ -1,18 +0,0 @@
|
||||
language: android
|
||||
jdk:
|
||||
- oraclejdk8
|
||||
android:
|
||||
components:
|
||||
# The BuildTools version used by NewPipe
|
||||
- tools
|
||||
- build-tools-29.0.3
|
||||
|
||||
# The SDK version used to compile NewPipe
|
||||
- android-29
|
||||
|
||||
before_install:
|
||||
- yes | sdkmanager "platforms;android-29"
|
||||
script: ./gradlew -Dorg.gradle.jvmargs=-Xmx1536m assembleDebug lintDebug testDebugUnitTest
|
||||
|
||||
licenses:
|
||||
- '.+'
|
||||
140
README.es.md
Normal file
140
README.es.md
Normal file
@@ -0,0 +1,140 @@
|
||||
<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>
|
||||
|
||||
<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://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="http://webchat.freenode.net/?channels=%23newpipe" 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>
|
||||
</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>
|
||||
<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) .*
|
||||
|
||||
<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>COLOCAR NEWPIPE O CUALQUIER FORK (BIFURCACIÓN) REALIZADO DE ELLO EN GOOGLE PLAY STORE VIOLA SUS TÉRMINOS Y CONDICIONES.</b>
|
||||
|
||||
## 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)
|
||||
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_03.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_03.png)
|
||||
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_04.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_04.png)
|
||||
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_05.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_05.png)
|
||||
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_06.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_06.png)
|
||||
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_07.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_07.png)
|
||||
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_08.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_08.png)
|
||||
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_09.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_09.png)
|
||||
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_10.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_10.png)
|
||||
[<img src="fastlane/metadata/android/en-US/images/tenInchScreenshots/shot_11.png" width=405>](fastlane/metadata/android/en-US/images/tenInchScreenshots/shot_11.png)
|
||||
[<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.
|
||||
|
||||
### Características
|
||||
* Buscar videos
|
||||
* Mostrar información general sobre videos
|
||||
* Mirar videos de YouTube
|
||||
* Escuchar audio de YouTube
|
||||
* Modo popup (reproductor flotante)
|
||||
* Elegir reproductor para mirar el video
|
||||
* Descargar videos
|
||||
* Descargar solamente audio
|
||||
* Abrir video en Kodi
|
||||
* Mostrar videos próximos/relacionados
|
||||
* Buscar a través de YouTube en un idioma específico
|
||||
* Mirar/Bloquear materiales restringidas por edad.
|
||||
* Mostrar información general sobre canales
|
||||
* Buscar 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
|
||||
* Subtítulos
|
||||
* Apoyo de medios en directo
|
||||
* 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:
|
||||
|
||||
* YouTube
|
||||
* SoundCloud \[beta\]
|
||||
* media.ccc.de \[beta\]
|
||||
* PeerTube instances \[beta\]
|
||||
* Bandcamp \[beta\]
|
||||
|
||||
<!-- Brecha escondida para mantener compatibles los enlaces viejos. -->
|
||||
<span id="actualizaciones"></span>
|
||||
|
||||
## 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.schabi.org/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.
|
||||
|
||||
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.
|
||||
|
||||
## 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 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" />
|
||||
</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).
|
||||
|
||||
<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><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>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="https://www.bountysource.com/teams/newpipe"><img src="https://upload.wikimedia.org/wikipedia/commons/thumb/2/22/Bountysource.png/320px-Bountysource.png" alt="Bountysource" width="190px"></a></td>
|
||||
<td><a href="https://www.bountysource.com/teams/newpipe"><img src="assets/bountysource_qr_code.png" alt="Visit NewPipe at bountysource.com" width="100px"></a></td>
|
||||
<td><a href="https://www.bountysource.com/teams/newpipe/issues"><img src="https://img.shields.io/bountysource/team/newpipe/activity.svg?colorB=cd201f" height="30px" alt="Check out how many bounties you can earn."></a></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
## 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/).
|
||||
|
||||
## Licencia
|
||||
[](http://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.
|
||||
149
README.ja.md
Normal file
149
README.ja.md
Normal file
@@ -0,0 +1,149 @@
|
||||
<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">自由で軽量な Android 向けストリーミングフロントエンド</h4>
|
||||
|
||||
<p align="center"><a href="https://f-droid.org/packages/org.schabi.newpipe/"><img src="https://fdroid.gitlab.io/artwork/badge/get-it-on-ja.svg"></a></p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/TeamNewPipe/NewPipe/releases" alt="GitHub リリース"><img src="https://img.shields.io/github/release/TeamNewPipe/NewPipe.svg"></a>
|
||||
<a href="https://www.gnu.org/licenses/gpl-3.0" alt="ライセンス: GPLv3"><img src="https://img.shields.io/badge/License-GPL%20v3-blue.svg"></a>
|
||||
<a href="https://github.com/TeamNewPipe/NewPipe/actions" alt="ビルド状態"><img src="https://github.com/TeamNewPipe/NewPipe/workflows/CI/badge.svg?branch=dev&event=push"></a>
|
||||
<a href="https://hosted.weblate.org/engage/newpipe/" alt="翻訳状態"><img src="https://hosted.weblate.org/widgets/newpipe/-/svg-badge.svg"></a>
|
||||
<a href="http://webchat.freenode.net/?channels=%23newpipe" alt="IRC チャンネル: #newpipe"><img src="https://img.shields.io/badge/IRC%20chat-%23newpipe-brightgreen.svg"></a>
|
||||
<a href="https://www.bountysource.com/teams/newpipe" alt="Bountysource 寄付"><img src="https://img.shields.io/bountysource/team/newpipe/activity.svg?colorB=cd201f"></a>
|
||||
</p>
|
||||
<hr>
|
||||
<p align="center"><a href="#screenshots">スクリーンショット</a> • <a href="#description">説明</a> • <a href="#features">機能</a> • <a href="#installation-and-updates">インストールと更新</a> • <a href="#contribution">貢献</a> • <a href="#donate">寄付</a> • <a href="#license">ライセンス</a></p>
|
||||
<p align="center"><a href="https://newpipe.net">ウェブサイト</a> • <a href="https://newpipe.net/blog/">ブログ</a> • <a href="https://newpipe.net/FAQ/">FAQ</a> • <a href="https://newpipe.net/press/">ニュース</a></p>
|
||||
<hr>
|
||||
|
||||
*他の言語で読む: [English](README.md), [Español](README.es.md), [한국어](README.ko.md), [Soomaali](README.so.md), [Português Brasil](README.pt.br.md), [日本語](README.ja.md), [Română](README.ro.md) 。*
|
||||
|
||||
<b>注意: これはベータ版のため、バグが発生する可能性があります。もしバグが発生した場合、GitHub のリポジトリで Issue を開いてください。</b>
|
||||
|
||||
<b>NewPipe 及びいずれのフォークを Google Play ストアに公開すると、Google の取引条件の違反になります。</b>
|
||||
|
||||
<span id="screenshots"></span>
|
||||
## スクリーンショット
|
||||
|
||||
[<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)
|
||||
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_03.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_03.png)
|
||||
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_04.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_04.png)
|
||||
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_05.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_05.png)
|
||||
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_06.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_06.png)
|
||||
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_07.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_07.png)
|
||||
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_08.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_08.png)
|
||||
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_09.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_09.png)
|
||||
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_10.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_10.png)
|
||||
[<img src="fastlane/metadata/android/en-US/images/tenInchScreenshots/shot_11.png" width=405>](fastlane/metadata/android/en-US/images/tenInchScreenshots/shot_11.png)
|
||||
[<img src="fastlane/metadata/android/en-US/images/tenInchScreenshots/shot_12.png" width=405>](fastlane/metadata/android/en-US/images/tenInchScreenshots/shot_12.png)
|
||||
|
||||
<span id="description"></span>
|
||||
## 説明
|
||||
|
||||
自由なコピーレフトソフトウェアの NewPipe は一切の Google フレームワークライブラリ及び、YouTube API を使用しません。ウェブサイトは必要な情報のためだけに読み込まれるため、このアプリは Google のサービスがインストールされていない端末で使用ができます。また、NewPipe の使用に YouTube アカウントは必要となりません。
|
||||
|
||||
<span id="features"></span>
|
||||
### 機能
|
||||
|
||||
* 動画の検索
|
||||
* 動画の基本情報の表示
|
||||
* YouTube の動画の視聴
|
||||
* YouTube の動画のバックグラウンド再生
|
||||
* ポップアップモード (フローティングプレイヤー)
|
||||
* 動画を視聴するストリーミングプレイヤーの選択
|
||||
* 動画のダウンロード
|
||||
* 音声のみのダウンロード
|
||||
* Kodi での動画再生
|
||||
* 次の動画/関連動画の表示
|
||||
* 特定の言語の YouTube の検索
|
||||
* 年齢制限のあるコンテンツの視聴/ブロック
|
||||
* チャンネルの基本情報の表示
|
||||
* チャンネルの検索
|
||||
* チャンネルからの動画の視聴
|
||||
* Orbot/Tor 対応 (直接的なものは未実装)
|
||||
* 1080p/2K/4K 対応
|
||||
* 履歴の表示
|
||||
* チャンネルの登録
|
||||
* 履歴の検索
|
||||
* 再生リストの検索/視聴
|
||||
* 再生リストをキューに追加して再生
|
||||
* 動画のキューへの追加
|
||||
* 端末内の再生リスト
|
||||
* 字幕
|
||||
* ライブ配信の対応
|
||||
* コメントの表示
|
||||
|
||||
### 対応しているサービス
|
||||
|
||||
NewPipe は複数のサービスに対応しています。[ドキュメント](https://teamnewpipe.github.io/documentation/)は、どのようにしてアプリと NewPipe Extractor にサービスを追加できるかについて詳細な情報を提供しています。もし、新しいサービスを追加するならば、是非私たちに連絡をお願いします。現在対応しているサービスは:
|
||||
|
||||
* YouTube
|
||||
* SoundCloud \[ベータ\]
|
||||
* media.ccc.de \[ベータ\]
|
||||
* PeerTube インスタンス \[ベータ\]
|
||||
* Bandcamp \[ベータ\]
|
||||
|
||||
<!-- Hidden span to keep old links compatible. -->
|
||||
<span id="updates"></span>
|
||||
|
||||
<span id="installation-and-updates"></span>
|
||||
## インストールと更新
|
||||
以下の方法のいずれかに従うことによって NewPipe をインストールできます。
|
||||
1. カスタムリポジトリを F-Droid に追加してリリースが公開され次第インストールする。この方法の説明はこちら: https://newpipe.net/FAQ/tutorials/install-add-fdroid-repo/
|
||||
2. リリースが公開され次第[GitHub のリリース](https://github.com/TeamNewPipe/NewPipe/releases)から APK をダウンロードしてインストールする。
|
||||
3. F-Droid から更新する。これは更新を手にする上で最も遅い方法です。F-Droid が変更を検知して、APK をビルドし、署名、そしてユーザーに更新を届ける必要があるためです。
|
||||
4. 自分でデバッグ APK をビルドする。これは新しい機能を使用する上で最も早い方法ですが、他と比べてとても複雑なので、他の方法の使用を推奨します。
|
||||
|
||||
私たちはほとんどのユーザーに方法1を推奨します。方法1と2でインストールされた APK は互換性がありますが、方法3でインストールされたものにはありません。これは方法1と2では、同じ署名鍵 (私たちが使用するもの)が使用されますが、方法3では異なった署名鍵 (F-Droidが使用するもの)が使用されるためです。方法4を使ったデバッグ APK のビルドは根本的に署名鍵の問題を除きます。署名鍵はユーザーが騙されて悪意のある更新がアプリにインストールされないことを助けるためにあります。
|
||||
|
||||
もし、何かしらの理由によりソースを切り替えたい場合 (例: NewPipe のコア機能が壊れてしまったが F-Droid はまだ更新をしていない) は、この手順を推奨します。
|
||||
1. 履歴や登録チャンネル、再生リストを保つために 設定 > コンテンツ > データベースをエクスポート からデータをバックアップ
|
||||
2. NewPipe をアンインストール
|
||||
3. 新しいソースから APK をダウンロードしてインストール
|
||||
4. 設定 > コンテンツ > データベースをインポート からステップ1で作ったデータベースをインポート
|
||||
|
||||
<span id="contribution"></span>
|
||||
## 貢献
|
||||
翻訳、デザインの変更、コードの整理、大規模なコードの変更などの助けはいつでも歓迎します。
|
||||
より良いものを一緒に作り上げましょう!
|
||||
|
||||
もし貢献をしたい場合、[貢献ノート](.github/CONTRIBUTING.md)をご確認ください。
|
||||
|
||||
<a href="https://hosted.weblate.org/engage/newpipe/">
|
||||
<img src="https://hosted.weblate.org/widgets/newpipe/-/287x66-grey.png" alt="翻訳状態" />
|
||||
</a>
|
||||
|
||||
<span id="donate"></span>
|
||||
## 寄付
|
||||
もし、NewPipe を気に入っていただけたら、寄付をしていただけると嬉しいです。Bitcoin または Bountysource, Liberapay から寄付をすることができます。NewPipe への寄付については、[ウェブサイト](https://newpipe.net/donate)からお願いします。
|
||||
|
||||
<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 コード" 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="liberapay.com で NewPipe を訪れる" width="100px"></a></td>
|
||||
<td><a href="https://liberapay.com/TeamNewPipe/donate"><img src="assets/liberapay_donate_button.svg" alt="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="bountysource.com で NewPipe を訪れる" 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="あなたがどれほどの寄付を得られるのか確認しましょう。"></a></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
## プライバシーポリシー
|
||||
NewPipe プロジェクトはメディアウェブサービスを使用する上でのプライベートで匿名の体験を提供することを目的としています。
|
||||
そのため、アプリはあなたの同意なしで一切のデータを収集しません。NewPipe のプライバシーポリシーはあなたがクラッシュレポートまたは、私たちのブログでコメントを送信した場合にどのようなデータが送信され、保存されるのかを詳細に説明しています。そのドキュメントは[こちら](https://newpipe.net/legal/privacy/)から見つけることができます。
|
||||
|
||||
<span id="license"></span>
|
||||
## ライセンス
|
||||
[](http://www.gnu.org/licenses/gpl-3.0.en.html)
|
||||
|
||||
NewPipe はフリーソフトウェアなので、あなたはあなたの望むように使用、習得、共有、改善を行えます。
|
||||
具体的には、フリーソフトウェア財団により公開された [GNU General Public License](https://www.gnu.org/licenses/gpl.html) のバージョン3のライセンスもしくは、(あなたの選択で) いずれかの後継バージョンの規約の元で配布または改変を行うことができます。
|
||||
20
README.ko.md
20
README.ko.md
@@ -1,22 +1,23 @@
|
||||
<p align="center"><a href="https://newpipe.schabi.org"><img src="assets/new_pipe_icon_5.png" width="150"></a></p>
|
||||
<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">A libre lightweight streaming frontend for 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-ko.svg"></a></p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/TeamNewPipe/NewPipe/releases" alt="GitHub release"><img src="https://img.shields.io/github/release/TeamNewPipe/NewPipe.svg" ></a>
|
||||
<a href="https://www.gnu.org/licenses/gpl-3.0" alt="License: GPLv3"><img src="https://img.shields.io/badge/License-GPL%20v3-blue.svg"></a>
|
||||
<a href="https://travis-ci.org/TeamNewPipe/NewPipe" alt="Build Status"><img src="https://travis-ci.org/TeamNewPipe/NewPipe.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/" alt="Translation Status"><img src="https://hosted.weblate.org/widgets/newpipe/-/svg-badge.svg"></a>
|
||||
<a href="http://webchat.freenode.net/?channels=%23newpipe" alt="IRC channel: #newpipe"><img src="https://img.shields.io/badge/IRC%20chat-%23newpipe-brightgreen.svg"></a>
|
||||
<a href="https://www.bountysource.com/teams/newpipe" alt="Bountysource bounties"><img src="https://img.shields.io/bountysource/team/newpipe/activity.svg?colorB=cd201f"></a>
|
||||
</p>
|
||||
<hr>
|
||||
<p align="center"><a href="#screenshots">Screenshots</a> • <a href="#description">Description</a> • <a href="#features">Features</a> • <a href="#updates">Updates</a> • <a href="#contribution">Contribution</a> • <a href="#donate">Donate</a> • <a href="#license">License</a></p>
|
||||
<p align="center"><a href="https://newpipe.schabi.org">Website</a> • <a href="https://newpipe.schabi.org/blog/">Blog</a> • <a href="https://newpipe.schabi.org/FAQ/">FAQ</a> • <a href="https://newpipe.schabi.org/press/">Press</a></p>
|
||||
<p align="center"><a href="https://newpipe.net">Website</a> • <a href="https://newpipe.net/blog/">Blog</a> • <a href="https://newpipe.net/FAQ/">FAQ</a> • <a href="https://newpipe.net/press/">Press</a></p>
|
||||
<hr>
|
||||
|
||||
*Read this in other languages: [English](README.md), [한국어](README.ko.md).*
|
||||
*Read this in other languages: [English](README.md), [Español](README.es.md), [한국어](README.ko.md), [Soomaali](README.so.md), [Português Brasil](README.pt_BR.md), [日本語](README.ja.md), [Română](README.ro.md).*
|
||||
|
||||
<b>경고: 이 버전은 베타 버전이므로, 버그가 발생할 수도 있습니다. 만약 버그가 발생하였다면, 우리의 GITHUB 저장소에서 ISSUE를 열람하여 주십시오.</b>
|
||||
|
||||
@@ -79,6 +80,7 @@ NewPipe는 여러가지 서비스를 지원합니다. 우리의 [문서](https:/
|
||||
* SoundCloud \[beta\]
|
||||
* media.ccc.de \[beta\]
|
||||
* PeerTube instances \[beta\]
|
||||
* Bandcamp \[beta\]
|
||||
|
||||
## Updates
|
||||
NewPipe 코드의 변경이 있을 때(기능 추가 또는 버그 수정으로 인해), 결국 릴리즈가 발생할 것입니다. 이것들의 형식은 x.xx.x 입니다.
|
||||
@@ -86,7 +88,7 @@ NewPipe 코드의 변경이 있을 때(기능 추가 또는 버그 수정으로
|
||||
1. 직접 디버그 APK를 생성할 수 있습니다. 이 방법은 당신의 기기에서 새로운 기능을 얻을 수 있는 가장 빠른 방법이지만, 꽤 많이 복잡합니다.
|
||||
따라서 우리는 다른 방법들 중 하나를 사용하는 것을 추천합니다.
|
||||
2. 우리의 커스텀 저장소를 F-Droid에 추가하고 우리가 릴리즈를 게시하는 대로 저곳에서 릴리즈를 설치할 수 있습니다.
|
||||
이에 대한 설명서는 이곳에서 확인할 수 있습니다: https://newpipe.schabi.org/FAQ/tutorials/install-add-fdroid-repo/
|
||||
이에 대한 설명서는 이곳에서 확인할 수 있습니다: https://newpipe.net/FAQ/tutorials/install-add-fdroid-repo/
|
||||
3. 우리가 릴리즈를 게시하는 대로 [Github Releases](https://github.com/TeamNewPipe/NewPipe/releases)에서 APK를 다운받고 이것을 설치할 수 있습니다.
|
||||
4. F-Droid를 통해 업데이트 할 수 있습니다. F-Droid는 변화를 인식하고, 스스로 APK를 생성하고, 이것에 서명하고, 사용자들에서 업데이트를 전달해야만 하기 때문에,
|
||||
이것은 업데이트를 받는 가장 느린 방법입니다.
|
||||
@@ -98,7 +100,7 @@ NewPipe 코드의 변경이 있을 때(기능 추가 또는 버그 수정으로
|
||||
1. 당신의 기록, 구독, 그리고 재생목록을 유지할 수 있도록 Settings > Content > Export Database 를 통해 데이터를 백업하십시오.
|
||||
2. NewPipe를 삭제하십시오.
|
||||
3. 새로운 소스에서 APK를 다운로드하고 이것을 설치하십시오.
|
||||
4. Step 1의 Settings > Content > Export Database 을 통해 데이터를 불러오십시오.
|
||||
4. Step 1의 Settings > Content > Import Database 을 통해 데이터를 불러오십시오.
|
||||
|
||||
## Contribution
|
||||
당신이 아이디어, 번역, 디자인 변경, 코드 정리, 또는 정말 큰 코드 수정에 대한 의견이 있다면, 도움은 항상 환영합니다.
|
||||
@@ -111,7 +113,7 @@ NewPipe 코드의 변경이 있을 때(기능 추가 또는 버그 수정으로
|
||||
</a>
|
||||
|
||||
## Donate
|
||||
만약 NewPipe가 마음에 들었다면, 우리는 기부에 대해 기꺼이 환영합니다. bitcoin을 보내거나, Bountysource 또는 Liberapay를 통해 기부할 수 있습니다. NewPipe에 기부하는 것에 대한 자세한 정보를 원한다면, 우리의 [웹사이트](https://newpipe.schabi.org/donate)를 방문하여 주십시오.
|
||||
만약 NewPipe가 마음에 들었다면, 우리는 기부에 대해 기꺼이 환영합니다. bitcoin을 보내거나, Bountysource 또는 Liberapay를 통해 기부할 수 있습니다. NewPipe에 기부하는 것에 대한 자세한 정보를 원한다면, 우리의 [웹사이트](https://newpipe.net/donate)를 방문하여 주십시오.
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
@@ -134,7 +136,7 @@ NewPipe 코드의 변경이 있을 때(기능 추가 또는 버그 수정으로
|
||||
## Privacy Policy
|
||||
|
||||
NewPipe 프로젝트는 미디어 웹 서비스를 사용하는 것에 대한 사적의, 익명의 경험을 제공하는 것을 목표로 하고 있습니다.
|
||||
그러므로, 앱은 당신의 동의 없이 어떤 데이터도 수집하지 않습니다. NewPipe의 개인정보보호정책은 당신이 충돌 리포트를 보내거나, 또는 우리의 블로그에 글을 남길 때 어떤 데이터가 보내지고 저장되는지에 대해 상세히 설명합니다. 이 문서는 [여기](https://newpipe.schabi.org/legal/privacy/)에서 확인할 수 있습니다.
|
||||
그러므로, 앱은 당신의 동의 없이 어떤 데이터도 수집하지 않습니다. NewPipe의 개인정보보호정책은 당신이 충돌 리포트를 보내거나, 또는 우리의 블로그에 글을 남길 때 어떤 데이터가 보내지고 저장되는지에 대해 상세히 설명합니다. 이 문서는 [여기](https://newpipe.net/legal/privacy/)에서 확인할 수 있습니다.
|
||||
|
||||
## License
|
||||
[](http://www.gnu.org/licenses/gpl-3.0.en.html)
|
||||
|
||||
35
README.md
35
README.md
@@ -1,22 +1,23 @@
|
||||
<p align="center"><a href="https://newpipe.schabi.org"><img src="assets/new_pipe_icon_5.png" width="150"></a></p>
|
||||
<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">A libre lightweight streaming frontend for 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://f-droid.org/wiki/images/0/06/F-Droid-button_get-it-on.png"></a></p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/TeamNewPipe/NewPipe/releases" alt="GitHub release"><img src="https://img.shields.io/github/release/TeamNewPipe/NewPipe.svg" ></a>
|
||||
<a href="https://www.gnu.org/licenses/gpl-3.0" alt="License: GPLv3"><img src="https://img.shields.io/badge/License-GPL%20v3-blue.svg"></a>
|
||||
<a href="https://travis-ci.org/TeamNewPipe/NewPipe" alt="Build Status"><img src="https://travis-ci.org/TeamNewPipe/NewPipe.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/" alt="Translation Status"><img src="https://hosted.weblate.org/widgets/newpipe/-/svg-badge.svg"></a>
|
||||
<a href="http://webchat.freenode.net/?channels=%23newpipe" alt="IRC channel: #newpipe"><img src="https://img.shields.io/badge/IRC%20chat-%23newpipe-brightgreen.svg"></a>
|
||||
<a href="https://www.bountysource.com/teams/newpipe" alt="Bountysource bounties"><img src="https://img.shields.io/bountysource/team/newpipe/activity.svg?colorB=cd201f"></a>
|
||||
</p>
|
||||
<hr>
|
||||
<p align="center"><a href="#screenshots">Screenshots</a> • <a href="#description">Description</a> • <a href="#features">Features</a> • <a href="#updates">Updates</a> • <a href="#contribution">Contribution</a> • <a href="#donate">Donate</a> • <a href="#license">License</a></p>
|
||||
<p align="center"><a href="https://newpipe.schabi.org">Website</a> • <a href="https://newpipe.schabi.org/blog/">Blog</a> • <a href="https://newpipe.schabi.org/FAQ/">FAQ</a> • <a href="https://newpipe.schabi.org/press/">Press</a></p>
|
||||
<p align="center"><a href="#screenshots">Screenshots</a> • <a href="#description">Description</a> • <a href="#features">Features</a> • <a href="#installation-and-updates">Installation and updates</a> • <a href="#contribution">Contribution</a> • <a href="#donate">Donate</a> • <a href="#license">License</a></p>
|
||||
<p align="center"><a href="https://newpipe.net">Website</a> • <a href="https://newpipe.net/blog/">Blog</a> • <a href="https://newpipe.net/FAQ/">FAQ</a> • <a href="https://newpipe.net/press/">Press</a></p>
|
||||
<hr>
|
||||
|
||||
*Read this in other languages: [English](README.md), [한국어](README.ko.md).*
|
||||
*Read this in other languages: [English](README.md), [Español](README.es.md), [한국어](README.ko.md), [Soomaali](README.so.md), [Português Brasil](README.pt_BR.md), [日本語](README.ja.md), [Română](README.ro.md) .*
|
||||
|
||||
<b>WARNING: THIS IS A BETA VERSION, THEREFORE YOU MAY ENCOUNTER BUGS. IF YOU DO, OPEN AN ISSUE VIA OUR GITHUB REPOSITORY.</b>
|
||||
|
||||
@@ -79,15 +80,19 @@ NewPipe supports multiple services. Our [docs](https://teamnewpipe.github.io/doc
|
||||
* SoundCloud \[beta\]
|
||||
* media.ccc.de \[beta\]
|
||||
* PeerTube instances \[beta\]
|
||||
* Bandcamp \[beta\]
|
||||
|
||||
## Updates
|
||||
When a change to the NewPipe code occurs (due to either adding features or bug fixing), eventually a release will occur. These are in the format x.xx.x . In order to get this new version, you can:
|
||||
1. 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.
|
||||
2. Add our custom repo to F-Droid and install it from there as soon as we publish a release. The instructions are here: https://newpipe.schabi.org/FAQ/tutorials/install-add-fdroid-repo/
|
||||
3. Download the APK from [Github Releases](https://github.com/TeamNewPipe/NewPipe/releases) and install it as soon as we publish a release.
|
||||
4. 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.
|
||||
<!-- Hidden span to keep old links compatible. -->
|
||||
<span id="updates"></span>
|
||||
|
||||
We recommend method 2 for most users. APKs installed using method 2 or 3 are compatible with each other, but not with those installed using method 4. This is due to the same signing key (ours) being used for 2 and 3, but a different signing key (F-Droid's) being used for 4. Building a debug APK using method 1 excludes a key entirely. Signing keys help ensure that a user isn't tricked into installing a malicious update to an app.
|
||||
## 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.schabi.org/FAQ/tutorials/install-add-fdroid-repo/
|
||||
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.
|
||||
|
||||
We recommend method 1 for most users. APKs installed using method 1 or 2 are compatible with each other, but not with those installed using method 3. This is due to the same signing key (ours) being used for 1 and 2, but a different signing key (F-Droid's) being used for 3. Building a debug APK using method 4 excludes a key entirely. Signing keys help ensure that a user isn't tricked into installing a malicious update to an app.
|
||||
|
||||
In the meanwhile, if you want to switch sources for some reason (e.g. NewPipe's core functionality was broken and F-Droid doesn't have the update yet), we recommend following this procedure:
|
||||
1. Back up your data via Settings > Content > Export Database so you keep your history, subscriptions, and playlists
|
||||
@@ -106,7 +111,7 @@ If you'd like to get involved, check our [contribution notes](.github/CONTRIBUTI
|
||||
</a>
|
||||
|
||||
## Donate
|
||||
If you like NewPipe we'd be happy about a donation. You can either send bitcoin or donate via Bountysource or Liberapay. For further info on donating to NewPipe, please visit our [website](https://newpipe.schabi.org/donate).
|
||||
If you like NewPipe we'd be happy about a donation. You can either send bitcoin or donate via Bountysource or Liberapay. For further info on donating to NewPipe, please visit our [website](https://newpipe.net/donate).
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
@@ -129,7 +134,7 @@ If you like NewPipe we'd be happy about a donation. You can either send bitcoin
|
||||
## Privacy Policy
|
||||
|
||||
The NewPipe project aims to provide a private, anonymous experience for using media web services.
|
||||
Therefore, the app does not collect any data without your consent. NewPipe's privacy policy explains in detail what data is sent and stored when you send a crash report, or comment in our blog. You can find the document [here](https://newpipe.schabi.org/legal/privacy/).
|
||||
Therefore, the app does not collect any data without your consent. NewPipe's privacy policy explains in detail what data is sent and stored when you send a crash report, or comment in our blog. You can find the document [here](https://newpipe.net/legal/privacy/).
|
||||
|
||||
## License
|
||||
[](http://www.gnu.org/licenses/gpl-3.0.en.html)
|
||||
|
||||
143
README.pt_BR.md
Normal file
143
README.pt_BR.md
Normal file
@@ -0,0 +1,143 @@
|
||||
<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">Uma interface de streaming leve e gratuita para Android.</h4>
|
||||
|
||||
<p align="center"><a href="https://f-droid.org/packages/org.schabi.newpipe/"><img src="https://fdroid.gitlab.io/artwork/badge/get-it-on-pt-br.svg"></a></p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/TeamNewPipe/NewPipe/releases" alt="GitHub release"><img src="https://img.shields.io/github/release/TeamNewPipe/NewPipe.svg" ></a>
|
||||
<a href="https://www.gnu.org/licenses/gpl-3.0" alt="License: GPLv3"><img src="https://img.shields.io/badge/License-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/" alt="Translation Status"><img src="https://hosted.weblate.org/widgets/newpipe/-/svg-badge.svg"></a>
|
||||
<a href="http://webchat.freenode.net/?channels=%23newpipe" alt="IRC channel: #newpipe"><img src="https://img.shields.io/badge/IRC%20chat-%23newpipe-brightgreen.svg"></a>
|
||||
<a href="https://www.bountysource.com/teams/newpipe" alt="Bountysource bounties"><img src="https://img.shields.io/bountysource/team/newpipe/activity.svg?colorB=cd201f"></a>
|
||||
</p>
|
||||
<hr>
|
||||
<p align="center"><a href="#screenshots">Screenshots</a> • <a href="#description">Descrição</a> • <a href="#features">Características</a> • <a href="#updates">Atualizações</a> • <a href="#contribution">Contribuição</a> • <a href="#donate">Doar</a> • <a href="#license">Licença</a></p>
|
||||
<p align="center"><a href="https://newpipe.net">Site</a> • <a href="https://newpipe.net/blog/">Blog</a> • <a href="https://newpipe.net/FAQ/">FAQ</a> • <a href="https://newpipe.net/press/">Press</a></p>
|
||||
<hr>
|
||||
|
||||
*Read this in other languages: [English](README.md), [Español](README.es.md), [한국어](README.ko.md), [Soomaali](README.so.md), [Português Brasil](README.pt_BR.md), [日本語](README.ja.md), [Română](README.ro.md).*
|
||||
|
||||
<b>AVISO: ESTA É UMA VERSÃO BETA, PORTANTO, VOCÊ PODE ENCONTRAR BUGS. ENCONTROU ALGUM, ABRA UM ISSUE ATRAVÉS DO NOSSO REPOSITÓRIO GITHUB.</b>
|
||||
|
||||
<b>COLOCAR NEWPIPE OU QUALQUER FORK DELE NA GOOGLE PLAY STORE VIOLA SEUS TERMOS E CONDIÇÕES.</b>
|
||||
|
||||
## Screenshots
|
||||
|
||||
[<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)
|
||||
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_03.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_03.png)
|
||||
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_04.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_04.png)
|
||||
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_05.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_05.png)
|
||||
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_06.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_06.png)
|
||||
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_07.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_07.png)
|
||||
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_08.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_08.png)
|
||||
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_09.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_09.png)
|
||||
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_10.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_10.png)
|
||||
[<img src="fastlane/metadata/android/en-US/images/tenInchScreenshots/shot_11.png" width=405>](fastlane/metadata/android/en-US/images/tenInchScreenshots/shot_11.png)
|
||||
[<img src="fastlane/metadata/android/en-US/images/tenInchScreenshots/shot_12.png" width=405>](fastlane/metadata/android/en-US/images/tenInchScreenshots/shot_12.png)
|
||||
|
||||
## Descrição
|
||||
|
||||
O NewPipe não usa nenhuma biblioteca de framework do Google, nem a API do YouTube. Os sites são apenas analisados para obter informações necessárias, para que este aplicativo possa ser usado em dispositivos sem os serviços do Google instalados. Além disso, você não precisa de uma conta no YouTube para usar o NewPipe, que é um software livre com copyleft.
|
||||
|
||||
### Características
|
||||
|
||||
* Procurar vídeos
|
||||
* Exibir informações gerais sobre vídeos
|
||||
* Assista aos vídeos do YouTube
|
||||
* Ouça vídeos do YouTube
|
||||
* Modo popup (player flutuante)
|
||||
* Selecione o player para assistir streaming
|
||||
* Baixar vídeos
|
||||
* Baixar somente áudio
|
||||
* Abrir vídeo no Kodi
|
||||
* Mostrar vídeos próximos/relacionados
|
||||
* Pesquise no YouTube em um idioma específico
|
||||
* Assistir/Bloquear material restrito
|
||||
* Exibir informações gerais sobre canais
|
||||
* Pesquisar canais
|
||||
* Assista a vídeos de um canal
|
||||
* Suporte Orbot/Tor (ainda não diretamente)
|
||||
* Suporte 1080p/2K/4K
|
||||
* Ver histórico
|
||||
* Inscreva-se nos canais
|
||||
* Procurar histórico
|
||||
* Porcurar/Assistir playlists
|
||||
* Assistir playlists em fila
|
||||
* Vídeos em fila
|
||||
* Playlists Local
|
||||
* Legenda
|
||||
* Suporte a live
|
||||
* Mostrar comentários
|
||||
|
||||
### Serviços Suportados
|
||||
|
||||
O NewPipe suporta vários serviços. Nosso [documentação](https://teamnewpipe.github.io/documentation/) fornecer mais informações sobre como um novo serviço pode ser adicionado ao aplicativo e ao extrator. Por favor, entre em contato conosco se você pretende adicionar um novo. Atualmente, os serviços suportados são:
|
||||
|
||||
* YouTube
|
||||
* SoundCloud \[beta\]
|
||||
* media.ccc.de \[beta\]
|
||||
* PeerTube instances \[beta\]
|
||||
* Bandcamp \[beta\]
|
||||
|
||||
## Atualizações
|
||||
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.
|
||||
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.
|
||||
|
||||
Enquanto isso, se você quiser trocar de fontes por algum motivo (por exemplo, a funcionalidade principal do NewPipe foi quebrada e o F-Droid ainda não tem a atualização), recomendamos seguir este procedimento:
|
||||
1. Back up your data via Settings > Content > Export Database so you keep your history, subscriptions, and playlistsFaça backup de seus dados através de Configurações > Conteúdo > Exportar Base de Dados para que você mantenha seu histórico, inscrições e playlists
|
||||
2. Desinstale o NewPipe
|
||||
3. Baixe o APK da nova fonte e instale-o
|
||||
4. Importe os dados da etapa 1 via Configurações > Conteúdo > Inportar Banco de Dados
|
||||
|
||||
## Contribuição
|
||||
Se você tem ideias, traduções, alterações de design, limpeza de códigos ou mudanças reais de código, a ajuda é sempre bem-vinda.
|
||||
Quanto mais for feito, melhor fica!
|
||||
|
||||
Se você quiser se envolver, verifique nossa [notas de contribuição](.github/CONTRIBUTING.md).
|
||||
|
||||
<a href="https://hosted.weblate.org/engage/newpipe/">
|
||||
<img src="https://hosted.weblate.org/widgets/newpipe/-/287x66-grey.png" alt="Translation status" />
|
||||
</a>
|
||||
|
||||
## Doar
|
||||
Se você gosta de NewPipe, ficaríamos felizes com uma doação. Você pode enviar bitcoin ou doar via Bountysource ou Liberapay. Para obter mais informações sobre como doar para a NewPipe, visite nosso [site](https://newpipe.net/donate).
|
||||
|
||||
<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><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>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="https://www.bountysource.com/teams/newpipe"><img src="https://upload.wikimedia.org/wikipedia/commons/thumb/2/22/Bountysource.png/320px-Bountysource.png" alt="Bountysource" width="190px"></a></td>
|
||||
<td><a href="https://www.bountysource.com/teams/newpipe"><img src="assets/bountysource_qr_code.png" alt="Visit NewPipe at bountysource.com" width="100px"></a></td>
|
||||
<td><a href="https://www.bountysource.com/teams/newpipe/issues"><img src="https://img.shields.io/bountysource/team/newpipe/activity.svg?colorB=cd201f" height="30px" alt="Check out how many bounties you can earn."></a></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
## Política de Privacidade
|
||||
|
||||
O projeto NewPipe tem como objetivo proporcionar uma experiência privada e anônima para o uso de serviços web de mídia.
|
||||
Portanto, o aplicativo não coleta nenhum dado sem o seu consentimento. A política de privacidade da NewPipe explica em detalhes quais dados são enviados e armazenados quando você envia um relatório de erro ou comenta em nosso blog. Você pode encontrar o documento [aqui](https://newpipe.net/legal/privacy/).
|
||||
|
||||
## Licença
|
||||
[](http://www.gnu.org/licenses/gpl-3.0.en.html)
|
||||
|
||||
NewPipe é Software Livre: Você pode usar, estudar compartilhamento e melhorá-lo à sua vontade.
|
||||
Especificamente, você pode redistribuir e/ou modificá-lo sob os termos do
|
||||
[GNU General Public License](https://www.gnu.org/licenses/gpl.html) publicado pela Free Software Foundation, seja a versão 3 da Licença, ou
|
||||
(a sua opção) qualquer versão posterior.
|
||||
144
README.ro.md
Normal file
144
README.ro.md
Normal file
@@ -0,0 +1,144 @@
|
||||
<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">Un front-end de streaming „uşor” liber, pentru Android.</h4>
|
||||
|
||||
<p align="center"><a href="https://f-droid.org/packages/org.schabi.newpipe/"><img src="https://fdroid.gitlab.io/artwork/badge/get-it-on-ro.svg"></a></p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/TeamNewPipe/NewPipe/releases" alt="GitHub release"><img src="https://img.shields.io/github/release/TeamNewPipe/NewPipe.svg" ></a>
|
||||
<a href="https://www.gnu.org/licenses/gpl-3.0" alt="License: GPLv3"><img src="https://img.shields.io/badge/License-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/" alt="Translation Status"><img src="https://hosted.weblate.org/widgets/newpipe/-/svg-badge.svg"></a>
|
||||
<a href="http://webchat.freenode.net/?channels=%23newpipe" alt="IRC channel: #newpipe"><img src="https://img.shields.io/badge/IRC%20chat-%23newpipe-brightgreen.svg"></a>
|
||||
<a href="https://www.bountysource.com/teams/newpipe" alt="Bountysource bounties"><img src="https://img.shields.io/bountysource/team/newpipe/activity.svg?colorB=cd201f"></a>
|
||||
</p>
|
||||
<hr>
|
||||
<p align="center"><a href="#screenshots">Capturi de ecran</a> • <a href="#description">Descriere</a> • <a href="#features">Funcţii</a> • <a href="#installation-and-updates">Instalare şi actualizări</a> • <a href="#contribution">Contribuţie</a> • <a href="#donate">Donaţi</a> • <a href="#license">Licenţă</a></p>
|
||||
<p align="center"><a href="https://newpipe.net">Website</a> • <a href="https://newpipe.net/blog/">Blog</a> • <a href="https://newpipe.net/FAQ/">FAQ</a> • <a href="https://newpipe.net/press/">Presă</a></p>
|
||||
<hr>
|
||||
|
||||
*Citiţi în alte limbi: [English](README.md), [Español](README.es.md), [한국어](README.ko.md), [Soomaali](README.so.md), [Português Brasil](README.pt_BR.md), [日本語](README.ja.md), [Română](README.ro.md)*
|
||||
|
||||
<b>Atenţionare: ACEASTA ESTE O VERSIUNE BETA, AŞA CĂ S-AR PUTE SĂ ÎNTÂLNIŢI ERORI. DACĂ SE ÎNTÂMPLĂ ACEST LUCRU, DESCHIDEŢI UN ISSUE PRIN REPSITORY-UL NOSTRU GITHUB.</b>
|
||||
|
||||
<b>PUNERA NEWPIPE SAU ORICĂRUI FORK AL ACESTUIA ÎN MAGAZINUL GOOGLE PLAY LE ÎNCALCĂ TERMENII ŞI CONDIŢIILE.</b>
|
||||
|
||||
## Capturi de ecran
|
||||
|
||||
[<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)
|
||||
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_03.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_03.png)
|
||||
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_04.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_04.png)
|
||||
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_05.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_05.png)
|
||||
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_06.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_06.png)
|
||||
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_07.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_07.png)
|
||||
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_08.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_08.png)
|
||||
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_09.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_09.png)
|
||||
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_10.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_10.png)
|
||||
[<img src="fastlane/metadata/android/en-US/images/tenInchScreenshots/shot_11.png" width=405>](fastlane/metadata/android/en-US/images/tenInchScreenshots/shot_11.png)
|
||||
[<img src="fastlane/metadata/android/en-US/images/tenInchScreenshots/shot_12.png" width=405>](fastlane/metadata/android/en-US/images/tenInchScreenshots/shot_12.png)
|
||||
|
||||
## Descriere
|
||||
|
||||
NewPipe nu foloseşte nici-o bibliotecă Google framework sau API-ul Youtube. Website-urile sunt doar analizate pentru a prelua informaţia necesară, aşa că această aplicaţie poate fi folosită pe telefoane fără Serviciile Google instalate. De asemenea, nu aveţi nevoie de un cont Youtube pentru a folosi Newpipe, care este sofware liber şi copylefted.
|
||||
|
||||
### Funcţii
|
||||
|
||||
* Căutarea videoclipurilor
|
||||
* Afişarea informaţiilor generale despre videoclipuri
|
||||
* Urmărirea videoclipurilor Youtube
|
||||
* Ascultarea videoclipurilor Youtube
|
||||
* Modul popup (player plutitor)
|
||||
* Selectarea playerului de streaming pentru vizionarea videoclipului
|
||||
* Descărcarea videoclipurilor
|
||||
* Doar descărcarea sunetului
|
||||
* Deschiderea videoclipurilor cu Kodi
|
||||
* Expunerea videoclipurilor următoare/asociate
|
||||
* Căutarea YouTube într-o limbă specifică
|
||||
* Vizionarea/Blocarea materialului restricţionat în funcţie de vârstă
|
||||
* Afişarea informaţiilor generale despre canale
|
||||
* Căutarea canalelor
|
||||
* Vizionarea videoclipurilor dintr-un canal
|
||||
* Suport Orbot/Tor (încă nu direct)
|
||||
* Suport 1080p/2K/4K
|
||||
* Vizionarea istoricului
|
||||
* Abonarea la canale
|
||||
* Căutarea în istoric
|
||||
* Căutarea/vizionarea playlisturilor
|
||||
* Vizionarea ca playlisturi puse în coadă
|
||||
* Punerea în coadă a videoclipurilor
|
||||
* Playlisturi locale
|
||||
* Subtitrări
|
||||
* Suport al transmiterilor live
|
||||
* Afişarea comentariilor
|
||||
|
||||
### Servicii întreţinute
|
||||
|
||||
NewPipe suportă servicii multiple. [Documentele](https://teamnewpipe.github.io/documentation/) noastre furnizează mai multe informaţii în legătură cu modalităţile prin care un nou serviciu poate fi adăugat aplicaţiei şi extractorului. Vă rugăm să ne contactaţi dacă doriţi să adăugaţi unul nou. Serviciile întreţinute acum sunt:
|
||||
|
||||
* YouTube
|
||||
* SoundCloud \[beta\]
|
||||
* media.ccc.de \[beta\]
|
||||
* Instanţe PeerTube \[beta\]
|
||||
* Bandcamp \[beta\]
|
||||
|
||||
<!-- Hidden span to keep old links compatible. -->
|
||||
<span id="updates"></span>
|
||||
|
||||
## 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.schabi.org/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.
|
||||
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. (**IMPORTANT**: în momentul scrierii, o problemă împiedică versiunile mai noi de 0.20.1 să fie publicate. Aşa că, dacă doriţi să folosiţi F-droid, până această problemă este rezolvată, vă recomandăm metoda 1.)
|
||||
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.
|
||||
|
||||
Recomandăm metoda 1 pentru majoritatea utilizatorilor. APK-urile din metodele 1 şi 2 suntcompatibile una cu cealaltă, dar nu cu cele din metoda 3. Acest lucru se datorează faptului că aceeași cheie de semnare (a noastră) este utilizată pentru 1 și 2, dar o altă cheie de semnare (F-Droid) este utilizată pentru 3. Construirea unui APK de depanare folosind metoda 4 exclude o cheie în întregime. Cheile de semnare vă asigură că un utilizator nu este păcălit să instaleze o actualizare rău intenționată a unei aplicații.
|
||||
|
||||
Între timp, dacă doriți să schimbați sursa dintr-un anumit motiv (de exemplu, funcționalitatea de bază a NewPipe a fost întreruptă și F-Droid nu are încă actualizarea), vă recomandăm să folosiţi următoarea procedură:
|
||||
1. Faceți o copie de rezervă a datelor prin Setări> Conținut> Exportați baza de date, astfel încât să vă păstrați istoricul, abonamentele și playlisturile
|
||||
2. Dezinstalaţi NewPipe
|
||||
3. Descărcaţi APK-ul din noua sursă şi instalaţi-l
|
||||
4. Importați datele de la pasul 1 prin Setări> Conținut> Importare bază de date
|
||||
|
||||
## Contribuţie
|
||||
Dacă aveţi idei, traduceri, schimbări de design, curaţarea codului, sau schimbări majore ale codului, ajutorul este întotdeauna binevenit.
|
||||
Cu cât se face mai mult cu atât mai bună devine aplicaţia!
|
||||
|
||||
Dacă doriţi să vă implicaţi, accesaţi [notele noastre de contribuţie](.github/CONTRIBUTING.md).
|
||||
|
||||
<a href="https://hosted.weblate.org/engage/newpipe/">
|
||||
<img src="https://hosted.weblate.org/widgets/newpipe/-/287x66-grey.png" alt="Translation status" />
|
||||
</a>
|
||||
|
||||
## Donaţii
|
||||
Dacă vă place NewPipe, am fi bucuroşi să primim o donaţie. Puteţi să ne trimiteţi bitcoin sau să ne donaţi cu Bountysource sau Liberapay. Pentru mai multe informaţii în legătură cu donaţiile către NewPipe, vă rugăm vizitaţi [website-ul nostru](https://newpipe.net/donate).
|
||||
|
||||
<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><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>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="https://www.bountysource.com/teams/newpipe"><img src="https://upload.wikimedia.org/wikipedia/commons/thumb/2/22/Bountysource.png/320px-Bountysource.png" alt="Bountysource" width="190px"></a></td>
|
||||
<td><a href="https://www.bountysource.com/teams/newpipe"><img src="assets/bountysource_qr_code.png" alt="Visit NewPipe at bountysource.com" width="100px"></a></td>
|
||||
<td><a href="https://www.bountysource.com/teams/newpipe/issues"><img src="https://img.shields.io/bountysource/team/newpipe/activity.svg?colorB=cd201f" height="30px" alt="Check out how many bounties you can earn."></a></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
## Politica de Confidenţialitate
|
||||
|
||||
Proiectul NewPipe îşi propune să furnizeze o experienţă privată şi anonimă pentru utilizarea serviciilor web media.
|
||||
Prin urmare, aplicaţia nu colectează niciun fel de informaţii fără acordul dumneavoastră. Politica de confidențialitate a NewPipe explică în detaliu ce date sunt trimise și stocate atunci când trimiteți un raport de blocare sau comentați pe blogul nostru. Puteți găsi documentul [aici](https://newpipe.net/legal/privacy/).
|
||||
|
||||
## Licenţă
|
||||
[](http://www.gnu.org/licenses/gpl-3.0.en.html)
|
||||
|
||||
NewPipe este Software Gratuit: Îl puteţi folosi şi împărtăşi cum doriţi. Mai exact, îl puteți redistribui și / sau modifica în conformitate cu termenii
|
||||
[GNU General Public License](https://www.gnu.org/licenses/gpl.html) aşa cum a fost publicat de Free Software Foundation, fie versiunea 3 a Licenței, fie
|
||||
(la alegerea dvs.) orice versiune ulterioară.
|
||||
138
README.so.md
Normal file
138
README.so.md
Normal file
@@ -0,0 +1,138 @@
|
||||
<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">App bilaash ah oo fudud looguna talagalay in Android-ka wax loogu daawado.</h4>
|
||||
|
||||
<p align="center"><a href="https://f-droid.org/packages/org.schabi.newpipe/"><img src="https://fdroid.gitlab.io/artwork/badge/get-it-on-so.svg"></a></p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/TeamNewPipe/NewPipe/releases" alt="Siidaynta GitHub "><img src="https://img.shields.io/github/release/TeamNewPipe/NewPipe.svg" ></a>
|
||||
<a href="https://www.gnu.org/licenses/gpl-3.0" alt="Laysinka: GPLv3"><img src="https://img.shields.io/badge/License-GPL%20v3-blue.svg"></a>
|
||||
<a href="https://github.com/TeamNewPipe/NewPipe/actions" alt="Darajada Dhismaha"><img src="https://github.com/TeamNewPipe/NewPipe/workflows/CI/badge.svg?branch=dev&event=push"></a>
|
||||
<a href="https://hosted.weblate.org/engage/newpipe/" alt="Heerka Turjimaada"><img src="https://hosted.weblate.org/widgets/newpipe/-/svg-badge.svg"></a>
|
||||
<a href="http://webchat.freenode.net/?channels=%23newpipe" alt="Kanaalka IRC: #newpipe"><img src="https://img.shields.io/badge/IRC%20chat-%23newpipe-brightgreen.svg"></a>
|
||||
<a href="https://www.bountysource.com/teams/newpipe" alt="Kuwa Bountysource "><img src="https://img.shields.io/bountysource/team/newpipe/activity.svg?colorB=cd201f"></a>
|
||||
</p>
|
||||
<hr>
|
||||
<p align="center"><a href="#sawir-shaashadeed">Sawir-shaashadeed</a> • <a href="#faahfaahin">Faahfaahin</a> • <a href="#waxqabadka">Waxqabadka</a> • <a href="#kushubida-iyo-cusboonaysiinta">Kushubida iyo cusboonaysiinta</a> • <a href="#kusoo-kordhin">Kusoo Kordhin</a> • <a href="#ugu-deeq">Ugu Deeq</a> • <a href="#laysinka">Laysinka</a></p>
|
||||
<p align="center"><a href="https://newpipe.net">Website-ka</a> • <a href="https://newpipe.net/blog/">Maqaalada</a> • <a href="https://newpipe.net/FAQ/">Su'aalaha Aalaa La-iswaydiiyo</a> • <a href="https://newpipe.net/press/">Warbaahinta</a></p>
|
||||
<hr>
|
||||
|
||||
*Ku akhri luuqad kale: [English](README.md), [Español](README.es.md), [한국어](README.ko.md), [Soomaali](README.so.md), [Português Brasil](README.pt_BR.md), [日本語](README.ja.md), [Română](README.ro.md).*
|
||||
|
||||
<b>DIGNIIN: MIDKAN, NOOCA APP-KA EE HADDA WALI TIJAABO AYUU KU JIRAA, SIDAA DARTEED CILLADO AYAAD LA KULMI KARTAA. HADAAD LA KULANTO, KA FUR ARIN SHARAXAYA QAYBTANADA ARRIMAHA EE GITHUB-KA.</b>
|
||||
|
||||
<b>NEWPIPE AMA KUWA KU SALAYSAN IN PLAYSTORE-KA LA GALIYO WAXAY KA HOR IMANAYSAA SHARCIGA IYO SHURUUDAHA AY LEEYIHIIN.</b>
|
||||
|
||||
## Sawir-shaashadeed
|
||||
|
||||
[<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)
|
||||
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_03.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_03.png)
|
||||
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_04.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_04.png)
|
||||
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_05.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_05.png)
|
||||
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_06.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_06.png)
|
||||
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_07.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_07.png)
|
||||
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_08.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_08.png)
|
||||
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_09.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_09.png)
|
||||
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_10.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_10.png)
|
||||
[<img src="fastlane/metadata/android/en-US/images/tenInchScreenshots/shot_11.png" width=405>](fastlane/metadata/android/en-US/images/tenInchScreenshots/shot_11.png)
|
||||
[<img src="fastlane/metadata/android/en-US/images/tenInchScreenshots/shot_12.png" width=405>](fastlane/metadata/android/en-US/images/tenInchScreenshots/shot_12.png)
|
||||
|
||||
## Faahfaahin
|
||||
|
||||
NewPipe ma isticmalo nidaamka wada shaqaynta Google, ama API-ga YouTube. Kaliya website-yada ayaa la furaa si xogta loo baahanyahay loogala soo dhex baxo, App-kan waxaa lagu isticmaali karaa aaladaha aysa ku jirin Adeegyada Google. Sidoo kale, uma baahnid akoon YouTube ah si aad u isticmaasho NewPipe, kaasoo ah barnaamij bilaash ah.
|
||||
|
||||
### Waxqabadka
|
||||
|
||||
* Raadi muuqaalo
|
||||
* Soo bandhiga faahfaahin guud oo muuqaalada ku saabsan
|
||||
* Ku daawo muuqaalada YouTube
|
||||
* Dhagayso muuqaalada YouTube
|
||||
* Qaab daaqad ah (muuqaal daare yar oo application-nada dul fuula)
|
||||
* Dooro muuqaal daareha aad rabto inaad wax ku daawato
|
||||
* Daji muuqaalada
|
||||
* Daji dhagaysiga kaliya (cod)
|
||||
* Ku fur muuqaal Kodi
|
||||
* Tus muuqaalada ka xiga/kuwa lamidka ah
|
||||
* Inaad luuqada aad rabto wax kaga dhex raadiso YouTube
|
||||
* Daawo/xanib muuqaalada da'da ku xidhan
|
||||
* Soo bandhig xog guud oo ku saabsan kanaalada
|
||||
* Raadi kanaalo
|
||||
* Daawo muuqaalada kanaal
|
||||
* Taageerida Orbot/Tor (wali toos ma aha)
|
||||
* Taageerida muuqaalada 1080p/2K/4K
|
||||
* Kaydka wixii hore [aad u daawatay]
|
||||
* Inaad rukumato kanaalada
|
||||
* Kaydinta waxaad raadisay
|
||||
* Raadi/daawo xulalka
|
||||
* U daawo sidii xulal la horay
|
||||
* Hormo gali muuqaalada
|
||||
* Xulal gudaha [aalada] ah
|
||||
* Qoraal-hooseed
|
||||
* Taageerida waxyaabaha tooska ah
|
||||
* Soo bandhiga faalooyinka
|
||||
|
||||
### Adeegyada la Taageero
|
||||
|
||||
NewPipe wuxuu taageeraa adeegyo badan. [warqadan](https://teamnewpipe.github.io/documentation/) ayaa si faahfaahsan u sharaxaysa sida adeeg cusub loogu soo dari lahaa iyo kala fur-furaha. Fadlan nala soo xidhiidh hadaad rabto inaad mid cusub kusoo darto. Adeegyada aan hadda taageero waxaa kamid ah:
|
||||
|
||||
* YouTube
|
||||
* SoundCloud \[tijaabo\]
|
||||
* media.ccc.de \[tijaabo\]
|
||||
* PeerTube instances \[tijaabo\]
|
||||
* Bandcamp \[tijaabo\]
|
||||
|
||||
## Kushubida iyo cusboonaysiinta
|
||||
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.
|
||||
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.
|
||||
|
||||
Waxaa kale, hadaad rabto inaad tixraacayada kala badasho sabab jirta awgeed (tusaale shaqaynta aasaasiga ah ee NewPipe ayaa khalkhashay F-Droid-na wali cusboonysiin ma hayo), waxaan soojeedinaynaa isticmaalka qaabkan:
|
||||
1. Xogtaada koobi ka samee adoo raacaya Fadhiga > Luuqada & Fadhiga Kale > Gudbi Xog Diyaaran si aysa kaaga bixin kaydka wixii hore, rukunka, iyo xulalka
|
||||
2. Saar NewPipe
|
||||
3. Kasoo daji APK-ga tixraaca cusub oo ku shub
|
||||
4. Kasoo gali xogta talaabada 1 xaga Fadhiga > Luuqada & Fadhiga Kale > Soo Gali Xog Kaydsan
|
||||
|
||||
## Kusoo Kordhin
|
||||
Hadaad hayso fikrado; rogid, qaab badal, nadiifin koodh, ama koodhka ood si wayn wax oga badashaa—caawinta marwalba waa lasoo dhawaynayaa. Waxbadan hadii la qabto waxbadan ayaa fiicnaan!
|
||||
|
||||
Hadaad jeceshahay inaad qayb ka noqoto, fiiri [ogaysiisyada kusoo kordhinta](.github/CONTRIBUTING.md).
|
||||
|
||||
<a href="https://hosted.weblate.org/engage/newpipe/">
|
||||
<img src="https://hosted.weblate.org/widgets/newpipe/-/287x66-grey.png" alt="Heerka turjimaada" />
|
||||
</a>
|
||||
|
||||
## Ugu Deeq
|
||||
Hadaad jeceshahay NewPipe waan ku faraxsanaan lahayn deeq. Waxaad soo diri kartaa bitcoin ama sidoo kale waxaad deeqda kusoo diri kartaa xaga Bountysource ama Liberapay. Faahfaahin dheeraad ah oo kusaabsan ugu deeqida NewPipe, fadlan booqo [website-kanaga](https://newpipe.net/donate).
|
||||
|
||||
<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><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>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="https://www.bountysource.com/teams/newpipe"><img src="https://upload.wikimedia.org/wikipedia/commons/thumb/2/22/Bountysource.png/320px-Bountysource.png" alt="Bountysource" width="190px"></a></td>
|
||||
<td><a href="https://www.bountysource.com/teams/newpipe"><img src="assets/bountysource_qr_code.png" alt="Visit NewPipe at bountysource.com" width="100px"></a></td>
|
||||
<td><a href="https://www.bountysource.com/teams/newpipe/issues"><img src="https://img.shields.io/bountysource/team/newpipe/activity.svg?colorB=cd201f" height="30px" alt="Check out how many bounties you can earn."></a></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
## Siyaasada Sirdhawrka
|
||||
|
||||
Mashruuca NewPipe waxay ujeedadiisu tahay inuu bixiyo wax kuu gaar ah, oo adoon shaqsi ahaan laguu aqoonsan aad isticmaasho website-yada wax laga daawado/dhagaysto.
|
||||
Sidaa darteed, app-ku wax xog ah ma uruuriyo fasaxaaga la'aantii. Siyaasada Sirdhawrka NewPipe ayaa si faahfaahsan u sharaxda waxii xog ah ee la diro markaad cillad wariso, ama aad bogganaga faallo ka dhiibato. Warqada waxaad ka heli kartaa [halkan](https://newpipe.net/legal/privacy/).
|
||||
|
||||
## Laysinka
|
||||
[](http://www.gnu.org/licenses/gpl-3.0.en.html)
|
||||
|
||||
NewPipe waa barnaamij bilaash ah oon lahayn xuquuqda daabacaada: Waad isticmaali kartaa, waad wadaagi kartaa waadna hormarin kartaa hadaad rabto. Gaar ahaan waad sii daabici kartaa ama wax baad ka badali kartaa ayadoo la raacayo shuruudaha sharciga guud ee [GNU](https://www.gnu.org/licenses/gpl.html) sida ay soosaareen Ururka Barnaamijyada Bilaashka ah, soosaarista 3aad ee laysinka, ama (hadaad doonto) nooc walba oo kasii dambeeyay laysinkii 3aad.
|
||||
@@ -1,3 +1,7 @@
|
||||
plugins {
|
||||
id "org.sonarqube" version "3.1.1"
|
||||
}
|
||||
|
||||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'kotlin-android'
|
||||
apply plugin: 'kotlin-android-extensions'
|
||||
@@ -13,8 +17,8 @@ android {
|
||||
resValue "string", "app_name", "NewPipe"
|
||||
minSdkVersion 19
|
||||
targetSdkVersion 29
|
||||
versionCode 960
|
||||
versionName "0.20.6"
|
||||
versionCode 967
|
||||
versionName "0.21.1"
|
||||
|
||||
multiDexEnabled true
|
||||
|
||||
@@ -85,19 +89,24 @@ android {
|
||||
sourceSets {
|
||||
androidTest.assets.srcDirs += files("$projectDir/schemas".toString())
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
viewBinding true
|
||||
}
|
||||
}
|
||||
|
||||
ext {
|
||||
icepickVersion = '3.2.0'
|
||||
checkstyleVersion = '8.37'
|
||||
checkstyleVersion = '8.38'
|
||||
stethoVersion = '1.5.1'
|
||||
leakCanaryVersion = '2.5'
|
||||
exoPlayerVersion = '2.11.8'
|
||||
exoPlayerVersion = '2.12.3'
|
||||
androidxLifecycleVersion = '2.2.0'
|
||||
androidxRoomVersion = '2.3.0-alpha03'
|
||||
groupieVersion = '2.8.1'
|
||||
markwonVersion = '4.6.0'
|
||||
googleAutoServiceVersion = '1.0-rc7'
|
||||
mockitoVersion = '3.6.0'
|
||||
}
|
||||
|
||||
configurations {
|
||||
@@ -106,7 +115,7 @@ configurations {
|
||||
}
|
||||
|
||||
checkstyle {
|
||||
configFile rootProject.file('checkstyle.xml')
|
||||
configDir rootProject.file(".")
|
||||
ignoreFailures false
|
||||
showViolations true
|
||||
toolVersion = checkstyleVersion
|
||||
@@ -153,6 +162,14 @@ afterEvaluate {
|
||||
preDebugBuild.dependsOn formatKtlint, runCheckstyle, runKtlint
|
||||
}
|
||||
|
||||
sonarqube {
|
||||
properties {
|
||||
property "sonar.projectKey", "TeamNewPipe_NewPipe"
|
||||
property "sonar.organization", "teamnewpipe"
|
||||
property "sonar.host.url", "https://sonarcloud.io"
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.1'
|
||||
|
||||
@@ -162,7 +179,7 @@ dependencies {
|
||||
kapt "frankiesardo:icepick-processor:${icepickVersion}"
|
||||
|
||||
checkstyle "com.puppycrawl.tools:checkstyle:${checkstyleVersion}"
|
||||
ktlint "com.pinterest:ktlint:0.39.0"
|
||||
ktlint "com.pinterest:ktlint:0.40.0"
|
||||
|
||||
debugImplementation "com.facebook.stetho:stetho:${stethoVersion}"
|
||||
debugImplementation "com.facebook.stetho:stetho-okhttp3:${stethoVersion}"
|
||||
@@ -175,13 +192,13 @@ dependencies {
|
||||
|
||||
// NewPipe dependencies
|
||||
// You can use a local version by uncommenting a few lines in settings.gradle
|
||||
implementation 'com.github.TeamNewPipe:NewPipeExtractor:b3835bd616ab28b861c83dcefd56e1754c6d20be'
|
||||
implementation 'com.github.TeamNewPipe:NewPipeExtractor:v0.21.1'
|
||||
implementation "com.github.TeamNewPipe:nanojson:1d9e1aea9049fc9f85e68b43ba39fe7be1c1f751"
|
||||
|
||||
implementation "org.jsoup:jsoup:1.13.1"
|
||||
|
||||
//noinspection GradleDependency --> do not update okhttp to keep supporting Android 4.4 users
|
||||
implementation "com.squareup.okhttp3:okhttp:3.12.12"
|
||||
implementation "com.squareup.okhttp3:okhttp:3.12.13"
|
||||
|
||||
implementation "com.google.android.exoplayer:exoplayer:${exoPlayerVersion}"
|
||||
implementation "com.google.android.exoplayer:extension-mediasession:${exoPlayerVersion}"
|
||||
@@ -199,6 +216,8 @@ dependencies {
|
||||
implementation 'androidx.core:core-ktx:1.3.2'
|
||||
implementation 'androidx.documentfile:documentfile:1.0.1'
|
||||
implementation 'androidx.localbroadcastmanager:localbroadcastmanager:1.0.0'
|
||||
implementation 'androidx.media:media:1.2.1'
|
||||
implementation 'androidx.webkit:webkit:1.4.0'
|
||||
|
||||
implementation "androidx.lifecycle:lifecycle-livedata:${androidxLifecycleVersion}"
|
||||
implementation "androidx.lifecycle:lifecycle-viewmodel:${androidxLifecycleVersion}"
|
||||
@@ -210,7 +229,7 @@ dependencies {
|
||||
implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0"
|
||||
|
||||
implementation "com.xwray:groupie:${groupieVersion}"
|
||||
implementation "com.xwray:groupie-kotlin-android-extensions:${groupieVersion}"
|
||||
implementation "com.xwray:groupie-viewbinding:${groupieVersion}"
|
||||
|
||||
implementation "de.hdodenhof:circleimageview:3.1.0"
|
||||
implementation "com.nostra13.universalimageloader:universal-image-loader:1.9.5"
|
||||
@@ -226,10 +245,11 @@ dependencies {
|
||||
implementation "io.reactivex.rxjava3:rxandroid:3.0.0"
|
||||
implementation "com.jakewharton.rxbinding4:rxbinding:4.0.0"
|
||||
|
||||
implementation "org.ocpsoft.prettytime:prettytime:4.0.6.Final"
|
||||
implementation "org.ocpsoft.prettytime:prettytime:5.0.0.Final"
|
||||
|
||||
testImplementation 'junit:junit:4.13.1'
|
||||
testImplementation 'org.mockito:mockito-core:3.6.0'
|
||||
testImplementation "org.mockito:mockito-core:${mockitoVersion}"
|
||||
testImplementation "org.mockito:mockito-inline:${mockitoVersion}"
|
||||
|
||||
androidTestImplementation "androidx.test.ext:junit:1.1.2"
|
||||
androidTestImplementation "androidx.room:room-testing:${androidxRoomVersion}"
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
package org.schabi.newpipe.error;
|
||||
|
||||
import android.os.Parcel;
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import androidx.test.filters.LargeTest;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.extractor.ServiceList;
|
||||
import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
/**
|
||||
* Instrumented tests for {@link ErrorInfo}.
|
||||
*/
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
@LargeTest
|
||||
public class ErrorInfoTest {
|
||||
|
||||
@Test
|
||||
public void errorInfoTestParcelable() {
|
||||
final ErrorInfo info = new ErrorInfo(new ParsingException("Hello"),
|
||||
UserAction.USER_REPORT, "request", ServiceList.YouTube.getServiceId());
|
||||
// Obtain a Parcel object and write the parcelable object to it:
|
||||
final Parcel parcel = Parcel.obtain();
|
||||
info.writeToParcel(parcel, 0);
|
||||
parcel.setDataPosition(0);
|
||||
final ErrorInfo infoFromParcel = (ErrorInfo) ErrorInfo.CREATOR.createFromParcel(parcel);
|
||||
|
||||
assertTrue(Arrays.toString(infoFromParcel.getStackTraces())
|
||||
.contains(ErrorInfoTest.class.getSimpleName()));
|
||||
assertEquals(UserAction.USER_REPORT, infoFromParcel.getUserAction());
|
||||
assertEquals(ServiceList.YouTube.getServiceInfo().getName(),
|
||||
infoFromParcel.getServiceName());
|
||||
assertEquals("request", infoFromParcel.getRequest());
|
||||
assertEquals(R.string.parsing_error, infoFromParcel.getMessageStringId());
|
||||
|
||||
parcel.recycle();
|
||||
}
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
package org.schabi.newpipe.report;
|
||||
|
||||
import android.os.Parcel;
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import androidx.test.filters.LargeTest;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.schabi.newpipe.R;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
/**
|
||||
* Instrumented tests for {@link ErrorInfo}.
|
||||
*/
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
@LargeTest
|
||||
public class ErrorInfoTest {
|
||||
|
||||
@Test
|
||||
public void errorInfoTestParcelable() {
|
||||
final ErrorInfo info = ErrorInfo.make(UserAction.USER_REPORT, "youtube", "request",
|
||||
R.string.general_error);
|
||||
// Obtain a Parcel object and write the parcelable object to it:
|
||||
final Parcel parcel = Parcel.obtain();
|
||||
info.writeToParcel(parcel, 0);
|
||||
parcel.setDataPosition(0);
|
||||
final ErrorInfo infoFromParcel = ErrorInfo.CREATOR.createFromParcel(parcel);
|
||||
|
||||
assertEquals(UserAction.USER_REPORT, infoFromParcel.getUserAction());
|
||||
assertEquals("youtube", infoFromParcel.getServiceName());
|
||||
assertEquals("request", infoFromParcel.getRequest());
|
||||
assertEquals(R.string.general_error, infoFromParcel.getMessage());
|
||||
|
||||
parcel.recycle();
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package org.schabi.newpipe.settings;
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.preference.Preference;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
|
||||
@@ -13,11 +14,22 @@ public class DebugSettingsFragment extends BasePreferenceFragment {
|
||||
public void onCreate(@Nullable final Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
findPreference(getString(R.string.show_memory_leaks_key))
|
||||
.setOnPreferenceClickListener(preference -> {
|
||||
startActivity(LeakCanary.INSTANCE.newLeakDisplayActivityIntent());
|
||||
return true;
|
||||
});
|
||||
final Preference showMemoryLeaksPreference
|
||||
= findPreference(getString(R.string.show_memory_leaks_key));
|
||||
final Preference crashTheAppPreference
|
||||
= findPreference(getString(R.string.crash_the_app_key));
|
||||
|
||||
assert showMemoryLeaksPreference != null;
|
||||
assert crashTheAppPreference != null;
|
||||
|
||||
showMemoryLeaksPreference.setOnPreferenceClickListener(preference -> {
|
||||
startActivity(LeakCanary.INSTANCE.newLeakDisplayActivityIntent());
|
||||
return true;
|
||||
});
|
||||
|
||||
crashTheAppPreference.setOnPreferenceClickListener(preference -> {
|
||||
throw new RuntimeException();
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -53,7 +53,7 @@
|
||||
</service>
|
||||
|
||||
<activity
|
||||
android:name=".player.BackgroundPlayerActivity"
|
||||
android:name=".player.PlayQueueActivity"
|
||||
android:label="@string/title_activity_play_queue"
|
||||
android:launchMode="singleTask" />
|
||||
|
||||
@@ -85,7 +85,7 @@
|
||||
android:name=".ExitActivity"
|
||||
android:label="@string/general_error"
|
||||
android:theme="@android:style/Theme.NoDisplay" />
|
||||
<activity android:name=".report.ErrorActivity" />
|
||||
<activity android:name=".error.ErrorActivity" />
|
||||
|
||||
<!-- giga get related -->
|
||||
<activity
|
||||
@@ -106,7 +106,7 @@
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".ReCaptchaActivity"
|
||||
android:name=".error.ReCaptchaActivity"
|
||||
android:label="@string/recaptcha" />
|
||||
|
||||
<provider
|
||||
@@ -227,20 +227,22 @@
|
||||
<data android:host="invidio.us" />
|
||||
<data android:host="dev.invidio.us" />
|
||||
<data android:host="www.invidio.us" />
|
||||
<data android:host="redirect.invidious.io" />
|
||||
<data android:host="invidious.snopyta.org" />
|
||||
<data android:host="fi.invidious.snopyta.org" />
|
||||
<data android:host="yewtu.be" />
|
||||
<data android:host="invidious.ggc-project.de" />
|
||||
<data android:host="yt.maisputain.ovh" />
|
||||
<data android:host="invidious.13ad.de" />
|
||||
<data android:host="invidious.toot.koeln" />
|
||||
<data android:host="tube.connect.cafe" />
|
||||
<data android:host="invidious.zapashcanon.fr" />
|
||||
<data android:host="invidious.kavin.rocks" />
|
||||
<data android:host="invidious.tube" />
|
||||
<data android:host="invidious.site" />
|
||||
<data android:host="invidious.xyz" />
|
||||
<data android:host="vid.mint.lgbt" />
|
||||
<data android:host="invidiou.site" />
|
||||
<data android:host="invidious.fdn.fr" />
|
||||
<data android:host="watch.nettohikari.com" />
|
||||
<data android:host="invidious.snwmds.net" />
|
||||
<data android:host="invidious.snwmds.org" />
|
||||
<data android:host="invidious.snwmds.com" />
|
||||
<data android:host="invidious.sunsetravens.com" />
|
||||
<data android:host="invidious.gachirangers.com" />
|
||||
<data android:host="invidious.048596.xyz" />
|
||||
<data android:host="invidious.zee.li" />
|
||||
<data android:host="vid.puffyan.us" />
|
||||
<data android:host="ytprivate.com" />
|
||||
<data android:pathPrefix="/" />
|
||||
</intent-filter>
|
||||
|
||||
@@ -268,7 +270,7 @@
|
||||
<data android:mimeType="text/plain" />
|
||||
</intent-filter>
|
||||
|
||||
<!-- MediaCCC filter -->
|
||||
<!-- media.ccc.de filter -->
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<action android:name="android.media.action.MEDIA_PLAY_FROM_SEARCH" />
|
||||
@@ -315,11 +317,29 @@
|
||||
<data android:pathPrefix="/accounts/" />
|
||||
<data android:pathPrefix="/video-channels/" />
|
||||
</intent-filter>
|
||||
|
||||
<!-- Bandcamp filter -->
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW"/>
|
||||
<action android:name="android.media.action.MEDIA_PLAY_FROM_SEARCH"/>
|
||||
<action android:name="android.nfc.action.NDEF_DISCOVERED"/>
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
<category android:name="android.intent.category.BROWSABLE"/>
|
||||
|
||||
<data android:scheme="http"/>
|
||||
<data android:scheme="https"/>
|
||||
<data android:host="bandcamp.com"/>
|
||||
<data android:host="*.bandcamp.com"/>
|
||||
<data android:pathPrefix="/"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<service
|
||||
android:name=".RouterActivity$FetcherService"
|
||||
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" />
|
||||
<!-- see https://github.com/TeamNewPipe/NewPipe/issues/3947 -->
|
||||
<!-- Version < 3.0. DeX Mode and Screen Mirroring support -->
|
||||
<meta-data android:name="com.samsung.android.keepalive.density" android:value="true"/>
|
||||
|
||||
245
app/src/main/assets/epl1.html
Normal file
245
app/src/main/assets/epl1.html
Normal file
@@ -0,0 +1,245 @@
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
|
||||
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||
<!-- saved from url=(0050)https://www.eclipse.org/org/documents/epl-v10.html -->
|
||||
<html xmlns="http://www.w3.org/1999/xhtml">
|
||||
<head>
|
||||
<meta content="text/html; charset=utf-8" http-equiv="Content-Type">
|
||||
|
||||
<title>Eclipse Public License - Version 1.0</title>
|
||||
|
||||
</head>
|
||||
|
||||
<body cz-shortcut-listen="true" lang="EN-US">
|
||||
|
||||
<h2>Eclipse Public License - v 1.0</h2>
|
||||
|
||||
<p>THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE
|
||||
PUBLIC LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR
|
||||
DISTRIBUTION OF THE PROGRAM CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS
|
||||
AGREEMENT.</p>
|
||||
|
||||
<p><b>1. DEFINITIONS</b></p>
|
||||
|
||||
<p>"Contribution" means:</p>
|
||||
|
||||
<p class="list">a) in the case of the initial Contributor, the initial
|
||||
code and documentation distributed under this Agreement, and</p>
|
||||
<p class="list">b) in the case of each subsequent Contributor:</p>
|
||||
<p class="list">i) changes to the Program, and</p>
|
||||
<p class="list">ii) additions to the Program;</p>
|
||||
<p class="list">where such changes and/or additions to the Program
|
||||
originate from and are distributed by that particular Contributor. A
|
||||
Contribution 'originates' from a Contributor if it was added to the
|
||||
Program by such Contributor itself or anyone acting on such
|
||||
Contributor's behalf. Contributions do not include additions to the
|
||||
Program which: (i) are separate modules of software distributed in
|
||||
conjunction with the Program under their own license agreement, and (ii)
|
||||
are not derivative works of the Program.</p>
|
||||
|
||||
<p>"Contributor" means any person or entity that distributes
|
||||
the Program.</p>
|
||||
|
||||
<p>"Licensed Patents" mean patent claims licensable by a
|
||||
Contributor which are necessarily infringed by the use or sale of its
|
||||
Contribution alone or when combined with the Program.</p>
|
||||
|
||||
<p>"Program" means the Contributions distributed in accordance
|
||||
with this Agreement.</p>
|
||||
|
||||
<p>"Recipient" means anyone who receives the Program under
|
||||
this Agreement, including all Contributors.</p>
|
||||
|
||||
<p><b>2. GRANT OF RIGHTS</b></p>
|
||||
|
||||
<p class="list">a) Subject to the terms of this Agreement, each
|
||||
Contributor hereby grants Recipient a non-exclusive, worldwide,
|
||||
royalty-free copyright license to reproduce, prepare derivative works
|
||||
of, publicly display, publicly perform, distribute and sublicense the
|
||||
Contribution of such Contributor, if any, and such derivative works, in
|
||||
source code and object code form.</p>
|
||||
|
||||
<p class="list">b) Subject to the terms of this Agreement, each
|
||||
Contributor hereby grants Recipient a non-exclusive, worldwide,
|
||||
royalty-free patent license under Licensed Patents to make, use, sell,
|
||||
offer to sell, import and otherwise transfer the Contribution of such
|
||||
Contributor, if any, in source code and object code form. This patent
|
||||
license shall apply to the combination of the Contribution and the
|
||||
Program if, at the time the Contribution is added by the Contributor,
|
||||
such addition of the Contribution causes such combination to be covered
|
||||
by the Licensed Patents. The patent license shall not apply to any other
|
||||
combinations which include the Contribution. No hardware per se is
|
||||
licensed hereunder.</p>
|
||||
|
||||
<p class="list">c) Recipient understands that although each Contributor
|
||||
grants the licenses to its Contributions set forth herein, no assurances
|
||||
are provided by any Contributor that the Program does not infringe the
|
||||
patent or other intellectual property rights of any other entity. Each
|
||||
Contributor disclaims any liability to Recipient for claims brought by
|
||||
any other entity based on infringement of intellectual property rights
|
||||
or otherwise. As a condition to exercising the rights and licenses
|
||||
granted hereunder, each Recipient hereby assumes sole responsibility to
|
||||
secure any other intellectual property rights needed, if any. For
|
||||
example, if a third party patent license is required to allow Recipient
|
||||
to distribute the Program, it is Recipient's responsibility to acquire
|
||||
that license before distributing the Program.</p>
|
||||
|
||||
<p class="list">d) Each Contributor represents that to its knowledge it
|
||||
has sufficient copyright rights in its Contribution, if any, to grant
|
||||
the copyright license set forth in this Agreement.</p>
|
||||
|
||||
<p><b>3. REQUIREMENTS</b></p>
|
||||
|
||||
<p>A Contributor may choose to distribute the Program in object code
|
||||
form under its own license agreement, provided that:</p>
|
||||
|
||||
<p class="list">a) it complies with the terms and conditions of this
|
||||
Agreement; and</p>
|
||||
|
||||
<p class="list">b) its license agreement:</p>
|
||||
|
||||
<p class="list">i) effectively disclaims on behalf of all Contributors
|
||||
all warranties and conditions, express and implied, including warranties
|
||||
or conditions of title and non-infringement, and implied warranties or
|
||||
conditions of merchantability and fitness for a particular purpose;</p>
|
||||
|
||||
<p class="list">ii) effectively excludes on behalf of all Contributors
|
||||
all liability for damages, including direct, indirect, special,
|
||||
incidental and consequential damages, such as lost profits;</p>
|
||||
|
||||
<p class="list">iii) states that any provisions which differ from this
|
||||
Agreement are offered by that Contributor alone and not by any other
|
||||
party; and</p>
|
||||
|
||||
<p class="list">iv) states that source code for the Program is available
|
||||
from such Contributor, and informs licensees how to obtain it in a
|
||||
reasonable manner on or through a medium customarily used for software
|
||||
exchange.</p>
|
||||
|
||||
<p>When the Program is made available in source code form:</p>
|
||||
|
||||
<p class="list">a) it must be made available under this Agreement; and</p>
|
||||
|
||||
<p class="list">b) a copy of this Agreement must be included with each
|
||||
copy of the Program.</p>
|
||||
|
||||
<p>Contributors may not remove or alter any copyright notices contained
|
||||
within the Program.</p>
|
||||
|
||||
<p>Each Contributor must identify itself as the originator of its
|
||||
Contribution, if any, in a manner that reasonably allows subsequent
|
||||
Recipients to identify the originator of the Contribution.</p>
|
||||
|
||||
<p><b>4. COMMERCIAL DISTRIBUTION</b></p>
|
||||
|
||||
<p>Commercial distributors of software may accept certain
|
||||
responsibilities with respect to end users, business partners and the
|
||||
like. While this license is intended to facilitate the commercial use of
|
||||
the Program, the Contributor who includes the Program in a commercial
|
||||
product offering should do so in a manner which does not create
|
||||
potential liability for other Contributors. Therefore, if a Contributor
|
||||
includes the Program in a commercial product offering, such Contributor
|
||||
("Commercial Contributor") hereby agrees to defend and
|
||||
indemnify every other Contributor ("Indemnified Contributor")
|
||||
against any losses, damages and costs (collectively "Losses")
|
||||
arising from claims, lawsuits and other legal actions brought by a third
|
||||
party against the Indemnified Contributor to the extent caused by the
|
||||
acts or omissions of such Commercial Contributor in connection with its
|
||||
distribution of the Program in a commercial product offering. The
|
||||
obligations in this section do not apply to any claims or Losses
|
||||
relating to any actual or alleged intellectual property infringement. In
|
||||
order to qualify, an Indemnified Contributor must: a) promptly notify
|
||||
the Commercial Contributor in writing of such claim, and b) allow the
|
||||
Commercial Contributor to control, and cooperate with the Commercial
|
||||
Contributor in, the defense and any related settlement negotiations. The
|
||||
Indemnified Contributor may participate in any such claim at its own
|
||||
expense.</p>
|
||||
|
||||
<p>For example, a Contributor might include the Program in a commercial
|
||||
product offering, Product X. That Contributor is then a Commercial
|
||||
Contributor. If that Commercial Contributor then makes performance
|
||||
claims, or offers warranties related to Product X, those performance
|
||||
claims and warranties are such Commercial Contributor's responsibility
|
||||
alone. Under this section, the Commercial Contributor would have to
|
||||
defend claims against the other Contributors related to those
|
||||
performance claims and warranties, and if a court requires any other
|
||||
Contributor to pay any damages as a result, the Commercial Contributor
|
||||
must pay those damages.</p>
|
||||
|
||||
<p><b>5. NO WARRANTY</b></p>
|
||||
|
||||
<p>EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, THE PROGRAM IS
|
||||
PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS
|
||||
OF ANY KIND, EITHER EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION,
|
||||
ANY WARRANTIES OR CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY
|
||||
OR FITNESS FOR A PARTICULAR PURPOSE. Each Recipient is solely
|
||||
responsible for determining the appropriateness of using and
|
||||
distributing the Program and assumes all risks associated with its
|
||||
exercise of rights under this Agreement , including but not limited to
|
||||
the risks and costs of program errors, compliance with applicable laws,
|
||||
damage to or loss of data, programs or equipment, and unavailability or
|
||||
interruption of operations.</p>
|
||||
|
||||
<p><b>6. DISCLAIMER OF LIABILITY</b></p>
|
||||
|
||||
<p>EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, NEITHER RECIPIENT
|
||||
NOR ANY CONTRIBUTORS SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT,
|
||||
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING
|
||||
WITHOUT LIMITATION LOST PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF
|
||||
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
||||
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OR
|
||||
DISTRIBUTION OF THE PROGRAM OR THE EXERCISE OF ANY RIGHTS GRANTED
|
||||
HEREUNDER, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.</p>
|
||||
|
||||
<p><b>7. GENERAL</b></p>
|
||||
|
||||
<p>If any provision of this Agreement is invalid or unenforceable under
|
||||
applicable law, it shall not affect the validity or enforceability of
|
||||
the remainder of the terms of this Agreement, and without further action
|
||||
by the parties hereto, such provision shall be reformed to the minimum
|
||||
extent necessary to make such provision valid and enforceable.</p>
|
||||
|
||||
<p>If Recipient institutes patent litigation against any entity
|
||||
(including a cross-claim or counterclaim in a lawsuit) alleging that the
|
||||
Program itself (excluding combinations of the Program with other
|
||||
software or hardware) infringes such Recipient's patent(s), then such
|
||||
Recipient's rights granted under Section 2(b) shall terminate as of the
|
||||
date such litigation is filed.</p>
|
||||
|
||||
<p>All Recipient's rights under this Agreement shall terminate if it
|
||||
fails to comply with any of the material terms or conditions of this
|
||||
Agreement and does not cure such failure in a reasonable period of time
|
||||
after becoming aware of such noncompliance. If all Recipient's rights
|
||||
under this Agreement terminate, Recipient agrees to cease use and
|
||||
distribution of the Program as soon as reasonably practicable. However,
|
||||
Recipient's obligations under this Agreement and any licenses granted by
|
||||
Recipient relating to the Program shall continue and survive.</p>
|
||||
|
||||
<p>Everyone is permitted to copy and distribute copies of this
|
||||
Agreement, but in order to avoid inconsistency the Agreement is
|
||||
copyrighted and may only be modified in the following manner. The
|
||||
Agreement Steward reserves the right to publish new versions (including
|
||||
revisions) of this Agreement from time to time. No one other than the
|
||||
Agreement Steward has the right to modify this Agreement. The Eclipse
|
||||
Foundation is the initial Agreement Steward. The Eclipse Foundation may
|
||||
assign the responsibility to serve as the Agreement Steward to a
|
||||
suitable separate entity. Each new version of the Agreement will be
|
||||
given a distinguishing version number. The Program (including
|
||||
Contributions) may always be distributed subject to the version of the
|
||||
Agreement under which it was received. In addition, after a new version
|
||||
of the Agreement is published, Contributor may elect to distribute the
|
||||
Program (including its Contributions) under the new version. Except as
|
||||
expressly stated in Sections 2(a) and 2(b) above, Recipient receives no
|
||||
rights or licenses to the intellectual property of any Contributor under
|
||||
this Agreement, whether expressly, by implication, estoppel or
|
||||
otherwise. All rights in the Program not expressly granted under this
|
||||
Agreement are reserved.</p>
|
||||
|
||||
<p>This Agreement is governed by the laws of the State of New York and
|
||||
the intellectual property laws of the United States of America. No party
|
||||
to this Agreement will bring a legal action under this Agreement more
|
||||
than one year after the cause of action arose. Each party waives its
|
||||
rights to a jury trial in any resulting litigation.</p>
|
||||
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -10,6 +10,7 @@ import android.widget.OverScroller;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.coordinatorlayout.widget.CoordinatorLayout;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
@@ -27,7 +28,7 @@ public final class FlingBehavior extends AppBarLayout.Behavior {
|
||||
private boolean allowScroll = true;
|
||||
private final Rect globalRect = new Rect();
|
||||
private final List<Integer> skipInterceptionOfElements = Arrays.asList(
|
||||
R.id.playQueuePanel, R.id.playbackSeekBar,
|
||||
R.id.itemsListPanel, R.id.playbackSeekBar,
|
||||
R.id.playPauseButton, R.id.playPreviousButton, R.id.playNextButton);
|
||||
|
||||
@Override
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
package org.schabi.newpipe;
|
||||
|
||||
/*
|
||||
* Created by Christian Schabesberger on 24.12.15.
|
||||
*
|
||||
* Copyright (C) Christian Schabesberger 2015 <chris.schabesberger@mailbox.org>
|
||||
* ActivityCommunicator.java is part of NewPipe.
|
||||
*
|
||||
* NewPipe is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* NewPipe is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Singleton:
|
||||
* Used to send data between certain Activity/Services within the same process.
|
||||
* This can be considered as an ugly hack inside the Android universe.
|
||||
**/
|
||||
public class ActivityCommunicator {
|
||||
|
||||
private static ActivityCommunicator activityCommunicator;
|
||||
private volatile Class returnActivity;
|
||||
|
||||
public static ActivityCommunicator getCommunicator() {
|
||||
if (activityCommunicator == null) {
|
||||
activityCommunicator = new ActivityCommunicator();
|
||||
}
|
||||
return activityCommunicator;
|
||||
}
|
||||
|
||||
public Class getReturnActivity() {
|
||||
return returnActivity;
|
||||
}
|
||||
|
||||
public void setReturnActivity(final Class returnActivity) {
|
||||
this.returnActivity = returnActivity;
|
||||
}
|
||||
}
|
||||
@@ -20,13 +20,14 @@ import org.acra.ACRA;
|
||||
import org.acra.config.ACRAConfigurationException;
|
||||
import org.acra.config.CoreConfiguration;
|
||||
import org.acra.config.CoreConfigurationBuilder;
|
||||
import org.schabi.newpipe.error.ErrorActivity;
|
||||
import org.schabi.newpipe.error.ErrorInfo;
|
||||
import org.schabi.newpipe.error.ReCaptchaActivity;
|
||||
import org.schabi.newpipe.error.UserAction;
|
||||
import org.schabi.newpipe.extractor.NewPipe;
|
||||
import org.schabi.newpipe.extractor.downloader.Downloader;
|
||||
import org.schabi.newpipe.report.ErrorActivity;
|
||||
import org.schabi.newpipe.report.ErrorInfo;
|
||||
import org.schabi.newpipe.report.UserAction;
|
||||
import org.schabi.newpipe.ktx.ExceptionUtils;
|
||||
import org.schabi.newpipe.settings.SettingsActivity;
|
||||
import org.schabi.newpipe.util.ExceptionUtils;
|
||||
import org.schabi.newpipe.util.Localization;
|
||||
import org.schabi.newpipe.util.ServiceHelper;
|
||||
import org.schabi.newpipe.util.StateSaver;
|
||||
@@ -67,8 +68,10 @@ 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;
|
||||
@Nullable
|
||||
private Disposable disposable = null;
|
||||
|
||||
@NonNull
|
||||
public static App getApp() {
|
||||
@@ -91,9 +94,9 @@ public class App extends MultiDexApplication {
|
||||
SettingsActivity.initSettings(this);
|
||||
|
||||
NewPipe.init(getDownloader(),
|
||||
Localization.getPreferredLocalization(this),
|
||||
Localization.getPreferredContentCountry(this));
|
||||
Localization.init(getApplicationContext());
|
||||
Localization.getPreferredLocalization(this),
|
||||
Localization.getPreferredContentCountry(this));
|
||||
Localization.initPrettyTime(Localization.resolvePrettyTime(getApplicationContext()));
|
||||
|
||||
StateSaver.init(this);
|
||||
initNotificationChannels();
|
||||
@@ -222,14 +225,10 @@ public class App extends MultiDexApplication {
|
||||
.setBuildConfigClass(BuildConfig.class)
|
||||
.build();
|
||||
ACRA.init(this, acraConfig);
|
||||
} catch (final ACRAConfigurationException ace) {
|
||||
ace.printStackTrace();
|
||||
ErrorActivity.reportError(this,
|
||||
ace,
|
||||
null,
|
||||
null,
|
||||
ErrorInfo.make(UserAction.SOMETHING_ELSE, "none",
|
||||
"Could not initialize ACRA crash report", R.string.app_ui_crash));
|
||||
} catch (final ACRAConfigurationException exception) {
|
||||
exception.printStackTrace();
|
||||
ErrorActivity.reportError(this, new ErrorInfo(exception,
|
||||
UserAction.SOMETHING_ELSE, "Could not initialize ACRA crash report"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -242,8 +241,9 @@ public class App extends MultiDexApplication {
|
||||
String name = getString(R.string.notification_channel_name);
|
||||
String description = getString(R.string.notification_channel_description);
|
||||
|
||||
// Keep this below DEFAULT to avoid making noise on every notification update
|
||||
final int importance = NotificationManager.IMPORTANCE_LOW;
|
||||
// Keep this below DEFAULT to avoid making noise on every notification update for the main
|
||||
// and update channels
|
||||
int importance = NotificationManager.IMPORTANCE_LOW;
|
||||
|
||||
final NotificationChannel mainChannel = new NotificationChannel(id, name, importance);
|
||||
mainChannel.setDescription(description);
|
||||
@@ -255,9 +255,17 @@ public class App extends MultiDexApplication {
|
||||
final NotificationChannel appUpdateChannel = new NotificationChannel(id, name, importance);
|
||||
appUpdateChannel.setDescription(description);
|
||||
|
||||
id = getString(R.string.hash_channel_id);
|
||||
name = getString(R.string.hash_channel_name);
|
||||
description = getString(R.string.hash_channel_description);
|
||||
importance = NotificationManager.IMPORTANCE_HIGH;
|
||||
|
||||
final NotificationChannel hashChannel = new NotificationChannel(id, name, importance);
|
||||
hashChannel.setDescription(description);
|
||||
|
||||
final NotificationManager notificationManager = getSystemService(NotificationManager.class);
|
||||
notificationManager.createNotificationChannels(Arrays.asList(mainChannel,
|
||||
appUpdateChannel));
|
||||
appUpdateChannel, hashChannel));
|
||||
}
|
||||
|
||||
protected boolean isDisposedRxExceptionsReported() {
|
||||
|
||||
@@ -35,7 +35,7 @@ public abstract class BaseFragment extends Fragment {
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public void onAttach(final Context context) {
|
||||
public void onAttach(@NonNull final Context context) {
|
||||
super.onAttach(context);
|
||||
activity = (AppCompatActivity) context;
|
||||
}
|
||||
@@ -61,7 +61,7 @@ public abstract class BaseFragment extends Fragment {
|
||||
|
||||
|
||||
@Override
|
||||
public void onViewCreated(final View rootView, final Bundle savedInstanceState) {
|
||||
public void onViewCreated(@NonNull final View rootView, final Bundle savedInstanceState) {
|
||||
super.onViewCreated(rootView, savedInstanceState);
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "onViewCreated() called with: "
|
||||
@@ -73,7 +73,7 @@ public abstract class BaseFragment extends Fragment {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSaveInstanceState(final Bundle outState) {
|
||||
public void onSaveInstanceState(@NonNull final Bundle outState) {
|
||||
super.onSaveInstanceState(outState);
|
||||
Icepick.saveInstanceState(this, outState);
|
||||
}
|
||||
|
||||
@@ -22,9 +22,9 @@ import com.grack.nanojson.JsonObject;
|
||||
import com.grack.nanojson.JsonParser;
|
||||
import com.grack.nanojson.JsonParserException;
|
||||
|
||||
import org.schabi.newpipe.report.ErrorActivity;
|
||||
import org.schabi.newpipe.report.ErrorInfo;
|
||||
import org.schabi.newpipe.report.UserAction;
|
||||
import org.schabi.newpipe.error.ErrorActivity;
|
||||
import org.schabi.newpipe.error.ErrorInfo;
|
||||
import org.schabi.newpipe.error.UserAction;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.InputStream;
|
||||
@@ -48,7 +48,7 @@ public final class CheckForNewAppVersion {
|
||||
|
||||
private static final String GITHUB_APK_SHA1
|
||||
= "B0:2E:90:7C:1C:D6:FC:57:C3:35:F0:88:D0:8F:50:5F:94:E4:D2:15";
|
||||
private static final String NEWPIPE_API_URL = "https://newpipe.schabi.org/api/data.json";
|
||||
private static final String NEWPIPE_API_URL = "https://newpipe.net/api/data.json";
|
||||
|
||||
/**
|
||||
* Method to get the APK's SHA1 key. See https://stackoverflow.com/questions/9293019/#22506133.
|
||||
@@ -63,9 +63,8 @@ public final class CheckForNewAppVersion {
|
||||
packageInfo = application.getPackageManager().getPackageInfo(
|
||||
application.getPackageName(), PackageManager.GET_SIGNATURES);
|
||||
} catch (final PackageManager.NameNotFoundException e) {
|
||||
ErrorActivity.reportError(application, e, null, null,
|
||||
ErrorInfo.make(UserAction.SOMETHING_ELSE, "none",
|
||||
"Could not find package info", R.string.app_ui_crash));
|
||||
ErrorActivity.reportError(application, new ErrorInfo(e,
|
||||
UserAction.CHECK_FOR_NEW_APP_VERSION, "Could not find package info"));
|
||||
return "";
|
||||
}
|
||||
|
||||
@@ -77,9 +76,8 @@ public final class CheckForNewAppVersion {
|
||||
final CertificateFactory cf = CertificateFactory.getInstance("X509");
|
||||
c = (X509Certificate) cf.generateCertificate(input);
|
||||
} catch (final CertificateException e) {
|
||||
ErrorActivity.reportError(application, e, null, null,
|
||||
ErrorInfo.make(UserAction.SOMETHING_ELSE, "none",
|
||||
"Certificate error", R.string.app_ui_crash));
|
||||
ErrorActivity.reportError(application, new ErrorInfo(e,
|
||||
UserAction.CHECK_FOR_NEW_APP_VERSION, "Certificate error"));
|
||||
return "";
|
||||
}
|
||||
|
||||
@@ -88,9 +86,8 @@ public final class CheckForNewAppVersion {
|
||||
final byte[] publicKey = md.digest(c.getEncoded());
|
||||
return byte2HexFormatted(publicKey);
|
||||
} catch (NoSuchAlgorithmException | CertificateEncodingException e) {
|
||||
ErrorActivity.reportError(application, e, null, null,
|
||||
ErrorInfo.make(UserAction.SOMETHING_ELSE, "none",
|
||||
"Could not retrieve SHA1 key", R.string.app_ui_crash));
|
||||
ErrorActivity.reportError(application, new ErrorInfo(e,
|
||||
UserAction.CHECK_FOR_NEW_APP_VERSION, "Could not retrieve SHA1 key"));
|
||||
return "";
|
||||
}
|
||||
}
|
||||
@@ -132,7 +129,13 @@ public final class CheckForNewAppVersion {
|
||||
|
||||
if (BuildConfig.VERSION_CODE < versionCode) {
|
||||
// A pending intent to open the apk location url in the browser.
|
||||
final Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(apkLocationUrl));
|
||||
final Intent viewIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(apkLocationUrl));
|
||||
|
||||
final Intent intent = new Intent(Intent.ACTION_CHOOSER);
|
||||
intent.putExtra(Intent.EXTRA_INTENT, viewIntent);
|
||||
intent.putExtra(Intent.EXTRA_TITLE, R.string.open_with);
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
|
||||
final PendingIntent pendingIntent
|
||||
= PendingIntent.getActivity(application, 0, intent, 0);
|
||||
|
||||
@@ -170,6 +173,7 @@ public final class CheckForNewAppVersion {
|
||||
@Nullable
|
||||
public static Disposable checkNewVersion(@NonNull final App app) {
|
||||
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.
|
||||
@@ -177,31 +181,52 @@ public final class CheckForNewAppVersion {
|
||||
return null;
|
||||
}
|
||||
|
||||
return Maybe
|
||||
.fromCallable(() -> {
|
||||
if (!isConnected(app)) {
|
||||
return null;
|
||||
}
|
||||
// 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;
|
||||
}
|
||||
|
||||
// Make a network request to get latest NewPipe data.
|
||||
return DownloaderImpl.getInstance().get(NEWPIPE_API_URL).responseBody();
|
||||
})
|
||||
return Maybe
|
||||
.fromCallable(() -> {
|
||||
if (!isConnected(app)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
||||
// Parse the json from the response.
|
||||
try {
|
||||
final JsonObject githubStableObject = JsonParser.object()
|
||||
.from(response).getObject("flavors").getObject("github")
|
||||
.getObject("stable");
|
||||
.from(response.responseBody()).getObject("flavors")
|
||||
.getObject("github").getObject("stable");
|
||||
|
||||
final String versionName = githubStableObject
|
||||
.getString("version");
|
||||
.getString("version");
|
||||
final int versionCode = githubStableObject
|
||||
.getInt("version_code");
|
||||
.getInt("version_code");
|
||||
final String apkLocationUrl = githubStableObject
|
||||
.getString("apk");
|
||||
.getString("apk");
|
||||
|
||||
compareAppVersionAndShowNotification(app, versionName,
|
||||
apkLocationUrl, versionCode);
|
||||
|
||||
@@ -2,11 +2,12 @@ package org.schabi.newpipe;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Build;
|
||||
import androidx.preference.PreferenceManager;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.preference.PreferenceManager;
|
||||
|
||||
import org.schabi.newpipe.error.ReCaptchaActivity;
|
||||
import org.schabi.newpipe.extractor.downloader.Downloader;
|
||||
import org.schabi.newpipe.extractor.downloader.Request;
|
||||
import org.schabi.newpipe.extractor.downloader.Response;
|
||||
@@ -43,7 +44,7 @@ import static org.schabi.newpipe.MainActivity.DEBUG;
|
||||
|
||||
public final class DownloaderImpl extends Downloader {
|
||||
public static final String USER_AGENT
|
||||
= "Mozilla/5.0 (Windows NT 10.0; WOW64; rv:68.0) Gecko/20100101 Firefox/68.0";
|
||||
= "Mozilla/5.0 (Windows NT 10.0; rv:78.0) Gecko/20100101 Firefox/78.0";
|
||||
public static final String YOUTUBE_RESTRICTED_MODE_COOKIE_KEY
|
||||
= "youtube_restricted_mode_key";
|
||||
public static final String YOUTUBE_RESTRICTED_MODE_COOKIE = "PREF=f2=8000000";
|
||||
|
||||
@@ -4,6 +4,7 @@ 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;
|
||||
|
||||
@@ -39,17 +39,14 @@ import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.AdapterView;
|
||||
import android.widget.ArrayAdapter;
|
||||
import android.widget.Button;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.Spinner;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
import androidx.appcompat.app.ActionBarDrawerToggle;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.core.app.ActivityCompat;
|
||||
import androidx.core.view.GravityCompat;
|
||||
import androidx.drawerlayout.widget.DrawerLayout;
|
||||
import androidx.fragment.app.Fragment;
|
||||
@@ -57,8 +54,13 @@ import androidx.fragment.app.FragmentManager;
|
||||
import androidx.preference.PreferenceManager;
|
||||
|
||||
import com.google.android.material.bottomsheet.BottomSheetBehavior;
|
||||
import com.google.android.material.navigation.NavigationView;
|
||||
|
||||
import org.schabi.newpipe.databinding.ActivityMainBinding;
|
||||
import org.schabi.newpipe.databinding.DrawerHeaderBinding;
|
||||
import org.schabi.newpipe.databinding.DrawerLayoutBinding;
|
||||
import org.schabi.newpipe.databinding.InstanceSpinnerLayoutBinding;
|
||||
import org.schabi.newpipe.databinding.ToolbarLayoutBinding;
|
||||
import org.schabi.newpipe.error.ErrorActivity;
|
||||
import org.schabi.newpipe.extractor.NewPipe;
|
||||
import org.schabi.newpipe.extractor.StreamingService;
|
||||
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||
@@ -67,11 +69,10 @@ import org.schabi.newpipe.fragments.BackPressable;
|
||||
import org.schabi.newpipe.fragments.MainFragment;
|
||||
import org.schabi.newpipe.fragments.detail.VideoDetailFragment;
|
||||
import org.schabi.newpipe.fragments.list.search.SearchFragment;
|
||||
import org.schabi.newpipe.player.VideoPlayer;
|
||||
import org.schabi.newpipe.player.Player;
|
||||
import org.schabi.newpipe.player.event.OnKeyDownListener;
|
||||
import org.schabi.newpipe.player.helper.PlayerHolder;
|
||||
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
||||
import org.schabi.newpipe.report.ErrorActivity;
|
||||
import org.schabi.newpipe.util.Constants;
|
||||
import org.schabi.newpipe.util.DeviceUtils;
|
||||
import org.schabi.newpipe.util.KioskTranslator;
|
||||
@@ -96,15 +97,14 @@ public class MainActivity extends AppCompatActivity {
|
||||
private static final String TAG = "MainActivity";
|
||||
public static final boolean DEBUG = !BuildConfig.BUILD_TYPE.equals("release");
|
||||
|
||||
private ActivityMainBinding mainBinding;
|
||||
private DrawerHeaderBinding drawerHeaderBinding;
|
||||
private DrawerLayoutBinding drawerLayoutBinding;
|
||||
private ToolbarLayoutBinding toolbarLayoutBinding;
|
||||
|
||||
private ActionBarDrawerToggle toggle;
|
||||
private DrawerLayout drawer;
|
||||
private NavigationView drawerItems;
|
||||
private ImageView headerServiceIcon;
|
||||
private TextView headerServiceView;
|
||||
private Button toggleServiceButton;
|
||||
|
||||
private boolean servicesShown = false;
|
||||
private ImageView serviceArrow;
|
||||
|
||||
private BroadcastReceiver broadcastReceiver;
|
||||
|
||||
@@ -129,7 +129,7 @@ public class MainActivity extends AppCompatActivity {
|
||||
+ "savedInstanceState = [" + savedInstanceState + "]");
|
||||
}
|
||||
|
||||
// enable TLS1.1/1.2 for kitkat devices, to fix download and play for mediaCCC sources
|
||||
// enable TLS1.1/1.2 for kitkat devices, to fix download and play for media.ccc.de sources
|
||||
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.KITKAT) {
|
||||
TLSSocketFactoryCompat.setAsDefault();
|
||||
}
|
||||
@@ -137,17 +137,23 @@ public class MainActivity extends AppCompatActivity {
|
||||
|
||||
assureCorrectAppLanguage(this);
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_main);
|
||||
|
||||
mainBinding = ActivityMainBinding.inflate(getLayoutInflater());
|
||||
drawerLayoutBinding = mainBinding.drawerLayout;
|
||||
drawerHeaderBinding = DrawerHeaderBinding.bind(drawerLayoutBinding.navigation
|
||||
.getHeaderView(0));
|
||||
toolbarLayoutBinding = mainBinding.toolbarLayout;
|
||||
setContentView(mainBinding.getRoot());
|
||||
|
||||
if (getSupportFragmentManager().getBackStackEntryCount() == 0) {
|
||||
initFragments();
|
||||
}
|
||||
|
||||
setSupportActionBar(findViewById(R.id.toolbar));
|
||||
setSupportActionBar(toolbarLayoutBinding.toolbar);
|
||||
try {
|
||||
setupDrawer();
|
||||
} catch (final Exception e) {
|
||||
ErrorActivity.reportUiError(this, e);
|
||||
ErrorActivity.reportUiErrorInSnackbar(this, "Setting up drawer", e);
|
||||
}
|
||||
|
||||
if (DeviceUtils.isTv(this)) {
|
||||
@@ -157,10 +163,6 @@ public class MainActivity extends AppCompatActivity {
|
||||
}
|
||||
|
||||
private void setupDrawer() throws Exception {
|
||||
final Toolbar toolbar = findViewById(R.id.toolbar);
|
||||
drawer = findViewById(R.id.drawer_layout);
|
||||
drawerItems = findViewById(R.id.navigation);
|
||||
|
||||
//Tabs
|
||||
final int currentServiceId = ServiceHelper.getSelectedServiceId(this);
|
||||
final StreamingService service = NewPipe.getService(currentServiceId);
|
||||
@@ -168,43 +170,43 @@ public class MainActivity extends AppCompatActivity {
|
||||
int kioskId = 0;
|
||||
|
||||
for (final String ks : service.getKioskList().getAvailableKiosks()) {
|
||||
drawerItems.getMenu()
|
||||
drawerLayoutBinding.navigation.getMenu()
|
||||
.add(R.id.menu_tabs_group, kioskId, 0, KioskTranslator
|
||||
.getTranslatedKioskName(ks, this))
|
||||
.setIcon(KioskTranslator.getKioskIcon(ks, this));
|
||||
kioskId++;
|
||||
}
|
||||
|
||||
drawerItems.getMenu()
|
||||
drawerLayoutBinding.navigation.getMenu()
|
||||
.add(R.id.menu_tabs_group, ITEM_ID_SUBSCRIPTIONS, ORDER,
|
||||
R.string.tab_subscriptions)
|
||||
.setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.ic_channel));
|
||||
drawerItems.getMenu()
|
||||
drawerLayoutBinding.navigation.getMenu()
|
||||
.add(R.id.menu_tabs_group, ITEM_ID_FEED, ORDER, R.string.fragment_feed_title)
|
||||
.setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.ic_rss));
|
||||
drawerItems.getMenu()
|
||||
drawerLayoutBinding.navigation.getMenu()
|
||||
.add(R.id.menu_tabs_group, ITEM_ID_BOOKMARKS, ORDER, R.string.tab_bookmarks)
|
||||
.setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.ic_bookmark));
|
||||
drawerItems.getMenu()
|
||||
drawerLayoutBinding.navigation.getMenu()
|
||||
.add(R.id.menu_tabs_group, ITEM_ID_DOWNLOADS, ORDER, R.string.downloads)
|
||||
.setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.ic_file_download));
|
||||
drawerItems.getMenu()
|
||||
drawerLayoutBinding.navigation.getMenu()
|
||||
.add(R.id.menu_tabs_group, ITEM_ID_HISTORY, ORDER, R.string.action_history)
|
||||
.setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.ic_history));
|
||||
|
||||
//Settings and About
|
||||
drawerItems.getMenu()
|
||||
drawerLayoutBinding.navigation.getMenu()
|
||||
.add(R.id.menu_options_about_group, ITEM_ID_SETTINGS, ORDER, R.string.settings)
|
||||
.setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.ic_settings));
|
||||
drawerItems.getMenu()
|
||||
drawerLayoutBinding.navigation.getMenu()
|
||||
.add(R.id.menu_options_about_group, ITEM_ID_ABOUT, ORDER, R.string.tab_about)
|
||||
.setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.ic_info_outline));
|
||||
|
||||
toggle = new ActionBarDrawerToggle(this, drawer, toolbar, R.string.drawer_open,
|
||||
R.string.drawer_close);
|
||||
toggle = new ActionBarDrawerToggle(this, mainBinding.getRoot(),
|
||||
toolbarLayoutBinding.toolbar, R.string.drawer_open, R.string.drawer_close);
|
||||
toggle.syncState();
|
||||
drawer.addDrawerListener(toggle);
|
||||
drawer.addDrawerListener(new DrawerLayout.SimpleDrawerListener() {
|
||||
mainBinding.getRoot().addDrawerListener(toggle);
|
||||
mainBinding.getRoot().addDrawerListener(new DrawerLayout.SimpleDrawerListener() {
|
||||
private int lastService;
|
||||
|
||||
@Override
|
||||
@@ -218,12 +220,12 @@ public class MainActivity extends AppCompatActivity {
|
||||
toggleServices();
|
||||
}
|
||||
if (lastService != ServiceHelper.getSelectedServiceId(MainActivity.this)) {
|
||||
new Handler(Looper.getMainLooper()).post(MainActivity.this::recreate);
|
||||
ActivityCompat.recreate(MainActivity.this);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
drawerItems.setNavigationItemSelectedListener(this::drawerItemSelected);
|
||||
drawerLayoutBinding.navigation.setNavigationItemSelectedListener(this::drawerItemSelected);
|
||||
setupDrawerHeader();
|
||||
}
|
||||
|
||||
@@ -236,7 +238,7 @@ public class MainActivity extends AppCompatActivity {
|
||||
try {
|
||||
tabSelected(item);
|
||||
} catch (final Exception e) {
|
||||
ErrorActivity.reportUiError(this, e);
|
||||
ErrorActivity.reportUiErrorInSnackbar(this, "Selecting main page tab", e);
|
||||
}
|
||||
break;
|
||||
case R.id.menu_options_about_group:
|
||||
@@ -246,15 +248,17 @@ public class MainActivity extends AppCompatActivity {
|
||||
return false;
|
||||
}
|
||||
|
||||
drawer.closeDrawers();
|
||||
mainBinding.getRoot().closeDrawers();
|
||||
return true;
|
||||
}
|
||||
|
||||
private void changeService(final MenuItem item) {
|
||||
drawerItems.getMenu().getItem(ServiceHelper.getSelectedServiceId(this))
|
||||
drawerLayoutBinding.navigation.getMenu()
|
||||
.getItem(ServiceHelper.getSelectedServiceId(this))
|
||||
.setChecked(false);
|
||||
ServiceHelper.setSelectedServiceId(this, item.getItemId());
|
||||
drawerItems.getMenu().getItem(ServiceHelper.getSelectedServiceId(this))
|
||||
drawerLayoutBinding.navigation.getMenu()
|
||||
.getItem(ServiceHelper.getSelectedServiceId(this))
|
||||
.setChecked(true);
|
||||
}
|
||||
|
||||
@@ -306,26 +310,19 @@ public class MainActivity extends AppCompatActivity {
|
||||
}
|
||||
|
||||
private void setupDrawerHeader() {
|
||||
final NavigationView navigationView = findViewById(R.id.navigation);
|
||||
final View hView = navigationView.getHeaderView(0);
|
||||
|
||||
serviceArrow = hView.findViewById(R.id.drawer_arrow);
|
||||
headerServiceIcon = hView.findViewById(R.id.drawer_header_service_icon);
|
||||
headerServiceView = hView.findViewById(R.id.drawer_header_service_view);
|
||||
toggleServiceButton = hView.findViewById(R.id.drawer_header_action_button);
|
||||
toggleServiceButton.setOnClickListener(view -> toggleServices());
|
||||
drawerHeaderBinding.drawerHeaderActionButton.setOnClickListener(view -> toggleServices());
|
||||
|
||||
// If the current app name is bigger than the default "NewPipe" (7 chars),
|
||||
// let the text view grow a little more as well.
|
||||
if (getString(R.string.app_name).length() > "NewPipe".length()) {
|
||||
final TextView headerTitle = hView.findViewById(R.id.drawer_header_newpipe_title);
|
||||
final ViewGroup.LayoutParams layoutParams = headerTitle.getLayoutParams();
|
||||
final ViewGroup.LayoutParams layoutParams =
|
||||
drawerHeaderBinding.drawerHeaderNewpipeTitle.getLayoutParams();
|
||||
layoutParams.width = ViewGroup.LayoutParams.WRAP_CONTENT;
|
||||
headerTitle.setLayoutParams(layoutParams);
|
||||
headerTitle.setMaxLines(2);
|
||||
headerTitle.setMinWidth(getResources()
|
||||
drawerHeaderBinding.drawerHeaderNewpipeTitle.setLayoutParams(layoutParams);
|
||||
drawerHeaderBinding.drawerHeaderNewpipeTitle.setMaxLines(2);
|
||||
drawerHeaderBinding.drawerHeaderNewpipeTitle.setMinWidth(getResources()
|
||||
.getDimensionPixelSize(R.dimen.drawer_header_newpipe_title_default_width));
|
||||
headerTitle.setMaxWidth(getResources()
|
||||
drawerHeaderBinding.drawerHeaderNewpipeTitle.setMaxWidth(getResources()
|
||||
.getDimensionPixelSize(R.dimen.drawer_header_newpipe_title_max_width));
|
||||
}
|
||||
}
|
||||
@@ -333,9 +330,9 @@ public class MainActivity extends AppCompatActivity {
|
||||
private void toggleServices() {
|
||||
servicesShown = !servicesShown;
|
||||
|
||||
drawerItems.getMenu().removeGroup(R.id.menu_services_group);
|
||||
drawerItems.getMenu().removeGroup(R.id.menu_tabs_group);
|
||||
drawerItems.getMenu().removeGroup(R.id.menu_options_about_group);
|
||||
drawerLayoutBinding.navigation.getMenu().removeGroup(R.id.menu_services_group);
|
||||
drawerLayoutBinding.navigation.getMenu().removeGroup(R.id.menu_tabs_group);
|
||||
drawerLayoutBinding.navigation.getMenu().removeGroup(R.id.menu_options_about_group);
|
||||
|
||||
if (servicesShown) {
|
||||
showServices();
|
||||
@@ -343,19 +340,19 @@ public class MainActivity extends AppCompatActivity {
|
||||
try {
|
||||
showTabs();
|
||||
} catch (final Exception e) {
|
||||
ErrorActivity.reportUiError(this, e);
|
||||
ErrorActivity.reportUiErrorInSnackbar(this, "Showing main page tabs", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void showServices() {
|
||||
serviceArrow.setImageResource(R.drawable.ic_arrow_drop_up_white_24dp);
|
||||
drawerHeaderBinding.drawerArrow.setImageResource(R.drawable.ic_arrow_drop_up_white_24dp);
|
||||
|
||||
for (final StreamingService s : NewPipe.getServices()) {
|
||||
final String title = s.getServiceInfo().getName()
|
||||
+ (ServiceHelper.isBeta(s) ? " (beta)" : "");
|
||||
|
||||
final MenuItem menuItem = drawerItems.getMenu()
|
||||
final MenuItem menuItem = drawerLayoutBinding.navigation.getMenu()
|
||||
.add(R.id.menu_services_group, s.getServiceId(), ORDER, title)
|
||||
.setIcon(ServiceHelper.getIcon(s.getServiceId()));
|
||||
|
||||
@@ -364,21 +361,22 @@ public class MainActivity extends AppCompatActivity {
|
||||
enhancePeertubeMenu(s, menuItem);
|
||||
}
|
||||
}
|
||||
drawerItems.getMenu().getItem(ServiceHelper.getSelectedServiceId(this))
|
||||
drawerLayoutBinding.navigation.getMenu()
|
||||
.getItem(ServiceHelper.getSelectedServiceId(this))
|
||||
.setChecked(true);
|
||||
}
|
||||
|
||||
private void enhancePeertubeMenu(final StreamingService s, final MenuItem menuItem) {
|
||||
final PeertubeInstance currentInstace = PeertubeHelper.getCurrentInstance();
|
||||
menuItem.setTitle(currentInstace.getName() + (ServiceHelper.isBeta(s) ? " (beta)" : ""));
|
||||
final Spinner spinner = (Spinner) LayoutInflater.from(this)
|
||||
.inflate(R.layout.instance_spinner_layout, null);
|
||||
final PeertubeInstance currentInstance = PeertubeHelper.getCurrentInstance();
|
||||
menuItem.setTitle(currentInstance.getName() + (ServiceHelper.isBeta(s) ? " (beta)" : ""));
|
||||
final Spinner spinner = InstanceSpinnerLayoutBinding.inflate(LayoutInflater.from(this))
|
||||
.getRoot();
|
||||
final List<PeertubeInstance> instances = PeertubeHelper.getInstanceList(this);
|
||||
final List<String> items = new ArrayList<>();
|
||||
int defaultSelect = 0;
|
||||
for (final PeertubeInstance instance : instances) {
|
||||
items.add(instance.getName());
|
||||
if (instance.getUrl().equals(currentInstace.getUrl())) {
|
||||
if (instance.getUrl().equals(currentInstance.getUrl())) {
|
||||
defaultSelect = items.size() - 1;
|
||||
}
|
||||
}
|
||||
@@ -397,7 +395,7 @@ public class MainActivity extends AppCompatActivity {
|
||||
}
|
||||
PeertubeHelper.selectInstance(newInstance, getApplicationContext());
|
||||
changeService(menuItem);
|
||||
drawer.closeDrawers();
|
||||
mainBinding.getRoot().closeDrawers();
|
||||
new Handler(Looper.getMainLooper()).postDelayed(() -> {
|
||||
getSupportFragmentManager().popBackStack(null,
|
||||
FragmentManager.POP_BACK_STACK_INCLUSIVE);
|
||||
@@ -414,7 +412,7 @@ public class MainActivity extends AppCompatActivity {
|
||||
}
|
||||
|
||||
private void showTabs() throws ExtractionException {
|
||||
serviceArrow.setImageResource(R.drawable.ic_arrow_drop_down_white_24dp);
|
||||
drawerHeaderBinding.drawerArrow.setImageResource(R.drawable.ic_arrow_drop_down_white_24dp);
|
||||
|
||||
//Tabs
|
||||
final int currentServiceId = ServiceHelper.getSelectedServiceId(this);
|
||||
@@ -423,34 +421,34 @@ public class MainActivity extends AppCompatActivity {
|
||||
int kioskId = 0;
|
||||
|
||||
for (final String ks : service.getKioskList().getAvailableKiosks()) {
|
||||
drawerItems.getMenu()
|
||||
drawerLayoutBinding.navigation.getMenu()
|
||||
.add(R.id.menu_tabs_group, kioskId, ORDER,
|
||||
KioskTranslator.getTranslatedKioskName(ks, this))
|
||||
.setIcon(KioskTranslator.getKioskIcon(ks, this));
|
||||
kioskId++;
|
||||
}
|
||||
|
||||
drawerItems.getMenu()
|
||||
drawerLayoutBinding.navigation.getMenu()
|
||||
.add(R.id.menu_tabs_group, ITEM_ID_SUBSCRIPTIONS, ORDER, R.string.tab_subscriptions)
|
||||
.setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.ic_channel));
|
||||
drawerItems.getMenu()
|
||||
drawerLayoutBinding.navigation.getMenu()
|
||||
.add(R.id.menu_tabs_group, ITEM_ID_FEED, ORDER, R.string.fragment_feed_title)
|
||||
.setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.ic_rss));
|
||||
drawerItems.getMenu()
|
||||
drawerLayoutBinding.navigation.getMenu()
|
||||
.add(R.id.menu_tabs_group, ITEM_ID_BOOKMARKS, ORDER, R.string.tab_bookmarks)
|
||||
.setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.ic_bookmark));
|
||||
drawerItems.getMenu()
|
||||
drawerLayoutBinding.navigation.getMenu()
|
||||
.add(R.id.menu_tabs_group, ITEM_ID_DOWNLOADS, ORDER, R.string.downloads)
|
||||
.setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.ic_file_download));
|
||||
drawerItems.getMenu()
|
||||
drawerLayoutBinding.navigation.getMenu()
|
||||
.add(R.id.menu_tabs_group, ITEM_ID_HISTORY, ORDER, R.string.action_history)
|
||||
.setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.ic_history));
|
||||
|
||||
//Settings and About
|
||||
drawerItems.getMenu()
|
||||
drawerLayoutBinding.navigation.getMenu()
|
||||
.add(R.id.menu_options_about_group, ITEM_ID_SETTINGS, ORDER, R.string.settings)
|
||||
.setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.ic_settings));
|
||||
drawerItems.getMenu()
|
||||
drawerLayoutBinding.navigation.getMenu()
|
||||
.add(R.id.menu_options_about_group, ITEM_ID_ABOUT, ORDER, R.string.tab_about)
|
||||
.setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.ic_info_outline));
|
||||
}
|
||||
@@ -470,24 +468,26 @@ public class MainActivity extends AppCompatActivity {
|
||||
protected void onResume() {
|
||||
assureCorrectAppLanguage(this);
|
||||
// Change the date format to match the selected language on resume
|
||||
Localization.init(getApplicationContext());
|
||||
Localization.initPrettyTime(Localization.resolvePrettyTime(getApplicationContext()));
|
||||
super.onResume();
|
||||
|
||||
// Close drawer on return, and don't show animation,
|
||||
// so it looks like the drawer isn't open when the user returns to MainActivity
|
||||
drawer.closeDrawer(GravityCompat.START, false);
|
||||
mainBinding.getRoot().closeDrawer(GravityCompat.START, false);
|
||||
try {
|
||||
final int selectedServiceId = ServiceHelper.getSelectedServiceId(this);
|
||||
final String selectedServiceName = NewPipe.getService(selectedServiceId)
|
||||
.getServiceInfo().getName();
|
||||
headerServiceView.setText(selectedServiceName);
|
||||
headerServiceIcon.setImageResource(ServiceHelper.getIcon(selectedServiceId));
|
||||
drawerHeaderBinding.drawerHeaderServiceView.setText(selectedServiceName);
|
||||
drawerHeaderBinding.drawerHeaderServiceIcon.setImageResource(ServiceHelper
|
||||
.getIcon(selectedServiceId));
|
||||
|
||||
headerServiceView.post(() -> headerServiceView.setSelected(true));
|
||||
toggleServiceButton.setContentDescription(
|
||||
drawerHeaderBinding.drawerHeaderServiceView.post(() -> drawerHeaderBinding
|
||||
.drawerHeaderServiceView.setSelected(true));
|
||||
drawerHeaderBinding.drawerHeaderActionButton.setContentDescription(
|
||||
getString(R.string.drawer_header_description) + selectedServiceName);
|
||||
} catch (final Exception e) {
|
||||
ErrorActivity.reportUiError(this, e);
|
||||
ErrorActivity.reportUiErrorInSnackbar(this, "Setting up service toggle", e);
|
||||
}
|
||||
|
||||
final SharedPreferences sharedPreferences
|
||||
@@ -497,10 +497,7 @@ public class MainActivity extends AppCompatActivity {
|
||||
Log.d(TAG, "Theme has changed, recreating activity...");
|
||||
}
|
||||
sharedPreferences.edit().putBoolean(Constants.KEY_THEME_CHANGE, false).apply();
|
||||
// https://stackoverflow.com/questions/10844112/
|
||||
// Briefly, let the activity resume
|
||||
// properly posting the recreate call to end of the message queue
|
||||
new Handler(Looper.getMainLooper()).post(MainActivity.this::recreate);
|
||||
ActivityCompat.recreate(this);
|
||||
}
|
||||
|
||||
if (sharedPreferences.getBoolean(Constants.KEY_MAIN_PAGE_CHANGE, false)) {
|
||||
@@ -513,7 +510,8 @@ public class MainActivity extends AppCompatActivity {
|
||||
|
||||
final boolean isHistoryEnabled = sharedPreferences.getBoolean(
|
||||
getString(R.string.enable_watch_history_key), true);
|
||||
drawerItems.getMenu().findItem(ITEM_ID_HISTORY).setVisible(isHistoryEnabled);
|
||||
drawerLayoutBinding.navigation.getMenu().findItem(ITEM_ID_HISTORY)
|
||||
.setVisible(isHistoryEnabled);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -557,9 +555,8 @@ public class MainActivity extends AppCompatActivity {
|
||||
}
|
||||
|
||||
if (DeviceUtils.isTv(this)) {
|
||||
final View drawerPanel = findViewById(R.id.navigation);
|
||||
if (drawer.isDrawerOpen(drawerPanel)) {
|
||||
drawer.closeDrawers();
|
||||
if (mainBinding.getRoot().isDrawerOpen(drawerLayoutBinding.navigation)) {
|
||||
mainBinding.getRoot().closeDrawers();
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -585,9 +582,7 @@ public class MainActivity extends AppCompatActivity {
|
||||
// delegate the back press to it
|
||||
if (fragmentPlayer instanceof BackPressable) {
|
||||
if (!((BackPressable) fragmentPlayer).onBackPressed()) {
|
||||
final FrameLayout bottomSheetLayout =
|
||||
findViewById(R.id.fragment_player_holder);
|
||||
BottomSheetBehavior.from(bottomSheetLayout)
|
||||
BottomSheetBehavior.from(mainBinding.fragmentPlayerHolder)
|
||||
.setState(BottomSheetBehavior.STATE_COLLAPSED);
|
||||
}
|
||||
return;
|
||||
@@ -670,8 +665,7 @@ public class MainActivity extends AppCompatActivity {
|
||||
final Fragment fragment
|
||||
= getSupportFragmentManager().findFragmentById(R.id.fragment_holder);
|
||||
if (!(fragment instanceof SearchFragment)) {
|
||||
findViewById(R.id.toolbar).findViewById(R.id.toolbar_search_container)
|
||||
.setVisibility(View.GONE);
|
||||
toolbarLayoutBinding.toolbarSearchContainer.getRoot().setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
final ActionBar actionBar = getSupportActionBar();
|
||||
@@ -685,19 +679,16 @@ public class MainActivity extends AppCompatActivity {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(final MenuItem item) {
|
||||
public boolean onOptionsItemSelected(@NonNull final MenuItem item) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "onOptionsItemSelected() called with: item = [" + item + "]");
|
||||
}
|
||||
final int id = item.getItemId();
|
||||
|
||||
switch (id) {
|
||||
case android.R.id.home:
|
||||
onHomeButtonPressed();
|
||||
return true;
|
||||
default:
|
||||
return super.onOptionsItemSelected(item);
|
||||
if (item.getItemId() == android.R.id.home) {
|
||||
onHomeButtonPressed();
|
||||
return true;
|
||||
}
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
@@ -732,21 +723,20 @@ public class MainActivity extends AppCompatActivity {
|
||||
return;
|
||||
}
|
||||
|
||||
final Toolbar toolbar = findViewById(R.id.toolbar);
|
||||
|
||||
final Fragment fragment = getSupportFragmentManager()
|
||||
.findFragmentById(R.id.fragment_holder);
|
||||
if (fragment instanceof MainFragment) {
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(false);
|
||||
if (toggle != null) {
|
||||
toggle.syncState();
|
||||
toolbar.setNavigationOnClickListener(v -> drawer.openDrawer(GravityCompat.START));
|
||||
drawer.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNDEFINED);
|
||||
toolbarLayoutBinding.toolbar.setNavigationOnClickListener(v -> mainBinding.getRoot()
|
||||
.openDrawer(GravityCompat.START));
|
||||
mainBinding.getRoot().setDrawerLockMode(DrawerLayout.LOCK_MODE_UNDEFINED);
|
||||
}
|
||||
} else {
|
||||
drawer.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED);
|
||||
mainBinding.getRoot().setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED);
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
toolbar.setNavigationOnClickListener(v -> onHomeButtonPressed());
|
||||
toolbarLayoutBinding.toolbar.setNavigationOnClickListener(v -> onHomeButtonPressed());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -770,7 +760,7 @@ public class MainActivity extends AppCompatActivity {
|
||||
switch (linkType) {
|
||||
case STREAM:
|
||||
final String intentCacheKey = intent.getStringExtra(
|
||||
VideoPlayer.PLAY_QUEUE_KEY);
|
||||
Player.PLAY_QUEUE_KEY);
|
||||
final PlayQueue playQueue = intentCacheKey != null
|
||||
? SerializedCache.getInstance()
|
||||
.take(intentCacheKey, PlayQueue.class)
|
||||
@@ -806,7 +796,7 @@ public class MainActivity extends AppCompatActivity {
|
||||
NavigationHelper.gotoMainFragment(getSupportFragmentManager());
|
||||
}
|
||||
} catch (final Exception e) {
|
||||
ErrorActivity.reportUiError(this, e);
|
||||
ErrorActivity.reportUiErrorInSnackbar(this, "Handling intent", e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -854,9 +844,8 @@ public class MainActivity extends AppCompatActivity {
|
||||
}
|
||||
|
||||
private boolean bottomSheetHiddenOrCollapsed() {
|
||||
final FrameLayout bottomSheetLayout = findViewById(R.id.fragment_player_holder);
|
||||
final BottomSheetBehavior<FrameLayout> bottomSheetBehavior =
|
||||
BottomSheetBehavior.from(bottomSheetLayout);
|
||||
BottomSheetBehavior.from(mainBinding.fragmentPlayerHolder);
|
||||
|
||||
final int sheetState = bottomSheetBehavior.getState();
|
||||
return sheetState == BottomSheetBehavior.STATE_HIDDEN
|
||||
|
||||
28
app/src/main/java/org/schabi/newpipe/NewVersionManager.kt
Normal file
28
app/src/main/java/org/schabi/newpipe/NewVersionManager.kt
Normal file
@@ -0,0 +1,28 @@
|
||||
package org.schabi.newpipe
|
||||
|
||||
import java.time.Instant
|
||||
import java.time.ZonedDateTime
|
||||
import java.time.format.DateTimeFormatter
|
||||
|
||||
class NewVersionManager {
|
||||
|
||||
fun isExpired(expiry: Long): Boolean {
|
||||
return Instant.ofEpochSecond(expiry).isBefore(Instant.now())
|
||||
}
|
||||
|
||||
/**
|
||||
* Coerce expiry date time in between 6 hours and 72 hours from now
|
||||
*
|
||||
* @return Epoch second of expiry date time
|
||||
*/
|
||||
fun coerceExpiry(expiryString: String?): Long {
|
||||
val now = ZonedDateTime.now()
|
||||
return expiryString?.let {
|
||||
|
||||
var expiry = ZonedDateTime.from(DateTimeFormatter.RFC_1123_DATE_TIME.parse(expiryString))
|
||||
expiry = maxOf(expiry, now.plusHours(6))
|
||||
expiry = minOf(expiry, now.plusHours(72))
|
||||
expiry.toEpochSecond()
|
||||
} ?: now.plusHours(6).toEpochSecond()
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,6 @@ import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.Button;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.RadioButton;
|
||||
import android.widget.RadioGroup;
|
||||
import android.widget.Toast;
|
||||
@@ -26,20 +25,37 @@ import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.appcompat.content.res.AppCompatResources;
|
||||
import androidx.core.app.NotificationCompat;
|
||||
import androidx.core.app.ServiceCompat;
|
||||
import androidx.core.widget.TextViewCompat;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
import androidx.preference.PreferenceManager;
|
||||
|
||||
import org.schabi.newpipe.databinding.ListRadioIconItemBinding;
|
||||
import org.schabi.newpipe.databinding.SingleChoiceDialogViewBinding;
|
||||
import org.schabi.newpipe.download.DownloadDialog;
|
||||
import org.schabi.newpipe.error.ErrorActivity;
|
||||
import org.schabi.newpipe.error.ErrorInfo;
|
||||
import org.schabi.newpipe.error.ReCaptchaActivity;
|
||||
import org.schabi.newpipe.error.UserAction;
|
||||
import org.schabi.newpipe.extractor.Info;
|
||||
import org.schabi.newpipe.extractor.NewPipe;
|
||||
import org.schabi.newpipe.extractor.StreamingService;
|
||||
import org.schabi.newpipe.extractor.StreamingService.LinkType;
|
||||
import org.schabi.newpipe.extractor.channel.ChannelInfo;
|
||||
import org.schabi.newpipe.extractor.exceptions.AgeRestrictedContentException;
|
||||
import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException;
|
||||
import org.schabi.newpipe.extractor.exceptions.ContentNotSupportedException;
|
||||
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||
import org.schabi.newpipe.extractor.exceptions.GeographicRestrictionException;
|
||||
import org.schabi.newpipe.extractor.exceptions.PaidContentException;
|
||||
import org.schabi.newpipe.extractor.exceptions.PrivateContentException;
|
||||
import org.schabi.newpipe.extractor.exceptions.ReCaptchaException;
|
||||
import org.schabi.newpipe.extractor.exceptions.SoundCloudGoPlusContentException;
|
||||
import org.schabi.newpipe.extractor.exceptions.YoutubeMusicPremiumContentException;
|
||||
import org.schabi.newpipe.extractor.playlist.PlaylistInfo;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||
import org.schabi.newpipe.extractor.stream.VideoStream;
|
||||
import org.schabi.newpipe.ktx.ExceptionUtils;
|
||||
import org.schabi.newpipe.player.MainPlayer;
|
||||
import org.schabi.newpipe.player.helper.PlayerHelper;
|
||||
import org.schabi.newpipe.player.helper.PlayerHolder;
|
||||
@@ -47,7 +63,6 @@ import org.schabi.newpipe.player.playqueue.ChannelPlayQueue;
|
||||
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
||||
import org.schabi.newpipe.player.playqueue.PlaylistPlayQueue;
|
||||
import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
|
||||
import org.schabi.newpipe.report.UserAction;
|
||||
import org.schabi.newpipe.util.Constants;
|
||||
import org.schabi.newpipe.util.DeviceUtils;
|
||||
import org.schabi.newpipe.util.ExtractorHelper;
|
||||
@@ -82,13 +97,6 @@ import static org.schabi.newpipe.util.ThemeHelper.resolveResourceIdFromAttr;
|
||||
* Get the url from the intent and open it in the chosen preferred player.
|
||||
*/
|
||||
public class RouterActivity extends AppCompatActivity {
|
||||
public static final String INTERNAL_ROUTE_KEY = "internalRoute";
|
||||
/**
|
||||
* Removes invisible separators (\p{Z}) and punctuation characters including
|
||||
* brackets (\p{P}). See http://www.regular-expressions.info/unicode.html for
|
||||
* more details.
|
||||
*/
|
||||
private static final String REGEX_REMOVE_FROM_URL = "[\\p{Z}\\p{P}]";
|
||||
protected final CompositeDisposable disposables = new CompositeDisposable();
|
||||
@State
|
||||
protected int currentServiceId = -1;
|
||||
@@ -98,7 +106,6 @@ public class RouterActivity extends AppCompatActivity {
|
||||
protected int selectedRadioPosition = -1;
|
||||
protected int selectedPreviously = -1;
|
||||
protected String currentUrl;
|
||||
protected boolean internalRoute = false;
|
||||
private StreamingService currentService;
|
||||
private boolean selectionIsDownload = false;
|
||||
|
||||
@@ -121,7 +128,7 @@ public class RouterActivity extends AppCompatActivity {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onSaveInstanceState(final Bundle outState) {
|
||||
protected void onSaveInstanceState(@NonNull final Bundle outState) {
|
||||
super.onSaveInstanceState(outState);
|
||||
Icepick.saveInstanceState(this, outState);
|
||||
}
|
||||
@@ -143,37 +150,79 @@ public class RouterActivity extends AppCompatActivity {
|
||||
private void handleUrl(final String url) {
|
||||
disposables.add(Observable
|
||||
.fromCallable(() -> {
|
||||
if (currentServiceId == -1) {
|
||||
currentService = NewPipe.getServiceByUrl(url);
|
||||
currentServiceId = currentService.getServiceId();
|
||||
currentLinkType = currentService.getLinkTypeByUrl(url);
|
||||
currentUrl = url;
|
||||
} else {
|
||||
currentService = NewPipe.getService(currentServiceId);
|
||||
}
|
||||
try {
|
||||
if (currentServiceId == -1) {
|
||||
currentService = NewPipe.getServiceByUrl(url);
|
||||
currentServiceId = currentService.getServiceId();
|
||||
currentLinkType = currentService.getLinkTypeByUrl(url);
|
||||
currentUrl = url;
|
||||
} else {
|
||||
currentService = NewPipe.getService(currentServiceId);
|
||||
}
|
||||
|
||||
return currentLinkType != LinkType.NONE;
|
||||
// return whether the url was found to be supported or not
|
||||
return currentLinkType != LinkType.NONE;
|
||||
} catch (final ExtractionException e) {
|
||||
// this can be reached only when the url is completely unsupported
|
||||
return false;
|
||||
}
|
||||
})
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(result -> {
|
||||
if (result) {
|
||||
.subscribe(isUrlSupported -> {
|
||||
if (isUrlSupported) {
|
||||
onSuccess();
|
||||
} else {
|
||||
showUnsupportedUrlDialog(url);
|
||||
}
|
||||
}, throwable -> handleError(throwable, url)));
|
||||
}, throwable -> handleError(this, new ErrorInfo(throwable,
|
||||
UserAction.SHARE_TO_NEWPIPE, "Getting service from url: " + url))));
|
||||
}
|
||||
|
||||
private void handleError(final Throwable throwable, final String url) {
|
||||
throwable.printStackTrace();
|
||||
/**
|
||||
* @param context the context. It will be {@code finish()}ed at the end of the handling if it is
|
||||
* an instance of {@link RouterActivity}.
|
||||
* @param errorInfo the error information
|
||||
*/
|
||||
private static void handleError(final Context context, final ErrorInfo errorInfo) {
|
||||
if (errorInfo.getThrowable() != null) {
|
||||
errorInfo.getThrowable().printStackTrace();
|
||||
}
|
||||
|
||||
if (throwable instanceof ExtractionException) {
|
||||
showUnsupportedUrlDialog(url);
|
||||
if (errorInfo.getThrowable() instanceof ReCaptchaException) {
|
||||
Toast.makeText(context, R.string.recaptcha_request_toast, Toast.LENGTH_LONG).show();
|
||||
// Starting ReCaptcha Challenge Activity
|
||||
final Intent intent = new Intent(context, ReCaptchaActivity.class);
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
context.startActivity(intent);
|
||||
} else if (errorInfo.getThrowable() != null
|
||||
&& ExceptionUtils.isNetworkRelated(errorInfo.getThrowable())) {
|
||||
Toast.makeText(context, R.string.network_error, Toast.LENGTH_LONG).show();
|
||||
} else if (errorInfo.getThrowable() instanceof AgeRestrictedContentException) {
|
||||
Toast.makeText(context, R.string.restricted_video_no_stream,
|
||||
Toast.LENGTH_LONG).show();
|
||||
} else if (errorInfo.getThrowable() instanceof GeographicRestrictionException) {
|
||||
Toast.makeText(context, R.string.georestricted_content, Toast.LENGTH_LONG).show();
|
||||
} else if (errorInfo.getThrowable() instanceof PaidContentException) {
|
||||
Toast.makeText(context, R.string.paid_content, Toast.LENGTH_LONG).show();
|
||||
} else if (errorInfo.getThrowable() instanceof PrivateContentException) {
|
||||
Toast.makeText(context, R.string.private_content, Toast.LENGTH_LONG).show();
|
||||
} else if (errorInfo.getThrowable() instanceof SoundCloudGoPlusContentException) {
|
||||
Toast.makeText(context, R.string.soundcloud_go_plus_content,
|
||||
Toast.LENGTH_LONG).show();
|
||||
} else if (errorInfo.getThrowable() instanceof YoutubeMusicPremiumContentException) {
|
||||
Toast.makeText(context, R.string.youtube_music_premium_content,
|
||||
Toast.LENGTH_LONG).show();
|
||||
} else if (errorInfo.getThrowable() instanceof ContentNotAvailableException) {
|
||||
Toast.makeText(context, R.string.content_not_available, Toast.LENGTH_LONG).show();
|
||||
} else if (errorInfo.getThrowable() instanceof ContentNotSupportedException) {
|
||||
Toast.makeText(context, R.string.content_not_supported, Toast.LENGTH_LONG).show();
|
||||
} else {
|
||||
ExtractorHelper.handleGeneralException(this, -1, url, throwable,
|
||||
UserAction.SOMETHING_ELSE, null);
|
||||
finish();
|
||||
ErrorActivity.reportError(context, errorInfo);
|
||||
}
|
||||
|
||||
if (context instanceof RouterActivity) {
|
||||
((RouterActivity) context).finish();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -186,7 +235,7 @@ public class RouterActivity extends AppCompatActivity {
|
||||
.setPositiveButton(R.string.open_in_browser,
|
||||
(dialog, which) -> ShareUtils.openUrlInBrowser(this, url))
|
||||
.setNegativeButton(R.string.share,
|
||||
(dialog, which) -> ShareUtils.shareUrl(this, "", url)) // no subject
|
||||
(dialog, which) -> ShareUtils.shareText(this, "", url)) // no subject
|
||||
.setNeutralButton(R.string.cancel, null)
|
||||
.setOnDismissListener(dialog -> finish())
|
||||
.show();
|
||||
@@ -267,9 +316,8 @@ public class RouterActivity extends AppCompatActivity {
|
||||
final Context themeWrapperContext = getThemeWrapperContext();
|
||||
|
||||
final LayoutInflater inflater = LayoutInflater.from(themeWrapperContext);
|
||||
final LinearLayout rootLayout = (LinearLayout) inflater.inflate(
|
||||
R.layout.single_choice_dialog_view, null, false);
|
||||
final RadioGroup radioGroup = rootLayout.findViewById(android.R.id.list);
|
||||
final RadioGroup radioGroup = SingleChoiceDialogViewBinding.inflate(getLayoutInflater())
|
||||
.list;
|
||||
|
||||
final DialogInterface.OnClickListener dialogButtonsClickListener = (dialog, which) -> {
|
||||
final int indexOfChild = radioGroup.indexOfChild(
|
||||
@@ -322,8 +370,7 @@ public class RouterActivity extends AppCompatActivity {
|
||||
|
||||
int id = 12345;
|
||||
for (final AdapterChoiceItem item : choices) {
|
||||
final RadioButton radioButton
|
||||
= (RadioButton) inflater.inflate(R.layout.list_radio_icon_item, null);
|
||||
final RadioButton radioButton = ListRadioIconItemBinding.inflate(inflater).getRoot();
|
||||
radioButton.setText(item.description);
|
||||
TextViewCompat.setCompoundDrawablesRelativeWithIntrinsicBounds(radioButton,
|
||||
AppCompatResources.getDrawable(getApplicationContext(), item.icon),
|
||||
@@ -500,7 +547,8 @@ public class RouterActivity extends AppCompatActivity {
|
||||
.subscribe(intent -> {
|
||||
startActivity(intent);
|
||||
finish();
|
||||
}, throwable -> handleError(throwable, currentUrl))
|
||||
}, throwable -> handleError(this, new ErrorInfo(throwable,
|
||||
UserAction.SHARE_TO_NEWPIPE, "Starting info activity: " + currentUrl)))
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -580,6 +628,7 @@ public class RouterActivity extends AppCompatActivity {
|
||||
this.playerChoice = playerChoice;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String toString() {
|
||||
return serviceId + ":" + url + " > " + linkType + " ::: " + playerChoice;
|
||||
@@ -646,9 +695,9 @@ public class RouterActivity extends AppCompatActivity {
|
||||
if (fetcher != null) {
|
||||
fetcher.dispose();
|
||||
}
|
||||
}, throwable -> ExtractorHelper.handleGeneralException(this,
|
||||
choice.serviceId, choice.url, throwable, finalUserAction,
|
||||
", opened with " + choice.playerChoice));
|
||||
}, throwable -> handleError(this, new ErrorInfo(throwable, finalUserAction,
|
||||
choice.url + " opened with " + choice.playerChoice,
|
||||
choice.serviceId)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -696,7 +745,7 @@ public class RouterActivity extends AppCompatActivity {
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
stopForeground(true);
|
||||
ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE);
|
||||
if (fetcher != null) {
|
||||
fetcher.dispose();
|
||||
}
|
||||
|
||||
@@ -6,22 +6,19 @@ import android.view.LayoutInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentActivity;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import androidx.viewpager2.adapter.FragmentStateAdapter;
|
||||
import androidx.viewpager2.widget.ViewPager2;
|
||||
|
||||
import com.google.android.material.tabs.TabLayout;
|
||||
import com.google.android.material.tabs.TabLayoutMediator;
|
||||
|
||||
import org.schabi.newpipe.BuildConfig;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.databinding.ActivityAboutBinding;
|
||||
import org.schabi.newpipe.databinding.FragmentAboutBinding;
|
||||
import org.schabi.newpipe.util.ThemeHelper;
|
||||
|
||||
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
|
||||
@@ -32,76 +29,72 @@ public class AboutActivity extends AppCompatActivity {
|
||||
* List of all software components.
|
||||
*/
|
||||
private static final SoftwareComponent[] SOFTWARE_COMPONENTS = {
|
||||
new SoftwareComponent("Giga Get", "2014 - 2015", "Peter Cai",
|
||||
new SoftwareComponent("ACRA", "2013", "Kevin Gaudin",
|
||||
"https://github.com/ACRA/acra", StandardLicenses.APACHE2),
|
||||
new SoftwareComponent("AndroidX", "2005 - 2011", "The Android Open Source Project",
|
||||
"https://developer.android.com/jetpack", StandardLicenses.APACHE2),
|
||||
new SoftwareComponent("CircleImageView", "2014 - 2020", "Henning Dodenhof",
|
||||
"https://github.com/hdodenhof/CircleImageView",
|
||||
StandardLicenses.APACHE2),
|
||||
new SoftwareComponent("ExoPlayer", "2014 - 2020", "Google, Inc.",
|
||||
"https://github.com/google/ExoPlayer", StandardLicenses.APACHE2),
|
||||
new SoftwareComponent("GigaGet", "2014 - 2015", "Peter Cai",
|
||||
"https://github.com/PaperAirplane-Dev-Team/GigaGet", StandardLicenses.GPL3),
|
||||
new SoftwareComponent("Groupie", "2016", "Lisa Wray",
|
||||
"https://github.com/lisawray/groupie", StandardLicenses.MIT),
|
||||
new SoftwareComponent("Icepick", "2015", "Frankie Sardo",
|
||||
"https://github.com/frankiesardo/icepick", StandardLicenses.EPL1),
|
||||
new SoftwareComponent("Jsoup", "2009 - 2020", "Jonathan Hedley",
|
||||
"https://github.com/jhy/jsoup", StandardLicenses.MIT),
|
||||
new SoftwareComponent("Markwon", "2019", "Dimitry Ivanov",
|
||||
"https://github.com/noties/Markwon", StandardLicenses.APACHE2),
|
||||
new SoftwareComponent("Material Components for Android", "2016 - 2020", "Google, Inc.",
|
||||
"https://github.com/material-components/material-components-android",
|
||||
StandardLicenses.APACHE2),
|
||||
new SoftwareComponent("NewPipe Extractor", "2017 - 2020", "Christian Schabesberger",
|
||||
"https://github.com/TeamNewPipe/NewPipeExtractor", StandardLicenses.GPL3),
|
||||
new SoftwareComponent("Jsoup", "2017", "Jonathan Hedley",
|
||||
"https://github.com/jhy/jsoup", StandardLicenses.MIT),
|
||||
new SoftwareComponent("Rhino", "2015", "Mozilla",
|
||||
"https://www.mozilla.org/rhino/", StandardLicenses.MPL2),
|
||||
new SoftwareComponent("ACRA", "2013", "Kevin Gaudin",
|
||||
"http://www.acra.ch", StandardLicenses.APACHE2),
|
||||
new SoftwareComponent("NoNonsense-FilePicker", "2016", "Jonas Kalderstam",
|
||||
"https://github.com/spacecowboy/NoNonsense-FilePicker",
|
||||
StandardLicenses.MPL2),
|
||||
new SoftwareComponent("OkHttp", "2019", "Square, Inc.",
|
||||
"https://square.github.io/okhttp/", StandardLicenses.APACHE2),
|
||||
new SoftwareComponent("PrettyTime", "2012 - 2020", "Lincoln Baxter, III",
|
||||
"https://github.com/ocpsoft/prettytime", StandardLicenses.APACHE2),
|
||||
new SoftwareComponent("RxAndroid", "2015", "The RxAndroid authors",
|
||||
"https://github.com/ReactiveX/RxAndroid", StandardLicenses.APACHE2),
|
||||
new SoftwareComponent("RxBinding", "2015", "Jake Wharton",
|
||||
"https://github.com/JakeWharton/RxBinding", StandardLicenses.APACHE2),
|
||||
new SoftwareComponent("RxJava", "2016 - 2020", "RxJava Contributors",
|
||||
"https://github.com/ReactiveX/RxJava", StandardLicenses.APACHE2),
|
||||
new SoftwareComponent("Universal Image Loader", "2011 - 2015", "Sergey Tarasevich",
|
||||
"https://github.com/nostra13/Android-Universal-Image-Loader",
|
||||
StandardLicenses.APACHE2),
|
||||
new SoftwareComponent("CircleImageView", "2014 - 2020", "Henning Dodenhof",
|
||||
"https://github.com/hdodenhof/CircleImageView", StandardLicenses.APACHE2),
|
||||
new SoftwareComponent("NoNonsense-FilePicker", "2016", "Jonas Kalderstam",
|
||||
"https://github.com/spacecowboy/NoNonsense-FilePicker", StandardLicenses.MPL2),
|
||||
new SoftwareComponent("ExoPlayer", "2014 - 2020", "Google Inc",
|
||||
"https://github.com/google/ExoPlayer", StandardLicenses.APACHE2),
|
||||
new SoftwareComponent("RxAndroid", "2015 - 2018", "The RxAndroid authors",
|
||||
"https://github.com/ReactiveX/RxAndroid", StandardLicenses.APACHE2),
|
||||
new SoftwareComponent("RxJava", "2016 - 2020", "RxJava Contributors",
|
||||
"https://github.com/ReactiveX/RxJava", StandardLicenses.APACHE2),
|
||||
new SoftwareComponent("RxBinding", "2015 - 2018", "Jake Wharton",
|
||||
"https://github.com/JakeWharton/RxBinding", StandardLicenses.APACHE2),
|
||||
new SoftwareComponent("PrettyTime", "2012 - 2020", "Lincoln Baxter, III",
|
||||
"https://github.com/ocpsoft/prettytime", StandardLicenses.APACHE2),
|
||||
new SoftwareComponent("Markwon", "2017 - 2020", "Noties",
|
||||
"https://github.com/noties/Markwon", StandardLicenses.APACHE2),
|
||||
new SoftwareComponent("Groupie", "2016", "Lisa Wray",
|
||||
"https://github.com/lisawray/groupie", StandardLicenses.MIT)
|
||||
};
|
||||
|
||||
private static final int POS_ABOUT = 0;
|
||||
private static final int POS_LICENSE = 1;
|
||||
private static final int TOTAL_COUNT = 2;
|
||||
/**
|
||||
* The {@link RecyclerView.Adapter} that will provide
|
||||
* fragments for each of the sections. We use a
|
||||
* {@link FragmentStateAdapter} derivative, which will keep every
|
||||
* loaded fragment in memory.
|
||||
*/
|
||||
private SectionsPagerAdapter mSectionsPagerAdapter;
|
||||
/**
|
||||
* The {@link ViewPager2} that will host the section contents.
|
||||
*/
|
||||
private ViewPager2 mViewPager;
|
||||
|
||||
@Override
|
||||
protected void onCreate(final Bundle savedInstanceState) {
|
||||
assureCorrectAppLanguage(this);
|
||||
super.onCreate(savedInstanceState);
|
||||
ThemeHelper.setTheme(this);
|
||||
this.setTitle(getString(R.string.title_activity_about));
|
||||
setTitle(getString(R.string.title_activity_about));
|
||||
|
||||
setContentView(R.layout.activity_about);
|
||||
final ActivityAboutBinding aboutBinding = ActivityAboutBinding.inflate(getLayoutInflater());
|
||||
setContentView(aboutBinding.getRoot());
|
||||
|
||||
final Toolbar toolbar = findViewById(R.id.toolbar);
|
||||
setSupportActionBar(toolbar);
|
||||
setSupportActionBar(aboutBinding.toolbar);
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
// Create the adapter that will return a fragment for each of the three
|
||||
// primary sections of the activity.
|
||||
mSectionsPagerAdapter = new SectionsPagerAdapter(this);
|
||||
final SectionsPagerAdapter mSectionsPagerAdapter = new SectionsPagerAdapter(this);
|
||||
|
||||
// Set up the ViewPager with the sections adapter.
|
||||
mViewPager = findViewById(R.id.container);
|
||||
mViewPager.setAdapter(mSectionsPagerAdapter);
|
||||
aboutBinding.container.setAdapter(mSectionsPagerAdapter);
|
||||
|
||||
final TabLayout tabLayout = findViewById(R.id.tabs);
|
||||
new TabLayoutMediator(tabLayout, mViewPager, (tab, position) -> {
|
||||
new TabLayoutMediator(aboutBinding.tabs, aboutBinding.container, (tab, position) -> {
|
||||
switch (position) {
|
||||
default:
|
||||
case POS_ABOUT:
|
||||
@@ -131,7 +124,8 @@ public class AboutActivity extends AppCompatActivity {
|
||||
* A placeholder fragment containing a simple view.
|
||||
*/
|
||||
public static class AboutFragment extends Fragment {
|
||||
public AboutFragment() { }
|
||||
public AboutFragment() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Created a new instance of this fragment for the given section number.
|
||||
@@ -143,33 +137,29 @@ public class AboutActivity extends AppCompatActivity {
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(final LayoutInflater inflater, final ViewGroup container,
|
||||
public View onCreateView(@NonNull final LayoutInflater inflater, final ViewGroup container,
|
||||
final Bundle savedInstanceState) {
|
||||
final View rootView = inflater.inflate(R.layout.fragment_about, container, false);
|
||||
final Context context = this.getContext();
|
||||
final FragmentAboutBinding aboutBinding =
|
||||
FragmentAboutBinding.inflate(inflater, container, false);
|
||||
final Context context = getContext();
|
||||
|
||||
final TextView version = rootView.findViewById(R.id.app_version);
|
||||
version.setText(BuildConfig.VERSION_NAME);
|
||||
aboutBinding.appVersion.setText(BuildConfig.VERSION_NAME);
|
||||
|
||||
final View githubLink = rootView.findViewById(R.id.github_link);
|
||||
githubLink.setOnClickListener(nv ->
|
||||
openUrlInBrowser(context, context.getString(R.string.github_url)));
|
||||
aboutBinding.githubLink.setOnClickListener(nv ->
|
||||
openUrlInBrowser(context, context.getString(R.string.github_url), false));
|
||||
|
||||
final View donationLink = rootView.findViewById(R.id.donation_link);
|
||||
donationLink.setOnClickListener(v ->
|
||||
openUrlInBrowser(context, context.getString(R.string.donation_url)));
|
||||
aboutBinding.donationLink.setOnClickListener(v ->
|
||||
openUrlInBrowser(context, context.getString(R.string.donation_url), false));
|
||||
|
||||
final View websiteLink = rootView.findViewById(R.id.website_link);
|
||||
websiteLink.setOnClickListener(nv ->
|
||||
openUrlInBrowser(context, context.getString(R.string.website_url)));
|
||||
aboutBinding.websiteLink.setOnClickListener(nv ->
|
||||
openUrlInBrowser(context, context.getString(R.string.website_url), false));
|
||||
|
||||
final View privacyPolicyLink = rootView.findViewById(R.id.privacy_policy_link);
|
||||
privacyPolicyLink.setOnClickListener(v ->
|
||||
openUrlInBrowser(context, context.getString(R.string.privacy_policy_url)));
|
||||
aboutBinding.privacyPolicyLink.setOnClickListener(v ->
|
||||
openUrlInBrowser(context, context.getString(R.string.privacy_policy_url),
|
||||
false));
|
||||
|
||||
return rootView;
|
||||
return aboutBinding.getRoot();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package org.schabi.newpipe.about
|
||||
|
||||
import android.net.Uri
|
||||
import android.os.Parcelable
|
||||
import kotlinx.android.parcel.Parcelize
|
||||
import java.io.Serializable
|
||||
@@ -9,11 +8,4 @@ import java.io.Serializable
|
||||
* Class for storing information about a software license.
|
||||
*/
|
||||
@Parcelize
|
||||
class License(val name: String, val abbreviation: String, val filename: String) : Parcelable, Serializable {
|
||||
val contentUri: Uri
|
||||
get() = Uri.Builder()
|
||||
.scheme("file")
|
||||
.path("/android_asset")
|
||||
.appendPath(filename)
|
||||
.build()
|
||||
}
|
||||
class License(val name: String, val abbreviation: String, val filename: String) : Parcelable, Serializable
|
||||
|
||||
@@ -7,18 +7,20 @@ import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.Fragment;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.databinding.FragmentLicensesBinding;
|
||||
import org.schabi.newpipe.databinding.ItemSoftwareComponentBinding;
|
||||
import org.schabi.newpipe.util.ShareUtils;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Arrays;
|
||||
import java.util.Comparator;
|
||||
import java.util.Objects;
|
||||
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
||||
|
||||
@@ -35,12 +37,9 @@ public class LicenseFragment extends Fragment {
|
||||
private final CompositeDisposable compositeDisposable = new CompositeDisposable();
|
||||
|
||||
public static LicenseFragment newInstance(final SoftwareComponent[] softwareComponents) {
|
||||
if (softwareComponents == null) {
|
||||
throw new NullPointerException("softwareComponents is null");
|
||||
}
|
||||
final LicenseFragment fragment = new LicenseFragment();
|
||||
final Bundle bundle = new Bundle();
|
||||
bundle.putParcelableArray(ARG_COMPONENTS, softwareComponents);
|
||||
bundle.putParcelableArray(ARG_COMPONENTS, Objects.requireNonNull(softwareComponents));
|
||||
final LicenseFragment fragment = new LicenseFragment();
|
||||
fragment.setArguments(bundle);
|
||||
return fragment;
|
||||
}
|
||||
@@ -69,43 +68,42 @@ public class LicenseFragment extends Fragment {
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public View onCreateView(final LayoutInflater inflater, @Nullable final ViewGroup container,
|
||||
public View onCreateView(@NonNull final LayoutInflater inflater,
|
||||
@Nullable final ViewGroup container,
|
||||
@Nullable final Bundle savedInstanceState) {
|
||||
final View rootView = inflater.inflate(R.layout.fragment_licenses, container, false);
|
||||
final ViewGroup softwareComponentsView = rootView.findViewById(R.id.software_components);
|
||||
final FragmentLicensesBinding binding = FragmentLicensesBinding
|
||||
.inflate(inflater, container, false);
|
||||
|
||||
final View licenseLink = rootView.findViewById(R.id.app_read_license);
|
||||
licenseLink.setOnClickListener(v -> {
|
||||
binding.appReadLicense.setOnClickListener(v -> {
|
||||
activeLicense = StandardLicenses.GPL3;
|
||||
compositeDisposable.add(LicenseFragmentHelper.showLicense(getActivity(),
|
||||
StandardLicenses.GPL3));
|
||||
});
|
||||
|
||||
for (final SoftwareComponent component : softwareComponents) {
|
||||
final View componentView = inflater
|
||||
.inflate(R.layout.item_software_component, container, false);
|
||||
final TextView softwareName = componentView.findViewById(R.id.name);
|
||||
final TextView copyright = componentView.findViewById(R.id.copyright);
|
||||
softwareName.setText(component.getName());
|
||||
copyright.setText(getString(R.string.copyright,
|
||||
final ItemSoftwareComponentBinding componentBinding = ItemSoftwareComponentBinding
|
||||
.inflate(inflater, container, false);
|
||||
componentBinding.name.setText(component.getName());
|
||||
componentBinding.copyright.setText(getString(R.string.copyright,
|
||||
component.getYears(),
|
||||
component.getCopyrightOwner(),
|
||||
component.getLicense().getAbbreviation()));
|
||||
|
||||
componentView.setTag(component);
|
||||
componentView.setOnClickListener(v -> {
|
||||
final View root = componentBinding.getRoot();
|
||||
root.setTag(component);
|
||||
root.setOnClickListener(v -> {
|
||||
activeLicense = component.getLicense();
|
||||
compositeDisposable.add(LicenseFragmentHelper.showLicense(getActivity(),
|
||||
component.getLicense()));
|
||||
});
|
||||
softwareComponentsView.addView(componentView);
|
||||
registerForContextMenu(componentView);
|
||||
binding.softwareComponents.addView(root);
|
||||
registerForContextMenu(root);
|
||||
}
|
||||
if (activeLicense != null) {
|
||||
compositeDisposable.add(LicenseFragmentHelper.showLicense(getActivity(),
|
||||
activeLicense));
|
||||
}
|
||||
return rootView;
|
||||
return binding.getRoot();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -12,6 +12,8 @@ public final class StandardLicenses {
|
||||
= new License("Mozilla Public License, Version 2.0", "MPL 2.0", "mpl2.html");
|
||||
public static final License MIT
|
||||
= new License("MIT License", "MIT", "mit.html");
|
||||
public static final License EPL1
|
||||
= new License("Eclipse Public License, Version 1.0", "EPL 1.0", "epl1.html");
|
||||
|
||||
private StandardLicenses() { }
|
||||
}
|
||||
|
||||
@@ -7,9 +7,8 @@ import org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity
|
||||
import org.schabi.newpipe.database.stream.model.StreamEntity
|
||||
import org.schabi.newpipe.database.stream.model.StreamStateEntity
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem
|
||||
import kotlin.jvm.Throws
|
||||
|
||||
class PlaylistStreamEntry(
|
||||
data class PlaylistStreamEntry(
|
||||
@Embedded
|
||||
val streamEntity: StreamEntity,
|
||||
|
||||
|
||||
@@ -9,10 +9,10 @@ import android.view.ViewTreeObserver;
|
||||
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.fragment.app.FragmentTransaction;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.databinding.ActivityDownloaderBinding;
|
||||
import org.schabi.newpipe.util.DeviceUtils;
|
||||
import org.schabi.newpipe.util.ThemeHelper;
|
||||
import org.schabi.newpipe.views.FocusOverlayView;
|
||||
@@ -35,11 +35,14 @@ public class DownloadActivity extends AppCompatActivity {
|
||||
|
||||
assureCorrectAppLanguage(this);
|
||||
ThemeHelper.setTheme(this);
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_downloader);
|
||||
|
||||
final Toolbar toolbar = findViewById(R.id.toolbar);
|
||||
setSupportActionBar(toolbar);
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
final ActivityDownloaderBinding downloaderBinding =
|
||||
ActivityDownloaderBinding.inflate(getLayoutInflater());
|
||||
setContentView(downloaderBinding.getRoot());
|
||||
|
||||
setSupportActionBar(downloaderBinding.toolbarLayout.toolbar);
|
||||
|
||||
final ActionBar actionBar = getSupportActionBar();
|
||||
if (actionBar != null) {
|
||||
|
||||
@@ -16,12 +16,8 @@ import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.AdapterView;
|
||||
import android.widget.EditText;
|
||||
import android.widget.RadioButton;
|
||||
import android.widget.RadioGroup;
|
||||
import android.widget.SeekBar;
|
||||
import android.widget.Spinner;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.IdRes;
|
||||
@@ -40,6 +36,10 @@ import com.nononsenseapps.filepicker.Utils;
|
||||
import org.schabi.newpipe.MainActivity;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.RouterActivity;
|
||||
import org.schabi.newpipe.databinding.DownloadDialogBinding;
|
||||
import org.schabi.newpipe.error.ErrorActivity;
|
||||
import org.schabi.newpipe.error.ErrorInfo;
|
||||
import org.schabi.newpipe.error.UserAction;
|
||||
import org.schabi.newpipe.extractor.MediaFormat;
|
||||
import org.schabi.newpipe.extractor.NewPipe;
|
||||
import org.schabi.newpipe.extractor.localization.Localization;
|
||||
@@ -48,9 +48,6 @@ import org.schabi.newpipe.extractor.stream.Stream;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||
import org.schabi.newpipe.extractor.stream.SubtitlesStream;
|
||||
import org.schabi.newpipe.extractor.stream.VideoStream;
|
||||
import org.schabi.newpipe.report.ErrorActivity;
|
||||
import org.schabi.newpipe.report.ErrorInfo;
|
||||
import org.schabi.newpipe.report.UserAction;
|
||||
import org.schabi.newpipe.settings.NewPipeSettings;
|
||||
import org.schabi.newpipe.util.FilePickerActivityHelper;
|
||||
import org.schabi.newpipe.util.FilenameUtils;
|
||||
@@ -64,7 +61,6 @@ import org.schabi.newpipe.util.ThemeHelper;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
@@ -116,11 +112,7 @@ public class DownloadDialog extends DialogFragment
|
||||
|
||||
private final CompositeDisposable disposables = new CompositeDisposable();
|
||||
|
||||
private EditText nameEditText;
|
||||
private Spinner streamsSpinner;
|
||||
private RadioGroup radioStreamsGroup;
|
||||
private TextView threadsCountTextView;
|
||||
private SeekBar threadsSeekBar;
|
||||
private DownloadDialogBinding dialogBinding;
|
||||
|
||||
private SharedPreferences prefs;
|
||||
|
||||
@@ -277,38 +269,35 @@ public class DownloadDialog extends DialogFragment
|
||||
@Override
|
||||
public void onViewCreated(@NonNull final View view, @Nullable final Bundle savedInstanceState) {
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
nameEditText = view.findViewById(R.id.file_name);
|
||||
nameEditText.setText(FilenameUtils.createFilename(getContext(), currentInfo.getName()));
|
||||
dialogBinding = DownloadDialogBinding.bind(view);
|
||||
|
||||
dialogBinding.fileName.setText(FilenameUtils.createFilename(getContext(),
|
||||
currentInfo.getName()));
|
||||
selectedAudioIndex = ListHelper
|
||||
.getDefaultAudioFormat(getContext(), currentInfo.getAudioStreams());
|
||||
|
||||
selectedSubtitleIndex = getSubtitleIndexBy(subtitleStreamsAdapter.getAll());
|
||||
|
||||
streamsSpinner = view.findViewById(R.id.quality_spinner);
|
||||
streamsSpinner.setOnItemSelectedListener(this);
|
||||
dialogBinding.qualitySpinner.setOnItemSelectedListener(this);
|
||||
|
||||
threadsCountTextView = view.findViewById(R.id.threads_count);
|
||||
threadsSeekBar = view.findViewById(R.id.threads);
|
||||
dialogBinding.videoAudioGroup.setOnCheckedChangeListener(this);
|
||||
|
||||
radioStreamsGroup = view.findViewById(R.id.video_audio_group);
|
||||
radioStreamsGroup.setOnCheckedChangeListener(this);
|
||||
|
||||
initToolbar(view.findViewById(R.id.toolbar));
|
||||
initToolbar(dialogBinding.toolbarLayout.toolbar);
|
||||
setupDownloadOptions();
|
||||
|
||||
prefs = PreferenceManager.getDefaultSharedPreferences(requireContext());
|
||||
|
||||
final int threads = prefs.getInt(getString(R.string.default_download_threads), 3);
|
||||
threadsCountTextView.setText(String.valueOf(threads));
|
||||
threadsSeekBar.setProgress(threads - 1);
|
||||
threadsSeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
|
||||
dialogBinding.threadsCount.setText(String.valueOf(threads));
|
||||
dialogBinding.threads.setProgress(threads - 1);
|
||||
dialogBinding.threads.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
|
||||
@Override
|
||||
public void onProgressChanged(final SeekBar seekbar, final int progress,
|
||||
final boolean fromUser) {
|
||||
final int newProgress = progress + 1;
|
||||
prefs.edit().putInt(getString(R.string.default_download_threads), newProgress)
|
||||
.apply();
|
||||
threadsCountTextView.setText(String.valueOf(newProgress));
|
||||
dialogBinding.threadsCount.setText(String.valueOf(newProgress));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -326,19 +315,19 @@ public class DownloadDialog extends DialogFragment
|
||||
|
||||
disposables.add(StreamSizeWrapper.fetchSizeForWrapper(wrappedVideoStreams)
|
||||
.subscribe(result -> {
|
||||
if (radioStreamsGroup.getCheckedRadioButtonId() == R.id.video_button) {
|
||||
if (dialogBinding.videoAudioGroup.getCheckedRadioButtonId() == R.id.video_button) {
|
||||
setupVideoSpinner();
|
||||
}
|
||||
}));
|
||||
disposables.add(StreamSizeWrapper.fetchSizeForWrapper(wrappedAudioStreams)
|
||||
.subscribe(result -> {
|
||||
if (radioStreamsGroup.getCheckedRadioButtonId() == R.id.audio_button) {
|
||||
if (dialogBinding.videoAudioGroup.getCheckedRadioButtonId() == R.id.audio_button) {
|
||||
setupAudioSpinner();
|
||||
}
|
||||
}));
|
||||
disposables.add(StreamSizeWrapper.fetchSizeForWrapper(wrappedSubtitleStreams)
|
||||
.subscribe(result -> {
|
||||
if (radioStreamsGroup.getCheckedRadioButtonId() == R.id.subtitle_button) {
|
||||
if (dialogBinding.videoAudioGroup.getCheckedRadioButtonId() == R.id.subtitle_button) {
|
||||
setupSubtitleSpinner();
|
||||
}
|
||||
}));
|
||||
@@ -350,6 +339,12 @@ public class DownloadDialog extends DialogFragment
|
||||
disposables.clear();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyView() {
|
||||
dialogBinding = null;
|
||||
super.onDestroyView();
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Radio group Video&Audio options - Listener
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
@@ -429,8 +424,8 @@ public class DownloadDialog extends DialogFragment
|
||||
return;
|
||||
}
|
||||
|
||||
streamsSpinner.setAdapter(audioStreamsAdapter);
|
||||
streamsSpinner.setSelection(selectedAudioIndex);
|
||||
dialogBinding.qualitySpinner.setAdapter(audioStreamsAdapter);
|
||||
dialogBinding.qualitySpinner.setSelection(selectedAudioIndex);
|
||||
setRadioButtonsState(true);
|
||||
}
|
||||
|
||||
@@ -439,8 +434,8 @@ public class DownloadDialog extends DialogFragment
|
||||
return;
|
||||
}
|
||||
|
||||
streamsSpinner.setAdapter(videoStreamsAdapter);
|
||||
streamsSpinner.setSelection(selectedVideoIndex);
|
||||
dialogBinding.qualitySpinner.setAdapter(videoStreamsAdapter);
|
||||
dialogBinding.qualitySpinner.setSelection(selectedVideoIndex);
|
||||
setRadioButtonsState(true);
|
||||
}
|
||||
|
||||
@@ -449,8 +444,8 @@ public class DownloadDialog extends DialogFragment
|
||||
return;
|
||||
}
|
||||
|
||||
streamsSpinner.setAdapter(subtitleStreamsAdapter);
|
||||
streamsSpinner.setSelection(selectedSubtitleIndex);
|
||||
dialogBinding.qualitySpinner.setAdapter(subtitleStreamsAdapter);
|
||||
dialogBinding.qualitySpinner.setSelection(selectedSubtitleIndex);
|
||||
setRadioButtonsState(true);
|
||||
}
|
||||
|
||||
@@ -475,7 +470,7 @@ public class DownloadDialog extends DialogFragment
|
||||
break;
|
||||
}
|
||||
|
||||
threadsSeekBar.setEnabled(flag);
|
||||
dialogBinding.threads.setEnabled(flag);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -486,7 +481,7 @@ public class DownloadDialog extends DialogFragment
|
||||
+ "parent = [" + parent + "], view = [" + view + "], "
|
||||
+ "position = [" + position + "], id = [" + id + "]");
|
||||
}
|
||||
switch (radioStreamsGroup.getCheckedRadioButtonId()) {
|
||||
switch (dialogBinding.videoAudioGroup.getCheckedRadioButtonId()) {
|
||||
case R.id.audio_button:
|
||||
selectedAudioIndex = position;
|
||||
break;
|
||||
@@ -506,16 +501,14 @@ public class DownloadDialog extends DialogFragment
|
||||
protected void setupDownloadOptions() {
|
||||
setRadioButtonsState(false);
|
||||
|
||||
final RadioButton audioButton = radioStreamsGroup.findViewById(R.id.audio_button);
|
||||
final RadioButton videoButton = radioStreamsGroup.findViewById(R.id.video_button);
|
||||
final RadioButton subtitleButton = radioStreamsGroup.findViewById(R.id.subtitle_button);
|
||||
final boolean isVideoStreamsAvailable = videoStreamsAdapter.getCount() > 0;
|
||||
final boolean isAudioStreamsAvailable = audioStreamsAdapter.getCount() > 0;
|
||||
final boolean isSubtitleStreamsAvailable = subtitleStreamsAdapter.getCount() > 0;
|
||||
|
||||
audioButton.setVisibility(isAudioStreamsAvailable ? View.VISIBLE : View.GONE);
|
||||
videoButton.setVisibility(isVideoStreamsAvailable ? View.VISIBLE : View.GONE);
|
||||
subtitleButton.setVisibility(isSubtitleStreamsAvailable ? View.VISIBLE : View.GONE);
|
||||
dialogBinding.audioButton.setVisibility(isAudioStreamsAvailable ? View.VISIBLE : View.GONE);
|
||||
dialogBinding.videoButton.setVisibility(isVideoStreamsAvailable ? View.VISIBLE : View.GONE);
|
||||
dialogBinding.subtitleButton.setVisibility(isSubtitleStreamsAvailable
|
||||
? View.VISIBLE : View.GONE);
|
||||
|
||||
prefs = PreferenceManager.getDefaultSharedPreferences(getContext());
|
||||
final String defaultMedia = prefs.getString(getString(R.string.last_used_download_type),
|
||||
@@ -523,24 +516,24 @@ public class DownloadDialog extends DialogFragment
|
||||
|
||||
if (isVideoStreamsAvailable
|
||||
&& (defaultMedia.equals(getString(R.string.last_download_type_video_key)))) {
|
||||
videoButton.setChecked(true);
|
||||
dialogBinding.videoButton.setChecked(true);
|
||||
setupVideoSpinner();
|
||||
} else if (isAudioStreamsAvailable
|
||||
&& (defaultMedia.equals(getString(R.string.last_download_type_audio_key)))) {
|
||||
audioButton.setChecked(true);
|
||||
dialogBinding.audioButton.setChecked(true);
|
||||
setupAudioSpinner();
|
||||
} else if (isSubtitleStreamsAvailable
|
||||
&& (defaultMedia.equals(getString(R.string.last_download_type_subtitle_key)))) {
|
||||
subtitleButton.setChecked(true);
|
||||
dialogBinding.subtitleButton.setChecked(true);
|
||||
setupSubtitleSpinner();
|
||||
} else if (isVideoStreamsAvailable) {
|
||||
videoButton.setChecked(true);
|
||||
dialogBinding.videoButton.setChecked(true);
|
||||
setupVideoSpinner();
|
||||
} else if (isAudioStreamsAvailable) {
|
||||
audioButton.setChecked(true);
|
||||
dialogBinding.audioButton.setChecked(true);
|
||||
setupAudioSpinner();
|
||||
} else if (isSubtitleStreamsAvailable) {
|
||||
subtitleButton.setChecked(true);
|
||||
dialogBinding.subtitleButton.setChecked(true);
|
||||
setupSubtitleSpinner();
|
||||
} else {
|
||||
Toast.makeText(getContext(), R.string.no_streams_available_download,
|
||||
@@ -550,9 +543,9 @@ public class DownloadDialog extends DialogFragment
|
||||
}
|
||||
|
||||
private void setRadioButtonsState(final boolean enabled) {
|
||||
radioStreamsGroup.findViewById(R.id.audio_button).setEnabled(enabled);
|
||||
radioStreamsGroup.findViewById(R.id.video_button).setEnabled(enabled);
|
||||
radioStreamsGroup.findViewById(R.id.subtitle_button).setEnabled(enabled);
|
||||
dialogBinding.audioButton.setEnabled(enabled);
|
||||
dialogBinding.videoButton.setEnabled(enabled);
|
||||
dialogBinding.subtitleButton.setEnabled(enabled);
|
||||
}
|
||||
|
||||
private int getSubtitleIndexBy(final List<SubtitlesStream> streams) {
|
||||
@@ -582,7 +575,7 @@ public class DownloadDialog extends DialogFragment
|
||||
}
|
||||
|
||||
private String getNameEditText() {
|
||||
final String str = nameEditText.getText().toString().trim();
|
||||
final String str = dialogBinding.fileName.getText().toString().trim();
|
||||
|
||||
return FilenameUtils.createFilename(context, str.isEmpty() ? currentInfo.getName() : str);
|
||||
}
|
||||
@@ -597,17 +590,6 @@ public class DownloadDialog extends DialogFragment
|
||||
.show();
|
||||
}
|
||||
|
||||
private void showErrorActivity(final Exception e) {
|
||||
ErrorActivity.reportError(
|
||||
context,
|
||||
Collections.singletonList(e),
|
||||
null,
|
||||
null,
|
||||
ErrorInfo
|
||||
.make(UserAction.SOMETHING_ELSE, "-", "-", R.string.general_error)
|
||||
);
|
||||
}
|
||||
|
||||
private void prepareSelectedDownload() {
|
||||
final StoredDirectoryHelper mainStorage;
|
||||
final MediaFormat format;
|
||||
@@ -619,7 +601,7 @@ public class DownloadDialog extends DialogFragment
|
||||
|
||||
String filename = getNameEditText().concat(".");
|
||||
|
||||
switch (radioStreamsGroup.getCheckedRadioButtonId()) {
|
||||
switch (dialogBinding.videoAudioGroup.getCheckedRadioButtonId()) {
|
||||
case R.id.audio_button:
|
||||
selectedMediaType = getString(R.string.last_download_type_audio_key);
|
||||
mainStorage = mainStorageAudio;
|
||||
@@ -669,7 +651,7 @@ public class DownloadDialog extends DialogFragment
|
||||
filename, mime);
|
||||
} else {
|
||||
File initialSavePath;
|
||||
if (radioStreamsGroup.getCheckedRadioButtonId() == R.id.audio_button) {
|
||||
if (dialogBinding.videoAudioGroup.getCheckedRadioButtonId() == R.id.audio_button) {
|
||||
initialSavePath = NewPipeSettings.getDir(Environment.DIRECTORY_MUSIC);
|
||||
} else {
|
||||
initialSavePath = NewPipeSettings.getDir(Environment.DIRECTORY_MOVIES);
|
||||
@@ -690,6 +672,9 @@ public class DownloadDialog extends DialogFragment
|
||||
prefs.edit()
|
||||
.putString(getString(R.string.last_used_download_type), selectedMediaType)
|
||||
.apply();
|
||||
|
||||
Toast.makeText(context, getString(R.string.download_has_started),
|
||||
Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
|
||||
private void checkSelectedDownload(final StoredDirectoryHelper mainStorage,
|
||||
@@ -711,7 +696,8 @@ public class DownloadDialog extends DialogFragment
|
||||
mainStorage.getTag());
|
||||
}
|
||||
} catch (final Exception e) {
|
||||
showErrorActivity(e);
|
||||
ErrorActivity.reportErrorInSnackbar(this,
|
||||
new ErrorInfo(e, UserAction.DOWNLOAD_FAILED, "Getting storage"));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -862,7 +848,7 @@ public class DownloadDialog extends DialogFragment
|
||||
final Stream selectedStream;
|
||||
Stream secondaryStream = null;
|
||||
final char kind;
|
||||
int threads = threadsSeekBar.getProgress() + 1;
|
||||
int threads = dialogBinding.threads.getProgress() + 1;
|
||||
final String[] urls;
|
||||
final MissionRecoveryInfo[] recoveryInfo;
|
||||
String psName = null;
|
||||
@@ -870,7 +856,7 @@ public class DownloadDialog extends DialogFragment
|
||||
long nearLength = 0;
|
||||
|
||||
// more download logic: select muxer, subtitle converter, etc.
|
||||
switch (radioStreamsGroup.getCheckedRadioButtonId()) {
|
||||
switch (dialogBinding.videoAudioGroup.getCheckedRadioButtonId()) {
|
||||
case R.id.audio_button:
|
||||
kind = 'a';
|
||||
selectedStream = audioStreamsAdapter.getItem(selectedAudioIndex);
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
package org.schabi.newpipe.report;
|
||||
package org.schabi.newpipe.error;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.acra.ReportField;
|
||||
import org.acra.data.CrashReportData;
|
||||
import org.acra.sender.ReportSender;
|
||||
import org.schabi.newpipe.R;
|
||||
@@ -32,8 +33,12 @@ public class AcraReportSender implements ReportSender {
|
||||
|
||||
@Override
|
||||
public void send(@NonNull final Context context, @NonNull final CrashReportData report) {
|
||||
ErrorActivity.reportError(context, report,
|
||||
ErrorInfo.make(UserAction.UI_ERROR, "none",
|
||||
"App crash, UI failure", R.string.app_ui_crash));
|
||||
ErrorActivity.reportError(context, new ErrorInfo(
|
||||
new String[]{report.getString(ReportField.STACK_TRACE)},
|
||||
UserAction.UI_ERROR,
|
||||
ErrorInfo.SERVICE_NONE,
|
||||
"ACRA report",
|
||||
R.string.app_ui_crash,
|
||||
null));
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.schabi.newpipe.report;
|
||||
package org.schabi.newpipe.error;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.schabi.newpipe.report;
|
||||
package org.schabi.newpipe.error;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.AlertDialog;
|
||||
@@ -8,44 +8,31 @@ import android.graphics.Color;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.util.Log;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.widget.Button;
|
||||
import android.widget.EditText;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.core.app.NavUtils;
|
||||
import androidx.fragment.app.Fragment;
|
||||
|
||||
import com.google.android.material.snackbar.Snackbar;
|
||||
import com.grack.nanojson.JsonWriter;
|
||||
|
||||
import org.acra.ReportField;
|
||||
import org.acra.data.CrashReportData;
|
||||
import org.schabi.newpipe.ActivityCommunicator;
|
||||
import org.schabi.newpipe.BuildConfig;
|
||||
import org.schabi.newpipe.MainActivity;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.databinding.ActivityErrorBinding;
|
||||
import org.schabi.newpipe.util.Localization;
|
||||
import org.schabi.newpipe.util.ShareUtils;
|
||||
import org.schabi.newpipe.util.ThemeHelper;
|
||||
|
||||
import java.io.PrintWriter;
|
||||
import java.io.StringWriter;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.Arrays;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.TimeZone;
|
||||
import java.util.Vector;
|
||||
|
||||
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
|
||||
|
||||
@@ -74,119 +61,90 @@ public class ErrorActivity extends AppCompatActivity {
|
||||
public static final String TAG = ErrorActivity.class.toString();
|
||||
// BUNDLE TAGS
|
||||
public static final String ERROR_INFO = "error_info";
|
||||
public static final String ERROR_LIST = "error_list";
|
||||
|
||||
public static final String ERROR_EMAIL_ADDRESS = "crashreport@newpipe.schabi.org";
|
||||
public static final String ERROR_EMAIL_SUBJECT
|
||||
= "Exception in NewPipe " + BuildConfig.VERSION_NAME;
|
||||
public static final String ERROR_EMAIL_SUBJECT = "Exception in ";
|
||||
|
||||
public static final String ERROR_GITHUB_ISSUE_URL
|
||||
= "https://github.com/TeamNewPipe/NewPipe/issues";
|
||||
|
||||
private String[] errorList;
|
||||
private ErrorInfo errorInfo;
|
||||
private Class returnActivity;
|
||||
private String currentTimeStamp;
|
||||
private EditText userCommentBox;
|
||||
public static final DateTimeFormatter CURRENT_TIMESTAMP_FORMATTER
|
||||
= DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm");
|
||||
|
||||
public static void reportUiError(final AppCompatActivity activity, final Throwable el) {
|
||||
reportError(activity, el, activity.getClass(), null, ErrorInfo.make(UserAction.UI_ERROR,
|
||||
"none", "", R.string.app_ui_crash));
|
||||
|
||||
private ErrorInfo errorInfo;
|
||||
private String currentTimeStamp;
|
||||
|
||||
private ActivityErrorBinding activityErrorBinding;
|
||||
|
||||
public static void reportError(final Context context, final ErrorInfo errorInfo) {
|
||||
final Intent intent = new Intent(context, ErrorActivity.class);
|
||||
intent.putExtra(ERROR_INFO, errorInfo);
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
context.startActivity(intent);
|
||||
}
|
||||
|
||||
public static void reportError(final Context context, final List<Throwable> el,
|
||||
final Class returnActivity, final View rootView,
|
||||
final ErrorInfo errorInfo) {
|
||||
public static void reportErrorInSnackbar(final Context context, final ErrorInfo errorInfo) {
|
||||
final View rootView = context instanceof Activity
|
||||
? ((Activity) context).findViewById(android.R.id.content) : null;
|
||||
reportErrorInSnackbar(context, rootView, errorInfo);
|
||||
}
|
||||
|
||||
public static void reportErrorInSnackbar(final Fragment fragment, final ErrorInfo errorInfo) {
|
||||
View rootView = fragment.getView();
|
||||
if (rootView == null && fragment.getActivity() != null) {
|
||||
rootView = fragment.getActivity().findViewById(android.R.id.content);
|
||||
}
|
||||
reportErrorInSnackbar(fragment.requireContext(), rootView, errorInfo);
|
||||
}
|
||||
|
||||
public static void reportUiErrorInSnackbar(final Context context,
|
||||
final String request,
|
||||
final Throwable throwable) {
|
||||
reportErrorInSnackbar(context, new ErrorInfo(throwable, UserAction.UI_ERROR, request));
|
||||
}
|
||||
|
||||
public static void reportUiErrorInSnackbar(final Fragment fragment,
|
||||
final String request,
|
||||
final Throwable throwable) {
|
||||
reportErrorInSnackbar(fragment, new ErrorInfo(throwable, UserAction.UI_ERROR, request));
|
||||
}
|
||||
|
||||
|
||||
////////////////////////////////////////////////////////////////////////
|
||||
// Utils
|
||||
////////////////////////////////////////////////////////////////////////
|
||||
|
||||
private static void reportErrorInSnackbar(final Context context,
|
||||
@Nullable final View rootView,
|
||||
final ErrorInfo errorInfo) {
|
||||
if (rootView != null) {
|
||||
Snackbar.make(rootView, R.string.error_snackbar_message, 3 * 1000)
|
||||
Snackbar.make(rootView, R.string.error_snackbar_message, Snackbar.LENGTH_LONG)
|
||||
.setActionTextColor(Color.YELLOW)
|
||||
.setAction(context.getString(R.string.error_snackbar_action).toUpperCase(), v ->
|
||||
startErrorActivity(returnActivity, context, errorInfo, el)).show();
|
||||
reportError(context, errorInfo)).show();
|
||||
} else {
|
||||
startErrorActivity(returnActivity, context, errorInfo, el);
|
||||
reportError(context, errorInfo);
|
||||
}
|
||||
}
|
||||
|
||||
private static void startErrorActivity(final Class returnActivity, final Context context,
|
||||
final ErrorInfo errorInfo, final List<Throwable> el) {
|
||||
final ActivityCommunicator ac = ActivityCommunicator.getCommunicator();
|
||||
ac.setReturnActivity(returnActivity);
|
||||
final Intent intent = new Intent(context, ErrorActivity.class);
|
||||
intent.putExtra(ERROR_INFO, errorInfo);
|
||||
intent.putExtra(ERROR_LIST, elToSl(el));
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
context.startActivity(intent);
|
||||
}
|
||||
|
||||
public static void reportError(final Context context, final Throwable e,
|
||||
final Class returnActivity, final View rootView,
|
||||
final ErrorInfo errorInfo) {
|
||||
List<Throwable> el = null;
|
||||
if (e != null) {
|
||||
el = new Vector<>();
|
||||
el.add(e);
|
||||
}
|
||||
reportError(context, el, returnActivity, rootView, errorInfo);
|
||||
}
|
||||
|
||||
// async call
|
||||
public static void reportError(final Handler handler, final Context context,
|
||||
final Throwable e, final Class returnActivity,
|
||||
final View rootView, final ErrorInfo errorInfo) {
|
||||
|
||||
List<Throwable> el = null;
|
||||
if (e != null) {
|
||||
el = new Vector<>();
|
||||
el.add(e);
|
||||
}
|
||||
reportError(handler, context, el, returnActivity, rootView, errorInfo);
|
||||
}
|
||||
|
||||
// async call
|
||||
public static void reportError(final Handler handler, final Context context,
|
||||
final List<Throwable> el, final Class returnActivity,
|
||||
final View rootView, final ErrorInfo errorInfo) {
|
||||
handler.post(() -> reportError(context, el, returnActivity, rootView, errorInfo));
|
||||
}
|
||||
|
||||
public static void reportError(final Context context, final CrashReportData report,
|
||||
final ErrorInfo errorInfo) {
|
||||
final String[] el = {report.getString(ReportField.STACK_TRACE)};
|
||||
|
||||
final Intent intent = new Intent(context, ErrorActivity.class);
|
||||
intent.putExtra(ERROR_INFO, errorInfo);
|
||||
intent.putExtra(ERROR_LIST, el);
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
context.startActivity(intent);
|
||||
}
|
||||
|
||||
private static String getStackTrace(final Throwable throwable) {
|
||||
final StringWriter sw = new StringWriter();
|
||||
final PrintWriter pw = new PrintWriter(sw, true);
|
||||
throwable.printStackTrace(pw);
|
||||
return sw.getBuffer().toString();
|
||||
}
|
||||
|
||||
// errorList to StringList
|
||||
private static String[] elToSl(final List<Throwable> stackTraces) {
|
||||
final String[] out = new String[stackTraces.size()];
|
||||
for (int i = 0; i < stackTraces.size(); i++) {
|
||||
out[i] = getStackTrace(stackTraces.get(i));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
////////////////////////////////////////////////////////////////////////
|
||||
// Activity lifecycle
|
||||
////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@Override
|
||||
protected void onCreate(final Bundle savedInstanceState) {
|
||||
assureCorrectAppLanguage(this);
|
||||
super.onCreate(savedInstanceState);
|
||||
ThemeHelper.setTheme(this);
|
||||
setContentView(R.layout.activity_error);
|
||||
|
||||
activityErrorBinding = ActivityErrorBinding.inflate(getLayoutInflater());
|
||||
setContentView(activityErrorBinding.getRoot());
|
||||
|
||||
final Intent intent = getIntent();
|
||||
|
||||
final Toolbar toolbar = findViewById(R.id.toolbar);
|
||||
setSupportActionBar(toolbar);
|
||||
setSupportActionBar(activityErrorBinding.toolbarLayout.toolbar);
|
||||
|
||||
final ActionBar actionBar = getSupportActionBar();
|
||||
if (actionBar != null) {
|
||||
@@ -195,49 +153,28 @@ public class ErrorActivity extends AppCompatActivity {
|
||||
actionBar.setDisplayShowTitleEnabled(true);
|
||||
}
|
||||
|
||||
final Button reportEmailButton = findViewById(R.id.errorReportEmailButton);
|
||||
final Button copyButton = findViewById(R.id.errorReportCopyButton);
|
||||
final Button reportGithubButton = findViewById(R.id.errorReportGitHubButton);
|
||||
|
||||
userCommentBox = findViewById(R.id.errorCommentBox);
|
||||
final TextView errorView = findViewById(R.id.errorView);
|
||||
final TextView infoView = findViewById(R.id.errorInfosView);
|
||||
final TextView errorMessageView = findViewById(R.id.errorMessageView);
|
||||
|
||||
final ActivityCommunicator ac = ActivityCommunicator.getCommunicator();
|
||||
returnActivity = ac.getReturnActivity();
|
||||
errorInfo = intent.getParcelableExtra(ERROR_INFO);
|
||||
errorList = intent.getStringArrayExtra(ERROR_LIST);
|
||||
|
||||
// important add guru meditation
|
||||
addGuruMeditation();
|
||||
currentTimeStamp = getCurrentTimeStamp();
|
||||
currentTimeStamp = CURRENT_TIMESTAMP_FORMATTER.format(LocalDateTime.now());
|
||||
|
||||
reportEmailButton.setOnClickListener(v ->
|
||||
activityErrorBinding.errorReportEmailButton.setOnClickListener(v ->
|
||||
openPrivacyPolicyDialog(this, "EMAIL"));
|
||||
|
||||
copyButton.setOnClickListener(v -> {
|
||||
ShareUtils.copyToClipboard(this, buildMarkdown());
|
||||
Toast.makeText(this, R.string.msg_copied, Toast.LENGTH_SHORT).show();
|
||||
});
|
||||
activityErrorBinding.errorReportCopyButton.setOnClickListener(v ->
|
||||
ShareUtils.copyToClipboard(this, buildMarkdown()));
|
||||
|
||||
reportGithubButton.setOnClickListener(v ->
|
||||
activityErrorBinding.errorReportGitHubButton.setOnClickListener(v ->
|
||||
openPrivacyPolicyDialog(this, "GITHUB"));
|
||||
|
||||
|
||||
// normal bugreport
|
||||
buildInfo(errorInfo);
|
||||
if (errorInfo.getMessage() != 0) {
|
||||
errorMessageView.setText(errorInfo.getMessage());
|
||||
} else {
|
||||
errorMessageView.setVisibility(View.GONE);
|
||||
findViewById(R.id.messageWhatHappenedView).setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
errorView.setText(formErrorText(errorList));
|
||||
activityErrorBinding.errorMessageView.setText(errorInfo.getMessageStringId());
|
||||
activityErrorBinding.errorView.setText(formErrorText(errorInfo.getStackTraces()));
|
||||
|
||||
// print stack trace once again for debugging:
|
||||
for (final String e : errorList) {
|
||||
for (final String e : errorInfo.getStackTraces()) {
|
||||
Log.e(TAG, e);
|
||||
}
|
||||
}
|
||||
@@ -252,19 +189,14 @@ public class ErrorActivity extends AppCompatActivity {
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(final MenuItem item) {
|
||||
final int id = item.getItemId();
|
||||
switch (id) {
|
||||
case android.R.id.home:
|
||||
goToReturnActivity();
|
||||
break;
|
||||
case R.id.menu_item_share_error:
|
||||
final Intent intent = new Intent();
|
||||
intent.setAction(Intent.ACTION_SEND);
|
||||
intent.putExtra(Intent.EXTRA_TEXT, buildJson());
|
||||
intent.setType("text/plain");
|
||||
startActivity(Intent.createChooser(intent, getString(R.string.share_dialog_title)));
|
||||
break;
|
||||
if (id == android.R.id.home) {
|
||||
onBackPressed();
|
||||
} else if (id == R.id.menu_item_share_error) {
|
||||
ShareUtils.shareText(this, getString(R.string.error_report_title), buildJson());
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
private void openPrivacyPolicyDialog(final Context context, final String action) {
|
||||
@@ -281,13 +213,15 @@ public class ErrorActivity extends AppCompatActivity {
|
||||
final Intent i = new Intent(Intent.ACTION_SENDTO)
|
||||
.setData(Uri.parse("mailto:")) // only email apps should handle this
|
||||
.putExtra(Intent.EXTRA_EMAIL, new String[]{ERROR_EMAIL_ADDRESS})
|
||||
.putExtra(Intent.EXTRA_SUBJECT, ERROR_EMAIL_SUBJECT)
|
||||
.putExtra(Intent.EXTRA_SUBJECT, ERROR_EMAIL_SUBJECT
|
||||
+ getString(R.string.app_name) + " "
|
||||
+ BuildConfig.VERSION_NAME)
|
||||
.putExtra(Intent.EXTRA_TEXT, buildJson());
|
||||
if (i.resolveActivity(getPackageManager()) != null) {
|
||||
startActivity(i);
|
||||
ShareUtils.openIntentInApp(context, i);
|
||||
}
|
||||
} else if (action.equals("GITHUB")) { // open the NewPipe issue page on GitHub
|
||||
ShareUtils.openUrlInBrowser(this, ERROR_GITHUB_ISSUE_URL);
|
||||
ShareUtils.openUrlInBrowser(this, ERROR_GITHUB_ISSUE_URL, false);
|
||||
}
|
||||
|
||||
})
|
||||
@@ -327,23 +261,11 @@ public class ErrorActivity extends AppCompatActivity {
|
||||
return checkedReturnActivity;
|
||||
}
|
||||
|
||||
private void goToReturnActivity() {
|
||||
final Class<? extends Activity> checkedReturnActivity = getReturnActivity(returnActivity);
|
||||
if (checkedReturnActivity == null) {
|
||||
super.onBackPressed();
|
||||
} else {
|
||||
final Intent intent = new Intent(this, checkedReturnActivity);
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
|
||||
NavUtils.navigateUpTo(this, intent);
|
||||
}
|
||||
}
|
||||
|
||||
private void buildInfo(final ErrorInfo info) {
|
||||
final TextView infoLabelView = findViewById(R.id.errorInfoLabelsView);
|
||||
final TextView infoView = findViewById(R.id.errorInfosView);
|
||||
String text = "";
|
||||
|
||||
infoLabelView.setText(getString(R.string.info_labels).replace("\\n", "\n"));
|
||||
activityErrorBinding.errorInfoLabelsView.setText(getString(R.string.info_labels)
|
||||
.replace("\\n", "\n"));
|
||||
|
||||
text += getUserActionString(info.getUserAction()) + "\n"
|
||||
+ info.getRequest() + "\n"
|
||||
@@ -356,7 +278,7 @@ public class ErrorActivity extends AppCompatActivity {
|
||||
+ BuildConfig.VERSION_NAME + "\n"
|
||||
+ getOsString();
|
||||
|
||||
infoView.setText(text);
|
||||
activityErrorBinding.errorInfosView.setText(text);
|
||||
}
|
||||
|
||||
private String buildJson() {
|
||||
@@ -373,8 +295,9 @@ public class ErrorActivity extends AppCompatActivity {
|
||||
.value("version", BuildConfig.VERSION_NAME)
|
||||
.value("os", getOsString())
|
||||
.value("time", currentTimeStamp)
|
||||
.array("exceptions", Arrays.asList(errorList))
|
||||
.value("user_comment", userCommentBox.getText().toString())
|
||||
.array("exceptions", Arrays.asList(errorInfo.getStackTraces()))
|
||||
.value("user_comment", activityErrorBinding.errorCommentBox.getText()
|
||||
.toString())
|
||||
.end()
|
||||
.done();
|
||||
} catch (final Throwable e) {
|
||||
@@ -389,7 +312,7 @@ public class ErrorActivity extends AppCompatActivity {
|
||||
try {
|
||||
final StringBuilder htmlErrorReport = new StringBuilder();
|
||||
|
||||
final String userComment = userCommentBox.getText().toString();
|
||||
final String userComment = activityErrorBinding.errorCommentBox.getText().toString();
|
||||
if (!userComment.isEmpty()) {
|
||||
htmlErrorReport.append(userComment).append("\n");
|
||||
}
|
||||
@@ -410,27 +333,27 @@ public class ErrorActivity extends AppCompatActivity {
|
||||
|
||||
// Collapse all logs to a single paragraph when there are more than one
|
||||
// to keep the GitHub issue clean.
|
||||
if (errorList.length > 1) {
|
||||
if (errorInfo.getStackTraces().length > 1) {
|
||||
htmlErrorReport
|
||||
.append("<details><summary><b>Exceptions (")
|
||||
.append(errorList.length)
|
||||
.append(errorInfo.getStackTraces().length)
|
||||
.append(")</b></summary><p>\n");
|
||||
}
|
||||
|
||||
// add the logs
|
||||
for (int i = 0; i < errorList.length; i++) {
|
||||
for (int i = 0; i < errorInfo.getStackTraces().length; i++) {
|
||||
htmlErrorReport.append("<details><summary><b>Crash log ");
|
||||
if (errorList.length > 1) {
|
||||
if (errorInfo.getStackTraces().length > 1) {
|
||||
htmlErrorReport.append(i + 1);
|
||||
}
|
||||
htmlErrorReport.append("</b>")
|
||||
.append("</summary><p>\n")
|
||||
.append("\n```\n").append(errorList[i]).append("\n```\n")
|
||||
.append("\n```\n").append(errorInfo.getStackTraces()[i]).append("\n```\n")
|
||||
.append("</details>\n");
|
||||
}
|
||||
|
||||
// make sure to close everything
|
||||
if (errorList.length > 1) {
|
||||
if (errorInfo.getStackTraces().length > 1) {
|
||||
htmlErrorReport.append("</p></details>\n");
|
||||
}
|
||||
htmlErrorReport.append("<hr>\n");
|
||||
@@ -473,22 +396,8 @@ public class ErrorActivity extends AppCompatActivity {
|
||||
|
||||
private void addGuruMeditation() {
|
||||
//just an easter egg
|
||||
final TextView sorryView = findViewById(R.id.errorSorryView);
|
||||
String text = sorryView.getText().toString();
|
||||
String text = activityErrorBinding.errorSorryView.getText().toString();
|
||||
text += "\n" + getString(R.string.guru_meditation);
|
||||
sorryView.setText(text);
|
||||
activityErrorBinding.errorSorryView.setText(text);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
//super.onBackPressed();
|
||||
goToReturnActivity();
|
||||
}
|
||||
|
||||
public String getCurrentTimeStamp() {
|
||||
final SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm");
|
||||
df.setTimeZone(TimeZone.getTimeZone("GMT"));
|
||||
return df.format(new Date());
|
||||
}
|
||||
|
||||
}
|
||||
113
app/src/main/java/org/schabi/newpipe/error/ErrorInfo.kt
Normal file
113
app/src/main/java/org/schabi/newpipe/error/ErrorInfo.kt
Normal file
@@ -0,0 +1,113 @@
|
||||
package org.schabi.newpipe.error
|
||||
|
||||
import android.os.Parcelable
|
||||
import androidx.annotation.StringRes
|
||||
import kotlinx.android.parcel.Parcelize
|
||||
import org.schabi.newpipe.R
|
||||
import org.schabi.newpipe.extractor.Info
|
||||
import org.schabi.newpipe.extractor.NewPipe
|
||||
import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException
|
||||
import org.schabi.newpipe.extractor.exceptions.ContentNotSupportedException
|
||||
import org.schabi.newpipe.extractor.exceptions.ExtractionException
|
||||
import org.schabi.newpipe.extractor.services.youtube.extractors.YoutubeStreamExtractor.DeobfuscateException
|
||||
import org.schabi.newpipe.ktx.isNetworkRelated
|
||||
import java.io.PrintWriter
|
||||
import java.io.StringWriter
|
||||
|
||||
@Parcelize
|
||||
class ErrorInfo(
|
||||
val stackTraces: Array<String>,
|
||||
val userAction: UserAction,
|
||||
val serviceName: String,
|
||||
val request: String,
|
||||
val messageStringId: Int,
|
||||
@Transient // no need to store throwable, all data for report is in other variables
|
||||
var throwable: Throwable? = null
|
||||
) : Parcelable {
|
||||
|
||||
private constructor(
|
||||
throwable: Throwable,
|
||||
userAction: UserAction,
|
||||
serviceName: String,
|
||||
request: String
|
||||
) : this(
|
||||
throwableToStringList(throwable),
|
||||
userAction,
|
||||
serviceName,
|
||||
request,
|
||||
getMessageStringId(throwable, userAction),
|
||||
throwable
|
||||
)
|
||||
|
||||
private constructor(
|
||||
throwable: List<Throwable>,
|
||||
userAction: UserAction,
|
||||
serviceName: String,
|
||||
request: String
|
||||
) : this(
|
||||
throwableListToStringList(throwable),
|
||||
userAction,
|
||||
serviceName,
|
||||
request,
|
||||
getMessageStringId(throwable.firstOrNull(), userAction),
|
||||
throwable.firstOrNull()
|
||||
)
|
||||
|
||||
// constructors with single throwable
|
||||
constructor(throwable: Throwable, userAction: UserAction, request: String) :
|
||||
this(throwable, userAction, SERVICE_NONE, request)
|
||||
constructor(throwable: Throwable, userAction: UserAction, request: String, serviceId: Int) :
|
||||
this(throwable, userAction, NewPipe.getNameOfService(serviceId), request)
|
||||
constructor(throwable: Throwable, userAction: UserAction, request: String, info: Info?) :
|
||||
this(throwable, userAction, getInfoServiceName(info), request)
|
||||
|
||||
// constructors with list of throwables
|
||||
constructor(throwable: List<Throwable>, userAction: UserAction, request: String) :
|
||||
this(throwable, userAction, SERVICE_NONE, request)
|
||||
constructor(throwable: List<Throwable>, userAction: UserAction, request: String, serviceId: Int) :
|
||||
this(throwable, userAction, NewPipe.getNameOfService(serviceId), request)
|
||||
constructor(throwable: List<Throwable>, userAction: UserAction, request: String, info: Info?) :
|
||||
this(throwable, userAction, getInfoServiceName(info), request)
|
||||
|
||||
companion object {
|
||||
const val SERVICE_NONE = "none"
|
||||
|
||||
private fun getStackTrace(throwable: Throwable): String {
|
||||
StringWriter().use { stringWriter ->
|
||||
PrintWriter(stringWriter, true).use { printWriter ->
|
||||
throwable.printStackTrace(printWriter)
|
||||
return stringWriter.buffer.toString()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun throwableToStringList(throwable: Throwable) = arrayOf(getStackTrace(throwable))
|
||||
|
||||
fun throwableListToStringList(throwable: List<Throwable>) =
|
||||
Array(throwable.size) { i -> getStackTrace(throwable[i]) }
|
||||
|
||||
private fun getInfoServiceName(info: Info?) =
|
||||
if (info == null) SERVICE_NONE else NewPipe.getNameOfService(info.serviceId)
|
||||
|
||||
@StringRes
|
||||
private fun getMessageStringId(
|
||||
throwable: Throwable?,
|
||||
action: UserAction
|
||||
): Int {
|
||||
return when {
|
||||
throwable is ContentNotAvailableException -> R.string.content_not_available
|
||||
throwable != null && throwable.isNetworkRelated -> R.string.network_error
|
||||
throwable is ContentNotSupportedException -> R.string.content_not_supported
|
||||
throwable is DeobfuscateException -> R.string.youtube_signature_deobfuscation_error
|
||||
throwable is ExtractionException -> R.string.parsing_error
|
||||
action == UserAction.UI_ERROR -> R.string.app_ui_crash
|
||||
action == UserAction.REQUESTED_COMMENTS -> R.string.error_unable_to_load_comments
|
||||
action == UserAction.SUBSCRIPTION_CHANGE -> R.string.subscription_change_failed
|
||||
action == UserAction.SUBSCRIPTION_UPDATE -> R.string.subscription_update_failed
|
||||
action == UserAction.LOAD_IMAGE -> R.string.could_not_load_thumbnails
|
||||
action == UserAction.DOWNLOAD_OPEN_DIALOG -> R.string.could_not_setup_download_menu
|
||||
else -> R.string.general_error
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
132
app/src/main/java/org/schabi/newpipe/error/ErrorPanelHelper.kt
Normal file
132
app/src/main/java/org/schabi/newpipe/error/ErrorPanelHelper.kt
Normal file
@@ -0,0 +1,132 @@
|
||||
package org.schabi.newpipe.error
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.util.Log
|
||||
import android.view.View
|
||||
import android.widget.Button
|
||||
import android.widget.TextView
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.fragment.app.Fragment
|
||||
import com.jakewharton.rxbinding4.view.clicks
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.disposables.Disposable
|
||||
import org.schabi.newpipe.MainActivity
|
||||
import org.schabi.newpipe.R
|
||||
import org.schabi.newpipe.extractor.exceptions.AgeRestrictedContentException
|
||||
import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException
|
||||
import org.schabi.newpipe.extractor.exceptions.ContentNotSupportedException
|
||||
import org.schabi.newpipe.extractor.exceptions.GeographicRestrictionException
|
||||
import org.schabi.newpipe.extractor.exceptions.PaidContentException
|
||||
import org.schabi.newpipe.extractor.exceptions.PrivateContentException
|
||||
import org.schabi.newpipe.extractor.exceptions.ReCaptchaException
|
||||
import org.schabi.newpipe.extractor.exceptions.SoundCloudGoPlusContentException
|
||||
import org.schabi.newpipe.extractor.exceptions.YoutubeMusicPremiumContentException
|
||||
import org.schabi.newpipe.ktx.animate
|
||||
import org.schabi.newpipe.ktx.isInterruptedCaused
|
||||
import org.schabi.newpipe.ktx.isNetworkRelated
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class ErrorPanelHelper(
|
||||
private val fragment: Fragment,
|
||||
rootView: View,
|
||||
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 errorButtonAction: Button = errorPanelRoot.findViewById(R.id.error_button_action)
|
||||
private val errorButtonRetry: Button = errorPanelRoot.findViewById(R.id.error_button_retry)
|
||||
|
||||
private var errorDisposable: Disposable? = null
|
||||
|
||||
init {
|
||||
errorDisposable = errorButtonRetry.clicks()
|
||||
.debounce(300, TimeUnit.MILLISECONDS)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe { onRetry.run() }
|
||||
}
|
||||
|
||||
fun showError(errorInfo: ErrorInfo) {
|
||||
|
||||
if (errorInfo.throwable != null && errorInfo.throwable!!.isInterruptedCaused) {
|
||||
if (DEBUG) {
|
||||
Log.w(TAG, "onError() isInterruptedCaused! = [$errorInfo.throwable]")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
errorButtonAction.isVisible = true
|
||||
if (errorInfo.throwable is ReCaptchaException) {
|
||||
errorButtonAction.setText(R.string.recaptcha_solve)
|
||||
errorButtonAction.setOnClickListener {
|
||||
// Starting ReCaptcha Challenge Activity
|
||||
val intent = Intent(context, ReCaptchaActivity::class.java)
|
||||
intent.putExtra(
|
||||
ReCaptchaActivity.RECAPTCHA_URL_EXTRA,
|
||||
(errorInfo.throwable as ReCaptchaException).url
|
||||
)
|
||||
fragment.startActivityForResult(intent, ReCaptchaActivity.RECAPTCHA_REQUEST)
|
||||
errorButtonAction.setOnClickListener(null)
|
||||
}
|
||||
errorTextView.setText(R.string.recaptcha_request_toast)
|
||||
errorButtonRetry.isVisible = true
|
||||
} else {
|
||||
errorButtonAction.setText(R.string.error_snackbar_action)
|
||||
errorButtonAction.setOnClickListener {
|
||||
ErrorActivity.reportError(context, errorInfo)
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
errorPanelRoot.animate(true, 300)
|
||||
}
|
||||
|
||||
fun showTextError(errorString: String) {
|
||||
errorButtonAction.isVisible = false
|
||||
errorButtonRetry.isVisible = false
|
||||
errorTextView.text = errorString
|
||||
}
|
||||
|
||||
fun hide() {
|
||||
errorButtonAction.setOnClickListener(null)
|
||||
errorPanelRoot.animate(false, 150)
|
||||
}
|
||||
|
||||
fun isVisible(): Boolean {
|
||||
return errorPanelRoot.isVisible
|
||||
}
|
||||
|
||||
fun dispose() {
|
||||
errorButtonAction.setOnClickListener(null)
|
||||
errorButtonRetry.setOnClickListener(null)
|
||||
errorDisposable?.dispose()
|
||||
}
|
||||
|
||||
companion object {
|
||||
val TAG: String = ErrorPanelHelper::class.simpleName!!
|
||||
val DEBUG: Boolean = MainActivity.DEBUG
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.schabi.newpipe;
|
||||
package org.schabi.newpipe.error;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
@@ -8,20 +8,21 @@ import android.util.Log;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.webkit.CookieManager;
|
||||
import android.webkit.WebResourceRequest;
|
||||
import android.webkit.WebSettings;
|
||||
import android.webkit.WebView;
|
||||
import android.webkit.WebViewClient;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.RequiresApi;
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.core.app.NavUtils;
|
||||
import androidx.preference.PreferenceManager;
|
||||
import androidx.webkit.WebViewClientCompat;
|
||||
|
||||
import org.schabi.newpipe.databinding.ActivityRecaptchaBinding;
|
||||
import org.schabi.newpipe.DownloaderImpl;
|
||||
import org.schabi.newpipe.MainActivity;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.util.ThemeHelper;
|
||||
|
||||
import java.io.UnsupportedEncodingException;
|
||||
@@ -53,46 +54,37 @@ public class ReCaptchaActivity extends AppCompatActivity {
|
||||
public static final String YT_URL = "https://www.youtube.com";
|
||||
public static final String RECAPTCHA_COOKIES_KEY = "recaptcha_cookies";
|
||||
|
||||
private WebView webView;
|
||||
public static String sanitizeRecaptchaUrl(@Nullable final String url) {
|
||||
if (url == null || url.trim().isEmpty()) {
|
||||
return YT_URL; // YouTube is the most likely service to have thrown a recaptcha
|
||||
} else {
|
||||
// remove "pbj=1" parameter from YouYube urls, as it makes the page JSON and not HTML
|
||||
return url.replace("&pbj=1", "").replace("pbj=1&", "").replace("?pbj=1", "");
|
||||
}
|
||||
}
|
||||
|
||||
private ActivityRecaptchaBinding recaptchaBinding;
|
||||
private String foundCookies = "";
|
||||
|
||||
@Override
|
||||
protected void onCreate(final Bundle savedInstanceState) {
|
||||
ThemeHelper.setTheme(this);
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_recaptcha);
|
||||
final Toolbar toolbar = findViewById(R.id.toolbar);
|
||||
setSupportActionBar(toolbar);
|
||||
|
||||
String url = getIntent().getStringExtra(RECAPTCHA_URL_EXTRA);
|
||||
if (url == null || url.isEmpty()) {
|
||||
url = YT_URL;
|
||||
}
|
||||
recaptchaBinding = ActivityRecaptchaBinding.inflate(getLayoutInflater());
|
||||
setContentView(recaptchaBinding.getRoot());
|
||||
setSupportActionBar(recaptchaBinding.toolbar);
|
||||
|
||||
final String url = sanitizeRecaptchaUrl(getIntent().getStringExtra(RECAPTCHA_URL_EXTRA));
|
||||
// set return to Cancel by default
|
||||
setResult(RESULT_CANCELED);
|
||||
|
||||
|
||||
webView = findViewById(R.id.reCaptchaWebView);
|
||||
|
||||
// enable Javascript
|
||||
final WebSettings webSettings = webView.getSettings();
|
||||
final WebSettings webSettings = recaptchaBinding.reCaptchaWebView.getSettings();
|
||||
webSettings.setJavaScriptEnabled(true);
|
||||
webSettings.setUserAgentString(DownloaderImpl.USER_AGENT);
|
||||
|
||||
webView.setWebViewClient(new WebViewClient() {
|
||||
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
|
||||
@Override
|
||||
public boolean shouldOverrideUrlLoading(final WebView view,
|
||||
final WebResourceRequest request) {
|
||||
final String url = request.getUrl().toString();
|
||||
if (MainActivity.DEBUG) {
|
||||
Log.d(TAG, "shouldOverrideUrlLoading: request.url=" + url);
|
||||
}
|
||||
|
||||
handleCookiesFromUrl(url);
|
||||
return false;
|
||||
}
|
||||
|
||||
recaptchaBinding.reCaptchaWebView.setWebViewClient(new WebViewClientCompat() {
|
||||
@Override
|
||||
public boolean shouldOverrideUrlLoading(final WebView view, final String url) {
|
||||
if (MainActivity.DEBUG) {
|
||||
@@ -111,17 +103,16 @@ public class ReCaptchaActivity extends AppCompatActivity {
|
||||
});
|
||||
|
||||
// cleaning cache, history and cookies from webView
|
||||
webView.clearCache(true);
|
||||
webView.clearHistory();
|
||||
final android.webkit.CookieManager cookieManager = CookieManager.getInstance();
|
||||
recaptchaBinding.reCaptchaWebView.clearCache(true);
|
||||
recaptchaBinding.reCaptchaWebView.clearHistory();
|
||||
final CookieManager cookieManager = CookieManager.getInstance();
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
cookieManager.removeAllCookies(aBoolean -> {
|
||||
});
|
||||
cookieManager.removeAllCookies(value -> { });
|
||||
} else {
|
||||
cookieManager.removeAllCookie();
|
||||
}
|
||||
|
||||
webView.loadUrl(url);
|
||||
recaptchaBinding.reCaptchaWebView.loadUrl(url);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -145,18 +136,16 @@ public class ReCaptchaActivity extends AppCompatActivity {
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(final MenuItem item) {
|
||||
final int id = item.getItemId();
|
||||
switch (id) {
|
||||
case R.id.menu_item_done:
|
||||
saveCookiesAndFinish();
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
if (item.getItemId() == R.id.menu_item_done) {
|
||||
saveCookiesAndFinish();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private void saveCookiesAndFinish() {
|
||||
handleCookiesFromUrl(webView.getUrl()); // try to get cookies of unclosed page
|
||||
// try to get cookies of unclosed page
|
||||
handleCookiesFromUrl(recaptchaBinding.reCaptchaWebView.getUrl());
|
||||
if (MainActivity.DEBUG) {
|
||||
Log.d(TAG, "saveCookiesAndFinish: foundCookies=" + foundCookies);
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.schabi.newpipe.report;
|
||||
package org.schabi.newpipe.error;
|
||||
|
||||
/**
|
||||
* The user actions that can cause an error.
|
||||
@@ -6,9 +6,12 @@ package org.schabi.newpipe.report;
|
||||
public enum UserAction {
|
||||
USER_REPORT("user report"),
|
||||
UI_ERROR("ui error"),
|
||||
SUBSCRIPTION("subscription"),
|
||||
SUBSCRIPTION_CHANGE("subscription change"),
|
||||
SUBSCRIPTION_UPDATE("subscription update"),
|
||||
SUBSCRIPTION_GET("get subscription"),
|
||||
SUBSCRIPTION_IMPORT_EXPORT("subscription import or export"),
|
||||
LOAD_IMAGE("load image"),
|
||||
SOMETHING_ELSE("something"),
|
||||
SOMETHING_ELSE("something else"),
|
||||
SEARCHED("searched"),
|
||||
GET_SUGGESTIONS("get suggestions"),
|
||||
REQUESTED_STREAM("requested stream"),
|
||||
@@ -17,11 +20,15 @@ public enum UserAction {
|
||||
REQUESTED_KIOSK("requested kiosk"),
|
||||
REQUESTED_COMMENTS("requested comments"),
|
||||
REQUESTED_FEED("requested feed"),
|
||||
REQUESTED_BOOKMARK("bookmark"),
|
||||
DELETE_FROM_HISTORY("delete from history"),
|
||||
PLAY_STREAM("Play stream"),
|
||||
PLAY_STREAM("play stream"),
|
||||
DOWNLOAD_OPEN_DIALOG("download open dialog"),
|
||||
DOWNLOAD_POSTPROCESSING("download post-processing"),
|
||||
DOWNLOAD_FAILED("download failed"),
|
||||
PREFERENCES_MIGRATION("migration of preferences");
|
||||
PREFERENCES_MIGRATION("migration of preferences"),
|
||||
SHARE_TO_NEWPIPE("share to newpipe"),
|
||||
CHECK_FOR_NEW_APP_VERSION("check for new app version");
|
||||
|
||||
|
||||
private final String message;
|
||||
@@ -1,43 +1,25 @@
|
||||
package org.schabi.newpipe.fragments;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
import android.widget.Button;
|
||||
import android.widget.ProgressBar;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.StringRes;
|
||||
|
||||
import com.jakewharton.rxbinding4.view.RxView;
|
||||
|
||||
import org.schabi.newpipe.BaseFragment;
|
||||
import org.schabi.newpipe.MainActivity;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.ReCaptchaActivity;
|
||||
import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException;
|
||||
import org.schabi.newpipe.extractor.exceptions.ContentNotSupportedException;
|
||||
import org.schabi.newpipe.extractor.exceptions.ReCaptchaException;
|
||||
import org.schabi.newpipe.report.ErrorActivity;
|
||||
import org.schabi.newpipe.report.ErrorInfo;
|
||||
import org.schabi.newpipe.report.UserAction;
|
||||
import org.schabi.newpipe.util.ExceptionUtils;
|
||||
import org.schabi.newpipe.error.ErrorActivity;
|
||||
import org.schabi.newpipe.error.ErrorInfo;
|
||||
import org.schabi.newpipe.error.ErrorPanelHelper;
|
||||
import org.schabi.newpipe.util.InfoCache;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
import icepick.State;
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.rxjava3.disposables.Disposable;
|
||||
|
||||
import static org.schabi.newpipe.util.AnimationUtils.animateView;
|
||||
import static org.schabi.newpipe.ktx.ViewUtils.animate;
|
||||
|
||||
public abstract class BaseStateFragment<I> extends BaseFragment implements ViewContract<I> {
|
||||
@State
|
||||
@@ -49,14 +31,13 @@ public abstract class BaseStateFragment<I> extends BaseFragment implements ViewC
|
||||
@Nullable
|
||||
private ProgressBar loadingProgressBar;
|
||||
|
||||
private Disposable errorDisposable;
|
||||
|
||||
protected View errorPanelRoot;
|
||||
private Button errorButtonRetry;
|
||||
private TextView errorTextView;
|
||||
private ErrorPanelHelper errorPanelHelper;
|
||||
@Nullable
|
||||
@State
|
||||
protected ErrorInfo lastPanelError = null;
|
||||
|
||||
@Override
|
||||
public void onViewCreated(final View rootView, final Bundle savedInstanceState) {
|
||||
public void onViewCreated(@NonNull final View rootView, final Bundle savedInstanceState) {
|
||||
super.onViewCreated(rootView, savedInstanceState);
|
||||
doInitialLoadLogic();
|
||||
}
|
||||
@@ -68,10 +49,10 @@ public abstract class BaseStateFragment<I> extends BaseFragment implements ViewC
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
if (errorDisposable != null) {
|
||||
errorDisposable.dispose();
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
if (lastPanelError != null) {
|
||||
showError(lastPanelError);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,22 +63,17 @@ public abstract class BaseStateFragment<I> extends BaseFragment implements ViewC
|
||||
@Override
|
||||
protected void initViews(final View rootView, final Bundle savedInstanceState) {
|
||||
super.initViews(rootView, savedInstanceState);
|
||||
|
||||
emptyStateView = rootView.findViewById(R.id.empty_state_view);
|
||||
loadingProgressBar = rootView.findViewById(R.id.loading_progress_bar);
|
||||
|
||||
errorPanelRoot = rootView.findViewById(R.id.error_panel);
|
||||
errorButtonRetry = rootView.findViewById(R.id.error_button_retry);
|
||||
errorTextView = rootView.findViewById(R.id.error_message_view);
|
||||
errorPanelHelper = new ErrorPanelHelper(this, rootView, this::onRetryButtonClicked);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void initListeners() {
|
||||
super.initListeners();
|
||||
errorDisposable = RxView.clicks(errorButtonRetry)
|
||||
.debounce(300, TimeUnit.MILLISECONDS)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(o -> onRetryButtonClicked());
|
||||
public void onDestroyView() {
|
||||
super.onDestroyView();
|
||||
if (errorPanelHelper != null) {
|
||||
errorPanelHelper.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
protected void onRetryButtonClicked() {
|
||||
@@ -131,54 +107,34 @@ public abstract class BaseStateFragment<I> extends BaseFragment implements ViewC
|
||||
@Override
|
||||
public void showLoading() {
|
||||
if (emptyStateView != null) {
|
||||
animateView(emptyStateView, false, 150);
|
||||
animate(emptyStateView, false, 150);
|
||||
}
|
||||
if (loadingProgressBar != null) {
|
||||
animateView(loadingProgressBar, true, 400);
|
||||
animate(loadingProgressBar, true, 400);
|
||||
}
|
||||
animateView(errorPanelRoot, false, 150);
|
||||
hideErrorPanel();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void hideLoading() {
|
||||
if (emptyStateView != null) {
|
||||
animateView(emptyStateView, false, 150);
|
||||
animate(emptyStateView, false, 150);
|
||||
}
|
||||
if (loadingProgressBar != null) {
|
||||
animateView(loadingProgressBar, false, 0);
|
||||
animate(loadingProgressBar, false, 0);
|
||||
}
|
||||
animateView(errorPanelRoot, false, 150);
|
||||
hideErrorPanel();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void showEmptyState() {
|
||||
isLoading.set(false);
|
||||
if (emptyStateView != null) {
|
||||
animateView(emptyStateView, true, 200);
|
||||
animate(emptyStateView, true, 200);
|
||||
}
|
||||
if (loadingProgressBar != null) {
|
||||
animateView(loadingProgressBar, false, 0);
|
||||
animate(loadingProgressBar, false, 0);
|
||||
}
|
||||
animateView(errorPanelRoot, false, 150);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void showError(final String message, final boolean showRetryButton) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "showError() called with: "
|
||||
+ "message = [" + message + "], showRetryButton = [" + showRetryButton + "]");
|
||||
}
|
||||
isLoading.set(false);
|
||||
InfoCache.getInstance().clearCache();
|
||||
hideLoading();
|
||||
|
||||
errorTextView.setText(message);
|
||||
if (showRetryButton) {
|
||||
animateView(errorButtonRetry, true, 600);
|
||||
} else {
|
||||
animateView(errorButtonRetry, false, 0);
|
||||
}
|
||||
animateView(errorPanelRoot, true, 300);
|
||||
hideErrorPanel();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -189,120 +145,69 @@ public abstract class BaseStateFragment<I> extends BaseFragment implements ViewC
|
||||
hideLoading();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleError() {
|
||||
isLoading.set(false);
|
||||
InfoCache.getInstance().clearCache();
|
||||
if (emptyStateView != null) {
|
||||
animate(emptyStateView, false, 150);
|
||||
}
|
||||
if (loadingProgressBar != null) {
|
||||
animate(loadingProgressBar, false, 0);
|
||||
}
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Error handling
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
/**
|
||||
* Default implementation handles some general exceptions.
|
||||
*
|
||||
* @param exception The exception that should be handled
|
||||
* @return If the exception was handled
|
||||
*/
|
||||
protected boolean onError(final Throwable exception) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "onError() called with: exception = [" + exception + "]");
|
||||
}
|
||||
isLoading.set(false);
|
||||
public final void showError(final ErrorInfo errorInfo) {
|
||||
handleError();
|
||||
|
||||
if (isDetached() || isRemoving()) {
|
||||
if (DEBUG) {
|
||||
Log.w(TAG, "onError() is detached or removing = [" + exception + "]");
|
||||
Log.w(TAG, "showError() is detached or removing = [" + errorInfo + "]");
|
||||
}
|
||||
return true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (ExceptionUtils.isInterruptedCaused(exception)) {
|
||||
errorPanelHelper.showError(errorInfo);
|
||||
lastPanelError = errorInfo;
|
||||
}
|
||||
|
||||
public final void showTextError(@NonNull final String errorString) {
|
||||
handleError();
|
||||
|
||||
if (isDetached() || isRemoving()) {
|
||||
if (DEBUG) {
|
||||
Log.w(TAG, "onError() isInterruptedCaused! = [" + exception + "]");
|
||||
Log.w(TAG, "showTextError() is detached or removing = [" + errorString + "]");
|
||||
}
|
||||
return true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (exception instanceof ReCaptchaException) {
|
||||
onReCaptchaException((ReCaptchaException) exception);
|
||||
return true;
|
||||
} else if (exception instanceof ContentNotAvailableException) {
|
||||
showError(getString(R.string.content_not_available), false);
|
||||
return true;
|
||||
} else if (ExceptionUtils.isNetworkRelated(exception)) {
|
||||
showError(getString(R.string.network_error), true);
|
||||
return true;
|
||||
} else if (exception instanceof ContentNotSupportedException) {
|
||||
showError(getString(R.string.content_not_supported), false);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
errorPanelHelper.showTextError(errorString);
|
||||
}
|
||||
|
||||
public void onReCaptchaException(final ReCaptchaException exception) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "onReCaptchaException() called");
|
||||
}
|
||||
Toast.makeText(activity, R.string.recaptcha_request_toast, Toast.LENGTH_LONG).show();
|
||||
// Starting ReCaptcha Challenge Activity
|
||||
final Intent intent = new Intent(activity, ReCaptchaActivity.class);
|
||||
intent.putExtra(ReCaptchaActivity.RECAPTCHA_URL_EXTRA, exception.getUrl());
|
||||
startActivityForResult(intent, ReCaptchaActivity.RECAPTCHA_REQUEST);
|
||||
|
||||
showError(getString(R.string.recaptcha_request_toast), false);
|
||||
public final void hideErrorPanel() {
|
||||
errorPanelHelper.hide();
|
||||
lastPanelError = null;
|
||||
}
|
||||
|
||||
public void onUnrecoverableError(final Throwable exception, final UserAction userAction,
|
||||
final String serviceName, final String request,
|
||||
@StringRes final int errorId) {
|
||||
onUnrecoverableError(Collections.singletonList(exception), userAction, serviceName,
|
||||
request, errorId);
|
||||
}
|
||||
|
||||
public void onUnrecoverableError(final List<Throwable> exception, final UserAction userAction,
|
||||
final String serviceName, final String request,
|
||||
@StringRes final int errorId) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "onUnrecoverableError() called with: exception = [" + exception + "]");
|
||||
}
|
||||
|
||||
ErrorActivity.reportError(getContext(), exception, MainActivity.class, null,
|
||||
ErrorInfo.make(userAction, serviceName == null ? "none" : serviceName,
|
||||
request == null ? "none" : request, errorId));
|
||||
}
|
||||
|
||||
public void showSnackBarError(final Throwable exception, final UserAction userAction,
|
||||
final String serviceName, final String request,
|
||||
@StringRes final int errorId) {
|
||||
showSnackBarError(Collections.singletonList(exception), userAction, serviceName, request,
|
||||
errorId);
|
||||
public final boolean isErrorPanelVisible() {
|
||||
return errorPanelHelper.isVisible();
|
||||
}
|
||||
|
||||
/**
|
||||
* Show a SnackBar and only call
|
||||
* {@link ErrorActivity#reportError(Context, List, Class, View, ErrorInfo)}
|
||||
* {@link ErrorActivity#reportErrorInSnackbar(androidx.fragment.app.Fragment, ErrorInfo)}
|
||||
* IF we a find a valid view (otherwise the error screen appears).
|
||||
*
|
||||
* @param exception List of the exceptions to show
|
||||
* @param userAction The user action that caused the exception
|
||||
* @param serviceName The service where the exception happened
|
||||
* @param request The page that was requested
|
||||
* @param errorId The ID of the error
|
||||
* @param errorInfo The error information
|
||||
*/
|
||||
public void showSnackBarError(final List<Throwable> exception, final UserAction userAction,
|
||||
final String serviceName, final String request,
|
||||
@StringRes final int errorId) {
|
||||
public void showSnackBarError(final ErrorInfo errorInfo) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "showSnackBarError() called with: "
|
||||
+ "exception = [" + exception + "], userAction = [" + userAction + "], "
|
||||
+ "request = [" + request + "], errorId = [" + errorId + "]");
|
||||
Log.d(TAG, "showSnackBarError() called with: errorInfo = [" + errorInfo + "]");
|
||||
}
|
||||
View rootView = activity != null ? activity.findViewById(android.R.id.content) : null;
|
||||
if (rootView == null && getView() != null) {
|
||||
rootView = getView();
|
||||
}
|
||||
if (rootView == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
ErrorActivity.reportError(getContext(), exception, MainActivity.class, rootView,
|
||||
ErrorInfo.make(userAction, serviceName, request, errorId));
|
||||
ErrorActivity.reportErrorInSnackbar(this, errorInfo);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,9 +11,18 @@ import org.schabi.newpipe.BaseFragment;
|
||||
import org.schabi.newpipe.R;
|
||||
|
||||
public class EmptyFragment extends BaseFragment {
|
||||
final boolean showMessage;
|
||||
|
||||
public EmptyFragment(final boolean showMessage) {
|
||||
this.showMessage = showMessage;
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(final LayoutInflater inflater, @Nullable final ViewGroup container,
|
||||
final Bundle savedInstanceState) {
|
||||
return inflater.inflate(R.layout.fragment_empty, container, false);
|
||||
final View view = inflater.inflate(R.layout.fragment_empty, container, false);
|
||||
view.findViewById(R.id.empty_state_view).setVisibility(
|
||||
showMessage ? View.VISIBLE : View.GONE);
|
||||
return view;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ package org.schabi.newpipe.fragments;
|
||||
import android.content.Context;
|
||||
import android.content.res.ColorStateList;
|
||||
import android.os.Bundle;
|
||||
import androidx.preference.PreferenceManager;
|
||||
import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
@@ -15,34 +14,30 @@ import android.view.ViewGroup;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
import androidx.fragment.app.FragmentStatePagerAdapterMenuWorkaround;
|
||||
import androidx.viewpager.widget.ViewPager;
|
||||
import androidx.preference.PreferenceManager;
|
||||
|
||||
import com.google.android.material.tabs.TabLayout;
|
||||
|
||||
import org.schabi.newpipe.BaseFragment;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.databinding.FragmentMainBinding;
|
||||
import org.schabi.newpipe.error.ErrorActivity;
|
||||
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||
import org.schabi.newpipe.report.ErrorActivity;
|
||||
import org.schabi.newpipe.report.ErrorInfo;
|
||||
import org.schabi.newpipe.report.UserAction;
|
||||
import org.schabi.newpipe.settings.tabs.Tab;
|
||||
import org.schabi.newpipe.settings.tabs.TabsManager;
|
||||
import org.schabi.newpipe.util.NavigationHelper;
|
||||
import org.schabi.newpipe.util.ServiceHelper;
|
||||
import org.schabi.newpipe.util.ThemeHelper;
|
||||
import org.schabi.newpipe.views.ScrollableTabLayout;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class MainFragment extends BaseFragment implements TabLayout.OnTabSelectedListener {
|
||||
private ViewPager viewPager;
|
||||
private FragmentMainBinding binding;
|
||||
private SelectedTabsPagerAdapter pagerAdapter;
|
||||
private ScrollableTabLayout tabLayout;
|
||||
|
||||
private final List<Tab> tabsList = new ArrayList<>();
|
||||
private TabsManager tabsManager;
|
||||
@@ -90,13 +85,12 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte
|
||||
protected void initViews(final View rootView, final Bundle savedInstanceState) {
|
||||
super.initViews(rootView, savedInstanceState);
|
||||
|
||||
tabLayout = rootView.findViewById(R.id.main_tab_layout);
|
||||
viewPager = rootView.findViewById(R.id.pager);
|
||||
binding = FragmentMainBinding.bind(rootView);
|
||||
|
||||
tabLayout.setTabIconTint(ColorStateList.valueOf(
|
||||
binding.mainTabLayout.setTabIconTint(ColorStateList.valueOf(
|
||||
ThemeHelper.resolveColorFromAttr(requireContext(), R.attr.colorAccent)));
|
||||
tabLayout.setupWithViewPager(viewPager);
|
||||
tabLayout.addOnTabSelectedListener(this);
|
||||
binding.mainTabLayout.setupWithViewPager(binding.pager);
|
||||
binding.mainTabLayout.addOnTabSelectedListener(this);
|
||||
|
||||
setupTabs();
|
||||
}
|
||||
@@ -120,8 +114,9 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
tabsManager.unsetSavedTabsListener();
|
||||
if (viewPager != null) {
|
||||
viewPager.setAdapter(null);
|
||||
if (binding != null) {
|
||||
binding.pager.setAdapter(null);
|
||||
binding = null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -130,7 +125,8 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public void onCreateOptionsMenu(final Menu menu, final MenuInflater inflater) {
|
||||
public void onCreateOptionsMenu(@NonNull final Menu menu,
|
||||
@NonNull final MenuInflater inflater) {
|
||||
super.onCreateOptionsMenu(menu, inflater);
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "onCreateOptionsMenu() called with: "
|
||||
@@ -146,15 +142,14 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(final MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case R.id.action_search:
|
||||
try {
|
||||
NavigationHelper.openSearchFragment(getFM(),
|
||||
ServiceHelper.getSelectedServiceId(activity), "");
|
||||
} catch (final Exception e) {
|
||||
ErrorActivity.reportUiError((AppCompatActivity) getActivity(), e);
|
||||
}
|
||||
return true;
|
||||
if (item.getItemId() == R.id.action_search) {
|
||||
try {
|
||||
NavigationHelper.openSearchFragment(getFM(),
|
||||
ServiceHelper.getSelectedServiceId(activity), "");
|
||||
} catch (final Exception e) {
|
||||
ErrorActivity.reportUiErrorInSnackbar(this, "Opening search fragment", e);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
@@ -172,19 +167,19 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte
|
||||
getChildFragmentManager(), tabsList);
|
||||
}
|
||||
|
||||
viewPager.setAdapter(null);
|
||||
viewPager.setOffscreenPageLimit(tabsList.size());
|
||||
viewPager.setAdapter(pagerAdapter);
|
||||
binding.pager.setAdapter(null);
|
||||
binding.pager.setOffscreenPageLimit(tabsList.size());
|
||||
binding.pager.setAdapter(pagerAdapter);
|
||||
|
||||
updateTabsIconAndDescription();
|
||||
updateTitleForTab(viewPager.getCurrentItem());
|
||||
updateTitleForTab(binding.pager.getCurrentItem());
|
||||
|
||||
hasTabsChanged = false;
|
||||
}
|
||||
|
||||
private void updateTabsIconAndDescription() {
|
||||
for (int i = 0; i < tabsList.size(); i++) {
|
||||
final TabLayout.Tab tabToSet = tabLayout.getTabAt(i);
|
||||
final TabLayout.Tab tabToSet = binding.mainTabLayout.getTabAt(i);
|
||||
if (tabToSet != null) {
|
||||
final Tab tab = tabsList.get(i);
|
||||
tabToSet.setIcon(tab.getTabIconRes(requireContext()));
|
||||
@@ -243,8 +238,7 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte
|
||||
}
|
||||
|
||||
if (throwable != null) {
|
||||
ErrorActivity.reportError(context, throwable, null, null, ErrorInfo
|
||||
.make(UserAction.UI_ERROR, "none", "", R.string.app_ui_crash));
|
||||
ErrorActivity.reportUiErrorInSnackbar(context, "Getting fragment item", throwable);
|
||||
return new BlankFragment();
|
||||
}
|
||||
|
||||
@@ -256,7 +250,7 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemPosition(final Object object) {
|
||||
public int getItemPosition(@NonNull final Object object) {
|
||||
// Causes adapter to reload all Fragments when
|
||||
// notifyDataSetChanged is called
|
||||
return POSITION_NONE;
|
||||
|
||||
@@ -7,7 +7,7 @@ public interface ViewContract<I> {
|
||||
|
||||
void showEmptyState();
|
||||
|
||||
void showError(String message, boolean showRetryButton);
|
||||
|
||||
void handleResult(I result);
|
||||
|
||||
void handleError();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
package org.schabi.newpipe.fragments.detail;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.text.HtmlCompat;
|
||||
|
||||
import org.schabi.newpipe.BaseFragment;
|
||||
import org.schabi.newpipe.databinding.FragmentDescriptionBinding;
|
||||
import org.schabi.newpipe.extractor.stream.Description;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||
import org.schabi.newpipe.util.Localization;
|
||||
import org.schabi.newpipe.util.TextLinkifier;
|
||||
|
||||
import icepick.State;
|
||||
import io.reactivex.rxjava3.disposables.Disposable;
|
||||
|
||||
import static android.text.TextUtils.isEmpty;
|
||||
|
||||
public class DescriptionFragment extends BaseFragment {
|
||||
|
||||
@State
|
||||
StreamInfo streamInfo = null;
|
||||
@Nullable
|
||||
Disposable descriptionDisposable = null;
|
||||
|
||||
public DescriptionFragment() {
|
||||
}
|
||||
|
||||
public DescriptionFragment(final StreamInfo streamInfo) {
|
||||
this.streamInfo = streamInfo;
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(@NonNull final LayoutInflater inflater,
|
||||
@Nullable final ViewGroup container,
|
||||
@Nullable final Bundle savedInstanceState) {
|
||||
final FragmentDescriptionBinding binding =
|
||||
FragmentDescriptionBinding.inflate(inflater, container, false);
|
||||
if (streamInfo != null) {
|
||||
setupUploadDate(binding.detailUploadDateView);
|
||||
setupDescription(binding.detailDescriptionView);
|
||||
}
|
||||
return binding.getRoot();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
if (descriptionDisposable != null) {
|
||||
descriptionDisposable.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
private void setupUploadDate(final TextView uploadDateTextView) {
|
||||
if (streamInfo.getUploadDate() != null) {
|
||||
uploadDateTextView.setText(Localization
|
||||
.localizeUploadDate(activity, streamInfo.getUploadDate().offsetDateTime()));
|
||||
} else {
|
||||
uploadDateTextView.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
private void setupDescription(final TextView descriptionTextView) {
|
||||
final Description description = streamInfo.getDescription();
|
||||
if (description == null || isEmpty(description.getContent())
|
||||
|| description == Description.emptyDescription) {
|
||||
descriptionTextView.setText("");
|
||||
return;
|
||||
}
|
||||
|
||||
switch (description.getType()) {
|
||||
case Description.HTML:
|
||||
descriptionDisposable = TextLinkifier.createLinksFromHtmlBlock(requireContext(),
|
||||
description.getContent(), descriptionTextView,
|
||||
HtmlCompat.FROM_HTML_MODE_LEGACY);
|
||||
break;
|
||||
case Description.MARKDOWN:
|
||||
descriptionDisposable = TextLinkifier.createLinksFromMarkdownText(requireContext(),
|
||||
description.getContent(), descriptionTextView);
|
||||
break;
|
||||
case Description.PLAIN_TEXT: default:
|
||||
descriptionDisposable = TextLinkifier.createLinksFromPlainText(requireContext(),
|
||||
description.getContent(), descriptionTextView);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -12,13 +12,16 @@ import android.view.MenuInflater;
|
||||
import android.view.View;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.preference.PreferenceManager;
|
||||
import androidx.recyclerview.widget.GridLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import androidx.viewbinding.ViewBinding;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.databinding.PignateFooterBinding;
|
||||
import org.schabi.newpipe.error.ErrorActivity;
|
||||
import org.schabi.newpipe.extractor.InfoItem;
|
||||
import org.schabi.newpipe.extractor.channel.ChannelInfoItem;
|
||||
import org.schabi.newpipe.extractor.comments.CommentsInfoItem;
|
||||
@@ -30,7 +33,7 @@ import org.schabi.newpipe.fragments.OnScrollBelowItemsListener;
|
||||
import org.schabi.newpipe.info_list.InfoItemDialog;
|
||||
import org.schabi.newpipe.info_list.InfoListAdapter;
|
||||
import org.schabi.newpipe.player.helper.PlayerHolder;
|
||||
import org.schabi.newpipe.report.ErrorActivity;
|
||||
import org.schabi.newpipe.util.KoreUtil;
|
||||
import org.schabi.newpipe.util.NavigationHelper;
|
||||
import org.schabi.newpipe.util.OnClickGesture;
|
||||
import org.schabi.newpipe.util.StateSaver;
|
||||
@@ -42,7 +45,8 @@ import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Queue;
|
||||
|
||||
import static org.schabi.newpipe.util.AnimationUtils.animateView;
|
||||
import static org.schabi.newpipe.ktx.ViewUtils.animate;
|
||||
import static org.schabi.newpipe.ktx.ViewUtils.animateHideRecyclerViewAllowingScrolling;
|
||||
|
||||
public abstract class BaseListFragment<I, N> extends BaseStateFragment<I>
|
||||
implements ListViewContract<I, N>, StateSaver.WriteRead,
|
||||
@@ -66,7 +70,7 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I>
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public void onAttach(final Context context) {
|
||||
public void onAttach(@NonNull final Context context) {
|
||||
super.onAttach(context);
|
||||
|
||||
if (infoListAdapter == null) {
|
||||
@@ -182,7 +186,7 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I>
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSaveInstanceState(final Bundle bundle) {
|
||||
public void onSaveInstanceState(@NonNull final Bundle bundle) {
|
||||
super.onSaveInstanceState(bundle);
|
||||
if (useDefaultStateSaving) {
|
||||
savedState = StateSaver
|
||||
@@ -214,12 +218,13 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I>
|
||||
// Init
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
protected View getListHeader() {
|
||||
@Nullable
|
||||
protected ViewBinding getListHeader() {
|
||||
return null;
|
||||
}
|
||||
|
||||
protected View getListFooter() {
|
||||
return activity.getLayoutInflater().inflate(R.layout.pignate_footer, itemsList, false);
|
||||
protected ViewBinding getListFooter() {
|
||||
return PignateFooterBinding.inflate(activity.getLayoutInflater(), itemsList, false);
|
||||
}
|
||||
|
||||
protected RecyclerView.LayoutManager getListLayoutManager() {
|
||||
@@ -246,8 +251,12 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I>
|
||||
itemsList.setLayoutManager(useGrid ? getGridLayoutManager() : getListLayoutManager());
|
||||
|
||||
infoListAdapter.setUseGridVariant(useGrid);
|
||||
infoListAdapter.setFooter(getListFooter());
|
||||
infoListAdapter.setHeader(getListHeader());
|
||||
infoListAdapter.setFooter(getListFooter().getRoot());
|
||||
|
||||
final ViewBinding listHeader = getListHeader();
|
||||
if (listHeader != null) {
|
||||
infoListAdapter.setHeader(listHeader.getRoot());
|
||||
}
|
||||
|
||||
itemsList.setAdapter(infoListAdapter);
|
||||
}
|
||||
@@ -283,7 +292,8 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I>
|
||||
selectedItem.getUrl(),
|
||||
selectedItem.getName());
|
||||
} catch (final Exception e) {
|
||||
ErrorActivity.reportUiError((AppCompatActivity) getActivity(), e);
|
||||
ErrorActivity.reportUiErrorInSnackbar(
|
||||
BaseListFragment.this, "Opening channel fragment", e);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -298,7 +308,8 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I>
|
||||
selectedItem.getUrl(),
|
||||
selectedItem.getName());
|
||||
} catch (final Exception e) {
|
||||
ErrorActivity.reportUiError((AppCompatActivity) getActivity(), e);
|
||||
ErrorActivity.reportUiErrorInSnackbar(BaseListFragment.this,
|
||||
"Opening playlist fragment", e);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -332,7 +343,6 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I>
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
protected void showStreamDialog(final StreamInfoItem item) {
|
||||
final Context context = getContext();
|
||||
final Activity activity = getActivity();
|
||||
@@ -359,6 +369,9 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I>
|
||||
StreamDialogEntry.share
|
||||
));
|
||||
}
|
||||
if (KoreUtil.shouldShowPlayWithKodi(context, item.getServiceId())) {
|
||||
entries.add(StreamDialogEntry.play_with_kodi);
|
||||
}
|
||||
StreamDialogEntry.setEnabledEntries(entries);
|
||||
|
||||
new InfoItemDialog(activity, item, StreamDialogEntry.getCommands(context),
|
||||
@@ -398,26 +411,20 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I>
|
||||
@Override
|
||||
public void showLoading() {
|
||||
super.showLoading();
|
||||
// animateView(itemsList, false, 400);
|
||||
animateHideRecyclerViewAllowingScrolling(itemsList);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void hideLoading() {
|
||||
super.hideLoading();
|
||||
animateView(itemsList, true, 300);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void showError(final String message, final boolean showRetryButton) {
|
||||
super.showError(message, showRetryButton);
|
||||
showListFooter(false);
|
||||
animateView(itemsList, false, 200);
|
||||
animate(itemsList, true, 300);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void showEmptyState() {
|
||||
super.showEmptyState();
|
||||
showListFooter(false);
|
||||
animateHideRecyclerViewAllowingScrolling(itemsList);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -434,6 +441,13 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I>
|
||||
isLoading.set(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleError() {
|
||||
super.handleError();
|
||||
showListFooter(false);
|
||||
animateHideRecyclerViewAllowingScrolling(itemsList);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSharedPreferenceChanged(final SharedPreferences sharedPreferences,
|
||||
final String key) {
|
||||
|
||||
@@ -7,12 +7,17 @@ import android.view.View;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.schabi.newpipe.error.ErrorInfo;
|
||||
import org.schabi.newpipe.error.UserAction;
|
||||
import org.schabi.newpipe.extractor.ListExtractor;
|
||||
import org.schabi.newpipe.extractor.ListInfo;
|
||||
import org.schabi.newpipe.extractor.Page;
|
||||
import org.schabi.newpipe.extractor.exceptions.ContentNotSupportedException;
|
||||
import org.schabi.newpipe.util.Constants;
|
||||
import org.schabi.newpipe.views.NewPipeRecyclerView;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Queue;
|
||||
|
||||
import icepick.State;
|
||||
@@ -30,10 +35,15 @@ public abstract class BaseListInfoFragment<I extends ListInfo>
|
||||
@State
|
||||
protected String url;
|
||||
|
||||
private final UserAction errorUserAction;
|
||||
protected I currentInfo;
|
||||
protected Page currentNextPage;
|
||||
protected Disposable currentWorker;
|
||||
|
||||
protected BaseListInfoFragment(final UserAction errorUserAction) {
|
||||
this.errorUserAction = errorUserAction;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void initViews(final View rootView, final Bundle savedInstanceState) {
|
||||
super.initViews(rootView, savedInstanceState);
|
||||
@@ -133,7 +143,9 @@ public abstract class BaseListInfoFragment<I extends ListInfo>
|
||||
currentInfo = result;
|
||||
currentNextPage = result.getNextPage();
|
||||
handleResult(result);
|
||||
}, this::onError);
|
||||
}, throwable ->
|
||||
showError(new ErrorInfo(throwable, errorUserAction,
|
||||
"Start loading: " + url, serviceId)));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -161,10 +173,9 @@ public abstract class BaseListInfoFragment<I extends ListInfo>
|
||||
.subscribe((@NonNull ListExtractor.InfoItemsPage InfoItemsPage) -> {
|
||||
isLoading.set(false);
|
||||
handleNextItems(InfoItemsPage);
|
||||
}, (@NonNull Throwable throwable) -> {
|
||||
isLoading.set(false);
|
||||
onError(throwable);
|
||||
});
|
||||
}, (@NonNull Throwable throwable) ->
|
||||
dynamicallyShowErrorPanelOrSnackbar(new ErrorInfo(throwable,
|
||||
errorUserAction, "Loading more items: " + url, serviceId)));
|
||||
}
|
||||
|
||||
private void forbidDownwardFocusScroll() {
|
||||
@@ -182,10 +193,16 @@ public abstract class BaseListInfoFragment<I extends ListInfo>
|
||||
@Override
|
||||
public void handleNextItems(final ListExtractor.InfoItemsPage result) {
|
||||
super.handleNextItems(result);
|
||||
|
||||
currentNextPage = result.getNextPage();
|
||||
infoListAdapter.addInfoItemList(result.getItems());
|
||||
|
||||
showListFooter(hasMoreItems());
|
||||
|
||||
if (!result.getErrors().isEmpty()) {
|
||||
dynamicallyShowErrorPanelOrSnackbar(new ErrorInfo(result.getErrors(), errorUserAction,
|
||||
"Get next items of: " + url, serviceId));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -213,6 +230,18 @@ public abstract class BaseListInfoFragment<I extends ListInfo>
|
||||
showEmptyState();
|
||||
}
|
||||
}
|
||||
|
||||
if (!result.getErrors().isEmpty()) {
|
||||
final List<Throwable> errors = new ArrayList<>(result.getErrors());
|
||||
// handling ContentNotSupportedException not to show the error but an appropriate string
|
||||
// so that crashes won't be sent uselessly and the user will understand what happened
|
||||
errors.removeIf(throwable -> throwable instanceof ContentNotSupportedException);
|
||||
|
||||
if (!errors.isEmpty()) {
|
||||
dynamicallyShowErrorPanelOrSnackbar(new ErrorInfo(result.getErrors(),
|
||||
errorUserAction, "Start loading: " + url, serviceId));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
@@ -224,4 +253,14 @@ public abstract class BaseListInfoFragment<I extends ListInfo>
|
||||
this.url = u;
|
||||
this.name = !TextUtils.isEmpty(title) ? title : "";
|
||||
}
|
||||
|
||||
private void dynamicallyShowErrorPanelOrSnackbar(final ErrorInfo errorInfo) {
|
||||
if (infoListAdapter.getItemCount() == 0) {
|
||||
// show error panel only if no items already visible
|
||||
showError(errorInfo);
|
||||
} else {
|
||||
isLoading.set(false);
|
||||
showSnackBarError(errorInfo);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
package org.schabi.newpipe.fragments.list.channel;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
@@ -14,34 +12,33 @@ import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.Button;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.viewbinding.ViewBinding;
|
||||
|
||||
import com.jakewharton.rxbinding4.view.RxView;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.database.subscription.SubscriptionEntity;
|
||||
import org.schabi.newpipe.databinding.ChannelHeaderBinding;
|
||||
import org.schabi.newpipe.databinding.FragmentChannelBinding;
|
||||
import org.schabi.newpipe.databinding.PlaylistControlBinding;
|
||||
import org.schabi.newpipe.error.ErrorActivity;
|
||||
import org.schabi.newpipe.error.ErrorInfo;
|
||||
import org.schabi.newpipe.error.UserAction;
|
||||
import org.schabi.newpipe.extractor.InfoItem;
|
||||
import org.schabi.newpipe.extractor.ListExtractor;
|
||||
import org.schabi.newpipe.extractor.NewPipe;
|
||||
import org.schabi.newpipe.extractor.channel.ChannelInfo;
|
||||
import org.schabi.newpipe.extractor.exceptions.ContentNotSupportedException;
|
||||
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||
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.playqueue.ChannelPlayQueue;
|
||||
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
||||
import org.schabi.newpipe.report.ErrorActivity;
|
||||
import org.schabi.newpipe.report.UserAction;
|
||||
import org.schabi.newpipe.util.AnimationUtils;
|
||||
import org.schabi.newpipe.util.ExtractorHelper;
|
||||
import org.schabi.newpipe.util.ImageDisplayConstants;
|
||||
import org.schabi.newpipe.util.Localization;
|
||||
@@ -63,9 +60,9 @@ import io.reactivex.rxjava3.functions.Consumer;
|
||||
import io.reactivex.rxjava3.functions.Function;
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers;
|
||||
|
||||
import static org.schabi.newpipe.util.AnimationUtils.animateBackgroundColor;
|
||||
import static org.schabi.newpipe.util.AnimationUtils.animateTextColor;
|
||||
import static org.schabi.newpipe.util.AnimationUtils.animateView;
|
||||
import static org.schabi.newpipe.ktx.TextViewUtils.animateTextColor;
|
||||
import static org.schabi.newpipe.ktx.ViewUtils.animate;
|
||||
import static org.schabi.newpipe.ktx.ViewUtils.animateBackgroundColor;
|
||||
|
||||
public class ChannelFragment extends BaseListInfoFragment<ChannelInfo>
|
||||
implements View.OnClickListener {
|
||||
@@ -78,22 +75,12 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo>
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
private SubscriptionManager subscriptionManager;
|
||||
private View headerRootLayout;
|
||||
private ImageView headerChannelBanner;
|
||||
private ImageView headerAvatarView;
|
||||
private TextView headerTitleView;
|
||||
private ImageView headerSubChannelAvatarView;
|
||||
private TextView headerSubChannelTitleView;
|
||||
private TextView headerSubscribersTextView;
|
||||
private Button headerSubscribeButton;
|
||||
private View playlistCtrl;
|
||||
private LinearLayout headerPlayAllButton;
|
||||
private LinearLayout headerPopupButton;
|
||||
private LinearLayout headerBackgroundButton;
|
||||
|
||||
private FragmentChannelBinding channelBinding;
|
||||
private ChannelHeaderBinding headerBinding;
|
||||
private PlaylistControlBinding playlistControlBinding;
|
||||
|
||||
private MenuItem menuRssButton;
|
||||
private TextView contentNotSupportedTextView;
|
||||
private TextView kaomojiTextView;
|
||||
private TextView noVideosTextView;
|
||||
|
||||
public static ChannelFragment getInstance(final int serviceId, final String url,
|
||||
final String name) {
|
||||
@@ -102,6 +89,10 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo>
|
||||
return instance;
|
||||
}
|
||||
|
||||
public ChannelFragment() {
|
||||
super(UserAction.REQUESTED_CHANNEL);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setUserVisibleHint(final boolean isVisibleToUser) {
|
||||
super.setUserVisibleHint(isVisibleToUser);
|
||||
@@ -117,7 +108,7 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo>
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public void onAttach(final Context context) {
|
||||
public void onAttach(@NonNull final Context context) {
|
||||
super.onAttach(context);
|
||||
subscriptionManager = new SubscriptionManager(activity);
|
||||
}
|
||||
@@ -130,55 +121,42 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo>
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(final View rootView, final Bundle savedInstanceState) {
|
||||
public void onViewCreated(@NonNull final View rootView, final Bundle savedInstanceState) {
|
||||
super.onViewCreated(rootView, savedInstanceState);
|
||||
contentNotSupportedTextView = rootView.findViewById(R.id.error_content_not_supported);
|
||||
kaomojiTextView = rootView.findViewById(R.id.channel_kaomoji);
|
||||
noVideosTextView = rootView.findViewById(R.id.channel_no_videos);
|
||||
channelBinding = FragmentChannelBinding.bind(rootView);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
if (disposables != null) {
|
||||
disposables.clear();
|
||||
}
|
||||
disposables.clear();
|
||||
if (subscribeButtonMonitor != null) {
|
||||
subscribeButtonMonitor.dispose();
|
||||
}
|
||||
channelBinding = null;
|
||||
headerBinding = null;
|
||||
playlistControlBinding = null;
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Init
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
protected View getListHeader() {
|
||||
headerRootLayout = activity.getLayoutInflater()
|
||||
.inflate(R.layout.channel_header, itemsList, false);
|
||||
headerChannelBanner = headerRootLayout.findViewById(R.id.channel_banner_image);
|
||||
headerAvatarView = headerRootLayout.findViewById(R.id.channel_avatar_view);
|
||||
headerTitleView = headerRootLayout.findViewById(R.id.channel_title_view);
|
||||
headerSubscribersTextView = headerRootLayout.findViewById(R.id.channel_subscriber_view);
|
||||
headerSubscribeButton = headerRootLayout.findViewById(R.id.channel_subscribe_button);
|
||||
playlistCtrl = headerRootLayout.findViewById(R.id.playlist_control);
|
||||
headerSubChannelAvatarView =
|
||||
headerRootLayout.findViewById(R.id.sub_channel_avatar_view);
|
||||
headerSubChannelTitleView =
|
||||
headerRootLayout.findViewById(R.id.sub_channel_title_view);
|
||||
@Override
|
||||
protected ViewBinding getListHeader() {
|
||||
headerBinding = ChannelHeaderBinding
|
||||
.inflate(activity.getLayoutInflater(), itemsList, false);
|
||||
playlistControlBinding = headerBinding.playlistControl;
|
||||
|
||||
headerPlayAllButton = headerRootLayout.findViewById(R.id.playlist_ctrl_play_all_button);
|
||||
headerPopupButton = headerRootLayout.findViewById(R.id.playlist_ctrl_play_popup_button);
|
||||
headerBackgroundButton = headerRootLayout.findViewById(R.id.playlist_ctrl_play_bg_button);
|
||||
|
||||
return headerRootLayout;
|
||||
return headerBinding;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void initListeners() {
|
||||
super.initListeners();
|
||||
|
||||
headerSubChannelTitleView.setOnClickListener(this);
|
||||
headerSubChannelAvatarView.setOnClickListener(this);
|
||||
headerBinding.subChannelTitleView.setOnClickListener(this);
|
||||
headerBinding.subChannelAvatarView.setOnClickListener(this);
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
@@ -205,8 +183,7 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo>
|
||||
private void openRssFeed() {
|
||||
final ChannelInfo info = currentInfo;
|
||||
if (info != null) {
|
||||
final Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(info.getFeedUrl()));
|
||||
startActivity(intent);
|
||||
ShareUtils.openUrlInBrowser(requireContext(), info.getFeedUrl(), false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -226,7 +203,7 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo>
|
||||
break;
|
||||
case R.id.menu_item_share:
|
||||
if (currentInfo != null) {
|
||||
ShareUtils.shareUrl(requireContext(), name, currentInfo.getOriginalUrl());
|
||||
ShareUtils.shareText(requireContext(), name, currentInfo.getOriginalUrl());
|
||||
}
|
||||
break;
|
||||
default:
|
||||
@@ -241,10 +218,9 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo>
|
||||
|
||||
private void monitorSubscription(final ChannelInfo info) {
|
||||
final Consumer<Throwable> onError = (Throwable throwable) -> {
|
||||
animateView(headerSubscribeButton, false, 100);
|
||||
showSnackBarError(throwable, UserAction.SUBSCRIPTION,
|
||||
NewPipe.getNameOfService(currentInfo.getServiceId()),
|
||||
"Get subscription status", 0);
|
||||
animate(headerBinding.channelSubscribeButton, false, 100);
|
||||
showSnackBarError(new ErrorInfo(throwable, UserAction.SUBSCRIPTION_GET,
|
||||
"Get subscription status", currentInfo));
|
||||
};
|
||||
|
||||
final Observable<List<SubscriptionEntity>> observable = subscriptionManager
|
||||
@@ -294,11 +270,8 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo>
|
||||
};
|
||||
|
||||
final Consumer<Throwable> onError = (@NonNull Throwable throwable) ->
|
||||
onUnrecoverableError(throwable,
|
||||
UserAction.SUBSCRIPTION,
|
||||
NewPipe.getNameOfService(info.getServiceId()),
|
||||
"Updating Subscription for " + info.getUrl(),
|
||||
R.string.subscription_update_failed);
|
||||
showSnackBarError(new ErrorInfo(throwable, UserAction.SUBSCRIPTION_UPDATE,
|
||||
"Updating subscription for " + info.getUrl(), info));
|
||||
|
||||
disposables.add(subscriptionManager.updateChannelInfo(info)
|
||||
.subscribeOn(Schedulers.io())
|
||||
@@ -315,11 +288,8 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo>
|
||||
};
|
||||
|
||||
final Consumer<Throwable> onError = (@NonNull Throwable throwable) ->
|
||||
onUnrecoverableError(throwable,
|
||||
UserAction.SUBSCRIPTION,
|
||||
NewPipe.getNameOfService(currentInfo.getServiceId()),
|
||||
"Subscription Change",
|
||||
R.string.subscription_change_failed);
|
||||
showSnackBarError(new ErrorInfo(throwable, UserAction.SUBSCRIPTION_CHANGE,
|
||||
"Changing subscription for " + currentInfo.getUrl(), currentInfo));
|
||||
|
||||
/* Emit clicks from main thread unto io thread */
|
||||
return RxView.clicks(subscribeButton)
|
||||
@@ -351,15 +321,15 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo>
|
||||
info.getAvatarUrl(),
|
||||
info.getDescription(),
|
||||
info.getSubscriberCount());
|
||||
subscribeButtonMonitor = monitorSubscribeButton(headerSubscribeButton,
|
||||
mapOnSubscribe(channel, info));
|
||||
subscribeButtonMonitor = monitorSubscribeButton(
|
||||
headerBinding.channelSubscribeButton, mapOnSubscribe(channel, info));
|
||||
} else {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "Found subscription to this channel!");
|
||||
}
|
||||
final SubscriptionEntity subscription = subscriptionEntities.get(0);
|
||||
subscribeButtonMonitor = monitorSubscribeButton(headerSubscribeButton,
|
||||
mapOnUnsubscribe(subscription));
|
||||
subscribeButtonMonitor = monitorSubscribeButton(
|
||||
headerBinding.channelSubscribeButton, mapOnUnsubscribe(subscription));
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -370,7 +340,8 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo>
|
||||
+ "isSubscribed = [" + isSubscribed + "]");
|
||||
}
|
||||
|
||||
final boolean isButtonVisible = headerSubscribeButton.getVisibility() == View.VISIBLE;
|
||||
final boolean isButtonVisible = headerBinding.channelSubscribeButton.getVisibility()
|
||||
== View.VISIBLE;
|
||||
final int backgroundDuration = isButtonVisible ? 300 : 0;
|
||||
final int textDuration = isButtonVisible ? 200 : 0;
|
||||
|
||||
@@ -382,18 +353,21 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo>
|
||||
final int subscribedText = ContextCompat.getColor(activity, R.color.subscribed_text_color);
|
||||
|
||||
if (!isSubscribed) {
|
||||
headerSubscribeButton.setText(R.string.subscribe_button_title);
|
||||
animateBackgroundColor(headerSubscribeButton, backgroundDuration, subscribedBackground,
|
||||
subscribeBackground);
|
||||
animateTextColor(headerSubscribeButton, textDuration, subscribedText, subscribeText);
|
||||
headerBinding.channelSubscribeButton.setText(R.string.subscribe_button_title);
|
||||
animateBackgroundColor(headerBinding.channelSubscribeButton, backgroundDuration,
|
||||
subscribedBackground, subscribeBackground);
|
||||
animateTextColor(headerBinding.channelSubscribeButton, textDuration, subscribedText,
|
||||
subscribeText);
|
||||
} else {
|
||||
headerSubscribeButton.setText(R.string.subscribed_button_title);
|
||||
animateBackgroundColor(headerSubscribeButton, backgroundDuration, subscribeBackground,
|
||||
subscribedBackground);
|
||||
animateTextColor(headerSubscribeButton, textDuration, subscribeText, subscribedText);
|
||||
headerBinding.channelSubscribeButton.setText(R.string.subscribed_button_title);
|
||||
animateBackgroundColor(headerBinding.channelSubscribeButton, backgroundDuration,
|
||||
subscribeBackground, subscribedBackground);
|
||||
animateTextColor(headerBinding.channelSubscribeButton, textDuration, subscribeText,
|
||||
subscribedText);
|
||||
}
|
||||
|
||||
animateView(headerSubscribeButton, AnimationUtils.Type.LIGHT_SCALE_AND_ALPHA, true, 100);
|
||||
animate(headerBinding.channelSubscribeButton, true, 100,
|
||||
AnimationType.LIGHT_SCALE_AND_ALPHA);
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
@@ -429,7 +403,7 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo>
|
||||
currentInfo.getParentChannelUrl(),
|
||||
currentInfo.getParentChannelName());
|
||||
} catch (final Exception e) {
|
||||
ErrorActivity.reportUiError((AppCompatActivity) getActivity(), e);
|
||||
ErrorActivity.reportUiErrorInSnackbar(this, "Opening channel fragment", e);
|
||||
}
|
||||
} else if (DEBUG) {
|
||||
Log.i(TAG, "Can't open parent channel because we got no channel URL");
|
||||
@@ -446,99 +420,89 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo>
|
||||
public void showLoading() {
|
||||
super.showLoading();
|
||||
|
||||
IMAGE_LOADER.cancelDisplayTask(headerChannelBanner);
|
||||
IMAGE_LOADER.cancelDisplayTask(headerAvatarView);
|
||||
IMAGE_LOADER.cancelDisplayTask(headerSubChannelAvatarView);
|
||||
animateView(headerSubscribeButton, false, 100);
|
||||
IMAGE_LOADER.cancelDisplayTask(headerBinding.channelBannerImage);
|
||||
IMAGE_LOADER.cancelDisplayTask(headerBinding.channelAvatarView);
|
||||
IMAGE_LOADER.cancelDisplayTask(headerBinding.subChannelAvatarView);
|
||||
animate(headerBinding.channelSubscribeButton, false, 100);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleResult(@NonNull final ChannelInfo result) {
|
||||
super.handleResult(result);
|
||||
|
||||
headerRootLayout.setVisibility(View.VISIBLE);
|
||||
IMAGE_LOADER.displayImage(result.getBannerUrl(), headerChannelBanner,
|
||||
headerBinding.getRoot().setVisibility(View.VISIBLE);
|
||||
IMAGE_LOADER.displayImage(result.getBannerUrl(), headerBinding.channelBannerImage,
|
||||
ImageDisplayConstants.DISPLAY_BANNER_OPTIONS);
|
||||
IMAGE_LOADER.displayImage(result.getAvatarUrl(), headerAvatarView,
|
||||
IMAGE_LOADER.displayImage(result.getAvatarUrl(), headerBinding.channelAvatarView,
|
||||
ImageDisplayConstants.DISPLAY_AVATAR_OPTIONS);
|
||||
IMAGE_LOADER.displayImage(result.getParentChannelAvatarUrl(), headerSubChannelAvatarView,
|
||||
IMAGE_LOADER.displayImage(result.getParentChannelAvatarUrl(),
|
||||
headerBinding.subChannelAvatarView,
|
||||
ImageDisplayConstants.DISPLAY_AVATAR_OPTIONS);
|
||||
|
||||
headerSubscribersTextView.setVisibility(View.VISIBLE);
|
||||
headerBinding.channelSubscriberView.setVisibility(View.VISIBLE);
|
||||
if (result.getSubscriberCount() >= 0) {
|
||||
headerSubscribersTextView.setText(Localization
|
||||
headerBinding.channelSubscriberView.setText(Localization
|
||||
.shortSubscriberCount(activity, result.getSubscriberCount()));
|
||||
} else {
|
||||
headerSubscribersTextView.setText(R.string.subscribers_count_not_available);
|
||||
headerBinding.channelSubscriberView.setText(R.string.subscribers_count_not_available);
|
||||
}
|
||||
|
||||
if (!TextUtils.isEmpty(currentInfo.getParentChannelName())) {
|
||||
headerSubChannelTitleView.setText(String.format(
|
||||
headerBinding.subChannelTitleView.setText(String.format(
|
||||
getString(R.string.channel_created_by),
|
||||
currentInfo.getParentChannelName())
|
||||
);
|
||||
headerSubChannelTitleView.setVisibility(View.VISIBLE);
|
||||
headerSubChannelAvatarView.setVisibility(View.VISIBLE);
|
||||
headerBinding.subChannelTitleView.setVisibility(View.VISIBLE);
|
||||
headerBinding.subChannelAvatarView.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
headerSubChannelTitleView.setVisibility(View.GONE);
|
||||
headerBinding.subChannelTitleView.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
if (menuRssButton != null) {
|
||||
menuRssButton.setVisible(!TextUtils.isEmpty(result.getFeedUrl()));
|
||||
}
|
||||
|
||||
playlistCtrl.setVisibility(View.VISIBLE);
|
||||
playlistControlBinding.getRoot().setVisibility(View.VISIBLE);
|
||||
|
||||
final List<Throwable> errors = new ArrayList<>(result.getErrors());
|
||||
if (!errors.isEmpty()) {
|
||||
|
||||
// handling ContentNotSupportedException not to show the error but an appropriate string
|
||||
// so that crashes won't be sent uselessly and the user will understand what happened
|
||||
errors.removeIf(throwable -> {
|
||||
if (throwable instanceof ContentNotSupportedException) {
|
||||
showContentNotSupported();
|
||||
}
|
||||
return throwable instanceof ContentNotSupportedException;
|
||||
});
|
||||
|
||||
if (!errors.isEmpty()) {
|
||||
showSnackBarError(errors, UserAction.REQUESTED_CHANNEL,
|
||||
NewPipe.getNameOfService(result.getServiceId()), result.getUrl(), 0);
|
||||
for (final Throwable throwable : result.getErrors()) {
|
||||
if (throwable instanceof ContentNotSupportedException) {
|
||||
showContentNotSupported();
|
||||
}
|
||||
}
|
||||
|
||||
if (disposables != null) {
|
||||
disposables.clear();
|
||||
}
|
||||
disposables.clear();
|
||||
if (subscribeButtonMonitor != null) {
|
||||
subscribeButtonMonitor.dispose();
|
||||
}
|
||||
updateSubscription(result);
|
||||
monitorSubscription(result);
|
||||
|
||||
headerPlayAllButton.setOnClickListener(view -> NavigationHelper
|
||||
.playOnMainPlayer(activity, getPlayQueue()));
|
||||
headerPopupButton.setOnClickListener(view -> NavigationHelper
|
||||
.playOnPopupPlayer(activity, getPlayQueue(), false));
|
||||
headerBackgroundButton.setOnClickListener(view -> NavigationHelper
|
||||
.playOnBackgroundPlayer(activity, getPlayQueue(), false));
|
||||
playlistControlBinding.playlistCtrlPlayAllButton
|
||||
.setOnClickListener(view -> NavigationHelper
|
||||
.playOnMainPlayer(activity, getPlayQueue()));
|
||||
playlistControlBinding.playlistCtrlPlayPopupButton
|
||||
.setOnClickListener(view -> NavigationHelper
|
||||
.playOnPopupPlayer(activity, getPlayQueue(), false));
|
||||
playlistControlBinding.playlistCtrlPlayBgButton
|
||||
.setOnClickListener(view -> NavigationHelper
|
||||
.playOnBackgroundPlayer(activity, getPlayQueue(), false));
|
||||
|
||||
headerPopupButton.setOnLongClickListener(view -> {
|
||||
playlistControlBinding.playlistCtrlPlayPopupButton.setOnLongClickListener(view -> {
|
||||
NavigationHelper.enqueueOnPopupPlayer(activity, getPlayQueue(), true);
|
||||
return true;
|
||||
});
|
||||
|
||||
headerBackgroundButton.setOnLongClickListener(view -> {
|
||||
playlistControlBinding.playlistCtrlPlayBgButton.setOnLongClickListener(view -> {
|
||||
NavigationHelper.enqueueOnBackgroundPlayer(activity, getPlayQueue(), true);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
private void showContentNotSupported() {
|
||||
contentNotSupportedTextView.setVisibility(View.VISIBLE);
|
||||
kaomojiTextView.setText("(︶︹︺)");
|
||||
kaomojiTextView.setTextSize(TypedValue.COMPLEX_UNIT_SP, 45f);
|
||||
noVideosTextView.setVisibility(View.GONE);
|
||||
channelBinding.errorContentNotSupported.setVisibility(View.VISIBLE);
|
||||
channelBinding.channelKaomoji.setText("(︶︹︺)");
|
||||
channelBinding.channelKaomoji.setTextSize(TypedValue.COMPLEX_UNIT_SP, 45f);
|
||||
channelBinding.channelNoVideos.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
private PlayQueue getPlayQueue() {
|
||||
@@ -556,38 +520,6 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo>
|
||||
currentInfo.getNextPage(), streamItems, index);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleNextItems(final ListExtractor.InfoItemsPage result) {
|
||||
super.handleNextItems(result);
|
||||
|
||||
if (!result.getErrors().isEmpty()) {
|
||||
showSnackBarError(result.getErrors(),
|
||||
UserAction.REQUESTED_CHANNEL,
|
||||
NewPipe.getNameOfService(serviceId),
|
||||
"Get next page of: " + url,
|
||||
R.string.general_error);
|
||||
}
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// OnError
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
protected boolean onError(final Throwable exception) {
|
||||
if (super.onError(exception)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
final int errorId = exception instanceof ExtractionException
|
||||
? R.string.parsing_error : R.string.general_error;
|
||||
|
||||
onUnrecoverableError(exception, UserAction.REQUESTED_CHANNEL,
|
||||
NewPipe.getNameOfService(serviceId), url, errorId);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Utils
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
@@ -596,7 +528,7 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo>
|
||||
public void setTitle(final String title) {
|
||||
super.setTitle(title);
|
||||
if (!useAsFrontPage) {
|
||||
headerTitleView.setText(title);
|
||||
headerBinding.channelTitleView.setText(title);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package org.schabi.newpipe.fragments.list.comments;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
@@ -12,12 +11,11 @@ import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.error.UserAction;
|
||||
import org.schabi.newpipe.extractor.ListExtractor;
|
||||
import org.schabi.newpipe.extractor.NewPipe;
|
||||
import org.schabi.newpipe.extractor.comments.CommentsInfo;
|
||||
import org.schabi.newpipe.fragments.list.BaseListInfoFragment;
|
||||
import org.schabi.newpipe.report.UserAction;
|
||||
import org.schabi.newpipe.util.AnimationUtils;
|
||||
import org.schabi.newpipe.ktx.ViewUtils;
|
||||
import org.schabi.newpipe.util.ExtractorHelper;
|
||||
|
||||
import io.reactivex.rxjava3.core.Single;
|
||||
@@ -26,22 +24,21 @@ import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
||||
public class CommentsFragment extends BaseListInfoFragment<CommentsInfo> {
|
||||
private final CompositeDisposable disposables = new CompositeDisposable();
|
||||
|
||||
public static CommentsFragment getInstance(final int serviceId, final String url,
|
||||
public static CommentsFragment getInstance(final int serviceId, final String url,
|
||||
final String name) {
|
||||
final CommentsFragment instance = new CommentsFragment();
|
||||
instance.setInitialData(serviceId, url, name);
|
||||
return instance;
|
||||
}
|
||||
|
||||
public CommentsFragment() {
|
||||
super(UserAction.REQUESTED_COMMENTS);
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// LifeCycle
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public void onAttach(final Context context) {
|
||||
super.onAttach(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(@NonNull final LayoutInflater inflater,
|
||||
@Nullable final ViewGroup container,
|
||||
@@ -52,9 +49,7 @@ public class CommentsFragment extends BaseListInfoFragment<CommentsInfo> {
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
if (disposables != null) {
|
||||
disposables.clear();
|
||||
}
|
||||
disposables.clear();
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
@@ -75,52 +70,11 @@ public class CommentsFragment extends BaseListInfoFragment<CommentsInfo> {
|
||||
// Contract
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public void showLoading() {
|
||||
super.showLoading();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleResult(@NonNull final CommentsInfo result) {
|
||||
super.handleResult(result);
|
||||
|
||||
AnimationUtils.slideUp(requireView(), 120, 150, 0.06f);
|
||||
|
||||
if (!result.getErrors().isEmpty()) {
|
||||
showSnackBarError(result.getErrors(), UserAction.REQUESTED_COMMENTS,
|
||||
NewPipe.getNameOfService(result.getServiceId()), result.getUrl(), 0);
|
||||
}
|
||||
|
||||
if (disposables != null) {
|
||||
disposables.clear();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleNextItems(final ListExtractor.InfoItemsPage result) {
|
||||
super.handleNextItems(result);
|
||||
|
||||
if (!result.getErrors().isEmpty()) {
|
||||
showSnackBarError(result.getErrors(), UserAction.REQUESTED_COMMENTS,
|
||||
NewPipe.getNameOfService(serviceId), "Get next page of: " + url,
|
||||
R.string.general_error);
|
||||
}
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// OnError
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
protected boolean onError(final Throwable exception) {
|
||||
if (super.onError(exception)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
hideLoading();
|
||||
showSnackBarError(exception, UserAction.REQUESTED_COMMENTS,
|
||||
NewPipe.getNameOfService(serviceId), url, R.string.error_unable_to_load_comments);
|
||||
return true;
|
||||
ViewUtils.slideUp(requireView(), 120, 150, 0.06f);
|
||||
disposables.clear();
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@@ -2,14 +2,16 @@ package org.schabi.newpipe.fragments.list.kiosk;
|
||||
|
||||
import android.os.Bundle;
|
||||
|
||||
import org.schabi.newpipe.error.ErrorInfo;
|
||||
import org.schabi.newpipe.error.UserAction;
|
||||
import org.schabi.newpipe.extractor.NewPipe;
|
||||
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||
import org.schabi.newpipe.extractor.kiosk.KioskList;
|
||||
import org.schabi.newpipe.report.UserAction;
|
||||
import org.schabi.newpipe.util.KioskTranslator;
|
||||
import org.schabi.newpipe.util.ServiceHelper;
|
||||
|
||||
public class DefaultKioskFragment extends KioskFragment {
|
||||
|
||||
@Override
|
||||
public void onCreate(final Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
@@ -46,8 +48,8 @@ public class DefaultKioskFragment extends KioskFragment {
|
||||
currentInfo = null;
|
||||
currentNextPage = null;
|
||||
} catch (final ExtractionException e) {
|
||||
onUnrecoverableError(e, UserAction.REQUESTED_KIOSK, "none",
|
||||
"Loading default kiosk from selected service", 0);
|
||||
showError(new ErrorInfo(e, UserAction.REQUESTED_KIOSK,
|
||||
"Loading default kiosk for selected service"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,8 @@ import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.error.ErrorInfo;
|
||||
import org.schabi.newpipe.error.UserAction;
|
||||
import org.schabi.newpipe.extractor.ListExtractor;
|
||||
import org.schabi.newpipe.extractor.NewPipe;
|
||||
import org.schabi.newpipe.extractor.StreamingService;
|
||||
@@ -20,7 +22,6 @@ import org.schabi.newpipe.extractor.kiosk.KioskInfo;
|
||||
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandlerFactory;
|
||||
import org.schabi.newpipe.extractor.localization.ContentCountry;
|
||||
import org.schabi.newpipe.fragments.list.BaseListInfoFragment;
|
||||
import org.schabi.newpipe.report.UserAction;
|
||||
import org.schabi.newpipe.util.ExtractorHelper;
|
||||
import org.schabi.newpipe.util.KioskTranslator;
|
||||
import org.schabi.newpipe.util.Localization;
|
||||
@@ -28,8 +29,6 @@ import org.schabi.newpipe.util.Localization;
|
||||
import icepick.State;
|
||||
import io.reactivex.rxjava3.core.Single;
|
||||
|
||||
import static org.schabi.newpipe.util.AnimationUtils.animateView;
|
||||
|
||||
/**
|
||||
* Created by Christian Schabesberger on 23.09.17.
|
||||
* <p>
|
||||
@@ -82,6 +81,10 @@ public class KioskFragment extends BaseListInfoFragment<KioskInfo> {
|
||||
return instance;
|
||||
}
|
||||
|
||||
public KioskFragment() {
|
||||
super(UserAction.REQUESTED_KIOSK);
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// LifeCycle
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
@@ -102,9 +105,7 @@ public class KioskFragment extends BaseListInfoFragment<KioskInfo> {
|
||||
try {
|
||||
setTitle(kioskTranslatedName);
|
||||
} catch (final Exception e) {
|
||||
onUnrecoverableError(e, UserAction.UI_ERROR,
|
||||
"none",
|
||||
"none", R.string.app_ui_crash);
|
||||
showSnackBarError(new ErrorInfo(e, UserAction.UI_ERROR, "Setting kiosk title"));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -157,34 +158,11 @@ public class KioskFragment extends BaseListInfoFragment<KioskInfo> {
|
||||
// Contract
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public void showLoading() {
|
||||
super.showLoading();
|
||||
animateView(itemsList, false, 100);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleResult(@NonNull final KioskInfo result) {
|
||||
super.handleResult(result);
|
||||
|
||||
name = kioskTranslatedName;
|
||||
setTitle(kioskTranslatedName);
|
||||
|
||||
if (!result.getErrors().isEmpty()) {
|
||||
showSnackBarError(result.getErrors(),
|
||||
UserAction.REQUESTED_KIOSK,
|
||||
NewPipe.getNameOfService(result.getServiceId()), result.getUrl(), 0);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleNextItems(final ListExtractor.InfoItemsPage result) {
|
||||
super.handleNextItems(result);
|
||||
|
||||
if (!result.getErrors().isEmpty()) {
|
||||
showSnackBarError(result.getErrors(),
|
||||
UserAction.REQUESTED_PLAYLIST, NewPipe.getNameOfService(serviceId),
|
||||
"Get next page of: " + url, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,23 +11,27 @@ import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.appcompat.content.res.AppCompatResources;
|
||||
import androidx.viewbinding.ViewBinding;
|
||||
|
||||
import org.reactivestreams.Subscriber;
|
||||
import org.reactivestreams.Subscription;
|
||||
import org.schabi.newpipe.NewPipeDatabase;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity;
|
||||
import org.schabi.newpipe.databinding.PlaylistControlBinding;
|
||||
import org.schabi.newpipe.databinding.PlaylistHeaderBinding;
|
||||
import org.schabi.newpipe.error.ErrorActivity;
|
||||
import org.schabi.newpipe.error.ErrorInfo;
|
||||
import org.schabi.newpipe.error.UserAction;
|
||||
import org.schabi.newpipe.extractor.InfoItem;
|
||||
import org.schabi.newpipe.extractor.ListExtractor;
|
||||
import org.schabi.newpipe.extractor.NewPipe;
|
||||
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||
import org.schabi.newpipe.extractor.ServiceList;
|
||||
import org.schabi.newpipe.extractor.playlist.PlaylistInfo;
|
||||
import org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||
import org.schabi.newpipe.extractor.stream.StreamType;
|
||||
import org.schabi.newpipe.fragments.list.BaseListInfoFragment;
|
||||
@@ -36,15 +40,13 @@ import org.schabi.newpipe.local.playlist.RemotePlaylistManager;
|
||||
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.report.ErrorActivity;
|
||||
import org.schabi.newpipe.report.UserAction;
|
||||
import org.schabi.newpipe.util.ExtractorHelper;
|
||||
import org.schabi.newpipe.util.ImageDisplayConstants;
|
||||
import org.schabi.newpipe.util.KoreUtil;
|
||||
import org.schabi.newpipe.util.Localization;
|
||||
import org.schabi.newpipe.util.NavigationHelper;
|
||||
import org.schabi.newpipe.util.ShareUtils;
|
||||
import org.schabi.newpipe.util.StreamDialogEntry;
|
||||
import org.schabi.newpipe.util.ThemeHelper;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
@@ -57,7 +59,9 @@ import io.reactivex.rxjava3.core.Single;
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
||||
import io.reactivex.rxjava3.disposables.Disposable;
|
||||
|
||||
import static org.schabi.newpipe.util.AnimationUtils.animateView;
|
||||
import static org.schabi.newpipe.ktx.ViewUtils.animate;
|
||||
import static org.schabi.newpipe.ktx.ViewUtils.animateHideRecyclerViewAllowingScrolling;
|
||||
import static org.schabi.newpipe.util.ThemeHelper.resolveResourceIdFromAttr;
|
||||
|
||||
public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
|
||||
private CompositeDisposable disposables;
|
||||
@@ -70,17 +74,8 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
|
||||
// Views
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
private View headerRootLayout;
|
||||
private TextView headerTitleView;
|
||||
private View headerUploaderLayout;
|
||||
private TextView headerUploaderName;
|
||||
private ImageView headerUploaderAvatar;
|
||||
private TextView headerStreamCount;
|
||||
private View playlistCtrl;
|
||||
|
||||
private View headerPlayAllButton;
|
||||
private View headerPopupButton;
|
||||
private View headerBackgroundButton;
|
||||
private PlaylistHeaderBinding headerBinding;
|
||||
private PlaylistControlBinding playlistControlBinding;
|
||||
|
||||
private MenuItem playlistBookmarkButton;
|
||||
|
||||
@@ -91,6 +86,10 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
|
||||
return instance;
|
||||
}
|
||||
|
||||
public PlaylistFragment() {
|
||||
super(UserAction.REQUESTED_PLAYLIST);
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// LifeCycle
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
@@ -115,22 +114,13 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
|
||||
// Init
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
protected View getListHeader() {
|
||||
headerRootLayout = activity.getLayoutInflater()
|
||||
.inflate(R.layout.playlist_header, itemsList, false);
|
||||
headerTitleView = headerRootLayout.findViewById(R.id.playlist_title_view);
|
||||
headerUploaderLayout = headerRootLayout.findViewById(R.id.uploader_layout);
|
||||
headerUploaderName = headerRootLayout.findViewById(R.id.uploader_name);
|
||||
headerUploaderAvatar = headerRootLayout.findViewById(R.id.uploader_avatar_view);
|
||||
headerStreamCount = headerRootLayout.findViewById(R.id.playlist_stream_count);
|
||||
playlistCtrl = headerRootLayout.findViewById(R.id.playlist_control);
|
||||
@Override
|
||||
protected ViewBinding getListHeader() {
|
||||
headerBinding = PlaylistHeaderBinding
|
||||
.inflate(activity.getLayoutInflater(), itemsList, false);
|
||||
playlistControlBinding = headerBinding.playlistControl;
|
||||
|
||||
headerPlayAllButton = headerRootLayout.findViewById(R.id.playlist_ctrl_play_all_button);
|
||||
headerPopupButton = headerRootLayout.findViewById(R.id.playlist_ctrl_play_popup_button);
|
||||
headerBackgroundButton = headerRootLayout.findViewById(R.id.playlist_ctrl_play_bg_button);
|
||||
|
||||
|
||||
return headerRootLayout;
|
||||
return headerBinding;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -171,6 +161,9 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
|
||||
StreamDialogEntry.share
|
||||
));
|
||||
}
|
||||
if (KoreUtil.shouldShowPlayWithKodi(context, item.getServiceId())) {
|
||||
entries.add(StreamDialogEntry.play_with_kodi);
|
||||
}
|
||||
StreamDialogEntry.setEnabledEntries(entries);
|
||||
|
||||
StreamDialogEntry.start_here_on_background.setCustomAction((fragment, infoItem) ->
|
||||
@@ -196,6 +189,9 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
|
||||
|
||||
@Override
|
||||
public void onDestroyView() {
|
||||
headerBinding = null;
|
||||
playlistControlBinding = null;
|
||||
|
||||
super.onDestroyView();
|
||||
if (isBookmarkButtonReady != null) {
|
||||
isBookmarkButtonReady.set(false);
|
||||
@@ -249,7 +245,7 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
|
||||
ShareUtils.openUrlInBrowser(requireContext(), url);
|
||||
break;
|
||||
case R.id.menu_item_share:
|
||||
ShareUtils.shareUrl(requireContext(), name, url);
|
||||
ShareUtils.shareText(requireContext(), name, url);
|
||||
break;
|
||||
case R.id.menu_item_bookmark:
|
||||
onBookmarkClicked();
|
||||
@@ -268,47 +264,62 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
|
||||
@Override
|
||||
public void showLoading() {
|
||||
super.showLoading();
|
||||
animateView(headerRootLayout, false, 200);
|
||||
animateView(itemsList, false, 100);
|
||||
animate(headerBinding.getRoot(), false, 200);
|
||||
animateHideRecyclerViewAllowingScrolling(itemsList);
|
||||
|
||||
IMAGE_LOADER.cancelDisplayTask(headerUploaderAvatar);
|
||||
animateView(headerUploaderLayout, false, 200);
|
||||
IMAGE_LOADER.cancelDisplayTask(headerBinding.uploaderAvatarView);
|
||||
animate(headerBinding.uploaderLayout, false, 200);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleResult(@NonNull final PlaylistInfo result) {
|
||||
super.handleResult(result);
|
||||
|
||||
animateView(headerRootLayout, true, 100);
|
||||
animateView(headerUploaderLayout, true, 300);
|
||||
headerUploaderLayout.setOnClickListener(null);
|
||||
animate(headerBinding.getRoot(), true, 100);
|
||||
animate(headerBinding.uploaderLayout, true, 300);
|
||||
headerBinding.uploaderLayout.setOnClickListener(null);
|
||||
// If we have an uploader put them into the UI
|
||||
if (!TextUtils.isEmpty(result.getUploaderName())) {
|
||||
headerUploaderName.setText(result.getUploaderName());
|
||||
headerBinding.uploaderName.setText(result.getUploaderName());
|
||||
if (!TextUtils.isEmpty(result.getUploaderUrl())) {
|
||||
headerUploaderLayout.setOnClickListener(v -> {
|
||||
headerBinding.uploaderLayout.setOnClickListener(v -> {
|
||||
try {
|
||||
NavigationHelper.openChannelFragment(getFM(), result.getServiceId(),
|
||||
result.getUploaderUrl(), result.getUploaderName());
|
||||
} catch (final Exception e) {
|
||||
ErrorActivity.reportUiError((AppCompatActivity) getActivity(), e);
|
||||
ErrorActivity.reportUiErrorInSnackbar(this, "Opening channel fragment", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
} else { // Otherwise say we have no uploader
|
||||
headerUploaderName.setText(R.string.playlist_no_uploader);
|
||||
headerBinding.uploaderName.setText(R.string.playlist_no_uploader);
|
||||
}
|
||||
|
||||
playlistCtrl.setVisibility(View.VISIBLE);
|
||||
playlistControlBinding.getRoot().setVisibility(View.VISIBLE);
|
||||
|
||||
IMAGE_LOADER.displayImage(result.getUploaderAvatarUrl(), headerUploaderAvatar,
|
||||
ImageDisplayConstants.DISPLAY_AVATAR_OPTIONS);
|
||||
headerStreamCount.setText(Localization
|
||||
final String avatarUrl = result.getUploaderAvatarUrl();
|
||||
if (result.getServiceId() == ServiceList.YouTube.getServiceId()
|
||||
&& (YoutubeParsingHelper.isYoutubeMixId(result.getId())
|
||||
|| YoutubeParsingHelper.isYoutubeMusicMixId(result.getId()))) {
|
||||
// this is an auto-generated playlist (e.g. Youtube mix), so a radio is shown
|
||||
headerBinding.uploaderAvatarView.setDisableCircularTransformation(true);
|
||||
headerBinding.uploaderAvatarView.setBorderColor(
|
||||
getResources().getColor(R.color.transparent_background_color));
|
||||
headerBinding.uploaderAvatarView.setImageDrawable(
|
||||
AppCompatResources.getDrawable(requireContext(),
|
||||
resolveResourceIdFromAttr(requireContext(), R.attr.ic_radio))
|
||||
);
|
||||
} else {
|
||||
IMAGE_LOADER.displayImage(avatarUrl, headerBinding.uploaderAvatarView,
|
||||
ImageDisplayConstants.DISPLAY_AVATAR_OPTIONS);
|
||||
}
|
||||
|
||||
headerBinding.playlistStreamCount.setText(Localization
|
||||
.localizeStreamCount(getContext(), result.getStreamCount()));
|
||||
|
||||
if (!result.getErrors().isEmpty()) {
|
||||
showSnackBarError(result.getErrors(), UserAction.REQUESTED_PLAYLIST,
|
||||
NewPipe.getNameOfService(result.getServiceId()), result.getUrl(), 0);
|
||||
showSnackBarError(new ErrorInfo(result.getErrors(), UserAction.REQUESTED_PLAYLIST,
|
||||
result.getUrl(), result));
|
||||
}
|
||||
|
||||
remotePlaylistManager.getPlaylist(result)
|
||||
@@ -317,19 +328,19 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(getPlaylistBookmarkSubscriber());
|
||||
|
||||
headerPlayAllButton.setOnClickListener(view ->
|
||||
playlistControlBinding.playlistCtrlPlayAllButton.setOnClickListener(view ->
|
||||
NavigationHelper.playOnMainPlayer(activity, getPlayQueue()));
|
||||
headerPopupButton.setOnClickListener(view ->
|
||||
playlistControlBinding.playlistCtrlPlayPopupButton.setOnClickListener(view ->
|
||||
NavigationHelper.playOnPopupPlayer(activity, getPlayQueue(), false));
|
||||
headerBackgroundButton.setOnClickListener(view ->
|
||||
playlistControlBinding.playlistCtrlPlayBgButton.setOnClickListener(view ->
|
||||
NavigationHelper.playOnBackgroundPlayer(activity, getPlayQueue(), false));
|
||||
|
||||
headerPopupButton.setOnLongClickListener(view -> {
|
||||
playlistControlBinding.playlistCtrlPlayPopupButton.setOnLongClickListener(view -> {
|
||||
NavigationHelper.enqueueOnPopupPlayer(activity, getPlayQueue(), true);
|
||||
return true;
|
||||
});
|
||||
|
||||
headerBackgroundButton.setOnLongClickListener(view -> {
|
||||
playlistControlBinding.playlistCtrlPlayBgButton.setOnLongClickListener(view -> {
|
||||
NavigationHelper.enqueueOnBackgroundPlayer(activity, getPlayQueue(), true);
|
||||
return true;
|
||||
});
|
||||
@@ -355,33 +366,6 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleNextItems(final ListExtractor.InfoItemsPage result) {
|
||||
super.handleNextItems(result);
|
||||
|
||||
if (!result.getErrors().isEmpty()) {
|
||||
showSnackBarError(result.getErrors(), UserAction.REQUESTED_PLAYLIST,
|
||||
NewPipe.getNameOfService(serviceId), "Get next page of: " + url, 0);
|
||||
}
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// OnError
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
protected boolean onError(final Throwable exception) {
|
||||
if (super.onError(exception)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
final int errorId = exception instanceof ExtractionException
|
||||
? R.string.parsing_error : R.string.general_error;
|
||||
onUnrecoverableError(exception, UserAction.REQUESTED_PLAYLIST,
|
||||
NewPipe.getNameOfService(serviceId), url, errorId);
|
||||
return true;
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Utils
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
@@ -426,8 +410,9 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(final Throwable t) {
|
||||
PlaylistFragment.this.onError(t);
|
||||
public void onError(final Throwable throwable) {
|
||||
showError(new ErrorInfo(throwable, UserAction.REQUESTED_BOOKMARK,
|
||||
"Get playlist bookmarks"));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -438,7 +423,7 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
|
||||
@Override
|
||||
public void setTitle(final String title) {
|
||||
super.setTitle(title);
|
||||
headerTitleView.setText(title);
|
||||
headerBinding.playlistTitleView.setText(title);
|
||||
}
|
||||
|
||||
private void onBookmarkClicked() {
|
||||
@@ -452,12 +437,16 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
|
||||
if (currentInfo != null && playlistEntity == null) {
|
||||
action = remotePlaylistManager.onBookmark(currentInfo)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(ignored -> { /* Do nothing */ }, this::onError);
|
||||
.subscribe(ignored -> { /* Do nothing */ }, throwable ->
|
||||
showError(new ErrorInfo(throwable, UserAction.REQUESTED_BOOKMARK,
|
||||
"Adding playlist bookmark")));
|
||||
} else if (playlistEntity != null) {
|
||||
action = remotePlaylistManager.deletePlaylist(playlistEntity.getUid())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.doFinally(() -> playlistEntity = null)
|
||||
.subscribe(ignored -> { /* Do nothing */ }, this::onError);
|
||||
.subscribe(ignored -> { /* Do nothing */ }, throwable ->
|
||||
showError(new ErrorInfo(throwable, UserAction.REQUESTED_BOOKMARK,
|
||||
"Deleting playlist bookmark")));
|
||||
} else {
|
||||
action = Disposable.empty();
|
||||
}
|
||||
@@ -476,7 +465,7 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
|
||||
final int titleRes = playlistEntity == null
|
||||
? R.string.bookmark_playlist : R.string.unbookmark_playlist;
|
||||
|
||||
playlistBookmarkButton.setIcon(ThemeHelper.resolveResourceIdFromAttr(activity, iconAttr));
|
||||
playlistBookmarkButton.setIcon(resolveResourceIdFromAttr(activity, iconAttr));
|
||||
playlistBookmarkButton.setTitle(titleRes);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,26 +35,29 @@ import androidx.recyclerview.widget.ItemTouchHelper;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.ReCaptchaActivity;
|
||||
import org.schabi.newpipe.database.history.model.SearchHistoryEntry;
|
||||
import org.schabi.newpipe.databinding.FragmentSearchBinding;
|
||||
import org.schabi.newpipe.error.ErrorActivity;
|
||||
import org.schabi.newpipe.error.ErrorInfo;
|
||||
import org.schabi.newpipe.error.ReCaptchaActivity;
|
||||
import org.schabi.newpipe.error.UserAction;
|
||||
import org.schabi.newpipe.extractor.InfoItem;
|
||||
import org.schabi.newpipe.extractor.ListExtractor;
|
||||
import org.schabi.newpipe.extractor.MetaInfo;
|
||||
import org.schabi.newpipe.extractor.NewPipe;
|
||||
import org.schabi.newpipe.extractor.Page;
|
||||
import org.schabi.newpipe.extractor.StreamingService;
|
||||
import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
||||
import org.schabi.newpipe.extractor.search.SearchExtractor;
|
||||
import org.schabi.newpipe.extractor.search.SearchInfo;
|
||||
import org.schabi.newpipe.extractor.services.peertube.linkHandler.PeertubeSearchQueryHandlerFactory;
|
||||
import org.schabi.newpipe.extractor.services.youtube.linkHandler.YoutubeSearchQueryHandlerFactory;
|
||||
import org.schabi.newpipe.fragments.BackPressable;
|
||||
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.report.ErrorActivity;
|
||||
import org.schabi.newpipe.report.ErrorInfo;
|
||||
import org.schabi.newpipe.report.UserAction;
|
||||
import org.schabi.newpipe.util.AnimationUtils;
|
||||
import org.schabi.newpipe.util.Constants;
|
||||
import org.schabi.newpipe.util.DeviceUtils;
|
||||
import org.schabi.newpipe.util.ExceptionUtils;
|
||||
import org.schabi.newpipe.util.ExtractorHelper;
|
||||
import org.schabi.newpipe.util.NavigationHelper;
|
||||
import org.schabi.newpipe.util.ServiceHelper;
|
||||
@@ -78,7 +81,8 @@ import io.reactivex.rxjava3.subjects.PublishSubject;
|
||||
|
||||
import static androidx.recyclerview.widget.ItemTouchHelper.Callback.makeMovementFlags;
|
||||
import static java.util.Arrays.asList;
|
||||
import static org.schabi.newpipe.util.AnimationUtils.animateView;
|
||||
import static org.schabi.newpipe.ktx.ViewUtils.animate;
|
||||
import static org.schabi.newpipe.util.ExtractorHelper.showMetaInfoInTextView;
|
||||
|
||||
public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.InfoItemsPage<?>>
|
||||
implements BackPressable {
|
||||
@@ -129,6 +133,9 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
||||
@State
|
||||
boolean isCorrectedSearch;
|
||||
|
||||
@State
|
||||
MetaInfo[] metaInfo;
|
||||
|
||||
@State
|
||||
boolean wasSearchFocused = false;
|
||||
|
||||
@@ -148,15 +155,13 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
||||
// Views
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
private FragmentSearchBinding searchBinding;
|
||||
|
||||
private View searchToolbarContainer;
|
||||
private EditText searchEditText;
|
||||
private View searchClear;
|
||||
|
||||
private TextView correctSuggestion;
|
||||
|
||||
private View suggestionsPanel;
|
||||
private boolean suggestionsPanelVisible = false;
|
||||
private RecyclerView suggestionsRecyclerView;
|
||||
|
||||
/*////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@@ -185,7 +190,7 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public void onAttach(final Context context) {
|
||||
public void onAttach(@NonNull final Context context) {
|
||||
super.onAttach(context);
|
||||
|
||||
suggestionListAdapter = new SuggestionListAdapter(activity);
|
||||
@@ -215,7 +220,7 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(final View rootView, final Bundle savedInstanceState) {
|
||||
public void onViewCreated(@NonNull final View rootView, final Bundle savedInstanceState) {
|
||||
super.onViewCreated(rootView, savedInstanceState);
|
||||
showSearchOnStart();
|
||||
initSearchListeners();
|
||||
@@ -247,20 +252,23 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
||||
try {
|
||||
service = NewPipe.getService(serviceId);
|
||||
} catch (final Exception e) {
|
||||
ErrorActivity.reportError(getActivity(), e, requireActivity().getClass(),
|
||||
requireActivity().findViewById(android.R.id.content),
|
||||
ErrorInfo.make(UserAction.UI_ERROR,
|
||||
"",
|
||||
"", R.string.general_error));
|
||||
ErrorActivity.reportUiErrorInSnackbar(this,
|
||||
"Getting service for id " + serviceId, e);
|
||||
}
|
||||
|
||||
if (suggestionDisposable == null || suggestionDisposable.isDisposed()) {
|
||||
initSuggestionObserver();
|
||||
}
|
||||
|
||||
if (!TextUtils.isEmpty(searchString)) {
|
||||
if (wasLoading.getAndSet(false)) {
|
||||
search(searchString, contentFilter, sortFilter);
|
||||
return;
|
||||
} else if (infoListAdapter.getItemsList().isEmpty()) {
|
||||
if (savedState == null) {
|
||||
search(searchString, contentFilter, sortFilter);
|
||||
} else if (!isLoading.get() && !wasSearchFocused) {
|
||||
return;
|
||||
} else if (!isLoading.get() && !wasSearchFocused && lastPanelError == null) {
|
||||
infoListAdapter.clearStreamItemList();
|
||||
showEmptyState();
|
||||
}
|
||||
@@ -269,9 +277,8 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
||||
|
||||
handleSearchSuggestion();
|
||||
|
||||
if (suggestionDisposable == null || suggestionDisposable.isDisposed()) {
|
||||
initSuggestionObserver();
|
||||
}
|
||||
disposables.add(showMetaInfoInTextView(metaInfo == null ? null : Arrays.asList(metaInfo),
|
||||
searchBinding.searchMetaInfoTextView, searchBinding.searchMetaInfoSeparator));
|
||||
|
||||
if (TextUtils.isEmpty(searchString) || wasSearchFocused) {
|
||||
showKeyboardSearch();
|
||||
@@ -289,6 +296,8 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
||||
Log.d(TAG, "onDestroyView() called");
|
||||
}
|
||||
unsetSearchListeners();
|
||||
|
||||
searchBinding = null;
|
||||
super.onDestroyView();
|
||||
}
|
||||
|
||||
@@ -325,9 +334,9 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
||||
@Override
|
||||
protected void initViews(final View rootView, final Bundle savedInstanceState) {
|
||||
super.initViews(rootView, savedInstanceState);
|
||||
suggestionsPanel = rootView.findViewById(R.id.suggestions_panel);
|
||||
suggestionsRecyclerView = rootView.findViewById(R.id.suggestions_list);
|
||||
suggestionsRecyclerView.setAdapter(suggestionListAdapter);
|
||||
searchBinding = FragmentSearchBinding.bind(rootView);
|
||||
|
||||
searchBinding.suggestionsList.setAdapter(suggestionListAdapter);
|
||||
new ItemTouchHelper(new ItemTouchHelper.Callback() {
|
||||
@Override
|
||||
public int getMovementFlags(@NonNull final RecyclerView recyclerView,
|
||||
@@ -346,13 +355,11 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
||||
public void onSwiped(@NonNull final RecyclerView.ViewHolder viewHolder, final int i) {
|
||||
onSuggestionItemSwiped(viewHolder);
|
||||
}
|
||||
}).attachToRecyclerView(suggestionsRecyclerView);
|
||||
}).attachToRecyclerView(searchBinding.suggestionsList);
|
||||
|
||||
searchToolbarContainer = activity.findViewById(R.id.toolbar_search_container);
|
||||
searchEditText = searchToolbarContainer.findViewById(R.id.toolbar_search_edit_text);
|
||||
searchClear = searchToolbarContainer.findViewById(R.id.toolbar_search_clear);
|
||||
|
||||
correctSuggestion = rootView.findViewById(R.id.correct_suggestion);
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
@@ -372,7 +379,7 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSaveInstanceState(final Bundle bundle) {
|
||||
public void onSaveInstanceState(@NonNull final Bundle bundle) {
|
||||
searchString = searchEditText != null
|
||||
? searchEditText.getText().toString()
|
||||
: searchString;
|
||||
@@ -395,7 +402,7 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
||||
searchEditText.setText("");
|
||||
showKeyboardSearch();
|
||||
}
|
||||
animateView(errorPanelRoot, false, 200);
|
||||
hideErrorPanel();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -419,12 +426,18 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
||||
boolean isFirstItem = true;
|
||||
final Context c = getContext();
|
||||
for (final String filter : service.getSearchQHFactory().getAvailableContentFilter()) {
|
||||
if (filter.equals("music_songs")) {
|
||||
if (filter.equals(YoutubeSearchQueryHandlerFactory.MUSIC_SONGS)) {
|
||||
final MenuItem musicItem = menu.add(2,
|
||||
itemId++,
|
||||
0,
|
||||
"YouTube Music");
|
||||
musicItem.setEnabled(false);
|
||||
} else if (filter.equals(PeertubeSearchQueryHandlerFactory.SEPIA_VIDEOS)) {
|
||||
final MenuItem sepiaItem = menu.add(2,
|
||||
itemId++,
|
||||
0,
|
||||
"Sepia Search");
|
||||
sepiaItem.setEnabled(false);
|
||||
}
|
||||
menuItemToFilterName.put(itemId, filter);
|
||||
final MenuItem item = menu.add(1,
|
||||
@@ -503,7 +516,7 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
||||
return;
|
||||
}
|
||||
|
||||
correctSuggestion.setVisibility(View.GONE);
|
||||
searchBinding.correctSuggestion.setVisibility(View.GONE);
|
||||
|
||||
searchEditText.setText("");
|
||||
suggestionListAdapter.setItems(new ArrayList<>());
|
||||
@@ -516,7 +529,7 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "onClick() called with: v = [" + v + "]");
|
||||
}
|
||||
if (isSuggestionsEnabled && errorPanelRoot.getVisibility() != View.VISIBLE) {
|
||||
if (isSuggestionsEnabled && !isErrorPanelVisible()) {
|
||||
showSuggestionsPanel();
|
||||
}
|
||||
if (DeviceUtils.isTv(getContext())) {
|
||||
@@ -529,8 +542,7 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
||||
Log.d(TAG, "onFocusChange() called with: "
|
||||
+ "v = [" + v + "], hasFocus = [" + hasFocus + "]");
|
||||
}
|
||||
if (isSuggestionsEnabled && hasFocus
|
||||
&& errorPanelRoot.getVisibility() != View.VISIBLE) {
|
||||
if (isSuggestionsEnabled && hasFocus && !isErrorPanelVisible()) {
|
||||
showSuggestionsPanel();
|
||||
}
|
||||
});
|
||||
@@ -620,7 +632,8 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
||||
Log.d(TAG, "showSuggestionsPanel() called");
|
||||
}
|
||||
suggestionsPanelVisible = true;
|
||||
animateView(suggestionsPanel, AnimationUtils.Type.LIGHT_SLIDE_AND_ALPHA, true, 200);
|
||||
animate(searchBinding.suggestionsPanel, true, 200,
|
||||
AnimationType.LIGHT_SLIDE_AND_ALPHA);
|
||||
}
|
||||
|
||||
private void hideSuggestionsPanel() {
|
||||
@@ -628,7 +641,8 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
||||
Log.d(TAG, "hideSuggestionsPanel() called");
|
||||
}
|
||||
suggestionsPanelVisible = false;
|
||||
animateView(suggestionsPanel, AnimationUtils.Type.LIGHT_SLIDE_AND_ALPHA, false, 200);
|
||||
animate(searchBinding.suggestionsPanel, false, 200,
|
||||
AnimationType.LIGHT_SLIDE_AND_ALPHA);
|
||||
}
|
||||
|
||||
private void showKeyboardSearch() {
|
||||
@@ -678,9 +692,9 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
||||
.subscribe(
|
||||
howManyDeleted -> suggestionPublisher
|
||||
.onNext(searchEditText.getText().toString()),
|
||||
throwable -> showSnackBarError(throwable,
|
||||
UserAction.DELETE_FROM_HISTORY, "none",
|
||||
"Deleting item failed", R.string.general_error));
|
||||
throwable -> showSnackBarError(new ErrorInfo(throwable,
|
||||
UserAction.DELETE_FROM_HISTORY,
|
||||
"Deleting item failed")));
|
||||
disposables.add(onDelete);
|
||||
})
|
||||
.show();
|
||||
@@ -707,14 +721,12 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
||||
suggestionDisposable.dispose();
|
||||
}
|
||||
|
||||
final Observable<String> observable = suggestionPublisher
|
||||
suggestionDisposable = suggestionPublisher
|
||||
.debounce(SUGGESTIONS_DEBOUNCE, TimeUnit.MILLISECONDS)
|
||||
.startWithItem(searchString != null
|
||||
? searchString
|
||||
: "")
|
||||
.filter(ss -> isSuggestionsEnabled);
|
||||
|
||||
suggestionDisposable = observable
|
||||
.filter(ss -> isSuggestionsEnabled)
|
||||
.switchMap(query -> {
|
||||
final Flowable<List<SearchHistoryEntry>> flowable = historyRecordManager
|
||||
.getRelatedSearches(query, 3, 25);
|
||||
@@ -737,8 +749,8 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
||||
.suggestionsFor(serviceId, query)
|
||||
.onErrorReturn(throwable -> {
|
||||
if (!ExceptionUtils.isNetworkRelated(throwable)) {
|
||||
showSnackBarError(throwable, UserAction.GET_SUGGESTIONS,
|
||||
NewPipe.getNameOfService(serviceId), searchString, 0);
|
||||
showSnackBarError(new ErrorInfo(throwable,
|
||||
UserAction.GET_SUGGESTIONS, searchString, serviceId));
|
||||
}
|
||||
return new ArrayList<>();
|
||||
})
|
||||
@@ -774,7 +786,8 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
||||
if (listNotification.isOnNext()) {
|
||||
handleSuggestions(listNotification.getValue());
|
||||
} else if (listNotification.isOnError()) {
|
||||
onSuggestionError(listNotification.getError());
|
||||
showError(new ErrorInfo(listNotification.getError(),
|
||||
UserAction.GET_SUGGESTIONS, searchString, serviceId));
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -806,8 +819,7 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
||||
.subscribe(intent -> {
|
||||
getFM().popBackStackImmediate();
|
||||
activity.startActivity(intent);
|
||||
}, throwable ->
|
||||
showError(getString(R.string.unsupported_url), false)));
|
||||
}, throwable -> showTextError(getString(R.string.unsupported_url))));
|
||||
return;
|
||||
}
|
||||
} catch (final Exception ignored) {
|
||||
@@ -818,15 +830,16 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
||||
this.searchString = theSearchString;
|
||||
infoListAdapter.clearStreamItemList();
|
||||
hideSuggestionsPanel();
|
||||
showMetaInfoInTextView(null, searchBinding.searchMetaInfoTextView,
|
||||
searchBinding.searchMetaInfoSeparator);
|
||||
hideKeyboardSearch();
|
||||
|
||||
disposables.add(historyRecordManager.onSearched(serviceId, theSearchString)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
ignored -> {
|
||||
},
|
||||
error -> showSnackBarError(error, UserAction.SEARCHED,
|
||||
NewPipe.getNameOfService(serviceId), theSearchString, 0)
|
||||
ignored -> { },
|
||||
throwable -> showSnackBarError(new ErrorInfo(throwable, UserAction.SEARCHED,
|
||||
theSearchString, serviceId))
|
||||
));
|
||||
suggestionPublisher.onNext(theSearchString);
|
||||
startLoading(false);
|
||||
@@ -846,7 +859,7 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.doOnEvent((searchResult, throwable) -> isLoading.set(false))
|
||||
.subscribe(this::handleResult, this::onError);
|
||||
.subscribe(this::handleResult, this::onItemError);
|
||||
|
||||
}
|
||||
|
||||
@@ -869,7 +882,7 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.doOnEvent((nextItemsResult, throwable) -> isLoading.set(false))
|
||||
.subscribe(this::handleNextItems, this::onError);
|
||||
.subscribe(this::handleNextItems, this::onItemError);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -883,6 +896,15 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
||||
hideKeyboardSearch();
|
||||
}
|
||||
|
||||
private void onItemError(final Throwable exception) {
|
||||
if (exception instanceof SearchExtractor.NothingFoundException) {
|
||||
infoListAdapter.clearStreamItemList();
|
||||
showEmptyState();
|
||||
} else {
|
||||
showError(new ErrorInfo(exception, UserAction.SEARCHED, searchString, serviceId));
|
||||
}
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Utils
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
@@ -916,29 +938,14 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "handleSuggestions() called with: suggestions = [" + suggestions + "]");
|
||||
}
|
||||
suggestionsRecyclerView.smoothScrollToPosition(0);
|
||||
suggestionsRecyclerView.post(() -> suggestionListAdapter.setItems(suggestions));
|
||||
searchBinding.suggestionsList.smoothScrollToPosition(0);
|
||||
searchBinding.suggestionsList.post(() -> suggestionListAdapter.setItems(suggestions));
|
||||
|
||||
if (suggestionsPanelVisible && errorPanelRoot.getVisibility() == View.VISIBLE) {
|
||||
if (suggestionsPanelVisible && isErrorPanelVisible()) {
|
||||
hideLoading();
|
||||
}
|
||||
}
|
||||
|
||||
public void onSuggestionError(final Throwable exception) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "onSuggestionError() called with: exception = [" + exception + "]");
|
||||
}
|
||||
if (super.onError(exception)) {
|
||||
return;
|
||||
}
|
||||
|
||||
final int errorId = exception instanceof ParsingException
|
||||
? R.string.parsing_error
|
||||
: R.string.general_error;
|
||||
onUnrecoverableError(exception, UserAction.GET_SUGGESTIONS,
|
||||
NewPipe.getNameOfService(serviceId), searchString, errorId);
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Contract
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
@@ -949,13 +956,6 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
||||
showListFooter(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void showError(final String message, final boolean showRetryButton) {
|
||||
super.showError(message, showRetryButton);
|
||||
hideSuggestionsPanel();
|
||||
hideKeyboardSearch();
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Search Results
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
@@ -966,13 +966,19 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
||||
if (!exceptions.isEmpty()
|
||||
&& !(exceptions.size() == 1
|
||||
&& exceptions.get(0) instanceof SearchExtractor.NothingFoundException)) {
|
||||
showSnackBarError(result.getErrors(), UserAction.SEARCHED,
|
||||
NewPipe.getNameOfService(serviceId), searchString, 0);
|
||||
showSnackBarError(new ErrorInfo(result.getErrors(), UserAction.SEARCHED,
|
||||
searchString, serviceId));
|
||||
}
|
||||
|
||||
searchSuggestion = result.getSearchSuggestion();
|
||||
isCorrectedSearch = result.isCorrectedSearch();
|
||||
|
||||
// List<MetaInfo> cannot be bundled without creating some containers
|
||||
metaInfo = new MetaInfo[result.getMetaInfo().size()];
|
||||
metaInfo = result.getMetaInfo().toArray(metaInfo);
|
||||
disposables.add(showMetaInfoInTextView(result.getMetaInfo(),
|
||||
searchBinding.searchMetaInfoTextView, searchBinding.searchMetaInfoSeparator));
|
||||
|
||||
handleSearchSuggestion();
|
||||
|
||||
lastSearchedString = searchString;
|
||||
@@ -993,7 +999,7 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
||||
|
||||
private void handleSearchSuggestion() {
|
||||
if (TextUtils.isEmpty(searchSuggestion)) {
|
||||
correctSuggestion.setVisibility(View.GONE);
|
||||
searchBinding.correctSuggestion.setVisibility(View.GONE);
|
||||
} else {
|
||||
final String helperText = getString(isCorrectedSearch
|
||||
? R.string.search_showing_result_for
|
||||
@@ -1002,22 +1008,23 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
||||
final String highlightedSearchSuggestion =
|
||||
"<b><i>" + Html.escapeHtml(searchSuggestion) + "</i></b>";
|
||||
final String text = String.format(helperText, highlightedSearchSuggestion);
|
||||
correctSuggestion.setText(HtmlCompat.fromHtml(text, HtmlCompat.FROM_HTML_MODE_LEGACY));
|
||||
searchBinding.correctSuggestion.setText(HtmlCompat.fromHtml(text,
|
||||
HtmlCompat.FROM_HTML_MODE_LEGACY));
|
||||
|
||||
correctSuggestion.setOnClickListener(v -> {
|
||||
correctSuggestion.setVisibility(View.GONE);
|
||||
searchBinding.correctSuggestion.setOnClickListener(v -> {
|
||||
searchBinding.correctSuggestion.setVisibility(View.GONE);
|
||||
search(searchSuggestion, contentFilter, sortFilter);
|
||||
searchEditText.setText(searchSuggestion);
|
||||
});
|
||||
|
||||
correctSuggestion.setOnLongClickListener(v -> {
|
||||
searchBinding.correctSuggestion.setOnLongClickListener(v -> {
|
||||
searchEditText.setText(searchSuggestion);
|
||||
searchEditText.setSelection(searchSuggestion.length());
|
||||
showKeyboardSearch();
|
||||
return true;
|
||||
});
|
||||
|
||||
correctSuggestion.setVisibility(View.VISIBLE);
|
||||
searchBinding.correctSuggestion.setVisibility(View.VISIBLE);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1028,33 +1035,20 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
||||
nextPage = result.getNextPage();
|
||||
|
||||
if (!result.getErrors().isEmpty()) {
|
||||
showSnackBarError(result.getErrors(), UserAction.SEARCHED,
|
||||
NewPipe.getNameOfService(serviceId),
|
||||
showSnackBarError(new ErrorInfo(result.getErrors(), UserAction.SEARCHED,
|
||||
"\"" + searchString + "\" → pageUrl: " + nextPage.getUrl() + ", "
|
||||
+ "pageIds: " + nextPage.getIds() + ", "
|
||||
+ "pageCookies: " + nextPage.getCookies(), 0);
|
||||
+ "pageCookies: " + nextPage.getCookies(),
|
||||
serviceId));
|
||||
}
|
||||
super.handleNextItems(result);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean onError(final Throwable exception) {
|
||||
if (super.onError(exception)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (exception instanceof SearchExtractor.NothingFoundException) {
|
||||
infoListAdapter.clearStreamItemList();
|
||||
showEmptyState();
|
||||
} else {
|
||||
final int errorId = exception instanceof ParsingException
|
||||
? R.string.parsing_error
|
||||
: R.string.general_error;
|
||||
onUnrecoverableError(exception, UserAction.SEARCHED,
|
||||
NewPipe.getNameOfService(serviceId), searchString, errorId);
|
||||
}
|
||||
|
||||
return true;
|
||||
public void handleError() {
|
||||
super.handleError();
|
||||
hideSuggestionsPanel();
|
||||
hideKeyboardSearch();
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
@@ -1080,9 +1074,8 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
||||
.subscribe(
|
||||
howManyDeleted -> suggestionPublisher
|
||||
.onNext(searchEditText.getText().toString()),
|
||||
throwable -> showSnackBarError(throwable,
|
||||
UserAction.DELETE_FROM_HISTORY, "none",
|
||||
"Deleting item failed", R.string.general_error));
|
||||
throwable -> showSnackBarError(new ErrorInfo(throwable,
|
||||
UserAction.DELETE_FROM_HISTORY, "Deleting item failed")));
|
||||
disposables.add(onDelete);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,19 +8,19 @@ import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.Switch;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.preference.PreferenceManager;
|
||||
import androidx.viewbinding.ViewBinding;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.databinding.RelatedStreamsHeaderBinding;
|
||||
import org.schabi.newpipe.error.UserAction;
|
||||
import org.schabi.newpipe.extractor.ListExtractor;
|
||||
import org.schabi.newpipe.extractor.NewPipe;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||
import org.schabi.newpipe.fragments.list.BaseListInfoFragment;
|
||||
import org.schabi.newpipe.report.UserAction;
|
||||
import org.schabi.newpipe.util.AnimationUtils;
|
||||
import org.schabi.newpipe.ktx.ViewUtils;
|
||||
import org.schabi.newpipe.util.RelatedStreamInfo;
|
||||
|
||||
import java.io.Serializable;
|
||||
@@ -38,8 +38,7 @@ public class RelatedVideosFragment extends BaseListInfoFragment<RelatedStreamInf
|
||||
// Views
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
private View headerRootLayout;
|
||||
private Switch autoplaySwitch;
|
||||
private RelatedStreamsHeaderBinding headerBinding;
|
||||
|
||||
public static RelatedVideosFragment getInstance(final StreamInfo info) {
|
||||
final RelatedVideosFragment instance = new RelatedVideosFragment();
|
||||
@@ -47,12 +46,16 @@ public class RelatedVideosFragment extends BaseListInfoFragment<RelatedStreamInf
|
||||
return instance;
|
||||
}
|
||||
|
||||
public RelatedVideosFragment() {
|
||||
super(UserAction.REQUESTED_STREAM);
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// LifeCycle
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public void onAttach(final Context context) {
|
||||
public void onAttach(@NonNull final Context context) {
|
||||
super.onAttach(context);
|
||||
}
|
||||
|
||||
@@ -66,25 +69,29 @@ public class RelatedVideosFragment extends BaseListInfoFragment<RelatedStreamInf
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
if (disposables != null) {
|
||||
disposables.clear();
|
||||
}
|
||||
disposables.clear();
|
||||
}
|
||||
|
||||
protected View getListHeader() {
|
||||
@Override
|
||||
public void onDestroyView() {
|
||||
headerBinding = null;
|
||||
super.onDestroyView();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ViewBinding getListHeader() {
|
||||
if (relatedStreamInfo != null && relatedStreamInfo.getRelatedItems() != null) {
|
||||
headerRootLayout = activity.getLayoutInflater()
|
||||
.inflate(R.layout.related_streams_header, itemsList, false);
|
||||
autoplaySwitch = headerRootLayout.findViewById(R.id.autoplay_switch);
|
||||
headerBinding = RelatedStreamsHeaderBinding
|
||||
.inflate(activity.getLayoutInflater(), itemsList, false);
|
||||
|
||||
final SharedPreferences pref = PreferenceManager
|
||||
.getDefaultSharedPreferences(requireContext());
|
||||
final boolean autoplay = pref.getBoolean(getString(R.string.auto_queue_key), false);
|
||||
autoplaySwitch.setChecked(autoplay);
|
||||
autoplaySwitch.setOnCheckedChangeListener((compoundButton, b) ->
|
||||
headerBinding.autoplaySwitch.setChecked(autoplay);
|
||||
headerBinding.autoplaySwitch.setOnCheckedChangeListener((compoundButton, b) ->
|
||||
PreferenceManager.getDefaultSharedPreferences(requireContext()).edit()
|
||||
.putBoolean(getString(R.string.auto_queue_key), b).apply());
|
||||
return headerRootLayout;
|
||||
return headerBinding;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
@@ -107,8 +114,8 @@ public class RelatedVideosFragment extends BaseListInfoFragment<RelatedStreamInf
|
||||
@Override
|
||||
public void showLoading() {
|
||||
super.showLoading();
|
||||
if (headerRootLayout != null) {
|
||||
headerRootLayout.setVisibility(View.INVISIBLE);
|
||||
if (headerBinding != null) {
|
||||
headerBinding.getRoot().setVisibility(View.INVISIBLE);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -116,48 +123,12 @@ public class RelatedVideosFragment extends BaseListInfoFragment<RelatedStreamInf
|
||||
public void handleResult(@NonNull final RelatedStreamInfo result) {
|
||||
super.handleResult(result);
|
||||
|
||||
if (headerRootLayout != null) {
|
||||
headerRootLayout.setVisibility(View.VISIBLE);
|
||||
if (headerBinding != null) {
|
||||
headerBinding.getRoot().setVisibility(View.VISIBLE);
|
||||
}
|
||||
AnimationUtils.slideUp(getView(), 120, 96, 0.06f);
|
||||
ViewUtils.slideUp(requireView(), 120, 96, 0.06f);
|
||||
|
||||
if (!result.getErrors().isEmpty()) {
|
||||
showSnackBarError(result.getErrors(), UserAction.REQUESTED_STREAM,
|
||||
NewPipe.getNameOfService(result.getServiceId()), result.getUrl(), 0);
|
||||
}
|
||||
|
||||
if (disposables != null) {
|
||||
disposables.clear();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleNextItems(final ListExtractor.InfoItemsPage result) {
|
||||
super.handleNextItems(result);
|
||||
|
||||
if (!result.getErrors().isEmpty()) {
|
||||
showSnackBarError(result.getErrors(),
|
||||
UserAction.REQUESTED_STREAM,
|
||||
NewPipe.getNameOfService(serviceId),
|
||||
"Get next page of: " + url,
|
||||
R.string.general_error);
|
||||
}
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// OnError
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
protected boolean onError(final Throwable exception) {
|
||||
if (super.onError(exception)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
hideLoading();
|
||||
showSnackBarError(exception, UserAction.REQUESTED_STREAM,
|
||||
NewPipe.getNameOfService(serviceId), url, R.string.general_error);
|
||||
return true;
|
||||
disposables.clear();
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
@@ -180,7 +151,7 @@ public class RelatedVideosFragment extends BaseListInfoFragment<RelatedStreamInf
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSaveInstanceState(final Bundle outState) {
|
||||
public void onSaveInstanceState(@NonNull final Bundle outState) {
|
||||
super.onSaveInstanceState(outState);
|
||||
outState.putSerializable(INFO_KEY, relatedStreamInfo);
|
||||
}
|
||||
@@ -188,11 +159,9 @@ public class RelatedVideosFragment extends BaseListInfoFragment<RelatedStreamInf
|
||||
@Override
|
||||
protected void onRestoreInstanceState(@NonNull final Bundle savedState) {
|
||||
super.onRestoreInstanceState(savedState);
|
||||
if (savedState != null) {
|
||||
final Serializable serializable = savedState.getSerializable(INFO_KEY);
|
||||
if (serializable instanceof RelatedStreamInfo) {
|
||||
this.relatedStreamInfo = (RelatedStreamInfo) serializable;
|
||||
}
|
||||
final Serializable serializable = savedState.getSerializable(INFO_KEY);
|
||||
if (serializable instanceof RelatedStreamInfo) {
|
||||
this.relatedStreamInfo = (RelatedStreamInfo) serializable;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -202,8 +171,8 @@ public class RelatedVideosFragment extends BaseListInfoFragment<RelatedStreamInf
|
||||
final SharedPreferences pref =
|
||||
PreferenceManager.getDefaultSharedPreferences(requireContext());
|
||||
final boolean autoplay = pref.getBoolean(getString(R.string.auto_queue_key), false);
|
||||
if (autoplaySwitch != null) {
|
||||
autoplaySwitch.setChecked(autoplay);
|
||||
if (headerBinding != null) {
|
||||
headerBinding.autoplaySwitch.setChecked(autoplay);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
package org.schabi.newpipe.info_list
|
||||
|
||||
import android.util.Log
|
||||
import com.xwray.groupie.GroupAdapter
|
||||
import com.xwray.groupie.GroupieViewHolder
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfo
|
||||
import kotlin.math.max
|
||||
|
||||
/**
|
||||
* Custom RecyclerView.Adapter/GroupieAdapter for [StreamSegmentItem] for handling selection state.
|
||||
*/
|
||||
class StreamSegmentAdapter(
|
||||
private val listener: StreamSegmentListener
|
||||
) : GroupAdapter<GroupieViewHolder>() {
|
||||
|
||||
var currentIndex: Int = 0
|
||||
private set
|
||||
|
||||
/**
|
||||
* Returns `true` if the provided [StreamInfo] contains segments, `false` otherwise.
|
||||
*/
|
||||
fun setItems(info: StreamInfo): Boolean {
|
||||
if (info.streamSegments.isNotEmpty()) {
|
||||
clear()
|
||||
addAll(info.streamSegments.map { StreamSegmentItem(it, listener) })
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
fun selectSegment(segment: StreamSegmentItem) {
|
||||
unSelectCurrentSegment()
|
||||
currentIndex = max(0, getAdapterPosition(segment))
|
||||
segment.isSelected = true
|
||||
segment.notifyChanged(StreamSegmentItem.PAYLOAD_SELECT)
|
||||
}
|
||||
|
||||
fun selectSegmentAt(position: Int) {
|
||||
try {
|
||||
selectSegment(getGroupAtAdapterPosition(position) as StreamSegmentItem)
|
||||
} catch (e: IndexOutOfBoundsException) {
|
||||
// Just to make sure that getGroupAtAdapterPosition doesn't close the app
|
||||
// Shouldn't happen since setItems is always called before select-methods but just in case
|
||||
currentIndex = 0
|
||||
Log.e("StreamSegmentAdapter", "selectSegmentAt: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
private fun unSelectCurrentSegment() {
|
||||
try {
|
||||
val segmentItem = getGroupAtAdapterPosition(currentIndex) as StreamSegmentItem
|
||||
currentIndex = 0
|
||||
segmentItem.isSelected = false
|
||||
segmentItem.notifyChanged(StreamSegmentItem.PAYLOAD_SELECT)
|
||||
} catch (e: IndexOutOfBoundsException) {
|
||||
// Just to make sure that getGroupAtAdapterPosition doesn't close the app
|
||||
// Shouldn't happen since setItems is always called before select-methods but just in case
|
||||
currentIndex = 0
|
||||
Log.e("StreamSegmentAdapter", "unSelectCurrentSegment: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
interface StreamSegmentListener {
|
||||
fun onItemClick(item: StreamSegmentItem, seconds: Int)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package org.schabi.newpipe.info_list
|
||||
|
||||
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
|
||||
|
||||
class StreamSegmentItem(
|
||||
private val item: StreamSegment,
|
||||
private val onClick: StreamSegmentAdapter.StreamSegmentListener
|
||||
) : Item<GroupieViewHolder>() {
|
||||
|
||||
companion object {
|
||||
const val PAYLOAD_SELECT = 1
|
||||
}
|
||||
|
||||
var isSelected = false
|
||||
|
||||
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
|
||||
)
|
||||
}
|
||||
viewHolder.root.findViewById<TextView>(R.id.textViewTitle).text = item.title
|
||||
viewHolder.root.findViewById<TextView>(R.id.textViewStartSeconds).text =
|
||||
Localization.getDurationString(item.startTimeSeconds.toLong())
|
||||
viewHolder.root.setOnClickListener { onClick.onItemClick(this, item.startTimeSeconds) }
|
||||
viewHolder.root.isSelected = isSelected
|
||||
}
|
||||
|
||||
override fun bind(viewHolder: GroupieViewHolder, position: Int, payloads: MutableList<Any>) {
|
||||
if (payloads.contains(PAYLOAD_SELECT)) {
|
||||
viewHolder.root.isSelected = isSelected
|
||||
return
|
||||
}
|
||||
super.bind(viewHolder, position, payloads)
|
||||
}
|
||||
|
||||
override fun getLayout() = R.layout.item_stream_segment
|
||||
}
|
||||
@@ -13,15 +13,14 @@ import android.widget.TextView;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.preference.PreferenceManager;
|
||||
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.error.ErrorActivity;
|
||||
import org.schabi.newpipe.extractor.InfoItem;
|
||||
import org.schabi.newpipe.extractor.comments.CommentsInfoItem;
|
||||
import org.schabi.newpipe.info_list.InfoItemBuilder;
|
||||
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
||||
import org.schabi.newpipe.report.ErrorActivity;
|
||||
import org.schabi.newpipe.util.DeviceUtils;
|
||||
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;
|
||||
@@ -172,15 +171,15 @@ public class CommentsMiniInfoItemHolder extends InfoItemHolder {
|
||||
if (TextUtils.isEmpty(item.getUploaderUrl())) {
|
||||
return;
|
||||
}
|
||||
final AppCompatActivity activity = (AppCompatActivity) itemBuilder.getContext();
|
||||
try {
|
||||
final AppCompatActivity activity = (AppCompatActivity) itemBuilder.getContext();
|
||||
NavigationHelper.openChannelFragment(
|
||||
activity.getSupportFragmentManager(),
|
||||
item.getServiceId(),
|
||||
item.getUploaderUrl(),
|
||||
item.getUploaderName());
|
||||
} catch (final Exception e) {
|
||||
ErrorActivity.reportUiError((AppCompatActivity) itemBuilder.getContext(), e);
|
||||
ErrorActivity.reportUiErrorInSnackbar(activity, "Opening channel fragment", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
package org.schabi.newpipe.info_list.holder;
|
||||
|
||||
import androidx.preference.PreferenceManager;
|
||||
import android.text.TextUtils;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.preference.PreferenceManager;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.extractor.InfoItem;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||
|
||||
@@ -13,8 +13,8 @@ import org.schabi.newpipe.extractor.InfoItem;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||
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.AnimationUtils;
|
||||
import org.schabi.newpipe.util.ImageDisplayConstants;
|
||||
import org.schabi.newpipe.util.Localization;
|
||||
import org.schabi.newpipe.views.AnimatedProgressBar;
|
||||
@@ -70,7 +70,8 @@ public class StreamMiniInfoItemHolder extends InfoItemHolder {
|
||||
} else {
|
||||
itemProgressView.setVisibility(View.GONE);
|
||||
}
|
||||
} else if (item.getStreamType() == StreamType.LIVE_STREAM) {
|
||||
} else if (item.getStreamType() == StreamType.LIVE_STREAM
|
||||
|| item.getStreamType() == StreamType.AUDIO_LIVE_STREAM) {
|
||||
itemDurationView.setText(R.string.duration_live);
|
||||
itemDurationView.setBackgroundColor(ContextCompat.getColor(itemBuilder.getContext(),
|
||||
R.color.live_duration_background_color));
|
||||
@@ -124,10 +125,10 @@ public class StreamMiniInfoItemHolder extends InfoItemHolder {
|
||||
} else {
|
||||
itemProgressView.setProgress((int) TimeUnit.MILLISECONDS
|
||||
.toSeconds(state.getProgressTime()));
|
||||
AnimationUtils.animateView(itemProgressView, true, 500);
|
||||
ViewUtils.animate(itemProgressView, true, 500);
|
||||
}
|
||||
} else if (itemProgressView.getVisibility() == View.VISIBLE) {
|
||||
AnimationUtils.animateView(itemProgressView, false, 500);
|
||||
ViewUtils.animate(itemProgressView, false, 500);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
package org.schabi.newpipe.ktx
|
||||
|
||||
import java.time.OffsetDateTime
|
||||
import java.time.ZoneOffset
|
||||
import java.time.temporal.ChronoField
|
||||
import java.util.Calendar
|
||||
import java.util.Date
|
||||
import java.util.GregorianCalendar
|
||||
import java.util.TimeZone
|
||||
|
||||
// This method is a modified version of GregorianCalendar.from(ZonedDateTime).
|
||||
// Math.addExact() and Math.multiplyExact() are desugared even though lint displays a warning.
|
||||
@SuppressWarnings("NewApi")
|
||||
fun OffsetDateTime.toCalendar(): Calendar {
|
||||
val cal = GregorianCalendar(TimeZone.getTimeZone("UTC"))
|
||||
val offsetDateTimeUTC = withOffsetSameInstant(ZoneOffset.UTC)
|
||||
cal.gregorianChange = Date(Long.MIN_VALUE)
|
||||
cal.firstDayOfWeek = Calendar.MONDAY
|
||||
cal.minimalDaysInFirstWeek = 4
|
||||
try {
|
||||
cal.timeInMillis = Math.addExact(
|
||||
Math.multiplyExact(offsetDateTimeUTC.toEpochSecond(), 1000),
|
||||
offsetDateTimeUTC[ChronoField.MILLI_OF_SECOND].toLong()
|
||||
)
|
||||
} catch (ex: ArithmeticException) {
|
||||
throw IllegalArgumentException(ex)
|
||||
}
|
||||
return cal
|
||||
}
|
||||
47
app/src/main/java/org/schabi/newpipe/ktx/TextView.kt
Normal file
47
app/src/main/java/org/schabi/newpipe/ktx/TextView.kt
Normal file
@@ -0,0 +1,47 @@
|
||||
@file:JvmName("TextViewUtils")
|
||||
|
||||
package org.schabi.newpipe.ktx
|
||||
|
||||
import android.animation.Animator
|
||||
import android.animation.AnimatorListenerAdapter
|
||||
import android.animation.ArgbEvaluator
|
||||
import android.animation.ValueAnimator
|
||||
import android.util.Log
|
||||
import android.widget.TextView
|
||||
import androidx.annotation.ColorInt
|
||||
import androidx.interpolator.view.animation.FastOutSlowInInterpolator
|
||||
import org.schabi.newpipe.MainActivity
|
||||
|
||||
private const val TAG = "TextViewUtils"
|
||||
|
||||
/**
|
||||
* Animate the text color of any view that extends [TextView] (Buttons, EditText...).
|
||||
*
|
||||
* @param duration the duration of the animation
|
||||
* @param colorStart the text color to start with
|
||||
* @param colorEnd the text color to end with
|
||||
*/
|
||||
fun TextView.animateTextColor(duration: Long, @ColorInt colorStart: Int, @ColorInt colorEnd: Int) {
|
||||
if (MainActivity.DEBUG) {
|
||||
Log.d(
|
||||
TAG,
|
||||
"animateTextColor() called with: " +
|
||||
"view = [" + this + "], duration = [" + duration + "], " +
|
||||
"colorStart = [" + colorStart + "], colorEnd = [" + colorEnd + "]"
|
||||
)
|
||||
}
|
||||
val viewPropertyAnimator = ValueAnimator.ofObject(ArgbEvaluator(), colorStart, colorEnd)
|
||||
viewPropertyAnimator.interpolator = FastOutSlowInInterpolator()
|
||||
viewPropertyAnimator.duration = duration
|
||||
viewPropertyAnimator.addUpdateListener { setTextColor(it.animatedValue as Int) }
|
||||
viewPropertyAnimator.addListener(object : AnimatorListenerAdapter() {
|
||||
override fun onAnimationEnd(animation: Animator) {
|
||||
setTextColor(colorEnd)
|
||||
}
|
||||
|
||||
override fun onAnimationCancel(animation: Animator) {
|
||||
setTextColor(colorEnd)
|
||||
}
|
||||
})
|
||||
viewPropertyAnimator.start()
|
||||
}
|
||||
75
app/src/main/java/org/schabi/newpipe/ktx/Throwable.kt
Normal file
75
app/src/main/java/org/schabi/newpipe/ktx/Throwable.kt
Normal file
@@ -0,0 +1,75 @@
|
||||
@file:JvmName("ExceptionUtils")
|
||||
|
||||
package org.schabi.newpipe.ktx
|
||||
|
||||
import java.io.IOException
|
||||
import java.io.InterruptedIOException
|
||||
|
||||
/**
|
||||
* @return if throwable is related to Interrupted exceptions, or one of its causes is.
|
||||
*/
|
||||
val Throwable.isInterruptedCaused: Boolean
|
||||
get() = hasExactCause(InterruptedIOException::class.java, InterruptedException::class.java)
|
||||
|
||||
/**
|
||||
* @return if throwable is related to network issues, or one of its causes is.
|
||||
*/
|
||||
val Throwable.isNetworkRelated: Boolean
|
||||
get() = hasAssignableCause<IOException>()
|
||||
|
||||
/**
|
||||
* Calls [hasCause] with the `checkSubtypes` parameter set to false.
|
||||
*/
|
||||
fun Throwable.hasExactCause(vararg causesToCheck: Class<*>) = hasCause(false, *causesToCheck)
|
||||
|
||||
/**
|
||||
* Calls [hasCause] with a reified [Throwable] type.
|
||||
*/
|
||||
inline fun <reified T : Throwable> Throwable.hasExactCause() = hasExactCause(T::class.java)
|
||||
|
||||
/**
|
||||
* Calls [hasCause] with the `checkSubtypes` parameter set to true.
|
||||
*/
|
||||
fun Throwable?.hasAssignableCause(vararg causesToCheck: Class<*>) = hasCause(true, *causesToCheck)
|
||||
|
||||
/**
|
||||
* Calls [hasCause] with a reified [Throwable] type.
|
||||
*/
|
||||
inline fun <reified T : Throwable> Throwable?.hasAssignableCause() = hasAssignableCause(T::class.java)
|
||||
|
||||
/**
|
||||
* Check if the throwable has some cause from the causes to check, or is itself in it.
|
||||
*
|
||||
* If `checkIfAssignable` is true, not only the exact type will be considered equals, but also its subtypes.
|
||||
*
|
||||
* @param checkSubtypes if subtypes are also checked.
|
||||
* @param causesToCheck an array of causes to check.
|
||||
*
|
||||
* @see Class.isAssignableFrom
|
||||
*/
|
||||
tailrec fun Throwable?.hasCause(checkSubtypes: Boolean, vararg causesToCheck: Class<*>): Boolean {
|
||||
if (this == null) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check if throwable is a subtype of any of the causes to check
|
||||
causesToCheck.forEach { causeClass ->
|
||||
if (checkSubtypes) {
|
||||
if (causeClass.isAssignableFrom(this.javaClass)) {
|
||||
return true
|
||||
}
|
||||
} else {
|
||||
if (causeClass == this.javaClass) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val currentCause: Throwable? = cause
|
||||
// Check if cause is not pointing to the same instance, to avoid infinite loops.
|
||||
if (this !== currentCause) {
|
||||
return currentCause.hasCause(checkSubtypes, *causesToCheck)
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
334
app/src/main/java/org/schabi/newpipe/ktx/View.kt
Normal file
334
app/src/main/java/org/schabi/newpipe/ktx/View.kt
Normal file
@@ -0,0 +1,334 @@
|
||||
@file:JvmName("ViewUtils")
|
||||
|
||||
package org.schabi.newpipe.ktx
|
||||
|
||||
import android.animation.Animator
|
||||
import android.animation.AnimatorListenerAdapter
|
||||
import android.animation.ArgbEvaluator
|
||||
import android.animation.ValueAnimator
|
||||
import android.content.res.ColorStateList
|
||||
import android.util.Log
|
||||
import android.view.View
|
||||
import androidx.annotation.ColorInt
|
||||
import androidx.annotation.FloatRange
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.isGone
|
||||
import androidx.core.view.isInvisible
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.interpolator.view.animation.FastOutSlowInInterpolator
|
||||
import org.schabi.newpipe.MainActivity
|
||||
|
||||
private const val TAG = "ViewUtils"
|
||||
|
||||
inline var View.backgroundTintListCompat: ColorStateList?
|
||||
get() = ViewCompat.getBackgroundTintList(this)
|
||||
set(value) = ViewCompat.setBackgroundTintList(this, value)
|
||||
|
||||
/**
|
||||
* Animate the view.
|
||||
*
|
||||
* @param enterOrExit true to enter, false to exit
|
||||
* @param duration how long the animation will take, in milliseconds
|
||||
* @param animationType Type of the animation
|
||||
* @param delay how long the animation will wait to start, in milliseconds
|
||||
* @param execOnEnd runnable that will be executed when the animation ends
|
||||
*/
|
||||
@JvmOverloads
|
||||
fun View.animate(
|
||||
enterOrExit: Boolean,
|
||||
duration: Long,
|
||||
animationType: AnimationType = AnimationType.ALPHA,
|
||||
delay: Long = 0,
|
||||
execOnEnd: Runnable? = null
|
||||
) {
|
||||
if (MainActivity.DEBUG) {
|
||||
val id = try {
|
||||
resources.getResourceEntryName(id)
|
||||
} catch (e: Exception) {
|
||||
id.toString()
|
||||
}
|
||||
val msg = String.format(
|
||||
"%8s → [%s:%s] [%s %s:%s] execOnEnd=%s", enterOrExit,
|
||||
javaClass.simpleName, id, animationType, duration, delay, execOnEnd
|
||||
)
|
||||
Log.d(TAG, "animate(): $msg")
|
||||
}
|
||||
if (isVisible && enterOrExit) {
|
||||
if (MainActivity.DEBUG) {
|
||||
Log.d(TAG, "animate(): view was already visible > view = [$this]")
|
||||
}
|
||||
animate().setListener(null).cancel()
|
||||
isVisible = true
|
||||
alpha = 1f
|
||||
execOnEnd?.run()
|
||||
return
|
||||
} else if ((isGone || isInvisible) && !enterOrExit) {
|
||||
if (MainActivity.DEBUG) {
|
||||
Log.d(TAG, "animate(): view was already gone > view = [$this]")
|
||||
}
|
||||
animate().setListener(null).cancel()
|
||||
isGone = true
|
||||
alpha = 0f
|
||||
execOnEnd?.run()
|
||||
return
|
||||
}
|
||||
animate().setListener(null).cancel()
|
||||
isVisible = true
|
||||
when (animationType) {
|
||||
AnimationType.ALPHA -> animateAlpha(enterOrExit, duration, delay, execOnEnd)
|
||||
AnimationType.SCALE_AND_ALPHA -> animateScaleAndAlpha(enterOrExit, duration, delay, execOnEnd)
|
||||
AnimationType.LIGHT_SCALE_AND_ALPHA -> animateLightScaleAndAlpha(enterOrExit, duration, delay, execOnEnd)
|
||||
AnimationType.SLIDE_AND_ALPHA -> animateSlideAndAlpha(enterOrExit, duration, delay, execOnEnd)
|
||||
AnimationType.LIGHT_SLIDE_AND_ALPHA -> animateLightSlideAndAlpha(enterOrExit, duration, delay, execOnEnd)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Animate the background color of a view.
|
||||
*
|
||||
* @param duration the duration of the animation
|
||||
* @param colorStart the background color to start with
|
||||
* @param colorEnd the background color to end with
|
||||
*/
|
||||
fun View.animateBackgroundColor(duration: Long, @ColorInt colorStart: Int, @ColorInt colorEnd: Int) {
|
||||
if (MainActivity.DEBUG) {
|
||||
Log.d(
|
||||
TAG,
|
||||
"animateBackgroundColor() called with: " +
|
||||
"view = [" + this + "], duration = [" + duration + "], " +
|
||||
"colorStart = [" + colorStart + "], colorEnd = [" + colorEnd + "]"
|
||||
)
|
||||
}
|
||||
val empty = arrayOf(IntArray(0))
|
||||
val viewPropertyAnimator = ValueAnimator.ofObject(ArgbEvaluator(), colorStart, colorEnd)
|
||||
viewPropertyAnimator.interpolator = FastOutSlowInInterpolator()
|
||||
viewPropertyAnimator.duration = duration
|
||||
viewPropertyAnimator.addUpdateListener { animation: ValueAnimator ->
|
||||
backgroundTintListCompat = ColorStateList(empty, intArrayOf(animation.animatedValue as Int))
|
||||
}
|
||||
viewPropertyAnimator.addListener(object : AnimatorListenerAdapter() {
|
||||
override fun onAnimationEnd(animation: Animator) {
|
||||
backgroundTintListCompat = ColorStateList(empty, intArrayOf(colorEnd))
|
||||
}
|
||||
|
||||
override fun onAnimationCancel(animation: Animator) {
|
||||
onAnimationEnd(animation)
|
||||
}
|
||||
})
|
||||
viewPropertyAnimator.start()
|
||||
}
|
||||
|
||||
fun View.animateHeight(duration: Long, targetHeight: Int): ValueAnimator {
|
||||
if (MainActivity.DEBUG) {
|
||||
Log.d(
|
||||
TAG,
|
||||
"animateHeight: duration = [" + duration + "], " +
|
||||
"from " + height + " to → " + targetHeight + " in: " + this
|
||||
)
|
||||
}
|
||||
val animator = ValueAnimator.ofFloat(height.toFloat(), targetHeight.toFloat())
|
||||
animator.interpolator = FastOutSlowInInterpolator()
|
||||
animator.duration = duration
|
||||
animator.addUpdateListener { animation: ValueAnimator ->
|
||||
val value = animation.animatedValue as Float
|
||||
layoutParams.height = value.toInt()
|
||||
requestLayout()
|
||||
}
|
||||
animator.addListener(object : AnimatorListenerAdapter() {
|
||||
override fun onAnimationEnd(animation: Animator) {
|
||||
layoutParams.height = targetHeight
|
||||
requestLayout()
|
||||
}
|
||||
|
||||
override fun onAnimationCancel(animation: Animator) {
|
||||
layoutParams.height = targetHeight
|
||||
requestLayout()
|
||||
}
|
||||
})
|
||||
animator.start()
|
||||
return animator
|
||||
}
|
||||
|
||||
fun View.animateRotation(duration: Long, targetRotation: Int) {
|
||||
if (MainActivity.DEBUG) {
|
||||
Log.d(
|
||||
TAG,
|
||||
"animateRotation: duration = [" + duration + "], " +
|
||||
"from " + rotation + " to → " + targetRotation + " in: " + this
|
||||
)
|
||||
}
|
||||
animate().setListener(null).cancel()
|
||||
animate()
|
||||
.rotation(targetRotation.toFloat()).setDuration(duration)
|
||||
.setInterpolator(FastOutSlowInInterpolator())
|
||||
.setListener(object : AnimatorListenerAdapter() {
|
||||
override fun onAnimationCancel(animation: Animator) {
|
||||
rotation = targetRotation.toFloat()
|
||||
}
|
||||
|
||||
override fun onAnimationEnd(animation: Animator) {
|
||||
rotation = targetRotation.toFloat()
|
||||
}
|
||||
}).start()
|
||||
}
|
||||
|
||||
private fun View.animateAlpha(enterOrExit: Boolean, duration: Long, delay: Long, execOnEnd: Runnable?) {
|
||||
if (enterOrExit) {
|
||||
animate().setInterpolator(FastOutSlowInInterpolator()).alpha(1f)
|
||||
.setDuration(duration).setStartDelay(delay)
|
||||
.setListener(object : AnimatorListenerAdapter() {
|
||||
override fun onAnimationEnd(animation: Animator) {
|
||||
execOnEnd?.run()
|
||||
}
|
||||
}).start()
|
||||
} else {
|
||||
animate().setInterpolator(FastOutSlowInInterpolator()).alpha(0f)
|
||||
.setDuration(duration).setStartDelay(delay)
|
||||
.setListener(object : AnimatorListenerAdapter() {
|
||||
override fun onAnimationEnd(animation: Animator) {
|
||||
isGone = true
|
||||
execOnEnd?.run()
|
||||
}
|
||||
}).start()
|
||||
}
|
||||
}
|
||||
|
||||
private fun View.animateScaleAndAlpha(enterOrExit: Boolean, duration: Long, delay: Long, execOnEnd: Runnable?) {
|
||||
if (enterOrExit) {
|
||||
scaleX = .8f
|
||||
scaleY = .8f
|
||||
animate()
|
||||
.setInterpolator(FastOutSlowInInterpolator())
|
||||
.alpha(1f).scaleX(1f).scaleY(1f)
|
||||
.setDuration(duration).setStartDelay(delay)
|
||||
.setListener(object : AnimatorListenerAdapter() {
|
||||
override fun onAnimationEnd(animation: Animator) {
|
||||
execOnEnd?.run()
|
||||
}
|
||||
}).start()
|
||||
} else {
|
||||
scaleX = 1f
|
||||
scaleY = 1f
|
||||
animate()
|
||||
.setInterpolator(FastOutSlowInInterpolator())
|
||||
.alpha(0f).scaleX(.8f).scaleY(.8f)
|
||||
.setDuration(duration).setStartDelay(delay)
|
||||
.setListener(object : AnimatorListenerAdapter() {
|
||||
override fun onAnimationEnd(animation: Animator) {
|
||||
isGone = true
|
||||
execOnEnd?.run()
|
||||
}
|
||||
}).start()
|
||||
}
|
||||
}
|
||||
|
||||
private fun View.animateLightScaleAndAlpha(enterOrExit: Boolean, duration: Long, delay: Long, execOnEnd: Runnable?) {
|
||||
if (enterOrExit) {
|
||||
alpha = .5f
|
||||
scaleX = .95f
|
||||
scaleY = .95f
|
||||
animate()
|
||||
.setInterpolator(FastOutSlowInInterpolator())
|
||||
.alpha(1f).scaleX(1f).scaleY(1f)
|
||||
.setDuration(duration).setStartDelay(delay)
|
||||
.setListener(object : AnimatorListenerAdapter() {
|
||||
override fun onAnimationEnd(animation: Animator) {
|
||||
execOnEnd?.run()
|
||||
}
|
||||
}).start()
|
||||
} else {
|
||||
alpha = 1f
|
||||
scaleX = 1f
|
||||
scaleY = 1f
|
||||
animate()
|
||||
.setInterpolator(FastOutSlowInInterpolator())
|
||||
.alpha(0f).scaleX(.95f).scaleY(.95f)
|
||||
.setDuration(duration).setStartDelay(delay)
|
||||
.setListener(object : AnimatorListenerAdapter() {
|
||||
override fun onAnimationEnd(animation: Animator) {
|
||||
isGone = true
|
||||
execOnEnd?.run()
|
||||
}
|
||||
}).start()
|
||||
}
|
||||
}
|
||||
|
||||
private fun View.animateSlideAndAlpha(enterOrExit: Boolean, duration: Long, delay: Long, execOnEnd: Runnable?) {
|
||||
if (enterOrExit) {
|
||||
translationY = -height.toFloat()
|
||||
alpha = 0f
|
||||
animate()
|
||||
.setInterpolator(FastOutSlowInInterpolator()).alpha(1f).translationY(0f)
|
||||
.setDuration(duration).setStartDelay(delay)
|
||||
.setListener(object : AnimatorListenerAdapter() {
|
||||
override fun onAnimationEnd(animation: Animator) {
|
||||
execOnEnd?.run()
|
||||
}
|
||||
}).start()
|
||||
} else {
|
||||
animate()
|
||||
.setInterpolator(FastOutSlowInInterpolator())
|
||||
.alpha(0f).translationY(-height.toFloat())
|
||||
.setDuration(duration).setStartDelay(delay)
|
||||
.setListener(object : AnimatorListenerAdapter() {
|
||||
override fun onAnimationEnd(animation: Animator) {
|
||||
isGone = true
|
||||
execOnEnd?.run()
|
||||
}
|
||||
}).start()
|
||||
}
|
||||
}
|
||||
|
||||
private fun View.animateLightSlideAndAlpha(enterOrExit: Boolean, duration: Long, delay: Long, execOnEnd: Runnable?) {
|
||||
if (enterOrExit) {
|
||||
translationY = -height / 2.0f
|
||||
alpha = 0f
|
||||
animate()
|
||||
.setInterpolator(FastOutSlowInInterpolator()).alpha(1f).translationY(0f)
|
||||
.setDuration(duration).setStartDelay(delay)
|
||||
.setListener(object : AnimatorListenerAdapter() {
|
||||
override fun onAnimationEnd(animation: Animator) {
|
||||
execOnEnd?.run()
|
||||
}
|
||||
}).start()
|
||||
} else {
|
||||
animate().setInterpolator(FastOutSlowInInterpolator())
|
||||
.alpha(0f).translationY(-height / 2.0f)
|
||||
.setDuration(duration).setStartDelay(delay)
|
||||
.setListener(object : AnimatorListenerAdapter() {
|
||||
override fun onAnimationEnd(animation: Animator) {
|
||||
isGone = true
|
||||
execOnEnd?.run()
|
||||
}
|
||||
}).start()
|
||||
}
|
||||
}
|
||||
|
||||
fun View.slideUp(duration: Long, delay: Long, @FloatRange(from = 0.0, to = 1.0) translationPercent: Float) {
|
||||
val newTranslationY = (resources.displayMetrics.heightPixels * translationPercent).toInt()
|
||||
animate().setListener(null).cancel()
|
||||
alpha = 0f
|
||||
translationY = newTranslationY.toFloat()
|
||||
visibility = View.VISIBLE
|
||||
animate()
|
||||
.alpha(1f)
|
||||
.translationY(0f)
|
||||
.setStartDelay(delay)
|
||||
.setDuration(duration)
|
||||
.setInterpolator(FastOutSlowInInterpolator())
|
||||
.start()
|
||||
}
|
||||
|
||||
/**
|
||||
* Instead of hiding normally using [animate], which would make
|
||||
* the recycler view unable to capture touches after being hidden, this just animates the alpha
|
||||
* value setting it to `0.0` after `200` milliseconds.
|
||||
*/
|
||||
fun View.animateHideRecyclerViewAllowingScrolling() {
|
||||
// not hiding normally because the view needs to still capture touches and allow scroll
|
||||
animate().alpha(0.0f).setDuration(200).start()
|
||||
}
|
||||
|
||||
enum class AnimationType {
|
||||
ALPHA, SCALE_AND_ALPHA, LIGHT_SCALE_AND_ALPHA, SLIDE_AND_ALPHA, LIGHT_SLIDE_AND_ALPHA
|
||||
}
|
||||
@@ -4,23 +4,27 @@ import android.content.SharedPreferences;
|
||||
import android.content.res.Configuration;
|
||||
import android.content.res.Resources;
|
||||
import android.os.Bundle;
|
||||
import androidx.preference.PreferenceManager;
|
||||
import android.util.Log;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.View;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.preference.PreferenceManager;
|
||||
import androidx.recyclerview.widget.GridLayoutManager;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import androidx.viewbinding.ViewBinding;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.databinding.PignateFooterBinding;
|
||||
import org.schabi.newpipe.fragments.BaseStateFragment;
|
||||
import org.schabi.newpipe.fragments.list.ListViewContract;
|
||||
|
||||
import static org.schabi.newpipe.util.AnimationUtils.animateView;
|
||||
import static org.schabi.newpipe.ktx.ViewUtils.animate;
|
||||
import static org.schabi.newpipe.ktx.ViewUtils.animateHideRecyclerViewAllowingScrolling;
|
||||
|
||||
/**
|
||||
* This fragment is design to be used with persistent data such as
|
||||
@@ -42,8 +46,8 @@ public abstract class BaseLocalListFragment<I, N> extends BaseStateFragment<I>
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
private static final int LIST_MODE_UPDATE_FLAG = 0x32;
|
||||
private View headerRootView;
|
||||
private View footerRootView;
|
||||
private ViewBinding headerRootBinding;
|
||||
private ViewBinding footerRootBinding;
|
||||
protected LocalItemListAdapter itemListAdapter;
|
||||
protected RecyclerView itemsList;
|
||||
private int updateFlags = 0;
|
||||
@@ -86,12 +90,13 @@ public abstract class BaseLocalListFragment<I, N> extends BaseStateFragment<I>
|
||||
// Lifecycle - View
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
protected View getListHeader() {
|
||||
@Nullable
|
||||
protected ViewBinding getListHeader() {
|
||||
return null;
|
||||
}
|
||||
|
||||
protected View getListFooter() {
|
||||
return activity.getLayoutInflater().inflate(R.layout.pignate_footer, itemsList, false);
|
||||
protected ViewBinding getListFooter() {
|
||||
return PignateFooterBinding.inflate(activity.getLayoutInflater(), itemsList, false);
|
||||
}
|
||||
|
||||
protected RecyclerView.LayoutManager getGridLayoutManager() {
|
||||
@@ -120,10 +125,12 @@ public abstract class BaseLocalListFragment<I, N> extends BaseStateFragment<I>
|
||||
itemsList.setLayoutManager(useGrid ? getGridLayoutManager() : getListLayoutManager());
|
||||
|
||||
itemListAdapter.setUseGridVariant(useGrid);
|
||||
headerRootView = getListHeader();
|
||||
itemListAdapter.setHeader(headerRootView);
|
||||
footerRootView = getListFooter();
|
||||
itemListAdapter.setFooter(footerRootView);
|
||||
headerRootBinding = getListHeader();
|
||||
if (headerRootBinding != null) {
|
||||
itemListAdapter.setHeader(headerRootBinding.getRoot());
|
||||
}
|
||||
footerRootBinding = getListFooter();
|
||||
itemListAdapter.setFooter(footerRootBinding.getRoot());
|
||||
|
||||
itemsList.setAdapter(itemListAdapter);
|
||||
}
|
||||
@@ -178,10 +185,10 @@ public abstract class BaseLocalListFragment<I, N> extends BaseStateFragment<I>
|
||||
public void showLoading() {
|
||||
super.showLoading();
|
||||
if (itemsList != null) {
|
||||
animateView(itemsList, false, 200);
|
||||
animateHideRecyclerViewAllowingScrolling(itemsList);
|
||||
}
|
||||
if (headerRootView != null) {
|
||||
animateView(headerRootView, false, 200);
|
||||
if (headerRootBinding != null) {
|
||||
animate(headerRootBinding.getRoot(), false, 200);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -189,23 +196,10 @@ public abstract class BaseLocalListFragment<I, N> extends BaseStateFragment<I>
|
||||
public void hideLoading() {
|
||||
super.hideLoading();
|
||||
if (itemsList != null) {
|
||||
animateView(itemsList, true, 200);
|
||||
animate(itemsList, true, 200);
|
||||
}
|
||||
if (headerRootView != null) {
|
||||
animateView(headerRootView, true, 200);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void showError(final String message, final boolean showRetryButton) {
|
||||
super.showError(message, showRetryButton);
|
||||
showListFooter(false);
|
||||
|
||||
if (itemsList != null) {
|
||||
animateView(itemsList, false, 200);
|
||||
}
|
||||
if (headerRootView != null) {
|
||||
animateView(headerRootView, false, 200);
|
||||
if (headerRootBinding != null) {
|
||||
animate(headerRootBinding.getRoot(), true, 200);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -243,9 +237,18 @@ public abstract class BaseLocalListFragment<I, N> extends BaseStateFragment<I>
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean onError(final Throwable exception) {
|
||||
public void handleError() {
|
||||
super.handleError();
|
||||
resetFragment();
|
||||
return super.onError(exception);
|
||||
|
||||
showListFooter(false);
|
||||
|
||||
if (itemsList != null) {
|
||||
animateHideRecyclerViewAllowingScrolling(itemsList);
|
||||
}
|
||||
if (headerRootBinding != null) {
|
||||
animate(headerRootBinding.getRoot(), false, 200);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -23,19 +23,20 @@ import org.schabi.newpipe.database.LocalItem;
|
||||
import org.schabi.newpipe.database.playlist.PlaylistLocalItem;
|
||||
import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry;
|
||||
import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity;
|
||||
import org.schabi.newpipe.error.ErrorInfo;
|
||||
import org.schabi.newpipe.error.UserAction;
|
||||
import org.schabi.newpipe.local.BaseLocalListFragment;
|
||||
import org.schabi.newpipe.local.playlist.LocalPlaylistManager;
|
||||
import org.schabi.newpipe.local.playlist.RemotePlaylistManager;
|
||||
import org.schabi.newpipe.report.UserAction;
|
||||
import org.schabi.newpipe.util.NavigationHelper;
|
||||
import org.schabi.newpipe.util.OnClickGesture;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import icepick.State;
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.rxjava3.core.Flowable;
|
||||
import io.reactivex.rxjava3.core.Single;
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
||||
import io.reactivex.rxjava3.disposables.Disposable;
|
||||
|
||||
@@ -206,7 +207,8 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL
|
||||
|
||||
@Override
|
||||
public void onError(final Throwable exception) {
|
||||
BookmarkFragment.this.onError(exception);
|
||||
showError(new ErrorInfo(exception,
|
||||
UserAction.REQUESTED_BOOKMARK, "Loading playlists"));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -237,17 +239,6 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL
|
||||
// Fragment Error Handling
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@Override
|
||||
protected boolean onError(final Throwable exception) {
|
||||
if (super.onError(exception)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
onUnrecoverableError(exception, UserAction.SOMETHING_ELSE,
|
||||
"none", "Bookmark", R.string.general_error);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void resetFragment() {
|
||||
super.resetFragment();
|
||||
@@ -295,8 +286,10 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL
|
||||
.setPositiveButton(R.string.delete, (dialog, i) ->
|
||||
disposables.add(deleteReactor
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(ignored -> { /*Do nothing on success*/ }, this::onError))
|
||||
)
|
||||
.subscribe(ignored -> { /*Do nothing on success*/ }, throwable ->
|
||||
showError(new ErrorInfo(throwable,
|
||||
UserAction.REQUESTED_BOOKMARK,
|
||||
"Deleting playlist")))))
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.show();
|
||||
}
|
||||
@@ -314,7 +307,10 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL
|
||||
localPlaylistManager.renamePlaylist(id, name);
|
||||
final Disposable disposable = localPlaylistManager.renamePlaylist(id, name)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(longs -> { /*Do nothing on success*/ }, this::onError);
|
||||
.subscribe(longs -> { /*Do nothing on success*/ }, throwable -> showError(
|
||||
new ErrorInfo(throwable,
|
||||
UserAction.REQUESTED_BOOKMARK,
|
||||
"Changing playlist name")));
|
||||
disposables.add(disposable);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,40 +32,33 @@ import androidx.appcompat.app.AlertDialog
|
||||
import androidx.core.content.edit
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.preference.PreferenceManager
|
||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||
import icepick.State
|
||||
import kotlinx.android.synthetic.main.error_retry.error_button_retry
|
||||
import kotlinx.android.synthetic.main.error_retry.error_message_view
|
||||
import kotlinx.android.synthetic.main.fragment_feed.empty_state_view
|
||||
import kotlinx.android.synthetic.main.fragment_feed.error_panel
|
||||
import kotlinx.android.synthetic.main.fragment_feed.items_list
|
||||
import kotlinx.android.synthetic.main.fragment_feed.loading_progress_bar
|
||||
import kotlinx.android.synthetic.main.fragment_feed.loading_progress_text
|
||||
import kotlinx.android.synthetic.main.fragment_feed.refresh_root_view
|
||||
import kotlinx.android.synthetic.main.fragment_feed.refresh_subtitle_text
|
||||
import kotlinx.android.synthetic.main.fragment_feed.refresh_text
|
||||
import org.schabi.newpipe.R
|
||||
import org.schabi.newpipe.database.feed.model.FeedGroupEntity
|
||||
import org.schabi.newpipe.databinding.FragmentFeedBinding
|
||||
import org.schabi.newpipe.error.ErrorInfo
|
||||
import org.schabi.newpipe.error.UserAction
|
||||
import org.schabi.newpipe.fragments.list.BaseListFragment
|
||||
import org.schabi.newpipe.ktx.animate
|
||||
import org.schabi.newpipe.ktx.animateHideRecyclerViewAllowingScrolling
|
||||
import org.schabi.newpipe.local.feed.service.FeedLoadService
|
||||
import org.schabi.newpipe.report.UserAction
|
||||
import org.schabi.newpipe.util.AnimationUtils.animateView
|
||||
import org.schabi.newpipe.util.Localization
|
||||
import java.util.Calendar
|
||||
import java.time.OffsetDateTime
|
||||
|
||||
class FeedFragment : BaseListFragment<FeedState, Unit>() {
|
||||
private var _feedBinding: FragmentFeedBinding? = null
|
||||
private val feedBinding get() = _feedBinding!!
|
||||
|
||||
private lateinit var viewModel: FeedViewModel
|
||||
private lateinit var swipeRefreshLayout: SwipeRefreshLayout
|
||||
@State
|
||||
@JvmField
|
||||
var listState: Parcelable? = null
|
||||
|
||||
private var groupId = FeedGroupEntity.GROUP_ALL_ID
|
||||
private var groupName = ""
|
||||
private var oldestSubscriptionUpdate: Calendar? = null
|
||||
private var oldestSubscriptionUpdate: OffsetDateTime? = null
|
||||
|
||||
init {
|
||||
setHasOptionsMenu(true)
|
||||
@@ -85,16 +78,17 @@ class FeedFragment : BaseListFragment<FeedState, Unit>() {
|
||||
}
|
||||
|
||||
override fun onViewCreated(rootView: View, savedInstanceState: Bundle?) {
|
||||
// super.onViewCreated() calls initListeners() which require the binding to be initialized
|
||||
_feedBinding = FragmentFeedBinding.bind(rootView)
|
||||
super.onViewCreated(rootView, savedInstanceState)
|
||||
swipeRefreshLayout = requireView().findViewById(R.id.swiperefresh)
|
||||
swipeRefreshLayout.setOnRefreshListener { reloadContent() }
|
||||
|
||||
viewModel = ViewModelProvider(this, FeedViewModel.Factory(requireContext(), groupId)).get(FeedViewModel::class.java)
|
||||
viewModel.stateLiveData.observe(viewLifecycleOwner, Observer { it?.let(::handleResult) })
|
||||
viewModel.stateLiveData.observe(viewLifecycleOwner) { it?.let(::handleResult) }
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
listState = items_list?.layoutManager?.onSaveInstanceState()
|
||||
listState = feedBinding.itemsList.layoutManager?.onSaveInstanceState()
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
@@ -112,9 +106,8 @@ class FeedFragment : BaseListFragment<FeedState, Unit>() {
|
||||
|
||||
override fun initListeners() {
|
||||
super.initListeners()
|
||||
refresh_root_view.setOnClickListener {
|
||||
triggerUpdate()
|
||||
}
|
||||
feedBinding.refreshRootView.setOnClickListener { reloadContent() }
|
||||
feedBinding.swipeRefreshLayout.setOnRefreshListener { reloadContent() }
|
||||
}
|
||||
|
||||
// /////////////////////////////////////////////////////////////////////////
|
||||
@@ -169,55 +162,36 @@ class FeedFragment : BaseListFragment<FeedState, Unit>() {
|
||||
activity?.supportActionBar?.subtitle = null
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
_feedBinding = null
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
// /////////////////////////////////////////////////////////////////////////
|
||||
// Handling
|
||||
// /////////////////////////////////////////////////////////////////////////
|
||||
|
||||
override fun showLoading() {
|
||||
animateView(refresh_root_view, false, 0)
|
||||
animateView(items_list, false, 0)
|
||||
|
||||
animateView(loading_progress_bar, true, 200)
|
||||
animateView(loading_progress_text, true, 200)
|
||||
|
||||
empty_state_view?.let { animateView(it, false, 0) }
|
||||
animateView(error_panel, false, 0)
|
||||
super.showLoading()
|
||||
feedBinding.itemsList.animateHideRecyclerViewAllowingScrolling()
|
||||
feedBinding.refreshRootView.animate(false, 0)
|
||||
feedBinding.loadingProgressText.animate(true, 200)
|
||||
feedBinding.swipeRefreshLayout.isRefreshing = true
|
||||
}
|
||||
|
||||
override fun hideLoading() {
|
||||
animateView(refresh_root_view, true, 200)
|
||||
animateView(items_list, true, 300)
|
||||
|
||||
animateView(loading_progress_bar, false, 0)
|
||||
animateView(loading_progress_text, false, 0)
|
||||
|
||||
empty_state_view?.let { animateView(it, false, 0) }
|
||||
animateView(error_panel, false, 0)
|
||||
swipeRefreshLayout.isRefreshing = false
|
||||
super.hideLoading()
|
||||
feedBinding.refreshRootView.animate(true, 200)
|
||||
feedBinding.loadingProgressText.animate(false, 0)
|
||||
feedBinding.swipeRefreshLayout.isRefreshing = false
|
||||
}
|
||||
|
||||
override fun showEmptyState() {
|
||||
animateView(refresh_root_view, true, 200)
|
||||
animateView(items_list, false, 0)
|
||||
|
||||
animateView(loading_progress_bar, false, 0)
|
||||
animateView(loading_progress_text, false, 0)
|
||||
|
||||
empty_state_view?.let { animateView(it, true, 800) }
|
||||
animateView(error_panel, false, 0)
|
||||
}
|
||||
|
||||
override fun showError(message: String, showRetryButton: Boolean) {
|
||||
infoListAdapter.clearStreamItemList()
|
||||
animateView(refresh_root_view, false, 120)
|
||||
animateView(items_list, false, 120)
|
||||
|
||||
animateView(loading_progress_bar, false, 120)
|
||||
animateView(loading_progress_text, false, 120)
|
||||
|
||||
error_message_view.text = message
|
||||
animateView(error_button_retry, showRetryButton, if (showRetryButton) 600 else 0)
|
||||
animateView(error_panel, true, 300)
|
||||
super.showEmptyState()
|
||||
feedBinding.itemsList.animateHideRecyclerViewAllowingScrolling()
|
||||
feedBinding.refreshRootView.animate(true, 200)
|
||||
feedBinding.loadingProgressText.animate(false, 0)
|
||||
feedBinding.swipeRefreshLayout.isRefreshing = false
|
||||
}
|
||||
|
||||
override fun handleResult(result: FeedState) {
|
||||
@@ -230,46 +204,51 @@ class FeedFragment : BaseListFragment<FeedState, Unit>() {
|
||||
updateRefreshViewState()
|
||||
}
|
||||
|
||||
override fun handleError() {
|
||||
super.handleError()
|
||||
infoListAdapter.clearStreamItemList()
|
||||
feedBinding.itemsList.animateHideRecyclerViewAllowingScrolling()
|
||||
feedBinding.refreshRootView.animate(false, 0)
|
||||
feedBinding.loadingProgressText.animate(false, 0)
|
||||
feedBinding.swipeRefreshLayout.isRefreshing = false
|
||||
}
|
||||
|
||||
private fun handleProgressState(progressState: FeedState.ProgressState) {
|
||||
showLoading()
|
||||
|
||||
val isIndeterminate = progressState.currentProgress == -1 &&
|
||||
progressState.maxProgress == -1
|
||||
|
||||
if (!isIndeterminate) {
|
||||
loading_progress_text.text = "${progressState.currentProgress}/${progressState.maxProgress}"
|
||||
feedBinding.loadingProgressText.text = if (!isIndeterminate) {
|
||||
"${progressState.currentProgress}/${progressState.maxProgress}"
|
||||
} else if (progressState.progressMessage > 0) {
|
||||
loading_progress_text?.setText(progressState.progressMessage)
|
||||
getString(progressState.progressMessage)
|
||||
} else {
|
||||
loading_progress_text?.text = "∞/∞"
|
||||
"∞/∞"
|
||||
}
|
||||
|
||||
loading_progress_bar.isIndeterminate = isIndeterminate ||
|
||||
feedBinding.loadingProgressBar.isIndeterminate = isIndeterminate ||
|
||||
(progressState.maxProgress > 0 && progressState.currentProgress == 0)
|
||||
loading_progress_bar.progress = progressState.currentProgress
|
||||
feedBinding.loadingProgressBar.progress = progressState.currentProgress
|
||||
|
||||
loading_progress_bar.max = progressState.maxProgress
|
||||
feedBinding.loadingProgressBar.max = progressState.maxProgress
|
||||
}
|
||||
|
||||
private fun handleLoadedState(loadedState: FeedState.LoadedState) {
|
||||
infoListAdapter.setInfoItemList(loadedState.items)
|
||||
listState?.run {
|
||||
items_list.layoutManager?.onRestoreInstanceState(listState)
|
||||
feedBinding.itemsList.layoutManager?.onRestoreInstanceState(listState)
|
||||
listState = null
|
||||
}
|
||||
|
||||
oldestSubscriptionUpdate = loadedState.oldestUpdate
|
||||
|
||||
val loadedCount = loadedState.notLoadedCount > 0
|
||||
refresh_subtitle_text.isVisible = loadedCount
|
||||
feedBinding.refreshSubtitleText.isVisible = loadedCount
|
||||
if (loadedCount) {
|
||||
refresh_subtitle_text.text = getString(R.string.feed_subscription_not_loaded_count, loadedState.notLoadedCount)
|
||||
}
|
||||
|
||||
if (loadedState.itemsErrors.isNotEmpty()) {
|
||||
showSnackBarError(
|
||||
loadedState.itemsErrors, UserAction.REQUESTED_FEED,
|
||||
"none", "Loading feed", R.string.general_error
|
||||
feedBinding.refreshSubtitleText.text = getString(
|
||||
R.string.feed_subscription_not_loaded_count,
|
||||
loadedState.notLoadedCount
|
||||
)
|
||||
}
|
||||
|
||||
@@ -281,12 +260,13 @@ class FeedFragment : BaseListFragment<FeedState, Unit>() {
|
||||
}
|
||||
|
||||
private fun handleErrorState(errorState: FeedState.ErrorState): Boolean {
|
||||
hideLoading()
|
||||
errorState.error?.let {
|
||||
onError(errorState.error)
|
||||
return true
|
||||
return if (errorState.error == null) {
|
||||
hideLoading()
|
||||
false
|
||||
} else {
|
||||
showError(ErrorInfo(errorState.error, UserAction.REQUESTED_FEED, "Loading feed"))
|
||||
true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private fun updateRelativeTimeViews() {
|
||||
@@ -295,12 +275,10 @@ class FeedFragment : BaseListFragment<FeedState, Unit>() {
|
||||
}
|
||||
|
||||
private fun updateRefreshViewState() {
|
||||
val oldestSubscriptionUpdateText = when {
|
||||
oldestSubscriptionUpdate != null -> Localization.relativeTime(oldestSubscriptionUpdate!!)
|
||||
else -> "—"
|
||||
}
|
||||
|
||||
refresh_text?.text = getString(R.string.feed_oldest_subscription_update, oldestSubscriptionUpdateText)
|
||||
feedBinding.refreshText.text = getString(
|
||||
R.string.feed_oldest_subscription_update,
|
||||
oldestSubscriptionUpdate?.let { Localization.relativeTime(it) } ?: "—"
|
||||
)
|
||||
}
|
||||
|
||||
// /////////////////////////////////////////////////////////////////////////
|
||||
@@ -308,11 +286,10 @@ class FeedFragment : BaseListFragment<FeedState, Unit>() {
|
||||
// /////////////////////////////////////////////////////////////////////////
|
||||
|
||||
override fun doInitialLoadLogic() {}
|
||||
override fun reloadContent() = triggerUpdate()
|
||||
override fun loadMoreItems() {}
|
||||
override fun hasMoreItems() = false
|
||||
|
||||
private fun triggerUpdate() {
|
||||
override fun reloadContent() {
|
||||
getActivity()?.startService(
|
||||
Intent(requireContext(), FeedLoadService::class.java).apply {
|
||||
putExtra(FeedLoadService.EXTRA_GROUP_ID, groupId)
|
||||
@@ -321,18 +298,6 @@ class FeedFragment : BaseListFragment<FeedState, Unit>() {
|
||||
listState = null
|
||||
}
|
||||
|
||||
override fun onError(exception: Throwable): Boolean {
|
||||
if (super.onError(exception)) return true
|
||||
|
||||
if (useAsFrontPage) {
|
||||
showSnackBarError(exception, UserAction.REQUESTED_FEED, "none", "Loading Feed", 0)
|
||||
return true
|
||||
}
|
||||
|
||||
onUnrecoverableError(exception, UserAction.REQUESTED_FEED, "none", "Loading Feed", 0)
|
||||
return true
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val KEY_GROUP_ID = "ARG_GROUP_ID"
|
||||
const val KEY_GROUP_NAME = "ARG_GROUP_NAME"
|
||||
|
||||
@@ -2,7 +2,7 @@ package org.schabi.newpipe.local.feed
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem
|
||||
import java.util.Calendar
|
||||
import java.time.OffsetDateTime
|
||||
|
||||
sealed class FeedState {
|
||||
data class ProgressState(
|
||||
@@ -13,7 +13,7 @@ sealed class FeedState {
|
||||
|
||||
data class LoadedState(
|
||||
val items: List<StreamInfoItem>,
|
||||
val oldestUpdate: Calendar? = null,
|
||||
val oldestUpdate: OffsetDateTime? = null,
|
||||
val notLoadedCount: Long,
|
||||
val itemsErrors: List<Throwable> = emptyList()
|
||||
) : FeedState()
|
||||
|
||||
@@ -11,7 +11,6 @@ import io.reactivex.rxjava3.functions.Function4
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import org.schabi.newpipe.database.feed.model.FeedGroupEntity
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem
|
||||
import org.schabi.newpipe.ktx.toCalendar
|
||||
import org.schabi.newpipe.local.feed.service.FeedEventManager
|
||||
import org.schabi.newpipe.local.feed.service.FeedEventManager.Event.ErrorResultEvent
|
||||
import org.schabi.newpipe.local.feed.service.FeedEventManager.Event.IdleEvent
|
||||
@@ -48,13 +47,11 @@ class FeedViewModel(applicationContext: Context, val groupId: Long = FeedGroupEn
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe { (event, listFromDB, notLoadedCount, oldestUpdate) ->
|
||||
val oldestUpdateCalendar = oldestUpdate?.toCalendar()
|
||||
|
||||
mutableStateLiveData.postValue(
|
||||
when (event) {
|
||||
is IdleEvent -> FeedState.LoadedState(listFromDB, oldestUpdateCalendar, notLoadedCount)
|
||||
is IdleEvent -> FeedState.LoadedState(listFromDB, oldestUpdate, notLoadedCount)
|
||||
is ProgressEvent -> FeedState.ProgressState(event.currentProgress, event.maxProgress, event.progressMessage)
|
||||
is SuccessResultEvent -> FeedState.LoadedState(listFromDB, oldestUpdateCalendar, notLoadedCount, event.itemsErrors)
|
||||
is SuccessResultEvent -> FeedState.LoadedState(listFromDB, oldestUpdate, notLoadedCount, event.itemsErrors)
|
||||
is ErrorResultEvent -> FeedState.ErrorState(event.error)
|
||||
}
|
||||
)
|
||||
|
||||
@@ -30,6 +30,7 @@ import android.os.IBinder
|
||||
import android.util.Log
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import androidx.core.app.ServiceCompat
|
||||
import androidx.preference.PreferenceManager
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.core.Flowable
|
||||
@@ -42,19 +43,20 @@ import io.reactivex.rxjava3.processors.PublishProcessor
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import org.reactivestreams.Subscriber
|
||||
import org.reactivestreams.Subscription
|
||||
import org.schabi.newpipe.App
|
||||
import org.schabi.newpipe.MainActivity.DEBUG
|
||||
import org.schabi.newpipe.R
|
||||
import org.schabi.newpipe.database.feed.model.FeedGroupEntity
|
||||
import org.schabi.newpipe.extractor.ListInfo
|
||||
import org.schabi.newpipe.extractor.exceptions.ReCaptchaException
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem
|
||||
import org.schabi.newpipe.ktx.isNetworkRelated
|
||||
import org.schabi.newpipe.local.feed.FeedDatabaseManager
|
||||
import org.schabi.newpipe.local.feed.service.FeedEventManager.Event.ErrorResultEvent
|
||||
import org.schabi.newpipe.local.feed.service.FeedEventManager.Event.ProgressEvent
|
||||
import org.schabi.newpipe.local.feed.service.FeedEventManager.Event.SuccessResultEvent
|
||||
import org.schabi.newpipe.local.feed.service.FeedEventManager.postEvent
|
||||
import org.schabi.newpipe.local.subscription.SubscriptionManager
|
||||
import org.schabi.newpipe.util.ExceptionUtils
|
||||
import org.schabi.newpipe.util.ExtractorHelper
|
||||
import java.io.IOException
|
||||
import java.time.OffsetDateTime
|
||||
@@ -67,7 +69,7 @@ class FeedLoadService : Service() {
|
||||
companion object {
|
||||
private val TAG = FeedLoadService::class.java.simpleName
|
||||
private const val NOTIFICATION_ID = 7293450
|
||||
private const val ACTION_CANCEL = "org.schabi.newpipe.local.feed.service.FeedLoadService.CANCEL"
|
||||
private const val ACTION_CANCEL = App.PACKAGE_NAME + ".local.feed.service.FeedLoadService.CANCEL"
|
||||
|
||||
/**
|
||||
* How often the notification will be updated.
|
||||
@@ -147,7 +149,7 @@ class FeedLoadService : Service() {
|
||||
|
||||
private fun stopService() {
|
||||
disposeAll()
|
||||
stopForeground(true)
|
||||
ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE)
|
||||
notificationManager.cancel(NOTIFICATION_ID)
|
||||
stopSelf()
|
||||
}
|
||||
@@ -342,7 +344,7 @@ class FeedLoadService : Service() {
|
||||
|
||||
error is IOException -> throw error
|
||||
cause is IOException -> throw cause
|
||||
ExceptionUtils.isNetworkRelated(error) -> throw IOException(error)
|
||||
error.isNetworkRelated -> throw IOException(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,13 +10,11 @@ import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.viewbinding.ViewBinding;
|
||||
|
||||
import com.google.android.material.snackbar.Snackbar;
|
||||
|
||||
@@ -26,6 +24,10 @@ import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.database.LocalItem;
|
||||
import org.schabi.newpipe.database.stream.StreamStatisticsEntry;
|
||||
import org.schabi.newpipe.database.stream.model.StreamEntity;
|
||||
import org.schabi.newpipe.databinding.PlaylistControlBinding;
|
||||
import org.schabi.newpipe.databinding.StatisticPlaylistControlBinding;
|
||||
import org.schabi.newpipe.error.ErrorInfo;
|
||||
import org.schabi.newpipe.error.UserAction;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||
import org.schabi.newpipe.extractor.stream.StreamType;
|
||||
import org.schabi.newpipe.info_list.InfoItemDialog;
|
||||
@@ -33,10 +35,8 @@ import org.schabi.newpipe.local.BaseLocalListFragment;
|
||||
import org.schabi.newpipe.player.helper.PlayerHolder;
|
||||
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
||||
import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
|
||||
import org.schabi.newpipe.report.ErrorActivity;
|
||||
import org.schabi.newpipe.report.ErrorInfo;
|
||||
import org.schabi.newpipe.report.UserAction;
|
||||
import org.schabi.newpipe.settings.SettingsActivity;
|
||||
import org.schabi.newpipe.settings.HistorySettingsFragment;
|
||||
import org.schabi.newpipe.util.KoreUtil;
|
||||
import org.schabi.newpipe.util.NavigationHelper;
|
||||
import org.schabi.newpipe.util.OnClickGesture;
|
||||
import org.schabi.newpipe.util.StreamDialogEntry;
|
||||
@@ -47,6 +47,7 @@ import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
import icepick.State;
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||
@@ -59,13 +60,10 @@ public class StatisticsPlaylistFragment
|
||||
@State
|
||||
Parcelable itemsListState;
|
||||
private StatisticSortMode sortMode = StatisticSortMode.LAST_PLAYED;
|
||||
private View headerPlayAllButton;
|
||||
private View headerPopupButton;
|
||||
private View headerBackgroundButton;
|
||||
private View playlistCtrl;
|
||||
private View sortButton;
|
||||
private ImageView sortButtonIcon;
|
||||
private TextView sortButtonText;
|
||||
|
||||
private StatisticPlaylistControlBinding headerBinding;
|
||||
private PlaylistControlBinding playlistControlBinding;
|
||||
|
||||
/* Used for independent events */
|
||||
private Subscription databaseSubscription;
|
||||
private HistoryRecordManager recordManager;
|
||||
@@ -130,17 +128,12 @@ public class StatisticsPlaylistFragment
|
||||
}
|
||||
|
||||
@Override
|
||||
protected View getListHeader() {
|
||||
final View headerRootLayout = activity.getLayoutInflater()
|
||||
.inflate(R.layout.statistic_playlist_control, itemsList, false);
|
||||
playlistCtrl = headerRootLayout.findViewById(R.id.playlist_control);
|
||||
headerPlayAllButton = headerRootLayout.findViewById(R.id.playlist_ctrl_play_all_button);
|
||||
headerPopupButton = headerRootLayout.findViewById(R.id.playlist_ctrl_play_popup_button);
|
||||
headerBackgroundButton = headerRootLayout.findViewById(R.id.playlist_ctrl_play_bg_button);
|
||||
sortButton = headerRootLayout.findViewById(R.id.sortButton);
|
||||
sortButtonIcon = headerRootLayout.findViewById(R.id.sortButtonIcon);
|
||||
sortButtonText = headerRootLayout.findViewById(R.id.sortButtonText);
|
||||
return headerRootLayout;
|
||||
protected ViewBinding getListHeader() {
|
||||
headerBinding = StatisticPlaylistControlBinding.inflate(activity.getLayoutInflater(),
|
||||
itemsList, false);
|
||||
playlistControlBinding = headerBinding.playlistControl;
|
||||
|
||||
return headerBinding;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -169,48 +162,11 @@ public class StatisticsPlaylistFragment
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(final MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case R.id.action_history_clear:
|
||||
new AlertDialog.Builder(activity)
|
||||
.setTitle(R.string.delete_view_history_alert)
|
||||
.setNegativeButton(R.string.cancel, ((dialog, which) -> dialog.dismiss()))
|
||||
.setPositiveButton(R.string.delete, ((dialog, which) -> {
|
||||
final Disposable onDelete = recordManager.deleteWholeStreamHistory()
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
howManyDeleted -> Toast.makeText(getContext(),
|
||||
R.string.watch_history_deleted,
|
||||
Toast.LENGTH_SHORT).show(),
|
||||
throwable -> ErrorActivity.reportError(getContext(),
|
||||
throwable,
|
||||
SettingsActivity.class, null,
|
||||
ErrorInfo.make(
|
||||
UserAction.DELETE_FROM_HISTORY,
|
||||
"none",
|
||||
"Delete view history",
|
||||
R.string.general_error)));
|
||||
|
||||
final Disposable onClearOrphans = recordManager.removeOrphanedRecords()
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
howManyDeleted -> {
|
||||
},
|
||||
throwable -> ErrorActivity.reportError(getContext(),
|
||||
throwable,
|
||||
SettingsActivity.class, null,
|
||||
ErrorInfo.make(
|
||||
UserAction.DELETE_FROM_HISTORY,
|
||||
"none",
|
||||
"Delete search history",
|
||||
R.string.general_error)));
|
||||
disposables.add(onClearOrphans);
|
||||
disposables.add(onDelete);
|
||||
}))
|
||||
.create()
|
||||
.show();
|
||||
break;
|
||||
default:
|
||||
return super.onOptionsItemSelected(item);
|
||||
if (item.getItemId() == R.id.action_history_clear) {
|
||||
HistorySettingsFragment
|
||||
.openDeleteWatchHistoryDialog(requireContext(), recordManager, disposables);
|
||||
} else {
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -234,7 +190,7 @@ public class StatisticsPlaylistFragment
|
||||
@Override
|
||||
public void onPause() {
|
||||
super.onPause();
|
||||
itemsListState = itemsList.getLayoutManager().onSaveInstanceState();
|
||||
itemsListState = Objects.requireNonNull(itemsList.getLayoutManager()).onSaveInstanceState();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -244,14 +200,13 @@ public class StatisticsPlaylistFragment
|
||||
if (itemListAdapter != null) {
|
||||
itemListAdapter.unsetSelectedListener();
|
||||
}
|
||||
if (headerBackgroundButton != null) {
|
||||
headerBackgroundButton.setOnClickListener(null);
|
||||
}
|
||||
if (headerPlayAllButton != null) {
|
||||
headerPlayAllButton.setOnClickListener(null);
|
||||
}
|
||||
if (headerPopupButton != null) {
|
||||
headerPopupButton.setOnClickListener(null);
|
||||
if (playlistControlBinding != null) {
|
||||
playlistControlBinding.playlistCtrlPlayBgButton.setOnClickListener(null);
|
||||
playlistControlBinding.playlistCtrlPlayAllButton.setOnClickListener(null);
|
||||
playlistControlBinding.playlistCtrlPlayPopupButton.setOnClickListener(null);
|
||||
|
||||
headerBinding = null;
|
||||
playlistControlBinding = null;
|
||||
}
|
||||
|
||||
if (databaseSubscription != null) {
|
||||
@@ -294,7 +249,8 @@ public class StatisticsPlaylistFragment
|
||||
|
||||
@Override
|
||||
public void onError(final Throwable exception) {
|
||||
StatisticsPlaylistFragment.this.onError(exception);
|
||||
showError(
|
||||
new ErrorInfo(exception, UserAction.SOMETHING_ELSE, "History Statistics"));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -310,7 +266,7 @@ public class StatisticsPlaylistFragment
|
||||
return;
|
||||
}
|
||||
|
||||
playlistCtrl.setVisibility(View.VISIBLE);
|
||||
playlistControlBinding.getRoot().setVisibility(View.VISIBLE);
|
||||
|
||||
itemListAdapter.clearStreamItemList();
|
||||
|
||||
@@ -320,18 +276,18 @@ public class StatisticsPlaylistFragment
|
||||
}
|
||||
|
||||
itemListAdapter.addItems(processResult(result));
|
||||
if (itemsListState != null) {
|
||||
if (itemsListState != null && itemsList.getLayoutManager() != null) {
|
||||
itemsList.getLayoutManager().onRestoreInstanceState(itemsListState);
|
||||
itemsListState = null;
|
||||
}
|
||||
|
||||
headerPlayAllButton.setOnClickListener(view ->
|
||||
playlistControlBinding.playlistCtrlPlayAllButton.setOnClickListener(view ->
|
||||
NavigationHelper.playOnMainPlayer(activity, getPlayQueue()));
|
||||
headerPopupButton.setOnClickListener(view ->
|
||||
playlistControlBinding.playlistCtrlPlayPopupButton.setOnClickListener(view ->
|
||||
NavigationHelper.playOnPopupPlayer(activity, getPlayQueue(), false));
|
||||
headerBackgroundButton.setOnClickListener(view ->
|
||||
playlistControlBinding.playlistCtrlPlayBgButton.setOnClickListener(view ->
|
||||
NavigationHelper.playOnBackgroundPlayer(activity, getPlayQueue(), false));
|
||||
sortButton.setOnClickListener(view -> toggleSortMode());
|
||||
headerBinding.sortButton.setOnClickListener(view -> toggleSortMode());
|
||||
|
||||
hideLoading();
|
||||
}
|
||||
@@ -348,17 +304,6 @@ public class StatisticsPlaylistFragment
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean onError(final Throwable exception) {
|
||||
if (super.onError(exception)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
onUnrecoverableError(exception, UserAction.SOMETHING_ELSE,
|
||||
"none", "History Statistics", R.string.general_error);
|
||||
return true;
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Utils
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
@@ -367,15 +312,15 @@ public class StatisticsPlaylistFragment
|
||||
if (sortMode == StatisticSortMode.LAST_PLAYED) {
|
||||
sortMode = StatisticSortMode.MOST_PLAYED;
|
||||
setTitle(getString(R.string.title_most_played));
|
||||
sortButtonIcon.setImageResource(
|
||||
headerBinding.sortButtonIcon.setImageResource(
|
||||
ThemeHelper.resolveResourceIdFromAttr(requireContext(), R.attr.ic_history));
|
||||
sortButtonText.setText(R.string.title_last_played);
|
||||
headerBinding.sortButtonText.setText(R.string.title_last_played);
|
||||
} else {
|
||||
sortMode = StatisticSortMode.LAST_PLAYED;
|
||||
setTitle(getString(R.string.title_last_played));
|
||||
sortButtonIcon.setImageResource(
|
||||
headerBinding.sortButtonIcon.setImageResource(
|
||||
ThemeHelper.resolveResourceIdFromAttr(requireContext(), R.attr.ic_filter_list));
|
||||
sortButtonText.setText(R.string.title_most_played);
|
||||
headerBinding.sortButtonText.setText(R.string.title_most_played);
|
||||
}
|
||||
startLoading(true);
|
||||
}
|
||||
@@ -413,6 +358,9 @@ public class StatisticsPlaylistFragment
|
||||
StreamDialogEntry.share
|
||||
));
|
||||
}
|
||||
if (KoreUtil.shouldShowPlayWithKodi(context, infoItem.getServiceId())) {
|
||||
entries.add(StreamDialogEntry.play_with_kodi);
|
||||
}
|
||||
StreamDialogEntry.setEnabledEntries(entries);
|
||||
|
||||
StreamDialogEntry.start_here_on_background.setCustomAction((fragment, infoItemDuplicate) ->
|
||||
@@ -443,9 +391,8 @@ public class StatisticsPlaylistFragment
|
||||
Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
},
|
||||
throwable -> showSnackBarError(throwable,
|
||||
UserAction.DELETE_FROM_HISTORY, "none",
|
||||
"Deleting item failed", R.string.general_error));
|
||||
throwable -> showSnackBarError(new ErrorInfo(throwable,
|
||||
UserAction.DELETE_FROM_HISTORY, "Deleting item")));
|
||||
|
||||
disposables.add(onDelete);
|
||||
}
|
||||
|
||||
@@ -12,9 +12,9 @@ import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.database.LocalItem;
|
||||
import org.schabi.newpipe.database.playlist.PlaylistStreamEntry;
|
||||
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.AnimationUtils;
|
||||
import org.schabi.newpipe.util.ImageDisplayConstants;
|
||||
import org.schabi.newpipe.util.Localization;
|
||||
import org.schabi.newpipe.views.AnimatedProgressBar;
|
||||
@@ -117,10 +117,10 @@ public class LocalPlaylistStreamItemHolder extends LocalItemHolder {
|
||||
} else {
|
||||
itemProgressView.setProgress((int) TimeUnit.MILLISECONDS
|
||||
.toSeconds(item.getProgressTime()));
|
||||
AnimationUtils.animateView(itemProgressView, true, 500);
|
||||
ViewUtils.animate(itemProgressView, true, 500);
|
||||
}
|
||||
} else if (itemProgressView.getVisibility() == View.VISIBLE) {
|
||||
AnimationUtils.animateView(itemProgressView, false, 500);
|
||||
ViewUtils.animate(itemProgressView, false, 500);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -12,9 +12,9 @@ import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.database.LocalItem;
|
||||
import org.schabi.newpipe.database.stream.StreamStatisticsEntry;
|
||||
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.AnimationUtils;
|
||||
import org.schabi.newpipe.util.ImageDisplayConstants;
|
||||
import org.schabi.newpipe.util.Localization;
|
||||
import org.schabi.newpipe.views.AnimatedProgressBar;
|
||||
@@ -148,10 +148,10 @@ public class LocalStatisticStreamItemHolder extends LocalItemHolder {
|
||||
} else {
|
||||
itemProgressView.setProgress((int) TimeUnit.MILLISECONDS
|
||||
.toSeconds(item.getProgressTime()));
|
||||
AnimationUtils.animateView(itemProgressView, true, 500);
|
||||
ViewUtils.animate(itemProgressView, true, 500);
|
||||
}
|
||||
} else if (itemProgressView.getVisibility() == View.VISIBLE) {
|
||||
AnimationUtils.animateView(itemProgressView, false, 500);
|
||||
ViewUtils.animate(itemProgressView, false, 500);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,6 @@ import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.EditText;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
@@ -22,6 +21,7 @@ import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.recyclerview.widget.ItemTouchHelper;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import androidx.viewbinding.ViewBinding;
|
||||
|
||||
import org.reactivestreams.Subscriber;
|
||||
import org.reactivestreams.Subscription;
|
||||
@@ -32,6 +32,10 @@ import org.schabi.newpipe.database.history.model.StreamHistoryEntry;
|
||||
import org.schabi.newpipe.database.playlist.PlaylistStreamEntry;
|
||||
import org.schabi.newpipe.database.stream.model.StreamEntity;
|
||||
import org.schabi.newpipe.database.stream.model.StreamStateEntity;
|
||||
import org.schabi.newpipe.databinding.LocalPlaylistHeaderBinding;
|
||||
import org.schabi.newpipe.databinding.PlaylistControlBinding;
|
||||
import org.schabi.newpipe.error.ErrorInfo;
|
||||
import org.schabi.newpipe.error.UserAction;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||
import org.schabi.newpipe.extractor.stream.StreamType;
|
||||
import org.schabi.newpipe.info_list.InfoItemDialog;
|
||||
@@ -40,7 +44,7 @@ import org.schabi.newpipe.local.history.HistoryRecordManager;
|
||||
import org.schabi.newpipe.player.helper.PlayerHolder;
|
||||
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
||||
import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
|
||||
import org.schabi.newpipe.report.UserAction;
|
||||
import org.schabi.newpipe.util.KoreUtil;
|
||||
import org.schabi.newpipe.util.Localization;
|
||||
import org.schabi.newpipe.util.NavigationHelper;
|
||||
import org.schabi.newpipe.util.OnClickGesture;
|
||||
@@ -55,14 +59,14 @@ import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
import icepick.State;
|
||||
import io.reactivex.rxjava3.core.Flowable;
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.rxjava3.core.Flowable;
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
||||
import io.reactivex.rxjava3.disposables.Disposable;
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers;
|
||||
import io.reactivex.rxjava3.subjects.PublishSubject;
|
||||
|
||||
import static org.schabi.newpipe.util.AnimationUtils.animateView;
|
||||
import static org.schabi.newpipe.ktx.ViewUtils.animate;
|
||||
|
||||
public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistStreamEntry>, Void> {
|
||||
// Save the list 10 seconds after the last change occurred
|
||||
@@ -76,13 +80,8 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
|
||||
@State
|
||||
Parcelable itemsListState;
|
||||
|
||||
private View headerRootLayout;
|
||||
private TextView headerTitleView;
|
||||
private TextView headerStreamCount;
|
||||
private View playlistControl;
|
||||
private View headerPlayAllButton;
|
||||
private View headerPopupButton;
|
||||
private View headerBackgroundButton;
|
||||
private LocalPlaylistHeaderBinding headerBinding;
|
||||
private PlaylistControlBinding playlistControlBinding;
|
||||
|
||||
private ItemTouchHelper itemTouchHelper;
|
||||
|
||||
@@ -112,7 +111,7 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
|
||||
@Override
|
||||
public void onCreate(final Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
playlistManager = new LocalPlaylistManager(NewPipeDatabase.getInstance(getContext()));
|
||||
playlistManager = new LocalPlaylistManager(NewPipeDatabase.getInstance(requireContext()));
|
||||
debouncedSaveSignal = PublishSubject.create();
|
||||
|
||||
disposables = new CompositeDisposable();
|
||||
@@ -136,8 +135,8 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
|
||||
public void setTitle(final String title) {
|
||||
super.setTitle(title);
|
||||
|
||||
if (headerTitleView != null) {
|
||||
headerTitleView.setText(title);
|
||||
if (headerBinding != null) {
|
||||
headerBinding.playlistTitleView.setText(title);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -148,28 +147,21 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
|
||||
}
|
||||
|
||||
@Override
|
||||
protected View getListHeader() {
|
||||
headerRootLayout = activity.getLayoutInflater()
|
||||
.inflate(R.layout.local_playlist_header, itemsList, false);
|
||||
protected ViewBinding getListHeader() {
|
||||
headerBinding = LocalPlaylistHeaderBinding.inflate(activity.getLayoutInflater(), itemsList,
|
||||
false);
|
||||
playlistControlBinding = headerBinding.playlistControl;
|
||||
|
||||
headerTitleView = headerRootLayout.findViewById(R.id.playlist_title_view);
|
||||
headerTitleView.setSelected(true);
|
||||
headerBinding.playlistTitleView.setSelected(true);
|
||||
|
||||
headerStreamCount = headerRootLayout.findViewById(R.id.playlist_stream_count);
|
||||
|
||||
playlistControl = headerRootLayout.findViewById(R.id.playlist_control);
|
||||
headerPlayAllButton = headerRootLayout.findViewById(R.id.playlist_ctrl_play_all_button);
|
||||
headerPopupButton = headerRootLayout.findViewById(R.id.playlist_ctrl_play_popup_button);
|
||||
headerBackgroundButton = headerRootLayout.findViewById(R.id.playlist_ctrl_play_bg_button);
|
||||
|
||||
return headerRootLayout;
|
||||
return headerBinding;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void initListeners() {
|
||||
super.initListeners();
|
||||
|
||||
headerTitleView.setOnClickListener(view -> createRenameDialog());
|
||||
headerBinding.playlistTitleView.setOnClickListener(view -> createRenameDialog());
|
||||
|
||||
itemTouchHelper = new ItemTouchHelper(getItemTouchCallback());
|
||||
itemTouchHelper.attachToRecyclerView(itemsList);
|
||||
@@ -209,22 +201,18 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
|
||||
@Override
|
||||
public void showLoading() {
|
||||
super.showLoading();
|
||||
if (headerRootLayout != null) {
|
||||
animateView(headerRootLayout, false, 200);
|
||||
}
|
||||
if (playlistControl != null) {
|
||||
animateView(playlistControl, false, 200);
|
||||
if (headerBinding != null) {
|
||||
animate(headerBinding.getRoot(), false, 200);
|
||||
animate(playlistControlBinding.getRoot(), false, 200);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void hideLoading() {
|
||||
super.hideLoading();
|
||||
if (headerRootLayout != null) {
|
||||
animateView(headerRootLayout, true, 200);
|
||||
}
|
||||
if (playlistControl != null) {
|
||||
animateView(playlistControl, true, 200);
|
||||
if (headerBinding != null) {
|
||||
animate(headerBinding.getRoot(), true, 200);
|
||||
animate(playlistControlBinding.getRoot(), true, 200);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -276,14 +264,13 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
|
||||
if (itemListAdapter != null) {
|
||||
itemListAdapter.unsetSelectedListener();
|
||||
}
|
||||
if (headerBackgroundButton != null) {
|
||||
headerBackgroundButton.setOnClickListener(null);
|
||||
}
|
||||
if (headerPlayAllButton != null) {
|
||||
headerPlayAllButton.setOnClickListener(null);
|
||||
}
|
||||
if (headerPopupButton != null) {
|
||||
headerPopupButton.setOnClickListener(null);
|
||||
if (playlistControlBinding != null) {
|
||||
playlistControlBinding.playlistCtrlPlayBgButton.setOnClickListener(null);
|
||||
playlistControlBinding.playlistCtrlPlayAllButton.setOnClickListener(null);
|
||||
playlistControlBinding.playlistCtrlPlayPopupButton.setOnClickListener(null);
|
||||
|
||||
headerBinding = null;
|
||||
playlistControlBinding = null;
|
||||
}
|
||||
|
||||
if (databaseSubscription != null) {
|
||||
@@ -348,7 +335,8 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
|
||||
|
||||
@Override
|
||||
public void onError(final Throwable exception) {
|
||||
LocalPlaylistFragment.this.onError(exception);
|
||||
showError(new ErrorInfo(exception, UserAction.REQUESTED_BOOKMARK,
|
||||
"Loading local playlist"));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -358,25 +346,23 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(final MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case R.id.menu_item_remove_watched:
|
||||
if (!isRemovingWatched) {
|
||||
new AlertDialog.Builder(requireContext())
|
||||
.setMessage(R.string.remove_watched_popup_warning)
|
||||
.setTitle(R.string.remove_watched_popup_title)
|
||||
.setPositiveButton(R.string.yes,
|
||||
(DialogInterface d, int id) -> removeWatchedStreams(false))
|
||||
.setNeutralButton(
|
||||
R.string.remove_watched_popup_yes_and_partially_watched_videos,
|
||||
(DialogInterface d, int id) -> removeWatchedStreams(true))
|
||||
.setNegativeButton(R.string.cancel,
|
||||
(DialogInterface d, int id) -> d.cancel())
|
||||
.create()
|
||||
.show();
|
||||
}
|
||||
break;
|
||||
default:
|
||||
return super.onOptionsItemSelected(item);
|
||||
if (item.getItemId() == R.id.menu_item_remove_watched) {
|
||||
if (!isRemovingWatched) {
|
||||
new AlertDialog.Builder(requireContext())
|
||||
.setMessage(R.string.remove_watched_popup_warning)
|
||||
.setTitle(R.string.remove_watched_popup_title)
|
||||
.setPositiveButton(R.string.yes,
|
||||
(DialogInterface d, int id) -> removeWatchedStreams(false))
|
||||
.setNeutralButton(
|
||||
R.string.remove_watched_popup_yes_and_partially_watched_videos,
|
||||
(DialogInterface d, int id) -> removeWatchedStreams(true))
|
||||
.setNegativeButton(R.string.cancel,
|
||||
(DialogInterface d, int id) -> d.cancel())
|
||||
.create()
|
||||
.show();
|
||||
}
|
||||
} else {
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -469,7 +455,8 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
|
||||
|
||||
hideLoading();
|
||||
isRemovingWatched = false;
|
||||
}, this::onError));
|
||||
}, throwable -> showError(new ErrorInfo(throwable, UserAction.REQUESTED_BOOKMARK,
|
||||
"Removing watched videos, partially watched=" + removePartiallyWatched))));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -493,19 +480,19 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
|
||||
}
|
||||
setVideoCount(itemListAdapter.getItemsList().size());
|
||||
|
||||
headerPlayAllButton.setOnClickListener(view ->
|
||||
playlistControlBinding.playlistCtrlPlayAllButton.setOnClickListener(view ->
|
||||
NavigationHelper.playOnMainPlayer(activity, getPlayQueue()));
|
||||
headerPopupButton.setOnClickListener(view ->
|
||||
playlistControlBinding.playlistCtrlPlayPopupButton.setOnClickListener(view ->
|
||||
NavigationHelper.playOnPopupPlayer(activity, getPlayQueue(), false));
|
||||
headerBackgroundButton.setOnClickListener(view ->
|
||||
playlistControlBinding.playlistCtrlPlayBgButton.setOnClickListener(view ->
|
||||
NavigationHelper.playOnBackgroundPlayer(activity, getPlayQueue(), false));
|
||||
|
||||
headerPopupButton.setOnLongClickListener(view -> {
|
||||
playlistControlBinding.playlistCtrlPlayPopupButton.setOnLongClickListener(view -> {
|
||||
NavigationHelper.enqueueOnPopupPlayer(activity, getPlayQueue(), true);
|
||||
return true;
|
||||
});
|
||||
|
||||
headerBackgroundButton.setOnLongClickListener(view -> {
|
||||
playlistControlBinding.playlistCtrlPlayBgButton.setOnLongClickListener(view -> {
|
||||
NavigationHelper.enqueueOnBackgroundPlayer(activity, getPlayQueue(), true);
|
||||
return true;
|
||||
});
|
||||
@@ -525,17 +512,6 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean onError(final Throwable exception) {
|
||||
if (super.onError(exception)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
onUnrecoverableError(exception, UserAction.SOMETHING_ELSE,
|
||||
"none", "Local Playlist", R.string.general_error);
|
||||
return true;
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Playlist Metadata/Streams Manipulation
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
@@ -576,7 +552,9 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
|
||||
|
||||
final Disposable disposable = playlistManager.renamePlaylist(playlistId, title)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(longs -> { /*Do nothing on success*/ }, this::onError);
|
||||
.subscribe(longs -> { /*Do nothing on success*/ }, throwable ->
|
||||
showError(new ErrorInfo(throwable, UserAction.REQUESTED_BOOKMARK,
|
||||
"Renaming playlist")));
|
||||
disposables.add(disposable);
|
||||
}
|
||||
|
||||
@@ -597,7 +575,9 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
|
||||
final Disposable disposable = playlistManager
|
||||
.changePlaylistThumbnail(playlistId, thumbnailUrl)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(ignore -> successToast.show(), this::onError);
|
||||
.subscribe(ignore -> successToast.show(), throwable ->
|
||||
showError(new ErrorInfo(throwable, UserAction.REQUESTED_BOOKMARK,
|
||||
"Changing playlist thumbnail")));
|
||||
disposables.add(disposable);
|
||||
}
|
||||
|
||||
@@ -646,7 +626,9 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
|
||||
return debouncedSaveSignal
|
||||
.debounce(SAVE_DEBOUNCE_MILLIS, TimeUnit.MILLISECONDS)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(ignored -> saveImmediate(), this::onError);
|
||||
.subscribe(ignored -> saveImmediate(), throwable ->
|
||||
showError(new ErrorInfo(throwable, UserAction.SOMETHING_ELSE,
|
||||
"Debounced saver")));
|
||||
}
|
||||
|
||||
private void saveImmediate() {
|
||||
@@ -683,7 +665,8 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
|
||||
isModified.set(false);
|
||||
}
|
||||
},
|
||||
this::onError
|
||||
throwable -> showError(new ErrorInfo(throwable,
|
||||
UserAction.REQUESTED_BOOKMARK, "Saving playlist"))
|
||||
);
|
||||
disposables.add(disposable);
|
||||
}
|
||||
@@ -697,7 +680,7 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
|
||||
return new ItemTouchHelper.SimpleCallback(directions,
|
||||
ItemTouchHelper.ACTION_STATE_IDLE) {
|
||||
@Override
|
||||
public int interpolateOutOfBoundsScroll(final RecyclerView recyclerView,
|
||||
public int interpolateOutOfBoundsScroll(@NonNull final RecyclerView recyclerView,
|
||||
final int viewSize,
|
||||
final int viewSizeOutOfBounds,
|
||||
final int totalSize,
|
||||
@@ -710,9 +693,9 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onMove(final RecyclerView recyclerView,
|
||||
final RecyclerView.ViewHolder source,
|
||||
final RecyclerView.ViewHolder target) {
|
||||
public boolean onMove(@NonNull final RecyclerView recyclerView,
|
||||
@NonNull final RecyclerView.ViewHolder source,
|
||||
@NonNull final RecyclerView.ViewHolder target) {
|
||||
if (source.getItemViewType() != target.getItemViewType()
|
||||
|| itemListAdapter == null) {
|
||||
return false;
|
||||
@@ -738,7 +721,8 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSwiped(final RecyclerView.ViewHolder viewHolder, final int swipeDir) { }
|
||||
public void onSwiped(@NonNull final RecyclerView.ViewHolder viewHolder,
|
||||
final int swipeDir) { }
|
||||
};
|
||||
}
|
||||
|
||||
@@ -781,6 +765,9 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
|
||||
StreamDialogEntry.share
|
||||
));
|
||||
}
|
||||
if (KoreUtil.shouldShowPlayWithKodi(context, infoItem.getServiceId())) {
|
||||
entries.add(StreamDialogEntry.play_with_kodi);
|
||||
}
|
||||
StreamDialogEntry.setEnabledEntries(entries);
|
||||
|
||||
StreamDialogEntry.start_here_on_background.setCustomAction((fragment, infoItemDuplicate) ->
|
||||
@@ -802,8 +789,9 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
|
||||
}
|
||||
|
||||
private void setVideoCount(final long count) {
|
||||
if (activity != null && headerStreamCount != null) {
|
||||
headerStreamCount.setText(Localization.localizeStreamCount(activity, count));
|
||||
if (activity != null && headerBinding != null) {
|
||||
headerBinding.playlistStreamCount.setText(Localization
|
||||
.localizeStreamCount(activity, count));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -26,16 +26,19 @@ import com.xwray.groupie.Group
|
||||
import com.xwray.groupie.GroupAdapter
|
||||
import com.xwray.groupie.Item
|
||||
import com.xwray.groupie.Section
|
||||
import com.xwray.groupie.kotlinandroidextensions.GroupieViewHolder
|
||||
import com.xwray.groupie.viewbinding.GroupieViewHolder
|
||||
import icepick.State
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||
import kotlinx.android.synthetic.main.dialog_title.view.itemAdditionalDetails
|
||||
import kotlinx.android.synthetic.main.dialog_title.view.itemTitleView
|
||||
import kotlinx.android.synthetic.main.fragment_subscription.items_list
|
||||
import org.schabi.newpipe.R
|
||||
import org.schabi.newpipe.database.feed.model.FeedGroupEntity
|
||||
import org.schabi.newpipe.databinding.DialogTitleBinding
|
||||
import org.schabi.newpipe.databinding.FeedItemCarouselBinding
|
||||
import org.schabi.newpipe.databinding.FragmentSubscriptionBinding
|
||||
import org.schabi.newpipe.error.ErrorInfo
|
||||
import org.schabi.newpipe.error.UserAction
|
||||
import org.schabi.newpipe.extractor.channel.ChannelInfoItem
|
||||
import org.schabi.newpipe.fragments.BaseStateFragment
|
||||
import org.schabi.newpipe.ktx.animate
|
||||
import org.schabi.newpipe.local.subscription.SubscriptionViewModel.SubscriptionState
|
||||
import org.schabi.newpipe.local.subscription.dialog.FeedGroupDialog
|
||||
import org.schabi.newpipe.local.subscription.dialog.FeedGroupReorderDialog
|
||||
@@ -55,8 +58,6 @@ import org.schabi.newpipe.local.subscription.services.SubscriptionsImportService
|
||||
import org.schabi.newpipe.local.subscription.services.SubscriptionsImportService.KEY_MODE
|
||||
import org.schabi.newpipe.local.subscription.services.SubscriptionsImportService.KEY_VALUE
|
||||
import org.schabi.newpipe.local.subscription.services.SubscriptionsImportService.PREVIOUS_EXPORT_MODE
|
||||
import org.schabi.newpipe.report.UserAction
|
||||
import org.schabi.newpipe.util.AnimationUtils.animateView
|
||||
import org.schabi.newpipe.util.FilePickerActivityHelper
|
||||
import org.schabi.newpipe.util.NavigationHelper
|
||||
import org.schabi.newpipe.util.OnClickGesture
|
||||
@@ -70,13 +71,16 @@ import kotlin.math.floor
|
||||
import kotlin.math.max
|
||||
|
||||
class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
|
||||
private var _binding: FragmentSubscriptionBinding? = null
|
||||
private val binding get() = _binding!!
|
||||
|
||||
private lateinit var viewModel: SubscriptionViewModel
|
||||
private lateinit var subscriptionManager: SubscriptionManager
|
||||
private val disposables: CompositeDisposable = CompositeDisposable()
|
||||
|
||||
private var subscriptionBroadcastReceiver: BroadcastReceiver? = null
|
||||
|
||||
private val groupAdapter = GroupAdapter<GroupieViewHolder>()
|
||||
private val groupAdapter = GroupAdapter<GroupieViewHolder<FeedItemCarouselBinding>>()
|
||||
private val feedGroupsSection = Section()
|
||||
private var feedGroupsCarousel: FeedGroupCarouselItem? = null
|
||||
private lateinit var importExportItem: FeedImportExportItem
|
||||
@@ -129,7 +133,7 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
itemsListState = items_list.layoutManager?.onSaveInstanceState()
|
||||
itemsListState = binding.itemsList.layoutManager?.onSaveInstanceState()
|
||||
feedGroupsListState = feedGroupsCarousel?.onSaveInstanceState()
|
||||
importExportItemExpandedState = importExportItem.isExpanded
|
||||
|
||||
@@ -169,7 +173,7 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
|
||||
filters.addAction(IMPORT_COMPLETE_ACTION)
|
||||
subscriptionBroadcastReceiver = object : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
items_list?.post {
|
||||
_binding?.itemsList?.post {
|
||||
importExportItem.isExpanded = false
|
||||
importExportItem.notifyChanged(FeedImportExportItem.REFRESH_EXPANDED_STATUS)
|
||||
}
|
||||
@@ -231,7 +235,7 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
|
||||
|
||||
private fun setupInitialLayout() {
|
||||
Section().apply {
|
||||
val carouselAdapter = GroupAdapter<GroupieViewHolder>()
|
||||
val carouselAdapter = GroupAdapter<GroupieViewHolder<FeedItemCarouselBinding>>()
|
||||
|
||||
carouselAdapter.add(FeedGroupCardItem(-1, getString(R.string.all), FeedGroupIcon.RSS))
|
||||
carouselAdapter.add(feedGroupsSection)
|
||||
@@ -275,39 +279,37 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
|
||||
|
||||
override fun initViews(rootView: View, savedInstanceState: Bundle?) {
|
||||
super.initViews(rootView, savedInstanceState)
|
||||
_binding = FragmentSubscriptionBinding.bind(rootView)
|
||||
|
||||
val shouldUseGridLayout = shouldUseGridLayout()
|
||||
groupAdapter.spanCount = if (shouldUseGridLayout) getGridSpanCount() else 1
|
||||
items_list.layoutManager = GridLayoutManager(requireContext(), groupAdapter.spanCount).apply {
|
||||
binding.itemsList.layoutManager = GridLayoutManager(requireContext(), groupAdapter.spanCount).apply {
|
||||
spanSizeLookup = groupAdapter.spanSizeLookup
|
||||
}
|
||||
items_list.adapter = groupAdapter
|
||||
binding.itemsList.adapter = groupAdapter
|
||||
|
||||
viewModel = ViewModelProvider(this).get(SubscriptionViewModel::class.java)
|
||||
viewModel.stateLiveData.observe(viewLifecycleOwner, androidx.lifecycle.Observer { it?.let(this::handleResult) })
|
||||
viewModel.feedGroupsLiveData.observe(viewLifecycleOwner, androidx.lifecycle.Observer { it?.let(this::handleFeedGroups) })
|
||||
viewModel.stateLiveData.observe(viewLifecycleOwner) { it?.let(this::handleResult) }
|
||||
viewModel.feedGroupsLiveData.observe(viewLifecycleOwner) { it?.let(this::handleFeedGroups) }
|
||||
}
|
||||
|
||||
private fun showLongTapDialog(selectedItem: ChannelInfoItem) {
|
||||
val commands = arrayOf(
|
||||
getString(R.string.share),
|
||||
getString(R.string.unsubscribe)
|
||||
)
|
||||
val commands = arrayOf(getString(R.string.share), getString(R.string.unsubscribe))
|
||||
|
||||
val actions = DialogInterface.OnClickListener { _, i ->
|
||||
when (i) {
|
||||
0 -> ShareUtils.shareUrl(requireContext(), selectedItem.name, selectedItem.url)
|
||||
0 -> ShareUtils.shareText(requireContext(), selectedItem.name, selectedItem.url)
|
||||
1 -> deleteChannel(selectedItem)
|
||||
}
|
||||
}
|
||||
|
||||
val bannerView = View.inflate(requireContext(), R.layout.dialog_title, null)
|
||||
bannerView.isSelected = true
|
||||
bannerView.itemTitleView.text = selectedItem.name
|
||||
bannerView.itemAdditionalDetails.visibility = View.GONE
|
||||
val dialogTitleBinding = DialogTitleBinding.inflate(LayoutInflater.from(requireContext()))
|
||||
dialogTitleBinding.root.isSelected = true
|
||||
dialogTitleBinding.itemTitleView.text = selectedItem.name
|
||||
dialogTitleBinding.itemAdditionalDetails.visibility = View.GONE
|
||||
|
||||
AlertDialog.Builder(requireContext())
|
||||
.setCustomTitle(bannerView)
|
||||
.setCustomTitle(dialogTitleBinding.root)
|
||||
.setItems(commands, actions)
|
||||
.create()
|
||||
.show()
|
||||
@@ -368,19 +370,21 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
|
||||
subscriptionsSection.setHideWhenEmpty(false)
|
||||
|
||||
if (result.subscriptions.isEmpty() && importExportItemExpandedState == null) {
|
||||
items_list.post {
|
||||
binding.itemsList.post {
|
||||
importExportItem.isExpanded = true
|
||||
importExportItem.notifyChanged(FeedImportExportItem.REFRESH_EXPANDED_STATUS)
|
||||
}
|
||||
}
|
||||
|
||||
if (itemsListState != null) {
|
||||
items_list.layoutManager?.onRestoreInstanceState(itemsListState)
|
||||
binding.itemsList.layoutManager?.onRestoreInstanceState(itemsListState)
|
||||
itemsListState = null
|
||||
}
|
||||
}
|
||||
is SubscriptionState.ErrorState -> {
|
||||
result.error?.let { onError(result.error) }
|
||||
result.error?.let {
|
||||
showError(ErrorInfo(result.error, UserAction.SOMETHING_ELSE, "Subscriptions"))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -394,7 +398,7 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
|
||||
}
|
||||
|
||||
feedGroupsSortMenuItem.showMenuItem = groups.size > 1
|
||||
items_list.post { feedGroupsSortMenuItem.notifyChanged(PAYLOAD_UPDATE_VISIBILITY_MENU_ITEM) }
|
||||
binding.itemsList.post { feedGroupsSortMenuItem.notifyChanged(PAYLOAD_UPDATE_VISIBILITY_MENU_ITEM) }
|
||||
}
|
||||
|
||||
// /////////////////////////////////////////////////////////////////////////
|
||||
@@ -403,23 +407,12 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
|
||||
|
||||
override fun showLoading() {
|
||||
super.showLoading()
|
||||
animateView(items_list, false, 100)
|
||||
binding.itemsList.animate(false, 100)
|
||||
}
|
||||
|
||||
override fun hideLoading() {
|
||||
super.hideLoading()
|
||||
animateView(items_list, true, 200)
|
||||
}
|
||||
|
||||
// /////////////////////////////////////////////////////////////////////////
|
||||
// Fragment Error Handling
|
||||
// /////////////////////////////////////////////////////////////////////////
|
||||
|
||||
override fun onError(exception: Throwable): Boolean {
|
||||
if (super.onError(exception)) return true
|
||||
|
||||
onUnrecoverableError(exception, UserAction.SOMETHING_ELSE, "none", "Subscriptions", R.string.general_error)
|
||||
return true
|
||||
binding.itemsList.animate(true, 200)
|
||||
}
|
||||
|
||||
// /////////////////////////////////////////////////////////////////////////
|
||||
@@ -435,11 +428,8 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
|
||||
return when (listMode) {
|
||||
getString(R.string.list_view_mode_auto_key) -> {
|
||||
val configuration = resources.configuration
|
||||
|
||||
(
|
||||
configuration.orientation == Configuration.ORIENTATION_LANDSCAPE &&
|
||||
configuration.isLayoutSizeAtLeast(Configuration.SCREENLAYOUT_SIZE_LARGE)
|
||||
)
|
||||
configuration.orientation == Configuration.ORIENTATION_LANDSCAPE &&
|
||||
configuration.isLayoutSizeAtLeast(Configuration.SCREENLAYOUT_SIZE_LARGE)
|
||||
}
|
||||
getString(R.string.list_view_mode_grid_key) -> true
|
||||
else -> false
|
||||
|
||||
@@ -22,13 +22,13 @@ import com.nononsenseapps.filepicker.Utils;
|
||||
|
||||
import org.schabi.newpipe.BaseFragment;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.error.ErrorActivity;
|
||||
import org.schabi.newpipe.error.ErrorInfo;
|
||||
import org.schabi.newpipe.error.UserAction;
|
||||
import org.schabi.newpipe.extractor.NewPipe;
|
||||
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||
import org.schabi.newpipe.extractor.subscription.SubscriptionExtractor;
|
||||
import org.schabi.newpipe.local.subscription.services.SubscriptionsImportService;
|
||||
import org.schabi.newpipe.report.ErrorActivity;
|
||||
import org.schabi.newpipe.report.ErrorInfo;
|
||||
import org.schabi.newpipe.report.UserAction;
|
||||
import org.schabi.newpipe.util.Constants;
|
||||
import org.schabi.newpipe.util.FilePickerActivityHelper;
|
||||
import org.schabi.newpipe.util.ServiceHelper;
|
||||
@@ -84,10 +84,12 @@ public class SubscriptionsImportFragment extends BaseFragment {
|
||||
|
||||
setupServiceVariables();
|
||||
if (supportedSources.isEmpty() && currentServiceId != Constants.NO_SERVICE_ID) {
|
||||
ErrorActivity.reportError(activity, Collections.emptyList(), null, null,
|
||||
ErrorInfo.make(UserAction.SOMETHING_ELSE,
|
||||
ErrorActivity.reportErrorInSnackbar(activity,
|
||||
new ErrorInfo(new String[]{}, UserAction.SUBSCRIPTION_IMPORT_EXPORT,
|
||||
NewPipe.getNameOfService(currentServiceId),
|
||||
"Service don't support importing", R.string.general_error));
|
||||
"Service does not support importing subscriptions",
|
||||
R.string.general_error,
|
||||
null));
|
||||
activity.finish();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,15 +19,15 @@ 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.OnItemClickListener
|
||||
import com.xwray.groupie.Section
|
||||
import com.xwray.groupie.kotlinandroidextensions.GroupieViewHolder
|
||||
import icepick.Icepick
|
||||
import icepick.State
|
||||
import kotlinx.android.synthetic.main.dialog_feed_group_create.*
|
||||
import kotlinx.android.synthetic.main.toolbar_search_layout.*
|
||||
import org.schabi.newpipe.R
|
||||
import org.schabi.newpipe.database.feed.model.FeedGroupEntity
|
||||
import org.schabi.newpipe.databinding.DialogFeedGroupCreateBinding
|
||||
import org.schabi.newpipe.databinding.ToolbarSearchLayoutBinding
|
||||
import org.schabi.newpipe.fragments.BackPressable
|
||||
import org.schabi.newpipe.local.subscription.FeedGroupIcon
|
||||
import org.schabi.newpipe.local.subscription.dialog.FeedGroupDialog.ScreenState.DeleteScreen
|
||||
@@ -42,9 +42,14 @@ import org.schabi.newpipe.local.subscription.item.PickerSubscriptionItem
|
||||
import org.schabi.newpipe.util.DeviceUtils
|
||||
import org.schabi.newpipe.util.ThemeHelper
|
||||
import java.io.Serializable
|
||||
import kotlin.collections.contains
|
||||
|
||||
class FeedGroupDialog : DialogFragment(), BackPressable {
|
||||
private var _feedGroupCreateBinding: DialogFeedGroupCreateBinding? = null
|
||||
private val feedGroupCreateBinding get() = _feedGroupCreateBinding!!
|
||||
|
||||
private var _searchLayoutBinding: ToolbarSearchLayoutBinding? = null
|
||||
private val searchLayoutBinding get() = _searchLayoutBinding!!
|
||||
|
||||
private lateinit var viewModel: FeedGroupDialogViewModel
|
||||
private var groupId: Long = NO_GROUP_SELECTED
|
||||
private var groupIcon: FeedGroupIcon? = null
|
||||
@@ -107,14 +112,16 @@ class FeedGroupDialog : DialogFragment(), BackPressable {
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
super.onSaveInstanceState(outState)
|
||||
|
||||
iconsListState = icon_selector.layoutManager?.onSaveInstanceState()
|
||||
subscriptionsListState = subscriptions_selector_list.layoutManager?.onSaveInstanceState()
|
||||
iconsListState = feedGroupCreateBinding.iconSelector.layoutManager?.onSaveInstanceState()
|
||||
subscriptionsListState = feedGroupCreateBinding.subscriptionsSelectorList.layoutManager?.onSaveInstanceState()
|
||||
|
||||
Icepick.saveInstanceState(this, outState)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
_feedGroupCreateBinding = DialogFeedGroupCreateBinding.bind(view)
|
||||
_searchLayoutBinding = feedGroupCreateBinding.subscriptionsHeaderSearchContainer
|
||||
|
||||
viewModel = ViewModelProvider(
|
||||
this,
|
||||
@@ -146,7 +153,7 @@ class FeedGroupDialog : DialogFragment(), BackPressable {
|
||||
add(subscriptionEmptyFooter)
|
||||
spanCount = 4
|
||||
}
|
||||
subscriptions_selector_list.apply {
|
||||
feedGroupCreateBinding.subscriptionsSelectorList.apply {
|
||||
// Disable animations, too distracting.
|
||||
itemAnimator = null
|
||||
adapter = subscriptionGroupAdapter
|
||||
@@ -172,8 +179,11 @@ class FeedGroupDialog : DialogFragment(), BackPressable {
|
||||
|
||||
override fun onDestroyView() {
|
||||
super.onDestroyView()
|
||||
subscriptions_selector_list?.adapter = null
|
||||
icon_selector?.adapter = null
|
||||
feedGroupCreateBinding.subscriptionsSelectorList.adapter = null
|
||||
feedGroupCreateBinding.iconSelector.adapter = null
|
||||
|
||||
_feedGroupCreateBinding = null
|
||||
_searchLayoutBinding = null
|
||||
}
|
||||
|
||||
/*///////////////////////////////////////////////////////////////////////////
|
||||
@@ -193,30 +203,30 @@ class FeedGroupDialog : DialogFragment(), BackPressable {
|
||||
}
|
||||
|
||||
private fun setupListeners() {
|
||||
delete_button.setOnClickListener { showScreen(DeleteScreen) }
|
||||
feedGroupCreateBinding.deleteButton.setOnClickListener { showScreen(DeleteScreen) }
|
||||
|
||||
cancel_button.setOnClickListener {
|
||||
feedGroupCreateBinding.cancelButton.setOnClickListener {
|
||||
when (currentScreen) {
|
||||
InitialScreen -> dismiss()
|
||||
else -> showScreen(InitialScreen)
|
||||
}
|
||||
}
|
||||
|
||||
group_name_input_container.error = null
|
||||
group_name_input.doOnTextChanged { text, _, _, _ ->
|
||||
if (group_name_input_container.isErrorEnabled && !text.isNullOrBlank()) {
|
||||
group_name_input_container.error = null
|
||||
feedGroupCreateBinding.groupNameInputContainer.error = null
|
||||
feedGroupCreateBinding.groupNameInput.doOnTextChanged { text, _, _, _ ->
|
||||
if (feedGroupCreateBinding.groupNameInputContainer.isErrorEnabled && !text.isNullOrBlank()) {
|
||||
feedGroupCreateBinding.groupNameInputContainer.error = null
|
||||
}
|
||||
}
|
||||
|
||||
confirm_button.setOnClickListener { handlePositiveButton() }
|
||||
feedGroupCreateBinding.confirmButton.setOnClickListener { handlePositiveButton() }
|
||||
|
||||
select_channel_button.setOnClickListener {
|
||||
subscriptions_selector_list.scrollToPosition(0)
|
||||
feedGroupCreateBinding.selectChannelButton.setOnClickListener {
|
||||
feedGroupCreateBinding.subscriptionsSelectorList.scrollToPosition(0)
|
||||
showScreen(SubscriptionsPickerScreen)
|
||||
}
|
||||
|
||||
val headerMenu = subscriptions_header_toolbar.menu
|
||||
val headerMenu = feedGroupCreateBinding.subscriptionsHeaderToolbar.menu
|
||||
requireActivity().menuInflater.inflate(R.menu.menu_feed_group_dialog, headerMenu)
|
||||
|
||||
headerMenu.findItem(R.id.action_search).setOnMenuItemClickListener {
|
||||
@@ -234,8 +244,8 @@ class FeedGroupDialog : DialogFragment(), BackPressable {
|
||||
}
|
||||
}
|
||||
|
||||
toolbar_search_clear.setOnClickListener {
|
||||
if (toolbar_search_edit_text.text.isEmpty()) {
|
||||
searchLayoutBinding.toolbarSearchClear.setOnClickListener {
|
||||
if (searchLayoutBinding.toolbarSearchEditText.text.isNullOrEmpty()) {
|
||||
hideSearch()
|
||||
return@setOnClickListener
|
||||
}
|
||||
@@ -243,14 +253,14 @@ class FeedGroupDialog : DialogFragment(), BackPressable {
|
||||
showKeyboardSearch()
|
||||
}
|
||||
|
||||
toolbar_search_edit_text.setOnClickListener {
|
||||
searchLayoutBinding.toolbarSearchEditText.setOnClickListener {
|
||||
if (DeviceUtils.isTv(context)) {
|
||||
showKeyboardSearch()
|
||||
}
|
||||
}
|
||||
|
||||
toolbar_search_edit_text.doOnTextChanged { _, _, _, _ ->
|
||||
val newQuery: String = toolbar_search_edit_text.text.toString()
|
||||
searchLayoutBinding.toolbarSearchEditText.doOnTextChanged { _, _, _, _ ->
|
||||
val newQuery: String = searchLayoutBinding.toolbarSearchEditText.text.toString()
|
||||
subscriptionsCurrentSearchQuery = newQuery
|
||||
viewModel.filterSubscriptionsBy(newQuery)
|
||||
}
|
||||
@@ -266,16 +276,16 @@ class FeedGroupDialog : DialogFragment(), BackPressable {
|
||||
}
|
||||
|
||||
private fun handlePositiveButtonInitialScreen() {
|
||||
val name = group_name_input.text.toString().trim()
|
||||
val name = feedGroupCreateBinding.groupNameInput.text.toString().trim()
|
||||
val icon = selectedIcon ?: groupIcon ?: FeedGroupIcon.ALL
|
||||
|
||||
if (name.isBlank()) {
|
||||
group_name_input_container.error = getString(R.string.feed_group_dialog_empty_name)
|
||||
group_name_input.text = null
|
||||
group_name_input.requestFocus()
|
||||
feedGroupCreateBinding.groupNameInputContainer.error = getString(R.string.feed_group_dialog_empty_name)
|
||||
feedGroupCreateBinding.groupNameInput.text = null
|
||||
feedGroupCreateBinding.groupNameInput.requestFocus()
|
||||
return
|
||||
} else {
|
||||
group_name_input_container.error = null
|
||||
feedGroupCreateBinding.groupNameInputContainer.error = null
|
||||
}
|
||||
|
||||
if (selectedSubscriptions.isEmpty()) {
|
||||
@@ -296,10 +306,10 @@ class FeedGroupDialog : DialogFragment(), BackPressable {
|
||||
groupSortOrder = feedGroupEntity?.sortOrder ?: -1
|
||||
|
||||
val feedGroupIcon = if (selectedIcon == null) icon else selectedIcon!!
|
||||
icon_preview.setImageResource(feedGroupIcon.getDrawableRes(requireContext()))
|
||||
feedGroupCreateBinding.iconPreview.setImageResource(feedGroupIcon.getDrawableRes(requireContext()))
|
||||
|
||||
if (group_name_input.text.isNullOrBlank()) {
|
||||
group_name_input.setText(name)
|
||||
if (feedGroupCreateBinding.groupNameInput.text.isNullOrBlank()) {
|
||||
feedGroupCreateBinding.groupNameInput.setText(name)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -346,10 +356,10 @@ class FeedGroupDialog : DialogFragment(), BackPressable {
|
||||
subscriptionMainSection.update(subscriptions, false)
|
||||
|
||||
if (subscriptionsListState != null) {
|
||||
subscriptions_selector_list.layoutManager?.onRestoreInstanceState(subscriptionsListState)
|
||||
feedGroupCreateBinding.subscriptionsSelectorList.layoutManager?.onRestoreInstanceState(subscriptionsListState)
|
||||
subscriptionsListState = null
|
||||
} else {
|
||||
subscriptions_selector_list.scrollToPosition(0)
|
||||
feedGroupCreateBinding.subscriptionsSelectorList.scrollToPosition(0)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -359,15 +369,15 @@ class FeedGroupDialog : DialogFragment(), BackPressable {
|
||||
R.plurals.feed_group_dialog_selection_count,
|
||||
selectedCount, selectedCount
|
||||
)
|
||||
selected_subscription_count_view.text = selectedCountText
|
||||
subscriptions_header_info.text = selectedCountText
|
||||
feedGroupCreateBinding.selectedSubscriptionCountView.text = selectedCountText
|
||||
feedGroupCreateBinding.subscriptionsHeaderInfo.text = selectedCountText
|
||||
}
|
||||
|
||||
private fun setupIconPicker() {
|
||||
val groupAdapter = GroupAdapter<GroupieViewHolder>()
|
||||
groupAdapter.addAll(FeedGroupIcon.values().map { PickerIconItem(requireContext(), it) })
|
||||
|
||||
icon_selector.apply {
|
||||
feedGroupCreateBinding.iconSelector.apply {
|
||||
layoutManager = GridLayoutManager(requireContext(), 7, RecyclerView.VERTICAL, false)
|
||||
adapter = groupAdapter
|
||||
|
||||
@@ -381,20 +391,20 @@ class FeedGroupDialog : DialogFragment(), BackPressable {
|
||||
when (item) {
|
||||
is PickerIconItem -> {
|
||||
selectedIcon = item.icon
|
||||
icon_preview.setImageResource(item.iconRes)
|
||||
feedGroupCreateBinding.iconPreview.setImageResource(item.iconRes)
|
||||
|
||||
showScreen(InitialScreen)
|
||||
}
|
||||
}
|
||||
}
|
||||
icon_preview.setOnClickListener {
|
||||
icon_selector.scrollToPosition(0)
|
||||
feedGroupCreateBinding.iconPreview.setOnClickListener {
|
||||
feedGroupCreateBinding.iconSelector.scrollToPosition(0)
|
||||
showScreen(IconPickerScreen)
|
||||
}
|
||||
|
||||
if (groupId == NO_GROUP_SELECTED) {
|
||||
val icon = selectedIcon ?: FeedGroupIcon.ALL
|
||||
icon_preview.setImageResource(icon.getDrawableRes(requireContext()))
|
||||
feedGroupCreateBinding.iconPreview.setImageResource(icon.getDrawableRes(requireContext()))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -405,22 +415,22 @@ class FeedGroupDialog : DialogFragment(), BackPressable {
|
||||
private fun showScreen(screen: ScreenState) {
|
||||
currentScreen = screen
|
||||
|
||||
options_root.onlyVisibleIn(InitialScreen)
|
||||
icon_selector.onlyVisibleIn(IconPickerScreen)
|
||||
subscriptions_selector.onlyVisibleIn(SubscriptionsPickerScreen)
|
||||
delete_screen_message.onlyVisibleIn(DeleteScreen)
|
||||
feedGroupCreateBinding.optionsRoot.onlyVisibleIn(InitialScreen)
|
||||
feedGroupCreateBinding.iconSelector.onlyVisibleIn(IconPickerScreen)
|
||||
feedGroupCreateBinding.subscriptionsSelector.onlyVisibleIn(SubscriptionsPickerScreen)
|
||||
feedGroupCreateBinding.deleteScreenMessage.onlyVisibleIn(DeleteScreen)
|
||||
|
||||
separator.onlyVisibleIn(SubscriptionsPickerScreen, IconPickerScreen)
|
||||
cancel_button.onlyVisibleIn(InitialScreen, DeleteScreen)
|
||||
feedGroupCreateBinding.separator.onlyVisibleIn(SubscriptionsPickerScreen, IconPickerScreen)
|
||||
feedGroupCreateBinding.cancelButton.onlyVisibleIn(InitialScreen, DeleteScreen)
|
||||
|
||||
confirm_button.setText(
|
||||
feedGroupCreateBinding.confirmButton.setText(
|
||||
when {
|
||||
currentScreen == InitialScreen && groupId == NO_GROUP_SELECTED -> R.string.create
|
||||
else -> android.R.string.ok
|
||||
}
|
||||
)
|
||||
|
||||
delete_button.isGone = currentScreen != InitialScreen || groupId == NO_GROUP_SELECTED
|
||||
feedGroupCreateBinding.deleteButton.isGone = currentScreen != InitialScreen || groupId == NO_GROUP_SELECTED
|
||||
|
||||
hideKeyboard()
|
||||
hideSearch()
|
||||
@@ -434,26 +444,26 @@ class FeedGroupDialog : DialogFragment(), BackPressable {
|
||||
// Utils
|
||||
////////////////////////////////////////////////////////////////////////// */
|
||||
|
||||
private fun isSearchVisible() = subscriptions_header_search_container?.visibility == View.VISIBLE
|
||||
private fun isSearchVisible() = _searchLayoutBinding?.root?.visibility == View.VISIBLE
|
||||
|
||||
private fun resetSearch() {
|
||||
toolbar_search_edit_text.setText("")
|
||||
searchLayoutBinding.toolbarSearchEditText.setText("")
|
||||
subscriptionsCurrentSearchQuery = ""
|
||||
viewModel.clearSubscriptionsFilter()
|
||||
}
|
||||
|
||||
private fun hideSearch() {
|
||||
resetSearch()
|
||||
subscriptions_header_search_container.visibility = View.GONE
|
||||
subscriptions_header_info_container.visibility = View.VISIBLE
|
||||
subscriptions_header_toolbar.menu.findItem(R.id.action_search).isVisible = true
|
||||
searchLayoutBinding.root.visibility = View.GONE
|
||||
feedGroupCreateBinding.subscriptionsHeaderInfoContainer.visibility = View.VISIBLE
|
||||
feedGroupCreateBinding.subscriptionsHeaderToolbar.menu.findItem(R.id.action_search).isVisible = true
|
||||
hideKeyboardSearch()
|
||||
}
|
||||
|
||||
private fun showSearch() {
|
||||
subscriptions_header_search_container.visibility = View.VISIBLE
|
||||
subscriptions_header_info_container.visibility = View.GONE
|
||||
subscriptions_header_toolbar.menu.findItem(R.id.action_search).isVisible = false
|
||||
searchLayoutBinding.root.visibility = View.VISIBLE
|
||||
feedGroupCreateBinding.subscriptionsHeaderInfoContainer.visibility = View.GONE
|
||||
feedGroupCreateBinding.subscriptionsHeaderToolbar.menu.findItem(R.id.action_search).isVisible = false
|
||||
showKeyboardSearch()
|
||||
}
|
||||
|
||||
@@ -462,37 +472,43 @@ class FeedGroupDialog : DialogFragment(), BackPressable {
|
||||
}
|
||||
|
||||
private fun showKeyboardSearch() {
|
||||
if (toolbar_search_edit_text.requestFocus()) {
|
||||
inputMethodManager.showSoftInput(toolbar_search_edit_text, InputMethodManager.SHOW_IMPLICIT)
|
||||
if (searchLayoutBinding.toolbarSearchEditText.requestFocus()) {
|
||||
inputMethodManager.showSoftInput(
|
||||
searchLayoutBinding.toolbarSearchEditText,
|
||||
InputMethodManager.SHOW_IMPLICIT
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun hideKeyboardSearch() {
|
||||
inputMethodManager.hideSoftInputFromWindow(
|
||||
toolbar_search_edit_text.windowToken,
|
||||
searchLayoutBinding.toolbarSearchEditText.windowToken,
|
||||
InputMethodManager.RESULT_UNCHANGED_SHOWN
|
||||
)
|
||||
toolbar_search_edit_text.clearFocus()
|
||||
searchLayoutBinding.toolbarSearchEditText.clearFocus()
|
||||
}
|
||||
|
||||
private fun showKeyboard() {
|
||||
if (group_name_input.requestFocus()) {
|
||||
inputMethodManager.showSoftInput(group_name_input, InputMethodManager.SHOW_IMPLICIT)
|
||||
if (feedGroupCreateBinding.groupNameInput.requestFocus()) {
|
||||
inputMethodManager.showSoftInput(
|
||||
feedGroupCreateBinding.groupNameInput,
|
||||
InputMethodManager.SHOW_IMPLICIT
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun hideKeyboard() {
|
||||
inputMethodManager.hideSoftInputFromWindow(
|
||||
group_name_input.windowToken,
|
||||
feedGroupCreateBinding.groupNameInput.windowToken,
|
||||
InputMethodManager.RESULT_UNCHANGED_SHOWN
|
||||
)
|
||||
group_name_input.clearFocus()
|
||||
feedGroupCreateBinding.groupNameInput.clearFocus()
|
||||
}
|
||||
|
||||
private fun disableInput() {
|
||||
delete_button?.isEnabled = false
|
||||
confirm_button?.isEnabled = false
|
||||
cancel_button?.isEnabled = false
|
||||
_feedGroupCreateBinding?.deleteButton?.isEnabled = false
|
||||
_feedGroupCreateBinding?.confirmButton?.isEnabled = false
|
||||
_feedGroupCreateBinding?.cancelButton?.isEnabled = false
|
||||
isCancelable = false
|
||||
|
||||
hideKeyboard()
|
||||
|
||||
@@ -12,21 +12,27 @@ 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.TouchCallback
|
||||
import com.xwray.groupie.kotlinandroidextensions.GroupieViewHolder
|
||||
import icepick.Icepick
|
||||
import icepick.State
|
||||
import kotlinx.android.synthetic.main.dialog_feed_group_reorder.confirm_button
|
||||
import kotlinx.android.synthetic.main.dialog_feed_group_reorder.feed_groups_list
|
||||
import org.schabi.newpipe.R
|
||||
import org.schabi.newpipe.database.feed.model.FeedGroupEntity
|
||||
import org.schabi.newpipe.databinding.DialogFeedGroupReorderBinding
|
||||
import org.schabi.newpipe.local.subscription.dialog.FeedGroupReorderDialogViewModel.DialogEvent.ProcessingEvent
|
||||
import org.schabi.newpipe.local.subscription.dialog.FeedGroupReorderDialogViewModel.DialogEvent.SuccessEvent
|
||||
import org.schabi.newpipe.local.subscription.item.FeedGroupReorderItem
|
||||
import org.schabi.newpipe.util.ThemeHelper
|
||||
import java.util.Collections
|
||||
import kotlin.collections.ArrayList
|
||||
import kotlin.collections.List
|
||||
import kotlin.collections.map
|
||||
import kotlin.collections.sortedBy
|
||||
|
||||
class FeedGroupReorderDialog : DialogFragment() {
|
||||
private var _binding: DialogFeedGroupReorderBinding? = null
|
||||
private val binding get() = _binding!!
|
||||
|
||||
private lateinit var viewModel: FeedGroupReorderDialogViewModel
|
||||
|
||||
@State
|
||||
@@ -48,6 +54,7 @@ class FeedGroupReorderDialog : DialogFragment() {
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
_binding = DialogFeedGroupReorderBinding.bind(view)
|
||||
|
||||
viewModel = ViewModelProvider(this).get(FeedGroupReorderDialogViewModel::class.java)
|
||||
viewModel.groupsLiveData.observe(viewLifecycleOwner, Observer(::handleGroups))
|
||||
@@ -61,15 +68,20 @@ class FeedGroupReorderDialog : DialogFragment() {
|
||||
}
|
||||
)
|
||||
|
||||
feed_groups_list.layoutManager = LinearLayoutManager(requireContext())
|
||||
feed_groups_list.adapter = groupAdapter
|
||||
itemTouchHelper.attachToRecyclerView(feed_groups_list)
|
||||
binding.feedGroupsList.layoutManager = LinearLayoutManager(requireContext())
|
||||
binding.feedGroupsList.adapter = groupAdapter
|
||||
itemTouchHelper.attachToRecyclerView(binding.feedGroupsList)
|
||||
|
||||
confirm_button.setOnClickListener {
|
||||
binding.confirmButton.setOnClickListener {
|
||||
viewModel.updateOrder(groupOrderedIdList)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
_binding = null
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
super.onSaveInstanceState(outState)
|
||||
Icepick.saveInstanceState(this, outState)
|
||||
@@ -89,7 +101,7 @@ class FeedGroupReorderDialog : DialogFragment() {
|
||||
}
|
||||
|
||||
private fun disableInput() {
|
||||
confirm_button?.isEnabled = false
|
||||
_binding?.confirmButton?.isEnabled = false
|
||||
isCancelable = false
|
||||
}
|
||||
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
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.kotlinandroidextensions.GroupieViewHolder
|
||||
import com.xwray.groupie.kotlinandroidextensions.Item
|
||||
import kotlinx.android.synthetic.main.list_channel_item.itemAdditionalDetails
|
||||
import kotlinx.android.synthetic.main.list_channel_item.itemChannelDescriptionView
|
||||
import kotlinx.android.synthetic.main.list_channel_item.itemThumbnailView
|
||||
import kotlinx.android.synthetic.main.list_channel_item.itemTitleView
|
||||
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
|
||||
@@ -19,8 +17,7 @@ class ChannelItem(
|
||||
private val subscriptionId: Long = -1L,
|
||||
var itemVersion: ItemVersion = ItemVersion.NORMAL,
|
||||
var gesturesListener: OnClickGesture<ChannelInfoItem>? = null
|
||||
) : Item() {
|
||||
|
||||
) : Item<GroupieViewHolder>() {
|
||||
override fun getId(): Long = if (subscriptionId == -1L) super.getId() else subscriptionId
|
||||
|
||||
enum class ItemVersion { NORMAL, MINI, GRID }
|
||||
@@ -32,18 +29,25 @@ class ChannelItem(
|
||||
}
|
||||
|
||||
override fun bind(viewHolder: GroupieViewHolder, position: Int) {
|
||||
viewHolder.itemTitleView.text = infoItem.name
|
||||
viewHolder.itemAdditionalDetails.text = getDetailLine(viewHolder.root.context)
|
||||
if (itemVersion == ItemVersion.NORMAL) viewHolder.itemChannelDescriptionView.text = infoItem.description
|
||||
val itemTitleView = viewHolder.root.findViewById<TextView>(R.id.itemTitleView)
|
||||
val itemAdditionalDetails = viewHolder.root.findViewById<TextView>(R.id.itemAdditionalDetails)
|
||||
val itemChannelDescriptionView = viewHolder.root.findViewById<TextView>(R.id.itemChannelDescriptionView)
|
||||
val itemThumbnailView = viewHolder.root.findViewById<ImageView>(R.id.itemThumbnailView)
|
||||
|
||||
itemTitleView.text = infoItem.name
|
||||
itemAdditionalDetails.text = getDetailLine(viewHolder.root.context)
|
||||
if (itemVersion == ItemVersion.NORMAL) {
|
||||
itemChannelDescriptionView.text = infoItem.description
|
||||
}
|
||||
|
||||
ImageLoader.getInstance().displayImage(
|
||||
infoItem.thumbnailUrl, viewHolder.itemThumbnailView,
|
||||
infoItem.thumbnailUrl, itemThumbnailView,
|
||||
ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS
|
||||
)
|
||||
|
||||
gesturesListener?.run {
|
||||
viewHolder.containerView.setOnClickListener { selected(infoItem) }
|
||||
viewHolder.containerView.setOnLongClickListener { held(infoItem); true }
|
||||
viewHolder.root.setOnClickListener { selected(infoItem) }
|
||||
viewHolder.root.setOnLongClickListener { held(infoItem); true }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
package org.schabi.newpipe.local.subscription.item
|
||||
|
||||
import com.xwray.groupie.kotlinandroidextensions.GroupieViewHolder
|
||||
import com.xwray.groupie.kotlinandroidextensions.Item
|
||||
import android.view.View
|
||||
import com.xwray.groupie.viewbinding.BindableItem
|
||||
import org.schabi.newpipe.R
|
||||
import org.schabi.newpipe.databinding.ListEmptyViewBinding
|
||||
|
||||
class EmptyPlaceholderItem : Item() {
|
||||
class EmptyPlaceholderItem : BindableItem<ListEmptyViewBinding>() {
|
||||
override fun getLayout(): Int = R.layout.list_empty_view
|
||||
override fun bind(viewHolder: GroupieViewHolder, position: Int) {}
|
||||
override fun bind(viewBinding: ListEmptyViewBinding, position: Int) {}
|
||||
override fun getSpanSize(spanCount: Int, position: Int): Int = spanCount
|
||||
override fun initializeViewBinding(view: View) = ListEmptyViewBinding.bind(view)
|
||||
}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
package org.schabi.newpipe.local.subscription.item
|
||||
|
||||
import com.xwray.groupie.kotlinandroidextensions.GroupieViewHolder
|
||||
import com.xwray.groupie.kotlinandroidextensions.Item
|
||||
import android.view.View
|
||||
import com.xwray.groupie.viewbinding.BindableItem
|
||||
import org.schabi.newpipe.R
|
||||
import org.schabi.newpipe.databinding.FeedGroupAddNewItemBinding
|
||||
|
||||
class FeedGroupAddItem : Item() {
|
||||
class FeedGroupAddItem : BindableItem<FeedGroupAddNewItemBinding>() {
|
||||
override fun getLayout(): Int = R.layout.feed_group_add_new_item
|
||||
override fun bind(viewHolder: GroupieViewHolder, position: Int) {}
|
||||
override fun bind(viewBinding: FeedGroupAddNewItemBinding, position: Int) {}
|
||||
override fun initializeViewBinding(view: View) = FeedGroupAddNewItemBinding.bind(view)
|
||||
}
|
||||
|
||||
@@ -1,18 +1,17 @@
|
||||
package org.schabi.newpipe.local.subscription.item
|
||||
|
||||
import com.xwray.groupie.kotlinandroidextensions.GroupieViewHolder
|
||||
import com.xwray.groupie.kotlinandroidextensions.Item
|
||||
import kotlinx.android.synthetic.main.feed_group_card_item.icon
|
||||
import kotlinx.android.synthetic.main.feed_group_card_item.title
|
||||
import android.view.View
|
||||
import com.xwray.groupie.viewbinding.BindableItem
|
||||
import org.schabi.newpipe.R
|
||||
import org.schabi.newpipe.database.feed.model.FeedGroupEntity
|
||||
import org.schabi.newpipe.databinding.FeedGroupCardItemBinding
|
||||
import org.schabi.newpipe.local.subscription.FeedGroupIcon
|
||||
|
||||
data class FeedGroupCardItem(
|
||||
val groupId: Long = FeedGroupEntity.GROUP_ALL_ID,
|
||||
val name: String,
|
||||
val icon: FeedGroupIcon
|
||||
) : Item() {
|
||||
) : BindableItem<FeedGroupCardItemBinding>() {
|
||||
constructor (feedGroupEntity: FeedGroupEntity) : this(feedGroupEntity.uid, feedGroupEntity.name, feedGroupEntity.icon)
|
||||
|
||||
override fun getId(): Long {
|
||||
@@ -24,8 +23,10 @@ data class FeedGroupCardItem(
|
||||
|
||||
override fun getLayout(): Int = R.layout.feed_group_card_item
|
||||
|
||||
override fun bind(viewHolder: GroupieViewHolder, position: Int) {
|
||||
viewHolder.title.text = name
|
||||
viewHolder.icon.setImageResource(icon.getDrawableRes(viewHolder.containerView.context))
|
||||
override fun bind(viewBinding: FeedGroupCardItemBinding, position: Int) {
|
||||
viewBinding.title.text = name
|
||||
viewBinding.icon.setImageResource(icon.getDrawableRes(viewBinding.root.context))
|
||||
}
|
||||
|
||||
override fun initializeViewBinding(view: View) = FeedGroupCardItemBinding.bind(view)
|
||||
}
|
||||
|
||||
@@ -6,13 +6,16 @@ import android.view.View
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.xwray.groupie.GroupAdapter
|
||||
import com.xwray.groupie.kotlinandroidextensions.GroupieViewHolder
|
||||
import com.xwray.groupie.kotlinandroidextensions.Item
|
||||
import kotlinx.android.synthetic.main.feed_item_carousel.recycler_view
|
||||
import com.xwray.groupie.viewbinding.BindableItem
|
||||
import com.xwray.groupie.viewbinding.GroupieViewHolder
|
||||
import org.schabi.newpipe.R
|
||||
import org.schabi.newpipe.databinding.FeedItemCarouselBinding
|
||||
import org.schabi.newpipe.local.subscription.decoration.FeedGroupCarouselDecoration
|
||||
|
||||
class FeedGroupCarouselItem(context: Context, private val carouselAdapter: GroupAdapter<GroupieViewHolder>) : Item() {
|
||||
class FeedGroupCarouselItem(
|
||||
context: Context,
|
||||
private val carouselAdapter: GroupAdapter<GroupieViewHolder<FeedItemCarouselBinding>>
|
||||
) : BindableItem<FeedItemCarouselBinding>() {
|
||||
private val feedGroupCarouselDecoration = FeedGroupCarouselDecoration(context)
|
||||
|
||||
private var linearLayoutManager: LinearLayoutManager? = null
|
||||
@@ -30,12 +33,12 @@ class FeedGroupCarouselItem(context: Context, private val carouselAdapter: Group
|
||||
listState = state
|
||||
}
|
||||
|
||||
override fun createViewHolder(itemView: View): GroupieViewHolder {
|
||||
val viewHolder = super.createViewHolder(itemView)
|
||||
override fun initializeViewBinding(view: View): FeedItemCarouselBinding {
|
||||
val viewHolder = FeedItemCarouselBinding.bind(view)
|
||||
|
||||
linearLayoutManager = LinearLayoutManager(itemView.context, RecyclerView.HORIZONTAL, false)
|
||||
linearLayoutManager = LinearLayoutManager(view.context, RecyclerView.HORIZONTAL, false)
|
||||
|
||||
viewHolder.recycler_view.apply {
|
||||
viewHolder.recyclerView.apply {
|
||||
layoutManager = linearLayoutManager
|
||||
adapter = carouselAdapter
|
||||
addItemDecoration(feedGroupCarouselDecoration)
|
||||
@@ -44,12 +47,12 @@ class FeedGroupCarouselItem(context: Context, private val carouselAdapter: Group
|
||||
return viewHolder
|
||||
}
|
||||
|
||||
override fun bind(viewHolder: GroupieViewHolder, position: Int) {
|
||||
viewHolder.recycler_view.apply { adapter = carouselAdapter }
|
||||
override fun bind(viewBinding: FeedItemCarouselBinding, position: Int) {
|
||||
viewBinding.recyclerView.apply { adapter = carouselAdapter }
|
||||
linearLayoutManager?.onRestoreInstanceState(listState)
|
||||
}
|
||||
|
||||
override fun unbind(viewHolder: GroupieViewHolder) {
|
||||
override fun unbind(viewHolder: GroupieViewHolder<FeedItemCarouselBinding>) {
|
||||
super.unbind(viewHolder)
|
||||
|
||||
listState = linearLayoutManager?.onSaveInstanceState()
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
package org.schabi.newpipe.local.subscription.item
|
||||
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import androidx.recyclerview.widget.ItemTouchHelper
|
||||
import androidx.recyclerview.widget.ItemTouchHelper.DOWN
|
||||
import androidx.recyclerview.widget.ItemTouchHelper.UP
|
||||
import com.xwray.groupie.kotlinandroidextensions.GroupieViewHolder
|
||||
import com.xwray.groupie.kotlinandroidextensions.Item
|
||||
import kotlinx.android.synthetic.main.feed_group_reorder_item.group_icon
|
||||
import kotlinx.android.synthetic.main.feed_group_reorder_item.group_name
|
||||
import kotlinx.android.synthetic.main.feed_group_reorder_item.handle
|
||||
import com.xwray.groupie.viewbinding.BindableItem
|
||||
import com.xwray.groupie.viewbinding.GroupieViewHolder
|
||||
import org.schabi.newpipe.R
|
||||
import org.schabi.newpipe.database.feed.model.FeedGroupEntity
|
||||
import org.schabi.newpipe.databinding.FeedGroupReorderItemBinding
|
||||
import org.schabi.newpipe.local.subscription.FeedGroupIcon
|
||||
|
||||
data class FeedGroupReorderItem(
|
||||
@@ -18,7 +17,7 @@ data class FeedGroupReorderItem(
|
||||
val name: String,
|
||||
val icon: FeedGroupIcon,
|
||||
val dragCallback: ItemTouchHelper
|
||||
) : Item() {
|
||||
) : BindableItem<FeedGroupReorderItemBinding>() {
|
||||
constructor (feedGroupEntity: FeedGroupEntity, dragCallback: ItemTouchHelper) :
|
||||
this(feedGroupEntity.uid, feedGroupEntity.name, feedGroupEntity.icon, dragCallback)
|
||||
|
||||
@@ -31,10 +30,14 @@ data class FeedGroupReorderItem(
|
||||
|
||||
override fun getLayout(): Int = R.layout.feed_group_reorder_item
|
||||
|
||||
override fun bind(viewHolder: GroupieViewHolder, position: Int) {
|
||||
viewHolder.group_name.text = name
|
||||
viewHolder.group_icon.setImageResource(icon.getDrawableRes(viewHolder.containerView.context))
|
||||
viewHolder.handle.setOnTouchListener { _, event ->
|
||||
override fun bind(viewBinding: FeedGroupReorderItemBinding, position: Int) {
|
||||
viewBinding.groupName.text = name
|
||||
viewBinding.groupIcon.setImageResource(icon.getDrawableRes(viewBinding.root.context))
|
||||
}
|
||||
|
||||
override fun bind(viewHolder: GroupieViewHolder<FeedGroupReorderItemBinding>, position: Int, payloads: MutableList<Any>) {
|
||||
super.bind(viewHolder, position, payloads)
|
||||
viewHolder.binding.handle.setOnTouchListener { _, event ->
|
||||
if (event.actionMasked == MotionEvent.ACTION_DOWN) {
|
||||
dragCallback.startDrag(viewHolder)
|
||||
return@setOnTouchListener true
|
||||
@@ -47,4 +50,6 @@ data class FeedGroupReorderItem(
|
||||
override fun getDragDirs(): Int {
|
||||
return UP or DOWN
|
||||
}
|
||||
|
||||
override fun initializeViewBinding(view: View) = FeedGroupReorderItemBinding.bind(view)
|
||||
}
|
||||
|
||||
@@ -7,17 +7,13 @@ import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.annotation.DrawableRes
|
||||
import com.xwray.groupie.kotlinandroidextensions.GroupieViewHolder
|
||||
import com.xwray.groupie.kotlinandroidextensions.Item
|
||||
import kotlinx.android.synthetic.main.feed_import_export_group.export_to_options
|
||||
import kotlinx.android.synthetic.main.feed_import_export_group.import_export
|
||||
import kotlinx.android.synthetic.main.feed_import_export_group.import_export_expand_icon
|
||||
import kotlinx.android.synthetic.main.feed_import_export_group.import_export_options
|
||||
import kotlinx.android.synthetic.main.feed_import_export_group.import_from_options
|
||||
import com.xwray.groupie.viewbinding.BindableItem
|
||||
import com.xwray.groupie.viewbinding.GroupieViewHolder
|
||||
import org.schabi.newpipe.R
|
||||
import org.schabi.newpipe.databinding.FeedImportExportGroupBinding
|
||||
import org.schabi.newpipe.extractor.NewPipe
|
||||
import org.schabi.newpipe.extractor.exceptions.ExtractionException
|
||||
import org.schabi.newpipe.util.AnimationUtils
|
||||
import org.schabi.newpipe.ktx.animateRotation
|
||||
import org.schabi.newpipe.util.ServiceHelper
|
||||
import org.schabi.newpipe.util.ThemeHelper
|
||||
import org.schabi.newpipe.views.CollapsibleView
|
||||
@@ -27,51 +23,52 @@ class FeedImportExportItem(
|
||||
val onImportFromServiceSelected: (Int) -> Unit,
|
||||
val onExportSelected: () -> Unit,
|
||||
var isExpanded: Boolean = false
|
||||
) : Item() {
|
||||
) : BindableItem<FeedImportExportGroupBinding>() {
|
||||
companion object {
|
||||
const val REFRESH_EXPANDED_STATUS = 123
|
||||
}
|
||||
|
||||
override fun bind(viewHolder: GroupieViewHolder, position: Int, payloads: MutableList<Any>) {
|
||||
override fun bind(viewBinding: FeedImportExportGroupBinding, position: Int, payloads: MutableList<Any>) {
|
||||
if (payloads.contains(REFRESH_EXPANDED_STATUS)) {
|
||||
viewHolder.import_export_options.apply { if (isExpanded) expand() else collapse() }
|
||||
viewBinding.importExportOptions.apply { if (isExpanded) expand() else collapse() }
|
||||
return
|
||||
}
|
||||
|
||||
super.bind(viewHolder, position, payloads)
|
||||
super.bind(viewBinding, position, payloads)
|
||||
}
|
||||
|
||||
override fun getLayout(): Int = R.layout.feed_import_export_group
|
||||
|
||||
override fun bind(viewHolder: GroupieViewHolder, position: Int) {
|
||||
if (viewHolder.import_from_options.childCount == 0) setupImportFromItems(viewHolder.import_from_options)
|
||||
if (viewHolder.export_to_options.childCount == 0) setupExportToItems(viewHolder.export_to_options)
|
||||
override fun bind(viewBinding: FeedImportExportGroupBinding, position: Int) {
|
||||
if (viewBinding.importFromOptions.childCount == 0) setupImportFromItems(viewBinding.importFromOptions)
|
||||
if (viewBinding.exportToOptions.childCount == 0) setupExportToItems(viewBinding.exportToOptions)
|
||||
|
||||
expandIconListener?.let { viewHolder.import_export_options.removeListener(it) }
|
||||
expandIconListener?.let { viewBinding.importExportOptions.removeListener(it) }
|
||||
expandIconListener = CollapsibleView.StateListener { newState ->
|
||||
AnimationUtils.animateRotation(
|
||||
viewHolder.import_export_expand_icon,
|
||||
viewBinding.importExportExpandIcon.animateRotation(
|
||||
250, if (newState == CollapsibleView.COLLAPSED) 0 else 180
|
||||
)
|
||||
}
|
||||
|
||||
viewHolder.import_export_options.currentState = if (isExpanded) CollapsibleView.EXPANDED else CollapsibleView.COLLAPSED
|
||||
viewHolder.import_export_expand_icon.rotation = if (isExpanded) 180F else 0F
|
||||
viewHolder.import_export_options.ready()
|
||||
viewBinding.importExportOptions.currentState = if (isExpanded) CollapsibleView.EXPANDED else CollapsibleView.COLLAPSED
|
||||
viewBinding.importExportExpandIcon.rotation = if (isExpanded) 180F else 0F
|
||||
viewBinding.importExportOptions.ready()
|
||||
|
||||
viewHolder.import_export_options.addListener(expandIconListener)
|
||||
viewHolder.import_export.setOnClickListener {
|
||||
viewHolder.import_export_options.switchState()
|
||||
isExpanded = viewHolder.import_export_options.currentState == CollapsibleView.EXPANDED
|
||||
viewBinding.importExportOptions.addListener(expandIconListener)
|
||||
viewBinding.importExport.setOnClickListener {
|
||||
viewBinding.importExportOptions.switchState()
|
||||
isExpanded = viewBinding.importExportOptions.currentState == CollapsibleView.EXPANDED
|
||||
}
|
||||
}
|
||||
|
||||
override fun unbind(viewHolder: GroupieViewHolder) {
|
||||
override fun unbind(viewHolder: GroupieViewHolder<FeedImportExportGroupBinding>) {
|
||||
super.unbind(viewHolder)
|
||||
expandIconListener?.let { viewHolder.import_export_options.removeListener(it) }
|
||||
expandIconListener?.let { viewHolder.binding.importExportOptions.removeListener(it) }
|
||||
expandIconListener = null
|
||||
}
|
||||
|
||||
override fun initializeViewBinding(view: View) = FeedImportExportGroupBinding.bind(view)
|
||||
|
||||
private var expandIconListener: CollapsibleView.StateListener? = null
|
||||
|
||||
private fun addItemView(title: String, @DrawableRes icon: Int, container: ViewGroup): View {
|
||||
@@ -118,7 +115,8 @@ class FeedImportExportItem(
|
||||
private fun setupExportToItems(listHolder: ViewGroup) {
|
||||
val previousBackupItem = addItemView(
|
||||
listHolder.context.getString(R.string.file),
|
||||
ThemeHelper.resolveResourceIdFromAttr(listHolder.context, R.attr.ic_save), listHolder
|
||||
ThemeHelper.resolveResourceIdFromAttr(listHolder.context, R.attr.ic_save),
|
||||
listHolder
|
||||
)
|
||||
previousBackupItem.setOnClickListener { onExportSelected() }
|
||||
}
|
||||
|
||||
@@ -1,19 +1,23 @@
|
||||
package org.schabi.newpipe.local.subscription.item
|
||||
|
||||
import android.view.View
|
||||
import android.view.View.OnClickListener
|
||||
import com.xwray.groupie.kotlinandroidextensions.GroupieViewHolder
|
||||
import com.xwray.groupie.kotlinandroidextensions.Item
|
||||
import kotlinx.android.synthetic.main.header_item.header_title
|
||||
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) : Item() {
|
||||
|
||||
class HeaderItem(
|
||||
val title: String,
|
||||
private val onClickListener: (() -> Unit)? = null
|
||||
) : BindableItem<HeaderItemBinding>() {
|
||||
override fun getLayout(): Int = R.layout.header_item
|
||||
|
||||
override fun bind(viewHolder: GroupieViewHolder, position: Int) {
|
||||
viewHolder.header_title.text = title
|
||||
override fun bind(viewBinding: HeaderItemBinding, position: Int) {
|
||||
viewBinding.headerTitle.text = title
|
||||
|
||||
val listener: OnClickListener? = if (onClickListener != null) OnClickListener { onClickListener.invoke() } else null
|
||||
viewHolder.root.setOnClickListener(listener)
|
||||
viewBinding.root.setOnClickListener(listener)
|
||||
}
|
||||
|
||||
override fun initializeViewBinding(view: View) = HeaderItemBinding.bind(view)
|
||||
}
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
package org.schabi.newpipe.local.subscription.item
|
||||
|
||||
import android.view.View
|
||||
import android.view.View.OnClickListener
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.core.view.isVisible
|
||||
import com.xwray.groupie.kotlinandroidextensions.GroupieViewHolder
|
||||
import com.xwray.groupie.kotlinandroidextensions.Item
|
||||
import kotlinx.android.synthetic.main.header_with_menu_item.header_menu_item
|
||||
import kotlinx.android.synthetic.main.header_with_menu_item.header_title
|
||||
import com.xwray.groupie.viewbinding.BindableItem
|
||||
import org.schabi.newpipe.R
|
||||
import org.schabi.newpipe.databinding.HeaderWithMenuItemBinding
|
||||
|
||||
class HeaderWithMenuItem(
|
||||
val title: String,
|
||||
@@ -15,37 +14,37 @@ class HeaderWithMenuItem(
|
||||
var showMenuItem: Boolean = true,
|
||||
private val onClickListener: (() -> Unit)? = null,
|
||||
private val menuItemOnClickListener: (() -> Unit)? = null
|
||||
) : Item() {
|
||||
) : BindableItem<HeaderWithMenuItemBinding>() {
|
||||
companion object {
|
||||
const val PAYLOAD_UPDATE_VISIBILITY_MENU_ITEM = 1
|
||||
}
|
||||
|
||||
override fun getLayout(): Int = R.layout.header_with_menu_item
|
||||
|
||||
override fun bind(viewHolder: GroupieViewHolder, position: Int, payloads: MutableList<Any>) {
|
||||
override fun bind(viewBinding: HeaderWithMenuItemBinding, position: Int, payloads: MutableList<Any>) {
|
||||
if (payloads.contains(PAYLOAD_UPDATE_VISIBILITY_MENU_ITEM)) {
|
||||
updateMenuItemVisibility(viewHolder)
|
||||
updateMenuItemVisibility(viewBinding)
|
||||
return
|
||||
}
|
||||
|
||||
super.bind(viewHolder, position, payloads)
|
||||
super.bind(viewBinding, position, payloads)
|
||||
}
|
||||
|
||||
override fun bind(viewHolder: GroupieViewHolder, position: Int) {
|
||||
viewHolder.header_title.text = title
|
||||
viewHolder.header_menu_item.setImageResource(itemIcon)
|
||||
override fun bind(viewBinding: HeaderWithMenuItemBinding, position: Int) {
|
||||
viewBinding.headerTitle.text = title
|
||||
viewBinding.headerMenuItem.setImageResource(itemIcon)
|
||||
|
||||
val listener: OnClickListener? =
|
||||
onClickListener?.let { OnClickListener { onClickListener.invoke() } }
|
||||
viewHolder.root.setOnClickListener(listener)
|
||||
val listener = onClickListener?.let { OnClickListener { onClickListener.invoke() } }
|
||||
viewBinding.root.setOnClickListener(listener)
|
||||
|
||||
val menuItemListener: OnClickListener? =
|
||||
menuItemOnClickListener?.let { OnClickListener { menuItemOnClickListener.invoke() } }
|
||||
viewHolder.header_menu_item.setOnClickListener(menuItemListener)
|
||||
updateMenuItemVisibility(viewHolder)
|
||||
val menuItemListener = menuItemOnClickListener?.let { OnClickListener { menuItemOnClickListener.invoke() } }
|
||||
viewBinding.headerMenuItem.setOnClickListener(menuItemListener)
|
||||
updateMenuItemVisibility(viewBinding)
|
||||
}
|
||||
|
||||
private fun updateMenuItemVisibility(viewHolder: GroupieViewHolder) {
|
||||
viewHolder.header_menu_item.isVisible = showMenuItem
|
||||
override fun initializeViewBinding(view: View) = HeaderWithMenuItemBinding.bind(view)
|
||||
|
||||
private fun updateMenuItemVisibility(viewBinding: HeaderWithMenuItemBinding) {
|
||||
viewBinding.headerMenuItem.isVisible = showMenuItem
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,20 +1,25 @@
|
||||
package org.schabi.newpipe.local.subscription.item
|
||||
|
||||
import android.content.Context
|
||||
import android.view.View
|
||||
import androidx.annotation.DrawableRes
|
||||
import com.xwray.groupie.kotlinandroidextensions.GroupieViewHolder
|
||||
import com.xwray.groupie.kotlinandroidextensions.Item
|
||||
import kotlinx.android.synthetic.main.picker_icon_item.icon_view
|
||||
import com.xwray.groupie.viewbinding.BindableItem
|
||||
import org.schabi.newpipe.R
|
||||
import org.schabi.newpipe.databinding.PickerIconItemBinding
|
||||
import org.schabi.newpipe.local.subscription.FeedGroupIcon
|
||||
|
||||
class PickerIconItem(context: Context, val icon: FeedGroupIcon) : Item() {
|
||||
class PickerIconItem(
|
||||
context: Context,
|
||||
val icon: FeedGroupIcon
|
||||
) : BindableItem<PickerIconItemBinding>() {
|
||||
@DrawableRes
|
||||
val iconRes: Int = icon.getDrawableRes(context)
|
||||
|
||||
override fun getLayout(): Int = R.layout.picker_icon_item
|
||||
|
||||
override fun bind(viewHolder: GroupieViewHolder, position: Int) {
|
||||
viewHolder.icon_view.setImageResource(iconRes)
|
||||
override fun bind(viewBinding: PickerIconItemBinding, position: Int) {
|
||||
viewBinding.iconView.setImageResource(iconRes)
|
||||
}
|
||||
|
||||
override fun initializeViewBinding(view: View) = PickerIconItemBinding.bind(view)
|
||||
}
|
||||
|
||||
@@ -1,49 +1,51 @@
|
||||
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.kotlinandroidextensions.GroupieViewHolder
|
||||
import com.xwray.groupie.kotlinandroidextensions.Item
|
||||
import kotlinx.android.synthetic.main.picker_subscription_item.*
|
||||
import kotlinx.android.synthetic.main.picker_subscription_item.view.*
|
||||
import com.xwray.groupie.viewbinding.BindableItem
|
||||
import com.xwray.groupie.viewbinding.GroupieViewHolder
|
||||
import org.schabi.newpipe.R
|
||||
import org.schabi.newpipe.database.subscription.SubscriptionEntity
|
||||
import org.schabi.newpipe.util.AnimationUtils
|
||||
import org.schabi.newpipe.util.AnimationUtils.animateView
|
||||
import org.schabi.newpipe.databinding.PickerSubscriptionItemBinding
|
||||
import org.schabi.newpipe.ktx.AnimationType
|
||||
import org.schabi.newpipe.ktx.animate
|
||||
import org.schabi.newpipe.util.ImageDisplayConstants
|
||||
|
||||
data class PickerSubscriptionItem(
|
||||
val subscriptionEntity: SubscriptionEntity,
|
||||
var isSelected: Boolean = false
|
||||
) : Item() {
|
||||
) : BindableItem<PickerSubscriptionItemBinding>() {
|
||||
override fun getId(): Long = subscriptionEntity.uid
|
||||
override fun getLayout(): Int = R.layout.picker_subscription_item
|
||||
override fun getSpanSize(spanCount: Int, position: Int): Int = 1
|
||||
|
||||
override fun bind(viewHolder: GroupieViewHolder, position: Int) {
|
||||
override fun bind(viewBinding: PickerSubscriptionItemBinding, position: Int) {
|
||||
ImageLoader.getInstance().displayImage(
|
||||
subscriptionEntity.avatarUrl,
|
||||
viewHolder.thumbnail_view, ImageDisplayConstants.DISPLAY_AVATAR_OPTIONS
|
||||
viewBinding.thumbnailView, ImageDisplayConstants.DISPLAY_AVATAR_OPTIONS
|
||||
)
|
||||
|
||||
viewHolder.title_view.text = subscriptionEntity.name
|
||||
viewHolder.selected_highlight.isVisible = isSelected
|
||||
viewBinding.titleView.text = subscriptionEntity.name
|
||||
viewBinding.selectedHighlight.isVisible = isSelected
|
||||
}
|
||||
|
||||
override fun unbind(viewHolder: GroupieViewHolder) {
|
||||
override fun unbind(viewHolder: GroupieViewHolder<PickerSubscriptionItemBinding>) {
|
||||
super.unbind(viewHolder)
|
||||
|
||||
viewHolder.selected_highlight.animate().setListener(null).cancel()
|
||||
viewHolder.selected_highlight.visibility = View.GONE
|
||||
viewHolder.selected_highlight.alpha = 1F
|
||||
viewHolder.binding.selectedHighlight.apply {
|
||||
animate().setListener(null).cancel()
|
||||
isGone = true
|
||||
alpha = 1F
|
||||
}
|
||||
}
|
||||
|
||||
override fun initializeViewBinding(view: View) = PickerSubscriptionItemBinding.bind(view)
|
||||
|
||||
fun updateSelected(containerView: View, isSelected: Boolean) {
|
||||
this.isSelected = isSelected
|
||||
animateView(
|
||||
containerView.selected_highlight,
|
||||
AnimationUtils.Type.LIGHT_SCALE_AND_ALPHA, isSelected, 150
|
||||
)
|
||||
PickerSubscriptionItemBinding.bind(containerView).selectedHighlight
|
||||
.animate(isSelected, 150, AnimationType.LIGHT_SCALE_AND_ALPHA)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,18 +31,18 @@ import androidx.annotation.Nullable;
|
||||
import androidx.annotation.StringRes;
|
||||
import androidx.core.app.NotificationCompat;
|
||||
import androidx.core.app.NotificationManagerCompat;
|
||||
import androidx.core.app.ServiceCompat;
|
||||
|
||||
import org.reactivestreams.Publisher;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.error.ErrorActivity;
|
||||
import org.schabi.newpipe.error.ErrorInfo;
|
||||
import org.schabi.newpipe.error.UserAction;
|
||||
import org.schabi.newpipe.extractor.subscription.SubscriptionExtractor;
|
||||
import org.schabi.newpipe.ktx.ExceptionUtils;
|
||||
import org.schabi.newpipe.local.subscription.SubscriptionManager;
|
||||
import org.schabi.newpipe.report.ErrorActivity;
|
||||
import org.schabi.newpipe.report.ErrorInfo;
|
||||
import org.schabi.newpipe.report.UserAction;
|
||||
import org.schabi.newpipe.util.ExceptionUtils;
|
||||
|
||||
import java.io.FileNotFoundException;
|
||||
import java.util.Collections;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
@@ -151,18 +151,15 @@ public abstract class BaseImportExportService extends Service {
|
||||
postErrorResult(null, null);
|
||||
}
|
||||
|
||||
protected void stopAndReportError(@Nullable final Throwable error, final String request) {
|
||||
protected void stopAndReportError(final Throwable throwable, final String request) {
|
||||
stopService();
|
||||
|
||||
final ErrorInfo errorInfo = ErrorInfo
|
||||
.make(UserAction.SUBSCRIPTION, "unknown", request, R.string.general_error);
|
||||
ErrorActivity.reportError(this, error != null ? Collections.singletonList(error)
|
||||
: Collections.emptyList(), null, null, errorInfo);
|
||||
ErrorActivity.reportError(this, new ErrorInfo(
|
||||
throwable, UserAction.SUBSCRIPTION_IMPORT_EXPORT, request));
|
||||
}
|
||||
|
||||
protected void postErrorResult(final String title, final String text) {
|
||||
disposeAll();
|
||||
stopForeground(true);
|
||||
ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE);
|
||||
stopSelf();
|
||||
|
||||
if (title == null) {
|
||||
|
||||
@@ -27,6 +27,7 @@ import androidx.localbroadcastmanager.content.LocalBroadcastManager;
|
||||
|
||||
import org.reactivestreams.Subscriber;
|
||||
import org.reactivestreams.Subscription;
|
||||
import org.schabi.newpipe.App;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.database.subscription.SubscriptionEntity;
|
||||
import org.schabi.newpipe.extractor.subscription.SubscriptionItem;
|
||||
@@ -50,7 +51,7 @@ public class SubscriptionsExportService extends BaseImportExportService {
|
||||
* A {@link LocalBroadcastManager local broadcast} will be made with this action
|
||||
* when the export is successfully completed.
|
||||
*/
|
||||
public static final String EXPORT_COMPLETE_ACTION = "org.schabi.newpipe.local.subscription"
|
||||
public static final String EXPORT_COMPLETE_ACTION = App.PACKAGE_NAME + ".local.subscription"
|
||||
+ ".services.SubscriptionsExportService.EXPORT_COMPLETE";
|
||||
|
||||
private Subscription subscription;
|
||||
|
||||
@@ -29,13 +29,14 @@ import androidx.localbroadcastmanager.content.LocalBroadcastManager;
|
||||
|
||||
import org.reactivestreams.Subscriber;
|
||||
import org.reactivestreams.Subscription;
|
||||
import org.schabi.newpipe.App;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.database.subscription.SubscriptionEntity;
|
||||
import org.schabi.newpipe.extractor.NewPipe;
|
||||
import org.schabi.newpipe.extractor.channel.ChannelInfo;
|
||||
import org.schabi.newpipe.extractor.subscription.SubscriptionItem;
|
||||
import org.schabi.newpipe.ktx.ExceptionUtils;
|
||||
import org.schabi.newpipe.util.Constants;
|
||||
import org.schabi.newpipe.util.ExceptionUtils;
|
||||
import org.schabi.newpipe.util.ExtractorHelper;
|
||||
|
||||
import java.io.File;
|
||||
@@ -66,7 +67,7 @@ public class SubscriptionsImportService extends BaseImportExportService {
|
||||
* A {@link LocalBroadcastManager local broadcast} will be made with this action
|
||||
* when the import is successfully completed.
|
||||
*/
|
||||
public static final String IMPORT_COMPLETE_ACTION = "org.schabi.newpipe.local.subscription"
|
||||
public static final String IMPORT_COMPLETE_ACTION = App.PACKAGE_NAME + ".local.subscription"
|
||||
+ ".services.SubscriptionsImportService.IMPORT_COMPLETE";
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
package org.schabi.newpipe.player;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.view.Menu;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
|
||||
public final class BackgroundPlayerActivity extends ServicePlayerActivity {
|
||||
|
||||
private static final String TAG = "BackgroundPlayerActivity";
|
||||
|
||||
@Override
|
||||
public String getTag() {
|
||||
return TAG;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getSupportActionTitle() {
|
||||
return getResources().getString(R.string.title_activity_play_queue);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Intent getBindIntent() {
|
||||
return new Intent(this, MainPlayer.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void startPlayerListener() {
|
||||
if (player instanceof VideoPlayerImpl) {
|
||||
((VideoPlayerImpl) player).setActivityListener(this);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stopPlayerListener() {
|
||||
if (player instanceof VideoPlayerImpl) {
|
||||
((VideoPlayerImpl) player).removeActivityListener(this);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getPlayerOptionMenuResource() {
|
||||
return R.menu.menu_play_queue_bg;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setupMenu(final Menu menu) {
|
||||
if (player == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
menu.findItem(R.id.action_switch_popup)
|
||||
.setVisible(!((VideoPlayerImpl) player).popupPlayerSelected());
|
||||
menu.findItem(R.id.action_switch_background)
|
||||
.setVisible(!((VideoPlayerImpl) player).audioPlayerSelected());
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -26,6 +26,7 @@ import android.os.Binder;
|
||||
import android.os.IBinder;
|
||||
import android.util.DisplayMetrics;
|
||||
import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.WindowManager;
|
||||
@@ -33,7 +34,8 @@ import android.view.WindowManager;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.content.ContextCompat;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.App;
|
||||
import org.schabi.newpipe.databinding.PlayerBinding;
|
||||
import org.schabi.newpipe.util.ThemeHelper;
|
||||
|
||||
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
|
||||
@@ -46,9 +48,9 @@ import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
|
||||
*/
|
||||
public final class MainPlayer extends Service {
|
||||
private static final String TAG = "MainPlayer";
|
||||
private static final boolean DEBUG = BasePlayer.DEBUG;
|
||||
private static final boolean DEBUG = Player.DEBUG;
|
||||
|
||||
private VideoPlayerImpl playerImpl;
|
||||
private Player player;
|
||||
private WindowManager windowManager;
|
||||
|
||||
private final IBinder mBinder = new MainPlayer.LocalBinder();
|
||||
@@ -64,25 +66,23 @@ public final class MainPlayer extends Service {
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
static final String ACTION_CLOSE
|
||||
= "org.schabi.newpipe.player.MainPlayer.CLOSE";
|
||||
= App.PACKAGE_NAME + ".player.MainPlayer.CLOSE";
|
||||
static final String ACTION_PLAY_PAUSE
|
||||
= "org.schabi.newpipe.player.MainPlayer.PLAY_PAUSE";
|
||||
static final String ACTION_OPEN_CONTROLS
|
||||
= "org.schabi.newpipe.player.MainPlayer.OPEN_CONTROLS";
|
||||
= App.PACKAGE_NAME + ".player.MainPlayer.PLAY_PAUSE";
|
||||
static final String ACTION_REPEAT
|
||||
= "org.schabi.newpipe.player.MainPlayer.REPEAT";
|
||||
= App.PACKAGE_NAME + ".player.MainPlayer.REPEAT";
|
||||
static final String ACTION_PLAY_NEXT
|
||||
= "org.schabi.newpipe.player.MainPlayer.ACTION_PLAY_NEXT";
|
||||
= App.PACKAGE_NAME + ".player.MainPlayer.ACTION_PLAY_NEXT";
|
||||
static final String ACTION_PLAY_PREVIOUS
|
||||
= "org.schabi.newpipe.player.MainPlayer.ACTION_PLAY_PREVIOUS";
|
||||
= App.PACKAGE_NAME + ".player.MainPlayer.ACTION_PLAY_PREVIOUS";
|
||||
static final String ACTION_FAST_REWIND
|
||||
= "org.schabi.newpipe.player.MainPlayer.ACTION_FAST_REWIND";
|
||||
= App.PACKAGE_NAME + ".player.MainPlayer.ACTION_FAST_REWIND";
|
||||
static final String ACTION_FAST_FORWARD
|
||||
= "org.schabi.newpipe.player.MainPlayer.ACTION_FAST_FORWARD";
|
||||
= App.PACKAGE_NAME + ".player.MainPlayer.ACTION_FAST_FORWARD";
|
||||
static final String ACTION_SHUFFLE
|
||||
= "org.schabi.newpipe.player.MainPlayer.ACTION_SHUFFLE";
|
||||
= App.PACKAGE_NAME + ".player.MainPlayer.ACTION_SHUFFLE";
|
||||
public static final String ACTION_RECREATE_NOTIFICATION
|
||||
= "org.schabi.newpipe.player.MainPlayer.ACTION_RECREATE_NOTIFICATION";
|
||||
= App.PACKAGE_NAME + ".player.MainPlayer.ACTION_RECREATE_NOTIFICATION";
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Service's LifeCycle
|
||||
@@ -101,13 +101,12 @@ public final class MainPlayer extends Service {
|
||||
}
|
||||
|
||||
private void createView() {
|
||||
final View layout = View.inflate(this, R.layout.player, null);
|
||||
final PlayerBinding binding = PlayerBinding.inflate(LayoutInflater.from(this));
|
||||
|
||||
playerImpl = new VideoPlayerImpl(this);
|
||||
playerImpl.setup(layout);
|
||||
playerImpl.shouldUpdateOnProgress = true;
|
||||
player = new Player(this);
|
||||
player.setupFromView(binding);
|
||||
|
||||
NotificationUtil.getInstance().createNotificationAndStartForeground(playerImpl, this);
|
||||
NotificationUtil.getInstance().createNotificationAndStartForeground(player, this);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -117,19 +116,19 @@ public final class MainPlayer extends Service {
|
||||
+ "], flags = [" + flags + "], startId = [" + startId + "]");
|
||||
}
|
||||
if (Intent.ACTION_MEDIA_BUTTON.equals(intent.getAction())
|
||||
&& playerImpl.playQueue == null) {
|
||||
&& player.getPlayQueue() == null) {
|
||||
// Player is not working, no need to process media button's action
|
||||
return START_NOT_STICKY;
|
||||
}
|
||||
|
||||
if (Intent.ACTION_MEDIA_BUTTON.equals(intent.getAction())
|
||||
|| intent.getStringExtra(VideoPlayer.PLAY_QUEUE_KEY) != null) {
|
||||
NotificationUtil.getInstance().createNotificationAndStartForeground(playerImpl, this);
|
||||
|| intent.getStringExtra(Player.PLAY_QUEUE_KEY) != null) {
|
||||
NotificationUtil.getInstance().createNotificationAndStartForeground(player, this);
|
||||
}
|
||||
|
||||
playerImpl.handleIntent(intent);
|
||||
if (playerImpl.mediaSessionManager != null) {
|
||||
playerImpl.mediaSessionManager.handleMediaButtonIntent(intent);
|
||||
player.handleIntent(intent);
|
||||
if (player.getMediaSessionManager() != null) {
|
||||
player.getMediaSessionManager().handleMediaButtonIntent(intent);
|
||||
}
|
||||
return START_NOT_STICKY;
|
||||
}
|
||||
@@ -139,20 +138,20 @@ public final class MainPlayer extends Service {
|
||||
Log.d(TAG, "stop() called");
|
||||
}
|
||||
|
||||
if (playerImpl.getPlayer() != null) {
|
||||
playerImpl.wasPlaying = playerImpl.getPlayer().getPlayWhenReady();
|
||||
if (!player.exoPlayerIsNull()) {
|
||||
player.saveWasPlaying();
|
||||
// Releases wifi & cpu, disables keepScreenOn, etc.
|
||||
if (!autoplayEnabled) {
|
||||
playerImpl.onPause();
|
||||
player.pause();
|
||||
}
|
||||
// We can't just pause the player here because it will make transition
|
||||
// from one stream to a new stream not smooth
|
||||
playerImpl.getPlayer().stop(false);
|
||||
playerImpl.setRecovery();
|
||||
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)
|
||||
playerImpl.hideControls(0, 0);
|
||||
playerImpl.onQueueClosed();
|
||||
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.
|
||||
@@ -166,7 +165,7 @@ public final class MainPlayer extends Service {
|
||||
@Override
|
||||
public void onTaskRemoved(final Intent rootIntent) {
|
||||
super.onTaskRemoved(rootIntent);
|
||||
if (!playerImpl.videoPlayerSelected()) {
|
||||
if (!player.videoPlayerSelected()) {
|
||||
return;
|
||||
}
|
||||
onDestroy();
|
||||
@@ -179,7 +178,23 @@ public final class MainPlayer extends Service {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "destroy() called");
|
||||
}
|
||||
onClose();
|
||||
|
||||
if (player != null) {
|
||||
// Exit from fullscreen when user closes the player via notification
|
||||
if (player.isFullscreen()) {
|
||||
player.toggleFullscreen();
|
||||
}
|
||||
removeViewFromParent();
|
||||
|
||||
player.saveStreamProgressState();
|
||||
player.setRecovery();
|
||||
player.stopActivityBinding();
|
||||
player.removePopupFromView();
|
||||
player.destroy();
|
||||
}
|
||||
|
||||
NotificationUtil.getInstance().cancelNotificationAndStopForeground(this);
|
||||
stopSelf();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -192,32 +207,6 @@ public final class MainPlayer extends Service {
|
||||
return mBinder;
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Actions
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
private void onClose() {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "onClose() called");
|
||||
}
|
||||
|
||||
if (playerImpl != null) {
|
||||
// Exit from fullscreen when user closes the player via notification
|
||||
if (playerImpl.isFullscreen()) {
|
||||
playerImpl.toggleFullscreen();
|
||||
}
|
||||
removeViewFromParent();
|
||||
|
||||
playerImpl.setRecovery();
|
||||
playerImpl.savePlaybackState();
|
||||
playerImpl.stopActivityBinding();
|
||||
playerImpl.removePopupFromView();
|
||||
playerImpl.destroy();
|
||||
}
|
||||
|
||||
NotificationUtil.getInstance().cancelNotificationAndStopForeground(this);
|
||||
stopSelf();
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Utils
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
@@ -225,25 +214,25 @@ 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 = (playerImpl != null
|
||||
&& playerImpl.getParentActivity() != null
|
||||
? playerImpl.getParentActivity().getResources()
|
||||
final DisplayMetrics metrics = (player != null
|
||||
&& player.getParentActivity() != null
|
||||
? player.getParentActivity().getResources()
|
||||
: getResources()).getDisplayMetrics();
|
||||
return metrics.heightPixels < metrics.widthPixels;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public View getView() {
|
||||
if (playerImpl == null) {
|
||||
if (player == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return playerImpl.getRootView();
|
||||
return player.getRootView();
|
||||
}
|
||||
|
||||
public void removeViewFromParent() {
|
||||
if (getView() != null && getView().getParent() != null) {
|
||||
if (playerImpl.getParentActivity() != null) {
|
||||
if (player.getParentActivity() != null) {
|
||||
// This means view was added to fragment
|
||||
final ViewGroup parent = (ViewGroup) getView().getParent();
|
||||
parent.removeView(getView());
|
||||
@@ -261,8 +250,8 @@ public final class MainPlayer extends Service {
|
||||
return MainPlayer.this;
|
||||
}
|
||||
|
||||
public VideoPlayerImpl getPlayer() {
|
||||
return MainPlayer.this.playerImpl;
|
||||
public Player getPlayer() {
|
||||
return MainPlayer.this.player;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ import androidx.annotation.Nullable;
|
||||
import androidx.annotation.StringRes;
|
||||
import androidx.core.app.NotificationCompat;
|
||||
import androidx.core.app.NotificationManagerCompat;
|
||||
import androidx.core.app.ServiceCompat;
|
||||
import androidx.core.content.ContextCompat;
|
||||
|
||||
import org.schabi.newpipe.MainActivity;
|
||||
@@ -42,7 +43,7 @@ import static org.schabi.newpipe.player.MainPlayer.ACTION_SHUFFLE;
|
||||
*/
|
||||
public final class NotificationUtil {
|
||||
private static final String TAG = NotificationUtil.class.getSimpleName();
|
||||
private static final boolean DEBUG = BasePlayer.DEBUG;
|
||||
private static final boolean DEBUG = Player.DEBUG;
|
||||
private static final int NOTIFICATION_ID = 123789;
|
||||
|
||||
@Nullable private static NotificationUtil instance = null;
|
||||
@@ -75,7 +76,7 @@ public final class NotificationUtil {
|
||||
* @param forceRecreate whether to force the recreation of the notification even if it already
|
||||
* exists
|
||||
*/
|
||||
synchronized void createNotificationIfNeededAndUpdate(final VideoPlayerImpl player,
|
||||
synchronized void createNotificationIfNeededAndUpdate(final Player player,
|
||||
final boolean forceRecreate) {
|
||||
if (forceRecreate || notificationBuilder == null) {
|
||||
notificationBuilder = createNotification(player);
|
||||
@@ -84,14 +85,14 @@ public final class NotificationUtil {
|
||||
notificationManager.notify(NOTIFICATION_ID, notificationBuilder.build());
|
||||
}
|
||||
|
||||
private synchronized NotificationCompat.Builder createNotification(
|
||||
final VideoPlayerImpl player) {
|
||||
private synchronized NotificationCompat.Builder createNotification(final Player player) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "createNotification()");
|
||||
}
|
||||
notificationManager = NotificationManagerCompat.from(player.context);
|
||||
final NotificationCompat.Builder builder = new NotificationCompat.Builder(player.context,
|
||||
player.context.getString(R.string.notification_channel_id));
|
||||
notificationManager = NotificationManagerCompat.from(player.getContext());
|
||||
final NotificationCompat.Builder builder =
|
||||
new NotificationCompat.Builder(player.getContext(),
|
||||
player.getContext().getString(R.string.notification_channel_id));
|
||||
|
||||
initializeNotificationSlots(player);
|
||||
|
||||
@@ -106,25 +107,25 @@ public final class NotificationUtil {
|
||||
|
||||
// build the compact slot indices array (need code to convert from Integer... because Java)
|
||||
final List<Integer> compactSlotList = NotificationConstants.getCompactSlotsFromPreferences(
|
||||
player.context, player.sharedPreferences, nonNothingSlotCount);
|
||||
player.getContext(), player.getPrefs(), nonNothingSlotCount);
|
||||
final int[] compactSlots = new int[compactSlotList.size()];
|
||||
for (int i = 0; i < compactSlotList.size(); i++) {
|
||||
compactSlots[i] = compactSlotList.get(i);
|
||||
}
|
||||
|
||||
builder.setStyle(new androidx.media.app.NotificationCompat.MediaStyle()
|
||||
.setMediaSession(player.mediaSessionManager.getSessionToken())
|
||||
.setMediaSession(player.getMediaSessionManager().getSessionToken())
|
||||
.setShowActionsInCompactView(compactSlots))
|
||||
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
||||
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
|
||||
.setCategory(NotificationCompat.CATEGORY_TRANSPORT)
|
||||
.setShowWhen(false)
|
||||
.setSmallIcon(R.drawable.ic_newpipe_triangle_white)
|
||||
.setColor(ContextCompat.getColor(player.context, R.color.dark_background_color))
|
||||
.setColorized(player.sharedPreferences.getBoolean(
|
||||
player.context.getString(R.string.notification_colorize_key),
|
||||
true))
|
||||
.setDeleteIntent(PendingIntent.getBroadcast(player.context, NOTIFICATION_ID,
|
||||
.setColor(ContextCompat.getColor(player.getContext(),
|
||||
R.color.dark_background_color))
|
||||
.setColorized(player.getPrefs().getBoolean(
|
||||
player.getContext().getString(R.string.notification_colorize_key), true))
|
||||
.setDeleteIntent(PendingIntent.getBroadcast(player.getContext(), NOTIFICATION_ID,
|
||||
new Intent(ACTION_CLOSE), FLAG_UPDATE_CURRENT));
|
||||
|
||||
return builder;
|
||||
@@ -134,20 +135,20 @@ public final class NotificationUtil {
|
||||
* Updates the notification builder and the button icons depending on the playback state.
|
||||
* @param player the player currently open, to take data from
|
||||
*/
|
||||
private synchronized void updateNotification(final VideoPlayerImpl player) {
|
||||
private synchronized void updateNotification(final Player player) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "updateNotification()");
|
||||
}
|
||||
|
||||
// also update content intent, in case the user switched players
|
||||
notificationBuilder.setContentIntent(PendingIntent.getActivity(player.context,
|
||||
notificationBuilder.setContentIntent(PendingIntent.getActivity(player.getContext(),
|
||||
NOTIFICATION_ID, getIntentForNotification(player), FLAG_UPDATE_CURRENT));
|
||||
notificationBuilder.setContentTitle(player.getVideoTitle());
|
||||
notificationBuilder.setContentText(player.getUploaderName());
|
||||
notificationBuilder.setTicker(player.getVideoTitle());
|
||||
updateActions(notificationBuilder, player);
|
||||
final boolean showThumbnail = player.sharedPreferences.getBoolean(
|
||||
player.context.getString(R.string.show_thumbnail_key), true);
|
||||
final boolean showThumbnail = player.getPrefs().getBoolean(
|
||||
player.getContext().getString(R.string.show_thumbnail_key), true);
|
||||
if (showThumbnail) {
|
||||
setLargeIcon(notificationBuilder, player);
|
||||
}
|
||||
@@ -173,7 +174,7 @@ public final class NotificationUtil {
|
||||
}
|
||||
|
||||
|
||||
void createNotificationAndStartForeground(final VideoPlayerImpl player, final Service service) {
|
||||
void createNotificationAndStartForeground(final Player player, final Service service) {
|
||||
if (notificationBuilder == null) {
|
||||
notificationBuilder = createNotification(player);
|
||||
}
|
||||
@@ -188,7 +189,7 @@ public final class NotificationUtil {
|
||||
}
|
||||
|
||||
void cancelNotificationAndStopForeground(final Service service) {
|
||||
service.stopForeground(true);
|
||||
ServiceCompat.stopForeground(service, ServiceCompat.STOP_FOREGROUND_REMOVE);
|
||||
|
||||
if (notificationManager != null) {
|
||||
notificationManager.cancel(NOTIFICATION_ID);
|
||||
@@ -202,17 +203,16 @@ public final class NotificationUtil {
|
||||
// ACTIONS
|
||||
/////////////////////////////////////////////////////
|
||||
|
||||
private void initializeNotificationSlots(final VideoPlayerImpl player) {
|
||||
private void initializeNotificationSlots(final Player player) {
|
||||
for (int i = 0; i < 5; ++i) {
|
||||
notificationSlots[i] = player.sharedPreferences.getInt(
|
||||
player.context.getString(NotificationConstants.SLOT_PREF_KEYS[i]),
|
||||
notificationSlots[i] = player.getPrefs().getInt(
|
||||
player.getContext().getString(NotificationConstants.SLOT_PREF_KEYS[i]),
|
||||
NotificationConstants.SLOT_DEFAULTS[i]);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("RestrictedApi")
|
||||
private void updateActions(final NotificationCompat.Builder builder,
|
||||
final VideoPlayerImpl player) {
|
||||
private void updateActions(final NotificationCompat.Builder builder, final Player player) {
|
||||
builder.mActions.clear();
|
||||
for (int i = 0; i < 5; ++i) {
|
||||
addAction(builder, player, notificationSlots[i]);
|
||||
@@ -220,7 +220,7 @@ public final class NotificationUtil {
|
||||
}
|
||||
|
||||
private void addAction(final NotificationCompat.Builder builder,
|
||||
final VideoPlayerImpl player,
|
||||
final Player player,
|
||||
@NotificationConstants.Action final int slot) {
|
||||
final NotificationCompat.Action action = getAction(player, slot);
|
||||
if (action != null) {
|
||||
@@ -230,7 +230,7 @@ public final class NotificationUtil {
|
||||
|
||||
@Nullable
|
||||
private NotificationCompat.Action getAction(
|
||||
final VideoPlayerImpl player,
|
||||
final Player player,
|
||||
@NotificationConstants.Action final int selectedAction) {
|
||||
final int baseActionIcon = NotificationConstants.ACTION_ICONS[selectedAction];
|
||||
switch (selectedAction) {
|
||||
@@ -251,7 +251,7 @@ public final class NotificationUtil {
|
||||
R.string.exo_controls_fastforward_description, ACTION_FAST_FORWARD);
|
||||
|
||||
case NotificationConstants.SMART_REWIND_PREVIOUS:
|
||||
if (player.playQueue != null && player.playQueue.size() > 1) {
|
||||
if (player.getPlayQueue() != null && player.getPlayQueue().size() > 1) {
|
||||
return getAction(player, R.drawable.exo_notification_previous,
|
||||
R.string.exo_controls_previous_description, ACTION_PLAY_PREVIOUS);
|
||||
} else {
|
||||
@@ -260,7 +260,7 @@ public final class NotificationUtil {
|
||||
}
|
||||
|
||||
case NotificationConstants.SMART_FORWARD_NEXT:
|
||||
if (player.playQueue != null && player.playQueue.size() > 1) {
|
||||
if (player.getPlayQueue() != null && player.getPlayQueue().size() > 1) {
|
||||
return getAction(player, R.drawable.exo_notification_next,
|
||||
R.string.exo_controls_next_description, ACTION_PLAY_NEXT);
|
||||
} else {
|
||||
@@ -269,23 +269,23 @@ public final class NotificationUtil {
|
||||
}
|
||||
|
||||
case NotificationConstants.PLAY_PAUSE_BUFFERING:
|
||||
if (player.getCurrentState() == BasePlayer.STATE_PREFLIGHT
|
||||
|| player.getCurrentState() == BasePlayer.STATE_BLOCKED
|
||||
|| player.getCurrentState() == BasePlayer.STATE_BUFFERING) {
|
||||
if (player.getCurrentState() == Player.STATE_PREFLIGHT
|
||||
|| player.getCurrentState() == Player.STATE_BLOCKED
|
||||
|| player.getCurrentState() == Player.STATE_BUFFERING) {
|
||||
// null intent -> show hourglass icon that does nothing when clicked
|
||||
return new NotificationCompat.Action(R.drawable.ic_hourglass_top_white_24dp_png,
|
||||
player.context.getString(R.string.notification_action_buffering),
|
||||
player.getContext().getString(R.string.notification_action_buffering),
|
||||
null);
|
||||
}
|
||||
|
||||
case NotificationConstants.PLAY_PAUSE:
|
||||
if (player.getCurrentState() == BasePlayer.STATE_COMPLETED) {
|
||||
if (player.getCurrentState() == Player.STATE_COMPLETED) {
|
||||
return getAction(player, R.drawable.ic_replay_white_24dp_png,
|
||||
R.string.exo_controls_pause_description, ACTION_PLAY_PAUSE);
|
||||
} else if (player.isPlaying()
|
||||
|| player.getCurrentState() == BasePlayer.STATE_PREFLIGHT
|
||||
|| player.getCurrentState() == BasePlayer.STATE_BLOCKED
|
||||
|| player.getCurrentState() == BasePlayer.STATE_BUFFERING) {
|
||||
|| player.getCurrentState() == Player.STATE_PREFLIGHT
|
||||
|| player.getCurrentState() == Player.STATE_BLOCKED
|
||||
|| player.getCurrentState() == Player.STATE_BUFFERING) {
|
||||
return getAction(player, R.drawable.exo_notification_pause,
|
||||
R.string.exo_controls_pause_description, ACTION_PLAY_PAUSE);
|
||||
} else {
|
||||
@@ -306,7 +306,7 @@ public final class NotificationUtil {
|
||||
}
|
||||
|
||||
case NotificationConstants.SHUFFLE:
|
||||
if (player.playQueue != null && player.playQueue.isShuffled()) {
|
||||
if (player.getPlayQueue() != null && player.getPlayQueue().isShuffled()) {
|
||||
return getAction(player, R.drawable.exo_controls_shuffle_on,
|
||||
R.string.exo_controls_shuffle_on_description, ACTION_SHUFFLE);
|
||||
} else {
|
||||
@@ -325,23 +325,23 @@ public final class NotificationUtil {
|
||||
}
|
||||
}
|
||||
|
||||
private NotificationCompat.Action getAction(final VideoPlayerImpl player,
|
||||
private NotificationCompat.Action getAction(final Player player,
|
||||
@DrawableRes final int drawable,
|
||||
@StringRes final int title,
|
||||
final String intentAction) {
|
||||
return new NotificationCompat.Action(drawable, player.context.getString(title),
|
||||
PendingIntent.getBroadcast(player.context, NOTIFICATION_ID,
|
||||
return new NotificationCompat.Action(drawable, player.getContext().getString(title),
|
||||
PendingIntent.getBroadcast(player.getContext(), NOTIFICATION_ID,
|
||||
new Intent(intentAction), FLAG_UPDATE_CURRENT));
|
||||
}
|
||||
|
||||
private Intent getIntentForNotification(final VideoPlayerImpl player) {
|
||||
private Intent getIntentForNotification(final Player player) {
|
||||
if (player.audioPlayerSelected() || player.popupPlayerSelected()) {
|
||||
// Means we play in popup or audio only. Let's show the play queue
|
||||
return NavigationHelper.getPlayQueueActivityIntent(player.context);
|
||||
return NavigationHelper.getPlayQueueActivityIntent(player.getContext());
|
||||
} else {
|
||||
// We are playing in fragment. Don't open another activity just show fragment. That's it
|
||||
final Intent intent = NavigationHelper.getPlayerIntent(
|
||||
player.context, MainActivity.class, null, true);
|
||||
player.getContext(), MainActivity.class, null, true);
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
intent.setAction(Intent.ACTION_MAIN);
|
||||
intent.addCategory(Intent.CATEGORY_LAUNCHER);
|
||||
@@ -354,10 +354,9 @@ public final class NotificationUtil {
|
||||
// BITMAP
|
||||
/////////////////////////////////////////////////////
|
||||
|
||||
private void setLargeIcon(final NotificationCompat.Builder builder,
|
||||
final VideoPlayerImpl player) {
|
||||
final boolean scaleImageToSquareAspectRatio = player.sharedPreferences.getBoolean(
|
||||
player.context.getString(R.string.scale_to_square_image_in_notifications_key),
|
||||
private void setLargeIcon(final NotificationCompat.Builder builder, final Player player) {
|
||||
final boolean scaleImageToSquareAspectRatio = player.getPrefs().getBoolean(
|
||||
player.getContext().getString(R.string.scale_to_square_image_in_notifications_key),
|
||||
false);
|
||||
if (scaleImageToSquareAspectRatio) {
|
||||
builder.setLargeIcon(getBitmapWithSquareAspectRatio(player.getThumbnail()));
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user