1
0
mirror of https://github.com/TeamNewPipe/NewPipe synced 2026-01-13 18:22:41 +00:00

Compare commits

..

422 Commits

Author SHA1 Message Date
TobiGr
493e47f7e6 Add Central Atlas Tamazight (Tamaziɣt) to app locales 2020-11-18 19:59:15 +01:00
Tobias Groza
ebb906c273 Merge pull request #4852 from TeamNewPipe/release_0.20.3
Release 0.20.3
2020-11-18 16:40:52 +01:00
TobiGr
46b91bf8b0 Merge remote-tracking branch 'Weblate/dev' into release_0.20.3 2020-11-18 15:57:57 +01:00
Bopol
a79d7c8417 Translated using Weblate (French)
Currently translated at 68.4% (26 of 38 strings)

Translation: NewPipe/Metadata
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/fr/
2020-11-16 22:09:30 +01:00
Stypox
7a6e0d651f Add Uzbek language (O'zbek) and remove Neapolitan
Neapolitan only has 43 translated strings, so it should not appear as a possible language
2020-11-15 23:37:39 +01:00
Stypox
7476498823 [Regression] Revert "Removed remember popup properties setting"
This reverts commit 314615bfef.
2020-11-15 22:24:27 +01:00
Stypox
4c7b5d44a0 [Regression] Fix videos added multiple times to detail fragment stack 2020-11-15 22:23:47 +01:00
Mukhamadjonov
620bb54881 Translated using Weblate (Uzbek (latin))
Currently translated at 66.7% (404 of 605 strings)
2020-11-15 22:23:47 +01:00
Prasanta-Hembram
73be747cbe Translated using Weblate (Santali)
Currently translated at 8.5% (52 of 605 strings)
2020-11-15 22:23:46 +01:00
WaldiS
4d874451c9 Translated using Weblate (Polish)
Currently translated at 37.8% (14 of 37 strings)

Translation: NewPipe/Metadata
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/pl/
2020-11-15 22:23:46 +01:00
Allan Nordhøy
3245d620c3 Translated using Weblate (Norwegian Bokmål)
Currently translated at 93.3% (565 of 605 strings)
2020-11-15 22:23:46 +01:00
Terry Louwers
a59f80589a Translated using Weblate (Dutch (Belgium))
Currently translated at 100.0% (605 of 605 strings)
2020-11-15 22:23:46 +01:00
Milo Ivir
6b269c7559 Translated using Weblate (Croatian)
Currently translated at 98.8% (598 of 605 strings)
2020-11-15 22:23:46 +01:00
Michal L
53cadeab61 Translated using Weblate (Polish)
Currently translated at 100.0% (605 of 605 strings)
2020-11-15 22:23:46 +01:00
2-Seol
e90d388fdb Translated using Weblate (Korean)
Currently translated at 82.8% (501 of 605 strings)
2020-11-15 22:23:46 +01:00
Terry Louwers
219f059834 Translated using Weblate (Dutch)
Currently translated at 100.0% (605 of 605 strings)
2020-11-15 22:23:46 +01:00
WaldiS
dc2dac66a3 Translated using Weblate (Polish)
Currently translated at 99.8% (604 of 605 strings)
2020-11-15 22:23:46 +01:00
Mukhamadjonov
e04ee666b8 Translated using Weblate (Uzbek (latin))
Currently translated at 36.5% (221 of 605 strings)
2020-11-15 22:23:46 +01:00
Prasanta-Hembram
7d27003bb2 Translated using Weblate (Santali)
Currently translated at 8.4% (51 of 605 strings)
2020-11-15 22:23:46 +01:00
Allan Nordhøy
b866c9dd08 Translated using Weblate (Norwegian Bokmål)
Currently translated at 10.8% (4 of 37 strings)

Translation: NewPipe/Metadata
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/nb_NO/
2020-11-15 22:23:46 +01:00
Ajeje Brazorf
d17236fe45 Translated using Weblate (Sardinian)
Currently translated at 10.8% (4 of 37 strings)

Translation: NewPipe/Metadata
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/sc/
2020-11-15 22:23:46 +01:00
ssantos
d2580ec87c Translated using Weblate (Portuguese (Portugal))
Currently translated at 54.0% (20 of 37 strings)

Translation: NewPipe/Metadata
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/pt_PT/
2020-11-15 22:23:46 +01:00
Florian
7c10f414dc Translated using Weblate (French)
Currently translated at 24.3% (9 of 37 strings)

Translation: NewPipe/Metadata
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/fr/
2020-11-15 22:23:46 +01:00
simo
2a4717cb7f Translated using Weblate (Arabic)
Currently translated at 62.1% (23 of 37 strings)

Translation: NewPipe/Metadata
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/ar/
2020-11-15 22:23:46 +01:00
Yaron Shahrabani
be9cb8a4da Translated using Weblate (Hebrew)
Currently translated at 21.6% (8 of 37 strings)

Translation: NewPipe/Metadata
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/he/
2020-11-15 22:23:46 +01:00
ssantos
e887363910 Translated using Weblate (Portuguese)
Currently translated at 54.0% (20 of 37 strings)

Translation: NewPipe/Metadata
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/pt/
2020-11-15 22:23:45 +01:00
nautilusx
a274159726 Translated using Weblate (German)
Currently translated at 18.9% (7 of 37 strings)

Translation: NewPipe/Metadata
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/de/
2020-11-15 22:23:45 +01:00
ssantos
83384e0de4 Translated using Weblate (Portuguese (Portugal))
Currently translated at 100.0% (605 of 605 strings)
2020-11-15 22:23:45 +01:00
Ajeje Brazorf
748904b8ad Translated using Weblate (Sardinian)
Currently translated at 100.0% (605 of 605 strings)
2020-11-15 22:23:44 +01:00
Yaron Shahrabani
3e72df8b1e Translated using Weblate (Hebrew)
Currently translated at 100.0% (605 of 605 strings)
2020-11-15 22:23:25 +01:00
Jeff Huang
2921563e9c Translated using Weblate (Chinese (Traditional))
Currently translated at 100.0% (605 of 605 strings)
2020-11-15 22:23:25 +01:00
Eric
01c1346696 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (605 of 605 strings)
2020-11-15 22:23:25 +01:00
simo
796e0456ef Translated using Weblate (Arabic)
Currently translated at 100.0% (605 of 605 strings)
2020-11-15 22:23:24 +01:00
Bopol
eeb68497fe Translated using Weblate (Esperanto)
Currently translated at 91.0% (551 of 605 strings)
2020-11-15 22:23:24 +01:00
ssantos
7eadb6acad Translated using Weblate (Portuguese)
Currently translated at 100.0% (605 of 605 strings)
2020-11-15 22:23:24 +01:00
Vancha March
9d588aa7e7 Translated using Weblate (Dutch)
Currently translated at 99.8% (604 of 605 strings)
2020-11-15 22:23:24 +01:00
Florian
204b5f7f09 Translated using Weblate (French)
Currently translated at 100.0% (605 of 605 strings)
2020-11-15 22:23:24 +01:00
nautilusx
e0f53b63ce Translated using Weblate (German)
Currently translated at 100.0% (605 of 605 strings)
2020-11-15 22:23:24 +01:00
Allan Nordhøy
83f4dbe40e Translated using Weblate (Norwegian Bokmål)
Currently translated at 93.0% (563 of 605 strings)
2020-11-15 22:23:24 +01:00
Mukhamadjonov
8d8ba68838 Translated using Weblate (Uzbek (latin))
Currently translated at 20.3% (123 of 603 strings)
2020-11-15 22:23:24 +01:00
Stypox
3f25940dec Release 0.20.3 2020-11-15 22:23:24 +01:00
Tobias Groza
35f5575595 Merge pull request #4892 from TeamNewPipe/correct_gigaget_license
Correct Gigaget's license from GPLv2 to GPLv3
2020-11-15 15:30:02 +01:00
opusforlife2
e4746f8b32 Remove GPLv2 - not needed 2020-11-15 14:04:10 +00:00
opusforlife2
7e0552efde Delete GPLv2 license file - not needed 2020-11-15 14:03:20 +00:00
opusforlife2
6075b98634 Correct Gigaget's license
It's GPLv3, not GPLv2.
2020-11-15 13:41:43 +00:00
Hosted Weblate
d1d8b911b9 Merge branch 'origin/dev' into Weblate. 2020-11-14 11:48:32 +01:00
Stypox
796e656328 Translated using Weblate (Italian)
Currently translated at 100.0% (605 of 605 strings)
2020-11-14 11:48:32 +01:00
ysard
8b869915e7 Translated using Weblate (French)
Currently translated at 67.5% (25 of 37 strings)

Translation: NewPipe/Metadata
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/fr/
2020-11-14 11:48:21 +01:00
Stypox
9b05243d61 Translated using Weblate (Italian)
Currently translated at 100.0% (605 of 605 strings)
2020-11-14 11:48:20 +01:00
Stypox
81c24510a8 Add Release 0.20.3 fastlane changelog to allow translation 2020-11-14 11:30:27 +01:00
Stypox
9e7fb4d21a Merge pull request #4771 from Stypox/fix-playlist-select
Fix playlist select dialog and do some refactoring
2020-11-14 11:21:20 +01:00
Prasanta-Hembram
ae7f04578d Translated using Weblate (Santali)
Currently translated at 9.7% (59 of 605 strings)
2020-11-14 00:56:38 +01:00
chr56
ce814cffd1 Translated using Weblate (Chinese (Simplified))
Currently translated at 64.8% (24 of 37 strings)

Translation: NewPipe/Metadata
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/zh_Hans/
2020-11-14 00:56:38 +01:00
Ville Rantanen
703b310ef0 Translated using Weblate (Finnish)
Currently translated at 5.4% (2 of 37 strings)

Translation: NewPipe/Metadata
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/fi/
2020-11-14 00:56:36 +01:00
ysard
da6c4ad36a Translated using Weblate (French)
Currently translated at 24.3% (9 of 37 strings)

Translation: NewPipe/Metadata
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/fr/
2020-11-14 00:56:36 +01:00
Michal L
a8c849d38a Translated using Weblate (Polish)
Currently translated at 40.5% (15 of 37 strings)

Translation: NewPipe/Metadata
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/pl/
2020-11-14 00:56:35 +01:00
jimman2003
d8d5e04a51 Translated using Weblate (Greek)
Currently translated at 16.2% (6 of 37 strings)

Translation: NewPipe/Metadata
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/el/
2020-11-14 00:56:35 +01:00
David Braz
fa348cb98f Translated using Weblate (Portuguese (Brazil))
Currently translated at 21.6% (8 of 37 strings)

Translation: NewPipe/Metadata
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/pt_BR/
2020-11-14 00:56:34 +01:00
Oymate
f4ec2d8107 Translated using Weblate (Bengali)
Currently translated at 78.5% (475 of 605 strings)
2020-11-14 00:56:33 +01:00
Ville Rantanen
de39d828de Translated using Weblate (Finnish)
Currently translated at 100.0% (605 of 605 strings)
2020-11-14 00:56:33 +01:00
David Braz
25d3d0d0ba Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (605 of 605 strings)
2020-11-14 00:56:33 +01:00
Habib Rohman
6f3b1000a7 Translated using Weblate (Indonesian)
Currently translated at 100.0% (605 of 605 strings)
2020-11-14 00:56:32 +01:00
Marian Hanzel
1ebb8d8d14 Translated using Weblate (Slovak)
Currently translated at 100.0% (605 of 605 strings)
2020-11-14 00:56:32 +01:00
jimman2003
4e4acdaecc Translated using Weblate (Greek)
Currently translated at 99.6% (603 of 605 strings)
2020-11-14 00:56:32 +01:00
pjammo
f7fb03bf56 Translated using Weblate (Italian)
Currently translated at 100.0% (605 of 605 strings)
2020-11-14 00:56:31 +01:00
Ács Zoltán
429aafc7ba Translated using Weblate (Hungarian)
Currently translated at 68.4% (414 of 605 strings)
2020-11-14 00:56:31 +01:00
Ldm Public
acdfede2a8 Translated using Weblate (French)
Currently translated at 21.6% (8 of 37 strings)

Translation: NewPipe/Metadata
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/fr/
2020-11-13 20:54:16 +01:00
Hosted Weblate
8366c4c165 Merge branch 'origin/dev' into Weblate. 2020-11-10 12:15:50 +01:00
Mukhamadjonov
4c7260b043 Translated using Weblate (Uzbek (latin))
Currently translated at 66.7% (404 of 605 strings)
2020-11-10 12:14:17 +01:00
Prasanta-Hembram
c878f7dc25 Translated using Weblate (Santali)
Currently translated at 8.5% (52 of 605 strings)
2020-11-10 12:14:13 +01:00
WaldiS
aca21f6ef2 Translated using Weblate (Polish)
Currently translated at 37.8% (14 of 37 strings)

Translation: NewPipe/Metadata
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/pl/
2020-11-10 12:14:13 +01:00
Allan Nordhøy
10c582bafb Translated using Weblate (Norwegian Bokmål)
Currently translated at 93.3% (565 of 605 strings)
2020-11-10 12:14:12 +01:00
Terry Louwers
7b3bd26631 Translated using Weblate (Dutch (Belgium))
Currently translated at 100.0% (605 of 605 strings)
2020-11-10 12:14:12 +01:00
Milo Ivir
731f88da84 Translated using Weblate (Croatian)
Currently translated at 98.8% (598 of 605 strings)
2020-11-10 12:14:12 +01:00
Michal L
b7fb9a65b6 Translated using Weblate (Polish)
Currently translated at 100.0% (605 of 605 strings)
2020-11-10 12:14:11 +01:00
2-Seol
843c24b17a Translated using Weblate (Korean)
Currently translated at 82.8% (501 of 605 strings)
2020-11-10 12:14:11 +01:00
Terry Louwers
18dbbfc95a Translated using Weblate (Dutch)
Currently translated at 100.0% (605 of 605 strings)
2020-11-10 12:14:11 +01:00
bopol
5b6e187b49 Merge pull request #4845 from KIMMINTAE98/rm_kor
Add korean translation of README.md
2020-11-10 12:07:27 +01:00
opusforlife2
9025a9b88c Rename and add links to translations 2020-11-10 06:58:20 +00:00
opusforlife2
07b2891671 Add links to translations
All READMEs should contains links to translated versions.
2020-11-10 06:56:01 +00:00
Tobias Groza
c4a739bef6 Merge pull request #4562 from Stypox/fix-detail-open
Fix opening VideoDetailFragment and much more
2020-11-09 22:04:39 +01:00
WaldiS
1008c74cd7 Translated using Weblate (Polish)
Currently translated at 99.8% (604 of 605 strings)
2020-11-09 20:57:14 +01:00
Stypox
60dc9d27bc Merge pull request #4784 from bd0n4lds/Dont-Use-Float-Type-For-Loop-Indices
Change loop index from float to int
2020-11-09 13:04:17 +01:00
Bri@n
9eb0f48a7a Change loop index from float to int 2020-11-09 12:55:59 +01:00
KIMMINTAE98
7b1fccdd06 complete korean translation of README.md 2020-11-09 15:29:56 +09:00
Mukhamadjonov
64ae07b03b Translated using Weblate (Uzbek (latin))
Currently translated at 36.5% (221 of 605 strings)
2020-11-08 23:47:25 +01:00
Prasanta-Hembram
6ecbbd1f79 Translated using Weblate (Santali)
Currently translated at 8.4% (51 of 605 strings)
2020-11-08 23:47:22 +01:00
Allan Nordhøy
a1fb268764 Translated using Weblate (Norwegian Bokmål)
Currently translated at 10.8% (4 of 37 strings)

Translation: NewPipe/Metadata
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/nb_NO/
2020-11-08 23:47:22 +01:00
Ajeje Brazorf
868661edf0 Translated using Weblate (Sardinian)
Currently translated at 10.8% (4 of 37 strings)

Translation: NewPipe/Metadata
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/sc/
2020-11-08 23:47:22 +01:00
ssantos
9899e63d53 Translated using Weblate (Portuguese (Portugal))
Currently translated at 54.0% (20 of 37 strings)

Translation: NewPipe/Metadata
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/pt_PT/
2020-11-08 23:47:22 +01:00
Florian
0b37b8b059 Translated using Weblate (French)
Currently translated at 24.3% (9 of 37 strings)

Translation: NewPipe/Metadata
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/fr/
2020-11-08 23:47:21 +01:00
simo
1b34ca822f Translated using Weblate (Arabic)
Currently translated at 62.1% (23 of 37 strings)

Translation: NewPipe/Metadata
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/ar/
2020-11-08 23:47:20 +01:00
Yaron Shahrabani
a4e3a874ad Translated using Weblate (Hebrew)
Currently translated at 21.6% (8 of 37 strings)

Translation: NewPipe/Metadata
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/he/
2020-11-08 23:47:19 +01:00
ssantos
8ec3df552a Translated using Weblate (Portuguese)
Currently translated at 54.0% (20 of 37 strings)

Translation: NewPipe/Metadata
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/pt/
2020-11-08 23:47:19 +01:00
nautilusx
b4b1c9256b Translated using Weblate (German)
Currently translated at 18.9% (7 of 37 strings)

Translation: NewPipe/Metadata
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/de/
2020-11-08 23:47:18 +01:00
ssantos
acee20d897 Translated using Weblate (Portuguese (Portugal))
Currently translated at 100.0% (605 of 605 strings)
2020-11-08 23:47:18 +01:00
Ajeje Brazorf
6ea3ebb72d Translated using Weblate (Sardinian)
Currently translated at 100.0% (605 of 605 strings)
2020-11-08 23:47:17 +01:00
Yaron Shahrabani
55f23e9304 Translated using Weblate (Hebrew)
Currently translated at 100.0% (605 of 605 strings)
2020-11-08 23:47:17 +01:00
Jeff Huang
ad223a04f8 Translated using Weblate (Chinese (Traditional))
Currently translated at 100.0% (605 of 605 strings)
2020-11-08 23:47:17 +01:00
Eric
0b150ea475 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (605 of 605 strings)
2020-11-08 23:47:16 +01:00
simo
167e9fbc6d Translated using Weblate (Arabic)
Currently translated at 100.0% (605 of 605 strings)
2020-11-08 23:47:16 +01:00
Bopol
77ea160cd9 Translated using Weblate (Esperanto)
Currently translated at 91.0% (551 of 605 strings)
2020-11-08 23:47:16 +01:00
ssantos
f9204450f1 Translated using Weblate (Portuguese)
Currently translated at 100.0% (605 of 605 strings)
2020-11-08 23:47:15 +01:00
Vancha March
21ef76816f Translated using Weblate (Dutch)
Currently translated at 99.8% (604 of 605 strings)
2020-11-08 23:47:15 +01:00
Florian
f166cfbac8 Translated using Weblate (French)
Currently translated at 100.0% (605 of 605 strings)
2020-11-08 23:47:15 +01:00
nautilusx
e5f64710f4 Translated using Weblate (German)
Currently translated at 100.0% (605 of 605 strings)
2020-11-08 23:47:15 +01:00
KIMMINTAE98
32a5062081 update korean translation of README.md 2020-11-09 03:37:56 +09:00
KIMMINTAE98
e6bc29281e create korean translation of README.md
just create
2020-11-08 23:01:02 +09:00
Stypox
617ee0afc0 Fix brightness and volume scroll swapped 2020-11-08 10:00:28 +01:00
Stypox
1b47a1a994 Fix switching to main player when MainActivity is closed 2020-11-08 10:00:28 +01:00
Stypox
5a87cfc25d Open mini player if player running on app open 2020-11-08 10:00:28 +01:00
Stypox
00a178f7d3 Fix tapping on video thumbnail does nothing 2020-11-08 10:00:28 +01:00
Stypox
2a2c82e73b More fixes with opening VideoDetailFragment 2020-11-08 10:00:28 +01:00
Stypox
bb882ada2c Show "Show info" instead of "Video player" if a stream is playing not on the main player when sharing something to NewPipe 2020-11-08 10:00:28 +01:00
Stypox
1d42e45d78 Unify all ways of opening VideoDetailFragment 2020-11-08 10:00:27 +01:00
Allan Nordhøy
15c4a5c9ea Translated using Weblate (Norwegian Bokmål)
Currently translated at 93.0% (563 of 605 strings)
2020-11-08 01:39:02 +01:00
Tobias Groza
f4435f9031 Merge pull request #4555 from Stypox/playqueue-crash
Fix NullPointerException in queue handling
2020-11-08 01:19:38 +01:00
Hosted Weblate
5a423c89a3 Merge branch 'origin/dev' into Weblate. 2020-11-08 01:08:16 +01:00
Tobias Groza
8b02154f5a Merge pull request #4749 from Prasanta-Hembram/dev
[Translation]Added Santali language
2020-11-08 01:02:37 +01:00
Hosted Weblate
97c454ea77 Merge branch 'origin/dev' into Weblate. 2020-11-08 01:00:30 +01:00
Mukhamadjonov
f07a6d03b5 Translated using Weblate (Uzbek (latin))
Currently translated at 20.3% (123 of 603 strings)
2020-11-08 00:55:24 +01:00
TobiGr
1f18fb5446 Remove unused "video_is_age_restricted" translations 2020-11-08 00:52:20 +01:00
TobiGr
92ee5b66ab Remove unused translation 2020-11-08 00:48:40 +01:00
TobiGr
fcc92c3e27 Merge remote-tracking branch 'Weblate/dev' into dev 2020-11-08 00:38:30 +01:00
Mukhamadjonov
3e91b5a793 Translated using Weblate (Uzbek (latin))
Currently translated at 7.4% (45 of 603 strings)
2020-11-07 22:39:48 +01:00
Vasilis K
42e5cc3bef Translated using Weblate (Greek)
Currently translated at 100.0% (603 of 603 strings)
2020-11-07 22:39:48 +01:00
Éfrit
16c61a1919 Translated using Weblate (French)
Currently translated at 100.0% (603 of 603 strings)
2020-11-07 22:39:47 +01:00
Weblate
c2b4b0490b Added translation using Weblate (Uzbek (latin)) 2020-11-07 22:00:26 +01:00
Mukhamadjonov
a310a06e3c Added translation using Weblate (Uzbek (latin)) 2020-11-07 22:00:22 +01:00
Prasanta-Hembram
9228511527 Translated using Weblate (Santali)
Currently translated at 7.9% (48 of 603 strings)
2020-11-07 08:13:43 +01:00
Gontzal Manuel Pujana Onaindia
bbc4174501 Translated using Weblate (Basque)
Currently translated at 16.6% (6 of 36 strings)

Translation: NewPipe/Metadata
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/eu/
2020-11-07 08:13:42 +01:00
Oymate
5f3196b74c Translated using Weblate (Bengali)
Currently translated at 77.9% (470 of 603 strings)
2020-11-07 08:13:42 +01:00
Vasilis K
725bd8029f Translated using Weblate (Greek)
Currently translated at 99.6% (601 of 603 strings)
2020-11-07 08:13:41 +01:00
Gontzal Manuel Pujana Onaindia
479ab5df0e Translated using Weblate (Basque)
Currently translated at 100.0% (603 of 603 strings)
2020-11-07 08:13:39 +01:00
xxkfqz
18c45ad30b Translated using Weblate (Russian)
Currently translated at 100.0% (603 of 603 strings)
2020-11-07 08:13:38 +01:00
Vasilis K
6f32f098eb Translated using Weblate (Greek)
Currently translated at 92.0% (555 of 603 strings)
2020-11-06 12:23:03 +01:00
Michalis Nikolaidis
e8289d3912 Translated using Weblate (Greek)
Currently translated at 92.0% (555 of 603 strings)
2020-11-06 12:23:03 +01:00
pjammo
472d9322ce Translated using Weblate (Italian)
Currently translated at 22.2% (8 of 36 strings)

Translation: NewPipe/Metadata
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/it/
2020-11-05 17:14:08 +01:00
Nyatsuki
0233ffafb6 Translated using Weblate (Japanese)
Currently translated at 16.6% (6 of 36 strings)

Translation: NewPipe/Metadata
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/ja/
2020-11-05 17:14:07 +01:00
Oymate
468ee4756f Translated using Weblate (Bengali)
Currently translated at 73.6% (444 of 603 strings)
2020-11-05 17:14:06 +01:00
Milo Ivir
abf9365bbe Translated using Weblate (Croatian)
Currently translated at 99.0% (597 of 603 strings)
2020-11-05 17:14:04 +01:00
Oymate
1e0789162f Translated using Weblate (Bengali (Bangladesh))
Currently translated at 61.3% (370 of 603 strings)
2020-11-05 17:14:04 +01:00
fieldfile
31f407f4e8 Translated using Weblate (Ukrainian)
Currently translated at 94.3% (569 of 603 strings)
2020-11-05 17:14:04 +01:00
Nyatsuki
77330ffc50 Translated using Weblate (Japanese)
Currently translated at 100.0% (603 of 603 strings)
2020-11-05 17:14:03 +01:00
Stypox
6f132f3fed Merge pull request #4556 from Isira-Seneviratne/Switch_to_Java_8_Date_Time_API
Switch to the Java 8 Date/Time API.
2020-11-05 13:02:04 +01:00
Stypox
c193b4f07c Merge pull request #4499 from Isira-Seneviratne/Replace_AsyncTasks_with_ReactiveX
Use RxJava instead of AsyncTask.
2020-11-05 12:39:33 +01:00
Isira Seneviratne
c745b845c5 Switch to the Java 8 Date/Time API. 2020-11-05 15:02:51 +05:30
Isira Seneviratne
3b69e0dd25 Use RxJava instead of AsyncTask in MissionAdapter. 2020-11-05 11:06:51 +05:30
Isira Seneviratne
8ec55ef394 Use RxJava instead of AsyncTask in LicenseFragmentHelper. 2020-11-05 11:06:51 +05:30
Isira Seneviratne
ef5084036c Use RxJava instead of AsyncTask to check for new app versions. 2020-11-05 11:06:50 +05:30
Tobias Groza
7dd317e530 Merge pull request #4759 from Stypox/yt-import
Change YouTube subscription import instructions to Google takeout
2020-11-04 16:52:43 +01:00
Prasanta-Hembram
e5db3ed9b7 Translated using Weblate (Santali)
Currently translated at 7.6% (46 of 603 strings)
2020-11-04 01:23:34 +01:00
aqwer-T
3a00dc5b5f Translated using Weblate (Lithuanian)
Currently translated at 0.0% (0 of 36 strings)

Translation: NewPipe/Metadata
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/lt/
2020-11-04 01:23:33 +01:00
Oymate
2d848020fc Translated using Weblate (Bengali)
Currently translated at 64.0% (386 of 603 strings)
2020-11-04 01:23:33 +01:00
Stjepan
70123d19fe Translated using Weblate (Croatian)
Currently translated at 98.5% (594 of 603 strings)
2020-11-04 01:23:33 +01:00
aqwer-T
ea3770260a Translated using Weblate (Lithuanian)
Currently translated at 52.2% (315 of 603 strings)
2020-11-04 01:23:32 +01:00
Vladislav
eea1a80de6 Translated using Weblate (Russian)
Currently translated at 99.3% (599 of 603 strings)
2020-11-04 01:23:32 +01:00
Tobias Groza
4889775ae6 Merge pull request #4790 from TeamNewPipe/promote_custom_repo
Update readme and add custom repo info
2020-11-03 22:02:24 +01:00
opusforlife2
355effd93d Mention custom repo and generally update section
Added custom repo as additional install method, and added comparison between them all. Also removed the outdated part.
2020-11-03 16:56:04 +00:00
Stypox
f1583b6e0c Merge pull request #4587 from vkay94/separate-player-gesture-logic-ui
Separate player gesture logic and UI
2020-11-02 16:36:50 +01:00
vkay94
347566c311 Player gestures: Add multi-double-tap logic 2020-11-02 15:50:34 +01:00
Stypox
1f73572dd3 Fix playlist select dialog and do some refactoring 2020-11-02 14:24:39 +01:00
Prasanta-Hembram
2bfb83c4cd Translated using Weblate (Santali)
Currently translated at 7.4% (45 of 603 strings)
2020-11-02 12:36:09 +01:00
zeritti
ceed1c4962 Translated using Weblate (Czech)
Currently translated at 100.0% (603 of 603 strings)
2020-11-02 12:36:09 +01:00
Juraj Liso
2b5b9d3599 Translated using Weblate (Slovak)
Currently translated at 100.0% (603 of 603 strings)
2020-11-02 12:36:08 +01:00
Franco
96e3709b7b Translated using Weblate (Spanish)
Currently translated at 100.0% (603 of 603 strings)
2020-11-02 12:36:08 +01:00
Stypox
239fc2f6f8 Change youtube subscription import instructions 2020-11-02 11:08:38 +01:00
Stypox
4c77e5cdd2 Merge pull request #4643 from XiangRongLin/colorize_notification
Add option to not colorize notification
2020-11-01 22:24:16 +01:00
Xiang Rong Lin
974f8f692c Add option to not colorize notification 2020-11-01 22:13:00 +01:00
Tobias Groza
e97d0b9a69 Merge pull request #3817 from Isira-Seneviratne/Use_Java_8_APIs
Use Java 8 APIs.
2020-11-01 16:30:50 +01:00
Isira Seneviratne
b0b0a75c87 Use Collection.removeIf() instead of using Iterator.remove() to remove elements conditionally. 2020-11-01 14:44:07 +05:30
Isira Seneviratne
abcacf8c74 Use Comparator's comparing(), nullsLast() and reversed() methods. 2020-11-01 14:44:04 +05:30
Isira Seneviratne
290428b981 Enable support for core library desugaring. 2020-11-01 14:44:02 +05:30
Prasanta Hembram
37d1541d6b Added Santali language 2020-11-01 13:46:51 +05:30
Tobias Groza
1500ce7490 Merge pull request #4710 from TacoTheDank/more-cleanup
More miscellaneous little fixes and improvements
2020-10-31 23:17:23 +01:00
TacoTheDank
a48529872d Fix a few Kotlin style issues 2020-10-31 15:57:49 -04:00
TacoTheDank
31cffa68c5 Push conditionals inside branch expressions 2020-10-31 15:57:26 -04:00
TacoTheDank
6909d1e527 Simplify an if else 2020-10-31 15:57:01 -04:00
TacoTheDank
972235bfba Add missing app:iconSpaceReserved 2020-10-31 15:56:46 -04:00
TacoTheDank
6db560fd2c Use FragmentActivity for AboutActivity's viewpager2 2020-10-31 15:54:39 -04:00
TacoTheDank
1a64d8aec9 Replace a ContextCompat with LayoutInflater.from 2020-10-31 15:54:19 -04:00
TacoTheDank
1e1fb32558 Fix some version checks to use android.os.Build 2020-10-31 15:54:02 -04:00
XiangRongLin
008eb5ba4a Convert notification actions to a custom preference (#4652) 2020-10-31 11:58:33 +01:00
J. Lavoie
f46e0acc89 Translated using Weblate (Finnish)
Currently translated at 100.0% (603 of 603 strings)
2020-10-31 10:38:59 +01:00
Davit Mayilyan
b615ef5810 Translated using Weblate (Armenian)
Currently translated at 16.7% (101 of 603 strings)
2020-10-31 10:38:59 +01:00
Berkay Gündüz
d773279de8 Translated using Weblate (Turkish)
Currently translated at 100.0% (603 of 603 strings)
2020-10-31 10:38:58 +01:00
zmni
56d721651a Translated using Weblate (Indonesian)
Currently translated at 100.0% (603 of 603 strings)
2020-10-31 10:38:57 +01:00
JohnFai
ee17abff92 Translated using Weblate (Chinese (Traditional, Hong Kong))
Currently translated at 27.1% (164 of 603 strings)
2020-10-31 10:38:57 +01:00
J. Lavoie
952bb1a2eb Translated using Weblate (Italian)
Currently translated at 100.0% (603 of 603 strings)
2020-10-31 10:38:57 +01:00
J. Lavoie
c287813e00 Translated using Weblate (French)
Currently translated at 100.0% (603 of 603 strings)
2020-10-31 10:38:56 +01:00
J. Lavoie
eab4fd80d7 Translated using Weblate (Spanish)
Currently translated at 100.0% (603 of 603 strings)
2020-10-31 10:38:56 +01:00
SiD ViCiO
5c8f8869d4 Translated using Weblate (Spanish)
Currently translated at 100.0% (603 of 603 strings)
2020-10-31 10:38:56 +01:00
J. Lavoie
4954dfe107 Translated using Weblate (German)
Currently translated at 100.0% (603 of 603 strings)
2020-10-31 10:38:56 +01:00
Tobias Groza
4eb8094fb8 Merge pull request #4695 from mimi89999/dev
Add missing screenshots
2020-10-30 18:04:32 +01:00
mimi89999
1dd2423a0b Add missing screenshots 2020-10-30 17:26:11 +01:00
Ville Rantanen
6fcf989c62 Translated using Weblate (Finnish)
Currently translated at 100.0% (603 of 603 strings)
2020-10-30 14:34:04 +01:00
J. Lavoie
67d1a4f643 Translated using Weblate (Finnish)
Currently translated at 100.0% (603 of 603 strings)
2020-10-30 14:34:03 +01:00
J. Lavoie
3df9433baf Translated using Weblate (German)
Currently translated at 100.0% (603 of 603 strings)
2020-10-30 14:21:03 +01:00
C. Rüdinger
b12d568147 Translated using Weblate (German)
Currently translated at 100.0% (603 of 603 strings)
2020-10-30 14:21:02 +01:00
Weblate
8691e035a0 Added translation using Weblate (English (United Kingdom)) 2020-10-29 13:06:34 +01:00
vkay94
2683043762 Player gestures: separate logic and UI 2020-10-29 12:46:34 +01:00
Boris Petrov
be5aa59f61 Translated using Weblate (French)
Currently translated at 19.4% (7 of 36 strings)

Translation: NewPipe/Metadata
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/fr/
2020-10-29 00:57:55 +01:00
Franco
9398dfb7cf Translated using Weblate (Spanish)
Currently translated at 16.6% (6 of 36 strings)

Translation: NewPipe/Metadata
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/es/
2020-10-29 00:57:54 +01:00
simo
ff6d2b30e4 Translated using Weblate (Arabic)
Currently translated at 52.7% (19 of 36 strings)

Translation: NewPipe/Metadata
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/ar/
2020-10-29 00:57:54 +01:00
Michal L
29bb999a32 Translated using Weblate (Polish)
Currently translated at 36.1% (13 of 36 strings)

Translation: NewPipe/Metadata
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/pl/
2020-10-29 00:57:53 +01:00
nalinalini
9320507e26 Translated using Weblate (Hindi)
Currently translated at 0.0% (0 of 36 strings)

Translation: NewPipe/Metadata
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/hi/
2020-10-29 00:57:52 +01:00
Paulo Almeida
181fc4fa0a Translated using Weblate (Portuguese (Brazil))
Currently translated at 16.6% (6 of 36 strings)

Translation: NewPipe/Metadata
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/pt_BR/
2020-10-29 00:57:52 +01:00
Allan Nordhøy
4f3dd4b662 Translated using Weblate (Norwegian Bokmål)
Currently translated at 93.3% (563 of 603 strings)
2020-10-29 00:57:52 +01:00
nalinalini
7b09de99ea Translated using Weblate (Hindi)
Currently translated at 77.9% (470 of 603 strings)
2020-10-29 00:57:51 +01:00
Milo Ivir
f9f0da18e1 Translated using Weblate (Croatian)
Currently translated at 98.5% (594 of 603 strings)
2020-10-29 00:57:51 +01:00
Michal L
ba0fdb9478 Translated using Weblate (Polish)
Currently translated at 100.0% (603 of 603 strings)
2020-10-29 00:57:49 +01:00
simo
8c684bca22 Translated using Weblate (Arabic)
Currently translated at 100.0% (603 of 603 strings)
2020-10-29 00:57:49 +01:00
Boris Petrov
1266a75549 Translated using Weblate (French)
Currently translated at 100.0% (603 of 603 strings)
2020-10-29 00:57:48 +01:00
TobiGr
3871d5aed7 Merge branch 'master' into dev 2020-10-28 15:14:48 +01:00
Tobias Groza
25d555126d Merge pull request #4675 from B0pol/hotfix
Release 0.20.2 (956)
2020-10-28 07:39:44 +01:00
Tobias Groza
f529d15d7a Update fastlane/metadata/android/en-US/changelogs/956.txt
Co-authored-by: opusforlife2 <53176348+opusforlife2@users.noreply.github.com>
2020-10-28 07:35:04 +01:00
TobiGr
9eda30fc71 Bump extractor version 2020-10-28 07:34:34 +01:00
Tobias Groza
dfd6424d9c Merge pull request #4655 from B0pol/polish
polish strings
2020-10-28 07:26:38 +01:00
Weblate
fdc961f2de Added translation using Weblate (Odia) 2020-10-27 15:17:51 +01:00
nalinalini
a6d4000d24 Added translation using Weblate (Odia) 2020-10-27 15:17:40 +01:00
bopol
e1024e59c3 Release 0.20.2 (956) 2020-10-27 14:47:23 +01:00
Edoardo Regni
990164802d Translated using Weblate (Dutch (Belgium))
Currently translated at 2.7% (1 of 36 strings)

Translation: NewPipe/Metadata
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/nl_BE/
2020-10-27 13:51:38 +01:00
Ajeje Brazorf
34f18fbdb3 Translated using Weblate (Sardinian)
Currently translated at 100.0% (603 of 603 strings)
2020-10-27 13:51:38 +01:00
Bjorn Roesbeke
17e24bb038 Translated using Weblate (Dutch (Belgium))
Currently translated at 100.0% (603 of 603 strings)
2020-10-27 13:51:37 +01:00
Edoardo Regni
df7e2b7734 Translated using Weblate (Dutch (Belgium))
Currently translated at 100.0% (603 of 603 strings)
2020-10-27 13:51:37 +01:00
Vojtěch Šamla
1ddef06bd2 Translated using Weblate (Czech)
Currently translated at 98.6% (595 of 603 strings)
2020-10-27 13:51:36 +01:00
pjammo
18bd910bf0 Translated using Weblate (Italian)
Currently translated at 100.0% (603 of 603 strings)
2020-10-27 13:51:36 +01:00
Bjorn Roesbeke
0db44f6e33 Translated using Weblate (Dutch)
Currently translated at 100.0% (603 of 603 strings)
2020-10-27 13:51:36 +01:00
bopol
7aac3d38f0 Update Extractor version 2020-10-26 18:59:06 +01:00
Stypox
e406b6f780 Fix NullPointerException in queue handling 2020-10-26 18:15:59 +01:00
bopol
2dad9666a9 polish strings, fix build error 2020-10-26 17:01:55 +01:00
TobiGr
063abf1688 Update checkstyle 2020-10-25 21:29:07 +01:00
Tobias Groza
a86f8e9a22 Merge pull request #4592 from Isira-Seneviratne/Use_DrawableCompat
Use DrawableCompat.
2020-10-25 21:13:02 +01:00
Edoardo Regni
0ce6d4fe92 Translated using Weblate (Dutch (Belgium))
Currently translated at 99.8% (602 of 603 strings)
2020-10-25 20:40:57 +01:00
zmni
886f6c721c Translated using Weblate (Indonesian)
Currently translated at 99.8% (602 of 603 strings)
2020-10-25 20:40:57 +01:00
Gontzal Manuel Pujana Onaindia
9d7d089279 Translated using Weblate (Basque)
Currently translated at 99.3% (599 of 603 strings)
2020-10-25 20:40:56 +01:00
Isira Seneviratne
0bd624dfa9 Use DrawableCompat. 2020-10-25 21:01:53 +05:30
Eric
8c29760d93 Translated using Weblate (Chinese (Simplified))
Currently translated at 63.8% (23 of 36 strings)

Translation: NewPipe/Metadata
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/zh_Hans/
2020-10-25 16:16:26 +01:00
SeungCheol Han
9b893d841d Translated using Weblate (Korean)
Currently translated at 5.5% (2 of 36 strings)

Translation: NewPipe/Metadata
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/ko/
2020-10-25 16:16:25 +01:00
Yaron Shahrabani
e8c0163153 Translated using Weblate (Hebrew)
Currently translated at 13.8% (5 of 36 strings)

Translation: NewPipe/Metadata
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/he/
2020-10-25 16:16:25 +01:00
nautilusx
256568d966 Translated using Weblate (German)
Currently translated at 16.6% (6 of 36 strings)

Translation: NewPipe/Metadata
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/de/
2020-10-25 16:16:24 +01:00
ssantos
34bed47a52 Translated using Weblate (Portuguese (Portugal))
Currently translated at 100.0% (603 of 603 strings)
2020-10-25 16:16:24 +01:00
Edoardo Regni
9b0996fade Translated using Weblate (Dutch (Belgium))
Currently translated at 99.8% (602 of 603 strings)
2020-10-25 16:16:23 +01:00
Azizov Aga
7c1028df5d Translated using Weblate (Azerbaijani)
Currently translated at 26.5% (160 of 603 strings)
2020-10-25 16:16:20 +01:00
Milo Ivir
eaea60e0cb Translated using Weblate (Croatian)
Currently translated at 90.5% (546 of 603 strings)
2020-10-25 16:16:19 +01:00
Allan Nordhøy
efab05dcfc Translated using Weblate (Swedish)
Currently translated at 93.2% (562 of 603 strings)
2020-10-25 16:16:19 +01:00
Yaron Shahrabani
d79f77f7e0 Translated using Weblate (Hebrew)
Currently translated at 100.0% (603 of 603 strings)
2020-10-25 16:16:18 +01:00
Jeff Huang
f0d459d490 Translated using Weblate (Chinese (Traditional))
Currently translated at 100.0% (603 of 603 strings)
2020-10-25 16:16:18 +01:00
Eric
e269c073ac Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (603 of 603 strings)
2020-10-25 16:16:17 +01:00
Samuel Carvalho de Araújo
85d5609144 Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (603 of 603 strings)
2020-10-25 16:16:17 +01:00
Emin Tufan Çetin
671c593db1 Translated using Weblate (Turkish)
Currently translated at 100.0% (603 of 603 strings)
2020-10-25 16:16:16 +01:00
simo
8b14c7a2cb Translated using Weblate (Arabic)
Currently translated at 99.0% (597 of 603 strings)
2020-10-25 16:16:16 +01:00
Josu
23862419eb Translated using Weblate (Basque)
Currently translated at 97.8% (590 of 603 strings)
2020-10-25 16:16:15 +01:00
ssantos
43d54db4dd Translated using Weblate (Portuguese)
Currently translated at 100.0% (603 of 603 strings)
2020-10-25 16:16:14 +01:00
AioiLight
b8b0060440 Translated using Weblate (Japanese)
Currently translated at 100.0% (603 of 603 strings)
2020-10-25 16:16:14 +01:00
Mitosagi
64d79ceb30 Translated using Weblate (Japanese)
Currently translated at 100.0% (603 of 603 strings)
2020-10-25 16:16:13 +01:00
Nikita Epifanov
25f7b44d48 Translated using Weblate (Russian)
Currently translated at 99.1% (598 of 603 strings)
2020-10-25 16:16:13 +01:00
Edoardo Regni
ada7e628da Translated using Weblate (Dutch)
Currently translated at 100.0% (603 of 603 strings)
2020-10-25 16:16:13 +01:00
nautilusx
76f2338c3d Translated using Weblate (German)
Currently translated at 100.0% (603 of 603 strings)
2020-10-25 16:16:12 +01:00
Stypox
a0ed8036c0 Merge pull request #4594 from Isira-Seneviratne/Use_TextViewCompat
Use TextViewCompat.setCompoundDrawablesRelativeWithIntrinsicBounds().
2020-10-25 14:44:01 +01:00
TobiGr
863ce65b10 Fix Issue Template config 2020-10-24 20:44:22 +02:00
Weblate
85190b16cb Added translation using Weblate (Kashmiri) 2020-10-24 05:01:01 +02:00
TobiGr
abc6fd8b2a polish more 2020-10-23 23:22:00 +02:00
TobiGr
b6f603154e polish some stuff 2020-10-23 23:18:34 +02:00
Tobias Groza
90cb9d3de1 Merge pull request #4549 from Stypox/fix-showMiniPlayer
Fix IllegalStateException after onSaveInstanceState
2020-10-23 21:58:20 +02:00
Viktor K
d47c9a2e29 Translated using Weblate (German)
Currently translated at 99.8% (602 of 603 strings)
2020-10-23 19:49:48 +02:00
TobiGr
b7aea96ca0 Translated using Weblate (German)
Currently translated at 99.8% (602 of 603 strings)
2020-10-23 19:49:47 +02:00
nautilusx
a5879a4407 Translated using Weblate (German)
Currently translated at 99.8% (602 of 603 strings)
2020-10-23 19:49:47 +02:00
Tobias Groza
0452c69771 Merge pull request #4610 from mhmdanas/optimize-pngs
Optimize app PNGs
2020-10-23 19:37:23 +02:00
Tobias Groza
4eb9dff45e Merge pull request #4611 from TeamNewPipe/prevent_blank_issues
Create config.yaml to prevent blank issues
2020-10-23 19:18:25 +02:00
opusforlife2
b7c1e88b59 Create config.yaml to prevent blank issues 2020-10-23 14:37:59 +00:00
mhmdanas
23814330d9 Optimize app PNGs 2020-10-23 17:00:39 +03:00
Hosted Weblate
8bc75aacea Merge branch 'origin/dev' into Weblate. 2020-10-23 10:54:04 +02:00
Prasanta-Hembram
17576d223a Translated using Weblate (Santali)
Currently translated at 6.1% (37 of 600 strings)
2020-10-23 10:54:04 +02:00
mmagian
90e8f0ca63 Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (600 of 600 strings)
2020-10-23 10:54:03 +02:00
TobiGr
adc4a811b7 Merge remote-tracking branch 'Weblate/dev' into dev 2020-10-23 10:52:51 +02:00
Tobias Groza
7c80233f26 Merge pull request #4345 from vkay94/fix-createplaylist
Fix PlaylistAppendDialog showing when no local playlists exist
2020-10-23 10:08:20 +02:00
Tobias Groza
f05ae2de35 Merge pull request #4606 from TeamNewPipe/refine_issue_templates
Refined instructions in the issue templates
2020-10-23 09:59:13 +02:00
opusforlife2
cf9da556a8 Refined instructions for feature request template 2020-10-23 07:55:31 +00:00
vkay94
32a142bf79 Fix PlaylistAppendDialog: Renamed method and replaced with Runnable 2020-10-23 09:44:26 +02:00
vkay94
2680d41a3d Fix PlaylistAppendDialog showing when no local playlists exist 2020-10-23 09:44:26 +02:00
opusforlife2
1550fc4398 Refined instructions for bug report template 2020-10-23 07:32:04 +00:00
Weblate
f2a85f3b7e Added translation using Weblate (Santali) 2020-10-22 20:40:29 +02:00
Milo Ivir
49c2b4c196 Translated using Weblate (Croatian)
Currently translated at 5.7% (2 of 35 strings)

Translation: NewPipe/Metadata
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/hr/
2020-10-22 20:34:47 +02:00
Digiwizkid
a8281e174e Translated using Weblate (Bengali (India))
Currently translated at 58.8% (353 of 600 strings)
2020-10-22 20:34:47 +02:00
Shiv
d7f5c8bd55 Translated using Weblate (Hindi)
Currently translated at 78.6% (472 of 600 strings)
2020-10-22 20:34:46 +02:00
Rishi Dutt Shukla
b3337df88b Translated using Weblate (Hindi)
Currently translated at 78.6% (472 of 600 strings)
2020-10-22 20:34:46 +02:00
Yaron Shahrabani
d760616e55 Translated using Weblate (Hebrew)
Currently translated at 100.0% (600 of 600 strings)
2020-10-22 20:34:46 +02:00
Jeff Huang
914a4d32b4 Translated using Weblate (Chinese (Traditional))
Currently translated at 100.0% (600 of 600 strings)
2020-10-22 20:34:46 +02:00
David Braz
148f53e21e Translated using Weblate (Portuguese (Brazil))
Currently translated at 99.8% (599 of 600 strings)
2020-10-22 20:34:45 +02:00
Vojtěch Šamla
5e7fa0f964 Translated using Weblate (Czech)
Currently translated at 100.0% (600 of 600 strings)
2020-10-22 20:34:45 +02:00
Vibo Lavida
0973ceb9d2 Translated using Weblate (Spanish)
Currently translated at 100.0% (600 of 600 strings)
2020-10-22 20:34:45 +02:00
bopol
5214bfe8cb Merge pull request #4554 from mitosagi/translate-numbers
Translates shortened notation of numbers
2020-10-22 19:05:53 +02:00
Isira Seneviratne
187aaafddc Use TextViewCompat.setCompoundDrawablesRelativeWithIntrinsicBounds(). 2020-10-22 06:01:49 +05:30
opusforlife2
208cb405ca Merge pull request #4559 from TeamNewPipe/remove_coming_features
Removed "Coming features" because why??
2020-10-19 04:35:53 +00:00
opusforlife2
9b9d267cd4 Merge pull request #4553 from TacoTheDank/about-viewpager2
Migrate AboutActivity to ViewPager2
2020-10-18 10:12:20 +00:00
opusforlife2
6f90a27f9f Removed "Coming features" because why?? 2020-10-18 10:09:45 +00:00
Stypox
4e1dddc06d Merge pull request #4557 from Isira-Seneviratne/Use_multidex_for_all_build_types
Use multidex for all build types.
2020-10-18 12:09:42 +02:00
Hakim Oubouali
1f504d6f23 Translated using Weblate (Central Atlas Tamazight)
Currently translated at 28.6% (172 of 600 strings)
2020-10-18 11:29:15 +02:00
Hakim Oubouali
2db1fd813f Translated using Weblate (Berber)
Currently translated at 23.6% (142 of 600 strings)
2020-10-18 11:29:11 +02:00
Sérgio Marques
f39383a3d8 Translated using Weblate (Portuguese (Portugal))
Currently translated at 100.0% (600 of 600 strings)
2020-10-18 11:29:08 +02:00
Ajeje Brazorf
b593bdbf1b Translated using Weblate (Sardinian)
Currently translated at 99.8% (599 of 600 strings)
2020-10-18 11:29:07 +02:00
PPNplus
4de93ba3c0 Translated using Weblate (Thai)
Currently translated at 47.0% (282 of 600 strings)
2020-10-18 11:29:07 +02:00
Azizov Aga
2eb7a91987 Translated using Weblate (Azerbaijani)
Currently translated at 22.3% (134 of 600 strings)
2020-10-18 11:29:06 +02:00
Garden Hose
94e4264c2a Translated using Weblate (Croatian)
Currently translated at 87.1% (523 of 600 strings)
2020-10-18 11:29:05 +02:00
Eric
e54d28f157 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (600 of 600 strings)
2020-10-18 11:29:05 +02:00
Ali Demirtas
6111c8bde0 Translated using Weblate (Turkish)
Currently translated at 100.0% (600 of 600 strings)
2020-10-18 11:29:04 +02:00
zmni
3bacdfd4fc Translated using Weblate (Indonesian)
Currently translated at 100.0% (600 of 600 strings)
2020-10-18 11:29:03 +02:00
Hakim Oubouali
65db645ff9 Translated using Weblate (Arabic)
Currently translated at 100.0% (600 of 600 strings)
2020-10-18 11:29:02 +02:00
random r
de2e2c45a5 Translated using Weblate (Italian)
Currently translated at 100.0% (600 of 600 strings)
2020-10-18 11:29:02 +02:00
Mitosagi
9b655e18e3 Translated using Weblate (Japanese)
Currently translated at 99.8% (599 of 600 strings)
2020-10-18 11:29:02 +02:00
Nikita Epifanov
fb4b9b5f76 Translated using Weblate (Russian)
Currently translated at 100.0% (600 of 600 strings)
2020-10-18 11:29:02 +02:00
Ali Demirtas
820b39840a Translated using Weblate (English)
Currently translated at 100.0% (600 of 600 strings)
2020-10-18 11:29:01 +02:00
mabroukb
4b1052eb70 Translated using Weblate (French)
Currently translated at 99.6% (598 of 600 strings)
2020-10-18 11:29:01 +02:00
nautilusx
c0eb3972a7 Translated using Weblate (German)
Currently translated at 100.0% (600 of 600 strings)
2020-10-18 11:29:01 +02:00
Isira Seneviratne
66ba8d56b7 Use multidex for all build types. 2020-10-18 14:14:27 +05:30
Stypox
a73baf32f1 Merge pull request #4547 from Isira-Seneviratne/Use_Core_KTX_functions
Use Core KTX functions.
2020-10-18 09:39:28 +02:00
Tobias Groza
333cf0a2f0 Merge pull request #4550 from Stypox/no-drag-thumbnails
Don't rearrange lists by dragging the thumbnails
2020-10-18 09:00:56 +02:00
mitosagi
8347d8700a Translate the numeric notation 2020-10-18 11:01:06 +09:00
TacoTheDank
09af0e2448 Migrate AboutActivity to viewpager2 2020-10-17 19:15:10 -04:00
Tobias Groza
001914764a Merge pull request #4530 from TeamNewPipe/weblate-widget
Add Weblate widget to README
2020-10-17 17:08:35 +02:00
Stypox
6938dd6267 Add Weblate widget to README 2020-10-17 16:41:39 +02:00
Stypox
941028ba6f Don't rearrange lists by dragging the thumbnails 2020-10-17 16:25:06 +02:00
Stypox
4ca7ed9f8c Fix IllegalStateException after onSaveInstanceState 2020-10-17 16:13:42 +02:00
Isira Seneviratne
03d99887c5 Use TextView.doOnTextChanged() extension. 2020-10-17 19:22:13 +05:30
Isira Seneviratne
293e2ff5e3 Use isVisible and isGone extensions for View. 2020-10-17 15:54:35 +05:30
Isira Seneviratne
55d242fa08 Use bundleOf(). 2020-10-17 15:38:45 +05:30
TobiGr
7e9fba2d96 Merge branch 'master' into dev 2020-10-16 22:33:58 +02:00
Tobias Groza
3c5ed2c885 Merge pull request #4453 from wb9688/clear-cookies
Add button in settings to clear reCAPTCHA cookies
2020-10-16 17:25:19 +02:00
Tobias Groza
c4af93c363 Merge pull request #4528 from TeamNewPipe/local-extractor
Add info on how to use a local NewPipe Extractor version
2020-10-16 17:24:35 +02:00
Sérgio Marques
e2685c4503 Translated using Weblate (Portuguese)
Currently translated at 100.0% (600 of 600 strings)
2020-10-16 16:55:13 +02:00
TobiGr
48b1d3fff8 Add info to build.gradle and settings.gradle on how to use a local
NewPipe extractor version.
2020-10-16 13:27:09 +02:00
Tobias Groza
6c4920949d Merge pull request #4517 from wb9688/disable-ktlint
Disable Ktlint for now
2020-10-15 17:54:48 +02:00
wb9688
69447b75af Disable Ktlint for now 2020-10-15 14:38:59 +02:00
Allan Nordhøy
e1104570a9 Pull request template reworked (#4317)
Co-authored-by: opusforlife2 <53176348+opusforlife2@users.noreply.github.com>
2020-10-14 21:30:30 +02:00
wb9688
2c23678fb9 Add button in settings to clear reCAPTCHA cookies 2020-10-14 14:39:08 +02:00
Tobias Groza
613070d39f Merge pull request #4471 from wb9688/format-xml
Format all XML resources
2020-10-14 13:36:32 +02:00
wb9688
6deae64f45 Delete old player notification layouts 2020-10-14 11:04:49 +02:00
wb9688
aced2b124c Format all XML resources 2020-10-14 11:04:48 +02:00
Hakim Oubouali
e6b08de2e8 Translated using Weblate (Central Atlas Tamazight)
Currently translated at 8.1% (49 of 600 strings)
2020-10-13 11:26:57 +02:00
random r
d2d02d0749 Translated using Weblate (Italian)
Currently translated at 11.4% (4 of 35 strings)

Translation: NewPipe/Metadata
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/it/
2020-10-13 11:26:57 +02:00
MohammedSR Vevo
28d27801b2 Translated using Weblate (Kurdish)
Currently translated at 99.3% (596 of 600 strings)
2020-10-13 11:26:56 +02:00
Hakim Oubouali
be340dd275 Translated using Weblate (Central Atlas Tamazight)
Currently translated at 8.0% (48 of 600 strings)
2020-10-12 11:27:02 +02:00
TobiGr
58090fb3de Translated using Weblate (German)
Currently translated at 100.0% (600 of 600 strings)
2020-10-12 11:27:01 +02:00
Stypox
ae33c6cf18 Merge pull request #4476 from vkay94/two-finger-to-close-player
Two finger to close player gesture
2020-10-11 14:51:45 +02:00
vkay94
f8cd6afbf8 Two finger gesture: Less code lines 2020-10-11 13:56:30 +02:00
Tobias Groza
6fce06906d Merge pull request #4354 from Stypox/restriction-strings
Improve age restriction and yt restricted content strings
2020-10-11 12:14:13 +02:00
Stypox
84694a8bbd Improve age restriction and yt restricted content strings 2020-10-11 12:06:36 +02:00
chr56
1639e68424 Translated using Weblate (Chinese (Simplified))
Currently translated at 62.8% (22 of 35 strings)

Translation: NewPipe/Metadata
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/zh_Hans/
2020-10-11 11:25:51 +02:00
sivemortenfan
ff48fe8b49 Translated using Weblate (Malayalam)
Currently translated at 2.8% (1 of 35 strings)

Translation: NewPipe/Metadata
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/ml/
2020-10-11 11:25:50 +02:00
Milo Ivir
efe06267ec Translated using Weblate (Croatian)
Currently translated at 2.8% (1 of 35 strings)

Translation: NewPipe/Metadata
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/hr/
2020-10-11 11:25:50 +02:00
Milo Ivir
53647ea5a8 Translated using Weblate (Croatian)
Currently translated at 86.8% (521 of 600 strings)
2020-10-11 11:25:50 +02:00
C. Rüdinger
7742de5af4 Translated using Weblate (German)
Currently translated at 100.0% (600 of 600 strings)
2020-10-11 11:25:49 +02:00
Stypox
724a260f71 Merge pull request #4413 from Stypox/delete-stream-state
Also delete stream state when deleting stream history
2020-10-10 22:29:33 +02:00
Stypox
cf75e40332 Merge pull request #4463 from opusforlife2/confirm_queue_delete_one_track
Ask for confirmation before clearing queue even if only 1 video in it
2020-10-10 22:01:55 +02:00
Stypox
3c67df263c Merge pull request #4276 from Isira-Seneviratne/Use_ContextCompat_methods
Use ContextCompat methods.
2020-10-10 21:51:34 +02:00
Stypox
c4ae72c3c1 Merge pull request #4450 from wb9688/fix-release-debug-settings
Fix compiling release build
2020-10-10 21:14:39 +02:00
vkay94
f6925fc5b8 Added two finger to close player gesture 2020-10-10 15:00:39 +02:00
Eric
18be9655d6 Translated using Weblate (Chinese (Simplified))
Currently translated at 62.8% (22 of 35 strings)

Translation: NewPipe/Metadata
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/zh_Hans/
2020-10-10 11:26:15 +02:00
S
6235b6123e Translated using Weblate (German)
Currently translated at 14.2% (5 of 35 strings)

Translation: NewPipe/Metadata
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/de/
2020-10-10 11:26:13 +02:00
opusforlife2
b3555385e6 Ask for confirmation before clearing queue...
Even when there is only one video in it.
2020-10-09 14:46:42 +00:00
Hakim Oubouali
c9fbdb322b Translated using Weblate (Central Atlas Tamazight)
Currently translated at 7.6% (46 of 600 strings)
2020-10-09 11:26:02 +02:00
Mostafa Ahangarha
94bac7d8db Translated using Weblate (Persian)
Currently translated at 14.2% (5 of 35 strings)

Translation: NewPipe/Metadata
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/fa/
2020-10-09 11:26:01 +02:00
Thien Bui
f38119be96 Translated using Weblate (Vietnamese)
Currently translated at 11.4% (4 of 35 strings)

Translation: NewPipe/Metadata
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/vi/
2020-10-09 11:26:01 +02:00
zeritti
bc1d2ba839 Translated using Weblate (Czech)
Currently translated at 5.7% (2 of 35 strings)

Translation: NewPipe/Metadata
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/cs/
2020-10-09 11:26:00 +02:00
Nikita Epifanov
18f5b70b1f Translated using Weblate (Russian)
Currently translated at 5.7% (2 of 35 strings)

Translation: NewPipe/Metadata
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/ru/
2020-10-09 11:26:00 +02:00
Ajeje Brazorf
7df9b07305 Translated using Weblate (Sardinian)
Currently translated at 100.0% (600 of 600 strings)
2020-10-09 11:26:00 +02:00
Stjepan
cf01c1fd1f Translated using Weblate (Croatian)
Currently translated at 86.3% (518 of 600 strings)
2020-10-09 11:25:59 +02:00
Thien Bui
2ce6fe420b Translated using Weblate (Vietnamese)
Currently translated at 100.0% (600 of 600 strings)
2020-10-09 11:25:58 +02:00
Isira Seneviratne
f55381d689 Combine initNotificationChannel() and setUpUpdateNotificationChannel() into a single method. 2020-10-09 08:52:05 +05:30
Isira Seneviratne
c4084c4f97 Use ContextCompat.startForegroundService(). 2020-10-09 08:52:05 +05:30
Isira Seneviratne
58b720b004 Use ContextCompat.getSystemService() and the Context.getSystemService() extension function. 2020-10-09 08:52:05 +05:30
wb9688
f6d0c1f05e Fix compiling release build 2020-10-08 18:36:20 +02:00
Stypox
f4620be859 Merge pull request #4431 from opusforlife2/checkbox_example
Added an example of how to use Markdown checkbox
2020-10-08 16:02:04 +02:00
Hakim Oubouali
e2b3a98690 Translated using Weblate (Central Atlas Tamazight)
Currently translated at 6.3% (38 of 600 strings)
2020-10-08 11:25:19 +02:00
postsorino
4cd391d5ef Translated using Weblate (Greek)
Currently translated at 93.8% (563 of 600 strings)
2020-10-08 11:25:18 +02:00
Weblate
01c37c34dd Added translation using Weblate (Central Atlas Tamazight) 2020-10-08 10:37:37 +02:00
Stjepan
f4827cde0e Translated using Weblate (Croatian)
Currently translated at 85.1% (511 of 600 strings)
2020-10-07 16:11:05 +02:00
Stjepan
3e722295b0 Translated using Weblate (Croatian)
Currently translated at 83.0% (498 of 600 strings)
2020-10-07 15:11:13 +02:00
zmni
0d2eab3ad4 Translated using Weblate (Indonesian)
Currently translated at 5.7% (2 of 35 strings)

Translation: NewPipe/Metadata
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/id/
2020-10-07 13:55:09 +02:00
Stypox
9e1bc631cf Merge pull request #4436 from wb9688/hide-leaks
Hide Leaks launcher icon
2020-10-07 13:26:12 +02:00
wb9688
ca9fbe2f11 Hide Leaks launcher icon 2020-10-07 12:56:22 +02:00
opusforlife2
2e28fad102 Added checkbox example 2020-10-07 07:40:55 +00:00
opusforlife2
69760200dd Added checkbox example 2020-10-07 07:39:49 +00:00
Mario Rossi
f945ee1288 Translated using Weblate (Neapolitan)
Currently translated at 5.6% (34 of 600 strings)
2020-10-07 09:24:11 +02:00
Eric
5e3486c481 Translated using Weblate (Chinese (Simplified))
Currently translated at 60.0% (21 of 35 strings)

Translation: NewPipe/Metadata
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/zh_Hans/
2020-10-07 09:24:11 +02:00
Jeff Huang
c15a943cf4 Translated using Weblate (Chinese (Traditional))
Currently translated at 62.8% (22 of 35 strings)

Translation: NewPipe/Metadata
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/zh_Hant/
2020-10-07 09:24:10 +02:00
Ortinomax
b8cb29c66c Translated using Weblate (French)
Currently translated at 14.2% (5 of 35 strings)

Translation: NewPipe/Metadata
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/fr/
2020-10-07 09:24:09 +02:00
Oğuz Ersen
003badcb5a Translated using Weblate (Turkish)
Currently translated at 5.7% (2 of 35 strings)

Translation: NewPipe/Metadata
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/tr/
2020-10-07 09:24:08 +02:00
Eric
5b2493fa68 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (600 of 600 strings)
2020-10-07 09:24:08 +02:00
Stypox
bc342b9b33 Merge pull request #4367 from panoreak/remove-remember-popup-properties
Removed remember popup properties setting
2020-10-07 07:01:56 +02:00
Panorea
314615bfef Removed remember popup properties setting 2020-10-06 18:32:20 -04:00
Stypox
44e82217c1 Merge pull request #4425 from vkay94/enqueue-stream
Replace specific enqueue options with one
2020-10-06 22:37:19 +02:00
vkay94
cbf364f24f Enqueue: Renamed string resource 2020-10-06 21:17:52 +02:00
Weblate
44dfcb927b Added translation using Weblate (Neapolitan) 2020-10-06 20:36:17 +02:00
vkay94
12f615c6da Enqueue: Removed unneeded dialog-entries and strings + adjustments 2020-10-06 17:22:12 +02:00
vkay94
ed6fc4d848 Enqueue: Replaced specific StreamDialogEntry items with one
The enqueue options won't be shown in the dialogs if the Player service is not running. When it's running one item (enqueue stream) will be shown and enqueues the item into the Player type which is currently selected.
2020-10-06 14:38:48 +02:00
vkay94
cd515993f5 Enqueue: Add auto-select StreamDialogEntry for current PlayerType 2020-10-06 13:33:44 +02:00
Tobias Groza
a918eaac3f Add device info fields to bug report template (#4415) 2020-10-05 19:38:30 +02:00
Tobias Groza
9f63e2d39a Merge pull request #4410 from Stypox/notification-fixes
Notification fixes
2020-10-05 19:08:26 +02:00
Stypox
36248ff046 Also delete stream state when deleting stream history 2020-10-05 17:47:48 +02:00
Stypox
a88f5113e0 Hide player notification "when" time
It is useless to see how much time ago a player notification was created
2020-10-05 15:57:14 +02:00
Stypox
06fb89fae2 Fix crash on fast forward 2020-10-05 15:55:10 +02:00
RainSlide
733531356f Translated using Weblate (Chinese (Simplified))
Currently translated at 42.8% (15 of 35 strings)

Translation: NewPipe/Metadata
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/zh_Hans/
2020-10-05 15:44:24 +02:00
Marian Hanzel
3f8fb30066 Translated using Weblate (Slovak)
Currently translated at 2.8% (1 of 35 strings)

Translation: NewPipe/Metadata
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/sk/
2020-10-05 15:44:23 +02:00
WaldiS
1a4a2d2b30 Translated using Weblate (Polish)
Currently translated at 31.4% (11 of 35 strings)

Translation: NewPipe/Metadata
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/pl/
2020-10-05 15:44:22 +02:00
David Braz
eb6d3b3f8d Translated using Weblate (Portuguese (Brazil))
Currently translated at 14.2% (5 of 35 strings)

Translation: NewPipe/Metadata
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/pt_BR/
2020-10-05 15:44:22 +02:00
San Kang
e8e3363d06 Translated using Weblate (Korean)
Currently translated at 84.0% (504 of 600 strings)
2020-10-05 15:44:21 +02:00
Tobias Groza
dd55ad61f4 Merge pull request #4338 from Isira-Seneviratne/Use_DisplayCutoutCompat
Use DisplayCutoutCompat in VideoPlayerImpl.
2020-10-05 08:33:41 +02:00
Guillaume Démurgé
2464bfd70b Translated using Weblate (French)
Currently translated at 11.4% (4 of 35 strings)

Translation: NewPipe/Metadata
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/fr/
2020-10-04 22:01:35 +02:00
JoC
09bc36bb13 Translated using Weblate (Spanish)
Currently translated at 14.2% (5 of 35 strings)

Translation: NewPipe/Metadata
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/es/
2020-10-04 22:01:34 +02:00
thami simo
39da89b556 Translated using Weblate (Arabic)
Currently translated at 20.0% (7 of 35 strings)

Translation: NewPipe/Metadata
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/ar/
2020-10-04 22:01:34 +02:00
thami simo
c23de4b3b0 Translated using Weblate (Arabic)
Currently translated at 100.0% (600 of 600 strings)
2020-10-04 22:01:33 +02:00
Mohammed Anas
7c79d7f5d7 Translated using Weblate (Arabic)
Currently translated at 100.0% (600 of 600 strings)
2020-10-04 20:52:43 +02:00
thami simo
710507da51 Translated using Weblate (Arabic)
Currently translated at 100.0% (600 of 600 strings)
2020-10-04 20:52:42 +02:00
Tobias Groza
10e95bf1b1 Merge pull request #4379 from opusforlife2/update_FR_template
Add check boxes to Feature Request template
2020-10-04 15:55:40 +02:00
Tobias Groza
66a893c84e Merge pull request #4378 from opusforlife2/update_issue_template
Replace long comments with check boxes in the Bug Report template
2020-10-04 15:55:22 +02:00
Stypox
dd6392e380 Replace per with for every 2020-10-04 15:47:20 +02:00
opusforlife2
b8f7ba62c7 Use @Stypox 's suggestion
Co-authored-by: Stypox <stypox@pm.me>
2020-10-04 12:30:44 +00:00
opusforlife2
25b318ba00 Add check boxes
People seem to ignore instructions in comments
2020-10-04 12:24:56 +00:00
opusforlife2
9add51b59d Add checkboxes instead of long comments
People just seemed to ignore them.
2020-10-04 12:03:14 +00:00
Isira Seneviratne
63d54e6570 Use DisplayCutoutCompat in VideoPlayerImpl. 2020-10-04 05:44:13 +05:30
777 changed files with 10248 additions and 7839 deletions

View File

@@ -7,20 +7,20 @@ assignees: ''
---
<!--
Oh no, a bug! It happens. Thanks for reporting an issue with NewPipe. If this is your first bug report, read the following information before proceeding:
Please note, we only support the latest version of NewPipe. In order to check your app version, open the left drawer and click on "About". If you don't have the latest version, upgrade to it and reproduce the problem before opening the issue. The release page (https://github.com/TeamNewPipe/NewPipe/releases/latest) is where you can get it.
P.S.: Our contribution guidelines might be a nice document to read before you fill out the report :) You can find it at https://github.com/TeamNewPipe/NewPipe/blob/HEAD/.github/CONTRIBUTING.md
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.
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.
-->
<!-- The comments between these brackets won't show up in the submitted issue (as you can see in the preview). -->
<!-- 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. -->
### Version
<!-- Which version are you using? Hopefully the latest! We just told you that above! -->
-
<!-- The comments between these brackets won't show up in the submitted issue (as you can see in the Preview). -->
### Checklist
<!-- The first box has been checked for you to show you how it is done. -->
- [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
<!--
@@ -31,16 +31,35 @@ To make it easier for us to help you please enter detailed information in the te
<!-- 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 behaviour
<!-- Tell us what happens with the steps given above. -->
### Expected behavior
<!-- Tell us what you expect to happen. -->
### Actual behaviour
<!-- Tell us what happens instead. -->
### 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), copy it to the clipboard (there is a share button for this), head over to our bug report to markdown converter at https://teamnewpipe.github.io/CrashReportToMarkdown/ and paste it. Copy the converted text (it is MUCH easier to read this way) from there and paste it here: -->
<!-- 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 out when you do not provide a log generate by NewPipe -->
### Device info
- Android version/Custom ROM version:
- Device model:

1
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

@@ -0,0 +1 @@
blank_issues_enabled: false

View File

@@ -5,35 +5,42 @@ labels: enhancement
assignees: ''
---
<!-- Hey. Our contribution guidelines (https://github.com/TeamNewPipe/NewPipe/blob/HEAD/.github/CONTRIBUTING.md) might be an appropriate
document to read before you fill out the request :) -->
<!-- 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
<!-- 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.
<!-- The comments between these brackets won't show up in the submitted issue (as you can see in the preview). -->
#### Describe the feature you want
<!-- A clear and concise description of what you want to happen. PLEASE MAKE SURE it is one feature ONLY. You should open separate issues for separate feature requests, because those issues will be used to track their status.
<!-- A clear and concise description of what you wish should happen.
Example: *I think it would be nice if you add feature Y which makes X possible.*
Optionally, also describe alternatives you've considered.
Example: *Z is also a good alternative. Not as good as Y, but at least...* or *I considered Z, but that didn't turn out to be a good idea because...* -->
<!-- Write below this -->
#### Is your feature request related to a problem? Please describe it
<!-- A clear and concise description of what the problem is. Maybe the developers could brainstorm and come up with a better solution to your problem. If they exist, link to related Issues and/or PRs for developers to keep track easier.
<!-- A clear and concise description of what the problem is. Maybe the developers and the community could brainstorm and come up with a better solution to your problem. If they exist, link to related Issues and/or PRs for developers to keep track easier.
Example: *I want to do X, but there is no way to do it.* -->
<!-- Write below this -->
#### Additional context
<!-- Add any other context, like screenshots, about the feature request here.
Example: *Here's a photo of my cat!* -->
<!-- Write below this -->
#### How will you/everyone benefit from this feature?
<!-- Convince us! How does it change your NewPipe experience and/or your life?
The better this paragraph is, the more likely a developer will think about working on it.
Example: *This feature will help us colonize the galaxy! -->
<!-- Write below this -->

View File

@@ -1,28 +1,28 @@
<!-- Hey there. Thank you so much for improving NewPipe. Please take a moment to fill out the following suggestion on how to structure this PR description. Having roughly the same layout helps everyone considerably :)-->
<!-- Hey there. Thank you so much for improving NewPipe, and filling out the details. Having roughly the same layout helps everyone considerably :)-->
#### What is it?
- [ ] Bug fix (user facing)
- [ ] Bugfix (user facing)
- [ ] Feature (user facing)
- [ ] Code base improvement (dev facing)
- [ ] Codebase improvement (dev facing)
- [ ] Meta improvement to the project (dev facing)
#### Description of the changes in your PR
<!-- While bullet points are the norm in this section, feel free to write a text instead if you can't fit it in a list -->
<!-- While bullet points are the norm in this section, feel free to write free-form text instead of a list -->
- record videos
- create clones
- take over the world
#### Fixes the following issue(s)
<!-- Also add reddit or other links which are relevant to your change. -->
<!-- Also add any other links relevant to your change. -->
-
#### Relies on the following changes
<!-- Delete this if it doesn't apply to you. -->
-
#### Testing apk
<!-- Ensure that you have your changes on a new branch which has a meaningful name. This name will be used as a suffix for the app ID to allow installing and testing multiple versions of NewPipe. Do NOT name your branches like "patch-0" and "feature-1". For example, if your PR implements a bug fix for comments, an appropriate branch name would be "commentfix". -->
#### APK testing
<!-- Use a new, meaningfully named branch. The name is used as a suffix for the app ID to allow installing and testing multiple versions of NewPipe, e.g. "commentfix", if your PR implements a bugfix for comments. (No names like "patch-0" and "feature-1".) -->
debug.zip
#### Agreement
- [ ] I carefully read the [contribution guidelines](https://github.com/TeamNewPipe/NewPipe/blob/HEAD/.github/CONTRIBUTING.md) and agree to them.
#### Due diligence
- [ ] I read the [contribution guidelines](https://github.com/TeamNewPipe/NewPipe/blob/HEAD/.github/CONTRIBUTING.md).

144
README.ko.md Normal file
View File

@@ -0,0 +1,144 @@
<p align="center"><a href="https://newpipe.schabi.org"><img src="assets/new_pipe_icon_5.png" width="150"></a></p>
<h2 align="center"><b>NewPipe</b></h2>
<h4 align="center">A libre lightweight streaming frontend for Android.</h4>
<p align="center"><a href="https://f-droid.org/packages/org.schabi.newpipe/"><img src="https://f-droid.org/wiki/images/0/06/F-Droid-button_get-it-on.png"></a></p>
<p align="center">
<a href="https://github.com/TeamNewPipe/NewPipe/releases" alt="GitHub release"><img src="https://img.shields.io/github/release/TeamNewPipe/NewPipe.svg" ></a>
<a href="https://www.gnu.org/licenses/gpl-3.0" alt="License: GPLv3"><img src="https://img.shields.io/badge/License-GPL%20v3-blue.svg"></a>
<a href="https://travis-ci.org/TeamNewPipe/NewPipe" alt="Build Status"><img src="https://travis-ci.org/TeamNewPipe/NewPipe.svg"></a>
<a href="https://hosted.weblate.org/engage/newpipe/" alt="Translation Status"><img src="https://hosted.weblate.org/widgets/newpipe/-/svg-badge.svg"></a>
<a href="http://webchat.freenode.net/?channels=%23newpipe" alt="IRC channel: #newpipe"><img src="https://img.shields.io/badge/IRC%20chat-%23newpipe-brightgreen.svg"></a>
<a href="https://www.bountysource.com/teams/newpipe" alt="Bountysource bounties"><img src="https://img.shields.io/bountysource/team/newpipe/activity.svg?colorB=cd201f"></a>
</p>
<hr>
<p align="center"><a href="#screenshots">Screenshots</a> &bull; <a href="#description">Description</a> &bull; <a href="#features">Features</a> &bull; <a href="#updates">Updates</a> &bull; <a href="#contribution">Contribution</a> &bull; <a href="#donate">Donate</a> &bull; <a href="#license">License</a></p>
<p align="center"><a href="https://newpipe.schabi.org">Website</a> &bull; <a href="https://newpipe.schabi.org/blog/">Blog</a> &bull; <a href="https://newpipe.schabi.org/FAQ/">FAQ</a> &bull; <a href="https://newpipe.schabi.org/press/">Press</a></p>
<hr>
*Read this in other languages: [English](README.md), [한국어](README.ko.md).*
<b>경고: 이 버전은 베타 버전이므로, 버그가 발생할 수도 있습니다. 만약 버그가 발생하였다면, 우리의 GITHUB 저장소에서 ISSUE를 열람하여 주십시오.</b>
<b>NEWPIPE 또는 이것의 FORK을 구글 플레이스토어에 올리는 것은 그들의 이용약관을 위반합니다.</b>
## Screenshots
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_01.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_01.png)
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_02.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_02.png)
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_03.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_03.png)
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_04.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_04.png)
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_05.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_05.png)
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_06.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_06.png)
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_07.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_07.png)
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_08.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_08.png)
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_09.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_09.png)
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_10.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_10.png)
[<img src="fastlane/metadata/android/en-US/images/tenInchScreenshots/shot_11.png" width=405>](fastlane/metadata/android/en-US/images/tenInchScreenshots/shot_11.png)
[<img src="fastlane/metadata/android/en-US/images/tenInchScreenshots/shot_12.png" width=405>](fastlane/metadata/android/en-US/images/tenInchScreenshots/shot_12.png)
## Description
NewPipe는 어떤 구글 프레임워크 라이브러리나, 유튜브 API를 사용하지 않습니다. 웹사이트는 단지 필요한 정보를 가져오기 위해 구문 분석 됩니다. 따라서 이 앱은 구글 서비스의 설치 없이 기기에서 사용될 수 있습니다. 또한, 카피레프트 자유 소프트웨어인 NewPipe를 사용하기 위해 유튜브 계정이 필요하지 않습니다.
### Features
* 영상 검색
* 영상의 일반적인 정보 표시
* 유튜브 영상 보기
* 유튜브 영상 듣기
* 팝업 모드 (floating player)
* 영상 공유
* 영상 다운로드
* 음성만 다운로드
* Kodi에서 영상 열람
* 다음/관련된 영상 표시
* 특정 언어로 유튜브 검색
* 연령 제한 컨텐츠 시청/차단
* 채널에 대한 일반적인 정보 표시
* 채널 검색
* 채널에서 영상 시청
* Orbot/Tor 지원 (아직 직접적이지 않음)
* 1080p/2K/4K 지원
* 기록 보기
* 채널 구독
* 기록 검색
* 재생목록 검색/시청
* 추가된 재생목록 시청
* 영상 추가
* 지역 재생목록
* 자막
* 실시간 방송 지원
* 댓글 표시
### Supported Services
NewPipe는 여러가지 서비스를 지원합니다. 우리의 [문서](https://teamnewpipe.github.io/documentation/)는 새로운 서비스가 앱과 추출기에 어떻게 추가될 수 있는지에 대한 더 많은 정보를 제공합니다. 만약 새로운 서비스를 추가하고자 한다면, 우리에게 연락해 주시기 바랍니다. 현재 지원되는 서비스:
* YouTube
* SoundCloud \[beta\]
* media.ccc.de \[beta\]
* PeerTube instances \[beta\]
## Updates
NewPipe 코드의 변경이 있을 때(기능 추가 또는 버그 수정으로 인해), 결국 릴리즈가 발생할 것입니다. 이것들의 형식은 x.xx.x 입니다.
이 새로운 버전을 얻기 위해서, 당신은:
1. 직접 디버그 APK를 생성할 수 있습니다. 이 방법은 당신의 기기에서 새로운 기능을 얻을 수 있는 가장 빠른 방법이지만, 꽤 많이 복잡합니다.
따라서 우리는 다른 방법들 중 하나를 사용하는 것을 추천합니다.
2. 우리의 커스텀 저장소를 F-Droid에 추가하고 우리가 릴리즈를 게시하는 대로 저곳에서 릴리즈를 설치할 수 있습니다.
이에 대한 설명서는 이곳에서 확인할 수 있습니다: https://newpipe.schabi.org/FAQ/tutorials/install-add-fdroid-repo/
3. 우리가 릴리즈를 게시하는 대로 [Github Releases](https://github.com/TeamNewPipe/NewPipe/releases)에서 APK를 다운받고 이것을 설치할 수 있습니다.
4. F-Droid를 통해 업데이트 할 수 있습니다. F-Droid는 변화를 인식하고, 스스로 APK를 생성하고, 이것에 서명하고, 사용자들에서 업데이트를 전달해야만 하기 때문에,
이것은 업데이트를 받는 가장 느린 방법입니다.
우리는 대부분의 사용자에게 2번쨰 방법을 추천합니다. 방법 2 또는 3을 사용하여 설치된 APK는 서로 호환되지만, 방법 4를 사용하여 설치된 것들과는 호환되지 않습니다. 이것은 방법 2 또는 3에서는 같은 (우리의)서명 키가 사용되지만, 방법 4에서는 다른 (F-Droid의)서명 키가 사용되기 때문입니다. 방법 1을 사용하여 디버그 APK를 생성하는 것에서는 키가 완전히 제외됩니다. 서명 키는 사용자가 앱에 악의적인 업데이트를 설치하는 것에 대해 속지 않도록 보장하는 것을 도와줍니다.
한편, 만약 어떠한 이유(예. NewPipe의 핵심 기능이 손상되었고 F-Droid가 아직 업데이트를 가지지 않는 경우) 때문에 소스를 바꾸길 원한다면,
우리는 다음과 같은 절차를 따르는 것을 권장합니다:
1. 당신의 기록, 구독, 그리고 재생목록을 유지할 수 있도록 Settings > Content > Export Database 를 통해 데이터를 백업하십시오.
2. NewPipe를 삭제하십시오.
3. 새로운 소스에서 APK를 다운로드하고 이것을 설치하십시오.
4. Step 1의 Settings > Content > Export Database 을 통해 데이터를 불러오십시오.
## Contribution
당신이 아이디어, 번역, 디자인 변경, 코드 정리, 또는 정말 큰 코드 수정에 대한 의견이 있다면, 도움은 항상 환영합니다.
더 많이 수행될수록 더 많이 발전할 수 있습니다!
만약 참여하고 싶다면, 우리의 [컨트리뷰션 공지](.github/CONTRIBUTING.md)를 참고하십시오.
<a href="https://hosted.weblate.org/engage/newpipe/">
<img src="https://hosted.weblate.org/widgets/newpipe/-/287x66-grey.png" alt="Translation status" />
</a>
## Donate
만약 NewPipe가 마음에 들었다면, 우리는 기부에 대해 기꺼이 환영합니다. bitcoin을 보내거나, Bountysource 또는 Liberapay를 통해 기부할 수 있습니다. NewPipe에 기부하는 것에 대한 자세한 정보를 원한다면, 우리의 [웹사이트](https://newpipe.schabi.org/donate)를 방문하여 주십시오.
<table>
<tr>
<td><img src="https://bitcoin.org/img/icons/logotop.svg" alt="Bitcoin"></td>
<td><img src="assets/bitcoin_qr_code.png" alt="Bitcoin QR code" width="100px"></td>
<td><samp>16A9J59ahMRqkLSZjhYj33n9j3fMztFxnh</samp></td>
</tr>
<tr>
<td><a href="https://liberapay.com/TeamNewPipe/"><img src="https://upload.wikimedia.org/wikipedia/commons/2/27/Liberapay_logo_v2_white-on-yellow.svg" alt="Liberapay" width="80px" ></a></td>
<td><a href="https://liberapay.com/TeamNewPipe/"><img src="assets/liberapay_qr_code.png" alt="Visit NewPipe at liberapay.com" width="100px"></a></td>
<td><a href="https://liberapay.com/TeamNewPipe/donate"><img src="assets/liberapay_donate_button.svg" alt="Donate via Liberapay" height="35px"></a></td>
</tr>
<tr>
<td><a href="https://www.bountysource.com/teams/newpipe"><img src="https://upload.wikimedia.org/wikipedia/commons/thumb/2/22/Bountysource.png/320px-Bountysource.png" alt="Bountysource" width="190px"></a></td>
<td><a href="https://www.bountysource.com/teams/newpipe"><img src="assets/bountysource_qr_code.png" alt="Visit NewPipe at bountysource.com" width="100px"></a></td>
<td><a href="https://www.bountysource.com/teams/newpipe/issues"><img src="https://img.shields.io/bountysource/team/newpipe/activity.svg?colorB=cd201f" height="30px" alt="Check out how many bounties you can earn."></a></td>
</tr>
</table>
## Privacy Policy
NewPipe 프로젝트는 미디어 웹 서비스를 사용하는 것에 대한 사적의, 익명의 경험을 제공하는 것을 목표로 하고 있습니다.
그러므로, 앱은 당신의 동의 없이 어떤 데이터도 수집하지 않습니다. NewPipe의 개인정보보호정책은 당신이 충돌 리포트를 보내거나, 또는 우리의 블로그에 글을 남길 때 어떤 데이터가 보내지고 저장되는지에 대해 상세히 설명합니다. 이 문서는 [여기](https://newpipe.schabi.org/legal/privacy/)에서 확인할 수 있습니다.
## License
[![GNU GPLv3 Image](https://www.gnu.org/graphics/gplv3-127x51.png)](http://www.gnu.org/licenses/gpl-3.0.en.html)
NewPipe는 자유 소프트웨어입니다: 당신의 마음대로 이것을 사용하고, 연구하고, 공유하고, 개선할 수 있습니다.
구체적으로 당신은 자유 소프트웨어 재단에서 발행되는, 버전 3 또는 (당신의 선택에 따라)이후 버전의,
[GNU General Public License](https://www.gnu.org/licenses/gpl.html) 하에서 이것을 재배포 및/또는 수정할 수 있습니다.

View File

@@ -16,6 +16,8 @@
<p align="center"><a href="https://newpipe.schabi.org">Website</a> &bull; <a href="https://newpipe.schabi.org/blog/">Blog</a> &bull; <a href="https://newpipe.schabi.org/FAQ/">FAQ</a> &bull; <a href="https://newpipe.schabi.org/press/">Press</a></p>
<hr>
*Read this in other languages: [English](README.md), [한국어](README.ko.md).*
<b>WARNING: THIS IS A BETA VERSION, THEREFORE YOU MAY ENCOUNTER BUGS. IF YOU DO, OPEN AN ISSUE VIA OUR GITHUB REPOSITORY.</b>
<b>PUTTING NEWPIPE OR ANY FORK OF IT INTO GOOGLE PLAYSTORE VIOLATES THEIR TERMS OF CONDITIONS.</b>
@@ -69,11 +71,6 @@ NewPipe does not use any Google framework libraries, nor the YouTube API. Websit
* Livestream support
* Show comments
### Coming Features
* Cast to UPnP and Cast
* … and many more
### Supported Services
NewPipe supports multiple services. Our [docs](https://teamnewpipe.github.io/documentation/) provide more info on how a new service can be added to the app and the extractor. Please get in touch with us if you intend to add a new one. Currently supported services are:
@@ -85,17 +82,18 @@ NewPipe supports multiple services. Our [docs](https://teamnewpipe.github.io/doc
## Updates
When a change to the NewPipe code occurs (due to either adding features or bug fixing), eventually a release will occur. These are in the format x.xx.x . In order to get this new version, you can:
* Build a debug APK yourself. This is the fastest way to get new features on your device, but is much more complicated, so we recommend using one of the other methods.
* Download the APK from [releases](https://github.com/TeamNewPipe/NewPipe/releases) and install it.
* Update via F-droid. This is the slowest method of getting updates, as F-Droid must recognize changes, build the APK itself, sign it, then push the update to users.
1. Build a debug APK yourself. This is the fastest way to get new features on your device, but is much more complicated, so we recommend using one of the other methods.
2. Add our custom repo to F-Droid and install it from there as soon as we publish a release. The instructions are here: https://newpipe.schabi.org/FAQ/tutorials/install-add-fdroid-repo/
3. Download the APK from [Github Releases](https://github.com/TeamNewPipe/NewPipe/releases) and install it as soon as we publish a release.
4. Update via F-droid. This is the slowest method of getting updates, as F-Droid must recognize changes, build the APK itself, sign it, then push the update to users.
When you install an APK from one of these options, it will be incompatible with an APK from one of the other options. This is due to different signing keys being used. Signing keys help ensure that a user isn't tricked into installing a malicious update to an app, and are independent. F-Droid and GitHub use different signing keys, and building an APK debug excludes a key. The signing key issue is being discussed in issue [#1981](https://github.com/TeamNewPipe/NewPipe/issues/1981), and may be fixed by setting up our own repository on F-Droid.
We recommend method 2 for most users. APKs installed using method 2 or 3 are compatible with each other, but not with those installed using method 4. This is due to the same signing key (ours) being using for 2 and 3, but a different signing key (F-Droid's) being used for 4. Building a debug APK using method 1 excludes a key entirely. Signing keys help ensure that a user isn't tricked into installing a malicious update to an app.
In the meanwhile, if you want to switch sources for some reason (e.g. NewPipe's core functionality was broken and F-Droid doesn't have the update yet), we recommend following this procedure:
1. Back up your data via "Settings>Content>Export Database" so you keep your history, subscriptions, and playlists
1. Back up your data via Settings > Content > Export Database so you keep your history, subscriptions, and playlists
2. Uninstall NewPipe
3. Download the APK from the new source and install it
4. Import the data from step 1 via "Settings>Content>Import Database"
4. Import the data from step 1 via Settings > Content > Import Database
## Contribution
Whether you have ideas, translations, design changes, code cleaning, or real heavy code changes, help is always welcome.
@@ -103,6 +101,10 @@ The more is done the better it gets!
If you'd like to get involved, check our [contribution notes](.github/CONTRIBUTING.md).
<a href="https://hosted.weblate.org/engage/newpipe/">
<img src="https://hosted.weblate.org/widgets/newpipe/-/287x66-grey.png" alt="Translation status" />
</a>
## Donate
If you like NewPipe we'd be happy about a donation. You can either send bitcoin or donate via Bountysource or Liberapay. For further info on donating to NewPipe, please visit our [website](https://newpipe.schabi.org/donate).

View File

@@ -13,8 +13,10 @@ android {
resValue "string", "app_name", "NewPipe"
minSdkVersion 19
targetSdkVersion 29
versionCode 955
versionName "0.20.1"
versionCode 957
versionName "0.20.3"
multiDexEnabled true
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables.useSupportLibrary = true
@@ -28,7 +30,6 @@ android {
buildTypes {
debug {
multiDexEnabled true
debuggable true
// suffix the app id and the app name with git branch name
@@ -64,11 +65,18 @@ android {
}
compileOptions {
// Flag to enable support for the new language APIs
coreLibraryDesugaringEnabled true
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
encoding 'utf-8'
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_1_8
}
// Required and used only by groupie
androidExtensions {
experimental = true
@@ -81,7 +89,7 @@ android {
ext {
icepickVersion = '3.2.0'
checkstyleVersion = '8.32'
checkstyleVersion = '8.36.2'
stethoVersion = '1.5.1'
leakCanaryVersion = '2.2'
exoPlayerVersion = '2.11.8'
@@ -94,7 +102,7 @@ ext {
configurations {
checkstyle
ktlint
// ktlint
}
checkstyle {
@@ -122,30 +130,32 @@ task runCheckstyle(type: Checkstyle) {
}
}
task runKtlint(type: JavaExec) {
main = "com.pinterest.ktlint.Main"
classpath = configurations.ktlint
args "src/**/*.kt"
}
task formatKtlint(type: JavaExec) {
main = "com.pinterest.ktlint.Main"
classpath = configurations.ktlint
args "-F", "src/**/*.kt"
}
//task runKtlint(type: JavaExec) {
// main = "com.pinterest.ktlint.Main"
// classpath = configurations.ktlint
// args "src/**/*.kt"
//}
//
//task formatKtlint(type: JavaExec) {
// main = "com.pinterest.ktlint.Main"
// classpath = configurations.ktlint
// args "-F", "src/**/*.kt"
//}
afterEvaluate {
preDebugBuild.dependsOn runCheckstyle, runKtlint
preDebugBuild.dependsOn runCheckstyle //, runKtlint
}
dependencies {
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.0.10'
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
implementation "frankiesardo:icepick:${icepickVersion}"
kapt "frankiesardo:icepick-processor:${icepickVersion}"
checkstyle "com.puppycrawl.tools:checkstyle:${checkstyleVersion}"
ktlint "com.pinterest:ktlint:0.35.0"
// ktlint "com.pinterest:ktlint:0.35.0"
debugImplementation "com.facebook.stetho:stetho:${stethoVersion}"
debugImplementation "com.facebook.stetho:stetho-okhttp3:${stethoVersion}"
@@ -153,9 +163,9 @@ dependencies {
debugImplementation "com.squareup.leakcanary:leakcanary-android:${leakCanaryVersion}"
implementation "com.squareup.leakcanary:leakcanary-object-watcher-android:${leakCanaryVersion}"
debugImplementation "androidx.multidex:multidex:2.0.1"
implementation "androidx.multidex:multidex:2.0.1"
testImplementation 'junit:junit:4.13'
testImplementation 'junit:junit:4.13.1'
testImplementation 'org.mockito:mockito-core:3.3.3'
androidTestImplementation "androidx.test.ext:junit:1.1.1"
@@ -164,9 +174,11 @@ dependencies {
exclude module: 'support-annotations'
}
implementation 'com.github.TeamNewPipe:NewPipeExtractor:350eed6214b93255d788dfa208b1e9a5e5da91e6'
// NewPipe dependencies
// You can use a local version by uncommenting a few lines in settings.gradle
implementation 'com.github.TeamNewPipe:NewPipeExtractor:6701b0fe718f6bdc385221341fa473e8aaab560e'
implementation "com.github.TeamNewPipe:nanojson:1d9e1aea9049fc9f85e68b43ba39fe7be1c1f751"
implementation "org.jsoup:jsoup:1.13.1"
implementation "com.squareup.okhttp3:okhttp:3.12.12"
@@ -184,6 +196,7 @@ dependencies {
implementation "androidx.recyclerview:recyclerview:1.1.0"
implementation "androidx.cardview:cardview:1.0.0"
implementation "androidx.constraintlayout:constraintlayout:1.1.3"
implementation 'androidx.core:core-ktx:1.3.1'
implementation "androidx.lifecycle:lifecycle-livedata:${androidxLifecycleVersion}"
implementation "androidx.lifecycle:lifecycle-viewmodel:${androidxLifecycleVersion}"

View File

@@ -1,6 +1,5 @@
package org.schabi.newpipe
import androidx.multidex.MultiDex
import androidx.preference.PreferenceManager
import com.facebook.stetho.Stetho
import com.facebook.stetho.okhttp3.StethoInterceptor
@@ -28,12 +27,6 @@ class DebugApp : App() {
return downloader
}
override fun initACRA() {
// install MultiDex before initializing ACRA
MultiDex.install(this)
super.initACRA()
}
private fun initStetho() {
// Create an InitializerBuilder
val initializerBuilder = Stetho.newInitializerBuilder(this)

View File

@@ -0,0 +1,27 @@
package org.schabi.newpipe.settings;
import android.os.Bundle;
import androidx.annotation.Nullable;
import org.schabi.newpipe.R;
import leakcanary.LeakCanary;
public class DebugSettingsFragment extends BasePreferenceFragment {
@Override
public void onCreate(@Nullable final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
findPreference(getString(R.string.show_memory_leaks_key))
.setOnPreferenceClickListener(preference -> {
startActivity(LeakCanary.INSTANCE.newLeakDisplayActivityIntent());
return true;
});
}
@Override
public void onCreatePreferences(final Bundle savedInstanceState, final String rootKey) {
addPreferencesFromResource(R.xml.debug_settings);
}
}

View File

@@ -1,57 +1,56 @@
<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen
xmlns:android="http://schemas.android.com/apk/res/android"
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:key="general_preferences"
android:title="@string/settings">
<PreferenceScreen
app:iconSpaceReserved="false"
android:fragment="org.schabi.newpipe.settings.VideoAudioSettingsFragment"
android:icon="?attr/ic_headset"
android:title="@string/settings_category_video_audio_title"/>
android:title="@string/settings_category_video_audio_title"
app:iconSpaceReserved="false" />
<PreferenceScreen
app:iconSpaceReserved="false"
android:fragment="org.schabi.newpipe.settings.DownloadSettingsFragment"
android:icon="?attr/ic_file_download"
android:title="@string/settings_category_downloads_title"/>
android:title="@string/settings_category_downloads_title"
app:iconSpaceReserved="false" />
<PreferenceScreen
app:iconSpaceReserved="false"
android:fragment="org.schabi.newpipe.settings.AppearanceSettingsFragment"
android:icon="?attr/ic_palette"
android:title="@string/settings_category_appearance_title"/>
android:title="@string/settings_category_appearance_title"
app:iconSpaceReserved="false" />
<PreferenceScreen
app:iconSpaceReserved="false"
android:fragment="org.schabi.newpipe.settings.HistorySettingsFragment"
android:icon="?attr/ic_history"
android:title="@string/settings_category_history_title"/>
android:title="@string/settings_category_history_title"
app:iconSpaceReserved="false" />
<PreferenceScreen
app:iconSpaceReserved="false"
android:fragment="org.schabi.newpipe.settings.ContentSettingsFragment"
android:icon="?attr/ic_language"
android:title="@string/content"/>
android:title="@string/content"
app:iconSpaceReserved="false" />
<PreferenceScreen
app:iconSpaceReserved="false"
android:fragment="org.schabi.newpipe.settings.NotificationSettingsFragment"
android:icon="?attr/ic_play_arrow"
android:title="@string/settings_category_notification_title"/>
android:title="@string/settings_category_notification_title"
app:iconSpaceReserved="false" />
<PreferenceScreen
app:iconSpaceReserved="false"
android:fragment="org.schabi.newpipe.settings.UpdateSettingsFragment"
android:icon="?attr/ic_settings_update"
android:key="update_pref_screen_key"
android:title="@string/settings_category_updates_title"
android:key="update_pref_screen_key"/>
app:iconSpaceReserved="false" />
<PreferenceScreen
app:iconSpaceReserved="false"
android:fragment="org.schabi.newpipe.settings.DebugSettingsFragment"
android:icon="?attr/ic_bug_report"
android:key="@string/debug_pref_screen_key"
android:title="@string/settings_category_debug_title"
android:key="@string/debug_pref_screen_key"/>
app:iconSpaceReserved="false" />
</PreferenceScreen>

View File

@@ -1,400 +0,0 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>GNU General Public License v2.0 - GNU Project - Free Software Foundation (FSF)</title>
<link rel="alternate" type="application/rdf+xml"
href="http://www.gnu.org/licenses/old-licenses/gpl-2.0.rdf" />
</head>
<body>
<h3><a id="SEC1">GNU GENERAL PUBLIC LICENSE</a></h3>
<p>
Version 2, June 1991
</p>
<pre>
Copyright (C) 1989, 1991 Free Software Foundation, Inc.<br/>
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA<br/>
<br/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
</pre>
<h3 id="preamble"><a id="SEC2">Preamble</a></h3>
<p>
The licenses for most software are designed to take away your
freedom to share and change it. By contrast, the GNU General Public
License is intended to guarantee your freedom to share and change free
software--to make sure the software is free for all its users. This
General Public License applies to most of the Free Software
Foundation's software and to any other program whose authors commit to
using it. (Some other Free Software Foundation software is covered by
the GNU Lesser General Public License instead.) You can apply it to
your programs, too.
</p>
<p>
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
this service if you wish), that you receive source code or can get it
if you want it, that you can change the software or use pieces of it
in new free programs; and that you know you can do these things.
</p>
<p>
To protect your rights, we need to make restrictions that forbid
anyone to deny you these rights or to ask you to surrender the rights.
These restrictions translate to certain responsibilities for you if you
distribute copies of the software, or if you modify it.
</p>
<p>
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must give the recipients all the rights that
you have. You must make sure that they, too, receive or can get the
source code. And you must show them these terms so they know their
rights.
</p>
<p>
We protect your rights with two steps: (1) copyright the software, and
(2) offer you this license which gives you legal permission to copy,
distribute and/or modify the software.
</p>
<p>
Also, for each author's protection and ours, we want to make certain
that everyone understands that there is no warranty for this free
software. If the software is modified by someone else and passed on, we
want its recipients to know that what they have is not the original, so
that any problems introduced by others will not reflect on the original
authors' reputations.
</p>
<p>
Finally, any free program is threatened constantly by software
patents. We wish to avoid the danger that redistributors of a free
program will individually obtain patent licenses, in effect making the
program proprietary. To prevent this, we have made it clear that any
patent must be licensed for everyone's free use or not licensed at all.
</p>
<p>
The precise terms and conditions for copying, distribution and
modification follow.
</p>
<h3 id="terms"><a id="SEC3">TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION</a></h3>
<p id="section0">
<strong>0.</strong>
This License applies to any program or other work which contains
a notice placed by the copyright holder saying it may be distributed
under the terms of this General Public License. The "Program", below,
refers to any such program or work, and a "work based on the Program"
means either the Program or any derivative work under copyright law:
that is to say, a work containing the Program or a portion of it,
either verbatim or with modifications and/or translated into another
language. (Hereinafter, translation is included without limitation in
the term "modification".) Each licensee is addressed as "you".
</p>
<p>
Activities other than copying, distribution and modification are not
covered by this License; they are outside its scope. The act of
running the Program is not restricted, and the output from the Program
is covered only if its contents constitute a work based on the
Program (independent of having been made by running the Program).
Whether that is true depends on what the Program does.
</p>
<p id="section1">
<strong>1.</strong>
You may copy and distribute verbatim copies of the Program's
source code as you receive it, in any medium, provided that you
conspicuously and appropriately publish on each copy an appropriate
copyright notice and disclaimer of warranty; keep intact all the
notices that refer to this License and to the absence of any warranty;
and give any other recipients of the Program a copy of this License
along with the Program.
</p>
<p>
You may charge a fee for the physical act of transferring a copy, and
you may at your option offer warranty protection in exchange for a fee.
</p>
<p id="section2">
<strong>2.</strong>
You may modify your copy or copies of the Program or any portion
of it, thus forming a work based on the Program, and copy and
distribute such modifications or work under the terms of Section 1
above, provided that you also meet all of these conditions:
</p>
<dl>
<dt></dt>
<dd>
<strong>a)</strong>
You must cause the modified files to carry prominent notices
stating that you changed the files and the date of any change.
</dd>
<dt></dt>
<dd>
<strong>b)</strong>
You must cause any work that you distribute or publish, that in
whole or in part contains or is derived from the Program or any
part thereof, to be licensed as a whole at no charge to all third
parties under the terms of this License.
</dd>
<dt></dt>
<dd>
<strong>c)</strong>
If the modified program normally reads commands interactively
when run, you must cause it, when started running for such
interactive use in the most ordinary way, to print or display an
announcement including an appropriate copyright notice and a
notice that there is no warranty (or else, saying that you provide
a warranty) and that users may redistribute the program under
these conditions, and telling the user how to view a copy of this
License. (Exception: if the Program itself is interactive but
does not normally print such an announcement, your work based on
the Program is not required to print an announcement.)
</dd>
</dl>
<p>
These requirements apply to the modified work as a whole. If
identifiable sections of that work are not derived from the Program,
and can be reasonably considered independent and separate works in
themselves, then this License, and its terms, do not apply to those
sections when you distribute them as separate works. But when you
distribute the same sections as part of a whole which is a work based
on the Program, the distribution of the whole must be on the terms of
this License, whose permissions for other licensees extend to the
entire whole, and thus to each and every part regardless of who wrote it.
</p>
<p>
Thus, it is not the intent of this section to claim rights or contest
your rights to work written entirely by you; rather, the intent is to
exercise the right to control the distribution of derivative or
collective works based on the Program.
</p>
<p>
In addition, mere aggregation of another work not based on the Program
with the Program (or with a work based on the Program) on a volume of
a storage or distribution medium does not bring the other work under
the scope of this License.
</p>
<p id="section3">
<strong>3.</strong>
You may copy and distribute the Program (or a work based on it,
under Section 2) in object code or executable form under the terms of
Sections 1 and 2 above provided that you also do one of the following:
</p>
<!-- we use this doubled UL to get the sub-sections indented, -->
<!-- while making the bullets as unobvious as possible. -->
<dl>
<dt></dt>
<dd>
<strong>a)</strong>
Accompany it with the complete corresponding machine-readable
source code, which must be distributed under the terms of Sections
1 and 2 above on a medium customarily used for software interchange; or,
</dd>
<dt></dt>
<dd>
<strong>b)</strong>
Accompany it with a written offer, valid for at least three
years, to give any third party, for a charge no more than your
cost of physically performing source distribution, a complete
machine-readable copy of the corresponding source code, to be
distributed under the terms of Sections 1 and 2 above on a medium
customarily used for software interchange; or,
</dd>
<dt></dt>
<dd>
<strong>c)</strong>
Accompany it with the information you received as to the offer
to distribute corresponding source code. (This alternative is
allowed only for noncommercial distribution and only if you
received the program in object code or executable form with such
an offer, in accord with Subsection b above.)
</dd>
</dl>
<p>
The source code for a work means the preferred form of the work for
making modifications to it. For an executable work, complete source
code means all the source code for all modules it contains, plus any
associated interface definition files, plus the scripts used to
control compilation and installation of the executable. However, as a
special exception, the source code distributed need not include
anything that is normally distributed (in either source or binary
form) with the major softwareComponents (compiler, kernel, and so on) of the
operating system on which the executable runs, unless that component
itself accompanies the executable.
</p>
<p>
If distribution of executable or object code is made by offering
access to copy from a designated place, then offering equivalent
access to copy the source code from the same place counts as
distribution of the source code, even though third parties are not
compelled to copy the source along with the object code.
</p>
<p id="section4">
<strong>4.</strong>
You may not copy, modify, sublicense, or distribute the Program
except as expressly provided under this License. Any attempt
otherwise to copy, modify, sublicense or distribute the Program is
void, and will automatically terminate your rights under this License.
However, parties who have received copies, or rights, from you under
this License will not have their licenses terminated so long as such
parties remain in full compliance.
</p>
<p id="section5">
<strong>5.</strong>
You are not required to accept this License, since you have not
signed it. However, nothing else grants you permission to modify or
distribute the Program or its derivative works. These actions are
prohibited by law if you do not accept this License. Therefore, by
modifying or distributing the Program (or any work based on the
Program), you indicate your acceptance of this License to do so, and
all its terms and conditions for copying, distributing or modifying
the Program or works based on it.
</p>
<p id="section6">
<strong>6.</strong>
Each time you redistribute the Program (or any work based on the
Program), the recipient automatically receives a license from the
original licensor to copy, distribute or modify the Program subject to
these terms and conditions. You may not impose any further
restrictions on the recipients' exercise of the rights granted herein.
You are not responsible for enforcing compliance by third parties to
this License.
</p>
<p id="section7">
<strong>7.</strong>
If, as a consequence of a court judgment or allegation of patent
infringement or for any other reason (not limited to patent issues),
conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot
distribute so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you
may not distribute the Program at all. For example, if a patent
license would not permit royalty-free redistribution of the Program by
all those who receive copies directly or indirectly through you, then
the only way you could satisfy both it and this License would be to
refrain entirely from distribution of the Program.
</p>
<p>
If any portion of this section is held invalid or unenforceable under
any particular circumstance, the balance of the section is intended to
apply and the section as a whole is intended to apply in other
circumstances.
</p>
<p>
It is not the purpose of this section to induce you to infringe any
patents or other property right claims or to contest validity of any
such claims; this section has the sole purpose of protecting the
integrity of the free software distribution system, which is
implemented by public license practices. Many people have made
generous contributions to the wide range of software distributed
through that system in reliance on consistent application of that
system; it is up to the author/donor to decide if he or she is willing
to distribute software through any other system and a licensee cannot
impose that choice.
</p>
<p>
This section is intended to make thoroughly clear what is believed to
be a consequence of the rest of this License.
</p>
<p id="section8">
<strong>8.</strong>
If the distribution and/or use of the Program is restricted in
certain countries either by patents or by copyrighted interfaces, the
original copyright holder who places the Program under this License
may add an explicit geographical distribution limitation excluding
those countries, so that distribution is permitted only in or among
countries not thus excluded. In such case, this License incorporates
the limitation as if written in the body of this License.
</p>
<p id="section9">
<strong>9.</strong>
The Free Software Foundation may publish revised and/or new versions
of the General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
</p>
<p>
Each version is given a distinguishing version number. If the Program
specifies a version number of this License which applies to it and "any
later version", you have the option of following the terms and conditions
either of that version or of any later version published by the Free
Software Foundation. If the Program does not specify a version number of
this License, you may choose any version ever published by the Free Software
Foundation.
</p>
<p id="section10">
<strong>10.</strong>
If you wish to incorporate parts of the Program into other free
programs whose distribution conditions are different, write to the author
to ask for permission. For software which is copyrighted by the Free
Software Foundation, write to the Free Software Foundation; we sometimes
make exceptions for this. Our decision will be guided by the two goals
of preserving the free status of all derivatives of our free software and
of promoting the sharing and reuse of software generally.
</p>
<p id="section11"><strong>NO WARRANTY</strong></p>
<p>
<strong>11.</strong>
BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
REPAIR OR CORRECTION.
</p>
<p id="section12">
<strong>12.</strong>
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
POSSIBILITY OF SUCH DAMAGES.
</p>
</body></html>

View File

@@ -1,7 +1,5 @@
package org.schabi.newpipe;
import android.annotation.TargetApi;
import android.app.Application;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.content.Context;
@@ -10,6 +8,7 @@ import android.os.Build;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.multidex.MultiDexApplication;
import androidx.preference.PreferenceManager;
import com.nostra13.universalimageloader.cache.memory.impl.LRULimitedMemoryCache;
@@ -33,9 +32,11 @@ import org.schabi.newpipe.util.StateSaver;
import java.io.IOException;
import java.io.InterruptedIOException;
import java.net.SocketException;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import io.reactivex.disposables.Disposable;
import io.reactivex.exceptions.CompositeException;
import io.reactivex.exceptions.MissingBackpressureException;
import io.reactivex.exceptions.OnErrorNotImplementedException;
@@ -61,10 +62,13 @@ import io.reactivex.plugins.RxJavaPlugins;
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
*/
public class App extends Application {
public class App extends MultiDexApplication {
protected static final String TAG = App.class.toString();
private static App app;
private Disposable disposable = null;
@NonNull
public static App getApp() {
return app;
}
@@ -90,7 +94,7 @@ public class App extends Application {
Localization.init(getApplicationContext());
StateSaver.init(this);
initNotificationChannel();
initNotificationChannels();
ServiceHelper.initServices(this);
@@ -100,7 +104,15 @@ public class App extends Application {
configureRxJavaErrorHandler();
// Check for new version
new CheckForNewAppVersionTask().execute();
disposable = CheckForNewAppVersion.checkNewVersion(this);
}
@Override
public void onTerminate() {
if (disposable != null) {
disposable.dispose();
}
super.onTerminate();
}
protected Downloader getDownloader() {
@@ -219,49 +231,31 @@ public class App extends Application {
}
}
public void initNotificationChannel() {
private void initNotificationChannels() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
return;
}
final String id = getString(R.string.notification_channel_id);
final CharSequence name = getString(R.string.notification_channel_name);
final String description = getString(R.string.notification_channel_description);
String id = getString(R.string.notification_channel_id);
String name = getString(R.string.notification_channel_name);
String description = getString(R.string.notification_channel_description);
// Keep this below DEFAULT to avoid making noise on every notification update
final int importance = NotificationManager.IMPORTANCE_LOW;
final NotificationChannel mChannel = new NotificationChannel(id, name, importance);
mChannel.setDescription(description);
final NotificationChannel mainChannel = new NotificationChannel(id, name, importance);
mainChannel.setDescription(description);
final NotificationManager mNotificationManager =
(NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
mNotificationManager.createNotificationChannel(mChannel);
id = getString(R.string.app_update_notification_channel_id);
name = getString(R.string.app_update_notification_channel_name);
description = getString(R.string.app_update_notification_channel_description);
setUpUpdateNotificationChannel(importance);
}
final NotificationChannel appUpdateChannel = new NotificationChannel(id, name, importance);
appUpdateChannel.setDescription(description);
/**
* Set up notification channel for app update.
*
* @param importance
*/
@TargetApi(Build.VERSION_CODES.O)
private void setUpUpdateNotificationChannel(final int importance) {
final String appUpdateId
= getString(R.string.app_update_notification_channel_id);
final CharSequence appUpdateName
= getString(R.string.app_update_notification_channel_name);
final String appUpdateDescription
= getString(R.string.app_update_notification_channel_description);
final NotificationChannel appUpdateChannel
= new NotificationChannel(appUpdateId, appUpdateName, importance);
appUpdateChannel.setDescription(appUpdateDescription);
final NotificationManager appUpdateNotificationManager
= (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
appUpdateNotificationManager.createNotificationChannel(appUpdateChannel);
final NotificationManager notificationManager = getSystemService(NotificationManager.class);
notificationManager.createNotificationChannels(Arrays.asList(mainChannel,
appUpdateChannel));
}
protected boolean isDisposedRxExceptionsReported() {

View File

@@ -2,7 +2,6 @@ package org.schabi.newpipe;
import android.app.Application;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.PackageInfo;
@@ -10,12 +9,13 @@ import android.content.pm.PackageManager;
import android.content.pm.Signature;
import android.net.ConnectivityManager;
import android.net.Uri;
import android.os.AsyncTask;
import androidx.preference.PreferenceManager;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.core.app.NotificationCompat;
import androidx.core.app.NotificationManagerCompat;
import androidx.core.content.ContextCompat;
import androidx.preference.PreferenceManager;
import com.grack.nanojson.JsonObject;
import com.grack.nanojson.JsonParser;
@@ -35,16 +35,18 @@ import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
/**
* AsyncTask to check if there is a newer version of the NewPipe github apk available or not.
* If there is a newer version we show a notification, informing the user. On tapping
* the notification, the user will be directed to the download link.
*/
public class CheckForNewAppVersionTask extends AsyncTask<Void, Void, String> {
private static final boolean DEBUG = MainActivity.DEBUG;
private static final String TAG = CheckForNewAppVersionTask.class.getSimpleName();
import io.reactivex.Observable;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.Disposable;
import io.reactivex.disposables.Disposables;
import io.reactivex.schedulers.Schedulers;
public final class CheckForNewAppVersion {
private CheckForNewAppVersion() { }
private static final boolean DEBUG = MainActivity.DEBUG;
private static final String TAG = CheckForNewAppVersion.class.getSimpleName();
private static final Application APP = App.getApp();
private static final String GITHUB_APK_SHA1
= "B0:2E:90:7C:1C:D6:FC:57:C3:35:F0:88:D0:8F:50:5F:94:E4:D2:15";
private static final String NEWPIPE_API_URL = "https://newpipe.schabi.org/api/data.json";
@@ -52,18 +54,19 @@ public class CheckForNewAppVersionTask extends AsyncTask<Void, Void, String> {
/**
* Method to get the apk's SHA1 key. See https://stackoverflow.com/questions/9293019/#22506133.
*
* @param application The application
* @return String with the apk's SHA1 fingeprint in hexadecimal
*/
private static String getCertificateSHA1Fingerprint() {
final PackageManager pm = APP.getPackageManager();
final String packageName = APP.getPackageName();
private static String getCertificateSHA1Fingerprint(@NonNull final Application application) {
final PackageManager pm = application.getPackageManager();
final String packageName = application.getPackageName();
final int flags = PackageManager.GET_SIGNATURES;
PackageInfo packageInfo = null;
try {
packageInfo = pm.getPackageInfo(packageName, flags);
} catch (final PackageManager.NameNotFoundException e) {
ErrorActivity.reportError(APP, e, null, null,
ErrorActivity.reportError(application, e, null, null,
ErrorActivity.ErrorInfo.make(UserAction.SOMETHING_ELSE, "none",
"Could not find package info", R.string.app_ui_crash));
}
@@ -78,7 +81,7 @@ public class CheckForNewAppVersionTask extends AsyncTask<Void, Void, String> {
final CertificateFactory cf = CertificateFactory.getInstance("X509");
c = (X509Certificate) cf.generateCertificate(input);
} catch (final CertificateException e) {
ErrorActivity.reportError(APP, e, null, null,
ErrorActivity.reportError(application, e, null, null,
ErrorActivity.ErrorInfo.make(UserAction.SOMETHING_ELSE, "none",
"Certificate error", R.string.app_ui_crash));
}
@@ -90,7 +93,7 @@ public class CheckForNewAppVersionTask extends AsyncTask<Void, Void, String> {
final byte[] publicKey = md.digest(c.getEncoded());
hexString = byte2HexFormatted(publicKey);
} catch (NoSuchAlgorithmException | CertificateEncodingException e) {
ErrorActivity.reportError(APP, e, null, null,
ErrorActivity.reportError(application, e, null, null,
ErrorActivity.ErrorInfo.make(UserAction.SOMETHING_ELSE, "none",
"Could not retrieve SHA1 key", R.string.app_ui_crash));
}
@@ -118,104 +121,108 @@ public class CheckForNewAppVersionTask extends AsyncTask<Void, Void, String> {
return str.toString();
}
public static boolean isGithubApk() {
return getCertificateSHA1Fingerprint().equals(GITHUB_APK_SHA1);
/**
* Method to compare the current and latest available app version.
* If a newer version is available, we show the update notification.
*
* @param application The application
* @param versionName Name of new version
* @param apkLocationUrl Url with the new apk
* @param versionCode Code of new version
*/
private static void compareAppVersionAndShowNotification(@NonNull final Application application,
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));
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());
}
}
@Override
protected void onPreExecute() {
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(APP);
private static boolean isConnected(@NonNull final App app) {
final ConnectivityManager cm = ContextCompat.getSystemService(app,
ConnectivityManager.class);
return cm.getActiveNetworkInfo() != null
&& cm.getActiveNetworkInfo().isConnected();
}
public static boolean isGithubApk(@NonNull final App app) {
return getCertificateSHA1Fingerprint(app).equals(GITHUB_APK_SHA1);
}
@NonNull
public static Disposable checkNewVersion(@NonNull final App app) {
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(app);
// Check if user has enabled/disabled update checking
// and if the current apk is a github one or not.
if (!prefs.getBoolean(APP.getString(R.string.update_app_key), true) || !isGithubApk()) {
this.cancel(true);
}
}
@Override
protected String doInBackground(final Void... voids) {
if (isCancelled() || !isConnected()) {
return null;
if (!prefs.getBoolean(app.getString(R.string.update_app_key), true)
|| !isGithubApk(app)) {
return Disposables.empty();
}
// Make a network request to get latest NewPipe data.
try {
return DownloaderImpl.getInstance().get(NEWPIPE_API_URL).responseBody();
} catch (IOException | ReCaptchaException e) {
// connectivity problems, do not alarm user and fail silently
if (DEBUG) {
Log.w(TAG, Log.getStackTraceString(e));
return Observable.fromCallable(() -> {
if (!isConnected(app)) {
return null;
}
}
return null;
}
@Override
protected void onPostExecute(final String response) {
// Parse the json from the response.
if (response != null) {
// Make a network request to get latest NewPipe data.
try {
final JsonObject githubStableObject = JsonParser.object().from(response)
.getObject("flavors").getObject("github").getObject("stable");
final String versionName = githubStableObject.getString("version");
final int versionCode = githubStableObject.getInt("version_code");
final String apkLocationUrl = githubStableObject.getString("apk");
compareAppVersionAndShowNotification(versionName, apkLocationUrl, versionCode);
} catch (final JsonParserException e) {
return DownloaderImpl.getInstance().get(NEWPIPE_API_URL).responseBody();
} catch (IOException | ReCaptchaException e) {
// connectivity problems, do not alarm user and fail silently
if (DEBUG) {
Log.w(TAG, Log.getStackTraceString(e));
}
}
}
}
/**
* Method to compare the current and latest available app version.
* If a newer version is available, we show the update notification.
*
* @param versionName Name of new version
* @param apkLocationUrl Url with the new apk
* @param versionCode Code of new version
*/
private void compareAppVersionAndShowNotification(final String versionName,
final String apkLocationUrl,
final int versionCode) {
final int notificationId = 2000;
return null;
})
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(response -> {
// Parse the json from the response.
if (response != null) {
try {
final JsonObject githubStableObject = JsonParser.object().from(response)
.getObject("flavors").getObject("github").getObject("stable");
if (BuildConfig.VERSION_CODE < versionCode) {
final String versionName = githubStableObject.getString("version");
final int versionCode = githubStableObject.getInt("version_code");
final String apkLocationUrl = githubStableObject.getString("apk");
// A pending intent to open the apk location url in the browser.
final Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(apkLocationUrl));
final PendingIntent pendingIntent
= PendingIntent.getActivity(APP, 0, intent, 0);
final NotificationCompat.Builder notificationBuilder = new NotificationCompat
.Builder(APP, APP.getString(R.string.app_update_notification_channel_id))
.setSmallIcon(R.drawable.ic_newpipe_update)
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
.setContentIntent(pendingIntent)
.setAutoCancel(true)
.setContentTitle(APP.getString(R.string.app_update_notification_content_title))
.setContentText(APP.getString(R.string.app_update_notification_content_text)
+ " " + versionName);
final NotificationManagerCompat notificationManager
= NotificationManagerCompat.from(APP);
notificationManager.notify(notificationId, notificationBuilder.build());
}
}
private boolean isConnected() {
final ConnectivityManager cm =
(ConnectivityManager) APP.getSystemService(Context.CONNECTIVITY_SERVICE);
return cm.getActiveNetworkInfo() != null
&& cm.getActiveNetworkInfo().isConnected();
compareAppVersionAndShowNotification(app, versionName, apkLocationUrl,
versionCode);
} catch (final JsonParserException e) {
// connectivity problems, do not alarm user and fail silently
if (DEBUG) {
Log.w(TAG, Log.getStackTraceString(e));
}
}
}
});
}
}

View File

@@ -30,9 +30,7 @@ import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import androidx.preference.PreferenceManager;
import android.util.Log;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.Menu;
@@ -46,6 +44,7 @@ import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.Spinner;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.ActionBarDrawerToggle;
@@ -55,6 +54,7 @@ import androidx.core.view.GravityCompat;
import androidx.drawerlayout.widget.DrawerLayout;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.preference.PreferenceManager;
import com.google.android.material.bottomsheet.BottomSheetBehavior;
import com.google.android.material.navigation.NavigationView;
@@ -69,10 +69,11 @@ import org.schabi.newpipe.fragments.detail.VideoDetailFragment;
import org.schabi.newpipe.fragments.list.search.SearchFragment;
import org.schabi.newpipe.player.VideoPlayer;
import org.schabi.newpipe.player.event.OnKeyDownListener;
import org.schabi.newpipe.player.helper.PlayerHolder;
import org.schabi.newpipe.player.playqueue.PlayQueue;
import org.schabi.newpipe.report.ErrorActivity;
import org.schabi.newpipe.util.DeviceUtils;
import org.schabi.newpipe.util.Constants;
import org.schabi.newpipe.util.DeviceUtils;
import org.schabi.newpipe.util.KioskTranslator;
import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.NavigationHelper;
@@ -87,6 +88,7 @@ import org.schabi.newpipe.views.FocusOverlayView;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
@@ -152,7 +154,7 @@ public class MainActivity extends AppCompatActivity {
if (DeviceUtils.isTv(this)) {
FocusOverlayView.setupFocusObserver(this);
}
setupBroadcastReceiver();
openMiniPlayerUponPlayerStarted();
}
private void setupDrawer() throws Exception {
@@ -758,32 +760,36 @@ public class MainActivity extends AppCompatActivity {
if (intent.hasExtra(Constants.KEY_LINK_TYPE)) {
final String url = intent.getStringExtra(Constants.KEY_URL);
final int serviceId = intent.getIntExtra(Constants.KEY_SERVICE_ID, 0);
final String title = intent.getStringExtra(Constants.KEY_TITLE);
switch (((StreamingService.LinkType) intent
.getSerializableExtra(Constants.KEY_LINK_TYPE))) {
String title = intent.getStringExtra(Constants.KEY_TITLE);
if (title == null) {
title = "";
}
final StreamingService.LinkType linkType = ((StreamingService.LinkType) intent
.getSerializableExtra(Constants.KEY_LINK_TYPE));
assert linkType != null;
switch (linkType) {
case STREAM:
final boolean autoPlay = intent
.getBooleanExtra(VideoDetailFragment.AUTO_PLAY, false);
final String intentCacheKey = intent
.getStringExtra(VideoPlayer.PLAY_QUEUE_KEY);
final String intentCacheKey = intent.getStringExtra(
VideoPlayer.PLAY_QUEUE_KEY);
final PlayQueue playQueue = intentCacheKey != null
? SerializedCache.getInstance()
.take(intentCacheKey, PlayQueue.class)
: null;
NavigationHelper.openVideoDetailFragment(getSupportFragmentManager(),
serviceId, url, title, autoPlay, playQueue);
final boolean switchingPlayers = intent.getBooleanExtra(
VideoDetailFragment.KEY_SWITCHING_PLAYERS, false);
NavigationHelper.openVideoDetailFragment(
getApplicationContext(), getSupportFragmentManager(),
serviceId, url, title, playQueue, switchingPlayers);
break;
case CHANNEL:
NavigationHelper.openChannelFragment(getSupportFragmentManager(),
serviceId,
url,
title);
serviceId, url, title);
break;
case PLAYLIST:
NavigationHelper.openPlaylistFragment(getSupportFragmentManager(),
serviceId,
url,
title);
serviceId, url, title);
break;
}
} else if (intent.hasExtra(Constants.KEY_OPEN_SEARCH)) {
@@ -805,34 +811,47 @@ public class MainActivity extends AppCompatActivity {
}
}
private void setupBroadcastReceiver() {
broadcastReceiver = new BroadcastReceiver() {
@Override
public void onReceive(final Context context, final Intent intent) {
if (intent.getAction().equals(VideoDetailFragment.ACTION_PLAYER_STARTED)) {
final Fragment fragmentPlayer = getSupportFragmentManager()
.findFragmentById(R.id.fragment_player_holder);
if (fragmentPlayer == null) {
/*
* We still don't have a fragment attached to the activity.
* It can happen when a user started popup or background players
* without opening a stream inside the fragment.
* Adding it in a collapsed state (only mini player will be visible)
* */
NavigationHelper.showMiniPlayer(getSupportFragmentManager());
private void openMiniPlayerIfMissing() {
final Fragment fragmentPlayer = getSupportFragmentManager()
.findFragmentById(R.id.fragment_player_holder);
if (fragmentPlayer == null) {
// We still don't have a fragment attached to the activity. It can happen when a user
// started popup or background players without opening a stream inside the fragment.
// Adding it in a collapsed state (only mini player will be visible).
NavigationHelper.showMiniPlayer(getSupportFragmentManager());
}
}
private void openMiniPlayerUponPlayerStarted() {
if (getIntent().getSerializableExtra(Constants.KEY_LINK_TYPE)
== StreamingService.LinkType.STREAM) {
// handleIntent() already takes care of opening video detail fragment
// due to an intent containing a STREAM link
return;
}
if (PlayerHolder.isPlayerOpen()) {
// if the player is already open, no need for a broadcast receiver
openMiniPlayerIfMissing();
} else {
// listen for player start intent being sent around
broadcastReceiver = new BroadcastReceiver() {
@Override
public void onReceive(final Context context, final Intent intent) {
if (Objects.equals(intent.getAction(),
VideoDetailFragment.ACTION_PLAYER_STARTED)) {
openMiniPlayerIfMissing();
// At this point the player is added 100%, we can unregister. Other actions
// are useless since the fragment will not be removed after that.
unregisterReceiver(broadcastReceiver);
broadcastReceiver = null;
}
/*
* At this point the player is added 100%, we can unregister.
* Other actions are useless since the fragment will not be removed after that
* */
unregisterReceiver(broadcastReceiver);
broadcastReceiver = null;
}
}
};
final IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(VideoDetailFragment.ACTION_PLAYER_STARTED);
registerReceiver(broadcastReceiver, intentFilter);
};
final IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(VideoDetailFragment.ACTION_PLAYER_STARTED);
registerReceiver(broadcastReceiver, intentFilter);
}
}
private boolean bottomSheetHiddenOrCollapsed() {

View File

@@ -8,7 +8,6 @@ import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.os.Bundle;
import androidx.preference.PreferenceManager;
import android.text.TextUtils;
import android.view.ContextThemeWrapper;
import android.view.LayoutInflater;
@@ -27,7 +26,9 @@ import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.content.res.AppCompatResources;
import androidx.core.app.NotificationCompat;
import androidx.core.widget.TextViewCompat;
import androidx.fragment.app.FragmentManager;
import androidx.preference.PreferenceManager;
import org.schabi.newpipe.download.DownloadDialog;
import org.schabi.newpipe.extractor.Info;
@@ -39,14 +40,16 @@ import org.schabi.newpipe.extractor.exceptions.ExtractionException;
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.player.MainPlayer;
import org.schabi.newpipe.player.helper.PlayerHelper;
import org.schabi.newpipe.player.helper.PlayerHolder;
import org.schabi.newpipe.player.playqueue.ChannelPlayQueue;
import org.schabi.newpipe.player.playqueue.PlayQueue;
import org.schabi.newpipe.player.playqueue.PlaylistPlayQueue;
import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
import org.schabi.newpipe.report.UserAction;
import org.schabi.newpipe.util.DeviceUtils;
import org.schabi.newpipe.util.Constants;
import org.schabi.newpipe.util.DeviceUtils;
import org.schabi.newpipe.util.ExtractorHelper;
import org.schabi.newpipe.util.ListHelper;
import org.schabi.newpipe.util.NavigationHelper;
@@ -59,8 +62,6 @@ import org.schabi.newpipe.views.FocusOverlayView;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import icepick.Icepick;
@@ -115,8 +116,6 @@ public class RouterActivity extends AppCompatActivity {
}
}
internalRoute = getIntent().getBooleanExtra(INTERNAL_ROUTE_KEY, false);
setTheme(ThemeHelper.isLightThemeSelected(this)
? R.style.RouterActivityThemeLight : R.style.RouterActivityThemeDark);
}
@@ -326,7 +325,7 @@ public class RouterActivity extends AppCompatActivity {
final RadioButton radioButton
= (RadioButton) inflater.inflate(R.layout.list_radio_icon_item, null);
radioButton.setText(item.description);
radioButton.setCompoundDrawablesWithIntrinsicBounds(
TextViewCompat.setCompoundDrawablesRelativeWithIntrinsicBounds(radioButton,
AppCompatResources.getDrawable(getApplicationContext(), item.icon),
null, null, null);
radioButton.setChecked(false);
@@ -397,14 +396,22 @@ public class RouterActivity extends AppCompatActivity {
// show both "show info" and "video player", they are two different activities
returnList.add(showInfo);
returnList.add(videoPlayer);
} else if (capabilities.contains(VIDEO)
&& PlayerHelper.isAutoplayAllowedByUser(context)) {
// show only "video player" since the details activity will be opened and the video
// will be autoplayed there and "show info" would do the exact same thing
returnList.add(videoPlayer);
} else {
// show only "show info" if video player is not applicable or autoplay is disabled
returnList.add(showInfo);
final MainPlayer.PlayerType playerType = PlayerHolder.getType();
if (capabilities.contains(VIDEO)
&& PlayerHelper.isAutoplayAllowedByUser(context)
&& playerType == null || playerType == MainPlayer.PlayerType.VIDEO) {
// show only "video player" since the details activity will be opened and the
// video will be auto played there. Since "show info" would do the exact same
// thing, use that as a key to let VideoDetailFragment load the stream instead
// of using FetcherService (see comment in handleChoice())
returnList.add(new AdapterChoiceItem(
showInfo.key, videoPlayer.description, videoPlayer.icon));
} else {
// show only "show info" if video player is not applicable, auto play is
// disabled or a video is playing in a player different than the main one
returnList.add(showInfo);
}
}
if (capabilities.contains(VIDEO)) {
@@ -491,12 +498,7 @@ public class RouterActivity extends AppCompatActivity {
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(intent -> {
if (!internalRoute) {
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK);
}
startActivity(intent);
finish();
}, throwable -> handleError(throwable, currentUrl))
);
@@ -514,7 +516,7 @@ public class RouterActivity extends AppCompatActivity {
@SuppressLint("CheckResult")
private void openDownloadDialog() {
ExtractorHelper.getStreamInfo(currentServiceId, currentUrl, true)
disposables.add(ExtractorHelper.getStreamInfo(currentServiceId, currentUrl, true)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe((@NonNull StreamInfo result) -> {
@@ -531,10 +533,10 @@ public class RouterActivity extends AppCompatActivity {
downloadDialog.setSelectedVideoStream(selectedVideoStreamIndex);
downloadDialog.show(fm, "downloadDialog");
fm.executePendingTransactions();
downloadDialog.getDialog().setOnDismissListener(dialog -> finish());
downloadDialog.requireDialog().setOnDismissListener(dialog -> finish());
}, (@NonNull Throwable throwable) -> {
showUnsupportedUrlDialog(currentUrl);
});
}));
}
@Override
@@ -552,66 +554,6 @@ public class RouterActivity extends AppCompatActivity {
}
}
/*//////////////////////////////////////////////////////////////////////////
// Service Fetcher
//////////////////////////////////////////////////////////////////////////*/
private String removeHeadingGibberish(final String input) {
int start = 0;
for (int i = input.indexOf("://") - 1; i >= 0; i--) {
if (!input.substring(i, i + 1).matches("\\p{L}")) {
start = i + 1;
break;
}
}
return input.substring(start);
}
/*//////////////////////////////////////////////////////////////////////////
// Utils
//////////////////////////////////////////////////////////////////////////*/
private String trim(final String input) {
if (input == null || input.length() < 1) {
return input;
} else {
String output = input;
while (output.length() > 0 && output.substring(0, 1).matches(REGEX_REMOVE_FROM_URL)) {
output = output.substring(1);
}
while (output.length() > 0
&& output.substring(output.length() - 1).matches(REGEX_REMOVE_FROM_URL)) {
output = output.substring(0, output.length() - 1);
}
return output;
}
}
/**
* Retrieves all Strings which look remotely like URLs from a text.
* Used if NewPipe was called through share menu.
*
* @param sharedText text to scan for URLs.
* @return potential URLs
*/
protected String[] getUris(final String sharedText) {
final Collection<String> result = new HashSet<>();
if (sharedText != null) {
final String[] array = sharedText.split("\\p{Space}");
for (String s : array) {
s = trim(s);
if (s.length() != 0) {
if (s.matches(".+://.+")) {
result.add(removeHeadingGibberish(s));
} else if (s.matches(".+\\..+")) {
result.add("http://" + s);
}
}
}
}
return result.toArray(new String[0]);
}
private static class AdapterChoiceItem {
final String description;
final String key;
@@ -724,50 +666,34 @@ public class RouterActivity extends AppCompatActivity {
final boolean isExtAudioEnabled = preferences.getBoolean(
getString(R.string.use_external_audio_player_key), false);
PlayQueue playQueue;
final String playerChoice = choice.playerChoice;
final PlayQueue playQueue;
if (info instanceof StreamInfo) {
if (playerChoice.equals(backgroundPlayerKey) && isExtAudioEnabled) {
if (choice.playerChoice.equals(backgroundPlayerKey) && isExtAudioEnabled) {
NavigationHelper.playOnExternalAudioPlayer(this, (StreamInfo) info);
} else if (playerChoice.equals(videoPlayerKey) && isExtVideoEnabled) {
return;
} else if (choice.playerChoice.equals(videoPlayerKey) && isExtVideoEnabled) {
NavigationHelper.playOnExternalVideoPlayer(this, (StreamInfo) info);
} else {
playQueue = new SinglePlayQueue((StreamInfo) info);
if (playerChoice.equals(videoPlayerKey)) {
openMainPlayer(playQueue, choice);
} else if (playerChoice.equals(backgroundPlayerKey)) {
NavigationHelper.enqueueOnBackgroundPlayer(this, playQueue, true);
} else if (playerChoice.equals(popupPlayerKey)) {
NavigationHelper.enqueueOnPopupPlayer(this, playQueue, true);
}
return;
}
playQueue = new SinglePlayQueue((StreamInfo) info);
} else if (info instanceof ChannelInfo) {
playQueue = new ChannelPlayQueue((ChannelInfo) info);
} else if (info instanceof PlaylistInfo) {
playQueue = new PlaylistPlayQueue((PlaylistInfo) info);
} else {
return;
}
if (info instanceof ChannelInfo || info instanceof PlaylistInfo) {
playQueue = info instanceof ChannelInfo
? new ChannelPlayQueue((ChannelInfo) info)
: new PlaylistPlayQueue((PlaylistInfo) info);
if (playerChoice.equals(videoPlayerKey)) {
openMainPlayer(playQueue, choice);
} else if (playerChoice.equals(backgroundPlayerKey)) {
NavigationHelper.playOnBackgroundPlayer(this, playQueue, true);
} else if (playerChoice.equals(popupPlayerKey)) {
NavigationHelper.playOnPopupPlayer(this, playQueue, true);
}
if (choice.playerChoice.equals(videoPlayerKey)) {
NavigationHelper.playOnMainPlayer(this, playQueue, false);
} else if (choice.playerChoice.equals(backgroundPlayerKey)) {
NavigationHelper.playOnBackgroundPlayer(this, playQueue, true);
} else if (choice.playerChoice.equals(popupPlayerKey)) {
NavigationHelper.playOnPopupPlayer(this, playQueue, true);
}
};
}
private void openMainPlayer(final PlayQueue playQueue, final Choice choice) {
NavigationHelper.playOnMainPlayer(this, playQueue, choice.linkType,
choice.url, "", true, true);
}
@Override
public void onDestroy() {
super.onDestroy();

View File

@@ -8,16 +8,17 @@ import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentPagerAdapter;
import androidx.fragment.app.FragmentStatePagerAdapter;
import androidx.viewpager.widget.PagerAdapter;
import androidx.viewpager.widget.ViewPager;
import androidx.fragment.app.FragmentActivity;
import androidx.recyclerview.widget.RecyclerView;
import androidx.viewpager2.adapter.FragmentStateAdapter;
import androidx.viewpager2.widget.ViewPager2;
import com.google.android.material.tabs.TabLayout;
import com.google.android.material.tabs.TabLayoutMediator;
import org.schabi.newpipe.BuildConfig;
import org.schabi.newpipe.R;
@@ -32,7 +33,7 @@ public class AboutActivity extends AppCompatActivity {
*/
private static final SoftwareComponent[] SOFTWARE_COMPONENTS = new SoftwareComponent[]{
new SoftwareComponent("Giga Get", "2014 - 2015", "Peter Cai",
"https://github.com/PaperAirplane-Dev-Team/GigaGet", StandardLicenses.GPL2),
"https://github.com/PaperAirplane-Dev-Team/GigaGet", StandardLicenses.GPL3),
new SoftwareComponent("NewPipe Extractor", "2017 - 2020", "Christian Schabesberger",
"https://github.com/TeamNewPipe/NewPipeExtractor", StandardLicenses.GPL3),
new SoftwareComponent("Jsoup", "2017", "Jonathan Hedley",
@@ -64,20 +65,20 @@ public class AboutActivity extends AppCompatActivity {
"https://github.com/lisawray/groupie", StandardLicenses.MIT)
};
private static final int POS_ABOUT = 0;
private static final int POS_LICENSE = 1;
private static final int TOTAL_COUNT = 2;
/**
* The {@link PagerAdapter} that will provide
* The {@link RecyclerView.Adapter} that will provide
* fragments for each of the sections. We use a
* {@link FragmentPagerAdapter} derivative, which will keep every
* loaded fragment in memory. If this becomes too memory intensive, it
* may be best to switch to a
* {@link FragmentStatePagerAdapter}.
* {@link FragmentStateAdapter} derivative, which will keep every
* loaded fragment in memory.
*/
private SectionsPagerAdapter mSectionsPagerAdapter;
/**
* The {@link ViewPager} that will host the section contents.
* The {@link ViewPager2} that will host the section contents.
*/
private ViewPager mViewPager;
private ViewPager2 mViewPager;
@Override
protected void onCreate(final Bundle savedInstanceState) {
@@ -93,14 +94,24 @@ public class AboutActivity extends AppCompatActivity {
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
// Create the adapter that will return a fragment for each of the three
// primary sections of the activity.
mSectionsPagerAdapter = new SectionsPagerAdapter(getSupportFragmentManager());
mSectionsPagerAdapter = new SectionsPagerAdapter(this);
// Set up the ViewPager with the sections adapter.
mViewPager = findViewById(R.id.container);
mViewPager.setAdapter(mSectionsPagerAdapter);
final TabLayout tabLayout = findViewById(R.id.tabs);
tabLayout.setupWithViewPager(mViewPager);
new TabLayoutMediator(tabLayout, mViewPager, (tab, position) -> {
switch (position) {
default:
case POS_ABOUT:
tab.setText(R.string.tab_about);
break;
case POS_LICENSE:
tab.setText(R.string.tab_licenses);
break;
}
}).attach();
}
@Override
@@ -162,40 +173,30 @@ public class AboutActivity extends AppCompatActivity {
}
/**
* A {@link FragmentPagerAdapter} that returns a fragment corresponding to
* A {@link FragmentStateAdapter} that returns a fragment corresponding to
* one of the sections/tabs/pages.
*/
public class SectionsPagerAdapter extends FragmentPagerAdapter {
public SectionsPagerAdapter(final FragmentManager fm) {
super(fm, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT);
public static class SectionsPagerAdapter extends FragmentStateAdapter {
public SectionsPagerAdapter(final FragmentActivity fa) {
super(fa);
}
@NonNull
@Override
public Fragment getItem(final int position) {
public Fragment createFragment(final int position) {
switch (position) {
case 0:
default:
case POS_ABOUT:
return AboutFragment.newInstance();
case 1:
case POS_LICENSE:
return LicenseFragment.newInstance(SOFTWARE_COMPONENTS);
}
return null;
}
@Override
public int getCount() {
public int getItemCount() {
// Show 2 total pages.
return 2;
}
@Override
public CharSequence getPageTitle(final int position) {
switch (position) {
case 0:
return getString(R.string.tab_about);
case 1:
return getString(R.string.tab_licenses);
}
return null;
return TOTAL_COUNT;
}
}
}

View File

@@ -1,6 +1,5 @@
package org.schabi.newpipe.about;
import android.app.Activity;
import android.os.Bundle;
import android.view.ContextMenu;
import android.view.LayoutInflater;
@@ -19,16 +18,21 @@ import org.schabi.newpipe.util.ShareUtils;
import java.io.Serializable;
import java.util.Arrays;
import java.util.Comparator;
import io.reactivex.disposables.CompositeDisposable;
/**
* Fragment containing the software licenses.
*/
public class LicenseFragment extends Fragment {
private static final String ARG_COMPONENTS = "components";
private static final String LICENSE_KEY = "ACTIVE_LICENSE";
private SoftwareComponent[] softwareComponents;
private SoftwareComponent componentForContextMenu;
private License activeLicense;
private static final String LICENSE_KEY = "ACTIVE_LICENSE";
private final CompositeDisposable compositeDisposable = new CompositeDisposable();
public static LicenseFragment newInstance(final SoftwareComponent[] softwareComponents) {
if (softwareComponents == null) {
@@ -41,16 +45,6 @@ public class LicenseFragment extends Fragment {
return fragment;
}
/**
* Shows a popup containing the license.
*
* @param context the context to use
* @param license the license to show
*/
private static void showLicense(final Activity context, final License license) {
new LicenseFragmentHelper(context).execute(license);
}
@Override
public void onCreate(@Nullable final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
@@ -64,7 +58,13 @@ public class LicenseFragment extends Fragment {
}
}
// Sort components by name
Arrays.sort(softwareComponents, (o1, o2) -> o1.getName().compareTo(o2.getName()));
Arrays.sort(softwareComponents, Comparator.comparing(SoftwareComponent::getName));
}
@Override
public void onDestroy() {
compositeDisposable.dispose();
super.onDestroy();
}
@Nullable
@@ -76,8 +76,9 @@ public class LicenseFragment extends Fragment {
final View licenseLink = rootView.findViewById(R.id.app_read_license);
licenseLink.setOnClickListener(v -> {
activeLicense = StandardLicenses.GPL3;
showLicense(getActivity(), StandardLicenses.GPL3);
activeLicense = StandardLicenses.GPL3;
compositeDisposable.add(LicenseFragmentHelper.showLicense(getActivity(),
StandardLicenses.GPL3));
});
for (final SoftwareComponent component : softwareComponents) {
@@ -94,13 +95,15 @@ public class LicenseFragment extends Fragment {
componentView.setTag(component);
componentView.setOnClickListener(v -> {
activeLicense = component.getLicense();
showLicense(getActivity(), component.getLicense());
compositeDisposable.add(LicenseFragmentHelper.showLicense(getActivity(),
component.getLicense()));
});
softwareComponentsView.addView(componentView);
registerForContextMenu(componentView);
}
if (activeLicense != null) {
showLicense(getActivity(), activeLicense);
compositeDisposable.add(LicenseFragmentHelper.showLicense(getActivity(),
activeLicense));
}
return rootView;
}
@@ -128,7 +131,8 @@ public class LicenseFragment extends Fragment {
ShareUtils.openUrlInBrowser(getActivity(), component.getLink());
return true;
case R.id.action_show_license:
showLicense(getActivity(), component.getLicense());
compositeDisposable.add(LicenseFragmentHelper.showLicense(getActivity(),
component.getLicense()));
}
return false;
}

View File

@@ -1,8 +1,6 @@
package org.schabi.newpipe.about;
import android.app.Activity;
import android.content.Context;
import android.os.AsyncTask;
import android.util.Base64;
import android.webkit.WebView;
@@ -16,18 +14,18 @@ import org.schabi.newpipe.util.ThemeHelper;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.lang.ref.WeakReference;
import java.nio.charset.StandardCharsets;
import io.reactivex.Observable;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.Disposable;
import io.reactivex.disposables.Disposables;
import io.reactivex.schedulers.Schedulers;
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
public class LicenseFragmentHelper extends AsyncTask<Object, Void, Integer> {
private final WeakReference<Activity> weakReference;
private License license;
public LicenseFragmentHelper(@Nullable final Activity activity) {
weakReference = new WeakReference<>(activity);
}
public final class LicenseFragmentHelper {
private LicenseFragmentHelper() { }
/**
* @param context the context to use
@@ -62,7 +60,7 @@ public class LicenseFragmentHelper extends AsyncTask<Object, Void, Integer> {
* @param context
* @return String which is a CSS stylesheet according to the context's theme
*/
private static String getLicenseStylesheet(final Context context) {
private static String getLicenseStylesheet(@NonNull final Context context) {
final boolean isLightTheme = ThemeHelper.isLightThemeSelected(context);
return "body{padding:12px 15px;margin:0;"
+ "background:#" + getHexRGBColor(context, isLightTheme
@@ -84,45 +82,31 @@ public class LicenseFragmentHelper extends AsyncTask<Object, Void, Integer> {
* @param color the color number from R.color
* @return a six characters long String with hexadecimal RGB values
*/
private static String getHexRGBColor(final Context context, final int color) {
private static String getHexRGBColor(@NonNull final Context context, final int color) {
return context.getResources().getString(color).substring(3);
}
@Nullable
private Activity getActivity() {
final Activity activity = weakReference.get();
if (activity != null && activity.isFinishing()) {
return null;
} else {
return activity;
}
}
@Override
protected Integer doInBackground(final Object... objects) {
license = (License) objects[0];
return 1;
}
@Override
protected void onPostExecute(final Integer result) {
final Activity activity = getActivity();
if (activity == null) {
return;
static Disposable showLicense(@Nullable final Context context, @NonNull final License license) {
if (context == null) {
return Disposables.empty();
}
final String webViewData = Base64.encodeToString(getFormattedLicense(activity, license)
.getBytes(StandardCharsets.UTF_8), Base64.NO_PADDING);
final WebView webView = new WebView(activity);
webView.loadData(webViewData, "text/html; charset=UTF-8", "base64");
return Observable.fromCallable(() -> getFormattedLicense(context, license))
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(formattedLicense -> {
final String webViewData = Base64.encodeToString(formattedLicense
.getBytes(StandardCharsets.UTF_8), Base64.NO_PADDING);
final WebView webView = new WebView(context);
webView.loadData(webViewData, "text/html; charset=UTF-8", "base64");
final AlertDialog.Builder alert = new AlertDialog.Builder(activity);
alert.setTitle(license.getName());
alert.setView(webView);
assureCorrectAppLanguage(activity);
alert.setNegativeButton(activity.getString(R.string.finish),
(dialog, which) -> dialog.dismiss());
alert.show();
final AlertDialog.Builder alert = new AlertDialog.Builder(context);
alert.setTitle(license.getName());
alert.setView(webView);
assureCorrectAppLanguage(context);
alert.setNegativeButton(context.getString(R.string.finish),
(dialog, which) -> dialog.dismiss());
alert.show();
});
}
}

View File

@@ -4,8 +4,6 @@ package org.schabi.newpipe.about;
* Class containing information about standard software licenses.
*/
public final class StandardLicenses {
public static final License GPL2
= new License("GNU General Public License, Version 2.0", "GPLv2", "gpl_2.html");
public static final License GPL3
= new License("GNU General Public License, Version 3.0", "GPLv3", "gpl_3.html");
public static final License APACHE2

View File

@@ -5,31 +5,35 @@ import androidx.room.TypeConverter;
import org.schabi.newpipe.extractor.stream.StreamType;
import org.schabi.newpipe.local.subscription.FeedGroupIcon;
import java.util.Date;
import java.time.Instant;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
public final class Converters {
private Converters() { }
/**
* Convert a long value to a date.
* Convert a long value to a {@link OffsetDateTime}.
*
* @param value the long value
* @return the date
* @return the {@code OffsetDateTime}
*/
@TypeConverter
public static Date fromTimestamp(final Long value) {
return value == null ? null : new Date(value);
public static OffsetDateTime offsetDateTimeFromTimestamp(final Long value) {
return value == null ? null : OffsetDateTime.ofInstant(Instant.ofEpochMilli(value),
ZoneOffset.UTC);
}
/**
* Convert a date to a long value.
* Convert a {@link OffsetDateTime} to a long value.
*
* @param date the date
* @param offsetDateTime the {@code OffsetDateTime}
* @return the long value
*/
@TypeConverter
public static Long dateToTimestamp(final Date date) {
return date == null ? null : date.getTime();
public static Long offsetDateTimeToTimestamp(final OffsetDateTime offsetDateTime) {
return offsetDateTime == null ? null : offsetDateTime.withOffsetSameInstant(ZoneOffset.UTC)
.toInstant().toEpochMilli();
}
@TypeConverter

View File

@@ -7,7 +7,7 @@ import androidx.room.Query
import androidx.room.Transaction
import androidx.room.Update
import io.reactivex.Flowable
import java.util.Date
import java.time.OffsetDateTime
import org.schabi.newpipe.database.feed.model.FeedEntity
import org.schabi.newpipe.database.feed.model.FeedLastUpdatedEntity
import org.schabi.newpipe.database.stream.model.StreamEntity
@@ -58,10 +58,10 @@ abstract class FeedDAO {
INNER JOIN feed f
ON s.uid = f.stream_id
WHERE s.upload_date < :date
WHERE s.upload_date < :offsetDateTime
)
""")
abstract fun unlinkStreamsOlderThan(date: Date)
abstract fun unlinkStreamsOlderThan(offsetDateTime: OffsetDateTime)
@Query("""
DELETE FROM feed
@@ -106,10 +106,10 @@ abstract class FeedDAO {
INNER JOIN feed_group_subscription_join fgs
ON fgs.subscription_id = lu.subscription_id AND fgs.group_id = :groupId
""")
abstract fun oldestSubscriptionUpdate(groupId: Long): Flowable<List<Date>>
abstract fun oldestSubscriptionUpdate(groupId: Long): Flowable<List<OffsetDateTime>>
@Query("SELECT MIN(last_updated) FROM feed_last_updated")
abstract fun oldestSubscriptionUpdateFromAll(): Flowable<List<Date>>
abstract fun oldestSubscriptionUpdateFromAll(): Flowable<List<OffsetDateTime>>
@Query("SELECT COUNT(*) FROM feed_last_updated WHERE last_updated IS NULL")
abstract fun notLoadedCount(): Flowable<Long>
@@ -135,7 +135,7 @@ abstract class FeedDAO {
WHERE lu.last_updated IS NULL OR lu.last_updated < :outdatedThreshold
""")
abstract fun getAllOutdated(outdatedThreshold: Date): Flowable<List<SubscriptionEntity>>
abstract fun getAllOutdated(outdatedThreshold: OffsetDateTime): Flowable<List<SubscriptionEntity>>
@Query("""
SELECT s.* FROM subscriptions s
@@ -148,5 +148,5 @@ abstract class FeedDAO {
WHERE lu.last_updated IS NULL OR lu.last_updated < :outdatedThreshold
""")
abstract fun getAllOutdatedForGroup(groupId: Long, outdatedThreshold: Date): Flowable<List<SubscriptionEntity>>
abstract fun getAllOutdatedForGroup(groupId: Long, outdatedThreshold: OffsetDateTime): Flowable<List<SubscriptionEntity>>
}

View File

@@ -4,7 +4,7 @@ import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.ForeignKey
import androidx.room.PrimaryKey
import java.util.Date
import java.time.OffsetDateTime
import org.schabi.newpipe.database.feed.model.FeedLastUpdatedEntity.Companion.FEED_LAST_UPDATED_TABLE
import org.schabi.newpipe.database.feed.model.FeedLastUpdatedEntity.Companion.SUBSCRIPTION_ID
import org.schabi.newpipe.database.subscription.SubscriptionEntity
@@ -25,9 +25,8 @@ data class FeedLastUpdatedEntity(
var subscriptionId: Long,
@ColumnInfo(name = LAST_UPDATED)
var lastUpdated: Date? = null
var lastUpdated: OffsetDateTime? = null
) {
companion object {
const val FEED_LAST_UPDATED_TABLE = "feed_last_updated"

View File

@@ -6,7 +6,7 @@ import androidx.room.Ignore;
import androidx.room.Index;
import androidx.room.PrimaryKey;
import java.util.Date;
import java.time.OffsetDateTime;
import static org.schabi.newpipe.database.history.model.SearchHistoryEntry.SEARCH;
@@ -24,7 +24,7 @@ public class SearchHistoryEntry {
private long id;
@ColumnInfo(name = CREATION_DATE)
private Date creationDate;
private OffsetDateTime creationDate;
@ColumnInfo(name = SERVICE_ID)
private int serviceId;
@@ -32,7 +32,8 @@ public class SearchHistoryEntry {
@ColumnInfo(name = SEARCH)
private String search;
public SearchHistoryEntry(final Date creationDate, final int serviceId, final String search) {
public SearchHistoryEntry(final OffsetDateTime creationDate, final int serviceId,
final String search) {
this.serviceId = serviceId;
this.creationDate = creationDate;
this.search = search;
@@ -46,11 +47,11 @@ public class SearchHistoryEntry {
this.id = id;
}
public Date getCreationDate() {
public OffsetDateTime getCreationDate() {
return creationDate;
}
public void setCreationDate(final Date creationDate) {
public void setCreationDate(final OffsetDateTime creationDate) {
this.creationDate = creationDate;
}

View File

@@ -9,7 +9,7 @@ import androidx.room.Index;
import org.schabi.newpipe.database.stream.model.StreamEntity;
import java.util.Date;
import java.time.OffsetDateTime;
import static androidx.room.ForeignKey.CASCADE;
import static org.schabi.newpipe.database.history.model.StreamHistoryEntity.JOIN_STREAM_ID;
@@ -37,12 +37,12 @@ public class StreamHistoryEntity {
@NonNull
@ColumnInfo(name = STREAM_ACCESS_DATE)
private Date accessDate;
private OffsetDateTime accessDate;
@ColumnInfo(name = STREAM_REPEAT_COUNT)
private long repeatCount;
public StreamHistoryEntity(final long streamUid, @NonNull final Date accessDate,
public StreamHistoryEntity(final long streamUid, @NonNull final OffsetDateTime accessDate,
final long repeatCount) {
this.streamUid = streamUid;
this.accessDate = accessDate;
@@ -50,7 +50,7 @@ public class StreamHistoryEntity {
}
@Ignore
public StreamHistoryEntity(final long streamUid, @NonNull final Date accessDate) {
public StreamHistoryEntity(final long streamUid, @NonNull final OffsetDateTime accessDate) {
this(streamUid, accessDate, 1);
}
@@ -62,11 +62,12 @@ public class StreamHistoryEntity {
this.streamUid = streamUid;
}
public Date getAccessDate() {
@NonNull
public OffsetDateTime getAccessDate() {
return accessDate;
}
public void setAccessDate(@NonNull final Date accessDate) {
public void setAccessDate(@NonNull final OffsetDateTime accessDate) {
this.accessDate = accessDate;
}

View File

@@ -2,7 +2,7 @@ package org.schabi.newpipe.database.history.model
import androidx.room.ColumnInfo
import androidx.room.Embedded
import java.util.Date
import java.time.OffsetDateTime
import org.schabi.newpipe.database.stream.model.StreamEntity
data class StreamHistoryEntry(
@@ -13,7 +13,7 @@ data class StreamHistoryEntry(
val streamId: Long,
@ColumnInfo(name = StreamHistoryEntity.STREAM_ACCESS_DATE)
val accessDate: Date,
val accessDate: OffsetDateTime,
@ColumnInfo(name = StreamHistoryEntity.STREAM_REPEAT_COUNT)
val repeatCount: Long
@@ -25,6 +25,6 @@ data class StreamHistoryEntry(
fun hasEqualValues(other: StreamHistoryEntry): Boolean {
return this.streamEntity.uid == other.streamEntity.uid && streamId == other.streamId &&
accessDate.compareTo(other.accessDate) == 0
accessDate.isEqual(other.accessDate)
}
}

View File

@@ -5,6 +5,7 @@ import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
public interface PlaylistLocalItem extends LocalItem {
@@ -18,15 +19,8 @@ public interface PlaylistLocalItem extends LocalItem {
items.addAll(localPlaylists);
items.addAll(remotePlaylists);
Collections.sort(items, (left, right) -> {
final String on1 = left.getOrderingName();
final String on2 = right.getOrderingName();
if (on1 == null) {
return on2 == null ? 0 : 1;
} else {
return on2 == null ? -1 : on1.compareToIgnoreCase(on2);
}
});
Collections.sort(items, Comparator.comparing(PlaylistLocalItem::getOrderingName,
Comparator.nullsLast(String.CASE_INSENSITIVE_ORDER)));
return items;
}

View File

@@ -33,4 +33,7 @@ public abstract class PlaylistDAO implements BasicDAO<PlaylistEntity> {
@Query("DELETE FROM " + PLAYLIST_TABLE + " WHERE " + PLAYLIST_ID + " = :playlistId")
public abstract int deletePlaylist(long playlistId);
@Query("SELECT COUNT(*) FROM " + PLAYLIST_TABLE)
public abstract Flowable<Long> getCount();
}

View File

@@ -2,26 +2,25 @@ package org.schabi.newpipe.database.stream
import androidx.room.ColumnInfo
import androidx.room.Embedded
import java.util.Date
import java.time.OffsetDateTime
import org.schabi.newpipe.database.LocalItem
import org.schabi.newpipe.database.history.model.StreamHistoryEntity
import org.schabi.newpipe.database.stream.model.StreamEntity
import org.schabi.newpipe.extractor.stream.StreamInfoItem
class StreamStatisticsEntry(
@Embedded
@Embedded
val streamEntity: StreamEntity,
@ColumnInfo(name = StreamHistoryEntity.JOIN_STREAM_ID)
@ColumnInfo(name = StreamHistoryEntity.JOIN_STREAM_ID)
val streamId: Long,
@ColumnInfo(name = STREAM_LATEST_DATE)
val latestAccessDate: Date,
@ColumnInfo(name = STREAM_LATEST_DATE)
val latestAccessDate: OffsetDateTime,
@ColumnInfo(name = STREAM_WATCH_COUNT)
@ColumnInfo(name = STREAM_WATCH_COUNT)
val watchCount: Long
) : LocalItem {
fun toStreamInfoItem(): StreamInfoItem {
val item = StreamInfoItem(streamEntity.serviceId, streamEntity.url, streamEntity.title, streamEntity.streamType)
item.duration = streamEntity.duration

View File

@@ -7,7 +7,7 @@ import androidx.room.OnConflictStrategy
import androidx.room.Query
import androidx.room.Transaction
import io.reactivex.Flowable
import java.util.Date
import java.time.OffsetDateTime
import org.schabi.newpipe.database.BasicDAO
import org.schabi.newpipe.database.stream.model.StreamEntity
import org.schabi.newpipe.database.stream.model.StreamEntity.Companion.STREAM_ID
@@ -129,7 +129,7 @@ abstract class StreamDAO : BasicDAO<StreamEntity> {
var textualUploadDate: String? = null,
@ColumnInfo(name = StreamEntity.STREAM_UPLOAD_DATE)
var uploadDate: Date? = null,
var uploadDate: OffsetDateTime? = null,
@ColumnInfo(name = StreamEntity.STREAM_IS_UPLOAD_DATE_APPROXIMATION)
var isUploadDateApproximation: Boolean? = null,

View File

@@ -6,8 +6,7 @@ import androidx.room.Ignore
import androidx.room.Index
import androidx.room.PrimaryKey
import java.io.Serializable
import java.util.Calendar
import java.util.Date
import java.time.OffsetDateTime
import org.schabi.newpipe.database.stream.model.StreamEntity.Companion.STREAM_SERVICE_ID
import org.schabi.newpipe.database.stream.model.StreamEntity.Companion.STREAM_TABLE
import org.schabi.newpipe.database.stream.model.StreamEntity.Companion.STREAM_URL
@@ -55,18 +54,17 @@ data class StreamEntity(
var textualUploadDate: String? = null,
@ColumnInfo(name = STREAM_UPLOAD_DATE)
var uploadDate: Date? = null,
var uploadDate: OffsetDateTime? = null,
@ColumnInfo(name = STREAM_IS_UPLOAD_DATE_APPROXIMATION)
var isUploadDateApproximation: Boolean? = null
) : Serializable {
@Ignore
constructor(item: StreamInfoItem) : this(
serviceId = item.serviceId, url = item.url, title = item.name,
streamType = item.streamType, duration = item.duration, uploader = item.uploaderName,
thumbnailUrl = item.thumbnailUrl, viewCount = item.viewCount,
textualUploadDate = item.textualUploadDate, uploadDate = item.uploadDate?.date()?.time,
textualUploadDate = item.textualUploadDate, uploadDate = item.uploadDate?.offsetDateTime(),
isUploadDateApproximation = item.uploadDate?.isApproximation
)
@@ -75,7 +73,7 @@ data class StreamEntity(
serviceId = info.serviceId, url = info.url, title = info.name,
streamType = info.streamType, duration = info.duration, uploader = info.uploaderName,
thumbnailUrl = info.thumbnailUrl, viewCount = info.viewCount,
textualUploadDate = info.textualUploadDate, uploadDate = info.uploadDate?.date()?.time,
textualUploadDate = info.textualUploadDate, uploadDate = info.uploadDate?.offsetDateTime(),
isUploadDateApproximation = info.uploadDate?.isApproximation
)
@@ -95,8 +93,7 @@ data class StreamEntity(
if (viewCount != null) item.viewCount = viewCount as Long
item.textualUploadDate = textualUploadDate
item.uploadDate = uploadDate?.let {
DateWrapper(Calendar.getInstance().apply { time = it }, isUploadDateApproximation
?: false)
DateWrapper(it, isUploadDateApproximation ?: false)
}
return item

View File

@@ -646,7 +646,7 @@ public class DownloadDialog extends DialogFragment
mainStorage = mainStorageVideo; // subtitle & video files go together
format = subtitleStreamsAdapter.getItem(selectedSubtitleIndex).getFormat();
mime = format.mimeType;
filename += format == MediaFormat.TTML ? MediaFormat.SRT.suffix : format.suffix;
filename += (format == MediaFormat.TTML ? MediaFormat.SRT : format).suffix;
break;
default:
throw new RuntimeException("No stream selected");

View File

@@ -80,6 +80,7 @@ import org.schabi.newpipe.fragments.EmptyFragment;
import org.schabi.newpipe.fragments.list.comments.CommentsFragment;
import org.schabi.newpipe.fragments.list.videos.RelatedVideosFragment;
import org.schabi.newpipe.local.dialog.PlaylistAppendDialog;
import org.schabi.newpipe.local.dialog.PlaylistCreationDialog;
import org.schabi.newpipe.local.history.HistoryRecordManager;
import org.schabi.newpipe.player.BasePlayer;
import org.schabi.newpipe.player.MainPlayer;
@@ -109,6 +110,7 @@ import org.schabi.newpipe.views.LargeTextMovementMethod;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
import icepick.State;
@@ -127,7 +129,7 @@ import static org.schabi.newpipe.player.helper.PlayerHelper.isClearingQueueConfi
import static org.schabi.newpipe.player.playqueue.PlayQueueItem.RECOVERY_UNSET;
import static org.schabi.newpipe.util.AnimationUtils.animateView;
public class VideoDetailFragment
public final class VideoDetailFragment
extends BaseStateFragment<StreamInfo>
implements BackPressable,
SharedPreferences.OnSharedPreferenceChangeListener,
@@ -135,7 +137,7 @@ public class VideoDetailFragment
View.OnLongClickListener,
PlayerServiceExtendedEventListener,
OnKeyDownListener {
public static final String AUTO_PLAY = "auto_play";
public static final String KEY_SWITCHING_PLAYERS = "switching_players";
private static final int RELATED_STREAMS_UPDATE_FLAG = 0x1;
private static final int COMMENTS_UPDATE_FLAG = 0x2;
@@ -166,19 +168,23 @@ public class VideoDetailFragment
@State
protected int serviceId = Constants.NO_SERVICE_ID;
@State
protected String name;
@NonNull
protected String title = "";
@State
protected String url;
protected static PlayQueue playQueue;
@Nullable
protected String url = null;
@Nullable
protected PlayQueue playQueue = null;
@State
int bottomSheetState = BottomSheetBehavior.STATE_EXPANDED;
@State
protected boolean autoPlayEnabled = true;
private static StreamInfo currentInfo;
@Nullable
private StreamInfo currentInfo = null;
private Disposable currentWorker;
@NonNull
private CompositeDisposable disposables = new CompositeDisposable();
private final CompositeDisposable disposables = new CompositeDisposable();
@Nullable
private Disposable positionSubscriber = null;
@@ -283,6 +289,7 @@ public class VideoDetailFragment
|| (currentInfo != null
&& isAutoplayEnabled()
&& player.getParentActivity() == null)) {
autoPlayEnabled = true; // forcefully start playing
openVideoPlayer();
}
}
@@ -297,8 +304,10 @@ public class VideoDetailFragment
/*////////////////////////////////////////////////////////////////////////*/
public static VideoDetailFragment getInstance(final int serviceId, final String videoUrl,
final String name, final PlayQueue queue) {
public static VideoDetailFragment getInstance(final int serviceId,
@Nullable final String videoUrl,
@NonNull final String name,
@Nullable final PlayQueue queue) {
final VideoDetailFragment instance = new VideoDetailFragment();
instance.setInitialData(serviceId, videoUrl, name, queue);
return instance;
@@ -443,8 +452,8 @@ public class VideoDetailFragment
switch (requestCode) {
case ReCaptchaActivity.RECAPTCHA_REQUEST:
if (resultCode == Activity.RESULT_OK) {
NavigationHelper
.openVideoDetailFragment(getFM(), serviceId, url, name);
NavigationHelper.openVideoDetailFragment(requireContext(), getFM(),
serviceId, url, title, null, false);
} else {
Log.e(TAG, "ReCaptcha failed");
}
@@ -482,8 +491,14 @@ public class VideoDetailFragment
break;
case R.id.detail_controls_playlist_append:
if (getFM() != null && currentInfo != null) {
PlaylistAppendDialog.fromStreamInfo(currentInfo)
.show(getFM(), TAG);
final PlaylistAppendDialog d = PlaylistAppendDialog.fromStreamInfo(currentInfo);
disposables.add(
PlaylistAppendDialog.onPlaylistFound(getContext(),
() -> d.show(getFM(), TAG),
() -> PlaylistCreationDialog.newInstance(d).show(getFM(), TAG)
)
);
}
break;
case R.id.detail_controls_download:
@@ -507,6 +522,7 @@ public class VideoDetailFragment
}
break;
case R.id.detail_thumbnail_root_layout:
autoPlayEnabled = true; // forcefully start playing
openVideoPlayer();
break;
case R.id.detail_title_root_layout:
@@ -523,6 +539,7 @@ public class VideoDetailFragment
player.hideControls(0, 0);
showSystemUi();
} else {
autoPlayEnabled = true; // forcefully start playing
openVideoPlayer();
}
@@ -784,7 +801,7 @@ public class VideoDetailFragment
player.onPause();
}
restoreDefaultOrientation();
setAutoplay(false);
setAutoPlay(false);
return true;
}
@@ -812,14 +829,11 @@ public class VideoDetailFragment
}
private void setupFromHistoryItem(final StackItem item) {
setAutoplay(false);
setAutoPlay(false);
hideMainPlayer();
setInitialData(
item.getServiceId(),
item.getUrl(),
!TextUtils.isEmpty(item.getTitle()) ? item.getTitle() : "",
item.getPlayQueue());
setInitialData(item.getServiceId(), item.getUrl(),
item.getTitle() == null ? "" : item.getTitle(), item.getPlayQueue());
startLoading(false);
// Maybe an item was deleted in background activity
@@ -853,18 +867,17 @@ public class VideoDetailFragment
}
}
public void selectAndLoadVideo(final int sid, final String videoUrl, final String title,
final PlayQueue queue) {
// Situation when user switches from players to main player.
// All needed data is here, we can start watching
if (this.playQueue != null && this.playQueue.equals(queue)) {
openVideoPlayer();
return;
}
setInitialData(sid, videoUrl, title, queue);
if (player != null) {
public void selectAndLoadVideo(final int newServiceId,
@Nullable final String newUrl,
@NonNull final String newTitle,
@Nullable final PlayQueue newQueue) {
if (player != null && newQueue != null && playQueue != null
&& !Objects.equals(newQueue.getItem(), playQueue.getItem())) {
// Preloading can be disabled since playback is surely being replaced.
player.disablePreloadingOfCurrentTrack();
}
setInitialData(newServiceId, newUrl, newTitle, newQueue);
startLoading(false, true);
}
@@ -949,7 +962,7 @@ public class VideoDetailFragment
playQueue = new SinglePlayQueue(result);
}
if (stack.isEmpty() || !stack.peek().getPlayQueue().equals(playQueue)) {
stack.push(new StackItem(serviceId, url, name, playQueue));
stack.push(new StackItem(serviceId, url, title, playQueue));
}
}
if (isAutoplayEnabled()) {
@@ -970,7 +983,7 @@ public class VideoDetailFragment
if (shouldShowComments()) {
pageAdapter.addFragment(
CommentsFragment.getInstance(serviceId, url, name), COMMENTS_TAB_TAG);
CommentsFragment.getInstance(serviceId, url, title), COMMENTS_TAB_TAG);
}
if (showRelatedStreams && null == relatedStreamsLayout) {
@@ -1061,7 +1074,7 @@ public class VideoDetailFragment
}
}
private void openVideoPlayer() {
public void openVideoPlayer() {
if (PreferenceManager.getDefaultSharedPreferences(activity)
.getBoolean(this.getString(R.string.use_external_video_player_key), false)) {
showExternalPlaybackDialog();
@@ -1087,7 +1100,7 @@ public class VideoDetailFragment
private void openMainPlayer() {
if (playerService == null) {
PlayerHolder.startService(App.getApp(), true, this);
PlayerHolder.startService(App.getApp(), autoPlayEnabled, this);
return;
}
if (currentInfo == null) {
@@ -1098,11 +1111,13 @@ public class VideoDetailFragment
// Video view can have elements visible from popup,
// We hide it here but once it ready the view will be shown in handleIntent()
playerService.getView().setVisibility(View.GONE);
if (playerService.getView() != null) {
playerService.getView().setVisibility(View.GONE);
}
addVideoPlayerView();
final Intent playerIntent = NavigationHelper
.getPlayerIntent(requireContext(), MainPlayer.class, queue, null, true);
.getPlayerIntent(requireContext(), MainPlayer.class, queue, true, autoPlayEnabled);
activity.startService(playerIntent);
}
@@ -1136,8 +1151,8 @@ public class VideoDetailFragment
// Utils
//////////////////////////////////////////////////////////////////////////*/
public void setAutoplay(final boolean autoplay) {
this.autoPlayEnabled = autoplay;
public void setAutoPlay(final boolean autoPlay) {
this.autoPlayEnabled = autoPlay;
}
private void startOnExternalPlayer(@NonNull final Context context,
@@ -1159,7 +1174,7 @@ public class VideoDetailFragment
.getBoolean(getString(R.string.use_external_video_player_key), false);
}
// This method overrides default behaviour when setAutoplay() is called.
// This method overrides default behaviour when setAutoPlay() is called.
// Don't auto play if the user selected an external player or disabled it in settings
private boolean isAutoplayEnabled() {
return autoPlayEnabled
@@ -1239,9 +1254,9 @@ public class VideoDetailFragment
final DisplayMetrics metrics = getResources().getDisplayMetrics();
if (getView() != null) {
final int height = isInMultiWindow()
? requireView().getHeight()
: activity.getWindow().getDecorView().getHeight();
final int height = (isInMultiWindow()
? requireView()
: activity.getWindow().getDecorView()).getHeight();
setHeightThumbnail(height, metrics);
getView().getViewTreeObserver().removeOnPreDrawListener(preDrawListener);
}
@@ -1262,9 +1277,9 @@ public class VideoDetailFragment
requireView().getViewTreeObserver().removeOnPreDrawListener(preDrawListener);
if (player != null && player.isFullscreen()) {
final int height = isInMultiWindow()
? requireView().getHeight()
: activity.getWindow().getDecorView().getHeight();
final int height = (isInMultiWindow()
? requireView()
: activity.getWindow().getDecorView()).getHeight();
// Height is zero when the view is not yet displayed like after orientation change
if (height != 0) {
setHeightThumbnail(height, metrics);
@@ -1272,9 +1287,9 @@ public class VideoDetailFragment
requireView().getViewTreeObserver().addOnPreDrawListener(preDrawListener);
}
} else {
final int height = isPortrait
? (int) (metrics.widthPixels / (16.0f / 9.0f))
: (int) (metrics.heightPixels / 2.0f);
final int height = (int) (isPortrait
? metrics.widthPixels / (16.0f / 9.0f)
: metrics.heightPixels / 2.0f);
setHeightThumbnail(height, metrics);
}
}
@@ -1295,12 +1310,14 @@ public class VideoDetailFragment
contentRootLayoutHiding.setVisibility(View.VISIBLE);
}
protected void setInitialData(final int sid, final String u, final String title,
final PlayQueue queue) {
this.serviceId = sid;
this.url = u;
this.name = !TextUtils.isEmpty(title) ? title : "";
this.playQueue = queue;
protected void setInitialData(final int newServiceId,
@Nullable final String newUrl,
@NonNull final String newTitle,
@Nullable final PlayQueue newPlayQueue) {
this.serviceId = newServiceId;
this.url = newUrl;
this.title = newTitle;
this.playQueue = newPlayQueue;
}
private void setErrorImage(final int imageResource) {
@@ -1393,7 +1410,7 @@ public class VideoDetailFragment
animateView(detailPositionView, false, 100);
animateView(positionView, false, 50);
videoTitleTextView.setText(name != null ? name : "");
videoTitleTextView.setText(title);
videoTitleTextView.setMaxLines(1);
animateView(videoTitleTextView, true, 0);
@@ -1438,7 +1455,7 @@ public class VideoDetailFragment
}
}
animateView(thumbnailPlayButton, true, 200);
videoTitleTextView.setText(name);
videoTitleTextView.setText(title);
if (!TextUtils.isEmpty(info.getSubChannelName())) {
displayBothUploaderAndSubChannel(info);
@@ -1520,7 +1537,7 @@ public class VideoDetailFragment
if (info.getUploadDate() != null) {
videoUploadDateView.setText(Localization
.localizeUploadDate(activity, info.getUploadDate().date().getTime()));
.localizeUploadDate(activity, info.getUploadDate().offsetDateTime()));
videoUploadDateView.setVisibility(View.VISIBLE);
} else {
videoUploadDateView.setText(null);
@@ -1563,7 +1580,8 @@ public class VideoDetailFragment
}
private void hideAgeRestrictedContent() {
showError(getString(R.string.restricted_video), false);
showError(getString(R.string.restricted_video,
getString(R.string.show_age_restricted_content_title)), false);
if (relatedStreamsLayout != null) { // tablet
relatedStreamsLayout.setVisibility(View.INVISIBLE);
@@ -1633,8 +1651,8 @@ public class VideoDetailFragment
return true;
}
final int errorId = exception instanceof YoutubeStreamExtractor.DecryptException
? R.string.youtube_signature_decryption_error
final int errorId = exception instanceof YoutubeStreamExtractor.DeobfuscateException
? R.string.youtube_signature_deobfuscation_error
: exception instanceof ExtractionException
? R.string.parsing_error
: R.string.general_error;
@@ -1727,31 +1745,33 @@ public class VideoDetailFragment
@Override
public void onQueueUpdate(final PlayQueue queue) {
playQueue = queue;
if (DEBUG) {
Log.d(TAG, "onQueueUpdate() called with: serviceId = ["
+ serviceId + "], videoUrl = [" + url + "], name = ["
+ title + "], playQueue = [" + playQueue + "]");
}
// This should be the only place where we push data to stack.
// It will allow to have live instance of PlayQueue with actual information about
// deleted/added items inside Channel/Playlist queue and makes possible to have
// a history of played items
if ((stack.isEmpty() || !stack.peek().getPlayQueue().equals(queue)
&& queue.getItem() != null)) {
stack.push(new StackItem(queue.getItem().getServiceId(),
queue.getItem().getUrl(),
queue.getItem().getTitle(),
queue));
} else {
final StackItem stackWithQueue = findQueueInStack(queue);
if (stackWithQueue != null) {
// On every MainPlayer service's destroy() playQueue gets disposed and
// no longer able to track progress. That's why we update our cached disposed
// queue with the new one that is active and have the same history.
// Without that the cached playQueue will have an old recovery position
stackWithQueue.setPlayQueue(queue);
}
@Nullable final StackItem stackPeek = stack.peek();
if (stackPeek != null && !stackPeek.getPlayQueue().equals(queue)) {
@Nullable final PlayQueueItem playQueueItem = queue.getItem();
if (playQueueItem != null) {
stack.push(new StackItem(playQueueItem.getServiceId(), playQueueItem.getUrl(),
playQueueItem.getTitle(), queue));
return;
} // else continue below
}
if (DEBUG) {
Log.d(TAG, "onQueueUpdate() called with: serviceId = ["
+ serviceId + "], videoUrl = [" + url + "], name = ["
+ name + "], playQueue = [" + playQueue + "]");
@Nullable final StackItem stackWithQueue = findQueueInStack(queue);
if (stackWithQueue != null) {
// On every MainPlayer service's destroy() playQueue gets disposed and
// no longer able to track progress. That's why we update our cached disposed
// queue with the new one that is active and have the same history.
// Without that the cached playQueue will have an old recovery position
stackWithQueue.setPlayQueue(queue);
}
}
@@ -1813,7 +1833,7 @@ public class VideoDetailFragment
currentInfo = info;
setInitialData(info.getServiceId(), info.getUrl(), info.getName(), queue);
setAutoplay(false);
setAutoPlay(false);
// Delay execution just because it freezes the main thread, and while playing
// next/previous video you see visual glitches
// (when non-vertical video goes after vertical video)
@@ -2027,7 +2047,7 @@ public class VideoDetailFragment
private void checkLandscape() {
if ((!player.isPlaying() && player.getPlayQueue() != playQueue)
|| player.getPlayQueue() == null) {
setAutoplay(true);
setAutoPlay(true);
}
player.checkLandscape();
@@ -2054,6 +2074,7 @@ public class VideoDetailFragment
return url == null;
}
@Nullable
private StackItem findQueueInStack(final PlayQueue queue) {
StackItem item = null;
final Iterator<StackItem> iterator = stack.descendingIterator();
@@ -2074,8 +2095,7 @@ public class VideoDetailFragment
if (isClearingQueueConfirmationRequired(activity)
&& playerIsNotStopped()
&& activeQueue != null
&& !activeQueue.equals(playQueue)
&& activeQueue.getStreams().size() > 1) {
&& !activeQueue.equals(playQueue)) {
showClearingQueueConfirmation(onAllow);
} else {
onAllow.run();
@@ -2277,10 +2297,10 @@ public class VideoDetailFragment
});
}
private void updateOverlayData(@Nullable final String title,
private void updateOverlayData(@Nullable final String overlayTitle,
@Nullable final String uploader,
@Nullable final String thumbnailUrl) {
overlayTitleTextView.setText(TextUtils.isEmpty(title) ? "" : title);
overlayTitleTextView.setText(TextUtils.isEmpty(overlayTitle) ? "" : overlayTitle);
overlayChannelTextView.setText(TextUtils.isEmpty(uploader) ? "" : uploader);
overlayThumbnailImageView.setImageResource(R.drawable.dummy_thumbnail_dark);
if (!TextUtils.isEmpty(thumbnailUrl)) {

View File

@@ -6,7 +6,6 @@ import android.content.SharedPreferences;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.os.Bundle;
import androidx.preference.PreferenceManager;
import android.util.Log;
import android.view.Menu;
import android.view.MenuInflater;
@@ -15,6 +14,7 @@ import android.view.View;
import androidx.annotation.NonNull;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AppCompatActivity;
import androidx.preference.PreferenceManager;
import androidx.recyclerview.widget.GridLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
@@ -29,6 +29,7 @@ import org.schabi.newpipe.fragments.BaseStateFragment;
import org.schabi.newpipe.fragments.OnScrollBelowItemsListener;
import org.schabi.newpipe.info_list.InfoItemDialog;
import org.schabi.newpipe.info_list.InfoListAdapter;
import org.schabi.newpipe.player.helper.PlayerHolder;
import org.schabi.newpipe.report.ErrorActivity;
import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.OnClickGesture;
@@ -36,6 +37,8 @@ import org.schabi.newpipe.util.StateSaver;
import org.schabi.newpipe.util.StreamDialogEntry;
import org.schabi.newpipe.views.SuperScrollLayoutManager;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Queue;
@@ -318,8 +321,9 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I>
private void onStreamSelected(final StreamInfoItem selectedItem) {
onItemSelected(selectedItem);
NavigationHelper.openVideoDetailFragment(getFM(),
selectedItem.getServiceId(), selectedItem.getUrl(), selectedItem.getName());
NavigationHelper.openVideoDetailFragment(requireContext(), getFM(),
selectedItem.getServiceId(), selectedItem.getUrl(), selectedItem.getName(),
null, false);
}
protected void onScrollToBottom() {
@@ -336,21 +340,26 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I>
return;
}
final ArrayList<StreamDialogEntry> entries = new ArrayList<>();
if (PlayerHolder.getType() != null) {
entries.add(StreamDialogEntry.enqueue);
}
if (item.getStreamType() == StreamType.AUDIO_STREAM) {
StreamDialogEntry.setEnabledEntries(
StreamDialogEntry.enqueue_on_background,
entries.addAll(Arrays.asList(
StreamDialogEntry.start_here_on_background,
StreamDialogEntry.append_playlist,
StreamDialogEntry.share);
} else {
StreamDialogEntry.setEnabledEntries(
StreamDialogEntry.enqueue_on_background,
StreamDialogEntry.enqueue_on_popup,
StreamDialogEntry.share
));
} else {
entries.addAll(Arrays.asList(
StreamDialogEntry.start_here_on_background,
StreamDialogEntry.start_here_on_popup,
StreamDialogEntry.append_playlist,
StreamDialogEntry.share);
StreamDialogEntry.share
));
}
StreamDialogEntry.setEnabledEntries(entries);
new InfoItemDialog(activity, item, StreamDialogEntry.getCommands(context),
(dialog, which) -> StreamDialogEntry.clickOn(which, this, item)).show();
@@ -370,11 +379,7 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I>
final ActionBar supportActionBar = activity.getSupportActionBar();
if (supportActionBar != null) {
supportActionBar.setDisplayShowTitleEnabled(true);
if (useAsFrontPage) {
supportActionBar.setDisplayHomeAsUpEnabled(false);
} else {
supportActionBar.setDisplayHomeAsUpEnabled(true);
}
supportActionBar.setDisplayHomeAsUpEnabled(!useAsFrontPage);
}
}

View File

@@ -50,7 +50,6 @@ import org.schabi.newpipe.util.ShareUtils;
import org.schabi.newpipe.util.ThemeHelper;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.TimeUnit;
@@ -495,13 +494,12 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo>
// 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
for (Iterator<Throwable> it = errors.iterator(); it.hasNext();) {
final Throwable throwable = it.next();
errors.removeIf(throwable -> {
if (throwable instanceof ContentNotSupportedException) {
showContentNotSupported();
it.remove();
}
}
return throwable instanceof ContentNotSupportedException;
});
if (!errors.isEmpty()) {
showSnackBarError(errors, UserAction.REQUESTED_CHANNEL,
@@ -519,7 +517,7 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo>
monitorSubscription(result);
headerPlayAllButton.setOnClickListener(view -> NavigationHelper
.playOnMainPlayer(activity, getPlayQueue(), true));
.playOnMainPlayer(activity, getPlayQueue()));
headerPopupButton.setOnClickListener(view -> NavigationHelper
.playOnPopupPlayer(activity, getPlayQueue(), false));
headerBackgroundButton.setOnClickListener(view -> NavigationHelper

View File

@@ -33,6 +33,7 @@ import org.schabi.newpipe.extractor.stream.StreamType;
import org.schabi.newpipe.fragments.list.BaseListInfoFragment;
import org.schabi.newpipe.info_list.InfoItemDialog;
import org.schabi.newpipe.local.playlist.RemotePlaylistManager;
import org.schabi.newpipe.player.helper.PlayerHolder;
import org.schabi.newpipe.player.playqueue.PlayQueue;
import org.schabi.newpipe.player.playqueue.PlaylistPlayQueue;
import org.schabi.newpipe.report.ErrorActivity;
@@ -46,6 +47,7 @@ import org.schabi.newpipe.util.StreamDialogEntry;
import org.schabi.newpipe.util.ThemeHelper;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
@@ -151,25 +153,26 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
return;
}
final ArrayList<StreamDialogEntry> entries = new ArrayList<>();
if (PlayerHolder.getType() != null) {
entries.add(StreamDialogEntry.enqueue);
}
if (item.getStreamType() == StreamType.AUDIO_STREAM) {
StreamDialogEntry.setEnabledEntries(
StreamDialogEntry.enqueue_on_background,
entries.addAll(Arrays.asList(
StreamDialogEntry.start_here_on_background,
StreamDialogEntry.append_playlist,
StreamDialogEntry.share);
} else {
StreamDialogEntry.setEnabledEntries(
StreamDialogEntry.enqueue_on_background,
StreamDialogEntry.enqueue_on_popup,
StreamDialogEntry.share
));
} else {
entries.addAll(Arrays.asList(
StreamDialogEntry.start_here_on_background,
StreamDialogEntry.start_here_on_popup,
StreamDialogEntry.append_playlist,
StreamDialogEntry.share);
StreamDialogEntry.start_here_on_popup.setCustomAction((fragment, infoItem) ->
NavigationHelper.playOnPopupPlayer(context,
getPlayQueueStartingAt(infoItem), true));
StreamDialogEntry.share
));
}
StreamDialogEntry.setEnabledEntries(entries);
StreamDialogEntry.start_here_on_background.setCustomAction((fragment, infoItem) ->
NavigationHelper.playOnBackgroundPlayer(context,
@@ -316,7 +319,7 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
.subscribe(getPlaylistBookmarkSubscriber());
headerPlayAllButton.setOnClickListener(view ->
NavigationHelper.playOnMainPlayer(activity, getPlayQueue(), true));
NavigationHelper.playOnMainPlayer(activity, getPlayQueue()));
headerPopupButton.setOnClickListener(view ->
NavigationHelper.playOnPopupPlayer(activity, getPlayQueue(), false));
headerBackgroundButton.setOnClickListener(view ->

View File

@@ -5,8 +5,6 @@ import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Bundle;
import androidx.core.text.HtmlCompat;
import androidx.preference.PreferenceManager;
import android.text.Editable;
import android.text.Html;
import android.text.TextUtils;
@@ -30,6 +28,9 @@ 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;
@@ -49,9 +50,9 @@ import org.schabi.newpipe.fragments.list.BaseListFragment;
import org.schabi.newpipe.local.history.HistoryRecordManager;
import org.schabi.newpipe.report.ErrorActivity;
import org.schabi.newpipe.report.UserAction;
import org.schabi.newpipe.util.DeviceUtils;
import org.schabi.newpipe.util.AnimationUtils;
import org.schabi.newpipe.util.Constants;
import org.schabi.newpipe.util.DeviceUtils;
import org.schabi.newpipe.util.ExceptionUtils;
import org.schabi.newpipe.util.ExtractorHelper;
import org.schabi.newpipe.util.NavigationHelper;
@@ -60,7 +61,6 @@ import org.schabi.newpipe.util.ServiceHelper;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Queue;
@@ -639,8 +639,8 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
}
if (searchEditText.requestFocus()) {
final InputMethodManager imm = (InputMethodManager) activity.getSystemService(
Context.INPUT_METHOD_SERVICE);
final InputMethodManager imm = ContextCompat.getSystemService(activity,
InputMethodManager.class);
imm.showSoftInput(searchEditText, InputMethodManager.SHOW_FORCED);
}
}
@@ -653,8 +653,8 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
return;
}
final InputMethodManager imm = (InputMethodManager) activity
.getSystemService(Context.INPUT_METHOD_SERVICE);
final InputMethodManager imm = ContextCompat.getSystemService(activity,
InputMethodManager.class);
imm.hideSoftInputFromWindow(searchEditText.getWindowToken(),
InputMethodManager.RESULT_UNCHANGED_SHOWN);
@@ -757,16 +757,9 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
}
// Remove duplicates
final Iterator<SuggestionItem> iterator = networkResult.iterator();
while (iterator.hasNext() && localResult.size() > 0) {
final SuggestionItem next = iterator.next();
for (final SuggestionItem item : localResult) {
if (item.query.equals(next.query)) {
iterator.remove();
break;
}
}
}
networkResult.removeIf(networkItem ->
localResult.stream().anyMatch(localItem ->
localItem.query.equals(networkItem.query)));
if (networkResult.size() > 0) {
result.addAll(networkResult);

View File

@@ -0,0 +1,10 @@
package org.schabi.newpipe.ktx
import java.time.OffsetDateTime
import java.time.ZoneId
import java.util.Calendar
import java.util.GregorianCalendar
fun OffsetDateTime.toCalendar(zoneId: ZoneId = ZoneId.systemDefault()): Calendar {
return GregorianCalendar.from(if (zoneId != offset) atZoneSameInstant(zoneId) else toZonedDateTime())
}

View File

@@ -26,7 +26,8 @@ import org.schabi.newpipe.util.FallbackViewHolder;
import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.OnClickGesture;
import java.text.DateFormat;
import java.time.format.DateTimeFormatter;
import java.time.format.FormatStyle;
import java.util.ArrayList;
import java.util.List;
@@ -69,7 +70,7 @@ public class LocalItemListAdapter extends RecyclerView.Adapter<RecyclerView.View
private final LocalItemBuilder localItemBuilder;
private final ArrayList<LocalItem> localItems;
private final HistoryRecordManager recordManager;
private final DateFormat dateFormat;
private final DateTimeFormatter dateTimeFormatter;
private boolean showFooter = false;
private boolean useGridVariant = false;
@@ -80,8 +81,8 @@ public class LocalItemListAdapter extends RecyclerView.Adapter<RecyclerView.View
recordManager = new HistoryRecordManager(context);
localItemBuilder = new LocalItemBuilder(context);
localItems = new ArrayList<>();
dateFormat = DateFormat.getDateInstance(DateFormat.SHORT,
Localization.getPreferredLocale(context));
dateTimeFormatter = DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT)
.withLocale(Localization.getPreferredLocale(context));
}
public void setSelectedListener(final OnClickGesture<LocalItem> listener) {
@@ -303,7 +304,7 @@ public class LocalItemListAdapter extends RecyclerView.Adapter<RecyclerView.View
}
((LocalItemHolder) holder)
.updateFromItem(localItems.get(position), recordManager, dateFormat);
.updateFromItem(localItems.get(position), recordManager, dateTimeFormatter);
} else if (holder instanceof HeaderFooterHolder && position == 0 && header != null) {
((HeaderFooterHolder) holder).view = header;
} else if (holder instanceof HeaderFooterHolder && position == sizeConsideringHeader()

View File

@@ -1,5 +1,6 @@
package org.schabi.newpipe.local.dialog;
import android.content.Context;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
@@ -29,6 +30,7 @@ import java.util.List;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.CompositeDisposable;
import io.reactivex.disposables.Disposable;
public final class PlaylistAppendDialog extends PlaylistDialog {
private static final String TAG = PlaylistAppendDialog.class.getCanonicalName();
@@ -38,6 +40,23 @@ public final class PlaylistAppendDialog extends PlaylistDialog {
private 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)));
@@ -136,11 +155,6 @@ public final class PlaylistAppendDialog extends PlaylistDialog {
}
private void onPlaylistsReceived(@NonNull final List<PlaylistMetadataEntry> playlists) {
if (playlists.isEmpty()) {
openCreatePlaylistDialog();
return;
}
if (playlistAdapter != null && playlistRecyclerView != null) {
playlistAdapter.clearStreamItemList();
playlistAdapter.addItems(playlists);

View File

@@ -26,6 +26,12 @@ public final class PlaylistCreationDialog extends PlaylistDialog {
return dialog;
}
public static PlaylistCreationDialog newInstance(final PlaylistAppendDialog appendDialog) {
final PlaylistCreationDialog dialog = new PlaylistCreationDialog();
dialog.setInfo(appendDialog.getStreams());
return dialog;
}
/*//////////////////////////////////////////////////////////////////////////
// Dialog
//////////////////////////////////////////////////////////////////////////*/

View File

@@ -7,8 +7,9 @@ import io.reactivex.Flowable
import io.reactivex.Maybe
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.schedulers.Schedulers
import java.util.Calendar
import java.util.Date
import java.time.LocalDate
import java.time.OffsetDateTime
import java.time.ZoneOffset
import org.schabi.newpipe.MainActivity.DEBUG
import org.schabi.newpipe.NewPipeDatabase
import org.schabi.newpipe.database.feed.model.FeedEntity
@@ -29,13 +30,8 @@ class FeedDatabaseManager(context: Context) {
/**
* Only items that are newer than this will be saved.
*/
val FEED_OLDEST_ALLOWED_DATE: Calendar = Calendar.getInstance().apply {
add(Calendar.WEEK_OF_YEAR, -13)
set(Calendar.HOUR_OF_DAY, 0)
set(Calendar.MINUTE, 0)
set(Calendar.SECOND, 0)
set(Calendar.MILLISECOND, 0)
}
val FEED_OLDEST_ALLOWED_DATE: OffsetDateTime = LocalDate.now().minusWeeks(13)
.atStartOfDay().atOffset(ZoneOffset.UTC)
}
fun groups() = feedGroupTable.getAll()
@@ -50,12 +46,12 @@ class FeedDatabaseManager(context: Context) {
return streams.map<List<StreamInfoItem>> {
val items = ArrayList<StreamInfoItem>(it.size)
for (streamEntity in it) items.add(streamEntity.toStreamInfoItem())
it.mapTo(items) { it.toStreamInfoItem() }
return@map items
}
}
fun outdatedSubscriptions(outdatedThreshold: Date) = feedTable.getAllOutdated(outdatedThreshold)
fun outdatedSubscriptions(outdatedThreshold: OffsetDateTime) = feedTable.getAllOutdated(outdatedThreshold)
fun notLoadedCount(groupId: Long = FeedGroupEntity.GROUP_ALL_ID): Flowable<Long> {
return when (groupId) {
@@ -64,7 +60,7 @@ class FeedDatabaseManager(context: Context) {
}
}
fun outdatedSubscriptionsForGroup(groupId: Long = FeedGroupEntity.GROUP_ALL_ID, outdatedThreshold: Date) =
fun outdatedSubscriptionsForGroup(groupId: Long = FeedGroupEntity.GROUP_ALL_ID, outdatedThreshold: OffsetDateTime) =
feedTable.getAllOutdatedForGroup(groupId, outdatedThreshold)
fun markAsOutdated(subscriptionId: Long) = feedTable
@@ -73,7 +69,7 @@ class FeedDatabaseManager(context: Context) {
fun upsertAll(
subscriptionId: Long,
items: List<StreamInfoItem>,
oldestAllowedDate: Date = FEED_OLDEST_ALLOWED_DATE.time
oldestAllowedDate: OffsetDateTime = FEED_OLDEST_ALLOWED_DATE
) {
val itemsToInsert = ArrayList<StreamInfoItem>()
loop@ for (streamItem in items) {
@@ -81,7 +77,7 @@ class FeedDatabaseManager(context: Context) {
itemsToInsert += when {
uploadDate == null && streamItem.streamType == StreamType.LIVE_STREAM -> streamItem
uploadDate != null && uploadDate.date().time >= oldestAllowedDate -> streamItem
uploadDate != null && uploadDate.offsetDateTime() >= oldestAllowedDate -> streamItem
else -> continue@loop
}
}
@@ -96,10 +92,11 @@ class FeedDatabaseManager(context: Context) {
feedTable.insertAll(feedEntities)
}
feedTable.setLastUpdatedForSubscription(FeedLastUpdatedEntity(subscriptionId, Calendar.getInstance().time))
feedTable.setLastUpdatedForSubscription(FeedLastUpdatedEntity(subscriptionId,
OffsetDateTime.now(ZoneOffset.UTC)))
}
fun removeOrphansOrOlderStreams(oldestAllowedDate: Date = FEED_OLDEST_ALLOWED_DATE.time) {
fun removeOrphansOrOlderStreams(oldestAllowedDate: OffsetDateTime = FEED_OLDEST_ALLOWED_DATE) {
feedTable.unlinkStreamsOlderThan(oldestAllowedDate)
streamTable.deleteOrphans()
}
@@ -159,7 +156,7 @@ class FeedDatabaseManager(context: Context) {
.observeOn(AndroidSchedulers.mainThread())
}
fun oldestSubscriptionUpdate(groupId: Long): Flowable<List<Date>> {
fun oldestSubscriptionUpdate(groupId: Long): Flowable<List<OffsetDateTime>> {
return when (groupId) {
FeedGroupEntity.GROUP_ALL_ID -> feedTable.oldestSubscriptionUpdateFromAll()
else -> feedTable.oldestSubscriptionUpdate(groupId)

View File

@@ -29,6 +29,8 @@ import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.app.AlertDialog
import androidx.core.os.bundleOf
import androidx.core.view.isVisible
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider
import androidx.preference.PreferenceManager
@@ -253,11 +255,9 @@ class FeedFragment : BaseListFragment<FeedState, Unit>() {
oldestSubscriptionUpdate = loadedState.oldestUpdate
refresh_subtitle_text.isVisible = loadedState.notLoadedCount > 0
if (loadedState.notLoadedCount > 0) {
refresh_subtitle_text.visibility = View.VISIBLE
refresh_subtitle_text.text = getString(R.string.feed_subscription_not_loaded_count, loadedState.notLoadedCount)
} else {
refresh_subtitle_text.visibility = View.GONE
}
if (loadedState.itemsErrors.isNotEmpty()) {
@@ -330,12 +330,7 @@ class FeedFragment : BaseListFragment<FeedState, Unit>() {
@JvmStatic
fun newInstance(groupId: Long = FeedGroupEntity.GROUP_ALL_ID, groupName: String? = null): FeedFragment {
val feedFragment = FeedFragment()
feedFragment.arguments = Bundle().apply {
putLong(KEY_GROUP_ID, groupId)
putString(KEY_GROUP_NAME, groupName)
}
feedFragment.arguments = bundleOf(KEY_GROUP_ID to groupId, KEY_GROUP_NAME to groupName)
return feedFragment
}
}

View File

@@ -9,11 +9,11 @@ import io.reactivex.Flowable
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.functions.Function4
import io.reactivex.schedulers.Schedulers
import java.util.Calendar
import java.util.Date
import java.time.OffsetDateTime
import java.util.concurrent.TimeUnit
import org.schabi.newpipe.database.feed.model.FeedGroupEntity
import org.schabi.newpipe.extractor.stream.StreamInfoItem
import org.schabi.newpipe.ktx.toCalendar
import org.schabi.newpipe.local.feed.service.FeedEventManager
import org.schabi.newpipe.local.feed.service.FeedEventManager.Event.ErrorResultEvent
import org.schabi.newpipe.local.feed.service.FeedEventManager.Event.IdleEvent
@@ -41,7 +41,7 @@ class FeedViewModel(applicationContext: Context, val groupId: Long = FeedGroupEn
feedDatabaseManager.notLoadedCount(groupId),
feedDatabaseManager.oldestSubscriptionUpdate(groupId),
Function4 { t1: FeedEventManager.Event, t2: List<StreamInfoItem>, t3: Long, t4: List<Date> ->
Function4 { t1: FeedEventManager.Event, t2: List<StreamInfoItem>, t3: Long, t4: List<OffsetDateTime> ->
return@Function4 CombineResultHolder(t1, t2, t3, t4.firstOrNull())
}
)
@@ -51,8 +51,7 @@ class FeedViewModel(applicationContext: Context, val groupId: Long = FeedGroupEn
.subscribe {
val (event, listFromDB, notLoadedCount, oldestUpdate) = it
val oldestUpdateCalendar =
oldestUpdate?.let { Calendar.getInstance().apply { time = it } }
val oldestUpdateCalendar = oldestUpdate?.toCalendar()
mutableStateLiveData.postValue(when (event) {
is IdleEvent -> FeedState.LoadedState(listFromDB, oldestUpdateCalendar, notLoadedCount)
@@ -71,5 +70,5 @@ class FeedViewModel(applicationContext: Context, val groupId: Long = FeedGroupEn
combineDisposable.dispose()
}
private data class CombineResultHolder(val t1: FeedEventManager.Event, val t2: List<StreamInfoItem>, val t3: Long, val t4: Date?)
private data class CombineResultHolder(val t1: FeedEventManager.Event, val t2: List<StreamInfoItem>, val t3: Long, val t4: OffsetDateTime?)
}

View File

@@ -41,7 +41,8 @@ import io.reactivex.functions.Function
import io.reactivex.processors.PublishProcessor
import io.reactivex.schedulers.Schedulers
import java.io.IOException
import java.util.Calendar
import java.time.OffsetDateTime
import java.time.ZoneOffset
import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicInteger
@@ -161,8 +162,8 @@ class FeedLoadService : Service() {
companion object {
fun wrapList(subscriptionId: Long, info: ListInfo<StreamInfoItem>): List<Throwable> {
val toReturn = ArrayList<Throwable>(info.errors.size)
for (error in info.errors) {
toReturn.add(RequestException(subscriptionId, info.serviceId.toString() + ":" + info.url, error))
info.errors.mapTo(toReturn) {
RequestException(subscriptionId, info.serviceId.toString() + ":" + info.url, it)
}
return toReturn
}
@@ -172,9 +173,7 @@ class FeedLoadService : Service() {
private fun startLoading(groupId: Long = FeedGroupEntity.GROUP_ALL_ID, useFeedExtractor: Boolean, thresholdOutdatedSeconds: Int) {
feedResultsHolder = ResultsHolder()
val outdatedThreshold = Calendar.getInstance().apply {
add(Calendar.SECOND, -thresholdOutdatedSeconds)
}.time
val outdatedThreshold = OffsetDateTime.now(ZoneOffset.UTC).minusSeconds(thresholdOutdatedSeconds.toLong())
val subscriptions = when (groupId) {
FeedGroupEntity.GROUP_ALL_ID -> feedDatabaseManager.outdatedSubscriptions(outdatedThreshold)

View File

@@ -20,9 +20,9 @@ package org.schabi.newpipe.local.history;
import android.content.Context;
import android.content.SharedPreferences;
import androidx.preference.PreferenceManager;
import androidx.annotation.NonNull;
import androidx.preference.PreferenceManager;
import org.schabi.newpipe.NewPipeDatabase;
import org.schabi.newpipe.R;
@@ -44,9 +44,10 @@ import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.player.playqueue.PlayQueueItem;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.List;
import io.reactivex.Completable;
@@ -85,7 +86,7 @@ public class HistoryRecordManager {
return Maybe.empty();
}
final Date currentTime = new Date();
final OffsetDateTime currentTime = OffsetDateTime.now(ZoneOffset.UTC);
return Maybe.fromCallable(() -> database.runInTransaction(() -> {
final long streamId = streamTable.upsert(new StreamEntity(info));
final StreamHistoryEntity latestEntry = streamHistoryTable.getLatestEntry(streamId);
@@ -101,9 +102,11 @@ public class HistoryRecordManager {
})).subscribeOn(Schedulers.io());
}
public Single<Integer> deleteStreamHistory(final long streamId) {
return Single.fromCallable(() -> streamHistoryTable.deleteStreamHistory(streamId))
.subscribeOn(Schedulers.io());
public Completable deleteStreamHistoryAndState(final long streamId) {
return Completable.fromAction(() -> {
streamStateTable.deleteState(streamId);
streamHistoryTable.deleteStreamHistory(streamId);
}).subscribeOn(Schedulers.io());
}
public Single<Integer> deleteWholeStreamHistory() {
@@ -111,7 +114,7 @@ public class HistoryRecordManager {
.subscribeOn(Schedulers.io());
}
public Single<Integer> deleteCompelteStreamStateHistory() {
public Single<Integer> deleteCompleteStreamStateHistory() {
return Single.fromCallable(streamStateTable::deleteAll)
.subscribeOn(Schedulers.io());
}
@@ -159,7 +162,7 @@ public class HistoryRecordManager {
return Maybe.empty();
}
final Date currentTime = new Date();
final OffsetDateTime currentTime = OffsetDateTime.now(ZoneOffset.UTC);
final SearchHistoryEntry newEntry = new SearchHistoryEntry(currentTime, serviceId, search);
return Maybe.fromCallable(() -> database.runInTransaction(() -> {

View File

@@ -25,10 +25,12 @@ import org.reactivestreams.Subscription;
import org.schabi.newpipe.R;
import org.schabi.newpipe.database.LocalItem;
import org.schabi.newpipe.database.stream.StreamStatisticsEntry;
import org.schabi.newpipe.database.stream.model.StreamEntity;
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
import org.schabi.newpipe.extractor.stream.StreamType;
import org.schabi.newpipe.info_list.InfoItemDialog;
import org.schabi.newpipe.local.BaseLocalListFragment;
import org.schabi.newpipe.player.helper.PlayerHolder;
import org.schabi.newpipe.player.playqueue.PlayQueue;
import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
import org.schabi.newpipe.report.ErrorActivity;
@@ -40,7 +42,9 @@ import org.schabi.newpipe.util.StreamDialogEntry;
import org.schabi.newpipe.util.ThemeHelper;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import icepick.State;
@@ -66,18 +70,19 @@ public class StatisticsPlaylistFragment
private HistoryRecordManager recordManager;
private List<StreamStatisticsEntry> processResult(final List<StreamStatisticsEntry> results) {
final Comparator<StreamStatisticsEntry> comparator;
switch (sortMode) {
case LAST_PLAYED:
Collections.sort(results, (left, right) ->
right.getLatestAccessDate().compareTo(left.getLatestAccessDate()));
return results;
comparator = Comparator.comparing(StreamStatisticsEntry::getLatestAccessDate);
break;
case MOST_PLAYED:
Collections.sort(results, (left, right) ->
Long.compare(right.getWatchCount(), left.getWatchCount()));
return results;
comparator = Comparator.comparingLong(StreamStatisticsEntry::getWatchCount);
break;
default:
return null;
}
Collections.sort(results, comparator.reversed());
return results;
}
///////////////////////////////////////////////////////////////////////////
@@ -145,11 +150,10 @@ public class StatisticsPlaylistFragment
@Override
public void selected(final LocalItem selectedItem) {
if (selectedItem instanceof StreamStatisticsEntry) {
final StreamStatisticsEntry item = (StreamStatisticsEntry) selectedItem;
NavigationHelper.openVideoDetailFragment(getFM(),
item.getStreamEntity().getServiceId(),
item.getStreamEntity().getUrl(),
item.getStreamEntity().getTitle());
final StreamEntity item =
((StreamStatisticsEntry) selectedItem).getStreamEntity();
NavigationHelper.openVideoDetailFragment(requireContext(), getFM(),
item.getServiceId(), item.getUrl(), item.getTitle(), null, false);
}
}
@@ -321,7 +325,7 @@ public class StatisticsPlaylistFragment
}
headerPlayAllButton.setOnClickListener(view ->
NavigationHelper.playOnMainPlayer(activity, getPlayQueue(), true));
NavigationHelper.playOnMainPlayer(activity, getPlayQueue()));
headerPopupButton.setOnClickListener(view ->
NavigationHelper.playOnPopupPlayer(activity, getPlayQueue(), false));
headerBackgroundButton.setOnClickListener(view ->
@@ -387,27 +391,28 @@ public class StatisticsPlaylistFragment
}
final StreamInfoItem infoItem = item.toStreamInfoItem();
final ArrayList<StreamDialogEntry> entries = new ArrayList<>();
if (PlayerHolder.getType() != null) {
entries.add(StreamDialogEntry.enqueue);
}
if (infoItem.getStreamType() == StreamType.AUDIO_STREAM) {
StreamDialogEntry.setEnabledEntries(
StreamDialogEntry.enqueue_on_background,
entries.addAll(Arrays.asList(
StreamDialogEntry.start_here_on_background,
StreamDialogEntry.delete,
StreamDialogEntry.append_playlist,
StreamDialogEntry.share);
} else {
StreamDialogEntry.setEnabledEntries(
StreamDialogEntry.enqueue_on_background,
StreamDialogEntry.enqueue_on_popup,
StreamDialogEntry.share
));
} else {
entries.addAll(Arrays.asList(
StreamDialogEntry.start_here_on_background,
StreamDialogEntry.start_here_on_popup,
StreamDialogEntry.delete,
StreamDialogEntry.append_playlist,
StreamDialogEntry.share);
StreamDialogEntry.start_here_on_popup.setCustomAction((fragment, infoItemDuplicate) ->
NavigationHelper
.playOnPopupPlayer(context, getPlayQueueStartingAt(item), true));
StreamDialogEntry.share
));
}
StreamDialogEntry.setEnabledEntries(entries);
StreamDialogEntry.start_here_on_background.setCustomAction((fragment, infoItemDuplicate) ->
NavigationHelper
@@ -420,14 +425,14 @@ public class StatisticsPlaylistFragment
}
private void deleteEntry(final int index) {
final LocalItem infoItem = itemListAdapter.getItemsList()
.get(index);
final LocalItem infoItem = itemListAdapter.getItemsList().get(index);
if (infoItem instanceof StreamStatisticsEntry) {
final StreamStatisticsEntry entry = (StreamStatisticsEntry) infoItem;
final Disposable onDelete = recordManager.deleteStreamHistory(entry.getStreamId())
final Disposable onDelete = recordManager
.deleteStreamHistoryAndState(entry.getStreamId())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
howManyDeleted -> {
() -> {
if (getView() != null) {
Snackbar.make(getView(), R.string.one_item_deleted,
Snackbar.LENGTH_SHORT).show();

View File

@@ -9,7 +9,7 @@ import org.schabi.newpipe.database.LocalItem;
import org.schabi.newpipe.local.LocalItemBuilder;
import org.schabi.newpipe.local.history.HistoryRecordManager;
import java.text.DateFormat;
import java.time.format.DateTimeFormatter;
/*
* Created by Christian Schabesberger on 12.02.17.
@@ -41,7 +41,7 @@ public abstract class LocalItemHolder extends RecyclerView.ViewHolder {
}
public abstract void updateFromItem(LocalItem item, HistoryRecordManager historyRecordManager,
DateFormat dateFormat);
DateTimeFormatter dateTimeFormatter);
public void updateState(final LocalItem localItem,
final HistoryRecordManager historyRecordManager) { }

View File

@@ -10,7 +10,7 @@ import org.schabi.newpipe.local.history.HistoryRecordManager;
import org.schabi.newpipe.util.ImageDisplayConstants;
import org.schabi.newpipe.util.Localization;
import java.text.DateFormat;
import java.time.format.DateTimeFormatter;
public class LocalPlaylistItemHolder extends PlaylistItemHolder {
public LocalPlaylistItemHolder(final LocalItemBuilder infoItemBuilder, final ViewGroup parent) {
@@ -25,7 +25,7 @@ public class LocalPlaylistItemHolder extends PlaylistItemHolder {
@Override
public void updateFromItem(final LocalItem localItem,
final HistoryRecordManager historyRecordManager,
final DateFormat dateFormat) {
final DateTimeFormatter dateTimeFormatter) {
if (!(localItem instanceof PlaylistMetadataEntry)) {
return;
}
@@ -39,6 +39,6 @@ public class LocalPlaylistItemHolder extends PlaylistItemHolder {
itemBuilder.displayImage(item.thumbnailUrl, itemThumbnailView,
ImageDisplayConstants.DISPLAY_PLAYLIST_OPTIONS);
super.updateFromItem(localItem, historyRecordManager, dateFormat);
super.updateFromItem(localItem, historyRecordManager, dateTimeFormatter);
}
}

View File

@@ -20,7 +20,7 @@ import org.schabi.newpipe.util.ImageDisplayConstants;
import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.views.AnimatedProgressBar;
import java.text.DateFormat;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.concurrent.TimeUnit;
@@ -52,7 +52,7 @@ public class LocalPlaylistStreamItemHolder extends LocalItemHolder {
@Override
public void updateFromItem(final LocalItem localItem,
final HistoryRecordManager historyRecordManager,
final DateFormat dateFormat) {
final DateTimeFormatter dateTimeFormatter) {
if (!(localItem instanceof PlaylistStreamEntry)) {
return;
}
@@ -104,7 +104,6 @@ public class LocalPlaylistStreamItemHolder extends LocalItemHolder {
return true;
});
itemThumbnailView.setOnTouchListener(getOnTouchListener(item));
itemHandleView.setOnTouchListener(getOnTouchListener(item));
}

View File

@@ -20,7 +20,7 @@ import org.schabi.newpipe.util.ImageDisplayConstants;
import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.views.AnimatedProgressBar;
import java.text.DateFormat;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.concurrent.TimeUnit;
@@ -71,10 +71,10 @@ public class LocalStatisticStreamItemHolder extends LocalItemHolder {
}
private String getStreamInfoDetailLine(final StreamStatisticsEntry entry,
final DateFormat dateFormat) {
final DateTimeFormatter dateTimeFormatter) {
final String watchCount = Localization
.shortViewCount(itemBuilder.getContext(), entry.getWatchCount());
final String uploadDate = dateFormat.format(entry.getLatestAccessDate());
final String uploadDate = dateTimeFormatter.format(entry.getLatestAccessDate());
final String serviceName = NewPipe.getNameOfService(entry.getStreamEntity().getServiceId());
return Localization.concatenateStrings(watchCount, uploadDate, serviceName);
}
@@ -82,7 +82,7 @@ public class LocalStatisticStreamItemHolder extends LocalItemHolder {
@Override
public void updateFromItem(final LocalItem localItem,
final HistoryRecordManager historyRecordManager,
final DateFormat dateFormat) {
final DateTimeFormatter dateTimeFormatter) {
if (!(localItem instanceof StreamStatisticsEntry)) {
return;
}
@@ -116,7 +116,7 @@ public class LocalStatisticStreamItemHolder extends LocalItemHolder {
}
if (itemAdditionalDetails != null) {
itemAdditionalDetails.setText(getStreamInfoDetailLine(item, dateFormat));
itemAdditionalDetails.setText(getStreamInfoDetailLine(item, dateTimeFormatter));
}
// Default thumbnail is shown on error, while loading and if the url is empty

View File

@@ -9,7 +9,7 @@ import org.schabi.newpipe.database.LocalItem;
import org.schabi.newpipe.local.LocalItemBuilder;
import org.schabi.newpipe.local.history.HistoryRecordManager;
import java.text.DateFormat;
import java.time.format.DateTimeFormatter;
public abstract class PlaylistItemHolder extends LocalItemHolder {
public final ImageView itemThumbnailView;
@@ -34,7 +34,7 @@ public abstract class PlaylistItemHolder extends LocalItemHolder {
@Override
public void updateFromItem(final LocalItem localItem,
final HistoryRecordManager historyRecordManager,
final DateFormat dateFormat) {
final DateTimeFormatter dateTimeFormatter) {
itemView.setOnClickListener(view -> {
if (itemBuilder.getOnItemSelectedListener() != null) {
itemBuilder.getOnItemSelectedListener().selected(localItem);

View File

@@ -11,7 +11,7 @@ import org.schabi.newpipe.local.history.HistoryRecordManager;
import org.schabi.newpipe.util.ImageDisplayConstants;
import org.schabi.newpipe.util.Localization;
import java.text.DateFormat;
import java.time.format.DateTimeFormatter;
public class RemotePlaylistItemHolder extends PlaylistItemHolder {
public RemotePlaylistItemHolder(final LocalItemBuilder infoItemBuilder,
@@ -27,7 +27,7 @@ public class RemotePlaylistItemHolder extends PlaylistItemHolder {
@Override
public void updateFromItem(final LocalItem localItem,
final HistoryRecordManager historyRecordManager,
final DateFormat dateFormat) {
final DateTimeFormatter dateTimeFormatter) {
if (!(localItem instanceof PlaylistRemoteEntity)) {
return;
}
@@ -48,6 +48,6 @@ public class RemotePlaylistItemHolder extends PlaylistItemHolder {
itemBuilder.displayImage(item.getThumbnailUrl(), itemThumbnailView,
ImageDisplayConstants.DISPLAY_PLAYLIST_OPTIONS);
super.updateFromItem(localItem, historyRecordManager, dateFormat);
super.updateFromItem(localItem, historyRecordManager, dateTimeFormatter);
}
}

View File

@@ -30,12 +30,14 @@ import org.schabi.newpipe.R;
import org.schabi.newpipe.database.LocalItem;
import org.schabi.newpipe.database.history.model.StreamHistoryEntry;
import org.schabi.newpipe.database.playlist.PlaylistStreamEntry;
import org.schabi.newpipe.database.stream.model.StreamEntity;
import org.schabi.newpipe.database.stream.model.StreamStateEntity;
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
import org.schabi.newpipe.extractor.stream.StreamType;
import org.schabi.newpipe.info_list.InfoItemDialog;
import org.schabi.newpipe.local.BaseLocalListFragment;
import org.schabi.newpipe.local.history.HistoryRecordManager;
import org.schabi.newpipe.player.helper.PlayerHolder;
import org.schabi.newpipe.player.playqueue.PlayQueue;
import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
import org.schabi.newpipe.report.UserAction;
@@ -45,6 +47,7 @@ import org.schabi.newpipe.util.OnClickGesture;
import org.schabi.newpipe.util.StreamDialogEntry;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
@@ -176,10 +179,10 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
@Override
public void selected(final LocalItem selectedItem) {
if (selectedItem instanceof PlaylistStreamEntry) {
final PlaylistStreamEntry item = (PlaylistStreamEntry) selectedItem;
NavigationHelper.openVideoDetailFragment(getFM(),
item.getStreamEntity().getServiceId(), item.getStreamEntity().getUrl(),
item.getStreamEntity().getTitle());
final StreamEntity item =
((PlaylistStreamEntry) selectedItem).getStreamEntity();
NavigationHelper.openVideoDetailFragment(requireContext(), getFM(),
item.getServiceId(), item.getUrl(), item.getTitle(), null, false);
}
}
@@ -492,7 +495,7 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
setVideoCount(itemListAdapter.getItemsList().size());
headerPlayAllButton.setOnClickListener(view ->
NavigationHelper.playOnMainPlayer(activity, getPlayQueue(), true));
NavigationHelper.playOnMainPlayer(activity, getPlayQueue()));
headerPopupButton.setOnClickListener(view ->
NavigationHelper.playOnPopupPlayer(activity, getPlayQueue(), false));
headerBackgroundButton.setOnClickListener(view ->
@@ -756,29 +759,30 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
}
final StreamInfoItem infoItem = item.toStreamInfoItem();
final ArrayList<StreamDialogEntry> entries = new ArrayList<>();
if (PlayerHolder.getType() != null) {
entries.add(StreamDialogEntry.enqueue);
}
if (infoItem.getStreamType() == StreamType.AUDIO_STREAM) {
StreamDialogEntry.setEnabledEntries(
StreamDialogEntry.enqueue_on_background,
entries.addAll(Arrays.asList(
StreamDialogEntry.start_here_on_background,
StreamDialogEntry.set_as_playlist_thumbnail,
StreamDialogEntry.delete,
StreamDialogEntry.append_playlist,
StreamDialogEntry.share);
} else {
StreamDialogEntry.setEnabledEntries(
StreamDialogEntry.enqueue_on_background,
StreamDialogEntry.enqueue_on_popup,
StreamDialogEntry.share
));
} else {
entries.addAll(Arrays.asList(
StreamDialogEntry.start_here_on_background,
StreamDialogEntry.start_here_on_popup,
StreamDialogEntry.set_as_playlist_thumbnail,
StreamDialogEntry.delete,
StreamDialogEntry.append_playlist,
StreamDialogEntry.share);
StreamDialogEntry.start_here_on_popup.setCustomAction(
(fragment, infoItemDuplicate) -> NavigationHelper.
playOnPopupPlayer(context, getPlayQueueStartingAt(item), true));
StreamDialogEntry.share
));
}
StreamDialogEntry.setEnabledEntries(entries);
StreamDialogEntry.start_here_on_background.setCustomAction((fragment, infoItemDuplicate) ->
NavigationHelper.playOnBackgroundPlayer(context,

View File

@@ -126,4 +126,10 @@ public class LocalPlaylistManager {
}).subscribeOn(Schedulers.io());
}
public Maybe<Boolean> hasPlaylists() {
return playlistTable.getCount()
.firstElement()
.map(count -> count > 0)
.subscribeOn(Schedulers.io());
}
}

View File

@@ -1,17 +1,19 @@
package org.schabi.newpipe.local.subscription.dialog
import android.app.Dialog
import android.content.Context
import android.os.Bundle
import android.os.Parcelable
import android.text.Editable
import android.text.TextUtils
import android.text.TextWatcher
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.inputmethod.InputMethodManager
import android.widget.Toast
import androidx.core.content.getSystemService
import androidx.core.os.bundleOf
import androidx.core.view.isGone
import androidx.core.view.isVisible
import androidx.core.widget.doOnTextChanged
import androidx.fragment.app.DialogFragment
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider
@@ -191,16 +193,11 @@ class FeedGroupDialog : DialogFragment(), BackPressable {
}
group_name_input_container.error = null
group_name_input.addTextChangedListener(object : TextWatcher {
override fun afterTextChanged(s: Editable?) {}
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
if (group_name_input_container.isErrorEnabled && !s.isNullOrBlank()) {
group_name_input_container.error = null
}
group_name_input.doOnTextChanged { text, _, _, _ ->
if (group_name_input_container.isErrorEnabled && !text.isNullOrBlank()) {
group_name_input_container.error = null
}
})
}
confirm_button.setOnClickListener { handlePositiveButton() }
@@ -242,15 +239,11 @@ class FeedGroupDialog : DialogFragment(), BackPressable {
}
}
toolbar_search_edit_text.addTextChangedListener(object : TextWatcher {
override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) = Unit
override fun afterTextChanged(s: Editable) = Unit
override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {
val newQuery: String = toolbar_search_edit_text.text.toString()
subscriptionsCurrentSearchQuery = newQuery
viewModel.filterSubscriptionsBy(newQuery)
}
})
toolbar_search_edit_text.doOnTextChanged { _, _, _, _ ->
val newQuery: String = toolbar_search_edit_text.text.toString()
subscriptionsCurrentSearchQuery = newQuery
viewModel.filterSubscriptionsBy(newQuery)
}
subscriptionGroupAdapter.setOnItemClickListener(subscriptionPickerItemListener)
}
@@ -414,21 +407,14 @@ class FeedGroupDialog : DialogFragment(), BackPressable {
else -> android.R.string.ok
})
delete_button.visibility = when {
currentScreen != InitialScreen -> View.GONE
groupId == NO_GROUP_SELECTED -> View.GONE
else -> View.VISIBLE
}
delete_button.isGone = currentScreen != InitialScreen || groupId == NO_GROUP_SELECTED
hideKeyboard()
hideSearch()
}
private fun View.onlyVisibleIn(vararg screens: ScreenState) {
visibility = when (currentScreen) {
in screens -> View.VISIBLE
else -> View.GONE
}
isVisible = currentScreen in screens
}
/*///////////////////////////////////////////////////////////////////////////
@@ -459,7 +445,7 @@ class FeedGroupDialog : DialogFragment(), BackPressable {
}
private val inputMethodManager by lazy {
requireActivity().getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
requireActivity().getSystemService<InputMethodManager>()!!
}
private fun showKeyboardSearch() {
@@ -501,11 +487,7 @@ class FeedGroupDialog : DialogFragment(), BackPressable {
fun newInstance(groupId: Long = NO_GROUP_SELECTED): FeedGroupDialog {
val dialog = FeedGroupDialog()
dialog.arguments = Bundle().apply {
putLong(KEY_GROUP_ID, groupId)
}
dialog.arguments = bundleOf(KEY_GROUP_ID to groupId)
return dialog
}
}

View File

@@ -37,8 +37,8 @@ class FeedGroupDialogViewModel(
BiFunction { t1: String, t2: Boolean -> Filter(t1, t2) }
)
.distinctUntilChanged()
.switchMap { filter ->
subscriptionManager.getSubscriptions(groupId, filter.query, filter.showOnlyUngrouped)
.switchMap { (query, showOnlyUngrouped) ->
subscriptionManager.getSubscriptions(groupId, query, showOnlyUngrouped)
}.map { list -> list.map { PickerSubscriptionItem(it) } }
private val mutableGroupLiveData = MutableLiveData<FeedGroupEntity>()

View File

@@ -1,9 +1,8 @@
package org.schabi.newpipe.local.subscription.item
import android.view.View.GONE
import android.view.View.OnClickListener
import android.view.View.VISIBLE
import androidx.annotation.DrawableRes
import androidx.core.view.isVisible
import com.xwray.groupie.kotlinandroidextensions.GroupieViewHolder
import com.xwray.groupie.kotlinandroidextensions.Item
import kotlinx.android.synthetic.main.header_with_menu_item.header_menu_item
@@ -47,6 +46,6 @@ class HeaderWithMenuItem(
}
private fun updateMenuItemVisibility(viewHolder: GroupieViewHolder) {
viewHolder.header_menu_item.visibility = if (showMenuItem) VISIBLE else GONE
viewHolder.header_menu_item.isVisible = showMenuItem
}
}

View File

@@ -1,6 +1,7 @@
package org.schabi.newpipe.local.subscription.item
import android.view.View
import androidx.core.view.isVisible
import com.nostra13.universalimageloader.core.ImageLoader
import com.xwray.groupie.kotlinandroidextensions.GroupieViewHolder
import com.xwray.groupie.kotlinandroidextensions.Item
@@ -25,7 +26,7 @@ data class PickerSubscriptionItem(
viewHolder.thumbnail_view, ImageDisplayConstants.DISPLAY_AVATAR_OPTIONS)
viewHolder.title_view.text = subscriptionEntity.name
viewHolder.selected_highlight.visibility = if (isSelected) View.VISIBLE else View.GONE
viewHolder.selected_highlight.isVisible = isSelected
}
override fun unbind(viewHolder: GroupieViewHolder) {

View File

@@ -2,11 +2,8 @@ package org.schabi.newpipe.player;
import android.content.Intent;
import android.view.Menu;
import android.view.MenuItem;
import org.schabi.newpipe.R;
import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.PermissionHelper;
public final class BackgroundPlayerActivity extends ServicePlayerActivity {
@@ -46,31 +43,6 @@ public final class BackgroundPlayerActivity extends ServicePlayerActivity {
return R.menu.menu_play_queue_bg;
}
@Override
public boolean onPlayerOptionSelected(final MenuItem item) {
if (item.getItemId() == R.id.action_switch_popup) {
if (!PermissionHelper.isPopupEnabled(this)) {
PermissionHelper.showPopupEnablementToast(this);
return true;
}
this.player.setRecovery();
NavigationHelper.playOnPopupPlayer(
getApplicationContext(), player.playQueue, this.player.isPlaying());
return true;
}
if (item.getItemId() == R.id.action_switch_background) {
this.player.setRecovery();
NavigationHelper.playOnBackgroundPlayer(
getApplicationContext(), player.playQueue, this.player.isPlaying());
return true;
}
return false;
}
@Override
public void setupMenu(final Menu menu) {
if (player == null) {

View File

@@ -125,7 +125,7 @@ public abstract class BasePlayer implements
@NonNull
public static final String RESUME_PLAYBACK = "resume_playback";
@NonNull
public static final String START_PAUSED = "start_paused";
public static final String PLAY_WHEN_READY = "play_when_ready";
@NonNull
public static final String SELECT_ON_APPEND = "select_on_append";
@NonNull
@@ -224,7 +224,7 @@ public abstract class BasePlayer implements
this.dataSource = new PlayerDataSource(context, userAgent, bandwidthMeter);
final TrackSelection.Factory trackSelectionFactory = PlayerHelper
.getQualitySelector(context);
.getQualitySelector();
this.trackSelector = new CustomTrackSelector(context, trackSelectionFactory);
this.loadControl = new LoadController();
@@ -302,6 +302,7 @@ public abstract class BasePlayer implements
final boolean samePlayQueue = playQueue != null && playQueue.equals(queue);
final int repeatMode = intent.getIntExtra(REPEAT_MODE, getRepeatMode());
final boolean playWhenReady = intent.getBooleanExtra(PLAY_WHEN_READY, true);
final boolean isMuted = intent
.getBooleanExtra(IS_MUTED, simpleExoPlayer != null && isMuted());
@@ -327,16 +328,20 @@ public abstract class BasePlayer implements
simpleExoPlayer.retry();
}
simpleExoPlayer.seekTo(playQueue.getIndex(), queue.getItem().getRecoveryPosition());
return;
simpleExoPlayer.setPlayWhenReady(playWhenReady);
} else if (samePlayQueue && !playQueue.isDisposed() && simpleExoPlayer != null) {
} else if (simpleExoPlayer != null
&& samePlayQueue
&& playQueue != null
&& !playQueue.isDisposed()) {
// Do not re-init the same PlayQueue. Save time
// Player can have state = IDLE when playback is stopped or failed
// and we should retry() in this case
if (simpleExoPlayer.getPlaybackState() == Player.STATE_IDLE) {
simpleExoPlayer.retry();
}
return;
simpleExoPlayer.setPlayWhenReady(playWhenReady);
} else if (intent.getBooleanExtra(RESUME_PLAYBACK, false)
&& isPlaybackResumeEnabled()
&& !samePlayQueue) {
@@ -351,7 +356,7 @@ public abstract class BasePlayer implements
state -> {
queue.setRecovery(queue.getIndex(), state.getProgressTime());
initPlayback(queue, repeatMode, playbackSpeed, playbackPitch,
playbackSkipSilence, true, isMuted);
playbackSkipSilence, playWhenReady, isMuted);
},
error -> {
if (DEBUG) {
@@ -359,24 +364,22 @@ public abstract class BasePlayer implements
}
// In case any error we can start playback without history
initPlayback(queue, repeatMode, playbackSpeed, playbackPitch,
playbackSkipSilence, true, isMuted);
playbackSkipSilence, playWhenReady, isMuted);
},
() -> {
// Completed but not found in history
initPlayback(queue, repeatMode, playbackSpeed, playbackPitch,
playbackSkipSilence, true, isMuted);
playbackSkipSilence, playWhenReady, isMuted);
}
);
databaseUpdateReactor.add(stateLoader);
return;
}
} else {
// Good to go...
// In a case of equal PlayQueues we can re-init old one but only when it is disposed
initPlayback(samePlayQueue ? playQueue : queue, repeatMode, playbackSpeed,
playbackPitch, playbackSkipSilence, playWhenReady, isMuted);
}
// Good to go...
// In a case of equal PlayQueues we can re-init old one but only when it is disposed
initPlayback(samePlayQueue ? playQueue : queue, repeatMode,
playbackSpeed, playbackPitch, playbackSkipSilence,
!intent.getBooleanExtra(START_PAUSED, false),
isMuted);
}
private PlaybackParameters retrievePlaybackParametersFromPreferences() {

View File

@@ -30,6 +30,9 @@ import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat;
import org.schabi.newpipe.R;
import org.schabi.newpipe.util.ThemeHelper;
@@ -91,7 +94,7 @@ public final class MainPlayer extends Service {
Log.d(TAG, "onCreate() called");
}
assureCorrectAppLanguage(this);
windowManager = (WindowManager) getSystemService(WINDOW_SERVICE);
windowManager = ContextCompat.getSystemService(this, WindowManager.class);
ThemeHelper.setTheme(this);
createView();
@@ -223,12 +226,13 @@ public final class MainPlayer extends Service {
// DisplayMetrics from activity context knows about MultiWindow feature
// while DisplayMetrics from app context doesn't
final DisplayMetrics metrics = (playerImpl != null
&& playerImpl.getParentActivity() != null)
? playerImpl.getParentActivity().getResources().getDisplayMetrics()
: getResources().getDisplayMetrics();
&& playerImpl.getParentActivity() != null
? playerImpl.getParentActivity().getResources()
: getResources()).getDisplayMetrics();
return metrics.heightPixels < metrics.widthPixels;
}
@Nullable
public View getView() {
if (playerImpl == null) {
return null;
@@ -238,7 +242,7 @@ public final class MainPlayer extends Service {
}
public void removeViewFromParent() {
if (getView().getParent() != null) {
if (getView() != null && getView().getParent() != null) {
if (playerImpl.getParentActivity() != null) {
// This means view was added to fragment
final ViewGroup parent = (ViewGroup) getView().getParent();

View File

@@ -116,10 +116,14 @@ public final class NotificationUtil {
.setMediaSession(player.mediaSessionManager.getSessionToken())
.setShowActionsInCompactView(compactSlots))
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setSmallIcon(R.drawable.ic_newpipe_triangle_white)
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
.setColor(ContextCompat.getColor(player.context, R.color.gray))
.setCategory(NotificationCompat.CATEGORY_TRANSPORT)
.setShowWhen(false)
.setSmallIcon(R.drawable.ic_newpipe_triangle_white)
.setColor(ContextCompat.getColor(player.context, R.color.dark_background_color))
.setColorized(player.sharedPreferences.getBoolean(
player.context.getString(R.string.notification_colorize_key),
true))
.setDeleteIntent(PendingIntent.getBroadcast(player.context, NOTIFICATION_ID,
new Intent(ACTION_CLOSE), FLAG_UPDATE_CURRENT));
@@ -148,7 +152,10 @@ public final class NotificationUtil {
@SuppressLint("RestrictedApi")
boolean shouldUpdateBufferingSlot() {
if (notificationBuilder.mActions.size() < 3) {
if (notificationBuilder == null) {
// if there is no notification active, there is no point in updating it
return false;
} else if (notificationBuilder.mActions.size() < 3) {
// this should never happen, but let's make sure notification actions are populated
return true;
}

View File

@@ -27,12 +27,11 @@ import androidx.recyclerview.widget.RecyclerView;
import com.google.android.exoplayer2.PlaybackParameters;
import com.google.android.exoplayer2.Player;
import org.schabi.newpipe.MainActivity;
import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.StreamingService;
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.player.event.PlayerEventListener;
import org.schabi.newpipe.player.helper.PlaybackParameterDialog;
import org.schabi.newpipe.player.playqueue.PlayQueue;
@@ -41,9 +40,9 @@ import org.schabi.newpipe.player.playqueue.PlayQueueItem;
import org.schabi.newpipe.player.playqueue.PlayQueueItemBuilder;
import org.schabi.newpipe.player.playqueue.PlayQueueItemHolder;
import org.schabi.newpipe.player.playqueue.PlayQueueItemTouchCallback;
import org.schabi.newpipe.util.Constants;
import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.PermissionHelper;
import org.schabi.newpipe.util.ThemeHelper;
import java.util.Collections;
@@ -112,9 +111,8 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
public abstract int getPlayerOptionMenuResource();
public abstract boolean onPlayerOptionSelected(MenuItem item);
public abstract void setupMenu(Menu m);
////////////////////////////////////////////////////////////////////////////
// Activity Lifecycle
////////////////////////////////////////////////////////////////////////////
@@ -186,12 +184,22 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
return true;
case R.id.action_switch_main:
this.player.setRecovery();
getApplicationContext().startActivity(
getSwitchIntent(MainActivity.class, MainPlayer.PlayerType.VIDEO)
.putExtra(BasePlayer.START_PAUSED, !this.player.isPlaying()));
NavigationHelper.playOnMainPlayer(this, player.getPlayQueue(), true);
return true;
case R.id.action_switch_popup:
if (PermissionHelper.isPopupEnabled(this)) {
this.player.setRecovery();
NavigationHelper.playOnPopupPlayer(this, player.playQueue, true);
} else {
PermissionHelper.showPopupEnablementToast(this);
}
return true;
case R.id.action_switch_background:
this.player.setRecovery();
NavigationHelper.playOnBackgroundPlayer(this, player.playQueue, true);
return true;
}
return onPlayerOptionSelected(item) || super.onOptionsItemSelected(item);
return super.onOptionsItemSelected(item);
}
@Override
@@ -200,24 +208,6 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
unbind();
}
protected Intent getSwitchIntent(final Class clazz, final MainPlayer.PlayerType playerType) {
return NavigationHelper.getPlayerIntent(getApplicationContext(), clazz,
this.player.getPlayQueue(), this.player.getRepeatMode(),
this.player.getPlaybackSpeed(), this.player.getPlaybackPitch(),
this.player.getPlaybackSkipSilence(),
null,
true,
!this.player.isPlaying(),
this.player.isMuted())
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
.putExtra(Constants.KEY_LINK_TYPE, StreamingService.LinkType.STREAM)
.putExtra(Constants.KEY_URL, this.player.getVideoUrl())
.putExtra(Constants.KEY_TITLE, this.player.getVideoTitle())
.putExtra(Constants.KEY_SERVICE_ID,
this.player.getCurrentMetadata().getMetadata().getServiceId())
.putExtra(VideoPlayer.PLAYER_TYPE, playerType);
}
////////////////////////////////////////////////////////////////////////////
// Service Connection
////////////////////////////////////////////////////////////////////////////
@@ -366,7 +356,9 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
final MenuItem detail = popupMenu.getMenu().add(RECYCLER_ITEM_POPUP_MENU_GROUP_ID, 1,
Menu.NONE, R.string.play_queue_stream_detail);
detail.setOnMenuItemClickListener(menuItem -> {
onOpenDetail(item.getServiceId(), item.getUrl(), item.getTitle());
// playQueue is null since we don't want any queue change
NavigationHelper.openVideoDetail(this, item.getServiceId(), item.getUrl(),
item.getTitle(), null, false);
return true;
});
@@ -453,11 +445,6 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
};
}
private void onOpenDetail(final int serviceId, final String videoUrl,
final String videoTitle) {
NavigationHelper.openVideoDetail(this, serviceId, videoUrl, videoTitle);
}
private void scrollToSelected() {
if (player == null) {
return;
@@ -571,8 +558,13 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
}
private void openPlaylistAppendDialog(final List<PlayQueueItem> playlist) {
PlaylistAppendDialog.fromPlayQueueItems(playlist)
.show(getSupportFragmentManager(), getTag());
final PlaylistAppendDialog d = PlaylistAppendDialog.fromPlayQueueItems(playlist);
PlaylistAppendDialog.onPlaylistFound(getApplicationContext(),
() -> d.show(getSupportFragmentManager(), getTag()),
() -> PlaylistCreationDialog.newInstance(d)
.show(getSupportFragmentManager(), getTag()
));
}
////////////////////////////////////////////////////////////////////////////
@@ -742,11 +734,10 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
//2) Icon change accordingly to current App Theme
// using rootView.getContext() because getApplicationContext() didn't work
item.setIcon(player.isMuted()
? ThemeHelper.resolveResourceIdFromAttr(rootView.getContext(),
R.attr.ic_volume_off)
: ThemeHelper.resolveResourceIdFromAttr(rootView.getContext(),
R.attr.ic_volume_up));
item.setIcon(ThemeHelper.resolveResourceIdFromAttr(rootView.getContext(),
player.isMuted()
? R.attr.ic_volume_off
: R.attr.ic_volume_up));
}
}
}

View File

@@ -32,8 +32,6 @@ import android.graphics.PixelFormat;
import android.net.Uri;
import android.os.Build;
import android.os.Handler;
import android.view.DisplayCutout;
import androidx.preference.PreferenceManager;
import android.provider.Settings;
import android.util.DisplayMetrics;
import android.util.Log;
@@ -60,8 +58,13 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.content.res.AppCompatResources;
import androidx.core.content.ContextCompat;
import androidx.core.view.DisplayCutoutCompat;
import androidx.core.view.ViewCompat;
import androidx.preference.PreferenceManager;
import androidx.recyclerview.widget.ItemTouchHelper;
import androidx.recyclerview.widget.RecyclerView;
import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.SimpleExoPlayer;
@@ -72,9 +75,8 @@ import com.google.android.exoplayer2.ui.AspectRatioFrameLayout;
import com.google.android.exoplayer2.ui.SubtitleView;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
import com.nostra13.universalimageloader.core.assist.FailReason;
import org.schabi.newpipe.MainActivity;
import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.StreamingService;
import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.extractor.stream.VideoStream;
import org.schabi.newpipe.fragments.OnScrollBelowItemsListener;
@@ -92,9 +94,8 @@ import org.schabi.newpipe.player.playqueue.PlayQueueItemTouchCallback;
import org.schabi.newpipe.player.resolver.AudioPlaybackResolver;
import org.schabi.newpipe.player.resolver.MediaSourceTag;
import org.schabi.newpipe.player.resolver.VideoPlaybackResolver;
import org.schabi.newpipe.util.DeviceUtils;
import org.schabi.newpipe.util.AnimationUtils;
import org.schabi.newpipe.util.Constants;
import org.schabi.newpipe.util.DeviceUtils;
import org.schabi.newpipe.util.KoreUtil;
import org.schabi.newpipe.util.ListHelper;
import org.schabi.newpipe.util.NavigationHelper;
@@ -102,7 +103,6 @@ import org.schabi.newpipe.util.ShareUtils;
import java.util.List;
import static android.content.Context.WINDOW_SERVICE;
import static org.schabi.newpipe.player.MainPlayer.ACTION_CLOSE;
import static org.schabi.newpipe.player.MainPlayer.ACTION_FAST_FORWARD;
import static org.schabi.newpipe.player.MainPlayer.ACTION_FAST_REWIND;
@@ -257,7 +257,12 @@ public class VideoPlayerImpl extends VideoPlayer
onQueueClosed();
// Android TV: without it focus will frame the whole player
playPauseButton.requestFocus();
onPlay();
if (simpleExoPlayer.getPlayWhenReady()) {
onPlay();
} else {
onPause();
}
}
NavigationHelper.sendPlayerStartedEvent(service);
}
@@ -266,7 +271,7 @@ public class VideoPlayerImpl extends VideoPlayer
super("MainPlayer" + TAG, service);
this.service = service;
this.shouldUpdateOnProgress = true;
this.windowManager = (WindowManager) service.getSystemService(WINDOW_SERVICE);
this.windowManager = ContextCompat.getSystemService(service, WindowManager.class);
this.defaultPreferences = PreferenceManager.getDefaultSharedPreferences(service);
this.resolver = new AudioPlaybackResolver(context, dataSource);
}
@@ -475,16 +480,14 @@ public class VideoPlayerImpl extends VideoPlayer
settingsContentObserver);
getRootView().addOnLayoutChangeListener(this);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
queueLayout.setOnApplyWindowInsetsListener((view, windowInsets) -> {
final DisplayCutout cutout = windowInsets.getDisplayCutout();
if (cutout != null) {
view.setPadding(cutout.getSafeInsetLeft(), cutout.getSafeInsetTop(),
cutout.getSafeInsetRight(), cutout.getSafeInsetBottom());
}
return windowInsets;
});
}
ViewCompat.setOnApplyWindowInsetsListener(queueLayout, (view, windowInsets) -> {
final DisplayCutoutCompat cutout = windowInsets.getDisplayCutout();
if (cutout != null) {
view.setPadding(cutout.getSafeInsetLeft(), cutout.getSafeInsetTop(),
cutout.getSafeInsetRight(), cutout.getSafeInsetBottom());
}
return windowInsets;
});
// PlaybackControlRoot already consumed window insets but we should pass them to
// player_overlays too. Without it they will be off-centered
@@ -755,40 +758,6 @@ public class VideoPlayerImpl extends VideoPlayer
setupScreenRotationButton();
}
public void switchFromPopupToMain() {
if (DEBUG) {
Log.d(TAG, "switchFromPopupToMain() called");
}
if (!popupPlayerSelected() || simpleExoPlayer == null || getCurrentMetadata() == null) {
return;
}
setRecovery();
service.removeViewFromParent();
final Intent intent = NavigationHelper.getPlayerIntent(
service,
MainActivity.class,
this.getPlayQueue(),
this.getRepeatMode(),
this.getPlaybackSpeed(),
this.getPlaybackPitch(),
this.getPlaybackSkipSilence(),
null,
true,
!isPlaying(),
isMuted()
);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.putExtra(Constants.KEY_SERVICE_ID,
getCurrentMetadata().getMetadata().getServiceId());
intent.putExtra(Constants.KEY_LINK_TYPE, StreamingService.LinkType.STREAM);
intent.putExtra(Constants.KEY_URL, getVideoUrl());
intent.putExtra(Constants.KEY_TITLE, getVideoTitle());
intent.putExtra(VideoDetailFragment.AUTO_PLAY, true);
service.onDestroy();
context.startActivity(intent);
}
@Override
public void onClick(final View v) {
super.onClick(v);
@@ -816,7 +785,9 @@ public class VideoPlayerImpl extends VideoPlayer
} else if (v.getId() == openInBrowser.getId()) {
onOpenInBrowserClicked();
} else if (v.getId() == fullscreenButton.getId()) {
switchFromPopupToMain();
setRecovery();
NavigationHelper.playOnMainPlayer(context, getPlayQueue(), true);
return;
} else if (v.getId() == screenRotationButton.getId()) {
// Only if it's not a vertical video or vertical video but in landscape with locked
// orientation a screen orientation can be changed automatically
@@ -2093,6 +2064,10 @@ public class VideoPlayerImpl extends VideoPlayer
return popupLayoutParams;
}
public MainPlayer.PlayerType getPlayerType() {
return playerType;
}
public float getScreenWidth() {
return screenWidth;
}

View File

@@ -0,0 +1,500 @@
package org.schabi.newpipe.player.event
import android.content.Context
import android.os.Handler
import android.util.Log
import android.view.GestureDetector
import android.view.MotionEvent
import android.view.View
import android.view.ViewConfiguration
import org.schabi.newpipe.player.BasePlayer
import org.schabi.newpipe.player.MainPlayer
import org.schabi.newpipe.player.VideoPlayerImpl
import org.schabi.newpipe.player.helper.PlayerHelper
import org.schabi.newpipe.util.AnimationUtils
import kotlin.math.abs
import kotlin.math.hypot
import kotlin.math.max
/**
* Base gesture handling for [VideoPlayerImpl]
*
* This class contains the logic for the player gestures like View preparations
* and provides some abstract methods to make it easier separating the logic from the UI.
*/
abstract class BasePlayerGestureListener(
@JvmField
protected val playerImpl: VideoPlayerImpl,
@JvmField
protected val service: MainPlayer
) : GestureDetector.SimpleOnGestureListener(), View.OnTouchListener {
// ///////////////////////////////////////////////////////////////////
// Abstract methods for VIDEO and POPUP
// ///////////////////////////////////////////////////////////////////
abstract fun onDoubleTap(event: MotionEvent, portion: DisplayPortion)
abstract fun onSingleTap(playerType: MainPlayer.PlayerType)
abstract fun onScroll(
playerType: MainPlayer.PlayerType,
portion: DisplayPortion,
initialEvent: MotionEvent,
movingEvent: MotionEvent,
distanceX: Float,
distanceY: Float
)
abstract fun onScrollEnd(playerType: MainPlayer.PlayerType, event: MotionEvent)
// ///////////////////////////////////////////////////////////////////
// Abstract methods for POPUP (exclusive)
// ///////////////////////////////////////////////////////////////////
abstract fun onPopupResizingStart()
abstract fun onPopupResizingEnd()
private var initialPopupX: Int = -1
private var initialPopupY: Int = -1
private var isMovingInMain = false
private var isMovingInPopup = false
private var isResizing = false
private val tossFlingVelocity = PlayerHelper.getTossFlingVelocity()
// [popup] initial coordinates and distance between fingers
private var initPointerDistance = -1.0
private var initFirstPointerX = -1f
private var initFirstPointerY = -1f
private var initSecPointerX = -1f
private var initSecPointerY = -1f
// ///////////////////////////////////////////////////////////////////
// onTouch implementation
// ///////////////////////////////////////////////////////////////////
override fun onTouch(v: View, event: MotionEvent): Boolean {
return if (playerImpl.popupPlayerSelected()) {
onTouchInPopup(v, event)
} else {
onTouchInMain(v, event)
}
}
private fun onTouchInMain(v: View, event: MotionEvent): Boolean {
playerImpl.gestureDetector.onTouchEvent(event)
if (event.action == MotionEvent.ACTION_UP && isMovingInMain) {
isMovingInMain = false
onScrollEnd(MainPlayer.PlayerType.VIDEO, event)
}
return when (event.action) {
MotionEvent.ACTION_DOWN, MotionEvent.ACTION_MOVE -> {
v.parent.requestDisallowInterceptTouchEvent(playerImpl.isFullscreen)
true
}
MotionEvent.ACTION_UP -> {
v.parent.requestDisallowInterceptTouchEvent(false)
false
}
else -> true
}
}
private fun onTouchInPopup(v: View, event: MotionEvent): Boolean {
playerImpl.gestureDetector.onTouchEvent(event)
if (event.pointerCount == 2 && !isMovingInPopup && !isResizing) {
if (DEBUG) {
Log.d(TAG, "onTouch() 2 finger pointer detected, enabling resizing.")
}
onPopupResizingStart()
// record coordinates of fingers
initFirstPointerX = event.getX(0)
initFirstPointerY = event.getY(0)
initSecPointerX = event.getX(1)
initSecPointerY = event.getY(1)
// record distance between fingers
initPointerDistance = Math.hypot(initFirstPointerX - initSecPointerX.toDouble(),
initFirstPointerY - initSecPointerY.toDouble())
isResizing = true
}
if (event.action == MotionEvent.ACTION_MOVE && !isMovingInPopup && isResizing) {
if (DEBUG) {
Log.d(TAG, "onTouch() ACTION_MOVE > v = [$v], e1.getRaw = [${event.rawX}" +
", ${event.rawY}]")
}
return handleMultiDrag(event)
}
if (event.action == MotionEvent.ACTION_UP) {
if (DEBUG) {
Log.d(TAG, "onTouch() ACTION_UP > v = [$v], e1.getRaw = [${event.rawX}" +
", ${event.rawY}]")
}
if (isMovingInPopup) {
isMovingInPopup = false
onScrollEnd(MainPlayer.PlayerType.POPUP, event)
}
if (isResizing) {
isResizing = false
initPointerDistance = (-1).toDouble()
initFirstPointerX = (-1).toFloat()
initFirstPointerY = (-1).toFloat()
initSecPointerX = (-1).toFloat()
initSecPointerY = (-1).toFloat()
onPopupResizingEnd()
playerImpl.changeState(playerImpl.currentState)
}
if (!playerImpl.isPopupClosing) {
playerImpl.savePositionAndSize()
}
}
v.performClick()
return true
}
private fun handleMultiDrag(event: MotionEvent): Boolean {
if (initPointerDistance != -1.0 && event.pointerCount == 2) {
// get the movements of the fingers
val firstPointerMove = hypot(event.getX(0) - initFirstPointerX.toDouble(),
event.getY(0) - initFirstPointerY.toDouble())
val secPointerMove = hypot(event.getX(1) - initSecPointerX.toDouble(),
event.getY(1) - initSecPointerY.toDouble())
// minimum threshold beyond which pinch gesture will work
val minimumMove = ViewConfiguration.get(service).scaledTouchSlop
if (max(firstPointerMove, secPointerMove) > minimumMove) {
// calculate current distance between the pointers
val currentPointerDistance = hypot(event.getX(0) - event.getX(1).toDouble(),
event.getY(0) - event.getY(1).toDouble())
val popupWidth = playerImpl.popupWidth.toDouble()
// change co-ordinates of popup so the center stays at the same position
val newWidth = popupWidth * currentPointerDistance / initPointerDistance
initPointerDistance = currentPointerDistance
playerImpl.popupLayoutParams.x += ((popupWidth - newWidth) / 2.0).toInt()
playerImpl.checkPopupPositionBounds()
playerImpl.updateScreenSize()
playerImpl.updatePopupSize(
Math.min(playerImpl.screenWidth.toDouble(), newWidth).toInt(),
-1)
return true
}
}
return false
}
// ///////////////////////////////////////////////////////////////////
// Simple gestures
// ///////////////////////////////////////////////////////////////////
override fun onDown(e: MotionEvent): Boolean {
if (DEBUG)
Log.d(TAG, "onDown called with e = [$e]")
if (isDoubleTapping && isDoubleTapEnabled) {
doubleTapControls?.onDoubleTapProgressDown(getDisplayPortion(e))
return true
}
return if (playerImpl.popupPlayerSelected())
onDownInPopup(e)
else
true
}
private fun onDownInPopup(e: MotionEvent): Boolean {
// Fix popup position when the user touch it, it may have the wrong one
// because the soft input is visible (the draggable area is currently resized).
playerImpl.updateScreenSize()
playerImpl.checkPopupPositionBounds()
initialPopupX = playerImpl.popupLayoutParams.x
initialPopupY = playerImpl.popupLayoutParams.y
playerImpl.popupWidth = playerImpl.popupLayoutParams.width.toFloat()
playerImpl.popupHeight = playerImpl.popupLayoutParams.height.toFloat()
return super.onDown(e)
}
override fun onDoubleTap(e: MotionEvent): Boolean {
if (DEBUG)
Log.d(TAG, "onDoubleTap called with e = [$e]")
onDoubleTap(e, getDisplayPortion(e))
return true
}
override fun onSingleTapConfirmed(e: MotionEvent): Boolean {
if (DEBUG)
Log.d(TAG, "onSingleTapConfirmed() called with: e = [$e]")
if (isDoubleTapping)
return true
if (playerImpl.popupPlayerSelected()) {
if (playerImpl.player == null)
return false
onSingleTap(MainPlayer.PlayerType.POPUP)
return true
} else {
super.onSingleTapConfirmed(e)
if (playerImpl.currentState == BasePlayer.STATE_BLOCKED)
return true
onSingleTap(MainPlayer.PlayerType.VIDEO)
}
return true
}
override fun onLongPress(e: MotionEvent?) {
if (playerImpl.popupPlayerSelected()) {
playerImpl.updateScreenSize()
playerImpl.checkPopupPositionBounds()
playerImpl.updatePopupSize(playerImpl.screenWidth.toInt(), -1)
}
}
override fun onScroll(
initialEvent: MotionEvent,
movingEvent: MotionEvent,
distanceX: Float,
distanceY: Float
): Boolean {
return if (playerImpl.popupPlayerSelected()) {
onScrollInPopup(initialEvent, movingEvent, distanceX, distanceY)
} else {
onScrollInMain(initialEvent, movingEvent, distanceX, distanceY)
}
}
override fun onFling(
e1: MotionEvent?,
e2: MotionEvent?,
velocityX: Float,
velocityY: Float
): Boolean {
return if (playerImpl.popupPlayerSelected()) {
val absVelocityX = abs(velocityX)
val absVelocityY = abs(velocityY)
if (absVelocityX.coerceAtLeast(absVelocityY) > tossFlingVelocity) {
if (absVelocityX > tossFlingVelocity) {
playerImpl.popupLayoutParams.x = velocityX.toInt()
}
if (absVelocityY > tossFlingVelocity) {
playerImpl.popupLayoutParams.y = velocityY.toInt()
}
playerImpl.checkPopupPositionBounds()
playerImpl.windowManager
.updateViewLayout(playerImpl.rootView, playerImpl.popupLayoutParams)
return true
}
return false
} else {
true
}
}
private fun onScrollInMain(
initialEvent: MotionEvent,
movingEvent: MotionEvent,
distanceX: Float,
distanceY: Float
): Boolean {
if (!playerImpl.isFullscreen) {
return false
}
val isTouchingStatusBar: Boolean = initialEvent.y < getStatusBarHeight(service)
val isTouchingNavigationBar: Boolean = (initialEvent.y
> playerImpl.rootView.height - getNavigationBarHeight(service))
if (isTouchingStatusBar || isTouchingNavigationBar) {
return false
}
val insideThreshold = abs(movingEvent.y - initialEvent.y) <= MOVEMENT_THRESHOLD
if (!isMovingInMain && (insideThreshold || abs(distanceX) > abs(distanceY)) ||
playerImpl.currentState == BasePlayer.STATE_COMPLETED) {
return false
}
isMovingInMain = true
onScroll(MainPlayer.PlayerType.VIDEO, getDisplayHalfPortion(initialEvent),
initialEvent, movingEvent, distanceX, distanceY)
return true
}
private fun onScrollInPopup(
initialEvent: MotionEvent,
movingEvent: MotionEvent,
distanceX: Float,
distanceY: Float
): Boolean {
if (isResizing) {
return super.onScroll(initialEvent, movingEvent, distanceX, distanceY)
}
if (!isMovingInPopup) {
AnimationUtils.animateView(playerImpl.closeOverlayButton, true, 200)
}
isMovingInPopup = true
val diffX: Float = (movingEvent.rawX - initialEvent.rawX)
var posX: Float = (initialPopupX + diffX)
val diffY: Float = (movingEvent.rawY - initialEvent.rawY)
var posY: Float = (initialPopupY + diffY)
if (posX > playerImpl.screenWidth - playerImpl.popupWidth) {
posX = (playerImpl.screenWidth - playerImpl.popupWidth)
} else if (posX < 0) {
posX = 0f
}
if (posY > playerImpl.screenHeight - playerImpl.popupHeight) {
posY = (playerImpl.screenHeight - playerImpl.popupHeight)
} else if (posY < 0) {
posY = 0f
}
playerImpl.popupLayoutParams.x = posX.toInt()
playerImpl.popupLayoutParams.y = posY.toInt()
onScroll(MainPlayer.PlayerType.POPUP, getDisplayHalfPortion(initialEvent),
initialEvent, movingEvent, distanceX, distanceY)
playerImpl.windowManager
.updateViewLayout(playerImpl.rootView, playerImpl.popupLayoutParams)
return true
}
// ///////////////////////////////////////////////////////////////////
// Multi double tapping
// ///////////////////////////////////////////////////////////////////
var doubleTapControls: DoubleTapListener? = null
private set
val isDoubleTapEnabled: Boolean
get() = doubleTapDelay > 0
var isDoubleTapping = false
private set
fun doubleTapControls(listener: DoubleTapListener) = apply {
doubleTapControls = listener
}
private var doubleTapDelay = DOUBLE_TAP_DELAY
private val doubleTapHandler: Handler = Handler()
private val doubleTapRunnable = Runnable {
if (DEBUG)
Log.d(TAG, "doubleTapRunnable called")
isDoubleTapping = false
doubleTapControls?.onDoubleTapFinished()
}
fun startMultiDoubleTap(e: MotionEvent) {
if (!isDoubleTapping) {
if (DEBUG)
Log.d(TAG, "startMultiDoubleTap called with e = [$e]")
keepInDoubleTapMode()
doubleTapControls?.onDoubleTapStarted(getDisplayPortion(e))
}
}
fun keepInDoubleTapMode() {
if (DEBUG)
Log.d(TAG, "keepInDoubleTapMode called")
isDoubleTapping = true
doubleTapHandler.removeCallbacks(doubleTapRunnable)
doubleTapHandler.postDelayed(doubleTapRunnable, doubleTapDelay)
}
fun endMultiDoubleTap() {
if (DEBUG)
Log.d(TAG, "endMultiDoubleTap called")
isDoubleTapping = false
doubleTapHandler.removeCallbacks(doubleTapRunnable)
doubleTapControls?.onDoubleTapFinished()
}
fun enableMultiDoubleTap(enable: Boolean) = apply {
doubleTapDelay = if (enable) DOUBLE_TAP_DELAY else 0
}
// ///////////////////////////////////////////////////////////////////
// Utils
// ///////////////////////////////////////////////////////////////////
private fun getDisplayPortion(e: MotionEvent): DisplayPortion {
return if (playerImpl.playerType == MainPlayer.PlayerType.POPUP) {
when {
e.x < playerImpl.popupWidth / 3.0 -> DisplayPortion.LEFT
e.x > playerImpl.popupWidth * 2.0 / 3.0 -> DisplayPortion.RIGHT
else -> DisplayPortion.MIDDLE
}
} else /* MainPlayer.PlayerType.VIDEO */ {
when {
e.x < playerImpl.rootView.width / 3.0 -> DisplayPortion.LEFT
e.x > playerImpl.rootView.width * 2.0 / 3.0 -> DisplayPortion.RIGHT
else -> DisplayPortion.MIDDLE
}
}
}
// Currently needed for scrolling since there is no action more the middle portion
private fun getDisplayHalfPortion(e: MotionEvent): DisplayPortion {
return if (playerImpl.playerType == MainPlayer.PlayerType.POPUP) {
when {
e.x < playerImpl.popupWidth / 2.0 -> DisplayPortion.LEFT_HALF
else -> DisplayPortion.RIGHT_HALF
}
} else /* MainPlayer.PlayerType.VIDEO */ {
when {
e.x < playerImpl.rootView.width / 2.0 -> DisplayPortion.LEFT_HALF
else -> DisplayPortion.RIGHT_HALF
}
}
}
private fun getNavigationBarHeight(context: Context): Int {
val resId = context.resources
.getIdentifier("navigation_bar_height", "dimen", "android")
return if (resId > 0) {
context.resources.getDimensionPixelSize(resId)
} else 0
}
private fun getStatusBarHeight(context: Context): Int {
val resId = context.resources
.getIdentifier("status_bar_height", "dimen", "android")
return if (resId > 0) {
context.resources.getDimensionPixelSize(resId)
} else 0
}
companion object {
private const val TAG = "BasePlayerGestListener"
private val DEBUG = BasePlayer.DEBUG
private const val DOUBLE_TAP_DELAY = 550L
private const val MOVEMENT_THRESHOLD = 40
}
}

View File

@@ -42,6 +42,14 @@ public class CustomBottomSheetBehavior extends BottomSheetBehavior<FrameLayout>
return false;
}
// The interception listens for the child view with the id "fragment_player_holder",
// so the following two-finger gesture will be triggered only for the player view on
// portrait and for the top controls (visible) on landscape.
setSkipCollapsed(event.getPointerCount() == 2);
if (event.getPointerCount() == 2) {
return super.onInterceptTouchEvent(parent, child, event);
}
// Don't need to do anything if bottomSheet isn't expanded
if (getState() == BottomSheetBehavior.STATE_EXPANDED
&& event.getAction() == MotionEvent.ACTION_DOWN) {

View File

@@ -0,0 +1,5 @@
package org.schabi.newpipe.player.event
enum class DisplayPortion {
LEFT, MIDDLE, RIGHT, LEFT_HALF, RIGHT_HALF
}

View File

@@ -0,0 +1,7 @@
package org.schabi.newpipe.player.event
interface DoubleTapListener {
fun onDoubleTapStarted(portion: DisplayPortion) {}
fun onDoubleTapProgressDown(portion: DisplayPortion) {}
fun onDoubleTapFinished() {}
}

View File

@@ -1,16 +1,16 @@
package org.schabi.newpipe.player.event;
import android.app.Activity;
import android.content.Context;
import android.util.Log;
import android.view.GestureDetector;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.Window;
import android.view.WindowManager;
import android.widget.ProgressBar;
import androidx.appcompat.content.res.AppCompatResources;
import org.jetbrains.annotations.NotNull;
import org.schabi.newpipe.R;
import org.schabi.newpipe.player.BasePlayer;
import org.schabi.newpipe.player.MainPlayer;
@@ -23,217 +23,116 @@ import static org.schabi.newpipe.player.VideoPlayer.DEFAULT_CONTROLS_HIDE_TIME;
import static org.schabi.newpipe.util.AnimationUtils.Type.SCALE_AND_ALPHA;
import static org.schabi.newpipe.util.AnimationUtils.animateView;
/**
* GestureListener for the player
*
* While {@link BasePlayerGestureListener} contains the logic behind the single gestures
* this class focuses on the visual aspect like hiding and showing the controls or changing
* volume/brightness during scrolling for specific events.
*/
public class PlayerGestureListener
extends GestureDetector.SimpleOnGestureListener
extends BasePlayerGestureListener
implements View.OnTouchListener {
private static final String TAG = ".PlayerGestureListener";
private static final boolean DEBUG = BasePlayer.DEBUG;
private final VideoPlayerImpl playerImpl;
private final MainPlayer service;
private int initialPopupX;
private int initialPopupY;
private boolean isMovingInMain;
private boolean isMovingInPopup;
private boolean isResizing;
private final int tossFlingVelocity;
private final boolean isVolumeGestureEnabled;
private final boolean isBrightnessGestureEnabled;
private final int maxVolume;
private static final int MOVEMENT_THRESHOLD = 40;
// [popup] initial coordinates and distance between fingers
private double initPointerDistance = -1;
private float initFirstPointerX = -1;
private float initFirstPointerY = -1;
private float initSecPointerX = -1;
private float initSecPointerY = -1;
public PlayerGestureListener(final VideoPlayerImpl playerImpl, final MainPlayer service) {
this.playerImpl = playerImpl;
this.service = service;
this.tossFlingVelocity = PlayerHelper.getTossFlingVelocity(service);
super(playerImpl, service);
isVolumeGestureEnabled = PlayerHelper.isVolumeGestureEnabled(service);
isBrightnessGestureEnabled = PlayerHelper.isBrightnessGestureEnabled(service);
maxVolume = playerImpl.getAudioReactor().getMaxVolume();
}
/*//////////////////////////////////////////////////////////////////////////
// Helpers
//////////////////////////////////////////////////////////////////////////*/
/*
* Main and popup players' gesture listeners is too different.
* So it will be better to have different implementations of them
* */
@Override
public boolean onDoubleTap(final MotionEvent e) {
public void onDoubleTap(@NotNull final MotionEvent event,
@NotNull final DisplayPortion portion) {
if (DEBUG) {
Log.d(TAG, "onDoubleTap() called with: e = [" + e + "]" + "rawXy = "
+ e.getRawX() + ", " + e.getRawY() + ", xy = " + e.getX() + ", " + e.getY());
Log.d(TAG, "onDoubleTap called with playerType = ["
+ playerImpl.getPlayerType() + "], portion = ["
+ portion + "]");
}
if (playerImpl.isSomePopupMenuVisible()) {
playerImpl.hideControls(0, 0);
}
if (playerImpl.popupPlayerSelected()) {
return onDoubleTapInPopup(e);
} else {
return onDoubleTapInMain(e);
}
}
@Override
public boolean onSingleTapConfirmed(final MotionEvent e) {
if (DEBUG) {
Log.d(TAG, "onSingleTapConfirmed() called with: e = [" + e + "]");
}
if (playerImpl.popupPlayerSelected()) {
return onSingleTapConfirmedInPopup(e);
} else {
return onSingleTapConfirmedInMain(e);
}
}
@Override
public boolean onDown(final MotionEvent e) {
if (DEBUG) {
Log.d(TAG, "onDown() called with: e = [" + e + "]");
}
if (playerImpl.popupPlayerSelected()) {
return onDownInPopup(e);
} else {
return true;
}
}
@Override
public void onLongPress(final MotionEvent e) {
if (DEBUG) {
Log.d(TAG, "onLongPress() called with: e = [" + e + "]");
}
if (playerImpl.popupPlayerSelected()) {
onLongPressInPopup(e);
}
}
@Override
public boolean onScroll(final MotionEvent initialEvent, final MotionEvent movingEvent,
final float distanceX, final float distanceY) {
if (playerImpl.popupPlayerSelected()) {
return onScrollInPopup(initialEvent, movingEvent, distanceX, distanceY);
} else {
return onScrollInMain(initialEvent, movingEvent, distanceX, distanceY);
}
}
@Override
public boolean onFling(final MotionEvent e1, final MotionEvent e2,
final float velocityX, final float velocityY) {
if (DEBUG) {
Log.d(TAG, "onFling() called with velocity: dX=["
+ velocityX + "], dY=[" + velocityY + "]");
}
if (playerImpl.popupPlayerSelected()) {
return onFlingInPopup(e1, e2, velocityX, velocityY);
} else {
return true;
}
}
@Override
public boolean onTouch(final View v, final MotionEvent event) {
/*if (DEBUG && false) {
Log.d(TAG, "onTouch() called with: v = [" + v + "], event = [" + event + "]");
}*/
if (playerImpl.popupPlayerSelected()) {
return onTouchInPopup(v, event);
} else {
return onTouchInMain(v, event);
}
}
/*//////////////////////////////////////////////////////////////////////////
// Main player listener
//////////////////////////////////////////////////////////////////////////*/
private boolean onDoubleTapInMain(final MotionEvent e) {
if (e.getX() > playerImpl.getRootView().getWidth() * 2.0 / 3.0) {
playerImpl.onFastForward();
} else if (e.getX() < playerImpl.getRootView().getWidth() / 3.0) {
if (portion == DisplayPortion.LEFT) {
playerImpl.onFastRewind();
} else {
playerImpl.getPlayPauseButton().performClick();
} else if (portion == DisplayPortion.MIDDLE) {
playerImpl.onPlayPause();
} else if (portion == DisplayPortion.RIGHT) {
playerImpl.onFastForward();
}
return true;
}
private boolean onSingleTapConfirmedInMain(final MotionEvent e) {
@Override
public void onSingleTap(@NotNull final MainPlayer.PlayerType playerType) {
if (DEBUG) {
Log.d(TAG, "onSingleTapConfirmed() called with: e = [" + e + "]");
Log.d(TAG, "onSingleTap called with playerType = ["
+ playerImpl.getPlayerType() + "]");
}
if (playerType == MainPlayer.PlayerType.POPUP) {
if (playerImpl.getCurrentState() == BasePlayer.STATE_BLOCKED) {
return true;
}
if (playerImpl.isControlsVisible()) {
playerImpl.hideControls(150, 0);
} else {
if (playerImpl.getCurrentState() == BasePlayer.STATE_COMPLETED) {
playerImpl.showControls(0);
if (playerImpl.isControlsVisible()) {
playerImpl.hideControls(100, 100);
} else {
playerImpl.getPlayPauseButton().requestFocus();
playerImpl.showControlsThenHide();
}
} else /* playerType == MainPlayer.PlayerType.VIDEO */ {
if (playerImpl.isControlsVisible()) {
playerImpl.hideControls(150, 0);
} else {
if (playerImpl.getCurrentState() == BasePlayer.STATE_COMPLETED) {
playerImpl.showControls(0);
} else {
playerImpl.showControlsThenHide();
}
}
}
return true;
}
private boolean onScrollInMain(final MotionEvent initialEvent, final MotionEvent movingEvent,
final float distanceX, final float distanceY) {
if ((!isVolumeGestureEnabled && !isBrightnessGestureEnabled)
|| !playerImpl.isFullscreen()) {
return false;
@Override
public void onScroll(@NotNull final MainPlayer.PlayerType playerType,
@NotNull final DisplayPortion portion,
@NotNull final MotionEvent initialEvent,
@NotNull final MotionEvent movingEvent,
final float distanceX, final float distanceY) {
if (DEBUG) {
Log.d(TAG, "onScroll called with playerType = ["
+ playerImpl.getPlayerType() + "], portion = ["
+ portion + "]");
}
if (playerType == MainPlayer.PlayerType.VIDEO) {
if (portion == DisplayPortion.LEFT_HALF) {
onScrollMainBrightness(distanceX, distanceY);
final boolean isTouchingStatusBar = initialEvent.getY() < getStatusBarHeight(service);
final boolean isTouchingNavigationBar = initialEvent.getY()
> playerImpl.getRootView().getHeight() - getNavigationBarHeight(service);
if (isTouchingStatusBar || isTouchingNavigationBar) {
return false;
} else /* DisplayPortion.RIGHT_HALF */ {
onScrollMainVolume(distanceX, distanceY);
}
} else /* MainPlayer.PlayerType.POPUP */ {
final View closingOverlayView = playerImpl.getClosingOverlayView();
if (playerImpl.isInsideClosingRadius(movingEvent)) {
if (closingOverlayView.getVisibility() == View.GONE) {
animateView(closingOverlayView, true, 250);
}
} else {
if (closingOverlayView.getVisibility() == View.VISIBLE) {
animateView(closingOverlayView, false, 0);
}
}
}
}
/*if (DEBUG && false) Log.d(TAG, "onScrollInMain = " +
", e1.getRaw = [" + initialEvent.getRawX() + ", " + initialEvent.getRawY() + "]" +
", e2.getRaw = [" + movingEvent.getRawX() + ", " + movingEvent.getRawY() + "]" +
", distanceXy = [" + distanceX + ", " + distanceY + "]");*/
final boolean insideThreshold =
Math.abs(movingEvent.getY() - initialEvent.getY()) <= MOVEMENT_THRESHOLD;
if (!isMovingInMain && (insideThreshold || Math.abs(distanceX) > Math.abs(distanceY))
|| playerImpl.getCurrentState() == BasePlayer.STATE_COMPLETED) {
return false;
}
isMovingInMain = true;
final boolean acceptAnyArea = isVolumeGestureEnabled != isBrightnessGestureEnabled;
final boolean acceptVolumeArea = acceptAnyArea
|| initialEvent.getX() > playerImpl.getRootView().getWidth() / 2.0;
if (isVolumeGestureEnabled && acceptVolumeArea) {
private void onScrollMainVolume(final float distanceX, final float distanceY) {
if (isVolumeGestureEnabled) {
playerImpl.getVolumeProgressBar().incrementProgressBy((int) distanceY);
final float currentProgressPercent = (float) playerImpl
.getVolumeProgressBar().getProgress() / playerImpl.getMaxGestureLength();
@@ -258,10 +157,14 @@ public class PlayerGestureListener
if (playerImpl.getBrightnessRelativeLayout().getVisibility() == View.VISIBLE) {
playerImpl.getBrightnessRelativeLayout().setVisibility(View.GONE);
}
} else {
}
}
private void onScrollMainBrightness(final float distanceX, final float distanceY) {
if (isBrightnessGestureEnabled) {
final Activity parent = playerImpl.getParentActivity();
if (parent == null) {
return true;
return;
}
final Window window = parent.getWindow();
@@ -299,330 +202,71 @@ public class PlayerGestureListener
playerImpl.getVolumeRelativeLayout().setVisibility(View.GONE);
}
}
return true;
}
private void onScrollEndInMain() {
@Override
public void onScrollEnd(@NotNull final MainPlayer.PlayerType playerType,
@NotNull final MotionEvent event) {
if (DEBUG) {
Log.d(TAG, "onScrollEnd() called");
Log.d(TAG, "onScrollEnd called with playerType = ["
+ playerImpl.getPlayerType() + "]");
}
if (playerType == MainPlayer.PlayerType.VIDEO) {
if (DEBUG) {
Log.d(TAG, "onScrollEnd() called");
}
if (playerImpl.getVolumeRelativeLayout().getVisibility() == View.VISIBLE) {
animateView(playerImpl.getVolumeRelativeLayout(), SCALE_AND_ALPHA, false, 200, 200);
}
if (playerImpl.getBrightnessRelativeLayout().getVisibility() == View.VISIBLE) {
animateView(playerImpl.getBrightnessRelativeLayout(), SCALE_AND_ALPHA, false, 200, 200);
}
if (playerImpl.getVolumeRelativeLayout().getVisibility() == View.VISIBLE) {
animateView(playerImpl.getVolumeRelativeLayout(), SCALE_AND_ALPHA,
false, 200, 200);
}
if (playerImpl.getBrightnessRelativeLayout().getVisibility() == View.VISIBLE) {
animateView(playerImpl.getBrightnessRelativeLayout(), SCALE_AND_ALPHA,
false, 200, 200);
}
if (playerImpl.isControlsVisible() && playerImpl.getCurrentState() == STATE_PLAYING) {
playerImpl.hideControls(DEFAULT_CONTROLS_DURATION, DEFAULT_CONTROLS_HIDE_TIME);
if (playerImpl.isControlsVisible() && playerImpl.getCurrentState() == STATE_PLAYING) {
playerImpl.hideControls(DEFAULT_CONTROLS_DURATION, DEFAULT_CONTROLS_HIDE_TIME);
}
} else {
if (playerImpl == null) {
return;
}
if (playerImpl.isControlsVisible() && playerImpl.getCurrentState() == STATE_PLAYING) {
playerImpl.hideControls(DEFAULT_CONTROLS_DURATION, DEFAULT_CONTROLS_HIDE_TIME);
}
if (playerImpl.isInsideClosingRadius(event)) {
playerImpl.closePopup();
} else {
animateView(playerImpl.getClosingOverlayView(), false, 0);
if (!playerImpl.isPopupClosing) {
animateView(playerImpl.getCloseOverlayButton(), false, 200);
}
}
}
}
private boolean onTouchInMain(final View v, final MotionEvent event) {
playerImpl.getGestureDetector().onTouchEvent(event);
if (event.getAction() == MotionEvent.ACTION_UP && isMovingInMain) {
isMovingInMain = false;
onScrollEndInMain();
}
// This hack allows to stop receiving touch events on appbar
// while touching video player's view
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
case MotionEvent.ACTION_MOVE:
v.getParent().requestDisallowInterceptTouchEvent(playerImpl.isFullscreen());
return true;
case MotionEvent.ACTION_UP:
v.getParent().requestDisallowInterceptTouchEvent(false);
return false;
default:
return true;
}
}
/*//////////////////////////////////////////////////////////////////////////
// Popup player listener
//////////////////////////////////////////////////////////////////////////*/
private boolean onDoubleTapInPopup(final MotionEvent e) {
if (playerImpl == null || !playerImpl.isPlaying()) {
return false;
@Override
public void onPopupResizingStart() {
if (DEBUG) {
Log.d(TAG, "onPopupResizingStart called");
}
playerImpl.showAndAnimateControl(-1, true);
playerImpl.getLoadingPanel().setVisibility(View.GONE);
playerImpl.hideControls(0, 0);
if (e.getX() > playerImpl.getPopupWidth() / 2) {
playerImpl.onFastForward();
} else {
playerImpl.onFastRewind();
}
return true;
animateView(playerImpl.getCurrentDisplaySeek(), false, 0, 0);
animateView(playerImpl.getResizingIndicator(), true, 200, 0);
}
private boolean onSingleTapConfirmedInPopup(final MotionEvent e) {
if (playerImpl == null || playerImpl.getPlayer() == null) {
return false;
@Override
public void onPopupResizingEnd() {
if (DEBUG) {
Log.d(TAG, "onPopupResizingEnd called");
}
if (playerImpl.isControlsVisible()) {
playerImpl.hideControls(100, 100);
} else {
playerImpl.getPlayPauseButton().requestFocus();
playerImpl.showControlsThenHide();
}
return true;
}
private boolean onDownInPopup(final MotionEvent e) {
// Fix popup position when the user touch it, it may have the wrong one
// because the soft input is visible (the draggable area is currently resized).
playerImpl.updateScreenSize();
playerImpl.checkPopupPositionBounds();
initialPopupX = playerImpl.getPopupLayoutParams().x;
initialPopupY = playerImpl.getPopupLayoutParams().y;
playerImpl.setPopupWidth(playerImpl.getPopupLayoutParams().width);
playerImpl.setPopupHeight(playerImpl.getPopupLayoutParams().height);
return super.onDown(e);
}
private void onLongPressInPopup(final MotionEvent e) {
playerImpl.updateScreenSize();
playerImpl.checkPopupPositionBounds();
playerImpl.updatePopupSize((int) playerImpl.getScreenWidth(), -1);
}
private boolean onScrollInPopup(final MotionEvent initialEvent,
final MotionEvent movingEvent,
final float distanceX,
final float distanceY) {
if (isResizing || playerImpl == null) {
return super.onScroll(initialEvent, movingEvent, distanceX, distanceY);
}
if (!isMovingInPopup) {
animateView(playerImpl.getCloseOverlayButton(), true, 200);
}
isMovingInPopup = true;
final float diffX = (int) (movingEvent.getRawX() - initialEvent.getRawX());
float posX = (int) (initialPopupX + diffX);
final float diffY = (int) (movingEvent.getRawY() - initialEvent.getRawY());
float posY = (int) (initialPopupY + diffY);
if (posX > (playerImpl.getScreenWidth() - playerImpl.getPopupWidth())) {
posX = (int) (playerImpl.getScreenWidth() - playerImpl.getPopupWidth());
} else if (posX < 0) {
posX = 0;
}
if (posY > (playerImpl.getScreenHeight() - playerImpl.getPopupHeight())) {
posY = (int) (playerImpl.getScreenHeight() - playerImpl.getPopupHeight());
} else if (posY < 0) {
posY = 0;
}
playerImpl.getPopupLayoutParams().x = (int) posX;
playerImpl.getPopupLayoutParams().y = (int) posY;
final View closingOverlayView = playerImpl.getClosingOverlayView();
if (playerImpl.isInsideClosingRadius(movingEvent)) {
if (closingOverlayView.getVisibility() == View.GONE) {
animateView(closingOverlayView, true, 250);
}
} else {
if (closingOverlayView.getVisibility() == View.VISIBLE) {
animateView(closingOverlayView, false, 0);
}
}
// if (DEBUG) {
// Log.d(TAG, "onScrollInPopup = "
// + "e1.getRaw = [" + initialEvent.getRawX() + ", "
// + initialEvent.getRawY() + "], "
// + "e1.getX,Y = [" + initialEvent.getX() + ", "
// + initialEvent.getY() + "], "
// + "e2.getRaw = [" + movingEvent.getRawX() + ", "
// + movingEvent.getRawY() + "], "
// + "e2.getX,Y = [" + movingEvent.getX() + ", " + movingEvent.getY() + "], "
// + "distanceX,Y = [" + distanceX + ", " + distanceY + "], "
// + "posX,Y = [" + posX + ", " + posY + "], "
// + "popupW,H = [" + popupWidth + " x " + popupHeight + "]");
// }
playerImpl.windowManager
.updateViewLayout(playerImpl.getRootView(), playerImpl.getPopupLayoutParams());
return true;
}
private void onScrollEndInPopup(final MotionEvent event) {
if (playerImpl == null) {
return;
}
if (playerImpl.isControlsVisible() && playerImpl.getCurrentState() == STATE_PLAYING) {
playerImpl.hideControls(DEFAULT_CONTROLS_DURATION, DEFAULT_CONTROLS_HIDE_TIME);
}
if (playerImpl.isInsideClosingRadius(event)) {
playerImpl.closePopup();
} else {
animateView(playerImpl.getClosingOverlayView(), false, 0);
if (!playerImpl.isPopupClosing) {
animateView(playerImpl.getCloseOverlayButton(), false, 200);
}
}
}
private boolean onFlingInPopup(final MotionEvent e1,
final MotionEvent e2,
final float velocityX,
final float velocityY) {
if (playerImpl == null) {
return false;
}
final float absVelocityX = Math.abs(velocityX);
final float absVelocityY = Math.abs(velocityY);
if (Math.max(absVelocityX, absVelocityY) > tossFlingVelocity) {
if (absVelocityX > tossFlingVelocity) {
playerImpl.getPopupLayoutParams().x = (int) velocityX;
}
if (absVelocityY > tossFlingVelocity) {
playerImpl.getPopupLayoutParams().y = (int) velocityY;
}
playerImpl.checkPopupPositionBounds();
playerImpl.windowManager
.updateViewLayout(playerImpl.getRootView(), playerImpl.getPopupLayoutParams());
return true;
}
return false;
}
private boolean onTouchInPopup(final View v, final MotionEvent event) {
if (playerImpl == null) {
return false;
}
playerImpl.getGestureDetector().onTouchEvent(event);
if (event.getPointerCount() == 2 && !isMovingInPopup && !isResizing) {
if (DEBUG) {
Log.d(TAG, "onTouch() 2 finger pointer detected, enabling resizing.");
}
playerImpl.showAndAnimateControl(-1, true);
playerImpl.getLoadingPanel().setVisibility(View.GONE);
playerImpl.hideControls(0, 0);
animateView(playerImpl.getCurrentDisplaySeek(), false, 0, 0);
animateView(playerImpl.getResizingIndicator(), true, 200, 0);
//record coordinates of fingers
initFirstPointerX = event.getX(0);
initFirstPointerY = event.getY(0);
initSecPointerX = event.getX(1);
initSecPointerY = event.getY(1);
//record distance between fingers
initPointerDistance = Math.hypot(initFirstPointerX - initSecPointerX,
initFirstPointerY - initSecPointerY);
isResizing = true;
}
if (event.getAction() == MotionEvent.ACTION_MOVE && !isMovingInPopup && isResizing) {
if (DEBUG) {
Log.d(TAG, "onTouch() ACTION_MOVE > v = [" + v + "], "
+ "e1.getRaw = [" + event.getRawX() + ", " + event.getRawY() + "]");
}
return handleMultiDrag(event);
}
if (event.getAction() == MotionEvent.ACTION_UP) {
if (DEBUG) {
Log.d(TAG, "onTouch() ACTION_UP > v = [" + v + "], "
+ "e1.getRaw = [" + event.getRawX() + ", " + event.getRawY() + "]");
}
if (isMovingInPopup) {
isMovingInPopup = false;
onScrollEndInPopup(event);
}
if (isResizing) {
isResizing = false;
initPointerDistance = -1;
initFirstPointerX = -1;
initFirstPointerY = -1;
initSecPointerX = -1;
initSecPointerY = -1;
animateView(playerImpl.getResizingIndicator(), false, 100, 0);
playerImpl.changeState(playerImpl.getCurrentState());
}
if (!playerImpl.isPopupClosing) {
playerImpl.savePositionAndSize();
}
}
v.performClick();
return true;
}
private boolean handleMultiDrag(final MotionEvent event) {
if (initPointerDistance != -1 && event.getPointerCount() == 2) {
// get the movements of the fingers
final double firstPointerMove = Math.hypot(event.getX(0) - initFirstPointerX,
event.getY(0) - initFirstPointerY);
final double secPointerMove = Math.hypot(event.getX(1) - initSecPointerX,
event.getY(1) - initSecPointerY);
// minimum threshold beyond which pinch gesture will work
final int minimumMove = ViewConfiguration.get(service).getScaledTouchSlop();
if (Math.max(firstPointerMove, secPointerMove) > minimumMove) {
// calculate current distance between the pointers
final double currentPointerDistance =
Math.hypot(event.getX(0) - event.getX(1),
event.getY(0) - event.getY(1));
final double popupWidth = playerImpl.getPopupWidth();
// change co-ordinates of popup so the center stays at the same position
final double newWidth = (popupWidth * currentPointerDistance / initPointerDistance);
initPointerDistance = currentPointerDistance;
playerImpl.getPopupLayoutParams().x += (popupWidth - newWidth) / 2;
playerImpl.checkPopupPositionBounds();
playerImpl.updateScreenSize();
playerImpl.updatePopupSize(
(int) Math.min(playerImpl.getScreenWidth(), newWidth),
-1);
return true;
}
}
return false;
}
/*
* Utils
* */
private int getNavigationBarHeight(final Context context) {
final int resId = context.getResources()
.getIdentifier("navigation_bar_height", "dimen", "android");
if (resId > 0) {
return context.getResources().getDimensionPixelSize(resId);
}
return 0;
}
private int getStatusBarHeight(final Context context) {
final int resId = context.getResources()
.getIdentifier("status_bar_height", "dimen", "android");
if (resId > 0) {
return context.getResources().getDimensionPixelSize(resId);
}
return 0;
animateView(playerImpl.getResizingIndicator(), false, 100, 0);
}
}

View File

@@ -12,6 +12,7 @@ import android.os.Build;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.core.content.ContextCompat;
import com.google.android.exoplayer2.SimpleExoPlayer;
import com.google.android.exoplayer2.analytics.AnalyticsListener;
@@ -39,7 +40,7 @@ public class AudioReactor implements AudioManager.OnAudioFocusChangeListener, An
@NonNull final SimpleExoPlayer player) {
this.player = player;
this.context = context;
this.audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
this.audioManager = ContextCompat.getSystemService(context, AudioManager.class);
player.addAnalyticsListener(this);
if (SHOULD_BUILD_FOCUS_REQUEST) {
@@ -163,7 +164,7 @@ public class AudioReactor implements AudioManager.OnAudioFocusChangeListener, An
@Override
public void onAudioSessionId(final EventTime eventTime, final int audioSessionId) {
if (!PlayerHelper.isUsingDSP(context)) {
if (!PlayerHelper.isUsingDSP()) {
return;
}

View File

@@ -5,8 +5,7 @@ import android.net.wifi.WifiManager;
import android.os.PowerManager;
import android.util.Log;
import static android.content.Context.POWER_SERVICE;
import static android.content.Context.WIFI_SERVICE;
import androidx.core.content.ContextCompat;
public class LockManager {
private final String TAG = "LockManager@" + hashCode();
@@ -18,10 +17,9 @@ public class LockManager {
private WifiManager.WifiLock wifiLock;
public LockManager(final Context context) {
powerManager = ((PowerManager) context.getApplicationContext()
.getSystemService(POWER_SERVICE));
wifiManager = ((WifiManager) context.getApplicationContext()
.getSystemService(WIFI_SERVICE));
powerManager = ContextCompat.getSystemService(context.getApplicationContext(),
PowerManager.class);
wifiManager = ContextCompat.getSystemService(context, WifiManager.class);
}
public void acquireWifiAndCpu() {

View File

@@ -8,6 +8,7 @@ import android.view.accessibility.CaptioningManager;
import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat;
import androidx.preference.PreferenceManager;
import com.google.android.exoplayer2.SeekParameters;
@@ -83,12 +84,12 @@ public final class PlayerHelper {
final int days = (milliSeconds % (86400000 * 7)) / 86400000;
STRING_BUILDER.setLength(0);
return days > 0
return (days > 0
? STRING_FORMATTER.format("%d:%02d:%02d:%02d", days, hours, minutes, seconds)
.toString()
: hours > 0
? STRING_FORMATTER.format("%d:%02d:%02d", hours, minutes, seconds).toString()
: STRING_FORMATTER.format("%02d:%02d", minutes, seconds).toString();
? STRING_FORMATTER.format("%d:%02d:%02d", hours, minutes, seconds)
: STRING_FORMATTER.format("%02d:%02d", minutes, seconds)
).toString();
}
public static String formatSpeed(final double speed) {
@@ -298,7 +299,7 @@ public final class PlayerHelper {
return 60000;
}
public static TrackSelection.Factory getQualitySelector(@NonNull final Context context) {
public static TrackSelection.Factory getQualitySelector() {
return new AdaptiveTrackSelection.Factory(
1000,
AdaptiveTrackSelection.DEFAULT_MAX_DURATION_FOR_QUALITY_DECREASE_MS,
@@ -306,18 +307,18 @@ public final class PlayerHelper {
AdaptiveTrackSelection.DEFAULT_BANDWIDTH_FRACTION);
}
public static boolean isUsingDSP(@NonNull final Context context) {
public static boolean isUsingDSP() {
return true;
}
public static int getTossFlingVelocity(@NonNull final Context context) {
public static int getTossFlingVelocity() {
return 2500;
}
@NonNull
public static CaptionStyleCompat getCaptionStyle(@NonNull final Context context) {
final CaptioningManager captioningManager = (CaptioningManager)
context.getSystemService(Context.CAPTIONING_SERVICE);
final CaptioningManager captioningManager = ContextCompat.getSystemService(context,
CaptioningManager.class);
if (captioningManager == null || !captioningManager.isEnabled()) {
return CaptionStyleCompat.DEFAULT;
}
@@ -340,8 +341,8 @@ public final class PlayerHelper {
* @return caption scaling
*/
public static float getCaptionScale(@NonNull final Context context) {
final CaptioningManager captioningManager
= (CaptioningManager) context.getSystemService(Context.CAPTIONING_SERVICE);
final CaptioningManager captioningManager = ContextCompat.getSystemService(context,
CaptioningManager.class);
if (captioningManager == null || !captioningManager.isEnabled()) {
return 1.0f;
}

View File

@@ -6,8 +6,12 @@ import android.content.Intent;
import android.content.ServiceConnection;
import android.os.IBinder;
import android.util.Log;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.PlaybackParameters;
import org.schabi.newpipe.App;
import org.schabi.newpipe.MainActivity;
import org.schabi.newpipe.extractor.stream.StreamInfo;
@@ -31,6 +35,31 @@ public final class PlayerHolder {
private static MainPlayer playerService;
private static VideoPlayerImpl player;
/**
* Returns the current {@link MainPlayer.PlayerType} of the {@link MainPlayer} service,
* otherwise `null` if no service running.
*
* @return Current PlayerType
*/
@Nullable
public static MainPlayer.PlayerType getType() {
if (player == null) {
return null;
}
return player.getPlayerType();
}
public static boolean isPlaying() {
if (player == null) {
return false;
}
return player.isPlaying();
}
public static boolean isPlayerOpen() {
return player != null;
}
public static void setListener(final PlayerServiceExtendedEventListener newListener) {
listener = newListener;
// Force reload data from service

View File

@@ -1,12 +1,8 @@
package org.schabi.newpipe.player.playqueue;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.reactivestreams.Subscriber;
import org.reactivestreams.Subscription;
import org.schabi.newpipe.MainActivity;
import org.schabi.newpipe.player.playqueue.events.AppendEvent;
import org.schabi.newpipe.player.playqueue.events.ErrorEvent;
@@ -43,7 +39,6 @@ import io.reactivex.subjects.BehaviorSubject;
* </p>
*/
public abstract class PlayQueue implements Serializable {
private final String TAG = "PlayQueue@" + Integer.toHexString(hashCode());
public static final boolean DEBUG = MainActivity.DEBUG;
private ArrayList<PlayQueueItem> backup;
@@ -55,7 +50,6 @@ public abstract class PlayQueue implements Serializable {
private transient BehaviorSubject<PlayQueueEvent> eventBroadcast;
private transient Flowable<PlayQueueEvent> broadcastReceiver;
private transient Subscription reportingReactor;
private transient boolean disposed;
@@ -87,10 +81,6 @@ public abstract class PlayQueue implements Serializable {
broadcastReceiver = eventBroadcast.toFlowable(BackpressureStrategy.BUFFER)
.observeOn(AndroidSchedulers.mainThread())
.startWith(new InitEvent());
if (DEBUG) {
broadcastReceiver.subscribe(getSelfReporter());
}
}
/**
@@ -100,13 +90,9 @@ public abstract class PlayQueue implements Serializable {
if (eventBroadcast != null) {
eventBroadcast.onComplete();
}
if (reportingReactor != null) {
reportingReactor.cancel();
}
eventBroadcast = null;
broadcastReceiver = null;
reportingReactor = null;
disposed = true;
}
@@ -167,19 +153,20 @@ public abstract class PlayQueue implements Serializable {
}
/**
* @return the current item that should be played
* @return the current item that should be played, or null if the queue is empty
*/
@Nullable
public PlayQueueItem getItem() {
return getItem(getIndex());
}
/**
* @param index the index of the item to return
* @return the item at the given index
* @throws IndexOutOfBoundsException
* @return the item at the given index, or null if the index is out of bounds
*/
@Nullable
public PlayQueueItem getItem(final int index) {
if (index < 0 || index >= streams.size() || streams.get(index) == null) {
if (index < 0 || index >= streams.size()) {
return null;
}
return streams.get(index);
@@ -543,35 +530,5 @@ public abstract class PlayQueue implements Serializable {
eventBroadcast.onNext(event);
}
}
private Subscriber<PlayQueueEvent> getSelfReporter() {
return new Subscriber<PlayQueueEvent>() {
@Override
public void onSubscribe(final Subscription s) {
if (reportingReactor != null) {
reportingReactor.cancel();
}
reportingReactor = s;
reportingReactor.request(1);
}
@Override
public void onNext(final PlayQueueEvent event) {
Log.d(TAG, "Received broadcast: " + event.type().name() + ". "
+ "Current index: " + getIndex() + ", play queue length: " + size() + ".");
reportingReactor.request(1);
}
@Override
public void onError(final Throwable t) {
Log.e(TAG, "Received broadcast error", t);
}
@Override
public void onComplete() {
Log.d(TAG, "Broadcast is shutting down.");
}
};
}
}

View File

@@ -64,6 +64,20 @@ public class PlayQueueItem implements Serializable {
this.recoveryPosition = RECOVERY_UNSET;
}
@Override
public boolean equals(final Object o) {
if (o instanceof PlayQueueItem) {
return url.equals(((PlayQueueItem) o).url);
} else {
return false;
}
}
@Override
public int hashCode() {
return url.hashCode();
}
@NonNull
public String getTitle() {
return title;

View File

@@ -52,7 +52,6 @@ public class PlayQueueItemBuilder {
return false;
});
holder.itemThumbnailView.setOnTouchListener(getOnTouchListener(holder));
holder.itemHandle.setOnTouchListener(getOnTouchListener(holder));
}

View File

@@ -7,13 +7,13 @@ import android.content.DialogInterface;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Bundle;
import androidx.preference.PreferenceManager;
import android.util.Log;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.preference.Preference;
import androidx.preference.PreferenceManager;
import com.nononsenseapps.filepicker.Utils;
import com.nostra13.universalimageloader.core.ImageLoader;
@@ -21,6 +21,7 @@ import com.nostra13.universalimageloader.core.ImageLoader;
import org.schabi.newpipe.DownloaderImpl;
import org.schabi.newpipe.NewPipeDatabase;
import org.schabi.newpipe.R;
import org.schabi.newpipe.ReCaptchaActivity;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.localization.ContentCountry;
import org.schabi.newpipe.extractor.localization.Localization;
@@ -75,6 +76,22 @@ public class ContentSettingsFragment extends BasePreferenceFragment {
.getPreferredContentCountry(requireContext());
initialLanguage = PreferenceManager
.getDefaultSharedPreferences(requireContext()).getString("app_language_key", "en");
final Preference clearCookiePref = findPreference(getString(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);
}
}
@Override

View File

@@ -1,12 +0,0 @@
package org.schabi.newpipe.settings;
import android.os.Bundle;
import org.schabi.newpipe.R;
public class DebugSettingsFragment extends BasePreferenceFragment {
@Override
public void onCreatePreferences(final Bundle savedInstanceState, final String rootKey) {
addPreferencesFromResource(R.xml.debug_settings);
}
}

View File

@@ -55,7 +55,7 @@ public class HistorySettingsFragment extends BasePreferenceFragment {
.setNegativeButton(R.string.cancel, ((dialog, which) -> dialog.dismiss()))
.setPositiveButton(R.string.delete, ((dialog, which) -> {
final Disposable onDeletePlaybackStates
= recordManager.deleteCompelteStreamStateHistory()
= recordManager.deleteCompleteStreamStateHistory()
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
howManyDeleted -> Toast.makeText(getActivity(),
@@ -113,7 +113,7 @@ public class HistorySettingsFragment extends BasePreferenceFragment {
.setPositiveButton(R.string.delete, ((dialog, which) -> {
final Disposable onDeletePlaybackStates
= recordManager.deleteCompelteStreamStateHistory()
= recordManager.deleteCompleteStreamStateHistory()
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
howManyDeleted -> Toast.makeText(getActivity(),

View File

@@ -4,7 +4,8 @@ import android.os.Bundle;
import androidx.preference.Preference;
import org.schabi.newpipe.CheckForNewAppVersionTask;
import org.schabi.newpipe.App;
import org.schabi.newpipe.CheckForNewAppVersion;
import org.schabi.newpipe.MainActivity;
import org.schabi.newpipe.R;
@@ -15,16 +16,11 @@ public class MainSettingsFragment extends BasePreferenceFragment {
public void onCreatePreferences(final Bundle savedInstanceState, final String rootKey) {
addPreferencesFromResource(R.xml.main_settings);
if (!CheckForNewAppVersionTask.isGithubApk()) {
if (!CheckForNewAppVersion.isGithubApk(App.getApp())) {
final Preference update = findPreference(getString(R.string.update_pref_screen_key));
getPreferenceScreen().removePreference(update);
defaultPreferences.edit().putBoolean(getString(R.string.update_app_key), false).apply();
}
if (!DEBUG) {
final Preference debug = findPreference(getString(R.string.debug_pref_screen_key));
getPreferenceScreen().removePreference(debug);
}
}
}

View File

@@ -0,0 +1,19 @@
package org.schabi.newpipe.settings
import android.os.Build
import android.os.Bundle
import androidx.preference.Preference
import org.schabi.newpipe.R
class NotificationSettingsFragment : BasePreferenceFragment() {
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
addPreferencesFromResource(R.xml.notification_settings)
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
val colorizePref: Preference? = findPreference(getString(R.string.notification_colorize_key))
colorizePref?.let {
preferenceScreen.removePreference(it)
}
}
}
}

View File

@@ -1,7 +1,6 @@
package org.schabi.newpipe.settings;
import android.app.Activity;
import android.content.DialogInterface;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
@@ -34,6 +33,7 @@ import java.util.List;
import java.util.Vector;
import io.reactivex.Flowable;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.Disposable;
public class SelectPlaylistFragment extends DialogFragment {
@@ -46,12 +46,11 @@ public class SelectPlaylistFragment extends DialogFragment {
private final ImageLoader imageLoader = ImageLoader.getInstance();
private OnSelectedListener onSelectedListener = null;
private OnCancelListener onCancelListener = null;
private ProgressBar progressBar;
private TextView emptyView;
private RecyclerView recyclerView;
private Disposable playlistsSubscriber;
private Disposable disposable = null;
private List<PlaylistLocalItem> playlists = new Vector<>();
@@ -59,10 +58,6 @@ public class SelectPlaylistFragment extends DialogFragment {
onSelectedListener = listener;
}
public void setOnCancelListener(final OnCancelListener listener) {
onCancelListener = listener;
}
/*//////////////////////////////////////////////////////////////////////////
// Fragment's Lifecycle
//////////////////////////////////////////////////////////////////////////*/
@@ -70,15 +65,32 @@ public class SelectPlaylistFragment extends DialogFragment {
@Override
public View onCreateView(@NonNull final LayoutInflater inflater, final ViewGroup container,
final Bundle savedInstanceState) {
final View v =
inflater.inflate(R.layout.select_playlist_fragment, container, false);
final View v = inflater.inflate(R.layout.select_playlist_fragment, container, false);
progressBar = v.findViewById(R.id.progressBar);
recyclerView = v.findViewById(R.id.items_list);
emptyView = v.findViewById(R.id.empty_state_view);
recyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
final SelectPlaylistAdapter playlistAdapter = new SelectPlaylistAdapter();
recyclerView.setAdapter(playlistAdapter);
progressBar = v.findViewById(R.id.progressBar);
emptyView = v.findViewById(R.id.empty_state_view);
loadPlaylists();
return v;
}
@Override
public void onDestroy() {
super.onDestroy();
if (disposable != null) {
disposable.dispose();
}
}
/*//////////////////////////////////////////////////////////////////////////
// Load and display playlists
//////////////////////////////////////////////////////////////////////////*/
private void loadPlaylists() {
progressBar.setVisibility(View.VISIBLE);
recyclerView.setVisibility(View.GONE);
emptyView.setVisibility(View.GONE);
@@ -87,43 +99,36 @@ public class SelectPlaylistFragment extends DialogFragment {
final LocalPlaylistManager localPlaylistManager = new LocalPlaylistManager(database);
final RemotePlaylistManager remotePlaylistManager = new RemotePlaylistManager(database);
playlistsSubscriber = Flowable.combineLatest(localPlaylistManager.getPlaylists(),
disposable = Flowable.combineLatest(localPlaylistManager.getPlaylists(),
remotePlaylistManager.getPlaylists(), PlaylistLocalItem::merge)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(this::displayPlaylists, this::onError);
return v;
}
@Override
public void onDestroy() {
super.onDestroy();
private void displayPlaylists(final List<PlaylistLocalItem> newPlaylists) {
playlists = newPlaylists;
progressBar.setVisibility(View.GONE);
emptyView.setVisibility(newPlaylists.isEmpty() ? View.VISIBLE : View.GONE);
recyclerView.setVisibility(newPlaylists.isEmpty() ? View.GONE : View.VISIBLE);
}
if (playlistsSubscriber != null) {
playlistsSubscriber.dispose();
playlistsSubscriber = null;
}
protected void onError(final Throwable e) {
final Activity activity = requireActivity();
ErrorActivity.reportError(activity, e, activity.getClass(), null, ErrorActivity.ErrorInfo
.make(UserAction.UI_ERROR, "none", "load_playlists", R.string.app_ui_crash));
}
/*//////////////////////////////////////////////////////////////////////////
// Handle actions
//////////////////////////////////////////////////////////////////////////*/
@Override
public void onCancel(final DialogInterface dialogInterface) {
super.onCancel(dialogInterface);
if (onCancelListener != null) {
onCancelListener.onCancel();
}
}
private void clickedItem(final int position) {
if (onSelectedListener != null) {
final LocalItem selectedItem = playlists.get(position);
if (selectedItem instanceof PlaylistMetadataEntry) {
final PlaylistMetadataEntry entry = ((PlaylistMetadataEntry) selectedItem);
onSelectedListener
.onLocalPlaylistSelected(entry.uid, entry.name);
onSelectedListener.onLocalPlaylistSelected(entry.uid, entry.name);
} else if (selectedItem instanceof PlaylistRemoteEntity) {
final PlaylistRemoteEntity entry = ((PlaylistRemoteEntity) selectedItem);
@@ -134,31 +139,6 @@ public class SelectPlaylistFragment extends DialogFragment {
dismiss();
}
/*//////////////////////////////////////////////////////////////////////////
// Item handling
//////////////////////////////////////////////////////////////////////////*/
private void displayPlaylists(final List<PlaylistLocalItem> newPlaylists) {
this.playlists = newPlaylists;
progressBar.setVisibility(View.GONE);
if (newPlaylists.isEmpty()) {
emptyView.setVisibility(View.VISIBLE);
return;
}
recyclerView.setVisibility(View.VISIBLE);
}
/*//////////////////////////////////////////////////////////////////////////
// Error
//////////////////////////////////////////////////////////////////////////*/
protected void onError(final Throwable e) {
final Activity activity = getActivity();
ErrorActivity.reportError(activity, e, activity.getClass(), null, ErrorActivity.ErrorInfo
.make(UserAction.UI_ERROR, "none", "", R.string.app_ui_crash));
}
/*//////////////////////////////////////////////////////////////////////////
// Interfaces
//////////////////////////////////////////////////////////////////////////*/
@@ -168,22 +148,20 @@ public class SelectPlaylistFragment extends DialogFragment {
void onRemotePlaylistSelected(int serviceId, String url, String name);
}
public interface OnCancelListener {
void onCancel();
}
private class SelectPlaylistAdapter
extends RecyclerView.Adapter<SelectPlaylistAdapter.SelectPlaylistItemHolder> {
@NonNull
@Override
public SelectPlaylistItemHolder onCreateViewHolder(final ViewGroup parent,
final int viewType) {
final int viewType) {
final View item = LayoutInflater.from(parent.getContext())
.inflate(R.layout.list_playlist_mini_item, parent, false);
return new SelectPlaylistItemHolder(item);
}
@Override
public void onBindViewHolder(final SelectPlaylistItemHolder holder, final int position) {
public void onBindViewHolder(@NonNull final SelectPlaylistItemHolder holder,
final int position) {
final PlaylistLocalItem selectedItem = playlists.get(position);
if (selectedItem instanceof PlaylistMetadataEntry) {

View File

@@ -1,12 +1,10 @@
package org.schabi.newpipe.settings;
package org.schabi.newpipe.settings.custom;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.graphics.PorterDuff;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
@@ -15,16 +13,16 @@ import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.RadioButton;
import android.widget.RadioGroup;
import android.widget.Switch;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.content.res.AppCompatResources;
import androidx.fragment.app.Fragment;
import androidx.core.graphics.drawable.DrawableCompat;
import androidx.core.widget.TextViewCompat;
import androidx.preference.Preference;
import androidx.preference.PreferenceViewHolder;
import java.util.List;
import org.schabi.newpipe.R;
import org.schabi.newpipe.player.MainPlayer;
import org.schabi.newpipe.player.NotificationConstants;
@@ -32,56 +30,35 @@ import org.schabi.newpipe.util.DeviceUtils;
import org.schabi.newpipe.util.ThemeHelper;
import org.schabi.newpipe.views.FocusOverlayView;
import java.util.List;
public class NotificationActionsPreference extends Preference {
public NotificationActionsPreference(final Context context, final AttributeSet attrs) {
super(context, attrs);
setLayoutResource(R.layout.settings_notification);
}
public class NotificationSettingsFragment extends Fragment {
private Switch scaleSwitch;
private NotificationSlot[] notificationSlots;
private SharedPreferences pref;
private List<Integer> compactSlots;
private String scaleKey;
////////////////////////////////////////////////////////////////////////////
// Lifecycle
////////////////////////////////////////////////////////////////////////////
@Override
public void onCreate(@Nullable final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
pref = PreferenceManager.getDefaultSharedPreferences(requireContext());
scaleKey = getString(R.string.scale_to_square_image_in_notifications_key);
public void onBindViewHolder(final PreferenceViewHolder holder) {
super.onBindViewHolder(holder);
holder.itemView.setClickable(false);
setupActions(holder.itemView);
}
@Override
public View onCreateView(@NonNull final LayoutInflater inflater,
final ViewGroup container,
@Nullable final Bundle savedInstanceState) {
return inflater.inflate(R.layout.settings_notification, container, false);
}
@Override
public void onViewCreated(@NonNull final View rootView,
@Nullable final Bundle savedInstanceState) {
super.onViewCreated(rootView, savedInstanceState);
setupScaleSwitch(rootView);
setupActions(rootView);
}
@Override
public void onResume() {
super.onResume();
ThemeHelper.setTitleToAppCompatActivity(getActivity(),
getString(R.string.settings_category_notification_title));
}
@Override
public void onPause() {
super.onPause();
public void onDetached() {
super.onDetached();
saveChanges();
requireContext().sendBroadcast(new Intent(MainPlayer.ACTION_RECREATE_NOTIFICATION));
getContext().sendBroadcast(new Intent(MainPlayer.ACTION_RECREATE_NOTIFICATION));
}
@@ -89,17 +66,10 @@ public class NotificationSettingsFragment extends Fragment {
// Setup
////////////////////////////////////////////////////////////////////////////
private void setupScaleSwitch(@NonNull final View view) {
scaleSwitch = view.findViewById(R.id.notificationScaleSwitch);
scaleSwitch.setChecked(pref.getBoolean(scaleKey, false));
view.findViewById(R.id.notificationScaleSwitchClickableArea)
.setOnClickListener(v -> scaleSwitch.toggle());
}
private void setupActions(@NonNull final View view) {
compactSlots =
NotificationConstants.getCompactSlotsFromPreferences(requireContext(), pref, 5);
NotificationConstants.getCompactSlotsFromPreferences(
getContext(), getSharedPreferences(), 5);
notificationSlots = new NotificationSlot[5];
for (int i = 0; i < 5; i++) {
notificationSlots[i] = new NotificationSlot(i, view);
@@ -112,16 +82,15 @@ public class NotificationSettingsFragment extends Fragment {
////////////////////////////////////////////////////////////////////////////
private void saveChanges() {
final SharedPreferences.Editor editor = pref.edit();
editor.putBoolean(scaleKey, scaleSwitch.isChecked());
final SharedPreferences.Editor editor = getSharedPreferences().edit();
for (int i = 0; i < 3; i++) {
editor.putInt(getString(NotificationConstants.SLOT_COMPACT_PREF_KEYS[i]),
editor.putInt(getContext().getString(NotificationConstants.SLOT_COMPACT_PREF_KEYS[i]),
(i < compactSlots.size() ? compactSlots.get(i) : -1));
}
for (int i = 0; i < 5; i++) {
editor.putInt(getString(NotificationConstants.SLOT_PREF_KEYS[i]),
editor.putInt(getContext().getString(NotificationConstants.SLOT_PREF_KEYS[i]),
notificationSlots[i].selectedAction);
}
@@ -183,7 +152,7 @@ public class NotificationSettingsFragment extends Fragment {
} else if (compactSlots.size() < 3) {
compactSlots.add(i);
} else {
Toast.makeText(requireContext(),
Toast.makeText(getContext(),
R.string.notification_actions_at_most_three,
Toast.LENGTH_SHORT).show();
return;
@@ -196,7 +165,8 @@ public class NotificationSettingsFragment extends Fragment {
void setupSelectedAction(final View view) {
icon = view.findViewById(R.id.notificationActionIcon);
summary = view.findViewById(R.id.notificationActionSummary);
selectedAction = pref.getInt(getString(NotificationConstants.SLOT_PREF_KEYS[i]),
selectedAction = getSharedPreferences().getInt(
getContext().getString(NotificationConstants.SLOT_PREF_KEYS[i]),
NotificationConstants.SLOT_DEFAULTS[i]);
updateInfo();
}
@@ -205,20 +175,20 @@ public class NotificationSettingsFragment extends Fragment {
if (NotificationConstants.ACTION_ICONS[selectedAction] == 0) {
icon.setImageDrawable(null);
} else {
icon.setImageDrawable(AppCompatResources.getDrawable(requireContext(),
icon.setImageDrawable(AppCompatResources.getDrawable(getContext(),
NotificationConstants.ACTION_ICONS[selectedAction]));
}
summary.setText(NotificationConstants.getActionName(requireContext(), selectedAction));
summary.setText(NotificationConstants.getActionName(getContext(), selectedAction));
}
void openActionChooserDialog() {
final LayoutInflater inflater = LayoutInflater.from(requireContext());
final LayoutInflater inflater = LayoutInflater.from(getContext());
final LinearLayout rootLayout = (LinearLayout) inflater.inflate(
R.layout.single_choice_dialog_view, null, false);
final RadioGroup radioGroup = rootLayout.findViewById(android.R.id.list);
final AlertDialog alertDialog = new AlertDialog.Builder(requireContext())
final AlertDialog alertDialog = new AlertDialog.Builder(getContext())
.setTitle(SLOT_TITLES[i])
.setView(radioGroup)
.setCancelable(true)
@@ -237,22 +207,19 @@ public class NotificationSettingsFragment extends Fragment {
// if present set action icon with correct color
if (NotificationConstants.ACTION_ICONS[action] != 0) {
final Drawable drawable = AppCompatResources.getDrawable(requireContext(),
Drawable drawable = AppCompatResources.getDrawable(getContext(),
NotificationConstants.ACTION_ICONS[action]);
if (drawable != null) {
final int color = ThemeHelper.resolveColorFromAttr(requireContext(),
final int color = ThemeHelper.resolveColorFromAttr(getContext(),
android.R.attr.textColorPrimary);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
drawable.setTint(color);
} else {
drawable.mutate().setColorFilter(color, PorterDuff.Mode.SRC_IN);
}
radioButton.setCompoundDrawablesWithIntrinsicBounds(
drawable = DrawableCompat.wrap(drawable).mutate();
DrawableCompat.setTint(drawable, color);
TextViewCompat.setCompoundDrawablesRelativeWithIntrinsicBounds(radioButton,
null, null, drawable, null);
}
}
radioButton.setText(NotificationConstants.getActionName(requireContext(), action));
radioButton.setText(NotificationConstants.getActionName(getContext(), action));
radioButton.setChecked(action == selectedAction);
radioButton.setId(id);
radioButton.setLayoutParams(new RadioGroup.LayoutParams(
@@ -262,7 +229,7 @@ public class NotificationSettingsFragment extends Fragment {
}
alertDialog.show();
if (DeviceUtils.isTv(requireContext())) {
if (DeviceUtils.isTv(getContext())) {
FocusOverlayView.setupFocusObserver(alertDialog);
}
}

View File

@@ -9,10 +9,9 @@ import android.os.Build;
import android.view.KeyEvent;
import androidx.annotation.NonNull;
import org.schabi.newpipe.App;
import androidx.core.content.ContextCompat;
import static android.content.Context.BATTERY_SERVICE;
import static android.content.Context.UI_MODE_SERVICE;
import org.schabi.newpipe.App;
public final class DeviceUtils {
@@ -30,15 +29,14 @@ public final class DeviceUtils {
final PackageManager pm = App.getApp().getPackageManager();
// from doc: https://developer.android.com/training/tv/start/hardware.html#runtime-check
boolean isTv = ((UiModeManager) context.getSystemService(UI_MODE_SERVICE))
boolean isTv = ContextCompat.getSystemService(context, UiModeManager.class)
.getCurrentModeType() == Configuration.UI_MODE_TYPE_TELEVISION
|| pm.hasSystemFeature(AMAZON_FEATURE_FIRE_TV)
|| pm.hasSystemFeature(PackageManager.FEATURE_TELEVISION);
// from https://stackoverflow.com/a/58932366
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
final boolean isBatteryAbsent
= ((BatteryManager) context.getSystemService(BATTERY_SERVICE))
final boolean isBatteryAbsent = context.getSystemService(BatteryManager.class)
.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY) == 0;
isTv = isTv || (isBatteryAbsent
&& !pm.hasSystemFeature(PackageManager.FEATURE_TOUCHSCREEN)

View File

@@ -293,8 +293,8 @@ public final class ExtractorHelper {
} else if (exception instanceof ContentNotSupportedException) {
Toast.makeText(context, R.string.content_not_supported, Toast.LENGTH_LONG).show();
} else {
final int errorId = exception instanceof YoutubeStreamExtractor.DecryptException
? R.string.youtube_signature_decryption_error
final int errorId = exception instanceof YoutubeStreamExtractor.DeobfuscateException
? R.string.youtube_signature_deobfuscation_error
: exception instanceof ParsingException
? R.string.parsing_error : R.string.general_error;
ErrorActivity.reportError(handler, context, exception, MainActivity.class, null,

View File

@@ -3,6 +3,8 @@ package org.schabi.newpipe.util;
import android.content.Context;
import android.content.SharedPreferences;
import android.net.ConnectivityManager;
import androidx.core.content.ContextCompat;
import androidx.preference.PreferenceManager;
import androidx.annotation.Nullable;
@@ -16,6 +18,7 @@ import org.schabi.newpipe.extractor.stream.VideoStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
@@ -263,10 +266,8 @@ public final class ListHelper {
*/
private static void sortStreamList(final List<VideoStream> videoStreams,
final boolean ascendingOrder) {
Collections.sort(videoStreams, (o1, o2) -> {
final int result = compareVideoStreamResolution(o1, o2);
return result == 0 ? 0 : (ascendingOrder ? result : -result);
});
final Comparator<VideoStream> comparator = ListHelper::compareVideoStreamResolution;
Collections.sort(videoStreams, ascendingOrder ? comparator : comparator.reversed());
}
/**
@@ -543,7 +544,7 @@ public final class ListHelper {
*/
public static boolean isMeteredNetwork(final Context context) {
final ConnectivityManager manager
= (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
= ContextCompat.getSystemService(context, ConnectivityManager.class);
if (manager == null || manager.getActiveNetworkInfo() == null) {
return false;
}

View File

@@ -5,13 +5,15 @@ import android.content.Context;
import android.content.SharedPreferences;
import android.content.res.Configuration;
import android.content.res.Resources;
import androidx.preference.PreferenceManager;
import android.icu.text.CompactDecimalFormat;
import android.os.Build;
import android.text.TextUtils;
import android.util.DisplayMetrics;
import androidx.annotation.NonNull;
import androidx.annotation.PluralsRes;
import androidx.annotation.StringRes;
import androidx.preference.PreferenceManager;
import org.ocpsoft.prettytime.PrettyTime;
import org.ocpsoft.prettytime.units.Decade;
@@ -21,11 +23,13 @@ import org.schabi.newpipe.extractor.localization.ContentCountry;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.text.DateFormat;
import java.text.NumberFormat;
import java.time.OffsetDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.time.format.FormatStyle;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Date;
import java.util.List;
import java.util.Locale;
@@ -137,13 +141,16 @@ public final class Localization {
return nf.format(number);
}
public static String formatDate(final Date date, final Context context) {
return DateFormat.getDateInstance(DateFormat.MEDIUM, getAppLocale(context)).format(date);
public static String formatDate(final OffsetDateTime offsetDateTime, final Context context) {
return DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM)
.withLocale(getAppLocale(context)).format(offsetDateTime
.atZoneSameInstant(ZoneId.systemDefault()));
}
@SuppressLint("StringFormatInvalid")
public static String localizeUploadDate(final Context context, final Date date) {
return context.getString(R.string.upload_date_text, formatDate(date, context));
public static String localizeUploadDate(final Context context,
final OffsetDateTime offsetDateTime) {
return context.getString(R.string.upload_date_text, formatDate(offsetDateTime, context));
}
public static String localizeViewCount(final Context context, final long viewCount) {
@@ -184,6 +191,11 @@ public final class Localization {
}
public static String shortCount(final Context context, final long count) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
return CompactDecimalFormat.getInstance(getAppLocale(context),
CompactDecimalFormat.CompactStyle.SHORT).format(count);
}
final double value = (double) count;
if (count >= 1000000000) {
return localizeNumber(context, round(value / 1000000000, 1))

View File

@@ -7,14 +7,14 @@ import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Build;
import androidx.preference.PreferenceManager;
import android.util.Log;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.content.ContextCompat;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentTransaction;
@@ -37,7 +37,6 @@ import org.schabi.newpipe.extractor.stream.VideoStream;
import org.schabi.newpipe.fragments.MainFragment;
import org.schabi.newpipe.fragments.detail.VideoDetailFragment;
import org.schabi.newpipe.fragments.list.channel.ChannelFragment;
import org.schabi.newpipe.fragments.list.comments.CommentsFragment;
import org.schabi.newpipe.fragments.list.kiosk.KioskFragment;
import org.schabi.newpipe.fragments.list.playlist.PlaylistFragment;
import org.schabi.newpipe.fragments.list.search.SearchFragment;
@@ -51,13 +50,14 @@ import org.schabi.newpipe.player.BackgroundPlayerActivity;
import org.schabi.newpipe.player.BasePlayer;
import org.schabi.newpipe.player.MainPlayer;
import org.schabi.newpipe.player.VideoPlayer;
import org.schabi.newpipe.player.helper.PlayerHelper;
import org.schabi.newpipe.player.helper.PlayerHolder;
import org.schabi.newpipe.player.playqueue.PlayQueue;
import org.schabi.newpipe.player.playqueue.PlayQueueItem;
import org.schabi.newpipe.settings.SettingsActivity;
import java.util.ArrayList;
@SuppressWarnings({"unused"})
public final class NavigationHelper {
public static final String MAIN_FRAGMENT_TAG = "main_fragment_tag";
public static final String SEARCH_FRAGMENT_TAG = "search_fragment_tag";
@@ -72,7 +72,6 @@ public final class NavigationHelper {
public static <T> Intent getPlayerIntent(@NonNull final Context context,
@NonNull final Class<T> targetClazz,
@Nullable final PlayQueue playQueue,
@Nullable final String quality,
final boolean resumePlayback) {
final Intent intent = new Intent(context, targetClazz);
@@ -82,9 +81,6 @@ public final class NavigationHelper {
intent.putExtra(VideoPlayer.PLAY_QUEUE_KEY, cacheKey);
}
}
if (quality != null) {
intent.putExtra(VideoPlayer.PLAYBACK_QUALITY, quality);
}
intent.putExtra(VideoPlayer.RESUME_PLAYBACK, resumePlayback);
intent.putExtra(VideoPlayer.PLAYER_TYPE, VideoPlayer.PLAYER_TYPE_VIDEO);
@@ -95,8 +91,10 @@ public final class NavigationHelper {
public static <T> Intent getPlayerIntent(@NonNull final Context context,
@NonNull final Class<T> targetClazz,
@Nullable final PlayQueue playQueue,
final boolean resumePlayback) {
return getPlayerIntent(context, targetClazz, playQueue, null, resumePlayback);
final boolean resumePlayback,
final boolean playWhenReady) {
return getPlayerIntent(context, targetClazz, playQueue, resumePlayback)
.putExtra(BasePlayer.PLAY_WHEN_READY, playWhenReady);
}
@NonNull
@@ -110,61 +108,25 @@ public final class NavigationHelper {
.putExtra(BasePlayer.SELECT_ON_APPEND, selectOnAppend);
}
@NonNull
public static <T> Intent getPlayerIntent(@NonNull final Context context,
@NonNull final Class<T> targetClazz,
@Nullable final PlayQueue playQueue,
final int repeatMode,
final float playbackSpeed,
final float playbackPitch,
final boolean playbackSkipSilence,
@Nullable final String playbackQuality,
final boolean resumePlayback,
final boolean startPaused,
final boolean isMuted) {
return getPlayerIntent(context, targetClazz, playQueue, playbackQuality, resumePlayback)
.putExtra(BasePlayer.REPEAT_MODE, repeatMode)
.putExtra(BasePlayer.START_PAUSED, startPaused)
.putExtra(BasePlayer.IS_MUTED, isMuted);
}
public static void playOnMainPlayer(final AppCompatActivity activity,
final PlayQueue queue,
final boolean autoPlay) {
playOnMainPlayer(activity.getSupportFragmentManager(), queue, autoPlay);
@NonNull final PlayQueue playQueue) {
final PlayQueueItem item = playQueue.getItem();
assert item != null;
openVideoDetailFragment(activity, activity.getSupportFragmentManager(),
item.getServiceId(), item.getUrl(), item.getTitle(), playQueue, false);
}
public static void playOnMainPlayer(final FragmentManager fragmentManager,
final PlayQueue queue,
final boolean autoPlay) {
final PlayQueueItem currentStream = queue.getItem();
openVideoDetailFragment(
fragmentManager,
currentStream.getServiceId(),
currentStream.getUrl(),
currentStream.getTitle(),
autoPlay,
queue);
public static void playOnMainPlayer(final Context context,
@NonNull final PlayQueue playQueue,
final boolean switchingPlayers) {
final PlayQueueItem item = playQueue.getItem();
assert item != null;
openVideoDetail(context,
item.getServiceId(), item.getUrl(), item.getTitle(), playQueue, switchingPlayers);
}
public static void playOnMainPlayer(@NonNull final Context context,
@Nullable final PlayQueue queue,
@NonNull final StreamingService.LinkType linkType,
@NonNull final String url,
@NonNull final String title,
final boolean autoPlay,
final boolean resumePlayback) {
final Intent intent = getPlayerIntent(context, MainActivity.class, queue, resumePlayback);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.putExtra(Constants.KEY_LINK_TYPE, linkType);
intent.putExtra(Constants.KEY_URL, url);
intent.putExtra(Constants.KEY_TITLE, title);
intent.putExtra(VideoDetailFragment.AUTO_PLAY, autoPlay);
context.startActivity(intent);
}
public static void playOnPopupPlayer(final Context context, final PlayQueue queue,
public static void playOnPopupPlayer(final Context context,
final PlayQueue queue,
final boolean resumePlayback) {
if (!PermissionHelper.isPopupEnabled(context)) {
PermissionHelper.showPopupEnablementToast(context);
@@ -174,7 +136,7 @@ public final class NavigationHelper {
Toast.makeText(context, R.string.popup_playing_toast, Toast.LENGTH_SHORT).show();
final Intent intent = getPlayerIntent(context, MainPlayer.class, queue, resumePlayback);
intent.putExtra(VideoPlayer.PLAYER_TYPE, VideoPlayer.PLAYER_TYPE_POPUP);
startService(context, intent);
ContextCompat.startForegroundService(context, intent);
}
public static void playOnBackgroundPlayer(final Context context,
@@ -184,7 +146,24 @@ public final class NavigationHelper {
.show();
final Intent intent = getPlayerIntent(context, MainPlayer.class, queue, resumePlayback);
intent.putExtra(VideoPlayer.PLAYER_TYPE, VideoPlayer.PLAYER_TYPE_AUDIO);
startService(context, intent);
ContextCompat.startForegroundService(context, intent);
}
public static void enqueueOnVideoPlayer(final Context context, final PlayQueue queue,
final boolean resumePlayback) {
enqueueOnVideoPlayer(context, queue, false, resumePlayback);
}
public static void enqueueOnVideoPlayer(final Context context, final PlayQueue queue,
final boolean selectOnAppend,
final boolean resumePlayback) {
Toast.makeText(context, R.string.enqueued, Toast.LENGTH_SHORT).show();
final Intent intent = getPlayerEnqueueIntent(
context, MainPlayer.class, queue, selectOnAppend, resumePlayback);
intent.putExtra(VideoPlayer.PLAYER_TYPE, VideoPlayer.PLAYER_TYPE_VIDEO);
ContextCompat.startForegroundService(context, intent);
}
public static void enqueueOnPopupPlayer(final Context context, final PlayQueue queue,
@@ -200,11 +179,11 @@ public final class NavigationHelper {
return;
}
Toast.makeText(context, R.string.popup_playing_append, Toast.LENGTH_SHORT).show();
Toast.makeText(context, R.string.enqueued, Toast.LENGTH_SHORT).show();
final Intent intent = getPlayerEnqueueIntent(
context, MainPlayer.class, queue, selectOnAppend, resumePlayback);
intent.putExtra(VideoPlayer.PLAYER_TYPE, VideoPlayer.PLAYER_TYPE_POPUP);
startService(context, intent);
ContextCompat.startForegroundService(context, intent);
}
public static void enqueueOnBackgroundPlayer(final Context context, final PlayQueue queue,
@@ -216,19 +195,11 @@ public final class NavigationHelper {
final PlayQueue queue,
final boolean selectOnAppend,
final boolean resumePlayback) {
Toast.makeText(context, R.string.background_player_append, Toast.LENGTH_SHORT).show();
Toast.makeText(context, R.string.enqueued, Toast.LENGTH_SHORT).show();
final Intent intent = getPlayerEnqueueIntent(
context, MainPlayer.class, queue, selectOnAppend, resumePlayback);
intent.putExtra(VideoPlayer.PLAYER_TYPE, VideoPlayer.PLAYER_TYPE_AUDIO);
startService(context, intent);
}
public static void startService(@NonNull final Context context, @NonNull final Intent intent) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
context.startForegroundService(intent);
} else {
context.startService(intent);
}
ContextCompat.startForegroundService(context, intent);
}
/*//////////////////////////////////////////////////////////////////////////
@@ -290,9 +261,6 @@ public final class NavigationHelper {
.setNegativeButton(R.string.cancel, (dialog, which)
-> Log.i("NavigationHelper", "You unlocked a secret unicorn."))
.show();
// Log.e("NavigationHelper",
// "Either no Streaming player for audio was installed, "
// + "or something important crashed:");
} else {
Toast.makeText(context, R.string.no_player_found_toast, Toast.LENGTH_LONG).show();
}
@@ -348,41 +316,6 @@ public final class NavigationHelper {
.commit();
}
public static void openVideoDetailFragment(final FragmentManager fragmentManager,
final int serviceId, final String url,
final String title) {
openVideoDetailFragment(fragmentManager, serviceId, url, title, true, null);
}
public static void openVideoDetailFragment(
final FragmentManager fragmentManager,
final int serviceId,
final String url,
final String title,
final boolean autoPlay,
final PlayQueue playQueue) {
final Fragment fragment = fragmentManager.findFragmentById(R.id.fragment_player_holder);
if (fragment instanceof VideoDetailFragment && fragment.isVisible()) {
expandMainPlayer(fragment.requireActivity());
final VideoDetailFragment detailFragment = (VideoDetailFragment) fragment;
detailFragment.setAutoplay(autoPlay);
detailFragment
.selectAndLoadVideo(serviceId, url, title == null ? "" : title, playQueue);
detailFragment.scrollToTop();
return;
}
final VideoDetailFragment instance = VideoDetailFragment
.getInstance(serviceId, url, title == null ? "" : title, playQueue);
instance.setAutoplay(autoPlay);
defaultTransaction(fragmentManager)
.replace(R.id.fragment_player_holder, instance)
.runOnCommit(() -> expandMainPlayer(instance.requireActivity()))
.commit();
}
public static void expandMainPlayer(final Context context) {
context.sendBroadcast(new Intent(VideoDetailFragment.ACTION_SHOW_MAIN_PLAYER));
}
@@ -396,36 +329,79 @@ public final class NavigationHelper {
defaultTransaction(fragmentManager)
.replace(R.id.fragment_player_holder, instance)
.runOnCommit(() -> sendPlayerStartedEvent(instance.requireActivity()))
.commit();
.commitAllowingStateLoss();
}
private interface RunnableWithVideoDetailFragment {
void run(VideoDetailFragment detailFragment);
}
public static void openVideoDetailFragment(@NonNull final Context context,
@NonNull final FragmentManager fragmentManager,
final int serviceId,
@Nullable final String url,
@NonNull final String title,
@Nullable final PlayQueue playQueue,
final boolean switchingPlayers) {
final boolean autoPlay;
@Nullable final MainPlayer.PlayerType playerType = PlayerHolder.getType();
if (playerType == null) {
// no player open
autoPlay = PlayerHelper.isAutoplayAllowedByUser(context);
} else if (switchingPlayers) {
// switching player to main player
autoPlay = PlayerHolder.isPlaying(); // keep play/pause state
} else if (playerType == MainPlayer.PlayerType.VIDEO) {
// opening new stream while already playing in main player
autoPlay = PlayerHelper.isAutoplayAllowedByUser(context);
} else {
// opening new stream while already playing in another player
autoPlay = false;
}
final RunnableWithVideoDetailFragment onVideoDetailFragmentReady = (detailFragment) -> {
expandMainPlayer(detailFragment.requireActivity());
detailFragment.setAutoPlay(autoPlay);
if (switchingPlayers) {
// Situation when user switches from players to main player. All needed data is
// here, we can start watching (assuming newQueue equals playQueue).
detailFragment.openVideoPlayer();
} else {
detailFragment.selectAndLoadVideo(serviceId, url, title, playQueue);
}
detailFragment.scrollToTop();
};
final Fragment fragment = fragmentManager.findFragmentById(R.id.fragment_player_holder);
if (fragment instanceof VideoDetailFragment && fragment.isVisible()) {
onVideoDetailFragmentReady.run((VideoDetailFragment) fragment);
} else {
final VideoDetailFragment instance = VideoDetailFragment
.getInstance(serviceId, url, title, playQueue);
instance.setAutoPlay(autoPlay);
defaultTransaction(fragmentManager)
.replace(R.id.fragment_player_holder, instance)
.runOnCommit(() -> onVideoDetailFragmentReady.run(instance))
.commit();
}
}
public static void openChannelFragment(final FragmentManager fragmentManager,
final int serviceId, final String url,
final String name) {
@NonNull final String name) {
defaultTransaction(fragmentManager)
.replace(R.id.fragment_holder, ChannelFragment.getInstance(serviceId, url,
name == null ? "" : name))
.addToBackStack(null)
.commit();
}
public static void openCommentsFragment(final FragmentManager fragmentManager,
final int serviceId, final String url,
final String name) {
fragmentManager.beginTransaction()
.setCustomAnimations(R.anim.switch_service_in, R.anim.switch_service_out)
.replace(R.id.fragment_holder, CommentsFragment.getInstance(serviceId, url,
name == null ? "" : name))
.replace(R.id.fragment_holder, ChannelFragment.getInstance(serviceId, url, name))
.addToBackStack(null)
.commit();
}
public static void openPlaylistFragment(final FragmentManager fragmentManager,
final int serviceId, final String url,
final String name) {
@NonNull final String name) {
defaultTransaction(fragmentManager)
.replace(R.id.fragment_holder, PlaylistFragment.getInstance(serviceId, url,
name == null ? "" : name))
.replace(R.id.fragment_holder, PlaylistFragment.getInstance(serviceId, url, name))
.addToBackStack(null)
.commit();
}
@@ -501,33 +477,26 @@ public final class NavigationHelper {
context.startActivity(mIntent);
}
public static void openChannel(final Context context, final int serviceId, final String url) {
openChannel(context, serviceId, url, null);
}
public static void openVideoDetail(final Context context,
final int serviceId,
final String url,
@NonNull final String title,
@Nullable final PlayQueue playQueue,
final boolean switchingPlayers) {
public static void openChannel(final Context context, final int serviceId,
final String url, final String name) {
final Intent openIntent = getOpenIntent(context, url, serviceId,
StreamingService.LinkType.CHANNEL);
if (name != null && !name.isEmpty()) {
openIntent.putExtra(Constants.KEY_TITLE, name);
}
context.startActivity(openIntent);
}
public static void openVideoDetail(final Context context, final int serviceId,
final String url) {
openVideoDetail(context, serviceId, url, null);
}
public static void openVideoDetail(final Context context, final int serviceId,
final String url, final String title) {
final Intent openIntent = getOpenIntent(context, url, serviceId,
final Intent intent = getOpenIntent(context, url, serviceId,
StreamingService.LinkType.STREAM);
if (title != null && !title.isEmpty()) {
openIntent.putExtra(Constants.KEY_TITLE, title);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.putExtra(Constants.KEY_TITLE, title);
intent.putExtra(VideoDetailFragment.KEY_SWITCHING_PLAYERS, switchingPlayers);
if (playQueue != null) {
final String cacheKey = SerializedCache.getInstance().put(playQueue, PlayQueue.class);
if (cacheKey != null) {
intent.putExtra(VideoPlayer.PLAY_QUEUE_KEY, cacheKey);
}
}
context.startActivity(openIntent);
context.startActivity(intent);
}
public static void openMainActivity(final Context context) {
@@ -540,7 +509,6 @@ public final class NavigationHelper {
public static void openRouterActivity(final Context context, final String url) {
final Intent mIntent = new Intent(context, RouterActivity.class);
mIntent.setData(Uri.parse(url));
mIntent.putExtra(RouterActivity.INTERNAL_ROUTE_KEY, true);
context.startActivity(mIntent);
}
@@ -554,14 +522,12 @@ public final class NavigationHelper {
context.startActivity(intent);
}
public static boolean openDownloads(final Activity activity) {
if (!PermissionHelper.checkStoragePermissions(
public static void openDownloads(final Activity activity) {
if (PermissionHelper.checkStoragePermissions(
activity, PermissionHelper.DOWNLOADS_REQUEST_CODE)) {
return false;
final Intent intent = new Intent(activity, DownloadActivity.class);
activity.startActivity(intent);
}
final Intent intent = new Intent(activity, DownloadActivity.class);
activity.startActivity(intent);
return true;
}
public static Intent getPlayQueueActivityIntent(final Context context) {
@@ -590,7 +556,8 @@ public final class NavigationHelper {
return getIntentByLink(context, NewPipe.getServiceByUrl(url), url);
}
public static Intent getIntentByLink(final Context context, final StreamingService service,
public static Intent getIntentByLink(final Context context,
final StreamingService service,
final String url) throws ExtractionException {
final StreamingService.LinkType linkType = service.getLinkTypeByUrl(url);
@@ -599,15 +566,7 @@ public final class NavigationHelper {
+ " url=" + url);
}
final Intent rIntent = getOpenIntent(context, url, service.getServiceId(), linkType);
if (linkType == StreamingService.LinkType.STREAM) {
rIntent.putExtra(VideoDetailFragment.AUTO_PLAY,
PreferenceManager.getDefaultSharedPreferences(context).getBoolean(
context.getString(R.string.autoplay_through_intent_key), false));
}
return rIntent;
return getOpenIntent(context, url, service.getServiceId(), linkType);
}
private static Uri openMarketUrl(final String packageName) {

View File

@@ -9,6 +9,8 @@ import android.content.pm.ResolveInfo;
import android.net.Uri;
import android.widget.Toast;
import androidx.core.content.ContextCompat;
import org.schabi.newpipe.R;
public final class ShareUtils {
@@ -95,7 +97,7 @@ public final class ShareUtils {
*/
public static void copyToClipboard(final Context context, final String text) {
final ClipboardManager clipboardManager =
(ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE);
ContextCompat.getSystemService(context, ClipboardManager.class);
if (clipboardManager == null) {
Toast.makeText(context,

View File

@@ -7,22 +7,41 @@ import androidx.fragment.app.Fragment;
import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
import org.schabi.newpipe.local.dialog.PlaylistAppendDialog;
import org.schabi.newpipe.local.dialog.PlaylistCreationDialog;
import org.schabi.newpipe.player.MainPlayer;
import org.schabi.newpipe.player.helper.PlayerHolder;
import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
import java.util.Collections;
import java.util.List;
import static org.schabi.newpipe.player.MainPlayer.PlayerType.AUDIO;
import static org.schabi.newpipe.player.MainPlayer.PlayerType.POPUP;
public enum StreamDialogEntry {
//////////////////////////////////////
// enum values with DEFAULT actions //
//////////////////////////////////////
enqueue_on_background(R.string.enqueue_on_background, (fragment, item) ->
NavigationHelper.enqueueOnBackgroundPlayer(fragment.getContext(),
new SinglePlayQueue(item), false)),
/**
* Enqueues the stream automatically to the current PlayerType.<br>
* <br>
* Info: Add this entry within showStreamDialog.
*/
enqueue(R.string.enqueue_stream, (fragment, item) -> {
final MainPlayer.PlayerType type = PlayerHolder.getType();
enqueue_on_popup(R.string.enqueue_on_popup, (fragment, item) ->
if (type == AUDIO) {
NavigationHelper.enqueueOnBackgroundPlayer(fragment.getContext(),
new SinglePlayQueue(item), false);
} else if (type == POPUP) {
NavigationHelper.enqueueOnPopupPlayer(fragment.getContext(),
new SinglePlayQueue(item), false)),
new SinglePlayQueue(item), false);
} else /* type == VIDEO */ {
NavigationHelper.enqueueOnVideoPlayer(fragment.getContext(),
new SinglePlayQueue(item), false);
}
}),
start_here_on_background(R.string.start_here_on_background, (fragment, item) ->
NavigationHelper.playOnBackgroundPlayer(fragment.getContext(),
@@ -40,8 +59,14 @@ public enum StreamDialogEntry {
append_playlist(R.string.append_playlist, (fragment, item) -> {
if (fragment.getFragmentManager() != null) {
PlaylistAppendDialog.fromStreamInfoItems(Collections.singletonList(item))
.show(fragment.getFragmentManager(), "StreamDialogEntry@append_playlist");
final PlaylistAppendDialog d = PlaylistAppendDialog
.fromStreamInfoItems(Collections.singletonList(item));
PlaylistAppendDialog.onPlaylistFound(fragment.getContext(),
() -> d.show(fragment.getFragmentManager(), "StreamDialogEntry@append_playlist"),
() -> PlaylistCreationDialog.newInstance(d)
.show(fragment.getFragmentManager(), "StreamDialogEntry@create_playlist")
);
}
}),
@@ -69,6 +94,10 @@ public enum StreamDialogEntry {
// non-static methods to initialize and edit entries //
///////////////////////////////////////////////////////
public static void setEnabledEntries(final List<StreamDialogEntry> entries) {
setEnabledEntries(entries.toArray(new StreamDialogEntry[0]));
}
/**
* To be called before using {@link #setCustomAction(StreamDialogEntryAction)}.
*

View File

@@ -1,8 +1,7 @@
package org.schabi.newpipe.views;
import android.content.Context;
import android.os.Build.VERSION;
import android.os.Build.VERSION_CODES;
import android.os.Build;
import android.util.AttributeSet;
import android.view.SurfaceView;
import com.google.android.exoplayer2.ui.AspectRatioFrameLayout;
@@ -47,7 +46,8 @@ public class ExpandableSurfaceView extends SurfaceView {
if (resizeMode == RESIZE_MODE_FIT
// KitKat doesn't work well when a view has a scale like needed for ZOOM
|| (resizeMode == RESIZE_MODE_ZOOM && VERSION.SDK_INT < VERSION_CODES.LOLLIPOP)) {
|| (resizeMode == RESIZE_MODE_ZOOM
&& Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP)) {
if (aspectDeformation > 0) {
height = (int) (width / videoAspectRatio);
} else {

View File

@@ -270,7 +270,7 @@ public final class FocusOverlayView extends Drawable implements
clearFocusObstacles((ViewGroup) decor);
}
@RequiresApi(api = 26)
@RequiresApi(api = Build.VERSION_CODES.O)
private static void clearFocusObstacles(final ViewGroup viewGroup) {
viewGroup.setTouchscreenBlocksFocus(false);

View File

@@ -35,6 +35,10 @@ public abstract class Mission implements Serializable {
*/
public StoredFileHelper storage;
public long getTimestamp() {
return timestamp;
}
/**
* Delete the downloaded file
*

View File

@@ -212,7 +212,7 @@ public class StoredDirectoryHelper {
@NonNull
@Override
public String toString() {
return docTree == null ? Uri.fromFile(ioTree).toString() : docTree.getUri().toString();
return (docTree == null ? Uri.fromFile(ioTree) : docTree.getUri()).toString();
}

View File

@@ -12,7 +12,8 @@ import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.Comparator;
import java.util.List;
import us.shandian.giga.get.DownloadMission;
import us.shandian.giga.get.FinishedMission;
@@ -198,7 +199,7 @@ public class DownloadManager {
}
if (mMissionsPending.size() > 1)
Collections.sort(mMissionsPending, (mission1, mission2) -> Long.compare(mission1.timestamp, mission2.timestamp));
Collections.sort(mMissionsPending, Comparator.comparingLong(Mission::getTimestamp));
}
/**
@@ -563,14 +564,10 @@ public class DownloadManager {
synchronized (DownloadManager.this) {
ArrayList<Mission> pending = new ArrayList<>(mMissionsPending);
ArrayList<Mission> finished = new ArrayList<>(mMissionsFinished);
ArrayList<Mission> remove = new ArrayList<>(hidden);
List<Mission> remove = new ArrayList<>(hidden);
// hide missions (if required)
Iterator<Mission> iterator = remove.iterator();
while (iterator.hasNext()) {
Mission mission = iterator.next();
if (pending.remove(mission) || finished.remove(mission)) iterator.remove();
}
remove.removeIf(mission -> pending.remove(mission) || finished.remove(mission));
int fakeTotal = pending.size();
if (fakeTotal > 0) fakeTotal++;

View File

@@ -24,6 +24,8 @@ import android.os.Handler.Callback;
import android.os.IBinder;
import android.os.Message;
import android.os.Parcelable;
import androidx.core.content.ContextCompat;
import androidx.preference.PreferenceManager;
import android.util.Log;
import android.util.SparseArray;
@@ -157,8 +159,10 @@ public class DownloadManagerService extends Service {
mNotification = builder.build();
mNotificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
mConnectivityManager = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
mNotificationManager = ContextCompat.getSystemService(this,
NotificationManager.class);
mConnectivityManager = ContextCompat.getSystemService(this,
ConnectivityManager.class);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
mNetworkStateListenerL = new ConnectivityManager.NetworkCallback() {

View File

@@ -1,13 +1,11 @@
package us.shandian.giga.ui.adapter;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.app.ProgressDialog;
import android.content.Context;
import android.content.Intent;
import android.graphics.Color;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Handler;
import android.os.Message;
@@ -26,7 +24,6 @@ import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import androidx.appcompat.app.AlertDialog;
import androidx.core.content.FileProvider;
@@ -46,12 +43,15 @@ import org.schabi.newpipe.report.UserAction;
import org.schabi.newpipe.util.NavigationHelper;
import java.io.File;
import java.lang.ref.WeakReference;
import java.net.URI;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import io.reactivex.Observable;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.CompositeDisposable;
import io.reactivex.schedulers.Schedulers;
import us.shandian.giga.get.DownloadMission;
import us.shandian.giga.get.FinishedMission;
import us.shandian.giga.get.Mission;
@@ -116,11 +116,13 @@ public class MissionAdapter extends Adapter<ViewHolder> implements Handler.Callb
private final Runnable rUpdater = this::updater;
private final Runnable rDelete = this::deleteFinishedDownloads;
private final CompositeDisposable compositeDisposable = new CompositeDisposable();
public MissionAdapter(Context context, @NonNull DownloadManager downloadManager, View emptyMessage, View root) {
mContext = context;
mDownloadManager = downloadManager;
mInflater = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
mInflater = LayoutInflater.from(mContext);
mLayout = R.layout.mission_item;
mHandler = new Handler(context.getMainLooper());
@@ -675,7 +677,30 @@ public class MissionAdapter extends Adapter<ViewHolder> implements Handler.Callb
return true;
case R.id.md5:
case R.id.sha1:
new ChecksumTask(mContext).execute(h.item.mission.storage, ALGORITHMS.get(id));
ProgressDialog progressDialog = null;
if (mContext != null) {
// Create dialog
progressDialog = new ProgressDialog(mContext);
progressDialog.setCancelable(false);
progressDialog.setMessage(mContext.getString(R.string.msg_wait));
progressDialog.show();
}
final ProgressDialog finalProgressDialog = progressDialog;
final StoredFileHelper storage = h.item.mission.storage;
compositeDisposable.add(
Observable.fromCallable(() -> Utility.checksum(storage, ALGORITHMS.get(id)))
.subscribeOn(Schedulers.computation())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(result -> {
if (finalProgressDialog != null) {
Utility.copyToClipboard(finalProgressDialog.getContext(),
result);
if (mContext != null) {
finalProgressDialog.dismiss();
}
}
})
);
return true;
case R.id.source:
/*Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(h.item.mission.source));
@@ -758,8 +783,8 @@ public class MissionAdapter extends Adapter<ViewHolder> implements Handler.Callb
}
}
public void onDestroy() {
compositeDisposable.dispose();
mDeleter.dispose();
}
@@ -960,60 +985,7 @@ public class MissionAdapter extends Adapter<ViewHolder> implements Handler.Callb
}
}
static class ChecksumTask extends AsyncTask<Object, Void, String> {
ProgressDialog progressDialog;
WeakReference<Activity> weakReference;
ChecksumTask(@NonNull Context context) {
weakReference = new WeakReference<>((Activity) context);
}
@Override
protected void onPreExecute() {
super.onPreExecute();
Activity activity = getActivity();
if (activity != null) {
// Create dialog
progressDialog = new ProgressDialog(activity);
progressDialog.setCancelable(false);
progressDialog.setMessage(activity.getString(R.string.msg_wait));
progressDialog.show();
}
}
@Override
protected String doInBackground(Object... params) {
return Utility.checksum((StoredFileHelper) params[0], (String) params[1]);
}
@Override
protected void onPostExecute(String result) {
super.onPostExecute(result);
if (progressDialog != null) {
Utility.copyToClipboard(progressDialog.getContext(), result);
if (getActivity() != null) {
progressDialog.dismiss();
}
}
}
@Nullable
private Activity getActivity() {
Activity activity = weakReference.get();
if (activity != null && activity.isFinishing()) {
return null;
} else {
return activity;
}
}
}
public interface RecoverHelper {
void tryRecover(DownloadMission mission);
}
}

View File

@@ -83,8 +83,8 @@ public class ProgressDrawable extends Drawable {
// render marquee
width += size * 2;
Path marquee = new Path();
for (float i = -size; i < width; i += size) {
marquee.addPath(mMarqueeLine, i + mMarqueeProgress, 0);
for (int i = -size; i < width; i += size) {
marquee.addPath(mMarqueeLine, ((float)i + mMarqueeProgress), 0);
}
marquee.close();

View File

@@ -224,9 +224,10 @@ public class MissionsFragment extends Fragment {
mList.setAdapter(mAdapter);
if (mSwitch != null) {
mSwitch.setIcon(mLinear
? ThemeHelper.resolveResourceIdFromAttr(requireContext(), R.attr.ic_grid)
: ThemeHelper.resolveResourceIdFromAttr(requireContext(), R.attr.ic_list));
mSwitch.setIcon(ThemeHelper.resolveResourceIdFromAttr(
requireContext(), mLinear
? R.attr.ic_grid
: R.attr.ic_list));
mSwitch.setTitle(mLinear ? R.string.grid : R.string.list);
mPrefs.edit().putBoolean("linear", mLinear).apply();
}

View File

@@ -201,7 +201,7 @@ public class Utility {
}
public static void copyToClipboard(Context context, String str) {
ClipboardManager cm = (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE);
ClipboardManager cm = ContextCompat.getSystemService(context, ClipboardManager.class);
if (cm == null) {
Toast.makeText(context, R.string.permission_denied, Toast.LENGTH_LONG).show();

Some files were not shown because too many files have changed in this diff Show More