mirror of
https://github.com/TeamNewPipe/NewPipe
synced 2026-01-15 05:58:02 +00:00
Compare commits
467 Commits
v0.27.2
...
player-cla
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
31ade7cd30 | ||
|
|
4e1b0e0555 | ||
|
|
0aa71a58ed | ||
|
|
7585cc2e73 | ||
|
|
803fd52859 | ||
|
|
ab8a9ae11c | ||
|
|
83486402df | ||
|
|
a5813f256a | ||
|
|
0a885492b6 | ||
|
|
731efc2124 | ||
|
|
a8da9946d1 | ||
|
|
3d069cdf5b | ||
|
|
eccedc0ab0 | ||
|
|
7cecda5713 | ||
|
|
d9dccfa8af | ||
|
|
81b4e3f970 | ||
|
|
ef068e1eca | ||
|
|
8407b5aefd | ||
|
|
b6aa07545a | ||
|
|
1dcb1953ba | ||
|
|
862a8e8f26 | ||
|
|
88395fa852 | ||
|
|
8d679626f0 | ||
|
|
e7f3750f5e | ||
|
|
48e826e912 | ||
|
|
088cb8353e | ||
|
|
5ca544bc42 | ||
|
|
aa1b7f8584 | ||
|
|
ce16c6df5f | ||
|
|
1d94fd1582 | ||
|
|
c9542ad6fd | ||
|
|
7615f79aca | ||
|
|
276bf390b2 | ||
|
|
f39eda086f | ||
|
|
756327da39 | ||
|
|
5840d3a437 | ||
|
|
47299c9184 | ||
|
|
6486f2de56 | ||
|
|
e1dedd45ed | ||
|
|
912f07a1dd | ||
|
|
205466c56a | ||
|
|
7f10312d0a | ||
|
|
63be3220e7 | ||
|
|
536b78f2e6 | ||
|
|
6d6b73ef73 | ||
|
|
196c27792b | ||
|
|
b3789315ad | ||
|
|
c7bf498c04 | ||
|
|
35abb99dac | ||
|
|
70416e73f3 | ||
|
|
a0b76c3385 | ||
|
|
c232193a46 | ||
|
|
f289bea6b3 | ||
|
|
48b200868a | ||
|
|
54bf7f0ced | ||
|
|
980a35a708 | ||
|
|
da106e2361 | ||
|
|
3532ac96b4 | ||
|
|
87693a2ad1 | ||
|
|
d321e57620 | ||
|
|
fb4a65a14a | ||
|
|
3047704e1c | ||
|
|
3dcfdaf510 | ||
|
|
2ceb70236e | ||
|
|
be097f26c8 | ||
|
|
098f60d593 | ||
|
|
eb0568044a | ||
|
|
f3b3d5c3e7 | ||
|
|
b888dc72cf | ||
|
|
599d86151a | ||
|
|
587df093ea | ||
|
|
8830e87242 | ||
|
|
f96b8f7b2a | ||
|
|
c28478ae53 | ||
|
|
10110397fd | ||
|
|
d81244e77c | ||
|
|
ea20ca9e72 | ||
|
|
f0c89494dd | ||
|
|
0fd2d4fed6 | ||
|
|
c1bdffd917 | ||
|
|
3c7b026d7d | ||
|
|
998d84de6c | ||
|
|
76a02d5858 | ||
|
|
24bb71a23f | ||
|
|
49b71942ad | ||
|
|
c9ec257a5e | ||
|
|
b1f995a78c | ||
|
|
acac50a1d1 | ||
|
|
c6b87cd316 | ||
|
|
94d4c21cc7 | ||
|
|
a7a7dc5363 | ||
|
|
126f4b0e30 | ||
|
|
6558794d26 | ||
|
|
1d12874937 | ||
|
|
1d98518bfa | ||
|
|
e5458bcb14 | ||
|
|
dc62d211f5 | ||
|
|
ec6612dd71 | ||
|
|
064e1d39c7 | ||
|
|
4c88a193bd | ||
|
|
3fcac10e7f | ||
|
|
6cedd117fe | ||
|
|
5eabcb52b5 | ||
|
|
690b40d0c4 | ||
|
|
9bb2c0b484 | ||
|
|
1e08cc8c8f | ||
|
|
7d17468266 | ||
|
|
5819546ea9 | ||
|
|
cfb6e114d6 | ||
|
|
b764ad33c4 | ||
|
|
430b4eb916 | ||
|
|
2339f51ad4 | ||
|
|
99aae7eb28 | ||
|
|
c6e1721884 | ||
|
|
94684fe380 | ||
|
|
398a2f55ce | ||
|
|
1f7b3b5b06 | ||
|
|
909ed616c4 | ||
|
|
dd223af28d | ||
|
|
dbee8d8128 | ||
|
|
b62a09b5b3 | ||
|
|
87317c6faf | ||
|
|
53b599b042 | ||
|
|
21df24abfd | ||
|
|
ca4592a935 | ||
|
|
3fc487310b | ||
|
|
056809cb0d | ||
|
|
a60bb3e7af | ||
|
|
ecd3f6c2ee | ||
|
|
70ff47b810 | ||
|
|
b8e050f6c4 | ||
|
|
46d0bc1004 | ||
|
|
e7fe84f2c7 | ||
|
|
2b183a0576 | ||
|
|
f856bd9306 | ||
|
|
0066b322e1 | ||
|
|
3bdae81c0a | ||
|
|
6010c4ea7f | ||
|
|
690b3410e9 | ||
|
|
ba86ce137b | ||
|
|
410c01547c | ||
|
|
fd99c5e461 | ||
|
|
407d2d768d | ||
|
|
47263f5254 | ||
|
|
01bf855015 | ||
|
|
ebf3008729 | ||
|
|
33ecfb757e | ||
|
|
b109e4d3cc | ||
|
|
137ade24ff | ||
|
|
ffe26d882b | ||
|
|
83f8141fe7 | ||
|
|
b78e0b2da8 | ||
|
|
9253640fae | ||
|
|
3e6e980362 | ||
|
|
8b5aa5cd9b | ||
|
|
58393ad4ef | ||
|
|
977f7e28b5 | ||
|
|
99e77249de | ||
|
|
1890fbb19a | ||
|
|
a955408053 | ||
|
|
86203d6800 | ||
|
|
edd19641ac | ||
|
|
65749cbac0 | ||
|
|
658ddfc921 | ||
|
|
efb3aa530d | ||
|
|
f7d0fd545d | ||
|
|
27e6be792f | ||
|
|
ce919215fb | ||
|
|
6a4aaba431 | ||
|
|
83d93e16e7 | ||
|
|
8d15a141b1 | ||
|
|
a78bed700a | ||
|
|
ef3c76645f | ||
|
|
d4ed18bf08 | ||
|
|
3fc0147f47 | ||
|
|
fbafdeb2ca | ||
|
|
c6b05c6094 | ||
|
|
240a2fe36b | ||
|
|
de46e3abb3 | ||
|
|
70748fa0bc | ||
|
|
781040efaa | ||
|
|
3847b32c11 | ||
|
|
9054575f6c | ||
|
|
0dca92dd59 | ||
|
|
b19cd00dba | ||
|
|
88d8d90bbd | ||
|
|
c569f08a32 | ||
|
|
246fc034c1 | ||
|
|
1547b50b4e | ||
|
|
3f7ef49979 | ||
|
|
52942ffd30 | ||
|
|
e4b0245530 | ||
|
|
c6b8bcf0f4 | ||
|
|
dab0148a78 | ||
|
|
c0c08a4f63 | ||
|
|
e31a8ad7a2 | ||
|
|
b21981a9c7 | ||
|
|
aaf337421d | ||
|
|
79a0edacd7 | ||
|
|
d56eef6ece | ||
|
|
72f054a4fa | ||
|
|
172c3c92ac | ||
|
|
137ef3fee4 | ||
|
|
e49156fb11 | ||
|
|
de5d45849f | ||
|
|
a25034b898 | ||
|
|
ae9e82b2c1 | ||
|
|
a70b38a8e5 | ||
|
|
08f3dba42c | ||
|
|
f9711a3402 | ||
|
|
df941670a8 | ||
|
|
57e66b17c6 | ||
|
|
d298a12533 | ||
|
|
a79bc3db14 | ||
|
|
661e6155c1 | ||
|
|
12558172d1 | ||
|
|
dc3f55674f | ||
|
|
acf2e88cb3 | ||
|
|
726c12e934 | ||
|
|
0cff3a6ecd | ||
|
|
33b96d238a | ||
|
|
213f49f5c4 | ||
|
|
9b78e49e45 | ||
|
|
16c79c8219 | ||
|
|
e6eea8f851 | ||
|
|
4e55f1bee6 | ||
|
|
cff3834fde | ||
|
|
c8b01a06b0 | ||
|
|
414b1a8344 | ||
|
|
404d9f3fac | ||
|
|
55e4014036 | ||
|
|
1cd5563b27 | ||
|
|
1abced992b | ||
|
|
46b9243661 | ||
|
|
ad72b2cb31 | ||
|
|
8b79d0ee29 | ||
|
|
295f719b77 | ||
|
|
b584353f4d | ||
|
|
d73314b4dd | ||
|
|
9f4a33c7a8 | ||
|
|
3a9540b042 | ||
|
|
14081505cd | ||
|
|
ca855cbca0 | ||
|
|
6a98b1dac7 | ||
|
|
ebd4880188 | ||
|
|
ffcba175ff | ||
|
|
c7848e5e86 | ||
|
|
6d686b93cb | ||
|
|
2cc38f59d3 | ||
|
|
8bf24e6b14 | ||
|
|
10e7a5cf9c | ||
|
|
9f2f219613 | ||
|
|
841471bf85 | ||
|
|
06d25b0310 | ||
|
|
3c8d81a3c2 | ||
|
|
cf870add49 | ||
|
|
a962e6d633 | ||
|
|
970ef9357b | ||
|
|
4ba961fe7a | ||
|
|
e6c03bf4ac | ||
|
|
1f39523429 | ||
|
|
b43031fb99 | ||
|
|
986cd52da0 | ||
|
|
7d4a2836fc | ||
|
|
6ea715a18d | ||
|
|
a56debfce6 | ||
|
|
226b6de34f | ||
|
|
bcd4579187 | ||
|
|
6fe417abc6 | ||
|
|
a229ab68d5 | ||
|
|
544b30290d | ||
|
|
cb300724da | ||
|
|
0ac5a269ff | ||
|
|
13585ca0be | ||
|
|
62ab9bd740 | ||
|
|
fdf36cbad6 | ||
|
|
aea2b7c7f3 | ||
|
|
37d1c784fa | ||
|
|
cea149f852 | ||
|
|
a92a28517e | ||
|
|
800961c3d7 | ||
|
|
9d8a79b0bd | ||
|
|
0009613608 | ||
|
|
7c18d4dd01 | ||
|
|
fe1c538f9c | ||
|
|
ef56dea817 | ||
|
|
23b3835af0 | ||
|
|
412e1d602a | ||
|
|
802a094154 | ||
|
|
e6b1341246 | ||
|
|
36ede243e3 | ||
|
|
bac9f7eebf | ||
|
|
8ada566bf1 | ||
|
|
5bd4ed77df | ||
|
|
97652ac015 | ||
|
|
6dd24033a4 | ||
|
|
4de3ef20be | ||
|
|
702f74291d | ||
|
|
d8759993a9 | ||
|
|
7787eafd3a | ||
|
|
f08e07873a | ||
|
|
1193b02ca1 | ||
|
|
c0b36b86b9 | ||
|
|
66ec596f67 | ||
|
|
90404a23ce | ||
|
|
64ad05d813 | ||
|
|
734b6e2b67 | ||
|
|
94f992a2e2 | ||
|
|
c8550695aa | ||
|
|
cdac50bab3 | ||
|
|
23961548c0 | ||
|
|
ba1e9c8e1b | ||
|
|
f4baf4628e | ||
|
|
05a87da827 | ||
|
|
fef40014a0 | ||
|
|
1996c1176c | ||
|
|
0190bcee25 | ||
|
|
1ed4928f40 | ||
|
|
63bc982cb2 | ||
|
|
3a286515f2 | ||
|
|
2e96b65fda | ||
|
|
2482615460 | ||
|
|
9384365061 | ||
|
|
b1d4b66aa6 | ||
|
|
ea0da5fdbd | ||
|
|
d80b6a759c | ||
|
|
8106ba68b5 | ||
|
|
ee15a72e4f | ||
|
|
4f4136c6e9 | ||
|
|
b399030e19 | ||
|
|
2eb256799d | ||
|
|
0cf4732d8a | ||
|
|
53edd054aa | ||
|
|
678f0a786a | ||
|
|
b14f65804d | ||
|
|
781a69d60d | ||
|
|
eb9f300e60 | ||
|
|
063568b620 | ||
|
|
0991461d04 | ||
|
|
49bcf2c41b | ||
|
|
c00c6c460c | ||
|
|
4c4fe3f511 | ||
|
|
db485c3d77 | ||
|
|
c0388d948b | ||
|
|
43bbddcc26 | ||
|
|
6471b64ab6 | ||
|
|
b9fcf0dff8 | ||
|
|
3177ca6e8a | ||
|
|
5017f4f05a | ||
|
|
035c394cf6 | ||
|
|
823d4a041f | ||
|
|
62d4044d6c | ||
|
|
3785404618 | ||
|
|
c98ad62163 | ||
|
|
4cac111b66 | ||
|
|
941b8eb194 | ||
|
|
b1add13bfd | ||
|
|
5fffee2c7d | ||
|
|
f9340ae604 | ||
|
|
d3a6991fd4 | ||
|
|
b0bfd4a807 | ||
|
|
3641698379 | ||
|
|
2836191fb3 | ||
|
|
0df6c7fc2c | ||
|
|
b1ebd3ecd9 | ||
|
|
4758244cf5 | ||
|
|
294b9cf347 | ||
|
|
fad3120b00 | ||
|
|
6d05af484e | ||
|
|
38c823a042 | ||
|
|
e082bca5e0 | ||
|
|
f9dae9078e | ||
|
|
e955beeef1 | ||
|
|
eaac7f3f85 | ||
|
|
ea414f57d4 | ||
|
|
f984b26626 | ||
|
|
edab9a6a1f | ||
|
|
4740e3be86 | ||
|
|
e639b02fed | ||
|
|
ac1ca1412d | ||
|
|
d131d3399a | ||
|
|
1009dc4d4e | ||
|
|
42cb914616 | ||
|
|
e72da94eb1 | ||
|
|
c5d94a5b60 | ||
|
|
02c5f2607a | ||
|
|
369a46f8fe | ||
|
|
909d214002 | ||
|
|
5e7e14ee4d | ||
|
|
b092fe2c76 | ||
|
|
b9dd7078ad | ||
|
|
93310955f2 | ||
|
|
9c52e039ee | ||
|
|
be037e0756 | ||
|
|
5bfb0449cf | ||
|
|
0ec81c9e52 | ||
|
|
5841eaa6d7 | ||
|
|
e92ba8f5d1 | ||
|
|
1908e18dc4 | ||
|
|
e30d5e4305 | ||
|
|
11bb2495ba | ||
|
|
341cc37ce7 | ||
|
|
1620668966 | ||
|
|
56c80ce6dd | ||
|
|
8ce9a7e43c | ||
|
|
e05d97732e | ||
|
|
644a345b55 | ||
|
|
bda961a04c | ||
|
|
ba2efded76 | ||
|
|
b05b98ca61 | ||
|
|
7a7f81ac7f | ||
|
|
6e6c171dd7 | ||
|
|
8a41c8cf66 | ||
|
|
05271d95a9 | ||
|
|
9d04a73c85 | ||
|
|
d336f4cef2 | ||
|
|
51ee2f8d1e | ||
|
|
0e8303f13a | ||
|
|
4ec7532126 | ||
|
|
da83646303 | ||
|
|
72e9f7f9cf | ||
|
|
ad6b676c81 | ||
|
|
07c63f794e | ||
|
|
26dd86e967 | ||
|
|
5062d38b65 | ||
|
|
82b492c050 | ||
|
|
73e3a69aaf | ||
|
|
348a79f91d | ||
|
|
5e5e77f746 | ||
|
|
c4ada7ff6e | ||
|
|
39d0691c7e | ||
|
|
71361de8ee | ||
|
|
8aa2590fd3 | ||
|
|
e3b7bf467e | ||
|
|
f74402bc94 | ||
|
|
4d3b4a7b20 | ||
|
|
e6302cc868 | ||
|
|
844b4edf48 | ||
|
|
92a7f22d3c | ||
|
|
03167a1e9c | ||
|
|
1f309854bc | ||
|
|
2ac0d1f13a | ||
|
|
4eeea7b787 | ||
|
|
e64c01d2da | ||
|
|
0c7a91f852 | ||
|
|
a2d93b389c | ||
|
|
c795214abb | ||
|
|
71822a47a5 | ||
|
|
e1bf67c676 | ||
|
|
8583c48264 | ||
|
|
2a3d133bcf | ||
|
|
3e3d1fd265 | ||
|
|
8645618f1a | ||
|
|
e48ce5a103 | ||
|
|
c02ceda22f | ||
|
|
46139340fe | ||
|
|
d479f29e9b | ||
|
|
1af798b04b | ||
|
|
7204407690 | ||
|
|
e37336eef2 | ||
|
|
cf21b9feaf | ||
|
|
b74cab6642 | ||
|
|
8267d325ed | ||
|
|
879d7a24f0 | ||
|
|
9e4ac2eacb | ||
|
|
d9d6fff48f | ||
|
|
f4fb960c62 |
12
.github/CONTRIBUTING.md
vendored
12
.github/CONTRIBUTING.md
vendored
@@ -6,7 +6,7 @@ NewPipe contribution guidelines
|
||||
## Crash reporting
|
||||
|
||||
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.
|
||||
This way all the data needed for debugging is included in your bug report for GitHub.
|
||||
You'll see *exactly* what is sent, be able to add **your comments**, and then send it.
|
||||
|
||||
## Issue reporting/feature requests
|
||||
@@ -42,10 +42,6 @@ You'll see *exactly* what is sent, be able to add **your comments**, and then se
|
||||
* Create PRs that cover only **one specific issue/solution/bug**. Do not create PRs that are huge monoliths and could have been split into multiple independent contributions.
|
||||
* NewPipe uses [NewPipeExtractor](https://github.com/TeamNewPipe/NewPipeExtractor) to fetch data from services. If you need to change something there, you must test your changes in NewPipe. Telling NewPipe to use your extractor version can be accomplished by editing the `app/build.gradle` file: the comments under the "NewPipe libraries" section of `dependencies` will help you out.
|
||||
|
||||
### Kotlin in NewPipe
|
||||
* NewPipe will remain mostly Java for time being
|
||||
* Contributions containing a simple conversion from Java to Kotlin should be avoided. Conversions to Kotlin should only be done if Kotlin actually brings improvements like bug fixes or better performance which are not, or only with much more effort, implementable in Java. The core team sees Java as an easier to learn and generally well adopted programming language.
|
||||
|
||||
### Creating a Pull Request (PR)
|
||||
|
||||
* Make changes on a **separate branch** with a meaningful name, not on the _master_ branch or the _dev_ branch. This is commonly known as *feature branch workflow*. You may then send your changes as a pull request (PR) on GitHub.
|
||||
@@ -83,6 +79,6 @@ The [ktlint](https://github.com/pinterest/ktlint) plugin does the same job as ch
|
||||
|
||||
## Communication
|
||||
|
||||
* The #newpipe channel on Libera Chat (`ircs://irc.libera.chat:6697/newpipe`) has the core team and other developers in it. [Click here for webchat](https://web.libera.chat/#newpipe)!
|
||||
* You can also use a Matrix account to join the NewPipe channel at [#newpipe:libera.chat](https://matrix.to/#/#newpipe:libera.chat). Some convenient clients, available both for phone and desktop, are listed at that link.
|
||||
* You can post your suggestions, changes, ideas etc. on either GitHub or IRC.
|
||||
* You can use a Matrix account to join the NewPipe channel at [#newpipe:matrix.newpipe-ev.de](https://matrix.to/#/#newpipe:matrix.newpipe-ev.de). Some convenient clients, available both for phone and desktop, are listed at that link.
|
||||
* Alternatively, the #newpipe channel on Libera Chat (`ircs://irc.libera.chat:6697/newpipe`) can also be joined, as it is bridged to the Matrix room. [Click here for webchat](https://web.libera.chat/#newpipe)!
|
||||
* You can post your suggestions, changes, ideas etc. on either GitHub or Matrix (including via IRC).
|
||||
|
||||
6
.github/ISSUE_TEMPLATE/config.yml
vendored
6
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -3,9 +3,9 @@ contact_links:
|
||||
- name: ❓ Question
|
||||
url: https://github.com/TeamNewPipe/NewPipe/discussions/new?category=questions
|
||||
about: Ask about anything NewPipe-related
|
||||
- name: 💬 Matrix
|
||||
url: https://matrix.to/#/#newpipe:matrix.newpipe-ev.de
|
||||
about: Chat with us via Matrix for quick Q/A
|
||||
- name: 💬 IRC
|
||||
url: https://web.libera.chat/#newpipe
|
||||
about: Chat with us via IRC for quick Q/A
|
||||
- name: 💬 Matrix
|
||||
url: https://matrix.to/#/#newpipe:libera.chat
|
||||
about: Chat with us via Matrix for quick Q/A
|
||||
|
||||
38
.github/workflows/build-release-apk.yml
vendored
Normal file
38
.github/workflows/build-release-apk.yml
vendored
Normal file
@@ -0,0 +1,38 @@
|
||||
name: "Build unsigned release APK on master"
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
release:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: 'master'
|
||||
|
||||
- uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: 'temurin'
|
||||
java-version: '21'
|
||||
cache: 'gradle'
|
||||
|
||||
- name: "Build release APK"
|
||||
run: ./gradlew assembleRelease --stacktrace
|
||||
|
||||
- name: "Rename APK"
|
||||
run: |
|
||||
VERSION_NAME="$(jq -r ".elements[0].versionName" "app/build/outputs/apk/release/output-metadata.json")"
|
||||
echo "Version name: $VERSION_NAME" >> "$GITHUB_STEP_SUMMARY"
|
||||
echo '```json' >> "$GITHUB_STEP_SUMMARY"
|
||||
cat "app/build/outputs/apk/release/output-metadata.json" >> "$GITHUB_STEP_SUMMARY"
|
||||
echo >> "$GITHUB_STEP_SUMMARY"
|
||||
echo '```' >> "$GITHUB_STEP_SUMMARY"
|
||||
# assume there is only one APK in that folder
|
||||
mv app/build/outputs/apk/release/*.apk "app/build/outputs/apk/release/NewPipe_v$VERSION_NAME.apk"
|
||||
|
||||
- name: "Upload APK"
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: app
|
||||
path: app/build/outputs/apk/release/*.apk
|
||||
22
.github/workflows/ci.yml
vendored
22
.github/workflows/ci.yml
vendored
@@ -6,6 +6,7 @@ on:
|
||||
branches:
|
||||
- dev
|
||||
- master
|
||||
- refactor
|
||||
- release**
|
||||
paths-ignore:
|
||||
- 'README.md'
|
||||
@@ -46,10 +47,10 @@ jobs:
|
||||
BRANCH: ${{ github.head_ref }}
|
||||
run: git checkout -B "$BRANCH"
|
||||
|
||||
- name: set up JDK 17
|
||||
- name: set up JDK
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
java-version: 17
|
||||
java-version: 21
|
||||
distribution: "temurin"
|
||||
cache: 'gradle'
|
||||
|
||||
@@ -63,8 +64,7 @@ jobs:
|
||||
path: app/build/outputs/apk/debug/*.apk
|
||||
|
||||
test-android:
|
||||
# macos has hardware acceleration. See android-emulator-runner action
|
||||
runs-on: macos-latest
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 20
|
||||
strategy:
|
||||
matrix:
|
||||
@@ -82,10 +82,16 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: set up JDK 17
|
||||
- name: Enable KVM
|
||||
run: |
|
||||
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
|
||||
sudo udevadm control --reload-rules
|
||||
sudo udevadm trigger --name-match=kvm
|
||||
|
||||
- name: set up JDK
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
java-version: 17
|
||||
java-version: 21
|
||||
distribution: "temurin"
|
||||
cache: 'gradle'
|
||||
|
||||
@@ -115,10 +121,10 @@ jobs:
|
||||
with:
|
||||
fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis
|
||||
|
||||
- name: Set up JDK 17
|
||||
- name: Set up JDK
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
java-version: 17
|
||||
java-version: 21
|
||||
distribution: "temurin"
|
||||
cache: 'gradle'
|
||||
|
||||
|
||||
8
.github/workflows/image-minimizer.js
vendored
8
.github/workflows/image-minimizer.js
vendored
@@ -32,12 +32,12 @@ module.exports = async ({github, context}) => {
|
||||
}
|
||||
|
||||
// Regex for finding images (simple variant) 
|
||||
const REGEX_USER_CONTENT_IMAGE_LOOKUP = /\!\[(.*)\]\((https:\/\/[-a-z0-9]+\.githubusercontent\.com\/\d+\/[-0-9a-f]{32,512}\.(jpg|gif|png))\)/gm;
|
||||
const REGEX_ASSETS_IMAGE_LOCKUP = /\!\[(.*)\]\((https:\/\/github\.com\/[-\w\d]+\/[-\w\d]+\/assets\/\d+\/[\-0-9a-f]{32,512})\)/gm;
|
||||
const REGEX_USER_CONTENT_IMAGE_LOOKUP = /\!\[([^\]]*)\]\((https:\/\/[-a-z0-9]+\.githubusercontent\.com\/\d+\/[-0-9a-f]{32,512}\.(jpg|gif|png))\)/gm;
|
||||
const REGEX_ASSETS_IMAGE_LOOKUP = /\!\[([^\]]*)\]\((https:\/\/github\.com\/(?:user-attachments\/assets|[-\w\d]+\/[-\w\d]+\/assets\/\d+)\/[\-0-9a-f]{32,512})\)/gm;
|
||||
|
||||
// Check if we found something
|
||||
let foundSimpleImages = REGEX_USER_CONTENT_IMAGE_LOOKUP.test(initialBody)
|
||||
|| REGEX_ASSETS_IMAGE_LOCKUP.test(initialBody);
|
||||
|| REGEX_ASSETS_IMAGE_LOOKUP.test(initialBody);
|
||||
if (!foundSimpleImages) {
|
||||
console.log('Found no simple images to process');
|
||||
return;
|
||||
@@ -52,7 +52,7 @@ module.exports = async ({github, context}) => {
|
||||
|
||||
// Try to find and replace the images with minimized ones
|
||||
let newBody = await replaceAsync(initialBody, REGEX_USER_CONTENT_IMAGE_LOOKUP, minimizeAsync);
|
||||
newBody = await replaceAsync(newBody, REGEX_ASSETS_IMAGE_LOCKUP, minimizeAsync);
|
||||
newBody = await replaceAsync(newBody, REGEX_ASSETS_IMAGE_LOOKUP, minimizeAsync);
|
||||
|
||||
if (!wasMatchModified) {
|
||||
console.log('Nothing was modified. Skipping update');
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -10,6 +10,7 @@ captures/
|
||||
*.class
|
||||
app/debug/
|
||||
app/release/
|
||||
.kotlin/
|
||||
|
||||
# vscode / eclipse files
|
||||
*.classpath
|
||||
|
||||
21
.idea/icon.svg
generated
Normal file
21
.idea/icon.svg
generated
Normal file
@@ -0,0 +1,21 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" x="0px" y="0px"
|
||||
viewBox="0 0 100 100" style="enable-background:new 0 0 100 100;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#CD201F;}
|
||||
.st1{fill:#FFFFFF;}
|
||||
</style>
|
||||
<g id="Alapkör">
|
||||
<circle id="XMLID_23_" class="st0" cx="50" cy="50" r="50"/>
|
||||
</g>
|
||||
<g id="Elemek">
|
||||
<path id="XMLID_19_" class="st1" d="M47,28.2c-9-5.3-15.3-9-15.3-9v61.7c0,0,30.4-18,52.3-30.9C72.1,43,57.7,34.5,47,28.2z"/>
|
||||
</g>
|
||||
<g id="Fedő">
|
||||
<path id="XMLID_5_" class="st0" d="M48.4,40.1c-4.1-2.4-7-4.1-7-4.1V64c0,0,13.9-8.2,23.8-14C59.8,46.8,53.3,42.9,48.4,40.1z"/>
|
||||
<rect id="XMLID_4_" x="41.4" y="55.6" class="st0" width="6.2" height="21"/>
|
||||
</g>
|
||||
<g id="Vonalak">
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 850 B |
18
README.md
18
README.md
@@ -20,7 +20,7 @@
|
||||
<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 document in other languages: [Deutsch](doc/README.de.md), [English](README.md), [Español](doc/README.es.md), [Français](doc/README.fr.md), [हिन्दी](doc/README.hi.md), [Italiano](doc/README.it.md), [한국어](doc/README.ko.md), [Português Brasil](doc/README.pt_BR.md), [Polski](doc/README.pl.md), [ਪੰਜਾਬੀ ](doc/README.pa.md), [日本語](doc/README.ja.md), [Română](doc/README.ro.md), [Soomaali](doc/README.so.md), [Türkçe](doc/README.tr.md), [正體中文](doc/README.zh_TW.md), [অসমীয়া](doc/README.asm.md), [Српски](doc/README.sr.md)*
|
||||
*Read this document in other languages: [Deutsch](doc/README.de.md), [English](README.md), [Español](doc/README.es.md), [Français](doc/README.fr.md), [हिन्दी](doc/README.hi.md), [Italiano](doc/README.it.md), [한국어](doc/README.ko.md), [Português Brasil](doc/README.pt_BR.md), [Polski](doc/README.pl.md), [ਪੰਜਾਬੀ ](doc/README.pa.md), [日本語](doc/README.ja.md), [Română](doc/README.ro.md), [Soomaali](doc/README.so.md), [Türkçe](doc/README.tr.md), [正體中文](doc/README.zh_TW.md), [অসমীয়া](doc/README.asm.md), [Српски](doc/README.sr.md), [العربية](README.ar.md)*
|
||||
|
||||
> [!warning]
|
||||
> <b>THIS APP IS IN BETA, SO YOU MAY ENCOUNTER BUGS. IF YOU DO, OPEN AN ISSUE IN OUR GITHUB REPOSITORY BY FILLING OUT THE ISSUE TEMPLATE.</b>
|
||||
@@ -96,7 +96,7 @@ Also, since they are free and open source software, neither the app nor the Extr
|
||||
## Installation and updates
|
||||
You can install NewPipe using one of the following methods:
|
||||
1. Add our custom repo to F-Droid and install it from there. The instructions are here: https://newpipe.net/FAQ/tutorials/install-add-fdroid-repo/
|
||||
2. Download the APK from [GitHub Releases](https://github.com/TeamNewPipe/NewPipe/releases) and install it.
|
||||
2. Download the APK from [GitHub Releases](https://github.com/TeamNewPipe/NewPipe/releases), [compare the signing key](#apk-info) and install it.
|
||||
3. Update via F-Droid. This is the slowest method of getting updates, as F-Droid must recognize changes, build the APK itself, sign it, and then push the update to users.
|
||||
4. Build a debug APK yourself. This is the fastest way to get new features on your device, but is much more complicated, so we recommend using one of the other methods.
|
||||
5. If you're interested in a specific feature or bugfix provided in a Pull Request in this repo, you can also download its APK from within the PR. Read the PR description for instructions. The great thing about PR-specific APKs is that they're installed side-by-side the official app, so you don't have to worry about losing your data or messing anything up.
|
||||
@@ -104,12 +104,20 @@ You can install NewPipe using one of the following methods:
|
||||
We recommend method 1 for most users. APKs installed using method 1 or 2 are compatible with each other (meaning that if you installed NewPipe using either method 1 or 2, you can also update NewPipe using the other), but not with those installed using method 3. This is due to the same signing key (ours) being used for 1 and 2, but a different signing key (F-Droid's) being used for 3. Building a debug APK using method 4 excludes a key entirely. Signing keys help ensure that a user isn't tricked into installing a malicious update to an app. When using method 5, each APK is signed with a different random key supplied by GitHub Actions, so you cannot even update it. You will have to backup and restore the app data each time you wish to use a new APK.
|
||||
|
||||
In the meanwhile, if you want to switch sources for some reason (e.g. NewPipe's core functionality breaks and F-Droid doesn't have the latest 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
|
||||
1. Back up your data via Settings > Backup and Restore > Export Database so you keep your history, subscriptions, and playlists
|
||||
2. Uninstall NewPipe
|
||||
3. Download the APK from the new source and install it
|
||||
4. Import the data from step 1 via Settings > Content > Import Database
|
||||
4. Import the data from step 1 via Settings > Backup and Restore > Import Database
|
||||
|
||||
<b>Note: when you're importing a database into the official app, always make sure that it is the one you exported _from_ the official app. If you import a database exported from an APK other than the official app, it may break things. Such an action is unsupported, and you should only do so when you're absolutely certain you know what you're doing.</b>
|
||||
> [!Note]
|
||||
> When you're importing a database into the official app, always make sure that it is the one you exported _from_ the official app. If you import a database exported from an APK other than the official app, it may break things. Such an action is unsupported, and you should only do so when you're absolutely certain you know what you're doing.
|
||||
|
||||
### APK Info
|
||||
|
||||
This is the SHA fingerprint of NewPipe's signing key to verify downloaded APKs which are signed by us. The fingerprint is also available on [NewPipe's website](https://newpipe.net#download). This is relevant for method 2.
|
||||
```
|
||||
CB:84:06:9B:D6:81:16:BA:FA:E5:EE:4E:E5:B0:8A:56:7A:A6:D8:98:40:4E:7C:B1:2F:9E:75:6D:F5:CF:5C:AB
|
||||
```
|
||||
|
||||
## Contribution
|
||||
Whether you have ideas, translations, design changes, code cleaning, or even major code changes, help is always welcome. The app gets better and better with each contribution, no matter how big or small! If you'd like to get involved, check our [contribution notes](.github/CONTRIBUTING.md).
|
||||
|
||||
232
app/build.gradle
232
app/build.gradle
@@ -1,14 +1,18 @@
|
||||
import com.android.tools.profgen.ArtProfileKt
|
||||
import com.android.tools.profgen.ArtProfileSerializer
|
||||
import com.android.tools.profgen.DexFile
|
||||
import com.mikepenz.aboutlibraries.plugin.DuplicateMode
|
||||
|
||||
plugins {
|
||||
id "com.android.application"
|
||||
id "kotlin-android"
|
||||
id "kotlin-kapt"
|
||||
id "kotlin-parcelize"
|
||||
id "checkstyle"
|
||||
id "org.sonarqube" version "4.0.0.2929"
|
||||
alias libs.plugins.android.application
|
||||
alias libs.plugins.kotlin.android
|
||||
alias libs.plugins.kotlin.compose
|
||||
alias libs.plugins.kotlin.kapt
|
||||
alias libs.plugins.kotlin.parcelize
|
||||
alias libs.plugins.checkstyle
|
||||
alias libs.plugins.sonarqube
|
||||
alias libs.plugins.hilt
|
||||
alias libs.plugins.aboutlibraries
|
||||
}
|
||||
|
||||
android {
|
||||
@@ -20,8 +24,15 @@ android {
|
||||
resValue "string", "app_name", "NewPipe"
|
||||
minSdk 21
|
||||
targetSdk 33
|
||||
versionCode 999
|
||||
versionName "0.27.2"
|
||||
if (System.properties.containsKey('versionCodeOverride')) {
|
||||
versionCode System.getProperty('versionCodeOverride') as Integer
|
||||
} else {
|
||||
versionCode 1004
|
||||
}
|
||||
versionName "0.27.7"
|
||||
if (System.properties.containsKey('versionNameSuffix')) {
|
||||
versionNameSuffix System.getProperty('versionNameSuffix')
|
||||
}
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
|
||||
@@ -90,8 +101,14 @@ android {
|
||||
androidTest.assets.srcDirs += files("$projectDir/schemas".toString())
|
||||
}
|
||||
|
||||
androidResources {
|
||||
generateLocaleConfig = true
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
viewBinding true
|
||||
compose true
|
||||
buildConfig true
|
||||
}
|
||||
|
||||
packagingOptions {
|
||||
@@ -105,23 +122,6 @@ android {
|
||||
}
|
||||
}
|
||||
|
||||
ext {
|
||||
checkstyleVersion = '10.12.1'
|
||||
|
||||
androidxLifecycleVersion = '2.6.2'
|
||||
androidxRoomVersion = '2.6.1'
|
||||
androidxWorkVersion = '2.8.1'
|
||||
|
||||
icepickVersion = '3.2.0'
|
||||
exoPlayerVersion = '2.18.7'
|
||||
googleAutoServiceVersion = '1.1.1'
|
||||
groupieVersion = '2.10.1'
|
||||
markwonVersion = '4.6.2'
|
||||
|
||||
leakCanaryVersion = '2.12'
|
||||
stethoVersion = '1.6.0'
|
||||
}
|
||||
|
||||
configurations {
|
||||
checkstyle
|
||||
ktlint
|
||||
@@ -131,7 +131,7 @@ checkstyle {
|
||||
getConfigDirectory().set(rootProject.file("checkstyle"))
|
||||
ignoreFailures false
|
||||
showViolations true
|
||||
toolVersion = checkstyleVersion
|
||||
toolVersion = libs.versions.checkstyle.get()
|
||||
}
|
||||
|
||||
tasks.register('runCheckstyle', Checkstyle) {
|
||||
@@ -173,11 +173,13 @@ tasks.register('formatKtlint', JavaExec) {
|
||||
jvmArgs("--add-opens", "java.base/java.lang=ALL-UNNAMED")
|
||||
}
|
||||
|
||||
apply from: 'check-dependencies.gradle'
|
||||
|
||||
afterEvaluate {
|
||||
if (!System.properties.containsKey('skipFormatKtlint')) {
|
||||
preDebugBuild.dependsOn formatKtlint
|
||||
}
|
||||
preDebugBuild.dependsOn runCheckstyle, runKtlint
|
||||
preDebugBuild.dependsOn runCheckstyle, runKtlint, checkDependenciesOrder
|
||||
}
|
||||
|
||||
sonar {
|
||||
@@ -188,119 +190,155 @@ sonar {
|
||||
}
|
||||
}
|
||||
|
||||
kapt {
|
||||
correctErrorTypes true
|
||||
}
|
||||
|
||||
aboutLibraries {
|
||||
// note: offline mode prevents the plugin from fetching licenses at build time, which would be
|
||||
// harmful for reproducible builds
|
||||
offlineMode = true
|
||||
duplicationMode = DuplicateMode.MERGE
|
||||
}
|
||||
|
||||
dependencies {
|
||||
/** Desugaring **/
|
||||
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs_nio:2.0.4'
|
||||
coreLibraryDesugaring libs.desugar.jdk.libs.nio
|
||||
|
||||
/** NewPipe libraries **/
|
||||
// You can use a local version by uncommenting a few lines in settings.gradle
|
||||
// Or you can use a commit you pushed to GitHub by just replacing TeamNewPipe with your GitHub
|
||||
// name and the commit hash with the commit hash of the (pushed) commit you want to test
|
||||
// This works thanks to JitPack: https://jitpack.io/
|
||||
implementation 'com.github.TeamNewPipe:nanojson:1d9e1aea9049fc9f85e68b43ba39fe7be1c1f751'
|
||||
implementation 'com.github.TeamNewPipe:NewPipeExtractor:v0.24.2'
|
||||
implementation 'com.github.TeamNewPipe:NoNonsense-FilePicker:5.0.0'
|
||||
implementation libs.teamnewpipe.nanojson
|
||||
implementation libs.teamnewpipe.newpipe.extractor
|
||||
implementation libs.teamnewpipe.nononsense.filepicker
|
||||
|
||||
/** Checkstyle **/
|
||||
checkstyle "com.puppycrawl.tools:checkstyle:${checkstyleVersion}"
|
||||
ktlint 'com.pinterest:ktlint:0.45.2'
|
||||
checkstyle libs.tools.checkstyle
|
||||
ktlint libs.tools.ktlint
|
||||
|
||||
/** Kotlin **/
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib:${kotlin_version}"
|
||||
implementation libs.kotlin.stdlib
|
||||
|
||||
/** AndroidX **/
|
||||
implementation 'androidx.appcompat:appcompat:1.6.1'
|
||||
implementation 'androidx.cardview:cardview:1.0.0'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
|
||||
implementation 'androidx.core:core-ktx:1.12.0'
|
||||
implementation 'androidx.documentfile:documentfile:1.0.1'
|
||||
implementation 'androidx.fragment:fragment-ktx:1.6.2'
|
||||
implementation "androidx.lifecycle:lifecycle-livedata-ktx:${androidxLifecycleVersion}"
|
||||
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:${androidxLifecycleVersion}"
|
||||
implementation 'androidx.localbroadcastmanager:localbroadcastmanager:1.1.0'
|
||||
implementation 'androidx.media:media:1.7.0'
|
||||
implementation 'androidx.preference:preference:1.2.1'
|
||||
implementation 'androidx.recyclerview:recyclerview:1.3.2'
|
||||
implementation "androidx.room:room-runtime:${androidxRoomVersion}"
|
||||
implementation "androidx.room:room-rxjava3:${androidxRoomVersion}"
|
||||
kapt "androidx.room:room-compiler:${androidxRoomVersion}"
|
||||
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
|
||||
// Newer version specified to prevent accessibility regressions with RecyclerView, see:
|
||||
// https://developer.android.com/jetpack/androidx/releases/viewpager2#1.1.0-alpha01
|
||||
implementation 'androidx.viewpager2:viewpager2:1.1.0-beta02'
|
||||
implementation "androidx.work:work-runtime-ktx:${androidxWorkVersion}"
|
||||
implementation "androidx.work:work-rxjava3:${androidxWorkVersion}"
|
||||
implementation 'com.google.android.material:material:1.11.0'
|
||||
implementation libs.androidx.appcompat
|
||||
implementation libs.androidx.cardview
|
||||
implementation libs.androidx.constraintlayout
|
||||
implementation libs.androidx.core.ktx
|
||||
implementation libs.androidx.documentfile
|
||||
implementation libs.androidx.fragment.compose
|
||||
implementation libs.androidx.lifecycle.livedata
|
||||
implementation libs.androidx.lifecycle.viewmodel
|
||||
implementation libs.androidx.localbroadcastmanager
|
||||
implementation libs.androidx.media
|
||||
implementation libs.androidx.preference
|
||||
implementation libs.androidx.recyclerview
|
||||
implementation libs.androidx.room.runtime
|
||||
implementation libs.androidx.room.rxjava3
|
||||
kapt libs.androidx.room.compiler
|
||||
implementation libs.androidx.swiperefreshlayout
|
||||
implementation libs.androidx.work.runtime
|
||||
implementation libs.androidx.work.rxjava3
|
||||
implementation libs.androidx.material
|
||||
implementation libs.androidx.webkit
|
||||
|
||||
/** Third-party libraries **/
|
||||
// Instance state boilerplate elimination
|
||||
implementation "frankiesardo:icepick:${icepickVersion}"
|
||||
kapt "frankiesardo:icepick-processor:${icepickVersion}"
|
||||
implementation libs.livefront.bridge
|
||||
implementation libs.android.state
|
||||
kapt libs.android.state.processor
|
||||
|
||||
// HTML parser
|
||||
implementation "org.jsoup:jsoup:1.17.2"
|
||||
implementation libs.jsoup
|
||||
|
||||
// HTTP client
|
||||
implementation "com.squareup.okhttp3:okhttp:4.12.0"
|
||||
implementation libs.okhttp
|
||||
|
||||
// Media player
|
||||
implementation "com.google.android.exoplayer:exoplayer-core:${exoPlayerVersion}"
|
||||
implementation "com.google.android.exoplayer:exoplayer-dash:${exoPlayerVersion}"
|
||||
implementation "com.google.android.exoplayer:exoplayer-database:${exoPlayerVersion}"
|
||||
implementation "com.google.android.exoplayer:exoplayer-datasource:${exoPlayerVersion}"
|
||||
implementation "com.google.android.exoplayer:exoplayer-hls:${exoPlayerVersion}"
|
||||
implementation "com.google.android.exoplayer:exoplayer-smoothstreaming:${exoPlayerVersion}"
|
||||
implementation "com.google.android.exoplayer:exoplayer-ui:${exoPlayerVersion}"
|
||||
implementation "com.google.android.exoplayer:extension-mediasession:${exoPlayerVersion}"
|
||||
implementation libs.exoplayer.core
|
||||
implementation libs.exoplayer.dash
|
||||
implementation libs.exoplayer.database
|
||||
implementation libs.exoplayer.datasource
|
||||
implementation libs.exoplayer.hls
|
||||
implementation libs.exoplayer.smoothstreaming
|
||||
implementation libs.exoplayer.ui
|
||||
implementation libs.extension.mediasession
|
||||
|
||||
// Metadata generator for service descriptors
|
||||
compileOnly "com.google.auto.service:auto-service-annotations:${googleAutoServiceVersion}"
|
||||
kapt "com.google.auto.service:auto-service:${googleAutoServiceVersion}"
|
||||
compileOnly libs.auto.service
|
||||
kapt libs.auto.service.kapt
|
||||
|
||||
// Manager for complex RecyclerView layouts
|
||||
implementation "com.github.lisawray.groupie:groupie:${groupieVersion}"
|
||||
implementation "com.github.lisawray.groupie:groupie-viewbinding:${groupieVersion}"
|
||||
implementation libs.lisawray.groupie
|
||||
implementation libs.lisawray.groupie.viewbinding
|
||||
|
||||
// Image loading
|
||||
//noinspection GradleDependency --> 2.8 is the last version, not 2.71828!
|
||||
implementation "com.squareup.picasso:picasso:2.8"
|
||||
implementation libs.coil.compose
|
||||
implementation libs.coil.network.okhttp
|
||||
|
||||
// Markdown library for Android
|
||||
implementation "io.noties.markwon:core:${markwonVersion}"
|
||||
implementation "io.noties.markwon:linkify:${markwonVersion}"
|
||||
implementation libs.markwon.core
|
||||
implementation libs.markwon.linkify
|
||||
|
||||
// Crash reporting
|
||||
implementation "ch.acra:acra-core:5.11.3"
|
||||
implementation libs.acra.core
|
||||
|
||||
// Properly restarting
|
||||
implementation 'com.jakewharton:process-phoenix:2.1.2'
|
||||
implementation libs.process.phoenix
|
||||
|
||||
// Reactive extensions for Java VM
|
||||
implementation "io.reactivex.rxjava3:rxjava:3.1.8"
|
||||
implementation "io.reactivex.rxjava3:rxandroid:3.0.2"
|
||||
implementation libs.rxjava3.rxjava
|
||||
implementation libs.rxjava3.rxandroid
|
||||
// RxJava binding APIs for Android UI widgets
|
||||
implementation "com.jakewharton.rxbinding4:rxbinding:4.0.0"
|
||||
implementation libs.rxbinding4.rxbinding
|
||||
|
||||
// Date and time formatting
|
||||
implementation "org.ocpsoft.prettytime:prettytime:5.0.7.Final"
|
||||
implementation libs.prettytime
|
||||
|
||||
// Jetpack Compose
|
||||
implementation(platform(libs.androidx.compose.bom))
|
||||
implementation libs.androidx.compose.material3
|
||||
implementation libs.androidx.compose.adaptive
|
||||
implementation libs.androidx.activity.compose
|
||||
implementation libs.androidx.compose.ui.tooling.preview
|
||||
implementation libs.androidx.lifecycle.viewmodel.compose
|
||||
implementation libs.androidx.compose.ui.text // Needed for parsing HTML to AnnotatedString
|
||||
implementation libs.androidx.compose.material.icons.extended
|
||||
|
||||
// Jetpack Compose related dependencies
|
||||
implementation libs.androidx.paging.compose
|
||||
implementation libs.androidx.navigation.compose
|
||||
|
||||
// Coroutines interop
|
||||
implementation libs.kotlinx.coroutines.rx3
|
||||
|
||||
// Library loading for About screen
|
||||
implementation libs.aboutlibraries.compose.m3
|
||||
|
||||
// Hilt
|
||||
implementation libs.hilt.android
|
||||
kapt(libs.hilt.compiler)
|
||||
|
||||
// Scroll
|
||||
implementation libs.lazycolumnscrollbar
|
||||
|
||||
/** Debugging **/
|
||||
// Memory leak detection
|
||||
debugImplementation "com.squareup.leakcanary:leakcanary-object-watcher-android:${leakCanaryVersion}"
|
||||
debugImplementation "com.squareup.leakcanary:plumber-android:${leakCanaryVersion}"
|
||||
debugImplementation "com.squareup.leakcanary:leakcanary-android-core:${leakCanaryVersion}"
|
||||
debugImplementation libs.leakcanary.object.watcher
|
||||
debugImplementation libs.leakcanary.plumber.android
|
||||
debugImplementation libs.leakcanary.android.core
|
||||
// Debug bridge for Android
|
||||
debugImplementation "com.facebook.stetho:stetho:${stethoVersion}"
|
||||
debugImplementation "com.facebook.stetho:stetho-okhttp3:${stethoVersion}"
|
||||
debugImplementation libs.stetho
|
||||
debugImplementation libs.stetho.okhttp3
|
||||
|
||||
// Jetpack Compose
|
||||
debugImplementation libs.androidx.compose.ui.tooling
|
||||
|
||||
/** Testing **/
|
||||
testImplementation 'junit:junit:4.13.2'
|
||||
testImplementation 'org.mockito:mockito-core:5.6.0'
|
||||
testImplementation libs.junit
|
||||
testImplementation libs.mockito.core
|
||||
|
||||
androidTestImplementation "androidx.test.ext:junit:1.1.5"
|
||||
androidTestImplementation "androidx.test:runner:1.5.2"
|
||||
androidTestImplementation "androidx.room:room-testing:${androidxRoomVersion}"
|
||||
androidTestImplementation "org.assertj:assertj-core:3.24.2"
|
||||
androidTestImplementation libs.androidx.junit
|
||||
androidTestImplementation libs.androidx.runner
|
||||
androidTestImplementation libs.androidx.room.testing
|
||||
androidTestImplementation libs.assertj.core
|
||||
}
|
||||
|
||||
static String getGitWorkingBranch() {
|
||||
|
||||
48
app/check-dependencies.gradle
Normal file
48
app/check-dependencies.gradle
Normal file
@@ -0,0 +1,48 @@
|
||||
tasks.register('checkDependenciesOrder') {
|
||||
group = 'verification'
|
||||
description = 'Checks that each section in libs.versions.toml is sorted alphabetically'
|
||||
|
||||
def tomlFile = file('../gradle/libs.versions.toml')
|
||||
|
||||
doLast {
|
||||
if (!tomlFile.exists()) {
|
||||
throw new GradleException('TOML file not found')
|
||||
}
|
||||
|
||||
def lines = tomlFile.readLines()
|
||||
def nonSortedBlocks = []
|
||||
def currentBlock = []
|
||||
def prevLine = ''
|
||||
def prevIndex = 0
|
||||
|
||||
lines.eachWithIndex { line, lineIndex ->
|
||||
if (line.trim() && !line.startsWith('#')) {
|
||||
if (line.startsWith('[')) {
|
||||
prevLine = ''
|
||||
} else {
|
||||
def currIndex = lineIndex + 1
|
||||
if (prevLine > line) {
|
||||
if (currentBlock && currentBlock[-1] == "${prevIndex}: ${prevLine}") {
|
||||
currentBlock.add("${currIndex}: ${line}")
|
||||
} else {
|
||||
if (!currentBlock.isEmpty()) {
|
||||
nonSortedBlocks.add(currentBlock)
|
||||
currentBlock = []
|
||||
}
|
||||
currentBlock.add("${prevIndex}: ${prevLine}")
|
||||
currentBlock.add("${currIndex}: ${line}")
|
||||
}
|
||||
}
|
||||
prevLine = line
|
||||
prevIndex = lineIndex + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!currentBlock.isEmpty()) {
|
||||
nonSortedBlocks.add(currentBlock)
|
||||
throw new GradleException("The following lines were not sorted:\n" +
|
||||
nonSortedBlocks.collect { it.join("\n") }.join("\n\n"))
|
||||
}
|
||||
}
|
||||
}
|
||||
16
app/proguard-rules.pro
vendored
16
app/proguard-rules.pro
vendored
@@ -5,23 +5,21 @@
|
||||
|
||||
## Rules for NewPipeExtractor
|
||||
-keep class org.schabi.newpipe.extractor.timeago.patterns.** { *; }
|
||||
## Rules for Rhino and Rhino Engine
|
||||
-keep class org.mozilla.javascript.* { *; }
|
||||
-keep class org.mozilla.javascript.** { *; }
|
||||
-keep class org.mozilla.javascript.engine.** { *; }
|
||||
-keep class org.mozilla.classfile.ClassFileWriter
|
||||
-dontwarn org.mozilla.javascript.JavaToJSONConverters
|
||||
-dontwarn org.mozilla.javascript.tools.**
|
||||
-keep class javax.script.** { *; }
|
||||
-dontwarn javax.script.**
|
||||
-keep class jdk.dynalink.** { *; }
|
||||
-dontwarn jdk.dynalink.**
|
||||
|
||||
## Rules for ExoPlayer
|
||||
-keep class com.google.android.exoplayer2.** { *; }
|
||||
|
||||
## Rules for Icepick. Copy pasted from https://github.com/frankiesardo/icepick
|
||||
-dontwarn icepick.**
|
||||
-keep class icepick.** { *; }
|
||||
-keep class **$$Icepick { *; }
|
||||
-keepclasseswithmembernames class * {
|
||||
@icepick.* <fields>;
|
||||
}
|
||||
-keepnames class * { @icepick.State *;}
|
||||
|
||||
## Rules for OkHttp. Copy pasted from https://github.com/square/okhttp
|
||||
-dontwarn okhttp3.**
|
||||
-dontwarn okio.**
|
||||
|
||||
@@ -64,6 +64,9 @@
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MEDIA_BUTTON" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.media.browse.MediaBrowserService"/>
|
||||
</intent-filter>
|
||||
</service>
|
||||
|
||||
<activity
|
||||
@@ -77,6 +80,11 @@
|
||||
android:exported="false"
|
||||
android:label="@string/settings" />
|
||||
|
||||
<activity
|
||||
android:name=".settings.SettingsV2Activity"
|
||||
android:exported="true"
|
||||
android:label="@string/settings" />
|
||||
|
||||
<activity
|
||||
android:name=".about.AboutActivity"
|
||||
android:exported="false"
|
||||
@@ -367,6 +375,7 @@
|
||||
<data android:host="tilvids.com" />
|
||||
<data android:host="video.lqdn.fr" />
|
||||
<data android:host="video.ploud.fr" />
|
||||
<data android:host="subscribeto.me" />
|
||||
|
||||
<data android:pathPrefix="/videos/" /> <!-- it contains playlists -->
|
||||
<data android:pathPrefix="/w/" /> <!-- short video URLs -->
|
||||
@@ -423,5 +432,10 @@
|
||||
<meta-data
|
||||
android:name="com.samsung.android.multidisplay.keep_process_alive"
|
||||
android:value="true" />
|
||||
<!-- Android Auto -->
|
||||
<meta-data android:name="com.google.android.gms.car.application"
|
||||
android:resource="@xml/automotive_app_desc" />
|
||||
<meta-data android:name="com.google.android.gms.car.notification.SmallIcon"
|
||||
android:resource="@mipmap/ic_launcher" />
|
||||
</application>
|
||||
</manifest>
|
||||
|
||||
127
app/src/main/assets/po_token.html
Normal file
127
app/src/main/assets/po_token.html
Normal file
@@ -0,0 +1,127 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en"><head><title></title><script>
|
||||
/**
|
||||
* Factory method to create and load a BotGuardClient instance.
|
||||
* @param options - Configuration options for the BotGuardClient.
|
||||
* @returns A promise that resolves to a loaded BotGuardClient instance.
|
||||
*/
|
||||
function loadBotGuard(challengeData) {
|
||||
this.vm = this[challengeData.globalName];
|
||||
this.program = challengeData.program;
|
||||
this.vmFunctions = {};
|
||||
this.syncSnapshotFunction = null;
|
||||
|
||||
if (!this.vm)
|
||||
throw new Error('[BotGuardClient]: VM not found in the global object');
|
||||
|
||||
if (!this.vm.a)
|
||||
throw new Error('[BotGuardClient]: Could not load program');
|
||||
|
||||
const vmFunctionsCallback = function (
|
||||
asyncSnapshotFunction,
|
||||
shutdownFunction,
|
||||
passEventFunction,
|
||||
checkCameraFunction
|
||||
) {
|
||||
this.vmFunctions = {
|
||||
asyncSnapshotFunction: asyncSnapshotFunction,
|
||||
shutdownFunction: shutdownFunction,
|
||||
passEventFunction: passEventFunction,
|
||||
checkCameraFunction: checkCameraFunction
|
||||
};
|
||||
};
|
||||
|
||||
this.syncSnapshotFunction = this.vm.a(this.program, vmFunctionsCallback, true, this.userInteractionElement, function () {/** no-op */ }, [ [], [] ])[0]
|
||||
|
||||
// an asynchronous function runs in the background and it will eventually call
|
||||
// `vmFunctionsCallback`, however we need to manually tell JavaScript to pass
|
||||
// control to the things running in the background by interrupting this async
|
||||
// function in any way, e.g. with a delay of 1ms. The loop is most probably not
|
||||
// needed but is there just because.
|
||||
return new Promise(function (resolve, reject) {
|
||||
i = 0
|
||||
refreshIntervalId = setInterval(function () {
|
||||
if (!!this.vmFunctions.asyncSnapshotFunction) {
|
||||
resolve(this)
|
||||
clearInterval(refreshIntervalId);
|
||||
}
|
||||
if (i >= 10000) {
|
||||
reject("asyncSnapshotFunction is null even after 10 seconds")
|
||||
clearInterval(refreshIntervalId);
|
||||
}
|
||||
i += 1;
|
||||
}, 1);
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes a snapshot asynchronously.
|
||||
* @returns The snapshot result.
|
||||
* @example
|
||||
* ```ts
|
||||
* const result = await botguard.snapshot({
|
||||
* contentBinding: {
|
||||
* c: "a=6&a2=10&b=SZWDwKVIuixOp7Y4euGTgwckbJA&c=1729143849&d=1&t=7200&c1a=1&c6a=1&c6b=1&hh=HrMb5mRWTyxGJphDr0nW2Oxonh0_wl2BDqWuLHyeKLo",
|
||||
* e: "ENGAGEMENT_TYPE_VIDEO_LIKE",
|
||||
* encryptedVideoId: "P-vC09ZJcnM"
|
||||
* }
|
||||
* });
|
||||
*
|
||||
* console.log(result);
|
||||
* ```
|
||||
*/
|
||||
function snapshot(args) {
|
||||
return new Promise(function (resolve, reject) {
|
||||
if (!this.vmFunctions.asyncSnapshotFunction)
|
||||
return reject(new Error('[BotGuardClient]: Async snapshot function not found'));
|
||||
|
||||
this.vmFunctions.asyncSnapshotFunction(function (response) { resolve(response) }, [
|
||||
args.contentBinding,
|
||||
args.signedTimestamp,
|
||||
args.webPoSignalOutput,
|
||||
args.skipPrivacyBuffer
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
function runBotGuard(challengeData) {
|
||||
const interpreterJavascript = challengeData.interpreterJavascript.privateDoNotAccessOrElseSafeScriptWrappedValue;
|
||||
|
||||
if (interpreterJavascript) {
|
||||
new Function(interpreterJavascript)();
|
||||
} else throw new Error('Could not load VM');
|
||||
|
||||
const webPoSignalOutput = [];
|
||||
return loadBotGuard({
|
||||
globalName: challengeData.globalName,
|
||||
globalObj: this,
|
||||
program: challengeData.program
|
||||
}).then(function (botguard) {
|
||||
return botguard.snapshot({ webPoSignalOutput: webPoSignalOutput })
|
||||
}).then(function (botguardResponse) {
|
||||
return { webPoSignalOutput: webPoSignalOutput, botguardResponse: botguardResponse }
|
||||
})
|
||||
}
|
||||
|
||||
function obtainPoToken(webPoSignalOutput, integrityToken, identifier) {
|
||||
const getMinter = webPoSignalOutput[0];
|
||||
|
||||
if (!getMinter)
|
||||
throw new Error('PMD:Undefined');
|
||||
|
||||
const mintCallback = getMinter(integrityToken);
|
||||
|
||||
if (!(mintCallback instanceof Function))
|
||||
throw new Error('APF:Failed');
|
||||
|
||||
const result = mintCallback(identifier);
|
||||
|
||||
if (!result)
|
||||
throw new Error('YNJ:Undefined');
|
||||
|
||||
if (!(result instanceof Uint8Array))
|
||||
throw new Error('ODM:Invalid');
|
||||
|
||||
return result;
|
||||
}
|
||||
</script></head><body></body></html>
|
||||
@@ -1,269 +0,0 @@
|
||||
package org.schabi.newpipe;
|
||||
|
||||
import android.app.Application;
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.core.app.NotificationChannelCompat;
|
||||
import androidx.core.app.NotificationManagerCompat;
|
||||
import androidx.preference.PreferenceManager;
|
||||
|
||||
import com.jakewharton.processphoenix.ProcessPhoenix;
|
||||
|
||||
import org.acra.ACRA;
|
||||
import org.acra.config.CoreConfigurationBuilder;
|
||||
import org.schabi.newpipe.error.ReCaptchaActivity;
|
||||
import org.schabi.newpipe.extractor.NewPipe;
|
||||
import org.schabi.newpipe.extractor.downloader.Downloader;
|
||||
import org.schabi.newpipe.ktx.ExceptionUtils;
|
||||
import org.schabi.newpipe.settings.NewPipeSettings;
|
||||
import org.schabi.newpipe.util.Localization;
|
||||
import org.schabi.newpipe.util.image.ImageStrategy;
|
||||
import org.schabi.newpipe.util.image.PicassoHelper;
|
||||
import org.schabi.newpipe.util.ServiceHelper;
|
||||
import org.schabi.newpipe.util.StateSaver;
|
||||
import org.schabi.newpipe.util.image.PreferredImageQuality;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InterruptedIOException;
|
||||
import java.net.SocketException;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
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>
|
||||
* App.java is part of NewPipe.
|
||||
*
|
||||
* NewPipe is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* NewPipe is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
public class App extends Application {
|
||||
public static final String PACKAGE_NAME = BuildConfig.APPLICATION_ID;
|
||||
private static final String TAG = App.class.toString();
|
||||
|
||||
private boolean isFirstRun = false;
|
||||
private static App app;
|
||||
|
||||
@NonNull
|
||||
public static App getApp() {
|
||||
return app;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void attachBaseContext(final Context base) {
|
||||
super.attachBaseContext(base);
|
||||
initACRA();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
|
||||
app = this;
|
||||
|
||||
if (ProcessPhoenix.isPhoenixProcess(this)) {
|
||||
Log.i(TAG, "This is a phoenix process! "
|
||||
+ "Aborting initialization of App[onCreate]");
|
||||
return;
|
||||
}
|
||||
|
||||
// check if the last used preference version is set
|
||||
// to determine whether this is the first app run
|
||||
final int lastUsedPrefVersion = PreferenceManager.getDefaultSharedPreferences(this)
|
||||
.getInt(getString(R.string.last_used_preferences_version), -1);
|
||||
isFirstRun = lastUsedPrefVersion == -1;
|
||||
|
||||
// Initialize settings first because other initializations can use its values
|
||||
NewPipeSettings.initSettings(this);
|
||||
|
||||
NewPipe.init(getDownloader(),
|
||||
Localization.getPreferredLocalization(this),
|
||||
Localization.getPreferredContentCountry(this));
|
||||
Localization.initPrettyTime(Localization.resolvePrettyTime(getApplicationContext()));
|
||||
|
||||
StateSaver.init(this);
|
||||
initNotificationChannels();
|
||||
|
||||
ServiceHelper.initServices(this);
|
||||
|
||||
// Initialize image loader
|
||||
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
|
||||
PicassoHelper.init(this);
|
||||
ImageStrategy.setPreferredImageQuality(PreferredImageQuality.fromPreferenceKey(this,
|
||||
prefs.getString(getString(R.string.image_quality_key),
|
||||
getString(R.string.image_quality_default))));
|
||||
PicassoHelper.setIndicatorsEnabled(MainActivity.DEBUG
|
||||
&& prefs.getBoolean(getString(R.string.show_image_indicators_key), false));
|
||||
|
||||
configureRxJavaErrorHandler();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTerminate() {
|
||||
super.onTerminate();
|
||||
PicassoHelper.terminate();
|
||||
}
|
||||
|
||||
protected Downloader getDownloader() {
|
||||
final DownloaderImpl downloader = DownloaderImpl.init(null);
|
||||
setCookiesToDownloader(downloader);
|
||||
return downloader;
|
||||
}
|
||||
|
||||
protected void setCookiesToDownloader(final DownloaderImpl downloader) {
|
||||
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(
|
||||
getApplicationContext());
|
||||
final String key = getApplicationContext().getString(R.string.recaptcha_cookies_key);
|
||||
downloader.setCookie(ReCaptchaActivity.RECAPTCHA_COOKIES_KEY, prefs.getString(key, null));
|
||||
downloader.updateYoutubeRestrictedModeCookies(getApplicationContext());
|
||||
}
|
||||
|
||||
private void configureRxJavaErrorHandler() {
|
||||
// https://github.com/ReactiveX/RxJava/wiki/What's-different-in-2.0#error-handling
|
||||
RxJavaPlugins.setErrorHandler(new Consumer<Throwable>() {
|
||||
@Override
|
||||
public void accept(@NonNull final Throwable throwable) {
|
||||
Log.e(TAG, "RxJavaPlugins.ErrorHandler called with -> : "
|
||||
+ "throwable = [" + throwable.getClass().getName() + "]");
|
||||
|
||||
final Throwable actualThrowable;
|
||||
if (throwable instanceof UndeliverableException) {
|
||||
// As UndeliverableException is a wrapper,
|
||||
// get the cause of it to get the "real" exception
|
||||
actualThrowable = Objects.requireNonNull(throwable.getCause());
|
||||
} else {
|
||||
actualThrowable = throwable;
|
||||
}
|
||||
|
||||
final List<Throwable> errors;
|
||||
if (actualThrowable instanceof CompositeException) {
|
||||
errors = ((CompositeException) actualThrowable).getExceptions();
|
||||
} else {
|
||||
errors = List.of(actualThrowable);
|
||||
}
|
||||
|
||||
for (final Throwable error : errors) {
|
||||
if (isThrowableIgnored(error)) {
|
||||
return;
|
||||
}
|
||||
if (isThrowableCritical(error)) {
|
||||
reportException(error);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Out-of-lifecycle exceptions should only be reported if a debug user wishes so,
|
||||
// When exception is not reported, log it
|
||||
if (isDisposedRxExceptionsReported()) {
|
||||
reportException(actualThrowable);
|
||||
} else {
|
||||
Log.e(TAG, "RxJavaPlugin: Undeliverable Exception received: ", actualThrowable);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isThrowableIgnored(@NonNull final Throwable throwable) {
|
||||
// Don't crash the application over a simple network problem
|
||||
return ExceptionUtils.hasAssignableCause(throwable,
|
||||
// network api cancellation
|
||||
IOException.class, SocketException.class,
|
||||
// blocking code disposed
|
||||
InterruptedException.class, InterruptedIOException.class);
|
||||
}
|
||||
|
||||
private boolean isThrowableCritical(@NonNull final Throwable throwable) {
|
||||
// Though these exceptions cannot be ignored
|
||||
return ExceptionUtils.hasAssignableCause(throwable,
|
||||
NullPointerException.class, IllegalArgumentException.class, // bug in app
|
||||
OnErrorNotImplementedException.class, MissingBackpressureException.class,
|
||||
IllegalStateException.class); // bug in operator
|
||||
}
|
||||
|
||||
private void reportException(@NonNull final Throwable throwable) {
|
||||
// Throw uncaught exception that will trigger the report system
|
||||
Thread.currentThread().getUncaughtExceptionHandler()
|
||||
.uncaughtException(Thread.currentThread(), throwable);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Called in {@link #attachBaseContext(Context)} after calling the {@code super} method.
|
||||
* Should be overridden if MultiDex is enabled, since it has to be initialized before ACRA.
|
||||
*/
|
||||
protected void initACRA() {
|
||||
if (ACRA.isACRASenderServiceProcess()) {
|
||||
return;
|
||||
}
|
||||
|
||||
final CoreConfigurationBuilder acraConfig = new CoreConfigurationBuilder()
|
||||
.withBuildConfigClass(BuildConfig.class);
|
||||
ACRA.init(this, acraConfig);
|
||||
}
|
||||
|
||||
private void initNotificationChannels() {
|
||||
// Keep the importance below DEFAULT to avoid making noise on every notification update for
|
||||
// the main and update channels
|
||||
final List<NotificationChannelCompat> notificationChannelCompats = List.of(
|
||||
new NotificationChannelCompat.Builder(getString(R.string.notification_channel_id),
|
||||
NotificationManagerCompat.IMPORTANCE_LOW)
|
||||
.setName(getString(R.string.notification_channel_name))
|
||||
.setDescription(getString(R.string.notification_channel_description))
|
||||
.build(),
|
||||
new NotificationChannelCompat
|
||||
.Builder(getString(R.string.app_update_notification_channel_id),
|
||||
NotificationManagerCompat.IMPORTANCE_LOW)
|
||||
.setName(getString(R.string.app_update_notification_channel_name))
|
||||
.setDescription(
|
||||
getString(R.string.app_update_notification_channel_description))
|
||||
.build(),
|
||||
new NotificationChannelCompat.Builder(getString(R.string.hash_channel_id),
|
||||
NotificationManagerCompat.IMPORTANCE_HIGH)
|
||||
.setName(getString(R.string.hash_channel_name))
|
||||
.setDescription(getString(R.string.hash_channel_description))
|
||||
.build(),
|
||||
new NotificationChannelCompat.Builder(getString(R.string.error_report_channel_id),
|
||||
NotificationManagerCompat.IMPORTANCE_LOW)
|
||||
.setName(getString(R.string.error_report_channel_name))
|
||||
.setDescription(getString(R.string.error_report_channel_description))
|
||||
.build(),
|
||||
new NotificationChannelCompat
|
||||
.Builder(getString(R.string.streams_notification_channel_id),
|
||||
NotificationManagerCompat.IMPORTANCE_DEFAULT)
|
||||
.setName(getString(R.string.streams_notification_channel_name))
|
||||
.setDescription(
|
||||
getString(R.string.streams_notification_channel_description))
|
||||
.build()
|
||||
);
|
||||
|
||||
final NotificationManagerCompat notificationManager = NotificationManagerCompat.from(this);
|
||||
notificationManager.createNotificationChannelsCompat(notificationChannelCompats);
|
||||
}
|
||||
|
||||
protected boolean isDisposedRxExceptionsReported() {
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean isFirstRun() {
|
||||
return isFirstRun;
|
||||
}
|
||||
}
|
||||
290
app/src/main/java/org/schabi/newpipe/App.kt
Normal file
290
app/src/main/java/org/schabi/newpipe/App.kt
Normal file
@@ -0,0 +1,290 @@
|
||||
package org.schabi.newpipe
|
||||
|
||||
import android.app.ActivityManager
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import androidx.core.app.NotificationChannelCompat
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import androidx.core.content.getSystemService
|
||||
import androidx.preference.PreferenceManager
|
||||
import coil3.ImageLoader
|
||||
import coil3.SingletonImageLoader
|
||||
import coil3.network.okhttp.OkHttpNetworkFetcherFactory
|
||||
import coil3.request.allowRgb565
|
||||
import coil3.request.crossfade
|
||||
import coil3.util.DebugLogger
|
||||
import com.jakewharton.processphoenix.ProcessPhoenix
|
||||
import dagger.hilt.android.HiltAndroidApp
|
||||
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
|
||||
import org.acra.ACRA.init
|
||||
import org.acra.ACRA.isACRASenderServiceProcess
|
||||
import org.acra.config.CoreConfigurationBuilder
|
||||
import org.schabi.newpipe.error.ReCaptchaActivity
|
||||
import org.schabi.newpipe.extractor.NewPipe
|
||||
import org.schabi.newpipe.extractor.downloader.Downloader
|
||||
import org.schabi.newpipe.extractor.services.youtube.extractors.YoutubeStreamExtractor
|
||||
import org.schabi.newpipe.ktx.hasAssignableCause
|
||||
import org.schabi.newpipe.settings.NewPipeSettings
|
||||
import org.schabi.newpipe.util.BridgeStateSaverInitializer
|
||||
import org.schabi.newpipe.util.Localization
|
||||
import org.schabi.newpipe.util.ServiceHelper
|
||||
import org.schabi.newpipe.util.StateSaver
|
||||
import org.schabi.newpipe.util.image.ImageStrategy
|
||||
import org.schabi.newpipe.util.image.PreferredImageQuality
|
||||
import org.schabi.newpipe.util.potoken.PoTokenProviderImpl
|
||||
import java.io.IOException
|
||||
import java.io.InterruptedIOException
|
||||
import java.net.SocketException
|
||||
|
||||
/*
|
||||
* Copyright (C) Hans-Christoph Steiner 2016 <hans@eds.org>
|
||||
* App.kt is part of NewPipe.
|
||||
*
|
||||
* NewPipe is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* NewPipe is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
@HiltAndroidApp
|
||||
open class App :
|
||||
Application(),
|
||||
SingletonImageLoader.Factory {
|
||||
var isFirstRun = false
|
||||
private set
|
||||
|
||||
override fun attachBaseContext(base: Context?) {
|
||||
super.attachBaseContext(base)
|
||||
initACRA()
|
||||
}
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
|
||||
instance = this
|
||||
|
||||
if (ProcessPhoenix.isPhoenixProcess(this)) {
|
||||
Log.i(TAG, "This is a phoenix process! Aborting initialization of App[onCreate]")
|
||||
return
|
||||
}
|
||||
|
||||
// check if the last used preference version is set
|
||||
// to determine whether this is the first app run
|
||||
val lastUsedPrefVersion =
|
||||
PreferenceManager
|
||||
.getDefaultSharedPreferences(this)
|
||||
.getInt(getString(R.string.last_used_preferences_version), -1)
|
||||
isFirstRun = lastUsedPrefVersion == -1
|
||||
|
||||
// Initialize settings first because other initializations can use its values
|
||||
NewPipeSettings.initSettings(this)
|
||||
|
||||
NewPipe.init(
|
||||
getDownloader(),
|
||||
Localization.getPreferredLocalization(this),
|
||||
Localization.getPreferredContentCountry(this),
|
||||
)
|
||||
Localization.initPrettyTime(Localization.resolvePrettyTime(this))
|
||||
|
||||
BridgeStateSaverInitializer.init(this)
|
||||
StateSaver.init(this)
|
||||
initNotificationChannels()
|
||||
|
||||
ServiceHelper.initServices(this)
|
||||
|
||||
// Initialize image loader
|
||||
val prefs = PreferenceManager.getDefaultSharedPreferences(this)
|
||||
ImageStrategy.setPreferredImageQuality(
|
||||
PreferredImageQuality.fromPreferenceKey(
|
||||
this,
|
||||
prefs.getString(
|
||||
getString(R.string.image_quality_key),
|
||||
getString(R.string.image_quality_default),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
configureRxJavaErrorHandler()
|
||||
|
||||
YoutubeStreamExtractor.setPoTokenProvider(PoTokenProviderImpl)
|
||||
}
|
||||
|
||||
override fun newImageLoader(context: Context): ImageLoader =
|
||||
ImageLoader
|
||||
.Builder(this)
|
||||
.logger(if (BuildConfig.DEBUG) DebugLogger() else null)
|
||||
.allowRgb565(getSystemService<ActivityManager>()!!.isLowRamDevice)
|
||||
.crossfade(true)
|
||||
.components {
|
||||
add(OkHttpNetworkFetcherFactory(callFactory = DownloaderImpl.getInstance().client))
|
||||
}.build()
|
||||
|
||||
protected open fun getDownloader(): Downloader {
|
||||
val downloader = DownloaderImpl.init(null)
|
||||
setCookiesToDownloader(downloader)
|
||||
return downloader
|
||||
}
|
||||
|
||||
protected fun setCookiesToDownloader(downloader: DownloaderImpl) {
|
||||
val prefs = PreferenceManager.getDefaultSharedPreferences(this)
|
||||
val key = getString(R.string.recaptcha_cookies_key)
|
||||
downloader.setCookie(ReCaptchaActivity.RECAPTCHA_COOKIES_KEY, prefs.getString(key, null))
|
||||
downloader.updateYoutubeRestrictedModeCookies(this)
|
||||
}
|
||||
|
||||
private fun configureRxJavaErrorHandler() {
|
||||
// https://github.com/ReactiveX/RxJava/wiki/What's-different-in-2.0#error-handling
|
||||
RxJavaPlugins.setErrorHandler(
|
||||
object : Consumer<Throwable> {
|
||||
override fun accept(throwable: Throwable) {
|
||||
Log.e(TAG, "RxJavaPlugins.ErrorHandler called with -> : throwable = [${throwable.javaClass.getName()}]")
|
||||
|
||||
// As UndeliverableException is a wrapper,
|
||||
// get the cause of it to get the "real" exception
|
||||
val actualThrowable = (throwable as? UndeliverableException)?.cause ?: throwable
|
||||
|
||||
val errors = (actualThrowable as? CompositeException)?.exceptions ?: listOf(actualThrowable)
|
||||
|
||||
for (error in errors) {
|
||||
if (isThrowableIgnored(error)) {
|
||||
return
|
||||
}
|
||||
if (isThrowableCritical(error)) {
|
||||
reportException(error)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Out-of-lifecycle exceptions should only be reported if a debug user wishes so,
|
||||
// When exception is not reported, log it
|
||||
if (isDisposedRxExceptionsReported()) {
|
||||
reportException(actualThrowable)
|
||||
} else {
|
||||
Log.e(TAG, "RxJavaPlugin: Undeliverable Exception received: ", actualThrowable)
|
||||
}
|
||||
}
|
||||
|
||||
fun isThrowableIgnored(throwable: Throwable): Boolean {
|
||||
// Don't crash the application over a simple network problem
|
||||
return throwable // network api cancellation
|
||||
.hasAssignableCause(
|
||||
IOException::class.java,
|
||||
SocketException::class.java, // blocking code disposed
|
||||
InterruptedException::class.java,
|
||||
InterruptedIOException::class.java,
|
||||
)
|
||||
}
|
||||
|
||||
fun isThrowableCritical(throwable: Throwable): Boolean {
|
||||
// Though these exceptions cannot be ignored
|
||||
return throwable
|
||||
.hasAssignableCause(
|
||||
// bug in app
|
||||
NullPointerException::class.java,
|
||||
IllegalArgumentException::class.java,
|
||||
OnErrorNotImplementedException::class.java,
|
||||
MissingBackpressureException::class.java,
|
||||
// bug in operator
|
||||
IllegalStateException::class.java,
|
||||
)
|
||||
}
|
||||
|
||||
fun reportException(throwable: Throwable) {
|
||||
// Throw uncaught exception that will trigger the report system
|
||||
Thread
|
||||
.currentThread()
|
||||
.uncaughtExceptionHandler
|
||||
.uncaughtException(Thread.currentThread(), throwable)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Called in [.attachBaseContext] after calling the `super` method.
|
||||
* Should be overridden if MultiDex is enabled, since it has to be initialized before ACRA.
|
||||
*/
|
||||
protected fun initACRA() {
|
||||
if (isACRASenderServiceProcess()) {
|
||||
return
|
||||
}
|
||||
|
||||
val acraConfig =
|
||||
CoreConfigurationBuilder()
|
||||
.withBuildConfigClass(BuildConfig::class.java)
|
||||
init(this, acraConfig)
|
||||
}
|
||||
|
||||
private fun initNotificationChannels() {
|
||||
// Keep the importance below DEFAULT to avoid making noise on every notification update for
|
||||
// the main and update channels
|
||||
val mainChannel =
|
||||
NotificationChannelCompat
|
||||
.Builder(
|
||||
getString(R.string.notification_channel_id),
|
||||
NotificationManagerCompat.IMPORTANCE_LOW,
|
||||
).setName(getString(R.string.notification_channel_name))
|
||||
.setDescription(getString(R.string.notification_channel_description))
|
||||
.build()
|
||||
val appUpdateChannel =
|
||||
NotificationChannelCompat
|
||||
.Builder(
|
||||
getString(R.string.app_update_notification_channel_id),
|
||||
NotificationManagerCompat.IMPORTANCE_LOW,
|
||||
).setName(getString(R.string.app_update_notification_channel_name))
|
||||
.setDescription(getString(R.string.app_update_notification_channel_description))
|
||||
.build()
|
||||
val hashChannel =
|
||||
NotificationChannelCompat
|
||||
.Builder(
|
||||
getString(R.string.hash_channel_id),
|
||||
NotificationManagerCompat.IMPORTANCE_HIGH,
|
||||
).setName(getString(R.string.hash_channel_name))
|
||||
.setDescription(getString(R.string.hash_channel_description))
|
||||
.build()
|
||||
val errorReportChannel =
|
||||
NotificationChannelCompat
|
||||
.Builder(
|
||||
getString(R.string.error_report_channel_id),
|
||||
NotificationManagerCompat.IMPORTANCE_LOW,
|
||||
).setName(getString(R.string.error_report_channel_name))
|
||||
.setDescription(getString(R.string.error_report_channel_description))
|
||||
.build()
|
||||
val newStreamChannel =
|
||||
NotificationChannelCompat
|
||||
.Builder(
|
||||
getString(R.string.streams_notification_channel_id),
|
||||
NotificationManagerCompat.IMPORTANCE_DEFAULT,
|
||||
).setName(getString(R.string.streams_notification_channel_name))
|
||||
.setDescription(getString(R.string.streams_notification_channel_description))
|
||||
.build()
|
||||
|
||||
val channels = listOf(mainChannel, appUpdateChannel, hashChannel, errorReportChannel, newStreamChannel)
|
||||
|
||||
NotificationManagerCompat.from(this).createNotificationChannelsCompat(channels)
|
||||
}
|
||||
|
||||
protected open fun isDisposedRxExceptionsReported(): Boolean = false
|
||||
|
||||
companion object {
|
||||
const val PACKAGE_NAME: String = BuildConfig.APPLICATION_ID
|
||||
private val TAG = App::class.java.toString()
|
||||
|
||||
@JvmStatic
|
||||
lateinit var instance: App
|
||||
private set
|
||||
}
|
||||
}
|
||||
22
app/src/main/java/org/schabi/newpipe/AppModule.kt
Normal file
22
app/src/main/java/org/schabi/newpipe/AppModule.kt
Normal file
@@ -0,0 +1,22 @@
|
||||
package org.schabi.newpipe
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import androidx.preference.PreferenceManager
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
class AppModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun providesSharedPreference(@ApplicationContext context: Context): SharedPreferences {
|
||||
return PreferenceManager.getDefaultSharedPreferences(context)
|
||||
}
|
||||
}
|
||||
@@ -10,8 +10,9 @@ import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
|
||||
import icepick.Icepick;
|
||||
import icepick.State;
|
||||
import com.evernote.android.state.State;
|
||||
import com.livefront.bridge.Bridge;
|
||||
|
||||
|
||||
public abstract class BaseFragment extends Fragment {
|
||||
protected final String TAG = getClass().getSimpleName() + "@" + Integer.toHexString(hashCode());
|
||||
@@ -48,7 +49,7 @@ public abstract class BaseFragment extends Fragment {
|
||||
+ "savedInstanceState = [" + savedInstanceState + "]");
|
||||
}
|
||||
super.onCreate(savedInstanceState);
|
||||
Icepick.restoreInstanceState(this, savedInstanceState);
|
||||
Bridge.restoreInstanceState(this, savedInstanceState);
|
||||
if (savedInstanceState != null) {
|
||||
onRestoreInstanceState(savedInstanceState);
|
||||
}
|
||||
@@ -70,7 +71,7 @@ public abstract class BaseFragment extends Fragment {
|
||||
@Override
|
||||
public void onSaveInstanceState(@NonNull final Bundle outState) {
|
||||
super.onSaveInstanceState(outState);
|
||||
Icepick.saveInstanceState(this, outState);
|
||||
Bridge.saveInstanceState(this, outState);
|
||||
}
|
||||
|
||||
protected void onRestoreInstanceState(@NonNull final Bundle savedInstanceState) {
|
||||
|
||||
@@ -29,7 +29,7 @@ import okhttp3.ResponseBody;
|
||||
|
||||
public final class DownloaderImpl extends Downloader {
|
||||
public static final String USER_AGENT =
|
||||
"Mozilla/5.0 (Windows NT 10.0; rv:91.0) Gecko/20100101 Firefox/91.0";
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:128.0) Gecko/20100101 Firefox/128.0";
|
||||
public static final String YOUTUBE_RESTRICTED_MODE_COOKIE_KEY =
|
||||
"youtube_restricted_mode_key";
|
||||
public static final String YOUTUBE_RESTRICTED_MODE_COOKIE = "PREF=f2=8000000";
|
||||
@@ -48,6 +48,11 @@ public final class DownloaderImpl extends Downloader {
|
||||
this.mCookies = new HashMap<>();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public OkHttpClient getClient() {
|
||||
return client;
|
||||
}
|
||||
|
||||
/**
|
||||
* It's recommended to call exactly once in the entire lifetime of the application.
|
||||
*
|
||||
@@ -137,7 +142,8 @@ public final class DownloaderImpl extends Downloader {
|
||||
}
|
||||
|
||||
final okhttp3.Request.Builder requestBuilder = new okhttp3.Request.Builder()
|
||||
.method(httpMethod, requestBody).url(url)
|
||||
.method(httpMethod, requestBody)
|
||||
.url(url)
|
||||
.addHeader("User-Agent", USER_AGENT);
|
||||
|
||||
final String cookies = getCookies(url);
|
||||
@@ -145,38 +151,33 @@ public final class DownloaderImpl extends Downloader {
|
||||
requestBuilder.addHeader("Cookie", cookies);
|
||||
}
|
||||
|
||||
for (final Map.Entry<String, List<String>> pair : headers.entrySet()) {
|
||||
final String headerName = pair.getKey();
|
||||
final List<String> headerValueList = pair.getValue();
|
||||
headers.forEach((headerName, headerValueList) -> {
|
||||
requestBuilder.removeHeader(headerName);
|
||||
headerValueList.forEach(headerValue ->
|
||||
requestBuilder.addHeader(headerName, headerValue));
|
||||
});
|
||||
|
||||
if (headerValueList.size() > 1) {
|
||||
requestBuilder.removeHeader(headerName);
|
||||
for (final String headerValue : headerValueList) {
|
||||
requestBuilder.addHeader(headerName, headerValue);
|
||||
}
|
||||
} else if (headerValueList.size() == 1) {
|
||||
requestBuilder.header(headerName, headerValueList.get(0));
|
||||
try (
|
||||
okhttp3.Response response = client.newCall(requestBuilder.build()).execute()
|
||||
) {
|
||||
if (response.code() == 429) {
|
||||
throw new ReCaptchaException("reCaptcha Challenge requested", url);
|
||||
}
|
||||
|
||||
String responseBodyToReturn = null;
|
||||
try (ResponseBody body = response.body()) {
|
||||
if (body != null) {
|
||||
responseBodyToReturn = body.string();
|
||||
}
|
||||
}
|
||||
|
||||
final String latestUrl = response.request().url().toString();
|
||||
return new Response(
|
||||
response.code(),
|
||||
response.message(),
|
||||
response.headers().toMultimap(),
|
||||
responseBodyToReturn,
|
||||
latestUrl);
|
||||
}
|
||||
|
||||
final okhttp3.Response response = client.newCall(requestBuilder.build()).execute();
|
||||
|
||||
if (response.code() == 429) {
|
||||
response.close();
|
||||
|
||||
throw new ReCaptchaException("reCaptcha Challenge requested", url);
|
||||
}
|
||||
|
||||
final ResponseBody body = response.body();
|
||||
String responseBodyToReturn = null;
|
||||
|
||||
if (body != null) {
|
||||
responseBodyToReturn = body.string();
|
||||
}
|
||||
|
||||
final String latestUrl = response.request().url().toString();
|
||||
return new Response(response.code(), response.message(), response.headers().toMultimap(),
|
||||
responseBodyToReturn, latestUrl);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,13 +38,13 @@ import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.webkit.WebView;
|
||||
import android.widget.AdapterView;
|
||||
import android.widget.ArrayAdapter;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.Spinner;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
import androidx.appcompat.app.ActionBarDrawerToggle;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
@@ -52,7 +52,6 @@ import androidx.core.app.ActivityCompat;
|
||||
import androidx.core.view.GravityCompat;
|
||||
import androidx.drawerlayout.widget.DrawerLayout;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentContainerView;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
import androidx.preference.PreferenceManager;
|
||||
|
||||
@@ -66,13 +65,11 @@ import org.schabi.newpipe.databinding.ToolbarLayoutBinding;
|
||||
import org.schabi.newpipe.error.ErrorUtil;
|
||||
import org.schabi.newpipe.extractor.NewPipe;
|
||||
import org.schabi.newpipe.extractor.StreamingService;
|
||||
import org.schabi.newpipe.extractor.comments.CommentsInfoItem;
|
||||
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||
import org.schabi.newpipe.extractor.services.peertube.PeertubeInstance;
|
||||
import org.schabi.newpipe.fragments.BackPressable;
|
||||
import org.schabi.newpipe.fragments.MainFragment;
|
||||
import org.schabi.newpipe.fragments.detail.VideoDetailFragment;
|
||||
import org.schabi.newpipe.fragments.list.comments.CommentRepliesFragment;
|
||||
import org.schabi.newpipe.fragments.list.search.SearchFragment;
|
||||
import org.schabi.newpipe.local.feed.notifications.NotificationWorker;
|
||||
import org.schabi.newpipe.player.Player;
|
||||
@@ -92,6 +89,7 @@ import org.schabi.newpipe.util.SerializedCache;
|
||||
import org.schabi.newpipe.util.ServiceHelper;
|
||||
import org.schabi.newpipe.util.StateSaver;
|
||||
import org.schabi.newpipe.util.ThemeHelper;
|
||||
import org.schabi.newpipe.util.external_communication.ShareUtils;
|
||||
import org.schabi.newpipe.views.FocusOverlayView;
|
||||
|
||||
import java.util.ArrayList;
|
||||
@@ -120,7 +118,8 @@ public class MainActivity extends AppCompatActivity {
|
||||
private static final int ITEM_ID_DOWNLOADS = -4;
|
||||
private static final int ITEM_ID_HISTORY = -5;
|
||||
private static final int ITEM_ID_SETTINGS = 0;
|
||||
private static final int ITEM_ID_ABOUT = 1;
|
||||
private static final int ITEM_ID_DONATION = 1;
|
||||
private static final int ITEM_ID_ABOUT = 2;
|
||||
|
||||
private static final int ORDER = 0;
|
||||
|
||||
@@ -138,6 +137,19 @@ public class MainActivity extends AppCompatActivity {
|
||||
ThemeHelper.setDayNightMode(this);
|
||||
ThemeHelper.setTheme(this, ServiceHelper.getSelectedServiceId(this));
|
||||
|
||||
// Fixes text color turning black in dark/black mode:
|
||||
// https://github.com/TeamNewPipe/NewPipe/issues/12016
|
||||
// For further reference see: https://issuetracker.google.com/issues/37124582
|
||||
if (DeviceUtils.supportsWebView()) {
|
||||
try {
|
||||
new WebView(this);
|
||||
} catch (final Throwable e) {
|
||||
if (DEBUG) {
|
||||
Log.e(TAG, "Failed to create WebView", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
assureCorrectAppLanguage(this);
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
@@ -170,17 +182,19 @@ public class MainActivity extends AppCompatActivity {
|
||||
NotificationWorker.initialize(this);
|
||||
}
|
||||
if (!UpdateSettingsFragment.wasUserAskedForConsent(this)
|
||||
&& !App.getApp().isFirstRun()
|
||||
&& !App.getInstance().isFirstRun()
|
||||
&& ReleaseVersionUtil.INSTANCE.isReleaseApk()) {
|
||||
UpdateSettingsFragment.askForConsentToUpdateChecks(this);
|
||||
}
|
||||
|
||||
Localization.migrateAppLanguageSettingIfNecessary(getApplicationContext());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostCreate(final Bundle savedInstanceState) {
|
||||
super.onPostCreate(savedInstanceState);
|
||||
|
||||
final App app = App.getApp();
|
||||
final App app = App.getInstance();
|
||||
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(app);
|
||||
|
||||
if (prefs.getBoolean(app.getString(R.string.update_app_key), false)
|
||||
@@ -262,6 +276,10 @@ public class MainActivity extends AppCompatActivity {
|
||||
drawerLayoutBinding.navigation.getMenu()
|
||||
.add(R.id.menu_options_about_group, ITEM_ID_SETTINGS, ORDER, R.string.settings)
|
||||
.setIcon(R.drawable.ic_settings);
|
||||
drawerLayoutBinding.navigation.getMenu()
|
||||
.add(R.id.menu_options_about_group, ITEM_ID_DONATION, ORDER,
|
||||
R.string.donation_title)
|
||||
.setIcon(R.drawable.volunteer_activism_ic);
|
||||
drawerLayoutBinding.navigation.getMenu()
|
||||
.add(R.id.menu_options_about_group, ITEM_ID_ABOUT, ORDER, R.string.tab_about)
|
||||
.setIcon(R.drawable.ic_info_outline);
|
||||
@@ -337,6 +355,9 @@ public class MainActivity extends AppCompatActivity {
|
||||
case ITEM_ID_SETTINGS:
|
||||
NavigationHelper.openSettings(this);
|
||||
break;
|
||||
case ITEM_ID_DONATION:
|
||||
ShareUtils.openUrlInBrowser(this, getString(R.string.donation_url));
|
||||
break;
|
||||
case ITEM_ID_ABOUT:
|
||||
NavigationHelper.openAbout(this);
|
||||
break;
|
||||
@@ -557,39 +578,27 @@ public class MainActivity extends AppCompatActivity {
|
||||
// In case bottomSheet is not visible on the screen or collapsed we can assume that the user
|
||||
// interacts with a fragment inside fragment_holder so all back presses should be
|
||||
// handled by it
|
||||
if (bottomSheetHiddenOrCollapsed()) {
|
||||
final FragmentManager fm = getSupportFragmentManager();
|
||||
final Fragment fragment = fm.findFragmentById(R.id.fragment_holder);
|
||||
// If current fragment implements BackPressable (i.e. can/wanna handle back press)
|
||||
// delegate the back press to it
|
||||
if (fragment instanceof BackPressable) {
|
||||
if (((BackPressable) fragment).onBackPressed()) {
|
||||
return;
|
||||
}
|
||||
} else if (fragment instanceof CommentRepliesFragment) {
|
||||
// expand DetailsFragment if CommentRepliesFragment was opened
|
||||
// to show the top level comments again
|
||||
// Expand DetailsFragment if CommentRepliesFragment was opened
|
||||
// and no other CommentRepliesFragments are on top of the back stack
|
||||
// to show the top level comments again.
|
||||
openDetailFragmentFromCommentReplies(fm, false);
|
||||
}
|
||||
final var fragmentManager = getSupportFragmentManager();
|
||||
|
||||
} else {
|
||||
final Fragment fragmentPlayer = getSupportFragmentManager()
|
||||
.findFragmentById(R.id.fragment_player_holder);
|
||||
if (bottomSheetHiddenOrCollapsed()) {
|
||||
final var fragment = fragmentManager.findFragmentById(R.id.fragment_holder);
|
||||
// If current fragment implements BackPressable (i.e. can/wanna handle back press)
|
||||
// delegate the back press to it
|
||||
if (fragmentPlayer instanceof BackPressable) {
|
||||
if (!((BackPressable) fragmentPlayer).onBackPressed()) {
|
||||
BottomSheetBehavior.from(mainBinding.fragmentPlayerHolder)
|
||||
.setState(BottomSheetBehavior.STATE_COLLAPSED);
|
||||
}
|
||||
if (fragment instanceof BackPressable backPressable && backPressable.onBackPressed()) {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
final var player = fragmentManager.findFragmentById(R.id.fragment_player_holder);
|
||||
// If current fragment implements BackPressable (i.e. can/wanna handle back press)
|
||||
// delegate the back press to it
|
||||
if (player instanceof BackPressable backPressable && !backPressable.onBackPressed()) {
|
||||
BottomSheetBehavior.from(mainBinding.fragmentPlayerHolder)
|
||||
.setState(BottomSheetBehavior.STATE_COLLAPSED);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (getSupportFragmentManager().getBackStackEntryCount() == 1) {
|
||||
if (fragmentManager.getBackStackEntryCount() == 1) {
|
||||
finish();
|
||||
} else {
|
||||
super.onBackPressed();
|
||||
@@ -648,15 +657,9 @@ public class MainActivity extends AppCompatActivity {
|
||||
* </pre>
|
||||
*/
|
||||
private void onHomeButtonPressed() {
|
||||
final FragmentManager fm = getSupportFragmentManager();
|
||||
final Fragment fragment = fm.findFragmentById(R.id.fragment_holder);
|
||||
final var fm = getSupportFragmentManager();
|
||||
|
||||
if (fragment instanceof CommentRepliesFragment) {
|
||||
// Expand DetailsFragment if CommentRepliesFragment was opened
|
||||
// and no other CommentRepliesFragments are on top of the back stack
|
||||
// to show the top level comments again.
|
||||
openDetailFragmentFromCommentReplies(fm, true);
|
||||
} else if (!NavigationHelper.tryGotoSearchFragment(fm)) {
|
||||
if (!NavigationHelper.tryGotoSearchFragment(fm)) {
|
||||
// If search fragment wasn't found in the backstack go to the main fragment
|
||||
NavigationHelper.gotoMainFragment(fm);
|
||||
}
|
||||
@@ -839,7 +842,8 @@ public class MainActivity extends AppCompatActivity {
|
||||
@Override
|
||||
public void onReceive(final Context context, final Intent intent) {
|
||||
if (Objects.equals(intent.getAction(),
|
||||
VideoDetailFragment.ACTION_PLAYER_STARTED)) {
|
||||
VideoDetailFragment.ACTION_PLAYER_STARTED)
|
||||
&& PlayerHolder.getInstance().isPlayerOpen()) {
|
||||
openMiniPlayerIfMissing();
|
||||
// At this point the player is added 100%, we can unregister. Other actions
|
||||
// are useless since the fragment will not be removed after that.
|
||||
@@ -851,71 +855,13 @@ public class MainActivity extends AppCompatActivity {
|
||||
final IntentFilter intentFilter = new IntentFilter();
|
||||
intentFilter.addAction(VideoDetailFragment.ACTION_PLAYER_STARTED);
|
||||
registerReceiver(broadcastReceiver, intentFilter);
|
||||
|
||||
// If the PlayerHolder is not bound yet, but the service is running, try to bind to it.
|
||||
// Once the connection is established, the ACTION_PLAYER_STARTED will be sent.
|
||||
PlayerHolder.getInstance().tryBindIfNeeded(this);
|
||||
}
|
||||
}
|
||||
|
||||
private void openDetailFragmentFromCommentReplies(
|
||||
@NonNull final FragmentManager fm,
|
||||
final boolean popBackStack
|
||||
) {
|
||||
// obtain the name of the fragment under the replies fragment that's going to be popped
|
||||
@Nullable final String fragmentUnderEntryName;
|
||||
if (fm.getBackStackEntryCount() < 2) {
|
||||
fragmentUnderEntryName = null;
|
||||
} else {
|
||||
fragmentUnderEntryName = fm.getBackStackEntryAt(fm.getBackStackEntryCount() - 2)
|
||||
.getName();
|
||||
}
|
||||
|
||||
// the root comment is the comment for which the user opened the replies page
|
||||
@Nullable final CommentRepliesFragment repliesFragment =
|
||||
(CommentRepliesFragment) fm.findFragmentByTag(CommentRepliesFragment.TAG);
|
||||
@Nullable final CommentsInfoItem rootComment =
|
||||
repliesFragment == null ? null : repliesFragment.getCommentsInfoItem();
|
||||
|
||||
// sometimes this function pops the backstack, other times it's handled by the system
|
||||
if (popBackStack) {
|
||||
fm.popBackStackImmediate();
|
||||
}
|
||||
|
||||
// only expand the bottom sheet back if there are no more nested comment replies fragments
|
||||
// stacked under the one that is currently being popped
|
||||
if (CommentRepliesFragment.TAG.equals(fragmentUnderEntryName)) {
|
||||
return;
|
||||
}
|
||||
|
||||
final BottomSheetBehavior<FragmentContainerView> behavior = BottomSheetBehavior
|
||||
.from(mainBinding.fragmentPlayerHolder);
|
||||
// do not return to the comment if the details fragment was closed
|
||||
if (behavior.getState() == BottomSheetBehavior.STATE_HIDDEN) {
|
||||
return;
|
||||
}
|
||||
|
||||
// scroll to the root comment once the bottom sheet expansion animation is finished
|
||||
behavior.addBottomSheetCallback(new BottomSheetBehavior.BottomSheetCallback() {
|
||||
@Override
|
||||
public void onStateChanged(@NonNull final View bottomSheet,
|
||||
final int newState) {
|
||||
if (newState == BottomSheetBehavior.STATE_EXPANDED) {
|
||||
final Fragment detailFragment = fm.findFragmentById(
|
||||
R.id.fragment_player_holder);
|
||||
if (detailFragment instanceof VideoDetailFragment && rootComment != null) {
|
||||
// should always be the case
|
||||
((VideoDetailFragment) detailFragment).scrollToComment(rootComment);
|
||||
}
|
||||
behavior.removeBottomSheetCallback(this);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSlide(@NonNull final View bottomSheet, final float slideOffset) {
|
||||
// not needed, listener is removed once the sheet is expanded
|
||||
}
|
||||
});
|
||||
|
||||
behavior.setState(BottomSheetBehavior.STATE_EXPANDED);
|
||||
}
|
||||
|
||||
private boolean bottomSheetHiddenOrCollapsed() {
|
||||
final BottomSheetBehavior<FrameLayout> bottomSheetBehavior =
|
||||
BottomSheetBehavior.from(mainBinding.fragmentPlayerHolder);
|
||||
@@ -924,4 +870,5 @@ public class MainActivity extends AppCompatActivity {
|
||||
return sheetState == BottomSheetBehavior.STATE_HIDDEN
|
||||
|| sheetState == BottomSheetBehavior.STATE_COLLAPSED;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -41,6 +41,9 @@ import androidx.lifecycle.Lifecycle;
|
||||
import androidx.lifecycle.LifecycleOwner;
|
||||
import androidx.preference.PreferenceManager;
|
||||
|
||||
import com.evernote.android.state.State;
|
||||
import com.livefront.bridge.Bridge;
|
||||
|
||||
import org.schabi.newpipe.database.stream.model.StreamEntity;
|
||||
import org.schabi.newpipe.databinding.ListRadioIconItemBinding;
|
||||
import org.schabi.newpipe.databinding.SingleChoiceDialogViewBinding;
|
||||
@@ -98,8 +101,6 @@ import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import icepick.Icepick;
|
||||
import icepick.State;
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.rxjava3.core.Observable;
|
||||
import io.reactivex.rxjava3.core.Single;
|
||||
@@ -152,7 +153,7 @@ public class RouterActivity extends AppCompatActivity {
|
||||
getWindow().setAttributes(params);
|
||||
|
||||
super.onCreate(savedInstanceState);
|
||||
Icepick.restoreInstanceState(this, savedInstanceState);
|
||||
Bridge.restoreInstanceState(this, savedInstanceState);
|
||||
|
||||
// FragmentManager will take care to recreate (Playlist|Download)Dialog when screen rotates
|
||||
// We used to .setOnDismissListener(dialog -> finish()); when creating these DialogFragments
|
||||
@@ -197,7 +198,7 @@ public class RouterActivity extends AppCompatActivity {
|
||||
@Override
|
||||
protected void onSaveInstanceState(@NonNull final Bundle outState) {
|
||||
super.onSaveInstanceState(outState);
|
||||
Icepick.saveInstanceState(this, outState);
|
||||
Bridge.saveInstanceState(this, outState);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -1,199 +1,31 @@
|
||||
package org.schabi.newpipe.about
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.Button
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import androidx.viewpager2.adapter.FragmentStateAdapter
|
||||
import com.google.android.material.tabs.TabLayoutMediator
|
||||
import org.schabi.newpipe.BuildConfig
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import org.schabi.newpipe.R
|
||||
import org.schabi.newpipe.databinding.ActivityAboutBinding
|
||||
import org.schabi.newpipe.databinding.FragmentAboutBinding
|
||||
import org.schabi.newpipe.ui.components.common.ScaffoldWithToolbar
|
||||
import org.schabi.newpipe.ui.screens.AboutScreen
|
||||
import org.schabi.newpipe.ui.theme.AppTheme
|
||||
import org.schabi.newpipe.util.Localization
|
||||
import org.schabi.newpipe.util.ThemeHelper
|
||||
import org.schabi.newpipe.util.external_communication.ShareUtils
|
||||
|
||||
class AboutActivity : AppCompatActivity() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
Localization.assureCorrectAppLanguage(this)
|
||||
enableEdgeToEdge()
|
||||
super.onCreate(savedInstanceState)
|
||||
ThemeHelper.setTheme(this)
|
||||
title = getString(R.string.title_activity_about)
|
||||
|
||||
val aboutBinding = ActivityAboutBinding.inflate(layoutInflater)
|
||||
setContentView(aboutBinding.root)
|
||||
setSupportActionBar(aboutBinding.aboutToolbar)
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
|
||||
// Create the adapter that will return a fragment for each of the three
|
||||
// primary sections of the activity.
|
||||
val mAboutStateAdapter = AboutStateAdapter(this)
|
||||
// Set up the ViewPager with the sections adapter.
|
||||
aboutBinding.aboutViewPager2.adapter = mAboutStateAdapter
|
||||
TabLayoutMediator(
|
||||
aboutBinding.aboutTabLayout,
|
||||
aboutBinding.aboutViewPager2
|
||||
) { tab, position ->
|
||||
tab.setText(mAboutStateAdapter.getPageTitle(position))
|
||||
}.attach()
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
if (item.itemId == android.R.id.home) {
|
||||
finish()
|
||||
return true
|
||||
}
|
||||
return super.onOptionsItemSelected(item)
|
||||
}
|
||||
|
||||
/**
|
||||
* A placeholder fragment containing a simple view.
|
||||
*/
|
||||
class AboutFragment : Fragment() {
|
||||
private fun Button.openLink(@StringRes url: Int) {
|
||||
setOnClickListener {
|
||||
ShareUtils.openUrlInApp(context, requireContext().getString(url))
|
||||
setContent {
|
||||
AppTheme {
|
||||
ScaffoldWithToolbar(
|
||||
title = stringResource(R.string.title_activity_about),
|
||||
onBackClick = { onBackPressedDispatcher.onBackPressed() }
|
||||
) { padding ->
|
||||
AboutScreen(padding)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
FragmentAboutBinding.inflate(inflater, container, false).apply {
|
||||
aboutAppVersion.text = BuildConfig.VERSION_NAME
|
||||
aboutGithubLink.openLink(R.string.github_url)
|
||||
aboutDonationLink.openLink(R.string.donation_url)
|
||||
aboutWebsiteLink.openLink(R.string.website_url)
|
||||
aboutPrivacyPolicyLink.openLink(R.string.privacy_policy_url)
|
||||
faqLink.openLink(R.string.faq_url)
|
||||
return root
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A [FragmentStateAdapter] that returns a fragment corresponding to
|
||||
* one of the sections/tabs/pages.
|
||||
*/
|
||||
private class AboutStateAdapter(fa: FragmentActivity) : FragmentStateAdapter(fa) {
|
||||
private val posAbout = 0
|
||||
private val posLicense = 1
|
||||
private val totalCount = 2
|
||||
|
||||
override fun createFragment(position: Int): Fragment {
|
||||
return when (position) {
|
||||
posAbout -> AboutFragment()
|
||||
posLicense -> LicenseFragment.newInstance(SOFTWARE_COMPONENTS)
|
||||
else -> throw IllegalArgumentException("Unknown position for ViewPager2")
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int {
|
||||
// Show 2 total pages.
|
||||
return totalCount
|
||||
}
|
||||
|
||||
fun getPageTitle(position: Int): Int {
|
||||
return when (position) {
|
||||
posAbout -> R.string.tab_about
|
||||
posLicense -> R.string.tab_licenses
|
||||
else -> throw IllegalArgumentException("Unknown position for ViewPager2")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* List of all software components.
|
||||
*/
|
||||
private val SOFTWARE_COMPONENTS = arrayListOf(
|
||||
SoftwareComponent(
|
||||
"ACRA", "2013", "Kevin Gaudin",
|
||||
"https://github.com/ACRA/acra", StandardLicenses.APACHE2
|
||||
),
|
||||
SoftwareComponent(
|
||||
"AndroidX", "2005 - 2011", "The Android Open Source Project",
|
||||
"https://developer.android.com/jetpack", StandardLicenses.APACHE2
|
||||
),
|
||||
SoftwareComponent(
|
||||
"ExoPlayer", "2014 - 2020", "Google, Inc.",
|
||||
"https://github.com/google/ExoPlayer", StandardLicenses.APACHE2
|
||||
),
|
||||
SoftwareComponent(
|
||||
"GigaGet", "2014 - 2015", "Peter Cai",
|
||||
"https://github.com/PaperAirplane-Dev-Team/GigaGet", StandardLicenses.GPL3
|
||||
),
|
||||
SoftwareComponent(
|
||||
"Groupie", "2016", "Lisa Wray",
|
||||
"https://github.com/lisawray/groupie", StandardLicenses.MIT
|
||||
),
|
||||
SoftwareComponent(
|
||||
"Icepick", "2015", "Frankie Sardo",
|
||||
"https://github.com/frankiesardo/icepick", StandardLicenses.EPL1
|
||||
),
|
||||
SoftwareComponent(
|
||||
"Jsoup", "2009 - 2020", "Jonathan Hedley",
|
||||
"https://github.com/jhy/jsoup", StandardLicenses.MIT
|
||||
),
|
||||
SoftwareComponent(
|
||||
"Markwon", "2019", "Dimitry Ivanov",
|
||||
"https://github.com/noties/Markwon", StandardLicenses.APACHE2
|
||||
),
|
||||
SoftwareComponent(
|
||||
"Material Components for Android", "2016 - 2020", "Google, Inc.",
|
||||
"https://github.com/material-components/material-components-android",
|
||||
StandardLicenses.APACHE2
|
||||
),
|
||||
SoftwareComponent(
|
||||
"NewPipe Extractor", "2017 - 2020", "Christian Schabesberger",
|
||||
"https://github.com/TeamNewPipe/NewPipeExtractor", StandardLicenses.GPL3
|
||||
),
|
||||
SoftwareComponent(
|
||||
"NoNonsense-FilePicker", "2016", "Jonas Kalderstam",
|
||||
"https://github.com/spacecowboy/NoNonsense-FilePicker", StandardLicenses.MPL2
|
||||
),
|
||||
SoftwareComponent(
|
||||
"OkHttp", "2019", "Square, Inc.",
|
||||
"https://square.github.io/okhttp/", StandardLicenses.APACHE2
|
||||
),
|
||||
SoftwareComponent(
|
||||
"Picasso", "2013", "Square, Inc.",
|
||||
"https://square.github.io/picasso/", StandardLicenses.APACHE2
|
||||
),
|
||||
SoftwareComponent(
|
||||
"PrettyTime", "2012 - 2020", "Lincoln Baxter, III",
|
||||
"https://github.com/ocpsoft/prettytime", StandardLicenses.APACHE2
|
||||
),
|
||||
SoftwareComponent(
|
||||
"ProcessPhoenix", "2015", "Jake Wharton",
|
||||
"https://github.com/JakeWharton/ProcessPhoenix", StandardLicenses.APACHE2
|
||||
),
|
||||
SoftwareComponent(
|
||||
"RxAndroid", "2015", "The RxAndroid authors",
|
||||
"https://github.com/ReactiveX/RxAndroid", StandardLicenses.APACHE2
|
||||
),
|
||||
SoftwareComponent(
|
||||
"RxBinding", "2015", "Jake Wharton",
|
||||
"https://github.com/JakeWharton/RxBinding", StandardLicenses.APACHE2
|
||||
),
|
||||
SoftwareComponent(
|
||||
"RxJava", "2016 - 2020", "RxJava Contributors",
|
||||
"https://github.com/ReactiveX/RxJava", StandardLicenses.APACHE2
|
||||
),
|
||||
SoftwareComponent(
|
||||
"SearchPreference", "2018", "ByteHamster",
|
||||
"https://github.com/ByteHamster/SearchPreference", StandardLicenses.MIT
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
package org.schabi.newpipe.about
|
||||
|
||||
import android.os.Parcelable
|
||||
import kotlinx.parcelize.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
|
||||
@@ -1,140 +0,0 @@
|
||||
package org.schabi.newpipe.about
|
||||
|
||||
import android.os.Bundle
|
||||
import android.util.Base64
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.webkit.WebView
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.fragment.app.Fragment
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.core.Observable
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||
import io.reactivex.rxjava3.disposables.Disposable
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import org.schabi.newpipe.BuildConfig
|
||||
import org.schabi.newpipe.R
|
||||
import org.schabi.newpipe.databinding.FragmentLicensesBinding
|
||||
import org.schabi.newpipe.databinding.ItemSoftwareComponentBinding
|
||||
import org.schabi.newpipe.ktx.parcelableArrayList
|
||||
import org.schabi.newpipe.util.Localization
|
||||
import org.schabi.newpipe.util.external_communication.ShareUtils
|
||||
|
||||
/**
|
||||
* Fragment containing the software licenses.
|
||||
*/
|
||||
class LicenseFragment : Fragment() {
|
||||
private lateinit var softwareComponents: List<SoftwareComponent>
|
||||
private var activeSoftwareComponent: SoftwareComponent? = null
|
||||
private val compositeDisposable = CompositeDisposable()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
softwareComponents = arguments?.parcelableArrayList<SoftwareComponent>(ARG_COMPONENTS)!!
|
||||
.sortedBy { it.name } // Sort components by name
|
||||
activeSoftwareComponent = savedInstanceState?.getSerializable(SOFTWARE_COMPONENT_KEY) as? SoftwareComponent
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
compositeDisposable.dispose()
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
val binding = FragmentLicensesBinding.inflate(inflater, container, false)
|
||||
binding.licensesAppReadLicense.setOnClickListener {
|
||||
compositeDisposable.add(
|
||||
showLicense(NEWPIPE_SOFTWARE_COMPONENT)
|
||||
)
|
||||
}
|
||||
for (component in softwareComponents) {
|
||||
val componentBinding = ItemSoftwareComponentBinding
|
||||
.inflate(inflater, container, false)
|
||||
componentBinding.name.text = component.name
|
||||
componentBinding.copyright.text = getString(
|
||||
R.string.copyright,
|
||||
component.years,
|
||||
component.copyrightOwner,
|
||||
component.license.abbreviation
|
||||
)
|
||||
val root: View = componentBinding.root
|
||||
root.tag = component
|
||||
root.setOnClickListener {
|
||||
compositeDisposable.add(
|
||||
showLicense(component)
|
||||
)
|
||||
}
|
||||
binding.licensesSoftwareComponents.addView(root)
|
||||
registerForContextMenu(root)
|
||||
}
|
||||
activeSoftwareComponent?.let { compositeDisposable.add(showLicense(it)) }
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(savedInstanceState: Bundle) {
|
||||
super.onSaveInstanceState(savedInstanceState)
|
||||
activeSoftwareComponent?.let { savedInstanceState.putSerializable(SOFTWARE_COMPONENT_KEY, it) }
|
||||
}
|
||||
|
||||
private fun showLicense(
|
||||
softwareComponent: SoftwareComponent
|
||||
): Disposable {
|
||||
return if (context == null) {
|
||||
Disposable.empty()
|
||||
} else {
|
||||
val context = requireContext()
|
||||
activeSoftwareComponent = softwareComponent
|
||||
Observable.fromCallable { getFormattedLicense(context, softwareComponent.license) }
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe { formattedLicense ->
|
||||
val webViewData = Base64.encodeToString(
|
||||
formattedLicense.toByteArray(), Base64.NO_PADDING
|
||||
)
|
||||
val webView = WebView(context)
|
||||
webView.loadData(webViewData, "text/html; charset=UTF-8", "base64")
|
||||
|
||||
Localization.assureCorrectAppLanguage(context)
|
||||
val builder = AlertDialog.Builder(requireContext())
|
||||
.setTitle(softwareComponent.name)
|
||||
.setView(webView)
|
||||
.setOnCancelListener { activeSoftwareComponent = null }
|
||||
.setOnDismissListener { activeSoftwareComponent = null }
|
||||
.setPositiveButton(R.string.done) { dialog, _ -> dialog.dismiss() }
|
||||
|
||||
if (softwareComponent != NEWPIPE_SOFTWARE_COMPONENT) {
|
||||
builder.setNeutralButton(R.string.open_website_license) { _, _ ->
|
||||
ShareUtils.openUrlInApp(requireContext(), softwareComponent.link)
|
||||
}
|
||||
}
|
||||
|
||||
builder.show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val ARG_COMPONENTS = "components"
|
||||
private const val SOFTWARE_COMPONENT_KEY = "ACTIVE_SOFTWARE_COMPONENT"
|
||||
private val NEWPIPE_SOFTWARE_COMPONENT = SoftwareComponent(
|
||||
"NewPipe",
|
||||
"2014-2023",
|
||||
"Team NewPipe",
|
||||
"https://newpipe.net/",
|
||||
StandardLicenses.GPL3,
|
||||
BuildConfig.VERSION_NAME
|
||||
)
|
||||
|
||||
fun newInstance(softwareComponents: ArrayList<SoftwareComponent>): LicenseFragment {
|
||||
val fragment = LicenseFragment()
|
||||
fragment.arguments = bundleOf(ARG_COMPONENTS to softwareComponents)
|
||||
return fragment
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
package org.schabi.newpipe.about
|
||||
|
||||
import android.content.Context
|
||||
import org.schabi.newpipe.R
|
||||
import org.schabi.newpipe.util.ThemeHelper
|
||||
import java.io.IOException
|
||||
|
||||
/**
|
||||
* @param context the context to use
|
||||
* @param license the license
|
||||
* @return String which contains a HTML formatted license page
|
||||
* styled according to the context's theme
|
||||
*/
|
||||
fun getFormattedLicense(context: Context, license: License): String {
|
||||
try {
|
||||
return context.assets.open(license.filename).bufferedReader().use { it.readText() }
|
||||
// split the HTML file and insert the stylesheet into the HEAD of the file
|
||||
.replace("</head>", "<style>${getLicenseStylesheet(context)}</style></head>")
|
||||
} catch (e: IOException) {
|
||||
throw IllegalArgumentException("Could not get license file: ${license.filename}", e)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param context the Android context
|
||||
* @return String which is a CSS stylesheet according to the context's theme
|
||||
*/
|
||||
fun getLicenseStylesheet(context: Context): String {
|
||||
val isLightTheme = ThemeHelper.isLightThemeSelected(context)
|
||||
val licenseBackgroundColor = getHexRGBColor(
|
||||
context, if (isLightTheme) R.color.light_license_background_color else R.color.dark_license_background_color
|
||||
)
|
||||
val licenseTextColor = getHexRGBColor(
|
||||
context, if (isLightTheme) R.color.light_license_text_color else R.color.dark_license_text_color
|
||||
)
|
||||
val youtubePrimaryColor = getHexRGBColor(
|
||||
context, if (isLightTheme) R.color.light_youtube_primary_color else R.color.dark_youtube_primary_color
|
||||
)
|
||||
return "body{padding:12px 15px;margin:0;background:#$licenseBackgroundColor;color:#$licenseTextColor}" +
|
||||
"a[href]{color:#$youtubePrimaryColor}pre{white-space:pre-wrap}"
|
||||
}
|
||||
|
||||
/**
|
||||
* Cast R.color to a hexadecimal color value.
|
||||
*
|
||||
* @param context the context to use
|
||||
* @param color the color number from R.color
|
||||
* @return a six characters long String with hexadecimal RGB values
|
||||
*/
|
||||
fun getHexRGBColor(context: Context, color: Int): String {
|
||||
return context.getString(color).substring(3)
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
package org.schabi.newpipe.about
|
||||
|
||||
import android.os.Parcelable
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import java.io.Serializable
|
||||
|
||||
@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, Serializable
|
||||
@@ -1,21 +0,0 @@
|
||||
package org.schabi.newpipe.about
|
||||
|
||||
/**
|
||||
* Class containing information about standard software licenses.
|
||||
*/
|
||||
object StandardLicenses {
|
||||
@JvmField
|
||||
val GPL3 = License("GNU General Public License, Version 3.0", "GPLv3", "gpl_3.html")
|
||||
|
||||
@JvmField
|
||||
val APACHE2 = License("Apache License, Version 2.0", "ALv2", "apache2.html")
|
||||
|
||||
@JvmField
|
||||
val MPL2 = License("Mozilla Public License, Version 2.0", "MPL 2.0", "mpl2.html")
|
||||
|
||||
@JvmField
|
||||
val MIT = License("MIT License", "MIT", "mit.html")
|
||||
|
||||
@JvmField
|
||||
val EPL1 = License("Eclipse Public License, Version 1.0", "EPL 1.0", "epl1.html")
|
||||
}
|
||||
@@ -3,6 +3,8 @@ package org.schabi.newpipe.database.history.model
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Embedded
|
||||
import org.schabi.newpipe.database.stream.model.StreamEntity
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem
|
||||
import org.schabi.newpipe.util.image.ImageStrategy
|
||||
import java.time.OffsetDateTime
|
||||
|
||||
data class StreamHistoryEntry(
|
||||
@@ -27,4 +29,17 @@ data class StreamHistoryEntry(
|
||||
return this.streamEntity.uid == other.streamEntity.uid && streamId == other.streamId &&
|
||||
accessDate.isEqual(other.accessDate)
|
||||
}
|
||||
|
||||
fun toStreamInfoItem(): StreamInfoItem =
|
||||
StreamInfoItem(
|
||||
streamEntity.serviceId,
|
||||
streamEntity.url,
|
||||
streamEntity.title,
|
||||
streamEntity.streamType,
|
||||
).apply {
|
||||
duration = streamEntity.duration
|
||||
uploaderName = streamEntity.uploader
|
||||
uploaderUrl = streamEntity.uploaderUrl
|
||||
thumbnails = ImageStrategy.dbUrlToImageList(streamEntity.thumbnailUrl)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package org.schabi.newpipe.database.playlist;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.schabi.newpipe.database.LocalItem;
|
||||
|
||||
public interface PlaylistLocalItem extends LocalItem {
|
||||
@@ -10,4 +12,7 @@ public interface PlaylistLocalItem extends LocalItem {
|
||||
long getUid();
|
||||
|
||||
void setDisplayIndex(long displayIndex);
|
||||
|
||||
@Nullable
|
||||
String getThumbnailUrl();
|
||||
}
|
||||
|
||||
@@ -9,6 +9,8 @@ import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST
|
||||
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_THUMBNAIL_STREAM_ID;
|
||||
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_THUMBNAIL_URL;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
public class PlaylistMetadataEntry implements PlaylistLocalItem {
|
||||
public static final String PLAYLIST_STREAM_COUNT = "streamCount";
|
||||
|
||||
@@ -71,4 +73,10 @@ public class PlaylistMetadataEntry implements PlaylistLocalItem {
|
||||
public void setDisplayIndex(final long displayIndex) {
|
||||
this.displayIndex = displayIndex;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public String getThumbnailUrl() {
|
||||
return thumbnailUrl;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ public interface PlaylistRemoteDAO extends BasicDAO<PlaylistRemoteEntity> {
|
||||
|
||||
@Query("SELECT * FROM " + REMOTE_PLAYLIST_TABLE + " WHERE "
|
||||
+ REMOTE_PLAYLIST_ID + " = :playlistId")
|
||||
Flowable<List<PlaylistRemoteEntity>> getPlaylist(long playlistId);
|
||||
Flowable<PlaylistRemoteEntity> getPlaylist(long playlistId);
|
||||
|
||||
@Query("SELECT * FROM " + REMOTE_PLAYLIST_TABLE + " WHERE "
|
||||
+ REMOTE_PLAYLIST_URL + " = :url AND " + REMOTE_PLAYLIST_SERVICE_ID + " = :serviceId")
|
||||
|
||||
@@ -154,6 +154,6 @@ public interface PlaylistStreamDAO extends BasicDAO<PlaylistStreamEntity> {
|
||||
+ " AND :streamUrl = :streamUrl"
|
||||
|
||||
+ " GROUP BY " + JOIN_PLAYLIST_ID
|
||||
+ " ORDER BY " + PLAYLIST_DISPLAY_INDEX)
|
||||
+ " ORDER BY " + PLAYLIST_DISPLAY_INDEX + ", " + PLAYLIST_NAME)
|
||||
Flowable<List<PlaylistDuplicatesEntry>> getPlaylistDuplicatesMetadata(String streamUrl);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package org.schabi.newpipe.database.playlist.model;
|
||||
|
||||
import android.text.TextUtils;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.room.ColumnInfo;
|
||||
import androidx.room.Entity;
|
||||
import androidx.room.Ignore;
|
||||
@@ -134,6 +135,8 @@ public class PlaylistRemoteEntity implements PlaylistLocalItem {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public String getThumbnailUrl() {
|
||||
return thumbnailUrl;
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import androidx.room.Query
|
||||
import androidx.room.Transaction
|
||||
import io.reactivex.rxjava3.core.Completable
|
||||
import io.reactivex.rxjava3.core.Flowable
|
||||
import io.reactivex.rxjava3.core.Maybe
|
||||
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
|
||||
@@ -27,7 +28,7 @@ abstract class StreamDAO : BasicDAO<StreamEntity> {
|
||||
abstract override fun listByService(serviceId: Int): Flowable<List<StreamEntity>>
|
||||
|
||||
@Query("SELECT * FROM streams WHERE url = :url AND service_id = :serviceId")
|
||||
abstract fun getStream(serviceId: Long, url: String): Flowable<List<StreamEntity>>
|
||||
abstract fun getStream(serviceId: Long, url: String): Maybe<StreamEntity>
|
||||
|
||||
@Query("UPDATE streams SET uploader_url = :uploaderUrl WHERE url = :url AND service_id = :serviceId")
|
||||
abstract fun setUploaderUrl(serviceId: Long, url: String, uploaderUrl: String): Completable
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
package org.schabi.newpipe.database.stream.dao;
|
||||
|
||||
import static org.schabi.newpipe.database.stream.model.StreamStateEntity.JOIN_STREAM_ID;
|
||||
import static org.schabi.newpipe.database.stream.model.StreamStateEntity.STREAM_STATE_TABLE;
|
||||
|
||||
import androidx.room.Dao;
|
||||
import androidx.room.Insert;
|
||||
import androidx.room.OnConflictStrategy;
|
||||
@@ -12,9 +15,7 @@ import org.schabi.newpipe.database.stream.model.StreamStateEntity;
|
||||
import java.util.List;
|
||||
|
||||
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;
|
||||
import io.reactivex.rxjava3.core.Maybe;
|
||||
|
||||
@Dao
|
||||
public interface StreamStateDAO extends BasicDAO<StreamStateEntity> {
|
||||
@@ -32,7 +33,7 @@ public interface StreamStateDAO extends BasicDAO<StreamStateEntity> {
|
||||
}
|
||||
|
||||
@Query("SELECT * FROM " + STREAM_STATE_TABLE + " WHERE " + JOIN_STREAM_ID + " = :streamId")
|
||||
Flowable<List<StreamStateEntity>> getState(long streamId);
|
||||
Maybe<StreamStateEntity> getState(long streamId);
|
||||
|
||||
@Query("DELETE FROM " + STREAM_STATE_TABLE + " WHERE " + JOIN_STREAM_ID + " = :streamId")
|
||||
int deleteState(long streamId);
|
||||
|
||||
@@ -39,6 +39,8 @@ import androidx.documentfile.provider.DocumentFile;
|
||||
import androidx.fragment.app.DialogFragment;
|
||||
import androidx.preference.PreferenceManager;
|
||||
|
||||
import com.evernote.android.state.State;
|
||||
import com.livefront.bridge.Bridge;
|
||||
import com.nononsenseapps.filepicker.Utils;
|
||||
|
||||
import org.schabi.newpipe.MainActivity;
|
||||
@@ -59,6 +61,8 @@ import org.schabi.newpipe.settings.NewPipeSettings;
|
||||
import org.schabi.newpipe.streams.io.NoFileManagerSafeGuard;
|
||||
import org.schabi.newpipe.streams.io.StoredDirectoryHelper;
|
||||
import org.schabi.newpipe.streams.io.StoredFileHelper;
|
||||
import org.schabi.newpipe.util.AudioTrackAdapter;
|
||||
import org.schabi.newpipe.util.AudioTrackAdapter.AudioTracksWrapper;
|
||||
import org.schabi.newpipe.util.FilePickerActivityHelper;
|
||||
import org.schabi.newpipe.util.FilenameUtils;
|
||||
import org.schabi.newpipe.util.ListHelper;
|
||||
@@ -67,8 +71,6 @@ import org.schabi.newpipe.util.SecondaryStreamHelper;
|
||||
import org.schabi.newpipe.util.SimpleOnSeekBarChangeListener;
|
||||
import org.schabi.newpipe.util.StreamItemAdapter;
|
||||
import org.schabi.newpipe.util.StreamItemAdapter.StreamInfoWrapper;
|
||||
import org.schabi.newpipe.util.AudioTrackAdapter;
|
||||
import org.schabi.newpipe.util.AudioTrackAdapter.AudioTracksWrapper;
|
||||
import org.schabi.newpipe.util.ThemeHelper;
|
||||
|
||||
import java.io.File;
|
||||
@@ -79,8 +81,6 @@ import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
|
||||
import icepick.Icepick;
|
||||
import icepick.State;
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
||||
import us.shandian.giga.get.MissionRecoveryInfo;
|
||||
import us.shandian.giga.postprocessing.Postprocessing;
|
||||
@@ -214,7 +214,7 @@ public class DownloadDialog extends DialogFragment
|
||||
context = getContext();
|
||||
|
||||
setStyle(STYLE_NO_TITLE, ThemeHelper.getDialogTheme(context));
|
||||
Icepick.restoreInstanceState(this, savedInstanceState);
|
||||
Bridge.restoreInstanceState(this, savedInstanceState);
|
||||
|
||||
this.audioTrackAdapter = new AudioTrackAdapter(wrappedAudioTracks);
|
||||
this.subtitleStreamsAdapter = new StreamItemAdapter<>(wrappedSubtitleStreams);
|
||||
@@ -372,7 +372,7 @@ public class DownloadDialog extends DialogFragment
|
||||
@Override
|
||||
public void onSaveInstanceState(@NonNull final Bundle outState) {
|
||||
super.onSaveInstanceState(outState);
|
||||
Icepick.saveInstanceState(this, outState);
|
||||
Bridge.saveInstanceState(this, outState);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -9,7 +9,6 @@ import com.google.auto.service.AutoService;
|
||||
import org.acra.config.CoreConfiguration;
|
||||
import org.acra.sender.ReportSender;
|
||||
import org.acra.sender.ReportSenderFactory;
|
||||
import org.schabi.newpipe.App;
|
||||
|
||||
/*
|
||||
* Created by Christian Schabesberger on 13.09.16.
|
||||
|
||||
@@ -2,7 +2,6 @@ package org.schabi.newpipe.error;
|
||||
|
||||
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
@@ -13,7 +12,6 @@ import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
@@ -22,14 +20,13 @@ import androidx.core.content.IntentCompat;
|
||||
import com.grack.nanojson.JsonWriter;
|
||||
|
||||
import org.schabi.newpipe.BuildConfig;
|
||||
import org.schabi.newpipe.MainActivity;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.databinding.ActivityErrorBinding;
|
||||
import org.schabi.newpipe.util.Localization;
|
||||
import org.schabi.newpipe.util.ThemeHelper;
|
||||
import org.schabi.newpipe.util.external_communication.ShareUtils;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.Arrays;
|
||||
import java.util.stream.Collectors;
|
||||
@@ -70,10 +67,6 @@ public class ErrorActivity extends AppCompatActivity {
|
||||
public static final String ERROR_GITHUB_ISSUE_URL =
|
||||
"https://github.com/TeamNewPipe/NewPipe/issues";
|
||||
|
||||
public static final DateTimeFormatter CURRENT_TIMESTAMP_FORMATTER =
|
||||
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm");
|
||||
|
||||
|
||||
private ErrorInfo errorInfo;
|
||||
private String currentTimeStamp;
|
||||
|
||||
@@ -110,7 +103,9 @@ public class ErrorActivity extends AppCompatActivity {
|
||||
|
||||
// important add guru meditation
|
||||
addGuruMeditation();
|
||||
currentTimeStamp = CURRENT_TIMESTAMP_FORMATTER.format(LocalDateTime.now());
|
||||
// print current time, as zoned ISO8601 timestamp
|
||||
final ZonedDateTime now = ZonedDateTime.now();
|
||||
currentTimeStamp = now.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME);
|
||||
|
||||
activityErrorBinding.errorReportEmailButton.setOnClickListener(v ->
|
||||
openPrivacyPolicyDialog(this, "EMAIL"));
|
||||
@@ -187,25 +182,6 @@ public class ErrorActivity extends AppCompatActivity {
|
||||
.collect(Collectors.joining(separator + "\n", separator + "\n", separator));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the checked activity.
|
||||
*
|
||||
* @param returnActivity the activity to return to
|
||||
* @return the casted return activity or null
|
||||
*/
|
||||
@Nullable
|
||||
static Class<? extends Activity> getReturnActivity(final Class<?> returnActivity) {
|
||||
Class<? extends Activity> checkedReturnActivity = null;
|
||||
if (returnActivity != null) {
|
||||
if (Activity.class.isAssignableFrom(returnActivity)) {
|
||||
checkedReturnActivity = returnActivity.asSubclass(Activity.class);
|
||||
} else {
|
||||
checkedReturnActivity = MainActivity.class;
|
||||
}
|
||||
}
|
||||
return checkedReturnActivity;
|
||||
}
|
||||
|
||||
private void buildInfo(final ErrorInfo info) {
|
||||
String text = "";
|
||||
|
||||
@@ -272,6 +248,9 @@ public class ErrorActivity extends AppCompatActivity {
|
||||
.append("\n* __Content Language:__ ").append(getContentLanguageString())
|
||||
.append("\n* __App Language:__ ").append(getAppLanguage())
|
||||
.append("\n* __Service:__ ").append(errorInfo.getServiceName())
|
||||
.append("\n* __Timestamp:__ ").append(currentTimeStamp)
|
||||
.append("\n* __Package:__ ").append(getPackageName())
|
||||
.append("\n* __Service:__ ").append(errorInfo.getServiceName())
|
||||
.append("\n* __Version:__ ").append(BuildConfig.VERSION_NAME)
|
||||
.append("\n* __OS:__ ").append(getOsString()).append("\n");
|
||||
|
||||
|
||||
@@ -54,7 +54,7 @@ class ErrorUtil {
|
||||
*/
|
||||
@JvmStatic
|
||||
fun showSnackbar(context: Context, errorInfo: ErrorInfo) {
|
||||
val rootView = if (context is Activity) context.findViewById<View>(R.id.content) else null
|
||||
val rootView = (context as? Activity)?.findViewById<View>(android.R.id.content)
|
||||
showSnackbar(context, rootView, errorInfo)
|
||||
}
|
||||
|
||||
@@ -71,7 +71,7 @@ class ErrorUtil {
|
||||
fun showSnackbar(fragment: Fragment, errorInfo: ErrorInfo) {
|
||||
var rootView = fragment.view
|
||||
if (rootView == null && fragment.activity != null) {
|
||||
rootView = fragment.requireActivity().findViewById(R.id.content)
|
||||
rootView = fragment.requireActivity().findViewById(android.R.id.content)
|
||||
}
|
||||
showSnackbar(fragment.requireContext(), rootView, errorInfo)
|
||||
}
|
||||
|
||||
@@ -185,10 +185,8 @@ public class ReCaptchaActivity extends AppCompatActivity {
|
||||
final int abuseEnd = url.indexOf("+path");
|
||||
|
||||
try {
|
||||
String abuseCookie = url.substring(abuseStart + 13, abuseEnd);
|
||||
abuseCookie = Utils.decodeUrlUtf8(abuseCookie);
|
||||
handleCookies(abuseCookie);
|
||||
} catch (IllegalArgumentException | StringIndexOutOfBoundsException e) {
|
||||
handleCookies(Utils.decodeUrlUtf8(url.substring(abuseStart + 13, abuseEnd)));
|
||||
} catch (final StringIndexOutOfBoundsException e) {
|
||||
if (MainActivity.DEBUG) {
|
||||
Log.e(TAG, "handleCookiesFromUrl: invalid google abuse starting at "
|
||||
+ abuseStart + " and ending at " + abuseEnd + " for url " + url, e);
|
||||
|
||||
@@ -13,6 +13,8 @@ import androidx.annotation.Nullable;
|
||||
import androidx.annotation.StringRes;
|
||||
import androidx.fragment.app.Fragment;
|
||||
|
||||
import com.evernote.android.state.State;
|
||||
|
||||
import org.schabi.newpipe.BaseFragment;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.error.ErrorInfo;
|
||||
@@ -22,8 +24,6 @@ import org.schabi.newpipe.util.InfoCache;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
import icepick.State;
|
||||
|
||||
public abstract class BaseStateFragment<I> extends BaseFragment implements ViewContract<I> {
|
||||
@State
|
||||
protected AtomicBoolean wasLoading = new AtomicBoolean();
|
||||
@@ -134,6 +134,7 @@ public abstract class BaseStateFragment<I> extends BaseFragment implements ViewC
|
||||
hideErrorPanel();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void showEmptyState() {
|
||||
isLoading.set(false);
|
||||
if (emptyStateView != null) {
|
||||
|
||||
@@ -6,9 +6,11 @@ import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.compose.ui.platform.ComposeView;
|
||||
|
||||
import org.schabi.newpipe.BaseFragment;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.ui.emptystate.EmptyStateUtil;
|
||||
|
||||
public class EmptyFragment extends BaseFragment {
|
||||
private static final String SHOW_MESSAGE = "SHOW_MESSAGE";
|
||||
@@ -26,8 +28,10 @@ public class EmptyFragment extends BaseFragment {
|
||||
final Bundle savedInstanceState) {
|
||||
final boolean showMessage = getArguments().getBoolean(SHOW_MESSAGE);
|
||||
final View view = inflater.inflate(R.layout.fragment_empty, container, false);
|
||||
view.findViewById(R.id.empty_state_view).setVisibility(
|
||||
showMessage ? View.VISIBLE : View.GONE);
|
||||
|
||||
final ComposeView composeView = view.findViewById(R.id.empty_state_view);
|
||||
EmptyStateUtil.setEmptyStateComposable(composeView);
|
||||
composeView.setVisibility(showMessage ? View.VISIBLE : View.GONE);
|
||||
return view;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -245,10 +245,10 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte
|
||||
// change the background and icon color of the tab layout:
|
||||
// service-colored at the top, app-background-colored at the bottom
|
||||
tabLayout.setBackgroundColor(ThemeHelper.resolveColorFromAttr(requireContext(),
|
||||
bottom ? R.attr.colorSecondary : R.attr.colorPrimary));
|
||||
bottom ? android.R.attr.windowBackground : R.attr.colorPrimary));
|
||||
|
||||
@ColorInt final int iconColor = bottom
|
||||
? ThemeHelper.resolveColorFromAttr(requireContext(), R.attr.colorAccent)
|
||||
? ThemeHelper.resolveColorFromAttr(requireContext(), android.R.attr.colorAccent)
|
||||
: Color.WHITE;
|
||||
tabLayout.setTabRippleColor(ColorStateList.valueOf(iconColor).withAlpha(32));
|
||||
tabLayout.setTabIconTint(ColorStateList.valueOf(iconColor));
|
||||
|
||||
@@ -11,6 +11,8 @@ import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.StringRes;
|
||||
|
||||
import com.evernote.android.state.State;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.extractor.StreamingService;
|
||||
import org.schabi.newpipe.extractor.stream.Description;
|
||||
@@ -19,8 +21,6 @@ import org.schabi.newpipe.util.Localization;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import icepick.State;
|
||||
|
||||
public class DescriptionFragment extends BaseDescriptionFragment {
|
||||
|
||||
@State
|
||||
@@ -31,7 +31,7 @@ public class DescriptionFragment extends BaseDescriptionFragment {
|
||||
}
|
||||
|
||||
public DescriptionFragment() {
|
||||
// keep empty constructor for IcePick when resuming fragment from memory
|
||||
// keep empty constructor for State when resuming fragment from memory
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -56,6 +56,7 @@ import androidx.core.content.ContextCompat;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.preference.PreferenceManager;
|
||||
|
||||
import com.evernote.android.state.State;
|
||||
import com.google.android.exoplayer2.PlaybackException;
|
||||
import com.google.android.exoplayer2.PlaybackParameters;
|
||||
import com.google.android.material.appbar.AppBarLayout;
|
||||
@@ -73,7 +74,6 @@ import org.schabi.newpipe.error.ReCaptchaActivity;
|
||||
import org.schabi.newpipe.error.UserAction;
|
||||
import org.schabi.newpipe.extractor.Image;
|
||||
import org.schabi.newpipe.extractor.NewPipe;
|
||||
import org.schabi.newpipe.extractor.comments.CommentsInfoItem;
|
||||
import org.schabi.newpipe.extractor.exceptions.ContentNotSupportedException;
|
||||
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||
import org.schabi.newpipe.extractor.stream.AudioStream;
|
||||
@@ -116,7 +116,7 @@ import org.schabi.newpipe.util.StreamTypeUtil;
|
||||
import org.schabi.newpipe.util.ThemeHelper;
|
||||
import org.schabi.newpipe.util.external_communication.KoreUtils;
|
||||
import org.schabi.newpipe.util.external_communication.ShareUtils;
|
||||
import org.schabi.newpipe.util.image.PicassoHelper;
|
||||
import org.schabi.newpipe.util.image.CoilHelper;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
@@ -127,7 +127,7 @@ import java.util.Optional;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import icepick.State;
|
||||
import coil3.util.CoilUtils;
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
||||
import io.reactivex.rxjava3.disposables.Disposable;
|
||||
@@ -159,8 +159,6 @@ public final class VideoDetailFragment
|
||||
private static final String DESCRIPTION_TAB_TAG = "DESCRIPTION TAB";
|
||||
private static final String EMPTY_TAB_TAG = "EMPTY TAB";
|
||||
|
||||
private static final String PICASSO_VIDEO_DETAILS_TAG = "PICASSO_VIDEO_DETAILS_TAG";
|
||||
|
||||
// tabs
|
||||
private boolean showComments;
|
||||
private boolean showRelatedItems;
|
||||
@@ -190,21 +188,21 @@ public final class VideoDetailFragment
|
||||
};
|
||||
|
||||
@State
|
||||
protected int serviceId = Constants.NO_SERVICE_ID;
|
||||
int serviceId = Constants.NO_SERVICE_ID;
|
||||
@State
|
||||
@NonNull
|
||||
protected String title = "";
|
||||
String title = "";
|
||||
@State
|
||||
@Nullable
|
||||
protected String url = null;
|
||||
String url = null;
|
||||
@Nullable
|
||||
protected PlayQueue playQueue = null;
|
||||
private PlayQueue playQueue = null;
|
||||
@State
|
||||
int bottomSheetState = BottomSheetBehavior.STATE_EXPANDED;
|
||||
@State
|
||||
int lastStableBottomSheetState = BottomSheetBehavior.STATE_EXPANDED;
|
||||
@State
|
||||
protected boolean autoPlayEnabled = true;
|
||||
boolean autoPlayEnabled = true;
|
||||
|
||||
@Nullable
|
||||
private StreamInfo currentInfo = null;
|
||||
@@ -236,16 +234,19 @@ public final class VideoDetailFragment
|
||||
// Service management
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
@Override
|
||||
public void onServiceConnected(final Player connectedPlayer,
|
||||
final PlayerService connectedPlayerService,
|
||||
final boolean playAfterConnect) {
|
||||
player = connectedPlayer;
|
||||
public void onServiceConnected(@NonNull final PlayerService connectedPlayerService) {
|
||||
playerService = connectedPlayerService;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPlayerConnected(@NonNull final Player connectedPlayer,
|
||||
final boolean playAfterConnect) {
|
||||
player = connectedPlayer;
|
||||
|
||||
// It will do nothing if the player is not in fullscreen mode
|
||||
hideSystemUiIfNeeded();
|
||||
|
||||
final Optional<MainPlayerUi> playerUi = player.UIs().get(MainPlayerUi.class);
|
||||
final Optional<MainPlayerUi> playerUi = player.UIs().getOpt(MainPlayerUi.class);
|
||||
if (!player.videoPlayerSelected() && !playAfterConnect) {
|
||||
return;
|
||||
}
|
||||
@@ -272,22 +273,29 @@ public final class VideoDetailFragment
|
||||
updateOverlayPlayQueueButtonVisibility();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPlayerDisconnected() {
|
||||
player = null;
|
||||
// the binding could be null at this point, if the app is finishing
|
||||
if (binding != null) {
|
||||
restoreDefaultBrightness();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onServiceDisconnected() {
|
||||
playerService = null;
|
||||
player = null;
|
||||
restoreDefaultBrightness();
|
||||
}
|
||||
|
||||
|
||||
/*////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
public static VideoDetailFragment getInstance(final int serviceId,
|
||||
@Nullable final String videoUrl,
|
||||
@Nullable final String url,
|
||||
@NonNull final String name,
|
||||
@Nullable final PlayQueue queue) {
|
||||
final VideoDetailFragment instance = new VideoDetailFragment();
|
||||
instance.setInitialData(serviceId, videoUrl, name, queue);
|
||||
instance.setInitialData(serviceId, url, name, queue);
|
||||
return instance;
|
||||
}
|
||||
|
||||
@@ -430,18 +438,15 @@ public final class VideoDetailFragment
|
||||
@Override
|
||||
public void onActivityResult(final int requestCode, final int resultCode, final Intent data) {
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
switch (requestCode) {
|
||||
case ReCaptchaActivity.RECAPTCHA_REQUEST:
|
||||
if (resultCode == Activity.RESULT_OK) {
|
||||
NavigationHelper.openVideoDetailFragment(requireContext(), getFM(),
|
||||
serviceId, url, title, null, false);
|
||||
} else {
|
||||
Log.e(TAG, "ReCaptcha failed");
|
||||
}
|
||||
break;
|
||||
default:
|
||||
Log.e(TAG, "Request code from activity not supported [" + requestCode + "]");
|
||||
break;
|
||||
if (requestCode == ReCaptchaActivity.RECAPTCHA_REQUEST) {
|
||||
if (resultCode == Activity.RESULT_OK) {
|
||||
NavigationHelper.openVideoDetailFragment(requireContext(), getFM(),
|
||||
serviceId, url, title, null, false);
|
||||
} else {
|
||||
Log.e(TAG, "ReCaptcha failed");
|
||||
}
|
||||
} else {
|
||||
Log.e(TAG, "Request code from activity not supported [" + requestCode + "]");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -521,7 +526,7 @@ public final class VideoDetailFragment
|
||||
binding.overlayPlayPauseButton.setOnClickListener(v -> {
|
||||
if (playerIsNotStopped()) {
|
||||
player.playPause();
|
||||
player.UIs().get(VideoPlayerUi.class).ifPresent(ui -> ui.hideControls(0, 0));
|
||||
player.UIs().getOpt(VideoPlayerUi.class).ifPresent(ui -> ui.hideControls(0, 0));
|
||||
showSystemUi();
|
||||
} else {
|
||||
autoPlayEnabled = true; // forcefully start playing
|
||||
@@ -680,7 +685,7 @@ public final class VideoDetailFragment
|
||||
@Override
|
||||
public boolean onKeyDown(final int keyCode) {
|
||||
return isPlayerAvailable()
|
||||
&& player.UIs().get(VideoPlayerUi.class)
|
||||
&& player.UIs().getOpt(VideoPlayerUi.class)
|
||||
.map(playerUi -> playerUi.onKeyDown(keyCode)).orElse(false);
|
||||
}
|
||||
|
||||
@@ -807,25 +812,17 @@ public final class VideoDetailFragment
|
||||
|
||||
}
|
||||
|
||||
protected void prepareAndLoadInfo() {
|
||||
private void prepareAndLoadInfo() {
|
||||
scrollToTop();
|
||||
startLoading(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void startLoading(final boolean forceLoad) {
|
||||
super.startLoading(forceLoad);
|
||||
|
||||
initTabs();
|
||||
currentInfo = null;
|
||||
if (currentWorker != null) {
|
||||
currentWorker.dispose();
|
||||
}
|
||||
|
||||
runWorker(forceLoad, stack.isEmpty());
|
||||
startLoading(forceLoad, null);
|
||||
}
|
||||
|
||||
private void startLoading(final boolean forceLoad, final boolean addToBackStack) {
|
||||
private void startLoading(final boolean forceLoad, final @Nullable Boolean addToBackStack) {
|
||||
super.startLoading(forceLoad);
|
||||
|
||||
initTabs();
|
||||
@@ -834,7 +831,7 @@ public final class VideoDetailFragment
|
||||
currentWorker.dispose();
|
||||
}
|
||||
|
||||
runWorker(forceLoad, addToBackStack);
|
||||
runWorker(forceLoad, addToBackStack != null ? addToBackStack : stack.isEmpty());
|
||||
}
|
||||
|
||||
private void runWorker(final boolean forceLoad, final boolean addToBackStack) {
|
||||
@@ -882,8 +879,7 @@ public final class VideoDetailFragment
|
||||
tabContentDescriptions.clear();
|
||||
|
||||
if (shouldShowComments()) {
|
||||
pageAdapter.addFragment(
|
||||
CommentsFragment.getInstance(serviceId, url, title), COMMENTS_TAB_TAG);
|
||||
pageAdapter.addFragment(CommentsFragment.getInstance(serviceId, url), COMMENTS_TAB_TAG);
|
||||
tabIcons.add(R.drawable.ic_comment);
|
||||
tabContentDescriptions.add(R.string.comments_tab_description);
|
||||
}
|
||||
@@ -1013,20 +1009,6 @@ public final class VideoDetailFragment
|
||||
updateTabLayoutVisibility();
|
||||
}
|
||||
|
||||
public void scrollToComment(final CommentsInfoItem comment) {
|
||||
final int commentsTabPos = pageAdapter.getItemPositionByTitle(COMMENTS_TAB_TAG);
|
||||
final Fragment fragment = pageAdapter.getItem(commentsTabPos);
|
||||
if (!(fragment instanceof CommentsFragment)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// unexpand the app bar only if scrolling to the comment succeeded
|
||||
if (((CommentsFragment) fragment).scrollToComment(comment)) {
|
||||
binding.appBarLayout.setExpanded(false, false);
|
||||
binding.viewPager.setCurrentItem(commentsTabPos, false);
|
||||
}
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Play Utils
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
@@ -1035,7 +1017,7 @@ public final class VideoDetailFragment
|
||||
// If a user watched video inside fullscreen mode and than chose another player
|
||||
// return to non-fullscreen mode
|
||||
if (isPlayerAvailable()) {
|
||||
player.UIs().get(MainPlayerUi.class).ifPresent(playerUi -> {
|
||||
player.UIs().getOpt(MainPlayerUi.class).ifPresent(playerUi -> {
|
||||
if (playerUi.isFullscreen()) {
|
||||
playerUi.toggleFullscreen();
|
||||
}
|
||||
@@ -1145,7 +1127,7 @@ public final class VideoDetailFragment
|
||||
}
|
||||
|
||||
private void openMainPlayer() {
|
||||
if (!isPlayerServiceAvailable()) {
|
||||
if (noPlayerServiceAvailable()) {
|
||||
playerHolder.startService(autoPlayEnabled, this);
|
||||
return;
|
||||
}
|
||||
@@ -1170,7 +1152,7 @@ public final class VideoDetailFragment
|
||||
*/
|
||||
private void hideMainPlayerOnLoadingNewStream() {
|
||||
final var root = getRoot();
|
||||
if (!isPlayerServiceAvailable() || root.isEmpty() || !player.videoPlayerSelected()) {
|
||||
if (noPlayerServiceAvailable() || root.isEmpty() || !player.videoPlayerSelected()) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1251,7 +1233,7 @@ public final class VideoDetailFragment
|
||||
// setup the surface view height, so that it fits the video correctly
|
||||
setHeightThumbnail();
|
||||
|
||||
player.UIs().get(MainPlayerUi.class).ifPresent(playerUi -> {
|
||||
player.UIs().getOpt(MainPlayerUi.class).ifPresent(playerUi -> {
|
||||
// sometimes binding would be null here, even though getView() != null above u.u
|
||||
if (binding != null) {
|
||||
// prevent from re-adding a view multiple times
|
||||
@@ -1267,7 +1249,7 @@ public final class VideoDetailFragment
|
||||
makeDefaultHeightForVideoPlaceholder();
|
||||
|
||||
if (player != null) {
|
||||
player.UIs().get(VideoPlayerUi.class).ifPresent(VideoPlayerUi::removeViewFromParent);
|
||||
player.UIs().getOpt(VideoPlayerUi.class).ifPresent(VideoPlayerUi::removeViewFromParent);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1334,7 +1316,7 @@ public final class VideoDetailFragment
|
||||
binding.detailThumbnailImageView.setMinimumHeight(newHeight);
|
||||
if (isPlayerAvailable()) {
|
||||
final int maxHeight = (int) (metrics.heightPixels * MAX_PLAYER_HEIGHT);
|
||||
player.UIs().get(VideoPlayerUi.class).ifPresent(ui ->
|
||||
player.UIs().getOpt(VideoPlayerUi.class).ifPresent(ui ->
|
||||
ui.getBinding().surfaceView.setHeights(newHeight,
|
||||
ui.isFullscreen() ? newHeight : maxHeight));
|
||||
}
|
||||
@@ -1344,23 +1326,23 @@ public final class VideoDetailFragment
|
||||
binding.detailContentRootHiding.setVisibility(View.VISIBLE);
|
||||
}
|
||||
|
||||
protected void setInitialData(final int newServiceId,
|
||||
@Nullable final String newUrl,
|
||||
@NonNull final String newTitle,
|
||||
@Nullable final PlayQueue newPlayQueue) {
|
||||
private void setInitialData(final int newServiceId,
|
||||
@Nullable final String newUrl,
|
||||
@NonNull final String newTitle,
|
||||
@Nullable final PlayQueue newPlayQueue) {
|
||||
this.serviceId = newServiceId;
|
||||
this.url = newUrl;
|
||||
this.title = newTitle;
|
||||
this.playQueue = newPlayQueue;
|
||||
}
|
||||
|
||||
private void setErrorImage(final int imageResource) {
|
||||
private void setErrorImage() {
|
||||
if (binding == null || activity == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
binding.detailThumbnailImageView.setImageDrawable(
|
||||
AppCompatResources.getDrawable(requireContext(), imageResource));
|
||||
AppCompatResources.getDrawable(requireContext(), R.drawable.not_available_monkey));
|
||||
animate(binding.detailThumbnailImageView, false, 0, AnimationType.ALPHA,
|
||||
0, () -> animate(binding.detailThumbnailImageView, true, 500));
|
||||
}
|
||||
@@ -1368,7 +1350,7 @@ public final class VideoDetailFragment
|
||||
@Override
|
||||
public void handleError() {
|
||||
super.handleError();
|
||||
setErrorImage(R.drawable.not_available_monkey);
|
||||
setErrorImage();
|
||||
|
||||
if (binding.relatedItemsLayout != null) { // hide related streams for tablets
|
||||
binding.relatedItemsLayout.setVisibility(View.INVISIBLE);
|
||||
@@ -1471,7 +1453,11 @@ public final class VideoDetailFragment
|
||||
}
|
||||
}
|
||||
|
||||
PicassoHelper.cancelTag(PICASSO_VIDEO_DETAILS_TAG);
|
||||
CoilUtils.dispose(binding.detailThumbnailImageView);
|
||||
CoilUtils.dispose(binding.detailSubChannelThumbnailView);
|
||||
CoilUtils.dispose(binding.overlayThumbnail);
|
||||
CoilUtils.dispose(binding.detailUploaderThumbnailView);
|
||||
|
||||
binding.detailThumbnailImageView.setImageBitmap(null);
|
||||
binding.detailSubChannelThumbnailView.setImageBitmap(null);
|
||||
}
|
||||
@@ -1562,8 +1548,8 @@ public final class VideoDetailFragment
|
||||
binding.detailSecondaryControlPanel.setVisibility(View.GONE);
|
||||
|
||||
checkUpdateProgressInfo(info);
|
||||
PicassoHelper.loadDetailsThumbnail(info.getThumbnails()).tag(PICASSO_VIDEO_DETAILS_TAG)
|
||||
.into(binding.detailThumbnailImageView);
|
||||
CoilHelper.INSTANCE.loadDetailsThumbnail(binding.detailThumbnailImageView,
|
||||
info.getThumbnails());
|
||||
showMetaInfoInTextView(info.getMetaInfo(), binding.detailMetaInfoTextView,
|
||||
binding.detailMetaInfoSeparator, disposables);
|
||||
|
||||
@@ -1613,8 +1599,8 @@ public final class VideoDetailFragment
|
||||
binding.detailUploaderTextView.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
PicassoHelper.loadAvatar(info.getUploaderAvatars()).tag(PICASSO_VIDEO_DETAILS_TAG)
|
||||
.into(binding.detailSubChannelThumbnailView);
|
||||
CoilHelper.INSTANCE.loadAvatar(binding.detailSubChannelThumbnailView,
|
||||
info.getUploaderAvatars());
|
||||
binding.detailSubChannelThumbnailView.setVisibility(View.VISIBLE);
|
||||
binding.detailUploaderThumbnailView.setVisibility(View.GONE);
|
||||
}
|
||||
@@ -1645,11 +1631,11 @@ public final class VideoDetailFragment
|
||||
binding.detailUploaderTextView.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
PicassoHelper.loadAvatar(info.getSubChannelAvatars()).tag(PICASSO_VIDEO_DETAILS_TAG)
|
||||
.into(binding.detailSubChannelThumbnailView);
|
||||
CoilHelper.INSTANCE.loadAvatar(binding.detailSubChannelThumbnailView,
|
||||
info.getSubChannelAvatars());
|
||||
binding.detailSubChannelThumbnailView.setVisibility(View.VISIBLE);
|
||||
PicassoHelper.loadAvatar(info.getUploaderAvatars()).tag(PICASSO_VIDEO_DETAILS_TAG)
|
||||
.into(binding.detailUploaderThumbnailView);
|
||||
CoilHelper.INSTANCE.loadAvatar(binding.detailUploaderThumbnailView,
|
||||
info.getUploaderAvatars());
|
||||
binding.detailUploaderThumbnailView.setVisibility(View.VISIBLE);
|
||||
}
|
||||
|
||||
@@ -1736,7 +1722,7 @@ public final class VideoDetailFragment
|
||||
playQueue = queue;
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "onQueueUpdate() called with: serviceId = ["
|
||||
+ serviceId + "], videoUrl = [" + url + "], name = ["
|
||||
+ serviceId + "], url = [" + url + "], name = ["
|
||||
+ title + "], playQueue = [" + playQueue + "]");
|
||||
}
|
||||
|
||||
@@ -1779,16 +1765,14 @@ public final class VideoDetailFragment
|
||||
final PlaybackParameters parameters) {
|
||||
setOverlayPlayPauseImage(player != null && player.isPlaying());
|
||||
|
||||
switch (state) {
|
||||
case Player.STATE_PLAYING:
|
||||
if (binding.positionView.getAlpha() != 1.0f
|
||||
&& player.getPlayQueue() != null
|
||||
&& player.getPlayQueue().getItem() != null
|
||||
&& player.getPlayQueue().getItem().getUrl().equals(url)) {
|
||||
animate(binding.positionView, true, 100);
|
||||
animate(binding.detailPositionView, true, 100);
|
||||
}
|
||||
break;
|
||||
if (state == Player.STATE_PLAYING) {
|
||||
if (binding.positionView.getAlpha() != 1.0f
|
||||
&& player.getPlayQueue() != null
|
||||
&& player.getPlayQueue().getItem() != null
|
||||
&& player.getPlayQueue().getItem().getUrl().equals(url)) {
|
||||
animate(binding.positionView, true, 100);
|
||||
animate(binding.detailPositionView, true, 100);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1848,20 +1832,23 @@ public final class VideoDetailFragment
|
||||
|
||||
@Override
|
||||
public void onServiceStopped() {
|
||||
setOverlayPlayPauseImage(false);
|
||||
if (currentInfo != null) {
|
||||
updateOverlayData(currentInfo.getName(),
|
||||
currentInfo.getUploaderName(),
|
||||
currentInfo.getThumbnails());
|
||||
// the binding could be null at this point, if the app is finishing
|
||||
if (binding != null) {
|
||||
setOverlayPlayPauseImage(false);
|
||||
if (currentInfo != null) {
|
||||
updateOverlayData(currentInfo.getName(),
|
||||
currentInfo.getUploaderName(),
|
||||
currentInfo.getThumbnails());
|
||||
}
|
||||
updateOverlayPlayQueueButtonVisibility();
|
||||
}
|
||||
updateOverlayPlayQueueButtonVisibility();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFullscreenStateChanged(final boolean fullscreen) {
|
||||
setupBrightness();
|
||||
if (!isPlayerAndPlayerServiceAvailable()
|
||||
|| player.UIs().get(MainPlayerUi.class).isEmpty()
|
||||
|| player.UIs().getOpt(MainPlayerUi.class).isEmpty()
|
||||
|| getRoot().map(View::getParent).isEmpty()) {
|
||||
return;
|
||||
}
|
||||
@@ -1890,7 +1877,7 @@ public final class VideoDetailFragment
|
||||
final boolean isLandscape = DeviceUtils.isLandscape(requireContext());
|
||||
if (DeviceUtils.isTablet(activity)
|
||||
&& (!globalScreenOrientationLocked(activity) || isLandscape)) {
|
||||
player.UIs().get(MainPlayerUi.class).ifPresent(MainPlayerUi::toggleFullscreen);
|
||||
player.UIs().getOpt(MainPlayerUi.class).ifPresent(MainPlayerUi::toggleFullscreen);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1990,7 +1977,7 @@ public final class VideoDetailFragment
|
||||
}
|
||||
|
||||
private boolean isFullscreen() {
|
||||
return isPlayerAvailable() && player.UIs().get(VideoPlayerUi.class)
|
||||
return isPlayerAvailable() && player.UIs().getOpt(VideoPlayerUi.class)
|
||||
.map(VideoPlayerUi::isFullscreen).orElse(false);
|
||||
}
|
||||
|
||||
@@ -2067,7 +2054,7 @@ public final class VideoDetailFragment
|
||||
setAutoPlay(true);
|
||||
}
|
||||
|
||||
player.UIs().get(MainPlayerUi.class).ifPresent(MainPlayerUi::checkLandscape);
|
||||
player.UIs().getOpt(MainPlayerUi.class).ifPresent(MainPlayerUi::checkLandscape);
|
||||
// Let's give a user time to look at video information page if video is not playing
|
||||
if (globalScreenOrientationLocked(activity) && !player.isPlaying()) {
|
||||
player.play();
|
||||
@@ -2332,7 +2319,7 @@ public final class VideoDetailFragment
|
||||
&& player.isPlaying()
|
||||
&& !isFullscreen()
|
||||
&& !DeviceUtils.isTablet(activity)) {
|
||||
player.UIs().get(MainPlayerUi.class)
|
||||
player.UIs().getOpt(MainPlayerUi.class)
|
||||
.ifPresent(MainPlayerUi::toggleFullscreen);
|
||||
}
|
||||
setOverlayLook(binding.appBarLayout, behavior, 1);
|
||||
@@ -2346,7 +2333,7 @@ public final class VideoDetailFragment
|
||||
// Re-enable clicks
|
||||
setOverlayElementsClickable(true);
|
||||
if (isPlayerAvailable()) {
|
||||
player.UIs().get(MainPlayerUi.class)
|
||||
player.UIs().getOpt(MainPlayerUi.class)
|
||||
.ifPresent(MainPlayerUi::closeItemsList);
|
||||
}
|
||||
setOverlayLook(binding.appBarLayout, behavior, 0);
|
||||
@@ -2357,7 +2344,7 @@ public final class VideoDetailFragment
|
||||
showSystemUi();
|
||||
}
|
||||
if (isPlayerAvailable()) {
|
||||
player.UIs().get(MainPlayerUi.class).ifPresent(ui -> {
|
||||
player.UIs().getOpt(MainPlayerUi.class).ifPresent(ui -> {
|
||||
if (ui.isControlsVisible()) {
|
||||
ui.hideControls(0, 0);
|
||||
}
|
||||
@@ -2403,8 +2390,7 @@ public final class VideoDetailFragment
|
||||
binding.overlayTitleTextView.setText(isEmpty(overlayTitle) ? "" : overlayTitle);
|
||||
binding.overlayChannelTextView.setText(isEmpty(uploader) ? "" : uploader);
|
||||
binding.overlayThumbnail.setImageDrawable(null);
|
||||
PicassoHelper.loadDetailsThumbnail(thumbnails).tag(PICASSO_VIDEO_DETAILS_TAG)
|
||||
.into(binding.overlayThumbnail);
|
||||
CoilHelper.INSTANCE.loadDetailsThumbnail(binding.overlayThumbnail, thumbnails);
|
||||
}
|
||||
|
||||
private void setOverlayPlayPauseImage(final boolean playerIsPlaying) {
|
||||
@@ -2445,8 +2431,8 @@ public final class VideoDetailFragment
|
||||
return player != null;
|
||||
}
|
||||
|
||||
boolean isPlayerServiceAvailable() {
|
||||
return playerService != null;
|
||||
boolean noPlayerServiceAvailable() {
|
||||
return playerService == null;
|
||||
}
|
||||
|
||||
boolean isPlayerAndPlayerServiceAvailable() {
|
||||
@@ -2455,7 +2441,7 @@ public final class VideoDetailFragment
|
||||
|
||||
public Optional<View> getRoot() {
|
||||
return Optional.ofNullable(player)
|
||||
.flatMap(player1 -> player1.UIs().get(VideoPlayerUi.class))
|
||||
.flatMap(player1 -> player1.UIs().getOpt(VideoPlayerUi.class))
|
||||
.map(playerUi -> playerUi.getBinding().getRoot());
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,8 @@ import android.view.View;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.evernote.android.state.State;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.error.ErrorInfo;
|
||||
import org.schabi.newpipe.error.UserAction;
|
||||
@@ -24,7 +26,6 @@ import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Queue;
|
||||
|
||||
import icepick.State;
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.rxjava3.core.Single;
|
||||
import io.reactivex.rxjava3.disposables.Disposable;
|
||||
@@ -143,7 +144,7 @@ public abstract class BaseListInfoFragment<I extends InfoItem, L extends ListInf
|
||||
currentWorker = loadResult(forceLoad)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe((@NonNull L result) -> {
|
||||
.subscribe((@NonNull final L result) -> {
|
||||
isLoading.set(false);
|
||||
currentInfo = result;
|
||||
currentNextPage = result.getNextPage();
|
||||
|
||||
@@ -10,6 +10,8 @@ import android.widget.LinearLayout;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.evernote.android.state.State;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.extractor.StreamingService;
|
||||
import org.schabi.newpipe.extractor.channel.ChannelInfo;
|
||||
@@ -20,8 +22,6 @@ import org.schabi.newpipe.util.Localization;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import icepick.State;
|
||||
|
||||
public class ChannelAboutFragment extends BaseDescriptionFragment {
|
||||
@State
|
||||
protected ChannelInfo channelInfo;
|
||||
@@ -31,7 +31,7 @@ public class ChannelAboutFragment extends BaseDescriptionFragment {
|
||||
}
|
||||
|
||||
public ChannelAboutFragment() {
|
||||
// keep empty constructor for IcePick when resuming fragment from memory
|
||||
// keep empty constructor for State when resuming fragment from memory
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -10,7 +10,6 @@ import android.graphics.Color;
|
||||
import android.os.Bundle;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
import android.util.TypedValue;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
@@ -22,8 +21,10 @@ import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.core.graphics.ColorUtils;
|
||||
import androidx.core.view.MenuProvider;
|
||||
import androidx.preference.PreferenceManager;
|
||||
|
||||
import com.evernote.android.state.State;
|
||||
import com.google.android.material.snackbar.Snackbar;
|
||||
import com.google.android.material.tabs.TabLayout;
|
||||
import com.jakewharton.rxbinding4.view.RxView;
|
||||
@@ -43,22 +44,24 @@ import org.schabi.newpipe.fragments.detail.TabAdapter;
|
||||
import org.schabi.newpipe.ktx.AnimationType;
|
||||
import org.schabi.newpipe.local.feed.notifications.NotificationHelper;
|
||||
import org.schabi.newpipe.local.subscription.SubscriptionManager;
|
||||
import org.schabi.newpipe.ui.emptystate.EmptyStateSpec;
|
||||
import org.schabi.newpipe.ui.emptystate.EmptyStateUtil;
|
||||
import org.schabi.newpipe.util.ChannelTabHelper;
|
||||
import org.schabi.newpipe.util.Constants;
|
||||
import org.schabi.newpipe.util.ExtractorHelper;
|
||||
import org.schabi.newpipe.util.Localization;
|
||||
import org.schabi.newpipe.util.NavigationHelper;
|
||||
import org.schabi.newpipe.util.StateSaver;
|
||||
import org.schabi.newpipe.util.image.ImageStrategy;
|
||||
import org.schabi.newpipe.util.image.PicassoHelper;
|
||||
import org.schabi.newpipe.util.ThemeHelper;
|
||||
import org.schabi.newpipe.util.external_communication.ShareUtils;
|
||||
import org.schabi.newpipe.util.image.CoilHelper;
|
||||
import org.schabi.newpipe.util.image.ImageStrategy;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Queue;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import icepick.State;
|
||||
import coil3.util.CoilUtils;
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.rxjava3.core.Observable;
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
||||
@@ -72,7 +75,6 @@ public class ChannelFragment extends BaseStateFragment<ChannelInfo>
|
||||
implements StateSaver.WriteRead {
|
||||
|
||||
private static final int BUTTON_DEBOUNCE_INTERVAL = 100;
|
||||
private static final String PICASSO_CHANNEL_TAG = "PICASSO_CHANNEL_TAG";
|
||||
|
||||
@State
|
||||
protected int serviceId = Constants.NO_SERVICE_ID;
|
||||
@@ -99,6 +101,7 @@ public class ChannelFragment extends BaseStateFragment<ChannelInfo>
|
||||
private MenuItem menuRssButton;
|
||||
private MenuItem menuNotifyButton;
|
||||
private SubscriptionEntity channelSubscription;
|
||||
private MenuProvider menuProvider;
|
||||
|
||||
public static ChannelFragment getInstance(final int serviceId, final String url,
|
||||
final String name) {
|
||||
@@ -118,12 +121,6 @@ public class ChannelFragment extends BaseStateFragment<ChannelInfo>
|
||||
// LifeCycle
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public void onCreate(final Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setHasOptionsMenu(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAttach(@NonNull final Context context) {
|
||||
super.onAttach(context);
|
||||
@@ -138,10 +135,76 @@ public class ChannelFragment extends BaseStateFragment<ChannelInfo>
|
||||
return binding.getRoot();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(@NonNull final View rootView, final Bundle savedInstanceState) {
|
||||
super.onViewCreated(rootView, savedInstanceState);
|
||||
menuProvider = new MenuProvider() {
|
||||
@Override
|
||||
public void onCreateMenu(@NonNull final Menu menu,
|
||||
@NonNull final MenuInflater inflater) {
|
||||
inflater.inflate(R.menu.menu_channel, menu);
|
||||
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "onCreateOptionsMenu() called with: "
|
||||
+ "menu = [" + menu + "], inflater = [" + inflater + "]");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPrepareMenu(@NonNull final Menu menu) {
|
||||
menuRssButton = menu.findItem(R.id.menu_item_rss);
|
||||
menuNotifyButton = menu.findItem(R.id.menu_item_notify);
|
||||
updateRssButton();
|
||||
updateNotifyButton(channelSubscription);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onMenuItemSelected(@NonNull final MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case R.id.menu_item_notify:
|
||||
final boolean value = !item.isChecked();
|
||||
item.setEnabled(false);
|
||||
setNotify(value);
|
||||
break;
|
||||
case R.id.action_settings:
|
||||
NavigationHelper.openSettings(requireContext());
|
||||
break;
|
||||
case R.id.menu_item_rss:
|
||||
if (currentInfo != null) {
|
||||
ShareUtils.openUrlInApp(requireContext(), currentInfo.getFeedUrl());
|
||||
}
|
||||
break;
|
||||
case R.id.menu_item_openInBrowser:
|
||||
if (currentInfo != null) {
|
||||
ShareUtils.openUrlInBrowser(requireContext(),
|
||||
currentInfo.getOriginalUrl());
|
||||
}
|
||||
break;
|
||||
case R.id.menu_item_share:
|
||||
if (currentInfo != null) {
|
||||
ShareUtils.shareText(requireContext(), name,
|
||||
currentInfo.getOriginalUrl(), currentInfo.getAvatars());
|
||||
}
|
||||
break;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
};
|
||||
activity.addMenuProvider(menuProvider);
|
||||
}
|
||||
|
||||
@Override // called from onViewCreated in BaseFragment.onViewCreated
|
||||
protected void initViews(final View rootView, final Bundle savedInstanceState) {
|
||||
super.initViews(rootView, savedInstanceState);
|
||||
|
||||
EmptyStateUtil.setEmptyStateComposable(
|
||||
binding.emptyStateView,
|
||||
EmptyStateSpec.Companion.getContentNotSupported()
|
||||
);
|
||||
|
||||
tabAdapter = new TabAdapter(getChildFragmentManager());
|
||||
binding.viewPager.setAdapter(tabAdapter);
|
||||
binding.tabLayout.setupWithViewPager(binding.viewPager);
|
||||
@@ -175,6 +238,14 @@ public class ChannelFragment extends BaseStateFragment<ChannelInfo>
|
||||
binding.subChannelTitleView.setOnClickListener(openSubChannel);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyView() {
|
||||
super.onDestroyView();
|
||||
if (menuProvider != null) {
|
||||
activity.removeMenuProvider(menuProvider);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
@@ -183,73 +254,15 @@ public class ChannelFragment extends BaseStateFragment<ChannelInfo>
|
||||
}
|
||||
disposables.clear();
|
||||
binding = null;
|
||||
menuProvider = null;
|
||||
}
|
||||
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Menu
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public void onCreateOptionsMenu(@NonNull final Menu menu,
|
||||
@NonNull final MenuInflater inflater) {
|
||||
super.onCreateOptionsMenu(menu, inflater);
|
||||
inflater.inflate(R.menu.menu_channel, menu);
|
||||
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "onCreateOptionsMenu() called with: "
|
||||
+ "menu = [" + menu + "], inflater = [" + inflater + "]");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPrepareOptionsMenu(@NonNull final Menu menu) {
|
||||
super.onPrepareOptionsMenu(menu);
|
||||
menuRssButton = menu.findItem(R.id.menu_item_rss);
|
||||
menuNotifyButton = menu.findItem(R.id.menu_item_notify);
|
||||
updateNotifyButton(channelSubscription);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(@NonNull final MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case R.id.menu_item_notify:
|
||||
final boolean value = !item.isChecked();
|
||||
item.setEnabled(false);
|
||||
setNotify(value);
|
||||
break;
|
||||
case R.id.action_settings:
|
||||
NavigationHelper.openSettings(requireContext());
|
||||
break;
|
||||
case R.id.menu_item_rss:
|
||||
if (currentInfo != null) {
|
||||
ShareUtils.openUrlInApp(requireContext(), currentInfo.getFeedUrl());
|
||||
}
|
||||
break;
|
||||
case R.id.menu_item_openInBrowser:
|
||||
if (currentInfo != null) {
|
||||
ShareUtils.openUrlInBrowser(requireContext(), currentInfo.getOriginalUrl());
|
||||
}
|
||||
break;
|
||||
case R.id.menu_item_share:
|
||||
if (currentInfo != null) {
|
||||
ShareUtils.shareText(requireContext(), name, currentInfo.getOriginalUrl(),
|
||||
currentInfo.getAvatars());
|
||||
}
|
||||
break;
|
||||
default:
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Channel Subscription
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
private void monitorSubscription(final ChannelInfo info) {
|
||||
final Consumer<Throwable> onError = (Throwable throwable) -> {
|
||||
final Consumer<Throwable> onError = (final Throwable throwable) -> {
|
||||
animate(binding.channelSubscribeButton, false, 100);
|
||||
showSnackBarError(new ErrorInfo(throwable, UserAction.SUBSCRIPTION_GET,
|
||||
"Get subscription status", currentInfo));
|
||||
@@ -284,14 +297,14 @@ public class ChannelFragment extends BaseStateFragment<ChannelInfo>
|
||||
}
|
||||
|
||||
private Function<Object, Object> mapOnSubscribe(final SubscriptionEntity subscription) {
|
||||
return (@NonNull Object o) -> {
|
||||
return (@NonNull final Object o) -> {
|
||||
subscriptionManager.insertSubscription(subscription);
|
||||
return o;
|
||||
};
|
||||
}
|
||||
|
||||
private Function<Object, Object> mapOnUnsubscribe(final SubscriptionEntity subscription) {
|
||||
return (@NonNull Object o) -> {
|
||||
return (@NonNull final Object o) -> {
|
||||
subscriptionManager.deleteSubscription(subscription);
|
||||
return o;
|
||||
};
|
||||
@@ -318,7 +331,7 @@ public class ChannelFragment extends BaseStateFragment<ChannelInfo>
|
||||
}
|
||||
|
||||
private Disposable monitorSubscribeButton(final Function<Object, Object> action) {
|
||||
final Consumer<Object> onNext = (@NonNull Object o) -> {
|
||||
final Consumer<Object> onNext = (@NonNull final Object o) -> {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "Changed subscription status to this channel!");
|
||||
}
|
||||
@@ -338,7 +351,7 @@ public class ChannelFragment extends BaseStateFragment<ChannelInfo>
|
||||
}
|
||||
|
||||
private Consumer<List<SubscriptionEntity>> getSubscribeUpdateMonitor(final ChannelInfo info) {
|
||||
return (List<SubscriptionEntity> subscriptionEntities) -> {
|
||||
return (final List<SubscriptionEntity> subscriptionEntities) -> {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "subscriptionManager.subscriptionTable.doOnNext() called with: "
|
||||
+ "subscriptionEntities = [" + subscriptionEntities + "]");
|
||||
@@ -408,6 +421,13 @@ public class ChannelFragment extends BaseStateFragment<ChannelInfo>
|
||||
animate(binding.channelSubscribeButton, true, 100, AnimationType.LIGHT_SCALE_AND_ALPHA);
|
||||
}
|
||||
|
||||
private void updateRssButton() {
|
||||
if (menuRssButton == null || currentInfo == null) {
|
||||
return;
|
||||
}
|
||||
menuRssButton.setVisible(!TextUtils.isEmpty(currentInfo.getFeedUrl()));
|
||||
}
|
||||
|
||||
private void updateNotifyButton(@Nullable final SubscriptionEntity subscription) {
|
||||
if (menuNotifyButton == null) {
|
||||
return;
|
||||
@@ -569,7 +589,9 @@ public class ChannelFragment extends BaseStateFragment<ChannelInfo>
|
||||
@Override
|
||||
public void showLoading() {
|
||||
super.showLoading();
|
||||
PicassoHelper.cancelTag(PICASSO_CHANNEL_TAG);
|
||||
CoilUtils.dispose(binding.channelAvatarView);
|
||||
CoilUtils.dispose(binding.channelBannerImage);
|
||||
CoilUtils.dispose(binding.subChannelAvatarView);
|
||||
animate(binding.channelSubscribeButton, false, 100);
|
||||
}
|
||||
|
||||
@@ -580,17 +602,15 @@ public class ChannelFragment extends BaseStateFragment<ChannelInfo>
|
||||
setInitialData(result.getServiceId(), result.getOriginalUrl(), result.getName());
|
||||
|
||||
if (ImageStrategy.shouldLoadImages() && !result.getBanners().isEmpty()) {
|
||||
PicassoHelper.loadBanner(result.getBanners()).tag(PICASSO_CHANNEL_TAG)
|
||||
.into(binding.channelBannerImage);
|
||||
CoilHelper.INSTANCE.loadBanner(binding.channelBannerImage, result.getBanners());
|
||||
} else {
|
||||
// do not waste space for the banner, if the user disabled images or there is not one
|
||||
binding.channelBannerImage.setImageDrawable(null);
|
||||
}
|
||||
|
||||
PicassoHelper.loadAvatar(result.getAvatars()).tag(PICASSO_CHANNEL_TAG)
|
||||
.into(binding.channelAvatarView);
|
||||
PicassoHelper.loadAvatar(result.getParentChannelAvatars()).tag(PICASSO_CHANNEL_TAG)
|
||||
.into(binding.subChannelAvatarView);
|
||||
CoilHelper.INSTANCE.loadAvatar(binding.channelAvatarView, result.getAvatars());
|
||||
CoilHelper.INSTANCE.loadAvatar(binding.subChannelAvatarView,
|
||||
result.getParentChannelAvatars());
|
||||
|
||||
binding.channelTitleView.setText(result.getName());
|
||||
binding.channelSubscriberView.setVisibility(View.VISIBLE);
|
||||
@@ -610,9 +630,7 @@ public class ChannelFragment extends BaseStateFragment<ChannelInfo>
|
||||
binding.subChannelAvatarView.setVisibility(View.VISIBLE);
|
||||
}
|
||||
|
||||
if (menuRssButton != null) {
|
||||
menuRssButton.setVisible(!TextUtils.isEmpty(result.getFeedUrl()));
|
||||
}
|
||||
updateRssButton();
|
||||
|
||||
channelContentNotSupported = false;
|
||||
for (final Throwable throwable : result.getErrors()) {
|
||||
@@ -640,8 +658,6 @@ public class ChannelFragment extends BaseStateFragment<ChannelInfo>
|
||||
return;
|
||||
}
|
||||
|
||||
binding.errorContentNotSupported.setVisibility(View.VISIBLE);
|
||||
binding.channelKaomoji.setText("(︶︹︺)");
|
||||
binding.channelKaomoji.setTextSize(TypedValue.COMPLEX_UNIT_SP, 45f);
|
||||
binding.emptyStateView.setVisibility(View.VISIBLE);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,8 @@ import android.view.ViewGroup;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.evernote.android.state.State;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.databinding.PlaylistControlBinding;
|
||||
import org.schabi.newpipe.error.UserAction;
|
||||
@@ -24,6 +26,7 @@ import org.schabi.newpipe.fragments.list.BaseListInfoFragment;
|
||||
import org.schabi.newpipe.fragments.list.playlist.PlaylistControlViewHolder;
|
||||
import org.schabi.newpipe.player.playqueue.ChannelTabPlayQueue;
|
||||
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
||||
import org.schabi.newpipe.ui.emptystate.EmptyStateUtil;
|
||||
import org.schabi.newpipe.util.ChannelTabHelper;
|
||||
import org.schabi.newpipe.util.ExtractorHelper;
|
||||
import org.schabi.newpipe.util.PlayButtonHelper;
|
||||
@@ -32,13 +35,12 @@ import java.util.List;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import icepick.State;
|
||||
import io.reactivex.rxjava3.core.Single;
|
||||
|
||||
public class ChannelTabFragment extends BaseListInfoFragment<InfoItem, ChannelTabInfo>
|
||||
implements PlaylistControlViewHolder {
|
||||
|
||||
// states must be protected and not private for IcePick being able to access them
|
||||
// states must be protected and not private for State being able to access them
|
||||
@State
|
||||
protected ListLinkHandler tabHandler;
|
||||
@State
|
||||
@@ -78,6 +80,12 @@ public class ChannelTabFragment extends BaseListInfoFragment<InfoItem, ChannelTa
|
||||
return inflater.inflate(R.layout.fragment_channel_tab, container, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(@NonNull final View rootView, final Bundle savedInstanceState) {
|
||||
super.onViewCreated(rootView, savedInstanceState);
|
||||
EmptyStateUtil.setEmptyStateComposable(rootView.findViewById(R.id.empty_state_view));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyView() {
|
||||
super.onDestroyView();
|
||||
@@ -156,6 +164,7 @@ public class ChannelTabFragment extends BaseListInfoFragment<InfoItem, ChannelTa
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public PlayQueue getPlayQueue() {
|
||||
final List<StreamInfoItem> streamItems = infoListAdapter.getItemsList().stream()
|
||||
.filter(StreamInfoItem.class::isInstance)
|
||||
|
||||
@@ -1,170 +0,0 @@
|
||||
package org.schabi.newpipe.fragments.list.comments;
|
||||
|
||||
import static org.schabi.newpipe.util.ServiceHelper.getServiceById;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.constraintlayout.widget.ConstraintLayout;
|
||||
import androidx.core.text.HtmlCompat;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.databinding.CommentRepliesHeaderBinding;
|
||||
import org.schabi.newpipe.error.UserAction;
|
||||
import org.schabi.newpipe.extractor.ListExtractor;
|
||||
import org.schabi.newpipe.extractor.comments.CommentsInfoItem;
|
||||
import org.schabi.newpipe.fragments.list.BaseListInfoFragment;
|
||||
import org.schabi.newpipe.info_list.ItemViewMode;
|
||||
import org.schabi.newpipe.util.DeviceUtils;
|
||||
import org.schabi.newpipe.util.ExtractorHelper;
|
||||
import org.schabi.newpipe.util.Localization;
|
||||
import org.schabi.newpipe.util.NavigationHelper;
|
||||
import org.schabi.newpipe.util.image.ImageStrategy;
|
||||
import org.schabi.newpipe.util.image.PicassoHelper;
|
||||
import org.schabi.newpipe.util.text.TextLinkifier;
|
||||
|
||||
import java.util.Queue;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import icepick.State;
|
||||
import io.reactivex.rxjava3.core.Single;
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
||||
|
||||
public final class CommentRepliesFragment
|
||||
extends BaseListInfoFragment<CommentsInfoItem, CommentRepliesInfo> {
|
||||
|
||||
public static final String TAG = CommentRepliesFragment.class.getSimpleName();
|
||||
|
||||
@State
|
||||
CommentsInfoItem commentsInfoItem; // the comment to show replies of
|
||||
private final CompositeDisposable disposables = new CompositeDisposable();
|
||||
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Constructors and lifecycle
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
// only called by the Android framework, after which readFrom is called and restores all data
|
||||
public CommentRepliesFragment() {
|
||||
super(UserAction.REQUESTED_COMMENT_REPLIES);
|
||||
}
|
||||
|
||||
public CommentRepliesFragment(@NonNull final CommentsInfoItem commentsInfoItem) {
|
||||
this();
|
||||
this.commentsInfoItem = commentsInfoItem;
|
||||
// setting "" as title since the title will be properly set right after
|
||||
setInitialData(commentsInfoItem.getServiceId(), commentsInfoItem.getUrl(), "");
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public View onCreateView(@NonNull final LayoutInflater inflater,
|
||||
@Nullable final ViewGroup container,
|
||||
@Nullable final Bundle savedInstanceState) {
|
||||
return inflater.inflate(R.layout.fragment_comments, container, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyView() {
|
||||
disposables.clear();
|
||||
super.onDestroyView();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Supplier<View> getListHeaderSupplier() {
|
||||
return () -> {
|
||||
final CommentRepliesHeaderBinding binding = CommentRepliesHeaderBinding
|
||||
.inflate(activity.getLayoutInflater(), itemsList, false);
|
||||
final CommentsInfoItem item = commentsInfoItem;
|
||||
|
||||
// load the author avatar
|
||||
PicassoHelper.loadAvatar(item.getUploaderAvatars()).into(binding.authorAvatar);
|
||||
binding.authorAvatar.setVisibility(ImageStrategy.shouldLoadImages()
|
||||
? View.VISIBLE : View.GONE);
|
||||
|
||||
// setup author name and comment date
|
||||
binding.authorName.setText(item.getUploaderName());
|
||||
binding.uploadDate.setText(Localization.relativeTimeOrTextual(
|
||||
getContext(), item.getUploadDate(), item.getTextualUploadDate()));
|
||||
binding.authorTouchArea.setOnClickListener(
|
||||
v -> NavigationHelper.openCommentAuthorIfPresent(requireActivity(), item));
|
||||
|
||||
// setup like count, hearted and pinned
|
||||
binding.thumbsUpCount.setText(
|
||||
Localization.likeCount(requireContext(), item.getLikeCount()));
|
||||
// for heartImage goneMarginEnd was used, but there is no way to tell ConstraintLayout
|
||||
// not to use a different margin only when both the next two views are gone
|
||||
((ConstraintLayout.LayoutParams) binding.thumbsUpCount.getLayoutParams())
|
||||
.setMarginEnd(DeviceUtils.dpToPx(
|
||||
(item.isHeartedByUploader() || item.isPinned() ? 8 : 16),
|
||||
requireContext()));
|
||||
binding.heartImage.setVisibility(item.isHeartedByUploader() ? View.VISIBLE : View.GONE);
|
||||
binding.pinnedImage.setVisibility(item.isPinned() ? View.VISIBLE : View.GONE);
|
||||
|
||||
// setup comment content
|
||||
TextLinkifier.fromDescription(binding.commentContent, item.getCommentText(),
|
||||
HtmlCompat.FROM_HTML_MODE_LEGACY, getServiceById(item.getServiceId()),
|
||||
item.getUrl(), disposables, null);
|
||||
|
||||
return binding.getRoot();
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// State saving
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public void writeTo(final Queue<Object> objectsToSave) {
|
||||
super.writeTo(objectsToSave);
|
||||
objectsToSave.add(commentsInfoItem);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void readFrom(@NonNull final Queue<Object> savedObjects) throws Exception {
|
||||
super.readFrom(savedObjects);
|
||||
commentsInfoItem = (CommentsInfoItem) savedObjects.poll();
|
||||
}
|
||||
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Data loading
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
protected Single<CommentRepliesInfo> loadResult(final boolean forceLoad) {
|
||||
return Single.fromCallable(() -> new CommentRepliesInfo(commentsInfoItem,
|
||||
// the reply count string will be shown as the activity title
|
||||
Localization.replyCount(requireContext(), commentsInfoItem.getReplyCount())));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Single<ListExtractor.InfoItemsPage<CommentsInfoItem>> loadMoreItemsLogic() {
|
||||
// commentsInfoItem.getUrl() should contain the url of the original
|
||||
// ListInfo<CommentsInfoItem>, which should be the stream url
|
||||
return ExtractorHelper.getMoreCommentItems(
|
||||
serviceId, commentsInfoItem.getUrl(), currentNextPage);
|
||||
}
|
||||
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Utils
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
protected ItemViewMode getItemViewMode() {
|
||||
return ItemViewMode.LIST;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the comment to which the replies are shown
|
||||
*/
|
||||
public CommentsInfoItem getCommentsInfoItem() {
|
||||
return commentsInfoItem;
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
package org.schabi.newpipe.fragments.list.comments;
|
||||
|
||||
import org.schabi.newpipe.extractor.ListInfo;
|
||||
import org.schabi.newpipe.extractor.comments.CommentsInfoItem;
|
||||
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler;
|
||||
|
||||
import java.util.Collections;
|
||||
|
||||
public final class CommentRepliesInfo extends ListInfo<CommentsInfoItem> {
|
||||
/**
|
||||
* This class is used to wrap the comment replies page into a ListInfo object.
|
||||
*
|
||||
* @param comment the comment from which to get replies
|
||||
* @param name will be shown as the fragment title
|
||||
*/
|
||||
public CommentRepliesInfo(final CommentsInfoItem comment, final String name) {
|
||||
super(comment.getServiceId(),
|
||||
new ListLinkHandler("", "", "", Collections.emptyList(), null), name);
|
||||
setNextPage(comment.getReplies());
|
||||
setRelatedItems(Collections.emptyList()); // since it must be non-null
|
||||
}
|
||||
}
|
||||
@@ -1,123 +0,0 @@
|
||||
package org.schabi.newpipe.fragments.list.comments;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.error.UserAction;
|
||||
import org.schabi.newpipe.extractor.ListExtractor;
|
||||
import org.schabi.newpipe.extractor.comments.CommentsInfo;
|
||||
import org.schabi.newpipe.extractor.comments.CommentsInfoItem;
|
||||
import org.schabi.newpipe.fragments.list.BaseListInfoFragment;
|
||||
import org.schabi.newpipe.info_list.ItemViewMode;
|
||||
import org.schabi.newpipe.ktx.ViewUtils;
|
||||
import org.schabi.newpipe.util.ExtractorHelper;
|
||||
|
||||
import io.reactivex.rxjava3.core.Single;
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
||||
|
||||
public class CommentsFragment extends BaseListInfoFragment<CommentsInfoItem, CommentsInfo> {
|
||||
private final CompositeDisposable disposables = new CompositeDisposable();
|
||||
|
||||
private TextView emptyStateDesc;
|
||||
|
||||
public static CommentsFragment getInstance(final int serviceId, final String url,
|
||||
final String name) {
|
||||
final CommentsFragment instance = new CommentsFragment();
|
||||
instance.setInitialData(serviceId, url, name);
|
||||
return instance;
|
||||
}
|
||||
|
||||
public CommentsFragment() {
|
||||
super(UserAction.REQUESTED_COMMENTS);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void initViews(final View rootView, final Bundle savedInstanceState) {
|
||||
super.initViews(rootView, savedInstanceState);
|
||||
|
||||
emptyStateDesc = rootView.findViewById(R.id.empty_state_desc);
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// LifeCycle
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public View onCreateView(@NonNull final LayoutInflater inflater,
|
||||
@Nullable final ViewGroup container,
|
||||
@Nullable final Bundle savedInstanceState) {
|
||||
return inflater.inflate(R.layout.fragment_comments, container, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
disposables.clear();
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Load and handle
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
protected Single<ListExtractor.InfoItemsPage<CommentsInfoItem>> loadMoreItemsLogic() {
|
||||
return ExtractorHelper.getMoreCommentItems(serviceId, currentInfo, currentNextPage);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Single<CommentsInfo> loadResult(final boolean forceLoad) {
|
||||
return ExtractorHelper.getCommentsInfo(serviceId, url, forceLoad);
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Contract
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public void handleResult(@NonNull final CommentsInfo result) {
|
||||
super.handleResult(result);
|
||||
|
||||
emptyStateDesc.setText(
|
||||
result.isCommentsDisabled()
|
||||
? R.string.comments_are_disabled
|
||||
: R.string.no_comments);
|
||||
|
||||
ViewUtils.slideUp(requireView(), 120, 150, 0.06f);
|
||||
disposables.clear();
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Utils
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public void setTitle(final String title) { }
|
||||
|
||||
@Override
|
||||
public void onCreateOptionsMenu(@NonNull final Menu menu,
|
||||
@NonNull final MenuInflater inflater) { }
|
||||
|
||||
@Override
|
||||
protected ItemViewMode getItemViewMode() {
|
||||
return ItemViewMode.LIST;
|
||||
}
|
||||
|
||||
public boolean scrollToComment(final CommentsInfoItem comment) {
|
||||
final int position = infoListAdapter.getItemsList().indexOf(comment);
|
||||
if (position < 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
itemsList.scrollToPosition(position);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package org.schabi.newpipe.fragments.list.comments
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.compose.content
|
||||
import org.schabi.newpipe.ui.components.video.comment.CommentSection
|
||||
import org.schabi.newpipe.ui.theme.AppTheme
|
||||
import org.schabi.newpipe.util.KEY_SERVICE_ID
|
||||
import org.schabi.newpipe.util.KEY_URL
|
||||
|
||||
class CommentsFragment : Fragment() {
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
) = content {
|
||||
AppTheme {
|
||||
Surface {
|
||||
CommentSection()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
fun getInstance(serviceId: Int, url: String?) = CommentsFragment().apply {
|
||||
arguments = bundleOf(KEY_SERVICE_ID to serviceId, KEY_URL to url)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,8 @@ import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
|
||||
import com.evernote.android.state.State;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.error.ErrorInfo;
|
||||
import org.schabi.newpipe.error.UserAction;
|
||||
@@ -29,7 +31,6 @@ import org.schabi.newpipe.util.ExtractorHelper;
|
||||
import org.schabi.newpipe.util.KioskTranslator;
|
||||
import org.schabi.newpipe.util.Localization;
|
||||
|
||||
import icepick.State;
|
||||
import io.reactivex.rxjava3.core.Single;
|
||||
|
||||
/**
|
||||
|
||||
@@ -53,7 +53,7 @@ import org.schabi.newpipe.util.Localization;
|
||||
import org.schabi.newpipe.util.NavigationHelper;
|
||||
import org.schabi.newpipe.util.PlayButtonHelper;
|
||||
import org.schabi.newpipe.util.external_communication.ShareUtils;
|
||||
import org.schabi.newpipe.util.image.PicassoHelper;
|
||||
import org.schabi.newpipe.util.image.CoilHelper;
|
||||
import org.schabi.newpipe.util.text.TextEllipsizer;
|
||||
|
||||
import java.util.ArrayList;
|
||||
@@ -62,6 +62,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import coil3.util.CoilUtils;
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.rxjava3.core.Flowable;
|
||||
import io.reactivex.rxjava3.core.Single;
|
||||
@@ -71,8 +72,6 @@ import io.reactivex.rxjava3.disposables.Disposable;
|
||||
public class PlaylistFragment extends BaseListInfoFragment<StreamInfoItem, PlaylistInfo>
|
||||
implements PlaylistControlViewHolder {
|
||||
|
||||
private static final String PICASSO_PLAYLIST_TAG = "PICASSO_PLAYLIST_TAG";
|
||||
|
||||
private CompositeDisposable disposables;
|
||||
private Subscription bookmarkReactor;
|
||||
private AtomicBoolean isBookmarkButtonReady;
|
||||
@@ -276,7 +275,7 @@ public class PlaylistFragment extends BaseListInfoFragment<StreamInfoItem, Playl
|
||||
animate(headerBinding.getRoot(), false, 200);
|
||||
animateHideRecyclerViewAllowingScrolling(itemsList);
|
||||
|
||||
PicassoHelper.cancelTag(PICASSO_PLAYLIST_TAG);
|
||||
CoilUtils.dispose(headerBinding.uploaderAvatarView);
|
||||
animate(headerBinding.uploaderLayout, false, 200);
|
||||
}
|
||||
|
||||
@@ -327,8 +326,8 @@ public class PlaylistFragment extends BaseListInfoFragment<StreamInfoItem, Playl
|
||||
R.drawable.ic_radio)
|
||||
);
|
||||
} else {
|
||||
PicassoHelper.loadAvatar(result.getUploaderAvatars()).tag(PICASSO_PLAYLIST_TAG)
|
||||
.into(headerBinding.uploaderAvatarView);
|
||||
CoilHelper.INSTANCE.loadAvatar(headerBinding.uploaderAvatarView,
|
||||
result.getUploaderAvatars());
|
||||
}
|
||||
|
||||
streamCount = result.getStreamCount();
|
||||
@@ -352,6 +351,7 @@ public class PlaylistFragment extends BaseListInfoFragment<StreamInfoItem, Playl
|
||||
});
|
||||
ellipsizer.setContent(description);
|
||||
headerBinding.playlistDescriptionReadMore.setOnClickListener(v -> ellipsizer.toggle());
|
||||
headerBinding.playlistDescription.setOnClickListener(v -> ellipsizer.toggle());
|
||||
} else {
|
||||
headerBinding.playlistDescription.setVisibility(View.GONE);
|
||||
headerBinding.playlistDescriptionReadMore.setVisibility(View.GONE);
|
||||
|
||||
@@ -40,6 +40,8 @@ import androidx.preference.PreferenceManager;
|
||||
import androidx.recyclerview.widget.ItemTouchHelper;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.evernote.android.state.State;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.databinding.FragmentSearchBinding;
|
||||
import org.schabi.newpipe.error.ErrorInfo;
|
||||
@@ -62,6 +64,8 @@ import org.schabi.newpipe.ktx.AnimationType;
|
||||
import org.schabi.newpipe.ktx.ExceptionUtils;
|
||||
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
||||
import org.schabi.newpipe.settings.NewPipeSettings;
|
||||
import org.schabi.newpipe.ui.emptystate.EmptyStateSpec;
|
||||
import org.schabi.newpipe.ui.emptystate.EmptyStateUtil;
|
||||
import org.schabi.newpipe.util.Constants;
|
||||
import org.schabi.newpipe.util.DeviceUtils;
|
||||
import org.schabi.newpipe.util.ExtractorHelper;
|
||||
@@ -77,7 +81,6 @@ import java.util.Queue;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import icepick.State;
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.rxjava3.core.Observable;
|
||||
import io.reactivex.rxjava3.core.Single;
|
||||
@@ -343,6 +346,10 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
||||
protected void initViews(final View rootView, final Bundle savedInstanceState) {
|
||||
super.initViews(rootView, savedInstanceState);
|
||||
|
||||
EmptyStateUtil.setEmptyStateComposable(
|
||||
searchBinding.emptyStateView,
|
||||
EmptyStateSpec.Companion.getNoSearchResult());
|
||||
|
||||
searchBinding.suggestionsList.setAdapter(suggestionListAdapter);
|
||||
// animations are just strange and useless, since the suggestions keep changing too much
|
||||
searchBinding.suggestionsList.setItemAnimator(null);
|
||||
@@ -550,7 +557,7 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
||||
}
|
||||
});
|
||||
|
||||
searchEditText.setOnFocusChangeListener((View v, boolean hasFocus) -> {
|
||||
searchEditText.setOnFocusChangeListener((final View v, final boolean hasFocus) -> {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "onFocusChange() called with: "
|
||||
+ "v = [" + v + "], hasFocus = [" + hasFocus + "]");
|
||||
@@ -611,7 +618,7 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
||||
};
|
||||
searchEditText.addTextChangedListener(textWatcher);
|
||||
searchEditText.setOnEditorActionListener(
|
||||
(TextView v, int actionId, KeyEvent event) -> {
|
||||
(final TextView v, final int actionId, final KeyEvent event) -> {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "onEditorAction() called with: v = [" + v + "], "
|
||||
+ "actionId = [" + actionId + "], event = [" + event + "]");
|
||||
|
||||
@@ -1,176 +0,0 @@
|
||||
package org.schabi.newpipe.fragments.list.videos;
|
||||
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.preference.PreferenceManager;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.databinding.RelatedItemsHeaderBinding;
|
||||
import org.schabi.newpipe.error.UserAction;
|
||||
import org.schabi.newpipe.extractor.InfoItem;
|
||||
import org.schabi.newpipe.extractor.ListExtractor;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||
import org.schabi.newpipe.fragments.list.BaseListInfoFragment;
|
||||
import org.schabi.newpipe.info_list.ItemViewMode;
|
||||
import org.schabi.newpipe.ktx.ViewUtils;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import io.reactivex.rxjava3.core.Single;
|
||||
|
||||
public class RelatedItemsFragment extends BaseListInfoFragment<InfoItem, RelatedItemsInfo>
|
||||
implements SharedPreferences.OnSharedPreferenceChangeListener {
|
||||
private static final String INFO_KEY = "related_info_key";
|
||||
|
||||
private RelatedItemsInfo relatedItemsInfo;
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Views
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
private RelatedItemsHeaderBinding headerBinding;
|
||||
|
||||
public static RelatedItemsFragment getInstance(final StreamInfo info) {
|
||||
final RelatedItemsFragment instance = new RelatedItemsFragment();
|
||||
instance.setInitialData(info);
|
||||
return instance;
|
||||
}
|
||||
|
||||
public RelatedItemsFragment() {
|
||||
super(UserAction.REQUESTED_STREAM);
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// LifeCycle
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public View onCreateView(@NonNull final LayoutInflater inflater,
|
||||
@Nullable final ViewGroup container,
|
||||
@Nullable final Bundle savedInstanceState) {
|
||||
return inflater.inflate(R.layout.fragment_related_items, container, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyView() {
|
||||
headerBinding = null;
|
||||
super.onDestroyView();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Supplier<View> getListHeaderSupplier() {
|
||||
if (relatedItemsInfo == null || relatedItemsInfo.getRelatedItems() == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
headerBinding = RelatedItemsHeaderBinding
|
||||
.inflate(activity.getLayoutInflater(), itemsList, false);
|
||||
|
||||
final SharedPreferences pref = PreferenceManager
|
||||
.getDefaultSharedPreferences(requireContext());
|
||||
final boolean autoplay = pref.getBoolean(getString(R.string.auto_queue_key), false);
|
||||
headerBinding.autoplaySwitch.setChecked(autoplay);
|
||||
headerBinding.autoplaySwitch.setOnCheckedChangeListener((compoundButton, b) ->
|
||||
PreferenceManager.getDefaultSharedPreferences(requireContext()).edit()
|
||||
.putBoolean(getString(R.string.auto_queue_key), b).apply());
|
||||
|
||||
return headerBinding::getRoot;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Single<ListExtractor.InfoItemsPage<InfoItem>> loadMoreItemsLogic() {
|
||||
return Single.fromCallable(ListExtractor.InfoItemsPage::emptyPage);
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Contract
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
protected Single<RelatedItemsInfo> loadResult(final boolean forceLoad) {
|
||||
return Single.fromCallable(() -> relatedItemsInfo);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void showLoading() {
|
||||
super.showLoading();
|
||||
if (headerBinding != null) {
|
||||
headerBinding.getRoot().setVisibility(View.INVISIBLE);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleResult(@NonNull final RelatedItemsInfo result) {
|
||||
super.handleResult(result);
|
||||
|
||||
if (headerBinding != null) {
|
||||
headerBinding.getRoot().setVisibility(View.VISIBLE);
|
||||
}
|
||||
ViewUtils.slideUp(requireView(), 120, 96, 0.06f);
|
||||
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Utils
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public void setTitle(final String title) {
|
||||
// Nothing to do - override parent
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreateOptionsMenu(@NonNull final Menu menu,
|
||||
@NonNull final MenuInflater inflater) {
|
||||
// Nothing to do - override parent
|
||||
}
|
||||
|
||||
private void setInitialData(final StreamInfo info) {
|
||||
super.setInitialData(info.getServiceId(), info.getUrl(), info.getName());
|
||||
if (this.relatedItemsInfo == null) {
|
||||
this.relatedItemsInfo = new RelatedItemsInfo(info);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSaveInstanceState(@NonNull final Bundle outState) {
|
||||
super.onSaveInstanceState(outState);
|
||||
outState.putSerializable(INFO_KEY, relatedItemsInfo);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onRestoreInstanceState(@NonNull final Bundle savedState) {
|
||||
super.onRestoreInstanceState(savedState);
|
||||
final Serializable serializable = savedState.getSerializable(INFO_KEY);
|
||||
if (serializable instanceof RelatedItemsInfo) {
|
||||
this.relatedItemsInfo = (RelatedItemsInfo) serializable;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSharedPreferenceChanged(final SharedPreferences sharedPreferences,
|
||||
final String key) {
|
||||
if (headerBinding != null && getString(R.string.auto_queue_key).equals(key)) {
|
||||
headerBinding.autoplaySwitch.setChecked(sharedPreferences.getBoolean(key, false));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ItemViewMode getItemViewMode() {
|
||||
ItemViewMode mode = super.getItemViewMode();
|
||||
// Only list mode is supported. Either List or card will be used.
|
||||
if (mode != ItemViewMode.LIST && mode != ItemViewMode.CARD) {
|
||||
mode = ItemViewMode.LIST;
|
||||
}
|
||||
return mode;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package org.schabi.newpipe.fragments.list.videos
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.compose.content
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfo
|
||||
import org.schabi.newpipe.ktx.serializable
|
||||
import org.schabi.newpipe.ui.components.video.RelatedItems
|
||||
import org.schabi.newpipe.ui.theme.AppTheme
|
||||
import org.schabi.newpipe.util.KEY_INFO
|
||||
|
||||
class RelatedItemsFragment : Fragment() {
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
) = content {
|
||||
AppTheme {
|
||||
Surface {
|
||||
RelatedItems(requireArguments().serializable<StreamInfo>(KEY_INFO)!!)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
fun getInstance(info: StreamInfo) = RelatedItemsFragment().apply {
|
||||
arguments = bundleOf(KEY_INFO to info)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
package org.schabi.newpipe.fragments.list.videos;
|
||||
|
||||
import org.schabi.newpipe.extractor.InfoItem;
|
||||
import org.schabi.newpipe.extractor.ListInfo;
|
||||
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
|
||||
public final class RelatedItemsInfo extends ListInfo<InfoItem> {
|
||||
/**
|
||||
* This class is used to wrap the related items of a StreamInfo into a ListInfo object.
|
||||
*
|
||||
* @param info the stream info from which to get related items
|
||||
*/
|
||||
public RelatedItemsInfo(final StreamInfo info) {
|
||||
super(info.getServiceId(), new ListLinkHandler(info.getOriginalUrl(), info.getUrl(),
|
||||
info.getId(), Collections.emptyList(), null), info.getName());
|
||||
setRelatedItems(new ArrayList<>(info.getRelatedItems()));
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,6 @@ import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||
import org.schabi.newpipe.info_list.holder.ChannelInfoItemHolder;
|
||||
import org.schabi.newpipe.info_list.holder.ChannelMiniInfoItemHolder;
|
||||
import org.schabi.newpipe.info_list.holder.CommentInfoItemHolder;
|
||||
import org.schabi.newpipe.info_list.holder.InfoItemHolder;
|
||||
import org.schabi.newpipe.info_list.holder.PlaylistInfoItemHolder;
|
||||
import org.schabi.newpipe.info_list.holder.PlaylistMiniInfoItemHolder;
|
||||
@@ -75,21 +74,16 @@ public class InfoItemBuilder {
|
||||
private InfoItemHolder holderFromInfoType(@NonNull final ViewGroup parent,
|
||||
@NonNull final InfoItem.InfoType infoType,
|
||||
final boolean useMiniVariant) {
|
||||
switch (infoType) {
|
||||
case STREAM:
|
||||
return useMiniVariant ? new StreamMiniInfoItemHolder(this, parent)
|
||||
: new StreamInfoItemHolder(this, parent);
|
||||
case CHANNEL:
|
||||
return useMiniVariant ? new ChannelMiniInfoItemHolder(this, parent)
|
||||
: new ChannelInfoItemHolder(this, parent);
|
||||
case PLAYLIST:
|
||||
return useMiniVariant ? new PlaylistMiniInfoItemHolder(this, parent)
|
||||
: new PlaylistInfoItemHolder(this, parent);
|
||||
case COMMENT:
|
||||
return new CommentInfoItemHolder(this, parent);
|
||||
default:
|
||||
throw new RuntimeException("InfoType not expected = " + infoType.name());
|
||||
}
|
||||
return switch (infoType) {
|
||||
case STREAM -> useMiniVariant ? new StreamMiniInfoItemHolder(this, parent)
|
||||
: new StreamInfoItemHolder(this, parent);
|
||||
case CHANNEL -> useMiniVariant ? new ChannelMiniInfoItemHolder(this, parent)
|
||||
: new ChannelInfoItemHolder(this, parent);
|
||||
case PLAYLIST -> useMiniVariant ? new PlaylistMiniInfoItemHolder(this, parent)
|
||||
: new PlaylistInfoItemHolder(this, parent);
|
||||
case COMMENT ->
|
||||
throw new IllegalArgumentException("Comments should be rendered using Compose");
|
||||
};
|
||||
}
|
||||
|
||||
public Context getContext() {
|
||||
|
||||
@@ -21,7 +21,6 @@ import org.schabi.newpipe.info_list.holder.ChannelCardInfoItemHolder;
|
||||
import org.schabi.newpipe.info_list.holder.ChannelGridInfoItemHolder;
|
||||
import org.schabi.newpipe.info_list.holder.ChannelInfoItemHolder;
|
||||
import org.schabi.newpipe.info_list.holder.ChannelMiniInfoItemHolder;
|
||||
import org.schabi.newpipe.info_list.holder.CommentInfoItemHolder;
|
||||
import org.schabi.newpipe.info_list.holder.InfoItemHolder;
|
||||
import org.schabi.newpipe.info_list.holder.PlaylistCardInfoItemHolder;
|
||||
import org.schabi.newpipe.info_list.holder.PlaylistGridInfoItemHolder;
|
||||
@@ -283,46 +282,32 @@ public class InfoListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolde
|
||||
Log.d(TAG, "onCreateViewHolder() called with: "
|
||||
+ "parent = [" + parent + "], type = [" + type + "]");
|
||||
}
|
||||
switch (type) {
|
||||
return switch (type) {
|
||||
// #4475 and #3368
|
||||
// Always create a new instance otherwise the same instance
|
||||
// is sometimes reused which causes a crash
|
||||
case HEADER_TYPE:
|
||||
return new HFHolder(headerSupplier.get());
|
||||
case FOOTER_TYPE:
|
||||
return new HFHolder(PignateFooterBinding
|
||||
.inflate(layoutInflater, parent, false)
|
||||
.getRoot()
|
||||
);
|
||||
case MINI_STREAM_HOLDER_TYPE:
|
||||
return new StreamMiniInfoItemHolder(infoItemBuilder, parent);
|
||||
case STREAM_HOLDER_TYPE:
|
||||
return new StreamInfoItemHolder(infoItemBuilder, parent);
|
||||
case GRID_STREAM_HOLDER_TYPE:
|
||||
return new StreamGridInfoItemHolder(infoItemBuilder, parent);
|
||||
case CARD_STREAM_HOLDER_TYPE:
|
||||
return new StreamCardInfoItemHolder(infoItemBuilder, parent);
|
||||
case MINI_CHANNEL_HOLDER_TYPE:
|
||||
return new ChannelMiniInfoItemHolder(infoItemBuilder, parent);
|
||||
case CHANNEL_HOLDER_TYPE:
|
||||
return new ChannelInfoItemHolder(infoItemBuilder, parent);
|
||||
case CARD_CHANNEL_HOLDER_TYPE:
|
||||
return new ChannelCardInfoItemHolder(infoItemBuilder, parent);
|
||||
case GRID_CHANNEL_HOLDER_TYPE:
|
||||
return new ChannelGridInfoItemHolder(infoItemBuilder, parent);
|
||||
case MINI_PLAYLIST_HOLDER_TYPE:
|
||||
return new PlaylistMiniInfoItemHolder(infoItemBuilder, parent);
|
||||
case PLAYLIST_HOLDER_TYPE:
|
||||
return new PlaylistInfoItemHolder(infoItemBuilder, parent);
|
||||
case GRID_PLAYLIST_HOLDER_TYPE:
|
||||
return new PlaylistGridInfoItemHolder(infoItemBuilder, parent);
|
||||
case CARD_PLAYLIST_HOLDER_TYPE:
|
||||
return new PlaylistCardInfoItemHolder(infoItemBuilder, parent);
|
||||
case COMMENT_HOLDER_TYPE:
|
||||
return new CommentInfoItemHolder(infoItemBuilder, parent);
|
||||
default:
|
||||
return new FallbackViewHolder(new View(parent.getContext()));
|
||||
}
|
||||
case HEADER_TYPE -> new HFHolder(headerSupplier.get());
|
||||
case FOOTER_TYPE -> new HFHolder(PignateFooterBinding
|
||||
.inflate(layoutInflater, parent, false)
|
||||
.getRoot()
|
||||
);
|
||||
case MINI_STREAM_HOLDER_TYPE -> new StreamMiniInfoItemHolder(infoItemBuilder, parent);
|
||||
case STREAM_HOLDER_TYPE -> new StreamInfoItemHolder(infoItemBuilder, parent);
|
||||
case GRID_STREAM_HOLDER_TYPE -> new StreamGridInfoItemHolder(infoItemBuilder, parent);
|
||||
case CARD_STREAM_HOLDER_TYPE -> new StreamCardInfoItemHolder(infoItemBuilder, parent);
|
||||
case MINI_CHANNEL_HOLDER_TYPE -> new ChannelMiniInfoItemHolder(infoItemBuilder, parent);
|
||||
case CHANNEL_HOLDER_TYPE -> new ChannelInfoItemHolder(infoItemBuilder, parent);
|
||||
case CARD_CHANNEL_HOLDER_TYPE -> new ChannelCardInfoItemHolder(infoItemBuilder, parent);
|
||||
case GRID_CHANNEL_HOLDER_TYPE -> new ChannelGridInfoItemHolder(infoItemBuilder, parent);
|
||||
case MINI_PLAYLIST_HOLDER_TYPE ->
|
||||
new PlaylistMiniInfoItemHolder(infoItemBuilder, parent);
|
||||
case PLAYLIST_HOLDER_TYPE -> new PlaylistInfoItemHolder(infoItemBuilder, parent);
|
||||
case GRID_PLAYLIST_HOLDER_TYPE ->
|
||||
new PlaylistGridInfoItemHolder(infoItemBuilder, parent);
|
||||
case CARD_PLAYLIST_HOLDER_TYPE ->
|
||||
new PlaylistCardInfoItemHolder(infoItemBuilder, parent);
|
||||
default -> new FallbackViewHolder(new View(parent.getContext()));
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -1,19 +1,18 @@
|
||||
package org.schabi.newpipe.info_list
|
||||
|
||||
import android.view.View
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import com.xwray.groupie.GroupieViewHolder
|
||||
import com.xwray.groupie.Item
|
||||
import com.xwray.groupie.viewbinding.BindableItem
|
||||
import com.xwray.groupie.viewbinding.GroupieViewHolder
|
||||
import org.schabi.newpipe.R
|
||||
import org.schabi.newpipe.databinding.ItemStreamSegmentBinding
|
||||
import org.schabi.newpipe.extractor.stream.StreamSegment
|
||||
import org.schabi.newpipe.util.Localization
|
||||
import org.schabi.newpipe.util.image.PicassoHelper
|
||||
import org.schabi.newpipe.util.image.CoilHelper
|
||||
|
||||
class StreamSegmentItem(
|
||||
private val item: StreamSegment,
|
||||
private val onClick: StreamSegmentAdapter.StreamSegmentListener
|
||||
) : Item<GroupieViewHolder>() {
|
||||
) : BindableItem<ItemStreamSegmentBinding>() {
|
||||
|
||||
companion object {
|
||||
const val PAYLOAD_SELECT = 1
|
||||
@@ -21,31 +20,32 @@ class StreamSegmentItem(
|
||||
|
||||
var isSelected = false
|
||||
|
||||
override fun bind(viewHolder: GroupieViewHolder, position: Int) {
|
||||
item.previewUrl?.let {
|
||||
PicassoHelper.loadThumbnail(it)
|
||||
.into(viewHolder.root.findViewById<ImageView>(R.id.previewImage))
|
||||
}
|
||||
viewHolder.root.findViewById<TextView>(R.id.textViewTitle).text = item.title
|
||||
override fun bind(viewBinding: ItemStreamSegmentBinding, position: Int) {
|
||||
CoilHelper.loadThumbnail(viewBinding.previewImage, item.previewUrl)
|
||||
viewBinding.textViewTitle.text = item.title
|
||||
if (item.channelName == null) {
|
||||
viewHolder.root.findViewById<TextView>(R.id.textViewChannel).visibility = View.GONE
|
||||
viewBinding.textViewChannel.visibility = View.GONE
|
||||
// When the channel name is displayed there is less space
|
||||
// and thus the segment title needs to be only one line height.
|
||||
// But when there is no channel name displayed, the title can be two lines long.
|
||||
// The default maxLines value is set to 1 to display all elements in the AS preview,
|
||||
viewHolder.root.findViewById<TextView>(R.id.textViewTitle).maxLines = 2
|
||||
viewBinding.textViewTitle.maxLines = 2
|
||||
} else {
|
||||
viewHolder.root.findViewById<TextView>(R.id.textViewChannel).text = item.channelName
|
||||
viewHolder.root.findViewById<TextView>(R.id.textViewChannel).visibility = View.VISIBLE
|
||||
viewBinding.textViewChannel.text = item.channelName
|
||||
viewBinding.textViewChannel.visibility = View.VISIBLE
|
||||
}
|
||||
viewHolder.root.findViewById<TextView>(R.id.textViewStartSeconds).text =
|
||||
viewBinding.textViewStartSeconds.text =
|
||||
Localization.getDurationString(item.startTimeSeconds.toLong())
|
||||
viewHolder.root.setOnClickListener { onClick.onItemClick(this, item.startTimeSeconds) }
|
||||
viewHolder.root.setOnLongClickListener { onClick.onItemLongClick(this, item.startTimeSeconds); true }
|
||||
viewHolder.root.isSelected = isSelected
|
||||
viewBinding.root.setOnClickListener { onClick.onItemClick(this, item.startTimeSeconds) }
|
||||
viewBinding.root.setOnLongClickListener { onClick.onItemLongClick(this, item.startTimeSeconds); true }
|
||||
viewBinding.root.isSelected = isSelected
|
||||
}
|
||||
|
||||
override fun bind(viewHolder: GroupieViewHolder, position: Int, payloads: MutableList<Any>) {
|
||||
override fun bind(
|
||||
viewHolder: GroupieViewHolder<ItemStreamSegmentBinding>,
|
||||
position: Int,
|
||||
payloads: MutableList<Any>
|
||||
) {
|
||||
if (payloads.contains(PAYLOAD_SELECT)) {
|
||||
viewHolder.root.isSelected = isSelected
|
||||
return
|
||||
@@ -54,4 +54,6 @@ class StreamSegmentItem(
|
||||
}
|
||||
|
||||
override fun getLayout() = R.layout.item_stream_segment
|
||||
|
||||
override fun initializeViewBinding(view: View) = ItemStreamSegmentBinding.bind(view)
|
||||
}
|
||||
|
||||
@@ -346,7 +346,7 @@ public final class InfoItemDialog {
|
||||
|
||||
public static void reportErrorDuringInitialization(final Throwable throwable,
|
||||
final InfoItem item) {
|
||||
ErrorUtil.showSnackbar(App.getApp().getBaseContext(), new ErrorInfo(
|
||||
ErrorUtil.showSnackbar(App.getInstance().getBaseContext(), new ErrorInfo(
|
||||
throwable,
|
||||
UserAction.OPEN_INFO_ITEM_DIALOG,
|
||||
"none",
|
||||
|
||||
@@ -41,10 +41,11 @@ import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||
* </p>
|
||||
*/
|
||||
public enum StreamDialogDefaultEntry {
|
||||
SHOW_CHANNEL_DETAILS(R.string.show_channel_details, (fragment, item) ->
|
||||
fetchUploaderUrlIfSparse(fragment.requireContext(), item.getServiceId(), item.getUrl(),
|
||||
item.getUploaderUrl(), url -> openChannelFragment(fragment, item, url))
|
||||
),
|
||||
SHOW_CHANNEL_DETAILS(R.string.show_channel_details, (fragment, item) -> {
|
||||
final var activity = fragment.requireActivity();
|
||||
fetchUploaderUrlIfSparse(activity, item.getServiceId(), item.getUrl(),
|
||||
item.getUploaderUrl(), url -> openChannelFragment(activity, item, url));
|
||||
}),
|
||||
|
||||
/**
|
||||
* Enqueues the stream automatically to the current PlayerType.
|
||||
@@ -113,7 +114,10 @@ public enum StreamDialogDefaultEntry {
|
||||
DOWNLOAD(R.string.download, (fragment, item) ->
|
||||
fetchStreamInfoAndSaveToDatabase(fragment.requireContext(), item.getServiceId(),
|
||||
item.getUrl(), info -> {
|
||||
if (fragment.getContext() != null) {
|
||||
// Ensure the fragment is attached and its state hasn't been saved to avoid
|
||||
// showing dialog during lifecycle changes or when the activity is paused,
|
||||
// e.g. by selecting the download option and opening a different fragment.
|
||||
if (fragment.isAdded() && !fragment.isStateSaved()) {
|
||||
final DownloadDialog downloadDialog =
|
||||
new DownloadDialog(fragment.requireContext(), info);
|
||||
downloadDialog.show(fragment.getChildFragmentManager(),
|
||||
|
||||
@@ -13,8 +13,8 @@ import org.schabi.newpipe.extractor.channel.ChannelInfoItem;
|
||||
import org.schabi.newpipe.extractor.utils.Utils;
|
||||
import org.schabi.newpipe.info_list.InfoItemBuilder;
|
||||
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
||||
import org.schabi.newpipe.util.image.PicassoHelper;
|
||||
import org.schabi.newpipe.util.Localization;
|
||||
import org.schabi.newpipe.util.image.CoilHelper;
|
||||
|
||||
public class ChannelMiniInfoItemHolder extends InfoItemHolder {
|
||||
private final ImageView itemThumbnailView;
|
||||
@@ -56,7 +56,7 @@ public class ChannelMiniInfoItemHolder extends InfoItemHolder {
|
||||
itemAdditionalDetailView.setText(getDetailLine(item));
|
||||
}
|
||||
|
||||
PicassoHelper.loadAvatar(item.getThumbnails()).into(itemThumbnailView);
|
||||
CoilHelper.INSTANCE.loadAvatar(itemThumbnailView, item.getThumbnails());
|
||||
|
||||
itemView.setOnClickListener(view -> {
|
||||
if (itemBuilder.getOnChannelSelectedListener() != null) {
|
||||
|
||||
@@ -1,188 +0,0 @@
|
||||
package org.schabi.newpipe.info_list.holder;
|
||||
|
||||
import static org.schabi.newpipe.util.ServiceHelper.getServiceById;
|
||||
|
||||
import android.text.method.LinkMovementMethod;
|
||||
import android.text.style.URLSpan;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.Button;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.RelativeLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.fragment.app.FragmentActivity;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.extractor.InfoItem;
|
||||
import org.schabi.newpipe.extractor.comments.CommentsInfoItem;
|
||||
import org.schabi.newpipe.info_list.InfoItemBuilder;
|
||||
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
||||
import org.schabi.newpipe.util.DeviceUtils;
|
||||
import org.schabi.newpipe.util.Localization;
|
||||
import org.schabi.newpipe.util.NavigationHelper;
|
||||
import org.schabi.newpipe.util.external_communication.ShareUtils;
|
||||
import org.schabi.newpipe.util.image.ImageStrategy;
|
||||
import org.schabi.newpipe.util.image.PicassoHelper;
|
||||
import org.schabi.newpipe.util.text.CommentTextOnTouchListener;
|
||||
import org.schabi.newpipe.util.text.TextEllipsizer;
|
||||
|
||||
public class CommentInfoItemHolder extends InfoItemHolder {
|
||||
|
||||
private static final int COMMENT_DEFAULT_LINES = 2;
|
||||
private final int commentHorizontalPadding;
|
||||
private final int commentVerticalPadding;
|
||||
|
||||
private final RelativeLayout itemRoot;
|
||||
private final ImageView itemThumbnailView;
|
||||
private final TextView itemContentView;
|
||||
private final ImageView itemThumbsUpView;
|
||||
private final TextView itemLikesCountView;
|
||||
private final TextView itemTitleView;
|
||||
private final ImageView itemHeartView;
|
||||
private final ImageView itemPinnedView;
|
||||
private final Button repliesButton;
|
||||
|
||||
@NonNull
|
||||
private final TextEllipsizer textEllipsizer;
|
||||
|
||||
public CommentInfoItemHolder(final InfoItemBuilder infoItemBuilder,
|
||||
final ViewGroup parent) {
|
||||
super(infoItemBuilder, R.layout.list_comment_item, parent);
|
||||
|
||||
itemRoot = itemView.findViewById(R.id.itemRoot);
|
||||
itemThumbnailView = itemView.findViewById(R.id.itemThumbnailView);
|
||||
itemContentView = itemView.findViewById(R.id.itemCommentContentView);
|
||||
itemThumbsUpView = itemView.findViewById(R.id.detail_thumbs_up_img_view);
|
||||
itemLikesCountView = itemView.findViewById(R.id.detail_thumbs_up_count_view);
|
||||
itemTitleView = itemView.findViewById(R.id.itemTitleView);
|
||||
itemHeartView = itemView.findViewById(R.id.detail_heart_image_view);
|
||||
itemPinnedView = itemView.findViewById(R.id.detail_pinned_view);
|
||||
repliesButton = itemView.findViewById(R.id.replies_button);
|
||||
|
||||
commentHorizontalPadding = (int) infoItemBuilder.getContext()
|
||||
.getResources().getDimension(R.dimen.comments_horizontal_padding);
|
||||
commentVerticalPadding = (int) infoItemBuilder.getContext()
|
||||
.getResources().getDimension(R.dimen.comments_vertical_padding);
|
||||
|
||||
textEllipsizer = new TextEllipsizer(itemContentView, COMMENT_DEFAULT_LINES, null);
|
||||
textEllipsizer.setStateChangeListener(isEllipsized -> {
|
||||
if (Boolean.TRUE.equals(isEllipsized)) {
|
||||
denyLinkFocus();
|
||||
} else {
|
||||
determineMovementMethod();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateFromItem(final InfoItem infoItem,
|
||||
final HistoryRecordManager historyRecordManager) {
|
||||
if (!(infoItem instanceof CommentsInfoItem)) {
|
||||
return;
|
||||
}
|
||||
final CommentsInfoItem item = (CommentsInfoItem) infoItem;
|
||||
|
||||
|
||||
// load the author avatar
|
||||
PicassoHelper.loadAvatar(item.getUploaderAvatars()).into(itemThumbnailView);
|
||||
if (ImageStrategy.shouldLoadImages()) {
|
||||
itemThumbnailView.setVisibility(View.VISIBLE);
|
||||
itemRoot.setPadding(commentVerticalPadding, commentVerticalPadding,
|
||||
commentVerticalPadding, commentVerticalPadding);
|
||||
} else {
|
||||
itemThumbnailView.setVisibility(View.GONE);
|
||||
itemRoot.setPadding(commentHorizontalPadding, commentVerticalPadding,
|
||||
commentHorizontalPadding, commentVerticalPadding);
|
||||
}
|
||||
itemThumbnailView.setOnClickListener(view -> openCommentAuthor(item));
|
||||
|
||||
|
||||
// setup the top row, with pinned icon, author name and comment date
|
||||
itemPinnedView.setVisibility(item.isPinned() ? View.VISIBLE : View.GONE);
|
||||
itemTitleView.setText(Localization.concatenateStrings(item.getUploaderName(),
|
||||
Localization.relativeTimeOrTextual(itemBuilder.getContext(), item.getUploadDate(),
|
||||
item.getTextualUploadDate())));
|
||||
|
||||
|
||||
// setup bottom row, with likes, heart and replies button
|
||||
itemLikesCountView.setText(
|
||||
Localization.likeCount(itemBuilder.getContext(), item.getLikeCount()));
|
||||
|
||||
itemHeartView.setVisibility(item.isHeartedByUploader() ? View.VISIBLE : View.GONE);
|
||||
|
||||
final boolean hasReplies = item.getReplies() != null;
|
||||
repliesButton.setOnClickListener(hasReplies ? v -> openCommentReplies(item) : null);
|
||||
repliesButton.setVisibility(hasReplies ? View.VISIBLE : View.GONE);
|
||||
repliesButton.setText(hasReplies
|
||||
? Localization.replyCount(itemBuilder.getContext(), item.getReplyCount()) : "");
|
||||
((RelativeLayout.LayoutParams) itemThumbsUpView.getLayoutParams()).topMargin =
|
||||
hasReplies ? 0 : DeviceUtils.dpToPx(6, itemBuilder.getContext());
|
||||
|
||||
|
||||
// setup comment content and click listeners to expand/ellipsize it
|
||||
textEllipsizer.setStreamingService(getServiceById(item.getServiceId()));
|
||||
textEllipsizer.setStreamUrl(item.getUrl());
|
||||
textEllipsizer.setContent(item.getCommentText());
|
||||
textEllipsizer.ellipsize();
|
||||
|
||||
//noinspection ClickableViewAccessibility
|
||||
itemContentView.setOnTouchListener(CommentTextOnTouchListener.INSTANCE);
|
||||
|
||||
itemView.setOnClickListener(view -> {
|
||||
textEllipsizer.toggle();
|
||||
if (itemBuilder.getOnCommentsSelectedListener() != null) {
|
||||
itemBuilder.getOnCommentsSelectedListener().selected(item);
|
||||
}
|
||||
});
|
||||
|
||||
itemView.setOnLongClickListener(view -> {
|
||||
if (DeviceUtils.isTv(itemBuilder.getContext())) {
|
||||
openCommentAuthor(item);
|
||||
} else {
|
||||
final CharSequence text = itemContentView.getText();
|
||||
if (text != null) {
|
||||
ShareUtils.copyToClipboard(itemBuilder.getContext(), text.toString());
|
||||
}
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
private void openCommentAuthor(@NonNull final CommentsInfoItem item) {
|
||||
NavigationHelper.openCommentAuthorIfPresent((FragmentActivity) itemBuilder.getContext(),
|
||||
item);
|
||||
}
|
||||
|
||||
private void openCommentReplies(@NonNull final CommentsInfoItem item) {
|
||||
NavigationHelper.openCommentRepliesFragment((FragmentActivity) itemBuilder.getContext(),
|
||||
item);
|
||||
}
|
||||
|
||||
private void allowLinkFocus() {
|
||||
itemContentView.setMovementMethod(LinkMovementMethod.getInstance());
|
||||
}
|
||||
|
||||
private void denyLinkFocus() {
|
||||
itemContentView.setMovementMethod(null);
|
||||
}
|
||||
|
||||
private boolean shouldFocusLinks() {
|
||||
if (itemView.isInTouchMode()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final URLSpan[] urls = itemContentView.getUrls();
|
||||
|
||||
return urls != null && urls.length != 0;
|
||||
}
|
||||
|
||||
private void determineMovementMethod() {
|
||||
if (shouldFocusLinks()) {
|
||||
allowLinkFocus();
|
||||
} else {
|
||||
denyLinkFocus();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,8 +9,8 @@ import org.schabi.newpipe.extractor.InfoItem;
|
||||
import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem;
|
||||
import org.schabi.newpipe.info_list.InfoItemBuilder;
|
||||
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
||||
import org.schabi.newpipe.util.image.PicassoHelper;
|
||||
import org.schabi.newpipe.util.Localization;
|
||||
import org.schabi.newpipe.util.image.CoilHelper;
|
||||
|
||||
public class PlaylistMiniInfoItemHolder extends InfoItemHolder {
|
||||
public final ImageView itemThumbnailView;
|
||||
@@ -46,7 +46,7 @@ public class PlaylistMiniInfoItemHolder extends InfoItemHolder {
|
||||
.localizeStreamCountMini(itemStreamCountView.getContext(), item.getStreamCount()));
|
||||
itemUploaderView.setText(item.getUploaderName());
|
||||
|
||||
PicassoHelper.loadPlaylistThumbnail(item.getThumbnails()).into(itemThumbnailView);
|
||||
CoilHelper.INSTANCE.loadPlaylistThumbnail(itemThumbnailView, item.getThumbnails());
|
||||
|
||||
itemView.setOnClickListener(view -> {
|
||||
if (itemBuilder.getOnPlaylistSelectedListener() != null) {
|
||||
|
||||
@@ -16,8 +16,8 @@ import org.schabi.newpipe.ktx.ViewUtils;
|
||||
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
||||
import org.schabi.newpipe.util.DependentPreferenceHelper;
|
||||
import org.schabi.newpipe.util.Localization;
|
||||
import org.schabi.newpipe.util.image.PicassoHelper;
|
||||
import org.schabi.newpipe.util.StreamTypeUtil;
|
||||
import org.schabi.newpipe.util.image.CoilHelper;
|
||||
import org.schabi.newpipe.views.AnimatedProgressBar;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
@@ -64,8 +64,7 @@ public class StreamMiniInfoItemHolder extends InfoItemHolder {
|
||||
StreamStateEntity state2 = null;
|
||||
if (DependentPreferenceHelper
|
||||
.getPositionsInListsEnabled(itemProgressView.getContext())) {
|
||||
state2 = historyRecordManager.loadStreamState(infoItem)
|
||||
.blockingGet()[0];
|
||||
state2 = historyRecordManager.loadStreamState(infoItem).blockingGet();
|
||||
}
|
||||
if (state2 != null) {
|
||||
itemProgressView.setVisibility(View.VISIBLE);
|
||||
@@ -87,7 +86,7 @@ public class StreamMiniInfoItemHolder extends InfoItemHolder {
|
||||
}
|
||||
|
||||
// Default thumbnail is shown on error, while loading and if the url is empty
|
||||
PicassoHelper.loadThumbnail(item.getThumbnails()).into(itemThumbnailView);
|
||||
CoilHelper.INSTANCE.loadThumbnail(itemThumbnailView, item.getThumbnails());
|
||||
|
||||
itemView.setOnClickListener(view -> {
|
||||
if (itemBuilder.getOnStreamSelectedListener() != null) {
|
||||
@@ -120,7 +119,7 @@ public class StreamMiniInfoItemHolder extends InfoItemHolder {
|
||||
if (DependentPreferenceHelper.getPositionsInListsEnabled(itemProgressView.getContext())) {
|
||||
state = historyRecordManager
|
||||
.loadStreamState(infoItem)
|
||||
.blockingGet()[0];
|
||||
.blockingGet();
|
||||
}
|
||||
if (state != null && item.getDuration() > 0
|
||||
&& !StreamTypeUtil.isLiveStream(item.getStreamType())) {
|
||||
|
||||
13
app/src/main/java/org/schabi/newpipe/ktx/Bitmap.kt
Normal file
13
app/src/main/java/org/schabi/newpipe/ktx/Bitmap.kt
Normal file
@@ -0,0 +1,13 @@
|
||||
package org.schabi.newpipe.ktx
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Rect
|
||||
import androidx.core.graphics.BitmapCompat
|
||||
|
||||
@Suppress("NOTHING_TO_INLINE")
|
||||
inline fun Bitmap.scale(
|
||||
width: Int,
|
||||
height: Int,
|
||||
srcRect: Rect? = null,
|
||||
scaleInLinearSpace: Boolean = true,
|
||||
) = BitmapCompat.createScaledBitmap(this, width, height, srcRect, scaleInLinearSpace)
|
||||
@@ -1,9 +1,22 @@
|
||||
package org.schabi.newpipe.ktx
|
||||
|
||||
import android.os.Bundle
|
||||
import android.os.Parcelable
|
||||
import androidx.core.os.BundleCompat
|
||||
import java.io.Serializable
|
||||
|
||||
inline fun <reified T : Parcelable> Bundle.parcelableArrayList(key: String?): ArrayList<T>? {
|
||||
return BundleCompat.getParcelableArrayList(this, key, T::class.java)
|
||||
inline fun <reified T : Serializable> Bundle.serializable(key: String?): T? {
|
||||
return BundleCompat.getSerializable(this, key, T::class.java)
|
||||
}
|
||||
|
||||
fun Bundle?.toDebugString(): String {
|
||||
if (this == null) {
|
||||
return "null"
|
||||
}
|
||||
val string = StringBuilder("Bundle{")
|
||||
for (key in this.keySet()) {
|
||||
@Suppress("DEPRECATION") // we want this[key] to return items of any type
|
||||
string.append(" ").append(key).append(" => ").append(this[key]).append(";")
|
||||
}
|
||||
string.append(" }")
|
||||
return string.toString()
|
||||
}
|
||||
|
||||
13
app/src/main/java/org/schabi/newpipe/ktx/Context.kt
Normal file
13
app/src/main/java/org/schabi/newpipe/ktx/Context.kt
Normal file
@@ -0,0 +1,13 @@
|
||||
package org.schabi.newpipe.ktx
|
||||
|
||||
import android.content.Context
|
||||
import android.content.ContextWrapper
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
|
||||
tailrec fun Context.findFragmentActivity(): FragmentActivity {
|
||||
return when (this) {
|
||||
is FragmentActivity -> this
|
||||
is ContextWrapper -> baseContext.findFragmentActivity()
|
||||
else -> throw IllegalStateException("Unable to find FragmentActivity")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package org.schabi.newpipe.ktx
|
||||
|
||||
import android.content.SharedPreferences
|
||||
|
||||
fun SharedPreferences.getStringSafe(key: String, defValue: String): String {
|
||||
return getString(key, null) ?: defValue
|
||||
}
|
||||
@@ -17,8 +17,10 @@ import androidx.core.view.isGone
|
||||
import androidx.core.view.isInvisible
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.interpolator.view.animation.FastOutSlowInInterpolator
|
||||
import org.schabi.newpipe.MainActivity
|
||||
|
||||
// logs in this class are disabled by default since it's usually not useful,
|
||||
// you can enable them by setting this flag to MainActivity.DEBUG
|
||||
private const val DEBUG = false
|
||||
private const val TAG = "ViewUtils"
|
||||
|
||||
/**
|
||||
@@ -38,7 +40,7 @@ fun View.animate(
|
||||
delay: Long = 0,
|
||||
execOnEnd: Runnable? = null
|
||||
) {
|
||||
if (MainActivity.DEBUG) {
|
||||
if (DEBUG) {
|
||||
val id = try {
|
||||
resources.getResourceEntryName(id)
|
||||
} catch (e: Exception) {
|
||||
@@ -51,7 +53,7 @@ fun View.animate(
|
||||
Log.d(TAG, "animate(): $msg")
|
||||
}
|
||||
if (isVisible && enterOrExit) {
|
||||
if (MainActivity.DEBUG) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "animate(): view was already visible > view = [$this]")
|
||||
}
|
||||
animate().setListener(null).cancel()
|
||||
@@ -60,7 +62,7 @@ fun View.animate(
|
||||
execOnEnd?.run()
|
||||
return
|
||||
} else if ((isGone || isInvisible) && !enterOrExit) {
|
||||
if (MainActivity.DEBUG) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "animate(): view was already gone > view = [$this]")
|
||||
}
|
||||
animate().setListener(null).cancel()
|
||||
@@ -89,7 +91,7 @@ fun View.animate(
|
||||
* @param colorEnd the background color to end with
|
||||
*/
|
||||
fun View.animateBackgroundColor(duration: Long, @ColorInt colorStart: Int, @ColorInt colorEnd: Int) {
|
||||
if (MainActivity.DEBUG) {
|
||||
if (DEBUG) {
|
||||
Log.d(
|
||||
TAG,
|
||||
"animateBackgroundColor() called with: view = [$this], duration = [$duration], " +
|
||||
@@ -109,7 +111,7 @@ fun View.animateBackgroundColor(duration: Long, @ColorInt colorStart: Int, @Colo
|
||||
}
|
||||
|
||||
fun View.animateHeight(duration: Long, targetHeight: Int): ValueAnimator {
|
||||
if (MainActivity.DEBUG) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "animateHeight: duration = [$duration], from $height to → $targetHeight in: $this")
|
||||
}
|
||||
val animator = ValueAnimator.ofFloat(height.toFloat(), targetHeight.toFloat())
|
||||
@@ -127,7 +129,7 @@ fun View.animateHeight(duration: Long, targetHeight: Int): ValueAnimator {
|
||||
}
|
||||
|
||||
fun View.animateRotation(duration: Long, targetRotation: Int) {
|
||||
if (MainActivity.DEBUG) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "animateRotation: duration = [$duration], from $rotation to → $targetRotation in: $this")
|
||||
}
|
||||
animate().setListener(null).cancel()
|
||||
|
||||
@@ -194,9 +194,6 @@ public abstract class BaseLocalListFragment<I, N> extends BaseStateFragment<I>
|
||||
if (itemsList != null) {
|
||||
animateHideRecyclerViewAllowingScrolling(itemsList);
|
||||
}
|
||||
if (headerRootBinding != null) {
|
||||
animate(headerRootBinding.getRoot(), false, 200);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -205,9 +202,6 @@ public abstract class BaseLocalListFragment<I, N> extends BaseStateFragment<I>
|
||||
if (itemsList != null) {
|
||||
animate(itemsList, true, 200);
|
||||
}
|
||||
if (headerRootBinding != null) {
|
||||
animate(headerRootBinding.getRoot(), true, 200);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -253,9 +247,6 @@ public abstract class BaseLocalListFragment<I, N> extends BaseStateFragment<I>
|
||||
if (itemsList != null) {
|
||||
animateHideRecyclerViewAllowingScrolling(itemsList);
|
||||
}
|
||||
if (headerRootBinding != null) {
|
||||
animate(headerRootBinding.getRoot(), false, 200);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -19,6 +19,8 @@ import androidx.fragment.app.FragmentManager;
|
||||
import androidx.recyclerview.widget.ItemTouchHelper;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.evernote.android.state.State;
|
||||
|
||||
import org.reactivestreams.Subscriber;
|
||||
import org.reactivestreams.Subscription;
|
||||
import org.schabi.newpipe.NewPipeDatabase;
|
||||
@@ -36,16 +38,17 @@ import org.schabi.newpipe.local.holder.LocalBookmarkPlaylistItemHolder;
|
||||
import org.schabi.newpipe.local.holder.RemoteBookmarkPlaylistItemHolder;
|
||||
import org.schabi.newpipe.local.playlist.LocalPlaylistManager;
|
||||
import org.schabi.newpipe.local.playlist.RemotePlaylistManager;
|
||||
import org.schabi.newpipe.util.debounce.DebounceSavable;
|
||||
import org.schabi.newpipe.util.debounce.DebounceSaver;
|
||||
import org.schabi.newpipe.ui.emptystate.EmptyStateSpec;
|
||||
import org.schabi.newpipe.ui.emptystate.EmptyStateUtil;
|
||||
import org.schabi.newpipe.util.NavigationHelper;
|
||||
import org.schabi.newpipe.util.OnClickGesture;
|
||||
import org.schabi.newpipe.util.debounce.DebounceSavable;
|
||||
import org.schabi.newpipe.util.debounce.DebounceSaver;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
import icepick.State;
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
||||
import io.reactivex.rxjava3.disposables.Disposable;
|
||||
@@ -122,6 +125,10 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL
|
||||
super.initViews(rootView, savedInstanceState);
|
||||
|
||||
itemListAdapter.setUseItemHandle(true);
|
||||
EmptyStateUtil.setEmptyStateComposable(
|
||||
rootView.findViewById(R.id.empty_state_view),
|
||||
EmptyStateSpec.Companion.getNoBookmarkedPlaylist()
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -160,7 +160,8 @@ public final class PlaylistAppendDialog extends PlaylistDialog {
|
||||
.subscribe(ignored -> {
|
||||
successToast.show();
|
||||
|
||||
if (playlist.thumbnailUrl.equals(PlaylistEntity.DEFAULT_THUMBNAIL)) {
|
||||
if (playlist.thumbnailUrl != null
|
||||
&& playlist.thumbnailUrl.equals(PlaylistEntity.DEFAULT_THUMBNAIL)) {
|
||||
playlistDisposables.add(manager
|
||||
.changePlaylistThumbnail(playlist.getUid(), streams.get(0).getUid(),
|
||||
false)
|
||||
|
||||
@@ -44,11 +44,11 @@ import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.preference.PreferenceManager
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.evernote.android.state.State
|
||||
import com.xwray.groupie.GroupieAdapter
|
||||
import com.xwray.groupie.Item
|
||||
import com.xwray.groupie.OnItemClickListener
|
||||
import com.xwray.groupie.OnItemLongClickListener
|
||||
import icepick.State
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||
@@ -74,6 +74,7 @@ import org.schabi.newpipe.ktx.slideUp
|
||||
import org.schabi.newpipe.local.feed.item.StreamItem
|
||||
import org.schabi.newpipe.local.feed.service.FeedLoadService
|
||||
import org.schabi.newpipe.local.subscription.SubscriptionManager
|
||||
import org.schabi.newpipe.ui.emptystate.setEmptyStateComposable
|
||||
import org.schabi.newpipe.util.DeviceUtils
|
||||
import org.schabi.newpipe.util.Localization
|
||||
import org.schabi.newpipe.util.NavigationHelper
|
||||
@@ -132,6 +133,7 @@ class FeedFragment : BaseStateFragment<FeedState>() {
|
||||
override fun onViewCreated(rootView: View, savedInstanceState: Bundle?) {
|
||||
// super.onViewCreated() calls initListeners() which require the binding to be initialized
|
||||
_feedBinding = FragmentFeedBinding.bind(rootView)
|
||||
feedBinding.emptyStateView.setEmptyStateComposable()
|
||||
super.onViewCreated(rootView, savedInstanceState)
|
||||
|
||||
val factory = FeedViewModel.getFactory(requireContext(), groupId)
|
||||
@@ -202,6 +204,7 @@ class FeedFragment : BaseStateFragment<FeedState>() {
|
||||
// Menu
|
||||
// /////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@Deprecated("Deprecated in Java")
|
||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||
super.onCreateOptionsMenu(menu, inflater)
|
||||
|
||||
@@ -212,6 +215,7 @@ class FeedFragment : BaseStateFragment<FeedState>() {
|
||||
inflater.inflate(R.menu.menu_feed_fragment, menu)
|
||||
}
|
||||
|
||||
@Deprecated("Deprecated in Java")
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
if (item.itemId == R.id.menu_item_feed_help) {
|
||||
val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
|
||||
@@ -253,7 +257,7 @@ class FeedFragment : BaseStateFragment<FeedState>() {
|
||||
viewModel.getShowFutureItemsFromPreferences()
|
||||
)
|
||||
|
||||
AlertDialog.Builder(context!!)
|
||||
AlertDialog.Builder(requireContext())
|
||||
.setTitle(R.string.feed_hide_streams_title)
|
||||
.setMultiChoiceItems(dialogItems, checkedDialogItems) { _, which, isChecked ->
|
||||
checkedDialogItems[which] = isChecked
|
||||
@@ -267,6 +271,7 @@ class FeedFragment : BaseStateFragment<FeedState>() {
|
||||
.show()
|
||||
}
|
||||
|
||||
@Deprecated("Deprecated in Java")
|
||||
override fun onDestroyOptionsMenu() {
|
||||
super.onDestroyOptionsMenu()
|
||||
activity?.supportActionBar?.subtitle = null
|
||||
@@ -549,7 +554,7 @@ class FeedFragment : BaseStateFragment<FeedState>() {
|
||||
|
||||
var typeface = Typeface.DEFAULT
|
||||
var backgroundSupplier = { ctx: Context ->
|
||||
resolveDrawable(ctx, R.attr.selectableItemBackground)
|
||||
resolveDrawable(ctx, android.R.attr.selectableItemBackground)
|
||||
}
|
||||
if (doCheck) {
|
||||
// If the uploadDate is null or true we should highlight the item
|
||||
@@ -562,7 +567,7 @@ class FeedFragment : BaseStateFragment<FeedState>() {
|
||||
LayerDrawable(
|
||||
arrayOf(
|
||||
resolveDrawable(ctx, R.attr.dashed_border),
|
||||
resolveDrawable(ctx, R.attr.selectableItemBackground)
|
||||
resolveDrawable(ctx, android.R.attr.selectableItemBackground)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -165,7 +165,7 @@ class FeedViewModel(
|
||||
fun getFactory(context: Context, groupId: Long) = viewModelFactory {
|
||||
initializer {
|
||||
FeedViewModel(
|
||||
App.getApp(),
|
||||
App.instance,
|
||||
groupId,
|
||||
// Read initial value from preferences
|
||||
getShowPlayedItemsFromPreferences(context.applicationContext),
|
||||
|
||||
@@ -19,7 +19,7 @@ import org.schabi.newpipe.extractor.stream.StreamType.POST_LIVE_STREAM
|
||||
import org.schabi.newpipe.extractor.stream.StreamType.VIDEO_STREAM
|
||||
import org.schabi.newpipe.util.Localization
|
||||
import org.schabi.newpipe.util.StreamTypeUtil
|
||||
import org.schabi.newpipe.util.image.PicassoHelper
|
||||
import org.schabi.newpipe.util.image.CoilHelper
|
||||
import java.util.concurrent.TimeUnit
|
||||
import java.util.function.Consumer
|
||||
|
||||
@@ -101,7 +101,7 @@ data class StreamItem(
|
||||
viewBinding.itemProgressView.visibility = View.GONE
|
||||
}
|
||||
|
||||
PicassoHelper.loadThumbnail(stream.thumbnailUrl).into(viewBinding.itemThumbnailView)
|
||||
CoilHelper.loadThumbnail(viewBinding.itemThumbnailView, stream.thumbnailUrl)
|
||||
|
||||
if (itemVersion != ItemVersion.MINI) {
|
||||
viewBinding.itemAdditionalDetails.text =
|
||||
|
||||
@@ -6,7 +6,6 @@ import android.app.PendingIntent
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.provider.Settings
|
||||
@@ -16,20 +15,17 @@ import androidx.core.app.PendingIntentCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.content.getSystemService
|
||||
import androidx.preference.PreferenceManager
|
||||
import com.squareup.picasso.Picasso
|
||||
import com.squareup.picasso.Target
|
||||
import org.schabi.newpipe.R
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem
|
||||
import org.schabi.newpipe.local.feed.service.FeedUpdateInfo
|
||||
import org.schabi.newpipe.util.NavigationHelper
|
||||
import org.schabi.newpipe.util.image.PicassoHelper
|
||||
import org.schabi.newpipe.util.image.CoilHelper
|
||||
|
||||
/**
|
||||
* Helper for everything related to show notifications about new streams to the user.
|
||||
*/
|
||||
class NotificationHelper(val context: Context) {
|
||||
private val manager = NotificationManagerCompat.from(context)
|
||||
private val iconLoadingTargets = ArrayList<Target>()
|
||||
|
||||
/**
|
||||
* Show notifications for new streams from a single channel. The individual notifications are
|
||||
@@ -68,51 +64,22 @@ class NotificationHelper(val context: Context) {
|
||||
summaryBuilder.setStyle(style)
|
||||
|
||||
// open the channel page when clicking on the summary notification
|
||||
val intent = NavigationHelper
|
||||
.getChannelIntent(context, data.serviceId, data.url)
|
||||
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
summaryBuilder.setContentIntent(
|
||||
PendingIntentCompat.getActivity(
|
||||
context,
|
||||
data.pseudoId,
|
||||
NavigationHelper
|
||||
.getChannelIntent(context, data.serviceId, data.url)
|
||||
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK),
|
||||
0,
|
||||
false
|
||||
)
|
||||
PendingIntentCompat.getActivity(context, data.pseudoId, intent, 0, false)
|
||||
)
|
||||
|
||||
// a Target is like a listener for image loading events
|
||||
val target = object : Target {
|
||||
override fun onBitmapLoaded(bitmap: Bitmap, from: Picasso.LoadedFrom) {
|
||||
// set channel icon only if there is actually one (for Android versions < 7.0)
|
||||
summaryBuilder.setLargeIcon(bitmap)
|
||||
val avatarIcon =
|
||||
CoilHelper.loadBitmapBlocking(context, data.avatarUrl, R.drawable.ic_newpipe_triangle_white)
|
||||
|
||||
// Show individual stream notifications, set channel icon only if there is actually
|
||||
// one
|
||||
showStreamNotifications(newStreams, data.serviceId, bitmap)
|
||||
// Show summary notification
|
||||
manager.notify(data.pseudoId, summaryBuilder.build())
|
||||
summaryBuilder.setLargeIcon(avatarIcon)
|
||||
|
||||
iconLoadingTargets.remove(this) // allow it to be garbage-collected
|
||||
}
|
||||
|
||||
override fun onBitmapFailed(e: Exception, errorDrawable: Drawable) {
|
||||
// Show individual stream notifications
|
||||
showStreamNotifications(newStreams, data.serviceId, null)
|
||||
// Show summary notification
|
||||
manager.notify(data.pseudoId, summaryBuilder.build())
|
||||
iconLoadingTargets.remove(this) // allow it to be garbage-collected
|
||||
}
|
||||
|
||||
override fun onPrepareLoad(placeHolderDrawable: Drawable) {
|
||||
// Nothing to do
|
||||
}
|
||||
}
|
||||
|
||||
// add the target to the list to hold a strong reference and prevent it from being garbage
|
||||
// collected, since Picasso only holds weak references to targets
|
||||
iconLoadingTargets.add(target)
|
||||
|
||||
PicassoHelper.loadNotificationIcon(data.avatarUrl).into(target)
|
||||
// Show individual stream notifications, set channel icon only if there is actually one
|
||||
showStreamNotifications(newStreams, data.serviceId, avatarIcon)
|
||||
// Show summary notification
|
||||
manager.notify(data.pseudoId, summaryBuilder.build())
|
||||
}
|
||||
|
||||
private fun showStreamNotifications(
|
||||
|
||||
@@ -17,8 +17,10 @@ import org.schabi.newpipe.database.subscription.NotificationMode
|
||||
import org.schabi.newpipe.database.subscription.SubscriptionEntity
|
||||
import org.schabi.newpipe.extractor.Info
|
||||
import org.schabi.newpipe.extractor.NewPipe
|
||||
import org.schabi.newpipe.extractor.ServiceList
|
||||
import org.schabi.newpipe.extractor.feed.FeedInfo
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem
|
||||
import org.schabi.newpipe.ktx.getStringSafe
|
||||
import org.schabi.newpipe.local.feed.FeedDatabaseManager
|
||||
import org.schabi.newpipe.local.subscription.SubscriptionManager
|
||||
import org.schabi.newpipe.util.ChannelTabHelper
|
||||
@@ -69,12 +71,10 @@ class FeedLoadManager(private val context: Context) {
|
||||
val outdatedThreshold = if (ignoreOutdatedThreshold) {
|
||||
OffsetDateTime.now(ZoneOffset.UTC)
|
||||
} else {
|
||||
val thresholdOutdatedSeconds = (
|
||||
defaultSharedPreferences.getString(
|
||||
context.getString(R.string.feed_update_threshold_key),
|
||||
context.getString(R.string.feed_update_threshold_default_value)
|
||||
) ?: context.getString(R.string.feed_update_threshold_default_value)
|
||||
).toInt()
|
||||
val thresholdOutdatedSeconds = defaultSharedPreferences.getStringSafe(
|
||||
context.getString(R.string.feed_update_threshold_key),
|
||||
context.getString(R.string.feed_update_threshold_default_value)
|
||||
).toInt()
|
||||
OffsetDateTime.now(ZoneOffset.UTC).minusSeconds(thresholdOutdatedSeconds.toLong())
|
||||
}
|
||||
|
||||
@@ -91,6 +91,10 @@ class FeedLoadManager(private val context: Context) {
|
||||
else -> feedDatabaseManager.outdatedSubscriptionsForGroup(groupId, outdatedThreshold)
|
||||
}
|
||||
|
||||
// like `currentProgress`, but counts the number of YouTube extractions that have begun, so
|
||||
// they can be properly throttled every once in a while (see doOnNext below)
|
||||
val youtubeExtractionCount = AtomicInteger()
|
||||
|
||||
return outdatedSubscriptions
|
||||
.take(1)
|
||||
.doOnNext {
|
||||
@@ -106,6 +110,15 @@ class FeedLoadManager(private val context: Context) {
|
||||
.observeOn(Schedulers.io())
|
||||
.flatMap { Flowable.fromIterable(it) }
|
||||
.takeWhile { !cancelSignal.get() }
|
||||
.doOnNext { subscriptionEntity ->
|
||||
// throttle YouTube extractions once every BATCH_SIZE to avoid being rate limited
|
||||
if (subscriptionEntity.serviceId == ServiceList.YouTube.serviceId) {
|
||||
val previousCount = youtubeExtractionCount.getAndIncrement()
|
||||
if (previousCount != 0 && previousCount % BATCH_SIZE == 0) {
|
||||
Thread.sleep(DELAY_BETWEEN_BATCHES_MILLIS.random())
|
||||
}
|
||||
}
|
||||
}
|
||||
.parallel(PARALLEL_EXTRACTIONS, PARALLEL_EXTRACTIONS * 2)
|
||||
.runOn(Schedulers.io(), PARALLEL_EXTRACTIONS * 2)
|
||||
.filter { !cancelSignal.get() }
|
||||
@@ -329,7 +342,19 @@ class FeedLoadManager(private val context: Context) {
|
||||
/**
|
||||
* How many extractions will be running in parallel.
|
||||
*/
|
||||
private const val PARALLEL_EXTRACTIONS = 6
|
||||
private const val PARALLEL_EXTRACTIONS = 3
|
||||
|
||||
/**
|
||||
* How many YouTube extractions to perform before waiting [DELAY_BETWEEN_BATCHES_MILLIS]
|
||||
* to avoid being rate limited
|
||||
*/
|
||||
private const val BATCH_SIZE = 50
|
||||
|
||||
/**
|
||||
* Wait a random delay in this range once every [BATCH_SIZE] YouTube extractions to avoid
|
||||
* being rate limited
|
||||
*/
|
||||
private val DELAY_BETWEEN_BATCHES_MILLIS = (6000L..12000L)
|
||||
|
||||
/**
|
||||
* Number of items to buffer to mass-insert in the database.
|
||||
|
||||
@@ -18,10 +18,13 @@ package org.schabi.newpipe.local.history;
|
||||
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import static org.schabi.newpipe.util.ExtractorHelper.getStreamInfo;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.collection.LongLongPair;
|
||||
import androidx.preference.PreferenceManager;
|
||||
|
||||
import org.schabi.newpipe.NewPipeDatabase;
|
||||
@@ -45,7 +48,6 @@ import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||
import org.schabi.newpipe.local.feed.FeedViewModel;
|
||||
import org.schabi.newpipe.player.playqueue.PlayQueueItem;
|
||||
import org.schabi.newpipe.util.ExtractorHelper;
|
||||
|
||||
import java.time.OffsetDateTime;
|
||||
import java.time.ZoneOffset;
|
||||
@@ -91,47 +93,39 @@ public class HistoryRecordManager {
|
||||
* @param info the item to mark as watched
|
||||
* @return a Maybe containing the ID of the item if successful
|
||||
*/
|
||||
public Maybe<Long> markAsWatched(final StreamInfoItem info) {
|
||||
public Completable markAsWatched(final StreamInfoItem info) {
|
||||
if (!isStreamHistoryEnabled()) {
|
||||
return Maybe.empty();
|
||||
return Completable.complete();
|
||||
}
|
||||
|
||||
final OffsetDateTime currentTime = OffsetDateTime.now(ZoneOffset.UTC);
|
||||
return Maybe.fromCallable(() -> database.runInTransaction(() -> {
|
||||
final long streamId;
|
||||
final long duration;
|
||||
// Duration will not exist if the item was loaded with fast mode, so fetch it if empty
|
||||
if (info.getDuration() < 0) {
|
||||
final StreamInfo completeInfo = ExtractorHelper.getStreamInfo(
|
||||
info.getServiceId(),
|
||||
info.getUrl(),
|
||||
false
|
||||
final var remoteInfo = getStreamInfo(info.getServiceId(), info.getUrl(), false)
|
||||
.map(item ->
|
||||
new LongLongPair(item.getDuration(), streamTable.upsert(new StreamEntity(item))));
|
||||
|
||||
return Single.just(info)
|
||||
.filter(item -> item.getDuration() >= 0)
|
||||
.map(item ->
|
||||
new LongLongPair(item.getDuration(), streamTable.upsert(new StreamEntity(item)))
|
||||
)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.blockingGet();
|
||||
duration = completeInfo.getDuration();
|
||||
streamId = streamTable.upsert(new StreamEntity(completeInfo));
|
||||
} else {
|
||||
duration = info.getDuration();
|
||||
streamId = streamTable.upsert(new StreamEntity(info));
|
||||
}
|
||||
.switchIfEmpty(remoteInfo)
|
||||
.flatMapCompletable(pair -> Completable.fromRunnable(() -> {
|
||||
final long duration = pair.getFirst();
|
||||
final long streamId = pair.getSecond();
|
||||
|
||||
// Update the stream progress to the full duration of the video
|
||||
final StreamStateEntity entity = new StreamStateEntity(
|
||||
streamId,
|
||||
duration * 1000
|
||||
);
|
||||
streamStateTable.upsert(entity);
|
||||
// Update the stream progress to the full duration of the video
|
||||
final var entity = new StreamStateEntity(streamId, duration * 1000);
|
||||
streamStateTable.upsert(entity);
|
||||
|
||||
// Add a history entry
|
||||
final StreamHistoryEntity latestEntry = streamHistoryTable.getLatestEntry(streamId);
|
||||
if (latestEntry == null) {
|
||||
// never actually viewed: add history entry but with 0 views
|
||||
return streamHistoryTable.insert(new StreamHistoryEntity(streamId, currentTime, 0));
|
||||
} else {
|
||||
return 0L;
|
||||
}
|
||||
})).subscribeOn(Schedulers.io());
|
||||
// Add a history entry
|
||||
final var latestEntry = streamHistoryTable.getLatestEntry(streamId);
|
||||
if (latestEntry == null) {
|
||||
final var currentTime = OffsetDateTime.now(ZoneOffset.UTC);
|
||||
// never actually viewed: add history entry but with 0 views
|
||||
final var entry = new StreamHistoryEntity(streamId, currentTime, 0);
|
||||
streamHistoryTable.insert(entry);
|
||||
}
|
||||
}))
|
||||
.subscribeOn(Schedulers.io());
|
||||
}
|
||||
|
||||
public Maybe<Long> onViewed(final StreamInfo info) {
|
||||
@@ -221,7 +215,7 @@ public class HistoryRecordManager {
|
||||
public Flowable<List<String>> getRelatedSearches(final String query,
|
||||
final int similarQueryLimit,
|
||||
final int uniqueQueryLimit) {
|
||||
return query.length() > 0
|
||||
return !query.isEmpty()
|
||||
? searchHistoryTable.getSimilarEntries(query, similarQueryLimit)
|
||||
: searchHistoryTable.getUniqueEntries(uniqueQueryLimit);
|
||||
}
|
||||
@@ -236,47 +230,31 @@ public class HistoryRecordManager {
|
||||
|
||||
public Maybe<StreamStateEntity> loadStreamState(final PlayQueueItem queueItem) {
|
||||
return queueItem.getStream()
|
||||
.map(info -> streamTable.upsert(new StreamEntity(info)))
|
||||
.flatMapPublisher(streamStateTable::getState)
|
||||
.firstElement()
|
||||
.flatMap(list -> list.isEmpty() ? Maybe.empty() : Maybe.just(list.get(0)))
|
||||
.flatMapMaybe(this::loadStreamState)
|
||||
.filter(state -> state.isValid(queueItem.getDuration()))
|
||||
.subscribeOn(Schedulers.io());
|
||||
}
|
||||
|
||||
public Maybe<StreamStateEntity> loadStreamState(final StreamInfo info) {
|
||||
return Single.fromCallable(() -> streamTable.upsert(new StreamEntity(info)))
|
||||
.flatMapPublisher(streamStateTable::getState)
|
||||
.firstElement()
|
||||
.flatMap(list -> list.isEmpty() ? Maybe.empty() : Maybe.just(list.get(0)))
|
||||
.filter(state -> state.isValid(info.getDuration()))
|
||||
.flatMapMaybe(streamStateTable::getState)
|
||||
.subscribeOn(Schedulers.io());
|
||||
}
|
||||
|
||||
public Completable saveStreamState(@NonNull final StreamInfo info, final long progressMillis) {
|
||||
return Completable.fromAction(() -> database.runInTransaction(() -> {
|
||||
final long streamId = streamTable.upsert(new StreamEntity(info));
|
||||
final StreamStateEntity state = new StreamStateEntity(streamId, progressMillis);
|
||||
final var state = new StreamStateEntity(streamId, progressMillis);
|
||||
if (state.isValid(info.getDuration())) {
|
||||
streamStateTable.upsert(state);
|
||||
}
|
||||
})).subscribeOn(Schedulers.io());
|
||||
}
|
||||
|
||||
public Single<StreamStateEntity[]> loadStreamState(final InfoItem info) {
|
||||
return Single.fromCallable(() -> {
|
||||
final List<StreamEntity> entities = streamTable
|
||||
.getStream(info.getServiceId(), info.getUrl()).blockingFirst();
|
||||
if (entities.isEmpty()) {
|
||||
return new StreamStateEntity[]{null};
|
||||
}
|
||||
final List<StreamStateEntity> states = streamStateTable
|
||||
.getState(entities.get(0).getUid()).blockingFirst();
|
||||
if (states.isEmpty()) {
|
||||
return new StreamStateEntity[]{null};
|
||||
}
|
||||
return new StreamStateEntity[]{states.get(0)};
|
||||
}).subscribeOn(Schedulers.io());
|
||||
public Maybe<StreamStateEntity> loadStreamState(final InfoItem info) {
|
||||
return streamTable.getStream(info.getServiceId(), info.getUrl())
|
||||
.flatMap(entity -> streamStateTable.getState(entity.getUid()))
|
||||
.subscribeOn(Schedulers.io());
|
||||
}
|
||||
|
||||
public Single<List<StreamStateEntity>> loadLocalStreamStateBatch(
|
||||
@@ -295,13 +273,7 @@ public class HistoryRecordManager {
|
||||
result.add(null);
|
||||
continue;
|
||||
}
|
||||
final List<StreamStateEntity> states = streamStateTable.getState(streamId)
|
||||
.blockingFirst();
|
||||
if (states.isEmpty()) {
|
||||
result.add(null);
|
||||
} else {
|
||||
result.add(states.get(0));
|
||||
}
|
||||
result.add(streamStateTable.getState(streamId).blockingGet());
|
||||
}
|
||||
return result;
|
||||
}).subscribeOn(Schedulers.io());
|
||||
|
||||
@@ -15,6 +15,7 @@ import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.viewbinding.ViewBinding;
|
||||
|
||||
import com.evernote.android.state.State;
|
||||
import com.google.android.material.snackbar.Snackbar;
|
||||
|
||||
import org.reactivestreams.Subscriber;
|
||||
@@ -45,7 +46,6 @@ import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
import icepick.State;
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
||||
import io.reactivex.rxjava3.disposables.Disposable;
|
||||
@@ -332,10 +332,6 @@ public class StatisticsPlaylistFragment
|
||||
StreamDialogDefaultEntry.DELETE,
|
||||
(f, i) -> deleteEntry(
|
||||
Math.max(itemListAdapter.getItemsList().indexOf(item), 0)))
|
||||
.setAction(
|
||||
StreamDialogDefaultEntry.START_HERE_ON_BACKGROUND,
|
||||
(f, i) -> NavigationHelper.playOnBackgroundPlayer(
|
||||
context, getPlayQueueStartingAt(item), true))
|
||||
.create()
|
||||
.show();
|
||||
} catch (final IllegalArgumentException e) {
|
||||
@@ -368,6 +364,7 @@ public class StatisticsPlaylistFragment
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public PlayQueue getPlayQueue() {
|
||||
return getPlayQueue(0);
|
||||
}
|
||||
|
||||
@@ -8,8 +8,8 @@ import org.schabi.newpipe.database.playlist.PlaylistDuplicatesEntry;
|
||||
import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry;
|
||||
import org.schabi.newpipe.local.LocalItemBuilder;
|
||||
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
||||
import org.schabi.newpipe.util.image.PicassoHelper;
|
||||
import org.schabi.newpipe.util.Localization;
|
||||
import org.schabi.newpipe.util.image.CoilHelper;
|
||||
|
||||
import java.time.format.DateTimeFormatter;
|
||||
|
||||
@@ -30,17 +30,16 @@ public class LocalPlaylistItemHolder extends PlaylistItemHolder {
|
||||
public void updateFromItem(final LocalItem localItem,
|
||||
final HistoryRecordManager historyRecordManager,
|
||||
final DateTimeFormatter dateTimeFormatter) {
|
||||
if (!(localItem instanceof PlaylistMetadataEntry)) {
|
||||
if (!(localItem instanceof PlaylistMetadataEntry item)) {
|
||||
return;
|
||||
}
|
||||
final PlaylistMetadataEntry item = (PlaylistMetadataEntry) localItem;
|
||||
|
||||
itemTitleView.setText(item.name);
|
||||
itemStreamCountView.setText(Localization.localizeStreamCountMini(
|
||||
itemStreamCountView.getContext(), item.streamCount));
|
||||
itemUploaderView.setVisibility(View.INVISIBLE);
|
||||
|
||||
PicassoHelper.loadPlaylistThumbnail(item.thumbnailUrl).into(itemThumbnailView);
|
||||
CoilHelper.INSTANCE.loadPlaylistThumbnail(itemThumbnailView, item.thumbnailUrl);
|
||||
|
||||
if (item instanceof PlaylistDuplicatesEntry
|
||||
&& ((PlaylistDuplicatesEntry) item).timesStreamIsContained > 0) {
|
||||
|
||||
@@ -16,8 +16,8 @@ import org.schabi.newpipe.local.LocalItemBuilder;
|
||||
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
||||
import org.schabi.newpipe.util.DependentPreferenceHelper;
|
||||
import org.schabi.newpipe.util.Localization;
|
||||
import org.schabi.newpipe.util.image.PicassoHelper;
|
||||
import org.schabi.newpipe.util.ServiceHelper;
|
||||
import org.schabi.newpipe.util.image.CoilHelper;
|
||||
import org.schabi.newpipe.views.AnimatedProgressBar;
|
||||
|
||||
import java.time.format.DateTimeFormatter;
|
||||
@@ -83,8 +83,8 @@ public class LocalPlaylistStreamItemHolder extends LocalItemHolder {
|
||||
}
|
||||
|
||||
// Default thumbnail is shown on error, while loading and if the url is empty
|
||||
PicassoHelper.loadThumbnail(item.getStreamEntity().getThumbnailUrl())
|
||||
.into(itemThumbnailView);
|
||||
CoilHelper.INSTANCE.loadThumbnail(itemThumbnailView,
|
||||
item.getStreamEntity().getThumbnailUrl());
|
||||
|
||||
itemView.setOnClickListener(view -> {
|
||||
if (itemBuilder.getOnItemSelectedListener() != null) {
|
||||
|
||||
@@ -16,8 +16,8 @@ import org.schabi.newpipe.local.LocalItemBuilder;
|
||||
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
||||
import org.schabi.newpipe.util.DependentPreferenceHelper;
|
||||
import org.schabi.newpipe.util.Localization;
|
||||
import org.schabi.newpipe.util.image.PicassoHelper;
|
||||
import org.schabi.newpipe.util.ServiceHelper;
|
||||
import org.schabi.newpipe.util.image.CoilHelper;
|
||||
import org.schabi.newpipe.views.AnimatedProgressBar;
|
||||
|
||||
import java.time.format.DateTimeFormatter;
|
||||
@@ -117,8 +117,8 @@ public class LocalStatisticStreamItemHolder extends LocalItemHolder {
|
||||
}
|
||||
|
||||
// Default thumbnail is shown on error, while loading and if the url is empty
|
||||
PicassoHelper.loadThumbnail(item.getStreamEntity().getThumbnailUrl())
|
||||
.into(itemThumbnailView);
|
||||
CoilHelper.INSTANCE.loadThumbnail(itemThumbnailView,
|
||||
item.getStreamEntity().getThumbnailUrl());
|
||||
|
||||
itemView.setOnClickListener(view -> {
|
||||
if (itemBuilder.getOnItemSelectedListener() != null) {
|
||||
|
||||
@@ -8,8 +8,8 @@ import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity;
|
||||
import org.schabi.newpipe.local.LocalItemBuilder;
|
||||
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
||||
import org.schabi.newpipe.util.Localization;
|
||||
import org.schabi.newpipe.util.image.PicassoHelper;
|
||||
import org.schabi.newpipe.util.ServiceHelper;
|
||||
import org.schabi.newpipe.util.image.CoilHelper;
|
||||
|
||||
import java.time.format.DateTimeFormatter;
|
||||
|
||||
@@ -29,10 +29,9 @@ public class RemotePlaylistItemHolder extends PlaylistItemHolder {
|
||||
public void updateFromItem(final LocalItem localItem,
|
||||
final HistoryRecordManager historyRecordManager,
|
||||
final DateTimeFormatter dateTimeFormatter) {
|
||||
if (!(localItem instanceof PlaylistRemoteEntity)) {
|
||||
if (!(localItem instanceof PlaylistRemoteEntity item)) {
|
||||
return;
|
||||
}
|
||||
final PlaylistRemoteEntity item = (PlaylistRemoteEntity) localItem;
|
||||
|
||||
itemTitleView.setText(item.getName());
|
||||
itemStreamCountView.setText(Localization.localizeStreamCountMini(
|
||||
@@ -45,7 +44,7 @@ public class RemotePlaylistItemHolder extends PlaylistItemHolder {
|
||||
itemUploaderView.setText(ServiceHelper.getNameOfServiceById(item.getServiceId()));
|
||||
}
|
||||
|
||||
PicassoHelper.loadPlaylistThumbnail(item.getThumbnailUrl()).into(itemThumbnailView);
|
||||
CoilHelper.INSTANCE.loadPlaylistThumbnail(itemThumbnailView, item.getThumbnailUrl());
|
||||
|
||||
super.updateFromItem(localItem, historyRecordManager, dateTimeFormatter);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
package org.schabi.newpipe.local.playlist
|
||||
|
||||
import android.content.Context
|
||||
import org.schabi.newpipe.R
|
||||
import org.schabi.newpipe.database.playlist.PlaylistStreamEntry
|
||||
import org.schabi.newpipe.extractor.exceptions.ParsingException
|
||||
import org.schabi.newpipe.extractor.services.youtube.linkHandler.YoutubeStreamLinkHandlerFactory
|
||||
import org.schabi.newpipe.local.playlist.PlayListShareMode.JUST_URLS
|
||||
import org.schabi.newpipe.local.playlist.PlayListShareMode.WITH_TITLES
|
||||
import org.schabi.newpipe.local.playlist.PlayListShareMode.YOUTUBE_TEMP_PLAYLIST
|
||||
|
||||
fun export(
|
||||
shareMode: PlayListShareMode,
|
||||
playlist: List<PlaylistStreamEntry>,
|
||||
context: Context
|
||||
): String {
|
||||
return when (shareMode) {
|
||||
WITH_TITLES -> exportWithTitles(playlist, context)
|
||||
JUST_URLS -> exportJustUrls(playlist)
|
||||
YOUTUBE_TEMP_PLAYLIST -> exportAsYoutubeTempPlaylist(playlist)
|
||||
}
|
||||
}
|
||||
|
||||
fun exportWithTitles(
|
||||
playlist: List<PlaylistStreamEntry>,
|
||||
context: Context
|
||||
): String {
|
||||
|
||||
return playlist.asSequence()
|
||||
.map { it.streamEntity }
|
||||
.map { entity ->
|
||||
context.getString(
|
||||
R.string.video_details_list_item,
|
||||
entity.title,
|
||||
entity.url
|
||||
)
|
||||
}
|
||||
.joinToString(separator = "\n")
|
||||
}
|
||||
|
||||
fun exportJustUrls(playlist: List<PlaylistStreamEntry>): String {
|
||||
|
||||
return playlist.asSequence()
|
||||
.map { it.streamEntity.url }
|
||||
.joinToString(separator = "\n")
|
||||
}
|
||||
|
||||
fun exportAsYoutubeTempPlaylist(playlist: List<PlaylistStreamEntry>): String {
|
||||
|
||||
val videoIDs = playlist.asReversed().asSequence()
|
||||
.map { it.streamEntity.url }
|
||||
.mapNotNull(::getYouTubeId)
|
||||
.take(50) // YouTube limitation: temp playlists can't have more than 50 items
|
||||
.toList()
|
||||
.asReversed()
|
||||
.joinToString(separator = ",")
|
||||
|
||||
return "https://www.youtube.com/watch_videos?video_ids=$videoIDs"
|
||||
}
|
||||
|
||||
val linkHandler: YoutubeStreamLinkHandlerFactory = YoutubeStreamLinkHandlerFactory.getInstance()
|
||||
|
||||
/**
|
||||
* Gets the video id from a YouTube URL.
|
||||
*
|
||||
* @param url YouTube URL
|
||||
* @return the video id
|
||||
*/
|
||||
fun getYouTubeId(url: String): String? {
|
||||
|
||||
return try { linkHandler.getId(url) } catch (e: ParsingException) { null }
|
||||
}
|
||||
@@ -2,8 +2,13 @@ package org.schabi.newpipe.local.playlist;
|
||||
|
||||
import static org.schabi.newpipe.error.ErrorUtil.showUiErrorSnackbar;
|
||||
import static org.schabi.newpipe.ktx.ViewUtils.animate;
|
||||
import static org.schabi.newpipe.local.playlist.ExportPlaylistKt.export;
|
||||
import static org.schabi.newpipe.local.playlist.PlayListShareMode.JUST_URLS;
|
||||
import static org.schabi.newpipe.local.playlist.PlayListShareMode.WITH_TITLES;
|
||||
import static org.schabi.newpipe.local.playlist.PlayListShareMode.YOUTUBE_TEMP_PLAYLIST;
|
||||
import static org.schabi.newpipe.util.ThemeHelper.shouldUseGridLayout;
|
||||
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
import android.os.Parcelable;
|
||||
@@ -26,6 +31,7 @@ import androidx.recyclerview.widget.ItemTouchHelper;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import androidx.viewbinding.ViewBinding;
|
||||
|
||||
import com.evernote.android.state.State;
|
||||
import org.reactivestreams.Subscriber;
|
||||
import org.reactivestreams.Subscription;
|
||||
import org.schabi.newpipe.NewPipeDatabase;
|
||||
@@ -49,12 +55,12 @@ import org.schabi.newpipe.local.BaseLocalListFragment;
|
||||
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
||||
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
||||
import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
|
||||
import org.schabi.newpipe.util.debounce.DebounceSavable;
|
||||
import org.schabi.newpipe.util.debounce.DebounceSaver;
|
||||
import org.schabi.newpipe.util.Localization;
|
||||
import org.schabi.newpipe.util.NavigationHelper;
|
||||
import org.schabi.newpipe.util.OnClickGesture;
|
||||
import org.schabi.newpipe.util.PlayButtonHelper;
|
||||
import org.schabi.newpipe.util.debounce.DebounceSavable;
|
||||
import org.schabi.newpipe.util.debounce.DebounceSaver;
|
||||
import org.schabi.newpipe.util.external_communication.ShareUtils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
@@ -63,7 +69,6 @@ import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import icepick.State;
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.rxjava3.core.Single;
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
||||
@@ -384,34 +389,41 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
|
||||
}
|
||||
|
||||
/**
|
||||
* Shares the playlist as a list of stream URLs if {@code shouldSharePlaylistDetails} is
|
||||
* set to {@code false}. Shares the playlist name along with a list of video titles and URLs
|
||||
* if {@code shouldSharePlaylistDetails} is set to {@code true}.
|
||||
* Shares the playlist in one of 3 ways, depending on the value of {@code shareMode}:
|
||||
* <ul>
|
||||
* <li>{@code JUST_URLS}: shares the URLs only.</li>
|
||||
* <li>{@code WITH_TITLES}: each entry in the list is accompanied by its title.</li>
|
||||
* <li>{@code YOUTUBE_TEMP_PLAYLIST}: shares as a YouTube temporary playlist.</li>
|
||||
* </ul>
|
||||
*
|
||||
* @param shouldSharePlaylistDetails Whether the playlist details should be included in the
|
||||
* shared content.
|
||||
* @param shareMode The way the playlist should be shared.
|
||||
*/
|
||||
private void sharePlaylist(final boolean shouldSharePlaylistDetails) {
|
||||
private void sharePlaylist(final PlayListShareMode shareMode) {
|
||||
final Context context = requireContext();
|
||||
|
||||
disposables.add(playlistManager.getPlaylistStreams(playlistId)
|
||||
.flatMapSingle(playlist -> Single.just(playlist.stream()
|
||||
.map(PlaylistStreamEntry::getStreamEntity)
|
||||
.map(streamEntity -> {
|
||||
if (shouldSharePlaylistDetails) {
|
||||
return context.getString(R.string.video_details_list_item,
|
||||
streamEntity.getTitle(), streamEntity.getUrl());
|
||||
} else {
|
||||
return streamEntity.getUrl();
|
||||
}
|
||||
})
|
||||
.collect(Collectors.joining("\n"))))
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(urlsText -> ShareUtils.shareText(
|
||||
context, name, shouldSharePlaylistDetails
|
||||
? context.getString(R.string.share_playlist_content_details,
|
||||
name, urlsText) : urlsText),
|
||||
throwable -> showUiErrorSnackbar(this, "Sharing playlist", throwable)));
|
||||
.flatMapSingle(playlist -> Single.just(export(
|
||||
|
||||
shareMode,
|
||||
playlist,
|
||||
context
|
||||
)))
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
urlsText -> {
|
||||
|
||||
final String content = shareMode == WITH_TITLES
|
||||
? context.getString(R.string.share_playlist_content_details,
|
||||
name,
|
||||
urlsText
|
||||
)
|
||||
: urlsText;
|
||||
|
||||
ShareUtils.shareText(context, name, content);
|
||||
},
|
||||
throwable -> showUiErrorSnackbar(this, "Sharing playlist", throwable)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
public void removeWatchedStreams(final boolean removePartiallyWatched) {
|
||||
@@ -843,6 +855,7 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public PlayQueue getPlayQueue() {
|
||||
return getPlayQueue(0);
|
||||
}
|
||||
@@ -870,13 +883,15 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
|
||||
private void createShareConfirmationDialog() {
|
||||
new AlertDialog.Builder(requireContext())
|
||||
.setTitle(R.string.share_playlist)
|
||||
.setMessage(R.string.share_playlist_with_titles_message)
|
||||
.setCancelable(true)
|
||||
.setPositiveButton(R.string.share_playlist_with_titles, (dialog, which) ->
|
||||
sharePlaylist(/* shouldSharePlaylistDetails= */ true)
|
||||
sharePlaylist(WITH_TITLES)
|
||||
)
|
||||
.setNeutralButton(R.string.share_playlist_as_youtube_temporary_playlist,
|
||||
(dialog, which) -> sharePlaylist(YOUTUBE_TEMP_PLAYLIST)
|
||||
)
|
||||
.setNegativeButton(R.string.share_playlist_with_list, (dialog, which) ->
|
||||
sharePlaylist(/* shouldSharePlaylistDetails= */ false)
|
||||
sharePlaylist(JUST_URLS)
|
||||
)
|
||||
.show();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
package org.schabi.newpipe.local.playlist;
|
||||
|
||||
public enum PlayListShareMode {
|
||||
|
||||
JUST_URLS,
|
||||
WITH_TITLES,
|
||||
YOUTUBE_TEMP_PLAYLIST
|
||||
}
|
||||
@@ -26,6 +26,10 @@ public class RemotePlaylistManager {
|
||||
return playlistRemoteTable.getPlaylists().subscribeOn(Schedulers.io());
|
||||
}
|
||||
|
||||
public Flowable<PlaylistRemoteEntity> getPlaylist(final long playlistId) {
|
||||
return playlistRemoteTable.getPlaylist(playlistId).subscribeOn(Schedulers.io());
|
||||
}
|
||||
|
||||
public Flowable<List<PlaylistRemoteEntity>> getPlaylist(final PlaylistInfo info) {
|
||||
return playlistRemoteTable.getPlaylist(info.getServiceId(), info.getUrl())
|
||||
.subscribeOn(Schedulers.io());
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package org.schabi.newpipe.local.subscription;
|
||||
|
||||
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
|
||||
|
||||
import android.app.Dialog;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
@@ -10,13 +12,11 @@ import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.fragment.app.DialogFragment;
|
||||
import androidx.fragment.app.Fragment;
|
||||
|
||||
import com.evernote.android.state.State;
|
||||
import com.livefront.bridge.Bridge;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
|
||||
import icepick.Icepick;
|
||||
import icepick.State;
|
||||
|
||||
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
|
||||
|
||||
public class ImportConfirmationDialog extends DialogFragment {
|
||||
@State
|
||||
protected Intent resultServiceIntent;
|
||||
@@ -57,12 +57,12 @@ public class ImportConfirmationDialog extends DialogFragment {
|
||||
throw new IllegalStateException("Result intent is null");
|
||||
}
|
||||
|
||||
Icepick.restoreInstanceState(this, savedInstanceState);
|
||||
Bridge.restoreInstanceState(this, savedInstanceState);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSaveInstanceState(@NonNull final Bundle outState) {
|
||||
super.onSaveInstanceState(outState);
|
||||
Icepick.saveInstanceState(this, outState);
|
||||
Bridge.saveInstanceState(this, outState);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,11 +20,11 @@ import androidx.annotation.StringRes
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import com.evernote.android.state.State
|
||||
import com.xwray.groupie.Group
|
||||
import com.xwray.groupie.GroupAdapter
|
||||
import com.xwray.groupie.Section
|
||||
import com.xwray.groupie.viewbinding.GroupieViewHolder
|
||||
import icepick.State
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||
import org.schabi.newpipe.R
|
||||
import org.schabi.newpipe.database.feed.model.FeedGroupEntity.Companion.GROUP_ALL_ID
|
||||
@@ -56,6 +56,7 @@ import org.schabi.newpipe.local.subscription.services.SubscriptionsImportService
|
||||
import org.schabi.newpipe.local.subscription.services.SubscriptionsImportService.PREVIOUS_EXPORT_MODE
|
||||
import org.schabi.newpipe.streams.io.NoFileManagerSafeGuard
|
||||
import org.schabi.newpipe.streams.io.StoredFileHelper
|
||||
import org.schabi.newpipe.ui.emptystate.setEmptyStateComposable
|
||||
import org.schabi.newpipe.util.NavigationHelper
|
||||
import org.schabi.newpipe.util.OnClickGesture
|
||||
import org.schabi.newpipe.util.ServiceHelper
|
||||
@@ -129,6 +130,7 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
|
||||
// Menu
|
||||
// ////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@Deprecated("Deprecated in Java")
|
||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||
super.onCreateOptionsMenu(menu, inflater)
|
||||
|
||||
@@ -256,6 +258,8 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
|
||||
binding.itemsList.adapter = groupAdapter
|
||||
binding.itemsList.itemAnimator = null
|
||||
|
||||
binding.emptyStateView.setEmptyStateComposable()
|
||||
|
||||
viewModel = ViewModelProvider(this)[SubscriptionViewModel::class.java]
|
||||
viewModel.stateLiveData.observe(viewLifecycleOwner) { it?.let(this::handleResult) }
|
||||
viewModel.feedGroupsLiveData.observe(viewLifecycleOwner) {
|
||||
|
||||
@@ -27,6 +27,8 @@ import androidx.annotation.StringRes;
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
import androidx.core.text.util.LinkifyCompat;
|
||||
|
||||
import com.evernote.android.state.State;
|
||||
|
||||
import org.schabi.newpipe.BaseFragment;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.error.ErrorInfo;
|
||||
@@ -44,8 +46,6 @@ import org.schabi.newpipe.util.ServiceHelper;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import icepick.State;
|
||||
|
||||
public class SubscriptionsImportFragment extends BaseFragment {
|
||||
@State
|
||||
int currentServiceId = Constants.NO_SERVICE_ID;
|
||||
|
||||
@@ -18,11 +18,11 @@ import androidx.lifecycle.Observer
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.evernote.android.state.State
|
||||
import com.livefront.bridge.Bridge
|
||||
import com.xwray.groupie.GroupieAdapter
|
||||
import com.xwray.groupie.OnItemClickListener
|
||||
import com.xwray.groupie.Section
|
||||
import icepick.Icepick
|
||||
import icepick.State
|
||||
import org.schabi.newpipe.R
|
||||
import org.schabi.newpipe.database.feed.model.FeedGroupEntity
|
||||
import org.schabi.newpipe.databinding.DialogFeedGroupCreateBinding
|
||||
@@ -78,7 +78,7 @@ class FeedGroupDialog : DialogFragment(), BackPressable {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
Icepick.restoreInstanceState(this, savedInstanceState)
|
||||
Bridge.restoreInstanceState(this, savedInstanceState)
|
||||
|
||||
setStyle(STYLE_NO_TITLE, ThemeHelper.getMinWidthDialogTheme(requireContext()))
|
||||
groupId = arguments?.getLong(KEY_GROUP_ID, NO_GROUP_SELECTED) ?: NO_GROUP_SELECTED
|
||||
@@ -94,6 +94,7 @@ class FeedGroupDialog : DialogFragment(), BackPressable {
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
return object : Dialog(requireActivity(), theme) {
|
||||
@Deprecated("Deprecated in Java")
|
||||
override fun onBackPressed() {
|
||||
if (!this@FeedGroupDialog.onBackPressed()) {
|
||||
super.onBackPressed()
|
||||
@@ -114,7 +115,7 @@ class FeedGroupDialog : DialogFragment(), BackPressable {
|
||||
iconsListState = feedGroupCreateBinding.iconSelector.layoutManager?.onSaveInstanceState()
|
||||
subscriptionsListState = feedGroupCreateBinding.subscriptionsSelectorList.layoutManager?.onSaveInstanceState()
|
||||
|
||||
Icepick.saveInstanceState(this, outState)
|
||||
Bridge.saveInstanceState(this, outState)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
|
||||
@@ -11,10 +11,10 @@ import androidx.recyclerview.widget.ItemTouchHelper
|
||||
import androidx.recyclerview.widget.ItemTouchHelper.SimpleCallback
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.evernote.android.state.State
|
||||
import com.livefront.bridge.Bridge
|
||||
import com.xwray.groupie.GroupieAdapter
|
||||
import com.xwray.groupie.TouchCallback
|
||||
import icepick.Icepick
|
||||
import icepick.State
|
||||
import org.schabi.newpipe.R
|
||||
import org.schabi.newpipe.database.feed.model.FeedGroupEntity
|
||||
import org.schabi.newpipe.databinding.DialogFeedGroupReorderBinding
|
||||
@@ -23,10 +23,6 @@ import org.schabi.newpipe.local.subscription.dialog.FeedGroupReorderDialogViewMo
|
||||
import org.schabi.newpipe.local.subscription.item.FeedGroupReorderItem
|
||||
import org.schabi.newpipe.util.ThemeHelper
|
||||
import java.util.Collections
|
||||
import kotlin.collections.ArrayList
|
||||
import kotlin.collections.List
|
||||
import kotlin.collections.map
|
||||
import kotlin.collections.sortedBy
|
||||
|
||||
class FeedGroupReorderDialog : DialogFragment() {
|
||||
private var _binding: DialogFeedGroupReorderBinding? = null
|
||||
@@ -42,7 +38,7 @@ class FeedGroupReorderDialog : DialogFragment() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
Icepick.restoreInstanceState(this, savedInstanceState)
|
||||
Bridge.restoreInstanceState(this, savedInstanceState)
|
||||
|
||||
setStyle(STYLE_NO_TITLE, ThemeHelper.getMinWidthDialogTheme(requireContext()))
|
||||
}
|
||||
@@ -80,7 +76,7 @@ class FeedGroupReorderDialog : DialogFragment() {
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
super.onSaveInstanceState(outState)
|
||||
Icepick.saveInstanceState(this, outState)
|
||||
Bridge.saveInstanceState(this, outState)
|
||||
}
|
||||
|
||||
private fun handleGroups(list: List<FeedGroupEntity>) {
|
||||
|
||||
@@ -9,7 +9,7 @@ import org.schabi.newpipe.R
|
||||
import org.schabi.newpipe.extractor.channel.ChannelInfoItem
|
||||
import org.schabi.newpipe.util.Localization
|
||||
import org.schabi.newpipe.util.OnClickGesture
|
||||
import org.schabi.newpipe.util.image.PicassoHelper
|
||||
import org.schabi.newpipe.util.image.CoilHelper
|
||||
|
||||
class ChannelItem(
|
||||
private val infoItem: ChannelInfoItem,
|
||||
@@ -39,7 +39,7 @@ class ChannelItem(
|
||||
itemChannelDescriptionView.text = infoItem.description
|
||||
}
|
||||
|
||||
PicassoHelper.loadAvatar(infoItem.thumbnails).into(itemThumbnailView)
|
||||
CoilHelper.loadAvatar(itemThumbnailView, infoItem.thumbnails)
|
||||
|
||||
gesturesListener?.run {
|
||||
viewHolder.root.setOnClickListener { selected(infoItem) }
|
||||
|
||||
@@ -3,14 +3,18 @@ package org.schabi.newpipe.local.subscription.item
|
||||
import android.view.View
|
||||
import com.xwray.groupie.viewbinding.BindableItem
|
||||
import org.schabi.newpipe.R
|
||||
import org.schabi.newpipe.databinding.ListEmptyViewBinding
|
||||
import org.schabi.newpipe.databinding.ListEmptyViewSubscriptionsBinding
|
||||
import org.schabi.newpipe.ui.emptystate.EmptyStateSpec
|
||||
import org.schabi.newpipe.ui.emptystate.setEmptyStateComposable
|
||||
|
||||
/**
|
||||
* When there are no subscriptions, show a hint to the user about how to import subscriptions
|
||||
*/
|
||||
class ImportSubscriptionsHintPlaceholderItem : BindableItem<ListEmptyViewBinding>() {
|
||||
class ImportSubscriptionsHintPlaceholderItem : BindableItem<ListEmptyViewSubscriptionsBinding>() {
|
||||
override fun getLayout(): Int = R.layout.list_empty_view_subscriptions
|
||||
override fun bind(viewBinding: ListEmptyViewBinding, position: Int) {}
|
||||
override fun bind(viewBinding: ListEmptyViewSubscriptionsBinding, position: Int) {
|
||||
viewBinding.root.setEmptyStateComposable(EmptyStateSpec.NoSubscriptionsHint)
|
||||
}
|
||||
override fun getSpanSize(spanCount: Int, position: Int): Int = spanCount
|
||||
override fun initializeViewBinding(view: View) = ListEmptyViewBinding.bind(view)
|
||||
override fun initializeViewBinding(view: View) = ListEmptyViewSubscriptionsBinding.bind(view)
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import org.schabi.newpipe.database.subscription.SubscriptionEntity
|
||||
import org.schabi.newpipe.databinding.PickerSubscriptionItemBinding
|
||||
import org.schabi.newpipe.ktx.AnimationType
|
||||
import org.schabi.newpipe.ktx.animate
|
||||
import org.schabi.newpipe.util.image.PicassoHelper
|
||||
import org.schabi.newpipe.util.image.CoilHelper
|
||||
|
||||
data class PickerSubscriptionItem(
|
||||
val subscriptionEntity: SubscriptionEntity,
|
||||
@@ -21,7 +21,7 @@ data class PickerSubscriptionItem(
|
||||
override fun getSpanSize(spanCount: Int, position: Int): Int = 1
|
||||
|
||||
override fun bind(viewBinding: PickerSubscriptionItemBinding, position: Int) {
|
||||
PicassoHelper.loadAvatar(subscriptionEntity.avatarUrl).into(viewBinding.thumbnailView)
|
||||
CoilHelper.loadAvatar(viewBinding.thumbnailView, subscriptionEntity.avatarUrl)
|
||||
viewBinding.titleView.text = subscriptionEntity.name
|
||||
viewBinding.selectedHighlight.isVisible = isSelected
|
||||
}
|
||||
|
||||
@@ -76,7 +76,10 @@ public class SubscriptionsExportService extends BaseImportExportService {
|
||||
|
||||
try {
|
||||
outFile = new StoredFileHelper(this, path, "application/json");
|
||||
outputStream = new SharpOutputStream(outFile.getStream());
|
||||
// truncate the file before writing to it, otherwise if the new content is smaller than
|
||||
// the previous file size, the file will retain part of the previous content and be
|
||||
// corrupted
|
||||
outputStream = new SharpOutputStream(outFile.openAndTruncateStream());
|
||||
} catch (final IOException e) {
|
||||
handleError(e);
|
||||
return START_NOT_STICKY;
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
package org.schabi.newpipe.paging
|
||||
|
||||
import androidx.paging.PagingSource
|
||||
import androidx.paging.PagingState
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.schabi.newpipe.extractor.NewPipe
|
||||
import org.schabi.newpipe.extractor.Page
|
||||
import org.schabi.newpipe.extractor.comments.CommentsInfo
|
||||
import org.schabi.newpipe.extractor.comments.CommentsInfoItem
|
||||
|
||||
class CommentRepliesSource(
|
||||
private val commentInfo: CommentsInfoItem,
|
||||
) : PagingSource<Page, CommentsInfoItem>() {
|
||||
private val service = NewPipe.getService(commentInfo.serviceId)
|
||||
|
||||
override suspend fun load(params: LoadParams<Page>): LoadResult<Page, CommentsInfoItem> {
|
||||
// params.key is null the first time load() is called, and we need to return the first page
|
||||
val repliesPage = params.key ?: commentInfo.replies
|
||||
val info = withContext(Dispatchers.IO) {
|
||||
CommentsInfo.getMoreItems(service, commentInfo.url, repliesPage)
|
||||
}
|
||||
return LoadResult.Page(info.items, null, info.nextPage)
|
||||
}
|
||||
|
||||
override fun getRefreshKey(state: PagingState<Page, CommentsInfoItem>) = null
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user