mirror of
https://github.com/TeamNewPipe/NewPipe.git
synced 2025-12-13 01:50:21 +00:00
Compare commits
675 Commits
v0.26.0
...
player-cla
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
31ade7cd30 | ||
|
|
4e1b0e0555 | ||
|
|
0aa71a58ed | ||
|
|
7585cc2e73 | ||
|
|
803fd52859 | ||
|
|
ab8a9ae11c | ||
|
|
83486402df | ||
|
|
a5813f256a | ||
|
|
0a885492b6 | ||
|
|
731efc2124 | ||
|
|
a8da9946d1 | ||
|
|
3d069cdf5b | ||
|
|
eccedc0ab0 | ||
|
|
7cecda5713 | ||
|
|
d9dccfa8af | ||
|
|
81b4e3f970 | ||
|
|
ef068e1eca | ||
|
|
8407b5aefd | ||
|
|
b6aa07545a | ||
|
|
1dcb1953ba | ||
|
|
862a8e8f26 | ||
|
|
88395fa852 | ||
|
|
8d679626f0 | ||
|
|
e7f3750f5e | ||
|
|
48e826e912 | ||
|
|
088cb8353e | ||
|
|
5ca544bc42 | ||
|
|
aa1b7f8584 | ||
|
|
ce16c6df5f | ||
|
|
1d94fd1582 | ||
|
|
c9542ad6fd | ||
|
|
7615f79aca | ||
|
|
276bf390b2 | ||
|
|
f39eda086f | ||
|
|
756327da39 | ||
|
|
5840d3a437 | ||
|
|
47299c9184 | ||
|
|
6486f2de56 | ||
|
|
e1dedd45ed | ||
|
|
912f07a1dd | ||
|
|
205466c56a | ||
|
|
7f10312d0a | ||
|
|
63be3220e7 | ||
|
|
536b78f2e6 | ||
|
|
6d6b73ef73 | ||
|
|
196c27792b | ||
|
|
b3789315ad | ||
|
|
c7bf498c04 | ||
|
|
35abb99dac | ||
|
|
70416e73f3 | ||
|
|
a0b76c3385 | ||
|
|
c232193a46 | ||
|
|
f289bea6b3 | ||
|
|
48b200868a | ||
|
|
54bf7f0ced | ||
|
|
980a35a708 | ||
|
|
da106e2361 | ||
|
|
3532ac96b4 | ||
|
|
87693a2ad1 | ||
|
|
d321e57620 | ||
|
|
fb4a65a14a | ||
|
|
3047704e1c | ||
|
|
3dcfdaf510 | ||
|
|
2ceb70236e | ||
|
|
be097f26c8 | ||
|
|
098f60d593 | ||
|
|
eb0568044a | ||
|
|
f3b3d5c3e7 | ||
|
|
b888dc72cf | ||
|
|
599d86151a | ||
|
|
587df093ea | ||
|
|
8830e87242 | ||
|
|
f96b8f7b2a | ||
|
|
c28478ae53 | ||
|
|
10110397fd | ||
|
|
d81244e77c | ||
|
|
ea20ca9e72 | ||
|
|
f0c89494dd | ||
|
|
0fd2d4fed6 | ||
|
|
c1bdffd917 | ||
|
|
3c7b026d7d | ||
|
|
998d84de6c | ||
|
|
76a02d5858 | ||
|
|
24bb71a23f | ||
|
|
49b71942ad | ||
|
|
c9ec257a5e | ||
|
|
b1f995a78c | ||
|
|
acac50a1d1 | ||
|
|
c6b87cd316 | ||
|
|
94d4c21cc7 | ||
|
|
a7a7dc5363 | ||
|
|
126f4b0e30 | ||
|
|
6558794d26 | ||
|
|
1d12874937 | ||
|
|
1d98518bfa | ||
|
|
e5458bcb14 | ||
|
|
dc62d211f5 | ||
|
|
ec6612dd71 | ||
|
|
064e1d39c7 | ||
|
|
4c88a193bd | ||
|
|
3fcac10e7f | ||
|
|
6cedd117fe | ||
|
|
5eabcb52b5 | ||
|
|
690b40d0c4 | ||
|
|
9bb2c0b484 | ||
|
|
1e08cc8c8f | ||
|
|
7d17468266 | ||
|
|
5819546ea9 | ||
|
|
cfb6e114d6 | ||
|
|
b764ad33c4 | ||
|
|
430b4eb916 | ||
|
|
2339f51ad4 | ||
|
|
99aae7eb28 | ||
|
|
c6e1721884 | ||
|
|
94684fe380 | ||
|
|
398a2f55ce | ||
|
|
1f7b3b5b06 | ||
|
|
909ed616c4 | ||
|
|
dd223af28d | ||
|
|
dbee8d8128 | ||
|
|
b62a09b5b3 | ||
|
|
87317c6faf | ||
|
|
53b599b042 | ||
|
|
21df24abfd | ||
|
|
ca4592a935 | ||
|
|
3fc487310b | ||
|
|
056809cb0d | ||
|
|
a60bb3e7af | ||
|
|
ecd3f6c2ee | ||
|
|
70ff47b810 | ||
|
|
b8e050f6c4 | ||
|
|
46d0bc1004 | ||
|
|
e7fe84f2c7 | ||
|
|
2b183a0576 | ||
|
|
f856bd9306 | ||
|
|
0066b322e1 | ||
|
|
3bdae81c0a | ||
|
|
6010c4ea7f | ||
|
|
690b3410e9 | ||
|
|
ba86ce137b | ||
|
|
410c01547c | ||
|
|
fd99c5e461 | ||
|
|
407d2d768d | ||
|
|
47263f5254 | ||
|
|
01bf855015 | ||
|
|
ebf3008729 | ||
|
|
33ecfb757e | ||
|
|
b109e4d3cc | ||
|
|
137ade24ff | ||
|
|
ffe26d882b | ||
|
|
83f8141fe7 | ||
|
|
b78e0b2da8 | ||
|
|
9253640fae | ||
|
|
3e6e980362 | ||
|
|
8b5aa5cd9b | ||
|
|
58393ad4ef | ||
|
|
977f7e28b5 | ||
|
|
99e77249de | ||
|
|
1890fbb19a | ||
|
|
a955408053 | ||
|
|
86203d6800 | ||
|
|
edd19641ac | ||
|
|
65749cbac0 | ||
|
|
658ddfc921 | ||
|
|
efb3aa530d | ||
|
|
f7d0fd545d | ||
|
|
27e6be792f | ||
|
|
ce919215fb | ||
|
|
6a4aaba431 | ||
|
|
83d93e16e7 | ||
|
|
8d15a141b1 | ||
|
|
a78bed700a | ||
|
|
ef3c76645f | ||
|
|
d4ed18bf08 | ||
|
|
3fc0147f47 | ||
|
|
fbafdeb2ca | ||
|
|
c6b05c6094 | ||
|
|
240a2fe36b | ||
|
|
de46e3abb3 | ||
|
|
70748fa0bc | ||
|
|
781040efaa | ||
|
|
3847b32c11 | ||
|
|
9054575f6c | ||
|
|
0dca92dd59 | ||
|
|
b19cd00dba | ||
|
|
88d8d90bbd | ||
|
|
c569f08a32 | ||
|
|
246fc034c1 | ||
|
|
1547b50b4e | ||
|
|
3f7ef49979 | ||
|
|
52942ffd30 | ||
|
|
e4b0245530 | ||
|
|
c6b8bcf0f4 | ||
|
|
dab0148a78 | ||
|
|
c0c08a4f63 | ||
|
|
e31a8ad7a2 | ||
|
|
b21981a9c7 | ||
|
|
aaf337421d | ||
|
|
79a0edacd7 | ||
|
|
d56eef6ece | ||
|
|
72f054a4fa | ||
|
|
172c3c92ac | ||
|
|
137ef3fee4 | ||
|
|
e49156fb11 | ||
|
|
de5d45849f | ||
|
|
a25034b898 | ||
|
|
ae9e82b2c1 | ||
|
|
a70b38a8e5 | ||
|
|
08f3dba42c | ||
|
|
f9711a3402 | ||
|
|
df941670a8 | ||
|
|
57e66b17c6 | ||
|
|
d298a12533 | ||
|
|
a79bc3db14 | ||
|
|
661e6155c1 | ||
|
|
12558172d1 | ||
|
|
dc3f55674f | ||
|
|
acf2e88cb3 | ||
|
|
726c12e934 | ||
|
|
0cff3a6ecd | ||
|
|
33b96d238a | ||
|
|
213f49f5c4 | ||
|
|
9b78e49e45 | ||
|
|
16c79c8219 | ||
|
|
e6eea8f851 | ||
|
|
4e55f1bee6 | ||
|
|
cff3834fde | ||
|
|
c8b01a06b0 | ||
|
|
414b1a8344 | ||
|
|
404d9f3fac | ||
|
|
55e4014036 | ||
|
|
1cd5563b27 | ||
|
|
1abced992b | ||
|
|
46b9243661 | ||
|
|
ad72b2cb31 | ||
|
|
8b79d0ee29 | ||
|
|
295f719b77 | ||
|
|
b584353f4d | ||
|
|
d73314b4dd | ||
|
|
9f4a33c7a8 | ||
|
|
3a9540b042 | ||
|
|
14081505cd | ||
|
|
ca855cbca0 | ||
|
|
6a98b1dac7 | ||
|
|
ebd4880188 | ||
|
|
ffcba175ff | ||
|
|
c7848e5e86 | ||
|
|
6d686b93cb | ||
|
|
2cc38f59d3 | ||
|
|
8bf24e6b14 | ||
|
|
10e7a5cf9c | ||
|
|
9f2f219613 | ||
|
|
841471bf85 | ||
|
|
06d25b0310 | ||
|
|
3c8d81a3c2 | ||
|
|
cf870add49 | ||
|
|
a962e6d633 | ||
|
|
970ef9357b | ||
|
|
4ba961fe7a | ||
|
|
e6c03bf4ac | ||
|
|
1f39523429 | ||
|
|
b43031fb99 | ||
|
|
986cd52da0 | ||
|
|
7d4a2836fc | ||
|
|
6ea715a18d | ||
|
|
a56debfce6 | ||
|
|
226b6de34f | ||
|
|
bcd4579187 | ||
|
|
6fe417abc6 | ||
|
|
a229ab68d5 | ||
|
|
544b30290d | ||
|
|
cb300724da | ||
|
|
0ac5a269ff | ||
|
|
13585ca0be | ||
|
|
62ab9bd740 | ||
|
|
fdf36cbad6 | ||
|
|
aea2b7c7f3 | ||
|
|
37d1c784fa | ||
|
|
cea149f852 | ||
|
|
a92a28517e | ||
|
|
800961c3d7 | ||
|
|
9d8a79b0bd | ||
|
|
0009613608 | ||
|
|
7c18d4dd01 | ||
|
|
fe1c538f9c | ||
|
|
ef56dea817 | ||
|
|
23b3835af0 | ||
|
|
412e1d602a | ||
|
|
802a094154 | ||
|
|
e6b1341246 | ||
|
|
36ede243e3 | ||
|
|
bac9f7eebf | ||
|
|
8ada566bf1 | ||
|
|
5bd4ed77df | ||
|
|
97652ac015 | ||
|
|
6dd24033a4 | ||
|
|
4de3ef20be | ||
|
|
702f74291d | ||
|
|
d8759993a9 | ||
|
|
7787eafd3a | ||
|
|
f08e07873a | ||
|
|
1193b02ca1 | ||
|
|
c0b36b86b9 | ||
|
|
66ec596f67 | ||
|
|
90404a23ce | ||
|
|
64ad05d813 | ||
|
|
734b6e2b67 | ||
|
|
94f992a2e2 | ||
|
|
c8550695aa | ||
|
|
cdac50bab3 | ||
|
|
23961548c0 | ||
|
|
ba1e9c8e1b | ||
|
|
f4baf4628e | ||
|
|
05a87da827 | ||
|
|
fef40014a0 | ||
|
|
1996c1176c | ||
|
|
0190bcee25 | ||
|
|
1ed4928f40 | ||
|
|
63bc982cb2 | ||
|
|
3a286515f2 | ||
|
|
2e96b65fda | ||
|
|
2482615460 | ||
|
|
9384365061 | ||
|
|
b1d4b66aa6 | ||
|
|
ea0da5fdbd | ||
|
|
d80b6a759c | ||
|
|
8106ba68b5 | ||
|
|
ee15a72e4f | ||
|
|
4f4136c6e9 | ||
|
|
b399030e19 | ||
|
|
2eb256799d | ||
|
|
0cf4732d8a | ||
|
|
53edd054aa | ||
|
|
678f0a786a | ||
|
|
b14f65804d | ||
|
|
781a69d60d | ||
|
|
eb9f300e60 | ||
|
|
063568b620 | ||
|
|
0991461d04 | ||
|
|
49bcf2c41b | ||
|
|
c00c6c460c | ||
|
|
4c4fe3f511 | ||
|
|
db485c3d77 | ||
|
|
c0388d948b | ||
|
|
43bbddcc26 | ||
|
|
6471b64ab6 | ||
|
|
b9fcf0dff8 | ||
|
|
3177ca6e8a | ||
|
|
5017f4f05a | ||
|
|
035c394cf6 | ||
|
|
823d4a041f | ||
|
|
62d4044d6c | ||
|
|
3785404618 | ||
|
|
c98ad62163 | ||
|
|
4cac111b66 | ||
|
|
941b8eb194 | ||
|
|
b1add13bfd | ||
|
|
5fffee2c7d | ||
|
|
f9340ae604 | ||
|
|
d3a6991fd4 | ||
|
|
b0bfd4a807 | ||
|
|
3641698379 | ||
|
|
2836191fb3 | ||
|
|
0df6c7fc2c | ||
|
|
b1ebd3ecd9 | ||
|
|
4758244cf5 | ||
|
|
294b9cf347 | ||
|
|
fad3120b00 | ||
|
|
6d05af484e | ||
|
|
38c823a042 | ||
|
|
e082bca5e0 | ||
|
|
f9dae9078e | ||
|
|
e955beeef1 | ||
|
|
eaac7f3f85 | ||
|
|
ea414f57d4 | ||
|
|
f984b26626 | ||
|
|
edab9a6a1f | ||
|
|
4740e3be86 | ||
|
|
e639b02fed | ||
|
|
ac1ca1412d | ||
|
|
d131d3399a | ||
|
|
1009dc4d4e | ||
|
|
42cb914616 | ||
|
|
e72da94eb1 | ||
|
|
c5d94a5b60 | ||
|
|
02c5f2607a | ||
|
|
369a46f8fe | ||
|
|
909d214002 | ||
|
|
5e7e14ee4d | ||
|
|
b092fe2c76 | ||
|
|
b9dd7078ad | ||
|
|
93310955f2 | ||
|
|
9c52e039ee | ||
|
|
be037e0756 | ||
|
|
5bfb0449cf | ||
|
|
0ec81c9e52 | ||
|
|
5841eaa6d7 | ||
|
|
e92ba8f5d1 | ||
|
|
1908e18dc4 | ||
|
|
e30d5e4305 | ||
|
|
11bb2495ba | ||
|
|
341cc37ce7 | ||
|
|
1620668966 | ||
|
|
56c80ce6dd | ||
|
|
8ce9a7e43c | ||
|
|
e05d97732e | ||
|
|
644a345b55 | ||
|
|
bda961a04c | ||
|
|
ba2efded76 | ||
|
|
b05b98ca61 | ||
|
|
7a7f81ac7f | ||
|
|
6e6c171dd7 | ||
|
|
8a41c8cf66 | ||
|
|
05271d95a9 | ||
|
|
9d04a73c85 | ||
|
|
d336f4cef2 | ||
|
|
51ee2f8d1e | ||
|
|
d442b45836 | ||
|
|
dbcb721dc2 | ||
|
|
64a8f6575b | ||
|
|
03a6b5c7b9 | ||
|
|
56b6241311 | ||
|
|
947ac2826a | ||
|
|
0e8303f13a | ||
|
|
4ec7532126 | ||
|
|
da83646303 | ||
|
|
72e9f7f9cf | ||
|
|
ad6b676c81 | ||
|
|
0f64158469 | ||
|
|
acc5be92ac | ||
|
|
0e0cee1bce | ||
|
|
6f71c000ad | ||
|
|
9f766ebf78 | ||
|
|
07c63f794e | ||
|
|
26dd86e967 | ||
|
|
5062d38b65 | ||
|
|
82b492c050 | ||
|
|
73e3a69aaf | ||
|
|
348a79f91d | ||
|
|
5e5e77f746 | ||
|
|
c4ada7ff6e | ||
|
|
39d0691c7e | ||
|
|
71361de8ee | ||
|
|
8aa2590fd3 | ||
|
|
e3b7bf467e | ||
|
|
f74402bc94 | ||
|
|
4d3b4a7b20 | ||
|
|
e6302cc868 | ||
|
|
844b4edf48 | ||
|
|
92a7f22d3c | ||
|
|
03167a1e9c | ||
|
|
1f309854bc | ||
|
|
2ac0d1f13a | ||
|
|
4eeea7b787 | ||
|
|
e64c01d2da | ||
|
|
0c7a91f852 | ||
|
|
a2d93b389c | ||
|
|
c795214abb | ||
|
|
71822a47a5 | ||
|
|
e1bf67c676 | ||
|
|
8583c48264 | ||
|
|
2a3d133bcf | ||
|
|
3e3d1fd265 | ||
|
|
8645618f1a | ||
|
|
e48ce5a103 | ||
|
|
c02ceda22f | ||
|
|
46139340fe | ||
|
|
d479f29e9b | ||
|
|
1af798b04b | ||
|
|
7204407690 | ||
|
|
e37336eef2 | ||
|
|
cf21b9feaf | ||
|
|
b74cab6642 | ||
|
|
8267d325ed | ||
|
|
879d7a24f0 | ||
|
|
9e4ac2eacb | ||
|
|
d9d6fff48f | ||
|
|
9828586762 | ||
|
|
8caaa6d297 | ||
|
|
83ca6b9468 | ||
|
|
24e65ef018 | ||
|
|
a69bbab732 | ||
|
|
a557ac3c7b | ||
|
|
d61b4b89ea | ||
|
|
b8daf16b92 | ||
|
|
caa3812e13 | ||
|
|
23a087c498 | ||
|
|
c3c39a7b24 | ||
|
|
00770fc634 | ||
|
|
5bf77160f7 | ||
|
|
d9da84c412 | ||
|
|
b3a6318672 | ||
|
|
67b41b970d | ||
|
|
3738e30949 | ||
|
|
0ba73b11c1 | ||
|
|
13baaa31cd | ||
|
|
f0db2aa43c | ||
|
|
f704721b59 | ||
|
|
7abf0f4886 | ||
|
|
c915b6e68b | ||
|
|
0b28c688c6 | ||
|
|
2756ef6d2f | ||
|
|
7da1d30010 | ||
|
|
8e192acb63 | ||
|
|
d8423499dc | ||
|
|
974167fcb8 | ||
|
|
6afdbd6fd3 | ||
|
|
d8668ed226 | ||
|
|
d75a6eaa41 | ||
|
|
235fb92638 | ||
|
|
ea18b4ea1f | ||
|
|
58f5ec0181 | ||
|
|
e42c9abdde | ||
|
|
5e7ad6ffd1 | ||
|
|
4c8238874e | ||
|
|
38d4887901 | ||
|
|
c9051d33c1 | ||
|
|
3cc0205def | ||
|
|
90979e2a81 | ||
|
|
e66e1b542c | ||
|
|
92e9c3e42e | ||
|
|
4591c09637 | ||
|
|
e1ce3fef1b | ||
|
|
3c0a200f7b | ||
|
|
bef5907ec3 | ||
|
|
f0beb662aa | ||
|
|
92402685f8 | ||
|
|
3703fed1a5 | ||
|
|
f4fb960c62 | ||
|
|
a3bbbf03b4 | ||
|
|
1d3a69a29f | ||
|
|
10c57b15da | ||
|
|
b85f7a6747 | ||
|
|
3f94e7b638 | ||
|
|
2af95cc1d4 | ||
|
|
cefdefdfd2 | ||
|
|
37f7fa7ef4 | ||
|
|
e687eb5631 | ||
|
|
88c3af7647 | ||
|
|
ddd6c8cbf1 | ||
|
|
81220f90d6 | ||
|
|
e0268a91ad | ||
|
|
29e4135aaa | ||
|
|
5d9adce40d | ||
|
|
d3afde8789 | ||
|
|
d8a5d5545d | ||
|
|
bed3516687 | ||
|
|
3a014d8d46 | ||
|
|
58ae7fbccb | ||
|
|
b06a9618d4 | ||
|
|
434c4a5cbc | ||
|
|
c34d30dc17 | ||
|
|
0d4c1bee3f | ||
|
|
34a25d0be3 | ||
|
|
3134f5e747 | ||
|
|
1732584e5e | ||
|
|
f50cafbac1 | ||
|
|
bc7c3f48ad | ||
|
|
b760419fd5 | ||
|
|
5cf3c58d0e | ||
|
|
206d1b6db4 | ||
|
|
2e318b8b03 | ||
|
|
5bdb6f18d6 | ||
|
|
2e53a99361 | ||
|
|
bec18e13d3 | ||
|
|
7edd471ec5 | ||
|
|
e6a4a3fa4f | ||
|
|
de2a139340 | ||
|
|
9d6ac67c46 | ||
|
|
6f7b905983 | ||
|
|
bcd4626008 | ||
|
|
27730a20d6 | ||
|
|
4aa0190175 | ||
|
|
6dd62335e9 | ||
|
|
32d2606a65 | ||
|
|
2051334bba | ||
|
|
575e809004 | ||
|
|
66e8e2a696 | ||
|
|
55373c95d9 | ||
|
|
04bdc1cc0b | ||
|
|
1d8850d1b2 | ||
|
|
f98548698a | ||
|
|
4b1824e8c1 | ||
|
|
17e88f1749 | ||
|
|
5edafca05a | ||
|
|
2c4c283099 | ||
|
|
9fb8125655 | ||
|
|
aab6580195 | ||
|
|
30f0db1d28 | ||
|
|
5a4dae2070 | ||
|
|
8345f348f6 | ||
|
|
9220e32463 | ||
|
|
845e72bf4a | ||
|
|
49429ff40a | ||
|
|
3df21ad25e | ||
|
|
d0f4600be4 | ||
|
|
0fa2e76c3e | ||
|
|
9ff1b5230f | ||
|
|
65eb631711 | ||
|
|
6c99557553 | ||
|
|
2b4357fa87 | ||
|
|
cda4b3faaa | ||
|
|
5d09a88335 | ||
|
|
edd4f6b9f3 | ||
|
|
1e7e2109d2 | ||
|
|
b31d3831e6 | ||
|
|
0f81a0504c | ||
|
|
4a7fda95ae | ||
|
|
ee3455e1e5 | ||
|
|
f9fc1cd817 | ||
|
|
76f1e588f7 | ||
|
|
f3b458c803 | ||
|
|
00566ed4d4 | ||
|
|
e4a07411b8 | ||
|
|
2c1bb2706f | ||
|
|
aa84d6fc8f | ||
|
|
d76e9b0bd8 | ||
|
|
b4016c91c1 | ||
|
|
5f32d001cc | ||
|
|
8c9287d0c8 | ||
|
|
3f37e27852 | ||
|
|
f41ab8b086 | ||
|
|
ad68f784ae | ||
|
|
4b6392df54 | ||
|
|
94ea329b50 | ||
|
|
591ed2e01f | ||
|
|
78cf9aaa7d | ||
|
|
f9494a294f | ||
|
|
0dd4553700 | ||
|
|
4f7b36cd70 | ||
|
|
5d350aec87 | ||
|
|
059db6fb31 | ||
|
|
4c709b2c4d | ||
|
|
8f4cd032b7 | ||
|
|
df2e0be08d | ||
|
|
ff1aca272e | ||
|
|
f2e352832a | ||
|
|
ad0855ac83 | ||
|
|
d7ef9b1f0c | ||
|
|
40a3e1b18a | ||
|
|
25a73090f5 | ||
|
|
a239a26b17 | ||
|
|
06d256294f | ||
|
|
81ad50e82a | ||
|
|
23de9bf93e | ||
|
|
5c46412faa | ||
|
|
076e9eee01 | ||
|
|
2103a04092 | ||
|
|
58517d1d27 | ||
|
|
aa1847189b | ||
|
|
5d101e7b88 | ||
|
|
9118ecd68f | ||
|
|
15fd47c7f2 | ||
|
|
ef40ac7bb3 | ||
|
|
881d04ba1e | ||
|
|
4af5b5f6f2 | ||
|
|
90f0809029 | ||
|
|
8ad7bf60d7 | ||
|
|
898a936064 | ||
|
|
4e401bc059 | ||
|
|
9ecef6f011 | ||
|
|
ba394a7ab4 | ||
|
|
d32490a4be | ||
|
|
6526ff1612 | ||
|
|
bb5390d63a | ||
|
|
bd1aae8d66 | ||
|
|
c24aed054f | ||
|
|
0aa08a5e40 | ||
|
|
3c48825699 | ||
|
|
bfb56b4144 | ||
|
|
ba8370bcfd | ||
|
|
813f55152a | ||
|
|
270a541a7c | ||
|
|
c34549a47d | ||
|
|
96d6b309ec |
12
.github/CONTRIBUTING.md
vendored
12
.github/CONTRIBUTING.md
vendored
@@ -6,7 +6,7 @@ NewPipe contribution guidelines
|
|||||||
## Crash reporting
|
## Crash reporting
|
||||||
|
|
||||||
Report crashes through the **automated crash report system** of NewPipe.
|
Report crashes through the **automated crash report system** of NewPipe.
|
||||||
This way all the data needed for debugging is included in your bugreport for GitHub.
|
This way all the data needed for debugging is included in your bug report for GitHub.
|
||||||
You'll see *exactly* what is sent, be able to add **your comments**, and then send it.
|
You'll see *exactly* what is sent, be able to add **your comments**, and then send it.
|
||||||
|
|
||||||
## Issue reporting/feature requests
|
## Issue reporting/feature requests
|
||||||
@@ -42,10 +42,6 @@ You'll see *exactly* what is sent, be able to add **your comments**, and then se
|
|||||||
* Create PRs that cover only **one specific issue/solution/bug**. Do not create PRs that are huge monoliths and could have been split into multiple independent contributions.
|
* Create PRs that cover only **one specific issue/solution/bug**. Do not create PRs that are huge monoliths and could have been split into multiple independent contributions.
|
||||||
* NewPipe uses [NewPipeExtractor](https://github.com/TeamNewPipe/NewPipeExtractor) to fetch data from services. If you need to change something there, you must test your changes in NewPipe. Telling NewPipe to use your extractor version can be accomplished by editing the `app/build.gradle` file: the comments under the "NewPipe libraries" section of `dependencies` will help you out.
|
* NewPipe uses [NewPipeExtractor](https://github.com/TeamNewPipe/NewPipeExtractor) to fetch data from services. If you need to change something there, you must test your changes in NewPipe. Telling NewPipe to use your extractor version can be accomplished by editing the `app/build.gradle` file: the comments under the "NewPipe libraries" section of `dependencies` will help you out.
|
||||||
|
|
||||||
### Kotlin in NewPipe
|
|
||||||
* NewPipe will remain mostly Java for time being
|
|
||||||
* Contributions containing a simple conversion from Java to Kotlin should be avoided. Conversions to Kotlin should only be done if Kotlin actually brings improvements like bug fixes or better performance which are not, or only with much more effort, implementable in Java. The core team sees Java as an easier to learn and generally well adopted programming language.
|
|
||||||
|
|
||||||
### Creating a Pull Request (PR)
|
### Creating a Pull Request (PR)
|
||||||
|
|
||||||
* Make changes on a **separate branch** with a meaningful name, not on the _master_ branch or the _dev_ branch. This is commonly known as *feature branch workflow*. You may then send your changes as a pull request (PR) on GitHub.
|
* Make changes on a **separate branch** with a meaningful name, not on the _master_ branch or the _dev_ branch. This is commonly known as *feature branch workflow*. You may then send your changes as a pull request (PR) on GitHub.
|
||||||
@@ -83,6 +79,6 @@ The [ktlint](https://github.com/pinterest/ktlint) plugin does the same job as ch
|
|||||||
|
|
||||||
## Communication
|
## Communication
|
||||||
|
|
||||||
* The #newpipe channel on Libera Chat (`ircs://irc.libera.chat:6697/newpipe`) has the core team and other developers in it. [Click here for webchat](https://web.libera.chat/#newpipe)!
|
* You can use a Matrix account to join the NewPipe channel at [#newpipe:matrix.newpipe-ev.de](https://matrix.to/#/#newpipe:matrix.newpipe-ev.de). Some convenient clients, available both for phone and desktop, are listed at that link.
|
||||||
* You can also use a Matrix account to join the NewPipe channel at [#newpipe:libera.chat](https://matrix.to/#/#newpipe:libera.chat). Some convenient clients, available both for phone and desktop, are listed at that link.
|
* Alternatively, the #newpipe channel on Libera Chat (`ircs://irc.libera.chat:6697/newpipe`) can also be joined, as it is bridged to the Matrix room. [Click here for webchat](https://web.libera.chat/#newpipe)!
|
||||||
* You can post your suggestions, changes, ideas etc. on either GitHub or IRC.
|
* You can post your suggestions, changes, ideas etc. on either GitHub or Matrix (including via IRC).
|
||||||
|
|||||||
3
.github/DISCUSSION_TEMPLATE/questions.yml
vendored
3
.github/DISCUSSION_TEMPLATE/questions.yml
vendored
@@ -1,6 +1,3 @@
|
|||||||
name: Question
|
|
||||||
description: Ask about anything NewPipe-related
|
|
||||||
labels: [question]
|
|
||||||
body:
|
body:
|
||||||
- type: markdown
|
- type: markdown
|
||||||
attributes:
|
attributes:
|
||||||
|
|||||||
6
.github/ISSUE_TEMPLATE/config.yml
vendored
6
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -3,9 +3,9 @@ contact_links:
|
|||||||
- name: ❓ Question
|
- name: ❓ Question
|
||||||
url: https://github.com/TeamNewPipe/NewPipe/discussions/new?category=questions
|
url: https://github.com/TeamNewPipe/NewPipe/discussions/new?category=questions
|
||||||
about: Ask about anything NewPipe-related
|
about: Ask about anything NewPipe-related
|
||||||
|
- name: 💬 Matrix
|
||||||
|
url: https://matrix.to/#/#newpipe:matrix.newpipe-ev.de
|
||||||
|
about: Chat with us via Matrix for quick Q/A
|
||||||
- name: 💬 IRC
|
- name: 💬 IRC
|
||||||
url: https://web.libera.chat/#newpipe
|
url: https://web.libera.chat/#newpipe
|
||||||
about: Chat with us via IRC for quick Q/A
|
about: Chat with us via IRC for quick Q/A
|
||||||
- name: 💬 Matrix
|
|
||||||
url: https://matrix.to/#/#newpipe:libera.chat
|
|
||||||
about: Chat with us via Matrix for quick Q/A
|
|
||||||
|
|||||||
38
.github/workflows/build-release-apk.yml
vendored
Normal file
38
.github/workflows/build-release-apk.yml
vendored
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
name: "Build unsigned release APK on master"
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
release:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
ref: 'master'
|
||||||
|
|
||||||
|
- uses: actions/setup-java@v4
|
||||||
|
with:
|
||||||
|
distribution: 'temurin'
|
||||||
|
java-version: '21'
|
||||||
|
cache: 'gradle'
|
||||||
|
|
||||||
|
- name: "Build release APK"
|
||||||
|
run: ./gradlew assembleRelease --stacktrace
|
||||||
|
|
||||||
|
- name: "Rename APK"
|
||||||
|
run: |
|
||||||
|
VERSION_NAME="$(jq -r ".elements[0].versionName" "app/build/outputs/apk/release/output-metadata.json")"
|
||||||
|
echo "Version name: $VERSION_NAME" >> "$GITHUB_STEP_SUMMARY"
|
||||||
|
echo '```json' >> "$GITHUB_STEP_SUMMARY"
|
||||||
|
cat "app/build/outputs/apk/release/output-metadata.json" >> "$GITHUB_STEP_SUMMARY"
|
||||||
|
echo >> "$GITHUB_STEP_SUMMARY"
|
||||||
|
echo '```' >> "$GITHUB_STEP_SUMMARY"
|
||||||
|
# assume there is only one APK in that folder
|
||||||
|
mv app/build/outputs/apk/release/*.apk "app/build/outputs/apk/release/NewPipe_v$VERSION_NAME.apk"
|
||||||
|
|
||||||
|
- name: "Upload APK"
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: app
|
||||||
|
path: app/build/outputs/apk/release/*.apk
|
||||||
42
.github/workflows/ci.yml
vendored
42
.github/workflows/ci.yml
vendored
@@ -6,6 +6,7 @@ on:
|
|||||||
branches:
|
branches:
|
||||||
- dev
|
- dev
|
||||||
- master
|
- master
|
||||||
|
- refactor
|
||||||
- release**
|
- release**
|
||||||
paths-ignore:
|
paths-ignore:
|
||||||
- 'README.md'
|
- 'README.md'
|
||||||
@@ -36,8 +37,8 @@ jobs:
|
|||||||
contents: read
|
contents: read
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
- uses: gradle/wrapper-validation-action@v1
|
- uses: gradle/wrapper-validation-action@v2
|
||||||
|
|
||||||
- name: create and checkout branch
|
- name: create and checkout branch
|
||||||
# push events already checked out the branch
|
# push events already checked out the branch
|
||||||
@@ -46,10 +47,10 @@ jobs:
|
|||||||
BRANCH: ${{ github.head_ref }}
|
BRANCH: ${{ github.head_ref }}
|
||||||
run: git checkout -B "$BRANCH"
|
run: git checkout -B "$BRANCH"
|
||||||
|
|
||||||
- name: set up JDK 17
|
- name: set up JDK
|
||||||
uses: actions/setup-java@v3
|
uses: actions/setup-java@v4
|
||||||
with:
|
with:
|
||||||
java-version: 17
|
java-version: 21
|
||||||
distribution: "temurin"
|
distribution: "temurin"
|
||||||
cache: 'gradle'
|
cache: 'gradle'
|
||||||
|
|
||||||
@@ -57,14 +58,13 @@ jobs:
|
|||||||
run: ./gradlew assembleDebug lintDebug testDebugUnitTest --stacktrace -DskipFormatKtlint
|
run: ./gradlew assembleDebug lintDebug testDebugUnitTest --stacktrace -DskipFormatKtlint
|
||||||
|
|
||||||
- name: Upload APK
|
- name: Upload APK
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: app
|
name: app
|
||||||
path: app/build/outputs/apk/debug/*.apk
|
path: app/build/outputs/apk/debug/*.apk
|
||||||
|
|
||||||
test-android:
|
test-android:
|
||||||
# macos has hardware acceleration. See android-emulator-runner action
|
runs-on: ubuntu-latest
|
||||||
runs-on: macos-latest
|
|
||||||
timeout-minutes: 20
|
timeout-minutes: 20
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
@@ -80,12 +80,18 @@ jobs:
|
|||||||
contents: read
|
contents: read
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: set up JDK 17
|
- name: Enable KVM
|
||||||
uses: actions/setup-java@v3
|
run: |
|
||||||
|
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
|
||||||
|
sudo udevadm control --reload-rules
|
||||||
|
sudo udevadm trigger --name-match=kvm
|
||||||
|
|
||||||
|
- name: set up JDK
|
||||||
|
uses: actions/setup-java@v4
|
||||||
with:
|
with:
|
||||||
java-version: 17
|
java-version: 21
|
||||||
distribution: "temurin"
|
distribution: "temurin"
|
||||||
cache: 'gradle'
|
cache: 'gradle'
|
||||||
|
|
||||||
@@ -98,7 +104,7 @@ jobs:
|
|||||||
script: ./gradlew connectedCheck --stacktrace
|
script: ./gradlew connectedCheck --stacktrace
|
||||||
|
|
||||||
- name: Upload test report when tests fail # because the printed out stacktrace (console) is too short, see also #7553
|
- name: Upload test report when tests fail # because the printed out stacktrace (console) is too short, see also #7553
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v4
|
||||||
if: failure()
|
if: failure()
|
||||||
with:
|
with:
|
||||||
name: android-test-report-api${{ matrix.api-level }}
|
name: android-test-report-api${{ matrix.api-level }}
|
||||||
@@ -111,19 +117,19 @@ jobs:
|
|||||||
contents: read
|
contents: read
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis
|
fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis
|
||||||
|
|
||||||
- name: Set up JDK 17
|
- name: Set up JDK
|
||||||
uses: actions/setup-java@v3
|
uses: actions/setup-java@v4
|
||||||
with:
|
with:
|
||||||
java-version: 17
|
java-version: 21
|
||||||
distribution: "temurin"
|
distribution: "temurin"
|
||||||
cache: 'gradle'
|
cache: 'gradle'
|
||||||
|
|
||||||
- name: Cache SonarCloud packages
|
- name: Cache SonarCloud packages
|
||||||
uses: actions/cache@v3
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
path: ~/.sonar/cache
|
path: ~/.sonar/cache
|
||||||
key: ${{ runner.os }}-sonar
|
key: ${{ runner.os }}-sonar
|
||||||
|
|||||||
8
.github/workflows/image-minimizer.js
vendored
8
.github/workflows/image-minimizer.js
vendored
@@ -32,12 +32,12 @@ module.exports = async ({github, context}) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Regex for finding images (simple variant) 
|
// Regex for finding images (simple variant) 
|
||||||
const REGEX_USER_CONTENT_IMAGE_LOOKUP = /\!\[(.*)\]\((https:\/\/[-a-z0-9]+\.githubusercontent\.com\/\d+\/[-0-9a-f]{32,512}\.(jpg|gif|png))\)/gm;
|
const REGEX_USER_CONTENT_IMAGE_LOOKUP = /\!\[([^\]]*)\]\((https:\/\/[-a-z0-9]+\.githubusercontent\.com\/\d+\/[-0-9a-f]{32,512}\.(jpg|gif|png))\)/gm;
|
||||||
const REGEX_ASSETS_IMAGE_LOCKUP = /\!\[(.*)\]\((https:\/\/github\.com\/[-\w\d]+\/[-\w\d]+\/assets\/\d+\/[\-0-9a-f]{32,512})\)/gm;
|
const REGEX_ASSETS_IMAGE_LOOKUP = /\!\[([^\]]*)\]\((https:\/\/github\.com\/(?:user-attachments\/assets|[-\w\d]+\/[-\w\d]+\/assets\/\d+)\/[\-0-9a-f]{32,512})\)/gm;
|
||||||
|
|
||||||
// Check if we found something
|
// Check if we found something
|
||||||
let foundSimpleImages = REGEX_USER_CONTENT_IMAGE_LOOKUP.test(initialBody)
|
let foundSimpleImages = REGEX_USER_CONTENT_IMAGE_LOOKUP.test(initialBody)
|
||||||
|| REGEX_ASSETS_IMAGE_LOCKUP.test(initialBody);
|
|| REGEX_ASSETS_IMAGE_LOOKUP.test(initialBody);
|
||||||
if (!foundSimpleImages) {
|
if (!foundSimpleImages) {
|
||||||
console.log('Found no simple images to process');
|
console.log('Found no simple images to process');
|
||||||
return;
|
return;
|
||||||
@@ -52,7 +52,7 @@ module.exports = async ({github, context}) => {
|
|||||||
|
|
||||||
// Try to find and replace the images with minimized ones
|
// Try to find and replace the images with minimized ones
|
||||||
let newBody = await replaceAsync(initialBody, REGEX_USER_CONTENT_IMAGE_LOOKUP, minimizeAsync);
|
let newBody = await replaceAsync(initialBody, REGEX_USER_CONTENT_IMAGE_LOOKUP, minimizeAsync);
|
||||||
newBody = await replaceAsync(newBody, REGEX_ASSETS_IMAGE_LOCKUP, minimizeAsync);
|
newBody = await replaceAsync(newBody, REGEX_ASSETS_IMAGE_LOOKUP, minimizeAsync);
|
||||||
|
|
||||||
if (!wasMatchModified) {
|
if (!wasMatchModified) {
|
||||||
console.log('Nothing was modified. Skipping update');
|
console.log('Nothing was modified. Skipping update');
|
||||||
|
|||||||
6
.github/workflows/image-minimizer.yml
vendored
6
.github/workflows/image-minimizer.yml
vendored
@@ -17,9 +17,9 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- uses: actions/setup-node@v3
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 16
|
node-version: 16
|
||||||
|
|
||||||
@@ -27,7 +27,7 @@ jobs:
|
|||||||
run: npm i probe-image-size@7.2.3 --ignore-scripts
|
run: npm i probe-image-size@7.2.3 --ignore-scripts
|
||||||
|
|
||||||
- name: Minimize simple images
|
- name: Minimize simple images
|
||||||
uses: actions/github-script@v6
|
uses: actions/github-script@v7
|
||||||
timeout-minutes: 3
|
timeout-minutes: 3
|
||||||
with:
|
with:
|
||||||
script: |
|
script: |
|
||||||
|
|||||||
2
.github/workflows/pr-labeler.yml
vendored
2
.github/workflows/pr-labeler.yml
vendored
@@ -1,5 +1,5 @@
|
|||||||
name: "PR size labeler"
|
name: "PR size labeler"
|
||||||
on: [pull_request]
|
on: [pull_request_target]
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
pull-requests: write
|
pull-requests: write
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -10,6 +10,7 @@ captures/
|
|||||||
*.class
|
*.class
|
||||||
app/debug/
|
app/debug/
|
||||||
app/release/
|
app/release/
|
||||||
|
.kotlin/
|
||||||
|
|
||||||
# vscode / eclipse files
|
# vscode / eclipse files
|
||||||
*.classpath
|
*.classpath
|
||||||
|
|||||||
21
.idea/icon.svg
generated
Normal file
21
.idea/icon.svg
generated
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||||
|
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" x="0px" y="0px"
|
||||||
|
viewBox="0 0 100 100" style="enable-background:new 0 0 100 100;" xml:space="preserve">
|
||||||
|
<style type="text/css">
|
||||||
|
.st0{fill:#CD201F;}
|
||||||
|
.st1{fill:#FFFFFF;}
|
||||||
|
</style>
|
||||||
|
<g id="Alapkör">
|
||||||
|
<circle id="XMLID_23_" class="st0" cx="50" cy="50" r="50"/>
|
||||||
|
</g>
|
||||||
|
<g id="Elemek">
|
||||||
|
<path id="XMLID_19_" class="st1" d="M47,28.2c-9-5.3-15.3-9-15.3-9v61.7c0,0,30.4-18,52.3-30.9C72.1,43,57.7,34.5,47,28.2z"/>
|
||||||
|
</g>
|
||||||
|
<g id="Fedő">
|
||||||
|
<path id="XMLID_5_" class="st0" d="M48.4,40.1c-4.1-2.4-7-4.1-7-4.1V64c0,0,13.9-8.2,23.8-14C59.8,46.8,53.3,42.9,48.4,40.1z"/>
|
||||||
|
<rect id="XMLID_4_" x="41.4" y="55.6" class="st0" width="6.2" height="21"/>
|
||||||
|
</g>
|
||||||
|
<g id="Vonalak">
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 850 B |
27
README.md
27
README.md
@@ -13,18 +13,19 @@
|
|||||||
<a href="https://github.com/TeamNewPipe/NewPipe/actions" alt="Build Status"><img src="https://github.com/TeamNewPipe/NewPipe/workflows/CI/badge.svg?branch=dev&event=push"></a>
|
<a href="https://github.com/TeamNewPipe/NewPipe/actions" alt="Build Status"><img src="https://github.com/TeamNewPipe/NewPipe/workflows/CI/badge.svg?branch=dev&event=push"></a>
|
||||||
<a href="https://hosted.weblate.org/engage/newpipe/" alt="Translation Status"><img src="https://hosted.weblate.org/widgets/newpipe/-/svg-badge.svg"></a>
|
<a href="https://hosted.weblate.org/engage/newpipe/" alt="Translation Status"><img src="https://hosted.weblate.org/widgets/newpipe/-/svg-badge.svg"></a>
|
||||||
<a href="https://web.libera.chat/#newpipe" alt="IRC channel: #newpipe"><img src="https://img.shields.io/badge/IRC%20chat-%23newpipe-brightgreen.svg"></a>
|
<a href="https://web.libera.chat/#newpipe" alt="IRC channel: #newpipe"><img src="https://img.shields.io/badge/IRC%20chat-%23newpipe-brightgreen.svg"></a>
|
||||||
<a href="https://matrix.to/#/#newpipe:libera.chat" alt="Matrix channel: #newpipe"><img src="https://img.shields.io/badge/Matrix%20chat-%23newpipe-blue"></a>
|
<a href="https://matrix.to/#/#newpipe:matrix.newpipe-ev.de" alt="Matrix channel: #newpipe"><img src="https://img.shields.io/badge/Matrix%20chat-%23newpipe-blue"></a>
|
||||||
</p>
|
</p>
|
||||||
<hr>
|
<hr>
|
||||||
<p align="center"><a href="#screenshots">Screenshots</a> • <a href="#supported-services">Supported Services</a> • <a href="#description">Description</a> • <a href="#features">Features</a> • <a href="#installation-and-updates">Installation and updates</a> • <a href="#contribution">Contribution</a> • <a href="#donate">Donate</a> • <a href="#license">License</a></p>
|
<p align="center"><a href="#screenshots">Screenshots</a> • <a href="#supported-services">Supported Services</a> • <a href="#description">Description</a> • <a href="#features">Features</a> • <a href="#installation-and-updates">Installation and 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.net">Website</a> • <a href="https://newpipe.net/blog/">Blog</a> • <a href="https://newpipe.net/FAQ/">FAQ</a> • <a href="https://newpipe.net/press/">Press</a></p>
|
<p align="center"><a href="https://newpipe.net">Website</a> • <a href="https://newpipe.net/blog/">Blog</a> • <a href="https://newpipe.net/FAQ/">FAQ</a> • <a href="https://newpipe.net/press/">Press</a></p>
|
||||||
<hr>
|
<hr>
|
||||||
|
|
||||||
*Read this document in other languages: [Deutsch](doc/README.de.md), [English](README.md), [Español](doc/README.es.md), [Français](doc/README.fr.md), [हिन्दी](doc/README.hi.md), [Italiano](doc/README.it.md), [한국어](doc/README.ko.md), [Português Brasil](doc/README.pt_BR.md), [Polski](doc/README.pl.md), [ਪੰਜਾਬੀ ](doc/README.pa.md), [日本語](doc/README.ja.md), [Română](doc/README.ro.md), [Soomaali](doc/README.so.md), [Türkçe](doc/README.tr.md), [正體中文](doc/README.zh_TW.md), [অসমীয়া](doc/README.asm.md), [Српски](doc/README.sr.md)*
|
*Read this document in other languages: [Deutsch](doc/README.de.md), [English](README.md), [Español](doc/README.es.md), [Français](doc/README.fr.md), [हिन्दी](doc/README.hi.md), [Italiano](doc/README.it.md), [한국어](doc/README.ko.md), [Português Brasil](doc/README.pt_BR.md), [Polski](doc/README.pl.md), [ਪੰਜਾਬੀ ](doc/README.pa.md), [日本語](doc/README.ja.md), [Română](doc/README.ro.md), [Soomaali](doc/README.so.md), [Türkçe](doc/README.tr.md), [正體中文](doc/README.zh_TW.md), [অসমীয়া](doc/README.asm.md), [Српски](doc/README.sr.md), [العربية](README.ar.md)*
|
||||||
|
|
||||||
<b>WARNING: THIS APP IS IN BETA, SO YOU MAY ENCOUNTER BUGS. IF YOU DO, OPEN AN ISSUE IN OUR GITHUB REPOSITORY BY FILLING OUT THE ISSUE TEMPLATE.</b>
|
> [!warning]
|
||||||
|
> <b>THIS APP IS IN BETA, SO YOU MAY ENCOUNTER BUGS. IF YOU DO, OPEN AN ISSUE IN OUR GITHUB REPOSITORY BY FILLING OUT THE ISSUE TEMPLATE.</b>
|
||||||
<b>PUTTING NEWPIPE, OR ANY FORK OF IT, INTO THE GOOGLE PLAY STORE VIOLATES THEIR TERMS AND CONDITIONS.</b>
|
>
|
||||||
|
> <b>PUTTING NEWPIPE, OR ANY FORK OF IT, INTO THE GOOGLE PLAY STORE VIOLATES THEIR TERMS AND CONDITIONS.</b>
|
||||||
|
|
||||||
## Screenshots
|
## Screenshots
|
||||||
|
|
||||||
@@ -95,7 +96,7 @@ Also, since they are free and open source software, neither the app nor the Extr
|
|||||||
## Installation and updates
|
## Installation and updates
|
||||||
You can install NewPipe using one of the following methods:
|
You can install NewPipe using one of the following methods:
|
||||||
1. Add our custom repo to F-Droid and install it from there. The instructions are here: https://newpipe.net/FAQ/tutorials/install-add-fdroid-repo/
|
1. Add our custom repo to F-Droid and install it from there. The instructions are here: https://newpipe.net/FAQ/tutorials/install-add-fdroid-repo/
|
||||||
2. Download the APK from [GitHub Releases](https://github.com/TeamNewPipe/NewPipe/releases) and install it.
|
2. Download the APK from [GitHub Releases](https://github.com/TeamNewPipe/NewPipe/releases), [compare the signing key](#apk-info) and install it.
|
||||||
3. Update via F-Droid. This is the slowest method of getting updates, as F-Droid must recognize changes, build the APK itself, sign it, and then push the update to users.
|
3. Update via F-Droid. This is the slowest method of getting updates, as F-Droid must recognize changes, build the APK itself, sign it, and then push the update to users.
|
||||||
4. Build a debug APK yourself. This is the fastest way to get new features on your device, but is much more complicated, so we recommend using one of the other methods.
|
4. Build a debug APK yourself. This is the fastest way to get new features on your device, but is much more complicated, so we recommend using one of the other methods.
|
||||||
5. If you're interested in a specific feature or bugfix provided in a Pull Request in this repo, you can also download its APK from within the PR. Read the PR description for instructions. The great thing about PR-specific APKs is that they're installed side-by-side the official app, so you don't have to worry about losing your data or messing anything up.
|
5. If you're interested in a specific feature or bugfix provided in a Pull Request in this repo, you can also download its APK from within the PR. Read the PR description for instructions. The great thing about PR-specific APKs is that they're installed side-by-side the official app, so you don't have to worry about losing your data or messing anything up.
|
||||||
@@ -103,12 +104,20 @@ You can install NewPipe using one of the following methods:
|
|||||||
We recommend method 1 for most users. APKs installed using method 1 or 2 are compatible with each other (meaning that if you installed NewPipe using either method 1 or 2, you can also update NewPipe using the other), but not with those installed using method 3. This is due to the same signing key (ours) being used for 1 and 2, but a different signing key (F-Droid's) being used for 3. Building a debug APK using method 4 excludes a key entirely. Signing keys help ensure that a user isn't tricked into installing a malicious update to an app. When using method 5, each APK is signed with a different random key supplied by GitHub Actions, so you cannot even update it. You will have to backup and restore the app data each time you wish to use a new APK.
|
We recommend method 1 for most users. APKs installed using method 1 or 2 are compatible with each other (meaning that if you installed NewPipe using either method 1 or 2, you can also update NewPipe using the other), but not with those installed using method 3. This is due to the same signing key (ours) being used for 1 and 2, but a different signing key (F-Droid's) being used for 3. Building a debug APK using method 4 excludes a key entirely. Signing keys help ensure that a user isn't tricked into installing a malicious update to an app. When using method 5, each APK is signed with a different random key supplied by GitHub Actions, so you cannot even update it. You will have to backup and restore the app data each time you wish to use a new APK.
|
||||||
|
|
||||||
In the meanwhile, if you want to switch sources for some reason (e.g. NewPipe's core functionality breaks and F-Droid doesn't have the latest update yet), we recommend following this procedure:
|
In the meanwhile, if you want to switch sources for some reason (e.g. NewPipe's core functionality breaks and F-Droid doesn't have the latest update yet), we recommend following this procedure:
|
||||||
1. Back up your data via Settings > Content > Export Database so you keep your history, subscriptions, and playlists
|
1. Back up your data via Settings > Backup and Restore > Export Database so you keep your history, subscriptions, and playlists
|
||||||
2. Uninstall NewPipe
|
2. Uninstall NewPipe
|
||||||
3. Download the APK from the new source and install it
|
3. Download the APK from the new source and install it
|
||||||
4. Import the data from step 1 via Settings > Content > Import Database
|
4. Import the data from step 1 via Settings > Backup and Restore > Import Database
|
||||||
|
|
||||||
<b>Note: when you're importing a database into the official app, always make sure that it is the one you exported _from_ the official app. If you import a database exported from an APK other than the official app, it may break things. Such an action is unsupported, and you should only do so when you're absolutely certain you know what you're doing.</b>
|
> [!Note]
|
||||||
|
> When you're importing a database into the official app, always make sure that it is the one you exported _from_ the official app. If you import a database exported from an APK other than the official app, it may break things. Such an action is unsupported, and you should only do so when you're absolutely certain you know what you're doing.
|
||||||
|
|
||||||
|
### APK Info
|
||||||
|
|
||||||
|
This is the SHA fingerprint of NewPipe's signing key to verify downloaded APKs which are signed by us. The fingerprint is also available on [NewPipe's website](https://newpipe.net#download). This is relevant for method 2.
|
||||||
|
```
|
||||||
|
CB:84:06:9B:D6:81:16:BA:FA:E5:EE:4E:E5:B0:8A:56:7A:A6:D8:98:40:4E:7C:B1:2F:9E:75:6D:F5:CF:5C:AB
|
||||||
|
```
|
||||||
|
|
||||||
## Contribution
|
## Contribution
|
||||||
Whether you have ideas, translations, design changes, code cleaning, or even major code changes, help is always welcome. The app gets better and better with each contribution, no matter how big or small! If you'd like to get involved, check our [contribution notes](.github/CONTRIBUTING.md).
|
Whether you have ideas, translations, design changes, code cleaning, or even major code changes, help is always welcome. The app gets better and better with each contribution, no matter how big or small! If you'd like to get involved, check our [contribution notes](.github/CONTRIBUTING.md).
|
||||||
|
|||||||
249
app/build.gradle
249
app/build.gradle
@@ -1,18 +1,22 @@
|
|||||||
import com.android.tools.profgen.ArtProfileKt
|
import com.android.tools.profgen.ArtProfileKt
|
||||||
import com.android.tools.profgen.ArtProfileSerializer
|
import com.android.tools.profgen.ArtProfileSerializer
|
||||||
import com.android.tools.profgen.DexFile
|
import com.android.tools.profgen.DexFile
|
||||||
|
import com.mikepenz.aboutlibraries.plugin.DuplicateMode
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
id "com.android.application"
|
alias libs.plugins.android.application
|
||||||
id "kotlin-android"
|
alias libs.plugins.kotlin.android
|
||||||
id "kotlin-kapt"
|
alias libs.plugins.kotlin.compose
|
||||||
id "kotlin-parcelize"
|
alias libs.plugins.kotlin.kapt
|
||||||
id "checkstyle"
|
alias libs.plugins.kotlin.parcelize
|
||||||
id "org.sonarqube" version "4.0.0.2929"
|
alias libs.plugins.checkstyle
|
||||||
|
alias libs.plugins.sonarqube
|
||||||
|
alias libs.plugins.hilt
|
||||||
|
alias libs.plugins.aboutlibraries
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdk 33
|
compileSdk 34
|
||||||
namespace 'org.schabi.newpipe'
|
namespace 'org.schabi.newpipe'
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
@@ -20,8 +24,15 @@ android {
|
|||||||
resValue "string", "app_name", "NewPipe"
|
resValue "string", "app_name", "NewPipe"
|
||||||
minSdk 21
|
minSdk 21
|
||||||
targetSdk 33
|
targetSdk 33
|
||||||
versionCode 995
|
if (System.properties.containsKey('versionCodeOverride')) {
|
||||||
versionName "0.26.0"
|
versionCode System.getProperty('versionCodeOverride') as Integer
|
||||||
|
} else {
|
||||||
|
versionCode 1004
|
||||||
|
}
|
||||||
|
versionName "0.27.7"
|
||||||
|
if (System.properties.containsKey('versionNameSuffix')) {
|
||||||
|
versionNameSuffix System.getProperty('versionNameSuffix')
|
||||||
|
}
|
||||||
|
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
|
|
||||||
@@ -90,37 +101,27 @@ android {
|
|||||||
androidTest.assets.srcDirs += files("$projectDir/schemas".toString())
|
androidTest.assets.srcDirs += files("$projectDir/schemas".toString())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
androidResources {
|
||||||
|
generateLocaleConfig = true
|
||||||
|
}
|
||||||
|
|
||||||
buildFeatures {
|
buildFeatures {
|
||||||
viewBinding true
|
viewBinding true
|
||||||
|
compose true
|
||||||
|
buildConfig true
|
||||||
}
|
}
|
||||||
|
|
||||||
packagingOptions {
|
packagingOptions {
|
||||||
resources {
|
resources {
|
||||||
// remove two files which belong to jsoup
|
// remove two files which belong to jsoup
|
||||||
// no idea how they ended up in the META-INF dir...
|
// no idea how they ended up in the META-INF dir...
|
||||||
excludes += ['META-INF/README.md', 'META-INF/CHANGES']
|
excludes += ['META-INF/README.md', 'META-INF/CHANGES',
|
||||||
|
// 'COPYRIGHT' belongs to RxJava...
|
||||||
|
'META-INF/COPYRIGHT']
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ext {
|
|
||||||
checkstyleVersion = '10.12.1'
|
|
||||||
|
|
||||||
androidxLifecycleVersion = '2.5.1'
|
|
||||||
androidxRoomVersion = '2.5.2'
|
|
||||||
androidxWorkVersion = '2.7.1'
|
|
||||||
|
|
||||||
icepickVersion = '3.2.0'
|
|
||||||
exoPlayerVersion = '2.18.7'
|
|
||||||
googleAutoServiceVersion = '1.1.1'
|
|
||||||
groupieVersion = '2.10.1'
|
|
||||||
markwonVersion = '4.6.2'
|
|
||||||
|
|
||||||
leakCanaryVersion = '2.12'
|
|
||||||
stethoVersion = '1.6.0'
|
|
||||||
mockitoVersion = '4.0.0'
|
|
||||||
}
|
|
||||||
|
|
||||||
configurations {
|
configurations {
|
||||||
checkstyle
|
checkstyle
|
||||||
ktlint
|
ktlint
|
||||||
@@ -130,10 +131,10 @@ checkstyle {
|
|||||||
getConfigDirectory().set(rootProject.file("checkstyle"))
|
getConfigDirectory().set(rootProject.file("checkstyle"))
|
||||||
ignoreFailures false
|
ignoreFailures false
|
||||||
showViolations true
|
showViolations true
|
||||||
toolVersion = checkstyleVersion
|
toolVersion = libs.versions.checkstyle.get()
|
||||||
}
|
}
|
||||||
|
|
||||||
task runCheckstyle(type: Checkstyle) {
|
tasks.register('runCheckstyle', Checkstyle) {
|
||||||
source 'src'
|
source 'src'
|
||||||
include '**/*.java'
|
include '**/*.java'
|
||||||
exclude '**/gen/**'
|
exclude '**/gen/**'
|
||||||
@@ -154,7 +155,7 @@ task runCheckstyle(type: Checkstyle) {
|
|||||||
def outputDir = "${project.buildDir}/reports/ktlint/"
|
def outputDir = "${project.buildDir}/reports/ktlint/"
|
||||||
def inputFiles = project.fileTree(dir: "src", include: "**/*.kt")
|
def inputFiles = project.fileTree(dir: "src", include: "**/*.kt")
|
||||||
|
|
||||||
task runKtlint(type: JavaExec) {
|
tasks.register('runKtlint', JavaExec) {
|
||||||
inputs.files(inputFiles)
|
inputs.files(inputFiles)
|
||||||
outputs.dir(outputDir)
|
outputs.dir(outputDir)
|
||||||
getMainClass().set("com.pinterest.ktlint.Main")
|
getMainClass().set("com.pinterest.ktlint.Main")
|
||||||
@@ -163,7 +164,7 @@ task runKtlint(type: JavaExec) {
|
|||||||
jvmArgs("--add-opens", "java.base/java.lang=ALL-UNNAMED")
|
jvmArgs("--add-opens", "java.base/java.lang=ALL-UNNAMED")
|
||||||
}
|
}
|
||||||
|
|
||||||
task formatKtlint(type: JavaExec) {
|
tasks.register('formatKtlint', JavaExec) {
|
||||||
inputs.files(inputFiles)
|
inputs.files(inputFiles)
|
||||||
outputs.dir(outputDir)
|
outputs.dir(outputDir)
|
||||||
getMainClass().set("com.pinterest.ktlint.Main")
|
getMainClass().set("com.pinterest.ktlint.Main")
|
||||||
@@ -172,11 +173,13 @@ task formatKtlint(type: JavaExec) {
|
|||||||
jvmArgs("--add-opens", "java.base/java.lang=ALL-UNNAMED")
|
jvmArgs("--add-opens", "java.base/java.lang=ALL-UNNAMED")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
apply from: 'check-dependencies.gradle'
|
||||||
|
|
||||||
afterEvaluate {
|
afterEvaluate {
|
||||||
if (!System.properties.containsKey('skipFormatKtlint')) {
|
if (!System.properties.containsKey('skipFormatKtlint')) {
|
||||||
preDebugBuild.dependsOn formatKtlint
|
preDebugBuild.dependsOn formatKtlint
|
||||||
}
|
}
|
||||||
preDebugBuild.dependsOn runCheckstyle, runKtlint
|
preDebugBuild.dependsOn runCheckstyle, runKtlint, checkDependenciesOrder
|
||||||
}
|
}
|
||||||
|
|
||||||
sonar {
|
sonar {
|
||||||
@@ -187,123 +190,155 @@ sonar {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
kapt {
|
||||||
|
correctErrorTypes true
|
||||||
|
}
|
||||||
|
|
||||||
|
aboutLibraries {
|
||||||
|
// note: offline mode prevents the plugin from fetching licenses at build time, which would be
|
||||||
|
// harmful for reproducible builds
|
||||||
|
offlineMode = true
|
||||||
|
duplicationMode = DuplicateMode.MERGE
|
||||||
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
/** Desugaring **/
|
/** Desugaring **/
|
||||||
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs_nio:2.0.3'
|
coreLibraryDesugaring libs.desugar.jdk.libs.nio
|
||||||
|
|
||||||
/** NewPipe libraries **/
|
/** NewPipe libraries **/
|
||||||
// You can use a local version by uncommenting a few lines in settings.gradle
|
implementation libs.teamnewpipe.nanojson
|
||||||
// Or you can use a commit you pushed to GitHub by just replacing TeamNewPipe with your GitHub
|
implementation libs.teamnewpipe.newpipe.extractor
|
||||||
// name and the commit hash with the commit hash of the (pushed) commit you want to test
|
implementation libs.teamnewpipe.nononsense.filepicker
|
||||||
// This works thanks to JitPack: https://jitpack.io/
|
|
||||||
implementation 'com.github.TeamNewPipe:nanojson:1d9e1aea9049fc9f85e68b43ba39fe7be1c1f751'
|
|
||||||
implementation 'com.github.TeamNewPipe:NewPipeExtractor:v0.23.1'
|
|
||||||
implementation 'com.github.TeamNewPipe:NoNonsense-FilePicker:5.0.0'
|
|
||||||
|
|
||||||
/** Checkstyle **/
|
/** Checkstyle **/
|
||||||
checkstyle "com.puppycrawl.tools:checkstyle:${checkstyleVersion}"
|
checkstyle libs.tools.checkstyle
|
||||||
ktlint 'com.pinterest:ktlint:0.45.2'
|
ktlint libs.tools.ktlint
|
||||||
|
|
||||||
/** Kotlin **/
|
/** Kotlin **/
|
||||||
implementation "org.jetbrains.kotlin:kotlin-stdlib:${kotlin_version}"
|
implementation libs.kotlin.stdlib
|
||||||
|
|
||||||
/** AndroidX **/
|
/** AndroidX **/
|
||||||
implementation 'androidx.appcompat:appcompat:1.5.1'
|
implementation libs.androidx.appcompat
|
||||||
implementation 'androidx.cardview:cardview:1.0.0'
|
implementation libs.androidx.cardview
|
||||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
|
implementation libs.androidx.constraintlayout
|
||||||
implementation 'androidx.core:core-ktx:1.10.0'
|
implementation libs.androidx.core.ktx
|
||||||
implementation 'androidx.documentfile:documentfile:1.0.1'
|
implementation libs.androidx.documentfile
|
||||||
implementation 'androidx.fragment:fragment-ktx:1.4.1'
|
implementation libs.androidx.fragment.compose
|
||||||
implementation "androidx.lifecycle:lifecycle-livedata-ktx:${androidxLifecycleVersion}"
|
implementation libs.androidx.lifecycle.livedata
|
||||||
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:${androidxLifecycleVersion}"
|
implementation libs.androidx.lifecycle.viewmodel
|
||||||
implementation 'androidx.localbroadcastmanager:localbroadcastmanager:1.1.0'
|
implementation libs.androidx.localbroadcastmanager
|
||||||
implementation 'androidx.media:media:1.6.0'
|
implementation libs.androidx.media
|
||||||
implementation 'androidx.preference:preference:1.2.0'
|
implementation libs.androidx.preference
|
||||||
implementation 'androidx.recyclerview:recyclerview:1.2.1'
|
implementation libs.androidx.recyclerview
|
||||||
implementation "androidx.room:room-runtime:${androidxRoomVersion}"
|
implementation libs.androidx.room.runtime
|
||||||
implementation "androidx.room:room-rxjava3:${androidxRoomVersion}"
|
implementation libs.androidx.room.rxjava3
|
||||||
kapt "androidx.room:room-compiler:${androidxRoomVersion}"
|
kapt libs.androidx.room.compiler
|
||||||
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
|
implementation libs.androidx.swiperefreshlayout
|
||||||
// Newer version specified to prevent accessibility regressions with RecyclerView, see:
|
implementation libs.androidx.work.runtime
|
||||||
// https://developer.android.com/jetpack/androidx/releases/viewpager2#1.1.0-alpha01
|
implementation libs.androidx.work.rxjava3
|
||||||
implementation 'androidx.viewpager2:viewpager2:1.1.0-beta01'
|
implementation libs.androidx.material
|
||||||
implementation "androidx.work:work-runtime-ktx:${androidxWorkVersion}"
|
implementation libs.androidx.webkit
|
||||||
implementation "androidx.work:work-rxjava3:${androidxWorkVersion}"
|
|
||||||
implementation 'com.google.android.material:material:1.9.0'
|
|
||||||
|
|
||||||
/** Third-party libraries **/
|
/** Third-party libraries **/
|
||||||
// Instance state boilerplate elimination
|
// Instance state boilerplate elimination
|
||||||
implementation "frankiesardo:icepick:${icepickVersion}"
|
implementation libs.livefront.bridge
|
||||||
kapt "frankiesardo:icepick-processor:${icepickVersion}"
|
implementation libs.android.state
|
||||||
|
kapt libs.android.state.processor
|
||||||
|
|
||||||
// HTML parser
|
// HTML parser
|
||||||
implementation "org.jsoup:jsoup:1.16.1"
|
implementation libs.jsoup
|
||||||
|
|
||||||
// HTTP client
|
// HTTP client
|
||||||
implementation "com.squareup.okhttp3:okhttp:4.11.0"
|
implementation libs.okhttp
|
||||||
// okhttp3:4.11.0 introduces a vulnerability from com.squareup.okio:okio@3.3.0,
|
|
||||||
// remove com.squareup.okio:okio when updating okhttp
|
|
||||||
implementation "com.squareup.okio:okio:3.4.0"
|
|
||||||
|
|
||||||
// Media player
|
// Media player
|
||||||
implementation "com.google.android.exoplayer:exoplayer-core:${exoPlayerVersion}"
|
implementation libs.exoplayer.core
|
||||||
implementation "com.google.android.exoplayer:exoplayer-dash:${exoPlayerVersion}"
|
implementation libs.exoplayer.dash
|
||||||
implementation "com.google.android.exoplayer:exoplayer-database:${exoPlayerVersion}"
|
implementation libs.exoplayer.database
|
||||||
implementation "com.google.android.exoplayer:exoplayer-datasource:${exoPlayerVersion}"
|
implementation libs.exoplayer.datasource
|
||||||
implementation "com.google.android.exoplayer:exoplayer-hls:${exoPlayerVersion}"
|
implementation libs.exoplayer.hls
|
||||||
implementation "com.google.android.exoplayer:exoplayer-smoothstreaming:${exoPlayerVersion}"
|
implementation libs.exoplayer.smoothstreaming
|
||||||
implementation "com.google.android.exoplayer:exoplayer-ui:${exoPlayerVersion}"
|
implementation libs.exoplayer.ui
|
||||||
implementation "com.google.android.exoplayer:extension-mediasession:${exoPlayerVersion}"
|
implementation libs.extension.mediasession
|
||||||
|
|
||||||
// Metadata generator for service descriptors
|
// Metadata generator for service descriptors
|
||||||
compileOnly "com.google.auto.service:auto-service-annotations:${googleAutoServiceVersion}"
|
compileOnly libs.auto.service
|
||||||
kapt "com.google.auto.service:auto-service:${googleAutoServiceVersion}"
|
kapt libs.auto.service.kapt
|
||||||
|
|
||||||
// Manager for complex RecyclerView layouts
|
// Manager for complex RecyclerView layouts
|
||||||
implementation "com.github.lisawray.groupie:groupie:${groupieVersion}"
|
implementation libs.lisawray.groupie
|
||||||
implementation "com.github.lisawray.groupie:groupie-viewbinding:${groupieVersion}"
|
implementation libs.lisawray.groupie.viewbinding
|
||||||
|
|
||||||
// Image loading
|
// Image loading
|
||||||
//noinspection GradleDependency --> 2.8 is the last version, not 2.71828!
|
implementation libs.coil.compose
|
||||||
implementation "com.squareup.picasso:picasso:2.8"
|
implementation libs.coil.network.okhttp
|
||||||
|
|
||||||
// Markdown library for Android
|
// Markdown library for Android
|
||||||
implementation "io.noties.markwon:core:${markwonVersion}"
|
implementation libs.markwon.core
|
||||||
implementation "io.noties.markwon:linkify:${markwonVersion}"
|
implementation libs.markwon.linkify
|
||||||
|
|
||||||
// Crash reporting
|
// Crash reporting
|
||||||
implementation "ch.acra:acra-core:5.10.1"
|
implementation libs.acra.core
|
||||||
|
|
||||||
// Properly restarting
|
// Properly restarting
|
||||||
implementation 'com.jakewharton:process-phoenix:2.1.2'
|
implementation libs.process.phoenix
|
||||||
|
|
||||||
// Reactive extensions for Java VM
|
// Reactive extensions for Java VM
|
||||||
implementation "io.reactivex.rxjava3:rxjava:3.1.6"
|
implementation libs.rxjava3.rxjava
|
||||||
implementation "io.reactivex.rxjava3:rxandroid:3.0.2"
|
implementation libs.rxjava3.rxandroid
|
||||||
// RxJava binding APIs for Android UI widgets
|
// RxJava binding APIs for Android UI widgets
|
||||||
implementation "com.jakewharton.rxbinding4:rxbinding:4.0.0"
|
implementation libs.rxbinding4.rxbinding
|
||||||
|
|
||||||
// Date and time formatting
|
// Date and time formatting
|
||||||
implementation "org.ocpsoft.prettytime:prettytime:5.0.6.Final"
|
implementation libs.prettytime
|
||||||
|
|
||||||
|
// Jetpack Compose
|
||||||
|
implementation(platform(libs.androidx.compose.bom))
|
||||||
|
implementation libs.androidx.compose.material3
|
||||||
|
implementation libs.androidx.compose.adaptive
|
||||||
|
implementation libs.androidx.activity.compose
|
||||||
|
implementation libs.androidx.compose.ui.tooling.preview
|
||||||
|
implementation libs.androidx.lifecycle.viewmodel.compose
|
||||||
|
implementation libs.androidx.compose.ui.text // Needed for parsing HTML to AnnotatedString
|
||||||
|
implementation libs.androidx.compose.material.icons.extended
|
||||||
|
|
||||||
|
// Jetpack Compose related dependencies
|
||||||
|
implementation libs.androidx.paging.compose
|
||||||
|
implementation libs.androidx.navigation.compose
|
||||||
|
|
||||||
|
// Coroutines interop
|
||||||
|
implementation libs.kotlinx.coroutines.rx3
|
||||||
|
|
||||||
|
// Library loading for About screen
|
||||||
|
implementation libs.aboutlibraries.compose.m3
|
||||||
|
|
||||||
|
// Hilt
|
||||||
|
implementation libs.hilt.android
|
||||||
|
kapt(libs.hilt.compiler)
|
||||||
|
|
||||||
|
// Scroll
|
||||||
|
implementation libs.lazycolumnscrollbar
|
||||||
|
|
||||||
/** Debugging **/
|
/** Debugging **/
|
||||||
// Memory leak detection
|
// Memory leak detection
|
||||||
debugImplementation "com.squareup.leakcanary:leakcanary-object-watcher-android:${leakCanaryVersion}"
|
debugImplementation libs.leakcanary.object.watcher
|
||||||
debugImplementation "com.squareup.leakcanary:plumber-android:${leakCanaryVersion}"
|
debugImplementation libs.leakcanary.plumber.android
|
||||||
debugImplementation "com.squareup.leakcanary:leakcanary-android-core:${leakCanaryVersion}"
|
debugImplementation libs.leakcanary.android.core
|
||||||
// Debug bridge for Android
|
// Debug bridge for Android
|
||||||
debugImplementation "com.facebook.stetho:stetho:${stethoVersion}"
|
debugImplementation libs.stetho
|
||||||
debugImplementation "com.facebook.stetho:stetho-okhttp3:${stethoVersion}"
|
debugImplementation libs.stetho.okhttp3
|
||||||
|
|
||||||
|
// Jetpack Compose
|
||||||
|
debugImplementation libs.androidx.compose.ui.tooling
|
||||||
|
|
||||||
/** Testing **/
|
/** Testing **/
|
||||||
testImplementation 'junit:junit:4.13.2'
|
testImplementation libs.junit
|
||||||
testImplementation "org.mockito:mockito-core:${mockitoVersion}"
|
testImplementation libs.mockito.core
|
||||||
testImplementation "org.mockito:mockito-inline:${mockitoVersion}"
|
|
||||||
|
|
||||||
androidTestImplementation "androidx.test.ext:junit:1.1.5"
|
androidTestImplementation libs.androidx.junit
|
||||||
androidTestImplementation "androidx.test:runner:1.5.2"
|
androidTestImplementation libs.androidx.runner
|
||||||
androidTestImplementation "androidx.room:room-testing:${androidxRoomVersion}"
|
androidTestImplementation libs.androidx.room.testing
|
||||||
androidTestImplementation "org.assertj:assertj-core:3.23.1"
|
androidTestImplementation libs.assertj.core
|
||||||
}
|
}
|
||||||
|
|
||||||
static String getGitWorkingBranch() {
|
static String getGitWorkingBranch() {
|
||||||
|
|||||||
48
app/check-dependencies.gradle
Normal file
48
app/check-dependencies.gradle
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
tasks.register('checkDependenciesOrder') {
|
||||||
|
group = 'verification'
|
||||||
|
description = 'Checks that each section in libs.versions.toml is sorted alphabetically'
|
||||||
|
|
||||||
|
def tomlFile = file('../gradle/libs.versions.toml')
|
||||||
|
|
||||||
|
doLast {
|
||||||
|
if (!tomlFile.exists()) {
|
||||||
|
throw new GradleException('TOML file not found')
|
||||||
|
}
|
||||||
|
|
||||||
|
def lines = tomlFile.readLines()
|
||||||
|
def nonSortedBlocks = []
|
||||||
|
def currentBlock = []
|
||||||
|
def prevLine = ''
|
||||||
|
def prevIndex = 0
|
||||||
|
|
||||||
|
lines.eachWithIndex { line, lineIndex ->
|
||||||
|
if (line.trim() && !line.startsWith('#')) {
|
||||||
|
if (line.startsWith('[')) {
|
||||||
|
prevLine = ''
|
||||||
|
} else {
|
||||||
|
def currIndex = lineIndex + 1
|
||||||
|
if (prevLine > line) {
|
||||||
|
if (currentBlock && currentBlock[-1] == "${prevIndex}: ${prevLine}") {
|
||||||
|
currentBlock.add("${currIndex}: ${line}")
|
||||||
|
} else {
|
||||||
|
if (!currentBlock.isEmpty()) {
|
||||||
|
nonSortedBlocks.add(currentBlock)
|
||||||
|
currentBlock = []
|
||||||
|
}
|
||||||
|
currentBlock.add("${prevIndex}: ${prevLine}")
|
||||||
|
currentBlock.add("${currIndex}: ${line}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
prevLine = line
|
||||||
|
prevIndex = lineIndex + 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!currentBlock.isEmpty()) {
|
||||||
|
nonSortedBlocks.add(currentBlock)
|
||||||
|
throw new GradleException("The following lines were not sorted:\n" +
|
||||||
|
nonSortedBlocks.collect { it.join("\n") }.join("\n\n"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
17
app/proguard-rules.pro
vendored
17
app/proguard-rules.pro
vendored
@@ -5,22 +5,21 @@
|
|||||||
|
|
||||||
## Rules for NewPipeExtractor
|
## Rules for NewPipeExtractor
|
||||||
-keep class org.schabi.newpipe.extractor.timeago.patterns.** { *; }
|
-keep class org.schabi.newpipe.extractor.timeago.patterns.** { *; }
|
||||||
|
## Rules for Rhino and Rhino Engine
|
||||||
|
-keep class org.mozilla.javascript.* { *; }
|
||||||
-keep class org.mozilla.javascript.** { *; }
|
-keep class org.mozilla.javascript.** { *; }
|
||||||
|
-keep class org.mozilla.javascript.engine.** { *; }
|
||||||
-keep class org.mozilla.classfile.ClassFileWriter
|
-keep class org.mozilla.classfile.ClassFileWriter
|
||||||
|
-dontwarn org.mozilla.javascript.JavaToJSONConverters
|
||||||
-dontwarn org.mozilla.javascript.tools.**
|
-dontwarn org.mozilla.javascript.tools.**
|
||||||
|
-keep class javax.script.** { *; }
|
||||||
|
-dontwarn javax.script.**
|
||||||
|
-keep class jdk.dynalink.** { *; }
|
||||||
|
-dontwarn jdk.dynalink.**
|
||||||
|
|
||||||
## Rules for ExoPlayer
|
## Rules for ExoPlayer
|
||||||
-keep class com.google.android.exoplayer2.** { *; }
|
-keep class com.google.android.exoplayer2.** { *; }
|
||||||
|
|
||||||
## Rules for Icepick. Copy pasted from https://github.com/frankiesardo/icepick
|
|
||||||
-dontwarn icepick.**
|
|
||||||
-keep class icepick.** { *; }
|
|
||||||
-keep class **$$Icepick { *; }
|
|
||||||
-keepclasseswithmembernames class * {
|
|
||||||
@icepick.* <fields>;
|
|
||||||
}
|
|
||||||
-keepnames class * { @icepick.State *;}
|
|
||||||
|
|
||||||
## Rules for OkHttp. Copy pasted from https://github.com/square/okhttp
|
## Rules for OkHttp. Copy pasted from https://github.com/square/okhttp
|
||||||
-dontwarn okhttp3.**
|
-dontwarn okhttp3.**
|
||||||
-dontwarn okio.**
|
-dontwarn okio.**
|
||||||
|
|||||||
737
app/schemas/org.schabi.newpipe.database.AppDatabase/8.json
Normal file
737
app/schemas/org.schabi.newpipe.database.AppDatabase/8.json
Normal file
@@ -0,0 +1,737 @@
|
|||||||
|
{
|
||||||
|
"formatVersion": 1,
|
||||||
|
"database": {
|
||||||
|
"version": 8,
|
||||||
|
"identityHash": "012fc8e7ad3333f1597347f34e76a513",
|
||||||
|
"entities": [
|
||||||
|
{
|
||||||
|
"tableName": "subscriptions",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `service_id` INTEGER NOT NULL, `url` TEXT, `name` TEXT, `avatar_url` TEXT, `subscriber_count` INTEGER, `description` TEXT, `notification_mode` INTEGER NOT NULL)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "uid",
|
||||||
|
"columnName": "uid",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "serviceId",
|
||||||
|
"columnName": "service_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "url",
|
||||||
|
"columnName": "url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "name",
|
||||||
|
"columnName": "name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "avatarUrl",
|
||||||
|
"columnName": "avatar_url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "subscriberCount",
|
||||||
|
"columnName": "subscriber_count",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "description",
|
||||||
|
"columnName": "description",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "notificationMode",
|
||||||
|
"columnName": "notification_mode",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": true,
|
||||||
|
"columnNames": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_subscriptions_service_id_url",
|
||||||
|
"unique": true,
|
||||||
|
"columnNames": [
|
||||||
|
"service_id",
|
||||||
|
"url"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_subscriptions_service_id_url` ON `${TABLE_NAME}` (`service_id`, `url`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "search_history",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`creation_date` INTEGER, `service_id` INTEGER NOT NULL, `search` TEXT, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "creationDate",
|
||||||
|
"columnName": "creation_date",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "serviceId",
|
||||||
|
"columnName": "service_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "search",
|
||||||
|
"columnName": "search",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": true,
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_search_history_search",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"search"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_search_history_search` ON `${TABLE_NAME}` (`search`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "streams",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `service_id` INTEGER NOT NULL, `url` TEXT NOT NULL, `title` TEXT NOT NULL, `stream_type` TEXT NOT NULL, `duration` INTEGER NOT NULL, `uploader` TEXT NOT NULL, `uploader_url` TEXT, `thumbnail_url` TEXT, `view_count` INTEGER, `textual_upload_date` TEXT, `upload_date` INTEGER, `is_upload_date_approximation` INTEGER)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "uid",
|
||||||
|
"columnName": "uid",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "serviceId",
|
||||||
|
"columnName": "service_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "url",
|
||||||
|
"columnName": "url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "title",
|
||||||
|
"columnName": "title",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "streamType",
|
||||||
|
"columnName": "stream_type",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "duration",
|
||||||
|
"columnName": "duration",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "uploader",
|
||||||
|
"columnName": "uploader",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "uploaderUrl",
|
||||||
|
"columnName": "uploader_url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "thumbnailUrl",
|
||||||
|
"columnName": "thumbnail_url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "viewCount",
|
||||||
|
"columnName": "view_count",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "textualUploadDate",
|
||||||
|
"columnName": "textual_upload_date",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "uploadDate",
|
||||||
|
"columnName": "upload_date",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "isUploadDateApproximation",
|
||||||
|
"columnName": "is_upload_date_approximation",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": true,
|
||||||
|
"columnNames": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_streams_service_id_url",
|
||||||
|
"unique": true,
|
||||||
|
"columnNames": [
|
||||||
|
"service_id",
|
||||||
|
"url"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_streams_service_id_url` ON `${TABLE_NAME}` (`service_id`, `url`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "stream_history",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`stream_id` INTEGER NOT NULL, `access_date` INTEGER NOT NULL, `repeat_count` INTEGER NOT NULL, PRIMARY KEY(`stream_id`, `access_date`), FOREIGN KEY(`stream_id`) REFERENCES `streams`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE )",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "streamUid",
|
||||||
|
"columnName": "stream_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "accessDate",
|
||||||
|
"columnName": "access_date",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "repeatCount",
|
||||||
|
"columnName": "repeat_count",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"stream_id",
|
||||||
|
"access_date"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_stream_history_stream_id",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"stream_id"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_stream_history_stream_id` ON `${TABLE_NAME}` (`stream_id`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "streams",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"stream_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "stream_state",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`stream_id` INTEGER NOT NULL, `progress_time` INTEGER NOT NULL, PRIMARY KEY(`stream_id`), FOREIGN KEY(`stream_id`) REFERENCES `streams`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE )",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "streamUid",
|
||||||
|
"columnName": "stream_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "progressMillis",
|
||||||
|
"columnName": "progress_time",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"stream_id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "streams",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"stream_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "playlists",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT, `is_thumbnail_permanent` INTEGER NOT NULL, `thumbnail_stream_id` INTEGER NOT NULL)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "uid",
|
||||||
|
"columnName": "uid",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "name",
|
||||||
|
"columnName": "name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "isThumbnailPermanent",
|
||||||
|
"columnName": "is_thumbnail_permanent",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "thumbnailStreamId",
|
||||||
|
"columnName": "thumbnail_stream_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": true,
|
||||||
|
"columnNames": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_playlists_name",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"name"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_playlists_name` ON `${TABLE_NAME}` (`name`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "playlist_stream_join",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`playlist_id` INTEGER NOT NULL, `stream_id` INTEGER NOT NULL, `join_index` INTEGER NOT NULL, PRIMARY KEY(`playlist_id`, `join_index`), FOREIGN KEY(`playlist_id`) REFERENCES `playlists`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, FOREIGN KEY(`stream_id`) REFERENCES `streams`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "playlistUid",
|
||||||
|
"columnName": "playlist_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "streamUid",
|
||||||
|
"columnName": "stream_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "index",
|
||||||
|
"columnName": "join_index",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"playlist_id",
|
||||||
|
"join_index"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_playlist_stream_join_playlist_id_join_index",
|
||||||
|
"unique": true,
|
||||||
|
"columnNames": [
|
||||||
|
"playlist_id",
|
||||||
|
"join_index"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_playlist_stream_join_playlist_id_join_index` ON `${TABLE_NAME}` (`playlist_id`, `join_index`)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "index_playlist_stream_join_stream_id",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"stream_id"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_playlist_stream_join_stream_id` ON `${TABLE_NAME}` (`stream_id`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "playlists",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"playlist_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"table": "streams",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"stream_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "remote_playlists",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `service_id` INTEGER NOT NULL, `name` TEXT, `url` TEXT, `thumbnail_url` TEXT, `uploader` TEXT, `stream_count` INTEGER)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "uid",
|
||||||
|
"columnName": "uid",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "serviceId",
|
||||||
|
"columnName": "service_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "name",
|
||||||
|
"columnName": "name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "url",
|
||||||
|
"columnName": "url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "thumbnailUrl",
|
||||||
|
"columnName": "thumbnail_url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "uploader",
|
||||||
|
"columnName": "uploader",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "streamCount",
|
||||||
|
"columnName": "stream_count",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": true,
|
||||||
|
"columnNames": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_remote_playlists_name",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"name"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_remote_playlists_name` ON `${TABLE_NAME}` (`name`)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "index_remote_playlists_service_id_url",
|
||||||
|
"unique": true,
|
||||||
|
"columnNames": [
|
||||||
|
"service_id",
|
||||||
|
"url"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_remote_playlists_service_id_url` ON `${TABLE_NAME}` (`service_id`, `url`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "feed",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`stream_id` INTEGER NOT NULL, `subscription_id` INTEGER NOT NULL, PRIMARY KEY(`stream_id`, `subscription_id`), FOREIGN KEY(`stream_id`) REFERENCES `streams`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, FOREIGN KEY(`subscription_id`) REFERENCES `subscriptions`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "streamId",
|
||||||
|
"columnName": "stream_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "subscriptionId",
|
||||||
|
"columnName": "subscription_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"stream_id",
|
||||||
|
"subscription_id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_feed_subscription_id",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"subscription_id"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_feed_subscription_id` ON `${TABLE_NAME}` (`subscription_id`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "streams",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"stream_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"table": "subscriptions",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"subscription_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "feed_group",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `icon_id` INTEGER NOT NULL, `sort_order` INTEGER NOT NULL)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "uid",
|
||||||
|
"columnName": "uid",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "name",
|
||||||
|
"columnName": "name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "icon",
|
||||||
|
"columnName": "icon_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "sortOrder",
|
||||||
|
"columnName": "sort_order",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": true,
|
||||||
|
"columnNames": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_feed_group_sort_order",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"sort_order"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_feed_group_sort_order` ON `${TABLE_NAME}` (`sort_order`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "feed_group_subscription_join",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`group_id` INTEGER NOT NULL, `subscription_id` INTEGER NOT NULL, PRIMARY KEY(`group_id`, `subscription_id`), FOREIGN KEY(`group_id`) REFERENCES `feed_group`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, FOREIGN KEY(`subscription_id`) REFERENCES `subscriptions`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "feedGroupId",
|
||||||
|
"columnName": "group_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "subscriptionId",
|
||||||
|
"columnName": "subscription_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"group_id",
|
||||||
|
"subscription_id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_feed_group_subscription_join_subscription_id",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"subscription_id"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_feed_group_subscription_join_subscription_id` ON `${TABLE_NAME}` (`subscription_id`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "feed_group",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"group_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"table": "subscriptions",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"subscription_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "feed_last_updated",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`subscription_id` INTEGER NOT NULL, `last_updated` INTEGER, PRIMARY KEY(`subscription_id`), FOREIGN KEY(`subscription_id`) REFERENCES `subscriptions`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "subscriptionId",
|
||||||
|
"columnName": "subscription_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastUpdated",
|
||||||
|
"columnName": "last_updated",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"subscription_id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "subscriptions",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"subscription_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"views": [],
|
||||||
|
"setupQueries": [
|
||||||
|
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||||
|
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '012fc8e7ad3333f1597347f34e76a513')"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
730
app/schemas/org.schabi.newpipe.database.AppDatabase/9.json
Normal file
730
app/schemas/org.schabi.newpipe.database.AppDatabase/9.json
Normal file
@@ -0,0 +1,730 @@
|
|||||||
|
{
|
||||||
|
"formatVersion": 1,
|
||||||
|
"database": {
|
||||||
|
"version": 9,
|
||||||
|
"identityHash": "7591e8039faa74d8c0517dc867af9d3e",
|
||||||
|
"entities": [
|
||||||
|
{
|
||||||
|
"tableName": "subscriptions",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `service_id` INTEGER NOT NULL, `url` TEXT, `name` TEXT, `avatar_url` TEXT, `subscriber_count` INTEGER, `description` TEXT, `notification_mode` INTEGER NOT NULL)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "uid",
|
||||||
|
"columnName": "uid",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "serviceId",
|
||||||
|
"columnName": "service_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "url",
|
||||||
|
"columnName": "url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "name",
|
||||||
|
"columnName": "name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "avatarUrl",
|
||||||
|
"columnName": "avatar_url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "subscriberCount",
|
||||||
|
"columnName": "subscriber_count",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "description",
|
||||||
|
"columnName": "description",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "notificationMode",
|
||||||
|
"columnName": "notification_mode",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": true,
|
||||||
|
"columnNames": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_subscriptions_service_id_url",
|
||||||
|
"unique": true,
|
||||||
|
"columnNames": [
|
||||||
|
"service_id",
|
||||||
|
"url"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_subscriptions_service_id_url` ON `${TABLE_NAME}` (`service_id`, `url`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "search_history",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`creation_date` INTEGER, `service_id` INTEGER NOT NULL, `search` TEXT, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "creationDate",
|
||||||
|
"columnName": "creation_date",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "serviceId",
|
||||||
|
"columnName": "service_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "search",
|
||||||
|
"columnName": "search",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": true,
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_search_history_search",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"search"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_search_history_search` ON `${TABLE_NAME}` (`search`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "streams",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `service_id` INTEGER NOT NULL, `url` TEXT NOT NULL, `title` TEXT NOT NULL, `stream_type` TEXT NOT NULL, `duration` INTEGER NOT NULL, `uploader` TEXT NOT NULL, `uploader_url` TEXT, `thumbnail_url` TEXT, `view_count` INTEGER, `textual_upload_date` TEXT, `upload_date` INTEGER, `is_upload_date_approximation` INTEGER)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "uid",
|
||||||
|
"columnName": "uid",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "serviceId",
|
||||||
|
"columnName": "service_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "url",
|
||||||
|
"columnName": "url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "title",
|
||||||
|
"columnName": "title",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "streamType",
|
||||||
|
"columnName": "stream_type",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "duration",
|
||||||
|
"columnName": "duration",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "uploader",
|
||||||
|
"columnName": "uploader",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "uploaderUrl",
|
||||||
|
"columnName": "uploader_url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "thumbnailUrl",
|
||||||
|
"columnName": "thumbnail_url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "viewCount",
|
||||||
|
"columnName": "view_count",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "textualUploadDate",
|
||||||
|
"columnName": "textual_upload_date",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "uploadDate",
|
||||||
|
"columnName": "upload_date",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "isUploadDateApproximation",
|
||||||
|
"columnName": "is_upload_date_approximation",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": true,
|
||||||
|
"columnNames": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_streams_service_id_url",
|
||||||
|
"unique": true,
|
||||||
|
"columnNames": [
|
||||||
|
"service_id",
|
||||||
|
"url"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_streams_service_id_url` ON `${TABLE_NAME}` (`service_id`, `url`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "stream_history",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`stream_id` INTEGER NOT NULL, `access_date` INTEGER NOT NULL, `repeat_count` INTEGER NOT NULL, PRIMARY KEY(`stream_id`, `access_date`), FOREIGN KEY(`stream_id`) REFERENCES `streams`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE )",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "streamUid",
|
||||||
|
"columnName": "stream_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "accessDate",
|
||||||
|
"columnName": "access_date",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "repeatCount",
|
||||||
|
"columnName": "repeat_count",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"stream_id",
|
||||||
|
"access_date"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_stream_history_stream_id",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"stream_id"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_stream_history_stream_id` ON `${TABLE_NAME}` (`stream_id`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "streams",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"stream_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "stream_state",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`stream_id` INTEGER NOT NULL, `progress_time` INTEGER NOT NULL, PRIMARY KEY(`stream_id`), FOREIGN KEY(`stream_id`) REFERENCES `streams`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE )",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "streamUid",
|
||||||
|
"columnName": "stream_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "progressMillis",
|
||||||
|
"columnName": "progress_time",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"stream_id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "streams",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"stream_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "playlists",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT, `is_thumbnail_permanent` INTEGER NOT NULL, `thumbnail_stream_id` INTEGER NOT NULL, `display_index` INTEGER NOT NULL)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "uid",
|
||||||
|
"columnName": "uid",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "name",
|
||||||
|
"columnName": "name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "isThumbnailPermanent",
|
||||||
|
"columnName": "is_thumbnail_permanent",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "thumbnailStreamId",
|
||||||
|
"columnName": "thumbnail_stream_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "displayIndex",
|
||||||
|
"columnName": "display_index",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": true,
|
||||||
|
"columnNames": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "playlist_stream_join",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`playlist_id` INTEGER NOT NULL, `stream_id` INTEGER NOT NULL, `join_index` INTEGER NOT NULL, PRIMARY KEY(`playlist_id`, `join_index`), FOREIGN KEY(`playlist_id`) REFERENCES `playlists`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, FOREIGN KEY(`stream_id`) REFERENCES `streams`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "playlistUid",
|
||||||
|
"columnName": "playlist_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "streamUid",
|
||||||
|
"columnName": "stream_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "index",
|
||||||
|
"columnName": "join_index",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"playlist_id",
|
||||||
|
"join_index"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_playlist_stream_join_playlist_id_join_index",
|
||||||
|
"unique": true,
|
||||||
|
"columnNames": [
|
||||||
|
"playlist_id",
|
||||||
|
"join_index"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_playlist_stream_join_playlist_id_join_index` ON `${TABLE_NAME}` (`playlist_id`, `join_index`)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "index_playlist_stream_join_stream_id",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"stream_id"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_playlist_stream_join_stream_id` ON `${TABLE_NAME}` (`stream_id`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "playlists",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"playlist_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"table": "streams",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"stream_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "remote_playlists",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `service_id` INTEGER NOT NULL, `name` TEXT, `url` TEXT, `thumbnail_url` TEXT, `uploader` TEXT, `display_index` INTEGER NOT NULL, `stream_count` INTEGER)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "uid",
|
||||||
|
"columnName": "uid",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "serviceId",
|
||||||
|
"columnName": "service_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "name",
|
||||||
|
"columnName": "name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "url",
|
||||||
|
"columnName": "url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "thumbnailUrl",
|
||||||
|
"columnName": "thumbnail_url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "uploader",
|
||||||
|
"columnName": "uploader",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "displayIndex",
|
||||||
|
"columnName": "display_index",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "streamCount",
|
||||||
|
"columnName": "stream_count",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": true,
|
||||||
|
"columnNames": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_remote_playlists_service_id_url",
|
||||||
|
"unique": true,
|
||||||
|
"columnNames": [
|
||||||
|
"service_id",
|
||||||
|
"url"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_remote_playlists_service_id_url` ON `${TABLE_NAME}` (`service_id`, `url`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "feed",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`stream_id` INTEGER NOT NULL, `subscription_id` INTEGER NOT NULL, PRIMARY KEY(`stream_id`, `subscription_id`), FOREIGN KEY(`stream_id`) REFERENCES `streams`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, FOREIGN KEY(`subscription_id`) REFERENCES `subscriptions`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "streamId",
|
||||||
|
"columnName": "stream_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "subscriptionId",
|
||||||
|
"columnName": "subscription_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"stream_id",
|
||||||
|
"subscription_id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_feed_subscription_id",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"subscription_id"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_feed_subscription_id` ON `${TABLE_NAME}` (`subscription_id`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "streams",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"stream_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"table": "subscriptions",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"subscription_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "feed_group",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `icon_id` INTEGER NOT NULL, `sort_order` INTEGER NOT NULL)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "uid",
|
||||||
|
"columnName": "uid",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "name",
|
||||||
|
"columnName": "name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "icon",
|
||||||
|
"columnName": "icon_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "sortOrder",
|
||||||
|
"columnName": "sort_order",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": true,
|
||||||
|
"columnNames": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_feed_group_sort_order",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"sort_order"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_feed_group_sort_order` ON `${TABLE_NAME}` (`sort_order`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "feed_group_subscription_join",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`group_id` INTEGER NOT NULL, `subscription_id` INTEGER NOT NULL, PRIMARY KEY(`group_id`, `subscription_id`), FOREIGN KEY(`group_id`) REFERENCES `feed_group`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, FOREIGN KEY(`subscription_id`) REFERENCES `subscriptions`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "feedGroupId",
|
||||||
|
"columnName": "group_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "subscriptionId",
|
||||||
|
"columnName": "subscription_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"group_id",
|
||||||
|
"subscription_id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_feed_group_subscription_join_subscription_id",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"subscription_id"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_feed_group_subscription_join_subscription_id` ON `${TABLE_NAME}` (`subscription_id`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "feed_group",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"group_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"table": "subscriptions",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"subscription_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "feed_last_updated",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`subscription_id` INTEGER NOT NULL, `last_updated` INTEGER, PRIMARY KEY(`subscription_id`), FOREIGN KEY(`subscription_id`) REFERENCES `subscriptions`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "subscriptionId",
|
||||||
|
"columnName": "subscription_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastUpdated",
|
||||||
|
"columnName": "last_updated",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"subscription_id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "subscriptions",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"subscription_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"views": [],
|
||||||
|
"setupQueries": [
|
||||||
|
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||||
|
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '7591e8039faa74d8c0517dc867af9d3e')"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,10 +8,14 @@ import androidx.test.core.app.ApplicationProvider
|
|||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
import androidx.test.platform.app.InstrumentationRegistry
|
import androidx.test.platform.app.InstrumentationRegistry
|
||||||
import org.junit.Assert.assertEquals
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Assert.assertNotEquals
|
||||||
import org.junit.Assert.assertNull
|
import org.junit.Assert.assertNull
|
||||||
import org.junit.Rule
|
import org.junit.Rule
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
|
import org.schabi.newpipe.database.playlist.model.PlaylistEntity
|
||||||
|
import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity
|
||||||
|
import org.schabi.newpipe.extractor.ServiceList
|
||||||
import org.schabi.newpipe.extractor.stream.StreamType
|
import org.schabi.newpipe.extractor.stream.StreamType
|
||||||
|
|
||||||
@RunWith(AndroidJUnit4::class)
|
@RunWith(AndroidJUnit4::class)
|
||||||
@@ -20,13 +24,17 @@ class DatabaseMigrationTest {
|
|||||||
private const val DEFAULT_SERVICE_ID = 0
|
private const val DEFAULT_SERVICE_ID = 0
|
||||||
private const val DEFAULT_URL = "https://www.youtube.com/watch?v=cDphUib5iG4"
|
private const val DEFAULT_URL = "https://www.youtube.com/watch?v=cDphUib5iG4"
|
||||||
private const val DEFAULT_TITLE = "Test Title"
|
private const val DEFAULT_TITLE = "Test Title"
|
||||||
|
private const val DEFAULT_NAME = "Test Name"
|
||||||
private val DEFAULT_TYPE = StreamType.VIDEO_STREAM
|
private val DEFAULT_TYPE = StreamType.VIDEO_STREAM
|
||||||
private const val DEFAULT_DURATION = 480L
|
private const val DEFAULT_DURATION = 480L
|
||||||
private const val DEFAULT_UPLOADER_NAME = "Uploader Test"
|
private const val DEFAULT_UPLOADER_NAME = "Uploader Test"
|
||||||
private const val DEFAULT_THUMBNAIL = "https://example.com/example.jpg"
|
private const val DEFAULT_THUMBNAIL = "https://example.com/example.jpg"
|
||||||
|
|
||||||
private const val DEFAULT_SECOND_SERVICE_ID = 0
|
private const val DEFAULT_SECOND_SERVICE_ID = 1
|
||||||
private const val DEFAULT_SECOND_URL = "https://www.youtube.com/watch?v=ncQU6iBn5Fc"
|
private const val DEFAULT_SECOND_URL = "https://www.youtube.com/watch?v=ncQU6iBn5Fc"
|
||||||
|
|
||||||
|
private const val DEFAULT_THIRD_SERVICE_ID = 2
|
||||||
|
private const val DEFAULT_THIRD_URL = "https://www.youtube.com/watch?v=dQw4w9WgXcQ"
|
||||||
}
|
}
|
||||||
|
|
||||||
@get:Rule
|
@get:Rule
|
||||||
@@ -106,6 +114,20 @@ class DatabaseMigrationTest {
|
|||||||
Migrations.MIGRATION_6_7
|
Migrations.MIGRATION_6_7
|
||||||
)
|
)
|
||||||
|
|
||||||
|
testHelper.runMigrationsAndValidate(
|
||||||
|
AppDatabase.DATABASE_NAME,
|
||||||
|
Migrations.DB_VER_8,
|
||||||
|
true,
|
||||||
|
Migrations.MIGRATION_7_8
|
||||||
|
)
|
||||||
|
|
||||||
|
testHelper.runMigrationsAndValidate(
|
||||||
|
AppDatabase.DATABASE_NAME,
|
||||||
|
Migrations.DB_VER_9,
|
||||||
|
true,
|
||||||
|
Migrations.MIGRATION_8_9
|
||||||
|
)
|
||||||
|
|
||||||
val migratedDatabaseV3 = getMigratedDatabase()
|
val migratedDatabaseV3 = getMigratedDatabase()
|
||||||
val listFromDB = migratedDatabaseV3.streamDAO().all.blockingFirst()
|
val listFromDB = migratedDatabaseV3.streamDAO().all.blockingFirst()
|
||||||
|
|
||||||
@@ -140,6 +162,157 @@ class DatabaseMigrationTest {
|
|||||||
assertNull(secondStreamFromMigratedDatabase.isUploadDateApproximation)
|
assertNull(secondStreamFromMigratedDatabase.isUploadDateApproximation)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun migrateDatabaseFrom7to8() {
|
||||||
|
val databaseInV7 = testHelper.createDatabase(AppDatabase.DATABASE_NAME, Migrations.DB_VER_7)
|
||||||
|
|
||||||
|
val defaultSearch1 = " abc "
|
||||||
|
val defaultSearch2 = " abc"
|
||||||
|
|
||||||
|
val serviceId = DEFAULT_SERVICE_ID // YouTube
|
||||||
|
// Use id different to YouTube because two searches with the same query
|
||||||
|
// but different service are considered not equal.
|
||||||
|
val otherServiceId = ServiceList.SoundCloud.serviceId
|
||||||
|
|
||||||
|
databaseInV7.run {
|
||||||
|
insert(
|
||||||
|
"search_history", SQLiteDatabase.CONFLICT_FAIL,
|
||||||
|
ContentValues().apply {
|
||||||
|
put("service_id", serviceId)
|
||||||
|
put("search", defaultSearch1)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
insert(
|
||||||
|
"search_history", SQLiteDatabase.CONFLICT_FAIL,
|
||||||
|
ContentValues().apply {
|
||||||
|
put("service_id", serviceId)
|
||||||
|
put("search", defaultSearch2)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
insert(
|
||||||
|
"search_history", SQLiteDatabase.CONFLICT_FAIL,
|
||||||
|
ContentValues().apply {
|
||||||
|
put("service_id", otherServiceId)
|
||||||
|
put("search", defaultSearch1)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
insert(
|
||||||
|
"search_history", SQLiteDatabase.CONFLICT_FAIL,
|
||||||
|
ContentValues().apply {
|
||||||
|
put("service_id", otherServiceId)
|
||||||
|
put("search", defaultSearch2)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
close()
|
||||||
|
}
|
||||||
|
|
||||||
|
testHelper.runMigrationsAndValidate(
|
||||||
|
AppDatabase.DATABASE_NAME, Migrations.DB_VER_8,
|
||||||
|
true, Migrations.MIGRATION_7_8
|
||||||
|
)
|
||||||
|
|
||||||
|
testHelper.runMigrationsAndValidate(
|
||||||
|
AppDatabase.DATABASE_NAME, Migrations.DB_VER_9,
|
||||||
|
true, Migrations.MIGRATION_8_9
|
||||||
|
)
|
||||||
|
|
||||||
|
val migratedDatabaseV8 = getMigratedDatabase()
|
||||||
|
val listFromDB = migratedDatabaseV8.searchHistoryDAO().all.blockingFirst()
|
||||||
|
|
||||||
|
assertEquals(2, listFromDB.size)
|
||||||
|
assertEquals("abc", listFromDB[0].search)
|
||||||
|
assertEquals("abc", listFromDB[1].search)
|
||||||
|
assertNotEquals(listFromDB[0].serviceId, listFromDB[1].serviceId)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun migrateDatabaseFrom8to9() {
|
||||||
|
val databaseInV8 = testHelper.createDatabase(AppDatabase.DATABASE_NAME, Migrations.DB_VER_8)
|
||||||
|
|
||||||
|
val localUid1: Long
|
||||||
|
val localUid2: Long
|
||||||
|
val remoteUid1: Long
|
||||||
|
val remoteUid2: Long
|
||||||
|
databaseInV8.run {
|
||||||
|
localUid1 = insert(
|
||||||
|
"playlists", SQLiteDatabase.CONFLICT_FAIL,
|
||||||
|
ContentValues().apply {
|
||||||
|
put("name", DEFAULT_NAME + "1")
|
||||||
|
put("is_thumbnail_permanent", false)
|
||||||
|
put("thumbnail_stream_id", -1)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
localUid2 = insert(
|
||||||
|
"playlists", SQLiteDatabase.CONFLICT_FAIL,
|
||||||
|
ContentValues().apply {
|
||||||
|
put("name", DEFAULT_NAME + "2")
|
||||||
|
put("is_thumbnail_permanent", false)
|
||||||
|
put("thumbnail_stream_id", -1)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
delete(
|
||||||
|
"playlists", "uid = ?",
|
||||||
|
Array(1) { localUid1 }
|
||||||
|
)
|
||||||
|
remoteUid1 = insert(
|
||||||
|
"remote_playlists", SQLiteDatabase.CONFLICT_FAIL,
|
||||||
|
ContentValues().apply {
|
||||||
|
put("service_id", DEFAULT_SERVICE_ID)
|
||||||
|
put("url", DEFAULT_URL)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
remoteUid2 = insert(
|
||||||
|
"remote_playlists", SQLiteDatabase.CONFLICT_FAIL,
|
||||||
|
ContentValues().apply {
|
||||||
|
put("service_id", DEFAULT_SECOND_SERVICE_ID)
|
||||||
|
put("url", DEFAULT_SECOND_URL)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
delete(
|
||||||
|
"remote_playlists", "uid = ?",
|
||||||
|
Array(1) { remoteUid2 }
|
||||||
|
)
|
||||||
|
close()
|
||||||
|
}
|
||||||
|
|
||||||
|
testHelper.runMigrationsAndValidate(
|
||||||
|
AppDatabase.DATABASE_NAME,
|
||||||
|
Migrations.DB_VER_9,
|
||||||
|
true,
|
||||||
|
Migrations.MIGRATION_8_9
|
||||||
|
)
|
||||||
|
|
||||||
|
val migratedDatabaseV9 = getMigratedDatabase()
|
||||||
|
var localListFromDB = migratedDatabaseV9.playlistDAO().all.blockingFirst()
|
||||||
|
var remoteListFromDB = migratedDatabaseV9.playlistRemoteDAO().all.blockingFirst()
|
||||||
|
|
||||||
|
assertEquals(1, localListFromDB.size)
|
||||||
|
assertEquals(localUid2, localListFromDB[0].uid)
|
||||||
|
assertEquals(-1, localListFromDB[0].displayIndex)
|
||||||
|
assertEquals(1, remoteListFromDB.size)
|
||||||
|
assertEquals(remoteUid1, remoteListFromDB[0].uid)
|
||||||
|
assertEquals(-1, remoteListFromDB[0].displayIndex)
|
||||||
|
|
||||||
|
val localUid3 = migratedDatabaseV9.playlistDAO().insert(
|
||||||
|
PlaylistEntity(DEFAULT_NAME + "3", false, -1, -1)
|
||||||
|
)
|
||||||
|
val remoteUid3 = migratedDatabaseV9.playlistRemoteDAO().insert(
|
||||||
|
PlaylistRemoteEntity(
|
||||||
|
DEFAULT_THIRD_SERVICE_ID, DEFAULT_NAME, DEFAULT_THIRD_URL,
|
||||||
|
DEFAULT_THUMBNAIL, DEFAULT_UPLOADER_NAME, -1, 10
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
localListFromDB = migratedDatabaseV9.playlistDAO().all.blockingFirst()
|
||||||
|
remoteListFromDB = migratedDatabaseV9.playlistRemoteDAO().all.blockingFirst()
|
||||||
|
assertEquals(2, localListFromDB.size)
|
||||||
|
assertEquals(localUid3, localListFromDB[1].uid)
|
||||||
|
assertEquals(-1, localListFromDB[1].displayIndex)
|
||||||
|
assertEquals(2, remoteListFromDB.size)
|
||||||
|
assertEquals(remoteUid3, remoteListFromDB[1].uid)
|
||||||
|
assertEquals(-1, remoteListFromDB[1].displayIndex)
|
||||||
|
}
|
||||||
|
|
||||||
private fun getMigratedDatabase(): AppDatabase {
|
private fun getMigratedDatabase(): AppDatabase {
|
||||||
val database: AppDatabase = Room.databaseBuilder(
|
val database: AppDatabase = Room.databaseBuilder(
|
||||||
ApplicationProvider.getApplicationContext(),
|
ApplicationProvider.getApplicationContext(),
|
||||||
|
|||||||
@@ -85,7 +85,13 @@ class FeedDAOTest {
|
|||||||
|
|
||||||
private fun assertEqual(streams: List<StreamWithState>?, allowedStreams: List<StreamEntity>) {
|
private fun assertEqual(streams: List<StreamWithState>?, allowedStreams: List<StreamEntity>) {
|
||||||
assertNotNull(streams)
|
assertNotNull(streams)
|
||||||
assertEquals(allowedStreams, streams!!.stream().map { it.stream }.toList().sortedBy { it.uid })
|
assertEquals(
|
||||||
|
allowedStreams,
|
||||||
|
streams!!
|
||||||
|
.map { it.stream }
|
||||||
|
.sortedBy { it.uid }
|
||||||
|
.toList()
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setupUnlinkDelete(time: String) {
|
private fun setupUnlinkDelete(time: String) {
|
||||||
|
|||||||
@@ -64,6 +64,9 @@
|
|||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MEDIA_BUTTON" />
|
<action android:name="android.intent.action.MEDIA_BUTTON" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.media.browse.MediaBrowserService"/>
|
||||||
|
</intent-filter>
|
||||||
</service>
|
</service>
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
@@ -77,6 +80,11 @@
|
|||||||
android:exported="false"
|
android:exported="false"
|
||||||
android:label="@string/settings" />
|
android:label="@string/settings" />
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name=".settings.SettingsV2Activity"
|
||||||
|
android:exported="true"
|
||||||
|
android:label="@string/settings" />
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".about.AboutActivity"
|
android:name=".about.AboutActivity"
|
||||||
android:exported="false"
|
android:exported="false"
|
||||||
@@ -367,6 +375,7 @@
|
|||||||
<data android:host="tilvids.com" />
|
<data android:host="tilvids.com" />
|
||||||
<data android:host="video.lqdn.fr" />
|
<data android:host="video.lqdn.fr" />
|
||||||
<data android:host="video.ploud.fr" />
|
<data android:host="video.ploud.fr" />
|
||||||
|
<data android:host="subscribeto.me" />
|
||||||
|
|
||||||
<data android:pathPrefix="/videos/" /> <!-- it contains playlists -->
|
<data android:pathPrefix="/videos/" /> <!-- it contains playlists -->
|
||||||
<data android:pathPrefix="/w/" /> <!-- short video URLs -->
|
<data android:pathPrefix="/w/" /> <!-- short video URLs -->
|
||||||
@@ -423,5 +432,10 @@
|
|||||||
<meta-data
|
<meta-data
|
||||||
android:name="com.samsung.android.multidisplay.keep_process_alive"
|
android:name="com.samsung.android.multidisplay.keep_process_alive"
|
||||||
android:value="true" />
|
android:value="true" />
|
||||||
|
<!-- Android Auto -->
|
||||||
|
<meta-data android:name="com.google.android.gms.car.application"
|
||||||
|
android:resource="@xml/automotive_app_desc" />
|
||||||
|
<meta-data android:name="com.google.android.gms.car.notification.SmallIcon"
|
||||||
|
android:resource="@mipmap/ic_launcher" />
|
||||||
</application>
|
</application>
|
||||||
</manifest>
|
</manifest>
|
||||||
|
|||||||
127
app/src/main/assets/po_token.html
Normal file
127
app/src/main/assets/po_token.html
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en"><head><title></title><script>
|
||||||
|
/**
|
||||||
|
* Factory method to create and load a BotGuardClient instance.
|
||||||
|
* @param options - Configuration options for the BotGuardClient.
|
||||||
|
* @returns A promise that resolves to a loaded BotGuardClient instance.
|
||||||
|
*/
|
||||||
|
function loadBotGuard(challengeData) {
|
||||||
|
this.vm = this[challengeData.globalName];
|
||||||
|
this.program = challengeData.program;
|
||||||
|
this.vmFunctions = {};
|
||||||
|
this.syncSnapshotFunction = null;
|
||||||
|
|
||||||
|
if (!this.vm)
|
||||||
|
throw new Error('[BotGuardClient]: VM not found in the global object');
|
||||||
|
|
||||||
|
if (!this.vm.a)
|
||||||
|
throw new Error('[BotGuardClient]: Could not load program');
|
||||||
|
|
||||||
|
const vmFunctionsCallback = function (
|
||||||
|
asyncSnapshotFunction,
|
||||||
|
shutdownFunction,
|
||||||
|
passEventFunction,
|
||||||
|
checkCameraFunction
|
||||||
|
) {
|
||||||
|
this.vmFunctions = {
|
||||||
|
asyncSnapshotFunction: asyncSnapshotFunction,
|
||||||
|
shutdownFunction: shutdownFunction,
|
||||||
|
passEventFunction: passEventFunction,
|
||||||
|
checkCameraFunction: checkCameraFunction
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
this.syncSnapshotFunction = this.vm.a(this.program, vmFunctionsCallback, true, this.userInteractionElement, function () {/** no-op */ }, [ [], [] ])[0]
|
||||||
|
|
||||||
|
// an asynchronous function runs in the background and it will eventually call
|
||||||
|
// `vmFunctionsCallback`, however we need to manually tell JavaScript to pass
|
||||||
|
// control to the things running in the background by interrupting this async
|
||||||
|
// function in any way, e.g. with a delay of 1ms. The loop is most probably not
|
||||||
|
// needed but is there just because.
|
||||||
|
return new Promise(function (resolve, reject) {
|
||||||
|
i = 0
|
||||||
|
refreshIntervalId = setInterval(function () {
|
||||||
|
if (!!this.vmFunctions.asyncSnapshotFunction) {
|
||||||
|
resolve(this)
|
||||||
|
clearInterval(refreshIntervalId);
|
||||||
|
}
|
||||||
|
if (i >= 10000) {
|
||||||
|
reject("asyncSnapshotFunction is null even after 10 seconds")
|
||||||
|
clearInterval(refreshIntervalId);
|
||||||
|
}
|
||||||
|
i += 1;
|
||||||
|
}, 1);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Takes a snapshot asynchronously.
|
||||||
|
* @returns The snapshot result.
|
||||||
|
* @example
|
||||||
|
* ```ts
|
||||||
|
* const result = await botguard.snapshot({
|
||||||
|
* contentBinding: {
|
||||||
|
* c: "a=6&a2=10&b=SZWDwKVIuixOp7Y4euGTgwckbJA&c=1729143849&d=1&t=7200&c1a=1&c6a=1&c6b=1&hh=HrMb5mRWTyxGJphDr0nW2Oxonh0_wl2BDqWuLHyeKLo",
|
||||||
|
* e: "ENGAGEMENT_TYPE_VIDEO_LIKE",
|
||||||
|
* encryptedVideoId: "P-vC09ZJcnM"
|
||||||
|
* }
|
||||||
|
* });
|
||||||
|
*
|
||||||
|
* console.log(result);
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
function snapshot(args) {
|
||||||
|
return new Promise(function (resolve, reject) {
|
||||||
|
if (!this.vmFunctions.asyncSnapshotFunction)
|
||||||
|
return reject(new Error('[BotGuardClient]: Async snapshot function not found'));
|
||||||
|
|
||||||
|
this.vmFunctions.asyncSnapshotFunction(function (response) { resolve(response) }, [
|
||||||
|
args.contentBinding,
|
||||||
|
args.signedTimestamp,
|
||||||
|
args.webPoSignalOutput,
|
||||||
|
args.skipPrivacyBuffer
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function runBotGuard(challengeData) {
|
||||||
|
const interpreterJavascript = challengeData.interpreterJavascript.privateDoNotAccessOrElseSafeScriptWrappedValue;
|
||||||
|
|
||||||
|
if (interpreterJavascript) {
|
||||||
|
new Function(interpreterJavascript)();
|
||||||
|
} else throw new Error('Could not load VM');
|
||||||
|
|
||||||
|
const webPoSignalOutput = [];
|
||||||
|
return loadBotGuard({
|
||||||
|
globalName: challengeData.globalName,
|
||||||
|
globalObj: this,
|
||||||
|
program: challengeData.program
|
||||||
|
}).then(function (botguard) {
|
||||||
|
return botguard.snapshot({ webPoSignalOutput: webPoSignalOutput })
|
||||||
|
}).then(function (botguardResponse) {
|
||||||
|
return { webPoSignalOutput: webPoSignalOutput, botguardResponse: botguardResponse }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function obtainPoToken(webPoSignalOutput, integrityToken, identifier) {
|
||||||
|
const getMinter = webPoSignalOutput[0];
|
||||||
|
|
||||||
|
if (!getMinter)
|
||||||
|
throw new Error('PMD:Undefined');
|
||||||
|
|
||||||
|
const mintCallback = getMinter(integrityToken);
|
||||||
|
|
||||||
|
if (!(mintCallback instanceof Function))
|
||||||
|
throw new Error('APF:Failed');
|
||||||
|
|
||||||
|
const result = mintCallback(identifier);
|
||||||
|
|
||||||
|
if (!result)
|
||||||
|
throw new Error('YNJ:Undefined');
|
||||||
|
|
||||||
|
if (!(result instanceof Uint8Array))
|
||||||
|
throw new Error('ODM:Invalid');
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
</script></head><body></body></html>
|
||||||
@@ -25,6 +25,7 @@ import android.view.ViewGroup;
|
|||||||
import androidx.annotation.IntDef;
|
import androidx.annotation.IntDef;
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.core.os.BundleCompat;
|
||||||
import androidx.lifecycle.Lifecycle;
|
import androidx.lifecycle.Lifecycle;
|
||||||
import androidx.viewpager.widget.PagerAdapter;
|
import androidx.viewpager.widget.PagerAdapter;
|
||||||
|
|
||||||
@@ -284,7 +285,7 @@ public abstract class FragmentStatePagerAdapterMenuWorkaround extends PagerAdapt
|
|||||||
Bundle state = null;
|
Bundle state = null;
|
||||||
if (!mSavedState.isEmpty()) {
|
if (!mSavedState.isEmpty()) {
|
||||||
state = new Bundle();
|
state = new Bundle();
|
||||||
state.putParcelableArray("states", mSavedState.toArray(new Fragment.SavedState[0]));
|
state.putParcelableArrayList("states", mSavedState);
|
||||||
}
|
}
|
||||||
for (int i = 0; i < mFragments.size(); i++) {
|
for (int i = 0; i < mFragments.size(); i++) {
|
||||||
final Fragment f = mFragments.get(i);
|
final Fragment f = mFragments.get(i);
|
||||||
@@ -311,13 +312,12 @@ public abstract class FragmentStatePagerAdapterMenuWorkaround extends PagerAdapt
|
|||||||
if (state != null) {
|
if (state != null) {
|
||||||
final Bundle bundle = (Bundle) state;
|
final Bundle bundle = (Bundle) state;
|
||||||
bundle.setClassLoader(loader);
|
bundle.setClassLoader(loader);
|
||||||
final Parcelable[] fss = bundle.getParcelableArray("states");
|
final var states = BundleCompat.getParcelableArrayList(bundle, "states",
|
||||||
|
Fragment.SavedState.class);
|
||||||
mSavedState.clear();
|
mSavedState.clear();
|
||||||
mFragments.clear();
|
mFragments.clear();
|
||||||
if (fss != null) {
|
if (states != null) {
|
||||||
for (final Parcelable parcelable : fss) {
|
mSavedState.addAll(states);
|
||||||
mSavedState.add((Fragment.SavedState) parcelable);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
final Iterable<String> keys = bundle.keySet();
|
final Iterable<String> keys = bundle.keySet();
|
||||||
for (final String key : keys) {
|
for (final String key : keys) {
|
||||||
|
|||||||
@@ -1,258 +0,0 @@
|
|||||||
package org.schabi.newpipe;
|
|
||||||
|
|
||||||
import android.app.Application;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.SharedPreferences;
|
|
||||||
import android.util.Log;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.core.app.NotificationChannelCompat;
|
|
||||||
import androidx.core.app.NotificationManagerCompat;
|
|
||||||
import androidx.preference.PreferenceManager;
|
|
||||||
|
|
||||||
import com.jakewharton.processphoenix.ProcessPhoenix;
|
|
||||||
|
|
||||||
import org.acra.ACRA;
|
|
||||||
import org.acra.config.CoreConfigurationBuilder;
|
|
||||||
import org.schabi.newpipe.error.ReCaptchaActivity;
|
|
||||||
import org.schabi.newpipe.extractor.NewPipe;
|
|
||||||
import org.schabi.newpipe.extractor.downloader.Downloader;
|
|
||||||
import org.schabi.newpipe.ktx.ExceptionUtils;
|
|
||||||
import org.schabi.newpipe.settings.NewPipeSettings;
|
|
||||||
import org.schabi.newpipe.util.Localization;
|
|
||||||
import org.schabi.newpipe.util.image.ImageStrategy;
|
|
||||||
import org.schabi.newpipe.util.image.PicassoHelper;
|
|
||||||
import org.schabi.newpipe.util.ServiceHelper;
|
|
||||||
import org.schabi.newpipe.util.StateSaver;
|
|
||||||
import org.schabi.newpipe.util.image.PreferredImageQuality;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InterruptedIOException;
|
|
||||||
import java.net.SocketException;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Objects;
|
|
||||||
|
|
||||||
import io.reactivex.rxjava3.exceptions.CompositeException;
|
|
||||||
import io.reactivex.rxjava3.exceptions.MissingBackpressureException;
|
|
||||||
import io.reactivex.rxjava3.exceptions.OnErrorNotImplementedException;
|
|
||||||
import io.reactivex.rxjava3.exceptions.UndeliverableException;
|
|
||||||
import io.reactivex.rxjava3.functions.Consumer;
|
|
||||||
import io.reactivex.rxjava3.plugins.RxJavaPlugins;
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Copyright (C) Hans-Christoph Steiner 2016 <hans@eds.org>
|
|
||||||
* App.java is part of NewPipe.
|
|
||||||
*
|
|
||||||
* NewPipe is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* NewPipe is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
public class App extends Application {
|
|
||||||
public static final String PACKAGE_NAME = BuildConfig.APPLICATION_ID;
|
|
||||||
private static final String TAG = App.class.toString();
|
|
||||||
private static App app;
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
public static App getApp() {
|
|
||||||
return app;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void attachBaseContext(final Context base) {
|
|
||||||
super.attachBaseContext(base);
|
|
||||||
initACRA();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onCreate() {
|
|
||||||
super.onCreate();
|
|
||||||
|
|
||||||
app = this;
|
|
||||||
|
|
||||||
if (ProcessPhoenix.isPhoenixProcess(this)) {
|
|
||||||
Log.i(TAG, "This is a phoenix process! "
|
|
||||||
+ "Aborting initialization of App[onCreate]");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize settings first because others inits can use its values
|
|
||||||
NewPipeSettings.initSettings(this);
|
|
||||||
|
|
||||||
NewPipe.init(getDownloader(),
|
|
||||||
Localization.getPreferredLocalization(this),
|
|
||||||
Localization.getPreferredContentCountry(this));
|
|
||||||
Localization.initPrettyTime(Localization.resolvePrettyTime(getApplicationContext()));
|
|
||||||
|
|
||||||
StateSaver.init(this);
|
|
||||||
initNotificationChannels();
|
|
||||||
|
|
||||||
ServiceHelper.initServices(this);
|
|
||||||
|
|
||||||
// Initialize image loader
|
|
||||||
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
|
|
||||||
PicassoHelper.init(this);
|
|
||||||
ImageStrategy.setPreferredImageQuality(PreferredImageQuality.fromPreferenceKey(this,
|
|
||||||
prefs.getString(getString(R.string.image_quality_key),
|
|
||||||
getString(R.string.image_quality_default))));
|
|
||||||
PicassoHelper.setIndicatorsEnabled(MainActivity.DEBUG
|
|
||||||
&& prefs.getBoolean(getString(R.string.show_image_indicators_key), false));
|
|
||||||
|
|
||||||
configureRxJavaErrorHandler();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onTerminate() {
|
|
||||||
super.onTerminate();
|
|
||||||
PicassoHelper.terminate();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected Downloader getDownloader() {
|
|
||||||
final DownloaderImpl downloader = DownloaderImpl.init(null);
|
|
||||||
setCookiesToDownloader(downloader);
|
|
||||||
return downloader;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void setCookiesToDownloader(final DownloaderImpl downloader) {
|
|
||||||
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(
|
|
||||||
getApplicationContext());
|
|
||||||
final String key = getApplicationContext().getString(R.string.recaptcha_cookies_key);
|
|
||||||
downloader.setCookie(ReCaptchaActivity.RECAPTCHA_COOKIES_KEY, prefs.getString(key, null));
|
|
||||||
downloader.updateYoutubeRestrictedModeCookies(getApplicationContext());
|
|
||||||
}
|
|
||||||
|
|
||||||
private void configureRxJavaErrorHandler() {
|
|
||||||
// https://github.com/ReactiveX/RxJava/wiki/What's-different-in-2.0#error-handling
|
|
||||||
RxJavaPlugins.setErrorHandler(new Consumer<Throwable>() {
|
|
||||||
@Override
|
|
||||||
public void accept(@NonNull final Throwable throwable) {
|
|
||||||
Log.e(TAG, "RxJavaPlugins.ErrorHandler called with -> : "
|
|
||||||
+ "throwable = [" + throwable.getClass().getName() + "]");
|
|
||||||
|
|
||||||
final Throwable actualThrowable;
|
|
||||||
if (throwable instanceof UndeliverableException) {
|
|
||||||
// As UndeliverableException is a wrapper,
|
|
||||||
// get the cause of it to get the "real" exception
|
|
||||||
actualThrowable = Objects.requireNonNull(throwable.getCause());
|
|
||||||
} else {
|
|
||||||
actualThrowable = throwable;
|
|
||||||
}
|
|
||||||
|
|
||||||
final List<Throwable> errors;
|
|
||||||
if (actualThrowable instanceof CompositeException) {
|
|
||||||
errors = ((CompositeException) actualThrowable).getExceptions();
|
|
||||||
} else {
|
|
||||||
errors = List.of(actualThrowable);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (final Throwable error : errors) {
|
|
||||||
if (isThrowableIgnored(error)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (isThrowableCritical(error)) {
|
|
||||||
reportException(error);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Out-of-lifecycle exceptions should only be reported if a debug user wishes so,
|
|
||||||
// When exception is not reported, log it
|
|
||||||
if (isDisposedRxExceptionsReported()) {
|
|
||||||
reportException(actualThrowable);
|
|
||||||
} else {
|
|
||||||
Log.e(TAG, "RxJavaPlugin: Undeliverable Exception received: ", actualThrowable);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isThrowableIgnored(@NonNull final Throwable throwable) {
|
|
||||||
// Don't crash the application over a simple network problem
|
|
||||||
return ExceptionUtils.hasAssignableCause(throwable,
|
|
||||||
// network api cancellation
|
|
||||||
IOException.class, SocketException.class,
|
|
||||||
// blocking code disposed
|
|
||||||
InterruptedException.class, InterruptedIOException.class);
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isThrowableCritical(@NonNull final Throwable throwable) {
|
|
||||||
// Though these exceptions cannot be ignored
|
|
||||||
return ExceptionUtils.hasAssignableCause(throwable,
|
|
||||||
NullPointerException.class, IllegalArgumentException.class, // bug in app
|
|
||||||
OnErrorNotImplementedException.class, MissingBackpressureException.class,
|
|
||||||
IllegalStateException.class); // bug in operator
|
|
||||||
}
|
|
||||||
|
|
||||||
private void reportException(@NonNull final Throwable throwable) {
|
|
||||||
// Throw uncaught exception that will trigger the report system
|
|
||||||
Thread.currentThread().getUncaughtExceptionHandler()
|
|
||||||
.uncaughtException(Thread.currentThread(), throwable);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called in {@link #attachBaseContext(Context)} after calling the {@code super} method.
|
|
||||||
* Should be overridden if MultiDex is enabled, since it has to be initialized before ACRA.
|
|
||||||
*/
|
|
||||||
protected void initACRA() {
|
|
||||||
if (ACRA.isACRASenderServiceProcess()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final CoreConfigurationBuilder acraConfig = new CoreConfigurationBuilder()
|
|
||||||
.withBuildConfigClass(BuildConfig.class);
|
|
||||||
ACRA.init(this, acraConfig);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void initNotificationChannels() {
|
|
||||||
// Keep the importance below DEFAULT to avoid making noise on every notification update for
|
|
||||||
// the main and update channels
|
|
||||||
final List<NotificationChannelCompat> notificationChannelCompats = List.of(
|
|
||||||
new NotificationChannelCompat.Builder(getString(R.string.notification_channel_id),
|
|
||||||
NotificationManagerCompat.IMPORTANCE_LOW)
|
|
||||||
.setName(getString(R.string.notification_channel_name))
|
|
||||||
.setDescription(getString(R.string.notification_channel_description))
|
|
||||||
.build(),
|
|
||||||
new NotificationChannelCompat
|
|
||||||
.Builder(getString(R.string.app_update_notification_channel_id),
|
|
||||||
NotificationManagerCompat.IMPORTANCE_LOW)
|
|
||||||
.setName(getString(R.string.app_update_notification_channel_name))
|
|
||||||
.setDescription(
|
|
||||||
getString(R.string.app_update_notification_channel_description))
|
|
||||||
.build(),
|
|
||||||
new NotificationChannelCompat.Builder(getString(R.string.hash_channel_id),
|
|
||||||
NotificationManagerCompat.IMPORTANCE_HIGH)
|
|
||||||
.setName(getString(R.string.hash_channel_name))
|
|
||||||
.setDescription(getString(R.string.hash_channel_description))
|
|
||||||
.build(),
|
|
||||||
new NotificationChannelCompat.Builder(getString(R.string.error_report_channel_id),
|
|
||||||
NotificationManagerCompat.IMPORTANCE_LOW)
|
|
||||||
.setName(getString(R.string.error_report_channel_name))
|
|
||||||
.setDescription(getString(R.string.error_report_channel_description))
|
|
||||||
.build(),
|
|
||||||
new NotificationChannelCompat
|
|
||||||
.Builder(getString(R.string.streams_notification_channel_id),
|
|
||||||
NotificationManagerCompat.IMPORTANCE_DEFAULT)
|
|
||||||
.setName(getString(R.string.streams_notification_channel_name))
|
|
||||||
.setDescription(
|
|
||||||
getString(R.string.streams_notification_channel_description))
|
|
||||||
.build()
|
|
||||||
);
|
|
||||||
|
|
||||||
final NotificationManagerCompat notificationManager = NotificationManagerCompat.from(this);
|
|
||||||
notificationManager.createNotificationChannelsCompat(notificationChannelCompats);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected boolean isDisposedRxExceptionsReported() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
290
app/src/main/java/org/schabi/newpipe/App.kt
Normal file
290
app/src/main/java/org/schabi/newpipe/App.kt
Normal file
@@ -0,0 +1,290 @@
|
|||||||
|
package org.schabi.newpipe
|
||||||
|
|
||||||
|
import android.app.ActivityManager
|
||||||
|
import android.app.Application
|
||||||
|
import android.content.Context
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.core.app.NotificationChannelCompat
|
||||||
|
import androidx.core.app.NotificationManagerCompat
|
||||||
|
import androidx.core.content.getSystemService
|
||||||
|
import androidx.preference.PreferenceManager
|
||||||
|
import coil3.ImageLoader
|
||||||
|
import coil3.SingletonImageLoader
|
||||||
|
import coil3.network.okhttp.OkHttpNetworkFetcherFactory
|
||||||
|
import coil3.request.allowRgb565
|
||||||
|
import coil3.request.crossfade
|
||||||
|
import coil3.util.DebugLogger
|
||||||
|
import com.jakewharton.processphoenix.ProcessPhoenix
|
||||||
|
import dagger.hilt.android.HiltAndroidApp
|
||||||
|
import io.reactivex.rxjava3.exceptions.CompositeException
|
||||||
|
import io.reactivex.rxjava3.exceptions.MissingBackpressureException
|
||||||
|
import io.reactivex.rxjava3.exceptions.OnErrorNotImplementedException
|
||||||
|
import io.reactivex.rxjava3.exceptions.UndeliverableException
|
||||||
|
import io.reactivex.rxjava3.functions.Consumer
|
||||||
|
import io.reactivex.rxjava3.plugins.RxJavaPlugins
|
||||||
|
import org.acra.ACRA.init
|
||||||
|
import org.acra.ACRA.isACRASenderServiceProcess
|
||||||
|
import org.acra.config.CoreConfigurationBuilder
|
||||||
|
import org.schabi.newpipe.error.ReCaptchaActivity
|
||||||
|
import org.schabi.newpipe.extractor.NewPipe
|
||||||
|
import org.schabi.newpipe.extractor.downloader.Downloader
|
||||||
|
import org.schabi.newpipe.extractor.services.youtube.extractors.YoutubeStreamExtractor
|
||||||
|
import org.schabi.newpipe.ktx.hasAssignableCause
|
||||||
|
import org.schabi.newpipe.settings.NewPipeSettings
|
||||||
|
import org.schabi.newpipe.util.BridgeStateSaverInitializer
|
||||||
|
import org.schabi.newpipe.util.Localization
|
||||||
|
import org.schabi.newpipe.util.ServiceHelper
|
||||||
|
import org.schabi.newpipe.util.StateSaver
|
||||||
|
import org.schabi.newpipe.util.image.ImageStrategy
|
||||||
|
import org.schabi.newpipe.util.image.PreferredImageQuality
|
||||||
|
import org.schabi.newpipe.util.potoken.PoTokenProviderImpl
|
||||||
|
import java.io.IOException
|
||||||
|
import java.io.InterruptedIOException
|
||||||
|
import java.net.SocketException
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Copyright (C) Hans-Christoph Steiner 2016 <hans@eds.org>
|
||||||
|
* App.kt is part of NewPipe.
|
||||||
|
*
|
||||||
|
* NewPipe is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* NewPipe is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
@HiltAndroidApp
|
||||||
|
open class App :
|
||||||
|
Application(),
|
||||||
|
SingletonImageLoader.Factory {
|
||||||
|
var isFirstRun = false
|
||||||
|
private set
|
||||||
|
|
||||||
|
override fun attachBaseContext(base: Context?) {
|
||||||
|
super.attachBaseContext(base)
|
||||||
|
initACRA()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreate() {
|
||||||
|
super.onCreate()
|
||||||
|
|
||||||
|
instance = this
|
||||||
|
|
||||||
|
if (ProcessPhoenix.isPhoenixProcess(this)) {
|
||||||
|
Log.i(TAG, "This is a phoenix process! Aborting initialization of App[onCreate]")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if the last used preference version is set
|
||||||
|
// to determine whether this is the first app run
|
||||||
|
val lastUsedPrefVersion =
|
||||||
|
PreferenceManager
|
||||||
|
.getDefaultSharedPreferences(this)
|
||||||
|
.getInt(getString(R.string.last_used_preferences_version), -1)
|
||||||
|
isFirstRun = lastUsedPrefVersion == -1
|
||||||
|
|
||||||
|
// Initialize settings first because other initializations can use its values
|
||||||
|
NewPipeSettings.initSettings(this)
|
||||||
|
|
||||||
|
NewPipe.init(
|
||||||
|
getDownloader(),
|
||||||
|
Localization.getPreferredLocalization(this),
|
||||||
|
Localization.getPreferredContentCountry(this),
|
||||||
|
)
|
||||||
|
Localization.initPrettyTime(Localization.resolvePrettyTime(this))
|
||||||
|
|
||||||
|
BridgeStateSaverInitializer.init(this)
|
||||||
|
StateSaver.init(this)
|
||||||
|
initNotificationChannels()
|
||||||
|
|
||||||
|
ServiceHelper.initServices(this)
|
||||||
|
|
||||||
|
// Initialize image loader
|
||||||
|
val prefs = PreferenceManager.getDefaultSharedPreferences(this)
|
||||||
|
ImageStrategy.setPreferredImageQuality(
|
||||||
|
PreferredImageQuality.fromPreferenceKey(
|
||||||
|
this,
|
||||||
|
prefs.getString(
|
||||||
|
getString(R.string.image_quality_key),
|
||||||
|
getString(R.string.image_quality_default),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
configureRxJavaErrorHandler()
|
||||||
|
|
||||||
|
YoutubeStreamExtractor.setPoTokenProvider(PoTokenProviderImpl)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun newImageLoader(context: Context): ImageLoader =
|
||||||
|
ImageLoader
|
||||||
|
.Builder(this)
|
||||||
|
.logger(if (BuildConfig.DEBUG) DebugLogger() else null)
|
||||||
|
.allowRgb565(getSystemService<ActivityManager>()!!.isLowRamDevice)
|
||||||
|
.crossfade(true)
|
||||||
|
.components {
|
||||||
|
add(OkHttpNetworkFetcherFactory(callFactory = DownloaderImpl.getInstance().client))
|
||||||
|
}.build()
|
||||||
|
|
||||||
|
protected open fun getDownloader(): Downloader {
|
||||||
|
val downloader = DownloaderImpl.init(null)
|
||||||
|
setCookiesToDownloader(downloader)
|
||||||
|
return downloader
|
||||||
|
}
|
||||||
|
|
||||||
|
protected fun setCookiesToDownloader(downloader: DownloaderImpl) {
|
||||||
|
val prefs = PreferenceManager.getDefaultSharedPreferences(this)
|
||||||
|
val key = getString(R.string.recaptcha_cookies_key)
|
||||||
|
downloader.setCookie(ReCaptchaActivity.RECAPTCHA_COOKIES_KEY, prefs.getString(key, null))
|
||||||
|
downloader.updateYoutubeRestrictedModeCookies(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun configureRxJavaErrorHandler() {
|
||||||
|
// https://github.com/ReactiveX/RxJava/wiki/What's-different-in-2.0#error-handling
|
||||||
|
RxJavaPlugins.setErrorHandler(
|
||||||
|
object : Consumer<Throwable> {
|
||||||
|
override fun accept(throwable: Throwable) {
|
||||||
|
Log.e(TAG, "RxJavaPlugins.ErrorHandler called with -> : throwable = [${throwable.javaClass.getName()}]")
|
||||||
|
|
||||||
|
// As UndeliverableException is a wrapper,
|
||||||
|
// get the cause of it to get the "real" exception
|
||||||
|
val actualThrowable = (throwable as? UndeliverableException)?.cause ?: throwable
|
||||||
|
|
||||||
|
val errors = (actualThrowable as? CompositeException)?.exceptions ?: listOf(actualThrowable)
|
||||||
|
|
||||||
|
for (error in errors) {
|
||||||
|
if (isThrowableIgnored(error)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (isThrowableCritical(error)) {
|
||||||
|
reportException(error)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Out-of-lifecycle exceptions should only be reported if a debug user wishes so,
|
||||||
|
// When exception is not reported, log it
|
||||||
|
if (isDisposedRxExceptionsReported()) {
|
||||||
|
reportException(actualThrowable)
|
||||||
|
} else {
|
||||||
|
Log.e(TAG, "RxJavaPlugin: Undeliverable Exception received: ", actualThrowable)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isThrowableIgnored(throwable: Throwable): Boolean {
|
||||||
|
// Don't crash the application over a simple network problem
|
||||||
|
return throwable // network api cancellation
|
||||||
|
.hasAssignableCause(
|
||||||
|
IOException::class.java,
|
||||||
|
SocketException::class.java, // blocking code disposed
|
||||||
|
InterruptedException::class.java,
|
||||||
|
InterruptedIOException::class.java,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isThrowableCritical(throwable: Throwable): Boolean {
|
||||||
|
// Though these exceptions cannot be ignored
|
||||||
|
return throwable
|
||||||
|
.hasAssignableCause(
|
||||||
|
// bug in app
|
||||||
|
NullPointerException::class.java,
|
||||||
|
IllegalArgumentException::class.java,
|
||||||
|
OnErrorNotImplementedException::class.java,
|
||||||
|
MissingBackpressureException::class.java,
|
||||||
|
// bug in operator
|
||||||
|
IllegalStateException::class.java,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun reportException(throwable: Throwable) {
|
||||||
|
// Throw uncaught exception that will trigger the report system
|
||||||
|
Thread
|
||||||
|
.currentThread()
|
||||||
|
.uncaughtExceptionHandler
|
||||||
|
.uncaughtException(Thread.currentThread(), throwable)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called in [.attachBaseContext] after calling the `super` method.
|
||||||
|
* Should be overridden if MultiDex is enabled, since it has to be initialized before ACRA.
|
||||||
|
*/
|
||||||
|
protected fun initACRA() {
|
||||||
|
if (isACRASenderServiceProcess()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val acraConfig =
|
||||||
|
CoreConfigurationBuilder()
|
||||||
|
.withBuildConfigClass(BuildConfig::class.java)
|
||||||
|
init(this, acraConfig)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initNotificationChannels() {
|
||||||
|
// Keep the importance below DEFAULT to avoid making noise on every notification update for
|
||||||
|
// the main and update channels
|
||||||
|
val mainChannel =
|
||||||
|
NotificationChannelCompat
|
||||||
|
.Builder(
|
||||||
|
getString(R.string.notification_channel_id),
|
||||||
|
NotificationManagerCompat.IMPORTANCE_LOW,
|
||||||
|
).setName(getString(R.string.notification_channel_name))
|
||||||
|
.setDescription(getString(R.string.notification_channel_description))
|
||||||
|
.build()
|
||||||
|
val appUpdateChannel =
|
||||||
|
NotificationChannelCompat
|
||||||
|
.Builder(
|
||||||
|
getString(R.string.app_update_notification_channel_id),
|
||||||
|
NotificationManagerCompat.IMPORTANCE_LOW,
|
||||||
|
).setName(getString(R.string.app_update_notification_channel_name))
|
||||||
|
.setDescription(getString(R.string.app_update_notification_channel_description))
|
||||||
|
.build()
|
||||||
|
val hashChannel =
|
||||||
|
NotificationChannelCompat
|
||||||
|
.Builder(
|
||||||
|
getString(R.string.hash_channel_id),
|
||||||
|
NotificationManagerCompat.IMPORTANCE_HIGH,
|
||||||
|
).setName(getString(R.string.hash_channel_name))
|
||||||
|
.setDescription(getString(R.string.hash_channel_description))
|
||||||
|
.build()
|
||||||
|
val errorReportChannel =
|
||||||
|
NotificationChannelCompat
|
||||||
|
.Builder(
|
||||||
|
getString(R.string.error_report_channel_id),
|
||||||
|
NotificationManagerCompat.IMPORTANCE_LOW,
|
||||||
|
).setName(getString(R.string.error_report_channel_name))
|
||||||
|
.setDescription(getString(R.string.error_report_channel_description))
|
||||||
|
.build()
|
||||||
|
val newStreamChannel =
|
||||||
|
NotificationChannelCompat
|
||||||
|
.Builder(
|
||||||
|
getString(R.string.streams_notification_channel_id),
|
||||||
|
NotificationManagerCompat.IMPORTANCE_DEFAULT,
|
||||||
|
).setName(getString(R.string.streams_notification_channel_name))
|
||||||
|
.setDescription(getString(R.string.streams_notification_channel_description))
|
||||||
|
.build()
|
||||||
|
|
||||||
|
val channels = listOf(mainChannel, appUpdateChannel, hashChannel, errorReportChannel, newStreamChannel)
|
||||||
|
|
||||||
|
NotificationManagerCompat.from(this).createNotificationChannelsCompat(channels)
|
||||||
|
}
|
||||||
|
|
||||||
|
protected open fun isDisposedRxExceptionsReported(): Boolean = false
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val PACKAGE_NAME: String = BuildConfig.APPLICATION_ID
|
||||||
|
private val TAG = App::class.java.toString()
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
lateinit var instance: App
|
||||||
|
private set
|
||||||
|
}
|
||||||
|
}
|
||||||
22
app/src/main/java/org/schabi/newpipe/AppModule.kt
Normal file
22
app/src/main/java/org/schabi/newpipe/AppModule.kt
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
package org.schabi.newpipe
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.SharedPreferences
|
||||||
|
import androidx.preference.PreferenceManager
|
||||||
|
import dagger.Module
|
||||||
|
import dagger.Provides
|
||||||
|
import dagger.hilt.InstallIn
|
||||||
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
|
import dagger.hilt.components.SingletonComponent
|
||||||
|
import javax.inject.Singleton
|
||||||
|
|
||||||
|
@Module
|
||||||
|
@InstallIn(SingletonComponent::class)
|
||||||
|
class AppModule {
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
fun providesSharedPreference(@ApplicationContext context: Context): SharedPreferences {
|
||||||
|
return PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,8 +10,9 @@ import androidx.appcompat.app.AppCompatActivity;
|
|||||||
import androidx.fragment.app.Fragment;
|
import androidx.fragment.app.Fragment;
|
||||||
import androidx.fragment.app.FragmentManager;
|
import androidx.fragment.app.FragmentManager;
|
||||||
|
|
||||||
import icepick.Icepick;
|
import com.evernote.android.state.State;
|
||||||
import icepick.State;
|
import com.livefront.bridge.Bridge;
|
||||||
|
|
||||||
|
|
||||||
public abstract class BaseFragment extends Fragment {
|
public abstract class BaseFragment extends Fragment {
|
||||||
protected final String TAG = getClass().getSimpleName() + "@" + Integer.toHexString(hashCode());
|
protected final String TAG = getClass().getSimpleName() + "@" + Integer.toHexString(hashCode());
|
||||||
@@ -48,7 +49,7 @@ public abstract class BaseFragment extends Fragment {
|
|||||||
+ "savedInstanceState = [" + savedInstanceState + "]");
|
+ "savedInstanceState = [" + savedInstanceState + "]");
|
||||||
}
|
}
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
Icepick.restoreInstanceState(this, savedInstanceState);
|
Bridge.restoreInstanceState(this, savedInstanceState);
|
||||||
if (savedInstanceState != null) {
|
if (savedInstanceState != null) {
|
||||||
onRestoreInstanceState(savedInstanceState);
|
onRestoreInstanceState(savedInstanceState);
|
||||||
}
|
}
|
||||||
@@ -70,7 +71,7 @@ public abstract class BaseFragment extends Fragment {
|
|||||||
@Override
|
@Override
|
||||||
public void onSaveInstanceState(@NonNull final Bundle outState) {
|
public void onSaveInstanceState(@NonNull final Bundle outState) {
|
||||||
super.onSaveInstanceState(outState);
|
super.onSaveInstanceState(outState);
|
||||||
Icepick.saveInstanceState(this, outState);
|
Bridge.saveInstanceState(this, outState);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void onRestoreInstanceState(@NonNull final Bundle savedInstanceState) {
|
protected void onRestoreInstanceState(@NonNull final Bundle savedInstanceState) {
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ import okhttp3.ResponseBody;
|
|||||||
|
|
||||||
public final class DownloaderImpl extends Downloader {
|
public final class DownloaderImpl extends Downloader {
|
||||||
public static final String USER_AGENT =
|
public static final String USER_AGENT =
|
||||||
"Mozilla/5.0 (Windows NT 10.0; rv:91.0) Gecko/20100101 Firefox/91.0";
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:128.0) Gecko/20100101 Firefox/128.0";
|
||||||
public static final String YOUTUBE_RESTRICTED_MODE_COOKIE_KEY =
|
public static final String YOUTUBE_RESTRICTED_MODE_COOKIE_KEY =
|
||||||
"youtube_restricted_mode_key";
|
"youtube_restricted_mode_key";
|
||||||
public static final String YOUTUBE_RESTRICTED_MODE_COOKIE = "PREF=f2=8000000";
|
public static final String YOUTUBE_RESTRICTED_MODE_COOKIE = "PREF=f2=8000000";
|
||||||
@@ -48,6 +48,11 @@ public final class DownloaderImpl extends Downloader {
|
|||||||
this.mCookies = new HashMap<>();
|
this.mCookies = new HashMap<>();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public OkHttpClient getClient() {
|
||||||
|
return client;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* It's recommended to call exactly once in the entire lifetime of the application.
|
* It's recommended to call exactly once in the entire lifetime of the application.
|
||||||
*
|
*
|
||||||
@@ -137,7 +142,8 @@ public final class DownloaderImpl extends Downloader {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final okhttp3.Request.Builder requestBuilder = new okhttp3.Request.Builder()
|
final okhttp3.Request.Builder requestBuilder = new okhttp3.Request.Builder()
|
||||||
.method(httpMethod, requestBody).url(url)
|
.method(httpMethod, requestBody)
|
||||||
|
.url(url)
|
||||||
.addHeader("User-Agent", USER_AGENT);
|
.addHeader("User-Agent", USER_AGENT);
|
||||||
|
|
||||||
final String cookies = getCookies(url);
|
final String cookies = getCookies(url);
|
||||||
@@ -145,38 +151,33 @@ public final class DownloaderImpl extends Downloader {
|
|||||||
requestBuilder.addHeader("Cookie", cookies);
|
requestBuilder.addHeader("Cookie", cookies);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (final Map.Entry<String, List<String>> pair : headers.entrySet()) {
|
headers.forEach((headerName, headerValueList) -> {
|
||||||
final String headerName = pair.getKey();
|
requestBuilder.removeHeader(headerName);
|
||||||
final List<String> headerValueList = pair.getValue();
|
headerValueList.forEach(headerValue ->
|
||||||
|
requestBuilder.addHeader(headerName, headerValue));
|
||||||
|
});
|
||||||
|
|
||||||
if (headerValueList.size() > 1) {
|
try (
|
||||||
requestBuilder.removeHeader(headerName);
|
okhttp3.Response response = client.newCall(requestBuilder.build()).execute()
|
||||||
for (final String headerValue : headerValueList) {
|
) {
|
||||||
requestBuilder.addHeader(headerName, headerValue);
|
if (response.code() == 429) {
|
||||||
}
|
throw new ReCaptchaException("reCaptcha Challenge requested", url);
|
||||||
} else if (headerValueList.size() == 1) {
|
|
||||||
requestBuilder.header(headerName, headerValueList.get(0));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String responseBodyToReturn = null;
|
||||||
|
try (ResponseBody body = response.body()) {
|
||||||
|
if (body != null) {
|
||||||
|
responseBodyToReturn = body.string();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final String latestUrl = response.request().url().toString();
|
||||||
|
return new Response(
|
||||||
|
response.code(),
|
||||||
|
response.message(),
|
||||||
|
response.headers().toMultimap(),
|
||||||
|
responseBodyToReturn,
|
||||||
|
latestUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
final okhttp3.Response response = client.newCall(requestBuilder.build()).execute();
|
|
||||||
|
|
||||||
if (response.code() == 429) {
|
|
||||||
response.close();
|
|
||||||
|
|
||||||
throw new ReCaptchaException("reCaptcha Challenge requested", url);
|
|
||||||
}
|
|
||||||
|
|
||||||
final ResponseBody body = response.body();
|
|
||||||
String responseBodyToReturn = null;
|
|
||||||
|
|
||||||
if (body != null) {
|
|
||||||
responseBodyToReturn = body.string();
|
|
||||||
}
|
|
||||||
|
|
||||||
final String latestUrl = response.request().url().toString();
|
|
||||||
return new Response(response.code(), response.message(), response.headers().toMultimap(),
|
|
||||||
responseBodyToReturn, latestUrl);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ import android.view.Menu;
|
|||||||
import android.view.MenuItem;
|
import android.view.MenuItem;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
|
import android.webkit.WebView;
|
||||||
import android.widget.AdapterView;
|
import android.widget.AdapterView;
|
||||||
import android.widget.ArrayAdapter;
|
import android.widget.ArrayAdapter;
|
||||||
import android.widget.FrameLayout;
|
import android.widget.FrameLayout;
|
||||||
@@ -75,6 +76,7 @@ import org.schabi.newpipe.player.Player;
|
|||||||
import org.schabi.newpipe.player.event.OnKeyDownListener;
|
import org.schabi.newpipe.player.event.OnKeyDownListener;
|
||||||
import org.schabi.newpipe.player.helper.PlayerHolder;
|
import org.schabi.newpipe.player.helper.PlayerHolder;
|
||||||
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
||||||
|
import org.schabi.newpipe.settings.UpdateSettingsFragment;
|
||||||
import org.schabi.newpipe.util.Constants;
|
import org.schabi.newpipe.util.Constants;
|
||||||
import org.schabi.newpipe.util.DeviceUtils;
|
import org.schabi.newpipe.util.DeviceUtils;
|
||||||
import org.schabi.newpipe.util.KioskTranslator;
|
import org.schabi.newpipe.util.KioskTranslator;
|
||||||
@@ -82,10 +84,12 @@ import org.schabi.newpipe.util.Localization;
|
|||||||
import org.schabi.newpipe.util.NavigationHelper;
|
import org.schabi.newpipe.util.NavigationHelper;
|
||||||
import org.schabi.newpipe.util.PeertubeHelper;
|
import org.schabi.newpipe.util.PeertubeHelper;
|
||||||
import org.schabi.newpipe.util.PermissionHelper;
|
import org.schabi.newpipe.util.PermissionHelper;
|
||||||
|
import org.schabi.newpipe.util.ReleaseVersionUtil;
|
||||||
import org.schabi.newpipe.util.SerializedCache;
|
import org.schabi.newpipe.util.SerializedCache;
|
||||||
import org.schabi.newpipe.util.ServiceHelper;
|
import org.schabi.newpipe.util.ServiceHelper;
|
||||||
import org.schabi.newpipe.util.StateSaver;
|
import org.schabi.newpipe.util.StateSaver;
|
||||||
import org.schabi.newpipe.util.ThemeHelper;
|
import org.schabi.newpipe.util.ThemeHelper;
|
||||||
|
import org.schabi.newpipe.util.external_communication.ShareUtils;
|
||||||
import org.schabi.newpipe.views.FocusOverlayView;
|
import org.schabi.newpipe.views.FocusOverlayView;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
@@ -114,7 +118,8 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
private static final int ITEM_ID_DOWNLOADS = -4;
|
private static final int ITEM_ID_DOWNLOADS = -4;
|
||||||
private static final int ITEM_ID_HISTORY = -5;
|
private static final int ITEM_ID_HISTORY = -5;
|
||||||
private static final int ITEM_ID_SETTINGS = 0;
|
private static final int ITEM_ID_SETTINGS = 0;
|
||||||
private static final int ITEM_ID_ABOUT = 1;
|
private static final int ITEM_ID_DONATION = 1;
|
||||||
|
private static final int ITEM_ID_ABOUT = 2;
|
||||||
|
|
||||||
private static final int ORDER = 0;
|
private static final int ORDER = 0;
|
||||||
|
|
||||||
@@ -132,6 +137,19 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
ThemeHelper.setDayNightMode(this);
|
ThemeHelper.setDayNightMode(this);
|
||||||
ThemeHelper.setTheme(this, ServiceHelper.getSelectedServiceId(this));
|
ThemeHelper.setTheme(this, ServiceHelper.getSelectedServiceId(this));
|
||||||
|
|
||||||
|
// Fixes text color turning black in dark/black mode:
|
||||||
|
// https://github.com/TeamNewPipe/NewPipe/issues/12016
|
||||||
|
// For further reference see: https://issuetracker.google.com/issues/37124582
|
||||||
|
if (DeviceUtils.supportsWebView()) {
|
||||||
|
try {
|
||||||
|
new WebView(this);
|
||||||
|
} catch (final Throwable e) {
|
||||||
|
if (DEBUG) {
|
||||||
|
Log.e(TAG, "Failed to create WebView", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
assureCorrectAppLanguage(this);
|
assureCorrectAppLanguage(this);
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
|
|
||||||
@@ -163,16 +181,24 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
// if this is enabled by the user.
|
// if this is enabled by the user.
|
||||||
NotificationWorker.initialize(this);
|
NotificationWorker.initialize(this);
|
||||||
}
|
}
|
||||||
|
if (!UpdateSettingsFragment.wasUserAskedForConsent(this)
|
||||||
|
&& !App.getInstance().isFirstRun()
|
||||||
|
&& ReleaseVersionUtil.INSTANCE.isReleaseApk()) {
|
||||||
|
UpdateSettingsFragment.askForConsentToUpdateChecks(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
Localization.migrateAppLanguageSettingIfNecessary(getApplicationContext());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onPostCreate(final Bundle savedInstanceState) {
|
protected void onPostCreate(final Bundle savedInstanceState) {
|
||||||
super.onPostCreate(savedInstanceState);
|
super.onPostCreate(savedInstanceState);
|
||||||
|
|
||||||
final App app = App.getApp();
|
final App app = App.getInstance();
|
||||||
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(app);
|
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(app);
|
||||||
|
|
||||||
if (prefs.getBoolean(app.getString(R.string.update_app_key), true)) {
|
if (prefs.getBoolean(app.getString(R.string.update_app_key), false)
|
||||||
|
&& prefs.getBoolean(app.getString(R.string.update_check_consent_key), false)) {
|
||||||
// Start the worker which is checking all conditions
|
// Start the worker which is checking all conditions
|
||||||
// and eventually searching for a new version.
|
// and eventually searching for a new version.
|
||||||
NewVersionWorker.enqueueNewVersionCheckingWork(app, false);
|
NewVersionWorker.enqueueNewVersionCheckingWork(app, false);
|
||||||
@@ -250,6 +276,10 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
drawerLayoutBinding.navigation.getMenu()
|
drawerLayoutBinding.navigation.getMenu()
|
||||||
.add(R.id.menu_options_about_group, ITEM_ID_SETTINGS, ORDER, R.string.settings)
|
.add(R.id.menu_options_about_group, ITEM_ID_SETTINGS, ORDER, R.string.settings)
|
||||||
.setIcon(R.drawable.ic_settings);
|
.setIcon(R.drawable.ic_settings);
|
||||||
|
drawerLayoutBinding.navigation.getMenu()
|
||||||
|
.add(R.id.menu_options_about_group, ITEM_ID_DONATION, ORDER,
|
||||||
|
R.string.donation_title)
|
||||||
|
.setIcon(R.drawable.volunteer_activism_ic);
|
||||||
drawerLayoutBinding.navigation.getMenu()
|
drawerLayoutBinding.navigation.getMenu()
|
||||||
.add(R.id.menu_options_about_group, ITEM_ID_ABOUT, ORDER, R.string.tab_about)
|
.add(R.id.menu_options_about_group, ITEM_ID_ABOUT, ORDER, R.string.tab_about)
|
||||||
.setIcon(R.drawable.ic_info_outline);
|
.setIcon(R.drawable.ic_info_outline);
|
||||||
@@ -325,6 +355,9 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
case ITEM_ID_SETTINGS:
|
case ITEM_ID_SETTINGS:
|
||||||
NavigationHelper.openSettings(this);
|
NavigationHelper.openSettings(this);
|
||||||
break;
|
break;
|
||||||
|
case ITEM_ID_DONATION:
|
||||||
|
ShareUtils.openUrlInBrowser(this, getString(R.string.donation_url));
|
||||||
|
break;
|
||||||
case ITEM_ID_ABOUT:
|
case ITEM_ID_ABOUT:
|
||||||
NavigationHelper.openAbout(this);
|
NavigationHelper.openAbout(this);
|
||||||
break;
|
break;
|
||||||
@@ -545,32 +578,27 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
// In case bottomSheet is not visible on the screen or collapsed we can assume that the user
|
// In case bottomSheet is not visible on the screen or collapsed we can assume that the user
|
||||||
// interacts with a fragment inside fragment_holder so all back presses should be
|
// interacts with a fragment inside fragment_holder so all back presses should be
|
||||||
// handled by it
|
// handled by it
|
||||||
if (bottomSheetHiddenOrCollapsed()) {
|
final var fragmentManager = getSupportFragmentManager();
|
||||||
final Fragment fragment = getSupportFragmentManager()
|
|
||||||
.findFragmentById(R.id.fragment_holder);
|
|
||||||
// If current fragment implements BackPressable (i.e. can/wanna handle back press)
|
|
||||||
// delegate the back press to it
|
|
||||||
if (fragment instanceof BackPressable) {
|
|
||||||
if (((BackPressable) fragment).onBackPressed()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
} else {
|
if (bottomSheetHiddenOrCollapsed()) {
|
||||||
final Fragment fragmentPlayer = getSupportFragmentManager()
|
final var fragment = fragmentManager.findFragmentById(R.id.fragment_holder);
|
||||||
.findFragmentById(R.id.fragment_player_holder);
|
|
||||||
// If current fragment implements BackPressable (i.e. can/wanna handle back press)
|
// If current fragment implements BackPressable (i.e. can/wanna handle back press)
|
||||||
// delegate the back press to it
|
// delegate the back press to it
|
||||||
if (fragmentPlayer instanceof BackPressable) {
|
if (fragment instanceof BackPressable backPressable && backPressable.onBackPressed()) {
|
||||||
if (!((BackPressable) fragmentPlayer).onBackPressed()) {
|
|
||||||
BottomSheetBehavior.from(mainBinding.fragmentPlayerHolder)
|
|
||||||
.setState(BottomSheetBehavior.STATE_COLLAPSED);
|
|
||||||
}
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
final var player = fragmentManager.findFragmentById(R.id.fragment_player_holder);
|
||||||
|
// If current fragment implements BackPressable (i.e. can/wanna handle back press)
|
||||||
|
// delegate the back press to it
|
||||||
|
if (player instanceof BackPressable backPressable && !backPressable.onBackPressed()) {
|
||||||
|
BottomSheetBehavior.from(mainBinding.fragmentPlayerHolder)
|
||||||
|
.setState(BottomSheetBehavior.STATE_COLLAPSED);
|
||||||
|
}
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (getSupportFragmentManager().getBackStackEntryCount() == 1) {
|
if (fragmentManager.getBackStackEntryCount() == 1) {
|
||||||
finish();
|
finish();
|
||||||
} else {
|
} else {
|
||||||
super.onBackPressed();
|
super.onBackPressed();
|
||||||
@@ -629,10 +657,11 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
* </pre>
|
* </pre>
|
||||||
*/
|
*/
|
||||||
private void onHomeButtonPressed() {
|
private void onHomeButtonPressed() {
|
||||||
// If search fragment wasn't found in the backstack...
|
final var fm = getSupportFragmentManager();
|
||||||
if (!NavigationHelper.tryGotoSearchFragment(getSupportFragmentManager())) {
|
|
||||||
// ...go to the main fragment
|
if (!NavigationHelper.tryGotoSearchFragment(fm)) {
|
||||||
NavigationHelper.gotoMainFragment(getSupportFragmentManager());
|
// If search fragment wasn't found in the backstack go to the main fragment
|
||||||
|
NavigationHelper.gotoMainFragment(fm);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -813,7 +842,8 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
@Override
|
@Override
|
||||||
public void onReceive(final Context context, final Intent intent) {
|
public void onReceive(final Context context, final Intent intent) {
|
||||||
if (Objects.equals(intent.getAction(),
|
if (Objects.equals(intent.getAction(),
|
||||||
VideoDetailFragment.ACTION_PLAYER_STARTED)) {
|
VideoDetailFragment.ACTION_PLAYER_STARTED)
|
||||||
|
&& PlayerHolder.getInstance().isPlayerOpen()) {
|
||||||
openMiniPlayerIfMissing();
|
openMiniPlayerIfMissing();
|
||||||
// At this point the player is added 100%, we can unregister. Other actions
|
// At this point the player is added 100%, we can unregister. Other actions
|
||||||
// are useless since the fragment will not be removed after that.
|
// are useless since the fragment will not be removed after that.
|
||||||
@@ -825,6 +855,10 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
final IntentFilter intentFilter = new IntentFilter();
|
final IntentFilter intentFilter = new IntentFilter();
|
||||||
intentFilter.addAction(VideoDetailFragment.ACTION_PLAYER_STARTED);
|
intentFilter.addAction(VideoDetailFragment.ACTION_PLAYER_STARTED);
|
||||||
registerReceiver(broadcastReceiver, intentFilter);
|
registerReceiver(broadcastReceiver, intentFilter);
|
||||||
|
|
||||||
|
// If the PlayerHolder is not bound yet, but the service is running, try to bind to it.
|
||||||
|
// Once the connection is established, the ACTION_PLAYER_STARTED will be sent.
|
||||||
|
PlayerHolder.getInstance().tryBindIfNeeded(this);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -836,4 +870,5 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
return sheetState == BottomSheetBehavior.STATE_HIDDEN
|
return sheetState == BottomSheetBehavior.STATE_HIDDEN
|
||||||
|| sheetState == BottomSheetBehavior.STATE_COLLAPSED;
|
|| sheetState == BottomSheetBehavior.STATE_COLLAPSED;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import static org.schabi.newpipe.database.Migrations.MIGRATION_3_4;
|
|||||||
import static org.schabi.newpipe.database.Migrations.MIGRATION_4_5;
|
import static org.schabi.newpipe.database.Migrations.MIGRATION_4_5;
|
||||||
import static org.schabi.newpipe.database.Migrations.MIGRATION_5_6;
|
import static org.schabi.newpipe.database.Migrations.MIGRATION_5_6;
|
||||||
import static org.schabi.newpipe.database.Migrations.MIGRATION_6_7;
|
import static org.schabi.newpipe.database.Migrations.MIGRATION_6_7;
|
||||||
|
import static org.schabi.newpipe.database.Migrations.MIGRATION_7_8;
|
||||||
|
import static org.schabi.newpipe.database.Migrations.MIGRATION_8_9;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.database.Cursor;
|
import android.database.Cursor;
|
||||||
@@ -27,7 +29,7 @@ public final class NewPipeDatabase {
|
|||||||
return Room
|
return Room
|
||||||
.databaseBuilder(context.getApplicationContext(), AppDatabase.class, DATABASE_NAME)
|
.databaseBuilder(context.getApplicationContext(), AppDatabase.class, DATABASE_NAME)
|
||||||
.addMigrations(MIGRATION_1_2, MIGRATION_2_3, MIGRATION_3_4, MIGRATION_4_5,
|
.addMigrations(MIGRATION_1_2, MIGRATION_2_3, MIGRATION_3_4, MIGRATION_4_5,
|
||||||
MIGRATION_5_6, MIGRATION_6_7)
|
MIGRATION_5_6, MIGRATION_6_7, MIGRATION_7_8, MIGRATION_8_9)
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,9 +20,7 @@ import com.grack.nanojson.JsonParser
|
|||||||
import com.grack.nanojson.JsonParserException
|
import com.grack.nanojson.JsonParserException
|
||||||
import org.schabi.newpipe.extractor.downloader.Response
|
import org.schabi.newpipe.extractor.downloader.Response
|
||||||
import org.schabi.newpipe.extractor.exceptions.ReCaptchaException
|
import org.schabi.newpipe.extractor.exceptions.ReCaptchaException
|
||||||
import org.schabi.newpipe.util.ReleaseVersionUtil.coerceUpdateCheckExpiry
|
import org.schabi.newpipe.util.ReleaseVersionUtil
|
||||||
import org.schabi.newpipe.util.ReleaseVersionUtil.isLastUpdateCheckExpired
|
|
||||||
import org.schabi.newpipe.util.ReleaseVersionUtil.isReleaseApk
|
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
|
|
||||||
class NewVersionWorker(
|
class NewVersionWorker(
|
||||||
@@ -84,7 +82,7 @@ class NewVersionWorker(
|
|||||||
@Throws(IOException::class, ReCaptchaException::class)
|
@Throws(IOException::class, ReCaptchaException::class)
|
||||||
private fun checkNewVersion() {
|
private fun checkNewVersion() {
|
||||||
// Check if the current apk is a github one or not.
|
// Check if the current apk is a github one or not.
|
||||||
if (!isReleaseApk()) {
|
if (!ReleaseVersionUtil.isReleaseApk) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -93,7 +91,7 @@ class NewVersionWorker(
|
|||||||
// Check if the last request has happened a certain time ago
|
// Check if the last request has happened a certain time ago
|
||||||
// to reduce the number of API requests.
|
// to reduce the number of API requests.
|
||||||
val expiry = prefs.getLong(applicationContext.getString(R.string.update_expiry_key), 0)
|
val expiry = prefs.getLong(applicationContext.getString(R.string.update_expiry_key), 0)
|
||||||
if (!isLastUpdateCheckExpired(expiry)) {
|
if (!ReleaseVersionUtil.isLastUpdateCheckExpired(expiry)) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -108,7 +106,7 @@ class NewVersionWorker(
|
|||||||
try {
|
try {
|
||||||
// Store a timestamp which needs to be exceeded,
|
// Store a timestamp which needs to be exceeded,
|
||||||
// before a new request to the API is made.
|
// before a new request to the API is made.
|
||||||
val newExpiry = coerceUpdateCheckExpiry(response.getHeader("expires"))
|
val newExpiry = ReleaseVersionUtil.coerceUpdateCheckExpiry(response.getHeader("expires"))
|
||||||
prefs.edit {
|
prefs.edit {
|
||||||
putLong(applicationContext.getString(R.string.update_expiry_key), newExpiry)
|
putLong(applicationContext.getString(R.string.update_expiry_key), newExpiry)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,6 +41,9 @@ import androidx.lifecycle.Lifecycle;
|
|||||||
import androidx.lifecycle.LifecycleOwner;
|
import androidx.lifecycle.LifecycleOwner;
|
||||||
import androidx.preference.PreferenceManager;
|
import androidx.preference.PreferenceManager;
|
||||||
|
|
||||||
|
import com.evernote.android.state.State;
|
||||||
|
import com.livefront.bridge.Bridge;
|
||||||
|
|
||||||
import org.schabi.newpipe.database.stream.model.StreamEntity;
|
import org.schabi.newpipe.database.stream.model.StreamEntity;
|
||||||
import org.schabi.newpipe.databinding.ListRadioIconItemBinding;
|
import org.schabi.newpipe.databinding.ListRadioIconItemBinding;
|
||||||
import org.schabi.newpipe.databinding.SingleChoiceDialogViewBinding;
|
import org.schabi.newpipe.databinding.SingleChoiceDialogViewBinding;
|
||||||
@@ -98,8 +101,6 @@ import java.util.List;
|
|||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
import icepick.Icepick;
|
|
||||||
import icepick.State;
|
|
||||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||||
import io.reactivex.rxjava3.core.Observable;
|
import io.reactivex.rxjava3.core.Observable;
|
||||||
import io.reactivex.rxjava3.core.Single;
|
import io.reactivex.rxjava3.core.Single;
|
||||||
@@ -152,7 +153,7 @@ public class RouterActivity extends AppCompatActivity {
|
|||||||
getWindow().setAttributes(params);
|
getWindow().setAttributes(params);
|
||||||
|
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
Icepick.restoreInstanceState(this, savedInstanceState);
|
Bridge.restoreInstanceState(this, savedInstanceState);
|
||||||
|
|
||||||
// FragmentManager will take care to recreate (Playlist|Download)Dialog when screen rotates
|
// FragmentManager will take care to recreate (Playlist|Download)Dialog when screen rotates
|
||||||
// We used to .setOnDismissListener(dialog -> finish()); when creating these DialogFragments
|
// We used to .setOnDismissListener(dialog -> finish()); when creating these DialogFragments
|
||||||
@@ -197,7 +198,7 @@ public class RouterActivity extends AppCompatActivity {
|
|||||||
@Override
|
@Override
|
||||||
protected void onSaveInstanceState(@NonNull final Bundle outState) {
|
protected void onSaveInstanceState(@NonNull final Bundle outState) {
|
||||||
super.onSaveInstanceState(outState);
|
super.onSaveInstanceState(outState);
|
||||||
Icepick.saveInstanceState(this, outState);
|
Bridge.saveInstanceState(this, outState);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -1,199 +1,31 @@
|
|||||||
package org.schabi.newpipe.about
|
package org.schabi.newpipe.about
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
import androidx.activity.compose.setContent
|
||||||
import android.view.MenuItem
|
import androidx.activity.enableEdgeToEdge
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import android.widget.Button
|
|
||||||
import androidx.annotation.StringRes
|
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.fragment.app.FragmentActivity
|
|
||||||
import androidx.viewpager2.adapter.FragmentStateAdapter
|
|
||||||
import com.google.android.material.tabs.TabLayoutMediator
|
|
||||||
import org.schabi.newpipe.BuildConfig
|
|
||||||
import org.schabi.newpipe.R
|
import org.schabi.newpipe.R
|
||||||
import org.schabi.newpipe.databinding.ActivityAboutBinding
|
import org.schabi.newpipe.ui.components.common.ScaffoldWithToolbar
|
||||||
import org.schabi.newpipe.databinding.FragmentAboutBinding
|
import org.schabi.newpipe.ui.screens.AboutScreen
|
||||||
|
import org.schabi.newpipe.ui.theme.AppTheme
|
||||||
import org.schabi.newpipe.util.Localization
|
import org.schabi.newpipe.util.Localization
|
||||||
import org.schabi.newpipe.util.ThemeHelper
|
|
||||||
import org.schabi.newpipe.util.external_communication.ShareUtils
|
|
||||||
|
|
||||||
class AboutActivity : AppCompatActivity() {
|
class AboutActivity : AppCompatActivity() {
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
Localization.assureCorrectAppLanguage(this)
|
Localization.assureCorrectAppLanguage(this)
|
||||||
|
enableEdgeToEdge()
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
ThemeHelper.setTheme(this)
|
|
||||||
title = getString(R.string.title_activity_about)
|
|
||||||
|
|
||||||
val aboutBinding = ActivityAboutBinding.inflate(layoutInflater)
|
setContent {
|
||||||
setContentView(aboutBinding.root)
|
AppTheme {
|
||||||
setSupportActionBar(aboutBinding.aboutToolbar)
|
ScaffoldWithToolbar(
|
||||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
title = stringResource(R.string.title_activity_about),
|
||||||
|
onBackClick = { onBackPressedDispatcher.onBackPressed() }
|
||||||
// Create the adapter that will return a fragment for each of the three
|
) { padding ->
|
||||||
// primary sections of the activity.
|
AboutScreen(padding)
|
||||||
val mAboutStateAdapter = AboutStateAdapter(this)
|
}
|
||||||
// Set up the ViewPager with the sections adapter.
|
|
||||||
aboutBinding.aboutViewPager2.adapter = mAboutStateAdapter
|
|
||||||
TabLayoutMediator(
|
|
||||||
aboutBinding.aboutTabLayout,
|
|
||||||
aboutBinding.aboutViewPager2
|
|
||||||
) { tab, position ->
|
|
||||||
tab.setText(mAboutStateAdapter.getPageTitle(position))
|
|
||||||
}.attach()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
|
||||||
if (item.itemId == android.R.id.home) {
|
|
||||||
finish()
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return super.onOptionsItemSelected(item)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A placeholder fragment containing a simple view.
|
|
||||||
*/
|
|
||||||
class AboutFragment : Fragment() {
|
|
||||||
private fun Button.openLink(@StringRes url: Int) {
|
|
||||||
setOnClickListener {
|
|
||||||
ShareUtils.openUrlInApp(context, requireContext().getString(url))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateView(
|
|
||||||
inflater: LayoutInflater,
|
|
||||||
container: ViewGroup?,
|
|
||||||
savedInstanceState: Bundle?
|
|
||||||
): View {
|
|
||||||
FragmentAboutBinding.inflate(inflater, container, false).apply {
|
|
||||||
aboutAppVersion.text = BuildConfig.VERSION_NAME
|
|
||||||
aboutGithubLink.openLink(R.string.github_url)
|
|
||||||
aboutDonationLink.openLink(R.string.donation_url)
|
|
||||||
aboutWebsiteLink.openLink(R.string.website_url)
|
|
||||||
aboutPrivacyPolicyLink.openLink(R.string.privacy_policy_url)
|
|
||||||
faqLink.openLink(R.string.faq_url)
|
|
||||||
return root
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A [FragmentStateAdapter] that returns a fragment corresponding to
|
|
||||||
* one of the sections/tabs/pages.
|
|
||||||
*/
|
|
||||||
private class AboutStateAdapter(fa: FragmentActivity) : FragmentStateAdapter(fa) {
|
|
||||||
private val posAbout = 0
|
|
||||||
private val posLicense = 1
|
|
||||||
private val totalCount = 2
|
|
||||||
|
|
||||||
override fun createFragment(position: Int): Fragment {
|
|
||||||
return when (position) {
|
|
||||||
posAbout -> AboutFragment()
|
|
||||||
posLicense -> LicenseFragment.newInstance(SOFTWARE_COMPONENTS)
|
|
||||||
else -> throw IllegalArgumentException("Unknown position for ViewPager2")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getItemCount(): Int {
|
|
||||||
// Show 2 total pages.
|
|
||||||
return totalCount
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getPageTitle(position: Int): Int {
|
|
||||||
return when (position) {
|
|
||||||
posAbout -> R.string.tab_about
|
|
||||||
posLicense -> R.string.tab_licenses
|
|
||||||
else -> throw IllegalArgumentException("Unknown position for ViewPager2")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
/**
|
|
||||||
* List of all software components.
|
|
||||||
*/
|
|
||||||
private val SOFTWARE_COMPONENTS = arrayOf(
|
|
||||||
SoftwareComponent(
|
|
||||||
"ACRA", "2013", "Kevin Gaudin",
|
|
||||||
"https://github.com/ACRA/acra", StandardLicenses.APACHE2
|
|
||||||
),
|
|
||||||
SoftwareComponent(
|
|
||||||
"AndroidX", "2005 - 2011", "The Android Open Source Project",
|
|
||||||
"https://developer.android.com/jetpack", StandardLicenses.APACHE2
|
|
||||||
),
|
|
||||||
SoftwareComponent(
|
|
||||||
"ExoPlayer", "2014 - 2020", "Google, Inc.",
|
|
||||||
"https://github.com/google/ExoPlayer", StandardLicenses.APACHE2
|
|
||||||
),
|
|
||||||
SoftwareComponent(
|
|
||||||
"GigaGet", "2014 - 2015", "Peter Cai",
|
|
||||||
"https://github.com/PaperAirplane-Dev-Team/GigaGet", StandardLicenses.GPL3
|
|
||||||
),
|
|
||||||
SoftwareComponent(
|
|
||||||
"Groupie", "2016", "Lisa Wray",
|
|
||||||
"https://github.com/lisawray/groupie", StandardLicenses.MIT
|
|
||||||
),
|
|
||||||
SoftwareComponent(
|
|
||||||
"Icepick", "2015", "Frankie Sardo",
|
|
||||||
"https://github.com/frankiesardo/icepick", StandardLicenses.EPL1
|
|
||||||
),
|
|
||||||
SoftwareComponent(
|
|
||||||
"Jsoup", "2009 - 2020", "Jonathan Hedley",
|
|
||||||
"https://github.com/jhy/jsoup", StandardLicenses.MIT
|
|
||||||
),
|
|
||||||
SoftwareComponent(
|
|
||||||
"Markwon", "2019", "Dimitry Ivanov",
|
|
||||||
"https://github.com/noties/Markwon", StandardLicenses.APACHE2
|
|
||||||
),
|
|
||||||
SoftwareComponent(
|
|
||||||
"Material Components for Android", "2016 - 2020", "Google, Inc.",
|
|
||||||
"https://github.com/material-components/material-components-android",
|
|
||||||
StandardLicenses.APACHE2
|
|
||||||
),
|
|
||||||
SoftwareComponent(
|
|
||||||
"NewPipe Extractor", "2017 - 2020", "Christian Schabesberger",
|
|
||||||
"https://github.com/TeamNewPipe/NewPipeExtractor", StandardLicenses.GPL3
|
|
||||||
),
|
|
||||||
SoftwareComponent(
|
|
||||||
"NoNonsense-FilePicker", "2016", "Jonas Kalderstam",
|
|
||||||
"https://github.com/spacecowboy/NoNonsense-FilePicker", StandardLicenses.MPL2
|
|
||||||
),
|
|
||||||
SoftwareComponent(
|
|
||||||
"OkHttp", "2019", "Square, Inc.",
|
|
||||||
"https://square.github.io/okhttp/", StandardLicenses.APACHE2
|
|
||||||
),
|
|
||||||
SoftwareComponent(
|
|
||||||
"Picasso", "2013", "Square, Inc.",
|
|
||||||
"https://square.github.io/picasso/", StandardLicenses.APACHE2
|
|
||||||
),
|
|
||||||
SoftwareComponent(
|
|
||||||
"PrettyTime", "2012 - 2020", "Lincoln Baxter, III",
|
|
||||||
"https://github.com/ocpsoft/prettytime", StandardLicenses.APACHE2
|
|
||||||
),
|
|
||||||
SoftwareComponent(
|
|
||||||
"ProcessPhoenix", "2015", "Jake Wharton",
|
|
||||||
"https://github.com/JakeWharton/ProcessPhoenix", StandardLicenses.APACHE2
|
|
||||||
),
|
|
||||||
SoftwareComponent(
|
|
||||||
"RxAndroid", "2015", "The RxAndroid authors",
|
|
||||||
"https://github.com/ReactiveX/RxAndroid", StandardLicenses.APACHE2
|
|
||||||
),
|
|
||||||
SoftwareComponent(
|
|
||||||
"RxBinding", "2015", "Jake Wharton",
|
|
||||||
"https://github.com/JakeWharton/RxBinding", StandardLicenses.APACHE2
|
|
||||||
),
|
|
||||||
SoftwareComponent(
|
|
||||||
"RxJava", "2016 - 2020", "RxJava Contributors",
|
|
||||||
"https://github.com/ReactiveX/RxJava", StandardLicenses.APACHE2
|
|
||||||
),
|
|
||||||
SoftwareComponent(
|
|
||||||
"SearchPreference", "2018", "ByteHamster",
|
|
||||||
"https://github.com/ByteHamster/SearchPreference", StandardLicenses.MIT
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
package org.schabi.newpipe.about
|
|
||||||
|
|
||||||
import android.os.Parcelable
|
|
||||||
import kotlinx.parcelize.Parcelize
|
|
||||||
import java.io.Serializable
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Class for storing information about a software license.
|
|
||||||
*/
|
|
||||||
@Parcelize
|
|
||||||
class License(val name: String, val abbreviation: String, val filename: String) : Parcelable, Serializable
|
|
||||||
@@ -1,139 +0,0 @@
|
|||||||
package org.schabi.newpipe.about
|
|
||||||
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.util.Base64
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import android.webkit.WebView
|
|
||||||
import androidx.appcompat.app.AlertDialog
|
|
||||||
import androidx.core.os.bundleOf
|
|
||||||
import androidx.fragment.app.Fragment
|
|
||||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
|
||||||
import io.reactivex.rxjava3.core.Observable
|
|
||||||
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
|
||||||
import io.reactivex.rxjava3.disposables.Disposable
|
|
||||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
|
||||||
import org.schabi.newpipe.BuildConfig
|
|
||||||
import org.schabi.newpipe.R
|
|
||||||
import org.schabi.newpipe.databinding.FragmentLicensesBinding
|
|
||||||
import org.schabi.newpipe.databinding.ItemSoftwareComponentBinding
|
|
||||||
import org.schabi.newpipe.util.Localization
|
|
||||||
import org.schabi.newpipe.util.external_communication.ShareUtils
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fragment containing the software licenses.
|
|
||||||
*/
|
|
||||||
class LicenseFragment : Fragment() {
|
|
||||||
private lateinit var softwareComponents: Array<SoftwareComponent>
|
|
||||||
private var activeSoftwareComponent: SoftwareComponent? = null
|
|
||||||
private val compositeDisposable = CompositeDisposable()
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
softwareComponents = arguments?.getParcelableArray(ARG_COMPONENTS) as Array<SoftwareComponent>
|
|
||||||
activeSoftwareComponent = savedInstanceState?.getSerializable(SOFTWARE_COMPONENT_KEY) as? SoftwareComponent
|
|
||||||
// Sort components by name
|
|
||||||
softwareComponents.sortBy { it.name }
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDestroy() {
|
|
||||||
compositeDisposable.dispose()
|
|
||||||
super.onDestroy()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateView(
|
|
||||||
inflater: LayoutInflater,
|
|
||||||
container: ViewGroup?,
|
|
||||||
savedInstanceState: Bundle?
|
|
||||||
): View {
|
|
||||||
val binding = FragmentLicensesBinding.inflate(inflater, container, false)
|
|
||||||
binding.licensesAppReadLicense.setOnClickListener {
|
|
||||||
compositeDisposable.add(
|
|
||||||
showLicense(NEWPIPE_SOFTWARE_COMPONENT)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
for (component in softwareComponents) {
|
|
||||||
val componentBinding = ItemSoftwareComponentBinding
|
|
||||||
.inflate(inflater, container, false)
|
|
||||||
componentBinding.name.text = component.name
|
|
||||||
componentBinding.copyright.text = getString(
|
|
||||||
R.string.copyright,
|
|
||||||
component.years,
|
|
||||||
component.copyrightOwner,
|
|
||||||
component.license.abbreviation
|
|
||||||
)
|
|
||||||
val root: View = componentBinding.root
|
|
||||||
root.tag = component
|
|
||||||
root.setOnClickListener {
|
|
||||||
compositeDisposable.add(
|
|
||||||
showLicense(component)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
binding.licensesSoftwareComponents.addView(root)
|
|
||||||
registerForContextMenu(root)
|
|
||||||
}
|
|
||||||
activeSoftwareComponent?.let { compositeDisposable.add(showLicense(it)) }
|
|
||||||
return binding.root
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onSaveInstanceState(savedInstanceState: Bundle) {
|
|
||||||
super.onSaveInstanceState(savedInstanceState)
|
|
||||||
activeSoftwareComponent?.let { savedInstanceState.putSerializable(SOFTWARE_COMPONENT_KEY, it) }
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun showLicense(
|
|
||||||
softwareComponent: SoftwareComponent
|
|
||||||
): Disposable {
|
|
||||||
return if (context == null) {
|
|
||||||
Disposable.empty()
|
|
||||||
} else {
|
|
||||||
val context = requireContext()
|
|
||||||
activeSoftwareComponent = softwareComponent
|
|
||||||
Observable.fromCallable { getFormattedLicense(context, softwareComponent.license) }
|
|
||||||
.subscribeOn(Schedulers.io())
|
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
|
||||||
.subscribe { formattedLicense ->
|
|
||||||
val webViewData = Base64.encodeToString(
|
|
||||||
formattedLicense.toByteArray(), Base64.NO_PADDING
|
|
||||||
)
|
|
||||||
val webView = WebView(context)
|
|
||||||
webView.loadData(webViewData, "text/html; charset=UTF-8", "base64")
|
|
||||||
|
|
||||||
Localization.assureCorrectAppLanguage(context)
|
|
||||||
val builder = AlertDialog.Builder(requireContext())
|
|
||||||
.setTitle(softwareComponent.name)
|
|
||||||
.setView(webView)
|
|
||||||
.setOnCancelListener { activeSoftwareComponent = null }
|
|
||||||
.setOnDismissListener { activeSoftwareComponent = null }
|
|
||||||
.setPositiveButton(R.string.done) { dialog, _ -> dialog.dismiss() }
|
|
||||||
|
|
||||||
if (softwareComponent != NEWPIPE_SOFTWARE_COMPONENT) {
|
|
||||||
builder.setNeutralButton(R.string.open_website_license) { _, _ ->
|
|
||||||
ShareUtils.openUrlInApp(requireContext(), softwareComponent.link)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
builder.show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private const val ARG_COMPONENTS = "components"
|
|
||||||
private const val SOFTWARE_COMPONENT_KEY = "ACTIVE_SOFTWARE_COMPONENT"
|
|
||||||
private val NEWPIPE_SOFTWARE_COMPONENT = SoftwareComponent(
|
|
||||||
"NewPipe",
|
|
||||||
"2014-2023",
|
|
||||||
"Team NewPipe",
|
|
||||||
"https://newpipe.net/",
|
|
||||||
StandardLicenses.GPL3,
|
|
||||||
BuildConfig.VERSION_NAME
|
|
||||||
)
|
|
||||||
fun newInstance(softwareComponents: Array<SoftwareComponent>): LicenseFragment {
|
|
||||||
val fragment = LicenseFragment()
|
|
||||||
fragment.arguments = bundleOf(ARG_COMPONENTS to softwareComponents)
|
|
||||||
return fragment
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
package org.schabi.newpipe.about
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import org.schabi.newpipe.R
|
|
||||||
import org.schabi.newpipe.util.ThemeHelper
|
|
||||||
import java.io.IOException
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param context the context to use
|
|
||||||
* @param license the license
|
|
||||||
* @return String which contains a HTML formatted license page
|
|
||||||
* styled according to the context's theme
|
|
||||||
*/
|
|
||||||
fun getFormattedLicense(context: Context, license: License): String {
|
|
||||||
try {
|
|
||||||
return context.assets.open(license.filename).bufferedReader().use { it.readText() }
|
|
||||||
// split the HTML file and insert the stylesheet into the HEAD of the file
|
|
||||||
.replace("</head>", "<style>${getLicenseStylesheet(context)}</style></head>")
|
|
||||||
} catch (e: IOException) {
|
|
||||||
throw IllegalArgumentException("Could not get license file: ${license.filename}", e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param context the Android context
|
|
||||||
* @return String which is a CSS stylesheet according to the context's theme
|
|
||||||
*/
|
|
||||||
fun getLicenseStylesheet(context: Context): String {
|
|
||||||
val isLightTheme = ThemeHelper.isLightThemeSelected(context)
|
|
||||||
val licenseBackgroundColor = getHexRGBColor(
|
|
||||||
context, if (isLightTheme) R.color.light_license_background_color else R.color.dark_license_background_color
|
|
||||||
)
|
|
||||||
val licenseTextColor = getHexRGBColor(
|
|
||||||
context, if (isLightTheme) R.color.light_license_text_color else R.color.dark_license_text_color
|
|
||||||
)
|
|
||||||
val youtubePrimaryColor = getHexRGBColor(
|
|
||||||
context, if (isLightTheme) R.color.light_youtube_primary_color else R.color.dark_youtube_primary_color
|
|
||||||
)
|
|
||||||
return "body{padding:12px 15px;margin:0;background:#$licenseBackgroundColor;color:#$licenseTextColor}" +
|
|
||||||
"a[href]{color:#$youtubePrimaryColor}pre{white-space:pre-wrap}"
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Cast R.color to a hexadecimal color value.
|
|
||||||
*
|
|
||||||
* @param context the context to use
|
|
||||||
* @param color the color number from R.color
|
|
||||||
* @return a six characters long String with hexadecimal RGB values
|
|
||||||
*/
|
|
||||||
fun getHexRGBColor(context: Context, color: Int): String {
|
|
||||||
return context.getString(color).substring(3)
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
package org.schabi.newpipe.about
|
|
||||||
|
|
||||||
import android.os.Parcelable
|
|
||||||
import kotlinx.parcelize.Parcelize
|
|
||||||
import java.io.Serializable
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
class SoftwareComponent
|
|
||||||
@JvmOverloads
|
|
||||||
constructor(
|
|
||||||
val name: String,
|
|
||||||
val years: String,
|
|
||||||
val copyrightOwner: String,
|
|
||||||
val link: String,
|
|
||||||
val license: License,
|
|
||||||
val version: String? = null
|
|
||||||
) : Parcelable, Serializable
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
package org.schabi.newpipe.about
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Class containing information about standard software licenses.
|
|
||||||
*/
|
|
||||||
object StandardLicenses {
|
|
||||||
@JvmField
|
|
||||||
val GPL3 = License("GNU General Public License, Version 3.0", "GPLv3", "gpl_3.html")
|
|
||||||
|
|
||||||
@JvmField
|
|
||||||
val APACHE2 = License("Apache License, Version 2.0", "ALv2", "apache2.html")
|
|
||||||
|
|
||||||
@JvmField
|
|
||||||
val MPL2 = License("Mozilla Public License, Version 2.0", "MPL 2.0", "mpl2.html")
|
|
||||||
|
|
||||||
@JvmField
|
|
||||||
val MIT = License("MIT License", "MIT", "mit.html")
|
|
||||||
|
|
||||||
@JvmField
|
|
||||||
val EPL1 = License("Eclipse Public License, Version 1.0", "EPL 1.0", "epl1.html")
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
package org.schabi.newpipe.database;
|
package org.schabi.newpipe.database;
|
||||||
|
|
||||||
import static org.schabi.newpipe.database.Migrations.DB_VER_7;
|
import static org.schabi.newpipe.database.Migrations.DB_VER_9;
|
||||||
|
|
||||||
import androidx.room.Database;
|
import androidx.room.Database;
|
||||||
import androidx.room.RoomDatabase;
|
import androidx.room.RoomDatabase;
|
||||||
@@ -38,7 +38,7 @@ import org.schabi.newpipe.database.subscription.SubscriptionEntity;
|
|||||||
FeedEntity.class, FeedGroupEntity.class, FeedGroupSubscriptionEntity.class,
|
FeedEntity.class, FeedGroupEntity.class, FeedGroupSubscriptionEntity.class,
|
||||||
FeedLastUpdatedEntity.class
|
FeedLastUpdatedEntity.class
|
||||||
},
|
},
|
||||||
version = DB_VER_7
|
version = DB_VER_9
|
||||||
)
|
)
|
||||||
public abstract class AppDatabase extends RoomDatabase {
|
public abstract class AppDatabase extends RoomDatabase {
|
||||||
public static final String DATABASE_NAME = "newpipe.db";
|
public static final String DATABASE_NAME = "newpipe.db";
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import java.time.Instant
|
|||||||
import java.time.OffsetDateTime
|
import java.time.OffsetDateTime
|
||||||
import java.time.ZoneOffset
|
import java.time.ZoneOffset
|
||||||
|
|
||||||
object Converters {
|
class Converters {
|
||||||
/**
|
/**
|
||||||
* Convert a long value to a [OffsetDateTime].
|
* Convert a long value to a [OffsetDateTime].
|
||||||
*
|
*
|
||||||
@@ -47,6 +47,6 @@ object Converters {
|
|||||||
|
|
||||||
@TypeConverter
|
@TypeConverter
|
||||||
fun feedGroupIconOf(id: Int): FeedGroupIcon {
|
fun feedGroupIconOf(id: Int): FeedGroupIcon {
|
||||||
return FeedGroupIcon.values().first { it.id == id }
|
return FeedGroupIcon.entries.first { it.id == id }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,6 +25,8 @@ public final class Migrations {
|
|||||||
public static final int DB_VER_5 = 5;
|
public static final int DB_VER_5 = 5;
|
||||||
public static final int DB_VER_6 = 6;
|
public static final int DB_VER_6 = 6;
|
||||||
public static final int DB_VER_7 = 7;
|
public static final int DB_VER_7 = 7;
|
||||||
|
public static final int DB_VER_8 = 8;
|
||||||
|
public static final int DB_VER_9 = 9;
|
||||||
|
|
||||||
private static final String TAG = Migrations.class.getName();
|
private static final String TAG = Migrations.class.getName();
|
||||||
public static final boolean DEBUG = MainActivity.DEBUG;
|
public static final boolean DEBUG = MainActivity.DEBUG;
|
||||||
@@ -186,7 +188,7 @@ public final class Migrations {
|
|||||||
@Override
|
@Override
|
||||||
public void migrate(@NonNull final SupportSQLiteDatabase database) {
|
public void migrate(@NonNull final SupportSQLiteDatabase database) {
|
||||||
database.execSQL("ALTER TABLE `subscriptions` ADD COLUMN `notification_mode` "
|
database.execSQL("ALTER TABLE `subscriptions` ADD COLUMN `notification_mode` "
|
||||||
+ "INTEGER NOT NULL DEFAULT 0");
|
+ "INTEGER NOT NULL DEFAULT 0");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -235,6 +237,71 @@ public final class Migrations {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
public static final Migration MIGRATION_7_8 = new Migration(DB_VER_7, DB_VER_8) {
|
||||||
|
@Override
|
||||||
|
public void migrate(@NonNull final SupportSQLiteDatabase database) {
|
||||||
|
database.execSQL("DELETE FROM search_history WHERE id NOT IN (SELECT id FROM (SELECT "
|
||||||
|
+ "MIN(id) as id FROM search_history GROUP BY trim(search), service_id ) tmp)");
|
||||||
|
database.execSQL("UPDATE search_history SET search = trim(search)");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
public static final Migration MIGRATION_8_9 = new Migration(DB_VER_8, DB_VER_9) {
|
||||||
|
@Override
|
||||||
|
public void migrate(@NonNull final SupportSQLiteDatabase database) {
|
||||||
|
try {
|
||||||
|
database.beginTransaction();
|
||||||
|
|
||||||
|
// Update playlists.
|
||||||
|
// Create a temp table to initialize display_index.
|
||||||
|
database.execSQL("CREATE TABLE `playlists_tmp` "
|
||||||
|
+ "(`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, "
|
||||||
|
+ "`name` TEXT, `is_thumbnail_permanent` INTEGER NOT NULL, "
|
||||||
|
+ "`thumbnail_stream_id` INTEGER NOT NULL, "
|
||||||
|
+ "`display_index` INTEGER NOT NULL)");
|
||||||
|
database.execSQL("INSERT INTO `playlists_tmp` "
|
||||||
|
+ "(`uid`, `name`, `is_thumbnail_permanent`, `thumbnail_stream_id`, "
|
||||||
|
+ "`display_index`) "
|
||||||
|
+ "SELECT `uid`, `name`, `is_thumbnail_permanent`, `thumbnail_stream_id`, "
|
||||||
|
+ "-1 "
|
||||||
|
+ "FROM `playlists`");
|
||||||
|
|
||||||
|
// Replace the old table, note that this also removes the index on the name which
|
||||||
|
// we don't need anymore.
|
||||||
|
database.execSQL("DROP TABLE `playlists`");
|
||||||
|
database.execSQL("ALTER TABLE `playlists_tmp` RENAME TO `playlists`");
|
||||||
|
|
||||||
|
|
||||||
|
// Update remote_playlists.
|
||||||
|
// Create a temp table to initialize display_index.
|
||||||
|
database.execSQL("CREATE TABLE `remote_playlists_tmp` "
|
||||||
|
+ "(`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, "
|
||||||
|
+ "`service_id` INTEGER NOT NULL, `name` TEXT, `url` TEXT, "
|
||||||
|
+ "`thumbnail_url` TEXT, `uploader` TEXT, "
|
||||||
|
+ "`display_index` INTEGER NOT NULL,"
|
||||||
|
+ "`stream_count` INTEGER)");
|
||||||
|
database.execSQL("INSERT INTO `remote_playlists_tmp` (`uid`, `service_id`, "
|
||||||
|
+ "`name`, `url`, `thumbnail_url`, `uploader`, `display_index`, "
|
||||||
|
+ "`stream_count`)"
|
||||||
|
+ "SELECT `uid`, `service_id`, `name`, `url`, `thumbnail_url`, `uploader`, "
|
||||||
|
+ "-1, `stream_count` FROM `remote_playlists`");
|
||||||
|
|
||||||
|
// Replace the old table, note that this also removes the index on the name which
|
||||||
|
// we don't need anymore.
|
||||||
|
database.execSQL("DROP TABLE `remote_playlists`");
|
||||||
|
database.execSQL("ALTER TABLE `remote_playlists_tmp` RENAME TO `remote_playlists`");
|
||||||
|
|
||||||
|
// Create index on the new table.
|
||||||
|
database.execSQL("CREATE UNIQUE INDEX `index_remote_playlists_service_id_url` "
|
||||||
|
+ "ON `remote_playlists` (`service_id`, `url`)");
|
||||||
|
|
||||||
|
database.setTransactionSuccessful();
|
||||||
|
} finally {
|
||||||
|
database.endTransaction();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
private Migrations() {
|
private Migrations() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ package org.schabi.newpipe.database.history.model
|
|||||||
import androidx.room.ColumnInfo
|
import androidx.room.ColumnInfo
|
||||||
import androidx.room.Embedded
|
import androidx.room.Embedded
|
||||||
import org.schabi.newpipe.database.stream.model.StreamEntity
|
import org.schabi.newpipe.database.stream.model.StreamEntity
|
||||||
|
import org.schabi.newpipe.extractor.stream.StreamInfoItem
|
||||||
|
import org.schabi.newpipe.util.image.ImageStrategy
|
||||||
import java.time.OffsetDateTime
|
import java.time.OffsetDateTime
|
||||||
|
|
||||||
data class StreamHistoryEntry(
|
data class StreamHistoryEntry(
|
||||||
@@ -27,4 +29,17 @@ data class StreamHistoryEntry(
|
|||||||
return this.streamEntity.uid == other.streamEntity.uid && streamId == other.streamId &&
|
return this.streamEntity.uid == other.streamEntity.uid && streamId == other.streamId &&
|
||||||
accessDate.isEqual(other.accessDate)
|
accessDate.isEqual(other.accessDate)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun toStreamInfoItem(): StreamInfoItem =
|
||||||
|
StreamInfoItem(
|
||||||
|
streamEntity.serviceId,
|
||||||
|
streamEntity.url,
|
||||||
|
streamEntity.title,
|
||||||
|
streamEntity.streamType,
|
||||||
|
).apply {
|
||||||
|
duration = streamEntity.duration
|
||||||
|
uploaderName = streamEntity.uploader
|
||||||
|
uploaderUrl = streamEntity.uploaderUrl
|
||||||
|
thumbnails = ImageStrategy.dbUrlToImageList(streamEntity.thumbnailUrl)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,12 +13,17 @@ public class PlaylistDuplicatesEntry extends PlaylistMetadataEntry {
|
|||||||
@ColumnInfo(name = PLAYLIST_TIMES_STREAM_IS_CONTAINED)
|
@ColumnInfo(name = PLAYLIST_TIMES_STREAM_IS_CONTAINED)
|
||||||
public final long timesStreamIsContained;
|
public final long timesStreamIsContained;
|
||||||
|
|
||||||
|
@SuppressWarnings("checkstyle:ParameterNumber")
|
||||||
public PlaylistDuplicatesEntry(final long uid,
|
public PlaylistDuplicatesEntry(final long uid,
|
||||||
final String name,
|
final String name,
|
||||||
final String thumbnailUrl,
|
final String thumbnailUrl,
|
||||||
|
final boolean isThumbnailPermanent,
|
||||||
|
final long thumbnailStreamId,
|
||||||
|
final long displayIndex,
|
||||||
final long streamCount,
|
final long streamCount,
|
||||||
final long timesStreamIsContained) {
|
final long timesStreamIsContained) {
|
||||||
super(uid, name, thumbnailUrl, streamCount);
|
super(uid, name, thumbnailUrl, isThumbnailPermanent, thumbnailStreamId, displayIndex,
|
||||||
|
streamCount);
|
||||||
this.timesStreamIsContained = timesStreamIsContained;
|
this.timesStreamIsContained = timesStreamIsContained;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,22 +1,18 @@
|
|||||||
package org.schabi.newpipe.database.playlist;
|
package org.schabi.newpipe.database.playlist;
|
||||||
|
|
||||||
import org.schabi.newpipe.database.LocalItem;
|
import androidx.annotation.Nullable;
|
||||||
import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity;
|
|
||||||
|
|
||||||
import java.util.Comparator;
|
import org.schabi.newpipe.database.LocalItem;
|
||||||
import java.util.List;
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
import java.util.stream.Stream;
|
|
||||||
|
|
||||||
public interface PlaylistLocalItem extends LocalItem {
|
public interface PlaylistLocalItem extends LocalItem {
|
||||||
String getOrderingName();
|
String getOrderingName();
|
||||||
|
|
||||||
static List<PlaylistLocalItem> merge(
|
long getDisplayIndex();
|
||||||
final List<PlaylistMetadataEntry> localPlaylists,
|
|
||||||
final List<PlaylistRemoteEntity> remotePlaylists) {
|
long getUid();
|
||||||
return Stream.concat(localPlaylists.stream(), remotePlaylists.stream())
|
|
||||||
.sorted(Comparator.comparing(PlaylistLocalItem::getOrderingName,
|
void setDisplayIndex(long displayIndex);
|
||||||
Comparator.nullsLast(String.CASE_INSENSITIVE_ORDER)))
|
|
||||||
.collect(Collectors.toList());
|
@Nullable
|
||||||
}
|
String getThumbnailUrl();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,27 +2,42 @@ package org.schabi.newpipe.database.playlist;
|
|||||||
|
|
||||||
import androidx.room.ColumnInfo;
|
import androidx.room.ColumnInfo;
|
||||||
|
|
||||||
|
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_DISPLAY_INDEX;
|
||||||
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_ID;
|
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_ID;
|
||||||
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_NAME;
|
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_NAME;
|
||||||
|
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_THUMBNAIL_PERMANENT;
|
||||||
|
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_THUMBNAIL_STREAM_ID;
|
||||||
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_THUMBNAIL_URL;
|
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_THUMBNAIL_URL;
|
||||||
|
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
public class PlaylistMetadataEntry implements PlaylistLocalItem {
|
public class PlaylistMetadataEntry implements PlaylistLocalItem {
|
||||||
public static final String PLAYLIST_STREAM_COUNT = "streamCount";
|
public static final String PLAYLIST_STREAM_COUNT = "streamCount";
|
||||||
|
|
||||||
@ColumnInfo(name = PLAYLIST_ID)
|
@ColumnInfo(name = PLAYLIST_ID)
|
||||||
public final long uid;
|
private final long uid;
|
||||||
@ColumnInfo(name = PLAYLIST_NAME)
|
@ColumnInfo(name = PLAYLIST_NAME)
|
||||||
public final String name;
|
public final String name;
|
||||||
|
@ColumnInfo(name = PLAYLIST_THUMBNAIL_PERMANENT)
|
||||||
|
private final boolean isThumbnailPermanent;
|
||||||
|
@ColumnInfo(name = PLAYLIST_THUMBNAIL_STREAM_ID)
|
||||||
|
private final long thumbnailStreamId;
|
||||||
@ColumnInfo(name = PLAYLIST_THUMBNAIL_URL)
|
@ColumnInfo(name = PLAYLIST_THUMBNAIL_URL)
|
||||||
public final String thumbnailUrl;
|
public final String thumbnailUrl;
|
||||||
|
@ColumnInfo(name = PLAYLIST_DISPLAY_INDEX)
|
||||||
|
private long displayIndex;
|
||||||
@ColumnInfo(name = PLAYLIST_STREAM_COUNT)
|
@ColumnInfo(name = PLAYLIST_STREAM_COUNT)
|
||||||
public final long streamCount;
|
public final long streamCount;
|
||||||
|
|
||||||
public PlaylistMetadataEntry(final long uid, final String name, final String thumbnailUrl,
|
public PlaylistMetadataEntry(final long uid, final String name, final String thumbnailUrl,
|
||||||
final long streamCount) {
|
final boolean isThumbnailPermanent, final long thumbnailStreamId,
|
||||||
|
final long displayIndex, final long streamCount) {
|
||||||
this.uid = uid;
|
this.uid = uid;
|
||||||
this.name = name;
|
this.name = name;
|
||||||
this.thumbnailUrl = thumbnailUrl;
|
this.thumbnailUrl = thumbnailUrl;
|
||||||
|
this.isThumbnailPermanent = isThumbnailPermanent;
|
||||||
|
this.thumbnailStreamId = thumbnailStreamId;
|
||||||
|
this.displayIndex = displayIndex;
|
||||||
this.streamCount = streamCount;
|
this.streamCount = streamCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -35,4 +50,33 @@ public class PlaylistMetadataEntry implements PlaylistLocalItem {
|
|||||||
public String getOrderingName() {
|
public String getOrderingName() {
|
||||||
return name;
|
return name;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean isThumbnailPermanent() {
|
||||||
|
return isThumbnailPermanent;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getThumbnailStreamId() {
|
||||||
|
return thumbnailStreamId;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long getDisplayIndex() {
|
||||||
|
return displayIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long getUid() {
|
||||||
|
return uid;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setDisplayIndex(final long displayIndex) {
|
||||||
|
this.displayIndex = displayIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
@Override
|
||||||
|
public String getThumbnailUrl() {
|
||||||
|
return thumbnailUrl;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package org.schabi.newpipe.database.playlist.dao;
|
|||||||
|
|
||||||
import androidx.room.Dao;
|
import androidx.room.Dao;
|
||||||
import androidx.room.Query;
|
import androidx.room.Query;
|
||||||
|
import androidx.room.Transaction;
|
||||||
|
|
||||||
import org.schabi.newpipe.database.BasicDAO;
|
import org.schabi.newpipe.database.BasicDAO;
|
||||||
import org.schabi.newpipe.database.playlist.model.PlaylistEntity;
|
import org.schabi.newpipe.database.playlist.model.PlaylistEntity;
|
||||||
@@ -36,4 +37,17 @@ public interface PlaylistDAO extends BasicDAO<PlaylistEntity> {
|
|||||||
|
|
||||||
@Query("SELECT COUNT(*) FROM " + PLAYLIST_TABLE)
|
@Query("SELECT COUNT(*) FROM " + PLAYLIST_TABLE)
|
||||||
Flowable<Long> getCount();
|
Flowable<Long> getCount();
|
||||||
|
|
||||||
|
@Transaction
|
||||||
|
default long upsertPlaylist(final PlaylistEntity playlist) {
|
||||||
|
final long playlistId = playlist.getUid();
|
||||||
|
|
||||||
|
if (playlistId == -1) {
|
||||||
|
// This situation is probably impossible.
|
||||||
|
return insert(playlist);
|
||||||
|
} else {
|
||||||
|
update(playlist);
|
||||||
|
return playlistId;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import java.util.List;
|
|||||||
|
|
||||||
import io.reactivex.rxjava3.core.Flowable;
|
import io.reactivex.rxjava3.core.Flowable;
|
||||||
|
|
||||||
|
import static org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity.REMOTE_PLAYLIST_DISPLAY_INDEX;
|
||||||
import static org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity.REMOTE_PLAYLIST_ID;
|
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;
|
import static org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity.REMOTE_PLAYLIST_SERVICE_ID;
|
||||||
import static org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity.REMOTE_PLAYLIST_TABLE;
|
import static org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity.REMOTE_PLAYLIST_TABLE;
|
||||||
@@ -31,10 +32,18 @@ public interface PlaylistRemoteDAO extends BasicDAO<PlaylistRemoteEntity> {
|
|||||||
+ " WHERE " + REMOTE_PLAYLIST_SERVICE_ID + " = :serviceId")
|
+ " WHERE " + REMOTE_PLAYLIST_SERVICE_ID + " = :serviceId")
|
||||||
Flowable<List<PlaylistRemoteEntity>> listByService(int serviceId);
|
Flowable<List<PlaylistRemoteEntity>> listByService(int serviceId);
|
||||||
|
|
||||||
|
@Query("SELECT * FROM " + REMOTE_PLAYLIST_TABLE + " WHERE "
|
||||||
|
+ REMOTE_PLAYLIST_ID + " = :playlistId")
|
||||||
|
Flowable<PlaylistRemoteEntity> getPlaylist(long playlistId);
|
||||||
|
|
||||||
@Query("SELECT * FROM " + REMOTE_PLAYLIST_TABLE + " WHERE "
|
@Query("SELECT * FROM " + REMOTE_PLAYLIST_TABLE + " WHERE "
|
||||||
+ REMOTE_PLAYLIST_URL + " = :url AND " + REMOTE_PLAYLIST_SERVICE_ID + " = :serviceId")
|
+ REMOTE_PLAYLIST_URL + " = :url AND " + REMOTE_PLAYLIST_SERVICE_ID + " = :serviceId")
|
||||||
Flowable<List<PlaylistRemoteEntity>> getPlaylist(long serviceId, String url);
|
Flowable<List<PlaylistRemoteEntity>> getPlaylist(long serviceId, String url);
|
||||||
|
|
||||||
|
@Query("SELECT * FROM " + REMOTE_PLAYLIST_TABLE
|
||||||
|
+ " ORDER BY " + REMOTE_PLAYLIST_DISPLAY_INDEX)
|
||||||
|
Flowable<List<PlaylistRemoteEntity>> getPlaylists();
|
||||||
|
|
||||||
@Query("SELECT " + REMOTE_PLAYLIST_ID + " FROM " + REMOTE_PLAYLIST_TABLE
|
@Query("SELECT " + REMOTE_PLAYLIST_ID + " FROM " + REMOTE_PLAYLIST_TABLE
|
||||||
+ " WHERE " + REMOTE_PLAYLIST_URL + " = :url "
|
+ " WHERE " + REMOTE_PLAYLIST_URL + " = :url "
|
||||||
+ "AND " + REMOTE_PLAYLIST_SERVICE_ID + " = :serviceId")
|
+ "AND " + REMOTE_PLAYLIST_SERVICE_ID + " = :serviceId")
|
||||||
|
|||||||
@@ -18,10 +18,12 @@ import io.reactivex.rxjava3.core.Flowable;
|
|||||||
|
|
||||||
import static org.schabi.newpipe.database.playlist.PlaylistDuplicatesEntry.PLAYLIST_TIMES_STREAM_IS_CONTAINED;
|
import static org.schabi.newpipe.database.playlist.PlaylistDuplicatesEntry.PLAYLIST_TIMES_STREAM_IS_CONTAINED;
|
||||||
import static org.schabi.newpipe.database.playlist.PlaylistMetadataEntry.PLAYLIST_STREAM_COUNT;
|
import static org.schabi.newpipe.database.playlist.PlaylistMetadataEntry.PLAYLIST_STREAM_COUNT;
|
||||||
|
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_DISPLAY_INDEX;
|
||||||
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.DEFAULT_THUMBNAIL;
|
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.DEFAULT_THUMBNAIL;
|
||||||
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_ID;
|
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_ID;
|
||||||
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_NAME;
|
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_NAME;
|
||||||
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_TABLE;
|
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_TABLE;
|
||||||
|
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_THUMBNAIL_PERMANENT;
|
||||||
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_THUMBNAIL_STREAM_ID;
|
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_THUMBNAIL_STREAM_ID;
|
||||||
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_THUMBNAIL_URL;
|
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_THUMBNAIL_URL;
|
||||||
import static org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity.JOIN_INDEX;
|
import static org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity.JOIN_INDEX;
|
||||||
@@ -91,7 +93,9 @@ public interface PlaylistStreamDAO extends BasicDAO<PlaylistStreamEntity> {
|
|||||||
Flowable<List<PlaylistStreamEntry>> getOrderedStreamsOf(long playlistId);
|
Flowable<List<PlaylistStreamEntry>> getOrderedStreamsOf(long playlistId);
|
||||||
|
|
||||||
@Transaction
|
@Transaction
|
||||||
@Query("SELECT " + PLAYLIST_ID + ", " + PLAYLIST_NAME + ","
|
@Query("SELECT " + PLAYLIST_ID + ", " + PLAYLIST_NAME + ", "
|
||||||
|
+ PLAYLIST_THUMBNAIL_PERMANENT + ", " + PLAYLIST_THUMBNAIL_STREAM_ID + ", "
|
||||||
|
+ PLAYLIST_DISPLAY_INDEX + ", "
|
||||||
|
|
||||||
+ " CASE WHEN " + PLAYLIST_THUMBNAIL_STREAM_ID + " = "
|
+ " CASE WHEN " + PLAYLIST_THUMBNAIL_STREAM_ID + " = "
|
||||||
+ PlaylistEntity.DEFAULT_THUMBNAIL_ID + " THEN " + "'" + DEFAULT_THUMBNAIL + "'"
|
+ PlaylistEntity.DEFAULT_THUMBNAIL_ID + " THEN " + "'" + DEFAULT_THUMBNAIL + "'"
|
||||||
@@ -105,7 +109,7 @@ public interface PlaylistStreamDAO extends BasicDAO<PlaylistStreamEntity> {
|
|||||||
+ " LEFT JOIN " + PLAYLIST_STREAM_JOIN_TABLE
|
+ " LEFT JOIN " + PLAYLIST_STREAM_JOIN_TABLE
|
||||||
+ " ON " + PLAYLIST_TABLE + "." + PLAYLIST_ID + " = " + JOIN_PLAYLIST_ID
|
+ " ON " + PLAYLIST_TABLE + "." + PLAYLIST_ID + " = " + JOIN_PLAYLIST_ID
|
||||||
+ " GROUP BY " + PLAYLIST_ID
|
+ " GROUP BY " + PLAYLIST_ID
|
||||||
+ " ORDER BY " + PLAYLIST_NAME + " COLLATE NOCASE ASC")
|
+ " ORDER BY " + PLAYLIST_DISPLAY_INDEX)
|
||||||
Flowable<List<PlaylistMetadataEntry>> getPlaylistMetadata();
|
Flowable<List<PlaylistMetadataEntry>> getPlaylistMetadata();
|
||||||
|
|
||||||
@RewriteQueriesToDropUnusedColumns
|
@RewriteQueriesToDropUnusedColumns
|
||||||
@@ -126,8 +130,9 @@ public interface PlaylistStreamDAO extends BasicDAO<PlaylistStreamEntity> {
|
|||||||
Flowable<List<PlaylistStreamEntry>> getStreamsWithoutDuplicates(long playlistId);
|
Flowable<List<PlaylistStreamEntry>> getStreamsWithoutDuplicates(long playlistId);
|
||||||
|
|
||||||
@Transaction
|
@Transaction
|
||||||
@Query("SELECT " + PLAYLIST_TABLE + "." + PLAYLIST_ID + ", "
|
@Query("SELECT " + PLAYLIST_TABLE + "." + PLAYLIST_ID + ", " + PLAYLIST_NAME + ", "
|
||||||
+ PLAYLIST_NAME + ", "
|
+ PLAYLIST_THUMBNAIL_PERMANENT + ", " + PLAYLIST_THUMBNAIL_STREAM_ID + ", "
|
||||||
|
+ PLAYLIST_DISPLAY_INDEX + ", "
|
||||||
|
|
||||||
+ " CASE WHEN " + PLAYLIST_THUMBNAIL_STREAM_ID + " = "
|
+ " CASE WHEN " + PLAYLIST_THUMBNAIL_STREAM_ID + " = "
|
||||||
+ PlaylistEntity.DEFAULT_THUMBNAIL_ID + " THEN " + "'" + DEFAULT_THUMBNAIL + "'"
|
+ PlaylistEntity.DEFAULT_THUMBNAIL_ID + " THEN " + "'" + DEFAULT_THUMBNAIL + "'"
|
||||||
@@ -149,6 +154,6 @@ public interface PlaylistStreamDAO extends BasicDAO<PlaylistStreamEntity> {
|
|||||||
+ " AND :streamUrl = :streamUrl"
|
+ " AND :streamUrl = :streamUrl"
|
||||||
|
|
||||||
+ " GROUP BY " + JOIN_PLAYLIST_ID
|
+ " GROUP BY " + JOIN_PLAYLIST_ID
|
||||||
+ " ORDER BY " + PLAYLIST_NAME + " COLLATE NOCASE ASC")
|
+ " ORDER BY " + PLAYLIST_DISPLAY_INDEX + ", " + PLAYLIST_NAME)
|
||||||
Flowable<List<PlaylistDuplicatesEntry>> getPlaylistDuplicatesMetadata(String streamUrl);
|
Flowable<List<PlaylistDuplicatesEntry>> getPlaylistDuplicatesMetadata(String streamUrl);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,16 +2,15 @@ package org.schabi.newpipe.database.playlist.model;
|
|||||||
|
|
||||||
import androidx.room.ColumnInfo;
|
import androidx.room.ColumnInfo;
|
||||||
import androidx.room.Entity;
|
import androidx.room.Entity;
|
||||||
import androidx.room.Index;
|
import androidx.room.Ignore;
|
||||||
import androidx.room.PrimaryKey;
|
import androidx.room.PrimaryKey;
|
||||||
|
|
||||||
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_NAME;
|
|
||||||
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_TABLE;
|
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_TABLE;
|
||||||
|
|
||||||
import org.schabi.newpipe.R;
|
import org.schabi.newpipe.R;
|
||||||
|
import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry;
|
||||||
|
|
||||||
@Entity(tableName = PLAYLIST_TABLE,
|
@Entity(tableName = PLAYLIST_TABLE)
|
||||||
indices = {@Index(value = {PLAYLIST_NAME})})
|
|
||||||
public class PlaylistEntity {
|
public class PlaylistEntity {
|
||||||
|
|
||||||
public static final String DEFAULT_THUMBNAIL = "drawable://"
|
public static final String DEFAULT_THUMBNAIL = "drawable://"
|
||||||
@@ -22,6 +21,7 @@ public class PlaylistEntity {
|
|||||||
public static final String PLAYLIST_ID = "uid";
|
public static final String PLAYLIST_ID = "uid";
|
||||||
public static final String PLAYLIST_NAME = "name";
|
public static final String PLAYLIST_NAME = "name";
|
||||||
public static final String PLAYLIST_THUMBNAIL_URL = "thumbnail_url";
|
public static final String PLAYLIST_THUMBNAIL_URL = "thumbnail_url";
|
||||||
|
public static final String PLAYLIST_DISPLAY_INDEX = "display_index";
|
||||||
public static final String PLAYLIST_THUMBNAIL_PERMANENT = "is_thumbnail_permanent";
|
public static final String PLAYLIST_THUMBNAIL_PERMANENT = "is_thumbnail_permanent";
|
||||||
public static final String PLAYLIST_THUMBNAIL_STREAM_ID = "thumbnail_stream_id";
|
public static final String PLAYLIST_THUMBNAIL_STREAM_ID = "thumbnail_stream_id";
|
||||||
|
|
||||||
@@ -38,11 +38,24 @@ public class PlaylistEntity {
|
|||||||
@ColumnInfo(name = PLAYLIST_THUMBNAIL_STREAM_ID)
|
@ColumnInfo(name = PLAYLIST_THUMBNAIL_STREAM_ID)
|
||||||
private long thumbnailStreamId;
|
private long thumbnailStreamId;
|
||||||
|
|
||||||
|
@ColumnInfo(name = PLAYLIST_DISPLAY_INDEX)
|
||||||
|
private long displayIndex;
|
||||||
|
|
||||||
public PlaylistEntity(final String name, final boolean isThumbnailPermanent,
|
public PlaylistEntity(final String name, final boolean isThumbnailPermanent,
|
||||||
final long thumbnailStreamId) {
|
final long thumbnailStreamId, final long displayIndex) {
|
||||||
this.name = name;
|
this.name = name;
|
||||||
this.isThumbnailPermanent = isThumbnailPermanent;
|
this.isThumbnailPermanent = isThumbnailPermanent;
|
||||||
this.thumbnailStreamId = thumbnailStreamId;
|
this.thumbnailStreamId = thumbnailStreamId;
|
||||||
|
this.displayIndex = displayIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Ignore
|
||||||
|
public PlaylistEntity(final PlaylistMetadataEntry item) {
|
||||||
|
this.uid = item.getUid();
|
||||||
|
this.name = item.name;
|
||||||
|
this.isThumbnailPermanent = item.isThumbnailPermanent();
|
||||||
|
this.thumbnailStreamId = item.getThumbnailStreamId();
|
||||||
|
this.displayIndex = item.getDisplayIndex();
|
||||||
}
|
}
|
||||||
|
|
||||||
public long getUid() {
|
public long getUid() {
|
||||||
@@ -77,4 +90,11 @@ public class PlaylistEntity {
|
|||||||
this.isThumbnailPermanent = isThumbnailSet;
|
this.isThumbnailPermanent = isThumbnailSet;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public long getDisplayIndex() {
|
||||||
|
return displayIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setDisplayIndex(final long displayIndex) {
|
||||||
|
this.displayIndex = displayIndex;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package org.schabi.newpipe.database.playlist.model;
|
|||||||
|
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
|
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
import androidx.room.ColumnInfo;
|
import androidx.room.ColumnInfo;
|
||||||
import androidx.room.Entity;
|
import androidx.room.Entity;
|
||||||
import androidx.room.Ignore;
|
import androidx.room.Ignore;
|
||||||
@@ -21,7 +22,6 @@ import static org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity.RE
|
|||||||
|
|
||||||
@Entity(tableName = REMOTE_PLAYLIST_TABLE,
|
@Entity(tableName = REMOTE_PLAYLIST_TABLE,
|
||||||
indices = {
|
indices = {
|
||||||
@Index(value = {REMOTE_PLAYLIST_NAME}),
|
|
||||||
@Index(value = {REMOTE_PLAYLIST_SERVICE_ID, REMOTE_PLAYLIST_URL}, unique = true)
|
@Index(value = {REMOTE_PLAYLIST_SERVICE_ID, REMOTE_PLAYLIST_URL}, unique = true)
|
||||||
})
|
})
|
||||||
public class PlaylistRemoteEntity implements PlaylistLocalItem {
|
public class PlaylistRemoteEntity implements PlaylistLocalItem {
|
||||||
@@ -32,6 +32,7 @@ public class PlaylistRemoteEntity implements PlaylistLocalItem {
|
|||||||
public static final String REMOTE_PLAYLIST_URL = "url";
|
public static final String REMOTE_PLAYLIST_URL = "url";
|
||||||
public static final String REMOTE_PLAYLIST_THUMBNAIL_URL = "thumbnail_url";
|
public static final String REMOTE_PLAYLIST_THUMBNAIL_URL = "thumbnail_url";
|
||||||
public static final String REMOTE_PLAYLIST_UPLOADER_NAME = "uploader";
|
public static final String REMOTE_PLAYLIST_UPLOADER_NAME = "uploader";
|
||||||
|
public static final String REMOTE_PLAYLIST_DISPLAY_INDEX = "display_index";
|
||||||
public static final String REMOTE_PLAYLIST_STREAM_COUNT = "stream_count";
|
public static final String REMOTE_PLAYLIST_STREAM_COUNT = "stream_count";
|
||||||
|
|
||||||
@PrimaryKey(autoGenerate = true)
|
@PrimaryKey(autoGenerate = true)
|
||||||
@@ -53,6 +54,9 @@ public class PlaylistRemoteEntity implements PlaylistLocalItem {
|
|||||||
@ColumnInfo(name = REMOTE_PLAYLIST_UPLOADER_NAME)
|
@ColumnInfo(name = REMOTE_PLAYLIST_UPLOADER_NAME)
|
||||||
private String uploader;
|
private String uploader;
|
||||||
|
|
||||||
|
@ColumnInfo(name = REMOTE_PLAYLIST_DISPLAY_INDEX)
|
||||||
|
private long displayIndex = -1; // Make sure the new item is on the top
|
||||||
|
|
||||||
@ColumnInfo(name = REMOTE_PLAYLIST_STREAM_COUNT)
|
@ColumnInfo(name = REMOTE_PLAYLIST_STREAM_COUNT)
|
||||||
private Long streamCount;
|
private Long streamCount;
|
||||||
|
|
||||||
@@ -67,6 +71,19 @@ public class PlaylistRemoteEntity implements PlaylistLocalItem {
|
|||||||
this.streamCount = streamCount;
|
this.streamCount = streamCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Ignore
|
||||||
|
public PlaylistRemoteEntity(final int serviceId, final String name, final String url,
|
||||||
|
final String thumbnailUrl, final String uploader,
|
||||||
|
final long displayIndex, final Long streamCount) {
|
||||||
|
this.serviceId = serviceId;
|
||||||
|
this.name = name;
|
||||||
|
this.url = url;
|
||||||
|
this.thumbnailUrl = thumbnailUrl;
|
||||||
|
this.uploader = uploader;
|
||||||
|
this.displayIndex = displayIndex;
|
||||||
|
this.streamCount = streamCount;
|
||||||
|
}
|
||||||
|
|
||||||
@Ignore
|
@Ignore
|
||||||
public PlaylistRemoteEntity(final PlaylistInfo info) {
|
public PlaylistRemoteEntity(final PlaylistInfo info) {
|
||||||
this(info.getServiceId(), info.getName(), info.getUrl(),
|
this(info.getServiceId(), info.getName(), info.getUrl(),
|
||||||
@@ -93,6 +110,7 @@ public class PlaylistRemoteEntity implements PlaylistLocalItem {
|
|||||||
&& TextUtils.equals(getUploader(), info.getUploaderName());
|
&& TextUtils.equals(getUploader(), info.getUploaderName());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
public long getUid() {
|
public long getUid() {
|
||||||
return uid;
|
return uid;
|
||||||
}
|
}
|
||||||
@@ -117,6 +135,8 @@ public class PlaylistRemoteEntity implements PlaylistLocalItem {
|
|||||||
this.name = name;
|
this.name = name;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
@Override
|
||||||
public String getThumbnailUrl() {
|
public String getThumbnailUrl() {
|
||||||
return thumbnailUrl;
|
return thumbnailUrl;
|
||||||
}
|
}
|
||||||
@@ -141,6 +161,16 @@ public class PlaylistRemoteEntity implements PlaylistLocalItem {
|
|||||||
this.uploader = uploader;
|
this.uploader = uploader;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long getDisplayIndex() {
|
||||||
|
return displayIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setDisplayIndex(final long displayIndex) {
|
||||||
|
this.displayIndex = displayIndex;
|
||||||
|
}
|
||||||
|
|
||||||
public Long getStreamCount() {
|
public Long getStreamCount() {
|
||||||
return streamCount;
|
return streamCount;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import androidx.room.Query
|
|||||||
import androidx.room.Transaction
|
import androidx.room.Transaction
|
||||||
import io.reactivex.rxjava3.core.Completable
|
import io.reactivex.rxjava3.core.Completable
|
||||||
import io.reactivex.rxjava3.core.Flowable
|
import io.reactivex.rxjava3.core.Flowable
|
||||||
|
import io.reactivex.rxjava3.core.Maybe
|
||||||
import org.schabi.newpipe.database.BasicDAO
|
import org.schabi.newpipe.database.BasicDAO
|
||||||
import org.schabi.newpipe.database.stream.model.StreamEntity
|
import org.schabi.newpipe.database.stream.model.StreamEntity
|
||||||
import org.schabi.newpipe.database.stream.model.StreamEntity.Companion.STREAM_ID
|
import org.schabi.newpipe.database.stream.model.StreamEntity.Companion.STREAM_ID
|
||||||
@@ -27,7 +28,7 @@ abstract class StreamDAO : BasicDAO<StreamEntity> {
|
|||||||
abstract override fun listByService(serviceId: Int): Flowable<List<StreamEntity>>
|
abstract override fun listByService(serviceId: Int): Flowable<List<StreamEntity>>
|
||||||
|
|
||||||
@Query("SELECT * FROM streams WHERE url = :url AND service_id = :serviceId")
|
@Query("SELECT * FROM streams WHERE url = :url AND service_id = :serviceId")
|
||||||
abstract fun getStream(serviceId: Long, url: String): Flowable<List<StreamEntity>>
|
abstract fun getStream(serviceId: Long, url: String): Maybe<StreamEntity>
|
||||||
|
|
||||||
@Query("UPDATE streams SET uploader_url = :uploaderUrl WHERE url = :url AND service_id = :serviceId")
|
@Query("UPDATE streams SET uploader_url = :uploaderUrl WHERE url = :url AND service_id = :serviceId")
|
||||||
abstract fun setUploaderUrl(serviceId: Long, url: String, uploaderUrl: String): Completable
|
abstract fun setUploaderUrl(serviceId: Long, url: String, uploaderUrl: String): Completable
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
package org.schabi.newpipe.database.stream.dao;
|
package org.schabi.newpipe.database.stream.dao;
|
||||||
|
|
||||||
|
import static org.schabi.newpipe.database.stream.model.StreamStateEntity.JOIN_STREAM_ID;
|
||||||
|
import static org.schabi.newpipe.database.stream.model.StreamStateEntity.STREAM_STATE_TABLE;
|
||||||
|
|
||||||
import androidx.room.Dao;
|
import androidx.room.Dao;
|
||||||
import androidx.room.Insert;
|
import androidx.room.Insert;
|
||||||
import androidx.room.OnConflictStrategy;
|
import androidx.room.OnConflictStrategy;
|
||||||
@@ -12,9 +15,7 @@ import org.schabi.newpipe.database.stream.model.StreamStateEntity;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import io.reactivex.rxjava3.core.Flowable;
|
import io.reactivex.rxjava3.core.Flowable;
|
||||||
|
import io.reactivex.rxjava3.core.Maybe;
|
||||||
import static org.schabi.newpipe.database.stream.model.StreamStateEntity.JOIN_STREAM_ID;
|
|
||||||
import static org.schabi.newpipe.database.stream.model.StreamStateEntity.STREAM_STATE_TABLE;
|
|
||||||
|
|
||||||
@Dao
|
@Dao
|
||||||
public interface StreamStateDAO extends BasicDAO<StreamStateEntity> {
|
public interface StreamStateDAO extends BasicDAO<StreamStateEntity> {
|
||||||
@@ -32,7 +33,7 @@ public interface StreamStateDAO extends BasicDAO<StreamStateEntity> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Query("SELECT * FROM " + STREAM_STATE_TABLE + " WHERE " + JOIN_STREAM_ID + " = :streamId")
|
@Query("SELECT * FROM " + STREAM_STATE_TABLE + " WHERE " + JOIN_STREAM_ID + " = :streamId")
|
||||||
Flowable<List<StreamStateEntity>> getState(long streamId);
|
Maybe<StreamStateEntity> getState(long streamId);
|
||||||
|
|
||||||
@Query("DELETE FROM " + STREAM_STATE_TABLE + " WHERE " + JOIN_STREAM_ID + " = :streamId")
|
@Query("DELETE FROM " + STREAM_STATE_TABLE + " WHERE " + JOIN_STREAM_ID + " = :streamId")
|
||||||
int deleteState(long streamId);
|
int deleteState(long streamId);
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package org.schabi.newpipe.database.subscription;
|
package org.schabi.newpipe.database.subscription;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
import androidx.room.ColumnInfo;
|
import androidx.room.ColumnInfo;
|
||||||
import androidx.room.Entity;
|
import androidx.room.Entity;
|
||||||
import androidx.room.Ignore;
|
import androidx.room.Ignore;
|
||||||
@@ -95,11 +96,12 @@ public class SubscriptionEntity {
|
|||||||
this.name = name;
|
this.name = name;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
public String getAvatarUrl() {
|
public String getAvatarUrl() {
|
||||||
return avatarUrl;
|
return avatarUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setAvatarUrl(final String avatarUrl) {
|
public void setAvatarUrl(@Nullable final String avatarUrl) {
|
||||||
this.avatarUrl = avatarUrl;
|
this.avatarUrl = avatarUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,8 +7,6 @@ import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
|
|||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
import android.content.ComponentName;
|
import android.content.ComponentName;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.DialogInterface;
|
|
||||||
import android.content.DialogInterface.OnDismissListener;
|
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.ServiceConnection;
|
import android.content.ServiceConnection;
|
||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
@@ -16,6 +14,7 @@ import android.net.Uri;
|
|||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.os.Environment;
|
import android.os.Environment;
|
||||||
import android.os.IBinder;
|
import android.os.IBinder;
|
||||||
|
import android.provider.Settings;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
@@ -40,6 +39,8 @@ import androidx.documentfile.provider.DocumentFile;
|
|||||||
import androidx.fragment.app.DialogFragment;
|
import androidx.fragment.app.DialogFragment;
|
||||||
import androidx.preference.PreferenceManager;
|
import androidx.preference.PreferenceManager;
|
||||||
|
|
||||||
|
import com.evernote.android.state.State;
|
||||||
|
import com.livefront.bridge.Bridge;
|
||||||
import com.nononsenseapps.filepicker.Utils;
|
import com.nononsenseapps.filepicker.Utils;
|
||||||
|
|
||||||
import org.schabi.newpipe.MainActivity;
|
import org.schabi.newpipe.MainActivity;
|
||||||
@@ -60,6 +61,8 @@ import org.schabi.newpipe.settings.NewPipeSettings;
|
|||||||
import org.schabi.newpipe.streams.io.NoFileManagerSafeGuard;
|
import org.schabi.newpipe.streams.io.NoFileManagerSafeGuard;
|
||||||
import org.schabi.newpipe.streams.io.StoredDirectoryHelper;
|
import org.schabi.newpipe.streams.io.StoredDirectoryHelper;
|
||||||
import org.schabi.newpipe.streams.io.StoredFileHelper;
|
import org.schabi.newpipe.streams.io.StoredFileHelper;
|
||||||
|
import org.schabi.newpipe.util.AudioTrackAdapter;
|
||||||
|
import org.schabi.newpipe.util.AudioTrackAdapter.AudioTracksWrapper;
|
||||||
import org.schabi.newpipe.util.FilePickerActivityHelper;
|
import org.schabi.newpipe.util.FilePickerActivityHelper;
|
||||||
import org.schabi.newpipe.util.FilenameUtils;
|
import org.schabi.newpipe.util.FilenameUtils;
|
||||||
import org.schabi.newpipe.util.ListHelper;
|
import org.schabi.newpipe.util.ListHelper;
|
||||||
@@ -68,19 +71,16 @@ import org.schabi.newpipe.util.SecondaryStreamHelper;
|
|||||||
import org.schabi.newpipe.util.SimpleOnSeekBarChangeListener;
|
import org.schabi.newpipe.util.SimpleOnSeekBarChangeListener;
|
||||||
import org.schabi.newpipe.util.StreamItemAdapter;
|
import org.schabi.newpipe.util.StreamItemAdapter;
|
||||||
import org.schabi.newpipe.util.StreamItemAdapter.StreamInfoWrapper;
|
import org.schabi.newpipe.util.StreamItemAdapter.StreamInfoWrapper;
|
||||||
import org.schabi.newpipe.util.AudioTrackAdapter;
|
|
||||||
import org.schabi.newpipe.util.AudioTrackAdapter.AudioTracksWrapper;
|
|
||||||
import org.schabi.newpipe.util.ThemeHelper;
|
import org.schabi.newpipe.util.ThemeHelper;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
import icepick.Icepick;
|
|
||||||
import icepick.State;
|
|
||||||
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
||||||
import us.shandian.giga.get.MissionRecoveryInfo;
|
import us.shandian.giga.get.MissionRecoveryInfo;
|
||||||
import us.shandian.giga.postprocessing.Postprocessing;
|
import us.shandian.giga.postprocessing.Postprocessing;
|
||||||
@@ -111,14 +111,11 @@ public class DownloadDialog extends DialogFragment
|
|||||||
@State
|
@State
|
||||||
int selectedSubtitleIndex = 0; // default to the first item
|
int selectedSubtitleIndex = 0; // default to the first item
|
||||||
|
|
||||||
@Nullable
|
|
||||||
private OnDismissListener onDismissListener = null;
|
|
||||||
|
|
||||||
private StoredDirectoryHelper mainStorageAudio = null;
|
private StoredDirectoryHelper mainStorageAudio = null;
|
||||||
private StoredDirectoryHelper mainStorageVideo = null;
|
private StoredDirectoryHelper mainStorageVideo = null;
|
||||||
private DownloadManager downloadManager = null;
|
private DownloadManager downloadManager = null;
|
||||||
private ActionMenuItemView okButton = null;
|
private ActionMenuItemView okButton = null;
|
||||||
private Context context;
|
private Context context = null;
|
||||||
private boolean askForSavePath;
|
private boolean askForSavePath;
|
||||||
|
|
||||||
private AudioTrackAdapter audioTrackAdapter;
|
private AudioTrackAdapter audioTrackAdapter;
|
||||||
@@ -146,7 +143,6 @@ public class DownloadDialog extends DialogFragment
|
|||||||
registerForActivityResult(
|
registerForActivityResult(
|
||||||
new StartActivityForResult(), this::requestDownloadPickVideoFolderResult);
|
new StartActivityForResult(), this::requestDownloadPickVideoFolderResult);
|
||||||
|
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
// Instance creation
|
// Instance creation
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
@@ -194,13 +190,6 @@ public class DownloadDialog extends DialogFragment
|
|||||||
this.selectedVideoIndex = ListHelper.getDefaultResolutionIndex(context, videoStreams);
|
this.selectedVideoIndex = ListHelper.getDefaultResolutionIndex(context, videoStreams);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @param onDismissListener the listener to call in {@link #onDismiss(DialogInterface)}
|
|
||||||
*/
|
|
||||||
public void setOnDismissListener(@Nullable final OnDismissListener onDismissListener) {
|
|
||||||
this.onDismissListener = onDismissListener;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
// Android lifecycle
|
// Android lifecycle
|
||||||
@@ -220,10 +209,12 @@ public class DownloadDialog extends DialogFragment
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// context will remain null if dismiss() was called above, allowing to check whether the
|
||||||
|
// dialog is being dismissed in onViewCreated()
|
||||||
context = getContext();
|
context = getContext();
|
||||||
|
|
||||||
setStyle(STYLE_NO_TITLE, ThemeHelper.getDialogTheme(context));
|
setStyle(STYLE_NO_TITLE, ThemeHelper.getDialogTheme(context));
|
||||||
Icepick.restoreInstanceState(this, savedInstanceState);
|
Bridge.restoreInstanceState(this, savedInstanceState);
|
||||||
|
|
||||||
this.audioTrackAdapter = new AudioTrackAdapter(wrappedAudioTracks);
|
this.audioTrackAdapter = new AudioTrackAdapter(wrappedAudioTracks);
|
||||||
this.subtitleStreamsAdapter = new StreamItemAdapter<>(wrappedSubtitleStreams);
|
this.subtitleStreamsAdapter = new StreamItemAdapter<>(wrappedSubtitleStreams);
|
||||||
@@ -304,6 +295,9 @@ public class DownloadDialog extends DialogFragment
|
|||||||
@Nullable final Bundle savedInstanceState) {
|
@Nullable final Bundle savedInstanceState) {
|
||||||
super.onViewCreated(view, savedInstanceState);
|
super.onViewCreated(view, savedInstanceState);
|
||||||
dialogBinding = DownloadDialogBinding.bind(view);
|
dialogBinding = DownloadDialogBinding.bind(view);
|
||||||
|
if (context == null) {
|
||||||
|
return; // the dialog is being dismissed, see the call to dismiss() in onCreate()
|
||||||
|
}
|
||||||
|
|
||||||
dialogBinding.fileName.setText(FilenameUtils.createFilename(getContext(),
|
dialogBinding.fileName.setText(FilenameUtils.createFilename(getContext(),
|
||||||
currentInfo.getName()));
|
currentInfo.getName()));
|
||||||
@@ -363,14 +357,6 @@ public class DownloadDialog extends DialogFragment
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onDismiss(@NonNull final DialogInterface dialog) {
|
|
||||||
super.onDismiss(dialog);
|
|
||||||
if (onDismissListener != null) {
|
|
||||||
onDismissListener.onDismiss(dialog);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onDestroy() {
|
public void onDestroy() {
|
||||||
super.onDestroy();
|
super.onDestroy();
|
||||||
@@ -386,7 +372,7 @@ public class DownloadDialog extends DialogFragment
|
|||||||
@Override
|
@Override
|
||||||
public void onSaveInstanceState(@NonNull final Bundle outState) {
|
public void onSaveInstanceState(@NonNull final Bundle outState) {
|
||||||
super.onSaveInstanceState(outState);
|
super.onSaveInstanceState(outState);
|
||||||
Icepick.saveInstanceState(this, outState);
|
Bridge.saveInstanceState(this, outState);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -564,7 +550,6 @@ public class DownloadDialog extends DialogFragment
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
// Listeners
|
// Listeners
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
@@ -783,6 +768,7 @@ public class DownloadDialog extends DialogFragment
|
|||||||
final StoredDirectoryHelper mainStorage;
|
final StoredDirectoryHelper mainStorage;
|
||||||
final MediaFormat format;
|
final MediaFormat format;
|
||||||
final String selectedMediaType;
|
final String selectedMediaType;
|
||||||
|
final long size;
|
||||||
|
|
||||||
// first, build the filename and get the output folder (if possible)
|
// first, build the filename and get the output folder (if possible)
|
||||||
// later, run a very very very large file checking logic
|
// later, run a very very very large file checking logic
|
||||||
@@ -794,6 +780,7 @@ public class DownloadDialog extends DialogFragment
|
|||||||
selectedMediaType = getString(R.string.last_download_type_audio_key);
|
selectedMediaType = getString(R.string.last_download_type_audio_key);
|
||||||
mainStorage = mainStorageAudio;
|
mainStorage = mainStorageAudio;
|
||||||
format = audioStreamsAdapter.getItem(selectedAudioIndex).getFormat();
|
format = audioStreamsAdapter.getItem(selectedAudioIndex).getFormat();
|
||||||
|
size = getWrappedAudioStreams().getSizeInBytes(selectedAudioIndex);
|
||||||
if (format == MediaFormat.WEBMA_OPUS) {
|
if (format == MediaFormat.WEBMA_OPUS) {
|
||||||
mimeTmp = "audio/ogg";
|
mimeTmp = "audio/ogg";
|
||||||
filenameTmp += "opus";
|
filenameTmp += "opus";
|
||||||
@@ -806,6 +793,7 @@ public class DownloadDialog extends DialogFragment
|
|||||||
selectedMediaType = getString(R.string.last_download_type_video_key);
|
selectedMediaType = getString(R.string.last_download_type_video_key);
|
||||||
mainStorage = mainStorageVideo;
|
mainStorage = mainStorageVideo;
|
||||||
format = videoStreamsAdapter.getItem(selectedVideoIndex).getFormat();
|
format = videoStreamsAdapter.getItem(selectedVideoIndex).getFormat();
|
||||||
|
size = wrappedVideoStreams.getSizeInBytes(selectedVideoIndex);
|
||||||
if (format != null) {
|
if (format != null) {
|
||||||
mimeTmp = format.mimeType;
|
mimeTmp = format.mimeType;
|
||||||
filenameTmp += format.getSuffix();
|
filenameTmp += format.getSuffix();
|
||||||
@@ -815,6 +803,7 @@ public class DownloadDialog extends DialogFragment
|
|||||||
selectedMediaType = getString(R.string.last_download_type_subtitle_key);
|
selectedMediaType = getString(R.string.last_download_type_subtitle_key);
|
||||||
mainStorage = mainStorageVideo; // subtitle & video files go together
|
mainStorage = mainStorageVideo; // subtitle & video files go together
|
||||||
format = subtitleStreamsAdapter.getItem(selectedSubtitleIndex).getFormat();
|
format = subtitleStreamsAdapter.getItem(selectedSubtitleIndex).getFormat();
|
||||||
|
size = wrappedSubtitleStreams.getSizeInBytes(selectedSubtitleIndex);
|
||||||
if (format != null) {
|
if (format != null) {
|
||||||
mimeTmp = format.mimeType;
|
mimeTmp = format.mimeType;
|
||||||
}
|
}
|
||||||
@@ -870,6 +859,21 @@ public class DownloadDialog extends DialogFragment
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check for free storage space
|
||||||
|
final long freeSpace = mainStorage.getFreeStorageSpace();
|
||||||
|
if (freeSpace <= size) {
|
||||||
|
Toast.makeText(context, getString(R.
|
||||||
|
string.error_insufficient_storage), Toast.LENGTH_LONG).show();
|
||||||
|
// move the user to storage setting tab
|
||||||
|
final Intent storageSettingsIntent = new Intent(Settings.
|
||||||
|
ACTION_INTERNAL_STORAGE_SETTINGS);
|
||||||
|
if (storageSettingsIntent.resolveActivity(context.getPackageManager())
|
||||||
|
!= null) {
|
||||||
|
startActivity(storageSettingsIntent);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// check for existing file with the same name
|
// check for existing file with the same name
|
||||||
checkSelectedDownload(mainStorage, mainStorage.findFile(filenameTmp), filenameTmp,
|
checkSelectedDownload(mainStorage, mainStorage.findFile(filenameTmp), filenameTmp,
|
||||||
mimeTmp);
|
mimeTmp);
|
||||||
@@ -1052,7 +1056,7 @@ public class DownloadDialog extends DialogFragment
|
|||||||
final char kind;
|
final char kind;
|
||||||
int threads = dialogBinding.threads.getProgress() + 1;
|
int threads = dialogBinding.threads.getProgress() + 1;
|
||||||
final String[] urls;
|
final String[] urls;
|
||||||
final MissionRecoveryInfo[] recoveryInfo;
|
final List<MissionRecoveryInfo> recoveryInfo;
|
||||||
String psName = null;
|
String psName = null;
|
||||||
String[] psArgs = null;
|
String[] psArgs = null;
|
||||||
long nearLength = 0;
|
long nearLength = 0;
|
||||||
@@ -1117,9 +1121,7 @@ public class DownloadDialog extends DialogFragment
|
|||||||
urls = new String[] {
|
urls = new String[] {
|
||||||
selectedStream.getContent()
|
selectedStream.getContent()
|
||||||
};
|
};
|
||||||
recoveryInfo = new MissionRecoveryInfo[] {
|
recoveryInfo = List.of(new MissionRecoveryInfo(selectedStream));
|
||||||
new MissionRecoveryInfo(selectedStream)
|
|
||||||
};
|
|
||||||
} else {
|
} else {
|
||||||
if (secondaryStream.getDeliveryMethod() != PROGRESSIVE_HTTP) {
|
if (secondaryStream.getDeliveryMethod() != PROGRESSIVE_HTTP) {
|
||||||
throw new IllegalArgumentException("Unsupported stream delivery format"
|
throw new IllegalArgumentException("Unsupported stream delivery format"
|
||||||
@@ -1129,12 +1131,14 @@ public class DownloadDialog extends DialogFragment
|
|||||||
urls = new String[] {
|
urls = new String[] {
|
||||||
selectedStream.getContent(), secondaryStream.getContent()
|
selectedStream.getContent(), secondaryStream.getContent()
|
||||||
};
|
};
|
||||||
recoveryInfo = new MissionRecoveryInfo[] {new MissionRecoveryInfo(selectedStream),
|
recoveryInfo = List.of(
|
||||||
new MissionRecoveryInfo(secondaryStream)};
|
new MissionRecoveryInfo(selectedStream),
|
||||||
|
new MissionRecoveryInfo(secondaryStream)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
DownloadManagerService.startMission(context, urls, storage, kind, threads,
|
DownloadManagerService.startMission(context, urls, storage, kind, threads,
|
||||||
currentInfo.getUrl(), psName, psArgs, nearLength, recoveryInfo);
|
currentInfo.getUrl(), psName, psArgs, nearLength, new ArrayList<>(recoveryInfo));
|
||||||
|
|
||||||
Toast.makeText(context, getString(R.string.download_has_started),
|
Toast.makeText(context, getString(R.string.download_has_started),
|
||||||
Toast.LENGTH_SHORT).show();
|
Toast.LENGTH_SHORT).show();
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import com.google.auto.service.AutoService;
|
|||||||
import org.acra.config.CoreConfiguration;
|
import org.acra.config.CoreConfiguration;
|
||||||
import org.acra.sender.ReportSender;
|
import org.acra.sender.ReportSender;
|
||||||
import org.acra.sender.ReportSenderFactory;
|
import org.acra.sender.ReportSenderFactory;
|
||||||
import org.schabi.newpipe.App;
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Created by Christian Schabesberger on 13.09.16.
|
* Created by Christian Schabesberger on 13.09.16.
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package org.schabi.newpipe.error;
|
|||||||
|
|
||||||
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
|
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
|
||||||
|
|
||||||
import android.app.Activity;
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
@@ -13,22 +12,21 @@ import android.view.Menu;
|
|||||||
import android.view.MenuInflater;
|
import android.view.MenuInflater;
|
||||||
import android.view.MenuItem;
|
import android.view.MenuItem;
|
||||||
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import androidx.appcompat.app.ActionBar;
|
import androidx.appcompat.app.ActionBar;
|
||||||
import androidx.appcompat.app.AlertDialog;
|
import androidx.appcompat.app.AlertDialog;
|
||||||
import androidx.appcompat.app.AppCompatActivity;
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
|
import androidx.core.content.IntentCompat;
|
||||||
|
|
||||||
import com.grack.nanojson.JsonWriter;
|
import com.grack.nanojson.JsonWriter;
|
||||||
|
|
||||||
import org.schabi.newpipe.BuildConfig;
|
import org.schabi.newpipe.BuildConfig;
|
||||||
import org.schabi.newpipe.MainActivity;
|
|
||||||
import org.schabi.newpipe.R;
|
import org.schabi.newpipe.R;
|
||||||
import org.schabi.newpipe.databinding.ActivityErrorBinding;
|
import org.schabi.newpipe.databinding.ActivityErrorBinding;
|
||||||
import org.schabi.newpipe.util.Localization;
|
import org.schabi.newpipe.util.Localization;
|
||||||
import org.schabi.newpipe.util.ThemeHelper;
|
import org.schabi.newpipe.util.ThemeHelper;
|
||||||
import org.schabi.newpipe.util.external_communication.ShareUtils;
|
import org.schabi.newpipe.util.external_communication.ShareUtils;
|
||||||
|
|
||||||
import java.time.LocalDateTime;
|
import java.time.ZonedDateTime;
|
||||||
import java.time.format.DateTimeFormatter;
|
import java.time.format.DateTimeFormatter;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
@@ -69,10 +67,6 @@ public class ErrorActivity extends AppCompatActivity {
|
|||||||
public static final String ERROR_GITHUB_ISSUE_URL =
|
public static final String ERROR_GITHUB_ISSUE_URL =
|
||||||
"https://github.com/TeamNewPipe/NewPipe/issues";
|
"https://github.com/TeamNewPipe/NewPipe/issues";
|
||||||
|
|
||||||
public static final DateTimeFormatter CURRENT_TIMESTAMP_FORMATTER =
|
|
||||||
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm");
|
|
||||||
|
|
||||||
|
|
||||||
private ErrorInfo errorInfo;
|
private ErrorInfo errorInfo;
|
||||||
private String currentTimeStamp;
|
private String currentTimeStamp;
|
||||||
|
|
||||||
@@ -105,11 +99,13 @@ public class ErrorActivity extends AppCompatActivity {
|
|||||||
actionBar.setDisplayShowTitleEnabled(true);
|
actionBar.setDisplayShowTitleEnabled(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
errorInfo = intent.getParcelableExtra(ERROR_INFO);
|
errorInfo = IntentCompat.getParcelableExtra(intent, ERROR_INFO, ErrorInfo.class);
|
||||||
|
|
||||||
// important add guru meditation
|
// important add guru meditation
|
||||||
addGuruMeditation();
|
addGuruMeditation();
|
||||||
currentTimeStamp = CURRENT_TIMESTAMP_FORMATTER.format(LocalDateTime.now());
|
// print current time, as zoned ISO8601 timestamp
|
||||||
|
final ZonedDateTime now = ZonedDateTime.now();
|
||||||
|
currentTimeStamp = now.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME);
|
||||||
|
|
||||||
activityErrorBinding.errorReportEmailButton.setOnClickListener(v ->
|
activityErrorBinding.errorReportEmailButton.setOnClickListener(v ->
|
||||||
openPrivacyPolicyDialog(this, "EMAIL"));
|
openPrivacyPolicyDialog(this, "EMAIL"));
|
||||||
@@ -186,25 +182,6 @@ public class ErrorActivity extends AppCompatActivity {
|
|||||||
.collect(Collectors.joining(separator + "\n", separator + "\n", separator));
|
.collect(Collectors.joining(separator + "\n", separator + "\n", separator));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the checked activity.
|
|
||||||
*
|
|
||||||
* @param returnActivity the activity to return to
|
|
||||||
* @return the casted return activity or null
|
|
||||||
*/
|
|
||||||
@Nullable
|
|
||||||
static Class<? extends Activity> getReturnActivity(final Class<?> returnActivity) {
|
|
||||||
Class<? extends Activity> checkedReturnActivity = null;
|
|
||||||
if (returnActivity != null) {
|
|
||||||
if (Activity.class.isAssignableFrom(returnActivity)) {
|
|
||||||
checkedReturnActivity = returnActivity.asSubclass(Activity.class);
|
|
||||||
} else {
|
|
||||||
checkedReturnActivity = MainActivity.class;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return checkedReturnActivity;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void buildInfo(final ErrorInfo info) {
|
private void buildInfo(final ErrorInfo info) {
|
||||||
String text = "";
|
String text = "";
|
||||||
|
|
||||||
@@ -271,6 +248,9 @@ public class ErrorActivity extends AppCompatActivity {
|
|||||||
.append("\n* __Content Language:__ ").append(getContentLanguageString())
|
.append("\n* __Content Language:__ ").append(getContentLanguageString())
|
||||||
.append("\n* __App Language:__ ").append(getAppLanguage())
|
.append("\n* __App Language:__ ").append(getAppLanguage())
|
||||||
.append("\n* __Service:__ ").append(errorInfo.getServiceName())
|
.append("\n* __Service:__ ").append(errorInfo.getServiceName())
|
||||||
|
.append("\n* __Timestamp:__ ").append(currentTimeStamp)
|
||||||
|
.append("\n* __Package:__ ").append(getPackageName())
|
||||||
|
.append("\n* __Service:__ ").append(errorInfo.getServiceName())
|
||||||
.append("\n* __Version:__ ").append(BuildConfig.VERSION_NAME)
|
.append("\n* __Version:__ ").append(BuildConfig.VERSION_NAME)
|
||||||
.append("\n* __OS:__ ").append(getOsString()).append("\n");
|
.append("\n* __OS:__ ").append(getOsString()).append("\n");
|
||||||
|
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ class ErrorUtil {
|
|||||||
*/
|
*/
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun showSnackbar(context: Context, errorInfo: ErrorInfo) {
|
fun showSnackbar(context: Context, errorInfo: ErrorInfo) {
|
||||||
val rootView = if (context is Activity) context.findViewById<View>(R.id.content) else null
|
val rootView = (context as? Activity)?.findViewById<View>(android.R.id.content)
|
||||||
showSnackbar(context, rootView, errorInfo)
|
showSnackbar(context, rootView, errorInfo)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,7 +71,7 @@ class ErrorUtil {
|
|||||||
fun showSnackbar(fragment: Fragment, errorInfo: ErrorInfo) {
|
fun showSnackbar(fragment: Fragment, errorInfo: ErrorInfo) {
|
||||||
var rootView = fragment.view
|
var rootView = fragment.view
|
||||||
if (rootView == null && fragment.activity != null) {
|
if (rootView == null && fragment.activity != null) {
|
||||||
rootView = fragment.requireActivity().findViewById(R.id.content)
|
rootView = fragment.requireActivity().findViewById(android.R.id.content)
|
||||||
}
|
}
|
||||||
showSnackbar(fragment.requireContext(), rootView, errorInfo)
|
showSnackbar(fragment.requireContext(), rootView, errorInfo)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,8 +27,6 @@ import org.schabi.newpipe.databinding.ActivityRecaptchaBinding;
|
|||||||
import org.schabi.newpipe.extractor.utils.Utils;
|
import org.schabi.newpipe.extractor.utils.Utils;
|
||||||
import org.schabi.newpipe.util.ThemeHelper;
|
import org.schabi.newpipe.util.ThemeHelper;
|
||||||
|
|
||||||
import java.io.UnsupportedEncodingException;
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Created by beneth <bmauduit@beneth.fr> on 06.12.16.
|
* Created by beneth <bmauduit@beneth.fr> on 06.12.16.
|
||||||
*
|
*
|
||||||
@@ -187,14 +185,11 @@ public class ReCaptchaActivity extends AppCompatActivity {
|
|||||||
final int abuseEnd = url.indexOf("+path");
|
final int abuseEnd = url.indexOf("+path");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
String abuseCookie = url.substring(abuseStart + 13, abuseEnd);
|
handleCookies(Utils.decodeUrlUtf8(url.substring(abuseStart + 13, abuseEnd)));
|
||||||
abuseCookie = Utils.decodeUrlUtf8(abuseCookie);
|
} catch (final StringIndexOutOfBoundsException e) {
|
||||||
handleCookies(abuseCookie);
|
|
||||||
} catch (UnsupportedEncodingException | StringIndexOutOfBoundsException e) {
|
|
||||||
if (MainActivity.DEBUG) {
|
if (MainActivity.DEBUG) {
|
||||||
e.printStackTrace();
|
Log.e(TAG, "handleCookiesFromUrl: invalid google abuse starting at "
|
||||||
Log.d(TAG, "handleCookiesFromUrl: invalid google abuse starting at "
|
+ abuseStart + " and ending at " + abuseEnd + " for url " + url, e);
|
||||||
+ abuseStart + " and ending at " + abuseEnd + " for url " + url);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ package org.schabi.newpipe.error;
|
|||||||
public enum UserAction {
|
public enum UserAction {
|
||||||
USER_REPORT("user report"),
|
USER_REPORT("user report"),
|
||||||
UI_ERROR("ui error"),
|
UI_ERROR("ui error"),
|
||||||
|
DATABASE_IMPORT_EXPORT("database import or export"),
|
||||||
SUBSCRIPTION_CHANGE("subscription change"),
|
SUBSCRIPTION_CHANGE("subscription change"),
|
||||||
SUBSCRIPTION_UPDATE("subscription update"),
|
SUBSCRIPTION_UPDATE("subscription update"),
|
||||||
SUBSCRIPTION_GET("get subscription"),
|
SUBSCRIPTION_GET("get subscription"),
|
||||||
@@ -19,6 +20,7 @@ public enum UserAction {
|
|||||||
REQUESTED_PLAYLIST("requested playlist"),
|
REQUESTED_PLAYLIST("requested playlist"),
|
||||||
REQUESTED_KIOSK("requested kiosk"),
|
REQUESTED_KIOSK("requested kiosk"),
|
||||||
REQUESTED_COMMENTS("requested comments"),
|
REQUESTED_COMMENTS("requested comments"),
|
||||||
|
REQUESTED_COMMENT_REPLIES("requested comment replies"),
|
||||||
REQUESTED_FEED("requested feed"),
|
REQUESTED_FEED("requested feed"),
|
||||||
REQUESTED_BOOKMARK("bookmark"),
|
REQUESTED_BOOKMARK("bookmark"),
|
||||||
DELETE_FROM_HISTORY("delete from history"),
|
DELETE_FROM_HISTORY("delete from history"),
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ import androidx.annotation.Nullable;
|
|||||||
import androidx.annotation.StringRes;
|
import androidx.annotation.StringRes;
|
||||||
import androidx.fragment.app.Fragment;
|
import androidx.fragment.app.Fragment;
|
||||||
|
|
||||||
|
import com.evernote.android.state.State;
|
||||||
|
|
||||||
import org.schabi.newpipe.BaseFragment;
|
import org.schabi.newpipe.BaseFragment;
|
||||||
import org.schabi.newpipe.R;
|
import org.schabi.newpipe.R;
|
||||||
import org.schabi.newpipe.error.ErrorInfo;
|
import org.schabi.newpipe.error.ErrorInfo;
|
||||||
@@ -22,8 +24,6 @@ import org.schabi.newpipe.util.InfoCache;
|
|||||||
|
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
|
||||||
import icepick.State;
|
|
||||||
|
|
||||||
public abstract class BaseStateFragment<I> extends BaseFragment implements ViewContract<I> {
|
public abstract class BaseStateFragment<I> extends BaseFragment implements ViewContract<I> {
|
||||||
@State
|
@State
|
||||||
protected AtomicBoolean wasLoading = new AtomicBoolean();
|
protected AtomicBoolean wasLoading = new AtomicBoolean();
|
||||||
@@ -134,6 +134,7 @@ public abstract class BaseStateFragment<I> extends BaseFragment implements ViewC
|
|||||||
hideErrorPanel();
|
hideErrorPanel();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
public void showEmptyState() {
|
public void showEmptyState() {
|
||||||
isLoading.set(false);
|
isLoading.set(false);
|
||||||
if (emptyStateView != null) {
|
if (emptyStateView != null) {
|
||||||
|
|||||||
@@ -6,9 +6,11 @@ import android.view.View;
|
|||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
|
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.compose.ui.platform.ComposeView;
|
||||||
|
|
||||||
import org.schabi.newpipe.BaseFragment;
|
import org.schabi.newpipe.BaseFragment;
|
||||||
import org.schabi.newpipe.R;
|
import org.schabi.newpipe.R;
|
||||||
|
import org.schabi.newpipe.ui.emptystate.EmptyStateUtil;
|
||||||
|
|
||||||
public class EmptyFragment extends BaseFragment {
|
public class EmptyFragment extends BaseFragment {
|
||||||
private static final String SHOW_MESSAGE = "SHOW_MESSAGE";
|
private static final String SHOW_MESSAGE = "SHOW_MESSAGE";
|
||||||
@@ -26,8 +28,10 @@ public class EmptyFragment extends BaseFragment {
|
|||||||
final Bundle savedInstanceState) {
|
final Bundle savedInstanceState) {
|
||||||
final boolean showMessage = getArguments().getBoolean(SHOW_MESSAGE);
|
final boolean showMessage = getArguments().getBoolean(SHOW_MESSAGE);
|
||||||
final View view = inflater.inflate(R.layout.fragment_empty, container, false);
|
final View view = inflater.inflate(R.layout.fragment_empty, container, false);
|
||||||
view.findViewById(R.id.empty_state_view).setVisibility(
|
|
||||||
showMessage ? View.VISIBLE : View.GONE);
|
final ComposeView composeView = view.findViewById(R.id.empty_state_view);
|
||||||
|
EmptyStateUtil.setEmptyStateComposable(composeView);
|
||||||
|
composeView.setVisibility(showMessage ? View.VISIBLE : View.GONE);
|
||||||
return view;
|
return view;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -220,7 +220,7 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte
|
|||||||
public void commitPlaylistTabs() {
|
public void commitPlaylistTabs() {
|
||||||
pagerAdapter.getLocalPlaylistFragments()
|
pagerAdapter.getLocalPlaylistFragments()
|
||||||
.stream()
|
.stream()
|
||||||
.forEach(LocalPlaylistFragment::commitChanges);
|
.forEach(LocalPlaylistFragment::saveImmediate);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateTabLayoutPosition() {
|
private void updateTabLayoutPosition() {
|
||||||
@@ -245,10 +245,10 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte
|
|||||||
// change the background and icon color of the tab layout:
|
// change the background and icon color of the tab layout:
|
||||||
// service-colored at the top, app-background-colored at the bottom
|
// service-colored at the top, app-background-colored at the bottom
|
||||||
tabLayout.setBackgroundColor(ThemeHelper.resolveColorFromAttr(requireContext(),
|
tabLayout.setBackgroundColor(ThemeHelper.resolveColorFromAttr(requireContext(),
|
||||||
bottom ? R.attr.colorSecondary : R.attr.colorPrimary));
|
bottom ? android.R.attr.windowBackground : R.attr.colorPrimary));
|
||||||
|
|
||||||
@ColorInt final int iconColor = bottom
|
@ColorInt final int iconColor = bottom
|
||||||
? ThemeHelper.resolveColorFromAttr(requireContext(), R.attr.colorAccent)
|
? ThemeHelper.resolveColorFromAttr(requireContext(), android.R.attr.colorAccent)
|
||||||
: Color.WHITE;
|
: Color.WHITE;
|
||||||
tabLayout.setTabRippleColor(ColorStateList.valueOf(iconColor).withAlpha(32));
|
tabLayout.setTabRippleColor(ColorStateList.valueOf(iconColor).withAlpha(32));
|
||||||
tabLayout.setTabIconTint(ColorStateList.valueOf(iconColor));
|
tabLayout.setTabIconTint(ColorStateList.valueOf(iconColor));
|
||||||
@@ -282,7 +282,7 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte
|
|||||||
* Keep reference to LocalPlaylistFragments, because their data can be modified by the user
|
* Keep reference to LocalPlaylistFragments, because their data can be modified by the user
|
||||||
* during runtime and changes are not committed immediately. However, in some cases,
|
* during runtime and changes are not committed immediately. However, in some cases,
|
||||||
* the changes need to be committed immediately by calling
|
* the changes need to be committed immediately by calling
|
||||||
* {@link LocalPlaylistFragment#commitChanges()}.
|
* {@link LocalPlaylistFragment#saveImmediate()}.
|
||||||
* The fragments are removed when {@link LocalPlaylistFragment#onDestroy()} is called.
|
* The fragments are removed when {@link LocalPlaylistFragment#onDestroy()} is called.
|
||||||
*/
|
*/
|
||||||
private final List<LocalPlaylistFragment> localPlaylistFragments = new ArrayList<>();
|
private final List<LocalPlaylistFragment> localPlaylistFragments = new ArrayList<>();
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ public abstract class BaseDescriptionFragment extends BaseFragment {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the description to display.
|
* Get the description to display.
|
||||||
* @return description object
|
* @return description object, if available
|
||||||
*/
|
*/
|
||||||
@Nullable
|
@Nullable
|
||||||
protected abstract Description getDescription();
|
protected abstract Description getDescription();
|
||||||
@@ -73,7 +73,7 @@ public abstract class BaseDescriptionFragment extends BaseFragment {
|
|||||||
* Get the streaming service. Used for generating description links.
|
* Get the streaming service. Used for generating description links.
|
||||||
* @return streaming service
|
* @return streaming service
|
||||||
*/
|
*/
|
||||||
@Nullable
|
@NonNull
|
||||||
protected abstract StreamingService getService();
|
protected abstract StreamingService getService();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -93,7 +93,7 @@ public abstract class BaseDescriptionFragment extends BaseFragment {
|
|||||||
* Get the list of tags to display below the description.
|
* Get the list of tags to display below the description.
|
||||||
* @return tag list
|
* @return tag list
|
||||||
*/
|
*/
|
||||||
@Nullable
|
@NonNull
|
||||||
public abstract List<String> getTags();
|
public abstract List<String> getTags();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -158,7 +158,7 @@ public abstract class BaseDescriptionFragment extends BaseFragment {
|
|||||||
final LinearLayout layout,
|
final LinearLayout layout,
|
||||||
final boolean linkifyContent,
|
final boolean linkifyContent,
|
||||||
@StringRes final int type,
|
@StringRes final int type,
|
||||||
@Nullable final String content) {
|
@NonNull final String content) {
|
||||||
if (isBlank(content)) {
|
if (isBlank(content)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -221,16 +221,12 @@ public abstract class BaseDescriptionFragment extends BaseFragment {
|
|||||||
urls.append(imageSizeToText(image.getWidth()));
|
urls.append(imageSizeToText(image.getWidth()));
|
||||||
} else {
|
} else {
|
||||||
switch (image.getEstimatedResolutionLevel()) {
|
switch (image.getEstimatedResolutionLevel()) {
|
||||||
case LOW:
|
case LOW -> urls.append(getString(R.string.image_quality_low));
|
||||||
urls.append(getString(R.string.image_quality_low));
|
case MEDIUM -> urls.append(getString(R.string.image_quality_medium));
|
||||||
break;
|
case HIGH -> urls.append(getString(R.string.image_quality_high));
|
||||||
default: // unreachable, Image.ResolutionLevel.UNKNOWN is already filtered out
|
default -> {
|
||||||
case MEDIUM:
|
// unreachable, Image.ResolutionLevel.UNKNOWN is already filtered out
|
||||||
urls.append(getString(R.string.image_quality_medium));
|
}
|
||||||
break;
|
|
||||||
case HIGH:
|
|
||||||
urls.append(getString(R.string.image_quality_high));
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -255,7 +251,7 @@ public abstract class BaseDescriptionFragment extends BaseFragment {
|
|||||||
private void addTagsMetadataItem(final LayoutInflater inflater, final LinearLayout layout) {
|
private void addTagsMetadataItem(final LayoutInflater inflater, final LinearLayout layout) {
|
||||||
final List<String> tags = getTags();
|
final List<String> tags = getTags();
|
||||||
|
|
||||||
if (tags != null && !tags.isEmpty()) {
|
if (!tags.isEmpty()) {
|
||||||
final var itemBinding = ItemMetadataTagsBinding.inflate(inflater, layout, false);
|
final var itemBinding = ItemMetadataTagsBinding.inflate(inflater, layout, false);
|
||||||
|
|
||||||
tags.stream().sorted(String.CASE_INSENSITIVE_ORDER).forEach(tag -> {
|
tags.stream().sorted(String.CASE_INSENSITIVE_ORDER).forEach(tag -> {
|
||||||
|
|||||||
@@ -7,9 +7,12 @@ import android.view.LayoutInflater;
|
|||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.widget.LinearLayout;
|
import android.widget.LinearLayout;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.annotation.StringRes;
|
import androidx.annotation.StringRes;
|
||||||
|
|
||||||
|
import com.evernote.android.state.State;
|
||||||
|
|
||||||
import org.schabi.newpipe.R;
|
import org.schabi.newpipe.R;
|
||||||
import org.schabi.newpipe.extractor.StreamingService;
|
import org.schabi.newpipe.extractor.StreamingService;
|
||||||
import org.schabi.newpipe.extractor.stream.Description;
|
import org.schabi.newpipe.extractor.stream.Description;
|
||||||
@@ -18,61 +21,46 @@ import org.schabi.newpipe.util.Localization;
|
|||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import icepick.State;
|
|
||||||
|
|
||||||
public class DescriptionFragment extends BaseDescriptionFragment {
|
public class DescriptionFragment extends BaseDescriptionFragment {
|
||||||
|
|
||||||
@State
|
@State
|
||||||
StreamInfo streamInfo = null;
|
StreamInfo streamInfo;
|
||||||
|
|
||||||
public DescriptionFragment() {
|
|
||||||
}
|
|
||||||
|
|
||||||
public DescriptionFragment(final StreamInfo streamInfo) {
|
public DescriptionFragment(final StreamInfo streamInfo) {
|
||||||
this.streamInfo = streamInfo;
|
this.streamInfo = streamInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nullable
|
public DescriptionFragment() {
|
||||||
@Override
|
// keep empty constructor for State when resuming fragment from memory
|
||||||
protected Description getDescription() {
|
|
||||||
if (streamInfo == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return streamInfo.getDescription();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
@Override
|
@Override
|
||||||
|
protected Description getDescription() {
|
||||||
|
return streamInfo.getDescription();
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
protected StreamingService getService() {
|
protected StreamingService getService() {
|
||||||
if (streamInfo == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return streamInfo.getService();
|
return streamInfo.getService();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected int getServiceId() {
|
protected int getServiceId() {
|
||||||
if (streamInfo == null) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
return streamInfo.getServiceId();
|
return streamInfo.getServiceId();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nullable
|
@NonNull
|
||||||
@Override
|
@Override
|
||||||
protected String getStreamUrl() {
|
protected String getStreamUrl() {
|
||||||
if (streamInfo == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return streamInfo.getUrl();
|
return streamInfo.getUrl();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nullable
|
@NonNull
|
||||||
@Override
|
@Override
|
||||||
public List<String> getTags() {
|
public List<String> getTags() {
|
||||||
if (streamInfo == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return streamInfo.getTags();
|
return streamInfo.getTags();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ import androidx.core.content.ContextCompat;
|
|||||||
import androidx.fragment.app.Fragment;
|
import androidx.fragment.app.Fragment;
|
||||||
import androidx.preference.PreferenceManager;
|
import androidx.preference.PreferenceManager;
|
||||||
|
|
||||||
|
import com.evernote.android.state.State;
|
||||||
import com.google.android.exoplayer2.PlaybackException;
|
import com.google.android.exoplayer2.PlaybackException;
|
||||||
import com.google.android.exoplayer2.PlaybackParameters;
|
import com.google.android.exoplayer2.PlaybackParameters;
|
||||||
import com.google.android.material.appbar.AppBarLayout;
|
import com.google.android.material.appbar.AppBarLayout;
|
||||||
@@ -72,7 +73,6 @@ import org.schabi.newpipe.error.ErrorUtil;
|
|||||||
import org.schabi.newpipe.error.ReCaptchaActivity;
|
import org.schabi.newpipe.error.ReCaptchaActivity;
|
||||||
import org.schabi.newpipe.error.UserAction;
|
import org.schabi.newpipe.error.UserAction;
|
||||||
import org.schabi.newpipe.extractor.Image;
|
import org.schabi.newpipe.extractor.Image;
|
||||||
import org.schabi.newpipe.extractor.InfoItem;
|
|
||||||
import org.schabi.newpipe.extractor.NewPipe;
|
import org.schabi.newpipe.extractor.NewPipe;
|
||||||
import org.schabi.newpipe.extractor.exceptions.ContentNotSupportedException;
|
import org.schabi.newpipe.extractor.exceptions.ContentNotSupportedException;
|
||||||
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||||
@@ -106,16 +106,17 @@ import org.schabi.newpipe.player.ui.VideoPlayerUi;
|
|||||||
import org.schabi.newpipe.util.Constants;
|
import org.schabi.newpipe.util.Constants;
|
||||||
import org.schabi.newpipe.util.DeviceUtils;
|
import org.schabi.newpipe.util.DeviceUtils;
|
||||||
import org.schabi.newpipe.util.ExtractorHelper;
|
import org.schabi.newpipe.util.ExtractorHelper;
|
||||||
|
import org.schabi.newpipe.util.InfoCache;
|
||||||
import org.schabi.newpipe.util.ListHelper;
|
import org.schabi.newpipe.util.ListHelper;
|
||||||
import org.schabi.newpipe.util.Localization;
|
import org.schabi.newpipe.util.Localization;
|
||||||
import org.schabi.newpipe.util.NavigationHelper;
|
import org.schabi.newpipe.util.NavigationHelper;
|
||||||
import org.schabi.newpipe.util.PermissionHelper;
|
import org.schabi.newpipe.util.PermissionHelper;
|
||||||
import org.schabi.newpipe.util.image.PicassoHelper;
|
import org.schabi.newpipe.util.PlayButtonHelper;
|
||||||
import org.schabi.newpipe.util.StreamTypeUtil;
|
import org.schabi.newpipe.util.StreamTypeUtil;
|
||||||
import org.schabi.newpipe.util.ThemeHelper;
|
import org.schabi.newpipe.util.ThemeHelper;
|
||||||
import org.schabi.newpipe.util.external_communication.KoreUtils;
|
import org.schabi.newpipe.util.external_communication.KoreUtils;
|
||||||
import org.schabi.newpipe.util.external_communication.ShareUtils;
|
import org.schabi.newpipe.util.external_communication.ShareUtils;
|
||||||
import org.schabi.newpipe.util.PlayButtonHelper;
|
import org.schabi.newpipe.util.image.CoilHelper;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Iterator;
|
import java.util.Iterator;
|
||||||
@@ -126,7 +127,7 @@ import java.util.Optional;
|
|||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
import icepick.State;
|
import coil3.util.CoilUtils;
|
||||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||||
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
||||||
import io.reactivex.rxjava3.disposables.Disposable;
|
import io.reactivex.rxjava3.disposables.Disposable;
|
||||||
@@ -158,8 +159,6 @@ public final class VideoDetailFragment
|
|||||||
private static final String DESCRIPTION_TAB_TAG = "DESCRIPTION TAB";
|
private static final String DESCRIPTION_TAB_TAG = "DESCRIPTION TAB";
|
||||||
private static final String EMPTY_TAB_TAG = "EMPTY TAB";
|
private static final String EMPTY_TAB_TAG = "EMPTY TAB";
|
||||||
|
|
||||||
private static final String PICASSO_VIDEO_DETAILS_TAG = "PICASSO_VIDEO_DETAILS_TAG";
|
|
||||||
|
|
||||||
// tabs
|
// tabs
|
||||||
private boolean showComments;
|
private boolean showComments;
|
||||||
private boolean showRelatedItems;
|
private boolean showRelatedItems;
|
||||||
@@ -189,21 +188,21 @@ public final class VideoDetailFragment
|
|||||||
};
|
};
|
||||||
|
|
||||||
@State
|
@State
|
||||||
protected int serviceId = Constants.NO_SERVICE_ID;
|
int serviceId = Constants.NO_SERVICE_ID;
|
||||||
@State
|
@State
|
||||||
@NonNull
|
@NonNull
|
||||||
protected String title = "";
|
String title = "";
|
||||||
@State
|
@State
|
||||||
@Nullable
|
@Nullable
|
||||||
protected String url = null;
|
String url = null;
|
||||||
@Nullable
|
@Nullable
|
||||||
protected PlayQueue playQueue = null;
|
private PlayQueue playQueue = null;
|
||||||
@State
|
@State
|
||||||
int bottomSheetState = BottomSheetBehavior.STATE_EXPANDED;
|
int bottomSheetState = BottomSheetBehavior.STATE_EXPANDED;
|
||||||
@State
|
@State
|
||||||
int lastStableBottomSheetState = BottomSheetBehavior.STATE_EXPANDED;
|
int lastStableBottomSheetState = BottomSheetBehavior.STATE_EXPANDED;
|
||||||
@State
|
@State
|
||||||
protected boolean autoPlayEnabled = true;
|
boolean autoPlayEnabled = true;
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
private StreamInfo currentInfo = null;
|
private StreamInfo currentInfo = null;
|
||||||
@@ -235,16 +234,19 @@ public final class VideoDetailFragment
|
|||||||
// Service management
|
// Service management
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
@Override
|
@Override
|
||||||
public void onServiceConnected(final Player connectedPlayer,
|
public void onServiceConnected(@NonNull final PlayerService connectedPlayerService) {
|
||||||
final PlayerService connectedPlayerService,
|
|
||||||
final boolean playAfterConnect) {
|
|
||||||
player = connectedPlayer;
|
|
||||||
playerService = connectedPlayerService;
|
playerService = connectedPlayerService;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onPlayerConnected(@NonNull final Player connectedPlayer,
|
||||||
|
final boolean playAfterConnect) {
|
||||||
|
player = connectedPlayer;
|
||||||
|
|
||||||
// It will do nothing if the player is not in fullscreen mode
|
// It will do nothing if the player is not in fullscreen mode
|
||||||
hideSystemUiIfNeeded();
|
hideSystemUiIfNeeded();
|
||||||
|
|
||||||
final Optional<MainPlayerUi> playerUi = player.UIs().get(MainPlayerUi.class);
|
final Optional<MainPlayerUi> playerUi = player.UIs().getOpt(MainPlayerUi.class);
|
||||||
if (!player.videoPlayerSelected() && !playAfterConnect) {
|
if (!player.videoPlayerSelected() && !playAfterConnect) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -271,22 +273,29 @@ public final class VideoDetailFragment
|
|||||||
updateOverlayPlayQueueButtonVisibility();
|
updateOverlayPlayQueueButtonVisibility();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onPlayerDisconnected() {
|
||||||
|
player = null;
|
||||||
|
// the binding could be null at this point, if the app is finishing
|
||||||
|
if (binding != null) {
|
||||||
|
restoreDefaultBrightness();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onServiceDisconnected() {
|
public void onServiceDisconnected() {
|
||||||
playerService = null;
|
playerService = null;
|
||||||
player = null;
|
|
||||||
restoreDefaultBrightness();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/*////////////////////////////////////////////////////////////////////////*/
|
/*////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
public static VideoDetailFragment getInstance(final int serviceId,
|
public static VideoDetailFragment getInstance(final int serviceId,
|
||||||
@Nullable final String videoUrl,
|
@Nullable final String url,
|
||||||
@NonNull final String name,
|
@NonNull final String name,
|
||||||
@Nullable final PlayQueue queue) {
|
@Nullable final PlayQueue queue) {
|
||||||
final VideoDetailFragment instance = new VideoDetailFragment();
|
final VideoDetailFragment instance = new VideoDetailFragment();
|
||||||
instance.setInitialData(serviceId, videoUrl, name, queue);
|
instance.setInitialData(serviceId, url, name, queue);
|
||||||
return instance;
|
return instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -429,18 +438,15 @@ public final class VideoDetailFragment
|
|||||||
@Override
|
@Override
|
||||||
public void onActivityResult(final int requestCode, final int resultCode, final Intent data) {
|
public void onActivityResult(final int requestCode, final int resultCode, final Intent data) {
|
||||||
super.onActivityResult(requestCode, resultCode, data);
|
super.onActivityResult(requestCode, resultCode, data);
|
||||||
switch (requestCode) {
|
if (requestCode == ReCaptchaActivity.RECAPTCHA_REQUEST) {
|
||||||
case ReCaptchaActivity.RECAPTCHA_REQUEST:
|
if (resultCode == Activity.RESULT_OK) {
|
||||||
if (resultCode == Activity.RESULT_OK) {
|
NavigationHelper.openVideoDetailFragment(requireContext(), getFM(),
|
||||||
NavigationHelper.openVideoDetailFragment(requireContext(), getFM(),
|
serviceId, url, title, null, false);
|
||||||
serviceId, url, title, null, false);
|
} else {
|
||||||
} else {
|
Log.e(TAG, "ReCaptcha failed");
|
||||||
Log.e(TAG, "ReCaptcha failed");
|
}
|
||||||
}
|
} else {
|
||||||
break;
|
Log.e(TAG, "Request code from activity not supported [" + requestCode + "]");
|
||||||
default:
|
|
||||||
Log.e(TAG, "Request code from activity not supported [" + requestCode + "]");
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -481,7 +487,7 @@ public final class VideoDetailFragment
|
|||||||
|
|
||||||
// commit previous pending changes to database
|
// commit previous pending changes to database
|
||||||
if (fragment instanceof LocalPlaylistFragment) {
|
if (fragment instanceof LocalPlaylistFragment) {
|
||||||
((LocalPlaylistFragment) fragment).commitChanges();
|
((LocalPlaylistFragment) fragment).saveImmediate();
|
||||||
} else if (fragment instanceof MainFragment) {
|
} else if (fragment instanceof MainFragment) {
|
||||||
((MainFragment) fragment).commitPlaylistTabs();
|
((MainFragment) fragment).commitPlaylistTabs();
|
||||||
}
|
}
|
||||||
@@ -520,7 +526,7 @@ public final class VideoDetailFragment
|
|||||||
binding.overlayPlayPauseButton.setOnClickListener(v -> {
|
binding.overlayPlayPauseButton.setOnClickListener(v -> {
|
||||||
if (playerIsNotStopped()) {
|
if (playerIsNotStopped()) {
|
||||||
player.playPause();
|
player.playPause();
|
||||||
player.UIs().get(VideoPlayerUi.class).ifPresent(ui -> ui.hideControls(0, 0));
|
player.UIs().getOpt(VideoPlayerUi.class).ifPresent(ui -> ui.hideControls(0, 0));
|
||||||
showSystemUi();
|
showSystemUi();
|
||||||
} else {
|
} else {
|
||||||
autoPlayEnabled = true; // forcefully start playing
|
autoPlayEnabled = true; // forcefully start playing
|
||||||
@@ -679,7 +685,7 @@ public final class VideoDetailFragment
|
|||||||
@Override
|
@Override
|
||||||
public boolean onKeyDown(final int keyCode) {
|
public boolean onKeyDown(final int keyCode) {
|
||||||
return isPlayerAvailable()
|
return isPlayerAvailable()
|
||||||
&& player.UIs().get(VideoPlayerUi.class)
|
&& player.UIs().getOpt(VideoPlayerUi.class)
|
||||||
.map(playerUi -> playerUi.onKeyDown(keyCode)).orElse(false);
|
.map(playerUi -> playerUi.onKeyDown(keyCode)).orElse(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -806,25 +812,17 @@ public final class VideoDetailFragment
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void prepareAndLoadInfo() {
|
private void prepareAndLoadInfo() {
|
||||||
scrollToTop();
|
scrollToTop();
|
||||||
startLoading(false);
|
startLoading(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void startLoading(final boolean forceLoad) {
|
public void startLoading(final boolean forceLoad) {
|
||||||
super.startLoading(forceLoad);
|
startLoading(forceLoad, null);
|
||||||
|
|
||||||
initTabs();
|
|
||||||
currentInfo = null;
|
|
||||||
if (currentWorker != null) {
|
|
||||||
currentWorker.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
runWorker(forceLoad, stack.isEmpty());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void startLoading(final boolean forceLoad, final boolean addToBackStack) {
|
private void startLoading(final boolean forceLoad, final @Nullable Boolean addToBackStack) {
|
||||||
super.startLoading(forceLoad);
|
super.startLoading(forceLoad);
|
||||||
|
|
||||||
initTabs();
|
initTabs();
|
||||||
@@ -833,7 +831,7 @@ public final class VideoDetailFragment
|
|||||||
currentWorker.dispose();
|
currentWorker.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
runWorker(forceLoad, addToBackStack);
|
runWorker(forceLoad, addToBackStack != null ? addToBackStack : stack.isEmpty());
|
||||||
}
|
}
|
||||||
|
|
||||||
private void runWorker(final boolean forceLoad, final boolean addToBackStack) {
|
private void runWorker(final boolean forceLoad, final boolean addToBackStack) {
|
||||||
@@ -881,8 +879,7 @@ public final class VideoDetailFragment
|
|||||||
tabContentDescriptions.clear();
|
tabContentDescriptions.clear();
|
||||||
|
|
||||||
if (shouldShowComments()) {
|
if (shouldShowComments()) {
|
||||||
pageAdapter.addFragment(
|
pageAdapter.addFragment(CommentsFragment.getInstance(serviceId, url), COMMENTS_TAB_TAG);
|
||||||
CommentsFragment.getInstance(serviceId, url, title), COMMENTS_TAB_TAG);
|
|
||||||
tabIcons.add(R.drawable.ic_comment);
|
tabIcons.add(R.drawable.ic_comment);
|
||||||
tabContentDescriptions.add(R.string.comments_tab_description);
|
tabContentDescriptions.add(R.string.comments_tab_description);
|
||||||
}
|
}
|
||||||
@@ -1020,7 +1017,7 @@ public final class VideoDetailFragment
|
|||||||
// If a user watched video inside fullscreen mode and than chose another player
|
// If a user watched video inside fullscreen mode and than chose another player
|
||||||
// return to non-fullscreen mode
|
// return to non-fullscreen mode
|
||||||
if (isPlayerAvailable()) {
|
if (isPlayerAvailable()) {
|
||||||
player.UIs().get(MainPlayerUi.class).ifPresent(playerUi -> {
|
player.UIs().getOpt(MainPlayerUi.class).ifPresent(playerUi -> {
|
||||||
if (playerUi.isFullscreen()) {
|
if (playerUi.isFullscreen()) {
|
||||||
playerUi.toggleFullscreen();
|
playerUi.toggleFullscreen();
|
||||||
}
|
}
|
||||||
@@ -1130,7 +1127,7 @@ public final class VideoDetailFragment
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void openMainPlayer() {
|
private void openMainPlayer() {
|
||||||
if (!isPlayerServiceAvailable()) {
|
if (noPlayerServiceAvailable()) {
|
||||||
playerHolder.startService(autoPlayEnabled, this);
|
playerHolder.startService(autoPlayEnabled, this);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1155,7 +1152,7 @@ public final class VideoDetailFragment
|
|||||||
*/
|
*/
|
||||||
private void hideMainPlayerOnLoadingNewStream() {
|
private void hideMainPlayerOnLoadingNewStream() {
|
||||||
final var root = getRoot();
|
final var root = getRoot();
|
||||||
if (!isPlayerServiceAvailable() || root.isEmpty() || !player.videoPlayerSelected()) {
|
if (noPlayerServiceAvailable() || root.isEmpty() || !player.videoPlayerSelected()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1236,7 +1233,7 @@ public final class VideoDetailFragment
|
|||||||
// setup the surface view height, so that it fits the video correctly
|
// setup the surface view height, so that it fits the video correctly
|
||||||
setHeightThumbnail();
|
setHeightThumbnail();
|
||||||
|
|
||||||
player.UIs().get(MainPlayerUi.class).ifPresent(playerUi -> {
|
player.UIs().getOpt(MainPlayerUi.class).ifPresent(playerUi -> {
|
||||||
// sometimes binding would be null here, even though getView() != null above u.u
|
// sometimes binding would be null here, even though getView() != null above u.u
|
||||||
if (binding != null) {
|
if (binding != null) {
|
||||||
// prevent from re-adding a view multiple times
|
// prevent from re-adding a view multiple times
|
||||||
@@ -1252,7 +1249,7 @@ public final class VideoDetailFragment
|
|||||||
makeDefaultHeightForVideoPlaceholder();
|
makeDefaultHeightForVideoPlaceholder();
|
||||||
|
|
||||||
if (player != null) {
|
if (player != null) {
|
||||||
player.UIs().get(VideoPlayerUi.class).ifPresent(VideoPlayerUi::removeViewFromParent);
|
player.UIs().getOpt(VideoPlayerUi.class).ifPresent(VideoPlayerUi::removeViewFromParent);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1319,7 +1316,7 @@ public final class VideoDetailFragment
|
|||||||
binding.detailThumbnailImageView.setMinimumHeight(newHeight);
|
binding.detailThumbnailImageView.setMinimumHeight(newHeight);
|
||||||
if (isPlayerAvailable()) {
|
if (isPlayerAvailable()) {
|
||||||
final int maxHeight = (int) (metrics.heightPixels * MAX_PLAYER_HEIGHT);
|
final int maxHeight = (int) (metrics.heightPixels * MAX_PLAYER_HEIGHT);
|
||||||
player.UIs().get(VideoPlayerUi.class).ifPresent(ui ->
|
player.UIs().getOpt(VideoPlayerUi.class).ifPresent(ui ->
|
||||||
ui.getBinding().surfaceView.setHeights(newHeight,
|
ui.getBinding().surfaceView.setHeights(newHeight,
|
||||||
ui.isFullscreen() ? newHeight : maxHeight));
|
ui.isFullscreen() ? newHeight : maxHeight));
|
||||||
}
|
}
|
||||||
@@ -1329,23 +1326,23 @@ public final class VideoDetailFragment
|
|||||||
binding.detailContentRootHiding.setVisibility(View.VISIBLE);
|
binding.detailContentRootHiding.setVisibility(View.VISIBLE);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void setInitialData(final int newServiceId,
|
private void setInitialData(final int newServiceId,
|
||||||
@Nullable final String newUrl,
|
@Nullable final String newUrl,
|
||||||
@NonNull final String newTitle,
|
@NonNull final String newTitle,
|
||||||
@Nullable final PlayQueue newPlayQueue) {
|
@Nullable final PlayQueue newPlayQueue) {
|
||||||
this.serviceId = newServiceId;
|
this.serviceId = newServiceId;
|
||||||
this.url = newUrl;
|
this.url = newUrl;
|
||||||
this.title = newTitle;
|
this.title = newTitle;
|
||||||
this.playQueue = newPlayQueue;
|
this.playQueue = newPlayQueue;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setErrorImage(final int imageResource) {
|
private void setErrorImage() {
|
||||||
if (binding == null || activity == null) {
|
if (binding == null || activity == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.detailThumbnailImageView.setImageDrawable(
|
binding.detailThumbnailImageView.setImageDrawable(
|
||||||
AppCompatResources.getDrawable(requireContext(), imageResource));
|
AppCompatResources.getDrawable(requireContext(), R.drawable.not_available_monkey));
|
||||||
animate(binding.detailThumbnailImageView, false, 0, AnimationType.ALPHA,
|
animate(binding.detailThumbnailImageView, false, 0, AnimationType.ALPHA,
|
||||||
0, () -> animate(binding.detailThumbnailImageView, true, 500));
|
0, () -> animate(binding.detailThumbnailImageView, true, 500));
|
||||||
}
|
}
|
||||||
@@ -1353,7 +1350,7 @@ public final class VideoDetailFragment
|
|||||||
@Override
|
@Override
|
||||||
public void handleError() {
|
public void handleError() {
|
||||||
super.handleError();
|
super.handleError();
|
||||||
setErrorImage(R.drawable.not_available_monkey);
|
setErrorImage();
|
||||||
|
|
||||||
if (binding.relatedItemsLayout != null) { // hide related streams for tablets
|
if (binding.relatedItemsLayout != null) { // hide related streams for tablets
|
||||||
binding.relatedItemsLayout.setVisibility(View.INVISIBLE);
|
binding.relatedItemsLayout.setVisibility(View.INVISIBLE);
|
||||||
@@ -1430,7 +1427,7 @@ public final class VideoDetailFragment
|
|||||||
super.showLoading();
|
super.showLoading();
|
||||||
|
|
||||||
//if data is already cached, transition from VISIBLE -> INVISIBLE -> VISIBLE is not required
|
//if data is already cached, transition from VISIBLE -> INVISIBLE -> VISIBLE is not required
|
||||||
if (!ExtractorHelper.isCached(serviceId, url, InfoItem.InfoType.STREAM)) {
|
if (!ExtractorHelper.isCached(serviceId, url, InfoCache.Type.STREAM)) {
|
||||||
binding.detailContentRootHiding.setVisibility(View.INVISIBLE);
|
binding.detailContentRootHiding.setVisibility(View.INVISIBLE);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1456,7 +1453,11 @@ public final class VideoDetailFragment
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
PicassoHelper.cancelTag(PICASSO_VIDEO_DETAILS_TAG);
|
CoilUtils.dispose(binding.detailThumbnailImageView);
|
||||||
|
CoilUtils.dispose(binding.detailSubChannelThumbnailView);
|
||||||
|
CoilUtils.dispose(binding.overlayThumbnail);
|
||||||
|
CoilUtils.dispose(binding.detailUploaderThumbnailView);
|
||||||
|
|
||||||
binding.detailThumbnailImageView.setImageBitmap(null);
|
binding.detailThumbnailImageView.setImageBitmap(null);
|
||||||
binding.detailSubChannelThumbnailView.setImageBitmap(null);
|
binding.detailSubChannelThumbnailView.setImageBitmap(null);
|
||||||
}
|
}
|
||||||
@@ -1547,8 +1548,8 @@ public final class VideoDetailFragment
|
|||||||
binding.detailSecondaryControlPanel.setVisibility(View.GONE);
|
binding.detailSecondaryControlPanel.setVisibility(View.GONE);
|
||||||
|
|
||||||
checkUpdateProgressInfo(info);
|
checkUpdateProgressInfo(info);
|
||||||
PicassoHelper.loadDetailsThumbnail(info.getThumbnails()).tag(PICASSO_VIDEO_DETAILS_TAG)
|
CoilHelper.INSTANCE.loadDetailsThumbnail(binding.detailThumbnailImageView,
|
||||||
.into(binding.detailThumbnailImageView);
|
info.getThumbnails());
|
||||||
showMetaInfoInTextView(info.getMetaInfo(), binding.detailMetaInfoTextView,
|
showMetaInfoInTextView(info.getMetaInfo(), binding.detailMetaInfoTextView,
|
||||||
binding.detailMetaInfoSeparator, disposables);
|
binding.detailMetaInfoSeparator, disposables);
|
||||||
|
|
||||||
@@ -1598,8 +1599,8 @@ public final class VideoDetailFragment
|
|||||||
binding.detailUploaderTextView.setVisibility(View.GONE);
|
binding.detailUploaderTextView.setVisibility(View.GONE);
|
||||||
}
|
}
|
||||||
|
|
||||||
PicassoHelper.loadAvatar(info.getUploaderAvatars()).tag(PICASSO_VIDEO_DETAILS_TAG)
|
CoilHelper.INSTANCE.loadAvatar(binding.detailSubChannelThumbnailView,
|
||||||
.into(binding.detailSubChannelThumbnailView);
|
info.getUploaderAvatars());
|
||||||
binding.detailSubChannelThumbnailView.setVisibility(View.VISIBLE);
|
binding.detailSubChannelThumbnailView.setVisibility(View.VISIBLE);
|
||||||
binding.detailUploaderThumbnailView.setVisibility(View.GONE);
|
binding.detailUploaderThumbnailView.setVisibility(View.GONE);
|
||||||
}
|
}
|
||||||
@@ -1630,11 +1631,11 @@ public final class VideoDetailFragment
|
|||||||
binding.detailUploaderTextView.setVisibility(View.GONE);
|
binding.detailUploaderTextView.setVisibility(View.GONE);
|
||||||
}
|
}
|
||||||
|
|
||||||
PicassoHelper.loadAvatar(info.getSubChannelAvatars()).tag(PICASSO_VIDEO_DETAILS_TAG)
|
CoilHelper.INSTANCE.loadAvatar(binding.detailSubChannelThumbnailView,
|
||||||
.into(binding.detailSubChannelThumbnailView);
|
info.getSubChannelAvatars());
|
||||||
binding.detailSubChannelThumbnailView.setVisibility(View.VISIBLE);
|
binding.detailSubChannelThumbnailView.setVisibility(View.VISIBLE);
|
||||||
PicassoHelper.loadAvatar(info.getUploaderAvatars()).tag(PICASSO_VIDEO_DETAILS_TAG)
|
CoilHelper.INSTANCE.loadAvatar(binding.detailUploaderThumbnailView,
|
||||||
.into(binding.detailUploaderThumbnailView);
|
info.getUploaderAvatars());
|
||||||
binding.detailUploaderThumbnailView.setVisibility(View.VISIBLE);
|
binding.detailUploaderThumbnailView.setVisibility(View.VISIBLE);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1721,7 +1722,7 @@ public final class VideoDetailFragment
|
|||||||
playQueue = queue;
|
playQueue = queue;
|
||||||
if (DEBUG) {
|
if (DEBUG) {
|
||||||
Log.d(TAG, "onQueueUpdate() called with: serviceId = ["
|
Log.d(TAG, "onQueueUpdate() called with: serviceId = ["
|
||||||
+ serviceId + "], videoUrl = [" + url + "], name = ["
|
+ serviceId + "], url = [" + url + "], name = ["
|
||||||
+ title + "], playQueue = [" + playQueue + "]");
|
+ title + "], playQueue = [" + playQueue + "]");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1764,16 +1765,14 @@ public final class VideoDetailFragment
|
|||||||
final PlaybackParameters parameters) {
|
final PlaybackParameters parameters) {
|
||||||
setOverlayPlayPauseImage(player != null && player.isPlaying());
|
setOverlayPlayPauseImage(player != null && player.isPlaying());
|
||||||
|
|
||||||
switch (state) {
|
if (state == Player.STATE_PLAYING) {
|
||||||
case Player.STATE_PLAYING:
|
if (binding.positionView.getAlpha() != 1.0f
|
||||||
if (binding.positionView.getAlpha() != 1.0f
|
&& player.getPlayQueue() != null
|
||||||
&& player.getPlayQueue() != null
|
&& player.getPlayQueue().getItem() != null
|
||||||
&& player.getPlayQueue().getItem() != null
|
&& player.getPlayQueue().getItem().getUrl().equals(url)) {
|
||||||
&& player.getPlayQueue().getItem().getUrl().equals(url)) {
|
animate(binding.positionView, true, 100);
|
||||||
animate(binding.positionView, true, 100);
|
animate(binding.detailPositionView, true, 100);
|
||||||
animate(binding.detailPositionView, true, 100);
|
}
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1833,20 +1832,23 @@ public final class VideoDetailFragment
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onServiceStopped() {
|
public void onServiceStopped() {
|
||||||
setOverlayPlayPauseImage(false);
|
// the binding could be null at this point, if the app is finishing
|
||||||
if (currentInfo != null) {
|
if (binding != null) {
|
||||||
updateOverlayData(currentInfo.getName(),
|
setOverlayPlayPauseImage(false);
|
||||||
currentInfo.getUploaderName(),
|
if (currentInfo != null) {
|
||||||
currentInfo.getThumbnails());
|
updateOverlayData(currentInfo.getName(),
|
||||||
|
currentInfo.getUploaderName(),
|
||||||
|
currentInfo.getThumbnails());
|
||||||
|
}
|
||||||
|
updateOverlayPlayQueueButtonVisibility();
|
||||||
}
|
}
|
||||||
updateOverlayPlayQueueButtonVisibility();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onFullscreenStateChanged(final boolean fullscreen) {
|
public void onFullscreenStateChanged(final boolean fullscreen) {
|
||||||
setupBrightness();
|
setupBrightness();
|
||||||
if (!isPlayerAndPlayerServiceAvailable()
|
if (!isPlayerAndPlayerServiceAvailable()
|
||||||
|| player.UIs().get(MainPlayerUi.class).isEmpty()
|
|| player.UIs().getOpt(MainPlayerUi.class).isEmpty()
|
||||||
|| getRoot().map(View::getParent).isEmpty()) {
|
|| getRoot().map(View::getParent).isEmpty()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1875,7 +1877,7 @@ public final class VideoDetailFragment
|
|||||||
final boolean isLandscape = DeviceUtils.isLandscape(requireContext());
|
final boolean isLandscape = DeviceUtils.isLandscape(requireContext());
|
||||||
if (DeviceUtils.isTablet(activity)
|
if (DeviceUtils.isTablet(activity)
|
||||||
&& (!globalScreenOrientationLocked(activity) || isLandscape)) {
|
&& (!globalScreenOrientationLocked(activity) || isLandscape)) {
|
||||||
player.UIs().get(MainPlayerUi.class).ifPresent(MainPlayerUi::toggleFullscreen);
|
player.UIs().getOpt(MainPlayerUi.class).ifPresent(MainPlayerUi::toggleFullscreen);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1975,7 +1977,7 @@ public final class VideoDetailFragment
|
|||||||
}
|
}
|
||||||
|
|
||||||
private boolean isFullscreen() {
|
private boolean isFullscreen() {
|
||||||
return isPlayerAvailable() && player.UIs().get(VideoPlayerUi.class)
|
return isPlayerAvailable() && player.UIs().getOpt(VideoPlayerUi.class)
|
||||||
.map(VideoPlayerUi::isFullscreen).orElse(false);
|
.map(VideoPlayerUi::isFullscreen).orElse(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2052,7 +2054,7 @@ public final class VideoDetailFragment
|
|||||||
setAutoPlay(true);
|
setAutoPlay(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
player.UIs().get(MainPlayerUi.class).ifPresent(MainPlayerUi::checkLandscape);
|
player.UIs().getOpt(MainPlayerUi.class).ifPresent(MainPlayerUi::checkLandscape);
|
||||||
// Let's give a user time to look at video information page if video is not playing
|
// Let's give a user time to look at video information page if video is not playing
|
||||||
if (globalScreenOrientationLocked(activity) && !player.isPlaying()) {
|
if (globalScreenOrientationLocked(activity) && !player.isPlaying()) {
|
||||||
player.play();
|
player.play();
|
||||||
@@ -2317,7 +2319,7 @@ public final class VideoDetailFragment
|
|||||||
&& player.isPlaying()
|
&& player.isPlaying()
|
||||||
&& !isFullscreen()
|
&& !isFullscreen()
|
||||||
&& !DeviceUtils.isTablet(activity)) {
|
&& !DeviceUtils.isTablet(activity)) {
|
||||||
player.UIs().get(MainPlayerUi.class)
|
player.UIs().getOpt(MainPlayerUi.class)
|
||||||
.ifPresent(MainPlayerUi::toggleFullscreen);
|
.ifPresent(MainPlayerUi::toggleFullscreen);
|
||||||
}
|
}
|
||||||
setOverlayLook(binding.appBarLayout, behavior, 1);
|
setOverlayLook(binding.appBarLayout, behavior, 1);
|
||||||
@@ -2331,7 +2333,7 @@ public final class VideoDetailFragment
|
|||||||
// Re-enable clicks
|
// Re-enable clicks
|
||||||
setOverlayElementsClickable(true);
|
setOverlayElementsClickable(true);
|
||||||
if (isPlayerAvailable()) {
|
if (isPlayerAvailable()) {
|
||||||
player.UIs().get(MainPlayerUi.class)
|
player.UIs().getOpt(MainPlayerUi.class)
|
||||||
.ifPresent(MainPlayerUi::closeItemsList);
|
.ifPresent(MainPlayerUi::closeItemsList);
|
||||||
}
|
}
|
||||||
setOverlayLook(binding.appBarLayout, behavior, 0);
|
setOverlayLook(binding.appBarLayout, behavior, 0);
|
||||||
@@ -2342,7 +2344,7 @@ public final class VideoDetailFragment
|
|||||||
showSystemUi();
|
showSystemUi();
|
||||||
}
|
}
|
||||||
if (isPlayerAvailable()) {
|
if (isPlayerAvailable()) {
|
||||||
player.UIs().get(MainPlayerUi.class).ifPresent(ui -> {
|
player.UIs().getOpt(MainPlayerUi.class).ifPresent(ui -> {
|
||||||
if (ui.isControlsVisible()) {
|
if (ui.isControlsVisible()) {
|
||||||
ui.hideControls(0, 0);
|
ui.hideControls(0, 0);
|
||||||
}
|
}
|
||||||
@@ -2388,8 +2390,7 @@ public final class VideoDetailFragment
|
|||||||
binding.overlayTitleTextView.setText(isEmpty(overlayTitle) ? "" : overlayTitle);
|
binding.overlayTitleTextView.setText(isEmpty(overlayTitle) ? "" : overlayTitle);
|
||||||
binding.overlayChannelTextView.setText(isEmpty(uploader) ? "" : uploader);
|
binding.overlayChannelTextView.setText(isEmpty(uploader) ? "" : uploader);
|
||||||
binding.overlayThumbnail.setImageDrawable(null);
|
binding.overlayThumbnail.setImageDrawable(null);
|
||||||
PicassoHelper.loadDetailsThumbnail(thumbnails).tag(PICASSO_VIDEO_DETAILS_TAG)
|
CoilHelper.INSTANCE.loadDetailsThumbnail(binding.overlayThumbnail, thumbnails);
|
||||||
.into(binding.overlayThumbnail);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setOverlayPlayPauseImage(final boolean playerIsPlaying) {
|
private void setOverlayPlayPauseImage(final boolean playerIsPlaying) {
|
||||||
@@ -2430,8 +2431,8 @@ public final class VideoDetailFragment
|
|||||||
return player != null;
|
return player != null;
|
||||||
}
|
}
|
||||||
|
|
||||||
boolean isPlayerServiceAvailable() {
|
boolean noPlayerServiceAvailable() {
|
||||||
return playerService != null;
|
return playerService == null;
|
||||||
}
|
}
|
||||||
|
|
||||||
boolean isPlayerAndPlayerServiceAvailable() {
|
boolean isPlayerAndPlayerServiceAvailable() {
|
||||||
@@ -2440,7 +2441,7 @@ public final class VideoDetailFragment
|
|||||||
|
|
||||||
public Optional<View> getRoot() {
|
public Optional<View> getRoot() {
|
||||||
return Optional.ofNullable(player)
|
return Optional.ofNullable(player)
|
||||||
.flatMap(player1 -> player1.UIs().get(VideoPlayerUi.class))
|
.flatMap(player1 -> player1.UIs().getOpt(VideoPlayerUi.class))
|
||||||
.map(playerUi -> playerUi.getBinding().getRoot());
|
.map(playerUi -> playerUi.getBinding().getRoot());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ import android.view.View;
|
|||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
import com.evernote.android.state.State;
|
||||||
|
|
||||||
import org.schabi.newpipe.R;
|
import org.schabi.newpipe.R;
|
||||||
import org.schabi.newpipe.error.ErrorInfo;
|
import org.schabi.newpipe.error.ErrorInfo;
|
||||||
import org.schabi.newpipe.error.UserAction;
|
import org.schabi.newpipe.error.UserAction;
|
||||||
@@ -24,7 +26,6 @@ import java.util.ArrayList;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Queue;
|
import java.util.Queue;
|
||||||
|
|
||||||
import icepick.State;
|
|
||||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||||
import io.reactivex.rxjava3.core.Single;
|
import io.reactivex.rxjava3.core.Single;
|
||||||
import io.reactivex.rxjava3.disposables.Disposable;
|
import io.reactivex.rxjava3.disposables.Disposable;
|
||||||
@@ -143,7 +144,7 @@ public abstract class BaseListInfoFragment<I extends InfoItem, L extends ListInf
|
|||||||
currentWorker = loadResult(forceLoad)
|
currentWorker = loadResult(forceLoad)
|
||||||
.subscribeOn(Schedulers.io())
|
.subscribeOn(Schedulers.io())
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.subscribe((@NonNull L result) -> {
|
.subscribe((@NonNull final L result) -> {
|
||||||
isLoading.set(false);
|
isLoading.set(false);
|
||||||
currentInfo = result;
|
currentInfo = result;
|
||||||
currentNextPage = result.getNextPage();
|
currentNextPage = result.getNextPage();
|
||||||
@@ -231,6 +232,8 @@ public abstract class BaseListInfoFragment<I extends InfoItem, L extends ListInf
|
|||||||
if (!result.getRelatedItems().isEmpty()) {
|
if (!result.getRelatedItems().isEmpty()) {
|
||||||
infoListAdapter.addInfoItemList(result.getRelatedItems());
|
infoListAdapter.addInfoItemList(result.getRelatedItems());
|
||||||
showListFooter(hasMoreItems());
|
showListFooter(hasMoreItems());
|
||||||
|
} else if (hasMoreItems()) {
|
||||||
|
loadMoreItems();
|
||||||
} else {
|
} else {
|
||||||
infoListAdapter.clearStreamItemList();
|
infoListAdapter.clearStreamItemList();
|
||||||
showEmptyState();
|
showEmptyState();
|
||||||
|
|||||||
@@ -2,14 +2,16 @@ package org.schabi.newpipe.fragments.list.channel;
|
|||||||
|
|
||||||
import static org.schabi.newpipe.extractor.stream.StreamExtractor.UNKNOWN_SUBSCRIBER_COUNT;
|
import static org.schabi.newpipe.extractor.stream.StreamExtractor.UNKNOWN_SUBSCRIBER_COUNT;
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.widget.LinearLayout;
|
import android.widget.LinearLayout;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
|
import com.evernote.android.state.State;
|
||||||
|
|
||||||
import org.schabi.newpipe.R;
|
import org.schabi.newpipe.R;
|
||||||
import org.schabi.newpipe.extractor.StreamingService;
|
import org.schabi.newpipe.extractor.StreamingService;
|
||||||
import org.schabi.newpipe.extractor.channel.ChannelInfo;
|
import org.schabi.newpipe.extractor.channel.ChannelInfo;
|
||||||
@@ -20,20 +22,16 @@ import org.schabi.newpipe.util.Localization;
|
|||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import icepick.State;
|
|
||||||
|
|
||||||
public class ChannelAboutFragment extends BaseDescriptionFragment {
|
public class ChannelAboutFragment extends BaseDescriptionFragment {
|
||||||
@State
|
@State
|
||||||
protected ChannelInfo channelInfo;
|
protected ChannelInfo channelInfo;
|
||||||
|
|
||||||
public static ChannelAboutFragment getInstance(final ChannelInfo channelInfo) {
|
ChannelAboutFragment(@NonNull final ChannelInfo channelInfo) {
|
||||||
final ChannelAboutFragment fragment = new ChannelAboutFragment();
|
this.channelInfo = channelInfo;
|
||||||
fragment.channelInfo = channelInfo;
|
|
||||||
return fragment;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public ChannelAboutFragment() {
|
public ChannelAboutFragment() {
|
||||||
super();
|
// keep empty constructor for State when resuming fragment from memory
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -45,26 +43,17 @@ public class ChannelAboutFragment extends BaseDescriptionFragment {
|
|||||||
@Nullable
|
@Nullable
|
||||||
@Override
|
@Override
|
||||||
protected Description getDescription() {
|
protected Description getDescription() {
|
||||||
if (channelInfo == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return new Description(channelInfo.getDescription(), Description.PLAIN_TEXT);
|
return new Description(channelInfo.getDescription(), Description.PLAIN_TEXT);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nullable
|
@NonNull
|
||||||
@Override
|
@Override
|
||||||
protected StreamingService getService() {
|
protected StreamingService getService() {
|
||||||
if (channelInfo == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return channelInfo.getService();
|
return channelInfo.getService();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected int getServiceId() {
|
protected int getServiceId() {
|
||||||
if (channelInfo == null) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
return channelInfo.getServiceId();
|
return channelInfo.getServiceId();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,12 +63,9 @@ public class ChannelAboutFragment extends BaseDescriptionFragment {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nullable
|
@NonNull
|
||||||
@Override
|
@Override
|
||||||
public List<String> getTags() {
|
public List<String> getTags() {
|
||||||
if (channelInfo == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return channelInfo.getTags();
|
return channelInfo.getTags();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -93,10 +79,11 @@ public class ChannelAboutFragment extends BaseDescriptionFragment {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
final Context context = getContext();
|
|
||||||
if (channelInfo.getSubscriberCount() != UNKNOWN_SUBSCRIBER_COUNT) {
|
if (channelInfo.getSubscriberCount() != UNKNOWN_SUBSCRIBER_COUNT) {
|
||||||
addMetadataItem(inflater, layout, false, R.string.metadata_subscribers,
|
addMetadataItem(inflater, layout, false, R.string.metadata_subscribers,
|
||||||
Localization.localizeNumber(context, channelInfo.getSubscriberCount()));
|
Localization.localizeNumber(
|
||||||
|
requireContext(),
|
||||||
|
channelInfo.getSubscriberCount()));
|
||||||
}
|
}
|
||||||
|
|
||||||
addImagesMetadataItem(inflater, layout, R.string.metadata_avatars,
|
addImagesMetadataItem(inflater, layout, R.string.metadata_avatars,
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import android.graphics.Color;
|
|||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import android.util.TypedValue;
|
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.Menu;
|
import android.view.Menu;
|
||||||
import android.view.MenuInflater;
|
import android.view.MenuInflater;
|
||||||
@@ -22,8 +21,10 @@ import androidx.annotation.NonNull;
|
|||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.core.content.ContextCompat;
|
import androidx.core.content.ContextCompat;
|
||||||
import androidx.core.graphics.ColorUtils;
|
import androidx.core.graphics.ColorUtils;
|
||||||
|
import androidx.core.view.MenuProvider;
|
||||||
import androidx.preference.PreferenceManager;
|
import androidx.preference.PreferenceManager;
|
||||||
|
|
||||||
|
import com.evernote.android.state.State;
|
||||||
import com.google.android.material.snackbar.Snackbar;
|
import com.google.android.material.snackbar.Snackbar;
|
||||||
import com.google.android.material.tabs.TabLayout;
|
import com.google.android.material.tabs.TabLayout;
|
||||||
import com.jakewharton.rxbinding4.view.RxView;
|
import com.jakewharton.rxbinding4.view.RxView;
|
||||||
@@ -43,22 +44,24 @@ import org.schabi.newpipe.fragments.detail.TabAdapter;
|
|||||||
import org.schabi.newpipe.ktx.AnimationType;
|
import org.schabi.newpipe.ktx.AnimationType;
|
||||||
import org.schabi.newpipe.local.feed.notifications.NotificationHelper;
|
import org.schabi.newpipe.local.feed.notifications.NotificationHelper;
|
||||||
import org.schabi.newpipe.local.subscription.SubscriptionManager;
|
import org.schabi.newpipe.local.subscription.SubscriptionManager;
|
||||||
|
import org.schabi.newpipe.ui.emptystate.EmptyStateSpec;
|
||||||
|
import org.schabi.newpipe.ui.emptystate.EmptyStateUtil;
|
||||||
import org.schabi.newpipe.util.ChannelTabHelper;
|
import org.schabi.newpipe.util.ChannelTabHelper;
|
||||||
import org.schabi.newpipe.util.Constants;
|
import org.schabi.newpipe.util.Constants;
|
||||||
import org.schabi.newpipe.util.ExtractorHelper;
|
import org.schabi.newpipe.util.ExtractorHelper;
|
||||||
import org.schabi.newpipe.util.Localization;
|
import org.schabi.newpipe.util.Localization;
|
||||||
import org.schabi.newpipe.util.NavigationHelper;
|
import org.schabi.newpipe.util.NavigationHelper;
|
||||||
import org.schabi.newpipe.util.StateSaver;
|
import org.schabi.newpipe.util.StateSaver;
|
||||||
import org.schabi.newpipe.util.image.ImageStrategy;
|
|
||||||
import org.schabi.newpipe.util.image.PicassoHelper;
|
|
||||||
import org.schabi.newpipe.util.ThemeHelper;
|
import org.schabi.newpipe.util.ThemeHelper;
|
||||||
import org.schabi.newpipe.util.external_communication.ShareUtils;
|
import org.schabi.newpipe.util.external_communication.ShareUtils;
|
||||||
|
import org.schabi.newpipe.util.image.CoilHelper;
|
||||||
|
import org.schabi.newpipe.util.image.ImageStrategy;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Queue;
|
import java.util.Queue;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
import icepick.State;
|
import coil3.util.CoilUtils;
|
||||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||||
import io.reactivex.rxjava3.core.Observable;
|
import io.reactivex.rxjava3.core.Observable;
|
||||||
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
||||||
@@ -72,7 +75,6 @@ public class ChannelFragment extends BaseStateFragment<ChannelInfo>
|
|||||||
implements StateSaver.WriteRead {
|
implements StateSaver.WriteRead {
|
||||||
|
|
||||||
private static final int BUTTON_DEBOUNCE_INTERVAL = 100;
|
private static final int BUTTON_DEBOUNCE_INTERVAL = 100;
|
||||||
private static final String PICASSO_CHANNEL_TAG = "PICASSO_CHANNEL_TAG";
|
|
||||||
|
|
||||||
@State
|
@State
|
||||||
protected int serviceId = Constants.NO_SERVICE_ID;
|
protected int serviceId = Constants.NO_SERVICE_ID;
|
||||||
@@ -99,6 +101,7 @@ public class ChannelFragment extends BaseStateFragment<ChannelInfo>
|
|||||||
private MenuItem menuRssButton;
|
private MenuItem menuRssButton;
|
||||||
private MenuItem menuNotifyButton;
|
private MenuItem menuNotifyButton;
|
||||||
private SubscriptionEntity channelSubscription;
|
private SubscriptionEntity channelSubscription;
|
||||||
|
private MenuProvider menuProvider;
|
||||||
|
|
||||||
public static ChannelFragment getInstance(final int serviceId, final String url,
|
public static ChannelFragment getInstance(final int serviceId, final String url,
|
||||||
final String name) {
|
final String name) {
|
||||||
@@ -118,12 +121,6 @@ public class ChannelFragment extends BaseStateFragment<ChannelInfo>
|
|||||||
// LifeCycle
|
// LifeCycle
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onCreate(final Bundle savedInstanceState) {
|
|
||||||
super.onCreate(savedInstanceState);
|
|
||||||
setHasOptionsMenu(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onAttach(@NonNull final Context context) {
|
public void onAttach(@NonNull final Context context) {
|
||||||
super.onAttach(context);
|
super.onAttach(context);
|
||||||
@@ -138,10 +135,76 @@ public class ChannelFragment extends BaseStateFragment<ChannelInfo>
|
|||||||
return binding.getRoot();
|
return binding.getRoot();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onViewCreated(@NonNull final View rootView, final Bundle savedInstanceState) {
|
||||||
|
super.onViewCreated(rootView, savedInstanceState);
|
||||||
|
menuProvider = new MenuProvider() {
|
||||||
|
@Override
|
||||||
|
public void onCreateMenu(@NonNull final Menu menu,
|
||||||
|
@NonNull final MenuInflater inflater) {
|
||||||
|
inflater.inflate(R.menu.menu_channel, menu);
|
||||||
|
|
||||||
|
if (DEBUG) {
|
||||||
|
Log.d(TAG, "onCreateOptionsMenu() called with: "
|
||||||
|
+ "menu = [" + menu + "], inflater = [" + inflater + "]");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onPrepareMenu(@NonNull final Menu menu) {
|
||||||
|
menuRssButton = menu.findItem(R.id.menu_item_rss);
|
||||||
|
menuNotifyButton = menu.findItem(R.id.menu_item_notify);
|
||||||
|
updateRssButton();
|
||||||
|
updateNotifyButton(channelSubscription);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onMenuItemSelected(@NonNull final MenuItem item) {
|
||||||
|
switch (item.getItemId()) {
|
||||||
|
case R.id.menu_item_notify:
|
||||||
|
final boolean value = !item.isChecked();
|
||||||
|
item.setEnabled(false);
|
||||||
|
setNotify(value);
|
||||||
|
break;
|
||||||
|
case R.id.action_settings:
|
||||||
|
NavigationHelper.openSettings(requireContext());
|
||||||
|
break;
|
||||||
|
case R.id.menu_item_rss:
|
||||||
|
if (currentInfo != null) {
|
||||||
|
ShareUtils.openUrlInApp(requireContext(), currentInfo.getFeedUrl());
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case R.id.menu_item_openInBrowser:
|
||||||
|
if (currentInfo != null) {
|
||||||
|
ShareUtils.openUrlInBrowser(requireContext(),
|
||||||
|
currentInfo.getOriginalUrl());
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case R.id.menu_item_share:
|
||||||
|
if (currentInfo != null) {
|
||||||
|
ShareUtils.shareText(requireContext(), name,
|
||||||
|
currentInfo.getOriginalUrl(), currentInfo.getAvatars());
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
activity.addMenuProvider(menuProvider);
|
||||||
|
}
|
||||||
|
|
||||||
@Override // called from onViewCreated in BaseFragment.onViewCreated
|
@Override // called from onViewCreated in BaseFragment.onViewCreated
|
||||||
protected void initViews(final View rootView, final Bundle savedInstanceState) {
|
protected void initViews(final View rootView, final Bundle savedInstanceState) {
|
||||||
super.initViews(rootView, savedInstanceState);
|
super.initViews(rootView, savedInstanceState);
|
||||||
|
|
||||||
|
EmptyStateUtil.setEmptyStateComposable(
|
||||||
|
binding.emptyStateView,
|
||||||
|
EmptyStateSpec.Companion.getContentNotSupported()
|
||||||
|
);
|
||||||
|
|
||||||
tabAdapter = new TabAdapter(getChildFragmentManager());
|
tabAdapter = new TabAdapter(getChildFragmentManager());
|
||||||
binding.viewPager.setAdapter(tabAdapter);
|
binding.viewPager.setAdapter(tabAdapter);
|
||||||
binding.tabLayout.setupWithViewPager(binding.viewPager);
|
binding.tabLayout.setupWithViewPager(binding.viewPager);
|
||||||
@@ -175,6 +238,14 @@ public class ChannelFragment extends BaseStateFragment<ChannelInfo>
|
|||||||
binding.subChannelTitleView.setOnClickListener(openSubChannel);
|
binding.subChannelTitleView.setOnClickListener(openSubChannel);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDestroyView() {
|
||||||
|
super.onDestroyView();
|
||||||
|
if (menuProvider != null) {
|
||||||
|
activity.removeMenuProvider(menuProvider);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onDestroy() {
|
public void onDestroy() {
|
||||||
super.onDestroy();
|
super.onDestroy();
|
||||||
@@ -183,73 +254,15 @@ public class ChannelFragment extends BaseStateFragment<ChannelInfo>
|
|||||||
}
|
}
|
||||||
disposables.clear();
|
disposables.clear();
|
||||||
binding = null;
|
binding = null;
|
||||||
|
menuProvider = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
|
||||||
// Menu
|
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onCreateOptionsMenu(@NonNull final Menu menu,
|
|
||||||
@NonNull final MenuInflater inflater) {
|
|
||||||
super.onCreateOptionsMenu(menu, inflater);
|
|
||||||
inflater.inflate(R.menu.menu_channel, menu);
|
|
||||||
|
|
||||||
if (DEBUG) {
|
|
||||||
Log.d(TAG, "onCreateOptionsMenu() called with: "
|
|
||||||
+ "menu = [" + menu + "], inflater = [" + inflater + "]");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onPrepareOptionsMenu(@NonNull final Menu menu) {
|
|
||||||
super.onPrepareOptionsMenu(menu);
|
|
||||||
menuRssButton = menu.findItem(R.id.menu_item_rss);
|
|
||||||
menuNotifyButton = menu.findItem(R.id.menu_item_notify);
|
|
||||||
updateNotifyButton(channelSubscription);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean onOptionsItemSelected(@NonNull final MenuItem item) {
|
|
||||||
switch (item.getItemId()) {
|
|
||||||
case R.id.menu_item_notify:
|
|
||||||
final boolean value = !item.isChecked();
|
|
||||||
item.setEnabled(false);
|
|
||||||
setNotify(value);
|
|
||||||
break;
|
|
||||||
case R.id.action_settings:
|
|
||||||
NavigationHelper.openSettings(requireContext());
|
|
||||||
break;
|
|
||||||
case R.id.menu_item_rss:
|
|
||||||
if (currentInfo != null) {
|
|
||||||
ShareUtils.openUrlInApp(requireContext(), currentInfo.getFeedUrl());
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case R.id.menu_item_openInBrowser:
|
|
||||||
if (currentInfo != null) {
|
|
||||||
ShareUtils.openUrlInBrowser(requireContext(), currentInfo.getOriginalUrl());
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case R.id.menu_item_share:
|
|
||||||
if (currentInfo != null) {
|
|
||||||
ShareUtils.shareText(requireContext(), name, currentInfo.getOriginalUrl(),
|
|
||||||
currentInfo.getAvatars());
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
return super.onOptionsItemSelected(item);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
// Channel Subscription
|
// Channel Subscription
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
private void monitorSubscription(final ChannelInfo info) {
|
private void monitorSubscription(final ChannelInfo info) {
|
||||||
final Consumer<Throwable> onError = (Throwable throwable) -> {
|
final Consumer<Throwable> onError = (final Throwable throwable) -> {
|
||||||
animate(binding.channelSubscribeButton, false, 100);
|
animate(binding.channelSubscribeButton, false, 100);
|
||||||
showSnackBarError(new ErrorInfo(throwable, UserAction.SUBSCRIPTION_GET,
|
showSnackBarError(new ErrorInfo(throwable, UserAction.SUBSCRIPTION_GET,
|
||||||
"Get subscription status", currentInfo));
|
"Get subscription status", currentInfo));
|
||||||
@@ -284,14 +297,14 @@ public class ChannelFragment extends BaseStateFragment<ChannelInfo>
|
|||||||
}
|
}
|
||||||
|
|
||||||
private Function<Object, Object> mapOnSubscribe(final SubscriptionEntity subscription) {
|
private Function<Object, Object> mapOnSubscribe(final SubscriptionEntity subscription) {
|
||||||
return (@NonNull Object o) -> {
|
return (@NonNull final Object o) -> {
|
||||||
subscriptionManager.insertSubscription(subscription);
|
subscriptionManager.insertSubscription(subscription);
|
||||||
return o;
|
return o;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private Function<Object, Object> mapOnUnsubscribe(final SubscriptionEntity subscription) {
|
private Function<Object, Object> mapOnUnsubscribe(final SubscriptionEntity subscription) {
|
||||||
return (@NonNull Object o) -> {
|
return (@NonNull final Object o) -> {
|
||||||
subscriptionManager.deleteSubscription(subscription);
|
subscriptionManager.deleteSubscription(subscription);
|
||||||
return o;
|
return o;
|
||||||
};
|
};
|
||||||
@@ -318,7 +331,7 @@ public class ChannelFragment extends BaseStateFragment<ChannelInfo>
|
|||||||
}
|
}
|
||||||
|
|
||||||
private Disposable monitorSubscribeButton(final Function<Object, Object> action) {
|
private Disposable monitorSubscribeButton(final Function<Object, Object> action) {
|
||||||
final Consumer<Object> onNext = (@NonNull Object o) -> {
|
final Consumer<Object> onNext = (@NonNull final Object o) -> {
|
||||||
if (DEBUG) {
|
if (DEBUG) {
|
||||||
Log.d(TAG, "Changed subscription status to this channel!");
|
Log.d(TAG, "Changed subscription status to this channel!");
|
||||||
}
|
}
|
||||||
@@ -338,7 +351,7 @@ public class ChannelFragment extends BaseStateFragment<ChannelInfo>
|
|||||||
}
|
}
|
||||||
|
|
||||||
private Consumer<List<SubscriptionEntity>> getSubscribeUpdateMonitor(final ChannelInfo info) {
|
private Consumer<List<SubscriptionEntity>> getSubscribeUpdateMonitor(final ChannelInfo info) {
|
||||||
return (List<SubscriptionEntity> subscriptionEntities) -> {
|
return (final List<SubscriptionEntity> subscriptionEntities) -> {
|
||||||
if (DEBUG) {
|
if (DEBUG) {
|
||||||
Log.d(TAG, "subscriptionManager.subscriptionTable.doOnNext() called with: "
|
Log.d(TAG, "subscriptionManager.subscriptionTable.doOnNext() called with: "
|
||||||
+ "subscriptionEntities = [" + subscriptionEntities + "]");
|
+ "subscriptionEntities = [" + subscriptionEntities + "]");
|
||||||
@@ -408,6 +421,13 @@ public class ChannelFragment extends BaseStateFragment<ChannelInfo>
|
|||||||
animate(binding.channelSubscribeButton, true, 100, AnimationType.LIGHT_SCALE_AND_ALPHA);
|
animate(binding.channelSubscribeButton, true, 100, AnimationType.LIGHT_SCALE_AND_ALPHA);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void updateRssButton() {
|
||||||
|
if (menuRssButton == null || currentInfo == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
menuRssButton.setVisible(!TextUtils.isEmpty(currentInfo.getFeedUrl()));
|
||||||
|
}
|
||||||
|
|
||||||
private void updateNotifyButton(@Nullable final SubscriptionEntity subscription) {
|
private void updateNotifyButton(@Nullable final SubscriptionEntity subscription) {
|
||||||
if (menuNotifyButton == null) {
|
if (menuNotifyButton == null) {
|
||||||
return;
|
return;
|
||||||
@@ -474,7 +494,7 @@ public class ChannelFragment extends BaseStateFragment<ChannelInfo>
|
|||||||
if (ChannelTabHelper.showChannelTab(
|
if (ChannelTabHelper.showChannelTab(
|
||||||
context, preferences, R.string.show_channel_tabs_about)) {
|
context, preferences, R.string.show_channel_tabs_about)) {
|
||||||
tabAdapter.addFragment(
|
tabAdapter.addFragment(
|
||||||
ChannelAboutFragment.getInstance(currentInfo),
|
new ChannelAboutFragment(currentInfo),
|
||||||
context.getString(R.string.channel_tab_about));
|
context.getString(R.string.channel_tab_about));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -569,7 +589,9 @@ public class ChannelFragment extends BaseStateFragment<ChannelInfo>
|
|||||||
@Override
|
@Override
|
||||||
public void showLoading() {
|
public void showLoading() {
|
||||||
super.showLoading();
|
super.showLoading();
|
||||||
PicassoHelper.cancelTag(PICASSO_CHANNEL_TAG);
|
CoilUtils.dispose(binding.channelAvatarView);
|
||||||
|
CoilUtils.dispose(binding.channelBannerImage);
|
||||||
|
CoilUtils.dispose(binding.subChannelAvatarView);
|
||||||
animate(binding.channelSubscribeButton, false, 100);
|
animate(binding.channelSubscribeButton, false, 100);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -580,17 +602,15 @@ public class ChannelFragment extends BaseStateFragment<ChannelInfo>
|
|||||||
setInitialData(result.getServiceId(), result.getOriginalUrl(), result.getName());
|
setInitialData(result.getServiceId(), result.getOriginalUrl(), result.getName());
|
||||||
|
|
||||||
if (ImageStrategy.shouldLoadImages() && !result.getBanners().isEmpty()) {
|
if (ImageStrategy.shouldLoadImages() && !result.getBanners().isEmpty()) {
|
||||||
PicassoHelper.loadBanner(result.getBanners()).tag(PICASSO_CHANNEL_TAG)
|
CoilHelper.INSTANCE.loadBanner(binding.channelBannerImage, result.getBanners());
|
||||||
.into(binding.channelBannerImage);
|
|
||||||
} else {
|
} else {
|
||||||
// do not waste space for the banner, if the user disabled images or there is not one
|
// do not waste space for the banner, if the user disabled images or there is not one
|
||||||
binding.channelBannerImage.setImageDrawable(null);
|
binding.channelBannerImage.setImageDrawable(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
PicassoHelper.loadAvatar(result.getAvatars()).tag(PICASSO_CHANNEL_TAG)
|
CoilHelper.INSTANCE.loadAvatar(binding.channelAvatarView, result.getAvatars());
|
||||||
.into(binding.channelAvatarView);
|
CoilHelper.INSTANCE.loadAvatar(binding.subChannelAvatarView,
|
||||||
PicassoHelper.loadAvatar(result.getParentChannelAvatars()).tag(PICASSO_CHANNEL_TAG)
|
result.getParentChannelAvatars());
|
||||||
.into(binding.subChannelAvatarView);
|
|
||||||
|
|
||||||
binding.channelTitleView.setText(result.getName());
|
binding.channelTitleView.setText(result.getName());
|
||||||
binding.channelSubscriberView.setVisibility(View.VISIBLE);
|
binding.channelSubscriberView.setVisibility(View.VISIBLE);
|
||||||
@@ -610,9 +630,7 @@ public class ChannelFragment extends BaseStateFragment<ChannelInfo>
|
|||||||
binding.subChannelAvatarView.setVisibility(View.VISIBLE);
|
binding.subChannelAvatarView.setVisibility(View.VISIBLE);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (menuRssButton != null) {
|
updateRssButton();
|
||||||
menuRssButton.setVisible(!TextUtils.isEmpty(result.getFeedUrl()));
|
|
||||||
}
|
|
||||||
|
|
||||||
channelContentNotSupported = false;
|
channelContentNotSupported = false;
|
||||||
for (final Throwable throwable : result.getErrors()) {
|
for (final Throwable throwable : result.getErrors()) {
|
||||||
@@ -640,8 +658,6 @@ public class ChannelFragment extends BaseStateFragment<ChannelInfo>
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.errorContentNotSupported.setVisibility(View.VISIBLE);
|
binding.emptyStateView.setVisibility(View.VISIBLE);
|
||||||
binding.channelKaomoji.setText("(︶︹︺)");
|
|
||||||
binding.channelKaomoji.setTextSize(TypedValue.COMPLEX_UNIT_SP, 45f);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ import android.view.ViewGroup;
|
|||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
|
import com.evernote.android.state.State;
|
||||||
|
|
||||||
import org.schabi.newpipe.R;
|
import org.schabi.newpipe.R;
|
||||||
import org.schabi.newpipe.databinding.PlaylistControlBinding;
|
import org.schabi.newpipe.databinding.PlaylistControlBinding;
|
||||||
import org.schabi.newpipe.error.UserAction;
|
import org.schabi.newpipe.error.UserAction;
|
||||||
@@ -17,12 +19,14 @@ import org.schabi.newpipe.extractor.ListExtractor;
|
|||||||
import org.schabi.newpipe.extractor.channel.tabs.ChannelTabInfo;
|
import org.schabi.newpipe.extractor.channel.tabs.ChannelTabInfo;
|
||||||
import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
||||||
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler;
|
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler;
|
||||||
|
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandlerFactory;
|
||||||
import org.schabi.newpipe.extractor.linkhandler.ReadyChannelTabListLinkHandler;
|
import org.schabi.newpipe.extractor.linkhandler.ReadyChannelTabListLinkHandler;
|
||||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||||
import org.schabi.newpipe.fragments.list.BaseListInfoFragment;
|
import org.schabi.newpipe.fragments.list.BaseListInfoFragment;
|
||||||
import org.schabi.newpipe.fragments.list.playlist.PlaylistControlViewHolder;
|
import org.schabi.newpipe.fragments.list.playlist.PlaylistControlViewHolder;
|
||||||
import org.schabi.newpipe.player.playqueue.ChannelTabPlayQueue;
|
import org.schabi.newpipe.player.playqueue.ChannelTabPlayQueue;
|
||||||
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
||||||
|
import org.schabi.newpipe.ui.emptystate.EmptyStateUtil;
|
||||||
import org.schabi.newpipe.util.ChannelTabHelper;
|
import org.schabi.newpipe.util.ChannelTabHelper;
|
||||||
import org.schabi.newpipe.util.ExtractorHelper;
|
import org.schabi.newpipe.util.ExtractorHelper;
|
||||||
import org.schabi.newpipe.util.PlayButtonHelper;
|
import org.schabi.newpipe.util.PlayButtonHelper;
|
||||||
@@ -31,13 +35,12 @@ import java.util.List;
|
|||||||
import java.util.function.Supplier;
|
import java.util.function.Supplier;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import icepick.State;
|
|
||||||
import io.reactivex.rxjava3.core.Single;
|
import io.reactivex.rxjava3.core.Single;
|
||||||
|
|
||||||
public class ChannelTabFragment extends BaseListInfoFragment<InfoItem, ChannelTabInfo>
|
public class ChannelTabFragment extends BaseListInfoFragment<InfoItem, ChannelTabInfo>
|
||||||
implements PlaylistControlViewHolder {
|
implements PlaylistControlViewHolder {
|
||||||
|
|
||||||
// states must be protected and not private for IcePick being able to access them
|
// states must be protected and not private for State being able to access them
|
||||||
@State
|
@State
|
||||||
protected ListLinkHandler tabHandler;
|
protected ListLinkHandler tabHandler;
|
||||||
@State
|
@State
|
||||||
@@ -77,6 +80,12 @@ public class ChannelTabFragment extends BaseListInfoFragment<InfoItem, ChannelTa
|
|||||||
return inflater.inflate(R.layout.fragment_channel_tab, container, false);
|
return inflater.inflate(R.layout.fragment_channel_tab, container, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onViewCreated(@NonNull final View rootView, final Bundle savedInstanceState) {
|
||||||
|
super.onViewCreated(rootView, savedInstanceState);
|
||||||
|
EmptyStateUtil.setEmptyStateComposable(rootView.findViewById(R.id.empty_state_view));
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onDestroyView() {
|
public void onDestroyView() {
|
||||||
super.onDestroyView();
|
super.onDestroyView();
|
||||||
@@ -128,10 +137,13 @@ public class ChannelTabFragment extends BaseListInfoFragment<InfoItem, ChannelTa
|
|||||||
// once `handleResult` is called, the parsed data was already saved to cache, so
|
// once `handleResult` is called, the parsed data was already saved to cache, so
|
||||||
// we can discard any raw data in ReadyChannelTabListLinkHandler and create a
|
// we can discard any raw data in ReadyChannelTabListLinkHandler and create a
|
||||||
// link handler with identical properties, but without any raw data
|
// link handler with identical properties, but without any raw data
|
||||||
tabHandler = result.getService()
|
final ListLinkHandlerFactory channelTabLHFactory = result.getService()
|
||||||
.getChannelTabLHFactory()
|
.getChannelTabLHFactory();
|
||||||
.fromQuery(tabHandler.getId(), tabHandler.getContentFilters(),
|
if (channelTabLHFactory != null) {
|
||||||
tabHandler.getSortFilter());
|
// some services do not not have a ChannelTabLHFactory
|
||||||
|
tabHandler = channelTabLHFactory.fromQuery(tabHandler.getId(),
|
||||||
|
tabHandler.getContentFilters(), tabHandler.getSortFilter());
|
||||||
|
}
|
||||||
} catch (final ParsingException e) {
|
} catch (final ParsingException e) {
|
||||||
// silently ignore the error, as the app can continue to function normally
|
// silently ignore the error, as the app can continue to function normally
|
||||||
Log.w(TAG, "Could not recreate channel tab handler", e);
|
Log.w(TAG, "Could not recreate channel tab handler", e);
|
||||||
@@ -152,6 +164,7 @@ public class ChannelTabFragment extends BaseListInfoFragment<InfoItem, ChannelTa
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
public PlayQueue getPlayQueue() {
|
public PlayQueue getPlayQueue() {
|
||||||
final List<StreamInfoItem> streamItems = infoListAdapter.getItemsList().stream()
|
final List<StreamInfoItem> streamItems = infoListAdapter.getItemsList().stream()
|
||||||
.filter(StreamInfoItem.class::isInstance)
|
.filter(StreamInfoItem.class::isInstance)
|
||||||
|
|||||||
@@ -1,113 +0,0 @@
|
|||||||
package org.schabi.newpipe.fragments.list.comments;
|
|
||||||
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.view.LayoutInflater;
|
|
||||||
import android.view.Menu;
|
|
||||||
import android.view.MenuInflater;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.ViewGroup;
|
|
||||||
import android.widget.TextView;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
|
|
||||||
import org.schabi.newpipe.R;
|
|
||||||
import org.schabi.newpipe.error.UserAction;
|
|
||||||
import org.schabi.newpipe.extractor.ListExtractor;
|
|
||||||
import org.schabi.newpipe.extractor.comments.CommentsInfo;
|
|
||||||
import org.schabi.newpipe.extractor.comments.CommentsInfoItem;
|
|
||||||
import org.schabi.newpipe.fragments.list.BaseListInfoFragment;
|
|
||||||
import org.schabi.newpipe.info_list.ItemViewMode;
|
|
||||||
import org.schabi.newpipe.ktx.ViewUtils;
|
|
||||||
import org.schabi.newpipe.util.ExtractorHelper;
|
|
||||||
|
|
||||||
import io.reactivex.rxjava3.core.Single;
|
|
||||||
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
|
||||||
|
|
||||||
public class CommentsFragment extends BaseListInfoFragment<CommentsInfoItem, CommentsInfo> {
|
|
||||||
private final CompositeDisposable disposables = new CompositeDisposable();
|
|
||||||
|
|
||||||
private TextView emptyStateDesc;
|
|
||||||
|
|
||||||
public static CommentsFragment getInstance(final int serviceId, final String url,
|
|
||||||
final String name) {
|
|
||||||
final CommentsFragment instance = new CommentsFragment();
|
|
||||||
instance.setInitialData(serviceId, url, name);
|
|
||||||
return instance;
|
|
||||||
}
|
|
||||||
|
|
||||||
public CommentsFragment() {
|
|
||||||
super(UserAction.REQUESTED_COMMENTS);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void initViews(final View rootView, final Bundle savedInstanceState) {
|
|
||||||
super.initViews(rootView, savedInstanceState);
|
|
||||||
|
|
||||||
emptyStateDesc = rootView.findViewById(R.id.empty_state_desc);
|
|
||||||
}
|
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
|
||||||
// LifeCycle
|
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public View onCreateView(@NonNull final LayoutInflater inflater,
|
|
||||||
@Nullable final ViewGroup container,
|
|
||||||
@Nullable final Bundle savedInstanceState) {
|
|
||||||
return inflater.inflate(R.layout.fragment_comments, container, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onDestroy() {
|
|
||||||
super.onDestroy();
|
|
||||||
disposables.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
|
||||||
// Load and handle
|
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected Single<ListExtractor.InfoItemsPage<CommentsInfoItem>> loadMoreItemsLogic() {
|
|
||||||
return ExtractorHelper.getMoreCommentItems(serviceId, currentInfo, currentNextPage);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected Single<CommentsInfo> loadResult(final boolean forceLoad) {
|
|
||||||
return ExtractorHelper.getCommentsInfo(serviceId, url, forceLoad);
|
|
||||||
}
|
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
|
||||||
// Contract
|
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void handleResult(@NonNull final CommentsInfo result) {
|
|
||||||
super.handleResult(result);
|
|
||||||
|
|
||||||
emptyStateDesc.setText(
|
|
||||||
result.isCommentsDisabled()
|
|
||||||
? R.string.comments_are_disabled
|
|
||||||
: R.string.no_comments);
|
|
||||||
|
|
||||||
ViewUtils.slideUp(requireView(), 120, 150, 0.06f);
|
|
||||||
disposables.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
|
||||||
// Utils
|
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void setTitle(final String title) { }
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onCreateOptionsMenu(@NonNull final Menu menu,
|
|
||||||
@NonNull final MenuInflater inflater) { }
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected ItemViewMode getItemViewMode() {
|
|
||||||
return ItemViewMode.LIST;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
package org.schabi.newpipe.fragments.list.comments
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.compose.material3.Surface
|
||||||
|
import androidx.core.os.bundleOf
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import androidx.fragment.compose.content
|
||||||
|
import org.schabi.newpipe.ui.components.video.comment.CommentSection
|
||||||
|
import org.schabi.newpipe.ui.theme.AppTheme
|
||||||
|
import org.schabi.newpipe.util.KEY_SERVICE_ID
|
||||||
|
import org.schabi.newpipe.util.KEY_URL
|
||||||
|
|
||||||
|
class CommentsFragment : Fragment() {
|
||||||
|
override fun onCreateView(
|
||||||
|
inflater: LayoutInflater,
|
||||||
|
container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?
|
||||||
|
) = content {
|
||||||
|
AppTheme {
|
||||||
|
Surface {
|
||||||
|
CommentSection()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
@JvmStatic
|
||||||
|
fun getInstance(serviceId: Int, url: String?) = CommentsFragment().apply {
|
||||||
|
arguments = bundleOf(KEY_SERVICE_ID to serviceId, KEY_URL to url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,6 +11,8 @@ import androidx.annotation.NonNull;
|
|||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.appcompat.app.ActionBar;
|
import androidx.appcompat.app.ActionBar;
|
||||||
|
|
||||||
|
import com.evernote.android.state.State;
|
||||||
|
|
||||||
import org.schabi.newpipe.R;
|
import org.schabi.newpipe.R;
|
||||||
import org.schabi.newpipe.error.ErrorInfo;
|
import org.schabi.newpipe.error.ErrorInfo;
|
||||||
import org.schabi.newpipe.error.UserAction;
|
import org.schabi.newpipe.error.UserAction;
|
||||||
@@ -29,7 +31,6 @@ import org.schabi.newpipe.util.ExtractorHelper;
|
|||||||
import org.schabi.newpipe.util.KioskTranslator;
|
import org.schabi.newpipe.util.KioskTranslator;
|
||||||
import org.schabi.newpipe.util.Localization;
|
import org.schabi.newpipe.util.Localization;
|
||||||
|
|
||||||
import icepick.State;
|
|
||||||
import io.reactivex.rxjava3.core.Single;
|
import io.reactivex.rxjava3.core.Single;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
package org.schabi.newpipe.fragments.list.playlist;
|
package org.schabi.newpipe.fragments.list.playlist;
|
||||||
|
|
||||||
|
import static org.schabi.newpipe.extractor.utils.Utils.isBlank;
|
||||||
import static org.schabi.newpipe.ktx.ViewUtils.animate;
|
import static org.schabi.newpipe.ktx.ViewUtils.animate;
|
||||||
import static org.schabi.newpipe.ktx.ViewUtils.animateHideRecyclerViewAllowingScrolling;
|
import static org.schabi.newpipe.ktx.ViewUtils.animateHideRecyclerViewAllowingScrolling;
|
||||||
|
import static org.schabi.newpipe.util.ServiceHelper.getServiceById;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
@@ -37,6 +39,7 @@ import org.schabi.newpipe.extractor.ListExtractor;
|
|||||||
import org.schabi.newpipe.extractor.ServiceList;
|
import org.schabi.newpipe.extractor.ServiceList;
|
||||||
import org.schabi.newpipe.extractor.playlist.PlaylistInfo;
|
import org.schabi.newpipe.extractor.playlist.PlaylistInfo;
|
||||||
import org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper;
|
import org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper;
|
||||||
|
import org.schabi.newpipe.extractor.stream.Description;
|
||||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||||
import org.schabi.newpipe.fragments.list.BaseListInfoFragment;
|
import org.schabi.newpipe.fragments.list.BaseListInfoFragment;
|
||||||
import org.schabi.newpipe.info_list.dialog.InfoItemDialog;
|
import org.schabi.newpipe.info_list.dialog.InfoItemDialog;
|
||||||
@@ -48,9 +51,10 @@ import org.schabi.newpipe.player.playqueue.PlaylistPlayQueue;
|
|||||||
import org.schabi.newpipe.util.ExtractorHelper;
|
import org.schabi.newpipe.util.ExtractorHelper;
|
||||||
import org.schabi.newpipe.util.Localization;
|
import org.schabi.newpipe.util.Localization;
|
||||||
import org.schabi.newpipe.util.NavigationHelper;
|
import org.schabi.newpipe.util.NavigationHelper;
|
||||||
import org.schabi.newpipe.util.image.PicassoHelper;
|
|
||||||
import org.schabi.newpipe.util.external_communication.ShareUtils;
|
|
||||||
import org.schabi.newpipe.util.PlayButtonHelper;
|
import org.schabi.newpipe.util.PlayButtonHelper;
|
||||||
|
import org.schabi.newpipe.util.external_communication.ShareUtils;
|
||||||
|
import org.schabi.newpipe.util.image.CoilHelper;
|
||||||
|
import org.schabi.newpipe.util.text.TextEllipsizer;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -58,6 +62,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
|||||||
import java.util.function.Supplier;
|
import java.util.function.Supplier;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import coil3.util.CoilUtils;
|
||||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||||
import io.reactivex.rxjava3.core.Flowable;
|
import io.reactivex.rxjava3.core.Flowable;
|
||||||
import io.reactivex.rxjava3.core.Single;
|
import io.reactivex.rxjava3.core.Single;
|
||||||
@@ -67,8 +72,6 @@ import io.reactivex.rxjava3.disposables.Disposable;
|
|||||||
public class PlaylistFragment extends BaseListInfoFragment<StreamInfoItem, PlaylistInfo>
|
public class PlaylistFragment extends BaseListInfoFragment<StreamInfoItem, PlaylistInfo>
|
||||||
implements PlaylistControlViewHolder {
|
implements PlaylistControlViewHolder {
|
||||||
|
|
||||||
private static final String PICASSO_PLAYLIST_TAG = "PICASSO_PLAYLIST_TAG";
|
|
||||||
|
|
||||||
private CompositeDisposable disposables;
|
private CompositeDisposable disposables;
|
||||||
private Subscription bookmarkReactor;
|
private Subscription bookmarkReactor;
|
||||||
private AtomicBoolean isBookmarkButtonReady;
|
private AtomicBoolean isBookmarkButtonReady;
|
||||||
@@ -85,6 +88,9 @@ public class PlaylistFragment extends BaseListInfoFragment<StreamInfoItem, Playl
|
|||||||
|
|
||||||
private MenuItem playlistBookmarkButton;
|
private MenuItem playlistBookmarkButton;
|
||||||
|
|
||||||
|
private long streamCount;
|
||||||
|
private long playlistOverallDurationSeconds;
|
||||||
|
|
||||||
public static PlaylistFragment getInstance(final int serviceId, final String url,
|
public static PlaylistFragment getInstance(final int serviceId, final String url,
|
||||||
final String name) {
|
final String name) {
|
||||||
final PlaylistFragment instance = new PlaylistFragment();
|
final PlaylistFragment instance = new PlaylistFragment();
|
||||||
@@ -269,10 +275,16 @@ public class PlaylistFragment extends BaseListInfoFragment<StreamInfoItem, Playl
|
|||||||
animate(headerBinding.getRoot(), false, 200);
|
animate(headerBinding.getRoot(), false, 200);
|
||||||
animateHideRecyclerViewAllowingScrolling(itemsList);
|
animateHideRecyclerViewAllowingScrolling(itemsList);
|
||||||
|
|
||||||
PicassoHelper.cancelTag(PICASSO_PLAYLIST_TAG);
|
CoilUtils.dispose(headerBinding.uploaderAvatarView);
|
||||||
animate(headerBinding.uploaderLayout, false, 200);
|
animate(headerBinding.uploaderLayout, false, 200);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void handleNextItems(final ListExtractor.InfoItemsPage result) {
|
||||||
|
super.handleNextItems(result);
|
||||||
|
setStreamCountAndOverallDuration(result.getItems(), !result.hasNextPage());
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void handleResult(@NonNull final PlaylistInfo result) {
|
public void handleResult(@NonNull final PlaylistInfo result) {
|
||||||
super.handleResult(result);
|
super.handleResult(result);
|
||||||
@@ -314,12 +326,36 @@ public class PlaylistFragment extends BaseListInfoFragment<StreamInfoItem, Playl
|
|||||||
R.drawable.ic_radio)
|
R.drawable.ic_radio)
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
PicassoHelper.loadAvatar(result.getUploaderAvatars()).tag(PICASSO_PLAYLIST_TAG)
|
CoilHelper.INSTANCE.loadAvatar(headerBinding.uploaderAvatarView,
|
||||||
.into(headerBinding.uploaderAvatarView);
|
result.getUploaderAvatars());
|
||||||
}
|
}
|
||||||
|
|
||||||
headerBinding.playlistStreamCount.setText(Localization
|
streamCount = result.getStreamCount();
|
||||||
.localizeStreamCount(getContext(), result.getStreamCount()));
|
setStreamCountAndOverallDuration(result.getRelatedItems(), !result.hasNextPage());
|
||||||
|
|
||||||
|
final Description description = result.getDescription();
|
||||||
|
if (description != null && description != Description.EMPTY_DESCRIPTION
|
||||||
|
&& !isBlank(description.getContent())) {
|
||||||
|
final TextEllipsizer ellipsizer = new TextEllipsizer(
|
||||||
|
headerBinding.playlistDescription, 5, getServiceById(result.getServiceId()));
|
||||||
|
ellipsizer.setStateChangeListener(isEllipsized ->
|
||||||
|
headerBinding.playlistDescriptionReadMore.setText(
|
||||||
|
Boolean.TRUE.equals(isEllipsized) ? R.string.show_more : R.string.show_less
|
||||||
|
));
|
||||||
|
ellipsizer.setOnContentChanged(canBeEllipsized -> {
|
||||||
|
headerBinding.playlistDescriptionReadMore.setVisibility(
|
||||||
|
Boolean.TRUE.equals(canBeEllipsized) ? View.VISIBLE : View.GONE);
|
||||||
|
if (Boolean.TRUE.equals(canBeEllipsized)) {
|
||||||
|
ellipsizer.ellipsize();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
ellipsizer.setContent(description);
|
||||||
|
headerBinding.playlistDescriptionReadMore.setOnClickListener(v -> ellipsizer.toggle());
|
||||||
|
headerBinding.playlistDescription.setOnClickListener(v -> ellipsizer.toggle());
|
||||||
|
} else {
|
||||||
|
headerBinding.playlistDescription.setVisibility(View.GONE);
|
||||||
|
headerBinding.playlistDescriptionReadMore.setVisibility(View.GONE);
|
||||||
|
}
|
||||||
|
|
||||||
if (!result.getErrors().isEmpty()) {
|
if (!result.getErrors().isEmpty()) {
|
||||||
showSnackBarError(new ErrorInfo(result.getErrors(), UserAction.REQUESTED_PLAYLIST,
|
showSnackBarError(new ErrorInfo(result.getErrors(), UserAction.REQUESTED_PLAYLIST,
|
||||||
@@ -459,4 +495,20 @@ public class PlaylistFragment extends BaseListInfoFragment<StreamInfoItem, Playl
|
|||||||
playlistBookmarkButton.setIcon(drawable);
|
playlistBookmarkButton.setIcon(drawable);
|
||||||
playlistBookmarkButton.setTitle(titleRes);
|
playlistBookmarkButton.setTitle(titleRes);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void setStreamCountAndOverallDuration(final List<StreamInfoItem> list,
|
||||||
|
final boolean isDurationComplete) {
|
||||||
|
if (activity != null && headerBinding != null) {
|
||||||
|
playlistOverallDurationSeconds += list.stream()
|
||||||
|
.mapToLong(x -> x.getDuration())
|
||||||
|
.sum();
|
||||||
|
headerBinding.playlistStreamCount.setText(
|
||||||
|
Localization.concatenateStrings(
|
||||||
|
Localization.localizeStreamCount(activity, streamCount),
|
||||||
|
Localization.getDurationString(playlistOverallDurationSeconds,
|
||||||
|
isDurationComplete, true))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package org.schabi.newpipe.fragments.list.search;
|
package org.schabi.newpipe.fragments.list.search;
|
||||||
|
|
||||||
import static androidx.recyclerview.widget.ItemTouchHelper.Callback.makeMovementFlags;
|
import static androidx.recyclerview.widget.ItemTouchHelper.Callback.makeMovementFlags;
|
||||||
|
import static org.schabi.newpipe.extractor.utils.Utils.isBlank;
|
||||||
import static org.schabi.newpipe.ktx.ViewUtils.animate;
|
import static org.schabi.newpipe.ktx.ViewUtils.animate;
|
||||||
import static org.schabi.newpipe.util.ExtractorHelper.showMetaInfoInTextView;
|
import static org.schabi.newpipe.util.ExtractorHelper.showMetaInfoInTextView;
|
||||||
import static java.util.Arrays.asList;
|
import static java.util.Arrays.asList;
|
||||||
@@ -39,6 +40,8 @@ import androidx.preference.PreferenceManager;
|
|||||||
import androidx.recyclerview.widget.ItemTouchHelper;
|
import androidx.recyclerview.widget.ItemTouchHelper;
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
|
|
||||||
|
import com.evernote.android.state.State;
|
||||||
|
|
||||||
import org.schabi.newpipe.R;
|
import org.schabi.newpipe.R;
|
||||||
import org.schabi.newpipe.databinding.FragmentSearchBinding;
|
import org.schabi.newpipe.databinding.FragmentSearchBinding;
|
||||||
import org.schabi.newpipe.error.ErrorInfo;
|
import org.schabi.newpipe.error.ErrorInfo;
|
||||||
@@ -61,6 +64,8 @@ import org.schabi.newpipe.ktx.AnimationType;
|
|||||||
import org.schabi.newpipe.ktx.ExceptionUtils;
|
import org.schabi.newpipe.ktx.ExceptionUtils;
|
||||||
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
||||||
import org.schabi.newpipe.settings.NewPipeSettings;
|
import org.schabi.newpipe.settings.NewPipeSettings;
|
||||||
|
import org.schabi.newpipe.ui.emptystate.EmptyStateSpec;
|
||||||
|
import org.schabi.newpipe.ui.emptystate.EmptyStateUtil;
|
||||||
import org.schabi.newpipe.util.Constants;
|
import org.schabi.newpipe.util.Constants;
|
||||||
import org.schabi.newpipe.util.DeviceUtils;
|
import org.schabi.newpipe.util.DeviceUtils;
|
||||||
import org.schabi.newpipe.util.ExtractorHelper;
|
import org.schabi.newpipe.util.ExtractorHelper;
|
||||||
@@ -76,7 +81,6 @@ import java.util.Queue;
|
|||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import icepick.State;
|
|
||||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||||
import io.reactivex.rxjava3.core.Observable;
|
import io.reactivex.rxjava3.core.Observable;
|
||||||
import io.reactivex.rxjava3.core.Single;
|
import io.reactivex.rxjava3.core.Single;
|
||||||
@@ -342,6 +346,10 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
|||||||
protected void initViews(final View rootView, final Bundle savedInstanceState) {
|
protected void initViews(final View rootView, final Bundle savedInstanceState) {
|
||||||
super.initViews(rootView, savedInstanceState);
|
super.initViews(rootView, savedInstanceState);
|
||||||
|
|
||||||
|
EmptyStateUtil.setEmptyStateComposable(
|
||||||
|
searchBinding.emptyStateView,
|
||||||
|
EmptyStateSpec.Companion.getNoSearchResult());
|
||||||
|
|
||||||
searchBinding.suggestionsList.setAdapter(suggestionListAdapter);
|
searchBinding.suggestionsList.setAdapter(suggestionListAdapter);
|
||||||
// animations are just strange and useless, since the suggestions keep changing too much
|
// animations are just strange and useless, since the suggestions keep changing too much
|
||||||
searchBinding.suggestionsList.setItemAnimator(null);
|
searchBinding.suggestionsList.setItemAnimator(null);
|
||||||
@@ -389,7 +397,7 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
|||||||
@Override
|
@Override
|
||||||
public void onSaveInstanceState(@NonNull final Bundle bundle) {
|
public void onSaveInstanceState(@NonNull final Bundle bundle) {
|
||||||
searchString = searchEditText != null
|
searchString = searchEditText != null
|
||||||
? searchEditText.getText().toString()
|
? getSearchEditString().trim()
|
||||||
: searchString;
|
: searchString;
|
||||||
super.onSaveInstanceState(bundle);
|
super.onSaveInstanceState(bundle);
|
||||||
}
|
}
|
||||||
@@ -400,11 +408,11 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void reloadContent() {
|
public void reloadContent() {
|
||||||
if (!TextUtils.isEmpty(searchString)
|
if (!TextUtils.isEmpty(searchString) || (searchEditText != null
|
||||||
|| (searchEditText != null && !TextUtils.isEmpty(searchEditText.getText()))) {
|
&& !isSearchEditBlank())) {
|
||||||
search(!TextUtils.isEmpty(searchString)
|
search(!TextUtils.isEmpty(searchString)
|
||||||
? searchString
|
? searchString
|
||||||
: searchEditText.getText().toString(), this.contentFilter, "");
|
: getSearchEditString(), this.contentFilter, "");
|
||||||
} else {
|
} else {
|
||||||
if (searchEditText != null) {
|
if (searchEditText != null) {
|
||||||
searchEditText.setText("");
|
searchEditText.setText("");
|
||||||
@@ -498,7 +506,8 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
|||||||
}
|
}
|
||||||
searchEditText.setText(searchString);
|
searchEditText.setText(searchString);
|
||||||
|
|
||||||
if (TextUtils.isEmpty(searchString) || TextUtils.isEmpty(searchEditText.getText())) {
|
if (TextUtils.isEmpty(searchString)
|
||||||
|
|| isSearchEditBlank()) {
|
||||||
searchToolbarContainer.setTranslationX(100);
|
searchToolbarContainer.setTranslationX(100);
|
||||||
searchToolbarContainer.setAlpha(0.0f);
|
searchToolbarContainer.setAlpha(0.0f);
|
||||||
searchToolbarContainer.setVisibility(View.VISIBLE);
|
searchToolbarContainer.setVisibility(View.VISIBLE);
|
||||||
@@ -522,7 +531,7 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
|||||||
if (DEBUG) {
|
if (DEBUG) {
|
||||||
Log.d(TAG, "onClick() called with: v = [" + v + "]");
|
Log.d(TAG, "onClick() called with: v = [" + v + "]");
|
||||||
}
|
}
|
||||||
if (TextUtils.isEmpty(searchEditText.getText())) {
|
if (isSearchEditBlank()) {
|
||||||
NavigationHelper.gotoMainFragment(getFM());
|
NavigationHelper.gotoMainFragment(getFM());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -548,7 +557,7 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
searchEditText.setOnFocusChangeListener((View v, boolean hasFocus) -> {
|
searchEditText.setOnFocusChangeListener((final View v, final boolean hasFocus) -> {
|
||||||
if (DEBUG) {
|
if (DEBUG) {
|
||||||
Log.d(TAG, "onFocusChange() called with: "
|
Log.d(TAG, "onFocusChange() called with: "
|
||||||
+ "v = [" + v + "], hasFocus = [" + hasFocus + "]");
|
+ "v = [" + v + "], hasFocus = [" + hasFocus + "]");
|
||||||
@@ -603,13 +612,13 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
|||||||
s.removeSpan(span);
|
s.removeSpan(span);
|
||||||
}
|
}
|
||||||
|
|
||||||
final String newText = searchEditText.getText().toString();
|
final String newText = getSearchEditString().trim();
|
||||||
suggestionPublisher.onNext(newText);
|
suggestionPublisher.onNext(newText);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
searchEditText.addTextChangedListener(textWatcher);
|
searchEditText.addTextChangedListener(textWatcher);
|
||||||
searchEditText.setOnEditorActionListener(
|
searchEditText.setOnEditorActionListener(
|
||||||
(TextView v, int actionId, KeyEvent event) -> {
|
(final TextView v, final int actionId, final KeyEvent event) -> {
|
||||||
if (DEBUG) {
|
if (DEBUG) {
|
||||||
Log.d(TAG, "onEditorAction() called with: v = [" + v + "], "
|
Log.d(TAG, "onEditorAction() called with: v = [" + v + "], "
|
||||||
+ "actionId = [" + actionId + "], event = [" + event + "]");
|
+ "actionId = [" + actionId + "], event = [" + event + "]");
|
||||||
@@ -619,7 +628,8 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
|||||||
} else if (event != null
|
} else if (event != null
|
||||||
&& (event.getKeyCode() == KeyEvent.KEYCODE_ENTER
|
&& (event.getKeyCode() == KeyEvent.KEYCODE_ENTER
|
||||||
|| event.getAction() == EditorInfo.IME_ACTION_SEARCH)) {
|
|| event.getAction() == EditorInfo.IME_ACTION_SEARCH)) {
|
||||||
search(searchEditText.getText().toString(), new String[0], "");
|
searchEditText.setText(getSearchEditString().trim());
|
||||||
|
search(getSearchEditString(), new String[0], "");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
@@ -694,7 +704,7 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
|||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.subscribe(
|
.subscribe(
|
||||||
howManyDeleted -> suggestionPublisher
|
howManyDeleted -> suggestionPublisher
|
||||||
.onNext(searchEditText.getText().toString()),
|
.onNext(getSearchEditString()),
|
||||||
throwable -> showSnackBarError(new ErrorInfo(throwable,
|
throwable -> showSnackBarError(new ErrorInfo(throwable,
|
||||||
UserAction.DELETE_FROM_HISTORY,
|
UserAction.DELETE_FROM_HISTORY,
|
||||||
"Deleting item failed")));
|
"Deleting item failed")));
|
||||||
@@ -723,9 +733,9 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
|||||||
.getRelatedSearches(query, similarQueryLimit, 25)
|
.getRelatedSearches(query, similarQueryLimit, 25)
|
||||||
.toObservable()
|
.toObservable()
|
||||||
.map(searchHistoryEntries ->
|
.map(searchHistoryEntries ->
|
||||||
searchHistoryEntries.stream()
|
searchHistoryEntries.stream()
|
||||||
.map(entry -> new SuggestionItem(true, entry))
|
.map(entry -> new SuggestionItem(true, entry))
|
||||||
.collect(Collectors.toList()));
|
.collect(Collectors.toList()));
|
||||||
}
|
}
|
||||||
|
|
||||||
private Observable<List<SuggestionItem>> getRemoteSuggestionsObservable(final String query) {
|
private Observable<List<SuggestionItem>> getRemoteSuggestionsObservable(final String query) {
|
||||||
@@ -792,12 +802,12 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
|||||||
} else if (listNotification.isOnError()
|
} else if (listNotification.isOnError()
|
||||||
&& listNotification.getError() != null
|
&& listNotification.getError() != null
|
||||||
&& !ExceptionUtils.isInterruptedCaused(
|
&& !ExceptionUtils.isInterruptedCaused(
|
||||||
listNotification.getError())) {
|
listNotification.getError())) {
|
||||||
showSnackBarError(new ErrorInfo(listNotification.getError(),
|
showSnackBarError(new ErrorInfo(listNotification.getError(),
|
||||||
UserAction.GET_SUGGESTIONS, searchString, serviceId));
|
UserAction.GET_SUGGESTIONS, searchString, serviceId));
|
||||||
}
|
}
|
||||||
}, throwable -> showSnackBarError(new ErrorInfo(
|
}, throwable -> showSnackBarError(new ErrorInfo(
|
||||||
throwable, UserAction.GET_SUGGESTIONS, searchString, serviceId)));
|
throwable, UserAction.GET_SUGGESTIONS, searchString, serviceId)));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -805,7 +815,13 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
|||||||
// no-op
|
// no-op
|
||||||
}
|
}
|
||||||
|
|
||||||
private void search(final String theSearchString,
|
/**
|
||||||
|
* Perform a search.
|
||||||
|
* @param theSearchString the trimmed search string
|
||||||
|
* @param theContentFilter the content filter to use. FIXME: unused param
|
||||||
|
* @param theSortFilter FIXME: unused param
|
||||||
|
*/
|
||||||
|
private void search(@NonNull final String theSearchString,
|
||||||
final String[] theContentFilter,
|
final String[] theContentFilter,
|
||||||
final String theSortFilter) {
|
final String theSortFilter) {
|
||||||
if (DEBUG) {
|
if (DEBUG) {
|
||||||
@@ -815,25 +831,26 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if theSearchString is a URL which can be opened by NewPipe directly
|
||||||
|
// and open it if possible.
|
||||||
try {
|
try {
|
||||||
final StreamingService streamingService = NewPipe.getServiceByUrl(theSearchString);
|
final StreamingService streamingService = NewPipe.getServiceByUrl(theSearchString);
|
||||||
if (streamingService != null) {
|
showLoading();
|
||||||
showLoading();
|
disposables.add(Observable
|
||||||
disposables.add(Observable
|
.fromCallable(() -> NavigationHelper.getIntentByLink(activity,
|
||||||
.fromCallable(() -> NavigationHelper.getIntentByLink(activity,
|
streamingService, theSearchString))
|
||||||
streamingService, theSearchString))
|
.subscribeOn(Schedulers.io())
|
||||||
.subscribeOn(Schedulers.io())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.subscribe(intent -> {
|
||||||
.subscribe(intent -> {
|
getFM().popBackStackImmediate();
|
||||||
getFM().popBackStackImmediate();
|
activity.startActivity(intent);
|
||||||
activity.startActivity(intent);
|
}, throwable -> showTextError(getString(R.string.unsupported_url))));
|
||||||
}, throwable -> showTextError(getString(R.string.unsupported_url))));
|
return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
} catch (final Exception ignored) {
|
} catch (final Exception ignored) {
|
||||||
// Exception occurred, it's not a url
|
// Exception occurred, it's not a url
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// prepare search
|
||||||
lastSearchedString = this.searchString;
|
lastSearchedString = this.searchString;
|
||||||
this.searchString = theSearchString;
|
this.searchString = theSearchString;
|
||||||
infoListAdapter.clearStreamItemList();
|
infoListAdapter.clearStreamItemList();
|
||||||
@@ -842,13 +859,17 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
|||||||
searchBinding.searchMetaInfoSeparator, disposables);
|
searchBinding.searchMetaInfoSeparator, disposables);
|
||||||
hideKeyboardSearch();
|
hideKeyboardSearch();
|
||||||
|
|
||||||
|
// store search query if search history is enabled
|
||||||
disposables.add(historyRecordManager.onSearched(serviceId, theSearchString)
|
disposables.add(historyRecordManager.onSearched(serviceId, theSearchString)
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.subscribe(
|
.subscribe(
|
||||||
ignored -> { },
|
ignored -> {
|
||||||
|
},
|
||||||
throwable -> showSnackBarError(new ErrorInfo(throwable, UserAction.SEARCHED,
|
throwable -> showSnackBarError(new ErrorInfo(throwable, UserAction.SEARCHED,
|
||||||
theSearchString, serviceId))
|
theSearchString, serviceId))
|
||||||
));
|
));
|
||||||
|
|
||||||
|
// load search results
|
||||||
suggestionPublisher.onNext(theSearchString);
|
suggestionPublisher.onNext(theSearchString);
|
||||||
startLoading(false);
|
startLoading(false);
|
||||||
}
|
}
|
||||||
@@ -938,6 +959,14 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
|||||||
sortFilter = theSortFilter;
|
sortFilter = theSortFilter;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String getSearchEditString() {
|
||||||
|
return searchEditText.getText().toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isSearchEditBlank() {
|
||||||
|
return isBlank(getSearchEditString());
|
||||||
|
}
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
// Suggestion Results
|
// Suggestion Results
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
@@ -979,6 +1008,9 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
|||||||
}
|
}
|
||||||
|
|
||||||
searchSuggestion = result.getSearchSuggestion();
|
searchSuggestion = result.getSearchSuggestion();
|
||||||
|
if (searchSuggestion != null) {
|
||||||
|
searchSuggestion = searchSuggestion.trim();
|
||||||
|
}
|
||||||
isCorrectedSearch = result.isCorrectedSearch();
|
isCorrectedSearch = result.isCorrectedSearch();
|
||||||
|
|
||||||
// List<MetaInfo> cannot be bundled without creating some containers
|
// List<MetaInfo> cannot be bundled without creating some containers
|
||||||
@@ -1080,7 +1112,7 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
|||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.subscribe(
|
.subscribe(
|
||||||
howManyDeleted -> suggestionPublisher
|
howManyDeleted -> suggestionPublisher
|
||||||
.onNext(searchEditText.getText().toString()),
|
.onNext(getSearchEditString()),
|
||||||
throwable -> showSnackBarError(new ErrorInfo(throwable,
|
throwable -> showSnackBarError(new ErrorInfo(throwable,
|
||||||
UserAction.DELETE_FROM_HISTORY, "Deleting item failed")));
|
UserAction.DELETE_FROM_HISTORY, "Deleting item failed")));
|
||||||
disposables.add(onDelete);
|
disposables.add(onDelete);
|
||||||
|
|||||||
@@ -1,177 +0,0 @@
|
|||||||
package org.schabi.newpipe.fragments.list.videos;
|
|
||||||
|
|
||||||
import android.content.SharedPreferences;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.view.LayoutInflater;
|
|
||||||
import android.view.Menu;
|
|
||||||
import android.view.MenuInflater;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.ViewGroup;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import androidx.preference.PreferenceManager;
|
|
||||||
|
|
||||||
import org.schabi.newpipe.R;
|
|
||||||
import org.schabi.newpipe.databinding.RelatedItemsHeaderBinding;
|
|
||||||
import org.schabi.newpipe.error.UserAction;
|
|
||||||
import org.schabi.newpipe.extractor.InfoItem;
|
|
||||||
import org.schabi.newpipe.extractor.ListExtractor;
|
|
||||||
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
|
||||||
import org.schabi.newpipe.fragments.list.BaseListInfoFragment;
|
|
||||||
import org.schabi.newpipe.info_list.ItemViewMode;
|
|
||||||
import org.schabi.newpipe.ktx.ViewUtils;
|
|
||||||
import org.schabi.newpipe.util.RelatedItemInfo;
|
|
||||||
|
|
||||||
import java.io.Serializable;
|
|
||||||
import java.util.function.Supplier;
|
|
||||||
|
|
||||||
import io.reactivex.rxjava3.core.Single;
|
|
||||||
|
|
||||||
public class RelatedItemsFragment extends BaseListInfoFragment<InfoItem, RelatedItemInfo>
|
|
||||||
implements SharedPreferences.OnSharedPreferenceChangeListener {
|
|
||||||
private static final String INFO_KEY = "related_info_key";
|
|
||||||
|
|
||||||
private RelatedItemInfo relatedItemInfo;
|
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
|
||||||
// Views
|
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
|
||||||
|
|
||||||
private RelatedItemsHeaderBinding headerBinding;
|
|
||||||
|
|
||||||
public static RelatedItemsFragment getInstance(final StreamInfo info) {
|
|
||||||
final RelatedItemsFragment instance = new RelatedItemsFragment();
|
|
||||||
instance.setInitialData(info);
|
|
||||||
return instance;
|
|
||||||
}
|
|
||||||
|
|
||||||
public RelatedItemsFragment() {
|
|
||||||
super(UserAction.REQUESTED_STREAM);
|
|
||||||
}
|
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
|
||||||
// LifeCycle
|
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public View onCreateView(@NonNull final LayoutInflater inflater,
|
|
||||||
@Nullable final ViewGroup container,
|
|
||||||
@Nullable final Bundle savedInstanceState) {
|
|
||||||
return inflater.inflate(R.layout.fragment_related_items, container, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onDestroyView() {
|
|
||||||
headerBinding = null;
|
|
||||||
super.onDestroyView();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected Supplier<View> getListHeaderSupplier() {
|
|
||||||
if (relatedItemInfo == null || relatedItemInfo.getRelatedItems() == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
headerBinding = RelatedItemsHeaderBinding
|
|
||||||
.inflate(activity.getLayoutInflater(), itemsList, false);
|
|
||||||
|
|
||||||
final SharedPreferences pref = PreferenceManager
|
|
||||||
.getDefaultSharedPreferences(requireContext());
|
|
||||||
final boolean autoplay = pref.getBoolean(getString(R.string.auto_queue_key), false);
|
|
||||||
headerBinding.autoplaySwitch.setChecked(autoplay);
|
|
||||||
headerBinding.autoplaySwitch.setOnCheckedChangeListener((compoundButton, b) ->
|
|
||||||
PreferenceManager.getDefaultSharedPreferences(requireContext()).edit()
|
|
||||||
.putBoolean(getString(R.string.auto_queue_key), b).apply());
|
|
||||||
|
|
||||||
return headerBinding::getRoot;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected Single<ListExtractor.InfoItemsPage<InfoItem>> loadMoreItemsLogic() {
|
|
||||||
return Single.fromCallable(ListExtractor.InfoItemsPage::emptyPage);
|
|
||||||
}
|
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
|
||||||
// Contract
|
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected Single<RelatedItemInfo> loadResult(final boolean forceLoad) {
|
|
||||||
return Single.fromCallable(() -> relatedItemInfo);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void showLoading() {
|
|
||||||
super.showLoading();
|
|
||||||
if (headerBinding != null) {
|
|
||||||
headerBinding.getRoot().setVisibility(View.INVISIBLE);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void handleResult(@NonNull final RelatedItemInfo result) {
|
|
||||||
super.handleResult(result);
|
|
||||||
|
|
||||||
if (headerBinding != null) {
|
|
||||||
headerBinding.getRoot().setVisibility(View.VISIBLE);
|
|
||||||
}
|
|
||||||
ViewUtils.slideUp(requireView(), 120, 96, 0.06f);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
|
||||||
// Utils
|
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void setTitle(final String title) {
|
|
||||||
// Nothing to do - override parent
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onCreateOptionsMenu(@NonNull final Menu menu,
|
|
||||||
@NonNull final MenuInflater inflater) {
|
|
||||||
// Nothing to do - override parent
|
|
||||||
}
|
|
||||||
|
|
||||||
private void setInitialData(final StreamInfo info) {
|
|
||||||
super.setInitialData(info.getServiceId(), info.getUrl(), info.getName());
|
|
||||||
if (this.relatedItemInfo == null) {
|
|
||||||
this.relatedItemInfo = RelatedItemInfo.getInfo(info);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onSaveInstanceState(@NonNull final Bundle outState) {
|
|
||||||
super.onSaveInstanceState(outState);
|
|
||||||
outState.putSerializable(INFO_KEY, relatedItemInfo);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onRestoreInstanceState(@NonNull final Bundle savedState) {
|
|
||||||
super.onRestoreInstanceState(savedState);
|
|
||||||
final Serializable serializable = savedState.getSerializable(INFO_KEY);
|
|
||||||
if (serializable instanceof RelatedItemInfo) {
|
|
||||||
this.relatedItemInfo = (RelatedItemInfo) serializable;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onSharedPreferenceChanged(final SharedPreferences sharedPreferences,
|
|
||||||
final String key) {
|
|
||||||
if (headerBinding != null && getString(R.string.auto_queue_key).equals(key)) {
|
|
||||||
headerBinding.autoplaySwitch.setChecked(sharedPreferences.getBoolean(key, false));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected ItemViewMode getItemViewMode() {
|
|
||||||
ItemViewMode mode = super.getItemViewMode();
|
|
||||||
// Only list mode is supported. Either List or card will be used.
|
|
||||||
if (mode != ItemViewMode.LIST && mode != ItemViewMode.CARD) {
|
|
||||||
mode = ItemViewMode.LIST;
|
|
||||||
}
|
|
||||||
return mode;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
package org.schabi.newpipe.fragments.list.videos
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.compose.material3.Surface
|
||||||
|
import androidx.core.os.bundleOf
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import androidx.fragment.compose.content
|
||||||
|
import org.schabi.newpipe.extractor.stream.StreamInfo
|
||||||
|
import org.schabi.newpipe.ktx.serializable
|
||||||
|
import org.schabi.newpipe.ui.components.video.RelatedItems
|
||||||
|
import org.schabi.newpipe.ui.theme.AppTheme
|
||||||
|
import org.schabi.newpipe.util.KEY_INFO
|
||||||
|
|
||||||
|
class RelatedItemsFragment : Fragment() {
|
||||||
|
override fun onCreateView(
|
||||||
|
inflater: LayoutInflater,
|
||||||
|
container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?
|
||||||
|
) = content {
|
||||||
|
AppTheme {
|
||||||
|
Surface {
|
||||||
|
RelatedItems(requireArguments().serializable<StreamInfo>(KEY_INFO)!!)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
@JvmStatic
|
||||||
|
fun getInstance(info: StreamInfo) = RelatedItemsFragment().apply {
|
||||||
|
arguments = bundleOf(KEY_INFO to info)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,8 +13,6 @@ import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem;
|
|||||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||||
import org.schabi.newpipe.info_list.holder.ChannelInfoItemHolder;
|
import org.schabi.newpipe.info_list.holder.ChannelInfoItemHolder;
|
||||||
import org.schabi.newpipe.info_list.holder.ChannelMiniInfoItemHolder;
|
import org.schabi.newpipe.info_list.holder.ChannelMiniInfoItemHolder;
|
||||||
import org.schabi.newpipe.info_list.holder.CommentsInfoItemHolder;
|
|
||||||
import org.schabi.newpipe.info_list.holder.CommentsMiniInfoItemHolder;
|
|
||||||
import org.schabi.newpipe.info_list.holder.InfoItemHolder;
|
import org.schabi.newpipe.info_list.holder.InfoItemHolder;
|
||||||
import org.schabi.newpipe.info_list.holder.PlaylistInfoItemHolder;
|
import org.schabi.newpipe.info_list.holder.PlaylistInfoItemHolder;
|
||||||
import org.schabi.newpipe.info_list.holder.PlaylistMiniInfoItemHolder;
|
import org.schabi.newpipe.info_list.holder.PlaylistMiniInfoItemHolder;
|
||||||
@@ -76,22 +74,16 @@ public class InfoItemBuilder {
|
|||||||
private InfoItemHolder holderFromInfoType(@NonNull final ViewGroup parent,
|
private InfoItemHolder holderFromInfoType(@NonNull final ViewGroup parent,
|
||||||
@NonNull final InfoItem.InfoType infoType,
|
@NonNull final InfoItem.InfoType infoType,
|
||||||
final boolean useMiniVariant) {
|
final boolean useMiniVariant) {
|
||||||
switch (infoType) {
|
return switch (infoType) {
|
||||||
case STREAM:
|
case STREAM -> useMiniVariant ? new StreamMiniInfoItemHolder(this, parent)
|
||||||
return useMiniVariant ? new StreamMiniInfoItemHolder(this, parent)
|
: new StreamInfoItemHolder(this, parent);
|
||||||
: new StreamInfoItemHolder(this, parent);
|
case CHANNEL -> useMiniVariant ? new ChannelMiniInfoItemHolder(this, parent)
|
||||||
case CHANNEL:
|
: new ChannelInfoItemHolder(this, parent);
|
||||||
return useMiniVariant ? new ChannelMiniInfoItemHolder(this, parent)
|
case PLAYLIST -> useMiniVariant ? new PlaylistMiniInfoItemHolder(this, parent)
|
||||||
: new ChannelInfoItemHolder(this, parent);
|
: new PlaylistInfoItemHolder(this, parent);
|
||||||
case PLAYLIST:
|
case COMMENT ->
|
||||||
return useMiniVariant ? new PlaylistMiniInfoItemHolder(this, parent)
|
throw new IllegalArgumentException("Comments should be rendered using Compose");
|
||||||
: new PlaylistInfoItemHolder(this, parent);
|
};
|
||||||
case COMMENT:
|
|
||||||
return useMiniVariant ? new CommentsMiniInfoItemHolder(this, parent)
|
|
||||||
: new CommentsInfoItemHolder(this, parent);
|
|
||||||
default:
|
|
||||||
throw new RuntimeException("InfoType not expected = " + infoType.name());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public Context getContext() {
|
public Context getContext() {
|
||||||
|
|||||||
@@ -21,8 +21,6 @@ import org.schabi.newpipe.info_list.holder.ChannelCardInfoItemHolder;
|
|||||||
import org.schabi.newpipe.info_list.holder.ChannelGridInfoItemHolder;
|
import org.schabi.newpipe.info_list.holder.ChannelGridInfoItemHolder;
|
||||||
import org.schabi.newpipe.info_list.holder.ChannelInfoItemHolder;
|
import org.schabi.newpipe.info_list.holder.ChannelInfoItemHolder;
|
||||||
import org.schabi.newpipe.info_list.holder.ChannelMiniInfoItemHolder;
|
import org.schabi.newpipe.info_list.holder.ChannelMiniInfoItemHolder;
|
||||||
import org.schabi.newpipe.info_list.holder.CommentsInfoItemHolder;
|
|
||||||
import org.schabi.newpipe.info_list.holder.CommentsMiniInfoItemHolder;
|
|
||||||
import org.schabi.newpipe.info_list.holder.InfoItemHolder;
|
import org.schabi.newpipe.info_list.holder.InfoItemHolder;
|
||||||
import org.schabi.newpipe.info_list.holder.PlaylistCardInfoItemHolder;
|
import org.schabi.newpipe.info_list.holder.PlaylistCardInfoItemHolder;
|
||||||
import org.schabi.newpipe.info_list.holder.PlaylistGridInfoItemHolder;
|
import org.schabi.newpipe.info_list.holder.PlaylistGridInfoItemHolder;
|
||||||
@@ -79,8 +77,7 @@ public class InfoListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolde
|
|||||||
private static final int PLAYLIST_HOLDER_TYPE = 0x301;
|
private static final int PLAYLIST_HOLDER_TYPE = 0x301;
|
||||||
private static final int GRID_PLAYLIST_HOLDER_TYPE = 0x302;
|
private static final int GRID_PLAYLIST_HOLDER_TYPE = 0x302;
|
||||||
private static final int CARD_PLAYLIST_HOLDER_TYPE = 0x303;
|
private static final int CARD_PLAYLIST_HOLDER_TYPE = 0x303;
|
||||||
private static final int MINI_COMMENT_HOLDER_TYPE = 0x400;
|
private static final int COMMENT_HOLDER_TYPE = 0x400;
|
||||||
private static final int COMMENT_HOLDER_TYPE = 0x401;
|
|
||||||
|
|
||||||
private final LayoutInflater layoutInflater;
|
private final LayoutInflater layoutInflater;
|
||||||
private final InfoItemBuilder infoItemBuilder;
|
private final InfoItemBuilder infoItemBuilder;
|
||||||
@@ -271,7 +268,7 @@ public class InfoListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolde
|
|||||||
return PLAYLIST_HOLDER_TYPE;
|
return PLAYLIST_HOLDER_TYPE;
|
||||||
}
|
}
|
||||||
case COMMENT:
|
case COMMENT:
|
||||||
return useMiniVariant ? MINI_COMMENT_HOLDER_TYPE : COMMENT_HOLDER_TYPE;
|
return COMMENT_HOLDER_TYPE;
|
||||||
default:
|
default:
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
@@ -285,48 +282,32 @@ public class InfoListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolde
|
|||||||
Log.d(TAG, "onCreateViewHolder() called with: "
|
Log.d(TAG, "onCreateViewHolder() called with: "
|
||||||
+ "parent = [" + parent + "], type = [" + type + "]");
|
+ "parent = [" + parent + "], type = [" + type + "]");
|
||||||
}
|
}
|
||||||
switch (type) {
|
return switch (type) {
|
||||||
// #4475 and #3368
|
// #4475 and #3368
|
||||||
// Always create a new instance otherwise the same instance
|
// Always create a new instance otherwise the same instance
|
||||||
// is sometimes reused which causes a crash
|
// is sometimes reused which causes a crash
|
||||||
case HEADER_TYPE:
|
case HEADER_TYPE -> new HFHolder(headerSupplier.get());
|
||||||
return new HFHolder(headerSupplier.get());
|
case FOOTER_TYPE -> new HFHolder(PignateFooterBinding
|
||||||
case FOOTER_TYPE:
|
.inflate(layoutInflater, parent, false)
|
||||||
return new HFHolder(PignateFooterBinding
|
.getRoot()
|
||||||
.inflate(layoutInflater, parent, false)
|
);
|
||||||
.getRoot()
|
case MINI_STREAM_HOLDER_TYPE -> new StreamMiniInfoItemHolder(infoItemBuilder, parent);
|
||||||
);
|
case STREAM_HOLDER_TYPE -> new StreamInfoItemHolder(infoItemBuilder, parent);
|
||||||
case MINI_STREAM_HOLDER_TYPE:
|
case GRID_STREAM_HOLDER_TYPE -> new StreamGridInfoItemHolder(infoItemBuilder, parent);
|
||||||
return new StreamMiniInfoItemHolder(infoItemBuilder, parent);
|
case CARD_STREAM_HOLDER_TYPE -> new StreamCardInfoItemHolder(infoItemBuilder, parent);
|
||||||
case STREAM_HOLDER_TYPE:
|
case MINI_CHANNEL_HOLDER_TYPE -> new ChannelMiniInfoItemHolder(infoItemBuilder, parent);
|
||||||
return new StreamInfoItemHolder(infoItemBuilder, parent);
|
case CHANNEL_HOLDER_TYPE -> new ChannelInfoItemHolder(infoItemBuilder, parent);
|
||||||
case GRID_STREAM_HOLDER_TYPE:
|
case CARD_CHANNEL_HOLDER_TYPE -> new ChannelCardInfoItemHolder(infoItemBuilder, parent);
|
||||||
return new StreamGridInfoItemHolder(infoItemBuilder, parent);
|
case GRID_CHANNEL_HOLDER_TYPE -> new ChannelGridInfoItemHolder(infoItemBuilder, parent);
|
||||||
case CARD_STREAM_HOLDER_TYPE:
|
case MINI_PLAYLIST_HOLDER_TYPE ->
|
||||||
return new StreamCardInfoItemHolder(infoItemBuilder, parent);
|
new PlaylistMiniInfoItemHolder(infoItemBuilder, parent);
|
||||||
case MINI_CHANNEL_HOLDER_TYPE:
|
case PLAYLIST_HOLDER_TYPE -> new PlaylistInfoItemHolder(infoItemBuilder, parent);
|
||||||
return new ChannelMiniInfoItemHolder(infoItemBuilder, parent);
|
case GRID_PLAYLIST_HOLDER_TYPE ->
|
||||||
case CHANNEL_HOLDER_TYPE:
|
new PlaylistGridInfoItemHolder(infoItemBuilder, parent);
|
||||||
return new ChannelInfoItemHolder(infoItemBuilder, parent);
|
case CARD_PLAYLIST_HOLDER_TYPE ->
|
||||||
case CARD_CHANNEL_HOLDER_TYPE:
|
new PlaylistCardInfoItemHolder(infoItemBuilder, parent);
|
||||||
return new ChannelCardInfoItemHolder(infoItemBuilder, parent);
|
default -> new FallbackViewHolder(new View(parent.getContext()));
|
||||||
case GRID_CHANNEL_HOLDER_TYPE:
|
};
|
||||||
return new ChannelGridInfoItemHolder(infoItemBuilder, parent);
|
|
||||||
case MINI_PLAYLIST_HOLDER_TYPE:
|
|
||||||
return new PlaylistMiniInfoItemHolder(infoItemBuilder, parent);
|
|
||||||
case PLAYLIST_HOLDER_TYPE:
|
|
||||||
return new PlaylistInfoItemHolder(infoItemBuilder, parent);
|
|
||||||
case GRID_PLAYLIST_HOLDER_TYPE:
|
|
||||||
return new PlaylistGridInfoItemHolder(infoItemBuilder, parent);
|
|
||||||
case CARD_PLAYLIST_HOLDER_TYPE:
|
|
||||||
return new PlaylistCardInfoItemHolder(infoItemBuilder, parent);
|
|
||||||
case MINI_COMMENT_HOLDER_TYPE:
|
|
||||||
return new CommentsMiniInfoItemHolder(infoItemBuilder, parent);
|
|
||||||
case COMMENT_HOLDER_TYPE:
|
|
||||||
return new CommentsInfoItemHolder(infoItemBuilder, parent);
|
|
||||||
default:
|
|
||||||
return new FallbackViewHolder(new View(parent.getContext()));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -1,19 +1,18 @@
|
|||||||
package org.schabi.newpipe.info_list
|
package org.schabi.newpipe.info_list
|
||||||
|
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.ImageView
|
import com.xwray.groupie.viewbinding.BindableItem
|
||||||
import android.widget.TextView
|
import com.xwray.groupie.viewbinding.GroupieViewHolder
|
||||||
import com.xwray.groupie.GroupieViewHolder
|
|
||||||
import com.xwray.groupie.Item
|
|
||||||
import org.schabi.newpipe.R
|
import org.schabi.newpipe.R
|
||||||
|
import org.schabi.newpipe.databinding.ItemStreamSegmentBinding
|
||||||
import org.schabi.newpipe.extractor.stream.StreamSegment
|
import org.schabi.newpipe.extractor.stream.StreamSegment
|
||||||
import org.schabi.newpipe.util.Localization
|
import org.schabi.newpipe.util.Localization
|
||||||
import org.schabi.newpipe.util.image.PicassoHelper
|
import org.schabi.newpipe.util.image.CoilHelper
|
||||||
|
|
||||||
class StreamSegmentItem(
|
class StreamSegmentItem(
|
||||||
private val item: StreamSegment,
|
private val item: StreamSegment,
|
||||||
private val onClick: StreamSegmentAdapter.StreamSegmentListener
|
private val onClick: StreamSegmentAdapter.StreamSegmentListener
|
||||||
) : Item<GroupieViewHolder>() {
|
) : BindableItem<ItemStreamSegmentBinding>() {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val PAYLOAD_SELECT = 1
|
const val PAYLOAD_SELECT = 1
|
||||||
@@ -21,31 +20,32 @@ class StreamSegmentItem(
|
|||||||
|
|
||||||
var isSelected = false
|
var isSelected = false
|
||||||
|
|
||||||
override fun bind(viewHolder: GroupieViewHolder, position: Int) {
|
override fun bind(viewBinding: ItemStreamSegmentBinding, position: Int) {
|
||||||
item.previewUrl?.let {
|
CoilHelper.loadThumbnail(viewBinding.previewImage, item.previewUrl)
|
||||||
PicassoHelper.loadThumbnail(it)
|
viewBinding.textViewTitle.text = item.title
|
||||||
.into(viewHolder.root.findViewById<ImageView>(R.id.previewImage))
|
|
||||||
}
|
|
||||||
viewHolder.root.findViewById<TextView>(R.id.textViewTitle).text = item.title
|
|
||||||
if (item.channelName == null) {
|
if (item.channelName == null) {
|
||||||
viewHolder.root.findViewById<TextView>(R.id.textViewChannel).visibility = View.GONE
|
viewBinding.textViewChannel.visibility = View.GONE
|
||||||
// When the channel name is displayed there is less space
|
// When the channel name is displayed there is less space
|
||||||
// and thus the segment title needs to be only one line height.
|
// and thus the segment title needs to be only one line height.
|
||||||
// But when there is no channel name displayed, the title can be two lines long.
|
// But when there is no channel name displayed, the title can be two lines long.
|
||||||
// The default maxLines value is set to 1 to display all elements in the AS preview,
|
// The default maxLines value is set to 1 to display all elements in the AS preview,
|
||||||
viewHolder.root.findViewById<TextView>(R.id.textViewTitle).maxLines = 2
|
viewBinding.textViewTitle.maxLines = 2
|
||||||
} else {
|
} else {
|
||||||
viewHolder.root.findViewById<TextView>(R.id.textViewChannel).text = item.channelName
|
viewBinding.textViewChannel.text = item.channelName
|
||||||
viewHolder.root.findViewById<TextView>(R.id.textViewChannel).visibility = View.VISIBLE
|
viewBinding.textViewChannel.visibility = View.VISIBLE
|
||||||
}
|
}
|
||||||
viewHolder.root.findViewById<TextView>(R.id.textViewStartSeconds).text =
|
viewBinding.textViewStartSeconds.text =
|
||||||
Localization.getDurationString(item.startTimeSeconds.toLong())
|
Localization.getDurationString(item.startTimeSeconds.toLong())
|
||||||
viewHolder.root.setOnClickListener { onClick.onItemClick(this, item.startTimeSeconds) }
|
viewBinding.root.setOnClickListener { onClick.onItemClick(this, item.startTimeSeconds) }
|
||||||
viewHolder.root.setOnLongClickListener { onClick.onItemLongClick(this, item.startTimeSeconds); true }
|
viewBinding.root.setOnLongClickListener { onClick.onItemLongClick(this, item.startTimeSeconds); true }
|
||||||
viewHolder.root.isSelected = isSelected
|
viewBinding.root.isSelected = isSelected
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun bind(viewHolder: GroupieViewHolder, position: Int, payloads: MutableList<Any>) {
|
override fun bind(
|
||||||
|
viewHolder: GroupieViewHolder<ItemStreamSegmentBinding>,
|
||||||
|
position: Int,
|
||||||
|
payloads: MutableList<Any>
|
||||||
|
) {
|
||||||
if (payloads.contains(PAYLOAD_SELECT)) {
|
if (payloads.contains(PAYLOAD_SELECT)) {
|
||||||
viewHolder.root.isSelected = isSelected
|
viewHolder.root.isSelected = isSelected
|
||||||
return
|
return
|
||||||
@@ -54,4 +54,6 @@ class StreamSegmentItem(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun getLayout() = R.layout.item_stream_segment
|
override fun getLayout() = R.layout.item_stream_segment
|
||||||
|
|
||||||
|
override fun initializeViewBinding(view: View) = ItemStreamSegmentBinding.bind(view)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -346,7 +346,7 @@ public final class InfoItemDialog {
|
|||||||
|
|
||||||
public static void reportErrorDuringInitialization(final Throwable throwable,
|
public static void reportErrorDuringInitialization(final Throwable throwable,
|
||||||
final InfoItem item) {
|
final InfoItem item) {
|
||||||
ErrorUtil.showSnackbar(App.getApp().getBaseContext(), new ErrorInfo(
|
ErrorUtil.showSnackbar(App.getInstance().getBaseContext(), new ErrorInfo(
|
||||||
throwable,
|
throwable,
|
||||||
UserAction.OPEN_INFO_ITEM_DIALOG,
|
UserAction.OPEN_INFO_ITEM_DIALOG,
|
||||||
"none",
|
"none",
|
||||||
|
|||||||
@@ -41,10 +41,11 @@ import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
|||||||
* </p>
|
* </p>
|
||||||
*/
|
*/
|
||||||
public enum StreamDialogDefaultEntry {
|
public enum StreamDialogDefaultEntry {
|
||||||
SHOW_CHANNEL_DETAILS(R.string.show_channel_details, (fragment, item) ->
|
SHOW_CHANNEL_DETAILS(R.string.show_channel_details, (fragment, item) -> {
|
||||||
fetchUploaderUrlIfSparse(fragment.requireContext(), item.getServiceId(), item.getUrl(),
|
final var activity = fragment.requireActivity();
|
||||||
item.getUploaderUrl(), url -> openChannelFragment(fragment, item, url))
|
fetchUploaderUrlIfSparse(activity, item.getServiceId(), item.getUrl(),
|
||||||
),
|
item.getUploaderUrl(), url -> openChannelFragment(activity, item, url));
|
||||||
|
}),
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Enqueues the stream automatically to the current PlayerType.
|
* Enqueues the stream automatically to the current PlayerType.
|
||||||
@@ -113,7 +114,10 @@ public enum StreamDialogDefaultEntry {
|
|||||||
DOWNLOAD(R.string.download, (fragment, item) ->
|
DOWNLOAD(R.string.download, (fragment, item) ->
|
||||||
fetchStreamInfoAndSaveToDatabase(fragment.requireContext(), item.getServiceId(),
|
fetchStreamInfoAndSaveToDatabase(fragment.requireContext(), item.getServiceId(),
|
||||||
item.getUrl(), info -> {
|
item.getUrl(), info -> {
|
||||||
if (fragment.getContext() != null) {
|
// Ensure the fragment is attached and its state hasn't been saved to avoid
|
||||||
|
// showing dialog during lifecycle changes or when the activity is paused,
|
||||||
|
// e.g. by selecting the download option and opening a different fragment.
|
||||||
|
if (fragment.isAdded() && !fragment.isStateSaved()) {
|
||||||
final DownloadDialog downloadDialog =
|
final DownloadDialog downloadDialog =
|
||||||
new DownloadDialog(fragment.requireContext(), info);
|
new DownloadDialog(fragment.requireContext(), info);
|
||||||
downloadDialog.show(fragment.getChildFragmentManager(),
|
downloadDialog.show(fragment.getChildFragmentManager(),
|
||||||
|
|||||||
@@ -13,8 +13,8 @@ import org.schabi.newpipe.extractor.channel.ChannelInfoItem;
|
|||||||
import org.schabi.newpipe.extractor.utils.Utils;
|
import org.schabi.newpipe.extractor.utils.Utils;
|
||||||
import org.schabi.newpipe.info_list.InfoItemBuilder;
|
import org.schabi.newpipe.info_list.InfoItemBuilder;
|
||||||
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
||||||
import org.schabi.newpipe.util.image.PicassoHelper;
|
|
||||||
import org.schabi.newpipe.util.Localization;
|
import org.schabi.newpipe.util.Localization;
|
||||||
|
import org.schabi.newpipe.util.image.CoilHelper;
|
||||||
|
|
||||||
public class ChannelMiniInfoItemHolder extends InfoItemHolder {
|
public class ChannelMiniInfoItemHolder extends InfoItemHolder {
|
||||||
private final ImageView itemThumbnailView;
|
private final ImageView itemThumbnailView;
|
||||||
@@ -56,7 +56,7 @@ public class ChannelMiniInfoItemHolder extends InfoItemHolder {
|
|||||||
itemAdditionalDetailView.setText(getDetailLine(item));
|
itemAdditionalDetailView.setText(getDetailLine(item));
|
||||||
}
|
}
|
||||||
|
|
||||||
PicassoHelper.loadAvatar(item.getThumbnails()).into(itemThumbnailView);
|
CoilHelper.INSTANCE.loadAvatar(itemThumbnailView, item.getThumbnails());
|
||||||
|
|
||||||
itemView.setOnClickListener(view -> {
|
itemView.setOnClickListener(view -> {
|
||||||
if (itemBuilder.getOnChannelSelectedListener() != null) {
|
if (itemBuilder.getOnChannelSelectedListener() != null) {
|
||||||
|
|||||||
@@ -1,63 +0,0 @@
|
|||||||
package org.schabi.newpipe.info_list.holder;
|
|
||||||
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.ViewGroup;
|
|
||||||
import android.widget.ImageView;
|
|
||||||
import android.widget.TextView;
|
|
||||||
|
|
||||||
import org.schabi.newpipe.R;
|
|
||||||
import org.schabi.newpipe.extractor.InfoItem;
|
|
||||||
import org.schabi.newpipe.extractor.comments.CommentsInfoItem;
|
|
||||||
import org.schabi.newpipe.info_list.InfoItemBuilder;
|
|
||||||
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Created by Christian Schabesberger on 12.02.17.
|
|
||||||
*
|
|
||||||
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
|
|
||||||
* ChannelInfoItemHolder .java is part of NewPipe.
|
|
||||||
*
|
|
||||||
* NewPipe is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* NewPipe is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
public class CommentsInfoItemHolder extends CommentsMiniInfoItemHolder {
|
|
||||||
public final TextView itemTitleView;
|
|
||||||
private final ImageView itemHeartView;
|
|
||||||
private final ImageView itemPinnedView;
|
|
||||||
|
|
||||||
public CommentsInfoItemHolder(final InfoItemBuilder infoItemBuilder, final ViewGroup parent) {
|
|
||||||
super(infoItemBuilder, R.layout.list_comments_item, parent);
|
|
||||||
|
|
||||||
itemTitleView = itemView.findViewById(R.id.itemTitleView);
|
|
||||||
itemHeartView = itemView.findViewById(R.id.detail_heart_image_view);
|
|
||||||
itemPinnedView = itemView.findViewById(R.id.detail_pinned_view);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void updateFromItem(final InfoItem infoItem,
|
|
||||||
final HistoryRecordManager historyRecordManager) {
|
|
||||||
super.updateFromItem(infoItem, historyRecordManager);
|
|
||||||
|
|
||||||
if (!(infoItem instanceof CommentsInfoItem)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
final CommentsInfoItem item = (CommentsInfoItem) infoItem;
|
|
||||||
|
|
||||||
itemTitleView.setText(item.getUploaderName());
|
|
||||||
|
|
||||||
itemHeartView.setVisibility(item.isHeartedByUploader() ? View.VISIBLE : View.GONE);
|
|
||||||
|
|
||||||
itemPinnedView.setVisibility(item.isPinned() ? View.VISIBLE : View.GONE);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,280 +0,0 @@
|
|||||||
package org.schabi.newpipe.info_list.holder;
|
|
||||||
|
|
||||||
import static android.text.TextUtils.isEmpty;
|
|
||||||
|
|
||||||
import android.graphics.Paint;
|
|
||||||
import android.text.Layout;
|
|
||||||
import android.text.method.LinkMovementMethod;
|
|
||||||
import android.text.style.URLSpan;
|
|
||||||
import android.util.Log;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.ViewGroup;
|
|
||||||
import android.widget.ImageView;
|
|
||||||
import android.widget.RelativeLayout;
|
|
||||||
import android.widget.TextView;
|
|
||||||
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import androidx.appcompat.app.AppCompatActivity;
|
|
||||||
import androidx.core.text.HtmlCompat;
|
|
||||||
|
|
||||||
import org.schabi.newpipe.R;
|
|
||||||
import org.schabi.newpipe.error.ErrorUtil;
|
|
||||||
import org.schabi.newpipe.extractor.InfoItem;
|
|
||||||
import org.schabi.newpipe.extractor.NewPipe;
|
|
||||||
import org.schabi.newpipe.extractor.ServiceList;
|
|
||||||
import org.schabi.newpipe.extractor.StreamingService;
|
|
||||||
import org.schabi.newpipe.extractor.comments.CommentsInfoItem;
|
|
||||||
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
|
||||||
import org.schabi.newpipe.extractor.stream.Description;
|
|
||||||
import org.schabi.newpipe.info_list.InfoItemBuilder;
|
|
||||||
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
|
||||||
import org.schabi.newpipe.util.DeviceUtils;
|
|
||||||
import org.schabi.newpipe.util.Localization;
|
|
||||||
import org.schabi.newpipe.util.NavigationHelper;
|
|
||||||
import org.schabi.newpipe.util.image.ImageStrategy;
|
|
||||||
import org.schabi.newpipe.util.image.PicassoHelper;
|
|
||||||
import org.schabi.newpipe.util.external_communication.ShareUtils;
|
|
||||||
import org.schabi.newpipe.util.text.CommentTextOnTouchListener;
|
|
||||||
import org.schabi.newpipe.util.text.TextLinkifier;
|
|
||||||
|
|
||||||
import java.util.function.Consumer;
|
|
||||||
|
|
||||||
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
|
||||||
|
|
||||||
public class CommentsMiniInfoItemHolder extends InfoItemHolder {
|
|
||||||
private static final String TAG = "CommentsMiniIIHolder";
|
|
||||||
private static final String ELLIPSIS = "…";
|
|
||||||
|
|
||||||
private static final int COMMENT_DEFAULT_LINES = 2;
|
|
||||||
private static final int COMMENT_EXPANDED_LINES = 1000;
|
|
||||||
|
|
||||||
private final int commentHorizontalPadding;
|
|
||||||
private final int commentVerticalPadding;
|
|
||||||
|
|
||||||
private final Paint paintAtContentSize;
|
|
||||||
private final float ellipsisWidthPx;
|
|
||||||
|
|
||||||
private final RelativeLayout itemRoot;
|
|
||||||
private final ImageView itemThumbnailView;
|
|
||||||
private final TextView itemContentView;
|
|
||||||
private final TextView itemLikesCountView;
|
|
||||||
private final TextView itemPublishedTime;
|
|
||||||
|
|
||||||
private final CompositeDisposable disposables = new CompositeDisposable();
|
|
||||||
@Nullable private Description commentText;
|
|
||||||
@Nullable private StreamingService streamService;
|
|
||||||
@Nullable private String streamUrl;
|
|
||||||
|
|
||||||
CommentsMiniInfoItemHolder(final InfoItemBuilder infoItemBuilder, final int layoutId,
|
|
||||||
final ViewGroup parent) {
|
|
||||||
super(infoItemBuilder, layoutId, parent);
|
|
||||||
|
|
||||||
itemRoot = itemView.findViewById(R.id.itemRoot);
|
|
||||||
itemThumbnailView = itemView.findViewById(R.id.itemThumbnailView);
|
|
||||||
itemLikesCountView = itemView.findViewById(R.id.detail_thumbs_up_count_view);
|
|
||||||
itemPublishedTime = itemView.findViewById(R.id.itemPublishedTime);
|
|
||||||
itemContentView = itemView.findViewById(R.id.itemCommentContentView);
|
|
||||||
|
|
||||||
commentHorizontalPadding = (int) infoItemBuilder.getContext()
|
|
||||||
.getResources().getDimension(R.dimen.comments_horizontal_padding);
|
|
||||||
commentVerticalPadding = (int) infoItemBuilder.getContext()
|
|
||||||
.getResources().getDimension(R.dimen.comments_vertical_padding);
|
|
||||||
|
|
||||||
paintAtContentSize = new Paint();
|
|
||||||
paintAtContentSize.setTextSize(itemContentView.getTextSize());
|
|
||||||
ellipsisWidthPx = paintAtContentSize.measureText(ELLIPSIS);
|
|
||||||
}
|
|
||||||
|
|
||||||
public CommentsMiniInfoItemHolder(final InfoItemBuilder infoItemBuilder,
|
|
||||||
final ViewGroup parent) {
|
|
||||||
this(infoItemBuilder, R.layout.list_comments_mini_item, parent);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void updateFromItem(final InfoItem infoItem,
|
|
||||||
final HistoryRecordManager historyRecordManager) {
|
|
||||||
if (!(infoItem instanceof CommentsInfoItem)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
final CommentsInfoItem item = (CommentsInfoItem) infoItem;
|
|
||||||
|
|
||||||
PicassoHelper.loadAvatar(item.getUploaderAvatars()).into(itemThumbnailView);
|
|
||||||
if (ImageStrategy.shouldLoadImages()) {
|
|
||||||
itemThumbnailView.setVisibility(View.VISIBLE);
|
|
||||||
itemRoot.setPadding(commentVerticalPadding, commentVerticalPadding,
|
|
||||||
commentVerticalPadding, commentVerticalPadding);
|
|
||||||
} else {
|
|
||||||
itemThumbnailView.setVisibility(View.GONE);
|
|
||||||
itemRoot.setPadding(commentHorizontalPadding, commentVerticalPadding,
|
|
||||||
commentHorizontalPadding, commentVerticalPadding);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
itemThumbnailView.setOnClickListener(view -> openCommentAuthor(item));
|
|
||||||
|
|
||||||
try {
|
|
||||||
streamService = NewPipe.getService(item.getServiceId());
|
|
||||||
} catch (final ExtractionException e) {
|
|
||||||
// should never happen
|
|
||||||
ErrorUtil.showUiErrorSnackbar(itemBuilder.getContext(), "Getting StreamingService", e);
|
|
||||||
Log.w(TAG, "Cannot obtain service from comment service id, defaulting to YouTube", e);
|
|
||||||
streamService = ServiceList.YouTube;
|
|
||||||
}
|
|
||||||
streamUrl = item.getUrl();
|
|
||||||
commentText = item.getCommentText();
|
|
||||||
ellipsize();
|
|
||||||
|
|
||||||
//noinspection ClickableViewAccessibility
|
|
||||||
itemContentView.setOnTouchListener(CommentTextOnTouchListener.INSTANCE);
|
|
||||||
|
|
||||||
if (item.getLikeCount() >= 0) {
|
|
||||||
itemLikesCountView.setText(
|
|
||||||
Localization.shortCount(
|
|
||||||
itemBuilder.getContext(),
|
|
||||||
item.getLikeCount()));
|
|
||||||
} else {
|
|
||||||
itemLikesCountView.setText("-");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (item.getUploadDate() != null) {
|
|
||||||
itemPublishedTime.setText(Localization.relativeTime(item.getUploadDate()
|
|
||||||
.offsetDateTime()));
|
|
||||||
} else {
|
|
||||||
itemPublishedTime.setText(item.getTextualUploadDate());
|
|
||||||
}
|
|
||||||
|
|
||||||
itemView.setOnClickListener(view -> {
|
|
||||||
toggleEllipsize();
|
|
||||||
if (itemBuilder.getOnCommentsSelectedListener() != null) {
|
|
||||||
itemBuilder.getOnCommentsSelectedListener().selected(item);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
itemView.setOnLongClickListener(view -> {
|
|
||||||
if (DeviceUtils.isTv(itemBuilder.getContext())) {
|
|
||||||
openCommentAuthor(item);
|
|
||||||
} else {
|
|
||||||
final CharSequence text = itemContentView.getText();
|
|
||||||
if (text != null) {
|
|
||||||
ShareUtils.copyToClipboard(itemBuilder.getContext(), text.toString());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private void openCommentAuthor(final CommentsInfoItem item) {
|
|
||||||
if (isEmpty(item.getUploaderUrl())) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
final AppCompatActivity activity = (AppCompatActivity) itemBuilder.getContext();
|
|
||||||
try {
|
|
||||||
NavigationHelper.openChannelFragment(
|
|
||||||
activity.getSupportFragmentManager(),
|
|
||||||
item.getServiceId(),
|
|
||||||
item.getUploaderUrl(),
|
|
||||||
item.getUploaderName());
|
|
||||||
} catch (final Exception e) {
|
|
||||||
ErrorUtil.showUiErrorSnackbar(activity, "Opening channel fragment", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void allowLinkFocus() {
|
|
||||||
itemContentView.setMovementMethod(LinkMovementMethod.getInstance());
|
|
||||||
}
|
|
||||||
|
|
||||||
private void denyLinkFocus() {
|
|
||||||
itemContentView.setMovementMethod(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean shouldFocusLinks() {
|
|
||||||
if (itemView.isInTouchMode()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
final URLSpan[] urls = itemContentView.getUrls();
|
|
||||||
|
|
||||||
return urls != null && urls.length != 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void determineMovementMethod() {
|
|
||||||
if (shouldFocusLinks()) {
|
|
||||||
allowLinkFocus();
|
|
||||||
} else {
|
|
||||||
denyLinkFocus();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ellipsize() {
|
|
||||||
itemContentView.setMaxLines(COMMENT_EXPANDED_LINES);
|
|
||||||
linkifyCommentContentView(v -> {
|
|
||||||
boolean hasEllipsis = false;
|
|
||||||
|
|
||||||
final CharSequence charSeqText = itemContentView.getText();
|
|
||||||
if (charSeqText != null && itemContentView.getLineCount() > COMMENT_DEFAULT_LINES) {
|
|
||||||
// Note that converting to String removes spans (i.e. links), but that's something
|
|
||||||
// we actually want since when the text is ellipsized we want all clicks on the
|
|
||||||
// comment to expand the comment, not to open links.
|
|
||||||
final String text = charSeqText.toString();
|
|
||||||
|
|
||||||
final Layout layout = itemContentView.getLayout();
|
|
||||||
final float lineWidth = layout.getLineWidth(COMMENT_DEFAULT_LINES - 1);
|
|
||||||
final float layoutWidth = layout.getWidth();
|
|
||||||
final int lineStart = layout.getLineStart(COMMENT_DEFAULT_LINES - 1);
|
|
||||||
final int lineEnd = layout.getLineEnd(COMMENT_DEFAULT_LINES - 1);
|
|
||||||
|
|
||||||
// remove characters up until there is enough space for the ellipsis
|
|
||||||
// (also summing 2 more pixels, just to be sure to avoid float rounding errors)
|
|
||||||
int end = lineEnd;
|
|
||||||
float removedCharactersWidth = 0.0f;
|
|
||||||
while (lineWidth - removedCharactersWidth + ellipsisWidthPx + 2.0f > layoutWidth
|
|
||||||
&& end >= lineStart) {
|
|
||||||
end -= 1;
|
|
||||||
// recalculate each time to account for ligatures or other similar things
|
|
||||||
removedCharactersWidth = paintAtContentSize.measureText(
|
|
||||||
text.substring(end, lineEnd));
|
|
||||||
}
|
|
||||||
|
|
||||||
// remove trailing spaces and newlines
|
|
||||||
while (end > 0 && Character.isWhitespace(text.charAt(end - 1))) {
|
|
||||||
end -= 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
final String newVal = text.substring(0, end) + ELLIPSIS;
|
|
||||||
itemContentView.setText(newVal);
|
|
||||||
hasEllipsis = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
itemContentView.setMaxLines(COMMENT_DEFAULT_LINES);
|
|
||||||
if (hasEllipsis) {
|
|
||||||
denyLinkFocus();
|
|
||||||
} else {
|
|
||||||
determineMovementMethod();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private void toggleEllipsize() {
|
|
||||||
final CharSequence text = itemContentView.getText();
|
|
||||||
if (!isEmpty(text) && text.charAt(text.length() - 1) == ELLIPSIS.charAt(0)) {
|
|
||||||
expand();
|
|
||||||
} else if (itemContentView.getLineCount() > COMMENT_DEFAULT_LINES) {
|
|
||||||
ellipsize();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void expand() {
|
|
||||||
itemContentView.setMaxLines(COMMENT_EXPANDED_LINES);
|
|
||||||
linkifyCommentContentView(v -> determineMovementMethod());
|
|
||||||
}
|
|
||||||
|
|
||||||
private void linkifyCommentContentView(@Nullable final Consumer<TextView> onCompletion) {
|
|
||||||
disposables.clear();
|
|
||||||
if (commentText != null) {
|
|
||||||
TextLinkifier.fromDescription(itemContentView, commentText,
|
|
||||||
HtmlCompat.FROM_HTML_MODE_LEGACY, streamService, streamUrl, disposables,
|
|
||||||
onCompletion);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -9,8 +9,8 @@ import org.schabi.newpipe.extractor.InfoItem;
|
|||||||
import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem;
|
import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem;
|
||||||
import org.schabi.newpipe.info_list.InfoItemBuilder;
|
import org.schabi.newpipe.info_list.InfoItemBuilder;
|
||||||
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
||||||
import org.schabi.newpipe.util.image.PicassoHelper;
|
|
||||||
import org.schabi.newpipe.util.Localization;
|
import org.schabi.newpipe.util.Localization;
|
||||||
|
import org.schabi.newpipe.util.image.CoilHelper;
|
||||||
|
|
||||||
public class PlaylistMiniInfoItemHolder extends InfoItemHolder {
|
public class PlaylistMiniInfoItemHolder extends InfoItemHolder {
|
||||||
public final ImageView itemThumbnailView;
|
public final ImageView itemThumbnailView;
|
||||||
@@ -46,7 +46,7 @@ public class PlaylistMiniInfoItemHolder extends InfoItemHolder {
|
|||||||
.localizeStreamCountMini(itemStreamCountView.getContext(), item.getStreamCount()));
|
.localizeStreamCountMini(itemStreamCountView.getContext(), item.getStreamCount()));
|
||||||
itemUploaderView.setText(item.getUploaderName());
|
itemUploaderView.setText(item.getUploaderName());
|
||||||
|
|
||||||
PicassoHelper.loadPlaylistThumbnail(item.getThumbnails()).into(itemThumbnailView);
|
CoilHelper.INSTANCE.loadPlaylistThumbnail(itemThumbnailView, item.getThumbnails());
|
||||||
|
|
||||||
itemView.setOnClickListener(view -> {
|
itemView.setOnClickListener(view -> {
|
||||||
if (itemBuilder.getOnPlaylistSelectedListener() != null) {
|
if (itemBuilder.getOnPlaylistSelectedListener() != null) {
|
||||||
|
|||||||
@@ -12,10 +12,6 @@ import org.schabi.newpipe.info_list.InfoItemBuilder;
|
|||||||
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
||||||
import org.schabi.newpipe.util.Localization;
|
import org.schabi.newpipe.util.Localization;
|
||||||
|
|
||||||
import androidx.preference.PreferenceManager;
|
|
||||||
|
|
||||||
import static org.schabi.newpipe.MainActivity.DEBUG;
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Created by Christian Schabesberger on 01.08.16.
|
* Created by Christian Schabesberger on 01.08.16.
|
||||||
* <p>
|
* <p>
|
||||||
@@ -81,7 +77,9 @@ public class StreamInfoItemHolder extends StreamMiniInfoItemHolder {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final String uploadDate = getFormattedRelativeUploadDate(infoItem);
|
final String uploadDate = Localization.relativeTimeOrTextual(itemBuilder.getContext(),
|
||||||
|
infoItem.getUploadDate(),
|
||||||
|
infoItem.getTextualUploadDate());
|
||||||
if (!TextUtils.isEmpty(uploadDate)) {
|
if (!TextUtils.isEmpty(uploadDate)) {
|
||||||
if (viewsAndDate.isEmpty()) {
|
if (viewsAndDate.isEmpty()) {
|
||||||
return uploadDate;
|
return uploadDate;
|
||||||
@@ -92,20 +90,4 @@ public class StreamInfoItemHolder extends StreamMiniInfoItemHolder {
|
|||||||
|
|
||||||
return viewsAndDate;
|
return viewsAndDate;
|
||||||
}
|
}
|
||||||
|
|
||||||
private String getFormattedRelativeUploadDate(final StreamInfoItem infoItem) {
|
|
||||||
if (infoItem.getUploadDate() != null) {
|
|
||||||
String formattedRelativeTime = Localization
|
|
||||||
.relativeTime(infoItem.getUploadDate().offsetDateTime());
|
|
||||||
|
|
||||||
if (DEBUG && PreferenceManager.getDefaultSharedPreferences(itemBuilder.getContext())
|
|
||||||
.getBoolean(itemBuilder.getContext()
|
|
||||||
.getString(R.string.show_original_time_ago_key), false)) {
|
|
||||||
formattedRelativeTime += " (" + infoItem.getTextualUploadDate() + ")";
|
|
||||||
}
|
|
||||||
return formattedRelativeTime;
|
|
||||||
} else {
|
|
||||||
return infoItem.getTextualUploadDate();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,8 +16,8 @@ import org.schabi.newpipe.ktx.ViewUtils;
|
|||||||
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
||||||
import org.schabi.newpipe.util.DependentPreferenceHelper;
|
import org.schabi.newpipe.util.DependentPreferenceHelper;
|
||||||
import org.schabi.newpipe.util.Localization;
|
import org.schabi.newpipe.util.Localization;
|
||||||
import org.schabi.newpipe.util.image.PicassoHelper;
|
|
||||||
import org.schabi.newpipe.util.StreamTypeUtil;
|
import org.schabi.newpipe.util.StreamTypeUtil;
|
||||||
|
import org.schabi.newpipe.util.image.CoilHelper;
|
||||||
import org.schabi.newpipe.views.AnimatedProgressBar;
|
import org.schabi.newpipe.views.AnimatedProgressBar;
|
||||||
|
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
@@ -64,8 +64,7 @@ public class StreamMiniInfoItemHolder extends InfoItemHolder {
|
|||||||
StreamStateEntity state2 = null;
|
StreamStateEntity state2 = null;
|
||||||
if (DependentPreferenceHelper
|
if (DependentPreferenceHelper
|
||||||
.getPositionsInListsEnabled(itemProgressView.getContext())) {
|
.getPositionsInListsEnabled(itemProgressView.getContext())) {
|
||||||
state2 = historyRecordManager.loadStreamState(infoItem)
|
state2 = historyRecordManager.loadStreamState(infoItem).blockingGet();
|
||||||
.blockingGet()[0];
|
|
||||||
}
|
}
|
||||||
if (state2 != null) {
|
if (state2 != null) {
|
||||||
itemProgressView.setVisibility(View.VISIBLE);
|
itemProgressView.setVisibility(View.VISIBLE);
|
||||||
@@ -87,7 +86,7 @@ public class StreamMiniInfoItemHolder extends InfoItemHolder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Default thumbnail is shown on error, while loading and if the url is empty
|
// Default thumbnail is shown on error, while loading and if the url is empty
|
||||||
PicassoHelper.loadThumbnail(item.getThumbnails()).into(itemThumbnailView);
|
CoilHelper.INSTANCE.loadThumbnail(itemThumbnailView, item.getThumbnails());
|
||||||
|
|
||||||
itemView.setOnClickListener(view -> {
|
itemView.setOnClickListener(view -> {
|
||||||
if (itemBuilder.getOnStreamSelectedListener() != null) {
|
if (itemBuilder.getOnStreamSelectedListener() != null) {
|
||||||
@@ -120,7 +119,7 @@ public class StreamMiniInfoItemHolder extends InfoItemHolder {
|
|||||||
if (DependentPreferenceHelper.getPositionsInListsEnabled(itemProgressView.getContext())) {
|
if (DependentPreferenceHelper.getPositionsInListsEnabled(itemProgressView.getContext())) {
|
||||||
state = historyRecordManager
|
state = historyRecordManager
|
||||||
.loadStreamState(infoItem)
|
.loadStreamState(infoItem)
|
||||||
.blockingGet()[0];
|
.blockingGet();
|
||||||
}
|
}
|
||||||
if (state != null && item.getDuration() > 0
|
if (state != null && item.getDuration() > 0
|
||||||
&& !StreamTypeUtil.isLiveStream(item.getStreamType())) {
|
&& !StreamTypeUtil.isLiveStream(item.getStreamType())) {
|
||||||
|
|||||||
13
app/src/main/java/org/schabi/newpipe/ktx/Bitmap.kt
Normal file
13
app/src/main/java/org/schabi/newpipe/ktx/Bitmap.kt
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
package org.schabi.newpipe.ktx
|
||||||
|
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.graphics.Rect
|
||||||
|
import androidx.core.graphics.BitmapCompat
|
||||||
|
|
||||||
|
@Suppress("NOTHING_TO_INLINE")
|
||||||
|
inline fun Bitmap.scale(
|
||||||
|
width: Int,
|
||||||
|
height: Int,
|
||||||
|
srcRect: Rect? = null,
|
||||||
|
scaleInLinearSpace: Boolean = true,
|
||||||
|
) = BitmapCompat.createScaledBitmap(this, width, height, srcRect, scaleInLinearSpace)
|
||||||
22
app/src/main/java/org/schabi/newpipe/ktx/Bundle.kt
Normal file
22
app/src/main/java/org/schabi/newpipe/ktx/Bundle.kt
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
package org.schabi.newpipe.ktx
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import androidx.core.os.BundleCompat
|
||||||
|
import java.io.Serializable
|
||||||
|
|
||||||
|
inline fun <reified T : Serializable> Bundle.serializable(key: String?): T? {
|
||||||
|
return BundleCompat.getSerializable(this, key, T::class.java)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Bundle?.toDebugString(): String {
|
||||||
|
if (this == null) {
|
||||||
|
return "null"
|
||||||
|
}
|
||||||
|
val string = StringBuilder("Bundle{")
|
||||||
|
for (key in this.keySet()) {
|
||||||
|
@Suppress("DEPRECATION") // we want this[key] to return items of any type
|
||||||
|
string.append(" ").append(key).append(" => ").append(this[key]).append(";")
|
||||||
|
}
|
||||||
|
string.append(" }")
|
||||||
|
return string.toString()
|
||||||
|
}
|
||||||
13
app/src/main/java/org/schabi/newpipe/ktx/Context.kt
Normal file
13
app/src/main/java/org/schabi/newpipe/ktx/Context.kt
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
package org.schabi.newpipe.ktx
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.ContextWrapper
|
||||||
|
import androidx.fragment.app.FragmentActivity
|
||||||
|
|
||||||
|
tailrec fun Context.findFragmentActivity(): FragmentActivity {
|
||||||
|
return when (this) {
|
||||||
|
is FragmentActivity -> this
|
||||||
|
is ContextWrapper -> baseContext.findFragmentActivity()
|
||||||
|
else -> throw IllegalStateException("Unable to find FragmentActivity")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package org.schabi.newpipe.ktx
|
||||||
|
|
||||||
|
import android.content.SharedPreferences
|
||||||
|
|
||||||
|
fun SharedPreferences.getStringSafe(key: String, defValue: String): String {
|
||||||
|
return getString(key, null) ?: defValue
|
||||||
|
}
|
||||||
@@ -17,8 +17,10 @@ import androidx.core.view.isGone
|
|||||||
import androidx.core.view.isInvisible
|
import androidx.core.view.isInvisible
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import androidx.interpolator.view.animation.FastOutSlowInInterpolator
|
import androidx.interpolator.view.animation.FastOutSlowInInterpolator
|
||||||
import org.schabi.newpipe.MainActivity
|
|
||||||
|
|
||||||
|
// logs in this class are disabled by default since it's usually not useful,
|
||||||
|
// you can enable them by setting this flag to MainActivity.DEBUG
|
||||||
|
private const val DEBUG = false
|
||||||
private const val TAG = "ViewUtils"
|
private const val TAG = "ViewUtils"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -38,7 +40,7 @@ fun View.animate(
|
|||||||
delay: Long = 0,
|
delay: Long = 0,
|
||||||
execOnEnd: Runnable? = null
|
execOnEnd: Runnable? = null
|
||||||
) {
|
) {
|
||||||
if (MainActivity.DEBUG) {
|
if (DEBUG) {
|
||||||
val id = try {
|
val id = try {
|
||||||
resources.getResourceEntryName(id)
|
resources.getResourceEntryName(id)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
@@ -51,7 +53,7 @@ fun View.animate(
|
|||||||
Log.d(TAG, "animate(): $msg")
|
Log.d(TAG, "animate(): $msg")
|
||||||
}
|
}
|
||||||
if (isVisible && enterOrExit) {
|
if (isVisible && enterOrExit) {
|
||||||
if (MainActivity.DEBUG) {
|
if (DEBUG) {
|
||||||
Log.d(TAG, "animate(): view was already visible > view = [$this]")
|
Log.d(TAG, "animate(): view was already visible > view = [$this]")
|
||||||
}
|
}
|
||||||
animate().setListener(null).cancel()
|
animate().setListener(null).cancel()
|
||||||
@@ -60,7 +62,7 @@ fun View.animate(
|
|||||||
execOnEnd?.run()
|
execOnEnd?.run()
|
||||||
return
|
return
|
||||||
} else if ((isGone || isInvisible) && !enterOrExit) {
|
} else if ((isGone || isInvisible) && !enterOrExit) {
|
||||||
if (MainActivity.DEBUG) {
|
if (DEBUG) {
|
||||||
Log.d(TAG, "animate(): view was already gone > view = [$this]")
|
Log.d(TAG, "animate(): view was already gone > view = [$this]")
|
||||||
}
|
}
|
||||||
animate().setListener(null).cancel()
|
animate().setListener(null).cancel()
|
||||||
@@ -89,7 +91,7 @@ fun View.animate(
|
|||||||
* @param colorEnd the background color to end with
|
* @param colorEnd the background color to end with
|
||||||
*/
|
*/
|
||||||
fun View.animateBackgroundColor(duration: Long, @ColorInt colorStart: Int, @ColorInt colorEnd: Int) {
|
fun View.animateBackgroundColor(duration: Long, @ColorInt colorStart: Int, @ColorInt colorEnd: Int) {
|
||||||
if (MainActivity.DEBUG) {
|
if (DEBUG) {
|
||||||
Log.d(
|
Log.d(
|
||||||
TAG,
|
TAG,
|
||||||
"animateBackgroundColor() called with: view = [$this], duration = [$duration], " +
|
"animateBackgroundColor() called with: view = [$this], duration = [$duration], " +
|
||||||
@@ -109,7 +111,7 @@ fun View.animateBackgroundColor(duration: Long, @ColorInt colorStart: Int, @Colo
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun View.animateHeight(duration: Long, targetHeight: Int): ValueAnimator {
|
fun View.animateHeight(duration: Long, targetHeight: Int): ValueAnimator {
|
||||||
if (MainActivity.DEBUG) {
|
if (DEBUG) {
|
||||||
Log.d(TAG, "animateHeight: duration = [$duration], from $height to → $targetHeight in: $this")
|
Log.d(TAG, "animateHeight: duration = [$duration], from $height to → $targetHeight in: $this")
|
||||||
}
|
}
|
||||||
val animator = ValueAnimator.ofFloat(height.toFloat(), targetHeight.toFloat())
|
val animator = ValueAnimator.ofFloat(height.toFloat(), targetHeight.toFloat())
|
||||||
@@ -127,7 +129,7 @@ fun View.animateHeight(duration: Long, targetHeight: Int): ValueAnimator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun View.animateRotation(duration: Long, targetRotation: Int) {
|
fun View.animateRotation(duration: Long, targetRotation: Int) {
|
||||||
if (MainActivity.DEBUG) {
|
if (DEBUG) {
|
||||||
Log.d(TAG, "animateRotation: duration = [$duration], from $rotation to → $targetRotation in: $this")
|
Log.d(TAG, "animateRotation: duration = [$duration], from $rotation to → $targetRotation in: $this")
|
||||||
}
|
}
|
||||||
animate().setListener(null).cancel()
|
animate().setListener(null).cancel()
|
||||||
|
|||||||
@@ -194,9 +194,6 @@ public abstract class BaseLocalListFragment<I, N> extends BaseStateFragment<I>
|
|||||||
if (itemsList != null) {
|
if (itemsList != null) {
|
||||||
animateHideRecyclerViewAllowingScrolling(itemsList);
|
animateHideRecyclerViewAllowingScrolling(itemsList);
|
||||||
}
|
}
|
||||||
if (headerRootBinding != null) {
|
|
||||||
animate(headerRootBinding.getRoot(), false, 200);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -205,9 +202,6 @@ public abstract class BaseLocalListFragment<I, N> extends BaseStateFragment<I>
|
|||||||
if (itemsList != null) {
|
if (itemsList != null) {
|
||||||
animate(itemsList, true, 200);
|
animate(itemsList, true, 200);
|
||||||
}
|
}
|
||||||
if (headerRootBinding != null) {
|
|
||||||
animate(headerRootBinding.getRoot(), true, 200);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -253,9 +247,6 @@ public abstract class BaseLocalListFragment<I, N> extends BaseStateFragment<I>
|
|||||||
if (itemsList != null) {
|
if (itemsList != null) {
|
||||||
animateHideRecyclerViewAllowingScrolling(itemsList);
|
animateHideRecyclerViewAllowingScrolling(itemsList);
|
||||||
}
|
}
|
||||||
if (headerRootBinding != null) {
|
|
||||||
animate(headerRootBinding.getRoot(), false, 200);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import org.schabi.newpipe.database.LocalItem;
|
|||||||
import org.schabi.newpipe.database.stream.model.StreamStateEntity;
|
import org.schabi.newpipe.database.stream.model.StreamStateEntity;
|
||||||
import org.schabi.newpipe.info_list.ItemViewMode;
|
import org.schabi.newpipe.info_list.ItemViewMode;
|
||||||
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
||||||
|
import org.schabi.newpipe.local.holder.LocalBookmarkPlaylistItemHolder;
|
||||||
import org.schabi.newpipe.local.holder.LocalItemHolder;
|
import org.schabi.newpipe.local.holder.LocalItemHolder;
|
||||||
import org.schabi.newpipe.local.holder.LocalPlaylistCardItemHolder;
|
import org.schabi.newpipe.local.holder.LocalPlaylistCardItemHolder;
|
||||||
import org.schabi.newpipe.local.holder.LocalPlaylistGridItemHolder;
|
import org.schabi.newpipe.local.holder.LocalPlaylistGridItemHolder;
|
||||||
@@ -24,6 +25,7 @@ import org.schabi.newpipe.local.holder.LocalPlaylistStreamItemHolder;
|
|||||||
import org.schabi.newpipe.local.holder.LocalStatisticStreamCardItemHolder;
|
import org.schabi.newpipe.local.holder.LocalStatisticStreamCardItemHolder;
|
||||||
import org.schabi.newpipe.local.holder.LocalStatisticStreamGridItemHolder;
|
import org.schabi.newpipe.local.holder.LocalStatisticStreamGridItemHolder;
|
||||||
import org.schabi.newpipe.local.holder.LocalStatisticStreamItemHolder;
|
import org.schabi.newpipe.local.holder.LocalStatisticStreamItemHolder;
|
||||||
|
import org.schabi.newpipe.local.holder.RemoteBookmarkPlaylistItemHolder;
|
||||||
import org.schabi.newpipe.local.holder.RemotePlaylistCardItemHolder;
|
import org.schabi.newpipe.local.holder.RemotePlaylistCardItemHolder;
|
||||||
import org.schabi.newpipe.local.holder.RemotePlaylistGridItemHolder;
|
import org.schabi.newpipe.local.holder.RemotePlaylistGridItemHolder;
|
||||||
import org.schabi.newpipe.local.holder.RemotePlaylistItemHolder;
|
import org.schabi.newpipe.local.holder.RemotePlaylistItemHolder;
|
||||||
@@ -73,10 +75,12 @@ public class LocalItemListAdapter extends RecyclerView.Adapter<RecyclerView.View
|
|||||||
private static final int LOCAL_PLAYLIST_HOLDER_TYPE = 0x2000;
|
private static final int LOCAL_PLAYLIST_HOLDER_TYPE = 0x2000;
|
||||||
private static final int LOCAL_PLAYLIST_GRID_HOLDER_TYPE = 0x2001;
|
private static final int LOCAL_PLAYLIST_GRID_HOLDER_TYPE = 0x2001;
|
||||||
private static final int LOCAL_PLAYLIST_CARD_HOLDER_TYPE = 0x2002;
|
private static final int LOCAL_PLAYLIST_CARD_HOLDER_TYPE = 0x2002;
|
||||||
|
private static final int LOCAL_BOOKMARK_PLAYLIST_HOLDER_TYPE = 0x2003;
|
||||||
|
|
||||||
private static final int REMOTE_PLAYLIST_HOLDER_TYPE = 0x3000;
|
private static final int REMOTE_PLAYLIST_HOLDER_TYPE = 0x3000;
|
||||||
private static final int REMOTE_PLAYLIST_GRID_HOLDER_TYPE = 0x3001;
|
private static final int REMOTE_PLAYLIST_GRID_HOLDER_TYPE = 0x3001;
|
||||||
private static final int REMOTE_PLAYLIST_CARD_HOLDER_TYPE = 0x3002;
|
private static final int REMOTE_PLAYLIST_CARD_HOLDER_TYPE = 0x3002;
|
||||||
|
private static final int REMOTE_BOOKMARK_PLAYLIST_HOLDER_TYPE = 0x3003;
|
||||||
|
|
||||||
private final LocalItemBuilder localItemBuilder;
|
private final LocalItemBuilder localItemBuilder;
|
||||||
private final ArrayList<LocalItem> localItems;
|
private final ArrayList<LocalItem> localItems;
|
||||||
@@ -87,6 +91,7 @@ public class LocalItemListAdapter extends RecyclerView.Adapter<RecyclerView.View
|
|||||||
private View header = null;
|
private View header = null;
|
||||||
private View footer = null;
|
private View footer = null;
|
||||||
private ItemViewMode itemViewMode = ItemViewMode.LIST;
|
private ItemViewMode itemViewMode = ItemViewMode.LIST;
|
||||||
|
private boolean useItemHandle = false;
|
||||||
|
|
||||||
public LocalItemListAdapter(final Context context) {
|
public LocalItemListAdapter(final Context context) {
|
||||||
recordManager = new HistoryRecordManager(context);
|
recordManager = new HistoryRecordManager(context);
|
||||||
@@ -180,6 +185,10 @@ public class LocalItemListAdapter extends RecyclerView.Adapter<RecyclerView.View
|
|||||||
this.itemViewMode = itemViewMode;
|
this.itemViewMode = itemViewMode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setUseItemHandle(final boolean useItemHandle) {
|
||||||
|
this.useItemHandle = useItemHandle;
|
||||||
|
}
|
||||||
|
|
||||||
public void setHeader(final View header) {
|
public void setHeader(final View header) {
|
||||||
final boolean changed = header != this.header;
|
final boolean changed = header != this.header;
|
||||||
this.header = header;
|
this.header = header;
|
||||||
@@ -257,7 +266,9 @@ public class LocalItemListAdapter extends RecyclerView.Adapter<RecyclerView.View
|
|||||||
final LocalItem item = localItems.get(position);
|
final LocalItem item = localItems.get(position);
|
||||||
switch (item.getLocalItemType()) {
|
switch (item.getLocalItemType()) {
|
||||||
case PLAYLIST_LOCAL_ITEM:
|
case PLAYLIST_LOCAL_ITEM:
|
||||||
if (itemViewMode == ItemViewMode.CARD) {
|
if (useItemHandle) {
|
||||||
|
return LOCAL_BOOKMARK_PLAYLIST_HOLDER_TYPE;
|
||||||
|
} else if (itemViewMode == ItemViewMode.CARD) {
|
||||||
return LOCAL_PLAYLIST_CARD_HOLDER_TYPE;
|
return LOCAL_PLAYLIST_CARD_HOLDER_TYPE;
|
||||||
} else if (itemViewMode == ItemViewMode.GRID) {
|
} else if (itemViewMode == ItemViewMode.GRID) {
|
||||||
return LOCAL_PLAYLIST_GRID_HOLDER_TYPE;
|
return LOCAL_PLAYLIST_GRID_HOLDER_TYPE;
|
||||||
@@ -265,7 +276,9 @@ public class LocalItemListAdapter extends RecyclerView.Adapter<RecyclerView.View
|
|||||||
return LOCAL_PLAYLIST_HOLDER_TYPE;
|
return LOCAL_PLAYLIST_HOLDER_TYPE;
|
||||||
}
|
}
|
||||||
case PLAYLIST_REMOTE_ITEM:
|
case PLAYLIST_REMOTE_ITEM:
|
||||||
if (itemViewMode == ItemViewMode.CARD) {
|
if (useItemHandle) {
|
||||||
|
return REMOTE_BOOKMARK_PLAYLIST_HOLDER_TYPE;
|
||||||
|
} else if (itemViewMode == ItemViewMode.CARD) {
|
||||||
return REMOTE_PLAYLIST_CARD_HOLDER_TYPE;
|
return REMOTE_PLAYLIST_CARD_HOLDER_TYPE;
|
||||||
} else if (itemViewMode == ItemViewMode.GRID) {
|
} else if (itemViewMode == ItemViewMode.GRID) {
|
||||||
return REMOTE_PLAYLIST_GRID_HOLDER_TYPE;
|
return REMOTE_PLAYLIST_GRID_HOLDER_TYPE;
|
||||||
@@ -314,12 +327,16 @@ public class LocalItemListAdapter extends RecyclerView.Adapter<RecyclerView.View
|
|||||||
return new LocalPlaylistGridItemHolder(localItemBuilder, parent);
|
return new LocalPlaylistGridItemHolder(localItemBuilder, parent);
|
||||||
case LOCAL_PLAYLIST_CARD_HOLDER_TYPE:
|
case LOCAL_PLAYLIST_CARD_HOLDER_TYPE:
|
||||||
return new LocalPlaylistCardItemHolder(localItemBuilder, parent);
|
return new LocalPlaylistCardItemHolder(localItemBuilder, parent);
|
||||||
|
case LOCAL_BOOKMARK_PLAYLIST_HOLDER_TYPE:
|
||||||
|
return new LocalBookmarkPlaylistItemHolder(localItemBuilder, parent);
|
||||||
case REMOTE_PLAYLIST_HOLDER_TYPE:
|
case REMOTE_PLAYLIST_HOLDER_TYPE:
|
||||||
return new RemotePlaylistItemHolder(localItemBuilder, parent);
|
return new RemotePlaylistItemHolder(localItemBuilder, parent);
|
||||||
case REMOTE_PLAYLIST_GRID_HOLDER_TYPE:
|
case REMOTE_PLAYLIST_GRID_HOLDER_TYPE:
|
||||||
return new RemotePlaylistGridItemHolder(localItemBuilder, parent);
|
return new RemotePlaylistGridItemHolder(localItemBuilder, parent);
|
||||||
case REMOTE_PLAYLIST_CARD_HOLDER_TYPE:
|
case REMOTE_PLAYLIST_CARD_HOLDER_TYPE:
|
||||||
return new RemotePlaylistCardItemHolder(localItemBuilder, parent);
|
return new RemotePlaylistCardItemHolder(localItemBuilder, parent);
|
||||||
|
case REMOTE_BOOKMARK_PLAYLIST_HOLDER_TYPE:
|
||||||
|
return new RemoteBookmarkPlaylistItemHolder(localItemBuilder, parent);
|
||||||
case STREAM_PLAYLIST_HOLDER_TYPE:
|
case STREAM_PLAYLIST_HOLDER_TYPE:
|
||||||
return new LocalPlaylistStreamItemHolder(localItemBuilder, parent);
|
return new LocalPlaylistStreamItemHolder(localItemBuilder, parent);
|
||||||
case STREAM_PLAYLIST_GRID_HOLDER_TYPE:
|
case STREAM_PLAYLIST_GRID_HOLDER_TYPE:
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
package org.schabi.newpipe.local.bookmark;
|
package org.schabi.newpipe.local.bookmark;
|
||||||
|
|
||||||
|
import static org.schabi.newpipe.local.bookmark.MergedPlaylistManager.getMergedOrderedPlaylists;
|
||||||
|
|
||||||
import android.content.DialogInterface;
|
import android.content.DialogInterface;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.os.Parcelable;
|
import android.os.Parcelable;
|
||||||
import android.text.InputType;
|
import android.text.InputType;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
import android.util.Pair;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
@@ -13,6 +16,10 @@ import androidx.annotation.NonNull;
|
|||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.appcompat.app.AlertDialog;
|
import androidx.appcompat.app.AlertDialog;
|
||||||
import androidx.fragment.app.FragmentManager;
|
import androidx.fragment.app.FragmentManager;
|
||||||
|
import androidx.recyclerview.widget.ItemTouchHelper;
|
||||||
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
|
|
||||||
|
import com.evernote.android.state.State;
|
||||||
|
|
||||||
import org.reactivestreams.Subscriber;
|
import org.reactivestreams.Subscriber;
|
||||||
import org.reactivestreams.Subscription;
|
import org.reactivestreams.Subscription;
|
||||||
@@ -27,29 +34,46 @@ import org.schabi.newpipe.databinding.DialogEditTextBinding;
|
|||||||
import org.schabi.newpipe.error.ErrorInfo;
|
import org.schabi.newpipe.error.ErrorInfo;
|
||||||
import org.schabi.newpipe.error.UserAction;
|
import org.schabi.newpipe.error.UserAction;
|
||||||
import org.schabi.newpipe.local.BaseLocalListFragment;
|
import org.schabi.newpipe.local.BaseLocalListFragment;
|
||||||
|
import org.schabi.newpipe.local.holder.LocalBookmarkPlaylistItemHolder;
|
||||||
|
import org.schabi.newpipe.local.holder.RemoteBookmarkPlaylistItemHolder;
|
||||||
import org.schabi.newpipe.local.playlist.LocalPlaylistManager;
|
import org.schabi.newpipe.local.playlist.LocalPlaylistManager;
|
||||||
import org.schabi.newpipe.local.playlist.RemotePlaylistManager;
|
import org.schabi.newpipe.local.playlist.RemotePlaylistManager;
|
||||||
|
import org.schabi.newpipe.ui.emptystate.EmptyStateSpec;
|
||||||
|
import org.schabi.newpipe.ui.emptystate.EmptyStateUtil;
|
||||||
import org.schabi.newpipe.util.NavigationHelper;
|
import org.schabi.newpipe.util.NavigationHelper;
|
||||||
import org.schabi.newpipe.util.OnClickGesture;
|
import org.schabi.newpipe.util.OnClickGesture;
|
||||||
|
import org.schabi.newpipe.util.debounce.DebounceSavable;
|
||||||
|
import org.schabi.newpipe.util.debounce.DebounceSaver;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
|
||||||
import icepick.State;
|
|
||||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
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.CompositeDisposable;
|
||||||
import io.reactivex.rxjava3.disposables.Disposable;
|
import io.reactivex.rxjava3.disposables.Disposable;
|
||||||
|
|
||||||
public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistLocalItem>, Void> {
|
public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistLocalItem>, Void>
|
||||||
|
implements DebounceSavable {
|
||||||
|
|
||||||
|
private static final int MINIMUM_INITIAL_DRAG_VELOCITY = 12;
|
||||||
@State
|
@State
|
||||||
protected Parcelable itemsListState;
|
Parcelable itemsListState;
|
||||||
|
|
||||||
private Subscription databaseSubscription;
|
private Subscription databaseSubscription;
|
||||||
private CompositeDisposable disposables = new CompositeDisposable();
|
private CompositeDisposable disposables = new CompositeDisposable();
|
||||||
private LocalPlaylistManager localPlaylistManager;
|
private LocalPlaylistManager localPlaylistManager;
|
||||||
private RemotePlaylistManager remotePlaylistManager;
|
private RemotePlaylistManager remotePlaylistManager;
|
||||||
|
private ItemTouchHelper itemTouchHelper;
|
||||||
|
|
||||||
|
/* Have the bookmarked playlists been fully loaded from db */
|
||||||
|
private AtomicBoolean isLoadingComplete;
|
||||||
|
|
||||||
|
/* Gives enough time to avoid interrupting user sorting operations */
|
||||||
|
@Nullable
|
||||||
|
private DebounceSaver debounceSaver;
|
||||||
|
|
||||||
|
private List<Pair<Long, LocalItem.LocalItemType>> deletedItems;
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////
|
||||||
// Fragment LifeCycle - Creation
|
// Fragment LifeCycle - Creation
|
||||||
@@ -65,6 +89,11 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL
|
|||||||
localPlaylistManager = new LocalPlaylistManager(database);
|
localPlaylistManager = new LocalPlaylistManager(database);
|
||||||
remotePlaylistManager = new RemotePlaylistManager(database);
|
remotePlaylistManager = new RemotePlaylistManager(database);
|
||||||
disposables = new CompositeDisposable();
|
disposables = new CompositeDisposable();
|
||||||
|
|
||||||
|
isLoadingComplete = new AtomicBoolean();
|
||||||
|
debounceSaver = new DebounceSaver(3000, this);
|
||||||
|
|
||||||
|
deletedItems = new ArrayList<>();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
@@ -91,10 +120,24 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL
|
|||||||
// Fragment LifeCycle - Views
|
// Fragment LifeCycle - Views
|
||||||
///////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void initViews(final View rootView, final Bundle savedInstanceState) {
|
||||||
|
super.initViews(rootView, savedInstanceState);
|
||||||
|
|
||||||
|
itemListAdapter.setUseItemHandle(true);
|
||||||
|
EmptyStateUtil.setEmptyStateComposable(
|
||||||
|
rootView.findViewById(R.id.empty_state_view),
|
||||||
|
EmptyStateSpec.Companion.getNoBookmarkedPlaylist()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void initListeners() {
|
protected void initListeners() {
|
||||||
super.initListeners();
|
super.initListeners();
|
||||||
|
|
||||||
|
itemTouchHelper = new ItemTouchHelper(getItemTouchCallback());
|
||||||
|
itemTouchHelper.attachToRecyclerView(itemsList);
|
||||||
|
|
||||||
itemListAdapter.setSelectedListener(new OnClickGesture<>() {
|
itemListAdapter.setSelectedListener(new OnClickGesture<>() {
|
||||||
@Override
|
@Override
|
||||||
public void selected(final LocalItem selectedItem) {
|
public void selected(final LocalItem selectedItem) {
|
||||||
@@ -102,7 +145,7 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL
|
|||||||
|
|
||||||
if (selectedItem instanceof PlaylistMetadataEntry) {
|
if (selectedItem instanceof PlaylistMetadataEntry) {
|
||||||
final PlaylistMetadataEntry entry = ((PlaylistMetadataEntry) selectedItem);
|
final PlaylistMetadataEntry entry = ((PlaylistMetadataEntry) selectedItem);
|
||||||
NavigationHelper.openLocalPlaylistFragment(fragmentManager, entry.uid,
|
NavigationHelper.openLocalPlaylistFragment(fragmentManager, entry.getUid(),
|
||||||
entry.name);
|
entry.name);
|
||||||
|
|
||||||
} else if (selectedItem instanceof PlaylistRemoteEntity) {
|
} else if (selectedItem instanceof PlaylistRemoteEntity) {
|
||||||
@@ -123,6 +166,14 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL
|
|||||||
showRemoteDeleteDialog((PlaylistRemoteEntity) selectedItem);
|
showRemoteDeleteDialog((PlaylistRemoteEntity) selectedItem);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void drag(final LocalItem selectedItem,
|
||||||
|
final RecyclerView.ViewHolder viewHolder) {
|
||||||
|
if (itemTouchHelper != null) {
|
||||||
|
itemTouchHelper.startDrag(viewHolder);
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -134,8 +185,13 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL
|
|||||||
public void startLoading(final boolean forceLoad) {
|
public void startLoading(final boolean forceLoad) {
|
||||||
super.startLoading(forceLoad);
|
super.startLoading(forceLoad);
|
||||||
|
|
||||||
Flowable.combineLatest(localPlaylistManager.getPlaylists(),
|
if (debounceSaver != null) {
|
||||||
remotePlaylistManager.getPlaylists(), PlaylistLocalItem::merge)
|
disposables.add(debounceSaver.getDebouncedSaver());
|
||||||
|
debounceSaver.setNoChangesToSave();
|
||||||
|
}
|
||||||
|
isLoadingComplete.set(false);
|
||||||
|
|
||||||
|
getMergedOrderedPlaylists(localPlaylistManager, remotePlaylistManager)
|
||||||
.onBackpressureLatest()
|
.onBackpressureLatest()
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.subscribe(getPlaylistsSubscriber());
|
.subscribe(getPlaylistsSubscriber());
|
||||||
@@ -149,6 +205,9 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL
|
|||||||
public void onPause() {
|
public void onPause() {
|
||||||
super.onPause();
|
super.onPause();
|
||||||
itemsListState = itemsList.getLayoutManager().onSaveInstanceState();
|
itemsListState = itemsList.getLayoutManager().onSaveInstanceState();
|
||||||
|
|
||||||
|
// Save on exit
|
||||||
|
saveImmediate();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -163,19 +222,27 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL
|
|||||||
}
|
}
|
||||||
|
|
||||||
databaseSubscription = null;
|
databaseSubscription = null;
|
||||||
|
itemTouchHelper = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onDestroy() {
|
public void onDestroy() {
|
||||||
super.onDestroy();
|
super.onDestroy();
|
||||||
|
if (debounceSaver != null) {
|
||||||
|
debounceSaver.getDebouncedSaveSignal().onComplete();
|
||||||
|
}
|
||||||
if (disposables != null) {
|
if (disposables != null) {
|
||||||
disposables.dispose();
|
disposables.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
debounceSaver = null;
|
||||||
disposables = null;
|
disposables = null;
|
||||||
localPlaylistManager = null;
|
localPlaylistManager = null;
|
||||||
remotePlaylistManager = null;
|
remotePlaylistManager = null;
|
||||||
itemsListState = null;
|
itemsListState = null;
|
||||||
|
|
||||||
|
isLoadingComplete = null;
|
||||||
|
deletedItems = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////
|
||||||
@@ -183,10 +250,12 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL
|
|||||||
///////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
private Subscriber<List<PlaylistLocalItem>> getPlaylistsSubscriber() {
|
private Subscriber<List<PlaylistLocalItem>> getPlaylistsSubscriber() {
|
||||||
return new Subscriber<List<PlaylistLocalItem>>() {
|
return new Subscriber<>() {
|
||||||
@Override
|
@Override
|
||||||
public void onSubscribe(final Subscription s) {
|
public void onSubscribe(final Subscription s) {
|
||||||
showLoading();
|
showLoading();
|
||||||
|
isLoadingComplete.set(false);
|
||||||
|
|
||||||
if (databaseSubscription != null) {
|
if (databaseSubscription != null) {
|
||||||
databaseSubscription.cancel();
|
databaseSubscription.cancel();
|
||||||
}
|
}
|
||||||
@@ -196,7 +265,10 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onNext(final List<PlaylistLocalItem> subscriptions) {
|
public void onNext(final List<PlaylistLocalItem> subscriptions) {
|
||||||
handleResult(subscriptions);
|
if (debounceSaver == null || !debounceSaver.getIsModified()) {
|
||||||
|
handleResult(subscriptions);
|
||||||
|
isLoadingComplete.set(true);
|
||||||
|
}
|
||||||
if (databaseSubscription != null) {
|
if (databaseSubscription != null) {
|
||||||
databaseSubscription.request(1);
|
databaseSubscription.request(1);
|
||||||
}
|
}
|
||||||
@@ -209,7 +281,8 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onComplete() { }
|
public void onComplete() {
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -244,12 +317,183 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
|
// Playlist Metadata Manipulation
|
||||||
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
|
private void changeLocalPlaylistName(final long id, final String name) {
|
||||||
|
if (localPlaylistManager == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (DEBUG) {
|
||||||
|
Log.d(TAG, "Updating playlist id=[" + id + "] "
|
||||||
|
+ "with new name=[" + name + "] items");
|
||||||
|
}
|
||||||
|
|
||||||
|
final Disposable disposable = localPlaylistManager.renamePlaylist(id, name)
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribe(longs -> { /*Do nothing on success*/ }, throwable -> showError(
|
||||||
|
new ErrorInfo(throwable,
|
||||||
|
UserAction.REQUESTED_BOOKMARK,
|
||||||
|
"Changing playlist name")));
|
||||||
|
disposables.add(disposable);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void deleteItem(final PlaylistLocalItem item) {
|
||||||
|
if (itemListAdapter == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
itemListAdapter.removeItem(item);
|
||||||
|
|
||||||
|
if (item instanceof PlaylistMetadataEntry) {
|
||||||
|
deletedItems.add(new Pair<>(item.getUid(),
|
||||||
|
LocalItem.LocalItemType.PLAYLIST_LOCAL_ITEM));
|
||||||
|
} else if (item instanceof PlaylistRemoteEntity) {
|
||||||
|
deletedItems.add(new Pair<>(item.getUid(),
|
||||||
|
LocalItem.LocalItemType.PLAYLIST_REMOTE_ITEM));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (debounceSaver != null) {
|
||||||
|
debounceSaver.setHasChangesToSave();
|
||||||
|
saveImmediate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void saveImmediate() {
|
||||||
|
if (itemListAdapter == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// List must be loaded and modified in order to save
|
||||||
|
if (isLoadingComplete == null || debounceSaver == null
|
||||||
|
|| !isLoadingComplete.get() || !debounceSaver.getIsModified()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final List<LocalItem> items = itemListAdapter.getItemsList();
|
||||||
|
final List<PlaylistMetadataEntry> localItemsUpdate = new ArrayList<>();
|
||||||
|
final List<Long> localItemsDeleteUid = new ArrayList<>();
|
||||||
|
final List<PlaylistRemoteEntity> remoteItemsUpdate = new ArrayList<>();
|
||||||
|
final List<Long> remoteItemsDeleteUid = new ArrayList<>();
|
||||||
|
|
||||||
|
// Calculate display index
|
||||||
|
for (int i = 0; i < items.size(); i++) {
|
||||||
|
final LocalItem item = items.get(i);
|
||||||
|
|
||||||
|
if (item instanceof PlaylistMetadataEntry
|
||||||
|
&& ((PlaylistMetadataEntry) item).getDisplayIndex() != i) {
|
||||||
|
((PlaylistMetadataEntry) item).setDisplayIndex(i);
|
||||||
|
localItemsUpdate.add((PlaylistMetadataEntry) item);
|
||||||
|
} else if (item instanceof PlaylistRemoteEntity
|
||||||
|
&& ((PlaylistRemoteEntity) item).getDisplayIndex() != i) {
|
||||||
|
((PlaylistRemoteEntity) item).setDisplayIndex(i);
|
||||||
|
remoteItemsUpdate.add((PlaylistRemoteEntity) item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find deleted items
|
||||||
|
for (final Pair<Long, LocalItem.LocalItemType> item : deletedItems) {
|
||||||
|
if (item.second.equals(LocalItem.LocalItemType.PLAYLIST_LOCAL_ITEM)) {
|
||||||
|
localItemsDeleteUid.add(item.first);
|
||||||
|
} else if (item.second.equals(LocalItem.LocalItemType.PLAYLIST_REMOTE_ITEM)) {
|
||||||
|
remoteItemsDeleteUid.add(item.first);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
deletedItems.clear();
|
||||||
|
|
||||||
|
// 1. Update local playlists
|
||||||
|
// 2. Update remote playlists
|
||||||
|
// 3. Set NoChangesToSave
|
||||||
|
disposables.add(localPlaylistManager.updatePlaylists(localItemsUpdate, localItemsDeleteUid)
|
||||||
|
.mergeWith(remotePlaylistManager.updatePlaylists(
|
||||||
|
remoteItemsUpdate, remoteItemsDeleteUid))
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribe(() -> {
|
||||||
|
if (debounceSaver != null) {
|
||||||
|
debounceSaver.setNoChangesToSave();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
throwable -> showError(new ErrorInfo(throwable,
|
||||||
|
UserAction.REQUESTED_BOOKMARK, "Saving playlist"))
|
||||||
|
));
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private ItemTouchHelper.SimpleCallback getItemTouchCallback() {
|
||||||
|
// if adding grid layout, also include ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT
|
||||||
|
// with an `if (shouldUseGridLayout()) ...`
|
||||||
|
return new ItemTouchHelper.SimpleCallback(ItemTouchHelper.UP | ItemTouchHelper.DOWN,
|
||||||
|
ItemTouchHelper.ACTION_STATE_IDLE) {
|
||||||
|
@Override
|
||||||
|
public int interpolateOutOfBoundsScroll(@NonNull final RecyclerView recyclerView,
|
||||||
|
final int viewSize,
|
||||||
|
final int viewSizeOutOfBounds,
|
||||||
|
final int totalSize,
|
||||||
|
final long msSinceStartScroll) {
|
||||||
|
final int standardSpeed = super.interpolateOutOfBoundsScroll(recyclerView,
|
||||||
|
viewSize, viewSizeOutOfBounds, totalSize, msSinceStartScroll);
|
||||||
|
final int minimumAbsVelocity = Math.max(MINIMUM_INITIAL_DRAG_VELOCITY,
|
||||||
|
Math.abs(standardSpeed));
|
||||||
|
return minimumAbsVelocity * (int) Math.signum(viewSizeOutOfBounds);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onMove(@NonNull final RecyclerView recyclerView,
|
||||||
|
@NonNull final RecyclerView.ViewHolder source,
|
||||||
|
@NonNull final RecyclerView.ViewHolder target) {
|
||||||
|
|
||||||
|
// Allow swap LocalBookmarkPlaylistItemHolder and RemoteBookmarkPlaylistItemHolder.
|
||||||
|
if (itemListAdapter == null
|
||||||
|
|| source.getItemViewType() != target.getItemViewType()
|
||||||
|
&& !(
|
||||||
|
(
|
||||||
|
(source instanceof LocalBookmarkPlaylistItemHolder)
|
||||||
|
|| (source instanceof RemoteBookmarkPlaylistItemHolder)
|
||||||
|
)
|
||||||
|
&& (
|
||||||
|
(target instanceof LocalBookmarkPlaylistItemHolder)
|
||||||
|
|| (target instanceof RemoteBookmarkPlaylistItemHolder)
|
||||||
|
))
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
final int sourceIndex = source.getBindingAdapterPosition();
|
||||||
|
final int targetIndex = target.getBindingAdapterPosition();
|
||||||
|
final boolean isSwapped = itemListAdapter.swapItems(sourceIndex, targetIndex);
|
||||||
|
if (isSwapped && debounceSaver != null) {
|
||||||
|
debounceSaver.setHasChangesToSave();
|
||||||
|
}
|
||||||
|
return isSwapped;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isLongPressDragEnabled() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isItemViewSwipeEnabled() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onSwiped(@NonNull final RecyclerView.ViewHolder viewHolder,
|
||||||
|
final int swipeDir) {
|
||||||
|
// Do nothing.
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////
|
||||||
// Utils
|
// Utils
|
||||||
///////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
private void showRemoteDeleteDialog(final PlaylistRemoteEntity item) {
|
private void showRemoteDeleteDialog(final PlaylistRemoteEntity item) {
|
||||||
showDeleteDialog(item.getName(), remotePlaylistManager.deletePlaylist(item.getUid()));
|
showDeleteDialog(item.getName(), item);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void showLocalDialog(final PlaylistMetadataEntry selectedItem) {
|
private void showLocalDialog(final PlaylistMetadataEntry selectedItem) {
|
||||||
@@ -257,7 +501,7 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL
|
|||||||
final String delete = getString(R.string.delete);
|
final String delete = getString(R.string.delete);
|
||||||
final String unsetThumbnail = getString(R.string.unset_playlist_thumbnail);
|
final String unsetThumbnail = getString(R.string.unset_playlist_thumbnail);
|
||||||
final boolean isThumbnailPermanent = localPlaylistManager
|
final boolean isThumbnailPermanent = localPlaylistManager
|
||||||
.getIsPlaylistThumbnailPermanent(selectedItem.uid);
|
.getIsPlaylistThumbnailPermanent(selectedItem.getUid());
|
||||||
|
|
||||||
final ArrayList<String> items = new ArrayList<>();
|
final ArrayList<String> items = new ArrayList<>();
|
||||||
items.add(rename);
|
items.add(rename);
|
||||||
@@ -270,13 +514,12 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL
|
|||||||
if (items.get(index).equals(rename)) {
|
if (items.get(index).equals(rename)) {
|
||||||
showRenameDialog(selectedItem);
|
showRenameDialog(selectedItem);
|
||||||
} else if (items.get(index).equals(delete)) {
|
} else if (items.get(index).equals(delete)) {
|
||||||
showDeleteDialog(selectedItem.name,
|
showDeleteDialog(selectedItem.name, selectedItem);
|
||||||
localPlaylistManager.deletePlaylist(selectedItem.uid));
|
|
||||||
} else if (isThumbnailPermanent && items.get(index).equals(unsetThumbnail)) {
|
} else if (isThumbnailPermanent && items.get(index).equals(unsetThumbnail)) {
|
||||||
final long thumbnailStreamId = localPlaylistManager
|
final long thumbnailStreamId = localPlaylistManager
|
||||||
.getAutomaticPlaylistThumbnailStreamId(selectedItem.uid);
|
.getAutomaticPlaylistThumbnailStreamId(selectedItem.getUid());
|
||||||
localPlaylistManager
|
localPlaylistManager
|
||||||
.changePlaylistThumbnail(selectedItem.uid, thumbnailStreamId, false)
|
.changePlaylistThumbnail(selectedItem.getUid(), thumbnailStreamId, false)
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.subscribe();
|
.subscribe();
|
||||||
}
|
}
|
||||||
@@ -298,13 +541,13 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL
|
|||||||
.setView(dialogBinding.getRoot())
|
.setView(dialogBinding.getRoot())
|
||||||
.setPositiveButton(R.string.rename_playlist, (dialog, which) ->
|
.setPositiveButton(R.string.rename_playlist, (dialog, which) ->
|
||||||
changeLocalPlaylistName(
|
changeLocalPlaylistName(
|
||||||
selectedItem.uid,
|
selectedItem.getUid(),
|
||||||
dialogBinding.dialogEditText.getText().toString()))
|
dialogBinding.dialogEditText.getText().toString()))
|
||||||
.setNegativeButton(R.string.cancel, null)
|
.setNegativeButton(R.string.cancel, null)
|
||||||
.show();
|
.show();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void showDeleteDialog(final String name, final Single<Integer> deleteReactor) {
|
private void showDeleteDialog(final String name, final PlaylistLocalItem item) {
|
||||||
if (activity == null || disposables == null) {
|
if (activity == null || disposables == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -313,35 +556,8 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL
|
|||||||
.setTitle(name)
|
.setTitle(name)
|
||||||
.setMessage(R.string.delete_playlist_prompt)
|
.setMessage(R.string.delete_playlist_prompt)
|
||||||
.setCancelable(true)
|
.setCancelable(true)
|
||||||
.setPositiveButton(R.string.delete, (dialog, i) ->
|
.setPositiveButton(R.string.delete, (dialog, i) -> deleteItem(item))
|
||||||
disposables.add(deleteReactor
|
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
|
||||||
.subscribe(ignored -> { /*Do nothing on success*/ }, throwable ->
|
|
||||||
showError(new ErrorInfo(throwable,
|
|
||||||
UserAction.REQUESTED_BOOKMARK,
|
|
||||||
"Deleting playlist")))))
|
|
||||||
.setNegativeButton(R.string.cancel, null)
|
.setNegativeButton(R.string.cancel, null)
|
||||||
.show();
|
.show();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void changeLocalPlaylistName(final long id, final String name) {
|
|
||||||
if (localPlaylistManager == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (DEBUG) {
|
|
||||||
Log.d(TAG, "Updating playlist id=[" + id + "] "
|
|
||||||
+ "with new name=[" + name + "] items");
|
|
||||||
}
|
|
||||||
|
|
||||||
localPlaylistManager.renamePlaylist(id, name);
|
|
||||||
final Disposable disposable = localPlaylistManager.renamePlaylist(id, name)
|
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
|
||||||
.subscribe(longs -> { /*Do nothing on success*/ }, throwable -> showError(
|
|
||||||
new ErrorInfo(throwable,
|
|
||||||
UserAction.REQUESTED_BOOKMARK,
|
|
||||||
"Changing playlist name")));
|
|
||||||
disposables.add(disposable);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,95 @@
|
|||||||
|
package org.schabi.newpipe.local.bookmark;
|
||||||
|
|
||||||
|
import org.schabi.newpipe.database.playlist.PlaylistLocalItem;
|
||||||
|
import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry;
|
||||||
|
import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity;
|
||||||
|
import org.schabi.newpipe.local.playlist.LocalPlaylistManager;
|
||||||
|
import org.schabi.newpipe.local.playlist.RemotePlaylistManager;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import io.reactivex.rxjava3.core.Flowable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Takes care of remote and local playlists at once, hence "merged".
|
||||||
|
*/
|
||||||
|
public final class MergedPlaylistManager {
|
||||||
|
|
||||||
|
private MergedPlaylistManager() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Flowable<List<PlaylistLocalItem>> getMergedOrderedPlaylists(
|
||||||
|
final LocalPlaylistManager localPlaylistManager,
|
||||||
|
final RemotePlaylistManager remotePlaylistManager) {
|
||||||
|
return Flowable.combineLatest(
|
||||||
|
localPlaylistManager.getPlaylists(),
|
||||||
|
remotePlaylistManager.getPlaylists(),
|
||||||
|
MergedPlaylistManager::merge
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Merge localPlaylists and remotePlaylists by the display index.
|
||||||
|
* If two items have the same display index, sort them in {@code CASE_INSENSITIVE_ORDER}.
|
||||||
|
*
|
||||||
|
* @param localPlaylists local playlists, already sorted by display index
|
||||||
|
* @param remotePlaylists remote playlists, already sorted by display index
|
||||||
|
* @return merged playlists
|
||||||
|
*/
|
||||||
|
public static List<PlaylistLocalItem> merge(
|
||||||
|
final List<PlaylistMetadataEntry> localPlaylists,
|
||||||
|
final List<PlaylistRemoteEntity> remotePlaylists) {
|
||||||
|
|
||||||
|
// This algorithm is similar to the merge operation in merge sort.
|
||||||
|
final List<PlaylistLocalItem> result = new ArrayList<>(
|
||||||
|
localPlaylists.size() + remotePlaylists.size());
|
||||||
|
final List<PlaylistLocalItem> itemsWithSameIndex = new ArrayList<>();
|
||||||
|
|
||||||
|
int i = 0;
|
||||||
|
int j = 0;
|
||||||
|
while (i < localPlaylists.size()) {
|
||||||
|
while (j < remotePlaylists.size()) {
|
||||||
|
if (remotePlaylists.get(j).getDisplayIndex()
|
||||||
|
<= localPlaylists.get(i).getDisplayIndex()) {
|
||||||
|
addItem(result, remotePlaylists.get(j), itemsWithSameIndex);
|
||||||
|
j++;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
addItem(result, localPlaylists.get(i), itemsWithSameIndex);
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
while (j < remotePlaylists.size()) {
|
||||||
|
addItem(result, remotePlaylists.get(j), itemsWithSameIndex);
|
||||||
|
j++;
|
||||||
|
}
|
||||||
|
addItemsWithSameIndex(result, itemsWithSameIndex);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void addItem(final List<PlaylistLocalItem> result,
|
||||||
|
final PlaylistLocalItem item,
|
||||||
|
final List<PlaylistLocalItem> itemsWithSameIndex) {
|
||||||
|
if (!itemsWithSameIndex.isEmpty()
|
||||||
|
&& itemsWithSameIndex.get(0).getDisplayIndex() != item.getDisplayIndex()) {
|
||||||
|
// The new item has a different display index, add previous items with same
|
||||||
|
// index to the result.
|
||||||
|
addItemsWithSameIndex(result, itemsWithSameIndex);
|
||||||
|
itemsWithSameIndex.clear();
|
||||||
|
}
|
||||||
|
itemsWithSameIndex.add(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void addItemsWithSameIndex(final List<PlaylistLocalItem> result,
|
||||||
|
final List<PlaylistLocalItem> itemsWithSameIndex) {
|
||||||
|
Collections.sort(itemsWithSameIndex,
|
||||||
|
Comparator.comparing(PlaylistLocalItem::getOrderingName,
|
||||||
|
Comparator.nullsLast(String.CASE_INSENSITIVE_ORDER)));
|
||||||
|
result.addAll(itemsWithSameIndex);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -155,14 +155,15 @@ public final class PlaylistAppendDialog extends PlaylistDialog {
|
|||||||
|
|
||||||
final Toast successToast = Toast.makeText(getContext(), toastText, Toast.LENGTH_SHORT);
|
final Toast successToast = Toast.makeText(getContext(), toastText, Toast.LENGTH_SHORT);
|
||||||
|
|
||||||
playlistDisposables.add(manager.appendToPlaylist(playlist.uid, streams)
|
playlistDisposables.add(manager.appendToPlaylist(playlist.getUid(), streams)
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.subscribe(ignored -> {
|
.subscribe(ignored -> {
|
||||||
successToast.show();
|
successToast.show();
|
||||||
|
|
||||||
if (playlist.thumbnailUrl.equals(PlaylistEntity.DEFAULT_THUMBNAIL)) {
|
if (playlist.thumbnailUrl != null
|
||||||
|
&& playlist.thumbnailUrl.equals(PlaylistEntity.DEFAULT_THUMBNAIL)) {
|
||||||
playlistDisposables.add(manager
|
playlistDisposables.add(manager
|
||||||
.changePlaylistThumbnail(playlist.uid, streams.get(0).getUid(),
|
.changePlaylistThumbnail(playlist.getUid(), streams.get(0).getUid(),
|
||||||
false)
|
false)
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.subscribe(ignore -> successToast.show()));
|
.subscribe(ignore -> successToast.show()));
|
||||||
|
|||||||
@@ -44,11 +44,11 @@ import androidx.lifecycle.ViewModelProvider
|
|||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
import androidx.recyclerview.widget.GridLayoutManager
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import com.evernote.android.state.State
|
||||||
import com.xwray.groupie.GroupieAdapter
|
import com.xwray.groupie.GroupieAdapter
|
||||||
import com.xwray.groupie.Item
|
import com.xwray.groupie.Item
|
||||||
import com.xwray.groupie.OnItemClickListener
|
import com.xwray.groupie.OnItemClickListener
|
||||||
import com.xwray.groupie.OnItemLongClickListener
|
import com.xwray.groupie.OnItemLongClickListener
|
||||||
import icepick.State
|
|
||||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||||
import io.reactivex.rxjava3.core.Single
|
import io.reactivex.rxjava3.core.Single
|
||||||
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||||
@@ -74,6 +74,7 @@ import org.schabi.newpipe.ktx.slideUp
|
|||||||
import org.schabi.newpipe.local.feed.item.StreamItem
|
import org.schabi.newpipe.local.feed.item.StreamItem
|
||||||
import org.schabi.newpipe.local.feed.service.FeedLoadService
|
import org.schabi.newpipe.local.feed.service.FeedLoadService
|
||||||
import org.schabi.newpipe.local.subscription.SubscriptionManager
|
import org.schabi.newpipe.local.subscription.SubscriptionManager
|
||||||
|
import org.schabi.newpipe.ui.emptystate.setEmptyStateComposable
|
||||||
import org.schabi.newpipe.util.DeviceUtils
|
import org.schabi.newpipe.util.DeviceUtils
|
||||||
import org.schabi.newpipe.util.Localization
|
import org.schabi.newpipe.util.Localization
|
||||||
import org.schabi.newpipe.util.NavigationHelper
|
import org.schabi.newpipe.util.NavigationHelper
|
||||||
@@ -132,6 +133,7 @@ class FeedFragment : BaseStateFragment<FeedState>() {
|
|||||||
override fun onViewCreated(rootView: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(rootView: View, savedInstanceState: Bundle?) {
|
||||||
// super.onViewCreated() calls initListeners() which require the binding to be initialized
|
// super.onViewCreated() calls initListeners() which require the binding to be initialized
|
||||||
_feedBinding = FragmentFeedBinding.bind(rootView)
|
_feedBinding = FragmentFeedBinding.bind(rootView)
|
||||||
|
feedBinding.emptyStateView.setEmptyStateComposable()
|
||||||
super.onViewCreated(rootView, savedInstanceState)
|
super.onViewCreated(rootView, savedInstanceState)
|
||||||
|
|
||||||
val factory = FeedViewModel.getFactory(requireContext(), groupId)
|
val factory = FeedViewModel.getFactory(requireContext(), groupId)
|
||||||
@@ -202,6 +204,7 @@ class FeedFragment : BaseStateFragment<FeedState>() {
|
|||||||
// Menu
|
// Menu
|
||||||
// /////////////////////////////////////////////////////////////////////////
|
// /////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
@Deprecated("Deprecated in Java")
|
||||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||||
super.onCreateOptionsMenu(menu, inflater)
|
super.onCreateOptionsMenu(menu, inflater)
|
||||||
|
|
||||||
@@ -212,6 +215,7 @@ class FeedFragment : BaseStateFragment<FeedState>() {
|
|||||||
inflater.inflate(R.menu.menu_feed_fragment, menu)
|
inflater.inflate(R.menu.menu_feed_fragment, menu)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Deprecated("Deprecated in Java")
|
||||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||||
if (item.itemId == R.id.menu_item_feed_help) {
|
if (item.itemId == R.id.menu_item_feed_help) {
|
||||||
val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
|
val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
|
||||||
@@ -253,7 +257,7 @@ class FeedFragment : BaseStateFragment<FeedState>() {
|
|||||||
viewModel.getShowFutureItemsFromPreferences()
|
viewModel.getShowFutureItemsFromPreferences()
|
||||||
)
|
)
|
||||||
|
|
||||||
AlertDialog.Builder(context!!)
|
AlertDialog.Builder(requireContext())
|
||||||
.setTitle(R.string.feed_hide_streams_title)
|
.setTitle(R.string.feed_hide_streams_title)
|
||||||
.setMultiChoiceItems(dialogItems, checkedDialogItems) { _, which, isChecked ->
|
.setMultiChoiceItems(dialogItems, checkedDialogItems) { _, which, isChecked ->
|
||||||
checkedDialogItems[which] = isChecked
|
checkedDialogItems[which] = isChecked
|
||||||
@@ -267,6 +271,7 @@ class FeedFragment : BaseStateFragment<FeedState>() {
|
|||||||
.show()
|
.show()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Deprecated("Deprecated in Java")
|
||||||
override fun onDestroyOptionsMenu() {
|
override fun onDestroyOptionsMenu() {
|
||||||
super.onDestroyOptionsMenu()
|
super.onDestroyOptionsMenu()
|
||||||
activity?.supportActionBar?.subtitle = null
|
activity?.supportActionBar?.subtitle = null
|
||||||
@@ -549,7 +554,7 @@ class FeedFragment : BaseStateFragment<FeedState>() {
|
|||||||
|
|
||||||
var typeface = Typeface.DEFAULT
|
var typeface = Typeface.DEFAULT
|
||||||
var backgroundSupplier = { ctx: Context ->
|
var backgroundSupplier = { ctx: Context ->
|
||||||
resolveDrawable(ctx, R.attr.selectableItemBackground)
|
resolveDrawable(ctx, android.R.attr.selectableItemBackground)
|
||||||
}
|
}
|
||||||
if (doCheck) {
|
if (doCheck) {
|
||||||
// If the uploadDate is null or true we should highlight the item
|
// If the uploadDate is null or true we should highlight the item
|
||||||
@@ -562,7 +567,7 @@ class FeedFragment : BaseStateFragment<FeedState>() {
|
|||||||
LayerDrawable(
|
LayerDrawable(
|
||||||
arrayOf(
|
arrayOf(
|
||||||
resolveDrawable(ctx, R.attr.dashed_border),
|
resolveDrawable(ctx, R.attr.dashed_border),
|
||||||
resolveDrawable(ctx, R.attr.selectableItemBackground)
|
resolveDrawable(ctx, android.R.attr.selectableItemBackground)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -165,7 +165,7 @@ class FeedViewModel(
|
|||||||
fun getFactory(context: Context, groupId: Long) = viewModelFactory {
|
fun getFactory(context: Context, groupId: Long) = viewModelFactory {
|
||||||
initializer {
|
initializer {
|
||||||
FeedViewModel(
|
FeedViewModel(
|
||||||
App.getApp(),
|
App.instance,
|
||||||
groupId,
|
groupId,
|
||||||
// Read initial value from preferences
|
// Read initial value from preferences
|
||||||
getShowPlayedItemsFromPreferences(context.applicationContext),
|
getShowPlayedItemsFromPreferences(context.applicationContext),
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import org.schabi.newpipe.extractor.stream.StreamType.POST_LIVE_STREAM
|
|||||||
import org.schabi.newpipe.extractor.stream.StreamType.VIDEO_STREAM
|
import org.schabi.newpipe.extractor.stream.StreamType.VIDEO_STREAM
|
||||||
import org.schabi.newpipe.util.Localization
|
import org.schabi.newpipe.util.Localization
|
||||||
import org.schabi.newpipe.util.StreamTypeUtil
|
import org.schabi.newpipe.util.StreamTypeUtil
|
||||||
import org.schabi.newpipe.util.image.PicassoHelper
|
import org.schabi.newpipe.util.image.CoilHelper
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
import java.util.function.Consumer
|
import java.util.function.Consumer
|
||||||
|
|
||||||
@@ -101,7 +101,7 @@ data class StreamItem(
|
|||||||
viewBinding.itemProgressView.visibility = View.GONE
|
viewBinding.itemProgressView.visibility = View.GONE
|
||||||
}
|
}
|
||||||
|
|
||||||
PicassoHelper.loadThumbnail(stream.thumbnailUrl).into(viewBinding.itemThumbnailView)
|
CoilHelper.loadThumbnail(viewBinding.itemThumbnailView, stream.thumbnailUrl)
|
||||||
|
|
||||||
if (itemVersion != ItemVersion.MINI) {
|
if (itemVersion != ItemVersion.MINI) {
|
||||||
viewBinding.itemAdditionalDetails.text =
|
viewBinding.itemAdditionalDetails.text =
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import android.app.PendingIntent
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
import android.graphics.drawable.Drawable
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.provider.Settings
|
import android.provider.Settings
|
||||||
@@ -16,20 +15,17 @@ import androidx.core.app.PendingIntentCompat
|
|||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.core.content.getSystemService
|
import androidx.core.content.getSystemService
|
||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
import com.squareup.picasso.Picasso
|
|
||||||
import com.squareup.picasso.Target
|
|
||||||
import org.schabi.newpipe.R
|
import org.schabi.newpipe.R
|
||||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem
|
import org.schabi.newpipe.extractor.stream.StreamInfoItem
|
||||||
import org.schabi.newpipe.local.feed.service.FeedUpdateInfo
|
import org.schabi.newpipe.local.feed.service.FeedUpdateInfo
|
||||||
import org.schabi.newpipe.util.NavigationHelper
|
import org.schabi.newpipe.util.NavigationHelper
|
||||||
import org.schabi.newpipe.util.image.PicassoHelper
|
import org.schabi.newpipe.util.image.CoilHelper
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Helper for everything related to show notifications about new streams to the user.
|
* Helper for everything related to show notifications about new streams to the user.
|
||||||
*/
|
*/
|
||||||
class NotificationHelper(val context: Context) {
|
class NotificationHelper(val context: Context) {
|
||||||
private val manager = NotificationManagerCompat.from(context)
|
private val manager = NotificationManagerCompat.from(context)
|
||||||
private val iconLoadingTargets = ArrayList<Target>()
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Show notifications for new streams from a single channel. The individual notifications are
|
* Show notifications for new streams from a single channel. The individual notifications are
|
||||||
@@ -68,51 +64,22 @@ class NotificationHelper(val context: Context) {
|
|||||||
summaryBuilder.setStyle(style)
|
summaryBuilder.setStyle(style)
|
||||||
|
|
||||||
// open the channel page when clicking on the summary notification
|
// open the channel page when clicking on the summary notification
|
||||||
|
val intent = NavigationHelper
|
||||||
|
.getChannelIntent(context, data.serviceId, data.url)
|
||||||
|
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||||
summaryBuilder.setContentIntent(
|
summaryBuilder.setContentIntent(
|
||||||
PendingIntentCompat.getActivity(
|
PendingIntentCompat.getActivity(context, data.pseudoId, intent, 0, false)
|
||||||
context,
|
|
||||||
data.pseudoId,
|
|
||||||
NavigationHelper
|
|
||||||
.getChannelIntent(context, data.serviceId, data.url)
|
|
||||||
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK),
|
|
||||||
0,
|
|
||||||
false
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// a Target is like a listener for image loading events
|
val avatarIcon =
|
||||||
val target = object : Target {
|
CoilHelper.loadBitmapBlocking(context, data.avatarUrl, R.drawable.ic_newpipe_triangle_white)
|
||||||
override fun onBitmapLoaded(bitmap: Bitmap, from: Picasso.LoadedFrom) {
|
|
||||||
// set channel icon only if there is actually one (for Android versions < 7.0)
|
|
||||||
summaryBuilder.setLargeIcon(bitmap)
|
|
||||||
|
|
||||||
// Show individual stream notifications, set channel icon only if there is actually
|
summaryBuilder.setLargeIcon(avatarIcon)
|
||||||
// one
|
|
||||||
showStreamNotifications(newStreams, data.serviceId, bitmap)
|
|
||||||
// Show summary notification
|
|
||||||
manager.notify(data.pseudoId, summaryBuilder.build())
|
|
||||||
|
|
||||||
iconLoadingTargets.remove(this) // allow it to be garbage-collected
|
// Show individual stream notifications, set channel icon only if there is actually one
|
||||||
}
|
showStreamNotifications(newStreams, data.serviceId, avatarIcon)
|
||||||
|
// Show summary notification
|
||||||
override fun onBitmapFailed(e: Exception, errorDrawable: Drawable) {
|
manager.notify(data.pseudoId, summaryBuilder.build())
|
||||||
// Show individual stream notifications
|
|
||||||
showStreamNotifications(newStreams, data.serviceId, null)
|
|
||||||
// Show summary notification
|
|
||||||
manager.notify(data.pseudoId, summaryBuilder.build())
|
|
||||||
iconLoadingTargets.remove(this) // allow it to be garbage-collected
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onPrepareLoad(placeHolderDrawable: Drawable) {
|
|
||||||
// Nothing to do
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// add the target to the list to hold a strong reference and prevent it from being garbage
|
|
||||||
// collected, since Picasso only holds weak references to targets
|
|
||||||
iconLoadingTargets.add(target)
|
|
||||||
|
|
||||||
PicassoHelper.loadNotificationIcon(data.avatarUrl).into(target)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun showStreamNotifications(
|
private fun showStreamNotifications(
|
||||||
|
|||||||
@@ -137,7 +137,7 @@ class NotificationWorker(
|
|||||||
.enqueueUniquePeriodicWork(
|
.enqueueUniquePeriodicWork(
|
||||||
WORK_TAG,
|
WORK_TAG,
|
||||||
if (force) {
|
if (force) {
|
||||||
ExistingPeriodicWorkPolicy.REPLACE
|
ExistingPeriodicWorkPolicy.CANCEL_AND_REENQUEUE
|
||||||
} else {
|
} else {
|
||||||
ExistingPeriodicWorkPolicy.KEEP
|
ExistingPeriodicWorkPolicy.KEEP
|
||||||
},
|
},
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user