mirror of
https://github.com/TeamNewPipe/NewPipe
synced 2026-01-14 19:47:55 +00:00
Compare commits
676 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3c306a0971 | ||
|
|
c0d6c8aeb3 | ||
|
|
b27b49e4f3 | ||
|
|
7ed0dbcf1a | ||
|
|
8a23de6b20 | ||
|
|
6cc3089204 | ||
|
|
093e95c078 | ||
|
|
7c8ac04e35 | ||
|
|
dc88f8b172 | ||
|
|
c94f0ded27 | ||
|
|
b553aa2159 | ||
|
|
a7bd2666f0 | ||
|
|
fe2fc60581 | ||
|
|
ce59c05d5b | ||
|
|
a4858bc702 | ||
|
|
a2bb58a991 | ||
|
|
f7b41227d2 | ||
|
|
5b1a6831d5 | ||
|
|
42b1bbe414 | ||
|
|
db9f20a22f | ||
|
|
cf67b592da | ||
|
|
e867bfbc82 | ||
|
|
9a671851df | ||
|
|
4b92f78cc8 | ||
|
|
c585982557 | ||
|
|
6bf22e7ad0 | ||
|
|
2f8dccf7f6 | ||
|
|
027768d97d | ||
|
|
085f63b8c5 | ||
|
|
6f7c337e00 | ||
|
|
16a968f3bb | ||
|
|
d7e0167fed | ||
|
|
41c4f515cf | ||
|
|
d9a8218372 | ||
|
|
dd9bd4da8b | ||
|
|
cf98500b7f | ||
|
|
2ce8facc05 | ||
|
|
d1b117d07c | ||
|
|
c0377c7ebf | ||
|
|
a2490a5730 | ||
|
|
177334ba62 | ||
|
|
7bce588767 | ||
|
|
4bb67c634f | ||
|
|
3653afbcc4 | ||
|
|
1f4a4ea09f | ||
|
|
3d38add4b4 | ||
|
|
124b7eefb5 | ||
|
|
b52924048c | ||
|
|
93393f5dff | ||
|
|
275a75ebaa | ||
|
|
3e4a7a19cc | ||
|
|
734af457f3 | ||
|
|
55bdb1f47a | ||
|
|
adff0d199d | ||
|
|
f95b3262a0 | ||
|
|
794a14e76c | ||
|
|
ba857b5ef7 | ||
|
|
2aed04a8c2 | ||
|
|
5f9e6b51da | ||
|
|
e7b5c99ed6 | ||
|
|
9c0b3d35be | ||
|
|
a54bc96eab | ||
|
|
a2a8e4b965 | ||
|
|
81ad2c61d9 | ||
|
|
32616493b3 | ||
|
|
05183ffd0f | ||
|
|
e72ddc9439 | ||
|
|
32e3caecac | ||
|
|
df43389183 | ||
|
|
19b77809ec | ||
|
|
be05b827f3 | ||
|
|
5dfc6f822d | ||
|
|
c3e004da03 | ||
|
|
8bae73b6ea | ||
|
|
d1e19d3b63 | ||
|
|
ffca897ddf | ||
|
|
4277b6e262 | ||
|
|
506c4ce701 | ||
|
|
d251e58984 | ||
|
|
4a1213c081 | ||
|
|
8b7609255c | ||
|
|
ef78fe0653 | ||
|
|
70b3ccb422 | ||
|
|
81d6b367fe | ||
|
|
0a78ae60be | ||
|
|
a61830a860 | ||
|
|
86bae9ddc9 | ||
|
|
033780862a | ||
|
|
6094d8a74e | ||
|
|
356ca3d177 | ||
|
|
d69806faa9 | ||
|
|
ab67635dcb | ||
|
|
cee3d49458 | ||
|
|
5b53a7aef7 | ||
|
|
9b29665cc0 | ||
|
|
f447c87b45 | ||
|
|
e3eea45d86 | ||
|
|
f61a06ce0a | ||
|
|
539842aa99 | ||
|
|
5925f1d2aa | ||
|
|
61eb150825 | ||
|
|
cf95de4d27 | ||
|
|
fdad7ec1ba | ||
|
|
850efb4237 | ||
|
|
853cb3887f | ||
|
|
412f2c1664 | ||
|
|
2810a69bd4 | ||
|
|
5347f95f50 | ||
|
|
6b469f0621 | ||
|
|
0021562c93 | ||
|
|
f2bd2b0a59 | ||
|
|
647eb8bbf5 | ||
|
|
816d13ae3f | ||
|
|
578fea4a9c | ||
|
|
1a660d9a4a | ||
|
|
227ac6d9e3 | ||
|
|
bb57407733 | ||
|
|
13ddcce0a2 | ||
|
|
53767a78d1 | ||
|
|
5600e8a2ad | ||
|
|
c6ed52c592 | ||
|
|
3ad14e4adf | ||
|
|
8a22bdea5d | ||
|
|
6135a3c3e2 | ||
|
|
1e3c979303 | ||
|
|
d0228406b6 | ||
|
|
507a2237b7 | ||
|
|
c15c597d99 | ||
|
|
7c26cd3270 | ||
|
|
938af73059 | ||
|
|
1c047366d2 | ||
|
|
cb20f0cbb0 | ||
|
|
468251c84e | ||
|
|
ca86ae0c9a | ||
|
|
59221b0b4e | ||
|
|
d3e0640400 | ||
|
|
bcb72321f5 | ||
|
|
4060af715d | ||
|
|
2ec0237e83 | ||
|
|
c5593880f2 | ||
|
|
3673cbce4f | ||
|
|
1f6f7be4b2 | ||
|
|
580cce3506 | ||
|
|
36ba546fc6 | ||
|
|
7f37799cbe | ||
|
|
5570eeeff9 | ||
|
|
2b186ce6e0 | ||
|
|
72938fed69 | ||
|
|
d54c806e03 | ||
|
|
7eb3551485 | ||
|
|
57abe27895 | ||
|
|
a628a36082 | ||
|
|
0d3e04ff25 | ||
|
|
0c78a3f7b0 | ||
|
|
fb1f574c26 | ||
|
|
7f15c18fca | ||
|
|
e274650956 | ||
|
|
2a1db4a338 | ||
|
|
0b6ea9ec61 | ||
|
|
257a826d45 | ||
|
|
e9f48f5134 | ||
|
|
fcf04624d4 | ||
|
|
16218d6dc5 | ||
|
|
071f33e3cd | ||
|
|
6d15389da8 | ||
|
|
2e8530ec00 | ||
|
|
4edd1c5497 | ||
|
|
8e693b8b42 | ||
|
|
29376066e8 | ||
|
|
a44f3071bf | ||
|
|
b66047e084 | ||
|
|
f0ca916432 | ||
|
|
c88b4032ef | ||
|
|
6f5e99be6f | ||
|
|
fd4c37e9b3 | ||
|
|
7a8dab2d58 | ||
|
|
6f3dfad550 | ||
|
|
18fb0a13d7 | ||
|
|
e88f9ae03b | ||
|
|
30c010ad3f | ||
|
|
e84e70bdc6 | ||
|
|
1e6b6165ae | ||
|
|
c6d149d091 | ||
|
|
d55d8d78de | ||
|
|
eff59f7b5e | ||
|
|
a7a5437245 | ||
|
|
6671b9e55b | ||
|
|
2dde1cc589 | ||
|
|
17866c29ae | ||
|
|
8dc4e6dc2a | ||
|
|
1197f44262 | ||
|
|
a86ed1f801 | ||
|
|
e98d3423e4 | ||
|
|
95333d37c8 | ||
|
|
8bcf0c6498 | ||
|
|
340b92e32b | ||
|
|
6e68ab19f9 | ||
|
|
15fed32d92 | ||
|
|
897c754dd4 | ||
|
|
ec1e746a22 | ||
|
|
001f078ba9 | ||
|
|
b5321152fd | ||
|
|
66d15ea635 | ||
|
|
72177033d2 | ||
|
|
06b7072240 | ||
|
|
4700f35739 | ||
|
|
bbfa280e86 | ||
|
|
2669ba944d | ||
|
|
c8788dbfbe | ||
|
|
5e7c3b53f8 | ||
|
|
99348c2300 | ||
|
|
ab2b9797fd | ||
|
|
637653ea11 | ||
|
|
04cb6ba3d0 | ||
|
|
eb1cddd85a | ||
|
|
723b230093 | ||
|
|
7185fae491 | ||
|
|
0274cd6beb | ||
|
|
ad2ea0b807 | ||
|
|
c24999075d | ||
|
|
773bde14ab | ||
|
|
00b08318a5 | ||
|
|
39e5d8ccc2 | ||
|
|
e25622df4b | ||
|
|
ea5939c1b7 | ||
|
|
4734d04d4f | ||
|
|
493e47f7e6 | ||
|
|
ef2b32eb05 | ||
|
|
1da91d44e1 | ||
|
|
ebd7ab3e46 | ||
|
|
99bddfdf0a | ||
|
|
144f48c9a6 | ||
|
|
8d0d2ba07b | ||
|
|
a6ad334dc0 | ||
|
|
468ca30070 | ||
|
|
7b2d2d9338 | ||
|
|
0e08819cf3 | ||
|
|
1d3f7b49dc | ||
|
|
3566ec7012 | ||
|
|
f37a36efa4 | ||
|
|
2ea069cd8c | ||
|
|
08e111f6dc | ||
|
|
6339881684 | ||
|
|
e222538575 | ||
|
|
c4a67ce420 | ||
|
|
77e348ba62 | ||
|
|
bde39d8c37 | ||
|
|
ebb906c273 | ||
|
|
46b91bf8b0 | ||
|
|
7a432b38e9 | ||
|
|
9b6a201bbb | ||
|
|
a79d7c8417 | ||
|
|
7a6e0d651f | ||
|
|
7476498823 | ||
|
|
4c7b5d44a0 | ||
|
|
620bb54881 | ||
|
|
73be747cbe | ||
|
|
4d874451c9 | ||
|
|
3245d620c3 | ||
|
|
a59f80589a | ||
|
|
6b269c7559 | ||
|
|
53cadeab61 | ||
|
|
e90d388fdb | ||
|
|
219f059834 | ||
|
|
dc2dac66a3 | ||
|
|
e04ee666b8 | ||
|
|
7d27003bb2 | ||
|
|
b866c9dd08 | ||
|
|
d17236fe45 | ||
|
|
d2580ec87c | ||
|
|
7c10f414dc | ||
|
|
2a4717cb7f | ||
|
|
be9cb8a4da | ||
|
|
e887363910 | ||
|
|
a274159726 | ||
|
|
83384e0de4 | ||
|
|
748904b8ad | ||
|
|
3e72df8b1e | ||
|
|
2921563e9c | ||
|
|
01c1346696 | ||
|
|
796e0456ef | ||
|
|
eeb68497fe | ||
|
|
7eadb6acad | ||
|
|
9d588aa7e7 | ||
|
|
204b5f7f09 | ||
|
|
e0f53b63ce | ||
|
|
83f4dbe40e | ||
|
|
8d8ba68838 | ||
|
|
3f25940dec | ||
|
|
745773b207 | ||
|
|
35f5575595 | ||
|
|
e4746f8b32 | ||
|
|
7e0552efde | ||
|
|
6075b98634 | ||
|
|
ebe9f518d0 | ||
|
|
37ceddd11b | ||
|
|
d1d8b911b9 | ||
|
|
796e656328 | ||
|
|
8b869915e7 | ||
|
|
9b05243d61 | ||
|
|
81c24510a8 | ||
|
|
9e7fb4d21a | ||
|
|
7805f8a9b1 | ||
|
|
ae7f04578d | ||
|
|
ce814cffd1 | ||
|
|
703b310ef0 | ||
|
|
da6c4ad36a | ||
|
|
a8c849d38a | ||
|
|
d8d5e04a51 | ||
|
|
fa348cb98f | ||
|
|
f4ec2d8107 | ||
|
|
de39d828de | ||
|
|
25d3d0d0ba | ||
|
|
6f3b1000a7 | ||
|
|
1ebb8d8d14 | ||
|
|
4e4acdaecc | ||
|
|
f7fb03bf56 | ||
|
|
429aafc7ba | ||
|
|
acdfede2a8 | ||
|
|
8366c4c165 | ||
|
|
4c7260b043 | ||
|
|
c878f7dc25 | ||
|
|
aca21f6ef2 | ||
|
|
10c582bafb | ||
|
|
7b3bd26631 | ||
|
|
731f88da84 | ||
|
|
b7fb9a65b6 | ||
|
|
843c24b17a | ||
|
|
18dbbfc95a | ||
|
|
5b6e187b49 | ||
|
|
9025a9b88c | ||
|
|
07b2891671 | ||
|
|
c4a739bef6 | ||
|
|
1008c74cd7 | ||
|
|
60dc9d27bc | ||
|
|
9eb0f48a7a | ||
|
|
7b1fccdd06 | ||
|
|
64ae07b03b | ||
|
|
6ecbbd1f79 | ||
|
|
a1fb268764 | ||
|
|
868661edf0 | ||
|
|
9899e63d53 | ||
|
|
0b37b8b059 | ||
|
|
1b34ca822f | ||
|
|
a4e3a874ad | ||
|
|
8ec3df552a | ||
|
|
b4b1c9256b | ||
|
|
acee20d897 | ||
|
|
6ea3ebb72d | ||
|
|
55f23e9304 | ||
|
|
ad223a04f8 | ||
|
|
0b150ea475 | ||
|
|
167e9fbc6d | ||
|
|
77ea160cd9 | ||
|
|
f9204450f1 | ||
|
|
21ef76816f | ||
|
|
f166cfbac8 | ||
|
|
e5f64710f4 | ||
|
|
32a5062081 | ||
|
|
e6bc29281e | ||
|
|
617ee0afc0 | ||
|
|
1b47a1a994 | ||
|
|
5a87cfc25d | ||
|
|
00a178f7d3 | ||
|
|
2a2c82e73b | ||
|
|
bb882ada2c | ||
|
|
1d42e45d78 | ||
|
|
15c4a5c9ea | ||
|
|
f4435f9031 | ||
|
|
5a423c89a3 | ||
|
|
8b02154f5a | ||
|
|
97c454ea77 | ||
|
|
f07a6d03b5 | ||
|
|
1f18fb5446 | ||
|
|
92ee5b66ab | ||
|
|
fcc92c3e27 | ||
|
|
3e91b5a793 | ||
|
|
42e5cc3bef | ||
|
|
16c61a1919 | ||
|
|
c2b4b0490b | ||
|
|
a310a06e3c | ||
|
|
9228511527 | ||
|
|
bbc4174501 | ||
|
|
5f3196b74c | ||
|
|
725bd8029f | ||
|
|
479ab5df0e | ||
|
|
18c45ad30b | ||
|
|
6f32f098eb | ||
|
|
e8289d3912 | ||
|
|
472d9322ce | ||
|
|
0233ffafb6 | ||
|
|
468ee4756f | ||
|
|
abf9365bbe | ||
|
|
1e0789162f | ||
|
|
31f407f4e8 | ||
|
|
77330ffc50 | ||
|
|
6f132f3fed | ||
|
|
c193b4f07c | ||
|
|
c745b845c5 | ||
|
|
3b69e0dd25 | ||
|
|
8ec55ef394 | ||
|
|
ef5084036c | ||
|
|
7dd317e530 | ||
|
|
e5db3ed9b7 | ||
|
|
3a00dc5b5f | ||
|
|
2d848020fc | ||
|
|
70123d19fe | ||
|
|
ea3770260a | ||
|
|
eea1a80de6 | ||
|
|
4889775ae6 | ||
|
|
355effd93d | ||
|
|
f1583b6e0c | ||
|
|
347566c311 | ||
|
|
1f73572dd3 | ||
|
|
2bfb83c4cd | ||
|
|
ceed1c4962 | ||
|
|
2b5b9d3599 | ||
|
|
96e3709b7b | ||
|
|
239fc2f6f8 | ||
|
|
4c77e5cdd2 | ||
|
|
974f8f692c | ||
|
|
e97d0b9a69 | ||
|
|
b0b0a75c87 | ||
|
|
abcacf8c74 | ||
|
|
290428b981 | ||
|
|
37d1541d6b | ||
|
|
1500ce7490 | ||
|
|
a48529872d | ||
|
|
31cffa68c5 | ||
|
|
6909d1e527 | ||
|
|
972235bfba | ||
|
|
6db560fd2c | ||
|
|
1a64d8aec9 | ||
|
|
1e1fb32558 | ||
|
|
008eb5ba4a | ||
|
|
f46e0acc89 | ||
|
|
b615ef5810 | ||
|
|
d773279de8 | ||
|
|
56d721651a | ||
|
|
ee17abff92 | ||
|
|
952bb1a2eb | ||
|
|
c287813e00 | ||
|
|
eab4fd80d7 | ||
|
|
5c8f8869d4 | ||
|
|
4954dfe107 | ||
|
|
4eb8094fb8 | ||
|
|
1dd2423a0b | ||
|
|
6fcf989c62 | ||
|
|
67d1a4f643 | ||
|
|
3df9433baf | ||
|
|
b12d568147 | ||
|
|
8691e035a0 | ||
|
|
2683043762 | ||
|
|
be5aa59f61 | ||
|
|
9398dfb7cf | ||
|
|
ff6d2b30e4 | ||
|
|
29bb999a32 | ||
|
|
9320507e26 | ||
|
|
181fc4fa0a | ||
|
|
4f3dd4b662 | ||
|
|
7b09de99ea | ||
|
|
f9f0da18e1 | ||
|
|
ba0fdb9478 | ||
|
|
8c684bca22 | ||
|
|
1266a75549 | ||
|
|
3871d5aed7 | ||
|
|
25d555126d | ||
|
|
f529d15d7a | ||
|
|
9eda30fc71 | ||
|
|
dfd6424d9c | ||
|
|
fdc961f2de | ||
|
|
a6d4000d24 | ||
|
|
e1024e59c3 | ||
|
|
990164802d | ||
|
|
34f18fbdb3 | ||
|
|
17e24bb038 | ||
|
|
df7e2b7734 | ||
|
|
1ddef06bd2 | ||
|
|
18bd910bf0 | ||
|
|
0db44f6e33 | ||
|
|
7aac3d38f0 | ||
|
|
e406b6f780 | ||
|
|
2dad9666a9 | ||
|
|
063abf1688 | ||
|
|
a86f8e9a22 | ||
|
|
0ce6d4fe92 | ||
|
|
886f6c721c | ||
|
|
9d7d089279 | ||
|
|
0bd624dfa9 | ||
|
|
8c29760d93 | ||
|
|
9b893d841d | ||
|
|
e8c0163153 | ||
|
|
256568d966 | ||
|
|
34bed47a52 | ||
|
|
9b0996fade | ||
|
|
7c1028df5d | ||
|
|
eaea60e0cb | ||
|
|
efab05dcfc | ||
|
|
d79f77f7e0 | ||
|
|
f0d459d490 | ||
|
|
e269c073ac | ||
|
|
85d5609144 | ||
|
|
671c593db1 | ||
|
|
8b14c7a2cb | ||
|
|
23862419eb | ||
|
|
43d54db4dd | ||
|
|
b8b0060440 | ||
|
|
64d79ceb30 | ||
|
|
25f7b44d48 | ||
|
|
ada7e628da | ||
|
|
76f2338c3d | ||
|
|
a0ed8036c0 | ||
|
|
863ce65b10 | ||
|
|
85190b16cb | ||
|
|
abc6fd8b2a | ||
|
|
b6f603154e | ||
|
|
90cb9d3de1 | ||
|
|
d47c9a2e29 | ||
|
|
b7aea96ca0 | ||
|
|
a5879a4407 | ||
|
|
0452c69771 | ||
|
|
4eb9dff45e | ||
|
|
b7c1e88b59 | ||
|
|
23814330d9 | ||
|
|
8bc75aacea | ||
|
|
17576d223a | ||
|
|
90e8f0ca63 | ||
|
|
adc4a811b7 | ||
|
|
7c80233f26 | ||
|
|
f05ae2de35 | ||
|
|
cf9da556a8 | ||
|
|
32a142bf79 | ||
|
|
2680d41a3d | ||
|
|
1550fc4398 | ||
|
|
f2a85f3b7e | ||
|
|
49c2b4c196 | ||
|
|
a8281e174e | ||
|
|
d7f5c8bd55 | ||
|
|
b3337df88b | ||
|
|
d760616e55 | ||
|
|
914a4d32b4 | ||
|
|
148f53e21e | ||
|
|
5e7fa0f964 | ||
|
|
0973ceb9d2 | ||
|
|
5214bfe8cb | ||
|
|
187aaafddc | ||
|
|
208cb405ca | ||
|
|
9b9d267cd4 | ||
|
|
6f90a27f9f | ||
|
|
4e1dddc06d | ||
|
|
1f504d6f23 | ||
|
|
2db1fd813f | ||
|
|
f39383a3d8 | ||
|
|
b593bdbf1b | ||
|
|
4de93ba3c0 | ||
|
|
2eb7a91987 | ||
|
|
94e4264c2a | ||
|
|
e54d28f157 | ||
|
|
6111c8bde0 | ||
|
|
3bacdfd4fc | ||
|
|
65db645ff9 | ||
|
|
de2e2c45a5 | ||
|
|
9b655e18e3 | ||
|
|
fb4b9b5f76 | ||
|
|
820b39840a | ||
|
|
4b1052eb70 | ||
|
|
c0eb3972a7 | ||
|
|
66ba8d56b7 | ||
|
|
a73baf32f1 | ||
|
|
333cf0a2f0 | ||
|
|
8347d8700a | ||
|
|
09af0e2448 | ||
|
|
001914764a | ||
|
|
6938dd6267 | ||
|
|
941028ba6f | ||
|
|
4ca7ed9f8c | ||
|
|
03d99887c5 | ||
|
|
293e2ff5e3 | ||
|
|
55d242fa08 | ||
|
|
7e9fba2d96 | ||
|
|
175652f23b | ||
|
|
3329e0c4a1 | ||
|
|
3c5ed2c885 | ||
|
|
c4af93c363 | ||
|
|
e2685c4503 | ||
|
|
48b1d3fff8 | ||
|
|
6c4920949d | ||
|
|
69447b75af | ||
|
|
e1104570a9 | ||
|
|
2c23678fb9 | ||
|
|
613070d39f | ||
|
|
6deae64f45 | ||
|
|
aced2b124c | ||
|
|
e6b08de2e8 | ||
|
|
d2d02d0749 | ||
|
|
28d27801b2 | ||
|
|
be340dd275 | ||
|
|
58090fb3de | ||
|
|
ae33c6cf18 | ||
|
|
f8cd6afbf8 | ||
|
|
6fce06906d | ||
|
|
84694a8bbd | ||
|
|
1639e68424 | ||
|
|
ff48fe8b49 | ||
|
|
efe06267ec | ||
|
|
53647ea5a8 | ||
|
|
7742de5af4 | ||
|
|
724a260f71 | ||
|
|
cf75e40332 | ||
|
|
3c67df263c | ||
|
|
c4ae72c3c1 | ||
|
|
f6925fc5b8 | ||
|
|
18be9655d6 | ||
|
|
6235b6123e | ||
|
|
b3555385e6 | ||
|
|
c9fbdb322b | ||
|
|
94bac7d8db | ||
|
|
f38119be96 | ||
|
|
bc1d2ba839 | ||
|
|
18f5b70b1f | ||
|
|
7df9b07305 | ||
|
|
cf01c1fd1f | ||
|
|
2ce6fe420b | ||
|
|
f55381d689 | ||
|
|
c4084c4f97 | ||
|
|
58b720b004 | ||
|
|
f6d0c1f05e | ||
|
|
f4620be859 | ||
|
|
e2b3a98690 | ||
|
|
4cd391d5ef | ||
|
|
01c37c34dd | ||
|
|
f4827cde0e | ||
|
|
3e722295b0 | ||
|
|
0d2eab3ad4 | ||
|
|
9e1bc631cf | ||
|
|
ca9fbe2f11 | ||
|
|
2e28fad102 | ||
|
|
69760200dd | ||
|
|
f945ee1288 | ||
|
|
5e3486c481 | ||
|
|
c15a943cf4 | ||
|
|
b8cb29c66c | ||
|
|
003badcb5a | ||
|
|
5b2493fa68 | ||
|
|
bc342b9b33 | ||
|
|
314615bfef | ||
|
|
44e82217c1 | ||
|
|
cbf364f24f | ||
|
|
44dfcb927b | ||
|
|
12f615c6da | ||
|
|
ed6fc4d848 | ||
|
|
cd515993f5 | ||
|
|
a918eaac3f | ||
|
|
9f63e2d39a | ||
|
|
36248ff046 | ||
|
|
a88f5113e0 | ||
|
|
06fb89fae2 | ||
|
|
733531356f | ||
|
|
3f8fb30066 | ||
|
|
1a4a2d2b30 | ||
|
|
eb6d3b3f8d | ||
|
|
e8e3363d06 | ||
|
|
dd55ad61f4 | ||
|
|
2464bfd70b | ||
|
|
09bc36bb13 | ||
|
|
39da89b556 | ||
|
|
c23de4b3b0 | ||
|
|
7c79d7f5d7 | ||
|
|
710507da51 | ||
|
|
10e95bf1b1 | ||
|
|
66a893c84e | ||
|
|
dd6392e380 | ||
|
|
b8f7ba62c7 | ||
|
|
25b318ba00 | ||
|
|
9add51b59d | ||
|
|
63d54e6570 |
69
.github/CONTRIBUTING.md
vendored
69
.github/CONTRIBUTING.md
vendored
@@ -1,65 +1,46 @@
|
||||
NewPipe contribution guidelines
|
||||
===============================
|
||||
|
||||
PLEASE READ THESE GUIDELINES CAREFULLY BEFORE ANY CONTRIBUTION!
|
||||
|
||||
## Crash reporting
|
||||
|
||||
Do not report crashes in the GitHub issue tracker. NewPipe has an automated crash report system that will ask you to
|
||||
send a report via e-mail when a crash occurs. This contains all the data we need for debugging, and allows you to even
|
||||
add a comment to it. You'll see exactly what is sent, the system is 100% transparent.
|
||||
Report crashes through the automated crash report system of NewPipe.
|
||||
This way all the data needed for debugging is included in your bugreport for GitHub.
|
||||
You'll see exactly what is sent, be able to add your comments, and then send it.
|
||||
|
||||
## Issue reporting/feature requests
|
||||
|
||||
* Search the [existing issues](https://github.com/TeamNewPipe/NewPipe/issues) first to make sure your issue/feature
|
||||
hasn't been reported/requested before.
|
||||
* Check whether your issue/feature is already fixed/implemented.
|
||||
* Check if the issue still exists in the latest release/beta version.
|
||||
* If you are an Android/Java developer, you are always welcome to fix an issue or implement a feature yourself. PRs welcome!
|
||||
* We use English for development. Issues in other languages will be closed and ignored.
|
||||
* Please only add *one* issue at a time. Do not put multiple issues into one thread.
|
||||
* Follow the template! Issues or feature requests not matching the template might be closed.
|
||||
* **Already reported**? Browse the [existing issues](https://github.com/TeamNewPipe/NewPipe/issues) to make sure your issue/feature hasn't been reported/requested.
|
||||
* **Already fixed**? Check whether your issue/feature is already fixed/implemented.
|
||||
* **Still relevant**? Check if the issue still exists in the latest release/beta version.
|
||||
* **Can you fix it**? If you are an Android/Java developer, you are always welcome to fix an issue or implement a feature yourself. PRs welcome! See [Code contribution](#code-contribution) for more info.
|
||||
* **Is it in English**? Issues in other languages will be ignored unless someone translates them.
|
||||
* **Is it one issue**? Multiple issues require multiple reports, that can be linked to track their statuses.
|
||||
* **The template**: Fill it out, everyone wins. Your issue has a chance of getting fixed.
|
||||
|
||||
## Bug Fixing
|
||||
* If you want to help NewPipe to become free of bugs (this is our utopic goal for NewPipe), you can send us an email to
|
||||
<a href="mailto:tnp@newpipe.schabi.org">tnp@newpipe.schabi.org</a> to let us know that you intend to help. We'll send you further instructions. You may, on request,
|
||||
register at our [Sentry](https://sentry.schabi.org) instance (see section "Crash reporting" for more information).
|
||||
|
||||
## Translation
|
||||
|
||||
* NewPipe is translated via [Weblate](https://hosted.weblate.org/projects/newpipe/strings/). You can log in there
|
||||
with your GitHub account.
|
||||
* If the language you want to translate is not on Weblate, you can add it: see [How to add a new language](https://github.com/TeamNewPipe/NewPipe/wiki/How-to-add-a-new-language-to-NewPipe) in the wiki.
|
||||
* NewPipe is translated via [Weblate](https://hosted.weblate.org/projects/newpipe/strings/). Log in there with your GitHub account, or register.
|
||||
* Add the language you want to translate if it is not there already: see [How to add a new language](https://github.com/TeamNewPipe/NewPipe/wiki/How-to-add-a-new-language-to-NewPipe) in the wiki.
|
||||
|
||||
## Code contribution
|
||||
|
||||
* If you want to add a feature or change one, please open an issue describing your change. This gives the team and community a chance to give feedback before you spend any time on something that could be done differently or not done at all. It also prevents two contributors from working on the same thing and one being disappointed when only one user's code can be added.
|
||||
* Stick to NewPipe's style conventions: follow [checkStyle](https://github.com/checkstyle/checkstyle). It will run each time you build the project.
|
||||
* Do not bring non-free software (e.g. binary blobs) into the project. Also, make sure you do not introduce Google
|
||||
* If you want to help out with an existing bug report or feature request, leave a comment on that issue saying you want to try your hand at it.
|
||||
* If there is no existing issue for what you want to work on, open a new one describing your changes. This gives the team and the community a chance to give feedback before you spend time on something that is already in development, should be done differently, or should be avoided completely.
|
||||
* Stick to NewPipe's style conventions of [checkStyle](https://github.com/checkstyle/checkstyle). It runs each time you build the project.
|
||||
* Do not bring non-free software (e.g. binary blobs) into the project. Make sure you do not introduce Google
|
||||
libraries.
|
||||
* Stick to [F-Droid contribution guidelines](https://f-droid.org/wiki/page/Inclusion_Policy).
|
||||
* Make changes on a separate branch with a meaningful name, not on the master neither dev branch. This is commonly known as *feature branch workflow*. You
|
||||
may then send your changes as a pull request (PR) on GitHub.
|
||||
* When submitting changes, you confirm that your code is licensed under the terms of the
|
||||
[GNU General Public License v3](https://www.gnu.org/licenses/gpl-3.0.html).
|
||||
* Please test (compile and run) your code before you submit changes! Ideally, provide test feedback in the PR
|
||||
description. Untested code will **not** be merged!
|
||||
* Make changes on a separate branch with a meaningful name, not on the _master_ branch or the _dev_ branch. This is commonly known as *feature branch workflow*. You may then send your changes as a pull request (PR) on GitHub.
|
||||
* Please test (compile and run) your code before submitting changes! Ideally, provide test feedback in the PR description. Untested code will **not** be merged!
|
||||
* Make sure your PR is up-to-date with the rest of the code. Often, a simple click on "Update branch" will do the job, but if not, you must rebase the dev branch manually and resolve the problems on your own. You can find help [on the wiki](https://github.com/TeamNewPipe/NewPipe/wiki/How-to-merge-a-PR). That makes the maintainers' jobs way easier.
|
||||
* Please show intention to maintain your features and code after you contribute a PR. Unmaintained code is a hassle for core developers. If you do not intend to maintain features you plan to contribute, please rethink your submission, or clearly state that in the PR description.
|
||||
* Respond if someone requests changes or otherwise raises issues about your PRs.
|
||||
* Send PRs that only cover one specific issue/solution/bug. Do not send PRs that are huge and consist of multiple independent solutions.
|
||||
* Try to figure out yourself why builds on our CI fail.
|
||||
* Make sure your PR is up-to-date with the rest of the code. Often, a simple click on "Update branch" will do the job,
|
||||
but if not, you are asked to rebase the dev branch manually and resolve the problems on your own. You can find help [on the wiki](https://github.com/TeamNewPipe/NewPipe/wiki/How-to-merge-a-PR). That will make the
|
||||
maintainers' jobs way easier.
|
||||
* Please show intention to maintain your features and code after you contributed it. Unmaintained code is a hassle for
|
||||
the core developers, and just adds work. If you do not intend to maintain features you contributed, please think again
|
||||
about submission, or clearly state that in the description of your PR.
|
||||
* Respond yourselves if someone requests changes or otherwise raises issues about your PRs.
|
||||
* Send PR that only cover one specific issue/solution/bug. Do not send PRs that are huge and consists of multiple
|
||||
independent solutions.
|
||||
|
||||
## Communication
|
||||
|
||||
* There is an IRC channel on Freenode which is regularly visited by the core team and other developers:
|
||||
[#newpipe](irc:irc.freenode.net/newpipe). [Click here for Webchat](https://webchat.freenode.net/?channels=newpipe)!
|
||||
* If you want to get in touch with the core team or one of our other contributors you can send an email to
|
||||
<a href="mailto:tnp@newpipe.schabi.org">tnp@newpipe.schabi.org</a>. Please do not send issue reports, they will be ignored and remain unanswered! Use the GitHub issue
|
||||
tracker described above!
|
||||
* Feel free to post suggestions, changes, ideas etc. on GitHub or IRC!
|
||||
* The [#newpipe](irc:irc.freenode.net/newpipe) channel on freenode has the core team and other developers in it. [Click here for webchat](https://webchat.freenode.net/?channels=newpipe)!
|
||||
* You can also use a Matrix account to join the Newpipe channel at [#freenode_#newpipe:matrix.org](https://matrix.to/#/#freenode_#newpipe:matrix.org).
|
||||
* Post suggestions, changes, ideas etc. on GitHub or IRC.
|
||||
|
||||
47
.github/ISSUE_TEMPLATE/bug_report.md
vendored
47
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -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
1
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@@ -0,0 +1 @@
|
||||
blank_issues_enabled: false
|
||||
25
.github/ISSUE_TEMPLATE/feature_request.md
vendored
25
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@@ -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 -->
|
||||
|
||||
18
.github/PULL_REQUEST_TEMPLATE.md
vendored
18
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -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
144
README.ko.md
Normal 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> • <a href="#description">Description</a> • <a href="#features">Features</a> • <a href="#updates">Updates</a> • <a href="#contribution">Contribution</a> • <a href="#donate">Donate</a> • <a href="#license">License</a></p>
|
||||
<p align="center"><a href="https://newpipe.schabi.org">Website</a> • <a href="https://newpipe.schabi.org/blog/">Blog</a> • <a href="https://newpipe.schabi.org/FAQ/">FAQ</a> • <a href="https://newpipe.schabi.org/press/">Press</a></p>
|
||||
<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
|
||||
[](http://www.gnu.org/licenses/gpl-3.0.en.html)
|
||||
|
||||
NewPipe는 자유 소프트웨어입니다: 당신의 마음대로 이것을 사용하고, 연구하고, 공유하고, 개선할 수 있습니다.
|
||||
구체적으로 당신은 자유 소프트웨어 재단에서 발행되는, 버전 3 또는 (당신의 선택에 따라)이후 버전의,
|
||||
[GNU General Public License](https://www.gnu.org/licenses/gpl.html) 하에서 이것을 재배포 및/또는 수정할 수 있습니다.
|
||||
26
README.md
26
README.md
@@ -16,9 +16,11 @@
|
||||
<p align="center"><a href="https://newpipe.schabi.org">Website</a> • <a href="https://newpipe.schabi.org/blog/">Blog</a> • <a href="https://newpipe.schabi.org/FAQ/">FAQ</a> • <a href="https://newpipe.schabi.org/press/">Press</a></p>
|
||||
<hr>
|
||||
|
||||
*Read this in other languages: [English](README.md), [한국어](README.ko.md).*
|
||||
|
||||
<b>WARNING: THIS IS A BETA VERSION, THEREFORE YOU MAY ENCOUNTER BUGS. IF YOU DO, OPEN AN ISSUE VIA OUR GITHUB REPOSITORY.</b>
|
||||
|
||||
<b>PUTTING NEWPIPE OR ANY FORK OF IT INTO GOOGLE PLAYSTORE VIOLATES THEIR TERMS OF CONDITIONS.</b>
|
||||
<b>PUTTING NEWPIPE OR ANY FORK OF IT INTO THE GOOGLE PLAY STORE VIOLATES THEIR TERMS AND CONDITIONS.</b>
|
||||
|
||||
## Screenshots
|
||||
|
||||
@@ -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 used for 2 and 3, but a different signing key (F-Droid's) being used for 4. Building a debug APK using method 1 excludes a key entirely. Signing keys help ensure that a user isn't tricked into installing a malicious update to an app.
|
||||
|
||||
In the meanwhile, if you want to switch sources for some reason (e.g. NewPipe's core functionality was broken and F-Droid doesn't have the update yet), we recommend following this procedure:
|
||||
1. Back up your data via "Settings>Content>Export Database" so you keep your history, subscriptions, and playlists
|
||||
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).
|
||||
|
||||
|
||||
@@ -13,8 +13,10 @@ android {
|
||||
resValue "string", "app_name", "NewPipe"
|
||||
minSdkVersion 19
|
||||
targetSdkVersion 29
|
||||
versionCode 954
|
||||
versionName "0.20.0"
|
||||
versionCode 960
|
||||
versionName "0.20.6"
|
||||
|
||||
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,14 +89,14 @@ android {
|
||||
|
||||
ext {
|
||||
icepickVersion = '3.2.0'
|
||||
checkstyleVersion = '8.32'
|
||||
checkstyleVersion = '8.37'
|
||||
stethoVersion = '1.5.1'
|
||||
leakCanaryVersion = '2.2'
|
||||
leakCanaryVersion = '2.5'
|
||||
exoPlayerVersion = '2.11.8'
|
||||
androidxLifecycleVersion = '2.2.0'
|
||||
androidxRoomVersion = '2.2.5'
|
||||
groupieVersion = '2.8.0'
|
||||
markwonVersion = '4.3.1'
|
||||
androidxRoomVersion = '2.3.0-alpha03'
|
||||
groupieVersion = '2.8.1'
|
||||
markwonVersion = '4.6.0'
|
||||
googleAutoServiceVersion = '1.0-rc7'
|
||||
}
|
||||
|
||||
@@ -122,76 +130,85 @@ task runCheckstyle(type: Checkstyle) {
|
||||
}
|
||||
}
|
||||
|
||||
def outputDir = "${project.buildDir}/reports/ktlint/"
|
||||
def inputFiles = project.fileTree(dir: "src", include: "**/*.kt")
|
||||
|
||||
task runKtlint(type: JavaExec) {
|
||||
inputs.files(inputFiles)
|
||||
outputs.dir(outputDir)
|
||||
main = "com.pinterest.ktlint.Main"
|
||||
classpath = configurations.ktlint
|
||||
args "src/**/*.kt"
|
||||
}
|
||||
|
||||
task formatKtlint(type: JavaExec) {
|
||||
inputs.files(inputFiles)
|
||||
outputs.dir(outputDir)
|
||||
main = "com.pinterest.ktlint.Main"
|
||||
classpath = configurations.ktlint
|
||||
args "-F", "src/**/*.kt"
|
||||
}
|
||||
|
||||
afterEvaluate {
|
||||
preDebugBuild.dependsOn runCheckstyle, runKtlint
|
||||
preDebugBuild.dependsOn formatKtlint, runCheckstyle, runKtlint
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
|
||||
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.1'
|
||||
|
||||
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.39.0"
|
||||
|
||||
debugImplementation "com.facebook.stetho:stetho:${stethoVersion}"
|
||||
debugImplementation "com.facebook.stetho:stetho-okhttp3:${stethoVersion}"
|
||||
|
||||
debugImplementation "com.squareup.leakcanary:leakcanary-android:${leakCanaryVersion}"
|
||||
implementation "com.squareup.leakcanary:leakcanary-object-watcher-android:${leakCanaryVersion}"
|
||||
implementation "com.squareup.leakcanary:plumber-android:${leakCanaryVersion}"
|
||||
|
||||
debugImplementation "androidx.multidex:multidex:2.0.1"
|
||||
|
||||
testImplementation 'junit:junit:4.13'
|
||||
testImplementation 'org.mockito:mockito-core:3.3.3'
|
||||
|
||||
androidTestImplementation "androidx.test.ext:junit:1.1.1"
|
||||
androidTestImplementation "androidx.room:room-testing:${androidxRoomVersion}"
|
||||
androidTestImplementation "androidx.test.espresso:espresso-core:3.2.0", {
|
||||
exclude module: 'support-annotations'
|
||||
}
|
||||
|
||||
implementation 'com.github.TeamNewPipe:NewPipeExtractor:2463884aa8b696df5812f7feff553008bbd2f888'
|
||||
implementation "androidx.multidex:multidex:2.0.1"
|
||||
|
||||
// NewPipe dependencies
|
||||
// You can use a local version by uncommenting a few lines in settings.gradle
|
||||
implementation 'com.github.TeamNewPipe:NewPipeExtractor:b3835bd616ab28b861c83dcefd56e1754c6d20be'
|
||||
implementation "com.github.TeamNewPipe:nanojson:1d9e1aea9049fc9f85e68b43ba39fe7be1c1f751"
|
||||
|
||||
implementation "org.jsoup:jsoup:1.13.1"
|
||||
|
||||
//noinspection GradleDependency --> do not update okhttp to keep supporting Android 4.4 users
|
||||
implementation "com.squareup.okhttp3:okhttp:3.12.12"
|
||||
|
||||
implementation "com.google.android.exoplayer:exoplayer:${exoPlayerVersion}"
|
||||
implementation "com.google.android.exoplayer:extension-mediasession:${exoPlayerVersion}"
|
||||
|
||||
implementation "com.google.android.material:material:1.1.0"
|
||||
implementation "com.google.android.material:material:1.2.1"
|
||||
|
||||
compileOnly "com.google.auto.service:auto-service-annotations:${googleAutoServiceVersion}"
|
||||
kapt "com.google.auto.service:auto-service:${googleAutoServiceVersion}"
|
||||
|
||||
implementation "androidx.appcompat:appcompat:1.1.0"
|
||||
implementation "androidx.appcompat:appcompat:1.2.0"
|
||||
implementation "androidx.preference:preference:1.1.1"
|
||||
implementation "androidx.recyclerview:recyclerview:1.1.0"
|
||||
implementation "androidx.cardview:cardview:1.0.0"
|
||||
implementation "androidx.constraintlayout:constraintlayout:1.1.3"
|
||||
implementation "androidx.constraintlayout:constraintlayout:2.0.4"
|
||||
implementation 'androidx.core:core-ktx:1.3.2'
|
||||
implementation 'androidx.documentfile:documentfile:1.0.1'
|
||||
implementation 'androidx.localbroadcastmanager:localbroadcastmanager:1.0.0'
|
||||
|
||||
implementation "androidx.lifecycle:lifecycle-livedata:${androidxLifecycleVersion}"
|
||||
implementation "androidx.lifecycle:lifecycle-viewmodel:${androidxLifecycleVersion}"
|
||||
|
||||
implementation "androidx.room:room-runtime:${androidxRoomVersion}"
|
||||
implementation "androidx.room:room-rxjava2:${androidxRoomVersion}"
|
||||
implementation "androidx.room:room-rxjava3:${androidxRoomVersion}"
|
||||
kapt "androidx.room:room-compiler:${androidxRoomVersion}"
|
||||
|
||||
implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0"
|
||||
|
||||
implementation "com.xwray:groupie:${groupieVersion}"
|
||||
implementation "com.xwray:groupie-kotlin-android-extensions:${groupieVersion}"
|
||||
|
||||
@@ -203,13 +220,22 @@ dependencies {
|
||||
|
||||
implementation "com.nononsenseapps:filepicker:4.2.1"
|
||||
|
||||
implementation "ch.acra:acra-core:5.5.0"
|
||||
implementation "ch.acra:acra-core:5.7.0"
|
||||
|
||||
implementation "io.reactivex.rxjava2:rxjava:2.2.19"
|
||||
implementation "io.reactivex.rxjava2:rxandroid:2.1.1"
|
||||
implementation "com.jakewharton.rxbinding2:rxbinding:2.2.0"
|
||||
implementation "io.reactivex.rxjava3:rxjava:3.0.7"
|
||||
implementation "io.reactivex.rxjava3:rxandroid:3.0.0"
|
||||
implementation "com.jakewharton.rxbinding4:rxbinding:4.0.0"
|
||||
|
||||
implementation "org.ocpsoft.prettytime:prettytime:4.0.6.Final"
|
||||
|
||||
testImplementation 'junit:junit:4.13.1'
|
||||
testImplementation 'org.mockito:mockito-core:3.6.0'
|
||||
|
||||
androidTestImplementation "androidx.test.ext:junit:1.1.2"
|
||||
androidTestImplementation "androidx.room:room-testing:${androidxRoomVersion}"
|
||||
androidTestImplementation "androidx.test.espresso:espresso-core:3.3.0", {
|
||||
exclude module: 'support-annotations'
|
||||
}
|
||||
}
|
||||
|
||||
static String getGitWorkingBranch() {
|
||||
|
||||
@@ -31,49 +31,62 @@ class AppDatabaseTest {
|
||||
}
|
||||
|
||||
@get:Rule
|
||||
val testHelper = MigrationTestHelper(InstrumentationRegistry.getInstrumentation(),
|
||||
AppDatabase::class.java.canonicalName, FrameworkSQLiteOpenHelperFactory())
|
||||
val testHelper = MigrationTestHelper(
|
||||
InstrumentationRegistry.getInstrumentation(),
|
||||
AppDatabase::class.java.canonicalName, FrameworkSQLiteOpenHelperFactory()
|
||||
)
|
||||
|
||||
@Test
|
||||
fun migrateDatabaseFrom2to3() {
|
||||
val databaseInV2 = testHelper.createDatabase(AppDatabase.DATABASE_NAME, Migrations.DB_VER_2)
|
||||
|
||||
databaseInV2.run {
|
||||
insert("streams", SQLiteDatabase.CONFLICT_FAIL, ContentValues().apply {
|
||||
// put("uid", null)
|
||||
put("service_id", DEFAULT_SERVICE_ID)
|
||||
put("url", DEFAULT_URL)
|
||||
put("title", DEFAULT_TITLE)
|
||||
put("stream_type", DEFAULT_TYPE.name)
|
||||
put("duration", DEFAULT_DURATION)
|
||||
put("uploader", DEFAULT_UPLOADER_NAME)
|
||||
put("thumbnail_url", DEFAULT_THUMBNAIL)
|
||||
})
|
||||
insert("streams", SQLiteDatabase.CONFLICT_FAIL, ContentValues().apply {
|
||||
// put("uid", null)
|
||||
put("service_id", DEFAULT_SECOND_SERVICE_ID)
|
||||
put("url", DEFAULT_SECOND_URL)
|
||||
// put("title", null)
|
||||
// put("stream_type", null)
|
||||
// put("duration", null)
|
||||
// put("uploader", null)
|
||||
// put("thumbnail_url", null)
|
||||
})
|
||||
insert("streams", SQLiteDatabase.CONFLICT_FAIL, ContentValues().apply {
|
||||
// put("uid", null)
|
||||
put("service_id", DEFAULT_SERVICE_ID)
|
||||
// put("url", null)
|
||||
// put("title", null)
|
||||
// put("stream_type", null)
|
||||
// put("duration", null)
|
||||
// put("uploader", null)
|
||||
// put("thumbnail_url", null)
|
||||
})
|
||||
insert(
|
||||
"streams", SQLiteDatabase.CONFLICT_FAIL,
|
||||
ContentValues().apply {
|
||||
// put("uid", null)
|
||||
put("service_id", DEFAULT_SERVICE_ID)
|
||||
put("url", DEFAULT_URL)
|
||||
put("title", DEFAULT_TITLE)
|
||||
put("stream_type", DEFAULT_TYPE.name)
|
||||
put("duration", DEFAULT_DURATION)
|
||||
put("uploader", DEFAULT_UPLOADER_NAME)
|
||||
put("thumbnail_url", DEFAULT_THUMBNAIL)
|
||||
}
|
||||
)
|
||||
insert(
|
||||
"streams", SQLiteDatabase.CONFLICT_FAIL,
|
||||
ContentValues().apply {
|
||||
// put("uid", null)
|
||||
put("service_id", DEFAULT_SECOND_SERVICE_ID)
|
||||
put("url", DEFAULT_SECOND_URL)
|
||||
// put("title", null)
|
||||
// put("stream_type", null)
|
||||
// put("duration", null)
|
||||
// put("uploader", null)
|
||||
// put("thumbnail_url", null)
|
||||
}
|
||||
)
|
||||
insert(
|
||||
"streams", SQLiteDatabase.CONFLICT_FAIL,
|
||||
ContentValues().apply {
|
||||
// put("uid", null)
|
||||
put("service_id", DEFAULT_SERVICE_ID)
|
||||
// put("url", null)
|
||||
// put("title", null)
|
||||
// put("stream_type", null)
|
||||
// put("duration", null)
|
||||
// put("uploader", null)
|
||||
// put("thumbnail_url", null)
|
||||
}
|
||||
)
|
||||
close()
|
||||
}
|
||||
|
||||
testHelper.runMigrationsAndValidate(AppDatabase.DATABASE_NAME, Migrations.DB_VER_3,
|
||||
true, Migrations.MIGRATION_2_3)
|
||||
testHelper.runMigrationsAndValidate(
|
||||
AppDatabase.DATABASE_NAME, Migrations.DB_VER_3,
|
||||
true, Migrations.MIGRATION_2_3
|
||||
)
|
||||
|
||||
val migratedDatabaseV3 = getMigratedDatabase()
|
||||
val listFromDB = migratedDatabaseV3.streamDAO().all.blockingFirst()
|
||||
@@ -110,9 +123,11 @@ class AppDatabaseTest {
|
||||
}
|
||||
|
||||
private fun getMigratedDatabase(): AppDatabase {
|
||||
val database: AppDatabase = Room.databaseBuilder(ApplicationProvider.getApplicationContext(),
|
||||
AppDatabase::class.java, AppDatabase.DATABASE_NAME)
|
||||
.build()
|
||||
val database: AppDatabase = Room.databaseBuilder(
|
||||
ApplicationProvider.getApplicationContext(),
|
||||
AppDatabase::class.java, AppDatabase.DATABASE_NAME
|
||||
)
|
||||
.build()
|
||||
testHelper.closeWhenFinished(database)
|
||||
return database
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ import androidx.test.filters.LargeTest;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.report.ErrorActivity.ErrorInfo;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
@@ -29,10 +28,10 @@ public class ErrorInfoTest {
|
||||
parcel.setDataPosition(0);
|
||||
final ErrorInfo infoFromParcel = ErrorInfo.CREATOR.createFromParcel(parcel);
|
||||
|
||||
assertEquals(UserAction.USER_REPORT, infoFromParcel.userAction);
|
||||
assertEquals("youtube", infoFromParcel.serviceName);
|
||||
assertEquals("request", infoFromParcel.request);
|
||||
assertEquals(R.string.general_error, infoFromParcel.message);
|
||||
assertEquals(UserAction.USER_REPORT, infoFromParcel.getUserAction());
|
||||
assertEquals("youtube", infoFromParcel.getServiceName());
|
||||
assertEquals("request", infoFromParcel.getRequest());
|
||||
assertEquals(R.string.general_error, infoFromParcel.getMessage());
|
||||
|
||||
parcel.recycle();
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -16,24 +15,26 @@ class DebugApp : App() {
|
||||
|
||||
// Give each object 10 seconds to be GC'ed, before LeakCanary gets nosy on it
|
||||
AppWatcher.config = AppWatcher.config.copy(watchDurationMillis = 10000)
|
||||
LeakCanary.config = LeakCanary.config.copy(dumpHeap = PreferenceManager
|
||||
.getDefaultSharedPreferences(this).getBoolean(getString(
|
||||
R.string.allow_heap_dumping_key), false))
|
||||
LeakCanary.config = LeakCanary.config.copy(
|
||||
dumpHeap = PreferenceManager
|
||||
.getDefaultSharedPreferences(this).getBoolean(
|
||||
getString(
|
||||
R.string.allow_heap_dumping_key
|
||||
),
|
||||
false
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
override fun getDownloader(): Downloader {
|
||||
val downloader = DownloaderImpl.init(OkHttpClient.Builder()
|
||||
.addNetworkInterceptor(StethoInterceptor()))
|
||||
val downloader = DownloaderImpl.init(
|
||||
OkHttpClient.Builder()
|
||||
.addNetworkInterceptor(StethoInterceptor())
|
||||
)
|
||||
setCookiesToDownloader(downloader)
|
||||
return downloader
|
||||
}
|
||||
|
||||
override fun initACRA() {
|
||||
// install MultiDex before initializing ACRA
|
||||
MultiDex.install(this)
|
||||
super.initACRA()
|
||||
}
|
||||
|
||||
private fun initStetho() {
|
||||
// Create an InitializerBuilder
|
||||
val initializerBuilder = Stetho.newInitializerBuilder(this)
|
||||
@@ -43,7 +44,8 @@ class DebugApp : App() {
|
||||
|
||||
// Enable command line interface
|
||||
initializerBuilder.enableDumpapp(
|
||||
Stetho.defaultDumperPluginsProvider(applicationContext))
|
||||
Stetho.defaultDumperPluginsProvider(applicationContext)
|
||||
)
|
||||
|
||||
// Use the InitializerBuilder to generate an Initializer
|
||||
val initializer = initializerBuilder.build()
|
||||
@@ -54,6 +56,6 @@ class DebugApp : App() {
|
||||
|
||||
override fun isDisposedRxExceptionsReported(): Boolean {
|
||||
return PreferenceManager.getDefaultSharedPreferences(this)
|
||||
.getBoolean(getString(R.string.allow_disposed_exceptions_key), false)
|
||||
.getBoolean(getString(R.string.allow_disposed_exceptions_key), false)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -305,8 +305,7 @@
|
||||
<data android:host="peertube.cpy.re" />
|
||||
<data android:host="peertube.mastodon.host" />
|
||||
<data android:host="peertube.fr" />
|
||||
<data android:host="peertube.live" />
|
||||
<data android:host="peertube.video" />
|
||||
<data android:host="tilvids.com" />
|
||||
<data android:host="tube.privacytools.io" />
|
||||
<data android:host="video.ploud.fr" />
|
||||
<data android:host="video.lqdn.fr" />
|
||||
|
||||
@@ -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>
|
||||
@@ -86,8 +86,8 @@ public abstract class FragmentStatePagerAdapterMenuWorkaround extends PagerAdapt
|
||||
private final int mBehavior;
|
||||
private FragmentTransaction mCurTransaction = null;
|
||||
|
||||
private ArrayList<Fragment.SavedState> mSavedState = new ArrayList<Fragment.SavedState>();
|
||||
private ArrayList<Fragment> mFragments = new ArrayList<Fragment>();
|
||||
private final ArrayList<Fragment.SavedState> mSavedState = new ArrayList<Fragment.SavedState>();
|
||||
private final ArrayList<Fragment> mFragments = new ArrayList<Fragment>();
|
||||
private Fragment mCurrentPrimaryItem = null;
|
||||
|
||||
/**
|
||||
|
||||
@@ -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,8 @@ import android.os.Build;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.multidex.MultiDexApplication;
|
||||
import androidx.preference.PreferenceManager;
|
||||
|
||||
import com.nostra13.universalimageloader.cache.memory.impl.LRULimitedMemoryCache;
|
||||
@@ -23,6 +23,7 @@ import org.acra.config.CoreConfigurationBuilder;
|
||||
import org.schabi.newpipe.extractor.NewPipe;
|
||||
import org.schabi.newpipe.extractor.downloader.Downloader;
|
||||
import org.schabi.newpipe.report.ErrorActivity;
|
||||
import org.schabi.newpipe.report.ErrorInfo;
|
||||
import org.schabi.newpipe.report.UserAction;
|
||||
import org.schabi.newpipe.settings.SettingsActivity;
|
||||
import org.schabi.newpipe.util.ExceptionUtils;
|
||||
@@ -33,15 +34,17 @@ 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.exceptions.CompositeException;
|
||||
import io.reactivex.exceptions.MissingBackpressureException;
|
||||
import io.reactivex.exceptions.OnErrorNotImplementedException;
|
||||
import io.reactivex.exceptions.UndeliverableException;
|
||||
import io.reactivex.functions.Consumer;
|
||||
import io.reactivex.plugins.RxJavaPlugins;
|
||||
import io.reactivex.rxjava3.disposables.Disposable;
|
||||
import io.reactivex.rxjava3.exceptions.CompositeException;
|
||||
import io.reactivex.rxjava3.exceptions.MissingBackpressureException;
|
||||
import io.reactivex.rxjava3.exceptions.OnErrorNotImplementedException;
|
||||
import io.reactivex.rxjava3.exceptions.UndeliverableException;
|
||||
import io.reactivex.rxjava3.functions.Consumer;
|
||||
import io.reactivex.rxjava3.plugins.RxJavaPlugins;
|
||||
|
||||
/*
|
||||
* Copyright (C) Hans-Christoph Steiner 2016 <hans@eds.org>
|
||||
@@ -61,10 +64,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;
|
||||
|
||||
@Nullable private Disposable disposable = null;
|
||||
|
||||
@NonNull
|
||||
public static App getApp() {
|
||||
return app;
|
||||
}
|
||||
@@ -90,7 +96,7 @@ public class App extends Application {
|
||||
Localization.init(getApplicationContext());
|
||||
|
||||
StateSaver.init(this);
|
||||
initNotificationChannel();
|
||||
initNotificationChannels();
|
||||
|
||||
ServiceHelper.initServices(this);
|
||||
|
||||
@@ -100,7 +106,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() {
|
||||
@@ -214,54 +228,36 @@ public class App extends Application {
|
||||
ace,
|
||||
null,
|
||||
null,
|
||||
ErrorActivity.ErrorInfo.make(UserAction.SOMETHING_ELSE, "none",
|
||||
ErrorInfo.make(UserAction.SOMETHING_ELSE, "none",
|
||||
"Could not initialize ACRA crash report", R.string.app_ui_crash));
|
||||
}
|
||||
}
|
||||
|
||||
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() {
|
||||
|
||||
222
app/src/main/java/org/schabi/newpipe/CheckForNewAppVersion.java
Normal file
222
app/src/main/java/org/schabi/newpipe/CheckForNewAppVersion.java
Normal file
@@ -0,0 +1,222 @@
|
||||
package org.schabi.newpipe;
|
||||
|
||||
import android.app.Application;
|
||||
import android.app.PendingIntent;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.pm.PackageInfo;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.content.pm.Signature;
|
||||
import android.net.ConnectivityManager;
|
||||
import android.net.Uri;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.app.NotificationCompat;
|
||||
import androidx.core.app.NotificationManagerCompat;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.preference.PreferenceManager;
|
||||
|
||||
import com.grack.nanojson.JsonObject;
|
||||
import com.grack.nanojson.JsonParser;
|
||||
import com.grack.nanojson.JsonParserException;
|
||||
|
||||
import org.schabi.newpipe.report.ErrorActivity;
|
||||
import org.schabi.newpipe.report.ErrorInfo;
|
||||
import org.schabi.newpipe.report.UserAction;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.InputStream;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.cert.CertificateEncodingException;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.security.cert.CertificateFactory;
|
||||
import java.security.cert.X509Certificate;
|
||||
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.rxjava3.core.Maybe;
|
||||
import io.reactivex.rxjava3.disposables.Disposable;
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers;
|
||||
|
||||
public final class CheckForNewAppVersion {
|
||||
private CheckForNewAppVersion() { }
|
||||
|
||||
private static final boolean DEBUG = MainActivity.DEBUG;
|
||||
private static final String TAG = CheckForNewAppVersion.class.getSimpleName();
|
||||
|
||||
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";
|
||||
|
||||
/**
|
||||
* 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 fingerprint in hexadecimal
|
||||
*/
|
||||
@NonNull
|
||||
private static String getCertificateSHA1Fingerprint(@NonNull final Application application) {
|
||||
final PackageInfo packageInfo;
|
||||
try {
|
||||
packageInfo = application.getPackageManager().getPackageInfo(
|
||||
application.getPackageName(), PackageManager.GET_SIGNATURES);
|
||||
} catch (final PackageManager.NameNotFoundException e) {
|
||||
ErrorActivity.reportError(application, e, null, null,
|
||||
ErrorInfo.make(UserAction.SOMETHING_ELSE, "none",
|
||||
"Could not find package info", R.string.app_ui_crash));
|
||||
return "";
|
||||
}
|
||||
|
||||
final X509Certificate c;
|
||||
try {
|
||||
final Signature[] signatures = packageInfo.signatures;
|
||||
final byte[] cert = signatures[0].toByteArray();
|
||||
final InputStream input = new ByteArrayInputStream(cert);
|
||||
final CertificateFactory cf = CertificateFactory.getInstance("X509");
|
||||
c = (X509Certificate) cf.generateCertificate(input);
|
||||
} catch (final CertificateException e) {
|
||||
ErrorActivity.reportError(application, e, null, null,
|
||||
ErrorInfo.make(UserAction.SOMETHING_ELSE, "none",
|
||||
"Certificate error", R.string.app_ui_crash));
|
||||
return "";
|
||||
}
|
||||
|
||||
try {
|
||||
final MessageDigest md = MessageDigest.getInstance("SHA1");
|
||||
final byte[] publicKey = md.digest(c.getEncoded());
|
||||
return byte2HexFormatted(publicKey);
|
||||
} catch (NoSuchAlgorithmException | CertificateEncodingException e) {
|
||||
ErrorActivity.reportError(application, e, null, null,
|
||||
ErrorInfo.make(UserAction.SOMETHING_ELSE, "none",
|
||||
"Could not retrieve SHA1 key", R.string.app_ui_crash));
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
private static String byte2HexFormatted(final byte[] arr) {
|
||||
final StringBuilder str = new StringBuilder(arr.length * 2);
|
||||
|
||||
for (int i = 0; i < arr.length; i++) {
|
||||
String h = Integer.toHexString(arr[i]);
|
||||
final int l = h.length();
|
||||
if (l == 1) {
|
||||
h = "0" + h;
|
||||
}
|
||||
if (l > 2) {
|
||||
h = h.substring(l - 2, l);
|
||||
}
|
||||
str.append(h.toUpperCase());
|
||||
if (i < (arr.length - 1)) {
|
||||
str.append(':');
|
||||
}
|
||||
}
|
||||
return str.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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());
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isConnected(@NonNull final App app) {
|
||||
final ConnectivityManager connectivityManager =
|
||||
ContextCompat.getSystemService(app, ConnectivityManager.class);
|
||||
return connectivityManager != null && connectivityManager.getActiveNetworkInfo() != null
|
||||
&& connectivityManager.getActiveNetworkInfo().isConnected();
|
||||
}
|
||||
|
||||
public static boolean isGithubApk(@NonNull final App app) {
|
||||
return getCertificateSHA1Fingerprint(app).equals(GITHUB_APK_SHA1);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static Disposable checkNewVersion(@NonNull final App app) {
|
||||
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(app);
|
||||
|
||||
// Check if user has enabled/disabled update checking
|
||||
// and if the current apk is a github one or not.
|
||||
if (!prefs.getBoolean(app.getString(R.string.update_app_key), true) || !isGithubApk(app)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return Maybe
|
||||
.fromCallable(() -> {
|
||||
if (!isConnected(app)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Make a network request to get latest NewPipe data.
|
||||
return DownloaderImpl.getInstance().get(NEWPIPE_API_URL).responseBody();
|
||||
})
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
response -> {
|
||||
// Parse the json from the response.
|
||||
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(app, versionName,
|
||||
apkLocationUrl, versionCode);
|
||||
} catch (final JsonParserException e) {
|
||||
// connectivity problems, do not alarm user and fail silently
|
||||
if (DEBUG) {
|
||||
Log.w(TAG, "Could not get NewPipe API: invalid json", e);
|
||||
}
|
||||
}
|
||||
},
|
||||
e -> {
|
||||
// connectivity problems, do not alarm user and fail silently
|
||||
if (DEBUG) {
|
||||
Log.w(TAG, "Could not get NewPipe API: network problem", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,221 +0,0 @@
|
||||
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;
|
||||
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.core.app.NotificationCompat;
|
||||
import androidx.core.app.NotificationManagerCompat;
|
||||
|
||||
import com.grack.nanojson.JsonObject;
|
||||
import com.grack.nanojson.JsonParser;
|
||||
import com.grack.nanojson.JsonParserException;
|
||||
|
||||
import org.schabi.newpipe.extractor.exceptions.ReCaptchaException;
|
||||
import org.schabi.newpipe.report.ErrorActivity;
|
||||
import org.schabi.newpipe.report.UserAction;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.cert.CertificateEncodingException;
|
||||
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();
|
||||
|
||||
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";
|
||||
|
||||
/**
|
||||
* Method to get the apk's SHA1 key. See https://stackoverflow.com/questions/9293019/#22506133.
|
||||
*
|
||||
* @return String with the apk's SHA1 fingeprint in hexadecimal
|
||||
*/
|
||||
private static String getCertificateSHA1Fingerprint() {
|
||||
final PackageManager pm = APP.getPackageManager();
|
||||
final String packageName = APP.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.ErrorInfo.make(UserAction.SOMETHING_ELSE, "none",
|
||||
"Could not find package info", R.string.app_ui_crash));
|
||||
}
|
||||
|
||||
final Signature[] signatures = packageInfo.signatures;
|
||||
final byte[] cert = signatures[0].toByteArray();
|
||||
final InputStream input = new ByteArrayInputStream(cert);
|
||||
|
||||
X509Certificate c = null;
|
||||
|
||||
try {
|
||||
final CertificateFactory cf = CertificateFactory.getInstance("X509");
|
||||
c = (X509Certificate) cf.generateCertificate(input);
|
||||
} catch (final CertificateException e) {
|
||||
ErrorActivity.reportError(APP, e, null, null,
|
||||
ErrorActivity.ErrorInfo.make(UserAction.SOMETHING_ELSE, "none",
|
||||
"Certificate error", R.string.app_ui_crash));
|
||||
}
|
||||
|
||||
String hexString = null;
|
||||
|
||||
try {
|
||||
final MessageDigest md = MessageDigest.getInstance("SHA1");
|
||||
final byte[] publicKey = md.digest(c.getEncoded());
|
||||
hexString = byte2HexFormatted(publicKey);
|
||||
} catch (NoSuchAlgorithmException | CertificateEncodingException e) {
|
||||
ErrorActivity.reportError(APP, e, null, null,
|
||||
ErrorActivity.ErrorInfo.make(UserAction.SOMETHING_ELSE, "none",
|
||||
"Could not retrieve SHA1 key", R.string.app_ui_crash));
|
||||
}
|
||||
|
||||
return hexString;
|
||||
}
|
||||
|
||||
private static String byte2HexFormatted(final byte[] arr) {
|
||||
final StringBuilder str = new StringBuilder(arr.length * 2);
|
||||
|
||||
for (int i = 0; i < arr.length; i++) {
|
||||
String h = Integer.toHexString(arr[i]);
|
||||
final int l = h.length();
|
||||
if (l == 1) {
|
||||
h = "0" + h;
|
||||
}
|
||||
if (l > 2) {
|
||||
h = h.substring(l - 2, l);
|
||||
}
|
||||
str.append(h.toUpperCase());
|
||||
if (i < (arr.length - 1)) {
|
||||
str.append(':');
|
||||
}
|
||||
}
|
||||
return str.toString();
|
||||
}
|
||||
|
||||
public static boolean isGithubApk() {
|
||||
return getCertificateSHA1Fingerprint().equals(GITHUB_APK_SHA1);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPreExecute() {
|
||||
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;
|
||||
}
|
||||
|
||||
// Make a network request to get latest NewPipe data.
|
||||
try {
|
||||
return DownloaderImpl.getInstance().get(NEWPIPE_API_URL).responseBody();
|
||||
} catch (IOException | ReCaptchaException e) {
|
||||
// connectivity problems, do not alarm user and fail silently
|
||||
if (DEBUG) {
|
||||
Log.w(TAG, Log.getStackTraceString(e));
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(final String response) {
|
||||
// Parse the json from the response.
|
||||
if (response != null) {
|
||||
|
||||
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) {
|
||||
// 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;
|
||||
|
||||
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(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();
|
||||
}
|
||||
}
|
||||
@@ -50,8 +50,8 @@ public final class DownloaderImpl extends Downloader {
|
||||
public static final String YOUTUBE_DOMAIN = "youtube.com";
|
||||
|
||||
private static DownloaderImpl instance;
|
||||
private Map<String, String> mCookies;
|
||||
private OkHttpClient client;
|
||||
private final Map<String, String> mCookies;
|
||||
private final OkHttpClient client;
|
||||
|
||||
private DownloaderImpl(final OkHttpClient.Builder builder) {
|
||||
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.KITKAT) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -137,8 +139,7 @@ public class MainActivity extends AppCompatActivity {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_main);
|
||||
|
||||
if (getSupportFragmentManager() != null
|
||||
&& getSupportFragmentManager().getBackStackEntryCount() == 0) {
|
||||
if (getSupportFragmentManager().getBackStackEntryCount() == 0) {
|
||||
initFragments();
|
||||
}
|
||||
|
||||
@@ -152,7 +153,7 @@ public class MainActivity extends AppCompatActivity {
|
||||
if (DeviceUtils.isTv(this)) {
|
||||
FocusOverlayView.setupFocusObserver(this);
|
||||
}
|
||||
setupBroadcastReceiver();
|
||||
openMiniPlayerUponPlayerStarted();
|
||||
}
|
||||
|
||||
private void setupDrawer() throws Exception {
|
||||
@@ -758,32 +759,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 +810,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() {
|
||||
|
||||
@@ -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,19 +62,17 @@ 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;
|
||||
import icepick.State;
|
||||
import io.reactivex.Observable;
|
||||
import io.reactivex.Single;
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.disposables.CompositeDisposable;
|
||||
import io.reactivex.disposables.Disposable;
|
||||
import io.reactivex.functions.Consumer;
|
||||
import io.reactivex.schedulers.Schedulers;
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.rxjava3.core.Observable;
|
||||
import io.reactivex.rxjava3.core.Single;
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
||||
import io.reactivex.rxjava3.disposables.Disposable;
|
||||
import io.reactivex.rxjava3.functions.Consumer;
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers;
|
||||
|
||||
import static org.schabi.newpipe.extractor.StreamingService.ServiceInfo.MediaCapability.AUDIO;
|
||||
import static org.schabi.newpipe.extractor.StreamingService.ServiceInfo.MediaCapability.VIDEO;
|
||||
@@ -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,10 +516,10 @@ 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) -> {
|
||||
.subscribe(result -> {
|
||||
final List<VideoStream> sortedVideoStreams = ListHelper
|
||||
.getSortedStreamVideosList(this, result.getVideoStreams(),
|
||||
result.getVideoOnlyStreams(), false);
|
||||
@@ -531,10 +533,9 @@ public class RouterActivity extends AppCompatActivity {
|
||||
downloadDialog.setSelectedVideoStream(selectedVideoStreamIndex);
|
||||
downloadDialog.show(fm, "downloadDialog");
|
||||
fm.executePendingTransactions();
|
||||
downloadDialog.getDialog().setOnDismissListener(dialog -> finish());
|
||||
}, (@NonNull Throwable throwable) -> {
|
||||
showUnsupportedUrlDialog(currentUrl);
|
||||
});
|
||||
downloadDialog.requireDialog().setOnDismissListener(dialog -> finish());
|
||||
}, throwable ->
|
||||
showUnsupportedUrlDialog(currentUrl)));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -552,66 +553,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 +665,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();
|
||||
|
||||
@@ -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;
|
||||
@@ -30,9 +31,9 @@ public class AboutActivity extends AppCompatActivity {
|
||||
/**
|
||||
* List of all software components.
|
||||
*/
|
||||
private static final SoftwareComponent[] SOFTWARE_COMPONENTS = new SoftwareComponent[]{
|
||||
private static final SoftwareComponent[] SOFTWARE_COMPONENTS = {
|
||||
new SoftwareComponent("Giga Get", "2014 - 2015", "Peter Cai",
|
||||
"https://github.com/PaperAirplane-Dev-Team/GigaGet", StandardLicenses.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,80 +0,0 @@
|
||||
package org.schabi.newpipe.about;
|
||||
|
||||
import android.net.Uri;
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* Class for storing information about a software license.
|
||||
*/
|
||||
public class License implements Parcelable, Serializable {
|
||||
public static final Creator<License> CREATOR = new Creator<License>() {
|
||||
@Override
|
||||
public License createFromParcel(final Parcel source) {
|
||||
return new License(source);
|
||||
}
|
||||
|
||||
@Override
|
||||
public License[] newArray(final int size) {
|
||||
return new License[size];
|
||||
}
|
||||
};
|
||||
private final String abbreviation;
|
||||
private final String name;
|
||||
private String filename;
|
||||
|
||||
public License(final String name, final String abbreviation, final String filename) {
|
||||
if (name == null) {
|
||||
throw new NullPointerException("name is null");
|
||||
}
|
||||
if (abbreviation == null) {
|
||||
throw new NullPointerException("abbreviation is null");
|
||||
}
|
||||
if (filename == null) {
|
||||
throw new NullPointerException("filename is null");
|
||||
}
|
||||
this.name = name;
|
||||
this.filename = filename;
|
||||
this.abbreviation = abbreviation;
|
||||
}
|
||||
|
||||
protected License(final Parcel in) {
|
||||
this.filename = in.readString();
|
||||
this.abbreviation = in.readString();
|
||||
this.name = in.readString();
|
||||
}
|
||||
|
||||
public Uri getContentUri() {
|
||||
return new Uri.Builder()
|
||||
.scheme("file")
|
||||
.path("/android_asset")
|
||||
.appendPath(filename)
|
||||
.build();
|
||||
}
|
||||
|
||||
public String getAbbreviation() {
|
||||
return abbreviation;
|
||||
}
|
||||
|
||||
public String getFilename() {
|
||||
return filename;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int describeContents() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeToParcel(final Parcel dest, final int flags) {
|
||||
dest.writeString(this.filename);
|
||||
dest.writeString(this.abbreviation);
|
||||
dest.writeString(this.name);
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
}
|
||||
19
app/src/main/java/org/schabi/newpipe/about/License.kt
Normal file
19
app/src/main/java/org/schabi/newpipe/about/License.kt
Normal file
@@ -0,0 +1,19 @@
|
||||
package org.schabi.newpipe.about
|
||||
|
||||
import android.net.Uri
|
||||
import android.os.Parcelable
|
||||
import kotlinx.android.parcel.Parcelize
|
||||
import java.io.Serializable
|
||||
|
||||
/**
|
||||
* Class for storing information about a software license.
|
||||
*/
|
||||
@Parcelize
|
||||
class License(val name: String, val abbreviation: String, val filename: String) : Parcelable, Serializable {
|
||||
val contentUri: Uri
|
||||
get() = Uri.Builder()
|
||||
.scheme("file")
|
||||
.path("/android_asset")
|
||||
.appendPath(filename)
|
||||
.build()
|
||||
}
|
||||
@@ -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.rxjava3.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;
|
||||
}
|
||||
|
||||
@@ -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,17 @@ 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.rxjava3.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.rxjava3.core.Observable;
|
||||
import io.reactivex.rxjava3.disposables.Disposable;
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers;
|
||||
|
||||
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
|
||||
|
||||
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
|
||||
@@ -39,14 +36,12 @@ public class LicenseFragmentHelper extends AsyncTask<Object, Void, Integer> {
|
||||
@NonNull final License license) {
|
||||
final StringBuilder licenseContent = new StringBuilder();
|
||||
final String webViewData;
|
||||
try {
|
||||
final BufferedReader in = new BufferedReader(new InputStreamReader(
|
||||
context.getAssets().open(license.getFilename()), StandardCharsets.UTF_8));
|
||||
try (BufferedReader in = new BufferedReader(new InputStreamReader(
|
||||
context.getAssets().open(license.getFilename()), StandardCharsets.UTF_8))) {
|
||||
String str;
|
||||
while ((str = in.readLine()) != null) {
|
||||
licenseContent.append(str);
|
||||
}
|
||||
in.close();
|
||||
|
||||
// split the HTML file and insert the stylesheet into the HEAD of the file
|
||||
webViewData = licenseContent.toString().replace("</head>",
|
||||
@@ -59,10 +54,10 @@ public class LicenseFragmentHelper extends AsyncTask<Object, Void, Integer> {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param context
|
||||
* @param context the Android 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 +79,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 Disposable.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();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,83 +0,0 @@
|
||||
package org.schabi.newpipe.about;
|
||||
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
|
||||
public class SoftwareComponent implements Parcelable {
|
||||
public static final Creator<SoftwareComponent> CREATOR = new Creator<SoftwareComponent>() {
|
||||
@Override
|
||||
public SoftwareComponent createFromParcel(final Parcel source) {
|
||||
return new SoftwareComponent(source);
|
||||
}
|
||||
|
||||
@Override
|
||||
public SoftwareComponent[] newArray(final int size) {
|
||||
return new SoftwareComponent[size];
|
||||
}
|
||||
};
|
||||
|
||||
private final License license;
|
||||
private final String name;
|
||||
private final String years;
|
||||
private final String copyrightOwner;
|
||||
private final String link;
|
||||
private final String version;
|
||||
|
||||
public SoftwareComponent(final String name, final String years, final String copyrightOwner,
|
||||
final String link, final License license) {
|
||||
this.name = name;
|
||||
this.years = years;
|
||||
this.copyrightOwner = copyrightOwner;
|
||||
this.link = link;
|
||||
this.license = license;
|
||||
this.version = null;
|
||||
}
|
||||
|
||||
protected SoftwareComponent(final Parcel in) {
|
||||
this.name = in.readString();
|
||||
this.license = in.readParcelable(License.class.getClassLoader());
|
||||
this.copyrightOwner = in.readString();
|
||||
this.link = in.readString();
|
||||
this.years = in.readString();
|
||||
this.version = in.readString();
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public String getYears() {
|
||||
return years;
|
||||
}
|
||||
|
||||
public String getCopyrightOwner() {
|
||||
return copyrightOwner;
|
||||
}
|
||||
|
||||
public String getLink() {
|
||||
return link;
|
||||
}
|
||||
|
||||
public String getVersion() {
|
||||
return version;
|
||||
}
|
||||
|
||||
public License getLicense() {
|
||||
return license;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int describeContents() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeToParcel(final Parcel dest, final int flags) {
|
||||
dest.writeString(name);
|
||||
dest.writeParcelable(license, flags);
|
||||
dest.writeString(copyrightOwner);
|
||||
dest.writeString(link);
|
||||
dest.writeString(years);
|
||||
dest.writeString(version);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package org.schabi.newpipe.about
|
||||
|
||||
import android.os.Parcelable
|
||||
import kotlinx.android.parcel.Parcelize
|
||||
|
||||
@Parcelize
|
||||
class SoftwareComponent
|
||||
@JvmOverloads
|
||||
constructor(
|
||||
val name: String,
|
||||
val years: String,
|
||||
val copyrightOwner: String,
|
||||
val link: String,
|
||||
val license: License,
|
||||
val version: String? = null
|
||||
) : Parcelable
|
||||
@@ -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
|
||||
|
||||
@@ -9,7 +9,7 @@ import androidx.room.Update;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
import io.reactivex.Flowable;
|
||||
import io.reactivex.rxjava3.core.Flowable;
|
||||
|
||||
@Dao
|
||||
public interface BasicDAO<Entity> {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -6,19 +6,20 @@ import androidx.room.OnConflictStrategy
|
||||
import androidx.room.Query
|
||||
import androidx.room.Transaction
|
||||
import androidx.room.Update
|
||||
import io.reactivex.Flowable
|
||||
import java.util.Date
|
||||
import io.reactivex.rxjava3.core.Flowable
|
||||
import org.schabi.newpipe.database.feed.model.FeedEntity
|
||||
import org.schabi.newpipe.database.feed.model.FeedLastUpdatedEntity
|
||||
import org.schabi.newpipe.database.stream.model.StreamEntity
|
||||
import org.schabi.newpipe.database.subscription.SubscriptionEntity
|
||||
import java.time.OffsetDateTime
|
||||
|
||||
@Dao
|
||||
abstract class FeedDAO {
|
||||
@Query("DELETE FROM feed")
|
||||
abstract fun deleteAll(): Int
|
||||
|
||||
@Query("""
|
||||
@Query(
|
||||
"""
|
||||
SELECT s.* FROM streams s
|
||||
|
||||
INNER JOIN feed f
|
||||
@@ -27,10 +28,12 @@ abstract class FeedDAO {
|
||||
ORDER BY s.upload_date IS NULL DESC, s.upload_date DESC, s.uploader ASC
|
||||
|
||||
LIMIT 500
|
||||
""")
|
||||
"""
|
||||
)
|
||||
abstract fun getAllStreams(): Flowable<List<StreamEntity>>
|
||||
|
||||
@Query("""
|
||||
@Query(
|
||||
"""
|
||||
SELECT s.* FROM streams s
|
||||
|
||||
INNER JOIN feed f
|
||||
@@ -46,10 +49,12 @@ abstract class FeedDAO {
|
||||
|
||||
ORDER BY s.upload_date IS NULL DESC, s.upload_date DESC, s.uploader ASC
|
||||
LIMIT 500
|
||||
""")
|
||||
"""
|
||||
)
|
||||
abstract fun getAllStreamsFromGroup(groupId: Long): Flowable<List<StreamEntity>>
|
||||
|
||||
@Query("""
|
||||
@Query(
|
||||
"""
|
||||
DELETE FROM feed WHERE
|
||||
|
||||
feed.stream_id IN (
|
||||
@@ -58,12 +63,14 @@ 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("""
|
||||
@Query(
|
||||
"""
|
||||
DELETE FROM feed
|
||||
|
||||
WHERE feed.subscription_id = :subscriptionId
|
||||
@@ -76,7 +83,8 @@ abstract class FeedDAO {
|
||||
|
||||
WHERE s.stream_type = "LIVE_STREAM" OR s.stream_type = "AUDIO_LIVE_STREAM"
|
||||
)
|
||||
""")
|
||||
"""
|
||||
)
|
||||
abstract fun unlinkOldLivestreams(subscriptionId: Long)
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
||||
@@ -100,21 +108,24 @@ abstract class FeedDAO {
|
||||
}
|
||||
}
|
||||
|
||||
@Query("""
|
||||
@Query(
|
||||
"""
|
||||
SELECT MIN(lu.last_updated) FROM feed_last_updated lu
|
||||
|
||||
INNER JOIN feed_group_subscription_join fgs
|
||||
ON fgs.subscription_id = lu.subscription_id AND fgs.group_id = :groupId
|
||||
""")
|
||||
abstract fun oldestSubscriptionUpdate(groupId: Long): Flowable<List<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>
|
||||
|
||||
@Query("""
|
||||
@Query(
|
||||
"""
|
||||
SELECT COUNT(*) FROM subscriptions s
|
||||
|
||||
INNER JOIN feed_group_subscription_join fgs
|
||||
@@ -124,20 +135,24 @@ abstract class FeedDAO {
|
||||
ON s.uid = lu.subscription_id
|
||||
|
||||
WHERE lu.last_updated IS NULL
|
||||
""")
|
||||
"""
|
||||
)
|
||||
abstract fun notLoadedCountForGroup(groupId: Long): Flowable<Long>
|
||||
|
||||
@Query("""
|
||||
@Query(
|
||||
"""
|
||||
SELECT s.* FROM subscriptions s
|
||||
|
||||
LEFT JOIN feed_last_updated lu
|
||||
ON s.uid = lu.subscription_id
|
||||
|
||||
WHERE lu.last_updated IS NULL OR lu.last_updated < :outdatedThreshold
|
||||
""")
|
||||
abstract fun getAllOutdated(outdatedThreshold: Date): Flowable<List<SubscriptionEntity>>
|
||||
"""
|
||||
)
|
||||
abstract fun getAllOutdated(outdatedThreshold: OffsetDateTime): Flowable<List<SubscriptionEntity>>
|
||||
|
||||
@Query("""
|
||||
@Query(
|
||||
"""
|
||||
SELECT s.* FROM subscriptions s
|
||||
|
||||
INNER JOIN feed_group_subscription_join fgs
|
||||
@@ -147,6 +162,7 @@ abstract class FeedDAO {
|
||||
ON s.uid = lu.subscription_id
|
||||
|
||||
WHERE lu.last_updated IS NULL OR lu.last_updated < :outdatedThreshold
|
||||
""")
|
||||
abstract fun getAllOutdatedForGroup(groupId: Long, outdatedThreshold: Date): Flowable<List<SubscriptionEntity>>
|
||||
"""
|
||||
)
|
||||
abstract fun getAllOutdatedForGroup(groupId: Long, outdatedThreshold: OffsetDateTime): Flowable<List<SubscriptionEntity>>
|
||||
}
|
||||
|
||||
@@ -6,8 +6,8 @@ import androidx.room.OnConflictStrategy
|
||||
import androidx.room.Query
|
||||
import androidx.room.Transaction
|
||||
import androidx.room.Update
|
||||
import io.reactivex.Flowable
|
||||
import io.reactivex.Maybe
|
||||
import io.reactivex.rxjava3.core.Flowable
|
||||
import io.reactivex.rxjava3.core.Maybe
|
||||
import org.schabi.newpipe.database.feed.model.FeedGroupEntity
|
||||
import org.schabi.newpipe.database.feed.model.FeedGroupSubscriptionEntity
|
||||
|
||||
|
||||
@@ -10,21 +10,24 @@ import org.schabi.newpipe.database.feed.model.FeedEntity.Companion.SUBSCRIPTION_
|
||||
import org.schabi.newpipe.database.stream.model.StreamEntity
|
||||
import org.schabi.newpipe.database.subscription.SubscriptionEntity
|
||||
|
||||
@Entity(tableName = FEED_TABLE,
|
||||
primaryKeys = [STREAM_ID, SUBSCRIPTION_ID],
|
||||
indices = [Index(SUBSCRIPTION_ID)],
|
||||
foreignKeys = [
|
||||
ForeignKey(
|
||||
entity = StreamEntity::class,
|
||||
parentColumns = [StreamEntity.STREAM_ID],
|
||||
childColumns = [STREAM_ID],
|
||||
onDelete = ForeignKey.CASCADE, onUpdate = ForeignKey.CASCADE, deferred = true),
|
||||
ForeignKey(
|
||||
entity = SubscriptionEntity::class,
|
||||
parentColumns = [SubscriptionEntity.SUBSCRIPTION_UID],
|
||||
childColumns = [SUBSCRIPTION_ID],
|
||||
onDelete = ForeignKey.CASCADE, onUpdate = ForeignKey.CASCADE, deferred = true)
|
||||
]
|
||||
@Entity(
|
||||
tableName = FEED_TABLE,
|
||||
primaryKeys = [STREAM_ID, SUBSCRIPTION_ID],
|
||||
indices = [Index(SUBSCRIPTION_ID)],
|
||||
foreignKeys = [
|
||||
ForeignKey(
|
||||
entity = StreamEntity::class,
|
||||
parentColumns = [StreamEntity.STREAM_ID],
|
||||
childColumns = [STREAM_ID],
|
||||
onDelete = ForeignKey.CASCADE, onUpdate = ForeignKey.CASCADE, deferred = true
|
||||
),
|
||||
ForeignKey(
|
||||
entity = SubscriptionEntity::class,
|
||||
parentColumns = [SubscriptionEntity.SUBSCRIPTION_UID],
|
||||
childColumns = [SUBSCRIPTION_ID],
|
||||
onDelete = ForeignKey.CASCADE, onUpdate = ForeignKey.CASCADE, deferred = true
|
||||
)
|
||||
]
|
||||
)
|
||||
data class FeedEntity(
|
||||
@ColumnInfo(name = STREAM_ID)
|
||||
|
||||
@@ -9,8 +9,8 @@ import org.schabi.newpipe.database.feed.model.FeedGroupEntity.Companion.SORT_ORD
|
||||
import org.schabi.newpipe.local.subscription.FeedGroupIcon
|
||||
|
||||
@Entity(
|
||||
tableName = FEED_GROUP_TABLE,
|
||||
indices = [Index(SORT_ORDER)]
|
||||
tableName = FEED_GROUP_TABLE,
|
||||
indices = [Index(SORT_ORDER)]
|
||||
)
|
||||
data class FeedGroupEntity(
|
||||
@PrimaryKey(autoGenerate = true)
|
||||
|
||||
@@ -11,22 +11,24 @@ import org.schabi.newpipe.database.feed.model.FeedGroupSubscriptionEntity.Compan
|
||||
import org.schabi.newpipe.database.subscription.SubscriptionEntity
|
||||
|
||||
@Entity(
|
||||
tableName = FEED_GROUP_SUBSCRIPTION_TABLE,
|
||||
primaryKeys = [GROUP_ID, SUBSCRIPTION_ID],
|
||||
indices = [Index(SUBSCRIPTION_ID)],
|
||||
foreignKeys = [
|
||||
ForeignKey(
|
||||
entity = FeedGroupEntity::class,
|
||||
parentColumns = [FeedGroupEntity.ID],
|
||||
childColumns = [GROUP_ID],
|
||||
onDelete = CASCADE, onUpdate = CASCADE, deferred = true),
|
||||
tableName = FEED_GROUP_SUBSCRIPTION_TABLE,
|
||||
primaryKeys = [GROUP_ID, SUBSCRIPTION_ID],
|
||||
indices = [Index(SUBSCRIPTION_ID)],
|
||||
foreignKeys = [
|
||||
ForeignKey(
|
||||
entity = FeedGroupEntity::class,
|
||||
parentColumns = [FeedGroupEntity.ID],
|
||||
childColumns = [GROUP_ID],
|
||||
onDelete = CASCADE, onUpdate = CASCADE, deferred = true
|
||||
),
|
||||
|
||||
ForeignKey(
|
||||
entity = SubscriptionEntity::class,
|
||||
parentColumns = [SubscriptionEntity.SUBSCRIPTION_UID],
|
||||
childColumns = [SUBSCRIPTION_ID],
|
||||
onDelete = CASCADE, onUpdate = CASCADE, deferred = true)
|
||||
]
|
||||
ForeignKey(
|
||||
entity = SubscriptionEntity::class,
|
||||
parentColumns = [SubscriptionEntity.SUBSCRIPTION_UID],
|
||||
childColumns = [SUBSCRIPTION_ID],
|
||||
onDelete = CASCADE, onUpdate = CASCADE, deferred = true
|
||||
)
|
||||
]
|
||||
)
|
||||
data class FeedGroupSubscriptionEntity(
|
||||
@ColumnInfo(name = GROUP_ID)
|
||||
|
||||
@@ -4,20 +4,21 @@ import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.ForeignKey
|
||||
import androidx.room.PrimaryKey
|
||||
import java.util.Date
|
||||
import org.schabi.newpipe.database.feed.model.FeedLastUpdatedEntity.Companion.FEED_LAST_UPDATED_TABLE
|
||||
import org.schabi.newpipe.database.feed.model.FeedLastUpdatedEntity.Companion.SUBSCRIPTION_ID
|
||||
import org.schabi.newpipe.database.subscription.SubscriptionEntity
|
||||
import java.time.OffsetDateTime
|
||||
|
||||
@Entity(
|
||||
tableName = FEED_LAST_UPDATED_TABLE,
|
||||
foreignKeys = [
|
||||
ForeignKey(
|
||||
entity = SubscriptionEntity::class,
|
||||
parentColumns = [SubscriptionEntity.SUBSCRIPTION_UID],
|
||||
childColumns = [SUBSCRIPTION_ID],
|
||||
onDelete = ForeignKey.CASCADE, onUpdate = ForeignKey.CASCADE, deferred = true)
|
||||
]
|
||||
tableName = FEED_LAST_UPDATED_TABLE,
|
||||
foreignKeys = [
|
||||
ForeignKey(
|
||||
entity = SubscriptionEntity::class,
|
||||
parentColumns = [SubscriptionEntity.SUBSCRIPTION_UID],
|
||||
childColumns = [SUBSCRIPTION_ID],
|
||||
onDelete = ForeignKey.CASCADE, onUpdate = ForeignKey.CASCADE, deferred = true
|
||||
)
|
||||
]
|
||||
)
|
||||
data class FeedLastUpdatedEntity(
|
||||
@PrimaryKey
|
||||
@@ -25,9 +26,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"
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import org.schabi.newpipe.database.history.model.SearchHistoryEntry;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import io.reactivex.Flowable;
|
||||
import io.reactivex.rxjava3.core.Flowable;
|
||||
|
||||
import static org.schabi.newpipe.database.history.model.SearchHistoryEntry.CREATION_DATE;
|
||||
import static org.schabi.newpipe.database.history.model.SearchHistoryEntry.ID;
|
||||
|
||||
@@ -10,7 +10,7 @@ import org.schabi.newpipe.database.stream.StreamStatisticsEntry;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import io.reactivex.Flowable;
|
||||
import io.reactivex.rxjava3.core.Flowable;
|
||||
|
||||
import static org.schabi.newpipe.database.history.model.StreamHistoryEntity.JOIN_STREAM_ID;
|
||||
import static org.schabi.newpipe.database.history.model.StreamHistoryEntity.STREAM_ACCESS_DATE;
|
||||
@@ -20,6 +20,9 @@ import static org.schabi.newpipe.database.stream.StreamStatisticsEntry.STREAM_LA
|
||||
import static org.schabi.newpipe.database.stream.StreamStatisticsEntry.STREAM_WATCH_COUNT;
|
||||
import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_ID;
|
||||
import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_TABLE;
|
||||
import static org.schabi.newpipe.database.stream.model.StreamStateEntity.JOIN_STREAM_ID_ALIAS;
|
||||
import static org.schabi.newpipe.database.stream.model.StreamStateEntity.STREAM_PROGRESS_TIME;
|
||||
import static org.schabi.newpipe.database.stream.model.StreamStateEntity.STREAM_STATE_TABLE;
|
||||
|
||||
@Dao
|
||||
public abstract class StreamHistoryDAO implements HistoryDAO<StreamHistoryEntity> {
|
||||
@@ -73,6 +76,12 @@ public abstract class StreamHistoryDAO implements HistoryDAO<StreamHistoryEntity
|
||||
+ " SUM(" + STREAM_REPEAT_COUNT + ") AS " + STREAM_WATCH_COUNT
|
||||
+ " FROM " + STREAM_HISTORY_TABLE + " GROUP BY " + JOIN_STREAM_ID + ")"
|
||||
|
||||
+ " ON " + STREAM_ID + " = " + JOIN_STREAM_ID)
|
||||
+ " ON " + STREAM_ID + " = " + JOIN_STREAM_ID
|
||||
|
||||
+ " LEFT JOIN "
|
||||
+ "(SELECT " + JOIN_STREAM_ID + " AS " + JOIN_STREAM_ID_ALIAS + ", "
|
||||
+ STREAM_PROGRESS_TIME
|
||||
+ " FROM " + STREAM_STATE_TABLE + " )"
|
||||
+ " ON " + STREAM_ID + " = " + JOIN_STREAM_ID_ALIAS)
|
||||
public abstract Flowable<List<StreamStatisticsEntry>> getStatistics();
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@ package org.schabi.newpipe.database.history.model
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Embedded
|
||||
import java.util.Date
|
||||
import org.schabi.newpipe.database.stream.model.StreamEntity
|
||||
import java.time.OffsetDateTime
|
||||
|
||||
data class StreamHistoryEntry(
|
||||
@Embedded
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -5,12 +5,17 @@ import androidx.room.Embedded
|
||||
import org.schabi.newpipe.database.LocalItem
|
||||
import org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity
|
||||
import org.schabi.newpipe.database.stream.model.StreamEntity
|
||||
import org.schabi.newpipe.database.stream.model.StreamStateEntity
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem
|
||||
import kotlin.jvm.Throws
|
||||
|
||||
class PlaylistStreamEntry(
|
||||
@Embedded
|
||||
val streamEntity: StreamEntity,
|
||||
|
||||
@ColumnInfo(name = StreamStateEntity.STREAM_PROGRESS_TIME, defaultValue = "0")
|
||||
val progressTime: Long,
|
||||
|
||||
@ColumnInfo(name = PlaylistStreamEntity.JOIN_STREAM_ID)
|
||||
val streamId: Long,
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import org.schabi.newpipe.database.playlist.model.PlaylistEntity;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import io.reactivex.Flowable;
|
||||
import io.reactivex.rxjava3.core.Flowable;
|
||||
|
||||
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_ID;
|
||||
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_TABLE;
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import io.reactivex.Flowable;
|
||||
import io.reactivex.rxjava3.core.Flowable;
|
||||
|
||||
import static org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity.REMOTE_PLAYLIST_ID;
|
||||
import static org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity.REMOTE_PLAYLIST_SERVICE_ID;
|
||||
|
||||
@@ -11,7 +11,7 @@ import org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import io.reactivex.Flowable;
|
||||
import io.reactivex.rxjava3.core.Flowable;
|
||||
|
||||
import static org.schabi.newpipe.database.playlist.PlaylistMetadataEntry.PLAYLIST_STREAM_COUNT;
|
||||
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_ID;
|
||||
@@ -24,6 +24,9 @@ import static org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity.JO
|
||||
import static org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity.PLAYLIST_STREAM_JOIN_TABLE;
|
||||
import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_ID;
|
||||
import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_TABLE;
|
||||
import static org.schabi.newpipe.database.stream.model.StreamStateEntity.JOIN_STREAM_ID_ALIAS;
|
||||
import static org.schabi.newpipe.database.stream.model.StreamStateEntity.STREAM_PROGRESS_TIME;
|
||||
import static org.schabi.newpipe.database.stream.model.StreamStateEntity.STREAM_STATE_TABLE;
|
||||
|
||||
@Dao
|
||||
public abstract class PlaylistStreamDAO implements BasicDAO<PlaylistStreamEntity> {
|
||||
@@ -58,6 +61,13 @@ public abstract class PlaylistStreamDAO implements BasicDAO<PlaylistStreamEntity
|
||||
|
||||
// then merge with the stream metadata
|
||||
+ " ON " + STREAM_ID + " = " + JOIN_STREAM_ID
|
||||
|
||||
+ " LEFT JOIN "
|
||||
+ "(SELECT " + JOIN_STREAM_ID + " AS " + JOIN_STREAM_ID_ALIAS + ", "
|
||||
+ STREAM_PROGRESS_TIME
|
||||
+ " FROM " + STREAM_STATE_TABLE + " )"
|
||||
+ " ON " + STREAM_ID + " = " + JOIN_STREAM_ID_ALIAS
|
||||
|
||||
+ " ORDER BY " + JOIN_INDEX + " ASC")
|
||||
public abstract Flowable<List<PlaylistStreamEntry>> getOrderedStreamsOf(long playlistId);
|
||||
|
||||
|
||||
@@ -2,26 +2,29 @@ package org.schabi.newpipe.database.stream
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Embedded
|
||||
import java.util.Date
|
||||
import org.schabi.newpipe.database.LocalItem
|
||||
import org.schabi.newpipe.database.history.model.StreamHistoryEntity
|
||||
import org.schabi.newpipe.database.stream.model.StreamEntity
|
||||
import org.schabi.newpipe.database.stream.model.StreamStateEntity.STREAM_PROGRESS_TIME
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem
|
||||
import java.time.OffsetDateTime
|
||||
|
||||
class StreamStatisticsEntry(
|
||||
@Embedded
|
||||
val streamEntity: StreamEntity,
|
||||
|
||||
@ColumnInfo(name = STREAM_PROGRESS_TIME, defaultValue = "0")
|
||||
val progressTime: Long,
|
||||
|
||||
@ColumnInfo(name = StreamHistoryEntity.JOIN_STREAM_ID)
|
||||
val streamId: Long,
|
||||
|
||||
@ColumnInfo(name = STREAM_LATEST_DATE)
|
||||
val latestAccessDate: Date,
|
||||
val latestAccessDate: OffsetDateTime,
|
||||
|
||||
@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
|
||||
|
||||
@@ -6,14 +6,14 @@ import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.Query
|
||||
import androidx.room.Transaction
|
||||
import io.reactivex.Flowable
|
||||
import java.util.Date
|
||||
import io.reactivex.rxjava3.core.Flowable
|
||||
import org.schabi.newpipe.database.BasicDAO
|
||||
import org.schabi.newpipe.database.stream.model.StreamEntity
|
||||
import org.schabi.newpipe.database.stream.model.StreamEntity.Companion.STREAM_ID
|
||||
import org.schabi.newpipe.extractor.stream.StreamType
|
||||
import org.schabi.newpipe.extractor.stream.StreamType.AUDIO_LIVE_STREAM
|
||||
import org.schabi.newpipe.extractor.stream.StreamType.LIVE_STREAM
|
||||
import java.time.OffsetDateTime
|
||||
|
||||
@Dao
|
||||
abstract class StreamDAO : BasicDAO<StreamEntity> {
|
||||
@@ -35,10 +35,12 @@ abstract class StreamDAO : BasicDAO<StreamEntity> {
|
||||
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
||||
internal abstract fun silentInsertAllInternal(streams: List<StreamEntity>): List<Long>
|
||||
|
||||
@Query("""
|
||||
@Query(
|
||||
"""
|
||||
SELECT uid, stream_type, textual_upload_date, upload_date, is_upload_date_approximation, duration
|
||||
FROM streams WHERE url = :url AND service_id = :serviceId
|
||||
""")
|
||||
"""
|
||||
)
|
||||
internal abstract fun getMinimalStreamForCompare(serviceId: Int, url: String): StreamCompareFeed?
|
||||
|
||||
@Transaction
|
||||
@@ -79,7 +81,7 @@ abstract class StreamDAO : BasicDAO<StreamEntity> {
|
||||
|
||||
private fun compareAndUpdateStream(newerStream: StreamEntity) {
|
||||
val existentMinimalStream = getMinimalStreamForCompare(newerStream.serviceId, newerStream.url)
|
||||
?: throw IllegalStateException("Stream cannot be null just after insertion.")
|
||||
?: throw IllegalStateException("Stream cannot be null just after insertion.")
|
||||
newerStream.uid = existentMinimalStream.uid
|
||||
|
||||
val isNewerStreamLive = newerStream.streamType == AUDIO_LIVE_STREAM || newerStream.streamType == LIVE_STREAM
|
||||
@@ -88,7 +90,7 @@ abstract class StreamDAO : BasicDAO<StreamEntity> {
|
||||
// Use the existent upload date if the newer stream does not have a better precision
|
||||
// (i.e. is an approximation). This is done to prevent unnecessary changes.
|
||||
val hasBetterPrecision =
|
||||
newerStream.uploadDate != null && newerStream.isUploadDateApproximation != true
|
||||
newerStream.uploadDate != null && newerStream.isUploadDateApproximation != true
|
||||
if (existentMinimalStream.uploadDate != null && !hasBetterPrecision) {
|
||||
newerStream.uploadDate = existentMinimalStream.uploadDate
|
||||
newerStream.textualUploadDate = existentMinimalStream.textualUploadDate
|
||||
@@ -101,7 +103,8 @@ abstract class StreamDAO : BasicDAO<StreamEntity> {
|
||||
}
|
||||
}
|
||||
|
||||
@Query("""
|
||||
@Query(
|
||||
"""
|
||||
DELETE FROM streams WHERE
|
||||
|
||||
NOT EXISTS (SELECT 1 FROM stream_history sh
|
||||
@@ -112,7 +115,8 @@ abstract class StreamDAO : BasicDAO<StreamEntity> {
|
||||
|
||||
AND NOT EXISTS (SELECT 1 FROM feed f
|
||||
WHERE f.stream_id = streams.uid)
|
||||
""")
|
||||
"""
|
||||
)
|
||||
abstract fun deleteOrphans(): Int
|
||||
|
||||
/**
|
||||
@@ -129,7 +133,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,
|
||||
|
||||
@@ -11,7 +11,7 @@ import org.schabi.newpipe.database.stream.model.StreamStateEntity;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import io.reactivex.Flowable;
|
||||
import io.reactivex.rxjava3.core.Flowable;
|
||||
|
||||
import static org.schabi.newpipe.database.stream.model.StreamStateEntity.JOIN_STREAM_ID;
|
||||
import static org.schabi.newpipe.database.stream.model.StreamStateEntity.STREAM_STATE_TABLE;
|
||||
|
||||
@@ -5,9 +5,6 @@ import androidx.room.Entity
|
||||
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 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
|
||||
@@ -16,11 +13,14 @@ import org.schabi.newpipe.extractor.stream.StreamInfo
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem
|
||||
import org.schabi.newpipe.extractor.stream.StreamType
|
||||
import org.schabi.newpipe.player.playqueue.PlayQueueItem
|
||||
import java.io.Serializable
|
||||
import java.time.OffsetDateTime
|
||||
|
||||
@Entity(tableName = STREAM_TABLE,
|
||||
indices = [
|
||||
Index(value = [STREAM_SERVICE_ID, STREAM_URL], unique = true)
|
||||
]
|
||||
@Entity(
|
||||
tableName = STREAM_TABLE,
|
||||
indices = [
|
||||
Index(value = [STREAM_SERVICE_ID, STREAM_URL], unique = true)
|
||||
]
|
||||
)
|
||||
data class StreamEntity(
|
||||
@PrimaryKey(autoGenerate = true)
|
||||
@@ -55,35 +55,34 @@ 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,
|
||||
isUploadDateApproximation = item.uploadDate?.isApproximation
|
||||
serviceId = item.serviceId, url = item.url, title = item.name,
|
||||
streamType = item.streamType, duration = item.duration, uploader = item.uploaderName,
|
||||
thumbnailUrl = item.thumbnailUrl, viewCount = item.viewCount,
|
||||
textualUploadDate = item.textualUploadDate, uploadDate = item.uploadDate?.offsetDateTime(),
|
||||
isUploadDateApproximation = item.uploadDate?.isApproximation
|
||||
)
|
||||
|
||||
@Ignore
|
||||
constructor(info: StreamInfo) : this(
|
||||
serviceId = info.serviceId, url = info.url, title = info.name,
|
||||
streamType = info.streamType, duration = info.duration, uploader = info.uploaderName,
|
||||
thumbnailUrl = info.thumbnailUrl, viewCount = info.viewCount,
|
||||
textualUploadDate = info.textualUploadDate, uploadDate = info.uploadDate?.date()?.time,
|
||||
isUploadDateApproximation = info.uploadDate?.isApproximation
|
||||
serviceId = info.serviceId, url = info.url, title = info.name,
|
||||
streamType = info.streamType, duration = info.duration, uploader = info.uploaderName,
|
||||
thumbnailUrl = info.thumbnailUrl, viewCount = info.viewCount,
|
||||
textualUploadDate = info.textualUploadDate, uploadDate = info.uploadDate?.offsetDateTime(),
|
||||
isUploadDateApproximation = info.uploadDate?.isApproximation
|
||||
)
|
||||
|
||||
@Ignore
|
||||
constructor(item: PlayQueueItem) : this(
|
||||
serviceId = item.serviceId, url = item.url, title = item.title,
|
||||
streamType = item.streamType, duration = item.duration, uploader = item.uploader,
|
||||
thumbnailUrl = item.thumbnailUrl
|
||||
serviceId = item.serviceId, url = item.url, title = item.title,
|
||||
streamType = item.streamType, duration = item.duration, uploader = item.uploader,
|
||||
thumbnailUrl = item.thumbnailUrl
|
||||
)
|
||||
|
||||
fun toStreamInfoItem(): StreamInfoItem {
|
||||
@@ -95,8 +94,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
|
||||
|
||||
@@ -22,6 +22,9 @@ import static org.schabi.newpipe.database.stream.model.StreamStateEntity.STREAM_
|
||||
public class StreamStateEntity {
|
||||
public static final String STREAM_STATE_TABLE = "stream_state";
|
||||
public static final String JOIN_STREAM_ID = "stream_id";
|
||||
// This additional field is required for the SQL query because 'stream_id' is used
|
||||
// for some other joins already
|
||||
public static final String JOIN_STREAM_ID_ALIAS = "stream_id_alias";
|
||||
public static final String STREAM_PROGRESS_TIME = "progress_time";
|
||||
|
||||
/**
|
||||
|
||||
@@ -5,8 +5,8 @@ import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.Query
|
||||
import androidx.room.Transaction
|
||||
import io.reactivex.Flowable
|
||||
import io.reactivex.Maybe
|
||||
import io.reactivex.rxjava3.core.Flowable
|
||||
import io.reactivex.rxjava3.core.Maybe
|
||||
import org.schabi.newpipe.database.BasicDAO
|
||||
|
||||
@Dao
|
||||
@@ -20,16 +20,19 @@ abstract class SubscriptionDAO : BasicDAO<SubscriptionEntity> {
|
||||
@Query("SELECT * FROM subscriptions ORDER BY name COLLATE NOCASE ASC")
|
||||
abstract override fun getAll(): Flowable<List<SubscriptionEntity>>
|
||||
|
||||
@Query("""
|
||||
@Query(
|
||||
"""
|
||||
SELECT * FROM subscriptions
|
||||
|
||||
WHERE name LIKE '%' || :filter || '%'
|
||||
|
||||
ORDER BY name COLLATE NOCASE ASC
|
||||
""")
|
||||
"""
|
||||
)
|
||||
abstract fun getSubscriptionsFiltered(filter: String): Flowable<List<SubscriptionEntity>>
|
||||
|
||||
@Query("""
|
||||
@Query(
|
||||
"""
|
||||
SELECT * FROM subscriptions s
|
||||
|
||||
LEFT JOIN feed_group_subscription_join fgs
|
||||
@@ -38,12 +41,14 @@ abstract class SubscriptionDAO : BasicDAO<SubscriptionEntity> {
|
||||
WHERE (fgs.subscription_id IS NULL OR fgs.group_id = :currentGroupId)
|
||||
|
||||
ORDER BY name COLLATE NOCASE ASC
|
||||
""")
|
||||
"""
|
||||
)
|
||||
abstract fun getSubscriptionsOnlyUngrouped(
|
||||
currentGroupId: Long
|
||||
): Flowable<List<SubscriptionEntity>>
|
||||
|
||||
@Query("""
|
||||
@Query(
|
||||
"""
|
||||
SELECT * FROM subscriptions s
|
||||
|
||||
LEFT JOIN feed_group_subscription_join fgs
|
||||
@@ -53,7 +58,8 @@ abstract class SubscriptionDAO : BasicDAO<SubscriptionEntity> {
|
||||
AND s.name LIKE '%' || :filter || '%'
|
||||
|
||||
ORDER BY name COLLATE NOCASE ASC
|
||||
""")
|
||||
"""
|
||||
)
|
||||
abstract fun getSubscriptionsOnlyUngroupedFiltered(
|
||||
currentGroupId: Long,
|
||||
filter: String
|
||||
|
||||
@@ -10,7 +10,6 @@ import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.os.Environment;
|
||||
import android.os.IBinder;
|
||||
import androidx.preference.PreferenceManager;
|
||||
import android.util.Log;
|
||||
import android.util.SparseArray;
|
||||
import android.view.LayoutInflater;
|
||||
@@ -34,6 +33,7 @@ import androidx.appcompat.view.menu.ActionMenuItemView;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.documentfile.provider.DocumentFile;
|
||||
import androidx.fragment.app.DialogFragment;
|
||||
import androidx.preference.PreferenceManager;
|
||||
|
||||
import com.nononsenseapps.filepicker.Utils;
|
||||
|
||||
@@ -49,6 +49,7 @@ import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||
import org.schabi.newpipe.extractor.stream.SubtitlesStream;
|
||||
import org.schabi.newpipe.extractor.stream.VideoStream;
|
||||
import org.schabi.newpipe.report.ErrorActivity;
|
||||
import org.schabi.newpipe.report.ErrorInfo;
|
||||
import org.schabi.newpipe.report.UserAction;
|
||||
import org.schabi.newpipe.settings.NewPipeSettings;
|
||||
import org.schabi.newpipe.util.FilePickerActivityHelper;
|
||||
@@ -69,7 +70,7 @@ import java.util.Locale;
|
||||
|
||||
import icepick.Icepick;
|
||||
import icepick.State;
|
||||
import io.reactivex.disposables.CompositeDisposable;
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
||||
import us.shandian.giga.get.MissionRecoveryInfo;
|
||||
import us.shandian.giga.io.StoredDirectoryHelper;
|
||||
import us.shandian.giga.io.StoredFileHelper;
|
||||
@@ -602,7 +603,7 @@ public class DownloadDialog extends DialogFragment
|
||||
Collections.singletonList(e),
|
||||
null,
|
||||
null,
|
||||
ErrorActivity.ErrorInfo
|
||||
ErrorInfo
|
||||
.make(UserAction.SOMETHING_ELSE, "-", "-", R.string.general_error)
|
||||
);
|
||||
}
|
||||
@@ -646,7 +647,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");
|
||||
|
||||
@@ -13,7 +13,7 @@ import android.widget.Toast;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.StringRes;
|
||||
|
||||
import com.jakewharton.rxbinding2.view.RxView;
|
||||
import com.jakewharton.rxbinding4.view.RxView;
|
||||
|
||||
import org.schabi.newpipe.BaseFragment;
|
||||
import org.schabi.newpipe.MainActivity;
|
||||
@@ -23,6 +23,7 @@ import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException;
|
||||
import org.schabi.newpipe.extractor.exceptions.ContentNotSupportedException;
|
||||
import org.schabi.newpipe.extractor.exceptions.ReCaptchaException;
|
||||
import org.schabi.newpipe.report.ErrorActivity;
|
||||
import org.schabi.newpipe.report.ErrorInfo;
|
||||
import org.schabi.newpipe.report.UserAction;
|
||||
import org.schabi.newpipe.util.ExceptionUtils;
|
||||
import org.schabi.newpipe.util.InfoCache;
|
||||
@@ -33,7 +34,8 @@ import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
import icepick.State;
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.rxjava3.disposables.Disposable;
|
||||
|
||||
import static org.schabi.newpipe.util.AnimationUtils.animateView;
|
||||
|
||||
@@ -47,6 +49,8 @@ public abstract class BaseStateFragment<I> extends BaseFragment implements ViewC
|
||||
@Nullable
|
||||
private ProgressBar loadingProgressBar;
|
||||
|
||||
private Disposable errorDisposable;
|
||||
|
||||
protected View errorPanelRoot;
|
||||
private Button errorButtonRetry;
|
||||
private TextView errorTextView;
|
||||
@@ -63,6 +67,14 @@ public abstract class BaseStateFragment<I> extends BaseFragment implements ViewC
|
||||
wasLoading.set(isLoading.get());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
if (errorDisposable != null) {
|
||||
errorDisposable.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Init
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
@@ -82,7 +94,7 @@ public abstract class BaseStateFragment<I> extends BaseFragment implements ViewC
|
||||
@Override
|
||||
protected void initListeners() {
|
||||
super.initListeners();
|
||||
RxView.clicks(errorButtonRetry)
|
||||
errorDisposable = RxView.clicks(errorButtonRetry)
|
||||
.debounce(300, TimeUnit.MILLISECONDS)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(o -> onRetryButtonClicked());
|
||||
@@ -252,7 +264,7 @@ public abstract class BaseStateFragment<I> extends BaseFragment implements ViewC
|
||||
}
|
||||
|
||||
ErrorActivity.reportError(getContext(), exception, MainActivity.class, null,
|
||||
ErrorActivity.ErrorInfo.make(userAction, serviceName == null ? "none" : serviceName,
|
||||
ErrorInfo.make(userAction, serviceName == null ? "none" : serviceName,
|
||||
request == null ? "none" : request, errorId));
|
||||
}
|
||||
|
||||
@@ -265,7 +277,7 @@ public abstract class BaseStateFragment<I> extends BaseFragment implements ViewC
|
||||
|
||||
/**
|
||||
* Show a SnackBar and only call
|
||||
* {@link ErrorActivity#reportError(Context, List, Class, View, ErrorActivity.ErrorInfo)}
|
||||
* {@link ErrorActivity#reportError(Context, List, Class, View, ErrorInfo)}
|
||||
* IF we a find a valid view (otherwise the error screen appears).
|
||||
*
|
||||
* @param exception List of the exceptions to show
|
||||
@@ -291,6 +303,6 @@ public abstract class BaseStateFragment<I> extends BaseFragment implements ViewC
|
||||
}
|
||||
|
||||
ErrorActivity.reportError(getContext(), exception, MainActivity.class, rootView,
|
||||
ErrorActivity.ErrorInfo.make(userAction, serviceName, request, errorId));
|
||||
ErrorInfo.make(userAction, serviceName, request, errorId));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ import org.schabi.newpipe.BaseFragment;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||
import org.schabi.newpipe.report.ErrorActivity;
|
||||
import org.schabi.newpipe.report.ErrorInfo;
|
||||
import org.schabi.newpipe.report.UserAction;
|
||||
import org.schabi.newpipe.settings.tabs.Tab;
|
||||
import org.schabi.newpipe.settings.tabs.TabsManager;
|
||||
@@ -43,7 +44,7 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte
|
||||
private SelectedTabsPagerAdapter pagerAdapter;
|
||||
private ScrollableTabLayout tabLayout;
|
||||
|
||||
private List<Tab> tabsList = new ArrayList<>();
|
||||
private final List<Tab> tabsList = new ArrayList<>();
|
||||
private TabsManager tabsManager;
|
||||
|
||||
private boolean hasTabsChanged = false;
|
||||
@@ -242,7 +243,7 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte
|
||||
}
|
||||
|
||||
if (throwable != null) {
|
||||
ErrorActivity.reportError(context, throwable, null, null, ErrorActivity.ErrorInfo
|
||||
ErrorActivity.reportError(context, throwable, null, null, ErrorInfo
|
||||
.make(UserAction.UI_ERROR, "none", "", R.string.app_ui_crash));
|
||||
return new BlankFragment();
|
||||
}
|
||||
|
||||
@@ -14,12 +14,10 @@ public abstract class OnScrollBelowItemsListener extends RecyclerView.OnScrollLi
|
||||
super.onScrolled(recyclerView, dx, dy);
|
||||
if (dy > 0) {
|
||||
int pastVisibleItems = 0;
|
||||
final int visibleItemCount;
|
||||
final int totalItemCount;
|
||||
final RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager();
|
||||
|
||||
visibleItemCount = layoutManager.getChildCount();
|
||||
totalItemCount = layoutManager.getItemCount();
|
||||
final int visibleItemCount = layoutManager.getChildCount();
|
||||
final int totalItemCount = layoutManager.getItemCount();
|
||||
|
||||
// Already covers the GridLayoutManager case
|
||||
if (layoutManager instanceof LinearLayoutManager) {
|
||||
|
||||
@@ -15,11 +15,7 @@ import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.view.ViewTreeObserver;
|
||||
import androidx.core.text.HtmlCompat;
|
||||
import androidx.preference.PreferenceManager;
|
||||
import android.provider.Settings;
|
||||
import android.text.Spanned;
|
||||
import android.text.TextUtils;
|
||||
import android.text.util.Linkify;
|
||||
import android.util.DisplayMetrics;
|
||||
@@ -28,6 +24,7 @@ import android.view.LayoutInflater;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.ViewTreeObserver;
|
||||
import android.view.WindowManager;
|
||||
import android.view.animation.DecelerateInterpolator;
|
||||
import android.widget.FrameLayout;
|
||||
@@ -46,7 +43,9 @@ import androidx.appcompat.content.res.AppCompatResources;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.coordinatorlayout.widget.CoordinatorLayout;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.core.text.HtmlCompat;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.preference.PreferenceManager;
|
||||
import androidx.viewpager.widget.ViewPager;
|
||||
|
||||
import com.google.android.exoplayer2.ExoPlaybackException;
|
||||
@@ -80,6 +79,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;
|
||||
@@ -92,6 +92,7 @@ import org.schabi.newpipe.player.playqueue.PlayQueue;
|
||||
import org.schabi.newpipe.player.playqueue.PlayQueueItem;
|
||||
import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
|
||||
import org.schabi.newpipe.report.ErrorActivity;
|
||||
import org.schabi.newpipe.report.ErrorInfo;
|
||||
import org.schabi.newpipe.report.UserAction;
|
||||
import org.schabi.newpipe.util.Constants;
|
||||
import org.schabi.newpipe.util.DeviceUtils;
|
||||
@@ -109,16 +110,17 @@ 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;
|
||||
import io.noties.markwon.Markwon;
|
||||
import io.noties.markwon.linkify.LinkifyPlugin;
|
||||
import io.reactivex.Single;
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.disposables.CompositeDisposable;
|
||||
import io.reactivex.disposables.Disposable;
|
||||
import io.reactivex.schedulers.Schedulers;
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.rxjava3.core.Single;
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
||||
import io.reactivex.rxjava3.disposables.Disposable;
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers;
|
||||
|
||||
import static org.schabi.newpipe.extractor.StreamingService.ServiceInfo.MediaCapability.COMMENTS;
|
||||
import static org.schabi.newpipe.extractor.stream.StreamExtractor.NO_AGE_LIMIT;
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -714,7 +731,7 @@ public class VideoDetailFragment
|
||||
}
|
||||
|
||||
private View.OnTouchListener getOnControlsTouchListener() {
|
||||
return (View view, MotionEvent motionEvent) -> {
|
||||
return (view, motionEvent) -> {
|
||||
if (!PreferenceManager.getDefaultSharedPreferences(activity)
|
||||
.getBoolean(getString(R.string.show_hold_to_append_key), true)) {
|
||||
return false;
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -935,7 +948,7 @@ public class VideoDetailFragment
|
||||
currentWorker = ExtractorHelper.getStreamInfo(serviceId, url, forceLoad)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe((@NonNull final StreamInfo result) -> {
|
||||
.subscribe(result -> {
|
||||
isLoading.set(false);
|
||||
hideMainPlayer();
|
||||
if (result.getAgeLimit() != NO_AGE_LIMIT && !prefs.getBoolean(
|
||||
@@ -949,14 +962,14 @@ 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()) {
|
||||
openVideoPlayer();
|
||||
}
|
||||
}
|
||||
}, (@NonNull final Throwable throwable) -> {
|
||||
}, throwable -> {
|
||||
isLoading.set(false);
|
||||
onError(throwable);
|
||||
});
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -1125,7 +1140,7 @@ public class VideoDetailFragment
|
||||
|
||||
PlayQueue queue = playQueue;
|
||||
// Size can be 0 because queue removes bad stream automatically when error occurs
|
||||
if (queue == null || queue.size() == 0) {
|
||||
if (queue == null || queue.isEmpty()) {
|
||||
queue = new SinglePlayQueue(currentInfo);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -1209,12 +1224,12 @@ public class VideoDetailFragment
|
||||
|
||||
if (description.getType() == Description.HTML) {
|
||||
disposables.add(Single.just(description.getContent())
|
||||
.map((@NonNull final String descriptionText) ->
|
||||
.map(descriptionText ->
|
||||
HtmlCompat.fromHtml(descriptionText,
|
||||
HtmlCompat.FROM_HTML_MODE_LEGACY))
|
||||
.subscribeOn(Schedulers.computation())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe((@NonNull final Spanned spanned) -> {
|
||||
.subscribe(spanned -> {
|
||||
videoDescriptionView.setText(spanned);
|
||||
videoDescriptionView.setVisibility(View.VISIBLE);
|
||||
}));
|
||||
@@ -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) {
|
||||
@@ -1329,19 +1346,24 @@ public class VideoDetailFragment
|
||||
broadcastReceiver = new BroadcastReceiver() {
|
||||
@Override
|
||||
public void onReceive(final Context context, final Intent intent) {
|
||||
if (intent.getAction().equals(ACTION_SHOW_MAIN_PLAYER)) {
|
||||
bottomSheetBehavior.setState(BottomSheetBehavior.STATE_EXPANDED);
|
||||
} else if (intent.getAction().equals(ACTION_HIDE_MAIN_PLAYER)) {
|
||||
bottomSheetBehavior.setState(BottomSheetBehavior.STATE_HIDDEN);
|
||||
} else if (intent.getAction().equals(ACTION_PLAYER_STARTED)) {
|
||||
// If the state is not hidden we don't need to show the mini player
|
||||
if (bottomSheetBehavior.getState() == BottomSheetBehavior.STATE_HIDDEN) {
|
||||
bottomSheetBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
|
||||
}
|
||||
// Rebound to the service if it was closed via notification or mini player
|
||||
if (!PlayerHolder.bound) {
|
||||
PlayerHolder.startService(App.getApp(), false, VideoDetailFragment.this);
|
||||
}
|
||||
switch (intent.getAction()) {
|
||||
case ACTION_SHOW_MAIN_PLAYER:
|
||||
bottomSheetBehavior.setState(BottomSheetBehavior.STATE_EXPANDED);
|
||||
break;
|
||||
case ACTION_HIDE_MAIN_PLAYER:
|
||||
bottomSheetBehavior.setState(BottomSheetBehavior.STATE_HIDDEN);
|
||||
break;
|
||||
case ACTION_PLAYER_STARTED:
|
||||
// If the state is not hidden we don't need to show the mini player
|
||||
if (bottomSheetBehavior.getState() == BottomSheetBehavior.STATE_HIDDEN) {
|
||||
bottomSheetBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
|
||||
}
|
||||
// Rebound to the service if it was closed via notification or mini player
|
||||
if (!PlayerHolder.bound) {
|
||||
PlayerHolder.startService(
|
||||
App.getApp(), false, VideoDetailFragment.this);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -1393,7 +1415,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 +1460,7 @@ public class VideoDetailFragment
|
||||
}
|
||||
}
|
||||
animateView(thumbnailPlayButton, true, 200);
|
||||
videoTitleTextView.setText(name);
|
||||
videoTitleTextView.setText(title);
|
||||
|
||||
if (!TextUtils.isEmpty(info.getSubChannelName())) {
|
||||
displayBothUploaderAndSubChannel(info);
|
||||
@@ -1520,7 +1542,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 +1585,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);
|
||||
@@ -1608,7 +1631,7 @@ public class VideoDetailFragment
|
||||
|
||||
downloadDialog.show(activity.getSupportFragmentManager(), "downloadDialog");
|
||||
} catch (final Exception e) {
|
||||
final ErrorActivity.ErrorInfo info = ErrorActivity.ErrorInfo.make(UserAction.UI_ERROR,
|
||||
final ErrorInfo info = ErrorInfo.make(UserAction.UI_ERROR,
|
||||
ServiceList.all()
|
||||
.get(currentInfo
|
||||
.getServiceId())
|
||||
@@ -1633,8 +1656,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 +1750,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 +1838,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 +2052,7 @@ public class VideoDetailFragment
|
||||
private void checkLandscape() {
|
||||
if ((!player.isPlaying() && player.getPlayQueue() != playQueue)
|
||||
|| player.getPlayQueue() == null) {
|
||||
setAutoplay(true);
|
||||
setAutoPlay(true);
|
||||
}
|
||||
|
||||
player.checkLandscape();
|
||||
@@ -2054,6 +2079,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 +2100,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 +2302,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)) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -45,7 +48,7 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I>
|
||||
implements ListViewContract<I, N>, StateSaver.WriteRead,
|
||||
SharedPreferences.OnSharedPreferenceChangeListener {
|
||||
private static final int LIST_MODE_UPDATE_FLAG = 0x32;
|
||||
protected StateSaver.SavedState savedState;
|
||||
protected org.schabi.newpipe.util.SavedState savedState;
|
||||
|
||||
private boolean useDefaultStateSaving = true;
|
||||
private int updateFlags = 0;
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -16,10 +16,10 @@ import org.schabi.newpipe.views.NewPipeRecyclerView;
|
||||
import java.util.Queue;
|
||||
|
||||
import icepick.State;
|
||||
import io.reactivex.Single;
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.disposables.Disposable;
|
||||
import io.reactivex.schedulers.Schedulers;
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.rxjava3.core.Single;
|
||||
import io.reactivex.rxjava3.disposables.Disposable;
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers;
|
||||
|
||||
public abstract class BaseListInfoFragment<I extends ListInfo>
|
||||
extends BaseListFragment<I, ListExtractor.InfoItemsPage> {
|
||||
@@ -204,7 +204,7 @@ public abstract class BaseListInfoFragment<I extends ListInfo>
|
||||
name = result.getName();
|
||||
setTitle(name);
|
||||
|
||||
if (infoListAdapter.getItemsList().size() == 0) {
|
||||
if (infoListAdapter.getItemsList().isEmpty()) {
|
||||
if (result.getRelatedItems().size() > 0) {
|
||||
infoListAdapter.addInfoItemList(result.getRelatedItems());
|
||||
showListFooter(hasMoreItems());
|
||||
|
||||
@@ -24,7 +24,7 @@ import androidx.appcompat.app.ActionBar;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.core.content.ContextCompat;
|
||||
|
||||
import com.jakewharton.rxbinding2.view.RxView;
|
||||
import com.jakewharton.rxbinding4.view.RxView;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.database.subscription.SubscriptionEntity;
|
||||
@@ -50,19 +50,18 @@ 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;
|
||||
|
||||
import io.reactivex.Observable;
|
||||
import io.reactivex.Single;
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.disposables.CompositeDisposable;
|
||||
import io.reactivex.disposables.Disposable;
|
||||
import io.reactivex.functions.Action;
|
||||
import io.reactivex.functions.Consumer;
|
||||
import io.reactivex.functions.Function;
|
||||
import io.reactivex.schedulers.Schedulers;
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.rxjava3.core.Observable;
|
||||
import io.reactivex.rxjava3.core.Single;
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
||||
import io.reactivex.rxjava3.disposables.Disposable;
|
||||
import io.reactivex.rxjava3.functions.Action;
|
||||
import io.reactivex.rxjava3.functions.Consumer;
|
||||
import io.reactivex.rxjava3.functions.Function;
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers;
|
||||
|
||||
import static org.schabi.newpipe.util.AnimationUtils.animateBackgroundColor;
|
||||
import static org.schabi.newpipe.util.AnimationUtils.animateTextColor;
|
||||
@@ -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
|
||||
|
||||
@@ -20,11 +20,11 @@ import org.schabi.newpipe.report.UserAction;
|
||||
import org.schabi.newpipe.util.AnimationUtils;
|
||||
import org.schabi.newpipe.util.ExtractorHelper;
|
||||
|
||||
import io.reactivex.Single;
|
||||
import io.reactivex.disposables.CompositeDisposable;
|
||||
import io.reactivex.rxjava3.core.Single;
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
||||
|
||||
public class CommentsFragment extends BaseListInfoFragment<CommentsInfo> {
|
||||
private CompositeDisposable disposables = new CompositeDisposable();
|
||||
private final CompositeDisposable disposables = new CompositeDisposable();
|
||||
|
||||
public static CommentsFragment getInstance(final int serviceId, final String url,
|
||||
final String name) {
|
||||
|
||||
@@ -26,7 +26,7 @@ import org.schabi.newpipe.util.KioskTranslator;
|
||||
import org.schabi.newpipe.util.Localization;
|
||||
|
||||
import icepick.State;
|
||||
import io.reactivex.Single;
|
||||
import io.reactivex.rxjava3.core.Single;
|
||||
|
||||
import static org.schabi.newpipe.util.AnimationUtils.animateView;
|
||||
|
||||
|
||||
@@ -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,15 +47,15 @@ import org.schabi.newpipe.util.StreamDialogEntry;
|
||||
import org.schabi.newpipe.util.ThemeHelper;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
import io.reactivex.Flowable;
|
||||
import io.reactivex.Single;
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.disposables.CompositeDisposable;
|
||||
import io.reactivex.disposables.Disposable;
|
||||
import io.reactivex.disposables.Disposables;
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.rxjava3.core.Flowable;
|
||||
import io.reactivex.rxjava3.core.Single;
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
||||
import io.reactivex.rxjava3.disposables.Disposable;
|
||||
|
||||
import static org.schabi.newpipe.util.AnimationUtils.animateView;
|
||||
|
||||
@@ -151,25 +152,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 +318,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 ->
|
||||
@@ -457,7 +459,7 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
|
||||
.doFinally(() -> playlistEntity = null)
|
||||
.subscribe(ignored -> { /* Do nothing */ }, this::onError);
|
||||
} else {
|
||||
action = Disposables.empty();
|
||||
action = Disposable.empty();
|
||||
}
|
||||
|
||||
disposables.add(action);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -48,10 +49,11 @@ import org.schabi.newpipe.fragments.BackPressable;
|
||||
import org.schabi.newpipe.fragments.list.BaseListFragment;
|
||||
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
||||
import org.schabi.newpipe.report.ErrorActivity;
|
||||
import org.schabi.newpipe.report.ErrorInfo;
|
||||
import org.schabi.newpipe.report.UserAction;
|
||||
import org.schabi.newpipe.util.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,20 +62,19 @@ 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;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import icepick.State;
|
||||
import io.reactivex.Flowable;
|
||||
import io.reactivex.Observable;
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.disposables.CompositeDisposable;
|
||||
import io.reactivex.disposables.Disposable;
|
||||
import io.reactivex.schedulers.Schedulers;
|
||||
import io.reactivex.subjects.PublishSubject;
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.rxjava3.core.Flowable;
|
||||
import io.reactivex.rxjava3.core.Observable;
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
||||
import io.reactivex.rxjava3.disposables.Disposable;
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers;
|
||||
import io.reactivex.rxjava3.subjects.PublishSubject;
|
||||
|
||||
import static androidx.recyclerview.widget.ItemTouchHelper.Callback.makeMovementFlags;
|
||||
import static java.util.Arrays.asList;
|
||||
@@ -248,7 +249,7 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
||||
} catch (final Exception e) {
|
||||
ErrorActivity.reportError(getActivity(), e, requireActivity().getClass(),
|
||||
requireActivity().findViewById(android.R.id.content),
|
||||
ErrorActivity.ErrorInfo.make(UserAction.UI_ERROR,
|
||||
ErrorInfo.make(UserAction.UI_ERROR,
|
||||
"",
|
||||
"", R.string.general_error));
|
||||
}
|
||||
@@ -256,7 +257,7 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
||||
if (!TextUtils.isEmpty(searchString)) {
|
||||
if (wasLoading.getAndSet(false)) {
|
||||
search(searchString, contentFilter, sortFilter);
|
||||
} else if (infoListAdapter.getItemsList().size() == 0) {
|
||||
} else if (infoListAdapter.getItemsList().isEmpty()) {
|
||||
if (savedState == null) {
|
||||
search(searchString, contentFilter, sortFilter);
|
||||
} else if (!isLoading.get() && !wasSearchFocused) {
|
||||
@@ -639,8 +640,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 +654,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);
|
||||
|
||||
@@ -708,7 +709,7 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
||||
|
||||
final Observable<String> observable = suggestionPublisher
|
||||
.debounce(SUGGESTIONS_DEBOUNCE, TimeUnit.MILLISECONDS)
|
||||
.startWith(searchString != null
|
||||
.startWithItem(searchString != null
|
||||
? searchString
|
||||
: "")
|
||||
.filter(ss -> isSuggestionsEnabled);
|
||||
@@ -757,16 +758,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);
|
||||
@@ -984,7 +978,7 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
||||
lastSearchedString = searchString;
|
||||
nextPage = result.getNextPage();
|
||||
|
||||
if (infoListAdapter.getItemsList().size() == 0) {
|
||||
if (infoListAdapter.getItemsList().isEmpty()) {
|
||||
if (!result.getRelatedItems().isEmpty()) {
|
||||
infoListAdapter.addInfoItemList(result.getRelatedItems());
|
||||
} else {
|
||||
|
||||
@@ -3,7 +3,6 @@ package org.schabi.newpipe.fragments.list.videos;
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.Bundle;
|
||||
import androidx.preference.PreferenceManager;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
@@ -13,6 +12,7 @@ import android.widget.Switch;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.preference.PreferenceManager;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.extractor.ListExtractor;
|
||||
@@ -25,13 +25,13 @@ import org.schabi.newpipe.util.RelatedStreamInfo;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
import io.reactivex.Single;
|
||||
import io.reactivex.disposables.CompositeDisposable;
|
||||
import io.reactivex.rxjava3.core.Single;
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
||||
|
||||
public class RelatedVideosFragment extends BaseListInfoFragment<RelatedStreamInfo>
|
||||
implements SharedPreferences.OnSharedPreferenceChangeListener {
|
||||
private static final String INFO_KEY = "related_info_key";
|
||||
private CompositeDisposable disposables = new CompositeDisposable();
|
||||
private final CompositeDisposable disposables = new CompositeDisposable();
|
||||
private RelatedStreamInfo relatedStreamInfo;
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@@ -371,7 +371,7 @@ public class InfoListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolde
|
||||
};
|
||||
}
|
||||
|
||||
public class HFHolder extends RecyclerView.ViewHolder {
|
||||
public static class HFHolder extends RecyclerView.ViewHolder {
|
||||
public View view;
|
||||
|
||||
HFHolder(final View v) {
|
||||
|
||||
@@ -144,7 +144,8 @@ public class CommentsMiniInfoItemHolder extends InfoItemHolder {
|
||||
}
|
||||
|
||||
if (item.getUploadDate() != null) {
|
||||
itemPublishedTime.setText(Localization.relativeTime(item.getUploadDate().date()));
|
||||
itemPublishedTime.setText(Localization.relativeTime(item.getUploadDate()
|
||||
.offsetDateTime()));
|
||||
} else {
|
||||
itemPublishedTime.setText(item.getTextualUploadDate());
|
||||
}
|
||||
|
||||
@@ -95,7 +95,7 @@ public class StreamInfoItemHolder extends StreamMiniInfoItemHolder {
|
||||
private String getFormattedRelativeUploadDate(final StreamInfoItem infoItem) {
|
||||
if (infoItem.getUploadDate() != null) {
|
||||
String formattedRelativeTime = Localization
|
||||
.relativeTime(infoItem.getUploadDate().date());
|
||||
.relativeTime(infoItem.getUploadDate().offsetDateTime());
|
||||
|
||||
if (DEBUG && PreferenceManager.getDefaultSharedPreferences(itemBuilder.getContext())
|
||||
.getBoolean(itemBuilder.getContext()
|
||||
|
||||
29
app/src/main/java/org/schabi/newpipe/ktx/OffsetDateTime.kt
Normal file
29
app/src/main/java/org/schabi/newpipe/ktx/OffsetDateTime.kt
Normal file
@@ -0,0 +1,29 @@
|
||||
package org.schabi.newpipe.ktx
|
||||
|
||||
import java.time.OffsetDateTime
|
||||
import java.time.ZoneOffset
|
||||
import java.time.temporal.ChronoField
|
||||
import java.util.Calendar
|
||||
import java.util.Date
|
||||
import java.util.GregorianCalendar
|
||||
import java.util.TimeZone
|
||||
|
||||
// This method is a modified version of GregorianCalendar.from(ZonedDateTime).
|
||||
// Math.addExact() and Math.multiplyExact() are desugared even though lint displays a warning.
|
||||
@SuppressWarnings("NewApi")
|
||||
fun OffsetDateTime.toCalendar(): Calendar {
|
||||
val cal = GregorianCalendar(TimeZone.getTimeZone("UTC"))
|
||||
val offsetDateTimeUTC = withOffsetSameInstant(ZoneOffset.UTC)
|
||||
cal.gregorianChange = Date(Long.MIN_VALUE)
|
||||
cal.firstDayOfWeek = Calendar.MONDAY
|
||||
cal.minimalDaysInFirstWeek = 4
|
||||
try {
|
||||
cal.timeInMillis = Math.addExact(
|
||||
Math.multiplyExact(offsetDateTimeUTC.toEpochSecond(), 1000),
|
||||
offsetDateTimeUTC[ChronoField.MILLI_OF_SECOND].toLong()
|
||||
)
|
||||
} catch (ex: ArithmeticException) {
|
||||
throw IllegalArgumentException(ex)
|
||||
}
|
||||
return cal
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
@@ -33,11 +33,11 @@ import org.schabi.newpipe.util.OnClickGesture;
|
||||
import java.util.List;
|
||||
|
||||
import icepick.State;
|
||||
import io.reactivex.Flowable;
|
||||
import io.reactivex.Single;
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.disposables.CompositeDisposable;
|
||||
import io.reactivex.disposables.Disposable;
|
||||
import io.reactivex.rxjava3.core.Flowable;
|
||||
import io.reactivex.rxjava3.core.Single;
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
||||
import io.reactivex.rxjava3.disposables.Disposable;
|
||||
|
||||
public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistLocalItem>, Void> {
|
||||
@State
|
||||
|
||||
@@ -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;
|
||||
@@ -27,8 +28,9 @@ import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.disposables.CompositeDisposable;
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
||||
import io.reactivex.rxjava3.disposables.Disposable;
|
||||
|
||||
public final class PlaylistAppendDialog extends PlaylistDialog {
|
||||
private static final String TAG = PlaylistAppendDialog.class.getCanonicalName();
|
||||
@@ -36,7 +38,24 @@ public final class PlaylistAppendDialog extends PlaylistDialog {
|
||||
private RecyclerView playlistRecyclerView;
|
||||
private LocalItemListAdapter playlistAdapter;
|
||||
|
||||
private CompositeDisposable playlistDisposables = new CompositeDisposable();
|
||||
private final CompositeDisposable playlistDisposables = new CompositeDisposable();
|
||||
|
||||
public static Disposable onPlaylistFound(
|
||||
final Context context, final Runnable onSuccess, final Runnable onFailed
|
||||
) {
|
||||
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();
|
||||
@@ -79,7 +98,7 @@ public final class PlaylistAppendDialog extends PlaylistDialog {
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
|
||||
final LocalPlaylistManager playlistManager =
|
||||
new LocalPlaylistManager(NewPipeDatabase.getInstance(getContext()));
|
||||
new LocalPlaylistManager(NewPipeDatabase.getInstance(requireContext()));
|
||||
|
||||
playlistAdapter = new LocalItemListAdapter(getActivity());
|
||||
playlistAdapter.setSelectedListener(new OnClickGesture<LocalItem>() {
|
||||
@@ -94,7 +113,7 @@ public final class PlaylistAppendDialog extends PlaylistDialog {
|
||||
});
|
||||
|
||||
playlistRecyclerView = view.findViewById(R.id.playlist_list);
|
||||
playlistRecyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
|
||||
playlistRecyclerView.setLayoutManager(new LinearLayoutManager(requireContext()));
|
||||
playlistRecyclerView.setAdapter(playlistAdapter);
|
||||
|
||||
final View newPlaylistButton = view.findViewById(R.id.newPlaylist);
|
||||
@@ -127,20 +146,15 @@ public final class PlaylistAppendDialog extends PlaylistDialog {
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
public void openCreatePlaylistDialog() {
|
||||
if (getStreams() == null || getFragmentManager() == null) {
|
||||
if (getStreams() == null || !isAdded()) {
|
||||
return;
|
||||
}
|
||||
|
||||
PlaylistCreationDialog.newInstance(getStreams()).show(getFragmentManager(), TAG);
|
||||
getDialog().dismiss();
|
||||
PlaylistCreationDialog.newInstance(getStreams()).show(getParentFragmentManager(), TAG);
|
||||
requireDialog().dismiss();
|
||||
}
|
||||
|
||||
private void onPlaylistsReceived(@NonNull final List<PlaylistMetadataEntry> playlists) {
|
||||
if (playlists.isEmpty()) {
|
||||
openCreatePlaylistDialog();
|
||||
return;
|
||||
}
|
||||
|
||||
if (playlistAdapter != null && playlistRecyclerView != null) {
|
||||
playlistAdapter.clearStreamItemList();
|
||||
playlistAdapter.addItems(playlists);
|
||||
@@ -169,6 +183,6 @@ public final class PlaylistAppendDialog extends PlaylistDialog {
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(ignored -> successToast.show()));
|
||||
|
||||
getDialog().dismiss();
|
||||
requireDialog().dismiss();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ import org.schabi.newpipe.local.playlist.LocalPlaylistManager;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||
|
||||
public final class PlaylistCreationDialog extends PlaylistDialog {
|
||||
public static PlaylistCreationDialog newInstance(final List<StreamEntity> streams) {
|
||||
@@ -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
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@@ -17,7 +17,7 @@ import java.util.Queue;
|
||||
public abstract class PlaylistDialog extends DialogFragment implements StateSaver.WriteRead {
|
||||
private List<StreamEntity> streamEntities;
|
||||
|
||||
private StateSaver.SavedState savedState;
|
||||
private org.schabi.newpipe.util.SavedState savedState;
|
||||
|
||||
protected void setInfo(final List<StreamEntity> entities) {
|
||||
this.streamEntities = entities;
|
||||
|
||||
@@ -2,13 +2,11 @@ package org.schabi.newpipe.local.feed
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import io.reactivex.Completable
|
||||
import io.reactivex.Flowable
|
||||
import io.reactivex.Maybe
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.schedulers.Schedulers
|
||||
import java.util.Calendar
|
||||
import java.util.Date
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.core.Completable
|
||||
import io.reactivex.rxjava3.core.Flowable
|
||||
import io.reactivex.rxjava3.core.Maybe
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import org.schabi.newpipe.MainActivity.DEBUG
|
||||
import org.schabi.newpipe.NewPipeDatabase
|
||||
import org.schabi.newpipe.database.feed.model.FeedEntity
|
||||
@@ -18,6 +16,9 @@ import org.schabi.newpipe.database.stream.model.StreamEntity
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem
|
||||
import org.schabi.newpipe.extractor.stream.StreamType
|
||||
import org.schabi.newpipe.local.subscription.FeedGroupIcon
|
||||
import java.time.LocalDate
|
||||
import java.time.OffsetDateTime
|
||||
import java.time.ZoneOffset
|
||||
|
||||
class FeedDatabaseManager(context: Context) {
|
||||
private val database = NewPipeDatabase.getInstance(context)
|
||||
@@ -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()
|
||||
@@ -48,14 +44,14 @@ class FeedDatabaseManager(context: Context) {
|
||||
else -> feedTable.getAllStreamsFromGroup(groupId)
|
||||
}
|
||||
|
||||
return streams.map<List<StreamInfoItem>> {
|
||||
return streams.map {
|
||||
val items = ArrayList<StreamInfoItem>(it.size)
|
||||
for (streamEntity in it) items.add(streamEntity.toStreamInfoItem())
|
||||
it.mapTo(items) { stream -> stream.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,16 +60,16 @@ class FeedDatabaseManager(context: Context) {
|
||||
}
|
||||
}
|
||||
|
||||
fun outdatedSubscriptionsForGroup(groupId: Long = FeedGroupEntity.GROUP_ALL_ID, outdatedThreshold: Date) =
|
||||
feedTable.getAllOutdatedForGroup(groupId, outdatedThreshold)
|
||||
fun outdatedSubscriptionsForGroup(groupId: Long = FeedGroupEntity.GROUP_ALL_ID, outdatedThreshold: OffsetDateTime) =
|
||||
feedTable.getAllOutdatedForGroup(groupId, outdatedThreshold)
|
||||
|
||||
fun markAsOutdated(subscriptionId: Long) = feedTable
|
||||
.setLastUpdatedForSubscription(FeedLastUpdatedEntity(subscriptionId, null))
|
||||
.setLastUpdatedForSubscription(FeedLastUpdatedEntity(subscriptionId, null))
|
||||
|
||||
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,15 @@ 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()
|
||||
}
|
||||
@@ -116,38 +117,38 @@ class FeedDatabaseManager(context: Context) {
|
||||
|
||||
fun subscriptionIdsForGroup(groupId: Long): Flowable<List<Long>> {
|
||||
return feedGroupTable.getSubscriptionIdsFor(groupId)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
}
|
||||
|
||||
fun updateSubscriptionsForGroup(groupId: Long, subscriptionIds: List<Long>): Completable {
|
||||
return Completable.fromCallable { feedGroupTable.updateSubscriptionsForGroup(groupId, subscriptionIds) }
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
}
|
||||
|
||||
fun createGroup(name: String, icon: FeedGroupIcon): Maybe<Long> {
|
||||
return Maybe.fromCallable { feedGroupTable.insert(FeedGroupEntity(0, name, icon)) }
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
}
|
||||
|
||||
fun getGroup(groupId: Long): Maybe<FeedGroupEntity> {
|
||||
return feedGroupTable.getGroup(groupId)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
}
|
||||
|
||||
fun updateGroup(feedGroupEntity: FeedGroupEntity): Completable {
|
||||
return Completable.fromCallable { feedGroupTable.update(feedGroupEntity) }
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
}
|
||||
|
||||
fun deleteGroup(groupId: Long): Completable {
|
||||
return Completable.fromCallable { feedGroupTable.delete(groupId) }
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
}
|
||||
|
||||
fun updateGroupsOrder(groupIdList: List<Long>): Completable {
|
||||
@@ -155,11 +156,11 @@ class FeedDatabaseManager(context: Context) {
|
||||
val orderMap = groupIdList.associateBy({ it }, { index++ })
|
||||
|
||||
return Completable.fromCallable { feedGroupTable.updateOrder(orderMap) }
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
}
|
||||
|
||||
fun oldestSubscriptionUpdate(groupId: Long): Flowable<List<Date>> {
|
||||
fun oldestSubscriptionUpdate(groupId: Long): Flowable<List<OffsetDateTime>> {
|
||||
return when (groupId) {
|
||||
FeedGroupEntity.GROUP_ALL_ID -> feedTable.oldestSubscriptionUpdateFromAll()
|
||||
else -> feedTable.oldestSubscriptionUpdate(groupId)
|
||||
|
||||
@@ -29,11 +29,14 @@ import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.core.content.edit
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.preference.PreferenceManager
|
||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||
import icepick.State
|
||||
import java.util.Calendar
|
||||
import kotlinx.android.synthetic.main.error_retry.error_button_retry
|
||||
import kotlinx.android.synthetic.main.error_retry.error_message_view
|
||||
import kotlinx.android.synthetic.main.fragment_feed.empty_state_view
|
||||
@@ -51,9 +54,11 @@ import org.schabi.newpipe.local.feed.service.FeedLoadService
|
||||
import org.schabi.newpipe.report.UserAction
|
||||
import org.schabi.newpipe.util.AnimationUtils.animateView
|
||||
import org.schabi.newpipe.util.Localization
|
||||
import java.util.Calendar
|
||||
|
||||
class FeedFragment : BaseListFragment<FeedState, Unit>() {
|
||||
private lateinit var viewModel: FeedViewModel
|
||||
private lateinit var swipeRefreshLayout: SwipeRefreshLayout
|
||||
@State
|
||||
@JvmField
|
||||
var listState: Parcelable? = null
|
||||
@@ -71,7 +76,7 @@ class FeedFragment : BaseListFragment<FeedState, Unit>() {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
groupId = arguments?.getLong(KEY_GROUP_ID, FeedGroupEntity.GROUP_ALL_ID)
|
||||
?: FeedGroupEntity.GROUP_ALL_ID
|
||||
?: FeedGroupEntity.GROUP_ALL_ID
|
||||
groupName = arguments?.getString(KEY_GROUP_NAME) ?: ""
|
||||
}
|
||||
|
||||
@@ -81,7 +86,8 @@ class FeedFragment : BaseListFragment<FeedState, Unit>() {
|
||||
|
||||
override fun onViewCreated(rootView: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(rootView, savedInstanceState)
|
||||
|
||||
swipeRefreshLayout = requireView().findViewById(R.id.swiperefresh)
|
||||
swipeRefreshLayout.setOnRefreshListener { reloadContent() }
|
||||
viewModel = ViewModelProvider(this, FeedViewModel.Factory(requireContext(), groupId)).get(FeedViewModel::class.java)
|
||||
viewModel.stateLiveData.observe(viewLifecycleOwner, Observer { it?.let(::handleResult) })
|
||||
}
|
||||
@@ -138,15 +144,15 @@ class FeedFragment : BaseListFragment<FeedState, Unit>() {
|
||||
}
|
||||
|
||||
AlertDialog.Builder(requireContext())
|
||||
.setMessage(R.string.feed_use_dedicated_fetch_method_help_text)
|
||||
.setNeutralButton(enableDisableButtonText) { _, _ ->
|
||||
sharedPreferences.edit()
|
||||
.putBoolean(getString(R.string.feed_use_dedicated_fetch_method_key), !usingDedicatedMethod)
|
||||
.apply()
|
||||
.setMessage(R.string.feed_use_dedicated_fetch_method_help_text)
|
||||
.setNeutralButton(enableDisableButtonText) { _, _ ->
|
||||
sharedPreferences.edit {
|
||||
putBoolean(getString(R.string.feed_use_dedicated_fetch_method_key), !usingDedicatedMethod)
|
||||
}
|
||||
.setPositiveButton(resources.getString(R.string.finish), null)
|
||||
.create()
|
||||
.show()
|
||||
}
|
||||
.setPositiveButton(resources.getString(R.string.finish), null)
|
||||
.create()
|
||||
.show()
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -187,6 +193,7 @@ class FeedFragment : BaseListFragment<FeedState, Unit>() {
|
||||
|
||||
empty_state_view?.let { animateView(it, false, 0) }
|
||||
animateView(error_panel, false, 0)
|
||||
swipeRefreshLayout.isRefreshing = false
|
||||
}
|
||||
|
||||
override fun showEmptyState() {
|
||||
@@ -227,7 +234,7 @@ class FeedFragment : BaseListFragment<FeedState, Unit>() {
|
||||
showLoading()
|
||||
|
||||
val isIndeterminate = progressState.currentProgress == -1 &&
|
||||
progressState.maxProgress == -1
|
||||
progressState.maxProgress == -1
|
||||
|
||||
if (!isIndeterminate) {
|
||||
loading_progress_text.text = "${progressState.currentProgress}/${progressState.maxProgress}"
|
||||
@@ -238,7 +245,7 @@ class FeedFragment : BaseListFragment<FeedState, Unit>() {
|
||||
}
|
||||
|
||||
loading_progress_bar.isIndeterminate = isIndeterminate ||
|
||||
(progressState.maxProgress > 0 && progressState.currentProgress == 0)
|
||||
(progressState.maxProgress > 0 && progressState.currentProgress == 0)
|
||||
loading_progress_bar.progress = progressState.currentProgress
|
||||
|
||||
loading_progress_bar.max = progressState.maxProgress
|
||||
@@ -253,16 +260,17 @@ class FeedFragment : BaseListFragment<FeedState, Unit>() {
|
||||
|
||||
oldestSubscriptionUpdate = loadedState.oldestUpdate
|
||||
|
||||
if (loadedState.notLoadedCount > 0) {
|
||||
refresh_subtitle_text.visibility = View.VISIBLE
|
||||
val loadedCount = loadedState.notLoadedCount > 0
|
||||
refresh_subtitle_text.isVisible = loadedCount
|
||||
if (loadedCount) {
|
||||
refresh_subtitle_text.text = getString(R.string.feed_subscription_not_loaded_count, loadedState.notLoadedCount)
|
||||
} else {
|
||||
refresh_subtitle_text.visibility = View.GONE
|
||||
}
|
||||
|
||||
if (loadedState.itemsErrors.isNotEmpty()) {
|
||||
showSnackBarError(loadedState.itemsErrors, UserAction.REQUESTED_FEED,
|
||||
"none", "Loading feed", R.string.general_error)
|
||||
showSnackBarError(
|
||||
loadedState.itemsErrors, UserAction.REQUESTED_FEED,
|
||||
"none", "Loading feed", R.string.general_error
|
||||
)
|
||||
}
|
||||
|
||||
if (loadedState.items.isEmpty()) {
|
||||
@@ -305,9 +313,11 @@ class FeedFragment : BaseListFragment<FeedState, Unit>() {
|
||||
override fun hasMoreItems() = false
|
||||
|
||||
private fun triggerUpdate() {
|
||||
getActivity()?.startService(Intent(requireContext(), FeedLoadService::class.java).apply {
|
||||
putExtra(FeedLoadService.EXTRA_GROUP_ID, groupId)
|
||||
})
|
||||
getActivity()?.startService(
|
||||
Intent(requireContext(), FeedLoadService::class.java).apply {
|
||||
putExtra(FeedLoadService.EXTRA_GROUP_ID, groupId)
|
||||
}
|
||||
)
|
||||
listState = null
|
||||
}
|
||||
|
||||
@@ -330,12 +340,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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
package org.schabi.newpipe.local.feed
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
import java.util.Calendar
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem
|
||||
import java.util.Calendar
|
||||
|
||||
sealed class FeedState {
|
||||
data class ProgressState(
|
||||
|
||||
@@ -5,21 +5,21 @@ import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import io.reactivex.Flowable
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.functions.Function4
|
||||
import io.reactivex.schedulers.Schedulers
|
||||
import java.util.Calendar
|
||||
import java.util.Date
|
||||
import java.util.concurrent.TimeUnit
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.core.Flowable
|
||||
import io.reactivex.rxjava3.functions.Function4
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import org.schabi.newpipe.database.feed.model.FeedGroupEntity
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem
|
||||
import org.schabi.newpipe.ktx.toCalendar
|
||||
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
|
||||
import org.schabi.newpipe.local.feed.service.FeedEventManager.Event.ProgressEvent
|
||||
import org.schabi.newpipe.local.feed.service.FeedEventManager.Event.SuccessResultEvent
|
||||
import org.schabi.newpipe.util.DEFAULT_THROTTLE_TIMEOUT
|
||||
import java.time.OffsetDateTime
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class FeedViewModel(applicationContext: Context, val groupId: Long = FeedGroupEntity.GROUP_ALL_ID) : ViewModel() {
|
||||
class Factory(val context: Context, val groupId: Long = FeedGroupEntity.GROUP_ALL_ID) : ViewModelProvider.Factory {
|
||||
@@ -35,41 +35,39 @@ class FeedViewModel(applicationContext: Context, val groupId: Long = FeedGroupEn
|
||||
val stateLiveData: LiveData<FeedState> = mutableStateLiveData
|
||||
|
||||
private var combineDisposable = Flowable
|
||||
.combineLatest(
|
||||
FeedEventManager.events(),
|
||||
feedDatabaseManager.asStreamItems(groupId),
|
||||
feedDatabaseManager.notLoadedCount(groupId),
|
||||
feedDatabaseManager.oldestSubscriptionUpdate(groupId),
|
||||
.combineLatest(
|
||||
FeedEventManager.events(),
|
||||
feedDatabaseManager.asStreamItems(groupId),
|
||||
feedDatabaseManager.notLoadedCount(groupId),
|
||||
feedDatabaseManager.oldestSubscriptionUpdate(groupId),
|
||||
Function4 { t1: FeedEventManager.Event, t2: List<StreamInfoItem>, t3: Long, t4: List<OffsetDateTime> ->
|
||||
return@Function4 CombineResultHolder(t1, t2, t3, t4.firstOrNull())
|
||||
}
|
||||
)
|
||||
.throttleLatest(DEFAULT_THROTTLE_TIMEOUT, TimeUnit.MILLISECONDS)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe { (event, listFromDB, notLoadedCount, oldestUpdate) ->
|
||||
val oldestUpdateCalendar = oldestUpdate?.toCalendar()
|
||||
|
||||
Function4 { t1: FeedEventManager.Event, t2: List<StreamInfoItem>, t3: Long, t4: List<Date> ->
|
||||
return@Function4 CombineResultHolder(t1, t2, t3, t4.firstOrNull())
|
||||
}
|
||||
)
|
||||
.throttleLatest(DEFAULT_THROTTLE_TIMEOUT, TimeUnit.MILLISECONDS)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe {
|
||||
val (event, listFromDB, notLoadedCount, oldestUpdate) = it
|
||||
|
||||
val oldestUpdateCalendar =
|
||||
oldestUpdate?.let { Calendar.getInstance().apply { time = it } }
|
||||
|
||||
mutableStateLiveData.postValue(when (event) {
|
||||
mutableStateLiveData.postValue(
|
||||
when (event) {
|
||||
is IdleEvent -> FeedState.LoadedState(listFromDB, oldestUpdateCalendar, notLoadedCount)
|
||||
is ProgressEvent -> FeedState.ProgressState(event.currentProgress, event.maxProgress, event.progressMessage)
|
||||
is SuccessResultEvent -> FeedState.LoadedState(listFromDB, oldestUpdateCalendar, notLoadedCount, event.itemsErrors)
|
||||
is ErrorResultEvent -> FeedState.ErrorState(event.error)
|
||||
})
|
||||
|
||||
if (event is ErrorResultEvent || event is SuccessResultEvent) {
|
||||
FeedEventManager.reset()
|
||||
}
|
||||
)
|
||||
|
||||
if (event is ErrorResultEvent || event is SuccessResultEvent) {
|
||||
FeedEventManager.reset()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
super.onCleared()
|
||||
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?)
|
||||
}
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
package org.schabi.newpipe.local.feed.service
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
import io.reactivex.Flowable
|
||||
import io.reactivex.processors.BehaviorProcessor
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import io.reactivex.rxjava3.core.Flowable
|
||||
import io.reactivex.rxjava3.processors.BehaviorProcessor
|
||||
import org.schabi.newpipe.local.feed.service.FeedEventManager.Event.IdleEvent
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
|
||||
object FeedEventManager {
|
||||
private var processor: BehaviorProcessor<Event> = BehaviorProcessor.create()
|
||||
private var ignoreUpstream = AtomicBoolean()
|
||||
private var eventsFlowable = processor.startWith(IdleEvent)
|
||||
private var eventsFlowable = processor.startWithItem(IdleEvent)
|
||||
|
||||
fun postEvent(event: Event) {
|
||||
processor.onNext(event)
|
||||
|
||||
@@ -31,20 +31,15 @@ import android.util.Log
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import androidx.preference.PreferenceManager
|
||||
import io.reactivex.Flowable
|
||||
import io.reactivex.Notification
|
||||
import io.reactivex.Single
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.disposables.CompositeDisposable
|
||||
import io.reactivex.functions.Consumer
|
||||
import io.reactivex.functions.Function
|
||||
import io.reactivex.processors.PublishProcessor
|
||||
import io.reactivex.schedulers.Schedulers
|
||||
import java.io.IOException
|
||||
import java.util.Calendar
|
||||
import java.util.concurrent.TimeUnit
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.core.Flowable
|
||||
import io.reactivex.rxjava3.core.Notification
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||
import io.reactivex.rxjava3.functions.Consumer
|
||||
import io.reactivex.rxjava3.functions.Function
|
||||
import io.reactivex.rxjava3.processors.PublishProcessor
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import org.reactivestreams.Subscriber
|
||||
import org.reactivestreams.Subscription
|
||||
import org.schabi.newpipe.MainActivity.DEBUG
|
||||
@@ -55,13 +50,18 @@ import org.schabi.newpipe.extractor.exceptions.ReCaptchaException
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem
|
||||
import org.schabi.newpipe.local.feed.FeedDatabaseManager
|
||||
import org.schabi.newpipe.local.feed.service.FeedEventManager.Event.ErrorResultEvent
|
||||
import org.schabi.newpipe.local.feed.service.FeedEventManager.Event.IdleEvent
|
||||
import org.schabi.newpipe.local.feed.service.FeedEventManager.Event.ProgressEvent
|
||||
import org.schabi.newpipe.local.feed.service.FeedEventManager.Event.SuccessResultEvent
|
||||
import org.schabi.newpipe.local.feed.service.FeedEventManager.postEvent
|
||||
import org.schabi.newpipe.local.subscription.SubscriptionManager
|
||||
import org.schabi.newpipe.util.ExceptionUtils
|
||||
import org.schabi.newpipe.util.ExtractorHelper
|
||||
import java.io.IOException
|
||||
import java.time.OffsetDateTime
|
||||
import java.time.ZoneOffset
|
||||
import java.util.concurrent.TimeUnit
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
|
||||
class FeedLoadService : Service() {
|
||||
companion object {
|
||||
@@ -108,8 +108,11 @@ class FeedLoadService : Service() {
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "onStartCommand() called with: intent = [" + intent + "]," +
|
||||
" flags = [" + flags + "], startId = [" + startId + "]")
|
||||
Log.d(
|
||||
TAG,
|
||||
"onStartCommand() called with: intent = [" + intent + "]," +
|
||||
" flags = [" + flags + "], startId = [" + startId + "]"
|
||||
)
|
||||
}
|
||||
|
||||
if (intent == null || loadingSubscription != null) {
|
||||
@@ -122,10 +125,10 @@ class FeedLoadService : Service() {
|
||||
|
||||
val groupId = intent.getLongExtra(EXTRA_GROUP_ID, FeedGroupEntity.GROUP_ALL_ID)
|
||||
val useFeedExtractor = defaultSharedPreferences
|
||||
.getBoolean(getString(R.string.feed_use_dedicated_fetch_method_key), false)
|
||||
.getBoolean(getString(R.string.feed_use_dedicated_fetch_method_key), false)
|
||||
|
||||
val thresholdOutdatedSecondsString = defaultSharedPreferences
|
||||
.getString(getString(R.string.feed_update_threshold_key), getString(R.string.feed_update_threshold_default_value))
|
||||
.getString(getString(R.string.feed_update_threshold_key), getString(R.string.feed_update_threshold_default_value))
|
||||
val thresholdOutdatedSeconds = thresholdOutdatedSecondsString!!.toInt()
|
||||
|
||||
startLoading(groupId, useFeedExtractor, thresholdOutdatedSeconds)
|
||||
@@ -161,8 +164,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 +175,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)
|
||||
@@ -182,63 +183,63 @@ class FeedLoadService : Service() {
|
||||
}
|
||||
|
||||
subscriptions
|
||||
.limit(1)
|
||||
.take(1)
|
||||
|
||||
.doOnNext {
|
||||
currentProgress.set(0)
|
||||
maxProgress.set(it.size)
|
||||
.doOnNext {
|
||||
currentProgress.set(0)
|
||||
maxProgress.set(it.size)
|
||||
}
|
||||
.filter { it.isNotEmpty() }
|
||||
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.doOnNext {
|
||||
startForeground(NOTIFICATION_ID, notificationBuilder.build())
|
||||
updateNotificationProgress(null)
|
||||
broadcastProgress()
|
||||
}
|
||||
|
||||
.observeOn(Schedulers.io())
|
||||
.flatMap { Flowable.fromIterable(it) }
|
||||
.takeWhile { !cancelSignal.get() }
|
||||
|
||||
.parallel(PARALLEL_EXTRACTIONS, PARALLEL_EXTRACTIONS * 2)
|
||||
.runOn(Schedulers.io(), PARALLEL_EXTRACTIONS * 2)
|
||||
.filter { !cancelSignal.get() }
|
||||
|
||||
.map { subscriptionEntity ->
|
||||
try {
|
||||
val listInfo = if (useFeedExtractor) {
|
||||
ExtractorHelper
|
||||
.getFeedInfoFallbackToChannelInfo(subscriptionEntity.serviceId, subscriptionEntity.url)
|
||||
.blockingGet()
|
||||
} else {
|
||||
ExtractorHelper
|
||||
.getChannelInfo(subscriptionEntity.serviceId, subscriptionEntity.url, true)
|
||||
.blockingGet()
|
||||
} as ListInfo<StreamInfoItem>
|
||||
|
||||
return@map Notification.createOnNext(Pair(subscriptionEntity.uid, listInfo))
|
||||
} catch (e: Throwable) {
|
||||
val request = "${subscriptionEntity.serviceId}:${subscriptionEntity.url}"
|
||||
val wrapper = RequestException(subscriptionEntity.uid, request, e)
|
||||
return@map Notification.createOnError<Pair<Long, ListInfo<StreamInfoItem>>>(wrapper)
|
||||
}
|
||||
.filter { it.isNotEmpty() }
|
||||
}
|
||||
.sequential()
|
||||
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.doOnNext {
|
||||
startForeground(NOTIFICATION_ID, notificationBuilder.build())
|
||||
updateNotificationProgress(null)
|
||||
broadcastProgress()
|
||||
}
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.doOnNext(errorHandlingConsumer)
|
||||
|
||||
.observeOn(Schedulers.io())
|
||||
.flatMap { Flowable.fromIterable(it) }
|
||||
.takeWhile { !cancelSignal.get() }
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.doOnNext(notificationsConsumer)
|
||||
|
||||
.parallel(PARALLEL_EXTRACTIONS, PARALLEL_EXTRACTIONS * 2)
|
||||
.runOn(Schedulers.io(), PARALLEL_EXTRACTIONS * 2)
|
||||
.filter { !cancelSignal.get() }
|
||||
.observeOn(Schedulers.io())
|
||||
.buffer(BUFFER_COUNT_BEFORE_INSERT)
|
||||
.doOnNext(databaseConsumer)
|
||||
|
||||
.map { subscriptionEntity ->
|
||||
try {
|
||||
val listInfo = if (useFeedExtractor) {
|
||||
ExtractorHelper
|
||||
.getFeedInfoFallbackToChannelInfo(subscriptionEntity.serviceId, subscriptionEntity.url)
|
||||
.blockingGet()
|
||||
} else {
|
||||
ExtractorHelper
|
||||
.getChannelInfo(subscriptionEntity.serviceId, subscriptionEntity.url, true)
|
||||
.blockingGet()
|
||||
} as ListInfo<StreamInfoItem>
|
||||
|
||||
return@map Notification.createOnNext(Pair(subscriptionEntity.uid, listInfo))
|
||||
} catch (e: Throwable) {
|
||||
val request = "${subscriptionEntity.serviceId}:${subscriptionEntity.url}"
|
||||
val wrapper = RequestException(subscriptionEntity.uid, request, e)
|
||||
return@map Notification.createOnError<Pair<Long, ListInfo<StreamInfoItem>>>(wrapper)
|
||||
}
|
||||
}
|
||||
.sequential()
|
||||
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.doOnNext(errorHandlingConsumer)
|
||||
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.doOnNext(notificationsConsumer)
|
||||
|
||||
.observeOn(Schedulers.io())
|
||||
.buffer(BUFFER_COUNT_BEFORE_INSERT)
|
||||
.doOnNext(databaseConsumer)
|
||||
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(resultSubscriber)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(resultSubscriber)
|
||||
}
|
||||
|
||||
private fun broadcastProgress() {
|
||||
@@ -263,7 +264,7 @@ class FeedLoadService : Service() {
|
||||
|
||||
override fun onComplete() {
|
||||
if (maxProgress.get() == 0) {
|
||||
postEvent(IdleEvent)
|
||||
postEvent(FeedEventManager.Event.IdleEvent)
|
||||
stopService()
|
||||
|
||||
return
|
||||
@@ -275,7 +276,8 @@ class FeedLoadService : Service() {
|
||||
notificationUpdater.onNext(getString(R.string.feed_processing_message))
|
||||
postEvent(ProgressEvent(R.string.feed_processing_message))
|
||||
|
||||
disposables.add(Single
|
||||
disposables.add(
|
||||
Single
|
||||
.fromCallable {
|
||||
feedResultsHolder.ready()
|
||||
|
||||
@@ -294,7 +296,8 @@ class FeedLoadService : Service() {
|
||||
return@subscribe
|
||||
}
|
||||
stopService()
|
||||
})
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -365,16 +368,18 @@ class FeedLoadService : Service() {
|
||||
private var maxProgress = AtomicInteger(-1)
|
||||
|
||||
private fun createNotification(): NotificationCompat.Builder {
|
||||
val cancelActionIntent = PendingIntent.getBroadcast(this,
|
||||
NOTIFICATION_ID, Intent(ACTION_CANCEL), 0)
|
||||
val cancelActionIntent = PendingIntent.getBroadcast(
|
||||
this,
|
||||
NOTIFICATION_ID, Intent(ACTION_CANCEL), 0
|
||||
)
|
||||
|
||||
return NotificationCompat.Builder(this, getString(R.string.notification_channel_id))
|
||||
.setOngoing(true)
|
||||
.setProgress(-1, -1, true)
|
||||
.setSmallIcon(R.drawable.ic_newpipe_triangle_white)
|
||||
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
|
||||
.addAction(0, getString(R.string.cancel), cancelActionIntent)
|
||||
.setContentTitle(getString(R.string.feed_notification_loading))
|
||||
.setOngoing(true)
|
||||
.setProgress(-1, -1, true)
|
||||
.setSmallIcon(R.drawable.ic_newpipe_triangle_white)
|
||||
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
|
||||
.addAction(0, getString(R.string.cancel), cancelActionIntent)
|
||||
.setContentTitle(getString(R.string.feed_notification_loading))
|
||||
}
|
||||
|
||||
private fun setupNotification() {
|
||||
@@ -382,13 +387,15 @@ class FeedLoadService : Service() {
|
||||
notificationBuilder = createNotification()
|
||||
|
||||
val throttleAfterFirstEmission = Function { flow: Flowable<String> ->
|
||||
flow.limit(1).concatWith(flow.skip(1).throttleLatest(NOTIFICATION_SAMPLING_PERIOD.toLong(), TimeUnit.MILLISECONDS))
|
||||
flow.take(1).concatWith(flow.skip(1).throttleLatest(NOTIFICATION_SAMPLING_PERIOD.toLong(), TimeUnit.MILLISECONDS))
|
||||
}
|
||||
|
||||
disposables.add(notificationUpdater
|
||||
disposables.add(
|
||||
notificationUpdater
|
||||
.publish(throttleAfterFirstEmission)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(this::updateNotificationProgress))
|
||||
.subscribe(this::updateNotificationProgress)
|
||||
)
|
||||
}
|
||||
|
||||
private fun updateNotificationProgress(updateDescription: String?) {
|
||||
|
||||
@@ -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,16 +44,17 @@ 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;
|
||||
import io.reactivex.Flowable;
|
||||
import io.reactivex.Maybe;
|
||||
import io.reactivex.Single;
|
||||
import io.reactivex.schedulers.Schedulers;
|
||||
import io.reactivex.rxjava3.core.Completable;
|
||||
import io.reactivex.rxjava3.core.Flowable;
|
||||
import io.reactivex.rxjava3.core.Maybe;
|
||||
import io.reactivex.rxjava3.core.Single;
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers;
|
||||
|
||||
public class HistoryRecordManager {
|
||||
private final AppDatabase database;
|
||||
@@ -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(() -> {
|
||||
|
||||
@@ -25,13 +25,16 @@ 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;
|
||||
import org.schabi.newpipe.report.ErrorInfo;
|
||||
import org.schabi.newpipe.report.UserAction;
|
||||
import org.schabi.newpipe.settings.SettingsActivity;
|
||||
import org.schabi.newpipe.util.NavigationHelper;
|
||||
@@ -40,13 +43,15 @@ 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;
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.disposables.CompositeDisposable;
|
||||
import io.reactivex.disposables.Disposable;
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
||||
import io.reactivex.rxjava3.disposables.Disposable;
|
||||
|
||||
public class StatisticsPlaylistFragment
|
||||
extends BaseLocalListFragment<List<StreamStatisticsEntry>, Void> {
|
||||
@@ -66,18 +71,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 +151,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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -179,7 +184,7 @@ public class StatisticsPlaylistFragment
|
||||
throwable -> ErrorActivity.reportError(getContext(),
|
||||
throwable,
|
||||
SettingsActivity.class, null,
|
||||
ErrorActivity.ErrorInfo.make(
|
||||
ErrorInfo.make(
|
||||
UserAction.DELETE_FROM_HISTORY,
|
||||
"none",
|
||||
"Delete view history",
|
||||
@@ -193,7 +198,7 @@ public class StatisticsPlaylistFragment
|
||||
throwable -> ErrorActivity.reportError(getContext(),
|
||||
throwable,
|
||||
SettingsActivity.class, null,
|
||||
ErrorActivity.ErrorInfo.make(
|
||||
ErrorInfo.make(
|
||||
UserAction.DELETE_FROM_HISTORY,
|
||||
"none",
|
||||
"Delete search history",
|
||||
@@ -321,7 +326,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 +392,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 +426,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();
|
||||
|
||||
@@ -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) { }
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,6 @@ import androidx.core.content.ContextCompat;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.database.LocalItem;
|
||||
import org.schabi.newpipe.database.playlist.PlaylistStreamEntry;
|
||||
import org.schabi.newpipe.database.stream.model.StreamStateEntity;
|
||||
import org.schabi.newpipe.extractor.NewPipe;
|
||||
import org.schabi.newpipe.local.LocalItemBuilder;
|
||||
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
||||
@@ -20,8 +19,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.util.ArrayList;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public class LocalPlaylistStreamItemHolder extends LocalItemHolder {
|
||||
@@ -52,7 +50,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;
|
||||
}
|
||||
@@ -70,15 +68,11 @@ public class LocalPlaylistStreamItemHolder extends LocalItemHolder {
|
||||
R.color.duration_background_color));
|
||||
itemDurationView.setVisibility(View.VISIBLE);
|
||||
|
||||
final StreamStateEntity state = historyRecordManager
|
||||
.loadLocalStreamStateBatch(new ArrayList<LocalItem>() {{
|
||||
add(localItem);
|
||||
}}).blockingGet().get(0);
|
||||
if (state != null) {
|
||||
if (item.getProgressTime() > 0) {
|
||||
itemProgressView.setVisibility(View.VISIBLE);
|
||||
itemProgressView.setMax((int) item.getStreamEntity().getDuration());
|
||||
itemProgressView.setProgress((int) TimeUnit.MILLISECONDS
|
||||
.toSeconds(state.getProgressTime()));
|
||||
.toSeconds(item.getProgressTime()));
|
||||
} else {
|
||||
itemProgressView.setVisibility(View.GONE);
|
||||
}
|
||||
@@ -104,7 +98,6 @@ public class LocalPlaylistStreamItemHolder extends LocalItemHolder {
|
||||
return true;
|
||||
});
|
||||
|
||||
itemThumbnailView.setOnTouchListener(getOnTouchListener(item));
|
||||
itemHandleView.setOnTouchListener(getOnTouchListener(item));
|
||||
}
|
||||
|
||||
@@ -116,18 +109,14 @@ public class LocalPlaylistStreamItemHolder extends LocalItemHolder {
|
||||
}
|
||||
final PlaylistStreamEntry item = (PlaylistStreamEntry) localItem;
|
||||
|
||||
final StreamStateEntity state = historyRecordManager
|
||||
.loadLocalStreamStateBatch(new ArrayList<LocalItem>() {{
|
||||
add(localItem);
|
||||
}}).blockingGet().get(0);
|
||||
if (state != null && item.getStreamEntity().getDuration() > 0) {
|
||||
if (item.getProgressTime() > 0 && item.getStreamEntity().getDuration() > 0) {
|
||||
itemProgressView.setMax((int) item.getStreamEntity().getDuration());
|
||||
if (itemProgressView.getVisibility() == View.VISIBLE) {
|
||||
itemProgressView.setProgressAnimated((int) TimeUnit.MILLISECONDS
|
||||
.toSeconds(state.getProgressTime()));
|
||||
.toSeconds(item.getProgressTime()));
|
||||
} else {
|
||||
itemProgressView.setProgress((int) TimeUnit.MILLISECONDS
|
||||
.toSeconds(state.getProgressTime()));
|
||||
.toSeconds(item.getProgressTime()));
|
||||
AnimationUtils.animateView(itemProgressView, true, 500);
|
||||
}
|
||||
} else if (itemProgressView.getVisibility() == View.VISIBLE) {
|
||||
|
||||
@@ -11,7 +11,6 @@ import androidx.core.content.ContextCompat;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.database.LocalItem;
|
||||
import org.schabi.newpipe.database.stream.StreamStatisticsEntry;
|
||||
import org.schabi.newpipe.database.stream.model.StreamStateEntity;
|
||||
import org.schabi.newpipe.extractor.NewPipe;
|
||||
import org.schabi.newpipe.local.LocalItemBuilder;
|
||||
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
||||
@@ -20,8 +19,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.util.ArrayList;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/*
|
||||
@@ -71,10 +69,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 +80,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;
|
||||
}
|
||||
@@ -98,15 +96,11 @@ public class LocalStatisticStreamItemHolder extends LocalItemHolder {
|
||||
R.color.duration_background_color));
|
||||
itemDurationView.setVisibility(View.VISIBLE);
|
||||
|
||||
final StreamStateEntity state = historyRecordManager
|
||||
.loadLocalStreamStateBatch(new ArrayList<LocalItem>() {{
|
||||
add(localItem);
|
||||
}}).blockingGet().get(0);
|
||||
if (state != null) {
|
||||
if (item.getProgressTime() > 0) {
|
||||
itemProgressView.setVisibility(View.VISIBLE);
|
||||
itemProgressView.setMax((int) item.getStreamEntity().getDuration());
|
||||
itemProgressView.setProgress((int) TimeUnit.MILLISECONDS
|
||||
.toSeconds(state.getProgressTime()));
|
||||
.toSeconds(item.getProgressTime()));
|
||||
} else {
|
||||
itemProgressView.setVisibility(View.GONE);
|
||||
}
|
||||
@@ -116,7 +110,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
|
||||
@@ -146,18 +140,14 @@ public class LocalStatisticStreamItemHolder extends LocalItemHolder {
|
||||
}
|
||||
final StreamStatisticsEntry item = (StreamStatisticsEntry) localItem;
|
||||
|
||||
final StreamStateEntity state = historyRecordManager
|
||||
.loadLocalStreamStateBatch(new ArrayList<LocalItem>() {{
|
||||
add(localItem);
|
||||
}}).blockingGet().get(0);
|
||||
if (state != null && item.getStreamEntity().getDuration() > 0) {
|
||||
if (item.getProgressTime() > 0 && item.getStreamEntity().getDuration() > 0) {
|
||||
itemProgressView.setMax((int) item.getStreamEntity().getDuration());
|
||||
if (itemProgressView.getVisibility() == View.VISIBLE) {
|
||||
itemProgressView.setProgressAnimated((int) TimeUnit.MILLISECONDS
|
||||
.toSeconds(state.getProgressTime()));
|
||||
.toSeconds(item.getProgressTime()));
|
||||
} else {
|
||||
itemProgressView.setProgress((int) TimeUnit.MILLISECONDS
|
||||
.toSeconds(state.getProgressTime()));
|
||||
.toSeconds(item.getProgressTime()));
|
||||
AnimationUtils.animateView(itemProgressView, true, 500);
|
||||
}
|
||||
} else if (itemProgressView.getVisibility() == View.VISIBLE) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -52,13 +55,12 @@ import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
import icepick.State;
|
||||
import io.reactivex.Flowable;
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.disposables.CompositeDisposable;
|
||||
import io.reactivex.disposables.Disposable;
|
||||
import io.reactivex.disposables.Disposables;
|
||||
import io.reactivex.schedulers.Schedulers;
|
||||
import io.reactivex.subjects.PublishSubject;
|
||||
import io.reactivex.rxjava3.core.Flowable;
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
||||
import io.reactivex.rxjava3.disposables.Disposable;
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers;
|
||||
import io.reactivex.rxjava3.subjects.PublishSubject;
|
||||
|
||||
import static org.schabi.newpipe.util.AnimationUtils.animateView;
|
||||
|
||||
@@ -176,10 +178,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 +494,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 ->
|
||||
@@ -638,7 +640,7 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
|
||||
|
||||
private Disposable getDebouncedSaver() {
|
||||
if (debouncedSaveSignal == null) {
|
||||
return Disposables.empty();
|
||||
return Disposable.empty();
|
||||
}
|
||||
|
||||
return debouncedSaveSignal
|
||||
@@ -756,29 +758,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,
|
||||
|
||||
@@ -15,11 +15,11 @@ import org.schabi.newpipe.database.stream.model.StreamEntity;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import io.reactivex.Completable;
|
||||
import io.reactivex.Flowable;
|
||||
import io.reactivex.Maybe;
|
||||
import io.reactivex.Single;
|
||||
import io.reactivex.schedulers.Schedulers;
|
||||
import io.reactivex.rxjava3.core.Completable;
|
||||
import io.reactivex.rxjava3.core.Flowable;
|
||||
import io.reactivex.rxjava3.core.Maybe;
|
||||
import io.reactivex.rxjava3.core.Single;
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers;
|
||||
|
||||
public class LocalPlaylistManager {
|
||||
private final AppDatabase database;
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,9 +7,9 @@ import org.schabi.newpipe.extractor.playlist.PlaylistInfo;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import io.reactivex.Flowable;
|
||||
import io.reactivex.Single;
|
||||
import io.reactivex.schedulers.Schedulers;
|
||||
import io.reactivex.rxjava3.core.Flowable;
|
||||
import io.reactivex.rxjava3.core.Single;
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers;
|
||||
|
||||
public class RemotePlaylistManager {
|
||||
|
||||
|
||||
@@ -28,13 +28,7 @@ import com.xwray.groupie.Item
|
||||
import com.xwray.groupie.Section
|
||||
import com.xwray.groupie.kotlinandroidextensions.GroupieViewHolder
|
||||
import icepick.State
|
||||
import io.reactivex.disposables.CompositeDisposable
|
||||
import java.io.File
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
import kotlin.math.floor
|
||||
import kotlin.math.max
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||
import kotlinx.android.synthetic.main.dialog_title.view.itemAdditionalDetails
|
||||
import kotlinx.android.synthetic.main.dialog_title.view.itemTitleView
|
||||
import kotlinx.android.synthetic.main.fragment_subscription.items_list
|
||||
@@ -68,6 +62,12 @@ import org.schabi.newpipe.util.NavigationHelper
|
||||
import org.schabi.newpipe.util.OnClickGesture
|
||||
import org.schabi.newpipe.util.ShareUtils
|
||||
import org.schabi.newpipe.util.ThemeHelper
|
||||
import java.io.File
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
import kotlin.math.floor
|
||||
import kotlin.math.max
|
||||
|
||||
class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
|
||||
private lateinit var viewModel: SubscriptionViewModel
|
||||
@@ -208,14 +208,19 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
|
||||
if (!exportFile.parentFile.canWrite() || !exportFile.parentFile.canRead()) {
|
||||
Toast.makeText(activity, R.string.invalid_directory, Toast.LENGTH_SHORT).show()
|
||||
} else {
|
||||
activity.startService(Intent(activity, SubscriptionsExportService::class.java)
|
||||
.putExtra(KEY_FILE_PATH, exportFile.absolutePath))
|
||||
activity.startService(
|
||||
Intent(activity, SubscriptionsExportService::class.java)
|
||||
.putExtra(KEY_FILE_PATH, exportFile.absolutePath)
|
||||
)
|
||||
}
|
||||
} else if (requestCode == REQUEST_IMPORT_CODE) {
|
||||
val path = Utils.getFileForUri(data.data!!).absolutePath
|
||||
ImportConfirmationDialog.show(this, Intent(activity, SubscriptionsImportService::class.java)
|
||||
ImportConfirmationDialog.show(
|
||||
this,
|
||||
Intent(activity, SubscriptionsImportService::class.java)
|
||||
.putExtra(KEY_MODE, PREVIOUS_EXPORT_MODE)
|
||||
.putExtra(KEY_VALUE, path))
|
||||
.putExtra(KEY_VALUE, path)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -247,9 +252,9 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
|
||||
|
||||
feedGroupsCarousel = FeedGroupCarouselItem(requireContext(), carouselAdapter)
|
||||
feedGroupsSortMenuItem = HeaderWithMenuItem(
|
||||
getString(R.string.feed_groups_header_title),
|
||||
ThemeHelper.resolveResourceIdFromAttr(requireContext(), R.attr.ic_sort),
|
||||
menuItemOnClickListener = ::openReorderDialog
|
||||
getString(R.string.feed_groups_header_title),
|
||||
ThemeHelper.resolveResourceIdFromAttr(requireContext(), R.attr.ic_sort),
|
||||
menuItemOnClickListener = ::openReorderDialog
|
||||
)
|
||||
add(Section(feedGroupsSortMenuItem, listOf(feedGroupsCarousel)))
|
||||
|
||||
@@ -260,10 +265,11 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
|
||||
subscriptionsSection.setHideWhenEmpty(true)
|
||||
|
||||
importExportItem = FeedImportExportItem(
|
||||
{ onImportPreviousSelected() },
|
||||
{ onImportFromServiceSelected(it) },
|
||||
{ onExportSelected() },
|
||||
importExportItemExpandedState ?: false)
|
||||
{ onImportPreviousSelected() },
|
||||
{ onImportFromServiceSelected(it) },
|
||||
{ onExportSelected() },
|
||||
importExportItemExpandedState ?: false
|
||||
)
|
||||
groupAdapter.add(Section(importExportItem, listOf(subscriptionsSection)))
|
||||
}
|
||||
|
||||
@@ -284,8 +290,8 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
|
||||
|
||||
private fun showLongTapDialog(selectedItem: ChannelInfoItem) {
|
||||
val commands = arrayOf(
|
||||
getString(R.string.share),
|
||||
getString(R.string.unsubscribe)
|
||||
getString(R.string.share),
|
||||
getString(R.string.unsubscribe)
|
||||
)
|
||||
|
||||
val actions = DialogInterface.OnClickListener { _, i ->
|
||||
@@ -301,16 +307,18 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
|
||||
bannerView.itemAdditionalDetails.visibility = View.GONE
|
||||
|
||||
AlertDialog.Builder(requireContext())
|
||||
.setCustomTitle(bannerView)
|
||||
.setItems(commands, actions)
|
||||
.create()
|
||||
.show()
|
||||
.setCustomTitle(bannerView)
|
||||
.setItems(commands, actions)
|
||||
.create()
|
||||
.show()
|
||||
}
|
||||
|
||||
private fun deleteChannel(selectedItem: ChannelInfoItem) {
|
||||
disposables.add(subscriptionManager.deleteSubscription(selectedItem.serviceId, selectedItem.url).subscribe {
|
||||
Toast.makeText(requireContext(), getString(R.string.channel_unsubscribed), Toast.LENGTH_SHORT).show()
|
||||
})
|
||||
disposables.add(
|
||||
subscriptionManager.deleteSubscription(selectedItem.serviceId, selectedItem.url).subscribe {
|
||||
Toast.makeText(requireContext(), getString(R.string.channel_unsubscribed), Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
override fun doInitialLoadLogic() = Unit
|
||||
@@ -332,8 +340,10 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
|
||||
}
|
||||
|
||||
private val listenerChannelItem = object : OnClickGesture<ChannelInfoItem>() {
|
||||
override fun selected(selectedItem: ChannelInfoItem) = NavigationHelper.openChannelFragment(fm,
|
||||
selectedItem.serviceId, selectedItem.url, selectedItem.name)
|
||||
override fun selected(selectedItem: ChannelInfoItem) = NavigationHelper.openChannelFragment(
|
||||
fm,
|
||||
selectedItem.serviceId, selectedItem.url, selectedItem.name
|
||||
)
|
||||
|
||||
override fun held(selectedItem: ChannelInfoItem) = showLongTapDialog(selectedItem)
|
||||
}
|
||||
@@ -420,14 +430,16 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
|
||||
|
||||
private fun shouldUseGridLayout(): Boolean {
|
||||
val listMode = PreferenceManager.getDefaultSharedPreferences(requireContext())
|
||||
.getString(getString(R.string.list_view_mode_key), getString(R.string.list_view_mode_value))
|
||||
.getString(getString(R.string.list_view_mode_key), getString(R.string.list_view_mode_value))
|
||||
|
||||
return when (listMode) {
|
||||
getString(R.string.list_view_mode_auto_key) -> {
|
||||
val configuration = resources.configuration
|
||||
|
||||
(configuration.orientation == Configuration.ORIENTATION_LANDSCAPE &&
|
||||
configuration.isLayoutSizeAtLeast(Configuration.SCREENLAYOUT_SIZE_LARGE))
|
||||
(
|
||||
configuration.orientation == Configuration.ORIENTATION_LANDSCAPE &&
|
||||
configuration.isLayoutSizeAtLeast(Configuration.SCREENLAYOUT_SIZE_LARGE)
|
||||
)
|
||||
}
|
||||
getString(R.string.list_view_mode_grid_key) -> true
|
||||
else -> false
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
package org.schabi.newpipe.local.subscription
|
||||
|
||||
import android.content.Context
|
||||
import io.reactivex.Completable
|
||||
import io.reactivex.Flowable
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.schedulers.Schedulers
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.core.Completable
|
||||
import io.reactivex.rxjava3.core.Flowable
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import org.schabi.newpipe.NewPipeDatabase
|
||||
import org.schabi.newpipe.database.feed.model.FeedGroupEntity
|
||||
import org.schabi.newpipe.database.subscription.SubscriptionDAO
|
||||
@@ -32,7 +32,8 @@ class SubscriptionManager(context: Context) {
|
||||
filterQuery.isNotEmpty() -> {
|
||||
return if (showOnlyUngrouped) {
|
||||
subscriptionTable.getSubscriptionsOnlyUngroupedFiltered(
|
||||
currentGroupId, filterQuery)
|
||||
currentGroupId, filterQuery
|
||||
)
|
||||
} else {
|
||||
subscriptionTable.getSubscriptionsFiltered(filterQuery)
|
||||
}
|
||||
@@ -44,7 +45,8 @@ class SubscriptionManager(context: Context) {
|
||||
|
||||
fun upsertAll(infoList: List<ChannelInfo>): List<SubscriptionEntity> {
|
||||
val listEntities = subscriptionTable.upsertAll(
|
||||
infoList.map { SubscriptionEntity.from(it) })
|
||||
infoList.map { SubscriptionEntity.from(it) }
|
||||
)
|
||||
|
||||
database.runInTransaction {
|
||||
infoList.forEachIndexed { index, info ->
|
||||
|
||||
@@ -5,12 +5,12 @@ import androidx.lifecycle.AndroidViewModel
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import com.xwray.groupie.Group
|
||||
import io.reactivex.schedulers.Schedulers
|
||||
import java.util.concurrent.TimeUnit
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import org.schabi.newpipe.local.feed.FeedDatabaseManager
|
||||
import org.schabi.newpipe.local.subscription.item.ChannelItem
|
||||
import org.schabi.newpipe.local.subscription.item.FeedGroupCardItem
|
||||
import org.schabi.newpipe.util.DEFAULT_THROTTLE_TIMEOUT
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class SubscriptionViewModel(application: Application) : AndroidViewModel(application) {
|
||||
private var feedDatabaseManager: FeedDatabaseManager = FeedDatabaseManager(application)
|
||||
@@ -22,22 +22,22 @@ class SubscriptionViewModel(application: Application) : AndroidViewModel(applica
|
||||
val feedGroupsLiveData: LiveData<List<Group>> = mutableFeedGroupsLiveData
|
||||
|
||||
private var feedGroupItemsDisposable = feedDatabaseManager.groups()
|
||||
.throttleLatest(DEFAULT_THROTTLE_TIMEOUT, TimeUnit.MILLISECONDS)
|
||||
.map { it.map(::FeedGroupCardItem) }
|
||||
.subscribeOn(Schedulers.io())
|
||||
.subscribe(
|
||||
{ mutableFeedGroupsLiveData.postValue(it) },
|
||||
{ mutableStateLiveData.postValue(SubscriptionState.ErrorState(it)) }
|
||||
)
|
||||
.throttleLatest(DEFAULT_THROTTLE_TIMEOUT, TimeUnit.MILLISECONDS)
|
||||
.map { it.map(::FeedGroupCardItem) }
|
||||
.subscribeOn(Schedulers.io())
|
||||
.subscribe(
|
||||
{ mutableFeedGroupsLiveData.postValue(it) },
|
||||
{ mutableStateLiveData.postValue(SubscriptionState.ErrorState(it)) }
|
||||
)
|
||||
|
||||
private var stateItemsDisposable = subscriptionManager.subscriptions()
|
||||
.throttleLatest(DEFAULT_THROTTLE_TIMEOUT, TimeUnit.MILLISECONDS)
|
||||
.map { it.map { entity -> ChannelItem(entity.toChannelInfoItem(), entity.uid, ChannelItem.ItemVersion.MINI) } }
|
||||
.subscribeOn(Schedulers.io())
|
||||
.subscribe(
|
||||
{ mutableStateLiveData.postValue(SubscriptionState.LoadedState(it)) },
|
||||
{ mutableStateLiveData.postValue(SubscriptionState.ErrorState(it)) }
|
||||
)
|
||||
.throttleLatest(DEFAULT_THROTTLE_TIMEOUT, TimeUnit.MILLISECONDS)
|
||||
.map { it.map { entity -> ChannelItem(entity.toChannelInfoItem(), entity.uid, ChannelItem.ItemVersion.MINI) } }
|
||||
.subscribeOn(Schedulers.io())
|
||||
.subscribe(
|
||||
{ mutableStateLiveData.postValue(SubscriptionState.LoadedState(it)) },
|
||||
{ mutableStateLiveData.postValue(SubscriptionState.ErrorState(it)) }
|
||||
)
|
||||
|
||||
override fun onCleared() {
|
||||
super.onCleared()
|
||||
|
||||
@@ -27,6 +27,7 @@ import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||
import org.schabi.newpipe.extractor.subscription.SubscriptionExtractor;
|
||||
import org.schabi.newpipe.local.subscription.services.SubscriptionsImportService;
|
||||
import org.schabi.newpipe.report.ErrorActivity;
|
||||
import org.schabi.newpipe.report.ErrorInfo;
|
||||
import org.schabi.newpipe.report.UserAction;
|
||||
import org.schabi.newpipe.util.Constants;
|
||||
import org.schabi.newpipe.util.FilePickerActivityHelper;
|
||||
@@ -84,7 +85,7 @@ public class SubscriptionsImportFragment extends BaseFragment {
|
||||
setupServiceVariables();
|
||||
if (supportedSources.isEmpty() && currentServiceId != Constants.NO_SERVICE_ID) {
|
||||
ErrorActivity.reportError(activity, Collections.emptyList(), null, null,
|
||||
ErrorActivity.ErrorInfo.make(UserAction.SOMETHING_ELSE,
|
||||
ErrorInfo.make(UserAction.SOMETHING_ELSE,
|
||||
NewPipe.getNameOfService(currentServiceId),
|
||||
"Service don't support importing", R.string.general_error));
|
||||
activity.finish();
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
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
|
||||
@@ -23,8 +24,6 @@ import com.xwray.groupie.Section
|
||||
import com.xwray.groupie.kotlinandroidextensions.GroupieViewHolder
|
||||
import icepick.Icepick
|
||||
import icepick.State
|
||||
import java.io.Serializable
|
||||
import kotlin.collections.contains
|
||||
import kotlinx.android.synthetic.main.dialog_feed_group_create.*
|
||||
import kotlinx.android.synthetic.main.toolbar_search_layout.*
|
||||
import org.schabi.newpipe.R
|
||||
@@ -42,6 +41,8 @@ import org.schabi.newpipe.local.subscription.item.PickerIconItem
|
||||
import org.schabi.newpipe.local.subscription.item.PickerSubscriptionItem
|
||||
import org.schabi.newpipe.util.DeviceUtils
|
||||
import org.schabi.newpipe.util.ThemeHelper
|
||||
import java.io.Serializable
|
||||
import kotlin.collections.contains
|
||||
|
||||
class FeedGroupDialog : DialogFragment(), BackPressable {
|
||||
private lateinit var viewModel: FeedGroupDialogViewModel
|
||||
@@ -115,21 +116,30 @@ class FeedGroupDialog : DialogFragment(), BackPressable {
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
viewModel = ViewModelProvider(this,
|
||||
FeedGroupDialogViewModel.Factory(requireContext(),
|
||||
groupId, subscriptionsCurrentSearchQuery, subscriptionsShowOnlyUngrouped)
|
||||
viewModel = ViewModelProvider(
|
||||
this,
|
||||
FeedGroupDialogViewModel.Factory(
|
||||
requireContext(),
|
||||
groupId, subscriptionsCurrentSearchQuery, subscriptionsShowOnlyUngrouped
|
||||
)
|
||||
).get(FeedGroupDialogViewModel::class.java)
|
||||
|
||||
viewModel.groupLiveData.observe(viewLifecycleOwner, Observer(::handleGroup))
|
||||
viewModel.subscriptionsLiveData.observe(viewLifecycleOwner, Observer {
|
||||
setupSubscriptionPicker(it.first, it.second)
|
||||
})
|
||||
viewModel.dialogEventLiveData.observe(viewLifecycleOwner, Observer {
|
||||
when (it) {
|
||||
ProcessingEvent -> disableInput()
|
||||
SuccessEvent -> dismiss()
|
||||
viewModel.subscriptionsLiveData.observe(
|
||||
viewLifecycleOwner,
|
||||
Observer {
|
||||
setupSubscriptionPicker(it.first, it.second)
|
||||
}
|
||||
})
|
||||
)
|
||||
viewModel.dialogEventLiveData.observe(
|
||||
viewLifecycleOwner,
|
||||
Observer {
|
||||
when (it) {
|
||||
ProcessingEvent -> disableInput()
|
||||
SuccessEvent -> dismiss()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
subscriptionGroupAdapter = GroupAdapter<GroupieViewHolder>().apply {
|
||||
add(subscriptionMainSection)
|
||||
@@ -140,8 +150,10 @@ class FeedGroupDialog : DialogFragment(), BackPressable {
|
||||
// Disable animations, too distracting.
|
||||
itemAnimator = null
|
||||
adapter = subscriptionGroupAdapter
|
||||
layoutManager = GridLayoutManager(requireContext(), subscriptionGroupAdapter.spanCount,
|
||||
RecyclerView.VERTICAL, false).apply {
|
||||
layoutManager = GridLayoutManager(
|
||||
requireContext(), subscriptionGroupAdapter.spanCount,
|
||||
RecyclerView.VERTICAL, false
|
||||
).apply {
|
||||
spanSizeLookup = subscriptionGroupAdapter.spanSizeLookup
|
||||
}
|
||||
}
|
||||
@@ -191,16 +203,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() }
|
||||
|
||||
@@ -228,7 +235,7 @@ class FeedGroupDialog : DialogFragment(), BackPressable {
|
||||
}
|
||||
|
||||
toolbar_search_clear.setOnClickListener {
|
||||
if (TextUtils.isEmpty(toolbar_search_edit_text.text)) {
|
||||
if (toolbar_search_edit_text.text.isEmpty()) {
|
||||
hideSearch()
|
||||
return@setOnClickListener
|
||||
}
|
||||
@@ -242,15 +249,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)
|
||||
}
|
||||
@@ -354,7 +357,8 @@ class FeedGroupDialog : DialogFragment(), BackPressable {
|
||||
val selectedCount = this.selectedSubscriptions.size
|
||||
val selectedCountText = resources.getQuantityString(
|
||||
R.plurals.feed_group_dialog_selection_count,
|
||||
selectedCount, selectedCount)
|
||||
selectedCount, selectedCount
|
||||
)
|
||||
selected_subscription_count_view.text = selectedCountText
|
||||
subscriptions_header_info.text = selectedCountText
|
||||
}
|
||||
@@ -409,26 +413,21 @@ class FeedGroupDialog : DialogFragment(), BackPressable {
|
||||
separator.onlyVisibleIn(SubscriptionsPickerScreen, IconPickerScreen)
|
||||
cancel_button.onlyVisibleIn(InitialScreen, DeleteScreen)
|
||||
|
||||
confirm_button.setText(when {
|
||||
currentScreen == InitialScreen && groupId == NO_GROUP_SELECTED -> R.string.create
|
||||
else -> android.R.string.ok
|
||||
})
|
||||
confirm_button.setText(
|
||||
when {
|
||||
currentScreen == InitialScreen && groupId == NO_GROUP_SELECTED -> R.string.create
|
||||
else -> android.R.string.ok
|
||||
}
|
||||
)
|
||||
|
||||
delete_button.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 +458,7 @@ class FeedGroupDialog : DialogFragment(), BackPressable {
|
||||
}
|
||||
|
||||
private val inputMethodManager by lazy {
|
||||
requireActivity().getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
|
||||
requireActivity().getSystemService<InputMethodManager>()!!
|
||||
}
|
||||
|
||||
private fun showKeyboardSearch() {
|
||||
@@ -469,8 +468,10 @@ class FeedGroupDialog : DialogFragment(), BackPressable {
|
||||
}
|
||||
|
||||
private fun hideKeyboardSearch() {
|
||||
inputMethodManager.hideSoftInputFromWindow(toolbar_search_edit_text.windowToken,
|
||||
InputMethodManager.RESULT_UNCHANGED_SHOWN)
|
||||
inputMethodManager.hideSoftInputFromWindow(
|
||||
toolbar_search_edit_text.windowToken,
|
||||
InputMethodManager.RESULT_UNCHANGED_SHOWN
|
||||
)
|
||||
toolbar_search_edit_text.clearFocus()
|
||||
}
|
||||
|
||||
@@ -481,8 +482,10 @@ class FeedGroupDialog : DialogFragment(), BackPressable {
|
||||
}
|
||||
|
||||
private fun hideKeyboard() {
|
||||
inputMethodManager.hideSoftInputFromWindow(group_name_input.windowToken,
|
||||
InputMethodManager.RESULT_UNCHANGED_SHOWN)
|
||||
inputMethodManager.hideSoftInputFromWindow(
|
||||
group_name_input.windowToken,
|
||||
InputMethodManager.RESULT_UNCHANGED_SHOWN
|
||||
)
|
||||
group_name_input.clearFocus()
|
||||
}
|
||||
|
||||
@@ -501,11 +504,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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,12 +5,12 @@ import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import io.reactivex.Completable
|
||||
import io.reactivex.Flowable
|
||||
import io.reactivex.disposables.Disposable
|
||||
import io.reactivex.functions.BiFunction
|
||||
import io.reactivex.processors.BehaviorProcessor
|
||||
import io.reactivex.schedulers.Schedulers
|
||||
import io.reactivex.rxjava3.core.Completable
|
||||
import io.reactivex.rxjava3.core.Flowable
|
||||
import io.reactivex.rxjava3.disposables.Disposable
|
||||
import io.reactivex.rxjava3.functions.BiFunction
|
||||
import io.reactivex.rxjava3.processors.BehaviorProcessor
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import org.schabi.newpipe.database.feed.model.FeedGroupEntity
|
||||
import org.schabi.newpipe.local.feed.FeedDatabaseManager
|
||||
import org.schabi.newpipe.local.subscription.FeedGroupIcon
|
||||
@@ -32,13 +32,13 @@ class FeedGroupDialogViewModel(
|
||||
|
||||
private var subscriptionsFlowable = Flowable
|
||||
.combineLatest(
|
||||
filterSubscriptions.startWith(initialQuery),
|
||||
toggleShowOnlyUngrouped.startWith(initialShowOnlyUngrouped),
|
||||
BiFunction { t1: String, t2: Boolean -> Filter(t1, t2) }
|
||||
filterSubscriptions.startWithItem(initialQuery),
|
||||
toggleShowOnlyUngrouped.startWithItem(initialShowOnlyUngrouped),
|
||||
BiFunction { t1: String, t2: Boolean -> Filter(t1, t2) }
|
||||
)
|
||||
.distinctUntilChanged()
|
||||
.switchMap { 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>()
|
||||
@@ -55,8 +55,10 @@ class FeedGroupDialogViewModel(
|
||||
.subscribe(mutableGroupLiveData::postValue)
|
||||
|
||||
private var subscriptionsDisposable = Flowable
|
||||
.combineLatest(subscriptionsFlowable, feedDatabaseManager.subscriptionIdsForGroup(groupId),
|
||||
BiFunction { t1: List<PickerSubscriptionItem>, t2: List<Long> -> t1 to t2.toSet() })
|
||||
.combineLatest(
|
||||
subscriptionsFlowable, feedDatabaseManager.subscriptionIdsForGroup(groupId),
|
||||
BiFunction { t1: List<PickerSubscriptionItem>, t2: List<Long> -> t1 to t2.toSet() }
|
||||
)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.subscribe(mutableSubscriptionsLiveData::postValue)
|
||||
|
||||
@@ -68,15 +70,19 @@ class FeedGroupDialogViewModel(
|
||||
}
|
||||
|
||||
fun createGroup(name: String, selectedIcon: FeedGroupIcon, selectedSubscriptions: Set<Long>) {
|
||||
doAction(feedDatabaseManager.createGroup(name, selectedIcon)
|
||||
.flatMapCompletable {
|
||||
feedDatabaseManager.updateSubscriptionsForGroup(it, selectedSubscriptions.toList())
|
||||
})
|
||||
doAction(
|
||||
feedDatabaseManager.createGroup(name, selectedIcon)
|
||||
.flatMapCompletable {
|
||||
feedDatabaseManager.updateSubscriptionsForGroup(it, selectedSubscriptions.toList())
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
fun updateGroup(name: String, selectedIcon: FeedGroupIcon, selectedSubscriptions: Set<Long>, sortOrder: Long) {
|
||||
doAction(feedDatabaseManager.updateSubscriptionsForGroup(groupId, selectedSubscriptions.toList())
|
||||
.andThen(feedDatabaseManager.updateGroup(FeedGroupEntity(groupId, name, selectedIcon, sortOrder))))
|
||||
doAction(
|
||||
feedDatabaseManager.updateSubscriptionsForGroup(groupId, selectedSubscriptions.toList())
|
||||
.andThen(feedDatabaseManager.updateGroup(FeedGroupEntity(groupId, name, selectedIcon, sortOrder)))
|
||||
)
|
||||
}
|
||||
|
||||
fun deleteGroup() {
|
||||
@@ -120,8 +126,10 @@ class FeedGroupDialogViewModel(
|
||||
) : ViewModelProvider.Factory {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
|
||||
return FeedGroupDialogViewModel(context.applicationContext,
|
||||
groupId, initialQuery, initialShowOnlyUngrouped) as T
|
||||
return FeedGroupDialogViewModel(
|
||||
context.applicationContext,
|
||||
groupId, initialQuery, initialShowOnlyUngrouped
|
||||
) as T
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,6 @@ import com.xwray.groupie.TouchCallback
|
||||
import com.xwray.groupie.kotlinandroidextensions.GroupieViewHolder
|
||||
import icepick.Icepick
|
||||
import icepick.State
|
||||
import java.util.Collections
|
||||
import kotlinx.android.synthetic.main.dialog_feed_group_reorder.confirm_button
|
||||
import kotlinx.android.synthetic.main.dialog_feed_group_reorder.feed_groups_list
|
||||
import org.schabi.newpipe.R
|
||||
@@ -25,6 +24,7 @@ import org.schabi.newpipe.local.subscription.dialog.FeedGroupReorderDialogViewMo
|
||||
import org.schabi.newpipe.local.subscription.dialog.FeedGroupReorderDialogViewModel.DialogEvent.SuccessEvent
|
||||
import org.schabi.newpipe.local.subscription.item.FeedGroupReorderItem
|
||||
import org.schabi.newpipe.util.ThemeHelper
|
||||
import java.util.Collections
|
||||
|
||||
class FeedGroupReorderDialog : DialogFragment() {
|
||||
private lateinit var viewModel: FeedGroupReorderDialogViewModel
|
||||
@@ -51,12 +51,15 @@ class FeedGroupReorderDialog : DialogFragment() {
|
||||
|
||||
viewModel = ViewModelProvider(this).get(FeedGroupReorderDialogViewModel::class.java)
|
||||
viewModel.groupsLiveData.observe(viewLifecycleOwner, Observer(::handleGroups))
|
||||
viewModel.dialogEventLiveData.observe(viewLifecycleOwner, Observer {
|
||||
when (it) {
|
||||
ProcessingEvent -> disableInput()
|
||||
SuccessEvent -> dismiss()
|
||||
viewModel.dialogEventLiveData.observe(
|
||||
viewLifecycleOwner,
|
||||
Observer {
|
||||
when (it) {
|
||||
ProcessingEvent -> disableInput()
|
||||
SuccessEvent -> dismiss()
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
feed_groups_list.layoutManager = LinearLayoutManager(requireContext())
|
||||
feed_groups_list.adapter = groupAdapter
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user