mirror of
https://github.com/TeamNewPipe/NewPipe
synced 2026-01-13 18:22:41 +00:00
Compare commits
371 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0ae04b8ead | ||
|
|
5d59025b3c | ||
|
|
768bb0bbcd | ||
|
|
ac071b383f | ||
|
|
b79ea7b51b | ||
|
|
28c72e7f63 | ||
|
|
5fcc3b4dab | ||
|
|
7eb13a9b93 | ||
|
|
7c9896beaf | ||
|
|
54d3bff26d | ||
|
|
e0ba9b3902 | ||
|
|
f11b5ae7a1 | ||
|
|
7baeb6eca7 | ||
|
|
4e8519a1b9 | ||
|
|
e6fffc0d5b | ||
|
|
4c5fc7fa7c | ||
|
|
b633108a4c | ||
|
|
3d42da5ff5 | ||
|
|
1b869199f4 | ||
|
|
f3cd2f6c9d | ||
|
|
2e3e7f9bf2 | ||
|
|
92327dd9e3 | ||
|
|
d40b432f46 | ||
|
|
5b3137093f | ||
|
|
4fc9f2e5fd | ||
|
|
ce592f4baf | ||
|
|
2b3edcf2d1 | ||
|
|
f165f97bd9 | ||
|
|
4ec572372e | ||
|
|
672eb34049 | ||
|
|
a0b042091b | ||
|
|
b753705a84 | ||
|
|
f48ff610a3 | ||
|
|
93aed9f34c | ||
|
|
3cf94382e6 | ||
|
|
f52cb3bbe0 | ||
|
|
d45182cb5c | ||
|
|
22847c6c92 | ||
|
|
a70c51b71c | ||
|
|
02d417476e | ||
|
|
bc3139e5f9 | ||
|
|
c1f7b2653c | ||
|
|
72dbb9441e | ||
|
|
bbc13756f3 | ||
|
|
ba0876b43b | ||
|
|
c0d41661e8 | ||
|
|
b2e2551e33 | ||
|
|
ac371e6fb4 | ||
|
|
a225ac5deb | ||
|
|
920695f90a | ||
|
|
49fc57eee9 | ||
|
|
b61d44aaa6 | ||
|
|
f36fd2f7b2 | ||
|
|
7e26748dc4 | ||
|
|
ba6fdecbae | ||
|
|
f791e83380 | ||
|
|
dd7f914b8d | ||
|
|
7667b2ce59 | ||
|
|
8272b2508b | ||
|
|
70354eb73e | ||
|
|
63083ac0c3 | ||
|
|
9346f9b0f3 | ||
|
|
605e5d265c | ||
|
|
25456b15e7 | ||
|
|
ebbe7ef944 | ||
|
|
60a272e70a | ||
|
|
672fcb9ce3 | ||
|
|
870d50ebcd | ||
|
|
b62b3e91a0 | ||
|
|
b022d90303 | ||
|
|
02af529551 | ||
|
|
dd9cc619ed | ||
|
|
75c9e959de | ||
|
|
fb8afec1bf | ||
|
|
a2887034a6 | ||
|
|
7eb5aa1bc5 | ||
|
|
08ebd7d39a | ||
|
|
9ea263f72e | ||
|
|
e4a2d2f3c1 | ||
|
|
892b4a15f6 | ||
|
|
fda0a550fd | ||
|
|
638825cdff | ||
|
|
6a1d81fcf3 | ||
|
|
8afd44a72f | ||
|
|
22c5135740 | ||
|
|
4d51ebc37a | ||
|
|
433c6dc33b | ||
|
|
ed4fdadd4d | ||
|
|
298e96b821 | ||
|
|
9006667b4d | ||
|
|
abbf71982d | ||
|
|
57110717d3 | ||
|
|
c3b5444281 | ||
|
|
7a542975ca | ||
|
|
490aff5846 | ||
|
|
1dfc036ead | ||
|
|
360d6b998c | ||
|
|
be7307cf39 | ||
|
|
12096ab050 | ||
|
|
225f23ce02 | ||
|
|
9c15ee7285 | ||
|
|
8dd617fc6b | ||
|
|
ae8e72f34b | ||
|
|
fc52a6e871 | ||
|
|
722b47b86f | ||
|
|
3a09039b93 | ||
|
|
669a35bc78 | ||
|
|
81fa0c1558 | ||
|
|
ed408b2094 | ||
|
|
3bc661f583 | ||
|
|
cf9b482be2 | ||
|
|
1d935b46f9 | ||
|
|
520ac2e935 | ||
|
|
c6316abbce | ||
|
|
2dfe837c35 | ||
|
|
3c2ea7697c | ||
|
|
faa7a91764 | ||
|
|
f629a4d206 | ||
|
|
4b7c37e919 | ||
|
|
a4c9732916 | ||
|
|
f8f2dfce4b | ||
|
|
5284072b8d | ||
|
|
e603dddc54 | ||
|
|
15691ba41a | ||
|
|
a555aab3e7 | ||
|
|
88f1c3a808 | ||
|
|
0e6668636d | ||
|
|
d0f4d8b132 | ||
|
|
cfdcb92fa3 | ||
|
|
039bd5d413 | ||
|
|
5ffba55b4a | ||
|
|
57ca281c80 | ||
|
|
46f74b908a | ||
|
|
703f1550d8 | ||
|
|
8bfd380b89 | ||
|
|
43e91ae4ae | ||
|
|
023a2c1d9c | ||
|
|
d931d058d9 | ||
|
|
a825253b7f | ||
|
|
d9086300f3 | ||
|
|
f18a7c91ca | ||
|
|
556aad0114 | ||
|
|
05f6ea6401 | ||
|
|
43d0543b9f | ||
|
|
e95637f7b7 | ||
|
|
4cd7c42b9e | ||
|
|
0787d62254 | ||
|
|
b061423847 | ||
|
|
dbd90299bd | ||
|
|
1faf1b261c | ||
|
|
c6ead351c0 | ||
|
|
bbcfdf2969 | ||
|
|
36e72d5a41 | ||
|
|
f8297a8a9b | ||
|
|
a4503eb609 | ||
|
|
a1cb3e59d6 | ||
|
|
ef94458249 | ||
|
|
1b05c404d5 | ||
|
|
5de455bb86 | ||
|
|
acdfee5c25 | ||
|
|
a6d6ed6474 | ||
|
|
87e7d95966 | ||
|
|
d37ee1e0dc | ||
|
|
1d33e7ab49 | ||
|
|
2027b743b4 | ||
|
|
7e27e73532 | ||
|
|
3705a1adad | ||
|
|
793b88a7d4 | ||
|
|
2928df0cc9 | ||
|
|
4f5e772157 | ||
|
|
f7a0b9951e | ||
|
|
44128f9145 | ||
|
|
6eaff5ca6a | ||
|
|
c0664c1cb6 | ||
|
|
e229e5355d | ||
|
|
52189fc5df | ||
|
|
314964c5f9 | ||
|
|
fcef783bbb | ||
|
|
9c5ac069d7 | ||
|
|
160f9df64e | ||
|
|
bdbb9bead2 | ||
|
|
e4dfce9ee2 | ||
|
|
6fbb601802 | ||
|
|
94b4c76749 | ||
|
|
8715e7dd98 | ||
|
|
ccc2d892c1 | ||
|
|
d1ce8e7baa | ||
|
|
82fbbbecac | ||
|
|
bf029ddd9f | ||
|
|
af5f0c042a | ||
|
|
4e15f0ddac | ||
|
|
b566355c4f | ||
|
|
5c31dff72d | ||
|
|
d69672e113 | ||
|
|
a209e87c69 | ||
|
|
71610a365f | ||
|
|
44860f2ea7 | ||
|
|
967bdf8f08 | ||
|
|
02aa6fcab0 | ||
|
|
712985ced1 | ||
|
|
0683dafa55 | ||
|
|
6f1958d398 | ||
|
|
85fbd2560d | ||
|
|
65f2730261 | ||
|
|
21bcadeecb | ||
|
|
bd0427c79f | ||
|
|
241054fd26 | ||
|
|
d8888e3495 | ||
|
|
137d9e6d6e | ||
|
|
d0cbd1e663 | ||
|
|
da51e1ed72 | ||
|
|
76803bfcb1 | ||
|
|
c248741c00 | ||
|
|
759a078ce0 | ||
|
|
a536311d56 | ||
|
|
9dd2a82b7d | ||
|
|
4d50a66e40 | ||
|
|
e6c56cacc6 | ||
|
|
c3b9465aa3 | ||
|
|
5f3b8bea52 | ||
|
|
0e4c8ea8af | ||
|
|
f9ab23bb4a | ||
|
|
9f8b2264a2 | ||
|
|
52cc3f10c1 | ||
|
|
1d61bb58f5 | ||
|
|
a3440cc8ef | ||
|
|
51c60e5261 | ||
|
|
c3349e18a5 | ||
|
|
12e46e0a36 | ||
|
|
f8caed139a | ||
|
|
a2297fb5b8 | ||
|
|
26c39381a8 | ||
|
|
a4742ad9e9 | ||
|
|
23a6973291 | ||
|
|
340a84e583 | ||
|
|
4291877830 | ||
|
|
c7f75bf7d1 | ||
|
|
4bf5ddbfe9 | ||
|
|
32dffb577c | ||
|
|
a9623f8e6a | ||
|
|
bc74bb6bf6 | ||
|
|
d32450255c | ||
|
|
896aec5295 | ||
|
|
d42a534fc3 | ||
|
|
398007ca90 | ||
|
|
551e8df8b8 | ||
|
|
dc0a28b93d | ||
|
|
644396149b | ||
|
|
a25bb2618a | ||
|
|
0e12cdea7c | ||
|
|
903296014a | ||
|
|
cd713db029 | ||
|
|
bdd16e06e0 | ||
|
|
4c632810ec | ||
|
|
f451bdbfa4 | ||
|
|
bfac73b992 | ||
|
|
2b41f710a8 | ||
|
|
5924edb289 | ||
|
|
5ceec31adf | ||
|
|
e2791cdf0f | ||
|
|
50f3b08c59 | ||
|
|
2aebf6ceaf | ||
|
|
7ceea2cd8d | ||
|
|
0cb801179c | ||
|
|
1822d21676 | ||
|
|
7fd2ebc252 | ||
|
|
f709ac16f8 | ||
|
|
74173317de | ||
|
|
3874e16187 | ||
|
|
39722a5563 | ||
|
|
1f9ad12593 | ||
|
|
52c136439e | ||
|
|
cd86ed3877 | ||
|
|
1d85661ab9 | ||
|
|
736cefed5a | ||
|
|
fa8630ddae | ||
|
|
4a2bd7bd7b | ||
|
|
a9e21a35ea | ||
|
|
fd4e1b8d2c | ||
|
|
420f0505ae | ||
|
|
c422f65935 | ||
|
|
63fdc100d6 | ||
|
|
9e2ece78dd | ||
|
|
cebcaf4d6a | ||
|
|
4a242e43a7 | ||
|
|
d8f442cc89 | ||
|
|
f6923e073e | ||
|
|
f02c6be10d | ||
|
|
5ba3ef0a25 | ||
|
|
9458b9f37d | ||
|
|
ca282f2be8 | ||
|
|
0cde08c46e | ||
|
|
bec8512c7b | ||
|
|
46e7da4e21 | ||
|
|
c7b8bd3436 | ||
|
|
1721817fdb | ||
|
|
d57bfde604 | ||
|
|
3167ab3ba0 | ||
|
|
8f559965f6 | ||
|
|
35e005caaa | ||
|
|
6c25ce56a3 | ||
|
|
baa12c7069 | ||
|
|
e2b044d2ee | ||
|
|
621af8d812 | ||
|
|
efd038a536 | ||
|
|
0b2629e910 | ||
|
|
a9b5ef3bd3 | ||
|
|
2a24532e1d | ||
|
|
88c4195260 | ||
|
|
c5f2eb1dd8 | ||
|
|
384d964827 | ||
|
|
253526e565 | ||
|
|
2e2dbaf77f | ||
|
|
43133df2ad | ||
|
|
eef568b24c | ||
|
|
e7d5011f42 | ||
|
|
36c198fc33 | ||
|
|
75a8edf20f | ||
|
|
81107df53f | ||
|
|
a932bc2503 | ||
|
|
f4e2eca256 | ||
|
|
08d5dfa49c | ||
|
|
e7f339a946 | ||
|
|
d3375a921d | ||
|
|
a2eb810df0 | ||
|
|
6e576a165c | ||
|
|
dfa941a9e7 | ||
|
|
1584028995 | ||
|
|
14dab85ff0 | ||
|
|
403e336a64 | ||
|
|
2aa5f68b7b | ||
|
|
56ea526cce | ||
|
|
96f5cd9f17 | ||
|
|
64efb89cce | ||
|
|
4d5b68792b | ||
|
|
85d813a94b | ||
|
|
e9b008ee84 | ||
|
|
79102a20d2 | ||
|
|
2e053ea25a | ||
|
|
6711dae4e0 | ||
|
|
85e864a01e | ||
|
|
573839c0ff | ||
|
|
9c636f5ee2 | ||
|
|
f78d2a5ed8 | ||
|
|
48c2c156cb | ||
|
|
435813355f | ||
|
|
e30a552b6c | ||
|
|
22a4a4b2df | ||
|
|
aaa3e20c5a | ||
|
|
cb1a138140 | ||
|
|
afe06b379f | ||
|
|
08d4651ef0 | ||
|
|
02b0909829 | ||
|
|
ae39b31c68 | ||
|
|
e5a1438673 | ||
|
|
72d305b283 | ||
|
|
785c0376f8 | ||
|
|
0bdf8de38e | ||
|
|
9767e98e50 | ||
|
|
0782410a14 | ||
|
|
f5d015e8f9 | ||
|
|
f00cffd17e | ||
|
|
40a2df847b | ||
|
|
fa1d7ffac3 | ||
|
|
272d589518 | ||
|
|
6ab4787e97 | ||
|
|
060f09ff55 | ||
|
|
f47ae3668f | ||
|
|
7fdb6e1425 | ||
|
|
621f049a5c | ||
|
|
eb6968fb3f |
4
.github/CONTRIBUTING.md
vendored
4
.github/CONTRIBUTING.md
vendored
@@ -39,6 +39,10 @@ You'll see *exactly* what is sent, be able to add **your comments**, and then se
|
||||
* Create PRs that cover only **one specific issue/solution/bug**. Do not create PRs that are huge monoliths and could have been split into multiple independent contributions.
|
||||
* NewPipe uses [NewPipeExtractor](https://github.com/TeamNewPipe/NewPipeExtractor) to fetch data from services. If you need to change something there, you must test your changes in NewPipe. Telling NewPipe to use your extractor version can be accomplished by editing the `app/build.gradle` file: the comments under the "NewPipe libraries" section of `dependencies` will help you out.
|
||||
|
||||
### Kotlin in NewPipe
|
||||
* NewPipe will remain mostly Java for time being
|
||||
* Contributions containing a simple conversion from Java to Kotlin should be avoided. Conversions to Kotlin should only be done if Kotlin actually brings improvements like bug fixes or better performance which are not, or only with much more effort, implementable in Java. The core team sees Java as an easier to learn and generally well adopted programming language.
|
||||
|
||||
### Creating a Pull Request (PR)
|
||||
|
||||
* Make changes on a **separate branch** with a meaningful name, not on the _master_ branch or the _dev_ branch. This is commonly known as *feature branch workflow*. You may then send your changes as a pull request (PR) on GitHub.
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/bug_report.md
vendored
2
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -57,7 +57,7 @@ Oh no, a bug! It happens. Thanks for reporting an issue with NewPipe. To make it
|
||||
|
||||
|
||||
|
||||
<!-- Please fill this out when you do not provide a log generate by NewPipe -->
|
||||
<!-- Please fill this section if you did not provide a log generated by NewPipe -->
|
||||
|
||||
### Device info
|
||||
|
||||
|
||||
34
.github/ISSUE_TEMPLATE/feature_request.md
vendored
34
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@@ -5,10 +5,9 @@ labels: enhancement
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
<!-- IF YOU DON'T FILL IN THE TEMPLATE PROPERLY, YOUR ISSUE IS LIABLE TO BE CLOSED. If you feel tired/lazy right now, open your issue some other time. We'll wait. -->
|
||||
<!-- IF YOU DON'T FILL IN THE TEMPLATE PROPERLY, YOUR ISSUE IS LIABLE TO BE CLOSED. If you are currently unable to do so for any reason, open your issue some other time. We'll wait. -->
|
||||
|
||||
|
||||
<!-- The comments between these brackets won't show up in the submitted issue (as you can see in the Preview). -->
|
||||
<!-- The comments between these brackets won't show up in the submitted issue (as you can see in the Preview tab). -->
|
||||
|
||||
### Checklist
|
||||
<!-- This checklist is COMPULSORY. The first box has been checked for you to show you how it is done. -->
|
||||
@@ -17,30 +16,9 @@ assignees: ''
|
||||
- [ ] I have read the contribution guidelines given at https://github.com/TeamNewPipe/NewPipe/blob/HEAD/.github/CONTRIBUTING.md.
|
||||
- [ ] This issue contains only one feature request. I will open one issue for every feature I want to request.
|
||||
|
||||
|
||||
#### Describe the feature you want
|
||||
<!-- A clear and concise description of what you wish should happen.
|
||||
Example: *I think it would be nice if you add feature Y which makes X possible.*
|
||||
|
||||
Optionally, also describe alternatives you've considered.
|
||||
Example: *Z is also a good alternative. Not as good as Y, but at least...* or *I considered Z, but that didn't turn out to be a good idea because...* -->
|
||||
#### What feature do you want?
|
||||
<!-- Explain how you want the app's look or behavior to change to suit your needs. -->
|
||||
|
||||
|
||||
|
||||
#### Is your feature request related to a problem? Please describe it
|
||||
<!-- A clear and concise description of what the problem is. Maybe the developers and the community could brainstorm and come up with a better solution to your problem. If they exist, link to related Issues and/or PRs for developers to keep track easier.
|
||||
Example: *I want to do X, but there is no way to do it.* -->
|
||||
|
||||
|
||||
|
||||
#### Additional context
|
||||
<!-- Add any other context, like screenshots, about the feature request here.
|
||||
Example: *Here's a photo of my cat!* -->
|
||||
|
||||
|
||||
|
||||
#### How will you/everyone benefit from this feature?
|
||||
<!-- Convince us! How does it change your NewPipe experience and/or your life?
|
||||
The better this paragraph is, the more likely a developer will think about working on it.
|
||||
Example: *This feature will help us colonize the galaxy! -->
|
||||
|
||||
#### Why do you want this feature?
|
||||
<!-- Describe any problem or limitation you come across while using the app which would be solved by this feature. -->
|
||||
|
||||
92
.github/workflows/ci.yml
vendored
92
.github/workflows/ci.yml
vendored
@@ -1,14 +1,25 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
pull_request:
|
||||
branches:
|
||||
- dev
|
||||
- master
|
||||
paths-ignore:
|
||||
- 'README*.md'
|
||||
- 'fastlane/**'
|
||||
- 'assets/**'
|
||||
- '.github/**/*.md'
|
||||
push:
|
||||
branches:
|
||||
- dev
|
||||
- master
|
||||
paths-ignore:
|
||||
- 'README*.md'
|
||||
- 'fastlane/**'
|
||||
- 'assets/**'
|
||||
- '.github/**/*.md'
|
||||
|
||||
jobs:
|
||||
build-and-test-jvm:
|
||||
@@ -22,21 +33,15 @@ jobs:
|
||||
if: github.event_name == 'pull_request'
|
||||
run: git checkout -B ${{ github.head_ref }}
|
||||
|
||||
- name: set up JDK 8
|
||||
- name: set up JDK 11
|
||||
uses: actions/setup-java@v2
|
||||
with:
|
||||
java-version: 8
|
||||
distribution: "adopt"
|
||||
|
||||
- name: Cache Gradle dependencies
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ~/.gradle/caches
|
||||
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }}
|
||||
restore-keys: ${{ runner.os }}-gradle
|
||||
|
||||
java-version: 11
|
||||
distribution: "temurin"
|
||||
cache: 'gradle'
|
||||
|
||||
- name: Build debug APK and run jvm tests
|
||||
run: ./gradlew assembleDebug lintDebug testDebugUnitTest --stacktrace
|
||||
run: ./gradlew assembleDebug lintDebug testDebugUnitTest --stacktrace -DskipFormatKtlint
|
||||
|
||||
- name: Upload APK
|
||||
uses: actions/upload-artifact@v2
|
||||
@@ -44,35 +49,30 @@ jobs:
|
||||
name: app
|
||||
path: app/build/outputs/apk/debug/*.apk
|
||||
|
||||
# Disabled until emulator works again. see https://github.com/TeamNewPipe/NewPipe/pull/6560
|
||||
# test-android:
|
||||
# macos has hardware acceleration. See android-emulator-runner action
|
||||
# runs-on: macos-latest
|
||||
# strategy:
|
||||
# matrix:
|
||||
# api-level 19 is min sdk, but throws errors related to desugaring
|
||||
# api-level: [21, 29]
|
||||
# steps:
|
||||
# - uses: actions/checkout@v2
|
||||
#
|
||||
# - name: set up JDK 8
|
||||
# uses: actions/setup-java@v2
|
||||
# with:
|
||||
# java-version: 8
|
||||
# distribution: "adopt"
|
||||
#
|
||||
# - name: Cache Gradle dependencies
|
||||
# uses: actions/cache@v2
|
||||
# with:
|
||||
# path: ~/.gradle/caches
|
||||
# key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }}
|
||||
# restore-keys: ${{ runner.os }}-gradle
|
||||
#
|
||||
# - name: Run android tests
|
||||
# uses: reactivecircus/android-emulator-runner@v2
|
||||
# with:
|
||||
# api-level: ${{ matrix.api-level }}
|
||||
# script: ./gradlew connectedCheck
|
||||
test-android:
|
||||
# macos has hardware acceleration. See android-emulator-runner action
|
||||
runs-on: macos-latest
|
||||
strategy:
|
||||
matrix:
|
||||
# api-level 19 is min sdk, but throws errors related to desugaring
|
||||
api-level: [ 21, 29 ]
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: set up JDK 11
|
||||
uses: actions/setup-java@v2
|
||||
with:
|
||||
java-version: 11
|
||||
distribution: "temurin"
|
||||
cache: 'gradle'
|
||||
|
||||
- name: Run android tests
|
||||
uses: reactivecircus/android-emulator-runner@v2
|
||||
with:
|
||||
api-level: ${{ matrix.api-level }}
|
||||
# workaround to emulator bug: https://github.com/ReactiveCircus/android-emulator-runner/issues/160
|
||||
emulator-build: 7425822
|
||||
script: ./gradlew connectedCheck
|
||||
|
||||
# sonar:
|
||||
# runs-on: ubuntu-latest
|
||||
@@ -85,7 +85,8 @@ jobs:
|
||||
# uses: actions/setup-java@v2
|
||||
# with:
|
||||
# java-version: 11 # Sonar requires JDK 11
|
||||
# distribution: "adopt"
|
||||
# distribution: "temurin"
|
||||
# cache: 'gradle'
|
||||
|
||||
# - name: Cache SonarCloud packages
|
||||
# uses: actions/cache@v2
|
||||
@@ -94,13 +95,6 @@ jobs:
|
||||
# key: ${{ runner.os }}-sonar
|
||||
# restore-keys: ${{ runner.os }}-sonar
|
||||
|
||||
# - name: Cache Gradle packages
|
||||
# uses: actions/cache@v2
|
||||
# with:
|
||||
# path: ~/.gradle/caches
|
||||
# key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }}
|
||||
# restore-keys: ${{ runner.os }}-gradle
|
||||
|
||||
# - name: Build and analyze
|
||||
# env:
|
||||
# GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any
|
||||
|
||||
20
.github/workflows/no-response.yml
vendored
Normal file
20
.github/workflows/no-response.yml
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
name: No Response
|
||||
|
||||
# Both `issue_comment` and `scheduled` event types are required for this Action
|
||||
# to work properly.
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
schedule:
|
||||
# Run daily at midnight.
|
||||
- cron: '0 0 * * *'
|
||||
|
||||
jobs:
|
||||
noResponse:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: lee-dohm/no-response@v0.5.0
|
||||
with:
|
||||
token: ${{ github.token }}
|
||||
daysUntilClose: 14
|
||||
responseRequiredLabel: waiting-for-author
|
||||
125
README.es.md
125
README.es.md
@@ -1,30 +1,30 @@
|
||||
<p align="center"><a href="https://newpipe.net"><img src="assets/new_pipe_icon_5.png" width="150"></a></p>
|
||||
<h2 align="center"><b>NewPipe</b></h2>
|
||||
<h4 align="center">Una interfaz de streaming lijera y libre para Android.</h4>
|
||||
<h4 align="center">Una interfaz de streaming ligera y libre para Android.</h4>
|
||||
|
||||
<p align="center"><a href="https://f-droid.org/packages/org.schabi.newpipe/"><img src="https://f-droid.org/wiki/images/0/06/F-Droid-button_get-it-on.png"></a></p>
|
||||
<p align="center"><a href="https://f-droid.org/packages/org.schabi.newpipe/"><img src="https://fdroid.gitlab.io/artwork/badge/get-it-on-es.svg"></a></p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/TeamNewPipe/NewPipe/releases" alt="GitHub release"><img src="https://img.shields.io/badge/Lanzamiento-v0.20.11-blue.svg" ></a>
|
||||
<a href="https://www.gnu.org/licenses/gpl-3.0" alt="License: GPLv3"><img src="https://img.shields.io/badge/Licencia-GPL%20v3-blue.svg"></a>
|
||||
<a href="https://github.com/TeamNewPipe/NewPipe/actions" alt="Build Status"><img src="https://github.com/TeamNewPipe/NewPipe/workflows/CI/badge.svg?branch=dev&event=push"></a>
|
||||
<a href="https://hosted.weblate.org/engage/newpipe/es/" alt="Estado de la traducción"><img src="https://hosted.weblate.org/widgets/newpipe/es/svg-badge.svg"></a>
|
||||
<a href="https://web.libera.chat/#newpipe" alt="IRC channel: #newpipe"><img src="https://img.shields.io/badge/Canal%20de%20IRC%20-%23newpipe-brightgreen.svg"></a>
|
||||
<a href="https://www.bountysource.com/teams/newpipe" alt="Bountysource bounties"><img src="https://img.shields.io/bountysource/team/newpipe/activity.svg?colorB=cd201f"></a>
|
||||
<a href="https://github.com/TeamNewPipe/NewPipe/releases" alt="Lanzamientos GitHub"><img src="https://img.shields.io/github/release/TeamNewPipe/NewPipe.svg" ></a>
|
||||
<a href="https://www.gnu.org/licenses/gpl-3.0" alt="Licencia: GPLv3"><img src="https://img.shields.io/badge/Licencia-GPL%20v3-blue.svg"></a>
|
||||
<a href="https://github.com/TeamNewPipe/NewPipe/actions" alt="Estado del Build"><img src="https://github.com/TeamNewPipe/NewPipe/workflows/CI/badge.svg?branch=dev&event=push"></a>
|
||||
<a href="https://hosted.weblate.org/engage/newpipe/es/" alt="Estado de la Traducción"><img src="https://hosted.weblate.org/widgets/newpipe/es/svg-badge.svg"></a>
|
||||
<a href="https://web.libera.chat/#newpipe" alt="Canal de IRC: #newpipe"><img src="https://img.shields.io/badge/Canal%20de%20IRC%20-%23newpipe-brightgreen.svg"></a>
|
||||
<a href="https://www.bountysource.com/teams/newpipe" alt="Recompensas en Bountysource"><img src="https://img.shields.io/bountysource/team/newpipe/activity.svg?colorB=cd201f"></a>
|
||||
</p>
|
||||
<hr>
|
||||
|
||||
<p align="center"><a href="#capturas-de-pantalla">Capturas de pantalla</a> • <a href="#descripción">Descripción</a> • <a href="#características">Características</a> • <a href="#installación-y-actualizaciones">Installación y actualizaciones</a> • <a href="#contribución">Contribución</a> • <a href="#donar">Donar</a> • <a href="#licencias">Licencias</a></p>
|
||||
<p align="center"><a href="https://newpipe.net">Sitio web</a> • <a href="https://newpipe.net/blog/">Blog</a> • <a href="https://newpipe.net/FAQ/">Preguntas Frecuentes</a> • <a href="https://newpipe.net/press/">Prensa</a></p>
|
||||
<p align="center"><a href="#capturas-de-pantalla">Capturas de Pantalla</a> • <a href="#descripción">Descripción</a> • <a href="#características">Características</a> • <a href="#instalación-y-actualizaciones">Instalación y Actualizaciones</a> • <a href="#contribución">Contribución</a> • <a href="#donar">Donar</a> • <a href="#licencia">Licencia</a></p>
|
||||
<p align="center"><a href="https://newpipe.net">Sitio Web</a> • <a href="https://newpipe.net/blog/">Blog</a> • <a href="https://newpipe.net/FAQ/">Preguntas Frecuentes</a> • <a href="https://newpipe.net/press/">Prensa</a></p>
|
||||
<hr>
|
||||
|
||||
*Lea esto en otros idiomas: [English](README.md), [한국어](README.ko.md), [Soomaali](README.so.md), [Português Brasil](README.pt_BR.md), [日本語](README.ja.md), [Română](README.ro.md), [Türkçe](README.tr.md).*
|
||||
|
||||
<b>AVISO: ESTA ES UNA VERSIÓN BETA, POR LO TANTO, PUEDE ENCONTRAR BUGS (ERRORES). SI ENCUENTRA UNO, ABRA UN ISSUE A TRAVÉS DE NUESTRO REPOSITORIO GITHUB.</b>
|
||||
<b>AVISO: ESTA ES UNA VERSIÓN BETA, POR LO TANTO, PUEDE ENCONTRAR BUGS. SI ENCUENTRA UNO ABRA UN ISSUE A TRAVÉS DE NUESTRO REPOSITORIO DE GITHUB.</b>
|
||||
|
||||
<b>COLOCAR NEWPIPE O CUALQUIER FORK (BIFURCACIÓN) REALIZADO DE ELLO EN GOOGLE PLAY STORE VIOLA SUS TÉRMINOS Y CONDICIONES.</b>
|
||||
<b>COLOCAR NEWPIPE O CUALQUIER FORK DE NEWPIPE EN LA GOOGLE PLAY STORE VIOLARÁ SUS TÉRMINOS Y CONDICIONES.</b>
|
||||
|
||||
## Capturas de pantalla
|
||||
## Capturas de Pantalla
|
||||
|
||||
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_01.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_01.png)
|
||||
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_02.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_02.png)
|
||||
@@ -40,39 +40,43 @@
|
||||
[<img src="fastlane/metadata/android/en-US/images/tenInchScreenshots/shot_12.png" width=405>](fastlane/metadata/android/en-US/images/tenInchScreenshots/shot_12.png)
|
||||
|
||||
## Descripción
|
||||
NewPipe no usa ninguna librería de framework de Google, ni la API de YouTube. Los sitios web solamente se analizan para extraer la información requerida, asi que esta app se puede usar sin los servicios de Google instalados. Además, no se necesita una cuenta de YouTube para usar NewPipe, lo cual es un software libre de copyleft.
|
||||
|
||||
NewPipe no usa ninguna librería del framework de Google, ni la API de YouTube. Los sitios web solamente se analizan para extraer la información requerida, por lo que esta app se puede usar sin los servicios de Google instalados. Además, no se necesita una cuenta de YouTube para usar NewPipe, lo cual es un software libre de copyleft.
|
||||
|
||||
### Características
|
||||
|
||||
* Buscar videos
|
||||
* No requiere inicio de sesión
|
||||
* Mostrar información general sobre videos
|
||||
* Mirar videos de YouTube
|
||||
* Escuchar audio de YouTube
|
||||
* Modo popup (reproductor flotante)
|
||||
* Elegir reproductor para mirar el video
|
||||
* Modo de solo audio en videos de YouTube
|
||||
* Modo pop-up (reproductor flotante)
|
||||
* Elegir un reproductor de video externo para mirar videos
|
||||
* Descargar videos
|
||||
* Descargar solamente audio
|
||||
* Abrir video en Kodi
|
||||
* Descargar solo audio
|
||||
* Abrir videos en Kodi
|
||||
* Mostrar videos próximos/relacionados
|
||||
* Buscar a través de YouTube en un idioma específico
|
||||
* Mirar/Bloquear materiales restringidas por edad.
|
||||
* Mirar/Bloquear videos restringidos por edad
|
||||
* Mostrar información general sobre canales
|
||||
* Buscar canales
|
||||
* Buscar de canales
|
||||
* Mirar videos de un canal
|
||||
* Apoyo Orbot/Tor (todavía no directamente)
|
||||
* Apoyo 1080p/2K/4K
|
||||
* Ver historias
|
||||
* Subscribirse a canales
|
||||
* Buscar historias
|
||||
* Buscar/mirar listas de reproducción
|
||||
* Mirar listas de reproducción en fila
|
||||
* Poner videos en fila
|
||||
* Listas locales de reproducción
|
||||
* Soporte Orbot/Tor (todavía no directamente)
|
||||
* Soporte para videos en 1080p/2K/4K
|
||||
* Historial de videos vistos
|
||||
* Suscripción a canales
|
||||
* Historial de búsquedas
|
||||
* Buscar/Mirar listas de reproducción
|
||||
* Mirar listas de reproducción en cola
|
||||
* Poner videos en cola
|
||||
* Listas de reproducción locales
|
||||
* Subtítulos
|
||||
* Apoyo de medios en directo
|
||||
* Soporte para transmisiones en vivo
|
||||
* Mostrar comentarios
|
||||
|
||||
### Servicios apoyados
|
||||
NewPipe apoya varios servicios. Nuestras [documentaciones](https://teamnewpipe.github.io/documentation/) proveen más información en como se puede agregar un servicio nuevo a la app y el extractor. Por favor contáctenos si pretende agregar uno nuevo. Actualmente los servicios apoyados son:
|
||||
### Servicios Soportados
|
||||
|
||||
NewPipe soporta varios servicios. Nuestras [documentaciones](https://teamnewpipe.github.io/documentation/) ofrecen más información sobre cómo se puede agregar un servicio nuevo a la app y al extractor. Por favor ponte en contacto con nosotros si tienes pensado agregar uno nuevo. Actualmente los servicios soportados son:
|
||||
|
||||
* YouTube
|
||||
* SoundCloud \[beta\]
|
||||
@@ -80,61 +84,60 @@ NewPipe apoya varios servicios. Nuestras [documentaciones](https://teamnewpipe.g
|
||||
* PeerTube instances \[beta\]
|
||||
* Bandcamp \[beta\]
|
||||
|
||||
<!-- Brecha escondida para mantener compatibles los enlaces viejos. -->
|
||||
<span id="actualizaciones"></span>
|
||||
## Instalación y Actualizaciones
|
||||
|
||||
## Installación y actualizaciones
|
||||
Se puede instalar NewPipe usando uno de los métodos siguientes:
|
||||
1. Agregar nuestro repositorio personalizado a F-Droid e instalarlo desde allí. Las instrucciones están aquí: https://newpipe.net/FAQ/tutorials/install-add-fdroid-repo/
|
||||
2. Descargar el archivo APK del enlace [Github Releases](https://github.com/TeamNewPipe/NewPipe/releases) e instalarlo.
|
||||
3. Actualizar a través de F-Droid. Este es el método más lento para obtener la actualización, como F-Droid debe reconocer cambios, construir el APK aparte, firmarlo con una clave, y finalmente empujar la actualización a los usuarios.
|
||||
4. Construir un APK de depuración por si mismo. Este es el modo más rápido para realizar nuevas características en su dispositivo, pero es mucho más complicado, asi que recomendamos uno de los otros métodos.
|
||||
|
||||
Recomendamos el método 1 para la mayoría de usuarios. Los APKs instalados usando método 1 o 2 son compatibles el uno con el otro, pero no con las instalaciones usando método 3. Esta es debida a la misma clave digital (la nuestra), siendo utilizado en los métodos 1 y 2, pero una clave digital diferente (la de F-Droid) siendo utilizado en el método 3. Construir un APK de depuración usando método 4 excluye una clave enteramente. Firmando con claves digitales ayuda a asegurar de que un
|
||||
usuario no esté engañado para instalar una actualización maliciosa a una app.
|
||||
1. Agregando nuestro repositorio personalizado a F-Droid e instalarlo desde allí. Las instrucciones están [aquí](https://newpipe.net/FAQ/tutorials/install-add-fdroid-repo/).
|
||||
2. Descargando el archivo APK de [aquí](https://github.com/TeamNewPipe/NewPipe/releases) y posteriormente instalarlo.
|
||||
3. Usando el repositorio oficial de F-Droid. Este es el método más lento para obtener actualizaciones, ya que F-Droid debe reconocer los cambios, construir el APK aparte, firmarlo con una clave, y finalmente publicar la actualización.
|
||||
4. Construyendo la app usted mismo. Este es el modo más rápido para obtener nuevas características en su dispositivo, pero es mucho más complicado, así que recomendamos uno de los otros métodos.
|
||||
|
||||
Mientras tanto, si quiere cambiar los fuentes por alguna razón (por ejemplo, la funcionalidad del nucleo de NewPipe se rompe y F-Droid aun no tiene la actualización), recomendamos el siguiente procedimiento:
|
||||
1. Repaldear sus datos a través de Ajustes > Contenido > Exporta base de datos para guardar su historia, subscripciones, y listas de reproducción
|
||||
2. Desinstalar NewPipe
|
||||
3. Descargar el APK del nuevo fuente e instalarlo.
|
||||
4. Importar los datos del paso 1 a través de Ajustes > Contenido > Importa base de datos.
|
||||
Recomendamos el método 1 para la mayoría de usuarios. Los APKs instalados usando método 1 y 2 son compatibles entre sí, pero no lo son con los instalados usando el método 3. Esto es debido a la clave de firmado, ya que los métodos 1 y 2 usan la misma clave (la nuestra), pero el método 3 usa una clave diferente (la de F-Droid). El método 4 excluye totalmente una clave de firmado. Las claves de firmado aseguran que el usuario no esté siendo engañado para instalar/actualizar una APK maliciosa.
|
||||
|
||||
Además, si quiere cambiar el método de instalación por alguna razón (por ejemplo: la funcionalidad del núcleo de NewPipe se rompe o F-Droid aún no publica la actualización), recomendamos el siguiente procedimiento:
|
||||
1. Respalde su información a través de Ajustes > Contenido > Exportar base de datos; esto guardará su historial (videos vistos y búsquedas), suscripciones, listas de reproducción y ajustes.
|
||||
2. Desinstale NewPipe.
|
||||
3. Descargue el APK con un método distinto e instálelo.
|
||||
4. Importe la información (la base de datos extraída del paso 1) a través de Ajustes > Contenido > Importar base de datos. Tenga en cuenta que esta opción superpondrá su historial actual (tanto de vídeos como de búsquedas), sus listas de reproducción y (opcionalmente) sus configuraciones.
|
||||
|
||||
## Contribución
|
||||
Si tiene ideas, traducciónes, cambios de diseño, limpieza de código, o cambios grandes de código, su ayuda es siempre bienvenida.
|
||||
Cuanto más realizamos, mejor se pone la aplicación!
|
||||
|
||||
Si tiene ideas, traducciones, cambios de diseño, limpieza de código o cambios grandes de código, su ayuda es siempre bienvenida. ¡Cuanto más hagamos, NewPipe será mucho mejor!
|
||||
|
||||
Si quiere involucrarse, fíjese en nuestras [notas de contribución](.github/CONTRIBUTING.md).
|
||||
|
||||
<a href="https://hosted.weblate.org/engage/newpipe/es/">
|
||||
<img src="https://hosted.weblate.org/widgets/newpipe/es/287x66-grey.png" alt="Estado de la traducción" />
|
||||
<img src="https://hosted.weblate.org/widgets/newpipe/es/287x66-grey.png" alt="Estado de la Traducción" />
|
||||
</a>
|
||||
|
||||
## Donar
|
||||
Si le gusta el NewPipe estaremos felices con una donación. O puede enviar bitcoin o donar a través de Bountysource o Liberapay. Para obtener más información sobre como donar a NewPipe, por favor visita nuestro [sitio web](https://newpipe.net/donate).
|
||||
Si te gusta NewPipe, estaremos felices con una donación. Puede enviar bitcoin o donar a través de Bountysource o Liberapay. Visita nuestro [sitio web](https://newpipe.net/donate) para más información.
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<td><img src="https://bitcoin.org/img/icons/logotop.svg" alt="Bitcoin"></td>
|
||||
<td><img src="assets/bitcoin_qr_code.png" alt="Bitcoin QR code" width="100px"></td>
|
||||
<td><img src="assets/bitcoin_qr_code.png" alt="Código QR del Bitcoin" width="100px"></td>
|
||||
<td><samp>16A9J59ahMRqkLSZjhYj33n9j3fMztFxnh</samp></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="https://liberapay.com/TeamNewPipe/"><img src="https://upload.wikimedia.org/wikipedia/commons/2/27/Liberapay_logo_v2_white-on-yellow.svg" alt="Liberapay" width="80px" ></a></td>
|
||||
<td><a href="https://liberapay.com/TeamNewPipe/"><img src="assets/liberapay_qr_code.png" alt="Visit NewPipe at liberapay.com" width="100px"></a></td>
|
||||
<td><a href="https://liberapay.com/TeamNewPipe/donate"><img src="assets/liberapay_donate_button.svg" alt="Donate via Liberapay" height="35px"></a></td>
|
||||
<td><a href="https://liberapay.com/TeamNewPipe/"><img src="assets/liberapay_qr_code.png" alt="Visita NewPipe en liberapay.com" width="100px"></a></td>
|
||||
<td><a href="https://liberapay.com/TeamNewPipe/donate"><img src="assets/liberapay_donate_button.svg" alt="Dona vía Liberapay" height="35px"></a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="https://www.bountysource.com/teams/newpipe"><img src="https://upload.wikimedia.org/wikipedia/commons/thumb/2/22/Bountysource.png/320px-Bountysource.png" alt="Bountysource" width="190px"></a></td>
|
||||
<td><a href="https://www.bountysource.com/teams/newpipe"><img src="assets/bountysource_qr_code.png" alt="Visit NewPipe at bountysource.com" width="100px"></a></td>
|
||||
<td><a href="https://www.bountysource.com/teams/newpipe/issues"><img src="https://img.shields.io/bountysource/team/newpipe/activity.svg?colorB=cd201f" height="30px" alt="Check out how many bounties you can earn."></a></td>
|
||||
<td><a href="https://www.bountysource.com/teams/newpipe"><img src="assets/bountysource_qr_code.png" alt="Visita NewPipe en bountysource.com" width="100px"></a></td>
|
||||
<td><a href="https://www.bountysource.com/teams/newpipe/issues"><img src="https://img.shields.io/bountysource/team/newpipe/activity.svg?colorB=cd201f" height="30px" alt="Revisa cuántas recompensas puedes obtener."></a></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
## Política de privacidad
|
||||
El proyecto NewPipe tiene como objetivo proveer una experience privada y anónima para usar servicios de medios web.
|
||||
Por lo tanto, la app no colecciona ningunos datos sin su consentimiento. La politica de privacidad de NewPipe explica en detalle los datos enviados y almacenados cuando envia un informe de error, o comentario en nuestro blog. Puede encontrar el documento [aqui](https://newpipe.net/legal/privacy/).
|
||||
## Política de Privacidad
|
||||
|
||||
El proyecto NewPipe tiene como objetivo ofrecer una experience privada y anónima al usar servicios web multimedia. Por lo tanto, la app no recoleta ningún tipo de información sin su consentimiento. La politica de privacidad de NewPipe explica en detalle qué información es enviada y almacenada cuando envía un informe de error o comenta en [nuestro blog](https://newpipe.net/blog/). Puede encontrar el documento [aquí](https://newpipe.net/legal/privacy/).
|
||||
|
||||
## Licencia
|
||||
[](https://www.gnu.org/licenses/gpl-3.0.en.html)
|
||||
|
||||
NewPipe es Software Libre: Puede usar, estudiar, compartir, y mejorarlo a su voluntad. Especificamente puede redistribuir y/o modificarlo bajo los términos de la [GNU General Public License](https://www.gnu.org/licenses/gpl.html) como publicado por la Free Software Foundation, o versión 3 de la licencia, o (en su opción) cualquier versión posterior.
|
||||
[](https://www.gnu.org/licenses/gpl-3.0.html)
|
||||
|
||||
NewPipe es Software Libre: Puede usarlo, estudiarlo, compartirlo y mejorarlo a su voluntad. Más específicamente, puede redistribuirlo y/o modificarlo bajo los términos de la [GNU General Public License](https://www.gnu.org/licenses/gpl.html) publicada por la Free Software Foundation tanto si usa la versión 3 o posterior de la licencia.
|
||||
|
||||
@@ -89,7 +89,7 @@ NewPipe 코드의 변경이 있을 때(기능 추가 또는 버그 수정으로
|
||||
따라서 우리는 다른 방법들 중 하나를 사용하는 것을 추천합니다.
|
||||
2. 우리의 커스텀 저장소를 F-Droid에 추가하고 우리가 릴리즈를 게시하는 대로 저곳에서 릴리즈를 설치할 수 있습니다.
|
||||
이에 대한 설명서는 이곳에서 확인할 수 있습니다: https://newpipe.net/FAQ/tutorials/install-add-fdroid-repo/
|
||||
3. 우리가 릴리즈를 게시하는 대로 [Github Releases](https://github.com/TeamNewPipe/NewPipe/releases)에서 APK를 다운받고 이것을 설치할 수 있습니다.
|
||||
3. 우리가 릴리즈를 게시하는 대로 [GitHub Releases](https://github.com/TeamNewPipe/NewPipe/releases)에서 APK를 다운받고 이것을 설치할 수 있습니다.
|
||||
4. F-Droid를 통해 업데이트 할 수 있습니다. F-Droid는 변화를 인식하고, 스스로 APK를 생성하고, 이것에 서명하고, 사용자들에서 업데이트를 전달해야만 하기 때문에,
|
||||
이것은 업데이트를 받는 가장 느린 방법입니다.
|
||||
|
||||
|
||||
@@ -89,7 +89,7 @@ NewPipe supports multiple services. Our [docs](https://teamnewpipe.github.io/doc
|
||||
## Installation and updates
|
||||
You can install NewPipe using one of the following methods:
|
||||
1. Add our custom repo to F-Droid and install it from there. The instructions are here: https://newpipe.net/FAQ/tutorials/install-add-fdroid-repo/
|
||||
2. Download the APK from [Github Releases](https://github.com/TeamNewPipe/NewPipe/releases) and install it.
|
||||
2. Download the APK from [GitHub Releases](https://github.com/TeamNewPipe/NewPipe/releases) and install it.
|
||||
3. Update via F-Droid. This is the slowest method of getting updates, as F-Droid must recognize changes, build the APK itself, sign it, then push the update to users.
|
||||
4. Build a debug APK yourself. This is the fastest way to get new features on your device, but is much more complicated, so we recommend using one of the other methods.
|
||||
|
||||
|
||||
@@ -87,7 +87,7 @@ O NewPipe suporta vários serviços. Nosso [documentação](https://teamnewpipe.
|
||||
Quando uma alteração no código NewPipe (devido à adição de recursos ou fixação de bugs), eventualmente ocorrerá uma versão. Estes estão no formato x.xx.x . A fim de obter esta nova versão, você pode:
|
||||
1. Construa um APK de depuração você mesmo. Esta é a maneira mais rápida de obter novos recursos em seu dispositivo, mas é muito mais complicado, por isso recomendamos usar um dos outros métodos.
|
||||
2. Adicione nosso repo personalizado ao F-Droid e instale-o a partir daí assim que publicarmos um lançamento. As instruções estão aqui.: https://newpipe.net/FAQ/tutorials/install-add-fdroid-repo/
|
||||
3. Baixe o APK do [Github Releases](https://github.com/TeamNewPipe/NewPipe/releases) e instalá-lo assim que publicarmos um lançamento.
|
||||
3. Baixe o APK do [GitHub Releases](https://github.com/TeamNewPipe/NewPipe/releases) e instalá-lo assim que publicarmos um lançamento.
|
||||
4. Atualização via F-droid. Este é o método mais lento para obter atualizações, pois o F-Droid deve reconhecer alterações, construir o próprio APK, assiná-lo e, em seguida, enviar a atualização para os usuários.
|
||||
|
||||
Recomendamos o método 2 para a maioria dos usuários. Os APKs instalados usando o método 2 ou 3 são compatíveis entre si, mas não com aqueles instalados usando o método 4. Isso se deve à mesma chave de assinatura (nossa) sendo usada para 2 e 3, mas uma chave de assinatura diferente (F-Droid's) está sendo usada para 4. Construir um APK depuração usando o método 1 exclui totalmente uma chave. Assinar chaves ajudam a garantir que um usuário não seja enganado para instalar uma atualização maliciosa em um aplicativo.
|
||||
|
||||
@@ -89,7 +89,7 @@ NewPipe suportă servicii multiple. [Documentele](https://teamnewpipe.github.io/
|
||||
## Instalare şi actualizări
|
||||
Puteţi instala NewPipe folosind una dintre următoarele metode:
|
||||
1. Adăugaţi depozitul nostru F-droid personalizat. Instrucţiunile sunt aici: https://newpipe.net/FAQ/tutorials/install-add-fdroid-repo/
|
||||
2. Descărcaţi APK-ul din [Github Releases](https://github.com/TeamNewPipe/NewPipe/releases) şi instalaţi-l.
|
||||
2. Descărcaţi APK-ul din [GitHub Releases](https://github.com/TeamNewPipe/NewPipe/releases) şi instalaţi-l.
|
||||
3. Actualizaţi via F-Droid. Aceasta este cea mai lentă metodă de a obţine actualizări, deoarece F-Droid trebuie să recunoască schimbările, să constriască APK-ul, să îl semneze, iar apoi să îl trimită utilizatorilor.
|
||||
4. Construiţi un APK de depanare. Aceasta este cea mai rapidă metodă de a primi funcţii noi, dar este mult mai complicată, aşa că vă recomandăm să folosiţi una dintre celelalte metode.
|
||||
|
||||
|
||||
@@ -86,7 +86,7 @@ NewPipe wuxuu taageeraa adeegyo badan. [warqadan](https://teamnewpipe.github.io/
|
||||
Marka koodhka NewPipe isbadal ku dhaco (wax cusub oo lagusoo kordhiyay ama cilad bixin), ugu dambayn waxaa lasii daayaa mid cusub (Siidayn). Siidaynta qaabkeedu waa x.xx.x . Si aad midka cusub u hesho, waxaad samayn kartaa:
|
||||
1. Inaad mid cusub (APK) adigu dhisato. Tani waa mida ugu dagdag badan eed waxyaabaha cusub ku heli karto, laakiin way adagtahay, sidaa darteed waxaan soojeedinaynaa inaad isticmaasho qababka kale.
|
||||
2. Ku dar qayb gaar ah xaganaga F-Droid oo xagaas kaga shub isla markay siidayn soobaxdo. Hagitaanka xagan ka eeg: https://newpipe.net/FAQ/tutorials/install-add-fdroid-repo/
|
||||
3. Kasoo dajiso APK-ga xaga [Siidaynta Github](https://github.com/TeamNewPipe/NewPipe/releases) oo ku shubo isla markay siidayn soobaxdo.
|
||||
3. Kasoo dajiso APK-ga xaga [Siidaynta GitHub](https://github.com/TeamNewPipe/NewPipe/releases) oo ku shubo isla markay siidayn soobaxdo.
|
||||
4. Ka cusboonaysii xaga F-Droid. Tani waa mida ugu daahitaanka badan, sababtoo ah F-Droid waxay fiirin isbadalka waxayna iyadu dhisi mid (app), sixiixi, kadibna ay cusboonaysiinta usiidayn isticmaalayaasha.
|
||||
|
||||
Waxaan usoojeedinaynaa isticmaaalka qaabka 2 dadka badankood. APK-yada loogu shubo qaabka 2 ama 3 way isqaadan karaan, laakiin isma qaadan karaan kuwa loogu shubay qaabka 4. Sababtuna waxaa weeye furaha sixiixa oo iskumid ah (kaanaga weeye) oo loo isticmaalay 2 iyo 3, laakiin furo sixiixeed ka duwan (midka F-Droid) oo loo isticmaalay 4. Dhisida APK ayadoo la isticmaalayo qaabka 1 waxay gabi ahaanba ka reebtaa wax fure ah. Furayaasha sixiixa waxay xaqiijiyaan in isticmaalaha aan lagu khaldin inuu ku shubto cusboonaysiin khalad ah (wax lasoo dhexraaciyay) app-ka.
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
|
||||
<b>GOOGLE PLAY STORE'A NEWPIPE VEYA BAŞKA BİR KOPYASINI KOYMAK, PLAY STORE ŞARTLARINI VE KOŞULLARINI İHLAL EDER.</b>
|
||||
|
||||
## Ekran fotoğrafları
|
||||
## Ekran görüntüleri
|
||||
|
||||
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_01.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_01.png)
|
||||
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_02.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_02.png)
|
||||
@@ -88,7 +88,7 @@ NewPipe birden fazla hizmeti destekler. Uygulamaya ve ayıklayıcıya yeni bir h
|
||||
## Kurulum ve güncellemeler
|
||||
Aşağıdaki yöntemlerden birini kullanarak NewPipe'ı kurabilirsiniz:
|
||||
1. Özel depomuzu F-Droid'e ekleyin ve oradan yükleyin. Kılavuzu şurada bulabilirsiniz: https://newpipe.net/FAQ/tutorials/install-add-fdroid-repo/
|
||||
2. APK'yı [Github sürümlerinden](https://github.com/TeamNewPipe/NewPipe/releases) indirin ve kurun.
|
||||
2. APK'yı [GitHub sürümlerinden](https://github.com/TeamNewPipe/NewPipe/releases) indirin ve kurun.
|
||||
3. F-Droid ile güncelleyin. Bu, güncellemeleri almanın en yavaş yöntemidir, çünkü F-Droid değişiklikleri tanımalı, APK'yı kendisi oluşturmalı, imzalamalı ve ardından güncellemeyi kullanıcılara dağıtmalıdır.
|
||||
4. Kendiniz bir APK derleyin. Bu yöntem, cihazınızda yeni özellikler edinmenin en hızlı yoludur, ancak çok daha karmaşıktır, bu nedenle diğer yöntemlerden birini kullanmanızı öneririz.
|
||||
|
||||
|
||||
@@ -4,21 +4,21 @@ plugins {
|
||||
|
||||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'kotlin-android'
|
||||
apply plugin: 'kotlin-android-extensions'
|
||||
apply plugin: 'kotlin-parcelize'
|
||||
apply plugin: 'kotlin-kapt'
|
||||
apply plugin: 'checkstyle'
|
||||
|
||||
android {
|
||||
compileSdkVersion 29
|
||||
buildToolsVersion '29.0.3'
|
||||
compileSdkVersion 30
|
||||
buildToolsVersion '30.0.3'
|
||||
|
||||
defaultConfig {
|
||||
applicationId "org.schabi.newpipe"
|
||||
resValue "string", "app_name", "NewPipe"
|
||||
minSdkVersion 19
|
||||
targetSdkVersion 29
|
||||
versionCode 974
|
||||
versionName "0.21.8"
|
||||
versionCode 979
|
||||
versionName "0.21.13"
|
||||
|
||||
multiDexEnabled true
|
||||
|
||||
@@ -54,6 +54,11 @@ android {
|
||||
// debug build. This seems to be a Gradle bug, therefore
|
||||
// TODO: update Gradle version
|
||||
release {
|
||||
if (System.properties.containsKey('packageSuffix')) {
|
||||
applicationIdSuffix System.getProperty('packageSuffix')
|
||||
resValue "string", "app_name", "NewPipe " + System.getProperty('packageSuffix')
|
||||
archivesBaseName = 'NewPipe_' + System.getProperty('packageSuffix')
|
||||
}
|
||||
minifyEnabled true
|
||||
shrinkResources false // disabled to fix F-Droid's reproducible build
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||
@@ -84,11 +89,6 @@ android {
|
||||
jvmTarget = JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
// Required and used only by groupie
|
||||
androidExtensions {
|
||||
experimental = true
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
androidTest.assets.srcDirs += files("$projectDir/schemas".toString())
|
||||
}
|
||||
@@ -101,17 +101,17 @@ android {
|
||||
ext {
|
||||
checkstyleVersion = '8.38'
|
||||
|
||||
androidxLifecycleVersion = '2.2.0'
|
||||
androidxLifecycleVersion = '2.3.1'
|
||||
androidxRoomVersion = '2.3.0'
|
||||
|
||||
icepickVersion = '3.2.0'
|
||||
exoPlayerVersion = '2.12.3'
|
||||
googleAutoServiceVersion = '1.0-rc7'
|
||||
groupieVersion = '2.8.1'
|
||||
markwonVersion = '4.6.0'
|
||||
googleAutoServiceVersion = '1.0'
|
||||
groupieVersion = '2.9.0'
|
||||
markwonVersion = '4.6.2'
|
||||
|
||||
leakCanaryVersion = '2.5'
|
||||
stethoVersion = '1.5.1'
|
||||
stethoVersion = '1.6.0'
|
||||
mockitoVersion = '3.6.0'
|
||||
}
|
||||
|
||||
@@ -121,7 +121,7 @@ configurations {
|
||||
}
|
||||
|
||||
checkstyle {
|
||||
configDir rootProject.file(".")
|
||||
getConfigDirectory().set(rootProject.file("."))
|
||||
ignoreFailures false
|
||||
showViolations true
|
||||
toolVersion = checkstyleVersion
|
||||
@@ -140,8 +140,8 @@ task runCheckstyle(type: Checkstyle) {
|
||||
showViolations true
|
||||
|
||||
reports {
|
||||
xml.enabled true
|
||||
html.enabled true
|
||||
xml.getRequired().set(true)
|
||||
html.getRequired().set(true)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -151,7 +151,7 @@ def inputFiles = project.fileTree(dir: "src", include: "**/*.kt")
|
||||
task runKtlint(type: JavaExec) {
|
||||
inputs.files(inputFiles)
|
||||
outputs.dir(outputDir)
|
||||
main = "com.pinterest.ktlint.Main"
|
||||
getMainClass().set("com.pinterest.ktlint.Main")
|
||||
classpath = configurations.ktlint
|
||||
args "src/**/*.kt"
|
||||
}
|
||||
@@ -159,13 +159,16 @@ task runKtlint(type: JavaExec) {
|
||||
task formatKtlint(type: JavaExec) {
|
||||
inputs.files(inputFiles)
|
||||
outputs.dir(outputDir)
|
||||
main = "com.pinterest.ktlint.Main"
|
||||
getMainClass().set("com.pinterest.ktlint.Main")
|
||||
classpath = configurations.ktlint
|
||||
args "-F", "src/**/*.kt"
|
||||
}
|
||||
|
||||
afterEvaluate {
|
||||
preDebugBuild.dependsOn formatKtlint, runCheckstyle, runKtlint
|
||||
if (!System.properties.containsKey('skipFormatKtlint')) {
|
||||
preDebugBuild.dependsOn formatKtlint
|
||||
}
|
||||
preDebugBuild.dependsOn runCheckstyle, runKtlint
|
||||
}
|
||||
|
||||
sonarqube {
|
||||
@@ -186,7 +189,7 @@ dependencies {
|
||||
// name and the commit hash with the commit hash of the (pushed) commit you want to test
|
||||
// This works thanks to JitPack: https://jitpack.io/
|
||||
implementation 'com.github.TeamNewPipe:nanojson:1d9e1aea9049fc9f85e68b43ba39fe7be1c1f751'
|
||||
implementation 'com.github.TeamNewPipe:NewPipeExtractor:v0.21.8'
|
||||
implementation 'com.github.TeamNewPipe:NewPipeExtractor:v0.21.11'
|
||||
|
||||
/** Checkstyle **/
|
||||
checkstyle "com.puppycrawl.tools:checkstyle:${checkstyleVersion}"
|
||||
@@ -196,16 +199,16 @@ dependencies {
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:${kotlin_version}"
|
||||
|
||||
/** AndroidX **/
|
||||
implementation 'androidx.appcompat:appcompat:1.2.0'
|
||||
implementation 'androidx.appcompat:appcompat:1.3.1'
|
||||
implementation 'androidx.cardview:cardview:1.0.0'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
|
||||
implementation 'androidx.core:core-ktx:1.3.2'
|
||||
implementation 'androidx.core:core-ktx:1.6.0'
|
||||
implementation 'androidx.documentfile:documentfile:1.0.1'
|
||||
implementation 'androidx.fragment:fragment-ktx:1.3.5'
|
||||
implementation 'androidx.fragment:fragment-ktx:1.3.6'
|
||||
implementation "androidx.lifecycle:lifecycle-livedata:${androidxLifecycleVersion}"
|
||||
implementation "androidx.lifecycle:lifecycle-viewmodel:${androidxLifecycleVersion}"
|
||||
implementation 'androidx.localbroadcastmanager:localbroadcastmanager:1.0.0'
|
||||
implementation 'androidx.media:media:1.2.1'
|
||||
implementation 'androidx.media:media:1.3.1'
|
||||
implementation 'androidx.multidex:multidex:2.0.1'
|
||||
implementation 'androidx.preference:preference:1.1.1'
|
||||
implementation 'androidx.recyclerview:recyclerview:1.1.0'
|
||||
@@ -237,13 +240,14 @@ dependencies {
|
||||
kapt "com.google.auto.service:auto-service:${googleAutoServiceVersion}"
|
||||
|
||||
// Manager for complex RecyclerView layouts
|
||||
implementation "com.xwray:groupie:${groupieVersion}"
|
||||
implementation "com.xwray:groupie-viewbinding:${groupieVersion}"
|
||||
implementation "com.github.lisawray.groupie:groupie:${groupieVersion}"
|
||||
implementation "com.github.lisawray.groupie:groupie-viewbinding:${groupieVersion}"
|
||||
|
||||
// Circular ImageView
|
||||
implementation "de.hdodenhof:circleimageview:3.1.0"
|
||||
// Image loading
|
||||
implementation "com.nostra13.universalimageloader:universal-image-loader:1.9.5"
|
||||
//noinspection GradleDependency --> 2.8 is the last version, not 2.71828!
|
||||
implementation "com.squareup.picasso:picasso:2.8"
|
||||
|
||||
// Markdown library for Android
|
||||
implementation "io.noties.markwon:core:${markwonVersion}"
|
||||
@@ -255,6 +259,9 @@ dependencies {
|
||||
// Crash reporting
|
||||
implementation "ch.acra:acra-core:5.7.0"
|
||||
|
||||
// Properly restarting
|
||||
implementation 'com.jakewharton:process-phoenix:2.1.2'
|
||||
|
||||
// Reactive extensions for Java VM
|
||||
implementation "io.reactivex.rxjava3:rxjava:3.0.7"
|
||||
implementation "io.reactivex.rxjava3:rxandroid:3.0.0"
|
||||
|
||||
713
app/schemas/org.schabi.newpipe.database.AppDatabase/4.json
Normal file
713
app/schemas/org.schabi.newpipe.database.AppDatabase/4.json
Normal file
@@ -0,0 +1,713 @@
|
||||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 4,
|
||||
"identityHash": "d8070091972a7011bce18aed62f80b90",
|
||||
"entities": [
|
||||
{
|
||||
"tableName": "subscriptions",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `service_id` INTEGER NOT NULL, `url` TEXT, `name` TEXT, `avatar_url` TEXT, `subscriber_count` INTEGER, `description` TEXT)",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "uid",
|
||||
"columnName": "uid",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "serviceId",
|
||||
"columnName": "service_id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "url",
|
||||
"columnName": "url",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "name",
|
||||
"columnName": "name",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "avatarUrl",
|
||||
"columnName": "avatar_url",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "subscriberCount",
|
||||
"columnName": "subscriber_count",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "description",
|
||||
"columnName": "description",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"columnNames": [
|
||||
"uid"
|
||||
],
|
||||
"autoGenerate": true
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_subscriptions_service_id_url",
|
||||
"unique": true,
|
||||
"columnNames": [
|
||||
"service_id",
|
||||
"url"
|
||||
],
|
||||
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_subscriptions_service_id_url` ON `${TABLE_NAME}` (`service_id`, `url`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "search_history",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `creation_date` INTEGER, `service_id` INTEGER NOT NULL, `search` TEXT)",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "creationDate",
|
||||
"columnName": "creation_date",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "serviceId",
|
||||
"columnName": "service_id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "search",
|
||||
"columnName": "search",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"columnNames": [
|
||||
"id"
|
||||
],
|
||||
"autoGenerate": true
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_search_history_search",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"search"
|
||||
],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_search_history_search` ON `${TABLE_NAME}` (`search`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "streams",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `service_id` INTEGER NOT NULL, `url` TEXT NOT NULL, `title` TEXT NOT NULL, `stream_type` TEXT NOT NULL, `duration` INTEGER NOT NULL, `uploader` TEXT NOT NULL, `uploader_url` TEXT, `thumbnail_url` TEXT, `view_count` INTEGER, `textual_upload_date` TEXT, `upload_date` INTEGER, `is_upload_date_approximation` INTEGER)",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "uid",
|
||||
"columnName": "uid",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "serviceId",
|
||||
"columnName": "service_id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "url",
|
||||
"columnName": "url",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "title",
|
||||
"columnName": "title",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "streamType",
|
||||
"columnName": "stream_type",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "duration",
|
||||
"columnName": "duration",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "uploader",
|
||||
"columnName": "uploader",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "uploaderUrl",
|
||||
"columnName": "uploader_url",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "thumbnailUrl",
|
||||
"columnName": "thumbnail_url",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "viewCount",
|
||||
"columnName": "view_count",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "textualUploadDate",
|
||||
"columnName": "textual_upload_date",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "uploadDate",
|
||||
"columnName": "upload_date",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "isUploadDateApproximation",
|
||||
"columnName": "is_upload_date_approximation",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"columnNames": [
|
||||
"uid"
|
||||
],
|
||||
"autoGenerate": true
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_streams_service_id_url",
|
||||
"unique": true,
|
||||
"columnNames": [
|
||||
"service_id",
|
||||
"url"
|
||||
],
|
||||
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_streams_service_id_url` ON `${TABLE_NAME}` (`service_id`, `url`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "stream_history",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`stream_id` INTEGER NOT NULL, `access_date` INTEGER NOT NULL, `repeat_count` INTEGER NOT NULL, PRIMARY KEY(`stream_id`, `access_date`), FOREIGN KEY(`stream_id`) REFERENCES `streams`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE )",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "streamUid",
|
||||
"columnName": "stream_id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "accessDate",
|
||||
"columnName": "access_date",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "repeatCount",
|
||||
"columnName": "repeat_count",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"columnNames": [
|
||||
"stream_id",
|
||||
"access_date"
|
||||
],
|
||||
"autoGenerate": false
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_stream_history_stream_id",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"stream_id"
|
||||
],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_stream_history_stream_id` ON `${TABLE_NAME}` (`stream_id`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"table": "streams",
|
||||
"onDelete": "CASCADE",
|
||||
"onUpdate": "CASCADE",
|
||||
"columns": [
|
||||
"stream_id"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"uid"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableName": "stream_state",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`stream_id` INTEGER NOT NULL, `progress_time` INTEGER NOT NULL, PRIMARY KEY(`stream_id`), FOREIGN KEY(`stream_id`) REFERENCES `streams`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE )",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "streamUid",
|
||||
"columnName": "stream_id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "progressMillis",
|
||||
"columnName": "progress_time",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"columnNames": [
|
||||
"stream_id"
|
||||
],
|
||||
"autoGenerate": false
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"table": "streams",
|
||||
"onDelete": "CASCADE",
|
||||
"onUpdate": "CASCADE",
|
||||
"columns": [
|
||||
"stream_id"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"uid"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableName": "playlists",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT, `thumbnail_url` TEXT)",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "uid",
|
||||
"columnName": "uid",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "name",
|
||||
"columnName": "name",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "thumbnailUrl",
|
||||
"columnName": "thumbnail_url",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"columnNames": [
|
||||
"uid"
|
||||
],
|
||||
"autoGenerate": true
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_playlists_name",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"name"
|
||||
],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_playlists_name` ON `${TABLE_NAME}` (`name`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "playlist_stream_join",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`playlist_id` INTEGER NOT NULL, `stream_id` INTEGER NOT NULL, `join_index` INTEGER NOT NULL, PRIMARY KEY(`playlist_id`, `join_index`), FOREIGN KEY(`playlist_id`) REFERENCES `playlists`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, FOREIGN KEY(`stream_id`) REFERENCES `streams`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "playlistUid",
|
||||
"columnName": "playlist_id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "streamUid",
|
||||
"columnName": "stream_id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "index",
|
||||
"columnName": "join_index",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"columnNames": [
|
||||
"playlist_id",
|
||||
"join_index"
|
||||
],
|
||||
"autoGenerate": false
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_playlist_stream_join_playlist_id_join_index",
|
||||
"unique": true,
|
||||
"columnNames": [
|
||||
"playlist_id",
|
||||
"join_index"
|
||||
],
|
||||
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_playlist_stream_join_playlist_id_join_index` ON `${TABLE_NAME}` (`playlist_id`, `join_index`)"
|
||||
},
|
||||
{
|
||||
"name": "index_playlist_stream_join_stream_id",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"stream_id"
|
||||
],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_playlist_stream_join_stream_id` ON `${TABLE_NAME}` (`stream_id`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"table": "playlists",
|
||||
"onDelete": "CASCADE",
|
||||
"onUpdate": "CASCADE",
|
||||
"columns": [
|
||||
"playlist_id"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"uid"
|
||||
]
|
||||
},
|
||||
{
|
||||
"table": "streams",
|
||||
"onDelete": "CASCADE",
|
||||
"onUpdate": "CASCADE",
|
||||
"columns": [
|
||||
"stream_id"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"uid"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableName": "remote_playlists",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `service_id` INTEGER NOT NULL, `name` TEXT, `url` TEXT, `thumbnail_url` TEXT, `uploader` TEXT, `stream_count` INTEGER)",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "uid",
|
||||
"columnName": "uid",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "serviceId",
|
||||
"columnName": "service_id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "name",
|
||||
"columnName": "name",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "url",
|
||||
"columnName": "url",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "thumbnailUrl",
|
||||
"columnName": "thumbnail_url",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "uploader",
|
||||
"columnName": "uploader",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "streamCount",
|
||||
"columnName": "stream_count",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"columnNames": [
|
||||
"uid"
|
||||
],
|
||||
"autoGenerate": true
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_remote_playlists_name",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"name"
|
||||
],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_remote_playlists_name` ON `${TABLE_NAME}` (`name`)"
|
||||
},
|
||||
{
|
||||
"name": "index_remote_playlists_service_id_url",
|
||||
"unique": true,
|
||||
"columnNames": [
|
||||
"service_id",
|
||||
"url"
|
||||
],
|
||||
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_remote_playlists_service_id_url` ON `${TABLE_NAME}` (`service_id`, `url`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "feed",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`stream_id` INTEGER NOT NULL, `subscription_id` INTEGER NOT NULL, PRIMARY KEY(`stream_id`, `subscription_id`), FOREIGN KEY(`stream_id`) REFERENCES `streams`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, FOREIGN KEY(`subscription_id`) REFERENCES `subscriptions`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "streamId",
|
||||
"columnName": "stream_id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "subscriptionId",
|
||||
"columnName": "subscription_id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"columnNames": [
|
||||
"stream_id",
|
||||
"subscription_id"
|
||||
],
|
||||
"autoGenerate": false
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_feed_subscription_id",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"subscription_id"
|
||||
],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_feed_subscription_id` ON `${TABLE_NAME}` (`subscription_id`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"table": "streams",
|
||||
"onDelete": "CASCADE",
|
||||
"onUpdate": "CASCADE",
|
||||
"columns": [
|
||||
"stream_id"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"uid"
|
||||
]
|
||||
},
|
||||
{
|
||||
"table": "subscriptions",
|
||||
"onDelete": "CASCADE",
|
||||
"onUpdate": "CASCADE",
|
||||
"columns": [
|
||||
"subscription_id"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"uid"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableName": "feed_group",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `icon_id` INTEGER NOT NULL, `sort_order` INTEGER NOT NULL)",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "uid",
|
||||
"columnName": "uid",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "name",
|
||||
"columnName": "name",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "icon",
|
||||
"columnName": "icon_id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "sortOrder",
|
||||
"columnName": "sort_order",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"columnNames": [
|
||||
"uid"
|
||||
],
|
||||
"autoGenerate": true
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_feed_group_sort_order",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"sort_order"
|
||||
],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_feed_group_sort_order` ON `${TABLE_NAME}` (`sort_order`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "feed_group_subscription_join",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`group_id` INTEGER NOT NULL, `subscription_id` INTEGER NOT NULL, PRIMARY KEY(`group_id`, `subscription_id`), FOREIGN KEY(`group_id`) REFERENCES `feed_group`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, FOREIGN KEY(`subscription_id`) REFERENCES `subscriptions`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "feedGroupId",
|
||||
"columnName": "group_id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "subscriptionId",
|
||||
"columnName": "subscription_id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"columnNames": [
|
||||
"group_id",
|
||||
"subscription_id"
|
||||
],
|
||||
"autoGenerate": false
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_feed_group_subscription_join_subscription_id",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"subscription_id"
|
||||
],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_feed_group_subscription_join_subscription_id` ON `${TABLE_NAME}` (`subscription_id`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"table": "feed_group",
|
||||
"onDelete": "CASCADE",
|
||||
"onUpdate": "CASCADE",
|
||||
"columns": [
|
||||
"group_id"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"uid"
|
||||
]
|
||||
},
|
||||
{
|
||||
"table": "subscriptions",
|
||||
"onDelete": "CASCADE",
|
||||
"onUpdate": "CASCADE",
|
||||
"columns": [
|
||||
"subscription_id"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"uid"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableName": "feed_last_updated",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`subscription_id` INTEGER NOT NULL, `last_updated` INTEGER, PRIMARY KEY(`subscription_id`), FOREIGN KEY(`subscription_id`) REFERENCES `subscriptions`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "subscriptionId",
|
||||
"columnName": "subscription_id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "lastUpdated",
|
||||
"columnName": "last_updated",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"columnNames": [
|
||||
"subscription_id"
|
||||
],
|
||||
"autoGenerate": false
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"table": "subscriptions",
|
||||
"onDelete": "CASCADE",
|
||||
"onUpdate": "CASCADE",
|
||||
"columns": [
|
||||
"subscription_id"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"uid"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"views": [],
|
||||
"setupQueries": [
|
||||
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'd8070091972a7011bce18aed62f80b90')"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,134 +0,0 @@
|
||||
package org.schabi.newpipe.database
|
||||
|
||||
import android.content.ContentValues
|
||||
import android.database.sqlite.SQLiteDatabase
|
||||
import androidx.room.Room
|
||||
import androidx.room.testing.MigrationTestHelper
|
||||
import androidx.sqlite.db.framework.FrameworkSQLiteOpenHelperFactory
|
||||
import androidx.test.core.app.ApplicationProvider
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.schabi.newpipe.extractor.stream.StreamType
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class AppDatabaseTest {
|
||||
companion object {
|
||||
private const val DEFAULT_SERVICE_ID = 0
|
||||
private const val DEFAULT_URL = "https://www.youtube.com/watch?v=cDphUib5iG4"
|
||||
private const val DEFAULT_TITLE = "Test Title"
|
||||
private val DEFAULT_TYPE = StreamType.VIDEO_STREAM
|
||||
private const val DEFAULT_DURATION = 480L
|
||||
private const val DEFAULT_UPLOADER_NAME = "Uploader Test"
|
||||
private const val DEFAULT_THUMBNAIL = "https://example.com/example.jpg"
|
||||
|
||||
private const val DEFAULT_SECOND_SERVICE_ID = 0
|
||||
private const val DEFAULT_SECOND_URL = "https://www.youtube.com/watch?v=ncQU6iBn5Fc"
|
||||
}
|
||||
|
||||
@get:Rule
|
||||
val testHelper = MigrationTestHelper(
|
||||
InstrumentationRegistry.getInstrumentation(),
|
||||
AppDatabase::class.java.canonicalName, FrameworkSQLiteOpenHelperFactory()
|
||||
)
|
||||
|
||||
@Test
|
||||
fun migrateDatabaseFrom2to3() {
|
||||
val databaseInV2 = testHelper.createDatabase(AppDatabase.DATABASE_NAME, Migrations.DB_VER_2)
|
||||
|
||||
databaseInV2.run {
|
||||
insert(
|
||||
"streams", SQLiteDatabase.CONFLICT_FAIL,
|
||||
ContentValues().apply {
|
||||
// put("uid", null)
|
||||
put("service_id", DEFAULT_SERVICE_ID)
|
||||
put("url", DEFAULT_URL)
|
||||
put("title", DEFAULT_TITLE)
|
||||
put("stream_type", DEFAULT_TYPE.name)
|
||||
put("duration", DEFAULT_DURATION)
|
||||
put("uploader", DEFAULT_UPLOADER_NAME)
|
||||
put("thumbnail_url", DEFAULT_THUMBNAIL)
|
||||
}
|
||||
)
|
||||
insert(
|
||||
"streams", SQLiteDatabase.CONFLICT_FAIL,
|
||||
ContentValues().apply {
|
||||
// put("uid", null)
|
||||
put("service_id", DEFAULT_SECOND_SERVICE_ID)
|
||||
put("url", DEFAULT_SECOND_URL)
|
||||
// put("title", null)
|
||||
// put("stream_type", null)
|
||||
// put("duration", null)
|
||||
// put("uploader", null)
|
||||
// put("thumbnail_url", null)
|
||||
}
|
||||
)
|
||||
insert(
|
||||
"streams", SQLiteDatabase.CONFLICT_FAIL,
|
||||
ContentValues().apply {
|
||||
// put("uid", null)
|
||||
put("service_id", DEFAULT_SERVICE_ID)
|
||||
// put("url", null)
|
||||
// put("title", null)
|
||||
// put("stream_type", null)
|
||||
// put("duration", null)
|
||||
// put("uploader", null)
|
||||
// put("thumbnail_url", null)
|
||||
}
|
||||
)
|
||||
close()
|
||||
}
|
||||
|
||||
testHelper.runMigrationsAndValidate(
|
||||
AppDatabase.DATABASE_NAME, Migrations.DB_VER_3,
|
||||
true, Migrations.MIGRATION_2_3
|
||||
)
|
||||
|
||||
val migratedDatabaseV3 = getMigratedDatabase()
|
||||
val listFromDB = migratedDatabaseV3.streamDAO().all.blockingFirst()
|
||||
|
||||
// Only expect 2, the one with the null url will be ignored
|
||||
assertEquals(2, listFromDB.size)
|
||||
|
||||
val streamFromMigratedDatabase = listFromDB[0]
|
||||
assertEquals(DEFAULT_SERVICE_ID, streamFromMigratedDatabase.serviceId)
|
||||
assertEquals(DEFAULT_URL, streamFromMigratedDatabase.url)
|
||||
assertEquals(DEFAULT_TITLE, streamFromMigratedDatabase.title)
|
||||
assertEquals(DEFAULT_TYPE, streamFromMigratedDatabase.streamType)
|
||||
assertEquals(DEFAULT_DURATION, streamFromMigratedDatabase.duration)
|
||||
assertEquals(DEFAULT_UPLOADER_NAME, streamFromMigratedDatabase.uploader)
|
||||
assertEquals(DEFAULT_THUMBNAIL, streamFromMigratedDatabase.thumbnailUrl)
|
||||
assertNull(streamFromMigratedDatabase.viewCount)
|
||||
assertNull(streamFromMigratedDatabase.textualUploadDate)
|
||||
assertNull(streamFromMigratedDatabase.uploadDate)
|
||||
assertNull(streamFromMigratedDatabase.isUploadDateApproximation)
|
||||
|
||||
val secondStreamFromMigratedDatabase = listFromDB[1]
|
||||
assertEquals(DEFAULT_SECOND_SERVICE_ID, secondStreamFromMigratedDatabase.serviceId)
|
||||
assertEquals(DEFAULT_SECOND_URL, secondStreamFromMigratedDatabase.url)
|
||||
assertEquals("", secondStreamFromMigratedDatabase.title)
|
||||
// Should fallback to VIDEO_STREAM
|
||||
assertEquals(StreamType.VIDEO_STREAM, secondStreamFromMigratedDatabase.streamType)
|
||||
assertEquals(0, secondStreamFromMigratedDatabase.duration)
|
||||
assertEquals("", secondStreamFromMigratedDatabase.uploader)
|
||||
assertEquals("", secondStreamFromMigratedDatabase.thumbnailUrl)
|
||||
assertNull(secondStreamFromMigratedDatabase.viewCount)
|
||||
assertNull(secondStreamFromMigratedDatabase.textualUploadDate)
|
||||
assertNull(secondStreamFromMigratedDatabase.uploadDate)
|
||||
assertNull(secondStreamFromMigratedDatabase.isUploadDateApproximation)
|
||||
}
|
||||
|
||||
private fun getMigratedDatabase(): AppDatabase {
|
||||
val database: AppDatabase = Room.databaseBuilder(
|
||||
ApplicationProvider.getApplicationContext(),
|
||||
AppDatabase::class.java, AppDatabase.DATABASE_NAME
|
||||
)
|
||||
.build()
|
||||
testHelper.closeWhenFinished(database)
|
||||
return database
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
package org.schabi.newpipe.local.playlist
|
||||
|
||||
import androidx.room.Room
|
||||
import androidx.test.core.app.ApplicationProvider
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.rules.Timeout
|
||||
import org.schabi.newpipe.database.AppDatabase
|
||||
import org.schabi.newpipe.database.stream.model.StreamEntity
|
||||
import org.schabi.newpipe.extractor.stream.StreamType
|
||||
import org.schabi.newpipe.testUtil.TrampolineSchedulerRule
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class LocalPlaylistManagerTest {
|
||||
|
||||
private lateinit var manager: LocalPlaylistManager
|
||||
private lateinit var database: AppDatabase
|
||||
|
||||
@get:Rule
|
||||
val trampolineScheduler = TrampolineSchedulerRule()
|
||||
|
||||
@get:Rule
|
||||
val timeout = Timeout(10, TimeUnit.SECONDS)
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
database = Room.inMemoryDatabaseBuilder(
|
||||
ApplicationProvider.getApplicationContext(),
|
||||
AppDatabase::class.java
|
||||
)
|
||||
.allowMainThreadQueries()
|
||||
.build()
|
||||
|
||||
manager = LocalPlaylistManager(database)
|
||||
}
|
||||
|
||||
@After
|
||||
fun cleanUp() {
|
||||
database.close()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun createPlaylist() {
|
||||
val NEWPIPE_URL = "https://newpipe.net/"
|
||||
val stream = StreamEntity(
|
||||
serviceId = 1, url = NEWPIPE_URL, title = "title",
|
||||
streamType = StreamType.VIDEO_STREAM, duration = 1, uploader = "uploader",
|
||||
uploaderUrl = NEWPIPE_URL
|
||||
)
|
||||
|
||||
val result = manager.createPlaylist("name", listOf(stream))
|
||||
|
||||
// This should not behave like this.
|
||||
// Currently list of all stream ids is returned instead of playlist id
|
||||
result.test().await().assertValue(listOf(1L))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun createPlaylist_emptyPlaylistMustReturnEmpty() {
|
||||
val result = manager.createPlaylist("name", emptyList())
|
||||
|
||||
// This should not behave like this.
|
||||
// It should throw an error because currently the result is null
|
||||
result.test().await().assertComplete()
|
||||
manager.playlists.test().awaitCount(1).assertValue(emptyList())
|
||||
}
|
||||
|
||||
@Test()
|
||||
fun createPlaylist_nonExistentStreamsAreUpserted() {
|
||||
val stream = StreamEntity(
|
||||
serviceId = 1, url = "https://newpipe.net/", title = "title",
|
||||
streamType = StreamType.VIDEO_STREAM, duration = 1, uploader = "uploader",
|
||||
uploaderUrl = "https://newpipe.net/"
|
||||
)
|
||||
database.streamDAO().insert(stream)
|
||||
val upserted = StreamEntity(
|
||||
serviceId = 1, url = "https://newpipe.net/2", title = "title2",
|
||||
streamType = StreamType.VIDEO_STREAM, duration = 1, uploader = "uploader",
|
||||
uploaderUrl = "https://newpipe.net/"
|
||||
)
|
||||
|
||||
val result = manager.createPlaylist("name", listOf(stream, upserted))
|
||||
|
||||
result.test().await().assertComplete()
|
||||
database.streamDAO().all.test().awaitCount(1).assertValue(listOf(stream, upserted))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package org.schabi.newpipe.testUtil
|
||||
|
||||
import io.reactivex.rxjava3.android.plugins.RxAndroidPlugins
|
||||
import io.reactivex.rxjava3.plugins.RxJavaPlugins
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import org.junit.rules.TestRule
|
||||
import org.junit.runner.Description
|
||||
import org.junit.runners.model.Statement
|
||||
|
||||
/**
|
||||
* Always run on [Schedulers.trampoline].
|
||||
* This executes the task in the current thread in FIFO manner.
|
||||
* This ensures that tasks are run quickly inside the tests
|
||||
* and not scheduled away to another thread for later execution
|
||||
*/
|
||||
class TrampolineSchedulerRule : TestRule {
|
||||
|
||||
private val scheduler = Schedulers.trampoline()
|
||||
|
||||
override fun apply(base: Statement, description: Description): Statement =
|
||||
object : Statement() {
|
||||
override fun evaluate() {
|
||||
try {
|
||||
RxJavaPlugins.setComputationSchedulerHandler { scheduler }
|
||||
RxJavaPlugins.setIoSchedulerHandler { scheduler }
|
||||
RxJavaPlugins.setNewThreadSchedulerHandler { scheduler }
|
||||
RxJavaPlugins.setSingleSchedulerHandler { scheduler }
|
||||
RxAndroidPlugins.setInitMainThreadSchedulerHandler { scheduler }
|
||||
|
||||
base.evaluate()
|
||||
} finally {
|
||||
RxJavaPlugins.reset()
|
||||
RxAndroidPlugins.reset()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import android.os.Bundle;
|
||||
import androidx.preference.Preference;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.util.PicassoHelper;
|
||||
|
||||
import leakcanary.LeakCanary;
|
||||
|
||||
@@ -15,10 +16,13 @@ public class DebugSettingsFragment extends BasePreferenceFragment {
|
||||
|
||||
final Preference showMemoryLeaksPreference
|
||||
= findPreference(getString(R.string.show_memory_leaks_key));
|
||||
final Preference showImageIndicatorsPreference
|
||||
= findPreference(getString(R.string.show_image_indicators_key));
|
||||
final Preference crashTheAppPreference
|
||||
= findPreference(getString(R.string.crash_the_app_key));
|
||||
|
||||
assert showMemoryLeaksPreference != null;
|
||||
assert showImageIndicatorsPreference != null;
|
||||
assert crashTheAppPreference != null;
|
||||
|
||||
showMemoryLeaksPreference.setOnPreferenceClickListener(preference -> {
|
||||
@@ -26,6 +30,11 @@ public class DebugSettingsFragment extends BasePreferenceFragment {
|
||||
return true;
|
||||
});
|
||||
|
||||
showImageIndicatorsPreference.setOnPreferenceChangeListener((preference, newValue) -> {
|
||||
PicassoHelper.setIndicatorsEnabled((Boolean) newValue);
|
||||
return true;
|
||||
});
|
||||
|
||||
crashTheAppPreference.setOnPreferenceClickListener(preference -> {
|
||||
throw new RuntimeException();
|
||||
});
|
||||
|
||||
@@ -146,6 +146,7 @@
|
||||
<data android:pathPrefix="/embed/" />
|
||||
<data android:pathPrefix="/watch" />
|
||||
<data android:pathPrefix="/attribution_link" />
|
||||
<data android:pathPrefix="/shorts/" />
|
||||
<!-- channel prefix -->
|
||||
<data android:pathPrefix="/channel/" />
|
||||
<data android:pathPrefix="/user/" />
|
||||
@@ -224,6 +225,7 @@
|
||||
|
||||
<data android:scheme="http" />
|
||||
<data android:scheme="https" />
|
||||
<data android:host="tubus.eduvid.org" />
|
||||
<data android:host="invidio.us" />
|
||||
<data android:host="dev.invidio.us" />
|
||||
<data android:host="www.invidio.us" />
|
||||
@@ -309,6 +311,7 @@
|
||||
<data android:scheme="http" />
|
||||
<data android:scheme="https" />
|
||||
|
||||
<data android:host="eduvid.org" />
|
||||
<data android:host="framatube.org" />
|
||||
<data android:host="media.assassinate-you.net" />
|
||||
<data android:host="peertube.co.uk" />
|
||||
@@ -358,6 +361,9 @@
|
||||
<service
|
||||
android:name=".RouterActivity$FetcherService"
|
||||
android:exported="false" />
|
||||
<service
|
||||
android:name=".CheckForNewAppVersion"
|
||||
android:exported="false" />
|
||||
|
||||
<!-- opting out of sending metrics to Google in Android System WebView -->
|
||||
<meta-data android:name="android.webkit.WebView.MetricsOptOut" android:value="true" />
|
||||
|
||||
@@ -1,20 +1,16 @@
|
||||
package org.schabi.newpipe;
|
||||
|
||||
import android.app.NotificationChannel;
|
||||
import android.app.NotificationManager;
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.Build;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.app.NotificationChannelCompat;
|
||||
import androidx.core.app.NotificationManagerCompat;
|
||||
import androidx.multidex.MultiDexApplication;
|
||||
import androidx.preference.PreferenceManager;
|
||||
|
||||
import com.nostra13.universalimageloader.cache.memory.impl.LRULimitedMemoryCache;
|
||||
import com.nostra13.universalimageloader.core.ImageLoader;
|
||||
import com.nostra13.universalimageloader.core.ImageLoaderConfiguration;
|
||||
import com.jakewharton.processphoenix.ProcessPhoenix;
|
||||
|
||||
import org.acra.ACRA;
|
||||
import org.acra.config.ACRAConfigurationException;
|
||||
@@ -29,6 +25,7 @@ import org.schabi.newpipe.extractor.downloader.Downloader;
|
||||
import org.schabi.newpipe.ktx.ExceptionUtils;
|
||||
import org.schabi.newpipe.settings.NewPipeSettings;
|
||||
import org.schabi.newpipe.util.Localization;
|
||||
import org.schabi.newpipe.util.PicassoHelper;
|
||||
import org.schabi.newpipe.util.ServiceHelper;
|
||||
import org.schabi.newpipe.util.StateSaver;
|
||||
|
||||
@@ -39,7 +36,6 @@ import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import io.reactivex.rxjava3.disposables.Disposable;
|
||||
import io.reactivex.rxjava3.exceptions.CompositeException;
|
||||
import io.reactivex.rxjava3.exceptions.MissingBackpressureException;
|
||||
import io.reactivex.rxjava3.exceptions.OnErrorNotImplementedException;
|
||||
@@ -66,12 +62,9 @@ import io.reactivex.rxjava3.plugins.RxJavaPlugins;
|
||||
*/
|
||||
|
||||
public class App extends MultiDexApplication {
|
||||
protected static final String TAG = App.class.toString();
|
||||
private static App app;
|
||||
public static final String PACKAGE_NAME = BuildConfig.APPLICATION_ID;
|
||||
|
||||
@Nullable
|
||||
private Disposable disposable = null;
|
||||
private static final String TAG = App.class.toString();
|
||||
private static App app;
|
||||
|
||||
@NonNull
|
||||
public static App getApp() {
|
||||
@@ -90,6 +83,12 @@ public class App extends MultiDexApplication {
|
||||
|
||||
app = this;
|
||||
|
||||
if (ProcessPhoenix.isPhoenixProcess(this)) {
|
||||
Log.i(TAG, "This is a phoenix process! "
|
||||
+ "Aborting initialization of App[onCreate]");
|
||||
return;
|
||||
}
|
||||
|
||||
// Initialize settings first because others inits can use its values
|
||||
NewPipeSettings.initSettings(this);
|
||||
|
||||
@@ -104,20 +103,20 @@ public class App extends MultiDexApplication {
|
||||
ServiceHelper.initServices(this);
|
||||
|
||||
// Initialize image loader
|
||||
ImageLoader.getInstance().init(getImageLoaderConfigurations(10, 50));
|
||||
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
|
||||
PicassoHelper.init(this);
|
||||
PicassoHelper.setShouldLoadImages(
|
||||
prefs.getBoolean(getString(R.string.download_thumbnail_key), true));
|
||||
PicassoHelper.setIndicatorsEnabled(MainActivity.DEBUG
|
||||
&& prefs.getBoolean(getString(R.string.show_image_indicators_key), false));
|
||||
|
||||
configureRxJavaErrorHandler();
|
||||
|
||||
// Check for new version
|
||||
disposable = CheckForNewAppVersion.checkNewVersion(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTerminate() {
|
||||
if (disposable != null) {
|
||||
disposable.dispose();
|
||||
}
|
||||
super.onTerminate();
|
||||
PicassoHelper.terminate();
|
||||
}
|
||||
|
||||
protected Downloader getDownloader() {
|
||||
@@ -202,15 +201,6 @@ public class App extends MultiDexApplication {
|
||||
});
|
||||
}
|
||||
|
||||
private ImageLoaderConfiguration getImageLoaderConfigurations(final int memoryCacheSizeMb,
|
||||
final int diskCacheSizeMb) {
|
||||
return new ImageLoaderConfiguration.Builder(this)
|
||||
.memoryCache(new LRULimitedMemoryCache(memoryCacheSizeMb * 1024 * 1024))
|
||||
.diskCacheSize(diskCacheSizeMb * 1024 * 1024)
|
||||
.imageDownloader(new ImageDownloader(getApplicationContext()))
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Called in {@link #attachBaseContext(Context)} after calling the {@code super} method.
|
||||
* Should be overridden if MultiDex is enabled, since it has to be initialized before ACRA.
|
||||
@@ -233,42 +223,36 @@ public class App extends MultiDexApplication {
|
||||
}
|
||||
|
||||
private void initNotificationChannels() {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
|
||||
return;
|
||||
}
|
||||
// Keep the importance below DEFAULT to avoid making noise on every notification update for
|
||||
// the main and update channels
|
||||
final NotificationChannelCompat mainChannel = new NotificationChannelCompat
|
||||
.Builder(getString(R.string.notification_channel_id),
|
||||
NotificationManagerCompat.IMPORTANCE_LOW)
|
||||
.setName(getString(R.string.notification_channel_name))
|
||||
.setDescription(getString(R.string.notification_channel_description))
|
||||
.build();
|
||||
|
||||
String id = getString(R.string.notification_channel_id);
|
||||
String name = getString(R.string.notification_channel_name);
|
||||
String description = getString(R.string.notification_channel_description);
|
||||
final NotificationChannelCompat appUpdateChannel = new NotificationChannelCompat
|
||||
.Builder(getString(R.string.app_update_notification_channel_id),
|
||||
NotificationManagerCompat.IMPORTANCE_LOW)
|
||||
.setName(getString(R.string.app_update_notification_channel_name))
|
||||
.setDescription(getString(R.string.app_update_notification_channel_description))
|
||||
.build();
|
||||
|
||||
// Keep this below DEFAULT to avoid making noise on every notification update for the main
|
||||
// and update channels
|
||||
int importance = NotificationManager.IMPORTANCE_LOW;
|
||||
final NotificationChannelCompat hashChannel = new NotificationChannelCompat
|
||||
.Builder(getString(R.string.hash_channel_id),
|
||||
NotificationManagerCompat.IMPORTANCE_HIGH)
|
||||
.setName(getString(R.string.hash_channel_name))
|
||||
.setDescription(getString(R.string.hash_channel_description))
|
||||
.build();
|
||||
|
||||
final NotificationChannel mainChannel = new NotificationChannel(id, name, importance);
|
||||
mainChannel.setDescription(description);
|
||||
|
||||
id = getString(R.string.app_update_notification_channel_id);
|
||||
name = getString(R.string.app_update_notification_channel_name);
|
||||
description = getString(R.string.app_update_notification_channel_description);
|
||||
|
||||
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,
|
||||
final NotificationManagerCompat notificationManager = NotificationManagerCompat.from(this);
|
||||
notificationManager.createNotificationChannelsCompat(Arrays.asList(mainChannel,
|
||||
appUpdateChannel, hashChannel));
|
||||
}
|
||||
|
||||
protected boolean isDisposedRxExceptionsReported() {
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -10,16 +10,13 @@ import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
|
||||
import com.nostra13.universalimageloader.core.ImageLoader;
|
||||
|
||||
import icepick.Icepick;
|
||||
import icepick.State;
|
||||
import leakcanary.AppWatcher;
|
||||
|
||||
public abstract class BaseFragment extends Fragment {
|
||||
public static final ImageLoader IMAGE_LOADER = ImageLoader.getInstance();
|
||||
protected final String TAG = getClass().getSimpleName() + "@" + Integer.toHexString(hashCode());
|
||||
protected final boolean DEBUG = MainActivity.DEBUG;
|
||||
protected static final boolean DEBUG = MainActivity.DEBUG;
|
||||
protected AppCompatActivity activity;
|
||||
//These values are used for controlling fragments when they are part of the frontpage
|
||||
@State
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
package org.schabi.newpipe;
|
||||
|
||||
import android.app.Application;
|
||||
import android.app.IntentService;
|
||||
import android.app.PendingIntent;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.pm.PackageInfo;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.content.pm.Signature;
|
||||
import android.net.ConnectivityManager;
|
||||
@@ -16,6 +16,7 @@ import androidx.annotation.Nullable;
|
||||
import androidx.core.app.NotificationCompat;
|
||||
import androidx.core.app.NotificationManagerCompat;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.core.content.pm.PackageInfoCompat;
|
||||
import androidx.preference.PreferenceManager;
|
||||
|
||||
import com.grack.nanojson.JsonObject;
|
||||
@@ -25,8 +26,11 @@ import com.grack.nanojson.JsonParserException;
|
||||
import org.schabi.newpipe.error.ErrorActivity;
|
||||
import org.schabi.newpipe.error.ErrorInfo;
|
||||
import org.schabi.newpipe.error.UserAction;
|
||||
import org.schabi.newpipe.extractor.downloader.Response;
|
||||
import org.schabi.newpipe.extractor.exceptions.ReCaptchaException;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
@@ -34,14 +38,12 @@ import java.security.cert.CertificateEncodingException;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.security.cert.CertificateFactory;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.List;
|
||||
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.rxjava3.core.Maybe;
|
||||
import io.reactivex.rxjava3.disposables.Disposable;
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers;
|
||||
|
||||
public final class CheckForNewAppVersion {
|
||||
private CheckForNewAppVersion() { }
|
||||
public final class CheckForNewAppVersion extends IntentService {
|
||||
public CheckForNewAppVersion() {
|
||||
super("CheckForNewAppVersion");
|
||||
}
|
||||
|
||||
private static final boolean DEBUG = MainActivity.DEBUG;
|
||||
private static final String TAG = CheckForNewAppVersion.class.getSimpleName();
|
||||
@@ -58,20 +60,22 @@ public final class CheckForNewAppVersion {
|
||||
*/
|
||||
@NonNull
|
||||
private static String getCertificateSHA1Fingerprint(@NonNull final Application application) {
|
||||
final PackageInfo packageInfo;
|
||||
final List<Signature> signatures;
|
||||
try {
|
||||
packageInfo = application.getPackageManager().getPackageInfo(
|
||||
application.getPackageName(), PackageManager.GET_SIGNATURES);
|
||||
signatures = PackageInfoCompat.getSignatures(application.getPackageManager(),
|
||||
application.getPackageName());
|
||||
} catch (final PackageManager.NameNotFoundException e) {
|
||||
ErrorActivity.reportError(application, new ErrorInfo(e,
|
||||
UserAction.CHECK_FOR_NEW_APP_VERSION, "Could not find package info"));
|
||||
return "";
|
||||
}
|
||||
if (signatures.isEmpty()) {
|
||||
return "";
|
||||
}
|
||||
|
||||
final X509Certificate c;
|
||||
try {
|
||||
final Signature[] signatures = packageInfo.signatures;
|
||||
final byte[] cert = signatures[0].toByteArray();
|
||||
final byte[] cert = signatures.get(0).toByteArray();
|
||||
final InputStream input = new ByteArrayInputStream(cert);
|
||||
final CertificateFactory cf = CertificateFactory.getInstance("X509");
|
||||
c = (X509Certificate) cf.generateCertificate(input);
|
||||
@@ -165,78 +169,104 @@ public final class CheckForNewAppVersion {
|
||||
return getCertificateSHA1Fingerprint(app).equals(GITHUB_APK_SHA1);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static Disposable checkNewVersion(@NonNull final App app) {
|
||||
private void checkNewVersion() throws IOException, ReCaptchaException {
|
||||
final App app = App.getApp();
|
||||
|
||||
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(app);
|
||||
final NewVersionManager manager = new NewVersionManager();
|
||||
|
||||
// Check if user has enabled/disabled update checking
|
||||
// and if the current apk is a github one or not.
|
||||
if (!prefs.getBoolean(app.getString(R.string.update_app_key), true) || !isGithubApk(app)) {
|
||||
return null;
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the last request has happened a certain time ago
|
||||
// to reduce the number of API requests.
|
||||
final long expiry = prefs.getLong(app.getString(R.string.update_expiry_key), 0);
|
||||
if (!manager.isExpired(expiry)) {
|
||||
return null;
|
||||
return;
|
||||
}
|
||||
|
||||
return Maybe
|
||||
.fromCallable(() -> {
|
||||
if (!isConnected(app)) {
|
||||
return null;
|
||||
}
|
||||
// Make a network request to get latest NewPipe data.
|
||||
final Response response = DownloaderImpl.getInstance().get(NEWPIPE_API_URL);
|
||||
handleResponse(response, manager, prefs, app);
|
||||
}
|
||||
|
||||
// Make a network request to get latest NewPipe data.
|
||||
return DownloaderImpl.getInstance().get(NEWPIPE_API_URL);
|
||||
})
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
response -> {
|
||||
try {
|
||||
// Store a timestamp which needs to be exceeded,
|
||||
// before a new request to the API is made.
|
||||
final long newExpiry = manager
|
||||
.coerceExpiry(response.getHeader("expires"));
|
||||
prefs.edit()
|
||||
.putLong(app.getString(R.string.update_expiry_key), newExpiry)
|
||||
.apply();
|
||||
} catch (final Exception e) {
|
||||
if (DEBUG) {
|
||||
Log.w(TAG, "Could not extract and save new expiry date", e);
|
||||
}
|
||||
}
|
||||
private void handleResponse(@NonNull final Response response,
|
||||
@NonNull final NewVersionManager manager,
|
||||
@NonNull final SharedPreferences prefs,
|
||||
@NonNull final App app) {
|
||||
try {
|
||||
// Store a timestamp which needs to be exceeded,
|
||||
// before a new request to the API is made.
|
||||
final long newExpiry = manager
|
||||
.coerceExpiry(response.getHeader("expires"));
|
||||
prefs.edit()
|
||||
.putLong(app.getString(R.string.update_expiry_key), newExpiry)
|
||||
.apply();
|
||||
} catch (final Exception e) {
|
||||
if (DEBUG) {
|
||||
Log.w(TAG, "Could not extract and save new expiry date", e);
|
||||
}
|
||||
}
|
||||
|
||||
// Parse the json from the response.
|
||||
try {
|
||||
final JsonObject githubStableObject = JsonParser.object()
|
||||
.from(response.responseBody()).getObject("flavors")
|
||||
.getObject("github").getObject("stable");
|
||||
// Parse the json from the response.
|
||||
try {
|
||||
final JsonObject githubStableObject = JsonParser.object()
|
||||
.from(response.responseBody()).getObject("flavors")
|
||||
.getObject("github").getObject("stable");
|
||||
|
||||
final String versionName = githubStableObject
|
||||
.getString("version");
|
||||
final int versionCode = githubStableObject
|
||||
.getInt("version_code");
|
||||
final String apkLocationUrl = githubStableObject
|
||||
.getString("apk");
|
||||
final String versionName = githubStableObject
|
||||
.getString("version");
|
||||
final int versionCode = githubStableObject
|
||||
.getInt("version_code");
|
||||
final String apkLocationUrl = githubStableObject
|
||||
.getString("apk");
|
||||
|
||||
compareAppVersionAndShowNotification(app, versionName,
|
||||
apkLocationUrl, versionCode);
|
||||
} catch (final JsonParserException e) {
|
||||
// Most likely something is wrong in data received from NEWPIPE_API_URL.
|
||||
// Do not alarm user and fail silently.
|
||||
if (DEBUG) {
|
||||
Log.w(TAG, "Could not get NewPipe API: invalid json", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Start a new service which
|
||||
* checks if all conditions for performing a version check are met,
|
||||
* fetches the API endpoint {@link #NEWPIPE_API_URL} containing info
|
||||
* about the latest NewPipe version
|
||||
* and displays a notification about ana available update.
|
||||
* <br>
|
||||
* Following conditions need to be met, before data is request from the server:
|
||||
* <ul>
|
||||
* <li> The app is signed with the correct signing key (by TeamNewPipe / schabi).
|
||||
* If the signing key differs from the one used upstream, the update cannot be installed.</li>
|
||||
* <li>The user enabled searching for and notifying about updates in the settings.</li>
|
||||
* <li>The app did not recently check for updates.
|
||||
* We do not want to make unnecessary connections and DOS our servers.</li>
|
||||
* </ul>
|
||||
* <b>Must not be executed</b> when the app is in background.
|
||||
*/
|
||||
public static void startNewVersionCheckService() {
|
||||
final Intent intent = new Intent(App.getApp().getApplicationContext(),
|
||||
CheckForNewAppVersion.class);
|
||||
App.getApp().startService(intent);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onHandleIntent(@Nullable final Intent intent) {
|
||||
try {
|
||||
checkNewVersion();
|
||||
} catch (final IOException e) {
|
||||
Log.w(TAG, "Could not fetch NewPipe API: probably network problem", e);
|
||||
} catch (final ReCaptchaException e) {
|
||||
Log.e(TAG, "ReCaptchaException should never happen here.", e);
|
||||
}
|
||||
|
||||
compareAppVersionAndShowNotification(app, versionName,
|
||||
apkLocationUrl, versionCode);
|
||||
} catch (final JsonParserException e) {
|
||||
// connectivity problems, do not alarm user and fail silently
|
||||
if (DEBUG) {
|
||||
Log.w(TAG, "Could not get NewPipe API: invalid json", e);
|
||||
}
|
||||
}
|
||||
},
|
||||
e -> {
|
||||
// connectivity problems, do not alarm user and fail silently
|
||||
if (DEBUG) {
|
||||
Log.w(TAG, "Could not get NewPipe API: network problem", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,6 @@ import org.schabi.newpipe.util.InfoCache;
|
||||
import org.schabi.newpipe.util.TLSSocketFactoryCompat;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.security.KeyManagementException;
|
||||
import java.security.KeyStore;
|
||||
import java.security.KeyStoreException;
|
||||
@@ -194,36 +193,6 @@ public final class DownloaderImpl extends Downloader {
|
||||
}
|
||||
}
|
||||
|
||||
public InputStream stream(final String siteUrl) throws IOException {
|
||||
try {
|
||||
final okhttp3.Request.Builder requestBuilder = new okhttp3.Request.Builder()
|
||||
.method("GET", null).url(siteUrl)
|
||||
.addHeader("User-Agent", USER_AGENT);
|
||||
|
||||
final String cookies = getCookies(siteUrl);
|
||||
if (!cookies.isEmpty()) {
|
||||
requestBuilder.addHeader("Cookie", cookies);
|
||||
}
|
||||
|
||||
final okhttp3.Request request = requestBuilder.build();
|
||||
final okhttp3.Response response = client.newCall(request).execute();
|
||||
final ResponseBody body = response.body();
|
||||
|
||||
if (response.code() == 429) {
|
||||
throw new ReCaptchaException("reCaptcha Challenge requested", siteUrl);
|
||||
}
|
||||
|
||||
if (body == null) {
|
||||
response.close();
|
||||
return null;
|
||||
}
|
||||
|
||||
return body.byteStream();
|
||||
} catch (final ReCaptchaException e) {
|
||||
throw new IOException(e.getMessage(), e.getCause());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Response execute(@NonNull final Request request)
|
||||
throws IOException, ReCaptchaException {
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
package org.schabi.newpipe;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.res.Resources;
|
||||
|
||||
import androidx.preference.PreferenceManager;
|
||||
|
||||
import com.nostra13.universalimageloader.core.download.BaseImageDownloader;
|
||||
|
||||
import org.schabi.newpipe.extractor.NewPipe;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
public class ImageDownloader extends BaseImageDownloader {
|
||||
private final Resources resources;
|
||||
private final SharedPreferences preferences;
|
||||
private final String downloadThumbnailKey;
|
||||
|
||||
public ImageDownloader(final Context context) {
|
||||
super(context);
|
||||
this.resources = context.getResources();
|
||||
this.preferences = PreferenceManager.getDefaultSharedPreferences(context);
|
||||
this.downloadThumbnailKey = context.getString(R.string.download_thumbnail_key);
|
||||
}
|
||||
|
||||
private boolean isDownloadingThumbnail() {
|
||||
return preferences.getBoolean(downloadThumbnailKey, true);
|
||||
}
|
||||
|
||||
@SuppressLint("ResourceType")
|
||||
@Override
|
||||
public InputStream getStream(final String imageUri, final Object extra) throws IOException {
|
||||
if (isDownloadingThumbnail()) {
|
||||
return super.getStream(imageUri, extra);
|
||||
} else {
|
||||
return resources.openRawResource(R.drawable.dummy_thumbnail_dark);
|
||||
}
|
||||
}
|
||||
|
||||
protected InputStream getStreamFromNetwork(final String imageUri, final Object extra)
|
||||
throws IOException {
|
||||
final DownloaderImpl downloader = (DownloaderImpl) NewPipe.getDownloader();
|
||||
return downloader.stream(imageUri);
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,9 @@
|
||||
|
||||
package org.schabi.newpipe;
|
||||
|
||||
import static org.schabi.newpipe.CheckForNewAppVersion.startNewVersionCheckService;
|
||||
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
|
||||
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
@@ -91,8 +94,6 @@ import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
|
||||
|
||||
public class MainActivity extends AppCompatActivity {
|
||||
private static final String TAG = "MainActivity";
|
||||
@SuppressWarnings("ConstantConditions")
|
||||
@@ -165,6 +166,15 @@ public class MainActivity extends AppCompatActivity {
|
||||
openMiniPlayerUponPlayerStarted();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostCreate(final Bundle savedInstanceState) {
|
||||
super.onPostCreate(savedInstanceState);
|
||||
// Start the service which is checking all conditions
|
||||
// and eventually searching for a new version.
|
||||
// The service searching for a new NewPipe version must not be started in background.
|
||||
startNewVersionCheckService();
|
||||
}
|
||||
|
||||
private void setupDrawer() throws Exception {
|
||||
//Tabs
|
||||
final int currentServiceId = ServiceHelper.getSelectedServiceId(this);
|
||||
@@ -402,7 +412,7 @@ public class MainActivity extends AppCompatActivity {
|
||||
new Handler(Looper.getMainLooper()).postDelayed(() -> {
|
||||
getSupportFragmentManager().popBackStack(null,
|
||||
FragmentManager.POP_BACK_STACK_INCLUSIVE);
|
||||
recreate();
|
||||
ActivityCompat.recreate(MainActivity.this);
|
||||
}, 300);
|
||||
}
|
||||
|
||||
@@ -823,7 +833,7 @@ public class MainActivity extends AppCompatActivity {
|
||||
return;
|
||||
}
|
||||
|
||||
if (PlayerHolder.isPlayerOpen()) {
|
||||
if (PlayerHolder.getInstance().isPlayerOpen()) {
|
||||
// if the player is already open, no need for a broadcast receiver
|
||||
openMiniPlayerIfMissing();
|
||||
} else {
|
||||
|
||||
@@ -11,6 +11,7 @@ import org.schabi.newpipe.database.AppDatabase;
|
||||
import static org.schabi.newpipe.database.AppDatabase.DATABASE_NAME;
|
||||
import static org.schabi.newpipe.database.Migrations.MIGRATION_1_2;
|
||||
import static org.schabi.newpipe.database.Migrations.MIGRATION_2_3;
|
||||
import static org.schabi.newpipe.database.Migrations.MIGRATION_3_4;
|
||||
|
||||
public final class NewPipeDatabase {
|
||||
private static volatile AppDatabase databaseInstance;
|
||||
@@ -22,7 +23,7 @@ public final class NewPipeDatabase {
|
||||
private static AppDatabase getDatabase(final Context context) {
|
||||
return Room
|
||||
.databaseBuilder(context.getApplicationContext(), AppDatabase.class, DATABASE_NAME)
|
||||
.addMigrations(MIGRATION_1_2, MIGRATION_2_3)
|
||||
.addMigrations(MIGRATION_1_2, MIGRATION_2_3, MIGRATION_3_4)
|
||||
.build();
|
||||
}
|
||||
|
||||
|
||||
70
app/src/main/java/org/schabi/newpipe/QueueItemMenuUtil.java
Normal file
70
app/src/main/java/org/schabi/newpipe/QueueItemMenuUtil.java
Normal file
@@ -0,0 +1,70 @@
|
||||
package org.schabi.newpipe;
|
||||
|
||||
import static org.schabi.newpipe.util.external_communication.ShareUtils.shareText;
|
||||
|
||||
import android.content.Context;
|
||||
import android.view.ContextThemeWrapper;
|
||||
import android.view.View;
|
||||
import android.widget.PopupMenu;
|
||||
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
|
||||
import org.schabi.newpipe.local.dialog.PlaylistAppendDialog;
|
||||
import org.schabi.newpipe.local.dialog.PlaylistCreationDialog;
|
||||
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
||||
import org.schabi.newpipe.player.playqueue.PlayQueueItem;
|
||||
import org.schabi.newpipe.util.NavigationHelper;
|
||||
|
||||
import java.util.Collections;
|
||||
|
||||
public final class QueueItemMenuUtil {
|
||||
public static void openPopupMenu(final PlayQueue playQueue,
|
||||
final PlayQueueItem item,
|
||||
final View view,
|
||||
final boolean hideDetails,
|
||||
final FragmentManager fragmentManager,
|
||||
final Context context) {
|
||||
final ContextThemeWrapper themeWrapper =
|
||||
new ContextThemeWrapper(context, R.style.DarkPopupMenu);
|
||||
|
||||
final PopupMenu popupMenu = new PopupMenu(themeWrapper, view);
|
||||
popupMenu.inflate(R.menu.menu_play_queue_item);
|
||||
|
||||
if (hideDetails) {
|
||||
popupMenu.getMenu().findItem(R.id.menu_item_details).setVisible(false);
|
||||
}
|
||||
|
||||
popupMenu.setOnMenuItemClickListener(menuItem -> {
|
||||
switch (menuItem.getItemId()) {
|
||||
case R.id.menu_item_remove:
|
||||
final int index = playQueue.indexOf(item);
|
||||
playQueue.remove(index);
|
||||
return true;
|
||||
case R.id.menu_item_details:
|
||||
// playQueue is null since we don't want any queue change
|
||||
NavigationHelper.openVideoDetail(context, item.getServiceId(),
|
||||
item.getUrl(), item.getTitle(), null,
|
||||
false);
|
||||
return true;
|
||||
case R.id.menu_item_append_playlist:
|
||||
final PlaylistAppendDialog d = PlaylistAppendDialog.fromPlayQueueItems(
|
||||
Collections.singletonList(item)
|
||||
);
|
||||
PlaylistAppendDialog.onPlaylistFound(context,
|
||||
() -> d.show(fragmentManager, "QueueItemMenuUtil@append_playlist"),
|
||||
() -> PlaylistCreationDialog.newInstance(d)
|
||||
.show(fragmentManager, "QueueItemMenuUtil@append_playlist"));
|
||||
return true;
|
||||
case R.id.menu_item_share:
|
||||
shareText(context, item.getTitle(), item.getUrl(),
|
||||
item.getThumbnailUrl());
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
popupMenu.show();
|
||||
}
|
||||
|
||||
private QueueItemMenuUtil() { }
|
||||
}
|
||||
@@ -453,7 +453,7 @@ public class RouterActivity extends AppCompatActivity {
|
||||
returnList.add(showInfo);
|
||||
returnList.add(videoPlayer);
|
||||
} else {
|
||||
final MainPlayer.PlayerType playerType = PlayerHolder.getType();
|
||||
final MainPlayer.PlayerType playerType = PlayerHolder.getInstance().getType();
|
||||
if (capabilities.contains(VIDEO)
|
||||
&& PlayerHelper.isAutoplayAllowedByUser(context)
|
||||
&& playerType == null || playerType == MainPlayer.PlayerType.VIDEO) {
|
||||
|
||||
@@ -162,10 +162,18 @@ class AboutActivity : AppCompatActivity() {
|
||||
"OkHttp", "2019", "Square, Inc.",
|
||||
"https://square.github.io/okhttp/", StandardLicenses.APACHE2
|
||||
),
|
||||
SoftwareComponent(
|
||||
"Picasso", "2013", "Square, Inc.",
|
||||
"https://square.github.io/picasso/", StandardLicenses.APACHE2
|
||||
),
|
||||
SoftwareComponent(
|
||||
"PrettyTime", "2012 - 2020", "Lincoln Baxter, III",
|
||||
"https://github.com/ocpsoft/prettytime", StandardLicenses.APACHE2
|
||||
),
|
||||
SoftwareComponent(
|
||||
"ProcessPhoenix", "2015", "Jake Wharton",
|
||||
"https://github.com/JakeWharton/ProcessPhoenix", StandardLicenses.APACHE2
|
||||
),
|
||||
SoftwareComponent(
|
||||
"RxAndroid", "2015", "The RxAndroid authors",
|
||||
"https://github.com/ReactiveX/RxAndroid", StandardLicenses.APACHE2
|
||||
@@ -177,11 +185,6 @@ class AboutActivity : AppCompatActivity() {
|
||||
SoftwareComponent(
|
||||
"RxJava", "2016 - 2020", "RxJava Contributors",
|
||||
"https://github.com/ReactiveX/RxJava", StandardLicenses.APACHE2
|
||||
),
|
||||
SoftwareComponent(
|
||||
"Universal Image Loader", "2011 - 2015", "Sergey Tarasevich",
|
||||
"https://github.com/nostra13/Android-Universal-Image-Loader",
|
||||
StandardLicenses.APACHE2
|
||||
)
|
||||
)
|
||||
private const val POS_ABOUT = 0
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package org.schabi.newpipe.about
|
||||
|
||||
import android.os.Parcelable
|
||||
import kotlinx.android.parcel.Parcelize
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import java.io.Serializable
|
||||
|
||||
/**
|
||||
|
||||
@@ -11,8 +11,6 @@ import org.schabi.newpipe.R
|
||||
import org.schabi.newpipe.about.LicenseFragmentHelper.showLicense
|
||||
import org.schabi.newpipe.databinding.FragmentLicensesBinding
|
||||
import org.schabi.newpipe.databinding.ItemSoftwareComponentBinding
|
||||
import java.util.Arrays
|
||||
import java.util.Objects
|
||||
|
||||
/**
|
||||
* Fragment containing the software licenses.
|
||||
@@ -24,16 +22,10 @@ class LicenseFragment : Fragment() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
softwareComponents =
|
||||
arguments?.getParcelableArray(ARG_COMPONENTS) as Array<SoftwareComponent>
|
||||
if (savedInstanceState != null) {
|
||||
val license = savedInstanceState.getSerializable(LICENSE_KEY)
|
||||
if (license != null) {
|
||||
activeLicense = license as License?
|
||||
}
|
||||
}
|
||||
softwareComponents = arguments?.getParcelableArray(ARG_COMPONENTS) as Array<SoftwareComponent>
|
||||
activeLicense = savedInstanceState?.getSerializable(LICENSE_KEY) as? License
|
||||
// Sort components by name
|
||||
Arrays.sort(softwareComponents, Comparator.comparing(SoftwareComponent::name))
|
||||
softwareComponents.sortBy { it.name }
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
@@ -74,19 +66,13 @@ class LicenseFragment : Fragment() {
|
||||
binding.licensesSoftwareComponents.addView(root)
|
||||
registerForContextMenu(root)
|
||||
}
|
||||
if (activeLicense != null) {
|
||||
compositeDisposable.add(
|
||||
showLicense(activity, activeLicense!!)
|
||||
)
|
||||
}
|
||||
activeLicense?.let { compositeDisposable.add(showLicense(activity, it)) }
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(savedInstanceState: Bundle) {
|
||||
super.onSaveInstanceState(savedInstanceState)
|
||||
if (activeLicense != null) {
|
||||
savedInstanceState.putSerializable(LICENSE_KEY, activeLicense)
|
||||
}
|
||||
activeLicense?.let { savedInstanceState.putSerializable(LICENSE_KEY, it) }
|
||||
}
|
||||
|
||||
companion object {
|
||||
@@ -94,8 +80,7 @@ class LicenseFragment : Fragment() {
|
||||
private const val LICENSE_KEY = "ACTIVE_LICENSE"
|
||||
fun newInstance(softwareComponents: Array<SoftwareComponent>): LicenseFragment {
|
||||
val fragment = LicenseFragment()
|
||||
fragment.arguments =
|
||||
bundleOf(ARG_COMPONENTS to Objects.requireNonNull(softwareComponents))
|
||||
fragment.arguments = bundleOf(ARG_COMPONENTS to softwareComponents)
|
||||
return fragment
|
||||
}
|
||||
}
|
||||
|
||||
@@ -108,7 +108,7 @@ object LicenseFragmentHelper {
|
||||
alert.setView(webView)
|
||||
Localization.assureCorrectAppLanguage(context)
|
||||
alert.setNegativeButton(
|
||||
context.getString(R.string.finish)
|
||||
context.getString(R.string.ok)
|
||||
) { dialog, _ -> dialog.dismiss() }
|
||||
alert.show()
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package org.schabi.newpipe.about
|
||||
|
||||
import android.os.Parcelable
|
||||
import kotlinx.android.parcel.Parcelize
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Parcelize
|
||||
class SoftwareComponent
|
||||
|
||||
@@ -27,7 +27,7 @@ import org.schabi.newpipe.database.stream.model.StreamStateEntity;
|
||||
import org.schabi.newpipe.database.subscription.SubscriptionDAO;
|
||||
import org.schabi.newpipe.database.subscription.SubscriptionEntity;
|
||||
|
||||
import static org.schabi.newpipe.database.Migrations.DB_VER_3;
|
||||
import static org.schabi.newpipe.database.Migrations.DB_VER_4;
|
||||
|
||||
@TypeConverters({Converters.class})
|
||||
@Database(
|
||||
@@ -38,7 +38,7 @@ import static org.schabi.newpipe.database.Migrations.DB_VER_3;
|
||||
FeedEntity.class, FeedGroupEntity.class, FeedGroupSubscriptionEntity.class,
|
||||
FeedLastUpdatedEntity.class
|
||||
},
|
||||
version = DB_VER_3
|
||||
version = DB_VER_4
|
||||
)
|
||||
public abstract class AppDatabase extends RoomDatabase {
|
||||
public static final String DATABASE_NAME = "newpipe.db";
|
||||
|
||||
@@ -9,9 +9,19 @@ import androidx.sqlite.db.SupportSQLiteDatabase;
|
||||
import org.schabi.newpipe.MainActivity;
|
||||
|
||||
public final class Migrations {
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
// Test new migrations manually by importing a database from daily usage //
|
||||
// and checking if the migration works (Use the Database Inspector //
|
||||
// https://developer.android.com/studio/inspect/database). //
|
||||
// If you add a migration point it out in the pull request, so that //
|
||||
// others remember to test it themselves. //
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
public static final int DB_VER_1 = 1;
|
||||
public static final int DB_VER_2 = 2;
|
||||
public static final int DB_VER_3 = 3;
|
||||
public static final int DB_VER_4 = 4;
|
||||
|
||||
private static final String TAG = Migrations.class.getName();
|
||||
public static final boolean DEBUG = MainActivity.DEBUG;
|
||||
@@ -160,5 +170,14 @@ public final class Migrations {
|
||||
}
|
||||
};
|
||||
|
||||
public static final Migration MIGRATION_3_4 = new Migration(DB_VER_3, DB_VER_4) {
|
||||
@Override
|
||||
public void migrate(@NonNull final SupportSQLiteDatabase database) {
|
||||
database.execSQL(
|
||||
"ALTER TABLE streams ADD COLUMN uploader_url TEXT"
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
private Migrations() { }
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ data class PlaylistStreamEntry(
|
||||
val item = StreamInfoItem(streamEntity.serviceId, streamEntity.url, streamEntity.title, streamEntity.streamType)
|
||||
item.duration = streamEntity.duration
|
||||
item.uploaderName = streamEntity.uploader
|
||||
item.uploaderUrl = streamEntity.uploaderUrl
|
||||
item.thumbnailUrl = streamEntity.thumbnailUrl
|
||||
|
||||
return item
|
||||
|
||||
@@ -29,6 +29,7 @@ class StreamStatisticsEntry(
|
||||
val item = StreamInfoItem(streamEntity.serviceId, streamEntity.url, streamEntity.title, streamEntity.streamType)
|
||||
item.duration = streamEntity.duration
|
||||
item.uploaderName = streamEntity.uploader
|
||||
item.uploaderUrl = streamEntity.uploaderUrl
|
||||
item.thumbnailUrl = streamEntity.thumbnailUrl
|
||||
|
||||
return item
|
||||
|
||||
@@ -6,6 +6,7 @@ import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.Query
|
||||
import androidx.room.Transaction
|
||||
import io.reactivex.rxjava3.core.Completable
|
||||
import io.reactivex.rxjava3.core.Flowable
|
||||
import org.schabi.newpipe.database.BasicDAO
|
||||
import org.schabi.newpipe.database.stream.model.StreamEntity
|
||||
@@ -29,6 +30,9 @@ abstract class StreamDAO : BasicDAO<StreamEntity> {
|
||||
@Query("SELECT * FROM streams WHERE url = :url AND service_id = :serviceId")
|
||||
abstract fun getStream(serviceId: Long, url: String): Flowable<List<StreamEntity>>
|
||||
|
||||
@Query("UPDATE streams SET uploader_url = :uploaderUrl WHERE url = :url AND service_id = :serviceId")
|
||||
abstract fun setUploaderUrl(serviceId: Long, url: String, uploaderUrl: String): Completable
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
||||
internal abstract fun silentInsertInternal(stream: StreamEntity): Long
|
||||
|
||||
|
||||
@@ -45,6 +45,9 @@ data class StreamEntity(
|
||||
@ColumnInfo(name = STREAM_UPLOADER)
|
||||
var uploader: String,
|
||||
|
||||
@ColumnInfo(name = STREAM_UPLOADER_URL)
|
||||
var uploaderUrl: String? = null,
|
||||
|
||||
@ColumnInfo(name = STREAM_THUMBNAIL_URL)
|
||||
var thumbnailUrl: String? = null,
|
||||
|
||||
@@ -64,7 +67,7 @@ data class StreamEntity(
|
||||
constructor(item: StreamInfoItem) : this(
|
||||
serviceId = item.serviceId, url = item.url, title = item.name,
|
||||
streamType = item.streamType, duration = item.duration, uploader = item.uploaderName,
|
||||
thumbnailUrl = item.thumbnailUrl, viewCount = item.viewCount,
|
||||
uploaderUrl = item.uploaderUrl, thumbnailUrl = item.thumbnailUrl, viewCount = item.viewCount,
|
||||
textualUploadDate = item.textualUploadDate, uploadDate = item.uploadDate?.offsetDateTime(),
|
||||
isUploadDateApproximation = item.uploadDate?.isApproximation
|
||||
)
|
||||
@@ -73,7 +76,7 @@ data class StreamEntity(
|
||||
constructor(info: StreamInfo) : this(
|
||||
serviceId = info.serviceId, url = info.url, title = info.name,
|
||||
streamType = info.streamType, duration = info.duration, uploader = info.uploaderName,
|
||||
thumbnailUrl = info.thumbnailUrl, viewCount = info.viewCount,
|
||||
uploaderUrl = info.uploaderUrl, thumbnailUrl = info.thumbnailUrl, viewCount = info.viewCount,
|
||||
textualUploadDate = info.textualUploadDate, uploadDate = info.uploadDate?.offsetDateTime(),
|
||||
isUploadDateApproximation = info.uploadDate?.isApproximation
|
||||
)
|
||||
@@ -82,13 +85,14 @@ data class StreamEntity(
|
||||
constructor(item: PlayQueueItem) : this(
|
||||
serviceId = item.serviceId, url = item.url, title = item.title,
|
||||
streamType = item.streamType, duration = item.duration, uploader = item.uploader,
|
||||
thumbnailUrl = item.thumbnailUrl
|
||||
uploaderUrl = item.uploaderUrl, thumbnailUrl = item.thumbnailUrl
|
||||
)
|
||||
|
||||
fun toStreamInfoItem(): StreamInfoItem {
|
||||
val item = StreamInfoItem(serviceId, url, title, streamType)
|
||||
item.duration = duration
|
||||
item.uploaderName = uploader
|
||||
item.uploaderUrl = uploaderUrl
|
||||
item.thumbnailUrl = thumbnailUrl
|
||||
|
||||
if (viewCount != null) item.viewCount = viewCount as Long
|
||||
@@ -109,6 +113,7 @@ data class StreamEntity(
|
||||
const val STREAM_TYPE = "stream_type"
|
||||
const val STREAM_DURATION = "duration"
|
||||
const val STREAM_UPLOADER = "uploader"
|
||||
const val STREAM_UPLOADER_URL = "uploader_url"
|
||||
const val STREAM_THUMBNAIL_URL = "thumbnail_url"
|
||||
|
||||
const val STREAM_VIEWS = "view_count"
|
||||
|
||||
@@ -681,7 +681,7 @@ public class DownloadDialog extends DialogFragment
|
||||
new AlertDialog.Builder(context)
|
||||
.setTitle(R.string.general_error)
|
||||
.setMessage(msg)
|
||||
.setNegativeButton(getString(R.string.finish), null)
|
||||
.setNegativeButton(getString(R.string.ok), null)
|
||||
.create()
|
||||
.show();
|
||||
}
|
||||
@@ -864,7 +864,7 @@ public class DownloadDialog extends DialogFragment
|
||||
final AlertDialog.Builder askDialog = new AlertDialog.Builder(context)
|
||||
.setTitle(R.string.download_dialog_title)
|
||||
.setMessage(msgBody)
|
||||
.setNegativeButton(android.R.string.cancel, null);
|
||||
.setNegativeButton(R.string.cancel, null);
|
||||
final StoredFileHelper finalStorage = storage;
|
||||
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ package org.schabi.newpipe.error
|
||||
|
||||
import android.os.Parcelable
|
||||
import androidx.annotation.StringRes
|
||||
import kotlinx.android.parcel.Parcelize
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import org.schabi.newpipe.R
|
||||
import org.schabi.newpipe.extractor.Info
|
||||
import org.schabi.newpipe.extractor.NewPipe
|
||||
|
||||
@@ -6,6 +6,8 @@ import android.util.Log
|
||||
import android.view.View
|
||||
import android.widget.Button
|
||||
import android.widget.TextView
|
||||
import androidx.annotation.Nullable
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.fragment.app.Fragment
|
||||
import com.jakewharton.rxbinding4.view.clicks
|
||||
@@ -37,22 +39,39 @@ class ErrorPanelHelper(
|
||||
onRetry: Runnable
|
||||
) {
|
||||
private val context: Context = rootView.context!!
|
||||
|
||||
private val errorPanelRoot: View = rootView.findViewById(R.id.error_panel)
|
||||
private val errorTextView: TextView = errorPanelRoot.findViewById(R.id.error_message_view)
|
||||
private val errorServiceInfoTextView: TextView = errorPanelRoot.findViewById(R.id.error_message_service_info_view)
|
||||
private val errorServiceExplenationTextView: TextView = errorPanelRoot.findViewById(R.id.error_message_service_explenation_view)
|
||||
private val errorButtonAction: Button = errorPanelRoot.findViewById(R.id.error_button_action)
|
||||
private val errorButtonRetry: Button = errorPanelRoot.findViewById(R.id.error_button_retry)
|
||||
|
||||
// the only element that is visible by default
|
||||
private val errorTextView: TextView =
|
||||
errorPanelRoot.findViewById(R.id.error_message_view)
|
||||
private val errorServiceInfoTextView: TextView =
|
||||
errorPanelRoot.findViewById(R.id.error_message_service_info_view)
|
||||
private val errorServiceExplanationTextView: TextView =
|
||||
errorPanelRoot.findViewById(R.id.error_message_service_explanation_view)
|
||||
private val errorActionButton: Button =
|
||||
errorPanelRoot.findViewById(R.id.error_action_button)
|
||||
private val errorRetryButton: Button =
|
||||
errorPanelRoot.findViewById(R.id.error_retry_button)
|
||||
|
||||
private var errorDisposable: Disposable? = null
|
||||
|
||||
init {
|
||||
errorDisposable = errorButtonRetry.clicks()
|
||||
errorDisposable = errorRetryButton.clicks()
|
||||
.debounce(300, TimeUnit.MILLISECONDS)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe { onRetry.run() }
|
||||
}
|
||||
|
||||
private fun ensureDefaultVisibility() {
|
||||
errorTextView.isVisible = true
|
||||
|
||||
errorServiceInfoTextView.isVisible = false
|
||||
errorServiceExplanationTextView.isVisible = false
|
||||
errorActionButton.isVisible = false
|
||||
errorRetryButton.isVisible = false
|
||||
}
|
||||
|
||||
fun showError(errorInfo: ErrorInfo) {
|
||||
|
||||
if (errorInfo.throwable != null && errorInfo.throwable!!.isInterruptedCaused) {
|
||||
@@ -62,10 +81,14 @@ class ErrorPanelHelper(
|
||||
return
|
||||
}
|
||||
|
||||
errorButtonAction.isVisible = true
|
||||
ensureDefaultVisibility()
|
||||
|
||||
if (errorInfo.throwable is ReCaptchaException) {
|
||||
errorButtonAction.setText(R.string.recaptcha_solve)
|
||||
errorButtonAction.setOnClickListener {
|
||||
errorTextView.setText(R.string.recaptcha_request_toast)
|
||||
|
||||
showAndSetErrorButtonAction(
|
||||
R.string.recaptcha_solve
|
||||
) {
|
||||
// Starting ReCaptcha Challenge Activity
|
||||
val intent = Intent(context, ReCaptchaActivity::class.java)
|
||||
intent.putExtra(
|
||||
@@ -73,78 +96,70 @@ class ErrorPanelHelper(
|
||||
(errorInfo.throwable as ReCaptchaException).url
|
||||
)
|
||||
fragment.startActivityForResult(intent, ReCaptchaActivity.RECAPTCHA_REQUEST)
|
||||
errorButtonAction.setOnClickListener(null)
|
||||
errorActionButton.setOnClickListener(null)
|
||||
}
|
||||
errorTextView.setText(R.string.recaptcha_request_toast)
|
||||
// additional info is only provided by AccountTerminatedException
|
||||
errorServiceInfoTextView.isVisible = false
|
||||
errorServiceExplenationTextView.isVisible = false
|
||||
errorButtonRetry.isVisible = true
|
||||
|
||||
errorRetryButton.isVisible = true
|
||||
} else if (errorInfo.throwable is AccountTerminatedException) {
|
||||
errorButtonRetry.isVisible = false
|
||||
errorButtonAction.isVisible = false
|
||||
errorTextView.setText(R.string.account_terminated)
|
||||
|
||||
if (!isNullOrEmpty((errorInfo.throwable as AccountTerminatedException).message)) {
|
||||
errorServiceInfoTextView.setText(
|
||||
context.resources.getString(
|
||||
R.string.service_provides_reason,
|
||||
NewPipe.getNameOfService(ServiceHelper.getSelectedServiceId(context))
|
||||
)
|
||||
)
|
||||
errorServiceExplenationTextView.setText(
|
||||
(errorInfo.throwable as AccountTerminatedException).message
|
||||
errorServiceInfoTextView.text = context.resources.getString(
|
||||
R.string.service_provides_reason,
|
||||
NewPipe.getNameOfService(ServiceHelper.getSelectedServiceId(context))
|
||||
)
|
||||
errorServiceInfoTextView.isVisible = true
|
||||
errorServiceExplenationTextView.isVisible = true
|
||||
} else {
|
||||
errorServiceInfoTextView.isVisible = false
|
||||
errorServiceExplenationTextView.isVisible = false
|
||||
|
||||
errorServiceExplanationTextView.text =
|
||||
(errorInfo.throwable as AccountTerminatedException).message
|
||||
errorServiceExplanationTextView.isVisible = true
|
||||
}
|
||||
} else {
|
||||
errorButtonAction.setText(R.string.error_snackbar_action)
|
||||
errorButtonAction.setOnClickListener {
|
||||
showAndSetErrorButtonAction(
|
||||
R.string.error_snackbar_action
|
||||
) {
|
||||
ErrorActivity.reportError(context, errorInfo)
|
||||
}
|
||||
|
||||
// additional info is only provided by AccountTerminatedException
|
||||
errorServiceInfoTextView.isVisible = false
|
||||
errorServiceExplenationTextView.isVisible = false
|
||||
errorTextView.setText(getExceptionDescription(errorInfo.throwable))
|
||||
|
||||
// hide retry button by default, then show only if not unavailable/unsupported content
|
||||
errorButtonRetry.isVisible = false
|
||||
errorTextView.setText(
|
||||
when (errorInfo.throwable) {
|
||||
is AgeRestrictedContentException -> R.string.restricted_video_no_stream
|
||||
is GeographicRestrictionException -> R.string.georestricted_content
|
||||
is PaidContentException -> R.string.paid_content
|
||||
is PrivateContentException -> R.string.private_content
|
||||
is SoundCloudGoPlusContentException -> R.string.soundcloud_go_plus_content
|
||||
is YoutubeMusicPremiumContentException -> R.string.youtube_music_premium_content
|
||||
is ContentNotAvailableException -> R.string.content_not_available
|
||||
is ContentNotSupportedException -> R.string.content_not_supported
|
||||
else -> {
|
||||
// show retry button only for content which is not unavailable or unsupported
|
||||
errorButtonRetry.isVisible = true
|
||||
if (errorInfo.throwable != null && errorInfo.throwable!!.isNetworkRelated) {
|
||||
R.string.network_error
|
||||
} else {
|
||||
R.string.error_snackbar_message
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
if (errorInfo.throwable !is ContentNotAvailableException &&
|
||||
errorInfo.throwable !is ContentNotSupportedException
|
||||
) {
|
||||
// show retry button only for content which is not unavailable or unsupported
|
||||
errorRetryButton.isVisible = true
|
||||
}
|
||||
}
|
||||
errorPanelRoot.animate(true, 300)
|
||||
|
||||
setRootVisible()
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows the errorButtonAction, sets a text into it and sets the click listener.
|
||||
*/
|
||||
private fun showAndSetErrorButtonAction(
|
||||
@StringRes resid: Int,
|
||||
@Nullable listener: View.OnClickListener
|
||||
) {
|
||||
errorActionButton.isVisible = true
|
||||
errorActionButton.setText(resid)
|
||||
errorActionButton.setOnClickListener(listener)
|
||||
}
|
||||
|
||||
fun showTextError(errorString: String) {
|
||||
errorButtonAction.isVisible = false
|
||||
errorButtonRetry.isVisible = false
|
||||
ensureDefaultVisibility()
|
||||
|
||||
errorTextView.text = errorString
|
||||
|
||||
setRootVisible()
|
||||
}
|
||||
|
||||
private fun setRootVisible() {
|
||||
errorPanelRoot.animate(true, 300)
|
||||
}
|
||||
|
||||
fun hide() {
|
||||
errorButtonAction.setOnClickListener(null)
|
||||
errorActionButton.setOnClickListener(null)
|
||||
errorPanelRoot.animate(false, 150)
|
||||
}
|
||||
|
||||
@@ -153,13 +168,35 @@ class ErrorPanelHelper(
|
||||
}
|
||||
|
||||
fun dispose() {
|
||||
errorButtonAction.setOnClickListener(null)
|
||||
errorButtonRetry.setOnClickListener(null)
|
||||
errorActionButton.setOnClickListener(null)
|
||||
errorRetryButton.setOnClickListener(null)
|
||||
errorDisposable?.dispose()
|
||||
}
|
||||
|
||||
companion object {
|
||||
val TAG: String = ErrorPanelHelper::class.simpleName!!
|
||||
val DEBUG: Boolean = MainActivity.DEBUG
|
||||
|
||||
@StringRes
|
||||
public fun getExceptionDescription(throwable: Throwable?): Int {
|
||||
return when (throwable) {
|
||||
is AgeRestrictedContentException -> R.string.restricted_video_no_stream
|
||||
is GeographicRestrictionException -> R.string.georestricted_content
|
||||
is PaidContentException -> R.string.paid_content
|
||||
is PrivateContentException -> R.string.private_content
|
||||
is SoundCloudGoPlusContentException -> R.string.soundcloud_go_plus_content
|
||||
is YoutubeMusicPremiumContentException -> R.string.youtube_music_premium_content
|
||||
is ContentNotAvailableException -> R.string.content_not_available
|
||||
is ContentNotSupportedException -> R.string.content_not_supported
|
||||
else -> {
|
||||
// show retry button only for content which is not unavailable or unsupported
|
||||
if (throwable != null && throwable.isNetworkRelated) {
|
||||
R.string.network_error
|
||||
} else {
|
||||
R.string.error_snackbar_message
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package org.schabi.newpipe.error;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.Build;
|
||||
@@ -66,6 +67,7 @@ public class ReCaptchaActivity extends AppCompatActivity {
|
||||
private ActivityRecaptchaBinding recaptchaBinding;
|
||||
private String foundCookies = "";
|
||||
|
||||
@SuppressLint("SetJavaScriptEnabled")
|
||||
@Override
|
||||
protected void onCreate(final Bundle savedInstanceState) {
|
||||
ThemeHelper.setTheme(this);
|
||||
@@ -162,6 +164,9 @@ public class ReCaptchaActivity extends AppCompatActivity {
|
||||
setResult(RESULT_OK);
|
||||
}
|
||||
|
||||
// Navigate to blank page (unloads youtube to prevent background playback)
|
||||
recaptchaBinding.reCaptchaWebView.loadUrl("about:blank");
|
||||
|
||||
final Intent intent = new Intent(this, org.schabi.newpipe.MainActivity.class);
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
|
||||
NavUtils.navigateUpTo(this, intent);
|
||||
|
||||
@@ -151,8 +151,6 @@ public class DescriptionFragment extends BaseFragment {
|
||||
addMetadataItem(inflater, layout, false,
|
||||
R.string.metadata_category, streamInfo.getCategory());
|
||||
|
||||
addTagsMetadataItem(inflater, layout);
|
||||
|
||||
addMetadataItem(inflater, layout, false,
|
||||
R.string.metadata_licence, streamInfo.getLicence());
|
||||
|
||||
@@ -174,6 +172,8 @@ public class DescriptionFragment extends BaseFragment {
|
||||
R.string.metadata_host, streamInfo.getHost());
|
||||
addMetadataItem(inflater, layout, true,
|
||||
R.string.metadata_thumbnail_url, streamInfo.getThumbnailUrl());
|
||||
|
||||
addTagsMetadataItem(inflater, layout);
|
||||
}
|
||||
|
||||
private void addMetadataItem(final LayoutInflater inflater,
|
||||
|
||||
@@ -48,9 +48,7 @@ import com.google.android.exoplayer2.PlaybackParameters;
|
||||
import com.google.android.material.appbar.AppBarLayout;
|
||||
import com.google.android.material.bottomsheet.BottomSheetBehavior;
|
||||
import com.google.android.material.tabs.TabLayout;
|
||||
import com.nostra13.universalimageloader.core.assist.FailReason;
|
||||
import com.nostra13.universalimageloader.core.listener.ImageLoadingListener;
|
||||
import com.nostra13.universalimageloader.core.listener.SimpleImageLoadingListener;
|
||||
import com.squareup.picasso.Callback;
|
||||
|
||||
import org.schabi.newpipe.App;
|
||||
import org.schabi.newpipe.R;
|
||||
@@ -79,6 +77,7 @@ import org.schabi.newpipe.local.dialog.PlaylistAppendDialog;
|
||||
import org.schabi.newpipe.local.dialog.PlaylistCreationDialog;
|
||||
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
||||
import org.schabi.newpipe.player.MainPlayer;
|
||||
import org.schabi.newpipe.player.MainPlayer.PlayerType;
|
||||
import org.schabi.newpipe.player.Player;
|
||||
import org.schabi.newpipe.player.event.OnKeyDownListener;
|
||||
import org.schabi.newpipe.player.event.PlayerServiceExtendedEventListener;
|
||||
@@ -90,14 +89,14 @@ import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
|
||||
import org.schabi.newpipe.util.Constants;
|
||||
import org.schabi.newpipe.util.DeviceUtils;
|
||||
import org.schabi.newpipe.util.ExtractorHelper;
|
||||
import org.schabi.newpipe.util.ImageDisplayConstants;
|
||||
import org.schabi.newpipe.util.external_communication.KoreUtils;
|
||||
import org.schabi.newpipe.util.ListHelper;
|
||||
import org.schabi.newpipe.util.Localization;
|
||||
import org.schabi.newpipe.util.NavigationHelper;
|
||||
import org.schabi.newpipe.util.PermissionHelper;
|
||||
import org.schabi.newpipe.util.external_communication.ShareUtils;
|
||||
import org.schabi.newpipe.util.PicassoHelper;
|
||||
import org.schabi.newpipe.util.ThemeHelper;
|
||||
import org.schabi.newpipe.util.external_communication.KoreUtils;
|
||||
import org.schabi.newpipe.util.external_communication.ShareUtils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
@@ -151,6 +150,8 @@ public final class VideoDetailFragment
|
||||
private static final String DESCRIPTION_TAB_TAG = "DESCRIPTION TAB";
|
||||
private static final String EMPTY_TAB_TAG = "EMPTY TAB";
|
||||
|
||||
private static final String PICASSO_VIDEO_DETAILS_TAG = "PICASSO_VIDEO_DETAILS_TAG";
|
||||
|
||||
// tabs
|
||||
private boolean showComments;
|
||||
private boolean showRelatedItems;
|
||||
@@ -201,6 +202,7 @@ public final class VideoDetailFragment
|
||||
@Nullable
|
||||
private MainPlayer playerService;
|
||||
private Player player;
|
||||
private final PlayerHolder playerHolder = PlayerHolder.getInstance();
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Service management
|
||||
@@ -219,7 +221,7 @@ public final class VideoDetailFragment
|
||||
return;
|
||||
}
|
||||
|
||||
if (isLandscape()) {
|
||||
if (DeviceUtils.isLandscape(requireContext())) {
|
||||
// If the video is playing but orientation changed
|
||||
// let's make the video in fullscreen again
|
||||
checkLandscape();
|
||||
@@ -240,7 +242,7 @@ public final class VideoDetailFragment
|
||||
&& isAutoplayEnabled()
|
||||
&& player.getParentActivity() == null)) {
|
||||
autoPlayEnabled = true; // forcefully start playing
|
||||
openVideoPlayer();
|
||||
openVideoPlayerAutoFullscreen();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -304,7 +306,8 @@ public final class VideoDetailFragment
|
||||
@Override
|
||||
public View onCreateView(@NonNull final LayoutInflater inflater, final ViewGroup container,
|
||||
final Bundle savedInstanceState) {
|
||||
return inflater.inflate(R.layout.fragment_video_detail, container, false);
|
||||
binding = FragmentVideoDetailBinding.inflate(inflater, container, false);
|
||||
return binding.getRoot();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -355,14 +358,13 @@ public final class VideoDetailFragment
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
binding = null;
|
||||
|
||||
// Stop the service when user leaves the app with double back press
|
||||
// if video player is selected. Otherwise unbind
|
||||
if (activity.isFinishing() && player != null && player.videoPlayerSelected()) {
|
||||
PlayerHolder.stopService(App.getApp());
|
||||
if (activity.isFinishing() && isPlayerAvailable() && player.videoPlayerSelected()) {
|
||||
playerHolder.stopService();
|
||||
} else {
|
||||
PlayerHolder.removeListener();
|
||||
playerHolder.setListener(null);
|
||||
}
|
||||
|
||||
PreferenceManager.getDefaultSharedPreferences(activity)
|
||||
@@ -388,6 +390,12 @@ public final class VideoDetailFragment
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyView() {
|
||||
super.onDestroyView();
|
||||
binding = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityResult(final int requestCode, final int resultCode, final Intent data) {
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
@@ -416,7 +424,7 @@ public final class VideoDetailFragment
|
||||
showRelatedItems = sharedPreferences.getBoolean(key, true);
|
||||
tabSettingsChanged = true;
|
||||
} else if (key.equals(getString(R.string.show_description_key))) {
|
||||
showComments = sharedPreferences.getBoolean(key, true);
|
||||
showDescription = sharedPreferences.getBoolean(key, true);
|
||||
tabSettingsChanged = true;
|
||||
}
|
||||
}
|
||||
@@ -492,7 +500,7 @@ public final class VideoDetailFragment
|
||||
break;
|
||||
case R.id.detail_thumbnail_root_layout:
|
||||
autoPlayEnabled = true; // forcefully start playing
|
||||
openVideoPlayer();
|
||||
openVideoPlayerAutoFullscreen();
|
||||
break;
|
||||
case R.id.detail_title_root_layout:
|
||||
toggleTitleAndSecondaryControls();
|
||||
@@ -509,10 +517,10 @@ public final class VideoDetailFragment
|
||||
showSystemUi();
|
||||
} else {
|
||||
autoPlayEnabled = true; // forcefully start playing
|
||||
openVideoPlayer();
|
||||
openVideoPlayer(false);
|
||||
}
|
||||
|
||||
setOverlayPlayPauseImage(player != null && player.isPlaying());
|
||||
setOverlayPlayPauseImage(isPlayerAvailable() && player.isPlaying());
|
||||
break;
|
||||
case R.id.overlay_close_button:
|
||||
bottomSheetBehavior.setState(BottomSheetBehavior.STATE_HIDDEN);
|
||||
@@ -586,10 +594,9 @@ public final class VideoDetailFragment
|
||||
// Init
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
@Override // called from onViewCreated in {@link BaseFragment#onViewCreated}
|
||||
protected void initViews(final View rootView, final Bundle savedInstanceState) {
|
||||
super.initViews(rootView, savedInstanceState);
|
||||
binding = FragmentVideoDetailBinding.bind(rootView);
|
||||
|
||||
pageAdapter = new TabAdapter(getChildFragmentManager());
|
||||
binding.viewPager.setAdapter(pageAdapter);
|
||||
@@ -655,10 +662,10 @@ public final class VideoDetailFragment
|
||||
});
|
||||
|
||||
setupBottomPlayer();
|
||||
if (!PlayerHolder.bound) {
|
||||
if (!playerHolder.bound) {
|
||||
setHeightThumbnail();
|
||||
} else {
|
||||
PlayerHolder.startService(App.getApp(), false, this);
|
||||
playerHolder.startService(false, this);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -680,33 +687,24 @@ public final class VideoDetailFragment
|
||||
}
|
||||
|
||||
private void initThumbnailViews(@NonNull final StreamInfo info) {
|
||||
binding.detailThumbnailImageView.setImageResource(R.drawable.dummy_thumbnail_dark);
|
||||
PicassoHelper.loadThumbnail(info.getThumbnailUrl()).tag(PICASSO_VIDEO_DETAILS_TAG)
|
||||
.into(binding.detailThumbnailImageView, new Callback() {
|
||||
@Override
|
||||
public void onSuccess() {
|
||||
// nothing to do, the image was loaded correctly into the thumbnail
|
||||
}
|
||||
|
||||
if (!isEmpty(info.getThumbnailUrl())) {
|
||||
final ImageLoadingListener onFailListener = new SimpleImageLoadingListener() {
|
||||
@Override
|
||||
public void onLoadingFailed(final String imageUri, final View view,
|
||||
final FailReason failReason) {
|
||||
showSnackBarError(new ErrorInfo(failReason.getCause(), UserAction.LOAD_IMAGE,
|
||||
imageUri, info));
|
||||
}
|
||||
};
|
||||
@Override
|
||||
public void onError(final Exception e) {
|
||||
showSnackBarError(new ErrorInfo(e, UserAction.LOAD_IMAGE,
|
||||
info.getThumbnailUrl(), info));
|
||||
}
|
||||
});
|
||||
|
||||
IMAGE_LOADER.displayImage(info.getThumbnailUrl(), binding.detailThumbnailImageView,
|
||||
ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS, onFailListener);
|
||||
}
|
||||
|
||||
if (!isEmpty(info.getSubChannelAvatarUrl())) {
|
||||
IMAGE_LOADER.displayImage(info.getSubChannelAvatarUrl(),
|
||||
binding.detailSubChannelThumbnailView,
|
||||
ImageDisplayConstants.DISPLAY_AVATAR_OPTIONS);
|
||||
}
|
||||
|
||||
if (!isEmpty(info.getUploaderAvatarUrl())) {
|
||||
IMAGE_LOADER.displayImage(info.getUploaderAvatarUrl(),
|
||||
binding.detailUploaderThumbnailView,
|
||||
ImageDisplayConstants.DISPLAY_AVATAR_OPTIONS);
|
||||
}
|
||||
PicassoHelper.loadAvatar(info.getSubChannelAvatarUrl()).tag(PICASSO_VIDEO_DETAILS_TAG)
|
||||
.into(binding.detailSubChannelThumbnailView);
|
||||
PicassoHelper.loadAvatar(info.getUploaderAvatarUrl()).tag(PICASSO_VIDEO_DETAILS_TAG)
|
||||
.into(binding.detailUploaderThumbnailView);
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
@@ -721,7 +719,7 @@ public final class VideoDetailFragment
|
||||
|
||||
@Override
|
||||
public boolean onKeyDown(final int keyCode) {
|
||||
return player != null && player.onKeyDown(keyCode);
|
||||
return isPlayerAvailable() && player.onKeyDown(keyCode);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -731,7 +729,7 @@ public final class VideoDetailFragment
|
||||
}
|
||||
|
||||
// If we are in fullscreen mode just exit from it via first back press
|
||||
if (player != null && player.isFullscreen()) {
|
||||
if (isPlayerAvailable() && player.isFullscreen()) {
|
||||
if (!DeviceUtils.isTablet(activity)) {
|
||||
player.pause();
|
||||
}
|
||||
@@ -741,31 +739,30 @@ public final class VideoDetailFragment
|
||||
}
|
||||
|
||||
// If we have something in history of played items we replay it here
|
||||
if (player != null
|
||||
if (isPlayerAvailable()
|
||||
&& player.getPlayQueue() != null
|
||||
&& player.videoPlayerSelected()
|
||||
&& player.getPlayQueue().previous()) {
|
||||
return true;
|
||||
return true; // no code here, as previous() was used in the if
|
||||
}
|
||||
|
||||
// That means that we are on the start of the stack,
|
||||
// return false to let the MainActivity handle the onBack
|
||||
if (stack.size() <= 1) {
|
||||
restoreDefaultOrientation();
|
||||
|
||||
return false;
|
||||
return false; // let MainActivity handle the onBack (e.g. to minimize the mini player)
|
||||
}
|
||||
|
||||
// Remove top
|
||||
stack.pop();
|
||||
// Get stack item from the new top
|
||||
assert stack.peek() != null;
|
||||
setupFromHistoryItem(stack.peek());
|
||||
setupFromHistoryItem(Objects.requireNonNull(stack.peek()));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void setupFromHistoryItem(final StackItem item) {
|
||||
setAutoPlay(false);
|
||||
hideMainPlayer();
|
||||
hideMainPlayerOnLoadingNewStream();
|
||||
|
||||
setInitialData(item.getServiceId(), item.getUrl(),
|
||||
item.getTitle() == null ? "" : item.getTitle(), item.getPlayQueue());
|
||||
@@ -778,7 +775,7 @@ public final class VideoDetailFragment
|
||||
|
||||
final PlayQueueItem playQueueItem = item.getPlayQueue().getItem();
|
||||
// Update title, url, uploader from the last item in the stack (it's current now)
|
||||
final boolean isPlayerStopped = player == null || player.isStopped();
|
||||
final boolean isPlayerStopped = !isPlayerAvailable() || player.isStopped();
|
||||
if (playQueueItem != null && isPlayerStopped) {
|
||||
updateOverlayData(playQueueItem.getTitle(),
|
||||
playQueueItem.getUploader(), playQueueItem.getThumbnailUrl());
|
||||
@@ -806,8 +803,8 @@ public final class VideoDetailFragment
|
||||
@Nullable final String newUrl,
|
||||
@NonNull final String newTitle,
|
||||
@Nullable final PlayQueue newQueue) {
|
||||
if (player != null && newQueue != null && playQueue != null
|
||||
&& !Objects.equals(newQueue.getItem(), playQueue.getItem())) {
|
||||
if (isPlayerAvailable() && newQueue != null && playQueue != null
|
||||
&& playQueue.getItem() != null && !playQueue.getItem().getUrl().equals(newUrl)) {
|
||||
// Preloading can be disabled since playback is surely being replaced.
|
||||
player.disablePreloadingOfCurrentTrack();
|
||||
}
|
||||
@@ -885,7 +882,7 @@ public final class VideoDetailFragment
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(result -> {
|
||||
isLoading.set(false);
|
||||
hideMainPlayer();
|
||||
hideMainPlayerOnLoadingNewStream();
|
||||
if (result.getAgeLimit() != NO_AGE_LIMIT && !prefs.getBoolean(
|
||||
getString(R.string.show_age_restricted_content), false)) {
|
||||
hideAgeRestrictedContent();
|
||||
@@ -900,8 +897,9 @@ public final class VideoDetailFragment
|
||||
stack.push(new StackItem(serviceId, url, title, playQueue));
|
||||
}
|
||||
}
|
||||
|
||||
if (isAutoplayEnabled()) {
|
||||
openVideoPlayer();
|
||||
openVideoPlayerAutoFullscreen();
|
||||
}
|
||||
}
|
||||
}, throwable -> showError(new ErrorInfo(throwable, UserAction.REQUESTED_STREAM,
|
||||
@@ -982,7 +980,7 @@ public final class VideoDetailFragment
|
||||
.replace(R.id.relatedItemsLayout, RelatedItemsFragment.getInstance(info))
|
||||
.commitAllowingStateLoss();
|
||||
binding.relatedItemsLayout.setVisibility(
|
||||
player != null && player.isFullscreen() ? View.GONE : View.VISIBLE);
|
||||
isPlayerAvailable() && player.isFullscreen() ? View.GONE : View.VISIBLE);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1059,6 +1057,14 @@ public final class VideoDetailFragment
|
||||
// Play Utils
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
private void toggleFullscreenIfInFullscreenMode() {
|
||||
// If a user watched video inside fullscreen mode and than chose another player
|
||||
// return to non-fullscreen mode
|
||||
if (isPlayerAvailable() && player.isFullscreen()) {
|
||||
player.toggleFullscreen();
|
||||
}
|
||||
}
|
||||
|
||||
private void openBackgroundPlayer(final boolean append) {
|
||||
final AudioStream audioStream = currentInfo.getAudioStreams()
|
||||
.get(ListHelper.getDefaultAudioFormat(activity, currentInfo.getAudioStreams()));
|
||||
@@ -1067,11 +1073,7 @@ public final class VideoDetailFragment
|
||||
.getDefaultSharedPreferences(activity)
|
||||
.getBoolean(activity.getString(R.string.use_external_audio_player_key), false);
|
||||
|
||||
// If a user watched video inside fullscreen mode and than chose another player
|
||||
// return to non-fullscreen mode
|
||||
if (player != null && player.isFullscreen()) {
|
||||
player.toggleFullscreen();
|
||||
}
|
||||
toggleFullscreenIfInFullscreenMode();
|
||||
|
||||
if (!useExternalAudioPlayer) {
|
||||
openNormalBackgroundPlayer(append);
|
||||
@@ -1087,26 +1089,44 @@ public final class VideoDetailFragment
|
||||
}
|
||||
|
||||
// See UI changes while remote playQueue changes
|
||||
if (player == null) {
|
||||
PlayerHolder.startService(App.getApp(), false, this);
|
||||
if (!isPlayerAvailable()) {
|
||||
playerHolder.startService(false, this);
|
||||
}
|
||||
|
||||
// If a user watched video inside fullscreen mode and than chose another player
|
||||
// return to non-fullscreen mode
|
||||
if (player != null && player.isFullscreen()) {
|
||||
player.toggleFullscreen();
|
||||
}
|
||||
toggleFullscreenIfInFullscreenMode();
|
||||
|
||||
final PlayQueue queue = setupPlayQueueForIntent(append);
|
||||
if (append) {
|
||||
NavigationHelper.enqueueOnPopupPlayer(activity, queue, false);
|
||||
if (append) { //resumePlayback: false
|
||||
NavigationHelper.enqueueOnPlayer(activity, queue, PlayerType.POPUP);
|
||||
} else {
|
||||
replaceQueueIfUserConfirms(() -> NavigationHelper
|
||||
.playOnPopupPlayer(activity, queue, true));
|
||||
}
|
||||
}
|
||||
|
||||
public void openVideoPlayer() {
|
||||
/**
|
||||
* Opens the video player, in fullscreen if needed. In order to open fullscreen, the activity
|
||||
* is toggled to landscape orientation (which will then cause fullscreen mode).
|
||||
*
|
||||
* @param directlyFullscreenIfApplicable whether to open fullscreen if we are not already
|
||||
* in landscape and screen orientation is locked
|
||||
*/
|
||||
public void openVideoPlayer(final boolean directlyFullscreenIfApplicable) {
|
||||
if (directlyFullscreenIfApplicable
|
||||
&& !DeviceUtils.isLandscape(requireContext())
|
||||
&& PlayerHelper.globalScreenOrientationLocked(requireContext())) {
|
||||
// Make sure the bottom sheet turns out expanded. When this code kicks in the bottom
|
||||
// sheet could not have fully expanded yet, and thus be in the STATE_SETTLING state.
|
||||
// When the activity is rotated, and its state is saved and then restored, the bottom
|
||||
// sheet would forget what it was doing, since even if STATE_SETTLING is restored, it
|
||||
// doesn't tell which state it was settling to, and thus the bottom sheet settles to
|
||||
// STATE_COLLAPSED. This can be solved by manually setting the state that will be
|
||||
// restored (i.e. bottomSheetState) to STATE_EXPANDED.
|
||||
bottomSheetState = BottomSheetBehavior.STATE_EXPANDED;
|
||||
// toggle landscape in order to open directly in fullscreen
|
||||
onScreenRotationButtonClicked();
|
||||
}
|
||||
|
||||
if (PreferenceManager.getDefaultSharedPreferences(activity)
|
||||
.getBoolean(this.getString(R.string.use_external_video_player_key), false)) {
|
||||
showExternalPlaybackDialog();
|
||||
@@ -1115,15 +1135,27 @@ public final class VideoDetailFragment
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If the option to start directly fullscreen is enabled, calls
|
||||
* {@link #openVideoPlayer(boolean)} with {@code directlyFullscreenIfApplicable = true}, so that
|
||||
* if the user is not already in landscape and he has screen orientation locked the activity
|
||||
* rotates and fullscreen starts. Otherwise, if the option to start directly fullscreen is
|
||||
* disabled, calls {@link #openVideoPlayer(boolean)} with {@code directlyFullscreenIfApplicable
|
||||
* = false}, hence preventing it from going directly fullscreen.
|
||||
*/
|
||||
public void openVideoPlayerAutoFullscreen() {
|
||||
openVideoPlayer(PlayerHelper.isStartMainPlayerFullscreenEnabled(requireContext()));
|
||||
}
|
||||
|
||||
private void openNormalBackgroundPlayer(final boolean append) {
|
||||
// See UI changes while remote playQueue changes
|
||||
if (player == null) {
|
||||
PlayerHolder.startService(App.getApp(), false, this);
|
||||
if (!isPlayerAvailable()) {
|
||||
playerHolder.startService(false, this);
|
||||
}
|
||||
|
||||
final PlayQueue queue = setupPlayQueueForIntent(append);
|
||||
if (append) {
|
||||
NavigationHelper.enqueueOnBackgroundPlayer(activity, queue, false);
|
||||
NavigationHelper.enqueueOnPlayer(activity, queue, PlayerType.AUDIO);
|
||||
} else {
|
||||
replaceQueueIfUserConfirms(() -> NavigationHelper
|
||||
.playOnBackgroundPlayer(activity, queue, true));
|
||||
@@ -1131,8 +1163,8 @@ public final class VideoDetailFragment
|
||||
}
|
||||
|
||||
private void openMainPlayer() {
|
||||
if (playerService == null) {
|
||||
PlayerHolder.startService(App.getApp(), autoPlayEnabled, this);
|
||||
if (!isPlayerServiceAvailable()) {
|
||||
playerHolder.startService(autoPlayEnabled, this);
|
||||
return;
|
||||
}
|
||||
if (currentInfo == null) {
|
||||
@@ -1148,21 +1180,32 @@ public final class VideoDetailFragment
|
||||
}
|
||||
addVideoPlayerView();
|
||||
|
||||
final Intent playerIntent = NavigationHelper
|
||||
.getPlayerIntent(requireContext(), MainPlayer.class, queue, true, autoPlayEnabled);
|
||||
activity.startService(playerIntent);
|
||||
final Intent playerIntent = NavigationHelper.getPlayerIntent(requireContext(),
|
||||
MainPlayer.class, queue, true, autoPlayEnabled);
|
||||
ContextCompat.startForegroundService(activity, playerIntent);
|
||||
}
|
||||
|
||||
private void hideMainPlayer() {
|
||||
if (playerService == null
|
||||
/**
|
||||
* When the video detail fragment is already showing details for a video and the user opens a
|
||||
* new one, the video detail fragment changes all of its old data to the new stream, so if there
|
||||
* is a video player currently open it should be hidden. This method does exactly that. If
|
||||
* autoplay is enabled, the underlying player is not stopped completely, since it is going to
|
||||
* be reused in a few milliseconds and the flickering would be annoying.
|
||||
*/
|
||||
private void hideMainPlayerOnLoadingNewStream() {
|
||||
if (!isPlayerServiceAvailable()
|
||||
|| playerService.getView() == null
|
||||
|| !player.videoPlayerSelected()) {
|
||||
return;
|
||||
}
|
||||
|
||||
removeVideoPlayerView();
|
||||
playerService.stop(isAutoplayEnabled());
|
||||
playerService.getView().setVisibility(View.GONE);
|
||||
if (isAutoplayEnabled()) {
|
||||
playerService.stopForImmediateReusing();
|
||||
playerService.getView().setVisibility(View.GONE);
|
||||
} else {
|
||||
playerHolder.stopService();
|
||||
}
|
||||
}
|
||||
|
||||
private PlayQueue setupPlayQueueForIntent(final boolean append) {
|
||||
@@ -1211,13 +1254,13 @@ public final class VideoDetailFragment
|
||||
private boolean isAutoplayEnabled() {
|
||||
return autoPlayEnabled
|
||||
&& !isExternalPlayerEnabled()
|
||||
&& (player == null || player.videoPlayerSelected())
|
||||
&& (!isPlayerAvailable() || player.videoPlayerSelected())
|
||||
&& bottomSheetState != BottomSheetBehavior.STATE_HIDDEN
|
||||
&& PlayerHelper.isAutoplayAllowedByUser(requireContext());
|
||||
}
|
||||
|
||||
private void addVideoPlayerView() {
|
||||
if (player == null || getView() == null) {
|
||||
if (!isPlayerAvailable() || getView() == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1255,7 +1298,7 @@ public final class VideoDetailFragment
|
||||
final DisplayMetrics metrics = getResources().getDisplayMetrics();
|
||||
|
||||
if (getView() != null) {
|
||||
final int height = (isInMultiWindow()
|
||||
final int height = (DeviceUtils.isInMultiWindow(activity)
|
||||
? requireView()
|
||||
: activity.getWindow().getDecorView()).getHeight();
|
||||
setHeightThumbnail(height, metrics);
|
||||
@@ -1277,8 +1320,8 @@ public final class VideoDetailFragment
|
||||
final boolean isPortrait = metrics.heightPixels > metrics.widthPixels;
|
||||
requireView().getViewTreeObserver().removeOnPreDrawListener(preDrawListener);
|
||||
|
||||
if (player != null && player.isFullscreen()) {
|
||||
final int height = (isInMultiWindow()
|
||||
if (isPlayerAvailable() && player.isFullscreen()) {
|
||||
final int height = (DeviceUtils.isInMultiWindow(activity)
|
||||
? requireView()
|
||||
: activity.getWindow().getDecorView()).getHeight();
|
||||
// Height is zero when the view is not yet displayed like after orientation change
|
||||
@@ -1300,7 +1343,7 @@ public final class VideoDetailFragment
|
||||
new FrameLayout.LayoutParams(
|
||||
RelativeLayout.LayoutParams.MATCH_PARENT, newHeight));
|
||||
binding.detailThumbnailImageView.setMinimumHeight(newHeight);
|
||||
if (player != null) {
|
||||
if (isPlayerAvailable()) {
|
||||
final int maxHeight = (int) (metrics.heightPixels * MAX_PLAYER_HEIGHT);
|
||||
player.getSurfaceView()
|
||||
.setHeights(newHeight, player.isFullscreen() ? newHeight : maxHeight);
|
||||
@@ -1368,9 +1411,9 @@ public final class VideoDetailFragment
|
||||
bottomSheetBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
|
||||
}
|
||||
// Rebound to the service if it was closed via notification or mini player
|
||||
if (!PlayerHolder.bound) {
|
||||
PlayerHolder.startService(
|
||||
App.getApp(), false, VideoDetailFragment.this);
|
||||
if (!playerHolder.bound) {
|
||||
playerHolder.startService(
|
||||
false, VideoDetailFragment.this);
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -1389,18 +1432,15 @@ public final class VideoDetailFragment
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
private void restoreDefaultOrientation() {
|
||||
if (player == null || !player.videoPlayerSelected() || activity == null) {
|
||||
return;
|
||||
if (isPlayerAvailable() && player.videoPlayerSelected()) {
|
||||
toggleFullscreenIfInFullscreenMode();
|
||||
}
|
||||
|
||||
if (player != null && player.isFullscreen()) {
|
||||
player.toggleFullscreen();
|
||||
}
|
||||
// This will show systemUI and pause the player.
|
||||
// User can tap on Play button and video will be in fullscreen mode again
|
||||
// Note for tablet: trying to avoid orientation changes since it's not easy
|
||||
// to physically rotate the tablet every time
|
||||
if (!DeviceUtils.isTablet(activity)) {
|
||||
if (activity != null && !DeviceUtils.isTablet(activity)) {
|
||||
activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED);
|
||||
}
|
||||
}
|
||||
@@ -1435,14 +1475,13 @@ public final class VideoDetailFragment
|
||||
if (binding.relatedItemsLayout != null) {
|
||||
if (showRelatedItems) {
|
||||
binding.relatedItemsLayout.setVisibility(
|
||||
player != null && player.isFullscreen() ? View.GONE : View.INVISIBLE);
|
||||
isPlayerAvailable() && player.isFullscreen() ? View.GONE : View.INVISIBLE);
|
||||
} else {
|
||||
binding.relatedItemsLayout.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
IMAGE_LOADER.cancelDisplayTask(binding.detailThumbnailImageView);
|
||||
IMAGE_LOADER.cancelDisplayTask(binding.detailSubChannelThumbnailView);
|
||||
PicassoHelper.cancelTag(PICASSO_VIDEO_DETAILS_TAG);
|
||||
binding.detailThumbnailImageView.setImageBitmap(null);
|
||||
binding.detailSubChannelThumbnailView.setImageBitmap(null);
|
||||
}
|
||||
@@ -1549,7 +1588,7 @@ public final class VideoDetailFragment
|
||||
showMetaInfoInTextView(info.getMetaInfo(), binding.detailMetaInfoTextView,
|
||||
binding.detailMetaInfoSeparator, disposables);
|
||||
|
||||
if (player == null || player.isStopped()) {
|
||||
if (!isPlayerAvailable() || player.isStopped()) {
|
||||
updateOverlayData(info.getName(), info.getUploaderName(), info.getThumbnailUrl());
|
||||
}
|
||||
|
||||
@@ -1812,10 +1851,8 @@ public final class VideoDetailFragment
|
||||
if (error.type == ExoPlaybackException.TYPE_SOURCE
|
||||
|| error.type == ExoPlaybackException.TYPE_UNEXPECTED) {
|
||||
// Properly exit from fullscreen
|
||||
if (playerService != null && player.isFullscreen()) {
|
||||
player.toggleFullscreen();
|
||||
}
|
||||
hideMainPlayer();
|
||||
toggleFullscreenIfInFullscreenMode();
|
||||
hideMainPlayerOnLoadingNewStream();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1832,7 +1869,9 @@ public final class VideoDetailFragment
|
||||
@Override
|
||||
public void onFullscreenStateChanged(final boolean fullscreen) {
|
||||
setupBrightness();
|
||||
if (playerService.getView() == null || player.getParentActivity() == null) {
|
||||
if (!isPlayerAndPlayerServiceAvailable()
|
||||
|| playerService.getView() == null
|
||||
|| player.getParentActivity() == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1869,13 +1908,14 @@ public final class VideoDetailFragment
|
||||
// from landscape to portrait every time.
|
||||
// Just turn on fullscreen mode in landscape orientation
|
||||
// or portrait & unlocked global orientation
|
||||
final boolean isLandscape = DeviceUtils.isLandscape(requireContext());
|
||||
if (DeviceUtils.isTablet(activity)
|
||||
&& (!globalScreenOrientationLocked(activity) || isLandscape())) {
|
||||
&& (!globalScreenOrientationLocked(activity) || isLandscape)) {
|
||||
player.toggleFullscreen();
|
||||
return;
|
||||
}
|
||||
|
||||
final int newOrientation = isLandscape()
|
||||
final int newOrientation = isLandscape
|
||||
? ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
|
||||
: ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE;
|
||||
|
||||
@@ -1947,15 +1987,17 @@ public final class VideoDetailFragment
|
||||
| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
|
||||
| View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
|
||||
| View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY;
|
||||
|
||||
// In multiWindow mode status bar is not transparent for devices with cutout
|
||||
// if I include this flag. So without it is better in this case
|
||||
if (!isInMultiWindow()) {
|
||||
final boolean isInMultiWindow = DeviceUtils.isInMultiWindow(activity);
|
||||
if (!isInMultiWindow) {
|
||||
visibility |= View.SYSTEM_UI_FLAG_FULLSCREEN;
|
||||
}
|
||||
activity.getWindow().getDecorView().setSystemUiVisibility(visibility);
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP
|
||||
&& (isInMultiWindow() || (player != null && player.isFullscreen()))) {
|
||||
&& (isInMultiWindow || (isPlayerAvailable() && player.isFullscreen()))) {
|
||||
activity.getWindow().setStatusBarColor(Color.TRANSPARENT);
|
||||
activity.getWindow().setNavigationBarColor(Color.TRANSPARENT);
|
||||
}
|
||||
@@ -1964,7 +2006,7 @@ public final class VideoDetailFragment
|
||||
|
||||
// Listener implementation
|
||||
public void hideSystemUiIfNeeded() {
|
||||
if (player != null
|
||||
if (isPlayerAvailable()
|
||||
&& player.isFullscreen()
|
||||
&& bottomSheetBehavior.getState() == BottomSheetBehavior.STATE_EXPANDED) {
|
||||
hideSystemUi();
|
||||
@@ -1972,7 +2014,7 @@ public final class VideoDetailFragment
|
||||
}
|
||||
|
||||
private boolean playerIsNotStopped() {
|
||||
return player != null && !player.isStopped();
|
||||
return isPlayerAvailable() && !player.isStopped();
|
||||
}
|
||||
|
||||
private void restoreDefaultBrightness() {
|
||||
@@ -1993,7 +2035,7 @@ public final class VideoDetailFragment
|
||||
}
|
||||
|
||||
final WindowManager.LayoutParams lp = activity.getWindow().getAttributes();
|
||||
if (player == null
|
||||
if (!isPlayerAvailable()
|
||||
|| !player.videoPlayerSelected()
|
||||
|| !player.isFullscreen()
|
||||
|| bottomSheetState != BottomSheetBehavior.STATE_EXPANDED) {
|
||||
@@ -2027,15 +2069,6 @@ public final class VideoDetailFragment
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isLandscape() {
|
||||
return getResources().getDisplayMetrics().heightPixels < getResources()
|
||||
.getDisplayMetrics().widthPixels;
|
||||
}
|
||||
|
||||
private boolean isInMultiWindow() {
|
||||
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && activity.isInMultiWindowMode();
|
||||
}
|
||||
|
||||
/*
|
||||
* Means that the player fragment was swiped away via BottomSheetLayout
|
||||
* and is empty but ready for any new actions. See cleanUp()
|
||||
@@ -2059,7 +2092,7 @@ public final class VideoDetailFragment
|
||||
}
|
||||
|
||||
private void replaceQueueIfUserConfirms(final Runnable onAllow) {
|
||||
@Nullable final PlayQueue activeQueue = player == null ? null : player.getPlayQueue();
|
||||
@Nullable final PlayQueue activeQueue = isPlayerAvailable() ? player.getPlayQueue() : null;
|
||||
|
||||
// Player will have STATE_IDLE when a user pressed back button
|
||||
if (isClearingQueueConfirmationRequired(activity)
|
||||
@@ -2075,8 +2108,8 @@ public final class VideoDetailFragment
|
||||
private void showClearingQueueConfirmation(final Runnable onAllow) {
|
||||
new AlertDialog.Builder(activity)
|
||||
.setTitle(R.string.clear_queue_confirmation_description)
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.setPositiveButton(android.R.string.yes, (dialog, which) -> {
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.setPositiveButton(R.string.ok, (dialog, which) -> {
|
||||
onAllow.run();
|
||||
dialog.dismiss();
|
||||
}).show();
|
||||
@@ -2091,7 +2124,7 @@ public final class VideoDetailFragment
|
||||
resolutions[i] = sortedVideoStreams.get(i).getResolution();
|
||||
}
|
||||
final AlertDialog.Builder builder = new AlertDialog.Builder(activity)
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.setNeutralButton(R.string.open_in_browser, (dialog, i) ->
|
||||
ShareUtils.openUrlInBrowser(requireActivity(), url)
|
||||
);
|
||||
@@ -2115,7 +2148,7 @@ public final class VideoDetailFragment
|
||||
if (currentWorker != null) {
|
||||
currentWorker.dispose();
|
||||
}
|
||||
PlayerHolder.stopService(App.getApp());
|
||||
playerHolder.stopService();
|
||||
setInitialData(0, null, "", null);
|
||||
currentInfo = null;
|
||||
updateOverlayData(null, null, null);
|
||||
@@ -2218,8 +2251,8 @@ public final class VideoDetailFragment
|
||||
setOverlayElementsClickable(false);
|
||||
hideSystemUiIfNeeded();
|
||||
// Conditions when the player should be expanded to fullscreen
|
||||
if (isLandscape()
|
||||
&& player != null
|
||||
if (DeviceUtils.isLandscape(requireContext())
|
||||
&& isPlayerAvailable()
|
||||
&& player.isPlaying()
|
||||
&& !player.isFullscreen()
|
||||
&& !DeviceUtils.isTablet(activity)
|
||||
@@ -2236,17 +2269,17 @@ public final class VideoDetailFragment
|
||||
|
||||
// Re-enable clicks
|
||||
setOverlayElementsClickable(true);
|
||||
if (player != null) {
|
||||
if (isPlayerAvailable()) {
|
||||
player.closeItemsList();
|
||||
}
|
||||
setOverlayLook(binding.appBarLayout, behavior, 0);
|
||||
break;
|
||||
case BottomSheetBehavior.STATE_DRAGGING:
|
||||
case BottomSheetBehavior.STATE_SETTLING:
|
||||
if (player != null && player.isFullscreen()) {
|
||||
if (isPlayerAvailable() && player.isFullscreen()) {
|
||||
showSystemUi();
|
||||
}
|
||||
if (player != null && player.isControlsVisible()) {
|
||||
if (isPlayerAvailable() && player.isControlsVisible()) {
|
||||
player.hideControls(0, 0);
|
||||
}
|
||||
break;
|
||||
@@ -2273,10 +2306,8 @@ public final class VideoDetailFragment
|
||||
binding.overlayTitleTextView.setText(isEmpty(overlayTitle) ? "" : overlayTitle);
|
||||
binding.overlayChannelTextView.setText(isEmpty(uploader) ? "" : uploader);
|
||||
binding.overlayThumbnail.setImageResource(R.drawable.dummy_thumbnail_dark);
|
||||
if (!isEmpty(thumbnailUrl)) {
|
||||
IMAGE_LOADER.displayImage(thumbnailUrl, binding.overlayThumbnail,
|
||||
ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS, null);
|
||||
}
|
||||
PicassoHelper.loadThumbnail(thumbnailUrl).tag(PICASSO_VIDEO_DETAILS_TAG)
|
||||
.into(binding.overlayThumbnail);
|
||||
}
|
||||
|
||||
private void setOverlayPlayPauseImage(final boolean playerIsPlaying) {
|
||||
@@ -2310,4 +2341,17 @@ public final class VideoDetailFragment
|
||||
binding.overlayPlayPauseButton.setClickable(enable);
|
||||
binding.overlayCloseButton.setClickable(enable);
|
||||
}
|
||||
|
||||
// helpers to check the state of player and playerService
|
||||
boolean isPlayerAvailable() {
|
||||
return (player != null);
|
||||
}
|
||||
|
||||
boolean isPlayerServiceAvailable() {
|
||||
return (playerService != null);
|
||||
}
|
||||
|
||||
boolean isPlayerAndPlayerServiceAvailable() {
|
||||
return (player != null && playerService != null);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -350,12 +350,16 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I>
|
||||
if (context == null || context.getResources() == null || activity == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final List<StreamDialogEntry> entries = new ArrayList<>();
|
||||
|
||||
if (PlayerHolder.getType() != null) {
|
||||
if (PlayerHolder.getInstance().isPlayerOpen()) {
|
||||
entries.add(StreamDialogEntry.enqueue);
|
||||
|
||||
if (PlayerHolder.getInstance().getQueueSize() > 1) {
|
||||
entries.add(StreamDialogEntry.enqueue_next);
|
||||
}
|
||||
}
|
||||
|
||||
if (item.getStreamType() == StreamType.AUDIO_STREAM) {
|
||||
entries.addAll(Arrays.asList(
|
||||
StreamDialogEntry.start_here_on_background,
|
||||
|
||||
@@ -37,13 +37,14 @@ import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||
import org.schabi.newpipe.fragments.list.BaseListInfoFragment;
|
||||
import org.schabi.newpipe.ktx.AnimationType;
|
||||
import org.schabi.newpipe.local.subscription.SubscriptionManager;
|
||||
import org.schabi.newpipe.player.MainPlayer.PlayerType;
|
||||
import org.schabi.newpipe.player.playqueue.ChannelPlayQueue;
|
||||
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
||||
import org.schabi.newpipe.util.ExtractorHelper;
|
||||
import org.schabi.newpipe.util.ImageDisplayConstants;
|
||||
import org.schabi.newpipe.util.Localization;
|
||||
import org.schabi.newpipe.util.NavigationHelper;
|
||||
import org.schabi.newpipe.util.external_communication.ShareUtils;
|
||||
import org.schabi.newpipe.util.PicassoHelper;
|
||||
import org.schabi.newpipe.util.ThemeHelper;
|
||||
|
||||
import java.util.ArrayList;
|
||||
@@ -66,7 +67,10 @@ import static org.schabi.newpipe.ktx.ViewUtils.animateBackgroundColor;
|
||||
|
||||
public class ChannelFragment extends BaseListInfoFragment<ChannelInfo>
|
||||
implements View.OnClickListener {
|
||||
|
||||
private static final int BUTTON_DEBOUNCE_INTERVAL = 100;
|
||||
private static final String PICASSO_CHANNEL_TAG = "PICASSO_CHANNEL_TAG";
|
||||
|
||||
private final CompositeDisposable disposables = new CompositeDisposable();
|
||||
private Disposable subscribeButtonMonitor;
|
||||
|
||||
@@ -421,10 +425,7 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo>
|
||||
@Override
|
||||
public void showLoading() {
|
||||
super.showLoading();
|
||||
|
||||
IMAGE_LOADER.cancelDisplayTask(headerBinding.channelBannerImage);
|
||||
IMAGE_LOADER.cancelDisplayTask(headerBinding.channelAvatarView);
|
||||
IMAGE_LOADER.cancelDisplayTask(headerBinding.subChannelAvatarView);
|
||||
PicassoHelper.cancelTag(PICASSO_CHANNEL_TAG);
|
||||
animate(headerBinding.channelSubscribeButton, false, 100);
|
||||
}
|
||||
|
||||
@@ -433,13 +434,12 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo>
|
||||
super.handleResult(result);
|
||||
|
||||
headerBinding.getRoot().setVisibility(View.VISIBLE);
|
||||
IMAGE_LOADER.displayImage(result.getBannerUrl(), headerBinding.channelBannerImage,
|
||||
ImageDisplayConstants.DISPLAY_BANNER_OPTIONS);
|
||||
IMAGE_LOADER.displayImage(result.getAvatarUrl(), headerBinding.channelAvatarView,
|
||||
ImageDisplayConstants.DISPLAY_AVATAR_OPTIONS);
|
||||
IMAGE_LOADER.displayImage(result.getParentChannelAvatarUrl(),
|
||||
headerBinding.subChannelAvatarView,
|
||||
ImageDisplayConstants.DISPLAY_AVATAR_OPTIONS);
|
||||
PicassoHelper.loadBanner(result.getBannerUrl()).tag(PICASSO_CHANNEL_TAG)
|
||||
.into(headerBinding.channelBannerImage);
|
||||
PicassoHelper.loadAvatar(result.getAvatarUrl()).tag(PICASSO_CHANNEL_TAG)
|
||||
.into(headerBinding.channelAvatarView);
|
||||
PicassoHelper.loadAvatar(result.getParentChannelAvatarUrl()).tag(PICASSO_CHANNEL_TAG)
|
||||
.into(headerBinding.subChannelAvatarView);
|
||||
|
||||
headerBinding.channelSubscriberView.setVisibility(View.VISIBLE);
|
||||
if (result.getSubscriberCount() >= 0) {
|
||||
@@ -496,12 +496,12 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo>
|
||||
.playOnBackgroundPlayer(activity, getPlayQueue(), false));
|
||||
|
||||
playlistControlBinding.playlistCtrlPlayPopupButton.setOnLongClickListener(view -> {
|
||||
NavigationHelper.enqueueOnPopupPlayer(activity, getPlayQueue(), true);
|
||||
NavigationHelper.enqueueOnPlayer(activity, getPlayQueue(), PlayerType.POPUP);
|
||||
return true;
|
||||
});
|
||||
|
||||
playlistControlBinding.playlistCtrlPlayBgButton.setOnLongClickListener(view -> {
|
||||
NavigationHelper.enqueueOnBackgroundPlayer(activity, getPlayQueue(), true);
|
||||
NavigationHelper.enqueueOnPlayer(activity, getPlayQueue(), PlayerType.AUDIO);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
@@ -24,6 +25,8 @@ import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
||||
public class CommentsFragment extends BaseListInfoFragment<CommentsInfo> {
|
||||
private final CompositeDisposable disposables = new CompositeDisposable();
|
||||
|
||||
private TextView emptyStateDesc;
|
||||
|
||||
public static CommentsFragment getInstance(final int serviceId, final String url,
|
||||
final String name) {
|
||||
final CommentsFragment instance = new CommentsFragment();
|
||||
@@ -35,6 +38,13 @@ public class CommentsFragment extends BaseListInfoFragment<CommentsInfo> {
|
||||
super(UserAction.REQUESTED_COMMENTS);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void initViews(final View rootView, final Bundle savedInstanceState) {
|
||||
super.initViews(rootView, savedInstanceState);
|
||||
|
||||
emptyStateDesc = rootView.findViewById(R.id.empty_state_desc);
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// LifeCycle
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
@@ -73,6 +83,12 @@ public class CommentsFragment extends BaseListInfoFragment<CommentsInfo> {
|
||||
@Override
|
||||
public void handleResult(@NonNull final CommentsInfo result) {
|
||||
super.handleResult(result);
|
||||
|
||||
emptyStateDesc.setText(
|
||||
result.isCommentsDisabled()
|
||||
? R.string.comments_are_disabled
|
||||
: R.string.no_comments);
|
||||
|
||||
ViewUtils.slideUp(requireView(), 120, 150, 0.06f);
|
||||
disposables.clear();
|
||||
}
|
||||
|
||||
@@ -37,11 +37,12 @@ import org.schabi.newpipe.extractor.stream.StreamType;
|
||||
import org.schabi.newpipe.fragments.list.BaseListInfoFragment;
|
||||
import org.schabi.newpipe.info_list.InfoItemDialog;
|
||||
import org.schabi.newpipe.local.playlist.RemotePlaylistManager;
|
||||
import org.schabi.newpipe.player.MainPlayer.PlayerType;
|
||||
import org.schabi.newpipe.player.helper.PlayerHolder;
|
||||
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
||||
import org.schabi.newpipe.player.playqueue.PlaylistPlayQueue;
|
||||
import org.schabi.newpipe.util.ExtractorHelper;
|
||||
import org.schabi.newpipe.util.ImageDisplayConstants;
|
||||
import org.schabi.newpipe.util.PicassoHelper;
|
||||
import org.schabi.newpipe.util.external_communication.KoreUtils;
|
||||
import org.schabi.newpipe.util.Localization;
|
||||
import org.schabi.newpipe.util.NavigationHelper;
|
||||
@@ -64,12 +65,16 @@ import static org.schabi.newpipe.ktx.ViewUtils.animate;
|
||||
import static org.schabi.newpipe.ktx.ViewUtils.animateHideRecyclerViewAllowingScrolling;
|
||||
|
||||
public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
|
||||
|
||||
private static final String PICASSO_PLAYLIST_TAG = "PICASSO_PLAYLIST_TAG";
|
||||
|
||||
private CompositeDisposable disposables;
|
||||
private Subscription bookmarkReactor;
|
||||
private AtomicBoolean isBookmarkButtonReady;
|
||||
|
||||
private RemotePlaylistManager remotePlaylistManager;
|
||||
private PlaylistRemoteEntity playlistEntity;
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Views
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
@@ -144,9 +149,14 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
|
||||
|
||||
final ArrayList<StreamDialogEntry> entries = new ArrayList<>();
|
||||
|
||||
if (PlayerHolder.getType() != null) {
|
||||
if (PlayerHolder.getInstance().isPlayerOpen()) {
|
||||
entries.add(StreamDialogEntry.enqueue);
|
||||
|
||||
if (PlayerHolder.getInstance().getQueueSize() > 1) {
|
||||
entries.add(StreamDialogEntry.enqueue_next);
|
||||
}
|
||||
}
|
||||
|
||||
if (item.getStreamType() == StreamType.AUDIO_STREAM) {
|
||||
entries.addAll(Arrays.asList(
|
||||
StreamDialogEntry.start_here_on_background,
|
||||
@@ -274,7 +284,7 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
|
||||
animate(headerBinding.getRoot(), false, 200);
|
||||
animateHideRecyclerViewAllowingScrolling(itemsList);
|
||||
|
||||
IMAGE_LOADER.cancelDisplayTask(headerBinding.uploaderAvatarView);
|
||||
PicassoHelper.cancelTag(PICASSO_PLAYLIST_TAG);
|
||||
animate(headerBinding.uploaderLayout, false, 200);
|
||||
}
|
||||
|
||||
@@ -317,8 +327,8 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
|
||||
R.drawable.ic_radio)
|
||||
);
|
||||
} else {
|
||||
IMAGE_LOADER.displayImage(avatarUrl, headerBinding.uploaderAvatarView,
|
||||
ImageDisplayConstants.DISPLAY_AVATAR_OPTIONS);
|
||||
PicassoHelper.loadAvatar(avatarUrl).tag(PICASSO_PLAYLIST_TAG)
|
||||
.into(headerBinding.uploaderAvatarView);
|
||||
}
|
||||
|
||||
headerBinding.playlistStreamCount.setText(Localization
|
||||
@@ -343,12 +353,12 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
|
||||
NavigationHelper.playOnBackgroundPlayer(activity, getPlayQueue(), false));
|
||||
|
||||
playlistControlBinding.playlistCtrlPlayPopupButton.setOnLongClickListener(view -> {
|
||||
NavigationHelper.enqueueOnPopupPlayer(activity, getPlayQueue(), true);
|
||||
NavigationHelper.enqueueOnPlayer(activity, getPlayQueue(), PlayerType.POPUP);
|
||||
return true;
|
||||
});
|
||||
|
||||
playlistControlBinding.playlistCtrlPlayBgButton.setOnLongClickListener(view -> {
|
||||
NavigationHelper.enqueueOnBackgroundPlayer(activity, getPlayQueue(), true);
|
||||
NavigationHelper.enqueueOnPlayer(activity, getPlayQueue(), PlayerType.AUDIO);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -57,6 +57,7 @@ import org.schabi.newpipe.fragments.list.BaseListFragment;
|
||||
import org.schabi.newpipe.ktx.AnimationType;
|
||||
import org.schabi.newpipe.ktx.ExceptionUtils;
|
||||
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
||||
import org.schabi.newpipe.settings.NewPipeSettings;
|
||||
import org.schabi.newpipe.util.Constants;
|
||||
import org.schabi.newpipe.util.DeviceUtils;
|
||||
import org.schabi.newpipe.util.ExtractorHelper;
|
||||
@@ -65,16 +66,19 @@ import org.schabi.newpipe.util.ServiceHelper;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Queue;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import icepick.State;
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.rxjava3.core.Flowable;
|
||||
import io.reactivex.rxjava3.core.Observable;
|
||||
import io.reactivex.rxjava3.core.Single;
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
||||
import io.reactivex.rxjava3.disposables.Disposable;
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers;
|
||||
@@ -143,7 +147,8 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
||||
@Nullable private Map<Integer, String> menuItemToFilterName = null;
|
||||
private StreamingService service;
|
||||
private Page nextPage;
|
||||
private boolean isSuggestionsEnabled = true;
|
||||
private boolean showLocalSuggestions = true;
|
||||
private boolean showRemoteSuggestions = true;
|
||||
|
||||
private Disposable searchDisposable;
|
||||
private Disposable suggestionDisposable;
|
||||
@@ -194,26 +199,14 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
||||
public void onAttach(@NonNull final Context context) {
|
||||
super.onAttach(context);
|
||||
|
||||
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(activity);
|
||||
showLocalSuggestions = NewPipeSettings.showLocalSearchSuggestions(activity, prefs);
|
||||
showRemoteSuggestions = NewPipeSettings.showRemoteSearchSuggestions(activity, prefs);
|
||||
|
||||
suggestionListAdapter = new SuggestionListAdapter(activity);
|
||||
final SharedPreferences preferences
|
||||
= PreferenceManager.getDefaultSharedPreferences(activity);
|
||||
final boolean isSearchHistoryEnabled = preferences
|
||||
.getBoolean(getString(R.string.enable_search_history_key), true);
|
||||
suggestionListAdapter.setShowSuggestionHistory(isSearchHistoryEnabled);
|
||||
|
||||
historyRecordManager = new HistoryRecordManager(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(final Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
final SharedPreferences preferences
|
||||
= PreferenceManager.getDefaultSharedPreferences(activity);
|
||||
isSuggestionsEnabled = preferences
|
||||
.getBoolean(getString(R.string.show_search_suggestions_key), true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(final LayoutInflater inflater, @Nullable final ViewGroup container,
|
||||
@Nullable final Bundle savedInstanceState) {
|
||||
@@ -222,6 +215,7 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
||||
|
||||
@Override
|
||||
public void onViewCreated(@NonNull final View rootView, final Bundle savedInstanceState) {
|
||||
searchBinding = FragmentSearchBinding.bind(rootView);
|
||||
super.onViewCreated(rootView, savedInstanceState);
|
||||
showSearchOnStart();
|
||||
initSearchListeners();
|
||||
@@ -348,7 +342,6 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
||||
@Override
|
||||
protected void initViews(final View rootView, final Bundle savedInstanceState) {
|
||||
super.initViews(rootView, savedInstanceState);
|
||||
searchBinding = FragmentSearchBinding.bind(rootView);
|
||||
|
||||
searchBinding.suggestionsList.setAdapter(suggestionListAdapter);
|
||||
new ItemTouchHelper(new ItemTouchHelper.Callback() {
|
||||
@@ -554,7 +547,7 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "onClick() called with: v = [" + v + "]");
|
||||
}
|
||||
if (isSuggestionsEnabled && !isErrorPanelVisible()) {
|
||||
if ((showLocalSuggestions || showRemoteSuggestions) && !isErrorPanelVisible()) {
|
||||
showSuggestionsPanel();
|
||||
}
|
||||
if (DeviceUtils.isTv(getContext())) {
|
||||
@@ -567,7 +560,8 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
||||
Log.d(TAG, "onFocusChange() called with: "
|
||||
+ "v = [" + v + "], hasFocus = [" + hasFocus + "]");
|
||||
}
|
||||
if (isSuggestionsEnabled && hasFocus && !isErrorPanelVisible()) {
|
||||
if ((showLocalSuggestions || showRemoteSuggestions)
|
||||
&& hasFocus && !isErrorPanelVisible()) {
|
||||
showSuggestionsPanel();
|
||||
}
|
||||
});
|
||||
@@ -743,6 +737,34 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
private Observable<List<SuggestionItem>> getLocalSuggestionsObservable(
|
||||
final String query, final int similarQueryLimit) {
|
||||
return historyRecordManager
|
||||
.getRelatedSearches(query, similarQueryLimit, 25)
|
||||
.toObservable()
|
||||
.map(searchHistoryEntries -> {
|
||||
final Set<SuggestionItem> result = new HashSet<>(); // remove duplicates
|
||||
for (final SearchHistoryEntry entry : searchHistoryEntries) {
|
||||
result.add(new SuggestionItem(true, entry.getSearch()));
|
||||
}
|
||||
return new ArrayList<>(result);
|
||||
});
|
||||
}
|
||||
|
||||
private Observable<List<SuggestionItem>> getRemoteSuggestionsObservable(final String query) {
|
||||
return ExtractorHelper
|
||||
.suggestionsFor(serviceId, query)
|
||||
.toObservable()
|
||||
.map(strings -> {
|
||||
final List<SuggestionItem> result = new ArrayList<>();
|
||||
for (final String entry : strings) {
|
||||
result.add(new SuggestionItem(false, entry));
|
||||
}
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
private void initSuggestionObserver() {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "initSuggestionObserver() called");
|
||||
@@ -753,73 +775,53 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
||||
|
||||
suggestionDisposable = suggestionPublisher
|
||||
.debounce(SUGGESTIONS_DEBOUNCE, TimeUnit.MILLISECONDS)
|
||||
.startWithItem(searchString != null
|
||||
? searchString
|
||||
: "")
|
||||
.filter(ss -> isSuggestionsEnabled)
|
||||
.startWithItem(searchString == null ? "" : searchString)
|
||||
.switchMap(query -> {
|
||||
final Flowable<List<SearchHistoryEntry>> flowable = historyRecordManager
|
||||
.getRelatedSearches(query, 3, 25);
|
||||
final Observable<List<SuggestionItem>> local = flowable.toObservable()
|
||||
.map(searchHistoryEntries -> {
|
||||
final List<SuggestionItem> result = new ArrayList<>();
|
||||
for (final SearchHistoryEntry entry : searchHistoryEntries) {
|
||||
result.add(new SuggestionItem(true, entry.getSearch()));
|
||||
}
|
||||
return result;
|
||||
});
|
||||
// Only show remote suggestions if they are enabled in settings and
|
||||
// the query length is at least THRESHOLD_NETWORK_SUGGESTION
|
||||
final boolean shallShowRemoteSuggestionsNow = showRemoteSuggestions
|
||||
&& query.length() >= THRESHOLD_NETWORK_SUGGESTION;
|
||||
|
||||
if (query.length() < THRESHOLD_NETWORK_SUGGESTION) {
|
||||
// Only pass through if the query length
|
||||
// is equal or greater than THRESHOLD_NETWORK_SUGGESTION
|
||||
return local.materialize();
|
||||
if (showLocalSuggestions && shallShowRemoteSuggestionsNow) {
|
||||
return Observable.zip(
|
||||
getLocalSuggestionsObservable(query, 3),
|
||||
getRemoteSuggestionsObservable(query),
|
||||
(local, remote) -> {
|
||||
remote.removeIf(remoteItem -> local.stream().anyMatch(
|
||||
localItem -> localItem.equals(remoteItem)));
|
||||
local.addAll(remote);
|
||||
return local;
|
||||
})
|
||||
.materialize();
|
||||
} else if (showLocalSuggestions) {
|
||||
return getLocalSuggestionsObservable(query, 25)
|
||||
.materialize();
|
||||
} else if (shallShowRemoteSuggestionsNow) {
|
||||
return getRemoteSuggestionsObservable(query)
|
||||
.materialize();
|
||||
} else {
|
||||
return Single.fromCallable(Collections::<SuggestionItem>emptyList)
|
||||
.toObservable()
|
||||
.materialize();
|
||||
}
|
||||
|
||||
final Observable<List<SuggestionItem>> network = ExtractorHelper
|
||||
.suggestionsFor(serviceId, query)
|
||||
.onErrorReturn(throwable -> {
|
||||
if (!ExceptionUtils.isNetworkRelated(throwable)) {
|
||||
showSnackBarError(new ErrorInfo(throwable,
|
||||
UserAction.GET_SUGGESTIONS, searchString, serviceId));
|
||||
}
|
||||
return new ArrayList<>();
|
||||
})
|
||||
.toObservable()
|
||||
.map(strings -> {
|
||||
final List<SuggestionItem> result = new ArrayList<>();
|
||||
for (final String entry : strings) {
|
||||
result.add(new SuggestionItem(false, entry));
|
||||
}
|
||||
return result;
|
||||
});
|
||||
|
||||
return Observable.zip(local, network, (localResult, networkResult) -> {
|
||||
final List<SuggestionItem> result = new ArrayList<>();
|
||||
if (localResult.size() > 0) {
|
||||
result.addAll(localResult);
|
||||
}
|
||||
|
||||
// Remove duplicates
|
||||
networkResult.removeIf(networkItem ->
|
||||
localResult.stream().anyMatch(localItem ->
|
||||
localItem.query.equals(networkItem.query)));
|
||||
|
||||
if (networkResult.size() > 0) {
|
||||
result.addAll(networkResult);
|
||||
}
|
||||
return result;
|
||||
}).materialize();
|
||||
})
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(listNotification -> {
|
||||
if (listNotification.isOnNext()) {
|
||||
handleSuggestions(listNotification.getValue());
|
||||
} else if (listNotification.isOnError()) {
|
||||
showError(new ErrorInfo(listNotification.getError(),
|
||||
UserAction.GET_SUGGESTIONS, searchString, serviceId));
|
||||
}
|
||||
});
|
||||
.subscribe(
|
||||
listNotification -> {
|
||||
if (listNotification.isOnNext()) {
|
||||
if (listNotification.getValue() != null) {
|
||||
handleSuggestions(listNotification.getValue());
|
||||
}
|
||||
} else if (listNotification.isOnError()
|
||||
&& listNotification.getError() != null
|
||||
&& !ExceptionUtils.isInterruptedCaused(
|
||||
listNotification.getError())) {
|
||||
showSnackBarError(new ErrorInfo(listNotification.getError(),
|
||||
UserAction.GET_SUGGESTIONS, searchString, serviceId));
|
||||
}
|
||||
}, throwable -> showSnackBarError(new ErrorInfo(
|
||||
throwable, UserAction.GET_SUGGESTIONS, searchString, serviceId)));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package org.schabi.newpipe.fragments.list.search;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
public class SuggestionItem {
|
||||
final boolean fromHistory;
|
||||
public final String query;
|
||||
@@ -9,6 +11,20 @@ public class SuggestionItem {
|
||||
this.query = query;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(final Object o) {
|
||||
if (o instanceof SuggestionItem) {
|
||||
return query.equals(((SuggestionItem) o).query);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return query.hashCode();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String toString() {
|
||||
return "[" + fromHistory + "→" + query + "]";
|
||||
|
||||
@@ -19,7 +19,6 @@ public class SuggestionListAdapter
|
||||
private final ArrayList<SuggestionItem> items = new ArrayList<>();
|
||||
private final Context context;
|
||||
private OnSuggestionItemSelected listener;
|
||||
private boolean showSuggestionHistory = true;
|
||||
|
||||
public SuggestionListAdapter(final Context context) {
|
||||
this.context = context;
|
||||
@@ -27,16 +26,7 @@ public class SuggestionListAdapter
|
||||
|
||||
public void setItems(final List<SuggestionItem> items) {
|
||||
this.items.clear();
|
||||
if (showSuggestionHistory) {
|
||||
this.items.addAll(items);
|
||||
} else {
|
||||
// remove history items if history is disabled
|
||||
for (final SuggestionItem item : items) {
|
||||
if (!item.fromHistory) {
|
||||
this.items.add(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
this.items.addAll(items);
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
@@ -44,10 +34,6 @@ public class SuggestionListAdapter
|
||||
this.listener = listener;
|
||||
}
|
||||
|
||||
public void setShowSuggestionHistory(final boolean v) {
|
||||
showSuggestionHistory = v;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SuggestionItemHolder onCreateViewHolder(final ViewGroup parent, final int viewType) {
|
||||
return new SuggestionItemHolder(LayoutInflater.from(context)
|
||||
|
||||
@@ -6,8 +6,6 @@ import android.view.ViewGroup;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.nostra13.universalimageloader.core.ImageLoader;
|
||||
|
||||
import org.schabi.newpipe.extractor.InfoItem;
|
||||
import org.schabi.newpipe.extractor.channel.ChannelInfoItem;
|
||||
import org.schabi.newpipe.extractor.comments.CommentsInfoItem;
|
||||
@@ -51,7 +49,6 @@ import org.schabi.newpipe.util.OnClickGesture;
|
||||
|
||||
public class InfoItemBuilder {
|
||||
private final Context context;
|
||||
private final ImageLoader imageLoader = ImageLoader.getInstance();
|
||||
|
||||
private OnClickGesture<StreamInfoItem> onStreamSelectedListener;
|
||||
private OnClickGesture<ChannelInfoItem> onChannelSelectedListener;
|
||||
@@ -101,10 +98,6 @@ public class InfoItemBuilder {
|
||||
return context;
|
||||
}
|
||||
|
||||
public ImageLoader getImageLoader() {
|
||||
return imageLoader;
|
||||
}
|
||||
|
||||
public OnClickGesture<StreamInfoItem> getOnStreamSelectedListener() {
|
||||
return onStreamSelectedListener;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
package org.schabi.newpipe.info_list
|
||||
|
||||
import android.util.Log
|
||||
import com.xwray.groupie.GroupAdapter
|
||||
import com.xwray.groupie.GroupieViewHolder
|
||||
import com.xwray.groupie.GroupieAdapter
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfo
|
||||
import kotlin.math.max
|
||||
|
||||
@@ -11,7 +10,7 @@ import kotlin.math.max
|
||||
*/
|
||||
class StreamSegmentAdapter(
|
||||
private val listener: StreamSegmentListener
|
||||
) : GroupAdapter<GroupieViewHolder>() {
|
||||
) : GroupieAdapter() {
|
||||
|
||||
var currentIndex: Int = 0
|
||||
private set
|
||||
|
||||
@@ -3,13 +3,12 @@ package org.schabi.newpipe.info_list
|
||||
import android.view.View
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import com.nostra13.universalimageloader.core.ImageLoader
|
||||
import com.xwray.groupie.GroupieViewHolder
|
||||
import com.xwray.groupie.Item
|
||||
import org.schabi.newpipe.R
|
||||
import org.schabi.newpipe.extractor.stream.StreamSegment
|
||||
import org.schabi.newpipe.util.ImageDisplayConstants
|
||||
import org.schabi.newpipe.util.Localization
|
||||
import org.schabi.newpipe.util.PicassoHelper
|
||||
|
||||
class StreamSegmentItem(
|
||||
private val item: StreamSegment,
|
||||
@@ -24,10 +23,8 @@ class StreamSegmentItem(
|
||||
|
||||
override fun bind(viewHolder: GroupieViewHolder, position: Int) {
|
||||
item.previewUrl?.let {
|
||||
ImageLoader.getInstance().displayImage(
|
||||
it, viewHolder.root.findViewById<ImageView>(R.id.previewImage),
|
||||
ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS
|
||||
)
|
||||
PicassoHelper.loadThumbnail(it)
|
||||
.into(viewHolder.root.findViewById<ImageView>(R.id.previewImage))
|
||||
}
|
||||
viewHolder.root.findViewById<TextView>(R.id.textViewTitle).text = item.title
|
||||
if (item.channelName == null) {
|
||||
|
||||
@@ -8,7 +8,7 @@ import org.schabi.newpipe.extractor.InfoItem;
|
||||
import org.schabi.newpipe.extractor.channel.ChannelInfoItem;
|
||||
import org.schabi.newpipe.info_list.InfoItemBuilder;
|
||||
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
||||
import org.schabi.newpipe.util.ImageDisplayConstants;
|
||||
import org.schabi.newpipe.util.PicassoHelper;
|
||||
import org.schabi.newpipe.util.Localization;
|
||||
|
||||
import de.hdodenhof.circleimageview.CircleImageView;
|
||||
@@ -43,10 +43,7 @@ public class ChannelMiniInfoItemHolder extends InfoItemHolder {
|
||||
itemTitleView.setText(item.getName());
|
||||
itemAdditionalDetailView.setText(getDetailLine(item));
|
||||
|
||||
itemBuilder.getImageLoader()
|
||||
.displayImage(item.getThumbnailUrl(),
|
||||
itemThumbnailView,
|
||||
ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS);
|
||||
PicassoHelper.loadThumbnail(item.getThumbnailUrl()).into(itemThumbnailView);
|
||||
|
||||
itemView.setOnClickListener(view -> {
|
||||
if (itemBuilder.getOnChannelSelectedListener() != null) {
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package org.schabi.newpipe.info_list.holder;
|
||||
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
@@ -31,11 +33,13 @@ import org.schabi.newpipe.local.history.HistoryRecordManager;
|
||||
|
||||
public class CommentsInfoItemHolder extends CommentsMiniInfoItemHolder {
|
||||
public final TextView itemTitleView;
|
||||
private final ImageView itemHeartView;
|
||||
|
||||
public CommentsInfoItemHolder(final InfoItemBuilder infoItemBuilder, final ViewGroup parent) {
|
||||
super(infoItemBuilder, R.layout.list_comments_item, parent);
|
||||
|
||||
itemTitleView = itemView.findViewById(R.id.itemTitleView);
|
||||
itemHeartView = itemView.findViewById(R.id.detail_heart_image_view);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -49,5 +53,7 @@ public class CommentsInfoItemHolder extends CommentsMiniInfoItemHolder {
|
||||
final CommentsInfoItem item = (CommentsInfoItem) infoItem;
|
||||
|
||||
itemTitleView.setText(item.getUploaderName());
|
||||
|
||||
itemHeartView.setVisibility(item.isHeartedByUploader() ? View.VISIBLE : View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,16 @@
|
||||
package org.schabi.newpipe.info_list.holder;
|
||||
|
||||
import android.content.SharedPreferences;
|
||||
import android.text.TextUtils;
|
||||
import android.text.method.LinkMovementMethod;
|
||||
import android.text.style.URLSpan;
|
||||
import android.text.util.Linkify;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.RelativeLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.preference.PreferenceManager;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.error.ErrorActivity;
|
||||
@@ -21,30 +20,29 @@ import org.schabi.newpipe.info_list.InfoItemBuilder;
|
||||
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
||||
import org.schabi.newpipe.util.CommentTextOnTouchListener;
|
||||
import org.schabi.newpipe.util.DeviceUtils;
|
||||
import org.schabi.newpipe.util.ImageDisplayConstants;
|
||||
import org.schabi.newpipe.util.Localization;
|
||||
import org.schabi.newpipe.util.NavigationHelper;
|
||||
import org.schabi.newpipe.util.external_communication.TimestampExtractor;
|
||||
import org.schabi.newpipe.util.external_communication.ShareUtils;
|
||||
import org.schabi.newpipe.util.PicassoHelper;
|
||||
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import de.hdodenhof.circleimageview.CircleImageView;
|
||||
|
||||
public class CommentsMiniInfoItemHolder extends InfoItemHolder {
|
||||
private static final String TAG = "CommentsMiniIIHolder";
|
||||
|
||||
private static final int COMMENT_DEFAULT_LINES = 2;
|
||||
private static final int COMMENT_EXPANDED_LINES = 1000;
|
||||
private static final Pattern PATTERN = Pattern.compile("(\\d+:)?(\\d+)?:(\\d+)");
|
||||
private final String downloadThumbnailKey;
|
||||
|
||||
private final int commentHorizontalPadding;
|
||||
private final int commentVerticalPadding;
|
||||
|
||||
private SharedPreferences preferences = null;
|
||||
private final RelativeLayout itemRoot;
|
||||
public final CircleImageView itemThumbnailView;
|
||||
private final TextView itemContentView;
|
||||
private final TextView itemLikesCountView;
|
||||
private final TextView itemDislikesCountView;
|
||||
private final TextView itemPublishedTime;
|
||||
|
||||
private String commentText;
|
||||
@@ -53,20 +51,21 @@ public class CommentsMiniInfoItemHolder extends InfoItemHolder {
|
||||
private final Linkify.TransformFilter timestampLink = new Linkify.TransformFilter() {
|
||||
@Override
|
||||
public String transformUrl(final Matcher match, final String url) {
|
||||
int timestamp = 0;
|
||||
final String hours = match.group(1);
|
||||
final String minutes = match.group(2);
|
||||
final String seconds = match.group(3);
|
||||
if (hours != null) {
|
||||
timestamp += (Integer.parseInt(hours.replace(":", "")) * 3600);
|
||||
try {
|
||||
final TimestampExtractor.TimestampMatchDTO timestampMatchDTO =
|
||||
TimestampExtractor.getTimestampFromMatcher(match, commentText);
|
||||
|
||||
if (timestampMatchDTO == null) {
|
||||
return url;
|
||||
}
|
||||
|
||||
return streamUrl + url.replace(
|
||||
match.group(0),
|
||||
"#timestamp=" + timestampMatchDTO.seconds());
|
||||
} catch (final Exception ex) {
|
||||
Log.e(TAG, "Unable to process url='" + url + "' as timestampLink", ex);
|
||||
return url;
|
||||
}
|
||||
if (minutes != null) {
|
||||
timestamp += (Integer.parseInt(minutes.replace(":", "")) * 60);
|
||||
}
|
||||
if (seconds != null) {
|
||||
timestamp += (Integer.parseInt(seconds));
|
||||
}
|
||||
return streamUrl + url.replace(match.group(0), "#timestamp=" + timestamp);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -77,13 +76,9 @@ public class CommentsMiniInfoItemHolder extends InfoItemHolder {
|
||||
itemRoot = itemView.findViewById(R.id.itemRoot);
|
||||
itemThumbnailView = itemView.findViewById(R.id.itemThumbnailView);
|
||||
itemLikesCountView = itemView.findViewById(R.id.detail_thumbs_up_count_view);
|
||||
itemDislikesCountView = itemView.findViewById(R.id.detail_thumbs_down_count_view);
|
||||
itemPublishedTime = itemView.findViewById(R.id.itemPublishedTime);
|
||||
itemContentView = itemView.findViewById(R.id.itemCommentContentView);
|
||||
|
||||
downloadThumbnailKey = infoItemBuilder.getContext().
|
||||
getString(R.string.download_thumbnail_key);
|
||||
|
||||
commentHorizontalPadding = (int) infoItemBuilder.getContext()
|
||||
.getResources().getDimension(R.dimen.comments_horizontal_padding);
|
||||
commentVerticalPadding = (int) infoItemBuilder.getContext()
|
||||
@@ -103,14 +98,8 @@ public class CommentsMiniInfoItemHolder extends InfoItemHolder {
|
||||
}
|
||||
final CommentsInfoItem item = (CommentsInfoItem) infoItem;
|
||||
|
||||
preferences = PreferenceManager.getDefaultSharedPreferences(itemBuilder.getContext());
|
||||
|
||||
itemBuilder.getImageLoader()
|
||||
.displayImage(item.getUploaderAvatarUrl(),
|
||||
itemThumbnailView,
|
||||
ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS);
|
||||
|
||||
if (preferences.getBoolean(downloadThumbnailKey, true)) {
|
||||
PicassoHelper.loadAvatar(item.getUploaderAvatarUrl()).into(itemThumbnailView);
|
||||
if (PicassoHelper.getShouldLoadImages()) {
|
||||
itemThumbnailView.setVisibility(View.VISIBLE);
|
||||
itemRoot.setPadding(commentVerticalPadding, commentVerticalPadding,
|
||||
commentVerticalPadding, commentVerticalPadding);
|
||||
@@ -254,7 +243,14 @@ public class CommentsMiniInfoItemHolder extends InfoItemHolder {
|
||||
}
|
||||
|
||||
private void linkify() {
|
||||
Linkify.addLinks(itemContentView, Linkify.WEB_URLS);
|
||||
Linkify.addLinks(itemContentView, PATTERN, null, null, timestampLink);
|
||||
Linkify.addLinks(
|
||||
itemContentView,
|
||||
Linkify.WEB_URLS);
|
||||
Linkify.addLinks(
|
||||
itemContentView,
|
||||
TimestampExtractor.TIMESTAMPS_PATTERN,
|
||||
null,
|
||||
null,
|
||||
timestampLink);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import org.schabi.newpipe.extractor.InfoItem;
|
||||
import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem;
|
||||
import org.schabi.newpipe.info_list.InfoItemBuilder;
|
||||
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
||||
import org.schabi.newpipe.util.ImageDisplayConstants;
|
||||
import org.schabi.newpipe.util.PicassoHelper;
|
||||
import org.schabi.newpipe.util.Localization;
|
||||
|
||||
public class PlaylistMiniInfoItemHolder extends InfoItemHolder {
|
||||
@@ -46,9 +46,7 @@ public class PlaylistMiniInfoItemHolder extends InfoItemHolder {
|
||||
.localizeStreamCountMini(itemStreamCountView.getContext(), item.getStreamCount()));
|
||||
itemUploaderView.setText(item.getUploaderName());
|
||||
|
||||
itemBuilder.getImageLoader()
|
||||
.displayImage(item.getThumbnailUrl(), itemThumbnailView,
|
||||
ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS);
|
||||
PicassoHelper.loadPlaylistThumbnail(item.getThumbnailUrl()).into(itemThumbnailView);
|
||||
|
||||
itemView.setOnClickListener(view -> {
|
||||
if (itemBuilder.getOnPlaylistSelectedListener() != null) {
|
||||
|
||||
@@ -15,7 +15,7 @@ import org.schabi.newpipe.extractor.stream.StreamType;
|
||||
import org.schabi.newpipe.info_list.InfoItemBuilder;
|
||||
import org.schabi.newpipe.ktx.ViewUtils;
|
||||
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
||||
import org.schabi.newpipe.util.ImageDisplayConstants;
|
||||
import org.schabi.newpipe.util.PicassoHelper;
|
||||
import org.schabi.newpipe.util.Localization;
|
||||
import org.schabi.newpipe.views.AnimatedProgressBar;
|
||||
|
||||
@@ -83,10 +83,7 @@ public class StreamMiniInfoItemHolder extends InfoItemHolder {
|
||||
}
|
||||
|
||||
// Default thumbnail is shown on error, while loading and if the url is empty
|
||||
itemBuilder.getImageLoader()
|
||||
.displayImage(item.getThumbnailUrl(),
|
||||
itemThumbnailView,
|
||||
ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS);
|
||||
PicassoHelper.loadThumbnail(item.getThumbnailUrl()).into(itemThumbnailView);
|
||||
|
||||
itemView.setOnClickListener(view -> {
|
||||
if (itemBuilder.getOnStreamSelectedListener() != null) {
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
package org.schabi.newpipe.local;
|
||||
|
||||
import android.content.Context;
|
||||
import android.widget.ImageView;
|
||||
|
||||
import com.nostra13.universalimageloader.core.DisplayImageOptions;
|
||||
import com.nostra13.universalimageloader.core.ImageLoader;
|
||||
|
||||
import org.schabi.newpipe.database.LocalItem;
|
||||
import org.schabi.newpipe.util.OnClickGesture;
|
||||
@@ -31,7 +27,6 @@ import org.schabi.newpipe.util.OnClickGesture;
|
||||
|
||||
public class LocalItemBuilder {
|
||||
private final Context context;
|
||||
private final ImageLoader imageLoader = ImageLoader.getInstance();
|
||||
|
||||
private OnClickGesture<LocalItem> onSelectedListener;
|
||||
|
||||
@@ -43,11 +38,6 @@ public class LocalItemBuilder {
|
||||
return context;
|
||||
}
|
||||
|
||||
public void displayImage(final String url, final ImageView view,
|
||||
final DisplayImageOptions options) {
|
||||
imageLoader.displayImage(url, view, options);
|
||||
}
|
||||
|
||||
public OnClickGesture<LocalItem> getOnItemSelectedListener() {
|
||||
return onSelectedListener;
|
||||
}
|
||||
|
||||
@@ -2,11 +2,11 @@ package org.schabi.newpipe.local.bookmark;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.os.Parcelable;
|
||||
import android.text.InputType;
|
||||
import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.EditText;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
@@ -22,6 +22,7 @@ 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.databinding.DialogEditTextBinding;
|
||||
import org.schabi.newpipe.error.ErrorInfo;
|
||||
import org.schabi.newpipe.error.UserAction;
|
||||
import org.schabi.newpipe.local.BaseLocalListFragment;
|
||||
@@ -255,14 +256,18 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL
|
||||
}
|
||||
|
||||
private void showLocalDialog(final PlaylistMetadataEntry selectedItem) {
|
||||
final View dialogView = View.inflate(getContext(), R.layout.dialog_bookmark, null);
|
||||
final EditText editText = dialogView.findViewById(R.id.playlist_name_edit_text);
|
||||
editText.setText(selectedItem.name);
|
||||
final DialogEditTextBinding dialogBinding
|
||||
= DialogEditTextBinding.inflate(getLayoutInflater());
|
||||
dialogBinding.dialogEditText.setHint(R.string.name);
|
||||
dialogBinding.dialogEditText.setInputType(InputType.TYPE_CLASS_TEXT);
|
||||
dialogBinding.dialogEditText.setText(selectedItem.name);
|
||||
|
||||
final AlertDialog.Builder builder = new AlertDialog.Builder(activity);
|
||||
builder.setView(dialogView)
|
||||
builder.setView(dialogBinding.getRoot())
|
||||
.setPositiveButton(R.string.rename_playlist, (dialog, which) ->
|
||||
changeLocalPlaylistName(selectedItem.uid, editText.getText().toString()))
|
||||
changeLocalPlaylistName(
|
||||
selectedItem.uid,
|
||||
dialogBinding.dialogEditText.getText().toString()))
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.setNeutralButton(R.string.delete, (dialog, which) -> {
|
||||
showDeleteDialog(selectedItem.name,
|
||||
|
||||
@@ -2,8 +2,7 @@ package org.schabi.newpipe.local.dialog;
|
||||
|
||||
import android.app.Dialog;
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
import android.widget.EditText;
|
||||
import android.text.InputType;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
@@ -13,6 +12,7 @@ import androidx.appcompat.app.AlertDialog;
|
||||
import org.schabi.newpipe.NewPipeDatabase;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.database.stream.model.StreamEntity;
|
||||
import org.schabi.newpipe.databinding.DialogEditTextBinding;
|
||||
import org.schabi.newpipe.local.playlist.LocalPlaylistManager;
|
||||
|
||||
import java.util.List;
|
||||
@@ -43,16 +43,18 @@ public final class PlaylistCreationDialog extends PlaylistDialog {
|
||||
return super.onCreateDialog(savedInstanceState);
|
||||
}
|
||||
|
||||
final View dialogView = View.inflate(getContext(), R.layout.dialog_playlist_name, null);
|
||||
final EditText nameInput = dialogView.findViewById(R.id.playlist_name);
|
||||
final DialogEditTextBinding dialogBinding
|
||||
= DialogEditTextBinding.inflate(getLayoutInflater());
|
||||
dialogBinding.dialogEditText.setHint(R.string.name);
|
||||
dialogBinding.dialogEditText.setInputType(InputType.TYPE_CLASS_TEXT);
|
||||
|
||||
final AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(requireContext())
|
||||
.setTitle(R.string.create_playlist)
|
||||
.setView(dialogView)
|
||||
.setView(dialogBinding.getRoot())
|
||||
.setCancelable(true)
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.setPositiveButton(R.string.create, (dialogInterface, i) -> {
|
||||
final String name = nameInput.getText().toString();
|
||||
final String name = dialogBinding.dialogEditText.getText().toString();
|
||||
final LocalPlaylistManager playlistManager =
|
||||
new LocalPlaylistManager(NewPipeDatabase.getInstance(requireContext()));
|
||||
final Toast successToast = Toast.makeText(getActivity(),
|
||||
|
||||
@@ -40,8 +40,7 @@ import androidx.core.view.isVisible
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.preference.PreferenceManager
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import com.xwray.groupie.GroupAdapter
|
||||
import com.xwray.groupie.GroupieViewHolder
|
||||
import com.xwray.groupie.GroupieAdapter
|
||||
import com.xwray.groupie.Item
|
||||
import com.xwray.groupie.OnItemClickListener
|
||||
import com.xwray.groupie.OnItemLongClickListener
|
||||
@@ -73,7 +72,7 @@ import org.schabi.newpipe.player.helper.PlayerHolder
|
||||
import org.schabi.newpipe.util.Localization
|
||||
import org.schabi.newpipe.util.NavigationHelper
|
||||
import org.schabi.newpipe.util.StreamDialogEntry
|
||||
import org.schabi.newpipe.util.ThemeHelper.getGridSpanCount
|
||||
import org.schabi.newpipe.util.ThemeHelper.getGridSpanCountStreams
|
||||
import org.schabi.newpipe.util.ThemeHelper.shouldUseGridLayout
|
||||
import java.time.OffsetDateTime
|
||||
import java.util.ArrayList
|
||||
@@ -91,11 +90,12 @@ class FeedFragment : BaseStateFragment<FeedState>() {
|
||||
private var groupName = ""
|
||||
private var oldestSubscriptionUpdate: OffsetDateTime? = null
|
||||
|
||||
private lateinit var groupAdapter: GroupAdapter<GroupieViewHolder>
|
||||
private lateinit var groupAdapter: GroupieAdapter
|
||||
@State @JvmField var showPlayedItems: Boolean = true
|
||||
|
||||
private var onSettingsChangeListener: SharedPreferences.OnSharedPreferenceChangeListener? = null
|
||||
private var updateListViewModeOnResume = false
|
||||
private var isRefreshing = false
|
||||
|
||||
init {
|
||||
setHasOptionsMenu(true)
|
||||
@@ -130,7 +130,7 @@ class FeedFragment : BaseStateFragment<FeedState>() {
|
||||
viewModel = ViewModelProvider(this, factory).get(FeedViewModel::class.java)
|
||||
viewModel.stateLiveData.observe(viewLifecycleOwner, { it?.let(::handleResult) })
|
||||
|
||||
groupAdapter = GroupAdapter<GroupieViewHolder>().apply {
|
||||
groupAdapter = GroupieAdapter().apply {
|
||||
setOnItemClickListener(listenerStreamItem)
|
||||
setOnItemLongClickListener(listenerStreamItem)
|
||||
}
|
||||
@@ -160,20 +160,12 @@ class FeedFragment : BaseStateFragment<FeedState>() {
|
||||
|
||||
fun setupListViewMode() {
|
||||
// does everything needed to setup the layouts for grid or list modes
|
||||
groupAdapter.spanCount = if (shouldUseGridLayout(context)) getGridSpanCount(context) else 1
|
||||
groupAdapter.spanCount = if (shouldUseGridLayout(context)) getGridSpanCountStreams(context) else 1
|
||||
feedBinding.itemsList.layoutManager = GridLayoutManager(requireContext(), groupAdapter.spanCount).apply {
|
||||
spanSizeLookup = groupAdapter.spanSizeLookup
|
||||
}
|
||||
}
|
||||
|
||||
override fun setUserVisibleHint(isVisibleToUser: Boolean) {
|
||||
super.setUserVisibleHint(isVisibleToUser)
|
||||
|
||||
if (!isVisibleToUser && view != null) {
|
||||
updateRelativeTimeViews()
|
||||
}
|
||||
}
|
||||
|
||||
override fun initListeners() {
|
||||
super.initListeners()
|
||||
feedBinding.refreshRootView.setOnClickListener { reloadContent() }
|
||||
@@ -213,7 +205,7 @@ class FeedFragment : BaseStateFragment<FeedState>() {
|
||||
putBoolean(getString(R.string.feed_use_dedicated_fetch_method_key), !usingDedicatedMethod)
|
||||
}
|
||||
}
|
||||
.setPositiveButton(resources.getString(R.string.finish), null)
|
||||
.setPositiveButton(resources.getString(R.string.ok), null)
|
||||
.create()
|
||||
.show()
|
||||
return true
|
||||
@@ -267,6 +259,7 @@ class FeedFragment : BaseStateFragment<FeedState>() {
|
||||
feedBinding.refreshRootView.animate(false, 0)
|
||||
feedBinding.loadingProgressText.animate(true, 200)
|
||||
feedBinding.swipeRefreshLayout.isRefreshing = true
|
||||
isRefreshing = true
|
||||
}
|
||||
|
||||
override fun hideLoading() {
|
||||
@@ -275,6 +268,7 @@ class FeedFragment : BaseStateFragment<FeedState>() {
|
||||
feedBinding.refreshRootView.animate(true, 200)
|
||||
feedBinding.loadingProgressText.animate(false, 0)
|
||||
feedBinding.swipeRefreshLayout.isRefreshing = false
|
||||
isRefreshing = false
|
||||
}
|
||||
|
||||
override fun showEmptyState() {
|
||||
@@ -301,6 +295,7 @@ class FeedFragment : BaseStateFragment<FeedState>() {
|
||||
feedBinding.refreshRootView.animate(false, 0)
|
||||
feedBinding.loadingProgressText.animate(false, 0)
|
||||
feedBinding.swipeRefreshLayout.isRefreshing = false
|
||||
isRefreshing = false
|
||||
}
|
||||
|
||||
private fun handleProgressState(progressState: FeedState.ProgressState) {
|
||||
@@ -330,9 +325,14 @@ class FeedFragment : BaseStateFragment<FeedState>() {
|
||||
if (context == null || context.resources == null || activity == null) return
|
||||
|
||||
val entries = ArrayList<StreamDialogEntry>()
|
||||
if (PlayerHolder.getType() != null) {
|
||||
if (PlayerHolder.getInstance().isPlayerOpen) {
|
||||
entries.add(StreamDialogEntry.enqueue)
|
||||
|
||||
if (PlayerHolder.getInstance().queueSize > 1) {
|
||||
entries.add(StreamDialogEntry.enqueue_next)
|
||||
}
|
||||
}
|
||||
|
||||
if (item.streamType == StreamType.AUDIO_STREAM) {
|
||||
entries.addAll(
|
||||
listOf(
|
||||
@@ -354,6 +354,20 @@ class FeedFragment : BaseStateFragment<FeedState>() {
|
||||
)
|
||||
}
|
||||
|
||||
// show "mark as watched" only when watch history is enabled
|
||||
val isWatchHistoryEnabled = PreferenceManager
|
||||
.getDefaultSharedPreferences(context)
|
||||
.getBoolean(getString(R.string.enable_watch_history_key), false)
|
||||
if (item.streamType != StreamType.AUDIO_LIVE_STREAM &&
|
||||
item.streamType != StreamType.LIVE_STREAM &&
|
||||
isWatchHistoryEnabled
|
||||
) {
|
||||
entries.add(
|
||||
StreamDialogEntry.mark_as_watched
|
||||
)
|
||||
}
|
||||
entries.add(StreamDialogEntry.show_channel_details)
|
||||
|
||||
StreamDialogEntry.setEnabledEntries(entries)
|
||||
InfoItemDialog(activity, item, StreamDialogEntry.getCommands(context)) { _, which ->
|
||||
StreamDialogEntry.clickOn(which, this, item)
|
||||
@@ -362,7 +376,7 @@ class FeedFragment : BaseStateFragment<FeedState>() {
|
||||
|
||||
private val listenerStreamItem = object : OnItemClickListener, OnItemLongClickListener {
|
||||
override fun onItemClick(item: Item<*>, view: View) {
|
||||
if (item is StreamItem) {
|
||||
if (item is StreamItem && !isRefreshing) {
|
||||
val stream = item.streamWithState.stream
|
||||
NavigationHelper.openVideoDetailFragment(
|
||||
requireContext(), fm,
|
||||
@@ -372,7 +386,7 @@ class FeedFragment : BaseStateFragment<FeedState>() {
|
||||
}
|
||||
|
||||
override fun onItemLongClick(item: Item<*>, view: View): Boolean {
|
||||
if (item is StreamItem) {
|
||||
if (item is StreamItem && !isRefreshing) {
|
||||
showStreamDialog(item.streamWithState.stream.toStreamInfoItem())
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ import android.text.TextUtils
|
||||
import android.view.View
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.preference.PreferenceManager
|
||||
import com.nostra13.universalimageloader.core.ImageLoader
|
||||
import com.xwray.groupie.viewbinding.BindableItem
|
||||
import org.schabi.newpipe.MainActivity
|
||||
import org.schabi.newpipe.R
|
||||
@@ -16,8 +15,9 @@ import org.schabi.newpipe.extractor.stream.StreamType.AUDIO_LIVE_STREAM
|
||||
import org.schabi.newpipe.extractor.stream.StreamType.AUDIO_STREAM
|
||||
import org.schabi.newpipe.extractor.stream.StreamType.LIVE_STREAM
|
||||
import org.schabi.newpipe.extractor.stream.StreamType.VIDEO_STREAM
|
||||
import org.schabi.newpipe.util.ImageDisplayConstants
|
||||
import org.schabi.newpipe.util.Localization
|
||||
import org.schabi.newpipe.util.PicassoHelper
|
||||
import org.schabi.newpipe.util.StreamTypeUtil
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
data class StreamItem(
|
||||
@@ -59,8 +59,6 @@ data class StreamItem(
|
||||
viewBinding.itemVideoTitleView.text = stream.title
|
||||
viewBinding.itemUploaderView.text = stream.uploader
|
||||
|
||||
val isLiveStream = stream.streamType == LIVE_STREAM || stream.streamType == AUDIO_LIVE_STREAM
|
||||
|
||||
if (stream.duration > 0) {
|
||||
viewBinding.itemDurationView.text = Localization.getDurationString(stream.duration)
|
||||
viewBinding.itemDurationView.setBackgroundColor(
|
||||
@@ -78,7 +76,7 @@ data class StreamItem(
|
||||
} else {
|
||||
viewBinding.itemProgressView.visibility = View.GONE
|
||||
}
|
||||
} else if (isLiveStream) {
|
||||
} else if (StreamTypeUtil.isLiveStream(stream.streamType)) {
|
||||
viewBinding.itemDurationView.setText(R.string.duration_live)
|
||||
viewBinding.itemDurationView.setBackgroundColor(
|
||||
ContextCompat.getColor(
|
||||
@@ -93,10 +91,7 @@ data class StreamItem(
|
||||
viewBinding.itemProgressView.visibility = View.GONE
|
||||
}
|
||||
|
||||
ImageLoader.getInstance().displayImage(
|
||||
stream.thumbnailUrl, viewBinding.itemThumbnailView,
|
||||
ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS
|
||||
)
|
||||
PicassoHelper.loadThumbnail(stream.thumbnailUrl).into(viewBinding.itemThumbnailView)
|
||||
|
||||
if (itemVersion != ItemVersion.MINI) {
|
||||
viewBinding.itemAdditionalDetails.text =
|
||||
|
||||
@@ -300,6 +300,12 @@ class FeedLoadService : Service() {
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe { _, throwable ->
|
||||
// There seems to be a bug in the kotlin plugin as it tells you when
|
||||
// building that this can't be null:
|
||||
// "Condition 'throwable != null' is always 'true'"
|
||||
// However it can indeed be null
|
||||
// The suppression may be removed in further versions
|
||||
@Suppress("SENSELESS_COMPARISON")
|
||||
if (throwable != null) {
|
||||
Log.e(TAG, "Error while storing result", throwable)
|
||||
handleError(throwable)
|
||||
|
||||
@@ -28,6 +28,7 @@ import org.schabi.newpipe.NewPipeDatabase;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.database.AppDatabase;
|
||||
import org.schabi.newpipe.database.LocalItem;
|
||||
import org.schabi.newpipe.database.feed.dao.FeedDAO;
|
||||
import org.schabi.newpipe.database.history.dao.SearchHistoryDAO;
|
||||
import org.schabi.newpipe.database.history.dao.StreamHistoryDAO;
|
||||
import org.schabi.newpipe.database.history.model.SearchHistoryEntry;
|
||||
@@ -42,7 +43,10 @@ import org.schabi.newpipe.database.stream.model.StreamEntity;
|
||||
import org.schabi.newpipe.database.stream.model.StreamStateEntity;
|
||||
import org.schabi.newpipe.extractor.InfoItem;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||
import org.schabi.newpipe.local.feed.FeedViewModel;
|
||||
import org.schabi.newpipe.player.playqueue.PlayQueueItem;
|
||||
import org.schabi.newpipe.util.ExtractorHelper;
|
||||
|
||||
import java.time.OffsetDateTime;
|
||||
import java.time.ZoneOffset;
|
||||
@@ -81,6 +85,68 @@ public class HistoryRecordManager {
|
||||
// Watch History
|
||||
///////////////////////////////////////////////////////
|
||||
|
||||
/**
|
||||
* Marks a stream item as watched such that it is hidden from the feed if watched videos are
|
||||
* hidden. Adds a history entry and updates the stream progress to 100%.
|
||||
*
|
||||
* @see FeedDAO#getLiveOrNotPlayedStreams
|
||||
* @see FeedViewModel#togglePlayedItems
|
||||
* @param info the item to mark as watched
|
||||
* @return a Maybe containing the ID of the item if successful
|
||||
*/
|
||||
public Maybe<Long> markAsWatched(final StreamInfoItem info) {
|
||||
if (!isStreamHistoryEnabled()) {
|
||||
return Maybe.empty();
|
||||
}
|
||||
|
||||
final OffsetDateTime currentTime = OffsetDateTime.now(ZoneOffset.UTC);
|
||||
return Maybe.fromCallable(() -> database.runInTransaction(() -> {
|
||||
final long streamId;
|
||||
final long duration;
|
||||
// Duration will not exist if the item was loaded with fast mode, so fetch it if empty
|
||||
if (info.getDuration() < 0) {
|
||||
final StreamInfo completeInfo = ExtractorHelper.getStreamInfo(
|
||||
info.getServiceId(),
|
||||
info.getUrl(),
|
||||
false
|
||||
)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.blockingGet();
|
||||
duration = completeInfo.getDuration();
|
||||
streamId = streamTable.upsert(new StreamEntity(completeInfo));
|
||||
} else {
|
||||
duration = info.getDuration();
|
||||
streamId = streamTable.upsert(new StreamEntity(info));
|
||||
}
|
||||
|
||||
// Update the stream progress to the full duration of the video
|
||||
final List<StreamStateEntity> states = streamStateTable.getState(streamId)
|
||||
.blockingFirst();
|
||||
if (!states.isEmpty()) {
|
||||
final StreamStateEntity entity = states.get(0);
|
||||
entity.setProgressMillis(duration * 1000);
|
||||
streamStateTable.update(entity);
|
||||
} else {
|
||||
final StreamStateEntity entity = new StreamStateEntity(
|
||||
streamId,
|
||||
duration * 1000
|
||||
);
|
||||
streamStateTable.insert(entity);
|
||||
}
|
||||
|
||||
// Add a history entry
|
||||
final StreamHistoryEntity latestEntry = streamHistoryTable.getLatestEntry(streamId);
|
||||
if (latestEntry != null) {
|
||||
streamHistoryTable.delete(latestEntry);
|
||||
latestEntry.setAccessDate(currentTime);
|
||||
latestEntry.setRepeatCount(latestEntry.getRepeatCount() + 1);
|
||||
return streamHistoryTable.insert(latestEntry);
|
||||
} else {
|
||||
return streamHistoryTable.insert(new StreamHistoryEntity(streamId, currentTime));
|
||||
}
|
||||
})).subscribeOn(Schedulers.io());
|
||||
}
|
||||
|
||||
public Maybe<Long> onViewed(final StreamInfo info) {
|
||||
if (!isStreamHistoryEnabled()) {
|
||||
return Maybe.empty();
|
||||
|
||||
@@ -53,8 +53,6 @@ import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
||||
import io.reactivex.rxjava3.disposables.Disposable;
|
||||
|
||||
import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty;
|
||||
|
||||
public class StatisticsPlaylistFragment
|
||||
extends BaseLocalListFragment<List<StreamStatisticsEntry>, Void> {
|
||||
private final CompositeDisposable disposables = new CompositeDisposable();
|
||||
@@ -340,9 +338,14 @@ public class StatisticsPlaylistFragment
|
||||
|
||||
final ArrayList<StreamDialogEntry> entries = new ArrayList<>();
|
||||
|
||||
if (PlayerHolder.getType() != null) {
|
||||
if (PlayerHolder.getInstance().isPlayerOpen()) {
|
||||
entries.add(StreamDialogEntry.enqueue);
|
||||
|
||||
if (PlayerHolder.getInstance().getQueueSize() > 1) {
|
||||
entries.add(StreamDialogEntry.enqueue_next);
|
||||
}
|
||||
}
|
||||
|
||||
if (infoItem.getStreamType() == StreamType.AUDIO_STREAM) {
|
||||
entries.addAll(Arrays.asList(
|
||||
StreamDialogEntry.start_here_on_background,
|
||||
@@ -363,10 +366,7 @@ public class StatisticsPlaylistFragment
|
||||
if (KoreUtils.shouldShowPlayWithKodi(context, infoItem.getServiceId())) {
|
||||
entries.add(StreamDialogEntry.play_with_kodi);
|
||||
}
|
||||
|
||||
if (!isNullOrEmpty(infoItem.getUploaderUrl())) {
|
||||
entries.add(StreamDialogEntry.show_channel_details);
|
||||
}
|
||||
entries.add(StreamDialogEntry.show_channel_details);
|
||||
|
||||
StreamDialogEntry.setEnabledEntries(entries);
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import org.schabi.newpipe.database.LocalItem;
|
||||
import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry;
|
||||
import org.schabi.newpipe.local.LocalItemBuilder;
|
||||
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
||||
import org.schabi.newpipe.util.ImageDisplayConstants;
|
||||
import org.schabi.newpipe.util.PicassoHelper;
|
||||
import org.schabi.newpipe.util.Localization;
|
||||
|
||||
import java.time.format.DateTimeFormatter;
|
||||
@@ -36,8 +36,7 @@ public class LocalPlaylistItemHolder extends PlaylistItemHolder {
|
||||
itemStreamCountView.getContext(), item.streamCount));
|
||||
itemUploaderView.setVisibility(View.INVISIBLE);
|
||||
|
||||
itemBuilder.displayImage(item.thumbnailUrl, itemThumbnailView,
|
||||
ImageDisplayConstants.DISPLAY_PLAYLIST_OPTIONS);
|
||||
PicassoHelper.loadPlaylistThumbnail(item.thumbnailUrl).into(itemThumbnailView);
|
||||
|
||||
super.updateFromItem(localItem, historyRecordManager, dateTimeFormatter);
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ import org.schabi.newpipe.extractor.NewPipe;
|
||||
import org.schabi.newpipe.ktx.ViewUtils;
|
||||
import org.schabi.newpipe.local.LocalItemBuilder;
|
||||
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
||||
import org.schabi.newpipe.util.ImageDisplayConstants;
|
||||
import org.schabi.newpipe.util.PicassoHelper;
|
||||
import org.schabi.newpipe.util.Localization;
|
||||
import org.schabi.newpipe.views.AnimatedProgressBar;
|
||||
|
||||
@@ -81,8 +81,8 @@ public class LocalPlaylistStreamItemHolder extends LocalItemHolder {
|
||||
}
|
||||
|
||||
// Default thumbnail is shown on error, while loading and if the url is empty
|
||||
itemBuilder.displayImage(item.getStreamEntity().getThumbnailUrl(), itemThumbnailView,
|
||||
ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS);
|
||||
PicassoHelper.loadThumbnail(item.getStreamEntity().getThumbnailUrl())
|
||||
.into(itemThumbnailView);
|
||||
|
||||
itemView.setOnClickListener(view -> {
|
||||
if (itemBuilder.getOnItemSelectedListener() != null) {
|
||||
|
||||
@@ -15,7 +15,7 @@ import org.schabi.newpipe.extractor.NewPipe;
|
||||
import org.schabi.newpipe.ktx.ViewUtils;
|
||||
import org.schabi.newpipe.local.LocalItemBuilder;
|
||||
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
||||
import org.schabi.newpipe.util.ImageDisplayConstants;
|
||||
import org.schabi.newpipe.util.PicassoHelper;
|
||||
import org.schabi.newpipe.util.Localization;
|
||||
import org.schabi.newpipe.views.AnimatedProgressBar;
|
||||
|
||||
@@ -114,8 +114,8 @@ public class LocalStatisticStreamItemHolder extends LocalItemHolder {
|
||||
}
|
||||
|
||||
// Default thumbnail is shown on error, while loading and if the url is empty
|
||||
itemBuilder.displayImage(item.getStreamEntity().getThumbnailUrl(), itemThumbnailView,
|
||||
ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS);
|
||||
PicassoHelper.loadThumbnail(item.getStreamEntity().getThumbnailUrl())
|
||||
.into(itemThumbnailView);
|
||||
|
||||
itemView.setOnClickListener(view -> {
|
||||
if (itemBuilder.getOnItemSelectedListener() != null) {
|
||||
|
||||
@@ -8,7 +8,7 @@ import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity;
|
||||
import org.schabi.newpipe.extractor.NewPipe;
|
||||
import org.schabi.newpipe.local.LocalItemBuilder;
|
||||
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
||||
import org.schabi.newpipe.util.ImageDisplayConstants;
|
||||
import org.schabi.newpipe.util.PicassoHelper;
|
||||
import org.schabi.newpipe.util.Localization;
|
||||
|
||||
import java.time.format.DateTimeFormatter;
|
||||
@@ -44,9 +44,7 @@ public class RemotePlaylistItemHolder extends PlaylistItemHolder {
|
||||
itemUploaderView.setText(NewPipe.getNameOfService(item.getServiceId()));
|
||||
}
|
||||
|
||||
|
||||
itemBuilder.displayImage(item.getThumbnailUrl(), itemThumbnailView,
|
||||
ImageDisplayConstants.DISPLAY_PLAYLIST_OPTIONS);
|
||||
PicassoHelper.loadPlaylistThumbnail(item.getThumbnailUrl()).into(itemThumbnailView);
|
||||
|
||||
super.updateFromItem(localItem, historyRecordManager, dateTimeFormatter);
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.os.Bundle;
|
||||
import android.os.Parcelable;
|
||||
import android.text.InputType;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
@@ -13,7 +14,6 @@ import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.EditText;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
@@ -32,6 +32,7 @@ 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.DialogEditTextBinding;
|
||||
import org.schabi.newpipe.databinding.LocalPlaylistHeaderBinding;
|
||||
import org.schabi.newpipe.databinding.PlaylistControlBinding;
|
||||
import org.schabi.newpipe.error.ErrorInfo;
|
||||
@@ -41,6 +42,7 @@ import org.schabi.newpipe.extractor.stream.StreamType;
|
||||
import org.schabi.newpipe.info_list.InfoItemDialog;
|
||||
import org.schabi.newpipe.local.BaseLocalListFragment;
|
||||
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
||||
import org.schabi.newpipe.player.MainPlayer.PlayerType;
|
||||
import org.schabi.newpipe.player.helper.PlayerHolder;
|
||||
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
||||
import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
|
||||
@@ -66,7 +68,6 @@ import io.reactivex.rxjava3.disposables.Disposable;
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers;
|
||||
import io.reactivex.rxjava3.subjects.PublishSubject;
|
||||
|
||||
import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty;
|
||||
import static org.schabi.newpipe.ktx.ViewUtils.animate;
|
||||
import static org.schabi.newpipe.util.ThemeHelper.shouldUseGridLayout;
|
||||
|
||||
@@ -493,12 +494,12 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
|
||||
NavigationHelper.playOnBackgroundPlayer(activity, getPlayQueue(), false));
|
||||
|
||||
playlistControlBinding.playlistCtrlPlayPopupButton.setOnLongClickListener(view -> {
|
||||
NavigationHelper.enqueueOnPopupPlayer(activity, getPlayQueue(), true);
|
||||
NavigationHelper.enqueueOnPlayer(activity, getPlayQueue(), PlayerType.POPUP);
|
||||
return true;
|
||||
});
|
||||
|
||||
playlistControlBinding.playlistCtrlPlayBgButton.setOnLongClickListener(view -> {
|
||||
NavigationHelper.enqueueOnBackgroundPlayer(activity, getPlayQueue(), true);
|
||||
NavigationHelper.enqueueOnPlayer(activity, getPlayQueue(), PlayerType.AUDIO);
|
||||
return true;
|
||||
});
|
||||
|
||||
@@ -526,18 +527,20 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
|
||||
return;
|
||||
}
|
||||
|
||||
final View dialogView = View.inflate(getContext(), R.layout.dialog_playlist_name, null);
|
||||
final EditText nameEdit = dialogView.findViewById(R.id.playlist_name);
|
||||
nameEdit.setText(name);
|
||||
nameEdit.setSelection(nameEdit.getText().length());
|
||||
final DialogEditTextBinding dialogBinding
|
||||
= DialogEditTextBinding.inflate(getLayoutInflater());
|
||||
dialogBinding.dialogEditText.setHint(R.string.name);
|
||||
dialogBinding.dialogEditText.setInputType(InputType.TYPE_CLASS_TEXT);
|
||||
dialogBinding.dialogEditText.setSelection(dialogBinding.dialogEditText.getText().length());
|
||||
dialogBinding.dialogEditText.setText(name);
|
||||
|
||||
final AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(getContext())
|
||||
.setTitle(R.string.rename_playlist)
|
||||
.setView(dialogView)
|
||||
.setView(dialogBinding.getRoot())
|
||||
.setCancelable(true)
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.setPositiveButton(R.string.rename, (dialogInterface, i) ->
|
||||
changePlaylistName(nameEdit.getText().toString()));
|
||||
changePlaylistName(dialogBinding.dialogEditText.getText().toString()));
|
||||
|
||||
dialogBuilder.show();
|
||||
}
|
||||
@@ -750,8 +753,12 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
|
||||
|
||||
final ArrayList<StreamDialogEntry> entries = new ArrayList<>();
|
||||
|
||||
if (PlayerHolder.getType() != null) {
|
||||
if (PlayerHolder.getInstance().isPlayerOpen()) {
|
||||
entries.add(StreamDialogEntry.enqueue);
|
||||
|
||||
if (PlayerHolder.getInstance().getQueueSize() > 1) {
|
||||
entries.add(StreamDialogEntry.enqueue_next);
|
||||
}
|
||||
}
|
||||
if (infoItem.getStreamType() == StreamType.AUDIO_STREAM) {
|
||||
entries.addAll(Arrays.asList(
|
||||
@@ -775,10 +782,7 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
|
||||
if (KoreUtils.shouldShowPlayWithKodi(context, infoItem.getServiceId())) {
|
||||
entries.add(StreamDialogEntry.play_with_kodi);
|
||||
}
|
||||
|
||||
if (!isNullOrEmpty(infoItem.getUploaderUrl())) {
|
||||
entries.add(StreamDialogEntry.show_channel_details);
|
||||
}
|
||||
entries.add(StreamDialogEntry.show_channel_details);
|
||||
|
||||
StreamDialogEntry.setEnabledEntries(entries);
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ enum class FeedGroupIcon(
|
||||
COMPUTER(5, R.drawable.ic_computer),
|
||||
GAMING(6, R.drawable.ic_videogame_asset),
|
||||
SPORTS(7, R.drawable.ic_directions_bike),
|
||||
NEWS(8, R.drawable.ic_megaphone),
|
||||
NEWS(8, R.drawable.ic_campaign),
|
||||
FAVORITES(9, R.drawable.ic_favorite),
|
||||
CAR(10, R.drawable.ic_directions_car),
|
||||
MOTORCYCLE(11, R.drawable.ic_motorcycle),
|
||||
|
||||
@@ -40,7 +40,7 @@ public class ImportConfirmationDialog extends DialogFragment {
|
||||
.setMessage(R.string.import_network_expensive_warning)
|
||||
.setCancelable(true)
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.setPositiveButton(R.string.finish, (dialogInterface, i) -> {
|
||||
.setPositiveButton(R.string.ok, (dialogInterface, i) -> {
|
||||
if (resultServiceIntent != null && getContext() != null) {
|
||||
getContext().startService(resultServiceIntent);
|
||||
}
|
||||
|
||||
@@ -58,7 +58,7 @@ import org.schabi.newpipe.local.subscription.services.SubscriptionsImportService
|
||||
import org.schabi.newpipe.streams.io.StoredFileHelper
|
||||
import org.schabi.newpipe.util.NavigationHelper
|
||||
import org.schabi.newpipe.util.OnClickGesture
|
||||
import org.schabi.newpipe.util.ThemeHelper.getGridSpanCount
|
||||
import org.schabi.newpipe.util.ThemeHelper.getGridSpanCountChannels
|
||||
import org.schabi.newpipe.util.ThemeHelper.shouldUseGridLayout
|
||||
import org.schabi.newpipe.util.external_communication.ShareUtils
|
||||
import java.text.SimpleDateFormat
|
||||
@@ -110,13 +110,6 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
|
||||
setupInitialLayout()
|
||||
}
|
||||
|
||||
override fun setUserVisibleHint(isVisibleToUser: Boolean) {
|
||||
super.setUserVisibleHint(isVisibleToUser)
|
||||
if (activity != null && isVisibleToUser) {
|
||||
setTitle(activity.getString(R.string.tab_subscriptions))
|
||||
}
|
||||
}
|
||||
|
||||
override fun onAttach(context: Context) {
|
||||
super.onAttach(context)
|
||||
subscriptionManager = SubscriptionManager(requireContext())
|
||||
@@ -154,11 +147,8 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
|
||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||
super.onCreateOptionsMenu(menu, inflater)
|
||||
|
||||
val supportActionBar = activity.supportActionBar
|
||||
if (supportActionBar != null) {
|
||||
supportActionBar.setDisplayShowTitleEnabled(true)
|
||||
setTitle(getString(R.string.tab_subscriptions))
|
||||
}
|
||||
activity.supportActionBar?.setDisplayShowTitleEnabled(true)
|
||||
activity.supportActionBar?.setTitle(R.string.tab_subscriptions)
|
||||
}
|
||||
|
||||
private fun setupBroadcastReceiver() {
|
||||
@@ -189,7 +179,7 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
|
||||
}
|
||||
|
||||
private fun onImportPreviousSelected() {
|
||||
requestImportLauncher.launch(StoredFileHelper.getPicker(activity))
|
||||
requestImportLauncher.launch(StoredFileHelper.getPicker(activity, JSON_MIME_TYPE))
|
||||
}
|
||||
|
||||
private fun onExportSelected() {
|
||||
@@ -197,7 +187,7 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
|
||||
val exportName = "newpipe_subscriptions_$date.json"
|
||||
|
||||
requestExportLauncher.launch(
|
||||
StoredFileHelper.getNewPicker(activity, exportName, "application/json", null)
|
||||
StoredFileHelper.getNewPicker(activity, exportName, JSON_MIME_TYPE, null)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -205,7 +195,7 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
|
||||
FeedGroupReorderDialog().show(parentFragmentManager, null)
|
||||
}
|
||||
|
||||
fun requestExportResult(result: ActivityResult) {
|
||||
private fun requestExportResult(result: ActivityResult) {
|
||||
if (result.data != null && result.resultCode == Activity.RESULT_OK) {
|
||||
activity.startService(
|
||||
Intent(activity, SubscriptionsExportService::class.java)
|
||||
@@ -214,7 +204,7 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
|
||||
}
|
||||
}
|
||||
|
||||
fun requestImportResult(result: ActivityResult) {
|
||||
private fun requestImportResult(result: ActivityResult) {
|
||||
if (result.data != null && result.resultCode == Activity.RESULT_OK) {
|
||||
ImportConfirmationDialog.show(
|
||||
this,
|
||||
@@ -277,7 +267,7 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
|
||||
super.initViews(rootView, savedInstanceState)
|
||||
_binding = FragmentSubscriptionBinding.bind(rootView)
|
||||
|
||||
groupAdapter.spanCount = if (shouldUseGridLayout(context)) getGridSpanCount(context) else 1
|
||||
groupAdapter.spanCount = if (shouldUseGridLayout(context)) getGridSpanCountChannels(context) else 1
|
||||
binding.itemsList.layoutManager = GridLayoutManager(requireContext(), groupAdapter.spanCount).apply {
|
||||
spanSizeLookup = groupAdapter.spanSizeLookup
|
||||
}
|
||||
@@ -417,4 +407,8 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
|
||||
super.hideLoading()
|
||||
binding.itemsList.animate(true, 200)
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val JSON_MIME_TYPE = "application/json"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -177,7 +177,8 @@ public class SubscriptionsImportFragment extends BaseFragment {
|
||||
}
|
||||
|
||||
public void onImportFile() {
|
||||
requestImportFileLauncher.launch(StoredFileHelper.getPicker(activity));
|
||||
// leave */* mime type to support all services with different mime types and file extensions
|
||||
requestImportFileLauncher.launch(StoredFileHelper.getPicker(activity, "*/*"));
|
||||
}
|
||||
|
||||
private void requestImportFileResult(final ActivityResult result) {
|
||||
|
||||
@@ -21,8 +21,7 @@ import androidx.lifecycle.Observer
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.xwray.groupie.GroupAdapter
|
||||
import com.xwray.groupie.GroupieViewHolder
|
||||
import com.xwray.groupie.GroupieAdapter
|
||||
import com.xwray.groupie.OnItemClickListener
|
||||
import com.xwray.groupie.Section
|
||||
import icepick.Icepick
|
||||
@@ -78,7 +77,7 @@ class FeedGroupDialog : DialogFragment(), BackPressable {
|
||||
|
||||
private val subscriptionMainSection = Section()
|
||||
private val subscriptionEmptyFooter = Section()
|
||||
private lateinit var subscriptionGroupAdapter: GroupAdapter<GroupieViewHolder>
|
||||
private lateinit var subscriptionGroupAdapter: GroupieAdapter
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
@@ -143,23 +142,17 @@ class FeedGroupDialog : DialogFragment(), BackPressable {
|
||||
).get(FeedGroupDialogViewModel::class.java)
|
||||
|
||||
viewModel.groupLiveData.observe(viewLifecycleOwner, Observer(::handleGroup))
|
||||
viewModel.subscriptionsLiveData.observe(
|
||||
viewLifecycleOwner,
|
||||
Observer {
|
||||
setupSubscriptionPicker(it.first, it.second)
|
||||
viewModel.subscriptionsLiveData.observe(viewLifecycleOwner) {
|
||||
setupSubscriptionPicker(it.first, it.second)
|
||||
}
|
||||
viewModel.dialogEventLiveData.observe(viewLifecycleOwner) {
|
||||
when (it) {
|
||||
ProcessingEvent -> disableInput()
|
||||
SuccessEvent -> dismiss()
|
||||
}
|
||||
)
|
||||
viewModel.dialogEventLiveData.observe(
|
||||
viewLifecycleOwner,
|
||||
Observer {
|
||||
when (it) {
|
||||
ProcessingEvent -> disableInput()
|
||||
SuccessEvent -> dismiss()
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
subscriptionGroupAdapter = GroupAdapter<GroupieViewHolder>().apply {
|
||||
subscriptionGroupAdapter = GroupieAdapter().apply {
|
||||
add(subscriptionMainSection)
|
||||
add(subscriptionEmptyFooter)
|
||||
spanCount = 4
|
||||
@@ -385,7 +378,7 @@ class FeedGroupDialog : DialogFragment(), BackPressable {
|
||||
}
|
||||
|
||||
private fun setupIconPicker() {
|
||||
val groupAdapter = GroupAdapter<GroupieViewHolder>()
|
||||
val groupAdapter = GroupieAdapter()
|
||||
groupAdapter.addAll(FeedGroupIcon.values().map { PickerIconItem(it) })
|
||||
|
||||
feedGroupCreateBinding.iconSelector.apply {
|
||||
@@ -437,7 +430,7 @@ class FeedGroupDialog : DialogFragment(), BackPressable {
|
||||
feedGroupCreateBinding.confirmButton.setText(
|
||||
when {
|
||||
currentScreen == InitialScreen && groupId == NO_GROUP_SELECTED -> R.string.create
|
||||
else -> android.R.string.ok
|
||||
else -> R.string.ok
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -11,8 +11,7 @@ import androidx.recyclerview.widget.ItemTouchHelper
|
||||
import androidx.recyclerview.widget.ItemTouchHelper.SimpleCallback
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.xwray.groupie.GroupAdapter
|
||||
import com.xwray.groupie.GroupieViewHolder
|
||||
import com.xwray.groupie.GroupieAdapter
|
||||
import com.xwray.groupie.TouchCallback
|
||||
import icepick.Icepick
|
||||
import icepick.State
|
||||
@@ -38,7 +37,7 @@ class FeedGroupReorderDialog : DialogFragment() {
|
||||
@State
|
||||
@JvmField
|
||||
var groupOrderedIdList = ArrayList<Long>()
|
||||
private val groupAdapter = GroupAdapter<GroupieViewHolder>()
|
||||
private val groupAdapter = GroupieAdapter()
|
||||
private val itemTouchHelper = ItemTouchHelper(getItemTouchCallback())
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
|
||||
@@ -3,14 +3,13 @@ package org.schabi.newpipe.local.subscription.item
|
||||
import android.content.Context
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import com.nostra13.universalimageloader.core.ImageLoader
|
||||
import com.xwray.groupie.GroupieViewHolder
|
||||
import com.xwray.groupie.Item
|
||||
import org.schabi.newpipe.R
|
||||
import org.schabi.newpipe.extractor.channel.ChannelInfoItem
|
||||
import org.schabi.newpipe.util.ImageDisplayConstants
|
||||
import org.schabi.newpipe.util.Localization
|
||||
import org.schabi.newpipe.util.OnClickGesture
|
||||
import org.schabi.newpipe.util.PicassoHelper
|
||||
|
||||
class ChannelItem(
|
||||
private val infoItem: ChannelInfoItem,
|
||||
@@ -40,10 +39,7 @@ class ChannelItem(
|
||||
itemChannelDescriptionView.text = infoItem.description
|
||||
}
|
||||
|
||||
ImageLoader.getInstance().displayImage(
|
||||
infoItem.thumbnailUrl, itemThumbnailView,
|
||||
ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS
|
||||
)
|
||||
PicassoHelper.loadThumbnail(infoItem.thumbnailUrl).into(itemThumbnailView)
|
||||
|
||||
gesturesListener?.run {
|
||||
viewHolder.root.setOnClickListener { selected(infoItem) }
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
package org.schabi.newpipe.local.subscription.item
|
||||
|
||||
import android.view.View
|
||||
import android.view.View.OnClickListener
|
||||
import com.xwray.groupie.viewbinding.BindableItem
|
||||
import org.schabi.newpipe.R
|
||||
import org.schabi.newpipe.databinding.HeaderItemBinding
|
||||
|
||||
class HeaderItem(
|
||||
val title: String,
|
||||
private val onClickListener: (() -> Unit)? = null
|
||||
) : BindableItem<HeaderItemBinding>() {
|
||||
override fun getLayout(): Int = R.layout.header_item
|
||||
|
||||
override fun bind(viewBinding: HeaderItemBinding, position: Int) {
|
||||
viewBinding.headerTitle.text = title
|
||||
|
||||
val listener = onClickListener?.let { OnClickListener { onClickListener.invoke() } }
|
||||
viewBinding.root.setOnClickListener(listener)
|
||||
}
|
||||
|
||||
override fun initializeViewBinding(view: View) = HeaderItemBinding.bind(view)
|
||||
}
|
||||
@@ -3,7 +3,6 @@ package org.schabi.newpipe.local.subscription.item
|
||||
import android.view.View
|
||||
import androidx.core.view.isGone
|
||||
import androidx.core.view.isVisible
|
||||
import com.nostra13.universalimageloader.core.ImageLoader
|
||||
import com.xwray.groupie.viewbinding.BindableItem
|
||||
import com.xwray.groupie.viewbinding.GroupieViewHolder
|
||||
import org.schabi.newpipe.R
|
||||
@@ -11,7 +10,7 @@ import org.schabi.newpipe.database.subscription.SubscriptionEntity
|
||||
import org.schabi.newpipe.databinding.PickerSubscriptionItemBinding
|
||||
import org.schabi.newpipe.ktx.AnimationType
|
||||
import org.schabi.newpipe.ktx.animate
|
||||
import org.schabi.newpipe.util.ImageDisplayConstants
|
||||
import org.schabi.newpipe.util.PicassoHelper
|
||||
|
||||
data class PickerSubscriptionItem(
|
||||
val subscriptionEntity: SubscriptionEntity,
|
||||
@@ -22,11 +21,7 @@ data class PickerSubscriptionItem(
|
||||
override fun getSpanSize(spanCount: Int, position: Int): Int = 1
|
||||
|
||||
override fun bind(viewBinding: PickerSubscriptionItemBinding, position: Int) {
|
||||
ImageLoader.getInstance().displayImage(
|
||||
subscriptionEntity.avatarUrl,
|
||||
viewBinding.thumbnailView, ImageDisplayConstants.DISPLAY_AVATAR_OPTIONS
|
||||
)
|
||||
|
||||
PicassoHelper.loadAvatar(subscriptionEntity.avatarUrl).into(viewBinding.thumbnailView)
|
||||
viewBinding.titleView.text = subscriptionEntity.name
|
||||
viewBinding.selectedHighlight.isVisible = isSelected
|
||||
}
|
||||
|
||||
@@ -19,6 +19,9 @@
|
||||
|
||||
package org.schabi.newpipe.local.subscription.services;
|
||||
|
||||
import static org.schabi.newpipe.MainActivity.DEBUG;
|
||||
import static org.schabi.newpipe.streams.io.StoredFileHelper.DEFAULT_MIME;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.text.TextUtils;
|
||||
@@ -46,6 +49,7 @@ import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.rxjava3.core.Flowable;
|
||||
@@ -54,9 +58,6 @@ import io.reactivex.rxjava3.functions.Consumer;
|
||||
import io.reactivex.rxjava3.functions.Function;
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers;
|
||||
|
||||
import static org.schabi.newpipe.MainActivity.DEBUG;
|
||||
import static org.schabi.newpipe.streams.io.StoredFileHelper.DEFAULT_MIME;
|
||||
|
||||
public class SubscriptionsImportService extends BaseImportExportService {
|
||||
public static final int CHANNEL_URL_MODE = 0;
|
||||
public static final int INPUT_STREAM_MODE = 1;
|
||||
@@ -89,6 +90,8 @@ public class SubscriptionsImportService extends BaseImportExportService {
|
||||
private String channelUrl;
|
||||
@Nullable
|
||||
private InputStream inputStream;
|
||||
@Nullable
|
||||
private String inputStreamType;
|
||||
|
||||
@Override
|
||||
public int onStartCommand(final Intent intent, final int flags, final int startId) {
|
||||
@@ -111,8 +114,20 @@ public class SubscriptionsImportService extends BaseImportExportService {
|
||||
}
|
||||
|
||||
try {
|
||||
inputStream = new SharpInputStream(
|
||||
new StoredFileHelper(this, uri, DEFAULT_MIME).getStream());
|
||||
final StoredFileHelper fileHelper = new StoredFileHelper(this, uri, DEFAULT_MIME);
|
||||
inputStream = new SharpInputStream(fileHelper.getStream());
|
||||
inputStreamType = fileHelper.getType();
|
||||
|
||||
if (inputStreamType == null || inputStreamType.equals(DEFAULT_MIME)) {
|
||||
// mime type could not be determined, just take file extension
|
||||
final String name = fileHelper.getName();
|
||||
final int pointIndex = name.lastIndexOf('.');
|
||||
if (pointIndex == -1 || pointIndex >= name.length() - 1) {
|
||||
inputStreamType = DEFAULT_MIME; // no extension, will fail in the extractor
|
||||
} else {
|
||||
inputStreamType = name.substring(pointIndex + 1);
|
||||
}
|
||||
}
|
||||
} catch (final IOException e) {
|
||||
handleError(e);
|
||||
return START_NOT_STICKY;
|
||||
@@ -248,9 +263,9 @@ public class SubscriptionsImportService extends BaseImportExportService {
|
||||
final Throwable error = notification.getError();
|
||||
final Throwable cause = error.getCause();
|
||||
if (error instanceof IOException) {
|
||||
throw (IOException) error;
|
||||
throw error;
|
||||
} else if (cause instanceof IOException) {
|
||||
throw (IOException) cause;
|
||||
throw cause;
|
||||
} else if (ExceptionUtils.isNetworkRelated(error)) {
|
||||
throw new IOException(error);
|
||||
}
|
||||
@@ -280,9 +295,12 @@ public class SubscriptionsImportService extends BaseImportExportService {
|
||||
}
|
||||
|
||||
private Flowable<List<SubscriptionItem>> importFromInputStream() {
|
||||
Objects.requireNonNull(inputStream);
|
||||
Objects.requireNonNull(inputStreamType);
|
||||
|
||||
return Flowable.fromCallable(() -> NewPipe.getService(currentServiceId)
|
||||
.getSubscriptionExtractor()
|
||||
.fromInputStream(inputStream));
|
||||
.fromInputStream(inputStream, inputStreamType));
|
||||
}
|
||||
|
||||
private Flowable<List<SubscriptionItem>> importFromPreviousExport() {
|
||||
|
||||
@@ -24,7 +24,6 @@ import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Binder;
|
||||
import android.os.IBinder;
|
||||
import android.util.DisplayMetrics;
|
||||
import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
@@ -36,6 +35,7 @@ import androidx.core.content.ContextCompat;
|
||||
|
||||
import org.schabi.newpipe.App;
|
||||
import org.schabi.newpipe.databinding.PlayerBinding;
|
||||
import org.schabi.newpipe.util.DeviceUtils;
|
||||
import org.schabi.newpipe.util.ThemeHelper;
|
||||
|
||||
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
|
||||
@@ -133,32 +133,29 @@ public final class MainPlayer extends Service {
|
||||
return START_NOT_STICKY;
|
||||
}
|
||||
|
||||
public void stop(final boolean autoplayEnabled) {
|
||||
public void stopForImmediateReusing() {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "stop() called");
|
||||
Log.d(TAG, "stopForImmediateReusing() called");
|
||||
}
|
||||
|
||||
if (!player.exoPlayerIsNull()) {
|
||||
player.saveWasPlaying();
|
||||
|
||||
// Releases wifi & cpu, disables keepScreenOn, etc.
|
||||
if (!autoplayEnabled) {
|
||||
player.pause();
|
||||
}
|
||||
// We can't just pause the player here because it will make transition
|
||||
// from one stream to a new stream not smooth
|
||||
player.smoothStopPlayer();
|
||||
player.setRecovery();
|
||||
|
||||
// Android TV will handle back button in case controls will be visible
|
||||
// (one more additional unneeded click while the player is hidden)
|
||||
player.hideControls(0, 0);
|
||||
player.closeItemsList();
|
||||
|
||||
// Notification shows information about old stream but if a user selects
|
||||
// a stream from backStack it's not actual anymore
|
||||
// So we should hide the notification at all.
|
||||
// When autoplay enabled such notification flashing is annoying so skip this case
|
||||
if (!autoplayEnabled) {
|
||||
NotificationUtil.getInstance().cancelNotificationAndStopForeground(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -178,7 +175,10 @@ public final class MainPlayer extends Service {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "destroy() called");
|
||||
}
|
||||
cleanup();
|
||||
}
|
||||
|
||||
private void cleanup() {
|
||||
if (player != null) {
|
||||
// Exit from fullscreen when user closes the player via notification
|
||||
if (player.isFullscreen()) {
|
||||
@@ -191,9 +191,14 @@ public final class MainPlayer extends Service {
|
||||
player.stopActivityBinding();
|
||||
player.removePopupFromView();
|
||||
player.destroy();
|
||||
}
|
||||
|
||||
player = null;
|
||||
}
|
||||
}
|
||||
|
||||
public void stopService() {
|
||||
NotificationUtil.getInstance().cancelNotificationAndStopForeground(this);
|
||||
cleanup();
|
||||
stopSelf();
|
||||
}
|
||||
|
||||
@@ -214,11 +219,8 @@ public final class MainPlayer extends Service {
|
||||
boolean isLandscape() {
|
||||
// DisplayMetrics from activity context knows about MultiWindow feature
|
||||
// while DisplayMetrics from app context doesn't
|
||||
final DisplayMetrics metrics = (player != null
|
||||
&& player.getParentActivity() != null
|
||||
? player.getParentActivity().getResources()
|
||||
: getResources()).getDisplayMetrics();
|
||||
return metrics.heightPixels < metrics.widthPixels;
|
||||
return DeviceUtils.isLandscape(player != null && player.getParentActivity() != null
|
||||
? player.getParentActivity() : this);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
|
||||
@@ -12,7 +12,6 @@ import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.PopupMenu;
|
||||
import android.widget.SeekBar;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
@@ -40,14 +39,14 @@ import org.schabi.newpipe.player.playqueue.PlayQueueItemTouchCallback;
|
||||
import org.schabi.newpipe.util.Localization;
|
||||
import org.schabi.newpipe.util.NavigationHelper;
|
||||
import org.schabi.newpipe.util.PermissionHelper;
|
||||
import org.schabi.newpipe.util.ServiceHelper;
|
||||
import org.schabi.newpipe.util.ThemeHelper;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import static org.schabi.newpipe.QueueItemMenuUtil.openPopupMenu;
|
||||
import static org.schabi.newpipe.player.helper.PlayerHelper.formatSpeed;
|
||||
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
|
||||
import static org.schabi.newpipe.util.external_communication.ShareUtils.shareText;
|
||||
|
||||
public final class PlayQueueActivity extends AppCompatActivity
|
||||
implements PlayerEventListener, SeekBar.OnSeekBarChangeListener,
|
||||
@@ -55,7 +54,6 @@ public final class PlayQueueActivity extends AppCompatActivity
|
||||
|
||||
private static final String TAG = PlayQueueActivity.class.getSimpleName();
|
||||
|
||||
private static final int RECYCLER_ITEM_POPUP_MENU_GROUP_ID = 47;
|
||||
private static final int SMOOTH_SCROLL_MAXIMUM_DISTANCE = 80;
|
||||
|
||||
protected Player player;
|
||||
@@ -83,7 +81,7 @@ public final class PlayQueueActivity extends AppCompatActivity
|
||||
protected void onCreate(final Bundle savedInstanceState) {
|
||||
assureCorrectAppLanguage(this);
|
||||
super.onCreate(savedInstanceState);
|
||||
ThemeHelper.setTheme(this);
|
||||
ThemeHelper.setTheme(this, ServiceHelper.getSelectedServiceId(this));
|
||||
|
||||
queueControlBinding = ActivityPlayerQueueControlBinding.inflate(getLayoutInflater());
|
||||
setContentView(queueControlBinding.getRoot());
|
||||
@@ -278,49 +276,6 @@ public final class PlayQueueActivity extends AppCompatActivity
|
||||
queueControlBinding.controlShuffle.setOnClickListener(this);
|
||||
}
|
||||
|
||||
private void buildItemPopupMenu(final PlayQueueItem item, final View view) {
|
||||
final PopupMenu popupMenu = new PopupMenu(this, view);
|
||||
final MenuItem remove = popupMenu.getMenu().add(RECYCLER_ITEM_POPUP_MENU_GROUP_ID, 0,
|
||||
Menu.NONE, R.string.play_queue_remove);
|
||||
remove.setOnMenuItemClickListener(menuItem -> {
|
||||
if (player == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final int index = player.getPlayQueue().indexOf(item);
|
||||
if (index != -1) {
|
||||
player.getPlayQueue().remove(index);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
final MenuItem detail = popupMenu.getMenu().add(RECYCLER_ITEM_POPUP_MENU_GROUP_ID, 1,
|
||||
Menu.NONE, R.string.play_queue_stream_detail);
|
||||
detail.setOnMenuItemClickListener(menuItem -> {
|
||||
// playQueue is null since we don't want any queue change
|
||||
NavigationHelper.openVideoDetail(this, item.getServiceId(), item.getUrl(),
|
||||
item.getTitle(), null, false);
|
||||
return true;
|
||||
});
|
||||
|
||||
final MenuItem append = popupMenu.getMenu().add(RECYCLER_ITEM_POPUP_MENU_GROUP_ID, 2,
|
||||
Menu.NONE, R.string.append_playlist);
|
||||
append.setOnMenuItemClickListener(menuItem -> {
|
||||
openPlaylistAppendDialog(Collections.singletonList(item));
|
||||
return true;
|
||||
});
|
||||
|
||||
final MenuItem share = popupMenu.getMenu().add(RECYCLER_ITEM_POPUP_MENU_GROUP_ID, 3,
|
||||
Menu.NONE, R.string.share);
|
||||
share.setOnMenuItemClickListener(menuItem -> {
|
||||
shareText(getApplicationContext(), item.getTitle(), item.getUrl(),
|
||||
item.getThumbnailUrl());
|
||||
return true;
|
||||
});
|
||||
|
||||
popupMenu.show();
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
// Component Helpers
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
@@ -368,13 +323,9 @@ public final class PlayQueueActivity extends AppCompatActivity
|
||||
|
||||
@Override
|
||||
public void held(final PlayQueueItem item, final View view) {
|
||||
if (player == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final int index = player.getPlayQueue().indexOf(item);
|
||||
if (index != -1) {
|
||||
buildItemPopupMenu(item, view);
|
||||
if (player != null && player.getPlayQueue().indexOf(item) != -1) {
|
||||
openPopupMenu(player.getPlayQueue(), item, view, false,
|
||||
getSupportFragmentManager(), PlayQueueActivity.this);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -16,6 +16,7 @@ import androidx.media.AudioManagerCompat;
|
||||
|
||||
import com.google.android.exoplayer2.SimpleExoPlayer;
|
||||
import com.google.android.exoplayer2.analytics.AnalyticsListener;
|
||||
import com.google.android.exoplayer2.decoder.DecoderCounters;
|
||||
|
||||
public class AudioReactor implements AudioManager.OnAudioFocusChangeListener, AnalyticsListener {
|
||||
|
||||
@@ -50,6 +51,7 @@ public class AudioReactor implements AudioManager.OnAudioFocusChangeListener, An
|
||||
public void dispose() {
|
||||
abandonAudioFocus();
|
||||
player.removeAnalyticsListener(this);
|
||||
notifyAudioSessionUpdate(false, player.getAudioSessionId());
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
@@ -149,11 +151,21 @@ public class AudioReactor implements AudioManager.OnAudioFocusChangeListener, An
|
||||
|
||||
@Override
|
||||
public void onAudioSessionId(final EventTime eventTime, final int audioSessionId) {
|
||||
notifyAudioSessionUpdate(true, audioSessionId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAudioDisabled(final EventTime eventTime, final DecoderCounters counters) {
|
||||
notifyAudioSessionUpdate(false, player.getAudioSessionId());
|
||||
}
|
||||
|
||||
private void notifyAudioSessionUpdate(final boolean active, final int audioSessionId) {
|
||||
if (!PlayerHelper.isUsingDSP()) {
|
||||
return;
|
||||
}
|
||||
|
||||
final Intent intent = new Intent(AudioEffect.ACTION_OPEN_AUDIO_EFFECT_CONTROL_SESSION);
|
||||
final Intent intent = new Intent(active
|
||||
? AudioEffect.ACTION_OPEN_AUDIO_EFFECT_CONTROL_SESSION
|
||||
: AudioEffect.ACTION_CLOSE_AUDIO_EFFECT_CONTROL_SESSION);
|
||||
intent.putExtra(AudioEffect.EXTRA_AUDIO_SESSION, audioSessionId);
|
||||
intent.putExtra(AudioEffect.EXTRA_PACKAGE_NAME, context.getPackageName());
|
||||
context.sendBroadcast(intent);
|
||||
|
||||
@@ -20,18 +20,16 @@ public class LoadController implements LoadControl {
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
public LoadController() {
|
||||
this(PlayerHelper.getPlaybackStartBufferMs(),
|
||||
PlayerHelper.getPlaybackMinimumBufferMs(),
|
||||
PlayerHelper.getPlaybackOptimalBufferMs());
|
||||
this(PlayerHelper.getPlaybackStartBufferMs());
|
||||
}
|
||||
|
||||
private LoadController(final int initialPlaybackBufferMs,
|
||||
final int minimumPlaybackBufferMs,
|
||||
final int optimalPlaybackBufferMs) {
|
||||
private LoadController(final int initialPlaybackBufferMs) {
|
||||
this.initialPlaybackBufferUs = initialPlaybackBufferMs * 1000;
|
||||
|
||||
final DefaultLoadControl.Builder builder = new DefaultLoadControl.Builder();
|
||||
builder.setBufferDurationsMs(minimumPlaybackBufferMs, optimalPlaybackBufferMs,
|
||||
builder.setBufferDurationsMs(
|
||||
DefaultLoadControl.DEFAULT_MIN_BUFFER_MS,
|
||||
DefaultLoadControl.DEFAULT_MAX_BUFFER_MS,
|
||||
initialPlaybackBufferMs,
|
||||
DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS);
|
||||
internalLoadControl = builder.build();
|
||||
|
||||
@@ -21,6 +21,8 @@ import org.schabi.newpipe.player.mediasession.MediaSessionCallback;
|
||||
import org.schabi.newpipe.player.mediasession.PlayQueueNavigator;
|
||||
import org.schabi.newpipe.player.mediasession.PlayQueuePlaybackController;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
public class MediaSessionManager {
|
||||
private static final String TAG = MediaSessionManager.class.getSimpleName();
|
||||
public static final boolean DEBUG = MainActivity.DEBUG;
|
||||
@@ -30,6 +32,9 @@ public class MediaSessionManager {
|
||||
@NonNull
|
||||
private final MediaSessionConnector sessionConnector;
|
||||
|
||||
private int lastTitleHashCode;
|
||||
private int lastArtistHashCode;
|
||||
private long lastDuration;
|
||||
private int lastAlbumArtHashCode;
|
||||
|
||||
public MediaSessionManager(@NonNull final Context context,
|
||||
@@ -65,47 +70,127 @@ public class MediaSessionManager {
|
||||
return mediaSession.getSessionToken();
|
||||
}
|
||||
|
||||
public void setMetadata(final String title,
|
||||
final String artist,
|
||||
final Bitmap albumArt,
|
||||
final long duration) {
|
||||
if (albumArt == null || !mediaSession.isActive()) {
|
||||
/**
|
||||
* sets the Metadata - if required.
|
||||
*
|
||||
* @param title {@link MediaMetadataCompat#METADATA_KEY_TITLE}
|
||||
* @param artist {@link MediaMetadataCompat#METADATA_KEY_ARTIST}
|
||||
* @param optAlbumArt {@link MediaMetadataCompat#METADATA_KEY_ALBUM_ART}
|
||||
* @param duration {@link MediaMetadataCompat#METADATA_KEY_DURATION}
|
||||
* - should be a negative value for unknown durations, e.g. for livestreams
|
||||
*/
|
||||
public void setMetadata(@NonNull final String title,
|
||||
@NonNull final String artist,
|
||||
@NonNull final Optional<Bitmap> optAlbumArt,
|
||||
final long duration
|
||||
) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "setMetadata called:"
|
||||
+ " t: " + title
|
||||
+ " a: " + artist
|
||||
+ " thumb: " + (
|
||||
optAlbumArt.isPresent()
|
||||
? optAlbumArt.get().hashCode()
|
||||
: "<none>")
|
||||
+ " d: " + duration);
|
||||
}
|
||||
|
||||
if (!mediaSession.isActive()) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "setMetadata: mediaSession not active - exiting");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!checkIfMetadataShouldBeSet(title, artist, optAlbumArt, duration)) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "setMetadata: No update required - exiting");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (DEBUG) {
|
||||
if (getMetadataAlbumArt() == null) {
|
||||
Log.d(TAG, "N_getMetadataAlbumArt: thumb == null");
|
||||
}
|
||||
if (getMetadataTitle() == null) {
|
||||
Log.d(TAG, "N_getMetadataTitle: title == null");
|
||||
}
|
||||
if (getMetadataArtist() == null) {
|
||||
Log.d(TAG, "N_getMetadataArtist: artist == null");
|
||||
}
|
||||
if (getMetadataDuration() <= 1) {
|
||||
Log.d(TAG, "N_getMetadataDuration: duration <= 1; " + getMetadataDuration());
|
||||
}
|
||||
Log.d(TAG, "setMetadata: N_Metadata update:"
|
||||
+ " t: " + title
|
||||
+ " a: " + artist
|
||||
+ " thumb: " + (
|
||||
optAlbumArt.isPresent()
|
||||
? optAlbumArt.get().hashCode()
|
||||
: "<none>")
|
||||
+ " d: " + duration);
|
||||
}
|
||||
|
||||
if (getMetadataAlbumArt() == null || getMetadataTitle() == null
|
||||
|| getMetadataArtist() == null || getMetadataDuration() <= 1
|
||||
|| albumArt.hashCode() != lastAlbumArtHashCode) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "setMetadata: N_Metadata update: t: " + title + " a: " + artist
|
||||
+ " thumb: " + albumArt.hashCode() + " d: " + duration);
|
||||
}
|
||||
final MediaMetadataCompat.Builder builder = new MediaMetadataCompat.Builder()
|
||||
.putString(MediaMetadataCompat.METADATA_KEY_TITLE, title)
|
||||
.putString(MediaMetadataCompat.METADATA_KEY_ARTIST, artist)
|
||||
.putLong(MediaMetadataCompat.METADATA_KEY_DURATION, duration);
|
||||
|
||||
mediaSession.setMetadata(new MediaMetadataCompat.Builder()
|
||||
.putString(MediaMetadataCompat.METADATA_KEY_TITLE, title)
|
||||
.putString(MediaMetadataCompat.METADATA_KEY_ARTIST, artist)
|
||||
.putBitmap(MediaMetadataCompat.METADATA_KEY_ALBUM_ART, albumArt)
|
||||
.putBitmap(MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON, albumArt)
|
||||
.putLong(MediaMetadataCompat.METADATA_KEY_DURATION, duration).build());
|
||||
lastAlbumArtHashCode = albumArt.hashCode();
|
||||
if (optAlbumArt.isPresent()) {
|
||||
builder.putBitmap(MediaMetadataCompat.METADATA_KEY_ALBUM_ART, optAlbumArt.get());
|
||||
builder.putBitmap(MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON, optAlbumArt.get());
|
||||
}
|
||||
|
||||
mediaSession.setMetadata(builder.build());
|
||||
|
||||
lastTitleHashCode = title.hashCode();
|
||||
lastArtistHashCode = artist.hashCode();
|
||||
lastDuration = duration;
|
||||
if (optAlbumArt.isPresent()) {
|
||||
lastAlbumArtHashCode = optAlbumArt.get().hashCode();
|
||||
}
|
||||
}
|
||||
|
||||
private boolean checkIfMetadataShouldBeSet(
|
||||
@NonNull final String title,
|
||||
@NonNull final String artist,
|
||||
@NonNull final Optional<Bitmap> optAlbumArt,
|
||||
final long duration
|
||||
) {
|
||||
// Check if the values have changed since the last time
|
||||
if (title.hashCode() != lastTitleHashCode
|
||||
|| artist.hashCode() != lastArtistHashCode
|
||||
|| duration != lastDuration
|
||||
|| (optAlbumArt.isPresent() && optAlbumArt.get().hashCode() != lastAlbumArtHashCode)
|
||||
) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG,
|
||||
"checkIfMetadataShouldBeSet: true - reason: changed values since last");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if the currently set metadata is valid
|
||||
if (getMetadataTitle() == null
|
||||
|| getMetadataArtist() == null
|
||||
// Note that the duration can be <= 0 for live streams
|
||||
) {
|
||||
if (DEBUG) {
|
||||
if (getMetadataTitle() == null) {
|
||||
Log.d(TAG,
|
||||
"N_getMetadataTitle: title == null");
|
||||
} else if (getMetadataArtist() == null) {
|
||||
Log.d(TAG,
|
||||
"N_getMetadataArtist: artist == null");
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// If we got an album art check if the current set AlbumArt is null
|
||||
if (optAlbumArt.isPresent() && getMetadataAlbumArt() == null) {
|
||||
if (DEBUG) {
|
||||
if (getMetadataAlbumArt() == null) {
|
||||
Log.d(TAG, "N_getMetadataAlbumArt: thumb == null");
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Default - no update required
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
private Bitmap getMetadataAlbumArt() {
|
||||
return mediaSession.getController().getMetadata()
|
||||
.getBitmap(MediaMetadataCompat.METADATA_KEY_ALBUM_ART);
|
||||
@@ -121,11 +206,6 @@ public class MediaSessionManager {
|
||||
.getString(MediaMetadataCompat.METADATA_KEY_ARTIST);
|
||||
}
|
||||
|
||||
private long getMetadataDuration() {
|
||||
return mediaSession.getController().getMetadata()
|
||||
.getLong(MediaMetadataCompat.METADATA_KEY_DURATION);
|
||||
}
|
||||
|
||||
/**
|
||||
* Should be called on player destruction to prevent leakage.
|
||||
*/
|
||||
|
||||
@@ -164,7 +164,7 @@ public class PlaybackParameterDialog extends DialogFragment {
|
||||
setPlaybackParameters(initialTempo, initialPitch, initialSkipSilence))
|
||||
.setNeutralButton(R.string.playback_reset, (dialogInterface, i) ->
|
||||
setPlaybackParameters(DEFAULT_TEMPO, DEFAULT_PITCH, DEFAULT_SKIP_SILENCE))
|
||||
.setPositiveButton(R.string.finish, (dialogInterface, i) ->
|
||||
.setPositiveButton(R.string.ok, (dialogInterface, i) ->
|
||||
setCurrentPlaybackParameters());
|
||||
|
||||
return dialogBuilder.create();
|
||||
|
||||
@@ -239,6 +239,11 @@ public final class PlayerHelper {
|
||||
.getBoolean(context.getString(R.string.brightness_gesture_control_key), true);
|
||||
}
|
||||
|
||||
public static boolean isStartMainPlayerFullscreenEnabled(@NonNull final Context context) {
|
||||
return getPreferences(context)
|
||||
.getBoolean(context.getString(R.string.start_main_player_fullscreen_key), false);
|
||||
}
|
||||
|
||||
public static boolean isAutoQueueEnabled(@NonNull final Context context) {
|
||||
return getPreferences(context)
|
||||
.getBoolean(context.getString(R.string.auto_queue_key), false);
|
||||
@@ -307,22 +312,6 @@ public final class PlayerHelper {
|
||||
return 500;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the minimum number of milliseconds the player always buffers to
|
||||
* after starting playback.
|
||||
*/
|
||||
public static int getPlaybackMinimumBufferMs() {
|
||||
return 25000;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the maximum/optimal number of milliseconds the player will buffer to once the buffer
|
||||
* hits the point of {@link #getPlaybackMinimumBufferMs()}.
|
||||
*/
|
||||
public static int getPlaybackOptimalBufferMs() {
|
||||
return 60000;
|
||||
}
|
||||
|
||||
public static TrackSelection.Factory getQualitySelector() {
|
||||
return new AdaptiveTrackSelection.Factory(
|
||||
1000,
|
||||
|
||||
@@ -8,6 +8,7 @@ import android.os.IBinder;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.content.ContextCompat;
|
||||
|
||||
import com.google.android.exoplayer2.ExoPlaybackException;
|
||||
import com.google.android.exoplayer2.PlaybackParameters;
|
||||
@@ -22,18 +23,27 @@ import org.schabi.newpipe.player.event.PlayerServiceExtendedEventListener;
|
||||
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
||||
|
||||
public final class PlayerHolder {
|
||||
|
||||
private PlayerHolder() {
|
||||
}
|
||||
|
||||
private static final boolean DEBUG = MainActivity.DEBUG;
|
||||
private static final String TAG = "PlayerHolder";
|
||||
private static PlayerHolder instance;
|
||||
public static synchronized PlayerHolder getInstance() {
|
||||
if (PlayerHolder.instance == null) {
|
||||
PlayerHolder.instance = new PlayerHolder();
|
||||
}
|
||||
return PlayerHolder.instance;
|
||||
}
|
||||
|
||||
private static PlayerServiceExtendedEventListener listener;
|
||||
private final boolean DEBUG = MainActivity.DEBUG;
|
||||
private final String TAG = PlayerHolder.class.getSimpleName();
|
||||
|
||||
private static ServiceConnection serviceConnection;
|
||||
public static boolean bound;
|
||||
private static MainPlayer playerService;
|
||||
private static Player player;
|
||||
private PlayerServiceExtendedEventListener listener;
|
||||
|
||||
private final PlayerServiceConnection serviceConnection = new PlayerServiceConnection();
|
||||
public boolean bound;
|
||||
private MainPlayer playerService;
|
||||
private Player player;
|
||||
|
||||
/**
|
||||
* Returns the current {@link MainPlayer.PlayerType} of the {@link MainPlayer} service,
|
||||
@@ -42,26 +52,35 @@ public final class PlayerHolder {
|
||||
* @return Current PlayerType
|
||||
*/
|
||||
@Nullable
|
||||
public static MainPlayer.PlayerType getType() {
|
||||
public MainPlayer.PlayerType getType() {
|
||||
if (player == null) {
|
||||
return null;
|
||||
}
|
||||
return player.getPlayerType();
|
||||
}
|
||||
|
||||
public static boolean isPlaying() {
|
||||
public boolean isPlaying() {
|
||||
if (player == null) {
|
||||
return false;
|
||||
}
|
||||
return player.isPlaying();
|
||||
}
|
||||
|
||||
public static boolean isPlayerOpen() {
|
||||
public boolean isPlayerOpen() {
|
||||
return player != null;
|
||||
}
|
||||
|
||||
public static void setListener(final PlayerServiceExtendedEventListener newListener) {
|
||||
public int getQueueSize() {
|
||||
return isPlayerOpen() ? player.getPlayQueue().size() : 0;
|
||||
}
|
||||
|
||||
public void setListener(@Nullable final PlayerServiceExtendedEventListener newListener) {
|
||||
listener = newListener;
|
||||
|
||||
if (listener == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Force reload data from service
|
||||
if (player != null) {
|
||||
listener.onServiceConnected(player, playerService, false);
|
||||
@@ -69,14 +88,15 @@ public final class PlayerHolder {
|
||||
}
|
||||
}
|
||||
|
||||
public static void removeListener() {
|
||||
listener = null;
|
||||
// helper to handle context in common place as using the same
|
||||
// context to bind/unbind a service is crucial
|
||||
private Context getCommonContext() {
|
||||
return App.getApp();
|
||||
}
|
||||
|
||||
|
||||
public static void startService(final Context context,
|
||||
final boolean playAfterConnect,
|
||||
final PlayerServiceExtendedEventListener newListener) {
|
||||
public void startService(final boolean playAfterConnect,
|
||||
final PlayerServiceExtendedEventListener newListener) {
|
||||
final Context context = getCommonContext();
|
||||
setListener(newListener);
|
||||
if (bound) {
|
||||
return;
|
||||
@@ -85,58 +105,65 @@ public final class PlayerHolder {
|
||||
// and NullPointerExceptions inside the service because the service will be
|
||||
// bound twice. Prevent it with unbinding first
|
||||
unbind(context);
|
||||
context.startService(new Intent(context, MainPlayer.class));
|
||||
serviceConnection = getServiceConnection(context, playAfterConnect);
|
||||
ContextCompat.startForegroundService(context, new Intent(context, MainPlayer.class));
|
||||
serviceConnection.doPlayAfterConnect(playAfterConnect);
|
||||
bind(context);
|
||||
}
|
||||
|
||||
public static void stopService(final Context context) {
|
||||
public void stopService() {
|
||||
final Context context = getCommonContext();
|
||||
unbind(context);
|
||||
context.stopService(new Intent(context, MainPlayer.class));
|
||||
}
|
||||
|
||||
private static ServiceConnection getServiceConnection(final Context context,
|
||||
final boolean playAfterConnect) {
|
||||
return new ServiceConnection() {
|
||||
@Override
|
||||
public void onServiceDisconnected(final ComponentName compName) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "Player service is disconnected");
|
||||
}
|
||||
class PlayerServiceConnection implements ServiceConnection {
|
||||
|
||||
unbind(context);
|
||||
private boolean playAfterConnect = false;
|
||||
|
||||
public void doPlayAfterConnect(final boolean playAfterConnection) {
|
||||
this.playAfterConnect = playAfterConnection;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onServiceDisconnected(final ComponentName compName) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "Player service is disconnected");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onServiceConnected(final ComponentName compName, final IBinder service) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "Player service is connected");
|
||||
}
|
||||
final MainPlayer.LocalBinder localBinder = (MainPlayer.LocalBinder) service;
|
||||
final Context context = getCommonContext();
|
||||
unbind(context);
|
||||
}
|
||||
|
||||
playerService = localBinder.getService();
|
||||
player = localBinder.getPlayer();
|
||||
if (listener != null) {
|
||||
listener.onServiceConnected(player, playerService, playAfterConnect);
|
||||
}
|
||||
startPlayerListener();
|
||||
@Override
|
||||
public void onServiceConnected(final ComponentName compName, final IBinder service) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "Player service is connected");
|
||||
}
|
||||
};
|
||||
}
|
||||
final MainPlayer.LocalBinder localBinder = (MainPlayer.LocalBinder) service;
|
||||
|
||||
private static void bind(final Context context) {
|
||||
playerService = localBinder.getService();
|
||||
player = localBinder.getPlayer();
|
||||
if (listener != null) {
|
||||
listener.onServiceConnected(player, playerService, playAfterConnect);
|
||||
}
|
||||
startPlayerListener();
|
||||
}
|
||||
};
|
||||
|
||||
private void bind(final Context context) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "bind() called");
|
||||
}
|
||||
|
||||
final Intent serviceIntent = new Intent(context, MainPlayer.class);
|
||||
bound = context.bindService(serviceIntent, serviceConnection, Context.BIND_AUTO_CREATE);
|
||||
bound = context.bindService(serviceIntent, serviceConnection,
|
||||
Context.BIND_AUTO_CREATE);
|
||||
if (!bound) {
|
||||
context.unbindService(serviceConnection);
|
||||
}
|
||||
}
|
||||
|
||||
private static void unbind(final Context context) {
|
||||
private void unbind(final Context context) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "unbind() called");
|
||||
}
|
||||
@@ -153,21 +180,19 @@ public final class PlayerHolder {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private static void startPlayerListener() {
|
||||
private void startPlayerListener() {
|
||||
if (player != null) {
|
||||
player.setFragmentListener(INNER_LISTENER);
|
||||
player.setFragmentListener(internalListener);
|
||||
}
|
||||
}
|
||||
|
||||
private static void stopPlayerListener() {
|
||||
private void stopPlayerListener() {
|
||||
if (player != null) {
|
||||
player.removeFragmentListener(INNER_LISTENER);
|
||||
player.removeFragmentListener(internalListener);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private static final PlayerServiceEventListener INNER_LISTENER =
|
||||
private final PlayerServiceEventListener internalListener =
|
||||
new PlayerServiceEventListener() {
|
||||
@Override
|
||||
public void onFullscreenStateChanged(final boolean fullscreen) {
|
||||
@@ -242,7 +267,7 @@ public final class PlayerHolder {
|
||||
if (listener != null) {
|
||||
listener.onServiceStopped();
|
||||
}
|
||||
unbind(App.getApp());
|
||||
unbind(getCommonContext());
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
package org.schabi.newpipe.player.playback;
|
||||
|
||||
import android.content.Context;
|
||||
import android.view.SurfaceHolder;
|
||||
|
||||
import com.google.android.exoplayer2.SimpleExoPlayer;
|
||||
import com.google.android.exoplayer2.video.DummySurface;
|
||||
|
||||
/**
|
||||
* Prevent error message: 'Unrecoverable player error occurred'
|
||||
* In case of rotation some users see this kind of an error which is preventable
|
||||
* having a Callback that handles the lifecycle of the surface.
|
||||
* <p>
|
||||
* How?: In case we are no longer able to write to the surface eg. through rotation/putting in
|
||||
* background we set set a DummySurface. Although it it works on API >= 23 only.
|
||||
* Result: we get a little video interruption (audio is still fine) but we won't get the
|
||||
* 'Unrecoverable player error occurred' error message.
|
||||
* <p>
|
||||
* This implementation is based on:
|
||||
* 'ExoPlayer stuck in buffering after re-adding the surface view a few time #2703'
|
||||
* <p>
|
||||
* -> exoplayer fix suggestion link
|
||||
* https://github.com/google/ExoPlayer/issues/2703#issuecomment-300599981
|
||||
*/
|
||||
public final class SurfaceHolderCallback implements SurfaceHolder.Callback {
|
||||
|
||||
private final Context context;
|
||||
private final SimpleExoPlayer player;
|
||||
private DummySurface dummySurface;
|
||||
|
||||
public SurfaceHolderCallback(final Context context, final SimpleExoPlayer player) {
|
||||
this.context = context;
|
||||
this.player = player;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void surfaceCreated(final SurfaceHolder holder) {
|
||||
player.setVideoSurface(holder.getSurface());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void surfaceChanged(final SurfaceHolder holder,
|
||||
final int format,
|
||||
final int width,
|
||||
final int height) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void surfaceDestroyed(final SurfaceHolder holder) {
|
||||
if (dummySurface == null) {
|
||||
dummySurface = DummySurface.newInstanceV17(context, false);
|
||||
}
|
||||
player.setVideoSurface(dummySurface);
|
||||
}
|
||||
|
||||
public void release() {
|
||||
if (dummySurface != null) {
|
||||
dummySurface.release();
|
||||
dummySurface = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -27,6 +27,7 @@ public class PlayQueueItem implements Serializable {
|
||||
private final String thumbnailUrl;
|
||||
@NonNull
|
||||
private final String uploader;
|
||||
private final String uploaderUrl;
|
||||
@NonNull
|
||||
private final StreamType streamType;
|
||||
|
||||
@@ -37,7 +38,8 @@ public class PlayQueueItem implements Serializable {
|
||||
|
||||
PlayQueueItem(@NonNull final StreamInfo info) {
|
||||
this(info.getName(), info.getUrl(), info.getServiceId(), info.getDuration(),
|
||||
info.getThumbnailUrl(), info.getUploaderName(), info.getStreamType());
|
||||
info.getThumbnailUrl(), info.getUploaderName(),
|
||||
info.getUploaderUrl(), info.getStreamType());
|
||||
|
||||
if (info.getStartPosition() > 0) {
|
||||
setRecoveryPosition(info.getStartPosition() * 1000);
|
||||
@@ -46,38 +48,26 @@ public class PlayQueueItem implements Serializable {
|
||||
|
||||
PlayQueueItem(@NonNull final StreamInfoItem item) {
|
||||
this(item.getName(), item.getUrl(), item.getServiceId(), item.getDuration(),
|
||||
item.getThumbnailUrl(), item.getUploaderName(), item.getStreamType());
|
||||
item.getThumbnailUrl(), item.getUploaderName(),
|
||||
item.getUploaderUrl(), item.getStreamType());
|
||||
}
|
||||
|
||||
private PlayQueueItem(@Nullable final String name, @Nullable final String url,
|
||||
final int serviceId, final long duration,
|
||||
@Nullable final String thumbnailUrl, @Nullable final String uploader,
|
||||
@NonNull final StreamType streamType) {
|
||||
final String uploaderUrl, @NonNull final StreamType streamType) {
|
||||
this.title = name != null ? name : EMPTY_STRING;
|
||||
this.url = url != null ? url : EMPTY_STRING;
|
||||
this.serviceId = serviceId;
|
||||
this.duration = duration;
|
||||
this.thumbnailUrl = thumbnailUrl != null ? thumbnailUrl : EMPTY_STRING;
|
||||
this.uploader = uploader != null ? uploader : EMPTY_STRING;
|
||||
this.uploaderUrl = uploaderUrl;
|
||||
this.streamType = streamType;
|
||||
|
||||
this.recoveryPosition = RECOVERY_UNSET;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(final Object o) {
|
||||
if (o instanceof PlayQueueItem) {
|
||||
return url.equals(((PlayQueueItem) o).url);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return url.hashCode();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public String getTitle() {
|
||||
return title;
|
||||
@@ -106,6 +96,10 @@ public class PlayQueueItem implements Serializable {
|
||||
return uploader;
|
||||
}
|
||||
|
||||
public String getUploaderUrl() {
|
||||
return uploaderUrl;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public StreamType getStreamType() {
|
||||
return streamType;
|
||||
|
||||
@@ -5,11 +5,9 @@ import android.text.TextUtils;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
|
||||
import com.nostra13.universalimageloader.core.ImageLoader;
|
||||
|
||||
import org.schabi.newpipe.extractor.NewPipe;
|
||||
import org.schabi.newpipe.util.ImageDisplayConstants;
|
||||
import org.schabi.newpipe.util.Localization;
|
||||
import org.schabi.newpipe.util.PicassoHelper;
|
||||
|
||||
public class PlayQueueItemBuilder {
|
||||
private static final String TAG = PlayQueueItemBuilder.class.toString();
|
||||
@@ -35,8 +33,7 @@ public class PlayQueueItemBuilder {
|
||||
holder.itemDurationView.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
ImageLoader.getInstance().displayImage(item.getThumbnailUrl(), holder.itemThumbnailView,
|
||||
ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS);
|
||||
PicassoHelper.loadThumbnail(item.getThumbnailUrl()).into(holder.itemThumbnailView);
|
||||
|
||||
holder.itemRoot.setOnClickListener(view -> {
|
||||
if (onItemClickListener != null) {
|
||||
|
||||
@@ -14,6 +14,7 @@ import com.google.android.exoplayer2.util.Util;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||
import org.schabi.newpipe.extractor.stream.StreamType;
|
||||
import org.schabi.newpipe.player.helper.PlayerDataSource;
|
||||
import org.schabi.newpipe.util.StreamTypeUtil;
|
||||
|
||||
public interface PlaybackResolver extends Resolver<StreamInfo, MediaSource> {
|
||||
|
||||
@@ -21,7 +22,7 @@ public interface PlaybackResolver extends Resolver<StreamInfo, MediaSource> {
|
||||
default MediaSource maybeBuildLiveMediaSource(@NonNull final PlayerDataSource dataSource,
|
||||
@NonNull final StreamInfo info) {
|
||||
final StreamType streamType = info.getStreamType();
|
||||
if (!(streamType == StreamType.AUDIO_LIVE_STREAM || streamType == StreamType.LIVE_STREAM)) {
|
||||
if (!StreamTypeUtil.isLiveStream(streamType)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
package org.schabi.newpipe.player.seekbarpreview;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Bitmap;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
import android.widget.ImageView;
|
||||
|
||||
import androidx.annotation.IntDef;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.preference.PreferenceManager;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.util.DeviceUtils;
|
||||
|
||||
import java.lang.annotation.Retention;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.function.IntSupplier;
|
||||
|
||||
import static java.lang.annotation.RetentionPolicy.SOURCE;
|
||||
import static org.schabi.newpipe.player.seekbarpreview.SeekbarPreviewThumbnailHelper.SeekbarPreviewThumbnailType.HIGH_QUALITY;
|
||||
import static org.schabi.newpipe.player.seekbarpreview.SeekbarPreviewThumbnailHelper.SeekbarPreviewThumbnailType.LOW_QUALITY;
|
||||
import static org.schabi.newpipe.player.seekbarpreview.SeekbarPreviewThumbnailHelper.SeekbarPreviewThumbnailType.NONE;
|
||||
|
||||
/**
|
||||
* Helper for the seekbar preview.
|
||||
*/
|
||||
public final class SeekbarPreviewThumbnailHelper {
|
||||
|
||||
// This has to be <= 23 chars on devices running Android 7 or lower (API <= 25)
|
||||
// or it fails with an IllegalArgumentException
|
||||
// https://stackoverflow.com/a/54744028
|
||||
public static final String TAG = "SeekbarPrevThumbHelper";
|
||||
|
||||
private SeekbarPreviewThumbnailHelper() {
|
||||
// No impl pls
|
||||
}
|
||||
|
||||
@Retention(SOURCE)
|
||||
@IntDef({HIGH_QUALITY, LOW_QUALITY,
|
||||
NONE})
|
||||
public @interface SeekbarPreviewThumbnailType {
|
||||
int HIGH_QUALITY = 0;
|
||||
int LOW_QUALITY = 1;
|
||||
int NONE = 2;
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
// Settings Resolution
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@SeekbarPreviewThumbnailType
|
||||
public static int getSeekbarPreviewThumbnailType(@NonNull final Context context) {
|
||||
final String type = PreferenceManager.getDefaultSharedPreferences(context).getString(
|
||||
context.getString(R.string.seekbar_preview_thumbnail_key), "");
|
||||
if (type.equals(context.getString(R.string.seekbar_preview_thumbnail_none))) {
|
||||
return NONE;
|
||||
} else if (type.equals(context.getString(R.string.seekbar_preview_thumbnail_low_quality))) {
|
||||
return LOW_QUALITY;
|
||||
} else {
|
||||
return HIGH_QUALITY; // default
|
||||
}
|
||||
}
|
||||
|
||||
public static void tryResizeAndSetSeekbarPreviewThumbnail(
|
||||
@NonNull final Context context,
|
||||
@NonNull final Optional<Bitmap> optPreviewThumbnail,
|
||||
@NonNull final ImageView currentSeekbarPreviewThumbnail,
|
||||
@NonNull final IntSupplier baseViewWidthSupplier) {
|
||||
|
||||
if (!optPreviewThumbnail.isPresent()) {
|
||||
currentSeekbarPreviewThumbnail.setVisibility(View.GONE);
|
||||
return;
|
||||
}
|
||||
|
||||
currentSeekbarPreviewThumbnail.setVisibility(View.VISIBLE);
|
||||
final Bitmap srcBitmap = optPreviewThumbnail.get();
|
||||
|
||||
// Resize original bitmap
|
||||
try {
|
||||
Objects.requireNonNull(srcBitmap);
|
||||
|
||||
final int srcWidth = srcBitmap.getWidth() > 0 ? srcBitmap.getWidth() : 1;
|
||||
final int newWidth = Math.max(
|
||||
Math.min(
|
||||
// Use 1/4 of the width for the preview
|
||||
Math.round(baseViewWidthSupplier.getAsInt() / 4f),
|
||||
// Scaling more than that factor looks really pixelated -> max
|
||||
Math.round(srcWidth * 2.5f)
|
||||
),
|
||||
// Min width = 10dp
|
||||
DeviceUtils.dpToPx(10, context)
|
||||
);
|
||||
|
||||
final float scaleFactor = (float) newWidth / srcWidth;
|
||||
final int newHeight = (int) (srcBitmap.getHeight() * scaleFactor);
|
||||
|
||||
currentSeekbarPreviewThumbnail.setImageBitmap(
|
||||
Bitmap.createScaledBitmap(srcBitmap, newWidth, newHeight, true));
|
||||
} catch (final Exception ex) {
|
||||
Log.e(TAG, "Failed to resize and set seekbar preview thumbnail", ex);
|
||||
currentSeekbarPreviewThumbnail.setVisibility(View.GONE);
|
||||
} finally {
|
||||
srcBitmap.recycle();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,239 @@
|
||||
package org.schabi.newpipe.player.seekbarpreview;
|
||||
|
||||
import static org.schabi.newpipe.player.seekbarpreview.SeekbarPreviewThumbnailHelper.SeekbarPreviewThumbnailType;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Bitmap;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.google.common.base.Stopwatch;
|
||||
|
||||
import org.schabi.newpipe.extractor.stream.Frameset;
|
||||
import org.schabi.newpipe.util.PicassoHelper;
|
||||
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
public class SeekbarPreviewThumbnailHolder {
|
||||
|
||||
// This has to be <= 23 chars on devices running Android 7 or lower (API <= 25)
|
||||
// or it fails with an IllegalArgumentException
|
||||
// https://stackoverflow.com/a/54744028
|
||||
public static final String TAG = "SeekbarPrevThumbHolder";
|
||||
|
||||
// Key = Position of the picture in milliseconds
|
||||
// Supplier = Supplies the bitmap for that position
|
||||
private final Map<Integer, Supplier<Bitmap>> seekbarPreviewData = new ConcurrentHashMap<>();
|
||||
|
||||
// This ensures that if the reset is still undergoing
|
||||
// and another reset starts, only the last reset is processed
|
||||
private UUID currentUpdateRequestIdentifier = UUID.randomUUID();
|
||||
|
||||
public synchronized void resetFrom(
|
||||
@NonNull final Context context,
|
||||
final List<Frameset> framesets) {
|
||||
|
||||
final int seekbarPreviewType =
|
||||
SeekbarPreviewThumbnailHelper.getSeekbarPreviewThumbnailType(context);
|
||||
|
||||
final UUID updateRequestIdentifier = UUID.randomUUID();
|
||||
this.currentUpdateRequestIdentifier = updateRequestIdentifier;
|
||||
|
||||
final ExecutorService executorService = Executors.newSingleThreadExecutor();
|
||||
executorService.submit(() -> {
|
||||
try {
|
||||
resetFromAsync(seekbarPreviewType, framesets, updateRequestIdentifier);
|
||||
} catch (final Exception ex) {
|
||||
Log.e(TAG, "Failed to execute async", ex);
|
||||
}
|
||||
});
|
||||
// ensure that the executorService stops/destroys it's threads
|
||||
// after the task is finished
|
||||
executorService.shutdown();
|
||||
}
|
||||
|
||||
private void resetFromAsync(
|
||||
final int seekbarPreviewType,
|
||||
final List<Frameset> framesets,
|
||||
final UUID updateRequestIdentifier) {
|
||||
|
||||
Log.d(TAG, "Clearing seekbarPreviewData");
|
||||
seekbarPreviewData.clear();
|
||||
|
||||
if (seekbarPreviewType == SeekbarPreviewThumbnailType.NONE) {
|
||||
Log.d(TAG, "Not processing seekbarPreviewData due to settings");
|
||||
return;
|
||||
}
|
||||
|
||||
final Frameset frameset = getFrameSetForType(framesets, seekbarPreviewType);
|
||||
if (frameset == null) {
|
||||
Log.d(TAG, "No frameset was found to fill seekbarPreviewData");
|
||||
return;
|
||||
}
|
||||
|
||||
Log.d(TAG, "Frameset quality info: "
|
||||
+ "[width=" + frameset.getFrameWidth()
|
||||
+ ", heigh=" + frameset.getFrameHeight() + "]");
|
||||
|
||||
// Abort method execution if we are not the latest request
|
||||
if (!isRequestIdentifierCurrent(updateRequestIdentifier)) {
|
||||
return;
|
||||
}
|
||||
|
||||
generateDataFrom(frameset, updateRequestIdentifier);
|
||||
}
|
||||
|
||||
private Frameset getFrameSetForType(
|
||||
final List<Frameset> framesets,
|
||||
final int seekbarPreviewType) {
|
||||
|
||||
if (seekbarPreviewType == SeekbarPreviewThumbnailType.HIGH_QUALITY) {
|
||||
Log.d(TAG, "Strategy for seekbarPreviewData: high quality");
|
||||
return framesets.stream()
|
||||
.max(Comparator.comparingInt(fs -> fs.getFrameHeight() * fs.getFrameWidth()))
|
||||
.orElse(null);
|
||||
} else {
|
||||
Log.d(TAG, "Strategy for seekbarPreviewData: low quality");
|
||||
return framesets.stream()
|
||||
.min(Comparator.comparingInt(fs -> fs.getFrameHeight() * fs.getFrameWidth()))
|
||||
.orElse(null);
|
||||
}
|
||||
}
|
||||
|
||||
private void generateDataFrom(
|
||||
final Frameset frameset,
|
||||
final UUID updateRequestIdentifier) {
|
||||
|
||||
Log.d(TAG, "Starting generation of seekbarPreviewData");
|
||||
final Stopwatch sw = Log.isLoggable(TAG, Log.DEBUG) ? Stopwatch.createStarted() : null;
|
||||
|
||||
int currentPosMs = 0;
|
||||
int pos = 1;
|
||||
|
||||
final int frameCountPerUrl = frameset.getFramesPerPageX() * frameset.getFramesPerPageY();
|
||||
|
||||
// Process each url in the frameset
|
||||
for (final String url : frameset.getUrls()) {
|
||||
// get the bitmap
|
||||
final Bitmap srcBitMap = getBitMapFrom(url);
|
||||
|
||||
// The data is not added directly to "seekbarPreviewData" due to
|
||||
// concurrency and checks for "updateRequestIdentifier"
|
||||
final Map<Integer, Supplier<Bitmap>> generatedDataForUrl = new HashMap<>();
|
||||
|
||||
// The bitmap consists of several images, which we process here
|
||||
// foreach frame in the returned bitmap
|
||||
for (int i = 0; i < frameCountPerUrl; i++) {
|
||||
// Frames outside the video length are skipped
|
||||
if (pos > frameset.getTotalCount()) {
|
||||
break;
|
||||
}
|
||||
|
||||
// Get the bounds where the frame is found
|
||||
final int[] bounds = frameset.getFrameBoundsAt(currentPosMs);
|
||||
generatedDataForUrl.put(currentPosMs, () -> {
|
||||
// It can happen, that the original bitmap could not be downloaded
|
||||
// In such a case - we don't want a NullPointer - simply return null
|
||||
if (srcBitMap == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Cut out the corresponding bitmap form the "srcBitMap"
|
||||
return Bitmap.createBitmap(srcBitMap, bounds[1], bounds[2],
|
||||
frameset.getFrameWidth(), frameset.getFrameHeight());
|
||||
});
|
||||
|
||||
currentPosMs += frameset.getDurationPerFrame();
|
||||
pos++;
|
||||
}
|
||||
|
||||
// Check if we are still the latest request
|
||||
// If not abort method execution
|
||||
if (isRequestIdentifierCurrent(updateRequestIdentifier)) {
|
||||
seekbarPreviewData.putAll(generatedDataForUrl);
|
||||
} else {
|
||||
Log.d(TAG, "Aborted of generation of seekbarPreviewData");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (sw != null) {
|
||||
Log.d(TAG, "Generation of seekbarPreviewData took " + sw.stop().toString());
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private Bitmap getBitMapFrom(final String url) {
|
||||
if (url == null) {
|
||||
Log.w(TAG, "url is null; This should never happen");
|
||||
return null;
|
||||
}
|
||||
|
||||
final Stopwatch sw = Log.isLoggable(TAG, Log.DEBUG) ? Stopwatch.createStarted() : null;
|
||||
try {
|
||||
Log.d(TAG, "Downloading bitmap for seekbarPreview from '" + url + "'");
|
||||
|
||||
// Gets the bitmap within the timeout of 15 seconds imposed by default by OkHttpClient
|
||||
// Ensure that your are not running on the main-Thread this will otherwise hang
|
||||
final Bitmap bitmap = PicassoHelper.loadSeekbarThumbnailPreview(url).get();
|
||||
|
||||
if (sw != null) {
|
||||
Log.d(TAG,
|
||||
"Download of bitmap for seekbarPreview from '" + url
|
||||
+ "' took " + sw.stop().toString());
|
||||
}
|
||||
|
||||
return bitmap;
|
||||
} catch (final Exception ex) {
|
||||
Log.w(TAG,
|
||||
"Failed to get bitmap for seekbarPreview from url='" + url
|
||||
+ "' in time",
|
||||
ex);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isRequestIdentifierCurrent(final UUID requestIdentifier) {
|
||||
return this.currentUpdateRequestIdentifier.equals(requestIdentifier);
|
||||
}
|
||||
|
||||
|
||||
public Optional<Bitmap> getBitmapAt(final int positionInMs) {
|
||||
// Check if the BitmapData is empty
|
||||
if (seekbarPreviewData.isEmpty()) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
// Get the closest frame to the requested position
|
||||
final int closestIndexPosition =
|
||||
seekbarPreviewData.keySet().stream()
|
||||
.min(Comparator.comparingInt(i -> Math.abs(i - positionInMs)))
|
||||
.orElse(-1);
|
||||
|
||||
// this should never happen, because
|
||||
// it indicates that "seekbarPreviewData" is empty which was already checked
|
||||
if (closestIndexPosition == -1) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
try {
|
||||
// Get the bitmap for the position (executes the supplier)
|
||||
return Optional.ofNullable(seekbarPreviewData.get(closestIndexPosition).get());
|
||||
} catch (final Exception ex) {
|
||||
// If there is an error, log it and return Optional.empty
|
||||
Log.w(TAG, "Unable to get seekbar preview", ex);
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,14 +12,11 @@ import android.widget.Toast;
|
||||
import androidx.activity.result.ActivityResult;
|
||||
import androidx.activity.result.ActivityResultLauncher;
|
||||
import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.preference.Preference;
|
||||
import androidx.preference.PreferenceManager;
|
||||
|
||||
import com.nostra13.universalimageloader.core.ImageLoader;
|
||||
|
||||
import org.schabi.newpipe.DownloaderImpl;
|
||||
import org.schabi.newpipe.NewPipeDatabase;
|
||||
import org.schabi.newpipe.R;
|
||||
@@ -30,9 +27,11 @@ import org.schabi.newpipe.extractor.localization.ContentCountry;
|
||||
import org.schabi.newpipe.extractor.localization.Localization;
|
||||
import org.schabi.newpipe.streams.io.StoredFileHelper;
|
||||
import org.schabi.newpipe.util.NavigationHelper;
|
||||
import org.schabi.newpipe.util.PicassoHelper;
|
||||
import org.schabi.newpipe.util.ZipHelper;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
import java.util.Locale;
|
||||
@@ -43,16 +42,15 @@ import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
|
||||
|
||||
public class ContentSettingsFragment extends BasePreferenceFragment {
|
||||
private static final String ZIP_MIME_TYPE = "application/zip";
|
||||
private static final SimpleDateFormat EXPORT_DATE_FORMAT
|
||||
|
||||
private final SimpleDateFormat exportDateFormat
|
||||
= new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US);
|
||||
|
||||
private ContentSettingsManager manager;
|
||||
|
||||
private String importExportDataPathKey;
|
||||
private String thumbnailLoadToggleKey;
|
||||
private String youtubeRestrictedModeEnabledKey;
|
||||
|
||||
@Nullable private Uri lastImportExportDataUri = null;
|
||||
private Localization initialSelectedLocalization;
|
||||
private ContentCountry initialSelectedContentCountry;
|
||||
private String initialLanguage;
|
||||
@@ -69,7 +67,6 @@ public class ContentSettingsFragment extends BasePreferenceFragment {
|
||||
manager.deleteSettingsFile();
|
||||
|
||||
importExportDataPathKey = getString(R.string.import_export_data_path);
|
||||
thumbnailLoadToggleKey = getString(R.string.download_thumbnail_key);
|
||||
youtubeRestrictedModeEnabledKey = getString(R.string.youtube_restricted_mode_enabled);
|
||||
|
||||
addPreferencesFromResource(R.xml.content_settings);
|
||||
@@ -77,7 +74,8 @@ public class ContentSettingsFragment extends BasePreferenceFragment {
|
||||
final Preference importDataPreference = requirePreference(R.string.import_data);
|
||||
importDataPreference.setOnPreferenceClickListener((Preference p) -> {
|
||||
requestImportPathLauncher.launch(
|
||||
StoredFileHelper.getPicker(requireContext(), getImportExportDataUri()));
|
||||
StoredFileHelper.getPicker(requireContext(),
|
||||
ZIP_MIME_TYPE, getImportExportDataUri()));
|
||||
return true;
|
||||
});
|
||||
|
||||
@@ -86,7 +84,7 @@ public class ContentSettingsFragment extends BasePreferenceFragment {
|
||||
|
||||
requestExportPathLauncher.launch(
|
||||
StoredFileHelper.getNewPicker(requireContext(),
|
||||
"NewPipeData-" + EXPORT_DATE_FORMAT.format(new Date()) + ".zip",
|
||||
"NewPipeData-" + exportDateFormat.format(new Date()) + ".zip",
|
||||
ZIP_MIME_TYPE, getImportExportDataUri()));
|
||||
return true;
|
||||
});
|
||||
@@ -95,8 +93,7 @@ public class ContentSettingsFragment extends BasePreferenceFragment {
|
||||
.getPreferredLocalization(requireContext());
|
||||
initialSelectedContentCountry = org.schabi.newpipe.util.Localization
|
||||
.getPreferredContentCountry(requireContext());
|
||||
initialLanguage = PreferenceManager
|
||||
.getDefaultSharedPreferences(requireContext()).getString("app_language_key", "en");
|
||||
initialLanguage = defaultPreferences.getString(getString(R.string.app_language_key), "en");
|
||||
|
||||
final Preference clearCookiePref = requirePreference(R.string.clear_cookie_key);
|
||||
clearCookiePref.setOnPreferenceClickListener(preference -> {
|
||||
@@ -112,20 +109,24 @@ public class ContentSettingsFragment extends BasePreferenceFragment {
|
||||
if (defaultPreferences.getString(getString(R.string.recaptcha_cookies_key), "").isEmpty()) {
|
||||
clearCookiePref.setVisible(false);
|
||||
}
|
||||
|
||||
findPreference(getString(R.string.download_thumbnail_key)).setOnPreferenceChangeListener(
|
||||
(preference, newValue) -> {
|
||||
PicassoHelper.setShouldLoadImages((Boolean) newValue);
|
||||
try {
|
||||
PicassoHelper.clearCache(preference.getContext());
|
||||
Toast.makeText(preference.getContext(),
|
||||
R.string.thumbnail_cache_wipe_complete_notice, Toast.LENGTH_SHORT)
|
||||
.show();
|
||||
} catch (final IOException e) {
|
||||
Log.e(TAG, "Unable to clear Picasso cache", e);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onPreferenceTreeClick(final Preference preference) {
|
||||
if (preference.getKey().equals(thumbnailLoadToggleKey)) {
|
||||
final ImageLoader imageLoader = ImageLoader.getInstance();
|
||||
imageLoader.stop();
|
||||
imageLoader.clearDiskCache();
|
||||
imageLoader.clearMemoryCache();
|
||||
imageLoader.resume();
|
||||
Toast.makeText(preference.getContext(), R.string.thumbnail_cache_wipe_complete_notice,
|
||||
Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
|
||||
if (preference.getKey().equals(youtubeRestrictedModeEnabledKey)) {
|
||||
final Context context = getContext();
|
||||
if (context != null) {
|
||||
@@ -146,8 +147,8 @@ public class ContentSettingsFragment extends BasePreferenceFragment {
|
||||
.getPreferredLocalization(requireContext());
|
||||
final ContentCountry selectedContentCountry = org.schabi.newpipe.util.Localization
|
||||
.getPreferredContentCountry(requireContext());
|
||||
final String selectedLanguage = PreferenceManager
|
||||
.getDefaultSharedPreferences(requireContext()).getString("app_language_key", "en");
|
||||
final String selectedLanguage =
|
||||
defaultPreferences.getString(getString(R.string.app_language_key), "en");
|
||||
|
||||
if (!selectedLocalization.equals(initialSelectedLocalization)
|
||||
|| !selectedContentCountry.equals(initialSelectedContentCountry)
|
||||
@@ -162,27 +163,29 @@ public class ContentSettingsFragment extends BasePreferenceFragment {
|
||||
private void requestExportPathResult(final ActivityResult result) {
|
||||
assureCorrectAppLanguage(getContext());
|
||||
if (result.getResultCode() == Activity.RESULT_OK && result.getData() != null) {
|
||||
lastImportExportDataUri = result.getData().getData(); // will be saved only on success
|
||||
// will be saved only on success
|
||||
final Uri lastExportDataUri = result.getData().getData();
|
||||
|
||||
final StoredFileHelper file
|
||||
= new StoredFileHelper(getContext(), result.getData().getData(), ZIP_MIME_TYPE);
|
||||
|
||||
exportDatabase(file);
|
||||
exportDatabase(file, lastExportDataUri);
|
||||
}
|
||||
}
|
||||
|
||||
private void requestImportPathResult(final ActivityResult result) {
|
||||
assureCorrectAppLanguage(getContext());
|
||||
if (result.getResultCode() == Activity.RESULT_OK && result.getData() != null) {
|
||||
lastImportExportDataUri = result.getData().getData(); // will be saved only on success
|
||||
// will be saved only on success
|
||||
final Uri lastImportDataUri = result.getData().getData();
|
||||
|
||||
final StoredFileHelper file
|
||||
= new StoredFileHelper(getContext(), result.getData().getData(), ZIP_MIME_TYPE);
|
||||
|
||||
new AlertDialog.Builder(requireActivity())
|
||||
.setMessage(R.string.override_current_data)
|
||||
.setPositiveButton(R.string.finish, (d, id) ->
|
||||
importDatabase(file))
|
||||
.setPositiveButton(R.string.ok, (d, id) ->
|
||||
importDatabase(file, lastImportDataUri))
|
||||
.setNegativeButton(R.string.cancel, (d, id) ->
|
||||
d.cancel())
|
||||
.create()
|
||||
@@ -190,33 +193,33 @@ public class ContentSettingsFragment extends BasePreferenceFragment {
|
||||
}
|
||||
}
|
||||
|
||||
private void exportDatabase(final StoredFileHelper file) {
|
||||
private void exportDatabase(final StoredFileHelper file, final Uri exportDataUri) {
|
||||
try {
|
||||
//checkpoint before export
|
||||
NewPipeDatabase.checkpoint();
|
||||
|
||||
final SharedPreferences preferences = PreferenceManager
|
||||
.getDefaultSharedPreferences(requireContext());
|
||||
.getDefaultSharedPreferences(requireContext());
|
||||
manager.exportDatabase(preferences, file);
|
||||
|
||||
saveLastImportExportDataUri(false); // save export path only on success
|
||||
saveLastImportExportDataUri(exportDataUri); // save export path only on success
|
||||
Toast.makeText(getContext(), R.string.export_complete_toast, Toast.LENGTH_SHORT).show();
|
||||
} catch (final Exception e) {
|
||||
ErrorActivity.reportUiErrorInSnackbar(this, "Exporting database", e);
|
||||
}
|
||||
}
|
||||
|
||||
private void importDatabase(final StoredFileHelper file) {
|
||||
private void importDatabase(final StoredFileHelper file, final Uri importDataUri) {
|
||||
// check if file is supported
|
||||
if (!ZipHelper.isValidZipFile(file)) {
|
||||
Toast.makeText(getContext(), R.string.no_valid_zip_file, Toast.LENGTH_SHORT)
|
||||
.show();
|
||||
.show();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (!manager.ensureDbDirectoryExists()) {
|
||||
throw new Exception("Could not create databases dir");
|
||||
throw new IOException("Could not create databases dir");
|
||||
}
|
||||
|
||||
if (!manager.extractDb(file)) {
|
||||
@@ -229,19 +232,19 @@ public class ContentSettingsFragment extends BasePreferenceFragment {
|
||||
final AlertDialog.Builder alert = new AlertDialog.Builder(requireContext());
|
||||
alert.setTitle(R.string.import_settings);
|
||||
|
||||
alert.setNegativeButton(android.R.string.no, (dialog, which) -> {
|
||||
alert.setNegativeButton(R.string.cancel, (dialog, which) -> {
|
||||
dialog.dismiss();
|
||||
finishImport();
|
||||
finishImport(importDataUri);
|
||||
});
|
||||
alert.setPositiveButton(getString(R.string.finish), (dialog, which) -> {
|
||||
alert.setPositiveButton(R.string.ok, (dialog, which) -> {
|
||||
dialog.dismiss();
|
||||
manager.loadSharedPreferences(PreferenceManager
|
||||
.getDefaultSharedPreferences(requireContext()));
|
||||
finishImport();
|
||||
.getDefaultSharedPreferences(requireContext()));
|
||||
finishImport(importDataUri);
|
||||
});
|
||||
alert.show();
|
||||
} else {
|
||||
finishImport();
|
||||
finishImport(importDataUri);
|
||||
}
|
||||
} catch (final Exception e) {
|
||||
ErrorActivity.reportUiErrorInSnackbar(this, "Importing database", e);
|
||||
@@ -250,10 +253,12 @@ public class ContentSettingsFragment extends BasePreferenceFragment {
|
||||
|
||||
/**
|
||||
* Save import path and restart system.
|
||||
*
|
||||
* @param importDataUri The import path to save
|
||||
*/
|
||||
private void finishImport() {
|
||||
// save import path only on success; save immediately because app is about to exit
|
||||
saveLastImportExportDataUri(true);
|
||||
private void finishImport(final Uri importDataUri) {
|
||||
// save import path only on success
|
||||
saveLastImportExportDataUri(importDataUri);
|
||||
// restart app to properly load db
|
||||
NavigationHelper.restartApp(requireActivity());
|
||||
}
|
||||
@@ -263,16 +268,9 @@ public class ContentSettingsFragment extends BasePreferenceFragment {
|
||||
return isBlank(path) ? null : Uri.parse(path);
|
||||
}
|
||||
|
||||
private void saveLastImportExportDataUri(final boolean immediately) {
|
||||
if (lastImportExportDataUri != null) {
|
||||
final SharedPreferences.Editor editor = defaultPreferences.edit()
|
||||
.putString(importExportDataPathKey, lastImportExportDataUri.toString());
|
||||
if (immediately) {
|
||||
// noinspection ApplySharedPref
|
||||
editor.commit(); // app about to be restarted, commit immediately
|
||||
} else {
|
||||
editor.apply();
|
||||
}
|
||||
}
|
||||
private void saveLastImportExportDataUri(final Uri importExportDataUri) {
|
||||
final SharedPreferences.Editor editor = defaultPreferences.edit()
|
||||
.putString(importExportDataPathKey, importExportDataUri.toString());
|
||||
editor.apply();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,6 +91,11 @@ class ContentSettingsManager(private val fileLocator: NewPipeFileLocator) {
|
||||
is String -> {
|
||||
preferenceEditor.putString(key, value)
|
||||
}
|
||||
is Set<*> -> {
|
||||
// There are currently only Sets with type String possible
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
preferenceEditor.putStringSet(key, value as Set<String>?)
|
||||
}
|
||||
}
|
||||
}
|
||||
preferenceEditor.commit()
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user