mirror of
https://github.com/TeamNewPipe/NewPipe
synced 2026-01-14 19:37:55 +00:00
Compare commits
421 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
47f9ed08e9 | ||
|
|
ee3c06394d | ||
|
|
443ebc46d6 | ||
|
|
c42f29446d | ||
|
|
1030e09fc1 | ||
|
|
96b930cd07 | ||
|
|
de08edb831 | ||
|
|
af80d96b9e | ||
|
|
3c23fb0b13 | ||
|
|
750490cd2f | ||
|
|
579b8611be | ||
|
|
da12b92d75 | ||
|
|
a3f99bd781 | ||
|
|
7ae908a466 | ||
|
|
00767f4bf3 | ||
|
|
54f0b3d8b3 | ||
|
|
08eb70833d | ||
|
|
42aafd3a2d | ||
|
|
2acaefdb2a | ||
|
|
9c2cdd2513 | ||
|
|
01683aa816 | ||
|
|
85f701b94e | ||
|
|
ff7cfe4715 | ||
|
|
d3cd3d62b4 | ||
|
|
91c67b085b | ||
|
|
cd8c7ec3c0 | ||
|
|
2c51a7970d | ||
|
|
fb362022f7 | ||
|
|
2814ae6d3c | ||
|
|
7225199deb | ||
|
|
c08a4e851b | ||
|
|
9f8e8c0856 | ||
|
|
e2a7b9ac56 | ||
|
|
5e593f687d | ||
|
|
3223ec04e3 | ||
|
|
f388a1af67 | ||
|
|
c1fe5c8d07 | ||
|
|
608e73e2f2 | ||
|
|
2e538b8959 | ||
|
|
1278fc27ae | ||
|
|
be95d7fe0f | ||
|
|
377914f1d8 | ||
|
|
5bf439ad9e | ||
|
|
3b1b23ba2a | ||
|
|
9274e6417a | ||
|
|
dce6565af4 | ||
|
|
8b3aec5edb | ||
|
|
b0e4f947ea | ||
|
|
79060f0bfe | ||
|
|
91bcd8766a | ||
|
|
4e633504a8 | ||
|
|
144a10f7a6 | ||
|
|
72a2644f25 | ||
|
|
e865c4350e | ||
|
|
52cc4a0a05 | ||
|
|
e103e4817c | ||
|
|
d0637a8832 | ||
|
|
94f774b82d | ||
|
|
651b79d3ed | ||
|
|
9e5b9ca326 | ||
|
|
dfa606ef49 | ||
|
|
2886bc3b01 | ||
|
|
71c5aaa11e | ||
|
|
466db83375 | ||
|
|
17c0fffd73 | ||
|
|
8a069b497f | ||
|
|
af79479716 | ||
|
|
8cfe8c17e3 | ||
|
|
5108d75682 | ||
|
|
ac53196dcc | ||
|
|
1e652b159e | ||
|
|
ea07d7751b | ||
|
|
82de35d724 | ||
|
|
f55e8ea3aa | ||
|
|
7067ebdd12 | ||
|
|
03bb2123f2 | ||
|
|
e2f449f0c8 | ||
|
|
b16e972710 | ||
|
|
37cd71328c | ||
|
|
9b2c86a37b | ||
|
|
ce4dd33eab | ||
|
|
8bbc3e531c | ||
|
|
c5a06243a6 | ||
|
|
bebd2b449c | ||
|
|
658168eb8d | ||
|
|
6b23df0659 | ||
|
|
d59314801c | ||
|
|
0f45c69388 | ||
|
|
52542e04e8 | ||
|
|
7fc0a3841a | ||
|
|
22db4175f3 | ||
|
|
8fc935b09d | ||
|
|
07fb319e88 | ||
|
|
12a78a826d | ||
|
|
4a061f20ed | ||
|
|
f3be89b503 | ||
|
|
12acaf29dd | ||
|
|
683d9816cb | ||
|
|
8802582997 | ||
|
|
983c98d262 | ||
|
|
c38389672a | ||
|
|
93148400a2 | ||
|
|
194e43f5cb | ||
|
|
08c928e1d0 | ||
|
|
69dacb34b9 | ||
|
|
60c3a2dc9c | ||
|
|
b8e5e036b2 | ||
|
|
2f87305f2d | ||
|
|
15dc99f110 | ||
|
|
2d907706ea | ||
|
|
f5dbb07893 | ||
|
|
a437672dc1 | ||
|
|
388a4860b5 | ||
|
|
4b72ee53b0 | ||
|
|
d77c23ed34 | ||
|
|
31635c122e | ||
|
|
afef793fbb | ||
|
|
3bc2ec90ef | ||
|
|
a3e68c93f8 | ||
|
|
15e6f1cb3b | ||
|
|
89c540c520 | ||
|
|
6632720bc3 | ||
|
|
b5662c2d07 | ||
|
|
0f74c2463e | ||
|
|
fdfdf94cb9 | ||
|
|
8595078053 | ||
|
|
80be089ca9 | ||
|
|
96ab2f855e | ||
|
|
4206ae84c1 | ||
|
|
2f21523da9 | ||
|
|
6c1222ea32 | ||
|
|
ba50de236c | ||
|
|
bef8882a7c | ||
|
|
0d8b7e23e7 | ||
|
|
864c19e7dc | ||
|
|
4b0ed9de5d | ||
|
|
d18a34b766 | ||
|
|
0cf24c5d36 | ||
|
|
fa293e3415 | ||
|
|
1531a5112c | ||
|
|
e127db6fa6 | ||
|
|
49b1649348 | ||
|
|
9f503917c2 | ||
|
|
54ef604569 | ||
|
|
30ce906f72 | ||
|
|
1c20eabb48 | ||
|
|
f8c52c4dac | ||
|
|
345ba74d58 | ||
|
|
d2aaf152a0 | ||
|
|
7bf1f3dba6 | ||
|
|
452fe3a8e2 | ||
|
|
c25e523df6 | ||
|
|
65bb1dcdbf | ||
|
|
fe42206e94 | ||
|
|
dac47d9f52 | ||
|
|
83a3d11f38 | ||
|
|
03d5372525 | ||
|
|
a454a41b51 | ||
|
|
95631dba46 | ||
|
|
ee827407aa | ||
|
|
3aebfa22e9 | ||
|
|
72eb3b4415 | ||
|
|
3a40759cd2 | ||
|
|
6f44ced7b6 | ||
|
|
81843ddb6e | ||
|
|
23d14ab443 | ||
|
|
3d3d94655b | ||
|
|
a6515d5450 | ||
|
|
2d0da2c7a4 | ||
|
|
f05affa984 | ||
|
|
cd265fc31f | ||
|
|
3c21be8fa5 | ||
|
|
f681b0bb5a | ||
|
|
d7fbddf6f8 | ||
|
|
993c34911a | ||
|
|
4a7cfd1a6c | ||
|
|
402990dd9d | ||
|
|
41faf70da1 | ||
|
|
15e3b6301c | ||
|
|
5b9c28b93b | ||
|
|
6672169707 | ||
|
|
9ff1baefde | ||
|
|
552734faa5 | ||
|
|
7268e04361 | ||
|
|
45d8fef00c | ||
|
|
0f83497284 | ||
|
|
1475ff805f | ||
|
|
7907182e7e | ||
|
|
0f457127df | ||
|
|
064242d962 | ||
|
|
ddcbe27fd3 | ||
|
|
ee19ea66b3 | ||
|
|
6b490ee547 | ||
|
|
e127697fff | ||
|
|
558c9147a2 | ||
|
|
4147c7c1d1 | ||
|
|
45ef9b0278 | ||
|
|
fc0e709817 | ||
|
|
b67bf16d4f | ||
|
|
fb3be544ce | ||
|
|
53f5741317 | ||
|
|
07015973d2 | ||
|
|
215880207e | ||
|
|
41c4ab5739 | ||
|
|
ff8868f6a3 | ||
|
|
8c6e37d1d1 | ||
|
|
c90237c14c | ||
|
|
989bcbf895 | ||
|
|
19dd9d266a | ||
|
|
05370dbb94 | ||
|
|
f3edc69897 | ||
|
|
f6cad2d9cf | ||
|
|
dc67628ba5 | ||
|
|
37b8a9375f | ||
|
|
d71af9a625 | ||
|
|
a163d5461d | ||
|
|
a274baf5cd | ||
|
|
96eb1425f8 | ||
|
|
361760be0a | ||
|
|
eea2768633 | ||
|
|
d3562c70f5 | ||
|
|
e06342eacf | ||
|
|
e8d909553d | ||
|
|
b21d231e3a | ||
|
|
4058277b7a | ||
|
|
dd9772cde2 | ||
|
|
a924f819a9 | ||
|
|
156bbad5b5 | ||
|
|
2963cd5c6e | ||
|
|
7d6688f497 | ||
|
|
b056faa97f | ||
|
|
3ff00ff50e | ||
|
|
baee915db5 | ||
|
|
4e6dcc693b | ||
|
|
3750561b4d | ||
|
|
6b026557d4 | ||
|
|
1ee137bbda | ||
|
|
c92a90749e | ||
|
|
e806f8c4e6 | ||
|
|
8a5e2ffa57 | ||
|
|
ad405d9e0b | ||
|
|
b9ee14ac30 | ||
|
|
bb49b1cfb1 | ||
|
|
4fc9443b4f | ||
|
|
581ede022e | ||
|
|
f86fc03c46 | ||
|
|
75db002369 | ||
|
|
dbfa4e554b | ||
|
|
84d87a2e60 | ||
|
|
9e3577e77b | ||
|
|
41a0dc1abd | ||
|
|
950956ebf2 | ||
|
|
c000c1d455 | ||
|
|
c8e2ab4c83 | ||
|
|
397f93b079 | ||
|
|
09d137f740 | ||
|
|
81f740d409 | ||
|
|
1d2642f1e3 | ||
|
|
7cd3603bbb | ||
|
|
ec7de2a6dc | ||
|
|
3d1a3606c9 | ||
|
|
6472e9b6b6 | ||
|
|
2c88e9d068 | ||
|
|
4825a0a35f | ||
|
|
7adebbe989 | ||
|
|
122b0b0de4 | ||
|
|
744cfe5672 | ||
|
|
17724a901c | ||
|
|
7dc85af5fb | ||
|
|
c7daf32904 | ||
|
|
b2323859e5 | ||
|
|
4c8dca5300 | ||
|
|
68e7fcf8ee | ||
|
|
f78983b16b | ||
|
|
ef91214085 | ||
|
|
dc09a4621b | ||
|
|
2f99a217c3 | ||
|
|
6992b2c308 | ||
|
|
0d51eefbb9 | ||
|
|
aa28a85747 | ||
|
|
f18ee8e83d | ||
|
|
fb58967766 | ||
|
|
c3f1478fde | ||
|
|
e5c00a7ef4 | ||
|
|
769791af7a | ||
|
|
e632fab4d0 | ||
|
|
91611fcae4 | ||
|
|
6cd25d7e55 | ||
|
|
c9488eb042 | ||
|
|
c8516a04dc | ||
|
|
02d1b98b1c | ||
|
|
d8236bbedd | ||
|
|
1de21fb0c2 | ||
|
|
13cac07b8d | ||
|
|
bd9dcfb28a | ||
|
|
d5199eac3e | ||
|
|
7638d229c0 | ||
|
|
a641c5bb58 | ||
|
|
1e0c9f46ad | ||
|
|
4eb02f584e | ||
|
|
700c1b4b25 | ||
|
|
4b4337e078 | ||
|
|
38ce800685 | ||
|
|
2310e8c1d6 | ||
|
|
1b2b3a4f88 | ||
|
|
d11129a76b | ||
|
|
02789122a0 | ||
|
|
676bc02d52 | ||
|
|
8b807b0706 | ||
|
|
72dfe974ab | ||
|
|
316db0e4c6 | ||
|
|
010c607e40 | ||
|
|
3e099fb2a3 | ||
|
|
9c9730b152 | ||
|
|
9e44053e22 | ||
|
|
dee32c3dc5 | ||
|
|
344fbff59a | ||
|
|
e39a816bdc | ||
|
|
605b8fac5e | ||
|
|
dfba10f8ae | ||
|
|
48a1ab64b0 | ||
|
|
dd2cde3c1a | ||
|
|
1b9c2b37c5 | ||
|
|
eae1f8b597 | ||
|
|
18ce86c2ed | ||
|
|
d5f25e05d9 | ||
|
|
53303ac5d3 | ||
|
|
90cc8e2144 | ||
|
|
adf9badbf6 | ||
|
|
c35fe4f3f1 | ||
|
|
63291f8101 | ||
|
|
62efb588ef | ||
|
|
203ca9afc6 | ||
|
|
a23f941ac8 | ||
|
|
b0a10f0542 | ||
|
|
478ad42977 | ||
|
|
0764983ac6 | ||
|
|
2b2f1ee8f5 | ||
|
|
28f167fd99 | ||
|
|
272be36dd9 | ||
|
|
f933db8117 | ||
|
|
cddb9bccb9 | ||
|
|
b5ad24eb47 | ||
|
|
ad8f791f71 | ||
|
|
2e862b4ccc | ||
|
|
ecac897e7b | ||
|
|
702adb53a7 | ||
|
|
4ea962f523 | ||
|
|
acaf92d671 | ||
|
|
c673cb6157 | ||
|
|
c0f7b123a3 | ||
|
|
bc2f0f9f3e | ||
|
|
e9e2afa61a | ||
|
|
403154b2e1 | ||
|
|
e5fd24b0d1 | ||
|
|
8dc34274a1 | ||
|
|
467bd21de2 | ||
|
|
5c9705d94e | ||
|
|
85fb5827aa | ||
|
|
0bcc9bd3ba | ||
|
|
25e120bec1 | ||
|
|
7067deb328 | ||
|
|
f6efd302dc | ||
|
|
61972141ae | ||
|
|
af936bc646 | ||
|
|
d66f933c69 | ||
|
|
cf81c37683 | ||
|
|
d2306b0fd7 | ||
|
|
94dfabf3dc | ||
|
|
5522dc10b8 | ||
|
|
0ae04b8ead | ||
|
|
44cad27d0a | ||
|
|
5d59025b3c | ||
|
|
768bb0bbcd | ||
|
|
ac071b383f | ||
|
|
e0b1a6b88b | ||
|
|
ed86b1c572 | ||
|
|
b6c2bade73 | ||
|
|
b6b19b474e | ||
|
|
231b7492fb | ||
|
|
b4950fcb2e | ||
|
|
b79ea7b51b | ||
|
|
28c72e7f63 | ||
|
|
5fcc3b4dab | ||
|
|
51837ce36f | ||
|
|
ddaafb68c8 | ||
|
|
a744775fe7 | ||
|
|
50b85a7734 | ||
|
|
aab09c0c65 | ||
|
|
3ded6feddb | ||
|
|
c8802fe5d0 | ||
|
|
411b3129f9 | ||
|
|
a55acd38df | ||
|
|
e7773d8807 | ||
|
|
7edef8d5a2 | ||
|
|
03d2ca9f9f | ||
|
|
2271ea4281 | ||
|
|
afc8db8f81 | ||
|
|
4af49ee5a6 | ||
|
|
d7b29aae5c | ||
|
|
9f7a8407ca | ||
|
|
a2050a5211 | ||
|
|
048743c062 | ||
|
|
e9bd2934c3 | ||
|
|
50634eb2b3 | ||
|
|
08489b81fb | ||
|
|
a2ff770afc | ||
|
|
658d988254 | ||
|
|
9d7e9289bb | ||
|
|
12aac09c7b | ||
|
|
d7d87691cb | ||
|
|
731640997e | ||
|
|
64d7432852 | ||
|
|
1c9f68bcae | ||
|
|
4fde62ff89 | ||
|
|
ceb55d0ede | ||
|
|
87c958b2e7 | ||
|
|
d844e0aba6 | ||
|
|
a953aab9b4 | ||
|
|
108af48b76 | ||
|
|
62d36126ea |
1
.github/FUNDING.yml
vendored
1
.github/FUNDING.yml
vendored
@@ -1 +1,2 @@
|
||||
liberapay: TeamNewPipe
|
||||
custom: 'https://newpipe.net/donate/'
|
||||
|
||||
65
.github/ISSUE_TEMPLATE/bug_report.md
vendored
65
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -1,65 +0,0 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a bug report to help us improve
|
||||
labels: bug
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
<!--
|
||||
Oh no, a bug! It happens. Thanks for reporting an issue with NewPipe. To make it easier for us to help you please enter detailed information in the template we have provided below. If a section isn't relevant, just delete it, though it would be helpful to still provide as much detail as possible.
|
||||
-->
|
||||
|
||||
<!-- IF YOU DON'T FILL IN THE TEMPLATE PROPERLY, YOUR ISSUE IS LIABLE TO BE CLOSED. If you feel tired/lazy right now, open your issue some other time. We'll wait. -->
|
||||
|
||||
<!-- The comments between these brackets won't show up in the submitted issue (as you can see in the Preview). -->
|
||||
|
||||
### Checklist
|
||||
<!-- This checklist is COMPULSORY. The first box has been checked for you to show you how it is done. -->
|
||||
|
||||
- [x] I am using the latest version - x.xx.x <!-- Check https://github.com/TeamNewPipe/NewPipe/releases -->
|
||||
- [ ] I checked, but didn't find any duplicates (open OR closed) of this issue in the repo. <!-- Seriously, check. O_O -->
|
||||
- [ ] I have read the contribution guidelines given at https://github.com/TeamNewPipe/NewPipe/blob/HEAD/.github/CONTRIBUTING.md.
|
||||
- [ ] This issue contains only one bug. I will open one issue for every bug report I want to file.
|
||||
|
||||
### Steps to reproduce the bug
|
||||
<!--
|
||||
1. Go to '...'
|
||||
2. Press on '....'
|
||||
3. Swipe down to '....'
|
||||
-->
|
||||
|
||||
<!-- If you can't cause the bug to show up again reliably (and hence don't have a proper set of steps to give us), please still try to give as many details as possible on how you think you encountered the bug. -->
|
||||
|
||||
|
||||
|
||||
### Actual behavior
|
||||
<!-- Tell us what happens with the steps given above. -->
|
||||
|
||||
|
||||
|
||||
### Expected behavior
|
||||
<!-- Tell us what you expect to happen. -->
|
||||
|
||||
|
||||
|
||||
### Screenshots/Screen recordings
|
||||
<!-- If applicable, add screenshots or a screen recording to help explain your problem. GitHub supports uploading them directly in the issue text box. If your file is too big for Github to accept, feel free to paste a link from an image/video hoster here instead. -->
|
||||
|
||||
<!-- DON'T POST SCREENSHOTS OF THE ERROR PAGE. Use the buttons given on the error page to paste the error as text in the Logs section below. -->
|
||||
|
||||
|
||||
|
||||
### Logs
|
||||
<!-- If your bug includes a crash (where you're shown the Error Report page with a bunch of info), tap on "Copy formatted report" at the bottom and paste it here: -->
|
||||
|
||||
<!-- That's right, here! -->
|
||||
|
||||
|
||||
|
||||
<!-- Please fill this section if you did not provide a log generated by NewPipe -->
|
||||
|
||||
### Device info
|
||||
|
||||
- Android version/Custom ROM version:
|
||||
- Device model:
|
||||
113
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
113
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
@@ -0,0 +1,113 @@
|
||||
name: Bug report
|
||||
description: Create a bug report to help us improve
|
||||
labels: [bug]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thank you for helping to make NewPipe better by reporting a bug. :hugs:
|
||||
|
||||
Please fill in as much information as possible about your bug so that we don't have to play "information ping-pong" and can help you immediately.
|
||||
|
||||
- type: checkboxes
|
||||
id: checklist
|
||||
attributes:
|
||||
label: "Checklist"
|
||||
options:
|
||||
- label: "I am able to reproduce the bug with the [latest version](https://github.com/TeamNewPipe/NewPipe/releases/latest)."
|
||||
required: true
|
||||
- label: "I made sure that there are *no existing issues* - [open](https://github.com/TeamNewPipe/NewPipe/issues) or [closed](https://github.com/TeamNewPipe/NewPipe/issues?q=is%3Aissue+is%3Aclosed) - which I could contribute my information to."
|
||||
required: true
|
||||
- label: "I have taken the time to fill in all the required details. I understand that the bug report will be dismissed otherwise."
|
||||
required: true
|
||||
- label: "This issue contains only one bug."
|
||||
required: true
|
||||
- label: "I have read and understood the [contribution guidelines](https://github.com/TeamNewPipe/NewPipe/blob/dev/.github/CONTRIBUTING.md)."
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: app-version
|
||||
attributes:
|
||||
label: Affected version
|
||||
description: "In which NewPipe version did you encounter the bug?"
|
||||
placeholder: "x.xx.x - Can be seen in the app from the 'About' section in the sidebar"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: steps-to-reproduce
|
||||
attributes:
|
||||
label: Steps to reproduce the bug
|
||||
description: |
|
||||
What did you do for the bug to show up?
|
||||
|
||||
If you can't cause the bug to show up again reliably (and hence don't have a proper set of steps to give us), please still try to give as many details as possible on how you think you encountered the bug.
|
||||
placeholder: |
|
||||
1. Go to '...'
|
||||
2. Press on '....'
|
||||
3. Swipe down to '....'
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: expected-behavior
|
||||
attributes:
|
||||
label: Expected behavior
|
||||
description: |
|
||||
Tell us what you expect to happen.
|
||||
|
||||
- type: textarea
|
||||
id: actual-behavior
|
||||
attributes:
|
||||
label: Actual behavior
|
||||
description: |
|
||||
Tell us what happens with the steps given above.
|
||||
|
||||
- type: textarea
|
||||
id: screen-media
|
||||
attributes:
|
||||
label: Screenshots/Screen recordings
|
||||
description: |
|
||||
A picture or video is worth a thousand words.
|
||||
|
||||
If applicable, add screenshots or a screen recording to help explain your problem.
|
||||
GitHub supports uploading them directly in the text box.
|
||||
If your file is too big for Github to accept, try to compress it (ZIP-file) or feel free to paste a link to an image/video hoster here instead.
|
||||
|
||||
:heavy_exclamation_mark: DON'T POST SCREENSHOTS OF THE ERROR PAGE.
|
||||
Instead, follow the instructions in the "Logs" section below.
|
||||
|
||||
- type: textarea
|
||||
id: logs
|
||||
attributes:
|
||||
label: Logs
|
||||
description: |
|
||||
If your bug includes a crash (where you're shown the Error Report page with a bunch of info), tap on "Copy formatted report" at the bottom and paste it here.
|
||||
|
||||
- type: input
|
||||
id: device-os-info
|
||||
attributes:
|
||||
label: Affected Android/Custom ROM version
|
||||
description: |
|
||||
With what operating system (+ version) did you encounter the bug?
|
||||
placeholder: "Example: Android 12 / LineageOS 18.1"
|
||||
|
||||
- type: input
|
||||
id: device-model-info
|
||||
attributes:
|
||||
label: Affected device model
|
||||
description: |
|
||||
On what device did you encounter the bug?
|
||||
placeholder: "Example: Huawei P20 lite (ANE-LX1) / Samsung Galaxy S20"
|
||||
|
||||
- type: textarea
|
||||
id: additional-information
|
||||
attributes:
|
||||
label: Additional information
|
||||
description: |
|
||||
Any other information you'd like to include, for instance that
|
||||
* the affected device is foldable or a TV
|
||||
* you have disabled all animations on your device
|
||||
* your cat disabled your network connection
|
||||
* ...
|
||||
|
||||
24
.github/ISSUE_TEMPLATE/feature_request.md
vendored
24
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@@ -1,24 +0,0 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
labels: enhancement
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
<!-- IF YOU DON'T FILL IN THE TEMPLATE PROPERLY, YOUR ISSUE IS LIABLE TO BE CLOSED. If you are currently unable to do so for any reason, open your issue some other time. We'll wait. -->
|
||||
|
||||
<!-- The comments between these brackets won't show up in the submitted issue (as you can see in the Preview tab). -->
|
||||
|
||||
### Checklist
|
||||
<!-- This checklist is COMPULSORY. The first box has been checked for you to show you how it is done. -->
|
||||
|
||||
- [x] I checked, but didn't find any duplicates (open OR closed) of this issue in the repo. <!-- Seriously, check. O_O -->
|
||||
- [ ] I have read the contribution guidelines given at https://github.com/TeamNewPipe/NewPipe/blob/HEAD/.github/CONTRIBUTING.md.
|
||||
- [ ] This issue contains only one feature request. I will open one issue for every feature I want to request.
|
||||
|
||||
#### What feature do you want?
|
||||
<!-- Explain how you want the app's look or behavior to change to suit your needs. -->
|
||||
|
||||
|
||||
#### Why do you want this feature?
|
||||
<!-- Describe any problem or limitation you come across while using the app which would be solved by this feature. -->
|
||||
51
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal file
51
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal file
@@ -0,0 +1,51 @@
|
||||
name: Feature request
|
||||
description: Suggest an idea for this project
|
||||
labels: [enhancement]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thank you for helping to make NewPipe better by suggesting a feature. :hugs:
|
||||
|
||||
Your ideas are highly welcome! The app is made for you, the users, after all.
|
||||
|
||||
- type: checkboxes
|
||||
id: checklist
|
||||
attributes:
|
||||
label: "Checklist"
|
||||
options:
|
||||
- label: "I made sure that there are *no existing issues* - [open](https://github.com/TeamNewPipe/NewPipe/issues) or [closed](https://github.com/TeamNewPipe/NewPipe/issues?q=is%3Aissue+is%3Aclosed) - which I could contribute my information to."
|
||||
required: true
|
||||
- label: "I'm aware that this is a request for NewPipe itself and that requests for adding a new service need to be made at [NewPipeExtractor](https://github.com/TeamNewPipe/NewPipeExtractor/issues)."
|
||||
required: true
|
||||
- label: "I have taken the time to fill in all the required details. I understand that the feature request will be dismissed otherwise."
|
||||
required: true
|
||||
- label: "This issue contains only one feature request."
|
||||
required: true
|
||||
- label: "I have read and understood the [contribution guidelines](https://github.com/TeamNewPipe/NewPipe/blob/dev/.github/CONTRIBUTING.md)."
|
||||
required: true
|
||||
|
||||
|
||||
- type: textarea
|
||||
id: feature-description
|
||||
attributes:
|
||||
label: Feature description
|
||||
description: |
|
||||
Explain how you want the app's look or behavior to change to suit your needs.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: why-is-the-feature-requested
|
||||
attributes:
|
||||
label: Why do you want this feature?
|
||||
description: |
|
||||
Describe any problem or limitation you come across while using the app which would be solved by this feature.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: additional-information
|
||||
attributes:
|
||||
label: Additional information
|
||||
description: Any other information you'd like to include, for instance sketches, mockups, pictures of cats, etc.
|
||||
24
.github/ISSUE_TEMPLATE/question.md
vendored
24
.github/ISSUE_TEMPLATE/question.md
vendored
@@ -1,24 +0,0 @@
|
||||
---
|
||||
name: Question
|
||||
about: Ask about anything NewPipe-related
|
||||
labels: question
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
<!-- IF YOU DON'T FILL IN THE TEMPLATE PROPERLY, YOUR ISSUE IS LIABLE TO BE CLOSED. If you feel tired/lazy right now, open your issue some other time. We'll wait. -->
|
||||
|
||||
<!-- The comments between these brackets won't show up in the submitted issue (as you can see in the Preview). -->
|
||||
|
||||
### Checklist
|
||||
<!-- This checklist is COMPULSORY. The first box has been checked for you to show you how it is done. -->
|
||||
|
||||
- [x] I checked, but didn't find any duplicates (open OR closed) of this issue in the repo. <!-- Seriously, check. O_O (If there's already an issue but you'd like to see if something changed, just make a comment on the issue instead of opening a new one.) -->
|
||||
- [ ] I have read the contribution guidelines given at https://github.com/TeamNewPipe/NewPipe/blob/HEAD/.github/CONTRIBUTING.md.
|
||||
|
||||
#### What's your question(s)?
|
||||
|
||||
|
||||
#### Additional context
|
||||
<!-- Add any other context, like screenshots or links, about the question here.
|
||||
Example: *Here's a photo of my cat!* -->
|
||||
35
.github/ISSUE_TEMPLATE/question.yml
vendored
Normal file
35
.github/ISSUE_TEMPLATE/question.yml
vendored
Normal file
@@ -0,0 +1,35 @@
|
||||
name: Question
|
||||
description: Ask about anything NewPipe-related
|
||||
labels: [question]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thanks for taking the time to fill out this issue! :hugs:
|
||||
|
||||
Note that you can also ask questions on our [IRC channel](https://web.libera.chat/#newpipe).
|
||||
|
||||
- type: checkboxes
|
||||
id: checklist
|
||||
attributes:
|
||||
label: "Checklist"
|
||||
options:
|
||||
- label: "I made sure that there are *no existing issues* - [open](https://github.com/TeamNewPipe/NewPipe/issues) or [closed](https://github.com/TeamNewPipe/NewPipe/issues?q=is%3Aissue+is%3Aclosed) - which I could contribute my information to."
|
||||
required: true
|
||||
- label: "I have taken the time to fill in all the required details. I understand that the question will be dismissed otherwise."
|
||||
required: true
|
||||
- label: "I have read and understood the [contribution guidelines](https://github.com/TeamNewPipe/NewPipe/blob/dev/.github/CONTRIBUTING.md)."
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: what-is-the-question
|
||||
attributes:
|
||||
label: What is/are your question(s)?
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: additional-information
|
||||
attributes:
|
||||
label: Additional information
|
||||
description: Any other information you'd like to include, for instance sketches, mockups, pictures of cats, etc.
|
||||
70
.github/workflows/ci.yml
vendored
70
.github/workflows/ci.yml
vendored
@@ -7,19 +7,25 @@ on:
|
||||
- dev
|
||||
- master
|
||||
paths-ignore:
|
||||
- 'README*.md'
|
||||
- 'README.md'
|
||||
- 'doc/**'
|
||||
- 'fastlane/**'
|
||||
- 'assets/**'
|
||||
- '.github/**/*.md'
|
||||
- '.github/FUNDING.yml'
|
||||
- '.github/ISSUE_TEMPLATE/**'
|
||||
push:
|
||||
branches:
|
||||
- dev
|
||||
- master
|
||||
paths-ignore:
|
||||
- 'README*.md'
|
||||
- 'README.md'
|
||||
- 'doc/**'
|
||||
- 'fastlane/**'
|
||||
- 'assets/**'
|
||||
- '.github/**/*.md'
|
||||
- '.github/FUNDING.yml'
|
||||
- '.github/ISSUE_TEMPLATE/**'
|
||||
|
||||
jobs:
|
||||
build-and-test-jvm:
|
||||
@@ -39,7 +45,7 @@ jobs:
|
||||
java-version: 11
|
||||
distribution: "temurin"
|
||||
cache: 'gradle'
|
||||
|
||||
|
||||
- name: Build debug APK and run jvm tests
|
||||
run: ./gradlew assembleDebug lintDebug testDebugUnitTest --stacktrace -DskipFormatKtlint
|
||||
|
||||
@@ -52,9 +58,10 @@ jobs:
|
||||
test-android:
|
||||
# macos has hardware acceleration. See android-emulator-runner action
|
||||
runs-on: macos-latest
|
||||
timeout-minutes: 20
|
||||
strategy:
|
||||
matrix:
|
||||
# api-level 19 is min sdk, but throws errors related to desugaring
|
||||
# api-level 19 is min sdk, but throws errors related to desugaring
|
||||
api-level: [ 21, 29 ]
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
@@ -72,31 +79,38 @@ jobs:
|
||||
api-level: ${{ matrix.api-level }}
|
||||
# workaround to emulator bug: https://github.com/ReactiveCircus/android-emulator-runner/issues/160
|
||||
emulator-build: 7425822
|
||||
script: ./gradlew connectedCheck
|
||||
script: ./gradlew connectedCheck --stacktrace
|
||||
|
||||
- name: Upload test report when tests fail # because the printed out stacktrace (console) is too short, see also #7553
|
||||
uses: actions/upload-artifact@v2
|
||||
if: failure()
|
||||
with:
|
||||
name: android-test-report-api${{ matrix.api-level }}
|
||||
path: app/build/reports/androidTests/connected/**
|
||||
|
||||
# sonar:
|
||||
# runs-on: ubuntu-latest
|
||||
# steps:
|
||||
# - uses: actions/checkout@v2
|
||||
# with:
|
||||
# fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis
|
||||
sonar:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis
|
||||
|
||||
# - name: Set up JDK 11
|
||||
# uses: actions/setup-java@v2
|
||||
# with:
|
||||
# java-version: 11 # Sonar requires JDK 11
|
||||
# distribution: "temurin"
|
||||
# cache: 'gradle'
|
||||
- name: Set up JDK 11
|
||||
uses: actions/setup-java@v2
|
||||
with:
|
||||
java-version: 11 # Sonar requires JDK 11
|
||||
distribution: "temurin"
|
||||
cache: 'gradle'
|
||||
|
||||
# - name: Cache SonarCloud packages
|
||||
# uses: actions/cache@v2
|
||||
# with:
|
||||
# path: ~/.sonar/cache
|
||||
# key: ${{ runner.os }}-sonar
|
||||
# restore-keys: ${{ runner.os }}-sonar
|
||||
- name: Cache SonarCloud packages
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ~/.sonar/cache
|
||||
key: ${{ runner.os }}-sonar
|
||||
restore-keys: ${{ runner.os }}-sonar
|
||||
|
||||
# - name: Build and analyze
|
||||
# env:
|
||||
# GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any
|
||||
# SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
||||
# run: ./gradlew build sonarqube --info
|
||||
- name: Build and analyze
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any
|
||||
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
||||
run: ./gradlew build sonarqube --info
|
||||
|
||||
130
.github/workflows/image-minimizer.js
vendored
Normal file
130
.github/workflows/image-minimizer.js
vendored
Normal file
@@ -0,0 +1,130 @@
|
||||
/*
|
||||
* Script for minimizing big images (jpg,gif,png) when they are uploaded to GitHub and not edited otherwise
|
||||
*/
|
||||
module.exports = async ({github, context}) => {
|
||||
const IGNORE_KEY = '<!-- IGNORE IMAGE MINIFY -->';
|
||||
const IGNORE_ALT_NAME_END = 'ignoreImageMinify';
|
||||
// Targeted maximum height
|
||||
const IMG_MAX_HEIGHT_PX = 600;
|
||||
// maximum width of GitHub issues/comments
|
||||
const IMG_MAX_WIDTH_PX = 800;
|
||||
// all images that have a lower aspect ratio (-> have a smaller width) than this will be minimized
|
||||
const MIN_ASPECT_RATIO = IMG_MAX_WIDTH_PX / IMG_MAX_HEIGHT_PX
|
||||
|
||||
// Get the body of the image
|
||||
let initialBody = null;
|
||||
if (context.eventName == 'issue_comment') {
|
||||
initialBody = context.payload.comment.body;
|
||||
} else if (context.eventName == 'issues') {
|
||||
initialBody = context.payload.issue.body;
|
||||
} else {
|
||||
console.log('Aborting: No body found');
|
||||
return;
|
||||
}
|
||||
console.log(`Found body: \n${initialBody}\n`);
|
||||
|
||||
// Check if we should ignore the currently processing element
|
||||
if (initialBody.includes(IGNORE_KEY)) {
|
||||
console.log('Ignoring: Body contains IGNORE_KEY');
|
||||
return;
|
||||
}
|
||||
|
||||
// Regex for finding images (simple variant) 
|
||||
const REGEX_IMAGE_LOOKUP = /\!\[(.*)\]\((https:\/\/[-a-z0-9]+\.githubusercontent\.com\/\d+\/[-0-9a-f]{32,512}\.(jpg|gif|png))\)/gm;
|
||||
|
||||
// Check if we found something
|
||||
let foundSimpleImages = REGEX_IMAGE_LOOKUP.test(initialBody);
|
||||
if (!foundSimpleImages) {
|
||||
console.log('Found no simple images to process');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Found at least one simple image to process');
|
||||
|
||||
// Require the probe lib for getting the image dimensions
|
||||
const probe = require('probe-image-size');
|
||||
|
||||
var wasMatchModified = false;
|
||||
|
||||
// Try to find and replace the images with minimized ones
|
||||
let newBody = await replaceAsync(initialBody, REGEX_IMAGE_LOOKUP, async (match, g1, g2) => {
|
||||
console.log(`Found match '${match}'`);
|
||||
|
||||
if (g1.endsWith(IGNORE_ALT_NAME_END)) {
|
||||
console.log(`Ignoring match '${match}': IGNORE_ALT_NAME_END`);
|
||||
return match;
|
||||
}
|
||||
|
||||
let shouldModify = false;
|
||||
try {
|
||||
console.log(`Probing ${g2}`);
|
||||
let probeResult = await probe(g2);
|
||||
if (probeResult == null) {
|
||||
throw 'No probeResult';
|
||||
}
|
||||
if (probeResult.hUnits != 'px') {
|
||||
throw `Unexpected probeResult.hUnits (expected px but got ${probeResult.hUnits})`;
|
||||
}
|
||||
if (probeResult.height <= 0) {
|
||||
throw `Unexpected probeResult.height (height is invalid: ${probeResult.height})`;
|
||||
}
|
||||
if (probeResult.wUnits != 'px') {
|
||||
throw `Unexpected probeResult.wUnits (expected px but got ${probeResult.wUnits})`;
|
||||
}
|
||||
if (probeResult.width <= 0) {
|
||||
throw `Unexpected probeResult.width (width is invalid: ${probeResult.width})`;
|
||||
}
|
||||
console.log(`Probing resulted in ${probeResult.width}x${probeResult.height}px`);
|
||||
|
||||
shouldModify = probeResult.height > IMG_MAX_HEIGHT_PX && (probeResult.width / probeResult.height) < MIN_ASPECT_RATIO;
|
||||
} catch(e) {
|
||||
console.log('Probing failed:', e);
|
||||
// Immediately abort
|
||||
return match;
|
||||
}
|
||||
|
||||
if (shouldModify) {
|
||||
wasMatchModified = true;
|
||||
console.log(`Modifying match '${match}'`);
|
||||
return `<img alt="${g1}" src="${g2}" height=${IMG_MAX_HEIGHT_PX} />`;
|
||||
}
|
||||
|
||||
console.log(`Match '${match}' is ok/will not be modified`);
|
||||
return match;
|
||||
});
|
||||
|
||||
if (!wasMatchModified) {
|
||||
console.log('Nothing was modified. Skipping update');
|
||||
return;
|
||||
}
|
||||
|
||||
// Update the corresponding element
|
||||
if (context.eventName == 'issue_comment') {
|
||||
console.log('Updating comment with id', context.payload.comment.id);
|
||||
await github.rest.issues.updateComment({
|
||||
comment_id: context.payload.comment.id,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
body: newBody
|
||||
})
|
||||
} else if (context.eventName == 'issues') {
|
||||
console.log('Updating issue', context.payload.issue.number);
|
||||
await github.rest.issues.update({
|
||||
issue_number: context.payload.issue.number,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
body: newBody
|
||||
});
|
||||
}
|
||||
|
||||
// Asnyc replace function from https://stackoverflow.com/a/48032528
|
||||
async function replaceAsync(str, regex, asyncFn) {
|
||||
const promises = [];
|
||||
str.replace(regex, (match, ...args) => {
|
||||
const promise = asyncFn(match, ...args);
|
||||
promises.push(promise);
|
||||
});
|
||||
const data = await Promise.all(promises);
|
||||
return str.replace(regex, () => data.shift());
|
||||
}
|
||||
}
|
||||
29
.github/workflows/image-minimizer.yml
vendored
Normal file
29
.github/workflows/image-minimizer.yml
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
name: Image Minimizer
|
||||
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created, edited]
|
||||
issues:
|
||||
types: [opened, edited]
|
||||
|
||||
jobs:
|
||||
try-minimize:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 16
|
||||
|
||||
- name: Install probe-image-size
|
||||
run: npm i probe-image-size@7.2.3 --ignore-scripts
|
||||
|
||||
- name: Minimize simple images
|
||||
uses: actions/github-script@v5
|
||||
timeout-minutes: 3
|
||||
with:
|
||||
script: |
|
||||
const script = require('.github/workflows/image-minimizer.js');
|
||||
await script({github, context});
|
||||
16
.gitignore
vendored
16
.gitignore
vendored
@@ -1,15 +1,15 @@
|
||||
.gitignore
|
||||
.gradle
|
||||
/local.properties
|
||||
.gradle/
|
||||
local.properties
|
||||
.DS_Store
|
||||
/build
|
||||
/captures
|
||||
/app/app.iml
|
||||
/.idea
|
||||
/*.iml
|
||||
build/
|
||||
captures/
|
||||
.idea/
|
||||
*.iml
|
||||
*~
|
||||
.weblate
|
||||
*.class
|
||||
app/debug/
|
||||
app/release/
|
||||
|
||||
# vscode / eclipse files
|
||||
*.classpath
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<h2 align="center"><b>NewPipe</b></h2>
|
||||
<h4 align="center">A libre lightweight streaming frontend for Android.</h4>
|
||||
|
||||
<p align="center"><a href="https://f-droid.org/packages/org.schabi.newpipe/"><img src="https://f-droid.org/wiki/images/0/06/F-Droid-button_get-it-on.png"></a></p>
|
||||
<p align="center"><a href="https://f-droid.org/packages/org.schabi.newpipe/"><img src="https://fdroid.gitlab.io/artwork/badge/get-it-on-en.svg" alt="Get it on F-Droid" height=80/></a></p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/TeamNewPipe/NewPipe/releases" alt="GitHub release"><img src="https://img.shields.io/github/release/TeamNewPipe/NewPipe.svg" ></a>
|
||||
@@ -17,7 +17,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 in other languages: [English](README.md), [Español](README.es.md), [한국어](README.ko.md), [Soomaali](README.so.md), [Português Brasil](README.pt_BR.md), [日本語](README.ja.md), [Română](README.ro.md), [Türkçe](README.tr.md).*
|
||||
*Read this in other languages: [English](README.md), [Español](doc/README.es.md), [한국어](doc/README.ko.md), [Soomaali](doc/README.so.md), [Português Brasil](doc/README.pt_BR.md), [Polski](doc/README.pl.md), [日本語](doc/README.ja.md), [Română](doc/README.ro.md), [Türkçe](doc/README.tr.md), [正體中文](doc/README.zh_TW.md).*
|
||||
|
||||
<b>WARNING: THIS IS A BETA VERSION, THEREFORE YOU MAY ENCOUNTER BUGS. IF YOU DO, OPEN AN ISSUE VIA OUR GITHUB REPOSITORY.</b>
|
||||
|
||||
@@ -140,7 +140,7 @@ Therefore, the app does not collect any data without your consent. NewPipe's pri
|
||||
## License
|
||||
[](https://www.gnu.org/licenses/gpl-3.0.en.html)
|
||||
|
||||
NewPipe is Free Software: You can use, study share and improve it at your
|
||||
NewPipe is Free Software: You can use, study, share, and improve it at
|
||||
will. Specifically you can redistribute and/or modify it under the terms of the
|
||||
[GNU General Public License](https://www.gnu.org/licenses/gpl.html) as
|
||||
published by the Free Software Foundation, either version 3 of the License, or
|
||||
|
||||
3
app/.gitignore
vendored
3
app/.gitignore
vendored
@@ -1,3 +0,0 @@
|
||||
.gitignore
|
||||
/build
|
||||
*.iml
|
||||
@@ -1,24 +1,23 @@
|
||||
plugins {
|
||||
id "org.sonarqube" version "3.1.1"
|
||||
id "com.android.application"
|
||||
id "kotlin-android"
|
||||
id "kotlin-kapt"
|
||||
id "kotlin-parcelize"
|
||||
id "checkstyle"
|
||||
id "org.sonarqube" version "3.3"
|
||||
}
|
||||
|
||||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'kotlin-android'
|
||||
apply plugin: 'kotlin-parcelize'
|
||||
apply plugin: 'kotlin-kapt'
|
||||
apply plugin: 'checkstyle'
|
||||
|
||||
android {
|
||||
compileSdkVersion 30
|
||||
compileSdk 31
|
||||
buildToolsVersion '30.0.3'
|
||||
|
||||
defaultConfig {
|
||||
applicationId "org.schabi.newpipe"
|
||||
resValue "string", "app_name", "NewPipe"
|
||||
minSdkVersion 19
|
||||
targetSdkVersion 29
|
||||
versionCode 978
|
||||
versionName "0.21.12"
|
||||
minSdk 19
|
||||
targetSdk 29
|
||||
versionCode 984
|
||||
versionName "0.22.1"
|
||||
|
||||
multiDexEnabled true
|
||||
|
||||
@@ -66,7 +65,7 @@ android {
|
||||
}
|
||||
}
|
||||
|
||||
lintOptions {
|
||||
lint {
|
||||
checkReleaseBuilds false
|
||||
// Or, if you prefer, you can continue to check for errors in release builds,
|
||||
// but continue the build even when errors are found:
|
||||
@@ -80,13 +79,13 @@ android {
|
||||
// Flag to enable support for the new language APIs
|
||||
coreLibraryDesugaringEnabled true
|
||||
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
sourceCompatibility JavaVersion.VERSION_11
|
||||
targetCompatibility JavaVersion.VERSION_11
|
||||
encoding 'utf-8'
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = JavaVersion.VERSION_1_8
|
||||
jvmTarget = JavaVersion.VERSION_11
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
@@ -99,20 +98,21 @@ android {
|
||||
}
|
||||
|
||||
ext {
|
||||
checkstyleVersion = '8.38'
|
||||
checkstyleVersion = '9.2.1'
|
||||
|
||||
androidxLifecycleVersion = '2.3.1'
|
||||
androidxRoomVersion = '2.3.0'
|
||||
|
||||
icepickVersion = '3.2.0'
|
||||
exoPlayerVersion = '2.12.3'
|
||||
googleAutoServiceVersion = '1.0'
|
||||
groupieVersion = '2.9.0'
|
||||
exoPlayerVersion = '2.14.2'
|
||||
googleAutoServiceVersion = '1.0.1'
|
||||
groupieVersion = '2.10.0'
|
||||
markwonVersion = '4.6.2'
|
||||
|
||||
leakCanaryVersion = '2.5'
|
||||
stethoVersion = '1.6.0'
|
||||
mockitoVersion = '3.6.0'
|
||||
mockitoVersion = '4.0.0'
|
||||
assertJVersion = '3.22.0'
|
||||
}
|
||||
|
||||
configurations {
|
||||
@@ -189,11 +189,11 @@ dependencies {
|
||||
// name and the commit hash with the commit hash of the (pushed) commit you want to test
|
||||
// This works thanks to JitPack: https://jitpack.io/
|
||||
implementation 'com.github.TeamNewPipe:nanojson:1d9e1aea9049fc9f85e68b43ba39fe7be1c1f751'
|
||||
implementation 'com.github.TeamNewPipe:NewPipeExtractor:v0.21.11'
|
||||
implementation 'com.github.TeamNewPipe:NewPipeExtractor:v0.21.14'
|
||||
|
||||
/** Checkstyle **/
|
||||
checkstyle "com.puppycrawl.tools:checkstyle:${checkstyleVersion}"
|
||||
ktlint 'com.pinterest:ktlint:0.40.0'
|
||||
ktlint 'com.pinterest:ktlint:0.43.2'
|
||||
|
||||
/** Kotlin **/
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:${kotlin_version}"
|
||||
@@ -201,23 +201,26 @@ dependencies {
|
||||
/** AndroidX **/
|
||||
implementation 'androidx.appcompat:appcompat:1.3.1'
|
||||
implementation 'androidx.cardview:cardview:1.0.0'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.2'
|
||||
implementation 'androidx.core:core-ktx:1.6.0'
|
||||
implementation 'androidx.documentfile:documentfile:1.0.1'
|
||||
implementation 'androidx.fragment:fragment-ktx:1.3.6'
|
||||
implementation "androidx.lifecycle:lifecycle-livedata:${androidxLifecycleVersion}"
|
||||
implementation "androidx.lifecycle:lifecycle-viewmodel:${androidxLifecycleVersion}"
|
||||
implementation 'androidx.localbroadcastmanager:localbroadcastmanager:1.0.0'
|
||||
implementation 'androidx.media:media:1.3.1'
|
||||
implementation 'androidx.media:media:1.4.3'
|
||||
implementation 'androidx.multidex:multidex:2.0.1'
|
||||
implementation 'androidx.preference:preference:1.1.1'
|
||||
implementation 'androidx.recyclerview:recyclerview:1.1.0'
|
||||
implementation 'androidx.recyclerview:recyclerview:1.2.1'
|
||||
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-beta01'
|
||||
implementation 'androidx.webkit:webkit:1.4.0'
|
||||
implementation 'com.google.android.material:material:1.2.1'
|
||||
implementation 'com.google.android.material:material:1.4.0'
|
||||
|
||||
/** Third-party libraries **/
|
||||
// Instance state boilerplate elimination
|
||||
@@ -225,7 +228,7 @@ dependencies {
|
||||
kapt "frankiesardo:icepick-processor:${icepickVersion}"
|
||||
|
||||
// HTML parser
|
||||
implementation "org.jsoup:jsoup:1.13.1"
|
||||
implementation "org.jsoup:jsoup:1.14.3"
|
||||
|
||||
// HTTP client
|
||||
//noinspection GradleDependency --> do not update okhttp to keep supporting Android 4.4 users
|
||||
@@ -257,19 +260,19 @@ dependencies {
|
||||
implementation "com.nononsenseapps:filepicker:4.2.1"
|
||||
|
||||
// Crash reporting
|
||||
implementation "ch.acra:acra-core:5.7.0"
|
||||
implementation "ch.acra:acra-core:5.8.4"
|
||||
|
||||
// Properly restarting
|
||||
implementation 'com.jakewharton:process-phoenix:2.1.2'
|
||||
|
||||
// Reactive extensions for Java VM
|
||||
implementation "io.reactivex.rxjava3:rxjava:3.0.7"
|
||||
implementation "io.reactivex.rxjava3:rxjava:3.0.13"
|
||||
implementation "io.reactivex.rxjava3:rxandroid:3.0.0"
|
||||
// RxJava binding APIs for Android UI widgets
|
||||
implementation "com.jakewharton.rxbinding4:rxbinding:4.0.0"
|
||||
|
||||
// Date and time formatting
|
||||
implementation "org.ocpsoft.prettytime:prettytime:5.0.1.Final"
|
||||
implementation "org.ocpsoft.prettytime:prettytime:5.0.2.Final"
|
||||
|
||||
/** Debugging **/
|
||||
// Memory leak detection
|
||||
@@ -285,11 +288,10 @@ dependencies {
|
||||
testImplementation "org.mockito:mockito-core:${mockitoVersion}"
|
||||
testImplementation "org.mockito:mockito-inline:${mockitoVersion}"
|
||||
|
||||
androidTestImplementation "androidx.test.ext:junit:1.1.2"
|
||||
androidTestImplementation "androidx.test.ext:junit:1.1.3"
|
||||
androidTestImplementation "androidx.test:runner:1.4.0"
|
||||
androidTestImplementation "androidx.room:room-testing:${androidxRoomVersion}"
|
||||
androidTestImplementation "androidx.test.espresso:espresso-core:3.3.0", {
|
||||
exclude module: 'support-annotations'
|
||||
}
|
||||
androidTestImplementation "org.assertj:assertj-core:${assertJVersion}"
|
||||
}
|
||||
|
||||
static String getGitWorkingBranch() {
|
||||
|
||||
@@ -0,0 +1,178 @@
|
||||
package org.schabi.newpipe.local.history
|
||||
|
||||
import androidx.test.core.app.ApplicationProvider
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.junit.After
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.schabi.newpipe.database.AppDatabase
|
||||
import org.schabi.newpipe.database.history.model.SearchHistoryEntry
|
||||
import org.schabi.newpipe.testUtil.TestDatabase
|
||||
import org.schabi.newpipe.testUtil.TrampolineSchedulerRule
|
||||
import java.time.LocalDateTime
|
||||
import java.time.OffsetDateTime
|
||||
import java.time.ZoneOffset
|
||||
|
||||
class HistoryRecordManagerTest {
|
||||
|
||||
private lateinit var manager: HistoryRecordManager
|
||||
private lateinit var database: AppDatabase
|
||||
|
||||
@get:Rule
|
||||
val trampolineScheduler = TrampolineSchedulerRule()
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
database = TestDatabase.createReplacingNewPipeDatabase()
|
||||
manager = HistoryRecordManager(ApplicationProvider.getApplicationContext())
|
||||
}
|
||||
|
||||
@After
|
||||
fun cleanUp() {
|
||||
database.close()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun onSearched() {
|
||||
manager.onSearched(0, "Hello").test().await().assertValue(1)
|
||||
|
||||
// For some reason the Flowable returned by getAll() never completes, so we can't assert
|
||||
// that the number of Lists it returns is exactly 1, we can only check if the first List is
|
||||
// correct. Why on earth has a Flowable been used instead of a Single for getAll()?!?
|
||||
val entities = database.searchHistoryDAO().all.blockingFirst()
|
||||
assertThat(entities).hasSize(1)
|
||||
assertThat(entities[0].id).isEqualTo(1)
|
||||
assertThat(entities[0].serviceId).isEqualTo(0)
|
||||
assertThat(entities[0].search).isEqualTo("Hello")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun deleteSearchHistory() {
|
||||
val entries = listOf(
|
||||
SearchHistoryEntry(time.minusSeconds(1), 0, "A"),
|
||||
SearchHistoryEntry(time.minusSeconds(2), 2, "A"),
|
||||
SearchHistoryEntry(time.minusSeconds(3), 1, "B"),
|
||||
SearchHistoryEntry(time.minusSeconds(4), 0, "B"),
|
||||
)
|
||||
|
||||
// make sure all 4 were inserted
|
||||
database.searchHistoryDAO().insertAll(entries)
|
||||
assertThat(database.searchHistoryDAO().all.blockingFirst()).hasSameSizeAs(entries)
|
||||
|
||||
// try to delete only "A" entries, "B" entries should be untouched
|
||||
manager.deleteSearchHistory("A").test().await().assertValue(2)
|
||||
val entities = database.searchHistoryDAO().all.blockingFirst()
|
||||
assertThat(entities).hasSize(2)
|
||||
assertThat(entities).usingElementComparator { o1, o2 -> if (o1.hasEqualValues(o2)) 0 else 1 }
|
||||
.containsExactly(*entries.subList(2, 4).toTypedArray())
|
||||
|
||||
// assert that nothing happens if we delete a search query that does exist in the db
|
||||
manager.deleteSearchHistory("A").test().await().assertValue(0)
|
||||
val entities2 = database.searchHistoryDAO().all.blockingFirst()
|
||||
assertThat(entities2).hasSize(2)
|
||||
assertThat(entities2).usingElementComparator { o1, o2 -> if (o1.hasEqualValues(o2)) 0 else 1 }
|
||||
.containsExactly(*entries.subList(2, 4).toTypedArray())
|
||||
|
||||
// delete all remaining entries
|
||||
manager.deleteSearchHistory("B").test().await().assertValue(2)
|
||||
assertThat(database.searchHistoryDAO().all.blockingFirst()).isEmpty()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun deleteCompleteSearchHistory() {
|
||||
val entries = listOf(
|
||||
SearchHistoryEntry(time.minusSeconds(1), 1, "A"),
|
||||
SearchHistoryEntry(time.minusSeconds(2), 2, "B"),
|
||||
SearchHistoryEntry(time.minusSeconds(3), 0, "C"),
|
||||
)
|
||||
|
||||
// make sure all 3 were inserted
|
||||
database.searchHistoryDAO().insertAll(entries)
|
||||
assertThat(database.searchHistoryDAO().all.blockingFirst()).hasSameSizeAs(entries)
|
||||
|
||||
// should remove everything
|
||||
manager.deleteCompleteSearchHistory().test().await().assertValue(entries.size)
|
||||
assertThat(database.searchHistoryDAO().all.blockingFirst()).isEmpty()
|
||||
}
|
||||
|
||||
private fun insertShuffledRelatedSearches(relatedSearches: Collection<SearchHistoryEntry>) {
|
||||
|
||||
// shuffle to make sure the order of items returned by queries depends only on
|
||||
// SearchHistoryEntry.creationDate, not on the actual insertion time, so that we can
|
||||
// verify that the `ORDER BY` clause does its job
|
||||
database.searchHistoryDAO().insertAll(relatedSearches.shuffled())
|
||||
|
||||
// make sure all entries were inserted
|
||||
assertEquals(
|
||||
relatedSearches.size,
|
||||
database.searchHistoryDAO().all.blockingFirst().size
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun getRelatedSearches_emptyQuery() {
|
||||
insertShuffledRelatedSearches(RELATED_SEARCHES_ENTRIES)
|
||||
|
||||
// make sure correct number of searches is returned and in correct order
|
||||
val searches = manager.getRelatedSearches("", 6, 4).blockingFirst()
|
||||
assertThat(searches).containsExactly(
|
||||
RELATED_SEARCHES_ENTRIES[6].search, // A (even if in two places)
|
||||
RELATED_SEARCHES_ENTRIES[4].search, // B
|
||||
RELATED_SEARCHES_ENTRIES[5].search, // AA
|
||||
RELATED_SEARCHES_ENTRIES[2].search, // BA
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun getRelatedSearches_emptyQuery_manyDuplicates() {
|
||||
insertShuffledRelatedSearches(
|
||||
listOf(
|
||||
SearchHistoryEntry(time.minusSeconds(9), 3, "A"),
|
||||
SearchHistoryEntry(time.minusSeconds(8), 3, "AB"),
|
||||
SearchHistoryEntry(time.minusSeconds(7), 3, "A"),
|
||||
SearchHistoryEntry(time.minusSeconds(6), 3, "A"),
|
||||
SearchHistoryEntry(time.minusSeconds(5), 3, "BA"),
|
||||
SearchHistoryEntry(time.minusSeconds(4), 3, "A"),
|
||||
SearchHistoryEntry(time.minusSeconds(3), 3, "A"),
|
||||
SearchHistoryEntry(time.minusSeconds(2), 0, "A"),
|
||||
SearchHistoryEntry(time.minusSeconds(1), 2, "AA"),
|
||||
)
|
||||
)
|
||||
|
||||
val searches = manager.getRelatedSearches("", 9, 3).blockingFirst()
|
||||
assertThat(searches).containsExactly("AA", "A", "BA")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun getRelatedSearched_nonEmptyQuery() {
|
||||
insertShuffledRelatedSearches(RELATED_SEARCHES_ENTRIES)
|
||||
|
||||
// make sure correct number of searches is returned and in correct order
|
||||
val searches = manager.getRelatedSearches("A", 3, 5).blockingFirst()
|
||||
assertThat(searches).containsExactly(
|
||||
RELATED_SEARCHES_ENTRIES[6].search, // A (even if in two places)
|
||||
RELATED_SEARCHES_ENTRIES[5].search, // AA
|
||||
RELATED_SEARCHES_ENTRIES[1].search, // BA
|
||||
)
|
||||
|
||||
// also make sure that the string comparison is case insensitive
|
||||
val searches2 = manager.getRelatedSearches("a", 3, 5).blockingFirst()
|
||||
assertThat(searches).isEqualTo(searches2)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val time = OffsetDateTime.of(LocalDateTime.of(2000, 1, 1, 1, 1), ZoneOffset.UTC)
|
||||
|
||||
private val RELATED_SEARCHES_ENTRIES = listOf(
|
||||
SearchHistoryEntry(time.minusSeconds(7), 2, "AC"),
|
||||
SearchHistoryEntry(time.minusSeconds(6), 0, "ABC"),
|
||||
SearchHistoryEntry(time.minusSeconds(5), 1, "BA"),
|
||||
SearchHistoryEntry(time.minusSeconds(4), 3, "A"),
|
||||
SearchHistoryEntry(time.minusSeconds(2), 0, "B"),
|
||||
SearchHistoryEntry(time.minusSeconds(3), 2, "AA"),
|
||||
SearchHistoryEntry(time.minusSeconds(1), 1, "A"),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,14 @@
|
||||
package org.schabi.newpipe.local.playlist
|
||||
|
||||
import androidx.room.Room
|
||||
import androidx.test.core.app.ApplicationProvider
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.rules.Timeout
|
||||
import org.schabi.newpipe.database.AppDatabase
|
||||
import org.schabi.newpipe.database.stream.model.StreamEntity
|
||||
import org.schabi.newpipe.extractor.stream.StreamType
|
||||
import org.schabi.newpipe.testUtil.TestDatabase
|
||||
import org.schabi.newpipe.testUtil.TrampolineSchedulerRule
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class LocalPlaylistManagerTest {
|
||||
|
||||
@@ -21,18 +18,9 @@ class LocalPlaylistManagerTest {
|
||||
@get:Rule
|
||||
val trampolineScheduler = TrampolineSchedulerRule()
|
||||
|
||||
@get:Rule
|
||||
val timeout = Timeout(10, TimeUnit.SECONDS)
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
database = Room.inMemoryDatabaseBuilder(
|
||||
ApplicationProvider.getApplicationContext(),
|
||||
AppDatabase::class.java
|
||||
)
|
||||
.allowMainThreadQueries()
|
||||
.build()
|
||||
|
||||
database = TestDatabase.createReplacingNewPipeDatabase()
|
||||
manager = LocalPlaylistManager(database)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
package org.schabi.newpipe.testUtil
|
||||
|
||||
import androidx.room.Room
|
||||
import androidx.test.core.app.ApplicationProvider
|
||||
import org.junit.Assert.assertSame
|
||||
import org.schabi.newpipe.NewPipeDatabase
|
||||
import org.schabi.newpipe.database.AppDatabase
|
||||
|
||||
class TestDatabase {
|
||||
companion object {
|
||||
fun createReplacingNewPipeDatabase(): AppDatabase {
|
||||
val database = Room.inMemoryDatabaseBuilder(
|
||||
ApplicationProvider.getApplicationContext(),
|
||||
AppDatabase::class.java
|
||||
)
|
||||
.allowMainThreadQueries()
|
||||
.build()
|
||||
|
||||
val databaseField = NewPipeDatabase::class.java.getDeclaredField("databaseInstance")
|
||||
databaseField.isAccessible = true
|
||||
databaseField.set(NewPipeDatabase::class, database)
|
||||
|
||||
assertSame(
|
||||
"Mocking database failed!",
|
||||
database,
|
||||
NewPipeDatabase.getInstance(ApplicationProvider.getApplicationContext())
|
||||
)
|
||||
|
||||
return database
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package org.schabi.newpipe.settings;
|
||||
|
||||
import android.content.Intent;
|
||||
|
||||
import leakcanary.LeakCanary;
|
||||
|
||||
/**
|
||||
* Build variant dependent (BVD) leak canary API implementation for the debug settings fragment.
|
||||
* This class is loaded via reflection by
|
||||
* {@link DebugSettingsFragment.DebugSettingsBVDLeakCanaryAPI}.
|
||||
*/
|
||||
@SuppressWarnings("unused") // Class is used but loaded via reflection
|
||||
public class DebugSettingsBVDLeakCanary
|
||||
implements DebugSettingsFragment.DebugSettingsBVDLeakCanaryAPI {
|
||||
|
||||
@Override
|
||||
public Intent getNewLeakDisplayActivityIntent() {
|
||||
return LeakCanary.INSTANCE.newLeakDisplayActivityIntent();
|
||||
}
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
package org.schabi.newpipe.settings;
|
||||
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.preference.Preference;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.util.PicassoHelper;
|
||||
|
||||
import leakcanary.LeakCanary;
|
||||
|
||||
public class DebugSettingsFragment extends BasePreferenceFragment {
|
||||
@Override
|
||||
public void onCreatePreferences(final Bundle savedInstanceState, final String rootKey) {
|
||||
addPreferencesFromResource(R.xml.debug_settings);
|
||||
|
||||
final Preference showMemoryLeaksPreference
|
||||
= findPreference(getString(R.string.show_memory_leaks_key));
|
||||
final Preference showImageIndicatorsPreference
|
||||
= findPreference(getString(R.string.show_image_indicators_key));
|
||||
final Preference crashTheAppPreference
|
||||
= findPreference(getString(R.string.crash_the_app_key));
|
||||
|
||||
assert showMemoryLeaksPreference != null;
|
||||
assert showImageIndicatorsPreference != null;
|
||||
assert crashTheAppPreference != null;
|
||||
|
||||
showMemoryLeaksPreference.setOnPreferenceClickListener(preference -> {
|
||||
startActivity(LeakCanary.INSTANCE.newLeakDisplayActivityIntent());
|
||||
return true;
|
||||
});
|
||||
|
||||
showImageIndicatorsPreference.setOnPreferenceChangeListener((preference, newValue) -> {
|
||||
PicassoHelper.setIndicatorsEnabled((Boolean) newValue);
|
||||
return true;
|
||||
});
|
||||
|
||||
crashTheAppPreference.setOnPreferenceClickListener(preference -> {
|
||||
throw new RuntimeException();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -256,6 +256,21 @@
|
||||
<data android:pathPrefix="/" />
|
||||
</intent-filter>
|
||||
|
||||
<!-- y2u.be filter -->
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<action android:name="android.media.action.MEDIA_PLAY_FROM_SEARCH" />
|
||||
<action android:name="android.nfc.action.NDEF_DISCOVERED" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data android:scheme="http" />
|
||||
<data android:scheme="https" />
|
||||
<data android:host="y2u.be" />
|
||||
<data android:pathPrefix="/" />
|
||||
</intent-filter>
|
||||
|
||||
<!-- Soundcloud filter -->
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
@@ -323,10 +338,15 @@
|
||||
<data android:host="video.ploud.fr" />
|
||||
<data android:host="video.lqdn.fr" />
|
||||
<data android:host="skeptikon.fr" />
|
||||
<data android:host="media.fsfe.org" />
|
||||
|
||||
<data android:pathPrefix="/videos/" /> <!-- it contains playlists -->
|
||||
<data android:pathPrefix="/w/" /> <!-- short video URLs -->
|
||||
<data android:pathPrefix="/w/p/" /> <!-- short playlist URLs -->
|
||||
<data android:pathPrefix="/accounts/" />
|
||||
<data android:pathPrefix="/a/" /> <!-- short account URLs -->
|
||||
<data android:pathPrefix="/video-channels/" />
|
||||
<data android:pathPrefix="/c/" /> <!-- short video-channels URLs -->
|
||||
</intent-filter>
|
||||
|
||||
<!-- Bandcamp filter for tracks, albums and playlists -->
|
||||
|
||||
@@ -51,8 +51,12 @@ import java.util.ArrayList;
|
||||
* <li>{@link #saveState()}</li>
|
||||
* <li>{@link #restoreState(Parcelable, ClassLoader)}</li>
|
||||
* </ul>
|
||||
*
|
||||
* @deprecated Switch to {@link androidx.viewpager2.widget.ViewPager2} and use
|
||||
* {@link androidx.viewpager2.adapter.FragmentStateAdapter} instead.
|
||||
*/
|
||||
@SuppressWarnings("deprecation")
|
||||
@Deprecated
|
||||
public abstract class FragmentStatePagerAdapterMenuWorkaround extends PagerAdapter {
|
||||
private static final String TAG = "FragmentStatePagerAdapt";
|
||||
private static final boolean DEBUG = false;
|
||||
@@ -86,9 +90,10 @@ public abstract class FragmentStatePagerAdapterMenuWorkaround extends PagerAdapt
|
||||
private final int mBehavior;
|
||||
private FragmentTransaction mCurTransaction = null;
|
||||
|
||||
private final ArrayList<Fragment.SavedState> mSavedState = new ArrayList<Fragment.SavedState>();
|
||||
private final ArrayList<Fragment> mFragments = new ArrayList<Fragment>();
|
||||
private final ArrayList<Fragment.SavedState> mSavedState = new ArrayList<>();
|
||||
private final ArrayList<Fragment> mFragments = new ArrayList<>();
|
||||
private Fragment mCurrentPrimaryItem = null;
|
||||
private boolean mExecutingFinishUpdate;
|
||||
|
||||
/**
|
||||
* Constructor for {@link FragmentStatePagerAdapterMenuWorkaround}
|
||||
@@ -208,7 +213,7 @@ public abstract class FragmentStatePagerAdapterMenuWorkaround extends PagerAdapt
|
||||
mFragments.set(position, null);
|
||||
|
||||
mCurTransaction.remove(fragment);
|
||||
if (fragment == mCurrentPrimaryItem) {
|
||||
if (fragment.equals(mCurrentPrimaryItem)) {
|
||||
mCurrentPrimaryItem = null;
|
||||
}
|
||||
}
|
||||
@@ -247,7 +252,19 @@ public abstract class FragmentStatePagerAdapterMenuWorkaround extends PagerAdapt
|
||||
@Override
|
||||
public void finishUpdate(@NonNull final ViewGroup container) {
|
||||
if (mCurTransaction != null) {
|
||||
mCurTransaction.commitNowAllowingStateLoss();
|
||||
// We drop any transactions that attempt to be committed
|
||||
// from a re-entrant call to finishUpdate(). We need to
|
||||
// do this as a workaround for Robolectric running measure/layout
|
||||
// calls inline rather than allowing them to be posted
|
||||
// as they would on a real device.
|
||||
if (!mExecutingFinishUpdate) {
|
||||
try {
|
||||
mExecutingFinishUpdate = true;
|
||||
mCurTransaction.commitNowAllowingStateLoss();
|
||||
} finally {
|
||||
mExecutingFinishUpdate = false;
|
||||
}
|
||||
}
|
||||
mCurTransaction = null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,6 +63,7 @@ public final class FlingBehavior extends AppBarLayout.Behavior {
|
||||
return consumed == dy;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onInterceptTouchEvent(@NonNull final CoordinatorLayout parent,
|
||||
@NonNull final AppBarLayout child,
|
||||
@NonNull final MotionEvent ev) {
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.apache.commons.text.similarity;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
* A matching algorithm that is similar to the searching algorithms implemented in editors such
|
||||
* as Sublime Text, TextMate, Atom and others.
|
||||
*
|
||||
* <p>
|
||||
* One point is given for every matched character. Subsequent matches yield two bonus points.
|
||||
* A higher score indicates a higher similarity.
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
* This code has been adapted from Apache Commons Lang 3.3.
|
||||
* </p>
|
||||
*
|
||||
* @since 1.0
|
||||
*
|
||||
* Note: This class was forked from
|
||||
* <a href="https://git.io/JyYJg">
|
||||
* apache/commons-text (8cfdafc) FuzzyScore.java
|
||||
* </a>
|
||||
*/
|
||||
public class FuzzyScore {
|
||||
|
||||
/**
|
||||
* Locale used to change the case of text.
|
||||
*/
|
||||
private final Locale locale;
|
||||
|
||||
|
||||
/**
|
||||
* This returns a {@link Locale}-specific {@link FuzzyScore}.
|
||||
*
|
||||
* @param locale The string matching logic is case insensitive.
|
||||
A {@link Locale} is necessary to normalize both Strings to lower case.
|
||||
* @throws IllegalArgumentException
|
||||
* This is thrown if the {@link Locale} parameter is {@code null}.
|
||||
*/
|
||||
public FuzzyScore(final Locale locale) {
|
||||
if (locale == null) {
|
||||
throw new IllegalArgumentException("Locale must not be null");
|
||||
}
|
||||
this.locale = locale;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the Fuzzy Score which indicates the similarity score between two
|
||||
* Strings.
|
||||
*
|
||||
* <pre>
|
||||
* score.fuzzyScore(null, null) = IllegalArgumentException
|
||||
* score.fuzzyScore("not null", null) = IllegalArgumentException
|
||||
* score.fuzzyScore(null, "not null") = IllegalArgumentException
|
||||
* score.fuzzyScore("", "") = 0
|
||||
* score.fuzzyScore("Workshop", "b") = 0
|
||||
* score.fuzzyScore("Room", "o") = 1
|
||||
* score.fuzzyScore("Workshop", "w") = 1
|
||||
* score.fuzzyScore("Workshop", "ws") = 2
|
||||
* score.fuzzyScore("Workshop", "wo") = 4
|
||||
* score.fuzzyScore("Apache Software Foundation", "asf") = 3
|
||||
* </pre>
|
||||
*
|
||||
* @param term a full term that should be matched against, must not be null
|
||||
* @param query the query that will be matched against a term, must not be
|
||||
* null
|
||||
* @return result score
|
||||
* @throws IllegalArgumentException if the term or query is {@code null}
|
||||
*/
|
||||
public Integer fuzzyScore(final CharSequence term, final CharSequence query) {
|
||||
if (term == null || query == null) {
|
||||
throw new IllegalArgumentException("CharSequences must not be null");
|
||||
}
|
||||
|
||||
// fuzzy logic is case insensitive. We normalize the Strings to lower
|
||||
// case right from the start. Turning characters to lower case
|
||||
// via Character.toLowerCase(char) is unfortunately insufficient
|
||||
// as it does not accept a locale.
|
||||
final String termLowerCase = term.toString().toLowerCase(locale);
|
||||
final String queryLowerCase = query.toString().toLowerCase(locale);
|
||||
|
||||
// the resulting score
|
||||
int score = 0;
|
||||
|
||||
// the position in the term which will be scanned next for potential
|
||||
// query character matches
|
||||
int termIndex = 0;
|
||||
|
||||
// index of the previously matched character in the term
|
||||
int previousMatchingCharacterIndex = Integer.MIN_VALUE;
|
||||
|
||||
for (int queryIndex = 0; queryIndex < queryLowerCase.length(); queryIndex++) {
|
||||
final char queryChar = queryLowerCase.charAt(queryIndex);
|
||||
|
||||
boolean termCharacterMatchFound = false;
|
||||
for (; termIndex < termLowerCase.length()
|
||||
&& !termCharacterMatchFound; termIndex++) {
|
||||
final char termChar = termLowerCase.charAt(termIndex);
|
||||
|
||||
if (queryChar == termChar) {
|
||||
// simple character matches result in one point
|
||||
score++;
|
||||
|
||||
// subsequent character matches further improve
|
||||
// the score.
|
||||
if (previousMatchingCharacterIndex + 1 == termIndex) {
|
||||
score += 2;
|
||||
}
|
||||
|
||||
previousMatchingCharacterIndex = termIndex;
|
||||
|
||||
// we can leave the nested loop. Every character in the
|
||||
// query can match at most one character in the term.
|
||||
termCharacterMatchFound = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return score;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the locale.
|
||||
*
|
||||
* @return The locale
|
||||
*/
|
||||
public Locale getLocale() {
|
||||
return locale;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -13,13 +13,8 @@ import androidx.preference.PreferenceManager;
|
||||
import com.jakewharton.processphoenix.ProcessPhoenix;
|
||||
|
||||
import org.acra.ACRA;
|
||||
import org.acra.config.ACRAConfigurationException;
|
||||
import org.acra.config.CoreConfiguration;
|
||||
import org.acra.config.CoreConfigurationBuilder;
|
||||
import org.schabi.newpipe.error.ErrorActivity;
|
||||
import org.schabi.newpipe.error.ErrorInfo;
|
||||
import org.schabi.newpipe.error.ReCaptchaActivity;
|
||||
import org.schabi.newpipe.error.UserAction;
|
||||
import org.schabi.newpipe.extractor.NewPipe;
|
||||
import org.schabi.newpipe.extractor.downloader.Downloader;
|
||||
import org.schabi.newpipe.ktx.ExceptionUtils;
|
||||
@@ -210,16 +205,9 @@ public class App extends MultiDexApplication {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
final CoreConfiguration acraConfig = new CoreConfigurationBuilder(this)
|
||||
.setBuildConfigClass(BuildConfig.class)
|
||||
.build();
|
||||
ACRA.init(this, acraConfig);
|
||||
} catch (final ACRAConfigurationException exception) {
|
||||
exception.printStackTrace();
|
||||
ErrorActivity.reportError(this, new ErrorInfo(exception,
|
||||
UserAction.SOMETHING_ELSE, "Could not initialize ACRA crash report"));
|
||||
}
|
||||
final CoreConfigurationBuilder acraConfig = new CoreConfigurationBuilder(this)
|
||||
.withBuildConfigClass(BuildConfig.class);
|
||||
ACRA.init(this, acraConfig);
|
||||
}
|
||||
|
||||
private void initNotificationChannels() {
|
||||
@@ -227,31 +215,39 @@ public class App extends MultiDexApplication {
|
||||
// the main and update channels
|
||||
final NotificationChannelCompat mainChannel = new NotificationChannelCompat
|
||||
.Builder(getString(R.string.notification_channel_id),
|
||||
NotificationManagerCompat.IMPORTANCE_LOW)
|
||||
NotificationManagerCompat.IMPORTANCE_LOW)
|
||||
.setName(getString(R.string.notification_channel_name))
|
||||
.setDescription(getString(R.string.notification_channel_description))
|
||||
.build();
|
||||
|
||||
final NotificationChannelCompat appUpdateChannel = new NotificationChannelCompat
|
||||
.Builder(getString(R.string.app_update_notification_channel_id),
|
||||
NotificationManagerCompat.IMPORTANCE_LOW)
|
||||
NotificationManagerCompat.IMPORTANCE_LOW)
|
||||
.setName(getString(R.string.app_update_notification_channel_name))
|
||||
.setDescription(getString(R.string.app_update_notification_channel_description))
|
||||
.build();
|
||||
|
||||
final NotificationChannelCompat hashChannel = new NotificationChannelCompat
|
||||
.Builder(getString(R.string.hash_channel_id),
|
||||
NotificationManagerCompat.IMPORTANCE_HIGH)
|
||||
NotificationManagerCompat.IMPORTANCE_HIGH)
|
||||
.setName(getString(R.string.hash_channel_name))
|
||||
.setDescription(getString(R.string.hash_channel_description))
|
||||
.build();
|
||||
|
||||
final NotificationChannelCompat errorReportChannel = 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();
|
||||
|
||||
final NotificationManagerCompat notificationManager = NotificationManagerCompat.from(this);
|
||||
notificationManager.createNotificationChannelsCompat(Arrays.asList(mainChannel,
|
||||
appUpdateChannel, hashChannel));
|
||||
appUpdateChannel, hashChannel, errorReportChannel));
|
||||
}
|
||||
|
||||
protected boolean isDisposedRxExceptionsReported() {
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -21,7 +21,6 @@ public abstract class BaseFragment extends Fragment {
|
||||
//These values are used for controlling fragments when they are part of the frontpage
|
||||
@State
|
||||
protected boolean useAsFrontPage = false;
|
||||
private boolean mIsVisibleToUser = false;
|
||||
|
||||
public void useAsFrontPage(final boolean value) {
|
||||
useAsFrontPage = value;
|
||||
@@ -85,12 +84,6 @@ public abstract class BaseFragment extends Fragment {
|
||||
AppWatcher.INSTANCE.getObjectWatcher().watch(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setUserVisibleHint(final boolean isVisibleToUser) {
|
||||
super.setUserVisibleHint(isVisibleToUser);
|
||||
mIsVisibleToUser = isVisibleToUser;
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Init
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
@@ -109,8 +102,7 @@ public abstract class BaseFragment extends Fragment {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "setTitle() called with: title = [" + title + "]");
|
||||
}
|
||||
if ((!useAsFrontPage || mIsVisibleToUser)
|
||||
&& (activity != null && activity.getSupportActionBar() != null)) {
|
||||
if (!useAsFrontPage && activity != null && activity.getSupportActionBar() != null) {
|
||||
activity.getSupportActionBar().setDisplayShowTitleEnabled(true);
|
||||
activity.getSupportActionBar().setTitle(title);
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.content.pm.Signature;
|
||||
import android.net.ConnectivityManager;
|
||||
import android.net.Uri;
|
||||
import android.util.Log;
|
||||
|
||||
@@ -15,7 +14,6 @@ import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.app.NotificationCompat;
|
||||
import androidx.core.app.NotificationManagerCompat;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.core.content.pm.PackageInfoCompat;
|
||||
import androidx.preference.PreferenceManager;
|
||||
|
||||
@@ -23,8 +21,8 @@ import com.grack.nanojson.JsonObject;
|
||||
import com.grack.nanojson.JsonParser;
|
||||
import com.grack.nanojson.JsonParserException;
|
||||
|
||||
import org.schabi.newpipe.error.ErrorActivity;
|
||||
import org.schabi.newpipe.error.ErrorInfo;
|
||||
import org.schabi.newpipe.error.ErrorUtil;
|
||||
import org.schabi.newpipe.error.UserAction;
|
||||
import org.schabi.newpipe.extractor.downloader.Response;
|
||||
import org.schabi.newpipe.extractor.exceptions.ReCaptchaException;
|
||||
@@ -48,7 +46,8 @@ public final class CheckForNewAppVersion extends IntentService {
|
||||
private static final boolean DEBUG = MainActivity.DEBUG;
|
||||
private static final String TAG = CheckForNewAppVersion.class.getSimpleName();
|
||||
|
||||
private static final String GITHUB_APK_SHA1
|
||||
// Public key of the certificate that is used in NewPipe release versions
|
||||
private static final String RELEASE_CERT_PUBLIC_KEY_SHA1
|
||||
= "B0:2E:90:7C:1C:D6:FC:57:C3:35:F0:88:D0:8F:50:5F:94:E4:D2:15";
|
||||
private static final String NEWPIPE_API_URL = "https://newpipe.net/api/data.json";
|
||||
|
||||
@@ -65,7 +64,7 @@ public final class CheckForNewAppVersion extends IntentService {
|
||||
signatures = PackageInfoCompat.getSignatures(application.getPackageManager(),
|
||||
application.getPackageName());
|
||||
} catch (final PackageManager.NameNotFoundException e) {
|
||||
ErrorActivity.reportError(application, new ErrorInfo(e,
|
||||
ErrorUtil.createNotification(application, new ErrorInfo(e,
|
||||
UserAction.CHECK_FOR_NEW_APP_VERSION, "Could not find package info"));
|
||||
return "";
|
||||
}
|
||||
@@ -80,7 +79,7 @@ public final class CheckForNewAppVersion extends IntentService {
|
||||
final CertificateFactory cf = CertificateFactory.getInstance("X509");
|
||||
c = (X509Certificate) cf.generateCertificate(input);
|
||||
} catch (final CertificateException e) {
|
||||
ErrorActivity.reportError(application, new ErrorInfo(e,
|
||||
ErrorUtil.createNotification(application, new ErrorInfo(e,
|
||||
UserAction.CHECK_FOR_NEW_APP_VERSION, "Certificate error"));
|
||||
return "";
|
||||
}
|
||||
@@ -90,7 +89,7 @@ public final class CheckForNewAppVersion extends IntentService {
|
||||
final byte[] publicKey = md.digest(c.getEncoded());
|
||||
return byte2HexFormatted(publicKey);
|
||||
} catch (NoSuchAlgorithmException | CertificateEncodingException e) {
|
||||
ErrorActivity.reportError(application, new ErrorInfo(e,
|
||||
ErrorUtil.createNotification(application, new ErrorInfo(e,
|
||||
UserAction.CHECK_FOR_NEW_APP_VERSION, "Could not retrieve SHA1 key"));
|
||||
return "";
|
||||
}
|
||||
@@ -129,44 +128,37 @@ public final class CheckForNewAppVersion extends IntentService {
|
||||
final String versionName,
|
||||
final String apkLocationUrl,
|
||||
final int versionCode) {
|
||||
final int notificationId = 2000;
|
||||
|
||||
if (BuildConfig.VERSION_CODE < versionCode) {
|
||||
// A pending intent to open the apk location url in the browser.
|
||||
final Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(apkLocationUrl));
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
final PendingIntent pendingIntent
|
||||
= PendingIntent.getActivity(application, 0, intent, 0);
|
||||
|
||||
final String channelId = application
|
||||
.getString(R.string.app_update_notification_channel_id);
|
||||
final NotificationCompat.Builder notificationBuilder
|
||||
= new NotificationCompat.Builder(application, channelId)
|
||||
.setSmallIcon(R.drawable.ic_newpipe_update)
|
||||
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
|
||||
.setContentIntent(pendingIntent)
|
||||
.setAutoCancel(true)
|
||||
.setContentTitle(application
|
||||
.getString(R.string.app_update_notification_content_title))
|
||||
.setContentText(application
|
||||
.getString(R.string.app_update_notification_content_text)
|
||||
+ " " + versionName);
|
||||
|
||||
final NotificationManagerCompat notificationManager
|
||||
= NotificationManagerCompat.from(application);
|
||||
notificationManager.notify(notificationId, notificationBuilder.build());
|
||||
if (BuildConfig.VERSION_CODE >= versionCode) {
|
||||
return;
|
||||
}
|
||||
|
||||
// A pending intent to open the apk location url in the browser.
|
||||
final Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(apkLocationUrl));
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
final PendingIntent pendingIntent
|
||||
= PendingIntent.getActivity(application, 0, intent, 0);
|
||||
|
||||
final String channelId = application
|
||||
.getString(R.string.app_update_notification_channel_id);
|
||||
final NotificationCompat.Builder notificationBuilder
|
||||
= new NotificationCompat.Builder(application, channelId)
|
||||
.setSmallIcon(R.drawable.ic_newpipe_update)
|
||||
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
|
||||
.setContentIntent(pendingIntent)
|
||||
.setAutoCancel(true)
|
||||
.setContentTitle(application
|
||||
.getString(R.string.app_update_notification_content_title))
|
||||
.setContentText(application
|
||||
.getString(R.string.app_update_notification_content_text)
|
||||
+ " " + versionName);
|
||||
|
||||
final NotificationManagerCompat notificationManager
|
||||
= NotificationManagerCompat.from(application);
|
||||
notificationManager.notify(2000, notificationBuilder.build());
|
||||
}
|
||||
|
||||
private static boolean isConnected(@NonNull final App app) {
|
||||
final ConnectivityManager connectivityManager =
|
||||
ContextCompat.getSystemService(app, ConnectivityManager.class);
|
||||
return connectivityManager != null && connectivityManager.getActiveNetworkInfo() != null
|
||||
&& connectivityManager.getActiveNetworkInfo().isConnected();
|
||||
}
|
||||
|
||||
public static boolean isGithubApk(@NonNull final App app) {
|
||||
return getCertificateSHA1Fingerprint(app).equals(GITHUB_APK_SHA1);
|
||||
public static boolean isReleaseApk(@NonNull final App app) {
|
||||
return getCertificateSHA1Fingerprint(app).equals(RELEASE_CERT_PUBLIC_KEY_SHA1);
|
||||
}
|
||||
|
||||
private void checkNewVersion() throws IOException, ReCaptchaException {
|
||||
@@ -175,9 +167,8 @@ public final class CheckForNewAppVersion extends IntentService {
|
||||
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(app);
|
||||
final NewVersionManager manager = new NewVersionManager();
|
||||
|
||||
// Check if user has enabled/disabled update checking
|
||||
// and if the current apk is a github one or not.
|
||||
if (!prefs.getBoolean(app.getString(R.string.update_app_key), true) || !isGithubApk(app)) {
|
||||
// Check if the current apk is a github one or not.
|
||||
if (!isReleaseApk(app)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -213,6 +204,7 @@ public final class CheckForNewAppVersion extends IntentService {
|
||||
|
||||
// Parse the json from the response.
|
||||
try {
|
||||
|
||||
final JsonObject githubStableObject = JsonParser.object()
|
||||
.from(response.responseBody()).getObject("flavors")
|
||||
.getObject("github").getObject("stable");
|
||||
@@ -235,6 +227,23 @@ public final class CheckForNewAppVersion extends IntentService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Start a new service which
|
||||
* checks if all conditions for performing a version check are met,
|
||||
* fetches the API endpoint {@link #NEWPIPE_API_URL} containing info
|
||||
* about the latest NewPipe version
|
||||
* and displays a notification about ana available update.
|
||||
* <br>
|
||||
* Following conditions need to be met, before data is request from the server:
|
||||
* <ul>
|
||||
* <li> The app is signed with the correct signing key (by TeamNewPipe / schabi).
|
||||
* If the signing key differs from the one used upstream, the update cannot be installed.</li>
|
||||
* <li>The user enabled searching for and notifying about updates in the settings.</li>
|
||||
* <li>The app did not recently check for updates.
|
||||
* We do not want to make unnecessary connections and DOS our servers.</li>
|
||||
* </ul>
|
||||
* <b>Must not be executed</b> when the app is in background.
|
||||
*/
|
||||
public static void startNewVersionCheckService() {
|
||||
final Intent intent = new Intent(App.getApp().getApplicationContext(),
|
||||
CheckForNewAppVersion.class);
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
package org.schabi.newpipe;
|
||||
|
||||
import static org.schabi.newpipe.CheckForNewAppVersion.startNewVersionCheckService;
|
||||
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
|
||||
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
@@ -62,7 +63,7 @@ import org.schabi.newpipe.databinding.DrawerHeaderBinding;
|
||||
import org.schabi.newpipe.databinding.DrawerLayoutBinding;
|
||||
import org.schabi.newpipe.databinding.InstanceSpinnerLayoutBinding;
|
||||
import org.schabi.newpipe.databinding.ToolbarLayoutBinding;
|
||||
import org.schabi.newpipe.error.ErrorActivity;
|
||||
import org.schabi.newpipe.error.ErrorUtil;
|
||||
import org.schabi.newpipe.extractor.NewPipe;
|
||||
import org.schabi.newpipe.extractor.StreamingService;
|
||||
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||
@@ -93,8 +94,6 @@ import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
|
||||
|
||||
public class MainActivity extends AppCompatActivity {
|
||||
private static final String TAG = "MainActivity";
|
||||
@SuppressWarnings("ConstantConditions")
|
||||
@@ -158,19 +157,66 @@ public class MainActivity extends AppCompatActivity {
|
||||
try {
|
||||
setupDrawer();
|
||||
} catch (final Exception e) {
|
||||
ErrorActivity.reportUiErrorInSnackbar(this, "Setting up drawer", e);
|
||||
ErrorUtil.showUiErrorSnackbar(this, "Setting up drawer", e);
|
||||
}
|
||||
|
||||
if (DeviceUtils.isTv(this)) {
|
||||
FocusOverlayView.setupFocusObserver(this);
|
||||
}
|
||||
openMiniPlayerUponPlayerStarted();
|
||||
|
||||
// Check for new version
|
||||
startNewVersionCheckService();
|
||||
}
|
||||
|
||||
private void setupDrawer() throws Exception {
|
||||
@Override
|
||||
protected void onPostCreate(final Bundle savedInstanceState) {
|
||||
super.onPostCreate(savedInstanceState);
|
||||
|
||||
final App app = App.getApp();
|
||||
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(app);
|
||||
|
||||
if (prefs.getBoolean(app.getString(R.string.update_app_key), true)) {
|
||||
// Start the service which is checking all conditions
|
||||
// and eventually searching for a new version.
|
||||
// The service searching for a new NewPipe version must not be started in background.
|
||||
startNewVersionCheckService();
|
||||
}
|
||||
}
|
||||
|
||||
private void setupDrawer() throws ExtractionException {
|
||||
addDrawerMenuForCurrentService();
|
||||
|
||||
toggle = new ActionBarDrawerToggle(this, mainBinding.getRoot(),
|
||||
toolbarLayoutBinding.toolbar, R.string.drawer_open, R.string.drawer_close);
|
||||
toggle.syncState();
|
||||
mainBinding.getRoot().addDrawerListener(toggle);
|
||||
mainBinding.getRoot().addDrawerListener(new DrawerLayout.SimpleDrawerListener() {
|
||||
private int lastService;
|
||||
|
||||
@Override
|
||||
public void onDrawerOpened(final View drawerView) {
|
||||
lastService = ServiceHelper.getSelectedServiceId(MainActivity.this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDrawerClosed(final View drawerView) {
|
||||
if (servicesShown) {
|
||||
toggleServices();
|
||||
}
|
||||
if (lastService != ServiceHelper.getSelectedServiceId(MainActivity.this)) {
|
||||
ActivityCompat.recreate(MainActivity.this);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
drawerLayoutBinding.navigation.setNavigationItemSelectedListener(this::drawerItemSelected);
|
||||
setupDrawerHeader();
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the drawer menu for the current service.
|
||||
*
|
||||
* @throws ExtractionException if the service didn't provide available kiosks
|
||||
*/
|
||||
private void addDrawerMenuForCurrentService() throws ExtractionException {
|
||||
//Tabs
|
||||
final int currentServiceId = ServiceHelper.getSelectedServiceId(this);
|
||||
final StreamingService service = NewPipe.getService(currentServiceId);
|
||||
@@ -209,32 +255,6 @@ public class MainActivity extends AppCompatActivity {
|
||||
drawerLayoutBinding.navigation.getMenu()
|
||||
.add(R.id.menu_options_about_group, ITEM_ID_ABOUT, ORDER, R.string.tab_about)
|
||||
.setIcon(R.drawable.ic_info_outline);
|
||||
|
||||
toggle = new ActionBarDrawerToggle(this, mainBinding.getRoot(),
|
||||
toolbarLayoutBinding.toolbar, R.string.drawer_open, R.string.drawer_close);
|
||||
toggle.syncState();
|
||||
mainBinding.getRoot().addDrawerListener(toggle);
|
||||
mainBinding.getRoot().addDrawerListener(new DrawerLayout.SimpleDrawerListener() {
|
||||
private int lastService;
|
||||
|
||||
@Override
|
||||
public void onDrawerOpened(final View drawerView) {
|
||||
lastService = ServiceHelper.getSelectedServiceId(MainActivity.this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDrawerClosed(final View drawerView) {
|
||||
if (servicesShown) {
|
||||
toggleServices();
|
||||
}
|
||||
if (lastService != ServiceHelper.getSelectedServiceId(MainActivity.this)) {
|
||||
ActivityCompat.recreate(MainActivity.this);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
drawerLayoutBinding.navigation.setNavigationItemSelectedListener(this::drawerItemSelected);
|
||||
setupDrawerHeader();
|
||||
}
|
||||
|
||||
private boolean drawerItemSelected(final MenuItem item) {
|
||||
@@ -246,7 +266,7 @@ public class MainActivity extends AppCompatActivity {
|
||||
try {
|
||||
tabSelected(item);
|
||||
} catch (final Exception e) {
|
||||
ErrorActivity.reportUiErrorInSnackbar(this, "Selecting main page tab", e);
|
||||
ErrorUtil.showUiErrorSnackbar(this, "Selecting main page tab", e);
|
||||
}
|
||||
break;
|
||||
case R.id.menu_options_about_group:
|
||||
@@ -342,20 +362,22 @@ public class MainActivity extends AppCompatActivity {
|
||||
drawerLayoutBinding.navigation.getMenu().removeGroup(R.id.menu_tabs_group);
|
||||
drawerLayoutBinding.navigation.getMenu().removeGroup(R.id.menu_options_about_group);
|
||||
|
||||
// Show up or down arrow
|
||||
drawerHeaderBinding.drawerArrow.setImageResource(
|
||||
servicesShown ? R.drawable.ic_arrow_drop_up : R.drawable.ic_arrow_drop_down);
|
||||
|
||||
if (servicesShown) {
|
||||
showServices();
|
||||
} else {
|
||||
try {
|
||||
showTabs();
|
||||
addDrawerMenuForCurrentService();
|
||||
} catch (final Exception e) {
|
||||
ErrorActivity.reportUiErrorInSnackbar(this, "Showing main page tabs", e);
|
||||
ErrorUtil.showUiErrorSnackbar(this, "Showing main page tabs", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void showServices() {
|
||||
drawerHeaderBinding.drawerArrow.setImageResource(R.drawable.ic_arrow_drop_up);
|
||||
|
||||
for (final StreamingService s : NewPipe.getServices()) {
|
||||
final String title = s.getServiceInfo().getName()
|
||||
+ (ServiceHelper.isBeta(s) ? " (beta)" : "");
|
||||
@@ -419,48 +441,6 @@ public class MainActivity extends AppCompatActivity {
|
||||
menuItem.setActionView(spinner);
|
||||
}
|
||||
|
||||
private void showTabs() throws ExtractionException {
|
||||
drawerHeaderBinding.drawerArrow.setImageResource(R.drawable.ic_arrow_drop_down);
|
||||
|
||||
//Tabs
|
||||
final int currentServiceId = ServiceHelper.getSelectedServiceId(this);
|
||||
final StreamingService service = NewPipe.getService(currentServiceId);
|
||||
|
||||
int kioskId = 0;
|
||||
|
||||
for (final String ks : service.getKioskList().getAvailableKiosks()) {
|
||||
drawerLayoutBinding.navigation.getMenu()
|
||||
.add(R.id.menu_tabs_group, kioskId, ORDER,
|
||||
KioskTranslator.getTranslatedKioskName(ks, this))
|
||||
.setIcon(KioskTranslator.getKioskIcon(ks, this));
|
||||
kioskId++;
|
||||
}
|
||||
|
||||
drawerLayoutBinding.navigation.getMenu()
|
||||
.add(R.id.menu_tabs_group, ITEM_ID_SUBSCRIPTIONS, ORDER, R.string.tab_subscriptions)
|
||||
.setIcon(R.drawable.ic_tv);
|
||||
drawerLayoutBinding.navigation.getMenu()
|
||||
.add(R.id.menu_tabs_group, ITEM_ID_FEED, ORDER, R.string.fragment_feed_title)
|
||||
.setIcon(R.drawable.ic_rss_feed);
|
||||
drawerLayoutBinding.navigation.getMenu()
|
||||
.add(R.id.menu_tabs_group, ITEM_ID_BOOKMARKS, ORDER, R.string.tab_bookmarks)
|
||||
.setIcon(R.drawable.ic_bookmark);
|
||||
drawerLayoutBinding.navigation.getMenu()
|
||||
.add(R.id.menu_tabs_group, ITEM_ID_DOWNLOADS, ORDER, R.string.downloads)
|
||||
.setIcon(R.drawable.ic_file_download);
|
||||
drawerLayoutBinding.navigation.getMenu()
|
||||
.add(R.id.menu_tabs_group, ITEM_ID_HISTORY, ORDER, R.string.action_history)
|
||||
.setIcon(R.drawable.ic_history);
|
||||
|
||||
//Settings and About
|
||||
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_ABOUT, ORDER, R.string.tab_about)
|
||||
.setIcon(R.drawable.ic_info_outline);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
super.onDestroy();
|
||||
@@ -495,7 +475,7 @@ public class MainActivity extends AppCompatActivity {
|
||||
drawerHeaderBinding.drawerHeaderActionButton.setContentDescription(
|
||||
getString(R.string.drawer_header_description) + selectedServiceName);
|
||||
} catch (final Exception e) {
|
||||
ErrorActivity.reportUiErrorInSnackbar(this, "Setting up service toggle", e);
|
||||
ErrorUtil.showUiErrorSnackbar(this, "Setting up service toggle", e);
|
||||
}
|
||||
|
||||
final SharedPreferences sharedPreferences
|
||||
@@ -805,7 +785,7 @@ public class MainActivity extends AppCompatActivity {
|
||||
NavigationHelper.gotoMainFragment(getSupportFragmentManager());
|
||||
}
|
||||
} catch (final Exception e) {
|
||||
ErrorActivity.reportUiErrorInSnackbar(this, "Handling intent", e);
|
||||
ErrorUtil.showUiErrorSnackbar(this, "Handling intent", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,15 +9,19 @@ import android.widget.PopupMenu;
|
||||
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
|
||||
import org.schabi.newpipe.local.dialog.PlaylistAppendDialog;
|
||||
import org.schabi.newpipe.local.dialog.PlaylistCreationDialog;
|
||||
import org.schabi.newpipe.database.stream.model.StreamEntity;
|
||||
import org.schabi.newpipe.local.dialog.PlaylistDialog;
|
||||
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
||||
import org.schabi.newpipe.player.playqueue.PlayQueueItem;
|
||||
import org.schabi.newpipe.util.NavigationHelper;
|
||||
import org.schabi.newpipe.util.SaveUploaderUrlHelper;
|
||||
|
||||
import java.util.Collections;
|
||||
|
||||
public final class QueueItemMenuUtil {
|
||||
private QueueItemMenuUtil() {
|
||||
}
|
||||
|
||||
public static void openPopupMenu(final PlayQueue playQueue,
|
||||
final PlayQueueItem item,
|
||||
final View view,
|
||||
@@ -47,13 +51,24 @@ public final class QueueItemMenuUtil {
|
||||
false);
|
||||
return true;
|
||||
case R.id.menu_item_append_playlist:
|
||||
final PlaylistAppendDialog d = PlaylistAppendDialog.fromPlayQueueItems(
|
||||
Collections.singletonList(item)
|
||||
PlaylistDialog.createCorrespondingDialog(
|
||||
context,
|
||||
Collections.singletonList(new StreamEntity(item)),
|
||||
dialog -> dialog.show(
|
||||
fragmentManager,
|
||||
"QueueItemMenuUtil@append_playlist"
|
||||
)
|
||||
);
|
||||
PlaylistAppendDialog.onPlaylistFound(context,
|
||||
() -> d.show(fragmentManager, "QueueItemMenuUtil@append_playlist"),
|
||||
() -> PlaylistCreationDialog.newInstance(d)
|
||||
.show(fragmentManager, "QueueItemMenuUtil@append_playlist"));
|
||||
|
||||
return true;
|
||||
case R.id.menu_item_channel_details:
|
||||
SaveUploaderUrlHelper.saveUploaderUrlIfNeeded(context, item,
|
||||
// An intent must be used here.
|
||||
// Opening with FragmentManager transactions is not working,
|
||||
// as PlayQueueActivity doesn't use fragments.
|
||||
uploaderUrl -> NavigationHelper.openChannelFragmentUsingIntent(
|
||||
context, item.getServiceId(), uploaderUrl, item.getUploader()
|
||||
));
|
||||
return true;
|
||||
case R.id.menu_item_share:
|
||||
shareText(context, item.getTitle(), item.getUrl(),
|
||||
@@ -65,6 +80,4 @@ public final class QueueItemMenuUtil {
|
||||
|
||||
popupMenu.show();
|
||||
}
|
||||
|
||||
private QueueItemMenuUtil() { }
|
||||
}
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
package org.schabi.newpipe;
|
||||
|
||||
import static org.schabi.newpipe.extractor.StreamingService.ServiceInfo.MediaCapability.AUDIO;
|
||||
import static org.schabi.newpipe.extractor.StreamingService.ServiceInfo.MediaCapability.VIDEO;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.IntentService;
|
||||
import android.content.Context;
|
||||
@@ -30,11 +33,12 @@ import androidx.core.widget.TextViewCompat;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
import androidx.preference.PreferenceManager;
|
||||
|
||||
import org.schabi.newpipe.database.stream.model.StreamEntity;
|
||||
import org.schabi.newpipe.databinding.ListRadioIconItemBinding;
|
||||
import org.schabi.newpipe.databinding.SingleChoiceDialogViewBinding;
|
||||
import org.schabi.newpipe.download.DownloadDialog;
|
||||
import org.schabi.newpipe.error.ErrorActivity;
|
||||
import org.schabi.newpipe.error.ErrorInfo;
|
||||
import org.schabi.newpipe.error.ErrorUtil;
|
||||
import org.schabi.newpipe.error.ReCaptchaActivity;
|
||||
import org.schabi.newpipe.error.UserAction;
|
||||
import org.schabi.newpipe.extractor.Info;
|
||||
@@ -56,6 +60,7 @@ import org.schabi.newpipe.extractor.playlist.PlaylistInfo;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||
import org.schabi.newpipe.extractor.stream.VideoStream;
|
||||
import org.schabi.newpipe.ktx.ExceptionUtils;
|
||||
import org.schabi.newpipe.local.dialog.PlaylistDialog;
|
||||
import org.schabi.newpipe.player.MainPlayer;
|
||||
import org.schabi.newpipe.player.helper.PlayerHelper;
|
||||
import org.schabi.newpipe.player.helper.PlayerHolder;
|
||||
@@ -69,14 +74,15 @@ import org.schabi.newpipe.util.ExtractorHelper;
|
||||
import org.schabi.newpipe.util.ListHelper;
|
||||
import org.schabi.newpipe.util.NavigationHelper;
|
||||
import org.schabi.newpipe.util.PermissionHelper;
|
||||
import org.schabi.newpipe.util.external_communication.ShareUtils;
|
||||
import org.schabi.newpipe.util.ThemeHelper;
|
||||
import org.schabi.newpipe.util.external_communication.ShareUtils;
|
||||
import org.schabi.newpipe.util.urlfinder.UrlFinder;
|
||||
import org.schabi.newpipe.views.FocusOverlayView;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import icepick.Icepick;
|
||||
@@ -89,9 +95,6 @@ import io.reactivex.rxjava3.disposables.Disposable;
|
||||
import io.reactivex.rxjava3.functions.Consumer;
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers;
|
||||
|
||||
import static org.schabi.newpipe.extractor.StreamingService.ServiceInfo.MediaCapability.AUDIO;
|
||||
import static org.schabi.newpipe.extractor.StreamingService.ServiceInfo.MediaCapability.VIDEO;
|
||||
|
||||
/**
|
||||
* Get the url from the intent and open it in the chosen preferred player.
|
||||
*/
|
||||
@@ -107,6 +110,7 @@ public class RouterActivity extends AppCompatActivity {
|
||||
protected String currentUrl;
|
||||
private StreamingService currentService;
|
||||
private boolean selectionIsDownload = false;
|
||||
private boolean selectionIsAddToPlaylist = false;
|
||||
private AlertDialog alertDialogChoice = null;
|
||||
|
||||
@Override
|
||||
@@ -227,7 +231,7 @@ public class RouterActivity extends AppCompatActivity {
|
||||
} else if (errorInfo.getThrowable() instanceof ContentNotSupportedException) {
|
||||
Toast.makeText(context, R.string.content_not_supported, Toast.LENGTH_LONG).show();
|
||||
} else {
|
||||
ErrorActivity.reportError(context, errorInfo);
|
||||
ErrorUtil.createNotification(context, errorInfo);
|
||||
}
|
||||
|
||||
if (context instanceof RouterActivity) {
|
||||
@@ -350,7 +354,7 @@ public class RouterActivity extends AppCompatActivity {
|
||||
.setNegativeButton(R.string.just_once, dialogButtonsClickListener)
|
||||
.setPositiveButton(R.string.always, dialogButtonsClickListener)
|
||||
.setOnDismissListener((dialog) -> {
|
||||
if (!selectionIsDownload) {
|
||||
if (!selectionIsDownload && !selectionIsAddToPlaylist) {
|
||||
finish();
|
||||
}
|
||||
})
|
||||
@@ -446,6 +450,10 @@ public class RouterActivity extends AppCompatActivity {
|
||||
final AdapterChoiceItem backgroundPlayer = new AdapterChoiceItem(
|
||||
getString(R.string.background_player_key), getString(R.string.background_player),
|
||||
R.drawable.ic_headset);
|
||||
final AdapterChoiceItem addToPlaylist = new AdapterChoiceItem(
|
||||
getString(R.string.add_to_playlist_key), getString(R.string.add_to_playlist),
|
||||
R.drawable.ic_add);
|
||||
|
||||
|
||||
if (linkType == LinkType.STREAM) {
|
||||
if (isExtVideoEnabled) {
|
||||
@@ -482,6 +490,10 @@ public class RouterActivity extends AppCompatActivity {
|
||||
getString(R.string.download),
|
||||
R.drawable.ic_file_download));
|
||||
|
||||
// Add to playlist is not necessary for CHANNEL and PLAYLIST linkType since those can
|
||||
// not be added to a playlist
|
||||
returnList.add(addToPlaylist);
|
||||
|
||||
} else {
|
||||
returnList.add(showInfo);
|
||||
if (capabilities.contains(VIDEO) && !isExtVideoEnabled) {
|
||||
@@ -547,6 +559,12 @@ public class RouterActivity extends AppCompatActivity {
|
||||
return;
|
||||
}
|
||||
|
||||
if (selectedChoiceKey.equals(getString(R.string.add_to_playlist_key))) {
|
||||
selectionIsAddToPlaylist = true;
|
||||
openAddToPlaylistDialog();
|
||||
return;
|
||||
}
|
||||
|
||||
// stop and bypass FetcherService if InfoScreen was selected since
|
||||
// StreamDetailFragment can fetch data itself
|
||||
if (selectedChoiceKey.equals(getString(R.string.show_info_key))) {
|
||||
@@ -572,6 +590,41 @@ public class RouterActivity extends AppCompatActivity {
|
||||
finish();
|
||||
}
|
||||
|
||||
private void openAddToPlaylistDialog() {
|
||||
// Getting the stream info usually takes a moment
|
||||
// Notifying the user here to ensure that no confusion arises
|
||||
Toast.makeText(
|
||||
getApplicationContext(),
|
||||
getString(R.string.processing_may_take_a_moment),
|
||||
Toast.LENGTH_SHORT)
|
||||
.show();
|
||||
|
||||
disposables.add(ExtractorHelper.getStreamInfo(currentServiceId, currentUrl, false)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
info -> PlaylistDialog.createCorrespondingDialog(
|
||||
getThemeWrapperContext(),
|
||||
Collections.singletonList(new StreamEntity(info)),
|
||||
playlistDialog -> {
|
||||
playlistDialog.setOnDismissListener(dialog -> finish());
|
||||
|
||||
playlistDialog.show(
|
||||
this.getSupportFragmentManager(),
|
||||
"addToPlaylistDialog"
|
||||
);
|
||||
}
|
||||
),
|
||||
throwable -> handleError(this, new ErrorInfo(
|
||||
throwable,
|
||||
UserAction.REQUESTED_STREAM,
|
||||
"Tried to add " + currentUrl + " to a playlist",
|
||||
currentService.getServiceId())
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@SuppressLint("CheckResult")
|
||||
private void openDownloadDialog() {
|
||||
disposables.add(ExtractorHelper.getStreamInfo(currentServiceId, currentUrl, true)
|
||||
|
||||
@@ -185,7 +185,11 @@ class AboutActivity : AppCompatActivity() {
|
||||
SoftwareComponent(
|
||||
"RxJava", "2016 - 2020", "RxJava Contributors",
|
||||
"https://github.com/ReactiveX/RxJava", StandardLicenses.APACHE2
|
||||
)
|
||||
),
|
||||
SoftwareComponent(
|
||||
"SearchPreference", "2018", "ByteHamster",
|
||||
"https://github.com/ByteHamster/SearchPreference", StandardLicenses.MIT
|
||||
),
|
||||
)
|
||||
private const val POS_ABOUT = 0
|
||||
private const val POS_LICENSE = 1
|
||||
|
||||
@@ -7,6 +7,7 @@ import androidx.room.Query
|
||||
import androidx.room.Transaction
|
||||
import androidx.room.Update
|
||||
import io.reactivex.rxjava3.core.Flowable
|
||||
import io.reactivex.rxjava3.core.Maybe
|
||||
import org.schabi.newpipe.database.feed.model.FeedEntity
|
||||
import org.schabi.newpipe.database.feed.model.FeedLastUpdatedEntity
|
||||
import org.schabi.newpipe.database.stream.StreamWithState
|
||||
@@ -37,7 +38,7 @@ abstract class FeedDAO {
|
||||
LIMIT 500
|
||||
"""
|
||||
)
|
||||
abstract fun getAllStreams(): Flowable<List<StreamWithState>>
|
||||
abstract fun getAllStreams(): Maybe<List<StreamWithState>>
|
||||
|
||||
@Query(
|
||||
"""
|
||||
@@ -62,7 +63,7 @@ abstract class FeedDAO {
|
||||
LIMIT 500
|
||||
"""
|
||||
)
|
||||
abstract fun getAllStreamsForGroup(groupId: Long): Flowable<List<StreamWithState>>
|
||||
abstract fun getAllStreamsForGroup(groupId: Long): Maybe<List<StreamWithState>>
|
||||
|
||||
/**
|
||||
* @see StreamStateEntity.isFinished()
|
||||
@@ -97,7 +98,7 @@ abstract class FeedDAO {
|
||||
LIMIT 500
|
||||
"""
|
||||
)
|
||||
abstract fun getLiveOrNotPlayedStreams(): Flowable<List<StreamWithState>>
|
||||
abstract fun getLiveOrNotPlayedStreams(): Maybe<List<StreamWithState>>
|
||||
|
||||
/**
|
||||
* @see StreamStateEntity.isFinished()
|
||||
@@ -137,7 +138,7 @@ abstract class FeedDAO {
|
||||
LIMIT 500
|
||||
"""
|
||||
)
|
||||
abstract fun getLiveOrNotPlayedStreamsForGroup(groupId: Long): Flowable<List<StreamWithState>>
|
||||
abstract fun getLiveOrNotPlayedStreamsForGroup(groupId: Long): Maybe<List<StreamWithState>>
|
||||
|
||||
@Query(
|
||||
"""
|
||||
|
||||
@@ -19,6 +19,7 @@ import static org.schabi.newpipe.database.history.model.SearchHistoryEntry.TABLE
|
||||
@Dao
|
||||
public interface SearchHistoryDAO extends HistoryDAO<SearchHistoryEntry> {
|
||||
String ORDER_BY_CREATION_DATE = " ORDER BY " + CREATION_DATE + " DESC";
|
||||
String ORDER_BY_MAX_CREATION_DATE = " ORDER BY MAX(" + CREATION_DATE + ") DESC";
|
||||
|
||||
@Query("SELECT * FROM " + TABLE_NAME
|
||||
+ " WHERE " + ID + " = (SELECT MAX(" + ID + ") FROM " + TABLE_NAME + ")")
|
||||
@@ -36,16 +37,16 @@ public interface SearchHistoryDAO extends HistoryDAO<SearchHistoryEntry> {
|
||||
@Override
|
||||
Flowable<List<SearchHistoryEntry>> getAll();
|
||||
|
||||
@Query("SELECT * FROM " + TABLE_NAME + " GROUP BY " + SEARCH + ORDER_BY_CREATION_DATE
|
||||
+ " LIMIT :limit")
|
||||
Flowable<List<SearchHistoryEntry>> getUniqueEntries(int limit);
|
||||
@Query("SELECT " + SEARCH + " FROM " + TABLE_NAME + " GROUP BY " + SEARCH
|
||||
+ ORDER_BY_MAX_CREATION_DATE + " LIMIT :limit")
|
||||
Flowable<List<String>> getUniqueEntries(int limit);
|
||||
|
||||
@Query("SELECT * FROM " + TABLE_NAME
|
||||
+ " WHERE " + SERVICE_ID + " = :serviceId" + ORDER_BY_CREATION_DATE)
|
||||
@Override
|
||||
Flowable<List<SearchHistoryEntry>> listByService(int serviceId);
|
||||
|
||||
@Query("SELECT * FROM " + TABLE_NAME + " WHERE " + SEARCH + " LIKE :query || '%'"
|
||||
+ " GROUP BY " + SEARCH + " LIMIT :limit")
|
||||
Flowable<List<SearchHistoryEntry>> getSimilarEntries(String query, int limit);
|
||||
@Query("SELECT " + SEARCH + " FROM " + TABLE_NAME + " WHERE " + SEARCH + " LIKE :query || '%'"
|
||||
+ " GROUP BY " + SEARCH + ORDER_BY_MAX_CREATION_DATE + " LIMIT :limit")
|
||||
Flowable<List<String>> getSimilarEntries(String query, int limit);
|
||||
}
|
||||
|
||||
@@ -1,79 +0,0 @@
|
||||
package org.schabi.newpipe.database.history.model;
|
||||
|
||||
import androidx.room.ColumnInfo;
|
||||
import androidx.room.Entity;
|
||||
import androidx.room.Ignore;
|
||||
import androidx.room.Index;
|
||||
import androidx.room.PrimaryKey;
|
||||
|
||||
import java.time.OffsetDateTime;
|
||||
|
||||
import static org.schabi.newpipe.database.history.model.SearchHistoryEntry.SEARCH;
|
||||
|
||||
@Entity(tableName = SearchHistoryEntry.TABLE_NAME,
|
||||
indices = {@Index(value = SEARCH)})
|
||||
public class SearchHistoryEntry {
|
||||
public static final String ID = "id";
|
||||
public static final String TABLE_NAME = "search_history";
|
||||
public static final String SERVICE_ID = "service_id";
|
||||
public static final String CREATION_DATE = "creation_date";
|
||||
public static final String SEARCH = "search";
|
||||
|
||||
@ColumnInfo(name = ID)
|
||||
@PrimaryKey(autoGenerate = true)
|
||||
private long id;
|
||||
|
||||
@ColumnInfo(name = CREATION_DATE)
|
||||
private OffsetDateTime creationDate;
|
||||
|
||||
@ColumnInfo(name = SERVICE_ID)
|
||||
private int serviceId;
|
||||
|
||||
@ColumnInfo(name = SEARCH)
|
||||
private String search;
|
||||
|
||||
public SearchHistoryEntry(final OffsetDateTime creationDate, final int serviceId,
|
||||
final String search) {
|
||||
this.serviceId = serviceId;
|
||||
this.creationDate = creationDate;
|
||||
this.search = search;
|
||||
}
|
||||
|
||||
public long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(final long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public OffsetDateTime getCreationDate() {
|
||||
return creationDate;
|
||||
}
|
||||
|
||||
public void setCreationDate(final OffsetDateTime creationDate) {
|
||||
this.creationDate = creationDate;
|
||||
}
|
||||
|
||||
public int getServiceId() {
|
||||
return serviceId;
|
||||
}
|
||||
|
||||
public void setServiceId(final int serviceId) {
|
||||
this.serviceId = serviceId;
|
||||
}
|
||||
|
||||
public String getSearch() {
|
||||
return search;
|
||||
}
|
||||
|
||||
public void setSearch(final String search) {
|
||||
this.search = search;
|
||||
}
|
||||
|
||||
@Ignore
|
||||
public boolean hasEqualValues(final SearchHistoryEntry otherEntry) {
|
||||
return getServiceId() == otherEntry.getServiceId()
|
||||
&& getSearch().equals(otherEntry.getSearch());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package org.schabi.newpipe.database.history.model
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.Ignore
|
||||
import androidx.room.Index
|
||||
import androidx.room.PrimaryKey
|
||||
import java.time.OffsetDateTime
|
||||
|
||||
@Entity(
|
||||
tableName = SearchHistoryEntry.TABLE_NAME,
|
||||
indices = [Index(value = [SearchHistoryEntry.SEARCH])]
|
||||
)
|
||||
data class SearchHistoryEntry(
|
||||
@field:ColumnInfo(name = CREATION_DATE) var creationDate: OffsetDateTime?,
|
||||
@field:ColumnInfo(
|
||||
name = SERVICE_ID
|
||||
) var serviceId: Int,
|
||||
@field:ColumnInfo(name = SEARCH) var search: String?
|
||||
) {
|
||||
@ColumnInfo(name = ID)
|
||||
@PrimaryKey(autoGenerate = true)
|
||||
var id: Long = 0
|
||||
|
||||
@Ignore
|
||||
fun hasEqualValues(otherEntry: SearchHistoryEntry): Boolean {
|
||||
return (
|
||||
serviceId == otherEntry.serviceId &&
|
||||
search == otherEntry.search
|
||||
)
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val ID = "id"
|
||||
const val TABLE_NAME = "search_history"
|
||||
const val SERVICE_ID = "service_id"
|
||||
const val CREATION_DATE = "creation_date"
|
||||
const val SEARCH = "search"
|
||||
}
|
||||
}
|
||||
@@ -41,8 +41,8 @@ import com.nononsenseapps.filepicker.Utils;
|
||||
import org.schabi.newpipe.MainActivity;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.databinding.DownloadDialogBinding;
|
||||
import org.schabi.newpipe.error.ErrorActivity;
|
||||
import org.schabi.newpipe.error.ErrorInfo;
|
||||
import org.schabi.newpipe.error.ErrorUtil;
|
||||
import org.schabi.newpipe.error.UserAction;
|
||||
import org.schabi.newpipe.extractor.MediaFormat;
|
||||
import org.schabi.newpipe.extractor.NewPipe;
|
||||
@@ -53,6 +53,7 @@ import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||
import org.schabi.newpipe.extractor.stream.SubtitlesStream;
|
||||
import org.schabi.newpipe.extractor.stream.VideoStream;
|
||||
import org.schabi.newpipe.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.FilePickerActivityHelper;
|
||||
@@ -402,7 +403,7 @@ public class DownloadDialog extends DialogFragment
|
||||
== R.id.video_button) {
|
||||
setupVideoSpinner();
|
||||
}
|
||||
}, throwable -> ErrorActivity.reportErrorInSnackbar(context,
|
||||
}, throwable -> ErrorUtil.showSnackbar(context,
|
||||
new ErrorInfo(throwable, UserAction.DOWNLOAD_OPEN_DIALOG,
|
||||
"Downloading video stream size",
|
||||
currentInfo.getServiceId()))));
|
||||
@@ -412,7 +413,7 @@ public class DownloadDialog extends DialogFragment
|
||||
== R.id.audio_button) {
|
||||
setupAudioSpinner();
|
||||
}
|
||||
}, throwable -> ErrorActivity.reportErrorInSnackbar(context,
|
||||
}, throwable -> ErrorUtil.showSnackbar(context,
|
||||
new ErrorInfo(throwable, UserAction.DOWNLOAD_OPEN_DIALOG,
|
||||
"Downloading audio stream size",
|
||||
currentInfo.getServiceId()))));
|
||||
@@ -422,7 +423,7 @@ public class DownloadDialog extends DialogFragment
|
||||
== R.id.subtitle_button) {
|
||||
setupSubtitleSpinner();
|
||||
}
|
||||
}, throwable -> ErrorActivity.reportErrorInSnackbar(context,
|
||||
}, throwable -> ErrorUtil.showSnackbar(context,
|
||||
new ErrorInfo(throwable, UserAction.DOWNLOAD_OPEN_DIALOG,
|
||||
"Downloading subtitle stream size",
|
||||
currentInfo.getServiceId()))));
|
||||
@@ -687,7 +688,12 @@ public class DownloadDialog extends DialogFragment
|
||||
}
|
||||
|
||||
private void launchDirectoryPicker(final ActivityResultLauncher<Intent> launcher) {
|
||||
launcher.launch(StoredDirectoryHelper.getPicker(context));
|
||||
NoFileManagerSafeGuard.launchSafe(
|
||||
launcher,
|
||||
StoredDirectoryHelper.getPicker(context),
|
||||
TAG,
|
||||
context
|
||||
);
|
||||
}
|
||||
|
||||
private void prepareSelectedDownload() {
|
||||
@@ -766,8 +772,12 @@ public class DownloadDialog extends DialogFragment
|
||||
initialPath = Uri.parse(initialSavePath.getAbsolutePath());
|
||||
}
|
||||
|
||||
requestDownloadSaveAsLauncher.launch(StoredFileHelper.getNewPicker(context,
|
||||
filenameTmp, mimeTmp, initialPath));
|
||||
NoFileManagerSafeGuard.launchSafe(
|
||||
requestDownloadSaveAsLauncher,
|
||||
StoredFileHelper.getNewPicker(context, filenameTmp, mimeTmp, initialPath),
|
||||
TAG,
|
||||
context
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
@@ -799,7 +809,7 @@ public class DownloadDialog extends DialogFragment
|
||||
mainStorage.getTag());
|
||||
}
|
||||
} catch (final Exception e) {
|
||||
ErrorActivity.reportErrorInSnackbar(this,
|
||||
ErrorUtil.createNotification(requireContext(),
|
||||
new ErrorInfo(e, UserAction.DOWNLOAD_FAILED, "Getting storage"));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -33,12 +33,11 @@ public class AcraReportSender implements ReportSender {
|
||||
|
||||
@Override
|
||||
public void send(@NonNull final Context context, @NonNull final CrashReportData report) {
|
||||
ErrorActivity.reportError(context, new ErrorInfo(
|
||||
ErrorUtil.openActivity(context, new ErrorInfo(
|
||||
new String[]{report.getString(ReportField.STACK_TRACE)},
|
||||
UserAction.UI_ERROR,
|
||||
ErrorInfo.SERVICE_NONE,
|
||||
"ACRA report",
|
||||
R.string.app_ui_crash,
|
||||
null));
|
||||
R.string.app_ui_crash));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
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.graphics.Color;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
@@ -11,15 +12,12 @@ import android.util.Log;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.fragment.app.Fragment;
|
||||
|
||||
import com.google.android.material.snackbar.Snackbar;
|
||||
import com.grack.nanojson.JsonWriter;
|
||||
|
||||
import org.schabi.newpipe.BuildConfig;
|
||||
@@ -27,15 +25,13 @@ 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.external_communication.ShareUtils;
|
||||
import org.schabi.newpipe.util.ThemeHelper;
|
||||
import org.schabi.newpipe.util.external_communication.ShareUtils;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.Arrays;
|
||||
|
||||
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
|
||||
|
||||
/*
|
||||
* Created by Christian Schabesberger on 24.10.15.
|
||||
*
|
||||
@@ -56,6 +52,10 @@ import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
|
||||
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/**
|
||||
* This activity is used to show error details and allow reporting them in various ways. Use {@link
|
||||
* ErrorUtil#openActivity(Context, ErrorInfo)} to correctly open this activity.
|
||||
*/
|
||||
public class ErrorActivity extends AppCompatActivity {
|
||||
// LOG TAGS
|
||||
public static final String TAG = ErrorActivity.class.toString();
|
||||
@@ -77,57 +77,6 @@ public class ErrorActivity extends AppCompatActivity {
|
||||
|
||||
private ActivityErrorBinding activityErrorBinding;
|
||||
|
||||
public static void reportError(final Context context, final ErrorInfo errorInfo) {
|
||||
final Intent intent = new Intent(context, ErrorActivity.class);
|
||||
intent.putExtra(ERROR_INFO, errorInfo);
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
context.startActivity(intent);
|
||||
}
|
||||
|
||||
public static void reportErrorInSnackbar(final Context context, final ErrorInfo errorInfo) {
|
||||
final View rootView = context instanceof Activity
|
||||
? ((Activity) context).findViewById(android.R.id.content) : null;
|
||||
reportErrorInSnackbar(context, rootView, errorInfo);
|
||||
}
|
||||
|
||||
public static void reportErrorInSnackbar(final Fragment fragment, final ErrorInfo errorInfo) {
|
||||
View rootView = fragment.getView();
|
||||
if (rootView == null && fragment.getActivity() != null) {
|
||||
rootView = fragment.getActivity().findViewById(android.R.id.content);
|
||||
}
|
||||
reportErrorInSnackbar(fragment.requireContext(), rootView, errorInfo);
|
||||
}
|
||||
|
||||
public static void reportUiErrorInSnackbar(final Context context,
|
||||
final String request,
|
||||
final Throwable throwable) {
|
||||
reportErrorInSnackbar(context, new ErrorInfo(throwable, UserAction.UI_ERROR, request));
|
||||
}
|
||||
|
||||
public static void reportUiErrorInSnackbar(final Fragment fragment,
|
||||
final String request,
|
||||
final Throwable throwable) {
|
||||
reportErrorInSnackbar(fragment, new ErrorInfo(throwable, UserAction.UI_ERROR, request));
|
||||
}
|
||||
|
||||
|
||||
////////////////////////////////////////////////////////////////////////
|
||||
// Utils
|
||||
////////////////////////////////////////////////////////////////////////
|
||||
|
||||
private static void reportErrorInSnackbar(final Context context,
|
||||
@Nullable final View rootView,
|
||||
final ErrorInfo errorInfo) {
|
||||
if (rootView != null) {
|
||||
Snackbar.make(rootView, R.string.error_snackbar_message, Snackbar.LENGTH_LONG)
|
||||
.setActionTextColor(Color.YELLOW)
|
||||
.setAction(context.getString(R.string.error_snackbar_action).toUpperCase(), v ->
|
||||
reportError(context, errorInfo)).show();
|
||||
} else {
|
||||
reportError(context, errorInfo);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
////////////////////////////////////////////////////////////////////////
|
||||
// Activity lifecycle
|
||||
|
||||
@@ -2,6 +2,8 @@ package org.schabi.newpipe.error
|
||||
|
||||
import android.os.Parcelable
|
||||
import androidx.annotation.StringRes
|
||||
import com.google.android.exoplayer2.ExoPlaybackException
|
||||
import kotlinx.parcelize.IgnoredOnParcel
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import org.schabi.newpipe.R
|
||||
import org.schabi.newpipe.extractor.Info
|
||||
@@ -21,11 +23,14 @@ class ErrorInfo(
|
||||
val userAction: UserAction,
|
||||
val serviceName: String,
|
||||
val request: String,
|
||||
val messageStringId: Int,
|
||||
@Transient // no need to store throwable, all data for report is in other variables
|
||||
var throwable: Throwable? = null
|
||||
val messageStringId: Int
|
||||
) : Parcelable {
|
||||
|
||||
// no need to store throwable, all data for report is in other variables
|
||||
// also, the throwable might not be serializable, see TeamNewPipe/NewPipe#7302
|
||||
@IgnoredOnParcel
|
||||
var throwable: Throwable? = null
|
||||
|
||||
private constructor(
|
||||
throwable: Throwable,
|
||||
userAction: UserAction,
|
||||
@@ -36,9 +41,10 @@ class ErrorInfo(
|
||||
userAction,
|
||||
serviceName,
|
||||
request,
|
||||
getMessageStringId(throwable, userAction),
|
||||
throwable
|
||||
)
|
||||
getMessageStringId(throwable, userAction)
|
||||
) {
|
||||
this.throwable = throwable
|
||||
}
|
||||
|
||||
private constructor(
|
||||
throwable: List<Throwable>,
|
||||
@@ -50,9 +56,10 @@ class ErrorInfo(
|
||||
userAction,
|
||||
serviceName,
|
||||
request,
|
||||
getMessageStringId(throwable.firstOrNull(), userAction),
|
||||
throwable.firstOrNull()
|
||||
)
|
||||
getMessageStringId(throwable.firstOrNull(), userAction)
|
||||
) {
|
||||
this.throwable = throwable.firstOrNull()
|
||||
}
|
||||
|
||||
// constructors with single throwable
|
||||
constructor(throwable: Throwable, userAction: UserAction, request: String) :
|
||||
@@ -102,6 +109,13 @@ class ErrorInfo(
|
||||
throwable is ContentNotSupportedException -> R.string.content_not_supported
|
||||
throwable is DeobfuscateException -> R.string.youtube_signature_deobfuscation_error
|
||||
throwable is ExtractionException -> R.string.parsing_error
|
||||
throwable is ExoPlaybackException -> {
|
||||
when (throwable.type) {
|
||||
ExoPlaybackException.TYPE_SOURCE -> R.string.player_stream_failure
|
||||
ExoPlaybackException.TYPE_UNEXPECTED -> R.string.player_recoverable_failure
|
||||
else -> R.string.player_unrecoverable_failure
|
||||
}
|
||||
}
|
||||
action == UserAction.UI_ERROR -> R.string.app_ui_crash
|
||||
action == UserAction.REQUESTED_COMMENTS -> R.string.error_unable_to_load_comments
|
||||
action == UserAction.SUBSCRIPTION_CHANGE -> R.string.subscription_change_failed
|
||||
|
||||
@@ -118,7 +118,7 @@ class ErrorPanelHelper(
|
||||
showAndSetErrorButtonAction(
|
||||
R.string.error_snackbar_action
|
||||
) {
|
||||
ErrorActivity.reportError(context, errorInfo)
|
||||
ErrorUtil.openActivity(context, errorInfo)
|
||||
}
|
||||
|
||||
errorTextView.setText(getExceptionDescription(errorInfo.throwable))
|
||||
@@ -178,7 +178,7 @@ class ErrorPanelHelper(
|
||||
val DEBUG: Boolean = MainActivity.DEBUG
|
||||
|
||||
@StringRes
|
||||
public fun getExceptionDescription(throwable: Throwable?): Int {
|
||||
fun getExceptionDescription(throwable: Throwable?): Int {
|
||||
return when (throwable) {
|
||||
is AgeRestrictedContentException -> R.string.restricted_video_no_stream
|
||||
is GeographicRestrictionException -> R.string.georestricted_content
|
||||
|
||||
165
app/src/main/java/org/schabi/newpipe/error/ErrorUtil.kt
Normal file
165
app/src/main/java/org/schabi/newpipe/error/ErrorUtil.kt
Normal file
@@ -0,0 +1,165 @@
|
||||
package org.schabi.newpipe.error
|
||||
|
||||
import android.app.Activity
|
||||
import android.app.NotificationManager
|
||||
import android.app.PendingIntent
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.graphics.Color
|
||||
import android.os.Build
|
||||
import android.view.View
|
||||
import android.widget.Toast
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.fragment.app.Fragment
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import org.schabi.newpipe.R
|
||||
|
||||
/**
|
||||
* This class contains all of the methods that should be used to let the user know that an error has
|
||||
* occurred in the least intrusive way possible for each case. This class is for unexpected errors,
|
||||
* for handled errors (e.g. network errors) use e.g. [ErrorPanelHelper] instead.
|
||||
* - Use a snackbar if the exception is not critical and it happens in a place where a root view
|
||||
* is available.
|
||||
* - Use a notification if the exception happens inside a background service (player, subscription
|
||||
* import, ...) or there is no activity/fragment from which to extract a root view.
|
||||
* - Finally use the error activity only as a last resort in case the exception is critical and
|
||||
* happens in an open activity (since the workflow would be interrupted anyway in that case).
|
||||
*/
|
||||
class ErrorUtil {
|
||||
companion object {
|
||||
private const val ERROR_REPORT_NOTIFICATION_ID = 5340681
|
||||
|
||||
/**
|
||||
* Starts a new error activity allowing the user to report the provided error. Only use this
|
||||
* method directly as a last resort in case the exception is critical and happens in an open
|
||||
* activity (since the workflow would be interrupted anyway in that case). So never use this
|
||||
* for background services.
|
||||
*
|
||||
* @param context the context to use to start the new activity
|
||||
* @param errorInfo the error info to be reported
|
||||
*/
|
||||
@JvmStatic
|
||||
fun openActivity(context: Context, errorInfo: ErrorInfo) {
|
||||
context.startActivity(getErrorActivityIntent(context, errorInfo))
|
||||
}
|
||||
|
||||
/**
|
||||
* Show a bottom snackbar to the user, with a report button that opens the error activity.
|
||||
* Use this method if the exception is not critical and it happens in a place where a root
|
||||
* view is available.
|
||||
*
|
||||
* @param context will be used to obtain the root view if it is an [Activity]; if no root
|
||||
* view can be found an error notification is shown instead
|
||||
* @param errorInfo the error info to be reported
|
||||
*/
|
||||
@JvmStatic
|
||||
fun showSnackbar(context: Context, errorInfo: ErrorInfo) {
|
||||
val rootView = if (context is Activity) context.findViewById<View>(R.id.content) else null
|
||||
showSnackbar(context, rootView, errorInfo)
|
||||
}
|
||||
|
||||
/**
|
||||
* Show a bottom snackbar to the user, with a report button that opens the error activity.
|
||||
* Use this method if the exception is not critical and it happens in a place where a root
|
||||
* view is available.
|
||||
*
|
||||
* @param fragment will be used to obtain the root view if it has a connected [Activity]; if
|
||||
* no root view can be found an error notification is shown instead
|
||||
* @param errorInfo the error info to be reported
|
||||
*/
|
||||
@JvmStatic
|
||||
fun showSnackbar(fragment: Fragment, errorInfo: ErrorInfo) {
|
||||
var rootView = fragment.view
|
||||
if (rootView == null && fragment.activity != null) {
|
||||
rootView = fragment.requireActivity().findViewById(R.id.content)
|
||||
}
|
||||
showSnackbar(fragment.requireContext(), rootView, errorInfo)
|
||||
}
|
||||
|
||||
/**
|
||||
* Shortcut to calling [showSnackbar] with an [ErrorInfo] of type [UserAction.UI_ERROR]
|
||||
*/
|
||||
@JvmStatic
|
||||
fun showUiErrorSnackbar(context: Context, request: String, throwable: Throwable) {
|
||||
showSnackbar(context, ErrorInfo(throwable, UserAction.UI_ERROR, request))
|
||||
}
|
||||
|
||||
/**
|
||||
* Shortcut to calling [showSnackbar] with an [ErrorInfo] of type [UserAction.UI_ERROR]
|
||||
*/
|
||||
@JvmStatic
|
||||
fun showUiErrorSnackbar(fragment: Fragment, request: String, throwable: Throwable) {
|
||||
showSnackbar(fragment, ErrorInfo(throwable, UserAction.UI_ERROR, request))
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an error notification. Tapping on the notification opens the error activity. Use
|
||||
* this method if the exception happens inside a background service (player, subscription
|
||||
* import, ...) or there is no activity/fragment from which to extract a root view.
|
||||
*
|
||||
* @param context the context to use to show the notification
|
||||
* @param errorInfo the error info to be reported; the error message
|
||||
* [ErrorInfo.messageStringId] will be shown in the notification
|
||||
* description
|
||||
*/
|
||||
@JvmStatic
|
||||
fun createNotification(context: Context, errorInfo: ErrorInfo) {
|
||||
val notificationManager =
|
||||
ContextCompat.getSystemService(context, NotificationManager::class.java)
|
||||
if (notificationManager == null) {
|
||||
// this should never happen, but just in case open error activity
|
||||
openActivity(context, errorInfo)
|
||||
}
|
||||
|
||||
var pendingIntentFlags = PendingIntent.FLAG_UPDATE_CURRENT
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
pendingIntentFlags = pendingIntentFlags or PendingIntent.FLAG_IMMUTABLE
|
||||
}
|
||||
|
||||
val notificationBuilder: NotificationCompat.Builder =
|
||||
NotificationCompat.Builder(
|
||||
context,
|
||||
context.getString(R.string.error_report_channel_id)
|
||||
)
|
||||
.setSmallIcon(R.drawable.ic_bug_report)
|
||||
.setContentTitle(context.getString(R.string.error_report_notification_title))
|
||||
.setContentText(context.getString(errorInfo.messageStringId))
|
||||
.setAutoCancel(true)
|
||||
.setContentIntent(
|
||||
PendingIntent.getActivity(
|
||||
context,
|
||||
0,
|
||||
getErrorActivityIntent(context, errorInfo),
|
||||
pendingIntentFlags
|
||||
)
|
||||
)
|
||||
|
||||
notificationManager!!.notify(ERROR_REPORT_NOTIFICATION_ID, notificationBuilder.build())
|
||||
|
||||
// since the notification is silent, also show a toast, otherwise the user is confused
|
||||
Toast.makeText(context, R.string.error_report_notification_toast, Toast.LENGTH_SHORT)
|
||||
.show()
|
||||
}
|
||||
|
||||
private fun getErrorActivityIntent(context: Context, errorInfo: ErrorInfo): Intent {
|
||||
val intent = Intent(context, ErrorActivity::class.java)
|
||||
intent.putExtra(ErrorActivity.ERROR_INFO, errorInfo)
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
return intent
|
||||
}
|
||||
|
||||
private fun showSnackbar(context: Context, rootView: View?, errorInfo: ErrorInfo) {
|
||||
if (rootView == null) {
|
||||
// fallback to showing a notification if no root view is available
|
||||
createNotification(context, errorInfo)
|
||||
} else {
|
||||
Snackbar.make(rootView, R.string.error_snackbar_message, Snackbar.LENGTH_LONG)
|
||||
.setActionTextColor(Color.YELLOW)
|
||||
.setAction(context.getString(R.string.error_snackbar_action).uppercase()) {
|
||||
openActivity(context, errorInfo)
|
||||
}.show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,12 +7,13 @@ import android.widget.ProgressBar;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.Fragment;
|
||||
|
||||
import org.schabi.newpipe.BaseFragment;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.error.ErrorActivity;
|
||||
import org.schabi.newpipe.error.ErrorInfo;
|
||||
import org.schabi.newpipe.error.ErrorPanelHelper;
|
||||
import org.schabi.newpipe.error.ErrorUtil;
|
||||
import org.schabi.newpipe.util.InfoCache;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
@@ -198,9 +199,8 @@ public abstract class BaseStateFragment<I> extends BaseFragment implements ViewC
|
||||
}
|
||||
|
||||
/**
|
||||
* Show a SnackBar and only call
|
||||
* {@link ErrorActivity#reportErrorInSnackbar(androidx.fragment.app.Fragment, ErrorInfo)}
|
||||
* IF we a find a valid view (otherwise the error screen appears).
|
||||
* Directly calls {@link ErrorUtil#showSnackbar(Fragment, ErrorInfo)}, that shows a snackbar if
|
||||
* a valid view can be found, otherwise creates an error report notification.
|
||||
*
|
||||
* @param errorInfo The error information
|
||||
*/
|
||||
@@ -208,6 +208,6 @@ public abstract class BaseStateFragment<I> extends BaseFragment implements ViewC
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "showSnackBarError() called with: errorInfo = [" + errorInfo + "]");
|
||||
}
|
||||
ErrorActivity.reportErrorInSnackbar(this, errorInfo);
|
||||
ErrorUtil.showSnackbar(this, errorInfo);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,8 +20,8 @@ public class BlankFragment extends BaseFragment {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setUserVisibleHint(final boolean isVisibleToUser) {
|
||||
super.setUserVisibleHint(isVisibleToUser);
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
setTitle("NewPipe");
|
||||
// leave this inline. Will make it harder for copy cats.
|
||||
// If you are a Copy cat FUCK YOU.
|
||||
|
||||
@@ -23,7 +23,7 @@ import com.google.android.material.tabs.TabLayout;
|
||||
import org.schabi.newpipe.BaseFragment;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.databinding.FragmentMainBinding;
|
||||
import org.schabi.newpipe.error.ErrorActivity;
|
||||
import org.schabi.newpipe.error.ErrorUtil;
|
||||
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||
import org.schabi.newpipe.settings.tabs.Tab;
|
||||
import org.schabi.newpipe.settings.tabs.TabsManager;
|
||||
@@ -145,7 +145,7 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte
|
||||
NavigationHelper.openSearchFragment(getFM(),
|
||||
ServiceHelper.getSelectedServiceId(activity), "");
|
||||
} catch (final Exception e) {
|
||||
ErrorActivity.reportUiErrorInSnackbar(this, "Opening search fragment", e);
|
||||
ErrorUtil.showUiErrorSnackbar(this, "Opening search fragment", e);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -227,16 +227,11 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte
|
||||
public Fragment getItem(final int position) {
|
||||
final Tab tab = internalTabsList.get(position);
|
||||
|
||||
Throwable throwable = null;
|
||||
Fragment fragment = null;
|
||||
final Fragment fragment;
|
||||
try {
|
||||
fragment = tab.getFragment(context);
|
||||
} catch (final ExtractionException e) {
|
||||
throwable = e;
|
||||
}
|
||||
|
||||
if (throwable != null) {
|
||||
ErrorActivity.reportUiErrorInSnackbar(context, "Getting fragment item", throwable);
|
||||
ErrorUtil.showUiErrorSnackbar(context, "Getting fragment item", e);
|
||||
return new BlankFragment();
|
||||
}
|
||||
|
||||
|
||||
@@ -52,10 +52,11 @@ import com.squareup.picasso.Callback;
|
||||
|
||||
import org.schabi.newpipe.App;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.database.stream.model.StreamEntity;
|
||||
import org.schabi.newpipe.databinding.FragmentVideoDetailBinding;
|
||||
import org.schabi.newpipe.download.DownloadDialog;
|
||||
import org.schabi.newpipe.error.ErrorActivity;
|
||||
import org.schabi.newpipe.error.ErrorInfo;
|
||||
import org.schabi.newpipe.error.ErrorUtil;
|
||||
import org.schabi.newpipe.error.ReCaptchaActivity;
|
||||
import org.schabi.newpipe.error.UserAction;
|
||||
import org.schabi.newpipe.extractor.InfoItem;
|
||||
@@ -73,8 +74,7 @@ import org.schabi.newpipe.fragments.EmptyFragment;
|
||||
import org.schabi.newpipe.fragments.list.comments.CommentsFragment;
|
||||
import org.schabi.newpipe.fragments.list.videos.RelatedItemsFragment;
|
||||
import org.schabi.newpipe.ktx.AnimationType;
|
||||
import org.schabi.newpipe.local.dialog.PlaylistAppendDialog;
|
||||
import org.schabi.newpipe.local.dialog.PlaylistCreationDialog;
|
||||
import org.schabi.newpipe.local.dialog.PlaylistDialog;
|
||||
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
||||
import org.schabi.newpipe.player.MainPlayer;
|
||||
import org.schabi.newpipe.player.MainPlayer.PlayerType;
|
||||
@@ -99,6 +99,7 @@ import org.schabi.newpipe.util.external_communication.KoreUtils;
|
||||
import org.schabi.newpipe.util.external_communication.ShareUtils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
@@ -444,12 +445,11 @@ public final class VideoDetailFragment
|
||||
break;
|
||||
case R.id.detail_controls_playlist_append:
|
||||
if (getFM() != null && currentInfo != null) {
|
||||
|
||||
final PlaylistAppendDialog d = PlaylistAppendDialog.fromStreamInfo(currentInfo);
|
||||
disposables.add(
|
||||
PlaylistAppendDialog.onPlaylistFound(getContext(),
|
||||
() -> d.show(getFM(), TAG),
|
||||
() -> PlaylistCreationDialog.newInstance(d).show(getFM(), TAG)
|
||||
PlaylistDialog.createCorrespondingDialog(
|
||||
getContext(),
|
||||
Collections.singletonList(new StreamEntity(currentInfo)),
|
||||
dialog -> dialog.show(getFM(), TAG)
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -500,6 +500,10 @@ public final class VideoDetailFragment
|
||||
break;
|
||||
case R.id.detail_thumbnail_root_layout:
|
||||
autoPlayEnabled = true; // forcefully start playing
|
||||
// FIXME Workaround #7427
|
||||
if (isPlayerAvailable()) {
|
||||
player.setRecovery();
|
||||
}
|
||||
openVideoPlayerAutoFullscreen();
|
||||
break;
|
||||
case R.id.detail_title_root_layout:
|
||||
@@ -533,7 +537,7 @@ public final class VideoDetailFragment
|
||||
NavigationHelper.openChannelFragment(getFM(), currentInfo.getServiceId(),
|
||||
subChannelUrl, subChannelName);
|
||||
} catch (final Exception e) {
|
||||
ErrorActivity.reportUiErrorInSnackbar(this, "Opening channel fragment", e);
|
||||
ErrorUtil.showUiErrorSnackbar(this, "Opening channel fragment", e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -594,6 +598,11 @@ public final class VideoDetailFragment
|
||||
// Init
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public void onViewCreated(@NonNull final View rootView, final Bundle savedInstanceState) {
|
||||
super.onViewCreated(rootView, savedInstanceState);
|
||||
}
|
||||
|
||||
@Override // called from onViewCreated in {@link BaseFragment#onViewCreated}
|
||||
protected void initViews(final View rootView, final Bundle savedInstanceState) {
|
||||
super.initViews(rootView, savedInstanceState);
|
||||
@@ -604,6 +613,18 @@ public final class VideoDetailFragment
|
||||
|
||||
binding.detailThumbnailRootLayout.requestFocus();
|
||||
|
||||
binding.detailControlsPlayWithKodi.setVisibility(
|
||||
KoreUtils.shouldShowPlayWithKodi(requireContext(), serviceId)
|
||||
? View.VISIBLE
|
||||
: View.GONE
|
||||
);
|
||||
binding.detailControlsCrashThePlayer.setVisibility(
|
||||
DEBUG && PreferenceManager.getDefaultSharedPreferences(getContext())
|
||||
.getBoolean(getString(R.string.show_crash_the_player_key), false)
|
||||
? View.VISIBLE
|
||||
: View.GONE
|
||||
);
|
||||
|
||||
if (DeviceUtils.isTv(getContext())) {
|
||||
// remove ripple effects from detail controls
|
||||
final int transparent = ContextCompat.getColor(requireContext(),
|
||||
@@ -638,8 +659,14 @@ public final class VideoDetailFragment
|
||||
binding.detailControlsShare.setOnClickListener(this);
|
||||
binding.detailControlsOpenInBrowser.setOnClickListener(this);
|
||||
binding.detailControlsPlayWithKodi.setOnClickListener(this);
|
||||
binding.detailControlsPlayWithKodi.setVisibility(KoreUtils.shouldShowPlayWithKodi(
|
||||
requireContext(), serviceId) ? View.VISIBLE : View.GONE);
|
||||
if (DEBUG) {
|
||||
binding.detailControlsCrashThePlayer.setOnClickListener(
|
||||
v -> VideoDetailPlayerCrasher.onCrashThePlayer(
|
||||
this.getContext(),
|
||||
this.player,
|
||||
getLayoutInflater())
|
||||
);
|
||||
}
|
||||
|
||||
binding.overlayThumbnail.setOnClickListener(this);
|
||||
binding.overlayThumbnail.setOnLongClickListener(this);
|
||||
@@ -662,7 +689,7 @@ public final class VideoDetailFragment
|
||||
});
|
||||
|
||||
setupBottomPlayer();
|
||||
if (!playerHolder.bound) {
|
||||
if (!playerHolder.isBound()) {
|
||||
setHeightThumbnail();
|
||||
} else {
|
||||
playerHolder.startService(false, this);
|
||||
@@ -1075,6 +1102,11 @@ public final class VideoDetailFragment
|
||||
|
||||
toggleFullscreenIfInFullscreenMode();
|
||||
|
||||
if (isPlayerAvailable()) {
|
||||
// FIXME Workaround #7427
|
||||
player.setRecovery();
|
||||
}
|
||||
|
||||
if (!useExternalAudioPlayer) {
|
||||
openNormalBackgroundPlayer(append);
|
||||
} else {
|
||||
@@ -1091,6 +1123,9 @@ public final class VideoDetailFragment
|
||||
// See UI changes while remote playQueue changes
|
||||
if (!isPlayerAvailable()) {
|
||||
playerHolder.startService(false, this);
|
||||
} else {
|
||||
// FIXME Workaround #7427
|
||||
player.setRecovery();
|
||||
}
|
||||
|
||||
toggleFullscreenIfInFullscreenMode();
|
||||
@@ -1181,7 +1216,7 @@ public final class VideoDetailFragment
|
||||
addVideoPlayerView();
|
||||
|
||||
final Intent playerIntent = NavigationHelper.getPlayerIntent(requireContext(),
|
||||
MainPlayer.class, queue, autoPlayEnabled);
|
||||
MainPlayer.class, queue, true, autoPlayEnabled);
|
||||
ContextCompat.startForegroundService(activity, playerIntent);
|
||||
}
|
||||
|
||||
@@ -1411,7 +1446,7 @@ public final class VideoDetailFragment
|
||||
bottomSheetBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
|
||||
}
|
||||
// Rebound to the service if it was closed via notification or mini player
|
||||
if (!playerHolder.bound) {
|
||||
if (!playerHolder.isBound()) {
|
||||
playerHolder.startService(
|
||||
false, VideoDetailFragment.this);
|
||||
}
|
||||
@@ -1498,6 +1533,8 @@ public final class VideoDetailFragment
|
||||
animate(binding.detailThumbnailPlayButton, true, 200);
|
||||
binding.detailVideoTitleView.setText(title);
|
||||
|
||||
binding.detailSubChannelThumbnailView.setVisibility(View.GONE);
|
||||
|
||||
if (!isEmpty(info.getSubChannelName())) {
|
||||
displayBothUploaderAndSubChannel(info);
|
||||
} else if (!isEmpty(info.getUploaderName())) {
|
||||
@@ -1658,9 +1695,8 @@ public final class VideoDetailFragment
|
||||
|
||||
downloadDialog.show(activity.getSupportFragmentManager(), "downloadDialog");
|
||||
} catch (final Exception e) {
|
||||
ErrorActivity.reportErrorInSnackbar(activity,
|
||||
new ErrorInfo(e, UserAction.DOWNLOAD_OPEN_DIALOG, "Showing download dialog",
|
||||
currentInfo));
|
||||
ErrorUtil.showSnackbar(activity, new ErrorInfo(e, UserAction.DOWNLOAD_OPEN_DIALOG,
|
||||
"Showing download dialog", currentInfo));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2175,12 +2211,20 @@ public final class VideoDetailFragment
|
||||
mainFragment.setDescendantFocusability(afterDescendants);
|
||||
toolbar.setDescendantFocusability(afterDescendants);
|
||||
((ViewGroup) requireView()).setDescendantFocusability(blockDescendants);
|
||||
mainFragment.requestFocus();
|
||||
// Only focus the mainFragment if the mainFragment (e.g. search-results)
|
||||
// or the toolbar (e.g. Textfield for search) don't have focus.
|
||||
// This was done to fix problems with the keyboard input, see also #7490
|
||||
if (!mainFragment.hasFocus() && !toolbar.hasFocus()) {
|
||||
mainFragment.requestFocus();
|
||||
}
|
||||
} else {
|
||||
mainFragment.setDescendantFocusability(blockDescendants);
|
||||
toolbar.setDescendantFocusability(blockDescendants);
|
||||
((ViewGroup) requireView()).setDescendantFocusability(afterDescendants);
|
||||
binding.detailThumbnailRootLayout.requestFocus();
|
||||
// Only focus the player if it not already has focus
|
||||
if (!binding.getRoot().hasFocus()) {
|
||||
binding.detailThumbnailRootLayout.requestFocus();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,159 @@
|
||||
package org.schabi.newpipe.fragments.detail;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.Log;
|
||||
import android.view.ContextThemeWrapper;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.RadioButton;
|
||||
import android.widget.RadioGroup;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.ExoPlaybackException;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.databinding.ListRadioIconItemBinding;
|
||||
import org.schabi.newpipe.databinding.SingleChoiceDialogViewBinding;
|
||||
import org.schabi.newpipe.player.Player;
|
||||
import org.schabi.newpipe.util.ThemeHelper;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
/**
|
||||
* Outsourced logic for crashing the player in the {@link VideoDetailFragment}.
|
||||
*/
|
||||
public final class VideoDetailPlayerCrasher {
|
||||
|
||||
// This has to be <= 23 chars on devices running Android 7 or lower (API <= 25)
|
||||
// or it fails with an IllegalArgumentException
|
||||
// https://stackoverflow.com/a/54744028
|
||||
private static final String TAG = "VideoDetPlayerCrasher";
|
||||
|
||||
private static final Map<String, Supplier<ExoPlaybackException>> AVAILABLE_EXCEPTION_TYPES =
|
||||
getExceptionTypes();
|
||||
|
||||
private VideoDetailPlayerCrasher() {
|
||||
// No impls
|
||||
}
|
||||
|
||||
private static Map<String, Supplier<ExoPlaybackException>> getExceptionTypes() {
|
||||
final String defaultMsg = "Dummy";
|
||||
final Map<String, Supplier<ExoPlaybackException>> exceptionTypes = new LinkedHashMap<>();
|
||||
exceptionTypes.put(
|
||||
"Source",
|
||||
() -> ExoPlaybackException.createForSource(
|
||||
new IOException(defaultMsg)
|
||||
)
|
||||
);
|
||||
exceptionTypes.put(
|
||||
"Renderer",
|
||||
() -> ExoPlaybackException.createForRenderer(
|
||||
new Exception(defaultMsg),
|
||||
"Dummy renderer",
|
||||
0,
|
||||
null,
|
||||
C.FORMAT_HANDLED
|
||||
)
|
||||
);
|
||||
exceptionTypes.put(
|
||||
"Unexpected",
|
||||
() -> ExoPlaybackException.createForUnexpected(
|
||||
new RuntimeException(defaultMsg)
|
||||
)
|
||||
);
|
||||
exceptionTypes.put(
|
||||
"Remote",
|
||||
() -> ExoPlaybackException.createForRemote(defaultMsg)
|
||||
);
|
||||
|
||||
return Collections.unmodifiableMap(exceptionTypes);
|
||||
}
|
||||
|
||||
private static Context getThemeWrapperContext(final Context context) {
|
||||
return new ContextThemeWrapper(
|
||||
context,
|
||||
ThemeHelper.isLightThemeSelected(context)
|
||||
? R.style.LightTheme
|
||||
: R.style.DarkTheme);
|
||||
}
|
||||
|
||||
public static void onCrashThePlayer(
|
||||
@NonNull final Context context,
|
||||
@Nullable final Player player,
|
||||
@NonNull final LayoutInflater layoutInflater
|
||||
) {
|
||||
if (player == null) {
|
||||
Log.d(TAG, "Player is not available");
|
||||
Toast.makeText(context, "Player is not available", Toast.LENGTH_SHORT)
|
||||
.show();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// -- Build the dialog/UI --
|
||||
|
||||
final Context themeWrapperContext = getThemeWrapperContext(context);
|
||||
|
||||
final LayoutInflater inflater = LayoutInflater.from(themeWrapperContext);
|
||||
final RadioGroup radioGroup = SingleChoiceDialogViewBinding.inflate(layoutInflater)
|
||||
.list;
|
||||
|
||||
final AlertDialog alertDialog = new AlertDialog.Builder(getThemeWrapperContext(context))
|
||||
.setTitle("Choose an exception")
|
||||
.setView(radioGroup)
|
||||
.setCancelable(true)
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.create();
|
||||
|
||||
for (final Map.Entry<String, Supplier<ExoPlaybackException>> entry
|
||||
: AVAILABLE_EXCEPTION_TYPES.entrySet()) {
|
||||
final RadioButton radioButton = ListRadioIconItemBinding.inflate(inflater).getRoot();
|
||||
radioButton.setText(entry.getKey());
|
||||
radioButton.setChecked(false);
|
||||
radioButton.setLayoutParams(
|
||||
new RadioGroup.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
)
|
||||
);
|
||||
radioButton.setOnClickListener(v -> {
|
||||
tryCrashPlayerWith(player, entry.getValue().get());
|
||||
if (alertDialog != null) {
|
||||
alertDialog.cancel();
|
||||
}
|
||||
});
|
||||
radioGroup.addView(radioButton);
|
||||
}
|
||||
|
||||
alertDialog.show();
|
||||
}
|
||||
|
||||
/**
|
||||
* Note that this method does not crash the underlying exoplayer directly (it's not possible).
|
||||
* It simply supplies a Exception to {@link Player#onPlayerError(ExoPlaybackException)}.
|
||||
* @param player
|
||||
* @param exception
|
||||
*/
|
||||
private static void tryCrashPlayerWith(
|
||||
@NonNull final Player player,
|
||||
@NonNull final ExoPlaybackException exception
|
||||
) {
|
||||
Log.d(TAG, "Crashing the player using player.onPlayerError(ex)");
|
||||
try {
|
||||
player.onPlayerError(exception);
|
||||
} catch (final Exception exPlayer) {
|
||||
Log.e(TAG,
|
||||
"Run into an exception while crashing the player:",
|
||||
exPlayer);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,11 +17,9 @@ import androidx.appcompat.app.ActionBar;
|
||||
import androidx.preference.PreferenceManager;
|
||||
import androidx.recyclerview.widget.GridLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import androidx.viewbinding.ViewBinding;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.databinding.PignateFooterBinding;
|
||||
import org.schabi.newpipe.error.ErrorActivity;
|
||||
import org.schabi.newpipe.error.ErrorUtil;
|
||||
import org.schabi.newpipe.extractor.InfoItem;
|
||||
import org.schabi.newpipe.extractor.channel.ChannelInfoItem;
|
||||
import org.schabi.newpipe.extractor.comments.CommentsInfoItem;
|
||||
@@ -44,6 +42,7 @@ import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Queue;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty;
|
||||
import static org.schabi.newpipe.ktx.ViewUtils.animate;
|
||||
@@ -79,11 +78,6 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I>
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDetach() {
|
||||
super.onDetach();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(final Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
@@ -143,7 +137,7 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I>
|
||||
final View focusedItem = itemsList.getFocusedChild();
|
||||
final RecyclerView.ViewHolder itemHolder =
|
||||
itemsList.findContainingViewHolder(focusedItem);
|
||||
return itemHolder.getAdapterPosition();
|
||||
return itemHolder.getBindingAdapterPosition();
|
||||
} catch (final NullPointerException e) {
|
||||
return -1;
|
||||
}
|
||||
@@ -220,14 +214,10 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I>
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Nullable
|
||||
protected ViewBinding getListHeader() {
|
||||
protected Supplier<View> getListHeaderSupplier() {
|
||||
return null;
|
||||
}
|
||||
|
||||
protected ViewBinding getListFooter() {
|
||||
return PignateFooterBinding.inflate(activity.getLayoutInflater(), itemsList, false);
|
||||
}
|
||||
|
||||
protected RecyclerView.LayoutManager getListLayoutManager() {
|
||||
return new SuperScrollLayoutManager(activity);
|
||||
}
|
||||
@@ -252,11 +242,10 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I>
|
||||
itemsList.setLayoutManager(useGrid ? getGridLayoutManager() : getListLayoutManager());
|
||||
|
||||
infoListAdapter.setUseGridVariant(useGrid);
|
||||
infoListAdapter.setFooter(getListFooter().getRoot());
|
||||
|
||||
final ViewBinding listHeader = getListHeader();
|
||||
if (listHeader != null) {
|
||||
infoListAdapter.setHeader(listHeader.getRoot());
|
||||
final Supplier<View> listHeaderSupplier = getListHeaderSupplier();
|
||||
if (listHeaderSupplier != null) {
|
||||
infoListAdapter.setHeaderSupplier(listHeaderSupplier);
|
||||
}
|
||||
|
||||
itemsList.setAdapter(infoListAdapter);
|
||||
@@ -271,7 +260,7 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I>
|
||||
@Override
|
||||
protected void initListeners() {
|
||||
super.initListeners();
|
||||
infoListAdapter.setOnStreamSelectedListener(new OnClickGesture<StreamInfoItem>() {
|
||||
infoListAdapter.setOnStreamSelectedListener(new OnClickGesture<>() {
|
||||
@Override
|
||||
public void selected(final StreamInfoItem selectedItem) {
|
||||
onStreamSelected(selectedItem);
|
||||
@@ -293,7 +282,7 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I>
|
||||
selectedItem.getUrl(),
|
||||
selectedItem.getName());
|
||||
} catch (final Exception e) {
|
||||
ErrorActivity.reportUiErrorInSnackbar(
|
||||
ErrorUtil.showUiErrorSnackbar(
|
||||
BaseListFragment.this, "Opening channel fragment", e);
|
||||
}
|
||||
}
|
||||
@@ -309,28 +298,104 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I>
|
||||
selectedItem.getUrl(),
|
||||
selectedItem.getName());
|
||||
} catch (final Exception e) {
|
||||
ErrorActivity.reportUiErrorInSnackbar(BaseListFragment.this,
|
||||
ErrorUtil.showUiErrorSnackbar(BaseListFragment.this,
|
||||
"Opening playlist fragment", e);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
infoListAdapter.setOnCommentsSelectedListener(new OnClickGesture<CommentsInfoItem>() {
|
||||
infoListAdapter.setOnCommentsSelectedListener(new OnClickGesture<>() {
|
||||
@Override
|
||||
public void selected(final CommentsInfoItem selectedItem) {
|
||||
onItemSelected(selectedItem);
|
||||
}
|
||||
});
|
||||
|
||||
// Ensure that there is always a scroll listener (e.g. when rotating the device)
|
||||
useNormalItemListScrollListener();
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all listeners and adds the normal scroll listener to the {@link #itemsList}.
|
||||
*/
|
||||
protected void useNormalItemListScrollListener() {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "useNormalItemListScrollListener called");
|
||||
}
|
||||
itemsList.clearOnScrollListeners();
|
||||
itemsList.addOnScrollListener(new OnScrollBelowItemsListener() {
|
||||
itemsList.addOnScrollListener(new DefaultItemListOnScrolledDownListener());
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all listeners and adds the initial scroll listener to the {@link #itemsList}.
|
||||
* <br/>
|
||||
* Which tries to load more items when not enough are in the view (not scrollable)
|
||||
* and more are available.
|
||||
* <br/>
|
||||
* Note: This method only works because "This callback will also be called if visible
|
||||
* item range changes after a layout calculation. In that case, dx and dy will be 0."
|
||||
* - which might be unexpected because no actual scrolling occurs...
|
||||
* <br/>
|
||||
* This listener will be replaced by DefaultItemListOnScrolledDownListener when
|
||||
* <ul>
|
||||
* <li>the view was actually scrolled</li>
|
||||
* <li>the view is scrollable</li>
|
||||
* <li>no more items can be loaded</li>
|
||||
* </ul>
|
||||
*/
|
||||
protected void useInitialItemListLoadScrollListener() {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "useInitialItemListLoadScrollListener called");
|
||||
}
|
||||
itemsList.clearOnScrollListeners();
|
||||
itemsList.addOnScrollListener(new DefaultItemListOnScrolledDownListener() {
|
||||
@Override
|
||||
public void onScrolledDown(final RecyclerView recyclerView) {
|
||||
onScrollToBottom();
|
||||
public void onScrolled(final RecyclerView recyclerView, final int dx, final int dy) {
|
||||
super.onScrolled(recyclerView, dx, dy);
|
||||
|
||||
if (dy != 0) {
|
||||
log("Vertical scroll occurred");
|
||||
|
||||
useNormalItemListScrollListener();
|
||||
return;
|
||||
}
|
||||
if (isLoading.get()) {
|
||||
log("Still loading data -> Skipping");
|
||||
return;
|
||||
}
|
||||
if (!hasMoreItems()) {
|
||||
log("No more items to load");
|
||||
|
||||
useNormalItemListScrollListener();
|
||||
return;
|
||||
}
|
||||
if (itemsList.canScrollVertically(1)
|
||||
|| itemsList.canScrollVertically(-1)) {
|
||||
log("View is scrollable");
|
||||
|
||||
useNormalItemListScrollListener();
|
||||
return;
|
||||
}
|
||||
|
||||
log("Loading more data");
|
||||
loadMoreItems();
|
||||
}
|
||||
|
||||
private void log(final String msg) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "initItemListLoadScrollListener - " + msg);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
class DefaultItemListOnScrolledDownListener extends OnScrollBelowItemsListener {
|
||||
@Override
|
||||
public void onScrolledDown(final RecyclerView recyclerView) {
|
||||
onScrollToBottom();
|
||||
}
|
||||
}
|
||||
|
||||
private void onStreamSelected(final StreamInfoItem selectedItem) {
|
||||
onItemSelected(selectedItem);
|
||||
NavigationHelper.openVideoDetailFragment(requireContext(), getFM(),
|
||||
@@ -352,7 +417,7 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I>
|
||||
}
|
||||
final List<StreamDialogEntry> entries = new ArrayList<>();
|
||||
|
||||
if (PlayerHolder.getInstance().isPlayerOpen()) {
|
||||
if (PlayerHolder.getInstance().isPlayQueueReady()) {
|
||||
entries.add(StreamDialogEntry.enqueue);
|
||||
|
||||
if (PlayerHolder.getInstance().getQueueSize() > 1) {
|
||||
@@ -378,6 +443,13 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I>
|
||||
if (KoreUtils.shouldShowPlayWithKodi(context, item.getServiceId())) {
|
||||
entries.add(StreamDialogEntry.play_with_kodi);
|
||||
}
|
||||
|
||||
// show "mark as watched" only when watch history is enabled
|
||||
if (StreamDialogEntry.shouldAddMarkAsWatched(item.getStreamType(), context)) {
|
||||
entries.add(
|
||||
StreamDialogEntry.mark_as_watched
|
||||
);
|
||||
}
|
||||
if (!isNullOrEmpty(item.getUploaderUrl())) {
|
||||
entries.add(StreamDialogEntry.show_channel_details);
|
||||
}
|
||||
@@ -411,6 +483,12 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I>
|
||||
// Load and handle
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
protected void startLoading(final boolean forceLoad) {
|
||||
useInitialItemListLoadScrollListener();
|
||||
super.startLoading(forceLoad);
|
||||
}
|
||||
|
||||
protected abstract void loadMoreItems();
|
||||
|
||||
protected abstract boolean hasMoreItems();
|
||||
|
||||
@@ -65,7 +65,7 @@ public abstract class BaseListInfoFragment<I extends ListInfo>
|
||||
super.onResume();
|
||||
// Check if it was loading when the fragment was stopped/paused,
|
||||
if (wasLoading.getAndSet(false)) {
|
||||
if (hasMoreItems() && infoListAdapter.getItemsList().size() > 0) {
|
||||
if (hasMoreItems() && !infoListAdapter.getItemsList().isEmpty()) {
|
||||
loadMoreItems();
|
||||
} else {
|
||||
doInitialLoadLogic();
|
||||
@@ -105,6 +105,7 @@ public abstract class BaseListInfoFragment<I extends ListInfo>
|
||||
// Load and handle
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
protected void doInitialLoadLogic() {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "doInitialLoadLogic() called");
|
||||
@@ -158,6 +159,7 @@ public abstract class BaseListInfoFragment<I extends ListInfo>
|
||||
*/
|
||||
protected abstract Single<ListExtractor.InfoItemsPage> loadMoreItemsLogic();
|
||||
|
||||
@Override
|
||||
protected void loadMoreItems() {
|
||||
isLoading.set(true);
|
||||
|
||||
@@ -171,9 +173,9 @@ public abstract class BaseListInfoFragment<I extends ListInfo>
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.doFinally(this::allowDownwardFocusScroll)
|
||||
.subscribe((@NonNull ListExtractor.InfoItemsPage InfoItemsPage) -> {
|
||||
.subscribe(infoItemsPage -> {
|
||||
isLoading.set(false);
|
||||
handleNextItems(InfoItemsPage);
|
||||
handleNextItems(infoItemsPage);
|
||||
}, (@NonNull Throwable throwable) ->
|
||||
dynamicallyShowErrorPanelOrSnackbar(new ErrorInfo(throwable,
|
||||
errorUserAction, "Loading more items: " + url, serviceId)));
|
||||
@@ -223,7 +225,7 @@ public abstract class BaseListInfoFragment<I extends ListInfo>
|
||||
setTitle(name);
|
||||
|
||||
if (infoListAdapter.getItemsList().isEmpty()) {
|
||||
if (result.getRelatedItems().size() > 0) {
|
||||
if (!result.getRelatedItems().isEmpty()) {
|
||||
infoListAdapter.addInfoItemList(result.getRelatedItems());
|
||||
showListFooter(hasMoreItems());
|
||||
} else {
|
||||
@@ -240,7 +242,7 @@ public abstract class BaseListInfoFragment<I extends ListInfo>
|
||||
final List<Throwable> errors = new ArrayList<>(result.getErrors());
|
||||
// handling ContentNotSupportedException not to show the error but an appropriate string
|
||||
// so that crashes won't be sent uselessly and the user will understand what happened
|
||||
errors.removeIf(throwable -> throwable instanceof ContentNotSupportedException);
|
||||
errors.removeIf(ContentNotSupportedException.class::isInstance);
|
||||
|
||||
if (!errors.isEmpty()) {
|
||||
dynamicallyShowErrorPanelOrSnackbar(new ErrorInfo(result.getErrors(),
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
package org.schabi.newpipe.fragments.list.channel;
|
||||
|
||||
import static org.schabi.newpipe.ktx.TextViewUtils.animateTextColor;
|
||||
import static org.schabi.newpipe.ktx.ViewUtils.animate;
|
||||
import static org.schabi.newpipe.ktx.ViewUtils.animateBackgroundColor;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
import android.text.TextUtils;
|
||||
@@ -17,7 +21,6 @@ import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.viewbinding.ViewBinding;
|
||||
|
||||
import com.jakewharton.rxbinding4.view.RxView;
|
||||
|
||||
@@ -26,10 +29,9 @@ import org.schabi.newpipe.database.subscription.SubscriptionEntity;
|
||||
import org.schabi.newpipe.databinding.ChannelHeaderBinding;
|
||||
import org.schabi.newpipe.databinding.FragmentChannelBinding;
|
||||
import org.schabi.newpipe.databinding.PlaylistControlBinding;
|
||||
import org.schabi.newpipe.error.ErrorActivity;
|
||||
import org.schabi.newpipe.error.ErrorInfo;
|
||||
import org.schabi.newpipe.error.ErrorUtil;
|
||||
import org.schabi.newpipe.error.UserAction;
|
||||
import org.schabi.newpipe.extractor.InfoItem;
|
||||
import org.schabi.newpipe.extractor.ListExtractor;
|
||||
import org.schabi.newpipe.extractor.channel.ChannelInfo;
|
||||
import org.schabi.newpipe.extractor.exceptions.ContentNotSupportedException;
|
||||
@@ -43,13 +45,14 @@ import org.schabi.newpipe.player.playqueue.PlayQueue;
|
||||
import org.schabi.newpipe.util.ExtractorHelper;
|
||||
import org.schabi.newpipe.util.Localization;
|
||||
import org.schabi.newpipe.util.NavigationHelper;
|
||||
import org.schabi.newpipe.util.external_communication.ShareUtils;
|
||||
import org.schabi.newpipe.util.PicassoHelper;
|
||||
import org.schabi.newpipe.util.ThemeHelper;
|
||||
import org.schabi.newpipe.util.external_communication.ShareUtils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.rxjava3.core.Observable;
|
||||
@@ -61,10 +64,6 @@ import io.reactivex.rxjava3.functions.Consumer;
|
||||
import io.reactivex.rxjava3.functions.Function;
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers;
|
||||
|
||||
import static org.schabi.newpipe.ktx.TextViewUtils.animateTextColor;
|
||||
import static org.schabi.newpipe.ktx.ViewUtils.animate;
|
||||
import static org.schabi.newpipe.ktx.ViewUtils.animateBackgroundColor;
|
||||
|
||||
public class ChannelFragment extends BaseListInfoFragment<ChannelInfo>
|
||||
implements View.OnClickListener {
|
||||
|
||||
@@ -98,11 +97,9 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo>
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setUserVisibleHint(final boolean isVisibleToUser) {
|
||||
super.setUserVisibleHint(isVisibleToUser);
|
||||
if (activity != null
|
||||
&& useAsFrontPage
|
||||
&& isVisibleToUser) {
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
if (activity != null && useAsFrontPage) {
|
||||
setTitle(currentInfo != null ? currentInfo.getName() : name);
|
||||
}
|
||||
}
|
||||
@@ -147,12 +144,12 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo>
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
protected ViewBinding getListHeader() {
|
||||
protected Supplier<View> getListHeaderSupplier() {
|
||||
headerBinding = ChannelHeaderBinding
|
||||
.inflate(activity.getLayoutInflater(), itemsList, false);
|
||||
playlistControlBinding = headerBinding.playlistControl;
|
||||
|
||||
return headerBinding;
|
||||
return headerBinding::getRoot;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -185,13 +182,6 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo>
|
||||
}
|
||||
}
|
||||
|
||||
private void openRssFeed() {
|
||||
final ChannelInfo info = currentInfo;
|
||||
if (info != null) {
|
||||
ShareUtils.openUrlInBrowser(requireContext(), info.getFeedUrl(), false);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(final MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
@@ -199,7 +189,10 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo>
|
||||
NavigationHelper.openSettings(requireContext());
|
||||
break;
|
||||
case R.id.menu_item_rss:
|
||||
openRssFeed();
|
||||
if (currentInfo != null) {
|
||||
ShareUtils.openUrlInBrowser(
|
||||
requireContext(), currentInfo.getFeedUrl(), false);
|
||||
}
|
||||
break;
|
||||
case R.id.menu_item_openInBrowser:
|
||||
if (currentInfo != null) {
|
||||
@@ -409,7 +402,7 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo>
|
||||
currentInfo.getParentChannelUrl(),
|
||||
currentInfo.getParentChannelName());
|
||||
} catch (final Exception e) {
|
||||
ErrorActivity.reportUiErrorInSnackbar(this, "Opening channel fragment", e);
|
||||
ErrorUtil.showUiErrorSnackbar(this, "Opening channel fragment", e);
|
||||
}
|
||||
} else if (DEBUG) {
|
||||
Log.i(TAG, "Can't open parent channel because we got no channel URL");
|
||||
@@ -518,12 +511,11 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo>
|
||||
}
|
||||
|
||||
private PlayQueue getPlayQueue(final int index) {
|
||||
final List<StreamInfoItem> streamItems = new ArrayList<>();
|
||||
for (final InfoItem i : infoListAdapter.getItemsList()) {
|
||||
if (i instanceof StreamInfoItem) {
|
||||
streamItems.add((StreamInfoItem) i);
|
||||
}
|
||||
}
|
||||
final List<StreamInfoItem> streamItems = infoListAdapter.getItemsList().stream()
|
||||
.filter(StreamInfoItem.class::isInstance)
|
||||
.map(StreamInfoItem.class::cast)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
return new ChannelPlayQueue(currentInfo.getServiceId(), currentInfo.getUrl(),
|
||||
currentInfo.getNextPage(), streamItems, index);
|
||||
}
|
||||
|
||||
@@ -99,9 +99,12 @@ public class KioskFragment extends BaseListInfoFragment<KioskInfo> {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setUserVisibleHint(final boolean isVisibleToUser) {
|
||||
super.setUserVisibleHint(isVisibleToUser);
|
||||
if (useAsFrontPage && isVisibleToUser && activity != null) {
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
if (!Localization.getPreferredContentCountry(requireContext()).equals(contentCountry)) {
|
||||
reloadContent();
|
||||
}
|
||||
if (useAsFrontPage && activity != null) {
|
||||
try {
|
||||
setTitle(kioskTranslatedName);
|
||||
} catch (final Exception e) {
|
||||
@@ -117,15 +120,6 @@ public class KioskFragment extends BaseListInfoFragment<KioskInfo> {
|
||||
return inflater.inflate(R.layout.fragment_kiosk, container, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
|
||||
if (!Localization.getPreferredContentCountry(requireContext()).equals(contentCountry)) {
|
||||
reloadContent();
|
||||
}
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Menu
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
package org.schabi.newpipe.fragments.list.playlist;
|
||||
|
||||
import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty;
|
||||
import static org.schabi.newpipe.ktx.ViewUtils.animate;
|
||||
import static org.schabi.newpipe.ktx.ViewUtils.animateHideRecyclerViewAllowingScrolling;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
@@ -15,7 +19,6 @@ import android.view.ViewGroup;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.content.res.AppCompatResources;
|
||||
import androidx.viewbinding.ViewBinding;
|
||||
|
||||
import org.reactivestreams.Subscriber;
|
||||
import org.reactivestreams.Subscription;
|
||||
@@ -24,8 +27,8 @@ import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity;
|
||||
import org.schabi.newpipe.databinding.PlaylistControlBinding;
|
||||
import org.schabi.newpipe.databinding.PlaylistHeaderBinding;
|
||||
import org.schabi.newpipe.error.ErrorActivity;
|
||||
import org.schabi.newpipe.error.ErrorInfo;
|
||||
import org.schabi.newpipe.error.ErrorUtil;
|
||||
import org.schabi.newpipe.error.UserAction;
|
||||
import org.schabi.newpipe.extractor.InfoItem;
|
||||
import org.schabi.newpipe.extractor.ListExtractor;
|
||||
@@ -42,17 +45,18 @@ import org.schabi.newpipe.player.helper.PlayerHolder;
|
||||
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
||||
import org.schabi.newpipe.player.playqueue.PlaylistPlayQueue;
|
||||
import org.schabi.newpipe.util.ExtractorHelper;
|
||||
import org.schabi.newpipe.util.PicassoHelper;
|
||||
import org.schabi.newpipe.util.external_communication.KoreUtils;
|
||||
import org.schabi.newpipe.util.Localization;
|
||||
import org.schabi.newpipe.util.NavigationHelper;
|
||||
import org.schabi.newpipe.util.external_communication.ShareUtils;
|
||||
import org.schabi.newpipe.util.PicassoHelper;
|
||||
import org.schabi.newpipe.util.StreamDialogEntry;
|
||||
import org.schabi.newpipe.util.external_communication.KoreUtils;
|
||||
import org.schabi.newpipe.util.external_communication.ShareUtils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.rxjava3.core.Flowable;
|
||||
@@ -60,10 +64,6 @@ import io.reactivex.rxjava3.core.Single;
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
||||
import io.reactivex.rxjava3.disposables.Disposable;
|
||||
|
||||
import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty;
|
||||
import static org.schabi.newpipe.ktx.ViewUtils.animate;
|
||||
import static org.schabi.newpipe.ktx.ViewUtils.animateHideRecyclerViewAllowingScrolling;
|
||||
|
||||
public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
|
||||
|
||||
private static final String PICASSO_PLAYLIST_TAG = "PICASSO_PLAYLIST_TAG";
|
||||
@@ -120,12 +120,12 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
protected ViewBinding getListHeader() {
|
||||
protected Supplier<View> getListHeaderSupplier() {
|
||||
headerBinding = PlaylistHeaderBinding
|
||||
.inflate(activity.getLayoutInflater(), itemsList, false);
|
||||
playlistControlBinding = headerBinding.playlistControl;
|
||||
|
||||
return headerBinding;
|
||||
return headerBinding::getRoot;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -149,7 +149,7 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
|
||||
|
||||
final ArrayList<StreamDialogEntry> entries = new ArrayList<>();
|
||||
|
||||
if (PlayerHolder.getInstance().isPlayerOpen()) {
|
||||
if (PlayerHolder.getInstance().isPlayQueueReady()) {
|
||||
entries.add(StreamDialogEntry.enqueue);
|
||||
|
||||
if (PlayerHolder.getInstance().getQueueSize() > 1) {
|
||||
@@ -176,6 +176,12 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
|
||||
entries.add(StreamDialogEntry.play_with_kodi);
|
||||
}
|
||||
|
||||
// show "mark as watched" only when watch history is enabled
|
||||
if (StreamDialogEntry.shouldAddMarkAsWatched(item.getStreamType(), context)) {
|
||||
entries.add(
|
||||
StreamDialogEntry.mark_as_watched
|
||||
);
|
||||
}
|
||||
if (!isNullOrEmpty(item.getUploaderUrl())) {
|
||||
entries.add(StreamDialogEntry.show_channel_details);
|
||||
}
|
||||
@@ -262,7 +268,10 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
|
||||
ShareUtils.openUrlInBrowser(requireContext(), url);
|
||||
break;
|
||||
case R.id.menu_item_share:
|
||||
ShareUtils.shareText(requireContext(), name, url, currentInfo.getThumbnailUrl());
|
||||
if (currentInfo != null) {
|
||||
ShareUtils.shareText(requireContext(), name, url,
|
||||
currentInfo.getThumbnailUrl());
|
||||
}
|
||||
break;
|
||||
case R.id.menu_item_bookmark:
|
||||
onBookmarkClicked();
|
||||
@@ -304,7 +313,7 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
|
||||
NavigationHelper.openChannelFragment(getFM(), result.getServiceId(),
|
||||
result.getUploaderUrl(), result.getUploaderName());
|
||||
} catch (final Exception e) {
|
||||
ErrorActivity.reportUiErrorInSnackbar(this, "Opening channel fragment", e);
|
||||
ErrorUtil.showUiErrorSnackbar(this, "Opening channel fragment", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -404,7 +413,7 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
|
||||
}
|
||||
|
||||
private Subscriber<List<PlaylistRemoteEntity>> getPlaylistBookmarkSubscriber() {
|
||||
return new Subscriber<List<PlaylistRemoteEntity>>() {
|
||||
return new Subscriber<>() {
|
||||
@Override
|
||||
public void onSubscribe(final Subscription s) {
|
||||
if (bookmarkReactor != null) {
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
package org.schabi.newpipe.fragments.list.search;
|
||||
|
||||
import static androidx.recyclerview.widget.ItemTouchHelper.Callback.makeMovementFlags;
|
||||
import static org.schabi.newpipe.ktx.ViewUtils.animate;
|
||||
import static org.schabi.newpipe.util.ExtractorHelper.showMetaInfoInTextView;
|
||||
import static java.util.Arrays.asList;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
@@ -20,7 +25,6 @@ import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.animation.DecelerateInterpolator;
|
||||
import android.view.inputmethod.EditorInfo;
|
||||
import android.view.inputmethod.InputMethodManager;
|
||||
import android.widget.EditText;
|
||||
import android.widget.TextView;
|
||||
|
||||
@@ -29,17 +33,15 @@ import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.appcompat.widget.TooltipCompat;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.core.text.HtmlCompat;
|
||||
import androidx.preference.PreferenceManager;
|
||||
import androidx.recyclerview.widget.ItemTouchHelper;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.database.history.model.SearchHistoryEntry;
|
||||
import org.schabi.newpipe.databinding.FragmentSearchBinding;
|
||||
import org.schabi.newpipe.error.ErrorActivity;
|
||||
import org.schabi.newpipe.error.ErrorInfo;
|
||||
import org.schabi.newpipe.error.ErrorUtil;
|
||||
import org.schabi.newpipe.error.ReCaptchaActivity;
|
||||
import org.schabi.newpipe.error.UserAction;
|
||||
import org.schabi.newpipe.extractor.InfoItem;
|
||||
@@ -61,6 +63,7 @@ import org.schabi.newpipe.settings.NewPipeSettings;
|
||||
import org.schabi.newpipe.util.Constants;
|
||||
import org.schabi.newpipe.util.DeviceUtils;
|
||||
import org.schabi.newpipe.util.ExtractorHelper;
|
||||
import org.schabi.newpipe.util.KeyboardUtil;
|
||||
import org.schabi.newpipe.util.NavigationHelper;
|
||||
import org.schabi.newpipe.util.ServiceHelper;
|
||||
|
||||
@@ -68,12 +71,11 @@ import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Queue;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import icepick.State;
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||
@@ -84,11 +86,6 @@ import io.reactivex.rxjava3.disposables.Disposable;
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers;
|
||||
import io.reactivex.rxjava3.subjects.PublishSubject;
|
||||
|
||||
import static androidx.recyclerview.widget.ItemTouchHelper.Callback.makeMovementFlags;
|
||||
import static java.util.Arrays.asList;
|
||||
import static org.schabi.newpipe.ktx.ViewUtils.animate;
|
||||
import static org.schabi.newpipe.util.ExtractorHelper.showMetaInfoInTextView;
|
||||
|
||||
public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.InfoItemsPage<?>>
|
||||
implements BackPressable {
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
@@ -225,8 +222,7 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
||||
try {
|
||||
service = NewPipe.getService(serviceId);
|
||||
} catch (final Exception e) {
|
||||
ErrorActivity.reportUiErrorInSnackbar(this,
|
||||
"Getting service for id " + serviceId, e);
|
||||
ErrorUtil.showUiErrorSnackbar(this, "Getting service for id " + serviceId, e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -673,31 +669,15 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "showKeyboardSearch() called");
|
||||
}
|
||||
if (searchEditText == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (searchEditText.requestFocus()) {
|
||||
final InputMethodManager imm = ContextCompat.getSystemService(activity,
|
||||
InputMethodManager.class);
|
||||
imm.showSoftInput(searchEditText, InputMethodManager.SHOW_FORCED);
|
||||
}
|
||||
KeyboardUtil.showKeyboard(activity, searchEditText);
|
||||
}
|
||||
|
||||
private void hideKeyboardSearch() {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "hideKeyboardSearch() called");
|
||||
}
|
||||
if (searchEditText == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final InputMethodManager imm = ContextCompat.getSystemService(activity,
|
||||
InputMethodManager.class);
|
||||
imm.hideSoftInputFromWindow(searchEditText.getWindowToken(),
|
||||
InputMethodManager.RESULT_UNCHANGED_SHOWN);
|
||||
|
||||
searchEditText.clearFocus();
|
||||
KeyboardUtil.hideKeyboard(activity, searchEditText);
|
||||
}
|
||||
|
||||
private void showDeleteSuggestionDialog(final SuggestionItem item) {
|
||||
@@ -727,7 +707,7 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
||||
@Override
|
||||
public boolean onBackPressed() {
|
||||
if (suggestionsPanelVisible
|
||||
&& infoListAdapter.getItemsList().size() > 0
|
||||
&& !infoListAdapter.getItemsList().isEmpty()
|
||||
&& !isLoading.get()) {
|
||||
hideSuggestionsPanel();
|
||||
hideKeyboardSearch();
|
||||
@@ -743,13 +723,10 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
||||
return historyRecordManager
|
||||
.getRelatedSearches(query, similarQueryLimit, 25)
|
||||
.toObservable()
|
||||
.map(searchHistoryEntries -> {
|
||||
final Set<SuggestionItem> result = new HashSet<>(); // remove duplicates
|
||||
for (final SearchHistoryEntry entry : searchHistoryEntries) {
|
||||
result.add(new SuggestionItem(true, entry.getSearch()));
|
||||
}
|
||||
return new ArrayList<>(result);
|
||||
});
|
||||
.map(searchHistoryEntries ->
|
||||
searchHistoryEntries.stream()
|
||||
.map(entry -> new SuggestionItem(true, entry))
|
||||
.collect(Collectors.toList()));
|
||||
}
|
||||
|
||||
private Observable<List<SuggestionItem>> getRemoteSuggestionsObservable(final String query) {
|
||||
@@ -1088,7 +1065,7 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
public int getSuggestionMovementFlags(@NonNull final RecyclerView.ViewHolder viewHolder) {
|
||||
final int position = viewHolder.getAdapterPosition();
|
||||
final int position = viewHolder.getBindingAdapterPosition();
|
||||
if (position == RecyclerView.NO_POSITION) {
|
||||
return 0;
|
||||
}
|
||||
@@ -1099,7 +1076,7 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
||||
}
|
||||
|
||||
public void onSuggestionItemSwiped(@NonNull final RecyclerView.ViewHolder viewHolder) {
|
||||
final int position = viewHolder.getAdapterPosition();
|
||||
final int position = viewHolder.getBindingAdapterPosition();
|
||||
final String query = suggestionListAdapter.getItem(position).query;
|
||||
final Disposable onDelete = historyRecordManager.deleteSearchHistory(query)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package org.schabi.newpipe.fragments.list.videos;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
@@ -12,7 +11,6 @@ import android.view.ViewGroup;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.preference.PreferenceManager;
|
||||
import androidx.viewbinding.ViewBinding;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.databinding.RelatedItemsHeaderBinding;
|
||||
@@ -24,14 +22,14 @@ import org.schabi.newpipe.ktx.ViewUtils;
|
||||
import org.schabi.newpipe.util.RelatedItemInfo;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import io.reactivex.rxjava3.core.Single;
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
||||
|
||||
public class RelatedItemsFragment extends BaseListInfoFragment<RelatedItemInfo>
|
||||
implements SharedPreferences.OnSharedPreferenceChangeListener {
|
||||
private static final String INFO_KEY = "related_info_key";
|
||||
private final CompositeDisposable disposables = new CompositeDisposable();
|
||||
|
||||
private RelatedItemInfo relatedItemInfo;
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
@@ -54,11 +52,6 @@ public class RelatedItemsFragment extends BaseListInfoFragment<RelatedItemInfo>
|
||||
// LifeCycle
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public void onAttach(@NonNull final Context context) {
|
||||
super.onAttach(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(@NonNull final LayoutInflater inflater,
|
||||
@Nullable final ViewGroup container,
|
||||
@@ -66,12 +59,6 @@ public class RelatedItemsFragment extends BaseListInfoFragment<RelatedItemInfo>
|
||||
return inflater.inflate(R.layout.fragment_related_items, container, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
disposables.clear();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyView() {
|
||||
headerBinding = null;
|
||||
@@ -79,22 +66,23 @@ public class RelatedItemsFragment extends BaseListInfoFragment<RelatedItemInfo>
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ViewBinding getListHeader() {
|
||||
if (relatedItemInfo != null && relatedItemInfo.getRelatedItems() != 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;
|
||||
} else {
|
||||
protected Supplier<View> getListHeaderSupplier() {
|
||||
if (relatedItemInfo == null || relatedItemInfo.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
|
||||
@@ -128,7 +116,6 @@ public class RelatedItemsFragment extends BaseListInfoFragment<RelatedItemInfo>
|
||||
}
|
||||
ViewUtils.slideUp(requireView(), 120, 96, 0.06f);
|
||||
|
||||
disposables.clear();
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
@@ -137,11 +124,13 @@ public class RelatedItemsFragment extends BaseListInfoFragment<RelatedItemInfo>
|
||||
|
||||
@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) {
|
||||
@@ -169,11 +158,10 @@ public class RelatedItemsFragment extends BaseListInfoFragment<RelatedItemInfo>
|
||||
@Override
|
||||
public void onSharedPreferenceChanged(final SharedPreferences sharedPreferences,
|
||||
final String s) {
|
||||
final SharedPreferences pref =
|
||||
PreferenceManager.getDefaultSharedPreferences(requireContext());
|
||||
final boolean autoplay = pref.getBoolean(getString(R.string.auto_queue_key), false);
|
||||
if (headerBinding != null) {
|
||||
headerBinding.autoplaySwitch.setChecked(autoplay);
|
||||
headerBinding.autoplaySwitch.setChecked(
|
||||
sharedPreferences.getBoolean(
|
||||
getString(R.string.auto_queue_key), false));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ package org.schabi.newpipe.info_list;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
@@ -10,7 +11,7 @@ import androidx.annotation.Nullable;
|
||||
import androidx.recyclerview.widget.GridLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import org.schabi.newpipe.database.stream.model.StreamStateEntity;
|
||||
import org.schabi.newpipe.databinding.PignateFooterBinding;
|
||||
import org.schabi.newpipe.extractor.InfoItem;
|
||||
import org.schabi.newpipe.extractor.channel.ChannelInfoItem;
|
||||
import org.schabi.newpipe.extractor.comments.CommentsInfoItem;
|
||||
@@ -34,6 +35,7 @@ import org.schabi.newpipe.util.OnClickGesture;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
/*
|
||||
* Created by Christian Schabesberger on 01.08.16.
|
||||
@@ -74,18 +76,20 @@ public class InfoListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolde
|
||||
private static final int MINI_COMMENT_HOLDER_TYPE = 0x400;
|
||||
private static final int COMMENT_HOLDER_TYPE = 0x401;
|
||||
|
||||
private final LayoutInflater layoutInflater;
|
||||
private final InfoItemBuilder infoItemBuilder;
|
||||
private final ArrayList<InfoItem> infoItemList;
|
||||
private final List<InfoItem> infoItemList;
|
||||
private final HistoryRecordManager recordManager;
|
||||
|
||||
private boolean useMiniVariant = false;
|
||||
private boolean useGridVariant = false;
|
||||
private boolean showFooter = false;
|
||||
private View header = null;
|
||||
private View footer = null;
|
||||
|
||||
private Supplier<View> headerSupplier = null;
|
||||
|
||||
public InfoListAdapter(final Context context) {
|
||||
this.recordManager = new HistoryRecordManager(context);
|
||||
layoutInflater = LayoutInflater.from(context);
|
||||
recordManager = new HistoryRecordManager(context);
|
||||
infoItemBuilder = new InfoItemBuilder(context);
|
||||
infoItemList = new ArrayList<>();
|
||||
}
|
||||
@@ -129,12 +133,12 @@ public class InfoListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolde
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "addInfoItemList() after > offsetStart = " + offsetStart + ", "
|
||||
+ "infoItemList.size() = " + infoItemList.size() + ", "
|
||||
+ "header = " + header + ", footer = " + footer + ", "
|
||||
+ "hasHeader = " + hasHeader() + ", "
|
||||
+ "showFooter = " + showFooter);
|
||||
}
|
||||
notifyItemRangeInserted(offsetStart, data.size());
|
||||
|
||||
if (footer != null && showFooter) {
|
||||
if (showFooter) {
|
||||
final int footerNow = sizeConsideringHeaderOffset();
|
||||
notifyItemMoved(offsetStart, footerNow);
|
||||
|
||||
@@ -145,43 +149,6 @@ public class InfoListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolde
|
||||
}
|
||||
}
|
||||
|
||||
public void setInfoItemList(final List<? extends InfoItem> data) {
|
||||
infoItemList.clear();
|
||||
infoItemList.addAll(data);
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
public void addInfoItem(@Nullable final InfoItem data) {
|
||||
if (data == null) {
|
||||
return;
|
||||
}
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "addInfoItem() before > infoItemList.size() = "
|
||||
+ infoItemList.size() + ", thread = " + Thread.currentThread());
|
||||
}
|
||||
|
||||
final int positionInserted = sizeConsideringHeaderOffset();
|
||||
infoItemList.add(data);
|
||||
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "addInfoItem() after > position = " + positionInserted + ", "
|
||||
+ "infoItemList.size() = " + infoItemList.size() + ", "
|
||||
+ "header = " + header + ", footer = " + footer + ", "
|
||||
+ "showFooter = " + showFooter);
|
||||
}
|
||||
notifyItemInserted(positionInserted);
|
||||
|
||||
if (footer != null && showFooter) {
|
||||
final int footerNow = sizeConsideringHeaderOffset();
|
||||
notifyItemMoved(positionInserted, footerNow);
|
||||
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "addInfoItem() footer from " + positionInserted
|
||||
+ " to " + footerNow);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void clearStreamItemList() {
|
||||
if (infoItemList.isEmpty()) {
|
||||
return;
|
||||
@@ -190,16 +157,16 @@ public class InfoListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolde
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
public void setHeader(final View header) {
|
||||
final boolean changed = header != this.header;
|
||||
this.header = header;
|
||||
public void setHeaderSupplier(@Nullable final Supplier<View> headerSupplier) {
|
||||
final boolean changed = headerSupplier != this.headerSupplier;
|
||||
this.headerSupplier = headerSupplier;
|
||||
if (changed) {
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public void setFooter(final View view) {
|
||||
this.footer = view;
|
||||
protected boolean hasHeader() {
|
||||
return this.headerSupplier != null;
|
||||
}
|
||||
|
||||
public void showFooter(final boolean show) {
|
||||
@@ -219,48 +186,49 @@ public class InfoListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolde
|
||||
}
|
||||
|
||||
private int sizeConsideringHeaderOffset() {
|
||||
final int i = infoItemList.size() + (header != null ? 1 : 0);
|
||||
final int i = infoItemList.size() + (hasHeader() ? 1 : 0);
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "sizeConsideringHeaderOffset() called → " + i);
|
||||
}
|
||||
return i;
|
||||
}
|
||||
|
||||
public ArrayList<InfoItem> getItemsList() {
|
||||
public List<InfoItem> getItemsList() {
|
||||
return infoItemList;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
int count = infoItemList.size();
|
||||
if (header != null) {
|
||||
if (hasHeader()) {
|
||||
count++;
|
||||
}
|
||||
if (footer != null && showFooter) {
|
||||
if (showFooter) {
|
||||
count++;
|
||||
}
|
||||
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "getItemCount() called with: "
|
||||
+ "count = " + count + ", infoItemList.size() = " + infoItemList.size() + ", "
|
||||
+ "header = " + header + ", footer = " + footer + ", "
|
||||
+ "hasHeader = " + hasHeader() + ", "
|
||||
+ "showFooter = " + showFooter);
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
@SuppressWarnings("FinalParameters")
|
||||
@Override
|
||||
public int getItemViewType(int position) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "getItemViewType() called with: position = [" + position + "]");
|
||||
}
|
||||
|
||||
if (header != null && position == 0) {
|
||||
if (hasHeader() && position == 0) {
|
||||
return HEADER_TYPE;
|
||||
} else if (header != null) {
|
||||
} else if (hasHeader()) {
|
||||
position--;
|
||||
}
|
||||
if (footer != null && position == infoItemList.size() && showFooter) {
|
||||
if (position == infoItemList.size() && showFooter) {
|
||||
return FOOTER_TYPE;
|
||||
}
|
||||
final InfoItem item = infoItemList.get(position);
|
||||
@@ -290,10 +258,16 @@ public class InfoListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolde
|
||||
+ "parent = [" + parent + "], type = [" + type + "]");
|
||||
}
|
||||
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(header);
|
||||
return new HFHolder(headerSupplier.get());
|
||||
case FOOTER_TYPE:
|
||||
return new HFHolder(footer);
|
||||
return new HFHolder(PignateFooterBinding
|
||||
.inflate(layoutInflater, parent, false)
|
||||
.getRoot()
|
||||
);
|
||||
case MINI_STREAM_HOLDER_TYPE:
|
||||
return new StreamMiniInfoItemHolder(infoItemBuilder, parent);
|
||||
case STREAM_HOLDER_TYPE:
|
||||
@@ -322,42 +296,17 @@ public class InfoListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolde
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull final RecyclerView.ViewHolder holder, int position) {
|
||||
public void onBindViewHolder(@NonNull final RecyclerView.ViewHolder holder,
|
||||
final int position) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "onBindViewHolder() called with: "
|
||||
+ "holder = [" + holder.getClass().getSimpleName() + "], "
|
||||
+ "position = [" + position + "]");
|
||||
}
|
||||
if (holder instanceof InfoItemHolder) {
|
||||
// If header isn't null, offset the items by -1
|
||||
if (header != null) {
|
||||
position--;
|
||||
}
|
||||
|
||||
((InfoItemHolder) holder).updateFromItem(infoItemList.get(position), recordManager);
|
||||
} else if (holder instanceof HFHolder && position == 0 && header != null) {
|
||||
((HFHolder) holder).view = header;
|
||||
} else if (holder instanceof HFHolder && position == sizeConsideringHeaderOffset()
|
||||
&& footer != null && showFooter) {
|
||||
((HFHolder) holder).view = footer;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull final RecyclerView.ViewHolder holder, final int position,
|
||||
@NonNull final List<Object> payloads) {
|
||||
if (!payloads.isEmpty() && holder instanceof InfoItemHolder) {
|
||||
for (final Object payload : payloads) {
|
||||
if (payload instanceof StreamStateEntity) {
|
||||
((InfoItemHolder) holder).updateState(infoItemList
|
||||
.get(header == null ? position : position - 1), recordManager);
|
||||
} else if (payload instanceof Boolean) {
|
||||
((InfoItemHolder) holder).updateState(infoItemList
|
||||
.get(header == null ? position : position - 1), recordManager);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
onBindViewHolder(holder, position);
|
||||
((InfoItemHolder) holder).updateFromItem(
|
||||
// If header is present, offset the items by -1
|
||||
infoItemList.get(hasHeader() ? position - 1 : position), recordManager);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -371,12 +320,9 @@ public class InfoListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolde
|
||||
};
|
||||
}
|
||||
|
||||
public static class HFHolder extends RecyclerView.ViewHolder {
|
||||
public View view;
|
||||
|
||||
static class HFHolder extends RecyclerView.ViewHolder {
|
||||
HFHolder(final View v) {
|
||||
super(v);
|
||||
view = v;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,12 +34,14 @@ import org.schabi.newpipe.local.history.HistoryRecordManager;
|
||||
public class CommentsInfoItemHolder extends CommentsMiniInfoItemHolder {
|
||||
public final TextView itemTitleView;
|
||||
private final ImageView itemHeartView;
|
||||
private final ImageView itemPinnedView;
|
||||
|
||||
public CommentsInfoItemHolder(final InfoItemBuilder infoItemBuilder, final ViewGroup parent) {
|
||||
super(infoItemBuilder, R.layout.list_comments_item, parent);
|
||||
|
||||
itemTitleView = itemView.findViewById(R.id.itemTitleView);
|
||||
itemHeartView = itemView.findViewById(R.id.detail_heart_image_view);
|
||||
itemPinnedView = itemView.findViewById(R.id.detail_pinned_view);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -55,5 +57,7 @@ public class CommentsInfoItemHolder extends CommentsMiniInfoItemHolder {
|
||||
itemTitleView.setText(item.getUploaderName());
|
||||
|
||||
itemHeartView.setVisibility(item.isHeartedByUploader() ? View.VISIBLE : View.GONE);
|
||||
|
||||
itemPinnedView.setVisibility(item.isPinned() ? View.VISIBLE : View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ import android.widget.TextView;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.error.ErrorActivity;
|
||||
import org.schabi.newpipe.error.ErrorUtil;
|
||||
import org.schabi.newpipe.extractor.InfoItem;
|
||||
import org.schabi.newpipe.extractor.comments.CommentsInfoItem;
|
||||
import org.schabi.newpipe.info_list.InfoItemBuilder;
|
||||
@@ -171,7 +171,7 @@ public class CommentsMiniInfoItemHolder extends InfoItemHolder {
|
||||
item.getUploaderUrl(),
|
||||
item.getUploaderName());
|
||||
} catch (final Exception e) {
|
||||
ErrorActivity.reportUiErrorInSnackbar(activity, "Opening channel fragment", e);
|
||||
ErrorUtil.showUiErrorSnackbar(activity, "Opening channel fragment", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -75,6 +75,7 @@ fun View.animate(
|
||||
}
|
||||
animate().setListener(null).cancel()
|
||||
isVisible = true
|
||||
|
||||
when (animationType) {
|
||||
AnimationType.ALPHA -> animateAlpha(enterOrExit, duration, delay, execOnEnd)
|
||||
AnimationType.SCALE_AND_ALPHA -> animateScaleAndAlpha(enterOrExit, duration, delay, execOnEnd)
|
||||
@@ -299,18 +300,36 @@ private fun View.animateLightSlideAndAlpha(enterOrExit: Boolean, duration: Long,
|
||||
}
|
||||
}
|
||||
|
||||
fun View.slideUp(duration: Long, delay: Long, @FloatRange(from = 0.0, to = 1.0) translationPercent: Float) {
|
||||
fun View.slideUp(
|
||||
duration: Long,
|
||||
delay: Long,
|
||||
@FloatRange(from = 0.0, to = 1.0) translationPercent: Float
|
||||
) {
|
||||
slideUp(duration, delay, translationPercent, null)
|
||||
}
|
||||
|
||||
fun View.slideUp(
|
||||
duration: Long,
|
||||
delay: Long = 0L,
|
||||
@FloatRange(from = 0.0, to = 1.0) translationPercent: Float = 1.0F,
|
||||
execOnEnd: Runnable? = null
|
||||
) {
|
||||
val newTranslationY = (resources.displayMetrics.heightPixels * translationPercent).toInt()
|
||||
animate().setListener(null).cancel()
|
||||
alpha = 0f
|
||||
translationY = newTranslationY.toFloat()
|
||||
visibility = View.VISIBLE
|
||||
isVisible = true
|
||||
animate()
|
||||
.alpha(1f)
|
||||
.translationY(0f)
|
||||
.setStartDelay(delay)
|
||||
.setDuration(duration)
|
||||
.setInterpolator(FastOutSlowInInterpolator())
|
||||
.setListener(object : AnimatorListenerAdapter() {
|
||||
override fun onAnimationEnd(animation: Animator) {
|
||||
execOnEnd?.run()
|
||||
}
|
||||
})
|
||||
.start()
|
||||
}
|
||||
|
||||
|
||||
@@ -78,9 +78,9 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setUserVisibleHint(final boolean isVisibleToUser) {
|
||||
super.setUserVisibleHint(isVisibleToUser);
|
||||
if (activity != null && isVisibleToUser) {
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
if (activity != null) {
|
||||
setTitle(activity.getString(R.string.tab_bookmarks));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package org.schabi.newpipe.local.dialog;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
@@ -17,20 +16,14 @@ import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.database.LocalItem;
|
||||
import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry;
|
||||
import org.schabi.newpipe.database.stream.model.StreamEntity;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||
import org.schabi.newpipe.local.LocalItemListAdapter;
|
||||
import org.schabi.newpipe.local.playlist.LocalPlaylistManager;
|
||||
import org.schabi.newpipe.player.playqueue.PlayQueueItem;
|
||||
import org.schabi.newpipe.util.OnClickGesture;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
||||
import io.reactivex.rxjava3.disposables.Disposable;
|
||||
|
||||
public final class PlaylistAppendDialog extends PlaylistDialog {
|
||||
private static final String TAG = PlaylistAppendDialog.class.getCanonicalName();
|
||||
@@ -40,47 +33,8 @@ public final class PlaylistAppendDialog extends PlaylistDialog {
|
||||
|
||||
private final CompositeDisposable playlistDisposables = new CompositeDisposable();
|
||||
|
||||
public static Disposable onPlaylistFound(
|
||||
final Context context, final Runnable onSuccess, final Runnable onFailed
|
||||
) {
|
||||
final LocalPlaylistManager playlistManager =
|
||||
new LocalPlaylistManager(NewPipeDatabase.getInstance(context));
|
||||
|
||||
return playlistManager.hasPlaylists()
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(hasPlaylists -> {
|
||||
if (hasPlaylists) {
|
||||
onSuccess.run();
|
||||
} else {
|
||||
onFailed.run();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static PlaylistAppendDialog fromStreamInfo(final StreamInfo info) {
|
||||
final PlaylistAppendDialog dialog = new PlaylistAppendDialog();
|
||||
dialog.setInfo(Collections.singletonList(new StreamEntity(info)));
|
||||
return dialog;
|
||||
}
|
||||
|
||||
public static PlaylistAppendDialog fromStreamInfoItems(final List<StreamInfoItem> items) {
|
||||
final PlaylistAppendDialog dialog = new PlaylistAppendDialog();
|
||||
final List<StreamEntity> entities = new ArrayList<>(items.size());
|
||||
for (final StreamInfoItem item : items) {
|
||||
entities.add(new StreamEntity(item));
|
||||
}
|
||||
dialog.setInfo(entities);
|
||||
return dialog;
|
||||
}
|
||||
|
||||
public static PlaylistAppendDialog fromPlayQueueItems(final List<PlayQueueItem> items) {
|
||||
final PlaylistAppendDialog dialog = new PlaylistAppendDialog();
|
||||
final List<StreamEntity> entities = new ArrayList<>(items.size());
|
||||
for (final PlayQueueItem item : items) {
|
||||
entities.add(new StreamEntity(item));
|
||||
}
|
||||
dialog.setInfo(entities);
|
||||
return dialog;
|
||||
public PlaylistAppendDialog(final List<StreamEntity> streamEntities) {
|
||||
super(streamEntities);
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
@@ -104,11 +58,15 @@ public final class PlaylistAppendDialog extends PlaylistDialog {
|
||||
playlistAdapter.setSelectedListener(new OnClickGesture<LocalItem>() {
|
||||
@Override
|
||||
public void selected(final LocalItem selectedItem) {
|
||||
if (!(selectedItem instanceof PlaylistMetadataEntry) || getStreams() == null) {
|
||||
if (!(selectedItem instanceof PlaylistMetadataEntry)
|
||||
|| getStreamEntities() == null) {
|
||||
return;
|
||||
}
|
||||
onPlaylistSelected(playlistManager, (PlaylistMetadataEntry) selectedItem,
|
||||
getStreams());
|
||||
onPlaylistSelected(
|
||||
playlistManager,
|
||||
(PlaylistMetadataEntry) selectedItem,
|
||||
getStreamEntities()
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -146,11 +104,17 @@ public final class PlaylistAppendDialog extends PlaylistDialog {
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
public void openCreatePlaylistDialog() {
|
||||
if (getStreams() == null || !isAdded()) {
|
||||
if (getStreamEntities() == null || !isAdded()) {
|
||||
return;
|
||||
}
|
||||
|
||||
PlaylistCreationDialog.newInstance(getStreams()).show(getParentFragmentManager(), TAG);
|
||||
final PlaylistCreationDialog playlistCreationDialog =
|
||||
new PlaylistCreationDialog(getStreamEntities());
|
||||
// Move the dismissListener to the new dialog.
|
||||
playlistCreationDialog.setOnDismissListener(this.getOnDismissListener());
|
||||
this.setOnDismissListener(null);
|
||||
|
||||
playlistCreationDialog.show(getParentFragmentManager(), TAG);
|
||||
requireDialog().dismiss();
|
||||
}
|
||||
|
||||
@@ -165,7 +129,7 @@ public final class PlaylistAppendDialog extends PlaylistDialog {
|
||||
private void onPlaylistSelected(@NonNull final LocalPlaylistManager manager,
|
||||
@NonNull final PlaylistMetadataEntry playlist,
|
||||
@NonNull final List<StreamEntity> streams) {
|
||||
if (getStreams() == null) {
|
||||
if (getStreamEntities() == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -7,29 +7,22 @@ import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.appcompat.app.AlertDialog.Builder;
|
||||
|
||||
import org.schabi.newpipe.NewPipeDatabase;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.database.stream.model.StreamEntity;
|
||||
import org.schabi.newpipe.databinding.DialogEditTextBinding;
|
||||
import org.schabi.newpipe.local.playlist.LocalPlaylistManager;
|
||||
import org.schabi.newpipe.util.ThemeHelper;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||
|
||||
public final class PlaylistCreationDialog extends PlaylistDialog {
|
||||
public static PlaylistCreationDialog newInstance(final List<StreamEntity> streams) {
|
||||
final PlaylistCreationDialog dialog = new PlaylistCreationDialog();
|
||||
dialog.setInfo(streams);
|
||||
return dialog;
|
||||
}
|
||||
|
||||
public static PlaylistCreationDialog newInstance(final PlaylistAppendDialog appendDialog) {
|
||||
final PlaylistCreationDialog dialog = new PlaylistCreationDialog();
|
||||
dialog.setInfo(appendDialog.getStreams());
|
||||
return dialog;
|
||||
public PlaylistCreationDialog(final List<StreamEntity> streamEntities) {
|
||||
super(streamEntities);
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
@@ -39,16 +32,18 @@ public final class PlaylistCreationDialog extends PlaylistDialog {
|
||||
@NonNull
|
||||
@Override
|
||||
public Dialog onCreateDialog(@Nullable final Bundle savedInstanceState) {
|
||||
if (getStreams() == null) {
|
||||
if (getStreamEntities() == null) {
|
||||
return super.onCreateDialog(savedInstanceState);
|
||||
}
|
||||
|
||||
final DialogEditTextBinding dialogBinding
|
||||
= DialogEditTextBinding.inflate(getLayoutInflater());
|
||||
dialogBinding.getRoot().getContext().setTheme(ThemeHelper.getDialogTheme(requireContext()));
|
||||
dialogBinding.dialogEditText.setHint(R.string.name);
|
||||
dialogBinding.dialogEditText.setInputType(InputType.TYPE_CLASS_TEXT);
|
||||
|
||||
final AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(requireContext())
|
||||
final Builder dialogBuilder = new Builder(requireContext(),
|
||||
ThemeHelper.getDialogTheme(requireContext()))
|
||||
.setTitle(R.string.create_playlist)
|
||||
.setView(dialogBinding.getRoot())
|
||||
.setCancelable(true)
|
||||
@@ -61,11 +56,10 @@ public final class PlaylistCreationDialog extends PlaylistDialog {
|
||||
R.string.playlist_creation_success,
|
||||
Toast.LENGTH_SHORT);
|
||||
|
||||
playlistManager.createPlaylist(name, getStreams())
|
||||
playlistManager.createPlaylist(name, getStreamEntities())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(longs -> successToast.show());
|
||||
});
|
||||
|
||||
return dialogBuilder.create();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package org.schabi.newpipe.local.dialog;
|
||||
|
||||
import android.app.Dialog;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.os.Bundle;
|
||||
import android.view.Window;
|
||||
|
||||
@@ -8,23 +10,29 @@ import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.DialogFragment;
|
||||
|
||||
import org.schabi.newpipe.NewPipeDatabase;
|
||||
import org.schabi.newpipe.database.stream.model.StreamEntity;
|
||||
import org.schabi.newpipe.local.playlist.LocalPlaylistManager;
|
||||
import org.schabi.newpipe.util.StateSaver;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Queue;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.rxjava3.disposables.Disposable;
|
||||
|
||||
public abstract class PlaylistDialog extends DialogFragment implements StateSaver.WriteRead {
|
||||
|
||||
@Nullable
|
||||
private DialogInterface.OnDismissListener onDismissListener = null;
|
||||
|
||||
private List<StreamEntity> streamEntities;
|
||||
|
||||
private org.schabi.newpipe.util.SavedState savedState;
|
||||
|
||||
protected void setInfo(final List<StreamEntity> entities) {
|
||||
this.streamEntities = entities;
|
||||
}
|
||||
|
||||
protected List<StreamEntity> getStreams() {
|
||||
return streamEntities;
|
||||
public PlaylistDialog(final List<StreamEntity> streamEntities) {
|
||||
this.streamEntities = streamEntities;
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
@@ -43,6 +51,10 @@ public abstract class PlaylistDialog extends DialogFragment implements StateSave
|
||||
StateSaver.onDestroy(savedState);
|
||||
}
|
||||
|
||||
public List<StreamEntity> getStreamEntities() {
|
||||
return streamEntities;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Dialog onCreateDialog(final Bundle savedInstanceState) {
|
||||
@@ -55,6 +67,14 @@ public abstract class PlaylistDialog extends DialogFragment implements StateSave
|
||||
return dialog;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDismiss(@NonNull final DialogInterface dialog) {
|
||||
super.onDismiss(dialog);
|
||||
if (onDismissListener != null) {
|
||||
onDismissListener.onDismiss(dialog);
|
||||
}
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// State Saving
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
@@ -84,4 +104,47 @@ public abstract class PlaylistDialog extends DialogFragment implements StateSave
|
||||
savedState, outState, this);
|
||||
}
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Getter + Setter
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Nullable
|
||||
public DialogInterface.OnDismissListener getOnDismissListener() {
|
||||
return onDismissListener;
|
||||
}
|
||||
|
||||
public void setOnDismissListener(
|
||||
@Nullable final DialogInterface.OnDismissListener onDismissListener
|
||||
) {
|
||||
this.onDismissListener = onDismissListener;
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Dialog creation
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
/**
|
||||
* Creates a {@link PlaylistAppendDialog} when playlists exists,
|
||||
* otherwise a {@link PlaylistCreationDialog}.
|
||||
*
|
||||
* @param context context used for accessing the database
|
||||
* @param streamEntities used for crating the dialog
|
||||
* @param onExec execution that should occur after a dialog got created, e.g. showing it
|
||||
* @return Disposable
|
||||
*/
|
||||
public static Disposable createCorrespondingDialog(
|
||||
final Context context,
|
||||
final List<StreamEntity> streamEntities,
|
||||
final Consumer<PlaylistDialog> onExec
|
||||
) {
|
||||
return new LocalPlaylistManager(NewPipeDatabase.getInstance(context))
|
||||
.hasPlaylists()
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(hasPlaylists ->
|
||||
onExec.accept(hasPlaylists
|
||||
? new PlaylistAppendDialog(streamEntities)
|
||||
: new PlaylistCreationDialog(streamEntities))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ class FeedDatabaseManager(context: Context) {
|
||||
fun getStreams(
|
||||
groupId: Long = FeedGroupEntity.GROUP_ALL_ID,
|
||||
getPlayedStreams: Boolean = true
|
||||
): Flowable<List<StreamWithState>> {
|
||||
): Maybe<List<StreamWithState>> {
|
||||
return when (groupId) {
|
||||
FeedGroupEntity.GROUP_ALL_ID -> {
|
||||
if (getPlayedStreams) feedTable.getAllStreams()
|
||||
|
||||
@@ -21,16 +21,23 @@ package org.schabi.newpipe.local.feed
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.SharedPreferences
|
||||
import android.graphics.Typeface
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.graphics.drawable.LayerDrawable
|
||||
import android.os.Bundle
|
||||
import android.os.Parcelable
|
||||
import android.util.Log
|
||||
import android.view.LayoutInflater
|
||||
import android.view.Menu
|
||||
import android.view.MenuInflater
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.Button
|
||||
import androidx.annotation.AttrRes
|
||||
import androidx.annotation.Nullable
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.appcompat.content.res.AppCompatResources
|
||||
@@ -40,8 +47,10 @@ import androidx.core.view.isVisible
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.preference.PreferenceManager
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.xwray.groupie.GroupieAdapter
|
||||
import com.xwray.groupie.Item
|
||||
import com.xwray.groupie.OnAsyncUpdateListener
|
||||
import com.xwray.groupie.OnItemClickListener
|
||||
import com.xwray.groupie.OnItemLongClickListener
|
||||
import icepick.State
|
||||
@@ -65,10 +74,12 @@ import org.schabi.newpipe.fragments.BaseStateFragment
|
||||
import org.schabi.newpipe.info_list.InfoItemDialog
|
||||
import org.schabi.newpipe.ktx.animate
|
||||
import org.schabi.newpipe.ktx.animateHideRecyclerViewAllowingScrolling
|
||||
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.player.helper.PlayerHolder
|
||||
import org.schabi.newpipe.util.DeviceUtils
|
||||
import org.schabi.newpipe.util.Localization
|
||||
import org.schabi.newpipe.util.NavigationHelper
|
||||
import org.schabi.newpipe.util.StreamDialogEntry
|
||||
@@ -76,6 +87,7 @@ import org.schabi.newpipe.util.ThemeHelper.getGridSpanCountStreams
|
||||
import org.schabi.newpipe.util.ThemeHelper.shouldUseGridLayout
|
||||
import java.time.OffsetDateTime
|
||||
import java.util.ArrayList
|
||||
import java.util.function.Consumer
|
||||
|
||||
class FeedFragment : BaseStateFragment<FeedState>() {
|
||||
private var _feedBinding: FragmentFeedBinding? = null
|
||||
@@ -97,6 +109,8 @@ class FeedFragment : BaseStateFragment<FeedState>() {
|
||||
private var updateListViewModeOnResume = false
|
||||
private var isRefreshing = false
|
||||
|
||||
private var lastNewItemsCount = 0
|
||||
|
||||
init {
|
||||
setHasOptionsMenu(true)
|
||||
}
|
||||
@@ -126,8 +140,9 @@ class FeedFragment : BaseStateFragment<FeedState>() {
|
||||
_feedBinding = FragmentFeedBinding.bind(rootView)
|
||||
super.onViewCreated(rootView, savedInstanceState)
|
||||
|
||||
val factory = FeedViewModel.Factory(requireContext(), groupId, showPlayedItems)
|
||||
val factory = FeedViewModel.Factory(requireContext(), groupId)
|
||||
viewModel = ViewModelProvider(this, factory).get(FeedViewModel::class.java)
|
||||
showPlayedItems = viewModel.getShowPlayedItemsFromPreferences()
|
||||
viewModel.stateLiveData.observe(viewLifecycleOwner, { it?.let(::handleResult) })
|
||||
|
||||
groupAdapter = GroupieAdapter().apply {
|
||||
@@ -135,6 +150,20 @@ class FeedFragment : BaseStateFragment<FeedState>() {
|
||||
setOnItemLongClickListener(listenerStreamItem)
|
||||
}
|
||||
|
||||
feedBinding.itemsList.addOnScrollListener(object : RecyclerView.OnScrollListener() {
|
||||
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
|
||||
// Check if we scrolled to the top
|
||||
if (newState == RecyclerView.SCROLL_STATE_IDLE &&
|
||||
!recyclerView.canScrollVertically(-1)
|
||||
) {
|
||||
|
||||
if (tryGetNewItemsLoadedButton()?.isVisible == true) {
|
||||
hideNewItemsLoaded(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
feedBinding.itemsList.adapter = groupAdapter
|
||||
setupListViewMode()
|
||||
}
|
||||
@@ -158,7 +187,7 @@ class FeedFragment : BaseStateFragment<FeedState>() {
|
||||
}
|
||||
}
|
||||
|
||||
fun setupListViewMode() {
|
||||
private fun setupListViewMode() {
|
||||
// does everything needed to setup the layouts for grid or list modes
|
||||
groupAdapter.spanCount = if (shouldUseGridLayout(context)) getGridSpanCountStreams(context) else 1
|
||||
feedBinding.itemsList.layoutManager = GridLayoutManager(requireContext(), groupAdapter.spanCount).apply {
|
||||
@@ -170,6 +199,10 @@ class FeedFragment : BaseStateFragment<FeedState>() {
|
||||
super.initListeners()
|
||||
feedBinding.refreshRootView.setOnClickListener { reloadContent() }
|
||||
feedBinding.swipeRefreshLayout.setOnRefreshListener { reloadContent() }
|
||||
feedBinding.newItemsLoadedButton.setOnClickListener {
|
||||
hideNewItemsLoaded(true)
|
||||
feedBinding.itemsList.scrollToPosition(0)
|
||||
}
|
||||
}
|
||||
|
||||
// /////////////////////////////////////////////////////////////////////////
|
||||
@@ -213,6 +246,7 @@ class FeedFragment : BaseStateFragment<FeedState>() {
|
||||
showPlayedItems = !item.isChecked
|
||||
updateTogglePlayedItemsButton(item)
|
||||
viewModel.togglePlayedItems(showPlayedItems)
|
||||
viewModel.saveShowPlayedItemsToPreferences(showPlayedItems)
|
||||
}
|
||||
|
||||
return super.onOptionsItemSelected(item)
|
||||
@@ -236,6 +270,9 @@ class FeedFragment : BaseStateFragment<FeedState>() {
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
// Ensure that all animations are canceled
|
||||
tryGetNewItemsLoadedButton()?.clearAnimation()
|
||||
|
||||
feedBinding.itemsList.adapter = null
|
||||
_feedBinding = null
|
||||
super.onDestroyView()
|
||||
@@ -325,7 +362,7 @@ class FeedFragment : BaseStateFragment<FeedState>() {
|
||||
if (context == null || context.resources == null || activity == null) return
|
||||
|
||||
val entries = ArrayList<StreamDialogEntry>()
|
||||
if (PlayerHolder.getInstance().isPlayerOpen) {
|
||||
if (PlayerHolder.getInstance().isPlayQueueReady) {
|
||||
entries.add(StreamDialogEntry.enqueue)
|
||||
|
||||
if (PlayerHolder.getInstance().queueSize > 1) {
|
||||
@@ -355,13 +392,7 @@ class FeedFragment : BaseStateFragment<FeedState>() {
|
||||
}
|
||||
|
||||
// show "mark as watched" only when watch history is enabled
|
||||
val isWatchHistoryEnabled = PreferenceManager
|
||||
.getDefaultSharedPreferences(context)
|
||||
.getBoolean(getString(R.string.enable_watch_history_key), false)
|
||||
if (item.streamType != StreamType.AUDIO_LIVE_STREAM &&
|
||||
item.streamType != StreamType.LIVE_STREAM &&
|
||||
isWatchHistoryEnabled
|
||||
) {
|
||||
if (StreamDialogEntry.shouldAddMarkAsWatched(item.streamType, context)) {
|
||||
entries.add(
|
||||
StreamDialogEntry.mark_as_watched
|
||||
)
|
||||
@@ -404,7 +435,17 @@ class FeedFragment : BaseStateFragment<FeedState>() {
|
||||
}
|
||||
loadedState.items.forEach { it.itemVersion = itemVersion }
|
||||
|
||||
groupAdapter.updateAsync(loadedState.items, false, null)
|
||||
// This need to be saved in a variable as the update occurs async
|
||||
val oldOldestSubscriptionUpdate = oldestSubscriptionUpdate
|
||||
|
||||
groupAdapter.updateAsync(
|
||||
loadedState.items, false,
|
||||
OnAsyncUpdateListener {
|
||||
oldOldestSubscriptionUpdate?.run {
|
||||
highlightNewItemsAfter(oldOldestSubscriptionUpdate)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
listState?.run {
|
||||
feedBinding.itemsList.layoutManager?.onRestoreInstanceState(listState)
|
||||
@@ -464,7 +505,7 @@ class FeedFragment : BaseStateFragment<FeedState>() {
|
||||
errors.subList(i + 1, errors.size)
|
||||
)
|
||||
},
|
||||
{ throwable -> throwable.printStackTrace() }
|
||||
{ throwable -> Log.e(TAG, "Unable to process", throwable) }
|
||||
)
|
||||
return // this will be called on the remaining errors by handleFeedNotAvailable()
|
||||
}
|
||||
@@ -526,6 +567,125 @@ class FeedFragment : BaseStateFragment<FeedState>() {
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Highlights all items that are after the specified time
|
||||
*/
|
||||
private fun highlightNewItemsAfter(updateTime: OffsetDateTime) {
|
||||
var highlightCount = 0
|
||||
|
||||
var doCheck = true
|
||||
|
||||
for (i in 0 until groupAdapter.itemCount) {
|
||||
val item = groupAdapter.getItem(i) as StreamItem
|
||||
|
||||
var typeface = Typeface.DEFAULT
|
||||
var backgroundSupplier = { ctx: Context ->
|
||||
resolveDrawable(ctx, R.attr.selectableItemBackground)
|
||||
}
|
||||
if (doCheck) {
|
||||
// If the uploadDate is null or true we should highlight the item
|
||||
if (item.streamWithState.stream.uploadDate?.isAfter(updateTime) != false) {
|
||||
highlightCount++
|
||||
|
||||
typeface = Typeface.DEFAULT_BOLD
|
||||
backgroundSupplier = { ctx: Context ->
|
||||
// Merge the drawables together. Otherwise we would lose the "select" effect
|
||||
LayerDrawable(
|
||||
arrayOf(
|
||||
resolveDrawable(ctx, R.attr.dashed_border),
|
||||
resolveDrawable(ctx, R.attr.selectableItemBackground)
|
||||
)
|
||||
)
|
||||
}
|
||||
} else {
|
||||
// Decreases execution time due to the order of the items (newest always on top)
|
||||
// Once a item is is before the updateTime we can skip all following items
|
||||
doCheck = false
|
||||
}
|
||||
}
|
||||
|
||||
// The highlighter has to be always set
|
||||
// When it's only set on items that are highlighted it will highlight all items
|
||||
// due to the fact that itemRoot is getting recycled
|
||||
item.execBindEnd = Consumer { viewBinding ->
|
||||
val context = viewBinding.itemRoot.context
|
||||
viewBinding.itemRoot.background = backgroundSupplier.invoke(context)
|
||||
viewBinding.itemVideoTitleView.typeface = typeface
|
||||
}
|
||||
}
|
||||
|
||||
// Force updates all items so that the highlighting is correct
|
||||
// If this isn't done visible items that are already highlighted will stay in a highlighted
|
||||
// state until the user scrolls them out of the visible area which causes a update/bind-call
|
||||
groupAdapter.notifyItemRangeChanged(
|
||||
0,
|
||||
minOf(groupAdapter.itemCount, maxOf(highlightCount, lastNewItemsCount))
|
||||
)
|
||||
|
||||
if (highlightCount > 0) {
|
||||
showNewItemsLoaded()
|
||||
}
|
||||
|
||||
lastNewItemsCount = highlightCount
|
||||
}
|
||||
|
||||
private fun resolveDrawable(context: Context, @AttrRes attrResId: Int): Drawable? {
|
||||
return androidx.core.content.ContextCompat.getDrawable(
|
||||
context,
|
||||
android.util.TypedValue().apply {
|
||||
context.theme.resolveAttribute(
|
||||
attrResId,
|
||||
this,
|
||||
true
|
||||
)
|
||||
}.resourceId
|
||||
)
|
||||
}
|
||||
|
||||
private fun showNewItemsLoaded() {
|
||||
tryGetNewItemsLoadedButton()?.clearAnimation()
|
||||
tryGetNewItemsLoadedButton()
|
||||
?.slideUp(
|
||||
250L,
|
||||
delay = 100,
|
||||
execOnEnd = {
|
||||
// Disabled animations would result in immediately hiding the button
|
||||
// after it showed up
|
||||
if (DeviceUtils.hasAnimationsAnimatorDurationEnabled(context)) {
|
||||
// Hide the new items-"popup" after 10s
|
||||
hideNewItemsLoaded(true, 10000)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun hideNewItemsLoaded(animate: Boolean, delay: Long = 0) {
|
||||
tryGetNewItemsLoadedButton()?.clearAnimation()
|
||||
if (animate) {
|
||||
tryGetNewItemsLoadedButton()?.animate(
|
||||
false,
|
||||
200,
|
||||
delay = delay,
|
||||
execOnEnd = {
|
||||
// Make the layout invisible so that the onScroll toTop method
|
||||
// only does necessary work
|
||||
tryGetNewItemsLoadedButton()?.isVisible = false
|
||||
}
|
||||
)
|
||||
} else {
|
||||
tryGetNewItemsLoadedButton()?.isVisible = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The view/button can be disposed/set to null under certain circumstances.
|
||||
* E.g. when the animation is still in progress but the view got destroyed.
|
||||
* This method is a helper for such states and can be used in affected code blocks.
|
||||
*/
|
||||
private fun tryGetNewItemsLoadedButton(): Button? {
|
||||
return _feedBinding?.newItemsLoadedButton
|
||||
}
|
||||
|
||||
// /////////////////////////////////////////////////////////////////////////
|
||||
// Load Service Handling
|
||||
// /////////////////////////////////////////////////////////////////////////
|
||||
@@ -533,6 +693,8 @@ class FeedFragment : BaseStateFragment<FeedState>() {
|
||||
override fun doInitialLoadLogic() {}
|
||||
|
||||
override fun reloadContent() {
|
||||
hideNewItemsLoaded(false)
|
||||
|
||||
getActivity()?.startService(
|
||||
Intent(requireContext(), FeedLoadService::class.java).apply {
|
||||
putExtra(FeedLoadService.EXTRA_GROUP_ID, groupId)
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
package org.schabi.newpipe.local.feed
|
||||
|
||||
import android.content.Context
|
||||
import androidx.core.content.edit
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.preference.PreferenceManager
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.core.Flowable
|
||||
import io.reactivex.rxjava3.functions.Function4
|
||||
import io.reactivex.rxjava3.processors.BehaviorProcessor
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import org.schabi.newpipe.R
|
||||
import org.schabi.newpipe.database.feed.model.FeedGroupEntity
|
||||
import org.schabi.newpipe.database.stream.StreamWithState
|
||||
import org.schabi.newpipe.local.feed.item.StreamItem
|
||||
@@ -23,19 +26,16 @@ import java.time.OffsetDateTime
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class FeedViewModel(
|
||||
applicationContext: Context,
|
||||
private val applicationContext: Context,
|
||||
groupId: Long = FeedGroupEntity.GROUP_ALL_ID,
|
||||
initialShowPlayedItems: Boolean = true
|
||||
) : ViewModel() {
|
||||
private var feedDatabaseManager: FeedDatabaseManager = FeedDatabaseManager(applicationContext)
|
||||
|
||||
private val toggleShowPlayedItems = BehaviorProcessor.create<Boolean>()
|
||||
private val streamItems = toggleShowPlayedItems
|
||||
private val toggleShowPlayedItemsFlowable = toggleShowPlayedItems
|
||||
.startWithItem(initialShowPlayedItems)
|
||||
.distinctUntilChanged()
|
||||
.switchMap { showPlayedItems ->
|
||||
feedDatabaseManager.getStreams(groupId, showPlayedItems)
|
||||
}
|
||||
|
||||
private val mutableStateLiveData = MutableLiveData<FeedState>()
|
||||
val stateLiveData: LiveData<FeedState> = mutableStateLiveData
|
||||
@@ -43,17 +43,28 @@ class FeedViewModel(
|
||||
private var combineDisposable = Flowable
|
||||
.combineLatest(
|
||||
FeedEventManager.events(),
|
||||
streamItems,
|
||||
toggleShowPlayedItemsFlowable,
|
||||
feedDatabaseManager.notLoadedCount(groupId),
|
||||
feedDatabaseManager.oldestSubscriptionUpdate(groupId),
|
||||
|
||||
Function4 { t1: FeedEventManager.Event, t2: List<StreamWithState>,
|
||||
Function4 { t1: FeedEventManager.Event, t2: Boolean,
|
||||
t3: Long, t4: List<OffsetDateTime> ->
|
||||
return@Function4 CombineResultHolder(t1, t2, t3, t4.firstOrNull())
|
||||
return@Function4 CombineResultEventHolder(t1, t2, t3, t4.firstOrNull())
|
||||
}
|
||||
)
|
||||
.throttleLatest(DEFAULT_THROTTLE_TIMEOUT, TimeUnit.MILLISECONDS)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(Schedulers.io())
|
||||
.map { (event, showPlayedItems, notLoadedCount, oldestUpdate) ->
|
||||
var streamItems = if (event is SuccessResultEvent || event is IdleEvent)
|
||||
feedDatabaseManager
|
||||
.getStreams(groupId, showPlayedItems)
|
||||
.blockingGet(arrayListOf())
|
||||
else
|
||||
arrayListOf()
|
||||
|
||||
CombineResultDataHolder(event, streamItems, notLoadedCount, oldestUpdate)
|
||||
}
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe { (event, listFromDB, notLoadedCount, oldestUpdate) ->
|
||||
mutableStateLiveData.postValue(
|
||||
@@ -75,20 +86,50 @@ class FeedViewModel(
|
||||
combineDisposable.dispose()
|
||||
}
|
||||
|
||||
private data class CombineResultHolder(val t1: FeedEventManager.Event, val t2: List<StreamWithState>, val t3: Long, val t4: OffsetDateTime?)
|
||||
private data class CombineResultEventHolder(
|
||||
val t1: FeedEventManager.Event,
|
||||
val t2: Boolean,
|
||||
val t3: Long,
|
||||
val t4: OffsetDateTime?
|
||||
)
|
||||
|
||||
private data class CombineResultDataHolder(
|
||||
val t1: FeedEventManager.Event,
|
||||
val t2: List<StreamWithState>,
|
||||
val t3: Long,
|
||||
val t4: OffsetDateTime?
|
||||
)
|
||||
|
||||
fun togglePlayedItems(showPlayedItems: Boolean) {
|
||||
toggleShowPlayedItems.onNext(showPlayedItems)
|
||||
}
|
||||
|
||||
fun saveShowPlayedItemsToPreferences(showPlayedItems: Boolean) =
|
||||
PreferenceManager.getDefaultSharedPreferences(applicationContext).edit {
|
||||
this.putBoolean(applicationContext.getString(R.string.feed_show_played_items_key), showPlayedItems)
|
||||
this.apply()
|
||||
}
|
||||
|
||||
fun getShowPlayedItemsFromPreferences() = getShowPlayedItemsFromPreferences(applicationContext)
|
||||
|
||||
companion object {
|
||||
private fun getShowPlayedItemsFromPreferences(context: Context) =
|
||||
PreferenceManager.getDefaultSharedPreferences(context)
|
||||
.getBoolean(context.getString(R.string.feed_show_played_items_key), true)
|
||||
}
|
||||
|
||||
class Factory(
|
||||
private val context: Context,
|
||||
private val groupId: Long = FeedGroupEntity.GROUP_ALL_ID,
|
||||
private val showPlayedItems: Boolean
|
||||
private val groupId: Long = FeedGroupEntity.GROUP_ALL_ID
|
||||
) : ViewModelProvider.Factory {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
|
||||
return FeedViewModel(context.applicationContext, groupId, showPlayedItems) as T
|
||||
return FeedViewModel(
|
||||
context.applicationContext,
|
||||
groupId,
|
||||
// Read initial value from preferences
|
||||
getShowPlayedItemsFromPreferences(context.applicationContext)
|
||||
) as T
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ import org.schabi.newpipe.util.Localization
|
||||
import org.schabi.newpipe.util.PicassoHelper
|
||||
import org.schabi.newpipe.util.StreamTypeUtil
|
||||
import java.util.concurrent.TimeUnit
|
||||
import java.util.function.Consumer
|
||||
|
||||
data class StreamItem(
|
||||
val streamWithState: StreamWithState,
|
||||
@@ -31,6 +32,12 @@ data class StreamItem(
|
||||
private val stream: StreamEntity = streamWithState.stream
|
||||
private val stateProgressTime: Long? = streamWithState.stateProgressMillis
|
||||
|
||||
/**
|
||||
* Will be executed at the end of the [StreamItem.bind] (with (ListStreamItemBinding,Int)).
|
||||
* Can be used e.g. for highlighting a item.
|
||||
*/
|
||||
var execBindEnd: Consumer<ListStreamItemBinding>? = null
|
||||
|
||||
override fun getId(): Long = stream.uid
|
||||
|
||||
enum class ItemVersion { NORMAL, MINI, GRID }
|
||||
@@ -97,6 +104,8 @@ data class StreamItem(
|
||||
viewBinding.itemAdditionalDetails.text =
|
||||
getStreamInfoDetailLine(viewBinding.itemAdditionalDetails.context)
|
||||
}
|
||||
|
||||
execBindEnd?.accept(viewBinding)
|
||||
}
|
||||
|
||||
override fun isLongClickable() = when (stream.streamType) {
|
||||
|
||||
@@ -183,28 +183,23 @@ class FeedLoadService : Service() {
|
||||
|
||||
subscriptions
|
||||
.take(1)
|
||||
|
||||
.doOnNext {
|
||||
currentProgress.set(0)
|
||||
maxProgress.set(it.size)
|
||||
}
|
||||
.filter { it.isNotEmpty() }
|
||||
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.doOnNext {
|
||||
startForeground(NOTIFICATION_ID, notificationBuilder.build())
|
||||
updateNotificationProgress(null)
|
||||
broadcastProgress()
|
||||
}
|
||||
|
||||
.observeOn(Schedulers.io())
|
||||
.flatMap { Flowable.fromIterable(it) }
|
||||
.takeWhile { !cancelSignal.get() }
|
||||
|
||||
.parallel(PARALLEL_EXTRACTIONS, PARALLEL_EXTRACTIONS * 2)
|
||||
.runOn(Schedulers.io(), PARALLEL_EXTRACTIONS * 2)
|
||||
.filter { !cancelSignal.get() }
|
||||
|
||||
.map { subscriptionEntity ->
|
||||
var error: Throwable? = null
|
||||
try {
|
||||
@@ -239,14 +234,11 @@ class FeedLoadService : Service() {
|
||||
}
|
||||
}
|
||||
.sequential()
|
||||
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.doOnNext(notificationsConsumer)
|
||||
|
||||
.observeOn(Schedulers.io())
|
||||
.buffer(BUFFER_COUNT_BEFORE_INSERT)
|
||||
.doOnNext(databaseConsumer)
|
||||
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(resultSubscriber)
|
||||
|
||||
@@ -120,19 +120,11 @@ public class HistoryRecordManager {
|
||||
}
|
||||
|
||||
// Update the stream progress to the full duration of the video
|
||||
final List<StreamStateEntity> states = streamStateTable.getState(streamId)
|
||||
.blockingFirst();
|
||||
if (!states.isEmpty()) {
|
||||
final StreamStateEntity entity = states.get(0);
|
||||
entity.setProgressMillis(duration * 1000);
|
||||
streamStateTable.update(entity);
|
||||
} else {
|
||||
final StreamStateEntity entity = new StreamStateEntity(
|
||||
streamId,
|
||||
duration * 1000
|
||||
);
|
||||
streamStateTable.insert(entity);
|
||||
}
|
||||
final StreamStateEntity entity = new StreamStateEntity(
|
||||
streamId,
|
||||
duration * 1000
|
||||
);
|
||||
streamStateTable.upsert(entity);
|
||||
|
||||
// Add a history entry
|
||||
final StreamHistoryEntity latestEntry = streamHistoryTable.getLatestEntry(streamId);
|
||||
@@ -252,9 +244,9 @@ public class HistoryRecordManager {
|
||||
.subscribeOn(Schedulers.io());
|
||||
}
|
||||
|
||||
public Flowable<List<SearchHistoryEntry>> getRelatedSearches(final String query,
|
||||
final int similarQueryLimit,
|
||||
final int uniqueQueryLimit) {
|
||||
public Flowable<List<String>> getRelatedSearches(final String query,
|
||||
final int similarQueryLimit,
|
||||
final int uniqueQueryLimit) {
|
||||
return query.length() > 0
|
||||
? searchHistoryTable.getSimilarEntries(query, similarQueryLimit)
|
||||
: searchHistoryTable.getUniqueEntries(uniqueQueryLimit);
|
||||
@@ -334,9 +326,9 @@ public class HistoryRecordManager {
|
||||
.getState(entities.get(0).getUid()).blockingFirst();
|
||||
if (states.isEmpty()) {
|
||||
result.add(null);
|
||||
continue;
|
||||
} else {
|
||||
result.add(states.get(0));
|
||||
}
|
||||
result.add(states.get(0));
|
||||
}
|
||||
return result;
|
||||
}).subscribeOn(Schedulers.io());
|
||||
@@ -362,9 +354,9 @@ public class HistoryRecordManager {
|
||||
.blockingFirst();
|
||||
if (states.isEmpty()) {
|
||||
result.add(null);
|
||||
continue;
|
||||
} else {
|
||||
result.add(states.get(0));
|
||||
}
|
||||
result.add(states.get(0));
|
||||
}
|
||||
return result;
|
||||
}).subscribeOn(Schedulers.io());
|
||||
|
||||
@@ -101,9 +101,9 @@ public class StatisticsPlaylistFragment
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setUserVisibleHint(final boolean isVisibleToUser) {
|
||||
super.setUserVisibleHint(isVisibleToUser);
|
||||
if (activity != null && isVisibleToUser) {
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
if (activity != null) {
|
||||
setTitle(activity.getString(R.string.title_activity_history));
|
||||
}
|
||||
}
|
||||
@@ -338,7 +338,7 @@ public class StatisticsPlaylistFragment
|
||||
|
||||
final ArrayList<StreamDialogEntry> entries = new ArrayList<>();
|
||||
|
||||
if (PlayerHolder.getInstance().isPlayerOpen()) {
|
||||
if (PlayerHolder.getInstance().isPlayQueueReady()) {
|
||||
entries.add(StreamDialogEntry.enqueue);
|
||||
|
||||
if (PlayerHolder.getInstance().getQueueSize() > 1) {
|
||||
@@ -366,6 +366,16 @@ public class StatisticsPlaylistFragment
|
||||
if (KoreUtils.shouldShowPlayWithKodi(context, infoItem.getServiceId())) {
|
||||
entries.add(StreamDialogEntry.play_with_kodi);
|
||||
}
|
||||
|
||||
// show "mark as watched" only when watch history is enabled
|
||||
if (StreamDialogEntry.shouldAddMarkAsWatched(
|
||||
item.getStreamEntity().getStreamType(),
|
||||
context
|
||||
)) {
|
||||
entries.add(
|
||||
StreamDialogEntry.mark_as_watched
|
||||
);
|
||||
}
|
||||
entries.add(StreamDialogEntry.show_channel_details);
|
||||
|
||||
StreamDialogEntry.setEnabledEntries(entries);
|
||||
|
||||
@@ -709,8 +709,8 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
|
||||
return false;
|
||||
}
|
||||
|
||||
final int sourceIndex = source.getAdapterPosition();
|
||||
final int targetIndex = target.getAdapterPosition();
|
||||
final int sourceIndex = source.getBindingAdapterPosition();
|
||||
final int targetIndex = target.getBindingAdapterPosition();
|
||||
final boolean isSwapped = itemListAdapter.swapItems(sourceIndex, targetIndex);
|
||||
if (isSwapped) {
|
||||
saveChanges();
|
||||
@@ -753,7 +753,7 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
|
||||
|
||||
final ArrayList<StreamDialogEntry> entries = new ArrayList<>();
|
||||
|
||||
if (PlayerHolder.getInstance().isPlayerOpen()) {
|
||||
if (PlayerHolder.getInstance().isPlayQueueReady()) {
|
||||
entries.add(StreamDialogEntry.enqueue);
|
||||
|
||||
if (PlayerHolder.getInstance().getQueueSize() > 1) {
|
||||
@@ -782,6 +782,16 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
|
||||
if (KoreUtils.shouldShowPlayWithKodi(context, infoItem.getServiceId())) {
|
||||
entries.add(StreamDialogEntry.play_with_kodi);
|
||||
}
|
||||
|
||||
// show "mark as watched" only when watch history is enabled
|
||||
if (StreamDialogEntry.shouldAddMarkAsWatched(
|
||||
item.getStreamEntity().getStreamType(),
|
||||
context
|
||||
)) {
|
||||
entries.add(
|
||||
StreamDialogEntry.mark_as_watched
|
||||
);
|
||||
}
|
||||
entries.add(StreamDialogEntry.show_channel_details);
|
||||
|
||||
StreamDialogEntry.setEnabledEntries(entries);
|
||||
|
||||
@@ -55,6 +55,7 @@ import org.schabi.newpipe.local.subscription.services.SubscriptionsImportService
|
||||
import org.schabi.newpipe.local.subscription.services.SubscriptionsImportService.KEY_MODE
|
||||
import org.schabi.newpipe.local.subscription.services.SubscriptionsImportService.KEY_VALUE
|
||||
import org.schabi.newpipe.local.subscription.services.SubscriptionsImportService.PREVIOUS_EXPORT_MODE
|
||||
import org.schabi.newpipe.streams.io.NoFileManagerSafeGuard
|
||||
import org.schabi.newpipe.streams.io.StoredFileHelper
|
||||
import org.schabi.newpipe.util.NavigationHelper
|
||||
import org.schabi.newpipe.util.OnClickGesture
|
||||
@@ -179,15 +180,23 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
|
||||
}
|
||||
|
||||
private fun onImportPreviousSelected() {
|
||||
requestImportLauncher.launch(StoredFileHelper.getPicker(activity, JSON_MIME_TYPE))
|
||||
NoFileManagerSafeGuard.launchSafe(
|
||||
requestImportLauncher,
|
||||
StoredFileHelper.getPicker(activity, JSON_MIME_TYPE),
|
||||
TAG,
|
||||
requireContext()
|
||||
)
|
||||
}
|
||||
|
||||
private fun onExportSelected() {
|
||||
val date = SimpleDateFormat("yyyyMMddHHmm", Locale.ENGLISH).format(Date())
|
||||
val exportName = "newpipe_subscriptions_$date.json"
|
||||
|
||||
requestExportLauncher.launch(
|
||||
StoredFileHelper.getNewPicker(activity, exportName, JSON_MIME_TYPE, null)
|
||||
NoFileManagerSafeGuard.launchSafe(
|
||||
requestExportLauncher,
|
||||
StoredFileHelper.getNewPicker(activity, exportName, JSON_MIME_TYPE, null),
|
||||
TAG,
|
||||
requireContext()
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -23,13 +23,14 @@ import androidx.core.text.util.LinkifyCompat;
|
||||
|
||||
import org.schabi.newpipe.BaseFragment;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.error.ErrorActivity;
|
||||
import org.schabi.newpipe.error.ErrorInfo;
|
||||
import org.schabi.newpipe.error.ErrorUtil;
|
||||
import org.schabi.newpipe.error.UserAction;
|
||||
import org.schabi.newpipe.extractor.NewPipe;
|
||||
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||
import org.schabi.newpipe.extractor.subscription.SubscriptionExtractor;
|
||||
import org.schabi.newpipe.local.subscription.services.SubscriptionsImportService;
|
||||
import org.schabi.newpipe.streams.io.NoFileManagerSafeGuard;
|
||||
import org.schabi.newpipe.streams.io.StoredFileHelper;
|
||||
import org.schabi.newpipe.util.Constants;
|
||||
import org.schabi.newpipe.util.ServiceHelper;
|
||||
@@ -86,22 +87,19 @@ public class SubscriptionsImportFragment extends BaseFragment {
|
||||
|
||||
setupServiceVariables();
|
||||
if (supportedSources.isEmpty() && currentServiceId != Constants.NO_SERVICE_ID) {
|
||||
ErrorActivity.reportErrorInSnackbar(activity,
|
||||
ErrorUtil.showSnackbar(activity,
|
||||
new ErrorInfo(new String[]{}, UserAction.SUBSCRIPTION_IMPORT_EXPORT,
|
||||
NewPipe.getNameOfService(currentServiceId),
|
||||
"Service does not support importing subscriptions",
|
||||
R.string.general_error,
|
||||
null));
|
||||
R.string.general_error));
|
||||
activity.finish();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setUserVisibleHint(final boolean isVisibleToUser) {
|
||||
super.setUserVisibleHint(isVisibleToUser);
|
||||
if (isVisibleToUser) {
|
||||
setTitle(getString(R.string.import_title));
|
||||
}
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
setTitle(getString(R.string.import_title));
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@@ -177,8 +175,14 @@ public class SubscriptionsImportFragment extends BaseFragment {
|
||||
}
|
||||
|
||||
public void onImportFile() {
|
||||
// leave */* mime type to support all services with different mime types and file extensions
|
||||
requestImportFileLauncher.launch(StoredFileHelper.getPicker(activity, "*/*"));
|
||||
NoFileManagerSafeGuard.launchSafe(
|
||||
requestImportFileLauncher,
|
||||
// leave */* mime type to support all services
|
||||
// with different mime types and file extensions
|
||||
StoredFileHelper.getPicker(activity, "*/*"),
|
||||
TAG,
|
||||
getContext()
|
||||
);
|
||||
}
|
||||
|
||||
private void requestImportFileResult(final ActivityResult result) {
|
||||
|
||||
@@ -112,8 +112,8 @@ class FeedGroupReorderDialog : DialogFragment() {
|
||||
source: RecyclerView.ViewHolder,
|
||||
target: RecyclerView.ViewHolder
|
||||
): Boolean {
|
||||
val sourceIndex = source.adapterPosition
|
||||
val targetIndex = target.adapterPosition
|
||||
val sourceIndex = source.bindingAdapterPosition
|
||||
val targetIndex = target.bindingAdapterPosition
|
||||
|
||||
groupAdapter.notifyItemMoved(sourceIndex, targetIndex)
|
||||
Collections.swap(groupOrderedIdList, sourceIndex, targetIndex)
|
||||
|
||||
@@ -54,11 +54,9 @@ class ChannelItem(
|
||||
context.getString(R.string.subscribers_count_not_available)
|
||||
}
|
||||
|
||||
if (itemVersion == ItemVersion.NORMAL) {
|
||||
if (infoItem.streamCount >= 0) {
|
||||
val formattedVideoAmount = Localization.localizeStreamCount(context, infoItem.streamCount)
|
||||
details = Localization.concatenateStrings(details, formattedVideoAmount)
|
||||
}
|
||||
if (itemVersion == ItemVersion.NORMAL && infoItem.streamCount >= 0) {
|
||||
val formattedVideoAmount = Localization.localizeStreamCount(context, infoItem.streamCount)
|
||||
details = Localization.concatenateStrings(details, formattedVideoAmount)
|
||||
}
|
||||
return details
|
||||
}
|
||||
|
||||
@@ -35,8 +35,8 @@ import androidx.core.app.ServiceCompat;
|
||||
|
||||
import org.reactivestreams.Publisher;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.error.ErrorActivity;
|
||||
import org.schabi.newpipe.error.ErrorInfo;
|
||||
import org.schabi.newpipe.error.ErrorUtil;
|
||||
import org.schabi.newpipe.error.UserAction;
|
||||
import org.schabi.newpipe.extractor.subscription.SubscriptionExtractor;
|
||||
import org.schabi.newpipe.ktx.ExceptionUtils;
|
||||
@@ -153,7 +153,7 @@ public abstract class BaseImportExportService extends Service {
|
||||
|
||||
protected void stopAndReportError(final Throwable throwable, final String request) {
|
||||
stopService();
|
||||
ErrorActivity.reportError(this, new ErrorInfo(
|
||||
ErrorUtil.createNotification(this, new ErrorInfo(
|
||||
throwable, UserAction.SUBSCRIPTION_IMPORT_EXPORT, request));
|
||||
}
|
||||
|
||||
|
||||
@@ -23,11 +23,11 @@ import androidx.recyclerview.widget.RecyclerView;
|
||||
import com.google.android.exoplayer2.PlaybackParameters;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.database.stream.model.StreamEntity;
|
||||
import org.schabi.newpipe.databinding.ActivityPlayerQueueControlBinding;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||
import org.schabi.newpipe.fragments.OnScrollBelowItemsListener;
|
||||
import org.schabi.newpipe.local.dialog.PlaylistAppendDialog;
|
||||
import org.schabi.newpipe.local.dialog.PlaylistCreationDialog;
|
||||
import org.schabi.newpipe.local.dialog.PlaylistDialog;
|
||||
import org.schabi.newpipe.player.event.PlayerEventListener;
|
||||
import org.schabi.newpipe.player.helper.PlaybackParameterDialog;
|
||||
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
||||
@@ -43,6 +43,7 @@ import org.schabi.newpipe.util.ServiceHelper;
|
||||
import org.schabi.newpipe.util.ThemeHelper;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.schabi.newpipe.QueueItemMenuUtil.openPopupMenu;
|
||||
import static org.schabi.newpipe.player.helper.PlayerHelper.formatSpeed;
|
||||
@@ -452,12 +453,12 @@ public final class PlayQueueActivity extends AppCompatActivity
|
||||
}
|
||||
}
|
||||
|
||||
private void openPlaylistAppendDialog(final List<PlayQueueItem> playlist) {
|
||||
final PlaylistAppendDialog d = PlaylistAppendDialog.fromPlayQueueItems(playlist);
|
||||
|
||||
PlaylistAppendDialog.onPlaylistFound(getApplicationContext(),
|
||||
() -> d.show(getSupportFragmentManager(), TAG),
|
||||
() -> PlaylistCreationDialog.newInstance(d).show(getSupportFragmentManager(), TAG));
|
||||
private void openPlaylistAppendDialog(final List<PlayQueueItem> playQueueItems) {
|
||||
PlaylistDialog.createCorrespondingDialog(
|
||||
getApplicationContext(),
|
||||
playQueueItems.stream().map(StreamEntity::new).collect(Collectors.toList()),
|
||||
dialog -> dialog.show(getSupportFragmentManager(), TAG)
|
||||
);
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
package org.schabi.newpipe.player;
|
||||
|
||||
import static com.google.android.exoplayer2.Player.DISCONTINUITY_REASON_AD_INSERTION;
|
||||
import static com.google.android.exoplayer2.Player.DISCONTINUITY_REASON_AUTO_TRANSITION;
|
||||
import static com.google.android.exoplayer2.Player.DISCONTINUITY_REASON_INTERNAL;
|
||||
import static com.google.android.exoplayer2.Player.DISCONTINUITY_REASON_PERIOD_TRANSITION;
|
||||
import static com.google.android.exoplayer2.Player.DISCONTINUITY_REASON_REMOVE;
|
||||
import static com.google.android.exoplayer2.Player.DISCONTINUITY_REASON_SEEK;
|
||||
import static com.google.android.exoplayer2.Player.DISCONTINUITY_REASON_SEEK_ADJUSTMENT;
|
||||
import static com.google.android.exoplayer2.Player.DISCONTINUITY_REASON_SKIP;
|
||||
import static com.google.android.exoplayer2.Player.DiscontinuityReason;
|
||||
import static com.google.android.exoplayer2.Player.EventListener;
|
||||
import static com.google.android.exoplayer2.Player.Listener;
|
||||
import static com.google.android.exoplayer2.Player.REPEAT_MODE_ALL;
|
||||
import static com.google.android.exoplayer2.Player.REPEAT_MODE_OFF;
|
||||
import static com.google.android.exoplayer2.Player.REPEAT_MODE_ONE;
|
||||
@@ -50,9 +51,6 @@ import static java.util.concurrent.TimeUnit.MILLISECONDS;
|
||||
|
||||
import android.animation.Animator;
|
||||
import android.animation.AnimatorListenerAdapter;
|
||||
import android.animation.ObjectAnimator;
|
||||
import android.animation.PropertyValuesHolder;
|
||||
import android.animation.ValueAnimator;
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
@@ -96,7 +94,6 @@ import android.widget.ProgressBar;
|
||||
import android.widget.RelativeLayout;
|
||||
import android.widget.SeekBar;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
@@ -116,6 +113,7 @@ import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.DefaultRenderersFactory;
|
||||
import com.google.android.exoplayer2.ExoPlaybackException;
|
||||
import com.google.android.exoplayer2.PlaybackParameters;
|
||||
import com.google.android.exoplayer2.Player.PositionInfo;
|
||||
import com.google.android.exoplayer2.RenderersFactory;
|
||||
import com.google.android.exoplayer2.SimpleExoPlayer;
|
||||
import com.google.android.exoplayer2.Timeline;
|
||||
@@ -123,13 +121,14 @@ import com.google.android.exoplayer2.source.BehindLiveWindowException;
|
||||
import com.google.android.exoplayer2.source.MediaSource;
|
||||
import com.google.android.exoplayer2.source.TrackGroup;
|
||||
import com.google.android.exoplayer2.source.TrackGroupArray;
|
||||
import com.google.android.exoplayer2.text.CaptionStyleCompat;
|
||||
import com.google.android.exoplayer2.text.Cue;
|
||||
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
|
||||
import com.google.android.exoplayer2.ui.AspectRatioFrameLayout;
|
||||
import com.google.android.exoplayer2.ui.CaptionStyleCompat;
|
||||
import com.google.android.exoplayer2.ui.SubtitleView;
|
||||
import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
import com.google.android.exoplayer2.video.VideoListener;
|
||||
import com.google.android.exoplayer2.video.VideoSize;
|
||||
import com.google.android.material.floatingactionbutton.FloatingActionButton;
|
||||
import com.squareup.picasso.Picasso;
|
||||
import com.squareup.picasso.Target;
|
||||
@@ -139,6 +138,9 @@ import org.schabi.newpipe.MainActivity;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.databinding.PlayerBinding;
|
||||
import org.schabi.newpipe.databinding.PlayerPopupCloseOverlayBinding;
|
||||
import org.schabi.newpipe.error.ErrorInfo;
|
||||
import org.schabi.newpipe.error.ErrorUtil;
|
||||
import org.schabi.newpipe.error.UserAction;
|
||||
import org.schabi.newpipe.extractor.MediaFormat;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||
import org.schabi.newpipe.extractor.stream.StreamSegment;
|
||||
@@ -149,6 +151,7 @@ import org.schabi.newpipe.info_list.StreamSegmentAdapter;
|
||||
import org.schabi.newpipe.ktx.AnimationType;
|
||||
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
||||
import org.schabi.newpipe.player.MainPlayer.PlayerType;
|
||||
import org.schabi.newpipe.player.event.DisplayPortion;
|
||||
import org.schabi.newpipe.player.event.PlayerEventListener;
|
||||
import org.schabi.newpipe.player.event.PlayerGestureListener;
|
||||
import org.schabi.newpipe.player.event.PlayerServiceEventListener;
|
||||
@@ -174,15 +177,16 @@ import org.schabi.newpipe.player.resolver.MediaSourceTag;
|
||||
import org.schabi.newpipe.player.resolver.VideoPlaybackResolver;
|
||||
import org.schabi.newpipe.player.seekbarpreview.SeekbarPreviewThumbnailHelper;
|
||||
import org.schabi.newpipe.player.seekbarpreview.SeekbarPreviewThumbnailHolder;
|
||||
import org.schabi.newpipe.util.StreamTypeUtil;
|
||||
import org.schabi.newpipe.util.DeviceUtils;
|
||||
import org.schabi.newpipe.util.ListHelper;
|
||||
import org.schabi.newpipe.util.NavigationHelper;
|
||||
import org.schabi.newpipe.util.PicassoHelper;
|
||||
import org.schabi.newpipe.util.SerializedCache;
|
||||
import org.schabi.newpipe.util.StreamTypeUtil;
|
||||
import org.schabi.newpipe.util.external_communication.KoreUtils;
|
||||
import org.schabi.newpipe.util.external_communication.ShareUtils;
|
||||
import org.schabi.newpipe.views.ExpandableSurfaceView;
|
||||
import org.schabi.newpipe.views.player.PlayerFastSeekOverlay;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
@@ -197,9 +201,8 @@ import io.reactivex.rxjava3.disposables.Disposable;
|
||||
import io.reactivex.rxjava3.disposables.SerialDisposable;
|
||||
|
||||
public final class Player implements
|
||||
EventListener,
|
||||
PlaybackListener,
|
||||
VideoListener,
|
||||
Listener,
|
||||
SeekBar.OnSeekBarChangeListener,
|
||||
View.OnClickListener,
|
||||
PopupMenu.OnMenuItemClickListener,
|
||||
@@ -243,6 +246,7 @@ public final class Player implements
|
||||
public static final int DEFAULT_CONTROLS_DURATION = 300; // 300 millis
|
||||
public static final int DEFAULT_CONTROLS_HIDE_TIME = 2000; // 2 Seconds
|
||||
public static final int DPAD_CONTROLS_HIDE_TIME = 7000; // 7 Seconds
|
||||
public static final int SEEK_OVERLAY_DURATION = 450; // 450 millis
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Other constants
|
||||
@@ -256,7 +260,8 @@ public final class Player implements
|
||||
// Playback
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
private PlayQueue playQueue;
|
||||
// play queue might be null e.g. while player is starting
|
||||
@Nullable private PlayQueue playQueue;
|
||||
private PlayQueueAdapter playQueueAdapter;
|
||||
private StreamSegmentAdapter segmentAdapter;
|
||||
|
||||
@@ -266,8 +271,6 @@ public final class Player implements
|
||||
@Nullable private MediaSourceTag currentMetadata;
|
||||
@Nullable private Bitmap currentThumbnail;
|
||||
|
||||
@Nullable private Toast errorToast;
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Player
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
@@ -311,7 +314,6 @@ public final class Player implements
|
||||
|
||||
private PlayerBinding binding;
|
||||
|
||||
private ValueAnimator controlViewAnimator;
|
||||
private final Handler controlsVisibilityHandler = new Handler();
|
||||
|
||||
// fullscreen player
|
||||
@@ -363,6 +365,7 @@ public final class Player implements
|
||||
|
||||
private int maxGestureLength; // scaled
|
||||
private GestureDetectorCompat gestureDetector;
|
||||
private PlayerGestureListener playerGestureListener;
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Listeners and disposables
|
||||
@@ -447,6 +450,8 @@ public final class Player implements
|
||||
initPlayer(true);
|
||||
}
|
||||
initListeners();
|
||||
|
||||
setupPlayerSeekOverlay();
|
||||
}
|
||||
|
||||
private void initViews(@NonNull final PlayerBinding playerBinding) {
|
||||
@@ -501,10 +506,6 @@ public final class Player implements
|
||||
|
||||
// Setup video view
|
||||
setupVideoSurface();
|
||||
simpleExoPlayer.addVideoListener(this);
|
||||
|
||||
// Setup subtitle view
|
||||
simpleExoPlayer.addTextOutput(binding.subtitleView);
|
||||
|
||||
// enable media tunneling
|
||||
if (DEBUG && PreferenceManager.getDefaultSharedPreferences(context)
|
||||
@@ -513,7 +514,7 @@ public final class Player implements
|
||||
+ "media tunneling disabled in debug preferences");
|
||||
} else if (DeviceUtils.shouldSupportMediaTunneling()) {
|
||||
trackSelector.setParameters(trackSelector.buildUponParameters()
|
||||
.setTunnelingAudioSessionId(C.generateAudioSessionIdV21(context)));
|
||||
.setTunnelingEnabled(true));
|
||||
} else if (DEBUG) {
|
||||
Log.d(TAG, "[" + Util.DEVICE_DEBUG_INFO + "] does not support media tunneling");
|
||||
}
|
||||
@@ -527,9 +528,9 @@ public final class Player implements
|
||||
binding.resizeTextView.setOnClickListener(this);
|
||||
binding.playbackLiveSync.setOnClickListener(this);
|
||||
|
||||
final PlayerGestureListener listener = new PlayerGestureListener(this, service);
|
||||
gestureDetector = new GestureDetectorCompat(context, listener);
|
||||
binding.getRoot().setOnTouchListener(listener);
|
||||
playerGestureListener = new PlayerGestureListener(this, service);
|
||||
gestureDetector = new GestureDetectorCompat(context, playerGestureListener);
|
||||
binding.getRoot().setOnTouchListener(playerGestureListener);
|
||||
|
||||
binding.queueButton.setOnClickListener(this);
|
||||
binding.segmentsButton.setOnClickListener(this);
|
||||
@@ -571,15 +572,89 @@ public final class Player implements
|
||||
});
|
||||
|
||||
// PlaybackControlRoot already consumed window insets but we should pass them to
|
||||
// player_overlays too. Without it they will be off-centered
|
||||
// player_overlays and fast_seek_overlay too. Without it they will be off-centered.
|
||||
binding.playbackControlRoot.addOnLayoutChangeListener(
|
||||
(v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) ->
|
||||
binding.playerOverlays.setPadding(
|
||||
v.getPaddingLeft(),
|
||||
v.getPaddingTop(),
|
||||
v.getPaddingRight(),
|
||||
v.getPaddingBottom()));
|
||||
(v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> {
|
||||
binding.playerOverlays.setPadding(
|
||||
v.getPaddingLeft(),
|
||||
v.getPaddingTop(),
|
||||
v.getPaddingRight(),
|
||||
v.getPaddingBottom());
|
||||
|
||||
// If we added padding to the fast seek overlay, too, it would not go under the
|
||||
// system ui. Instead we apply negative margins equal to the window insets of
|
||||
// the opposite side, so that the view covers all of the player (overflowing on
|
||||
// some sides) and its center coincides with the center of other controls.
|
||||
final RelativeLayout.LayoutParams fastSeekParams = (RelativeLayout.LayoutParams)
|
||||
binding.fastSeekOverlay.getLayoutParams();
|
||||
fastSeekParams.leftMargin = -v.getPaddingRight();
|
||||
fastSeekParams.topMargin = -v.getPaddingBottom();
|
||||
fastSeekParams.rightMargin = -v.getPaddingLeft();
|
||||
fastSeekParams.bottomMargin = -v.getPaddingTop();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the Fast-For/Backward overlay.
|
||||
*/
|
||||
private void setupPlayerSeekOverlay() {
|
||||
binding.fastSeekOverlay
|
||||
.seekSecondsSupplier(() -> retrieveSeekDurationFromPreferences(this) / 1000)
|
||||
.performListener(new PlayerFastSeekOverlay.PerformListener() {
|
||||
|
||||
@Override
|
||||
public void onDoubleTap() {
|
||||
animate(binding.fastSeekOverlay, true, SEEK_OVERLAY_DURATION);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDoubleTapEnd() {
|
||||
animate(binding.fastSeekOverlay, false, SEEK_OVERLAY_DURATION);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public FastSeekDirection getFastSeekDirection(
|
||||
@NonNull final DisplayPortion portion
|
||||
) {
|
||||
if (exoPlayerIsNull()) {
|
||||
// Abort seeking
|
||||
playerGestureListener.endMultiDoubleTap();
|
||||
return FastSeekDirection.NONE;
|
||||
}
|
||||
if (portion == DisplayPortion.LEFT) {
|
||||
// Check if it's possible to rewind
|
||||
// Small puffer to eliminate infinite rewind seeking
|
||||
if (simpleExoPlayer.getCurrentPosition() < 500L) {
|
||||
return FastSeekDirection.NONE;
|
||||
}
|
||||
return FastSeekDirection.BACKWARD;
|
||||
} else if (portion == DisplayPortion.RIGHT) {
|
||||
// Check if it's possible to fast-forward
|
||||
if (currentState == STATE_COMPLETED
|
||||
|| simpleExoPlayer.getCurrentPosition()
|
||||
>= simpleExoPlayer.getDuration()) {
|
||||
return FastSeekDirection.NONE;
|
||||
}
|
||||
return FastSeekDirection.FORWARD;
|
||||
}
|
||||
/* portion == DisplayPortion.MIDDLE */
|
||||
return FastSeekDirection.NONE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void seek(final boolean forward) {
|
||||
playerGestureListener.keepInDoubleTapMode();
|
||||
if (forward) {
|
||||
fastForward();
|
||||
} else {
|
||||
fastRewind();
|
||||
}
|
||||
}
|
||||
});
|
||||
playerGestureListener.doubleTapControls(binding.fastSeekOverlay);
|
||||
}
|
||||
|
||||
//endregion
|
||||
|
||||
|
||||
@@ -637,6 +712,7 @@ public final class Player implements
|
||||
final boolean isMuted = intent.getBooleanExtra(IS_MUTED, isMuted());
|
||||
|
||||
/*
|
||||
* TODO As seen in #7427 this does not work:
|
||||
* There are 3 situations when playback shouldn't be started from scratch (zero timestamp):
|
||||
* 1. User pressed on a timestamp link and the same video should be rewound to the timestamp
|
||||
* 2. User changed a player from, for example. main to popup, or from audio to main, etc
|
||||
@@ -695,7 +771,7 @@ public final class Player implements
|
||||
},
|
||||
error -> {
|
||||
if (DEBUG) {
|
||||
error.printStackTrace();
|
||||
Log.w(TAG, "Failed to start playback", error);
|
||||
}
|
||||
// In case any error we can start playback without history
|
||||
initPlayback(newQueue, repeatMode, playbackSpeed, playbackPitch,
|
||||
@@ -774,6 +850,8 @@ public final class Player implements
|
||||
destroyPlayer();
|
||||
initPlayer(playOnReady);
|
||||
setRepeatMode(repeatMode);
|
||||
// #6825 - Ensure that the shuffle-button is in the correct state on the UI
|
||||
setShuffleButton(binding.shuffleButton, simpleExoPlayer.getShuffleModeEnabled());
|
||||
setPlaybackParameters(playbackSpeed, playbackPitch, playbackSkipSilence);
|
||||
|
||||
playQueue = queue;
|
||||
@@ -807,7 +885,6 @@ public final class Player implements
|
||||
|
||||
if (!exoPlayerIsNull()) {
|
||||
simpleExoPlayer.removeListener(this);
|
||||
simpleExoPlayer.removeVideoListener(this);
|
||||
simpleExoPlayer.stop();
|
||||
simpleExoPlayer.release();
|
||||
}
|
||||
@@ -858,10 +935,10 @@ public final class Player implements
|
||||
|
||||
final int queuePos = playQueue.getIndex();
|
||||
final long windowPos = simpleExoPlayer.getCurrentPosition();
|
||||
final long duration = simpleExoPlayer.getDuration();
|
||||
|
||||
if (windowPos > 0 && windowPos <= simpleExoPlayer.getDuration()) {
|
||||
setRecovery(queuePos, windowPos);
|
||||
}
|
||||
// No checks due to https://github.com/TeamNewPipe/NewPipe/pull/7195#issuecomment-962624380
|
||||
setRecovery(queuePos, Math.max(0, Math.min(windowPos, duration)));
|
||||
}
|
||||
|
||||
private void setRecovery(final int queuePos, final long windowPos) {
|
||||
@@ -896,7 +973,7 @@ public final class Player implements
|
||||
|
||||
public void smoothStopPlayer() {
|
||||
// Pausing would make transition from one stream to a new stream not smooth, so only stop
|
||||
simpleExoPlayer.stop(false);
|
||||
simpleExoPlayer.stop();
|
||||
}
|
||||
//endregion
|
||||
|
||||
@@ -1796,71 +1873,6 @@ public final class Player implements
|
||||
return binding != null && binding.playbackControlRoot.getVisibility() == View.VISIBLE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Show a animation, and depending on goneOnEnd, will stay on the screen or be gone.
|
||||
*
|
||||
* @param drawableId the drawable that will be used to animate,
|
||||
* pass -1 to clear any animation that is visible
|
||||
* @param goneOnEnd will set the animation view to GONE on the end of the animation
|
||||
*/
|
||||
public void showAndAnimateControl(final int drawableId, final boolean goneOnEnd) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "showAndAnimateControl() called with: "
|
||||
+ "drawableId = [" + drawableId + "], goneOnEnd = [" + goneOnEnd + "]");
|
||||
}
|
||||
if (controlViewAnimator != null && controlViewAnimator.isRunning()) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "showAndAnimateControl: controlViewAnimator.isRunning");
|
||||
}
|
||||
controlViewAnimator.end();
|
||||
}
|
||||
|
||||
if (drawableId == -1) {
|
||||
if (binding.controlAnimationView.getVisibility() == View.VISIBLE) {
|
||||
controlViewAnimator = ObjectAnimator.ofPropertyValuesHolder(
|
||||
binding.controlAnimationView,
|
||||
PropertyValuesHolder.ofFloat(View.ALPHA, 1.0f, 0.0f),
|
||||
PropertyValuesHolder.ofFloat(View.SCALE_X, 1.4f, 1.0f),
|
||||
PropertyValuesHolder.ofFloat(View.SCALE_Y, 1.4f, 1.0f)
|
||||
).setDuration(DEFAULT_CONTROLS_DURATION);
|
||||
controlViewAnimator.addListener(new AnimatorListenerAdapter() {
|
||||
@Override
|
||||
public void onAnimationEnd(final Animator animation) {
|
||||
binding.controlAnimationView.setVisibility(View.GONE);
|
||||
}
|
||||
});
|
||||
controlViewAnimator.start();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
final float scaleFrom = goneOnEnd ? 1f : 1f;
|
||||
final float scaleTo = goneOnEnd ? 1.8f : 1.4f;
|
||||
final float alphaFrom = goneOnEnd ? 1f : 0f;
|
||||
final float alphaTo = goneOnEnd ? 0f : 1f;
|
||||
|
||||
|
||||
controlViewAnimator = ObjectAnimator.ofPropertyValuesHolder(
|
||||
binding.controlAnimationView,
|
||||
PropertyValuesHolder.ofFloat(View.ALPHA, alphaFrom, alphaTo),
|
||||
PropertyValuesHolder.ofFloat(View.SCALE_X, scaleFrom, scaleTo),
|
||||
PropertyValuesHolder.ofFloat(View.SCALE_Y, scaleFrom, scaleTo)
|
||||
);
|
||||
controlViewAnimator.setDuration(goneOnEnd ? 1000 : 500);
|
||||
controlViewAnimator.addListener(new AnimatorListenerAdapter() {
|
||||
@Override
|
||||
public void onAnimationEnd(final Animator animation) {
|
||||
binding.controlAnimationView.setVisibility(goneOnEnd ? View.GONE : View.VISIBLE);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
binding.controlAnimationView.setVisibility(View.VISIBLE);
|
||||
binding.controlAnimationView.setImageDrawable(
|
||||
AppCompatResources.getDrawable(context, drawableId));
|
||||
controlViewAnimator.start();
|
||||
}
|
||||
|
||||
public void showControlsThenHide() {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "showControlsThenHide() called");
|
||||
@@ -1905,6 +1917,7 @@ public final class Player implements
|
||||
}
|
||||
|
||||
private void showHideShadow(final boolean show, final long duration) {
|
||||
animate(binding.playbackControlsShadow, show, duration, AnimationType.ALPHA, 0, null);
|
||||
animate(binding.playerTopShadow, show, duration, AnimationType.ALPHA, 0, null);
|
||||
animate(binding.playerBottomShadow, show, duration, AnimationType.ALPHA, 0, null);
|
||||
}
|
||||
@@ -2048,7 +2061,7 @@ public final class Player implements
|
||||
if (currentState == STATE_BLOCKED) {
|
||||
changeState(STATE_BUFFERING);
|
||||
}
|
||||
simpleExoPlayer.setMediaSource(mediaSource);
|
||||
simpleExoPlayer.setMediaSource(mediaSource, false);
|
||||
simpleExoPlayer.prepare();
|
||||
}
|
||||
|
||||
@@ -2102,8 +2115,8 @@ public final class Player implements
|
||||
startProgressLoop();
|
||||
}
|
||||
|
||||
controlsVisibilityHandler.removeCallbacksAndMessages(null);
|
||||
animate(binding.playbackControlRoot, false, DEFAULT_CONTROLS_DURATION);
|
||||
// if we are e.g. switching players, hide controls
|
||||
hideControls(DEFAULT_CONTROLS_DURATION, 0);
|
||||
|
||||
binding.playbackSeekBar.setEnabled(false);
|
||||
binding.playbackSeekBar.getThumb()
|
||||
@@ -2130,8 +2143,6 @@ public final class Player implements
|
||||
|
||||
updateStreamRelatedViews();
|
||||
|
||||
showAndAnimateControl(-1, true);
|
||||
|
||||
binding.playbackSeekBar.setEnabled(true);
|
||||
binding.playbackSeekBar.getThumb()
|
||||
.setColorFilter(new PorterDuffColorFilter(Color.RED, PorterDuff.Mode.SRC_IN));
|
||||
@@ -2179,18 +2190,21 @@ public final class Player implements
|
||||
stopProgressLoop();
|
||||
}
|
||||
|
||||
showControls(400);
|
||||
binding.loadingPanel.setVisibility(View.GONE);
|
||||
|
||||
animate(binding.playPauseButton, false, 80, AnimationType.SCALE_AND_ALPHA, 0,
|
||||
() -> {
|
||||
binding.playPauseButton.setImageResource(R.drawable.ic_play_arrow);
|
||||
animatePlayButtons(true, 200);
|
||||
if (!isQueueVisible) {
|
||||
binding.playPauseButton.requestFocus();
|
||||
}
|
||||
});
|
||||
// Don't let UI elements popup during double tap seeking. This state is entered sometimes
|
||||
// during seeking/loading. This if-else check ensures that the controls aren't popping up.
|
||||
if (!playerGestureListener.isDoubleTapping()) {
|
||||
showControls(400);
|
||||
binding.loadingPanel.setVisibility(View.GONE);
|
||||
|
||||
animate(binding.playPauseButton, false, 80, AnimationType.SCALE_AND_ALPHA, 0,
|
||||
() -> {
|
||||
binding.playPauseButton.setImageResource(R.drawable.ic_play_arrow);
|
||||
animatePlayButtons(true, 200);
|
||||
if (!isQueueVisible) {
|
||||
binding.playPauseButton.requestFocus();
|
||||
}
|
||||
});
|
||||
}
|
||||
changePopupWindowFlags(IDLE_WINDOW_FLAGS);
|
||||
|
||||
// Remove running notification when user does not want minimization to background or popup
|
||||
@@ -2208,7 +2222,6 @@ public final class Player implements
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "onPausedSeek() called");
|
||||
}
|
||||
showAndAnimateControl(-1, true);
|
||||
|
||||
animatePlayButtons(false, 100);
|
||||
binding.getRoot().setKeepScreenOn(true);
|
||||
@@ -2349,7 +2362,8 @@ public final class Player implements
|
||||
NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, false);
|
||||
}
|
||||
|
||||
private void setRepeatModeButton(final AppCompatImageButton imageButton, final int repeatMode) {
|
||||
private void setRepeatModeButton(final AppCompatImageButton imageButton,
|
||||
@RepeatMode final int repeatMode) {
|
||||
switch (repeatMode) {
|
||||
case REPEAT_MODE_OFF:
|
||||
imageButton.setImageResource(R.drawable.exo_controls_repeat_off);
|
||||
@@ -2363,7 +2377,7 @@ public final class Player implements
|
||||
}
|
||||
}
|
||||
|
||||
private void setShuffleButton(final ImageButton button, final boolean shuffled) {
|
||||
private void setShuffleButton(@NonNull final ImageButton button, final boolean shuffled) {
|
||||
button.setImageAlpha(shuffled ? 255 : 77);
|
||||
}
|
||||
//endregion
|
||||
@@ -2388,7 +2402,7 @@ public final class Player implements
|
||||
return !exoPlayerIsNull() && simpleExoPlayer.getVolume() == 0;
|
||||
}
|
||||
|
||||
private void setMuteButton(final ImageButton button, final boolean isMuted) {
|
||||
private void setMuteButton(@NonNull final ImageButton button, final boolean isMuted) {
|
||||
button.setImageDrawable(AppCompatResources.getDrawable(context, isMuted
|
||||
? R.drawable.ic_volume_off : R.drawable.ic_volume_up));
|
||||
}
|
||||
@@ -2435,7 +2449,9 @@ public final class Player implements
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPositionDiscontinuity(@DiscontinuityReason final int discontinuityReason) {
|
||||
public void onPositionDiscontinuity(
|
||||
final PositionInfo oldPosition, final PositionInfo newPosition,
|
||||
@DiscontinuityReason final int discontinuityReason) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "ExoPlayer - onPositionDiscontinuity() called with "
|
||||
+ "discontinuityReason = [" + discontinuityReason + "]");
|
||||
@@ -2447,7 +2463,8 @@ public final class Player implements
|
||||
// Refresh the playback if there is a transition to the next video
|
||||
final int newWindowIndex = simpleExoPlayer.getCurrentWindowIndex();
|
||||
switch (discontinuityReason) {
|
||||
case DISCONTINUITY_REASON_PERIOD_TRANSITION:
|
||||
case DISCONTINUITY_REASON_AUTO_TRANSITION:
|
||||
case DISCONTINUITY_REASON_REMOVE:
|
||||
// When player is in single repeat mode and a period transition occurs,
|
||||
// we need to register a view count here since no metadata has changed
|
||||
if (getRepeatMode() == REPEAT_MODE_ONE && newWindowIndex == playQueue.getIndex()) {
|
||||
@@ -2468,7 +2485,7 @@ public final class Player implements
|
||||
playQueue.setIndex(newWindowIndex);
|
||||
}
|
||||
break;
|
||||
case DISCONTINUITY_REASON_AD_INSERTION:
|
||||
case DISCONTINUITY_REASON_SKIP:
|
||||
break; // only makes Android Studio linter happy, as there are no ads
|
||||
}
|
||||
|
||||
@@ -2480,6 +2497,11 @@ public final class Player implements
|
||||
//TODO check if this causes black screen when switching to fullscreen
|
||||
animate(binding.surfaceForeground, false, DEFAULT_CONTROLS_DURATION);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCues(final List<Cue> cues) {
|
||||
binding.subtitleView.onCues(cues);
|
||||
}
|
||||
//endregion
|
||||
|
||||
|
||||
@@ -2501,85 +2523,84 @@ public final class Player implements
|
||||
* </ul>
|
||||
*
|
||||
* @see #processSourceError(IOException)
|
||||
* @see com.google.android.exoplayer2.Player.EventListener#onPlayerError(ExoPlaybackException)
|
||||
* @see com.google.android.exoplayer2.Player.Listener#onPlayerError(ExoPlaybackException)
|
||||
*/
|
||||
@Override
|
||||
public void onPlayerError(@NonNull final ExoPlaybackException error) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "ExoPlayer - onPlayerError() called with: " + "error = [" + error + "]");
|
||||
}
|
||||
if (errorToast != null) {
|
||||
errorToast.cancel();
|
||||
errorToast = null;
|
||||
}
|
||||
Log.e(TAG, "ExoPlayer - onPlayerError() called with:", error);
|
||||
|
||||
saveStreamProgressState();
|
||||
boolean isCatchableException = false;
|
||||
|
||||
switch (error.type) {
|
||||
case ExoPlaybackException.TYPE_SOURCE:
|
||||
processSourceError(error.getSourceException());
|
||||
showStreamError(error);
|
||||
isCatchableException = processSourceError(error.getSourceException());
|
||||
break;
|
||||
case ExoPlaybackException.TYPE_UNEXPECTED:
|
||||
showRecoverableError(error);
|
||||
setRecovery();
|
||||
reloadPlayQueueManager();
|
||||
break;
|
||||
case ExoPlaybackException.TYPE_REMOTE:
|
||||
case ExoPlaybackException.TYPE_RENDERER:
|
||||
default:
|
||||
showUnrecoverableError(error);
|
||||
onPlaybackShutdown();
|
||||
break;
|
||||
}
|
||||
|
||||
if (isCatchableException) {
|
||||
return;
|
||||
}
|
||||
|
||||
createErrorNotification(error);
|
||||
|
||||
if (fragmentListener != null) {
|
||||
fragmentListener.onPlayerError(error);
|
||||
}
|
||||
}
|
||||
|
||||
private void processSourceError(final IOException error) {
|
||||
if (exoPlayerIsNull() || playQueue == null) {
|
||||
return;
|
||||
private void createErrorNotification(@NonNull final ExoPlaybackException error) {
|
||||
final ErrorInfo errorInfo;
|
||||
if (currentMetadata == null) {
|
||||
errorInfo = new ErrorInfo(error, UserAction.PLAY_STREAM,
|
||||
"Player error[type=" + error.type + "] occurred, currentMetadata is null");
|
||||
} else {
|
||||
errorInfo = new ErrorInfo(error, UserAction.PLAY_STREAM,
|
||||
"Player error[type=" + error.type + "] occurred while playing "
|
||||
+ currentMetadata.getMetadata().getUrl(),
|
||||
currentMetadata.getMetadata());
|
||||
}
|
||||
ErrorUtil.createNotification(context, errorInfo);
|
||||
}
|
||||
|
||||
/**
|
||||
* Process an {@link IOException} returned by {@link ExoPlaybackException#getSourceException()}
|
||||
* for {@link ExoPlaybackException#TYPE_SOURCE} exceptions.
|
||||
*
|
||||
* <p>
|
||||
* This method sets the recovery position and sends an error message to the play queue if the
|
||||
* exception is not a {@link BehindLiveWindowException}.
|
||||
* </p>
|
||||
* @param error the source error which was thrown by ExoPlayer
|
||||
* @return whether the exception thrown is a {@link BehindLiveWindowException} ({@code false}
|
||||
* is always returned if ExoPlayer or the play queue is null)
|
||||
*/
|
||||
private boolean processSourceError(final IOException error) {
|
||||
if (exoPlayerIsNull() || playQueue == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
setRecovery();
|
||||
|
||||
if (error instanceof BehindLiveWindowException) {
|
||||
reloadPlayQueueManager();
|
||||
} else {
|
||||
playQueue.error();
|
||||
simpleExoPlayer.seekToDefaultPosition();
|
||||
simpleExoPlayer.prepare();
|
||||
// Inform the user that we are reloading the stream by switching to the buffering state
|
||||
onBuffering();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private void showStreamError(final Exception exception) {
|
||||
exception.printStackTrace();
|
||||
|
||||
if (errorToast == null) {
|
||||
errorToast = Toast
|
||||
.makeText(context, R.string.player_stream_failure, Toast.LENGTH_SHORT);
|
||||
errorToast.show();
|
||||
}
|
||||
}
|
||||
|
||||
private void showRecoverableError(final Exception exception) {
|
||||
exception.printStackTrace();
|
||||
|
||||
if (errorToast == null) {
|
||||
errorToast = Toast
|
||||
.makeText(context, R.string.player_recoverable_failure, Toast.LENGTH_SHORT);
|
||||
errorToast.show();
|
||||
}
|
||||
}
|
||||
|
||||
private void showUnrecoverableError(final Exception exception) {
|
||||
exception.printStackTrace();
|
||||
|
||||
if (errorToast != null) {
|
||||
errorToast.cancel();
|
||||
}
|
||||
errorToast = Toast
|
||||
.makeText(context, R.string.player_unrecoverable_failure, Toast.LENGTH_SHORT);
|
||||
errorToast.show();
|
||||
playQueue.error();
|
||||
return false;
|
||||
}
|
||||
//endregion
|
||||
|
||||
@@ -2856,7 +2877,6 @@ public final class Player implements
|
||||
}
|
||||
seekBy(retrieveSeekDurationFromPreferences(this));
|
||||
triggerProgressUpdate();
|
||||
showAndAnimateControl(R.drawable.ic_fast_forward, true);
|
||||
}
|
||||
|
||||
public void fastRewind() {
|
||||
@@ -2865,7 +2885,6 @@ public final class Player implements
|
||||
}
|
||||
seekBy(-retrieveSeekDurationFromPreferences(this));
|
||||
triggerProgressUpdate();
|
||||
showAndAnimateControl(R.drawable.ic_fast_rewind, true);
|
||||
}
|
||||
//endregion
|
||||
|
||||
@@ -2896,7 +2915,7 @@ public final class Player implements
|
||||
databaseUpdateDisposable
|
||||
.add(recordManager.saveStreamState(currentMetadata.getMetadata(), progressMillis)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.doOnError((e) -> {
|
||||
.doOnError(e -> {
|
||||
if (DEBUG) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
@@ -3406,7 +3425,7 @@ public final class Player implements
|
||||
playbackSpeedPopupMenu.setOnDismissListener(this);
|
||||
}
|
||||
|
||||
private void buildCaptionMenu(final List<String> availableLanguages) {
|
||||
private void buildCaptionMenu(@NonNull final List<String> availableLanguages) {
|
||||
if (captionPopupMenu == null) {
|
||||
return;
|
||||
}
|
||||
@@ -3474,7 +3493,7 @@ public final class Player implements
|
||||
* Called when an item of the quality selector or the playback speed selector is selected.
|
||||
*/
|
||||
@Override
|
||||
public boolean onMenuItemClick(final MenuItem menuItem) {
|
||||
public boolean onMenuItemClick(@NonNull final MenuItem menuItem) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "onMenuItemClick() called with: "
|
||||
+ "menuItem = [" + menuItem + "], "
|
||||
@@ -3511,7 +3530,7 @@ public final class Player implements
|
||||
* Called when some popup menu is dismissed.
|
||||
*/
|
||||
@Override
|
||||
public void onDismiss(final PopupMenu menu) {
|
||||
public void onDismiss(@Nullable final PopupMenu menu) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "onDismiss() called with: menu = [" + menu + "]");
|
||||
}
|
||||
@@ -3564,7 +3583,7 @@ public final class Player implements
|
||||
isSomePopupMenuVisible = true;
|
||||
}
|
||||
|
||||
private void setPlaybackQuality(final String quality) {
|
||||
private void setPlaybackQuality(@Nullable final String quality) {
|
||||
videoResolver.setPlaybackQuality(quality);
|
||||
}
|
||||
//endregion
|
||||
@@ -3588,7 +3607,7 @@ public final class Player implements
|
||||
final int minimumLength = Math.min(metrics.heightPixels, metrics.widthPixels);
|
||||
final float captionRatioInverse = 20f + 4f * (1.0f - captionScale);
|
||||
binding.subtitleView.setFixedTextSize(
|
||||
TypedValue.COMPLEX_UNIT_PX, (float) minimumLength / captionRatioInverse);
|
||||
TypedValue.COMPLEX_UNIT_PX, minimumLength / captionRatioInverse);
|
||||
}
|
||||
binding.subtitleView.setApplyEmbeddedStyles(captionStyle == CaptionStyleCompat.DEFAULT);
|
||||
binding.subtitleView.setStyle(captionStyle);
|
||||
@@ -3865,19 +3884,17 @@ public final class Player implements
|
||||
}
|
||||
|
||||
@Override // exoplayer listener
|
||||
public void onVideoSizeChanged(final int width, final int height,
|
||||
final int unappliedRotationDegrees,
|
||||
final float pixelWidthHeightRatio) {
|
||||
public void onVideoSizeChanged(@NonNull final VideoSize videoSize) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "onVideoSizeChanged() called with: "
|
||||
+ "width / height = [" + width + " / " + height
|
||||
+ " = " + (((float) width) / height) + "], "
|
||||
+ "unappliedRotationDegrees = [" + unappliedRotationDegrees + "], "
|
||||
+ "pixelWidthHeightRatio = [" + pixelWidthHeightRatio + "]");
|
||||
+ "width / height = [" + videoSize.width + " / " + videoSize.height
|
||||
+ " = " + (((float) videoSize.width) / videoSize.height) + "], "
|
||||
+ "unappliedRotationDegrees = [" + videoSize.unappliedRotationDegrees + "], "
|
||||
+ "pixelWidthHeightRatio = [" + videoSize.pixelWidthHeightRatio + "]");
|
||||
}
|
||||
|
||||
binding.surfaceView.setAspectRatio(((float) width) / height);
|
||||
isVerticalVideo = width < height;
|
||||
binding.surfaceView.setAspectRatio(((float) videoSize.width) / videoSize.height);
|
||||
isVerticalVideo = videoSize.width < videoSize.height;
|
||||
|
||||
if (globalScreenOrientationLocked(context)
|
||||
&& isFullscreen
|
||||
@@ -3981,7 +3998,7 @@ public final class Player implements
|
||||
}
|
||||
}
|
||||
|
||||
private int distanceFromCloseButton(final MotionEvent popupMotionEvent) {
|
||||
private int distanceFromCloseButton(@NonNull final MotionEvent popupMotionEvent) {
|
||||
final int closeOverlayButtonX = closeOverlayBinding.closeButton.getLeft()
|
||||
+ closeOverlayBinding.closeButton.getWidth() / 2;
|
||||
final int closeOverlayButtonY = closeOverlayBinding.closeButton.getTop()
|
||||
@@ -4000,7 +4017,7 @@ public final class Player implements
|
||||
return buttonRadius * 1.2f;
|
||||
}
|
||||
|
||||
public boolean isInsideClosingRadius(final MotionEvent popupMotionEvent) {
|
||||
public boolean isInsideClosingRadius(@NonNull final MotionEvent popupMotionEvent) {
|
||||
return distanceFromCloseButton(popupMotionEvent) <= getClosingRadius();
|
||||
}
|
||||
//endregion
|
||||
@@ -4120,6 +4137,7 @@ public final class Player implements
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public AppCompatActivity getParentActivity() {
|
||||
// ! instanceof ViewGroup means that view was added via windowManager for Popup
|
||||
if (binding == null || !(binding.getRoot().getParent() instanceof ViewGroup)) {
|
||||
@@ -4182,8 +4200,7 @@ public final class Player implements
|
||||
} catch (@NonNull final IndexOutOfBoundsException e) {
|
||||
// Why would this even happen =(... but lets log it anyway, better safe than sorry
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "player.isCurrentWindowDynamic() failed: " + e.getMessage());
|
||||
e.printStackTrace();
|
||||
Log.d(TAG, "player.isCurrentWindowDynamic() failed: ", e);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -4222,6 +4239,7 @@ public final class Player implements
|
||||
}
|
||||
|
||||
|
||||
@Nullable
|
||||
public PlayQueue getPlayQueue() {
|
||||
return playQueue;
|
||||
}
|
||||
@@ -4299,6 +4317,10 @@ public final class Player implements
|
||||
return binding.currentDisplaySeek;
|
||||
}
|
||||
|
||||
public PlayerFastSeekOverlay getFastSeekOverlay() {
|
||||
return binding.fastSeekOverlay;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public WindowManager.LayoutParams getPopupLayoutParams() {
|
||||
return popupLayoutParams;
|
||||
|
||||
@@ -411,7 +411,7 @@ abstract class BasePlayerGestureListener(
|
||||
var doubleTapControls: DoubleTapListener? = null
|
||||
private set
|
||||
|
||||
val isDoubleTapEnabled: Boolean
|
||||
private val isDoubleTapEnabled: Boolean
|
||||
get() = doubleTapDelay > 0
|
||||
|
||||
var isDoubleTapping = false
|
||||
@@ -459,10 +459,6 @@ abstract class BasePlayerGestureListener(
|
||||
doubleTapControls?.onDoubleTapFinished()
|
||||
}
|
||||
|
||||
fun enableMultiDoubleTap(enable: Boolean) = apply {
|
||||
doubleTapDelay = if (enable) DOUBLE_TAP_DELAY else 0
|
||||
}
|
||||
|
||||
// ///////////////////////////////////////////////////////////////////
|
||||
// Utils
|
||||
// ///////////////////////////////////////////////////////////////////
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
package org.schabi.newpipe.player.event;
|
||||
|
||||
import static org.schabi.newpipe.ktx.AnimationType.ALPHA;
|
||||
import static org.schabi.newpipe.ktx.AnimationType.SCALE_AND_ALPHA;
|
||||
import static org.schabi.newpipe.ktx.ViewUtils.animate;
|
||||
import static org.schabi.newpipe.player.Player.DEFAULT_CONTROLS_DURATION;
|
||||
import static org.schabi.newpipe.player.Player.DEFAULT_CONTROLS_HIDE_TIME;
|
||||
import static org.schabi.newpipe.player.Player.STATE_PLAYING;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.util.Log;
|
||||
import android.view.MotionEvent;
|
||||
@@ -8,22 +15,15 @@ import android.view.Window;
|
||||
import android.view.WindowManager;
|
||||
import android.widget.ProgressBar;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.content.res.AppCompatResources;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.schabi.newpipe.MainActivity;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.player.MainPlayer;
|
||||
import org.schabi.newpipe.player.Player;
|
||||
import org.schabi.newpipe.player.helper.PlayerHelper;
|
||||
|
||||
import static org.schabi.newpipe.ktx.AnimationType.ALPHA;
|
||||
import static org.schabi.newpipe.ktx.AnimationType.SCALE_AND_ALPHA;
|
||||
import static org.schabi.newpipe.ktx.ViewUtils.animate;
|
||||
import static org.schabi.newpipe.player.Player.DEFAULT_CONTROLS_DURATION;
|
||||
import static org.schabi.newpipe.player.Player.DEFAULT_CONTROLS_HIDE_TIME;
|
||||
import static org.schabi.newpipe.player.Player.STATE_PLAYING;
|
||||
|
||||
/**
|
||||
* GestureListener for the player
|
||||
*
|
||||
@@ -45,8 +45,8 @@ public class PlayerGestureListener
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDoubleTap(@NotNull final MotionEvent event,
|
||||
@NotNull final DisplayPortion portion) {
|
||||
public void onDoubleTap(@NonNull final MotionEvent event,
|
||||
@NonNull final DisplayPortion portion) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "onDoubleTap called with playerType = ["
|
||||
+ player.getPlayerType() + "], portion = [" + portion + "]");
|
||||
@@ -55,54 +55,46 @@ public class PlayerGestureListener
|
||||
player.hideControls(0, 0);
|
||||
}
|
||||
|
||||
if (portion == DisplayPortion.LEFT) {
|
||||
player.fastRewind();
|
||||
if (portion == DisplayPortion.LEFT || portion == DisplayPortion.RIGHT) {
|
||||
startMultiDoubleTap(event);
|
||||
} else if (portion == DisplayPortion.MIDDLE) {
|
||||
player.playPause();
|
||||
} else if (portion == DisplayPortion.RIGHT) {
|
||||
player.fastForward();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSingleTap(@NotNull final MainPlayer.PlayerType playerType) {
|
||||
public void onSingleTap(@NonNull final MainPlayer.PlayerType playerType) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "onSingleTap called with playerType = [" + player.getPlayerType() + "]");
|
||||
}
|
||||
if (playerType == MainPlayer.PlayerType.POPUP) {
|
||||
|
||||
if (player.isControlsVisible()) {
|
||||
player.hideControls(100, 100);
|
||||
} else {
|
||||
player.getPlayPauseButton().requestFocus();
|
||||
player.showControlsThenHide();
|
||||
}
|
||||
if (player.isControlsVisible()) {
|
||||
player.hideControls(150, 0);
|
||||
return;
|
||||
}
|
||||
// -- Controls are not visible --
|
||||
|
||||
} else /* playerType == MainPlayer.PlayerType.VIDEO */ {
|
||||
|
||||
if (player.isControlsVisible()) {
|
||||
player.hideControls(150, 0);
|
||||
} else {
|
||||
if (player.getCurrentState() == Player.STATE_COMPLETED) {
|
||||
player.showControls(0);
|
||||
} else {
|
||||
player.showControlsThenHide();
|
||||
}
|
||||
}
|
||||
// When player is completed show controls and don't hide them later
|
||||
if (player.getCurrentState() == Player.STATE_COMPLETED) {
|
||||
player.showControls(0);
|
||||
} else {
|
||||
player.showControlsThenHide();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onScroll(@NotNull final MainPlayer.PlayerType playerType,
|
||||
@NotNull final DisplayPortion portion,
|
||||
@NotNull final MotionEvent initialEvent,
|
||||
@NotNull final MotionEvent movingEvent,
|
||||
public void onScroll(@NonNull final MainPlayer.PlayerType playerType,
|
||||
@NonNull final DisplayPortion portion,
|
||||
@NonNull final MotionEvent initialEvent,
|
||||
@NonNull final MotionEvent movingEvent,
|
||||
final float distanceX, final float distanceY) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "onScroll called with playerType = ["
|
||||
+ player.getPlayerType() + "], portion = [" + portion + "]");
|
||||
}
|
||||
if (playerType == MainPlayer.PlayerType.VIDEO) {
|
||||
|
||||
// -- Brightness and Volume control --
|
||||
final boolean isBrightnessGestureEnabled =
|
||||
PlayerHelper.isBrightnessGestureEnabled(service);
|
||||
final boolean isVolumeGestureEnabled = PlayerHelper.isVolumeGestureEnabled(service);
|
||||
@@ -121,15 +113,14 @@ public class PlayerGestureListener
|
||||
}
|
||||
|
||||
} else /* MainPlayer.PlayerType.POPUP */ {
|
||||
|
||||
// -- Determine if the ClosingOverlayView (red X) has to be shown or hidden --
|
||||
final View closingOverlayView = player.getClosingOverlayView();
|
||||
if (player.isInsideClosingRadius(movingEvent)) {
|
||||
if (closingOverlayView.getVisibility() == View.GONE) {
|
||||
animate(closingOverlayView, true, 200);
|
||||
}
|
||||
} else {
|
||||
if (closingOverlayView.getVisibility() == View.VISIBLE) {
|
||||
animate(closingOverlayView, false, 200);
|
||||
}
|
||||
final boolean showClosingOverlayView = player.isInsideClosingRadius(movingEvent);
|
||||
// Check if an view is in expected state and if not animate it into the correct state
|
||||
final int expectedVisibility = showClosingOverlayView ? View.VISIBLE : View.GONE;
|
||||
if (closingOverlayView.getVisibility() != expectedVisibility) {
|
||||
animate(closingOverlayView, showClosingOverlayView, 200);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -204,17 +195,18 @@ public class PlayerGestureListener
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onScrollEnd(@NotNull final MainPlayer.PlayerType playerType,
|
||||
@NotNull final MotionEvent event) {
|
||||
public void onScrollEnd(@NonNull final MainPlayer.PlayerType playerType,
|
||||
@NonNull final MotionEvent event) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "onScrollEnd called with playerType = ["
|
||||
+ player.getPlayerType() + "]");
|
||||
}
|
||||
if (playerType == MainPlayer.PlayerType.VIDEO) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "onScrollEnd() called");
|
||||
}
|
||||
|
||||
if (player.isControlsVisible() && player.getCurrentState() == STATE_PLAYING) {
|
||||
player.hideControls(DEFAULT_CONTROLS_DURATION, DEFAULT_CONTROLS_HIDE_TIME);
|
||||
}
|
||||
|
||||
if (playerType == MainPlayer.PlayerType.VIDEO) {
|
||||
if (player.getVolumeRelativeLayout().getVisibility() == View.VISIBLE) {
|
||||
animate(player.getVolumeRelativeLayout(), false, 200, SCALE_AND_ALPHA,
|
||||
200);
|
||||
@@ -223,15 +215,7 @@ public class PlayerGestureListener
|
||||
animate(player.getBrightnessRelativeLayout(), false, 200, SCALE_AND_ALPHA,
|
||||
200);
|
||||
}
|
||||
|
||||
if (player.isControlsVisible() && player.getCurrentState() == STATE_PLAYING) {
|
||||
player.hideControls(DEFAULT_CONTROLS_DURATION, DEFAULT_CONTROLS_HIDE_TIME);
|
||||
}
|
||||
} else {
|
||||
if (player.isControlsVisible() && player.getCurrentState() == STATE_PLAYING) {
|
||||
player.hideControls(DEFAULT_CONTROLS_DURATION, DEFAULT_CONTROLS_HIDE_TIME);
|
||||
}
|
||||
|
||||
} else /* Popup-Player */ {
|
||||
if (player.isInsideClosingRadius(event)) {
|
||||
player.closePopup();
|
||||
} else if (!player.isPopupClosing()) {
|
||||
@@ -246,10 +230,10 @@ public class PlayerGestureListener
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "onPopupResizingStart called");
|
||||
}
|
||||
player.showAndAnimateControl(-1, true);
|
||||
player.getLoadingPanel().setVisibility(View.GONE);
|
||||
|
||||
player.hideControls(0, 0);
|
||||
animate(player.getFastSeekOverlay(), false, 0);
|
||||
animate(player.getCurrentDisplaySeek(), false, 0, ALPHA, 0);
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,6 @@ import androidx.media.AudioManagerCompat;
|
||||
|
||||
import com.google.android.exoplayer2.SimpleExoPlayer;
|
||||
import com.google.android.exoplayer2.analytics.AnalyticsListener;
|
||||
import com.google.android.exoplayer2.decoder.DecoderCounters;
|
||||
|
||||
public class AudioReactor implements AudioManager.OnAudioFocusChangeListener, AnalyticsListener {
|
||||
|
||||
@@ -150,15 +149,9 @@ public class AudioReactor implements AudioManager.OnAudioFocusChangeListener, An
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public void onAudioSessionId(final EventTime eventTime, final int audioSessionId) {
|
||||
public void onAudioSessionIdChanged(final EventTime eventTime, final int audioSessionId) {
|
||||
notifyAudioSessionUpdate(true, audioSessionId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAudioDisabled(final EventTime eventTime, final DecoderCounters counters) {
|
||||
notifyAudioSessionUpdate(false, player.getAudioSessionId());
|
||||
}
|
||||
|
||||
private void notifyAudioSessionUpdate(final boolean active, final int audioSessionId) {
|
||||
if (!PlayerHelper.isUsingDSP()) {
|
||||
return;
|
||||
|
||||
@@ -1,81 +1,28 @@
|
||||
package org.schabi.newpipe.player.helper;
|
||||
|
||||
import com.google.android.exoplayer2.DefaultLoadControl;
|
||||
import com.google.android.exoplayer2.LoadControl;
|
||||
import com.google.android.exoplayer2.Renderer;
|
||||
import com.google.android.exoplayer2.source.TrackGroupArray;
|
||||
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
|
||||
import com.google.android.exoplayer2.upstream.Allocator;
|
||||
|
||||
public class LoadController implements LoadControl {
|
||||
public class LoadController extends DefaultLoadControl {
|
||||
|
||||
public static final String TAG = "LoadController";
|
||||
|
||||
private final long initialPlaybackBufferUs;
|
||||
private final LoadControl internalLoadControl;
|
||||
private boolean preloadingEnabled = true;
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Default Load Control
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
public LoadController() {
|
||||
this(PlayerHelper.getPlaybackStartBufferMs());
|
||||
}
|
||||
|
||||
private LoadController(final int initialPlaybackBufferMs) {
|
||||
this.initialPlaybackBufferUs = initialPlaybackBufferMs * 1000;
|
||||
|
||||
final DefaultLoadControl.Builder builder = new DefaultLoadControl.Builder();
|
||||
builder.setBufferDurationsMs(
|
||||
DefaultLoadControl.DEFAULT_MIN_BUFFER_MS,
|
||||
DefaultLoadControl.DEFAULT_MAX_BUFFER_MS,
|
||||
initialPlaybackBufferMs,
|
||||
DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS);
|
||||
internalLoadControl = builder.build();
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Custom behaviours
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public void onPrepared() {
|
||||
preloadingEnabled = true;
|
||||
internalLoadControl.onPrepared();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTracksSelected(final Renderer[] renderers, final TrackGroupArray trackGroups,
|
||||
final TrackSelectionArray trackSelections) {
|
||||
internalLoadControl.onTracksSelected(renderers, trackGroups, trackSelections);
|
||||
super.onPrepared();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStopped() {
|
||||
preloadingEnabled = true;
|
||||
internalLoadControl.onStopped();
|
||||
super.onStopped();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onReleased() {
|
||||
preloadingEnabled = true;
|
||||
internalLoadControl.onReleased();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Allocator getAllocator() {
|
||||
return internalLoadControl.getAllocator();
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getBackBufferDurationUs() {
|
||||
return internalLoadControl.getBackBufferDurationUs();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean retainBackBufferFromKeyframe() {
|
||||
return internalLoadControl.retainBackBufferFromKeyframe();
|
||||
super.onReleased();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -85,20 +32,10 @@ public class LoadController implements LoadControl {
|
||||
if (!preloadingEnabled) {
|
||||
return false;
|
||||
}
|
||||
return internalLoadControl.shouldContinueLoading(
|
||||
return super.shouldContinueLoading(
|
||||
playbackPositionUs, bufferedDurationUs, playbackSpeed);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldStartPlayback(final long bufferedDurationUs, final float playbackSpeed,
|
||||
final boolean rebuffering) {
|
||||
final boolean isInitialPlaybackBufferFilled
|
||||
= bufferedDurationUs >= this.initialPlaybackBufferUs * playbackSpeed;
|
||||
final boolean isInternalStartingPlayback = internalLoadControl
|
||||
.shouldStartPlayback(bufferedDurationUs, playbackSpeed, rebuffering);
|
||||
return isInitialPlaybackBufferFilled || isInternalStartingPlayback;
|
||||
}
|
||||
|
||||
public void disablePreloadingOfCurrentTrack() {
|
||||
preloadingEnabled = false;
|
||||
}
|
||||
|
||||
@@ -179,9 +179,7 @@ public class MediaSessionManager {
|
||||
// If we got an album art check if the current set AlbumArt is null
|
||||
if (optAlbumArt.isPresent() && getMetadataAlbumArt() == null) {
|
||||
if (DEBUG) {
|
||||
if (getMetadataAlbumArt() == null) {
|
||||
Log.d(TAG, "N_getMetadataAlbumArt: thumb == null");
|
||||
}
|
||||
Log.d(TAG, "N_getMetadataAlbumArt: thumb == null");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -191,16 +189,19 @@ public class MediaSessionManager {
|
||||
}
|
||||
|
||||
|
||||
@Nullable
|
||||
private Bitmap getMetadataAlbumArt() {
|
||||
return mediaSession.getController().getMetadata()
|
||||
.getBitmap(MediaMetadataCompat.METADATA_KEY_ALBUM_ART);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private String getMetadataTitle() {
|
||||
return mediaSession.getController().getMetadata()
|
||||
.getString(MediaMetadataCompat.METADATA_KEY_TITLE);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private String getMetadataArtist() {
|
||||
return mediaSession.getController().getMetadata()
|
||||
.getString(MediaMetadataCompat.METADATA_KEY_ARTIST);
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
package org.schabi.newpipe.player.helper;
|
||||
|
||||
import static org.schabi.newpipe.player.Player.DEBUG;
|
||||
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
|
||||
|
||||
import android.app.Dialog;
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
@@ -18,9 +21,6 @@ import androidx.preference.PreferenceManager;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.util.SliderStrategy;
|
||||
|
||||
import static org.schabi.newpipe.player.Player.DEBUG;
|
||||
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
|
||||
|
||||
public class PlaybackParameterDialog extends DialogFragment {
|
||||
// Minimum allowable range in ExoPlayer
|
||||
private static final double MINIMUM_PLAYBACK_VALUE = 0.10f;
|
||||
@@ -157,7 +157,6 @@ public class PlaybackParameterDialog extends DialogFragment {
|
||||
setupControlViews(view);
|
||||
|
||||
final AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(requireActivity())
|
||||
.setTitle(R.string.playback_speed_control)
|
||||
.setView(view)
|
||||
.setCancelable(true)
|
||||
.setNegativeButton(R.string.cancel, (dialogInterface, i) ->
|
||||
|
||||
@@ -9,6 +9,7 @@ import com.google.android.exoplayer2.source.SingleSampleMediaSource;
|
||||
import com.google.android.exoplayer2.source.dash.DashMediaSource;
|
||||
import com.google.android.exoplayer2.source.dash.DefaultDashChunkSource;
|
||||
import com.google.android.exoplayer2.source.hls.HlsMediaSource;
|
||||
import com.google.android.exoplayer2.source.hls.playlist.DefaultHlsPlaylistTracker;
|
||||
import com.google.android.exoplayer2.source.smoothstreaming.DefaultSsChunkSource;
|
||||
import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource;
|
||||
import com.google.android.exoplayer2.upstream.DataSource;
|
||||
@@ -17,9 +18,18 @@ import com.google.android.exoplayer2.upstream.DefaultLoadErrorHandlingPolicy;
|
||||
import com.google.android.exoplayer2.upstream.TransferListener;
|
||||
|
||||
public class PlayerDataSource {
|
||||
|
||||
public static final int LIVE_STREAM_EDGE_GAP_MILLIS = 10000;
|
||||
|
||||
/**
|
||||
* An approximately 4.3 times greater value than the
|
||||
* {@link DefaultHlsPlaylistTracker#DEFAULT_PLAYLIST_STUCK_TARGET_DURATION_COEFFICIENT default}
|
||||
* to ensure that (very) low latency livestreams which got stuck for a moment don't crash too
|
||||
* early.
|
||||
*/
|
||||
private static final double PLAYLIST_STUCK_TARGET_DURATION_COEFFICIENT = 15;
|
||||
private static final int MANIFEST_MINIMUM_RETRY = 5;
|
||||
private static final int EXTRACTOR_MINIMUM_RETRY = Integer.MAX_VALUE;
|
||||
private static final int LIVE_STREAM_EDGE_GAP_MILLIS = 10000;
|
||||
|
||||
private final DataSource.Factory cacheDataSourceFactory;
|
||||
private final DataSource.Factory cachelessDataSourceFactory;
|
||||
@@ -32,8 +42,10 @@ public class PlayerDataSource {
|
||||
}
|
||||
|
||||
public SsMediaSource.Factory getLiveSsMediaSourceFactory() {
|
||||
return new SsMediaSource.Factory(new DefaultSsChunkSource.Factory(
|
||||
cachelessDataSourceFactory), cachelessDataSourceFactory)
|
||||
return new SsMediaSource.Factory(
|
||||
new DefaultSsChunkSource.Factory(cachelessDataSourceFactory),
|
||||
cachelessDataSourceFactory
|
||||
)
|
||||
.setLoadErrorHandlingPolicy(
|
||||
new DefaultLoadErrorHandlingPolicy(MANIFEST_MINIMUM_RETRY))
|
||||
.setLivePresentationDelayMs(LIVE_STREAM_EDGE_GAP_MILLIS);
|
||||
@@ -42,21 +54,28 @@ public class PlayerDataSource {
|
||||
public HlsMediaSource.Factory getLiveHlsMediaSourceFactory() {
|
||||
return new HlsMediaSource.Factory(cachelessDataSourceFactory)
|
||||
.setAllowChunklessPreparation(true)
|
||||
.setLoadErrorHandlingPolicy(new DefaultLoadErrorHandlingPolicy(
|
||||
MANIFEST_MINIMUM_RETRY))
|
||||
.setPlaylistTrackerFactory((dataSourceFactory, loadErrorHandlingPolicy,
|
||||
playlistParserFactory) ->
|
||||
new DefaultHlsPlaylistTracker(dataSourceFactory, loadErrorHandlingPolicy,
|
||||
playlistParserFactory, PLAYLIST_STUCK_TARGET_DURATION_COEFFICIENT)
|
||||
);
|
||||
}
|
||||
|
||||
public DashMediaSource.Factory getLiveDashMediaSourceFactory() {
|
||||
return new DashMediaSource.Factory(
|
||||
getDefaultDashChunkSourceFactory(cachelessDataSourceFactory),
|
||||
cachelessDataSourceFactory
|
||||
)
|
||||
.setLoadErrorHandlingPolicy(
|
||||
new DefaultLoadErrorHandlingPolicy(MANIFEST_MINIMUM_RETRY));
|
||||
}
|
||||
|
||||
public DashMediaSource.Factory getLiveDashMediaSourceFactory() {
|
||||
return new DashMediaSource.Factory(new DefaultDashChunkSource.Factory(
|
||||
cachelessDataSourceFactory), cachelessDataSourceFactory)
|
||||
.setLoadErrorHandlingPolicy(
|
||||
new DefaultLoadErrorHandlingPolicy(MANIFEST_MINIMUM_RETRY))
|
||||
.setLivePresentationDelayMs(LIVE_STREAM_EDGE_GAP_MILLIS, true);
|
||||
}
|
||||
|
||||
public SsMediaSource.Factory getSsMediaSourceFactory() {
|
||||
return new SsMediaSource.Factory(new DefaultSsChunkSource.Factory(
|
||||
cacheDataSourceFactory), cacheDataSourceFactory);
|
||||
private DefaultDashChunkSource.Factory getDefaultDashChunkSourceFactory(
|
||||
final DataSource.Factory dataSourceFactory
|
||||
) {
|
||||
return new DefaultDashChunkSource.Factory(dataSourceFactory);
|
||||
}
|
||||
|
||||
public HlsMediaSource.Factory getHlsMediaSourceFactory() {
|
||||
@@ -64,8 +83,10 @@ public class PlayerDataSource {
|
||||
}
|
||||
|
||||
public DashMediaSource.Factory getDashMediaSourceFactory() {
|
||||
return new DashMediaSource.Factory(new DefaultDashChunkSource.Factory(
|
||||
cacheDataSourceFactory), cacheDataSourceFactory);
|
||||
return new DashMediaSource.Factory(
|
||||
getDefaultDashChunkSourceFactory(cacheDataSourceFactory),
|
||||
cacheDataSourceFactory
|
||||
);
|
||||
}
|
||||
|
||||
public ProgressiveMediaSource.Factory getExtractorMediaSourceFactory() {
|
||||
@@ -74,11 +95,6 @@ public class PlayerDataSource {
|
||||
new DefaultLoadErrorHandlingPolicy(EXTRACTOR_MINIMUM_RETRY));
|
||||
}
|
||||
|
||||
public ProgressiveMediaSource.Factory getExtractorMediaSourceFactory(
|
||||
@NonNull final String key) {
|
||||
return getExtractorMediaSourceFactory().setCustomCacheKey(key);
|
||||
}
|
||||
|
||||
public SingleSampleMediaSource.Factory getSampleMediaSourceFactory() {
|
||||
return new SingleSampleMediaSource.Factory(cacheDataSourceFactory);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,18 @@
|
||||
package org.schabi.newpipe.player.helper;
|
||||
|
||||
import static com.google.android.exoplayer2.Player.REPEAT_MODE_ALL;
|
||||
import static com.google.android.exoplayer2.Player.REPEAT_MODE_OFF;
|
||||
import static com.google.android.exoplayer2.Player.REPEAT_MODE_ONE;
|
||||
import static org.schabi.newpipe.player.Player.IDLE_WINDOW_FLAGS;
|
||||
import static org.schabi.newpipe.player.Player.PLAYER_TYPE;
|
||||
import static org.schabi.newpipe.player.helper.PlayerHelper.AutoplayType.AUTOPLAY_TYPE_ALWAYS;
|
||||
import static org.schabi.newpipe.player.helper.PlayerHelper.AutoplayType.AUTOPLAY_TYPE_NEVER;
|
||||
import static org.schabi.newpipe.player.helper.PlayerHelper.AutoplayType.AUTOPLAY_TYPE_WIFI;
|
||||
import static org.schabi.newpipe.player.helper.PlayerHelper.MinimizeMode.MINIMIZE_ON_EXIT_MODE_BACKGROUND;
|
||||
import static org.schabi.newpipe.player.helper.PlayerHelper.MinimizeMode.MINIMIZE_ON_EXIT_MODE_NONE;
|
||||
import static org.schabi.newpipe.player.helper.PlayerHelper.MinimizeMode.MINIMIZE_ON_EXIT_MODE_POPUP;
|
||||
import static java.lang.annotation.RetentionPolicy.SOURCE;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
@@ -21,11 +34,11 @@ import androidx.preference.PreferenceManager;
|
||||
import com.google.android.exoplayer2.PlaybackParameters;
|
||||
import com.google.android.exoplayer2.Player.RepeatMode;
|
||||
import com.google.android.exoplayer2.SeekParameters;
|
||||
import com.google.android.exoplayer2.text.CaptionStyleCompat;
|
||||
import com.google.android.exoplayer2.trackselection.AdaptiveTrackSelection;
|
||||
import com.google.android.exoplayer2.trackselection.TrackSelection;
|
||||
import com.google.android.exoplayer2.trackselection.ExoTrackSelection;
|
||||
import com.google.android.exoplayer2.ui.AspectRatioFrameLayout;
|
||||
import com.google.android.exoplayer2.ui.AspectRatioFrameLayout.ResizeMode;
|
||||
import com.google.android.exoplayer2.ui.CaptionStyleCompat;
|
||||
import com.google.android.exoplayer2.util.MimeTypes;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
@@ -57,19 +70,6 @@ import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static com.google.android.exoplayer2.Player.REPEAT_MODE_ALL;
|
||||
import static com.google.android.exoplayer2.Player.REPEAT_MODE_OFF;
|
||||
import static com.google.android.exoplayer2.Player.REPEAT_MODE_ONE;
|
||||
import static java.lang.annotation.RetentionPolicy.SOURCE;
|
||||
import static org.schabi.newpipe.player.Player.IDLE_WINDOW_FLAGS;
|
||||
import static org.schabi.newpipe.player.Player.PLAYER_TYPE;
|
||||
import static org.schabi.newpipe.player.helper.PlayerHelper.AutoplayType.AUTOPLAY_TYPE_ALWAYS;
|
||||
import static org.schabi.newpipe.player.helper.PlayerHelper.AutoplayType.AUTOPLAY_TYPE_NEVER;
|
||||
import static org.schabi.newpipe.player.helper.PlayerHelper.AutoplayType.AUTOPLAY_TYPE_WIFI;
|
||||
import static org.schabi.newpipe.player.helper.PlayerHelper.MinimizeMode.MINIMIZE_ON_EXIT_MODE_BACKGROUND;
|
||||
import static org.schabi.newpipe.player.helper.PlayerHelper.MinimizeMode.MINIMIZE_ON_EXIT_MODE_NONE;
|
||||
import static org.schabi.newpipe.player.helper.PlayerHelper.MinimizeMode.MINIMIZE_ON_EXIT_MODE_POPUP;
|
||||
|
||||
public final class PlayerHelper {
|
||||
private static final StringBuilder STRING_BUILDER = new StringBuilder();
|
||||
private static final Formatter STRING_FORMATTER
|
||||
@@ -305,14 +305,7 @@ public final class PlayerHelper {
|
||||
return 2 * 1024 * 1024L; // ExoPlayer CacheDataSink.MIN_RECOMMENDED_FRAGMENT_SIZE
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the number of milliseconds the player buffers for before starting playback
|
||||
*/
|
||||
public static int getPlaybackStartBufferMs() {
|
||||
return 500;
|
||||
}
|
||||
|
||||
public static TrackSelection.Factory getQualitySelector() {
|
||||
public static ExoTrackSelection.Factory getQualitySelector() {
|
||||
return new AdaptiveTrackSelection.Factory(
|
||||
1000,
|
||||
AdaptiveTrackSelection.DEFAULT_MAX_DURATION_FOR_QUALITY_DECREASE_MS,
|
||||
|
||||
@@ -35,15 +35,15 @@ public final class PlayerHolder {
|
||||
return PlayerHolder.instance;
|
||||
}
|
||||
|
||||
private final boolean DEBUG = MainActivity.DEBUG;
|
||||
private final String TAG = PlayerHolder.class.getSimpleName();
|
||||
private static final boolean DEBUG = MainActivity.DEBUG;
|
||||
private static final String TAG = PlayerHolder.class.getSimpleName();
|
||||
|
||||
private PlayerServiceExtendedEventListener listener;
|
||||
@Nullable private PlayerServiceExtendedEventListener listener;
|
||||
|
||||
private final PlayerServiceConnection serviceConnection = new PlayerServiceConnection();
|
||||
public boolean bound;
|
||||
private MainPlayer playerService;
|
||||
private Player player;
|
||||
private boolean bound;
|
||||
@Nullable private MainPlayer playerService;
|
||||
@Nullable private Player player;
|
||||
|
||||
/**
|
||||
* Returns the current {@link MainPlayer.PlayerType} of the {@link MainPlayer} service,
|
||||
@@ -70,8 +70,25 @@ public final class PlayerHolder {
|
||||
return player != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Use this method to only allow the user to manipulate the play queue (e.g. by enqueueing via
|
||||
* the stream long press menu) when there actually is a play queue to manipulate.
|
||||
* @return true only if the player is open and its play queue is ready (i.e. it is not null)
|
||||
*/
|
||||
public boolean isPlayQueueReady() {
|
||||
return player != null && player.getPlayQueue() != null;
|
||||
}
|
||||
|
||||
public boolean isBound() {
|
||||
return bound;
|
||||
}
|
||||
|
||||
public int getQueueSize() {
|
||||
return isPlayerOpen() ? player.getPlayQueue().size() : 0;
|
||||
if (player == null || player.getPlayQueue() == null) {
|
||||
// player play queue might be null e.g. while player is starting
|
||||
return 0;
|
||||
}
|
||||
return player.getPlayQueue().size();
|
||||
}
|
||||
|
||||
public void setListener(@Nullable final PlayerServiceExtendedEventListener newListener) {
|
||||
@@ -148,7 +165,7 @@ public final class PlayerHolder {
|
||||
}
|
||||
startPlayerListener();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private void bind(final Context context) {
|
||||
if (DEBUG) {
|
||||
|
||||
@@ -13,7 +13,7 @@ import com.google.android.exoplayer2.RendererCapabilities.Capabilities;
|
||||
import com.google.android.exoplayer2.source.TrackGroup;
|
||||
import com.google.android.exoplayer2.source.TrackGroupArray;
|
||||
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
|
||||
import com.google.android.exoplayer2.trackselection.TrackSelection;
|
||||
import com.google.android.exoplayer2.trackselection.ExoTrackSelection;
|
||||
import com.google.android.exoplayer2.util.Assertions;
|
||||
|
||||
/**
|
||||
@@ -28,7 +28,7 @@ public class CustomTrackSelector extends DefaultTrackSelector {
|
||||
private String preferredTextLanguage;
|
||||
|
||||
public CustomTrackSelector(final Context context,
|
||||
final TrackSelection.Factory adaptiveTrackSelectionFactory) {
|
||||
final ExoTrackSelection.Factory adaptiveTrackSelectionFactory) {
|
||||
super(context, adaptiveTrackSelectionFactory);
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@ public class CustomTrackSelector extends DefaultTrackSelector {
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
protected Pair<TrackSelection.Definition, TextTrackScore> selectTextTrack(
|
||||
protected Pair<ExoTrackSelection.Definition, TextTrackScore> selectTextTrack(
|
||||
final TrackGroupArray groups,
|
||||
@NonNull final int[][] formatSupport,
|
||||
@NonNull final Parameters params,
|
||||
@@ -86,7 +86,7 @@ public class CustomTrackSelector extends DefaultTrackSelector {
|
||||
}
|
||||
}
|
||||
return selectedGroup == null ? null
|
||||
: Pair.create(new TrackSelection.Definition(selectedGroup, selectedTrackIndex),
|
||||
: Pair.create(new ExoTrackSelection.Definition(selectedGroup, selectedTrackIndex),
|
||||
Assertions.checkNotNull(selectedTrackScore));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -436,14 +436,16 @@ public abstract class PlayQueue implements Serializable {
|
||||
* top, so shuffling a size-2 list does nothing)
|
||||
*/
|
||||
public synchronized void shuffle() {
|
||||
// Can't shuffle an list that's empty or only has one element
|
||||
if (size() <= 2) {
|
||||
return;
|
||||
}
|
||||
// Create a backup if it doesn't already exist
|
||||
// Note: The backup-list has to be created at all cost (even when size <= 2).
|
||||
// Otherwise it's not possible to enter shuffle-mode!
|
||||
if (backup == null) {
|
||||
backup = new ArrayList<>(streams);
|
||||
}
|
||||
// Can't shuffle a list that's empty or only has one element
|
||||
if (size() <= 2) {
|
||||
return;
|
||||
}
|
||||
|
||||
final int originalIndex = getIndex();
|
||||
final PlayQueueItem currentItem = getItem();
|
||||
|
||||
@@ -51,6 +51,6 @@ public abstract class PlayQueueItemTouchCallback extends ItemTouchHelper.SimpleC
|
||||
|
||||
@Override
|
||||
public void onSwiped(final RecyclerView.ViewHolder viewHolder, final int swipeDir) {
|
||||
onSwiped(viewHolder.getAdapterPosition());
|
||||
onSwiped(viewHolder.getBindingAdapterPosition());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import androidx.annotation.Nullable;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.MediaItem;
|
||||
import com.google.android.exoplayer2.source.MediaSource;
|
||||
import com.google.android.exoplayer2.source.MediaSourceFactory;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||
@@ -41,20 +42,28 @@ public interface PlaybackResolver extends Resolver<StreamInfo, MediaSource> {
|
||||
@NonNull final String sourceUrl,
|
||||
@C.ContentType final int type,
|
||||
@NonNull final MediaSourceTag metadata) {
|
||||
final Uri uri = Uri.parse(sourceUrl);
|
||||
final MediaSourceFactory factory;
|
||||
switch (type) {
|
||||
case C.TYPE_SS:
|
||||
return dataSource.getLiveSsMediaSourceFactory().setTag(metadata)
|
||||
.createMediaSource(MediaItem.fromUri(uri));
|
||||
factory = dataSource.getLiveSsMediaSourceFactory();
|
||||
break;
|
||||
case C.TYPE_DASH:
|
||||
return dataSource.getLiveDashMediaSourceFactory().setTag(metadata)
|
||||
.createMediaSource(MediaItem.fromUri(uri));
|
||||
factory = dataSource.getLiveDashMediaSourceFactory();
|
||||
break;
|
||||
case C.TYPE_HLS:
|
||||
return dataSource.getLiveHlsMediaSourceFactory().setTag(metadata)
|
||||
.createMediaSource(MediaItem.fromUri(uri));
|
||||
factory = dataSource.getLiveHlsMediaSourceFactory();
|
||||
break;
|
||||
default:
|
||||
throw new IllegalStateException("Unsupported type: " + type);
|
||||
}
|
||||
|
||||
return factory.createMediaSource(
|
||||
new MediaItem.Builder()
|
||||
.setTag(metadata)
|
||||
.setUri(Uri.parse(sourceUrl))
|
||||
.setLiveTargetOffsetMs(PlayerDataSource.LIVE_STREAM_EDGE_GAP_MILLIS)
|
||||
.build()
|
||||
);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@@ -67,21 +76,30 @@ public interface PlaybackResolver extends Resolver<StreamInfo, MediaSource> {
|
||||
@C.ContentType final int type = TextUtils.isEmpty(overrideExtension)
|
||||
? Util.inferContentType(uri) : Util.inferContentType("." + overrideExtension);
|
||||
|
||||
final MediaSourceFactory factory;
|
||||
switch (type) {
|
||||
case C.TYPE_SS:
|
||||
return dataSource.getLiveSsMediaSourceFactory().setTag(metadata)
|
||||
.createMediaSource(MediaItem.fromUri(uri));
|
||||
factory = dataSource.getLiveSsMediaSourceFactory();
|
||||
break;
|
||||
case C.TYPE_DASH:
|
||||
return dataSource.getDashMediaSourceFactory().setTag(metadata)
|
||||
.createMediaSource(MediaItem.fromUri(uri));
|
||||
factory = dataSource.getDashMediaSourceFactory();
|
||||
break;
|
||||
case C.TYPE_HLS:
|
||||
return dataSource.getHlsMediaSourceFactory().setTag(metadata)
|
||||
.createMediaSource(MediaItem.fromUri(uri));
|
||||
factory = dataSource.getHlsMediaSourceFactory();
|
||||
break;
|
||||
case C.TYPE_OTHER:
|
||||
return dataSource.getExtractorMediaSourceFactory(cacheKey).setTag(metadata)
|
||||
.createMediaSource(MediaItem.fromUri(uri));
|
||||
factory = dataSource.getExtractorMediaSourceFactory();
|
||||
break;
|
||||
default:
|
||||
throw new IllegalStateException("Unsupported type: " + type);
|
||||
}
|
||||
|
||||
return factory.createMediaSource(
|
||||
new MediaItem.Builder()
|
||||
.setTag(metadata)
|
||||
.setUri(uri)
|
||||
.setCustomCacheKey(cacheKey)
|
||||
.build()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ package org.schabi.newpipe.settings;
|
||||
|
||||
import android.content.ActivityNotFoundException;
|
||||
import android.content.Intent;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.provider.Settings;
|
||||
import android.widget.Toast;
|
||||
@@ -15,14 +14,10 @@ import org.schabi.newpipe.util.Constants;
|
||||
import org.schabi.newpipe.util.ThemeHelper;
|
||||
|
||||
public class AppearanceSettingsFragment extends BasePreferenceFragment {
|
||||
private static final boolean CAPTIONING_SETTINGS_ACCESSIBLE =
|
||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT;
|
||||
|
||||
private String captionSettingsKey;
|
||||
|
||||
@Override
|
||||
public void onCreatePreferences(final Bundle savedInstanceState, final String rootKey) {
|
||||
addPreferencesFromResource(R.xml.appearance_settings);
|
||||
addPreferencesFromResourceRegistry();
|
||||
|
||||
final String themeKey = getString(R.string.theme_key);
|
||||
// the key of the active theme when settings were opened (or recreated after theme change)
|
||||
@@ -51,16 +46,11 @@ public class AppearanceSettingsFragment extends BasePreferenceFragment {
|
||||
} else {
|
||||
removePreference(nightThemeKey);
|
||||
}
|
||||
|
||||
captionSettingsKey = getString(R.string.caption_settings_key);
|
||||
if (!CAPTIONING_SETTINGS_ACCESSIBLE) {
|
||||
removePreference(captionSettingsKey);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onPreferenceTreeClick(final Preference preference) {
|
||||
if (preference.getKey().equals(captionSettingsKey) && CAPTIONING_SETTINGS_ACCESSIBLE) {
|
||||
if (preference.getKey().equals(getString(R.string.caption_settings_key))) {
|
||||
try {
|
||||
startActivity(new Intent(Settings.ACTION_CAPTIONING_SETTINGS));
|
||||
} catch (final ActivityNotFoundException e) {
|
||||
|
||||
@@ -18,7 +18,7 @@ import java.util.Objects;
|
||||
|
||||
public abstract class BasePreferenceFragment extends PreferenceFragmentCompat {
|
||||
protected final String TAG = getClass().getSimpleName() + "@" + Integer.toHexString(hashCode());
|
||||
protected final boolean DEBUG = MainActivity.DEBUG;
|
||||
protected static final boolean DEBUG = MainActivity.DEBUG;
|
||||
|
||||
SharedPreferences defaultPreferences;
|
||||
|
||||
@@ -28,6 +28,11 @@ public abstract class BasePreferenceFragment extends PreferenceFragmentCompat {
|
||||
super.onCreate(savedInstanceState);
|
||||
}
|
||||
|
||||
protected void addPreferencesFromResourceRegistry() {
|
||||
addPreferencesFromResource(
|
||||
SettingsResourceRegistry.getInstance().getPreferencesResId(this.getClass()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(@NonNull final View rootView,
|
||||
@Nullable final Bundle savedInstanceState) {
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
package org.schabi.newpipe.settings;
|
||||
|
||||
import static org.schabi.newpipe.extractor.utils.Utils.isBlank;
|
||||
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
@@ -20,11 +23,11 @@ import androidx.preference.PreferenceManager;
|
||||
import org.schabi.newpipe.DownloaderImpl;
|
||||
import org.schabi.newpipe.NewPipeDatabase;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.error.ErrorActivity;
|
||||
import org.schabi.newpipe.error.ReCaptchaActivity;
|
||||
import org.schabi.newpipe.error.ErrorUtil;
|
||||
import org.schabi.newpipe.extractor.NewPipe;
|
||||
import org.schabi.newpipe.extractor.localization.ContentCountry;
|
||||
import org.schabi.newpipe.extractor.localization.Localization;
|
||||
import org.schabi.newpipe.streams.io.NoFileManagerSafeGuard;
|
||||
import org.schabi.newpipe.streams.io.StoredFileHelper;
|
||||
import org.schabi.newpipe.util.NavigationHelper;
|
||||
import org.schabi.newpipe.util.PicassoHelper;
|
||||
@@ -37,9 +40,6 @@ import java.util.Date;
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
|
||||
import static org.schabi.newpipe.extractor.utils.Utils.isBlank;
|
||||
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
|
||||
|
||||
public class ContentSettingsFragment extends BasePreferenceFragment {
|
||||
private static final String ZIP_MIME_TYPE = "application/zip";
|
||||
|
||||
@@ -69,23 +69,32 @@ public class ContentSettingsFragment extends BasePreferenceFragment {
|
||||
importExportDataPathKey = getString(R.string.import_export_data_path);
|
||||
youtubeRestrictedModeEnabledKey = getString(R.string.youtube_restricted_mode_enabled);
|
||||
|
||||
addPreferencesFromResource(R.xml.content_settings);
|
||||
addPreferencesFromResourceRegistry();
|
||||
|
||||
final Preference importDataPreference = requirePreference(R.string.import_data);
|
||||
importDataPreference.setOnPreferenceClickListener((Preference p) -> {
|
||||
requestImportPathLauncher.launch(
|
||||
NoFileManagerSafeGuard.launchSafe(
|
||||
requestImportPathLauncher,
|
||||
StoredFileHelper.getPicker(requireContext(),
|
||||
ZIP_MIME_TYPE, getImportExportDataUri()));
|
||||
ZIP_MIME_TYPE, getImportExportDataUri()),
|
||||
TAG,
|
||||
getContext()
|
||||
);
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
final Preference exportDataPreference = requirePreference(R.string.export_data);
|
||||
exportDataPreference.setOnPreferenceClickListener((final Preference p) -> {
|
||||
|
||||
requestExportPathLauncher.launch(
|
||||
NoFileManagerSafeGuard.launchSafe(
|
||||
requestExportPathLauncher,
|
||||
StoredFileHelper.getNewPicker(requireContext(),
|
||||
"NewPipeData-" + exportDateFormat.format(new Date()) + ".zip",
|
||||
ZIP_MIME_TYPE, getImportExportDataUri()));
|
||||
ZIP_MIME_TYPE, getImportExportDataUri()),
|
||||
TAG,
|
||||
getContext()
|
||||
);
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
@@ -95,21 +104,6 @@ public class ContentSettingsFragment extends BasePreferenceFragment {
|
||||
.getPreferredContentCountry(requireContext());
|
||||
initialLanguage = defaultPreferences.getString(getString(R.string.app_language_key), "en");
|
||||
|
||||
final Preference clearCookiePref = requirePreference(R.string.clear_cookie_key);
|
||||
clearCookiePref.setOnPreferenceClickListener(preference -> {
|
||||
defaultPreferences.edit()
|
||||
.putString(getString(R.string.recaptcha_cookies_key), "").apply();
|
||||
DownloaderImpl.getInstance().setCookie(ReCaptchaActivity.RECAPTCHA_COOKIES_KEY, "");
|
||||
Toast.makeText(getActivity(), R.string.recaptcha_cookies_cleared,
|
||||
Toast.LENGTH_SHORT).show();
|
||||
clearCookiePref.setVisible(false);
|
||||
return true;
|
||||
});
|
||||
|
||||
if (defaultPreferences.getString(getString(R.string.recaptcha_cookies_key), "").isEmpty()) {
|
||||
clearCookiePref.setVisible(false);
|
||||
}
|
||||
|
||||
findPreference(getString(R.string.download_thumbnail_key)).setOnPreferenceChangeListener(
|
||||
(preference, newValue) -> {
|
||||
PicassoHelper.setShouldLoadImages((Boolean) newValue);
|
||||
@@ -205,7 +199,7 @@ public class ContentSettingsFragment extends BasePreferenceFragment {
|
||||
saveLastImportExportDataUri(exportDataUri); // save export path only on success
|
||||
Toast.makeText(getContext(), R.string.export_complete_toast, Toast.LENGTH_SHORT).show();
|
||||
} catch (final Exception e) {
|
||||
ErrorActivity.reportUiErrorInSnackbar(this, "Exporting database", e);
|
||||
ErrorUtil.showUiErrorSnackbar(this, "Exporting database", e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -247,7 +241,7 @@ public class ContentSettingsFragment extends BasePreferenceFragment {
|
||||
finishImport(importDataUri);
|
||||
}
|
||||
} catch (final Exception e) {
|
||||
ErrorActivity.reportUiErrorInSnackbar(this, "Importing database", e);
|
||||
ErrorUtil.showUiErrorSnackbar(this, "Importing database", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package org.schabi.newpipe.settings
|
||||
|
||||
import android.content.SharedPreferences
|
||||
import android.util.Log
|
||||
import org.schabi.newpipe.streams.io.SharpOutputStream
|
||||
import org.schabi.newpipe.streams.io.StoredFileHelper
|
||||
import org.schabi.newpipe.util.ZipHelper
|
||||
@@ -13,6 +14,9 @@ import java.io.ObjectOutputStream
|
||||
import java.util.zip.ZipOutputStream
|
||||
|
||||
class ContentSettingsManager(private val fileLocator: NewPipeFileLocator) {
|
||||
companion object {
|
||||
const val TAG = "ContentSetManager"
|
||||
}
|
||||
|
||||
/**
|
||||
* Exports given [SharedPreferences] to the file in given outputPath.
|
||||
@@ -31,7 +35,7 @@ class ContentSettingsManager(private val fileLocator: NewPipeFileLocator) {
|
||||
output.flush()
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
e.printStackTrace()
|
||||
Log.e(TAG, "Unable to exportDatabase", e)
|
||||
}
|
||||
|
||||
ZipHelper.addFileToZip(outZip, fileLocator.settings.path, "newpipe.settings")
|
||||
@@ -101,9 +105,9 @@ class ContentSettingsManager(private val fileLocator: NewPipeFileLocator) {
|
||||
preferenceEditor.commit()
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
e.printStackTrace()
|
||||
Log.e(TAG, "Unable to loadSharedPreferences", e)
|
||||
} catch (e: ClassNotFoundException) {
|
||||
e.printStackTrace()
|
||||
Log.e(TAG, "Unable to loadSharedPreferences", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
package org.schabi.newpipe.settings;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.preference.Preference;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.error.ErrorInfo;
|
||||
import org.schabi.newpipe.error.ErrorUtil;
|
||||
import org.schabi.newpipe.error.UserAction;
|
||||
import org.schabi.newpipe.util.PicassoHelper;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
public class DebugSettingsFragment extends BasePreferenceFragment {
|
||||
private static final String DUMMY = "Dummy";
|
||||
|
||||
@Override
|
||||
public void onCreatePreferences(final Bundle savedInstanceState, final String rootKey) {
|
||||
addPreferencesFromResourceRegistry();
|
||||
|
||||
final Preference allowHeapDumpingPreference
|
||||
= findPreference(getString(R.string.allow_heap_dumping_key));
|
||||
final Preference showMemoryLeaksPreference
|
||||
= findPreference(getString(R.string.show_memory_leaks_key));
|
||||
final Preference showImageIndicatorsPreference
|
||||
= findPreference(getString(R.string.show_image_indicators_key));
|
||||
final Preference crashTheAppPreference
|
||||
= findPreference(getString(R.string.crash_the_app_key));
|
||||
final Preference showErrorSnackbarPreference
|
||||
= findPreference(getString(R.string.show_error_snackbar_key));
|
||||
final Preference createErrorNotificationPreference
|
||||
= findPreference(getString(R.string.create_error_notification_key));
|
||||
|
||||
assert allowHeapDumpingPreference != null;
|
||||
assert showMemoryLeaksPreference != null;
|
||||
assert showImageIndicatorsPreference != null;
|
||||
assert crashTheAppPreference != null;
|
||||
assert showErrorSnackbarPreference != null;
|
||||
assert createErrorNotificationPreference != null;
|
||||
|
||||
final Optional<DebugSettingsBVDLeakCanaryAPI> optBVLeakCanary = getBVDLeakCanary();
|
||||
|
||||
allowHeapDumpingPreference.setEnabled(optBVLeakCanary.isPresent());
|
||||
showMemoryLeaksPreference.setEnabled(optBVLeakCanary.isPresent());
|
||||
|
||||
if (optBVLeakCanary.isPresent()) {
|
||||
final DebugSettingsBVDLeakCanaryAPI pdLeakCanary = optBVLeakCanary.get();
|
||||
|
||||
showMemoryLeaksPreference.setOnPreferenceClickListener(preference -> {
|
||||
startActivity(pdLeakCanary.getNewLeakDisplayActivityIntent());
|
||||
return true;
|
||||
});
|
||||
} else {
|
||||
allowHeapDumpingPreference.setSummary(R.string.leak_canary_not_available);
|
||||
showMemoryLeaksPreference.setSummary(R.string.leak_canary_not_available);
|
||||
}
|
||||
|
||||
showImageIndicatorsPreference.setOnPreferenceChangeListener((preference, newValue) -> {
|
||||
PicassoHelper.setIndicatorsEnabled((Boolean) newValue);
|
||||
return true;
|
||||
});
|
||||
|
||||
crashTheAppPreference.setOnPreferenceClickListener(preference -> {
|
||||
throw new RuntimeException(DUMMY);
|
||||
});
|
||||
|
||||
showErrorSnackbarPreference.setOnPreferenceClickListener(preference -> {
|
||||
ErrorUtil.showUiErrorSnackbar(DebugSettingsFragment.this,
|
||||
DUMMY, new RuntimeException(DUMMY));
|
||||
return true;
|
||||
});
|
||||
|
||||
createErrorNotificationPreference.setOnPreferenceClickListener(preference -> {
|
||||
ErrorUtil.createNotification(requireContext(),
|
||||
new ErrorInfo(new RuntimeException(DUMMY), UserAction.UI_ERROR, DUMMY));
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to find the {@link DebugSettingsBVDLeakCanaryAPI#IMPL_CLASS} and loads it if available.
|
||||
* @return An {@link Optional} which is empty if the implementation class couldn't be loaded.
|
||||
*/
|
||||
private Optional<DebugSettingsBVDLeakCanaryAPI> getBVDLeakCanary() {
|
||||
try {
|
||||
// Try to find the implementation of the LeakCanary API
|
||||
return Optional.of((DebugSettingsBVDLeakCanaryAPI)
|
||||
Class.forName(DebugSettingsBVDLeakCanaryAPI.IMPL_CLASS)
|
||||
.getDeclaredConstructor()
|
||||
.newInstance());
|
||||
} catch (final Exception e) {
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build variant dependent (BVD) leak canary API for this fragment.
|
||||
* Why is LeakCanary not used directly? Because it can't be assured
|
||||
*/
|
||||
public interface DebugSettingsBVDLeakCanaryAPI {
|
||||
String IMPL_CLASS =
|
||||
"org.schabi.newpipe.settings.DebugSettingsBVDLeakCanary";
|
||||
|
||||
Intent getNewLeakDisplayActivityIntent();
|
||||
}
|
||||
}
|
||||
@@ -21,6 +21,7 @@ import androidx.preference.SwitchPreferenceCompat;
|
||||
import com.nononsenseapps.filepicker.Utils;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.streams.io.NoFileManagerSafeGuard;
|
||||
import org.schabi.newpipe.streams.io.StoredDirectoryHelper;
|
||||
import org.schabi.newpipe.util.FilePickerActivityHelper;
|
||||
|
||||
@@ -53,7 +54,7 @@ public class DownloadSettingsFragment extends BasePreferenceFragment {
|
||||
|
||||
@Override
|
||||
public void onCreatePreferences(final Bundle savedInstanceState, final String rootKey) {
|
||||
addPreferencesFromResource(R.xml.download_settings);
|
||||
addPreferencesFromResourceRegistry();
|
||||
|
||||
downloadPathVideoPreference = getString(R.string.download_path_video_key);
|
||||
downloadPathAudioPreference = getString(R.string.download_path_audio_key);
|
||||
@@ -214,7 +215,12 @@ public class DownloadSettingsFragment extends BasePreferenceFragment {
|
||||
}
|
||||
|
||||
private void launchDirectoryPicker(final ActivityResultLauncher<Intent> launcher) {
|
||||
launcher.launch(StoredDirectoryHelper.getPicker(ctx));
|
||||
NoFileManagerSafeGuard.launchSafe(
|
||||
launcher,
|
||||
StoredDirectoryHelper.getPicker(ctx),
|
||||
TAG,
|
||||
ctx
|
||||
);
|
||||
}
|
||||
|
||||
private void requestDownloadVideoPathResult(final ActivityResult result) {
|
||||
|
||||
@@ -8,9 +8,11 @@ import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.preference.Preference;
|
||||
|
||||
import org.schabi.newpipe.DownloaderImpl;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.error.ErrorActivity;
|
||||
import org.schabi.newpipe.error.ErrorInfo;
|
||||
import org.schabi.newpipe.error.ErrorUtil;
|
||||
import org.schabi.newpipe.error.ReCaptchaActivity;
|
||||
import org.schabi.newpipe.error.UserAction;
|
||||
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
||||
import org.schabi.newpipe.util.InfoCache;
|
||||
@@ -29,7 +31,7 @@ public class HistorySettingsFragment extends BasePreferenceFragment {
|
||||
|
||||
@Override
|
||||
public void onCreatePreferences(final Bundle savedInstanceState, final String rootKey) {
|
||||
addPreferencesFromResource(R.xml.history_settings);
|
||||
addPreferencesFromResourceRegistry();
|
||||
|
||||
cacheWipeKey = getString(R.string.metadata_cache_wipe_key);
|
||||
viewsHistoryClearKey = getString(R.string.clear_views_history_key);
|
||||
@@ -37,6 +39,21 @@ public class HistorySettingsFragment extends BasePreferenceFragment {
|
||||
searchHistoryClearKey = getString(R.string.clear_search_history_key);
|
||||
recordManager = new HistoryRecordManager(getActivity());
|
||||
disposables = new CompositeDisposable();
|
||||
|
||||
final Preference clearCookiePref = requirePreference(R.string.clear_cookie_key);
|
||||
clearCookiePref.setOnPreferenceClickListener(preference -> {
|
||||
defaultPreferences.edit()
|
||||
.putString(getString(R.string.recaptcha_cookies_key), "").apply();
|
||||
DownloaderImpl.getInstance().setCookie(ReCaptchaActivity.RECAPTCHA_COOKIES_KEY, "");
|
||||
Toast.makeText(getActivity(), R.string.recaptcha_cookies_cleared,
|
||||
Toast.LENGTH_SHORT).show();
|
||||
clearCookiePref.setEnabled(false);
|
||||
return true;
|
||||
});
|
||||
|
||||
if (defaultPreferences.getString(getString(R.string.recaptcha_cookies_key), "").isEmpty()) {
|
||||
clearCookiePref.setEnabled(false);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -64,7 +81,7 @@ public class HistorySettingsFragment extends BasePreferenceFragment {
|
||||
.subscribe(
|
||||
howManyDeleted -> Toast.makeText(context,
|
||||
R.string.watch_history_states_deleted, Toast.LENGTH_SHORT).show(),
|
||||
throwable -> ErrorActivity.reportError(context,
|
||||
throwable -> ErrorUtil.openActivity(context,
|
||||
new ErrorInfo(throwable, UserAction.DELETE_FROM_HISTORY,
|
||||
"Delete playback states")));
|
||||
}
|
||||
@@ -76,7 +93,7 @@ public class HistorySettingsFragment extends BasePreferenceFragment {
|
||||
.subscribe(
|
||||
howManyDeleted -> Toast.makeText(context,
|
||||
R.string.watch_history_deleted, Toast.LENGTH_SHORT).show(),
|
||||
throwable -> ErrorActivity.reportError(context,
|
||||
throwable -> ErrorUtil.openActivity(context,
|
||||
new ErrorInfo(throwable, UserAction.DELETE_FROM_HISTORY,
|
||||
"Delete from history")));
|
||||
}
|
||||
@@ -87,7 +104,7 @@ public class HistorySettingsFragment extends BasePreferenceFragment {
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
howManyDeleted -> { },
|
||||
throwable -> ErrorActivity.reportError(context,
|
||||
throwable -> ErrorUtil.openActivity(context,
|
||||
new ErrorInfo(throwable, UserAction.DELETE_FROM_HISTORY,
|
||||
"Clear orphaned records")));
|
||||
}
|
||||
@@ -99,7 +116,7 @@ public class HistorySettingsFragment extends BasePreferenceFragment {
|
||||
.subscribe(
|
||||
howManyDeleted -> Toast.makeText(context,
|
||||
R.string.search_history_deleted, Toast.LENGTH_SHORT).show(),
|
||||
throwable -> ErrorActivity.reportError(context,
|
||||
throwable -> ErrorUtil.openActivity(context,
|
||||
new ErrorInfo(throwable, UserAction.DELETE_FROM_HISTORY,
|
||||
"Delete search history")));
|
||||
}
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
package org.schabi.newpipe.settings;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
|
||||
import androidx.preference.Preference;
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.schabi.newpipe.App;
|
||||
import org.schabi.newpipe.CheckForNewAppVersion;
|
||||
@@ -12,15 +15,58 @@ import org.schabi.newpipe.R;
|
||||
public class MainSettingsFragment extends BasePreferenceFragment {
|
||||
public static final boolean DEBUG = MainActivity.DEBUG;
|
||||
|
||||
private SettingsActivity settingsActivity;
|
||||
|
||||
@Override
|
||||
public void onCreatePreferences(final Bundle savedInstanceState, final String rootKey) {
|
||||
addPreferencesFromResource(R.xml.main_settings);
|
||||
addPreferencesFromResourceRegistry();
|
||||
|
||||
if (!CheckForNewAppVersion.isGithubApk(App.getApp())) {
|
||||
final Preference update = findPreference(getString(R.string.update_pref_screen_key));
|
||||
getPreferenceScreen().removePreference(update);
|
||||
setHasOptionsMenu(true); // Otherwise onCreateOptionsMenu is not called
|
||||
|
||||
// Check if the app is updatable
|
||||
if (!CheckForNewAppVersion.isReleaseApk(App.getApp())) {
|
||||
getPreferenceScreen().removePreference(
|
||||
findPreference(getString(R.string.update_pref_screen_key)));
|
||||
|
||||
defaultPreferences.edit().putBoolean(getString(R.string.update_app_key), false).apply();
|
||||
}
|
||||
|
||||
// Hide debug preferences in RELEASE build variant
|
||||
if (!DEBUG) {
|
||||
getPreferenceScreen().removePreference(
|
||||
findPreference(getString(R.string.debug_pref_screen_key)));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreateOptionsMenu(
|
||||
@NonNull final Menu menu,
|
||||
@NonNull final MenuInflater inflater
|
||||
) {
|
||||
super.onCreateOptionsMenu(menu, inflater);
|
||||
|
||||
// -- Link settings activity and register menu --
|
||||
settingsActivity = (SettingsActivity) getActivity();
|
||||
|
||||
inflater.inflate(R.menu.menu_settings_main_fragment, menu);
|
||||
|
||||
final MenuItem menuSearchItem = menu.getItem(0);
|
||||
|
||||
settingsActivity.setMenuSearchItem(menuSearchItem);
|
||||
|
||||
menuSearchItem.setOnMenuItemClickListener(ev -> {
|
||||
settingsActivity.setSearchActive(true);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
// Unlink activity so that we don't get memory problems
|
||||
if (settingsActivity != null) {
|
||||
settingsActivity.setMenuSearchItem(null);
|
||||
settingsActivity = null;
|
||||
}
|
||||
super.onDestroy();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import org.schabi.newpipe.R
|
||||
|
||||
class NotificationSettingsFragment : BasePreferenceFragment() {
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
addPreferencesFromResource(R.xml.notification_settings)
|
||||
addPreferencesFromResourceRegistry()
|
||||
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
|
||||
val colorizePref: Preference? = findPreference(getString(R.string.notification_colorize_key))
|
||||
|
||||
@@ -303,8 +303,8 @@ public class PeertubeInstanceListFragment extends Fragment {
|
||||
return false;
|
||||
}
|
||||
|
||||
final int sourceIndex = source.getAdapterPosition();
|
||||
final int targetIndex = target.getAdapterPosition();
|
||||
final int sourceIndex = source.getBindingAdapterPosition();
|
||||
final int targetIndex = target.getBindingAdapterPosition();
|
||||
instanceListAdapter.swapItems(sourceIndex, targetIndex);
|
||||
return true;
|
||||
}
|
||||
@@ -322,7 +322,7 @@ public class PeertubeInstanceListFragment extends Fragment {
|
||||
@Override
|
||||
public void onSwiped(@NonNull final RecyclerView.ViewHolder viewHolder,
|
||||
final int swipeDir) {
|
||||
final int position = viewHolder.getAdapterPosition();
|
||||
final int position = viewHolder.getBindingAdapterPosition();
|
||||
// do not allow swiping the selected instance
|
||||
if (instanceList.get(position).getUrl().equals(selectedInstance.getUrl())) {
|
||||
instanceListAdapter.notifyItemChanged(position);
|
||||
|
||||
@@ -16,7 +16,7 @@ import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.database.subscription.SubscriptionEntity;
|
||||
import org.schabi.newpipe.error.ErrorActivity;
|
||||
import org.schabi.newpipe.error.ErrorUtil;
|
||||
import org.schabi.newpipe.local.subscription.SubscriptionManager;
|
||||
import org.schabi.newpipe.util.PicassoHelper;
|
||||
import org.schabi.newpipe.util.ThemeHelper;
|
||||
@@ -153,7 +153,7 @@ public class SelectChannelFragment extends DialogFragment {
|
||||
|
||||
@Override
|
||||
public void onError(@NonNull final Throwable exception) {
|
||||
ErrorActivity.reportUiErrorInSnackbar(SelectChannelFragment.this,
|
||||
ErrorUtil.showUiErrorSnackbar(SelectChannelFragment.this,
|
||||
"Loading subscription", exception);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package org.schabi.newpipe.settings;
|
||||
|
||||
import android.content.DialogInterface;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
@@ -16,7 +15,7 @@ import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.error.ErrorActivity;
|
||||
import org.schabi.newpipe.error.ErrorUtil;
|
||||
import org.schabi.newpipe.extractor.NewPipe;
|
||||
import org.schabi.newpipe.extractor.StreamingService;
|
||||
import org.schabi.newpipe.util.KioskTranslator;
|
||||
@@ -48,20 +47,14 @@ import java.util.Vector;
|
||||
*/
|
||||
|
||||
public class SelectKioskFragment extends DialogFragment {
|
||||
private RecyclerView recyclerView = null;
|
||||
private SelectKioskAdapter selectKioskAdapter = null;
|
||||
|
||||
private OnSelectedListener onSelectedListener = null;
|
||||
private OnCancelListener onCancelListener = null;
|
||||
|
||||
public void setOnSelectedListener(final OnSelectedListener listener) {
|
||||
onSelectedListener = listener;
|
||||
}
|
||||
|
||||
public void setOnCancelListener(final OnCancelListener listener) {
|
||||
onCancelListener = listener;
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Init
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
@@ -76,12 +69,12 @@ public class SelectKioskFragment extends DialogFragment {
|
||||
public View onCreateView(final LayoutInflater inflater, final ViewGroup container,
|
||||
final Bundle savedInstanceState) {
|
||||
final View v = inflater.inflate(R.layout.select_kiosk_fragment, container, false);
|
||||
recyclerView = v.findViewById(R.id.items_list);
|
||||
final RecyclerView recyclerView = v.findViewById(R.id.items_list);
|
||||
recyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
|
||||
try {
|
||||
selectKioskAdapter = new SelectKioskAdapter();
|
||||
} catch (final Exception e) {
|
||||
ErrorActivity.reportUiErrorInSnackbar(this, "Selecting kiosk", e);
|
||||
ErrorUtil.showUiErrorSnackbar(this, "Selecting kiosk", e);
|
||||
}
|
||||
recyclerView.setAdapter(selectKioskAdapter);
|
||||
|
||||
@@ -92,14 +85,6 @@ public class SelectKioskFragment extends DialogFragment {
|
||||
// Handle actions
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public void onCancel(@NonNull final DialogInterface dialogInterface) {
|
||||
super.onCancel(dialogInterface);
|
||||
if (onCancelListener != null) {
|
||||
onCancelListener.onCancel();
|
||||
}
|
||||
}
|
||||
|
||||
private void clickedItem(final SelectKioskAdapter.Entry entry) {
|
||||
if (onSelectedListener != null) {
|
||||
onSelectedListener.onKioskSelected(entry.serviceId, entry.kioskId, entry.kioskName);
|
||||
@@ -115,10 +100,6 @@ public class SelectKioskFragment extends DialogFragment {
|
||||
void onKioskSelected(int serviceId, String kioskId, String kioskName);
|
||||
}
|
||||
|
||||
public interface OnCancelListener {
|
||||
void onCancel();
|
||||
}
|
||||
|
||||
private class SelectKioskAdapter
|
||||
extends RecyclerView.Adapter<SelectKioskAdapter.SelectKioskItemHolder> {
|
||||
private final List<Entry> kioskList = new Vector<>();
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user