mirror of
https://github.com/TeamNewPipe/NewPipe
synced 2026-01-15 05:58:02 +00:00
Compare commits
351 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7faa107547 | ||
|
|
85ccc2384f | ||
|
|
7153506ddb | ||
|
|
0483d3ff32 | ||
|
|
7e784ce9a7 | ||
|
|
8343d9cc18 | ||
|
|
db0ecd92ca | ||
|
|
b5140cfecd | ||
|
|
36aea35a92 | ||
|
|
1984436b41 | ||
|
|
8ba2f5f964 | ||
|
|
c923d35a1f | ||
|
|
90d3c9ced0 | ||
|
|
43cbc09f1f | ||
|
|
feea084c60 | ||
|
|
d403a83a24 | ||
|
|
35fc27cfb0 | ||
|
|
81742565a4 | ||
|
|
923d0b7c80 | ||
|
|
d416465371 | ||
|
|
2586c543d3 | ||
|
|
c32bc26328 | ||
|
|
4074c71b6a | ||
|
|
a7493d1039 | ||
|
|
d7cab6a8d8 | ||
|
|
eddc12693a | ||
|
|
ced3898499 | ||
|
|
318a5df109 | ||
|
|
b2e9981313 | ||
|
|
74f43639ad | ||
|
|
fc342bd458 | ||
|
|
adfbf5b49f | ||
|
|
2cb7bb84f7 | ||
|
|
84d1792e7f | ||
|
|
531859ac60 | ||
|
|
bf071d65d7 | ||
|
|
e4aa7a90c7 | ||
|
|
6d4e3c5633 | ||
|
|
19f9b4f502 | ||
|
|
f3dbb19364 | ||
|
|
0a831ec84e | ||
|
|
0c656abb8e | ||
|
|
7a7a90bf79 | ||
|
|
908dff3931 | ||
|
|
a786cff036 | ||
|
|
28802805f8 | ||
|
|
f59099395f | ||
|
|
0fe3fe7594 | ||
|
|
467dacd35a | ||
|
|
173150591d | ||
|
|
e4d94b1a4e | ||
|
|
75e34a5a8e | ||
|
|
d6121c8e21 | ||
|
|
b4d77df1be | ||
|
|
e6021465f6 | ||
|
|
22ec70e94d | ||
|
|
a1a70a94a8 | ||
|
|
a65ed7e914 | ||
|
|
4545b8e92d | ||
|
|
ba0c0fb109 | ||
|
|
18d530021c | ||
|
|
31bb70e333 | ||
|
|
a919a039e5 | ||
|
|
aacb1f46a8 | ||
|
|
96862cbcb3 | ||
|
|
10f79e1307 | ||
|
|
e0ee3dce40 | ||
|
|
13e7d2e7ac | ||
|
|
a7723373a0 | ||
|
|
7e469ead45 | ||
|
|
5397a4e410 | ||
|
|
99b59f0126 | ||
|
|
d46c7eb8fe | ||
|
|
e4a1fc9d95 | ||
|
|
276f50a944 | ||
|
|
40fcd93312 | ||
|
|
807e4d4af9 | ||
|
|
480348f11a | ||
|
|
30613b7064 | ||
|
|
79189dcc83 | ||
|
|
c2210330b6 | ||
|
|
917f459569 | ||
|
|
0ced9ba799 | ||
|
|
5e95277d7c | ||
|
|
efb417dba7 | ||
|
|
5c2d4c4d9d | ||
|
|
e8bd9920fd | ||
|
|
69ed531a5c | ||
|
|
b967d7c148 | ||
|
|
90150c42ed | ||
|
|
8e2fd9ccce | ||
|
|
567ffad41d | ||
|
|
3c306a0971 | ||
|
|
c0d6c8aeb3 | ||
|
|
b27b49e4f3 | ||
|
|
7ed0dbcf1a | ||
|
|
8a23de6b20 | ||
|
|
6cc3089204 | ||
|
|
093e95c078 | ||
|
|
7c8ac04e35 | ||
|
|
dc88f8b172 | ||
|
|
a00ac6b9ca | ||
|
|
c94f0ded27 | ||
|
|
b553aa2159 | ||
|
|
a7bd2666f0 | ||
|
|
fe2fc60581 | ||
|
|
ce59c05d5b | ||
|
|
a4858bc702 | ||
|
|
a2bb58a991 | ||
|
|
f7b41227d2 | ||
|
|
5b1a6831d5 | ||
|
|
42b1bbe414 | ||
|
|
ac86fe80c8 | ||
|
|
db9f20a22f | ||
|
|
b30e025bda | ||
|
|
5f3eb4871a | ||
|
|
9a223532c5 | ||
|
|
cf67b592da | ||
|
|
e867bfbc82 | ||
|
|
9a671851df | ||
|
|
4b92f78cc8 | ||
|
|
c585982557 | ||
|
|
6bf22e7ad0 | ||
|
|
2f8dccf7f6 | ||
|
|
027768d97d | ||
|
|
085f63b8c5 | ||
|
|
6f7c337e00 | ||
|
|
16a968f3bb | ||
|
|
d7e0167fed | ||
|
|
41c4f515cf | ||
|
|
d9a8218372 | ||
|
|
dd9bd4da8b | ||
|
|
cf98500b7f | ||
|
|
2ce8facc05 | ||
|
|
d1b117d07c | ||
|
|
c0377c7ebf | ||
|
|
a2490a5730 | ||
|
|
177334ba62 | ||
|
|
f7f00293cc | ||
|
|
7bce588767 | ||
|
|
4bb67c634f | ||
|
|
3653afbcc4 | ||
|
|
1f4a4ea09f | ||
|
|
3d38add4b4 | ||
|
|
124b7eefb5 | ||
|
|
b52924048c | ||
|
|
93393f5dff | ||
|
|
275a75ebaa | ||
|
|
3e4a7a19cc | ||
|
|
734af457f3 | ||
|
|
55bdb1f47a | ||
|
|
adff0d199d | ||
|
|
f95b3262a0 | ||
|
|
794a14e76c | ||
|
|
ba857b5ef7 | ||
|
|
2aed04a8c2 | ||
|
|
5f9e6b51da | ||
|
|
e7b5c99ed6 | ||
|
|
9c0b3d35be | ||
|
|
a54bc96eab | ||
|
|
a2a8e4b965 | ||
|
|
81ad2c61d9 | ||
|
|
32616493b3 | ||
|
|
05183ffd0f | ||
|
|
e72ddc9439 | ||
|
|
32e3caecac | ||
|
|
df43389183 | ||
|
|
19b77809ec | ||
|
|
be05b827f3 | ||
|
|
5dfc6f822d | ||
|
|
c3e004da03 | ||
|
|
8bae73b6ea | ||
|
|
d1e19d3b63 | ||
|
|
ffca897ddf | ||
|
|
4277b6e262 | ||
|
|
506c4ce701 | ||
|
|
d251e58984 | ||
|
|
4a1213c081 | ||
|
|
8b7609255c | ||
|
|
ef78fe0653 | ||
|
|
70b3ccb422 | ||
|
|
81d6b367fe | ||
|
|
0a78ae60be | ||
|
|
a61830a860 | ||
|
|
86bae9ddc9 | ||
|
|
033780862a | ||
|
|
6094d8a74e | ||
|
|
356ca3d177 | ||
|
|
d69806faa9 | ||
|
|
ab67635dcb | ||
|
|
cee3d49458 | ||
|
|
5b53a7aef7 | ||
|
|
9b29665cc0 | ||
|
|
f447c87b45 | ||
|
|
e3eea45d86 | ||
|
|
f61a06ce0a | ||
|
|
539842aa99 | ||
|
|
5925f1d2aa | ||
|
|
61eb150825 | ||
|
|
cf95de4d27 | ||
|
|
fdad7ec1ba | ||
|
|
850efb4237 | ||
|
|
853cb3887f | ||
|
|
412f2c1664 | ||
|
|
2810a69bd4 | ||
|
|
5347f95f50 | ||
|
|
6b469f0621 | ||
|
|
0021562c93 | ||
|
|
f2bd2b0a59 | ||
|
|
647eb8bbf5 | ||
|
|
816d13ae3f | ||
|
|
578fea4a9c | ||
|
|
1a660d9a4a | ||
|
|
227ac6d9e3 | ||
|
|
bb57407733 | ||
|
|
13ddcce0a2 | ||
|
|
53767a78d1 | ||
|
|
5600e8a2ad | ||
|
|
c6ed52c592 | ||
|
|
3ad14e4adf | ||
|
|
8a22bdea5d | ||
|
|
6135a3c3e2 | ||
|
|
1e3c979303 | ||
|
|
d0228406b6 | ||
|
|
507a2237b7 | ||
|
|
c15c597d99 | ||
|
|
7c26cd3270 | ||
|
|
938af73059 | ||
|
|
1c047366d2 | ||
|
|
cb20f0cbb0 | ||
|
|
468251c84e | ||
|
|
ca86ae0c9a | ||
|
|
59221b0b4e | ||
|
|
d3e0640400 | ||
|
|
bcb72321f5 | ||
|
|
4060af715d | ||
|
|
2ec0237e83 | ||
|
|
c5593880f2 | ||
|
|
3673cbce4f | ||
|
|
1f6f7be4b2 | ||
|
|
580cce3506 | ||
|
|
36ba546fc6 | ||
|
|
7f37799cbe | ||
|
|
5570eeeff9 | ||
|
|
2b186ce6e0 | ||
|
|
72938fed69 | ||
|
|
d54c806e03 | ||
|
|
7eb3551485 | ||
|
|
57abe27895 | ||
|
|
a628a36082 | ||
|
|
0d3e04ff25 | ||
|
|
0c78a3f7b0 | ||
|
|
fb1f574c26 | ||
|
|
7f15c18fca | ||
|
|
e274650956 | ||
|
|
2a1db4a338 | ||
|
|
0b6ea9ec61 | ||
|
|
257a826d45 | ||
|
|
e9f48f5134 | ||
|
|
fcf04624d4 | ||
|
|
16218d6dc5 | ||
|
|
071f33e3cd | ||
|
|
6d15389da8 | ||
|
|
2e8530ec00 | ||
|
|
4edd1c5497 | ||
|
|
8e693b8b42 | ||
|
|
29376066e8 | ||
|
|
a44f3071bf | ||
|
|
b66047e084 | ||
|
|
f0ca916432 | ||
|
|
c88b4032ef | ||
|
|
6f5e99be6f | ||
|
|
fd4c37e9b3 | ||
|
|
7a8dab2d58 | ||
|
|
6f3dfad550 | ||
|
|
18fb0a13d7 | ||
|
|
e88f9ae03b | ||
|
|
30c010ad3f | ||
|
|
e84e70bdc6 | ||
|
|
1e6b6165ae | ||
|
|
c6d149d091 | ||
|
|
d55d8d78de | ||
|
|
eff59f7b5e | ||
|
|
a7a5437245 | ||
|
|
6671b9e55b | ||
|
|
2dde1cc589 | ||
|
|
17866c29ae | ||
|
|
8dc4e6dc2a | ||
|
|
1197f44262 | ||
|
|
a86ed1f801 | ||
|
|
e98d3423e4 | ||
|
|
95333d37c8 | ||
|
|
8bcf0c6498 | ||
|
|
340b92e32b | ||
|
|
6e68ab19f9 | ||
|
|
15fed32d92 | ||
|
|
897c754dd4 | ||
|
|
ec1e746a22 | ||
|
|
001f078ba9 | ||
|
|
b5321152fd | ||
|
|
66d15ea635 | ||
|
|
72177033d2 | ||
|
|
06b7072240 | ||
|
|
4700f35739 | ||
|
|
bbfa280e86 | ||
|
|
2669ba944d | ||
|
|
c8788dbfbe | ||
|
|
5e7c3b53f8 | ||
|
|
99348c2300 | ||
|
|
ab2b9797fd | ||
|
|
637653ea11 | ||
|
|
04cb6ba3d0 | ||
|
|
eb1cddd85a | ||
|
|
723b230093 | ||
|
|
7185fae491 | ||
|
|
0274cd6beb | ||
|
|
ad2ea0b807 | ||
|
|
c24999075d | ||
|
|
773bde14ab | ||
|
|
00b08318a5 | ||
|
|
39e5d8ccc2 | ||
|
|
e25622df4b | ||
|
|
ea5939c1b7 | ||
|
|
4734d04d4f | ||
|
|
ef2b32eb05 | ||
|
|
1da91d44e1 | ||
|
|
ebd7ab3e46 | ||
|
|
99bddfdf0a | ||
|
|
144f48c9a6 | ||
|
|
8d0d2ba07b | ||
|
|
a6ad334dc0 | ||
|
|
468ca30070 | ||
|
|
7b2d2d9338 | ||
|
|
0e08819cf3 | ||
|
|
1d3f7b49dc | ||
|
|
3566ec7012 | ||
|
|
f37a36efa4 | ||
|
|
2ea069cd8c | ||
|
|
08e111f6dc | ||
|
|
6339881684 | ||
|
|
e222538575 | ||
|
|
c4a67ce420 | ||
|
|
77e348ba62 | ||
|
|
bde39d8c37 | ||
|
|
7a432b38e9 | ||
|
|
9b6a201bbb | ||
|
|
745773b207 | ||
|
|
ebe9f518d0 | ||
|
|
37ceddd11b | ||
|
|
7805f8a9b1 | ||
|
|
b822c5a039 |
69
.github/CONTRIBUTING.md
vendored
69
.github/CONTRIBUTING.md
vendored
@@ -1,65 +1,46 @@
|
||||
NewPipe contribution guidelines
|
||||
===============================
|
||||
|
||||
PLEASE READ THESE GUIDELINES CAREFULLY BEFORE ANY CONTRIBUTION!
|
||||
|
||||
## Crash reporting
|
||||
|
||||
Do not report crashes in the GitHub issue tracker. NewPipe has an automated crash report system that will ask you to
|
||||
send a report via e-mail when a crash occurs. This contains all the data we need for debugging, and allows you to even
|
||||
add a comment to it. You'll see exactly what is sent, the system is 100% transparent.
|
||||
Report crashes through the automated crash report system of NewPipe.
|
||||
This way all the data needed for debugging is included in your bugreport for GitHub.
|
||||
You'll see exactly what is sent, be able to add your comments, and then send it.
|
||||
|
||||
## Issue reporting/feature requests
|
||||
|
||||
* Search the [existing issues](https://github.com/TeamNewPipe/NewPipe/issues) first to make sure your issue/feature
|
||||
hasn't been reported/requested before.
|
||||
* Check whether your issue/feature is already fixed/implemented.
|
||||
* Check if the issue still exists in the latest release/beta version.
|
||||
* If you are an Android/Java developer, you are always welcome to fix an issue or implement a feature yourself. PRs welcome!
|
||||
* We use English for development. Issues in other languages will be closed and ignored.
|
||||
* Please only add *one* issue at a time. Do not put multiple issues into one thread.
|
||||
* Follow the template! Issues or feature requests not matching the template might be closed.
|
||||
* **Already reported**? Browse the [existing issues](https://github.com/TeamNewPipe/NewPipe/issues) to make sure your issue/feature hasn't been reported/requested.
|
||||
* **Already fixed**? Check whether your issue/feature is already fixed/implemented.
|
||||
* **Still relevant**? Check if the issue still exists in the latest release/beta version.
|
||||
* **Can you fix it**? If you are an Android/Java developer, you are always welcome to fix an issue or implement a feature yourself. PRs welcome! See [Code contribution](#code-contribution) for more info.
|
||||
* **Is it in English**? Issues in other languages will be ignored unless someone translates them.
|
||||
* **Is it one issue**? Multiple issues require multiple reports, that can be linked to track their statuses.
|
||||
* **The template**: Fill it out, everyone wins. Your issue has a chance of getting fixed.
|
||||
|
||||
## Bug Fixing
|
||||
* If you want to help NewPipe to become free of bugs (this is our utopic goal for NewPipe), you can send us an email to
|
||||
<a href="mailto:tnp@newpipe.schabi.org">tnp@newpipe.schabi.org</a> to let us know that you intend to help. We'll send you further instructions. You may, on request,
|
||||
register at our [Sentry](https://sentry.schabi.org) instance (see section "Crash reporting" for more information).
|
||||
|
||||
## Translation
|
||||
|
||||
* NewPipe is translated via [Weblate](https://hosted.weblate.org/projects/newpipe/strings/). You can log in there
|
||||
with your GitHub account.
|
||||
* If the language you want to translate is not on Weblate, you can add it: see [How to add a new language](https://github.com/TeamNewPipe/NewPipe/wiki/How-to-add-a-new-language-to-NewPipe) in the wiki.
|
||||
* NewPipe is translated via [Weblate](https://hosted.weblate.org/projects/newpipe/strings/). Log in there with your GitHub account, or register.
|
||||
* Add the language you want to translate if it is not there already: see [How to add a new language](https://github.com/TeamNewPipe/NewPipe/wiki/How-to-add-a-new-language-to-NewPipe) in the wiki.
|
||||
|
||||
## Code contribution
|
||||
|
||||
* If you want to add a feature or change one, please open an issue describing your change. This gives the team and community a chance to give feedback before you spend any time on something that could be done differently or not done at all. It also prevents two contributors from working on the same thing and one being disappointed when only one user's code can be added.
|
||||
* Stick to NewPipe's style conventions: follow [checkStyle](https://github.com/checkstyle/checkstyle). It will run each time you build the project.
|
||||
* Do not bring non-free software (e.g. binary blobs) into the project. Also, make sure you do not introduce Google
|
||||
* If you want to help out with an existing bug report or feature request, leave a comment on that issue saying you want to try your hand at it.
|
||||
* If there is no existing issue for what you want to work on, open a new one describing your changes. This gives the team and the community a chance to give feedback before you spend time on something that is already in development, should be done differently, or should be avoided completely.
|
||||
* Stick to NewPipe's style conventions of [checkStyle](https://github.com/checkstyle/checkstyle). It runs each time you build the project.
|
||||
* Do not bring non-free software (e.g. binary blobs) into the project. Make sure you do not introduce Google
|
||||
libraries.
|
||||
* Stick to [F-Droid contribution guidelines](https://f-droid.org/wiki/page/Inclusion_Policy).
|
||||
* Make changes on a separate branch with a meaningful name, not on the master neither dev branch. This is commonly known as *feature branch workflow*. You
|
||||
may then send your changes as a pull request (PR) on GitHub.
|
||||
* When submitting changes, you confirm that your code is licensed under the terms of the
|
||||
[GNU General Public License v3](https://www.gnu.org/licenses/gpl-3.0.html).
|
||||
* Please test (compile and run) your code before you submit changes! Ideally, provide test feedback in the PR
|
||||
description. Untested code will **not** be merged!
|
||||
* 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.
|
||||
* Please test (compile and run) your code before submitting changes! Ideally, provide test feedback in the PR description. Untested code will **not** be merged!
|
||||
* Make sure your PR is up-to-date with the rest of the code. Often, a simple click on "Update branch" will do the job, but if not, you must rebase the dev branch manually and resolve the problems on your own. You can find help [on the wiki](https://github.com/TeamNewPipe/NewPipe/wiki/How-to-merge-a-PR). That makes the maintainers' jobs way easier.
|
||||
* Please show intention to maintain your features and code after you contribute a PR. Unmaintained code is a hassle for core developers. If you do not intend to maintain features you plan to contribute, please rethink your submission, or clearly state that in the PR description.
|
||||
* Respond if someone requests changes or otherwise raises issues about your PRs.
|
||||
* Send PRs that only cover one specific issue/solution/bug. Do not send PRs that are huge and consist of multiple independent solutions.
|
||||
* Try to figure out yourself why builds on our CI fail.
|
||||
* Make sure your PR is up-to-date with the rest of the code. Often, a simple click on "Update branch" will do the job,
|
||||
but if not, you are asked to rebase the dev branch manually and resolve the problems on your own. You can find help [on the wiki](https://github.com/TeamNewPipe/NewPipe/wiki/How-to-merge-a-PR). That will make the
|
||||
maintainers' jobs way easier.
|
||||
* Please show intention to maintain your features and code after you contributed it. Unmaintained code is a hassle for
|
||||
the core developers, and just adds work. If you do not intend to maintain features you contributed, please think again
|
||||
about submission, or clearly state that in the description of your PR.
|
||||
* Respond yourselves if someone requests changes or otherwise raises issues about your PRs.
|
||||
* Send PR that only cover one specific issue/solution/bug. Do not send PRs that are huge and consists of multiple
|
||||
independent solutions.
|
||||
|
||||
## Communication
|
||||
|
||||
* There is an IRC channel on Freenode which is regularly visited by the core team and other developers:
|
||||
[#newpipe](irc:irc.freenode.net/newpipe). [Click here for Webchat](https://webchat.freenode.net/?channels=newpipe)!
|
||||
* If you want to get in touch with the core team or one of our other contributors you can send an email to
|
||||
<a href="mailto:tnp@newpipe.schabi.org">tnp@newpipe.schabi.org</a>. Please do not send issue reports, they will be ignored and remain unanswered! Use the GitHub issue
|
||||
tracker described above!
|
||||
* Feel free to post suggestions, changes, ideas etc. on GitHub or IRC!
|
||||
* The [#newpipe](irc:irc.freenode.net/newpipe) channel on freenode has the core team and other developers in it. [Click here for webchat](https://webchat.freenode.net/?channels=newpipe)!
|
||||
* You can also use a Matrix account to join the Newpipe channel at [#freenode_#newpipe:matrix.org](https://matrix.to/#/#freenode_#newpipe:matrix.org).
|
||||
* Post suggestions, changes, ideas etc. on GitHub or IRC.
|
||||
|
||||
30
.github/workflows/ci.yml
vendored
Normal file
30
.github/workflows/ci.yml
vendored
Normal file
@@ -0,0 +1,30 @@
|
||||
name: CI
|
||||
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
build-and-test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: set up JDK 1.8
|
||||
uses: actions/setup-java@v1.4.3
|
||||
with:
|
||||
java-version: 1.8
|
||||
|
||||
- name: Cache Gradle dependencies
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ~/.gradle/caches
|
||||
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }}
|
||||
restore-keys: ${{ runner.os }}-gradle
|
||||
|
||||
- name: Build debug APK and run Tests
|
||||
run: ./gradlew assembleDebug lintDebug testDebugUnitTest --stacktrace
|
||||
|
||||
- name: Upload APK
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: app
|
||||
path: app/build/outputs/apk/debug/*.apk
|
||||
18
.travis.yml
18
.travis.yml
@@ -1,18 +0,0 @@
|
||||
language: android
|
||||
jdk:
|
||||
- oraclejdk8
|
||||
android:
|
||||
components:
|
||||
# The BuildTools version used by NewPipe
|
||||
- tools
|
||||
- build-tools-29.0.3
|
||||
|
||||
# The SDK version used to compile NewPipe
|
||||
- android-29
|
||||
|
||||
before_install:
|
||||
- yes | sdkmanager "platforms;android-29"
|
||||
script: ./gradlew -Dorg.gradle.jvmargs=-Xmx1536m assembleDebug lintDebug testDebugUnitTest
|
||||
|
||||
licenses:
|
||||
- '.+'
|
||||
12
README.ko.md
12
README.ko.md
@@ -1,4 +1,4 @@
|
||||
<p align="center"><a href="https://newpipe.schabi.org"><img src="assets/new_pipe_icon_5.png" width="150"></a></p>
|
||||
<p align="center"><a href="https://newpipe.net"><img src="assets/new_pipe_icon_5.png" width="150"></a></p>
|
||||
<h2 align="center"><b>NewPipe</b></h2>
|
||||
<h4 align="center">A libre lightweight streaming frontend for Android.</h4>
|
||||
<p align="center"><a href="https://f-droid.org/packages/org.schabi.newpipe/"><img src="https://f-droid.org/wiki/images/0/06/F-Droid-button_get-it-on.png"></a></p>
|
||||
@@ -6,14 +6,14 @@
|
||||
<p align="center">
|
||||
<a href="https://github.com/TeamNewPipe/NewPipe/releases" alt="GitHub release"><img src="https://img.shields.io/github/release/TeamNewPipe/NewPipe.svg" ></a>
|
||||
<a href="https://www.gnu.org/licenses/gpl-3.0" alt="License: GPLv3"><img src="https://img.shields.io/badge/License-GPL%20v3-blue.svg"></a>
|
||||
<a href="https://travis-ci.org/TeamNewPipe/NewPipe" alt="Build Status"><img src="https://travis-ci.org/TeamNewPipe/NewPipe.svg"></a>
|
||||
<a href="https://github.com/TeamNewPipe/NewPipe/actions" alt="Build Status"><img src="https://github.com/TeamNewPipe/NewPipe/workflows/CI/badge.svg?branch=dev&event=push"></a>
|
||||
<a href="https://hosted.weblate.org/engage/newpipe/" alt="Translation Status"><img src="https://hosted.weblate.org/widgets/newpipe/-/svg-badge.svg"></a>
|
||||
<a href="http://webchat.freenode.net/?channels=%23newpipe" alt="IRC channel: #newpipe"><img src="https://img.shields.io/badge/IRC%20chat-%23newpipe-brightgreen.svg"></a>
|
||||
<a href="https://www.bountysource.com/teams/newpipe" alt="Bountysource bounties"><img src="https://img.shields.io/bountysource/team/newpipe/activity.svg?colorB=cd201f"></a>
|
||||
</p>
|
||||
<hr>
|
||||
<p align="center"><a href="#screenshots">Screenshots</a> • <a href="#description">Description</a> • <a href="#features">Features</a> • <a href="#updates">Updates</a> • <a href="#contribution">Contribution</a> • <a href="#donate">Donate</a> • <a href="#license">License</a></p>
|
||||
<p align="center"><a href="https://newpipe.schabi.org">Website</a> • <a href="https://newpipe.schabi.org/blog/">Blog</a> • <a href="https://newpipe.schabi.org/FAQ/">FAQ</a> • <a href="https://newpipe.schabi.org/press/">Press</a></p>
|
||||
<p align="center"><a href="https://newpipe.net">Website</a> • <a href="https://newpipe.net/blog/">Blog</a> • <a href="https://newpipe.net/FAQ/">FAQ</a> • <a href="https://newpipe.net/press/">Press</a></p>
|
||||
<hr>
|
||||
|
||||
*Read this in other languages: [English](README.md), [한국어](README.ko.md).*
|
||||
@@ -86,7 +86,7 @@ NewPipe 코드의 변경이 있을 때(기능 추가 또는 버그 수정으로
|
||||
1. 직접 디버그 APK를 생성할 수 있습니다. 이 방법은 당신의 기기에서 새로운 기능을 얻을 수 있는 가장 빠른 방법이지만, 꽤 많이 복잡합니다.
|
||||
따라서 우리는 다른 방법들 중 하나를 사용하는 것을 추천합니다.
|
||||
2. 우리의 커스텀 저장소를 F-Droid에 추가하고 우리가 릴리즈를 게시하는 대로 저곳에서 릴리즈를 설치할 수 있습니다.
|
||||
이에 대한 설명서는 이곳에서 확인할 수 있습니다: https://newpipe.schabi.org/FAQ/tutorials/install-add-fdroid-repo/
|
||||
이에 대한 설명서는 이곳에서 확인할 수 있습니다: https://newpipe.net/FAQ/tutorials/install-add-fdroid-repo/
|
||||
3. 우리가 릴리즈를 게시하는 대로 [Github Releases](https://github.com/TeamNewPipe/NewPipe/releases)에서 APK를 다운받고 이것을 설치할 수 있습니다.
|
||||
4. F-Droid를 통해 업데이트 할 수 있습니다. F-Droid는 변화를 인식하고, 스스로 APK를 생성하고, 이것에 서명하고, 사용자들에서 업데이트를 전달해야만 하기 때문에,
|
||||
이것은 업데이트를 받는 가장 느린 방법입니다.
|
||||
@@ -111,7 +111,7 @@ NewPipe 코드의 변경이 있을 때(기능 추가 또는 버그 수정으로
|
||||
</a>
|
||||
|
||||
## Donate
|
||||
만약 NewPipe가 마음에 들었다면, 우리는 기부에 대해 기꺼이 환영합니다. bitcoin을 보내거나, Bountysource 또는 Liberapay를 통해 기부할 수 있습니다. NewPipe에 기부하는 것에 대한 자세한 정보를 원한다면, 우리의 [웹사이트](https://newpipe.schabi.org/donate)를 방문하여 주십시오.
|
||||
만약 NewPipe가 마음에 들었다면, 우리는 기부에 대해 기꺼이 환영합니다. bitcoin을 보내거나, Bountysource 또는 Liberapay를 통해 기부할 수 있습니다. NewPipe에 기부하는 것에 대한 자세한 정보를 원한다면, 우리의 [웹사이트](https://newpipe.net/donate)를 방문하여 주십시오.
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
@@ -134,7 +134,7 @@ NewPipe 코드의 변경이 있을 때(기능 추가 또는 버그 수정으로
|
||||
## Privacy Policy
|
||||
|
||||
NewPipe 프로젝트는 미디어 웹 서비스를 사용하는 것에 대한 사적의, 익명의 경험을 제공하는 것을 목표로 하고 있습니다.
|
||||
그러므로, 앱은 당신의 동의 없이 어떤 데이터도 수집하지 않습니다. NewPipe의 개인정보보호정책은 당신이 충돌 리포트를 보내거나, 또는 우리의 블로그에 글을 남길 때 어떤 데이터가 보내지고 저장되는지에 대해 상세히 설명합니다. 이 문서는 [여기](https://newpipe.schabi.org/legal/privacy/)에서 확인할 수 있습니다.
|
||||
그러므로, 앱은 당신의 동의 없이 어떤 데이터도 수집하지 않습니다. NewPipe의 개인정보보호정책은 당신이 충돌 리포트를 보내거나, 또는 우리의 블로그에 글을 남길 때 어떤 데이터가 보내지고 저장되는지에 대해 상세히 설명합니다. 이 문서는 [여기](https://newpipe.net/legal/privacy/)에서 확인할 수 있습니다.
|
||||
|
||||
## License
|
||||
[](http://www.gnu.org/licenses/gpl-3.0.en.html)
|
||||
|
||||
16
README.md
16
README.md
@@ -1,4 +1,4 @@
|
||||
<p align="center"><a href="https://newpipe.schabi.org"><img src="assets/new_pipe_icon_5.png" width="150"></a></p>
|
||||
<p align="center"><a href="https://newpipe.net"><img src="assets/new_pipe_icon_5.png" width="150"></a></p>
|
||||
<h2 align="center"><b>NewPipe</b></h2>
|
||||
<h4 align="center">A libre lightweight streaming frontend for Android.</h4>
|
||||
<p align="center"><a href="https://f-droid.org/packages/org.schabi.newpipe/"><img src="https://f-droid.org/wiki/images/0/06/F-Droid-button_get-it-on.png"></a></p>
|
||||
@@ -6,21 +6,21 @@
|
||||
<p align="center">
|
||||
<a href="https://github.com/TeamNewPipe/NewPipe/releases" alt="GitHub release"><img src="https://img.shields.io/github/release/TeamNewPipe/NewPipe.svg" ></a>
|
||||
<a href="https://www.gnu.org/licenses/gpl-3.0" alt="License: GPLv3"><img src="https://img.shields.io/badge/License-GPL%20v3-blue.svg"></a>
|
||||
<a href="https://travis-ci.org/TeamNewPipe/NewPipe" alt="Build Status"><img src="https://travis-ci.org/TeamNewPipe/NewPipe.svg"></a>
|
||||
<a href="https://github.com/TeamNewPipe/NewPipe/actions" alt="Build Status"><img src="https://github.com/TeamNewPipe/NewPipe/workflows/CI/badge.svg?branch=dev&event=push"></a>
|
||||
<a href="https://hosted.weblate.org/engage/newpipe/" alt="Translation Status"><img src="https://hosted.weblate.org/widgets/newpipe/-/svg-badge.svg"></a>
|
||||
<a href="http://webchat.freenode.net/?channels=%23newpipe" alt="IRC channel: #newpipe"><img src="https://img.shields.io/badge/IRC%20chat-%23newpipe-brightgreen.svg"></a>
|
||||
<a href="https://www.bountysource.com/teams/newpipe" alt="Bountysource bounties"><img src="https://img.shields.io/bountysource/team/newpipe/activity.svg?colorB=cd201f"></a>
|
||||
</p>
|
||||
<hr>
|
||||
<p align="center"><a href="#screenshots">Screenshots</a> • <a href="#description">Description</a> • <a href="#features">Features</a> • <a href="#updates">Updates</a> • <a href="#contribution">Contribution</a> • <a href="#donate">Donate</a> • <a href="#license">License</a></p>
|
||||
<p align="center"><a href="https://newpipe.schabi.org">Website</a> • <a href="https://newpipe.schabi.org/blog/">Blog</a> • <a href="https://newpipe.schabi.org/FAQ/">FAQ</a> • <a href="https://newpipe.schabi.org/press/">Press</a></p>
|
||||
<p align="center"><a href="https://newpipe.net">Website</a> • <a href="https://newpipe.net/blog/">Blog</a> • <a href="https://newpipe.net/FAQ/">FAQ</a> • <a href="https://newpipe.net/press/">Press</a></p>
|
||||
<hr>
|
||||
|
||||
*Read this in other languages: [English](README.md), [한국어](README.ko.md).*
|
||||
|
||||
<b>WARNING: THIS IS A BETA VERSION, THEREFORE YOU MAY ENCOUNTER BUGS. IF YOU DO, OPEN AN ISSUE VIA OUR GITHUB REPOSITORY.</b>
|
||||
|
||||
<b>PUTTING NEWPIPE OR ANY FORK OF IT INTO GOOGLE PLAYSTORE VIOLATES THEIR TERMS OF CONDITIONS.</b>
|
||||
<b>PUTTING NEWPIPE OR ANY FORK OF IT INTO THE GOOGLE PLAY STORE VIOLATES THEIR TERMS AND CONDITIONS.</b>
|
||||
|
||||
## Screenshots
|
||||
|
||||
@@ -83,11 +83,11 @@ NewPipe supports multiple services. Our [docs](https://teamnewpipe.github.io/doc
|
||||
## Updates
|
||||
When a change to the NewPipe code occurs (due to either adding features or bug fixing), eventually a release will occur. These are in the format x.xx.x . In order to get this new version, you can:
|
||||
1. Build a debug APK yourself. This is the fastest way to get new features on your device, but is much more complicated, so we recommend using one of the other methods.
|
||||
2. Add our custom repo to F-Droid and install it from there as soon as we publish a release. The instructions are here: https://newpipe.schabi.org/FAQ/tutorials/install-add-fdroid-repo/
|
||||
2. Add our custom repo to F-Droid and install it from there as soon as we publish a release. The instructions are here: https://newpipe.net/FAQ/tutorials/install-add-fdroid-repo/
|
||||
3. Download the APK from [Github Releases](https://github.com/TeamNewPipe/NewPipe/releases) and install it as soon as we publish a release.
|
||||
4. Update via F-droid. This is the slowest method of getting updates, as F-Droid must recognize changes, build the APK itself, sign it, then push the update to users.
|
||||
|
||||
We recommend method 2 for most users. APKs installed using method 2 or 3 are compatible with each other, but not with those installed using method 4. This is due to the same signing key (ours) being using for 2 and 3, but a different signing key (F-Droid's) being used for 4. Building a debug APK using method 1 excludes a key entirely. Signing keys help ensure that a user isn't tricked into installing a malicious update to an app.
|
||||
We recommend method 2 for most users. APKs installed using method 2 or 3 are compatible with each other, but not with those installed using method 4. This is due to the same signing key (ours) being used for 2 and 3, but a different signing key (F-Droid's) being used for 4. Building a debug APK using method 1 excludes a key entirely. Signing keys help ensure that a user isn't tricked into installing a malicious update to an app.
|
||||
|
||||
In the meanwhile, if you want to switch sources for some reason (e.g. NewPipe's core functionality was broken and F-Droid doesn't have the update yet), we recommend following this procedure:
|
||||
1. Back up your data via Settings > Content > Export Database so you keep your history, subscriptions, and playlists
|
||||
@@ -106,7 +106,7 @@ If you'd like to get involved, check our [contribution notes](.github/CONTRIBUTI
|
||||
</a>
|
||||
|
||||
## Donate
|
||||
If you like NewPipe we'd be happy about a donation. You can either send bitcoin or donate via Bountysource or Liberapay. For further info on donating to NewPipe, please visit our [website](https://newpipe.schabi.org/donate).
|
||||
If you like NewPipe we'd be happy about a donation. You can either send bitcoin or donate via Bountysource or Liberapay. For further info on donating to NewPipe, please visit our [website](https://newpipe.net/donate).
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
@@ -129,7 +129,7 @@ If you like NewPipe we'd be happy about a donation. You can either send bitcoin
|
||||
## Privacy Policy
|
||||
|
||||
The NewPipe project aims to provide a private, anonymous experience for using media web services.
|
||||
Therefore, the app does not collect any data without your consent. NewPipe's privacy policy explains in detail what data is sent and stored when you send a crash report, or comment in our blog. You can find the document [here](https://newpipe.schabi.org/legal/privacy/).
|
||||
Therefore, the app does not collect any data without your consent. NewPipe's privacy policy explains in detail what data is sent and stored when you send a crash report, or comment in our blog. You can find the document [here](https://newpipe.net/legal/privacy/).
|
||||
|
||||
## License
|
||||
[](http://www.gnu.org/licenses/gpl-3.0.en.html)
|
||||
|
||||
102
app/build.gradle
102
app/build.gradle
@@ -13,8 +13,8 @@ android {
|
||||
resValue "string", "app_name", "NewPipe"
|
||||
minSdkVersion 19
|
||||
targetSdkVersion 29
|
||||
versionCode 957
|
||||
versionName "0.20.3"
|
||||
versionCode 963
|
||||
versionName "0.20.9"
|
||||
|
||||
multiDexEnabled true
|
||||
|
||||
@@ -85,24 +85,28 @@ android {
|
||||
sourceSets {
|
||||
androidTest.assets.srcDirs += files("$projectDir/schemas".toString())
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
viewBinding true
|
||||
}
|
||||
}
|
||||
|
||||
ext {
|
||||
icepickVersion = '3.2.0'
|
||||
checkstyleVersion = '8.36.2'
|
||||
checkstyleVersion = '8.38'
|
||||
stethoVersion = '1.5.1'
|
||||
leakCanaryVersion = '2.2'
|
||||
leakCanaryVersion = '2.5'
|
||||
exoPlayerVersion = '2.11.8'
|
||||
androidxLifecycleVersion = '2.2.0'
|
||||
androidxRoomVersion = '2.2.5'
|
||||
groupieVersion = '2.8.0'
|
||||
markwonVersion = '4.3.1'
|
||||
androidxRoomVersion = '2.3.0-alpha03'
|
||||
groupieVersion = '2.8.1'
|
||||
markwonVersion = '4.6.0'
|
||||
googleAutoServiceVersion = '1.0-rc7'
|
||||
}
|
||||
|
||||
configurations {
|
||||
checkstyle
|
||||
// ktlint
|
||||
ktlint
|
||||
}
|
||||
|
||||
checkstyle {
|
||||
@@ -130,81 +134,86 @@ task runCheckstyle(type: Checkstyle) {
|
||||
}
|
||||
}
|
||||
|
||||
//task runKtlint(type: JavaExec) {
|
||||
// main = "com.pinterest.ktlint.Main"
|
||||
// classpath = configurations.ktlint
|
||||
// args "src/**/*.kt"
|
||||
//}
|
||||
//
|
||||
//task formatKtlint(type: JavaExec) {
|
||||
// main = "com.pinterest.ktlint.Main"
|
||||
// classpath = configurations.ktlint
|
||||
// args "-F", "src/**/*.kt"
|
||||
//}
|
||||
def outputDir = "${project.buildDir}/reports/ktlint/"
|
||||
def inputFiles = project.fileTree(dir: "src", include: "**/*.kt")
|
||||
|
||||
task runKtlint(type: JavaExec) {
|
||||
inputs.files(inputFiles)
|
||||
outputs.dir(outputDir)
|
||||
main = "com.pinterest.ktlint.Main"
|
||||
classpath = configurations.ktlint
|
||||
args "src/**/*.kt"
|
||||
}
|
||||
|
||||
task formatKtlint(type: JavaExec) {
|
||||
inputs.files(inputFiles)
|
||||
outputs.dir(outputDir)
|
||||
main = "com.pinterest.ktlint.Main"
|
||||
classpath = configurations.ktlint
|
||||
args "-F", "src/**/*.kt"
|
||||
}
|
||||
|
||||
afterEvaluate {
|
||||
preDebugBuild.dependsOn runCheckstyle //, runKtlint
|
||||
preDebugBuild.dependsOn formatKtlint, runCheckstyle, runKtlint
|
||||
}
|
||||
|
||||
dependencies {
|
||||
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.0.10'
|
||||
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.1'
|
||||
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:${kotlin_version}"
|
||||
|
||||
implementation "frankiesardo:icepick:${icepickVersion}"
|
||||
kapt "frankiesardo:icepick-processor:${icepickVersion}"
|
||||
|
||||
checkstyle "com.puppycrawl.tools:checkstyle:${checkstyleVersion}"
|
||||
// ktlint "com.pinterest:ktlint:0.35.0"
|
||||
ktlint "com.pinterest:ktlint:0.40.0"
|
||||
|
||||
debugImplementation "com.facebook.stetho:stetho:${stethoVersion}"
|
||||
debugImplementation "com.facebook.stetho:stetho-okhttp3:${stethoVersion}"
|
||||
|
||||
debugImplementation "com.squareup.leakcanary:leakcanary-android:${leakCanaryVersion}"
|
||||
implementation "com.squareup.leakcanary:leakcanary-object-watcher-android:${leakCanaryVersion}"
|
||||
implementation "com.squareup.leakcanary:plumber-android:${leakCanaryVersion}"
|
||||
|
||||
implementation "androidx.multidex:multidex:2.0.1"
|
||||
|
||||
testImplementation 'junit:junit:4.13.1'
|
||||
testImplementation 'org.mockito:mockito-core:3.3.3'
|
||||
|
||||
androidTestImplementation "androidx.test.ext:junit:1.1.1"
|
||||
androidTestImplementation "androidx.room:room-testing:${androidxRoomVersion}"
|
||||
androidTestImplementation "androidx.test.espresso:espresso-core:3.2.0", {
|
||||
exclude module: 'support-annotations'
|
||||
}
|
||||
|
||||
// NewPipe dependencies
|
||||
// You can use a local version by uncommenting a few lines in settings.gradle
|
||||
implementation 'com.github.TeamNewPipe:NewPipeExtractor:6701b0fe718f6bdc385221341fa473e8aaab560e'
|
||||
implementation 'com.github.TeamNewPipe:NewPipeExtractor:v0.20.9'
|
||||
implementation "com.github.TeamNewPipe:nanojson:1d9e1aea9049fc9f85e68b43ba39fe7be1c1f751"
|
||||
|
||||
implementation "org.jsoup:jsoup:1.13.1"
|
||||
|
||||
//noinspection GradleDependency --> do not update okhttp to keep supporting Android 4.4 users
|
||||
implementation "com.squareup.okhttp3:okhttp:3.12.12"
|
||||
|
||||
implementation "com.google.android.exoplayer:exoplayer:${exoPlayerVersion}"
|
||||
implementation "com.google.android.exoplayer:extension-mediasession:${exoPlayerVersion}"
|
||||
|
||||
implementation "com.google.android.material:material:1.1.0"
|
||||
implementation "com.google.android.material:material:1.2.1"
|
||||
|
||||
compileOnly "com.google.auto.service:auto-service-annotations:${googleAutoServiceVersion}"
|
||||
kapt "com.google.auto.service:auto-service:${googleAutoServiceVersion}"
|
||||
|
||||
implementation "androidx.appcompat:appcompat:1.1.0"
|
||||
implementation "androidx.appcompat:appcompat:1.2.0"
|
||||
implementation "androidx.preference:preference:1.1.1"
|
||||
implementation "androidx.recyclerview:recyclerview:1.1.0"
|
||||
implementation "androidx.cardview:cardview:1.0.0"
|
||||
implementation "androidx.constraintlayout:constraintlayout:1.1.3"
|
||||
implementation 'androidx.core:core-ktx:1.3.1'
|
||||
implementation "androidx.constraintlayout:constraintlayout:2.0.4"
|
||||
implementation 'androidx.core:core-ktx:1.3.2'
|
||||
implementation 'androidx.documentfile:documentfile:1.0.1'
|
||||
implementation 'androidx.localbroadcastmanager:localbroadcastmanager:1.0.0'
|
||||
implementation 'androidx.webkit:webkit:1.4.0'
|
||||
|
||||
implementation "androidx.lifecycle:lifecycle-livedata:${androidxLifecycleVersion}"
|
||||
implementation "androidx.lifecycle:lifecycle-viewmodel:${androidxLifecycleVersion}"
|
||||
|
||||
implementation "androidx.room:room-runtime:${androidxRoomVersion}"
|
||||
implementation "androidx.room:room-rxjava2:${androidxRoomVersion}"
|
||||
implementation "androidx.room:room-rxjava3:${androidxRoomVersion}"
|
||||
kapt "androidx.room:room-compiler:${androidxRoomVersion}"
|
||||
|
||||
implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0"
|
||||
|
||||
implementation "com.xwray:groupie:${groupieVersion}"
|
||||
implementation "com.xwray:groupie-kotlin-android-extensions:${groupieVersion}"
|
||||
|
||||
@@ -216,13 +225,22 @@ dependencies {
|
||||
|
||||
implementation "com.nononsenseapps:filepicker:4.2.1"
|
||||
|
||||
implementation "ch.acra:acra-core:5.5.0"
|
||||
implementation "ch.acra:acra-core:5.7.0"
|
||||
|
||||
implementation "io.reactivex.rxjava2:rxjava:2.2.19"
|
||||
implementation "io.reactivex.rxjava2:rxandroid:2.1.1"
|
||||
implementation "com.jakewharton.rxbinding2:rxbinding:2.2.0"
|
||||
implementation "io.reactivex.rxjava3:rxjava:3.0.7"
|
||||
implementation "io.reactivex.rxjava3:rxandroid:3.0.0"
|
||||
implementation "com.jakewharton.rxbinding4:rxbinding:4.0.0"
|
||||
|
||||
implementation "org.ocpsoft.prettytime:prettytime:4.0.6.Final"
|
||||
|
||||
testImplementation 'junit:junit:4.13.1'
|
||||
testImplementation 'org.mockito:mockito-core:3.6.0'
|
||||
|
||||
androidTestImplementation "androidx.test.ext:junit:1.1.2"
|
||||
androidTestImplementation "androidx.room:room-testing:${androidxRoomVersion}"
|
||||
androidTestImplementation "androidx.test.espresso:espresso-core:3.3.0", {
|
||||
exclude module: 'support-annotations'
|
||||
}
|
||||
}
|
||||
|
||||
static String getGitWorkingBranch() {
|
||||
|
||||
@@ -31,49 +31,62 @@ class AppDatabaseTest {
|
||||
}
|
||||
|
||||
@get:Rule
|
||||
val testHelper = MigrationTestHelper(InstrumentationRegistry.getInstrumentation(),
|
||||
AppDatabase::class.java.canonicalName, FrameworkSQLiteOpenHelperFactory())
|
||||
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)
|
||||
})
|
||||
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)
|
||||
testHelper.runMigrationsAndValidate(
|
||||
AppDatabase.DATABASE_NAME, Migrations.DB_VER_3,
|
||||
true, Migrations.MIGRATION_2_3
|
||||
)
|
||||
|
||||
val migratedDatabaseV3 = getMigratedDatabase()
|
||||
val listFromDB = migratedDatabaseV3.streamDAO().all.blockingFirst()
|
||||
@@ -110,9 +123,11 @@ class AppDatabaseTest {
|
||||
}
|
||||
|
||||
private fun getMigratedDatabase(): AppDatabase {
|
||||
val database: AppDatabase = Room.databaseBuilder(ApplicationProvider.getApplicationContext(),
|
||||
AppDatabase::class.java, AppDatabase.DATABASE_NAME)
|
||||
.build()
|
||||
val database: AppDatabase = Room.databaseBuilder(
|
||||
ApplicationProvider.getApplicationContext(),
|
||||
AppDatabase::class.java, AppDatabase.DATABASE_NAME
|
||||
)
|
||||
.build()
|
||||
testHelper.closeWhenFinished(database)
|
||||
return database
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ import androidx.test.filters.LargeTest;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.report.ErrorActivity.ErrorInfo;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
@@ -29,10 +28,10 @@ public class ErrorInfoTest {
|
||||
parcel.setDataPosition(0);
|
||||
final ErrorInfo infoFromParcel = ErrorInfo.CREATOR.createFromParcel(parcel);
|
||||
|
||||
assertEquals(UserAction.USER_REPORT, infoFromParcel.userAction);
|
||||
assertEquals("youtube", infoFromParcel.serviceName);
|
||||
assertEquals("request", infoFromParcel.request);
|
||||
assertEquals(R.string.general_error, infoFromParcel.message);
|
||||
assertEquals(UserAction.USER_REPORT, infoFromParcel.getUserAction());
|
||||
assertEquals("youtube", infoFromParcel.getServiceName());
|
||||
assertEquals("request", infoFromParcel.getRequest());
|
||||
assertEquals(R.string.general_error, infoFromParcel.getMessage());
|
||||
|
||||
parcel.recycle();
|
||||
}
|
||||
|
||||
@@ -15,14 +15,22 @@ class DebugApp : App() {
|
||||
|
||||
// Give each object 10 seconds to be GC'ed, before LeakCanary gets nosy on it
|
||||
AppWatcher.config = AppWatcher.config.copy(watchDurationMillis = 10000)
|
||||
LeakCanary.config = LeakCanary.config.copy(dumpHeap = PreferenceManager
|
||||
.getDefaultSharedPreferences(this).getBoolean(getString(
|
||||
R.string.allow_heap_dumping_key), false))
|
||||
LeakCanary.config = LeakCanary.config.copy(
|
||||
dumpHeap = PreferenceManager
|
||||
.getDefaultSharedPreferences(this).getBoolean(
|
||||
getString(
|
||||
R.string.allow_heap_dumping_key
|
||||
),
|
||||
false
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
override fun getDownloader(): Downloader {
|
||||
val downloader = DownloaderImpl.init(OkHttpClient.Builder()
|
||||
.addNetworkInterceptor(StethoInterceptor()))
|
||||
val downloader = DownloaderImpl.init(
|
||||
OkHttpClient.Builder()
|
||||
.addNetworkInterceptor(StethoInterceptor())
|
||||
)
|
||||
setCookiesToDownloader(downloader)
|
||||
return downloader
|
||||
}
|
||||
@@ -36,7 +44,8 @@ class DebugApp : App() {
|
||||
|
||||
// Enable command line interface
|
||||
initializerBuilder.enableDumpapp(
|
||||
Stetho.defaultDumperPluginsProvider(applicationContext))
|
||||
Stetho.defaultDumperPluginsProvider(applicationContext)
|
||||
)
|
||||
|
||||
// Use the InitializerBuilder to generate an Initializer
|
||||
val initializer = initializerBuilder.build()
|
||||
@@ -47,6 +56,6 @@ class DebugApp : App() {
|
||||
|
||||
override fun isDisposedRxExceptionsReported(): Boolean {
|
||||
return PreferenceManager.getDefaultSharedPreferences(this)
|
||||
.getBoolean(getString(R.string.allow_disposed_exceptions_key), false)
|
||||
.getBoolean(getString(R.string.allow_disposed_exceptions_key), false)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -227,20 +227,18 @@
|
||||
<data android:host="invidio.us" />
|
||||
<data android:host="dev.invidio.us" />
|
||||
<data android:host="www.invidio.us" />
|
||||
<data android:host="vid.encryptionin.space" />
|
||||
<data android:host="invidious.snopyta.org" />
|
||||
<data android:host="fi.invidious.snopyta.org" />
|
||||
<data android:host="yewtu.be" />
|
||||
<data android:host="invidious.ggc-project.de" />
|
||||
<data android:host="yt.maisputain.ovh" />
|
||||
<data android:host="invidious.13ad.de" />
|
||||
<data android:host="invidious.toot.koeln" />
|
||||
<data android:host="tube.connect.cafe" />
|
||||
<data android:host="invidious.zapashcanon.fr" />
|
||||
<data android:host="invidious.kavin.rocks" />
|
||||
<data android:host="invidious.tube" />
|
||||
<data android:host="invidious.site" />
|
||||
<data android:host="invidious.xyz" />
|
||||
<data android:host="vid.mint.lgbt" />
|
||||
<data android:host="invidiou.site" />
|
||||
<data android:host="invidious.fdn.fr" />
|
||||
<data android:host="watch.nettohikari.com" />
|
||||
<data android:host="invidious.snwmds.net" />
|
||||
<data android:host="invidious.snwmds.org" />
|
||||
<data android:host="invidious.snwmds.com" />
|
||||
<data android:host="invidious.sunsetravens.com" />
|
||||
<data android:host="invidious.gachirangers.com" />
|
||||
<data android:pathPrefix="/" />
|
||||
</intent-filter>
|
||||
|
||||
@@ -268,7 +266,7 @@
|
||||
<data android:mimeType="text/plain" />
|
||||
</intent-filter>
|
||||
|
||||
<!-- MediaCCC filter -->
|
||||
<!-- media.ccc.de filter -->
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<action android:name="android.media.action.MEDIA_PLAY_FROM_SEARCH" />
|
||||
@@ -305,8 +303,7 @@
|
||||
<data android:host="peertube.cpy.re" />
|
||||
<data android:host="peertube.mastodon.host" />
|
||||
<data android:host="peertube.fr" />
|
||||
<data android:host="peertube.live" />
|
||||
<data android:host="peertube.video" />
|
||||
<data android:host="tilvids.com" />
|
||||
<data android:host="tube.privacytools.io" />
|
||||
<data android:host="video.ploud.fr" />
|
||||
<data android:host="video.lqdn.fr" />
|
||||
|
||||
@@ -86,8 +86,8 @@ public abstract class FragmentStatePagerAdapterMenuWorkaround extends PagerAdapt
|
||||
private final int mBehavior;
|
||||
private FragmentTransaction mCurTransaction = null;
|
||||
|
||||
private ArrayList<Fragment.SavedState> mSavedState = new ArrayList<Fragment.SavedState>();
|
||||
private ArrayList<Fragment> mFragments = new ArrayList<Fragment>();
|
||||
private final ArrayList<Fragment.SavedState> mSavedState = new ArrayList<Fragment.SavedState>();
|
||||
private final ArrayList<Fragment> mFragments = new ArrayList<Fragment>();
|
||||
private Fragment mCurrentPrimaryItem = null;
|
||||
|
||||
/**
|
||||
|
||||
@@ -8,6 +8,7 @@ import android.os.Build;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.multidex.MultiDexApplication;
|
||||
import androidx.preference.PreferenceManager;
|
||||
|
||||
@@ -22,6 +23,7 @@ import org.acra.config.CoreConfigurationBuilder;
|
||||
import org.schabi.newpipe.extractor.NewPipe;
|
||||
import org.schabi.newpipe.extractor.downloader.Downloader;
|
||||
import org.schabi.newpipe.report.ErrorActivity;
|
||||
import org.schabi.newpipe.report.ErrorInfo;
|
||||
import org.schabi.newpipe.report.UserAction;
|
||||
import org.schabi.newpipe.settings.SettingsActivity;
|
||||
import org.schabi.newpipe.util.ExceptionUtils;
|
||||
@@ -36,13 +38,13 @@ import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import io.reactivex.disposables.Disposable;
|
||||
import io.reactivex.exceptions.CompositeException;
|
||||
import io.reactivex.exceptions.MissingBackpressureException;
|
||||
import io.reactivex.exceptions.OnErrorNotImplementedException;
|
||||
import io.reactivex.exceptions.UndeliverableException;
|
||||
import io.reactivex.functions.Consumer;
|
||||
import io.reactivex.plugins.RxJavaPlugins;
|
||||
import io.reactivex.rxjava3.disposables.Disposable;
|
||||
import io.reactivex.rxjava3.exceptions.CompositeException;
|
||||
import io.reactivex.rxjava3.exceptions.MissingBackpressureException;
|
||||
import io.reactivex.rxjava3.exceptions.OnErrorNotImplementedException;
|
||||
import io.reactivex.rxjava3.exceptions.UndeliverableException;
|
||||
import io.reactivex.rxjava3.functions.Consumer;
|
||||
import io.reactivex.rxjava3.plugins.RxJavaPlugins;
|
||||
|
||||
/*
|
||||
* Copyright (C) Hans-Christoph Steiner 2016 <hans@eds.org>
|
||||
@@ -66,7 +68,7 @@ public class App extends MultiDexApplication {
|
||||
protected static final String TAG = App.class.toString();
|
||||
private static App app;
|
||||
|
||||
private Disposable disposable = null;
|
||||
@Nullable private Disposable disposable = null;
|
||||
|
||||
@NonNull
|
||||
public static App getApp() {
|
||||
@@ -226,7 +228,7 @@ public class App extends MultiDexApplication {
|
||||
ace,
|
||||
null,
|
||||
null,
|
||||
ErrorActivity.ErrorInfo.make(UserAction.SOMETHING_ELSE, "none",
|
||||
ErrorInfo.make(UserAction.SOMETHING_ELSE, "none",
|
||||
"Could not initialize ACRA crash report", R.string.app_ui_crash));
|
||||
}
|
||||
}
|
||||
@@ -240,8 +242,9 @@ public class App extends MultiDexApplication {
|
||||
String name = getString(R.string.notification_channel_name);
|
||||
String description = getString(R.string.notification_channel_description);
|
||||
|
||||
// Keep this below DEFAULT to avoid making noise on every notification update
|
||||
final int importance = NotificationManager.IMPORTANCE_LOW;
|
||||
// Keep this below DEFAULT to avoid making noise on every notification update for the main
|
||||
// and update channels
|
||||
int importance = NotificationManager.IMPORTANCE_LOW;
|
||||
|
||||
final NotificationChannel mainChannel = new NotificationChannel(id, name, importance);
|
||||
mainChannel.setDescription(description);
|
||||
@@ -253,9 +256,17 @@ public class App extends MultiDexApplication {
|
||||
final NotificationChannel appUpdateChannel = new NotificationChannel(id, name, importance);
|
||||
appUpdateChannel.setDescription(description);
|
||||
|
||||
id = getString(R.string.hash_channel_id);
|
||||
name = getString(R.string.hash_channel_name);
|
||||
description = getString(R.string.hash_channel_description);
|
||||
importance = NotificationManager.IMPORTANCE_HIGH;
|
||||
|
||||
final NotificationChannel hashChannel = new NotificationChannel(id, name, importance);
|
||||
hashChannel.setDescription(description);
|
||||
|
||||
final NotificationManager notificationManager = getSystemService(NotificationManager.class);
|
||||
notificationManager.createNotificationChannels(Arrays.asList(mainChannel,
|
||||
appUpdateChannel));
|
||||
appUpdateChannel, hashChannel));
|
||||
}
|
||||
|
||||
protected boolean isDisposedRxExceptionsReported() {
|
||||
|
||||
@@ -12,6 +12,7 @@ import android.net.Uri;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.app.NotificationCompat;
|
||||
import androidx.core.app.NotificationManagerCompat;
|
||||
import androidx.core.content.ContextCompat;
|
||||
@@ -21,12 +22,11 @@ import com.grack.nanojson.JsonObject;
|
||||
import com.grack.nanojson.JsonParser;
|
||||
import com.grack.nanojson.JsonParserException;
|
||||
|
||||
import org.schabi.newpipe.extractor.exceptions.ReCaptchaException;
|
||||
import org.schabi.newpipe.report.ErrorActivity;
|
||||
import org.schabi.newpipe.report.ErrorInfo;
|
||||
import org.schabi.newpipe.report.UserAction;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
@@ -35,11 +35,10 @@ import java.security.cert.CertificateException;
|
||||
import java.security.cert.CertificateFactory;
|
||||
import java.security.cert.X509Certificate;
|
||||
|
||||
import io.reactivex.Observable;
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.disposables.Disposable;
|
||||
import io.reactivex.disposables.Disposables;
|
||||
import io.reactivex.schedulers.Schedulers;
|
||||
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() { }
|
||||
@@ -49,56 +48,51 @@ public final class CheckForNewAppVersion {
|
||||
|
||||
private static final String GITHUB_APK_SHA1
|
||||
= "B0:2E:90:7C:1C:D6:FC:57:C3:35:F0:88:D0:8F:50:5F:94:E4:D2:15";
|
||||
private static final String NEWPIPE_API_URL = "https://newpipe.schabi.org/api/data.json";
|
||||
private static final String NEWPIPE_API_URL = "https://newpipe.net/api/data.json";
|
||||
|
||||
/**
|
||||
* Method to get the apk's SHA1 key. See https://stackoverflow.com/questions/9293019/#22506133.
|
||||
* Method to get the APK's SHA1 key. See https://stackoverflow.com/questions/9293019/#22506133.
|
||||
*
|
||||
* @param application The application
|
||||
* @return String with the apk's SHA1 fingeprint in hexadecimal
|
||||
* @return String with the APK's SHA1 fingerprint in hexadecimal
|
||||
*/
|
||||
@NonNull
|
||||
private static String getCertificateSHA1Fingerprint(@NonNull final Application application) {
|
||||
final PackageManager pm = application.getPackageManager();
|
||||
final String packageName = application.getPackageName();
|
||||
final int flags = PackageManager.GET_SIGNATURES;
|
||||
PackageInfo packageInfo = null;
|
||||
|
||||
final PackageInfo packageInfo;
|
||||
try {
|
||||
packageInfo = pm.getPackageInfo(packageName, flags);
|
||||
packageInfo = application.getPackageManager().getPackageInfo(
|
||||
application.getPackageName(), PackageManager.GET_SIGNATURES);
|
||||
} catch (final PackageManager.NameNotFoundException e) {
|
||||
ErrorActivity.reportError(application, e, null, null,
|
||||
ErrorActivity.ErrorInfo.make(UserAction.SOMETHING_ELSE, "none",
|
||||
ErrorInfo.make(UserAction.SOMETHING_ELSE, "none",
|
||||
"Could not find package info", R.string.app_ui_crash));
|
||||
return "";
|
||||
}
|
||||
|
||||
final Signature[] signatures = packageInfo.signatures;
|
||||
final byte[] cert = signatures[0].toByteArray();
|
||||
final InputStream input = new ByteArrayInputStream(cert);
|
||||
|
||||
X509Certificate c = null;
|
||||
|
||||
final X509Certificate c;
|
||||
try {
|
||||
final Signature[] signatures = packageInfo.signatures;
|
||||
final byte[] cert = signatures[0].toByteArray();
|
||||
final InputStream input = new ByteArrayInputStream(cert);
|
||||
final CertificateFactory cf = CertificateFactory.getInstance("X509");
|
||||
c = (X509Certificate) cf.generateCertificate(input);
|
||||
} catch (final CertificateException e) {
|
||||
ErrorActivity.reportError(application, e, null, null,
|
||||
ErrorActivity.ErrorInfo.make(UserAction.SOMETHING_ELSE, "none",
|
||||
ErrorInfo.make(UserAction.SOMETHING_ELSE, "none",
|
||||
"Certificate error", R.string.app_ui_crash));
|
||||
return "";
|
||||
}
|
||||
|
||||
String hexString = null;
|
||||
|
||||
try {
|
||||
final MessageDigest md = MessageDigest.getInstance("SHA1");
|
||||
final byte[] publicKey = md.digest(c.getEncoded());
|
||||
hexString = byte2HexFormatted(publicKey);
|
||||
return byte2HexFormatted(publicKey);
|
||||
} catch (NoSuchAlgorithmException | CertificateEncodingException e) {
|
||||
ErrorActivity.reportError(application, e, null, null,
|
||||
ErrorActivity.ErrorInfo.make(UserAction.SOMETHING_ELSE, "none",
|
||||
ErrorInfo.make(UserAction.SOMETHING_ELSE, "none",
|
||||
"Could not retrieve SHA1 key", R.string.app_ui_crash));
|
||||
return "";
|
||||
}
|
||||
|
||||
return hexString;
|
||||
}
|
||||
|
||||
private static String byte2HexFormatted(final byte[] arr) {
|
||||
@@ -163,66 +157,66 @@ public final class CheckForNewAppVersion {
|
||||
}
|
||||
|
||||
private static boolean isConnected(@NonNull final App app) {
|
||||
final ConnectivityManager cm = ContextCompat.getSystemService(app,
|
||||
ConnectivityManager.class);
|
||||
return cm.getActiveNetworkInfo() != null
|
||||
&& cm.getActiveNetworkInfo().isConnected();
|
||||
final ConnectivityManager connectivityManager =
|
||||
ContextCompat.getSystemService(app, ConnectivityManager.class);
|
||||
return connectivityManager != null && connectivityManager.getActiveNetworkInfo() != null
|
||||
&& connectivityManager.getActiveNetworkInfo().isConnected();
|
||||
}
|
||||
|
||||
public static boolean isGithubApk(@NonNull final App app) {
|
||||
return getCertificateSHA1Fingerprint(app).equals(GITHUB_APK_SHA1);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Nullable
|
||||
public static Disposable checkNewVersion(@NonNull final App app) {
|
||||
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(app);
|
||||
|
||||
// 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 Disposables.empty();
|
||||
if (!prefs.getBoolean(app.getString(R.string.update_app_key), true) || !isGithubApk(app)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return Observable.fromCallable(() -> {
|
||||
if (!isConnected(app)) {
|
||||
return null;
|
||||
}
|
||||
return Maybe
|
||||
.fromCallable(() -> {
|
||||
if (!isConnected(app)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Make a network request to get latest NewPipe data.
|
||||
try {
|
||||
return DownloaderImpl.getInstance().get(NEWPIPE_API_URL).responseBody();
|
||||
} catch (IOException | ReCaptchaException e) {
|
||||
// connectivity problems, do not alarm user and fail silently
|
||||
if (DEBUG) {
|
||||
Log.w(TAG, Log.getStackTraceString(e));
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
})
|
||||
// Make a network request to get latest NewPipe data.
|
||||
return DownloaderImpl.getInstance().get(NEWPIPE_API_URL).responseBody();
|
||||
})
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(response -> {
|
||||
// Parse the json from the response.
|
||||
if (response != null) {
|
||||
try {
|
||||
final JsonObject githubStableObject = JsonParser.object().from(response)
|
||||
.getObject("flavors").getObject("github").getObject("stable");
|
||||
.subscribe(
|
||||
response -> {
|
||||
// Parse the json from the response.
|
||||
try {
|
||||
final JsonObject githubStableObject = JsonParser.object()
|
||||
.from(response).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) {
|
||||
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, Log.getStackTraceString(e));
|
||||
Log.w(TAG, "Could not get NewPipe API: network problem", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,8 +50,8 @@ public final class DownloaderImpl extends Downloader {
|
||||
public static final String YOUTUBE_DOMAIN = "youtube.com";
|
||||
|
||||
private static DownloaderImpl instance;
|
||||
private Map<String, String> mCookies;
|
||||
private OkHttpClient client;
|
||||
private final Map<String, String> mCookies;
|
||||
private final OkHttpClient client;
|
||||
|
||||
private DownloaderImpl(final OkHttpClient.Builder builder) {
|
||||
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.KITKAT) {
|
||||
|
||||
@@ -39,17 +39,14 @@ import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.AdapterView;
|
||||
import android.widget.ArrayAdapter;
|
||||
import android.widget.Button;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.Spinner;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
import androidx.appcompat.app.ActionBarDrawerToggle;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.core.app.ActivityCompat;
|
||||
import androidx.core.view.GravityCompat;
|
||||
import androidx.drawerlayout.widget.DrawerLayout;
|
||||
import androidx.fragment.app.Fragment;
|
||||
@@ -57,8 +54,12 @@ import androidx.fragment.app.FragmentManager;
|
||||
import androidx.preference.PreferenceManager;
|
||||
|
||||
import com.google.android.material.bottomsheet.BottomSheetBehavior;
|
||||
import com.google.android.material.navigation.NavigationView;
|
||||
|
||||
import org.schabi.newpipe.databinding.ActivityMainBinding;
|
||||
import org.schabi.newpipe.databinding.DrawerHeaderBinding;
|
||||
import org.schabi.newpipe.databinding.DrawerLayoutBinding;
|
||||
import org.schabi.newpipe.databinding.InstanceSpinnerLayoutBinding;
|
||||
import org.schabi.newpipe.databinding.ToolbarLayoutBinding;
|
||||
import org.schabi.newpipe.extractor.NewPipe;
|
||||
import org.schabi.newpipe.extractor.StreamingService;
|
||||
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||
@@ -96,15 +97,14 @@ public class MainActivity extends AppCompatActivity {
|
||||
private static final String TAG = "MainActivity";
|
||||
public static final boolean DEBUG = !BuildConfig.BUILD_TYPE.equals("release");
|
||||
|
||||
private ActivityMainBinding mainBinding;
|
||||
private DrawerHeaderBinding drawerHeaderBinding;
|
||||
private DrawerLayoutBinding drawerLayoutBinding;
|
||||
private ToolbarLayoutBinding toolbarLayoutBinding;
|
||||
|
||||
private ActionBarDrawerToggle toggle;
|
||||
private DrawerLayout drawer;
|
||||
private NavigationView drawerItems;
|
||||
private ImageView headerServiceIcon;
|
||||
private TextView headerServiceView;
|
||||
private Button toggleServiceButton;
|
||||
|
||||
private boolean servicesShown = false;
|
||||
private ImageView serviceArrow;
|
||||
|
||||
private BroadcastReceiver broadcastReceiver;
|
||||
|
||||
@@ -129,7 +129,7 @@ public class MainActivity extends AppCompatActivity {
|
||||
+ "savedInstanceState = [" + savedInstanceState + "]");
|
||||
}
|
||||
|
||||
// enable TLS1.1/1.2 for kitkat devices, to fix download and play for mediaCCC sources
|
||||
// enable TLS1.1/1.2 for kitkat devices, to fix download and play for media.ccc.de sources
|
||||
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.KITKAT) {
|
||||
TLSSocketFactoryCompat.setAsDefault();
|
||||
}
|
||||
@@ -137,14 +137,19 @@ public class MainActivity extends AppCompatActivity {
|
||||
|
||||
assureCorrectAppLanguage(this);
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_main);
|
||||
|
||||
if (getSupportFragmentManager() != null
|
||||
&& getSupportFragmentManager().getBackStackEntryCount() == 0) {
|
||||
mainBinding = ActivityMainBinding.inflate(getLayoutInflater());
|
||||
drawerLayoutBinding = mainBinding.drawerLayout;
|
||||
drawerHeaderBinding = DrawerHeaderBinding.bind(drawerLayoutBinding.navigation
|
||||
.getHeaderView(0));
|
||||
toolbarLayoutBinding = mainBinding.toolbarLayout;
|
||||
setContentView(mainBinding.getRoot());
|
||||
|
||||
if (getSupportFragmentManager().getBackStackEntryCount() == 0) {
|
||||
initFragments();
|
||||
}
|
||||
|
||||
setSupportActionBar(findViewById(R.id.toolbar));
|
||||
setSupportActionBar(toolbarLayoutBinding.toolbar);
|
||||
try {
|
||||
setupDrawer();
|
||||
} catch (final Exception e) {
|
||||
@@ -158,10 +163,6 @@ public class MainActivity extends AppCompatActivity {
|
||||
}
|
||||
|
||||
private void setupDrawer() throws Exception {
|
||||
final Toolbar toolbar = findViewById(R.id.toolbar);
|
||||
drawer = findViewById(R.id.drawer_layout);
|
||||
drawerItems = findViewById(R.id.navigation);
|
||||
|
||||
//Tabs
|
||||
final int currentServiceId = ServiceHelper.getSelectedServiceId(this);
|
||||
final StreamingService service = NewPipe.getService(currentServiceId);
|
||||
@@ -169,43 +170,43 @@ public class MainActivity extends AppCompatActivity {
|
||||
int kioskId = 0;
|
||||
|
||||
for (final String ks : service.getKioskList().getAvailableKiosks()) {
|
||||
drawerItems.getMenu()
|
||||
drawerLayoutBinding.navigation.getMenu()
|
||||
.add(R.id.menu_tabs_group, kioskId, 0, KioskTranslator
|
||||
.getTranslatedKioskName(ks, this))
|
||||
.setIcon(KioskTranslator.getKioskIcon(ks, this));
|
||||
kioskId++;
|
||||
}
|
||||
|
||||
drawerItems.getMenu()
|
||||
drawerLayoutBinding.navigation.getMenu()
|
||||
.add(R.id.menu_tabs_group, ITEM_ID_SUBSCRIPTIONS, ORDER,
|
||||
R.string.tab_subscriptions)
|
||||
.setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.ic_channel));
|
||||
drawerItems.getMenu()
|
||||
drawerLayoutBinding.navigation.getMenu()
|
||||
.add(R.id.menu_tabs_group, ITEM_ID_FEED, ORDER, R.string.fragment_feed_title)
|
||||
.setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.ic_rss));
|
||||
drawerItems.getMenu()
|
||||
drawerLayoutBinding.navigation.getMenu()
|
||||
.add(R.id.menu_tabs_group, ITEM_ID_BOOKMARKS, ORDER, R.string.tab_bookmarks)
|
||||
.setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.ic_bookmark));
|
||||
drawerItems.getMenu()
|
||||
drawerLayoutBinding.navigation.getMenu()
|
||||
.add(R.id.menu_tabs_group, ITEM_ID_DOWNLOADS, ORDER, R.string.downloads)
|
||||
.setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.ic_file_download));
|
||||
drawerItems.getMenu()
|
||||
drawerLayoutBinding.navigation.getMenu()
|
||||
.add(R.id.menu_tabs_group, ITEM_ID_HISTORY, ORDER, R.string.action_history)
|
||||
.setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.ic_history));
|
||||
|
||||
//Settings and About
|
||||
drawerItems.getMenu()
|
||||
drawerLayoutBinding.navigation.getMenu()
|
||||
.add(R.id.menu_options_about_group, ITEM_ID_SETTINGS, ORDER, R.string.settings)
|
||||
.setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.ic_settings));
|
||||
drawerItems.getMenu()
|
||||
drawerLayoutBinding.navigation.getMenu()
|
||||
.add(R.id.menu_options_about_group, ITEM_ID_ABOUT, ORDER, R.string.tab_about)
|
||||
.setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.ic_info_outline));
|
||||
|
||||
toggle = new ActionBarDrawerToggle(this, drawer, toolbar, R.string.drawer_open,
|
||||
R.string.drawer_close);
|
||||
toggle = new ActionBarDrawerToggle(this, mainBinding.getRoot(),
|
||||
toolbarLayoutBinding.toolbar, R.string.drawer_open, R.string.drawer_close);
|
||||
toggle.syncState();
|
||||
drawer.addDrawerListener(toggle);
|
||||
drawer.addDrawerListener(new DrawerLayout.SimpleDrawerListener() {
|
||||
mainBinding.getRoot().addDrawerListener(toggle);
|
||||
mainBinding.getRoot().addDrawerListener(new DrawerLayout.SimpleDrawerListener() {
|
||||
private int lastService;
|
||||
|
||||
@Override
|
||||
@@ -219,12 +220,12 @@ public class MainActivity extends AppCompatActivity {
|
||||
toggleServices();
|
||||
}
|
||||
if (lastService != ServiceHelper.getSelectedServiceId(MainActivity.this)) {
|
||||
new Handler(Looper.getMainLooper()).post(MainActivity.this::recreate);
|
||||
ActivityCompat.recreate(MainActivity.this);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
drawerItems.setNavigationItemSelectedListener(this::drawerItemSelected);
|
||||
drawerLayoutBinding.navigation.setNavigationItemSelectedListener(this::drawerItemSelected);
|
||||
setupDrawerHeader();
|
||||
}
|
||||
|
||||
@@ -247,15 +248,17 @@ public class MainActivity extends AppCompatActivity {
|
||||
return false;
|
||||
}
|
||||
|
||||
drawer.closeDrawers();
|
||||
mainBinding.getRoot().closeDrawers();
|
||||
return true;
|
||||
}
|
||||
|
||||
private void changeService(final MenuItem item) {
|
||||
drawerItems.getMenu().getItem(ServiceHelper.getSelectedServiceId(this))
|
||||
drawerLayoutBinding.navigation.getMenu()
|
||||
.getItem(ServiceHelper.getSelectedServiceId(this))
|
||||
.setChecked(false);
|
||||
ServiceHelper.setSelectedServiceId(this, item.getItemId());
|
||||
drawerItems.getMenu().getItem(ServiceHelper.getSelectedServiceId(this))
|
||||
drawerLayoutBinding.navigation.getMenu()
|
||||
.getItem(ServiceHelper.getSelectedServiceId(this))
|
||||
.setChecked(true);
|
||||
}
|
||||
|
||||
@@ -307,26 +310,19 @@ public class MainActivity extends AppCompatActivity {
|
||||
}
|
||||
|
||||
private void setupDrawerHeader() {
|
||||
final NavigationView navigationView = findViewById(R.id.navigation);
|
||||
final View hView = navigationView.getHeaderView(0);
|
||||
|
||||
serviceArrow = hView.findViewById(R.id.drawer_arrow);
|
||||
headerServiceIcon = hView.findViewById(R.id.drawer_header_service_icon);
|
||||
headerServiceView = hView.findViewById(R.id.drawer_header_service_view);
|
||||
toggleServiceButton = hView.findViewById(R.id.drawer_header_action_button);
|
||||
toggleServiceButton.setOnClickListener(view -> toggleServices());
|
||||
drawerHeaderBinding.drawerHeaderActionButton.setOnClickListener(view -> toggleServices());
|
||||
|
||||
// If the current app name is bigger than the default "NewPipe" (7 chars),
|
||||
// let the text view grow a little more as well.
|
||||
if (getString(R.string.app_name).length() > "NewPipe".length()) {
|
||||
final TextView headerTitle = hView.findViewById(R.id.drawer_header_newpipe_title);
|
||||
final ViewGroup.LayoutParams layoutParams = headerTitle.getLayoutParams();
|
||||
final ViewGroup.LayoutParams layoutParams =
|
||||
drawerHeaderBinding.drawerHeaderNewpipeTitle.getLayoutParams();
|
||||
layoutParams.width = ViewGroup.LayoutParams.WRAP_CONTENT;
|
||||
headerTitle.setLayoutParams(layoutParams);
|
||||
headerTitle.setMaxLines(2);
|
||||
headerTitle.setMinWidth(getResources()
|
||||
drawerHeaderBinding.drawerHeaderNewpipeTitle.setLayoutParams(layoutParams);
|
||||
drawerHeaderBinding.drawerHeaderNewpipeTitle.setMaxLines(2);
|
||||
drawerHeaderBinding.drawerHeaderNewpipeTitle.setMinWidth(getResources()
|
||||
.getDimensionPixelSize(R.dimen.drawer_header_newpipe_title_default_width));
|
||||
headerTitle.setMaxWidth(getResources()
|
||||
drawerHeaderBinding.drawerHeaderNewpipeTitle.setMaxWidth(getResources()
|
||||
.getDimensionPixelSize(R.dimen.drawer_header_newpipe_title_max_width));
|
||||
}
|
||||
}
|
||||
@@ -334,9 +330,9 @@ public class MainActivity extends AppCompatActivity {
|
||||
private void toggleServices() {
|
||||
servicesShown = !servicesShown;
|
||||
|
||||
drawerItems.getMenu().removeGroup(R.id.menu_services_group);
|
||||
drawerItems.getMenu().removeGroup(R.id.menu_tabs_group);
|
||||
drawerItems.getMenu().removeGroup(R.id.menu_options_about_group);
|
||||
drawerLayoutBinding.navigation.getMenu().removeGroup(R.id.menu_services_group);
|
||||
drawerLayoutBinding.navigation.getMenu().removeGroup(R.id.menu_tabs_group);
|
||||
drawerLayoutBinding.navigation.getMenu().removeGroup(R.id.menu_options_about_group);
|
||||
|
||||
if (servicesShown) {
|
||||
showServices();
|
||||
@@ -350,13 +346,13 @@ public class MainActivity extends AppCompatActivity {
|
||||
}
|
||||
|
||||
private void showServices() {
|
||||
serviceArrow.setImageResource(R.drawable.ic_arrow_drop_up_white_24dp);
|
||||
drawerHeaderBinding.drawerArrow.setImageResource(R.drawable.ic_arrow_drop_up_white_24dp);
|
||||
|
||||
for (final StreamingService s : NewPipe.getServices()) {
|
||||
final String title = s.getServiceInfo().getName()
|
||||
+ (ServiceHelper.isBeta(s) ? " (beta)" : "");
|
||||
|
||||
final MenuItem menuItem = drawerItems.getMenu()
|
||||
final MenuItem menuItem = drawerLayoutBinding.navigation.getMenu()
|
||||
.add(R.id.menu_services_group, s.getServiceId(), ORDER, title)
|
||||
.setIcon(ServiceHelper.getIcon(s.getServiceId()));
|
||||
|
||||
@@ -365,21 +361,22 @@ public class MainActivity extends AppCompatActivity {
|
||||
enhancePeertubeMenu(s, menuItem);
|
||||
}
|
||||
}
|
||||
drawerItems.getMenu().getItem(ServiceHelper.getSelectedServiceId(this))
|
||||
drawerLayoutBinding.navigation.getMenu()
|
||||
.getItem(ServiceHelper.getSelectedServiceId(this))
|
||||
.setChecked(true);
|
||||
}
|
||||
|
||||
private void enhancePeertubeMenu(final StreamingService s, final MenuItem menuItem) {
|
||||
final PeertubeInstance currentInstace = PeertubeHelper.getCurrentInstance();
|
||||
menuItem.setTitle(currentInstace.getName() + (ServiceHelper.isBeta(s) ? " (beta)" : ""));
|
||||
final Spinner spinner = (Spinner) LayoutInflater.from(this)
|
||||
.inflate(R.layout.instance_spinner_layout, null);
|
||||
final PeertubeInstance currentInstance = PeertubeHelper.getCurrentInstance();
|
||||
menuItem.setTitle(currentInstance.getName() + (ServiceHelper.isBeta(s) ? " (beta)" : ""));
|
||||
final Spinner spinner = InstanceSpinnerLayoutBinding.inflate(LayoutInflater.from(this))
|
||||
.getRoot();
|
||||
final List<PeertubeInstance> instances = PeertubeHelper.getInstanceList(this);
|
||||
final List<String> items = new ArrayList<>();
|
||||
int defaultSelect = 0;
|
||||
for (final PeertubeInstance instance : instances) {
|
||||
items.add(instance.getName());
|
||||
if (instance.getUrl().equals(currentInstace.getUrl())) {
|
||||
if (instance.getUrl().equals(currentInstance.getUrl())) {
|
||||
defaultSelect = items.size() - 1;
|
||||
}
|
||||
}
|
||||
@@ -398,7 +395,7 @@ public class MainActivity extends AppCompatActivity {
|
||||
}
|
||||
PeertubeHelper.selectInstance(newInstance, getApplicationContext());
|
||||
changeService(menuItem);
|
||||
drawer.closeDrawers();
|
||||
mainBinding.getRoot().closeDrawers();
|
||||
new Handler(Looper.getMainLooper()).postDelayed(() -> {
|
||||
getSupportFragmentManager().popBackStack(null,
|
||||
FragmentManager.POP_BACK_STACK_INCLUSIVE);
|
||||
@@ -415,7 +412,7 @@ public class MainActivity extends AppCompatActivity {
|
||||
}
|
||||
|
||||
private void showTabs() throws ExtractionException {
|
||||
serviceArrow.setImageResource(R.drawable.ic_arrow_drop_down_white_24dp);
|
||||
drawerHeaderBinding.drawerArrow.setImageResource(R.drawable.ic_arrow_drop_down_white_24dp);
|
||||
|
||||
//Tabs
|
||||
final int currentServiceId = ServiceHelper.getSelectedServiceId(this);
|
||||
@@ -424,34 +421,34 @@ public class MainActivity extends AppCompatActivity {
|
||||
int kioskId = 0;
|
||||
|
||||
for (final String ks : service.getKioskList().getAvailableKiosks()) {
|
||||
drawerItems.getMenu()
|
||||
drawerLayoutBinding.navigation.getMenu()
|
||||
.add(R.id.menu_tabs_group, kioskId, ORDER,
|
||||
KioskTranslator.getTranslatedKioskName(ks, this))
|
||||
.setIcon(KioskTranslator.getKioskIcon(ks, this));
|
||||
kioskId++;
|
||||
}
|
||||
|
||||
drawerItems.getMenu()
|
||||
drawerLayoutBinding.navigation.getMenu()
|
||||
.add(R.id.menu_tabs_group, ITEM_ID_SUBSCRIPTIONS, ORDER, R.string.tab_subscriptions)
|
||||
.setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.ic_channel));
|
||||
drawerItems.getMenu()
|
||||
drawerLayoutBinding.navigation.getMenu()
|
||||
.add(R.id.menu_tabs_group, ITEM_ID_FEED, ORDER, R.string.fragment_feed_title)
|
||||
.setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.ic_rss));
|
||||
drawerItems.getMenu()
|
||||
drawerLayoutBinding.navigation.getMenu()
|
||||
.add(R.id.menu_tabs_group, ITEM_ID_BOOKMARKS, ORDER, R.string.tab_bookmarks)
|
||||
.setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.ic_bookmark));
|
||||
drawerItems.getMenu()
|
||||
drawerLayoutBinding.navigation.getMenu()
|
||||
.add(R.id.menu_tabs_group, ITEM_ID_DOWNLOADS, ORDER, R.string.downloads)
|
||||
.setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.ic_file_download));
|
||||
drawerItems.getMenu()
|
||||
drawerLayoutBinding.navigation.getMenu()
|
||||
.add(R.id.menu_tabs_group, ITEM_ID_HISTORY, ORDER, R.string.action_history)
|
||||
.setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.ic_history));
|
||||
|
||||
//Settings and About
|
||||
drawerItems.getMenu()
|
||||
drawerLayoutBinding.navigation.getMenu()
|
||||
.add(R.id.menu_options_about_group, ITEM_ID_SETTINGS, ORDER, R.string.settings)
|
||||
.setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.ic_settings));
|
||||
drawerItems.getMenu()
|
||||
drawerLayoutBinding.navigation.getMenu()
|
||||
.add(R.id.menu_options_about_group, ITEM_ID_ABOUT, ORDER, R.string.tab_about)
|
||||
.setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.ic_info_outline));
|
||||
}
|
||||
@@ -476,16 +473,18 @@ public class MainActivity extends AppCompatActivity {
|
||||
|
||||
// Close drawer on return, and don't show animation,
|
||||
// so it looks like the drawer isn't open when the user returns to MainActivity
|
||||
drawer.closeDrawer(GravityCompat.START, false);
|
||||
mainBinding.getRoot().closeDrawer(GravityCompat.START, false);
|
||||
try {
|
||||
final int selectedServiceId = ServiceHelper.getSelectedServiceId(this);
|
||||
final String selectedServiceName = NewPipe.getService(selectedServiceId)
|
||||
.getServiceInfo().getName();
|
||||
headerServiceView.setText(selectedServiceName);
|
||||
headerServiceIcon.setImageResource(ServiceHelper.getIcon(selectedServiceId));
|
||||
drawerHeaderBinding.drawerHeaderServiceView.setText(selectedServiceName);
|
||||
drawerHeaderBinding.drawerHeaderServiceIcon.setImageResource(ServiceHelper
|
||||
.getIcon(selectedServiceId));
|
||||
|
||||
headerServiceView.post(() -> headerServiceView.setSelected(true));
|
||||
toggleServiceButton.setContentDescription(
|
||||
drawerHeaderBinding.drawerHeaderServiceView.post(() -> drawerHeaderBinding
|
||||
.drawerHeaderServiceView.setSelected(true));
|
||||
drawerHeaderBinding.drawerHeaderActionButton.setContentDescription(
|
||||
getString(R.string.drawer_header_description) + selectedServiceName);
|
||||
} catch (final Exception e) {
|
||||
ErrorActivity.reportUiError(this, e);
|
||||
@@ -498,10 +497,7 @@ public class MainActivity extends AppCompatActivity {
|
||||
Log.d(TAG, "Theme has changed, recreating activity...");
|
||||
}
|
||||
sharedPreferences.edit().putBoolean(Constants.KEY_THEME_CHANGE, false).apply();
|
||||
// https://stackoverflow.com/questions/10844112/
|
||||
// Briefly, let the activity resume
|
||||
// properly posting the recreate call to end of the message queue
|
||||
new Handler(Looper.getMainLooper()).post(MainActivity.this::recreate);
|
||||
ActivityCompat.recreate(this);
|
||||
}
|
||||
|
||||
if (sharedPreferences.getBoolean(Constants.KEY_MAIN_PAGE_CHANGE, false)) {
|
||||
@@ -514,7 +510,8 @@ public class MainActivity extends AppCompatActivity {
|
||||
|
||||
final boolean isHistoryEnabled = sharedPreferences.getBoolean(
|
||||
getString(R.string.enable_watch_history_key), true);
|
||||
drawerItems.getMenu().findItem(ITEM_ID_HISTORY).setVisible(isHistoryEnabled);
|
||||
drawerLayoutBinding.navigation.getMenu().findItem(ITEM_ID_HISTORY)
|
||||
.setVisible(isHistoryEnabled);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -558,9 +555,8 @@ public class MainActivity extends AppCompatActivity {
|
||||
}
|
||||
|
||||
if (DeviceUtils.isTv(this)) {
|
||||
final View drawerPanel = findViewById(R.id.navigation);
|
||||
if (drawer.isDrawerOpen(drawerPanel)) {
|
||||
drawer.closeDrawers();
|
||||
if (mainBinding.getRoot().isDrawerOpen(drawerLayoutBinding.navigation)) {
|
||||
mainBinding.getRoot().closeDrawers();
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -586,9 +582,7 @@ public class MainActivity extends AppCompatActivity {
|
||||
// delegate the back press to it
|
||||
if (fragmentPlayer instanceof BackPressable) {
|
||||
if (!((BackPressable) fragmentPlayer).onBackPressed()) {
|
||||
final FrameLayout bottomSheetLayout =
|
||||
findViewById(R.id.fragment_player_holder);
|
||||
BottomSheetBehavior.from(bottomSheetLayout)
|
||||
BottomSheetBehavior.from(mainBinding.fragmentPlayerHolder)
|
||||
.setState(BottomSheetBehavior.STATE_COLLAPSED);
|
||||
}
|
||||
return;
|
||||
@@ -671,8 +665,7 @@ public class MainActivity extends AppCompatActivity {
|
||||
final Fragment fragment
|
||||
= getSupportFragmentManager().findFragmentById(R.id.fragment_holder);
|
||||
if (!(fragment instanceof SearchFragment)) {
|
||||
findViewById(R.id.toolbar).findViewById(R.id.toolbar_search_container)
|
||||
.setVisibility(View.GONE);
|
||||
toolbarLayoutBinding.toolbarSearchContainer.getRoot().setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
final ActionBar actionBar = getSupportActionBar();
|
||||
@@ -733,21 +726,20 @@ public class MainActivity extends AppCompatActivity {
|
||||
return;
|
||||
}
|
||||
|
||||
final Toolbar toolbar = findViewById(R.id.toolbar);
|
||||
|
||||
final Fragment fragment = getSupportFragmentManager()
|
||||
.findFragmentById(R.id.fragment_holder);
|
||||
if (fragment instanceof MainFragment) {
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(false);
|
||||
if (toggle != null) {
|
||||
toggle.syncState();
|
||||
toolbar.setNavigationOnClickListener(v -> drawer.openDrawer(GravityCompat.START));
|
||||
drawer.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNDEFINED);
|
||||
toolbarLayoutBinding.toolbar.setNavigationOnClickListener(v -> mainBinding.getRoot()
|
||||
.openDrawer(GravityCompat.START));
|
||||
mainBinding.getRoot().setDrawerLockMode(DrawerLayout.LOCK_MODE_UNDEFINED);
|
||||
}
|
||||
} else {
|
||||
drawer.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED);
|
||||
mainBinding.getRoot().setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED);
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
toolbar.setNavigationOnClickListener(v -> onHomeButtonPressed());
|
||||
toolbarLayoutBinding.toolbar.setNavigationOnClickListener(v -> onHomeButtonPressed());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -855,9 +847,8 @@ public class MainActivity extends AppCompatActivity {
|
||||
}
|
||||
|
||||
private boolean bottomSheetHiddenOrCollapsed() {
|
||||
final FrameLayout bottomSheetLayout = findViewById(R.id.fragment_player_holder);
|
||||
final BottomSheetBehavior<FrameLayout> bottomSheetBehavior =
|
||||
BottomSheetBehavior.from(bottomSheetLayout);
|
||||
BottomSheetBehavior.from(mainBinding.fragmentPlayerHolder);
|
||||
|
||||
final int sheetState = bottomSheetBehavior.getState();
|
||||
return sheetState == BottomSheetBehavior.STATE_HIDDEN
|
||||
|
||||
@@ -8,20 +8,18 @@ import android.util.Log;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.webkit.CookieManager;
|
||||
import android.webkit.WebResourceRequest;
|
||||
import android.webkit.WebSettings;
|
||||
import android.webkit.WebView;
|
||||
import android.webkit.WebViewClient;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.RequiresApi;
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.core.app.NavUtils;
|
||||
import androidx.preference.PreferenceManager;
|
||||
import androidx.webkit.WebViewClientCompat;
|
||||
|
||||
import org.schabi.newpipe.databinding.ActivityRecaptchaBinding;
|
||||
import org.schabi.newpipe.util.ThemeHelper;
|
||||
|
||||
import java.io.UnsupportedEncodingException;
|
||||
@@ -53,46 +51,37 @@ public class ReCaptchaActivity extends AppCompatActivity {
|
||||
public static final String YT_URL = "https://www.youtube.com";
|
||||
public static final String RECAPTCHA_COOKIES_KEY = "recaptcha_cookies";
|
||||
|
||||
private WebView webView;
|
||||
public static String sanitizeRecaptchaUrl(@Nullable final String url) {
|
||||
if (url == null || url.trim().isEmpty()) {
|
||||
return YT_URL; // YouTube is the most likely service to have thrown a recaptcha
|
||||
} else {
|
||||
// remove "pbj=1" parameter from YouYube urls, as it makes the page JSON and not HTML
|
||||
return url.replace("&pbj=1", "").replace("pbj=1&", "").replace("?pbj=1", "");
|
||||
}
|
||||
}
|
||||
|
||||
private ActivityRecaptchaBinding recaptchaBinding;
|
||||
private String foundCookies = "";
|
||||
|
||||
@Override
|
||||
protected void onCreate(final Bundle savedInstanceState) {
|
||||
ThemeHelper.setTheme(this);
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_recaptcha);
|
||||
final Toolbar toolbar = findViewById(R.id.toolbar);
|
||||
setSupportActionBar(toolbar);
|
||||
|
||||
String url = getIntent().getStringExtra(RECAPTCHA_URL_EXTRA);
|
||||
if (url == null || url.isEmpty()) {
|
||||
url = YT_URL;
|
||||
}
|
||||
recaptchaBinding = ActivityRecaptchaBinding.inflate(getLayoutInflater());
|
||||
setContentView(recaptchaBinding.getRoot());
|
||||
setSupportActionBar(recaptchaBinding.toolbar);
|
||||
|
||||
final String url = sanitizeRecaptchaUrl(getIntent().getStringExtra(RECAPTCHA_URL_EXTRA));
|
||||
// set return to Cancel by default
|
||||
setResult(RESULT_CANCELED);
|
||||
|
||||
|
||||
webView = findViewById(R.id.reCaptchaWebView);
|
||||
|
||||
// enable Javascript
|
||||
final WebSettings webSettings = webView.getSettings();
|
||||
final WebSettings webSettings = recaptchaBinding.reCaptchaWebView.getSettings();
|
||||
webSettings.setJavaScriptEnabled(true);
|
||||
webSettings.setUserAgentString(DownloaderImpl.USER_AGENT);
|
||||
|
||||
webView.setWebViewClient(new WebViewClient() {
|
||||
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
|
||||
@Override
|
||||
public boolean shouldOverrideUrlLoading(final WebView view,
|
||||
final WebResourceRequest request) {
|
||||
final String url = request.getUrl().toString();
|
||||
if (MainActivity.DEBUG) {
|
||||
Log.d(TAG, "shouldOverrideUrlLoading: request.url=" + url);
|
||||
}
|
||||
|
||||
handleCookiesFromUrl(url);
|
||||
return false;
|
||||
}
|
||||
|
||||
recaptchaBinding.reCaptchaWebView.setWebViewClient(new WebViewClientCompat() {
|
||||
@Override
|
||||
public boolean shouldOverrideUrlLoading(final WebView view, final String url) {
|
||||
if (MainActivity.DEBUG) {
|
||||
@@ -111,17 +100,16 @@ public class ReCaptchaActivity extends AppCompatActivity {
|
||||
});
|
||||
|
||||
// cleaning cache, history and cookies from webView
|
||||
webView.clearCache(true);
|
||||
webView.clearHistory();
|
||||
final android.webkit.CookieManager cookieManager = CookieManager.getInstance();
|
||||
recaptchaBinding.reCaptchaWebView.clearCache(true);
|
||||
recaptchaBinding.reCaptchaWebView.clearHistory();
|
||||
final CookieManager cookieManager = CookieManager.getInstance();
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
cookieManager.removeAllCookies(aBoolean -> {
|
||||
});
|
||||
cookieManager.removeAllCookies(value -> { });
|
||||
} else {
|
||||
cookieManager.removeAllCookie();
|
||||
}
|
||||
|
||||
webView.loadUrl(url);
|
||||
recaptchaBinding.reCaptchaWebView.loadUrl(url);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -145,18 +133,16 @@ public class ReCaptchaActivity extends AppCompatActivity {
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(final MenuItem item) {
|
||||
final int id = item.getItemId();
|
||||
switch (id) {
|
||||
case R.id.menu_item_done:
|
||||
saveCookiesAndFinish();
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
if (item.getItemId() == R.id.menu_item_done) {
|
||||
saveCookiesAndFinish();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private void saveCookiesAndFinish() {
|
||||
handleCookiesFromUrl(webView.getUrl()); // try to get cookies of unclosed page
|
||||
// try to get cookies of unclosed page
|
||||
handleCookiesFromUrl(recaptchaBinding.reCaptchaWebView.getUrl());
|
||||
if (MainActivity.DEBUG) {
|
||||
Log.d(TAG, "saveCookiesAndFinish: foundCookies=" + foundCookies);
|
||||
}
|
||||
|
||||
@@ -14,7 +14,6 @@ import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.Button;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.RadioButton;
|
||||
import android.widget.RadioGroup;
|
||||
import android.widget.Toast;
|
||||
@@ -26,10 +25,13 @@ import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.appcompat.content.res.AppCompatResources;
|
||||
import androidx.core.app.NotificationCompat;
|
||||
import androidx.core.app.ServiceCompat;
|
||||
import androidx.core.widget.TextViewCompat;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
import androidx.preference.PreferenceManager;
|
||||
|
||||
import org.schabi.newpipe.databinding.ListRadioIconItemBinding;
|
||||
import org.schabi.newpipe.databinding.SingleChoiceDialogViewBinding;
|
||||
import org.schabi.newpipe.download.DownloadDialog;
|
||||
import org.schabi.newpipe.extractor.Info;
|
||||
import org.schabi.newpipe.extractor.NewPipe;
|
||||
@@ -66,13 +68,13 @@ import java.util.List;
|
||||
|
||||
import icepick.Icepick;
|
||||
import icepick.State;
|
||||
import io.reactivex.Observable;
|
||||
import io.reactivex.Single;
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.disposables.CompositeDisposable;
|
||||
import io.reactivex.disposables.Disposable;
|
||||
import io.reactivex.functions.Consumer;
|
||||
import io.reactivex.schedulers.Schedulers;
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||
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.functions.Consumer;
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers;
|
||||
|
||||
import static org.schabi.newpipe.extractor.StreamingService.ServiceInfo.MediaCapability.AUDIO;
|
||||
import static org.schabi.newpipe.extractor.StreamingService.ServiceInfo.MediaCapability.VIDEO;
|
||||
@@ -267,9 +269,8 @@ public class RouterActivity extends AppCompatActivity {
|
||||
final Context themeWrapperContext = getThemeWrapperContext();
|
||||
|
||||
final LayoutInflater inflater = LayoutInflater.from(themeWrapperContext);
|
||||
final LinearLayout rootLayout = (LinearLayout) inflater.inflate(
|
||||
R.layout.single_choice_dialog_view, null, false);
|
||||
final RadioGroup radioGroup = rootLayout.findViewById(android.R.id.list);
|
||||
final RadioGroup radioGroup = SingleChoiceDialogViewBinding.inflate(getLayoutInflater())
|
||||
.list;
|
||||
|
||||
final DialogInterface.OnClickListener dialogButtonsClickListener = (dialog, which) -> {
|
||||
final int indexOfChild = radioGroup.indexOfChild(
|
||||
@@ -322,8 +323,7 @@ public class RouterActivity extends AppCompatActivity {
|
||||
|
||||
int id = 12345;
|
||||
for (final AdapterChoiceItem item : choices) {
|
||||
final RadioButton radioButton
|
||||
= (RadioButton) inflater.inflate(R.layout.list_radio_icon_item, null);
|
||||
final RadioButton radioButton = ListRadioIconItemBinding.inflate(inflater).getRoot();
|
||||
radioButton.setText(item.description);
|
||||
TextViewCompat.setCompoundDrawablesRelativeWithIntrinsicBounds(radioButton,
|
||||
AppCompatResources.getDrawable(getApplicationContext(), item.icon),
|
||||
@@ -519,7 +519,7 @@ public class RouterActivity extends AppCompatActivity {
|
||||
disposables.add(ExtractorHelper.getStreamInfo(currentServiceId, currentUrl, true)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe((@NonNull StreamInfo result) -> {
|
||||
.subscribe(result -> {
|
||||
final List<VideoStream> sortedVideoStreams = ListHelper
|
||||
.getSortedStreamVideosList(this, result.getVideoStreams(),
|
||||
result.getVideoOnlyStreams(), false);
|
||||
@@ -534,9 +534,8 @@ public class RouterActivity extends AppCompatActivity {
|
||||
downloadDialog.show(fm, "downloadDialog");
|
||||
fm.executePendingTransactions();
|
||||
downloadDialog.requireDialog().setOnDismissListener(dialog -> finish());
|
||||
}, (@NonNull Throwable throwable) -> {
|
||||
showUnsupportedUrlDialog(currentUrl);
|
||||
}));
|
||||
}, throwable ->
|
||||
showUnsupportedUrlDialog(currentUrl)));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -697,7 +696,7 @@ public class RouterActivity extends AppCompatActivity {
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
stopForeground(true);
|
||||
ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE);
|
||||
if (fetcher != null) {
|
||||
fetcher.dispose();
|
||||
}
|
||||
|
||||
@@ -6,22 +6,19 @@ import android.view.LayoutInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentActivity;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import androidx.viewpager2.adapter.FragmentStateAdapter;
|
||||
import androidx.viewpager2.widget.ViewPager2;
|
||||
|
||||
import com.google.android.material.tabs.TabLayout;
|
||||
import com.google.android.material.tabs.TabLayoutMediator;
|
||||
|
||||
import org.schabi.newpipe.BuildConfig;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.databinding.ActivityAboutBinding;
|
||||
import org.schabi.newpipe.databinding.FragmentAboutBinding;
|
||||
import org.schabi.newpipe.util.ThemeHelper;
|
||||
|
||||
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
|
||||
@@ -31,7 +28,7 @@ public class AboutActivity extends AppCompatActivity {
|
||||
/**
|
||||
* List of all software components.
|
||||
*/
|
||||
private static final SoftwareComponent[] SOFTWARE_COMPONENTS = new SoftwareComponent[]{
|
||||
private static final SoftwareComponent[] SOFTWARE_COMPONENTS = {
|
||||
new SoftwareComponent("Giga Get", "2014 - 2015", "Peter Cai",
|
||||
"https://github.com/PaperAirplane-Dev-Team/GigaGet", StandardLicenses.GPL3),
|
||||
new SoftwareComponent("NewPipe Extractor", "2017 - 2020", "Christian Schabesberger",
|
||||
@@ -68,40 +65,27 @@ public class AboutActivity extends AppCompatActivity {
|
||||
private static final int POS_ABOUT = 0;
|
||||
private static final int POS_LICENSE = 1;
|
||||
private static final int TOTAL_COUNT = 2;
|
||||
/**
|
||||
* The {@link RecyclerView.Adapter} that will provide
|
||||
* fragments for each of the sections. We use a
|
||||
* {@link FragmentStateAdapter} derivative, which will keep every
|
||||
* loaded fragment in memory.
|
||||
*/
|
||||
private SectionsPagerAdapter mSectionsPagerAdapter;
|
||||
/**
|
||||
* The {@link ViewPager2} that will host the section contents.
|
||||
*/
|
||||
private ViewPager2 mViewPager;
|
||||
|
||||
@Override
|
||||
protected void onCreate(final Bundle savedInstanceState) {
|
||||
assureCorrectAppLanguage(this);
|
||||
super.onCreate(savedInstanceState);
|
||||
ThemeHelper.setTheme(this);
|
||||
this.setTitle(getString(R.string.title_activity_about));
|
||||
setTitle(getString(R.string.title_activity_about));
|
||||
|
||||
setContentView(R.layout.activity_about);
|
||||
final ActivityAboutBinding aboutBinding = ActivityAboutBinding.inflate(getLayoutInflater());
|
||||
setContentView(aboutBinding.getRoot());
|
||||
|
||||
final Toolbar toolbar = findViewById(R.id.toolbar);
|
||||
setSupportActionBar(toolbar);
|
||||
setSupportActionBar(aboutBinding.toolbar);
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
// Create the adapter that will return a fragment for each of the three
|
||||
// primary sections of the activity.
|
||||
mSectionsPagerAdapter = new SectionsPagerAdapter(this);
|
||||
final SectionsPagerAdapter mSectionsPagerAdapter = new SectionsPagerAdapter(this);
|
||||
|
||||
// Set up the ViewPager with the sections adapter.
|
||||
mViewPager = findViewById(R.id.container);
|
||||
mViewPager.setAdapter(mSectionsPagerAdapter);
|
||||
aboutBinding.container.setAdapter(mSectionsPagerAdapter);
|
||||
|
||||
final TabLayout tabLayout = findViewById(R.id.tabs);
|
||||
new TabLayoutMediator(tabLayout, mViewPager, (tab, position) -> {
|
||||
new TabLayoutMediator(aboutBinding.tabs, aboutBinding.container, (tab, position) -> {
|
||||
switch (position) {
|
||||
default:
|
||||
case POS_ABOUT:
|
||||
@@ -143,33 +127,28 @@ public class AboutActivity extends AppCompatActivity {
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(final LayoutInflater inflater, final ViewGroup container,
|
||||
public View onCreateView(@NonNull final LayoutInflater inflater, final ViewGroup container,
|
||||
final Bundle savedInstanceState) {
|
||||
final View rootView = inflater.inflate(R.layout.fragment_about, container, false);
|
||||
final Context context = this.getContext();
|
||||
final FragmentAboutBinding aboutBinding =
|
||||
FragmentAboutBinding.inflate(inflater, container, false);
|
||||
final Context context = getContext();
|
||||
|
||||
final TextView version = rootView.findViewById(R.id.app_version);
|
||||
version.setText(BuildConfig.VERSION_NAME);
|
||||
aboutBinding.appVersion.setText(BuildConfig.VERSION_NAME);
|
||||
|
||||
final View githubLink = rootView.findViewById(R.id.github_link);
|
||||
githubLink.setOnClickListener(nv ->
|
||||
aboutBinding.githubLink.setOnClickListener(nv ->
|
||||
openUrlInBrowser(context, context.getString(R.string.github_url)));
|
||||
|
||||
final View donationLink = rootView.findViewById(R.id.donation_link);
|
||||
donationLink.setOnClickListener(v ->
|
||||
aboutBinding.donationLink.setOnClickListener(v ->
|
||||
openUrlInBrowser(context, context.getString(R.string.donation_url)));
|
||||
|
||||
final View websiteLink = rootView.findViewById(R.id.website_link);
|
||||
websiteLink.setOnClickListener(nv ->
|
||||
aboutBinding.websiteLink.setOnClickListener(nv ->
|
||||
openUrlInBrowser(context, context.getString(R.string.website_url)));
|
||||
|
||||
final View privacyPolicyLink = rootView.findViewById(R.id.privacy_policy_link);
|
||||
privacyPolicyLink.setOnClickListener(v ->
|
||||
aboutBinding.privacyPolicyLink.setOnClickListener(v ->
|
||||
openUrlInBrowser(context, context.getString(R.string.privacy_policy_url)));
|
||||
|
||||
return rootView;
|
||||
return aboutBinding.getRoot();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,80 +0,0 @@
|
||||
package org.schabi.newpipe.about;
|
||||
|
||||
import android.net.Uri;
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* Class for storing information about a software license.
|
||||
*/
|
||||
public class License implements Parcelable, Serializable {
|
||||
public static final Creator<License> CREATOR = new Creator<License>() {
|
||||
@Override
|
||||
public License createFromParcel(final Parcel source) {
|
||||
return new License(source);
|
||||
}
|
||||
|
||||
@Override
|
||||
public License[] newArray(final int size) {
|
||||
return new License[size];
|
||||
}
|
||||
};
|
||||
private final String abbreviation;
|
||||
private final String name;
|
||||
private String filename;
|
||||
|
||||
public License(final String name, final String abbreviation, final String filename) {
|
||||
if (name == null) {
|
||||
throw new NullPointerException("name is null");
|
||||
}
|
||||
if (abbreviation == null) {
|
||||
throw new NullPointerException("abbreviation is null");
|
||||
}
|
||||
if (filename == null) {
|
||||
throw new NullPointerException("filename is null");
|
||||
}
|
||||
this.name = name;
|
||||
this.filename = filename;
|
||||
this.abbreviation = abbreviation;
|
||||
}
|
||||
|
||||
protected License(final Parcel in) {
|
||||
this.filename = in.readString();
|
||||
this.abbreviation = in.readString();
|
||||
this.name = in.readString();
|
||||
}
|
||||
|
||||
public Uri getContentUri() {
|
||||
return new Uri.Builder()
|
||||
.scheme("file")
|
||||
.path("/android_asset")
|
||||
.appendPath(filename)
|
||||
.build();
|
||||
}
|
||||
|
||||
public String getAbbreviation() {
|
||||
return abbreviation;
|
||||
}
|
||||
|
||||
public String getFilename() {
|
||||
return filename;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int describeContents() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeToParcel(final Parcel dest, final int flags) {
|
||||
dest.writeString(this.filename);
|
||||
dest.writeString(this.abbreviation);
|
||||
dest.writeString(this.name);
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
}
|
||||
19
app/src/main/java/org/schabi/newpipe/about/License.kt
Normal file
19
app/src/main/java/org/schabi/newpipe/about/License.kt
Normal file
@@ -0,0 +1,19 @@
|
||||
package org.schabi.newpipe.about
|
||||
|
||||
import android.net.Uri
|
||||
import android.os.Parcelable
|
||||
import kotlinx.android.parcel.Parcelize
|
||||
import java.io.Serializable
|
||||
|
||||
/**
|
||||
* Class for storing information about a software license.
|
||||
*/
|
||||
@Parcelize
|
||||
class License(val name: String, val abbreviation: String, val filename: String) : Parcelable, Serializable {
|
||||
val contentUri: Uri
|
||||
get() = Uri.Builder()
|
||||
.scheme("file")
|
||||
.path("/android_asset")
|
||||
.appendPath(filename)
|
||||
.build()
|
||||
}
|
||||
@@ -20,7 +20,7 @@ import java.io.Serializable;
|
||||
import java.util.Arrays;
|
||||
import java.util.Comparator;
|
||||
|
||||
import io.reactivex.disposables.CompositeDisposable;
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
||||
|
||||
/**
|
||||
* Fragment containing the software licenses.
|
||||
|
||||
@@ -16,11 +16,10 @@ import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
import io.reactivex.Observable;
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.disposables.Disposable;
|
||||
import io.reactivex.disposables.Disposables;
|
||||
import io.reactivex.schedulers.Schedulers;
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.rxjava3.core.Observable;
|
||||
import io.reactivex.rxjava3.disposables.Disposable;
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers;
|
||||
|
||||
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
|
||||
|
||||
@@ -37,14 +36,12 @@ public final class LicenseFragmentHelper {
|
||||
@NonNull final License license) {
|
||||
final StringBuilder licenseContent = new StringBuilder();
|
||||
final String webViewData;
|
||||
try {
|
||||
final BufferedReader in = new BufferedReader(new InputStreamReader(
|
||||
context.getAssets().open(license.getFilename()), StandardCharsets.UTF_8));
|
||||
try (BufferedReader in = new BufferedReader(new InputStreamReader(
|
||||
context.getAssets().open(license.getFilename()), StandardCharsets.UTF_8))) {
|
||||
String str;
|
||||
while ((str = in.readLine()) != null) {
|
||||
licenseContent.append(str);
|
||||
}
|
||||
in.close();
|
||||
|
||||
// split the HTML file and insert the stylesheet into the HEAD of the file
|
||||
webViewData = licenseContent.toString().replace("</head>",
|
||||
@@ -57,7 +54,7 @@ public final class LicenseFragmentHelper {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param context
|
||||
* @param context the Android context
|
||||
* @return String which is a CSS stylesheet according to the context's theme
|
||||
*/
|
||||
private static String getLicenseStylesheet(@NonNull final Context context) {
|
||||
@@ -88,7 +85,7 @@ public final class LicenseFragmentHelper {
|
||||
|
||||
static Disposable showLicense(@Nullable final Context context, @NonNull final License license) {
|
||||
if (context == null) {
|
||||
return Disposables.empty();
|
||||
return Disposable.empty();
|
||||
}
|
||||
|
||||
return Observable.fromCallable(() -> getFormattedLicense(context, license))
|
||||
|
||||
@@ -1,83 +0,0 @@
|
||||
package org.schabi.newpipe.about;
|
||||
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
|
||||
public class SoftwareComponent implements Parcelable {
|
||||
public static final Creator<SoftwareComponent> CREATOR = new Creator<SoftwareComponent>() {
|
||||
@Override
|
||||
public SoftwareComponent createFromParcel(final Parcel source) {
|
||||
return new SoftwareComponent(source);
|
||||
}
|
||||
|
||||
@Override
|
||||
public SoftwareComponent[] newArray(final int size) {
|
||||
return new SoftwareComponent[size];
|
||||
}
|
||||
};
|
||||
|
||||
private final License license;
|
||||
private final String name;
|
||||
private final String years;
|
||||
private final String copyrightOwner;
|
||||
private final String link;
|
||||
private final String version;
|
||||
|
||||
public SoftwareComponent(final String name, final String years, final String copyrightOwner,
|
||||
final String link, final License license) {
|
||||
this.name = name;
|
||||
this.years = years;
|
||||
this.copyrightOwner = copyrightOwner;
|
||||
this.link = link;
|
||||
this.license = license;
|
||||
this.version = null;
|
||||
}
|
||||
|
||||
protected SoftwareComponent(final Parcel in) {
|
||||
this.name = in.readString();
|
||||
this.license = in.readParcelable(License.class.getClassLoader());
|
||||
this.copyrightOwner = in.readString();
|
||||
this.link = in.readString();
|
||||
this.years = in.readString();
|
||||
this.version = in.readString();
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public String getYears() {
|
||||
return years;
|
||||
}
|
||||
|
||||
public String getCopyrightOwner() {
|
||||
return copyrightOwner;
|
||||
}
|
||||
|
||||
public String getLink() {
|
||||
return link;
|
||||
}
|
||||
|
||||
public String getVersion() {
|
||||
return version;
|
||||
}
|
||||
|
||||
public License getLicense() {
|
||||
return license;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int describeContents() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeToParcel(final Parcel dest, final int flags) {
|
||||
dest.writeString(name);
|
||||
dest.writeParcelable(license, flags);
|
||||
dest.writeString(copyrightOwner);
|
||||
dest.writeString(link);
|
||||
dest.writeString(years);
|
||||
dest.writeString(version);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package org.schabi.newpipe.about
|
||||
|
||||
import android.os.Parcelable
|
||||
import kotlinx.android.parcel.Parcelize
|
||||
|
||||
@Parcelize
|
||||
class SoftwareComponent
|
||||
@JvmOverloads
|
||||
constructor(
|
||||
val name: String,
|
||||
val years: String,
|
||||
val copyrightOwner: String,
|
||||
val link: String,
|
||||
val license: License,
|
||||
val version: String? = null
|
||||
) : Parcelable
|
||||
@@ -9,7 +9,7 @@ import androidx.room.Update;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
import io.reactivex.Flowable;
|
||||
import io.reactivex.rxjava3.core.Flowable;
|
||||
|
||||
@Dao
|
||||
public interface BasicDAO<Entity> {
|
||||
|
||||
@@ -6,19 +6,20 @@ import androidx.room.OnConflictStrategy
|
||||
import androidx.room.Query
|
||||
import androidx.room.Transaction
|
||||
import androidx.room.Update
|
||||
import io.reactivex.Flowable
|
||||
import java.time.OffsetDateTime
|
||||
import io.reactivex.rxjava3.core.Flowable
|
||||
import org.schabi.newpipe.database.feed.model.FeedEntity
|
||||
import org.schabi.newpipe.database.feed.model.FeedLastUpdatedEntity
|
||||
import org.schabi.newpipe.database.stream.model.StreamEntity
|
||||
import org.schabi.newpipe.database.subscription.SubscriptionEntity
|
||||
import java.time.OffsetDateTime
|
||||
|
||||
@Dao
|
||||
abstract class FeedDAO {
|
||||
@Query("DELETE FROM feed")
|
||||
abstract fun deleteAll(): Int
|
||||
|
||||
@Query("""
|
||||
@Query(
|
||||
"""
|
||||
SELECT s.* FROM streams s
|
||||
|
||||
INNER JOIN feed f
|
||||
@@ -27,10 +28,12 @@ abstract class FeedDAO {
|
||||
ORDER BY s.upload_date IS NULL DESC, s.upload_date DESC, s.uploader ASC
|
||||
|
||||
LIMIT 500
|
||||
""")
|
||||
"""
|
||||
)
|
||||
abstract fun getAllStreams(): Flowable<List<StreamEntity>>
|
||||
|
||||
@Query("""
|
||||
@Query(
|
||||
"""
|
||||
SELECT s.* FROM streams s
|
||||
|
||||
INNER JOIN feed f
|
||||
@@ -46,10 +49,12 @@ abstract class FeedDAO {
|
||||
|
||||
ORDER BY s.upload_date IS NULL DESC, s.upload_date DESC, s.uploader ASC
|
||||
LIMIT 500
|
||||
""")
|
||||
"""
|
||||
)
|
||||
abstract fun getAllStreamsFromGroup(groupId: Long): Flowable<List<StreamEntity>>
|
||||
|
||||
@Query("""
|
||||
@Query(
|
||||
"""
|
||||
DELETE FROM feed WHERE
|
||||
|
||||
feed.stream_id IN (
|
||||
@@ -60,10 +65,12 @@ abstract class FeedDAO {
|
||||
|
||||
WHERE s.upload_date < :offsetDateTime
|
||||
)
|
||||
""")
|
||||
"""
|
||||
)
|
||||
abstract fun unlinkStreamsOlderThan(offsetDateTime: OffsetDateTime)
|
||||
|
||||
@Query("""
|
||||
@Query(
|
||||
"""
|
||||
DELETE FROM feed
|
||||
|
||||
WHERE feed.subscription_id = :subscriptionId
|
||||
@@ -76,7 +83,8 @@ abstract class FeedDAO {
|
||||
|
||||
WHERE s.stream_type = "LIVE_STREAM" OR s.stream_type = "AUDIO_LIVE_STREAM"
|
||||
)
|
||||
""")
|
||||
"""
|
||||
)
|
||||
abstract fun unlinkOldLivestreams(subscriptionId: Long)
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
||||
@@ -100,12 +108,14 @@ abstract class FeedDAO {
|
||||
}
|
||||
}
|
||||
|
||||
@Query("""
|
||||
@Query(
|
||||
"""
|
||||
SELECT MIN(lu.last_updated) FROM feed_last_updated lu
|
||||
|
||||
INNER JOIN feed_group_subscription_join fgs
|
||||
ON fgs.subscription_id = lu.subscription_id AND fgs.group_id = :groupId
|
||||
""")
|
||||
"""
|
||||
)
|
||||
abstract fun oldestSubscriptionUpdate(groupId: Long): Flowable<List<OffsetDateTime>>
|
||||
|
||||
@Query("SELECT MIN(last_updated) FROM feed_last_updated")
|
||||
@@ -114,7 +124,8 @@ abstract class FeedDAO {
|
||||
@Query("SELECT COUNT(*) FROM feed_last_updated WHERE last_updated IS NULL")
|
||||
abstract fun notLoadedCount(): Flowable<Long>
|
||||
|
||||
@Query("""
|
||||
@Query(
|
||||
"""
|
||||
SELECT COUNT(*) FROM subscriptions s
|
||||
|
||||
INNER JOIN feed_group_subscription_join fgs
|
||||
@@ -124,20 +135,24 @@ abstract class FeedDAO {
|
||||
ON s.uid = lu.subscription_id
|
||||
|
||||
WHERE lu.last_updated IS NULL
|
||||
""")
|
||||
"""
|
||||
)
|
||||
abstract fun notLoadedCountForGroup(groupId: Long): Flowable<Long>
|
||||
|
||||
@Query("""
|
||||
@Query(
|
||||
"""
|
||||
SELECT s.* FROM subscriptions s
|
||||
|
||||
LEFT JOIN feed_last_updated lu
|
||||
ON s.uid = lu.subscription_id
|
||||
|
||||
WHERE lu.last_updated IS NULL OR lu.last_updated < :outdatedThreshold
|
||||
""")
|
||||
"""
|
||||
)
|
||||
abstract fun getAllOutdated(outdatedThreshold: OffsetDateTime): Flowable<List<SubscriptionEntity>>
|
||||
|
||||
@Query("""
|
||||
@Query(
|
||||
"""
|
||||
SELECT s.* FROM subscriptions s
|
||||
|
||||
INNER JOIN feed_group_subscription_join fgs
|
||||
@@ -147,6 +162,7 @@ abstract class FeedDAO {
|
||||
ON s.uid = lu.subscription_id
|
||||
|
||||
WHERE lu.last_updated IS NULL OR lu.last_updated < :outdatedThreshold
|
||||
""")
|
||||
"""
|
||||
)
|
||||
abstract fun getAllOutdatedForGroup(groupId: Long, outdatedThreshold: OffsetDateTime): Flowable<List<SubscriptionEntity>>
|
||||
}
|
||||
|
||||
@@ -6,8 +6,8 @@ import androidx.room.OnConflictStrategy
|
||||
import androidx.room.Query
|
||||
import androidx.room.Transaction
|
||||
import androidx.room.Update
|
||||
import io.reactivex.Flowable
|
||||
import io.reactivex.Maybe
|
||||
import io.reactivex.rxjava3.core.Flowable
|
||||
import io.reactivex.rxjava3.core.Maybe
|
||||
import org.schabi.newpipe.database.feed.model.FeedGroupEntity
|
||||
import org.schabi.newpipe.database.feed.model.FeedGroupSubscriptionEntity
|
||||
|
||||
|
||||
@@ -10,21 +10,24 @@ import org.schabi.newpipe.database.feed.model.FeedEntity.Companion.SUBSCRIPTION_
|
||||
import org.schabi.newpipe.database.stream.model.StreamEntity
|
||||
import org.schabi.newpipe.database.subscription.SubscriptionEntity
|
||||
|
||||
@Entity(tableName = FEED_TABLE,
|
||||
primaryKeys = [STREAM_ID, SUBSCRIPTION_ID],
|
||||
indices = [Index(SUBSCRIPTION_ID)],
|
||||
foreignKeys = [
|
||||
ForeignKey(
|
||||
entity = StreamEntity::class,
|
||||
parentColumns = [StreamEntity.STREAM_ID],
|
||||
childColumns = [STREAM_ID],
|
||||
onDelete = ForeignKey.CASCADE, onUpdate = ForeignKey.CASCADE, deferred = true),
|
||||
ForeignKey(
|
||||
entity = SubscriptionEntity::class,
|
||||
parentColumns = [SubscriptionEntity.SUBSCRIPTION_UID],
|
||||
childColumns = [SUBSCRIPTION_ID],
|
||||
onDelete = ForeignKey.CASCADE, onUpdate = ForeignKey.CASCADE, deferred = true)
|
||||
]
|
||||
@Entity(
|
||||
tableName = FEED_TABLE,
|
||||
primaryKeys = [STREAM_ID, SUBSCRIPTION_ID],
|
||||
indices = [Index(SUBSCRIPTION_ID)],
|
||||
foreignKeys = [
|
||||
ForeignKey(
|
||||
entity = StreamEntity::class,
|
||||
parentColumns = [StreamEntity.STREAM_ID],
|
||||
childColumns = [STREAM_ID],
|
||||
onDelete = ForeignKey.CASCADE, onUpdate = ForeignKey.CASCADE, deferred = true
|
||||
),
|
||||
ForeignKey(
|
||||
entity = SubscriptionEntity::class,
|
||||
parentColumns = [SubscriptionEntity.SUBSCRIPTION_UID],
|
||||
childColumns = [SUBSCRIPTION_ID],
|
||||
onDelete = ForeignKey.CASCADE, onUpdate = ForeignKey.CASCADE, deferred = true
|
||||
)
|
||||
]
|
||||
)
|
||||
data class FeedEntity(
|
||||
@ColumnInfo(name = STREAM_ID)
|
||||
|
||||
@@ -9,8 +9,8 @@ import org.schabi.newpipe.database.feed.model.FeedGroupEntity.Companion.SORT_ORD
|
||||
import org.schabi.newpipe.local.subscription.FeedGroupIcon
|
||||
|
||||
@Entity(
|
||||
tableName = FEED_GROUP_TABLE,
|
||||
indices = [Index(SORT_ORDER)]
|
||||
tableName = FEED_GROUP_TABLE,
|
||||
indices = [Index(SORT_ORDER)]
|
||||
)
|
||||
data class FeedGroupEntity(
|
||||
@PrimaryKey(autoGenerate = true)
|
||||
|
||||
@@ -11,22 +11,24 @@ import org.schabi.newpipe.database.feed.model.FeedGroupSubscriptionEntity.Compan
|
||||
import org.schabi.newpipe.database.subscription.SubscriptionEntity
|
||||
|
||||
@Entity(
|
||||
tableName = FEED_GROUP_SUBSCRIPTION_TABLE,
|
||||
primaryKeys = [GROUP_ID, SUBSCRIPTION_ID],
|
||||
indices = [Index(SUBSCRIPTION_ID)],
|
||||
foreignKeys = [
|
||||
ForeignKey(
|
||||
entity = FeedGroupEntity::class,
|
||||
parentColumns = [FeedGroupEntity.ID],
|
||||
childColumns = [GROUP_ID],
|
||||
onDelete = CASCADE, onUpdate = CASCADE, deferred = true),
|
||||
tableName = FEED_GROUP_SUBSCRIPTION_TABLE,
|
||||
primaryKeys = [GROUP_ID, SUBSCRIPTION_ID],
|
||||
indices = [Index(SUBSCRIPTION_ID)],
|
||||
foreignKeys = [
|
||||
ForeignKey(
|
||||
entity = FeedGroupEntity::class,
|
||||
parentColumns = [FeedGroupEntity.ID],
|
||||
childColumns = [GROUP_ID],
|
||||
onDelete = CASCADE, onUpdate = CASCADE, deferred = true
|
||||
),
|
||||
|
||||
ForeignKey(
|
||||
entity = SubscriptionEntity::class,
|
||||
parentColumns = [SubscriptionEntity.SUBSCRIPTION_UID],
|
||||
childColumns = [SUBSCRIPTION_ID],
|
||||
onDelete = CASCADE, onUpdate = CASCADE, deferred = true)
|
||||
]
|
||||
ForeignKey(
|
||||
entity = SubscriptionEntity::class,
|
||||
parentColumns = [SubscriptionEntity.SUBSCRIPTION_UID],
|
||||
childColumns = [SUBSCRIPTION_ID],
|
||||
onDelete = CASCADE, onUpdate = CASCADE, deferred = true
|
||||
)
|
||||
]
|
||||
)
|
||||
data class FeedGroupSubscriptionEntity(
|
||||
@ColumnInfo(name = GROUP_ID)
|
||||
|
||||
@@ -4,20 +4,21 @@ import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.ForeignKey
|
||||
import androidx.room.PrimaryKey
|
||||
import java.time.OffsetDateTime
|
||||
import org.schabi.newpipe.database.feed.model.FeedLastUpdatedEntity.Companion.FEED_LAST_UPDATED_TABLE
|
||||
import org.schabi.newpipe.database.feed.model.FeedLastUpdatedEntity.Companion.SUBSCRIPTION_ID
|
||||
import org.schabi.newpipe.database.subscription.SubscriptionEntity
|
||||
import java.time.OffsetDateTime
|
||||
|
||||
@Entity(
|
||||
tableName = FEED_LAST_UPDATED_TABLE,
|
||||
foreignKeys = [
|
||||
ForeignKey(
|
||||
entity = SubscriptionEntity::class,
|
||||
parentColumns = [SubscriptionEntity.SUBSCRIPTION_UID],
|
||||
childColumns = [SUBSCRIPTION_ID],
|
||||
onDelete = ForeignKey.CASCADE, onUpdate = ForeignKey.CASCADE, deferred = true)
|
||||
]
|
||||
tableName = FEED_LAST_UPDATED_TABLE,
|
||||
foreignKeys = [
|
||||
ForeignKey(
|
||||
entity = SubscriptionEntity::class,
|
||||
parentColumns = [SubscriptionEntity.SUBSCRIPTION_UID],
|
||||
childColumns = [SUBSCRIPTION_ID],
|
||||
onDelete = ForeignKey.CASCADE, onUpdate = ForeignKey.CASCADE, deferred = true
|
||||
)
|
||||
]
|
||||
)
|
||||
data class FeedLastUpdatedEntity(
|
||||
@PrimaryKey
|
||||
|
||||
@@ -8,7 +8,7 @@ import org.schabi.newpipe.database.history.model.SearchHistoryEntry;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import io.reactivex.Flowable;
|
||||
import io.reactivex.rxjava3.core.Flowable;
|
||||
|
||||
import static org.schabi.newpipe.database.history.model.SearchHistoryEntry.CREATION_DATE;
|
||||
import static org.schabi.newpipe.database.history.model.SearchHistoryEntry.ID;
|
||||
|
||||
@@ -10,7 +10,7 @@ import org.schabi.newpipe.database.stream.StreamStatisticsEntry;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import io.reactivex.Flowable;
|
||||
import io.reactivex.rxjava3.core.Flowable;
|
||||
|
||||
import static org.schabi.newpipe.database.history.model.StreamHistoryEntity.JOIN_STREAM_ID;
|
||||
import static org.schabi.newpipe.database.history.model.StreamHistoryEntity.STREAM_ACCESS_DATE;
|
||||
@@ -20,6 +20,9 @@ import static org.schabi.newpipe.database.stream.StreamStatisticsEntry.STREAM_LA
|
||||
import static org.schabi.newpipe.database.stream.StreamStatisticsEntry.STREAM_WATCH_COUNT;
|
||||
import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_ID;
|
||||
import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_TABLE;
|
||||
import static org.schabi.newpipe.database.stream.model.StreamStateEntity.JOIN_STREAM_ID_ALIAS;
|
||||
import static org.schabi.newpipe.database.stream.model.StreamStateEntity.STREAM_PROGRESS_TIME;
|
||||
import static org.schabi.newpipe.database.stream.model.StreamStateEntity.STREAM_STATE_TABLE;
|
||||
|
||||
@Dao
|
||||
public abstract class StreamHistoryDAO implements HistoryDAO<StreamHistoryEntity> {
|
||||
@@ -73,6 +76,12 @@ public abstract class StreamHistoryDAO implements HistoryDAO<StreamHistoryEntity
|
||||
+ " SUM(" + STREAM_REPEAT_COUNT + ") AS " + STREAM_WATCH_COUNT
|
||||
+ " FROM " + STREAM_HISTORY_TABLE + " GROUP BY " + JOIN_STREAM_ID + ")"
|
||||
|
||||
+ " ON " + STREAM_ID + " = " + JOIN_STREAM_ID)
|
||||
+ " ON " + STREAM_ID + " = " + JOIN_STREAM_ID
|
||||
|
||||
+ " LEFT JOIN "
|
||||
+ "(SELECT " + JOIN_STREAM_ID + " AS " + JOIN_STREAM_ID_ALIAS + ", "
|
||||
+ STREAM_PROGRESS_TIME
|
||||
+ " FROM " + STREAM_STATE_TABLE + " )"
|
||||
+ " ON " + STREAM_ID + " = " + JOIN_STREAM_ID_ALIAS)
|
||||
public abstract Flowable<List<StreamStatisticsEntry>> getStatistics();
|
||||
}
|
||||
|
||||
@@ -2,8 +2,8 @@ package org.schabi.newpipe.database.history.model
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Embedded
|
||||
import java.time.OffsetDateTime
|
||||
import org.schabi.newpipe.database.stream.model.StreamEntity
|
||||
import java.time.OffsetDateTime
|
||||
|
||||
data class StreamHistoryEntry(
|
||||
@Embedded
|
||||
@@ -25,6 +25,6 @@ data class StreamHistoryEntry(
|
||||
|
||||
fun hasEqualValues(other: StreamHistoryEntry): Boolean {
|
||||
return this.streamEntity.uid == other.streamEntity.uid && streamId == other.streamId &&
|
||||
accessDate.isEqual(other.accessDate)
|
||||
accessDate.isEqual(other.accessDate)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,12 +5,17 @@ import androidx.room.Embedded
|
||||
import org.schabi.newpipe.database.LocalItem
|
||||
import org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity
|
||||
import org.schabi.newpipe.database.stream.model.StreamEntity
|
||||
import org.schabi.newpipe.database.stream.model.StreamStateEntity
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem
|
||||
import kotlin.jvm.Throws
|
||||
|
||||
class PlaylistStreamEntry(
|
||||
data class PlaylistStreamEntry(
|
||||
@Embedded
|
||||
val streamEntity: StreamEntity,
|
||||
|
||||
@ColumnInfo(name = StreamStateEntity.STREAM_PROGRESS_TIME, defaultValue = "0")
|
||||
val progressTime: Long,
|
||||
|
||||
@ColumnInfo(name = PlaylistStreamEntity.JOIN_STREAM_ID)
|
||||
val streamId: Long,
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import org.schabi.newpipe.database.playlist.model.PlaylistEntity;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import io.reactivex.Flowable;
|
||||
import io.reactivex.rxjava3.core.Flowable;
|
||||
|
||||
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_ID;
|
||||
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_TABLE;
|
||||
|
||||
@@ -9,7 +9,7 @@ import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import io.reactivex.Flowable;
|
||||
import io.reactivex.rxjava3.core.Flowable;
|
||||
|
||||
import static org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity.REMOTE_PLAYLIST_ID;
|
||||
import static org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity.REMOTE_PLAYLIST_SERVICE_ID;
|
||||
|
||||
@@ -11,7 +11,7 @@ import org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import io.reactivex.Flowable;
|
||||
import io.reactivex.rxjava3.core.Flowable;
|
||||
|
||||
import static org.schabi.newpipe.database.playlist.PlaylistMetadataEntry.PLAYLIST_STREAM_COUNT;
|
||||
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_ID;
|
||||
@@ -24,6 +24,9 @@ import static org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity.JO
|
||||
import static org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity.PLAYLIST_STREAM_JOIN_TABLE;
|
||||
import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_ID;
|
||||
import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_TABLE;
|
||||
import static org.schabi.newpipe.database.stream.model.StreamStateEntity.JOIN_STREAM_ID_ALIAS;
|
||||
import static org.schabi.newpipe.database.stream.model.StreamStateEntity.STREAM_PROGRESS_TIME;
|
||||
import static org.schabi.newpipe.database.stream.model.StreamStateEntity.STREAM_STATE_TABLE;
|
||||
|
||||
@Dao
|
||||
public abstract class PlaylistStreamDAO implements BasicDAO<PlaylistStreamEntity> {
|
||||
@@ -58,6 +61,13 @@ public abstract class PlaylistStreamDAO implements BasicDAO<PlaylistStreamEntity
|
||||
|
||||
// then merge with the stream metadata
|
||||
+ " ON " + STREAM_ID + " = " + JOIN_STREAM_ID
|
||||
|
||||
+ " LEFT JOIN "
|
||||
+ "(SELECT " + JOIN_STREAM_ID + " AS " + JOIN_STREAM_ID_ALIAS + ", "
|
||||
+ STREAM_PROGRESS_TIME
|
||||
+ " FROM " + STREAM_STATE_TABLE + " )"
|
||||
+ " ON " + STREAM_ID + " = " + JOIN_STREAM_ID_ALIAS
|
||||
|
||||
+ " ORDER BY " + JOIN_INDEX + " ASC")
|
||||
public abstract Flowable<List<PlaylistStreamEntry>> getOrderedStreamsOf(long playlistId);
|
||||
|
||||
|
||||
@@ -2,23 +2,27 @@ package org.schabi.newpipe.database.stream
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Embedded
|
||||
import java.time.OffsetDateTime
|
||||
import org.schabi.newpipe.database.LocalItem
|
||||
import org.schabi.newpipe.database.history.model.StreamHistoryEntity
|
||||
import org.schabi.newpipe.database.stream.model.StreamEntity
|
||||
import org.schabi.newpipe.database.stream.model.StreamStateEntity.STREAM_PROGRESS_TIME
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem
|
||||
import java.time.OffsetDateTime
|
||||
|
||||
class StreamStatisticsEntry(
|
||||
@Embedded
|
||||
@Embedded
|
||||
val streamEntity: StreamEntity,
|
||||
|
||||
@ColumnInfo(name = StreamHistoryEntity.JOIN_STREAM_ID)
|
||||
@ColumnInfo(name = STREAM_PROGRESS_TIME, defaultValue = "0")
|
||||
val progressTime: Long,
|
||||
|
||||
@ColumnInfo(name = StreamHistoryEntity.JOIN_STREAM_ID)
|
||||
val streamId: Long,
|
||||
|
||||
@ColumnInfo(name = STREAM_LATEST_DATE)
|
||||
@ColumnInfo(name = STREAM_LATEST_DATE)
|
||||
val latestAccessDate: OffsetDateTime,
|
||||
|
||||
@ColumnInfo(name = STREAM_WATCH_COUNT)
|
||||
@ColumnInfo(name = STREAM_WATCH_COUNT)
|
||||
val watchCount: Long
|
||||
) : LocalItem {
|
||||
fun toStreamInfoItem(): StreamInfoItem {
|
||||
|
||||
@@ -6,14 +6,14 @@ import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.Query
|
||||
import androidx.room.Transaction
|
||||
import io.reactivex.Flowable
|
||||
import java.time.OffsetDateTime
|
||||
import io.reactivex.rxjava3.core.Flowable
|
||||
import org.schabi.newpipe.database.BasicDAO
|
||||
import org.schabi.newpipe.database.stream.model.StreamEntity
|
||||
import org.schabi.newpipe.database.stream.model.StreamEntity.Companion.STREAM_ID
|
||||
import org.schabi.newpipe.extractor.stream.StreamType
|
||||
import org.schabi.newpipe.extractor.stream.StreamType.AUDIO_LIVE_STREAM
|
||||
import org.schabi.newpipe.extractor.stream.StreamType.LIVE_STREAM
|
||||
import java.time.OffsetDateTime
|
||||
|
||||
@Dao
|
||||
abstract class StreamDAO : BasicDAO<StreamEntity> {
|
||||
@@ -35,10 +35,12 @@ abstract class StreamDAO : BasicDAO<StreamEntity> {
|
||||
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
||||
internal abstract fun silentInsertAllInternal(streams: List<StreamEntity>): List<Long>
|
||||
|
||||
@Query("""
|
||||
@Query(
|
||||
"""
|
||||
SELECT uid, stream_type, textual_upload_date, upload_date, is_upload_date_approximation, duration
|
||||
FROM streams WHERE url = :url AND service_id = :serviceId
|
||||
""")
|
||||
"""
|
||||
)
|
||||
internal abstract fun getMinimalStreamForCompare(serviceId: Int, url: String): StreamCompareFeed?
|
||||
|
||||
@Transaction
|
||||
@@ -79,7 +81,7 @@ abstract class StreamDAO : BasicDAO<StreamEntity> {
|
||||
|
||||
private fun compareAndUpdateStream(newerStream: StreamEntity) {
|
||||
val existentMinimalStream = getMinimalStreamForCompare(newerStream.serviceId, newerStream.url)
|
||||
?: throw IllegalStateException("Stream cannot be null just after insertion.")
|
||||
?: throw IllegalStateException("Stream cannot be null just after insertion.")
|
||||
newerStream.uid = existentMinimalStream.uid
|
||||
|
||||
val isNewerStreamLive = newerStream.streamType == AUDIO_LIVE_STREAM || newerStream.streamType == LIVE_STREAM
|
||||
@@ -88,7 +90,7 @@ abstract class StreamDAO : BasicDAO<StreamEntity> {
|
||||
// Use the existent upload date if the newer stream does not have a better precision
|
||||
// (i.e. is an approximation). This is done to prevent unnecessary changes.
|
||||
val hasBetterPrecision =
|
||||
newerStream.uploadDate != null && newerStream.isUploadDateApproximation != true
|
||||
newerStream.uploadDate != null && newerStream.isUploadDateApproximation != true
|
||||
if (existentMinimalStream.uploadDate != null && !hasBetterPrecision) {
|
||||
newerStream.uploadDate = existentMinimalStream.uploadDate
|
||||
newerStream.textualUploadDate = existentMinimalStream.textualUploadDate
|
||||
@@ -101,7 +103,8 @@ abstract class StreamDAO : BasicDAO<StreamEntity> {
|
||||
}
|
||||
}
|
||||
|
||||
@Query("""
|
||||
@Query(
|
||||
"""
|
||||
DELETE FROM streams WHERE
|
||||
|
||||
NOT EXISTS (SELECT 1 FROM stream_history sh
|
||||
@@ -112,7 +115,8 @@ abstract class StreamDAO : BasicDAO<StreamEntity> {
|
||||
|
||||
AND NOT EXISTS (SELECT 1 FROM feed f
|
||||
WHERE f.stream_id = streams.uid)
|
||||
""")
|
||||
"""
|
||||
)
|
||||
abstract fun deleteOrphans(): Int
|
||||
|
||||
/**
|
||||
|
||||
@@ -11,7 +11,7 @@ import org.schabi.newpipe.database.stream.model.StreamStateEntity;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import io.reactivex.Flowable;
|
||||
import io.reactivex.rxjava3.core.Flowable;
|
||||
|
||||
import static org.schabi.newpipe.database.stream.model.StreamStateEntity.JOIN_STREAM_ID;
|
||||
import static org.schabi.newpipe.database.stream.model.StreamStateEntity.STREAM_STATE_TABLE;
|
||||
|
||||
@@ -5,8 +5,6 @@ import androidx.room.Entity
|
||||
import androidx.room.Ignore
|
||||
import androidx.room.Index
|
||||
import androidx.room.PrimaryKey
|
||||
import java.io.Serializable
|
||||
import java.time.OffsetDateTime
|
||||
import org.schabi.newpipe.database.stream.model.StreamEntity.Companion.STREAM_SERVICE_ID
|
||||
import org.schabi.newpipe.database.stream.model.StreamEntity.Companion.STREAM_TABLE
|
||||
import org.schabi.newpipe.database.stream.model.StreamEntity.Companion.STREAM_URL
|
||||
@@ -15,11 +13,14 @@ import org.schabi.newpipe.extractor.stream.StreamInfo
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem
|
||||
import org.schabi.newpipe.extractor.stream.StreamType
|
||||
import org.schabi.newpipe.player.playqueue.PlayQueueItem
|
||||
import java.io.Serializable
|
||||
import java.time.OffsetDateTime
|
||||
|
||||
@Entity(tableName = STREAM_TABLE,
|
||||
indices = [
|
||||
Index(value = [STREAM_SERVICE_ID, STREAM_URL], unique = true)
|
||||
]
|
||||
@Entity(
|
||||
tableName = STREAM_TABLE,
|
||||
indices = [
|
||||
Index(value = [STREAM_SERVICE_ID, STREAM_URL], unique = true)
|
||||
]
|
||||
)
|
||||
data class StreamEntity(
|
||||
@PrimaryKey(autoGenerate = true)
|
||||
@@ -61,27 +62,27 @@ data class StreamEntity(
|
||||
) : Serializable {
|
||||
@Ignore
|
||||
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,
|
||||
textualUploadDate = item.textualUploadDate, uploadDate = item.uploadDate?.offsetDateTime(),
|
||||
isUploadDateApproximation = item.uploadDate?.isApproximation
|
||||
serviceId = item.serviceId, url = item.url, title = item.name,
|
||||
streamType = item.streamType, duration = item.duration, uploader = item.uploaderName,
|
||||
thumbnailUrl = item.thumbnailUrl, viewCount = item.viewCount,
|
||||
textualUploadDate = item.textualUploadDate, uploadDate = item.uploadDate?.offsetDateTime(),
|
||||
isUploadDateApproximation = item.uploadDate?.isApproximation
|
||||
)
|
||||
|
||||
@Ignore
|
||||
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,
|
||||
textualUploadDate = info.textualUploadDate, uploadDate = info.uploadDate?.offsetDateTime(),
|
||||
isUploadDateApproximation = info.uploadDate?.isApproximation
|
||||
serviceId = info.serviceId, url = info.url, title = info.name,
|
||||
streamType = info.streamType, duration = info.duration, uploader = info.uploaderName,
|
||||
thumbnailUrl = info.thumbnailUrl, viewCount = info.viewCount,
|
||||
textualUploadDate = info.textualUploadDate, uploadDate = info.uploadDate?.offsetDateTime(),
|
||||
isUploadDateApproximation = info.uploadDate?.isApproximation
|
||||
)
|
||||
|
||||
@Ignore
|
||||
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
|
||||
serviceId = item.serviceId, url = item.url, title = item.title,
|
||||
streamType = item.streamType, duration = item.duration, uploader = item.uploader,
|
||||
thumbnailUrl = item.thumbnailUrl
|
||||
)
|
||||
|
||||
fun toStreamInfoItem(): StreamInfoItem {
|
||||
|
||||
@@ -22,6 +22,9 @@ import static org.schabi.newpipe.database.stream.model.StreamStateEntity.STREAM_
|
||||
public class StreamStateEntity {
|
||||
public static final String STREAM_STATE_TABLE = "stream_state";
|
||||
public static final String JOIN_STREAM_ID = "stream_id";
|
||||
// This additional field is required for the SQL query because 'stream_id' is used
|
||||
// for some other joins already
|
||||
public static final String JOIN_STREAM_ID_ALIAS = "stream_id_alias";
|
||||
public static final String STREAM_PROGRESS_TIME = "progress_time";
|
||||
|
||||
/**
|
||||
|
||||
@@ -5,8 +5,8 @@ import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.Query
|
||||
import androidx.room.Transaction
|
||||
import io.reactivex.Flowable
|
||||
import io.reactivex.Maybe
|
||||
import io.reactivex.rxjava3.core.Flowable
|
||||
import io.reactivex.rxjava3.core.Maybe
|
||||
import org.schabi.newpipe.database.BasicDAO
|
||||
|
||||
@Dao
|
||||
@@ -20,16 +20,19 @@ abstract class SubscriptionDAO : BasicDAO<SubscriptionEntity> {
|
||||
@Query("SELECT * FROM subscriptions ORDER BY name COLLATE NOCASE ASC")
|
||||
abstract override fun getAll(): Flowable<List<SubscriptionEntity>>
|
||||
|
||||
@Query("""
|
||||
@Query(
|
||||
"""
|
||||
SELECT * FROM subscriptions
|
||||
|
||||
WHERE name LIKE '%' || :filter || '%'
|
||||
|
||||
ORDER BY name COLLATE NOCASE ASC
|
||||
""")
|
||||
"""
|
||||
)
|
||||
abstract fun getSubscriptionsFiltered(filter: String): Flowable<List<SubscriptionEntity>>
|
||||
|
||||
@Query("""
|
||||
@Query(
|
||||
"""
|
||||
SELECT * FROM subscriptions s
|
||||
|
||||
LEFT JOIN feed_group_subscription_join fgs
|
||||
@@ -38,12 +41,14 @@ abstract class SubscriptionDAO : BasicDAO<SubscriptionEntity> {
|
||||
WHERE (fgs.subscription_id IS NULL OR fgs.group_id = :currentGroupId)
|
||||
|
||||
ORDER BY name COLLATE NOCASE ASC
|
||||
""")
|
||||
"""
|
||||
)
|
||||
abstract fun getSubscriptionsOnlyUngrouped(
|
||||
currentGroupId: Long
|
||||
): Flowable<List<SubscriptionEntity>>
|
||||
|
||||
@Query("""
|
||||
@Query(
|
||||
"""
|
||||
SELECT * FROM subscriptions s
|
||||
|
||||
LEFT JOIN feed_group_subscription_join fgs
|
||||
@@ -53,7 +58,8 @@ abstract class SubscriptionDAO : BasicDAO<SubscriptionEntity> {
|
||||
AND s.name LIKE '%' || :filter || '%'
|
||||
|
||||
ORDER BY name COLLATE NOCASE ASC
|
||||
""")
|
||||
"""
|
||||
)
|
||||
abstract fun getSubscriptionsOnlyUngroupedFiltered(
|
||||
currentGroupId: Long,
|
||||
filter: String
|
||||
|
||||
@@ -9,10 +9,10 @@ import android.view.ViewTreeObserver;
|
||||
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.fragment.app.FragmentTransaction;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.databinding.ActivityDownloaderBinding;
|
||||
import org.schabi.newpipe.util.DeviceUtils;
|
||||
import org.schabi.newpipe.util.ThemeHelper;
|
||||
import org.schabi.newpipe.views.FocusOverlayView;
|
||||
@@ -35,11 +35,14 @@ public class DownloadActivity extends AppCompatActivity {
|
||||
|
||||
assureCorrectAppLanguage(this);
|
||||
ThemeHelper.setTheme(this);
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_downloader);
|
||||
|
||||
final Toolbar toolbar = findViewById(R.id.toolbar);
|
||||
setSupportActionBar(toolbar);
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
final ActivityDownloaderBinding downloaderBinding =
|
||||
ActivityDownloaderBinding.inflate(getLayoutInflater());
|
||||
setContentView(downloaderBinding.getRoot());
|
||||
|
||||
setSupportActionBar(downloaderBinding.toolbarLayout.toolbar);
|
||||
|
||||
final ActionBar actionBar = getSupportActionBar();
|
||||
if (actionBar != null) {
|
||||
|
||||
@@ -10,7 +10,6 @@ import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.os.Environment;
|
||||
import android.os.IBinder;
|
||||
import androidx.preference.PreferenceManager;
|
||||
import android.util.Log;
|
||||
import android.util.SparseArray;
|
||||
import android.view.LayoutInflater;
|
||||
@@ -34,6 +33,7 @@ import androidx.appcompat.view.menu.ActionMenuItemView;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.documentfile.provider.DocumentFile;
|
||||
import androidx.fragment.app.DialogFragment;
|
||||
import androidx.preference.PreferenceManager;
|
||||
|
||||
import com.nononsenseapps.filepicker.Utils;
|
||||
|
||||
@@ -49,6 +49,7 @@ import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||
import org.schabi.newpipe.extractor.stream.SubtitlesStream;
|
||||
import org.schabi.newpipe.extractor.stream.VideoStream;
|
||||
import org.schabi.newpipe.report.ErrorActivity;
|
||||
import org.schabi.newpipe.report.ErrorInfo;
|
||||
import org.schabi.newpipe.report.UserAction;
|
||||
import org.schabi.newpipe.settings.NewPipeSettings;
|
||||
import org.schabi.newpipe.util.FilePickerActivityHelper;
|
||||
@@ -69,7 +70,7 @@ import java.util.Locale;
|
||||
|
||||
import icepick.Icepick;
|
||||
import icepick.State;
|
||||
import io.reactivex.disposables.CompositeDisposable;
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
||||
import us.shandian.giga.get.MissionRecoveryInfo;
|
||||
import us.shandian.giga.io.StoredDirectoryHelper;
|
||||
import us.shandian.giga.io.StoredFileHelper;
|
||||
@@ -602,7 +603,7 @@ public class DownloadDialog extends DialogFragment
|
||||
Collections.singletonList(e),
|
||||
null,
|
||||
null,
|
||||
ErrorActivity.ErrorInfo
|
||||
ErrorInfo
|
||||
.make(UserAction.SOMETHING_ELSE, "-", "-", R.string.general_error)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ import android.widget.Toast;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.StringRes;
|
||||
|
||||
import com.jakewharton.rxbinding2.view.RxView;
|
||||
import com.jakewharton.rxbinding4.view.RxView;
|
||||
|
||||
import org.schabi.newpipe.BaseFragment;
|
||||
import org.schabi.newpipe.MainActivity;
|
||||
@@ -23,6 +23,7 @@ import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException;
|
||||
import org.schabi.newpipe.extractor.exceptions.ContentNotSupportedException;
|
||||
import org.schabi.newpipe.extractor.exceptions.ReCaptchaException;
|
||||
import org.schabi.newpipe.report.ErrorActivity;
|
||||
import org.schabi.newpipe.report.ErrorInfo;
|
||||
import org.schabi.newpipe.report.UserAction;
|
||||
import org.schabi.newpipe.util.ExceptionUtils;
|
||||
import org.schabi.newpipe.util.InfoCache;
|
||||
@@ -33,7 +34,8 @@ import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
import icepick.State;
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.rxjava3.disposables.Disposable;
|
||||
|
||||
import static org.schabi.newpipe.util.AnimationUtils.animateView;
|
||||
|
||||
@@ -47,6 +49,8 @@ public abstract class BaseStateFragment<I> extends BaseFragment implements ViewC
|
||||
@Nullable
|
||||
private ProgressBar loadingProgressBar;
|
||||
|
||||
private Disposable errorDisposable;
|
||||
|
||||
protected View errorPanelRoot;
|
||||
private Button errorButtonRetry;
|
||||
private TextView errorTextView;
|
||||
@@ -63,6 +67,14 @@ public abstract class BaseStateFragment<I> extends BaseFragment implements ViewC
|
||||
wasLoading.set(isLoading.get());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
if (errorDisposable != null) {
|
||||
errorDisposable.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Init
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
@@ -82,7 +94,7 @@ public abstract class BaseStateFragment<I> extends BaseFragment implements ViewC
|
||||
@Override
|
||||
protected void initListeners() {
|
||||
super.initListeners();
|
||||
RxView.clicks(errorButtonRetry)
|
||||
errorDisposable = RxView.clicks(errorButtonRetry)
|
||||
.debounce(300, TimeUnit.MILLISECONDS)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(o -> onRetryButtonClicked());
|
||||
@@ -252,7 +264,7 @@ public abstract class BaseStateFragment<I> extends BaseFragment implements ViewC
|
||||
}
|
||||
|
||||
ErrorActivity.reportError(getContext(), exception, MainActivity.class, null,
|
||||
ErrorActivity.ErrorInfo.make(userAction, serviceName == null ? "none" : serviceName,
|
||||
ErrorInfo.make(userAction, serviceName == null ? "none" : serviceName,
|
||||
request == null ? "none" : request, errorId));
|
||||
}
|
||||
|
||||
@@ -265,7 +277,7 @@ public abstract class BaseStateFragment<I> extends BaseFragment implements ViewC
|
||||
|
||||
/**
|
||||
* Show a SnackBar and only call
|
||||
* {@link ErrorActivity#reportError(Context, List, Class, View, ErrorActivity.ErrorInfo)}
|
||||
* {@link ErrorActivity#reportError(Context, List, Class, View, ErrorInfo)}
|
||||
* IF we a find a valid view (otherwise the error screen appears).
|
||||
*
|
||||
* @param exception List of the exceptions to show
|
||||
@@ -291,6 +303,6 @@ public abstract class BaseStateFragment<I> extends BaseFragment implements ViewC
|
||||
}
|
||||
|
||||
ErrorActivity.reportError(getContext(), exception, MainActivity.class, rootView,
|
||||
ErrorActivity.ErrorInfo.make(userAction, serviceName, request, errorId));
|
||||
ErrorInfo.make(userAction, serviceName, request, errorId));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ package org.schabi.newpipe.fragments;
|
||||
import android.content.Context;
|
||||
import android.content.res.ColorStateList;
|
||||
import android.os.Bundle;
|
||||
import androidx.preference.PreferenceManager;
|
||||
import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
@@ -19,6 +18,7 @@ import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
import androidx.fragment.app.FragmentStatePagerAdapterMenuWorkaround;
|
||||
import androidx.preference.PreferenceManager;
|
||||
import androidx.viewpager.widget.ViewPager;
|
||||
|
||||
import com.google.android.material.tabs.TabLayout;
|
||||
@@ -27,6 +27,7 @@ import org.schabi.newpipe.BaseFragment;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||
import org.schabi.newpipe.report.ErrorActivity;
|
||||
import org.schabi.newpipe.report.ErrorInfo;
|
||||
import org.schabi.newpipe.report.UserAction;
|
||||
import org.schabi.newpipe.settings.tabs.Tab;
|
||||
import org.schabi.newpipe.settings.tabs.TabsManager;
|
||||
@@ -43,7 +44,7 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte
|
||||
private SelectedTabsPagerAdapter pagerAdapter;
|
||||
private ScrollableTabLayout tabLayout;
|
||||
|
||||
private List<Tab> tabsList = new ArrayList<>();
|
||||
private final List<Tab> tabsList = new ArrayList<>();
|
||||
private TabsManager tabsManager;
|
||||
|
||||
private boolean hasTabsChanged = false;
|
||||
@@ -242,7 +243,7 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte
|
||||
}
|
||||
|
||||
if (throwable != null) {
|
||||
ErrorActivity.reportError(context, throwable, null, null, ErrorActivity.ErrorInfo
|
||||
ErrorActivity.reportError(context, throwable, null, null, ErrorInfo
|
||||
.make(UserAction.UI_ERROR, "none", "", R.string.app_ui_crash));
|
||||
return new BlankFragment();
|
||||
}
|
||||
|
||||
@@ -14,12 +14,10 @@ public abstract class OnScrollBelowItemsListener extends RecyclerView.OnScrollLi
|
||||
super.onScrolled(recyclerView, dx, dy);
|
||||
if (dy > 0) {
|
||||
int pastVisibleItems = 0;
|
||||
final int visibleItemCount;
|
||||
final int totalItemCount;
|
||||
final RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager();
|
||||
|
||||
visibleItemCount = layoutManager.getChildCount();
|
||||
totalItemCount = layoutManager.getItemCount();
|
||||
final int visibleItemCount = layoutManager.getChildCount();
|
||||
final int totalItemCount = layoutManager.getItemCount();
|
||||
|
||||
// Already covers the GridLayoutManager case
|
||||
if (layoutManager instanceof LinearLayoutManager) {
|
||||
|
||||
@@ -15,12 +15,7 @@ import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.view.ViewTreeObserver;
|
||||
import androidx.core.text.HtmlCompat;
|
||||
import androidx.preference.PreferenceManager;
|
||||
import android.provider.Settings;
|
||||
import android.text.Spanned;
|
||||
import android.text.TextUtils;
|
||||
import android.text.util.Linkify;
|
||||
import android.util.DisplayMetrics;
|
||||
import android.util.Log;
|
||||
@@ -28,6 +23,7 @@ import android.view.LayoutInflater;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.ViewTreeObserver;
|
||||
import android.view.WindowManager;
|
||||
import android.view.animation.DecelerateInterpolator;
|
||||
import android.widget.FrameLayout;
|
||||
@@ -46,7 +42,9 @@ import androidx.appcompat.content.res.AppCompatResources;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.coordinatorlayout.widget.CoordinatorLayout;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.core.text.HtmlCompat;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.preference.PreferenceManager;
|
||||
import androidx.viewpager.widget.ViewPager;
|
||||
|
||||
import com.google.android.exoplayer2.ExoPlaybackException;
|
||||
@@ -93,6 +91,7 @@ import org.schabi.newpipe.player.playqueue.PlayQueue;
|
||||
import org.schabi.newpipe.player.playqueue.PlayQueueItem;
|
||||
import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
|
||||
import org.schabi.newpipe.report.ErrorActivity;
|
||||
import org.schabi.newpipe.report.ErrorInfo;
|
||||
import org.schabi.newpipe.report.UserAction;
|
||||
import org.schabi.newpipe.util.Constants;
|
||||
import org.schabi.newpipe.util.DeviceUtils;
|
||||
@@ -116,18 +115,20 @@ import java.util.concurrent.TimeUnit;
|
||||
import icepick.State;
|
||||
import io.noties.markwon.Markwon;
|
||||
import io.noties.markwon.linkify.LinkifyPlugin;
|
||||
import io.reactivex.Single;
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.disposables.CompositeDisposable;
|
||||
import io.reactivex.disposables.Disposable;
|
||||
import io.reactivex.schedulers.Schedulers;
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.rxjava3.core.Single;
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
||||
import io.reactivex.rxjava3.disposables.Disposable;
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers;
|
||||
|
||||
import static android.text.TextUtils.isEmpty;
|
||||
import static org.schabi.newpipe.extractor.StreamingService.ServiceInfo.MediaCapability.COMMENTS;
|
||||
import static org.schabi.newpipe.extractor.stream.StreamExtractor.NO_AGE_LIMIT;
|
||||
import static org.schabi.newpipe.player.helper.PlayerHelper.globalScreenOrientationLocked;
|
||||
import static org.schabi.newpipe.player.helper.PlayerHelper.isClearingQueueConfirmationRequired;
|
||||
import static org.schabi.newpipe.player.playqueue.PlayQueueItem.RECOVERY_UNSET;
|
||||
import static org.schabi.newpipe.util.AnimationUtils.animateView;
|
||||
import static org.schabi.newpipe.util.ExtractorHelper.showMetaInfoInTextView;
|
||||
|
||||
public final class VideoDetailFragment
|
||||
extends BaseStateFragment<StreamInfo>
|
||||
@@ -218,6 +219,9 @@ public final class VideoDetailFragment
|
||||
private TextView detailDurationView;
|
||||
private TextView detailPositionView;
|
||||
|
||||
private View detailMetaInfoSeparator;
|
||||
private TextView detailMetaInfoTextView;
|
||||
|
||||
private LinearLayout videoDescriptionRootLayout;
|
||||
private TextView videoUploadDateView;
|
||||
private TextView videoDescriptionView;
|
||||
@@ -508,8 +512,8 @@ public final class VideoDetailFragment
|
||||
}
|
||||
break;
|
||||
case R.id.detail_uploader_root_layout:
|
||||
if (TextUtils.isEmpty(currentInfo.getSubChannelUrl())) {
|
||||
if (!TextUtils.isEmpty(currentInfo.getUploaderUrl())) {
|
||||
if (isEmpty(currentInfo.getSubChannelUrl())) {
|
||||
if (!isEmpty(currentInfo.getUploaderUrl())) {
|
||||
openChannel(currentInfo.getUploaderUrl(), currentInfo.getUploaderName());
|
||||
}
|
||||
|
||||
@@ -583,7 +587,7 @@ public final class VideoDetailFragment
|
||||
}
|
||||
break;
|
||||
case R.id.detail_uploader_root_layout:
|
||||
if (TextUtils.isEmpty(currentInfo.getSubChannelUrl())) {
|
||||
if (isEmpty(currentInfo.getSubChannelUrl())) {
|
||||
Log.w(TAG,
|
||||
"Can't open parent channel because we got no parent channel URL");
|
||||
} else {
|
||||
@@ -644,6 +648,9 @@ public final class VideoDetailFragment
|
||||
detailDurationView = rootView.findViewById(R.id.detail_duration_view);
|
||||
detailPositionView = rootView.findViewById(R.id.detail_position_view);
|
||||
|
||||
detailMetaInfoSeparator = rootView.findViewById(R.id.detail_meta_info_separator);
|
||||
detailMetaInfoTextView = rootView.findViewById(R.id.detail_meta_info_text_view);
|
||||
|
||||
videoDescriptionRootLayout = rootView.findViewById(R.id.detail_description_root_layout);
|
||||
videoUploadDateView = rootView.findViewById(R.id.detail_upload_date_view);
|
||||
videoDescriptionView = rootView.findViewById(R.id.detail_description_view);
|
||||
@@ -731,7 +738,7 @@ public final class VideoDetailFragment
|
||||
}
|
||||
|
||||
private View.OnTouchListener getOnControlsTouchListener() {
|
||||
return (View view, MotionEvent motionEvent) -> {
|
||||
return (view, motionEvent) -> {
|
||||
if (!PreferenceManager.getDefaultSharedPreferences(activity)
|
||||
.getBoolean(getString(R.string.show_hold_to_append_key), true)) {
|
||||
return false;
|
||||
@@ -748,7 +755,7 @@ public final class VideoDetailFragment
|
||||
private void initThumbnailViews(@NonNull final StreamInfo info) {
|
||||
thumbnailImageView.setImageResource(R.drawable.dummy_thumbnail_dark);
|
||||
|
||||
if (!TextUtils.isEmpty(info.getThumbnailUrl())) {
|
||||
if (!isEmpty(info.getThumbnailUrl())) {
|
||||
final String infoServiceName = NewPipe.getNameOfService(info.getServiceId());
|
||||
final ImageLoadingListener onFailListener = new SimpleImageLoadingListener() {
|
||||
@Override
|
||||
@@ -763,12 +770,12 @@ public final class VideoDetailFragment
|
||||
ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS, onFailListener);
|
||||
}
|
||||
|
||||
if (!TextUtils.isEmpty(info.getSubChannelAvatarUrl())) {
|
||||
if (!isEmpty(info.getSubChannelAvatarUrl())) {
|
||||
IMAGE_LOADER.displayImage(info.getSubChannelAvatarUrl(), subChannelThumb,
|
||||
ImageDisplayConstants.DISPLAY_AVATAR_OPTIONS);
|
||||
}
|
||||
|
||||
if (!TextUtils.isEmpty(info.getUploaderAvatarUrl())) {
|
||||
if (!isEmpty(info.getUploaderAvatarUrl())) {
|
||||
IMAGE_LOADER.displayImage(info.getUploaderAvatarUrl(), uploaderThumb,
|
||||
ImageDisplayConstants.DISPLAY_AVATAR_OPTIONS);
|
||||
}
|
||||
@@ -948,7 +955,7 @@ public final class VideoDetailFragment
|
||||
currentWorker = ExtractorHelper.getStreamInfo(serviceId, url, forceLoad)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe((@NonNull final StreamInfo result) -> {
|
||||
.subscribe(result -> {
|
||||
isLoading.set(false);
|
||||
hideMainPlayer();
|
||||
if (result.getAgeLimit() != NO_AGE_LIMIT && !prefs.getBoolean(
|
||||
@@ -969,7 +976,7 @@ public final class VideoDetailFragment
|
||||
openVideoPlayer();
|
||||
}
|
||||
}
|
||||
}, (@NonNull final Throwable throwable) -> {
|
||||
}, throwable -> {
|
||||
isLoading.set(false);
|
||||
onError(throwable);
|
||||
});
|
||||
@@ -1140,7 +1147,7 @@ public final class VideoDetailFragment
|
||||
|
||||
PlayQueue queue = playQueue;
|
||||
// Size can be 0 because queue removes bad stream automatically when error occurs
|
||||
if (queue == null || queue.size() == 0) {
|
||||
if (queue == null || queue.isEmpty()) {
|
||||
queue = new SinglePlayQueue(currentInfo);
|
||||
}
|
||||
|
||||
@@ -1217,19 +1224,19 @@ public final class VideoDetailFragment
|
||||
}
|
||||
|
||||
private void prepareDescription(final Description description) {
|
||||
if (description == null || TextUtils.isEmpty(description.getContent())
|
||||
if (description == null || isEmpty(description.getContent())
|
||||
|| description == Description.emptyDescription) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (description.getType() == Description.HTML) {
|
||||
disposables.add(Single.just(description.getContent())
|
||||
.map((@NonNull final String descriptionText) ->
|
||||
.map(descriptionText ->
|
||||
HtmlCompat.fromHtml(descriptionText,
|
||||
HtmlCompat.FROM_HTML_MODE_LEGACY))
|
||||
.subscribeOn(Schedulers.computation())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe((@NonNull final Spanned spanned) -> {
|
||||
.subscribe(spanned -> {
|
||||
videoDescriptionView.setText(spanned);
|
||||
videoDescriptionView.setVisibility(View.VISIBLE);
|
||||
}));
|
||||
@@ -1346,19 +1353,24 @@ public final class VideoDetailFragment
|
||||
broadcastReceiver = new BroadcastReceiver() {
|
||||
@Override
|
||||
public void onReceive(final Context context, final Intent intent) {
|
||||
if (intent.getAction().equals(ACTION_SHOW_MAIN_PLAYER)) {
|
||||
bottomSheetBehavior.setState(BottomSheetBehavior.STATE_EXPANDED);
|
||||
} else if (intent.getAction().equals(ACTION_HIDE_MAIN_PLAYER)) {
|
||||
bottomSheetBehavior.setState(BottomSheetBehavior.STATE_HIDDEN);
|
||||
} else if (intent.getAction().equals(ACTION_PLAYER_STARTED)) {
|
||||
// If the state is not hidden we don't need to show the mini player
|
||||
if (bottomSheetBehavior.getState() == BottomSheetBehavior.STATE_HIDDEN) {
|
||||
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);
|
||||
}
|
||||
switch (intent.getAction()) {
|
||||
case ACTION_SHOW_MAIN_PLAYER:
|
||||
bottomSheetBehavior.setState(BottomSheetBehavior.STATE_EXPANDED);
|
||||
break;
|
||||
case ACTION_HIDE_MAIN_PLAYER:
|
||||
bottomSheetBehavior.setState(BottomSheetBehavior.STATE_HIDDEN);
|
||||
break;
|
||||
case ACTION_PLAYER_STARTED:
|
||||
// If the state is not hidden we don't need to show the mini player
|
||||
if (bottomSheetBehavior.getState() == BottomSheetBehavior.STATE_HIDDEN) {
|
||||
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);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -1457,9 +1469,9 @@ public final class VideoDetailFragment
|
||||
animateView(thumbnailPlayButton, true, 200);
|
||||
videoTitleTextView.setText(title);
|
||||
|
||||
if (!TextUtils.isEmpty(info.getSubChannelName())) {
|
||||
if (!isEmpty(info.getSubChannelName())) {
|
||||
displayBothUploaderAndSubChannel(info);
|
||||
} else if (!TextUtils.isEmpty(info.getUploaderName())) {
|
||||
} else if (!isEmpty(info.getUploaderName())) {
|
||||
displayUploaderAsSubChannel(info);
|
||||
} else {
|
||||
uploaderTextView.setVisibility(View.GONE);
|
||||
@@ -1554,6 +1566,8 @@ public final class VideoDetailFragment
|
||||
prepareDescription(info.getDescription());
|
||||
updateProgressInfo(info);
|
||||
initThumbnailViews(info);
|
||||
showMetaInfoInTextView(info.getMetaInfo(), detailMetaInfoTextView, detailMetaInfoSeparator);
|
||||
|
||||
|
||||
if (player == null || player.isPlayerStopped()) {
|
||||
updateOverlayData(info.getName(), info.getUploaderName(), info.getThumbnailUrl());
|
||||
@@ -1605,7 +1619,7 @@ public final class VideoDetailFragment
|
||||
|
||||
subChannelThumb.setVisibility(View.VISIBLE);
|
||||
|
||||
if (!TextUtils.isEmpty(info.getUploaderName())) {
|
||||
if (!isEmpty(info.getUploaderName())) {
|
||||
uploaderTextView.setText(
|
||||
String.format(getString(R.string.video_detail_by), info.getUploaderName()));
|
||||
uploaderTextView.setVisibility(View.VISIBLE);
|
||||
@@ -1626,7 +1640,7 @@ public final class VideoDetailFragment
|
||||
|
||||
downloadDialog.show(activity.getSupportFragmentManager(), "downloadDialog");
|
||||
} catch (final Exception e) {
|
||||
final ErrorActivity.ErrorInfo info = ErrorActivity.ErrorInfo.make(UserAction.UI_ERROR,
|
||||
final ErrorInfo info = ErrorInfo.make(UserAction.UI_ERROR,
|
||||
ServiceList.all()
|
||||
.get(currentInfo
|
||||
.getServiceId())
|
||||
@@ -2300,10 +2314,10 @@ public final class VideoDetailFragment
|
||||
private void updateOverlayData(@Nullable final String overlayTitle,
|
||||
@Nullable final String uploader,
|
||||
@Nullable final String thumbnailUrl) {
|
||||
overlayTitleTextView.setText(TextUtils.isEmpty(overlayTitle) ? "" : overlayTitle);
|
||||
overlayChannelTextView.setText(TextUtils.isEmpty(uploader) ? "" : uploader);
|
||||
overlayTitleTextView.setText(isEmpty(title) ? "" : title);
|
||||
overlayChannelTextView.setText(isEmpty(uploader) ? "" : uploader);
|
||||
overlayThumbnailImageView.setImageResource(R.drawable.dummy_thumbnail_dark);
|
||||
if (!TextUtils.isEmpty(thumbnailUrl)) {
|
||||
if (!isEmpty(thumbnailUrl)) {
|
||||
IMAGE_LOADER.displayImage(thumbnailUrl, overlayThumbnailImageView,
|
||||
ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS, null);
|
||||
}
|
||||
|
||||
@@ -48,7 +48,7 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I>
|
||||
implements ListViewContract<I, N>, StateSaver.WriteRead,
|
||||
SharedPreferences.OnSharedPreferenceChangeListener {
|
||||
private static final int LIST_MODE_UPDATE_FLAG = 0x32;
|
||||
protected StateSaver.SavedState savedState;
|
||||
protected org.schabi.newpipe.util.SavedState savedState;
|
||||
|
||||
private boolean useDefaultStateSaving = true;
|
||||
private int updateFlags = 0;
|
||||
|
||||
@@ -16,10 +16,10 @@ import org.schabi.newpipe.views.NewPipeRecyclerView;
|
||||
import java.util.Queue;
|
||||
|
||||
import icepick.State;
|
||||
import io.reactivex.Single;
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.disposables.Disposable;
|
||||
import io.reactivex.schedulers.Schedulers;
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.rxjava3.core.Single;
|
||||
import io.reactivex.rxjava3.disposables.Disposable;
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers;
|
||||
|
||||
public abstract class BaseListInfoFragment<I extends ListInfo>
|
||||
extends BaseListFragment<I, ListExtractor.InfoItemsPage> {
|
||||
@@ -204,7 +204,7 @@ public abstract class BaseListInfoFragment<I extends ListInfo>
|
||||
name = result.getName();
|
||||
setTitle(name);
|
||||
|
||||
if (infoListAdapter.getItemsList().size() == 0) {
|
||||
if (infoListAdapter.getItemsList().isEmpty()) {
|
||||
if (result.getRelatedItems().size() > 0) {
|
||||
infoListAdapter.addInfoItemList(result.getRelatedItems());
|
||||
showListFooter(hasMoreItems());
|
||||
|
||||
@@ -24,7 +24,7 @@ import androidx.appcompat.app.ActionBar;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.core.content.ContextCompat;
|
||||
|
||||
import com.jakewharton.rxbinding2.view.RxView;
|
||||
import com.jakewharton.rxbinding4.view.RxView;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.database.subscription.SubscriptionEntity;
|
||||
@@ -53,15 +53,15 @@ import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import io.reactivex.Observable;
|
||||
import io.reactivex.Single;
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.disposables.CompositeDisposable;
|
||||
import io.reactivex.disposables.Disposable;
|
||||
import io.reactivex.functions.Action;
|
||||
import io.reactivex.functions.Consumer;
|
||||
import io.reactivex.functions.Function;
|
||||
import io.reactivex.schedulers.Schedulers;
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||
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.functions.Action;
|
||||
import io.reactivex.rxjava3.functions.Consumer;
|
||||
import io.reactivex.rxjava3.functions.Function;
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers;
|
||||
|
||||
import static org.schabi.newpipe.util.AnimationUtils.animateBackgroundColor;
|
||||
import static org.schabi.newpipe.util.AnimationUtils.animateTextColor;
|
||||
|
||||
@@ -20,11 +20,11 @@ import org.schabi.newpipe.report.UserAction;
|
||||
import org.schabi.newpipe.util.AnimationUtils;
|
||||
import org.schabi.newpipe.util.ExtractorHelper;
|
||||
|
||||
import io.reactivex.Single;
|
||||
import io.reactivex.disposables.CompositeDisposable;
|
||||
import io.reactivex.rxjava3.core.Single;
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
||||
|
||||
public class CommentsFragment extends BaseListInfoFragment<CommentsInfo> {
|
||||
private CompositeDisposable disposables = new CompositeDisposable();
|
||||
private final CompositeDisposable disposables = new CompositeDisposable();
|
||||
|
||||
public static CommentsFragment getInstance(final int serviceId, final String url,
|
||||
final String name) {
|
||||
|
||||
@@ -26,7 +26,7 @@ import org.schabi.newpipe.util.KioskTranslator;
|
||||
import org.schabi.newpipe.util.Localization;
|
||||
|
||||
import icepick.State;
|
||||
import io.reactivex.Single;
|
||||
import io.reactivex.rxjava3.core.Single;
|
||||
|
||||
import static org.schabi.newpipe.util.AnimationUtils.animateView;
|
||||
|
||||
|
||||
@@ -11,12 +11,12 @@ import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.appcompat.content.res.AppCompatResources;
|
||||
|
||||
import org.reactivestreams.Subscriber;
|
||||
import org.reactivestreams.Subscription;
|
||||
@@ -26,8 +26,10 @@ import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity;
|
||||
import org.schabi.newpipe.extractor.InfoItem;
|
||||
import org.schabi.newpipe.extractor.ListExtractor;
|
||||
import org.schabi.newpipe.extractor.NewPipe;
|
||||
import org.schabi.newpipe.extractor.ServiceList;
|
||||
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||
import org.schabi.newpipe.extractor.playlist.PlaylistInfo;
|
||||
import org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||
import org.schabi.newpipe.extractor.stream.StreamType;
|
||||
import org.schabi.newpipe.fragments.list.BaseListInfoFragment;
|
||||
@@ -44,21 +46,21 @@ import org.schabi.newpipe.util.Localization;
|
||||
import org.schabi.newpipe.util.NavigationHelper;
|
||||
import org.schabi.newpipe.util.ShareUtils;
|
||||
import org.schabi.newpipe.util.StreamDialogEntry;
|
||||
import org.schabi.newpipe.util.ThemeHelper;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
import io.reactivex.Flowable;
|
||||
import io.reactivex.Single;
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.disposables.CompositeDisposable;
|
||||
import io.reactivex.disposables.Disposable;
|
||||
import io.reactivex.disposables.Disposables;
|
||||
import de.hdodenhof.circleimageview.CircleImageView;
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.rxjava3.core.Flowable;
|
||||
import io.reactivex.rxjava3.core.Single;
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
||||
import io.reactivex.rxjava3.disposables.Disposable;
|
||||
|
||||
import static org.schabi.newpipe.util.AnimationUtils.animateView;
|
||||
import static org.schabi.newpipe.util.ThemeHelper.resolveResourceIdFromAttr;
|
||||
|
||||
public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
|
||||
private CompositeDisposable disposables;
|
||||
@@ -75,7 +77,7 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
|
||||
private TextView headerTitleView;
|
||||
private View headerUploaderLayout;
|
||||
private TextView headerUploaderName;
|
||||
private ImageView headerUploaderAvatar;
|
||||
private CircleImageView headerUploaderAvatar;
|
||||
private TextView headerStreamCount;
|
||||
private View playlistCtrl;
|
||||
|
||||
@@ -302,8 +304,22 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
|
||||
|
||||
playlistCtrl.setVisibility(View.VISIBLE);
|
||||
|
||||
IMAGE_LOADER.displayImage(result.getUploaderAvatarUrl(), headerUploaderAvatar,
|
||||
ImageDisplayConstants.DISPLAY_AVATAR_OPTIONS);
|
||||
final String avatarUrl = result.getUploaderAvatarUrl();
|
||||
if (result.getServiceId() == ServiceList.YouTube.getServiceId()
|
||||
&& (YoutubeParsingHelper.isYoutubeMixId(result.getId())
|
||||
|| YoutubeParsingHelper.isYoutubeMusicMixId(result.getId()))) {
|
||||
// this is an auto-generated playlist (e.g. Youtube mix), so a radio is shown
|
||||
headerUploaderAvatar.setDisableCircularTransformation(true);
|
||||
headerUploaderAvatar.setBorderColor(
|
||||
getResources().getColor(R.color.transparent_background_color));
|
||||
headerUploaderAvatar.setImageDrawable(AppCompatResources.getDrawable(requireContext(),
|
||||
resolveResourceIdFromAttr(requireContext(), R.attr.ic_radio)));
|
||||
|
||||
} else {
|
||||
IMAGE_LOADER.displayImage(avatarUrl, headerUploaderAvatar,
|
||||
ImageDisplayConstants.DISPLAY_AVATAR_OPTIONS);
|
||||
}
|
||||
|
||||
headerStreamCount.setText(Localization
|
||||
.localizeStreamCount(getContext(), result.getStreamCount()));
|
||||
|
||||
@@ -460,7 +476,7 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
|
||||
.doFinally(() -> playlistEntity = null)
|
||||
.subscribe(ignored -> { /* Do nothing */ }, this::onError);
|
||||
} else {
|
||||
action = Disposables.empty();
|
||||
action = Disposable.empty();
|
||||
}
|
||||
|
||||
disposables.add(action);
|
||||
@@ -477,7 +493,7 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
|
||||
final int titleRes = playlistEntity == null
|
||||
? R.string.bookmark_playlist : R.string.unbookmark_playlist;
|
||||
|
||||
playlistBookmarkButton.setIcon(ThemeHelper.resolveResourceIdFromAttr(activity, iconAttr));
|
||||
playlistBookmarkButton.setIcon(resolveResourceIdFromAttr(activity, iconAttr));
|
||||
playlistBookmarkButton.setTitle(titleRes);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,6 +39,7 @@ import org.schabi.newpipe.ReCaptchaActivity;
|
||||
import org.schabi.newpipe.database.history.model.SearchHistoryEntry;
|
||||
import org.schabi.newpipe.extractor.InfoItem;
|
||||
import org.schabi.newpipe.extractor.ListExtractor;
|
||||
import org.schabi.newpipe.extractor.MetaInfo;
|
||||
import org.schabi.newpipe.extractor.NewPipe;
|
||||
import org.schabi.newpipe.extractor.Page;
|
||||
import org.schabi.newpipe.extractor.StreamingService;
|
||||
@@ -49,6 +50,7 @@ import org.schabi.newpipe.fragments.BackPressable;
|
||||
import org.schabi.newpipe.fragments.list.BaseListFragment;
|
||||
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
||||
import org.schabi.newpipe.report.ErrorActivity;
|
||||
import org.schabi.newpipe.report.ErrorInfo;
|
||||
import org.schabi.newpipe.report.UserAction;
|
||||
import org.schabi.newpipe.util.AnimationUtils;
|
||||
import org.schabi.newpipe.util.Constants;
|
||||
@@ -67,17 +69,18 @@ import java.util.Queue;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import icepick.State;
|
||||
import io.reactivex.Flowable;
|
||||
import io.reactivex.Observable;
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.disposables.CompositeDisposable;
|
||||
import io.reactivex.disposables.Disposable;
|
||||
import io.reactivex.schedulers.Schedulers;
|
||||
import io.reactivex.subjects.PublishSubject;
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.rxjava3.core.Flowable;
|
||||
import io.reactivex.rxjava3.core.Observable;
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
||||
import io.reactivex.rxjava3.disposables.Disposable;
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers;
|
||||
import io.reactivex.rxjava3.subjects.PublishSubject;
|
||||
|
||||
import static androidx.recyclerview.widget.ItemTouchHelper.Callback.makeMovementFlags;
|
||||
import static java.util.Arrays.asList;
|
||||
import static org.schabi.newpipe.util.AnimationUtils.animateView;
|
||||
import static org.schabi.newpipe.util.ExtractorHelper.showMetaInfoInTextView;
|
||||
|
||||
public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.InfoItemsPage<?>>
|
||||
implements BackPressable {
|
||||
@@ -128,6 +131,9 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
||||
@State
|
||||
boolean isCorrectedSearch;
|
||||
|
||||
@State
|
||||
MetaInfo[] metaInfo;
|
||||
|
||||
@State
|
||||
boolean wasSearchFocused = false;
|
||||
|
||||
@@ -152,6 +158,8 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
||||
private View searchClear;
|
||||
|
||||
private TextView correctSuggestion;
|
||||
private TextView metaInfoTextView;
|
||||
private View metaInfoSeparator;
|
||||
|
||||
private View suggestionsPanel;
|
||||
private boolean suggestionsPanelVisible = false;
|
||||
@@ -248,7 +256,7 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
||||
} catch (final Exception e) {
|
||||
ErrorActivity.reportError(getActivity(), e, requireActivity().getClass(),
|
||||
requireActivity().findViewById(android.R.id.content),
|
||||
ErrorActivity.ErrorInfo.make(UserAction.UI_ERROR,
|
||||
ErrorInfo.make(UserAction.UI_ERROR,
|
||||
"",
|
||||
"", R.string.general_error));
|
||||
}
|
||||
@@ -256,7 +264,7 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
||||
if (!TextUtils.isEmpty(searchString)) {
|
||||
if (wasLoading.getAndSet(false)) {
|
||||
search(searchString, contentFilter, sortFilter);
|
||||
} else if (infoListAdapter.getItemsList().size() == 0) {
|
||||
} else if (infoListAdapter.getItemsList().isEmpty()) {
|
||||
if (savedState == null) {
|
||||
search(searchString, contentFilter, sortFilter);
|
||||
} else if (!isLoading.get() && !wasSearchFocused) {
|
||||
@@ -268,6 +276,9 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
||||
|
||||
handleSearchSuggestion();
|
||||
|
||||
showMetaInfoInTextView(metaInfo == null ? null : Arrays.asList(metaInfo),
|
||||
metaInfoTextView, metaInfoSeparator);
|
||||
|
||||
if (suggestionDisposable == null || suggestionDisposable.isDisposed()) {
|
||||
initSuggestionObserver();
|
||||
}
|
||||
@@ -352,6 +363,8 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
||||
searchClear = searchToolbarContainer.findViewById(R.id.toolbar_search_clear);
|
||||
|
||||
correctSuggestion = rootView.findViewById(R.id.correct_suggestion);
|
||||
metaInfoTextView = rootView.findViewById(R.id.search_meta_info_text_view);
|
||||
metaInfoSeparator = rootView.findViewById(R.id.search_meta_info_separator);
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
@@ -708,7 +721,7 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
||||
|
||||
final Observable<String> observable = suggestionPublisher
|
||||
.debounce(SUGGESTIONS_DEBOUNCE, TimeUnit.MILLISECONDS)
|
||||
.startWith(searchString != null
|
||||
.startWithItem(searchString != null
|
||||
? searchString
|
||||
: "")
|
||||
.filter(ss -> isSuggestionsEnabled);
|
||||
@@ -972,12 +985,18 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
||||
searchSuggestion = result.getSearchSuggestion();
|
||||
isCorrectedSearch = result.isCorrectedSearch();
|
||||
|
||||
// List<MetaInfo> cannot be bundled without creating some containers
|
||||
metaInfo = new MetaInfo[result.getMetaInfo().size()];
|
||||
metaInfo = result.getMetaInfo().toArray(metaInfo);
|
||||
|
||||
handleSearchSuggestion();
|
||||
|
||||
showMetaInfoInTextView(result.getMetaInfo(), metaInfoTextView, metaInfoSeparator);
|
||||
|
||||
lastSearchedString = searchString;
|
||||
nextPage = result.getNextPage();
|
||||
|
||||
if (infoListAdapter.getItemsList().size() == 0) {
|
||||
if (infoListAdapter.getItemsList().isEmpty()) {
|
||||
if (!result.getRelatedItems().isEmpty()) {
|
||||
infoListAdapter.addInfoItemList(result.getRelatedItems());
|
||||
} else {
|
||||
|
||||
@@ -3,7 +3,6 @@ package org.schabi.newpipe.fragments.list.videos;
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.Bundle;
|
||||
import androidx.preference.PreferenceManager;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
@@ -13,6 +12,7 @@ import android.widget.Switch;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.preference.PreferenceManager;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.extractor.ListExtractor;
|
||||
@@ -25,13 +25,13 @@ import org.schabi.newpipe.util.RelatedStreamInfo;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
import io.reactivex.Single;
|
||||
import io.reactivex.disposables.CompositeDisposable;
|
||||
import io.reactivex.rxjava3.core.Single;
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
||||
|
||||
public class RelatedVideosFragment extends BaseListInfoFragment<RelatedStreamInfo>
|
||||
implements SharedPreferences.OnSharedPreferenceChangeListener {
|
||||
private static final String INFO_KEY = "related_info_key";
|
||||
private CompositeDisposable disposables = new CompositeDisposable();
|
||||
private final CompositeDisposable disposables = new CompositeDisposable();
|
||||
private RelatedStreamInfo relatedStreamInfo;
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@@ -371,7 +371,7 @@ public class InfoListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolde
|
||||
};
|
||||
}
|
||||
|
||||
public class HFHolder extends RecyclerView.ViewHolder {
|
||||
public static class HFHolder extends RecyclerView.ViewHolder {
|
||||
public View view;
|
||||
|
||||
HFHolder(final View v) {
|
||||
|
||||
@@ -144,7 +144,8 @@ public class CommentsMiniInfoItemHolder extends InfoItemHolder {
|
||||
}
|
||||
|
||||
if (item.getUploadDate() != null) {
|
||||
itemPublishedTime.setText(Localization.relativeTime(item.getUploadDate().date()));
|
||||
itemPublishedTime.setText(Localization.relativeTime(item.getUploadDate()
|
||||
.offsetDateTime()));
|
||||
} else {
|
||||
itemPublishedTime.setText(item.getTextualUploadDate());
|
||||
}
|
||||
|
||||
@@ -95,7 +95,7 @@ public class StreamInfoItemHolder extends StreamMiniInfoItemHolder {
|
||||
private String getFormattedRelativeUploadDate(final StreamInfoItem infoItem) {
|
||||
if (infoItem.getUploadDate() != null) {
|
||||
String formattedRelativeTime = Localization
|
||||
.relativeTime(infoItem.getUploadDate().date());
|
||||
.relativeTime(infoItem.getUploadDate().offsetDateTime());
|
||||
|
||||
if (DEBUG && PreferenceManager.getDefaultSharedPreferences(itemBuilder.getContext())
|
||||
.getBoolean(itemBuilder.getContext()
|
||||
|
||||
@@ -70,7 +70,8 @@ public class StreamMiniInfoItemHolder extends InfoItemHolder {
|
||||
} else {
|
||||
itemProgressView.setVisibility(View.GONE);
|
||||
}
|
||||
} else if (item.getStreamType() == StreamType.LIVE_STREAM) {
|
||||
} else if (item.getStreamType() == StreamType.LIVE_STREAM
|
||||
|| item.getStreamType() == StreamType.AUDIO_LIVE_STREAM) {
|
||||
itemDurationView.setText(R.string.duration_live);
|
||||
itemDurationView.setBackgroundColor(ContextCompat.getColor(itemBuilder.getContext(),
|
||||
R.color.live_duration_background_color));
|
||||
|
||||
@@ -1,10 +1,29 @@
|
||||
package org.schabi.newpipe.ktx
|
||||
|
||||
import java.time.OffsetDateTime
|
||||
import java.time.ZoneId
|
||||
import java.time.ZoneOffset
|
||||
import java.time.temporal.ChronoField
|
||||
import java.util.Calendar
|
||||
import java.util.Date
|
||||
import java.util.GregorianCalendar
|
||||
import java.util.TimeZone
|
||||
|
||||
fun OffsetDateTime.toCalendar(zoneId: ZoneId = ZoneId.systemDefault()): Calendar {
|
||||
return GregorianCalendar.from(if (zoneId != offset) atZoneSameInstant(zoneId) else toZonedDateTime())
|
||||
// This method is a modified version of GregorianCalendar.from(ZonedDateTime).
|
||||
// Math.addExact() and Math.multiplyExact() are desugared even though lint displays a warning.
|
||||
@SuppressWarnings("NewApi")
|
||||
fun OffsetDateTime.toCalendar(): Calendar {
|
||||
val cal = GregorianCalendar(TimeZone.getTimeZone("UTC"))
|
||||
val offsetDateTimeUTC = withOffsetSameInstant(ZoneOffset.UTC)
|
||||
cal.gregorianChange = Date(Long.MIN_VALUE)
|
||||
cal.firstDayOfWeek = Calendar.MONDAY
|
||||
cal.minimalDaysInFirstWeek = 4
|
||||
try {
|
||||
cal.timeInMillis = Math.addExact(
|
||||
Math.multiplyExact(offsetDateTimeUTC.toEpochSecond(), 1000),
|
||||
offsetDateTimeUTC[ChronoField.MILLI_OF_SECOND].toLong()
|
||||
)
|
||||
} catch (ex: ArithmeticException) {
|
||||
throw IllegalArgumentException(ex)
|
||||
}
|
||||
return cal
|
||||
}
|
||||
|
||||
@@ -33,11 +33,11 @@ import org.schabi.newpipe.util.OnClickGesture;
|
||||
import java.util.List;
|
||||
|
||||
import icepick.State;
|
||||
import io.reactivex.Flowable;
|
||||
import io.reactivex.Single;
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.disposables.CompositeDisposable;
|
||||
import io.reactivex.disposables.Disposable;
|
||||
import io.reactivex.rxjava3.core.Flowable;
|
||||
import io.reactivex.rxjava3.core.Single;
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
||||
import io.reactivex.rxjava3.disposables.Disposable;
|
||||
|
||||
public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistLocalItem>, Void> {
|
||||
@State
|
||||
|
||||
@@ -28,9 +28,9 @@ import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.disposables.CompositeDisposable;
|
||||
import io.reactivex.disposables.Disposable;
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
||||
import io.reactivex.rxjava3.disposables.Disposable;
|
||||
|
||||
public final class PlaylistAppendDialog extends PlaylistDialog {
|
||||
private static final String TAG = PlaylistAppendDialog.class.getCanonicalName();
|
||||
@@ -38,7 +38,7 @@ public final class PlaylistAppendDialog extends PlaylistDialog {
|
||||
private RecyclerView playlistRecyclerView;
|
||||
private LocalItemListAdapter playlistAdapter;
|
||||
|
||||
private CompositeDisposable playlistDisposables = new CompositeDisposable();
|
||||
private final CompositeDisposable playlistDisposables = new CompositeDisposable();
|
||||
|
||||
public static Disposable onPlaylistFound(
|
||||
final Context context, final Runnable onSuccess, final Runnable onFailed
|
||||
@@ -98,7 +98,7 @@ public final class PlaylistAppendDialog extends PlaylistDialog {
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
|
||||
final LocalPlaylistManager playlistManager =
|
||||
new LocalPlaylistManager(NewPipeDatabase.getInstance(getContext()));
|
||||
new LocalPlaylistManager(NewPipeDatabase.getInstance(requireContext()));
|
||||
|
||||
playlistAdapter = new LocalItemListAdapter(getActivity());
|
||||
playlistAdapter.setSelectedListener(new OnClickGesture<LocalItem>() {
|
||||
@@ -113,7 +113,7 @@ public final class PlaylistAppendDialog extends PlaylistDialog {
|
||||
});
|
||||
|
||||
playlistRecyclerView = view.findViewById(R.id.playlist_list);
|
||||
playlistRecyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
|
||||
playlistRecyclerView.setLayoutManager(new LinearLayoutManager(requireContext()));
|
||||
playlistRecyclerView.setAdapter(playlistAdapter);
|
||||
|
||||
final View newPlaylistButton = view.findViewById(R.id.newPlaylist);
|
||||
@@ -146,12 +146,12 @@ public final class PlaylistAppendDialog extends PlaylistDialog {
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
public void openCreatePlaylistDialog() {
|
||||
if (getStreams() == null || getFragmentManager() == null) {
|
||||
if (getStreams() == null || !isAdded()) {
|
||||
return;
|
||||
}
|
||||
|
||||
PlaylistCreationDialog.newInstance(getStreams()).show(getFragmentManager(), TAG);
|
||||
getDialog().dismiss();
|
||||
PlaylistCreationDialog.newInstance(getStreams()).show(getParentFragmentManager(), TAG);
|
||||
requireDialog().dismiss();
|
||||
}
|
||||
|
||||
private void onPlaylistsReceived(@NonNull final List<PlaylistMetadataEntry> playlists) {
|
||||
@@ -183,6 +183,6 @@ public final class PlaylistAppendDialog extends PlaylistDialog {
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(ignored -> successToast.show()));
|
||||
|
||||
getDialog().dismiss();
|
||||
requireDialog().dismiss();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ import org.schabi.newpipe.local.playlist.LocalPlaylistManager;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||
|
||||
public final class PlaylistCreationDialog extends PlaylistDialog {
|
||||
public static PlaylistCreationDialog newInstance(final List<StreamEntity> streams) {
|
||||
|
||||
@@ -17,7 +17,7 @@ import java.util.Queue;
|
||||
public abstract class PlaylistDialog extends DialogFragment implements StateSaver.WriteRead {
|
||||
private List<StreamEntity> streamEntities;
|
||||
|
||||
private StateSaver.SavedState savedState;
|
||||
private org.schabi.newpipe.util.SavedState savedState;
|
||||
|
||||
protected void setInfo(final List<StreamEntity> entities) {
|
||||
this.streamEntities = entities;
|
||||
|
||||
@@ -2,14 +2,11 @@ package org.schabi.newpipe.local.feed
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import io.reactivex.Completable
|
||||
import io.reactivex.Flowable
|
||||
import io.reactivex.Maybe
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.schedulers.Schedulers
|
||||
import java.time.LocalDate
|
||||
import java.time.OffsetDateTime
|
||||
import java.time.ZoneOffset
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.core.Completable
|
||||
import io.reactivex.rxjava3.core.Flowable
|
||||
import io.reactivex.rxjava3.core.Maybe
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import org.schabi.newpipe.MainActivity.DEBUG
|
||||
import org.schabi.newpipe.NewPipeDatabase
|
||||
import org.schabi.newpipe.database.feed.model.FeedEntity
|
||||
@@ -19,6 +16,9 @@ import org.schabi.newpipe.database.stream.model.StreamEntity
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem
|
||||
import org.schabi.newpipe.extractor.stream.StreamType
|
||||
import org.schabi.newpipe.local.subscription.FeedGroupIcon
|
||||
import java.time.LocalDate
|
||||
import java.time.OffsetDateTime
|
||||
import java.time.ZoneOffset
|
||||
|
||||
class FeedDatabaseManager(context: Context) {
|
||||
private val database = NewPipeDatabase.getInstance(context)
|
||||
@@ -31,7 +31,7 @@ class FeedDatabaseManager(context: Context) {
|
||||
* Only items that are newer than this will be saved.
|
||||
*/
|
||||
val FEED_OLDEST_ALLOWED_DATE: OffsetDateTime = LocalDate.now().minusWeeks(13)
|
||||
.atStartOfDay().atOffset(ZoneOffset.UTC)
|
||||
.atStartOfDay().atOffset(ZoneOffset.UTC)
|
||||
}
|
||||
|
||||
fun groups() = feedGroupTable.getAll()
|
||||
@@ -44,9 +44,9 @@ class FeedDatabaseManager(context: Context) {
|
||||
else -> feedTable.getAllStreamsFromGroup(groupId)
|
||||
}
|
||||
|
||||
return streams.map<List<StreamInfoItem>> {
|
||||
return streams.map {
|
||||
val items = ArrayList<StreamInfoItem>(it.size)
|
||||
it.mapTo(items) { it.toStreamInfoItem() }
|
||||
it.mapTo(items) { stream -> stream.toStreamInfoItem() }
|
||||
return@map items
|
||||
}
|
||||
}
|
||||
@@ -61,10 +61,10 @@ class FeedDatabaseManager(context: Context) {
|
||||
}
|
||||
|
||||
fun outdatedSubscriptionsForGroup(groupId: Long = FeedGroupEntity.GROUP_ALL_ID, outdatedThreshold: OffsetDateTime) =
|
||||
feedTable.getAllOutdatedForGroup(groupId, outdatedThreshold)
|
||||
feedTable.getAllOutdatedForGroup(groupId, outdatedThreshold)
|
||||
|
||||
fun markAsOutdated(subscriptionId: Long) = feedTable
|
||||
.setLastUpdatedForSubscription(FeedLastUpdatedEntity(subscriptionId, null))
|
||||
.setLastUpdatedForSubscription(FeedLastUpdatedEntity(subscriptionId, null))
|
||||
|
||||
fun upsertAll(
|
||||
subscriptionId: Long,
|
||||
@@ -92,8 +92,12 @@ class FeedDatabaseManager(context: Context) {
|
||||
feedTable.insertAll(feedEntities)
|
||||
}
|
||||
|
||||
feedTable.setLastUpdatedForSubscription(FeedLastUpdatedEntity(subscriptionId,
|
||||
OffsetDateTime.now(ZoneOffset.UTC)))
|
||||
feedTable.setLastUpdatedForSubscription(
|
||||
FeedLastUpdatedEntity(
|
||||
subscriptionId,
|
||||
OffsetDateTime.now(ZoneOffset.UTC)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
fun removeOrphansOrOlderStreams(oldestAllowedDate: OffsetDateTime = FEED_OLDEST_ALLOWED_DATE) {
|
||||
@@ -113,38 +117,38 @@ class FeedDatabaseManager(context: Context) {
|
||||
|
||||
fun subscriptionIdsForGroup(groupId: Long): Flowable<List<Long>> {
|
||||
return feedGroupTable.getSubscriptionIdsFor(groupId)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
}
|
||||
|
||||
fun updateSubscriptionsForGroup(groupId: Long, subscriptionIds: List<Long>): Completable {
|
||||
return Completable.fromCallable { feedGroupTable.updateSubscriptionsForGroup(groupId, subscriptionIds) }
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
}
|
||||
|
||||
fun createGroup(name: String, icon: FeedGroupIcon): Maybe<Long> {
|
||||
return Maybe.fromCallable { feedGroupTable.insert(FeedGroupEntity(0, name, icon)) }
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
}
|
||||
|
||||
fun getGroup(groupId: Long): Maybe<FeedGroupEntity> {
|
||||
return feedGroupTable.getGroup(groupId)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
}
|
||||
|
||||
fun updateGroup(feedGroupEntity: FeedGroupEntity): Completable {
|
||||
return Completable.fromCallable { feedGroupTable.update(feedGroupEntity) }
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
}
|
||||
|
||||
fun deleteGroup(groupId: Long): Completable {
|
||||
return Completable.fromCallable { feedGroupTable.delete(groupId) }
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
}
|
||||
|
||||
fun updateGroupsOrder(groupIdList: List<Long>): Completable {
|
||||
@@ -152,8 +156,8 @@ class FeedDatabaseManager(context: Context) {
|
||||
val orderMap = groupIdList.associateBy({ it }, { index++ })
|
||||
|
||||
return Completable.fromCallable { feedGroupTable.updateOrder(orderMap) }
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
}
|
||||
|
||||
fun oldestSubscriptionUpdate(groupId: Long): Flowable<List<OffsetDateTime>> {
|
||||
|
||||
@@ -29,13 +29,14 @@ import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.core.content.edit
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.preference.PreferenceManager
|
||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||
import icepick.State
|
||||
import java.util.Calendar
|
||||
import kotlinx.android.synthetic.main.error_retry.error_button_retry
|
||||
import kotlinx.android.synthetic.main.error_retry.error_message_view
|
||||
import kotlinx.android.synthetic.main.fragment_feed.empty_state_view
|
||||
@@ -53,9 +54,11 @@ import org.schabi.newpipe.local.feed.service.FeedLoadService
|
||||
import org.schabi.newpipe.report.UserAction
|
||||
import org.schabi.newpipe.util.AnimationUtils.animateView
|
||||
import org.schabi.newpipe.util.Localization
|
||||
import java.util.Calendar
|
||||
|
||||
class FeedFragment : BaseListFragment<FeedState, Unit>() {
|
||||
private lateinit var viewModel: FeedViewModel
|
||||
private lateinit var swipeRefreshLayout: SwipeRefreshLayout
|
||||
@State
|
||||
@JvmField
|
||||
var listState: Parcelable? = null
|
||||
@@ -73,7 +76,7 @@ class FeedFragment : BaseListFragment<FeedState, Unit>() {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
groupId = arguments?.getLong(KEY_GROUP_ID, FeedGroupEntity.GROUP_ALL_ID)
|
||||
?: FeedGroupEntity.GROUP_ALL_ID
|
||||
?: FeedGroupEntity.GROUP_ALL_ID
|
||||
groupName = arguments?.getString(KEY_GROUP_NAME) ?: ""
|
||||
}
|
||||
|
||||
@@ -83,7 +86,8 @@ class FeedFragment : BaseListFragment<FeedState, Unit>() {
|
||||
|
||||
override fun onViewCreated(rootView: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(rootView, savedInstanceState)
|
||||
|
||||
swipeRefreshLayout = requireView().findViewById(R.id.swiperefresh)
|
||||
swipeRefreshLayout.setOnRefreshListener { reloadContent() }
|
||||
viewModel = ViewModelProvider(this, FeedViewModel.Factory(requireContext(), groupId)).get(FeedViewModel::class.java)
|
||||
viewModel.stateLiveData.observe(viewLifecycleOwner, Observer { it?.let(::handleResult) })
|
||||
}
|
||||
@@ -140,15 +144,15 @@ class FeedFragment : BaseListFragment<FeedState, Unit>() {
|
||||
}
|
||||
|
||||
AlertDialog.Builder(requireContext())
|
||||
.setMessage(R.string.feed_use_dedicated_fetch_method_help_text)
|
||||
.setNeutralButton(enableDisableButtonText) { _, _ ->
|
||||
sharedPreferences.edit()
|
||||
.putBoolean(getString(R.string.feed_use_dedicated_fetch_method_key), !usingDedicatedMethod)
|
||||
.apply()
|
||||
.setMessage(R.string.feed_use_dedicated_fetch_method_help_text)
|
||||
.setNeutralButton(enableDisableButtonText) { _, _ ->
|
||||
sharedPreferences.edit {
|
||||
putBoolean(getString(R.string.feed_use_dedicated_fetch_method_key), !usingDedicatedMethod)
|
||||
}
|
||||
.setPositiveButton(resources.getString(R.string.finish), null)
|
||||
.create()
|
||||
.show()
|
||||
}
|
||||
.setPositiveButton(resources.getString(R.string.finish), null)
|
||||
.create()
|
||||
.show()
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -189,6 +193,7 @@ class FeedFragment : BaseListFragment<FeedState, Unit>() {
|
||||
|
||||
empty_state_view?.let { animateView(it, false, 0) }
|
||||
animateView(error_panel, false, 0)
|
||||
swipeRefreshLayout.isRefreshing = false
|
||||
}
|
||||
|
||||
override fun showEmptyState() {
|
||||
@@ -229,7 +234,7 @@ class FeedFragment : BaseListFragment<FeedState, Unit>() {
|
||||
showLoading()
|
||||
|
||||
val isIndeterminate = progressState.currentProgress == -1 &&
|
||||
progressState.maxProgress == -1
|
||||
progressState.maxProgress == -1
|
||||
|
||||
if (!isIndeterminate) {
|
||||
loading_progress_text.text = "${progressState.currentProgress}/${progressState.maxProgress}"
|
||||
@@ -240,7 +245,7 @@ class FeedFragment : BaseListFragment<FeedState, Unit>() {
|
||||
}
|
||||
|
||||
loading_progress_bar.isIndeterminate = isIndeterminate ||
|
||||
(progressState.maxProgress > 0 && progressState.currentProgress == 0)
|
||||
(progressState.maxProgress > 0 && progressState.currentProgress == 0)
|
||||
loading_progress_bar.progress = progressState.currentProgress
|
||||
|
||||
loading_progress_bar.max = progressState.maxProgress
|
||||
@@ -255,14 +260,17 @@ class FeedFragment : BaseListFragment<FeedState, Unit>() {
|
||||
|
||||
oldestSubscriptionUpdate = loadedState.oldestUpdate
|
||||
|
||||
refresh_subtitle_text.isVisible = loadedState.notLoadedCount > 0
|
||||
if (loadedState.notLoadedCount > 0) {
|
||||
val loadedCount = loadedState.notLoadedCount > 0
|
||||
refresh_subtitle_text.isVisible = loadedCount
|
||||
if (loadedCount) {
|
||||
refresh_subtitle_text.text = getString(R.string.feed_subscription_not_loaded_count, loadedState.notLoadedCount)
|
||||
}
|
||||
|
||||
if (loadedState.itemsErrors.isNotEmpty()) {
|
||||
showSnackBarError(loadedState.itemsErrors, UserAction.REQUESTED_FEED,
|
||||
"none", "Loading feed", R.string.general_error)
|
||||
showSnackBarError(
|
||||
loadedState.itemsErrors, UserAction.REQUESTED_FEED,
|
||||
"none", "Loading feed", R.string.general_error
|
||||
)
|
||||
}
|
||||
|
||||
if (loadedState.items.isEmpty()) {
|
||||
@@ -305,9 +313,11 @@ class FeedFragment : BaseListFragment<FeedState, Unit>() {
|
||||
override fun hasMoreItems() = false
|
||||
|
||||
private fun triggerUpdate() {
|
||||
getActivity()?.startService(Intent(requireContext(), FeedLoadService::class.java).apply {
|
||||
putExtra(FeedLoadService.EXTRA_GROUP_ID, groupId)
|
||||
})
|
||||
getActivity()?.startService(
|
||||
Intent(requireContext(), FeedLoadService::class.java).apply {
|
||||
putExtra(FeedLoadService.EXTRA_GROUP_ID, groupId)
|
||||
}
|
||||
)
|
||||
listState = null
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
package org.schabi.newpipe.local.feed
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
import java.util.Calendar
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem
|
||||
import java.util.Calendar
|
||||
|
||||
sealed class FeedState {
|
||||
data class ProgressState(
|
||||
|
||||
@@ -5,12 +5,10 @@ import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import io.reactivex.Flowable
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.functions.Function4
|
||||
import io.reactivex.schedulers.Schedulers
|
||||
import java.time.OffsetDateTime
|
||||
import java.util.concurrent.TimeUnit
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.core.Flowable
|
||||
import io.reactivex.rxjava3.functions.Function4
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import org.schabi.newpipe.database.feed.model.FeedGroupEntity
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem
|
||||
import org.schabi.newpipe.ktx.toCalendar
|
||||
@@ -20,6 +18,8 @@ import org.schabi.newpipe.local.feed.service.FeedEventManager.Event.IdleEvent
|
||||
import org.schabi.newpipe.local.feed.service.FeedEventManager.Event.ProgressEvent
|
||||
import org.schabi.newpipe.local.feed.service.FeedEventManager.Event.SuccessResultEvent
|
||||
import org.schabi.newpipe.util.DEFAULT_THROTTLE_TIMEOUT
|
||||
import java.time.OffsetDateTime
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class FeedViewModel(applicationContext: Context, val groupId: Long = FeedGroupEntity.GROUP_ALL_ID) : ViewModel() {
|
||||
class Factory(val context: Context, val groupId: Long = FeedGroupEntity.GROUP_ALL_ID) : ViewModelProvider.Factory {
|
||||
@@ -35,35 +35,34 @@ class FeedViewModel(applicationContext: Context, val groupId: Long = FeedGroupEn
|
||||
val stateLiveData: LiveData<FeedState> = mutableStateLiveData
|
||||
|
||||
private var combineDisposable = Flowable
|
||||
.combineLatest(
|
||||
FeedEventManager.events(),
|
||||
feedDatabaseManager.asStreamItems(groupId),
|
||||
feedDatabaseManager.notLoadedCount(groupId),
|
||||
feedDatabaseManager.oldestSubscriptionUpdate(groupId),
|
||||
.combineLatest(
|
||||
FeedEventManager.events(),
|
||||
feedDatabaseManager.asStreamItems(groupId),
|
||||
feedDatabaseManager.notLoadedCount(groupId),
|
||||
feedDatabaseManager.oldestSubscriptionUpdate(groupId),
|
||||
Function4 { t1: FeedEventManager.Event, t2: List<StreamInfoItem>, t3: Long, t4: List<OffsetDateTime> ->
|
||||
return@Function4 CombineResultHolder(t1, t2, t3, t4.firstOrNull())
|
||||
}
|
||||
)
|
||||
.throttleLatest(DEFAULT_THROTTLE_TIMEOUT, TimeUnit.MILLISECONDS)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe { (event, listFromDB, notLoadedCount, oldestUpdate) ->
|
||||
val oldestUpdateCalendar = oldestUpdate?.toCalendar()
|
||||
|
||||
Function4 { t1: FeedEventManager.Event, t2: List<StreamInfoItem>, t3: Long, t4: List<OffsetDateTime> ->
|
||||
return@Function4 CombineResultHolder(t1, t2, t3, t4.firstOrNull())
|
||||
}
|
||||
)
|
||||
.throttleLatest(DEFAULT_THROTTLE_TIMEOUT, TimeUnit.MILLISECONDS)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe {
|
||||
val (event, listFromDB, notLoadedCount, oldestUpdate) = it
|
||||
|
||||
val oldestUpdateCalendar = oldestUpdate?.toCalendar()
|
||||
|
||||
mutableStateLiveData.postValue(when (event) {
|
||||
mutableStateLiveData.postValue(
|
||||
when (event) {
|
||||
is IdleEvent -> FeedState.LoadedState(listFromDB, oldestUpdateCalendar, notLoadedCount)
|
||||
is ProgressEvent -> FeedState.ProgressState(event.currentProgress, event.maxProgress, event.progressMessage)
|
||||
is SuccessResultEvent -> FeedState.LoadedState(listFromDB, oldestUpdateCalendar, notLoadedCount, event.itemsErrors)
|
||||
is ErrorResultEvent -> FeedState.ErrorState(event.error)
|
||||
})
|
||||
|
||||
if (event is ErrorResultEvent || event is SuccessResultEvent) {
|
||||
FeedEventManager.reset()
|
||||
}
|
||||
)
|
||||
|
||||
if (event is ErrorResultEvent || event is SuccessResultEvent) {
|
||||
FeedEventManager.reset()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
super.onCleared()
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
package org.schabi.newpipe.local.feed.service
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
import io.reactivex.Flowable
|
||||
import io.reactivex.processors.BehaviorProcessor
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import io.reactivex.rxjava3.core.Flowable
|
||||
import io.reactivex.rxjava3.processors.BehaviorProcessor
|
||||
import org.schabi.newpipe.local.feed.service.FeedEventManager.Event.IdleEvent
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
|
||||
object FeedEventManager {
|
||||
private var processor: BehaviorProcessor<Event> = BehaviorProcessor.create()
|
||||
private var ignoreUpstream = AtomicBoolean()
|
||||
private var eventsFlowable = processor.startWith(IdleEvent)
|
||||
private var eventsFlowable = processor.startWithItem(IdleEvent)
|
||||
|
||||
fun postEvent(event: Event) {
|
||||
processor.onNext(event)
|
||||
|
||||
@@ -30,22 +30,17 @@ import android.os.IBinder
|
||||
import android.util.Log
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import androidx.core.app.ServiceCompat
|
||||
import androidx.preference.PreferenceManager
|
||||
import io.reactivex.Flowable
|
||||
import io.reactivex.Notification
|
||||
import io.reactivex.Single
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.disposables.CompositeDisposable
|
||||
import io.reactivex.functions.Consumer
|
||||
import io.reactivex.functions.Function
|
||||
import io.reactivex.processors.PublishProcessor
|
||||
import io.reactivex.schedulers.Schedulers
|
||||
import java.io.IOException
|
||||
import java.time.OffsetDateTime
|
||||
import java.time.ZoneOffset
|
||||
import java.util.concurrent.TimeUnit
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.core.Flowable
|
||||
import io.reactivex.rxjava3.core.Notification
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||
import io.reactivex.rxjava3.functions.Consumer
|
||||
import io.reactivex.rxjava3.functions.Function
|
||||
import io.reactivex.rxjava3.processors.PublishProcessor
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import org.reactivestreams.Subscriber
|
||||
import org.reactivestreams.Subscription
|
||||
import org.schabi.newpipe.MainActivity.DEBUG
|
||||
@@ -56,13 +51,18 @@ import org.schabi.newpipe.extractor.exceptions.ReCaptchaException
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem
|
||||
import org.schabi.newpipe.local.feed.FeedDatabaseManager
|
||||
import org.schabi.newpipe.local.feed.service.FeedEventManager.Event.ErrorResultEvent
|
||||
import org.schabi.newpipe.local.feed.service.FeedEventManager.Event.IdleEvent
|
||||
import org.schabi.newpipe.local.feed.service.FeedEventManager.Event.ProgressEvent
|
||||
import org.schabi.newpipe.local.feed.service.FeedEventManager.Event.SuccessResultEvent
|
||||
import org.schabi.newpipe.local.feed.service.FeedEventManager.postEvent
|
||||
import org.schabi.newpipe.local.subscription.SubscriptionManager
|
||||
import org.schabi.newpipe.util.ExceptionUtils
|
||||
import org.schabi.newpipe.util.ExtractorHelper
|
||||
import java.io.IOException
|
||||
import java.time.OffsetDateTime
|
||||
import java.time.ZoneOffset
|
||||
import java.util.concurrent.TimeUnit
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
|
||||
class FeedLoadService : Service() {
|
||||
companion object {
|
||||
@@ -109,8 +109,11 @@ class FeedLoadService : Service() {
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "onStartCommand() called with: intent = [" + intent + "]," +
|
||||
" flags = [" + flags + "], startId = [" + startId + "]")
|
||||
Log.d(
|
||||
TAG,
|
||||
"onStartCommand() called with: intent = [" + intent + "]," +
|
||||
" flags = [" + flags + "], startId = [" + startId + "]"
|
||||
)
|
||||
}
|
||||
|
||||
if (intent == null || loadingSubscription != null) {
|
||||
@@ -123,10 +126,10 @@ class FeedLoadService : Service() {
|
||||
|
||||
val groupId = intent.getLongExtra(EXTRA_GROUP_ID, FeedGroupEntity.GROUP_ALL_ID)
|
||||
val useFeedExtractor = defaultSharedPreferences
|
||||
.getBoolean(getString(R.string.feed_use_dedicated_fetch_method_key), false)
|
||||
.getBoolean(getString(R.string.feed_use_dedicated_fetch_method_key), false)
|
||||
|
||||
val thresholdOutdatedSecondsString = defaultSharedPreferences
|
||||
.getString(getString(R.string.feed_update_threshold_key), getString(R.string.feed_update_threshold_default_value))
|
||||
.getString(getString(R.string.feed_update_threshold_key), getString(R.string.feed_update_threshold_default_value))
|
||||
val thresholdOutdatedSeconds = thresholdOutdatedSecondsString!!.toInt()
|
||||
|
||||
startLoading(groupId, useFeedExtractor, thresholdOutdatedSeconds)
|
||||
@@ -145,7 +148,7 @@ class FeedLoadService : Service() {
|
||||
|
||||
private fun stopService() {
|
||||
disposeAll()
|
||||
stopForeground(true)
|
||||
ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE)
|
||||
notificationManager.cancel(NOTIFICATION_ID)
|
||||
stopSelf()
|
||||
}
|
||||
@@ -181,63 +184,63 @@ class FeedLoadService : Service() {
|
||||
}
|
||||
|
||||
subscriptions
|
||||
.limit(1)
|
||||
.take(1)
|
||||
|
||||
.doOnNext {
|
||||
currentProgress.set(0)
|
||||
maxProgress.set(it.size)
|
||||
.doOnNext {
|
||||
currentProgress.set(0)
|
||||
maxProgress.set(it.size)
|
||||
}
|
||||
.filter { it.isNotEmpty() }
|
||||
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.doOnNext {
|
||||
startForeground(NOTIFICATION_ID, notificationBuilder.build())
|
||||
updateNotificationProgress(null)
|
||||
broadcastProgress()
|
||||
}
|
||||
|
||||
.observeOn(Schedulers.io())
|
||||
.flatMap { Flowable.fromIterable(it) }
|
||||
.takeWhile { !cancelSignal.get() }
|
||||
|
||||
.parallel(PARALLEL_EXTRACTIONS, PARALLEL_EXTRACTIONS * 2)
|
||||
.runOn(Schedulers.io(), PARALLEL_EXTRACTIONS * 2)
|
||||
.filter { !cancelSignal.get() }
|
||||
|
||||
.map { subscriptionEntity ->
|
||||
try {
|
||||
val listInfo = if (useFeedExtractor) {
|
||||
ExtractorHelper
|
||||
.getFeedInfoFallbackToChannelInfo(subscriptionEntity.serviceId, subscriptionEntity.url)
|
||||
.blockingGet()
|
||||
} else {
|
||||
ExtractorHelper
|
||||
.getChannelInfo(subscriptionEntity.serviceId, subscriptionEntity.url, true)
|
||||
.blockingGet()
|
||||
} as ListInfo<StreamInfoItem>
|
||||
|
||||
return@map Notification.createOnNext(Pair(subscriptionEntity.uid, listInfo))
|
||||
} catch (e: Throwable) {
|
||||
val request = "${subscriptionEntity.serviceId}:${subscriptionEntity.url}"
|
||||
val wrapper = RequestException(subscriptionEntity.uid, request, e)
|
||||
return@map Notification.createOnError<Pair<Long, ListInfo<StreamInfoItem>>>(wrapper)
|
||||
}
|
||||
.filter { it.isNotEmpty() }
|
||||
}
|
||||
.sequential()
|
||||
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.doOnNext {
|
||||
startForeground(NOTIFICATION_ID, notificationBuilder.build())
|
||||
updateNotificationProgress(null)
|
||||
broadcastProgress()
|
||||
}
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.doOnNext(errorHandlingConsumer)
|
||||
|
||||
.observeOn(Schedulers.io())
|
||||
.flatMap { Flowable.fromIterable(it) }
|
||||
.takeWhile { !cancelSignal.get() }
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.doOnNext(notificationsConsumer)
|
||||
|
||||
.parallel(PARALLEL_EXTRACTIONS, PARALLEL_EXTRACTIONS * 2)
|
||||
.runOn(Schedulers.io(), PARALLEL_EXTRACTIONS * 2)
|
||||
.filter { !cancelSignal.get() }
|
||||
.observeOn(Schedulers.io())
|
||||
.buffer(BUFFER_COUNT_BEFORE_INSERT)
|
||||
.doOnNext(databaseConsumer)
|
||||
|
||||
.map { subscriptionEntity ->
|
||||
try {
|
||||
val listInfo = if (useFeedExtractor) {
|
||||
ExtractorHelper
|
||||
.getFeedInfoFallbackToChannelInfo(subscriptionEntity.serviceId, subscriptionEntity.url)
|
||||
.blockingGet()
|
||||
} else {
|
||||
ExtractorHelper
|
||||
.getChannelInfo(subscriptionEntity.serviceId, subscriptionEntity.url, true)
|
||||
.blockingGet()
|
||||
} as ListInfo<StreamInfoItem>
|
||||
|
||||
return@map Notification.createOnNext(Pair(subscriptionEntity.uid, listInfo))
|
||||
} catch (e: Throwable) {
|
||||
val request = "${subscriptionEntity.serviceId}:${subscriptionEntity.url}"
|
||||
val wrapper = RequestException(subscriptionEntity.uid, request, e)
|
||||
return@map Notification.createOnError<Pair<Long, ListInfo<StreamInfoItem>>>(wrapper)
|
||||
}
|
||||
}
|
||||
.sequential()
|
||||
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.doOnNext(errorHandlingConsumer)
|
||||
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.doOnNext(notificationsConsumer)
|
||||
|
||||
.observeOn(Schedulers.io())
|
||||
.buffer(BUFFER_COUNT_BEFORE_INSERT)
|
||||
.doOnNext(databaseConsumer)
|
||||
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(resultSubscriber)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(resultSubscriber)
|
||||
}
|
||||
|
||||
private fun broadcastProgress() {
|
||||
@@ -262,7 +265,7 @@ class FeedLoadService : Service() {
|
||||
|
||||
override fun onComplete() {
|
||||
if (maxProgress.get() == 0) {
|
||||
postEvent(IdleEvent)
|
||||
postEvent(FeedEventManager.Event.IdleEvent)
|
||||
stopService()
|
||||
|
||||
return
|
||||
@@ -274,7 +277,8 @@ class FeedLoadService : Service() {
|
||||
notificationUpdater.onNext(getString(R.string.feed_processing_message))
|
||||
postEvent(ProgressEvent(R.string.feed_processing_message))
|
||||
|
||||
disposables.add(Single
|
||||
disposables.add(
|
||||
Single
|
||||
.fromCallable {
|
||||
feedResultsHolder.ready()
|
||||
|
||||
@@ -293,7 +297,8 @@ class FeedLoadService : Service() {
|
||||
return@subscribe
|
||||
}
|
||||
stopService()
|
||||
})
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -364,16 +369,18 @@ class FeedLoadService : Service() {
|
||||
private var maxProgress = AtomicInteger(-1)
|
||||
|
||||
private fun createNotification(): NotificationCompat.Builder {
|
||||
val cancelActionIntent = PendingIntent.getBroadcast(this,
|
||||
NOTIFICATION_ID, Intent(ACTION_CANCEL), 0)
|
||||
val cancelActionIntent = PendingIntent.getBroadcast(
|
||||
this,
|
||||
NOTIFICATION_ID, Intent(ACTION_CANCEL), 0
|
||||
)
|
||||
|
||||
return NotificationCompat.Builder(this, getString(R.string.notification_channel_id))
|
||||
.setOngoing(true)
|
||||
.setProgress(-1, -1, true)
|
||||
.setSmallIcon(R.drawable.ic_newpipe_triangle_white)
|
||||
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
|
||||
.addAction(0, getString(R.string.cancel), cancelActionIntent)
|
||||
.setContentTitle(getString(R.string.feed_notification_loading))
|
||||
.setOngoing(true)
|
||||
.setProgress(-1, -1, true)
|
||||
.setSmallIcon(R.drawable.ic_newpipe_triangle_white)
|
||||
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
|
||||
.addAction(0, getString(R.string.cancel), cancelActionIntent)
|
||||
.setContentTitle(getString(R.string.feed_notification_loading))
|
||||
}
|
||||
|
||||
private fun setupNotification() {
|
||||
@@ -381,13 +388,15 @@ class FeedLoadService : Service() {
|
||||
notificationBuilder = createNotification()
|
||||
|
||||
val throttleAfterFirstEmission = Function { flow: Flowable<String> ->
|
||||
flow.limit(1).concatWith(flow.skip(1).throttleLatest(NOTIFICATION_SAMPLING_PERIOD.toLong(), TimeUnit.MILLISECONDS))
|
||||
flow.take(1).concatWith(flow.skip(1).throttleLatest(NOTIFICATION_SAMPLING_PERIOD.toLong(), TimeUnit.MILLISECONDS))
|
||||
}
|
||||
|
||||
disposables.add(notificationUpdater
|
||||
disposables.add(
|
||||
notificationUpdater
|
||||
.publish(throttleAfterFirstEmission)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(this::updateNotificationProgress))
|
||||
.subscribe(this::updateNotificationProgress)
|
||||
)
|
||||
}
|
||||
|
||||
private fun updateNotificationProgress(updateDescription: String?) {
|
||||
|
||||
@@ -50,11 +50,11 @@ import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
import io.reactivex.Completable;
|
||||
import io.reactivex.Flowable;
|
||||
import io.reactivex.Maybe;
|
||||
import io.reactivex.Single;
|
||||
import io.reactivex.schedulers.Schedulers;
|
||||
import io.reactivex.rxjava3.core.Completable;
|
||||
import io.reactivex.rxjava3.core.Flowable;
|
||||
import io.reactivex.rxjava3.core.Maybe;
|
||||
import io.reactivex.rxjava3.core.Single;
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers;
|
||||
|
||||
public class HistoryRecordManager {
|
||||
private final AppDatabase database;
|
||||
|
||||
@@ -34,6 +34,7 @@ import org.schabi.newpipe.player.helper.PlayerHolder;
|
||||
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
||||
import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
|
||||
import org.schabi.newpipe.report.ErrorActivity;
|
||||
import org.schabi.newpipe.report.ErrorInfo;
|
||||
import org.schabi.newpipe.report.UserAction;
|
||||
import org.schabi.newpipe.settings.SettingsActivity;
|
||||
import org.schabi.newpipe.util.NavigationHelper;
|
||||
@@ -48,9 +49,9 @@ import java.util.Comparator;
|
||||
import java.util.List;
|
||||
|
||||
import icepick.State;
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.disposables.CompositeDisposable;
|
||||
import io.reactivex.disposables.Disposable;
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
||||
import io.reactivex.rxjava3.disposables.Disposable;
|
||||
|
||||
public class StatisticsPlaylistFragment
|
||||
extends BaseLocalListFragment<List<StreamStatisticsEntry>, Void> {
|
||||
@@ -183,7 +184,7 @@ public class StatisticsPlaylistFragment
|
||||
throwable -> ErrorActivity.reportError(getContext(),
|
||||
throwable,
|
||||
SettingsActivity.class, null,
|
||||
ErrorActivity.ErrorInfo.make(
|
||||
ErrorInfo.make(
|
||||
UserAction.DELETE_FROM_HISTORY,
|
||||
"none",
|
||||
"Delete view history",
|
||||
@@ -197,7 +198,7 @@ public class StatisticsPlaylistFragment
|
||||
throwable -> ErrorActivity.reportError(getContext(),
|
||||
throwable,
|
||||
SettingsActivity.class, null,
|
||||
ErrorActivity.ErrorInfo.make(
|
||||
ErrorInfo.make(
|
||||
UserAction.DELETE_FROM_HISTORY,
|
||||
"none",
|
||||
"Delete search history",
|
||||
|
||||
@@ -11,7 +11,6 @@ import androidx.core.content.ContextCompat;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.database.LocalItem;
|
||||
import org.schabi.newpipe.database.playlist.PlaylistStreamEntry;
|
||||
import org.schabi.newpipe.database.stream.model.StreamStateEntity;
|
||||
import org.schabi.newpipe.extractor.NewPipe;
|
||||
import org.schabi.newpipe.local.LocalItemBuilder;
|
||||
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
||||
@@ -21,7 +20,6 @@ import org.schabi.newpipe.util.Localization;
|
||||
import org.schabi.newpipe.views.AnimatedProgressBar;
|
||||
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public class LocalPlaylistStreamItemHolder extends LocalItemHolder {
|
||||
@@ -70,15 +68,11 @@ public class LocalPlaylistStreamItemHolder extends LocalItemHolder {
|
||||
R.color.duration_background_color));
|
||||
itemDurationView.setVisibility(View.VISIBLE);
|
||||
|
||||
final StreamStateEntity state = historyRecordManager
|
||||
.loadLocalStreamStateBatch(new ArrayList<LocalItem>() {{
|
||||
add(localItem);
|
||||
}}).blockingGet().get(0);
|
||||
if (state != null) {
|
||||
if (item.getProgressTime() > 0) {
|
||||
itemProgressView.setVisibility(View.VISIBLE);
|
||||
itemProgressView.setMax((int) item.getStreamEntity().getDuration());
|
||||
itemProgressView.setProgress((int) TimeUnit.MILLISECONDS
|
||||
.toSeconds(state.getProgressTime()));
|
||||
.toSeconds(item.getProgressTime()));
|
||||
} else {
|
||||
itemProgressView.setVisibility(View.GONE);
|
||||
}
|
||||
@@ -115,18 +109,14 @@ public class LocalPlaylistStreamItemHolder extends LocalItemHolder {
|
||||
}
|
||||
final PlaylistStreamEntry item = (PlaylistStreamEntry) localItem;
|
||||
|
||||
final StreamStateEntity state = historyRecordManager
|
||||
.loadLocalStreamStateBatch(new ArrayList<LocalItem>() {{
|
||||
add(localItem);
|
||||
}}).blockingGet().get(0);
|
||||
if (state != null && item.getStreamEntity().getDuration() > 0) {
|
||||
if (item.getProgressTime() > 0 && item.getStreamEntity().getDuration() > 0) {
|
||||
itemProgressView.setMax((int) item.getStreamEntity().getDuration());
|
||||
if (itemProgressView.getVisibility() == View.VISIBLE) {
|
||||
itemProgressView.setProgressAnimated((int) TimeUnit.MILLISECONDS
|
||||
.toSeconds(state.getProgressTime()));
|
||||
.toSeconds(item.getProgressTime()));
|
||||
} else {
|
||||
itemProgressView.setProgress((int) TimeUnit.MILLISECONDS
|
||||
.toSeconds(state.getProgressTime()));
|
||||
.toSeconds(item.getProgressTime()));
|
||||
AnimationUtils.animateView(itemProgressView, true, 500);
|
||||
}
|
||||
} else if (itemProgressView.getVisibility() == View.VISIBLE) {
|
||||
|
||||
@@ -11,7 +11,6 @@ import androidx.core.content.ContextCompat;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.database.LocalItem;
|
||||
import org.schabi.newpipe.database.stream.StreamStatisticsEntry;
|
||||
import org.schabi.newpipe.database.stream.model.StreamStateEntity;
|
||||
import org.schabi.newpipe.extractor.NewPipe;
|
||||
import org.schabi.newpipe.local.LocalItemBuilder;
|
||||
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
||||
@@ -21,7 +20,6 @@ import org.schabi.newpipe.util.Localization;
|
||||
import org.schabi.newpipe.views.AnimatedProgressBar;
|
||||
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/*
|
||||
@@ -98,15 +96,11 @@ public class LocalStatisticStreamItemHolder extends LocalItemHolder {
|
||||
R.color.duration_background_color));
|
||||
itemDurationView.setVisibility(View.VISIBLE);
|
||||
|
||||
final StreamStateEntity state = historyRecordManager
|
||||
.loadLocalStreamStateBatch(new ArrayList<LocalItem>() {{
|
||||
add(localItem);
|
||||
}}).blockingGet().get(0);
|
||||
if (state != null) {
|
||||
if (item.getProgressTime() > 0) {
|
||||
itemProgressView.setVisibility(View.VISIBLE);
|
||||
itemProgressView.setMax((int) item.getStreamEntity().getDuration());
|
||||
itemProgressView.setProgress((int) TimeUnit.MILLISECONDS
|
||||
.toSeconds(state.getProgressTime()));
|
||||
.toSeconds(item.getProgressTime()));
|
||||
} else {
|
||||
itemProgressView.setVisibility(View.GONE);
|
||||
}
|
||||
@@ -146,18 +140,14 @@ public class LocalStatisticStreamItemHolder extends LocalItemHolder {
|
||||
}
|
||||
final StreamStatisticsEntry item = (StreamStatisticsEntry) localItem;
|
||||
|
||||
final StreamStateEntity state = historyRecordManager
|
||||
.loadLocalStreamStateBatch(new ArrayList<LocalItem>() {{
|
||||
add(localItem);
|
||||
}}).blockingGet().get(0);
|
||||
if (state != null && item.getStreamEntity().getDuration() > 0) {
|
||||
if (item.getProgressTime() > 0 && item.getStreamEntity().getDuration() > 0) {
|
||||
itemProgressView.setMax((int) item.getStreamEntity().getDuration());
|
||||
if (itemProgressView.getVisibility() == View.VISIBLE) {
|
||||
itemProgressView.setProgressAnimated((int) TimeUnit.MILLISECONDS
|
||||
.toSeconds(state.getProgressTime()));
|
||||
.toSeconds(item.getProgressTime()));
|
||||
} else {
|
||||
itemProgressView.setProgress((int) TimeUnit.MILLISECONDS
|
||||
.toSeconds(state.getProgressTime()));
|
||||
.toSeconds(item.getProgressTime()));
|
||||
AnimationUtils.animateView(itemProgressView, true, 500);
|
||||
}
|
||||
} else if (itemProgressView.getVisibility() == View.VISIBLE) {
|
||||
|
||||
@@ -55,13 +55,12 @@ import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
import icepick.State;
|
||||
import io.reactivex.Flowable;
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.disposables.CompositeDisposable;
|
||||
import io.reactivex.disposables.Disposable;
|
||||
import io.reactivex.disposables.Disposables;
|
||||
import io.reactivex.schedulers.Schedulers;
|
||||
import io.reactivex.subjects.PublishSubject;
|
||||
import io.reactivex.rxjava3.core.Flowable;
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
||||
import io.reactivex.rxjava3.disposables.Disposable;
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers;
|
||||
import io.reactivex.rxjava3.subjects.PublishSubject;
|
||||
|
||||
import static org.schabi.newpipe.util.AnimationUtils.animateView;
|
||||
|
||||
@@ -641,7 +640,7 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
|
||||
|
||||
private Disposable getDebouncedSaver() {
|
||||
if (debouncedSaveSignal == null) {
|
||||
return Disposables.empty();
|
||||
return Disposable.empty();
|
||||
}
|
||||
|
||||
return debouncedSaveSignal
|
||||
|
||||
@@ -15,11 +15,11 @@ import org.schabi.newpipe.database.stream.model.StreamEntity;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import io.reactivex.Completable;
|
||||
import io.reactivex.Flowable;
|
||||
import io.reactivex.Maybe;
|
||||
import io.reactivex.Single;
|
||||
import io.reactivex.schedulers.Schedulers;
|
||||
import io.reactivex.rxjava3.core.Completable;
|
||||
import io.reactivex.rxjava3.core.Flowable;
|
||||
import io.reactivex.rxjava3.core.Maybe;
|
||||
import io.reactivex.rxjava3.core.Single;
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers;
|
||||
|
||||
public class LocalPlaylistManager {
|
||||
private final AppDatabase database;
|
||||
|
||||
@@ -7,9 +7,9 @@ import org.schabi.newpipe.extractor.playlist.PlaylistInfo;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import io.reactivex.Flowable;
|
||||
import io.reactivex.Single;
|
||||
import io.reactivex.schedulers.Schedulers;
|
||||
import io.reactivex.rxjava3.core.Flowable;
|
||||
import io.reactivex.rxjava3.core.Single;
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers;
|
||||
|
||||
public class RemotePlaylistManager {
|
||||
|
||||
|
||||
@@ -28,13 +28,7 @@ import com.xwray.groupie.Item
|
||||
import com.xwray.groupie.Section
|
||||
import com.xwray.groupie.kotlinandroidextensions.GroupieViewHolder
|
||||
import icepick.State
|
||||
import io.reactivex.disposables.CompositeDisposable
|
||||
import java.io.File
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
import kotlin.math.floor
|
||||
import kotlin.math.max
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||
import kotlinx.android.synthetic.main.dialog_title.view.itemAdditionalDetails
|
||||
import kotlinx.android.synthetic.main.dialog_title.view.itemTitleView
|
||||
import kotlinx.android.synthetic.main.fragment_subscription.items_list
|
||||
@@ -68,6 +62,12 @@ import org.schabi.newpipe.util.NavigationHelper
|
||||
import org.schabi.newpipe.util.OnClickGesture
|
||||
import org.schabi.newpipe.util.ShareUtils
|
||||
import org.schabi.newpipe.util.ThemeHelper
|
||||
import java.io.File
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
import kotlin.math.floor
|
||||
import kotlin.math.max
|
||||
|
||||
class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
|
||||
private lateinit var viewModel: SubscriptionViewModel
|
||||
@@ -208,14 +208,19 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
|
||||
if (!exportFile.parentFile.canWrite() || !exportFile.parentFile.canRead()) {
|
||||
Toast.makeText(activity, R.string.invalid_directory, Toast.LENGTH_SHORT).show()
|
||||
} else {
|
||||
activity.startService(Intent(activity, SubscriptionsExportService::class.java)
|
||||
.putExtra(KEY_FILE_PATH, exportFile.absolutePath))
|
||||
activity.startService(
|
||||
Intent(activity, SubscriptionsExportService::class.java)
|
||||
.putExtra(KEY_FILE_PATH, exportFile.absolutePath)
|
||||
)
|
||||
}
|
||||
} else if (requestCode == REQUEST_IMPORT_CODE) {
|
||||
val path = Utils.getFileForUri(data.data!!).absolutePath
|
||||
ImportConfirmationDialog.show(this, Intent(activity, SubscriptionsImportService::class.java)
|
||||
ImportConfirmationDialog.show(
|
||||
this,
|
||||
Intent(activity, SubscriptionsImportService::class.java)
|
||||
.putExtra(KEY_MODE, PREVIOUS_EXPORT_MODE)
|
||||
.putExtra(KEY_VALUE, path))
|
||||
.putExtra(KEY_VALUE, path)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -247,9 +252,9 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
|
||||
|
||||
feedGroupsCarousel = FeedGroupCarouselItem(requireContext(), carouselAdapter)
|
||||
feedGroupsSortMenuItem = HeaderWithMenuItem(
|
||||
getString(R.string.feed_groups_header_title),
|
||||
ThemeHelper.resolveResourceIdFromAttr(requireContext(), R.attr.ic_sort),
|
||||
menuItemOnClickListener = ::openReorderDialog
|
||||
getString(R.string.feed_groups_header_title),
|
||||
ThemeHelper.resolveResourceIdFromAttr(requireContext(), R.attr.ic_sort),
|
||||
menuItemOnClickListener = ::openReorderDialog
|
||||
)
|
||||
add(Section(feedGroupsSortMenuItem, listOf(feedGroupsCarousel)))
|
||||
|
||||
@@ -260,10 +265,11 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
|
||||
subscriptionsSection.setHideWhenEmpty(true)
|
||||
|
||||
importExportItem = FeedImportExportItem(
|
||||
{ onImportPreviousSelected() },
|
||||
{ onImportFromServiceSelected(it) },
|
||||
{ onExportSelected() },
|
||||
importExportItemExpandedState ?: false)
|
||||
{ onImportPreviousSelected() },
|
||||
{ onImportFromServiceSelected(it) },
|
||||
{ onExportSelected() },
|
||||
importExportItemExpandedState ?: false
|
||||
)
|
||||
groupAdapter.add(Section(importExportItem, listOf(subscriptionsSection)))
|
||||
}
|
||||
|
||||
@@ -284,8 +290,8 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
|
||||
|
||||
private fun showLongTapDialog(selectedItem: ChannelInfoItem) {
|
||||
val commands = arrayOf(
|
||||
getString(R.string.share),
|
||||
getString(R.string.unsubscribe)
|
||||
getString(R.string.share),
|
||||
getString(R.string.unsubscribe)
|
||||
)
|
||||
|
||||
val actions = DialogInterface.OnClickListener { _, i ->
|
||||
@@ -301,16 +307,18 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
|
||||
bannerView.itemAdditionalDetails.visibility = View.GONE
|
||||
|
||||
AlertDialog.Builder(requireContext())
|
||||
.setCustomTitle(bannerView)
|
||||
.setItems(commands, actions)
|
||||
.create()
|
||||
.show()
|
||||
.setCustomTitle(bannerView)
|
||||
.setItems(commands, actions)
|
||||
.create()
|
||||
.show()
|
||||
}
|
||||
|
||||
private fun deleteChannel(selectedItem: ChannelInfoItem) {
|
||||
disposables.add(subscriptionManager.deleteSubscription(selectedItem.serviceId, selectedItem.url).subscribe {
|
||||
Toast.makeText(requireContext(), getString(R.string.channel_unsubscribed), Toast.LENGTH_SHORT).show()
|
||||
})
|
||||
disposables.add(
|
||||
subscriptionManager.deleteSubscription(selectedItem.serviceId, selectedItem.url).subscribe {
|
||||
Toast.makeText(requireContext(), getString(R.string.channel_unsubscribed), Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
override fun doInitialLoadLogic() = Unit
|
||||
@@ -332,8 +340,10 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
|
||||
}
|
||||
|
||||
private val listenerChannelItem = object : OnClickGesture<ChannelInfoItem>() {
|
||||
override fun selected(selectedItem: ChannelInfoItem) = NavigationHelper.openChannelFragment(fm,
|
||||
selectedItem.serviceId, selectedItem.url, selectedItem.name)
|
||||
override fun selected(selectedItem: ChannelInfoItem) = NavigationHelper.openChannelFragment(
|
||||
fm,
|
||||
selectedItem.serviceId, selectedItem.url, selectedItem.name
|
||||
)
|
||||
|
||||
override fun held(selectedItem: ChannelInfoItem) = showLongTapDialog(selectedItem)
|
||||
}
|
||||
@@ -420,14 +430,16 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
|
||||
|
||||
private fun shouldUseGridLayout(): Boolean {
|
||||
val listMode = PreferenceManager.getDefaultSharedPreferences(requireContext())
|
||||
.getString(getString(R.string.list_view_mode_key), getString(R.string.list_view_mode_value))
|
||||
.getString(getString(R.string.list_view_mode_key), getString(R.string.list_view_mode_value))
|
||||
|
||||
return when (listMode) {
|
||||
getString(R.string.list_view_mode_auto_key) -> {
|
||||
val configuration = resources.configuration
|
||||
|
||||
(configuration.orientation == Configuration.ORIENTATION_LANDSCAPE &&
|
||||
configuration.isLayoutSizeAtLeast(Configuration.SCREENLAYOUT_SIZE_LARGE))
|
||||
(
|
||||
configuration.orientation == Configuration.ORIENTATION_LANDSCAPE &&
|
||||
configuration.isLayoutSizeAtLeast(Configuration.SCREENLAYOUT_SIZE_LARGE)
|
||||
)
|
||||
}
|
||||
getString(R.string.list_view_mode_grid_key) -> true
|
||||
else -> false
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
package org.schabi.newpipe.local.subscription
|
||||
|
||||
import android.content.Context
|
||||
import io.reactivex.Completable
|
||||
import io.reactivex.Flowable
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.schedulers.Schedulers
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.core.Completable
|
||||
import io.reactivex.rxjava3.core.Flowable
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import org.schabi.newpipe.NewPipeDatabase
|
||||
import org.schabi.newpipe.database.feed.model.FeedGroupEntity
|
||||
import org.schabi.newpipe.database.subscription.SubscriptionDAO
|
||||
@@ -32,7 +32,8 @@ class SubscriptionManager(context: Context) {
|
||||
filterQuery.isNotEmpty() -> {
|
||||
return if (showOnlyUngrouped) {
|
||||
subscriptionTable.getSubscriptionsOnlyUngroupedFiltered(
|
||||
currentGroupId, filterQuery)
|
||||
currentGroupId, filterQuery
|
||||
)
|
||||
} else {
|
||||
subscriptionTable.getSubscriptionsFiltered(filterQuery)
|
||||
}
|
||||
@@ -44,7 +45,8 @@ class SubscriptionManager(context: Context) {
|
||||
|
||||
fun upsertAll(infoList: List<ChannelInfo>): List<SubscriptionEntity> {
|
||||
val listEntities = subscriptionTable.upsertAll(
|
||||
infoList.map { SubscriptionEntity.from(it) })
|
||||
infoList.map { SubscriptionEntity.from(it) }
|
||||
)
|
||||
|
||||
database.runInTransaction {
|
||||
infoList.forEachIndexed { index, info ->
|
||||
|
||||
@@ -5,12 +5,12 @@ import androidx.lifecycle.AndroidViewModel
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import com.xwray.groupie.Group
|
||||
import io.reactivex.schedulers.Schedulers
|
||||
import java.util.concurrent.TimeUnit
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import org.schabi.newpipe.local.feed.FeedDatabaseManager
|
||||
import org.schabi.newpipe.local.subscription.item.ChannelItem
|
||||
import org.schabi.newpipe.local.subscription.item.FeedGroupCardItem
|
||||
import org.schabi.newpipe.util.DEFAULT_THROTTLE_TIMEOUT
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class SubscriptionViewModel(application: Application) : AndroidViewModel(application) {
|
||||
private var feedDatabaseManager: FeedDatabaseManager = FeedDatabaseManager(application)
|
||||
@@ -22,22 +22,22 @@ class SubscriptionViewModel(application: Application) : AndroidViewModel(applica
|
||||
val feedGroupsLiveData: LiveData<List<Group>> = mutableFeedGroupsLiveData
|
||||
|
||||
private var feedGroupItemsDisposable = feedDatabaseManager.groups()
|
||||
.throttleLatest(DEFAULT_THROTTLE_TIMEOUT, TimeUnit.MILLISECONDS)
|
||||
.map { it.map(::FeedGroupCardItem) }
|
||||
.subscribeOn(Schedulers.io())
|
||||
.subscribe(
|
||||
{ mutableFeedGroupsLiveData.postValue(it) },
|
||||
{ mutableStateLiveData.postValue(SubscriptionState.ErrorState(it)) }
|
||||
)
|
||||
.throttleLatest(DEFAULT_THROTTLE_TIMEOUT, TimeUnit.MILLISECONDS)
|
||||
.map { it.map(::FeedGroupCardItem) }
|
||||
.subscribeOn(Schedulers.io())
|
||||
.subscribe(
|
||||
{ mutableFeedGroupsLiveData.postValue(it) },
|
||||
{ mutableStateLiveData.postValue(SubscriptionState.ErrorState(it)) }
|
||||
)
|
||||
|
||||
private var stateItemsDisposable = subscriptionManager.subscriptions()
|
||||
.throttleLatest(DEFAULT_THROTTLE_TIMEOUT, TimeUnit.MILLISECONDS)
|
||||
.map { it.map { entity -> ChannelItem(entity.toChannelInfoItem(), entity.uid, ChannelItem.ItemVersion.MINI) } }
|
||||
.subscribeOn(Schedulers.io())
|
||||
.subscribe(
|
||||
{ mutableStateLiveData.postValue(SubscriptionState.LoadedState(it)) },
|
||||
{ mutableStateLiveData.postValue(SubscriptionState.ErrorState(it)) }
|
||||
)
|
||||
.throttleLatest(DEFAULT_THROTTLE_TIMEOUT, TimeUnit.MILLISECONDS)
|
||||
.map { it.map { entity -> ChannelItem(entity.toChannelInfoItem(), entity.uid, ChannelItem.ItemVersion.MINI) } }
|
||||
.subscribeOn(Schedulers.io())
|
||||
.subscribe(
|
||||
{ mutableStateLiveData.postValue(SubscriptionState.LoadedState(it)) },
|
||||
{ mutableStateLiveData.postValue(SubscriptionState.ErrorState(it)) }
|
||||
)
|
||||
|
||||
override fun onCleared() {
|
||||
super.onCleared()
|
||||
|
||||
@@ -27,6 +27,7 @@ import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||
import org.schabi.newpipe.extractor.subscription.SubscriptionExtractor;
|
||||
import org.schabi.newpipe.local.subscription.services.SubscriptionsImportService;
|
||||
import org.schabi.newpipe.report.ErrorActivity;
|
||||
import org.schabi.newpipe.report.ErrorInfo;
|
||||
import org.schabi.newpipe.report.UserAction;
|
||||
import org.schabi.newpipe.util.Constants;
|
||||
import org.schabi.newpipe.util.FilePickerActivityHelper;
|
||||
@@ -84,7 +85,7 @@ public class SubscriptionsImportFragment extends BaseFragment {
|
||||
setupServiceVariables();
|
||||
if (supportedSources.isEmpty() && currentServiceId != Constants.NO_SERVICE_ID) {
|
||||
ErrorActivity.reportError(activity, Collections.emptyList(), null, null,
|
||||
ErrorActivity.ErrorInfo.make(UserAction.SOMETHING_ELSE,
|
||||
ErrorInfo.make(UserAction.SOMETHING_ELSE,
|
||||
NewPipe.getNameOfService(currentServiceId),
|
||||
"Service don't support importing", R.string.general_error));
|
||||
activity.finish();
|
||||
|
||||
@@ -3,7 +3,6 @@ package org.schabi.newpipe.local.subscription.dialog
|
||||
import android.app.Dialog
|
||||
import android.os.Bundle
|
||||
import android.os.Parcelable
|
||||
import android.text.TextUtils
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
@@ -25,8 +24,6 @@ import com.xwray.groupie.Section
|
||||
import com.xwray.groupie.kotlinandroidextensions.GroupieViewHolder
|
||||
import icepick.Icepick
|
||||
import icepick.State
|
||||
import java.io.Serializable
|
||||
import kotlin.collections.contains
|
||||
import kotlinx.android.synthetic.main.dialog_feed_group_create.*
|
||||
import kotlinx.android.synthetic.main.toolbar_search_layout.*
|
||||
import org.schabi.newpipe.R
|
||||
@@ -44,6 +41,8 @@ import org.schabi.newpipe.local.subscription.item.PickerIconItem
|
||||
import org.schabi.newpipe.local.subscription.item.PickerSubscriptionItem
|
||||
import org.schabi.newpipe.util.DeviceUtils
|
||||
import org.schabi.newpipe.util.ThemeHelper
|
||||
import java.io.Serializable
|
||||
import kotlin.collections.contains
|
||||
|
||||
class FeedGroupDialog : DialogFragment(), BackPressable {
|
||||
private lateinit var viewModel: FeedGroupDialogViewModel
|
||||
@@ -117,21 +116,30 @@ class FeedGroupDialog : DialogFragment(), BackPressable {
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
viewModel = ViewModelProvider(this,
|
||||
FeedGroupDialogViewModel.Factory(requireContext(),
|
||||
groupId, subscriptionsCurrentSearchQuery, subscriptionsShowOnlyUngrouped)
|
||||
viewModel = ViewModelProvider(
|
||||
this,
|
||||
FeedGroupDialogViewModel.Factory(
|
||||
requireContext(),
|
||||
groupId, subscriptionsCurrentSearchQuery, subscriptionsShowOnlyUngrouped
|
||||
)
|
||||
).get(FeedGroupDialogViewModel::class.java)
|
||||
|
||||
viewModel.groupLiveData.observe(viewLifecycleOwner, Observer(::handleGroup))
|
||||
viewModel.subscriptionsLiveData.observe(viewLifecycleOwner, Observer {
|
||||
setupSubscriptionPicker(it.first, it.second)
|
||||
})
|
||||
viewModel.dialogEventLiveData.observe(viewLifecycleOwner, Observer {
|
||||
when (it) {
|
||||
ProcessingEvent -> disableInput()
|
||||
SuccessEvent -> dismiss()
|
||||
viewModel.subscriptionsLiveData.observe(
|
||||
viewLifecycleOwner,
|
||||
Observer {
|
||||
setupSubscriptionPicker(it.first, it.second)
|
||||
}
|
||||
})
|
||||
)
|
||||
viewModel.dialogEventLiveData.observe(
|
||||
viewLifecycleOwner,
|
||||
Observer {
|
||||
when (it) {
|
||||
ProcessingEvent -> disableInput()
|
||||
SuccessEvent -> dismiss()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
subscriptionGroupAdapter = GroupAdapter<GroupieViewHolder>().apply {
|
||||
add(subscriptionMainSection)
|
||||
@@ -142,8 +150,10 @@ class FeedGroupDialog : DialogFragment(), BackPressable {
|
||||
// Disable animations, too distracting.
|
||||
itemAnimator = null
|
||||
adapter = subscriptionGroupAdapter
|
||||
layoutManager = GridLayoutManager(requireContext(), subscriptionGroupAdapter.spanCount,
|
||||
RecyclerView.VERTICAL, false).apply {
|
||||
layoutManager = GridLayoutManager(
|
||||
requireContext(), subscriptionGroupAdapter.spanCount,
|
||||
RecyclerView.VERTICAL, false
|
||||
).apply {
|
||||
spanSizeLookup = subscriptionGroupAdapter.spanSizeLookup
|
||||
}
|
||||
}
|
||||
@@ -225,7 +235,7 @@ class FeedGroupDialog : DialogFragment(), BackPressable {
|
||||
}
|
||||
|
||||
toolbar_search_clear.setOnClickListener {
|
||||
if (TextUtils.isEmpty(toolbar_search_edit_text.text)) {
|
||||
if (toolbar_search_edit_text.text.isEmpty()) {
|
||||
hideSearch()
|
||||
return@setOnClickListener
|
||||
}
|
||||
@@ -347,7 +357,8 @@ class FeedGroupDialog : DialogFragment(), BackPressable {
|
||||
val selectedCount = this.selectedSubscriptions.size
|
||||
val selectedCountText = resources.getQuantityString(
|
||||
R.plurals.feed_group_dialog_selection_count,
|
||||
selectedCount, selectedCount)
|
||||
selectedCount, selectedCount
|
||||
)
|
||||
selected_subscription_count_view.text = selectedCountText
|
||||
subscriptions_header_info.text = selectedCountText
|
||||
}
|
||||
@@ -402,10 +413,12 @@ class FeedGroupDialog : DialogFragment(), BackPressable {
|
||||
separator.onlyVisibleIn(SubscriptionsPickerScreen, IconPickerScreen)
|
||||
cancel_button.onlyVisibleIn(InitialScreen, DeleteScreen)
|
||||
|
||||
confirm_button.setText(when {
|
||||
currentScreen == InitialScreen && groupId == NO_GROUP_SELECTED -> R.string.create
|
||||
else -> android.R.string.ok
|
||||
})
|
||||
confirm_button.setText(
|
||||
when {
|
||||
currentScreen == InitialScreen && groupId == NO_GROUP_SELECTED -> R.string.create
|
||||
else -> android.R.string.ok
|
||||
}
|
||||
)
|
||||
|
||||
delete_button.isGone = currentScreen != InitialScreen || groupId == NO_GROUP_SELECTED
|
||||
|
||||
@@ -455,8 +468,10 @@ class FeedGroupDialog : DialogFragment(), BackPressable {
|
||||
}
|
||||
|
||||
private fun hideKeyboardSearch() {
|
||||
inputMethodManager.hideSoftInputFromWindow(toolbar_search_edit_text.windowToken,
|
||||
InputMethodManager.RESULT_UNCHANGED_SHOWN)
|
||||
inputMethodManager.hideSoftInputFromWindow(
|
||||
toolbar_search_edit_text.windowToken,
|
||||
InputMethodManager.RESULT_UNCHANGED_SHOWN
|
||||
)
|
||||
toolbar_search_edit_text.clearFocus()
|
||||
}
|
||||
|
||||
@@ -467,8 +482,10 @@ class FeedGroupDialog : DialogFragment(), BackPressable {
|
||||
}
|
||||
|
||||
private fun hideKeyboard() {
|
||||
inputMethodManager.hideSoftInputFromWindow(group_name_input.windowToken,
|
||||
InputMethodManager.RESULT_UNCHANGED_SHOWN)
|
||||
inputMethodManager.hideSoftInputFromWindow(
|
||||
group_name_input.windowToken,
|
||||
InputMethodManager.RESULT_UNCHANGED_SHOWN
|
||||
)
|
||||
group_name_input.clearFocus()
|
||||
}
|
||||
|
||||
|
||||
@@ -5,12 +5,12 @@ import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import io.reactivex.Completable
|
||||
import io.reactivex.Flowable
|
||||
import io.reactivex.disposables.Disposable
|
||||
import io.reactivex.functions.BiFunction
|
||||
import io.reactivex.processors.BehaviorProcessor
|
||||
import io.reactivex.schedulers.Schedulers
|
||||
import io.reactivex.rxjava3.core.Completable
|
||||
import io.reactivex.rxjava3.core.Flowable
|
||||
import io.reactivex.rxjava3.disposables.Disposable
|
||||
import io.reactivex.rxjava3.functions.BiFunction
|
||||
import io.reactivex.rxjava3.processors.BehaviorProcessor
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import org.schabi.newpipe.database.feed.model.FeedGroupEntity
|
||||
import org.schabi.newpipe.local.feed.FeedDatabaseManager
|
||||
import org.schabi.newpipe.local.subscription.FeedGroupIcon
|
||||
@@ -32,9 +32,9 @@ class FeedGroupDialogViewModel(
|
||||
|
||||
private var subscriptionsFlowable = Flowable
|
||||
.combineLatest(
|
||||
filterSubscriptions.startWith(initialQuery),
|
||||
toggleShowOnlyUngrouped.startWith(initialShowOnlyUngrouped),
|
||||
BiFunction { t1: String, t2: Boolean -> Filter(t1, t2) }
|
||||
filterSubscriptions.startWithItem(initialQuery),
|
||||
toggleShowOnlyUngrouped.startWithItem(initialShowOnlyUngrouped),
|
||||
BiFunction { t1: String, t2: Boolean -> Filter(t1, t2) }
|
||||
)
|
||||
.distinctUntilChanged()
|
||||
.switchMap { (query, showOnlyUngrouped) ->
|
||||
@@ -55,8 +55,10 @@ class FeedGroupDialogViewModel(
|
||||
.subscribe(mutableGroupLiveData::postValue)
|
||||
|
||||
private var subscriptionsDisposable = Flowable
|
||||
.combineLatest(subscriptionsFlowable, feedDatabaseManager.subscriptionIdsForGroup(groupId),
|
||||
BiFunction { t1: List<PickerSubscriptionItem>, t2: List<Long> -> t1 to t2.toSet() })
|
||||
.combineLatest(
|
||||
subscriptionsFlowable, feedDatabaseManager.subscriptionIdsForGroup(groupId),
|
||||
BiFunction { t1: List<PickerSubscriptionItem>, t2: List<Long> -> t1 to t2.toSet() }
|
||||
)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.subscribe(mutableSubscriptionsLiveData::postValue)
|
||||
|
||||
@@ -68,15 +70,19 @@ class FeedGroupDialogViewModel(
|
||||
}
|
||||
|
||||
fun createGroup(name: String, selectedIcon: FeedGroupIcon, selectedSubscriptions: Set<Long>) {
|
||||
doAction(feedDatabaseManager.createGroup(name, selectedIcon)
|
||||
.flatMapCompletable {
|
||||
feedDatabaseManager.updateSubscriptionsForGroup(it, selectedSubscriptions.toList())
|
||||
})
|
||||
doAction(
|
||||
feedDatabaseManager.createGroup(name, selectedIcon)
|
||||
.flatMapCompletable {
|
||||
feedDatabaseManager.updateSubscriptionsForGroup(it, selectedSubscriptions.toList())
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
fun updateGroup(name: String, selectedIcon: FeedGroupIcon, selectedSubscriptions: Set<Long>, sortOrder: Long) {
|
||||
doAction(feedDatabaseManager.updateSubscriptionsForGroup(groupId, selectedSubscriptions.toList())
|
||||
.andThen(feedDatabaseManager.updateGroup(FeedGroupEntity(groupId, name, selectedIcon, sortOrder))))
|
||||
doAction(
|
||||
feedDatabaseManager.updateSubscriptionsForGroup(groupId, selectedSubscriptions.toList())
|
||||
.andThen(feedDatabaseManager.updateGroup(FeedGroupEntity(groupId, name, selectedIcon, sortOrder)))
|
||||
)
|
||||
}
|
||||
|
||||
fun deleteGroup() {
|
||||
@@ -120,8 +126,10 @@ class FeedGroupDialogViewModel(
|
||||
) : ViewModelProvider.Factory {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
|
||||
return FeedGroupDialogViewModel(context.applicationContext,
|
||||
groupId, initialQuery, initialShowOnlyUngrouped) as T
|
||||
return FeedGroupDialogViewModel(
|
||||
context.applicationContext,
|
||||
groupId, initialQuery, initialShowOnlyUngrouped
|
||||
) as T
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,6 @@ import com.xwray.groupie.TouchCallback
|
||||
import com.xwray.groupie.kotlinandroidextensions.GroupieViewHolder
|
||||
import icepick.Icepick
|
||||
import icepick.State
|
||||
import java.util.Collections
|
||||
import kotlinx.android.synthetic.main.dialog_feed_group_reorder.confirm_button
|
||||
import kotlinx.android.synthetic.main.dialog_feed_group_reorder.feed_groups_list
|
||||
import org.schabi.newpipe.R
|
||||
@@ -25,6 +24,7 @@ import org.schabi.newpipe.local.subscription.dialog.FeedGroupReorderDialogViewMo
|
||||
import org.schabi.newpipe.local.subscription.dialog.FeedGroupReorderDialogViewModel.DialogEvent.SuccessEvent
|
||||
import org.schabi.newpipe.local.subscription.item.FeedGroupReorderItem
|
||||
import org.schabi.newpipe.util.ThemeHelper
|
||||
import java.util.Collections
|
||||
|
||||
class FeedGroupReorderDialog : DialogFragment() {
|
||||
private lateinit var viewModel: FeedGroupReorderDialogViewModel
|
||||
@@ -51,12 +51,15 @@ class FeedGroupReorderDialog : DialogFragment() {
|
||||
|
||||
viewModel = ViewModelProvider(this).get(FeedGroupReorderDialogViewModel::class.java)
|
||||
viewModel.groupsLiveData.observe(viewLifecycleOwner, Observer(::handleGroups))
|
||||
viewModel.dialogEventLiveData.observe(viewLifecycleOwner, Observer {
|
||||
when (it) {
|
||||
ProcessingEvent -> disableInput()
|
||||
SuccessEvent -> dismiss()
|
||||
viewModel.dialogEventLiveData.observe(
|
||||
viewLifecycleOwner,
|
||||
Observer {
|
||||
when (it) {
|
||||
ProcessingEvent -> disableInput()
|
||||
SuccessEvent -> dismiss()
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
feed_groups_list.layoutManager = LinearLayoutManager(requireContext())
|
||||
feed_groups_list.adapter = groupAdapter
|
||||
|
||||
@@ -4,9 +4,9 @@ import android.app.Application
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import io.reactivex.Completable
|
||||
import io.reactivex.disposables.Disposable
|
||||
import io.reactivex.schedulers.Schedulers
|
||||
import io.reactivex.rxjava3.core.Completable
|
||||
import io.reactivex.rxjava3.disposables.Disposable
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import org.schabi.newpipe.database.feed.model.FeedGroupEntity
|
||||
import org.schabi.newpipe.local.feed.FeedDatabaseManager
|
||||
|
||||
@@ -21,9 +21,9 @@ class FeedGroupReorderDialogViewModel(application: Application) : AndroidViewMod
|
||||
private var actionProcessingDisposable: Disposable? = null
|
||||
|
||||
private var groupsDisposable = feedDatabaseManager.groups()
|
||||
.limit(1)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.subscribe(mutableGroupsLiveData::postValue)
|
||||
.take(1)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.subscribe(mutableGroupsLiveData::postValue)
|
||||
|
||||
override fun onCleared() {
|
||||
super.onCleared()
|
||||
@@ -40,8 +40,8 @@ class FeedGroupReorderDialogViewModel(application: Application) : AndroidViewMod
|
||||
mutableDialogEventLiveData.value = DialogEvent.ProcessingEvent
|
||||
|
||||
actionProcessingDisposable = completable
|
||||
.subscribeOn(Schedulers.io())
|
||||
.subscribe { mutableDialogEventLiveData.postValue(DialogEvent.SuccessEvent) }
|
||||
.subscribeOn(Schedulers.io())
|
||||
.subscribe { mutableDialogEventLiveData.postValue(DialogEvent.SuccessEvent) }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -36,8 +36,10 @@ class ChannelItem(
|
||||
viewHolder.itemAdditionalDetails.text = getDetailLine(viewHolder.root.context)
|
||||
if (itemVersion == ItemVersion.NORMAL) viewHolder.itemChannelDescriptionView.text = infoItem.description
|
||||
|
||||
ImageLoader.getInstance().displayImage(infoItem.thumbnailUrl, viewHolder.itemThumbnailView,
|
||||
ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS)
|
||||
ImageLoader.getInstance().displayImage(
|
||||
infoItem.thumbnailUrl, viewHolder.itemThumbnailView,
|
||||
ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS
|
||||
)
|
||||
|
||||
gesturesListener?.run {
|
||||
viewHolder.containerView.setOnClickListener { selected(infoItem) }
|
||||
|
||||
@@ -20,7 +20,7 @@ data class FeedGroupReorderItem(
|
||||
val dragCallback: ItemTouchHelper
|
||||
) : Item() {
|
||||
constructor (feedGroupEntity: FeedGroupEntity, dragCallback: ItemTouchHelper) :
|
||||
this(feedGroupEntity.uid, feedGroupEntity.name, feedGroupEntity.icon, dragCallback)
|
||||
this(feedGroupEntity.uid, feedGroupEntity.name, feedGroupEntity.icon, dragCallback)
|
||||
|
||||
override fun getId(): Long {
|
||||
return when (groupId) {
|
||||
|
||||
@@ -49,8 +49,10 @@ class FeedImportExportItem(
|
||||
|
||||
expandIconListener?.let { viewHolder.import_export_options.removeListener(it) }
|
||||
expandIconListener = CollapsibleView.StateListener { newState ->
|
||||
AnimationUtils.animateRotation(viewHolder.import_export_expand_icon,
|
||||
250, if (newState == CollapsibleView.COLLAPSED) 0 else 180)
|
||||
AnimationUtils.animateRotation(
|
||||
viewHolder.import_export_expand_icon,
|
||||
250, if (newState == CollapsibleView.COLLAPSED) 0 else 180
|
||||
)
|
||||
}
|
||||
|
||||
viewHolder.import_export_options.currentState = if (isExpanded) CollapsibleView.EXPANDED else CollapsibleView.COLLAPSED
|
||||
@@ -85,8 +87,10 @@ class FeedImportExportItem(
|
||||
}
|
||||
|
||||
private fun setupImportFromItems(listHolder: ViewGroup) {
|
||||
val previousBackupItem = addItemView(listHolder.context.getString(R.string.previous_export),
|
||||
ThemeHelper.resolveResourceIdFromAttr(listHolder.context, R.attr.ic_backup), listHolder)
|
||||
val previousBackupItem = addItemView(
|
||||
listHolder.context.getString(R.string.previous_export),
|
||||
ThemeHelper.resolveResourceIdFromAttr(listHolder.context, R.attr.ic_backup), listHolder
|
||||
)
|
||||
previousBackupItem.setOnClickListener { onImportPreviousSelected() }
|
||||
|
||||
val iconColor = if (ThemeHelper.isLightThemeSelected(listHolder.context)) Color.BLACK else Color.WHITE
|
||||
@@ -112,8 +116,10 @@ class FeedImportExportItem(
|
||||
}
|
||||
|
||||
private fun setupExportToItems(listHolder: ViewGroup) {
|
||||
val previousBackupItem = addItemView(listHolder.context.getString(R.string.file),
|
||||
ThemeHelper.resolveResourceIdFromAttr(listHolder.context, R.attr.ic_save), listHolder)
|
||||
val previousBackupItem = addItemView(
|
||||
listHolder.context.getString(R.string.file),
|
||||
ThemeHelper.resolveResourceIdFromAttr(listHolder.context, R.attr.ic_save), listHolder
|
||||
)
|
||||
previousBackupItem.setOnClickListener { onExportSelected() }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,11 +36,11 @@ class HeaderWithMenuItem(
|
||||
viewHolder.header_menu_item.setImageResource(itemIcon)
|
||||
|
||||
val listener: OnClickListener? =
|
||||
onClickListener?.let { OnClickListener { onClickListener.invoke() } }
|
||||
onClickListener?.let { OnClickListener { onClickListener.invoke() } }
|
||||
viewHolder.root.setOnClickListener(listener)
|
||||
|
||||
val menuItemListener: OnClickListener? =
|
||||
menuItemOnClickListener?.let { OnClickListener { menuItemOnClickListener.invoke() } }
|
||||
menuItemOnClickListener?.let { OnClickListener { menuItemOnClickListener.invoke() } }
|
||||
viewHolder.header_menu_item.setOnClickListener(menuItemListener)
|
||||
updateMenuItemVisibility(viewHolder)
|
||||
}
|
||||
|
||||
@@ -22,8 +22,10 @@ data class PickerSubscriptionItem(
|
||||
override fun getSpanSize(spanCount: Int, position: Int): Int = 1
|
||||
|
||||
override fun bind(viewHolder: GroupieViewHolder, position: Int) {
|
||||
ImageLoader.getInstance().displayImage(subscriptionEntity.avatarUrl,
|
||||
viewHolder.thumbnail_view, ImageDisplayConstants.DISPLAY_AVATAR_OPTIONS)
|
||||
ImageLoader.getInstance().displayImage(
|
||||
subscriptionEntity.avatarUrl,
|
||||
viewHolder.thumbnail_view, ImageDisplayConstants.DISPLAY_AVATAR_OPTIONS
|
||||
)
|
||||
|
||||
viewHolder.title_view.text = subscriptionEntity.name
|
||||
viewHolder.selected_highlight.isVisible = isSelected
|
||||
@@ -39,7 +41,9 @@ data class PickerSubscriptionItem(
|
||||
|
||||
fun updateSelected(containerView: View, isSelected: Boolean) {
|
||||
this.isSelected = isSelected
|
||||
animateView(containerView.selected_highlight,
|
||||
AnimationUtils.Type.LIGHT_SCALE_AND_ALPHA, isSelected, 150)
|
||||
animateView(
|
||||
containerView.selected_highlight,
|
||||
AnimationUtils.Type.LIGHT_SCALE_AND_ALPHA, isSelected, 150
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,12 +31,14 @@ import androidx.annotation.Nullable;
|
||||
import androidx.annotation.StringRes;
|
||||
import androidx.core.app.NotificationCompat;
|
||||
import androidx.core.app.NotificationManagerCompat;
|
||||
import androidx.core.app.ServiceCompat;
|
||||
|
||||
import org.reactivestreams.Publisher;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.extractor.subscription.SubscriptionExtractor;
|
||||
import org.schabi.newpipe.local.subscription.SubscriptionManager;
|
||||
import org.schabi.newpipe.report.ErrorActivity;
|
||||
import org.schabi.newpipe.report.ErrorInfo;
|
||||
import org.schabi.newpipe.report.UserAction;
|
||||
import org.schabi.newpipe.util.ExceptionUtils;
|
||||
|
||||
@@ -45,11 +47,11 @@ import java.util.Collections;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import io.reactivex.Flowable;
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.disposables.CompositeDisposable;
|
||||
import io.reactivex.functions.Function;
|
||||
import io.reactivex.processors.PublishProcessor;
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.rxjava3.core.Flowable;
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
||||
import io.reactivex.rxjava3.functions.Function;
|
||||
import io.reactivex.rxjava3.processors.PublishProcessor;
|
||||
|
||||
public abstract class BaseImportExportService extends Service {
|
||||
protected final String TAG = this.getClass().getSimpleName();
|
||||
@@ -119,7 +121,7 @@ public abstract class BaseImportExportService extends Service {
|
||||
startForeground(getNotificationId(), notificationBuilder.build());
|
||||
|
||||
final Function<Flowable<String>, Publisher<String>> throttleAfterFirstEmission = flow ->
|
||||
flow.limit(1).concatWith(flow.skip(1)
|
||||
flow.take(1).concatWith(flow.skip(1)
|
||||
.throttleLast(NOTIFICATION_SAMPLING_PERIOD, TimeUnit.MILLISECONDS));
|
||||
|
||||
disposables.add(notificationUpdater
|
||||
@@ -153,7 +155,7 @@ public abstract class BaseImportExportService extends Service {
|
||||
protected void stopAndReportError(@Nullable final Throwable error, final String request) {
|
||||
stopService();
|
||||
|
||||
final ErrorActivity.ErrorInfo errorInfo = ErrorActivity.ErrorInfo
|
||||
final ErrorInfo errorInfo = ErrorInfo
|
||||
.make(UserAction.SUBSCRIPTION, "unknown", request, R.string.general_error);
|
||||
ErrorActivity.reportError(this, error != null ? Collections.singletonList(error)
|
||||
: Collections.emptyList(), null, null, errorInfo);
|
||||
@@ -161,7 +163,7 @@ public abstract class BaseImportExportService extends Service {
|
||||
|
||||
protected void postErrorResult(final String title, final String text) {
|
||||
disposeAll();
|
||||
stopForeground(true);
|
||||
ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE);
|
||||
stopSelf();
|
||||
|
||||
if (title == null) {
|
||||
|
||||
@@ -9,7 +9,7 @@ public interface ImportExportEventListener {
|
||||
void onSizeReceived(int size);
|
||||
|
||||
/**
|
||||
* Called everytime an item has been parsed/resolved.
|
||||
* Called every time an item has been parsed/resolved.
|
||||
*
|
||||
* @param itemName the name of the subscription item
|
||||
*/
|
||||
|
||||
@@ -37,9 +37,9 @@ import java.io.FileOutputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.functions.Function;
|
||||
import io.reactivex.schedulers.Schedulers;
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.rxjava3.functions.Function;
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers;
|
||||
|
||||
import static org.schabi.newpipe.MainActivity.DEBUG;
|
||||
|
||||
|
||||
@@ -46,12 +46,12 @@ import java.io.InputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import io.reactivex.Flowable;
|
||||
import io.reactivex.Notification;
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.functions.Consumer;
|
||||
import io.reactivex.functions.Function;
|
||||
import io.reactivex.schedulers.Schedulers;
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.rxjava3.core.Flowable;
|
||||
import io.reactivex.rxjava3.core.Notification;
|
||||
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;
|
||||
|
||||
|
||||
@@ -27,13 +27,13 @@ import android.content.SharedPreferences;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.media.AudioManager;
|
||||
import androidx.preference.PreferenceManager;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.preference.PreferenceManager;
|
||||
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.DefaultRenderersFactory;
|
||||
@@ -77,17 +77,16 @@ import org.schabi.newpipe.util.SerializedCache;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import io.reactivex.Observable;
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.disposables.CompositeDisposable;
|
||||
import io.reactivex.disposables.Disposable;
|
||||
import io.reactivex.disposables.SerialDisposable;
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.rxjava3.core.Observable;
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
||||
import io.reactivex.rxjava3.disposables.Disposable;
|
||||
import io.reactivex.rxjava3.disposables.SerialDisposable;
|
||||
|
||||
import static com.google.android.exoplayer2.Player.DISCONTINUITY_REASON_INTERNAL;
|
||||
import static com.google.android.exoplayer2.Player.DISCONTINUITY_REASON_PERIOD_TRANSITION;
|
||||
import static com.google.android.exoplayer2.Player.DISCONTINUITY_REASON_SEEK;
|
||||
import static com.google.android.exoplayer2.Player.DISCONTINUITY_REASON_SEEK_ADJUSTMENT;
|
||||
import static io.reactivex.android.schedulers.AndroidSchedulers.mainThread;
|
||||
import static java.util.concurrent.TimeUnit.MILLISECONDS;
|
||||
|
||||
/**
|
||||
@@ -685,7 +684,7 @@ public abstract class BasePlayer implements
|
||||
|
||||
public void onMuteUnmuteButtonClicked() {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "onMuteUnmuteButtonClicled() called");
|
||||
Log.d(TAG, "onMuteUnmuteButtonClicked() called");
|
||||
}
|
||||
simpleExoPlayer.setVolume(isMuted() ? 1 : 0);
|
||||
}
|
||||
@@ -720,8 +719,9 @@ public abstract class BasePlayer implements
|
||||
}
|
||||
|
||||
private Disposable getProgressReactor() {
|
||||
return Observable.interval(PROGRESS_LOOP_INTERVAL_MILLIS, MILLISECONDS, mainThread())
|
||||
.observeOn(mainThread())
|
||||
return Observable.interval(PROGRESS_LOOP_INTERVAL_MILLIS, MILLISECONDS,
|
||||
AndroidSchedulers.mainThread())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(ignored -> triggerProgressUpdate(),
|
||||
error -> Log.e(TAG, "Progress update failure: ", error));
|
||||
}
|
||||
@@ -1319,7 +1319,7 @@ public abstract class BasePlayer implements
|
||||
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
||||
if (prefs.getBoolean(context.getString(R.string.enable_watch_history_key), true)) {
|
||||
final Disposable stateSaver = recordManager.saveStreamState(info, progress)
|
||||
.observeOn(mainThread())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.doOnError((e) -> {
|
||||
if (DEBUG) {
|
||||
e.printStackTrace();
|
||||
@@ -1339,7 +1339,7 @@ public abstract class BasePlayer implements
|
||||
if (prefs.getBoolean(context.getString(R.string.enable_watch_history_key), true)) {
|
||||
final Disposable stateSaver = queueItem.getStream()
|
||||
.flatMapCompletable(info -> recordManager.saveStreamState(info, 0))
|
||||
.observeOn(mainThread())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.doOnError((e) -> {
|
||||
if (DEBUG) {
|
||||
e.printStackTrace();
|
||||
@@ -1545,8 +1545,7 @@ public abstract class BasePlayer implements
|
||||
if (simpleExoPlayer == null) {
|
||||
return PlaybackParameters.DEFAULT;
|
||||
}
|
||||
final PlaybackParameters parameters = simpleExoPlayer.getPlaybackParameters();
|
||||
return parameters == null ? PlaybackParameters.DEFAULT : parameters;
|
||||
return simpleExoPlayer.getPlaybackParameters();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -15,6 +15,7 @@ import androidx.annotation.Nullable;
|
||||
import androidx.annotation.StringRes;
|
||||
import androidx.core.app.NotificationCompat;
|
||||
import androidx.core.app.NotificationManagerCompat;
|
||||
import androidx.core.app.ServiceCompat;
|
||||
import androidx.core.content.ContextCompat;
|
||||
|
||||
import org.schabi.newpipe.MainActivity;
|
||||
@@ -48,7 +49,7 @@ public final class NotificationUtil {
|
||||
@Nullable private static NotificationUtil instance = null;
|
||||
|
||||
@NotificationConstants.Action
|
||||
private int[] notificationSlots = NotificationConstants.SLOT_DEFAULTS.clone();
|
||||
private final int[] notificationSlots = NotificationConstants.SLOT_DEFAULTS.clone();
|
||||
|
||||
private NotificationManagerCompat notificationManager;
|
||||
private NotificationCompat.Builder notificationBuilder;
|
||||
@@ -146,7 +147,11 @@ public final class NotificationUtil {
|
||||
notificationBuilder.setContentText(player.getUploaderName());
|
||||
notificationBuilder.setTicker(player.getVideoTitle());
|
||||
updateActions(notificationBuilder, player);
|
||||
setLargeIcon(notificationBuilder, player);
|
||||
final boolean showThumbnail = player.sharedPreferences.getBoolean(
|
||||
player.context.getString(R.string.show_thumbnail_key), true);
|
||||
if (showThumbnail) {
|
||||
setLargeIcon(notificationBuilder, player);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -184,7 +189,7 @@ public final class NotificationUtil {
|
||||
}
|
||||
|
||||
void cancelNotificationAndStopForeground(final Service service) {
|
||||
service.stopForeground(true);
|
||||
ServiceCompat.stopForeground(service, ServiceCompat.STOP_FOREGROUND_REMOVE);
|
||||
|
||||
if (notificationManager != null) {
|
||||
notificationManager.cancel(NOTIFICATION_ID);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package org.schabi.newpipe.player;
|
||||
|
||||
import android.content.ComponentName;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.ServiceConnection;
|
||||
import android.os.Bundle;
|
||||
@@ -11,15 +12,11 @@ import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageButton;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.PopupMenu;
|
||||
import android.widget.ProgressBar;
|
||||
import android.widget.SeekBar;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.core.app.ActivityCompat;
|
||||
import androidx.recyclerview.widget.ItemTouchHelper;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
@@ -28,6 +25,7 @@ import com.google.android.exoplayer2.PlaybackParameters;
|
||||
import com.google.android.exoplayer2.Player;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.databinding.ActivityPlayerQueueControlBinding;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||
import org.schabi.newpipe.fragments.OnScrollBelowItemsListener;
|
||||
import org.schabi.newpipe.local.dialog.PlaylistAppendDialog;
|
||||
@@ -69,30 +67,10 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
|
||||
// Views
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
private View rootView;
|
||||
private ActivityPlayerQueueControlBinding queueControlBinding;
|
||||
|
||||
private RecyclerView itemsList;
|
||||
private ItemTouchHelper itemTouchHelper;
|
||||
|
||||
private LinearLayout metadata;
|
||||
private TextView metadataTitle;
|
||||
private TextView metadataArtist;
|
||||
|
||||
private SeekBar progressSeekBar;
|
||||
private TextView progressCurrentTime;
|
||||
private TextView progressEndTime;
|
||||
private TextView progressLiveSync;
|
||||
private TextView seekDisplay;
|
||||
|
||||
private ImageButton repeatButton;
|
||||
private ImageButton backwardButton;
|
||||
private ImageButton fastRewindButton;
|
||||
private ImageButton playPauseButton;
|
||||
private ImageButton fastForwardButton;
|
||||
private ImageButton forwardButton;
|
||||
private ImageButton shuffleButton;
|
||||
private ProgressBar progressBar;
|
||||
|
||||
private Menu menu;
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
@@ -122,11 +100,11 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
|
||||
assureCorrectAppLanguage(this);
|
||||
super.onCreate(savedInstanceState);
|
||||
ThemeHelper.setTheme(this);
|
||||
setContentView(R.layout.activity_player_queue_control);
|
||||
rootView = findViewById(R.id.main_content);
|
||||
|
||||
final Toolbar toolbar = rootView.findViewById(R.id.toolbar);
|
||||
setSupportActionBar(toolbar);
|
||||
queueControlBinding = ActivityPlayerQueueControlBinding.inflate(getLayoutInflater());
|
||||
setContentView(queueControlBinding.getRoot());
|
||||
|
||||
setSupportActionBar(queueControlBinding.toolbar);
|
||||
if (getSupportActionBar() != null) {
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
getSupportActionBar().setTitle(getSupportActionTitle());
|
||||
@@ -140,7 +118,7 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
if (redraw) {
|
||||
recreate();
|
||||
ActivityCompat.recreate(this);
|
||||
redraw = false;
|
||||
}
|
||||
}
|
||||
@@ -229,14 +207,11 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
|
||||
if (player != null && player.getPlayQueueAdapter() != null) {
|
||||
player.getPlayQueueAdapter().unsetSelectedListener();
|
||||
}
|
||||
if (itemsList != null) {
|
||||
itemsList.setAdapter(null);
|
||||
}
|
||||
queueControlBinding.playQueue.setAdapter(null);
|
||||
if (itemTouchHelper != null) {
|
||||
itemTouchHelper.attachToRecyclerView(null);
|
||||
}
|
||||
|
||||
itemsList = null;
|
||||
itemTouchHelper = null;
|
||||
player = null;
|
||||
}
|
||||
@@ -283,58 +258,38 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
|
||||
}
|
||||
|
||||
private void buildQueue() {
|
||||
itemsList = findViewById(R.id.play_queue);
|
||||
itemsList.setLayoutManager(new LinearLayoutManager(this));
|
||||
itemsList.setAdapter(player.getPlayQueueAdapter());
|
||||
itemsList.setClickable(true);
|
||||
itemsList.setLongClickable(true);
|
||||
itemsList.clearOnScrollListeners();
|
||||
itemsList.addOnScrollListener(getQueueScrollListener());
|
||||
queueControlBinding.playQueue.setLayoutManager(new LinearLayoutManager(this));
|
||||
queueControlBinding.playQueue.setAdapter(player.getPlayQueueAdapter());
|
||||
queueControlBinding.playQueue.setClickable(true);
|
||||
queueControlBinding.playQueue.setLongClickable(true);
|
||||
queueControlBinding.playQueue.clearOnScrollListeners();
|
||||
queueControlBinding.playQueue.addOnScrollListener(getQueueScrollListener());
|
||||
|
||||
itemTouchHelper = new ItemTouchHelper(getItemTouchCallback());
|
||||
itemTouchHelper.attachToRecyclerView(itemsList);
|
||||
itemTouchHelper.attachToRecyclerView(queueControlBinding.playQueue);
|
||||
|
||||
player.getPlayQueueAdapter().setSelectedListener(getOnSelectedListener());
|
||||
}
|
||||
|
||||
private void buildMetadata() {
|
||||
metadata = rootView.findViewById(R.id.metadata);
|
||||
metadataTitle = rootView.findViewById(R.id.song_name);
|
||||
metadataArtist = rootView.findViewById(R.id.artist_name);
|
||||
|
||||
metadata.setOnClickListener(this);
|
||||
metadataTitle.setSelected(true);
|
||||
metadataArtist.setSelected(true);
|
||||
queueControlBinding.metadata.setOnClickListener(this);
|
||||
queueControlBinding.songName.setSelected(true);
|
||||
queueControlBinding.artistName.setSelected(true);
|
||||
}
|
||||
|
||||
private void buildSeekBar() {
|
||||
progressCurrentTime = rootView.findViewById(R.id.current_time);
|
||||
progressSeekBar = rootView.findViewById(R.id.seek_bar);
|
||||
progressEndTime = rootView.findViewById(R.id.end_time);
|
||||
progressLiveSync = rootView.findViewById(R.id.live_sync);
|
||||
seekDisplay = rootView.findViewById(R.id.seek_display);
|
||||
|
||||
progressSeekBar.setOnSeekBarChangeListener(this);
|
||||
progressLiveSync.setOnClickListener(this);
|
||||
queueControlBinding.seekBar.setOnSeekBarChangeListener(this);
|
||||
queueControlBinding.liveSync.setOnClickListener(this);
|
||||
}
|
||||
|
||||
private void buildControls() {
|
||||
repeatButton = rootView.findViewById(R.id.control_repeat);
|
||||
backwardButton = rootView.findViewById(R.id.control_backward);
|
||||
fastRewindButton = rootView.findViewById(R.id.control_fast_rewind);
|
||||
playPauseButton = rootView.findViewById(R.id.control_play_pause);
|
||||
fastForwardButton = rootView.findViewById(R.id.control_fast_forward);
|
||||
forwardButton = rootView.findViewById(R.id.control_forward);
|
||||
shuffleButton = rootView.findViewById(R.id.control_shuffle);
|
||||
progressBar = rootView.findViewById(R.id.control_progress_bar);
|
||||
|
||||
repeatButton.setOnClickListener(this);
|
||||
backwardButton.setOnClickListener(this);
|
||||
fastRewindButton.setOnClickListener(this);
|
||||
playPauseButton.setOnClickListener(this);
|
||||
fastForwardButton.setOnClickListener(this);
|
||||
forwardButton.setOnClickListener(this);
|
||||
shuffleButton.setOnClickListener(this);
|
||||
queueControlBinding.controlRepeat.setOnClickListener(this);
|
||||
queueControlBinding.controlBackward.setOnClickListener(this);
|
||||
queueControlBinding.controlFastRewind.setOnClickListener(this);
|
||||
queueControlBinding.controlPlayPause.setOnClickListener(this);
|
||||
queueControlBinding.controlFastForward.setOnClickListener(this);
|
||||
queueControlBinding.controlForward.setOnClickListener(this);
|
||||
queueControlBinding.controlShuffle.setOnClickListener(this);
|
||||
}
|
||||
|
||||
private void buildItemPopupMenu(final PlayQueueItem item, final View view) {
|
||||
@@ -390,8 +345,8 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
|
||||
if (player != null && player.getPlayQueue() != null
|
||||
&& !player.getPlayQueue().isComplete()) {
|
||||
player.getPlayQueue().fetch();
|
||||
} else if (itemsList != null) {
|
||||
itemsList.clearOnScrollListeners();
|
||||
} else {
|
||||
queueControlBinding.playQueue.clearOnScrollListeners();
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -452,8 +407,9 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
|
||||
|
||||
final int currentPlayingIndex = player.getPlayQueue().getIndex();
|
||||
final int currentVisibleIndex;
|
||||
if (itemsList.getLayoutManager() instanceof LinearLayoutManager) {
|
||||
final LinearLayoutManager layout = ((LinearLayoutManager) itemsList.getLayoutManager());
|
||||
if (queueControlBinding.playQueue.getLayoutManager() instanceof LinearLayoutManager) {
|
||||
final LinearLayoutManager layout =
|
||||
(LinearLayoutManager) queueControlBinding.playQueue.getLayoutManager();
|
||||
currentVisibleIndex = layout.findFirstVisibleItemPosition();
|
||||
} else {
|
||||
currentVisibleIndex = 0;
|
||||
@@ -461,9 +417,9 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
|
||||
|
||||
final int distance = Math.abs(currentPlayingIndex - currentVisibleIndex);
|
||||
if (distance < SMOOTH_SCROLL_MAXIMUM_DISTANCE) {
|
||||
itemsList.smoothScrollToPosition(currentPlayingIndex);
|
||||
queueControlBinding.playQueue.smoothScrollToPosition(currentPlayingIndex);
|
||||
} else {
|
||||
itemsList.scrollToPosition(currentPlayingIndex);
|
||||
queueControlBinding.playQueue.scrollToPosition(currentPlayingIndex);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -477,23 +433,23 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
|
||||
return;
|
||||
}
|
||||
|
||||
if (view.getId() == repeatButton.getId()) {
|
||||
if (view.getId() == queueControlBinding.controlRepeat.getId()) {
|
||||
player.onRepeatClicked();
|
||||
} else if (view.getId() == backwardButton.getId()) {
|
||||
} else if (view.getId() == queueControlBinding.controlBackward.getId()) {
|
||||
player.onPlayPrevious();
|
||||
} else if (view.getId() == fastRewindButton.getId()) {
|
||||
} else if (view.getId() == queueControlBinding.controlFastRewind.getId()) {
|
||||
player.onFastRewind();
|
||||
} else if (view.getId() == playPauseButton.getId()) {
|
||||
} else if (view.getId() == queueControlBinding.controlPlayPause.getId()) {
|
||||
player.onPlayPause();
|
||||
} else if (view.getId() == fastForwardButton.getId()) {
|
||||
} else if (view.getId() == queueControlBinding.controlFastForward.getId()) {
|
||||
player.onFastForward();
|
||||
} else if (view.getId() == forwardButton.getId()) {
|
||||
} else if (view.getId() == queueControlBinding.controlForward.getId()) {
|
||||
player.onPlayNext();
|
||||
} else if (view.getId() == shuffleButton.getId()) {
|
||||
} else if (view.getId() == queueControlBinding.controlShuffle.getId()) {
|
||||
player.onShuffleClicked();
|
||||
} else if (view.getId() == metadata.getId()) {
|
||||
} else if (view.getId() == queueControlBinding.metadata.getId()) {
|
||||
scrollToSelected();
|
||||
} else if (view.getId() == progressLiveSync.getId()) {
|
||||
} else if (view.getId() == queueControlBinding.liveSync.getId()) {
|
||||
player.seekToDefault();
|
||||
}
|
||||
}
|
||||
@@ -527,15 +483,15 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
|
||||
final boolean fromUser) {
|
||||
if (fromUser) {
|
||||
final String seekTime = Localization.getDurationString(progress / 1000);
|
||||
progressCurrentTime.setText(seekTime);
|
||||
seekDisplay.setText(seekTime);
|
||||
queueControlBinding.currentTime.setText(seekTime);
|
||||
queueControlBinding.seekDisplay.setText(seekTime);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStartTrackingTouch(final SeekBar seekBar) {
|
||||
seeking = true;
|
||||
seekDisplay.setVisibility(View.VISIBLE);
|
||||
queueControlBinding.seekDisplay.setVisibility(View.VISIBLE);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -543,7 +499,7 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
|
||||
if (player != null) {
|
||||
player.seekTo(seekBar.getProgress());
|
||||
}
|
||||
seekDisplay.setVisibility(View.GONE);
|
||||
queueControlBinding.seekDisplay.setVisibility(View.GONE);
|
||||
seeking = false;
|
||||
}
|
||||
|
||||
@@ -601,45 +557,46 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
|
||||
public void onProgressUpdate(final int currentProgress, final int duration,
|
||||
final int bufferPercent) {
|
||||
// Set buffer progress
|
||||
progressSeekBar.setSecondaryProgress((int) (progressSeekBar.getMax()
|
||||
queueControlBinding.seekBar.setSecondaryProgress((int) (queueControlBinding.seekBar.getMax()
|
||||
* ((float) bufferPercent / 100)));
|
||||
|
||||
// Set Duration
|
||||
progressSeekBar.setMax(duration);
|
||||
progressEndTime.setText(Localization.getDurationString(duration / 1000));
|
||||
queueControlBinding.seekBar.setMax(duration);
|
||||
queueControlBinding.endTime.setText(Localization.getDurationString(duration / 1000));
|
||||
|
||||
// Set current time if not seeking
|
||||
if (!seeking) {
|
||||
progressSeekBar.setProgress(currentProgress);
|
||||
progressCurrentTime.setText(Localization.getDurationString(currentProgress / 1000));
|
||||
queueControlBinding.seekBar.setProgress(currentProgress);
|
||||
queueControlBinding.currentTime.setText(Localization
|
||||
.getDurationString(currentProgress / 1000));
|
||||
}
|
||||
|
||||
if (player != null) {
|
||||
progressLiveSync.setClickable(!player.isLiveEdge());
|
||||
queueControlBinding.liveSync.setClickable(!player.isLiveEdge());
|
||||
}
|
||||
|
||||
// this will make shure progressCurrentTime has the same width as progressEndTime
|
||||
final ViewGroup.LayoutParams endTimeParams = progressEndTime.getLayoutParams();
|
||||
final ViewGroup.LayoutParams currentTimeParams = progressCurrentTime.getLayoutParams();
|
||||
currentTimeParams.width = progressEndTime.getWidth();
|
||||
progressCurrentTime.setLayoutParams(currentTimeParams);
|
||||
// this will make sure progressCurrentTime has the same width as progressEndTime
|
||||
final ViewGroup.LayoutParams currentTimeParams =
|
||||
queueControlBinding.currentTime.getLayoutParams();
|
||||
currentTimeParams.width = queueControlBinding.endTime.getWidth();
|
||||
queueControlBinding.currentTime.setLayoutParams(currentTimeParams);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMetadataUpdate(final StreamInfo info, final PlayQueue queue) {
|
||||
if (info != null) {
|
||||
metadataTitle.setText(info.getName());
|
||||
metadataArtist.setText(info.getUploaderName());
|
||||
queueControlBinding.songName.setText(info.getName());
|
||||
queueControlBinding.artistName.setText(info.getUploaderName());
|
||||
|
||||
progressEndTime.setVisibility(View.GONE);
|
||||
progressLiveSync.setVisibility(View.GONE);
|
||||
queueControlBinding.endTime.setVisibility(View.GONE);
|
||||
queueControlBinding.liveSync.setVisibility(View.GONE);
|
||||
switch (info.getStreamType()) {
|
||||
case LIVE_STREAM:
|
||||
case AUDIO_LIVE_STREAM:
|
||||
progressLiveSync.setVisibility(View.VISIBLE);
|
||||
queueControlBinding.liveSync.setVisibility(View.VISIBLE);
|
||||
break;
|
||||
default:
|
||||
progressEndTime.setVisibility(View.VISIBLE);
|
||||
queueControlBinding.endTime.setVisibility(View.VISIBLE);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -660,13 +617,16 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
|
||||
private void onStateChanged(final int state) {
|
||||
switch (state) {
|
||||
case BasePlayer.STATE_PAUSED:
|
||||
playPauseButton.setImageResource(R.drawable.ic_play_arrow_white_24dp);
|
||||
queueControlBinding.controlPlayPause
|
||||
.setImageResource(R.drawable.ic_play_arrow_white_24dp);
|
||||
break;
|
||||
case BasePlayer.STATE_PLAYING:
|
||||
playPauseButton.setImageResource(R.drawable.ic_pause_white_24dp);
|
||||
queueControlBinding.controlPlayPause
|
||||
.setImageResource(R.drawable.ic_pause_white_24dp);
|
||||
break;
|
||||
case BasePlayer.STATE_COMPLETED:
|
||||
playPauseButton.setImageResource(R.drawable.ic_replay_white_24dp);
|
||||
queueControlBinding.controlPlayPause
|
||||
.setImageResource(R.drawable.ic_replay_white_24dp);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
@@ -676,14 +636,14 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
|
||||
case BasePlayer.STATE_PAUSED:
|
||||
case BasePlayer.STATE_PLAYING:
|
||||
case BasePlayer.STATE_COMPLETED:
|
||||
playPauseButton.setClickable(true);
|
||||
playPauseButton.setVisibility(View.VISIBLE);
|
||||
progressBar.setVisibility(View.GONE);
|
||||
queueControlBinding.controlPlayPause.setClickable(true);
|
||||
queueControlBinding.controlPlayPause.setVisibility(View.VISIBLE);
|
||||
queueControlBinding.controlProgressBar.setVisibility(View.GONE);
|
||||
break;
|
||||
default:
|
||||
playPauseButton.setClickable(false);
|
||||
playPauseButton.setVisibility(View.INVISIBLE);
|
||||
progressBar.setVisibility(View.VISIBLE);
|
||||
queueControlBinding.controlPlayPause.setClickable(false);
|
||||
queueControlBinding.controlPlayPause.setVisibility(View.INVISIBLE);
|
||||
queueControlBinding.controlProgressBar.setVisibility(View.VISIBLE);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -691,18 +651,21 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
|
||||
private void onPlayModeChanged(final int repeatMode, final boolean shuffled) {
|
||||
switch (repeatMode) {
|
||||
case Player.REPEAT_MODE_OFF:
|
||||
repeatButton.setImageResource(R.drawable.exo_controls_repeat_off);
|
||||
queueControlBinding.controlRepeat
|
||||
.setImageResource(R.drawable.exo_controls_repeat_off);
|
||||
break;
|
||||
case Player.REPEAT_MODE_ONE:
|
||||
repeatButton.setImageResource(R.drawable.exo_controls_repeat_one);
|
||||
queueControlBinding.controlRepeat
|
||||
.setImageResource(R.drawable.exo_controls_repeat_one);
|
||||
break;
|
||||
case Player.REPEAT_MODE_ALL:
|
||||
repeatButton.setImageResource(R.drawable.exo_controls_repeat_all);
|
||||
queueControlBinding.controlRepeat
|
||||
.setImageResource(R.drawable.exo_controls_repeat_all);
|
||||
break;
|
||||
}
|
||||
|
||||
final int shuffleAlpha = shuffled ? 255 : 77;
|
||||
shuffleButton.setImageAlpha(shuffleAlpha);
|
||||
queueControlBinding.controlShuffle.setImageAlpha(shuffleAlpha);
|
||||
}
|
||||
|
||||
private void onPlaybackParameterChanged(final PlaybackParameters parameters) {
|
||||
@@ -715,12 +678,13 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
|
||||
}
|
||||
|
||||
private void onMaybePlaybackAdapterChanged() {
|
||||
if (itemsList == null || player == null) {
|
||||
if (player == null) {
|
||||
return;
|
||||
}
|
||||
final PlayQueueAdapter maybeNewAdapter = player.getPlayQueueAdapter();
|
||||
if (maybeNewAdapter != null && itemsList.getAdapter() != maybeNewAdapter) {
|
||||
itemsList.setAdapter(maybeNewAdapter);
|
||||
if (maybeNewAdapter != null
|
||||
&& queueControlBinding.playQueue.getAdapter() != maybeNewAdapter) {
|
||||
queueControlBinding.playQueue.setAdapter(maybeNewAdapter);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -734,7 +698,8 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
|
||||
|
||||
//2) Icon change accordingly to current App Theme
|
||||
// using rootView.getContext() because getApplicationContext() didn't work
|
||||
item.setIcon(ThemeHelper.resolveResourceIdFromAttr(rootView.getContext(),
|
||||
final Context context = queueControlBinding.getRoot().getContext();
|
||||
item.setIcon(ThemeHelper.resolveResourceIdFromAttr(context,
|
||||
player.isMuted()
|
||||
? R.attr.ic_volume_off
|
||||
: R.attr.ic_volume_up));
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user