mirror of
https://github.com/TeamNewPipe/NewPipe
synced 2026-01-15 05:58:02 +00:00
Compare commits
951 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
47f9ed08e9 | ||
|
|
ee3c06394d | ||
|
|
443ebc46d6 | ||
|
|
c42f29446d | ||
|
|
1030e09fc1 | ||
|
|
96b930cd07 | ||
|
|
de08edb831 | ||
|
|
af80d96b9e | ||
|
|
3c23fb0b13 | ||
|
|
750490cd2f | ||
|
|
579b8611be | ||
|
|
da12b92d75 | ||
|
|
a3f99bd781 | ||
|
|
7ae908a466 | ||
|
|
00767f4bf3 | ||
|
|
54f0b3d8b3 | ||
|
|
08eb70833d | ||
|
|
42aafd3a2d | ||
|
|
2acaefdb2a | ||
|
|
9c2cdd2513 | ||
|
|
01683aa816 | ||
|
|
85f701b94e | ||
|
|
ff7cfe4715 | ||
|
|
d3cd3d62b4 | ||
|
|
91c67b085b | ||
|
|
cd8c7ec3c0 | ||
|
|
2c51a7970d | ||
|
|
fb362022f7 | ||
|
|
2814ae6d3c | ||
|
|
7225199deb | ||
|
|
c08a4e851b | ||
|
|
9f8e8c0856 | ||
|
|
e2a7b9ac56 | ||
|
|
5e593f687d | ||
|
|
3223ec04e3 | ||
|
|
f388a1af67 | ||
|
|
c1fe5c8d07 | ||
|
|
608e73e2f2 | ||
|
|
2e538b8959 | ||
|
|
1278fc27ae | ||
|
|
be95d7fe0f | ||
|
|
377914f1d8 | ||
|
|
5bf439ad9e | ||
|
|
3b1b23ba2a | ||
|
|
9274e6417a | ||
|
|
dce6565af4 | ||
|
|
8b3aec5edb | ||
|
|
b0e4f947ea | ||
|
|
79060f0bfe | ||
|
|
91bcd8766a | ||
|
|
4e633504a8 | ||
|
|
144a10f7a6 | ||
|
|
72a2644f25 | ||
|
|
e865c4350e | ||
|
|
52cc4a0a05 | ||
|
|
e103e4817c | ||
|
|
d0637a8832 | ||
|
|
94f774b82d | ||
|
|
651b79d3ed | ||
|
|
9e5b9ca326 | ||
|
|
dfa606ef49 | ||
|
|
2886bc3b01 | ||
|
|
71c5aaa11e | ||
|
|
466db83375 | ||
|
|
17c0fffd73 | ||
|
|
8a069b497f | ||
|
|
af79479716 | ||
|
|
8cfe8c17e3 | ||
|
|
5108d75682 | ||
|
|
ac53196dcc | ||
|
|
1e652b159e | ||
|
|
ea07d7751b | ||
|
|
82de35d724 | ||
|
|
f55e8ea3aa | ||
|
|
7067ebdd12 | ||
|
|
03bb2123f2 | ||
|
|
e2f449f0c8 | ||
|
|
b16e972710 | ||
|
|
37cd71328c | ||
|
|
9b2c86a37b | ||
|
|
ce4dd33eab | ||
|
|
8bbc3e531c | ||
|
|
c5a06243a6 | ||
|
|
bebd2b449c | ||
|
|
658168eb8d | ||
|
|
6b23df0659 | ||
|
|
d59314801c | ||
|
|
0f45c69388 | ||
|
|
52542e04e8 | ||
|
|
7fc0a3841a | ||
|
|
22db4175f3 | ||
|
|
8fc935b09d | ||
|
|
07fb319e88 | ||
|
|
12a78a826d | ||
|
|
4a061f20ed | ||
|
|
f3be89b503 | ||
|
|
12acaf29dd | ||
|
|
683d9816cb | ||
|
|
8802582997 | ||
|
|
983c98d262 | ||
|
|
c38389672a | ||
|
|
93148400a2 | ||
|
|
194e43f5cb | ||
|
|
08c928e1d0 | ||
|
|
69dacb34b9 | ||
|
|
60c3a2dc9c | ||
|
|
b8e5e036b2 | ||
|
|
2f87305f2d | ||
|
|
15dc99f110 | ||
|
|
2d907706ea | ||
|
|
f5dbb07893 | ||
|
|
a437672dc1 | ||
|
|
388a4860b5 | ||
|
|
4b72ee53b0 | ||
|
|
d77c23ed34 | ||
|
|
31635c122e | ||
|
|
afef793fbb | ||
|
|
3bc2ec90ef | ||
|
|
a3e68c93f8 | ||
|
|
15e6f1cb3b | ||
|
|
89c540c520 | ||
|
|
6632720bc3 | ||
|
|
b5662c2d07 | ||
|
|
0f74c2463e | ||
|
|
fdfdf94cb9 | ||
|
|
8595078053 | ||
|
|
80be089ca9 | ||
|
|
96ab2f855e | ||
|
|
4206ae84c1 | ||
|
|
2f21523da9 | ||
|
|
6c1222ea32 | ||
|
|
ba50de236c | ||
|
|
bef8882a7c | ||
|
|
0d8b7e23e7 | ||
|
|
864c19e7dc | ||
|
|
4b0ed9de5d | ||
|
|
d18a34b766 | ||
|
|
0cf24c5d36 | ||
|
|
fa293e3415 | ||
|
|
1531a5112c | ||
|
|
e127db6fa6 | ||
|
|
49b1649348 | ||
|
|
9f503917c2 | ||
|
|
54ef604569 | ||
|
|
30ce906f72 | ||
|
|
1c20eabb48 | ||
|
|
f8c52c4dac | ||
|
|
345ba74d58 | ||
|
|
d2aaf152a0 | ||
|
|
7bf1f3dba6 | ||
|
|
452fe3a8e2 | ||
|
|
c25e523df6 | ||
|
|
65bb1dcdbf | ||
|
|
fe42206e94 | ||
|
|
dac47d9f52 | ||
|
|
83a3d11f38 | ||
|
|
03d5372525 | ||
|
|
a454a41b51 | ||
|
|
95631dba46 | ||
|
|
ee827407aa | ||
|
|
3aebfa22e9 | ||
|
|
72eb3b4415 | ||
|
|
3a40759cd2 | ||
|
|
6f44ced7b6 | ||
|
|
81843ddb6e | ||
|
|
23d14ab443 | ||
|
|
3d3d94655b | ||
|
|
a6515d5450 | ||
|
|
2d0da2c7a4 | ||
|
|
f05affa984 | ||
|
|
cd265fc31f | ||
|
|
3c21be8fa5 | ||
|
|
f681b0bb5a | ||
|
|
d7fbddf6f8 | ||
|
|
993c34911a | ||
|
|
4a7cfd1a6c | ||
|
|
402990dd9d | ||
|
|
41faf70da1 | ||
|
|
15e3b6301c | ||
|
|
5b9c28b93b | ||
|
|
6672169707 | ||
|
|
9ff1baefde | ||
|
|
552734faa5 | ||
|
|
7268e04361 | ||
|
|
45d8fef00c | ||
|
|
0f83497284 | ||
|
|
1475ff805f | ||
|
|
7907182e7e | ||
|
|
0f457127df | ||
|
|
064242d962 | ||
|
|
ddcbe27fd3 | ||
|
|
ee19ea66b3 | ||
|
|
6b490ee547 | ||
|
|
e127697fff | ||
|
|
558c9147a2 | ||
|
|
4147c7c1d1 | ||
|
|
45ef9b0278 | ||
|
|
fc0e709817 | ||
|
|
b67bf16d4f | ||
|
|
fb3be544ce | ||
|
|
53f5741317 | ||
|
|
07015973d2 | ||
|
|
215880207e | ||
|
|
41c4ab5739 | ||
|
|
ff8868f6a3 | ||
|
|
8c6e37d1d1 | ||
|
|
c90237c14c | ||
|
|
989bcbf895 | ||
|
|
19dd9d266a | ||
|
|
05370dbb94 | ||
|
|
f3edc69897 | ||
|
|
f6cad2d9cf | ||
|
|
dc67628ba5 | ||
|
|
37b8a9375f | ||
|
|
d71af9a625 | ||
|
|
a163d5461d | ||
|
|
a274baf5cd | ||
|
|
96eb1425f8 | ||
|
|
361760be0a | ||
|
|
eea2768633 | ||
|
|
d3562c70f5 | ||
|
|
e06342eacf | ||
|
|
e8d909553d | ||
|
|
b21d231e3a | ||
|
|
4058277b7a | ||
|
|
dd9772cde2 | ||
|
|
a924f819a9 | ||
|
|
156bbad5b5 | ||
|
|
2963cd5c6e | ||
|
|
7d6688f497 | ||
|
|
b056faa97f | ||
|
|
3ff00ff50e | ||
|
|
baee915db5 | ||
|
|
4e6dcc693b | ||
|
|
3750561b4d | ||
|
|
6b026557d4 | ||
|
|
1ee137bbda | ||
|
|
c92a90749e | ||
|
|
e806f8c4e6 | ||
|
|
8a5e2ffa57 | ||
|
|
ad405d9e0b | ||
|
|
b9ee14ac30 | ||
|
|
bb49b1cfb1 | ||
|
|
4fc9443b4f | ||
|
|
581ede022e | ||
|
|
f86fc03c46 | ||
|
|
75db002369 | ||
|
|
dbfa4e554b | ||
|
|
84d87a2e60 | ||
|
|
9e3577e77b | ||
|
|
41a0dc1abd | ||
|
|
950956ebf2 | ||
|
|
c000c1d455 | ||
|
|
c8e2ab4c83 | ||
|
|
397f93b079 | ||
|
|
09d137f740 | ||
|
|
81f740d409 | ||
|
|
1d2642f1e3 | ||
|
|
7cd3603bbb | ||
|
|
ec7de2a6dc | ||
|
|
3d1a3606c9 | ||
|
|
6472e9b6b6 | ||
|
|
2c88e9d068 | ||
|
|
4825a0a35f | ||
|
|
7adebbe989 | ||
|
|
122b0b0de4 | ||
|
|
744cfe5672 | ||
|
|
17724a901c | ||
|
|
7dc85af5fb | ||
|
|
c7daf32904 | ||
|
|
b2323859e5 | ||
|
|
4c8dca5300 | ||
|
|
68e7fcf8ee | ||
|
|
f78983b16b | ||
|
|
ef91214085 | ||
|
|
dc09a4621b | ||
|
|
2f99a217c3 | ||
|
|
6992b2c308 | ||
|
|
0d51eefbb9 | ||
|
|
aa28a85747 | ||
|
|
f18ee8e83d | ||
|
|
fb58967766 | ||
|
|
c3f1478fde | ||
|
|
e5c00a7ef4 | ||
|
|
769791af7a | ||
|
|
e632fab4d0 | ||
|
|
91611fcae4 | ||
|
|
6cd25d7e55 | ||
|
|
c9488eb042 | ||
|
|
c8516a04dc | ||
|
|
02d1b98b1c | ||
|
|
d8236bbedd | ||
|
|
1de21fb0c2 | ||
|
|
13cac07b8d | ||
|
|
bd9dcfb28a | ||
|
|
d5199eac3e | ||
|
|
7638d229c0 | ||
|
|
a641c5bb58 | ||
|
|
1e0c9f46ad | ||
|
|
4eb02f584e | ||
|
|
700c1b4b25 | ||
|
|
4b4337e078 | ||
|
|
38ce800685 | ||
|
|
2310e8c1d6 | ||
|
|
1b2b3a4f88 | ||
|
|
d11129a76b | ||
|
|
02789122a0 | ||
|
|
676bc02d52 | ||
|
|
8b807b0706 | ||
|
|
72dfe974ab | ||
|
|
316db0e4c6 | ||
|
|
010c607e40 | ||
|
|
3e099fb2a3 | ||
|
|
9c9730b152 | ||
|
|
9e44053e22 | ||
|
|
dee32c3dc5 | ||
|
|
344fbff59a | ||
|
|
e39a816bdc | ||
|
|
605b8fac5e | ||
|
|
dfba10f8ae | ||
|
|
48a1ab64b0 | ||
|
|
dd2cde3c1a | ||
|
|
1b9c2b37c5 | ||
|
|
eae1f8b597 | ||
|
|
18ce86c2ed | ||
|
|
d5f25e05d9 | ||
|
|
53303ac5d3 | ||
|
|
90cc8e2144 | ||
|
|
adf9badbf6 | ||
|
|
c35fe4f3f1 | ||
|
|
63291f8101 | ||
|
|
62efb588ef | ||
|
|
203ca9afc6 | ||
|
|
a23f941ac8 | ||
|
|
b0a10f0542 | ||
|
|
478ad42977 | ||
|
|
0764983ac6 | ||
|
|
2b2f1ee8f5 | ||
|
|
28f167fd99 | ||
|
|
272be36dd9 | ||
|
|
f933db8117 | ||
|
|
cddb9bccb9 | ||
|
|
b5ad24eb47 | ||
|
|
ad8f791f71 | ||
|
|
2e862b4ccc | ||
|
|
ecac897e7b | ||
|
|
702adb53a7 | ||
|
|
4ea962f523 | ||
|
|
acaf92d671 | ||
|
|
c673cb6157 | ||
|
|
c0f7b123a3 | ||
|
|
bc2f0f9f3e | ||
|
|
e9e2afa61a | ||
|
|
403154b2e1 | ||
|
|
e5fd24b0d1 | ||
|
|
8dc34274a1 | ||
|
|
467bd21de2 | ||
|
|
5c9705d94e | ||
|
|
85fb5827aa | ||
|
|
0bcc9bd3ba | ||
|
|
25e120bec1 | ||
|
|
7067deb328 | ||
|
|
f6efd302dc | ||
|
|
61972141ae | ||
|
|
af936bc646 | ||
|
|
d66f933c69 | ||
|
|
cf81c37683 | ||
|
|
d2306b0fd7 | ||
|
|
94dfabf3dc | ||
|
|
5522dc10b8 | ||
|
|
0ae04b8ead | ||
|
|
44cad27d0a | ||
|
|
5d59025b3c | ||
|
|
768bb0bbcd | ||
|
|
ac071b383f | ||
|
|
e0b1a6b88b | ||
|
|
ed86b1c572 | ||
|
|
b6c2bade73 | ||
|
|
b6b19b474e | ||
|
|
231b7492fb | ||
|
|
b4950fcb2e | ||
|
|
b79ea7b51b | ||
|
|
28c72e7f63 | ||
|
|
5fcc3b4dab | ||
|
|
51837ce36f | ||
|
|
ddaafb68c8 | ||
|
|
a744775fe7 | ||
|
|
50b85a7734 | ||
|
|
aab09c0c65 | ||
|
|
3ded6feddb | ||
|
|
c8802fe5d0 | ||
|
|
411b3129f9 | ||
|
|
a55acd38df | ||
|
|
e7773d8807 | ||
|
|
7edef8d5a2 | ||
|
|
03d2ca9f9f | ||
|
|
2271ea4281 | ||
|
|
afc8db8f81 | ||
|
|
4af49ee5a6 | ||
|
|
d7b29aae5c | ||
|
|
9f7a8407ca | ||
|
|
7eb13a9b93 | ||
|
|
7c9896beaf | ||
|
|
54d3bff26d | ||
|
|
a2050a5211 | ||
|
|
048743c062 | ||
|
|
e9bd2934c3 | ||
|
|
50634eb2b3 | ||
|
|
08489b81fb | ||
|
|
a2ff770afc | ||
|
|
e0ba9b3902 | ||
|
|
f11b5ae7a1 | ||
|
|
7baeb6eca7 | ||
|
|
658d988254 | ||
|
|
9d7e9289bb | ||
|
|
4e8519a1b9 | ||
|
|
12aac09c7b | ||
|
|
d7d87691cb | ||
|
|
731640997e | ||
|
|
64d7432852 | ||
|
|
e6fffc0d5b | ||
|
|
1c9f68bcae | ||
|
|
4fde62ff89 | ||
|
|
4c5fc7fa7c | ||
|
|
b633108a4c | ||
|
|
ceb55d0ede | ||
|
|
87c958b2e7 | ||
|
|
d844e0aba6 | ||
|
|
3d42da5ff5 | ||
|
|
1b869199f4 | ||
|
|
f3cd2f6c9d | ||
|
|
2e3e7f9bf2 | ||
|
|
92327dd9e3 | ||
|
|
d40b432f46 | ||
|
|
5b3137093f | ||
|
|
4fc9f2e5fd | ||
|
|
ce592f4baf | ||
|
|
2b3edcf2d1 | ||
|
|
f165f97bd9 | ||
|
|
4ec572372e | ||
|
|
a953aab9b4 | ||
|
|
672eb34049 | ||
|
|
a0b042091b | ||
|
|
b753705a84 | ||
|
|
f48ff610a3 | ||
|
|
93aed9f34c | ||
|
|
3cf94382e6 | ||
|
|
f52cb3bbe0 | ||
|
|
d45182cb5c | ||
|
|
22847c6c92 | ||
|
|
a70c51b71c | ||
|
|
02d417476e | ||
|
|
bc3139e5f9 | ||
|
|
c1f7b2653c | ||
|
|
72dbb9441e | ||
|
|
bbc13756f3 | ||
|
|
ba0876b43b | ||
|
|
c0d41661e8 | ||
|
|
b2e2551e33 | ||
|
|
ac371e6fb4 | ||
|
|
108af48b76 | ||
|
|
a225ac5deb | ||
|
|
920695f90a | ||
|
|
49fc57eee9 | ||
|
|
b61d44aaa6 | ||
|
|
f36fd2f7b2 | ||
|
|
7e26748dc4 | ||
|
|
ba6fdecbae | ||
|
|
f791e83380 | ||
|
|
dd7f914b8d | ||
|
|
7667b2ce59 | ||
|
|
62d36126ea | ||
|
|
8272b2508b | ||
|
|
70354eb73e | ||
|
|
63083ac0c3 | ||
|
|
9346f9b0f3 | ||
|
|
605e5d265c | ||
|
|
25456b15e7 | ||
|
|
ebbe7ef944 | ||
|
|
60a272e70a | ||
|
|
672fcb9ce3 | ||
|
|
870d50ebcd | ||
|
|
b62b3e91a0 | ||
|
|
b022d90303 | ||
|
|
02af529551 | ||
|
|
dd9cc619ed | ||
|
|
75c9e959de | ||
|
|
fb8afec1bf | ||
|
|
a2887034a6 | ||
|
|
7eb5aa1bc5 | ||
|
|
08ebd7d39a | ||
|
|
9ea263f72e | ||
|
|
e4a2d2f3c1 | ||
|
|
892b4a15f6 | ||
|
|
fda0a550fd | ||
|
|
638825cdff | ||
|
|
6a1d81fcf3 | ||
|
|
8afd44a72f | ||
|
|
22c5135740 | ||
|
|
4d51ebc37a | ||
|
|
433c6dc33b | ||
|
|
ed4fdadd4d | ||
|
|
298e96b821 | ||
|
|
9006667b4d | ||
|
|
abbf71982d | ||
|
|
57110717d3 | ||
|
|
c3b5444281 | ||
|
|
7a542975ca | ||
|
|
490aff5846 | ||
|
|
1dfc036ead | ||
|
|
360d6b998c | ||
|
|
be7307cf39 | ||
|
|
12096ab050 | ||
|
|
225f23ce02 | ||
|
|
9c15ee7285 | ||
|
|
8dd617fc6b | ||
|
|
ae8e72f34b | ||
|
|
fc52a6e871 | ||
|
|
722b47b86f | ||
|
|
3a09039b93 | ||
|
|
669a35bc78 | ||
|
|
81fa0c1558 | ||
|
|
ed408b2094 | ||
|
|
3bc661f583 | ||
|
|
cf9b482be2 | ||
|
|
1d935b46f9 | ||
|
|
520ac2e935 | ||
|
|
c6316abbce | ||
|
|
2dfe837c35 | ||
|
|
3c2ea7697c | ||
|
|
faa7a91764 | ||
|
|
f629a4d206 | ||
|
|
4b7c37e919 | ||
|
|
a4c9732916 | ||
|
|
f8f2dfce4b | ||
|
|
5284072b8d | ||
|
|
e603dddc54 | ||
|
|
15691ba41a | ||
|
|
a555aab3e7 | ||
|
|
88f1c3a808 | ||
|
|
0e6668636d | ||
|
|
d0f4d8b132 | ||
|
|
cfdcb92fa3 | ||
|
|
039bd5d413 | ||
|
|
5ffba55b4a | ||
|
|
57ca281c80 | ||
|
|
46f74b908a | ||
|
|
703f1550d8 | ||
|
|
8bfd380b89 | ||
|
|
43e91ae4ae | ||
|
|
023a2c1d9c | ||
|
|
d931d058d9 | ||
|
|
a825253b7f | ||
|
|
d9086300f3 | ||
|
|
f18a7c91ca | ||
|
|
556aad0114 | ||
|
|
05f6ea6401 | ||
|
|
43d0543b9f | ||
|
|
e95637f7b7 | ||
|
|
4cd7c42b9e | ||
|
|
0787d62254 | ||
|
|
b061423847 | ||
|
|
dbd90299bd | ||
|
|
1faf1b261c | ||
|
|
c6ead351c0 | ||
|
|
bbcfdf2969 | ||
|
|
36e72d5a41 | ||
|
|
f8297a8a9b | ||
|
|
a4503eb609 | ||
|
|
a1cb3e59d6 | ||
|
|
ef94458249 | ||
|
|
1b05c404d5 | ||
|
|
5de455bb86 | ||
|
|
acdfee5c25 | ||
|
|
a6d6ed6474 | ||
|
|
87e7d95966 | ||
|
|
d37ee1e0dc | ||
|
|
1d33e7ab49 | ||
|
|
2027b743b4 | ||
|
|
7e27e73532 | ||
|
|
3705a1adad | ||
|
|
793b88a7d4 | ||
|
|
2928df0cc9 | ||
|
|
4f5e772157 | ||
|
|
f7a0b9951e | ||
|
|
44128f9145 | ||
|
|
6eaff5ca6a | ||
|
|
c0664c1cb6 | ||
|
|
e229e5355d | ||
|
|
52189fc5df | ||
|
|
314964c5f9 | ||
|
|
fcef783bbb | ||
|
|
9c5ac069d7 | ||
|
|
160f9df64e | ||
|
|
bdbb9bead2 | ||
|
|
e4dfce9ee2 | ||
|
|
6fbb601802 | ||
|
|
94b4c76749 | ||
|
|
8715e7dd98 | ||
|
|
ccc2d892c1 | ||
|
|
d1ce8e7baa | ||
|
|
82fbbbecac | ||
|
|
bf029ddd9f | ||
|
|
af5f0c042a | ||
|
|
4e15f0ddac | ||
|
|
b566355c4f | ||
|
|
5c31dff72d | ||
|
|
d69672e113 | ||
|
|
a209e87c69 | ||
|
|
71610a365f | ||
|
|
44860f2ea7 | ||
|
|
967bdf8f08 | ||
|
|
02aa6fcab0 | ||
|
|
712985ced1 | ||
|
|
0683dafa55 | ||
|
|
6f1958d398 | ||
|
|
85fbd2560d | ||
|
|
65f2730261 | ||
|
|
21bcadeecb | ||
|
|
bd0427c79f | ||
|
|
241054fd26 | ||
|
|
d8888e3495 | ||
|
|
137d9e6d6e | ||
|
|
d0cbd1e663 | ||
|
|
da51e1ed72 | ||
|
|
76803bfcb1 | ||
|
|
c248741c00 | ||
|
|
759a078ce0 | ||
|
|
a536311d56 | ||
|
|
9dd2a82b7d | ||
|
|
4d50a66e40 | ||
|
|
e6c56cacc6 | ||
|
|
c3b9465aa3 | ||
|
|
5f3b8bea52 | ||
|
|
0e4c8ea8af | ||
|
|
f9ab23bb4a | ||
|
|
9f8b2264a2 | ||
|
|
52cc3f10c1 | ||
|
|
1d61bb58f5 | ||
|
|
a3440cc8ef | ||
|
|
51c60e5261 | ||
|
|
c3349e18a5 | ||
|
|
12e46e0a36 | ||
|
|
f8caed139a | ||
|
|
a2297fb5b8 | ||
|
|
26c39381a8 | ||
|
|
a4742ad9e9 | ||
|
|
23a6973291 | ||
|
|
340a84e583 | ||
|
|
4291877830 | ||
|
|
8f6d608a43 | ||
|
|
45dd98e639 | ||
|
|
2ac265a6f5 | ||
|
|
e100806fd9 | ||
|
|
c7f75bf7d1 | ||
|
|
4bf5ddbfe9 | ||
|
|
32dffb577c | ||
|
|
a9623f8e6a | ||
|
|
bc74bb6bf6 | ||
|
|
d32450255c | ||
|
|
896aec5295 | ||
|
|
d42a534fc3 | ||
|
|
398007ca90 | ||
|
|
551e8df8b8 | ||
|
|
dc0a28b93d | ||
|
|
644396149b | ||
|
|
a25bb2618a | ||
|
|
0e12cdea7c | ||
|
|
903296014a | ||
|
|
cd713db029 | ||
|
|
bdd16e06e0 | ||
|
|
4c632810ec | ||
|
|
f451bdbfa4 | ||
|
|
bfac73b992 | ||
|
|
2b41f710a8 | ||
|
|
5924edb289 | ||
|
|
5ceec31adf | ||
|
|
e2791cdf0f | ||
|
|
50f3b08c59 | ||
|
|
2aebf6ceaf | ||
|
|
7ceea2cd8d | ||
|
|
0cb801179c | ||
|
|
1822d21676 | ||
|
|
7fd2ebc252 | ||
|
|
f709ac16f8 | ||
|
|
74173317de | ||
|
|
3874e16187 | ||
|
|
39722a5563 | ||
|
|
1f9ad12593 | ||
|
|
52c136439e | ||
|
|
cd86ed3877 | ||
|
|
1d85661ab9 | ||
|
|
736cefed5a | ||
|
|
fa8630ddae | ||
|
|
4a2bd7bd7b | ||
|
|
a9e21a35ea | ||
|
|
fd4e1b8d2c | ||
|
|
420f0505ae | ||
|
|
b58f7856a1 | ||
|
|
44a6429267 | ||
|
|
472bde9eea | ||
|
|
c422f65935 | ||
|
|
f5962375f8 | ||
|
|
4e33f2dcb6 | ||
|
|
dce874bbc7 | ||
|
|
7d69dfa62a | ||
|
|
a56f17cc3b | ||
|
|
7be7a32d70 | ||
|
|
a7dd3af4e5 | ||
|
|
63fdc100d6 | ||
|
|
9e2ece78dd | ||
|
|
cebcaf4d6a | ||
|
|
4a242e43a7 | ||
|
|
d8f442cc89 | ||
|
|
f6923e073e | ||
|
|
f02c6be10d | ||
|
|
5ba3ef0a25 | ||
|
|
9458b9f37d | ||
|
|
ca282f2be8 | ||
|
|
0cde08c46e | ||
|
|
bec8512c7b | ||
|
|
46e7da4e21 | ||
|
|
c7b8bd3436 | ||
|
|
1721817fdb | ||
|
|
d57bfde604 | ||
|
|
3167ab3ba0 | ||
|
|
8f559965f6 | ||
|
|
35e005caaa | ||
|
|
6c25ce56a3 | ||
|
|
baa12c7069 | ||
|
|
e2b044d2ee | ||
|
|
621af8d812 | ||
|
|
efd038a536 | ||
|
|
0b2629e910 | ||
|
|
a9b5ef3bd3 | ||
|
|
2a24532e1d | ||
|
|
88c4195260 | ||
|
|
c5f2eb1dd8 | ||
|
|
384d964827 | ||
|
|
253526e565 | ||
|
|
2e2dbaf77f | ||
|
|
43133df2ad | ||
|
|
eef568b24c | ||
|
|
e7d5011f42 | ||
|
|
36c198fc33 | ||
|
|
75a8edf20f | ||
|
|
81107df53f | ||
|
|
a932bc2503 | ||
|
|
f4e2eca256 | ||
|
|
08d5dfa49c | ||
|
|
e7f339a946 | ||
|
|
d3375a921d | ||
|
|
a2eb810df0 | ||
|
|
6e576a165c | ||
|
|
dfa941a9e7 | ||
|
|
1584028995 | ||
|
|
14dab85ff0 | ||
|
|
403e336a64 | ||
|
|
2aa5f68b7b | ||
|
|
56ea526cce | ||
|
|
96f5cd9f17 | ||
|
|
64efb89cce | ||
|
|
4d5b68792b | ||
|
|
85d813a94b | ||
|
|
e9b008ee84 | ||
|
|
b795c5f017 | ||
|
|
1e4686463b | ||
|
|
4e9631a8d8 | ||
|
|
3a83062670 | ||
|
|
79102a20d2 | ||
|
|
2e053ea25a | ||
|
|
fd3d46c813 | ||
|
|
ab838fd84f | ||
|
|
9ca2691a2c | ||
|
|
7c3f5a62c5 | ||
|
|
6711dae4e0 | ||
|
|
a73a4afcad | ||
|
|
4ea2d8e7ba | ||
|
|
bb386fea16 | ||
|
|
82cdb0fdb3 | ||
|
|
a94dacf03c | ||
|
|
de312eb768 | ||
|
|
29aa1de4e3 | ||
|
|
09435a1b63 | ||
|
|
85e864a01e | ||
|
|
573839c0ff | ||
|
|
9c636f5ee2 | ||
|
|
f78d2a5ed8 | ||
|
|
48c2c156cb | ||
|
|
435813355f | ||
|
|
e30a552b6c | ||
|
|
22a4a4b2df | ||
|
|
5ac418aa61 | ||
|
|
d8a0a74d47 | ||
|
|
3931c0d200 | ||
|
|
e26607fbd1 | ||
|
|
a63683e6b8 | ||
|
|
83b198f6fe | ||
|
|
23f6e1084b | ||
|
|
99335bab7a | ||
|
|
33fbc889fb | ||
|
|
201e5ee09d | ||
|
|
c398308872 | ||
|
|
090c063644 | ||
|
|
ec40c8ed1e | ||
|
|
78a99526a9 | ||
|
|
25914b0263 | ||
|
|
aaa3e20c5a | ||
|
|
0da8e28651 | ||
|
|
d7dcfa5729 | ||
|
|
65824ff64d | ||
|
|
63cad7ebb0 | ||
|
|
b996fa7eef | ||
|
|
5ebf3726ed | ||
|
|
484c852efd | ||
|
|
25cf8dc20a | ||
|
|
cb1a138140 | ||
|
|
384ca66205 | ||
|
|
46bfec66cb | ||
|
|
afe06b379f | ||
|
|
a9e85abd7f | ||
|
|
08d4651ef0 | ||
|
|
62b4f333bb | ||
|
|
02b0909829 | ||
|
|
ae39b31c68 | ||
|
|
e5a1438673 | ||
|
|
72d305b283 | ||
|
|
785c0376f8 | ||
|
|
0bdf8de38e | ||
|
|
6c575511be | ||
|
|
9767e98e50 | ||
|
|
79deff3261 | ||
|
|
0782410a14 | ||
|
|
f5d015e8f9 | ||
|
|
74ad488f4a | ||
|
|
0db3406ad8 | ||
|
|
6e377dd3c5 | ||
|
|
be676ad93c | ||
|
|
f00cffd17e | ||
|
|
0803d9f2b5 | ||
|
|
841fb4cfc5 | ||
|
|
8b3e32b6eb | ||
|
|
90de75968d | ||
|
|
2de9d7b4a7 | ||
|
|
a9ab2f54ea | ||
|
|
a1432e939f | ||
|
|
cae160b5be | ||
|
|
aa4e5da146 | ||
|
|
1061fca6a3 | ||
|
|
e4885e3c52 | ||
|
|
a98c0bdec7 | ||
|
|
d6e0bd8c26 | ||
|
|
e01ef42d31 | ||
|
|
92910eb227 | ||
|
|
cdfe686322 | ||
|
|
553943ab93 | ||
|
|
32df4d39a4 | ||
|
|
1281ea858c | ||
|
|
30a303f873 | ||
|
|
fdb6679d2d | ||
|
|
7145b117cc | ||
|
|
4698d07323 | ||
|
|
2142f05a88 | ||
|
|
40a2df847b | ||
|
|
6063ff063b | ||
|
|
547a1a9970 | ||
|
|
8c52a812d9 | ||
|
|
4eef498d24 | ||
|
|
32b0bdb98c | ||
|
|
edfe0f9c30 | ||
|
|
eef418a757 | ||
|
|
218f25c171 | ||
|
|
f02df6d80c | ||
|
|
da4d379b22 | ||
|
|
f13f4cc5d2 | ||
|
|
a79badd783 | ||
|
|
2702700d10 | ||
|
|
267686fd37 | ||
|
|
e5df2f65b8 | ||
|
|
d6decc05d7 | ||
|
|
d85afd6435 | ||
|
|
2fb86364ab | ||
|
|
c972940338 | ||
|
|
6abdd2a6d8 | ||
|
|
9e9d1a04e4 | ||
|
|
ae9349e36c | ||
|
|
4031777606 | ||
|
|
9591f14551 | ||
|
|
06d10cf9aa | ||
|
|
0113ad5e14 | ||
|
|
e58feadba9 | ||
|
|
360f5ac6f7 | ||
|
|
e846f69e38 | ||
|
|
fa1d7ffac3 | ||
|
|
272d589518 | ||
|
|
6ab4787e97 | ||
|
|
060f09ff55 | ||
|
|
f47ae3668f | ||
|
|
56cd84c1fe | ||
|
|
a2eead521f | ||
|
|
a2fd5ae20c | ||
|
|
543440e38d | ||
|
|
0b64382ef6 | ||
|
|
bede758507 | ||
|
|
5532666ad5 | ||
|
|
63cff25616 | ||
|
|
5e2735aaa2 | ||
|
|
6fc0d8fce4 | ||
|
|
e0c1ca1209 | ||
|
|
3dc4ed1764 | ||
|
|
f63a4ee2ae | ||
|
|
c96bdfcb32 | ||
|
|
2a99e0e435 | ||
|
|
5ffc667bea | ||
|
|
21b8df0375 | ||
|
|
b78ac7d2e9 | ||
|
|
114dc8ffa0 | ||
|
|
eea43d5a73 | ||
|
|
bcb1cf6603 | ||
|
|
6a0c5a874c | ||
|
|
1e8b3826dc | ||
|
|
7efe62ee80 | ||
|
|
febb21a01d | ||
|
|
cb4e6159c4 | ||
|
|
1164ea52f9 | ||
|
|
0f75024e03 | ||
|
|
1e09a1768e | ||
|
|
7c78d963d9 | ||
|
|
b57ecae565 | ||
|
|
89317d4abc | ||
|
|
c5dd3dc7a9 | ||
|
|
ccc46971b4 | ||
|
|
6ad4b425e4 | ||
|
|
761e01c3b9 | ||
|
|
3b0045917c | ||
|
|
a102fc9cad | ||
|
|
448989f32f | ||
|
|
2fc26bc154 | ||
|
|
1812249d37 | ||
|
|
14bbaccb9f | ||
|
|
d2b03afcf4 | ||
|
|
1cac3895dc | ||
|
|
01aab25889 | ||
|
|
96d731dfc7 | ||
|
|
3b1c4b043d | ||
|
|
e8b8391868 | ||
|
|
cd0a87785e | ||
|
|
7fdb6e1425 | ||
|
|
621f049a5c | ||
|
|
eb6968fb3f | ||
|
|
a3c4a10721 |
67
.github/CONTRIBUTING.md
vendored
67
.github/CONTRIBUTING.md
vendored
@@ -3,9 +3,9 @@ NewPipe contribution guidelines
|
||||
|
||||
## 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.
|
||||
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
|
||||
|
||||
@@ -25,22 +25,61 @@ You'll see exactly what is sent, be able to add your comments, and then send it.
|
||||
|
||||
## Code contribution
|
||||
|
||||
* If you want to help out with an existing bug report or feature request, leave a comment on that issue saying you want to try your hand at it.
|
||||
* If there is no existing issue for what you want to work on, open a new one describing your changes. This gives the team and the community a chance to give feedback before you spend time on something that is already in development, should be done differently, or should be avoided completely.
|
||||
* Stick to NewPipe's style conventions of [checkStyle](https://github.com/checkstyle/checkstyle). It runs each time you build the project.
|
||||
* Do not bring non-free software (e.g. binary blobs) into the project. Make sure you do not introduce Google
|
||||
libraries.
|
||||
### Guidelines
|
||||
|
||||
* Stick to NewPipe's *style conventions* of [checkStyle](https://github.com/checkstyle/checkstyle) and [ktlint](https://github.com/pinterest/ktlint). They run each time you build the project.
|
||||
* Stick to [F-Droid contribution guidelines](https://f-droid.org/wiki/page/Inclusion_Policy).
|
||||
* Make changes on a separate branch with a meaningful name, not on the _master_ branch or the _dev_ branch. This is commonly known as *feature branch workflow*. You may then send your changes as a pull request (PR) on GitHub.
|
||||
* Please test (compile and run) your code before submitting changes! Ideally, provide test feedback in the PR description. Untested code will **not** be merged!
|
||||
* Make sure your PR is up-to-date with the rest of the code. Often, a simple click on "Update branch" will do the job, but if not, you must rebase the dev branch manually and resolve the problems on your own. You can find help [on the wiki](https://github.com/TeamNewPipe/NewPipe/wiki/How-to-merge-a-PR). That makes the maintainers' jobs way easier.
|
||||
* Please show intention to maintain your features and code after you contribute a PR. Unmaintained code is a hassle for core developers. If you do not intend to maintain features you plan to contribute, please rethink your submission, or clearly state that in the PR description.
|
||||
* In particular **do not bring non-free software** (e.g. binary blobs) into the project. Make sure you do not introduce any closed-source library from Google.
|
||||
|
||||
### Before starting development
|
||||
|
||||
* If you want to help out with an existing bug report or feature request, **leave a comment** on that issue saying you want to try your hand at it.
|
||||
* If there is no existing issue for what you want to work on, **open a new one** describing the changes you are planning to introduce. This gives the team and the community a chance to give **feedback** before you spend time on something that is already in development, should be done differently, or should be avoided completely.
|
||||
* Please show **intention to maintain your features** and code after you contribute a PR. Unmaintained code is a hassle for core developers. If you do not intend to maintain features you plan to contribute, please rethink your submission, or clearly state that in the PR description.
|
||||
* Create PRs that cover only **one specific issue/solution/bug**. Do not create PRs that are huge monoliths and could have been split into multiple independent contributions.
|
||||
* NewPipe uses [NewPipeExtractor](https://github.com/TeamNewPipe/NewPipeExtractor) to fetch data from services. If you need to change something there, you must test your changes in NewPipe. Telling NewPipe to use your extractor version can be accomplished by editing the `app/build.gradle` file: the comments under the "NewPipe libraries" section of `dependencies` will help you out.
|
||||
|
||||
### Kotlin in NewPipe
|
||||
* NewPipe will remain mostly Java for time being
|
||||
* Contributions containing a simple conversion from Java to Kotlin should be avoided. Conversions to Kotlin should only be done if Kotlin actually brings improvements like bug fixes or better performance which are not, or only with much more effort, implementable in Java. The core team sees Java as an easier to learn and generally well adopted programming language.
|
||||
|
||||
### Creating a Pull Request (PR)
|
||||
|
||||
* Make changes on a **separate branch** with a meaningful name, not on the _master_ branch or the _dev_ branch. This is commonly known as *feature branch workflow*. You may then send your changes as a pull request (PR) on GitHub.
|
||||
* Please **test** (compile and run) your code before submitting changes! Ideally, provide test feedback in the PR description. Untested code will **not** be merged!
|
||||
* Respond if someone requests changes or otherwise raises issues about your PRs.
|
||||
* Send PRs that only cover one specific issue/solution/bug. Do not send PRs that are huge and consist of multiple independent solutions.
|
||||
* Try to figure out yourself why builds on our CI fail.
|
||||
* Make sure your PR is **up-to-date** with the rest of the code. Often, a simple click on "Update branch" will do the job, but if not, you must *rebase* your branch on the `dev` branch manually and resolve the conflicts on your own. You can find help [on the wiki](https://github.com/TeamNewPipe/NewPipe/wiki/How-to-merge-a-PR). Doing this makes the maintainers' job way easier.
|
||||
|
||||
## IDE setup & building the app
|
||||
|
||||
### Basic setup
|
||||
|
||||
NewPipe is developed using [Android Studio](https://developer.android.com/studio/). Learn more about how to install it and how it works in the [official documentation](https://developer.android.com/studio/intro). In particular, make sure you have accepted Android Studio's SDK licences. Once Android Studio is ready, setting up the NewPipe project is fairly simple:
|
||||
- Clone the NewPipe repository with `git clone https://github.com/TeamNewPipe/NewPipe.git` (or use the link from your own fork, if you want to open a PR).
|
||||
- Open the folder you just cloned with Android Studio.
|
||||
- Build and run it just like you would do with any other app, with the green triangle in the top bar.
|
||||
|
||||
You may find [SonarLint](https://www.sonarlint.org/intellij)'s **inspections** useful in helping you to write good code and prevent bugs.
|
||||
|
||||
### checkStyle setup
|
||||
|
||||
The [checkStyle](https://github.com/checkstyle/checkstyle) plugin verifies that Java code abides by the project style. It runs automatically each time you build the project. If you want to view errors directly in the editor, instead of having to skim through the build output, you can install an Android Studio plugin:
|
||||
- Go to `File -> Settings -> Plugins`, search for `checkstyle` and install `CheckStyle-IDEA`.
|
||||
- Go to `File -> Settings -> Tools -> Checkstyle`.
|
||||
- Add NewPipe's configuration file by clicking the `+` in the right toolbar of the "Configuration File" list.
|
||||
- Under the "Use a local Checkstyle file" bullet, click on `Browse` and pick the file named `checkstyle.xml` in the project's root folder.
|
||||
- Enable "Store relative to project location" so that moving the directory around does not create issues.
|
||||
- Insert a description in the top bar, then click `Next` and then `Finish`.
|
||||
- Activate the configuration file you just added by enabling the checkbox on the left.
|
||||
- Click `Ok` and you are done.
|
||||
|
||||
### ktlint setup
|
||||
|
||||
The [ktlint](https://github.com/pinterest/ktlint) plugin does the same job as checkStyle for Kotlin files. Installing the related plugin is as simple as going to `File -> Settings -> Plugins`, searching for `ktlint` and installing `Ktlint (unofficial)`.
|
||||
|
||||
## Communication
|
||||
|
||||
* The #newpipe channel on Libera Chat (`ircs://irc.libera.chat:6697/newpipe`) has the core team and other developers in it. [Click here for webchat](https://web.libera.chat/#newpipe)!
|
||||
* You can also use a Matrix account to join the NewPipe channel at [#newpipe:libera.chat](https://matrix.to/#/#newpipe:libera.chat).
|
||||
* Post suggestions, changes, ideas etc. on GitHub or IRC.
|
||||
* You can also use a Matrix account to join the NewPipe channel at [#newpipe:libera.chat](https://matrix.to/#/#newpipe:libera.chat). Some convenient clients, available both for phone and desktop, are listed at that link.
|
||||
* You can post your suggestions, changes, ideas etc. on either GitHub or IRC.
|
||||
|
||||
1
.github/FUNDING.yml
vendored
1
.github/FUNDING.yml
vendored
@@ -1 +1,2 @@
|
||||
liberapay: TeamNewPipe
|
||||
custom: 'https://newpipe.net/donate/'
|
||||
|
||||
65
.github/ISSUE_TEMPLATE/bug_report.md
vendored
65
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -1,65 +0,0 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a bug report to help us improve
|
||||
labels: bug
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
<!--
|
||||
Oh no, a bug! It happens. Thanks for reporting an issue with NewPipe. To make it easier for us to help you please enter detailed information in the template we have provided below. If a section isn't relevant, just delete it, though it would be helpful to still provide as much detail as possible.
|
||||
-->
|
||||
|
||||
<!-- IF YOU DON'T FILL IN THE TEMPLATE PROPERLY, YOUR ISSUE IS LIABLE TO BE CLOSED. If you feel tired/lazy right now, open your issue some other time. We'll wait. -->
|
||||
|
||||
<!-- The comments between these brackets won't show up in the submitted issue (as you can see in the Preview). -->
|
||||
|
||||
### Checklist
|
||||
<!-- This checklist is COMPULSORY. The first box has been checked for you to show you how it is done. -->
|
||||
|
||||
- [x] I am using the latest version - x.xx.x <!-- Check https://github.com/TeamNewPipe/NewPipe/releases -->
|
||||
- [ ] I checked, but didn't find any duplicates (open OR closed) of this issue in the repo. <!-- Seriously, check. O_O -->
|
||||
- [ ] I have read the contribution guidelines given at https://github.com/TeamNewPipe/NewPipe/blob/HEAD/.github/CONTRIBUTING.md.
|
||||
- [ ] This issue contains only one bug. I will open one issue for every bug report I want to file.
|
||||
|
||||
### Steps to reproduce the bug
|
||||
<!--
|
||||
1. Go to '...'
|
||||
2. Press on '....'
|
||||
3. Swipe down to '....'
|
||||
-->
|
||||
|
||||
<!-- If you can't cause the bug to show up again reliably (and hence don't have a proper set of steps to give us), please still try to give as many details as possible on how you think you encountered the bug. -->
|
||||
|
||||
|
||||
|
||||
### Actual behaviour
|
||||
<!-- Tell us what happens with the steps given above. -->
|
||||
|
||||
|
||||
|
||||
### Expected behavior
|
||||
<!-- Tell us what you expect to happen. -->
|
||||
|
||||
|
||||
|
||||
### Screenshots/Screen recordings
|
||||
<!-- If applicable, add screenshots or a screen recording to help explain your problem. GitHub supports uploading them directly in the issue text box. If your file is too big for Github to accept, feel free to paste a link from an image/video hoster here instead. -->
|
||||
|
||||
<!-- DON'T POST SCREENSHOTS OF THE ERROR PAGE. Use the buttons given on the error page to paste the error as text in the Logs section below. -->
|
||||
|
||||
|
||||
|
||||
### Logs
|
||||
<!-- If your bug includes a crash (where you're shown the Error Report page with a bunch of info), tap on "Copy formatted report" at the bottom and paste it here: -->
|
||||
|
||||
<!-- That's right, here! -->
|
||||
|
||||
|
||||
|
||||
<!-- Please fill this out when you do not provide a log generate by NewPipe -->
|
||||
|
||||
### Device info
|
||||
|
||||
- Android version/Custom ROM version:
|
||||
- Device model:
|
||||
113
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
113
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
@@ -0,0 +1,113 @@
|
||||
name: Bug report
|
||||
description: Create a bug report to help us improve
|
||||
labels: [bug]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thank you for helping to make NewPipe better by reporting a bug. :hugs:
|
||||
|
||||
Please fill in as much information as possible about your bug so that we don't have to play "information ping-pong" and can help you immediately.
|
||||
|
||||
- type: checkboxes
|
||||
id: checklist
|
||||
attributes:
|
||||
label: "Checklist"
|
||||
options:
|
||||
- label: "I am able to reproduce the bug with the [latest version](https://github.com/TeamNewPipe/NewPipe/releases/latest)."
|
||||
required: true
|
||||
- label: "I made sure that there are *no existing issues* - [open](https://github.com/TeamNewPipe/NewPipe/issues) or [closed](https://github.com/TeamNewPipe/NewPipe/issues?q=is%3Aissue+is%3Aclosed) - which I could contribute my information to."
|
||||
required: true
|
||||
- label: "I have taken the time to fill in all the required details. I understand that the bug report will be dismissed otherwise."
|
||||
required: true
|
||||
- label: "This issue contains only one bug."
|
||||
required: true
|
||||
- label: "I have read and understood the [contribution guidelines](https://github.com/TeamNewPipe/NewPipe/blob/dev/.github/CONTRIBUTING.md)."
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: app-version
|
||||
attributes:
|
||||
label: Affected version
|
||||
description: "In which NewPipe version did you encounter the bug?"
|
||||
placeholder: "x.xx.x - Can be seen in the app from the 'About' section in the sidebar"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: steps-to-reproduce
|
||||
attributes:
|
||||
label: Steps to reproduce the bug
|
||||
description: |
|
||||
What did you do for the bug to show up?
|
||||
|
||||
If you can't cause the bug to show up again reliably (and hence don't have a proper set of steps to give us), please still try to give as many details as possible on how you think you encountered the bug.
|
||||
placeholder: |
|
||||
1. Go to '...'
|
||||
2. Press on '....'
|
||||
3. Swipe down to '....'
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: expected-behavior
|
||||
attributes:
|
||||
label: Expected behavior
|
||||
description: |
|
||||
Tell us what you expect to happen.
|
||||
|
||||
- type: textarea
|
||||
id: actual-behavior
|
||||
attributes:
|
||||
label: Actual behavior
|
||||
description: |
|
||||
Tell us what happens with the steps given above.
|
||||
|
||||
- type: textarea
|
||||
id: screen-media
|
||||
attributes:
|
||||
label: Screenshots/Screen recordings
|
||||
description: |
|
||||
A picture or video is worth a thousand words.
|
||||
|
||||
If applicable, add screenshots or a screen recording to help explain your problem.
|
||||
GitHub supports uploading them directly in the text box.
|
||||
If your file is too big for Github to accept, try to compress it (ZIP-file) or feel free to paste a link to an image/video hoster here instead.
|
||||
|
||||
:heavy_exclamation_mark: DON'T POST SCREENSHOTS OF THE ERROR PAGE.
|
||||
Instead, follow the instructions in the "Logs" section below.
|
||||
|
||||
- type: textarea
|
||||
id: logs
|
||||
attributes:
|
||||
label: Logs
|
||||
description: |
|
||||
If your bug includes a crash (where you're shown the Error Report page with a bunch of info), tap on "Copy formatted report" at the bottom and paste it here.
|
||||
|
||||
- type: input
|
||||
id: device-os-info
|
||||
attributes:
|
||||
label: Affected Android/Custom ROM version
|
||||
description: |
|
||||
With what operating system (+ version) did you encounter the bug?
|
||||
placeholder: "Example: Android 12 / LineageOS 18.1"
|
||||
|
||||
- type: input
|
||||
id: device-model-info
|
||||
attributes:
|
||||
label: Affected device model
|
||||
description: |
|
||||
On what device did you encounter the bug?
|
||||
placeholder: "Example: Huawei P20 lite (ANE-LX1) / Samsung Galaxy S20"
|
||||
|
||||
- type: textarea
|
||||
id: additional-information
|
||||
attributes:
|
||||
label: Additional information
|
||||
description: |
|
||||
Any other information you'd like to include, for instance that
|
||||
* the affected device is foldable or a TV
|
||||
* you have disabled all animations on your device
|
||||
* your cat disabled your network connection
|
||||
* ...
|
||||
|
||||
46
.github/ISSUE_TEMPLATE/feature_request.md
vendored
46
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@@ -1,46 +0,0 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
labels: enhancement
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
<!-- IF YOU DON'T FILL IN THE TEMPLATE PROPERLY, YOUR ISSUE IS LIABLE TO BE CLOSED. If you feel tired/lazy right now, open your issue some other time. We'll wait. -->
|
||||
|
||||
|
||||
<!-- The comments between these brackets won't show up in the submitted issue (as you can see in the Preview). -->
|
||||
|
||||
### Checklist
|
||||
<!-- This checklist is COMPULSORY. The first box has been checked for you to show you how it is done. -->
|
||||
|
||||
- [x] I checked, but didn't find any duplicates (open OR closed) of this issue in the repo. <!-- Seriously, check. O_O -->
|
||||
- [ ] I have read the contribution guidelines given at https://github.com/TeamNewPipe/NewPipe/blob/HEAD/.github/CONTRIBUTING.md.
|
||||
- [ ] This issue contains only one feature request. I will open one issue for every feature I want to request.
|
||||
|
||||
|
||||
#### Describe the feature you want
|
||||
<!-- A clear and concise description of what you wish should happen.
|
||||
Example: *I think it would be nice if you add feature Y which makes X possible.*
|
||||
|
||||
Optionally, also describe alternatives you've considered.
|
||||
Example: *Z is also a good alternative. Not as good as Y, but at least...* or *I considered Z, but that didn't turn out to be a good idea because...* -->
|
||||
|
||||
|
||||
|
||||
#### Is your feature request related to a problem? Please describe it
|
||||
<!-- A clear and concise description of what the problem is. Maybe the developers and the community could brainstorm and come up with a better solution to your problem. If they exist, link to related Issues and/or PRs for developers to keep track easier.
|
||||
Example: *I want to do X, but there is no way to do it.* -->
|
||||
|
||||
|
||||
|
||||
#### Additional context
|
||||
<!-- Add any other context, like screenshots, about the feature request here.
|
||||
Example: *Here's a photo of my cat!* -->
|
||||
|
||||
|
||||
|
||||
#### How will you/everyone benefit from this feature?
|
||||
<!-- Convince us! How does it change your NewPipe experience and/or your life?
|
||||
The better this paragraph is, the more likely a developer will think about working on it.
|
||||
Example: *This feature will help us colonize the galaxy! -->
|
||||
|
||||
51
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal file
51
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal file
@@ -0,0 +1,51 @@
|
||||
name: Feature request
|
||||
description: Suggest an idea for this project
|
||||
labels: [enhancement]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thank you for helping to make NewPipe better by suggesting a feature. :hugs:
|
||||
|
||||
Your ideas are highly welcome! The app is made for you, the users, after all.
|
||||
|
||||
- type: checkboxes
|
||||
id: checklist
|
||||
attributes:
|
||||
label: "Checklist"
|
||||
options:
|
||||
- label: "I made sure that there are *no existing issues* - [open](https://github.com/TeamNewPipe/NewPipe/issues) or [closed](https://github.com/TeamNewPipe/NewPipe/issues?q=is%3Aissue+is%3Aclosed) - which I could contribute my information to."
|
||||
required: true
|
||||
- label: "I'm aware that this is a request for NewPipe itself and that requests for adding a new service need to be made at [NewPipeExtractor](https://github.com/TeamNewPipe/NewPipeExtractor/issues)."
|
||||
required: true
|
||||
- label: "I have taken the time to fill in all the required details. I understand that the feature request will be dismissed otherwise."
|
||||
required: true
|
||||
- label: "This issue contains only one feature request."
|
||||
required: true
|
||||
- label: "I have read and understood the [contribution guidelines](https://github.com/TeamNewPipe/NewPipe/blob/dev/.github/CONTRIBUTING.md)."
|
||||
required: true
|
||||
|
||||
|
||||
- type: textarea
|
||||
id: feature-description
|
||||
attributes:
|
||||
label: Feature description
|
||||
description: |
|
||||
Explain how you want the app's look or behavior to change to suit your needs.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: why-is-the-feature-requested
|
||||
attributes:
|
||||
label: Why do you want this feature?
|
||||
description: |
|
||||
Describe any problem or limitation you come across while using the app which would be solved by this feature.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: additional-information
|
||||
attributes:
|
||||
label: Additional information
|
||||
description: Any other information you'd like to include, for instance sketches, mockups, pictures of cats, etc.
|
||||
35
.github/ISSUE_TEMPLATE/question.yml
vendored
Normal file
35
.github/ISSUE_TEMPLATE/question.yml
vendored
Normal file
@@ -0,0 +1,35 @@
|
||||
name: Question
|
||||
description: Ask about anything NewPipe-related
|
||||
labels: [question]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thanks for taking the time to fill out this issue! :hugs:
|
||||
|
||||
Note that you can also ask questions on our [IRC channel](https://web.libera.chat/#newpipe).
|
||||
|
||||
- type: checkboxes
|
||||
id: checklist
|
||||
attributes:
|
||||
label: "Checklist"
|
||||
options:
|
||||
- label: "I made sure that there are *no existing issues* - [open](https://github.com/TeamNewPipe/NewPipe/issues) or [closed](https://github.com/TeamNewPipe/NewPipe/issues?q=is%3Aissue+is%3Aclosed) - which I could contribute my information to."
|
||||
required: true
|
||||
- label: "I have taken the time to fill in all the required details. I understand that the question will be dismissed otherwise."
|
||||
required: true
|
||||
- label: "I have read and understood the [contribution guidelines](https://github.com/TeamNewPipe/NewPipe/blob/dev/.github/CONTRIBUTING.md)."
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: what-is-the-question
|
||||
attributes:
|
||||
label: What is/are your question(s)?
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: additional-information
|
||||
attributes:
|
||||
label: Additional information
|
||||
description: Any other information you'd like to include, for instance sketches, mockups, pictures of cats, etc.
|
||||
9
.github/PULL_REQUEST_TEMPLATE.md
vendored
9
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -12,18 +12,23 @@
|
||||
- create clones
|
||||
- take over the world
|
||||
|
||||
#### Before/After Screenshots/Screen Record
|
||||
<!-- If your PR changes the app's UI in any way, please include screenshots or a video showing exactly what changed, so that developers and users can pinpoint it easily. Delete this if it doesn't apply to your PR.-->
|
||||
- Before:
|
||||
- After:
|
||||
|
||||
#### Fixes the following issue(s)
|
||||
<!-- Prefix issues with "Fixes" so that GitHub closes them when the PR is merged (note that each "Fixes #" should be in its own item). Also add any other relevant links. -->
|
||||
- Fixes #
|
||||
|
||||
#### Relies on the following changes
|
||||
<!-- Delete this if it doesn't apply to you. -->
|
||||
<!-- Delete this if it doesn't apply to your PR. -->
|
||||
-
|
||||
|
||||
#### APK testing
|
||||
<!-- Use a new, meaningfully named branch. The name is used as a suffix for the app ID to allow installing and testing multiple versions of NewPipe, e.g. "commentfix", if your PR implements a bugfix for comments. (No names like "patch-0" and "feature-1".) -->
|
||||
<!-- Remove the following line if you directly link the APK created by the CI pipeline. Directly linking is preferred if you need to let users test.-->
|
||||
On the website the APK can be found by going to the "Checks" tab below the title and then on "artifacts" on the right.
|
||||
The APK can be found by going to the "Checks" tab below the title. On the left pane, click on "CI", scroll down to "artifacts" and click "app" to download the zip file which contains the debug APK of this PR.
|
||||
|
||||
#### Due diligence
|
||||
- [ ] I read the [contribution guidelines](https://github.com/TeamNewPipe/NewPipe/blob/HEAD/.github/CONTRIBUTING.md).
|
||||
|
||||
115
.github/workflows/ci.yml
vendored
115
.github/workflows/ci.yml
vendored
@@ -1,14 +1,31 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
pull_request:
|
||||
branches:
|
||||
- dev
|
||||
- master
|
||||
paths-ignore:
|
||||
- 'README.md'
|
||||
- 'doc/**'
|
||||
- 'fastlane/**'
|
||||
- 'assets/**'
|
||||
- '.github/**/*.md'
|
||||
- '.github/FUNDING.yml'
|
||||
- '.github/ISSUE_TEMPLATE/**'
|
||||
push:
|
||||
branches:
|
||||
- dev
|
||||
- master
|
||||
paths-ignore:
|
||||
- 'README.md'
|
||||
- 'doc/**'
|
||||
- 'fastlane/**'
|
||||
- 'assets/**'
|
||||
- '.github/**/*.md'
|
||||
- '.github/FUNDING.yml'
|
||||
- '.github/ISSUE_TEMPLATE/**'
|
||||
|
||||
jobs:
|
||||
build-and-test-jvm:
|
||||
@@ -22,82 +39,78 @@ jobs:
|
||||
if: github.event_name == 'pull_request'
|
||||
run: git checkout -B ${{ github.head_ref }}
|
||||
|
||||
- name: set up JDK 8
|
||||
- name: set up JDK 11
|
||||
uses: actions/setup-java@v2
|
||||
with:
|
||||
java-version: 8
|
||||
distribution: "adopt"
|
||||
|
||||
- name: Cache Gradle dependencies
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ~/.gradle/caches
|
||||
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }}
|
||||
restore-keys: ${{ runner.os }}-gradle
|
||||
java-version: 11
|
||||
distribution: "temurin"
|
||||
cache: 'gradle'
|
||||
|
||||
- name: Build debug APK and run jvm tests
|
||||
run: ./gradlew assembleDebug lintDebug testDebugUnitTest --stacktrace
|
||||
run: ./gradlew assembleDebug lintDebug testDebugUnitTest --stacktrace -DskipFormatKtlint
|
||||
|
||||
- name: Upload APK
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: app
|
||||
path: app/build/outputs/apk/debug/*.apk
|
||||
|
||||
test-android:
|
||||
# macos has hardware acceleration. See android-emulator-runner action
|
||||
runs-on: macos-latest
|
||||
timeout-minutes: 20
|
||||
strategy:
|
||||
matrix:
|
||||
api-level: [21, 29]
|
||||
# api-level 19 is min sdk, but throws errors related to desugaring
|
||||
api-level: [ 21, 29 ]
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: set up JDK 8
|
||||
- name: set up JDK 11
|
||||
uses: actions/setup-java@v2
|
||||
with:
|
||||
java-version: 8
|
||||
distribution: "adopt"
|
||||
|
||||
- name: Cache Gradle dependencies
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ~/.gradle/caches
|
||||
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }}
|
||||
restore-keys: ${{ runner.os }}-gradle
|
||||
java-version: 11
|
||||
distribution: "temurin"
|
||||
cache: 'gradle'
|
||||
|
||||
- name: Run android tests
|
||||
uses: reactivecircus/android-emulator-runner@v2
|
||||
with:
|
||||
api-level: ${{ matrix.api-level }}
|
||||
script: ./gradlew connectedCheck
|
||||
# sonar:
|
||||
# runs-on: ubuntu-latest
|
||||
# steps:
|
||||
# - uses: actions/checkout@v2
|
||||
# with:
|
||||
# fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis
|
||||
# workaround to emulator bug: https://github.com/ReactiveCircus/android-emulator-runner/issues/160
|
||||
emulator-build: 7425822
|
||||
script: ./gradlew connectedCheck --stacktrace
|
||||
|
||||
- name: Upload test report when tests fail # because the printed out stacktrace (console) is too short, see also #7553
|
||||
uses: actions/upload-artifact@v2
|
||||
if: failure()
|
||||
with:
|
||||
name: android-test-report-api${{ matrix.api-level }}
|
||||
path: app/build/reports/androidTests/connected/**
|
||||
|
||||
# - name: Set up JDK 11
|
||||
# uses: actions/setup-java@v2
|
||||
# with:
|
||||
# java-version: 11 # Sonar requires JDK 11
|
||||
# distribution: "adopt"
|
||||
sonar:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis
|
||||
|
||||
# - name: Cache SonarCloud packages
|
||||
# uses: actions/cache@v2
|
||||
# with:
|
||||
# path: ~/.sonar/cache
|
||||
# key: ${{ runner.os }}-sonar
|
||||
# restore-keys: ${{ runner.os }}-sonar
|
||||
- name: Set up JDK 11
|
||||
uses: actions/setup-java@v2
|
||||
with:
|
||||
java-version: 11 # Sonar requires JDK 11
|
||||
distribution: "temurin"
|
||||
cache: 'gradle'
|
||||
|
||||
# - name: Cache Gradle packages
|
||||
# uses: actions/cache@v2
|
||||
# with:
|
||||
# path: ~/.gradle/caches
|
||||
# key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }}
|
||||
# restore-keys: ${{ runner.os }}-gradle
|
||||
- name: Cache SonarCloud packages
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ~/.sonar/cache
|
||||
key: ${{ runner.os }}-sonar
|
||||
restore-keys: ${{ runner.os }}-sonar
|
||||
|
||||
# - name: Build and analyze
|
||||
# env:
|
||||
# GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any
|
||||
# SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
||||
# run: ./gradlew build sonarqube --info
|
||||
- name: Build and analyze
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any
|
||||
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
||||
run: ./gradlew build sonarqube --info
|
||||
|
||||
130
.github/workflows/image-minimizer.js
vendored
Normal file
130
.github/workflows/image-minimizer.js
vendored
Normal file
@@ -0,0 +1,130 @@
|
||||
/*
|
||||
* Script for minimizing big images (jpg,gif,png) when they are uploaded to GitHub and not edited otherwise
|
||||
*/
|
||||
module.exports = async ({github, context}) => {
|
||||
const IGNORE_KEY = '<!-- IGNORE IMAGE MINIFY -->';
|
||||
const IGNORE_ALT_NAME_END = 'ignoreImageMinify';
|
||||
// Targeted maximum height
|
||||
const IMG_MAX_HEIGHT_PX = 600;
|
||||
// maximum width of GitHub issues/comments
|
||||
const IMG_MAX_WIDTH_PX = 800;
|
||||
// all images that have a lower aspect ratio (-> have a smaller width) than this will be minimized
|
||||
const MIN_ASPECT_RATIO = IMG_MAX_WIDTH_PX / IMG_MAX_HEIGHT_PX
|
||||
|
||||
// Get the body of the image
|
||||
let initialBody = null;
|
||||
if (context.eventName == 'issue_comment') {
|
||||
initialBody = context.payload.comment.body;
|
||||
} else if (context.eventName == 'issues') {
|
||||
initialBody = context.payload.issue.body;
|
||||
} else {
|
||||
console.log('Aborting: No body found');
|
||||
return;
|
||||
}
|
||||
console.log(`Found body: \n${initialBody}\n`);
|
||||
|
||||
// Check if we should ignore the currently processing element
|
||||
if (initialBody.includes(IGNORE_KEY)) {
|
||||
console.log('Ignoring: Body contains IGNORE_KEY');
|
||||
return;
|
||||
}
|
||||
|
||||
// Regex for finding images (simple variant) 
|
||||
const REGEX_IMAGE_LOOKUP = /\!\[(.*)\]\((https:\/\/[-a-z0-9]+\.githubusercontent\.com\/\d+\/[-0-9a-f]{32,512}\.(jpg|gif|png))\)/gm;
|
||||
|
||||
// Check if we found something
|
||||
let foundSimpleImages = REGEX_IMAGE_LOOKUP.test(initialBody);
|
||||
if (!foundSimpleImages) {
|
||||
console.log('Found no simple images to process');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Found at least one simple image to process');
|
||||
|
||||
// Require the probe lib for getting the image dimensions
|
||||
const probe = require('probe-image-size');
|
||||
|
||||
var wasMatchModified = false;
|
||||
|
||||
// Try to find and replace the images with minimized ones
|
||||
let newBody = await replaceAsync(initialBody, REGEX_IMAGE_LOOKUP, async (match, g1, g2) => {
|
||||
console.log(`Found match '${match}'`);
|
||||
|
||||
if (g1.endsWith(IGNORE_ALT_NAME_END)) {
|
||||
console.log(`Ignoring match '${match}': IGNORE_ALT_NAME_END`);
|
||||
return match;
|
||||
}
|
||||
|
||||
let shouldModify = false;
|
||||
try {
|
||||
console.log(`Probing ${g2}`);
|
||||
let probeResult = await probe(g2);
|
||||
if (probeResult == null) {
|
||||
throw 'No probeResult';
|
||||
}
|
||||
if (probeResult.hUnits != 'px') {
|
||||
throw `Unexpected probeResult.hUnits (expected px but got ${probeResult.hUnits})`;
|
||||
}
|
||||
if (probeResult.height <= 0) {
|
||||
throw `Unexpected probeResult.height (height is invalid: ${probeResult.height})`;
|
||||
}
|
||||
if (probeResult.wUnits != 'px') {
|
||||
throw `Unexpected probeResult.wUnits (expected px but got ${probeResult.wUnits})`;
|
||||
}
|
||||
if (probeResult.width <= 0) {
|
||||
throw `Unexpected probeResult.width (width is invalid: ${probeResult.width})`;
|
||||
}
|
||||
console.log(`Probing resulted in ${probeResult.width}x${probeResult.height}px`);
|
||||
|
||||
shouldModify = probeResult.height > IMG_MAX_HEIGHT_PX && (probeResult.width / probeResult.height) < MIN_ASPECT_RATIO;
|
||||
} catch(e) {
|
||||
console.log('Probing failed:', e);
|
||||
// Immediately abort
|
||||
return match;
|
||||
}
|
||||
|
||||
if (shouldModify) {
|
||||
wasMatchModified = true;
|
||||
console.log(`Modifying match '${match}'`);
|
||||
return `<img alt="${g1}" src="${g2}" height=${IMG_MAX_HEIGHT_PX} />`;
|
||||
}
|
||||
|
||||
console.log(`Match '${match}' is ok/will not be modified`);
|
||||
return match;
|
||||
});
|
||||
|
||||
if (!wasMatchModified) {
|
||||
console.log('Nothing was modified. Skipping update');
|
||||
return;
|
||||
}
|
||||
|
||||
// Update the corresponding element
|
||||
if (context.eventName == 'issue_comment') {
|
||||
console.log('Updating comment with id', context.payload.comment.id);
|
||||
await github.rest.issues.updateComment({
|
||||
comment_id: context.payload.comment.id,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
body: newBody
|
||||
})
|
||||
} else if (context.eventName == 'issues') {
|
||||
console.log('Updating issue', context.payload.issue.number);
|
||||
await github.rest.issues.update({
|
||||
issue_number: context.payload.issue.number,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
body: newBody
|
||||
});
|
||||
}
|
||||
|
||||
// Asnyc replace function from https://stackoverflow.com/a/48032528
|
||||
async function replaceAsync(str, regex, asyncFn) {
|
||||
const promises = [];
|
||||
str.replace(regex, (match, ...args) => {
|
||||
const promise = asyncFn(match, ...args);
|
||||
promises.push(promise);
|
||||
});
|
||||
const data = await Promise.all(promises);
|
||||
return str.replace(regex, () => data.shift());
|
||||
}
|
||||
}
|
||||
29
.github/workflows/image-minimizer.yml
vendored
Normal file
29
.github/workflows/image-minimizer.yml
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
name: Image Minimizer
|
||||
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created, edited]
|
||||
issues:
|
||||
types: [opened, edited]
|
||||
|
||||
jobs:
|
||||
try-minimize:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 16
|
||||
|
||||
- name: Install probe-image-size
|
||||
run: npm i probe-image-size@7.2.3 --ignore-scripts
|
||||
|
||||
- name: Minimize simple images
|
||||
uses: actions/github-script@v5
|
||||
timeout-minutes: 3
|
||||
with:
|
||||
script: |
|
||||
const script = require('.github/workflows/image-minimizer.js');
|
||||
await script({github, context});
|
||||
20
.github/workflows/no-response.yml
vendored
Normal file
20
.github/workflows/no-response.yml
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
name: No Response
|
||||
|
||||
# Both `issue_comment` and `scheduled` event types are required for this Action
|
||||
# to work properly.
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
schedule:
|
||||
# Run daily at midnight.
|
||||
- cron: '0 0 * * *'
|
||||
|
||||
jobs:
|
||||
noResponse:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: lee-dohm/no-response@v0.5.0
|
||||
with:
|
||||
token: ${{ github.token }}
|
||||
daysUntilClose: 14
|
||||
responseRequiredLabel: waiting-for-author
|
||||
16
.gitignore
vendored
16
.gitignore
vendored
@@ -1,15 +1,15 @@
|
||||
.gitignore
|
||||
.gradle
|
||||
/local.properties
|
||||
.gradle/
|
||||
local.properties
|
||||
.DS_Store
|
||||
/build
|
||||
/captures
|
||||
/app/app.iml
|
||||
/.idea
|
||||
/*.iml
|
||||
build/
|
||||
captures/
|
||||
.idea/
|
||||
*.iml
|
||||
*~
|
||||
.weblate
|
||||
*.class
|
||||
app/debug/
|
||||
app/release/
|
||||
|
||||
# vscode / eclipse files
|
||||
*.classpath
|
||||
|
||||
140
README.es.md
140
README.es.md
@@ -1,140 +0,0 @@
|
||||
<p align="center"><a href="https://newpipe.net"><img src="assets/new_pipe_icon_5.png" width="150"></a></p>
|
||||
<h2 align="center"><b>NewPipe</b></h2>
|
||||
<h4 align="center">Una interfaz de streaming lijera y libre para Android.</h4>
|
||||
|
||||
<p align="center"><a href="https://f-droid.org/packages/org.schabi.newpipe/"><img src="https://f-droid.org/wiki/images/0/06/F-Droid-button_get-it-on.png"></a></p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/TeamNewPipe/NewPipe/releases" alt="GitHub release"><img src="https://img.shields.io/badge/Lanzamiento-v0.20.11-blue.svg" ></a>
|
||||
<a href="https://www.gnu.org/licenses/gpl-3.0" alt="License: GPLv3"><img src="https://img.shields.io/badge/Licencia-GPL%20v3-blue.svg"></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/es/" alt="Estado de la traducción"><img src="https://hosted.weblate.org/widgets/newpipe/es/svg-badge.svg"></a>
|
||||
<a href="https://web.libera.chat/#newpipe" alt="IRC channel: #newpipe"><img src="https://img.shields.io/badge/Canal%20de%20IRC%20-%23newpipe-brightgreen.svg"></a>
|
||||
<a href="https://www.bountysource.com/teams/newpipe" alt="Bountysource bounties"><img src="https://img.shields.io/bountysource/team/newpipe/activity.svg?colorB=cd201f"></a>
|
||||
</p>
|
||||
<hr>
|
||||
|
||||
<p align="center"><a href="#capturas-de-pantalla">Capturas de pantalla</a> • <a href="#descripción">Descripción</a> • <a href="#características">Características</a> • <a href="#installación-y-actualizaciones">Installación y actualizaciones</a> • <a href="#contribución">Contribución</a> • <a href="#donar">Donar</a> • <a href="#licencias">Licencias</a></p>
|
||||
<p align="center"><a href="https://newpipe.net">Sitio web</a> • <a href="https://newpipe.net/blog/">Blog</a> • <a href="https://newpipe.net/FAQ/">Preguntas Frecuentes</a> • <a href="https://newpipe.net/press/">Prensa</a></p>
|
||||
<hr>
|
||||
|
||||
*Lea esto en otros idiomas: [English](README.md), [한국어](README.ko.md), [Soomaali](README.so.md), [Português Brasil](README.pt_BR.md), [日本語](README.ja.md), [Română](README.ro.md), [Türkçe](README.tr.md).*
|
||||
|
||||
<b>AVISO: ESTA ES UNA VERSIÓN BETA, POR LO TANTO, PUEDE ENCONTRAR BUGS (ERRORES). SI ENCUENTRA UNO, ABRA UN ISSUE A TRAVÉS DE NUESTRO REPOSITORIO GITHUB.</b>
|
||||
|
||||
<b>COLOCAR NEWPIPE O CUALQUIER FORK (BIFURCACIÓN) REALIZADO DE ELLO EN GOOGLE PLAY STORE VIOLA SUS TÉRMINOS Y CONDICIONES.</b>
|
||||
|
||||
## Capturas de pantalla
|
||||
|
||||
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_01.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_01.png)
|
||||
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_02.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_02.png)
|
||||
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_03.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_03.png)
|
||||
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_04.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_04.png)
|
||||
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_05.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_05.png)
|
||||
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_06.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_06.png)
|
||||
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_07.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_07.png)
|
||||
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_08.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_08.png)
|
||||
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_09.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_09.png)
|
||||
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_10.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_10.png)
|
||||
[<img src="fastlane/metadata/android/en-US/images/tenInchScreenshots/shot_11.png" width=405>](fastlane/metadata/android/en-US/images/tenInchScreenshots/shot_11.png)
|
||||
[<img src="fastlane/metadata/android/en-US/images/tenInchScreenshots/shot_12.png" width=405>](fastlane/metadata/android/en-US/images/tenInchScreenshots/shot_12.png)
|
||||
|
||||
## Descripción
|
||||
NewPipe no usa ninguna librería de framework de Google, ni la API de YouTube. Los sitios web solamente se analizan para extraer la información requerida, asi que esta app se puede usar sin los servicios de Google instalados. Además, no se necesita una cuenta de YouTube para usar NewPipe, lo cual es un software libre de copyleft.
|
||||
|
||||
### Características
|
||||
* Buscar videos
|
||||
* Mostrar información general sobre videos
|
||||
* Mirar videos de YouTube
|
||||
* Escuchar audio de YouTube
|
||||
* Modo popup (reproductor flotante)
|
||||
* Elegir reproductor para mirar el video
|
||||
* Descargar videos
|
||||
* Descargar solamente audio
|
||||
* Abrir video en Kodi
|
||||
* Mostrar videos próximos/relacionados
|
||||
* Buscar a través de YouTube en un idioma específico
|
||||
* Mirar/Bloquear materiales restringidas por edad.
|
||||
* Mostrar información general sobre canales
|
||||
* Buscar canales
|
||||
* Mirar videos de un canal
|
||||
* Apoyo Orbot/Tor (todavía no directamente)
|
||||
* Apoyo 1080p/2K/4K
|
||||
* Ver historias
|
||||
* Subscribirse a canales
|
||||
* Buscar historias
|
||||
* Buscar/mirar listas de reproducción
|
||||
* Mirar listas de reproducción en fila
|
||||
* Poner videos en fila
|
||||
* Listas locales de reproducción
|
||||
* Subtítulos
|
||||
* Apoyo de medios en directo
|
||||
* Mostrar comentarios
|
||||
|
||||
### Servicios apoyados
|
||||
NewPipe apoya varios servicios. Nuestras [documentaciones](https://teamnewpipe.github.io/documentation/) proveen más información en como se puede agregar un servicio nuevo a la app y el extractor. Por favor contáctenos si pretende agregar uno nuevo. Actualmente los servicios apoyados son:
|
||||
|
||||
* YouTube
|
||||
* SoundCloud \[beta\]
|
||||
* media.ccc.de \[beta\]
|
||||
* PeerTube instances \[beta\]
|
||||
* Bandcamp \[beta\]
|
||||
|
||||
<!-- Brecha escondida para mantener compatibles los enlaces viejos. -->
|
||||
<span id="actualizaciones"></span>
|
||||
|
||||
## Installación y actualizaciones
|
||||
Se puede instalar NewPipe usando uno de los métodos siguientes:
|
||||
1. Agregar nuestro repositorio personalizado a F-Droid e instalarlo desde allí. Las instrucciones están aquí: https://newpipe.net/FAQ/tutorials/install-add-fdroid-repo/
|
||||
2. Descargar el archivo APK del enlace [Github Releases](https://github.com/TeamNewPipe/NewPipe/releases) e instalarlo.
|
||||
3. Actualizar a través de F-Droid. Este es el método más lento para obtener la actualización, como F-Droid debe reconocer cambios, construir el APK aparte, firmarlo con una clave, y finalmente empujar la actualización a los usuarios.
|
||||
4. Construir un APK de depuración por si mismo. Este es el modo más rápido para realizar nuevas características en su dispositivo, pero es mucho más complicado, asi que recomendamos uno de los otros métodos.
|
||||
|
||||
Recomendamos el método 1 para la mayoría de usuarios. Los APKs instalados usando método 1 o 2 son compatibles el uno con el otro, pero no con las instalaciones usando método 3. Esta es debida a la misma clave digital (la nuestra), siendo utilizado en los métodos 1 y 2, pero una clave digital diferente (la de F-Droid) siendo utilizado en el método 3. Construir un APK de depuración usando método 4 excluye una clave enteramente. Firmando con claves digitales ayuda a asegurar de que un
|
||||
usuario no esté engañado para instalar una actualización maliciosa a una app.
|
||||
|
||||
Mientras tanto, si quiere cambiar los fuentes por alguna razón (por ejemplo, la funcionalidad del nucleo de NewPipe se rompe y F-Droid aun no tiene la actualización), recomendamos el siguiente procedimiento:
|
||||
1. Repaldear sus datos a través de Ajustes > Contenido > Exporta base de datos para guardar su historia, subscripciones, y listas de reproducción
|
||||
2. Desinstalar NewPipe
|
||||
3. Descargar el APK del nuevo fuente e instalarlo.
|
||||
4. Importar los datos del paso 1 a través de Ajustes > Contenido > Importa base de datos.
|
||||
|
||||
## Contribución
|
||||
Si tiene ideas, traducciónes, cambios de diseño, limpieza de código, o cambios grandes de código, su ayuda es siempre bienvenida.
|
||||
Cuanto más realizamos, mejor se pone la aplicación!
|
||||
|
||||
Si quiere involucrarse, fíjese en nuestras [notas de contribución](.github/CONTRIBUTING.md).
|
||||
|
||||
<a href="https://hosted.weblate.org/engage/newpipe/es/">
|
||||
<img src="https://hosted.weblate.org/widgets/newpipe/es/287x66-grey.png" alt="Estado de la traducción" />
|
||||
</a>
|
||||
|
||||
## Donar
|
||||
Si le gusta el NewPipe estaremos felices con una donación. O puede enviar bitcoin o donar a través de Bountysource o Liberapay. Para obtener más información sobre como donar a NewPipe, por favor visita nuestro [sitio web](https://newpipe.net/donate).
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<td><img src="https://bitcoin.org/img/icons/logotop.svg" alt="Bitcoin"></td>
|
||||
<td><img src="assets/bitcoin_qr_code.png" alt="Bitcoin QR code" width="100px"></td>
|
||||
<td><samp>16A9J59ahMRqkLSZjhYj33n9j3fMztFxnh</samp></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="https://liberapay.com/TeamNewPipe/"><img src="https://upload.wikimedia.org/wikipedia/commons/2/27/Liberapay_logo_v2_white-on-yellow.svg" alt="Liberapay" width="80px" ></a></td>
|
||||
<td><a href="https://liberapay.com/TeamNewPipe/"><img src="assets/liberapay_qr_code.png" alt="Visit NewPipe at liberapay.com" width="100px"></a></td>
|
||||
<td><a href="https://liberapay.com/TeamNewPipe/donate"><img src="assets/liberapay_donate_button.svg" alt="Donate via Liberapay" height="35px"></a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="https://www.bountysource.com/teams/newpipe"><img src="https://upload.wikimedia.org/wikipedia/commons/thumb/2/22/Bountysource.png/320px-Bountysource.png" alt="Bountysource" width="190px"></a></td>
|
||||
<td><a href="https://www.bountysource.com/teams/newpipe"><img src="assets/bountysource_qr_code.png" alt="Visit NewPipe at bountysource.com" width="100px"></a></td>
|
||||
<td><a href="https://www.bountysource.com/teams/newpipe/issues"><img src="https://img.shields.io/bountysource/team/newpipe/activity.svg?colorB=cd201f" height="30px" alt="Check out how many bounties you can earn."></a></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
## Política de privacidad
|
||||
El proyecto NewPipe tiene como objetivo proveer una experience privada y anónima para usar servicios de medios web.
|
||||
Por lo tanto, la app no colecciona ningunos datos sin su consentimiento. La politica de privacidad de NewPipe explica en detalle los datos enviados y almacenados cuando envia un informe de error, o comentario en nuestro blog. Puede encontrar el documento [aqui](https://newpipe.net/legal/privacy/).
|
||||
|
||||
## Licencia
|
||||
[](https://www.gnu.org/licenses/gpl-3.0.en.html)
|
||||
|
||||
NewPipe es Software Libre: Puede usar, estudiar, compartir, y mejorarlo a su voluntad. Especificamente puede redistribuir y/o modificarlo bajo los términos de la [GNU General Public License](https://www.gnu.org/licenses/gpl.html) como publicado por la Free Software Foundation, o versión 3 de la licencia, o (en su opción) cualquier versión posterior.
|
||||
@@ -2,7 +2,7 @@
|
||||
<h2 align="center"><b>NewPipe</b></h2>
|
||||
<h4 align="center">A libre lightweight streaming frontend for Android.</h4>
|
||||
|
||||
<p align="center"><a href="https://f-droid.org/packages/org.schabi.newpipe/"><img src="https://f-droid.org/wiki/images/0/06/F-Droid-button_get-it-on.png"></a></p>
|
||||
<p align="center"><a href="https://f-droid.org/packages/org.schabi.newpipe/"><img src="https://fdroid.gitlab.io/artwork/badge/get-it-on-en.svg" alt="Get it on F-Droid" height=80/></a></p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/TeamNewPipe/NewPipe/releases" alt="GitHub release"><img src="https://img.shields.io/github/release/TeamNewPipe/NewPipe.svg" ></a>
|
||||
@@ -17,7 +17,7 @@
|
||||
<p align="center"><a href="https://newpipe.net">Website</a> • <a href="https://newpipe.net/blog/">Blog</a> • <a href="https://newpipe.net/FAQ/">FAQ</a> • <a href="https://newpipe.net/press/">Press</a></p>
|
||||
<hr>
|
||||
|
||||
*Read this in other languages: [English](README.md), [Español](README.es.md), [한국어](README.ko.md), [Soomaali](README.so.md), [Português Brasil](README.pt_BR.md), [日本語](README.ja.md), [Română](README.ro.md), [Türkçe](README.tr.md).*
|
||||
*Read this in other languages: [English](README.md), [Español](doc/README.es.md), [한국어](doc/README.ko.md), [Soomaali](doc/README.so.md), [Português Brasil](doc/README.pt_BR.md), [Polski](doc/README.pl.md), [日本語](doc/README.ja.md), [Română](doc/README.ro.md), [Türkçe](doc/README.tr.md), [正體中文](doc/README.zh_TW.md).*
|
||||
|
||||
<b>WARNING: THIS IS A BETA VERSION, THEREFORE YOU MAY ENCOUNTER BUGS. IF YOU DO, OPEN AN ISSUE VIA OUR GITHUB REPOSITORY.</b>
|
||||
|
||||
@@ -89,7 +89,7 @@ NewPipe supports multiple services. Our [docs](https://teamnewpipe.github.io/doc
|
||||
## Installation and updates
|
||||
You can install NewPipe using one of the following methods:
|
||||
1. Add our custom repo to F-Droid and install it from there. The instructions are here: https://newpipe.net/FAQ/tutorials/install-add-fdroid-repo/
|
||||
2. Download the APK from [Github Releases](https://github.com/TeamNewPipe/NewPipe/releases) and install it.
|
||||
2. Download the APK from [GitHub Releases](https://github.com/TeamNewPipe/NewPipe/releases) 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, 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.
|
||||
|
||||
@@ -140,7 +140,7 @@ Therefore, the app does not collect any data without your consent. NewPipe's pri
|
||||
## License
|
||||
[](https://www.gnu.org/licenses/gpl-3.0.en.html)
|
||||
|
||||
NewPipe is Free Software: You can use, study share and improve it at your
|
||||
NewPipe is Free Software: You can use, study, share, and improve it at
|
||||
will. Specifically you can redistribute and/or modify it under the terms of the
|
||||
[GNU General Public License](https://www.gnu.org/licenses/gpl.html) as
|
||||
published by the Free Software Foundation, either version 3 of the License, or
|
||||
|
||||
3
app/.gitignore
vendored
3
app/.gitignore
vendored
@@ -1,3 +0,0 @@
|
||||
.gitignore
|
||||
/build
|
||||
*.iml
|
||||
125
app/build.gradle
125
app/build.gradle
@@ -1,24 +1,23 @@
|
||||
plugins {
|
||||
id "org.sonarqube" version "3.1.1"
|
||||
id "com.android.application"
|
||||
id "kotlin-android"
|
||||
id "kotlin-kapt"
|
||||
id "kotlin-parcelize"
|
||||
id "checkstyle"
|
||||
id "org.sonarqube" version "3.3"
|
||||
}
|
||||
|
||||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'kotlin-android'
|
||||
apply plugin: 'kotlin-android-extensions'
|
||||
apply plugin: 'kotlin-kapt'
|
||||
apply plugin: 'checkstyle'
|
||||
|
||||
android {
|
||||
compileSdkVersion 29
|
||||
buildToolsVersion '29.0.3'
|
||||
compileSdk 31
|
||||
buildToolsVersion '30.0.3'
|
||||
|
||||
defaultConfig {
|
||||
applicationId "org.schabi.newpipe"
|
||||
resValue "string", "app_name", "NewPipe"
|
||||
minSdkVersion 19
|
||||
targetSdkVersion 29
|
||||
versionCode 970
|
||||
versionName "0.21.4"
|
||||
minSdk 19
|
||||
targetSdk 29
|
||||
versionCode 984
|
||||
versionName "0.22.1"
|
||||
|
||||
multiDexEnabled true
|
||||
|
||||
@@ -54,6 +53,11 @@ android {
|
||||
// debug build. This seems to be a Gradle bug, therefore
|
||||
// TODO: update Gradle version
|
||||
release {
|
||||
if (System.properties.containsKey('packageSuffix')) {
|
||||
applicationIdSuffix System.getProperty('packageSuffix')
|
||||
resValue "string", "app_name", "NewPipe " + System.getProperty('packageSuffix')
|
||||
archivesBaseName = 'NewPipe_' + System.getProperty('packageSuffix')
|
||||
}
|
||||
minifyEnabled true
|
||||
shrinkResources false // disabled to fix F-Droid's reproducible build
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||
@@ -61,7 +65,7 @@ android {
|
||||
}
|
||||
}
|
||||
|
||||
lintOptions {
|
||||
lint {
|
||||
checkReleaseBuilds false
|
||||
// Or, if you prefer, you can continue to check for errors in release builds,
|
||||
// but continue the build even when errors are found:
|
||||
@@ -75,18 +79,13 @@ android {
|
||||
// Flag to enable support for the new language APIs
|
||||
coreLibraryDesugaringEnabled true
|
||||
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
sourceCompatibility JavaVersion.VERSION_11
|
||||
targetCompatibility JavaVersion.VERSION_11
|
||||
encoding 'utf-8'
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
// Required and used only by groupie
|
||||
androidExtensions {
|
||||
experimental = true
|
||||
jvmTarget = JavaVersion.VERSION_11
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
@@ -99,20 +98,21 @@ android {
|
||||
}
|
||||
|
||||
ext {
|
||||
checkstyleVersion = '8.38'
|
||||
checkstyleVersion = '9.2.1'
|
||||
|
||||
androidxLifecycleVersion = '2.2.0'
|
||||
androidxRoomVersion = '2.3.0-alpha03'
|
||||
androidxLifecycleVersion = '2.3.1'
|
||||
androidxRoomVersion = '2.3.0'
|
||||
|
||||
icepickVersion = '3.2.0'
|
||||
exoPlayerVersion = '2.12.3'
|
||||
googleAutoServiceVersion = '1.0-rc7'
|
||||
groupieVersion = '2.8.1'
|
||||
markwonVersion = '4.6.0'
|
||||
exoPlayerVersion = '2.14.2'
|
||||
googleAutoServiceVersion = '1.0.1'
|
||||
groupieVersion = '2.10.0'
|
||||
markwonVersion = '4.6.2'
|
||||
|
||||
leakCanaryVersion = '2.5'
|
||||
stethoVersion = '1.5.1'
|
||||
mockitoVersion = '3.6.0'
|
||||
stethoVersion = '1.6.0'
|
||||
mockitoVersion = '4.0.0'
|
||||
assertJVersion = '3.22.0'
|
||||
}
|
||||
|
||||
configurations {
|
||||
@@ -121,7 +121,7 @@ configurations {
|
||||
}
|
||||
|
||||
checkstyle {
|
||||
configDir rootProject.file(".")
|
||||
getConfigDirectory().set(rootProject.file("."))
|
||||
ignoreFailures false
|
||||
showViolations true
|
||||
toolVersion = checkstyleVersion
|
||||
@@ -140,8 +140,8 @@ task runCheckstyle(type: Checkstyle) {
|
||||
showViolations true
|
||||
|
||||
reports {
|
||||
xml.enabled true
|
||||
html.enabled true
|
||||
xml.getRequired().set(true)
|
||||
html.getRequired().set(true)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -151,7 +151,7 @@ def inputFiles = project.fileTree(dir: "src", include: "**/*.kt")
|
||||
task runKtlint(type: JavaExec) {
|
||||
inputs.files(inputFiles)
|
||||
outputs.dir(outputDir)
|
||||
main = "com.pinterest.ktlint.Main"
|
||||
getMainClass().set("com.pinterest.ktlint.Main")
|
||||
classpath = configurations.ktlint
|
||||
args "src/**/*.kt"
|
||||
}
|
||||
@@ -159,13 +159,16 @@ task runKtlint(type: JavaExec) {
|
||||
task formatKtlint(type: JavaExec) {
|
||||
inputs.files(inputFiles)
|
||||
outputs.dir(outputDir)
|
||||
main = "com.pinterest.ktlint.Main"
|
||||
getMainClass().set("com.pinterest.ktlint.Main")
|
||||
classpath = configurations.ktlint
|
||||
args "-F", "src/**/*.kt"
|
||||
}
|
||||
|
||||
afterEvaluate {
|
||||
preDebugBuild.dependsOn formatKtlint, runCheckstyle, runKtlint
|
||||
if (!System.properties.containsKey('skipFormatKtlint')) {
|
||||
preDebugBuild.dependsOn formatKtlint
|
||||
}
|
||||
preDebugBuild.dependsOn runCheckstyle, runKtlint
|
||||
}
|
||||
|
||||
sonarqube {
|
||||
@@ -182,35 +185,42 @@ dependencies {
|
||||
|
||||
/** NewPipe libraries **/
|
||||
// You can use a local version by uncommenting a few lines in settings.gradle
|
||||
// Or you can use a commit you pushed to GitHub by just replacing TeamNewPipe with your GitHub
|
||||
// name and the commit hash with the commit hash of the (pushed) commit you want to test
|
||||
// This works thanks to JitPack: https://jitpack.io/
|
||||
implementation 'com.github.TeamNewPipe:nanojson:1d9e1aea9049fc9f85e68b43ba39fe7be1c1f751'
|
||||
implementation 'com.github.TeamNewPipe:NewPipeExtractor:v0.21.4'
|
||||
implementation 'com.github.TeamNewPipe:NewPipeExtractor:v0.21.14'
|
||||
|
||||
/** Checkstyle **/
|
||||
checkstyle "com.puppycrawl.tools:checkstyle:${checkstyleVersion}"
|
||||
ktlint 'com.pinterest:ktlint:0.40.0'
|
||||
ktlint 'com.pinterest:ktlint:0.43.2'
|
||||
|
||||
/** Kotlin **/
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:${kotlin_version}"
|
||||
|
||||
/** AndroidX **/
|
||||
implementation 'androidx.appcompat:appcompat:1.2.0'
|
||||
implementation 'androidx.appcompat:appcompat:1.3.1'
|
||||
implementation 'androidx.cardview:cardview:1.0.0'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
|
||||
implementation 'androidx.core:core-ktx:1.3.2'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.2'
|
||||
implementation 'androidx.core:core-ktx:1.6.0'
|
||||
implementation 'androidx.documentfile:documentfile:1.0.1'
|
||||
implementation 'androidx.fragment:fragment-ktx:1.3.6'
|
||||
implementation "androidx.lifecycle:lifecycle-livedata:${androidxLifecycleVersion}"
|
||||
implementation "androidx.lifecycle:lifecycle-viewmodel:${androidxLifecycleVersion}"
|
||||
implementation 'androidx.localbroadcastmanager:localbroadcastmanager:1.0.0'
|
||||
implementation 'androidx.media:media:1.2.1'
|
||||
implementation 'androidx.media:media:1.4.3'
|
||||
implementation 'androidx.multidex:multidex:2.0.1'
|
||||
implementation 'androidx.preference:preference:1.1.1'
|
||||
implementation 'androidx.recyclerview:recyclerview:1.1.0'
|
||||
implementation 'androidx.recyclerview:recyclerview:1.2.1'
|
||||
implementation "androidx.room:room-runtime:${androidxRoomVersion}"
|
||||
implementation "androidx.room:room-rxjava3:${androidxRoomVersion}"
|
||||
kapt "androidx.room:room-compiler:${androidxRoomVersion}"
|
||||
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
|
||||
// Newer version specified to prevent accessibility regressions with RecyclerView, see:
|
||||
// https://developer.android.com/jetpack/androidx/releases/viewpager2#1.1.0-alpha01
|
||||
implementation 'androidx.viewpager2:viewpager2:1.1.0-beta01'
|
||||
implementation 'androidx.webkit:webkit:1.4.0'
|
||||
implementation 'com.google.android.material:material:1.2.1'
|
||||
implementation 'com.google.android.material:material:1.4.0'
|
||||
|
||||
/** Third-party libraries **/
|
||||
// Instance state boilerplate elimination
|
||||
@@ -218,7 +228,7 @@ dependencies {
|
||||
kapt "frankiesardo:icepick-processor:${icepickVersion}"
|
||||
|
||||
// HTML parser
|
||||
implementation "org.jsoup:jsoup:1.13.1"
|
||||
implementation "org.jsoup:jsoup:1.14.3"
|
||||
|
||||
// HTTP client
|
||||
//noinspection GradleDependency --> do not update okhttp to keep supporting Android 4.4 users
|
||||
@@ -233,13 +243,14 @@ dependencies {
|
||||
kapt "com.google.auto.service:auto-service:${googleAutoServiceVersion}"
|
||||
|
||||
// Manager for complex RecyclerView layouts
|
||||
implementation "com.xwray:groupie:${groupieVersion}"
|
||||
implementation "com.xwray:groupie-viewbinding:${groupieVersion}"
|
||||
implementation "com.github.lisawray.groupie:groupie:${groupieVersion}"
|
||||
implementation "com.github.lisawray.groupie:groupie-viewbinding:${groupieVersion}"
|
||||
|
||||
// Circular ImageView
|
||||
implementation "de.hdodenhof:circleimageview:3.1.0"
|
||||
// Image loading
|
||||
implementation "com.nostra13.universalimageloader:universal-image-loader:1.9.5"
|
||||
//noinspection GradleDependency --> 2.8 is the last version, not 2.71828!
|
||||
implementation "com.squareup.picasso:picasso:2.8"
|
||||
|
||||
// Markdown library for Android
|
||||
implementation "io.noties.markwon:core:${markwonVersion}"
|
||||
@@ -249,16 +260,19 @@ dependencies {
|
||||
implementation "com.nononsenseapps:filepicker:4.2.1"
|
||||
|
||||
// Crash reporting
|
||||
implementation "ch.acra:acra-core:5.7.0"
|
||||
implementation "ch.acra:acra-core:5.8.4"
|
||||
|
||||
// Properly restarting
|
||||
implementation 'com.jakewharton:process-phoenix:2.1.2'
|
||||
|
||||
// Reactive extensions for Java VM
|
||||
implementation "io.reactivex.rxjava3:rxjava:3.0.7"
|
||||
implementation "io.reactivex.rxjava3:rxjava:3.0.13"
|
||||
implementation "io.reactivex.rxjava3:rxandroid:3.0.0"
|
||||
// RxJava binding APIs for Android UI widgets
|
||||
implementation "com.jakewharton.rxbinding4:rxbinding:4.0.0"
|
||||
|
||||
// Date and time formatting
|
||||
implementation "org.ocpsoft.prettytime:prettytime:5.0.1.Final"
|
||||
implementation "org.ocpsoft.prettytime:prettytime:5.0.2.Final"
|
||||
|
||||
/** Debugging **/
|
||||
// Memory leak detection
|
||||
@@ -274,11 +288,10 @@ dependencies {
|
||||
testImplementation "org.mockito:mockito-core:${mockitoVersion}"
|
||||
testImplementation "org.mockito:mockito-inline:${mockitoVersion}"
|
||||
|
||||
androidTestImplementation "androidx.test.ext:junit:1.1.2"
|
||||
androidTestImplementation "androidx.test.ext:junit:1.1.3"
|
||||
androidTestImplementation "androidx.test:runner:1.4.0"
|
||||
androidTestImplementation "androidx.room:room-testing:${androidxRoomVersion}"
|
||||
androidTestImplementation "androidx.test.espresso:espresso-core:3.3.0", {
|
||||
exclude module: 'support-annotations'
|
||||
}
|
||||
androidTestImplementation "org.assertj:assertj-core:${assertJVersion}"
|
||||
}
|
||||
|
||||
static String getGitWorkingBranch() {
|
||||
|
||||
713
app/schemas/org.schabi.newpipe.database.AppDatabase/4.json
Normal file
713
app/schemas/org.schabi.newpipe.database.AppDatabase/4.json
Normal file
@@ -0,0 +1,713 @@
|
||||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 4,
|
||||
"identityHash": "d8070091972a7011bce18aed62f80b90",
|
||||
"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)",
|
||||
"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
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"columnNames": [
|
||||
"uid"
|
||||
],
|
||||
"autoGenerate": true
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_subscriptions_service_id_url",
|
||||
"unique": true,
|
||||
"columnNames": [
|
||||
"service_id",
|
||||
"url"
|
||||
],
|
||||
"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}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `creation_date` INTEGER, `service_id` INTEGER NOT NULL, `search` TEXT)",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"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
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"columnNames": [
|
||||
"id"
|
||||
],
|
||||
"autoGenerate": true
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_search_history_search",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"search"
|
||||
],
|
||||
"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": {
|
||||
"columnNames": [
|
||||
"uid"
|
||||
],
|
||||
"autoGenerate": true
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_streams_service_id_url",
|
||||
"unique": true,
|
||||
"columnNames": [
|
||||
"service_id",
|
||||
"url"
|
||||
],
|
||||
"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": {
|
||||
"columnNames": [
|
||||
"stream_id",
|
||||
"access_date"
|
||||
],
|
||||
"autoGenerate": false
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_stream_history_stream_id",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"stream_id"
|
||||
],
|
||||
"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": {
|
||||
"columnNames": [
|
||||
"stream_id"
|
||||
],
|
||||
"autoGenerate": false
|
||||
},
|
||||
"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, `thumbnail_url` TEXT)",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "uid",
|
||||
"columnName": "uid",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "name",
|
||||
"columnName": "name",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "thumbnailUrl",
|
||||
"columnName": "thumbnail_url",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"columnNames": [
|
||||
"uid"
|
||||
],
|
||||
"autoGenerate": true
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_playlists_name",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"name"
|
||||
],
|
||||
"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": {
|
||||
"columnNames": [
|
||||
"playlist_id",
|
||||
"join_index"
|
||||
],
|
||||
"autoGenerate": false
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_playlist_stream_join_playlist_id_join_index",
|
||||
"unique": true,
|
||||
"columnNames": [
|
||||
"playlist_id",
|
||||
"join_index"
|
||||
],
|
||||
"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"
|
||||
],
|
||||
"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": {
|
||||
"columnNames": [
|
||||
"uid"
|
||||
],
|
||||
"autoGenerate": true
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_remote_playlists_name",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"name"
|
||||
],
|
||||
"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"
|
||||
],
|
||||
"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": {
|
||||
"columnNames": [
|
||||
"stream_id",
|
||||
"subscription_id"
|
||||
],
|
||||
"autoGenerate": false
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_feed_subscription_id",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"subscription_id"
|
||||
],
|
||||
"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": {
|
||||
"columnNames": [
|
||||
"uid"
|
||||
],
|
||||
"autoGenerate": true
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_feed_group_sort_order",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"sort_order"
|
||||
],
|
||||
"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": {
|
||||
"columnNames": [
|
||||
"group_id",
|
||||
"subscription_id"
|
||||
],
|
||||
"autoGenerate": false
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_feed_group_subscription_join_subscription_id",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"subscription_id"
|
||||
],
|
||||
"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": {
|
||||
"columnNames": [
|
||||
"subscription_id"
|
||||
],
|
||||
"autoGenerate": false
|
||||
},
|
||||
"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, 'd8070091972a7011bce18aed62f80b90')"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,134 +0,0 @@
|
||||
package org.schabi.newpipe.database
|
||||
|
||||
import android.content.ContentValues
|
||||
import android.database.sqlite.SQLiteDatabase
|
||||
import androidx.room.Room
|
||||
import androidx.room.testing.MigrationTestHelper
|
||||
import androidx.sqlite.db.framework.FrameworkSQLiteOpenHelperFactory
|
||||
import androidx.test.core.app.ApplicationProvider
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.schabi.newpipe.extractor.stream.StreamType
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class AppDatabaseTest {
|
||||
companion object {
|
||||
private const val DEFAULT_SERVICE_ID = 0
|
||||
private const val DEFAULT_URL = "https://www.youtube.com/watch?v=cDphUib5iG4"
|
||||
private const val DEFAULT_TITLE = "Test Title"
|
||||
private val DEFAULT_TYPE = StreamType.VIDEO_STREAM
|
||||
private const val DEFAULT_DURATION = 480L
|
||||
private const val DEFAULT_UPLOADER_NAME = "Uploader Test"
|
||||
private const val DEFAULT_THUMBNAIL = "https://example.com/example.jpg"
|
||||
|
||||
private const val DEFAULT_SECOND_SERVICE_ID = 0
|
||||
private const val DEFAULT_SECOND_URL = "https://www.youtube.com/watch?v=ncQU6iBn5Fc"
|
||||
}
|
||||
|
||||
@get:Rule
|
||||
val testHelper = MigrationTestHelper(
|
||||
InstrumentationRegistry.getInstrumentation(),
|
||||
AppDatabase::class.java.canonicalName, FrameworkSQLiteOpenHelperFactory()
|
||||
)
|
||||
|
||||
@Test
|
||||
fun migrateDatabaseFrom2to3() {
|
||||
val databaseInV2 = testHelper.createDatabase(AppDatabase.DATABASE_NAME, Migrations.DB_VER_2)
|
||||
|
||||
databaseInV2.run {
|
||||
insert(
|
||||
"streams", SQLiteDatabase.CONFLICT_FAIL,
|
||||
ContentValues().apply {
|
||||
// put("uid", null)
|
||||
put("service_id", DEFAULT_SERVICE_ID)
|
||||
put("url", DEFAULT_URL)
|
||||
put("title", DEFAULT_TITLE)
|
||||
put("stream_type", DEFAULT_TYPE.name)
|
||||
put("duration", DEFAULT_DURATION)
|
||||
put("uploader", DEFAULT_UPLOADER_NAME)
|
||||
put("thumbnail_url", DEFAULT_THUMBNAIL)
|
||||
}
|
||||
)
|
||||
insert(
|
||||
"streams", SQLiteDatabase.CONFLICT_FAIL,
|
||||
ContentValues().apply {
|
||||
// put("uid", null)
|
||||
put("service_id", DEFAULT_SECOND_SERVICE_ID)
|
||||
put("url", DEFAULT_SECOND_URL)
|
||||
// put("title", null)
|
||||
// put("stream_type", null)
|
||||
// put("duration", null)
|
||||
// put("uploader", null)
|
||||
// put("thumbnail_url", null)
|
||||
}
|
||||
)
|
||||
insert(
|
||||
"streams", SQLiteDatabase.CONFLICT_FAIL,
|
||||
ContentValues().apply {
|
||||
// put("uid", null)
|
||||
put("service_id", DEFAULT_SERVICE_ID)
|
||||
// put("url", null)
|
||||
// put("title", null)
|
||||
// put("stream_type", null)
|
||||
// put("duration", null)
|
||||
// put("uploader", null)
|
||||
// put("thumbnail_url", null)
|
||||
}
|
||||
)
|
||||
close()
|
||||
}
|
||||
|
||||
testHelper.runMigrationsAndValidate(
|
||||
AppDatabase.DATABASE_NAME, Migrations.DB_VER_3,
|
||||
true, Migrations.MIGRATION_2_3
|
||||
)
|
||||
|
||||
val migratedDatabaseV3 = getMigratedDatabase()
|
||||
val listFromDB = migratedDatabaseV3.streamDAO().all.blockingFirst()
|
||||
|
||||
// Only expect 2, the one with the null url will be ignored
|
||||
assertEquals(2, listFromDB.size)
|
||||
|
||||
val streamFromMigratedDatabase = listFromDB[0]
|
||||
assertEquals(DEFAULT_SERVICE_ID, streamFromMigratedDatabase.serviceId)
|
||||
assertEquals(DEFAULT_URL, streamFromMigratedDatabase.url)
|
||||
assertEquals(DEFAULT_TITLE, streamFromMigratedDatabase.title)
|
||||
assertEquals(DEFAULT_TYPE, streamFromMigratedDatabase.streamType)
|
||||
assertEquals(DEFAULT_DURATION, streamFromMigratedDatabase.duration)
|
||||
assertEquals(DEFAULT_UPLOADER_NAME, streamFromMigratedDatabase.uploader)
|
||||
assertEquals(DEFAULT_THUMBNAIL, streamFromMigratedDatabase.thumbnailUrl)
|
||||
assertNull(streamFromMigratedDatabase.viewCount)
|
||||
assertNull(streamFromMigratedDatabase.textualUploadDate)
|
||||
assertNull(streamFromMigratedDatabase.uploadDate)
|
||||
assertNull(streamFromMigratedDatabase.isUploadDateApproximation)
|
||||
|
||||
val secondStreamFromMigratedDatabase = listFromDB[1]
|
||||
assertEquals(DEFAULT_SECOND_SERVICE_ID, secondStreamFromMigratedDatabase.serviceId)
|
||||
assertEquals(DEFAULT_SECOND_URL, secondStreamFromMigratedDatabase.url)
|
||||
assertEquals("", secondStreamFromMigratedDatabase.title)
|
||||
// Should fallback to VIDEO_STREAM
|
||||
assertEquals(StreamType.VIDEO_STREAM, secondStreamFromMigratedDatabase.streamType)
|
||||
assertEquals(0, secondStreamFromMigratedDatabase.duration)
|
||||
assertEquals("", secondStreamFromMigratedDatabase.uploader)
|
||||
assertEquals("", secondStreamFromMigratedDatabase.thumbnailUrl)
|
||||
assertNull(secondStreamFromMigratedDatabase.viewCount)
|
||||
assertNull(secondStreamFromMigratedDatabase.textualUploadDate)
|
||||
assertNull(secondStreamFromMigratedDatabase.uploadDate)
|
||||
assertNull(secondStreamFromMigratedDatabase.isUploadDateApproximation)
|
||||
}
|
||||
|
||||
private fun getMigratedDatabase(): AppDatabase {
|
||||
val database: AppDatabase = Room.databaseBuilder(
|
||||
ApplicationProvider.getApplicationContext(),
|
||||
AppDatabase::class.java, AppDatabase.DATABASE_NAME
|
||||
)
|
||||
.build()
|
||||
testHelper.closeWhenFinished(database)
|
||||
return database
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
package org.schabi.newpipe.local.history
|
||||
|
||||
import androidx.test.core.app.ApplicationProvider
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.junit.After
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.schabi.newpipe.database.AppDatabase
|
||||
import org.schabi.newpipe.database.history.model.SearchHistoryEntry
|
||||
import org.schabi.newpipe.testUtil.TestDatabase
|
||||
import org.schabi.newpipe.testUtil.TrampolineSchedulerRule
|
||||
import java.time.LocalDateTime
|
||||
import java.time.OffsetDateTime
|
||||
import java.time.ZoneOffset
|
||||
|
||||
class HistoryRecordManagerTest {
|
||||
|
||||
private lateinit var manager: HistoryRecordManager
|
||||
private lateinit var database: AppDatabase
|
||||
|
||||
@get:Rule
|
||||
val trampolineScheduler = TrampolineSchedulerRule()
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
database = TestDatabase.createReplacingNewPipeDatabase()
|
||||
manager = HistoryRecordManager(ApplicationProvider.getApplicationContext())
|
||||
}
|
||||
|
||||
@After
|
||||
fun cleanUp() {
|
||||
database.close()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun onSearched() {
|
||||
manager.onSearched(0, "Hello").test().await().assertValue(1)
|
||||
|
||||
// For some reason the Flowable returned by getAll() never completes, so we can't assert
|
||||
// that the number of Lists it returns is exactly 1, we can only check if the first List is
|
||||
// correct. Why on earth has a Flowable been used instead of a Single for getAll()?!?
|
||||
val entities = database.searchHistoryDAO().all.blockingFirst()
|
||||
assertThat(entities).hasSize(1)
|
||||
assertThat(entities[0].id).isEqualTo(1)
|
||||
assertThat(entities[0].serviceId).isEqualTo(0)
|
||||
assertThat(entities[0].search).isEqualTo("Hello")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun deleteSearchHistory() {
|
||||
val entries = listOf(
|
||||
SearchHistoryEntry(time.minusSeconds(1), 0, "A"),
|
||||
SearchHistoryEntry(time.minusSeconds(2), 2, "A"),
|
||||
SearchHistoryEntry(time.minusSeconds(3), 1, "B"),
|
||||
SearchHistoryEntry(time.minusSeconds(4), 0, "B"),
|
||||
)
|
||||
|
||||
// make sure all 4 were inserted
|
||||
database.searchHistoryDAO().insertAll(entries)
|
||||
assertThat(database.searchHistoryDAO().all.blockingFirst()).hasSameSizeAs(entries)
|
||||
|
||||
// try to delete only "A" entries, "B" entries should be untouched
|
||||
manager.deleteSearchHistory("A").test().await().assertValue(2)
|
||||
val entities = database.searchHistoryDAO().all.blockingFirst()
|
||||
assertThat(entities).hasSize(2)
|
||||
assertThat(entities).usingElementComparator { o1, o2 -> if (o1.hasEqualValues(o2)) 0 else 1 }
|
||||
.containsExactly(*entries.subList(2, 4).toTypedArray())
|
||||
|
||||
// assert that nothing happens if we delete a search query that does exist in the db
|
||||
manager.deleteSearchHistory("A").test().await().assertValue(0)
|
||||
val entities2 = database.searchHistoryDAO().all.blockingFirst()
|
||||
assertThat(entities2).hasSize(2)
|
||||
assertThat(entities2).usingElementComparator { o1, o2 -> if (o1.hasEqualValues(o2)) 0 else 1 }
|
||||
.containsExactly(*entries.subList(2, 4).toTypedArray())
|
||||
|
||||
// delete all remaining entries
|
||||
manager.deleteSearchHistory("B").test().await().assertValue(2)
|
||||
assertThat(database.searchHistoryDAO().all.blockingFirst()).isEmpty()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun deleteCompleteSearchHistory() {
|
||||
val entries = listOf(
|
||||
SearchHistoryEntry(time.minusSeconds(1), 1, "A"),
|
||||
SearchHistoryEntry(time.minusSeconds(2), 2, "B"),
|
||||
SearchHistoryEntry(time.minusSeconds(3), 0, "C"),
|
||||
)
|
||||
|
||||
// make sure all 3 were inserted
|
||||
database.searchHistoryDAO().insertAll(entries)
|
||||
assertThat(database.searchHistoryDAO().all.blockingFirst()).hasSameSizeAs(entries)
|
||||
|
||||
// should remove everything
|
||||
manager.deleteCompleteSearchHistory().test().await().assertValue(entries.size)
|
||||
assertThat(database.searchHistoryDAO().all.blockingFirst()).isEmpty()
|
||||
}
|
||||
|
||||
private fun insertShuffledRelatedSearches(relatedSearches: Collection<SearchHistoryEntry>) {
|
||||
|
||||
// shuffle to make sure the order of items returned by queries depends only on
|
||||
// SearchHistoryEntry.creationDate, not on the actual insertion time, so that we can
|
||||
// verify that the `ORDER BY` clause does its job
|
||||
database.searchHistoryDAO().insertAll(relatedSearches.shuffled())
|
||||
|
||||
// make sure all entries were inserted
|
||||
assertEquals(
|
||||
relatedSearches.size,
|
||||
database.searchHistoryDAO().all.blockingFirst().size
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun getRelatedSearches_emptyQuery() {
|
||||
insertShuffledRelatedSearches(RELATED_SEARCHES_ENTRIES)
|
||||
|
||||
// make sure correct number of searches is returned and in correct order
|
||||
val searches = manager.getRelatedSearches("", 6, 4).blockingFirst()
|
||||
assertThat(searches).containsExactly(
|
||||
RELATED_SEARCHES_ENTRIES[6].search, // A (even if in two places)
|
||||
RELATED_SEARCHES_ENTRIES[4].search, // B
|
||||
RELATED_SEARCHES_ENTRIES[5].search, // AA
|
||||
RELATED_SEARCHES_ENTRIES[2].search, // BA
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun getRelatedSearches_emptyQuery_manyDuplicates() {
|
||||
insertShuffledRelatedSearches(
|
||||
listOf(
|
||||
SearchHistoryEntry(time.minusSeconds(9), 3, "A"),
|
||||
SearchHistoryEntry(time.minusSeconds(8), 3, "AB"),
|
||||
SearchHistoryEntry(time.minusSeconds(7), 3, "A"),
|
||||
SearchHistoryEntry(time.minusSeconds(6), 3, "A"),
|
||||
SearchHistoryEntry(time.minusSeconds(5), 3, "BA"),
|
||||
SearchHistoryEntry(time.minusSeconds(4), 3, "A"),
|
||||
SearchHistoryEntry(time.minusSeconds(3), 3, "A"),
|
||||
SearchHistoryEntry(time.minusSeconds(2), 0, "A"),
|
||||
SearchHistoryEntry(time.minusSeconds(1), 2, "AA"),
|
||||
)
|
||||
)
|
||||
|
||||
val searches = manager.getRelatedSearches("", 9, 3).blockingFirst()
|
||||
assertThat(searches).containsExactly("AA", "A", "BA")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun getRelatedSearched_nonEmptyQuery() {
|
||||
insertShuffledRelatedSearches(RELATED_SEARCHES_ENTRIES)
|
||||
|
||||
// make sure correct number of searches is returned and in correct order
|
||||
val searches = manager.getRelatedSearches("A", 3, 5).blockingFirst()
|
||||
assertThat(searches).containsExactly(
|
||||
RELATED_SEARCHES_ENTRIES[6].search, // A (even if in two places)
|
||||
RELATED_SEARCHES_ENTRIES[5].search, // AA
|
||||
RELATED_SEARCHES_ENTRIES[1].search, // BA
|
||||
)
|
||||
|
||||
// also make sure that the string comparison is case insensitive
|
||||
val searches2 = manager.getRelatedSearches("a", 3, 5).blockingFirst()
|
||||
assertThat(searches).isEqualTo(searches2)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val time = OffsetDateTime.of(LocalDateTime.of(2000, 1, 1, 1, 1), ZoneOffset.UTC)
|
||||
|
||||
private val RELATED_SEARCHES_ENTRIES = listOf(
|
||||
SearchHistoryEntry(time.minusSeconds(7), 2, "AC"),
|
||||
SearchHistoryEntry(time.minusSeconds(6), 0, "ABC"),
|
||||
SearchHistoryEntry(time.minusSeconds(5), 1, "BA"),
|
||||
SearchHistoryEntry(time.minusSeconds(4), 3, "A"),
|
||||
SearchHistoryEntry(time.minusSeconds(2), 0, "B"),
|
||||
SearchHistoryEntry(time.minusSeconds(3), 2, "AA"),
|
||||
SearchHistoryEntry(time.minusSeconds(1), 1, "A"),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
package org.schabi.newpipe.local.playlist
|
||||
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.schabi.newpipe.database.AppDatabase
|
||||
import org.schabi.newpipe.database.stream.model.StreamEntity
|
||||
import org.schabi.newpipe.extractor.stream.StreamType
|
||||
import org.schabi.newpipe.testUtil.TestDatabase
|
||||
import org.schabi.newpipe.testUtil.TrampolineSchedulerRule
|
||||
|
||||
class LocalPlaylistManagerTest {
|
||||
|
||||
private lateinit var manager: LocalPlaylistManager
|
||||
private lateinit var database: AppDatabase
|
||||
|
||||
@get:Rule
|
||||
val trampolineScheduler = TrampolineSchedulerRule()
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
database = TestDatabase.createReplacingNewPipeDatabase()
|
||||
manager = LocalPlaylistManager(database)
|
||||
}
|
||||
|
||||
@After
|
||||
fun cleanUp() {
|
||||
database.close()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun createPlaylist() {
|
||||
val NEWPIPE_URL = "https://newpipe.net/"
|
||||
val stream = StreamEntity(
|
||||
serviceId = 1, url = NEWPIPE_URL, title = "title",
|
||||
streamType = StreamType.VIDEO_STREAM, duration = 1, uploader = "uploader",
|
||||
uploaderUrl = NEWPIPE_URL
|
||||
)
|
||||
|
||||
val result = manager.createPlaylist("name", listOf(stream))
|
||||
|
||||
// This should not behave like this.
|
||||
// Currently list of all stream ids is returned instead of playlist id
|
||||
result.test().await().assertValue(listOf(1L))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun createPlaylist_emptyPlaylistMustReturnEmpty() {
|
||||
val result = manager.createPlaylist("name", emptyList())
|
||||
|
||||
// This should not behave like this.
|
||||
// It should throw an error because currently the result is null
|
||||
result.test().await().assertComplete()
|
||||
manager.playlists.test().awaitCount(1).assertValue(emptyList())
|
||||
}
|
||||
|
||||
@Test()
|
||||
fun createPlaylist_nonExistentStreamsAreUpserted() {
|
||||
val stream = StreamEntity(
|
||||
serviceId = 1, url = "https://newpipe.net/", title = "title",
|
||||
streamType = StreamType.VIDEO_STREAM, duration = 1, uploader = "uploader",
|
||||
uploaderUrl = "https://newpipe.net/"
|
||||
)
|
||||
database.streamDAO().insert(stream)
|
||||
val upserted = StreamEntity(
|
||||
serviceId = 1, url = "https://newpipe.net/2", title = "title2",
|
||||
streamType = StreamType.VIDEO_STREAM, duration = 1, uploader = "uploader",
|
||||
uploaderUrl = "https://newpipe.net/"
|
||||
)
|
||||
|
||||
val result = manager.createPlaylist("name", listOf(stream, upserted))
|
||||
|
||||
result.test().await().assertComplete()
|
||||
database.streamDAO().all.test().awaitCount(1).assertValue(listOf(stream, upserted))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package org.schabi.newpipe.testUtil
|
||||
|
||||
import androidx.room.Room
|
||||
import androidx.test.core.app.ApplicationProvider
|
||||
import org.junit.Assert.assertSame
|
||||
import org.schabi.newpipe.NewPipeDatabase
|
||||
import org.schabi.newpipe.database.AppDatabase
|
||||
|
||||
class TestDatabase {
|
||||
companion object {
|
||||
fun createReplacingNewPipeDatabase(): AppDatabase {
|
||||
val database = Room.inMemoryDatabaseBuilder(
|
||||
ApplicationProvider.getApplicationContext(),
|
||||
AppDatabase::class.java
|
||||
)
|
||||
.allowMainThreadQueries()
|
||||
.build()
|
||||
|
||||
val databaseField = NewPipeDatabase::class.java.getDeclaredField("databaseInstance")
|
||||
databaseField.isAccessible = true
|
||||
databaseField.set(NewPipeDatabase::class, database)
|
||||
|
||||
assertSame(
|
||||
"Mocking database failed!",
|
||||
database,
|
||||
NewPipeDatabase.getInstance(ApplicationProvider.getApplicationContext())
|
||||
)
|
||||
|
||||
return database
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package org.schabi.newpipe.testUtil
|
||||
|
||||
import io.reactivex.rxjava3.android.plugins.RxAndroidPlugins
|
||||
import io.reactivex.rxjava3.plugins.RxJavaPlugins
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import org.junit.rules.TestRule
|
||||
import org.junit.runner.Description
|
||||
import org.junit.runners.model.Statement
|
||||
|
||||
/**
|
||||
* Always run on [Schedulers.trampoline].
|
||||
* This executes the task in the current thread in FIFO manner.
|
||||
* This ensures that tasks are run quickly inside the tests
|
||||
* and not scheduled away to another thread for later execution
|
||||
*/
|
||||
class TrampolineSchedulerRule : TestRule {
|
||||
|
||||
private val scheduler = Schedulers.trampoline()
|
||||
|
||||
override fun apply(base: Statement, description: Description): Statement =
|
||||
object : Statement() {
|
||||
override fun evaluate() {
|
||||
try {
|
||||
RxJavaPlugins.setComputationSchedulerHandler { scheduler }
|
||||
RxJavaPlugins.setIoSchedulerHandler { scheduler }
|
||||
RxJavaPlugins.setNewThreadSchedulerHandler { scheduler }
|
||||
RxJavaPlugins.setSingleSchedulerHandler { scheduler }
|
||||
RxAndroidPlugins.setInitMainThreadSchedulerHandler { scheduler }
|
||||
|
||||
base.evaluate()
|
||||
} finally {
|
||||
RxJavaPlugins.reset()
|
||||
RxAndroidPlugins.reset()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package org.schabi.newpipe.settings;
|
||||
|
||||
import android.content.Intent;
|
||||
|
||||
import leakcanary.LeakCanary;
|
||||
|
||||
/**
|
||||
* Build variant dependent (BVD) leak canary API implementation for the debug settings fragment.
|
||||
* This class is loaded via reflection by
|
||||
* {@link DebugSettingsFragment.DebugSettingsBVDLeakCanaryAPI}.
|
||||
*/
|
||||
@SuppressWarnings("unused") // Class is used but loaded via reflection
|
||||
public class DebugSettingsBVDLeakCanary
|
||||
implements DebugSettingsFragment.DebugSettingsBVDLeakCanaryAPI {
|
||||
|
||||
@Override
|
||||
public Intent getNewLeakDisplayActivityIntent() {
|
||||
return LeakCanary.INSTANCE.newLeakDisplayActivityIntent();
|
||||
}
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
package org.schabi.newpipe.settings;
|
||||
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.preference.Preference;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
|
||||
import leakcanary.LeakCanary;
|
||||
|
||||
public class DebugSettingsFragment extends BasePreferenceFragment {
|
||||
@Override
|
||||
public void onCreatePreferences(final Bundle savedInstanceState, final String rootKey) {
|
||||
addPreferencesFromResource(R.xml.debug_settings);
|
||||
|
||||
final Preference showMemoryLeaksPreference
|
||||
= findPreference(getString(R.string.show_memory_leaks_key));
|
||||
final Preference crashTheAppPreference
|
||||
= findPreference(getString(R.string.crash_the_app_key));
|
||||
|
||||
assert showMemoryLeaksPreference != null;
|
||||
assert crashTheAppPreference != null;
|
||||
|
||||
showMemoryLeaksPreference.setOnPreferenceClickListener(preference -> {
|
||||
startActivity(LeakCanary.INSTANCE.newLeakDisplayActivityIntent());
|
||||
return true;
|
||||
});
|
||||
|
||||
crashTheAppPreference.setOnPreferenceClickListener(preference -> {
|
||||
throw new RuntimeException();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -22,7 +22,6 @@
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:logo="@mipmap/ic_launcher"
|
||||
android:requestLegacyExternalStorage="true"
|
||||
android:theme="@style/OpeningTheme"
|
||||
android:resizeableActivity="true"
|
||||
tools:ignore="AllowBackup">
|
||||
@@ -147,6 +146,7 @@
|
||||
<data android:pathPrefix="/embed/" />
|
||||
<data android:pathPrefix="/watch" />
|
||||
<data android:pathPrefix="/attribution_link" />
|
||||
<data android:pathPrefix="/shorts/" />
|
||||
<!-- channel prefix -->
|
||||
<data android:pathPrefix="/channel/" />
|
||||
<data android:pathPrefix="/user/" />
|
||||
@@ -225,6 +225,7 @@
|
||||
|
||||
<data android:scheme="http" />
|
||||
<data android:scheme="https" />
|
||||
<data android:host="tubus.eduvid.org" />
|
||||
<data android:host="invidio.us" />
|
||||
<data android:host="dev.invidio.us" />
|
||||
<data android:host="www.invidio.us" />
|
||||
@@ -232,11 +233,10 @@
|
||||
<data android:host="invidious.snopyta.org" />
|
||||
<data android:host="yewtu.be" />
|
||||
<data android:host="tube.connect.cafe" />
|
||||
<data android:host="invidious.zapashcanon.fr" />
|
||||
<data android:host="invidious.kavin.rocks" />
|
||||
<data android:host="invidious.tube" />
|
||||
<data android:host="invidious-us.kavin.rocks" />
|
||||
<data android:host="piped.kavin.rocks" />
|
||||
<data android:host="invidious.site" />
|
||||
<data android:host="invidious.xyz" />
|
||||
<data android:host="vid.mint.lgbt" />
|
||||
<data android:host="invidiou.site" />
|
||||
<data android:host="invidious.fdn.fr" />
|
||||
@@ -244,6 +244,30 @@
|
||||
<data android:host="invidious.zee.li" />
|
||||
<data android:host="vid.puffyan.us" />
|
||||
<data android:host="ytprivate.com" />
|
||||
<data android:host="invidious.namazso.eu" />
|
||||
<data android:host="invidious.silkky.cloud" />
|
||||
<data android:host="invidious.exonip.de" />
|
||||
<data android:host="inv.riverside.rocks" />
|
||||
<data android:host="invidious.blamefran.net" />
|
||||
<data android:host="invidious.moomoo.me" />
|
||||
<data android:host="ytb.trom.tf" />
|
||||
<data android:host="yt.cyberhost.uk" />
|
||||
<data android:host="y.com.cm" />
|
||||
<data android:pathPrefix="/" />
|
||||
</intent-filter>
|
||||
|
||||
<!-- y2u.be filter -->
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<action android:name="android.media.action.MEDIA_PLAY_FROM_SEARCH" />
|
||||
<action android:name="android.nfc.action.NDEF_DISCOVERED" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data android:scheme="http" />
|
||||
<data android:scheme="https" />
|
||||
<data android:host="y2u.be" />
|
||||
<data android:pathPrefix="/" />
|
||||
</intent-filter>
|
||||
|
||||
@@ -302,6 +326,7 @@
|
||||
<data android:scheme="http" />
|
||||
<data android:scheme="https" />
|
||||
|
||||
<data android:host="eduvid.org" />
|
||||
<data android:host="framatube.org" />
|
||||
<data android:host="media.assassinate-you.net" />
|
||||
<data android:host="peertube.co.uk" />
|
||||
@@ -313,10 +338,15 @@
|
||||
<data android:host="video.ploud.fr" />
|
||||
<data android:host="video.lqdn.fr" />
|
||||
<data android:host="skeptikon.fr" />
|
||||
<data android:host="media.fsfe.org" />
|
||||
|
||||
<data android:pathPrefix="/videos/" /> <!-- it contains playlists -->
|
||||
<data android:pathPrefix="/w/" /> <!-- short video URLs -->
|
||||
<data android:pathPrefix="/w/p/" /> <!-- short playlist URLs -->
|
||||
<data android:pathPrefix="/accounts/" />
|
||||
<data android:pathPrefix="/a/" /> <!-- short account URLs -->
|
||||
<data android:pathPrefix="/video-channels/" />
|
||||
<data android:pathPrefix="/c/" /> <!-- short video-channels URLs -->
|
||||
</intent-filter>
|
||||
|
||||
<!-- Bandcamp filter for tracks, albums and playlists -->
|
||||
@@ -351,6 +381,9 @@
|
||||
<service
|
||||
android:name=".RouterActivity$FetcherService"
|
||||
android:exported="false" />
|
||||
<service
|
||||
android:name=".CheckForNewAppVersion"
|
||||
android:exported="false" />
|
||||
|
||||
<!-- opting out of sending metrics to Google in Android System WebView -->
|
||||
<meta-data android:name="android.webkit.WebView.MetricsOptOut" android:value="true" />
|
||||
|
||||
@@ -51,8 +51,12 @@ import java.util.ArrayList;
|
||||
* <li>{@link #saveState()}</li>
|
||||
* <li>{@link #restoreState(Parcelable, ClassLoader)}</li>
|
||||
* </ul>
|
||||
*
|
||||
* @deprecated Switch to {@link androidx.viewpager2.widget.ViewPager2} and use
|
||||
* {@link androidx.viewpager2.adapter.FragmentStateAdapter} instead.
|
||||
*/
|
||||
@SuppressWarnings("deprecation")
|
||||
@Deprecated
|
||||
public abstract class FragmentStatePagerAdapterMenuWorkaround extends PagerAdapter {
|
||||
private static final String TAG = "FragmentStatePagerAdapt";
|
||||
private static final boolean DEBUG = false;
|
||||
@@ -86,9 +90,10 @@ public abstract class FragmentStatePagerAdapterMenuWorkaround extends PagerAdapt
|
||||
private final int mBehavior;
|
||||
private FragmentTransaction mCurTransaction = null;
|
||||
|
||||
private final ArrayList<Fragment.SavedState> mSavedState = new ArrayList<Fragment.SavedState>();
|
||||
private final ArrayList<Fragment> mFragments = new ArrayList<Fragment>();
|
||||
private final ArrayList<Fragment.SavedState> mSavedState = new ArrayList<>();
|
||||
private final ArrayList<Fragment> mFragments = new ArrayList<>();
|
||||
private Fragment mCurrentPrimaryItem = null;
|
||||
private boolean mExecutingFinishUpdate;
|
||||
|
||||
/**
|
||||
* Constructor for {@link FragmentStatePagerAdapterMenuWorkaround}
|
||||
@@ -208,7 +213,7 @@ public abstract class FragmentStatePagerAdapterMenuWorkaround extends PagerAdapt
|
||||
mFragments.set(position, null);
|
||||
|
||||
mCurTransaction.remove(fragment);
|
||||
if (fragment == mCurrentPrimaryItem) {
|
||||
if (fragment.equals(mCurrentPrimaryItem)) {
|
||||
mCurrentPrimaryItem = null;
|
||||
}
|
||||
}
|
||||
@@ -247,7 +252,19 @@ public abstract class FragmentStatePagerAdapterMenuWorkaround extends PagerAdapt
|
||||
@Override
|
||||
public void finishUpdate(@NonNull final ViewGroup container) {
|
||||
if (mCurTransaction != null) {
|
||||
mCurTransaction.commitNowAllowingStateLoss();
|
||||
// We drop any transactions that attempt to be committed
|
||||
// from a re-entrant call to finishUpdate(). We need to
|
||||
// do this as a workaround for Robolectric running measure/layout
|
||||
// calls inline rather than allowing them to be posted
|
||||
// as they would on a real device.
|
||||
if (!mExecutingFinishUpdate) {
|
||||
try {
|
||||
mExecutingFinishUpdate = true;
|
||||
mCurTransaction.commitNowAllowingStateLoss();
|
||||
} finally {
|
||||
mExecutingFinishUpdate = false;
|
||||
}
|
||||
}
|
||||
mCurTransaction = null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,8 +63,10 @@ public final class FlingBehavior extends AppBarLayout.Behavior {
|
||||
return consumed == dy;
|
||||
}
|
||||
|
||||
public boolean onInterceptTouchEvent(final CoordinatorLayout parent, final AppBarLayout child,
|
||||
final MotionEvent ev) {
|
||||
@Override
|
||||
public boolean onInterceptTouchEvent(@NonNull final CoordinatorLayout parent,
|
||||
@NonNull final AppBarLayout child,
|
||||
@NonNull final MotionEvent ev) {
|
||||
for (final Integer element : skipInterceptionOfElements) {
|
||||
final View view = child.findViewById(element);
|
||||
if (view != null) {
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.apache.commons.text.similarity;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
* A matching algorithm that is similar to the searching algorithms implemented in editors such
|
||||
* as Sublime Text, TextMate, Atom and others.
|
||||
*
|
||||
* <p>
|
||||
* One point is given for every matched character. Subsequent matches yield two bonus points.
|
||||
* A higher score indicates a higher similarity.
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
* This code has been adapted from Apache Commons Lang 3.3.
|
||||
* </p>
|
||||
*
|
||||
* @since 1.0
|
||||
*
|
||||
* Note: This class was forked from
|
||||
* <a href="https://git.io/JyYJg">
|
||||
* apache/commons-text (8cfdafc) FuzzyScore.java
|
||||
* </a>
|
||||
*/
|
||||
public class FuzzyScore {
|
||||
|
||||
/**
|
||||
* Locale used to change the case of text.
|
||||
*/
|
||||
private final Locale locale;
|
||||
|
||||
|
||||
/**
|
||||
* This returns a {@link Locale}-specific {@link FuzzyScore}.
|
||||
*
|
||||
* @param locale The string matching logic is case insensitive.
|
||||
A {@link Locale} is necessary to normalize both Strings to lower case.
|
||||
* @throws IllegalArgumentException
|
||||
* This is thrown if the {@link Locale} parameter is {@code null}.
|
||||
*/
|
||||
public FuzzyScore(final Locale locale) {
|
||||
if (locale == null) {
|
||||
throw new IllegalArgumentException("Locale must not be null");
|
||||
}
|
||||
this.locale = locale;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the Fuzzy Score which indicates the similarity score between two
|
||||
* Strings.
|
||||
*
|
||||
* <pre>
|
||||
* score.fuzzyScore(null, null) = IllegalArgumentException
|
||||
* score.fuzzyScore("not null", null) = IllegalArgumentException
|
||||
* score.fuzzyScore(null, "not null") = IllegalArgumentException
|
||||
* score.fuzzyScore("", "") = 0
|
||||
* score.fuzzyScore("Workshop", "b") = 0
|
||||
* score.fuzzyScore("Room", "o") = 1
|
||||
* score.fuzzyScore("Workshop", "w") = 1
|
||||
* score.fuzzyScore("Workshop", "ws") = 2
|
||||
* score.fuzzyScore("Workshop", "wo") = 4
|
||||
* score.fuzzyScore("Apache Software Foundation", "asf") = 3
|
||||
* </pre>
|
||||
*
|
||||
* @param term a full term that should be matched against, must not be null
|
||||
* @param query the query that will be matched against a term, must not be
|
||||
* null
|
||||
* @return result score
|
||||
* @throws IllegalArgumentException if the term or query is {@code null}
|
||||
*/
|
||||
public Integer fuzzyScore(final CharSequence term, final CharSequence query) {
|
||||
if (term == null || query == null) {
|
||||
throw new IllegalArgumentException("CharSequences must not be null");
|
||||
}
|
||||
|
||||
// fuzzy logic is case insensitive. We normalize the Strings to lower
|
||||
// case right from the start. Turning characters to lower case
|
||||
// via Character.toLowerCase(char) is unfortunately insufficient
|
||||
// as it does not accept a locale.
|
||||
final String termLowerCase = term.toString().toLowerCase(locale);
|
||||
final String queryLowerCase = query.toString().toLowerCase(locale);
|
||||
|
||||
// the resulting score
|
||||
int score = 0;
|
||||
|
||||
// the position in the term which will be scanned next for potential
|
||||
// query character matches
|
||||
int termIndex = 0;
|
||||
|
||||
// index of the previously matched character in the term
|
||||
int previousMatchingCharacterIndex = Integer.MIN_VALUE;
|
||||
|
||||
for (int queryIndex = 0; queryIndex < queryLowerCase.length(); queryIndex++) {
|
||||
final char queryChar = queryLowerCase.charAt(queryIndex);
|
||||
|
||||
boolean termCharacterMatchFound = false;
|
||||
for (; termIndex < termLowerCase.length()
|
||||
&& !termCharacterMatchFound; termIndex++) {
|
||||
final char termChar = termLowerCase.charAt(termIndex);
|
||||
|
||||
if (queryChar == termChar) {
|
||||
// simple character matches result in one point
|
||||
score++;
|
||||
|
||||
// subsequent character matches further improve
|
||||
// the score.
|
||||
if (previousMatchingCharacterIndex + 1 == termIndex) {
|
||||
score += 2;
|
||||
}
|
||||
|
||||
previousMatchingCharacterIndex = termIndex;
|
||||
|
||||
// we can leave the nested loop. Every character in the
|
||||
// query can match at most one character in the term.
|
||||
termCharacterMatchFound = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return score;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the locale.
|
||||
*
|
||||
* @return The locale
|
||||
*/
|
||||
public Locale getLocale() {
|
||||
return locale;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,34 +1,26 @@
|
||||
package org.schabi.newpipe;
|
||||
|
||||
import android.app.NotificationChannel;
|
||||
import android.app.NotificationManager;
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.Build;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.app.NotificationChannelCompat;
|
||||
import androidx.core.app.NotificationManagerCompat;
|
||||
import androidx.multidex.MultiDexApplication;
|
||||
import androidx.preference.PreferenceManager;
|
||||
|
||||
import com.nostra13.universalimageloader.cache.memory.impl.LRULimitedMemoryCache;
|
||||
import com.nostra13.universalimageloader.core.ImageLoader;
|
||||
import com.nostra13.universalimageloader.core.ImageLoaderConfiguration;
|
||||
import com.jakewharton.processphoenix.ProcessPhoenix;
|
||||
|
||||
import org.acra.ACRA;
|
||||
import org.acra.config.ACRAConfigurationException;
|
||||
import org.acra.config.CoreConfiguration;
|
||||
import org.acra.config.CoreConfigurationBuilder;
|
||||
import org.schabi.newpipe.error.ErrorActivity;
|
||||
import org.schabi.newpipe.error.ErrorInfo;
|
||||
import org.schabi.newpipe.error.ReCaptchaActivity;
|
||||
import org.schabi.newpipe.error.UserAction;
|
||||
import org.schabi.newpipe.extractor.NewPipe;
|
||||
import org.schabi.newpipe.extractor.downloader.Downloader;
|
||||
import org.schabi.newpipe.ktx.ExceptionUtils;
|
||||
import org.schabi.newpipe.settings.SettingsActivity;
|
||||
import org.schabi.newpipe.settings.NewPipeSettings;
|
||||
import org.schabi.newpipe.util.Localization;
|
||||
import org.schabi.newpipe.util.PicassoHelper;
|
||||
import org.schabi.newpipe.util.ServiceHelper;
|
||||
import org.schabi.newpipe.util.StateSaver;
|
||||
|
||||
@@ -39,7 +31,6 @@ import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import io.reactivex.rxjava3.disposables.Disposable;
|
||||
import io.reactivex.rxjava3.exceptions.CompositeException;
|
||||
import io.reactivex.rxjava3.exceptions.MissingBackpressureException;
|
||||
import io.reactivex.rxjava3.exceptions.OnErrorNotImplementedException;
|
||||
@@ -66,12 +57,9 @@ import io.reactivex.rxjava3.plugins.RxJavaPlugins;
|
||||
*/
|
||||
|
||||
public class App extends MultiDexApplication {
|
||||
protected static final String TAG = App.class.toString();
|
||||
private static App app;
|
||||
public static final String PACKAGE_NAME = BuildConfig.APPLICATION_ID;
|
||||
|
||||
@Nullable
|
||||
private Disposable disposable = null;
|
||||
private static final String TAG = App.class.toString();
|
||||
private static App app;
|
||||
|
||||
@NonNull
|
||||
public static App getApp() {
|
||||
@@ -90,8 +78,14 @@ public class App extends MultiDexApplication {
|
||||
|
||||
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
|
||||
SettingsActivity.initSettings(this);
|
||||
NewPipeSettings.initSettings(this);
|
||||
|
||||
NewPipe.init(getDownloader(),
|
||||
Localization.getPreferredLocalization(this),
|
||||
@@ -104,20 +98,20 @@ public class App extends MultiDexApplication {
|
||||
ServiceHelper.initServices(this);
|
||||
|
||||
// Initialize image loader
|
||||
ImageLoader.getInstance().init(getImageLoaderConfigurations(10, 50));
|
||||
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
|
||||
PicassoHelper.init(this);
|
||||
PicassoHelper.setShouldLoadImages(
|
||||
prefs.getBoolean(getString(R.string.download_thumbnail_key), true));
|
||||
PicassoHelper.setIndicatorsEnabled(MainActivity.DEBUG
|
||||
&& prefs.getBoolean(getString(R.string.show_image_indicators_key), false));
|
||||
|
||||
configureRxJavaErrorHandler();
|
||||
|
||||
// Check for new version
|
||||
disposable = CheckForNewAppVersion.checkNewVersion(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTerminate() {
|
||||
if (disposable != null) {
|
||||
disposable.dispose();
|
||||
}
|
||||
super.onTerminate();
|
||||
PicassoHelper.terminate();
|
||||
}
|
||||
|
||||
protected Downloader getDownloader() {
|
||||
@@ -202,15 +196,6 @@ public class App extends MultiDexApplication {
|
||||
});
|
||||
}
|
||||
|
||||
private ImageLoaderConfiguration getImageLoaderConfigurations(final int memoryCacheSizeMb,
|
||||
final int diskCacheSizeMb) {
|
||||
return new ImageLoaderConfiguration.Builder(this)
|
||||
.memoryCache(new LRULimitedMemoryCache(memoryCacheSizeMb * 1024 * 1024))
|
||||
.diskCacheSize(diskCacheSizeMb * 1024 * 1024)
|
||||
.imageDownloader(new ImageDownloader(getApplicationContext()))
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
@@ -220,55 +205,49 @@ public class App extends MultiDexApplication {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
final CoreConfiguration acraConfig = new CoreConfigurationBuilder(this)
|
||||
.setBuildConfigClass(BuildConfig.class)
|
||||
.build();
|
||||
ACRA.init(this, acraConfig);
|
||||
} catch (final ACRAConfigurationException exception) {
|
||||
exception.printStackTrace();
|
||||
ErrorActivity.reportError(this, new ErrorInfo(exception,
|
||||
UserAction.SOMETHING_ELSE, "Could not initialize ACRA crash report"));
|
||||
}
|
||||
final CoreConfigurationBuilder acraConfig = new CoreConfigurationBuilder(this)
|
||||
.withBuildConfigClass(BuildConfig.class);
|
||||
ACRA.init(this, acraConfig);
|
||||
}
|
||||
|
||||
private void initNotificationChannels() {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
|
||||
return;
|
||||
}
|
||||
// Keep the importance below DEFAULT to avoid making noise on every notification update for
|
||||
// the main and update channels
|
||||
final NotificationChannelCompat mainChannel = 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();
|
||||
|
||||
String id = getString(R.string.notification_channel_id);
|
||||
String name = getString(R.string.notification_channel_name);
|
||||
String description = getString(R.string.notification_channel_description);
|
||||
final NotificationChannelCompat appUpdateChannel = 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();
|
||||
|
||||
// Keep this below DEFAULT to avoid making noise on every notification update for the main
|
||||
// and update channels
|
||||
int importance = NotificationManager.IMPORTANCE_LOW;
|
||||
final NotificationChannelCompat hashChannel = 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();
|
||||
|
||||
final NotificationChannel mainChannel = new NotificationChannel(id, name, importance);
|
||||
mainChannel.setDescription(description);
|
||||
final NotificationChannelCompat errorReportChannel = new NotificationChannelCompat
|
||||
.Builder(getString(R.string.error_report_channel_id),
|
||||
NotificationManagerCompat.IMPORTANCE_LOW)
|
||||
.setName(getString(R.string.error_report_channel_name))
|
||||
.setDescription(getString(R.string.error_report_channel_description))
|
||||
.build();
|
||||
|
||||
id = getString(R.string.app_update_notification_channel_id);
|
||||
name = getString(R.string.app_update_notification_channel_name);
|
||||
description = getString(R.string.app_update_notification_channel_description);
|
||||
|
||||
final NotificationChannel appUpdateChannel = new NotificationChannel(id, name, importance);
|
||||
appUpdateChannel.setDescription(description);
|
||||
|
||||
id = getString(R.string.hash_channel_id);
|
||||
name = getString(R.string.hash_channel_name);
|
||||
description = getString(R.string.hash_channel_description);
|
||||
importance = NotificationManager.IMPORTANCE_HIGH;
|
||||
|
||||
final NotificationChannel hashChannel = new NotificationChannel(id, name, importance);
|
||||
hashChannel.setDescription(description);
|
||||
|
||||
final NotificationManager notificationManager = getSystemService(NotificationManager.class);
|
||||
notificationManager.createNotificationChannels(Arrays.asList(mainChannel,
|
||||
appUpdateChannel, hashChannel));
|
||||
final NotificationManagerCompat notificationManager = NotificationManagerCompat.from(this);
|
||||
notificationManager.createNotificationChannelsCompat(Arrays.asList(mainChannel,
|
||||
appUpdateChannel, hashChannel, errorReportChannel));
|
||||
}
|
||||
|
||||
protected boolean isDisposedRxExceptionsReported() {
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -10,21 +10,17 @@ import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
|
||||
import com.nostra13.universalimageloader.core.ImageLoader;
|
||||
|
||||
import icepick.Icepick;
|
||||
import icepick.State;
|
||||
import leakcanary.AppWatcher;
|
||||
|
||||
public abstract class BaseFragment extends Fragment {
|
||||
public static final ImageLoader IMAGE_LOADER = ImageLoader.getInstance();
|
||||
protected final String TAG = getClass().getSimpleName() + "@" + Integer.toHexString(hashCode());
|
||||
protected final boolean DEBUG = MainActivity.DEBUG;
|
||||
protected static final boolean DEBUG = MainActivity.DEBUG;
|
||||
protected AppCompatActivity activity;
|
||||
//These values are used for controlling fragments when they are part of the frontpage
|
||||
@State
|
||||
protected boolean useAsFrontPage = false;
|
||||
private boolean mIsVisibleToUser = false;
|
||||
|
||||
public void useAsFrontPage(final boolean value) {
|
||||
useAsFrontPage = value;
|
||||
@@ -88,12 +84,6 @@ public abstract class BaseFragment extends Fragment {
|
||||
AppWatcher.INSTANCE.getObjectWatcher().watch(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setUserVisibleHint(final boolean isVisibleToUser) {
|
||||
super.setUserVisibleHint(isVisibleToUser);
|
||||
mIsVisibleToUser = isVisibleToUser;
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Init
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
@@ -112,8 +102,7 @@ public abstract class BaseFragment extends Fragment {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "setTitle() called with: title = [" + title + "]");
|
||||
}
|
||||
if ((!useAsFrontPage || mIsVisibleToUser)
|
||||
&& (activity != null && activity.getSupportActionBar() != null)) {
|
||||
if (!useAsFrontPage && activity != null && activity.getSupportActionBar() != null) {
|
||||
activity.getSupportActionBar().setDisplayShowTitleEnabled(true);
|
||||
activity.getSupportActionBar().setTitle(title);
|
||||
}
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
package org.schabi.newpipe;
|
||||
|
||||
import android.app.Application;
|
||||
import android.app.IntentService;
|
||||
import android.app.PendingIntent;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.pm.PackageInfo;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.content.pm.Signature;
|
||||
import android.net.ConnectivityManager;
|
||||
import android.net.Uri;
|
||||
import android.util.Log;
|
||||
|
||||
@@ -15,18 +14,21 @@ import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.app.NotificationCompat;
|
||||
import androidx.core.app.NotificationManagerCompat;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.core.content.pm.PackageInfoCompat;
|
||||
import androidx.preference.PreferenceManager;
|
||||
|
||||
import com.grack.nanojson.JsonObject;
|
||||
import com.grack.nanojson.JsonParser;
|
||||
import com.grack.nanojson.JsonParserException;
|
||||
|
||||
import org.schabi.newpipe.error.ErrorActivity;
|
||||
import org.schabi.newpipe.error.ErrorInfo;
|
||||
import org.schabi.newpipe.error.ErrorUtil;
|
||||
import org.schabi.newpipe.error.UserAction;
|
||||
import org.schabi.newpipe.extractor.downloader.Response;
|
||||
import org.schabi.newpipe.extractor.exceptions.ReCaptchaException;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
@@ -34,19 +36,18 @@ import java.security.cert.CertificateEncodingException;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.security.cert.CertificateFactory;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.List;
|
||||
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.rxjava3.core.Maybe;
|
||||
import io.reactivex.rxjava3.disposables.Disposable;
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers;
|
||||
|
||||
public final class CheckForNewAppVersion {
|
||||
private CheckForNewAppVersion() { }
|
||||
public final class CheckForNewAppVersion extends IntentService {
|
||||
public CheckForNewAppVersion() {
|
||||
super("CheckForNewAppVersion");
|
||||
}
|
||||
|
||||
private static final boolean DEBUG = MainActivity.DEBUG;
|
||||
private static final String TAG = CheckForNewAppVersion.class.getSimpleName();
|
||||
|
||||
private static final String GITHUB_APK_SHA1
|
||||
// Public key of the certificate that is used in NewPipe release versions
|
||||
private static final String RELEASE_CERT_PUBLIC_KEY_SHA1
|
||||
= "B0:2E:90:7C:1C:D6:FC:57:C3:35:F0:88:D0:8F:50:5F:94:E4:D2:15";
|
||||
private static final String NEWPIPE_API_URL = "https://newpipe.net/api/data.json";
|
||||
|
||||
@@ -58,25 +59,27 @@ public final class CheckForNewAppVersion {
|
||||
*/
|
||||
@NonNull
|
||||
private static String getCertificateSHA1Fingerprint(@NonNull final Application application) {
|
||||
final PackageInfo packageInfo;
|
||||
final List<Signature> signatures;
|
||||
try {
|
||||
packageInfo = application.getPackageManager().getPackageInfo(
|
||||
application.getPackageName(), PackageManager.GET_SIGNATURES);
|
||||
signatures = PackageInfoCompat.getSignatures(application.getPackageManager(),
|
||||
application.getPackageName());
|
||||
} catch (final PackageManager.NameNotFoundException e) {
|
||||
ErrorActivity.reportError(application, new ErrorInfo(e,
|
||||
ErrorUtil.createNotification(application, new ErrorInfo(e,
|
||||
UserAction.CHECK_FOR_NEW_APP_VERSION, "Could not find package info"));
|
||||
return "";
|
||||
}
|
||||
if (signatures.isEmpty()) {
|
||||
return "";
|
||||
}
|
||||
|
||||
final X509Certificate c;
|
||||
try {
|
||||
final Signature[] signatures = packageInfo.signatures;
|
||||
final byte[] cert = signatures[0].toByteArray();
|
||||
final byte[] cert = signatures.get(0).toByteArray();
|
||||
final InputStream input = new ByteArrayInputStream(cert);
|
||||
final CertificateFactory cf = CertificateFactory.getInstance("X509");
|
||||
c = (X509Certificate) cf.generateCertificate(input);
|
||||
} catch (final CertificateException e) {
|
||||
ErrorActivity.reportError(application, new ErrorInfo(e,
|
||||
ErrorUtil.createNotification(application, new ErrorInfo(e,
|
||||
UserAction.CHECK_FOR_NEW_APP_VERSION, "Certificate error"));
|
||||
return "";
|
||||
}
|
||||
@@ -86,7 +89,7 @@ public final class CheckForNewAppVersion {
|
||||
final byte[] publicKey = md.digest(c.getEncoded());
|
||||
return byte2HexFormatted(publicKey);
|
||||
} catch (NoSuchAlgorithmException | CertificateEncodingException e) {
|
||||
ErrorActivity.reportError(application, new ErrorInfo(e,
|
||||
ErrorUtil.createNotification(application, new ErrorInfo(e,
|
||||
UserAction.CHECK_FOR_NEW_APP_VERSION, "Could not retrieve SHA1 key"));
|
||||
return "";
|
||||
}
|
||||
@@ -125,123 +128,137 @@ public final class CheckForNewAppVersion {
|
||||
final String versionName,
|
||||
final String apkLocationUrl,
|
||||
final int versionCode) {
|
||||
final int notificationId = 2000;
|
||||
|
||||
if (BuildConfig.VERSION_CODE < versionCode) {
|
||||
// A pending intent to open the apk location url in the browser.
|
||||
final Intent viewIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(apkLocationUrl));
|
||||
|
||||
final Intent intent = new Intent(Intent.ACTION_CHOOSER);
|
||||
intent.putExtra(Intent.EXTRA_INTENT, viewIntent);
|
||||
intent.putExtra(Intent.EXTRA_TITLE, R.string.open_with);
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
|
||||
final PendingIntent pendingIntent
|
||||
= PendingIntent.getActivity(application, 0, intent, 0);
|
||||
|
||||
final String channelId = application
|
||||
.getString(R.string.app_update_notification_channel_id);
|
||||
final NotificationCompat.Builder notificationBuilder
|
||||
= new NotificationCompat.Builder(application, channelId)
|
||||
.setSmallIcon(R.drawable.ic_newpipe_update)
|
||||
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
|
||||
.setContentIntent(pendingIntent)
|
||||
.setAutoCancel(true)
|
||||
.setContentTitle(application
|
||||
.getString(R.string.app_update_notification_content_title))
|
||||
.setContentText(application
|
||||
.getString(R.string.app_update_notification_content_text)
|
||||
+ " " + versionName);
|
||||
|
||||
final NotificationManagerCompat notificationManager
|
||||
= NotificationManagerCompat.from(application);
|
||||
notificationManager.notify(notificationId, notificationBuilder.build());
|
||||
if (BuildConfig.VERSION_CODE >= versionCode) {
|
||||
return;
|
||||
}
|
||||
|
||||
// A pending intent to open the apk location url in the browser.
|
||||
final Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(apkLocationUrl));
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
final PendingIntent pendingIntent
|
||||
= PendingIntent.getActivity(application, 0, intent, 0);
|
||||
|
||||
final String channelId = application
|
||||
.getString(R.string.app_update_notification_channel_id);
|
||||
final NotificationCompat.Builder notificationBuilder
|
||||
= new NotificationCompat.Builder(application, channelId)
|
||||
.setSmallIcon(R.drawable.ic_newpipe_update)
|
||||
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
|
||||
.setContentIntent(pendingIntent)
|
||||
.setAutoCancel(true)
|
||||
.setContentTitle(application
|
||||
.getString(R.string.app_update_notification_content_title))
|
||||
.setContentText(application
|
||||
.getString(R.string.app_update_notification_content_text)
|
||||
+ " " + versionName);
|
||||
|
||||
final NotificationManagerCompat notificationManager
|
||||
= NotificationManagerCompat.from(application);
|
||||
notificationManager.notify(2000, notificationBuilder.build());
|
||||
}
|
||||
|
||||
private static boolean isConnected(@NonNull final App app) {
|
||||
final ConnectivityManager connectivityManager =
|
||||
ContextCompat.getSystemService(app, ConnectivityManager.class);
|
||||
return connectivityManager != null && connectivityManager.getActiveNetworkInfo() != null
|
||||
&& connectivityManager.getActiveNetworkInfo().isConnected();
|
||||
public static boolean isReleaseApk(@NonNull final App app) {
|
||||
return getCertificateSHA1Fingerprint(app).equals(RELEASE_CERT_PUBLIC_KEY_SHA1);
|
||||
}
|
||||
|
||||
public static boolean isGithubApk(@NonNull final App app) {
|
||||
return getCertificateSHA1Fingerprint(app).equals(GITHUB_APK_SHA1);
|
||||
}
|
||||
private void checkNewVersion() throws IOException, ReCaptchaException {
|
||||
final App app = App.getApp();
|
||||
|
||||
@Nullable
|
||||
public static Disposable checkNewVersion(@NonNull final App app) {
|
||||
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(app);
|
||||
final NewVersionManager manager = new NewVersionManager();
|
||||
|
||||
// Check if user has enabled/disabled update checking
|
||||
// and if the current apk is a github one or not.
|
||||
if (!prefs.getBoolean(app.getString(R.string.update_app_key), true) || !isGithubApk(app)) {
|
||||
return null;
|
||||
// Check if the current apk is a github one or not.
|
||||
if (!isReleaseApk(app)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the last request has happened a certain time ago
|
||||
// to reduce the number of API requests.
|
||||
final long expiry = prefs.getLong(app.getString(R.string.update_expiry_key), 0);
|
||||
if (!manager.isExpired(expiry)) {
|
||||
return null;
|
||||
return;
|
||||
}
|
||||
|
||||
return Maybe
|
||||
.fromCallable(() -> {
|
||||
if (!isConnected(app)) {
|
||||
return null;
|
||||
}
|
||||
// Make a network request to get latest NewPipe data.
|
||||
final Response response = DownloaderImpl.getInstance().get(NEWPIPE_API_URL);
|
||||
handleResponse(response, manager, prefs, app);
|
||||
}
|
||||
|
||||
// Make a network request to get latest NewPipe data.
|
||||
return DownloaderImpl.getInstance().get(NEWPIPE_API_URL);
|
||||
})
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
response -> {
|
||||
try {
|
||||
// Store a timestamp which needs to be exceeded,
|
||||
// before a new request to the API is made.
|
||||
final long newExpiry = manager
|
||||
.coerceExpiry(response.getHeader("expires"));
|
||||
prefs.edit()
|
||||
.putLong(app.getString(R.string.update_expiry_key), newExpiry)
|
||||
.apply();
|
||||
} catch (final Exception e) {
|
||||
if (DEBUG) {
|
||||
Log.w(TAG, "Could not extract and save new expiry date", e);
|
||||
}
|
||||
}
|
||||
private void handleResponse(@NonNull final Response response,
|
||||
@NonNull final NewVersionManager manager,
|
||||
@NonNull final SharedPreferences prefs,
|
||||
@NonNull final App app) {
|
||||
try {
|
||||
// Store a timestamp which needs to be exceeded,
|
||||
// before a new request to the API is made.
|
||||
final long newExpiry = manager
|
||||
.coerceExpiry(response.getHeader("expires"));
|
||||
prefs.edit()
|
||||
.putLong(app.getString(R.string.update_expiry_key), newExpiry)
|
||||
.apply();
|
||||
} catch (final Exception e) {
|
||||
if (DEBUG) {
|
||||
Log.w(TAG, "Could not extract and save new expiry date", e);
|
||||
}
|
||||
}
|
||||
|
||||
// Parse the json from the response.
|
||||
try {
|
||||
final JsonObject githubStableObject = JsonParser.object()
|
||||
.from(response.responseBody()).getObject("flavors")
|
||||
.getObject("github").getObject("stable");
|
||||
// Parse the json from the response.
|
||||
try {
|
||||
|
||||
final String versionName = githubStableObject
|
||||
.getString("version");
|
||||
final int versionCode = githubStableObject
|
||||
.getInt("version_code");
|
||||
final String apkLocationUrl = githubStableObject
|
||||
.getString("apk");
|
||||
final JsonObject githubStableObject = JsonParser.object()
|
||||
.from(response.responseBody()).getObject("flavors")
|
||||
.getObject("github").getObject("stable");
|
||||
|
||||
final String versionName = githubStableObject
|
||||
.getString("version");
|
||||
final int versionCode = githubStableObject
|
||||
.getInt("version_code");
|
||||
final String apkLocationUrl = githubStableObject
|
||||
.getString("apk");
|
||||
|
||||
compareAppVersionAndShowNotification(app, versionName,
|
||||
apkLocationUrl, versionCode);
|
||||
} catch (final JsonParserException e) {
|
||||
// Most likely something is wrong in data received from NEWPIPE_API_URL.
|
||||
// Do not alarm user and fail silently.
|
||||
if (DEBUG) {
|
||||
Log.w(TAG, "Could not get NewPipe API: invalid json", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Start a new service which
|
||||
* checks if all conditions for performing a version check are met,
|
||||
* fetches the API endpoint {@link #NEWPIPE_API_URL} containing info
|
||||
* about the latest NewPipe version
|
||||
* and displays a notification about ana available update.
|
||||
* <br>
|
||||
* Following conditions need to be met, before data is request from the server:
|
||||
* <ul>
|
||||
* <li> The app is signed with the correct signing key (by TeamNewPipe / schabi).
|
||||
* If the signing key differs from the one used upstream, the update cannot be installed.</li>
|
||||
* <li>The user enabled searching for and notifying about updates in the settings.</li>
|
||||
* <li>The app did not recently check for updates.
|
||||
* We do not want to make unnecessary connections and DOS our servers.</li>
|
||||
* </ul>
|
||||
* <b>Must not be executed</b> when the app is in background.
|
||||
*/
|
||||
public static void startNewVersionCheckService() {
|
||||
final Intent intent = new Intent(App.getApp().getApplicationContext(),
|
||||
CheckForNewAppVersion.class);
|
||||
App.getApp().startService(intent);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onHandleIntent(@Nullable final Intent intent) {
|
||||
try {
|
||||
checkNewVersion();
|
||||
} catch (final IOException e) {
|
||||
Log.w(TAG, "Could not fetch NewPipe API: probably network problem", e);
|
||||
} catch (final ReCaptchaException e) {
|
||||
Log.e(TAG, "ReCaptchaException should never happen here.", e);
|
||||
}
|
||||
|
||||
compareAppVersionAndShowNotification(app, versionName,
|
||||
apkLocationUrl, versionCode);
|
||||
} catch (final JsonParserException e) {
|
||||
// connectivity problems, do not alarm user and fail silently
|
||||
if (DEBUG) {
|
||||
Log.w(TAG, "Could not get NewPipe API: invalid json", e);
|
||||
}
|
||||
}
|
||||
},
|
||||
e -> {
|
||||
// connectivity problems, do not alarm user and fail silently
|
||||
if (DEBUG) {
|
||||
Log.w(TAG, "Could not get NewPipe API: network problem", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,6 @@ import org.schabi.newpipe.util.InfoCache;
|
||||
import org.schabi.newpipe.util.TLSSocketFactoryCompat;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.security.KeyManagementException;
|
||||
import java.security.KeyStore;
|
||||
import java.security.KeyStoreException;
|
||||
@@ -194,36 +193,6 @@ public final class DownloaderImpl extends Downloader {
|
||||
}
|
||||
}
|
||||
|
||||
public InputStream stream(final String siteUrl) throws IOException {
|
||||
try {
|
||||
final okhttp3.Request.Builder requestBuilder = new okhttp3.Request.Builder()
|
||||
.method("GET", null).url(siteUrl)
|
||||
.addHeader("User-Agent", USER_AGENT);
|
||||
|
||||
final String cookies = getCookies(siteUrl);
|
||||
if (!cookies.isEmpty()) {
|
||||
requestBuilder.addHeader("Cookie", cookies);
|
||||
}
|
||||
|
||||
final okhttp3.Request request = requestBuilder.build();
|
||||
final okhttp3.Response response = client.newCall(request).execute();
|
||||
final ResponseBody body = response.body();
|
||||
|
||||
if (response.code() == 429) {
|
||||
throw new ReCaptchaException("reCaptcha Challenge requested", siteUrl);
|
||||
}
|
||||
|
||||
if (body == null) {
|
||||
response.close();
|
||||
return null;
|
||||
}
|
||||
|
||||
return body.byteStream();
|
||||
} catch (final ReCaptchaException e) {
|
||||
throw new IOException(e.getMessage(), e.getCause());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Response execute(@NonNull final Request request)
|
||||
throws IOException, ReCaptchaException {
|
||||
|
||||
@@ -6,6 +6,8 @@ import android.content.Intent;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
|
||||
import org.schabi.newpipe.util.NavigationHelper;
|
||||
|
||||
/*
|
||||
* Copyright (C) Hans-Christoph Steiner 2016 <hans@eds.org>
|
||||
* ExitActivity.java is part of NewPipe.
|
||||
@@ -48,6 +50,6 @@ public class ExitActivity extends Activity {
|
||||
finish();
|
||||
}
|
||||
|
||||
System.exit(0);
|
||||
NavigationHelper.restartApp(this);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
package org.schabi.newpipe;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.res.Resources;
|
||||
|
||||
import androidx.preference.PreferenceManager;
|
||||
|
||||
import com.nostra13.universalimageloader.core.download.BaseImageDownloader;
|
||||
|
||||
import org.schabi.newpipe.extractor.NewPipe;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
public class ImageDownloader extends BaseImageDownloader {
|
||||
private final Resources resources;
|
||||
private final SharedPreferences preferences;
|
||||
private final String downloadThumbnailKey;
|
||||
|
||||
public ImageDownloader(final Context context) {
|
||||
super(context);
|
||||
this.resources = context.getResources();
|
||||
this.preferences = PreferenceManager.getDefaultSharedPreferences(context);
|
||||
this.downloadThumbnailKey = context.getString(R.string.download_thumbnail_key);
|
||||
}
|
||||
|
||||
private boolean isDownloadingThumbnail() {
|
||||
return preferences.getBoolean(downloadThumbnailKey, true);
|
||||
}
|
||||
|
||||
@SuppressLint("ResourceType")
|
||||
@Override
|
||||
public InputStream getStream(final String imageUri, final Object extra) throws IOException {
|
||||
if (isDownloadingThumbnail()) {
|
||||
return super.getStream(imageUri, extra);
|
||||
} else {
|
||||
return resources.openRawResource(R.drawable.dummy_thumbnail_dark);
|
||||
}
|
||||
}
|
||||
|
||||
protected InputStream getStreamFromNetwork(final String imageUri, final Object extra)
|
||||
throws IOException {
|
||||
final DownloaderImpl downloader = (DownloaderImpl) NewPipe.getDownloader();
|
||||
return downloader.stream(imageUri);
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,9 @@
|
||||
|
||||
package org.schabi.newpipe;
|
||||
|
||||
import static org.schabi.newpipe.CheckForNewAppVersion.startNewVersionCheckService;
|
||||
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
|
||||
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
@@ -60,7 +63,7 @@ import org.schabi.newpipe.databinding.DrawerHeaderBinding;
|
||||
import org.schabi.newpipe.databinding.DrawerLayoutBinding;
|
||||
import org.schabi.newpipe.databinding.InstanceSpinnerLayoutBinding;
|
||||
import org.schabi.newpipe.databinding.ToolbarLayoutBinding;
|
||||
import org.schabi.newpipe.error.ErrorActivity;
|
||||
import org.schabi.newpipe.error.ErrorUtil;
|
||||
import org.schabi.newpipe.extractor.NewPipe;
|
||||
import org.schabi.newpipe.extractor.StreamingService;
|
||||
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||
@@ -91,8 +94,6 @@ import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
|
||||
|
||||
public class MainActivity extends AppCompatActivity {
|
||||
private static final String TAG = "MainActivity";
|
||||
@SuppressWarnings("ConstantConditions")
|
||||
@@ -156,7 +157,7 @@ public class MainActivity extends AppCompatActivity {
|
||||
try {
|
||||
setupDrawer();
|
||||
} catch (final Exception e) {
|
||||
ErrorActivity.reportUiErrorInSnackbar(this, "Setting up drawer", e);
|
||||
ErrorUtil.showUiErrorSnackbar(this, "Setting up drawer", e);
|
||||
}
|
||||
|
||||
if (DeviceUtils.isTv(this)) {
|
||||
@@ -165,7 +166,57 @@ public class MainActivity extends AppCompatActivity {
|
||||
openMiniPlayerUponPlayerStarted();
|
||||
}
|
||||
|
||||
private void setupDrawer() throws Exception {
|
||||
@Override
|
||||
protected void onPostCreate(final Bundle savedInstanceState) {
|
||||
super.onPostCreate(savedInstanceState);
|
||||
|
||||
final App app = App.getApp();
|
||||
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(app);
|
||||
|
||||
if (prefs.getBoolean(app.getString(R.string.update_app_key), true)) {
|
||||
// Start the service which is checking all conditions
|
||||
// and eventually searching for a new version.
|
||||
// The service searching for a new NewPipe version must not be started in background.
|
||||
startNewVersionCheckService();
|
||||
}
|
||||
}
|
||||
|
||||
private void setupDrawer() throws ExtractionException {
|
||||
addDrawerMenuForCurrentService();
|
||||
|
||||
toggle = new ActionBarDrawerToggle(this, mainBinding.getRoot(),
|
||||
toolbarLayoutBinding.toolbar, R.string.drawer_open, R.string.drawer_close);
|
||||
toggle.syncState();
|
||||
mainBinding.getRoot().addDrawerListener(toggle);
|
||||
mainBinding.getRoot().addDrawerListener(new DrawerLayout.SimpleDrawerListener() {
|
||||
private int lastService;
|
||||
|
||||
@Override
|
||||
public void onDrawerOpened(final View drawerView) {
|
||||
lastService = ServiceHelper.getSelectedServiceId(MainActivity.this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDrawerClosed(final View drawerView) {
|
||||
if (servicesShown) {
|
||||
toggleServices();
|
||||
}
|
||||
if (lastService != ServiceHelper.getSelectedServiceId(MainActivity.this)) {
|
||||
ActivityCompat.recreate(MainActivity.this);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
drawerLayoutBinding.navigation.setNavigationItemSelectedListener(this::drawerItemSelected);
|
||||
setupDrawerHeader();
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the drawer menu for the current service.
|
||||
*
|
||||
* @throws ExtractionException if the service didn't provide available kiosks
|
||||
*/
|
||||
private void addDrawerMenuForCurrentService() throws ExtractionException {
|
||||
//Tabs
|
||||
final int currentServiceId = ServiceHelper.getSelectedServiceId(this);
|
||||
final StreamingService service = NewPipe.getService(currentServiceId);
|
||||
@@ -204,32 +255,6 @@ public class MainActivity extends AppCompatActivity {
|
||||
drawerLayoutBinding.navigation.getMenu()
|
||||
.add(R.id.menu_options_about_group, ITEM_ID_ABOUT, ORDER, R.string.tab_about)
|
||||
.setIcon(R.drawable.ic_info_outline);
|
||||
|
||||
toggle = new ActionBarDrawerToggle(this, mainBinding.getRoot(),
|
||||
toolbarLayoutBinding.toolbar, R.string.drawer_open, R.string.drawer_close);
|
||||
toggle.syncState();
|
||||
mainBinding.getRoot().addDrawerListener(toggle);
|
||||
mainBinding.getRoot().addDrawerListener(new DrawerLayout.SimpleDrawerListener() {
|
||||
private int lastService;
|
||||
|
||||
@Override
|
||||
public void onDrawerOpened(final View drawerView) {
|
||||
lastService = ServiceHelper.getSelectedServiceId(MainActivity.this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDrawerClosed(final View drawerView) {
|
||||
if (servicesShown) {
|
||||
toggleServices();
|
||||
}
|
||||
if (lastService != ServiceHelper.getSelectedServiceId(MainActivity.this)) {
|
||||
ActivityCompat.recreate(MainActivity.this);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
drawerLayoutBinding.navigation.setNavigationItemSelectedListener(this::drawerItemSelected);
|
||||
setupDrawerHeader();
|
||||
}
|
||||
|
||||
private boolean drawerItemSelected(final MenuItem item) {
|
||||
@@ -241,7 +266,7 @@ public class MainActivity extends AppCompatActivity {
|
||||
try {
|
||||
tabSelected(item);
|
||||
} catch (final Exception e) {
|
||||
ErrorActivity.reportUiErrorInSnackbar(this, "Selecting main page tab", e);
|
||||
ErrorUtil.showUiErrorSnackbar(this, "Selecting main page tab", e);
|
||||
}
|
||||
break;
|
||||
case R.id.menu_options_about_group:
|
||||
@@ -337,20 +362,22 @@ public class MainActivity extends AppCompatActivity {
|
||||
drawerLayoutBinding.navigation.getMenu().removeGroup(R.id.menu_tabs_group);
|
||||
drawerLayoutBinding.navigation.getMenu().removeGroup(R.id.menu_options_about_group);
|
||||
|
||||
// Show up or down arrow
|
||||
drawerHeaderBinding.drawerArrow.setImageResource(
|
||||
servicesShown ? R.drawable.ic_arrow_drop_up : R.drawable.ic_arrow_drop_down);
|
||||
|
||||
if (servicesShown) {
|
||||
showServices();
|
||||
} else {
|
||||
try {
|
||||
showTabs();
|
||||
addDrawerMenuForCurrentService();
|
||||
} catch (final Exception e) {
|
||||
ErrorActivity.reportUiErrorInSnackbar(this, "Showing main page tabs", e);
|
||||
ErrorUtil.showUiErrorSnackbar(this, "Showing main page tabs", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void showServices() {
|
||||
drawerHeaderBinding.drawerArrow.setImageResource(R.drawable.ic_arrow_drop_up);
|
||||
|
||||
for (final StreamingService s : NewPipe.getServices()) {
|
||||
final String title = s.getServiceInfo().getName()
|
||||
+ (ServiceHelper.isBeta(s) ? " (beta)" : "");
|
||||
@@ -402,7 +429,7 @@ public class MainActivity extends AppCompatActivity {
|
||||
new Handler(Looper.getMainLooper()).postDelayed(() -> {
|
||||
getSupportFragmentManager().popBackStack(null,
|
||||
FragmentManager.POP_BACK_STACK_INCLUSIVE);
|
||||
recreate();
|
||||
ActivityCompat.recreate(MainActivity.this);
|
||||
}, 300);
|
||||
}
|
||||
|
||||
@@ -414,48 +441,6 @@ public class MainActivity extends AppCompatActivity {
|
||||
menuItem.setActionView(spinner);
|
||||
}
|
||||
|
||||
private void showTabs() throws ExtractionException {
|
||||
drawerHeaderBinding.drawerArrow.setImageResource(R.drawable.ic_arrow_drop_down);
|
||||
|
||||
//Tabs
|
||||
final int currentServiceId = ServiceHelper.getSelectedServiceId(this);
|
||||
final StreamingService service = NewPipe.getService(currentServiceId);
|
||||
|
||||
int kioskId = 0;
|
||||
|
||||
for (final String ks : service.getKioskList().getAvailableKiosks()) {
|
||||
drawerLayoutBinding.navigation.getMenu()
|
||||
.add(R.id.menu_tabs_group, kioskId, ORDER,
|
||||
KioskTranslator.getTranslatedKioskName(ks, this))
|
||||
.setIcon(KioskTranslator.getKioskIcon(ks, this));
|
||||
kioskId++;
|
||||
}
|
||||
|
||||
drawerLayoutBinding.navigation.getMenu()
|
||||
.add(R.id.menu_tabs_group, ITEM_ID_SUBSCRIPTIONS, ORDER, R.string.tab_subscriptions)
|
||||
.setIcon(R.drawable.ic_tv);
|
||||
drawerLayoutBinding.navigation.getMenu()
|
||||
.add(R.id.menu_tabs_group, ITEM_ID_FEED, ORDER, R.string.fragment_feed_title)
|
||||
.setIcon(R.drawable.ic_rss_feed);
|
||||
drawerLayoutBinding.navigation.getMenu()
|
||||
.add(R.id.menu_tabs_group, ITEM_ID_BOOKMARKS, ORDER, R.string.tab_bookmarks)
|
||||
.setIcon(R.drawable.ic_bookmark);
|
||||
drawerLayoutBinding.navigation.getMenu()
|
||||
.add(R.id.menu_tabs_group, ITEM_ID_DOWNLOADS, ORDER, R.string.downloads)
|
||||
.setIcon(R.drawable.ic_file_download);
|
||||
drawerLayoutBinding.navigation.getMenu()
|
||||
.add(R.id.menu_tabs_group, ITEM_ID_HISTORY, ORDER, R.string.action_history)
|
||||
.setIcon(R.drawable.ic_history);
|
||||
|
||||
//Settings and About
|
||||
drawerLayoutBinding.navigation.getMenu()
|
||||
.add(R.id.menu_options_about_group, ITEM_ID_SETTINGS, ORDER, R.string.settings)
|
||||
.setIcon(R.drawable.ic_settings);
|
||||
drawerLayoutBinding.navigation.getMenu()
|
||||
.add(R.id.menu_options_about_group, ITEM_ID_ABOUT, ORDER, R.string.tab_about)
|
||||
.setIcon(R.drawable.ic_info_outline);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
super.onDestroy();
|
||||
@@ -490,7 +475,7 @@ public class MainActivity extends AppCompatActivity {
|
||||
drawerHeaderBinding.drawerHeaderActionButton.setContentDescription(
|
||||
getString(R.string.drawer_header_description) + selectedServiceName);
|
||||
} catch (final Exception e) {
|
||||
ErrorActivity.reportUiErrorInSnackbar(this, "Setting up service toggle", e);
|
||||
ErrorUtil.showUiErrorSnackbar(this, "Setting up service toggle", e);
|
||||
}
|
||||
|
||||
final SharedPreferences sharedPreferences
|
||||
@@ -603,6 +588,7 @@ public class MainActivity extends AppCompatActivity {
|
||||
public void onRequestPermissionsResult(final int requestCode,
|
||||
@NonNull final String[] permissions,
|
||||
@NonNull final int[] grantResults) {
|
||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
|
||||
for (final int i : grantResults) {
|
||||
if (i == PackageManager.PERMISSION_DENIED) {
|
||||
return;
|
||||
@@ -799,7 +785,7 @@ public class MainActivity extends AppCompatActivity {
|
||||
NavigationHelper.gotoMainFragment(getSupportFragmentManager());
|
||||
}
|
||||
} catch (final Exception e) {
|
||||
ErrorActivity.reportUiErrorInSnackbar(this, "Handling intent", e);
|
||||
ErrorUtil.showUiErrorSnackbar(this, "Handling intent", e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -822,7 +808,7 @@ public class MainActivity extends AppCompatActivity {
|
||||
return;
|
||||
}
|
||||
|
||||
if (PlayerHolder.isPlayerOpen()) {
|
||||
if (PlayerHolder.getInstance().isPlayerOpen()) {
|
||||
// if the player is already open, no need for a broadcast receiver
|
||||
openMiniPlayerIfMissing();
|
||||
} else {
|
||||
|
||||
@@ -11,6 +11,7 @@ import org.schabi.newpipe.database.AppDatabase;
|
||||
import static org.schabi.newpipe.database.AppDatabase.DATABASE_NAME;
|
||||
import static org.schabi.newpipe.database.Migrations.MIGRATION_1_2;
|
||||
import static org.schabi.newpipe.database.Migrations.MIGRATION_2_3;
|
||||
import static org.schabi.newpipe.database.Migrations.MIGRATION_3_4;
|
||||
|
||||
public final class NewPipeDatabase {
|
||||
private static volatile AppDatabase databaseInstance;
|
||||
@@ -22,7 +23,7 @@ public final class NewPipeDatabase {
|
||||
private static AppDatabase getDatabase(final Context context) {
|
||||
return Room
|
||||
.databaseBuilder(context.getApplicationContext(), AppDatabase.class, DATABASE_NAME)
|
||||
.addMigrations(MIGRATION_1_2, MIGRATION_2_3)
|
||||
.addMigrations(MIGRATION_1_2, MIGRATION_2_3, MIGRATION_3_4)
|
||||
.build();
|
||||
}
|
||||
|
||||
@@ -51,4 +52,15 @@ public final class NewPipeDatabase {
|
||||
throw new RuntimeException("Checkpoint was blocked from completing");
|
||||
}
|
||||
}
|
||||
|
||||
public static void close() {
|
||||
if (databaseInstance != null) {
|
||||
synchronized (NewPipeDatabase.class) {
|
||||
if (databaseInstance != null) {
|
||||
databaseInstance.close();
|
||||
databaseInstance = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
83
app/src/main/java/org/schabi/newpipe/QueueItemMenuUtil.java
Normal file
83
app/src/main/java/org/schabi/newpipe/QueueItemMenuUtil.java
Normal file
@@ -0,0 +1,83 @@
|
||||
package org.schabi.newpipe;
|
||||
|
||||
import static org.schabi.newpipe.util.external_communication.ShareUtils.shareText;
|
||||
|
||||
import android.content.Context;
|
||||
import android.view.ContextThemeWrapper;
|
||||
import android.view.View;
|
||||
import android.widget.PopupMenu;
|
||||
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
|
||||
import org.schabi.newpipe.database.stream.model.StreamEntity;
|
||||
import org.schabi.newpipe.local.dialog.PlaylistDialog;
|
||||
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
||||
import org.schabi.newpipe.player.playqueue.PlayQueueItem;
|
||||
import org.schabi.newpipe.util.NavigationHelper;
|
||||
import org.schabi.newpipe.util.SaveUploaderUrlHelper;
|
||||
|
||||
import java.util.Collections;
|
||||
|
||||
public final class QueueItemMenuUtil {
|
||||
private QueueItemMenuUtil() {
|
||||
}
|
||||
|
||||
public static void openPopupMenu(final PlayQueue playQueue,
|
||||
final PlayQueueItem item,
|
||||
final View view,
|
||||
final boolean hideDetails,
|
||||
final FragmentManager fragmentManager,
|
||||
final Context context) {
|
||||
final ContextThemeWrapper themeWrapper =
|
||||
new ContextThemeWrapper(context, R.style.DarkPopupMenu);
|
||||
|
||||
final PopupMenu popupMenu = new PopupMenu(themeWrapper, view);
|
||||
popupMenu.inflate(R.menu.menu_play_queue_item);
|
||||
|
||||
if (hideDetails) {
|
||||
popupMenu.getMenu().findItem(R.id.menu_item_details).setVisible(false);
|
||||
}
|
||||
|
||||
popupMenu.setOnMenuItemClickListener(menuItem -> {
|
||||
switch (menuItem.getItemId()) {
|
||||
case R.id.menu_item_remove:
|
||||
final int index = playQueue.indexOf(item);
|
||||
playQueue.remove(index);
|
||||
return true;
|
||||
case R.id.menu_item_details:
|
||||
// playQueue is null since we don't want any queue change
|
||||
NavigationHelper.openVideoDetail(context, item.getServiceId(),
|
||||
item.getUrl(), item.getTitle(), null,
|
||||
false);
|
||||
return true;
|
||||
case R.id.menu_item_append_playlist:
|
||||
PlaylistDialog.createCorrespondingDialog(
|
||||
context,
|
||||
Collections.singletonList(new StreamEntity(item)),
|
||||
dialog -> dialog.show(
|
||||
fragmentManager,
|
||||
"QueueItemMenuUtil@append_playlist"
|
||||
)
|
||||
);
|
||||
|
||||
return true;
|
||||
case R.id.menu_item_channel_details:
|
||||
SaveUploaderUrlHelper.saveUploaderUrlIfNeeded(context, item,
|
||||
// An intent must be used here.
|
||||
// Opening with FragmentManager transactions is not working,
|
||||
// as PlayQueueActivity doesn't use fragments.
|
||||
uploaderUrl -> NavigationHelper.openChannelFragmentUsingIntent(
|
||||
context, item.getServiceId(), uploaderUrl, item.getUploader()
|
||||
));
|
||||
return true;
|
||||
case R.id.menu_item_share:
|
||||
shareText(context, item.getTitle(), item.getUrl(),
|
||||
item.getThumbnailUrl());
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
popupMenu.show();
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,8 @@
|
||||
package org.schabi.newpipe;
|
||||
|
||||
import static org.schabi.newpipe.extractor.StreamingService.ServiceInfo.MediaCapability.AUDIO;
|
||||
import static org.schabi.newpipe.extractor.StreamingService.ServiceInfo.MediaCapability.VIDEO;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.IntentService;
|
||||
import android.content.Context;
|
||||
@@ -30,11 +33,12 @@ import androidx.core.widget.TextViewCompat;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
import androidx.preference.PreferenceManager;
|
||||
|
||||
import org.schabi.newpipe.database.stream.model.StreamEntity;
|
||||
import org.schabi.newpipe.databinding.ListRadioIconItemBinding;
|
||||
import org.schabi.newpipe.databinding.SingleChoiceDialogViewBinding;
|
||||
import org.schabi.newpipe.download.DownloadDialog;
|
||||
import org.schabi.newpipe.error.ErrorActivity;
|
||||
import org.schabi.newpipe.error.ErrorInfo;
|
||||
import org.schabi.newpipe.error.ErrorUtil;
|
||||
import org.schabi.newpipe.error.ReCaptchaActivity;
|
||||
import org.schabi.newpipe.error.UserAction;
|
||||
import org.schabi.newpipe.extractor.Info;
|
||||
@@ -56,6 +60,7 @@ import org.schabi.newpipe.extractor.playlist.PlaylistInfo;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||
import org.schabi.newpipe.extractor.stream.VideoStream;
|
||||
import org.schabi.newpipe.ktx.ExceptionUtils;
|
||||
import org.schabi.newpipe.local.dialog.PlaylistDialog;
|
||||
import org.schabi.newpipe.player.MainPlayer;
|
||||
import org.schabi.newpipe.player.helper.PlayerHelper;
|
||||
import org.schabi.newpipe.player.helper.PlayerHolder;
|
||||
@@ -69,14 +74,15 @@ import org.schabi.newpipe.util.ExtractorHelper;
|
||||
import org.schabi.newpipe.util.ListHelper;
|
||||
import org.schabi.newpipe.util.NavigationHelper;
|
||||
import org.schabi.newpipe.util.PermissionHelper;
|
||||
import org.schabi.newpipe.util.ShareUtils;
|
||||
import org.schabi.newpipe.util.ThemeHelper;
|
||||
import org.schabi.newpipe.util.external_communication.ShareUtils;
|
||||
import org.schabi.newpipe.util.urlfinder.UrlFinder;
|
||||
import org.schabi.newpipe.views.FocusOverlayView;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import icepick.Icepick;
|
||||
@@ -89,9 +95,6 @@ import io.reactivex.rxjava3.disposables.Disposable;
|
||||
import io.reactivex.rxjava3.functions.Consumer;
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers;
|
||||
|
||||
import static org.schabi.newpipe.extractor.StreamingService.ServiceInfo.MediaCapability.AUDIO;
|
||||
import static org.schabi.newpipe.extractor.StreamingService.ServiceInfo.MediaCapability.VIDEO;
|
||||
|
||||
/**
|
||||
* Get the url from the intent and open it in the chosen preferred player.
|
||||
*/
|
||||
@@ -107,6 +110,8 @@ public class RouterActivity extends AppCompatActivity {
|
||||
protected String currentUrl;
|
||||
private StreamingService currentService;
|
||||
private boolean selectionIsDownload = false;
|
||||
private boolean selectionIsAddToPlaylist = false;
|
||||
private AlertDialog alertDialogChoice = null;
|
||||
|
||||
@Override
|
||||
protected void onCreate(final Bundle savedInstanceState) {
|
||||
@@ -126,6 +131,15 @@ public class RouterActivity extends AppCompatActivity {
|
||||
? R.style.RouterActivityThemeLight : R.style.RouterActivityThemeDark);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onStop() {
|
||||
super.onStop();
|
||||
// we need to dismiss the dialog before leaving the activity or we get leaks
|
||||
if (alertDialogChoice != null) {
|
||||
alertDialogChoice.dismiss();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onSaveInstanceState(@NonNull final Bundle outState) {
|
||||
super.onSaveInstanceState(outState);
|
||||
@@ -217,7 +231,7 @@ public class RouterActivity extends AppCompatActivity {
|
||||
} else if (errorInfo.getThrowable() instanceof ContentNotSupportedException) {
|
||||
Toast.makeText(context, R.string.content_not_supported, Toast.LENGTH_LONG).show();
|
||||
} else {
|
||||
ErrorActivity.reportError(context, errorInfo);
|
||||
ErrorUtil.createNotification(context, errorInfo);
|
||||
}
|
||||
|
||||
if (context instanceof RouterActivity) {
|
||||
@@ -333,26 +347,26 @@ public class RouterActivity extends AppCompatActivity {
|
||||
}
|
||||
};
|
||||
|
||||
final AlertDialog alertDialog = new AlertDialog.Builder(themeWrapperContext)
|
||||
alertDialogChoice = new AlertDialog.Builder(themeWrapperContext)
|
||||
.setTitle(R.string.preferred_open_action_share_menu_title)
|
||||
.setView(radioGroup)
|
||||
.setCancelable(true)
|
||||
.setNegativeButton(R.string.just_once, dialogButtonsClickListener)
|
||||
.setPositiveButton(R.string.always, dialogButtonsClickListener)
|
||||
.setOnDismissListener((dialog) -> {
|
||||
if (!selectionIsDownload) {
|
||||
if (!selectionIsDownload && !selectionIsAddToPlaylist) {
|
||||
finish();
|
||||
}
|
||||
})
|
||||
.create();
|
||||
|
||||
//noinspection CodeBlock2Expr
|
||||
alertDialog.setOnShowListener(dialog -> {
|
||||
setDialogButtonsState(alertDialog, radioGroup.getCheckedRadioButtonId() != -1);
|
||||
alertDialogChoice.setOnShowListener(dialog -> {
|
||||
setDialogButtonsState(alertDialogChoice, radioGroup.getCheckedRadioButtonId() != -1);
|
||||
});
|
||||
|
||||
radioGroup.setOnCheckedChangeListener((group, checkedId) ->
|
||||
setDialogButtonsState(alertDialog, true));
|
||||
setDialogButtonsState(alertDialogChoice, true));
|
||||
final View.OnClickListener radioButtonsClickListener = v -> {
|
||||
final int indexOfChild = radioGroup.indexOfChild(v);
|
||||
if (indexOfChild == -1) {
|
||||
@@ -402,10 +416,10 @@ public class RouterActivity extends AppCompatActivity {
|
||||
}
|
||||
selectedPreviously = selectedRadioPosition;
|
||||
|
||||
alertDialog.show();
|
||||
alertDialogChoice.show();
|
||||
|
||||
if (DeviceUtils.isTv(this)) {
|
||||
FocusOverlayView.setupFocusObserver(alertDialog);
|
||||
FocusOverlayView.setupFocusObserver(alertDialogChoice);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -436,6 +450,10 @@ public class RouterActivity extends AppCompatActivity {
|
||||
final AdapterChoiceItem backgroundPlayer = new AdapterChoiceItem(
|
||||
getString(R.string.background_player_key), getString(R.string.background_player),
|
||||
R.drawable.ic_headset);
|
||||
final AdapterChoiceItem addToPlaylist = new AdapterChoiceItem(
|
||||
getString(R.string.add_to_playlist_key), getString(R.string.add_to_playlist),
|
||||
R.drawable.ic_add);
|
||||
|
||||
|
||||
if (linkType == LinkType.STREAM) {
|
||||
if (isExtVideoEnabled) {
|
||||
@@ -443,7 +461,7 @@ public class RouterActivity extends AppCompatActivity {
|
||||
returnList.add(showInfo);
|
||||
returnList.add(videoPlayer);
|
||||
} else {
|
||||
final MainPlayer.PlayerType playerType = PlayerHolder.getType();
|
||||
final MainPlayer.PlayerType playerType = PlayerHolder.getInstance().getType();
|
||||
if (capabilities.contains(VIDEO)
|
||||
&& PlayerHelper.isAutoplayAllowedByUser(context)
|
||||
&& playerType == null || playerType == MainPlayer.PlayerType.VIDEO) {
|
||||
@@ -472,6 +490,10 @@ public class RouterActivity extends AppCompatActivity {
|
||||
getString(R.string.download),
|
||||
R.drawable.ic_file_download));
|
||||
|
||||
// Add to playlist is not necessary for CHANNEL and PLAYLIST linkType since those can
|
||||
// not be added to a playlist
|
||||
returnList.add(addToPlaylist);
|
||||
|
||||
} else {
|
||||
returnList.add(showInfo);
|
||||
if (capabilities.contains(VIDEO) && !isExtVideoEnabled) {
|
||||
@@ -537,6 +559,12 @@ public class RouterActivity extends AppCompatActivity {
|
||||
return;
|
||||
}
|
||||
|
||||
if (selectedChoiceKey.equals(getString(R.string.add_to_playlist_key))) {
|
||||
selectionIsAddToPlaylist = true;
|
||||
openAddToPlaylistDialog();
|
||||
return;
|
||||
}
|
||||
|
||||
// stop and bypass FetcherService if InfoScreen was selected since
|
||||
// StreamDetailFragment can fetch data itself
|
||||
if (selectedChoiceKey.equals(getString(R.string.show_info_key))) {
|
||||
@@ -562,6 +590,41 @@ public class RouterActivity extends AppCompatActivity {
|
||||
finish();
|
||||
}
|
||||
|
||||
private void openAddToPlaylistDialog() {
|
||||
// Getting the stream info usually takes a moment
|
||||
// Notifying the user here to ensure that no confusion arises
|
||||
Toast.makeText(
|
||||
getApplicationContext(),
|
||||
getString(R.string.processing_may_take_a_moment),
|
||||
Toast.LENGTH_SHORT)
|
||||
.show();
|
||||
|
||||
disposables.add(ExtractorHelper.getStreamInfo(currentServiceId, currentUrl, false)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
info -> PlaylistDialog.createCorrespondingDialog(
|
||||
getThemeWrapperContext(),
|
||||
Collections.singletonList(new StreamEntity(info)),
|
||||
playlistDialog -> {
|
||||
playlistDialog.setOnDismissListener(dialog -> finish());
|
||||
|
||||
playlistDialog.show(
|
||||
this.getSupportFragmentManager(),
|
||||
"addToPlaylistDialog"
|
||||
);
|
||||
}
|
||||
),
|
||||
throwable -> handleError(this, new ErrorInfo(
|
||||
throwable,
|
||||
UserAction.REQUESTED_STREAM,
|
||||
"Tried to add " + currentUrl + " to a playlist",
|
||||
currentService.getServiceId())
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@SuppressLint("CheckResult")
|
||||
private void openDownloadDialog() {
|
||||
disposables.add(ExtractorHelper.getStreamInfo(currentServiceId, currentUrl, true)
|
||||
@@ -579,9 +642,9 @@ public class RouterActivity extends AppCompatActivity {
|
||||
downloadDialog.setVideoStreams(sortedVideoStreams);
|
||||
downloadDialog.setAudioStreams(result.getAudioStreams());
|
||||
downloadDialog.setSelectedVideoStream(selectedVideoStreamIndex);
|
||||
downloadDialog.setOnDismissListener(dialog -> finish());
|
||||
downloadDialog.show(fm, "downloadDialog");
|
||||
fm.executePendingTransactions();
|
||||
downloadDialog.requireDialog().setOnDismissListener(dialog -> finish());
|
||||
}, throwable ->
|
||||
showUnsupportedUrlDialog(currentUrl)));
|
||||
}
|
||||
@@ -590,6 +653,7 @@ public class RouterActivity extends AppCompatActivity {
|
||||
public void onRequestPermissionsResult(final int requestCode,
|
||||
@NonNull final String[] permissions,
|
||||
@NonNull final int[] grantResults) {
|
||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
|
||||
for (final int i : grantResults) {
|
||||
if (i == PackageManager.PERMISSION_DENIED) {
|
||||
finish();
|
||||
|
||||
@@ -17,8 +17,8 @@ import org.schabi.newpipe.R
|
||||
import org.schabi.newpipe.databinding.ActivityAboutBinding
|
||||
import org.schabi.newpipe.databinding.FragmentAboutBinding
|
||||
import org.schabi.newpipe.util.Localization
|
||||
import org.schabi.newpipe.util.ShareUtils
|
||||
import org.schabi.newpipe.util.ThemeHelper
|
||||
import org.schabi.newpipe.util.external_communication.ShareUtils
|
||||
|
||||
class AboutActivity : AppCompatActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
@@ -162,10 +162,18 @@ class AboutActivity : AppCompatActivity() {
|
||||
"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
|
||||
@@ -179,10 +187,9 @@ class AboutActivity : AppCompatActivity() {
|
||||
"https://github.com/ReactiveX/RxJava", StandardLicenses.APACHE2
|
||||
),
|
||||
SoftwareComponent(
|
||||
"Universal Image Loader", "2011 - 2015", "Sergey Tarasevich",
|
||||
"https://github.com/nostra13/Android-Universal-Image-Loader",
|
||||
StandardLicenses.APACHE2
|
||||
)
|
||||
"SearchPreference", "2018", "ByteHamster",
|
||||
"https://github.com/ByteHamster/SearchPreference", StandardLicenses.MIT
|
||||
),
|
||||
)
|
||||
private const val POS_ABOUT = 0
|
||||
private const val POS_LICENSE = 1
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package org.schabi.newpipe.about
|
||||
|
||||
import android.os.Parcelable
|
||||
import kotlinx.android.parcel.Parcelize
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import java.io.Serializable
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
package org.schabi.newpipe.about
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.ContextMenu
|
||||
import android.view.ContextMenu.ContextMenuInfo
|
||||
import android.view.LayoutInflater
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.os.bundleOf
|
||||
@@ -14,31 +11,21 @@ import org.schabi.newpipe.R
|
||||
import org.schabi.newpipe.about.LicenseFragmentHelper.showLicense
|
||||
import org.schabi.newpipe.databinding.FragmentLicensesBinding
|
||||
import org.schabi.newpipe.databinding.ItemSoftwareComponentBinding
|
||||
import org.schabi.newpipe.util.ShareUtils
|
||||
import java.util.Arrays
|
||||
import java.util.Objects
|
||||
|
||||
/**
|
||||
* Fragment containing the software licenses.
|
||||
*/
|
||||
class LicenseFragment : Fragment() {
|
||||
private lateinit var softwareComponents: Array<SoftwareComponent>
|
||||
private var componentForContextMenu: SoftwareComponent? = null
|
||||
private var activeLicense: License? = null
|
||||
private val compositeDisposable = CompositeDisposable()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
softwareComponents =
|
||||
arguments?.getParcelableArray(ARG_COMPONENTS) as Array<SoftwareComponent>
|
||||
if (savedInstanceState != null) {
|
||||
val license = savedInstanceState.getSerializable(LICENSE_KEY)
|
||||
if (license != null) {
|
||||
activeLicense = license as License?
|
||||
}
|
||||
}
|
||||
softwareComponents = arguments?.getParcelableArray(ARG_COMPONENTS) as Array<SoftwareComponent>
|
||||
activeLicense = savedInstanceState?.getSerializable(LICENSE_KEY) as? License
|
||||
// Sort components by name
|
||||
Arrays.sort(softwareComponents, Comparator.comparing(SoftwareComponent::name))
|
||||
softwareComponents.sortBy { it.name }
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
@@ -73,49 +60,19 @@ class LicenseFragment : Fragment() {
|
||||
root.setOnClickListener {
|
||||
activeLicense = component.license
|
||||
compositeDisposable.add(
|
||||
showLicense(activity, component.license)
|
||||
showLicense(activity, component)
|
||||
)
|
||||
}
|
||||
binding.licensesSoftwareComponents.addView(root)
|
||||
registerForContextMenu(root)
|
||||
}
|
||||
if (activeLicense != null) {
|
||||
compositeDisposable.add(
|
||||
showLicense(activity, activeLicense!!)
|
||||
)
|
||||
}
|
||||
activeLicense?.let { compositeDisposable.add(showLicense(activity, it)) }
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onCreateContextMenu(menu: ContextMenu, v: View, menuInfo: ContextMenuInfo?) {
|
||||
val inflater = requireActivity().menuInflater
|
||||
val component = v.tag as SoftwareComponent
|
||||
menu.setHeaderTitle(component.name)
|
||||
inflater.inflate(R.menu.software_component, menu)
|
||||
super.onCreateContextMenu(menu, v, menuInfo)
|
||||
componentForContextMenu = component
|
||||
}
|
||||
|
||||
override fun onContextItemSelected(item: MenuItem): Boolean {
|
||||
// item.getMenuInfo() is null so we use the tag of the view
|
||||
val component = componentForContextMenu ?: return false
|
||||
when (item.itemId) {
|
||||
R.id.menu_software_website -> {
|
||||
ShareUtils.openUrlInBrowser(activity, component.link)
|
||||
return true
|
||||
}
|
||||
R.id.menu_software_show_license -> compositeDisposable.add(
|
||||
showLicense(activity, component.license)
|
||||
)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(savedInstanceState: Bundle) {
|
||||
super.onSaveInstanceState(savedInstanceState)
|
||||
if (activeLicense != null) {
|
||||
savedInstanceState.putSerializable(LICENSE_KEY, activeLicense)
|
||||
}
|
||||
activeLicense?.let { savedInstanceState.putSerializable(LICENSE_KEY, it) }
|
||||
}
|
||||
|
||||
companion object {
|
||||
@@ -123,8 +80,7 @@ class LicenseFragment : Fragment() {
|
||||
private const val LICENSE_KEY = "ACTIVE_LICENSE"
|
||||
fun newInstance(softwareComponents: Array<SoftwareComponent>): LicenseFragment {
|
||||
val fragment = LicenseFragment()
|
||||
fragment.arguments =
|
||||
bundleOf(ARG_COMPONENTS to Objects.requireNonNull(softwareComponents))
|
||||
fragment.arguments = bundleOf(ARG_COMPONENTS to softwareComponents)
|
||||
return fragment
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import org.schabi.newpipe.R
|
||||
import org.schabi.newpipe.util.Localization
|
||||
import org.schabi.newpipe.util.ThemeHelper
|
||||
import org.schabi.newpipe.util.external_communication.ShareUtils
|
||||
import java.io.BufferedReader
|
||||
import java.io.IOException
|
||||
import java.io.InputStreamReader
|
||||
@@ -107,10 +108,40 @@ object LicenseFragmentHelper {
|
||||
alert.setView(webView)
|
||||
Localization.assureCorrectAppLanguage(context)
|
||||
alert.setNegativeButton(
|
||||
context.getString(R.string.finish)
|
||||
context.getString(R.string.ok)
|
||||
) { dialog, _ -> dialog.dismiss() }
|
||||
alert.show()
|
||||
}
|
||||
}
|
||||
}
|
||||
@JvmStatic
|
||||
fun showLicense(context: Context?, component: SoftwareComponent): Disposable {
|
||||
return if (context == null) {
|
||||
Disposable.empty()
|
||||
} else {
|
||||
Observable.fromCallable { getFormattedLicense(context, component.license) }
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe { formattedLicense: String ->
|
||||
val webViewData = Base64.encodeToString(
|
||||
formattedLicense
|
||||
.toByteArray(StandardCharsets.UTF_8),
|
||||
Base64.NO_PADDING
|
||||
)
|
||||
val webView = WebView(context)
|
||||
webView.loadData(webViewData, "text/html; charset=UTF-8", "base64")
|
||||
val alert = AlertDialog.Builder(context)
|
||||
alert.setTitle(component.license.name)
|
||||
alert.setView(webView)
|
||||
Localization.assureCorrectAppLanguage(context)
|
||||
alert.setPositiveButton(
|
||||
R.string.dismiss
|
||||
) { dialog, _ -> dialog.dismiss() }
|
||||
alert.setNeutralButton(R.string.open_website_license) { _, _ ->
|
||||
ShareUtils.openUrlInBrowser(context, component.link)
|
||||
}
|
||||
alert.show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package org.schabi.newpipe.about
|
||||
|
||||
import android.os.Parcelable
|
||||
import kotlinx.android.parcel.Parcelize
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Parcelize
|
||||
class SoftwareComponent
|
||||
|
||||
@@ -27,7 +27,7 @@ import org.schabi.newpipe.database.stream.model.StreamStateEntity;
|
||||
import org.schabi.newpipe.database.subscription.SubscriptionDAO;
|
||||
import org.schabi.newpipe.database.subscription.SubscriptionEntity;
|
||||
|
||||
import static org.schabi.newpipe.database.Migrations.DB_VER_3;
|
||||
import static org.schabi.newpipe.database.Migrations.DB_VER_4;
|
||||
|
||||
@TypeConverters({Converters.class})
|
||||
@Database(
|
||||
@@ -38,7 +38,7 @@ import static org.schabi.newpipe.database.Migrations.DB_VER_3;
|
||||
FeedEntity.class, FeedGroupEntity.class, FeedGroupSubscriptionEntity.class,
|
||||
FeedLastUpdatedEntity.class
|
||||
},
|
||||
version = DB_VER_3
|
||||
version = DB_VER_4
|
||||
)
|
||||
public abstract class AppDatabase extends RoomDatabase {
|
||||
public static final String DATABASE_NAME = "newpipe.db";
|
||||
|
||||
@@ -1,64 +0,0 @@
|
||||
package org.schabi.newpipe.database;
|
||||
|
||||
import androidx.room.TypeConverter;
|
||||
|
||||
import org.schabi.newpipe.extractor.stream.StreamType;
|
||||
import org.schabi.newpipe.local.subscription.FeedGroupIcon;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.time.ZoneOffset;
|
||||
|
||||
public final class Converters {
|
||||
private Converters() { }
|
||||
|
||||
/**
|
||||
* Convert a long value to a {@link OffsetDateTime}.
|
||||
*
|
||||
* @param value the long value
|
||||
* @return the {@code OffsetDateTime}
|
||||
*/
|
||||
@TypeConverter
|
||||
public static OffsetDateTime offsetDateTimeFromTimestamp(final Long value) {
|
||||
return value == null ? null : OffsetDateTime.ofInstant(Instant.ofEpochMilli(value),
|
||||
ZoneOffset.UTC);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a {@link OffsetDateTime} to a long value.
|
||||
*
|
||||
* @param offsetDateTime the {@code OffsetDateTime}
|
||||
* @return the long value
|
||||
*/
|
||||
@TypeConverter
|
||||
public static Long offsetDateTimeToTimestamp(final OffsetDateTime offsetDateTime) {
|
||||
return offsetDateTime == null ? null : offsetDateTime.withOffsetSameInstant(ZoneOffset.UTC)
|
||||
.toInstant().toEpochMilli();
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
public static StreamType streamTypeOf(final String value) {
|
||||
return StreamType.valueOf(value);
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
public static String stringOf(final StreamType streamType) {
|
||||
return streamType.name();
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
public static Integer integerOf(final FeedGroupIcon feedGroupIcon) {
|
||||
return feedGroupIcon.getId();
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
public static FeedGroupIcon feedGroupIconOf(final Integer id) {
|
||||
for (final FeedGroupIcon icon : FeedGroupIcon.values()) {
|
||||
if (icon.getId() == id) {
|
||||
return icon;
|
||||
}
|
||||
}
|
||||
|
||||
throw new IllegalArgumentException("There's no feed group icon with the id \"" + id + "\"");
|
||||
}
|
||||
}
|
||||
52
app/src/main/java/org/schabi/newpipe/database/Converters.kt
Normal file
52
app/src/main/java/org/schabi/newpipe/database/Converters.kt
Normal file
@@ -0,0 +1,52 @@
|
||||
package org.schabi.newpipe.database
|
||||
|
||||
import androidx.room.TypeConverter
|
||||
import org.schabi.newpipe.extractor.stream.StreamType
|
||||
import org.schabi.newpipe.local.subscription.FeedGroupIcon
|
||||
import java.time.Instant
|
||||
import java.time.OffsetDateTime
|
||||
import java.time.ZoneOffset
|
||||
|
||||
object Converters {
|
||||
/**
|
||||
* Convert a long value to a [OffsetDateTime].
|
||||
*
|
||||
* @param value the long value
|
||||
* @return the `OffsetDateTime`
|
||||
*/
|
||||
@TypeConverter
|
||||
fun offsetDateTimeFromTimestamp(value: Long?): OffsetDateTime? {
|
||||
return value?.let { OffsetDateTime.ofInstant(Instant.ofEpochMilli(it), ZoneOffset.UTC) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a [OffsetDateTime] to a long value.
|
||||
*
|
||||
* @param offsetDateTime the `OffsetDateTime`
|
||||
* @return the long value
|
||||
*/
|
||||
@TypeConverter
|
||||
fun offsetDateTimeToTimestamp(offsetDateTime: OffsetDateTime?): Long? {
|
||||
return offsetDateTime?.withOffsetSameInstant(ZoneOffset.UTC)?.toInstant()?.toEpochMilli()
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
fun streamTypeOf(value: String): StreamType {
|
||||
return StreamType.valueOf(value)
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
fun stringOf(streamType: StreamType): String {
|
||||
return streamType.name
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
fun integerOf(feedGroupIcon: FeedGroupIcon): Int {
|
||||
return feedGroupIcon.id
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
fun feedGroupIconOf(id: Int): FeedGroupIcon {
|
||||
return FeedGroupIcon.values().first { it.id == id }
|
||||
}
|
||||
}
|
||||
@@ -9,9 +9,19 @@ import androidx.sqlite.db.SupportSQLiteDatabase;
|
||||
import org.schabi.newpipe.MainActivity;
|
||||
|
||||
public final class Migrations {
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
// Test new migrations manually by importing a database from daily usage //
|
||||
// and checking if the migration works (Use the Database Inspector //
|
||||
// https://developer.android.com/studio/inspect/database). //
|
||||
// If you add a migration point it out in the pull request, so that //
|
||||
// others remember to test it themselves. //
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
public static final int DB_VER_1 = 1;
|
||||
public static final int DB_VER_2 = 2;
|
||||
public static final int DB_VER_3 = 3;
|
||||
public static final int DB_VER_4 = 4;
|
||||
|
||||
private static final String TAG = Migrations.class.getName();
|
||||
public static final boolean DEBUG = MainActivity.DEBUG;
|
||||
@@ -160,5 +170,14 @@ public final class Migrations {
|
||||
}
|
||||
};
|
||||
|
||||
public static final Migration MIGRATION_3_4 = new Migration(DB_VER_3, DB_VER_4) {
|
||||
@Override
|
||||
public void migrate(@NonNull final SupportSQLiteDatabase database) {
|
||||
database.execSQL(
|
||||
"ALTER TABLE streams ADD COLUMN uploader_url TEXT"
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
private Migrations() { }
|
||||
}
|
||||
|
||||
@@ -7,9 +7,11 @@ import androidx.room.Query
|
||||
import androidx.room.Transaction
|
||||
import androidx.room.Update
|
||||
import io.reactivex.rxjava3.core.Flowable
|
||||
import io.reactivex.rxjava3.core.Maybe
|
||||
import org.schabi.newpipe.database.feed.model.FeedEntity
|
||||
import org.schabi.newpipe.database.feed.model.FeedLastUpdatedEntity
|
||||
import org.schabi.newpipe.database.stream.model.StreamEntity
|
||||
import org.schabi.newpipe.database.stream.StreamWithState
|
||||
import org.schabi.newpipe.database.stream.model.StreamStateEntity
|
||||
import org.schabi.newpipe.database.subscription.SubscriptionEntity
|
||||
import java.time.OffsetDateTime
|
||||
|
||||
@@ -20,21 +22,34 @@ abstract class FeedDAO {
|
||||
|
||||
@Query(
|
||||
"""
|
||||
SELECT s.* FROM streams s
|
||||
SELECT s.*, sst.progress_time
|
||||
FROM streams s
|
||||
|
||||
LEFT JOIN stream_state sst
|
||||
ON s.uid = sst.stream_id
|
||||
|
||||
LEFT JOIN stream_history sh
|
||||
ON s.uid = sh.stream_id
|
||||
|
||||
INNER JOIN feed f
|
||||
ON s.uid = f.stream_id
|
||||
|
||||
ORDER BY s.upload_date IS NULL DESC, s.upload_date DESC, s.uploader ASC
|
||||
|
||||
LIMIT 500
|
||||
"""
|
||||
)
|
||||
abstract fun getAllStreams(): Flowable<List<StreamEntity>>
|
||||
abstract fun getAllStreams(): Maybe<List<StreamWithState>>
|
||||
|
||||
@Query(
|
||||
"""
|
||||
SELECT s.* FROM streams s
|
||||
SELECT s.*, sst.progress_time
|
||||
FROM streams s
|
||||
|
||||
LEFT JOIN stream_state sst
|
||||
ON s.uid = sst.stream_id
|
||||
|
||||
LEFT JOIN stream_history sh
|
||||
ON s.uid = sh.stream_id
|
||||
|
||||
INNER JOIN feed f
|
||||
ON s.uid = f.stream_id
|
||||
@@ -42,16 +57,88 @@ abstract class FeedDAO {
|
||||
INNER JOIN feed_group_subscription_join fgs
|
||||
ON fgs.subscription_id = f.subscription_id
|
||||
|
||||
INNER JOIN feed_group fg
|
||||
ON fg.uid = fgs.group_id
|
||||
|
||||
WHERE fgs.group_id = :groupId
|
||||
|
||||
ORDER BY s.upload_date IS NULL DESC, s.upload_date DESC, s.uploader ASC
|
||||
LIMIT 500
|
||||
"""
|
||||
)
|
||||
abstract fun getAllStreamsFromGroup(groupId: Long): Flowable<List<StreamEntity>>
|
||||
abstract fun getAllStreamsForGroup(groupId: Long): Maybe<List<StreamWithState>>
|
||||
|
||||
/**
|
||||
* @see StreamStateEntity.isFinished()
|
||||
* @see StreamStateEntity.PLAYBACK_FINISHED_END_MILLISECONDS
|
||||
* @return all of the non-live, never-played and non-finished streams in the feed
|
||||
* (all of the cited conditions must hold for a stream to be in the returned list)
|
||||
*/
|
||||
@Query(
|
||||
"""
|
||||
SELECT s.*, sst.progress_time
|
||||
FROM streams s
|
||||
|
||||
LEFT JOIN stream_state sst
|
||||
ON s.uid = sst.stream_id
|
||||
|
||||
LEFT JOIN stream_history sh
|
||||
ON s.uid = sh.stream_id
|
||||
|
||||
INNER JOIN feed f
|
||||
ON s.uid = f.stream_id
|
||||
|
||||
WHERE (
|
||||
sh.stream_id IS NULL
|
||||
OR sst.stream_id IS NULL
|
||||
OR sst.progress_time < s.duration * 1000 - ${StreamStateEntity.PLAYBACK_FINISHED_END_MILLISECONDS}
|
||||
OR sst.progress_time < s.duration * 1000 * 3 / 4
|
||||
OR s.stream_type = 'LIVE_STREAM'
|
||||
OR s.stream_type = 'AUDIO_LIVE_STREAM'
|
||||
)
|
||||
|
||||
ORDER BY s.upload_date IS NULL DESC, s.upload_date DESC, s.uploader ASC
|
||||
LIMIT 500
|
||||
"""
|
||||
)
|
||||
abstract fun getLiveOrNotPlayedStreams(): Maybe<List<StreamWithState>>
|
||||
|
||||
/**
|
||||
* @see StreamStateEntity.isFinished()
|
||||
* @see StreamStateEntity.PLAYBACK_FINISHED_END_MILLISECONDS
|
||||
* @param groupId the group id to get streams of
|
||||
* @return all of the non-live, never-played and non-finished streams for the given feed group
|
||||
* (all of the cited conditions must hold for a stream to be in the returned list)
|
||||
*/
|
||||
@Query(
|
||||
"""
|
||||
SELECT s.*, sst.progress_time
|
||||
FROM streams s
|
||||
|
||||
LEFT JOIN stream_state sst
|
||||
ON s.uid = sst.stream_id
|
||||
|
||||
LEFT JOIN stream_history sh
|
||||
ON s.uid = sh.stream_id
|
||||
|
||||
INNER JOIN feed f
|
||||
ON s.uid = f.stream_id
|
||||
|
||||
INNER JOIN feed_group_subscription_join fgs
|
||||
ON fgs.subscription_id = f.subscription_id
|
||||
|
||||
WHERE fgs.group_id = :groupId
|
||||
AND (
|
||||
sh.stream_id IS NULL
|
||||
OR sst.stream_id IS NULL
|
||||
OR sst.progress_time < s.duration * 1000 - ${StreamStateEntity.PLAYBACK_FINISHED_END_MILLISECONDS}
|
||||
OR sst.progress_time < s.duration * 1000 * 3 / 4
|
||||
OR s.stream_type = 'LIVE_STREAM'
|
||||
OR s.stream_type = 'AUDIO_LIVE_STREAM'
|
||||
)
|
||||
|
||||
ORDER BY s.upload_date IS NULL DESC, s.upload_date DESC, s.uploader ASC
|
||||
LIMIT 500
|
||||
"""
|
||||
)
|
||||
abstract fun getLiveOrNotPlayedStreamsForGroup(groupId: Long): Maybe<List<StreamWithState>>
|
||||
|
||||
@Query(
|
||||
"""
|
||||
|
||||
@@ -19,6 +19,7 @@ import static org.schabi.newpipe.database.history.model.SearchHistoryEntry.TABLE
|
||||
@Dao
|
||||
public interface SearchHistoryDAO extends HistoryDAO<SearchHistoryEntry> {
|
||||
String ORDER_BY_CREATION_DATE = " ORDER BY " + CREATION_DATE + " DESC";
|
||||
String ORDER_BY_MAX_CREATION_DATE = " ORDER BY MAX(" + CREATION_DATE + ") DESC";
|
||||
|
||||
@Query("SELECT * FROM " + TABLE_NAME
|
||||
+ " WHERE " + ID + " = (SELECT MAX(" + ID + ") FROM " + TABLE_NAME + ")")
|
||||
@@ -36,16 +37,16 @@ public interface SearchHistoryDAO extends HistoryDAO<SearchHistoryEntry> {
|
||||
@Override
|
||||
Flowable<List<SearchHistoryEntry>> getAll();
|
||||
|
||||
@Query("SELECT * FROM " + TABLE_NAME + " GROUP BY " + SEARCH + ORDER_BY_CREATION_DATE
|
||||
+ " LIMIT :limit")
|
||||
Flowable<List<SearchHistoryEntry>> getUniqueEntries(int limit);
|
||||
@Query("SELECT " + SEARCH + " FROM " + TABLE_NAME + " GROUP BY " + SEARCH
|
||||
+ ORDER_BY_MAX_CREATION_DATE + " LIMIT :limit")
|
||||
Flowable<List<String>> getUniqueEntries(int limit);
|
||||
|
||||
@Query("SELECT * FROM " + TABLE_NAME
|
||||
+ " WHERE " + SERVICE_ID + " = :serviceId" + ORDER_BY_CREATION_DATE)
|
||||
@Override
|
||||
Flowable<List<SearchHistoryEntry>> listByService(int serviceId);
|
||||
|
||||
@Query("SELECT * FROM " + TABLE_NAME + " WHERE " + SEARCH + " LIKE :query || '%'"
|
||||
+ " GROUP BY " + SEARCH + " LIMIT :limit")
|
||||
Flowable<List<SearchHistoryEntry>> getSimilarEntries(String query, int limit);
|
||||
@Query("SELECT " + SEARCH + " FROM " + TABLE_NAME + " WHERE " + SEARCH + " LIKE :query || '%'"
|
||||
+ " GROUP BY " + SEARCH + ORDER_BY_MAX_CREATION_DATE + " LIMIT :limit")
|
||||
Flowable<List<String>> getSimilarEntries(String query, int limit);
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ import static org.schabi.newpipe.database.stream.StreamStatisticsEntry.STREAM_WA
|
||||
import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_ID;
|
||||
import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_TABLE;
|
||||
import static org.schabi.newpipe.database.stream.model.StreamStateEntity.JOIN_STREAM_ID_ALIAS;
|
||||
import static org.schabi.newpipe.database.stream.model.StreamStateEntity.STREAM_PROGRESS_TIME;
|
||||
import static org.schabi.newpipe.database.stream.model.StreamStateEntity.STREAM_PROGRESS_MILLIS;
|
||||
import static org.schabi.newpipe.database.stream.model.StreamStateEntity.STREAM_STATE_TABLE;
|
||||
|
||||
@Dao
|
||||
@@ -80,7 +80,7 @@ public abstract class StreamHistoryDAO implements HistoryDAO<StreamHistoryEntity
|
||||
|
||||
+ " LEFT JOIN "
|
||||
+ "(SELECT " + JOIN_STREAM_ID + " AS " + JOIN_STREAM_ID_ALIAS + ", "
|
||||
+ STREAM_PROGRESS_TIME
|
||||
+ STREAM_PROGRESS_MILLIS
|
||||
+ " FROM " + STREAM_STATE_TABLE + " )"
|
||||
+ " ON " + STREAM_ID + " = " + JOIN_STREAM_ID_ALIAS)
|
||||
public abstract Flowable<List<StreamStatisticsEntry>> getStatistics();
|
||||
|
||||
@@ -1,79 +0,0 @@
|
||||
package org.schabi.newpipe.database.history.model;
|
||||
|
||||
import androidx.room.ColumnInfo;
|
||||
import androidx.room.Entity;
|
||||
import androidx.room.Ignore;
|
||||
import androidx.room.Index;
|
||||
import androidx.room.PrimaryKey;
|
||||
|
||||
import java.time.OffsetDateTime;
|
||||
|
||||
import static org.schabi.newpipe.database.history.model.SearchHistoryEntry.SEARCH;
|
||||
|
||||
@Entity(tableName = SearchHistoryEntry.TABLE_NAME,
|
||||
indices = {@Index(value = SEARCH)})
|
||||
public class SearchHistoryEntry {
|
||||
public static final String ID = "id";
|
||||
public static final String TABLE_NAME = "search_history";
|
||||
public static final String SERVICE_ID = "service_id";
|
||||
public static final String CREATION_DATE = "creation_date";
|
||||
public static final String SEARCH = "search";
|
||||
|
||||
@ColumnInfo(name = ID)
|
||||
@PrimaryKey(autoGenerate = true)
|
||||
private long id;
|
||||
|
||||
@ColumnInfo(name = CREATION_DATE)
|
||||
private OffsetDateTime creationDate;
|
||||
|
||||
@ColumnInfo(name = SERVICE_ID)
|
||||
private int serviceId;
|
||||
|
||||
@ColumnInfo(name = SEARCH)
|
||||
private String search;
|
||||
|
||||
public SearchHistoryEntry(final OffsetDateTime creationDate, final int serviceId,
|
||||
final String search) {
|
||||
this.serviceId = serviceId;
|
||||
this.creationDate = creationDate;
|
||||
this.search = search;
|
||||
}
|
||||
|
||||
public long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(final long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public OffsetDateTime getCreationDate() {
|
||||
return creationDate;
|
||||
}
|
||||
|
||||
public void setCreationDate(final OffsetDateTime creationDate) {
|
||||
this.creationDate = creationDate;
|
||||
}
|
||||
|
||||
public int getServiceId() {
|
||||
return serviceId;
|
||||
}
|
||||
|
||||
public void setServiceId(final int serviceId) {
|
||||
this.serviceId = serviceId;
|
||||
}
|
||||
|
||||
public String getSearch() {
|
||||
return search;
|
||||
}
|
||||
|
||||
public void setSearch(final String search) {
|
||||
this.search = search;
|
||||
}
|
||||
|
||||
@Ignore
|
||||
public boolean hasEqualValues(final SearchHistoryEntry otherEntry) {
|
||||
return getServiceId() == otherEntry.getServiceId()
|
||||
&& getSearch().equals(otherEntry.getSearch());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package org.schabi.newpipe.database.history.model
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.Ignore
|
||||
import androidx.room.Index
|
||||
import androidx.room.PrimaryKey
|
||||
import java.time.OffsetDateTime
|
||||
|
||||
@Entity(
|
||||
tableName = SearchHistoryEntry.TABLE_NAME,
|
||||
indices = [Index(value = [SearchHistoryEntry.SEARCH])]
|
||||
)
|
||||
data class SearchHistoryEntry(
|
||||
@field:ColumnInfo(name = CREATION_DATE) var creationDate: OffsetDateTime?,
|
||||
@field:ColumnInfo(
|
||||
name = SERVICE_ID
|
||||
) var serviceId: Int,
|
||||
@field:ColumnInfo(name = SEARCH) var search: String?
|
||||
) {
|
||||
@ColumnInfo(name = ID)
|
||||
@PrimaryKey(autoGenerate = true)
|
||||
var id: Long = 0
|
||||
|
||||
@Ignore
|
||||
fun hasEqualValues(otherEntry: SearchHistoryEntry): Boolean {
|
||||
return (
|
||||
serviceId == otherEntry.serviceId &&
|
||||
search == otherEntry.search
|
||||
)
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val ID = "id"
|
||||
const val TABLE_NAME = "search_history"
|
||||
const val SERVICE_ID = "service_id"
|
||||
const val CREATION_DATE = "creation_date"
|
||||
const val SEARCH = "search"
|
||||
}
|
||||
}
|
||||
@@ -12,8 +12,8 @@ data class PlaylistStreamEntry(
|
||||
@Embedded
|
||||
val streamEntity: StreamEntity,
|
||||
|
||||
@ColumnInfo(name = StreamStateEntity.STREAM_PROGRESS_TIME, defaultValue = "0")
|
||||
val progressTime: Long,
|
||||
@ColumnInfo(name = StreamStateEntity.STREAM_PROGRESS_MILLIS, defaultValue = "0")
|
||||
val progressMillis: Long,
|
||||
|
||||
@ColumnInfo(name = PlaylistStreamEntity.JOIN_STREAM_ID)
|
||||
val streamId: Long,
|
||||
@@ -27,6 +27,7 @@ data class PlaylistStreamEntry(
|
||||
val item = StreamInfoItem(streamEntity.serviceId, streamEntity.url, streamEntity.title, streamEntity.streamType)
|
||||
item.duration = streamEntity.duration
|
||||
item.uploaderName = streamEntity.uploader
|
||||
item.uploaderUrl = streamEntity.uploaderUrl
|
||||
item.thumbnailUrl = streamEntity.thumbnailUrl
|
||||
|
||||
return item
|
||||
|
||||
@@ -14,26 +14,26 @@ import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST
|
||||
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_TABLE;
|
||||
|
||||
@Dao
|
||||
public abstract class PlaylistDAO implements BasicDAO<PlaylistEntity> {
|
||||
public interface PlaylistDAO extends BasicDAO<PlaylistEntity> {
|
||||
@Override
|
||||
@Query("SELECT * FROM " + PLAYLIST_TABLE)
|
||||
public abstract Flowable<List<PlaylistEntity>> getAll();
|
||||
Flowable<List<PlaylistEntity>> getAll();
|
||||
|
||||
@Override
|
||||
@Query("DELETE FROM " + PLAYLIST_TABLE)
|
||||
public abstract int deleteAll();
|
||||
int deleteAll();
|
||||
|
||||
@Override
|
||||
public Flowable<List<PlaylistEntity>> listByService(final int serviceId) {
|
||||
default Flowable<List<PlaylistEntity>> listByService(final int serviceId) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Query("SELECT * FROM " + PLAYLIST_TABLE + " WHERE " + PLAYLIST_ID + " = :playlistId")
|
||||
public abstract Flowable<List<PlaylistEntity>> getPlaylist(long playlistId);
|
||||
Flowable<List<PlaylistEntity>> getPlaylist(long playlistId);
|
||||
|
||||
@Query("DELETE FROM " + PLAYLIST_TABLE + " WHERE " + PLAYLIST_ID + " = :playlistId")
|
||||
public abstract int deletePlaylist(long playlistId);
|
||||
int deletePlaylist(long playlistId);
|
||||
|
||||
@Query("SELECT COUNT(*) FROM " + PLAYLIST_TABLE)
|
||||
public abstract Flowable<Long> getCount();
|
||||
Flowable<Long> getCount();
|
||||
}
|
||||
|
||||
@@ -17,31 +17,31 @@ import static org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity.RE
|
||||
import static org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity.REMOTE_PLAYLIST_URL;
|
||||
|
||||
@Dao
|
||||
public abstract class PlaylistRemoteDAO implements BasicDAO<PlaylistRemoteEntity> {
|
||||
public interface PlaylistRemoteDAO extends BasicDAO<PlaylistRemoteEntity> {
|
||||
@Override
|
||||
@Query("SELECT * FROM " + REMOTE_PLAYLIST_TABLE)
|
||||
public abstract Flowable<List<PlaylistRemoteEntity>> getAll();
|
||||
Flowable<List<PlaylistRemoteEntity>> getAll();
|
||||
|
||||
@Override
|
||||
@Query("DELETE FROM " + REMOTE_PLAYLIST_TABLE)
|
||||
public abstract int deleteAll();
|
||||
int deleteAll();
|
||||
|
||||
@Override
|
||||
@Query("SELECT * FROM " + REMOTE_PLAYLIST_TABLE
|
||||
+ " WHERE " + REMOTE_PLAYLIST_SERVICE_ID + " = :serviceId")
|
||||
public abstract Flowable<List<PlaylistRemoteEntity>> listByService(int serviceId);
|
||||
Flowable<List<PlaylistRemoteEntity>> listByService(int serviceId);
|
||||
|
||||
@Query("SELECT * FROM " + REMOTE_PLAYLIST_TABLE + " WHERE "
|
||||
+ REMOTE_PLAYLIST_URL + " = :url AND " + REMOTE_PLAYLIST_SERVICE_ID + " = :serviceId")
|
||||
public abstract Flowable<List<PlaylistRemoteEntity>> getPlaylist(long serviceId, String url);
|
||||
Flowable<List<PlaylistRemoteEntity>> getPlaylist(long serviceId, String url);
|
||||
|
||||
@Query("SELECT " + REMOTE_PLAYLIST_ID + " FROM " + REMOTE_PLAYLIST_TABLE
|
||||
+ " WHERE " + REMOTE_PLAYLIST_URL + " = :url "
|
||||
+ "AND " + REMOTE_PLAYLIST_SERVICE_ID + " = :serviceId")
|
||||
abstract Long getPlaylistIdInternal(long serviceId, String url);
|
||||
Long getPlaylistIdInternal(long serviceId, String url);
|
||||
|
||||
@Transaction
|
||||
public long upsert(final PlaylistRemoteEntity playlist) {
|
||||
default long upsert(final PlaylistRemoteEntity playlist) {
|
||||
final Long playlistId = getPlaylistIdInternal(playlist.getServiceId(), playlist.getUrl());
|
||||
|
||||
if (playlistId == null) {
|
||||
@@ -55,5 +55,5 @@ public abstract class PlaylistRemoteDAO implements BasicDAO<PlaylistRemoteEntity
|
||||
|
||||
@Query("DELETE FROM " + REMOTE_PLAYLIST_TABLE
|
||||
+ " WHERE " + REMOTE_PLAYLIST_ID + " = :playlistId")
|
||||
public abstract int deletePlaylist(long playlistId);
|
||||
int deletePlaylist(long playlistId);
|
||||
}
|
||||
|
||||
@@ -25,32 +25,32 @@ import static org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity.PL
|
||||
import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_ID;
|
||||
import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_TABLE;
|
||||
import static org.schabi.newpipe.database.stream.model.StreamStateEntity.JOIN_STREAM_ID_ALIAS;
|
||||
import static org.schabi.newpipe.database.stream.model.StreamStateEntity.STREAM_PROGRESS_TIME;
|
||||
import static org.schabi.newpipe.database.stream.model.StreamStateEntity.STREAM_PROGRESS_MILLIS;
|
||||
import static org.schabi.newpipe.database.stream.model.StreamStateEntity.STREAM_STATE_TABLE;
|
||||
|
||||
@Dao
|
||||
public abstract class PlaylistStreamDAO implements BasicDAO<PlaylistStreamEntity> {
|
||||
public interface PlaylistStreamDAO extends BasicDAO<PlaylistStreamEntity> {
|
||||
@Override
|
||||
@Query("SELECT * FROM " + PLAYLIST_STREAM_JOIN_TABLE)
|
||||
public abstract Flowable<List<PlaylistStreamEntity>> getAll();
|
||||
Flowable<List<PlaylistStreamEntity>> getAll();
|
||||
|
||||
@Override
|
||||
@Query("DELETE FROM " + PLAYLIST_STREAM_JOIN_TABLE)
|
||||
public abstract int deleteAll();
|
||||
int deleteAll();
|
||||
|
||||
@Override
|
||||
public Flowable<List<PlaylistStreamEntity>> listByService(final int serviceId) {
|
||||
default Flowable<List<PlaylistStreamEntity>> listByService(final int serviceId) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Query("DELETE FROM " + PLAYLIST_STREAM_JOIN_TABLE
|
||||
+ " WHERE " + JOIN_PLAYLIST_ID + " = :playlistId")
|
||||
public abstract void deleteBatch(long playlistId);
|
||||
void deleteBatch(long playlistId);
|
||||
|
||||
@Query("SELECT COALESCE(MAX(" + JOIN_INDEX + "), -1)"
|
||||
+ " FROM " + PLAYLIST_STREAM_JOIN_TABLE
|
||||
+ " WHERE " + JOIN_PLAYLIST_ID + " = :playlistId")
|
||||
public abstract Flowable<Integer> getMaximumIndexOf(long playlistId);
|
||||
Flowable<Integer> getMaximumIndexOf(long playlistId);
|
||||
|
||||
@Transaction
|
||||
@Query("SELECT * FROM " + STREAM_TABLE + " INNER JOIN "
|
||||
@@ -64,12 +64,12 @@ public abstract class PlaylistStreamDAO implements BasicDAO<PlaylistStreamEntity
|
||||
|
||||
+ " LEFT JOIN "
|
||||
+ "(SELECT " + JOIN_STREAM_ID + " AS " + JOIN_STREAM_ID_ALIAS + ", "
|
||||
+ STREAM_PROGRESS_TIME
|
||||
+ STREAM_PROGRESS_MILLIS
|
||||
+ " FROM " + STREAM_STATE_TABLE + " )"
|
||||
+ " ON " + STREAM_ID + " = " + JOIN_STREAM_ID_ALIAS
|
||||
|
||||
+ " ORDER BY " + JOIN_INDEX + " ASC")
|
||||
public abstract Flowable<List<PlaylistStreamEntry>> getOrderedStreamsOf(long playlistId);
|
||||
Flowable<List<PlaylistStreamEntry>> getOrderedStreamsOf(long playlistId);
|
||||
|
||||
@Transaction
|
||||
@Query("SELECT " + PLAYLIST_ID + ", " + PLAYLIST_NAME + ", " + PLAYLIST_THUMBNAIL_URL + ", "
|
||||
@@ -80,5 +80,5 @@ public abstract class PlaylistStreamDAO implements BasicDAO<PlaylistStreamEntity
|
||||
+ " ON " + PLAYLIST_ID + " = " + JOIN_PLAYLIST_ID
|
||||
+ " GROUP BY " + JOIN_PLAYLIST_ID
|
||||
+ " ORDER BY " + PLAYLIST_NAME + " COLLATE NOCASE ASC")
|
||||
public abstract Flowable<List<PlaylistMetadataEntry>> getPlaylistMetadata();
|
||||
Flowable<List<PlaylistMetadataEntry>> getPlaylistMetadata();
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import androidx.room.Embedded
|
||||
import org.schabi.newpipe.database.LocalItem
|
||||
import org.schabi.newpipe.database.history.model.StreamHistoryEntity
|
||||
import org.schabi.newpipe.database.stream.model.StreamEntity
|
||||
import org.schabi.newpipe.database.stream.model.StreamStateEntity.STREAM_PROGRESS_TIME
|
||||
import org.schabi.newpipe.database.stream.model.StreamStateEntity.STREAM_PROGRESS_MILLIS
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem
|
||||
import java.time.OffsetDateTime
|
||||
|
||||
@@ -13,8 +13,8 @@ class StreamStatisticsEntry(
|
||||
@Embedded
|
||||
val streamEntity: StreamEntity,
|
||||
|
||||
@ColumnInfo(name = STREAM_PROGRESS_TIME, defaultValue = "0")
|
||||
val progressTime: Long,
|
||||
@ColumnInfo(name = STREAM_PROGRESS_MILLIS, defaultValue = "0")
|
||||
val progressMillis: Long,
|
||||
|
||||
@ColumnInfo(name = StreamHistoryEntity.JOIN_STREAM_ID)
|
||||
val streamId: Long,
|
||||
@@ -29,6 +29,7 @@ class StreamStatisticsEntry(
|
||||
val item = StreamInfoItem(streamEntity.serviceId, streamEntity.url, streamEntity.title, streamEntity.streamType)
|
||||
item.duration = streamEntity.duration
|
||||
item.uploaderName = streamEntity.uploader
|
||||
item.uploaderUrl = streamEntity.uploaderUrl
|
||||
item.thumbnailUrl = streamEntity.thumbnailUrl
|
||||
|
||||
return item
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
package org.schabi.newpipe.database.stream
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Embedded
|
||||
import org.schabi.newpipe.database.stream.model.StreamEntity
|
||||
import org.schabi.newpipe.database.stream.model.StreamStateEntity
|
||||
|
||||
data class StreamWithState(
|
||||
@Embedded
|
||||
val stream: StreamEntity,
|
||||
|
||||
@ColumnInfo(name = StreamStateEntity.STREAM_PROGRESS_MILLIS)
|
||||
val stateProgressMillis: Long?
|
||||
)
|
||||
@@ -6,6 +6,7 @@ import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.Query
|
||||
import androidx.room.Transaction
|
||||
import io.reactivex.rxjava3.core.Completable
|
||||
import io.reactivex.rxjava3.core.Flowable
|
||||
import org.schabi.newpipe.database.BasicDAO
|
||||
import org.schabi.newpipe.database.stream.model.StreamEntity
|
||||
@@ -29,6 +30,9 @@ abstract class StreamDAO : BasicDAO<StreamEntity> {
|
||||
@Query("SELECT * FROM streams WHERE url = :url AND service_id = :serviceId")
|
||||
abstract fun getStream(serviceId: Long, url: String): Flowable<List<StreamEntity>>
|
||||
|
||||
@Query("UPDATE streams SET uploader_url = :uploaderUrl WHERE url = :url AND service_id = :serviceId")
|
||||
abstract fun setUploaderUrl(serviceId: Long, url: String, uploaderUrl: String): Completable
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
||||
internal abstract fun silentInsertInternal(stream: StreamEntity): Long
|
||||
|
||||
|
||||
@@ -17,31 +17,31 @@ import static org.schabi.newpipe.database.stream.model.StreamStateEntity.JOIN_ST
|
||||
import static org.schabi.newpipe.database.stream.model.StreamStateEntity.STREAM_STATE_TABLE;
|
||||
|
||||
@Dao
|
||||
public abstract class StreamStateDAO implements BasicDAO<StreamStateEntity> {
|
||||
public interface StreamStateDAO extends BasicDAO<StreamStateEntity> {
|
||||
@Override
|
||||
@Query("SELECT * FROM " + STREAM_STATE_TABLE)
|
||||
public abstract Flowable<List<StreamStateEntity>> getAll();
|
||||
Flowable<List<StreamStateEntity>> getAll();
|
||||
|
||||
@Override
|
||||
@Query("DELETE FROM " + STREAM_STATE_TABLE)
|
||||
public abstract int deleteAll();
|
||||
int deleteAll();
|
||||
|
||||
@Override
|
||||
public Flowable<List<StreamStateEntity>> listByService(final int serviceId) {
|
||||
default Flowable<List<StreamStateEntity>> listByService(final int serviceId) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Query("SELECT * FROM " + STREAM_STATE_TABLE + " WHERE " + JOIN_STREAM_ID + " = :streamId")
|
||||
public abstract Flowable<List<StreamStateEntity>> getState(long streamId);
|
||||
Flowable<List<StreamStateEntity>> getState(long streamId);
|
||||
|
||||
@Query("DELETE FROM " + STREAM_STATE_TABLE + " WHERE " + JOIN_STREAM_ID + " = :streamId")
|
||||
public abstract int deleteState(long streamId);
|
||||
int deleteState(long streamId);
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
||||
abstract void silentInsertInternal(StreamStateEntity streamState);
|
||||
void silentInsertInternal(StreamStateEntity streamState);
|
||||
|
||||
@Transaction
|
||||
public long upsert(final StreamStateEntity stream) {
|
||||
default long upsert(final StreamStateEntity stream) {
|
||||
silentInsertInternal(stream);
|
||||
return update(stream);
|
||||
}
|
||||
|
||||
@@ -45,6 +45,9 @@ data class StreamEntity(
|
||||
@ColumnInfo(name = STREAM_UPLOADER)
|
||||
var uploader: String,
|
||||
|
||||
@ColumnInfo(name = STREAM_UPLOADER_URL)
|
||||
var uploaderUrl: String? = null,
|
||||
|
||||
@ColumnInfo(name = STREAM_THUMBNAIL_URL)
|
||||
var thumbnailUrl: String? = null,
|
||||
|
||||
@@ -64,7 +67,7 @@ data class StreamEntity(
|
||||
constructor(item: StreamInfoItem) : this(
|
||||
serviceId = item.serviceId, url = item.url, title = item.name,
|
||||
streamType = item.streamType, duration = item.duration, uploader = item.uploaderName,
|
||||
thumbnailUrl = item.thumbnailUrl, viewCount = item.viewCount,
|
||||
uploaderUrl = item.uploaderUrl, thumbnailUrl = item.thumbnailUrl, viewCount = item.viewCount,
|
||||
textualUploadDate = item.textualUploadDate, uploadDate = item.uploadDate?.offsetDateTime(),
|
||||
isUploadDateApproximation = item.uploadDate?.isApproximation
|
||||
)
|
||||
@@ -73,7 +76,7 @@ data class StreamEntity(
|
||||
constructor(info: StreamInfo) : this(
|
||||
serviceId = info.serviceId, url = info.url, title = info.name,
|
||||
streamType = info.streamType, duration = info.duration, uploader = info.uploaderName,
|
||||
thumbnailUrl = info.thumbnailUrl, viewCount = info.viewCount,
|
||||
uploaderUrl = info.uploaderUrl, thumbnailUrl = info.thumbnailUrl, viewCount = info.viewCount,
|
||||
textualUploadDate = info.textualUploadDate, uploadDate = info.uploadDate?.offsetDateTime(),
|
||||
isUploadDateApproximation = info.uploadDate?.isApproximation
|
||||
)
|
||||
@@ -82,13 +85,14 @@ data class StreamEntity(
|
||||
constructor(item: PlayQueueItem) : this(
|
||||
serviceId = item.serviceId, url = item.url, title = item.title,
|
||||
streamType = item.streamType, duration = item.duration, uploader = item.uploader,
|
||||
thumbnailUrl = item.thumbnailUrl
|
||||
uploaderUrl = item.uploaderUrl, thumbnailUrl = item.thumbnailUrl
|
||||
)
|
||||
|
||||
fun toStreamInfoItem(): StreamInfoItem {
|
||||
val item = StreamInfoItem(serviceId, url, title, streamType)
|
||||
item.duration = duration
|
||||
item.uploaderName = uploader
|
||||
item.uploaderUrl = uploaderUrl
|
||||
item.thumbnailUrl = thumbnailUrl
|
||||
|
||||
if (viewCount != null) item.viewCount = viewCount as Long
|
||||
@@ -109,6 +113,7 @@ data class StreamEntity(
|
||||
const val STREAM_TYPE = "stream_type"
|
||||
const val STREAM_DURATION = "duration"
|
||||
const val STREAM_UPLOADER = "uploader"
|
||||
const val STREAM_UPLOADER_URL = "uploader_url"
|
||||
const val STREAM_THUMBNAIL_URL = "thumbnail_url"
|
||||
|
||||
const val STREAM_VIEWS = "view_count"
|
||||
|
||||
@@ -5,7 +5,7 @@ import androidx.room.ColumnInfo;
|
||||
import androidx.room.Entity;
|
||||
import androidx.room.ForeignKey;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.Objects;
|
||||
|
||||
import static androidx.room.ForeignKey.CASCADE;
|
||||
import static org.schabi.newpipe.database.stream.model.StreamStateEntity.JOIN_STREAM_ID;
|
||||
@@ -25,26 +25,31 @@ public class StreamStateEntity {
|
||||
// This additional field is required for the SQL query because 'stream_id' is used
|
||||
// for some other joins already
|
||||
public static final String JOIN_STREAM_ID_ALIAS = "stream_id_alias";
|
||||
public static final String STREAM_PROGRESS_TIME = "progress_time";
|
||||
public static final String STREAM_PROGRESS_MILLIS = "progress_time";
|
||||
|
||||
/**
|
||||
* Playback state will not be saved, if playback time is less than this threshold.
|
||||
* Playback state will not be saved, if playback time is less than this threshold (5000ms = 5s).
|
||||
*/
|
||||
private static final int PLAYBACK_SAVE_THRESHOLD_START_SECONDS = 5;
|
||||
private static final long PLAYBACK_SAVE_THRESHOLD_START_MILLISECONDS = 5000;
|
||||
|
||||
/**
|
||||
* Playback state will not be saved, if time left is less than this threshold.
|
||||
* Stream will be considered finished if the playback time left exceeds this threshold
|
||||
* (60000ms = 60s).
|
||||
* @see #isFinished(long)
|
||||
* @see org.schabi.newpipe.database.feed.dao.FeedDAO#getLiveOrNotPlayedStreams()
|
||||
* @see org.schabi.newpipe.database.feed.dao.FeedDAO#getLiveOrNotPlayedStreamsForGroup(long)
|
||||
*/
|
||||
private static final int PLAYBACK_SAVE_THRESHOLD_END_SECONDS = 10;
|
||||
public static final long PLAYBACK_FINISHED_END_MILLISECONDS = 60000;
|
||||
|
||||
@ColumnInfo(name = JOIN_STREAM_ID)
|
||||
private long streamUid;
|
||||
|
||||
@ColumnInfo(name = STREAM_PROGRESS_TIME)
|
||||
private long progressTime;
|
||||
@ColumnInfo(name = STREAM_PROGRESS_MILLIS)
|
||||
private long progressMillis;
|
||||
|
||||
public StreamStateEntity(final long streamUid, final long progressTime) {
|
||||
public StreamStateEntity(final long streamUid, final long progressMillis) {
|
||||
this.streamUid = streamUid;
|
||||
this.progressTime = progressTime;
|
||||
this.progressMillis = progressMillis;
|
||||
}
|
||||
|
||||
public long getStreamUid() {
|
||||
@@ -55,27 +60,53 @@ public class StreamStateEntity {
|
||||
this.streamUid = streamUid;
|
||||
}
|
||||
|
||||
public long getProgressTime() {
|
||||
return progressTime;
|
||||
public long getProgressMillis() {
|
||||
return progressMillis;
|
||||
}
|
||||
|
||||
public void setProgressTime(final long progressTime) {
|
||||
this.progressTime = progressTime;
|
||||
public void setProgressMillis(final long progressMillis) {
|
||||
this.progressMillis = progressMillis;
|
||||
}
|
||||
|
||||
public boolean isValid(final int durationInSeconds) {
|
||||
final int seconds = (int) TimeUnit.MILLISECONDS.toSeconds(progressTime);
|
||||
return seconds > PLAYBACK_SAVE_THRESHOLD_START_SECONDS
|
||||
&& seconds < durationInSeconds - PLAYBACK_SAVE_THRESHOLD_END_SECONDS;
|
||||
/**
|
||||
* The state will be considered valid, and thus be saved, if the progress is more than {@link
|
||||
* #PLAYBACK_SAVE_THRESHOLD_START_MILLISECONDS} or at least 1/4 of the video length.
|
||||
* @param durationInSeconds the duration of the stream connected with this state, in seconds
|
||||
* @return whether this stream state entity should be saved or not
|
||||
*/
|
||||
public boolean isValid(final long durationInSeconds) {
|
||||
return progressMillis > PLAYBACK_SAVE_THRESHOLD_START_MILLISECONDS
|
||||
|| progressMillis > durationInSeconds * 1000 / 4;
|
||||
}
|
||||
|
||||
/**
|
||||
* The video will be considered as finished, if the time left is less than {@link
|
||||
* #PLAYBACK_FINISHED_END_MILLISECONDS} and the progress is at least 3/4 of the video length.
|
||||
* The state will be saved anyway, so that it can be shown under stream info items, but the
|
||||
* player will not resume if a state is considered as finished. Finished streams are also the
|
||||
* ones that can be filtered out in the feed fragment.
|
||||
* @see org.schabi.newpipe.database.feed.dao.FeedDAO#getLiveOrNotPlayedStreams()
|
||||
* @see org.schabi.newpipe.database.feed.dao.FeedDAO#getLiveOrNotPlayedStreamsForGroup(long)
|
||||
* @param durationInSeconds the duration of the stream connected with this state, in seconds
|
||||
* @return whether the stream is finished or not
|
||||
*/
|
||||
public boolean isFinished(final long durationInSeconds) {
|
||||
return progressMillis >= durationInSeconds * 1000 - PLAYBACK_FINISHED_END_MILLISECONDS
|
||||
&& progressMillis >= durationInSeconds * 1000 * 3 / 4;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(@Nullable final Object obj) {
|
||||
if (obj instanceof StreamStateEntity) {
|
||||
return ((StreamStateEntity) obj).streamUid == streamUid
|
||||
&& ((StreamStateEntity) obj).progressTime == progressTime;
|
||||
&& ((StreamStateEntity) obj).progressMillis == progressMillis;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(streamUid, progressMillis);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@ package org.schabi.newpipe.download;
|
||||
import android.app.Activity;
|
||||
import android.content.ComponentName;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.DialogInterface.OnDismissListener;
|
||||
import android.content.Intent;
|
||||
import android.content.ServiceConnection;
|
||||
import android.content.SharedPreferences;
|
||||
@@ -20,6 +22,9 @@ import android.widget.RadioGroup;
|
||||
import android.widget.SeekBar;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.activity.result.ActivityResult;
|
||||
import androidx.activity.result.ActivityResultLauncher;
|
||||
import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult;
|
||||
import androidx.annotation.IdRes;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
@@ -35,10 +40,9 @@ import com.nononsenseapps.filepicker.Utils;
|
||||
|
||||
import org.schabi.newpipe.MainActivity;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.RouterActivity;
|
||||
import org.schabi.newpipe.databinding.DownloadDialogBinding;
|
||||
import org.schabi.newpipe.error.ErrorActivity;
|
||||
import org.schabi.newpipe.error.ErrorInfo;
|
||||
import org.schabi.newpipe.error.ErrorUtil;
|
||||
import org.schabi.newpipe.error.UserAction;
|
||||
import org.schabi.newpipe.extractor.MediaFormat;
|
||||
import org.schabi.newpipe.extractor.NewPipe;
|
||||
@@ -49,6 +53,9 @@ import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||
import org.schabi.newpipe.extractor.stream.SubtitlesStream;
|
||||
import org.schabi.newpipe.extractor.stream.VideoStream;
|
||||
import org.schabi.newpipe.settings.NewPipeSettings;
|
||||
import org.schabi.newpipe.streams.io.NoFileManagerSafeGuard;
|
||||
import org.schabi.newpipe.streams.io.StoredDirectoryHelper;
|
||||
import org.schabi.newpipe.streams.io.StoredFileHelper;
|
||||
import org.schabi.newpipe.util.FilePickerActivityHelper;
|
||||
import org.schabi.newpipe.util.FilenameUtils;
|
||||
import org.schabi.newpipe.util.ListHelper;
|
||||
@@ -68,8 +75,6 @@ import icepick.Icepick;
|
||||
import icepick.State;
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
||||
import us.shandian.giga.get.MissionRecoveryInfo;
|
||||
import us.shandian.giga.io.StoredDirectoryHelper;
|
||||
import us.shandian.giga.io.StoredFileHelper;
|
||||
import us.shandian.giga.postprocessing.Postprocessing;
|
||||
import us.shandian.giga.service.DownloadManager;
|
||||
import us.shandian.giga.service.DownloadManagerService;
|
||||
@@ -82,7 +87,6 @@ public class DownloadDialog extends DialogFragment
|
||||
implements RadioGroup.OnCheckedChangeListener, AdapterView.OnItemSelectedListener {
|
||||
private static final String TAG = "DialogFragment";
|
||||
private static final boolean DEBUG = MainActivity.DEBUG;
|
||||
private static final int REQUEST_DOWNLOAD_SAVE_AS = 0x1230;
|
||||
|
||||
@State
|
||||
StreamInfo currentInfo;
|
||||
@@ -99,6 +103,9 @@ public class DownloadDialog extends DialogFragment
|
||||
@State
|
||||
int selectedSubtitleIndex = 0;
|
||||
|
||||
@Nullable
|
||||
private OnDismissListener onDismissListener = null;
|
||||
|
||||
private StoredDirectoryHelper mainStorageAudio = null;
|
||||
private StoredDirectoryHelper mainStorageVideo = null;
|
||||
private DownloadManager downloadManager = null;
|
||||
@@ -116,6 +123,25 @@ public class DownloadDialog extends DialogFragment
|
||||
|
||||
private SharedPreferences prefs;
|
||||
|
||||
// Variables for file name and MIME type when picking new folder because it's not set yet
|
||||
private String filenameTmp;
|
||||
private String mimeTmp;
|
||||
|
||||
private final ActivityResultLauncher<Intent> requestDownloadSaveAsLauncher =
|
||||
registerForActivityResult(
|
||||
new StartActivityForResult(), this::requestDownloadSaveAsResult);
|
||||
private final ActivityResultLauncher<Intent> requestDownloadPickAudioFolderLauncher =
|
||||
registerForActivityResult(
|
||||
new StartActivityForResult(), this::requestDownloadPickAudioFolderResult);
|
||||
private final ActivityResultLauncher<Intent> requestDownloadPickVideoFolderLauncher =
|
||||
registerForActivityResult(
|
||||
new StartActivityForResult(), this::requestDownloadPickVideoFolderResult);
|
||||
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Instance creation
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
public static DownloadDialog newInstance(final StreamInfo info) {
|
||||
final DownloadDialog dialog = new DownloadDialog();
|
||||
dialog.setInfo(info);
|
||||
@@ -137,6 +163,11 @@ public class DownloadDialog extends DialogFragment
|
||||
return instance;
|
||||
}
|
||||
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Setters
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
private void setInfo(final StreamInfo info) {
|
||||
this.currentInfo = info;
|
||||
}
|
||||
@@ -153,10 +184,6 @@ public class DownloadDialog extends DialogFragment
|
||||
setVideoStreams(new StreamSizeWrapper<>(videoStreams, getContext()));
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// LifeCycle
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
public void setVideoStreams(final StreamSizeWrapper<VideoStream> wvs) {
|
||||
this.wrappedVideoStreams = wvs;
|
||||
}
|
||||
@@ -182,6 +209,14 @@ public class DownloadDialog extends DialogFragment
|
||||
this.selectedSubtitleIndex = ssi;
|
||||
}
|
||||
|
||||
public void setOnDismissListener(@Nullable final OnDismissListener onDismissListener) {
|
||||
this.onDismissListener = onDismissListener;
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Android lifecycle
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public void onCreate(@Nullable final Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
@@ -192,7 +227,7 @@ public class DownloadDialog extends DialogFragment
|
||||
|
||||
if (!PermissionHelper.checkStoragePermissions(getActivity(),
|
||||
PermissionHelper.DOWNLOAD_DIALOG_REQUEST_CODE)) {
|
||||
getDialog().dismiss();
|
||||
dismiss();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -251,10 +286,6 @@ public class DownloadDialog extends DialogFragment
|
||||
}, Context.BIND_AUTO_CREATE);
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Inits
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public View onCreateView(@NonNull final LayoutInflater inflater, final ViewGroup container,
|
||||
final Bundle savedInstanceState) {
|
||||
@@ -310,38 +341,35 @@ public class DownloadDialog extends DialogFragment
|
||||
fetchStreamsSize();
|
||||
}
|
||||
|
||||
private void fetchStreamsSize() {
|
||||
disposables.clear();
|
||||
disposables.add(StreamSizeWrapper.fetchSizeForWrapper(wrappedVideoStreams)
|
||||
.subscribe(result -> {
|
||||
if (dialogBinding.videoAudioGroup.getCheckedRadioButtonId()
|
||||
== R.id.video_button) {
|
||||
setupVideoSpinner();
|
||||
}
|
||||
}, throwable -> ErrorActivity.reportErrorInSnackbar(context,
|
||||
new ErrorInfo(throwable, UserAction.DOWNLOAD_OPEN_DIALOG,
|
||||
"Downloading video stream size",
|
||||
currentInfo.getServiceId()))));
|
||||
disposables.add(StreamSizeWrapper.fetchSizeForWrapper(wrappedAudioStreams)
|
||||
.subscribe(result -> {
|
||||
if (dialogBinding.videoAudioGroup.getCheckedRadioButtonId()
|
||||
== R.id.audio_button) {
|
||||
setupAudioSpinner();
|
||||
}
|
||||
}, throwable -> ErrorActivity.reportErrorInSnackbar(context,
|
||||
new ErrorInfo(throwable, UserAction.DOWNLOAD_OPEN_DIALOG,
|
||||
"Downloading audio stream size",
|
||||
currentInfo.getServiceId()))));
|
||||
disposables.add(StreamSizeWrapper.fetchSizeForWrapper(wrappedSubtitleStreams)
|
||||
.subscribe(result -> {
|
||||
if (dialogBinding.videoAudioGroup.getCheckedRadioButtonId()
|
||||
== R.id.subtitle_button) {
|
||||
setupSubtitleSpinner();
|
||||
}
|
||||
}, throwable -> ErrorActivity.reportErrorInSnackbar(context,
|
||||
new ErrorInfo(throwable, UserAction.DOWNLOAD_OPEN_DIALOG,
|
||||
"Downloading subtitle stream size",
|
||||
currentInfo.getServiceId()))));
|
||||
private void initToolbar(final Toolbar toolbar) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "initToolbar() called with: toolbar = [" + toolbar + "]");
|
||||
}
|
||||
|
||||
toolbar.setTitle(R.string.download_dialog_title);
|
||||
toolbar.setNavigationIcon(R.drawable.ic_arrow_back);
|
||||
toolbar.inflateMenu(R.menu.dialog_url);
|
||||
toolbar.setNavigationOnClickListener(v -> dismiss());
|
||||
toolbar.setNavigationContentDescription(R.string.cancel);
|
||||
|
||||
okButton = toolbar.findViewById(R.id.okay);
|
||||
okButton.setEnabled(false); // disable until the download service connection is done
|
||||
|
||||
toolbar.setOnMenuItemClickListener(item -> {
|
||||
if (item.getItemId() == R.id.okay) {
|
||||
prepareSelectedDownload();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDismiss(@NonNull final DialogInterface dialog) {
|
||||
super.onDismiss(dialog);
|
||||
if (onDismissListener != null) {
|
||||
onDismissListener.onDismiss(dialog);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -356,79 +384,51 @@ public class DownloadDialog extends DialogFragment
|
||||
super.onDestroyView();
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Radio group Video&Audio options - Listener
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public void onSaveInstanceState(@NonNull final Bundle outState) {
|
||||
super.onSaveInstanceState(outState);
|
||||
Icepick.saveInstanceState(this, outState);
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Streams Spinner Listener
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public void onActivityResult(final int requestCode, final int resultCode, final Intent data) {
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
|
||||
if (requestCode == REQUEST_DOWNLOAD_SAVE_AS && resultCode == Activity.RESULT_OK) {
|
||||
if (data.getData() == null) {
|
||||
showFailedDialog(R.string.general_error);
|
||||
return;
|
||||
}
|
||||
|
||||
if (FilePickerActivityHelper.isOwnFileUri(context, data.getData())) {
|
||||
final File file = Utils.getFileForUri(data.getData());
|
||||
checkSelectedDownload(null, Uri.fromFile(file), file.getName(),
|
||||
StoredFileHelper.DEFAULT_MIME);
|
||||
return;
|
||||
}
|
||||
|
||||
final DocumentFile docFile = DocumentFile.fromSingleUri(context, data.getData());
|
||||
if (docFile == null) {
|
||||
showFailedDialog(R.string.general_error);
|
||||
return;
|
||||
}
|
||||
|
||||
// check if the selected file was previously used
|
||||
checkSelectedDownload(null, data.getData(), docFile.getName(),
|
||||
docFile.getType());
|
||||
}
|
||||
}
|
||||
|
||||
private void initToolbar(final Toolbar toolbar) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "initToolbar() called with: toolbar = [" + toolbar + "]");
|
||||
}
|
||||
|
||||
toolbar.setTitle(R.string.download_dialog_title);
|
||||
toolbar.setNavigationIcon(R.drawable.ic_arrow_back);
|
||||
toolbar.inflateMenu(R.menu.dialog_url);
|
||||
toolbar.setNavigationOnClickListener(v -> requireDialog().dismiss());
|
||||
toolbar.setNavigationContentDescription(R.string.cancel);
|
||||
|
||||
okButton = toolbar.findViewById(R.id.okay);
|
||||
okButton.setEnabled(false); // disable until the download service connection is done
|
||||
|
||||
toolbar.setOnMenuItemClickListener(item -> {
|
||||
if (item.getItemId() == R.id.okay) {
|
||||
prepareSelectedDownload();
|
||||
if (getActivity() instanceof RouterActivity) {
|
||||
getActivity().finish();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Utils
|
||||
// Video, audio and subtitle spinners
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
private void fetchStreamsSize() {
|
||||
disposables.clear();
|
||||
disposables.add(StreamSizeWrapper.fetchSizeForWrapper(wrappedVideoStreams)
|
||||
.subscribe(result -> {
|
||||
if (dialogBinding.videoAudioGroup.getCheckedRadioButtonId()
|
||||
== R.id.video_button) {
|
||||
setupVideoSpinner();
|
||||
}
|
||||
}, throwable -> ErrorUtil.showSnackbar(context,
|
||||
new ErrorInfo(throwable, UserAction.DOWNLOAD_OPEN_DIALOG,
|
||||
"Downloading video stream size",
|
||||
currentInfo.getServiceId()))));
|
||||
disposables.add(StreamSizeWrapper.fetchSizeForWrapper(wrappedAudioStreams)
|
||||
.subscribe(result -> {
|
||||
if (dialogBinding.videoAudioGroup.getCheckedRadioButtonId()
|
||||
== R.id.audio_button) {
|
||||
setupAudioSpinner();
|
||||
}
|
||||
}, throwable -> ErrorUtil.showSnackbar(context,
|
||||
new ErrorInfo(throwable, UserAction.DOWNLOAD_OPEN_DIALOG,
|
||||
"Downloading audio stream size",
|
||||
currentInfo.getServiceId()))));
|
||||
disposables.add(StreamSizeWrapper.fetchSizeForWrapper(wrappedSubtitleStreams)
|
||||
.subscribe(result -> {
|
||||
if (dialogBinding.videoAudioGroup.getCheckedRadioButtonId()
|
||||
== R.id.subtitle_button) {
|
||||
setupSubtitleSpinner();
|
||||
}
|
||||
}, throwable -> ErrorUtil.showSnackbar(context,
|
||||
new ErrorInfo(throwable, UserAction.DOWNLOAD_OPEN_DIALOG,
|
||||
"Downloading subtitle stream size",
|
||||
currentInfo.getServiceId()))));
|
||||
}
|
||||
|
||||
private void setupAudioSpinner() {
|
||||
if (getContext() == null) {
|
||||
return;
|
||||
@@ -459,6 +459,88 @@ public class DownloadDialog extends DialogFragment
|
||||
setRadioButtonsState(true);
|
||||
}
|
||||
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Activity results
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
private void requestDownloadPickAudioFolderResult(final ActivityResult result) {
|
||||
requestDownloadPickFolderResult(
|
||||
result, getString(R.string.download_path_audio_key), DownloadManager.TAG_AUDIO);
|
||||
}
|
||||
|
||||
private void requestDownloadPickVideoFolderResult(final ActivityResult result) {
|
||||
requestDownloadPickFolderResult(
|
||||
result, getString(R.string.download_path_video_key), DownloadManager.TAG_VIDEO);
|
||||
}
|
||||
|
||||
private void requestDownloadSaveAsResult(final ActivityResult result) {
|
||||
if (result.getResultCode() != Activity.RESULT_OK) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (result.getData() == null || result.getData().getData() == null) {
|
||||
showFailedDialog(R.string.general_error);
|
||||
return;
|
||||
}
|
||||
|
||||
if (FilePickerActivityHelper.isOwnFileUri(context, result.getData().getData())) {
|
||||
final File file = Utils.getFileForUri(result.getData().getData());
|
||||
checkSelectedDownload(null, Uri.fromFile(file), file.getName(),
|
||||
StoredFileHelper.DEFAULT_MIME);
|
||||
return;
|
||||
}
|
||||
|
||||
final DocumentFile docFile
|
||||
= DocumentFile.fromSingleUri(context, result.getData().getData());
|
||||
if (docFile == null) {
|
||||
showFailedDialog(R.string.general_error);
|
||||
return;
|
||||
}
|
||||
|
||||
// check if the selected file was previously used
|
||||
checkSelectedDownload(null, result.getData().getData(), docFile.getName(),
|
||||
docFile.getType());
|
||||
}
|
||||
|
||||
private void requestDownloadPickFolderResult(final ActivityResult result,
|
||||
final String key,
|
||||
final String tag) {
|
||||
if (result.getResultCode() != Activity.RESULT_OK) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (result.getData() == null || result.getData().getData() == null) {
|
||||
showFailedDialog(R.string.general_error);
|
||||
return;
|
||||
}
|
||||
|
||||
Uri uri = result.getData().getData();
|
||||
if (FilePickerActivityHelper.isOwnFileUri(context, uri)) {
|
||||
uri = Uri.fromFile(Utils.getFileForUri(uri));
|
||||
} else {
|
||||
context.grantUriPermission(context.getPackageName(), uri,
|
||||
StoredDirectoryHelper.PERMISSION_FLAGS);
|
||||
}
|
||||
|
||||
PreferenceManager.getDefaultSharedPreferences(context).edit()
|
||||
.putString(key, uri.toString()).apply();
|
||||
|
||||
try {
|
||||
final StoredDirectoryHelper mainStorage
|
||||
= new StoredDirectoryHelper(context, uri, tag);
|
||||
checkSelectedDownload(mainStorage, mainStorage.findFile(filenameTmp),
|
||||
filenameTmp, mimeTmp);
|
||||
} catch (final IOException e) {
|
||||
showFailedDialog(R.string.general_error);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Listeners
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public void onCheckedChanged(final RadioGroup group, @IdRes final int checkedId) {
|
||||
if (DEBUG) {
|
||||
@@ -508,6 +590,11 @@ public class DownloadDialog extends DialogFragment
|
||||
public void onNothingSelected(final AdapterView<?> parent) {
|
||||
}
|
||||
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Download
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
protected void setupDownloadOptions() {
|
||||
setRadioButtonsState(false);
|
||||
|
||||
@@ -520,7 +607,7 @@ public class DownloadDialog extends DialogFragment
|
||||
dialogBinding.subtitleButton.setVisibility(isSubtitleStreamsAvailable
|
||||
? View.VISIBLE : View.GONE);
|
||||
|
||||
prefs = PreferenceManager.getDefaultSharedPreferences(getContext());
|
||||
prefs = PreferenceManager.getDefaultSharedPreferences(requireContext());
|
||||
final String defaultMedia = prefs.getString(getString(R.string.last_used_download_type),
|
||||
getString(R.string.last_download_type_video_key));
|
||||
|
||||
@@ -548,7 +635,7 @@ public class DownloadDialog extends DialogFragment
|
||||
} else {
|
||||
Toast.makeText(getContext(), R.string.no_streams_available_download,
|
||||
Toast.LENGTH_SHORT).show();
|
||||
getDialog().dismiss();
|
||||
dismiss();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -595,92 +682,111 @@ public class DownloadDialog extends DialogFragment
|
||||
new AlertDialog.Builder(context)
|
||||
.setTitle(R.string.general_error)
|
||||
.setMessage(msg)
|
||||
.setNegativeButton(getString(R.string.finish), null)
|
||||
.setNegativeButton(getString(R.string.ok), null)
|
||||
.create()
|
||||
.show();
|
||||
}
|
||||
|
||||
private void launchDirectoryPicker(final ActivityResultLauncher<Intent> launcher) {
|
||||
NoFileManagerSafeGuard.launchSafe(
|
||||
launcher,
|
||||
StoredDirectoryHelper.getPicker(context),
|
||||
TAG,
|
||||
context
|
||||
);
|
||||
}
|
||||
|
||||
private void prepareSelectedDownload() {
|
||||
final StoredDirectoryHelper mainStorage;
|
||||
final MediaFormat format;
|
||||
final String mime;
|
||||
final String selectedMediaType;
|
||||
|
||||
// first, build the filename and get the output folder (if possible)
|
||||
// later, run a very very very large file checking logic
|
||||
|
||||
String filename = getNameEditText().concat(".");
|
||||
filenameTmp = getNameEditText().concat(".");
|
||||
|
||||
switch (dialogBinding.videoAudioGroup.getCheckedRadioButtonId()) {
|
||||
case R.id.audio_button:
|
||||
selectedMediaType = getString(R.string.last_download_type_audio_key);
|
||||
mainStorage = mainStorageAudio;
|
||||
format = audioStreamsAdapter.getItem(selectedAudioIndex).getFormat();
|
||||
switch (format) {
|
||||
case WEBMA_OPUS:
|
||||
mime = "audio/ogg";
|
||||
filename += "opus";
|
||||
break;
|
||||
default:
|
||||
mime = format.mimeType;
|
||||
filename += format.suffix;
|
||||
break;
|
||||
if (format == MediaFormat.WEBMA_OPUS) {
|
||||
mimeTmp = "audio/ogg";
|
||||
filenameTmp += "opus";
|
||||
} else {
|
||||
mimeTmp = format.mimeType;
|
||||
filenameTmp += format.suffix;
|
||||
}
|
||||
break;
|
||||
case R.id.video_button:
|
||||
selectedMediaType = getString(R.string.last_download_type_video_key);
|
||||
mainStorage = mainStorageVideo;
|
||||
format = videoStreamsAdapter.getItem(selectedVideoIndex).getFormat();
|
||||
mime = format.mimeType;
|
||||
filename += format.suffix;
|
||||
mimeTmp = format.mimeType;
|
||||
filenameTmp += format.suffix;
|
||||
break;
|
||||
case R.id.subtitle_button:
|
||||
selectedMediaType = getString(R.string.last_download_type_subtitle_key);
|
||||
mainStorage = mainStorageVideo; // subtitle & video files go together
|
||||
format = subtitleStreamsAdapter.getItem(selectedSubtitleIndex).getFormat();
|
||||
mime = format.mimeType;
|
||||
filename += (format == MediaFormat.TTML ? MediaFormat.SRT : format).suffix;
|
||||
mimeTmp = format.mimeType;
|
||||
filenameTmp += (format == MediaFormat.TTML ? MediaFormat.SRT : format).suffix;
|
||||
break;
|
||||
default:
|
||||
throw new RuntimeException("No stream selected");
|
||||
}
|
||||
|
||||
if (mainStorage == null || askForSavePath) {
|
||||
// This part is called if with SAF preferred:
|
||||
// * older android version running
|
||||
// * save path not defined (via download settings)
|
||||
// * the user checked the "ask where to download" option
|
||||
if (!askForSavePath
|
||||
&& (mainStorage == null
|
||||
|| mainStorage.isDirect() == NewPipeSettings.useStorageAccessFramework(context)
|
||||
|| mainStorage.isInvalidSafStorage())) {
|
||||
// Pick new download folder if one of:
|
||||
// - Download folder is not set
|
||||
// - Download folder uses SAF while SAF is disabled
|
||||
// - Download folder doesn't use SAF while SAF is enabled
|
||||
// - Download folder uses SAF but the user manually revoked access to it
|
||||
Toast.makeText(context, getString(R.string.no_dir_yet),
|
||||
Toast.LENGTH_LONG).show();
|
||||
|
||||
if (!askForSavePath) {
|
||||
Toast.makeText(context, getString(R.string.no_available_dir),
|
||||
Toast.LENGTH_LONG).show();
|
||||
}
|
||||
|
||||
if (NewPipeSettings.useStorageAccessFramework(context)) {
|
||||
StoredFileHelper.requestSafWithFileCreation(this, REQUEST_DOWNLOAD_SAVE_AS,
|
||||
filename, mime);
|
||||
if (dialogBinding.videoAudioGroup.getCheckedRadioButtonId() == R.id.audio_button) {
|
||||
launchDirectoryPicker(requestDownloadPickAudioFolderLauncher);
|
||||
} else {
|
||||
File initialSavePath;
|
||||
if (dialogBinding.videoAudioGroup.getCheckedRadioButtonId() == R.id.audio_button) {
|
||||
initialSavePath = NewPipeSettings.getDir(Environment.DIRECTORY_MUSIC);
|
||||
} else {
|
||||
initialSavePath = NewPipeSettings.getDir(Environment.DIRECTORY_MOVIES);
|
||||
}
|
||||
|
||||
initialSavePath = new File(initialSavePath, filename);
|
||||
startActivityForResult(FilePickerActivityHelper.chooseFileToSave(context,
|
||||
initialSavePath.getAbsolutePath()), REQUEST_DOWNLOAD_SAVE_AS);
|
||||
launchDirectoryPicker(requestDownloadPickVideoFolderLauncher);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (askForSavePath) {
|
||||
final Uri initialPath;
|
||||
if (NewPipeSettings.useStorageAccessFramework(context)) {
|
||||
initialPath = null;
|
||||
} else {
|
||||
final File initialSavePath;
|
||||
if (dialogBinding.videoAudioGroup.getCheckedRadioButtonId() == R.id.audio_button) {
|
||||
initialSavePath = NewPipeSettings.getDir(Environment.DIRECTORY_MUSIC);
|
||||
} else {
|
||||
initialSavePath = NewPipeSettings.getDir(Environment.DIRECTORY_MOVIES);
|
||||
}
|
||||
initialPath = Uri.parse(initialSavePath.getAbsolutePath());
|
||||
}
|
||||
|
||||
NoFileManagerSafeGuard.launchSafe(
|
||||
requestDownloadSaveAsLauncher,
|
||||
StoredFileHelper.getNewPicker(context, filenameTmp, mimeTmp, initialPath),
|
||||
TAG,
|
||||
context
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// check for existing file with the same name
|
||||
checkSelectedDownload(mainStorage, mainStorage.findFile(filename), filename, mime);
|
||||
checkSelectedDownload(mainStorage, mainStorage.findFile(filenameTmp), filenameTmp, mimeTmp);
|
||||
|
||||
// remember the last media type downloaded by the user
|
||||
prefs.edit()
|
||||
.putString(getString(R.string.last_used_download_type), selectedMediaType)
|
||||
prefs.edit().putString(getString(R.string.last_used_download_type), selectedMediaType)
|
||||
.apply();
|
||||
}
|
||||
|
||||
@@ -703,20 +809,19 @@ public class DownloadDialog extends DialogFragment
|
||||
mainStorage.getTag());
|
||||
}
|
||||
} catch (final Exception e) {
|
||||
ErrorActivity.reportErrorInSnackbar(this,
|
||||
ErrorUtil.createNotification(requireContext(),
|
||||
new ErrorInfo(e, UserAction.DOWNLOAD_FAILED, "Getting storage"));
|
||||
return;
|
||||
}
|
||||
|
||||
// check if is our file
|
||||
// get state of potential mission referring to the same file
|
||||
final MissionState state = downloadManager.checkForExistingMission(storage);
|
||||
@StringRes
|
||||
final int msgBtn;
|
||||
@StringRes
|
||||
final int msgBody;
|
||||
@StringRes final int msgBtn;
|
||||
@StringRes final int msgBody;
|
||||
|
||||
// this switch checks if there is already a mission referring to the same file
|
||||
switch (state) {
|
||||
case Finished:
|
||||
case Finished: // there is already a finished mission
|
||||
msgBtn = R.string.overwrite;
|
||||
msgBody = R.string.overwrite_finished_warning;
|
||||
break;
|
||||
@@ -728,7 +833,7 @@ public class DownloadDialog extends DialogFragment
|
||||
msgBtn = R.string.generate_unique_name;
|
||||
msgBody = R.string.download_already_running;
|
||||
break;
|
||||
case None:
|
||||
case None: // there is no mission referring to the same file
|
||||
if (mainStorage == null) {
|
||||
// This part is called if:
|
||||
// * using SAF on older android version
|
||||
@@ -763,13 +868,13 @@ public class DownloadDialog extends DialogFragment
|
||||
msgBody = R.string.overwrite_unrelated_warning;
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
return; // unreachable
|
||||
}
|
||||
|
||||
final AlertDialog.Builder askDialog = new AlertDialog.Builder(context)
|
||||
.setTitle(R.string.download_dialog_title)
|
||||
.setMessage(msgBody)
|
||||
.setNegativeButton(android.R.string.cancel, null);
|
||||
.setNegativeButton(R.string.cancel, null);
|
||||
final StoredFileHelper finalStorage = storage;
|
||||
|
||||
|
||||
|
||||
@@ -33,12 +33,11 @@ public class AcraReportSender implements ReportSender {
|
||||
|
||||
@Override
|
||||
public void send(@NonNull final Context context, @NonNull final CrashReportData report) {
|
||||
ErrorActivity.reportError(context, new ErrorInfo(
|
||||
ErrorUtil.openActivity(context, new ErrorInfo(
|
||||
new String[]{report.getString(ReportField.STACK_TRACE)},
|
||||
UserAction.UI_ERROR,
|
||||
ErrorInfo.SERVICE_NONE,
|
||||
"ACRA report",
|
||||
R.string.app_ui_crash,
|
||||
null));
|
||||
R.string.app_ui_crash));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
package org.schabi.newpipe.error;
|
||||
|
||||
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.graphics.Color;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
@@ -11,15 +12,12 @@ import android.util.Log;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.fragment.app.Fragment;
|
||||
|
||||
import com.google.android.material.snackbar.Snackbar;
|
||||
import com.grack.nanojson.JsonWriter;
|
||||
|
||||
import org.schabi.newpipe.BuildConfig;
|
||||
@@ -27,15 +25,13 @@ import org.schabi.newpipe.MainActivity;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.databinding.ActivityErrorBinding;
|
||||
import org.schabi.newpipe.util.Localization;
|
||||
import org.schabi.newpipe.util.ShareUtils;
|
||||
import org.schabi.newpipe.util.ThemeHelper;
|
||||
import org.schabi.newpipe.util.external_communication.ShareUtils;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.Arrays;
|
||||
|
||||
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
|
||||
|
||||
/*
|
||||
* Created by Christian Schabesberger on 24.10.15.
|
||||
*
|
||||
@@ -56,6 +52,10 @@ import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
|
||||
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/**
|
||||
* This activity is used to show error details and allow reporting them in various ways. Use {@link
|
||||
* ErrorUtil#openActivity(Context, ErrorInfo)} to correctly open this activity.
|
||||
*/
|
||||
public class ErrorActivity extends AppCompatActivity {
|
||||
// LOG TAGS
|
||||
public static final String TAG = ErrorActivity.class.toString();
|
||||
@@ -77,57 +77,6 @@ public class ErrorActivity extends AppCompatActivity {
|
||||
|
||||
private ActivityErrorBinding activityErrorBinding;
|
||||
|
||||
public static void reportError(final Context context, final ErrorInfo errorInfo) {
|
||||
final Intent intent = new Intent(context, ErrorActivity.class);
|
||||
intent.putExtra(ERROR_INFO, errorInfo);
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
context.startActivity(intent);
|
||||
}
|
||||
|
||||
public static void reportErrorInSnackbar(final Context context, final ErrorInfo errorInfo) {
|
||||
final View rootView = context instanceof Activity
|
||||
? ((Activity) context).findViewById(android.R.id.content) : null;
|
||||
reportErrorInSnackbar(context, rootView, errorInfo);
|
||||
}
|
||||
|
||||
public static void reportErrorInSnackbar(final Fragment fragment, final ErrorInfo errorInfo) {
|
||||
View rootView = fragment.getView();
|
||||
if (rootView == null && fragment.getActivity() != null) {
|
||||
rootView = fragment.getActivity().findViewById(android.R.id.content);
|
||||
}
|
||||
reportErrorInSnackbar(fragment.requireContext(), rootView, errorInfo);
|
||||
}
|
||||
|
||||
public static void reportUiErrorInSnackbar(final Context context,
|
||||
final String request,
|
||||
final Throwable throwable) {
|
||||
reportErrorInSnackbar(context, new ErrorInfo(throwable, UserAction.UI_ERROR, request));
|
||||
}
|
||||
|
||||
public static void reportUiErrorInSnackbar(final Fragment fragment,
|
||||
final String request,
|
||||
final Throwable throwable) {
|
||||
reportErrorInSnackbar(fragment, new ErrorInfo(throwable, UserAction.UI_ERROR, request));
|
||||
}
|
||||
|
||||
|
||||
////////////////////////////////////////////////////////////////////////
|
||||
// Utils
|
||||
////////////////////////////////////////////////////////////////////////
|
||||
|
||||
private static void reportErrorInSnackbar(final Context context,
|
||||
@Nullable final View rootView,
|
||||
final ErrorInfo errorInfo) {
|
||||
if (rootView != null) {
|
||||
Snackbar.make(rootView, R.string.error_snackbar_message, Snackbar.LENGTH_LONG)
|
||||
.setActionTextColor(Color.YELLOW)
|
||||
.setAction(context.getString(R.string.error_snackbar_action).toUpperCase(), v ->
|
||||
reportError(context, errorInfo)).show();
|
||||
} else {
|
||||
reportError(context, errorInfo);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
////////////////////////////////////////////////////////////////////////
|
||||
// Activity lifecycle
|
||||
@@ -195,7 +144,8 @@ public class ErrorActivity extends AppCompatActivity {
|
||||
onBackPressed();
|
||||
return true;
|
||||
case R.id.menu_item_share_error:
|
||||
ShareUtils.shareText(this, getString(R.string.error_report_title), buildJson());
|
||||
ShareUtils.shareText(getApplicationContext(),
|
||||
getString(R.string.error_report_title), buildJson());
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
@@ -220,13 +170,10 @@ public class ErrorActivity extends AppCompatActivity {
|
||||
+ getString(R.string.app_name) + " "
|
||||
+ BuildConfig.VERSION_NAME)
|
||||
.putExtra(Intent.EXTRA_TEXT, buildJson());
|
||||
if (i.resolveActivity(getPackageManager()) != null) {
|
||||
ShareUtils.openIntentInApp(context, i);
|
||||
}
|
||||
ShareUtils.openIntentInApp(context, i, true);
|
||||
} else if (action.equals("GITHUB")) { // open the NewPipe issue page on GitHub
|
||||
ShareUtils.openUrlInBrowser(this, ERROR_GITHUB_ISSUE_URL, false);
|
||||
}
|
||||
|
||||
})
|
||||
.setNegativeButton(R.string.decline, (dialog, which) -> {
|
||||
// do nothing
|
||||
|
||||
@@ -2,10 +2,13 @@ package org.schabi.newpipe.error
|
||||
|
||||
import android.os.Parcelable
|
||||
import androidx.annotation.StringRes
|
||||
import kotlinx.android.parcel.Parcelize
|
||||
import com.google.android.exoplayer2.ExoPlaybackException
|
||||
import kotlinx.parcelize.IgnoredOnParcel
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import org.schabi.newpipe.R
|
||||
import org.schabi.newpipe.extractor.Info
|
||||
import org.schabi.newpipe.extractor.NewPipe
|
||||
import org.schabi.newpipe.extractor.exceptions.AccountTerminatedException
|
||||
import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException
|
||||
import org.schabi.newpipe.extractor.exceptions.ContentNotSupportedException
|
||||
import org.schabi.newpipe.extractor.exceptions.ExtractionException
|
||||
@@ -20,11 +23,14 @@ class ErrorInfo(
|
||||
val userAction: UserAction,
|
||||
val serviceName: String,
|
||||
val request: String,
|
||||
val messageStringId: Int,
|
||||
@Transient // no need to store throwable, all data for report is in other variables
|
||||
var throwable: Throwable? = null
|
||||
val messageStringId: Int
|
||||
) : Parcelable {
|
||||
|
||||
// no need to store throwable, all data for report is in other variables
|
||||
// also, the throwable might not be serializable, see TeamNewPipe/NewPipe#7302
|
||||
@IgnoredOnParcel
|
||||
var throwable: Throwable? = null
|
||||
|
||||
private constructor(
|
||||
throwable: Throwable,
|
||||
userAction: UserAction,
|
||||
@@ -35,9 +41,10 @@ class ErrorInfo(
|
||||
userAction,
|
||||
serviceName,
|
||||
request,
|
||||
getMessageStringId(throwable, userAction),
|
||||
throwable
|
||||
)
|
||||
getMessageStringId(throwable, userAction)
|
||||
) {
|
||||
this.throwable = throwable
|
||||
}
|
||||
|
||||
private constructor(
|
||||
throwable: List<Throwable>,
|
||||
@@ -49,9 +56,10 @@ class ErrorInfo(
|
||||
userAction,
|
||||
serviceName,
|
||||
request,
|
||||
getMessageStringId(throwable.firstOrNull(), userAction),
|
||||
throwable.firstOrNull()
|
||||
)
|
||||
getMessageStringId(throwable.firstOrNull(), userAction)
|
||||
) {
|
||||
this.throwable = throwable.firstOrNull()
|
||||
}
|
||||
|
||||
// constructors with single throwable
|
||||
constructor(throwable: Throwable, userAction: UserAction, request: String) :
|
||||
@@ -95,11 +103,19 @@ class ErrorInfo(
|
||||
action: UserAction
|
||||
): Int {
|
||||
return when {
|
||||
throwable is AccountTerminatedException -> R.string.account_terminated
|
||||
throwable is ContentNotAvailableException -> R.string.content_not_available
|
||||
throwable != null && throwable.isNetworkRelated -> R.string.network_error
|
||||
throwable is ContentNotSupportedException -> R.string.content_not_supported
|
||||
throwable is DeobfuscateException -> R.string.youtube_signature_deobfuscation_error
|
||||
throwable is ExtractionException -> R.string.parsing_error
|
||||
throwable is ExoPlaybackException -> {
|
||||
when (throwable.type) {
|
||||
ExoPlaybackException.TYPE_SOURCE -> R.string.player_stream_failure
|
||||
ExoPlaybackException.TYPE_UNEXPECTED -> R.string.player_recoverable_failure
|
||||
else -> R.string.player_unrecoverable_failure
|
||||
}
|
||||
}
|
||||
action == UserAction.UI_ERROR -> R.string.app_ui_crash
|
||||
action == UserAction.REQUESTED_COMMENTS -> R.string.error_unable_to_load_comments
|
||||
action == UserAction.SUBSCRIPTION_CHANGE -> R.string.subscription_change_failed
|
||||
|
||||
@@ -6,6 +6,8 @@ import android.util.Log
|
||||
import android.view.View
|
||||
import android.widget.Button
|
||||
import android.widget.TextView
|
||||
import androidx.annotation.Nullable
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.fragment.app.Fragment
|
||||
import com.jakewharton.rxbinding4.view.clicks
|
||||
@@ -13,6 +15,8 @@ import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.disposables.Disposable
|
||||
import org.schabi.newpipe.MainActivity
|
||||
import org.schabi.newpipe.R
|
||||
import org.schabi.newpipe.extractor.NewPipe
|
||||
import org.schabi.newpipe.extractor.exceptions.AccountTerminatedException
|
||||
import org.schabi.newpipe.extractor.exceptions.AgeRestrictedContentException
|
||||
import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException
|
||||
import org.schabi.newpipe.extractor.exceptions.ContentNotSupportedException
|
||||
@@ -22,9 +26,11 @@ import org.schabi.newpipe.extractor.exceptions.PrivateContentException
|
||||
import org.schabi.newpipe.extractor.exceptions.ReCaptchaException
|
||||
import org.schabi.newpipe.extractor.exceptions.SoundCloudGoPlusContentException
|
||||
import org.schabi.newpipe.extractor.exceptions.YoutubeMusicPremiumContentException
|
||||
import org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty
|
||||
import org.schabi.newpipe.ktx.animate
|
||||
import org.schabi.newpipe.ktx.isInterruptedCaused
|
||||
import org.schabi.newpipe.ktx.isNetworkRelated
|
||||
import org.schabi.newpipe.util.ServiceHelper
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class ErrorPanelHelper(
|
||||
@@ -33,20 +39,39 @@ class ErrorPanelHelper(
|
||||
onRetry: Runnable
|
||||
) {
|
||||
private val context: Context = rootView.context!!
|
||||
|
||||
private val errorPanelRoot: View = rootView.findViewById(R.id.error_panel)
|
||||
private val errorTextView: TextView = errorPanelRoot.findViewById(R.id.error_message_view)
|
||||
private val errorButtonAction: Button = errorPanelRoot.findViewById(R.id.error_button_action)
|
||||
private val errorButtonRetry: Button = errorPanelRoot.findViewById(R.id.error_button_retry)
|
||||
|
||||
// the only element that is visible by default
|
||||
private val errorTextView: TextView =
|
||||
errorPanelRoot.findViewById(R.id.error_message_view)
|
||||
private val errorServiceInfoTextView: TextView =
|
||||
errorPanelRoot.findViewById(R.id.error_message_service_info_view)
|
||||
private val errorServiceExplanationTextView: TextView =
|
||||
errorPanelRoot.findViewById(R.id.error_message_service_explanation_view)
|
||||
private val errorActionButton: Button =
|
||||
errorPanelRoot.findViewById(R.id.error_action_button)
|
||||
private val errorRetryButton: Button =
|
||||
errorPanelRoot.findViewById(R.id.error_retry_button)
|
||||
|
||||
private var errorDisposable: Disposable? = null
|
||||
|
||||
init {
|
||||
errorDisposable = errorButtonRetry.clicks()
|
||||
errorDisposable = errorRetryButton.clicks()
|
||||
.debounce(300, TimeUnit.MILLISECONDS)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe { onRetry.run() }
|
||||
}
|
||||
|
||||
private fun ensureDefaultVisibility() {
|
||||
errorTextView.isVisible = true
|
||||
|
||||
errorServiceInfoTextView.isVisible = false
|
||||
errorServiceExplanationTextView.isVisible = false
|
||||
errorActionButton.isVisible = false
|
||||
errorRetryButton.isVisible = false
|
||||
}
|
||||
|
||||
fun showError(errorInfo: ErrorInfo) {
|
||||
|
||||
if (errorInfo.throwable != null && errorInfo.throwable!!.isInterruptedCaused) {
|
||||
@@ -56,10 +81,14 @@ class ErrorPanelHelper(
|
||||
return
|
||||
}
|
||||
|
||||
errorButtonAction.isVisible = true
|
||||
ensureDefaultVisibility()
|
||||
|
||||
if (errorInfo.throwable is ReCaptchaException) {
|
||||
errorButtonAction.setText(R.string.recaptcha_solve)
|
||||
errorButtonAction.setOnClickListener {
|
||||
errorTextView.setText(R.string.recaptcha_request_toast)
|
||||
|
||||
showAndSetErrorButtonAction(
|
||||
R.string.recaptcha_solve
|
||||
) {
|
||||
// Starting ReCaptcha Challenge Activity
|
||||
val intent = Intent(context, ReCaptchaActivity::class.java)
|
||||
intent.putExtra(
|
||||
@@ -67,51 +96,70 @@ class ErrorPanelHelper(
|
||||
(errorInfo.throwable as ReCaptchaException).url
|
||||
)
|
||||
fragment.startActivityForResult(intent, ReCaptchaActivity.RECAPTCHA_REQUEST)
|
||||
errorButtonAction.setOnClickListener(null)
|
||||
}
|
||||
errorTextView.setText(R.string.recaptcha_request_toast)
|
||||
errorButtonRetry.isVisible = true
|
||||
} else {
|
||||
errorButtonAction.setText(R.string.error_snackbar_action)
|
||||
errorButtonAction.setOnClickListener {
|
||||
ErrorActivity.reportError(context, errorInfo)
|
||||
errorActionButton.setOnClickListener(null)
|
||||
}
|
||||
|
||||
// hide retry button by default, then show only if not unavailable/unsupported content
|
||||
errorButtonRetry.isVisible = false
|
||||
errorTextView.setText(
|
||||
when (errorInfo.throwable) {
|
||||
is AgeRestrictedContentException -> R.string.restricted_video_no_stream
|
||||
is GeographicRestrictionException -> R.string.georestricted_content
|
||||
is PaidContentException -> R.string.paid_content
|
||||
is PrivateContentException -> R.string.private_content
|
||||
is SoundCloudGoPlusContentException -> R.string.soundcloud_go_plus_content
|
||||
is YoutubeMusicPremiumContentException -> R.string.youtube_music_premium_content
|
||||
is ContentNotAvailableException -> R.string.content_not_available
|
||||
is ContentNotSupportedException -> R.string.content_not_supported
|
||||
else -> {
|
||||
// show retry button only for content which is not unavailable or unsupported
|
||||
errorButtonRetry.isVisible = true
|
||||
if (errorInfo.throwable != null && errorInfo.throwable!!.isNetworkRelated) {
|
||||
R.string.network_error
|
||||
} else {
|
||||
R.string.error_snackbar_message
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
errorRetryButton.isVisible = true
|
||||
} else if (errorInfo.throwable is AccountTerminatedException) {
|
||||
errorTextView.setText(R.string.account_terminated)
|
||||
|
||||
if (!isNullOrEmpty((errorInfo.throwable as AccountTerminatedException).message)) {
|
||||
errorServiceInfoTextView.text = context.resources.getString(
|
||||
R.string.service_provides_reason,
|
||||
NewPipe.getNameOfService(ServiceHelper.getSelectedServiceId(context))
|
||||
)
|
||||
errorServiceInfoTextView.isVisible = true
|
||||
|
||||
errorServiceExplanationTextView.text =
|
||||
(errorInfo.throwable as AccountTerminatedException).message
|
||||
errorServiceExplanationTextView.isVisible = true
|
||||
}
|
||||
} else {
|
||||
showAndSetErrorButtonAction(
|
||||
R.string.error_snackbar_action
|
||||
) {
|
||||
ErrorUtil.openActivity(context, errorInfo)
|
||||
}
|
||||
|
||||
errorTextView.setText(getExceptionDescription(errorInfo.throwable))
|
||||
|
||||
if (errorInfo.throwable !is ContentNotAvailableException &&
|
||||
errorInfo.throwable !is ContentNotSupportedException
|
||||
) {
|
||||
// show retry button only for content which is not unavailable or unsupported
|
||||
errorRetryButton.isVisible = true
|
||||
}
|
||||
}
|
||||
errorPanelRoot.animate(true, 300)
|
||||
|
||||
setRootVisible()
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows the errorButtonAction, sets a text into it and sets the click listener.
|
||||
*/
|
||||
private fun showAndSetErrorButtonAction(
|
||||
@StringRes resid: Int,
|
||||
@Nullable listener: View.OnClickListener
|
||||
) {
|
||||
errorActionButton.isVisible = true
|
||||
errorActionButton.setText(resid)
|
||||
errorActionButton.setOnClickListener(listener)
|
||||
}
|
||||
|
||||
fun showTextError(errorString: String) {
|
||||
errorButtonAction.isVisible = false
|
||||
errorButtonRetry.isVisible = false
|
||||
ensureDefaultVisibility()
|
||||
|
||||
errorTextView.text = errorString
|
||||
|
||||
setRootVisible()
|
||||
}
|
||||
|
||||
private fun setRootVisible() {
|
||||
errorPanelRoot.animate(true, 300)
|
||||
}
|
||||
|
||||
fun hide() {
|
||||
errorButtonAction.setOnClickListener(null)
|
||||
errorActionButton.setOnClickListener(null)
|
||||
errorPanelRoot.animate(false, 150)
|
||||
}
|
||||
|
||||
@@ -120,13 +168,35 @@ class ErrorPanelHelper(
|
||||
}
|
||||
|
||||
fun dispose() {
|
||||
errorButtonAction.setOnClickListener(null)
|
||||
errorButtonRetry.setOnClickListener(null)
|
||||
errorActionButton.setOnClickListener(null)
|
||||
errorRetryButton.setOnClickListener(null)
|
||||
errorDisposable?.dispose()
|
||||
}
|
||||
|
||||
companion object {
|
||||
val TAG: String = ErrorPanelHelper::class.simpleName!!
|
||||
val DEBUG: Boolean = MainActivity.DEBUG
|
||||
|
||||
@StringRes
|
||||
fun getExceptionDescription(throwable: Throwable?): Int {
|
||||
return when (throwable) {
|
||||
is AgeRestrictedContentException -> R.string.restricted_video_no_stream
|
||||
is GeographicRestrictionException -> R.string.georestricted_content
|
||||
is PaidContentException -> R.string.paid_content
|
||||
is PrivateContentException -> R.string.private_content
|
||||
is SoundCloudGoPlusContentException -> R.string.soundcloud_go_plus_content
|
||||
is YoutubeMusicPremiumContentException -> R.string.youtube_music_premium_content
|
||||
is ContentNotAvailableException -> R.string.content_not_available
|
||||
is ContentNotSupportedException -> R.string.content_not_supported
|
||||
else -> {
|
||||
// show retry button only for content which is not unavailable or unsupported
|
||||
if (throwable != null && throwable.isNetworkRelated) {
|
||||
R.string.network_error
|
||||
} else {
|
||||
R.string.error_snackbar_message
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
165
app/src/main/java/org/schabi/newpipe/error/ErrorUtil.kt
Normal file
165
app/src/main/java/org/schabi/newpipe/error/ErrorUtil.kt
Normal file
@@ -0,0 +1,165 @@
|
||||
package org.schabi.newpipe.error
|
||||
|
||||
import android.app.Activity
|
||||
import android.app.NotificationManager
|
||||
import android.app.PendingIntent
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.graphics.Color
|
||||
import android.os.Build
|
||||
import android.view.View
|
||||
import android.widget.Toast
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.fragment.app.Fragment
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import org.schabi.newpipe.R
|
||||
|
||||
/**
|
||||
* This class contains all of the methods that should be used to let the user know that an error has
|
||||
* occurred in the least intrusive way possible for each case. This class is for unexpected errors,
|
||||
* for handled errors (e.g. network errors) use e.g. [ErrorPanelHelper] instead.
|
||||
* - Use a snackbar if the exception is not critical and it happens in a place where a root view
|
||||
* is available.
|
||||
* - Use a notification if the exception happens inside a background service (player, subscription
|
||||
* import, ...) or there is no activity/fragment from which to extract a root view.
|
||||
* - Finally use the error activity only as a last resort in case the exception is critical and
|
||||
* happens in an open activity (since the workflow would be interrupted anyway in that case).
|
||||
*/
|
||||
class ErrorUtil {
|
||||
companion object {
|
||||
private const val ERROR_REPORT_NOTIFICATION_ID = 5340681
|
||||
|
||||
/**
|
||||
* Starts a new error activity allowing the user to report the provided error. Only use this
|
||||
* method directly as a last resort in case the exception is critical and happens in an open
|
||||
* activity (since the workflow would be interrupted anyway in that case). So never use this
|
||||
* for background services.
|
||||
*
|
||||
* @param context the context to use to start the new activity
|
||||
* @param errorInfo the error info to be reported
|
||||
*/
|
||||
@JvmStatic
|
||||
fun openActivity(context: Context, errorInfo: ErrorInfo) {
|
||||
context.startActivity(getErrorActivityIntent(context, errorInfo))
|
||||
}
|
||||
|
||||
/**
|
||||
* Show a bottom snackbar to the user, with a report button that opens the error activity.
|
||||
* Use this method if the exception is not critical and it happens in a place where a root
|
||||
* view is available.
|
||||
*
|
||||
* @param context will be used to obtain the root view if it is an [Activity]; if no root
|
||||
* view can be found an error notification is shown instead
|
||||
* @param errorInfo the error info to be reported
|
||||
*/
|
||||
@JvmStatic
|
||||
fun showSnackbar(context: Context, errorInfo: ErrorInfo) {
|
||||
val rootView = if (context is Activity) context.findViewById<View>(R.id.content) else null
|
||||
showSnackbar(context, rootView, errorInfo)
|
||||
}
|
||||
|
||||
/**
|
||||
* Show a bottom snackbar to the user, with a report button that opens the error activity.
|
||||
* Use this method if the exception is not critical and it happens in a place where a root
|
||||
* view is available.
|
||||
*
|
||||
* @param fragment will be used to obtain the root view if it has a connected [Activity]; if
|
||||
* no root view can be found an error notification is shown instead
|
||||
* @param errorInfo the error info to be reported
|
||||
*/
|
||||
@JvmStatic
|
||||
fun showSnackbar(fragment: Fragment, errorInfo: ErrorInfo) {
|
||||
var rootView = fragment.view
|
||||
if (rootView == null && fragment.activity != null) {
|
||||
rootView = fragment.requireActivity().findViewById(R.id.content)
|
||||
}
|
||||
showSnackbar(fragment.requireContext(), rootView, errorInfo)
|
||||
}
|
||||
|
||||
/**
|
||||
* Shortcut to calling [showSnackbar] with an [ErrorInfo] of type [UserAction.UI_ERROR]
|
||||
*/
|
||||
@JvmStatic
|
||||
fun showUiErrorSnackbar(context: Context, request: String, throwable: Throwable) {
|
||||
showSnackbar(context, ErrorInfo(throwable, UserAction.UI_ERROR, request))
|
||||
}
|
||||
|
||||
/**
|
||||
* Shortcut to calling [showSnackbar] with an [ErrorInfo] of type [UserAction.UI_ERROR]
|
||||
*/
|
||||
@JvmStatic
|
||||
fun showUiErrorSnackbar(fragment: Fragment, request: String, throwable: Throwable) {
|
||||
showSnackbar(fragment, ErrorInfo(throwable, UserAction.UI_ERROR, request))
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an error notification. Tapping on the notification opens the error activity. Use
|
||||
* this method if the exception happens inside a background service (player, subscription
|
||||
* import, ...) or there is no activity/fragment from which to extract a root view.
|
||||
*
|
||||
* @param context the context to use to show the notification
|
||||
* @param errorInfo the error info to be reported; the error message
|
||||
* [ErrorInfo.messageStringId] will be shown in the notification
|
||||
* description
|
||||
*/
|
||||
@JvmStatic
|
||||
fun createNotification(context: Context, errorInfo: ErrorInfo) {
|
||||
val notificationManager =
|
||||
ContextCompat.getSystemService(context, NotificationManager::class.java)
|
||||
if (notificationManager == null) {
|
||||
// this should never happen, but just in case open error activity
|
||||
openActivity(context, errorInfo)
|
||||
}
|
||||
|
||||
var pendingIntentFlags = PendingIntent.FLAG_UPDATE_CURRENT
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
pendingIntentFlags = pendingIntentFlags or PendingIntent.FLAG_IMMUTABLE
|
||||
}
|
||||
|
||||
val notificationBuilder: NotificationCompat.Builder =
|
||||
NotificationCompat.Builder(
|
||||
context,
|
||||
context.getString(R.string.error_report_channel_id)
|
||||
)
|
||||
.setSmallIcon(R.drawable.ic_bug_report)
|
||||
.setContentTitle(context.getString(R.string.error_report_notification_title))
|
||||
.setContentText(context.getString(errorInfo.messageStringId))
|
||||
.setAutoCancel(true)
|
||||
.setContentIntent(
|
||||
PendingIntent.getActivity(
|
||||
context,
|
||||
0,
|
||||
getErrorActivityIntent(context, errorInfo),
|
||||
pendingIntentFlags
|
||||
)
|
||||
)
|
||||
|
||||
notificationManager!!.notify(ERROR_REPORT_NOTIFICATION_ID, notificationBuilder.build())
|
||||
|
||||
// since the notification is silent, also show a toast, otherwise the user is confused
|
||||
Toast.makeText(context, R.string.error_report_notification_toast, Toast.LENGTH_SHORT)
|
||||
.show()
|
||||
}
|
||||
|
||||
private fun getErrorActivityIntent(context: Context, errorInfo: ErrorInfo): Intent {
|
||||
val intent = Intent(context, ErrorActivity::class.java)
|
||||
intent.putExtra(ErrorActivity.ERROR_INFO, errorInfo)
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
return intent
|
||||
}
|
||||
|
||||
private fun showSnackbar(context: Context, rootView: View?, errorInfo: ErrorInfo) {
|
||||
if (rootView == null) {
|
||||
// fallback to showing a notification if no root view is available
|
||||
createNotification(context, errorInfo)
|
||||
} else {
|
||||
Snackbar.make(rootView, R.string.error_snackbar_message, Snackbar.LENGTH_LONG)
|
||||
.setActionTextColor(Color.YELLOW)
|
||||
.setAction(context.getString(R.string.error_snackbar_action).uppercase()) {
|
||||
openActivity(context, errorInfo)
|
||||
}.show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
package org.schabi.newpipe.error;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.Build;
|
||||
@@ -66,6 +67,7 @@ public class ReCaptchaActivity extends AppCompatActivity {
|
||||
private ActivityRecaptchaBinding recaptchaBinding;
|
||||
private String foundCookies = "";
|
||||
|
||||
@SuppressLint("SetJavaScriptEnabled")
|
||||
@Override
|
||||
protected void onCreate(final Bundle savedInstanceState) {
|
||||
ThemeHelper.setTheme(this);
|
||||
@@ -162,6 +164,9 @@ public class ReCaptchaActivity extends AppCompatActivity {
|
||||
setResult(RESULT_OK);
|
||||
}
|
||||
|
||||
// Navigate to blank page (unloads youtube to prevent background playback)
|
||||
recaptchaBinding.reCaptchaWebView.loadUrl("about:blank");
|
||||
|
||||
final Intent intent = new Intent(this, org.schabi.newpipe.MainActivity.class);
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
|
||||
NavUtils.navigateUpTo(this, intent);
|
||||
|
||||
@@ -7,12 +7,13 @@ import android.widget.ProgressBar;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.Fragment;
|
||||
|
||||
import org.schabi.newpipe.BaseFragment;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.error.ErrorActivity;
|
||||
import org.schabi.newpipe.error.ErrorInfo;
|
||||
import org.schabi.newpipe.error.ErrorPanelHelper;
|
||||
import org.schabi.newpipe.error.ErrorUtil;
|
||||
import org.schabi.newpipe.util.InfoCache;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
@@ -198,9 +199,8 @@ public abstract class BaseStateFragment<I> extends BaseFragment implements ViewC
|
||||
}
|
||||
|
||||
/**
|
||||
* Show a SnackBar and only call
|
||||
* {@link ErrorActivity#reportErrorInSnackbar(androidx.fragment.app.Fragment, ErrorInfo)}
|
||||
* IF we a find a valid view (otherwise the error screen appears).
|
||||
* Directly calls {@link ErrorUtil#showSnackbar(Fragment, ErrorInfo)}, that shows a snackbar if
|
||||
* a valid view can be found, otherwise creates an error report notification.
|
||||
*
|
||||
* @param errorInfo The error information
|
||||
*/
|
||||
@@ -208,6 +208,6 @@ public abstract class BaseStateFragment<I> extends BaseFragment implements ViewC
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "showSnackBarError() called with: errorInfo = [" + errorInfo + "]");
|
||||
}
|
||||
ErrorActivity.reportErrorInSnackbar(this, errorInfo);
|
||||
ErrorUtil.showSnackbar(this, errorInfo);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,8 +20,8 @@ public class BlankFragment extends BaseFragment {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setUserVisibleHint(final boolean isVisibleToUser) {
|
||||
super.setUserVisibleHint(isVisibleToUser);
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
setTitle("NewPipe");
|
||||
// leave this inline. Will make it harder for copy cats.
|
||||
// If you are a Copy cat FUCK YOU.
|
||||
|
||||
@@ -23,7 +23,7 @@ import com.google.android.material.tabs.TabLayout;
|
||||
import org.schabi.newpipe.BaseFragment;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.databinding.FragmentMainBinding;
|
||||
import org.schabi.newpipe.error.ErrorActivity;
|
||||
import org.schabi.newpipe.error.ErrorUtil;
|
||||
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||
import org.schabi.newpipe.settings.tabs.Tab;
|
||||
import org.schabi.newpipe.settings.tabs.TabsManager;
|
||||
@@ -130,7 +130,7 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte
|
||||
Log.d(TAG, "onCreateOptionsMenu() called with: "
|
||||
+ "menu = [" + menu + "], inflater = [" + inflater + "]");
|
||||
}
|
||||
inflater.inflate(R.menu.main_fragment_menu, menu);
|
||||
inflater.inflate(R.menu.menu_main_fragment, menu);
|
||||
|
||||
final ActionBar supportActionBar = activity.getSupportActionBar();
|
||||
if (supportActionBar != null) {
|
||||
@@ -145,7 +145,7 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte
|
||||
NavigationHelper.openSearchFragment(getFM(),
|
||||
ServiceHelper.getSelectedServiceId(activity), "");
|
||||
} catch (final Exception e) {
|
||||
ErrorActivity.reportUiErrorInSnackbar(this, "Opening search fragment", e);
|
||||
ErrorUtil.showUiErrorSnackbar(this, "Opening search fragment", e);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -227,16 +227,11 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte
|
||||
public Fragment getItem(final int position) {
|
||||
final Tab tab = internalTabsList.get(position);
|
||||
|
||||
Throwable throwable = null;
|
||||
Fragment fragment = null;
|
||||
final Fragment fragment;
|
||||
try {
|
||||
fragment = tab.getFragment(context);
|
||||
} catch (final ExtractionException e) {
|
||||
throwable = e;
|
||||
}
|
||||
|
||||
if (throwable != null) {
|
||||
ErrorActivity.reportUiErrorInSnackbar(context, "Getting fragment item", throwable);
|
||||
ErrorUtil.showUiErrorSnackbar(context, "Getting fragment item", e);
|
||||
return new BlankFragment();
|
||||
}
|
||||
|
||||
|
||||
@@ -23,15 +23,15 @@ import org.schabi.newpipe.extractor.stream.Description;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||
import org.schabi.newpipe.util.Localization;
|
||||
import org.schabi.newpipe.util.NavigationHelper;
|
||||
import org.schabi.newpipe.util.ShareUtils;
|
||||
import org.schabi.newpipe.util.TextLinkifier;
|
||||
import org.schabi.newpipe.util.external_communication.ShareUtils;
|
||||
import org.schabi.newpipe.util.external_communication.TextLinkifier;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import icepick.State;
|
||||
import io.reactivex.rxjava3.disposables.Disposable;
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
||||
|
||||
import static android.text.TextUtils.isEmpty;
|
||||
import static org.schabi.newpipe.extractor.stream.StreamExtractor.NO_AGE_LIMIT;
|
||||
@@ -41,8 +41,7 @@ public class DescriptionFragment extends BaseFragment {
|
||||
|
||||
@State
|
||||
StreamInfo streamInfo = null;
|
||||
@Nullable
|
||||
Disposable descriptionDisposable = null;
|
||||
final CompositeDisposable descriptionDisposables = new CompositeDisposable();
|
||||
FragmentDescriptionBinding binding;
|
||||
|
||||
public DescriptionFragment() {
|
||||
@@ -67,10 +66,8 @@ public class DescriptionFragment extends BaseFragment {
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
descriptionDisposables.clear();
|
||||
super.onDestroy();
|
||||
if (descriptionDisposable != null) {
|
||||
descriptionDisposable.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -133,17 +130,17 @@ public class DescriptionFragment extends BaseFragment {
|
||||
final Description description = streamInfo.getDescription();
|
||||
switch (description.getType()) {
|
||||
case Description.HTML:
|
||||
descriptionDisposable = TextLinkifier.createLinksFromHtmlBlock(requireContext(),
|
||||
description.getContent(), binding.detailDescriptionView,
|
||||
HtmlCompat.FROM_HTML_MODE_LEGACY);
|
||||
TextLinkifier.createLinksFromHtmlBlock(binding.detailDescriptionView,
|
||||
description.getContent(), HtmlCompat.FROM_HTML_MODE_LEGACY, streamInfo,
|
||||
descriptionDisposables);
|
||||
break;
|
||||
case Description.MARKDOWN:
|
||||
descriptionDisposable = TextLinkifier.createLinksFromMarkdownText(requireContext(),
|
||||
description.getContent(), binding.detailDescriptionView);
|
||||
TextLinkifier.createLinksFromMarkdownText(binding.detailDescriptionView,
|
||||
description.getContent(), streamInfo, descriptionDisposables);
|
||||
break;
|
||||
case Description.PLAIN_TEXT: default:
|
||||
descriptionDisposable = TextLinkifier.createLinksFromPlainText(requireContext(),
|
||||
description.getContent(), binding.detailDescriptionView);
|
||||
TextLinkifier.createLinksFromPlainText(binding.detailDescriptionView,
|
||||
description.getContent(), streamInfo, descriptionDisposables);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -154,8 +151,6 @@ public class DescriptionFragment extends BaseFragment {
|
||||
addMetadataItem(inflater, layout, false,
|
||||
R.string.metadata_category, streamInfo.getCategory());
|
||||
|
||||
addTagsMetadataItem(inflater, layout);
|
||||
|
||||
addMetadataItem(inflater, layout, false,
|
||||
R.string.metadata_licence, streamInfo.getLicence());
|
||||
|
||||
@@ -177,6 +172,8 @@ public class DescriptionFragment extends BaseFragment {
|
||||
R.string.metadata_host, streamInfo.getHost());
|
||||
addMetadataItem(inflater, layout, true,
|
||||
R.string.metadata_thumbnail_url, streamInfo.getThumbnailUrl());
|
||||
|
||||
addTagsMetadataItem(inflater, layout);
|
||||
}
|
||||
|
||||
private void addMetadataItem(final LayoutInflater inflater,
|
||||
@@ -198,8 +195,8 @@ public class DescriptionFragment extends BaseFragment {
|
||||
});
|
||||
|
||||
if (linkifyContent) {
|
||||
TextLinkifier.createLinksFromPlainText(requireContext(),
|
||||
content, itemBinding.metadataContentView);
|
||||
TextLinkifier.createLinksFromPlainText(itemBinding.metadataContentView, content, null,
|
||||
descriptionDisposables);
|
||||
} else {
|
||||
itemBinding.metadataContentView.setText(content);
|
||||
}
|
||||
|
||||
@@ -48,16 +48,15 @@ import com.google.android.exoplayer2.PlaybackParameters;
|
||||
import com.google.android.material.appbar.AppBarLayout;
|
||||
import com.google.android.material.bottomsheet.BottomSheetBehavior;
|
||||
import com.google.android.material.tabs.TabLayout;
|
||||
import com.nostra13.universalimageloader.core.assist.FailReason;
|
||||
import com.nostra13.universalimageloader.core.listener.ImageLoadingListener;
|
||||
import com.nostra13.universalimageloader.core.listener.SimpleImageLoadingListener;
|
||||
import com.squareup.picasso.Callback;
|
||||
|
||||
import org.schabi.newpipe.App;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.database.stream.model.StreamEntity;
|
||||
import org.schabi.newpipe.databinding.FragmentVideoDetailBinding;
|
||||
import org.schabi.newpipe.download.DownloadDialog;
|
||||
import org.schabi.newpipe.error.ErrorActivity;
|
||||
import org.schabi.newpipe.error.ErrorInfo;
|
||||
import org.schabi.newpipe.error.ErrorUtil;
|
||||
import org.schabi.newpipe.error.ReCaptchaActivity;
|
||||
import org.schabi.newpipe.error.UserAction;
|
||||
import org.schabi.newpipe.extractor.InfoItem;
|
||||
@@ -75,10 +74,10 @@ import org.schabi.newpipe.fragments.EmptyFragment;
|
||||
import org.schabi.newpipe.fragments.list.comments.CommentsFragment;
|
||||
import org.schabi.newpipe.fragments.list.videos.RelatedItemsFragment;
|
||||
import org.schabi.newpipe.ktx.AnimationType;
|
||||
import org.schabi.newpipe.local.dialog.PlaylistAppendDialog;
|
||||
import org.schabi.newpipe.local.dialog.PlaylistCreationDialog;
|
||||
import org.schabi.newpipe.local.dialog.PlaylistDialog;
|
||||
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
||||
import org.schabi.newpipe.player.MainPlayer;
|
||||
import org.schabi.newpipe.player.MainPlayer.PlayerType;
|
||||
import org.schabi.newpipe.player.Player;
|
||||
import org.schabi.newpipe.player.event.OnKeyDownListener;
|
||||
import org.schabi.newpipe.player.event.PlayerServiceExtendedEventListener;
|
||||
@@ -90,16 +89,17 @@ import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
|
||||
import org.schabi.newpipe.util.Constants;
|
||||
import org.schabi.newpipe.util.DeviceUtils;
|
||||
import org.schabi.newpipe.util.ExtractorHelper;
|
||||
import org.schabi.newpipe.util.ImageDisplayConstants;
|
||||
import org.schabi.newpipe.util.KoreUtil;
|
||||
import org.schabi.newpipe.util.ListHelper;
|
||||
import org.schabi.newpipe.util.Localization;
|
||||
import org.schabi.newpipe.util.NavigationHelper;
|
||||
import org.schabi.newpipe.util.PermissionHelper;
|
||||
import org.schabi.newpipe.util.ShareUtils;
|
||||
import org.schabi.newpipe.util.PicassoHelper;
|
||||
import org.schabi.newpipe.util.ThemeHelper;
|
||||
import org.schabi.newpipe.util.external_communication.KoreUtils;
|
||||
import org.schabi.newpipe.util.external_communication.ShareUtils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
@@ -151,6 +151,8 @@ public final class VideoDetailFragment
|
||||
private static final String DESCRIPTION_TAB_TAG = "DESCRIPTION TAB";
|
||||
private static final String EMPTY_TAB_TAG = "EMPTY TAB";
|
||||
|
||||
private static final String PICASSO_VIDEO_DETAILS_TAG = "PICASSO_VIDEO_DETAILS_TAG";
|
||||
|
||||
// tabs
|
||||
private boolean showComments;
|
||||
private boolean showRelatedItems;
|
||||
@@ -201,6 +203,7 @@ public final class VideoDetailFragment
|
||||
@Nullable
|
||||
private MainPlayer playerService;
|
||||
private Player player;
|
||||
private final PlayerHolder playerHolder = PlayerHolder.getInstance();
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Service management
|
||||
@@ -219,7 +222,7 @@ public final class VideoDetailFragment
|
||||
return;
|
||||
}
|
||||
|
||||
if (isLandscape()) {
|
||||
if (DeviceUtils.isLandscape(requireContext())) {
|
||||
// If the video is playing but orientation changed
|
||||
// let's make the video in fullscreen again
|
||||
checkLandscape();
|
||||
@@ -240,7 +243,7 @@ public final class VideoDetailFragment
|
||||
&& isAutoplayEnabled()
|
||||
&& player.getParentActivity() == null)) {
|
||||
autoPlayEnabled = true; // forcefully start playing
|
||||
openVideoPlayer();
|
||||
openVideoPlayerAutoFullscreen();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -304,7 +307,8 @@ public final class VideoDetailFragment
|
||||
@Override
|
||||
public View onCreateView(@NonNull final LayoutInflater inflater, final ViewGroup container,
|
||||
final Bundle savedInstanceState) {
|
||||
return inflater.inflate(R.layout.fragment_video_detail, container, false);
|
||||
binding = FragmentVideoDetailBinding.inflate(inflater, container, false);
|
||||
return binding.getRoot();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -355,14 +359,13 @@ public final class VideoDetailFragment
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
binding = null;
|
||||
|
||||
// Stop the service when user leaves the app with double back press
|
||||
// if video player is selected. Otherwise unbind
|
||||
if (activity.isFinishing() && player != null && player.videoPlayerSelected()) {
|
||||
PlayerHolder.stopService(App.getApp());
|
||||
if (activity.isFinishing() && isPlayerAvailable() && player.videoPlayerSelected()) {
|
||||
playerHolder.stopService();
|
||||
} else {
|
||||
PlayerHolder.removeListener();
|
||||
playerHolder.setListener(null);
|
||||
}
|
||||
|
||||
PreferenceManager.getDefaultSharedPreferences(activity)
|
||||
@@ -388,6 +391,12 @@ public final class VideoDetailFragment
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyView() {
|
||||
super.onDestroyView();
|
||||
binding = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityResult(final int requestCode, final int resultCode, final Intent data) {
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
@@ -416,7 +425,7 @@ public final class VideoDetailFragment
|
||||
showRelatedItems = sharedPreferences.getBoolean(key, true);
|
||||
tabSettingsChanged = true;
|
||||
} else if (key.equals(getString(R.string.show_description_key))) {
|
||||
showComments = sharedPreferences.getBoolean(key, true);
|
||||
showDescription = sharedPreferences.getBoolean(key, true);
|
||||
tabSettingsChanged = true;
|
||||
}
|
||||
}
|
||||
@@ -436,12 +445,11 @@ public final class VideoDetailFragment
|
||||
break;
|
||||
case R.id.detail_controls_playlist_append:
|
||||
if (getFM() != null && currentInfo != null) {
|
||||
|
||||
final PlaylistAppendDialog d = PlaylistAppendDialog.fromStreamInfo(currentInfo);
|
||||
disposables.add(
|
||||
PlaylistAppendDialog.onPlaylistFound(getContext(),
|
||||
() -> d.show(getFM(), TAG),
|
||||
() -> PlaylistCreationDialog.newInstance(d).show(getFM(), TAG)
|
||||
PlaylistDialog.createCorrespondingDialog(
|
||||
getContext(),
|
||||
Collections.singletonList(new StreamEntity(currentInfo)),
|
||||
dialog -> dialog.show(getFM(), TAG)
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -454,8 +462,8 @@ public final class VideoDetailFragment
|
||||
break;
|
||||
case R.id.detail_controls_share:
|
||||
if (currentInfo != null) {
|
||||
ShareUtils.shareText(requireContext(),
|
||||
currentInfo.getName(), currentInfo.getUrl());
|
||||
ShareUtils.shareText(requireContext(), currentInfo.getName(),
|
||||
currentInfo.getUrl(), currentInfo.getThumbnailUrl());
|
||||
}
|
||||
break;
|
||||
case R.id.detail_controls_open_in_browser:
|
||||
@@ -472,7 +480,7 @@ public final class VideoDetailFragment
|
||||
if (DEBUG) {
|
||||
Log.i(TAG, "Failed to start kore", e);
|
||||
}
|
||||
KoreUtil.showInstallKoreDialog(requireContext());
|
||||
KoreUtils.showInstallKoreDialog(requireContext());
|
||||
}
|
||||
}
|
||||
break;
|
||||
@@ -492,7 +500,11 @@ public final class VideoDetailFragment
|
||||
break;
|
||||
case R.id.detail_thumbnail_root_layout:
|
||||
autoPlayEnabled = true; // forcefully start playing
|
||||
openVideoPlayer();
|
||||
// FIXME Workaround #7427
|
||||
if (isPlayerAvailable()) {
|
||||
player.setRecovery();
|
||||
}
|
||||
openVideoPlayerAutoFullscreen();
|
||||
break;
|
||||
case R.id.detail_title_root_layout:
|
||||
toggleTitleAndSecondaryControls();
|
||||
@@ -509,10 +521,10 @@ public final class VideoDetailFragment
|
||||
showSystemUi();
|
||||
} else {
|
||||
autoPlayEnabled = true; // forcefully start playing
|
||||
openVideoPlayer();
|
||||
openVideoPlayer(false);
|
||||
}
|
||||
|
||||
setOverlayPlayPauseImage(player != null && player.isPlaying());
|
||||
setOverlayPlayPauseImage(isPlayerAvailable() && player.isPlaying());
|
||||
break;
|
||||
case R.id.overlay_close_button:
|
||||
bottomSheetBehavior.setState(BottomSheetBehavior.STATE_HIDDEN);
|
||||
@@ -525,7 +537,7 @@ public final class VideoDetailFragment
|
||||
NavigationHelper.openChannelFragment(getFM(), currentInfo.getServiceId(),
|
||||
subChannelUrl, subChannelName);
|
||||
} catch (final Exception e) {
|
||||
ErrorActivity.reportUiErrorInSnackbar(this, "Opening channel fragment", e);
|
||||
ErrorUtil.showUiErrorSnackbar(this, "Opening channel fragment", e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -587,9 +599,13 @@ public final class VideoDetailFragment
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public void onViewCreated(@NonNull final View rootView, final Bundle savedInstanceState) {
|
||||
super.onViewCreated(rootView, savedInstanceState);
|
||||
}
|
||||
|
||||
@Override // called from onViewCreated in {@link BaseFragment#onViewCreated}
|
||||
protected void initViews(final View rootView, final Bundle savedInstanceState) {
|
||||
super.initViews(rootView, savedInstanceState);
|
||||
binding = FragmentVideoDetailBinding.bind(rootView);
|
||||
|
||||
pageAdapter = new TabAdapter(getChildFragmentManager());
|
||||
binding.viewPager.setAdapter(pageAdapter);
|
||||
@@ -597,6 +613,18 @@ public final class VideoDetailFragment
|
||||
|
||||
binding.detailThumbnailRootLayout.requestFocus();
|
||||
|
||||
binding.detailControlsPlayWithKodi.setVisibility(
|
||||
KoreUtils.shouldShowPlayWithKodi(requireContext(), serviceId)
|
||||
? View.VISIBLE
|
||||
: View.GONE
|
||||
);
|
||||
binding.detailControlsCrashThePlayer.setVisibility(
|
||||
DEBUG && PreferenceManager.getDefaultSharedPreferences(getContext())
|
||||
.getBoolean(getString(R.string.show_crash_the_player_key), false)
|
||||
? View.VISIBLE
|
||||
: View.GONE
|
||||
);
|
||||
|
||||
if (DeviceUtils.isTv(getContext())) {
|
||||
// remove ripple effects from detail controls
|
||||
final int transparent = ContextCompat.getColor(requireContext(),
|
||||
@@ -631,8 +659,14 @@ public final class VideoDetailFragment
|
||||
binding.detailControlsShare.setOnClickListener(this);
|
||||
binding.detailControlsOpenInBrowser.setOnClickListener(this);
|
||||
binding.detailControlsPlayWithKodi.setOnClickListener(this);
|
||||
binding.detailControlsPlayWithKodi.setVisibility(KoreUtil.shouldShowPlayWithKodi(
|
||||
requireContext(), serviceId) ? View.VISIBLE : View.GONE);
|
||||
if (DEBUG) {
|
||||
binding.detailControlsCrashThePlayer.setOnClickListener(
|
||||
v -> VideoDetailPlayerCrasher.onCrashThePlayer(
|
||||
this.getContext(),
|
||||
this.player,
|
||||
getLayoutInflater())
|
||||
);
|
||||
}
|
||||
|
||||
binding.overlayThumbnail.setOnClickListener(this);
|
||||
binding.overlayThumbnail.setOnLongClickListener(this);
|
||||
@@ -655,10 +689,10 @@ public final class VideoDetailFragment
|
||||
});
|
||||
|
||||
setupBottomPlayer();
|
||||
if (!PlayerHolder.bound) {
|
||||
if (!playerHolder.isBound()) {
|
||||
setHeightThumbnail();
|
||||
} else {
|
||||
PlayerHolder.startService(App.getApp(), false, this);
|
||||
playerHolder.startService(false, this);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -680,33 +714,24 @@ public final class VideoDetailFragment
|
||||
}
|
||||
|
||||
private void initThumbnailViews(@NonNull final StreamInfo info) {
|
||||
binding.detailThumbnailImageView.setImageResource(R.drawable.dummy_thumbnail_dark);
|
||||
PicassoHelper.loadThumbnail(info.getThumbnailUrl()).tag(PICASSO_VIDEO_DETAILS_TAG)
|
||||
.into(binding.detailThumbnailImageView, new Callback() {
|
||||
@Override
|
||||
public void onSuccess() {
|
||||
// nothing to do, the image was loaded correctly into the thumbnail
|
||||
}
|
||||
|
||||
if (!isEmpty(info.getThumbnailUrl())) {
|
||||
final ImageLoadingListener onFailListener = new SimpleImageLoadingListener() {
|
||||
@Override
|
||||
public void onLoadingFailed(final String imageUri, final View view,
|
||||
final FailReason failReason) {
|
||||
showSnackBarError(new ErrorInfo(failReason.getCause(), UserAction.LOAD_IMAGE,
|
||||
imageUri, info));
|
||||
}
|
||||
};
|
||||
@Override
|
||||
public void onError(final Exception e) {
|
||||
showSnackBarError(new ErrorInfo(e, UserAction.LOAD_IMAGE,
|
||||
info.getThumbnailUrl(), info));
|
||||
}
|
||||
});
|
||||
|
||||
IMAGE_LOADER.displayImage(info.getThumbnailUrl(), binding.detailThumbnailImageView,
|
||||
ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS, onFailListener);
|
||||
}
|
||||
|
||||
if (!isEmpty(info.getSubChannelAvatarUrl())) {
|
||||
IMAGE_LOADER.displayImage(info.getSubChannelAvatarUrl(),
|
||||
binding.detailSubChannelThumbnailView,
|
||||
ImageDisplayConstants.DISPLAY_AVATAR_OPTIONS);
|
||||
}
|
||||
|
||||
if (!isEmpty(info.getUploaderAvatarUrl())) {
|
||||
IMAGE_LOADER.displayImage(info.getUploaderAvatarUrl(),
|
||||
binding.detailUploaderThumbnailView,
|
||||
ImageDisplayConstants.DISPLAY_AVATAR_OPTIONS);
|
||||
}
|
||||
PicassoHelper.loadAvatar(info.getSubChannelAvatarUrl()).tag(PICASSO_VIDEO_DETAILS_TAG)
|
||||
.into(binding.detailSubChannelThumbnailView);
|
||||
PicassoHelper.loadAvatar(info.getUploaderAvatarUrl()).tag(PICASSO_VIDEO_DETAILS_TAG)
|
||||
.into(binding.detailUploaderThumbnailView);
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
@@ -721,7 +746,7 @@ public final class VideoDetailFragment
|
||||
|
||||
@Override
|
||||
public boolean onKeyDown(final int keyCode) {
|
||||
return player != null && player.onKeyDown(keyCode);
|
||||
return isPlayerAvailable() && player.onKeyDown(keyCode);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -731,7 +756,7 @@ public final class VideoDetailFragment
|
||||
}
|
||||
|
||||
// If we are in fullscreen mode just exit from it via first back press
|
||||
if (player != null && player.isFullscreen()) {
|
||||
if (isPlayerAvailable() && player.isFullscreen()) {
|
||||
if (!DeviceUtils.isTablet(activity)) {
|
||||
player.pause();
|
||||
}
|
||||
@@ -741,31 +766,30 @@ public final class VideoDetailFragment
|
||||
}
|
||||
|
||||
// If we have something in history of played items we replay it here
|
||||
if (player != null
|
||||
if (isPlayerAvailable()
|
||||
&& player.getPlayQueue() != null
|
||||
&& player.videoPlayerSelected()
|
||||
&& player.getPlayQueue().previous()) {
|
||||
return true;
|
||||
return true; // no code here, as previous() was used in the if
|
||||
}
|
||||
|
||||
// That means that we are on the start of the stack,
|
||||
// return false to let the MainActivity handle the onBack
|
||||
if (stack.size() <= 1) {
|
||||
restoreDefaultOrientation();
|
||||
|
||||
return false;
|
||||
return false; // let MainActivity handle the onBack (e.g. to minimize the mini player)
|
||||
}
|
||||
|
||||
// Remove top
|
||||
stack.pop();
|
||||
// Get stack item from the new top
|
||||
assert stack.peek() != null;
|
||||
setupFromHistoryItem(stack.peek());
|
||||
setupFromHistoryItem(Objects.requireNonNull(stack.peek()));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void setupFromHistoryItem(final StackItem item) {
|
||||
setAutoPlay(false);
|
||||
hideMainPlayer();
|
||||
hideMainPlayerOnLoadingNewStream();
|
||||
|
||||
setInitialData(item.getServiceId(), item.getUrl(),
|
||||
item.getTitle() == null ? "" : item.getTitle(), item.getPlayQueue());
|
||||
@@ -778,7 +802,7 @@ public final class VideoDetailFragment
|
||||
|
||||
final PlayQueueItem playQueueItem = item.getPlayQueue().getItem();
|
||||
// Update title, url, uploader from the last item in the stack (it's current now)
|
||||
final boolean isPlayerStopped = player == null || player.isStopped();
|
||||
final boolean isPlayerStopped = !isPlayerAvailable() || player.isStopped();
|
||||
if (playQueueItem != null && isPlayerStopped) {
|
||||
updateOverlayData(playQueueItem.getTitle(),
|
||||
playQueueItem.getUploader(), playQueueItem.getThumbnailUrl());
|
||||
@@ -806,8 +830,8 @@ public final class VideoDetailFragment
|
||||
@Nullable final String newUrl,
|
||||
@NonNull final String newTitle,
|
||||
@Nullable final PlayQueue newQueue) {
|
||||
if (player != null && newQueue != null && playQueue != null
|
||||
&& !Objects.equals(newQueue.getItem(), playQueue.getItem())) {
|
||||
if (isPlayerAvailable() && newQueue != null && playQueue != null
|
||||
&& playQueue.getItem() != null && !playQueue.getItem().getUrl().equals(newUrl)) {
|
||||
// Preloading can be disabled since playback is surely being replaced.
|
||||
player.disablePreloadingOfCurrentTrack();
|
||||
}
|
||||
@@ -885,7 +909,7 @@ public final class VideoDetailFragment
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(result -> {
|
||||
isLoading.set(false);
|
||||
hideMainPlayer();
|
||||
hideMainPlayerOnLoadingNewStream();
|
||||
if (result.getAgeLimit() != NO_AGE_LIMIT && !prefs.getBoolean(
|
||||
getString(R.string.show_age_restricted_content), false)) {
|
||||
hideAgeRestrictedContent();
|
||||
@@ -900,8 +924,9 @@ public final class VideoDetailFragment
|
||||
stack.push(new StackItem(serviceId, url, title, playQueue));
|
||||
}
|
||||
}
|
||||
|
||||
if (isAutoplayEnabled()) {
|
||||
openVideoPlayer();
|
||||
openVideoPlayerAutoFullscreen();
|
||||
}
|
||||
}
|
||||
}, throwable -> showError(new ErrorInfo(throwable, UserAction.REQUESTED_STREAM,
|
||||
@@ -982,7 +1007,7 @@ public final class VideoDetailFragment
|
||||
.replace(R.id.relatedItemsLayout, RelatedItemsFragment.getInstance(info))
|
||||
.commitAllowingStateLoss();
|
||||
binding.relatedItemsLayout.setVisibility(
|
||||
player != null && player.isFullscreen() ? View.GONE : View.VISIBLE);
|
||||
isPlayerAvailable() && player.isFullscreen() ? View.GONE : View.VISIBLE);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1059,6 +1084,14 @@ public final class VideoDetailFragment
|
||||
// Play Utils
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
private void toggleFullscreenIfInFullscreenMode() {
|
||||
// If a user watched video inside fullscreen mode and than chose another player
|
||||
// return to non-fullscreen mode
|
||||
if (isPlayerAvailable() && player.isFullscreen()) {
|
||||
player.toggleFullscreen();
|
||||
}
|
||||
}
|
||||
|
||||
private void openBackgroundPlayer(final boolean append) {
|
||||
final AudioStream audioStream = currentInfo.getAudioStreams()
|
||||
.get(ListHelper.getDefaultAudioFormat(activity, currentInfo.getAudioStreams()));
|
||||
@@ -1067,10 +1100,11 @@ public final class VideoDetailFragment
|
||||
.getDefaultSharedPreferences(activity)
|
||||
.getBoolean(activity.getString(R.string.use_external_audio_player_key), false);
|
||||
|
||||
// If a user watched video inside fullscreen mode and than chose another player
|
||||
// return to non-fullscreen mode
|
||||
if (player != null && player.isFullscreen()) {
|
||||
player.toggleFullscreen();
|
||||
toggleFullscreenIfInFullscreenMode();
|
||||
|
||||
if (isPlayerAvailable()) {
|
||||
// FIXME Workaround #7427
|
||||
player.setRecovery();
|
||||
}
|
||||
|
||||
if (!useExternalAudioPlayer) {
|
||||
@@ -1087,26 +1121,47 @@ public final class VideoDetailFragment
|
||||
}
|
||||
|
||||
// See UI changes while remote playQueue changes
|
||||
if (player == null) {
|
||||
PlayerHolder.startService(App.getApp(), false, this);
|
||||
if (!isPlayerAvailable()) {
|
||||
playerHolder.startService(false, this);
|
||||
} else {
|
||||
// FIXME Workaround #7427
|
||||
player.setRecovery();
|
||||
}
|
||||
|
||||
// If a user watched video inside fullscreen mode and than chose another player
|
||||
// return to non-fullscreen mode
|
||||
if (player != null && player.isFullscreen()) {
|
||||
player.toggleFullscreen();
|
||||
}
|
||||
toggleFullscreenIfInFullscreenMode();
|
||||
|
||||
final PlayQueue queue = setupPlayQueueForIntent(append);
|
||||
if (append) {
|
||||
NavigationHelper.enqueueOnPopupPlayer(activity, queue, false);
|
||||
if (append) { //resumePlayback: false
|
||||
NavigationHelper.enqueueOnPlayer(activity, queue, PlayerType.POPUP);
|
||||
} else {
|
||||
replaceQueueIfUserConfirms(() -> NavigationHelper
|
||||
.playOnPopupPlayer(activity, queue, true));
|
||||
}
|
||||
}
|
||||
|
||||
public void openVideoPlayer() {
|
||||
/**
|
||||
* Opens the video player, in fullscreen if needed. In order to open fullscreen, the activity
|
||||
* is toggled to landscape orientation (which will then cause fullscreen mode).
|
||||
*
|
||||
* @param directlyFullscreenIfApplicable whether to open fullscreen if we are not already
|
||||
* in landscape and screen orientation is locked
|
||||
*/
|
||||
public void openVideoPlayer(final boolean directlyFullscreenIfApplicable) {
|
||||
if (directlyFullscreenIfApplicable
|
||||
&& !DeviceUtils.isLandscape(requireContext())
|
||||
&& PlayerHelper.globalScreenOrientationLocked(requireContext())) {
|
||||
// Make sure the bottom sheet turns out expanded. When this code kicks in the bottom
|
||||
// sheet could not have fully expanded yet, and thus be in the STATE_SETTLING state.
|
||||
// When the activity is rotated, and its state is saved and then restored, the bottom
|
||||
// sheet would forget what it was doing, since even if STATE_SETTLING is restored, it
|
||||
// doesn't tell which state it was settling to, and thus the bottom sheet settles to
|
||||
// STATE_COLLAPSED. This can be solved by manually setting the state that will be
|
||||
// restored (i.e. bottomSheetState) to STATE_EXPANDED.
|
||||
bottomSheetState = BottomSheetBehavior.STATE_EXPANDED;
|
||||
// toggle landscape in order to open directly in fullscreen
|
||||
onScreenRotationButtonClicked();
|
||||
}
|
||||
|
||||
if (PreferenceManager.getDefaultSharedPreferences(activity)
|
||||
.getBoolean(this.getString(R.string.use_external_video_player_key), false)) {
|
||||
showExternalPlaybackDialog();
|
||||
@@ -1115,15 +1170,27 @@ public final class VideoDetailFragment
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If the option to start directly fullscreen is enabled, calls
|
||||
* {@link #openVideoPlayer(boolean)} with {@code directlyFullscreenIfApplicable = true}, so that
|
||||
* if the user is not already in landscape and he has screen orientation locked the activity
|
||||
* rotates and fullscreen starts. Otherwise, if the option to start directly fullscreen is
|
||||
* disabled, calls {@link #openVideoPlayer(boolean)} with {@code directlyFullscreenIfApplicable
|
||||
* = false}, hence preventing it from going directly fullscreen.
|
||||
*/
|
||||
public void openVideoPlayerAutoFullscreen() {
|
||||
openVideoPlayer(PlayerHelper.isStartMainPlayerFullscreenEnabled(requireContext()));
|
||||
}
|
||||
|
||||
private void openNormalBackgroundPlayer(final boolean append) {
|
||||
// See UI changes while remote playQueue changes
|
||||
if (player == null) {
|
||||
PlayerHolder.startService(App.getApp(), false, this);
|
||||
if (!isPlayerAvailable()) {
|
||||
playerHolder.startService(false, this);
|
||||
}
|
||||
|
||||
final PlayQueue queue = setupPlayQueueForIntent(append);
|
||||
if (append) {
|
||||
NavigationHelper.enqueueOnBackgroundPlayer(activity, queue, false);
|
||||
NavigationHelper.enqueueOnPlayer(activity, queue, PlayerType.AUDIO);
|
||||
} else {
|
||||
replaceQueueIfUserConfirms(() -> NavigationHelper
|
||||
.playOnBackgroundPlayer(activity, queue, true));
|
||||
@@ -1131,8 +1198,8 @@ public final class VideoDetailFragment
|
||||
}
|
||||
|
||||
private void openMainPlayer() {
|
||||
if (playerService == null) {
|
||||
PlayerHolder.startService(App.getApp(), autoPlayEnabled, this);
|
||||
if (!isPlayerServiceAvailable()) {
|
||||
playerHolder.startService(autoPlayEnabled, this);
|
||||
return;
|
||||
}
|
||||
if (currentInfo == null) {
|
||||
@@ -1148,21 +1215,32 @@ public final class VideoDetailFragment
|
||||
}
|
||||
addVideoPlayerView();
|
||||
|
||||
final Intent playerIntent = NavigationHelper
|
||||
.getPlayerIntent(requireContext(), MainPlayer.class, queue, true, autoPlayEnabled);
|
||||
activity.startService(playerIntent);
|
||||
final Intent playerIntent = NavigationHelper.getPlayerIntent(requireContext(),
|
||||
MainPlayer.class, queue, true, autoPlayEnabled);
|
||||
ContextCompat.startForegroundService(activity, playerIntent);
|
||||
}
|
||||
|
||||
private void hideMainPlayer() {
|
||||
if (playerService == null
|
||||
/**
|
||||
* When the video detail fragment is already showing details for a video and the user opens a
|
||||
* new one, the video detail fragment changes all of its old data to the new stream, so if there
|
||||
* is a video player currently open it should be hidden. This method does exactly that. If
|
||||
* autoplay is enabled, the underlying player is not stopped completely, since it is going to
|
||||
* be reused in a few milliseconds and the flickering would be annoying.
|
||||
*/
|
||||
private void hideMainPlayerOnLoadingNewStream() {
|
||||
if (!isPlayerServiceAvailable()
|
||||
|| playerService.getView() == null
|
||||
|| !player.videoPlayerSelected()) {
|
||||
return;
|
||||
}
|
||||
|
||||
removeVideoPlayerView();
|
||||
playerService.stop(isAutoplayEnabled());
|
||||
playerService.getView().setVisibility(View.GONE);
|
||||
if (isAutoplayEnabled()) {
|
||||
playerService.stopForImmediateReusing();
|
||||
playerService.getView().setVisibility(View.GONE);
|
||||
} else {
|
||||
playerHolder.stopService();
|
||||
}
|
||||
}
|
||||
|
||||
private PlayQueue setupPlayQueueForIntent(final boolean append) {
|
||||
@@ -1211,13 +1289,13 @@ public final class VideoDetailFragment
|
||||
private boolean isAutoplayEnabled() {
|
||||
return autoPlayEnabled
|
||||
&& !isExternalPlayerEnabled()
|
||||
&& (player == null || player.videoPlayerSelected())
|
||||
&& (!isPlayerAvailable() || player.videoPlayerSelected())
|
||||
&& bottomSheetState != BottomSheetBehavior.STATE_HIDDEN
|
||||
&& PlayerHelper.isAutoplayAllowedByUser(requireContext());
|
||||
}
|
||||
|
||||
private void addVideoPlayerView() {
|
||||
if (player == null || getView() == null) {
|
||||
if (!isPlayerAvailable() || getView() == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1255,7 +1333,7 @@ public final class VideoDetailFragment
|
||||
final DisplayMetrics metrics = getResources().getDisplayMetrics();
|
||||
|
||||
if (getView() != null) {
|
||||
final int height = (isInMultiWindow()
|
||||
final int height = (DeviceUtils.isInMultiWindow(activity)
|
||||
? requireView()
|
||||
: activity.getWindow().getDecorView()).getHeight();
|
||||
setHeightThumbnail(height, metrics);
|
||||
@@ -1277,8 +1355,8 @@ public final class VideoDetailFragment
|
||||
final boolean isPortrait = metrics.heightPixels > metrics.widthPixels;
|
||||
requireView().getViewTreeObserver().removeOnPreDrawListener(preDrawListener);
|
||||
|
||||
if (player != null && player.isFullscreen()) {
|
||||
final int height = (isInMultiWindow()
|
||||
if (isPlayerAvailable() && player.isFullscreen()) {
|
||||
final int height = (DeviceUtils.isInMultiWindow(activity)
|
||||
? requireView()
|
||||
: activity.getWindow().getDecorView()).getHeight();
|
||||
// Height is zero when the view is not yet displayed like after orientation change
|
||||
@@ -1300,7 +1378,7 @@ public final class VideoDetailFragment
|
||||
new FrameLayout.LayoutParams(
|
||||
RelativeLayout.LayoutParams.MATCH_PARENT, newHeight));
|
||||
binding.detailThumbnailImageView.setMinimumHeight(newHeight);
|
||||
if (player != null) {
|
||||
if (isPlayerAvailable()) {
|
||||
final int maxHeight = (int) (metrics.heightPixels * MAX_PLAYER_HEIGHT);
|
||||
player.getSurfaceView()
|
||||
.setHeights(newHeight, player.isFullscreen() ? newHeight : maxHeight);
|
||||
@@ -1368,9 +1446,9 @@ public final class VideoDetailFragment
|
||||
bottomSheetBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
|
||||
}
|
||||
// Rebound to the service if it was closed via notification or mini player
|
||||
if (!PlayerHolder.bound) {
|
||||
PlayerHolder.startService(
|
||||
App.getApp(), false, VideoDetailFragment.this);
|
||||
if (!playerHolder.isBound()) {
|
||||
playerHolder.startService(
|
||||
false, VideoDetailFragment.this);
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -1389,18 +1467,15 @@ public final class VideoDetailFragment
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
private void restoreDefaultOrientation() {
|
||||
if (player == null || !player.videoPlayerSelected() || activity == null) {
|
||||
return;
|
||||
if (isPlayerAvailable() && player.videoPlayerSelected()) {
|
||||
toggleFullscreenIfInFullscreenMode();
|
||||
}
|
||||
|
||||
if (player != null && player.isFullscreen()) {
|
||||
player.toggleFullscreen();
|
||||
}
|
||||
// This will show systemUI and pause the player.
|
||||
// User can tap on Play button and video will be in fullscreen mode again
|
||||
// Note for tablet: trying to avoid orientation changes since it's not easy
|
||||
// to physically rotate the tablet every time
|
||||
if (!DeviceUtils.isTablet(activity)) {
|
||||
if (activity != null && !DeviceUtils.isTablet(activity)) {
|
||||
activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED);
|
||||
}
|
||||
}
|
||||
@@ -1435,14 +1510,13 @@ public final class VideoDetailFragment
|
||||
if (binding.relatedItemsLayout != null) {
|
||||
if (showRelatedItems) {
|
||||
binding.relatedItemsLayout.setVisibility(
|
||||
player != null && player.isFullscreen() ? View.GONE : View.INVISIBLE);
|
||||
isPlayerAvailable() && player.isFullscreen() ? View.GONE : View.INVISIBLE);
|
||||
} else {
|
||||
binding.relatedItemsLayout.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
IMAGE_LOADER.cancelDisplayTask(binding.detailThumbnailImageView);
|
||||
IMAGE_LOADER.cancelDisplayTask(binding.detailSubChannelThumbnailView);
|
||||
PicassoHelper.cancelTag(PICASSO_VIDEO_DETAILS_TAG);
|
||||
binding.detailThumbnailImageView.setImageBitmap(null);
|
||||
binding.detailSubChannelThumbnailView.setImageBitmap(null);
|
||||
}
|
||||
@@ -1459,6 +1533,8 @@ public final class VideoDetailFragment
|
||||
animate(binding.detailThumbnailPlayButton, true, 200);
|
||||
binding.detailVideoTitleView.setText(title);
|
||||
|
||||
binding.detailSubChannelThumbnailView.setVisibility(View.GONE);
|
||||
|
||||
if (!isEmpty(info.getSubChannelName())) {
|
||||
displayBothUploaderAndSubChannel(info);
|
||||
} else if (!isEmpty(info.getUploaderName())) {
|
||||
@@ -1546,10 +1622,10 @@ public final class VideoDetailFragment
|
||||
.getDefaultResolutionIndex(activity, sortedVideoStreams);
|
||||
updateProgressInfo(info);
|
||||
initThumbnailViews(info);
|
||||
disposables.add(showMetaInfoInTextView(info.getMetaInfo(), binding.detailMetaInfoTextView,
|
||||
binding.detailMetaInfoSeparator));
|
||||
showMetaInfoInTextView(info.getMetaInfo(), binding.detailMetaInfoTextView,
|
||||
binding.detailMetaInfoSeparator, disposables);
|
||||
|
||||
if (player == null || player.isStopped()) {
|
||||
if (!isPlayerAvailable() || player.isStopped()) {
|
||||
updateOverlayData(info.getName(), info.getUploaderName(), info.getThumbnailUrl());
|
||||
}
|
||||
|
||||
@@ -1619,9 +1695,8 @@ public final class VideoDetailFragment
|
||||
|
||||
downloadDialog.show(activity.getSupportFragmentManager(), "downloadDialog");
|
||||
} catch (final Exception e) {
|
||||
ErrorActivity.reportErrorInSnackbar(activity,
|
||||
new ErrorInfo(e, UserAction.DOWNLOAD_OPEN_DIALOG, "Showing download dialog",
|
||||
currentInfo));
|
||||
ErrorUtil.showSnackbar(activity, new ErrorInfo(e, UserAction.DOWNLOAD_OPEN_DIALOG,
|
||||
"Showing download dialog", currentInfo));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1669,7 +1744,7 @@ public final class VideoDetailFragment
|
||||
.onErrorComplete()
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(state -> {
|
||||
showPlaybackProgress(state.getProgressTime(), info.getDuration() * 1000);
|
||||
showPlaybackProgress(state.getProgressMillis(), info.getDuration() * 1000);
|
||||
animate(binding.positionView, true, 500);
|
||||
animate(binding.detailPositionView, true, 500);
|
||||
}, e -> {
|
||||
@@ -1812,10 +1887,8 @@ public final class VideoDetailFragment
|
||||
if (error.type == ExoPlaybackException.TYPE_SOURCE
|
||||
|| error.type == ExoPlaybackException.TYPE_UNEXPECTED) {
|
||||
// Properly exit from fullscreen
|
||||
if (playerService != null && player.isFullscreen()) {
|
||||
player.toggleFullscreen();
|
||||
}
|
||||
hideMainPlayer();
|
||||
toggleFullscreenIfInFullscreenMode();
|
||||
hideMainPlayerOnLoadingNewStream();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1832,7 +1905,9 @@ public final class VideoDetailFragment
|
||||
@Override
|
||||
public void onFullscreenStateChanged(final boolean fullscreen) {
|
||||
setupBrightness();
|
||||
if (playerService.getView() == null || player.getParentActivity() == null) {
|
||||
if (!isPlayerAndPlayerServiceAvailable()
|
||||
|| playerService.getView() == null
|
||||
|| player.getParentActivity() == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1869,13 +1944,14 @@ public final class VideoDetailFragment
|
||||
// from landscape to portrait every time.
|
||||
// Just turn on fullscreen mode in landscape orientation
|
||||
// or portrait & unlocked global orientation
|
||||
final boolean isLandscape = DeviceUtils.isLandscape(requireContext());
|
||||
if (DeviceUtils.isTablet(activity)
|
||||
&& (!globalScreenOrientationLocked(activity) || isLandscape())) {
|
||||
&& (!globalScreenOrientationLocked(activity) || isLandscape)) {
|
||||
player.toggleFullscreen();
|
||||
return;
|
||||
}
|
||||
|
||||
final int newOrientation = isLandscape()
|
||||
final int newOrientation = isLandscape
|
||||
? ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
|
||||
: ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE;
|
||||
|
||||
@@ -1947,15 +2023,17 @@ public final class VideoDetailFragment
|
||||
| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
|
||||
| View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
|
||||
| View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY;
|
||||
|
||||
// In multiWindow mode status bar is not transparent for devices with cutout
|
||||
// if I include this flag. So without it is better in this case
|
||||
if (!isInMultiWindow()) {
|
||||
final boolean isInMultiWindow = DeviceUtils.isInMultiWindow(activity);
|
||||
if (!isInMultiWindow) {
|
||||
visibility |= View.SYSTEM_UI_FLAG_FULLSCREEN;
|
||||
}
|
||||
activity.getWindow().getDecorView().setSystemUiVisibility(visibility);
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP
|
||||
&& (isInMultiWindow() || (player != null && player.isFullscreen()))) {
|
||||
&& (isInMultiWindow || (isPlayerAvailable() && player.isFullscreen()))) {
|
||||
activity.getWindow().setStatusBarColor(Color.TRANSPARENT);
|
||||
activity.getWindow().setNavigationBarColor(Color.TRANSPARENT);
|
||||
}
|
||||
@@ -1964,7 +2042,7 @@ public final class VideoDetailFragment
|
||||
|
||||
// Listener implementation
|
||||
public void hideSystemUiIfNeeded() {
|
||||
if (player != null
|
||||
if (isPlayerAvailable()
|
||||
&& player.isFullscreen()
|
||||
&& bottomSheetBehavior.getState() == BottomSheetBehavior.STATE_EXPANDED) {
|
||||
hideSystemUi();
|
||||
@@ -1972,7 +2050,7 @@ public final class VideoDetailFragment
|
||||
}
|
||||
|
||||
private boolean playerIsNotStopped() {
|
||||
return player != null && !player.isStopped();
|
||||
return isPlayerAvailable() && !player.isStopped();
|
||||
}
|
||||
|
||||
private void restoreDefaultBrightness() {
|
||||
@@ -1993,7 +2071,7 @@ public final class VideoDetailFragment
|
||||
}
|
||||
|
||||
final WindowManager.LayoutParams lp = activity.getWindow().getAttributes();
|
||||
if (player == null
|
||||
if (!isPlayerAvailable()
|
||||
|| !player.videoPlayerSelected()
|
||||
|| !player.isFullscreen()
|
||||
|| bottomSheetState != BottomSheetBehavior.STATE_EXPANDED) {
|
||||
@@ -2027,15 +2105,6 @@ public final class VideoDetailFragment
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isLandscape() {
|
||||
return getResources().getDisplayMetrics().heightPixels < getResources()
|
||||
.getDisplayMetrics().widthPixels;
|
||||
}
|
||||
|
||||
private boolean isInMultiWindow() {
|
||||
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && activity.isInMultiWindowMode();
|
||||
}
|
||||
|
||||
/*
|
||||
* Means that the player fragment was swiped away via BottomSheetLayout
|
||||
* and is empty but ready for any new actions. See cleanUp()
|
||||
@@ -2059,7 +2128,7 @@ public final class VideoDetailFragment
|
||||
}
|
||||
|
||||
private void replaceQueueIfUserConfirms(final Runnable onAllow) {
|
||||
@Nullable final PlayQueue activeQueue = player == null ? null : player.getPlayQueue();
|
||||
@Nullable final PlayQueue activeQueue = isPlayerAvailable() ? player.getPlayQueue() : null;
|
||||
|
||||
// Player will have STATE_IDLE when a user pressed back button
|
||||
if (isClearingQueueConfirmationRequired(activity)
|
||||
@@ -2075,8 +2144,8 @@ public final class VideoDetailFragment
|
||||
private void showClearingQueueConfirmation(final Runnable onAllow) {
|
||||
new AlertDialog.Builder(activity)
|
||||
.setTitle(R.string.clear_queue_confirmation_description)
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.setPositiveButton(android.R.string.yes, (dialog, which) -> {
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.setPositiveButton(R.string.ok, (dialog, which) -> {
|
||||
onAllow.run();
|
||||
dialog.dismiss();
|
||||
}).show();
|
||||
@@ -2091,7 +2160,7 @@ public final class VideoDetailFragment
|
||||
resolutions[i] = sortedVideoStreams.get(i).getResolution();
|
||||
}
|
||||
final AlertDialog.Builder builder = new AlertDialog.Builder(activity)
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.setNeutralButton(R.string.open_in_browser, (dialog, i) ->
|
||||
ShareUtils.openUrlInBrowser(requireActivity(), url)
|
||||
);
|
||||
@@ -2115,7 +2184,7 @@ public final class VideoDetailFragment
|
||||
if (currentWorker != null) {
|
||||
currentWorker.dispose();
|
||||
}
|
||||
PlayerHolder.stopService(App.getApp());
|
||||
playerHolder.stopService();
|
||||
setInitialData(0, null, "", null);
|
||||
currentInfo = null;
|
||||
updateOverlayData(null, null, null);
|
||||
@@ -2142,12 +2211,20 @@ public final class VideoDetailFragment
|
||||
mainFragment.setDescendantFocusability(afterDescendants);
|
||||
toolbar.setDescendantFocusability(afterDescendants);
|
||||
((ViewGroup) requireView()).setDescendantFocusability(blockDescendants);
|
||||
mainFragment.requestFocus();
|
||||
// Only focus the mainFragment if the mainFragment (e.g. search-results)
|
||||
// or the toolbar (e.g. Textfield for search) don't have focus.
|
||||
// This was done to fix problems with the keyboard input, see also #7490
|
||||
if (!mainFragment.hasFocus() && !toolbar.hasFocus()) {
|
||||
mainFragment.requestFocus();
|
||||
}
|
||||
} else {
|
||||
mainFragment.setDescendantFocusability(blockDescendants);
|
||||
toolbar.setDescendantFocusability(blockDescendants);
|
||||
((ViewGroup) requireView()).setDescendantFocusability(afterDescendants);
|
||||
binding.detailThumbnailRootLayout.requestFocus();
|
||||
// Only focus the player if it not already has focus
|
||||
if (!binding.getRoot().hasFocus()) {
|
||||
binding.detailThumbnailRootLayout.requestFocus();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2218,8 +2295,8 @@ public final class VideoDetailFragment
|
||||
setOverlayElementsClickable(false);
|
||||
hideSystemUiIfNeeded();
|
||||
// Conditions when the player should be expanded to fullscreen
|
||||
if (isLandscape()
|
||||
&& player != null
|
||||
if (DeviceUtils.isLandscape(requireContext())
|
||||
&& isPlayerAvailable()
|
||||
&& player.isPlaying()
|
||||
&& !player.isFullscreen()
|
||||
&& !DeviceUtils.isTablet(activity)
|
||||
@@ -2236,17 +2313,17 @@ public final class VideoDetailFragment
|
||||
|
||||
// Re-enable clicks
|
||||
setOverlayElementsClickable(true);
|
||||
if (player != null) {
|
||||
if (isPlayerAvailable()) {
|
||||
player.closeItemsList();
|
||||
}
|
||||
setOverlayLook(binding.appBarLayout, behavior, 0);
|
||||
break;
|
||||
case BottomSheetBehavior.STATE_DRAGGING:
|
||||
case BottomSheetBehavior.STATE_SETTLING:
|
||||
if (player != null && player.isFullscreen()) {
|
||||
if (isPlayerAvailable() && player.isFullscreen()) {
|
||||
showSystemUi();
|
||||
}
|
||||
if (player != null && player.isControlsVisible()) {
|
||||
if (isPlayerAvailable() && player.isControlsVisible()) {
|
||||
player.hideControls(0, 0);
|
||||
}
|
||||
break;
|
||||
@@ -2273,10 +2350,8 @@ public final class VideoDetailFragment
|
||||
binding.overlayTitleTextView.setText(isEmpty(overlayTitle) ? "" : overlayTitle);
|
||||
binding.overlayChannelTextView.setText(isEmpty(uploader) ? "" : uploader);
|
||||
binding.overlayThumbnail.setImageResource(R.drawable.dummy_thumbnail_dark);
|
||||
if (!isEmpty(thumbnailUrl)) {
|
||||
IMAGE_LOADER.displayImage(thumbnailUrl, binding.overlayThumbnail,
|
||||
ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS, null);
|
||||
}
|
||||
PicassoHelper.loadThumbnail(thumbnailUrl).tag(PICASSO_VIDEO_DETAILS_TAG)
|
||||
.into(binding.overlayThumbnail);
|
||||
}
|
||||
|
||||
private void setOverlayPlayPauseImage(final boolean playerIsPlaying) {
|
||||
@@ -2310,4 +2385,17 @@ public final class VideoDetailFragment
|
||||
binding.overlayPlayPauseButton.setClickable(enable);
|
||||
binding.overlayCloseButton.setClickable(enable);
|
||||
}
|
||||
|
||||
// helpers to check the state of player and playerService
|
||||
boolean isPlayerAvailable() {
|
||||
return (player != null);
|
||||
}
|
||||
|
||||
boolean isPlayerServiceAvailable() {
|
||||
return (playerService != null);
|
||||
}
|
||||
|
||||
boolean isPlayerAndPlayerServiceAvailable() {
|
||||
return (player != null && playerService != null);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,159 @@
|
||||
package org.schabi.newpipe.fragments.detail;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.Log;
|
||||
import android.view.ContextThemeWrapper;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.RadioButton;
|
||||
import android.widget.RadioGroup;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.ExoPlaybackException;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.databinding.ListRadioIconItemBinding;
|
||||
import org.schabi.newpipe.databinding.SingleChoiceDialogViewBinding;
|
||||
import org.schabi.newpipe.player.Player;
|
||||
import org.schabi.newpipe.util.ThemeHelper;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
/**
|
||||
* Outsourced logic for crashing the player in the {@link VideoDetailFragment}.
|
||||
*/
|
||||
public final class VideoDetailPlayerCrasher {
|
||||
|
||||
// This has to be <= 23 chars on devices running Android 7 or lower (API <= 25)
|
||||
// or it fails with an IllegalArgumentException
|
||||
// https://stackoverflow.com/a/54744028
|
||||
private static final String TAG = "VideoDetPlayerCrasher";
|
||||
|
||||
private static final Map<String, Supplier<ExoPlaybackException>> AVAILABLE_EXCEPTION_TYPES =
|
||||
getExceptionTypes();
|
||||
|
||||
private VideoDetailPlayerCrasher() {
|
||||
// No impls
|
||||
}
|
||||
|
||||
private static Map<String, Supplier<ExoPlaybackException>> getExceptionTypes() {
|
||||
final String defaultMsg = "Dummy";
|
||||
final Map<String, Supplier<ExoPlaybackException>> exceptionTypes = new LinkedHashMap<>();
|
||||
exceptionTypes.put(
|
||||
"Source",
|
||||
() -> ExoPlaybackException.createForSource(
|
||||
new IOException(defaultMsg)
|
||||
)
|
||||
);
|
||||
exceptionTypes.put(
|
||||
"Renderer",
|
||||
() -> ExoPlaybackException.createForRenderer(
|
||||
new Exception(defaultMsg),
|
||||
"Dummy renderer",
|
||||
0,
|
||||
null,
|
||||
C.FORMAT_HANDLED
|
||||
)
|
||||
);
|
||||
exceptionTypes.put(
|
||||
"Unexpected",
|
||||
() -> ExoPlaybackException.createForUnexpected(
|
||||
new RuntimeException(defaultMsg)
|
||||
)
|
||||
);
|
||||
exceptionTypes.put(
|
||||
"Remote",
|
||||
() -> ExoPlaybackException.createForRemote(defaultMsg)
|
||||
);
|
||||
|
||||
return Collections.unmodifiableMap(exceptionTypes);
|
||||
}
|
||||
|
||||
private static Context getThemeWrapperContext(final Context context) {
|
||||
return new ContextThemeWrapper(
|
||||
context,
|
||||
ThemeHelper.isLightThemeSelected(context)
|
||||
? R.style.LightTheme
|
||||
: R.style.DarkTheme);
|
||||
}
|
||||
|
||||
public static void onCrashThePlayer(
|
||||
@NonNull final Context context,
|
||||
@Nullable final Player player,
|
||||
@NonNull final LayoutInflater layoutInflater
|
||||
) {
|
||||
if (player == null) {
|
||||
Log.d(TAG, "Player is not available");
|
||||
Toast.makeText(context, "Player is not available", Toast.LENGTH_SHORT)
|
||||
.show();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// -- Build the dialog/UI --
|
||||
|
||||
final Context themeWrapperContext = getThemeWrapperContext(context);
|
||||
|
||||
final LayoutInflater inflater = LayoutInflater.from(themeWrapperContext);
|
||||
final RadioGroup radioGroup = SingleChoiceDialogViewBinding.inflate(layoutInflater)
|
||||
.list;
|
||||
|
||||
final AlertDialog alertDialog = new AlertDialog.Builder(getThemeWrapperContext(context))
|
||||
.setTitle("Choose an exception")
|
||||
.setView(radioGroup)
|
||||
.setCancelable(true)
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.create();
|
||||
|
||||
for (final Map.Entry<String, Supplier<ExoPlaybackException>> entry
|
||||
: AVAILABLE_EXCEPTION_TYPES.entrySet()) {
|
||||
final RadioButton radioButton = ListRadioIconItemBinding.inflate(inflater).getRoot();
|
||||
radioButton.setText(entry.getKey());
|
||||
radioButton.setChecked(false);
|
||||
radioButton.setLayoutParams(
|
||||
new RadioGroup.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
)
|
||||
);
|
||||
radioButton.setOnClickListener(v -> {
|
||||
tryCrashPlayerWith(player, entry.getValue().get());
|
||||
if (alertDialog != null) {
|
||||
alertDialog.cancel();
|
||||
}
|
||||
});
|
||||
radioGroup.addView(radioButton);
|
||||
}
|
||||
|
||||
alertDialog.show();
|
||||
}
|
||||
|
||||
/**
|
||||
* Note that this method does not crash the underlying exoplayer directly (it's not possible).
|
||||
* It simply supplies a Exception to {@link Player#onPlayerError(ExoPlaybackException)}.
|
||||
* @param player
|
||||
* @param exception
|
||||
*/
|
||||
private static void tryCrashPlayerWith(
|
||||
@NonNull final Player player,
|
||||
@NonNull final ExoPlaybackException exception
|
||||
) {
|
||||
Log.d(TAG, "Crashing the player using player.onPlayerError(ex)");
|
||||
try {
|
||||
player.onPlayerError(exception);
|
||||
} catch (final Exception exPlayer) {
|
||||
Log.e(TAG,
|
||||
"Run into an exception while crashing the player:",
|
||||
exPlayer);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,11 +17,9 @@ import androidx.appcompat.app.ActionBar;
|
||||
import androidx.preference.PreferenceManager;
|
||||
import androidx.recyclerview.widget.GridLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import androidx.viewbinding.ViewBinding;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.databinding.PignateFooterBinding;
|
||||
import org.schabi.newpipe.error.ErrorActivity;
|
||||
import org.schabi.newpipe.error.ErrorUtil;
|
||||
import org.schabi.newpipe.extractor.InfoItem;
|
||||
import org.schabi.newpipe.extractor.channel.ChannelInfoItem;
|
||||
import org.schabi.newpipe.extractor.comments.CommentsInfoItem;
|
||||
@@ -33,7 +31,7 @@ import org.schabi.newpipe.fragments.OnScrollBelowItemsListener;
|
||||
import org.schabi.newpipe.info_list.InfoItemDialog;
|
||||
import org.schabi.newpipe.info_list.InfoListAdapter;
|
||||
import org.schabi.newpipe.player.helper.PlayerHolder;
|
||||
import org.schabi.newpipe.util.KoreUtil;
|
||||
import org.schabi.newpipe.util.external_communication.KoreUtils;
|
||||
import org.schabi.newpipe.util.NavigationHelper;
|
||||
import org.schabi.newpipe.util.OnClickGesture;
|
||||
import org.schabi.newpipe.util.StateSaver;
|
||||
@@ -44,6 +42,7 @@ import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Queue;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty;
|
||||
import static org.schabi.newpipe.ktx.ViewUtils.animate;
|
||||
@@ -79,11 +78,6 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I>
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDetach() {
|
||||
super.onDetach();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(final Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
@@ -143,7 +137,7 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I>
|
||||
final View focusedItem = itemsList.getFocusedChild();
|
||||
final RecyclerView.ViewHolder itemHolder =
|
||||
itemsList.findContainingViewHolder(focusedItem);
|
||||
return itemHolder.getAdapterPosition();
|
||||
return itemHolder.getBindingAdapterPosition();
|
||||
} catch (final NullPointerException e) {
|
||||
return -1;
|
||||
}
|
||||
@@ -220,14 +214,10 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I>
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Nullable
|
||||
protected ViewBinding getListHeader() {
|
||||
protected Supplier<View> getListHeaderSupplier() {
|
||||
return null;
|
||||
}
|
||||
|
||||
protected ViewBinding getListFooter() {
|
||||
return PignateFooterBinding.inflate(activity.getLayoutInflater(), itemsList, false);
|
||||
}
|
||||
|
||||
protected RecyclerView.LayoutManager getListLayoutManager() {
|
||||
return new SuperScrollLayoutManager(activity);
|
||||
}
|
||||
@@ -252,11 +242,10 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I>
|
||||
itemsList.setLayoutManager(useGrid ? getGridLayoutManager() : getListLayoutManager());
|
||||
|
||||
infoListAdapter.setUseGridVariant(useGrid);
|
||||
infoListAdapter.setFooter(getListFooter().getRoot());
|
||||
|
||||
final ViewBinding listHeader = getListHeader();
|
||||
if (listHeader != null) {
|
||||
infoListAdapter.setHeader(listHeader.getRoot());
|
||||
final Supplier<View> listHeaderSupplier = getListHeaderSupplier();
|
||||
if (listHeaderSupplier != null) {
|
||||
infoListAdapter.setHeaderSupplier(listHeaderSupplier);
|
||||
}
|
||||
|
||||
itemsList.setAdapter(infoListAdapter);
|
||||
@@ -271,7 +260,7 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I>
|
||||
@Override
|
||||
protected void initListeners() {
|
||||
super.initListeners();
|
||||
infoListAdapter.setOnStreamSelectedListener(new OnClickGesture<StreamInfoItem>() {
|
||||
infoListAdapter.setOnStreamSelectedListener(new OnClickGesture<>() {
|
||||
@Override
|
||||
public void selected(final StreamInfoItem selectedItem) {
|
||||
onStreamSelected(selectedItem);
|
||||
@@ -293,7 +282,7 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I>
|
||||
selectedItem.getUrl(),
|
||||
selectedItem.getName());
|
||||
} catch (final Exception e) {
|
||||
ErrorActivity.reportUiErrorInSnackbar(
|
||||
ErrorUtil.showUiErrorSnackbar(
|
||||
BaseListFragment.this, "Opening channel fragment", e);
|
||||
}
|
||||
}
|
||||
@@ -309,28 +298,104 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I>
|
||||
selectedItem.getUrl(),
|
||||
selectedItem.getName());
|
||||
} catch (final Exception e) {
|
||||
ErrorActivity.reportUiErrorInSnackbar(BaseListFragment.this,
|
||||
ErrorUtil.showUiErrorSnackbar(BaseListFragment.this,
|
||||
"Opening playlist fragment", e);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
infoListAdapter.setOnCommentsSelectedListener(new OnClickGesture<CommentsInfoItem>() {
|
||||
infoListAdapter.setOnCommentsSelectedListener(new OnClickGesture<>() {
|
||||
@Override
|
||||
public void selected(final CommentsInfoItem selectedItem) {
|
||||
onItemSelected(selectedItem);
|
||||
}
|
||||
});
|
||||
|
||||
// Ensure that there is always a scroll listener (e.g. when rotating the device)
|
||||
useNormalItemListScrollListener();
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all listeners and adds the normal scroll listener to the {@link #itemsList}.
|
||||
*/
|
||||
protected void useNormalItemListScrollListener() {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "useNormalItemListScrollListener called");
|
||||
}
|
||||
itemsList.clearOnScrollListeners();
|
||||
itemsList.addOnScrollListener(new OnScrollBelowItemsListener() {
|
||||
itemsList.addOnScrollListener(new DefaultItemListOnScrolledDownListener());
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all listeners and adds the initial scroll listener to the {@link #itemsList}.
|
||||
* <br/>
|
||||
* Which tries to load more items when not enough are in the view (not scrollable)
|
||||
* and more are available.
|
||||
* <br/>
|
||||
* Note: This method only works because "This callback will also be called if visible
|
||||
* item range changes after a layout calculation. In that case, dx and dy will be 0."
|
||||
* - which might be unexpected because no actual scrolling occurs...
|
||||
* <br/>
|
||||
* This listener will be replaced by DefaultItemListOnScrolledDownListener when
|
||||
* <ul>
|
||||
* <li>the view was actually scrolled</li>
|
||||
* <li>the view is scrollable</li>
|
||||
* <li>no more items can be loaded</li>
|
||||
* </ul>
|
||||
*/
|
||||
protected void useInitialItemListLoadScrollListener() {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "useInitialItemListLoadScrollListener called");
|
||||
}
|
||||
itemsList.clearOnScrollListeners();
|
||||
itemsList.addOnScrollListener(new DefaultItemListOnScrolledDownListener() {
|
||||
@Override
|
||||
public void onScrolledDown(final RecyclerView recyclerView) {
|
||||
onScrollToBottom();
|
||||
public void onScrolled(final RecyclerView recyclerView, final int dx, final int dy) {
|
||||
super.onScrolled(recyclerView, dx, dy);
|
||||
|
||||
if (dy != 0) {
|
||||
log("Vertical scroll occurred");
|
||||
|
||||
useNormalItemListScrollListener();
|
||||
return;
|
||||
}
|
||||
if (isLoading.get()) {
|
||||
log("Still loading data -> Skipping");
|
||||
return;
|
||||
}
|
||||
if (!hasMoreItems()) {
|
||||
log("No more items to load");
|
||||
|
||||
useNormalItemListScrollListener();
|
||||
return;
|
||||
}
|
||||
if (itemsList.canScrollVertically(1)
|
||||
|| itemsList.canScrollVertically(-1)) {
|
||||
log("View is scrollable");
|
||||
|
||||
useNormalItemListScrollListener();
|
||||
return;
|
||||
}
|
||||
|
||||
log("Loading more data");
|
||||
loadMoreItems();
|
||||
}
|
||||
|
||||
private void log(final String msg) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "initItemListLoadScrollListener - " + msg);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
class DefaultItemListOnScrolledDownListener extends OnScrollBelowItemsListener {
|
||||
@Override
|
||||
public void onScrolledDown(final RecyclerView recyclerView) {
|
||||
onScrollToBottom();
|
||||
}
|
||||
}
|
||||
|
||||
private void onStreamSelected(final StreamInfoItem selectedItem) {
|
||||
onItemSelected(selectedItem);
|
||||
NavigationHelper.openVideoDetailFragment(requireContext(), getFM(),
|
||||
@@ -350,12 +415,16 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I>
|
||||
if (context == null || context.getResources() == null || activity == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final List<StreamDialogEntry> entries = new ArrayList<>();
|
||||
|
||||
if (PlayerHolder.getType() != null) {
|
||||
if (PlayerHolder.getInstance().isPlayQueueReady()) {
|
||||
entries.add(StreamDialogEntry.enqueue);
|
||||
|
||||
if (PlayerHolder.getInstance().getQueueSize() > 1) {
|
||||
entries.add(StreamDialogEntry.enqueue_next);
|
||||
}
|
||||
}
|
||||
|
||||
if (item.getStreamType() == StreamType.AUDIO_STREAM) {
|
||||
entries.addAll(Arrays.asList(
|
||||
StreamDialogEntry.start_here_on_background,
|
||||
@@ -371,9 +440,16 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I>
|
||||
));
|
||||
}
|
||||
entries.add(StreamDialogEntry.open_in_browser);
|
||||
if (KoreUtil.shouldShowPlayWithKodi(context, item.getServiceId())) {
|
||||
if (KoreUtils.shouldShowPlayWithKodi(context, item.getServiceId())) {
|
||||
entries.add(StreamDialogEntry.play_with_kodi);
|
||||
}
|
||||
|
||||
// show "mark as watched" only when watch history is enabled
|
||||
if (StreamDialogEntry.shouldAddMarkAsWatched(item.getStreamType(), context)) {
|
||||
entries.add(
|
||||
StreamDialogEntry.mark_as_watched
|
||||
);
|
||||
}
|
||||
if (!isNullOrEmpty(item.getUploaderUrl())) {
|
||||
entries.add(StreamDialogEntry.show_channel_details);
|
||||
}
|
||||
@@ -389,7 +465,8 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I>
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public void onCreateOptionsMenu(final Menu menu, final MenuInflater inflater) {
|
||||
public void onCreateOptionsMenu(@NonNull final Menu menu,
|
||||
@NonNull final MenuInflater inflater) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "onCreateOptionsMenu() called with: "
|
||||
+ "menu = [" + menu + "], inflater = [" + inflater + "]");
|
||||
@@ -406,6 +483,12 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I>
|
||||
// Load and handle
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
protected void startLoading(final boolean forceLoad) {
|
||||
useInitialItemListLoadScrollListener();
|
||||
super.startLoading(forceLoad);
|
||||
}
|
||||
|
||||
protected abstract void loadMoreItems();
|
||||
|
||||
protected abstract boolean hasMoreItems();
|
||||
|
||||
@@ -65,7 +65,7 @@ public abstract class BaseListInfoFragment<I extends ListInfo>
|
||||
super.onResume();
|
||||
// Check if it was loading when the fragment was stopped/paused,
|
||||
if (wasLoading.getAndSet(false)) {
|
||||
if (hasMoreItems() && infoListAdapter.getItemsList().size() > 0) {
|
||||
if (hasMoreItems() && !infoListAdapter.getItemsList().isEmpty()) {
|
||||
loadMoreItems();
|
||||
} else {
|
||||
doInitialLoadLogic();
|
||||
@@ -105,6 +105,7 @@ public abstract class BaseListInfoFragment<I extends ListInfo>
|
||||
// Load and handle
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
protected void doInitialLoadLogic() {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "doInitialLoadLogic() called");
|
||||
@@ -158,6 +159,7 @@ public abstract class BaseListInfoFragment<I extends ListInfo>
|
||||
*/
|
||||
protected abstract Single<ListExtractor.InfoItemsPage> loadMoreItemsLogic();
|
||||
|
||||
@Override
|
||||
protected void loadMoreItems() {
|
||||
isLoading.set(true);
|
||||
|
||||
@@ -171,9 +173,9 @@ public abstract class BaseListInfoFragment<I extends ListInfo>
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.doFinally(this::allowDownwardFocusScroll)
|
||||
.subscribe((@NonNull ListExtractor.InfoItemsPage InfoItemsPage) -> {
|
||||
.subscribe(infoItemsPage -> {
|
||||
isLoading.set(false);
|
||||
handleNextItems(InfoItemsPage);
|
||||
handleNextItems(infoItemsPage);
|
||||
}, (@NonNull Throwable throwable) ->
|
||||
dynamicallyShowErrorPanelOrSnackbar(new ErrorInfo(throwable,
|
||||
errorUserAction, "Loading more items: " + url, serviceId)));
|
||||
@@ -223,7 +225,7 @@ public abstract class BaseListInfoFragment<I extends ListInfo>
|
||||
setTitle(name);
|
||||
|
||||
if (infoListAdapter.getItemsList().isEmpty()) {
|
||||
if (result.getRelatedItems().size() > 0) {
|
||||
if (!result.getRelatedItems().isEmpty()) {
|
||||
infoListAdapter.addInfoItemList(result.getRelatedItems());
|
||||
showListFooter(hasMoreItems());
|
||||
} else {
|
||||
@@ -240,7 +242,7 @@ public abstract class BaseListInfoFragment<I extends ListInfo>
|
||||
final List<Throwable> errors = new ArrayList<>(result.getErrors());
|
||||
// handling ContentNotSupportedException not to show the error but an appropriate string
|
||||
// so that crashes won't be sent uselessly and the user will understand what happened
|
||||
errors.removeIf(throwable -> throwable instanceof ContentNotSupportedException);
|
||||
errors.removeIf(ContentNotSupportedException.class::isInstance);
|
||||
|
||||
if (!errors.isEmpty()) {
|
||||
dynamicallyShowErrorPanelOrSnackbar(new ErrorInfo(result.getErrors(),
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
package org.schabi.newpipe.fragments.list.channel;
|
||||
|
||||
import static org.schabi.newpipe.ktx.TextViewUtils.animateTextColor;
|
||||
import static org.schabi.newpipe.ktx.ViewUtils.animate;
|
||||
import static org.schabi.newpipe.ktx.ViewUtils.animateBackgroundColor;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
import android.text.TextUtils;
|
||||
@@ -17,7 +21,6 @@ import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.viewbinding.ViewBinding;
|
||||
|
||||
import com.jakewharton.rxbinding4.view.RxView;
|
||||
|
||||
@@ -26,10 +29,9 @@ import org.schabi.newpipe.database.subscription.SubscriptionEntity;
|
||||
import org.schabi.newpipe.databinding.ChannelHeaderBinding;
|
||||
import org.schabi.newpipe.databinding.FragmentChannelBinding;
|
||||
import org.schabi.newpipe.databinding.PlaylistControlBinding;
|
||||
import org.schabi.newpipe.error.ErrorActivity;
|
||||
import org.schabi.newpipe.error.ErrorInfo;
|
||||
import org.schabi.newpipe.error.ErrorUtil;
|
||||
import org.schabi.newpipe.error.UserAction;
|
||||
import org.schabi.newpipe.extractor.InfoItem;
|
||||
import org.schabi.newpipe.extractor.ListExtractor;
|
||||
import org.schabi.newpipe.extractor.channel.ChannelInfo;
|
||||
import org.schabi.newpipe.extractor.exceptions.ContentNotSupportedException;
|
||||
@@ -37,18 +39,20 @@ import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||
import org.schabi.newpipe.fragments.list.BaseListInfoFragment;
|
||||
import org.schabi.newpipe.ktx.AnimationType;
|
||||
import org.schabi.newpipe.local.subscription.SubscriptionManager;
|
||||
import org.schabi.newpipe.player.MainPlayer.PlayerType;
|
||||
import org.schabi.newpipe.player.playqueue.ChannelPlayQueue;
|
||||
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
||||
import org.schabi.newpipe.util.ExtractorHelper;
|
||||
import org.schabi.newpipe.util.ImageDisplayConstants;
|
||||
import org.schabi.newpipe.util.Localization;
|
||||
import org.schabi.newpipe.util.NavigationHelper;
|
||||
import org.schabi.newpipe.util.ShareUtils;
|
||||
import org.schabi.newpipe.util.PicassoHelper;
|
||||
import org.schabi.newpipe.util.ThemeHelper;
|
||||
import org.schabi.newpipe.util.external_communication.ShareUtils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.rxjava3.core.Observable;
|
||||
@@ -60,13 +64,12 @@ import io.reactivex.rxjava3.functions.Consumer;
|
||||
import io.reactivex.rxjava3.functions.Function;
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers;
|
||||
|
||||
import static org.schabi.newpipe.ktx.TextViewUtils.animateTextColor;
|
||||
import static org.schabi.newpipe.ktx.ViewUtils.animate;
|
||||
import static org.schabi.newpipe.ktx.ViewUtils.animateBackgroundColor;
|
||||
|
||||
public class ChannelFragment extends BaseListInfoFragment<ChannelInfo>
|
||||
implements View.OnClickListener {
|
||||
|
||||
private static final int BUTTON_DEBOUNCE_INTERVAL = 100;
|
||||
private static final String PICASSO_CHANNEL_TAG = "PICASSO_CHANNEL_TAG";
|
||||
|
||||
private final CompositeDisposable disposables = new CompositeDisposable();
|
||||
private Disposable subscribeButtonMonitor;
|
||||
|
||||
@@ -94,11 +97,9 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo>
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setUserVisibleHint(final boolean isVisibleToUser) {
|
||||
super.setUserVisibleHint(isVisibleToUser);
|
||||
if (activity != null
|
||||
&& useAsFrontPage
|
||||
&& isVisibleToUser) {
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
if (activity != null && useAsFrontPage) {
|
||||
setTitle(currentInfo != null ? currentInfo.getName() : name);
|
||||
}
|
||||
}
|
||||
@@ -143,12 +144,12 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo>
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
protected ViewBinding getListHeader() {
|
||||
protected Supplier<View> getListHeaderSupplier() {
|
||||
headerBinding = ChannelHeaderBinding
|
||||
.inflate(activity.getLayoutInflater(), itemsList, false);
|
||||
playlistControlBinding = headerBinding.playlistControl;
|
||||
|
||||
return headerBinding;
|
||||
return headerBinding::getRoot;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -164,7 +165,8 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo>
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public void onCreateOptionsMenu(final Menu menu, final MenuInflater inflater) {
|
||||
public void onCreateOptionsMenu(@NonNull final Menu menu,
|
||||
@NonNull final MenuInflater inflater) {
|
||||
super.onCreateOptionsMenu(menu, inflater);
|
||||
final ActionBar supportActionBar = activity.getSupportActionBar();
|
||||
if (useAsFrontPage && supportActionBar != null) {
|
||||
@@ -180,13 +182,6 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo>
|
||||
}
|
||||
}
|
||||
|
||||
private void openRssFeed() {
|
||||
final ChannelInfo info = currentInfo;
|
||||
if (info != null) {
|
||||
ShareUtils.openUrlInBrowser(requireContext(), info.getFeedUrl(), false);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(final MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
@@ -194,7 +189,10 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo>
|
||||
NavigationHelper.openSettings(requireContext());
|
||||
break;
|
||||
case R.id.menu_item_rss:
|
||||
openRssFeed();
|
||||
if (currentInfo != null) {
|
||||
ShareUtils.openUrlInBrowser(
|
||||
requireContext(), currentInfo.getFeedUrl(), false);
|
||||
}
|
||||
break;
|
||||
case R.id.menu_item_openInBrowser:
|
||||
if (currentInfo != null) {
|
||||
@@ -203,7 +201,8 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo>
|
||||
break;
|
||||
case R.id.menu_item_share:
|
||||
if (currentInfo != null) {
|
||||
ShareUtils.shareText(requireContext(), name, currentInfo.getOriginalUrl());
|
||||
ShareUtils.shareText(requireContext(), name, currentInfo.getOriginalUrl(),
|
||||
currentInfo.getAvatarUrl());
|
||||
}
|
||||
break;
|
||||
default:
|
||||
@@ -403,7 +402,7 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo>
|
||||
currentInfo.getParentChannelUrl(),
|
||||
currentInfo.getParentChannelName());
|
||||
} catch (final Exception e) {
|
||||
ErrorActivity.reportUiErrorInSnackbar(this, "Opening channel fragment", e);
|
||||
ErrorUtil.showUiErrorSnackbar(this, "Opening channel fragment", e);
|
||||
}
|
||||
} else if (DEBUG) {
|
||||
Log.i(TAG, "Can't open parent channel because we got no channel URL");
|
||||
@@ -419,10 +418,7 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo>
|
||||
@Override
|
||||
public void showLoading() {
|
||||
super.showLoading();
|
||||
|
||||
IMAGE_LOADER.cancelDisplayTask(headerBinding.channelBannerImage);
|
||||
IMAGE_LOADER.cancelDisplayTask(headerBinding.channelAvatarView);
|
||||
IMAGE_LOADER.cancelDisplayTask(headerBinding.subChannelAvatarView);
|
||||
PicassoHelper.cancelTag(PICASSO_CHANNEL_TAG);
|
||||
animate(headerBinding.channelSubscribeButton, false, 100);
|
||||
}
|
||||
|
||||
@@ -431,13 +427,12 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo>
|
||||
super.handleResult(result);
|
||||
|
||||
headerBinding.getRoot().setVisibility(View.VISIBLE);
|
||||
IMAGE_LOADER.displayImage(result.getBannerUrl(), headerBinding.channelBannerImage,
|
||||
ImageDisplayConstants.DISPLAY_BANNER_OPTIONS);
|
||||
IMAGE_LOADER.displayImage(result.getAvatarUrl(), headerBinding.channelAvatarView,
|
||||
ImageDisplayConstants.DISPLAY_AVATAR_OPTIONS);
|
||||
IMAGE_LOADER.displayImage(result.getParentChannelAvatarUrl(),
|
||||
headerBinding.subChannelAvatarView,
|
||||
ImageDisplayConstants.DISPLAY_AVATAR_OPTIONS);
|
||||
PicassoHelper.loadBanner(result.getBannerUrl()).tag(PICASSO_CHANNEL_TAG)
|
||||
.into(headerBinding.channelBannerImage);
|
||||
PicassoHelper.loadAvatar(result.getAvatarUrl()).tag(PICASSO_CHANNEL_TAG)
|
||||
.into(headerBinding.channelAvatarView);
|
||||
PicassoHelper.loadAvatar(result.getParentChannelAvatarUrl()).tag(PICASSO_CHANNEL_TAG)
|
||||
.into(headerBinding.subChannelAvatarView);
|
||||
|
||||
headerBinding.channelSubscriberView.setVisibility(View.VISIBLE);
|
||||
if (result.getSubscriberCount() >= 0) {
|
||||
@@ -494,12 +489,12 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo>
|
||||
.playOnBackgroundPlayer(activity, getPlayQueue(), false));
|
||||
|
||||
playlistControlBinding.playlistCtrlPlayPopupButton.setOnLongClickListener(view -> {
|
||||
NavigationHelper.enqueueOnPopupPlayer(activity, getPlayQueue(), true);
|
||||
NavigationHelper.enqueueOnPlayer(activity, getPlayQueue(), PlayerType.POPUP);
|
||||
return true;
|
||||
});
|
||||
|
||||
playlistControlBinding.playlistCtrlPlayBgButton.setOnLongClickListener(view -> {
|
||||
NavigationHelper.enqueueOnBackgroundPlayer(activity, getPlayQueue(), true);
|
||||
NavigationHelper.enqueueOnPlayer(activity, getPlayQueue(), PlayerType.AUDIO);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
@@ -516,12 +511,11 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo>
|
||||
}
|
||||
|
||||
private PlayQueue getPlayQueue(final int index) {
|
||||
final List<StreamInfoItem> streamItems = new ArrayList<>();
|
||||
for (final InfoItem i : infoListAdapter.getItemsList()) {
|
||||
if (i instanceof StreamInfoItem) {
|
||||
streamItems.add((StreamInfoItem) i);
|
||||
}
|
||||
}
|
||||
final List<StreamInfoItem> streamItems = infoListAdapter.getItemsList().stream()
|
||||
.filter(StreamInfoItem.class::isInstance)
|
||||
.map(StreamInfoItem.class::cast)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
return new ChannelPlayQueue(currentInfo.getServiceId(), currentInfo.getUrl(),
|
||||
currentInfo.getNextPage(), streamItems, index);
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ 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;
|
||||
@@ -24,6 +25,8 @@ import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
||||
public class CommentsFragment extends BaseListInfoFragment<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();
|
||||
@@ -35,6 +38,13 @@ public class CommentsFragment extends BaseListInfoFragment<CommentsInfo> {
|
||||
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
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
@@ -73,6 +83,12 @@ public class CommentsFragment extends BaseListInfoFragment<CommentsInfo> {
|
||||
@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();
|
||||
}
|
||||
@@ -85,7 +101,8 @@ public class CommentsFragment extends BaseListInfoFragment<CommentsInfo> {
|
||||
public void setTitle(final String title) { }
|
||||
|
||||
@Override
|
||||
public void onCreateOptionsMenu(final Menu menu, final MenuInflater inflater) { }
|
||||
public void onCreateOptionsMenu(@NonNull final Menu menu,
|
||||
@NonNull final MenuInflater inflater) { }
|
||||
|
||||
@Override
|
||||
protected boolean isGridLayout() {
|
||||
|
||||
@@ -99,9 +99,12 @@ public class KioskFragment extends BaseListInfoFragment<KioskInfo> {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setUserVisibleHint(final boolean isVisibleToUser) {
|
||||
super.setUserVisibleHint(isVisibleToUser);
|
||||
if (useAsFrontPage && isVisibleToUser && activity != null) {
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
if (!Localization.getPreferredContentCountry(requireContext()).equals(contentCountry)) {
|
||||
reloadContent();
|
||||
}
|
||||
if (useAsFrontPage && activity != null) {
|
||||
try {
|
||||
setTitle(kioskTranslatedName);
|
||||
} catch (final Exception e) {
|
||||
@@ -117,21 +120,13 @@ public class KioskFragment extends BaseListInfoFragment<KioskInfo> {
|
||||
return inflater.inflate(R.layout.fragment_kiosk, container, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
|
||||
if (!Localization.getPreferredContentCountry(requireContext()).equals(contentCountry)) {
|
||||
reloadContent();
|
||||
}
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Menu
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public void onCreateOptionsMenu(final Menu menu, final MenuInflater inflater) {
|
||||
public void onCreateOptionsMenu(@NonNull final Menu menu,
|
||||
@NonNull final MenuInflater inflater) {
|
||||
super.onCreateOptionsMenu(menu, inflater);
|
||||
final ActionBar supportActionBar = activity.getSupportActionBar();
|
||||
if (supportActionBar != null && useAsFrontPage) {
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
package org.schabi.newpipe.fragments.list.playlist;
|
||||
|
||||
import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty;
|
||||
import static org.schabi.newpipe.ktx.ViewUtils.animate;
|
||||
import static org.schabi.newpipe.ktx.ViewUtils.animateHideRecyclerViewAllowingScrolling;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
@@ -15,7 +19,6 @@ import android.view.ViewGroup;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.content.res.AppCompatResources;
|
||||
import androidx.viewbinding.ViewBinding;
|
||||
|
||||
import org.reactivestreams.Subscriber;
|
||||
import org.reactivestreams.Subscription;
|
||||
@@ -24,8 +27,8 @@ import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity;
|
||||
import org.schabi.newpipe.databinding.PlaylistControlBinding;
|
||||
import org.schabi.newpipe.databinding.PlaylistHeaderBinding;
|
||||
import org.schabi.newpipe.error.ErrorActivity;
|
||||
import org.schabi.newpipe.error.ErrorInfo;
|
||||
import org.schabi.newpipe.error.ErrorUtil;
|
||||
import org.schabi.newpipe.error.UserAction;
|
||||
import org.schabi.newpipe.extractor.InfoItem;
|
||||
import org.schabi.newpipe.extractor.ListExtractor;
|
||||
@@ -37,21 +40,23 @@ import org.schabi.newpipe.extractor.stream.StreamType;
|
||||
import org.schabi.newpipe.fragments.list.BaseListInfoFragment;
|
||||
import org.schabi.newpipe.info_list.InfoItemDialog;
|
||||
import org.schabi.newpipe.local.playlist.RemotePlaylistManager;
|
||||
import org.schabi.newpipe.player.MainPlayer.PlayerType;
|
||||
import org.schabi.newpipe.player.helper.PlayerHolder;
|
||||
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
||||
import org.schabi.newpipe.player.playqueue.PlaylistPlayQueue;
|
||||
import org.schabi.newpipe.util.ExtractorHelper;
|
||||
import org.schabi.newpipe.util.ImageDisplayConstants;
|
||||
import org.schabi.newpipe.util.KoreUtil;
|
||||
import org.schabi.newpipe.util.Localization;
|
||||
import org.schabi.newpipe.util.NavigationHelper;
|
||||
import org.schabi.newpipe.util.ShareUtils;
|
||||
import org.schabi.newpipe.util.PicassoHelper;
|
||||
import org.schabi.newpipe.util.StreamDialogEntry;
|
||||
import org.schabi.newpipe.util.external_communication.KoreUtils;
|
||||
import org.schabi.newpipe.util.external_communication.ShareUtils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.rxjava3.core.Flowable;
|
||||
@@ -59,17 +64,17 @@ import io.reactivex.rxjava3.core.Single;
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
||||
import io.reactivex.rxjava3.disposables.Disposable;
|
||||
|
||||
import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty;
|
||||
import static org.schabi.newpipe.ktx.ViewUtils.animate;
|
||||
import static org.schabi.newpipe.ktx.ViewUtils.animateHideRecyclerViewAllowingScrolling;
|
||||
|
||||
public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
|
||||
|
||||
private static final String PICASSO_PLAYLIST_TAG = "PICASSO_PLAYLIST_TAG";
|
||||
|
||||
private CompositeDisposable disposables;
|
||||
private Subscription bookmarkReactor;
|
||||
private AtomicBoolean isBookmarkButtonReady;
|
||||
|
||||
private RemotePlaylistManager remotePlaylistManager;
|
||||
private PlaylistRemoteEntity playlistEntity;
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Views
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
@@ -115,12 +120,12 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
protected ViewBinding getListHeader() {
|
||||
protected Supplier<View> getListHeaderSupplier() {
|
||||
headerBinding = PlaylistHeaderBinding
|
||||
.inflate(activity.getLayoutInflater(), itemsList, false);
|
||||
playlistControlBinding = headerBinding.playlistControl;
|
||||
|
||||
return headerBinding;
|
||||
return headerBinding::getRoot;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -144,9 +149,14 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
|
||||
|
||||
final ArrayList<StreamDialogEntry> entries = new ArrayList<>();
|
||||
|
||||
if (PlayerHolder.getType() != null) {
|
||||
if (PlayerHolder.getInstance().isPlayQueueReady()) {
|
||||
entries.add(StreamDialogEntry.enqueue);
|
||||
|
||||
if (PlayerHolder.getInstance().getQueueSize() > 1) {
|
||||
entries.add(StreamDialogEntry.enqueue_next);
|
||||
}
|
||||
}
|
||||
|
||||
if (item.getStreamType() == StreamType.AUDIO_STREAM) {
|
||||
entries.addAll(Arrays.asList(
|
||||
StreamDialogEntry.start_here_on_background,
|
||||
@@ -162,10 +172,16 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
|
||||
));
|
||||
}
|
||||
entries.add(StreamDialogEntry.open_in_browser);
|
||||
if (KoreUtil.shouldShowPlayWithKodi(context, item.getServiceId())) {
|
||||
if (KoreUtils.shouldShowPlayWithKodi(context, item.getServiceId())) {
|
||||
entries.add(StreamDialogEntry.play_with_kodi);
|
||||
}
|
||||
|
||||
// show "mark as watched" only when watch history is enabled
|
||||
if (StreamDialogEntry.shouldAddMarkAsWatched(item.getStreamType(), context)) {
|
||||
entries.add(
|
||||
StreamDialogEntry.mark_as_watched
|
||||
);
|
||||
}
|
||||
if (!isNullOrEmpty(item.getUploaderUrl())) {
|
||||
entries.add(StreamDialogEntry.show_channel_details);
|
||||
}
|
||||
@@ -181,7 +197,8 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreateOptionsMenu(final Menu menu, final MenuInflater inflater) {
|
||||
public void onCreateOptionsMenu(@NonNull final Menu menu,
|
||||
@NonNull final MenuInflater inflater) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "onCreateOptionsMenu() called with: "
|
||||
+ "menu = [" + menu + "], inflater = [" + inflater + "]");
|
||||
@@ -251,7 +268,10 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
|
||||
ShareUtils.openUrlInBrowser(requireContext(), url);
|
||||
break;
|
||||
case R.id.menu_item_share:
|
||||
ShareUtils.shareText(requireContext(), name, url);
|
||||
if (currentInfo != null) {
|
||||
ShareUtils.shareText(requireContext(), name, url,
|
||||
currentInfo.getThumbnailUrl());
|
||||
}
|
||||
break;
|
||||
case R.id.menu_item_bookmark:
|
||||
onBookmarkClicked();
|
||||
@@ -273,7 +293,7 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
|
||||
animate(headerBinding.getRoot(), false, 200);
|
||||
animateHideRecyclerViewAllowingScrolling(itemsList);
|
||||
|
||||
IMAGE_LOADER.cancelDisplayTask(headerBinding.uploaderAvatarView);
|
||||
PicassoHelper.cancelTag(PICASSO_PLAYLIST_TAG);
|
||||
animate(headerBinding.uploaderLayout, false, 200);
|
||||
}
|
||||
|
||||
@@ -293,7 +313,7 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
|
||||
NavigationHelper.openChannelFragment(getFM(), result.getServiceId(),
|
||||
result.getUploaderUrl(), result.getUploaderName());
|
||||
} catch (final Exception e) {
|
||||
ErrorActivity.reportUiErrorInSnackbar(this, "Opening channel fragment", e);
|
||||
ErrorUtil.showUiErrorSnackbar(this, "Opening channel fragment", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -316,8 +336,8 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
|
||||
R.drawable.ic_radio)
|
||||
);
|
||||
} else {
|
||||
IMAGE_LOADER.displayImage(avatarUrl, headerBinding.uploaderAvatarView,
|
||||
ImageDisplayConstants.DISPLAY_AVATAR_OPTIONS);
|
||||
PicassoHelper.loadAvatar(avatarUrl).tag(PICASSO_PLAYLIST_TAG)
|
||||
.into(headerBinding.uploaderAvatarView);
|
||||
}
|
||||
|
||||
headerBinding.playlistStreamCount.setText(Localization
|
||||
@@ -342,12 +362,12 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
|
||||
NavigationHelper.playOnBackgroundPlayer(activity, getPlayQueue(), false));
|
||||
|
||||
playlistControlBinding.playlistCtrlPlayPopupButton.setOnLongClickListener(view -> {
|
||||
NavigationHelper.enqueueOnPopupPlayer(activity, getPlayQueue(), true);
|
||||
NavigationHelper.enqueueOnPlayer(activity, getPlayQueue(), PlayerType.POPUP);
|
||||
return true;
|
||||
});
|
||||
|
||||
playlistControlBinding.playlistCtrlPlayBgButton.setOnLongClickListener(view -> {
|
||||
NavigationHelper.enqueueOnBackgroundPlayer(activity, getPlayQueue(), true);
|
||||
NavigationHelper.enqueueOnPlayer(activity, getPlayQueue(), PlayerType.AUDIO);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
@@ -393,7 +413,7 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
|
||||
}
|
||||
|
||||
private Subscriber<List<PlaylistRemoteEntity>> getPlaylistBookmarkSubscriber() {
|
||||
return new Subscriber<List<PlaylistRemoteEntity>>() {
|
||||
return new Subscriber<>() {
|
||||
@Override
|
||||
public void onSubscribe(final Subscription s) {
|
||||
if (bookmarkReactor != null) {
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
package org.schabi.newpipe.fragments.list.search;
|
||||
|
||||
import static androidx.recyclerview.widget.ItemTouchHelper.Callback.makeMovementFlags;
|
||||
import static org.schabi.newpipe.ktx.ViewUtils.animate;
|
||||
import static org.schabi.newpipe.util.ExtractorHelper.showMetaInfoInTextView;
|
||||
import static java.util.Arrays.asList;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
@@ -20,7 +25,6 @@ import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.animation.DecelerateInterpolator;
|
||||
import android.view.inputmethod.EditorInfo;
|
||||
import android.view.inputmethod.InputMethodManager;
|
||||
import android.widget.EditText;
|
||||
import android.widget.TextView;
|
||||
|
||||
@@ -29,17 +33,15 @@ import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.appcompat.widget.TooltipCompat;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.core.text.HtmlCompat;
|
||||
import androidx.preference.PreferenceManager;
|
||||
import androidx.recyclerview.widget.ItemTouchHelper;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.database.history.model.SearchHistoryEntry;
|
||||
import org.schabi.newpipe.databinding.FragmentSearchBinding;
|
||||
import org.schabi.newpipe.error.ErrorActivity;
|
||||
import org.schabi.newpipe.error.ErrorInfo;
|
||||
import org.schabi.newpipe.error.ErrorUtil;
|
||||
import org.schabi.newpipe.error.ReCaptchaActivity;
|
||||
import org.schabi.newpipe.error.UserAction;
|
||||
import org.schabi.newpipe.extractor.InfoItem;
|
||||
@@ -57,34 +59,33 @@ import org.schabi.newpipe.fragments.list.BaseListFragment;
|
||||
import org.schabi.newpipe.ktx.AnimationType;
|
||||
import org.schabi.newpipe.ktx.ExceptionUtils;
|
||||
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
||||
import org.schabi.newpipe.settings.NewPipeSettings;
|
||||
import org.schabi.newpipe.util.Constants;
|
||||
import org.schabi.newpipe.util.DeviceUtils;
|
||||
import org.schabi.newpipe.util.ExtractorHelper;
|
||||
import org.schabi.newpipe.util.KeyboardUtil;
|
||||
import org.schabi.newpipe.util.NavigationHelper;
|
||||
import org.schabi.newpipe.util.ServiceHelper;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Queue;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import icepick.State;
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.rxjava3.core.Flowable;
|
||||
import io.reactivex.rxjava3.core.Observable;
|
||||
import io.reactivex.rxjava3.core.Single;
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
||||
import io.reactivex.rxjava3.disposables.Disposable;
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers;
|
||||
import io.reactivex.rxjava3.subjects.PublishSubject;
|
||||
|
||||
import static androidx.recyclerview.widget.ItemTouchHelper.Callback.makeMovementFlags;
|
||||
import static java.util.Arrays.asList;
|
||||
import static org.schabi.newpipe.ktx.ViewUtils.animate;
|
||||
import static org.schabi.newpipe.util.ExtractorHelper.showMetaInfoInTextView;
|
||||
|
||||
public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.InfoItemsPage<?>>
|
||||
implements BackPressable {
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
@@ -143,7 +144,8 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
||||
@Nullable private Map<Integer, String> menuItemToFilterName = null;
|
||||
private StreamingService service;
|
||||
private Page nextPage;
|
||||
private boolean isSuggestionsEnabled = true;
|
||||
private boolean showLocalSuggestions = true;
|
||||
private boolean showRemoteSuggestions = true;
|
||||
|
||||
private Disposable searchDisposable;
|
||||
private Disposable suggestionDisposable;
|
||||
@@ -194,26 +196,14 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
||||
public void onAttach(@NonNull final Context context) {
|
||||
super.onAttach(context);
|
||||
|
||||
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(activity);
|
||||
showLocalSuggestions = NewPipeSettings.showLocalSearchSuggestions(activity, prefs);
|
||||
showRemoteSuggestions = NewPipeSettings.showRemoteSearchSuggestions(activity, prefs);
|
||||
|
||||
suggestionListAdapter = new SuggestionListAdapter(activity);
|
||||
final SharedPreferences preferences
|
||||
= PreferenceManager.getDefaultSharedPreferences(activity);
|
||||
final boolean isSearchHistoryEnabled = preferences
|
||||
.getBoolean(getString(R.string.enable_search_history_key), true);
|
||||
suggestionListAdapter.setShowSuggestionHistory(isSearchHistoryEnabled);
|
||||
|
||||
historyRecordManager = new HistoryRecordManager(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(final Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
final SharedPreferences preferences
|
||||
= PreferenceManager.getDefaultSharedPreferences(activity);
|
||||
isSuggestionsEnabled = preferences
|
||||
.getBoolean(getString(R.string.show_search_suggestions_key), true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(final LayoutInflater inflater, @Nullable final ViewGroup container,
|
||||
@Nullable final Bundle savedInstanceState) {
|
||||
@@ -222,11 +212,30 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
||||
|
||||
@Override
|
||||
public void onViewCreated(@NonNull final View rootView, final Bundle savedInstanceState) {
|
||||
searchBinding = FragmentSearchBinding.bind(rootView);
|
||||
super.onViewCreated(rootView, savedInstanceState);
|
||||
showSearchOnStart();
|
||||
initSearchListeners();
|
||||
}
|
||||
|
||||
private void updateService() {
|
||||
try {
|
||||
service = NewPipe.getService(serviceId);
|
||||
} catch (final Exception e) {
|
||||
ErrorUtil.showUiErrorSnackbar(this, "Getting service for id " + serviceId, e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStart() {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "onStart() called");
|
||||
}
|
||||
super.onStart();
|
||||
|
||||
updateService();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPause() {
|
||||
super.onPause();
|
||||
@@ -250,13 +259,6 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
||||
}
|
||||
super.onResume();
|
||||
|
||||
try {
|
||||
service = NewPipe.getService(serviceId);
|
||||
} catch (final Exception e) {
|
||||
ErrorActivity.reportUiErrorInSnackbar(this,
|
||||
"Getting service for id " + serviceId, e);
|
||||
}
|
||||
|
||||
if (suggestionDisposable == null || suggestionDisposable.isDisposed()) {
|
||||
initSuggestionObserver();
|
||||
}
|
||||
@@ -278,8 +280,9 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
||||
|
||||
handleSearchSuggestion();
|
||||
|
||||
disposables.add(showMetaInfoInTextView(metaInfo == null ? null : Arrays.asList(metaInfo),
|
||||
searchBinding.searchMetaInfoTextView, searchBinding.searchMetaInfoSeparator));
|
||||
showMetaInfoInTextView(metaInfo == null ? null : Arrays.asList(metaInfo),
|
||||
searchBinding.searchMetaInfoTextView, searchBinding.searchMetaInfoSeparator,
|
||||
disposables);
|
||||
|
||||
if (TextUtils.isEmpty(searchString) || wasSearchFocused) {
|
||||
showKeyboardSearch();
|
||||
@@ -335,7 +338,6 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
||||
@Override
|
||||
protected void initViews(final View rootView, final Bundle savedInstanceState) {
|
||||
super.initViews(rootView, savedInstanceState);
|
||||
searchBinding = FragmentSearchBinding.bind(rootView);
|
||||
|
||||
searchBinding.suggestionsList.setAdapter(suggestionListAdapter);
|
||||
new ItemTouchHelper(new ItemTouchHelper.Callback() {
|
||||
@@ -412,7 +414,8 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public void onCreateOptionsMenu(final Menu menu, final MenuInflater inflater) {
|
||||
public void onCreateOptionsMenu(@NonNull final Menu menu,
|
||||
@NonNull final MenuInflater inflater) {
|
||||
super.onCreateOptionsMenu(menu, inflater);
|
||||
|
||||
final ActionBar supportActionBar = activity.getSupportActionBar();
|
||||
@@ -426,6 +429,12 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
||||
int itemId = 0;
|
||||
boolean isFirstItem = true;
|
||||
final Context c = getContext();
|
||||
|
||||
if (service == null) {
|
||||
Log.w(TAG, "onCreateOptionsMenu() called with null service");
|
||||
updateService();
|
||||
}
|
||||
|
||||
for (final String filter : service.getSearchQHFactory().getAvailableContentFilter()) {
|
||||
if (filter.equals(YoutubeSearchQueryHandlerFactory.MUSIC_SONGS)) {
|
||||
final MenuItem musicItem = menu.add(2,
|
||||
@@ -534,7 +543,7 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "onClick() called with: v = [" + v + "]");
|
||||
}
|
||||
if (isSuggestionsEnabled && !isErrorPanelVisible()) {
|
||||
if ((showLocalSuggestions || showRemoteSuggestions) && !isErrorPanelVisible()) {
|
||||
showSuggestionsPanel();
|
||||
}
|
||||
if (DeviceUtils.isTv(getContext())) {
|
||||
@@ -547,7 +556,8 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
||||
Log.d(TAG, "onFocusChange() called with: "
|
||||
+ "v = [" + v + "], hasFocus = [" + hasFocus + "]");
|
||||
}
|
||||
if (isSuggestionsEnabled && hasFocus && !isErrorPanelVisible()) {
|
||||
if ((showLocalSuggestions || showRemoteSuggestions)
|
||||
&& hasFocus && !isErrorPanelVisible()) {
|
||||
showSuggestionsPanel();
|
||||
}
|
||||
});
|
||||
@@ -659,31 +669,15 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "showKeyboardSearch() called");
|
||||
}
|
||||
if (searchEditText == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (searchEditText.requestFocus()) {
|
||||
final InputMethodManager imm = ContextCompat.getSystemService(activity,
|
||||
InputMethodManager.class);
|
||||
imm.showSoftInput(searchEditText, InputMethodManager.SHOW_FORCED);
|
||||
}
|
||||
KeyboardUtil.showKeyboard(activity, searchEditText);
|
||||
}
|
||||
|
||||
private void hideKeyboardSearch() {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "hideKeyboardSearch() called");
|
||||
}
|
||||
if (searchEditText == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final InputMethodManager imm = ContextCompat.getSystemService(activity,
|
||||
InputMethodManager.class);
|
||||
imm.hideSoftInputFromWindow(searchEditText.getWindowToken(),
|
||||
InputMethodManager.RESULT_UNCHANGED_SHOWN);
|
||||
|
||||
searchEditText.clearFocus();
|
||||
KeyboardUtil.hideKeyboard(activity, searchEditText);
|
||||
}
|
||||
|
||||
private void showDeleteSuggestionDialog(final SuggestionItem item) {
|
||||
@@ -713,7 +707,7 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
||||
@Override
|
||||
public boolean onBackPressed() {
|
||||
if (suggestionsPanelVisible
|
||||
&& infoListAdapter.getItemsList().size() > 0
|
||||
&& !infoListAdapter.getItemsList().isEmpty()
|
||||
&& !isLoading.get()) {
|
||||
hideSuggestionsPanel();
|
||||
hideKeyboardSearch();
|
||||
@@ -723,6 +717,31 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
private Observable<List<SuggestionItem>> getLocalSuggestionsObservable(
|
||||
final String query, final int similarQueryLimit) {
|
||||
return historyRecordManager
|
||||
.getRelatedSearches(query, similarQueryLimit, 25)
|
||||
.toObservable()
|
||||
.map(searchHistoryEntries ->
|
||||
searchHistoryEntries.stream()
|
||||
.map(entry -> new SuggestionItem(true, entry))
|
||||
.collect(Collectors.toList()));
|
||||
}
|
||||
|
||||
private Observable<List<SuggestionItem>> getRemoteSuggestionsObservable(final String query) {
|
||||
return ExtractorHelper
|
||||
.suggestionsFor(serviceId, query)
|
||||
.toObservable()
|
||||
.map(strings -> {
|
||||
final List<SuggestionItem> result = new ArrayList<>();
|
||||
for (final String entry : strings) {
|
||||
result.add(new SuggestionItem(false, entry));
|
||||
}
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
private void initSuggestionObserver() {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "initSuggestionObserver() called");
|
||||
@@ -733,73 +752,53 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
||||
|
||||
suggestionDisposable = suggestionPublisher
|
||||
.debounce(SUGGESTIONS_DEBOUNCE, TimeUnit.MILLISECONDS)
|
||||
.startWithItem(searchString != null
|
||||
? searchString
|
||||
: "")
|
||||
.filter(ss -> isSuggestionsEnabled)
|
||||
.startWithItem(searchString == null ? "" : searchString)
|
||||
.switchMap(query -> {
|
||||
final Flowable<List<SearchHistoryEntry>> flowable = historyRecordManager
|
||||
.getRelatedSearches(query, 3, 25);
|
||||
final Observable<List<SuggestionItem>> local = flowable.toObservable()
|
||||
.map(searchHistoryEntries -> {
|
||||
final List<SuggestionItem> result = new ArrayList<>();
|
||||
for (final SearchHistoryEntry entry : searchHistoryEntries) {
|
||||
result.add(new SuggestionItem(true, entry.getSearch()));
|
||||
}
|
||||
return result;
|
||||
});
|
||||
// Only show remote suggestions if they are enabled in settings and
|
||||
// the query length is at least THRESHOLD_NETWORK_SUGGESTION
|
||||
final boolean shallShowRemoteSuggestionsNow = showRemoteSuggestions
|
||||
&& query.length() >= THRESHOLD_NETWORK_SUGGESTION;
|
||||
|
||||
if (query.length() < THRESHOLD_NETWORK_SUGGESTION) {
|
||||
// Only pass through if the query length
|
||||
// is equal or greater than THRESHOLD_NETWORK_SUGGESTION
|
||||
return local.materialize();
|
||||
if (showLocalSuggestions && shallShowRemoteSuggestionsNow) {
|
||||
return Observable.zip(
|
||||
getLocalSuggestionsObservable(query, 3),
|
||||
getRemoteSuggestionsObservable(query),
|
||||
(local, remote) -> {
|
||||
remote.removeIf(remoteItem -> local.stream().anyMatch(
|
||||
localItem -> localItem.equals(remoteItem)));
|
||||
local.addAll(remote);
|
||||
return local;
|
||||
})
|
||||
.materialize();
|
||||
} else if (showLocalSuggestions) {
|
||||
return getLocalSuggestionsObservable(query, 25)
|
||||
.materialize();
|
||||
} else if (shallShowRemoteSuggestionsNow) {
|
||||
return getRemoteSuggestionsObservable(query)
|
||||
.materialize();
|
||||
} else {
|
||||
return Single.fromCallable(Collections::<SuggestionItem>emptyList)
|
||||
.toObservable()
|
||||
.materialize();
|
||||
}
|
||||
|
||||
final Observable<List<SuggestionItem>> network = ExtractorHelper
|
||||
.suggestionsFor(serviceId, query)
|
||||
.onErrorReturn(throwable -> {
|
||||
if (!ExceptionUtils.isNetworkRelated(throwable)) {
|
||||
showSnackBarError(new ErrorInfo(throwable,
|
||||
UserAction.GET_SUGGESTIONS, searchString, serviceId));
|
||||
}
|
||||
return new ArrayList<>();
|
||||
})
|
||||
.toObservable()
|
||||
.map(strings -> {
|
||||
final List<SuggestionItem> result = new ArrayList<>();
|
||||
for (final String entry : strings) {
|
||||
result.add(new SuggestionItem(false, entry));
|
||||
}
|
||||
return result;
|
||||
});
|
||||
|
||||
return Observable.zip(local, network, (localResult, networkResult) -> {
|
||||
final List<SuggestionItem> result = new ArrayList<>();
|
||||
if (localResult.size() > 0) {
|
||||
result.addAll(localResult);
|
||||
}
|
||||
|
||||
// Remove duplicates
|
||||
networkResult.removeIf(networkItem ->
|
||||
localResult.stream().anyMatch(localItem ->
|
||||
localItem.query.equals(networkItem.query)));
|
||||
|
||||
if (networkResult.size() > 0) {
|
||||
result.addAll(networkResult);
|
||||
}
|
||||
return result;
|
||||
}).materialize();
|
||||
})
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(listNotification -> {
|
||||
if (listNotification.isOnNext()) {
|
||||
handleSuggestions(listNotification.getValue());
|
||||
} else if (listNotification.isOnError()) {
|
||||
showError(new ErrorInfo(listNotification.getError(),
|
||||
UserAction.GET_SUGGESTIONS, searchString, serviceId));
|
||||
}
|
||||
});
|
||||
.subscribe(
|
||||
listNotification -> {
|
||||
if (listNotification.isOnNext()) {
|
||||
if (listNotification.getValue() != null) {
|
||||
handleSuggestions(listNotification.getValue());
|
||||
}
|
||||
} else if (listNotification.isOnError()
|
||||
&& listNotification.getError() != null
|
||||
&& !ExceptionUtils.isInterruptedCaused(
|
||||
listNotification.getError())) {
|
||||
showSnackBarError(new ErrorInfo(listNotification.getError(),
|
||||
UserAction.GET_SUGGESTIONS, searchString, serviceId));
|
||||
}
|
||||
}, throwable -> showSnackBarError(new ErrorInfo(
|
||||
throwable, UserAction.GET_SUGGESTIONS, searchString, serviceId)));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -841,7 +840,7 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
||||
infoListAdapter.clearStreamItemList();
|
||||
hideSuggestionsPanel();
|
||||
showMetaInfoInTextView(null, searchBinding.searchMetaInfoTextView,
|
||||
searchBinding.searchMetaInfoSeparator);
|
||||
searchBinding.searchMetaInfoSeparator, disposables);
|
||||
hideKeyboardSearch();
|
||||
|
||||
disposables.add(historyRecordManager.onSearched(serviceId, theSearchString)
|
||||
@@ -986,8 +985,8 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
||||
// List<MetaInfo> cannot be bundled without creating some containers
|
||||
metaInfo = new MetaInfo[result.getMetaInfo().size()];
|
||||
metaInfo = result.getMetaInfo().toArray(metaInfo);
|
||||
disposables.add(showMetaInfoInTextView(result.getMetaInfo(),
|
||||
searchBinding.searchMetaInfoTextView, searchBinding.searchMetaInfoSeparator));
|
||||
showMetaInfoInTextView(result.getMetaInfo(), searchBinding.searchMetaInfoTextView,
|
||||
searchBinding.searchMetaInfoSeparator, disposables);
|
||||
|
||||
handleSearchSuggestion();
|
||||
|
||||
@@ -1066,7 +1065,7 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
public int getSuggestionMovementFlags(@NonNull final RecyclerView.ViewHolder viewHolder) {
|
||||
final int position = viewHolder.getAdapterPosition();
|
||||
final int position = viewHolder.getBindingAdapterPosition();
|
||||
if (position == RecyclerView.NO_POSITION) {
|
||||
return 0;
|
||||
}
|
||||
@@ -1077,7 +1076,7 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
||||
}
|
||||
|
||||
public void onSuggestionItemSwiped(@NonNull final RecyclerView.ViewHolder viewHolder) {
|
||||
final int position = viewHolder.getAdapterPosition();
|
||||
final int position = viewHolder.getBindingAdapterPosition();
|
||||
final String query = suggestionListAdapter.getItem(position).query;
|
||||
final Disposable onDelete = historyRecordManager.deleteSearchHistory(query)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package org.schabi.newpipe.fragments.list.search;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
public class SuggestionItem {
|
||||
final boolean fromHistory;
|
||||
public final String query;
|
||||
@@ -9,6 +11,20 @@ public class SuggestionItem {
|
||||
this.query = query;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(final Object o) {
|
||||
if (o instanceof SuggestionItem) {
|
||||
return query.equals(((SuggestionItem) o).query);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return query.hashCode();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String toString() {
|
||||
return "[" + fromHistory + "→" + query + "]";
|
||||
|
||||
@@ -19,7 +19,6 @@ public class SuggestionListAdapter
|
||||
private final ArrayList<SuggestionItem> items = new ArrayList<>();
|
||||
private final Context context;
|
||||
private OnSuggestionItemSelected listener;
|
||||
private boolean showSuggestionHistory = true;
|
||||
|
||||
public SuggestionListAdapter(final Context context) {
|
||||
this.context = context;
|
||||
@@ -27,16 +26,7 @@ public class SuggestionListAdapter
|
||||
|
||||
public void setItems(final List<SuggestionItem> items) {
|
||||
this.items.clear();
|
||||
if (showSuggestionHistory) {
|
||||
this.items.addAll(items);
|
||||
} else {
|
||||
// remove history items if history is disabled
|
||||
for (final SuggestionItem item : items) {
|
||||
if (!item.fromHistory) {
|
||||
this.items.add(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
this.items.addAll(items);
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
@@ -44,10 +34,6 @@ public class SuggestionListAdapter
|
||||
this.listener = listener;
|
||||
}
|
||||
|
||||
public void setShowSuggestionHistory(final boolean v) {
|
||||
showSuggestionHistory = v;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SuggestionItemHolder onCreateViewHolder(final ViewGroup parent, final int viewType) {
|
||||
return new SuggestionItemHolder(LayoutInflater.from(context)
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package org.schabi.newpipe.fragments.list.videos;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
@@ -12,7 +11,6 @@ import android.view.ViewGroup;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.preference.PreferenceManager;
|
||||
import androidx.viewbinding.ViewBinding;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.databinding.RelatedItemsHeaderBinding;
|
||||
@@ -24,14 +22,14 @@ import org.schabi.newpipe.ktx.ViewUtils;
|
||||
import org.schabi.newpipe.util.RelatedItemInfo;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import io.reactivex.rxjava3.core.Single;
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
||||
|
||||
public class RelatedItemsFragment extends BaseListInfoFragment<RelatedItemInfo>
|
||||
implements SharedPreferences.OnSharedPreferenceChangeListener {
|
||||
private static final String INFO_KEY = "related_info_key";
|
||||
private final CompositeDisposable disposables = new CompositeDisposable();
|
||||
|
||||
private RelatedItemInfo relatedItemInfo;
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
@@ -54,11 +52,6 @@ public class RelatedItemsFragment extends BaseListInfoFragment<RelatedItemInfo>
|
||||
// LifeCycle
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public void onAttach(@NonNull final Context context) {
|
||||
super.onAttach(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(@NonNull final LayoutInflater inflater,
|
||||
@Nullable final ViewGroup container,
|
||||
@@ -66,12 +59,6 @@ public class RelatedItemsFragment extends BaseListInfoFragment<RelatedItemInfo>
|
||||
return inflater.inflate(R.layout.fragment_related_items, container, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
disposables.clear();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyView() {
|
||||
headerBinding = null;
|
||||
@@ -79,22 +66,23 @@ public class RelatedItemsFragment extends BaseListInfoFragment<RelatedItemInfo>
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ViewBinding getListHeader() {
|
||||
if (relatedItemInfo != null && relatedItemInfo.getRelatedItems() != null) {
|
||||
headerBinding = RelatedItemsHeaderBinding
|
||||
.inflate(activity.getLayoutInflater(), itemsList, false);
|
||||
|
||||
final SharedPreferences pref = PreferenceManager
|
||||
.getDefaultSharedPreferences(requireContext());
|
||||
final boolean autoplay = pref.getBoolean(getString(R.string.auto_queue_key), false);
|
||||
headerBinding.autoplaySwitch.setChecked(autoplay);
|
||||
headerBinding.autoplaySwitch.setOnCheckedChangeListener((compoundButton, b) ->
|
||||
PreferenceManager.getDefaultSharedPreferences(requireContext()).edit()
|
||||
.putBoolean(getString(R.string.auto_queue_key), b).apply());
|
||||
return headerBinding;
|
||||
} else {
|
||||
protected Supplier<View> getListHeaderSupplier() {
|
||||
if (relatedItemInfo == null || relatedItemInfo.getRelatedItems() == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
headerBinding = RelatedItemsHeaderBinding
|
||||
.inflate(activity.getLayoutInflater(), itemsList, false);
|
||||
|
||||
final SharedPreferences pref = PreferenceManager
|
||||
.getDefaultSharedPreferences(requireContext());
|
||||
final boolean autoplay = pref.getBoolean(getString(R.string.auto_queue_key), false);
|
||||
headerBinding.autoplaySwitch.setChecked(autoplay);
|
||||
headerBinding.autoplaySwitch.setOnCheckedChangeListener((compoundButton, b) ->
|
||||
PreferenceManager.getDefaultSharedPreferences(requireContext()).edit()
|
||||
.putBoolean(getString(R.string.auto_queue_key), b).apply());
|
||||
|
||||
return headerBinding::getRoot;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -128,7 +116,6 @@ public class RelatedItemsFragment extends BaseListInfoFragment<RelatedItemInfo>
|
||||
}
|
||||
ViewUtils.slideUp(requireView(), 120, 96, 0.06f);
|
||||
|
||||
disposables.clear();
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
@@ -137,10 +124,13 @@ public class RelatedItemsFragment extends BaseListInfoFragment<RelatedItemInfo>
|
||||
|
||||
@Override
|
||||
public void setTitle(final String title) {
|
||||
// Nothing to do - override parent
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreateOptionsMenu(final Menu menu, final MenuInflater inflater) {
|
||||
public void onCreateOptionsMenu(@NonNull final Menu menu,
|
||||
@NonNull final MenuInflater inflater) {
|
||||
// Nothing to do - override parent
|
||||
}
|
||||
|
||||
private void setInitialData(final StreamInfo info) {
|
||||
@@ -168,11 +158,10 @@ public class RelatedItemsFragment extends BaseListInfoFragment<RelatedItemInfo>
|
||||
@Override
|
||||
public void onSharedPreferenceChanged(final SharedPreferences sharedPreferences,
|
||||
final String s) {
|
||||
final SharedPreferences pref =
|
||||
PreferenceManager.getDefaultSharedPreferences(requireContext());
|
||||
final boolean autoplay = pref.getBoolean(getString(R.string.auto_queue_key), false);
|
||||
if (headerBinding != null) {
|
||||
headerBinding.autoplaySwitch.setChecked(autoplay);
|
||||
headerBinding.autoplaySwitch.setChecked(
|
||||
sharedPreferences.getBoolean(
|
||||
getString(R.string.auto_queue_key), false));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,8 +6,6 @@ import android.view.ViewGroup;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.nostra13.universalimageloader.core.ImageLoader;
|
||||
|
||||
import org.schabi.newpipe.extractor.InfoItem;
|
||||
import org.schabi.newpipe.extractor.channel.ChannelInfoItem;
|
||||
import org.schabi.newpipe.extractor.comments.CommentsInfoItem;
|
||||
@@ -51,7 +49,6 @@ import org.schabi.newpipe.util.OnClickGesture;
|
||||
|
||||
public class InfoItemBuilder {
|
||||
private final Context context;
|
||||
private final ImageLoader imageLoader = ImageLoader.getInstance();
|
||||
|
||||
private OnClickGesture<StreamInfoItem> onStreamSelectedListener;
|
||||
private OnClickGesture<ChannelInfoItem> onChannelSelectedListener;
|
||||
@@ -101,10 +98,6 @@ public class InfoItemBuilder {
|
||||
return context;
|
||||
}
|
||||
|
||||
public ImageLoader getImageLoader() {
|
||||
return imageLoader;
|
||||
}
|
||||
|
||||
public OnClickGesture<StreamInfoItem> getOnStreamSelectedListener() {
|
||||
return onStreamSelectedListener;
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package org.schabi.newpipe.info_list;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
@@ -10,7 +11,7 @@ import androidx.annotation.Nullable;
|
||||
import androidx.recyclerview.widget.GridLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import org.schabi.newpipe.database.stream.model.StreamStateEntity;
|
||||
import org.schabi.newpipe.databinding.PignateFooterBinding;
|
||||
import org.schabi.newpipe.extractor.InfoItem;
|
||||
import org.schabi.newpipe.extractor.channel.ChannelInfoItem;
|
||||
import org.schabi.newpipe.extractor.comments.CommentsInfoItem;
|
||||
@@ -34,6 +35,7 @@ import org.schabi.newpipe.util.OnClickGesture;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
/*
|
||||
* Created by Christian Schabesberger on 01.08.16.
|
||||
@@ -74,18 +76,20 @@ public class InfoListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolde
|
||||
private static final int MINI_COMMENT_HOLDER_TYPE = 0x400;
|
||||
private static final int COMMENT_HOLDER_TYPE = 0x401;
|
||||
|
||||
private final LayoutInflater layoutInflater;
|
||||
private final InfoItemBuilder infoItemBuilder;
|
||||
private final ArrayList<InfoItem> infoItemList;
|
||||
private final List<InfoItem> infoItemList;
|
||||
private final HistoryRecordManager recordManager;
|
||||
|
||||
private boolean useMiniVariant = false;
|
||||
private boolean useGridVariant = false;
|
||||
private boolean showFooter = false;
|
||||
private View header = null;
|
||||
private View footer = null;
|
||||
|
||||
private Supplier<View> headerSupplier = null;
|
||||
|
||||
public InfoListAdapter(final Context context) {
|
||||
this.recordManager = new HistoryRecordManager(context);
|
||||
layoutInflater = LayoutInflater.from(context);
|
||||
recordManager = new HistoryRecordManager(context);
|
||||
infoItemBuilder = new InfoItemBuilder(context);
|
||||
infoItemList = new ArrayList<>();
|
||||
}
|
||||
@@ -129,12 +133,12 @@ public class InfoListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolde
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "addInfoItemList() after > offsetStart = " + offsetStart + ", "
|
||||
+ "infoItemList.size() = " + infoItemList.size() + ", "
|
||||
+ "header = " + header + ", footer = " + footer + ", "
|
||||
+ "hasHeader = " + hasHeader() + ", "
|
||||
+ "showFooter = " + showFooter);
|
||||
}
|
||||
notifyItemRangeInserted(offsetStart, data.size());
|
||||
|
||||
if (footer != null && showFooter) {
|
||||
if (showFooter) {
|
||||
final int footerNow = sizeConsideringHeaderOffset();
|
||||
notifyItemMoved(offsetStart, footerNow);
|
||||
|
||||
@@ -145,43 +149,6 @@ public class InfoListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolde
|
||||
}
|
||||
}
|
||||
|
||||
public void setInfoItemList(final List<? extends InfoItem> data) {
|
||||
infoItemList.clear();
|
||||
infoItemList.addAll(data);
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
public void addInfoItem(@Nullable final InfoItem data) {
|
||||
if (data == null) {
|
||||
return;
|
||||
}
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "addInfoItem() before > infoItemList.size() = "
|
||||
+ infoItemList.size() + ", thread = " + Thread.currentThread());
|
||||
}
|
||||
|
||||
final int positionInserted = sizeConsideringHeaderOffset();
|
||||
infoItemList.add(data);
|
||||
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "addInfoItem() after > position = " + positionInserted + ", "
|
||||
+ "infoItemList.size() = " + infoItemList.size() + ", "
|
||||
+ "header = " + header + ", footer = " + footer + ", "
|
||||
+ "showFooter = " + showFooter);
|
||||
}
|
||||
notifyItemInserted(positionInserted);
|
||||
|
||||
if (footer != null && showFooter) {
|
||||
final int footerNow = sizeConsideringHeaderOffset();
|
||||
notifyItemMoved(positionInserted, footerNow);
|
||||
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "addInfoItem() footer from " + positionInserted
|
||||
+ " to " + footerNow);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void clearStreamItemList() {
|
||||
if (infoItemList.isEmpty()) {
|
||||
return;
|
||||
@@ -190,16 +157,16 @@ public class InfoListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolde
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
public void setHeader(final View header) {
|
||||
final boolean changed = header != this.header;
|
||||
this.header = header;
|
||||
public void setHeaderSupplier(@Nullable final Supplier<View> headerSupplier) {
|
||||
final boolean changed = headerSupplier != this.headerSupplier;
|
||||
this.headerSupplier = headerSupplier;
|
||||
if (changed) {
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public void setFooter(final View view) {
|
||||
this.footer = view;
|
||||
protected boolean hasHeader() {
|
||||
return this.headerSupplier != null;
|
||||
}
|
||||
|
||||
public void showFooter(final boolean show) {
|
||||
@@ -219,48 +186,49 @@ public class InfoListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolde
|
||||
}
|
||||
|
||||
private int sizeConsideringHeaderOffset() {
|
||||
final int i = infoItemList.size() + (header != null ? 1 : 0);
|
||||
final int i = infoItemList.size() + (hasHeader() ? 1 : 0);
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "sizeConsideringHeaderOffset() called → " + i);
|
||||
}
|
||||
return i;
|
||||
}
|
||||
|
||||
public ArrayList<InfoItem> getItemsList() {
|
||||
public List<InfoItem> getItemsList() {
|
||||
return infoItemList;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
int count = infoItemList.size();
|
||||
if (header != null) {
|
||||
if (hasHeader()) {
|
||||
count++;
|
||||
}
|
||||
if (footer != null && showFooter) {
|
||||
if (showFooter) {
|
||||
count++;
|
||||
}
|
||||
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "getItemCount() called with: "
|
||||
+ "count = " + count + ", infoItemList.size() = " + infoItemList.size() + ", "
|
||||
+ "header = " + header + ", footer = " + footer + ", "
|
||||
+ "hasHeader = " + hasHeader() + ", "
|
||||
+ "showFooter = " + showFooter);
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
@SuppressWarnings("FinalParameters")
|
||||
@Override
|
||||
public int getItemViewType(int position) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "getItemViewType() called with: position = [" + position + "]");
|
||||
}
|
||||
|
||||
if (header != null && position == 0) {
|
||||
if (hasHeader() && position == 0) {
|
||||
return HEADER_TYPE;
|
||||
} else if (header != null) {
|
||||
} else if (hasHeader()) {
|
||||
position--;
|
||||
}
|
||||
if (footer != null && position == infoItemList.size() && showFooter) {
|
||||
if (position == infoItemList.size() && showFooter) {
|
||||
return FOOTER_TYPE;
|
||||
}
|
||||
final InfoItem item = infoItemList.get(position);
|
||||
@@ -290,10 +258,16 @@ public class InfoListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolde
|
||||
+ "parent = [" + parent + "], type = [" + type + "]");
|
||||
}
|
||||
switch (type) {
|
||||
// #4475 and #3368
|
||||
// Always create a new instance otherwise the same instance
|
||||
// is sometimes reused which causes a crash
|
||||
case HEADER_TYPE:
|
||||
return new HFHolder(header);
|
||||
return new HFHolder(headerSupplier.get());
|
||||
case FOOTER_TYPE:
|
||||
return new HFHolder(footer);
|
||||
return new HFHolder(PignateFooterBinding
|
||||
.inflate(layoutInflater, parent, false)
|
||||
.getRoot()
|
||||
);
|
||||
case MINI_STREAM_HOLDER_TYPE:
|
||||
return new StreamMiniInfoItemHolder(infoItemBuilder, parent);
|
||||
case STREAM_HOLDER_TYPE:
|
||||
@@ -322,42 +296,17 @@ public class InfoListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolde
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull final RecyclerView.ViewHolder holder, int position) {
|
||||
public void onBindViewHolder(@NonNull final RecyclerView.ViewHolder holder,
|
||||
final int position) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "onBindViewHolder() called with: "
|
||||
+ "holder = [" + holder.getClass().getSimpleName() + "], "
|
||||
+ "position = [" + position + "]");
|
||||
}
|
||||
if (holder instanceof InfoItemHolder) {
|
||||
// If header isn't null, offset the items by -1
|
||||
if (header != null) {
|
||||
position--;
|
||||
}
|
||||
|
||||
((InfoItemHolder) holder).updateFromItem(infoItemList.get(position), recordManager);
|
||||
} else if (holder instanceof HFHolder && position == 0 && header != null) {
|
||||
((HFHolder) holder).view = header;
|
||||
} else if (holder instanceof HFHolder && position == sizeConsideringHeaderOffset()
|
||||
&& footer != null && showFooter) {
|
||||
((HFHolder) holder).view = footer;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull final RecyclerView.ViewHolder holder, final int position,
|
||||
@NonNull final List<Object> payloads) {
|
||||
if (!payloads.isEmpty() && holder instanceof InfoItemHolder) {
|
||||
for (final Object payload : payloads) {
|
||||
if (payload instanceof StreamStateEntity) {
|
||||
((InfoItemHolder) holder).updateState(infoItemList
|
||||
.get(header == null ? position : position - 1), recordManager);
|
||||
} else if (payload instanceof Boolean) {
|
||||
((InfoItemHolder) holder).updateState(infoItemList
|
||||
.get(header == null ? position : position - 1), recordManager);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
onBindViewHolder(holder, position);
|
||||
((InfoItemHolder) holder).updateFromItem(
|
||||
// If header is present, offset the items by -1
|
||||
infoItemList.get(hasHeader() ? position - 1 : position), recordManager);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -371,12 +320,9 @@ public class InfoListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolde
|
||||
};
|
||||
}
|
||||
|
||||
public static class HFHolder extends RecyclerView.ViewHolder {
|
||||
public View view;
|
||||
|
||||
static class HFHolder extends RecyclerView.ViewHolder {
|
||||
HFHolder(final View v) {
|
||||
super(v);
|
||||
view = v;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
package org.schabi.newpipe.info_list
|
||||
|
||||
import android.util.Log
|
||||
import com.xwray.groupie.GroupAdapter
|
||||
import com.xwray.groupie.GroupieViewHolder
|
||||
import com.xwray.groupie.GroupieAdapter
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfo
|
||||
import kotlin.math.max
|
||||
|
||||
@@ -11,7 +10,7 @@ import kotlin.math.max
|
||||
*/
|
||||
class StreamSegmentAdapter(
|
||||
private val listener: StreamSegmentListener
|
||||
) : GroupAdapter<GroupieViewHolder>() {
|
||||
) : GroupieAdapter() {
|
||||
|
||||
var currentIndex: Int = 0
|
||||
private set
|
||||
|
||||
@@ -3,13 +3,12 @@ package org.schabi.newpipe.info_list
|
||||
import android.view.View
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import com.nostra13.universalimageloader.core.ImageLoader
|
||||
import com.xwray.groupie.GroupieViewHolder
|
||||
import com.xwray.groupie.Item
|
||||
import org.schabi.newpipe.R
|
||||
import org.schabi.newpipe.extractor.stream.StreamSegment
|
||||
import org.schabi.newpipe.util.ImageDisplayConstants
|
||||
import org.schabi.newpipe.util.Localization
|
||||
import org.schabi.newpipe.util.PicassoHelper
|
||||
|
||||
class StreamSegmentItem(
|
||||
private val item: StreamSegment,
|
||||
@@ -24,10 +23,8 @@ class StreamSegmentItem(
|
||||
|
||||
override fun bind(viewHolder: GroupieViewHolder, position: Int) {
|
||||
item.previewUrl?.let {
|
||||
ImageLoader.getInstance().displayImage(
|
||||
it, viewHolder.root.findViewById<ImageView>(R.id.previewImage),
|
||||
ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS
|
||||
)
|
||||
PicassoHelper.loadThumbnail(it)
|
||||
.into(viewHolder.root.findViewById<ImageView>(R.id.previewImage))
|
||||
}
|
||||
viewHolder.root.findViewById<TextView>(R.id.textViewTitle).text = item.title
|
||||
if (item.channelName == null) {
|
||||
|
||||
@@ -8,7 +8,7 @@ import org.schabi.newpipe.extractor.InfoItem;
|
||||
import org.schabi.newpipe.extractor.channel.ChannelInfoItem;
|
||||
import org.schabi.newpipe.info_list.InfoItemBuilder;
|
||||
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
||||
import org.schabi.newpipe.util.ImageDisplayConstants;
|
||||
import org.schabi.newpipe.util.PicassoHelper;
|
||||
import org.schabi.newpipe.util.Localization;
|
||||
|
||||
import de.hdodenhof.circleimageview.CircleImageView;
|
||||
@@ -43,10 +43,7 @@ public class ChannelMiniInfoItemHolder extends InfoItemHolder {
|
||||
itemTitleView.setText(item.getName());
|
||||
itemAdditionalDetailView.setText(getDetailLine(item));
|
||||
|
||||
itemBuilder.getImageLoader()
|
||||
.displayImage(item.getThumbnailUrl(),
|
||||
itemThumbnailView,
|
||||
ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS);
|
||||
PicassoHelper.loadThumbnail(item.getThumbnailUrl()).into(itemThumbnailView);
|
||||
|
||||
itemView.setOnClickListener(view -> {
|
||||
if (itemBuilder.getOnChannelSelectedListener() != null) {
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
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;
|
||||
@@ -31,11 +33,15 @@ import org.schabi.newpipe.local.history.HistoryRecordManager;
|
||||
|
||||
public class CommentsInfoItemHolder extends CommentsMiniInfoItemHolder {
|
||||
public final TextView itemTitleView;
|
||||
private final ImageView itemHeartView;
|
||||
private final ImageView itemPinnedView;
|
||||
|
||||
public CommentsInfoItemHolder(final InfoItemBuilder infoItemBuilder, final ViewGroup parent) {
|
||||
super(infoItemBuilder, R.layout.list_comments_item, parent);
|
||||
|
||||
itemTitleView = itemView.findViewById(R.id.itemTitleView);
|
||||
itemHeartView = itemView.findViewById(R.id.detail_heart_image_view);
|
||||
itemPinnedView = itemView.findViewById(R.id.detail_pinned_view);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -49,5 +55,9 @@ public class CommentsInfoItemHolder extends CommentsMiniInfoItemHolder {
|
||||
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,50 +1,48 @@
|
||||
package org.schabi.newpipe.info_list.holder;
|
||||
|
||||
import android.content.SharedPreferences;
|
||||
import android.text.TextUtils;
|
||||
import android.text.method.LinkMovementMethod;
|
||||
import android.text.style.URLSpan;
|
||||
import android.text.util.Linkify;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.RelativeLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.preference.PreferenceManager;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.error.ErrorActivity;
|
||||
import org.schabi.newpipe.error.ErrorUtil;
|
||||
import org.schabi.newpipe.extractor.InfoItem;
|
||||
import org.schabi.newpipe.extractor.comments.CommentsInfoItem;
|
||||
import org.schabi.newpipe.info_list.InfoItemBuilder;
|
||||
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
||||
import org.schabi.newpipe.util.CommentTextOnTouchListener;
|
||||
import org.schabi.newpipe.util.DeviceUtils;
|
||||
import org.schabi.newpipe.util.ImageDisplayConstants;
|
||||
import org.schabi.newpipe.util.Localization;
|
||||
import org.schabi.newpipe.util.NavigationHelper;
|
||||
import org.schabi.newpipe.util.ShareUtils;
|
||||
import org.schabi.newpipe.util.external_communication.TimestampExtractor;
|
||||
import org.schabi.newpipe.util.external_communication.ShareUtils;
|
||||
import org.schabi.newpipe.util.PicassoHelper;
|
||||
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import de.hdodenhof.circleimageview.CircleImageView;
|
||||
|
||||
public class CommentsMiniInfoItemHolder extends InfoItemHolder {
|
||||
private static final String TAG = "CommentsMiniIIHolder";
|
||||
|
||||
private static final int COMMENT_DEFAULT_LINES = 2;
|
||||
private static final int COMMENT_EXPANDED_LINES = 1000;
|
||||
private static final Pattern PATTERN = Pattern.compile("(\\d+:)?(\\d+)?:(\\d+)");
|
||||
private final String downloadThumbnailKey;
|
||||
|
||||
private final int commentHorizontalPadding;
|
||||
private final int commentVerticalPadding;
|
||||
|
||||
private SharedPreferences preferences = null;
|
||||
private final RelativeLayout itemRoot;
|
||||
public final CircleImageView itemThumbnailView;
|
||||
private final TextView itemContentView;
|
||||
private final TextView itemLikesCountView;
|
||||
private final TextView itemDislikesCountView;
|
||||
private final TextView itemPublishedTime;
|
||||
|
||||
private String commentText;
|
||||
@@ -53,20 +51,21 @@ public class CommentsMiniInfoItemHolder extends InfoItemHolder {
|
||||
private final Linkify.TransformFilter timestampLink = new Linkify.TransformFilter() {
|
||||
@Override
|
||||
public String transformUrl(final Matcher match, final String url) {
|
||||
int timestamp = 0;
|
||||
final String hours = match.group(1);
|
||||
final String minutes = match.group(2);
|
||||
final String seconds = match.group(3);
|
||||
if (hours != null) {
|
||||
timestamp += (Integer.parseInt(hours.replace(":", "")) * 3600);
|
||||
try {
|
||||
final TimestampExtractor.TimestampMatchDTO timestampMatchDTO =
|
||||
TimestampExtractor.getTimestampFromMatcher(match, commentText);
|
||||
|
||||
if (timestampMatchDTO == null) {
|
||||
return url;
|
||||
}
|
||||
|
||||
return streamUrl + url.replace(
|
||||
match.group(0),
|
||||
"#timestamp=" + timestampMatchDTO.seconds());
|
||||
} catch (final Exception ex) {
|
||||
Log.e(TAG, "Unable to process url='" + url + "' as timestampLink", ex);
|
||||
return url;
|
||||
}
|
||||
if (minutes != null) {
|
||||
timestamp += (Integer.parseInt(minutes.replace(":", "")) * 60);
|
||||
}
|
||||
if (seconds != null) {
|
||||
timestamp += (Integer.parseInt(seconds));
|
||||
}
|
||||
return streamUrl + url.replace(match.group(0), "#timestamp=" + timestamp);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -77,13 +76,9 @@ public class CommentsMiniInfoItemHolder extends InfoItemHolder {
|
||||
itemRoot = itemView.findViewById(R.id.itemRoot);
|
||||
itemThumbnailView = itemView.findViewById(R.id.itemThumbnailView);
|
||||
itemLikesCountView = itemView.findViewById(R.id.detail_thumbs_up_count_view);
|
||||
itemDislikesCountView = itemView.findViewById(R.id.detail_thumbs_down_count_view);
|
||||
itemPublishedTime = itemView.findViewById(R.id.itemPublishedTime);
|
||||
itemContentView = itemView.findViewById(R.id.itemCommentContentView);
|
||||
|
||||
downloadThumbnailKey = infoItemBuilder.getContext().
|
||||
getString(R.string.download_thumbnail_key);
|
||||
|
||||
commentHorizontalPadding = (int) infoItemBuilder.getContext()
|
||||
.getResources().getDimension(R.dimen.comments_horizontal_padding);
|
||||
commentVerticalPadding = (int) infoItemBuilder.getContext()
|
||||
@@ -103,14 +98,8 @@ public class CommentsMiniInfoItemHolder extends InfoItemHolder {
|
||||
}
|
||||
final CommentsInfoItem item = (CommentsInfoItem) infoItem;
|
||||
|
||||
preferences = PreferenceManager.getDefaultSharedPreferences(itemBuilder.getContext());
|
||||
|
||||
itemBuilder.getImageLoader()
|
||||
.displayImage(item.getUploaderAvatarUrl(),
|
||||
itemThumbnailView,
|
||||
ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS);
|
||||
|
||||
if (preferences.getBoolean(downloadThumbnailKey, true)) {
|
||||
PicassoHelper.loadAvatar(item.getUploaderAvatarUrl()).into(itemThumbnailView);
|
||||
if (PicassoHelper.getShouldLoadImages()) {
|
||||
itemThumbnailView.setVisibility(View.VISIBLE);
|
||||
itemRoot.setPadding(commentVerticalPadding, commentVerticalPadding,
|
||||
commentVerticalPadding, commentVerticalPadding);
|
||||
@@ -182,7 +171,7 @@ public class CommentsMiniInfoItemHolder extends InfoItemHolder {
|
||||
item.getUploaderUrl(),
|
||||
item.getUploaderName());
|
||||
} catch (final Exception e) {
|
||||
ErrorActivity.reportUiErrorInSnackbar(activity, "Opening channel fragment", e);
|
||||
ErrorUtil.showUiErrorSnackbar(activity, "Opening channel fragment", e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -254,7 +243,14 @@ public class CommentsMiniInfoItemHolder extends InfoItemHolder {
|
||||
}
|
||||
|
||||
private void linkify() {
|
||||
Linkify.addLinks(itemContentView, Linkify.WEB_URLS);
|
||||
Linkify.addLinks(itemContentView, PATTERN, null, null, timestampLink);
|
||||
Linkify.addLinks(
|
||||
itemContentView,
|
||||
Linkify.WEB_URLS);
|
||||
Linkify.addLinks(
|
||||
itemContentView,
|
||||
TimestampExtractor.TIMESTAMPS_PATTERN,
|
||||
null,
|
||||
null,
|
||||
timestampLink);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import org.schabi.newpipe.extractor.InfoItem;
|
||||
import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem;
|
||||
import org.schabi.newpipe.info_list.InfoItemBuilder;
|
||||
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
||||
import org.schabi.newpipe.util.ImageDisplayConstants;
|
||||
import org.schabi.newpipe.util.PicassoHelper;
|
||||
import org.schabi.newpipe.util.Localization;
|
||||
|
||||
public class PlaylistMiniInfoItemHolder extends InfoItemHolder {
|
||||
@@ -46,9 +46,7 @@ public class PlaylistMiniInfoItemHolder extends InfoItemHolder {
|
||||
.localizeStreamCountMini(itemStreamCountView.getContext(), item.getStreamCount()));
|
||||
itemUploaderView.setText(item.getUploaderName());
|
||||
|
||||
itemBuilder.getImageLoader()
|
||||
.displayImage(item.getThumbnailUrl(), itemThumbnailView,
|
||||
ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS);
|
||||
PicassoHelper.loadPlaylistThumbnail(item.getThumbnailUrl()).into(itemThumbnailView);
|
||||
|
||||
itemView.setOnClickListener(view -> {
|
||||
if (itemBuilder.getOnPlaylistSelectedListener() != null) {
|
||||
|
||||
@@ -15,7 +15,7 @@ import org.schabi.newpipe.extractor.stream.StreamType;
|
||||
import org.schabi.newpipe.info_list.InfoItemBuilder;
|
||||
import org.schabi.newpipe.ktx.ViewUtils;
|
||||
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
||||
import org.schabi.newpipe.util.ImageDisplayConstants;
|
||||
import org.schabi.newpipe.util.PicassoHelper;
|
||||
import org.schabi.newpipe.util.Localization;
|
||||
import org.schabi.newpipe.views.AnimatedProgressBar;
|
||||
|
||||
@@ -66,7 +66,7 @@ public class StreamMiniInfoItemHolder extends InfoItemHolder {
|
||||
itemProgressView.setVisibility(View.VISIBLE);
|
||||
itemProgressView.setMax((int) item.getDuration());
|
||||
itemProgressView.setProgress((int) TimeUnit.MILLISECONDS
|
||||
.toSeconds(state2.getProgressTime()));
|
||||
.toSeconds(state2.getProgressMillis()));
|
||||
} else {
|
||||
itemProgressView.setVisibility(View.GONE);
|
||||
}
|
||||
@@ -83,10 +83,7 @@ public class StreamMiniInfoItemHolder extends InfoItemHolder {
|
||||
}
|
||||
|
||||
// Default thumbnail is shown on error, while loading and if the url is empty
|
||||
itemBuilder.getImageLoader()
|
||||
.displayImage(item.getThumbnailUrl(),
|
||||
itemThumbnailView,
|
||||
ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS);
|
||||
PicassoHelper.loadThumbnail(item.getThumbnailUrl()).into(itemThumbnailView);
|
||||
|
||||
itemView.setOnClickListener(view -> {
|
||||
if (itemBuilder.getOnStreamSelectedListener() != null) {
|
||||
@@ -121,10 +118,10 @@ public class StreamMiniInfoItemHolder extends InfoItemHolder {
|
||||
itemProgressView.setMax((int) item.getDuration());
|
||||
if (itemProgressView.getVisibility() == View.VISIBLE) {
|
||||
itemProgressView.setProgressAnimated((int) TimeUnit.MILLISECONDS
|
||||
.toSeconds(state.getProgressTime()));
|
||||
.toSeconds(state.getProgressMillis()));
|
||||
} else {
|
||||
itemProgressView.setProgress((int) TimeUnit.MILLISECONDS
|
||||
.toSeconds(state.getProgressTime()));
|
||||
.toSeconds(state.getProgressMillis()));
|
||||
ViewUtils.animate(itemProgressView, true, 500);
|
||||
}
|
||||
} else if (itemProgressView.getVisibility() == View.VISIBLE) {
|
||||
|
||||
@@ -75,6 +75,7 @@ fun View.animate(
|
||||
}
|
||||
animate().setListener(null).cancel()
|
||||
isVisible = true
|
||||
|
||||
when (animationType) {
|
||||
AnimationType.ALPHA -> animateAlpha(enterOrExit, duration, delay, execOnEnd)
|
||||
AnimationType.SCALE_AND_ALPHA -> animateScaleAndAlpha(enterOrExit, duration, delay, execOnEnd)
|
||||
@@ -299,18 +300,36 @@ private fun View.animateLightSlideAndAlpha(enterOrExit: Boolean, duration: Long,
|
||||
}
|
||||
}
|
||||
|
||||
fun View.slideUp(duration: Long, delay: Long, @FloatRange(from = 0.0, to = 1.0) translationPercent: Float) {
|
||||
fun View.slideUp(
|
||||
duration: Long,
|
||||
delay: Long,
|
||||
@FloatRange(from = 0.0, to = 1.0) translationPercent: Float
|
||||
) {
|
||||
slideUp(duration, delay, translationPercent, null)
|
||||
}
|
||||
|
||||
fun View.slideUp(
|
||||
duration: Long,
|
||||
delay: Long = 0L,
|
||||
@FloatRange(from = 0.0, to = 1.0) translationPercent: Float = 1.0F,
|
||||
execOnEnd: Runnable? = null
|
||||
) {
|
||||
val newTranslationY = (resources.displayMetrics.heightPixels * translationPercent).toInt()
|
||||
animate().setListener(null).cancel()
|
||||
alpha = 0f
|
||||
translationY = newTranslationY.toFloat()
|
||||
visibility = View.VISIBLE
|
||||
isVisible = true
|
||||
animate()
|
||||
.alpha(1f)
|
||||
.translationY(0f)
|
||||
.setStartDelay(delay)
|
||||
.setDuration(duration)
|
||||
.setInterpolator(FastOutSlowInInterpolator())
|
||||
.setListener(object : AnimatorListenerAdapter() {
|
||||
override fun onAnimationEnd(animation: Animator) {
|
||||
execOnEnd?.run()
|
||||
}
|
||||
})
|
||||
.start()
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package org.schabi.newpipe.local;
|
||||
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.res.Configuration;
|
||||
import android.content.res.Resources;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
@@ -9,6 +8,7 @@ import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.View;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
import androidx.fragment.app.Fragment;
|
||||
@@ -25,6 +25,7 @@ import org.schabi.newpipe.fragments.list.ListViewContract;
|
||||
|
||||
import static org.schabi.newpipe.ktx.ViewUtils.animate;
|
||||
import static org.schabi.newpipe.ktx.ViewUtils.animateHideRecyclerViewAllowingScrolling;
|
||||
import static org.schabi.newpipe.util.ThemeHelper.shouldUseGridLayout;
|
||||
|
||||
/**
|
||||
* This fragment is design to be used with persistent data such as
|
||||
@@ -76,7 +77,7 @@ public abstract class BaseLocalListFragment<I, N> extends BaseStateFragment<I>
|
||||
super.onResume();
|
||||
if (updateFlags != 0) {
|
||||
if ((updateFlags & LIST_MODE_UPDATE_FLAG) != 0) {
|
||||
final boolean useGrid = isGridLayout();
|
||||
final boolean useGrid = shouldUseGridLayout(requireContext());
|
||||
itemsList.setLayoutManager(
|
||||
useGrid ? getGridLayoutManager() : getListLayoutManager());
|
||||
itemListAdapter.setUseGridVariant(useGrid);
|
||||
@@ -120,7 +121,7 @@ public abstract class BaseLocalListFragment<I, N> extends BaseStateFragment<I>
|
||||
|
||||
itemListAdapter = new LocalItemListAdapter(activity);
|
||||
|
||||
final boolean useGrid = isGridLayout();
|
||||
final boolean useGrid = shouldUseGridLayout(requireContext());
|
||||
itemsList = rootView.findViewById(R.id.items_list);
|
||||
itemsList.setLayoutManager(useGrid ? getGridLayoutManager() : getListLayoutManager());
|
||||
|
||||
@@ -145,7 +146,8 @@ public abstract class BaseLocalListFragment<I, N> extends BaseStateFragment<I>
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public void onCreateOptionsMenu(final Menu menu, final MenuInflater inflater) {
|
||||
public void onCreateOptionsMenu(@NonNull final Menu menu,
|
||||
@NonNull final MenuInflater inflater) {
|
||||
super.onCreateOptionsMenu(menu, inflater);
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "onCreateOptionsMenu() called with: "
|
||||
@@ -258,17 +260,4 @@ public abstract class BaseLocalListFragment<I, N> extends BaseStateFragment<I>
|
||||
updateFlags |= LIST_MODE_UPDATE_FLAG;
|
||||
}
|
||||
}
|
||||
|
||||
protected boolean isGridLayout() {
|
||||
final String listMode = PreferenceManager.getDefaultSharedPreferences(activity)
|
||||
.getString(getString(R.string.list_view_mode_key),
|
||||
getString(R.string.list_view_mode_value));
|
||||
if ("auto".equals(listMode)) {
|
||||
final Configuration configuration = getResources().getConfiguration();
|
||||
return configuration.orientation == Configuration.ORIENTATION_LANDSCAPE
|
||||
&& configuration.isLayoutSizeAtLeast(Configuration.SCREENLAYOUT_SIZE_LARGE);
|
||||
} else {
|
||||
return "grid".equals(listMode);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
package org.schabi.newpipe.local;
|
||||
|
||||
import android.content.Context;
|
||||
import android.widget.ImageView;
|
||||
|
||||
import com.nostra13.universalimageloader.core.DisplayImageOptions;
|
||||
import com.nostra13.universalimageloader.core.ImageLoader;
|
||||
|
||||
import org.schabi.newpipe.database.LocalItem;
|
||||
import org.schabi.newpipe.util.OnClickGesture;
|
||||
@@ -31,7 +27,6 @@ import org.schabi.newpipe.util.OnClickGesture;
|
||||
|
||||
public class LocalItemBuilder {
|
||||
private final Context context;
|
||||
private final ImageLoader imageLoader = ImageLoader.getInstance();
|
||||
|
||||
private OnClickGesture<LocalItem> onSelectedListener;
|
||||
|
||||
@@ -43,11 +38,6 @@ public class LocalItemBuilder {
|
||||
return context;
|
||||
}
|
||||
|
||||
public void displayImage(final String url, final ImageView view,
|
||||
final DisplayImageOptions options) {
|
||||
imageLoader.displayImage(url, view, options);
|
||||
}
|
||||
|
||||
public OnClickGesture<LocalItem> getOnItemSelectedListener() {
|
||||
return onSelectedListener;
|
||||
}
|
||||
|
||||
@@ -2,11 +2,11 @@ package org.schabi.newpipe.local.bookmark;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.os.Parcelable;
|
||||
import android.text.InputType;
|
||||
import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.EditText;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
@@ -22,6 +22,7 @@ import org.schabi.newpipe.database.LocalItem;
|
||||
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.databinding.DialogEditTextBinding;
|
||||
import org.schabi.newpipe.error.ErrorInfo;
|
||||
import org.schabi.newpipe.error.UserAction;
|
||||
import org.schabi.newpipe.local.BaseLocalListFragment;
|
||||
@@ -77,9 +78,9 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setUserVisibleHint(final boolean isVisibleToUser) {
|
||||
super.setUserVisibleHint(isVisibleToUser);
|
||||
if (activity != null && isVisibleToUser) {
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
if (activity != null) {
|
||||
setTitle(activity.getString(R.string.tab_bookmarks));
|
||||
}
|
||||
}
|
||||
@@ -255,14 +256,18 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL
|
||||
}
|
||||
|
||||
private void showLocalDialog(final PlaylistMetadataEntry selectedItem) {
|
||||
final View dialogView = View.inflate(getContext(), R.layout.dialog_bookmark, null);
|
||||
final EditText editText = dialogView.findViewById(R.id.playlist_name_edit_text);
|
||||
editText.setText(selectedItem.name);
|
||||
final DialogEditTextBinding dialogBinding
|
||||
= DialogEditTextBinding.inflate(getLayoutInflater());
|
||||
dialogBinding.dialogEditText.setHint(R.string.name);
|
||||
dialogBinding.dialogEditText.setInputType(InputType.TYPE_CLASS_TEXT);
|
||||
dialogBinding.dialogEditText.setText(selectedItem.name);
|
||||
|
||||
final AlertDialog.Builder builder = new AlertDialog.Builder(activity);
|
||||
builder.setView(dialogView)
|
||||
builder.setView(dialogBinding.getRoot())
|
||||
.setPositiveButton(R.string.rename_playlist, (dialog, which) ->
|
||||
changeLocalPlaylistName(selectedItem.uid, editText.getText().toString()))
|
||||
changeLocalPlaylistName(
|
||||
selectedItem.uid,
|
||||
dialogBinding.dialogEditText.getText().toString()))
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.setNeutralButton(R.string.delete, (dialog, which) -> {
|
||||
showDeleteDialog(selectedItem.name,
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package org.schabi.newpipe.local.dialog;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
@@ -17,20 +16,14 @@ import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.database.LocalItem;
|
||||
import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry;
|
||||
import org.schabi.newpipe.database.stream.model.StreamEntity;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||
import org.schabi.newpipe.local.LocalItemListAdapter;
|
||||
import org.schabi.newpipe.local.playlist.LocalPlaylistManager;
|
||||
import org.schabi.newpipe.player.playqueue.PlayQueueItem;
|
||||
import org.schabi.newpipe.util.OnClickGesture;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
||||
import io.reactivex.rxjava3.disposables.Disposable;
|
||||
|
||||
public final class PlaylistAppendDialog extends PlaylistDialog {
|
||||
private static final String TAG = PlaylistAppendDialog.class.getCanonicalName();
|
||||
@@ -40,47 +33,8 @@ public final class PlaylistAppendDialog extends PlaylistDialog {
|
||||
|
||||
private final CompositeDisposable playlistDisposables = new CompositeDisposable();
|
||||
|
||||
public static Disposable onPlaylistFound(
|
||||
final Context context, final Runnable onSuccess, final Runnable onFailed
|
||||
) {
|
||||
final LocalPlaylistManager playlistManager =
|
||||
new LocalPlaylistManager(NewPipeDatabase.getInstance(context));
|
||||
|
||||
return playlistManager.hasPlaylists()
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(hasPlaylists -> {
|
||||
if (hasPlaylists) {
|
||||
onSuccess.run();
|
||||
} else {
|
||||
onFailed.run();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static PlaylistAppendDialog fromStreamInfo(final StreamInfo info) {
|
||||
final PlaylistAppendDialog dialog = new PlaylistAppendDialog();
|
||||
dialog.setInfo(Collections.singletonList(new StreamEntity(info)));
|
||||
return dialog;
|
||||
}
|
||||
|
||||
public static PlaylistAppendDialog fromStreamInfoItems(final List<StreamInfoItem> items) {
|
||||
final PlaylistAppendDialog dialog = new PlaylistAppendDialog();
|
||||
final List<StreamEntity> entities = new ArrayList<>(items.size());
|
||||
for (final StreamInfoItem item : items) {
|
||||
entities.add(new StreamEntity(item));
|
||||
}
|
||||
dialog.setInfo(entities);
|
||||
return dialog;
|
||||
}
|
||||
|
||||
public static PlaylistAppendDialog fromPlayQueueItems(final List<PlayQueueItem> items) {
|
||||
final PlaylistAppendDialog dialog = new PlaylistAppendDialog();
|
||||
final List<StreamEntity> entities = new ArrayList<>(items.size());
|
||||
for (final PlayQueueItem item : items) {
|
||||
entities.add(new StreamEntity(item));
|
||||
}
|
||||
dialog.setInfo(entities);
|
||||
return dialog;
|
||||
public PlaylistAppendDialog(final List<StreamEntity> streamEntities) {
|
||||
super(streamEntities);
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
@@ -104,11 +58,15 @@ public final class PlaylistAppendDialog extends PlaylistDialog {
|
||||
playlistAdapter.setSelectedListener(new OnClickGesture<LocalItem>() {
|
||||
@Override
|
||||
public void selected(final LocalItem selectedItem) {
|
||||
if (!(selectedItem instanceof PlaylistMetadataEntry) || getStreams() == null) {
|
||||
if (!(selectedItem instanceof PlaylistMetadataEntry)
|
||||
|| getStreamEntities() == null) {
|
||||
return;
|
||||
}
|
||||
onPlaylistSelected(playlistManager, (PlaylistMetadataEntry) selectedItem,
|
||||
getStreams());
|
||||
onPlaylistSelected(
|
||||
playlistManager,
|
||||
(PlaylistMetadataEntry) selectedItem,
|
||||
getStreamEntities()
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -146,11 +104,17 @@ public final class PlaylistAppendDialog extends PlaylistDialog {
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
public void openCreatePlaylistDialog() {
|
||||
if (getStreams() == null || !isAdded()) {
|
||||
if (getStreamEntities() == null || !isAdded()) {
|
||||
return;
|
||||
}
|
||||
|
||||
PlaylistCreationDialog.newInstance(getStreams()).show(getParentFragmentManager(), TAG);
|
||||
final PlaylistCreationDialog playlistCreationDialog =
|
||||
new PlaylistCreationDialog(getStreamEntities());
|
||||
// Move the dismissListener to the new dialog.
|
||||
playlistCreationDialog.setOnDismissListener(this.getOnDismissListener());
|
||||
this.setOnDismissListener(null);
|
||||
|
||||
playlistCreationDialog.show(getParentFragmentManager(), TAG);
|
||||
requireDialog().dismiss();
|
||||
}
|
||||
|
||||
@@ -165,7 +129,7 @@ public final class PlaylistAppendDialog extends PlaylistDialog {
|
||||
private void onPlaylistSelected(@NonNull final LocalPlaylistManager manager,
|
||||
@NonNull final PlaylistMetadataEntry playlist,
|
||||
@NonNull final List<StreamEntity> streams) {
|
||||
if (getStreams() == null) {
|
||||
if (getStreamEntities() == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user