mirror of
https://github.com/TeamNewPipe/NewPipe
synced 2026-01-14 10:42:40 +00:00
Compare commits
919 Commits
v0.9.10
...
v0.12.0-be
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
72a9940863 | ||
|
|
46e088b5f3 | ||
|
|
a3468b51e2 | ||
|
|
34f19c4268 | ||
|
|
1d2c616ce0 | ||
|
|
99e0f0c3e4 | ||
|
|
1a92dfb019 | ||
|
|
cc7f27fb53 | ||
|
|
e8402008bc | ||
|
|
c1a302834c | ||
|
|
762f374f93 | ||
|
|
e21d2bd511 | ||
|
|
d936ca6b89 | ||
|
|
88ac821070 | ||
|
|
c20837d5f5 | ||
|
|
81a4c66f92 | ||
|
|
e4dfb02cb0 | ||
|
|
0abaab4880 | ||
|
|
a1aaa52c2a | ||
|
|
b3a509ad14 | ||
|
|
ad0f58090f | ||
|
|
43c4e619c2 | ||
|
|
427397ba7b | ||
|
|
b51abf1ea6 | ||
|
|
76e082159d | ||
|
|
d36c371c1d | ||
|
|
a5b2100f8a | ||
|
|
fe19780f06 | ||
|
|
83f1d7af82 | ||
|
|
1916616b07 | ||
| 0a6a684acc | |||
|
|
6d9aecd500 | ||
|
|
4d25db2e11 | ||
|
|
77d5714059 | ||
|
|
76c59cbdea | ||
|
|
212f7dfc93 | ||
|
|
9ba37ce34c | ||
|
|
6c439bfbc4 | ||
|
|
352d0db08b | ||
|
|
be8ce1fce5 | ||
|
|
f6356e576a | ||
|
|
83a34a8ba1 | ||
|
|
fb7a855eda | ||
|
|
9c1d778623 | ||
|
|
1bad2a023d | ||
|
|
999da51e99 | ||
|
|
ea4b965eeb | ||
|
|
33160e83cb | ||
|
|
3e5e7f49cc | ||
|
|
cc02b01c2b | ||
|
|
243e5391db | ||
|
|
230ad5c04f | ||
|
|
289cfaa407 | ||
|
|
0798745c16 | ||
|
|
094695a7ff | ||
|
|
86f041b803 | ||
|
|
00e65153f4 | ||
|
|
b12f0490f3 | ||
|
|
42a2bc8a9a | ||
|
|
8adca3725d | ||
|
|
b57d4b3048 | ||
|
|
9c7aa241e4 | ||
|
|
c3ec9b2ad7 | ||
|
|
195070f8f5 | ||
|
|
cbfe91f36f | ||
|
|
738e2ac344 | ||
|
|
ba0be665ae | ||
|
|
0ba6f8b39f | ||
|
|
2773f5fbc8 | ||
|
|
263a816c3b | ||
|
|
e7d148336b | ||
|
|
829059ea01 | ||
|
|
622d698ff8 | ||
|
|
f09b04dce0 | ||
|
|
59f8583895 | ||
|
|
f506fc0478 | ||
|
|
880676d670 | ||
|
|
6485327b97 | ||
|
|
5773152ed3 | ||
|
|
e88312659b | ||
|
|
c4d0ba549f | ||
|
|
cb41afb11f | ||
|
|
6d27aea9f2 | ||
|
|
39e1f9cb76 | ||
|
|
8fb7d64f79 | ||
|
|
08fdef4870 | ||
| aa0196b9d0 | |||
|
|
a3426f92ac | ||
| d50d4254c5 | |||
|
|
50cdadc4a2 | ||
| 5aa9b6cb12 | |||
|
|
44fc8d80e0 | ||
| 817fa57bfe | |||
|
|
668e2da01b | ||
| 7f3982d153 | |||
|
|
f62ae930c7 | ||
|
|
d0808ce159 | ||
|
|
7b19dadbf5 | ||
|
|
43ab0283d9 | ||
|
|
c27e9d5901 | ||
|
|
e0d21627bb | ||
|
|
1b1dd6ef88 | ||
|
|
10700007d5 | ||
|
|
c5ec8d04c1 | ||
|
|
490b250db6 | ||
|
|
0630423c8e | ||
|
|
c2e06517e1 | ||
|
|
b01ae33d1e | ||
|
|
a55ee32058 | ||
|
|
c3941d5bec | ||
|
|
6020dc2b2d | ||
|
|
7ab41e0c3a | ||
|
|
c0a75f5b98 | ||
|
|
efd4db40ef | ||
|
|
3c3fe7bf83 | ||
|
|
268762166a | ||
|
|
53a1833e26 | ||
|
|
1ff8b5fb9f | ||
|
|
225b43ca3c | ||
|
|
75a58d6381 | ||
|
|
62814f083e | ||
|
|
6f9deea873 | ||
|
|
d3160eed9d | ||
|
|
9b4a07de34 | ||
|
|
d31eeac49e | ||
|
|
84c5d27416 | ||
|
|
17d77aa31f | ||
|
|
388ec3e3d3 | ||
|
|
f0829f9ef3 | ||
|
|
81f481833c | ||
|
|
a74c4168f3 | ||
|
|
776dbc34f7 | ||
|
|
168ac91ab8 | ||
|
|
9bd26798b6 | ||
|
|
4ae81a2de4 | ||
|
|
3c314ced0a | ||
|
|
ba9d0d7707 | ||
|
|
38946e4b0f | ||
|
|
f71242a036 | ||
|
|
960fd9be38 | ||
|
|
40844dcd76 | ||
|
|
420d28c713 | ||
|
|
5bbd6afaf1 | ||
|
|
77a06c7604 | ||
|
|
1f4f87d3bd | ||
|
|
2e8d86575e | ||
|
|
ef0659f436 | ||
|
|
e18e69966f | ||
|
|
e7b4b88055 | ||
|
|
de65d1e1fc | ||
|
|
7ea0862f95 | ||
|
|
efc7049dfd | ||
|
|
629549d76f | ||
|
|
756fb795d6 | ||
|
|
13d1974a5b | ||
|
|
d3168a9022 | ||
|
|
059378eedf | ||
|
|
e973868a90 | ||
|
|
2a0e5d6835 | ||
|
|
5537abe2c3 | ||
|
|
5eae235b3c | ||
|
|
bfc7718a21 | ||
|
|
405d6bee78 | ||
|
|
f65f2da890 | ||
|
|
30ab58c33d | ||
|
|
87ba5a7eb6 | ||
|
|
6e8593af91 | ||
|
|
0ab1d3fc40 | ||
|
|
f22d13e695 | ||
|
|
cdde61a460 | ||
|
|
989ce126f1 | ||
|
|
28618e822e | ||
|
|
6772381afc | ||
|
|
75b45beabc | ||
|
|
56d53e9b01 | ||
|
|
3a8b04e2d1 | ||
|
|
1ce7d66fb1 | ||
|
|
7b5a9b69fe | ||
|
|
837b22ccac | ||
|
|
7146719393 | ||
|
|
71ee604c69 | ||
|
|
9945a5b813 | ||
|
|
7254387042 | ||
|
|
3a7f2a94a6 | ||
|
|
6bea4aa96b | ||
|
|
fa262bbceb | ||
|
|
3139fe0170 | ||
|
|
ef6c5de65b | ||
|
|
07799563b5 | ||
|
|
b1de4b7bd6 | ||
|
|
2b8ae9a5ea | ||
|
|
7c52d3ec5d | ||
|
|
aefaa7619e | ||
|
|
ca202290bf | ||
|
|
cbdbc4cba2 | ||
|
|
8abf904a78 | ||
|
|
ecf7969c46 | ||
|
|
a473e3d623 | ||
|
|
1f8e90858e | ||
|
|
2c2edca8fa | ||
|
|
50e86ff1ca | ||
|
|
02ecc5011a | ||
|
|
6e666a018b | ||
|
|
66fbb2ce1e | ||
|
|
b00722ec0a | ||
|
|
77b1413319 | ||
|
|
87b8d60c9d | ||
|
|
db5203e1ff | ||
|
|
ea022670c4 | ||
|
|
54b009cc49 | ||
|
|
80c3acace9 | ||
|
|
cea9428b47 | ||
|
|
f8ffbfabbe | ||
|
|
836a1e652b | ||
|
|
d8544e0b84 | ||
|
|
4817d7fddc | ||
|
|
e6a385a85e | ||
|
|
3634f68364 | ||
|
|
f17ffa94fe | ||
|
|
d52bcd46a1 | ||
|
|
7e58b0b6fe | ||
|
|
f451f1f65d | ||
|
|
4d12e71fba | ||
|
|
66fde7a212 | ||
|
|
86eccf219d | ||
|
|
677865f347 | ||
|
|
eb4b3810e9 | ||
|
|
5f26501ddf | ||
|
|
b33a72f864 | ||
|
|
675f43b968 | ||
|
|
9f117a2e59 | ||
|
|
a0844229a3 | ||
|
|
114fcc144c | ||
|
|
43372ff648 | ||
|
|
b8ebbc5404 | ||
|
|
28309f82f3 | ||
|
|
c70fa391b6 | ||
|
|
caab589dce | ||
|
|
20370054e7 | ||
| f1882cb1e1 | |||
|
|
2ed149852d | ||
|
|
ac7226a0df | ||
|
|
5705650ca8 | ||
|
|
60855ca7c5 | ||
|
|
b8d8d181f3 | ||
|
|
6309160fc6 | ||
|
|
fc0e6ed273 | ||
|
|
ada0cee656 | ||
|
|
6dde524d2c | ||
|
|
6a631e1915 | ||
|
|
bb6fa343cf | ||
|
|
7557acde6c | ||
|
|
25ed8952f9 | ||
|
|
b93d94b0bd | ||
|
|
33d75fd2fb | ||
|
|
28a9855fd2 | ||
|
|
9aad07621c | ||
|
|
384cde6eaa | ||
|
|
5ae98661ad | ||
|
|
8f4d9ceca9 | ||
|
|
83d9a1233e | ||
|
|
522a287d79 | ||
|
|
e052d4660d | ||
|
|
39b0b2f032 | ||
|
|
0223d6d200 | ||
|
|
808ce72078 | ||
|
|
3a84c47176 | ||
|
|
20c2426128 | ||
|
|
bf11d4c9fa | ||
|
|
783c0f79d7 | ||
|
|
98b94bd9c4 | ||
|
|
ff0178f965 | ||
|
|
7f88c3d0a9 | ||
|
|
11e8e38f2c | ||
|
|
50c5314eaf | ||
|
|
a7a76d4f58 | ||
|
|
4df4f68fe1 | ||
|
|
7db8d37137 | ||
|
|
b7503a7d81 | ||
|
|
9f5d4034e3 | ||
|
|
3c941f6c4b | ||
|
|
ec8fff421a | ||
|
|
a47a0b5432 | ||
|
|
96ba46f21d | ||
|
|
90f48d5817 | ||
|
|
7288dd097a | ||
|
|
0119d62a35 | ||
|
|
4d6ab73fa9 | ||
| f58c95840a | |||
|
|
ecf24f81ec | ||
|
|
dd3306a940 | ||
|
|
bbb2b98f27 | ||
|
|
bc7332780d | ||
|
|
6f3bc3ac8f | ||
|
|
cd04d869b7 | ||
|
|
909e15cbdd | ||
|
|
33fa30ab78 | ||
|
|
44933ac17a | ||
|
|
bb2af96deb | ||
|
|
1fe6da14ea | ||
|
|
b7b9653c21 | ||
|
|
8adc5918f8 | ||
|
|
0db593b1bb | ||
|
|
a0c9dbeb78 | ||
|
|
61d5546d89 | ||
|
|
f97b7c943b | ||
|
|
97549b633b | ||
|
|
1949e4a9d4 | ||
|
|
ecb5f7a5ba | ||
|
|
6021f72cf0 | ||
|
|
c00ef74f96 | ||
|
|
962e070150 | ||
|
|
221cbf5e07 | ||
|
|
a72d37ab69 | ||
|
|
f92227e5df | ||
|
|
c50617452f | ||
|
|
3957eca94d | ||
|
|
70111cf614 | ||
|
|
f9e03c9a40 | ||
|
|
88fbdf1cc4 | ||
|
|
7b76bd79e8 | ||
|
|
f4b58e649d | ||
|
|
91ff301d53 | ||
|
|
f5e1c99259 | ||
|
|
abdcd3cc30 | ||
|
|
23615a39ac | ||
|
|
5fa5fc39fc | ||
|
|
01b3c7e91b | ||
|
|
e4e364af3f | ||
|
|
f2358692af | ||
|
|
26ed6299e3 | ||
|
|
3a85187111 | ||
|
|
4261a2eed3 | ||
|
|
fef17163a9 | ||
|
|
9dd447a14f | ||
|
|
ecf4407ba4 | ||
|
|
039c0d3ee6 | ||
|
|
5808aead55 | ||
|
|
e797e2e7f1 | ||
|
|
23cacbfe65 | ||
|
|
4f94ee9b72 | ||
|
|
0d1a26298a | ||
|
|
8ccd0b23e9 | ||
|
|
9d8b991354 | ||
|
|
d273f69852 | ||
|
|
eee3ccafc3 | ||
|
|
3686e90e81 | ||
|
|
f5f8371865 | ||
|
|
1191455d37 | ||
|
|
1a5d9da2bf | ||
|
|
011e151c91 | ||
|
|
54aa40eac1 | ||
|
|
21a7a73f6d | ||
|
|
5090b41eef | ||
|
|
0e55aa6249 | ||
|
|
dd2dcf4df2 | ||
|
|
2e84b28998 | ||
|
|
e6cbfea5a7 | ||
|
|
641d662944 | ||
|
|
09208e183b | ||
|
|
fba3ece688 | ||
|
|
f7d849a3cc | ||
|
|
709c700cc6 | ||
|
|
2da411c1ec | ||
|
|
d6e4f3c809 | ||
|
|
1bb08db8ba | ||
|
|
2ba116b1e6 | ||
|
|
7088de0fb9 | ||
|
|
7084f73d6c | ||
|
|
69374e25fe | ||
|
|
5e16969d61 | ||
|
|
0fe5a44e5a | ||
|
|
98e617001d | ||
|
|
979bd09b29 | ||
|
|
77678b8f31 | ||
|
|
3575cac9d7 | ||
|
|
8cdeaf1b27 | ||
|
|
08a8c6c414 | ||
|
|
fa5c1b22ae | ||
|
|
a99e7f3288 | ||
|
|
a29506ed2f | ||
|
|
1434b40d03 | ||
|
|
6d6609187b | ||
| 9682eaae2a | |||
| 1cdb4ccc17 | |||
|
|
2a878dffbc | ||
| 49f4fb7ed7 | |||
|
|
caa985660a | ||
|
|
3842a1e4fb | ||
|
|
e06d83cb93 | ||
|
|
67df894448 | ||
|
|
d1ec6cf21b | ||
|
|
537c561cee | ||
|
|
a65ddc5b36 | ||
|
|
1fb1b3a784 | ||
|
|
b80879765c | ||
|
|
10919fe15b | ||
|
|
9e5fe1edca | ||
|
|
03ee3f3d2a | ||
|
|
4113283069 | ||
|
|
56c5f696df | ||
|
|
9beb76e641 | ||
|
|
f71403be58 | ||
|
|
d0e5d36b1b | ||
|
|
fdeb7543ca | ||
|
|
90716f4f5b | ||
|
|
54d41bc288 | ||
|
|
d63c7a9042 | ||
|
|
cd5b60cbed | ||
|
|
6d60e6698a | ||
|
|
d53cb01396 | ||
|
|
8baaecab1b | ||
|
|
1368f9f89e | ||
|
|
ce36f3ae3b | ||
|
|
cf147aa161 | ||
|
|
7700cff5e5 | ||
|
|
b883f313ba | ||
|
|
b1ee22cde6 | ||
|
|
b32f149a1b | ||
|
|
1d136c6c35 | ||
|
|
b8a17580c5 | ||
|
|
87febf8679 | ||
|
|
38b2ffd450 | ||
|
|
01e031e7e7 | ||
|
|
0b1eda3050 | ||
|
|
52cdf96dfe | ||
|
|
1f5eba59c5 | ||
|
|
372c2f2be0 | ||
|
|
25e0b46396 | ||
|
|
621a1909ec | ||
|
|
d3332583b6 | ||
|
|
cb5cf9bb09 | ||
|
|
6cb2c2a84e | ||
|
|
633137fd79 | ||
|
|
9627fdf33f | ||
|
|
b39366c80a | ||
|
|
ac01c49666 | ||
|
|
80d16ea407 | ||
|
|
4f44f26333 | ||
|
|
a21bdb1487 | ||
|
|
4d6a2f40d3 | ||
|
|
e6773aac0e | ||
|
|
997381d0c3 | ||
|
|
ac53eeb76d | ||
|
|
a09c8934fc | ||
|
|
b4120c39e6 | ||
|
|
5d6320d925 | ||
|
|
985bf50f7f | ||
|
|
84d21af644 | ||
|
|
267cd99b04 | ||
|
|
401960079c | ||
|
|
1fbc8a2850 | ||
|
|
16fe5a94ac | ||
|
|
1c20a4d9eb | ||
|
|
3a9f30d954 | ||
|
|
856aacf8ce | ||
|
|
441b510775 | ||
|
|
88a10b5af1 | ||
|
|
65205ace95 | ||
|
|
1a4ef06ee9 | ||
|
|
64ac631040 | ||
|
|
4e4cabb929 | ||
|
|
b242c86869 | ||
|
|
d37fee346a | ||
|
|
cc52d3b0af | ||
|
|
d5b1bae305 | ||
|
|
04e22faf85 | ||
|
|
48cb3ed138 | ||
|
|
ebdeee8b3c | ||
| 6449d7d4ee | |||
|
|
4b775d15a2 | ||
|
|
e4d6a453b0 | ||
| 9dcbcd57cb | |||
|
|
17aa44c88b | ||
|
|
60dc266e13 | ||
|
|
60ed308caa | ||
|
|
c485e7e167 | ||
|
|
71a8361580 | ||
|
|
666dce1b6f | ||
|
|
7bed1c2295 | ||
|
|
51325089f0 | ||
|
|
90666a84ac | ||
|
|
0c37bd3a64 | ||
|
|
753517fb56 | ||
|
|
b939daac2a | ||
|
|
8f2b7b2783 | ||
|
|
883d4b4065 | ||
|
|
ff38ae202b | ||
|
|
7b71302a63 | ||
|
|
cbf8fc5bb9 | ||
|
|
00797a7834 | ||
|
|
de092e5357 | ||
|
|
bba8739008 | ||
|
|
3c5564b274 | ||
|
|
f3ff24cfbf | ||
|
|
975b519585 | ||
|
|
7f1f34f812 | ||
|
|
d5bab1006e | ||
|
|
b52e48a355 | ||
|
|
68a807a446 | ||
|
|
0dc6b66825 | ||
|
|
a9db7616aa | ||
|
|
b71e2833d6 | ||
|
|
56bc919866 | ||
|
|
ac3d8cddbe | ||
|
|
aa10b392ae | ||
|
|
e8ea1c92b1 | ||
|
|
b5d7b80fe9 | ||
|
|
2a328e28da | ||
|
|
3d1cc348c8 | ||
|
|
3d5c173d61 | ||
|
|
9a5da5199d | ||
|
|
764a171a25 | ||
|
|
25061ab07c | ||
|
|
6074925102 | ||
|
|
f8c0c449bf | ||
|
|
26d18c588e | ||
|
|
6f18dd26a2 | ||
|
|
7340bc05b4 | ||
|
|
b0948cf9fc | ||
|
|
86c16fa5d8 | ||
|
|
68695bbf92 | ||
|
|
b4fdbdeb1b | ||
|
|
1fb3774e03 | ||
|
|
f284a799ef | ||
|
|
c6e759a94c | ||
|
|
052c9a9869 | ||
|
|
0806344ffb | ||
|
|
d0e626c6ee | ||
|
|
9068247856 | ||
|
|
4553850412 | ||
|
|
21d42c92e5 | ||
|
|
eb9770e3ba | ||
|
|
d54a6e0b0e | ||
|
|
a8f5cfa640 | ||
|
|
0d3e0c201e | ||
|
|
cc4e4a4f91 | ||
|
|
b597774bb9 | ||
|
|
87fca5cffe | ||
|
|
9685456ee4 | ||
|
|
6a9e3ef639 | ||
|
|
b5a9f042cc | ||
|
|
770dcc1832 | ||
|
|
94f7baf299 | ||
|
|
77979eddde | ||
|
|
f1e52b8b92 | ||
|
|
2e414cfd63 | ||
|
|
f5b5982e1c | ||
|
|
eebd83d6ac | ||
|
|
a9aee21e58 | ||
|
|
bd9ee18e56 | ||
|
|
c75c2d0f21 | ||
|
|
80f3e3c3b6 | ||
|
|
86a1fcf009 | ||
|
|
c235c647a0 | ||
|
|
09d8ae1316 | ||
|
|
cb7e94449c | ||
|
|
e742091a37 | ||
|
|
8e3be3826f | ||
|
|
9bc95f030c | ||
|
|
9576d5bd89 | ||
|
|
a0ba3ce2e4 | ||
|
|
6b816a11f7 | ||
|
|
86c7b8522e | ||
|
|
f9eb2a1ee5 | ||
|
|
174d040ca3 | ||
|
|
6b16b08712 | ||
|
|
e9cdb28a06 | ||
|
|
f8abf92a66 | ||
|
|
9413856463 | ||
|
|
c24d46cf0f | ||
|
|
c38e4190f1 | ||
|
|
53cec61cdf | ||
|
|
150c3b413a | ||
|
|
eb15c04254 | ||
|
|
7d7a6f7ccc | ||
|
|
b54d18d888 | ||
|
|
705028c79d | ||
|
|
a91ef2ce9e | ||
|
|
73f46d3762 | ||
|
|
1ceda017c7 | ||
|
|
725cedab72 | ||
|
|
40b60e8313 | ||
|
|
5c01f04a07 | ||
|
|
183181ee54 | ||
|
|
74b58cae59 | ||
|
|
b859823011 | ||
|
|
701320b100 | ||
|
|
dcdcf17f5e | ||
|
|
7c9c3de644 | ||
|
|
cbcd281784 | ||
|
|
e70dcdc642 | ||
|
|
391d3e7fc7 | ||
|
|
02d986fc89 | ||
|
|
65a6488e44 | ||
|
|
cf703d5213 | ||
|
|
3a8437c6f3 | ||
|
|
640396da64 | ||
|
|
7e005549fe | ||
|
|
59b3362715 | ||
|
|
9dc8e47113 | ||
|
|
e958545230 | ||
|
|
76f3e170d5 | ||
|
|
a5af49dafd | ||
|
|
736d9fe450 | ||
|
|
bd7a520316 | ||
|
|
7f5b5d6f03 | ||
|
|
8d5a59e7d5 | ||
|
|
989acce0a5 | ||
|
|
641b4a806a | ||
|
|
025a44b629 | ||
|
|
dd64bf2af7 | ||
|
|
d5b3f65076 | ||
|
|
66de4cbead | ||
|
|
be3d6adf77 | ||
|
|
65e83e8fb6 | ||
|
|
7a2be6f12c | ||
|
|
0d6662b558 | ||
|
|
0a2aa54508 | ||
|
|
f6353cfb47 | ||
|
|
fb71ba3b7c | ||
|
|
ac6e086c26 | ||
|
|
4c4cfb49b4 | ||
|
|
9a073713bb | ||
|
|
4db3bd3270 | ||
|
|
d5d9ed7200 | ||
|
|
0e409bb993 | ||
|
|
89a8769399 | ||
|
|
fdfd94b9d0 | ||
|
|
ba56aec353 | ||
|
|
3ac3cedc19 | ||
|
|
2756db6601 | ||
|
|
7d296ee650 | ||
|
|
ccd26b4146 | ||
|
|
5d4269be4c | ||
|
|
54cdfc0c16 | ||
|
|
fd899a2e95 | ||
|
|
d1f446aae2 | ||
|
|
c3f04ea67d | ||
|
|
7b56aaad53 | ||
|
|
b73677fa5e | ||
|
|
0040ee5cb6 | ||
|
|
f391574113 | ||
|
|
ea863b0c24 | ||
|
|
4506c90e59 | ||
|
|
894a63ed82 | ||
|
|
0155454526 | ||
|
|
f7aafb87a8 | ||
|
|
227001ec32 | ||
|
|
d765364915 | ||
|
|
3d47e63d6f | ||
|
|
79c5c3cc57 | ||
|
|
8babbddcf9 | ||
|
|
13756508d3 | ||
|
|
b7fe001b13 | ||
|
|
35b4110a7a | ||
|
|
cdca0c6325 | ||
|
|
d928f5759f | ||
|
|
23eeb4353d | ||
|
|
8e8d74b5b7 | ||
|
|
094851cc7d | ||
|
|
f55612be40 | ||
|
|
0951f0f824 | ||
|
|
b70fd826e7 | ||
|
|
994559b39b | ||
|
|
f7534b3a0f | ||
|
|
7fcc07805a | ||
|
|
7f9f075147 | ||
|
|
cbfc359a99 | ||
|
|
907842c672 | ||
|
|
c890ab44d6 | ||
|
|
89b11ff71c | ||
|
|
3e34eeeed7 | ||
|
|
69302fcbd0 | ||
|
|
60879351a9 | ||
|
|
f4433ac508 | ||
|
|
7b7d0d6171 | ||
|
|
a6e0ed09a8 | ||
|
|
19ed6ebbaf | ||
|
|
dfeee17d39 | ||
|
|
4c429c869c | ||
|
|
6d8a361c9a | ||
|
|
825de1b6ee | ||
|
|
124461f587 | ||
|
|
a570fa6110 | ||
|
|
0e8df83bbd | ||
|
|
952c8428d8 | ||
|
|
5fe2c10aa1 | ||
|
|
1f4aa2506b | ||
|
|
9f8844fa5f | ||
|
|
990aa88e00 | ||
|
|
9e335c1894 | ||
|
|
8f2b9a6bb7 | ||
|
|
448f3e8918 | ||
|
|
62b2ab7571 | ||
|
|
46fa9a9366 | ||
|
|
29fee28d1d | ||
|
|
78cbfa20d9 | ||
|
|
ccd42d42ab | ||
|
|
abe03ef9a3 | ||
|
|
9813ffb83b | ||
|
|
b4eefa3eed | ||
|
|
3490273b49 | ||
|
|
65c8b6e66a | ||
|
|
6a166b798a | ||
|
|
0686aec606 | ||
|
|
54b3c62803 | ||
|
|
40d83ba7e7 | ||
|
|
ea3c379d88 | ||
|
|
a6ee3e99ea | ||
|
|
b2cab4aea0 | ||
|
|
2a2e532acc | ||
|
|
0954a494e7 | ||
|
|
bc32c946ff | ||
|
|
95debf66e5 | ||
|
|
3b166f82a8 | ||
|
|
7d19250565 | ||
|
|
c510a4149d | ||
|
|
33e473c509 | ||
|
|
e3dfdb0b06 | ||
|
|
efa262480a | ||
|
|
2c8dd9ce2a | ||
|
|
ead1399e7b | ||
|
|
5ebde97352 | ||
|
|
f6c624b59a | ||
|
|
4a4b1e0e49 | ||
|
|
cb5e37184a | ||
|
|
4b78a9366d | ||
|
|
90b9223aae | ||
|
|
094a3af8ae | ||
|
|
0d2296917a | ||
|
|
d6fffc7e55 | ||
|
|
283d33aa27 | ||
|
|
6c445c0833 | ||
|
|
dd10c1756f | ||
|
|
74bda719a6 | ||
|
|
ced75a9b60 | ||
|
|
5ecad47d6e | ||
|
|
6e11ca1ac4 | ||
|
|
edfdabb691 | ||
|
|
4653e94c57 | ||
|
|
54df50f84d | ||
|
|
7588c8de5a | ||
|
|
d564199e88 | ||
|
|
e458adb342 | ||
|
|
b43a7354cf | ||
|
|
6341ad88e8 | ||
|
|
314b2fb14f | ||
|
|
795ba89dc4 | ||
|
|
5b8ff28556 | ||
|
|
5e66a66111 | ||
|
|
3e6bed538a | ||
|
|
f893edeb82 | ||
|
|
64e4adef29 | ||
|
|
c1fe03aab6 | ||
|
|
25db3c2940 | ||
|
|
442290d7f0 | ||
|
|
a6eb871f5e | ||
|
|
b500c3f526 | ||
|
|
4b0a071a35 | ||
|
|
4ef69969a7 | ||
|
|
c5885d9ce4 | ||
|
|
4c2d705311 | ||
|
|
939017ada9 | ||
|
|
322c5c05cf | ||
|
|
cd174b8ce9 | ||
|
|
f5999f28e9 | ||
|
|
428a754bfc | ||
|
|
9ba2a9d907 | ||
|
|
71a22348c9 | ||
|
|
4ac0db1869 | ||
|
|
bca9035302 | ||
|
|
33b316d72f | ||
|
|
020322df0b | ||
|
|
6068043fd4 | ||
|
|
2bf4032a6f | ||
|
|
5e659e3f46 | ||
|
|
23f70d6b8b | ||
|
|
ddcf2c5206 | ||
|
|
6b88349c77 | ||
|
|
4eb2d3805d | ||
|
|
6cdddc4e9f | ||
|
|
406d844722 | ||
|
|
09ee0357c7 | ||
|
|
f603f63361 | ||
|
|
720bb5d615 | ||
|
|
eebd76661d | ||
|
|
7885ae5d3f | ||
|
|
08257cc5dd | ||
|
|
27f8144bb8 | ||
|
|
146d4a8365 | ||
|
|
bddd9b3409 | ||
|
|
0d4d83f3a6 | ||
|
|
4bb0bb4ff0 | ||
|
|
2d75968532 | ||
|
|
aa0e759168 | ||
|
|
cdae745c00 | ||
|
|
a8b6af1f03 | ||
|
|
c7106073b4 | ||
|
|
d96139798f | ||
|
|
bd560644fe | ||
|
|
f6772c4138 | ||
|
|
dd733640e5 | ||
|
|
75a0c0a527 | ||
|
|
a5925875a7 | ||
|
|
84157f9247 | ||
|
|
fcf3ed7881 | ||
|
|
dac3be69d6 | ||
|
|
5118e5cceb | ||
|
|
7ca0d20dda | ||
|
|
378222e484 | ||
|
|
812bec205c | ||
|
|
2ee8803e33 | ||
|
|
007616ca42 | ||
|
|
f3b235e4a7 | ||
| 074d4be4ba | |||
|
|
03cd48257b | ||
|
|
546db83de3 | ||
|
|
cc11a39f7e | ||
|
|
a83b365afd | ||
|
|
6a6cfac603 | ||
|
|
5270cedc56 | ||
|
|
2ac833d2a6 | ||
|
|
f7be210b12 | ||
|
|
24774d7921 | ||
|
|
2b2e954b84 | ||
|
|
85108be686 | ||
|
|
9fbac943bb | ||
|
|
d901eaa8d2 | ||
|
|
5fb0e8d007 | ||
|
|
cd5c76cb76 | ||
|
|
b2a64f8bf3 | ||
|
|
b91acc9de5 | ||
|
|
07836c7ffa | ||
|
|
8b30a0d7cc | ||
|
|
13a3e2feb2 | ||
|
|
f4dca71497 | ||
|
|
e4c6e87338 | ||
|
|
b2862520fd | ||
|
|
bb29f1ea95 | ||
|
|
69b92757f4 | ||
|
|
62ae2652c7 | ||
| 6f6633050b | |||
|
|
d37bfd6651 | ||
| 55a17a8a83 | |||
|
|
422b06b72d | ||
|
|
873a1d1c3b | ||
|
|
44d06767b5 | ||
|
|
3f4379afbf | ||
|
|
c0515de6b7 | ||
| 09159ec245 | |||
|
|
ab0bf80888 | ||
| 74b8eaed04 | |||
| d64b5ccae2 | |||
| 3883212ca7 | |||
| 1cec41dba0 | |||
| d00c0ff60c | |||
| 1d597c5f5b | |||
| c004ef7d0b | |||
|
|
1bfb977b28 | ||
|
|
04138047f6 | ||
|
|
147e99a915 | ||
|
|
6f7d162edc | ||
|
|
910aee4b73 | ||
|
|
77e376751a | ||
|
|
7e1264ac44 | ||
|
|
8f3a4a5a04 | ||
| 2d2689362d | |||
|
|
913500a6de | ||
|
|
81d7c54a55 | ||
|
|
4501d5a8fc | ||
|
|
becc90409f | ||
|
|
10c4f7b465 | ||
|
|
cb5b0ff764 | ||
|
|
277a817eaf | ||
|
|
fbac6f6a57 | ||
|
|
b1b0d6ceb6 | ||
|
|
07421542df | ||
|
|
74a8139ac9 | ||
|
|
77eb76fefb | ||
|
|
42a450c61f | ||
|
|
9e2ed10144 | ||
|
|
27e5ce7f9b | ||
|
|
f7b322da49 | ||
|
|
16ad13c962 | ||
|
|
a8880bbc0a | ||
|
|
f020b88db3 | ||
|
|
a4f5c9e2a3 | ||
|
|
96d3841dba | ||
|
|
39277d569f | ||
|
|
75b5bc03f7 | ||
|
|
01e17d9c2e | ||
|
|
8047ff96a8 | ||
|
|
0e737a2f3b | ||
|
|
854579d814 | ||
|
|
f3cbd99e45 | ||
|
|
c13ed3f347 | ||
|
|
7409b58546 | ||
|
|
404ebb8810 | ||
|
|
b4a9a300c5 | ||
|
|
aa637e6a2b | ||
|
|
85132e1e6e | ||
|
|
f0aa5a4646 | ||
|
|
b0479d0bd9 | ||
|
|
e475b888aa | ||
|
|
a8a3bbd9f5 | ||
|
|
f3cbd17598 | ||
|
|
3d7e4dc286 | ||
| 2e98729527 | |||
|
|
3ac838a7ec | ||
|
|
a1bd5bfc00 | ||
|
|
b99571a797 | ||
| 43dec8914c | |||
|
|
5b5cb78595 | ||
|
|
cb8c919609 | ||
|
|
be3810dfed | ||
|
|
b961b59ad4 | ||
|
|
d2fbb86d8a | ||
|
|
782c6211d0 |
47
.github/CONTRIBUTING.md
vendored
47
.github/CONTRIBUTING.md
vendored
@@ -1,40 +1,45 @@
|
||||
NewPipe contribution guidelines
|
||||
===============================
|
||||
|
||||
READ THIS GUIDELINES CAREFULLY BEFORE CONTRIBUTING.
|
||||
PLEASE READ THESE GUIDELINES CAREFULLY BEFORE ANY CONTRIBUTION!
|
||||
|
||||
## Crash reporting
|
||||
|
||||
Do not report crashes in the GitHub issue tracker. NewPipe has an automated crash report system that will ask you to send a report if a crash occurs.
|
||||
Do not report crashes in the GitHub issue tracker. NewPipe has an automated crash report system that will ask you to send a report via e-mail when a crash occurs. This contains all the data we need for debugging, and allows you to even add a comment to it. You'll see exactly what is sent, the system is 100% transparent.
|
||||
|
||||
## Issue reporting/feature request
|
||||
## Issue reporting/feature requests
|
||||
|
||||
* Search the [existing issues](https://github.com/theScrabi/NewPipe/issues) first to make sure your issue/feature hasn't been reported/requested before
|
||||
* Check if this issue/feature is already fixed/implemented in the repository
|
||||
* If you are an android/java developer you are always welcome to fix/implement an issue/a feature yourself
|
||||
* Use english
|
||||
* Search the [existing issues](https://github.com/TeamNewPipe/NewPipe/issues) first to make sure your issue/feature hasn't been reported/requested before
|
||||
* Check whether your issue/feature is already fixed/implemented
|
||||
* If you are an Android/Java developer, you are always welcome to fix/implement an issue/a feature yourself. PRs welcome!
|
||||
* We use English for development. Issues in other languages will be closed and ignored.
|
||||
* Please only add *one* issue at a time. Do not put multiple issues into one thread.
|
||||
|
||||
## Bugfixing
|
||||
* If you want to help NewPipe getting bug free, you can send me a mail to tnp@newpipe.schabi.org to let me know that you intent to help, and than register at our [sentry](https://support.schabi.org) setup.
|
||||
## Bug Fixing
|
||||
* If you want to help NewPipe to become free of bugs (this is our utopic goal for NewPipe), you can send us an email to tnp@newpipe.schabi.org to let me know that you intend to help. We'll send you further instructions. You may, on request, register at our [Sentry](https://sentry.schabi.org) instance (see section "Crash reporting" for more information.
|
||||
|
||||
## Translation
|
||||
|
||||
* NewPipe can be translated on [weblate](https://hosted.weblate.org/projects/newpipe/strings/)
|
||||
* NewPipe can be translated via [Weblate](https://hosted.weblate.org/projects/newpipe/strings/). You can log in there with your GitHub account.
|
||||
|
||||
## Code contribution
|
||||
|
||||
* Stick to NewPipe style guidelines (just look the other code and than do it the same way :) )
|
||||
* Do not bring nonfree software/binary blobs into the project (keep it google free)
|
||||
* Stick to [f-droid contribution guidelines](https://f-droid.org/wiki/page/Inclusion_Policy)
|
||||
* Make changes on a separate branch, not on the master branch (Feature-branching)
|
||||
* When submitting changes, you agree that your code will be licensed under GPLv3
|
||||
* Please test (compile and run) your code before you submit changes!!!
|
||||
* Try to figure out you selves why CI fails, or why a merge request collides
|
||||
* Please maintain your code after you contributed it.
|
||||
* Respond yourselves if someone request changes or notifies issues
|
||||
* Stick to NewPipe's style conventions (well, just look the other code and then do it the same way :))
|
||||
* Do not bring non-free software (e.g., binary blobs) into the project. Also, make sure you do not introduce Google libraries.
|
||||
* Stick to [F-Droid contribution guidelines](https://f-droid.org/wiki/page/Inclusion_Policy)
|
||||
* Make changes on a separate branch, not on the master branch. This is commonly known as *feature branch workflow*. You may then send your changes as a pull request on GitHub. Patches to the email address mentioned in this document might not be considered, GitHub is the primary platform.
|
||||
* When submitting changes, you confirm that your code is licensed under the terms of the [GNU General Public License v3](https://www.gnu.org/licenses/gpl-3.0.html).
|
||||
* Please test (compile and run) your code before you submit changes! Ideally, provide test feedback in the PR description. Untested code will **not** be merged!
|
||||
* Try to figure out yourself why builds on our CI fail.
|
||||
* Make sure your PR is up-to-date with the rest of the code. Often, a simple click on "Update branch" will do the job, but if not, you are asked to merge the master branch manually and resolve the problems on your own. That will make the maintainers' jobs way easier.
|
||||
* Please show intention to maintain your features and code after you contributed it. Unmaintained code is a hassle for the core developers, and just adds work. If you do not intend to maintain features you contributed, please think again about submission, or clearly state that in the description of your PR.
|
||||
* Respond yourselves if someone requests changes or otherwise raises issues about your PRs.
|
||||
* Check if your contributions align with the [fdroid inclusion guidelines](https://f-droid.org/en/docs/Inclusion_Policy/).
|
||||
* Check if your submission can be build with the current fdroid build server setup.
|
||||
|
||||
## Communication
|
||||
|
||||
* WE DO NOW HAVE A MAILING LIST: [newpipe@list.schabi.org](https://list.schabi.org/cgi-bin/mailman/listinfo/newpipe).
|
||||
* If you want to get in contact with me or one of our other contributors you can send me an email at tnp(at)schabi.org
|
||||
* Feel free to post suggestions, changes, ideas etc!
|
||||
* There is an IRC channel on Freenode which is regularly visited by the core team and other developers: [#newpipe](irc:irc.freenode.net/newpipe). [Click here for Webchat](https://webchat.freenode.net/?channels=newpipe)!
|
||||
* If you want to get in touch with the core team or one of our other contributors you can send an email to tnp(at)schabi.org. Please do not send issue reports, they will be ignored and remain unanswered! Use the GitHub issue tracker described above!
|
||||
* Feel free to post suggestions, changes, ideas etc. on GitHub, IRC or the mailing list!
|
||||
|
||||
1
.github/PULL_REQUEST_TEAMPLATE.md
vendored
1
.github/PULL_REQUEST_TEAMPLATE.md
vendored
@@ -1 +0,0 @@
|
||||
[ ] I carefully reed the [contribution guidelines](https://github.com/TeamNewPipe/NewPipe/blob/HEAD/.github/CONTRIBUTING.md) and agree to them.
|
||||
1
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
1
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
@@ -0,0 +1 @@
|
||||
- [ ] I carefully read the [contribution guidelines](https://github.com/TeamNewPipe/NewPipe/blob/HEAD/.github/CONTRIBUTING.md) and agree to them.
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -10,3 +10,4 @@
|
||||
gradle.properties
|
||||
*~
|
||||
.weblate
|
||||
*.class
|
||||
|
||||
3
.gitmodules
vendored
3
.gitmodules
vendored
@@ -1,3 +0,0 @@
|
||||
[submodule "app/src/main/java/org/schabi/newpipe/extractor"]
|
||||
path = app/src/main/java/org/schabi/newpipe/extractor
|
||||
url = https://github.com/TeamNewPipe/NewPipeExtractor.git
|
||||
|
||||
10
.travis.yml
10
.travis.yml
@@ -5,16 +5,14 @@ android:
|
||||
components:
|
||||
# The BuildTools version used by NewPipe
|
||||
- tools
|
||||
- build-tools-25.0.2
|
||||
- build-tools-27.0.1
|
||||
|
||||
# The SDK version used to compile NewPipe
|
||||
- android-25
|
||||
|
||||
# Additional components
|
||||
- extra-android-m2repository
|
||||
- android-27
|
||||
|
||||
before_install:
|
||||
- yes | sdkmanager "platforms;android-27"
|
||||
script: ./gradlew -Dorg.gradle.jvmargs=-Xmx1536m assembleDebug lintDebug testDebugUnitTest
|
||||
|
||||
licenses:
|
||||
- '.+'
|
||||
|
||||
|
||||
170
CheckTranslations.java
Normal file
170
CheckTranslations.java
Normal file
@@ -0,0 +1,170 @@
|
||||
import java.io.BufferedReader;
|
||||
import java.io.File;
|
||||
import java.io.FileReader;
|
||||
import java.io.IOException;
|
||||
import java.util.regex.*;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.ArrayList;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.charset.Charset;
|
||||
|
||||
public final class CheckTranslations {
|
||||
|
||||
private static boolean debug = false;
|
||||
private static boolean plurals = false;
|
||||
private static boolean empty = false;
|
||||
private static boolean remove = false;
|
||||
private static int checks = 0;
|
||||
private static int matches = 0;
|
||||
private static int changes = 0;
|
||||
private static Pattern p, pb, pe, e, o;
|
||||
|
||||
/**
|
||||
* Search translated strings.xml files for empty item / plural tags
|
||||
* and remove them.
|
||||
* @param args directories which contain string.xml files (in any subdirectory)
|
||||
* -e option to find all empty string tags
|
||||
* -p option to find all empty plurals and item tags
|
||||
* -r option to remove all occurrences from the files
|
||||
* -d option to see more details
|
||||
*/
|
||||
public static void main(String[] args) {
|
||||
if (args.length < 1 || (args[0].equals("-d") && args.length < 2)) {
|
||||
System.out.println("Not enough arguments");
|
||||
return;
|
||||
}
|
||||
for (int i = 0; i < args.length; i++) {
|
||||
switch (args[i]) {
|
||||
case "-d":
|
||||
debug = true;
|
||||
break;
|
||||
case "-p":
|
||||
plurals = true;
|
||||
break;
|
||||
case "-e":
|
||||
empty = true;
|
||||
break;
|
||||
case "-r":
|
||||
remove = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!plurals && !empty) {
|
||||
plurals = true;
|
||||
empty = true;
|
||||
}
|
||||
|
||||
p = Pattern.compile("(<item quantity=\")(zero|one|two|three|few|many|other)(\"></item>|\"/>)");
|
||||
pb = Pattern.compile("(<plurals[\\sa-zA-Z=\"]*>)");
|
||||
pe = Pattern.compile("(</plurals>)");
|
||||
e = Pattern.compile("(<string[\\sa-z_\\\"=]*)((><\\/string>|\\/>){1})");
|
||||
o = Pattern.compile("(<item quantity=\"other\">)[^</>]*(<\\/item>)");
|
||||
|
||||
for (int i = 0; i < args.length; i++) {
|
||||
if (!args[i].equals("-d") && !args[i].equals("-p") && !args[i].equals("-e") && !args[i].equals("-r")) {
|
||||
File f = new File(args[i]);
|
||||
if (f.exists() && !f.isDirectory()) {
|
||||
checkFile(f);
|
||||
} else if (f.isDirectory()) {
|
||||
checkFiles(f.listFiles());
|
||||
} else {
|
||||
System.out.println("'" + args[i] + "' does not exist!");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
System.out.println(checks + " files were checked.");
|
||||
System.out.println(matches + " corrupt lines detected.");
|
||||
if (remove) {
|
||||
System.out.println(matches + " corrupt lines removed and " + changes + " lines fixed.");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private static void checkFiles(File[] f) {
|
||||
for (int i = 0; i < f.length; i++) {
|
||||
if (f[i].exists() && !f[i].isDirectory()) {
|
||||
if (f[i].toString().contains("strings.xml")) {
|
||||
checkFile(f[i]);
|
||||
}
|
||||
} else if (f[i].isDirectory()) {
|
||||
checkFiles(f[i].listFiles());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void checkFile(File f) {
|
||||
// Do not check our original English strings to cause no unwanted changes
|
||||
// Btw. there should not be empty plural/item tags
|
||||
if (f.toString().contains("values/strings.xml")) {
|
||||
return;
|
||||
}
|
||||
if (debug) System.out.println("Checking " + f.toString());
|
||||
checks++;
|
||||
|
||||
|
||||
List<String> lines = new ArrayList<String>();
|
||||
boolean checkFailed = false;
|
||||
boolean otherDetected = false;
|
||||
boolean inPlurals = false;
|
||||
try (BufferedReader br = new BufferedReader(new FileReader(f))) {
|
||||
String line;
|
||||
int ln = 0;
|
||||
while ((line = br.readLine()) != null) {
|
||||
ln++;
|
||||
if (plurals && p.matcher(line).find()) {
|
||||
matches++;
|
||||
if (debug) System.out.println(" Line " + ln + " was " + ((remove) ? "removed" : "detected") + ": '" + line + "'");
|
||||
checkFailed = true;
|
||||
} else if (empty && e.matcher(line).find()) {
|
||||
matches++;
|
||||
checkFailed = true;
|
||||
if (debug) System.out.println(" Line " + ln + " was " + ((remove) ? "removed" : "detected") + ": '" + line + "'");
|
||||
} else {
|
||||
if (remove) lines.add(line);
|
||||
}
|
||||
}
|
||||
br.close();
|
||||
int pluralsLine = 0;
|
||||
for (int i = 0; i < lines.size(); i++) {
|
||||
if (o.matcher(lines.get(i)).find()) {
|
||||
otherDetected = true;
|
||||
}
|
||||
if (plurals && pb.matcher(lines.get(i)).find()) {
|
||||
inPlurals = true;
|
||||
pluralsLine = i;
|
||||
} else if (plurals && pe.matcher(lines.get(i)).find()) {
|
||||
inPlurals = false;
|
||||
if (!otherDetected) {
|
||||
boolean b = false;
|
||||
check: for(int j = pluralsLine; j < i; j++) {
|
||||
if (lines.get(j).contains("many")) {
|
||||
b = true;
|
||||
pluralsLine = j;
|
||||
break check;
|
||||
}
|
||||
}
|
||||
if (remove && b) {
|
||||
if (debug) System.out.println(" Line " + (pluralsLine + 1) + " was " + ((remove) ? "changed" : "detected") + ": '" + lines.get(pluralsLine) + "'");
|
||||
lines.set(pluralsLine, lines.get(pluralsLine).replace("many", "other"));
|
||||
changes++;
|
||||
checkFailed = true;
|
||||
} else if (debug) {
|
||||
if (debug) System.out.println(" WARNING: Line " + (i + 1) + " - No <item quantity=\"other\"> found!");
|
||||
}
|
||||
}
|
||||
otherDetected = false;
|
||||
}
|
||||
|
||||
}
|
||||
if (remove && checkFailed) {
|
||||
Files.write(f.toPath(), lines, Charset.forName("UTF-8"));
|
||||
}
|
||||
} catch (IOException e) {
|
||||
System.out.println(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
88
README.md
88
README.md
@@ -1,34 +1,34 @@
|
||||
<p align="center"><a href="https://newpipe.schabi.org"><img src="assets/new_pipe_icon_5.png" width="150"/></a></p>
|
||||
<h2 align="center"><b>NewPipe</b></h2>
|
||||
<h4 align="center">A free lightweight YouTube frontend for Android.</h4>
|
||||
<p align="center"><a href="https://f-droid.org/packages/org.schabi.newpipe/"><img src="https://f-droid.org/wiki/images/0/06/F-Droid-button_get-it-on.png"/></a></p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/TeamNewPipe/NewPipe" alt="GitHub release"><img src="https://img.shields.io/github/release/TeamNewPipe/NewPipe.svg" /></a>
|
||||
<a href="https://www.gnu.org/licenses/gpl-3.0" alt="License: GPL v3"><img src="https://img.shields.io/badge/License-GPL%20v3-blue.svg" /></a>
|
||||
<a href="https://travis-ci.org/TeamNewPipe/NewPipe" alt="Build Status"><img src="https://travis-ci.org/TeamNewPipe/NewPipe.svg" /></a>
|
||||
<a href="https://hosted.weblate.org/engage/NewPipe/" alt="Translation Status"><img src="https://hosted.weblate.org/widgets/NewPipe/-/svg-badge.svg" /></a>
|
||||
<a href="http://webchat.freenode.net/?channels=%23newpipe" alt="IRC channel: #newpipe"><img src="https://img.shields.io/badge/IRC%20chat-%23newpipe-brightgreen.svg" /></a>
|
||||
<a href="https://www.bountysource.com/teams/newpipe" alt="Bountysource bounties"><img src="https://img.shields.io/bountysource/team/newpipe/activity.svg?colorB=cd201f"/></a>
|
||||
</p>
|
||||
<hr />
|
||||
<p align="center"><a href="#screenshots">Screenshots</a> • <a href="#description">Description</a> • <a href="#features">Features</a> • <a href="#contribution">Contribution</a> • <a href="#donate">Donate</a> • <a href="#license">License</a></p>
|
||||
<p align="center"><a href="https://newpipe.schabi.org">Website</a> • <a href="https://newpipe.schabi.org/blog/">Blog</a> • <a href="https://newpipe.schabi.org/press/">Press</a></p>
|
||||
<hr />
|
||||
WARNING: PUTTING NEWPIPE OR ANY FORK OF IT INTO GOOGLE PLAYSTORE VIOLATES THEIR TERMS OF CONDITIONS.
|
||||
|
||||
# NewPipe
|
||||
NewPipe: A free lightweight Youtube frontend for Android.
|
||||
|
||||
[](https://newpipe.schabi.org)
|
||||
[](https://f-droid.org/packages/org.schabi.newpipe/)
|
||||
|
||||
|
||||
Project status:
|
||||
[](https://hosted.weblate.org/engage/NewPipe/)
|
||||
[](https://travis-ci.org/TeamNewPipe/NewPipe)
|
||||
|
||||
## Donate
|
||||

|
||||

|
||||
|
||||
`16A9J59ahMRqkLSZjhYj33n9j3fMztFxnh`
|
||||
|
||||
## Screenshots
|
||||
|
||||
[<img src="screenshots/screenshot_1.png" width=160>](screenshots/screenshot_1.png)
|
||||
[<img src="screenshots/screenshot_2.png" width=160>](screenshots/screenshot_2.png)
|
||||
[<img src="screenshots/screenshot_3.png" width=160>](screenshots/screenshot_3.png)
|
||||
[<img src="screenshots/screenshot_4.png" width=160>](screenshots/screenshot_4.png)
|
||||
[<img src="screenshots/screenshot_5.png" width=160>](screenshots/screenshot_5.png)
|
||||
[<img src="screenshots/screenshot_6.png" width=160>](screenshots/screenshot_6.png)
|
||||
[<img src="screenshots/screenshot_7.png" width=160>](screenshots/screenshot_7.png)
|
||||
[<img src="screenshots/screenshot_8.png" width=160>](screenshots/screenshot_8.png)
|
||||
[<img src="screenshots/screenshot_9.png" width=160>](screenshots/screenshot_9.png)
|
||||
|
||||
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_1.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_1.png)
|
||||
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_2.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_2.png)
|
||||
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_3.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_3.png)
|
||||
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_4.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_4.png)
|
||||
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_5.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_5.png)
|
||||
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_6.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_6.png)
|
||||
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_7.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_7.png)
|
||||
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_8.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_8.png)
|
||||
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_9.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_9.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)
|
||||
|
||||
## Description
|
||||
|
||||
@@ -39,7 +39,7 @@ NewPipe does not use any Google framework libraries, or the YouTube API. It only
|
||||
* Search videos
|
||||
* Display general information about a video
|
||||
* Watch YouTube videos
|
||||
* Listen to YouTube videos (experimental)
|
||||
* Listen to YouTube videos
|
||||
* Popup mode (floating player)
|
||||
* Select the streaming player to watch the video with
|
||||
* Download videos
|
||||
@@ -47,21 +47,23 @@ NewPipe does not use any Google framework libraries, or the YouTube API. It only
|
||||
* Open a video in Kodi
|
||||
* Show Next/Related videos
|
||||
* Search YouTube in a specific language
|
||||
* Watch age restricted material
|
||||
* Watch/Block age restricted material
|
||||
* Display general information about channels
|
||||
* Search channels
|
||||
* Watch videos from a channel
|
||||
* Orbot/Tor support (not yet directly)
|
||||
* 1080p/2k/4k support
|
||||
* View history
|
||||
* Subscribe to channels
|
||||
* Search history
|
||||
* Search/Watch Playlists
|
||||
* Watch as queues Playlists
|
||||
* Queuing videos
|
||||
|
||||
### Coming Features
|
||||
|
||||
* Multiservice support (eg. SoundCloud)
|
||||
* Bookmarks
|
||||
* View history
|
||||
* Search history
|
||||
* Subscribe to channels
|
||||
* Search/Watch Playlists
|
||||
* Queeing videos
|
||||
* Subtitles support
|
||||
* livestream support
|
||||
* ... and many more
|
||||
@@ -70,11 +72,27 @@ NewPipe does not use any Google framework libraries, or the YouTube API. It only
|
||||
Although NewPipe only supports YouTube at the moment, it's designed to support many more streaming services. The plan is, that NewPipe will get such support by the version 2.0.
|
||||
|
||||
## Contribution
|
||||
Whether you have ideas, translation, design changes, code cleaning, or real heavy code changes, help is always welcome.
|
||||
Whether you have ideas, translations, design changes, code cleaning, or real heavy code changes, help is always welcome.
|
||||
The more is done the better it gets!
|
||||
|
||||
If you'd like to get involved, check our [contribution notes](.github/CONTRIBUTING.md).
|
||||
|
||||
## Donate
|
||||
If you like NewPipe we'd be happy about a donation. You can either donate via Bitcoin or BountySource. For further information about donating to NewPipe, please visit our [website](https://newpipe.schabi.org/donate/).
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<td><img src="https://bitcoin.org/img/icons/logotop.svg" alt="Bitcoin" /></td>
|
||||
<td><img src="assets/bitcoin_qr_code.png" alt="Bitcoin QR Code" width="100px"/></td>
|
||||
<td><samp>16A9J59ahMRqkLSZjhYj33n9j3fMztFxnh</samp></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="https://www.bountysource.com/teams/newpipe"><img src="https://upload.wikimedia.org/wikipedia/commons/thumb/2/22/Bountysource.png/320px-Bountysource.png" alz="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>
|
||||
|
||||
## License
|
||||
[](http://www.gnu.org/licenses/gpl-3.0.en.html)
|
||||
|
||||
|
||||
@@ -1,23 +1,38 @@
|
||||
apply plugin: 'com.android.application'
|
||||
|
||||
android {
|
||||
compileSdkVersion 25
|
||||
buildToolsVersion '25.0.2'
|
||||
compileSdkVersion 27
|
||||
buildToolsVersion '27.0.1'
|
||||
|
||||
defaultConfig {
|
||||
applicationId "org.schabi.newpipe"
|
||||
minSdkVersion 15
|
||||
targetSdkVersion 25
|
||||
versionCode 37
|
||||
versionName "0.9.10"
|
||||
targetSdkVersion 27
|
||||
versionCode 48
|
||||
versionName "0.12.0"
|
||||
|
||||
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
|
||||
vectorDrawables.useSupportLibrary = true
|
||||
}
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled false
|
||||
minifyEnabled true
|
||||
shrinkResources true
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
debug {
|
||||
multiDexEnabled true
|
||||
|
||||
debuggable true
|
||||
applicationIdSuffix ".debug"
|
||||
}
|
||||
beta {
|
||||
minifyEnabled true
|
||||
shrinkResources true
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||
|
||||
applicationIdSuffix ".beta"
|
||||
}
|
||||
}
|
||||
|
||||
lintOptions {
|
||||
@@ -27,35 +42,58 @@ android {
|
||||
abortOnError false
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_7
|
||||
targetCompatibility JavaVersion.VERSION_1_7
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
}
|
||||
|
||||
ext {
|
||||
supportLibVersion = '27.0.2'
|
||||
}
|
||||
dependencies {
|
||||
androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2') {
|
||||
androidTestImplementation('com.android.support.test.espresso:espresso-core:2.2.2') {
|
||||
exclude module: 'support-annotations'
|
||||
}
|
||||
|
||||
implementation 'com.github.TeamNewPipe:NewPipeExtractor:7716b1437815'
|
||||
|
||||
testCompile 'junit:junit:4.12'
|
||||
testCompile 'org.mockito:mockito-core:1.10.19'
|
||||
testCompile 'org.json:json:20160810'
|
||||
testImplementation 'junit:junit:4.12'
|
||||
testImplementation 'org.mockito:mockito-core:1.10.19'
|
||||
|
||||
compile 'com.android.support:appcompat-v7:25.3.1'
|
||||
compile 'com.android.support:support-v4:25.3.1'
|
||||
compile 'com.android.support:design:25.3.1'
|
||||
compile 'com.android.support:recyclerview-v7:25.3.1'
|
||||
implementation "com.android.support:appcompat-v7:$supportLibVersion"
|
||||
implementation "com.android.support:support-v4:$supportLibVersion"
|
||||
implementation "com.android.support:design:$supportLibVersion"
|
||||
implementation "com.android.support:recyclerview-v7:$supportLibVersion"
|
||||
implementation "com.android.support:preference-v14:$supportLibVersion"
|
||||
|
||||
compile 'com.google.code.gson:gson:2.7'
|
||||
compile 'org.jsoup:jsoup:1.8.3'
|
||||
compile 'org.mozilla:rhino:1.7.7'
|
||||
compile 'ch.acra:acra:4.9.0'
|
||||
compile 'info.guardianproject.netcipher:netcipher:1.2'
|
||||
implementation 'com.google.code.gson:gson:2.8.2'
|
||||
implementation 'ch.acra:acra:4.9.2'
|
||||
|
||||
compile 'com.nostra13.universalimageloader:universal-image-loader:1.9.5'
|
||||
compile 'de.hdodenhof:circleimageview:2.0.0'
|
||||
compile 'com.github.nirhart:parallaxscroll:1.0'
|
||||
compile 'com.nononsenseapps:filepicker:3.0.0'
|
||||
compile 'com.google.android.exoplayer:exoplayer:r2.4.2'
|
||||
implementation 'com.nostra13.universalimageloader:universal-image-loader:1.9.5'
|
||||
implementation 'de.hdodenhof:circleimageview:2.2.0'
|
||||
implementation 'com.github.nirhart:ParallaxScroll:dd53d1f9d1'
|
||||
implementation 'com.nononsenseapps:filepicker:3.0.1'
|
||||
implementation 'com.google.android.exoplayer:exoplayer:2.6.0'
|
||||
|
||||
debugImplementation 'com.facebook.stetho:stetho:1.5.0'
|
||||
debugImplementation 'com.facebook.stetho:stetho-urlconnection:1.5.0'
|
||||
debugImplementation 'com.android.support:multidex:1.0.2'
|
||||
|
||||
implementation 'io.reactivex.rxjava2:rxjava:2.1.7'
|
||||
implementation 'io.reactivex.rxjava2:rxandroid:2.0.1'
|
||||
implementation 'com.jakewharton.rxbinding2:rxbinding:2.0.0'
|
||||
|
||||
implementation 'android.arch.persistence.room:runtime:1.0.0'
|
||||
implementation 'android.arch.persistence.room:rxjava2:1.0.0'
|
||||
annotationProcessor 'android.arch.persistence.room:compiler:1.0.0'
|
||||
|
||||
implementation 'frankiesardo:icepick:3.2.0'
|
||||
annotationProcessor 'frankiesardo:icepick-processor:3.2.0'
|
||||
|
||||
debugImplementation 'com.squareup.leakcanary:leakcanary-android:1.5.4'
|
||||
betaImplementation 'com.squareup.leakcanary:leakcanary-android-no-op:1.5.4'
|
||||
releaseImplementation 'com.squareup.leakcanary:leakcanary-android-no-op:1.5.4'
|
||||
|
||||
implementation 'com.squareup.okhttp3:okhttp:3.9.1'
|
||||
debugImplementation 'com.facebook.stetho:stetho-okhttp3:1.5.0'
|
||||
}
|
||||
|
||||
27
app/proguard-rules.pro
vendored
27
app/proguard-rules.pro
vendored
@@ -15,3 +15,30 @@
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
-dontobfuscate
|
||||
-keep class org.mozilla.javascript.** { *; }
|
||||
|
||||
-keep class org.mozilla.classfile.ClassFileWriter
|
||||
-keep class com.google.android.exoplayer2.** { *; }
|
||||
|
||||
-dontwarn org.mozilla.javascript.tools.**
|
||||
-dontwarn android.arch.util.paging.CountedDataSource
|
||||
-dontwarn android.arch.persistence.room.paging.LimitOffsetDataSource
|
||||
|
||||
|
||||
# Rules for icepick. Copy paste from https://github.com/frankiesardo/icepick
|
||||
-dontwarn icepick.**
|
||||
-keep class icepick.** { *; }
|
||||
-keep class **$$Icepick { *; }
|
||||
-keepclasseswithmembernames class * {
|
||||
@icepick.* <fields>;
|
||||
}
|
||||
-keepnames class * { @icepick.State *;}
|
||||
|
||||
# Rules for OkHttp. Copy paste from https://github.com/square/okhttp
|
||||
-dontwarn okhttp3.**
|
||||
-dontwarn okio.**
|
||||
-dontwarn javax.annotation.**
|
||||
# A resource is loaded with a relative path so the package of this class must be preserved.
|
||||
-keepnames class okhttp3.internal.publicsuffix.PublicSuffixDatabase
|
||||
|
||||
10
app/src/beta/AndroidManifest.xml
Normal file
10
app/src/beta/AndroidManifest.xml
Normal file
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<application
|
||||
android:label="NewPipe Beta"
|
||||
tools:replace="android:label">
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
BIN
app/src/beta/res/mipmap-hdpi/ic_launcher.png
Normal file
BIN
app/src/beta/res/mipmap-hdpi/ic_launcher.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.8 KiB |
BIN
app/src/beta/res/mipmap-mdpi/ic_launcher.png
Normal file
BIN
app/src/beta/res/mipmap-mdpi/ic_launcher.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.0 KiB |
BIN
app/src/beta/res/mipmap-xhdpi/ic_launcher.png
Normal file
BIN
app/src/beta/res/mipmap-xhdpi/ic_launcher.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 8.1 KiB |
BIN
app/src/beta/res/mipmap-xxhdpi/ic_launcher.png
Normal file
BIN
app/src/beta/res/mipmap-xxhdpi/ic_launcher.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
BIN
app/src/beta/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
BIN
app/src/beta/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 19 KiB |
17
app/src/debug/AndroidManifest.xml
Normal file
17
app/src/debug/AndroidManifest.xml
Normal file
@@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="org.schabi.newpipe">
|
||||
|
||||
<application
|
||||
android:name=".DebugApp"
|
||||
android:label="NewPipe Debug"
|
||||
tools:replace="android:name, android:label">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:label="NewPipe Debug"
|
||||
tools:replace="android:label"/>
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
104
app/src/debug/java/org/schabi/newpipe/DebugApp.java
Normal file
104
app/src/debug/java/org/schabi/newpipe/DebugApp.java
Normal file
@@ -0,0 +1,104 @@
|
||||
package org.schabi.newpipe;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.multidex.MultiDex;
|
||||
|
||||
import com.facebook.stetho.Stetho;
|
||||
import com.facebook.stetho.okhttp3.StethoInterceptor;
|
||||
import com.squareup.leakcanary.AndroidHeapDumper;
|
||||
import com.squareup.leakcanary.DefaultLeakDirectoryProvider;
|
||||
import com.squareup.leakcanary.HeapDumper;
|
||||
import com.squareup.leakcanary.LeakCanary;
|
||||
import com.squareup.leakcanary.LeakDirectoryProvider;
|
||||
import com.squareup.leakcanary.RefWatcher;
|
||||
|
||||
import org.schabi.newpipe.extractor.Downloader;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import okhttp3.OkHttpClient;
|
||||
|
||||
public class DebugApp extends App {
|
||||
private static final String TAG = DebugApp.class.toString();
|
||||
|
||||
@Override
|
||||
protected void attachBaseContext(Context base) {
|
||||
super.attachBaseContext(base);
|
||||
MultiDex.install(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
initStetho();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Downloader getDownloader() {
|
||||
return org.schabi.newpipe.Downloader.init(new OkHttpClient.Builder()
|
||||
.addNetworkInterceptor(new StethoInterceptor()));
|
||||
}
|
||||
|
||||
private void initStetho() {
|
||||
// Create an InitializerBuilder
|
||||
Stetho.InitializerBuilder initializerBuilder =
|
||||
Stetho.newInitializerBuilder(this);
|
||||
|
||||
// Enable Chrome DevTools
|
||||
initializerBuilder.enableWebKitInspector(
|
||||
Stetho.defaultInspectorModulesProvider(this)
|
||||
);
|
||||
|
||||
// Enable command line interface
|
||||
initializerBuilder.enableDumpapp(
|
||||
Stetho.defaultDumperPluginsProvider(getApplicationContext())
|
||||
);
|
||||
|
||||
// Use the InitializerBuilder to generate an Initializer
|
||||
Stetho.Initializer initializer = initializerBuilder.build();
|
||||
|
||||
// Initialize Stetho with the Initializer
|
||||
Stetho.initialize(initializer);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean isDisposedRxExceptionsReported() {
|
||||
return PreferenceManager.getDefaultSharedPreferences(this)
|
||||
.getBoolean(getString(R.string.allow_disposed_exceptions_key), false);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected RefWatcher installLeakCanary() {
|
||||
return LeakCanary.refWatcher(this)
|
||||
.heapDumper(new ToggleableHeapDumper(this))
|
||||
// give each object 10 seconds to be gc'ed, before leak canary gets nosy on it
|
||||
.watchDelay(10, TimeUnit.SECONDS)
|
||||
.buildAndInstall();
|
||||
}
|
||||
|
||||
public static class ToggleableHeapDumper implements HeapDumper {
|
||||
private final HeapDumper dumper;
|
||||
private final SharedPreferences preferences;
|
||||
private final String dumpingAllowanceKey;
|
||||
|
||||
ToggleableHeapDumper(@NonNull final Context context) {
|
||||
LeakDirectoryProvider leakDirectoryProvider = new DefaultLeakDirectoryProvider(context);
|
||||
this.dumper = new AndroidHeapDumper(context, leakDirectoryProvider);
|
||||
this.preferences = PreferenceManager.getDefaultSharedPreferences(context);
|
||||
this.dumpingAllowanceKey = context.getString(R.string.allow_heap_dumping_key);
|
||||
}
|
||||
|
||||
private boolean isDumpingAllowed() {
|
||||
return preferences.getBoolean(dumpingAllowanceKey, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public File dumpHeap() {
|
||||
return isDumpingAllowed() ? dumper.dumpHeap() : HeapDumper.RETRY_LATER;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -16,11 +16,12 @@
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:logo="@mipmap/ic_launcher"
|
||||
android:theme="@style/AppTheme"
|
||||
android:theme="@style/OpeningTheme"
|
||||
tools:ignore="AllowBackup">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:label="@string/app_name">
|
||||
android:label="@string/app_name"
|
||||
android:launchMode="singleTask">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
@@ -28,15 +29,25 @@
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".player.PlayVideoActivity"
|
||||
android:name=".player.old.PlayVideoActivity"
|
||||
android:configChanges="orientation|keyboardHidden|screenSize"
|
||||
android:theme="@style/VideoPlayerTheme"
|
||||
android:theme="@style/OldVideoPlayerTheme"
|
||||
tools:ignore="UnusedAttribute"/>
|
||||
|
||||
<service
|
||||
android:name=".player.BackgroundPlayer"
|
||||
android:exported="false"/>
|
||||
|
||||
<activity
|
||||
android:name=".player.BackgroundPlayerActivity"
|
||||
android:launchMode="singleTask"
|
||||
android:label="@string/title_activity_background_player"/>
|
||||
|
||||
<activity
|
||||
android:name=".player.PopupVideoPlayerActivity"
|
||||
android:launchMode="singleTask"
|
||||
android:label="@string/title_activity_popup_player"/>
|
||||
|
||||
<service
|
||||
android:name=".player.PopupVideoPlayer"
|
||||
android:exported="false"/>
|
||||
@@ -45,12 +56,20 @@
|
||||
android:name=".player.MainVideoPlayer"
|
||||
android:configChanges="keyboard|keyboardHidden|orientation|screenSize"
|
||||
android:label="@string/app_name"
|
||||
android:launchMode="singleTask"
|
||||
android:theme="@style/PlayerTheme"/>
|
||||
android:launchMode="singleTask"/>
|
||||
|
||||
<activity
|
||||
android:name=".settings.SettingsActivity"
|
||||
android:label="@string/settings"/>
|
||||
|
||||
<activity
|
||||
android:name=".about.AboutActivity"
|
||||
android:label="@string/title_activity_about"/>
|
||||
|
||||
<activity
|
||||
android:name=".history.HistoryActivity"
|
||||
android:label="@string/title_activity_history"/>
|
||||
|
||||
<activity
|
||||
android:name=".PanicResponderActivity"
|
||||
android:launchMode="singleInstance"
|
||||
@@ -62,6 +81,7 @@
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".ExitActivity"
|
||||
android:label="@string/general_error"
|
||||
@@ -72,16 +92,20 @@
|
||||
<activity
|
||||
android:name=".download.DownloadActivity"
|
||||
android:label="@string/app_name"
|
||||
android:launchMode="singleTask"
|
||||
android:theme="@style/AppTheme"/>
|
||||
android:launchMode="singleTask"/>
|
||||
|
||||
<service android:name="us.shandian.giga.service.DownloadManagerService"/>
|
||||
|
||||
<activity
|
||||
android:name="com.nononsenseapps.filepicker.FilePickerActivity"
|
||||
android:name=".util.FilePickerActivityHelper"
|
||||
android:label="@string/app_name"
|
||||
android:launchMode="singleTop"
|
||||
android:theme="@style/FilePickerTheme"/>
|
||||
android:theme="@style/FilePickerThemeDark">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.GET_CONTENT" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".ReCaptchaActivity"
|
||||
android:label="@string/reCaptchaActivity"/>
|
||||
@@ -98,8 +122,12 @@
|
||||
|
||||
<activity
|
||||
android:name=".RouterActivity"
|
||||
android:excludeFromRecents="true"
|
||||
android:label="@string/preferred_player_share_menu_title"
|
||||
android:taskAffinity=""
|
||||
android:theme="@android:style/Theme.NoDisplay">
|
||||
android:theme="@style/RouterActivityThemeDark">
|
||||
|
||||
<!-- Youtube filter -->
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW"/>
|
||||
<action android:name="android.media.action.MEDIA_PLAY_FROM_SEARCH"/>
|
||||
@@ -121,6 +149,8 @@
|
||||
<!-- channel prefix -->
|
||||
<data android:pathPrefix="/channel/"/>
|
||||
<data android:pathPrefix="/user/"/>
|
||||
<!-- playlist prefix -->
|
||||
<data android:pathPrefix="/playlist"/>
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW"/>
|
||||
@@ -146,20 +176,8 @@
|
||||
<data android:scheme="vnd.youtube"/>
|
||||
<data android:scheme="vnd.youtube.launch"/>
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEND"/>
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
|
||||
<data android:mimeType="text/plain"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".RouterPopupActivity"
|
||||
android:taskAffinity=""
|
||||
android:theme="@android:style/Theme.NoDisplay"
|
||||
android:label="@string/popup_mode_share_menu_title">
|
||||
<!-- Hooktube filter -->
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW"/>
|
||||
<action android:name="android.media.action.MEDIA_PLAY_FROM_SEARCH"/>
|
||||
@@ -170,15 +188,18 @@
|
||||
|
||||
<data android:scheme="http"/>
|
||||
<data android:scheme="https"/>
|
||||
<data android:host="youtube.com"/>
|
||||
<data android:host="m.youtube.com"/>
|
||||
<data android:host="www.youtube.com"/>
|
||||
<data android:host="hooktube.com"/>
|
||||
<data android:host="*.hooktube.com"/>
|
||||
<!-- video prefix -->
|
||||
<data android:pathPrefix="/v/"/>
|
||||
<data android:pathPrefix="/embed/"/>
|
||||
<data android:pathPrefix="/watch"/>
|
||||
<data android:pathPrefix="/attribution_link"/>
|
||||
<!-- channel prefix -->
|
||||
<data android:pathPrefix="/channel/"/>
|
||||
<data android:pathPrefix="/user/"/>
|
||||
</intent-filter>
|
||||
|
||||
<!-- Soundcloud filter -->
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW"/>
|
||||
<action android:name="android.media.action.MEDIA_PLAY_FROM_SEARCH"/>
|
||||
@@ -189,31 +210,22 @@
|
||||
|
||||
<data android:scheme="http"/>
|
||||
<data android:scheme="https"/>
|
||||
<data android:host="youtu.be"/>
|
||||
<data android:host="soundcloud.com"/>
|
||||
<data android:host="m.soundcloud.com"/>
|
||||
<data android:host="www.soundcloud.com"/>
|
||||
<data android:pathPrefix="/"/>
|
||||
</intent-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="vnd.youtube"/>
|
||||
<data android:scheme="vnd.youtube.launch"/>
|
||||
</intent-filter>
|
||||
<!-- Share filter -->
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEND"/>
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
<data android:mimeType="text/plain"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".about.AboutActivity"
|
||||
android:label="@string/title_activity_about"
|
||||
android:theme="@style/AppTheme">
|
||||
</activity>
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
<service
|
||||
android:name=".RouterActivity$FetcherService"
|
||||
android:exported="false"/>
|
||||
</application>
|
||||
</manifest>
|
||||
@@ -15,9 +15,9 @@ Version 2, June 1991
|
||||
</p>
|
||||
|
||||
<pre>
|
||||
Copyright (C) 1989, 1991 Free Software Foundation, Inc.
|
||||
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
|
||||
|
||||
Copyright (C) 1989, 1991 Free Software Foundation, Inc.<br/>
|
||||
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA<br/>
|
||||
<br/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
</pre>
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
||||
<title>Mozilla Public License, version 2.0</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1 id="mozilla-public-license-version-2.0">Mozilla Public License<br>Version 2.0</h1>
|
||||
<h2 id="definitions">1. Definitions</h2>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package org.schabi.newpipe;
|
||||
|
||||
/**
|
||||
/*
|
||||
* Created by Christian Schabesberger on 24.12.15.
|
||||
*
|
||||
* Copyright (C) Christian Schabesberger 2015 <chris.schabesberger@mailbox.org>
|
||||
|
||||
@@ -1,27 +1,48 @@
|
||||
package org.schabi.newpipe;
|
||||
|
||||
import android.app.Application;
|
||||
import android.app.NotificationChannel;
|
||||
import android.app.NotificationManager;
|
||||
import android.content.Context;
|
||||
import android.os.Build;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.util.Log;
|
||||
|
||||
import com.nostra13.universalimageloader.cache.memory.impl.LRULimitedMemoryCache;
|
||||
import com.nostra13.universalimageloader.core.ImageLoader;
|
||||
import com.nostra13.universalimageloader.core.ImageLoaderConfiguration;
|
||||
import com.squareup.leakcanary.LeakCanary;
|
||||
import com.squareup.leakcanary.RefWatcher;
|
||||
|
||||
import org.acra.ACRA;
|
||||
import org.acra.config.ACRAConfiguration;
|
||||
import org.acra.config.ACRAConfigurationException;
|
||||
import org.acra.config.ConfigurationBuilder;
|
||||
import org.acra.sender.ReportSenderFactory;
|
||||
import org.schabi.newpipe.extractor.Downloader;
|
||||
import org.schabi.newpipe.extractor.NewPipe;
|
||||
import org.schabi.newpipe.report.AcraReportSenderFactory;
|
||||
import org.schabi.newpipe.report.ErrorActivity;
|
||||
import org.schabi.newpipe.report.UserAction;
|
||||
import org.schabi.newpipe.settings.SettingsActivity;
|
||||
import org.schabi.newpipe.util.ThemeHelper;
|
||||
import org.schabi.newpipe.util.ExtractorHelper;
|
||||
import org.schabi.newpipe.util.StateSaver;
|
||||
|
||||
import info.guardianproject.netcipher.NetCipher;
|
||||
import info.guardianproject.netcipher.proxy.OrbotHelper;
|
||||
import java.io.IOException;
|
||||
import java.io.InterruptedIOException;
|
||||
import java.net.SocketException;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
import io.reactivex.annotations.NonNull;
|
||||
import io.reactivex.exceptions.CompositeException;
|
||||
import io.reactivex.exceptions.MissingBackpressureException;
|
||||
import io.reactivex.exceptions.OnErrorNotImplementedException;
|
||||
import io.reactivex.exceptions.UndeliverableException;
|
||||
import io.reactivex.functions.Consumer;
|
||||
import io.reactivex.plugins.RxJavaPlugins;
|
||||
|
||||
/*
|
||||
* Copyright (C) Hans-Christoph Steiner 2016 <hans@eds.org>
|
||||
* App.java is part of NewPipe.
|
||||
*
|
||||
@@ -40,73 +61,160 @@ import info.guardianproject.netcipher.proxy.OrbotHelper;
|
||||
*/
|
||||
|
||||
public class App extends Application {
|
||||
private static final String TAG = App.class.toString();
|
||||
protected static final String TAG = App.class.toString();
|
||||
private RefWatcher refWatcher;
|
||||
|
||||
private static boolean useTor;
|
||||
@SuppressWarnings("unchecked")
|
||||
private static final Class<? extends ReportSenderFactory>[] reportSenderFactoryClasses = new Class[]{AcraReportSenderFactory.class};
|
||||
|
||||
final Class<? extends ReportSenderFactory>[] reportSenderFactoryClasses
|
||||
= new Class[]{AcraReportSenderFactory.class};
|
||||
@Override
|
||||
protected void attachBaseContext(Context base) {
|
||||
super.attachBaseContext(base);
|
||||
|
||||
initACRA();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
|
||||
// init crashreport
|
||||
if (LeakCanary.isInAnalyzerProcess(this)) {
|
||||
// This process is dedicated to LeakCanary for heap analysis.
|
||||
// You should not init your app in this process.
|
||||
return;
|
||||
}
|
||||
refWatcher = installLeakCanary();
|
||||
|
||||
// Initialize settings first because others inits can use its values
|
||||
SettingsActivity.initSettings(this);
|
||||
|
||||
NewPipe.init(getDownloader());
|
||||
NewPipeDatabase.init(this);
|
||||
StateSaver.init(this);
|
||||
initNotificationChannel();
|
||||
|
||||
// Initialize image loader
|
||||
ImageLoader.getInstance().init(getImageLoaderConfigurations(10, 50));
|
||||
|
||||
configureRxJavaErrorHandler();
|
||||
}
|
||||
|
||||
protected Downloader getDownloader() {
|
||||
return org.schabi.newpipe.Downloader.init(null);
|
||||
}
|
||||
|
||||
private void configureRxJavaErrorHandler() {
|
||||
// https://github.com/ReactiveX/RxJava/wiki/What's-different-in-2.0#error-handling
|
||||
RxJavaPlugins.setErrorHandler(new Consumer<Throwable>() {
|
||||
@Override
|
||||
public void accept(@NonNull Throwable throwable) throws Exception {
|
||||
Log.e(TAG, "RxJavaPlugins.ErrorHandler called with -> : " +
|
||||
"throwable = [" + throwable.getClass().getName() + "]");
|
||||
|
||||
if (throwable instanceof UndeliverableException) {
|
||||
// As UndeliverableException is a wrapper, get the cause of it to get the "real" exception
|
||||
throwable = throwable.getCause();
|
||||
}
|
||||
|
||||
final List<Throwable> errors;
|
||||
if (throwable instanceof CompositeException) {
|
||||
errors = ((CompositeException) throwable).getExceptions();
|
||||
} else {
|
||||
errors = Collections.singletonList(throwable);
|
||||
}
|
||||
|
||||
for (final Throwable error : errors) {
|
||||
if (isThrowableIgnored(error)) return;
|
||||
if (isThrowableCritical(error)) {
|
||||
reportException(error);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Out-of-lifecycle exceptions should only be reported if a debug user wishes so,
|
||||
// When exception is not reported, log it
|
||||
if (isDisposedRxExceptionsReported()) {
|
||||
reportException(throwable);
|
||||
} else {
|
||||
Log.e(TAG, "RxJavaPlugin: Undeliverable Exception received: ", throwable);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isThrowableIgnored(@NonNull final Throwable throwable) {
|
||||
// Don't crash the application over a simple network problem
|
||||
return ExtractorHelper.hasAssignableCauseThrowable(throwable,
|
||||
IOException.class, SocketException.class, // network api cancellation
|
||||
InterruptedException.class, InterruptedIOException.class); // blocking code disposed
|
||||
}
|
||||
|
||||
private boolean isThrowableCritical(@NonNull final Throwable throwable) {
|
||||
// Though these exceptions cannot be ignored
|
||||
return ExtractorHelper.hasAssignableCauseThrowable(throwable,
|
||||
NullPointerException.class, IllegalArgumentException.class, // bug in app
|
||||
OnErrorNotImplementedException.class, MissingBackpressureException.class,
|
||||
IllegalStateException.class); // bug in operator
|
||||
}
|
||||
|
||||
private void reportException(@NonNull final Throwable throwable) {
|
||||
// Throw uncaught exception that will trigger the report system
|
||||
Thread.currentThread().getUncaughtExceptionHandler()
|
||||
.uncaughtException(Thread.currentThread(), throwable);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private ImageLoaderConfiguration getImageLoaderConfigurations(final int memoryCacheSizeMb,
|
||||
final int diskCacheSizeMb) {
|
||||
return new ImageLoaderConfiguration.Builder(this)
|
||||
.memoryCache(new LRULimitedMemoryCache(memoryCacheSizeMb * 1024 * 1024))
|
||||
.diskCacheSize(diskCacheSizeMb * 1024 * 1024)
|
||||
.build();
|
||||
}
|
||||
|
||||
private void initACRA() {
|
||||
try {
|
||||
final ACRAConfiguration acraConfig = new ConfigurationBuilder(this)
|
||||
.setReportSenderFactoryClasses(reportSenderFactoryClasses)
|
||||
.setBuildConfigClass(BuildConfig.class)
|
||||
.build();
|
||||
ACRA.init(this, acraConfig);
|
||||
} catch(ACRAConfigurationException ace) {
|
||||
} catch (ACRAConfigurationException ace) {
|
||||
ace.printStackTrace();
|
||||
ErrorActivity.reportError(this, ace, null, null,
|
||||
ErrorActivity.ErrorInfo.make(UserAction.SEARCHED,"none",
|
||||
"Could not initialize ACRA crash report", R.string.app_ui_crash));
|
||||
}
|
||||
|
||||
//init NewPipe
|
||||
NewPipe.init(Downloader.getInstance());
|
||||
|
||||
// Initialize image loader
|
||||
ImageLoaderConfiguration config = new ImageLoaderConfiguration.Builder(this).build();
|
||||
ImageLoader.getInstance().init(config);
|
||||
|
||||
/*
|
||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
|
||||
if(prefs.getBoolean(getString(R.string.use_tor_key), false)) {
|
||||
OrbotHelper.requestStartTor(this);
|
||||
configureTor(true);
|
||||
} else {
|
||||
configureTor(false);
|
||||
}*/
|
||||
configureTor(false);
|
||||
|
||||
// DO NOT REMOVE THIS FUNCTION!!!
|
||||
// Otherwise downloadPathPreference has invalid value.
|
||||
SettingsActivity.initSettings(this);
|
||||
|
||||
ThemeHelper.setTheme(getApplicationContext());
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the proxy settings based on whether Tor should be enabled or not.
|
||||
*/
|
||||
public static void configureTor(boolean enabled) {
|
||||
useTor = enabled;
|
||||
if (useTor) {
|
||||
NetCipher.useTor();
|
||||
} else {
|
||||
NetCipher.setProxy(null);
|
||||
ErrorActivity.reportError(this, ace, null, null, ErrorActivity.ErrorInfo.make(UserAction.SOMETHING_ELSE, "none",
|
||||
"Could not initialize ACRA crash report", R.string.app_ui_crash));
|
||||
}
|
||||
}
|
||||
|
||||
public static void checkStartTor(Context context) {
|
||||
if (useTor) {
|
||||
OrbotHelper.requestStartTor(context);
|
||||
public void initNotificationChannel() {
|
||||
if (Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.O) {
|
||||
return;
|
||||
}
|
||||
|
||||
final String id = getString(R.string.notification_channel_id);
|
||||
final CharSequence name = getString(R.string.notification_channel_name);
|
||||
final String description = getString(R.string.notification_channel_description);
|
||||
|
||||
// Keep this below DEFAULT to avoid making noise on every notification update
|
||||
final int importance = NotificationManager.IMPORTANCE_LOW;
|
||||
|
||||
NotificationChannel mChannel = new NotificationChannel(id, name, importance);
|
||||
mChannel.setDescription(description);
|
||||
|
||||
NotificationManager mNotificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
|
||||
mNotificationManager.createNotificationChannel(mChannel);
|
||||
}
|
||||
|
||||
public static boolean isUsingTor() {
|
||||
return useTor;
|
||||
@Nullable
|
||||
public static RefWatcher getRefWatcher(Context context) {
|
||||
final App application = (App) context.getApplicationContext();
|
||||
return application.refWatcher;
|
||||
}
|
||||
|
||||
protected RefWatcher installLeakCanary() {
|
||||
return RefWatcher.DISABLED;
|
||||
}
|
||||
|
||||
protected boolean isDisposedRxExceptionsReported() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
119
app/src/main/java/org/schabi/newpipe/BaseFragment.java
Normal file
119
app/src/main/java/org/schabi/newpipe/BaseFragment.java
Normal file
@@ -0,0 +1,119 @@
|
||||
package org.schabi.newpipe;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.TypedArray;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.AttrRes;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.v4.app.Fragment;
|
||||
import android.support.v7.app.AppCompatActivity;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
|
||||
import com.nostra13.universalimageloader.core.DisplayImageOptions;
|
||||
import com.nostra13.universalimageloader.core.ImageLoader;
|
||||
import com.nostra13.universalimageloader.core.display.FadeInBitmapDisplayer;
|
||||
import com.squareup.leakcanary.RefWatcher;
|
||||
|
||||
import icepick.Icepick;
|
||||
|
||||
public abstract class BaseFragment extends Fragment {
|
||||
protected final String TAG = getClass().getSimpleName() + "@" + Integer.toHexString(hashCode());
|
||||
protected boolean DEBUG = MainActivity.DEBUG;
|
||||
|
||||
protected AppCompatActivity activity;
|
||||
public static final ImageLoader imageLoader = ImageLoader.getInstance();
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Fragment's Lifecycle
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public void onAttach(Context context) {
|
||||
super.onAttach(context);
|
||||
activity = (AppCompatActivity) context;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDetach() {
|
||||
super.onDetach();
|
||||
activity = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
if (DEBUG) Log.d(TAG, "onCreate() called with: savedInstanceState = [" + savedInstanceState + "]");
|
||||
super.onCreate(savedInstanceState);
|
||||
Icepick.restoreInstanceState(this, savedInstanceState);
|
||||
if (savedInstanceState != null) onRestoreInstanceState(savedInstanceState);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void onViewCreated(View rootView, Bundle savedInstanceState) {
|
||||
super.onViewCreated(rootView, savedInstanceState);
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "onViewCreated() called with: rootView = [" + rootView + "], savedInstanceState = [" + savedInstanceState + "]");
|
||||
}
|
||||
initViews(rootView, savedInstanceState);
|
||||
initListeners();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSaveInstanceState(Bundle outState) {
|
||||
super.onSaveInstanceState(outState);
|
||||
Icepick.saveInstanceState(this, outState);
|
||||
}
|
||||
|
||||
protected void onRestoreInstanceState(@NonNull Bundle savedInstanceState) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
|
||||
RefWatcher refWatcher = App.getRefWatcher(getActivity());
|
||||
if (refWatcher != null) refWatcher.watch(this);
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Init
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
protected void initViews(View rootView, Bundle savedInstanceState) {
|
||||
}
|
||||
|
||||
protected void initListeners() {
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// DisplayImageOptions default configurations
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
public static final DisplayImageOptions BASE_OPTIONS =
|
||||
new DisplayImageOptions.Builder().cacheInMemory(true).build();
|
||||
|
||||
public static final DisplayImageOptions DISPLAY_AVATAR_OPTIONS =
|
||||
new DisplayImageOptions.Builder()
|
||||
.cloneFrom(BASE_OPTIONS)
|
||||
.showImageOnLoading(R.drawable.buddy)
|
||||
.showImageForEmptyUri(R.drawable.buddy)
|
||||
.showImageOnFail(R.drawable.buddy)
|
||||
.build();
|
||||
|
||||
public static final DisplayImageOptions DISPLAY_THUMBNAIL_OPTIONS =
|
||||
new DisplayImageOptions.Builder()
|
||||
.cloneFrom(BASE_OPTIONS)
|
||||
.displayer(new FadeInBitmapDisplayer(250))
|
||||
.showImageForEmptyUri(R.drawable.dummy_thumbnail)
|
||||
.showImageOnFail(R.drawable.dummy_thumbnail)
|
||||
.build();
|
||||
|
||||
public static final DisplayImageOptions DISPLAY_BANNER_OPTIONS =
|
||||
new DisplayImageOptions.Builder()
|
||||
.cloneFrom(BASE_OPTIONS)
|
||||
.showImageOnLoading(R.drawable.channel_banner)
|
||||
.showImageForEmptyUri(R.drawable.channel_banner)
|
||||
.showImageOnFail(R.drawable.channel_banner)
|
||||
.build();
|
||||
}
|
||||
@@ -1,20 +1,23 @@
|
||||
package org.schabi.newpipe;
|
||||
|
||||
import android.support.annotation.Nullable;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import org.schabi.newpipe.extractor.exceptions.ReCaptchaException;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.net.URL;
|
||||
import java.net.UnknownHostException;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import javax.net.ssl.HttpsURLConnection;
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.Response;
|
||||
import okhttp3.ResponseBody;
|
||||
|
||||
|
||||
/**
|
||||
/*
|
||||
* Created by Christian Schabesberger on 28.01.16.
|
||||
*
|
||||
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
|
||||
@@ -35,112 +38,103 @@ import javax.net.ssl.HttpsURLConnection;
|
||||
*/
|
||||
|
||||
public class Downloader implements org.schabi.newpipe.extractor.Downloader {
|
||||
|
||||
private static final String USER_AGENT = "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:43.0) Gecko/20100101 Firefox/43.0";
|
||||
private static String mCookies = "";
|
||||
public static final String USER_AGENT = "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:43.0) Gecko/20100101 Firefox/43.0";
|
||||
|
||||
private static Downloader instance = null;
|
||||
private static Downloader instance;
|
||||
private String mCookies;
|
||||
private OkHttpClient client;
|
||||
|
||||
private Downloader() {}
|
||||
private Downloader(OkHttpClient.Builder builder) {
|
||||
this.client = builder
|
||||
.readTimeout(30, TimeUnit.SECONDS)
|
||||
//.cache(new Cache(new File(context.getExternalCacheDir(), "okhttp"), 16 * 1024 * 1024))
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* It's recommended to call exactly once in the entire lifetime of the application.
|
||||
*
|
||||
* @param builder if null, default builder will be used
|
||||
*/
|
||||
public static Downloader init(@Nullable OkHttpClient.Builder builder) {
|
||||
return instance = new Downloader(builder != null ? builder : new OkHttpClient.Builder());
|
||||
}
|
||||
|
||||
public static Downloader getInstance() {
|
||||
if(instance == null) {
|
||||
synchronized (Downloader.class) {
|
||||
if (instance == null) {
|
||||
instance = new Downloader();
|
||||
}
|
||||
}
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
|
||||
public static synchronized void setCookies(String cookies) {
|
||||
Downloader.mCookies = cookies;
|
||||
public String getCookies() {
|
||||
return mCookies;
|
||||
}
|
||||
|
||||
public static synchronized String getCookies() {
|
||||
return Downloader.mCookies;
|
||||
public void setCookies(String cookies) {
|
||||
mCookies = cookies;
|
||||
}
|
||||
|
||||
/**Download the text file at the supplied URL as in download(String),
|
||||
/**
|
||||
* Download the text file at the supplied URL as in download(String),
|
||||
* but set the HTTP header field "Accept-Language" to the supplied string.
|
||||
* @param siteUrl the URL of the text file to return the contents of
|
||||
*
|
||||
* @param siteUrl the URL of the text file to return the contents of
|
||||
* @param language the language (usually a 2-character code) to set as the preferred language
|
||||
* @return the contents of the specified text file*/
|
||||
* @return the contents of the specified text file
|
||||
*/
|
||||
@Override
|
||||
public String download(String siteUrl, String language) throws IOException, ReCaptchaException {
|
||||
Map<String, String> requestProperties = new HashMap<>();
|
||||
requestProperties.put("Accept-Language", language);
|
||||
return download(siteUrl, requestProperties);
|
||||
}
|
||||
|
||||
|
||||
/**Download the text file at the supplied URL as in download(String),
|
||||
* but set the HTTP header field "Accept-Language" to the supplied string.
|
||||
* @param siteUrl the URL of the text file to return the contents of
|
||||
/**
|
||||
* Download the text file at the supplied URL as in download(String),
|
||||
* but set the HTTP headers included in the customProperties map.
|
||||
*
|
||||
* @param siteUrl the URL of the text file to return the contents of
|
||||
* @param customProperties set request header properties
|
||||
* @return the contents of the specified text file
|
||||
* @throws IOException*/
|
||||
* @throws IOException
|
||||
*/
|
||||
@Override
|
||||
public String download(String siteUrl, Map<String, String> customProperties) throws IOException, ReCaptchaException {
|
||||
URL url = new URL(siteUrl);
|
||||
HttpsURLConnection con = (HttpsURLConnection) url.openConnection();
|
||||
Iterator it = customProperties.entrySet().iterator();
|
||||
while(it.hasNext()) {
|
||||
Map.Entry pair = (Map.Entry)it.next();
|
||||
con.setRequestProperty((String)pair.getKey(), (String)pair.getValue());
|
||||
}
|
||||
return dl(con);
|
||||
}
|
||||
final Request.Builder requestBuilder = new Request.Builder()
|
||||
.method("GET", null).url(siteUrl)
|
||||
.addHeader("User-Agent", USER_AGENT);
|
||||
|
||||
/**Common functionality between download(String url) and download(String url, String language)*/
|
||||
private static String dl(HttpsURLConnection con) throws IOException, ReCaptchaException {
|
||||
StringBuilder response = new StringBuilder();
|
||||
BufferedReader in = null;
|
||||
|
||||
try {
|
||||
con.setRequestMethod("GET");
|
||||
con.setRequestProperty("User-Agent", USER_AGENT);
|
||||
|
||||
if (getCookies().length() > 0) {
|
||||
con.setRequestProperty("Cookie", getCookies());
|
||||
}
|
||||
|
||||
in = new BufferedReader(
|
||||
new InputStreamReader(con.getInputStream()));
|
||||
String inputLine;
|
||||
|
||||
while((inputLine = in.readLine()) != null) {
|
||||
response.append(inputLine);
|
||||
}
|
||||
} catch(UnknownHostException uhe) {//thrown when there's no internet connection
|
||||
throw new IOException("unknown host or no network", uhe);
|
||||
//Toast.makeText(getActivity(), uhe.getMessage(), Toast.LENGTH_LONG).show();
|
||||
} catch(Exception e) {
|
||||
/*
|
||||
* HTTP 429 == Too Many Request
|
||||
* Receive from Youtube.com = ReCaptcha challenge request
|
||||
* See : https://github.com/rg3/youtube-dl/issues/5138
|
||||
*/
|
||||
if (con.getResponseCode() == 429) {
|
||||
throw new ReCaptchaException("reCaptcha Challenge requested");
|
||||
}
|
||||
throw new IOException(e);
|
||||
} finally {
|
||||
if(in != null) {
|
||||
in.close();
|
||||
}
|
||||
for (Map.Entry<String, String> header : customProperties.entrySet()) {
|
||||
requestBuilder.addHeader(header.getKey(), header.getValue());
|
||||
}
|
||||
|
||||
return response.toString();
|
||||
if (!TextUtils.isEmpty(mCookies)) {
|
||||
requestBuilder.addHeader("Cookie", mCookies);
|
||||
}
|
||||
|
||||
final Request request = requestBuilder.build();
|
||||
final Response response = client.newCall(request).execute();
|
||||
final ResponseBody body = response.body();
|
||||
|
||||
if (response.code() == 429) {
|
||||
throw new ReCaptchaException("reCaptcha Challenge requested");
|
||||
}
|
||||
|
||||
if (body == null) {
|
||||
response.close();
|
||||
return null;
|
||||
}
|
||||
|
||||
return body.string();
|
||||
}
|
||||
|
||||
/**Download (via HTTP) the text file located at the supplied URL, and return its contents.
|
||||
/**
|
||||
* Download (via HTTP) the text file located at the supplied URL, and return its contents.
|
||||
* Primarily intended for downloading web pages.
|
||||
*
|
||||
* @param siteUrl the URL of the text file to download
|
||||
* @return the contents of the specified text file*/
|
||||
* @return the contents of the specified text file
|
||||
*/
|
||||
@Override
|
||||
public String download(String siteUrl) throws IOException, ReCaptchaException {
|
||||
URL url = new URL(siteUrl);
|
||||
HttpsURLConnection con = (HttpsURLConnection) url.openConnection();
|
||||
//HttpsURLConnection con = NetCipher.getHttpsURLConnection(url);
|
||||
return dl(con);
|
||||
return download(siteUrl, Collections.emptyMap());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import android.content.Intent;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
|
||||
/**
|
||||
/*
|
||||
* Copyright (C) Hans-Christoph Steiner 2016 <hans@eds.org>
|
||||
* ExitActivity.java is part of NewPipe.
|
||||
*
|
||||
|
||||
@@ -1,65 +0,0 @@
|
||||
package org.schabi.newpipe;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Bitmap;
|
||||
import android.view.View;
|
||||
|
||||
import com.nostra13.universalimageloader.core.assist.FailReason;
|
||||
import com.nostra13.universalimageloader.core.listener.ImageLoadingListener;
|
||||
|
||||
import org.schabi.newpipe.extractor.NewPipe;
|
||||
import org.schabi.newpipe.report.ErrorActivity;
|
||||
import org.schabi.newpipe.report.UserAction;
|
||||
|
||||
/**
|
||||
* Created by Christian Schabesberger on 01.08.16.
|
||||
*
|
||||
* Copyright (C) Christian Schabesberger 2015 <chris.schabesberger@mailbox.org>
|
||||
* StreamInfoItemViewCreator.java is part of NewPipe.
|
||||
*
|
||||
* NewPipe is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* NewPipe is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
public class ImageErrorLoadingListener implements ImageLoadingListener {
|
||||
|
||||
private int serviceId = -1;
|
||||
private Context context = null;
|
||||
private View rootView = null;
|
||||
|
||||
public ImageErrorLoadingListener(Context context, View rootView, int serviceId) {
|
||||
this.context = context;
|
||||
this.serviceId= serviceId;
|
||||
this.rootView = rootView;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadingStarted(String imageUri, View view) {}
|
||||
|
||||
@Override
|
||||
public void onLoadingFailed(String imageUri, View view, FailReason failReason) {
|
||||
ErrorActivity.reportError(context,
|
||||
failReason.getCause(), null, rootView,
|
||||
ErrorActivity.ErrorInfo.make(UserAction.LOAD_IMAGE,
|
||||
NewPipe.getNameOfService(serviceId), imageUri,
|
||||
R.string.could_not_load_image));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadingComplete(String imageUri, View view, Bitmap loadedImage) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadingCancelled(String imageUri, View view) {}
|
||||
}
|
||||
@@ -22,31 +22,51 @@ package org.schabi.newpipe;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.support.design.widget.NavigationView;
|
||||
import android.support.v4.app.Fragment;
|
||||
import android.support.v4.view.GravityCompat;
|
||||
import android.support.v4.widget.DrawerLayout;
|
||||
import android.support.v7.app.ActionBar;
|
||||
import android.support.v7.app.ActionBarDrawerToggle;
|
||||
import android.support.v7.app.AppCompatActivity;
|
||||
import android.support.v7.widget.Toolbar;
|
||||
import android.util.Log;
|
||||
import android.view.Gravity;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.widget.Button;
|
||||
import android.widget.ImageButton;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import org.schabi.newpipe.download.DownloadActivity;
|
||||
import org.schabi.newpipe.extractor.NewPipe;
|
||||
import org.schabi.newpipe.extractor.StreamingService;
|
||||
import org.schabi.newpipe.fragments.BackPressable;
|
||||
import org.schabi.newpipe.fragments.MainFragment;
|
||||
import org.schabi.newpipe.fragments.detail.VideoDetailFragment;
|
||||
import org.schabi.newpipe.fragments.search.SearchFragment;
|
||||
import org.schabi.newpipe.settings.SettingsActivity;
|
||||
import org.schabi.newpipe.fragments.list.search.SearchFragment;
|
||||
import org.schabi.newpipe.report.ErrorActivity;
|
||||
import org.schabi.newpipe.util.Constants;
|
||||
import org.schabi.newpipe.util.NavigationHelper;
|
||||
import org.schabi.newpipe.util.PermissionHelper;
|
||||
import org.schabi.newpipe.util.ServiceHelper;
|
||||
import org.schabi.newpipe.util.StateSaver;
|
||||
import org.schabi.newpipe.util.ThemeHelper;
|
||||
|
||||
public class MainActivity extends AppCompatActivity {
|
||||
private static final String TAG = "MainActivity";
|
||||
public static final boolean DEBUG = false;
|
||||
public static final boolean DEBUG = !BuildConfig.BUILD_TYPE.equals("release");
|
||||
|
||||
private ActionBarDrawerToggle toggle = null;
|
||||
private DrawerLayout drawer = null;
|
||||
private NavigationView drawerItems = null;
|
||||
private TextView headerServiceView = null;
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Activity's LifeCycle
|
||||
@@ -55,7 +75,9 @@ public class MainActivity extends AppCompatActivity {
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
if (DEBUG) Log.d(TAG, "onCreate() called with: savedInstanceState = [" + savedInstanceState + "]");
|
||||
ThemeHelper.setTheme(this);
|
||||
|
||||
ThemeHelper.setTheme(this, ServiceHelper.getSelectedServiceId(this));
|
||||
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_main);
|
||||
|
||||
@@ -63,21 +85,118 @@ public class MainActivity extends AppCompatActivity {
|
||||
initFragments();
|
||||
}
|
||||
|
||||
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
|
||||
setSupportActionBar(toolbar);
|
||||
setSupportActionBar(findViewById(R.id.toolbar));
|
||||
setupDrawer();
|
||||
}
|
||||
|
||||
private void setupDrawer() {
|
||||
final Toolbar toolbar = findViewById(R.id.toolbar);
|
||||
drawer = findViewById(R.id.drawer_layout);
|
||||
drawerItems = findViewById(R.id.navigation);
|
||||
|
||||
//drawerItems.setItemIconTintList(null); // Set null to use the original icon
|
||||
drawerItems.getMenu().getItem(ServiceHelper.getSelectedServiceId(this)).setChecked(true);
|
||||
|
||||
if (!BuildConfig.BUILD_TYPE.equals("release")) {
|
||||
toggle = new ActionBarDrawerToggle(this, drawer, toolbar, R.string.drawer_open, R.string.drawer_close);
|
||||
toggle.syncState();
|
||||
drawer.addDrawerListener(toggle);
|
||||
drawer.addDrawerListener(new DrawerLayout.SimpleDrawerListener() {
|
||||
private int lastService;
|
||||
|
||||
@Override
|
||||
public void onDrawerOpened(View drawerView) {
|
||||
lastService = ServiceHelper.getSelectedServiceId(MainActivity.this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDrawerClosed(View drawerView) {
|
||||
if (lastService != ServiceHelper.getSelectedServiceId(MainActivity.this)) {
|
||||
new Handler(Looper.getMainLooper()).post(MainActivity.this::recreate);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
drawerItems.setNavigationItemSelectedListener(this::changeService);
|
||||
|
||||
setupDrawerFooter();
|
||||
setupDrawerHeader();
|
||||
} else {
|
||||
drawer.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean changeService(MenuItem item) {
|
||||
if (item.getGroupId() == R.id.menu_services_group) {
|
||||
drawerItems.getMenu().getItem(ServiceHelper.getSelectedServiceId(this)).setChecked(false);
|
||||
ServiceHelper.setSelectedServiceId(this, item.getTitle().toString());
|
||||
drawerItems.getMenu().getItem(ServiceHelper.getSelectedServiceId(this)).setChecked(true);
|
||||
headerServiceView.setText("gurken");
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
drawer.closeDrawers();
|
||||
return true;
|
||||
}
|
||||
|
||||
private void setupDrawerFooter() {
|
||||
ImageButton settings = findViewById(R.id.drawer_settings);
|
||||
ImageButton downloads = findViewById(R.id.drawer_downloads);
|
||||
ImageButton history = findViewById(R.id.drawer_history);
|
||||
|
||||
settings.setOnClickListener(view -> NavigationHelper.openSettings(this));
|
||||
downloads.setOnClickListener(view ->NavigationHelper.openDownloads(this));
|
||||
history.setOnClickListener(view -> NavigationHelper.openHistory(this));
|
||||
}
|
||||
|
||||
private void setupDrawerHeader() {
|
||||
headerServiceView = findViewById(R.id.drawer_header_service_view);
|
||||
Button action = findViewById(R.id.drawer_header_action_button);
|
||||
action.setOnClickListener(view -> {
|
||||
Intent intent = new Intent(Intent.ACTION_VIEW);
|
||||
intent.setData(Uri.parse("https://newpipe.schabi.org/blog/"));
|
||||
startActivity(intent);
|
||||
drawer.closeDrawers();
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
super.onDestroy();
|
||||
if (!isChangingConfigurations()) {
|
||||
StateSaver.clearStateFiles();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
|
||||
// close drawer on return, and don't show animation, so its looks like the drawer isn't open
|
||||
// when the user returns to MainActivity
|
||||
drawer.closeDrawer(Gravity.START, false);
|
||||
try {
|
||||
String selectedServiceName = NewPipe.getService(
|
||||
ServiceHelper.getSelectedServiceId(this)).getServiceInfo().getName();
|
||||
headerServiceView.setText(selectedServiceName);
|
||||
} catch (Exception e) {
|
||||
ErrorActivity.reportUiError(this, e);
|
||||
}
|
||||
|
||||
SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
|
||||
if (sharedPreferences.getBoolean(Constants.KEY_THEME_CHANGE, false)) {
|
||||
if (DEBUG) Log.d(TAG, "Theme has changed, recreating activity...");
|
||||
sharedPreferences.edit().putBoolean(Constants.KEY_THEME_CHANGE, false).apply();
|
||||
this.recreate();
|
||||
// https://stackoverflow.com/questions/10844112/runtimeexception-performing-pause-of-activity-that-is-not-resumed
|
||||
// Briefly, let the activity resume properly posting the recreate call to end of the message queue
|
||||
new Handler(Looper.getMainLooper()).post(MainActivity.this::recreate);
|
||||
}
|
||||
|
||||
if (sharedPreferences.getBoolean(Constants.KEY_MAIN_PAGE_CHANGE, false)) {
|
||||
if (DEBUG) Log.d(TAG, "main page has changed, recreating main fragment...");
|
||||
sharedPreferences.edit().putBoolean(Constants.KEY_MAIN_PAGE_CHANGE, false).apply();
|
||||
NavigationHelper.openMainActivity(this);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -100,7 +219,10 @@ public class MainActivity extends AppCompatActivity {
|
||||
if (DEBUG) Log.d(TAG, "onBackPressed() called");
|
||||
|
||||
Fragment fragment = getSupportFragmentManager().findFragmentById(R.id.fragment_holder);
|
||||
if (fragment instanceof VideoDetailFragment) if (((VideoDetailFragment) fragment).onActivityBackPressed()) return;
|
||||
// If current fragment implements BackPressable (i.e. can/wanna handle back press) delegate the back press to it
|
||||
if (fragment instanceof BackPressable) {
|
||||
if (((BackPressable) fragment).onBackPressed()) return;
|
||||
}
|
||||
|
||||
|
||||
if (getSupportFragmentManager().getBackStackEntryCount() == 1) {
|
||||
@@ -108,6 +230,38 @@ public class MainActivity extends AppCompatActivity {
|
||||
} else super.onBackPressed();
|
||||
}
|
||||
|
||||
/**
|
||||
* Implement the following diagram behavior for the up button:
|
||||
* <pre>
|
||||
* +---------------+
|
||||
* | Main Screen +----+
|
||||
* +-------+-------+ |
|
||||
* | |
|
||||
* ▲ Up | Search Button
|
||||
* | |
|
||||
* +----+-----+ |
|
||||
* +------------+ Search |◄-----+
|
||||
* | +----+-----+
|
||||
* | Open |
|
||||
* | something ▲ Up
|
||||
* | |
|
||||
* | +------------+-------------+
|
||||
* | | |
|
||||
* | | Video <-> Channel |
|
||||
* +---►| Channel <-> Playlist |
|
||||
* | Video <-> .... |
|
||||
* | |
|
||||
* +--------------------------+
|
||||
* </pre>
|
||||
*/
|
||||
private void onHomeButtonPressed() {
|
||||
// If search fragment wasn't found in the backstack...
|
||||
if (!NavigationHelper.tryGotoSearchFragment(getSupportFragmentManager())) {
|
||||
// ...go to the main fragment
|
||||
NavigationHelper.gotoMainFragment(getSupportFragmentManager());
|
||||
}
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Menu
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
@@ -131,9 +285,11 @@ public class MainActivity extends AppCompatActivity {
|
||||
|
||||
ActionBar actionBar = getSupportActionBar();
|
||||
if (actionBar != null) {
|
||||
actionBar.setDisplayShowTitleEnabled(false);
|
||||
actionBar.setDisplayHomeAsUpEnabled(false);
|
||||
}
|
||||
|
||||
updateDrawerNavigation();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -143,20 +299,20 @@ public class MainActivity extends AppCompatActivity {
|
||||
int id = item.getItemId();
|
||||
|
||||
switch (id) {
|
||||
case android.R.id.home: {
|
||||
NavigationHelper.gotoMainFragment(getSupportFragmentManager());
|
||||
case android.R.id.home:
|
||||
onHomeButtonPressed();
|
||||
return true;
|
||||
}
|
||||
case R.id.action_settings: {
|
||||
case R.id.action_settings:
|
||||
NavigationHelper.openSettings(this);
|
||||
return true;
|
||||
}
|
||||
case R.id.action_show_downloads: {
|
||||
case R.id.action_show_downloads:
|
||||
return NavigationHelper.openDownloads(this);
|
||||
}
|
||||
case R.id.action_about:
|
||||
NavigationHelper.openAbout(this);
|
||||
return true;
|
||||
case R.id.action_history:
|
||||
NavigationHelper.openHistory(this);
|
||||
return true;
|
||||
default:
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
@@ -167,6 +323,8 @@ public class MainActivity extends AppCompatActivity {
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
private void initFragments() {
|
||||
if (DEBUG) Log.d(TAG, "initFragments() called");
|
||||
StateSaver.clearStateFiles();
|
||||
if (getIntent() != null && getIntent().hasExtra(Constants.KEY_LINK_TYPE)) {
|
||||
handleIntent(getIntent());
|
||||
} else NavigationHelper.gotoMainFragment(getSupportFragmentManager());
|
||||
@@ -176,6 +334,27 @@ public class MainActivity extends AppCompatActivity {
|
||||
// Utils
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
private void updateDrawerNavigation() {
|
||||
if (getSupportActionBar() == null) return;
|
||||
|
||||
final Toolbar toolbar = findViewById(R.id.toolbar);
|
||||
final DrawerLayout drawer = findViewById(R.id.drawer_layout);
|
||||
|
||||
final Fragment fragment = getSupportFragmentManager().findFragmentById(R.id.fragment_holder);
|
||||
if (fragment instanceof MainFragment) {
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(false);
|
||||
if (toggle != null) {
|
||||
toggle.syncState();
|
||||
toolbar.setNavigationOnClickListener(v -> drawer.openDrawer(GravityCompat.START));
|
||||
drawer.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNDEFINED);
|
||||
}
|
||||
} else {
|
||||
drawer.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED);
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
toolbar.setNavigationOnClickListener(v -> onHomeButtonPressed());
|
||||
}
|
||||
}
|
||||
|
||||
private void handleIntent(Intent intent) {
|
||||
if (DEBUG) Log.d(TAG, "handleIntent() called with: intent = [" + intent + "]");
|
||||
|
||||
@@ -191,6 +370,9 @@ public class MainActivity extends AppCompatActivity {
|
||||
case CHANNEL:
|
||||
NavigationHelper.openChannelFragment(getSupportFragmentManager(), serviceId, url, title);
|
||||
break;
|
||||
case PLAYLIST:
|
||||
NavigationHelper.openPlaylistFragment(getSupportFragmentManager(), serviceId, url, title);
|
||||
break;
|
||||
}
|
||||
} else if (intent.hasExtra(Constants.KEY_OPEN_SEARCH)) {
|
||||
String searchQuery = intent.getStringExtra(Constants.KEY_QUERY);
|
||||
|
||||
41
app/src/main/java/org/schabi/newpipe/NewPipeDatabase.java
Normal file
41
app/src/main/java/org/schabi/newpipe/NewPipeDatabase.java
Normal file
@@ -0,0 +1,41 @@
|
||||
package org.schabi.newpipe;
|
||||
|
||||
import android.arch.persistence.room.Room;
|
||||
import android.content.Context;
|
||||
import android.support.annotation.NonNull;
|
||||
|
||||
import org.schabi.newpipe.database.AppDatabase;
|
||||
|
||||
import static org.schabi.newpipe.database.AppDatabase.DATABASE_NAME;
|
||||
import static org.schabi.newpipe.database.Migrations.MIGRATION_11_12;
|
||||
|
||||
public final class NewPipeDatabase {
|
||||
|
||||
private static AppDatabase databaseInstance;
|
||||
|
||||
private NewPipeDatabase() {
|
||||
//no instance
|
||||
}
|
||||
|
||||
public static void init(Context context) {
|
||||
databaseInstance = Room
|
||||
.databaseBuilder(context, AppDatabase.class, DATABASE_NAME)
|
||||
.addMigrations(MIGRATION_11_12)
|
||||
.fallbackToDestructiveMigration()
|
||||
.build();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Deprecated
|
||||
public static AppDatabase getInstance() {
|
||||
if (databaseInstance == null) throw new RuntimeException("Database not initialized");
|
||||
|
||||
return databaseInstance;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public static AppDatabase getInstance(Context context) {
|
||||
if (databaseInstance == null) init(context);
|
||||
return databaseInstance;
|
||||
}
|
||||
}
|
||||
@@ -6,9 +6,8 @@ import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.media.AudioManager;
|
||||
|
||||
/**
|
||||
/*
|
||||
* Copyright (C) Hans-Christoph Steiner 2016 <hans@eds.org>
|
||||
* PanicResponderActivity.java is part of NewPipe.
|
||||
*
|
||||
|
||||
@@ -16,7 +16,7 @@ import android.webkit.WebSettings;
|
||||
import android.webkit.WebView;
|
||||
import android.webkit.WebViewClient;
|
||||
|
||||
/**
|
||||
/*
|
||||
* Created by beneth <bmauduit@beneth.fr> on 06.12.16.
|
||||
*
|
||||
* Copyright (C) Christian Schabesberger 2015 <chris.schabesberger@mailbox.org>
|
||||
@@ -49,7 +49,7 @@ public class ReCaptchaActivity extends AppCompatActivity {
|
||||
// Set return to Cancel by default
|
||||
setResult(RESULT_CANCELED);
|
||||
|
||||
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
|
||||
Toolbar toolbar = findViewById(R.id.toolbar);
|
||||
setSupportActionBar(toolbar);
|
||||
|
||||
ActionBar actionBar = getSupportActionBar();
|
||||
@@ -59,7 +59,7 @@ public class ReCaptchaActivity extends AppCompatActivity {
|
||||
actionBar.setDisplayShowTitleEnabled(true);
|
||||
}
|
||||
|
||||
WebView myWebView = (WebView) findViewById(R.id.reCaptchaWebView);
|
||||
WebView myWebView = findViewById(R.id.reCaptchaWebView);
|
||||
|
||||
// Enable Javascript
|
||||
WebSettings webSettings = myWebView.getSettings();
|
||||
@@ -107,7 +107,7 @@ public class ReCaptchaActivity extends AppCompatActivity {
|
||||
// find cookies : s_gl & goojf and Add cookies to Downloader
|
||||
if (find_access_cookies(cookies)) {
|
||||
// Give cookies to Downloader class
|
||||
Downloader.setCookies(mCookies);
|
||||
Downloader.getInstance().setCookies(mCookies);
|
||||
|
||||
// Closing activity and return to parent
|
||||
setResult(RESULT_OK);
|
||||
|
||||
@@ -1,54 +1,495 @@
|
||||
package org.schabi.newpipe;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.IntentService;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.Bundle;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.support.annotation.DrawableRes;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v4.app.NotificationCompat;
|
||||
import android.support.v7.app.AlertDialog;
|
||||
import android.support.v7.app.AppCompatActivity;
|
||||
import android.text.TextUtils;
|
||||
import android.view.ContextThemeWrapper;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.Button;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.RadioButton;
|
||||
import android.widget.RadioGroup;
|
||||
import android.widget.Toast;
|
||||
|
||||
import org.schabi.newpipe.extractor.Info;
|
||||
import org.schabi.newpipe.extractor.NewPipe;
|
||||
import org.schabi.newpipe.extractor.ServiceList;
|
||||
import org.schabi.newpipe.extractor.StreamingService;
|
||||
import org.schabi.newpipe.extractor.StreamingService.LinkType;
|
||||
import org.schabi.newpipe.extractor.channel.ChannelInfo;
|
||||
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||
import org.schabi.newpipe.extractor.playlist.PlaylistInfo;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||
import org.schabi.newpipe.player.helper.PlayerHelper;
|
||||
import org.schabi.newpipe.playlist.ChannelPlayQueue;
|
||||
import org.schabi.newpipe.playlist.PlayQueue;
|
||||
import org.schabi.newpipe.playlist.PlaylistPlayQueue;
|
||||
import org.schabi.newpipe.playlist.SinglePlayQueue;
|
||||
import org.schabi.newpipe.report.UserAction;
|
||||
import org.schabi.newpipe.util.ExtractorHelper;
|
||||
import org.schabi.newpipe.util.NavigationHelper;
|
||||
import org.schabi.newpipe.util.PermissionHelper;
|
||||
import org.schabi.newpipe.util.ThemeHelper;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.HashSet;
|
||||
|
||||
/*
|
||||
* Copyright (C) Christian Schabesberger 2017 <chris.schabesberger@mailbox.org>
|
||||
* RouterActivity .java is part of NewPipe.
|
||||
*
|
||||
* OpenHitboxStreams is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* OpenHitboxStreams is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with OpenHitboxStreams. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import icepick.Icepick;
|
||||
import icepick.State;
|
||||
import io.reactivex.Observable;
|
||||
import io.reactivex.Single;
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.disposables.CompositeDisposable;
|
||||
import io.reactivex.disposables.Disposable;
|
||||
import io.reactivex.functions.Consumer;
|
||||
import io.reactivex.schedulers.Schedulers;
|
||||
|
||||
import static org.schabi.newpipe.util.ThemeHelper.resolveResourceIdFromAttr;
|
||||
|
||||
/**
|
||||
* This Acitivty is designed to route share/open intents to the specified service, and
|
||||
* to the part of the service which can handle the url.
|
||||
* Get the url from the intent and open it in the chosen preferred player
|
||||
*/
|
||||
public class RouterActivity extends Activity {
|
||||
public class RouterActivity extends AppCompatActivity {
|
||||
|
||||
@State
|
||||
protected int currentServiceId = -1;
|
||||
private StreamingService currentService;
|
||||
@State
|
||||
protected LinkType currentLinkType;
|
||||
@State
|
||||
protected int selectedRadioPosition = -1;
|
||||
protected int selectedPreviously = -1;
|
||||
|
||||
protected String currentUrl;
|
||||
protected CompositeDisposable disposables = new CompositeDisposable();
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
Icepick.restoreInstanceState(this, savedInstanceState);
|
||||
|
||||
String videoUrl = getUrl(getIntent());
|
||||
handleUrl(videoUrl);
|
||||
if (TextUtils.isEmpty(currentUrl)) {
|
||||
currentUrl = getUrl(getIntent());
|
||||
|
||||
if (TextUtils.isEmpty(currentUrl)) {
|
||||
Toast.makeText(this, R.string.invalid_url_toast, Toast.LENGTH_LONG).show();
|
||||
finish();
|
||||
}
|
||||
}
|
||||
|
||||
setTheme(ThemeHelper.isLightThemeSelected(this)
|
||||
? R.style.RouterActivityThemeLight
|
||||
: R.style.RouterActivityThemeDark);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onSaveInstanceState(Bundle outState) {
|
||||
super.onSaveInstanceState(outState);
|
||||
Icepick.saveInstanceState(this, outState);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onStart() {
|
||||
super.onStart();
|
||||
|
||||
handleUrl(currentUrl);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
super.onDestroy();
|
||||
|
||||
disposables.clear();
|
||||
}
|
||||
|
||||
private void handleUrl(String url) {
|
||||
disposables.add(Observable
|
||||
.fromCallable(() -> {
|
||||
if (currentServiceId == -1) {
|
||||
currentService = NewPipe.getServiceByUrl(url);
|
||||
currentServiceId = currentService.getServiceId();
|
||||
currentLinkType = currentService.getLinkTypeByUrl(url);
|
||||
currentUrl = NavigationHelper.getCleanUrl(currentService, url, currentLinkType);
|
||||
} else {
|
||||
currentService = NewPipe.getService(currentServiceId);
|
||||
}
|
||||
|
||||
return currentLinkType != LinkType.NONE;
|
||||
})
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(result -> {
|
||||
if (result) {
|
||||
onSuccess();
|
||||
} else {
|
||||
onError();
|
||||
}
|
||||
}, this::handleError));
|
||||
}
|
||||
|
||||
private void handleError(Throwable error) {
|
||||
error.printStackTrace();
|
||||
|
||||
if (error instanceof ExtractionException) {
|
||||
Toast.makeText(this, R.string.url_not_supported_toast, Toast.LENGTH_LONG).show();
|
||||
} else {
|
||||
ExtractorHelper.handleGeneralException(this, -1, null, error, UserAction.SOMETHING_ELSE, null);
|
||||
}
|
||||
|
||||
finish();
|
||||
}
|
||||
|
||||
protected void handleUrl(String url) {
|
||||
try {
|
||||
NavigationHelper.openByLink(this, url);
|
||||
} catch (Exception e) {
|
||||
Toast.makeText(this, R.string.url_not_supported_toast, Toast.LENGTH_LONG).show();
|
||||
private void onError() {
|
||||
Toast.makeText(this, R.string.url_not_supported_toast, Toast.LENGTH_LONG).show();
|
||||
finish();
|
||||
}
|
||||
|
||||
protected void onSuccess() {
|
||||
final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this);
|
||||
boolean isExtVideoEnabled = preferences.getBoolean(getString(R.string.use_external_video_player_key), false);
|
||||
boolean isExtAudioEnabled = preferences.getBoolean(getString(R.string.use_external_audio_player_key), false);
|
||||
|
||||
if ((isExtAudioEnabled || isExtVideoEnabled) && currentLinkType != LinkType.STREAM) {
|
||||
Toast.makeText(this, R.string.external_player_unsupported_link_type, Toast.LENGTH_LONG).show();
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: Add some sort of "capabilities" field to services (audio only, video and audio, etc.)
|
||||
if (currentService == ServiceList.SoundCloud) {
|
||||
handleChoice(getString(R.string.background_player_key));
|
||||
return;
|
||||
}
|
||||
|
||||
final String playerChoiceKey = preferences.getString(
|
||||
getString(R.string.preferred_open_action_key),
|
||||
getString(R.string.preferred_open_action_default));
|
||||
final String alwaysAskKey = getString(R.string.always_ask_open_action_key);
|
||||
|
||||
if (playerChoiceKey.equals(alwaysAskKey)) {
|
||||
showDialog();
|
||||
} else {
|
||||
handleChoice(playerChoiceKey);
|
||||
}
|
||||
}
|
||||
|
||||
private void showDialog() {
|
||||
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this);
|
||||
final ContextThemeWrapper themeWrapper = new ContextThemeWrapper(this,
|
||||
ThemeHelper.isLightThemeSelected(this) ? R.style.LightTheme : R.style.DarkTheme);
|
||||
|
||||
LayoutInflater inflater = LayoutInflater.from(themeWrapper);
|
||||
final LinearLayout rootLayout = (LinearLayout) inflater.inflate(R.layout.preferred_player_dialog_view, null, false);
|
||||
final RadioGroup radioGroup = rootLayout.findViewById(android.R.id.list);
|
||||
|
||||
final AdapterChoiceItem[] choices = {
|
||||
new AdapterChoiceItem(getString(R.string.show_info_key), getString(R.string.show_info),
|
||||
resolveResourceIdFromAttr(themeWrapper, R.attr.info)),
|
||||
new AdapterChoiceItem(getString(R.string.video_player_key), getString(R.string.video_player),
|
||||
resolveResourceIdFromAttr(themeWrapper, R.attr.play)),
|
||||
new AdapterChoiceItem(getString(R.string.background_player_key), getString(R.string.background_player),
|
||||
resolveResourceIdFromAttr(themeWrapper, R.attr.audio)),
|
||||
new AdapterChoiceItem(getString(R.string.popup_player_key), getString(R.string.popup_player),
|
||||
resolveResourceIdFromAttr(themeWrapper, R.attr.popup))
|
||||
};
|
||||
|
||||
final DialogInterface.OnClickListener dialogButtonsClickListener = (dialog, which) -> {
|
||||
final int indexOfChild = radioGroup.indexOfChild(
|
||||
radioGroup.findViewById(radioGroup.getCheckedRadioButtonId()));
|
||||
final AdapterChoiceItem choice = choices[indexOfChild];
|
||||
|
||||
handleChoice(choice.key);
|
||||
|
||||
if (which == DialogInterface.BUTTON_POSITIVE) {
|
||||
preferences.edit().putString(getString(R.string.preferred_open_action_key), choice.key).apply();
|
||||
}
|
||||
};
|
||||
|
||||
final AlertDialog alertDialog = new AlertDialog.Builder(themeWrapper)
|
||||
.setTitle(R.string.preferred_player_share_menu_title)
|
||||
.setView(radioGroup)
|
||||
.setCancelable(true)
|
||||
.setNegativeButton(R.string.just_once, dialogButtonsClickListener)
|
||||
.setPositiveButton(R.string.always, dialogButtonsClickListener)
|
||||
.setOnDismissListener((dialog) -> finish())
|
||||
.create();
|
||||
|
||||
alertDialog.setOnShowListener(dialog -> {
|
||||
setDialogButtonsState(alertDialog, radioGroup.getCheckedRadioButtonId() != -1);
|
||||
});
|
||||
|
||||
radioGroup.setOnCheckedChangeListener((group, checkedId) -> setDialogButtonsState(alertDialog, true));
|
||||
final View.OnClickListener radioButtonsClickListener = v -> {
|
||||
final int indexOfChild = radioGroup.indexOfChild(v);
|
||||
if (indexOfChild == -1) return;
|
||||
|
||||
selectedPreviously = selectedRadioPosition;
|
||||
selectedRadioPosition = indexOfChild;
|
||||
|
||||
if (selectedPreviously == selectedRadioPosition) {
|
||||
handleChoice(choices[selectedRadioPosition].key);
|
||||
}
|
||||
};
|
||||
|
||||
int id = 12345;
|
||||
for (AdapterChoiceItem item : choices) {
|
||||
final RadioButton radioButton = (RadioButton) inflater.inflate(R.layout.list_radio_icon_item, null);
|
||||
radioButton.setText(item.description);
|
||||
radioButton.setCompoundDrawablesWithIntrinsicBounds(item.icon, 0, 0, 0);
|
||||
radioButton.setChecked(false);
|
||||
radioButton.setId(id++);
|
||||
radioButton.setLayoutParams(new RadioGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT));
|
||||
radioButton.setOnClickListener(radioButtonsClickListener);
|
||||
radioGroup.addView(radioButton);
|
||||
}
|
||||
|
||||
if (selectedRadioPosition == -1) {
|
||||
final String lastSelectedPlayer = preferences.getString(getString(R.string.preferred_open_action_last_selected_key), null);
|
||||
if (!TextUtils.isEmpty(lastSelectedPlayer)) {
|
||||
for (int i = 0; i < choices.length; i++) {
|
||||
AdapterChoiceItem c = choices[i];
|
||||
if (lastSelectedPlayer.equals(c.key)) {
|
||||
selectedRadioPosition = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
selectedRadioPosition = Math.min(Math.max(-1, selectedRadioPosition), choices.length - 1);
|
||||
if (selectedRadioPosition != -1) {
|
||||
((RadioButton) radioGroup.getChildAt(selectedRadioPosition)).setChecked(true);
|
||||
}
|
||||
selectedPreviously = selectedRadioPosition;
|
||||
|
||||
alertDialog.show();
|
||||
}
|
||||
|
||||
private void setDialogButtonsState(AlertDialog dialog, boolean state) {
|
||||
final Button negativeButton = dialog.getButton(DialogInterface.BUTTON_NEGATIVE);
|
||||
final Button positiveButton = dialog.getButton(DialogInterface.BUTTON_POSITIVE);
|
||||
if (negativeButton == null || positiveButton == null) return;
|
||||
|
||||
negativeButton.setEnabled(state);
|
||||
positiveButton.setEnabled(state);
|
||||
}
|
||||
|
||||
private void handleChoice(final String playerChoiceKey) {
|
||||
if (Arrays.asList(getResources()
|
||||
.getStringArray(R.array.preferred_open_action_values_list))
|
||||
.contains(playerChoiceKey)) {
|
||||
PreferenceManager.getDefaultSharedPreferences(this).edit()
|
||||
.putString(getString(R.string.preferred_open_action_last_selected_key),
|
||||
playerChoiceKey).apply();
|
||||
}
|
||||
|
||||
if (playerChoiceKey.equals(getString(R.string.popup_player_key))
|
||||
&& !PermissionHelper.isPopupEnabled(this)) {
|
||||
PermissionHelper.showPopupEnablementToast(this);
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
// stop and bypass FetcherService if InfoScreen was selected since
|
||||
// StreamDetailFragment can fetch data itself
|
||||
if(playerChoiceKey.equals(getString(R.string.show_info_key))) {
|
||||
disposables.add(Observable
|
||||
.fromCallable(() -> NavigationHelper.getIntentByLink(this, currentUrl))
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(intent -> {
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK);
|
||||
startActivity(intent);
|
||||
|
||||
finish();
|
||||
}, this::handleError)
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
final Intent intent = new Intent(this, FetcherService.class);
|
||||
intent.putExtra(FetcherService.KEY_CHOICE,
|
||||
new Choice(currentService.getServiceId(),
|
||||
currentLinkType,
|
||||
currentUrl,
|
||||
playerChoiceKey));
|
||||
startService(intent);
|
||||
|
||||
finish();
|
||||
}
|
||||
|
||||
private static class AdapterChoiceItem {
|
||||
final String description, key;
|
||||
@DrawableRes
|
||||
final int icon;
|
||||
|
||||
AdapterChoiceItem(String key, String description, int icon) {
|
||||
this.description = description;
|
||||
this.key = key;
|
||||
this.icon = icon;
|
||||
}
|
||||
}
|
||||
|
||||
private static class Choice implements Serializable {
|
||||
final int serviceId;
|
||||
final String url, playerChoice;
|
||||
final LinkType linkType;
|
||||
|
||||
Choice(int serviceId, LinkType linkType, String url, String playerChoice) {
|
||||
this.serviceId = serviceId;
|
||||
this.linkType = linkType;
|
||||
this.url = url;
|
||||
this.playerChoice = playerChoice;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return serviceId + ":" + url + " > " + linkType + " ::: " + playerChoice;
|
||||
}
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Service Fetcher
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
public static class FetcherService extends IntentService {
|
||||
|
||||
private static final int ID = 456;
|
||||
public static final String KEY_CHOICE = "key_choice";
|
||||
private Disposable fetcher;
|
||||
|
||||
public FetcherService() {
|
||||
super(FetcherService.class.getSimpleName());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
startForeground(ID, createNotification().build());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onHandleIntent(@Nullable Intent intent) {
|
||||
if (intent == null) return;
|
||||
|
||||
final Serializable serializable = intent.getSerializableExtra(KEY_CHOICE);
|
||||
if (!(serializable instanceof Choice)) return;
|
||||
Choice playerChoice = (Choice) serializable;
|
||||
handleChoice(playerChoice);
|
||||
}
|
||||
|
||||
public void handleChoice(Choice choice) {
|
||||
Single<? extends Info> single = null;
|
||||
UserAction userAction = UserAction.SOMETHING_ELSE;
|
||||
|
||||
switch (choice.linkType) {
|
||||
case STREAM:
|
||||
single = ExtractorHelper.getStreamInfo(choice.serviceId, choice.url, false);
|
||||
userAction = UserAction.REQUESTED_STREAM;
|
||||
break;
|
||||
case CHANNEL:
|
||||
single = ExtractorHelper.getChannelInfo(choice.serviceId, choice.url, false);
|
||||
userAction = UserAction.REQUESTED_CHANNEL;
|
||||
break;
|
||||
case PLAYLIST:
|
||||
single = ExtractorHelper.getPlaylistInfo(choice.serviceId, choice.url, false);
|
||||
userAction = UserAction.REQUESTED_PLAYLIST;
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
if (single != null) {
|
||||
final UserAction finalUserAction = userAction;
|
||||
final Consumer<Info> resultHandler = getResultHandler(choice);
|
||||
fetcher = single
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(info -> {
|
||||
resultHandler.accept(info);
|
||||
if (fetcher != null) fetcher.dispose();
|
||||
}, throwable -> ExtractorHelper.handleGeneralException(this,
|
||||
choice.serviceId, choice.url, throwable, finalUserAction, ", opened with " + choice.playerChoice));
|
||||
}
|
||||
}
|
||||
|
||||
public Consumer<Info> getResultHandler(Choice choice) {
|
||||
return info -> {
|
||||
final String videoPlayerKey = getString(R.string.video_player_key);
|
||||
final String backgroundPlayerKey = getString(R.string.background_player_key);
|
||||
final String popupPlayerKey = getString(R.string.popup_player_key);
|
||||
|
||||
final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this);
|
||||
boolean isExtVideoEnabled = preferences.getBoolean(getString(R.string.use_external_video_player_key), false);
|
||||
boolean isExtAudioEnabled = preferences.getBoolean(getString(R.string.use_external_audio_player_key), false);
|
||||
boolean useOldVideoPlayer = PlayerHelper.isUsingOldPlayer(this);
|
||||
|
||||
PlayQueue playQueue;
|
||||
String playerChoice = choice.playerChoice;
|
||||
|
||||
if (info instanceof StreamInfo) {
|
||||
if (playerChoice.equals(backgroundPlayerKey) && isExtAudioEnabled) {
|
||||
NavigationHelper.playOnExternalAudioPlayer(this, (StreamInfo) info);
|
||||
|
||||
} else if (playerChoice.equals(videoPlayerKey) && isExtVideoEnabled) {
|
||||
NavigationHelper.playOnExternalVideoPlayer(this, (StreamInfo) info);
|
||||
|
||||
} else if (playerChoice.equals(videoPlayerKey) && useOldVideoPlayer) {
|
||||
NavigationHelper.playOnOldVideoPlayer(this, (StreamInfo) info);
|
||||
|
||||
} else {
|
||||
playQueue = new SinglePlayQueue((StreamInfo) info);
|
||||
|
||||
if (playerChoice.equals(videoPlayerKey)) {
|
||||
NavigationHelper.playOnMainPlayer(this, playQueue);
|
||||
} else if (playerChoice.equals(backgroundPlayerKey)) {
|
||||
NavigationHelper.enqueueOnBackgroundPlayer(this, playQueue, true);
|
||||
} else if (playerChoice.equals(popupPlayerKey)) {
|
||||
NavigationHelper.enqueueOnPopupPlayer(this, playQueue, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (info instanceof ChannelInfo || info instanceof PlaylistInfo) {
|
||||
playQueue = info instanceof ChannelInfo ? new ChannelPlayQueue((ChannelInfo) info) : new PlaylistPlayQueue((PlaylistInfo) info);
|
||||
|
||||
if (playerChoice.equals(videoPlayerKey)) {
|
||||
NavigationHelper.playOnMainPlayer(this, playQueue);
|
||||
} else if (playerChoice.equals(backgroundPlayerKey)) {
|
||||
NavigationHelper.playOnBackgroundPlayer(this, playQueue);
|
||||
} else if (playerChoice.equals(popupPlayerKey)) {
|
||||
NavigationHelper.playOnPopupPlayer(this, playQueue);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
stopForeground(true);
|
||||
if (fetcher != null) fetcher.dispose();
|
||||
}
|
||||
|
||||
private NotificationCompat.Builder createNotification() {
|
||||
return new NotificationCompat.Builder(this, getString(R.string.notification_channel_id))
|
||||
.setOngoing(true)
|
||||
.setSmallIcon(R.drawable.ic_newpipe_triangle_white)
|
||||
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
|
||||
.setContentTitle(getString(R.string.preferred_player_fetcher_notification_title))
|
||||
.setContentText(getString(R.string.preferred_player_fetcher_notification_message));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,9 +502,9 @@ public class RouterActivity extends Activity {
|
||||
* brackets (\p{P}). See http://www.regular-expressions.info/unicode.html for
|
||||
* more details.
|
||||
*/
|
||||
protected final static String REGEX_REMOVE_FROM_URL = "[\\p{Z}\\p{P}]";
|
||||
private final static String REGEX_REMOVE_FROM_URL = "[\\p{Z}\\p{P}]";
|
||||
|
||||
protected String getUrl(Intent intent) {
|
||||
private String getUrl(Intent intent) {
|
||||
// first gather data and find service
|
||||
String videoUrl = null;
|
||||
if (intent.getData() != null) {
|
||||
@@ -72,13 +513,14 @@ public class RouterActivity extends Activity {
|
||||
} else if (intent.getStringExtra(Intent.EXTRA_TEXT) != null) {
|
||||
//this means that vidoe was called through share menu
|
||||
String extraText = intent.getStringExtra(Intent.EXTRA_TEXT);
|
||||
videoUrl = getUris(extraText)[0];
|
||||
final String[] uris = getUris(extraText);
|
||||
videoUrl = uris.length > 0 ? uris[0] : null;
|
||||
}
|
||||
|
||||
return videoUrl;
|
||||
}
|
||||
|
||||
protected String removeHeadingGibberish(final String input) {
|
||||
private String removeHeadingGibberish(final String input) {
|
||||
int start = 0;
|
||||
for (int i = input.indexOf("://") - 1; i >= 0; i--) {
|
||||
if (!input.substring(i, i + 1).matches("\\p{L}")) {
|
||||
@@ -89,7 +531,7 @@ public class RouterActivity extends Activity {
|
||||
return input.substring(start, input.length());
|
||||
}
|
||||
|
||||
protected String trim(final String input) {
|
||||
private String trim(final String input) {
|
||||
if (input == null || input.length() < 1) {
|
||||
return input;
|
||||
} else {
|
||||
@@ -129,5 +571,4 @@ public class RouterActivity extends Activity {
|
||||
}
|
||||
return result.toArray(new String[result.size()]);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
package org.schabi.newpipe;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.os.Build;
|
||||
import android.widget.Toast;
|
||||
|
||||
import org.schabi.newpipe.extractor.NewPipe;
|
||||
import org.schabi.newpipe.extractor.StreamingService;
|
||||
import org.schabi.newpipe.player.PopupVideoPlayer;
|
||||
import org.schabi.newpipe.util.Constants;
|
||||
import org.schabi.newpipe.util.PermissionHelper;
|
||||
|
||||
/**
|
||||
* Get the url from the intent and open a popup player
|
||||
*/
|
||||
public class RouterPopupActivity extends RouterActivity {
|
||||
|
||||
@Override
|
||||
protected void handleUrl(String url) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
|
||||
&& !PermissionHelper.checkSystemAlertWindowPermission(this)) {
|
||||
Toast.makeText(this, R.string.msg_popup_permission, Toast.LENGTH_LONG).show();
|
||||
return;
|
||||
}
|
||||
StreamingService service = NewPipe.getServiceByUrl(url);
|
||||
if (service == null) {
|
||||
Toast.makeText(this, R.string.url_not_supported_toast, Toast.LENGTH_LONG).show();
|
||||
return;
|
||||
}
|
||||
|
||||
Intent callIntent = new Intent(this, PopupVideoPlayer.class);
|
||||
switch (service.getLinkTypeByUrl(url)) {
|
||||
case STREAM:
|
||||
break;
|
||||
default:
|
||||
Toast.makeText(this, R.string.url_not_supported_toast, Toast.LENGTH_LONG).show();
|
||||
return;
|
||||
}
|
||||
|
||||
callIntent.putExtra(Constants.KEY_URL, url);
|
||||
callIntent.putExtra(Constants.KEY_SERVICE_ID, service.getServiceId());
|
||||
startService(callIntent);
|
||||
}
|
||||
}
|
||||
@@ -36,11 +36,13 @@ public class AboutActivity extends AppCompatActivity {
|
||||
new SoftwareComponent("Rhino", "2015", "Mozilla", "https://www.mozilla.org/rhino/", StandardLicenses.MPL2),
|
||||
new SoftwareComponent("ACRA", "2013", "Kevin Gaudin", "http://www.acra.ch", StandardLicenses.APACHE2),
|
||||
new SoftwareComponent("Universal Image Loader", "2011 - 2015", "Sergey Tarasevich", "https://github.com/nostra13/Android-Universal-Image-Loader", StandardLicenses.APACHE2),
|
||||
new SoftwareComponent("Netcipher", "2015", "The Guardian Project", "https://guardianproject.info/code/netcipher/", StandardLicenses.APACHE2),
|
||||
new SoftwareComponent("CircleImageView", "2014 - 2017", "Henning Dodenhof", "https://github.com/hdodenhof/CircleImageView", StandardLicenses.APACHE2),
|
||||
new SoftwareComponent("ParalaxScrollView", "2014", "Nir Hartmann", "https://github.com/nirhart/ParallaxScroll", StandardLicenses.MIT),
|
||||
new SoftwareComponent("NoNonsense-FilePicker", "2016", "Jonas Kalderstam", "https://github.com/spacecowboy/NoNonsense-FilePicker", StandardLicenses.MPL2),
|
||||
new SoftwareComponent("ExoPlayer", "2014-2017", "Google Inc", "https://github.com/google/ExoPlayer", StandardLicenses.APACHE2)
|
||||
new SoftwareComponent("ExoPlayer", "2014-2017", "Google Inc", "https://github.com/google/ExoPlayer", StandardLicenses.APACHE2),
|
||||
new SoftwareComponent("RxAndroid", "2015", "The RxAndroid authors", "https://github.com/ReactiveX/RxAndroid", StandardLicenses.APACHE2),
|
||||
new SoftwareComponent("RxJava", "2016-present", "RxJava Contributors", "https://github.com/ReactiveX/RxJava", StandardLicenses.APACHE2),
|
||||
new SoftwareComponent("RxBinding", "2015", "Jake Wharton", "https://github.com/JakeWharton/RxBinding", StandardLicenses.APACHE2)
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -65,7 +67,7 @@ public class AboutActivity extends AppCompatActivity {
|
||||
|
||||
setContentView(R.layout.activity_about);
|
||||
|
||||
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
|
||||
Toolbar toolbar = findViewById(R.id.toolbar);
|
||||
setSupportActionBar(toolbar);
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
// Create the adapter that will return a fragment for each of the three
|
||||
@@ -73,10 +75,10 @@ public class AboutActivity extends AppCompatActivity {
|
||||
mSectionsPagerAdapter = new SectionsPagerAdapter(getSupportFragmentManager());
|
||||
|
||||
// Set up the ViewPager with the sections adapter.
|
||||
mViewPager = (ViewPager) findViewById(R.id.container);
|
||||
mViewPager = findViewById(R.id.container);
|
||||
mViewPager.setAdapter(mSectionsPagerAdapter);
|
||||
|
||||
TabLayout tabLayout = (TabLayout) findViewById(R.id.tabs);
|
||||
TabLayout tabLayout = findViewById(R.id.tabs);
|
||||
tabLayout.setupWithViewPager(mViewPager);
|
||||
}
|
||||
|
||||
@@ -127,14 +129,18 @@ public class AboutActivity extends AppCompatActivity {
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container,
|
||||
Bundle savedInstanceState) {
|
||||
View rootView = inflater.inflate(R.layout.fragment_about, container, false);
|
||||
TextView version = (TextView) rootView.findViewById(R.id.app_version);
|
||||
TextView version = rootView.findViewById(R.id.app_version);
|
||||
version.setText(BuildConfig.VERSION_NAME);
|
||||
|
||||
View githubLink = rootView.findViewById(R.id.github_link);
|
||||
githubLink.setOnClickListener(new OnGithubLinkClickListener());
|
||||
|
||||
View licenseLink = rootView.findViewById(R.id.app_read_license);
|
||||
licenseLink.setOnClickListener(new OnReadFullLicenseClickListener());
|
||||
View donationLink = rootView.findViewById(R.id.donation_link);
|
||||
donationLink.setOnClickListener(new OnDonationLinkClickListener());
|
||||
|
||||
View websiteLink = rootView.findViewById(R.id.website_link);
|
||||
websiteLink.setOnClickListener(new OnWebsiteLinkClickListener());
|
||||
|
||||
return rootView;
|
||||
}
|
||||
|
||||
@@ -147,10 +153,21 @@ public class AboutActivity extends AppCompatActivity {
|
||||
}
|
||||
}
|
||||
|
||||
private static class OnReadFullLicenseClickListener implements View.OnClickListener {
|
||||
private static class OnDonationLinkClickListener implements View.OnClickListener {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
LicenseFragment.showLicense(v.getContext(), StandardLicenses.GPL3);
|
||||
public void onClick(final View view) {
|
||||
final Context context = view.getContext();
|
||||
Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(context.getString(R.string.donation_url)));
|
||||
context.startActivity(intent);
|
||||
}
|
||||
}
|
||||
|
||||
private static class OnWebsiteLinkClickListener implements View.OnClickListener {
|
||||
@Override
|
||||
public void onClick(final View view) {
|
||||
final Context context = view.getContext();
|
||||
Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(context.getString(R.string.website_url)));
|
||||
context.startActivity(intent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,9 +3,6 @@ package org.schabi.newpipe.about;
|
||||
import android.net.Uri;
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import java.net.URLEncoder;
|
||||
|
||||
/**
|
||||
* A software license
|
||||
@@ -53,6 +50,10 @@ public class License implements Parcelable {
|
||||
public String getAbbreviation() {
|
||||
return abbreviation;
|
||||
}
|
||||
|
||||
public String getFilename() {
|
||||
return filename;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int describeContents() {
|
||||
|
||||
@@ -1,22 +1,13 @@
|
||||
package org.schabi.newpipe.about;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v4.app.Fragment;
|
||||
import android.support.v7.app.AlertDialog;
|
||||
import android.view.ContextMenu;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.webkit.WebView;
|
||||
import android.view.*;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
|
||||
import java.util.Arrays;
|
||||
@@ -48,25 +39,7 @@ public class LicenseFragment extends Fragment {
|
||||
* @param license the license to show
|
||||
*/
|
||||
public static void showLicense(Context context, License license) {
|
||||
if(context == null) {
|
||||
throw new NullPointerException("context is null");
|
||||
}
|
||||
if(license == null) {
|
||||
throw new NullPointerException("license is null");
|
||||
}
|
||||
AlertDialog.Builder alert = new AlertDialog.Builder(context);
|
||||
alert.setTitle(license.getName());
|
||||
|
||||
WebView wv = new WebView(context);
|
||||
wv.loadUrl(license.getContentUri().toString());
|
||||
alert.setView(wv);
|
||||
alert.setNegativeButton(android.R.string.ok, new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
dialog.dismiss();
|
||||
}
|
||||
});
|
||||
alert.show();
|
||||
new LicenseFragmentHelper().execute(context, license);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -87,12 +60,15 @@ public class LicenseFragment extends Fragment {
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||||
View rootView = inflater.inflate(R.layout.fragment_licenses, container, false);
|
||||
ViewGroup softwareComponentsView = (ViewGroup) rootView.findViewById(R.id.software_components);
|
||||
ViewGroup softwareComponentsView = rootView.findViewById(R.id.software_components);
|
||||
|
||||
View licenseLink = rootView.findViewById(R.id.app_read_license);
|
||||
licenseLink.setOnClickListener(new OnReadFullLicenseClickListener());
|
||||
|
||||
for (final SoftwareComponent component : softwareComponents) {
|
||||
View componentView = inflater.inflate(R.layout.item_software_component, container, false);
|
||||
TextView softwareName = (TextView) componentView.findViewById(R.id.name);
|
||||
TextView copyright = (TextView) componentView.findViewById(R.id.copyright);
|
||||
TextView softwareName = componentView.findViewById(R.id.name);
|
||||
TextView copyright = componentView.findViewById(R.id.copyright);
|
||||
softwareName.setText(component.getName());
|
||||
copyright.setText(getContext().getString(R.string.copyright,
|
||||
component.getYears(),
|
||||
@@ -111,7 +87,6 @@ public class LicenseFragment extends Fragment {
|
||||
});
|
||||
softwareComponentsView.addView(componentView);
|
||||
registerForContextMenu(componentView);
|
||||
|
||||
}
|
||||
return rootView;
|
||||
}
|
||||
@@ -147,4 +122,11 @@ public class LicenseFragment extends Fragment {
|
||||
Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(componentLink));
|
||||
startActivity(browserIntent);
|
||||
}
|
||||
|
||||
private static class OnReadFullLicenseClickListener implements View.OnClickListener {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
LicenseFragment.showLicense(v.getContext(), StandardLicenses.GPL3);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
package org.schabi.newpipe.about;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.os.AsyncTask;
|
||||
import android.support.v7.app.AlertDialog;
|
||||
import android.webkit.WebView;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.util.ThemeHelper;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.InputStreamReader;
|
||||
|
||||
public class LicenseFragmentHelper extends AsyncTask<Object, Void, Integer> {
|
||||
|
||||
private Context context;
|
||||
private License license;
|
||||
|
||||
@Override
|
||||
protected Integer doInBackground(Object... objects) {
|
||||
context = (Context) objects[0];
|
||||
license = (License) objects[1];
|
||||
return 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(Integer result){
|
||||
String webViewData = getFormattedLicense(context, license);
|
||||
AlertDialog.Builder alert = new AlertDialog.Builder(context);
|
||||
alert.setTitle(license.getName());
|
||||
|
||||
WebView wv = new WebView(context);
|
||||
wv.loadData(webViewData, "text/html; charset=UTF-8", null);
|
||||
|
||||
alert.setView(wv);
|
||||
alert.setNegativeButton(android.R.string.ok, new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
dialog.dismiss();
|
||||
}
|
||||
});
|
||||
alert.show();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param context the context to use
|
||||
* @param license the license
|
||||
* @return String which contains a HTML formatted license page styled according to the context's theme
|
||||
*/
|
||||
public static String getFormattedLicense(Context context, License license) {
|
||||
if(context == null) {
|
||||
throw new NullPointerException("context is null");
|
||||
}
|
||||
if(license == null) {
|
||||
throw new NullPointerException("license is null");
|
||||
}
|
||||
|
||||
String licenseContent = "";
|
||||
String webViewData;
|
||||
try {
|
||||
BufferedReader in = new BufferedReader(new InputStreamReader(context.getAssets().open(license.getFilename()), "UTF-8"));
|
||||
String str;
|
||||
while ((str = in.readLine()) != null) {
|
||||
licenseContent += str;
|
||||
}
|
||||
in.close();
|
||||
|
||||
// split the HTML file and insert the stylesheet into the HEAD of the file
|
||||
String[] insert = licenseContent.split("</head>");
|
||||
webViewData = insert[0] + "<style type=\"text/css\">"
|
||||
+ getLicenseStylesheet(context) + "</style></head>"
|
||||
+ insert[1];
|
||||
} catch (Exception e) {
|
||||
throw new NullPointerException("could not get license file:" + getLicenseStylesheet(context));
|
||||
}
|
||||
return webViewData;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param context
|
||||
* @return String which is a CSS stylesheet according to the context's theme
|
||||
*/
|
||||
public static String getLicenseStylesheet(Context context) {
|
||||
boolean isLightTheme = ThemeHelper.isLightThemeSelected(context);
|
||||
return "body{padding:12px 15px;margin:0;background:#"
|
||||
+ getHexRGBColor(context, isLightTheme
|
||||
? R.color.light_license_background_color
|
||||
: R.color.dark_license_background_color)
|
||||
+ ";color:#"
|
||||
+ getHexRGBColor(context, isLightTheme
|
||||
? R.color.light_license_text_color
|
||||
: R.color.dark_license_text_color) + ";}"
|
||||
+ "a[href]{color:#"
|
||||
+ getHexRGBColor(context, isLightTheme
|
||||
? R.color.light_youtube_primary_color
|
||||
: R.color.dark_youtube_primary_color) + ";}"
|
||||
+ "pre{white-space: pre-wrap;}";
|
||||
}
|
||||
|
||||
/**
|
||||
* Cast R.color to a hexadecimal color value
|
||||
* @param context the context to use
|
||||
* @param color the color number from R.color
|
||||
* @return a six characters long String with hexadecimal RGB values
|
||||
*/
|
||||
public static String getHexRGBColor(Context context, int color) {
|
||||
return context.getResources().getString(color).substring(3);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package org.schabi.newpipe.database;
|
||||
|
||||
import android.arch.persistence.room.Database;
|
||||
import android.arch.persistence.room.RoomDatabase;
|
||||
import android.arch.persistence.room.TypeConverters;
|
||||
|
||||
import org.schabi.newpipe.database.history.dao.SearchHistoryDAO;
|
||||
import org.schabi.newpipe.database.history.dao.StreamHistoryDAO;
|
||||
import org.schabi.newpipe.database.history.model.SearchHistoryEntry;
|
||||
import org.schabi.newpipe.database.history.model.StreamHistoryEntity;
|
||||
import org.schabi.newpipe.database.playlist.dao.PlaylistDAO;
|
||||
import org.schabi.newpipe.database.playlist.dao.PlaylistRemoteDAO;
|
||||
import org.schabi.newpipe.database.playlist.dao.PlaylistStreamDAO;
|
||||
import org.schabi.newpipe.database.playlist.model.PlaylistEntity;
|
||||
import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity;
|
||||
import org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity;
|
||||
import org.schabi.newpipe.database.stream.dao.StreamDAO;
|
||||
import org.schabi.newpipe.database.stream.dao.StreamStateDAO;
|
||||
import org.schabi.newpipe.database.stream.model.StreamEntity;
|
||||
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_12_0;
|
||||
|
||||
@TypeConverters({Converters.class})
|
||||
@Database(
|
||||
entities = {
|
||||
SubscriptionEntity.class, SearchHistoryEntry.class,
|
||||
StreamEntity.class, StreamHistoryEntity.class, StreamStateEntity.class,
|
||||
PlaylistEntity.class, PlaylistStreamEntity.class, PlaylistRemoteEntity.class
|
||||
},
|
||||
version = DB_VER_12_0,
|
||||
exportSchema = false
|
||||
)
|
||||
public abstract class AppDatabase extends RoomDatabase {
|
||||
|
||||
public static final String DATABASE_NAME = "newpipe.db";
|
||||
|
||||
public abstract SubscriptionDAO subscriptionDAO();
|
||||
|
||||
public abstract SearchHistoryDAO searchHistoryDAO();
|
||||
|
||||
public abstract StreamDAO streamDAO();
|
||||
|
||||
public abstract StreamHistoryDAO streamHistoryDAO();
|
||||
|
||||
public abstract StreamStateDAO streamStateDAO();
|
||||
|
||||
public abstract PlaylistDAO playlistDAO();
|
||||
|
||||
public abstract PlaylistStreamDAO playlistStreamDAO();
|
||||
|
||||
public abstract PlaylistRemoteDAO playlistRemoteDAO();
|
||||
}
|
||||
46
app/src/main/java/org/schabi/newpipe/database/BasicDAO.java
Normal file
46
app/src/main/java/org/schabi/newpipe/database/BasicDAO.java
Normal file
@@ -0,0 +1,46 @@
|
||||
package org.schabi.newpipe.database;
|
||||
|
||||
import android.arch.persistence.room.Dao;
|
||||
import android.arch.persistence.room.Delete;
|
||||
import android.arch.persistence.room.Insert;
|
||||
import android.arch.persistence.room.OnConflictStrategy;
|
||||
import android.arch.persistence.room.Update;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
import io.reactivex.Flowable;
|
||||
|
||||
@Dao
|
||||
public interface BasicDAO<Entity> {
|
||||
/* Inserts */
|
||||
@Insert(onConflict = OnConflictStrategy.FAIL)
|
||||
long insert(final Entity entity);
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.FAIL)
|
||||
List<Long> insertAll(final Entity... entities);
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.FAIL)
|
||||
List<Long> insertAll(final Collection<Entity> entities);
|
||||
|
||||
/* Searches */
|
||||
Flowable<List<Entity>> getAll();
|
||||
|
||||
Flowable<List<Entity>> listByService(int serviceId);
|
||||
|
||||
/* Deletes */
|
||||
@Delete
|
||||
int delete(final Entity entity);
|
||||
|
||||
@Delete
|
||||
int delete(final Collection<Entity> entities);
|
||||
|
||||
int deleteAll();
|
||||
|
||||
/* Updates */
|
||||
@Update
|
||||
int update(final Entity entity);
|
||||
|
||||
@Update
|
||||
int update(final Collection<Entity> entities);
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package org.schabi.newpipe.database;
|
||||
|
||||
import android.arch.persistence.room.TypeConverter;
|
||||
|
||||
import org.schabi.newpipe.extractor.stream.StreamType;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
public class Converters {
|
||||
|
||||
/**
|
||||
* Convert a long value to a date
|
||||
* @param value the long value
|
||||
* @return the date
|
||||
*/
|
||||
@TypeConverter
|
||||
public static Date fromTimestamp(Long value) {
|
||||
return value == null ? null : new Date(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a date to a long value
|
||||
* @param date the date
|
||||
* @return the long value
|
||||
*/
|
||||
@TypeConverter
|
||||
public static Long dateToTimestamp(Date date) {
|
||||
return date == null ? null : date.getTime();
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
public static StreamType streamTypeOf(String value) {
|
||||
return StreamType.valueOf(value);
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
public static String stringOf(StreamType streamType) {
|
||||
return streamType.name();
|
||||
}
|
||||
}
|
||||
13
app/src/main/java/org/schabi/newpipe/database/LocalItem.java
Normal file
13
app/src/main/java/org/schabi/newpipe/database/LocalItem.java
Normal file
@@ -0,0 +1,13 @@
|
||||
package org.schabi.newpipe.database;
|
||||
|
||||
public interface LocalItem {
|
||||
enum LocalItemType {
|
||||
PLAYLIST_LOCAL_ITEM,
|
||||
PLAYLIST_REMOTE_ITEM,
|
||||
|
||||
PLAYLIST_STREAM_ITEM,
|
||||
STATISTIC_STREAM_ITEM,
|
||||
}
|
||||
|
||||
LocalItemType getLocalItemType();
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
package org.schabi.newpipe.database;
|
||||
|
||||
import android.arch.persistence.db.SupportSQLiteDatabase;
|
||||
import android.arch.persistence.room.migration.Migration;
|
||||
import android.support.annotation.NonNull;
|
||||
|
||||
public class Migrations {
|
||||
|
||||
public static final int DB_VER_11_0 = 1;
|
||||
public static final int DB_VER_12_0 = 2;
|
||||
|
||||
public static final Migration MIGRATION_11_12 = new Migration(DB_VER_11_0, DB_VER_12_0) {
|
||||
@Override
|
||||
public void migrate(@NonNull SupportSQLiteDatabase database) {
|
||||
/*
|
||||
* Unfortunately these queries must be hardcoded due to the possibility of
|
||||
* schema and names changing at a later date, thus invalidating the older migration
|
||||
* scripts if they are not hardcoded.
|
||||
* */
|
||||
|
||||
// Not much we can do about this, since room doesn't create tables before migration.
|
||||
// It's either this or blasting the entire database anew.
|
||||
database.execSQL("CREATE INDEX `index_search_history_search` ON `search_history` (`search`)");
|
||||
database.execSQL("CREATE TABLE IF NOT EXISTS `streams` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `service_id` INTEGER NOT NULL, `url` TEXT, `title` TEXT, `stream_type` TEXT, `duration` INTEGER, `uploader` TEXT, `thumbnail_url` TEXT)");
|
||||
database.execSQL("CREATE UNIQUE INDEX `index_streams_service_id_url` ON `streams` (`service_id`, `url`)");
|
||||
database.execSQL("CREATE TABLE IF NOT EXISTS `stream_history` (`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 )");
|
||||
database.execSQL("CREATE INDEX `index_stream_history_stream_id` ON `stream_history` (`stream_id`)");
|
||||
database.execSQL("CREATE TABLE IF NOT EXISTS `stream_state` (`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 )");
|
||||
database.execSQL("CREATE TABLE IF NOT EXISTS `playlists` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT, `thumbnail_url` TEXT)");
|
||||
database.execSQL("CREATE INDEX `index_playlists_name` ON `playlists` (`name`)");
|
||||
database.execSQL("CREATE TABLE IF NOT EXISTS `playlist_stream_join` (`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)");
|
||||
database.execSQL("CREATE UNIQUE INDEX `index_playlist_stream_join_playlist_id_join_index` ON `playlist_stream_join` (`playlist_id`, `join_index`)");
|
||||
database.execSQL("CREATE INDEX `index_playlist_stream_join_stream_id` ON `playlist_stream_join` (`stream_id`)");
|
||||
database.execSQL("CREATE TABLE IF NOT EXISTS `remote_playlists` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `service_id` INTEGER NOT NULL, `name` TEXT, `url` TEXT, `thumbnail_url` TEXT, `uploader` TEXT, `stream_count` INTEGER)");
|
||||
database.execSQL("CREATE INDEX `index_remote_playlists_name` ON `remote_playlists` (`name`)");
|
||||
database.execSQL("CREATE UNIQUE INDEX `index_remote_playlists_service_id_url` ON `remote_playlists` (`service_id`, `url`)");
|
||||
|
||||
// Populate streams table with existing entries in watch history
|
||||
// Latest data first, thus ignoring older entries with the same indices
|
||||
database.execSQL("INSERT OR IGNORE INTO streams (service_id, url, title, " +
|
||||
"stream_type, duration, uploader, thumbnail_url) " +
|
||||
|
||||
"SELECT service_id, url, title, 'VIDEO_STREAM', duration, " +
|
||||
"uploader, thumbnail_url " +
|
||||
|
||||
"FROM watch_history " +
|
||||
"ORDER BY creation_date DESC");
|
||||
|
||||
// Once the streams have PKs, join them with the normalized history table
|
||||
// and populate it with the remaining data from watch history
|
||||
database.execSQL("INSERT INTO stream_history (stream_id, access_date, repeat_count)" +
|
||||
"SELECT uid, creation_date, 1 " +
|
||||
"FROM watch_history INNER JOIN streams " +
|
||||
"ON watch_history.service_id == streams.service_id " +
|
||||
"AND watch_history.url == streams.url " +
|
||||
"ORDER BY creation_date DESC");
|
||||
|
||||
database.execSQL("DROP TABLE IF EXISTS watch_history");
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package org.schabi.newpipe.database.history.dao;
|
||||
|
||||
import org.schabi.newpipe.database.BasicDAO;
|
||||
|
||||
public interface HistoryDAO<T> extends BasicDAO<T> {
|
||||
T getLatestEntry();
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package org.schabi.newpipe.database.history.dao;
|
||||
|
||||
import android.arch.persistence.room.Dao;
|
||||
import android.arch.persistence.room.Query;
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
import org.schabi.newpipe.database.BasicDAO;
|
||||
import org.schabi.newpipe.database.history.model.SearchHistoryEntry;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import io.reactivex.Flowable;
|
||||
|
||||
import static org.schabi.newpipe.database.history.model.SearchHistoryEntry.CREATION_DATE;
|
||||
import static org.schabi.newpipe.database.history.model.SearchHistoryEntry.ID;
|
||||
import static org.schabi.newpipe.database.history.model.SearchHistoryEntry.SEARCH;
|
||||
import static org.schabi.newpipe.database.history.model.SearchHistoryEntry.SERVICE_ID;
|
||||
import static org.schabi.newpipe.database.history.model.SearchHistoryEntry.TABLE_NAME;
|
||||
|
||||
@Dao
|
||||
public interface SearchHistoryDAO extends HistoryDAO<SearchHistoryEntry> {
|
||||
|
||||
String ORDER_BY_CREATION_DATE = " ORDER BY " + CREATION_DATE + " DESC";
|
||||
|
||||
@Query("SELECT * FROM " + TABLE_NAME +
|
||||
" WHERE " + ID + " = (SELECT MAX(" + ID + ") FROM " + TABLE_NAME + ")")
|
||||
@Nullable
|
||||
SearchHistoryEntry getLatestEntry();
|
||||
|
||||
@Query("DELETE FROM " + TABLE_NAME)
|
||||
@Override
|
||||
int deleteAll();
|
||||
|
||||
@Query("DELETE FROM " + TABLE_NAME + " WHERE " + SEARCH + " = :query")
|
||||
int deleteAllWhereQuery(String query);
|
||||
|
||||
@Query("SELECT * FROM " + TABLE_NAME + ORDER_BY_CREATION_DATE)
|
||||
@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 * 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);
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package org.schabi.newpipe.database.history.dao;
|
||||
|
||||
|
||||
import android.arch.persistence.room.Dao;
|
||||
import android.arch.persistence.room.Query;
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
import org.schabi.newpipe.database.BasicDAO;
|
||||
import org.schabi.newpipe.database.history.model.StreamHistoryEntry;
|
||||
import org.schabi.newpipe.database.stream.StreamStatisticsEntry;
|
||||
import org.schabi.newpipe.database.history.model.StreamHistoryEntity;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import io.reactivex.Flowable;
|
||||
|
||||
import static org.schabi.newpipe.database.history.model.StreamHistoryEntity.STREAM_REPEAT_COUNT;
|
||||
import static org.schabi.newpipe.database.stream.StreamStatisticsEntry.STREAM_LATEST_DATE;
|
||||
import static org.schabi.newpipe.database.stream.StreamStatisticsEntry.STREAM_WATCH_COUNT;
|
||||
import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_ID;
|
||||
import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_TABLE;
|
||||
import static org.schabi.newpipe.database.history.model.StreamHistoryEntity.JOIN_STREAM_ID;
|
||||
import static org.schabi.newpipe.database.history.model.StreamHistoryEntity.STREAM_ACCESS_DATE;
|
||||
import static org.schabi.newpipe.database.history.model.StreamHistoryEntity.STREAM_HISTORY_TABLE;
|
||||
|
||||
@Dao
|
||||
public abstract class StreamHistoryDAO implements HistoryDAO<StreamHistoryEntity> {
|
||||
@Query("SELECT * FROM " + STREAM_HISTORY_TABLE +
|
||||
" WHERE " + STREAM_ACCESS_DATE + " = " +
|
||||
"(SELECT MAX(" + STREAM_ACCESS_DATE + ") FROM " + STREAM_HISTORY_TABLE + ")")
|
||||
@Override
|
||||
@Nullable
|
||||
public abstract StreamHistoryEntity getLatestEntry();
|
||||
|
||||
@Override
|
||||
@Query("SELECT * FROM " + STREAM_HISTORY_TABLE)
|
||||
public abstract Flowable<List<StreamHistoryEntity>> getAll();
|
||||
|
||||
@Override
|
||||
@Query("DELETE FROM " + STREAM_HISTORY_TABLE)
|
||||
public abstract int deleteAll();
|
||||
|
||||
@Override
|
||||
public Flowable<List<StreamHistoryEntity>> listByService(int serviceId) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Query("SELECT * FROM " + STREAM_TABLE +
|
||||
" INNER JOIN " + STREAM_HISTORY_TABLE +
|
||||
" ON " + STREAM_ID + " = " + JOIN_STREAM_ID +
|
||||
" ORDER BY " + STREAM_ACCESS_DATE + " DESC")
|
||||
public abstract Flowable<List<StreamHistoryEntry>> getHistory();
|
||||
|
||||
@Query("DELETE FROM " + STREAM_HISTORY_TABLE + " WHERE " + JOIN_STREAM_ID + " = :streamId")
|
||||
public abstract int deleteStreamHistory(final long streamId);
|
||||
|
||||
@Query("SELECT * FROM " + STREAM_TABLE +
|
||||
|
||||
// Select the latest entry and watch count for each stream id on history table
|
||||
" INNER JOIN " +
|
||||
"(SELECT " + JOIN_STREAM_ID + ", " +
|
||||
" MAX(" + STREAM_ACCESS_DATE + ") AS " + STREAM_LATEST_DATE + ", " +
|
||||
" SUM(" + STREAM_REPEAT_COUNT + ") AS " + STREAM_WATCH_COUNT +
|
||||
" FROM " + STREAM_HISTORY_TABLE + " GROUP BY " + JOIN_STREAM_ID + ")" +
|
||||
|
||||
" ON " + STREAM_ID + " = " + JOIN_STREAM_ID)
|
||||
public abstract Flowable<List<StreamStatisticsEntry>> getStatistics();
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
package org.schabi.newpipe.database.history.model;
|
||||
|
||||
import android.arch.persistence.room.ColumnInfo;
|
||||
import android.arch.persistence.room.Entity;
|
||||
import android.arch.persistence.room.Ignore;
|
||||
import android.arch.persistence.room.Index;
|
||||
import android.arch.persistence.room.PrimaryKey;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
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 Date creationDate;
|
||||
|
||||
@ColumnInfo(name = SERVICE_ID)
|
||||
private int serviceId;
|
||||
|
||||
@ColumnInfo(name = SEARCH)
|
||||
private String search;
|
||||
|
||||
public SearchHistoryEntry(Date creationDate, int serviceId, String search) {
|
||||
this.serviceId = serviceId;
|
||||
this.creationDate = creationDate;
|
||||
this.search = search;
|
||||
}
|
||||
|
||||
public long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public Date getCreationDate() {
|
||||
return creationDate;
|
||||
}
|
||||
|
||||
public void setCreationDate(Date creationDate) {
|
||||
this.creationDate = creationDate;
|
||||
}
|
||||
|
||||
public int getServiceId() {
|
||||
return serviceId;
|
||||
}
|
||||
|
||||
public void setServiceId(int serviceId) {
|
||||
this.serviceId = serviceId;
|
||||
}
|
||||
|
||||
public String getSearch() {
|
||||
return search;
|
||||
}
|
||||
|
||||
public void setSearch(String search) {
|
||||
this.search = search;
|
||||
}
|
||||
|
||||
@Ignore
|
||||
public boolean hasEqualValues(SearchHistoryEntry otherEntry) {
|
||||
return getServiceId() == otherEntry.getServiceId() &&
|
||||
getSearch().equals(otherEntry.getSearch());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
package org.schabi.newpipe.database.history.model;
|
||||
|
||||
import android.arch.persistence.room.ColumnInfo;
|
||||
import android.arch.persistence.room.Entity;
|
||||
import android.arch.persistence.room.ForeignKey;
|
||||
import android.arch.persistence.room.Ignore;
|
||||
import android.arch.persistence.room.Index;
|
||||
import android.support.annotation.NonNull;
|
||||
|
||||
import org.schabi.newpipe.database.stream.model.StreamEntity;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
import static android.arch.persistence.room.ForeignKey.CASCADE;
|
||||
import static org.schabi.newpipe.database.history.model.StreamHistoryEntity.STREAM_HISTORY_TABLE;
|
||||
import static org.schabi.newpipe.database.history.model.StreamHistoryEntity.JOIN_STREAM_ID;
|
||||
import static org.schabi.newpipe.database.history.model.StreamHistoryEntity.STREAM_ACCESS_DATE;
|
||||
|
||||
@Entity(tableName = STREAM_HISTORY_TABLE,
|
||||
primaryKeys = {JOIN_STREAM_ID, STREAM_ACCESS_DATE},
|
||||
// No need to index for timestamp as they will almost always be unique
|
||||
indices = {@Index(value = {JOIN_STREAM_ID})},
|
||||
foreignKeys = {
|
||||
@ForeignKey(entity = StreamEntity.class,
|
||||
parentColumns = StreamEntity.STREAM_ID,
|
||||
childColumns = JOIN_STREAM_ID,
|
||||
onDelete = CASCADE, onUpdate = CASCADE)
|
||||
})
|
||||
public class StreamHistoryEntity {
|
||||
final public static String STREAM_HISTORY_TABLE = "stream_history";
|
||||
final public static String JOIN_STREAM_ID = "stream_id";
|
||||
final public static String STREAM_ACCESS_DATE = "access_date";
|
||||
final public static String STREAM_REPEAT_COUNT = "repeat_count";
|
||||
|
||||
@ColumnInfo(name = JOIN_STREAM_ID)
|
||||
private long streamUid;
|
||||
|
||||
@NonNull
|
||||
@ColumnInfo(name = STREAM_ACCESS_DATE)
|
||||
private Date accessDate;
|
||||
|
||||
@ColumnInfo(name = STREAM_REPEAT_COUNT)
|
||||
private long repeatCount;
|
||||
|
||||
public StreamHistoryEntity(long streamUid, @NonNull Date accessDate, long repeatCount) {
|
||||
this.streamUid = streamUid;
|
||||
this.accessDate = accessDate;
|
||||
this.repeatCount = repeatCount;
|
||||
}
|
||||
|
||||
@Ignore
|
||||
public StreamHistoryEntity(long streamUid, @NonNull Date accessDate) {
|
||||
this(streamUid, accessDate, 1);
|
||||
}
|
||||
|
||||
public long getStreamUid() {
|
||||
return streamUid;
|
||||
}
|
||||
|
||||
public void setStreamUid(long streamUid) {
|
||||
this.streamUid = streamUid;
|
||||
}
|
||||
|
||||
public Date getAccessDate() {
|
||||
return accessDate;
|
||||
}
|
||||
|
||||
public void setAccessDate(@NonNull Date accessDate) {
|
||||
this.accessDate = accessDate;
|
||||
}
|
||||
|
||||
public long getRepeatCount() {
|
||||
return repeatCount;
|
||||
}
|
||||
|
||||
public void setRepeatCount(long repeatCount) {
|
||||
this.repeatCount = repeatCount;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package org.schabi.newpipe.database.history.model;
|
||||
|
||||
import android.arch.persistence.room.ColumnInfo;
|
||||
|
||||
import org.schabi.newpipe.database.stream.model.StreamEntity;
|
||||
import org.schabi.newpipe.extractor.stream.StreamType;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
public class StreamHistoryEntry {
|
||||
@ColumnInfo(name = StreamEntity.STREAM_ID)
|
||||
final public long uid;
|
||||
@ColumnInfo(name = StreamEntity.STREAM_SERVICE_ID)
|
||||
final public int serviceId;
|
||||
@ColumnInfo(name = StreamEntity.STREAM_URL)
|
||||
final public String url;
|
||||
@ColumnInfo(name = StreamEntity.STREAM_TITLE)
|
||||
final public String title;
|
||||
@ColumnInfo(name = StreamEntity.STREAM_TYPE)
|
||||
final public StreamType streamType;
|
||||
@ColumnInfo(name = StreamEntity.STREAM_DURATION)
|
||||
final public long duration;
|
||||
@ColumnInfo(name = StreamEntity.STREAM_UPLOADER)
|
||||
final public String uploader;
|
||||
@ColumnInfo(name = StreamEntity.STREAM_THUMBNAIL_URL)
|
||||
final public String thumbnailUrl;
|
||||
@ColumnInfo(name = StreamHistoryEntity.JOIN_STREAM_ID)
|
||||
final public long streamId;
|
||||
@ColumnInfo(name = StreamHistoryEntity.STREAM_ACCESS_DATE)
|
||||
final public Date accessDate;
|
||||
@ColumnInfo(name = StreamHistoryEntity.STREAM_REPEAT_COUNT)
|
||||
final public long repeatCount;
|
||||
|
||||
public StreamHistoryEntry(long uid, int serviceId, String url, String title,
|
||||
StreamType streamType, long duration, String uploader,
|
||||
String thumbnailUrl, long streamId, Date accessDate,
|
||||
long repeatCount) {
|
||||
this.uid = uid;
|
||||
this.serviceId = serviceId;
|
||||
this.url = url;
|
||||
this.title = title;
|
||||
this.streamType = streamType;
|
||||
this.duration = duration;
|
||||
this.uploader = uploader;
|
||||
this.thumbnailUrl = thumbnailUrl;
|
||||
this.streamId = streamId;
|
||||
this.accessDate = accessDate;
|
||||
this.repeatCount = repeatCount;
|
||||
}
|
||||
|
||||
public StreamHistoryEntity toStreamHistoryEntity() {
|
||||
return new StreamHistoryEntity(streamId, accessDate, repeatCount);
|
||||
}
|
||||
|
||||
public boolean hasEqualValues(StreamHistoryEntry other) {
|
||||
return this.uid == other.uid && streamId == other.streamId &&
|
||||
accessDate.compareTo(other.accessDate) == 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package org.schabi.newpipe.database.playlist;
|
||||
|
||||
import org.schabi.newpipe.database.LocalItem;
|
||||
|
||||
public interface PlaylistLocalItem extends LocalItem {
|
||||
String getOrderingName();
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package org.schabi.newpipe.database.playlist;
|
||||
|
||||
import android.arch.persistence.room.ColumnInfo;
|
||||
|
||||
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_ID;
|
||||
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_NAME;
|
||||
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_THUMBNAIL_URL;
|
||||
|
||||
public class PlaylistMetadataEntry implements PlaylistLocalItem {
|
||||
final public static String PLAYLIST_STREAM_COUNT = "streamCount";
|
||||
|
||||
@ColumnInfo(name = PLAYLIST_ID)
|
||||
final public long uid;
|
||||
@ColumnInfo(name = PLAYLIST_NAME)
|
||||
final public String name;
|
||||
@ColumnInfo(name = PLAYLIST_THUMBNAIL_URL)
|
||||
final public String thumbnailUrl;
|
||||
@ColumnInfo(name = PLAYLIST_STREAM_COUNT)
|
||||
final public long streamCount;
|
||||
|
||||
public PlaylistMetadataEntry(long uid, String name, String thumbnailUrl, long streamCount) {
|
||||
this.uid = uid;
|
||||
this.name = name;
|
||||
this.thumbnailUrl = thumbnailUrl;
|
||||
this.streamCount = streamCount;
|
||||
}
|
||||
|
||||
@Override
|
||||
public LocalItemType getLocalItemType() {
|
||||
return LocalItemType.PLAYLIST_LOCAL_ITEM;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getOrderingName() {
|
||||
return name;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package org.schabi.newpipe.database.playlist;
|
||||
|
||||
import android.arch.persistence.room.ColumnInfo;
|
||||
|
||||
import org.schabi.newpipe.database.LocalItem;
|
||||
import org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity;
|
||||
import org.schabi.newpipe.database.stream.model.StreamEntity;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||
import org.schabi.newpipe.extractor.stream.StreamType;
|
||||
|
||||
public class PlaylistStreamEntry implements LocalItem {
|
||||
@ColumnInfo(name = StreamEntity.STREAM_ID)
|
||||
final public long uid;
|
||||
@ColumnInfo(name = StreamEntity.STREAM_SERVICE_ID)
|
||||
final public int serviceId;
|
||||
@ColumnInfo(name = StreamEntity.STREAM_URL)
|
||||
final public String url;
|
||||
@ColumnInfo(name = StreamEntity.STREAM_TITLE)
|
||||
final public String title;
|
||||
@ColumnInfo(name = StreamEntity.STREAM_TYPE)
|
||||
final public StreamType streamType;
|
||||
@ColumnInfo(name = StreamEntity.STREAM_DURATION)
|
||||
final public long duration;
|
||||
@ColumnInfo(name = StreamEntity.STREAM_UPLOADER)
|
||||
final public String uploader;
|
||||
@ColumnInfo(name = StreamEntity.STREAM_THUMBNAIL_URL)
|
||||
final public String thumbnailUrl;
|
||||
@ColumnInfo(name = PlaylistStreamEntity.JOIN_STREAM_ID)
|
||||
final public long streamId;
|
||||
@ColumnInfo(name = PlaylistStreamEntity.JOIN_INDEX)
|
||||
final public int joinIndex;
|
||||
|
||||
public PlaylistStreamEntry(long uid, int serviceId, String url, String title,
|
||||
StreamType streamType, long duration, String uploader,
|
||||
String thumbnailUrl, long streamId, int joinIndex) {
|
||||
this.uid = uid;
|
||||
this.serviceId = serviceId;
|
||||
this.url = url;
|
||||
this.title = title;
|
||||
this.streamType = streamType;
|
||||
this.duration = duration;
|
||||
this.uploader = uploader;
|
||||
this.thumbnailUrl = thumbnailUrl;
|
||||
this.streamId = streamId;
|
||||
this.joinIndex = joinIndex;
|
||||
}
|
||||
|
||||
public StreamInfoItem toStreamInfoItem() throws IllegalArgumentException {
|
||||
StreamInfoItem item = new StreamInfoItem(serviceId, url, title, streamType);
|
||||
item.setThumbnailUrl(thumbnailUrl);
|
||||
item.setUploaderName(uploader);
|
||||
item.setDuration(duration);
|
||||
return item;
|
||||
}
|
||||
|
||||
@Override
|
||||
public LocalItemType getLocalItemType() {
|
||||
return LocalItemType.PLAYLIST_STREAM_ITEM;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package org.schabi.newpipe.database.playlist.dao;
|
||||
|
||||
import android.arch.persistence.room.Dao;
|
||||
import android.arch.persistence.room.Query;
|
||||
import android.arch.persistence.room.Transaction;
|
||||
|
||||
import org.schabi.newpipe.database.BasicDAO;
|
||||
import org.schabi.newpipe.database.playlist.model.PlaylistEntity;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import io.reactivex.Flowable;
|
||||
|
||||
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_ID;
|
||||
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_NAME;
|
||||
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_TABLE;
|
||||
|
||||
@Dao
|
||||
public abstract class PlaylistDAO implements BasicDAO<PlaylistEntity> {
|
||||
@Override
|
||||
@Query("SELECT * FROM " + PLAYLIST_TABLE)
|
||||
public abstract Flowable<List<PlaylistEntity>> getAll();
|
||||
|
||||
@Override
|
||||
@Query("DELETE FROM " + PLAYLIST_TABLE)
|
||||
public abstract int deleteAll();
|
||||
|
||||
@Override
|
||||
public Flowable<List<PlaylistEntity>> listByService(int serviceId) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Query("SELECT * FROM " + PLAYLIST_TABLE + " WHERE " + PLAYLIST_ID + " = :playlistId")
|
||||
public abstract Flowable<List<PlaylistEntity>> getPlaylist(final long playlistId);
|
||||
|
||||
@Query("DELETE FROM " + PLAYLIST_TABLE + " WHERE " + PLAYLIST_ID + " = :playlistId")
|
||||
public abstract int deletePlaylist(final long playlistId);
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package org.schabi.newpipe.database.playlist.dao;
|
||||
|
||||
import android.arch.persistence.room.Dao;
|
||||
import android.arch.persistence.room.Query;
|
||||
import android.arch.persistence.room.Transaction;
|
||||
|
||||
import org.schabi.newpipe.database.BasicDAO;
|
||||
import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import io.reactivex.Flowable;
|
||||
|
||||
import static org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity.REMOTE_PLAYLIST_ID;
|
||||
import static org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity.REMOTE_PLAYLIST_SERVICE_ID;
|
||||
import static org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity.REMOTE_PLAYLIST_TABLE;
|
||||
import static org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity.REMOTE_PLAYLIST_URL;
|
||||
|
||||
@Dao
|
||||
public abstract class PlaylistRemoteDAO implements BasicDAO<PlaylistRemoteEntity> {
|
||||
@Override
|
||||
@Query("SELECT * FROM " + REMOTE_PLAYLIST_TABLE)
|
||||
public abstract Flowable<List<PlaylistRemoteEntity>> getAll();
|
||||
|
||||
@Override
|
||||
@Query("DELETE FROM " + REMOTE_PLAYLIST_TABLE)
|
||||
public abstract int deleteAll();
|
||||
|
||||
@Override
|
||||
@Query("SELECT * FROM " + REMOTE_PLAYLIST_TABLE +
|
||||
" WHERE " + REMOTE_PLAYLIST_SERVICE_ID + " = :serviceId")
|
||||
public abstract 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);
|
||||
|
||||
@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);
|
||||
|
||||
@Transaction
|
||||
public long upsert(PlaylistRemoteEntity playlist) {
|
||||
final Long playlistId = getPlaylistIdInternal(playlist.getServiceId(), playlist.getUrl());
|
||||
|
||||
if (playlistId == null) {
|
||||
return insert(playlist);
|
||||
} else {
|
||||
playlist.setUid(playlistId);
|
||||
update(playlist);
|
||||
return playlistId;
|
||||
}
|
||||
}
|
||||
|
||||
@Query("DELETE FROM " + REMOTE_PLAYLIST_TABLE +
|
||||
" WHERE " + REMOTE_PLAYLIST_ID + " = :playlistId")
|
||||
public abstract int deletePlaylist(final long playlistId);
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
package org.schabi.newpipe.database.playlist.dao;
|
||||
|
||||
import android.arch.persistence.room.Dao;
|
||||
import android.arch.persistence.room.Query;
|
||||
import android.arch.persistence.room.Transaction;
|
||||
|
||||
import org.schabi.newpipe.database.BasicDAO;
|
||||
import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry;
|
||||
import org.schabi.newpipe.database.playlist.PlaylistStreamEntry;
|
||||
import org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity;
|
||||
import org.schabi.newpipe.database.stream.model.StreamEntity;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import io.reactivex.Flowable;
|
||||
|
||||
import static org.schabi.newpipe.database.playlist.PlaylistMetadataEntry.PLAYLIST_STREAM_COUNT;
|
||||
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.*;
|
||||
import static org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity.*;
|
||||
import static org.schabi.newpipe.database.stream.model.StreamEntity.*;
|
||||
|
||||
@Dao
|
||||
public abstract class PlaylistStreamDAO implements BasicDAO<PlaylistStreamEntity> {
|
||||
@Override
|
||||
@Query("SELECT * FROM " + PLAYLIST_STREAM_JOIN_TABLE)
|
||||
public abstract Flowable<List<PlaylistStreamEntity>> getAll();
|
||||
|
||||
@Override
|
||||
@Query("DELETE FROM " + PLAYLIST_STREAM_JOIN_TABLE)
|
||||
public abstract int deleteAll();
|
||||
|
||||
@Override
|
||||
public Flowable<List<PlaylistStreamEntity>> listByService(int serviceId) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Query("DELETE FROM " + PLAYLIST_STREAM_JOIN_TABLE +
|
||||
" WHERE " + JOIN_PLAYLIST_ID + " = :playlistId")
|
||||
public abstract void deleteBatch(final long playlistId);
|
||||
|
||||
@Query("SELECT COALESCE(MAX(" + JOIN_INDEX + "), -1)" +
|
||||
" FROM " + PLAYLIST_STREAM_JOIN_TABLE +
|
||||
" WHERE " + JOIN_PLAYLIST_ID + " = :playlistId")
|
||||
public abstract Flowable<Integer> getMaximumIndexOf(final long playlistId);
|
||||
|
||||
@Transaction
|
||||
@Query("SELECT * FROM " + STREAM_TABLE + " INNER JOIN " +
|
||||
// get ids of streams of the given playlist
|
||||
"(SELECT " + JOIN_STREAM_ID + "," + JOIN_INDEX +
|
||||
" FROM " + PLAYLIST_STREAM_JOIN_TABLE +
|
||||
" WHERE " + JOIN_PLAYLIST_ID + " = :playlistId)" +
|
||||
|
||||
// then merge with the stream metadata
|
||||
" ON " + STREAM_ID + " = " + JOIN_STREAM_ID +
|
||||
" ORDER BY " + JOIN_INDEX + " ASC")
|
||||
public abstract Flowable<List<PlaylistStreamEntry>> getOrderedStreamsOf(long playlistId);
|
||||
|
||||
@Transaction
|
||||
@Query("SELECT " + PLAYLIST_ID + ", " + PLAYLIST_NAME + ", " +
|
||||
PLAYLIST_THUMBNAIL_URL + ", " +
|
||||
"COALESCE(COUNT(" + JOIN_PLAYLIST_ID + "), 0) AS " + PLAYLIST_STREAM_COUNT +
|
||||
|
||||
" FROM " + PLAYLIST_TABLE +
|
||||
" LEFT JOIN " + PLAYLIST_STREAM_JOIN_TABLE +
|
||||
" ON " + PLAYLIST_ID + " = " + JOIN_PLAYLIST_ID +
|
||||
" GROUP BY " + JOIN_PLAYLIST_ID +
|
||||
" ORDER BY " + PLAYLIST_NAME + " COLLATE NOCASE ASC")
|
||||
public abstract Flowable<List<PlaylistMetadataEntry>> getPlaylistMetadata();
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package org.schabi.newpipe.database.playlist.model;
|
||||
|
||||
import android.arch.persistence.room.ColumnInfo;
|
||||
import android.arch.persistence.room.Entity;
|
||||
import android.arch.persistence.room.Index;
|
||||
import android.arch.persistence.room.PrimaryKey;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_NAME;
|
||||
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_TABLE;
|
||||
|
||||
@Entity(tableName = PLAYLIST_TABLE,
|
||||
indices = {@Index(value = {PLAYLIST_NAME})})
|
||||
public class PlaylistEntity {
|
||||
final public static String PLAYLIST_TABLE = "playlists";
|
||||
final public static String PLAYLIST_ID = "uid";
|
||||
final public static String PLAYLIST_NAME = "name";
|
||||
final public static String PLAYLIST_THUMBNAIL_URL = "thumbnail_url";
|
||||
|
||||
@PrimaryKey(autoGenerate = true)
|
||||
@ColumnInfo(name = PLAYLIST_ID)
|
||||
private long uid = 0;
|
||||
|
||||
@ColumnInfo(name = PLAYLIST_NAME)
|
||||
private String name;
|
||||
|
||||
@ColumnInfo(name = PLAYLIST_THUMBNAIL_URL)
|
||||
private String thumbnailUrl;
|
||||
|
||||
public PlaylistEntity(String name, String thumbnailUrl) {
|
||||
this.name = name;
|
||||
this.thumbnailUrl = thumbnailUrl;
|
||||
}
|
||||
|
||||
public long getUid() {
|
||||
return uid;
|
||||
}
|
||||
|
||||
public void setUid(long uid) {
|
||||
this.uid = uid;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String getThumbnailUrl() {
|
||||
return thumbnailUrl;
|
||||
}
|
||||
|
||||
public void setThumbnailUrl(String thumbnailUrl) {
|
||||
this.thumbnailUrl = thumbnailUrl;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
package org.schabi.newpipe.database.playlist.model;
|
||||
|
||||
import android.arch.persistence.room.ColumnInfo;
|
||||
import android.arch.persistence.room.Entity;
|
||||
import android.arch.persistence.room.Ignore;
|
||||
import android.arch.persistence.room.Index;
|
||||
import android.arch.persistence.room.PrimaryKey;
|
||||
|
||||
import org.schabi.newpipe.database.LocalItem;
|
||||
import org.schabi.newpipe.database.playlist.PlaylistLocalItem;
|
||||
import org.schabi.newpipe.extractor.playlist.PlaylistInfo;
|
||||
import org.schabi.newpipe.util.Constants;
|
||||
|
||||
import static org.schabi.newpipe.database.LocalItem.LocalItemType.PLAYLIST_REMOTE_ITEM;
|
||||
import static org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity.REMOTE_PLAYLIST_NAME;
|
||||
import static org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity.REMOTE_PLAYLIST_SERVICE_ID;
|
||||
import static org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity.REMOTE_PLAYLIST_TABLE;
|
||||
import static org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity.REMOTE_PLAYLIST_URL;
|
||||
|
||||
@Entity(tableName = REMOTE_PLAYLIST_TABLE,
|
||||
indices = {
|
||||
@Index(value = {REMOTE_PLAYLIST_NAME}),
|
||||
@Index(value = {REMOTE_PLAYLIST_SERVICE_ID, REMOTE_PLAYLIST_URL}, unique = true)
|
||||
})
|
||||
public class PlaylistRemoteEntity implements PlaylistLocalItem {
|
||||
final public static String REMOTE_PLAYLIST_TABLE = "remote_playlists";
|
||||
final public static String REMOTE_PLAYLIST_ID = "uid";
|
||||
final public static String REMOTE_PLAYLIST_SERVICE_ID = "service_id";
|
||||
final public static String REMOTE_PLAYLIST_NAME = "name";
|
||||
final public static String REMOTE_PLAYLIST_URL = "url";
|
||||
final public static String REMOTE_PLAYLIST_THUMBNAIL_URL = "thumbnail_url";
|
||||
final public static String REMOTE_PLAYLIST_UPLOADER_NAME = "uploader";
|
||||
final public static String REMOTE_PLAYLIST_STREAM_COUNT = "stream_count";
|
||||
|
||||
@PrimaryKey(autoGenerate = true)
|
||||
@ColumnInfo(name = REMOTE_PLAYLIST_ID)
|
||||
private long uid = 0;
|
||||
|
||||
@ColumnInfo(name = REMOTE_PLAYLIST_SERVICE_ID)
|
||||
private int serviceId = Constants.NO_SERVICE_ID;
|
||||
|
||||
@ColumnInfo(name = REMOTE_PLAYLIST_NAME)
|
||||
private String name;
|
||||
|
||||
@ColumnInfo(name = REMOTE_PLAYLIST_URL)
|
||||
private String url;
|
||||
|
||||
@ColumnInfo(name = REMOTE_PLAYLIST_THUMBNAIL_URL)
|
||||
private String thumbnailUrl;
|
||||
|
||||
@ColumnInfo(name = REMOTE_PLAYLIST_UPLOADER_NAME)
|
||||
private String uploader;
|
||||
|
||||
@ColumnInfo(name = REMOTE_PLAYLIST_STREAM_COUNT)
|
||||
private Long streamCount;
|
||||
|
||||
public PlaylistRemoteEntity(int serviceId, String name, String url, String thumbnailUrl,
|
||||
String uploader, Long streamCount) {
|
||||
this.serviceId = serviceId;
|
||||
this.name = name;
|
||||
this.url = url;
|
||||
this.thumbnailUrl = thumbnailUrl;
|
||||
this.uploader = uploader;
|
||||
this.streamCount = streamCount;
|
||||
}
|
||||
|
||||
@Ignore
|
||||
public PlaylistRemoteEntity(final PlaylistInfo info) {
|
||||
this(info.getServiceId(), info.getName(), info.getUrl(),
|
||||
info.getThumbnailUrl() == null ? info.getUploaderAvatarUrl() : info.getThumbnailUrl(),
|
||||
info.getUploaderName(), info.getStreamCount());
|
||||
}
|
||||
|
||||
public long getUid() {
|
||||
return uid;
|
||||
}
|
||||
|
||||
public void setUid(long uid) {
|
||||
this.uid = uid;
|
||||
}
|
||||
|
||||
public int getServiceId() {
|
||||
return serviceId;
|
||||
}
|
||||
|
||||
public void setServiceId(int serviceId) {
|
||||
this.serviceId = serviceId;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String getThumbnailUrl() {
|
||||
return thumbnailUrl;
|
||||
}
|
||||
|
||||
public void setThumbnailUrl(String thumbnailUrl) {
|
||||
this.thumbnailUrl = thumbnailUrl;
|
||||
}
|
||||
|
||||
public String getUrl() {
|
||||
return url;
|
||||
}
|
||||
|
||||
public void setUrl(String url) {
|
||||
this.url = url;
|
||||
}
|
||||
|
||||
public String getUploader() {
|
||||
return uploader;
|
||||
}
|
||||
|
||||
public void setUploader(String uploader) {
|
||||
this.uploader = uploader;
|
||||
}
|
||||
|
||||
public Long getStreamCount() {
|
||||
return streamCount;
|
||||
}
|
||||
|
||||
public void setStreamCount(Long streamCount) {
|
||||
this.streamCount = streamCount;
|
||||
}
|
||||
|
||||
@Override
|
||||
public LocalItemType getLocalItemType() {
|
||||
return PLAYLIST_REMOTE_ITEM;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getOrderingName() {
|
||||
return name;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
package org.schabi.newpipe.database.playlist.model;
|
||||
|
||||
import android.arch.persistence.room.ColumnInfo;
|
||||
import android.arch.persistence.room.Entity;
|
||||
import android.arch.persistence.room.ForeignKey;
|
||||
import android.arch.persistence.room.Index;
|
||||
|
||||
import org.schabi.newpipe.database.stream.model.StreamEntity;
|
||||
|
||||
import static android.arch.persistence.room.ForeignKey.CASCADE;
|
||||
import static org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity.JOIN_INDEX;
|
||||
import static org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity.JOIN_PLAYLIST_ID;
|
||||
import static org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity.JOIN_STREAM_ID;
|
||||
import static org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity.PLAYLIST_STREAM_JOIN_TABLE;
|
||||
|
||||
@Entity(tableName = PLAYLIST_STREAM_JOIN_TABLE,
|
||||
primaryKeys = {JOIN_PLAYLIST_ID, JOIN_INDEX},
|
||||
indices = {
|
||||
@Index(value = {JOIN_PLAYLIST_ID, JOIN_INDEX}, unique = true),
|
||||
@Index(value = {JOIN_STREAM_ID})
|
||||
},
|
||||
foreignKeys = {
|
||||
@ForeignKey(entity = PlaylistEntity.class,
|
||||
parentColumns = PlaylistEntity.PLAYLIST_ID,
|
||||
childColumns = JOIN_PLAYLIST_ID,
|
||||
onDelete = CASCADE, onUpdate = CASCADE, deferred = true),
|
||||
@ForeignKey(entity = StreamEntity.class,
|
||||
parentColumns = StreamEntity.STREAM_ID,
|
||||
childColumns = JOIN_STREAM_ID,
|
||||
onDelete = CASCADE, onUpdate = CASCADE, deferred = true)
|
||||
})
|
||||
public class PlaylistStreamEntity {
|
||||
|
||||
final public static String PLAYLIST_STREAM_JOIN_TABLE = "playlist_stream_join";
|
||||
final public static String JOIN_PLAYLIST_ID = "playlist_id";
|
||||
final public static String JOIN_STREAM_ID = "stream_id";
|
||||
final public static String JOIN_INDEX = "join_index";
|
||||
|
||||
@ColumnInfo(name = JOIN_PLAYLIST_ID)
|
||||
private long playlistUid;
|
||||
|
||||
@ColumnInfo(name = JOIN_STREAM_ID)
|
||||
private long streamUid;
|
||||
|
||||
@ColumnInfo(name = JOIN_INDEX)
|
||||
private int index;
|
||||
|
||||
public PlaylistStreamEntity(final long playlistUid, final long streamUid, final int index) {
|
||||
this.playlistUid = playlistUid;
|
||||
this.streamUid = streamUid;
|
||||
this.index = index;
|
||||
}
|
||||
|
||||
public long getPlaylistUid() {
|
||||
return playlistUid;
|
||||
}
|
||||
|
||||
public long getStreamUid() {
|
||||
return streamUid;
|
||||
}
|
||||
|
||||
public int getIndex() {
|
||||
return index;
|
||||
}
|
||||
|
||||
public void setPlaylistUid(long playlistUid) {
|
||||
this.playlistUid = playlistUid;
|
||||
}
|
||||
|
||||
public void setStreamUid(long streamUid) {
|
||||
this.streamUid = streamUid;
|
||||
}
|
||||
|
||||
public void setIndex(int index) {
|
||||
this.index = index;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
package org.schabi.newpipe.database.stream;
|
||||
|
||||
import android.arch.persistence.room.ColumnInfo;
|
||||
|
||||
import org.schabi.newpipe.database.LocalItem;
|
||||
import org.schabi.newpipe.database.history.model.StreamHistoryEntity;
|
||||
import org.schabi.newpipe.database.stream.model.StreamEntity;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||
import org.schabi.newpipe.extractor.stream.StreamType;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
public class StreamStatisticsEntry implements LocalItem {
|
||||
final public static String STREAM_LATEST_DATE = "latestAccess";
|
||||
final public static String STREAM_WATCH_COUNT = "watchCount";
|
||||
|
||||
@ColumnInfo(name = StreamEntity.STREAM_ID)
|
||||
final public long uid;
|
||||
@ColumnInfo(name = StreamEntity.STREAM_SERVICE_ID)
|
||||
final public int serviceId;
|
||||
@ColumnInfo(name = StreamEntity.STREAM_URL)
|
||||
final public String url;
|
||||
@ColumnInfo(name = StreamEntity.STREAM_TITLE)
|
||||
final public String title;
|
||||
@ColumnInfo(name = StreamEntity.STREAM_TYPE)
|
||||
final public StreamType streamType;
|
||||
@ColumnInfo(name = StreamEntity.STREAM_DURATION)
|
||||
final public long duration;
|
||||
@ColumnInfo(name = StreamEntity.STREAM_UPLOADER)
|
||||
final public String uploader;
|
||||
@ColumnInfo(name = StreamEntity.STREAM_THUMBNAIL_URL)
|
||||
final public String thumbnailUrl;
|
||||
@ColumnInfo(name = StreamHistoryEntity.JOIN_STREAM_ID)
|
||||
final public long streamId;
|
||||
@ColumnInfo(name = StreamStatisticsEntry.STREAM_LATEST_DATE)
|
||||
final public Date latestAccessDate;
|
||||
@ColumnInfo(name = StreamStatisticsEntry.STREAM_WATCH_COUNT)
|
||||
final public long watchCount;
|
||||
|
||||
public StreamStatisticsEntry(long uid, int serviceId, String url, String title,
|
||||
StreamType streamType, long duration, String uploader,
|
||||
String thumbnailUrl, long streamId, Date latestAccessDate,
|
||||
long watchCount) {
|
||||
this.uid = uid;
|
||||
this.serviceId = serviceId;
|
||||
this.url = url;
|
||||
this.title = title;
|
||||
this.streamType = streamType;
|
||||
this.duration = duration;
|
||||
this.uploader = uploader;
|
||||
this.thumbnailUrl = thumbnailUrl;
|
||||
this.streamId = streamId;
|
||||
this.latestAccessDate = latestAccessDate;
|
||||
this.watchCount = watchCount;
|
||||
}
|
||||
|
||||
public StreamInfoItem toStreamInfoItem() {
|
||||
StreamInfoItem item = new StreamInfoItem(serviceId, url, title, streamType);
|
||||
item.setDuration(duration);
|
||||
item.setUploaderName(uploader);
|
||||
item.setThumbnailUrl(thumbnailUrl);
|
||||
return item;
|
||||
}
|
||||
|
||||
@Override
|
||||
public LocalItemType getLocalItemType() {
|
||||
return LocalItemType.STATISTIC_STREAM_ITEM;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
package org.schabi.newpipe.database.stream.dao;
|
||||
|
||||
import android.arch.persistence.room.Dao;
|
||||
import android.arch.persistence.room.Insert;
|
||||
import android.arch.persistence.room.OnConflictStrategy;
|
||||
import android.arch.persistence.room.Query;
|
||||
import android.arch.persistence.room.Transaction;
|
||||
|
||||
import org.schabi.newpipe.database.BasicDAO;
|
||||
import org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity;
|
||||
import org.schabi.newpipe.database.stream.model.StreamEntity;
|
||||
import org.schabi.newpipe.database.history.model.StreamHistoryEntity;
|
||||
import org.schabi.newpipe.database.stream.model.StreamStateEntity;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import io.reactivex.Flowable;
|
||||
|
||||
import static org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity.PLAYLIST_STREAM_JOIN_TABLE;
|
||||
import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_ID;
|
||||
import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_SERVICE_ID;
|
||||
import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_TABLE;
|
||||
import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_URL;
|
||||
import static org.schabi.newpipe.database.history.model.StreamHistoryEntity.STREAM_HISTORY_TABLE;
|
||||
import static org.schabi.newpipe.database.stream.model.StreamStateEntity.STREAM_STATE_TABLE;
|
||||
|
||||
@Dao
|
||||
public abstract class StreamDAO implements BasicDAO<StreamEntity> {
|
||||
@Override
|
||||
@Query("SELECT * FROM " + STREAM_TABLE)
|
||||
public abstract Flowable<List<StreamEntity>> getAll();
|
||||
|
||||
@Override
|
||||
@Query("DELETE FROM " + STREAM_TABLE)
|
||||
public abstract int deleteAll();
|
||||
|
||||
@Override
|
||||
@Query("SELECT * FROM " + STREAM_TABLE + " WHERE " + STREAM_SERVICE_ID + " = :serviceId")
|
||||
public abstract Flowable<List<StreamEntity>> listByService(int serviceId);
|
||||
|
||||
@Query("SELECT * FROM " + STREAM_TABLE + " WHERE " +
|
||||
STREAM_URL + " = :url AND " +
|
||||
STREAM_SERVICE_ID + " = :serviceId")
|
||||
public abstract Flowable<List<StreamEntity>> getStream(long serviceId, String url);
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
||||
abstract void silentInsertAllInternal(final List<StreamEntity> streams);
|
||||
|
||||
@Query("SELECT " + STREAM_ID + " FROM " + STREAM_TABLE + " WHERE " +
|
||||
STREAM_URL + " = :url AND " +
|
||||
STREAM_SERVICE_ID + " = :serviceId")
|
||||
abstract Long getStreamIdInternal(long serviceId, String url);
|
||||
|
||||
@Transaction
|
||||
public long upsert(StreamEntity stream) {
|
||||
final Long streamIdCandidate = getStreamIdInternal(stream.getServiceId(), stream.getUrl());
|
||||
|
||||
if (streamIdCandidate == null) {
|
||||
return insert(stream);
|
||||
} else {
|
||||
stream.setUid(streamIdCandidate);
|
||||
update(stream);
|
||||
return streamIdCandidate;
|
||||
}
|
||||
}
|
||||
|
||||
@Transaction
|
||||
public List<Long> upsertAll(List<StreamEntity> streams) {
|
||||
silentInsertAllInternal(streams);
|
||||
|
||||
final List<Long> streamIds = new ArrayList<>(streams.size());
|
||||
for (StreamEntity stream : streams) {
|
||||
final Long streamId = getStreamIdInternal(stream.getServiceId(), stream.getUrl());
|
||||
if (streamId == null) {
|
||||
throw new IllegalStateException("StreamID cannot be null just after insertion.");
|
||||
}
|
||||
|
||||
streamIds.add(streamId);
|
||||
stream.setUid(streamId);
|
||||
}
|
||||
|
||||
update(streams);
|
||||
return streamIds;
|
||||
}
|
||||
|
||||
@Query("DELETE FROM " + STREAM_TABLE + " WHERE " + STREAM_ID +
|
||||
" NOT IN " +
|
||||
"(SELECT DISTINCT " + STREAM_ID + " FROM " + STREAM_TABLE +
|
||||
|
||||
" LEFT JOIN " + STREAM_HISTORY_TABLE +
|
||||
" ON " + STREAM_ID + " = " +
|
||||
StreamHistoryEntity.STREAM_HISTORY_TABLE + "." + StreamHistoryEntity.JOIN_STREAM_ID +
|
||||
|
||||
" LEFT JOIN " + PLAYLIST_STREAM_JOIN_TABLE +
|
||||
" ON " + STREAM_ID + " = " +
|
||||
PlaylistStreamEntity.PLAYLIST_STREAM_JOIN_TABLE + "." + PlaylistStreamEntity.JOIN_STREAM_ID +
|
||||
")")
|
||||
public abstract int deleteOrphans();
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package org.schabi.newpipe.database.stream.dao;
|
||||
|
||||
import android.arch.persistence.room.Dao;
|
||||
import android.arch.persistence.room.Insert;
|
||||
import android.arch.persistence.room.OnConflictStrategy;
|
||||
import android.arch.persistence.room.Query;
|
||||
import android.arch.persistence.room.Transaction;
|
||||
|
||||
import org.schabi.newpipe.database.BasicDAO;
|
||||
import org.schabi.newpipe.database.stream.model.StreamStateEntity;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import io.reactivex.Flowable;
|
||||
|
||||
import static org.schabi.newpipe.database.stream.model.StreamStateEntity.JOIN_STREAM_ID;
|
||||
import static org.schabi.newpipe.database.stream.model.StreamStateEntity.STREAM_STATE_TABLE;
|
||||
|
||||
@Dao
|
||||
public abstract class StreamStateDAO implements BasicDAO<StreamStateEntity> {
|
||||
@Override
|
||||
@Query("SELECT * FROM " + STREAM_STATE_TABLE)
|
||||
public abstract Flowable<List<StreamStateEntity>> getAll();
|
||||
|
||||
@Override
|
||||
@Query("DELETE FROM " + STREAM_STATE_TABLE)
|
||||
public abstract int deleteAll();
|
||||
|
||||
@Override
|
||||
public Flowable<List<StreamStateEntity>> listByService(int serviceId) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Query("SELECT * FROM " + STREAM_STATE_TABLE + " WHERE " + JOIN_STREAM_ID + " = :streamId")
|
||||
public abstract Flowable<List<StreamStateEntity>> getState(final long streamId);
|
||||
|
||||
@Query("DELETE FROM " + STREAM_STATE_TABLE + " WHERE " + JOIN_STREAM_ID + " = :streamId")
|
||||
public abstract int deleteState(final long streamId);
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
||||
abstract void silentInsertInternal(final StreamStateEntity streamState);
|
||||
|
||||
@Transaction
|
||||
public long upsert(StreamStateEntity stream) {
|
||||
silentInsertInternal(stream);
|
||||
return update(stream);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
package org.schabi.newpipe.database.stream.model;
|
||||
|
||||
import android.arch.persistence.room.ColumnInfo;
|
||||
import android.arch.persistence.room.Entity;
|
||||
import android.arch.persistence.room.Ignore;
|
||||
import android.arch.persistence.room.Index;
|
||||
import android.arch.persistence.room.PrimaryKey;
|
||||
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||
import org.schabi.newpipe.extractor.stream.StreamType;
|
||||
import org.schabi.newpipe.playlist.PlayQueueItem;
|
||||
import org.schabi.newpipe.util.Constants;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_SERVICE_ID;
|
||||
import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_TABLE;
|
||||
import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_URL;
|
||||
|
||||
@Entity(tableName = STREAM_TABLE,
|
||||
indices = {@Index(value = {STREAM_SERVICE_ID, STREAM_URL}, unique = true)})
|
||||
public class StreamEntity implements Serializable {
|
||||
|
||||
final public static String STREAM_TABLE = "streams";
|
||||
final public static String STREAM_ID = "uid";
|
||||
final public static String STREAM_SERVICE_ID = "service_id";
|
||||
final public static String STREAM_URL = "url";
|
||||
final public static String STREAM_TITLE = "title";
|
||||
final public static String STREAM_TYPE = "stream_type";
|
||||
final public static String STREAM_DURATION = "duration";
|
||||
final public static String STREAM_UPLOADER = "uploader";
|
||||
final public static String STREAM_THUMBNAIL_URL = "thumbnail_url";
|
||||
|
||||
@PrimaryKey(autoGenerate = true)
|
||||
@ColumnInfo(name = STREAM_ID)
|
||||
private long uid = 0;
|
||||
|
||||
@ColumnInfo(name = STREAM_SERVICE_ID)
|
||||
private int serviceId = Constants.NO_SERVICE_ID;
|
||||
|
||||
@ColumnInfo(name = STREAM_URL)
|
||||
private String url;
|
||||
|
||||
@ColumnInfo(name = STREAM_TITLE)
|
||||
private String title;
|
||||
|
||||
@ColumnInfo(name = STREAM_TYPE)
|
||||
private StreamType streamType;
|
||||
|
||||
@ColumnInfo(name = STREAM_DURATION)
|
||||
private Long duration;
|
||||
|
||||
@ColumnInfo(name = STREAM_UPLOADER)
|
||||
private String uploader;
|
||||
|
||||
@ColumnInfo(name = STREAM_THUMBNAIL_URL)
|
||||
private String thumbnailUrl;
|
||||
|
||||
public StreamEntity(final int serviceId, final String title, final String url,
|
||||
final StreamType streamType, final String thumbnailUrl, final String uploader,
|
||||
final long duration) {
|
||||
this.serviceId = serviceId;
|
||||
this.title = title;
|
||||
this.url = url;
|
||||
this.streamType = streamType;
|
||||
this.thumbnailUrl = thumbnailUrl;
|
||||
this.uploader = uploader;
|
||||
this.duration = duration;
|
||||
}
|
||||
|
||||
@Ignore
|
||||
public StreamEntity(final StreamInfoItem item) {
|
||||
this(item.service_id, item.name, item.url, item.stream_type, item.thumbnail_url,
|
||||
item.uploader_name, item.duration);
|
||||
}
|
||||
|
||||
@Ignore
|
||||
public StreamEntity(final StreamInfo info) {
|
||||
this(info.service_id, info.name, info.url, info.stream_type, info.thumbnail_url,
|
||||
info.uploader_name, info.duration);
|
||||
}
|
||||
|
||||
@Ignore
|
||||
public StreamEntity(final PlayQueueItem item) {
|
||||
this(item.getServiceId(), item.getTitle(), item.getUrl(), item.getStreamType(),
|
||||
item.getThumbnailUrl(), item.getUploader(), item.getDuration());
|
||||
}
|
||||
|
||||
public long getUid() {
|
||||
return uid;
|
||||
}
|
||||
|
||||
public void setUid(long uid) {
|
||||
this.uid = uid;
|
||||
}
|
||||
|
||||
public int getServiceId() {
|
||||
return serviceId;
|
||||
}
|
||||
|
||||
public void setServiceId(int serviceId) {
|
||||
this.serviceId = serviceId;
|
||||
}
|
||||
|
||||
public String getUrl() {
|
||||
return url;
|
||||
}
|
||||
|
||||
public void setUrl(String url) {
|
||||
this.url = url;
|
||||
}
|
||||
|
||||
public String getTitle() {
|
||||
return title;
|
||||
}
|
||||
|
||||
public void setTitle(String title) {
|
||||
this.title = title;
|
||||
}
|
||||
|
||||
public StreamType getStreamType() {
|
||||
return streamType;
|
||||
}
|
||||
|
||||
public void setStreamType(StreamType type) {
|
||||
this.streamType = type;
|
||||
}
|
||||
|
||||
public Long getDuration() {
|
||||
return duration;
|
||||
}
|
||||
|
||||
public void setDuration(Long duration) {
|
||||
this.duration = duration;
|
||||
}
|
||||
|
||||
public String getUploader() {
|
||||
return uploader;
|
||||
}
|
||||
|
||||
public void setUploader(String uploader) {
|
||||
this.uploader = uploader;
|
||||
}
|
||||
|
||||
public String getThumbnailUrl() {
|
||||
return thumbnailUrl;
|
||||
}
|
||||
|
||||
public void setThumbnailUrl(String thumbnailUrl) {
|
||||
this.thumbnailUrl = thumbnailUrl;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package org.schabi.newpipe.database.stream.model;
|
||||
|
||||
|
||||
import android.arch.persistence.room.ColumnInfo;
|
||||
import android.arch.persistence.room.Entity;
|
||||
import android.arch.persistence.room.ForeignKey;
|
||||
|
||||
import static android.arch.persistence.room.ForeignKey.CASCADE;
|
||||
import static org.schabi.newpipe.database.stream.model.StreamStateEntity.JOIN_STREAM_ID;
|
||||
import static org.schabi.newpipe.database.stream.model.StreamStateEntity.STREAM_STATE_TABLE;
|
||||
|
||||
@Entity(tableName = STREAM_STATE_TABLE,
|
||||
primaryKeys = {JOIN_STREAM_ID},
|
||||
foreignKeys = {
|
||||
@ForeignKey(entity = StreamEntity.class,
|
||||
parentColumns = StreamEntity.STREAM_ID,
|
||||
childColumns = JOIN_STREAM_ID,
|
||||
onDelete = CASCADE, onUpdate = CASCADE)
|
||||
})
|
||||
public class StreamStateEntity {
|
||||
final public static String STREAM_STATE_TABLE = "stream_state";
|
||||
final public static String JOIN_STREAM_ID = "stream_id";
|
||||
final public static String STREAM_PROGRESS_TIME = "progress_time";
|
||||
|
||||
@ColumnInfo(name = JOIN_STREAM_ID)
|
||||
private long streamUid;
|
||||
|
||||
@ColumnInfo(name = STREAM_PROGRESS_TIME)
|
||||
private long progressTime;
|
||||
|
||||
public StreamStateEntity(long streamUid, long progressTime) {
|
||||
this.streamUid = streamUid;
|
||||
this.progressTime = progressTime;
|
||||
}
|
||||
|
||||
public long getStreamUid() {
|
||||
return streamUid;
|
||||
}
|
||||
|
||||
public void setStreamUid(long streamUid) {
|
||||
this.streamUid = streamUid;
|
||||
}
|
||||
|
||||
public long getProgressTime() {
|
||||
return progressTime;
|
||||
}
|
||||
|
||||
public void setProgressTime(long progressTime) {
|
||||
this.progressTime = progressTime;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package org.schabi.newpipe.database.subscription;
|
||||
|
||||
import android.arch.persistence.room.Dao;
|
||||
import android.arch.persistence.room.Query;
|
||||
|
||||
import org.schabi.newpipe.database.BasicDAO;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import io.reactivex.Flowable;
|
||||
|
||||
import static org.schabi.newpipe.database.subscription.SubscriptionEntity.SUBSCRIPTION_SERVICE_ID;
|
||||
import static org.schabi.newpipe.database.subscription.SubscriptionEntity.SUBSCRIPTION_TABLE;
|
||||
import static org.schabi.newpipe.database.subscription.SubscriptionEntity.SUBSCRIPTION_URL;
|
||||
|
||||
@Dao
|
||||
public interface SubscriptionDAO extends BasicDAO<SubscriptionEntity> {
|
||||
@Override
|
||||
@Query("SELECT * FROM " + SUBSCRIPTION_TABLE)
|
||||
Flowable<List<SubscriptionEntity>> getAll();
|
||||
|
||||
@Override
|
||||
@Query("DELETE FROM " + SUBSCRIPTION_TABLE)
|
||||
int deleteAll();
|
||||
|
||||
@Override
|
||||
@Query("SELECT * FROM " + SUBSCRIPTION_TABLE + " WHERE " + SUBSCRIPTION_SERVICE_ID + " = :serviceId")
|
||||
Flowable<List<SubscriptionEntity>> listByService(int serviceId);
|
||||
|
||||
@Query("SELECT * FROM " + SUBSCRIPTION_TABLE + " WHERE " +
|
||||
SUBSCRIPTION_URL + " LIKE :url AND " +
|
||||
SUBSCRIPTION_SERVICE_ID + " = :serviceId")
|
||||
Flowable<List<SubscriptionEntity>> getSubscription(int serviceId, String url);
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
package org.schabi.newpipe.database.subscription;
|
||||
|
||||
import android.arch.persistence.room.ColumnInfo;
|
||||
import android.arch.persistence.room.Entity;
|
||||
import android.arch.persistence.room.Ignore;
|
||||
import android.arch.persistence.room.Index;
|
||||
import android.arch.persistence.room.PrimaryKey;
|
||||
|
||||
import org.schabi.newpipe.extractor.channel.ChannelInfoItem;
|
||||
import org.schabi.newpipe.util.Constants;
|
||||
|
||||
import static org.schabi.newpipe.database.subscription.SubscriptionEntity.SUBSCRIPTION_SERVICE_ID;
|
||||
import static org.schabi.newpipe.database.subscription.SubscriptionEntity.SUBSCRIPTION_TABLE;
|
||||
import static org.schabi.newpipe.database.subscription.SubscriptionEntity.SUBSCRIPTION_URL;
|
||||
|
||||
@Entity(tableName = SUBSCRIPTION_TABLE,
|
||||
indices = {@Index(value = {SUBSCRIPTION_SERVICE_ID, SUBSCRIPTION_URL}, unique = true)})
|
||||
public class SubscriptionEntity {
|
||||
|
||||
final static String SUBSCRIPTION_TABLE = "subscriptions";
|
||||
final static String SUBSCRIPTION_SERVICE_ID = "service_id";
|
||||
final static String SUBSCRIPTION_URL = "url";
|
||||
final static String SUBSCRIPTION_NAME = "name";
|
||||
final static String SUBSCRIPTION_AVATAR_URL = "avatar_url";
|
||||
final static String SUBSCRIPTION_SUBSCRIBER_COUNT = "subscriber_count";
|
||||
final static String SUBSCRIPTION_DESCRIPTION = "description";
|
||||
|
||||
@PrimaryKey(autoGenerate = true)
|
||||
private long uid = 0;
|
||||
|
||||
@ColumnInfo(name = SUBSCRIPTION_SERVICE_ID)
|
||||
private int serviceId = Constants.NO_SERVICE_ID;
|
||||
|
||||
@ColumnInfo(name = SUBSCRIPTION_URL)
|
||||
private String url;
|
||||
|
||||
@ColumnInfo(name = SUBSCRIPTION_NAME)
|
||||
private String name;
|
||||
|
||||
@ColumnInfo(name = SUBSCRIPTION_AVATAR_URL)
|
||||
private String avatarUrl;
|
||||
|
||||
@ColumnInfo(name = SUBSCRIPTION_SUBSCRIBER_COUNT)
|
||||
private Long subscriberCount;
|
||||
|
||||
@ColumnInfo(name = SUBSCRIPTION_DESCRIPTION)
|
||||
private String description;
|
||||
|
||||
public long getUid() {
|
||||
return uid;
|
||||
}
|
||||
|
||||
public void setUid(long uid) {
|
||||
this.uid = uid;
|
||||
}
|
||||
|
||||
public int getServiceId() {
|
||||
return serviceId;
|
||||
}
|
||||
|
||||
public void setServiceId(int serviceId) {
|
||||
this.serviceId = serviceId;
|
||||
}
|
||||
|
||||
public String getUrl() {
|
||||
return url;
|
||||
}
|
||||
|
||||
public void setUrl(String url) {
|
||||
this.url = url;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String getAvatarUrl() {
|
||||
return avatarUrl;
|
||||
}
|
||||
|
||||
public void setAvatarUrl(String avatarUrl) {
|
||||
this.avatarUrl = avatarUrl;
|
||||
}
|
||||
|
||||
public Long getSubscriberCount() {
|
||||
return subscriberCount;
|
||||
}
|
||||
|
||||
public void setSubscriberCount(Long subscriberCount) {
|
||||
this.subscriberCount = subscriberCount;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
|
||||
public void setDescription(String description) {
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
@Ignore
|
||||
public void setData(final String name,
|
||||
final String avatarUrl,
|
||||
final String description,
|
||||
final Long subscriberCount) {
|
||||
this.setName(name);
|
||||
this.setAvatarUrl(avatarUrl);
|
||||
this.setDescription(description);
|
||||
this.setSubscriberCount(subscriberCount);
|
||||
}
|
||||
|
||||
@Ignore
|
||||
public ChannelInfoItem toChannelInfoItem() {
|
||||
ChannelInfoItem item = new ChannelInfoItem(getServiceId(), getUrl(), getName());
|
||||
item.thumbnail_url = getAvatarUrl();
|
||||
item.subscriber_count = getSubscriberCount();
|
||||
item.description = getDescription();
|
||||
return item;
|
||||
}
|
||||
}
|
||||
@@ -32,7 +32,7 @@ public class DownloadActivity extends AppCompatActivity {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_downloader);
|
||||
|
||||
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
|
||||
Toolbar toolbar = findViewById(R.id.toolbar);
|
||||
setSupportActionBar(toolbar);
|
||||
|
||||
ActionBar actionBar = getSupportActionBar();
|
||||
|
||||
@@ -22,14 +22,15 @@ import android.widget.TextView;
|
||||
import org.schabi.newpipe.MainActivity;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.extractor.MediaFormat;
|
||||
import org.schabi.newpipe.extractor.stream_info.AudioStream;
|
||||
import org.schabi.newpipe.extractor.stream_info.StreamInfo;
|
||||
import org.schabi.newpipe.extractor.stream_info.VideoStream;
|
||||
import org.schabi.newpipe.extractor.stream.AudioStream;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||
import org.schabi.newpipe.extractor.stream.VideoStream;
|
||||
import org.schabi.newpipe.fragments.detail.SpinnerToolbarAdapter;
|
||||
import org.schabi.newpipe.settings.NewPipeSettings;
|
||||
import org.schabi.newpipe.util.FilenameUtils;
|
||||
import org.schabi.newpipe.util.ListHelper;
|
||||
import org.schabi.newpipe.util.PermissionHelper;
|
||||
import org.schabi.newpipe.util.ThemeHelper;
|
||||
import org.schabi.newpipe.util.Utils;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
@@ -106,19 +107,19 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
|
||||
@Override
|
||||
public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
nameEditText = ((EditText) view.findViewById(R.id.file_name));
|
||||
nameEditText.setText(createFileName(currentInfo.title));
|
||||
selectedAudioIndex = Utils.getPreferredAudioFormat(getContext(), currentInfo.audio_streams);
|
||||
nameEditText = view.findViewById(R.id.file_name);
|
||||
nameEditText.setText(FilenameUtils.createFilename(getContext(), currentInfo.getName()));
|
||||
selectedAudioIndex = ListHelper.getDefaultAudioFormat(getContext(), currentInfo.getAudioStreams());
|
||||
|
||||
streamsSpinner = (Spinner) view.findViewById(R.id.quality_spinner);
|
||||
streamsSpinner = view.findViewById(R.id.quality_spinner);
|
||||
streamsSpinner.setOnItemSelectedListener(this);
|
||||
|
||||
threadsCountTextView = (TextView) view.findViewById(R.id.threads_count);
|
||||
threadsSeekBar = (SeekBar) view.findViewById(R.id.threads);
|
||||
radioVideoAudioGroup = (RadioGroup) view.findViewById(R.id.video_audio_group);
|
||||
threadsCountTextView = view.findViewById(R.id.threads_count);
|
||||
threadsSeekBar = view.findViewById(R.id.threads);
|
||||
radioVideoAudioGroup = view.findViewById(R.id.video_audio_group);
|
||||
radioVideoAudioGroup.setOnCheckedChangeListener(this);
|
||||
|
||||
initToolbar((Toolbar) view.findViewById(R.id.toolbar));
|
||||
initToolbar(view.<Toolbar>findViewById(R.id.toolbar));
|
||||
checkDownloadOptions(view);
|
||||
setupVideoSpinner(sortedStreamVideosList, streamsSpinner);
|
||||
|
||||
@@ -134,12 +135,10 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
|
||||
|
||||
@Override
|
||||
public void onStartTrackingTouch(SeekBar p1) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStopTrackingTouch(SeekBar p1) {
|
||||
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -184,7 +183,7 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
|
||||
String[] items = new String[audioStreams.size()];
|
||||
for (int i = 0; i < audioStreams.size(); i++) {
|
||||
AudioStream audioStream = audioStreams.get(i);
|
||||
items[i] = MediaFormat.getNameById(audioStream.format) + " " + audioStream.avgBitrate + "kbps";
|
||||
items[i] = audioStream.getFormat().getName() + " " + audioStream.getAverageBitrate() + "kbps";
|
||||
}
|
||||
|
||||
ArrayAdapter<String> itemAdapter = new ArrayAdapter<>(getContext(), android.R.layout.simple_spinner_dropdown_item, items);
|
||||
@@ -240,10 +239,10 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
protected void checkDownloadOptions(View view) {
|
||||
RadioButton audioButton = (RadioButton) view.findViewById(R.id.audio_button);
|
||||
RadioButton videoButton = (RadioButton) view.findViewById(R.id.video_button);
|
||||
RadioButton audioButton = view.findViewById(R.id.audio_button);
|
||||
RadioButton videoButton = view.findViewById(R.id.video_button);
|
||||
|
||||
if (currentInfo.audio_streams == null || currentInfo.audio_streams.size() == 0) {
|
||||
if (currentInfo.getAudioStreams() == null || currentInfo.getAudioStreams().size() == 0) {
|
||||
audioButton.setVisibility(View.GONE);
|
||||
videoButton.setChecked(true);
|
||||
} else if (sortedStreamVideosList == null || sortedStreamVideosList.size() == 0) {
|
||||
@@ -252,37 +251,23 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* #143 #44 #42 #22: make sure that the filename does not contain illegal chars.
|
||||
* This should fix some of the "cannot download" problems.
|
||||
*/
|
||||
private String createFileName(String fileName) {
|
||||
// from http://eng-przemelek.blogspot.de/2009/07/how-to-create-valid-file-name.html
|
||||
|
||||
List<String> forbiddenCharsPatterns = new ArrayList<>();
|
||||
forbiddenCharsPatterns.add("[:]+"); // Mac OS, but it looks that also Windows XP
|
||||
forbiddenCharsPatterns.add("[\\*\"/\\\\\\[\\]\\:\\;\\|\\=\\,]+"); // Windows
|
||||
forbiddenCharsPatterns.add("[^\\w\\d\\.]+"); // last chance... only latin letters and digits
|
||||
String nameToTest = fileName;
|
||||
for (String pattern : forbiddenCharsPatterns) {
|
||||
nameToTest = nameToTest.replaceAll(pattern, "_");
|
||||
}
|
||||
return nameToTest;
|
||||
}
|
||||
|
||||
|
||||
private void downloadSelected() {
|
||||
String url, location;
|
||||
|
||||
String fileName = nameEditText.getText().toString().trim();
|
||||
if (fileName.isEmpty()) fileName = createFileName(currentInfo.title);
|
||||
if (fileName.isEmpty()) fileName = FilenameUtils.createFilename(getContext(), currentInfo.getName());
|
||||
|
||||
boolean isAudio = radioVideoAudioGroup.getCheckedRadioButtonId() == R.id.audio_button;
|
||||
url = isAudio ? currentInfo.audio_streams.get(selectedAudioIndex).url : sortedStreamVideosList.get(selectedVideoIndex).url;
|
||||
location = isAudio ? NewPipeSettings.getAudioDownloadPath(getContext()) : NewPipeSettings.getVideoDownloadPath(getContext());
|
||||
|
||||
if (isAudio) fileName += "." + MediaFormat.getSuffixById(currentInfo.audio_streams.get(selectedAudioIndex).format);
|
||||
else fileName += "." + MediaFormat.getSuffixById(sortedStreamVideosList.get(selectedVideoIndex).format);
|
||||
if (isAudio) {
|
||||
url = currentInfo.getAudioStreams().get(selectedAudioIndex).getUrl();
|
||||
location = NewPipeSettings.getAudioDownloadPath(getContext());
|
||||
fileName += "." + currentInfo.getAudioStreams().get(selectedAudioIndex).getFormat().getSuffix();
|
||||
} else {
|
||||
url = sortedStreamVideosList.get(selectedVideoIndex).getUrl();
|
||||
location = NewPipeSettings.getVideoDownloadPath(getContext());
|
||||
fileName += "." + sortedStreamVideosList.get(selectedVideoIndex).getFormat().getSuffix();
|
||||
}
|
||||
|
||||
DownloadManagerService.startMission(getContext(), url, location, fileName, isAudio, threadsSeekBar.getProgress() + 1);
|
||||
getDialog().dismiss();
|
||||
|
||||
Submodule app/src/main/java/org/schabi/newpipe/extractor deleted from ab530381cf
@@ -0,0 +1,13 @@
|
||||
package org.schabi.newpipe.fragments;
|
||||
|
||||
/**
|
||||
* Indicates that the current fragment can handle back presses
|
||||
*/
|
||||
public interface BackPressable {
|
||||
/**
|
||||
* A back press was delegated to this fragment
|
||||
*
|
||||
* @return if the back press was handled
|
||||
*/
|
||||
boolean onBackPressed();
|
||||
}
|
||||
@@ -1,177 +0,0 @@
|
||||
package org.schabi.newpipe.fragments;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.TypedArray;
|
||||
import android.graphics.Rect;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.AttrRes;
|
||||
import android.support.v4.app.Fragment;
|
||||
import android.support.v4.view.ViewCompat;
|
||||
import android.support.v7.app.AppCompatActivity;
|
||||
import android.support.v7.widget.Toolbar;
|
||||
import android.util.Log;
|
||||
import android.view.Gravity;
|
||||
import android.view.View;
|
||||
import android.widget.Button;
|
||||
import android.widget.ProgressBar;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.nostra13.universalimageloader.core.DisplayImageOptions;
|
||||
import com.nostra13.universalimageloader.core.ImageLoader;
|
||||
import com.nostra13.universalimageloader.core.display.FadeInBitmapDisplayer;
|
||||
|
||||
import org.schabi.newpipe.MainActivity;
|
||||
import org.schabi.newpipe.R;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
import static org.schabi.newpipe.util.AnimationUtils.animateView;
|
||||
|
||||
public abstract class BaseFragment extends Fragment {
|
||||
protected final String TAG = "BaseFragment@" + Integer.toHexString(hashCode());
|
||||
protected static final boolean DEBUG = MainActivity.DEBUG;
|
||||
|
||||
protected AppCompatActivity activity;
|
||||
|
||||
protected AtomicBoolean isLoading = new AtomicBoolean(false);
|
||||
protected AtomicBoolean wasLoading = new AtomicBoolean(false);
|
||||
|
||||
protected static final ImageLoader imageLoader = ImageLoader.getInstance();
|
||||
protected static final DisplayImageOptions displayImageOptions =
|
||||
new DisplayImageOptions.Builder().displayer(new FadeInBitmapDisplayer(400)).cacheInMemory(false).build();
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Views
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
protected Toolbar toolbar;
|
||||
|
||||
protected View errorPanel;
|
||||
protected Button errorButtonRetry;
|
||||
protected TextView errorTextView;
|
||||
protected ProgressBar loadingProgressBar;
|
||||
//protected SwipeRefreshLayout swipeRefreshLayout;
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Fragment's Lifecycle
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public void onAttach(Context context) {
|
||||
super.onAttach(context);
|
||||
if (DEBUG) Log.d(TAG, "onAttach() called with: context = [" + context + "]");
|
||||
|
||||
activity = (AppCompatActivity) context;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
if (DEBUG) Log.d(TAG, "onCreate() called with: savedInstanceState = [" + savedInstanceState + "]");
|
||||
|
||||
isLoading.set(false);
|
||||
setHasOptionsMenu(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(View rootView, Bundle savedInstanceState) {
|
||||
if (DEBUG) Log.d(TAG, "onViewCreated() called with: rootView = [" + rootView + "], savedInstanceState = [" + savedInstanceState + "]");
|
||||
initViews(rootView, savedInstanceState);
|
||||
initListeners();
|
||||
wasLoading.set(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyView() {
|
||||
super.onDestroyView();
|
||||
if (DEBUG) Log.d(TAG, "onDestroyView() called");
|
||||
toolbar = null;
|
||||
|
||||
errorPanel = null;
|
||||
errorButtonRetry = null;
|
||||
errorTextView = null;
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Init
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
protected void initViews(View rootView, Bundle savedInstanceState) {
|
||||
toolbar = (Toolbar) activity.findViewById(R.id.toolbar);
|
||||
|
||||
loadingProgressBar = (ProgressBar) rootView.findViewById(R.id.loading_progress_bar);
|
||||
//swipeRefreshLayout = (SwipeRefreshLayout) rootView.findViewById(R.id.swipe_refresh);
|
||||
|
||||
errorPanel = rootView.findViewById(R.id.error_panel);
|
||||
errorButtonRetry = (Button) rootView.findViewById(R.id.error_button_retry);
|
||||
errorTextView = (TextView) rootView.findViewById(R.id.error_message_view);
|
||||
}
|
||||
|
||||
protected void initListeners() {
|
||||
errorButtonRetry.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
onRetryButtonClicked();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected abstract void reloadContent();
|
||||
|
||||
protected void onRetryButtonClicked() {
|
||||
if (DEBUG) Log.d(TAG, "onRetryButtonClicked() called");
|
||||
reloadContent();
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Utils
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
protected void setErrorMessage(String message, boolean showRetryButton) {
|
||||
if (errorTextView == null || activity == null) return;
|
||||
|
||||
errorTextView.setText(message);
|
||||
if (showRetryButton) animateView(errorButtonRetry, true, 300);
|
||||
else animateView(errorButtonRetry, false, 0);
|
||||
|
||||
animateView(errorPanel, true, 300);
|
||||
isLoading.set(false);
|
||||
|
||||
animateView(loadingProgressBar, false, 200);
|
||||
}
|
||||
|
||||
protected int getResourceIdFromAttr(@AttrRes int attr) {
|
||||
TypedArray a = activity.getTheme().obtainStyledAttributes(new int[]{attr});
|
||||
int attributeResourceId = a.getResourceId(0, 0);
|
||||
a.recycle();
|
||||
return attributeResourceId;
|
||||
}
|
||||
|
||||
public static void showMenuTooltip(View v, String message) {
|
||||
final int[] screenPos = new int[2];
|
||||
final Rect displayFrame = new Rect();
|
||||
v.getLocationOnScreen(screenPos);
|
||||
v.getWindowVisibleDisplayFrame(displayFrame);
|
||||
|
||||
final Context context = v.getContext();
|
||||
final int width = v.getWidth();
|
||||
final int height = v.getHeight();
|
||||
final int midy = screenPos[1] + height / 2;
|
||||
int referenceX = screenPos[0] + width / 2;
|
||||
if (ViewCompat.getLayoutDirection(v) == View.LAYOUT_DIRECTION_LTR) {
|
||||
final int screenWidth = context.getResources().getDisplayMetrics().widthPixels;
|
||||
referenceX = screenWidth - referenceX; // mirror
|
||||
}
|
||||
Toast cheatSheet = Toast.makeText(context, message, Toast.LENGTH_SHORT);
|
||||
if (midy < displayFrame.height()) {
|
||||
// Show along the top; follow action buttons
|
||||
cheatSheet.setGravity(Gravity.TOP | Gravity.END, referenceX,
|
||||
screenPos[1] + height - displayFrame.top);
|
||||
} else {
|
||||
// Show along the bottom center
|
||||
cheatSheet.setGravity(Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL, 0, height);
|
||||
}
|
||||
cheatSheet.show();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,268 @@
|
||||
package org.schabi.newpipe.fragments;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.annotation.StringRes;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
import android.widget.Button;
|
||||
import android.widget.ProgressBar;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.jakewharton.rxbinding2.view.RxView;
|
||||
|
||||
import org.schabi.newpipe.BaseFragment;
|
||||
import org.schabi.newpipe.MainActivity;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.ReCaptchaActivity;
|
||||
import org.schabi.newpipe.extractor.exceptions.ReCaptchaException;
|
||||
import org.schabi.newpipe.report.ErrorActivity;
|
||||
import org.schabi.newpipe.report.UserAction;
|
||||
import org.schabi.newpipe.util.ExtractorHelper;
|
||||
import org.schabi.newpipe.util.InfoCache;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
import icepick.State;
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.functions.Consumer;
|
||||
|
||||
import static org.schabi.newpipe.util.AnimationUtils.animateView;
|
||||
|
||||
public abstract class BaseStateFragment<I> extends BaseFragment implements ViewContract<I> {
|
||||
|
||||
@State
|
||||
protected AtomicBoolean wasLoading = new AtomicBoolean();
|
||||
protected AtomicBoolean isLoading = new AtomicBoolean();
|
||||
|
||||
@Nullable
|
||||
protected View emptyStateView;
|
||||
@Nullable
|
||||
protected ProgressBar loadingProgressBar;
|
||||
|
||||
protected View errorPanelRoot;
|
||||
protected Button errorButtonRetry;
|
||||
protected TextView errorTextView;
|
||||
|
||||
@State
|
||||
protected boolean useAsFrontPage = false;
|
||||
|
||||
@Override
|
||||
public void onViewCreated(View rootView, Bundle savedInstanceState) {
|
||||
super.onViewCreated(rootView, savedInstanceState);
|
||||
doInitialLoadLogic();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPause() {
|
||||
super.onPause();
|
||||
wasLoading.set(isLoading.get());
|
||||
}
|
||||
|
||||
public void useAsFrontPage(boolean value) {
|
||||
useAsFrontPage = value;
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Init
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
|
||||
@Override
|
||||
protected void initViews(View rootView, Bundle savedInstanceState) {
|
||||
super.initViews(rootView, savedInstanceState);
|
||||
|
||||
emptyStateView = rootView.findViewById(R.id.empty_state_view);
|
||||
loadingProgressBar = rootView.findViewById(R.id.loading_progress_bar);
|
||||
|
||||
errorPanelRoot = rootView.findViewById(R.id.error_panel);
|
||||
errorButtonRetry = rootView.findViewById(R.id.error_button_retry);
|
||||
errorTextView = rootView.findViewById(R.id.error_message_view);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void initListeners() {
|
||||
super.initListeners();
|
||||
RxView.clicks(errorButtonRetry)
|
||||
.debounce(300, TimeUnit.MILLISECONDS)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(new Consumer<Object>() {
|
||||
@Override
|
||||
public void accept(Object o) throws Exception {
|
||||
onRetryButtonClicked();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected void onRetryButtonClicked() {
|
||||
reloadContent();
|
||||
}
|
||||
|
||||
public void reloadContent() {
|
||||
startLoading(true);
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Load
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
protected void doInitialLoadLogic() {
|
||||
startLoading(true);
|
||||
}
|
||||
|
||||
protected void startLoading(boolean forceLoad) {
|
||||
if (DEBUG) Log.d(TAG, "startLoading() called with: forceLoad = [" + forceLoad + "]");
|
||||
showLoading();
|
||||
isLoading.set(true);
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Contract
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public void showLoading() {
|
||||
if (emptyStateView != null) animateView(emptyStateView, false, 150);
|
||||
if (loadingProgressBar != null) animateView(loadingProgressBar, true, 400);
|
||||
animateView(errorPanelRoot, false, 150);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void hideLoading() {
|
||||
if (emptyStateView != null) animateView(emptyStateView, false, 150);
|
||||
if (loadingProgressBar != null) animateView(loadingProgressBar, false, 0);
|
||||
animateView(errorPanelRoot, false, 150);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void showEmptyState() {
|
||||
isLoading.set(false);
|
||||
if (emptyStateView != null) animateView(emptyStateView, true, 200);
|
||||
if (loadingProgressBar != null) animateView(loadingProgressBar, false, 0);
|
||||
animateView(errorPanelRoot, false, 150);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void showError(String message, boolean showRetryButton) {
|
||||
if (DEBUG) Log.d(TAG, "showError() called with: message = [" + message + "], showRetryButton = [" + showRetryButton + "]");
|
||||
isLoading.set(false);
|
||||
InfoCache.getInstance().clearCache();
|
||||
hideLoading();
|
||||
|
||||
errorTextView.setText(message);
|
||||
if (showRetryButton) animateView(errorButtonRetry, true, 600);
|
||||
else animateView(errorButtonRetry, false, 0);
|
||||
animateView(errorPanelRoot, true, 300);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleResult(I result) {
|
||||
if (DEBUG) Log.d(TAG, "handleResult() called with: result = [" + result + "]");
|
||||
hideLoading();
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Error handling
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
/**
|
||||
* Default implementation handles some general exceptions
|
||||
*
|
||||
* @return if the exception was handled
|
||||
*/
|
||||
protected boolean onError(Throwable exception) {
|
||||
if (DEBUG) Log.d(TAG, "onError() called with: exception = [" + exception + "]");
|
||||
isLoading.set(false);
|
||||
|
||||
if (isDetached() || isRemoving()) {
|
||||
if (DEBUG) Log.w(TAG, "onError() is detached or removing = [" + exception + "]");
|
||||
return true;
|
||||
}
|
||||
|
||||
if (ExtractorHelper.isInterruptedCaused(exception)) {
|
||||
if (DEBUG) Log.w(TAG, "onError() isInterruptedCaused! = [" + exception + "]");
|
||||
return true;
|
||||
}
|
||||
|
||||
if (exception instanceof ReCaptchaException) {
|
||||
onReCaptchaException();
|
||||
return true;
|
||||
} else if (exception instanceof IOException) {
|
||||
showError(getString(R.string.network_error), true);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public void onReCaptchaException() {
|
||||
if (DEBUG) Log.d(TAG, "onReCaptchaException() called");
|
||||
Toast.makeText(activity, R.string.recaptcha_request_toast, Toast.LENGTH_LONG).show();
|
||||
// Starting ReCaptcha Challenge Activity
|
||||
startActivityForResult(new Intent(activity, ReCaptchaActivity.class), ReCaptchaActivity.RECAPTCHA_REQUEST);
|
||||
|
||||
showError(getString(R.string.recaptcha_request_toast), false);
|
||||
}
|
||||
|
||||
public void onUnrecoverableError(Throwable exception, UserAction userAction, String serviceName, String request, @StringRes int errorId) {
|
||||
onUnrecoverableError(Collections.singletonList(exception), userAction, serviceName, request, errorId);
|
||||
}
|
||||
|
||||
public void onUnrecoverableError(List<Throwable> exception, UserAction userAction, String serviceName, String request, @StringRes int errorId) {
|
||||
if (DEBUG) Log.d(TAG, "onUnrecoverableError() called with: exception = [" + exception + "]");
|
||||
|
||||
if (serviceName == null) serviceName = "none";
|
||||
if (request == null) request = "none";
|
||||
|
||||
ErrorActivity.reportError(getContext(), exception, MainActivity.class, null, ErrorActivity.ErrorInfo.make(userAction, serviceName, request, errorId));
|
||||
}
|
||||
|
||||
public void showSnackBarError(Throwable exception, UserAction userAction, String serviceName, String request, @StringRes int errorId) {
|
||||
showSnackBarError(Collections.singletonList(exception), userAction, serviceName, request, errorId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show a SnackBar and only call ErrorActivity#reportError IF we a find a valid view (otherwise the error screen appears)
|
||||
*/
|
||||
public void showSnackBarError(List<Throwable> exception, UserAction userAction, String serviceName, String request, @StringRes int errorId) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "showSnackBarError() called with: exception = [" + exception + "], userAction = [" + userAction + "], request = [" + request + "], errorId = [" + errorId + "]");
|
||||
}
|
||||
View rootView = activity != null ? activity.findViewById(android.R.id.content) : null;
|
||||
if (rootView == null && getView() != null) rootView = getView();
|
||||
if (rootView == null) return;
|
||||
|
||||
ErrorActivity.reportError(getContext(), exception, MainActivity.class, rootView, ErrorActivity.ErrorInfo.make(userAction, serviceName, request, errorId));
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Utils
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
public void setTitle(String title) {
|
||||
if (DEBUG) Log.d(TAG, "setTitle() called with: title = [" + title + "]");
|
||||
if (activity != null && activity.getSupportActionBar() != null) {
|
||||
activity.getSupportActionBar().setTitle(title);
|
||||
}
|
||||
}
|
||||
|
||||
protected void openUrlInBrowser(String url) {
|
||||
Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
|
||||
startActivity(Intent.createChooser(intent, activity.getString(R.string.share_dialog_title)));
|
||||
}
|
||||
|
||||
protected void shareUrl(String subject, String url) {
|
||||
Intent intent = new Intent(Intent.ACTION_SEND);
|
||||
intent.setType("text/plain");
|
||||
intent.putExtra(Intent.EXTRA_SUBJECT, subject);
|
||||
intent.putExtra(Intent.EXTRA_TEXT, url);
|
||||
startActivity(Intent.createChooser(intent, getString(R.string.share_dialog_title)));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package org.schabi.newpipe.fragments;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import org.schabi.newpipe.BaseFragment;
|
||||
import org.schabi.newpipe.R;
|
||||
|
||||
public class BlankFragment extends BaseFragment {
|
||||
@Nullable
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) {
|
||||
if(activity != null && activity.getSupportActionBar() != null) {
|
||||
activity.getSupportActionBar()
|
||||
.setTitle("NewPipe");
|
||||
}
|
||||
return inflater.inflate(R.layout.fragment_blank, container, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setUserVisibleHint(boolean isVisibleToUser) {
|
||||
super.setUserVisibleHint(isVisibleToUser);
|
||||
if(isVisibleToUser) {
|
||||
if(activity != null && activity.getSupportActionBar() != null) {
|
||||
activity.getSupportActionBar()
|
||||
.setTitle("NewPipe");
|
||||
}
|
||||
// leave this inline. Will make it harder for copy cats.
|
||||
// If you are a Copy cat FUCK YOU.
|
||||
// I WILL FIND YOU, AND I WILL ...
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,53 +1,102 @@
|
||||
package org.schabi.newpipe.fragments;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.design.widget.TabLayout;
|
||||
import android.support.v4.app.Fragment;
|
||||
import android.support.v4.app.FragmentManager;
|
||||
import android.support.v4.app.FragmentPagerAdapter;
|
||||
import android.support.v4.view.ViewPager;
|
||||
import android.support.v7.app.ActionBar;
|
||||
import android.support.v7.app.AppCompatActivity;
|
||||
import android.support.v7.preference.PreferenceManager;
|
||||
import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.SubMenu;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import org.schabi.newpipe.MainActivity;
|
||||
import org.schabi.newpipe.BaseFragment;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.extractor.NewPipe;
|
||||
import org.schabi.newpipe.extractor.ServiceList;
|
||||
import org.schabi.newpipe.extractor.StreamingService;
|
||||
import org.schabi.newpipe.extractor.kiosk.KioskList;
|
||||
import org.schabi.newpipe.fragments.list.channel.ChannelFragment;
|
||||
import org.schabi.newpipe.fragments.list.feed.FeedFragment;
|
||||
import org.schabi.newpipe.fragments.list.kiosk.KioskFragment;
|
||||
import org.schabi.newpipe.fragments.local.bookmark.BookmarkFragment;
|
||||
import org.schabi.newpipe.fragments.subscription.SubscriptionFragment;
|
||||
import org.schabi.newpipe.report.ErrorActivity;
|
||||
import org.schabi.newpipe.report.UserAction;
|
||||
import org.schabi.newpipe.util.KioskTranslator;
|
||||
import org.schabi.newpipe.util.NavigationHelper;
|
||||
import org.schabi.newpipe.util.ServiceHelper;
|
||||
import org.schabi.newpipe.util.ThemeHelper;
|
||||
|
||||
public class MainFragment extends Fragment {
|
||||
private final String TAG = "MainFragment@" + Integer.toHexString(hashCode());
|
||||
private static final boolean DEBUG = MainActivity.DEBUG;
|
||||
public class MainFragment extends BaseFragment implements TabLayout.OnTabSelectedListener {
|
||||
|
||||
private AppCompatActivity activity;
|
||||
public int currentServiceId = -1;
|
||||
private ViewPager viewPager;
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Constants
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
private static final int FALLBACK_SERVICE_ID = ServiceList.YouTube.getServiceId();
|
||||
private static final String FALLBACK_CHANNEL_URL = "https://www.youtube.com/channel/UC-9-kyTW8ZkZNDHQJ6FgpwQ";
|
||||
private static final String FALLBACK_CHANNEL_NAME = "Music";
|
||||
private static final String FALLBACK_KIOSK_ID = "Trending";
|
||||
private static final int KIOSK_MENU_OFFSET = 2000;
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Fragment's LifeCycle
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public void onAttach(Context context) {
|
||||
super.onAttach(context);
|
||||
if (DEBUG) Log.d(TAG, "onAttach() called with: context = [" + context + "]");
|
||||
activity = ((AppCompatActivity) context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
if (DEBUG) Log.d(TAG, "onCreate() called with: savedInstanceState = [" + savedInstanceState + "]");
|
||||
setHasOptionsMenu(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||||
if (DEBUG) Log.d(TAG, "onCreateView() called with: inflater = [" + inflater + "], container = [" + container + "], savedInstanceState = [" + savedInstanceState + "]");
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||||
currentServiceId = ServiceHelper.getSelectedServiceId(activity);
|
||||
return inflater.inflate(R.layout.fragment_main, container, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void initViews(View rootView, Bundle savedInstanceState) {
|
||||
super.initViews(rootView, savedInstanceState);
|
||||
|
||||
TabLayout tabLayout = rootView.findViewById(R.id.main_tab_layout);
|
||||
viewPager = rootView.findViewById(R.id.pager);
|
||||
|
||||
/* Nested fragment, use child fragment here to maintain backstack in view pager. */
|
||||
PagerAdapter adapter = new PagerAdapter(getChildFragmentManager());
|
||||
viewPager.setAdapter(adapter);
|
||||
viewPager.setOffscreenPageLimit(adapter.getCount());
|
||||
|
||||
tabLayout.setupWithViewPager(viewPager);
|
||||
|
||||
int channelIcon = ThemeHelper.resolveResourceIdFromAttr(activity, R.attr.ic_channel);
|
||||
int whatsHotIcon = ThemeHelper.resolveResourceIdFromAttr(activity, R.attr.ic_hot);
|
||||
int bookmarkIcon = ThemeHelper.resolveResourceIdFromAttr(activity, R.attr.ic_bookmark);
|
||||
|
||||
if (isSubscriptionsPageOnlySelected()) {
|
||||
tabLayout.getTabAt(0).setIcon(channelIcon);
|
||||
tabLayout.getTabAt(1).setIcon(bookmarkIcon);
|
||||
} else {
|
||||
tabLayout.getTabAt(0).setIcon(whatsHotIcon);
|
||||
tabLayout.getTabAt(1).setIcon(channelIcon);
|
||||
tabLayout.getTabAt(2).setIcon(bookmarkIcon);
|
||||
}
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Menu
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
@@ -57,10 +106,19 @@ public class MainFragment extends Fragment {
|
||||
super.onCreateOptionsMenu(menu, inflater);
|
||||
if (DEBUG) Log.d(TAG, "onCreateOptionsMenu() called with: menu = [" + menu + "], inflater = [" + inflater + "]");
|
||||
inflater.inflate(R.menu.main_fragment_menu, menu);
|
||||
SubMenu kioskMenu = menu.addSubMenu(getString(R.string.kiosk));
|
||||
try {
|
||||
createKioskMenu(kioskMenu, inflater);
|
||||
} catch (Exception e) {
|
||||
ErrorActivity.reportError(activity, e,
|
||||
activity.getClass(),
|
||||
null,
|
||||
ErrorActivity.ErrorInfo.make(UserAction.UI_ERROR,
|
||||
"none", "", R.string.app_ui_crash));
|
||||
}
|
||||
|
||||
ActionBar supportActionBar = activity.getSupportActionBar();
|
||||
if (supportActionBar != null) {
|
||||
supportActionBar.setDisplayShowTitleEnabled(false);
|
||||
supportActionBar.setDisplayHomeAsUpEnabled(false);
|
||||
}
|
||||
}
|
||||
@@ -69,9 +127,150 @@ public class MainFragment extends Fragment {
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case R.id.action_search:
|
||||
NavigationHelper.openSearchFragment(getFragmentManager(), 0, "");
|
||||
NavigationHelper.openSearchFragment(getFragmentManager(), ServiceHelper.getSelectedServiceId(activity), "");
|
||||
return true;
|
||||
}
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Tabs
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public void onTabSelected(TabLayout.Tab tab) {
|
||||
viewPager.setCurrentItem(tab.getPosition());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTabUnselected(TabLayout.Tab tab) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTabReselected(TabLayout.Tab tab) {
|
||||
}
|
||||
|
||||
private class PagerAdapter extends FragmentPagerAdapter {
|
||||
PagerAdapter(FragmentManager fm) {
|
||||
super(fm);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Fragment getItem(int position) {
|
||||
switch (position) {
|
||||
case 0:
|
||||
return isSubscriptionsPageOnlySelected() ? new SubscriptionFragment() : getMainPageFragment();
|
||||
case 1:
|
||||
if(PreferenceManager.getDefaultSharedPreferences(getActivity())
|
||||
.getString(getString(R.string.main_page_content_key), getString(R.string.blank_page_key))
|
||||
.equals(getString(R.string.subscription_page_key))) {
|
||||
return new BookmarkFragment();
|
||||
} else {
|
||||
return new SubscriptionFragment();
|
||||
}
|
||||
case 2:
|
||||
return new BookmarkFragment();
|
||||
default:
|
||||
return new BlankFragment();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public CharSequence getPageTitle(int position) {
|
||||
//return getString(this.tabTitles[position]);
|
||||
return "";
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCount() {
|
||||
return isSubscriptionsPageOnlySelected() ? 2 : 3;
|
||||
}
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Main page content
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
private boolean isSubscriptionsPageOnlySelected() {
|
||||
return PreferenceManager.getDefaultSharedPreferences(activity)
|
||||
.getString(getString(R.string.main_page_content_key), getString(R.string.blank_page_key))
|
||||
.equals(getString(R.string.subscription_page_key));
|
||||
}
|
||||
|
||||
private Fragment getMainPageFragment() {
|
||||
if (getActivity() == null) return new BlankFragment();
|
||||
|
||||
try {
|
||||
SharedPreferences preferences =
|
||||
PreferenceManager.getDefaultSharedPreferences(getActivity());
|
||||
final String setMainPage = preferences.getString(getString(R.string.main_page_content_key),
|
||||
getString(R.string.main_page_selectd_kiosk_id));
|
||||
if (setMainPage.equals(getString(R.string.blank_page_key))) {
|
||||
return new BlankFragment();
|
||||
} else if (setMainPage.equals(getString(R.string.kiosk_page_key))) {
|
||||
int serviceId = preferences.getInt(getString(R.string.main_page_selected_service),
|
||||
FALLBACK_SERVICE_ID);
|
||||
String kioskId = preferences.getString(getString(R.string.main_page_selectd_kiosk_id),
|
||||
FALLBACK_KIOSK_ID);
|
||||
KioskFragment fragment = KioskFragment.getInstance(serviceId, kioskId);
|
||||
fragment.useAsFrontPage(true);
|
||||
return fragment;
|
||||
} else if (setMainPage.equals(getString(R.string.feed_page_key))) {
|
||||
FeedFragment fragment = new FeedFragment();
|
||||
fragment.useAsFrontPage(true);
|
||||
return fragment;
|
||||
} else if (setMainPage.equals(getString(R.string.channel_page_key))) {
|
||||
int serviceId = preferences.getInt(getString(R.string.main_page_selected_service),
|
||||
FALLBACK_SERVICE_ID);
|
||||
String url = preferences.getString(getString(R.string.main_page_selected_channel_url),
|
||||
FALLBACK_CHANNEL_URL);
|
||||
String name = preferences.getString(getString(R.string.main_page_selected_channel_name),
|
||||
FALLBACK_CHANNEL_NAME);
|
||||
ChannelFragment fragment = ChannelFragment.getInstance(serviceId, url, name);
|
||||
fragment.useAsFrontPage(true);
|
||||
return fragment;
|
||||
} else {
|
||||
return new BlankFragment();
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
ErrorActivity.reportError(activity, e,
|
||||
activity.getClass(),
|
||||
null,
|
||||
ErrorActivity.ErrorInfo.make(UserAction.UI_ERROR,
|
||||
"none", "", R.string.app_ui_crash));
|
||||
return new BlankFragment();
|
||||
}
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Select Kiosk
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
private void createKioskMenu(Menu menu, MenuInflater menuInflater)
|
||||
throws Exception {
|
||||
StreamingService service = NewPipe.getService(currentServiceId);
|
||||
KioskList kl = service.getKioskList();
|
||||
int i = 0;
|
||||
for (final String ks : kl.getAvailableKiosks()) {
|
||||
menu.add(0, KIOSK_MENU_OFFSET + i, Menu.NONE,
|
||||
KioskTranslator.getTranslatedKioskName(ks, getContext()))
|
||||
.setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() {
|
||||
@Override
|
||||
public boolean onMenuItemClick(MenuItem menuItem) {
|
||||
try {
|
||||
NavigationHelper.openKioskFragment(getFragmentManager(), currentServiceId, ks);
|
||||
} catch (Exception e) {
|
||||
ErrorActivity.reportError(activity, e,
|
||||
activity.getClass(),
|
||||
null,
|
||||
ErrorActivity.ErrorInfo.make(UserAction.UI_ERROR,
|
||||
"none", "", R.string.app_ui_crash));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
});
|
||||
i++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
package org.schabi.newpipe.fragments;
|
||||
|
||||
import android.support.v7.widget.LinearLayoutManager;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.support.v7.widget.StaggeredGridLayoutManager;
|
||||
|
||||
/**
|
||||
* Recycler view scroll listener which calls the method {@link #onScrolledDown(RecyclerView)}
|
||||
* if the view is scrolled below the last item.
|
||||
*/
|
||||
public abstract class OnScrollBelowItemsListener extends RecyclerView.OnScrollListener {
|
||||
|
||||
@Override
|
||||
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
|
||||
super.onScrolled(recyclerView, dx, dy);
|
||||
if (dy > 0) {
|
||||
int pastVisibleItems = 0, visibleItemCount, totalItemCount;
|
||||
RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager();
|
||||
|
||||
visibleItemCount = layoutManager.getChildCount();
|
||||
totalItemCount = layoutManager.getItemCount();
|
||||
|
||||
// Already covers the GridLayoutManager case
|
||||
if (layoutManager instanceof LinearLayoutManager) {
|
||||
pastVisibleItems = ((LinearLayoutManager) layoutManager).findFirstVisibleItemPosition();
|
||||
} else if (layoutManager instanceof StaggeredGridLayoutManager) {
|
||||
int[] positions = ((StaggeredGridLayoutManager) layoutManager).findFirstVisibleItemPositions(null);
|
||||
if (positions != null && positions.length > 0) pastVisibleItems = positions[0];
|
||||
}
|
||||
|
||||
if ((visibleItemCount + pastVisibleItems) >= totalItemCount) {
|
||||
onScrolledDown(recyclerView);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the recycler view is scrolled below the last item.
|
||||
*
|
||||
* @param recyclerView the recycler view
|
||||
*/
|
||||
public abstract void onScrolledDown(RecyclerView recyclerView);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package org.schabi.newpipe.fragments;
|
||||
|
||||
public interface ViewContract<I> {
|
||||
void showLoading();
|
||||
void hideLoading();
|
||||
void showEmptyState();
|
||||
void showError(String message, boolean showRetryButton);
|
||||
|
||||
void handleResult(I result);
|
||||
}
|
||||
@@ -1,403 +0,0 @@
|
||||
package org.schabi.newpipe.fragments.channel;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v4.app.Fragment;
|
||||
import android.support.v4.content.ContextCompat;
|
||||
import android.support.v7.app.ActionBar;
|
||||
import android.support.v7.widget.LinearLayoutManager;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.Button;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.schabi.newpipe.ImageErrorLoadingListener;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.extractor.InfoItem;
|
||||
import org.schabi.newpipe.extractor.channel.ChannelInfo;
|
||||
import org.schabi.newpipe.fragments.BaseFragment;
|
||||
import org.schabi.newpipe.fragments.search.OnScrollBelowItemsListener;
|
||||
import org.schabi.newpipe.info_list.InfoItemBuilder;
|
||||
import org.schabi.newpipe.info_list.InfoListAdapter;
|
||||
import org.schabi.newpipe.util.Constants;
|
||||
import org.schabi.newpipe.util.NavigationHelper;
|
||||
import org.schabi.newpipe.workers.ChannelExtractorWorker;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.text.NumberFormat;
|
||||
import java.util.ArrayList;
|
||||
|
||||
import static org.schabi.newpipe.util.AnimationUtils.animateView;
|
||||
|
||||
public class ChannelFragment extends BaseFragment implements ChannelExtractorWorker.OnChannelInfoReceive {
|
||||
private final String TAG = "ChannelFragment@" + Integer.toHexString(hashCode());
|
||||
|
||||
private static final String INFO_LIST_KEY = "info_list_key";
|
||||
private static final String CHANNEL_INFO_KEY = "channel_info_key";
|
||||
private static final String PAGE_NUMBER_KEY = "page_number_key";
|
||||
|
||||
private InfoListAdapter infoListAdapter;
|
||||
|
||||
private ChannelExtractorWorker currentChannelWorker;
|
||||
private ChannelInfo currentChannelInfo;
|
||||
private int serviceId = -1;
|
||||
private String channelName = "";
|
||||
private String channelUrl = "";
|
||||
private int pageNumber = 0;
|
||||
private boolean hasNextPage = true;
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Views
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
private RecyclerView channelVideosList;
|
||||
|
||||
private View headerRootLayout;
|
||||
private ImageView headerChannelBanner;
|
||||
private ImageView headerAvatarView;
|
||||
private TextView headerTitleView;
|
||||
private TextView headerSubscribersTextView;
|
||||
private Button headerRssButton;
|
||||
|
||||
/*////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
public ChannelFragment() {
|
||||
}
|
||||
|
||||
public static Fragment getInstance(int serviceId, String channelUrl, String name) {
|
||||
ChannelFragment instance = new ChannelFragment();
|
||||
instance.setChannel(serviceId, channelUrl, name);
|
||||
return instance;
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Fragment's LifeCycle
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
if (DEBUG) Log.d(TAG, "onCreate() called with: savedInstanceState = [" + savedInstanceState + "]");
|
||||
super.onCreate(savedInstanceState);
|
||||
setHasOptionsMenu(true);
|
||||
if (savedInstanceState != null) {
|
||||
channelUrl = savedInstanceState.getString(Constants.KEY_URL);
|
||||
channelName = savedInstanceState.getString(Constants.KEY_TITLE);
|
||||
serviceId = savedInstanceState.getInt(Constants.KEY_SERVICE_ID, -1);
|
||||
|
||||
pageNumber = savedInstanceState.getInt(PAGE_NUMBER_KEY, 0);
|
||||
Serializable serializable = savedInstanceState.getSerializable(CHANNEL_INFO_KEY);
|
||||
if (serializable instanceof ChannelInfo) currentChannelInfo = (ChannelInfo) serializable;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||||
if (DEBUG) Log.d(TAG, "onCreateView() called with: inflater = [" + inflater + "], container = [" + container + "], savedInstanceState = [" + savedInstanceState + "]");
|
||||
return inflater.inflate(R.layout.fragment_channel, container, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
|
||||
if (currentChannelInfo == null) loadPage(0);
|
||||
else handleChannelInfo(currentChannelInfo, false, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyView() {
|
||||
if (DEBUG) Log.d(TAG, "onDestroyView() called");
|
||||
headerAvatarView.setImageBitmap(null);
|
||||
headerChannelBanner.setImageBitmap(null);
|
||||
channelVideosList.removeAllViews();
|
||||
|
||||
channelVideosList = null;
|
||||
headerRootLayout = null;
|
||||
headerChannelBanner = null;
|
||||
headerAvatarView = null;
|
||||
headerTitleView = null;
|
||||
headerSubscribersTextView = null;
|
||||
headerRssButton = null;
|
||||
|
||||
super.onDestroyView();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
if (DEBUG) Log.d(TAG, "onResume() called");
|
||||
super.onResume();
|
||||
if (wasLoading.getAndSet(false) && (currentChannelWorker == null || !currentChannelWorker.isRunning())) {
|
||||
loadPage(pageNumber);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStop() {
|
||||
if (DEBUG) Log.d(TAG, "onStop() called");
|
||||
super.onStop();
|
||||
wasLoading.set(currentChannelWorker != null && currentChannelWorker.isRunning());
|
||||
if (currentChannelWorker != null && currentChannelWorker.isRunning()) currentChannelWorker.cancel();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSaveInstanceState(Bundle outState) {
|
||||
if (DEBUG) Log.d(TAG, "onSaveInstanceState() called with: outState = [" + outState + "]");
|
||||
super.onSaveInstanceState(outState);
|
||||
outState.putString(Constants.KEY_URL, channelUrl);
|
||||
outState.putString(Constants.KEY_TITLE, channelName);
|
||||
outState.putInt(Constants.KEY_SERVICE_ID, serviceId);
|
||||
|
||||
outState.putSerializable(INFO_LIST_KEY, ((ArrayList<InfoItem>) infoListAdapter.getItemsList()));
|
||||
outState.putSerializable(CHANNEL_INFO_KEY, currentChannelInfo);
|
||||
outState.putInt(PAGE_NUMBER_KEY, pageNumber);
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Menu
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
@Override
|
||||
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
|
||||
if (DEBUG) Log.d(TAG, "onCreateOptionsMenu() called with: menu = [" + menu + "], inflater = [" + inflater + "]");
|
||||
super.onCreateOptionsMenu(menu, inflater);
|
||||
inflater.inflate(R.menu.menu_channel, menu);
|
||||
|
||||
ActionBar supportActionBar = activity.getSupportActionBar();
|
||||
if (supportActionBar != null) {
|
||||
supportActionBar.setDisplayShowTitleEnabled(true);
|
||||
supportActionBar.setDisplayHomeAsUpEnabled(true);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
if (DEBUG) Log.d(TAG, "onOptionsItemSelected() called with: item = [" + item + "]");
|
||||
super.onOptionsItemSelected(item);
|
||||
switch (item.getItemId()) {
|
||||
case R.id.menu_item_openInBrowser: {
|
||||
Intent intent = new Intent();
|
||||
intent.setAction(Intent.ACTION_VIEW);
|
||||
intent.setData(Uri.parse(channelUrl));
|
||||
startActivity(Intent.createChooser(intent, getString(R.string.choose_browser)));
|
||||
return true;
|
||||
}
|
||||
case R.id.menu_item_share:
|
||||
Intent intent = new Intent();
|
||||
intent.setAction(Intent.ACTION_SEND);
|
||||
intent.putExtra(Intent.EXTRA_TEXT, channelUrl);
|
||||
intent.setType("text/plain");
|
||||
startActivity(Intent.createChooser(intent, getString(R.string.share_dialog_title)));
|
||||
return true;
|
||||
default:
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Init's
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
protected void initViews(View rootView, Bundle savedInstanceState) {
|
||||
super.initViews(rootView, savedInstanceState);
|
||||
|
||||
channelVideosList = (RecyclerView) rootView.findViewById(R.id.channel_streams_view);
|
||||
|
||||
channelVideosList.setLayoutManager(new LinearLayoutManager(activity));
|
||||
if (infoListAdapter == null) {
|
||||
infoListAdapter = new InfoListAdapter(activity);
|
||||
if (savedInstanceState != null) {
|
||||
//noinspection unchecked
|
||||
ArrayList<InfoItem> serializable = (ArrayList<InfoItem>) savedInstanceState.getSerializable(INFO_LIST_KEY);
|
||||
infoListAdapter.addInfoItemList(serializable);
|
||||
}
|
||||
}
|
||||
|
||||
channelVideosList.setAdapter(infoListAdapter);
|
||||
headerRootLayout = activity.getLayoutInflater().inflate(R.layout.channel_header, channelVideosList, false);
|
||||
infoListAdapter.setHeader(headerRootLayout);
|
||||
infoListAdapter.setFooter(activity.getLayoutInflater().inflate(R.layout.pignate_footer, channelVideosList, false));
|
||||
|
||||
headerChannelBanner = (ImageView) headerRootLayout.findViewById(R.id.channel_banner_image);
|
||||
headerAvatarView = (ImageView) headerRootLayout.findViewById(R.id.channel_avatar_view);
|
||||
headerTitleView = (TextView) headerRootLayout.findViewById(R.id.channel_title_view);
|
||||
headerSubscribersTextView = (TextView) headerRootLayout.findViewById(R.id.channel_subscriber_view);
|
||||
headerRssButton = (Button) headerRootLayout.findViewById(R.id.channel_rss_button);
|
||||
}
|
||||
|
||||
protected void initListeners() {
|
||||
super.initListeners();
|
||||
|
||||
infoListAdapter.setOnStreamInfoItemSelectedListener(new InfoItemBuilder.OnInfoItemSelectedListener() {
|
||||
@Override
|
||||
public void selected(int serviceId, String url, String title) {
|
||||
if (DEBUG) Log.d(TAG, "selected() called with: serviceId = [" + serviceId + "], url = [" + url + "], title = [" + title + "]");
|
||||
NavigationHelper.openVideoDetailFragment(getFragmentManager(), serviceId, url, title);
|
||||
}
|
||||
});
|
||||
|
||||
channelVideosList.clearOnScrollListeners();
|
||||
channelVideosList.addOnScrollListener(new OnScrollBelowItemsListener() {
|
||||
@Override
|
||||
public void onScrolledDown(RecyclerView recyclerView) {
|
||||
if ((currentChannelWorker == null || !currentChannelWorker.isRunning()) && hasNextPage && !isLoading.get()) {
|
||||
pageNumber++;
|
||||
loadMoreVideos();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
headerRssButton.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
if (DEBUG) Log.d(TAG, "onClick() called with: view = [" + view + "] feed url > " + currentChannelInfo.feed_url);
|
||||
Intent i = new Intent(Intent.ACTION_VIEW, Uri.parse(currentChannelInfo.feed_url));
|
||||
startActivity(i);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void reloadContent() {
|
||||
if (DEBUG) Log.d(TAG, "reloadContent() called");
|
||||
currentChannelInfo = null;
|
||||
infoListAdapter.clearStreamItemList();
|
||||
loadPage(0);
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Utils
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
private String buildSubscriberString(long count) {
|
||||
String out = NumberFormat.getNumberInstance().format(count);
|
||||
out += " " + getString(count > 1 ? R.string.subscriber_plural : R.string.subscriber);
|
||||
return out;
|
||||
}
|
||||
|
||||
private void loadPage(int page) {
|
||||
if (DEBUG) Log.d(TAG, "loadPage() called with: page = [" + page + "]");
|
||||
if (currentChannelWorker != null && currentChannelWorker.isRunning()) currentChannelWorker.cancel();
|
||||
isLoading.set(true);
|
||||
pageNumber = page;
|
||||
infoListAdapter.showFooter(false);
|
||||
|
||||
animateView(loadingProgressBar, true, 200);
|
||||
animateView(errorPanel, false, 200);
|
||||
|
||||
imageLoader.cancelDisplayTask(headerChannelBanner);
|
||||
imageLoader.cancelDisplayTask(headerAvatarView);
|
||||
|
||||
headerRssButton.setVisibility(View.GONE);
|
||||
headerSubscribersTextView.setVisibility(View.GONE);
|
||||
|
||||
headerTitleView.setText(channelName != null ? channelName : "");
|
||||
headerChannelBanner.setImageDrawable(ContextCompat.getDrawable(activity, R.drawable.channel_banner));
|
||||
headerAvatarView.setImageDrawable(ContextCompat.getDrawable(activity, R.drawable.buddy));
|
||||
if (activity.getSupportActionBar() != null) activity.getSupportActionBar().setTitle(channelName != null ? channelName : "");
|
||||
|
||||
currentChannelWorker = new ChannelExtractorWorker(activity, serviceId, channelUrl, page, false, this);
|
||||
currentChannelWorker.start();
|
||||
}
|
||||
|
||||
private void loadMoreVideos() {
|
||||
if (DEBUG) Log.d(TAG, "loadMoreVideos() called");
|
||||
if (currentChannelWorker != null && currentChannelWorker.isRunning()) currentChannelWorker.cancel();
|
||||
isLoading.set(true);
|
||||
currentChannelWorker = new ChannelExtractorWorker(activity, serviceId, channelUrl, pageNumber, true, this);
|
||||
currentChannelWorker.start();
|
||||
}
|
||||
|
||||
private void setChannel(int serviceId, String channelUrl, String name) {
|
||||
this.serviceId = serviceId;
|
||||
this.channelUrl = channelUrl;
|
||||
this.channelName = name;
|
||||
}
|
||||
|
||||
private void handleChannelInfo(ChannelInfo info, boolean onlyVideos, boolean addVideos) {
|
||||
currentChannelInfo = info;
|
||||
|
||||
animateView(errorPanel, false, 300);
|
||||
animateView(channelVideosList, true, 200);
|
||||
animateView(loadingProgressBar, false, 200);
|
||||
|
||||
if (!onlyVideos) {
|
||||
headerRootLayout.setVisibility(View.VISIBLE);
|
||||
//animateView(loadingProgressBar, false, 200, null);
|
||||
|
||||
if (!TextUtils.isEmpty(info.channel_name)) {
|
||||
if (activity.getSupportActionBar() != null) activity.getSupportActionBar().setTitle(info.channel_name);
|
||||
headerTitleView.setText(info.channel_name);
|
||||
channelName = info.channel_name;
|
||||
} else channelName = "";
|
||||
|
||||
if (!TextUtils.isEmpty(info.banner_url)) {
|
||||
imageLoader.displayImage(info.banner_url, headerChannelBanner, displayImageOptions, new ImageErrorLoadingListener(activity, getView(), info.service_id));
|
||||
}
|
||||
|
||||
if (!TextUtils.isEmpty(info.avatar_url)) {
|
||||
headerAvatarView.setVisibility(View.VISIBLE);
|
||||
imageLoader.displayImage(info.avatar_url, headerAvatarView, displayImageOptions, new ImageErrorLoadingListener(activity, getView(), info.service_id));
|
||||
}
|
||||
|
||||
if (info.subscriberCount != -1) {
|
||||
headerSubscribersTextView.setText(buildSubscriberString(info.subscriberCount));
|
||||
headerSubscribersTextView.setVisibility(View.VISIBLE);
|
||||
} else headerSubscribersTextView.setVisibility(View.GONE);
|
||||
|
||||
if (!TextUtils.isEmpty(info.feed_url)) headerRssButton.setVisibility(View.VISIBLE);
|
||||
else headerRssButton.setVisibility(View.INVISIBLE);
|
||||
|
||||
infoListAdapter.showFooter(true);
|
||||
}
|
||||
|
||||
hasNextPage = info.hasNextPage;
|
||||
if (!hasNextPage) infoListAdapter.showFooter(false);
|
||||
|
||||
//if (!listRestored) {
|
||||
if (addVideos) infoListAdapter.addInfoItemList(info.related_streams);
|
||||
//}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void setErrorMessage(String message, boolean showRetryButton) {
|
||||
super.setErrorMessage(message, showRetryButton);
|
||||
|
||||
animateView(channelVideosList, false, 200);
|
||||
currentChannelInfo = null;
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// OnChannelInfoReceiveListener
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public void onReceive(ChannelInfo info, boolean onlyVideos) {
|
||||
if (DEBUG) Log.d(TAG, "onReceive() called with: info = [" + info + "]");
|
||||
if (info == null || isRemoving() || !isVisible()) return;
|
||||
|
||||
handleChannelInfo(info, onlyVideos, true);
|
||||
isLoading.set(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(int messageId) {
|
||||
if (DEBUG) Log.d(TAG, "onError() called with: messageId = [" + messageId + "]");
|
||||
setErrorMessage(getString(messageId), true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUnrecoverableError(Exception exception) {
|
||||
if (DEBUG) Log.d(TAG, "onUnrecoverableError() called with: exception = [" + exception + "]");
|
||||
activity.finish();
|
||||
}
|
||||
}
|
||||
@@ -1,163 +0,0 @@
|
||||
package org.schabi.newpipe.fragments.detail;
|
||||
|
||||
import android.content.SharedPreferences;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.support.v7.app.AppCompatActivity;
|
||||
import android.util.Log;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.widget.AdapterView;
|
||||
import android.widget.Spinner;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.extractor.stream_info.VideoStream;
|
||||
import org.schabi.newpipe.util.Utils;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Created by Christian Schabesberger on 18.08.15.
|
||||
* <p>
|
||||
* Copyright (C) Christian Schabesberger 2015 <chris.schabesberger@mailbox.org>
|
||||
* DetailsMenuHandler.java is part of NewPipe.
|
||||
* <p>
|
||||
* NewPipe is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
* <p>
|
||||
* NewPipe is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
* <p>
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
|
||||
@SuppressWarnings("WeakerAccess")
|
||||
class ActionBarHandler {
|
||||
private static final String TAG = "ActionBarHandler";
|
||||
|
||||
private AppCompatActivity activity;
|
||||
private int selectedVideoStream = -1;
|
||||
|
||||
private SharedPreferences defaultPreferences;
|
||||
|
||||
private Menu menu;
|
||||
|
||||
// Only callbacks are listed here, there are more actions which don't need a callback.
|
||||
// those are edited directly. Typically VideoDetailFragment will implement those callbacks.
|
||||
private OnActionListener onShareListener;
|
||||
private OnActionListener onOpenInBrowserListener;
|
||||
private OnActionListener onDownloadListener;
|
||||
private OnActionListener onPlayWithKodiListener;
|
||||
|
||||
// Triggered when a stream related action is triggered.
|
||||
public interface OnActionListener {
|
||||
void onActionSelected(int selectedStreamId);
|
||||
}
|
||||
|
||||
public ActionBarHandler(AppCompatActivity activity) {
|
||||
this.activity = activity;
|
||||
}
|
||||
|
||||
public void setupStreamList(final List<VideoStream> videoStreams, Spinner toolbarSpinner) {
|
||||
if (activity == null) return;
|
||||
|
||||
selectedVideoStream = Utils.getDefaultResolution(activity, videoStreams);
|
||||
|
||||
boolean isExternalPlayerEnabled = PreferenceManager.getDefaultSharedPreferences(activity).getBoolean(activity.getString(R.string.use_external_video_player_key), false);
|
||||
toolbarSpinner.setAdapter(new SpinnerToolbarAdapter(activity, videoStreams, isExternalPlayerEnabled));
|
||||
toolbarSpinner.setSelection(selectedVideoStream);
|
||||
toolbarSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
|
||||
@Override
|
||||
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
|
||||
selectedVideoStream = position;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNothingSelected(AdapterView<?> parent) {
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
public void setupMenu(Menu menu, MenuInflater inflater) {
|
||||
this.menu = menu;
|
||||
|
||||
// CAUTION set item properties programmatically otherwise it would not be accepted by
|
||||
// appcompat itemsinflater.inflate(R.menu.videoitem_detail, menu);
|
||||
|
||||
defaultPreferences = PreferenceManager.getDefaultSharedPreferences(activity);
|
||||
inflater.inflate(R.menu.video_detail_menu, menu);
|
||||
|
||||
updateItemsVisibility();
|
||||
}
|
||||
|
||||
public void updateItemsVisibility(){
|
||||
showPlayWithKodiAction(defaultPreferences.getBoolean(activity.getString(R.string.show_play_with_kodi_key), false));
|
||||
}
|
||||
|
||||
public boolean onItemSelected(MenuItem item) {
|
||||
int id = item.getItemId();
|
||||
switch (id) {
|
||||
case R.id.menu_item_share: {
|
||||
if (onShareListener != null) {
|
||||
onShareListener.onActionSelected(selectedVideoStream);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
case R.id.menu_item_openInBrowser: {
|
||||
if (onOpenInBrowserListener != null) {
|
||||
onOpenInBrowserListener.onActionSelected(selectedVideoStream);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
case R.id.menu_item_download:
|
||||
if (onDownloadListener != null) {
|
||||
onDownloadListener.onActionSelected(selectedVideoStream);
|
||||
}
|
||||
return true;
|
||||
case R.id.action_play_with_kodi:
|
||||
if (onPlayWithKodiListener != null) {
|
||||
onPlayWithKodiListener.onActionSelected(selectedVideoStream);
|
||||
}
|
||||
return true;
|
||||
default:
|
||||
Log.e(TAG, "Menu Item not known");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public int getSelectedVideoStream() {
|
||||
return selectedVideoStream;
|
||||
}
|
||||
|
||||
public void setOnShareListener(OnActionListener listener) {
|
||||
onShareListener = listener;
|
||||
}
|
||||
|
||||
public void setOnOpenInBrowserListener(OnActionListener listener) {
|
||||
onOpenInBrowserListener = listener;
|
||||
}
|
||||
|
||||
public void setOnDownloadListener(OnActionListener listener) {
|
||||
onDownloadListener = listener;
|
||||
}
|
||||
|
||||
public void setOnPlayWithKodiListener(OnActionListener listener) {
|
||||
onPlayWithKodiListener = listener;
|
||||
}
|
||||
|
||||
public void showDownloadAction(boolean visible) {
|
||||
menu.findItem(R.id.menu_item_download).setVisible(visible);
|
||||
}
|
||||
|
||||
public void showPlayWithKodiAction(boolean visible) {
|
||||
menu.findItem(R.id.action_play_with_kodi).setVisible(visible);
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,7 @@ import android.widget.TextView;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.extractor.MediaFormat;
|
||||
import org.schabi.newpipe.extractor.stream_info.VideoStream;
|
||||
import org.schabi.newpipe.extractor.stream.VideoStream;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@@ -57,10 +57,10 @@ public class SpinnerToolbarAdapter extends BaseAdapter {
|
||||
convertView = LayoutInflater.from(context).inflate(R.layout.resolutions_spinner_item, parent, false);
|
||||
}
|
||||
|
||||
ImageView woSoundIcon = (ImageView) convertView.findViewById(R.id.wo_sound_icon);
|
||||
TextView text = (TextView) convertView.findViewById(android.R.id.text1);
|
||||
ImageView woSoundIcon = convertView.findViewById(R.id.wo_sound_icon);
|
||||
TextView text = convertView.findViewById(android.R.id.text1);
|
||||
VideoStream item = (VideoStream) getItem(position);
|
||||
text.setText(MediaFormat.getNameById(item.format) + " " + item.resolution);
|
||||
text.setText(item.getFormat().getName() + " " + item.getResolution());
|
||||
|
||||
int visibility = !showIconNoAudio ? View.GONE
|
||||
: item.isVideoOnly ? View.VISIBLE
|
||||
|
||||
@@ -1,24 +1,26 @@
|
||||
package org.schabi.newpipe.fragments.detail;
|
||||
|
||||
import org.schabi.newpipe.extractor.stream_info.StreamInfo;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
class StackItem implements Serializable {
|
||||
private int serviceId;
|
||||
private String title;
|
||||
private String url;
|
||||
|
||||
@SuppressWarnings("WeakerAccess")
|
||||
public class StackItem implements Serializable {
|
||||
private String title, url;
|
||||
private StreamInfo info;
|
||||
|
||||
public StackItem(String url, String title) {
|
||||
this.title = title;
|
||||
StackItem(int serviceId, String url, String title) {
|
||||
this.serviceId = serviceId;
|
||||
this.url = url;
|
||||
this.title = title;
|
||||
}
|
||||
|
||||
public void setTitle(String title) {
|
||||
this.title = title;
|
||||
}
|
||||
|
||||
public int getServiceId() {
|
||||
return serviceId;
|
||||
}
|
||||
|
||||
public String getTitle() {
|
||||
return title;
|
||||
}
|
||||
@@ -27,16 +29,8 @@ public class StackItem implements Serializable {
|
||||
return url;
|
||||
}
|
||||
|
||||
public void setInfo(StreamInfo info) {
|
||||
this.info = info;
|
||||
}
|
||||
|
||||
public StreamInfo getInfo() {
|
||||
return info;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return getUrl() + " > " + getTitle();
|
||||
return getServiceId() + ":" + getUrl() + " > " + getTitle();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,85 +0,0 @@
|
||||
package org.schabi.newpipe.fragments.detail;
|
||||
|
||||
import android.support.annotation.NonNull;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
|
||||
import org.schabi.newpipe.MainActivity;
|
||||
import org.schabi.newpipe.extractor.stream_info.StreamInfo;
|
||||
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedHashMap;
|
||||
|
||||
|
||||
@SuppressWarnings("WeakerAccess")
|
||||
public class StreamInfoCache {
|
||||
private static String TAG = "StreamInfoCache@";
|
||||
private static final boolean DEBUG = MainActivity.DEBUG;
|
||||
private static final StreamInfoCache instance = new StreamInfoCache();
|
||||
private static final int MAX_ITEMS_ON_CACHE = 20;
|
||||
|
||||
private final LinkedHashMap<String, StreamInfo> myCache = new LinkedHashMap<>();
|
||||
|
||||
private StreamInfoCache() {
|
||||
TAG += "" + Integer.toHexString(hashCode());
|
||||
}
|
||||
|
||||
public static StreamInfoCache getInstance() {
|
||||
if (DEBUG) Log.d(TAG, "getInstance() called");
|
||||
return instance;
|
||||
}
|
||||
|
||||
public boolean hasKey(@NonNull String url) {
|
||||
if (DEBUG) Log.d(TAG, "hasKey() called with: url = [" + url + "]");
|
||||
return !TextUtils.isEmpty(url) && myCache.containsKey(url) && myCache.get(url) != null;
|
||||
}
|
||||
|
||||
public StreamInfo getFromKey(@NonNull String url) {
|
||||
if (DEBUG) Log.d(TAG, "getFromKey() called with: url = [" + url + "]");
|
||||
return myCache.get(url);
|
||||
}
|
||||
|
||||
public void putInfo(@NonNull StreamInfo info) {
|
||||
if (DEBUG) Log.d(TAG, "putInfo() called with: info = [" + info + "]");
|
||||
putInfo(info.webpage_url, info);
|
||||
}
|
||||
|
||||
public void putInfo(@NonNull String url, @NonNull StreamInfo info) {
|
||||
if (DEBUG) Log.d(TAG, "putInfo() called with: url = [" + url + "], info = [" + info + "]");
|
||||
myCache.put(url, info);
|
||||
}
|
||||
|
||||
public void removeInfo(@NonNull StreamInfo info) {
|
||||
if (DEBUG) Log.d(TAG, "removeInfo() called with: info = [" + info + "]");
|
||||
myCache.remove(info.webpage_url);
|
||||
}
|
||||
|
||||
public void removeInfo(@NonNull String url) {
|
||||
if (DEBUG) Log.d(TAG, "removeInfo() called with: url = [" + url + "]");
|
||||
myCache.remove(url);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public void clearCache() {
|
||||
if (DEBUG) Log.d(TAG, "clearCache() called");
|
||||
myCache.clear();
|
||||
}
|
||||
|
||||
public void removeOldEntries() {
|
||||
if (DEBUG) Log.d(TAG, "removeOldEntries() called , size = " + getSize());
|
||||
if (getSize() > MAX_ITEMS_ON_CACHE) {
|
||||
Iterator<String> iterator = myCache.keySet().iterator();
|
||||
while (iterator.hasNext()) {
|
||||
iterator.next();
|
||||
iterator.remove();
|
||||
if (DEBUG) Log.d(TAG, "getSize() = " + getSize());
|
||||
if (getSize() <= MAX_ITEMS_ON_CACHE) break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public int getSize() {
|
||||
return myCache.size();
|
||||
}
|
||||
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,292 @@
|
||||
package org.schabi.newpipe.fragments.list;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.v7.app.ActionBar;
|
||||
import android.support.v7.widget.LinearLayoutManager;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.util.Log;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.View;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.extractor.InfoItem;
|
||||
import org.schabi.newpipe.extractor.channel.ChannelInfoItem;
|
||||
import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||
import org.schabi.newpipe.fragments.BaseStateFragment;
|
||||
import org.schabi.newpipe.fragments.OnScrollBelowItemsListener;
|
||||
import org.schabi.newpipe.fragments.local.dialog.PlaylistAppendDialog;
|
||||
import org.schabi.newpipe.info_list.InfoItemDialog;
|
||||
import org.schabi.newpipe.info_list.InfoListAdapter;
|
||||
import org.schabi.newpipe.playlist.SinglePlayQueue;
|
||||
import org.schabi.newpipe.util.NavigationHelper;
|
||||
import org.schabi.newpipe.util.OnClickGesture;
|
||||
import org.schabi.newpipe.util.StateSaver;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Queue;
|
||||
|
||||
import static org.schabi.newpipe.util.AnimationUtils.animateView;
|
||||
|
||||
public abstract class BaseListFragment<I, N> extends BaseStateFragment<I> implements ListViewContract<I, N>, StateSaver.WriteRead {
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Views
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
protected InfoListAdapter infoListAdapter;
|
||||
protected RecyclerView itemsList;
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// LifeCycle
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public void onAttach(Context context) {
|
||||
super.onAttach(context);
|
||||
infoListAdapter = new InfoListAdapter(activity);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setHasOptionsMenu(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
StateSaver.onDestroy(savedState);
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// State Saving
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
protected StateSaver.SavedState savedState;
|
||||
|
||||
@Override
|
||||
public String generateSuffix() {
|
||||
// Naive solution, but it's good for now (the items don't change)
|
||||
return "." + infoListAdapter.getItemsList().size() + ".list";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeTo(Queue<Object> objectsToSave) {
|
||||
objectsToSave.add(infoListAdapter.getItemsList());
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public void readFrom(@NonNull Queue<Object> savedObjects) throws Exception {
|
||||
infoListAdapter.getItemsList().clear();
|
||||
infoListAdapter.getItemsList().addAll((List<InfoItem>) savedObjects.poll());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSaveInstanceState(Bundle bundle) {
|
||||
super.onSaveInstanceState(bundle);
|
||||
savedState = StateSaver.tryToSave(activity.isChangingConfigurations(), savedState, bundle, this);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onRestoreInstanceState(@NonNull Bundle bundle) {
|
||||
super.onRestoreInstanceState(bundle);
|
||||
savedState = StateSaver.tryToRestore(bundle, this);
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Init
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
protected View getListHeader() {
|
||||
return null;
|
||||
}
|
||||
|
||||
protected View getListFooter() {
|
||||
return activity.getLayoutInflater().inflate(R.layout.pignate_footer, itemsList, false);
|
||||
}
|
||||
|
||||
protected RecyclerView.LayoutManager getListLayoutManager() {
|
||||
return new LinearLayoutManager(activity);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void initViews(View rootView, Bundle savedInstanceState) {
|
||||
super.initViews(rootView, savedInstanceState);
|
||||
|
||||
itemsList = rootView.findViewById(R.id.items_list);
|
||||
itemsList.setLayoutManager(getListLayoutManager());
|
||||
|
||||
infoListAdapter.setFooter(getListFooter());
|
||||
infoListAdapter.setHeader(getListHeader());
|
||||
|
||||
itemsList.setAdapter(infoListAdapter);
|
||||
}
|
||||
|
||||
protected void onItemSelected(InfoItem selectedItem) {
|
||||
if (DEBUG) Log.d(TAG, "onItemSelected() called with: selectedItem = [" + selectedItem + "]");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void initListeners() {
|
||||
super.initListeners();
|
||||
infoListAdapter.setOnStreamSelectedListener(new OnClickGesture<StreamInfoItem>() {
|
||||
@Override
|
||||
public void selected(StreamInfoItem selectedItem) {
|
||||
onItemSelected(selectedItem);
|
||||
NavigationHelper.openVideoDetailFragment(
|
||||
useAsFrontPage?getParentFragment().getFragmentManager():getFragmentManager(),
|
||||
selectedItem.getServiceId(), selectedItem.getUrl(), selectedItem.getName());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void held(StreamInfoItem selectedItem) {
|
||||
showStreamDialog(selectedItem);
|
||||
}
|
||||
});
|
||||
|
||||
infoListAdapter.setOnChannelSelectedListener(new OnClickGesture<ChannelInfoItem>() {
|
||||
@Override
|
||||
public void selected(ChannelInfoItem selectedItem) {
|
||||
onItemSelected(selectedItem);
|
||||
NavigationHelper.openChannelFragment(
|
||||
useAsFrontPage?getParentFragment().getFragmentManager():getFragmentManager(),
|
||||
selectedItem.getServiceId(), selectedItem.getUrl(), selectedItem.getName());
|
||||
}
|
||||
});
|
||||
|
||||
infoListAdapter.setOnPlaylistSelectedListener(new OnClickGesture<PlaylistInfoItem>() {
|
||||
@Override
|
||||
public void selected(PlaylistInfoItem selectedItem) {
|
||||
onItemSelected(selectedItem);
|
||||
NavigationHelper.openPlaylistFragment(
|
||||
useAsFrontPage?getParentFragment().getFragmentManager():getFragmentManager(),
|
||||
selectedItem.getServiceId(), selectedItem.getUrl(), selectedItem.getName());
|
||||
}
|
||||
});
|
||||
|
||||
itemsList.clearOnScrollListeners();
|
||||
itemsList.addOnScrollListener(new OnScrollBelowItemsListener() {
|
||||
@Override
|
||||
public void onScrolledDown(RecyclerView recyclerView) {
|
||||
onScrollToBottom();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected void onScrollToBottom() {
|
||||
if (hasMoreItems() && !isLoading.get()) {
|
||||
loadMoreItems();
|
||||
}
|
||||
}
|
||||
|
||||
protected void showStreamDialog(final StreamInfoItem item) {
|
||||
final Context context = getContext();
|
||||
final Activity activity = getActivity();
|
||||
if (context == null || context.getResources() == null || getActivity() == null) return;
|
||||
|
||||
final String[] commands = new String[]{
|
||||
context.getResources().getString(R.string.enqueue_on_background),
|
||||
context.getResources().getString(R.string.enqueue_on_popup),
|
||||
context.getResources().getString(R.string.append_playlist)
|
||||
};
|
||||
|
||||
final DialogInterface.OnClickListener actions = (dialogInterface, i) -> {
|
||||
switch (i) {
|
||||
case 0:
|
||||
NavigationHelper.enqueueOnBackgroundPlayer(context, new SinglePlayQueue(item));
|
||||
break;
|
||||
case 1:
|
||||
NavigationHelper.enqueueOnPopupPlayer(activity, new SinglePlayQueue(item));
|
||||
break;
|
||||
case 2:
|
||||
if (getFragmentManager() != null) {
|
||||
PlaylistAppendDialog.fromStreamInfoItems(Collections.singletonList(item))
|
||||
.show(getFragmentManager(), TAG);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
new InfoItemDialog(getActivity(), item, commands, actions).show();
|
||||
}
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Menu
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
|
||||
if (DEBUG) Log.d(TAG, "onCreateOptionsMenu() called with: menu = [" + menu + "], inflater = [" + inflater + "]");
|
||||
super.onCreateOptionsMenu(menu, inflater);
|
||||
ActionBar supportActionBar = activity.getSupportActionBar();
|
||||
if (supportActionBar != null) {
|
||||
supportActionBar.setDisplayShowTitleEnabled(true);
|
||||
if(useAsFrontPage) {
|
||||
supportActionBar.setDisplayHomeAsUpEnabled(false);
|
||||
} else {
|
||||
supportActionBar.setDisplayHomeAsUpEnabled(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Load and handle
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
protected abstract void loadMoreItems();
|
||||
|
||||
protected abstract boolean hasMoreItems();
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Contract
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public void showLoading() {
|
||||
super.showLoading();
|
||||
// animateView(itemsList, false, 400);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void hideLoading() {
|
||||
super.hideLoading();
|
||||
animateView(itemsList, true, 300);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void showError(String message, boolean showRetryButton) {
|
||||
super.showError(message, showRetryButton);
|
||||
showListFooter(false);
|
||||
animateView(itemsList, false, 200);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void showEmptyState() {
|
||||
super.showEmptyState();
|
||||
showListFooter(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void showListFooter(final boolean show) {
|
||||
itemsList.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
infoListAdapter.showFooter(show);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleNextItems(N result) {
|
||||
isLoading.set(false);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
package org.schabi.newpipe.fragments.list;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
|
||||
import org.schabi.newpipe.extractor.ListExtractor;
|
||||
import org.schabi.newpipe.extractor.ListInfo;
|
||||
import org.schabi.newpipe.util.Constants;
|
||||
|
||||
import java.util.Queue;
|
||||
|
||||
import icepick.State;
|
||||
import io.reactivex.Single;
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.disposables.Disposable;
|
||||
import io.reactivex.functions.Consumer;
|
||||
import io.reactivex.schedulers.Schedulers;
|
||||
|
||||
public abstract class BaseListInfoFragment<I extends ListInfo> extends BaseListFragment<I, ListExtractor.NextItemsResult> {
|
||||
|
||||
@State
|
||||
protected int serviceId = Constants.NO_SERVICE_ID;
|
||||
@State
|
||||
protected String name;
|
||||
@State
|
||||
protected String url;
|
||||
|
||||
protected I currentInfo;
|
||||
protected String currentNextItemsUrl;
|
||||
protected Disposable currentWorker;
|
||||
|
||||
@Override
|
||||
protected void initViews(View rootView, Bundle savedInstanceState) {
|
||||
super.initViews(rootView, savedInstanceState);
|
||||
setTitle(name);
|
||||
showListFooter(hasMoreItems());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPause() {
|
||||
super.onPause();
|
||||
if (currentWorker != null) currentWorker.dispose();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
// Check if it was loading when the fragment was stopped/paused,
|
||||
if (wasLoading.getAndSet(false)) {
|
||||
if (hasMoreItems() && infoListAdapter.getItemsList().size() > 0) {
|
||||
loadMoreItems();
|
||||
} else {
|
||||
doInitialLoadLogic();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
if (currentWorker != null) currentWorker.dispose();
|
||||
currentWorker = null;
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// State Saving
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public void writeTo(Queue<Object> objectsToSave) {
|
||||
super.writeTo(objectsToSave);
|
||||
objectsToSave.add(currentInfo);
|
||||
objectsToSave.add(currentNextItemsUrl);
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public void readFrom(@NonNull Queue<Object> savedObjects) throws Exception {
|
||||
super.readFrom(savedObjects);
|
||||
currentInfo = (I) savedObjects.poll();
|
||||
currentNextItemsUrl = (String) savedObjects.poll();
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Load and handle
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
protected void doInitialLoadLogic() {
|
||||
if (DEBUG) Log.d(TAG, "doInitialLoadLogic() called");
|
||||
if (currentInfo == null) {
|
||||
startLoading(false);
|
||||
} else handleResult(currentInfo);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implement the logic to load the info from the network.<br/>
|
||||
* You can use the default implementations from {@link org.schabi.newpipe.util.ExtractorHelper}.
|
||||
*
|
||||
* @param forceLoad allow or disallow the result to come from the cache
|
||||
*/
|
||||
protected abstract Single<I> loadResult(boolean forceLoad);
|
||||
|
||||
@Override
|
||||
public void startLoading(boolean forceLoad) {
|
||||
super.startLoading(forceLoad);
|
||||
|
||||
showListFooter(false);
|
||||
currentInfo = null;
|
||||
if (currentWorker != null) currentWorker.dispose();
|
||||
currentWorker = loadResult(forceLoad)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe((@NonNull I result) -> {
|
||||
isLoading.set(false);
|
||||
currentInfo = result;
|
||||
currentNextItemsUrl = result.next_streams_url;
|
||||
handleResult(result);
|
||||
}, (@NonNull Throwable throwable) -> onError(throwable));
|
||||
}
|
||||
|
||||
/**
|
||||
* Implement the logic to load more items<br/>
|
||||
* You can use the default implementations from {@link org.schabi.newpipe.util.ExtractorHelper}
|
||||
*/
|
||||
protected abstract Single<ListExtractor.NextItemsResult> loadMoreItemsLogic();
|
||||
|
||||
protected void loadMoreItems() {
|
||||
isLoading.set(true);
|
||||
|
||||
if (currentWorker != null) currentWorker.dispose();
|
||||
currentWorker = loadMoreItemsLogic()
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe((@io.reactivex.annotations.NonNull ListExtractor.NextItemsResult nextItemsResult) -> {
|
||||
isLoading.set(false);
|
||||
handleNextItems(nextItemsResult);
|
||||
}, (@io.reactivex.annotations.NonNull Throwable throwable) -> {
|
||||
isLoading.set(false);
|
||||
onError(throwable);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleNextItems(ListExtractor.NextItemsResult result) {
|
||||
super.handleNextItems(result);
|
||||
currentNextItemsUrl = result.nextItemsUrl;
|
||||
infoListAdapter.addInfoItemList(result.nextItemsList);
|
||||
|
||||
showListFooter(hasMoreItems());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean hasMoreItems() {
|
||||
return !TextUtils.isEmpty(currentNextItemsUrl);
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Contract
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public void handleResult(@NonNull I result) {
|
||||
super.handleResult(result);
|
||||
|
||||
url = result.getUrl();
|
||||
name = result.getName();
|
||||
setTitle(name);
|
||||
|
||||
if (infoListAdapter.getItemsList().size() == 0) {
|
||||
if (result.related_streams.size() > 0) {
|
||||
infoListAdapter.addInfoItemList(result.related_streams);
|
||||
showListFooter(hasMoreItems());
|
||||
} else {
|
||||
infoListAdapter.clearStreamItemList();
|
||||
showEmptyState();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Utils
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
protected void setInitialData(int serviceId, String url, String name) {
|
||||
this.serviceId = serviceId;
|
||||
this.url = url;
|
||||
this.name = !TextUtils.isEmpty(name) ? name : "";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package org.schabi.newpipe.fragments.list;
|
||||
|
||||
import org.schabi.newpipe.fragments.ViewContract;
|
||||
|
||||
public interface ListViewContract<I, N> extends ViewContract<I> {
|
||||
void showListFooter(boolean show);
|
||||
|
||||
void handleNextItems(N result);
|
||||
}
|
||||
@@ -0,0 +1,505 @@
|
||||
package org.schabi.newpipe.fragments.list.channel;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v4.content.ContextCompat;
|
||||
import android.support.v7.app.ActionBar;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.Button;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.jakewharton.rxbinding2.view.RxView;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.database.subscription.SubscriptionEntity;
|
||||
import org.schabi.newpipe.extractor.ListExtractor;
|
||||
import org.schabi.newpipe.extractor.NewPipe;
|
||||
import org.schabi.newpipe.extractor.channel.ChannelInfo;
|
||||
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||
import org.schabi.newpipe.fragments.list.BaseListInfoFragment;
|
||||
import org.schabi.newpipe.fragments.subscription.SubscriptionService;
|
||||
import org.schabi.newpipe.info_list.InfoItemDialog;
|
||||
import org.schabi.newpipe.playlist.ChannelPlayQueue;
|
||||
import org.schabi.newpipe.playlist.PlayQueue;
|
||||
import org.schabi.newpipe.playlist.SinglePlayQueue;
|
||||
import org.schabi.newpipe.report.UserAction;
|
||||
import org.schabi.newpipe.util.AnimationUtils;
|
||||
import org.schabi.newpipe.util.ExtractorHelper;
|
||||
import org.schabi.newpipe.util.Localization;
|
||||
import org.schabi.newpipe.util.NavigationHelper;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import io.reactivex.Observable;
|
||||
import io.reactivex.Single;
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.disposables.CompositeDisposable;
|
||||
import io.reactivex.disposables.Disposable;
|
||||
import io.reactivex.functions.Action;
|
||||
import io.reactivex.functions.Consumer;
|
||||
import io.reactivex.functions.Function;
|
||||
import io.reactivex.schedulers.Schedulers;
|
||||
|
||||
import static org.schabi.newpipe.util.AnimationUtils.animateBackgroundColor;
|
||||
import static org.schabi.newpipe.util.AnimationUtils.animateTextColor;
|
||||
import static org.schabi.newpipe.util.AnimationUtils.animateView;
|
||||
|
||||
public class ChannelFragment extends BaseListInfoFragment<ChannelInfo> {
|
||||
|
||||
private CompositeDisposable disposables = new CompositeDisposable();
|
||||
private Disposable subscribeButtonMonitor;
|
||||
private SubscriptionService subscriptionService;
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Views
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
private View headerRootLayout;
|
||||
private ImageView headerChannelBanner;
|
||||
private ImageView headerAvatarView;
|
||||
private TextView headerTitleView;
|
||||
private TextView headerSubscribersTextView;
|
||||
private Button headerSubscribeButton;
|
||||
private View playlistCtrl;
|
||||
|
||||
private LinearLayout headerPlayAllButton;
|
||||
private LinearLayout headerPopupButton;
|
||||
private LinearLayout headerBackgroundButton;
|
||||
|
||||
private MenuItem menuRssButton;
|
||||
|
||||
public static ChannelFragment getInstance(int serviceId, String url, String name) {
|
||||
ChannelFragment instance = new ChannelFragment();
|
||||
instance.setInitialData(serviceId, url, name);
|
||||
return instance;
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// LifeCycle
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public void setUserVisibleHint(boolean isVisibleToUser) {
|
||||
super.setUserVisibleHint(isVisibleToUser);
|
||||
if(activity != null
|
||||
&& useAsFrontPage
|
||||
&& isVisibleToUser) {
|
||||
setTitle(currentInfo != null ? currentInfo.getName() : name);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAttach(Context context) {
|
||||
super.onAttach(context);
|
||||
subscriptionService = SubscriptionService.getInstance();
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||||
return inflater.inflate(R.layout.fragment_channel, container, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
if (disposables != null) disposables.clear();
|
||||
if (subscribeButtonMonitor != null) subscribeButtonMonitor.dispose();
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Init
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
protected View getListHeader() {
|
||||
headerRootLayout = activity.getLayoutInflater().inflate(R.layout.channel_header, itemsList, false);
|
||||
headerChannelBanner = headerRootLayout.findViewById(R.id.channel_banner_image);
|
||||
headerAvatarView = headerRootLayout.findViewById(R.id.channel_avatar_view);
|
||||
headerTitleView = headerRootLayout.findViewById(R.id.channel_title_view);
|
||||
headerSubscribersTextView = headerRootLayout.findViewById(R.id.channel_subscriber_view);
|
||||
headerSubscribeButton = headerRootLayout.findViewById(R.id.channel_subscribe_button);
|
||||
playlistCtrl = headerRootLayout.findViewById(R.id.playlist_control);
|
||||
|
||||
|
||||
headerPlayAllButton = headerRootLayout.findViewById(R.id.playlist_ctrl_play_all_button);
|
||||
headerPopupButton = headerRootLayout.findViewById(R.id.playlist_ctrl_play_popup_button);
|
||||
headerBackgroundButton = headerRootLayout.findViewById(R.id.playlist_ctrl_play_bg_button);
|
||||
|
||||
return headerRootLayout;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void showStreamDialog(final StreamInfoItem item) {
|
||||
final Activity activity = getActivity();
|
||||
final Context context = getContext();
|
||||
if (context == null || context.getResources() == null || getActivity() == null) return;
|
||||
|
||||
final String[] commands = new String[]{
|
||||
context.getResources().getString(R.string.enqueue_on_background),
|
||||
context.getResources().getString(R.string.enqueue_on_popup),
|
||||
context.getResources().getString(R.string.start_here_on_main),
|
||||
context.getResources().getString(R.string.start_here_on_background),
|
||||
context.getResources().getString(R.string.start_here_on_popup),
|
||||
};
|
||||
|
||||
final DialogInterface.OnClickListener actions = new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialogInterface, int i) {
|
||||
final int index = Math.max(infoListAdapter.getItemsList().indexOf(item), 0);
|
||||
switch (i) {
|
||||
case 0:
|
||||
NavigationHelper.enqueueOnBackgroundPlayer(context, new SinglePlayQueue(item));
|
||||
break;
|
||||
case 1:
|
||||
NavigationHelper.enqueueOnPopupPlayer(activity, new SinglePlayQueue(item));
|
||||
break;
|
||||
case 2:
|
||||
NavigationHelper.playOnMainPlayer(context, getPlayQueue(index));
|
||||
break;
|
||||
case 3:
|
||||
NavigationHelper.playOnBackgroundPlayer(context, getPlayQueue(index));
|
||||
break;
|
||||
case 4:
|
||||
NavigationHelper.playOnPopupPlayer(activity, getPlayQueue(index));
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
new InfoItemDialog(getActivity(), item, commands, actions).show();
|
||||
}
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Menu
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
|
||||
super.onCreateOptionsMenu(menu, inflater);
|
||||
ActionBar supportActionBar = activity.getSupportActionBar();
|
||||
if(useAsFrontPage && supportActionBar != null) {
|
||||
supportActionBar.setDisplayHomeAsUpEnabled(false);
|
||||
} else {
|
||||
inflater.inflate(R.menu.menu_channel, menu);
|
||||
|
||||
if (DEBUG) Log.d(TAG, "onCreateOptionsMenu() called with: menu = [" + menu +
|
||||
"], inflater = [" + inflater + "]");
|
||||
menuRssButton = menu.findItem(R.id.menu_item_rss);
|
||||
}
|
||||
}
|
||||
|
||||
private void openRssFeed() {
|
||||
final ChannelInfo info = currentInfo;
|
||||
if(info != null) {
|
||||
Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(info.getFeedUrl()));
|
||||
startActivity(intent);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case R.id.menu_item_rss:
|
||||
openRssFeed();
|
||||
break;
|
||||
case R.id.menu_item_openInBrowser:
|
||||
openUrlInBrowser(url);
|
||||
break;
|
||||
case R.id.menu_item_share:
|
||||
shareUrl(name, url);
|
||||
break;
|
||||
default:
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Channel Subscription
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
private static final int BUTTON_DEBOUNCE_INTERVAL = 100;
|
||||
|
||||
private void monitorSubscription(final ChannelInfo info) {
|
||||
final Consumer<Throwable> onError = new Consumer<Throwable>() {
|
||||
@Override
|
||||
public void accept(Throwable throwable) throws Exception {
|
||||
animateView(headerSubscribeButton, false, 100);
|
||||
showSnackBarError(throwable, UserAction.SUBSCRIPTION, NewPipe.getNameOfService(currentInfo.getServiceId()), "Get subscription status", 0);
|
||||
}
|
||||
};
|
||||
|
||||
final Observable<List<SubscriptionEntity>> observable = subscriptionService.subscriptionTable()
|
||||
.getSubscription(info.getServiceId(), info.getUrl())
|
||||
.toObservable();
|
||||
|
||||
disposables.add(observable
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(getSubscribeUpdateMonitor(info), onError));
|
||||
|
||||
disposables.add(observable
|
||||
// Some updates are very rapid (when calling the updateSubscription(info), for example)
|
||||
// so only update the UI for the latest emission ("sync" the subscribe button's state)
|
||||
.debounce(100, TimeUnit.MILLISECONDS)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(new Consumer<List<SubscriptionEntity>>() {
|
||||
@Override
|
||||
public void accept(List<SubscriptionEntity> subscriptionEntities) throws Exception {
|
||||
updateSubscribeButton(!subscriptionEntities.isEmpty());
|
||||
}
|
||||
}, onError));
|
||||
|
||||
}
|
||||
|
||||
private Function<Object, Object> mapOnSubscribe(final SubscriptionEntity subscription) {
|
||||
return new Function<Object, Object>() {
|
||||
@Override
|
||||
public Object apply(@NonNull Object o) throws Exception {
|
||||
subscriptionService.subscriptionTable().insert(subscription);
|
||||
return o;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private Function<Object, Object> mapOnUnsubscribe(final SubscriptionEntity subscription) {
|
||||
return new Function<Object, Object>() {
|
||||
@Override
|
||||
public Object apply(@NonNull Object o) throws Exception {
|
||||
subscriptionService.subscriptionTable().delete(subscription);
|
||||
return o;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private void updateSubscription(final ChannelInfo info) {
|
||||
if (DEBUG) Log.d(TAG, "updateSubscription() called with: info = [" + info + "]");
|
||||
final Action onComplete = new Action() {
|
||||
@Override
|
||||
public void run() throws Exception {
|
||||
if (DEBUG) Log.d(TAG, "Updated subscription: " + info.getUrl());
|
||||
}
|
||||
};
|
||||
|
||||
final Consumer<Throwable> onError = new Consumer<Throwable>() {
|
||||
@Override
|
||||
public void accept(@NonNull Throwable throwable) throws Exception {
|
||||
onUnrecoverableError(throwable, UserAction.SUBSCRIPTION, NewPipe.getNameOfService(info.getServiceId()), "Updating Subscription for " + info.getUrl(), R.string.subscription_update_failed);
|
||||
}
|
||||
};
|
||||
|
||||
disposables.add(subscriptionService.updateChannelInfo(info)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(onComplete, onError));
|
||||
}
|
||||
|
||||
private Disposable monitorSubscribeButton(final Button subscribeButton, final Function<Object, Object> action) {
|
||||
final Consumer<Object> onNext = new Consumer<Object>() {
|
||||
@Override
|
||||
public void accept(@NonNull Object o) throws Exception {
|
||||
if (DEBUG) Log.d(TAG, "Changed subscription status to this channel!");
|
||||
}
|
||||
};
|
||||
|
||||
final Consumer<Throwable> onError = new Consumer<Throwable>() {
|
||||
@Override
|
||||
public void accept(@NonNull Throwable throwable) throws Exception {
|
||||
onUnrecoverableError(throwable, UserAction.SUBSCRIPTION, NewPipe.getNameOfService(currentInfo.getServiceId()), "Subscription Change", R.string.subscription_change_failed);
|
||||
}
|
||||
};
|
||||
|
||||
/* Emit clicks from main thread unto io thread */
|
||||
return RxView.clicks(subscribeButton)
|
||||
.subscribeOn(AndroidSchedulers.mainThread())
|
||||
.observeOn(Schedulers.io())
|
||||
.debounce(BUTTON_DEBOUNCE_INTERVAL, TimeUnit.MILLISECONDS) // Ignore rapid clicks
|
||||
.map(action)
|
||||
.subscribe(onNext, onError);
|
||||
}
|
||||
|
||||
private Consumer<List<SubscriptionEntity>> getSubscribeUpdateMonitor(final ChannelInfo info) {
|
||||
return new Consumer<List<SubscriptionEntity>>() {
|
||||
@Override
|
||||
public void accept(List<SubscriptionEntity> subscriptionEntities) throws Exception {
|
||||
if (DEBUG)
|
||||
Log.d(TAG, "subscriptionService.subscriptionTable.doOnNext() called with: subscriptionEntities = [" + subscriptionEntities + "]");
|
||||
if (subscribeButtonMonitor != null) subscribeButtonMonitor.dispose();
|
||||
|
||||
if (subscriptionEntities.isEmpty()) {
|
||||
if (DEBUG) Log.d(TAG, "No subscription to this channel!");
|
||||
SubscriptionEntity channel = new SubscriptionEntity();
|
||||
channel.setServiceId(info.getServiceId());
|
||||
channel.setUrl(info.getUrl());
|
||||
channel.setData(info.getName(), info.getAvatarUrl(), info.getDescription(), info.getSubscriberCount());
|
||||
subscribeButtonMonitor = monitorSubscribeButton(headerSubscribeButton, mapOnSubscribe(channel));
|
||||
} else {
|
||||
if (DEBUG) Log.d(TAG, "Found subscription to this channel!");
|
||||
final SubscriptionEntity subscription = subscriptionEntities.get(0);
|
||||
subscribeButtonMonitor = monitorSubscribeButton(headerSubscribeButton, mapOnUnsubscribe(subscription));
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private void updateSubscribeButton(boolean isSubscribed) {
|
||||
if (DEBUG) Log.d(TAG, "updateSubscribeButton() called with: isSubscribed = [" + isSubscribed + "]");
|
||||
|
||||
boolean isButtonVisible = headerSubscribeButton.getVisibility() == View.VISIBLE;
|
||||
int backgroundDuration = isButtonVisible ? 300 : 0;
|
||||
int textDuration = isButtonVisible ? 200 : 0;
|
||||
|
||||
int subscribeBackground = ContextCompat.getColor(activity, R.color.subscribe_background_color);
|
||||
int subscribeText = ContextCompat.getColor(activity, R.color.subscribe_text_color);
|
||||
int subscribedBackground = ContextCompat.getColor(activity, R.color.subscribed_background_color);
|
||||
int subscribedText = ContextCompat.getColor(activity, R.color.subscribed_text_color);
|
||||
|
||||
if (!isSubscribed) {
|
||||
headerSubscribeButton.setText(R.string.subscribe_button_title);
|
||||
animateBackgroundColor(headerSubscribeButton, backgroundDuration, subscribedBackground, subscribeBackground);
|
||||
animateTextColor(headerSubscribeButton, textDuration, subscribedText, subscribeText);
|
||||
} else {
|
||||
headerSubscribeButton.setText(R.string.subscribed_button_title);
|
||||
animateBackgroundColor(headerSubscribeButton, backgroundDuration, subscribeBackground, subscribedBackground);
|
||||
animateTextColor(headerSubscribeButton, textDuration, subscribeText, subscribedText);
|
||||
}
|
||||
|
||||
animateView(headerSubscribeButton, AnimationUtils.Type.LIGHT_SCALE_AND_ALPHA, true, 100);
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Load and handle
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
protected Single<ListExtractor.NextItemsResult> loadMoreItemsLogic() {
|
||||
return ExtractorHelper.getMoreChannelItems(serviceId, url, currentNextItemsUrl);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Single<ChannelInfo> loadResult(boolean forceLoad) {
|
||||
return ExtractorHelper.getChannelInfo(serviceId, url, forceLoad);
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Contract
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public void showLoading() {
|
||||
super.showLoading();
|
||||
|
||||
imageLoader.cancelDisplayTask(headerChannelBanner);
|
||||
imageLoader.cancelDisplayTask(headerAvatarView);
|
||||
animateView(headerSubscribeButton, false, 100);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleResult(@NonNull ChannelInfo result) {
|
||||
super.handleResult(result);
|
||||
|
||||
headerRootLayout.setVisibility(View.VISIBLE);
|
||||
imageLoader.displayImage(result.banner_url, headerChannelBanner, DISPLAY_BANNER_OPTIONS);
|
||||
imageLoader.displayImage(result.avatar_url, headerAvatarView, DISPLAY_AVATAR_OPTIONS);
|
||||
|
||||
if (result.getSubscriberCount() != -1) {
|
||||
headerSubscribersTextView.setText(Localization.localizeSubscribersCount(activity, result.getSubscriberCount()));
|
||||
headerSubscribersTextView.setVisibility(View.VISIBLE);
|
||||
} else headerSubscribersTextView.setVisibility(View.GONE);
|
||||
|
||||
if (menuRssButton != null) menuRssButton.setVisible(!TextUtils.isEmpty(result.getFeedUrl()));
|
||||
|
||||
playlistCtrl.setVisibility(View.VISIBLE);
|
||||
|
||||
if (!result.errors.isEmpty()) {
|
||||
showSnackBarError(result.errors, UserAction.REQUESTED_CHANNEL, NewPipe.getNameOfService(result.getServiceId()), result.getUrl(), 0);
|
||||
}
|
||||
|
||||
if (disposables != null) disposables.clear();
|
||||
if (subscribeButtonMonitor != null) subscribeButtonMonitor.dispose();
|
||||
updateSubscription(result);
|
||||
monitorSubscription(result);
|
||||
|
||||
headerPlayAllButton.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
NavigationHelper.playOnMainPlayer(activity, getPlayQueue());
|
||||
}
|
||||
});
|
||||
headerPopupButton.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
NavigationHelper.playOnPopupPlayer(activity, getPlayQueue());
|
||||
}
|
||||
});
|
||||
headerBackgroundButton.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
NavigationHelper.playOnBackgroundPlayer(activity, getPlayQueue());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private PlayQueue getPlayQueue() {
|
||||
return getPlayQueue(0);
|
||||
}
|
||||
|
||||
private PlayQueue getPlayQueue(final int index) {
|
||||
return new ChannelPlayQueue(
|
||||
currentInfo.getServiceId(),
|
||||
currentInfo.getUrl(),
|
||||
currentInfo.getNextStreamsUrl(),
|
||||
infoListAdapter.getItemsList(),
|
||||
index
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleNextItems(ListExtractor.NextItemsResult result) {
|
||||
super.handleNextItems(result);
|
||||
|
||||
if (!result.getErrors().isEmpty()) {
|
||||
showSnackBarError(result.getErrors(), UserAction.REQUESTED_CHANNEL, NewPipe.getNameOfService(serviceId),
|
||||
"Get next page of: " + url, R.string.general_error);
|
||||
}
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// OnError
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
protected boolean onError(Throwable exception) {
|
||||
if (super.onError(exception)) return true;
|
||||
|
||||
int errorId = exception instanceof ExtractionException ? R.string.parsing_error : R.string.general_error;
|
||||
onUnrecoverableError(exception, UserAction.REQUESTED_CHANNEL, NewPipe.getNameOfService(serviceId), url, errorId);
|
||||
return true;
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Utils
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public void setTitle(String title) {
|
||||
super.setTitle(title);
|
||||
headerTitleView.setText(title);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,448 @@
|
||||
package org.schabi.newpipe.fragments.list.feed;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v7.app.ActionBar;
|
||||
import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import org.reactivestreams.Subscriber;
|
||||
import org.reactivestreams.Subscription;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.database.subscription.SubscriptionEntity;
|
||||
import org.schabi.newpipe.extractor.InfoItem;
|
||||
import org.schabi.newpipe.extractor.NewPipe;
|
||||
import org.schabi.newpipe.extractor.channel.ChannelInfo;
|
||||
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||
import org.schabi.newpipe.fragments.list.BaseListFragment;
|
||||
import org.schabi.newpipe.fragments.subscription.SubscriptionService;
|
||||
import org.schabi.newpipe.report.UserAction;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Queue;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import io.reactivex.Flowable;
|
||||
import io.reactivex.MaybeObserver;
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.disposables.CompositeDisposable;
|
||||
import io.reactivex.disposables.Disposable;
|
||||
import io.reactivex.functions.Consumer;
|
||||
import io.reactivex.functions.Predicate;
|
||||
|
||||
public class FeedFragment extends BaseListFragment<List<SubscriptionEntity>, Void> {
|
||||
|
||||
private static final int OFF_SCREEN_ITEMS_COUNT = 3;
|
||||
private static final int MIN_ITEMS_INITIAL_LOAD = 8;
|
||||
private int FEED_LOAD_COUNT = MIN_ITEMS_INITIAL_LOAD;
|
||||
|
||||
private int subscriptionPoolSize;
|
||||
|
||||
private SubscriptionService subscriptionService;
|
||||
|
||||
private AtomicBoolean allItemsLoaded = new AtomicBoolean(false);
|
||||
private HashSet<String> itemsLoaded = new HashSet<>();
|
||||
private final AtomicInteger requestLoadedAtomic = new AtomicInteger();
|
||||
|
||||
private CompositeDisposable compositeDisposable = new CompositeDisposable();
|
||||
private Disposable subscriptionObserver;
|
||||
private Subscription feedSubscriber;
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Fragment LifeCycle
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
subscriptionService = SubscriptionService.getInstance();
|
||||
|
||||
FEED_LOAD_COUNT = howManyItemsToLoad();
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) {
|
||||
return inflater.inflate(R.layout.fragment_feed, container, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPause() {
|
||||
super.onPause();
|
||||
disposeEverything();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
if (wasLoading.get()) doInitialLoadLogic();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
|
||||
disposeEverything();
|
||||
subscriptionService = null;
|
||||
compositeDisposable = null;
|
||||
subscriptionObserver = null;
|
||||
feedSubscriber = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyView() {
|
||||
// Do not monitor for updates when user is not viewing the feed fragment.
|
||||
// This is a waste of bandwidth.
|
||||
disposeEverything();
|
||||
super.onDestroyView();
|
||||
}
|
||||
|
||||
/*@Override
|
||||
protected RecyclerView.LayoutManager getListLayoutManager() {
|
||||
boolean isPortrait = getResources().getDisplayMetrics().heightPixels > getResources().getDisplayMetrics().widthPixels;
|
||||
return new GridLayoutManager(activity, isPortrait ? 1 : 2);
|
||||
}*/
|
||||
|
||||
@Override
|
||||
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
|
||||
super.onCreateOptionsMenu(menu, inflater);
|
||||
|
||||
ActionBar supportActionBar = activity.getSupportActionBar();
|
||||
if (supportActionBar != null) {
|
||||
supportActionBar.setTitle(R.string.fragment_whats_new);
|
||||
}
|
||||
|
||||
if(useAsFrontPage) {
|
||||
supportActionBar.setDisplayShowTitleEnabled(true);
|
||||
//supportActionBar.setDisplayShowTitleEnabled(false);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reloadContent() {
|
||||
resetFragment();
|
||||
super.reloadContent();
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// StateSaving
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public void writeTo(Queue<Object> objectsToSave) {
|
||||
super.writeTo(objectsToSave);
|
||||
objectsToSave.add(allItemsLoaded);
|
||||
objectsToSave.add(itemsLoaded);
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public void readFrom(@NonNull Queue<Object> savedObjects) throws Exception {
|
||||
super.readFrom(savedObjects);
|
||||
allItemsLoaded = (AtomicBoolean) savedObjects.poll();
|
||||
itemsLoaded = (HashSet<String>) savedObjects.poll();
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Feed Loader
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public void startLoading(boolean forceLoad) {
|
||||
if (DEBUG) Log.d(TAG, "startLoading() called with: forceLoad = [" + forceLoad + "]");
|
||||
if (subscriptionObserver != null) subscriptionObserver.dispose();
|
||||
|
||||
if (allItemsLoaded.get()) {
|
||||
if (infoListAdapter.getItemsList().size() == 0) {
|
||||
showEmptyState();
|
||||
} else {
|
||||
showListFooter(false);
|
||||
hideLoading();
|
||||
}
|
||||
|
||||
isLoading.set(false);
|
||||
return;
|
||||
}
|
||||
|
||||
isLoading.set(true);
|
||||
showLoading();
|
||||
showListFooter(true);
|
||||
subscriptionObserver = subscriptionService.getSubscription()
|
||||
.onErrorReturnItem(Collections.<SubscriptionEntity>emptyList())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(new Consumer<List<SubscriptionEntity>>() {
|
||||
@Override
|
||||
public void accept(List<SubscriptionEntity> subscriptionEntities) throws Exception {
|
||||
handleResult(subscriptionEntities);
|
||||
}
|
||||
}, new Consumer<Throwable>() {
|
||||
@Override
|
||||
public void accept(Throwable throwable) throws Exception {
|
||||
onError(throwable);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleResult(@android.support.annotation.NonNull List<SubscriptionEntity> result) {
|
||||
super.handleResult(result);
|
||||
|
||||
if (result.isEmpty()) {
|
||||
infoListAdapter.clearStreamItemList();
|
||||
showEmptyState();
|
||||
return;
|
||||
}
|
||||
|
||||
subscriptionPoolSize = result.size();
|
||||
Flowable.fromIterable(result)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(getSubscriptionObserver());
|
||||
}
|
||||
|
||||
/**
|
||||
* Responsible for reacting to user pulling request and starting a request for new feed stream.
|
||||
* <p>
|
||||
* On initialization, it automatically requests the amount of feed needed to display
|
||||
* a minimum amount required (FEED_LOAD_SIZE).
|
||||
* <p>
|
||||
* Upon receiving a user pull, it creates a Single Observer to fetch the ChannelInfo
|
||||
* containing the feed streams.
|
||||
**/
|
||||
private Subscriber<SubscriptionEntity> getSubscriptionObserver() {
|
||||
return new Subscriber<SubscriptionEntity>() {
|
||||
@Override
|
||||
public void onSubscribe(Subscription s) {
|
||||
if (feedSubscriber != null) feedSubscriber.cancel();
|
||||
feedSubscriber = s;
|
||||
|
||||
int requestSize = FEED_LOAD_COUNT - infoListAdapter.getItemsList().size();
|
||||
if (wasLoading.getAndSet(false)) requestSize = FEED_LOAD_COUNT;
|
||||
|
||||
boolean hasToLoad = requestSize > 0;
|
||||
if (hasToLoad) {
|
||||
requestLoadedAtomic.set(infoListAdapter.getItemsList().size());
|
||||
requestFeed(requestSize);
|
||||
}
|
||||
isLoading.set(hasToLoad);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNext(SubscriptionEntity subscriptionEntity) {
|
||||
if (!itemsLoaded.contains(subscriptionEntity.getServiceId() + subscriptionEntity.getUrl())) {
|
||||
subscriptionService.getChannelInfo(subscriptionEntity)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.onErrorComplete(new Predicate<Throwable>() {
|
||||
@Override
|
||||
public boolean test(@io.reactivex.annotations.NonNull Throwable throwable) throws Exception {
|
||||
return FeedFragment.super.onError(throwable);
|
||||
}
|
||||
})
|
||||
.subscribe(getChannelInfoObserver(subscriptionEntity.getServiceId(), subscriptionEntity.getUrl()));
|
||||
} else {
|
||||
requestFeed(1);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(Throwable exception) {
|
||||
FeedFragment.this.onError(exception);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onComplete() {
|
||||
if (DEBUG) Log.d(TAG, "getSubscriptionObserver > onComplete() called");
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* On each request, a subscription item from the updated table is transformed
|
||||
* into a ChannelInfo, containing the latest streams from the channel.
|
||||
* <p>
|
||||
* Currently, the feed uses the first into from the list of streams.
|
||||
* <p>
|
||||
* If chosen feed already displayed, then we request another feed from another
|
||||
* subscription, until the subscription table runs out of new items.
|
||||
* <p>
|
||||
* This Observer is self-contained and will dispose itself when complete. However, this
|
||||
* does not obey the fragment lifecycle and may continue running in the background
|
||||
* until it is complete. This is done due to RxJava2 no longer propagate errors once
|
||||
* an observer is unsubscribed while the thread process is still running.
|
||||
* <p>
|
||||
* To solve the above issue, we can either set a global RxJava Error Handler, or
|
||||
* manage exceptions case by case. This should be done if the current implementation is
|
||||
* too costly when dealing with larger subscription sets.
|
||||
*
|
||||
* @param url + serviceId to put in {@link #allItemsLoaded} to signal that this specific entity has been loaded.
|
||||
*/
|
||||
private MaybeObserver<ChannelInfo> getChannelInfoObserver(final int serviceId, final String url) {
|
||||
return new MaybeObserver<ChannelInfo>() {
|
||||
private Disposable observer;
|
||||
|
||||
@Override
|
||||
public void onSubscribe(Disposable d) {
|
||||
observer = d;
|
||||
compositeDisposable.add(d);
|
||||
isLoading.set(true);
|
||||
}
|
||||
|
||||
// Called only when response is non-empty
|
||||
@Override
|
||||
public void onSuccess(final ChannelInfo channelInfo) {
|
||||
if (infoListAdapter == null || channelInfo.getRelatedStreams().isEmpty()) {
|
||||
onDone();
|
||||
return;
|
||||
}
|
||||
|
||||
final InfoItem item = channelInfo.getRelatedStreams().get(0);
|
||||
// Keep requesting new items if the current one already exists
|
||||
boolean itemExists = doesItemExist(infoListAdapter.getItemsList(), item);
|
||||
if (!itemExists) {
|
||||
infoListAdapter.addInfoItem(item);
|
||||
//updateSubscription(channelInfo);
|
||||
} else {
|
||||
requestFeed(1);
|
||||
}
|
||||
onDone();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(Throwable exception) {
|
||||
showSnackBarError(exception, UserAction.SUBSCRIPTION, NewPipe.getNameOfService(serviceId), url, 0);
|
||||
requestFeed(1);
|
||||
onDone();
|
||||
}
|
||||
|
||||
// Called only when response is empty
|
||||
@Override
|
||||
public void onComplete() {
|
||||
onDone();
|
||||
}
|
||||
|
||||
private void onDone() {
|
||||
if (observer.isDisposed()) {
|
||||
return;
|
||||
}
|
||||
|
||||
itemsLoaded.add(serviceId + url);
|
||||
compositeDisposable.remove(observer);
|
||||
|
||||
int loaded = requestLoadedAtomic.incrementAndGet();
|
||||
if (loaded >= Math.min(FEED_LOAD_COUNT, subscriptionPoolSize)) {
|
||||
requestLoadedAtomic.set(0);
|
||||
isLoading.set(false);
|
||||
}
|
||||
|
||||
if (itemsLoaded.size() == subscriptionPoolSize) {
|
||||
if (DEBUG) Log.d(TAG, "getChannelInfoObserver > All Items Loaded");
|
||||
allItemsLoaded.set(true);
|
||||
showListFooter(false);
|
||||
isLoading.set(false);
|
||||
hideLoading();
|
||||
if (infoListAdapter.getItemsList().size() == 0) {
|
||||
showEmptyState();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void loadMoreItems() {
|
||||
isLoading.set(true);
|
||||
delayHandler.removeCallbacksAndMessages(null);
|
||||
// Add a little of a delay when requesting more items because the cache is so fast,
|
||||
// that the view seems stuck to the user when he scroll to the bottom
|
||||
delayHandler.postDelayed(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
requestFeed(FEED_LOAD_COUNT);
|
||||
}
|
||||
}, 300);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean hasMoreItems() {
|
||||
return !allItemsLoaded.get();
|
||||
}
|
||||
|
||||
private final Handler delayHandler = new Handler();
|
||||
|
||||
private void requestFeed(final int count) {
|
||||
if (DEBUG) Log.d(TAG, "requestFeed() called with: count = [" + count + "], feedSubscriber = [" + feedSubscriber + "]");
|
||||
if (feedSubscriber == null) return;
|
||||
|
||||
isLoading.set(true);
|
||||
delayHandler.removeCallbacksAndMessages(null);
|
||||
feedSubscriber.request(count);
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Utils
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
private void resetFragment() {
|
||||
if (DEBUG) Log.d(TAG, "resetFragment() called");
|
||||
if (subscriptionObserver != null) subscriptionObserver.dispose();
|
||||
if (compositeDisposable != null) compositeDisposable.clear();
|
||||
if (infoListAdapter != null) infoListAdapter.clearStreamItemList();
|
||||
|
||||
delayHandler.removeCallbacksAndMessages(null);
|
||||
requestLoadedAtomic.set(0);
|
||||
allItemsLoaded.set(false);
|
||||
showListFooter(false);
|
||||
itemsLoaded.clear();
|
||||
}
|
||||
|
||||
private void disposeEverything() {
|
||||
if (subscriptionObserver != null) subscriptionObserver.dispose();
|
||||
if (compositeDisposable != null) compositeDisposable.clear();
|
||||
if (feedSubscriber != null) feedSubscriber.cancel();
|
||||
delayHandler.removeCallbacksAndMessages(null);
|
||||
}
|
||||
|
||||
private boolean doesItemExist(final List<InfoItem> items, final InfoItem item) {
|
||||
for (final InfoItem existingItem : items) {
|
||||
if (existingItem.info_type == item.info_type &&
|
||||
existingItem.getServiceId() == item.getServiceId() &&
|
||||
existingItem.getName().equals(item.getName()) &&
|
||||
existingItem.getUrl().equals(item.getUrl())) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private int howManyItemsToLoad() {
|
||||
int heightPixels = getResources().getDisplayMetrics().heightPixels;
|
||||
int itemHeightPixels = activity.getResources().getDimensionPixelSize(R.dimen.video_item_search_height);
|
||||
|
||||
int items = itemHeightPixels > 0 ? heightPixels / itemHeightPixels + OFF_SCREEN_ITEMS_COUNT : MIN_ITEMS_INITIAL_LOAD;
|
||||
return Math.max(MIN_ITEMS_INITIAL_LOAD, items);
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Fragment Error Handling
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public void showError(String message, boolean showRetryButton) {
|
||||
resetFragment();
|
||||
super.showError(message, showRetryButton);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean onError(Throwable exception) {
|
||||
if (super.onError(exception)) return true;
|
||||
|
||||
int errorId = exception instanceof ExtractionException ? R.string.parsing_error : R.string.general_error;
|
||||
onUnrecoverableError(exception, UserAction.SOMETHING_ELSE, "none", "Requesting feed", errorId);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
package org.schabi.newpipe.fragments.list.kiosk;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v7.app.ActionBar;
|
||||
import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.extractor.ListExtractor;
|
||||
import org.schabi.newpipe.extractor.NewPipe;
|
||||
import org.schabi.newpipe.extractor.StreamingService;
|
||||
import org.schabi.newpipe.extractor.UrlIdHandler;
|
||||
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||
import org.schabi.newpipe.extractor.kiosk.KioskInfo;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||
import org.schabi.newpipe.fragments.list.BaseListInfoFragment;
|
||||
import org.schabi.newpipe.info_list.InfoItemBuilder;
|
||||
import org.schabi.newpipe.report.UserAction;
|
||||
import org.schabi.newpipe.util.ExtractorHelper;
|
||||
import org.schabi.newpipe.util.KioskTranslator;
|
||||
import org.schabi.newpipe.util.NavigationHelper;
|
||||
|
||||
import icepick.State;
|
||||
import io.reactivex.Single;
|
||||
|
||||
import static org.schabi.newpipe.util.AnimationUtils.animateView;
|
||||
|
||||
/**
|
||||
* Created by Christian Schabesberger on 23.09.17.
|
||||
*
|
||||
* Copyright (C) Christian Schabesberger 2017 <chris.schabesberger@mailbox.org>
|
||||
* KioskFragment.java is part of NewPipe.
|
||||
*
|
||||
* NewPipe is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* NewPipe is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
public class KioskFragment extends BaseListInfoFragment<KioskInfo> {
|
||||
|
||||
@State
|
||||
protected String kioskId = "";
|
||||
protected String kioskTranslatedName;
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Views
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
public static KioskFragment getInstance(int serviceId)
|
||||
throws ExtractionException {
|
||||
return getInstance(serviceId, NewPipe.getService(serviceId)
|
||||
.getKioskList()
|
||||
.getDefaultKioskId());
|
||||
}
|
||||
|
||||
public static KioskFragment getInstance(int serviceId, String kioskId)
|
||||
throws ExtractionException {
|
||||
KioskFragment instance = new KioskFragment();
|
||||
StreamingService service = NewPipe.getService(serviceId);
|
||||
UrlIdHandler kioskTypeUrlIdHandler = service.getKioskList()
|
||||
.getUrlIdHandlerByType(kioskId);
|
||||
instance.setInitialData(serviceId,
|
||||
kioskTypeUrlIdHandler.getUrl(kioskId),
|
||||
kioskId);
|
||||
instance.kioskId = kioskId;
|
||||
return instance;
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// LifeCycle
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
kioskTranslatedName = KioskTranslator.getTranslatedKioskName(kioskId, activity);
|
||||
name = kioskTranslatedName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setUserVisibleHint(boolean isVisibleToUser) {
|
||||
super.setUserVisibleHint(isVisibleToUser);
|
||||
if(useAsFrontPage && isVisibleToUser && activity != null) {
|
||||
try {
|
||||
setTitle(kioskTranslatedName);
|
||||
} catch (Exception e) {
|
||||
onUnrecoverableError(e, UserAction.UI_ERROR,
|
||||
"none",
|
||||
"none", R.string.app_ui_crash);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||||
return inflater.inflate(R.layout.fragment_kiosk, container, false);
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Menu
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
|
||||
super.onCreateOptionsMenu(menu, inflater);
|
||||
ActionBar supportActionBar = activity.getSupportActionBar();
|
||||
if (supportActionBar != null && useAsFrontPage) {
|
||||
supportActionBar.setDisplayHomeAsUpEnabled(false);
|
||||
}
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Load and handle
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public Single<KioskInfo> loadResult(boolean forceReload) {
|
||||
String contentCountry = PreferenceManager
|
||||
.getDefaultSharedPreferences(activity)
|
||||
.getString(getString(R.string.content_country_key),
|
||||
getString(R.string.default_country_value));
|
||||
return ExtractorHelper.getKioskInfo(serviceId, url, contentCountry, forceReload);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Single<ListExtractor.NextItemsResult> loadMoreItemsLogic() {
|
||||
String contentCountry = PreferenceManager
|
||||
.getDefaultSharedPreferences(activity)
|
||||
.getString(getString(R.string.content_country_key),
|
||||
getString(R.string.default_country_value));
|
||||
return ExtractorHelper.getMoreKioskItems(serviceId, url, currentNextItemsUrl, contentCountry);
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Contract
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public void showLoading() {
|
||||
super.showLoading();
|
||||
animateView(itemsList, false, 100);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleResult(@NonNull final KioskInfo result) {
|
||||
super.handleResult(result);
|
||||
|
||||
name = kioskTranslatedName;
|
||||
setTitle(kioskTranslatedName);
|
||||
|
||||
if (!result.getErrors().isEmpty()) {
|
||||
showSnackBarError(result.getErrors(),
|
||||
UserAction.REQUESTED_PLAYLIST,
|
||||
NewPipe.getNameOfService(result.getServiceId()), result.getUrl(), 0);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleNextItems(ListExtractor.NextItemsResult result) {
|
||||
super.handleNextItems(result);
|
||||
|
||||
if (!result.getErrors().isEmpty()) {
|
||||
showSnackBarError(result.getErrors(),
|
||||
UserAction.REQUESTED_PLAYLIST, NewPipe.getNameOfService(serviceId)
|
||||
, "Get next page of: " + url, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,408 @@
|
||||
package org.schabi.newpipe.fragments.list.playlist;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.reactivestreams.Subscriber;
|
||||
import org.reactivestreams.Subscription;
|
||||
import org.schabi.newpipe.NewPipeDatabase;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity;
|
||||
import org.schabi.newpipe.extractor.ListExtractor;
|
||||
import org.schabi.newpipe.extractor.NewPipe;
|
||||
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||
import org.schabi.newpipe.extractor.playlist.PlaylistInfo;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||
import org.schabi.newpipe.fragments.list.BaseListInfoFragment;
|
||||
import org.schabi.newpipe.fragments.local.RemotePlaylistManager;
|
||||
import org.schabi.newpipe.info_list.InfoItemDialog;
|
||||
import org.schabi.newpipe.playlist.PlayQueue;
|
||||
import org.schabi.newpipe.playlist.PlaylistPlayQueue;
|
||||
import org.schabi.newpipe.playlist.SinglePlayQueue;
|
||||
import org.schabi.newpipe.report.UserAction;
|
||||
import org.schabi.newpipe.util.ExtractorHelper;
|
||||
import org.schabi.newpipe.util.NavigationHelper;
|
||||
import org.schabi.newpipe.util.ThemeHelper;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
import io.reactivex.Single;
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.disposables.CompositeDisposable;
|
||||
import io.reactivex.disposables.Disposable;
|
||||
import io.reactivex.disposables.Disposables;
|
||||
|
||||
import static org.schabi.newpipe.util.AnimationUtils.animateView;
|
||||
|
||||
public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
|
||||
|
||||
private CompositeDisposable disposables;
|
||||
private Subscription bookmarkReactor;
|
||||
private AtomicBoolean isBookmarkButtonReady;
|
||||
|
||||
private RemotePlaylistManager remotePlaylistManager;
|
||||
private PlaylistRemoteEntity playlistEntity;
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Views
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
private View headerRootLayout;
|
||||
private TextView headerTitleView;
|
||||
private View headerUploaderLayout;
|
||||
private TextView headerUploaderName;
|
||||
private ImageView headerUploaderAvatar;
|
||||
private TextView headerStreamCount;
|
||||
private View playlistCtrl;
|
||||
|
||||
private View headerPlayAllButton;
|
||||
private View headerPopupButton;
|
||||
private View headerBackgroundButton;
|
||||
|
||||
private MenuItem playlistBookmarkButton;
|
||||
|
||||
public static PlaylistFragment getInstance(int serviceId, String url, String name) {
|
||||
PlaylistFragment instance = new PlaylistFragment();
|
||||
instance.setInitialData(serviceId, url, name);
|
||||
return instance;
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// LifeCycle
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
disposables = new CompositeDisposable();
|
||||
isBookmarkButtonReady = new AtomicBoolean(false);
|
||||
remotePlaylistManager = new RemotePlaylistManager(NewPipeDatabase.getInstance(getContext()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
|
||||
@Nullable Bundle savedInstanceState) {
|
||||
return inflater.inflate(R.layout.fragment_playlist, container, false);
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Init
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
protected View getListHeader() {
|
||||
headerRootLayout = activity.getLayoutInflater().inflate(R.layout.playlist_header, itemsList, false);
|
||||
headerTitleView = headerRootLayout.findViewById(R.id.playlist_title_view);
|
||||
headerUploaderLayout = headerRootLayout.findViewById(R.id.uploader_layout);
|
||||
headerUploaderName = headerRootLayout.findViewById(R.id.uploader_name);
|
||||
headerUploaderAvatar = headerRootLayout.findViewById(R.id.uploader_avatar_view);
|
||||
headerStreamCount = headerRootLayout.findViewById(R.id.playlist_stream_count);
|
||||
playlistCtrl = headerRootLayout.findViewById(R.id.playlist_control);
|
||||
|
||||
headerPlayAllButton = headerRootLayout.findViewById(R.id.playlist_ctrl_play_all_button);
|
||||
headerPopupButton = headerRootLayout.findViewById(R.id.playlist_ctrl_play_popup_button);
|
||||
headerBackgroundButton = headerRootLayout.findViewById(R.id.playlist_ctrl_play_bg_button);
|
||||
|
||||
|
||||
return headerRootLayout;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void initViews(View rootView, Bundle savedInstanceState) {
|
||||
super.initViews(rootView, savedInstanceState);
|
||||
|
||||
infoListAdapter.useMiniItemVariants(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void showStreamDialog(final StreamInfoItem item) {
|
||||
final Context context = getContext();
|
||||
final Activity activity = getActivity();
|
||||
if (context == null || context.getResources() == null || getActivity() == null) return;
|
||||
|
||||
final String[] commands = new String[]{
|
||||
context.getResources().getString(R.string.enqueue_on_background),
|
||||
context.getResources().getString(R.string.enqueue_on_popup),
|
||||
context.getResources().getString(R.string.start_here_on_main),
|
||||
context.getResources().getString(R.string.start_here_on_background),
|
||||
context.getResources().getString(R.string.start_here_on_popup),
|
||||
};
|
||||
|
||||
final DialogInterface.OnClickListener actions = (dialogInterface, i) -> {
|
||||
final int index = Math.max(infoListAdapter.getItemsList().indexOf(item), 0);
|
||||
switch (i) {
|
||||
case 0:
|
||||
NavigationHelper.enqueueOnBackgroundPlayer(context, new SinglePlayQueue(item));
|
||||
break;
|
||||
case 1:
|
||||
NavigationHelper.enqueueOnPopupPlayer(activity, new SinglePlayQueue(item));
|
||||
break;
|
||||
case 2:
|
||||
NavigationHelper.playOnMainPlayer(context, getPlayQueue(index));
|
||||
break;
|
||||
case 3:
|
||||
NavigationHelper.playOnBackgroundPlayer(context, getPlayQueue(index));
|
||||
break;
|
||||
case 4:
|
||||
NavigationHelper.playOnPopupPlayer(activity, getPlayQueue(index));
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
new InfoItemDialog(getActivity(), item, commands, actions).show();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
|
||||
if (DEBUG) Log.d(TAG, "onCreateOptionsMenu() called with: menu = [" + menu +
|
||||
"], inflater = [" + inflater + "]");
|
||||
super.onCreateOptionsMenu(menu, inflater);
|
||||
inflater.inflate(R.menu.menu_playlist, menu);
|
||||
|
||||
playlistBookmarkButton = menu.findItem(R.id.menu_item_bookmark);
|
||||
updateBookmarkButtons();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyView() {
|
||||
super.onDestroyView();
|
||||
if (isBookmarkButtonReady != null) isBookmarkButtonReady.set(false);
|
||||
|
||||
if (disposables != null) disposables.clear();
|
||||
if (bookmarkReactor != null) bookmarkReactor.cancel();
|
||||
|
||||
bookmarkReactor = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
|
||||
if (disposables != null) disposables.dispose();
|
||||
|
||||
disposables = null;
|
||||
remotePlaylistManager = null;
|
||||
playlistEntity = null;
|
||||
isBookmarkButtonReady = null;
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Load and handle
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
protected Single<ListExtractor.NextItemsResult> loadMoreItemsLogic() {
|
||||
return ExtractorHelper.getMorePlaylistItems(serviceId, url, currentNextItemsUrl);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Single<PlaylistInfo> loadResult(boolean forceLoad) {
|
||||
return ExtractorHelper.getPlaylistInfo(serviceId, url, forceLoad);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case R.id.menu_item_openInBrowser:
|
||||
openUrlInBrowser(url);
|
||||
break;
|
||||
case R.id.menu_item_share:
|
||||
shareUrl(name, url);
|
||||
break;
|
||||
case R.id.menu_item_bookmark:
|
||||
onBookmarkClicked();
|
||||
break;
|
||||
default:
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Contract
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public void showLoading() {
|
||||
super.showLoading();
|
||||
animateView(headerRootLayout, false, 200);
|
||||
animateView(itemsList, false, 100);
|
||||
|
||||
imageLoader.cancelDisplayTask(headerUploaderAvatar);
|
||||
animateView(headerUploaderLayout, false, 200);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleResult(@NonNull final PlaylistInfo result) {
|
||||
super.handleResult(result);
|
||||
|
||||
animateView(headerRootLayout, true, 100);
|
||||
animateView(headerUploaderLayout, true, 300);
|
||||
headerUploaderLayout.setOnClickListener(null);
|
||||
if (!TextUtils.isEmpty(result.getUploaderName())) {
|
||||
headerUploaderName.setText(result.getUploaderName());
|
||||
if (!TextUtils.isEmpty(result.getUploaderUrl())) {
|
||||
headerUploaderLayout.setOnClickListener(v ->
|
||||
NavigationHelper.openChannelFragment(getFragmentManager(),
|
||||
result.getServiceId(), result.getUploaderUrl(),
|
||||
result.getUploaderName())
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
playlistCtrl.setVisibility(View.VISIBLE);
|
||||
|
||||
imageLoader.displayImage(result.getUploaderAvatarUrl(), headerUploaderAvatar, DISPLAY_AVATAR_OPTIONS);
|
||||
headerStreamCount.setText(getResources().getQuantityString(R.plurals.videos, (int) result.stream_count, (int) result.stream_count));
|
||||
|
||||
if (!result.getErrors().isEmpty()) {
|
||||
showSnackBarError(result.getErrors(), UserAction.REQUESTED_PLAYLIST, NewPipe.getNameOfService(result.getServiceId()), result.getUrl(), 0);
|
||||
}
|
||||
|
||||
remotePlaylistManager.getPlaylist(result)
|
||||
.onBackpressureLatest()
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(getPlaylistBookmarkSubscriber());
|
||||
|
||||
remotePlaylistManager.onUpdate(result)
|
||||
.subscribeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(integer -> {/* Do nothing*/}, this::onError);
|
||||
|
||||
headerPlayAllButton.setOnClickListener(view ->
|
||||
NavigationHelper.playOnMainPlayer(activity, getPlayQueue()));
|
||||
headerPopupButton.setOnClickListener(view ->
|
||||
NavigationHelper.playOnPopupPlayer(activity, getPlayQueue()));
|
||||
headerBackgroundButton.setOnClickListener(view ->
|
||||
NavigationHelper.playOnBackgroundPlayer(activity, getPlayQueue()));
|
||||
}
|
||||
|
||||
private PlayQueue getPlayQueue() {
|
||||
return getPlayQueue(0);
|
||||
}
|
||||
|
||||
private PlayQueue getPlayQueue(final int index) {
|
||||
return new PlaylistPlayQueue(
|
||||
currentInfo.getServiceId(),
|
||||
currentInfo.getUrl(),
|
||||
currentInfo.getNextStreamsUrl(),
|
||||
infoListAdapter.getItemsList(),
|
||||
index
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleNextItems(ListExtractor.NextItemsResult result) {
|
||||
super.handleNextItems(result);
|
||||
|
||||
if (!result.getErrors().isEmpty()) {
|
||||
showSnackBarError(result.getErrors(), UserAction.REQUESTED_PLAYLIST, NewPipe.getNameOfService(serviceId)
|
||||
, "Get next page of: " + url, 0);
|
||||
}
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// OnError
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
protected boolean onError(Throwable exception) {
|
||||
if (super.onError(exception)) return true;
|
||||
|
||||
int errorId = exception instanceof ExtractionException ? R.string.parsing_error : R.string.general_error;
|
||||
onUnrecoverableError(exception, UserAction.REQUESTED_PLAYLIST, NewPipe.getNameOfService(serviceId), url, errorId);
|
||||
return true;
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Utils
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
private Subscriber<List<PlaylistRemoteEntity>> getPlaylistBookmarkSubscriber() {
|
||||
return new Subscriber<List<PlaylistRemoteEntity>>() {
|
||||
@Override
|
||||
public void onSubscribe(Subscription s) {
|
||||
if (bookmarkReactor != null) bookmarkReactor.cancel();
|
||||
bookmarkReactor = s;
|
||||
bookmarkReactor.request(1);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNext(List<PlaylistRemoteEntity> playlist) {
|
||||
playlistEntity = playlist.isEmpty() ? null : playlist.get(0);
|
||||
|
||||
updateBookmarkButtons();
|
||||
isBookmarkButtonReady.set(true);
|
||||
|
||||
if (bookmarkReactor != null) bookmarkReactor.request(1);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(Throwable t) {
|
||||
PlaylistFragment.this.onError(t);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onComplete() {
|
||||
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setTitle(String title) {
|
||||
super.setTitle(title);
|
||||
headerTitleView.setText(title);
|
||||
}
|
||||
|
||||
private void onBookmarkClicked() {
|
||||
if (isBookmarkButtonReady == null || !isBookmarkButtonReady.get() ||
|
||||
remotePlaylistManager == null)
|
||||
return;
|
||||
|
||||
final Disposable action;
|
||||
|
||||
if (currentInfo != null && playlistEntity == null) {
|
||||
action = remotePlaylistManager.onBookmark(currentInfo)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(ignored -> {/* Do nothing */}, this::onError);
|
||||
} else if (playlistEntity != null) {
|
||||
action = remotePlaylistManager.deletePlaylist(playlistEntity.getUid())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.doFinally(() -> playlistEntity = null)
|
||||
.subscribe(ignored -> {/* Do nothing */}, this::onError);
|
||||
} else {
|
||||
action = Disposables.empty();
|
||||
}
|
||||
|
||||
disposables.add(action);
|
||||
}
|
||||
|
||||
private void updateBookmarkButtons() {
|
||||
if (playlistBookmarkButton == null || activity == null) return;
|
||||
|
||||
final int iconAttr = playlistEntity == null ?
|
||||
R.attr.ic_playlist_add : R.attr.ic_playlist_check;
|
||||
|
||||
final int titleRes = playlistEntity == null ?
|
||||
R.string.bookmark_playlist : R.string.unbookmark_playlist;
|
||||
|
||||
playlistBookmarkButton.setIcon(ThemeHelper.resolveResourceIdFromAttr(activity, iconAttr));
|
||||
playlistBookmarkButton.setTitle(titleRes);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,870 @@
|
||||
package org.schabi.newpipe.fragments.list.search;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.Bundle;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v7.app.ActionBar;
|
||||
import android.support.v7.app.AlertDialog;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.support.v7.widget.TooltipCompat;
|
||||
import android.text.Editable;
|
||||
import android.text.TextUtils;
|
||||
import android.text.TextWatcher;
|
||||
import android.util.Log;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.animation.DecelerateInterpolator;
|
||||
import android.view.inputmethod.EditorInfo;
|
||||
import android.view.inputmethod.InputMethodManager;
|
||||
import android.widget.EditText;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.ReCaptchaActivity;
|
||||
import org.schabi.newpipe.database.history.model.SearchHistoryEntry;
|
||||
import org.schabi.newpipe.extractor.InfoItem;
|
||||
import org.schabi.newpipe.extractor.ListExtractor;
|
||||
import org.schabi.newpipe.extractor.NewPipe;
|
||||
import org.schabi.newpipe.extractor.StreamingService;
|
||||
import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
||||
import org.schabi.newpipe.extractor.search.SearchEngine;
|
||||
import org.schabi.newpipe.extractor.search.SearchResult;
|
||||
import org.schabi.newpipe.fragments.BackPressable;
|
||||
import org.schabi.newpipe.fragments.list.BaseListFragment;
|
||||
import org.schabi.newpipe.history.HistoryRecordManager;
|
||||
import org.schabi.newpipe.report.UserAction;
|
||||
import org.schabi.newpipe.util.Constants;
|
||||
import org.schabi.newpipe.util.AnimationUtils;
|
||||
import org.schabi.newpipe.util.ExtractorHelper;
|
||||
import org.schabi.newpipe.util.LayoutManagerSmoothScroller;
|
||||
import org.schabi.newpipe.util.NavigationHelper;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InterruptedIOException;
|
||||
import java.net.SocketException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Queue;
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import icepick.State;
|
||||
import io.reactivex.Flowable;
|
||||
import io.reactivex.Observable;
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.disposables.CompositeDisposable;
|
||||
import io.reactivex.disposables.Disposable;
|
||||
import io.reactivex.functions.Consumer;
|
||||
import io.reactivex.schedulers.Schedulers;
|
||||
import io.reactivex.subjects.PublishSubject;
|
||||
|
||||
import static org.schabi.newpipe.util.AnimationUtils.animateView;
|
||||
|
||||
public class SearchFragment extends BaseListFragment<SearchResult, ListExtractor.NextItemsResult> implements BackPressable {
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Search
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
/**
|
||||
* The suggestions will only be fetched from network if the query meet this threshold (>=).
|
||||
* (local ones will be fetched regardless of the length)
|
||||
*/
|
||||
private static final int THRESHOLD_NETWORK_SUGGESTION = 1;
|
||||
|
||||
/**
|
||||
* How much time have to pass without emitting a item (i.e. the user stop typing) to fetch/show the suggestions, in milliseconds.
|
||||
*/
|
||||
private static final int SUGGESTIONS_DEBOUNCE = 120; //ms
|
||||
|
||||
@State
|
||||
protected int filterItemCheckedId = -1;
|
||||
private SearchEngine.Filter filter = SearchEngine.Filter.ANY;
|
||||
|
||||
@State
|
||||
protected int serviceId = Constants.NO_SERVICE_ID;
|
||||
@State
|
||||
protected String searchQuery;
|
||||
@State
|
||||
protected String lastSearchedQuery;
|
||||
@State
|
||||
protected boolean wasSearchFocused = false;
|
||||
|
||||
private int currentPage = 0;
|
||||
private int currentNextPage = 0;
|
||||
private String contentCountry;
|
||||
private boolean isSuggestionsEnabled = true;
|
||||
private boolean isSearchHistoryEnabled = true;
|
||||
|
||||
private PublishSubject<String> suggestionPublisher = PublishSubject.create();
|
||||
private Disposable searchDisposable;
|
||||
private Disposable suggestionDisposable;
|
||||
private CompositeDisposable disposables = new CompositeDisposable();
|
||||
|
||||
private SuggestionListAdapter suggestionListAdapter;
|
||||
private HistoryRecordManager historyRecordManager;
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Views
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
private View searchToolbarContainer;
|
||||
private EditText searchEditText;
|
||||
private View searchClear;
|
||||
|
||||
private View suggestionsPanel;
|
||||
private RecyclerView suggestionsRecyclerView;
|
||||
|
||||
/*////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
public static SearchFragment getInstance(int serviceId, String query) {
|
||||
SearchFragment searchFragment = new SearchFragment();
|
||||
searchFragment.setQuery(serviceId, query);
|
||||
|
||||
if (!TextUtils.isEmpty(query)) {
|
||||
searchFragment.setSearchOnResume();
|
||||
}
|
||||
|
||||
return searchFragment;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set wasLoading to true so when the fragment onResume is called, the initial search is done.
|
||||
*/
|
||||
private void setSearchOnResume() {
|
||||
wasLoading.set(true);
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Fragment's LifeCycle
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public void onAttach(Context context) {
|
||||
super.onAttach(context);
|
||||
|
||||
suggestionListAdapter = new SuggestionListAdapter(activity);
|
||||
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(activity);
|
||||
isSearchHistoryEnabled = preferences.getBoolean(getString(R.string.enable_search_history_key), true);
|
||||
suggestionListAdapter.setShowSuggestionHistory(isSearchHistoryEnabled);
|
||||
|
||||
historyRecordManager = new HistoryRecordManager(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(activity);
|
||||
isSuggestionsEnabled = preferences.getBoolean(getString(R.string.show_search_suggestions_key), true);
|
||||
contentCountry = preferences.getString(getString(R.string.content_country_key), getString(R.string.default_country_value));
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||||
return inflater.inflate(R.layout.fragment_search, container, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(View rootView, Bundle savedInstanceState) {
|
||||
super.onViewCreated(rootView, savedInstanceState);
|
||||
showSearchOnStart();
|
||||
initSearchListeners();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPause() {
|
||||
super.onPause();
|
||||
|
||||
wasSearchFocused = searchEditText.hasFocus();
|
||||
|
||||
if (searchDisposable != null) searchDisposable.dispose();
|
||||
if (suggestionDisposable != null) suggestionDisposable.dispose();
|
||||
if (disposables != null) disposables.clear();
|
||||
hideKeyboardSearch();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
if (DEBUG) Log.d(TAG, "onResume() called");
|
||||
super.onResume();
|
||||
|
||||
if (!TextUtils.isEmpty(searchQuery)) {
|
||||
if (wasLoading.getAndSet(false)) {
|
||||
if (currentNextPage > currentPage) loadMoreItems();
|
||||
else search(searchQuery);
|
||||
} else if (infoListAdapter.getItemsList().size() == 0) {
|
||||
if (savedState == null) {
|
||||
search(searchQuery);
|
||||
} else if (!isLoading.get() && !wasSearchFocused) {
|
||||
infoListAdapter.clearStreamItemList();
|
||||
showEmptyState();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (suggestionDisposable == null || suggestionDisposable.isDisposed()) initSuggestionObserver();
|
||||
|
||||
if (TextUtils.isEmpty(searchQuery) || wasSearchFocused) {
|
||||
showKeyboardSearch();
|
||||
showSuggestionsPanel();
|
||||
} else {
|
||||
hideKeyboardSearch();
|
||||
hideSuggestionsPanel();
|
||||
}
|
||||
wasSearchFocused = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyView() {
|
||||
if (DEBUG) Log.d(TAG, "onDestroyView() called");
|
||||
unsetSearchListeners();
|
||||
super.onDestroyView();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
if (searchDisposable != null) searchDisposable.dispose();
|
||||
if (suggestionDisposable != null) suggestionDisposable.dispose();
|
||||
if (disposables != null) disposables.clear();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
switch (requestCode) {
|
||||
case ReCaptchaActivity.RECAPTCHA_REQUEST:
|
||||
if (resultCode == Activity.RESULT_OK && !TextUtils.isEmpty(searchQuery)) {
|
||||
search(searchQuery);
|
||||
} else Log.e(TAG, "ReCaptcha failed");
|
||||
break;
|
||||
|
||||
default:
|
||||
Log.e(TAG, "Request code from activity not supported [" + requestCode + "]");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Init
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
protected void initViews(View rootView, Bundle savedInstanceState) {
|
||||
super.initViews(rootView, savedInstanceState);
|
||||
suggestionsPanel = rootView.findViewById(R.id.suggestions_panel);
|
||||
suggestionsRecyclerView = rootView.findViewById(R.id.suggestions_list);
|
||||
suggestionsRecyclerView.setAdapter(suggestionListAdapter);
|
||||
suggestionsRecyclerView.setLayoutManager(new LayoutManagerSmoothScroller(activity));
|
||||
|
||||
searchToolbarContainer = activity.findViewById(R.id.toolbar_search_container);
|
||||
searchEditText = searchToolbarContainer.findViewById(R.id.toolbar_search_edit_text);
|
||||
searchClear = searchToolbarContainer.findViewById(R.id.toolbar_search_clear);
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// State Saving
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public void writeTo(Queue<Object> objectsToSave) {
|
||||
super.writeTo(objectsToSave);
|
||||
objectsToSave.add(currentPage);
|
||||
objectsToSave.add(currentNextPage);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void readFrom(@NonNull Queue<Object> savedObjects) throws Exception {
|
||||
super.readFrom(savedObjects);
|
||||
currentPage = (int) savedObjects.poll();
|
||||
currentNextPage = (int) savedObjects.poll();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSaveInstanceState(Bundle bundle) {
|
||||
searchQuery = searchEditText != null ? searchEditText.getText().toString() : searchQuery;
|
||||
super.onSaveInstanceState(bundle);
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Init's
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public void reloadContent() {
|
||||
if (!TextUtils.isEmpty(searchQuery) || (searchEditText != null && !TextUtils.isEmpty(searchEditText.getText()))) {
|
||||
search(!TextUtils.isEmpty(searchQuery) ? searchQuery : searchEditText.getText().toString());
|
||||
} else {
|
||||
if (searchEditText != null) {
|
||||
searchEditText.setText("");
|
||||
showKeyboardSearch();
|
||||
}
|
||||
animateView(errorPanelRoot, false, 200);
|
||||
}
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Menu
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
|
||||
super.onCreateOptionsMenu(menu, inflater);
|
||||
|
||||
ActionBar supportActionBar = activity.getSupportActionBar();
|
||||
if (supportActionBar != null) {
|
||||
supportActionBar.setDisplayShowTitleEnabled(false);
|
||||
supportActionBar.setDisplayHomeAsUpEnabled(true);
|
||||
}
|
||||
|
||||
inflater.inflate(R.menu.menu_search, menu);
|
||||
restoreFilterChecked(menu, filterItemCheckedId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case R.id.menu_filter_all:
|
||||
case R.id.menu_filter_video:
|
||||
case R.id.menu_filter_channel:
|
||||
case R.id.menu_filter_playlist:
|
||||
changeFilter(item, getFilterFromMenuId(item.getItemId()));
|
||||
return true;
|
||||
default:
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
}
|
||||
|
||||
private void restoreFilterChecked(Menu menu, int itemId) {
|
||||
if (itemId != -1) {
|
||||
MenuItem item = menu.findItem(itemId);
|
||||
if (item == null) return;
|
||||
|
||||
item.setChecked(true);
|
||||
filter = getFilterFromMenuId(itemId);
|
||||
}
|
||||
}
|
||||
|
||||
private SearchEngine.Filter getFilterFromMenuId(int itemId) {
|
||||
switch (itemId) {
|
||||
case R.id.menu_filter_video:
|
||||
return SearchEngine.Filter.STREAM;
|
||||
case R.id.menu_filter_channel:
|
||||
return SearchEngine.Filter.CHANNEL;
|
||||
case R.id.menu_filter_playlist:
|
||||
return SearchEngine.Filter.PLAYLIST;
|
||||
case R.id.menu_filter_all:
|
||||
default:
|
||||
return SearchEngine.Filter.ANY;
|
||||
}
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Search
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
private TextWatcher textWatcher;
|
||||
|
||||
private void showSearchOnStart() {
|
||||
if (DEBUG) Log.d(TAG, "showSearchOnStart() called, searchQuery → " + searchQuery+", lastSearchedQuery → " + lastSearchedQuery);
|
||||
searchEditText.setText(searchQuery);
|
||||
|
||||
if (TextUtils.isEmpty(searchQuery) || TextUtils.isEmpty(searchEditText.getText())) {
|
||||
searchToolbarContainer.setTranslationX(100);
|
||||
searchToolbarContainer.setAlpha(0f);
|
||||
searchToolbarContainer.setVisibility(View.VISIBLE);
|
||||
searchToolbarContainer.animate().translationX(0).alpha(1f).setDuration(200).setInterpolator(new DecelerateInterpolator()).start();
|
||||
} else {
|
||||
searchToolbarContainer.setTranslationX(0);
|
||||
searchToolbarContainer.setAlpha(1f);
|
||||
searchToolbarContainer.setVisibility(View.VISIBLE);
|
||||
}
|
||||
}
|
||||
|
||||
private void initSearchListeners() {
|
||||
if (DEBUG) Log.d(TAG, "initSearchListeners() called");
|
||||
searchClear.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
if (DEBUG) Log.d(TAG, "onClick() called with: v = [" + v + "]");
|
||||
if (TextUtils.isEmpty(searchEditText.getText())) {
|
||||
NavigationHelper.gotoMainFragment(getFragmentManager());
|
||||
return;
|
||||
}
|
||||
|
||||
searchEditText.setText("");
|
||||
suggestionListAdapter.setItems(new ArrayList<SuggestionItem>());
|
||||
showKeyboardSearch();
|
||||
}
|
||||
});
|
||||
|
||||
TooltipCompat.setTooltipText(searchClear, getString(R.string.clear));
|
||||
|
||||
searchEditText.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
if (DEBUG) Log.d(TAG, "onClick() called with: v = [" + v + "]");
|
||||
if (isSuggestionsEnabled && errorPanelRoot.getVisibility() != View.VISIBLE) {
|
||||
showSuggestionsPanel();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
searchEditText.setOnFocusChangeListener(new View.OnFocusChangeListener() {
|
||||
@Override
|
||||
public void onFocusChange(View v, boolean hasFocus) {
|
||||
if (DEBUG) Log.d(TAG, "onFocusChange() called with: v = [" + v + "], hasFocus = [" + hasFocus + "]");
|
||||
if (isSuggestionsEnabled && hasFocus && errorPanelRoot.getVisibility() != View.VISIBLE) {
|
||||
showSuggestionsPanel();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
suggestionListAdapter.setListener(new SuggestionListAdapter.OnSuggestionItemSelected() {
|
||||
@Override
|
||||
public void onSuggestionItemSelected(SuggestionItem item) {
|
||||
search(item.query);
|
||||
searchEditText.setText(item.query);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSuggestionItemInserted(SuggestionItem item) {
|
||||
searchEditText.setText(item.query);
|
||||
searchEditText.setSelection(searchEditText.getText().length());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSuggestionItemLongClick(SuggestionItem item) {
|
||||
if (item.fromHistory) showDeleteSuggestionDialog(item);
|
||||
}
|
||||
});
|
||||
|
||||
if (textWatcher != null) searchEditText.removeTextChangedListener(textWatcher);
|
||||
textWatcher = new TextWatcher() {
|
||||
@Override
|
||||
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTextChanged(CharSequence s, int start, int before, int count) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterTextChanged(Editable s) {
|
||||
String newText = searchEditText.getText().toString();
|
||||
suggestionPublisher.onNext(newText);
|
||||
}
|
||||
};
|
||||
searchEditText.addTextChangedListener(textWatcher);
|
||||
searchEditText.setOnEditorActionListener(new TextView.OnEditorActionListener() {
|
||||
@Override
|
||||
public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "onEditorAction() called with: v = [" + v + "], actionId = [" + actionId + "], event = [" + event + "]");
|
||||
}
|
||||
if (event != null && (event.getKeyCode() == KeyEvent.KEYCODE_ENTER || event.getAction() == EditorInfo.IME_ACTION_SEARCH)) {
|
||||
search(searchEditText.getText().toString());
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
if (suggestionDisposable == null || suggestionDisposable.isDisposed()) initSuggestionObserver();
|
||||
}
|
||||
|
||||
private void unsetSearchListeners() {
|
||||
if (DEBUG) Log.d(TAG, "unsetSearchListeners() called");
|
||||
searchClear.setOnClickListener(null);
|
||||
searchClear.setOnLongClickListener(null);
|
||||
searchEditText.setOnClickListener(null);
|
||||
searchEditText.setOnFocusChangeListener(null);
|
||||
searchEditText.setOnEditorActionListener(null);
|
||||
|
||||
if (textWatcher != null) searchEditText.removeTextChangedListener(textWatcher);
|
||||
textWatcher = null;
|
||||
}
|
||||
|
||||
private void showSuggestionsPanel() {
|
||||
if (DEBUG) Log.d(TAG, "showSuggestionsPanel() called");
|
||||
animateView(suggestionsPanel, AnimationUtils.Type.LIGHT_SLIDE_AND_ALPHA, true, 200);
|
||||
}
|
||||
|
||||
private void hideSuggestionsPanel() {
|
||||
if (DEBUG) Log.d(TAG, "hideSuggestionsPanel() called");
|
||||
animateView(suggestionsPanel, AnimationUtils.Type.LIGHT_SLIDE_AND_ALPHA, false, 200);
|
||||
}
|
||||
|
||||
private void showKeyboardSearch() {
|
||||
if (DEBUG) Log.d(TAG, "showKeyboardSearch() called");
|
||||
if (searchEditText == null) return;
|
||||
|
||||
if (searchEditText.requestFocus()) {
|
||||
InputMethodManager imm = (InputMethodManager) activity.getSystemService(Context.INPUT_METHOD_SERVICE);
|
||||
imm.showSoftInput(searchEditText, InputMethodManager.SHOW_IMPLICIT);
|
||||
}
|
||||
}
|
||||
|
||||
private void hideKeyboardSearch() {
|
||||
if (DEBUG) Log.d(TAG, "hideKeyboardSearch() called");
|
||||
if (searchEditText == null) return;
|
||||
|
||||
InputMethodManager imm = (InputMethodManager) activity.getSystemService(Context.INPUT_METHOD_SERVICE);
|
||||
imm.hideSoftInputFromWindow(searchEditText.getWindowToken(), InputMethodManager.HIDE_NOT_ALWAYS);
|
||||
|
||||
searchEditText.clearFocus();
|
||||
}
|
||||
|
||||
private void showDeleteSuggestionDialog(final SuggestionItem item) {
|
||||
final Disposable onDelete = historyRecordManager.deleteSearchHistory(item.query)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
howManyDeleted -> suggestionPublisher
|
||||
.onNext(searchEditText.getText().toString()),
|
||||
|
||||
throwable -> showSnackBarError(throwable,
|
||||
UserAction.SOMETHING_ELSE, "none",
|
||||
"Deleting item failed", R.string.general_error)
|
||||
);
|
||||
|
||||
new AlertDialog.Builder(activity)
|
||||
.setTitle(item.query)
|
||||
.setMessage(R.string.delete_item_search_history)
|
||||
.setCancelable(true)
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.setPositiveButton(R.string.delete, (dialog, which) -> disposables.add(onDelete))
|
||||
.show();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onBackPressed() {
|
||||
if (suggestionsPanel.getVisibility() == View.VISIBLE && infoListAdapter.getItemsList().size() > 0 && !isLoading.get()) {
|
||||
hideSuggestionsPanel();
|
||||
hideKeyboardSearch();
|
||||
searchEditText.setText(lastSearchedQuery);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public void giveSearchEditTextFocus() {
|
||||
showKeyboardSearch();
|
||||
}
|
||||
|
||||
private void initSuggestionObserver() {
|
||||
if (DEBUG) Log.d(TAG, "initSuggestionObserver() called");
|
||||
if (suggestionDisposable != null) suggestionDisposable.dispose();
|
||||
|
||||
final Observable<String> observable = suggestionPublisher
|
||||
.debounce(SUGGESTIONS_DEBOUNCE, TimeUnit.MILLISECONDS)
|
||||
.startWith(searchQuery != null ? searchQuery : "")
|
||||
.filter(query -> isSuggestionsEnabled);
|
||||
|
||||
suggestionDisposable = observable
|
||||
.switchMap(query -> {
|
||||
final Flowable<List<SearchHistoryEntry>> flowable = historyRecordManager
|
||||
.getRelatedSearches(query, 3, 25);
|
||||
final Observable<List<SuggestionItem>> local = flowable.toObservable()
|
||||
.map(searchHistoryEntries -> {
|
||||
List<SuggestionItem> result = new ArrayList<>();
|
||||
for (SearchHistoryEntry entry : searchHistoryEntries)
|
||||
result.add(new SuggestionItem(true, entry.getSearch()));
|
||||
return result;
|
||||
});
|
||||
|
||||
if (query.length() < THRESHOLD_NETWORK_SUGGESTION) {
|
||||
// Only pass through if the query length is equal or greater than THRESHOLD_NETWORK_SUGGESTION
|
||||
return local.materialize();
|
||||
}
|
||||
|
||||
final Observable<List<SuggestionItem>> network = ExtractorHelper
|
||||
.suggestionsFor(serviceId, query, contentCountry)
|
||||
.toObservable()
|
||||
.map(strings -> {
|
||||
List<SuggestionItem> result = new ArrayList<>();
|
||||
for (String entry : strings) {
|
||||
result.add(new SuggestionItem(false, entry));
|
||||
}
|
||||
return result;
|
||||
});
|
||||
|
||||
return Observable.zip(local, network, (localResult, networkResult) -> {
|
||||
List<SuggestionItem> result = new ArrayList<>();
|
||||
if (localResult.size() > 0) result.addAll(localResult);
|
||||
|
||||
// Remove duplicates
|
||||
final Iterator<SuggestionItem> iterator = networkResult.iterator();
|
||||
while (iterator.hasNext() && localResult.size() > 0) {
|
||||
final SuggestionItem next = iterator.next();
|
||||
for (SuggestionItem item : localResult) {
|
||||
if (item.query.equals(next.query)) {
|
||||
iterator.remove();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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()) {
|
||||
Throwable error = listNotification.getError();
|
||||
if (!ExtractorHelper.hasAssignableCauseThrowable(error,
|
||||
IOException.class, SocketException.class,
|
||||
InterruptedException.class, InterruptedIOException.class)) {
|
||||
onSuggestionError(error);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doInitialLoadLogic() {
|
||||
// no-op
|
||||
}
|
||||
|
||||
private void search(final String query) {
|
||||
if (DEBUG) Log.d(TAG, "search() called with: query = [" + query + "]");
|
||||
if (query.isEmpty()) return;
|
||||
|
||||
try {
|
||||
final StreamingService service = NewPipe.getServiceByUrl(query);
|
||||
if (service != null) {
|
||||
showLoading();
|
||||
disposables.add(Observable
|
||||
.fromCallable(new Callable<Intent>() {
|
||||
@Override
|
||||
public Intent call() throws Exception {
|
||||
return NavigationHelper.getIntentByLink(activity, service, query);
|
||||
}
|
||||
})
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(new Consumer<Intent>() {
|
||||
@Override
|
||||
public void accept(Intent intent) throws Exception {
|
||||
getFragmentManager().popBackStackImmediate();
|
||||
activity.startActivity(intent);
|
||||
}
|
||||
}, new Consumer<Throwable>() {
|
||||
@Override
|
||||
public void accept(Throwable throwable) throws Exception {
|
||||
showError(getString(R.string.url_not_supported_toast), false);
|
||||
}
|
||||
}));
|
||||
return;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// Exception occurred, it's not a url
|
||||
}
|
||||
|
||||
lastSearchedQuery = query;
|
||||
searchQuery = query;
|
||||
currentPage = 0;
|
||||
infoListAdapter.clearStreamItemList();
|
||||
hideSuggestionsPanel();
|
||||
hideKeyboardSearch();
|
||||
|
||||
historyRecordManager.onSearched(serviceId, query)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
ignored -> {},
|
||||
error -> showSnackBarError(error, UserAction.SEARCHED,
|
||||
NewPipe.getNameOfService(serviceId), query, 0)
|
||||
);
|
||||
suggestionPublisher.onNext(query);
|
||||
startLoading(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void startLoading(boolean forceLoad) {
|
||||
super.startLoading(forceLoad);
|
||||
if (disposables != null) disposables.clear();
|
||||
if (searchDisposable != null) searchDisposable.dispose();
|
||||
searchDisposable = ExtractorHelper.searchFor(serviceId, searchQuery, currentPage, contentCountry, filter)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(new Consumer<SearchResult>() {
|
||||
@Override
|
||||
public void accept(@NonNull SearchResult result) throws Exception {
|
||||
isLoading.set(false);
|
||||
handleResult(result);
|
||||
}
|
||||
}, new Consumer<Throwable>() {
|
||||
@Override
|
||||
public void accept(@NonNull Throwable throwable) throws Exception {
|
||||
isLoading.set(false);
|
||||
onError(throwable);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void loadMoreItems() {
|
||||
isLoading.set(true);
|
||||
showListFooter(true);
|
||||
if (searchDisposable != null) searchDisposable.dispose();
|
||||
currentNextPage = currentPage + 1;
|
||||
searchDisposable = ExtractorHelper.getMoreSearchItems(serviceId, searchQuery, currentNextPage, contentCountry, filter)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(new Consumer<ListExtractor.NextItemsResult>() {
|
||||
@Override
|
||||
public void accept(@NonNull ListExtractor.NextItemsResult result) throws Exception {
|
||||
isLoading.set(false);
|
||||
handleNextItems(result);
|
||||
}
|
||||
}, new Consumer<Throwable>() {
|
||||
@Override
|
||||
public void accept(@NonNull Throwable throwable) throws Exception {
|
||||
isLoading.set(false);
|
||||
onError(throwable);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean hasMoreItems() {
|
||||
// TODO: No way to tell if search has more items in the moment
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onItemSelected(InfoItem selectedItem) {
|
||||
super.onItemSelected(selectedItem);
|
||||
hideKeyboardSearch();
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Utils
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
private void changeFilter(MenuItem item, SearchEngine.Filter filter) {
|
||||
this.filter = filter;
|
||||
this.filterItemCheckedId = item.getItemId();
|
||||
item.setChecked(true);
|
||||
|
||||
if (!TextUtils.isEmpty(searchQuery)) {
|
||||
search(searchQuery);
|
||||
}
|
||||
}
|
||||
|
||||
private void setQuery(int serviceId, String searchQuery) {
|
||||
this.serviceId = serviceId;
|
||||
this.searchQuery = searchQuery;
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Suggestion Results
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
public void handleSuggestions(@NonNull final List<SuggestionItem> suggestions) {
|
||||
if (DEBUG) Log.d(TAG, "handleSuggestions() called with: suggestions = [" + suggestions + "]");
|
||||
suggestionsRecyclerView.smoothScrollToPosition(0);
|
||||
suggestionsRecyclerView.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
suggestionListAdapter.setItems(suggestions);
|
||||
}
|
||||
});
|
||||
|
||||
if (errorPanelRoot.getVisibility() == View.VISIBLE) {
|
||||
hideLoading();
|
||||
}
|
||||
}
|
||||
|
||||
public void onSuggestionError(Throwable exception) {
|
||||
if (DEBUG) Log.d(TAG, "onSuggestionError() called with: exception = [" + exception + "]");
|
||||
if (super.onError(exception)) return;
|
||||
|
||||
int errorId = exception instanceof ParsingException ? R.string.parsing_error : R.string.general_error;
|
||||
onUnrecoverableError(exception, UserAction.GET_SUGGESTIONS, NewPipe.getNameOfService(serviceId), searchQuery, errorId);
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Contract
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public void hideLoading() {
|
||||
super.hideLoading();
|
||||
showListFooter(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void showError(String message, boolean showRetryButton) {
|
||||
super.showError(message, showRetryButton);
|
||||
hideSuggestionsPanel();
|
||||
hideKeyboardSearch();
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Search Results
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public void handleResult(@NonNull SearchResult result) {
|
||||
if (!result.errors.isEmpty()) {
|
||||
showSnackBarError(result.errors, UserAction.SEARCHED, NewPipe.getNameOfService(serviceId), searchQuery, 0);
|
||||
}
|
||||
|
||||
lastSearchedQuery = searchQuery;
|
||||
|
||||
if (infoListAdapter.getItemsList().size() == 0) {
|
||||
if (!result.getResults().isEmpty()) {
|
||||
infoListAdapter.addInfoItemList(result.getResults());
|
||||
} else {
|
||||
infoListAdapter.clearStreamItemList();
|
||||
showEmptyState();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
super.handleResult(result);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleNextItems(ListExtractor.NextItemsResult result) {
|
||||
showListFooter(false);
|
||||
currentPage = Integer.parseInt(result.getNextItemsUrl());
|
||||
infoListAdapter.addInfoItemList(result.getNextItemsList());
|
||||
|
||||
if (!result.getErrors().isEmpty()) {
|
||||
showSnackBarError(result.getErrors(), UserAction.SEARCHED, NewPipe.getNameOfService(serviceId)
|
||||
, "\"" + searchQuery + "\" → page " + currentPage, 0);
|
||||
}
|
||||
super.handleNextItems(result);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean onError(Throwable exception) {
|
||||
if (super.onError(exception)) return true;
|
||||
|
||||
if (exception instanceof SearchEngine.NothingFoundException) {
|
||||
infoListAdapter.clearStreamItemList();
|
||||
showEmptyState();
|
||||
} else {
|
||||
int errorId = exception instanceof ParsingException ? R.string.parsing_error : R.string.general_error;
|
||||
onUnrecoverableError(exception, UserAction.SEARCHED, NewPipe.getNameOfService(serviceId), searchQuery, errorId);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package org.schabi.newpipe.fragments.list.search;
|
||||
|
||||
public class SuggestionItem {
|
||||
public final boolean fromHistory;
|
||||
public final String query;
|
||||
|
||||
public SuggestionItem(boolean fromHistory, String query) {
|
||||
this.fromHistory = fromHistory;
|
||||
this.query = query;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "[" + fromHistory + "→" + query + "]";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
package org.schabi.newpipe.fragments.list.search;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.TypedArray;
|
||||
import android.support.annotation.AttrRes;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class SuggestionListAdapter extends RecyclerView.Adapter<SuggestionListAdapter.SuggestionItemHolder> {
|
||||
private final ArrayList<SuggestionItem> items = new ArrayList<>();
|
||||
private final Context context;
|
||||
private OnSuggestionItemSelected listener;
|
||||
private boolean showSuggestionHistory = true;
|
||||
|
||||
public interface OnSuggestionItemSelected {
|
||||
void onSuggestionItemSelected(SuggestionItem item);
|
||||
void onSuggestionItemInserted(SuggestionItem item);
|
||||
void onSuggestionItemLongClick(SuggestionItem item);
|
||||
}
|
||||
|
||||
public SuggestionListAdapter(Context context) {
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
public void setItems(List<SuggestionItem> items) {
|
||||
this.items.clear();
|
||||
if (showSuggestionHistory) {
|
||||
this.items.addAll(items);
|
||||
} else {
|
||||
// remove history items if history is disabled
|
||||
for (SuggestionItem item : items) {
|
||||
if (!item.fromHistory) {
|
||||
this.items.add(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
public void setListener(OnSuggestionItemSelected listener) {
|
||||
this.listener = listener;
|
||||
}
|
||||
|
||||
public void setShowSuggestionHistory(boolean v) {
|
||||
showSuggestionHistory = v;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SuggestionItemHolder onCreateViewHolder(ViewGroup parent, int viewType) {
|
||||
return new SuggestionItemHolder(LayoutInflater.from(context).inflate(R.layout.item_search_suggestion, parent, false));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(SuggestionItemHolder holder, int position) {
|
||||
final SuggestionItem currentItem = getItem(position);
|
||||
holder.updateFrom(currentItem);
|
||||
holder.queryView.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
if (listener != null) listener.onSuggestionItemSelected(currentItem);
|
||||
}
|
||||
});
|
||||
holder.queryView.setOnLongClickListener(new View.OnLongClickListener() {
|
||||
@Override
|
||||
public boolean onLongClick(View v) {
|
||||
if (listener != null) listener.onSuggestionItemLongClick(currentItem);
|
||||
return true;
|
||||
}
|
||||
});
|
||||
holder.insertView.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
if (listener != null) listener.onSuggestionItemInserted(currentItem);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private SuggestionItem getItem(int position) {
|
||||
return items.get(position);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return items.size();
|
||||
}
|
||||
|
||||
public boolean isEmpty() {
|
||||
return getItemCount() == 0;
|
||||
}
|
||||
|
||||
public static class SuggestionItemHolder extends RecyclerView.ViewHolder {
|
||||
private final TextView itemSuggestionQuery;
|
||||
private final ImageView suggestionIcon;
|
||||
private final View queryView;
|
||||
private final View insertView;
|
||||
|
||||
// Cache some ids, as they can potentially be constantly updated/recycled
|
||||
private final int historyResId;
|
||||
private final int searchResId;
|
||||
|
||||
private SuggestionItemHolder(View rootView) {
|
||||
super(rootView);
|
||||
suggestionIcon = rootView.findViewById(R.id.item_suggestion_icon);
|
||||
itemSuggestionQuery = rootView.findViewById(R.id.item_suggestion_query);
|
||||
|
||||
queryView = rootView.findViewById(R.id.suggestion_search);
|
||||
insertView = rootView.findViewById(R.id.suggestion_insert);
|
||||
|
||||
historyResId = resolveResourceIdFromAttr(rootView.getContext(), R.attr.history);
|
||||
searchResId = resolveResourceIdFromAttr(rootView.getContext(), R.attr.search);
|
||||
}
|
||||
|
||||
private void updateFrom(SuggestionItem item) {
|
||||
suggestionIcon.setImageResource(item.fromHistory ? historyResId : searchResId);
|
||||
itemSuggestionQuery.setText(item.query);
|
||||
}
|
||||
|
||||
private static int resolveResourceIdFromAttr(Context context, @AttrRes int attr) {
|
||||
TypedArray a = context.getTheme().obtainStyledAttributes(new int[]{attr});
|
||||
int attributeResourceId = a.getResourceId(0, 0);
|
||||
a.recycle();
|
||||
return attributeResourceId;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package org.schabi.newpipe.fragments.local;
|
||||
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.view.View;
|
||||
|
||||
public class HeaderFooterHolder extends RecyclerView.ViewHolder {
|
||||
public View view;
|
||||
|
||||
public HeaderFooterHolder(View v) {
|
||||
super(v);
|
||||
view = v;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package org.schabi.newpipe.fragments.local;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Bitmap;
|
||||
import android.widget.ImageView;
|
||||
|
||||
import com.nostra13.universalimageloader.core.DisplayImageOptions;
|
||||
import com.nostra13.universalimageloader.core.ImageLoader;
|
||||
import com.nostra13.universalimageloader.core.process.BitmapProcessor;
|
||||
|
||||
import org.schabi.newpipe.database.LocalItem;
|
||||
import org.schabi.newpipe.util.OnClickGesture;
|
||||
|
||||
/*
|
||||
* Created by Christian Schabesberger on 26.09.16.
|
||||
* <p>
|
||||
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
|
||||
* InfoItemBuilder.java is part of NewPipe.
|
||||
* <p>
|
||||
* NewPipe is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
* <p>
|
||||
* NewPipe is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
* <p>
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
public class LocalItemBuilder {
|
||||
private static final String TAG = LocalItemBuilder.class.toString();
|
||||
|
||||
private final Context context;
|
||||
private ImageLoader imageLoader = ImageLoader.getInstance();
|
||||
|
||||
private OnClickGesture<LocalItem> onSelectedListener;
|
||||
|
||||
public LocalItemBuilder(Context context) {
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
public Context getContext() {
|
||||
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;
|
||||
}
|
||||
|
||||
public void setOnItemSelectedListener(OnClickGesture<LocalItem> listener) {
|
||||
this.onSelectedListener = listener;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,247 @@
|
||||
package org.schabi.newpipe.fragments.local;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import org.schabi.newpipe.database.LocalItem;
|
||||
import org.schabi.newpipe.fragments.local.holder.LocalItemHolder;
|
||||
import org.schabi.newpipe.fragments.local.holder.LocalPlaylistItemHolder;
|
||||
import org.schabi.newpipe.fragments.local.holder.LocalPlaylistStreamItemHolder;
|
||||
import org.schabi.newpipe.fragments.local.holder.LocalStatisticStreamItemHolder;
|
||||
import org.schabi.newpipe.fragments.local.holder.RemotePlaylistItemHolder;
|
||||
import org.schabi.newpipe.util.Localization;
|
||||
import org.schabi.newpipe.util.OnClickGesture;
|
||||
|
||||
import java.text.DateFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/*
|
||||
* Created by Christian Schabesberger on 01.08.16.
|
||||
*
|
||||
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
|
||||
* InfoListAdapter.java is part of NewPipe.
|
||||
*
|
||||
* NewPipe is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* NewPipe is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
public class LocalItemListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
|
||||
|
||||
private static final String TAG = LocalItemListAdapter.class.getSimpleName();
|
||||
private static final boolean DEBUG = false;
|
||||
|
||||
private static final int HEADER_TYPE = 0;
|
||||
private static final int FOOTER_TYPE = 1;
|
||||
|
||||
private static final int STREAM_STATISTICS_HOLDER_TYPE = 0x1000;
|
||||
private static final int STREAM_PLAYLIST_HOLDER_TYPE = 0x1001;
|
||||
private static final int LOCAL_PLAYLIST_HOLDER_TYPE = 0x2000;
|
||||
private static final int REMOTE_PLAYLIST_HOLDER_TYPE = 0x2001;
|
||||
|
||||
private final LocalItemBuilder localItemBuilder;
|
||||
private final ArrayList<LocalItem> localItems;
|
||||
private final DateFormat dateFormat;
|
||||
|
||||
private boolean showFooter = false;
|
||||
private View header = null;
|
||||
private View footer = null;
|
||||
|
||||
public LocalItemListAdapter(Activity activity) {
|
||||
localItemBuilder = new LocalItemBuilder(activity);
|
||||
localItems = new ArrayList<>();
|
||||
dateFormat = DateFormat.getDateInstance(DateFormat.SHORT,
|
||||
Localization.getPreferredLocale(activity));
|
||||
}
|
||||
|
||||
public void setSelectedListener(OnClickGesture<LocalItem> listener) {
|
||||
localItemBuilder.setOnItemSelectedListener(listener);
|
||||
}
|
||||
|
||||
public void unsetSelectedListener() {
|
||||
localItemBuilder.setOnItemSelectedListener(null);
|
||||
}
|
||||
|
||||
public void addItems(List<? extends LocalItem> data) {
|
||||
if (data != null) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "addItems() before > localItems.size() = " +
|
||||
localItems.size() + ", data.size() = " + data.size());
|
||||
}
|
||||
|
||||
int offsetStart = sizeConsideringHeader();
|
||||
localItems.addAll(data);
|
||||
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "addItems() after > offsetStart = " + offsetStart +
|
||||
", localItems.size() = " + localItems.size() +
|
||||
", header = " + header + ", footer = " + footer +
|
||||
", showFooter = " + showFooter);
|
||||
}
|
||||
|
||||
notifyItemRangeInserted(offsetStart, data.size());
|
||||
|
||||
if (footer != null && showFooter) {
|
||||
int footerNow = sizeConsideringHeader();
|
||||
notifyItemMoved(offsetStart, footerNow);
|
||||
|
||||
if (DEBUG) Log.d(TAG, "addItems() footer from " + offsetStart +
|
||||
" to " + footerNow);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void removeItem(final LocalItem data) {
|
||||
final int index = localItems.indexOf(data);
|
||||
|
||||
localItems.remove(index);
|
||||
notifyItemRemoved(index + (header != null ? 1 : 0));
|
||||
}
|
||||
|
||||
public boolean swapItems(int fromAdapterPosition, int toAdapterPosition) {
|
||||
final int actualFrom = adapterOffsetWithoutHeader(fromAdapterPosition);
|
||||
final int actualTo = adapterOffsetWithoutHeader(toAdapterPosition);
|
||||
|
||||
if (actualFrom < 0 || actualTo < 0) return false;
|
||||
if (actualFrom >= localItems.size() || actualTo >= localItems.size()) return false;
|
||||
|
||||
localItems.add(actualTo, localItems.remove(actualFrom));
|
||||
notifyItemMoved(fromAdapterPosition, toAdapterPosition);
|
||||
return true;
|
||||
}
|
||||
|
||||
public void clearStreamItemList() {
|
||||
if (localItems.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
localItems.clear();
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
public void setHeader(View header) {
|
||||
boolean changed = header != this.header;
|
||||
this.header = header;
|
||||
if (changed) notifyDataSetChanged();
|
||||
}
|
||||
|
||||
public void setFooter(View view) {
|
||||
this.footer = view;
|
||||
}
|
||||
|
||||
public void showFooter(boolean show) {
|
||||
if (DEBUG) Log.d(TAG, "showFooter() called with: show = [" + show + "]");
|
||||
if (show == showFooter) return;
|
||||
|
||||
showFooter = show;
|
||||
if (show) notifyItemInserted(sizeConsideringHeader());
|
||||
else notifyItemRemoved(sizeConsideringHeader());
|
||||
}
|
||||
|
||||
private int adapterOffsetWithoutHeader(final int offset) {
|
||||
return offset - (header != null ? 1 : 0);
|
||||
}
|
||||
|
||||
private int sizeConsideringHeader() {
|
||||
return localItems.size() + (header != null ? 1 : 0);
|
||||
}
|
||||
|
||||
public ArrayList<LocalItem> getItemsList() {
|
||||
return localItems;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
int count = localItems.size();
|
||||
if (header != null) count++;
|
||||
if (footer != null && showFooter) count++;
|
||||
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "getItemCount() called, count = " + count +
|
||||
", localItems.size() = " + localItems.size() +
|
||||
", header = " + header + ", footer = " + footer +
|
||||
", showFooter = " + showFooter);
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemViewType(int position) {
|
||||
if (DEBUG) Log.d(TAG, "getItemViewType() called with: position = [" + position + "]");
|
||||
|
||||
if (header != null && position == 0) {
|
||||
return HEADER_TYPE;
|
||||
} else if (header != null) {
|
||||
position--;
|
||||
}
|
||||
if (footer != null && position == localItems.size() && showFooter) {
|
||||
return FOOTER_TYPE;
|
||||
}
|
||||
final LocalItem item = localItems.get(position);
|
||||
|
||||
switch (item.getLocalItemType()) {
|
||||
case PLAYLIST_LOCAL_ITEM: return LOCAL_PLAYLIST_HOLDER_TYPE;
|
||||
case PLAYLIST_REMOTE_ITEM: return REMOTE_PLAYLIST_HOLDER_TYPE;
|
||||
|
||||
case PLAYLIST_STREAM_ITEM: return STREAM_PLAYLIST_HOLDER_TYPE;
|
||||
case STATISTIC_STREAM_ITEM: return STREAM_STATISTICS_HOLDER_TYPE;
|
||||
default:
|
||||
Log.e(TAG, "No holder type has been considered for item: [" +
|
||||
item.getLocalItemType() + "]");
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int type) {
|
||||
if (DEBUG) Log.d(TAG, "onCreateViewHolder() called with: parent = [" +
|
||||
parent + "], type = [" + type + "]");
|
||||
switch (type) {
|
||||
case HEADER_TYPE:
|
||||
return new HeaderFooterHolder(header);
|
||||
case FOOTER_TYPE:
|
||||
return new HeaderFooterHolder(footer);
|
||||
case LOCAL_PLAYLIST_HOLDER_TYPE:
|
||||
return new LocalPlaylistItemHolder(localItemBuilder, parent);
|
||||
case REMOTE_PLAYLIST_HOLDER_TYPE:
|
||||
return new RemotePlaylistItemHolder(localItemBuilder, parent);
|
||||
case STREAM_PLAYLIST_HOLDER_TYPE:
|
||||
return new LocalPlaylistStreamItemHolder(localItemBuilder, parent);
|
||||
case STREAM_STATISTICS_HOLDER_TYPE:
|
||||
return new LocalStatisticStreamItemHolder(localItemBuilder, parent);
|
||||
default:
|
||||
Log.e(TAG, "No view type has been considered for holder: [" + type + "]");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
|
||||
if (DEBUG) Log.d(TAG, "onBindViewHolder() called with: holder = [" +
|
||||
holder.getClass().getSimpleName() + "], position = [" + position + "]");
|
||||
|
||||
if (holder instanceof LocalItemHolder) {
|
||||
// If header isn't null, offset the items by -1
|
||||
if (header != null) position--;
|
||||
|
||||
((LocalItemHolder) holder).updateFromItem(localItems.get(position), dateFormat);
|
||||
} else if (holder instanceof HeaderFooterHolder && position == 0 && header != null) {
|
||||
((HeaderFooterHolder) holder).view = header;
|
||||
} else if (holder instanceof HeaderFooterHolder && position == sizeConsideringHeader()
|
||||
&& footer != null && showFooter) {
|
||||
((HeaderFooterHolder) holder).view = footer;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
package org.schabi.newpipe.fragments.local;
|
||||
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
import org.schabi.newpipe.database.AppDatabase;
|
||||
import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry;
|
||||
import org.schabi.newpipe.database.playlist.PlaylistStreamEntry;
|
||||
import org.schabi.newpipe.database.playlist.dao.PlaylistDAO;
|
||||
import org.schabi.newpipe.database.playlist.dao.PlaylistStreamDAO;
|
||||
import org.schabi.newpipe.database.playlist.model.PlaylistEntity;
|
||||
import org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity;
|
||||
import org.schabi.newpipe.database.stream.dao.StreamDAO;
|
||||
import org.schabi.newpipe.database.stream.model.StreamEntity;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import io.reactivex.Completable;
|
||||
import io.reactivex.Flowable;
|
||||
import io.reactivex.Maybe;
|
||||
import io.reactivex.Single;
|
||||
import io.reactivex.schedulers.Schedulers;
|
||||
|
||||
public class LocalPlaylistManager {
|
||||
|
||||
private final AppDatabase database;
|
||||
private final StreamDAO streamTable;
|
||||
private final PlaylistDAO playlistTable;
|
||||
private final PlaylistStreamDAO playlistStreamTable;
|
||||
|
||||
public LocalPlaylistManager(final AppDatabase db) {
|
||||
database = db;
|
||||
streamTable = db.streamDAO();
|
||||
playlistTable = db.playlistDAO();
|
||||
playlistStreamTable = db.playlistStreamDAO();
|
||||
}
|
||||
|
||||
public Maybe<List<Long>> createPlaylist(final String name, final List<StreamEntity> streams) {
|
||||
// Disallow creation of empty playlists
|
||||
if (streams.isEmpty()) return Maybe.empty();
|
||||
final StreamEntity defaultStream = streams.get(0);
|
||||
final PlaylistEntity newPlaylist =
|
||||
new PlaylistEntity(name, defaultStream.getThumbnailUrl());
|
||||
|
||||
return Maybe.fromCallable(() -> database.runInTransaction(() ->
|
||||
upsertStreams(playlistTable.insert(newPlaylist), streams, 0))
|
||||
).subscribeOn(Schedulers.io());
|
||||
}
|
||||
|
||||
public Maybe<List<Long>> appendToPlaylist(final long playlistId,
|
||||
final List<StreamEntity> streams) {
|
||||
return playlistStreamTable.getMaximumIndexOf(playlistId)
|
||||
.firstElement()
|
||||
.map(maxJoinIndex -> database.runInTransaction(() ->
|
||||
upsertStreams(playlistId, streams, maxJoinIndex + 1))
|
||||
).subscribeOn(Schedulers.io());
|
||||
}
|
||||
|
||||
private List<Long> upsertStreams(final long playlistId,
|
||||
final List<StreamEntity> streams,
|
||||
final int indexOffset) {
|
||||
|
||||
List<PlaylistStreamEntity> joinEntities = new ArrayList<>(streams.size());
|
||||
final List<Long> streamIds = streamTable.upsertAll(streams);
|
||||
for (int index = 0; index < streamIds.size(); index++) {
|
||||
joinEntities.add(new PlaylistStreamEntity(playlistId, streamIds.get(index),
|
||||
index + indexOffset));
|
||||
}
|
||||
return playlistStreamTable.insertAll(joinEntities);
|
||||
}
|
||||
|
||||
public Completable updateJoin(final long playlistId, final List<Long> streamIds) {
|
||||
List<PlaylistStreamEntity> joinEntities = new ArrayList<>(streamIds.size());
|
||||
for (int i = 0; i < streamIds.size(); i++) {
|
||||
joinEntities.add(new PlaylistStreamEntity(playlistId, streamIds.get(i), i));
|
||||
}
|
||||
|
||||
return Completable.fromRunnable(() -> database.runInTransaction(() -> {
|
||||
playlistStreamTable.deleteBatch(playlistId);
|
||||
playlistStreamTable.insertAll(joinEntities);
|
||||
})).subscribeOn(Schedulers.io());
|
||||
}
|
||||
|
||||
public Flowable<List<PlaylistMetadataEntry>> getPlaylists() {
|
||||
return playlistStreamTable.getPlaylistMetadata().subscribeOn(Schedulers.io());
|
||||
}
|
||||
|
||||
public Flowable<List<PlaylistStreamEntry>> getPlaylistStreams(final long playlistId) {
|
||||
return playlistStreamTable.getOrderedStreamsOf(playlistId).subscribeOn(Schedulers.io());
|
||||
}
|
||||
|
||||
public Single<Integer> deletePlaylist(final long playlistId) {
|
||||
return Single.fromCallable(() -> playlistTable.deletePlaylist(playlistId))
|
||||
.subscribeOn(Schedulers.io());
|
||||
}
|
||||
|
||||
public Maybe<Integer> renamePlaylist(final long playlistId, final String name) {
|
||||
return modifyPlaylist(playlistId, name, null);
|
||||
}
|
||||
|
||||
public Maybe<Integer> changePlaylistThumbnail(final long playlistId,
|
||||
final String thumbnailUrl) {
|
||||
return modifyPlaylist(playlistId, null, thumbnailUrl);
|
||||
}
|
||||
|
||||
private Maybe<Integer> modifyPlaylist(final long playlistId,
|
||||
@Nullable final String name,
|
||||
@Nullable final String thumbnailUrl) {
|
||||
return playlistTable.getPlaylist(playlistId)
|
||||
.firstElement()
|
||||
.filter(playlistEntities -> !playlistEntities.isEmpty())
|
||||
.map(playlistEntities -> {
|
||||
PlaylistEntity playlist = playlistEntities.get(0);
|
||||
if (name != null) playlist.setName(name);
|
||||
if (thumbnailUrl != null) playlist.setThumbnailUrl(thumbnailUrl);
|
||||
return playlistTable.update(playlist);
|
||||
}).subscribeOn(Schedulers.io());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package org.schabi.newpipe.fragments.local;
|
||||
|
||||
import org.schabi.newpipe.database.AppDatabase;
|
||||
import org.schabi.newpipe.database.playlist.dao.PlaylistRemoteDAO;
|
||||
import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity;
|
||||
import org.schabi.newpipe.extractor.playlist.PlaylistInfo;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import io.reactivex.Flowable;
|
||||
import io.reactivex.Single;
|
||||
import io.reactivex.schedulers.Schedulers;
|
||||
|
||||
public class RemotePlaylistManager {
|
||||
|
||||
private final AppDatabase database;
|
||||
private final PlaylistRemoteDAO playlistRemoteTable;
|
||||
|
||||
public RemotePlaylistManager(final AppDatabase db) {
|
||||
database = db;
|
||||
playlistRemoteTable = db.playlistRemoteDAO();
|
||||
}
|
||||
|
||||
public Flowable<List<PlaylistRemoteEntity>> getPlaylists() {
|
||||
return playlistRemoteTable.getAll().subscribeOn(Schedulers.io());
|
||||
}
|
||||
|
||||
public Flowable<List<PlaylistRemoteEntity>> getPlaylist(final PlaylistInfo info) {
|
||||
return playlistRemoteTable.getPlaylist(info.getServiceId(), info.getUrl())
|
||||
.subscribeOn(Schedulers.io());
|
||||
}
|
||||
|
||||
public Single<Integer> deletePlaylist(final long playlistId) {
|
||||
return Single.fromCallable(() -> playlistRemoteTable.deletePlaylist(playlistId))
|
||||
.subscribeOn(Schedulers.io());
|
||||
}
|
||||
|
||||
public Single<Long> onBookmark(final PlaylistInfo playlistInfo) {
|
||||
return Single.fromCallable(() -> {
|
||||
final PlaylistRemoteEntity playlist = new PlaylistRemoteEntity(playlistInfo);
|
||||
return playlistRemoteTable.upsert(playlist);
|
||||
}).subscribeOn(Schedulers.io());
|
||||
}
|
||||
|
||||
public Single<Integer> onUpdate(final PlaylistInfo playlistInfo) {
|
||||
return Single.fromCallable(() -> playlistRemoteTable.update(new PlaylistRemoteEntity(playlistInfo)))
|
||||
.subscribeOn(Schedulers.io());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
package org.schabi.newpipe.fragments.local.bookmark;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.support.v4.app.Fragment;
|
||||
import android.support.v7.app.ActionBar;
|
||||
import android.support.v7.widget.LinearLayoutManager;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.util.Log;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.View;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.fragments.BaseStateFragment;
|
||||
import org.schabi.newpipe.fragments.list.ListViewContract;
|
||||
import org.schabi.newpipe.fragments.local.LocalItemListAdapter;
|
||||
|
||||
import static org.schabi.newpipe.util.AnimationUtils.animateView;
|
||||
|
||||
/**
|
||||
* This fragment is design to be used with persistent data such as
|
||||
* {@link org.schabi.newpipe.database.LocalItem}, and does not cache the data contained
|
||||
* in the list adapter to avoid extra writes when the it exits or re-enters its lifecycle.
|
||||
*
|
||||
* This fragment destroys its adapter and views when {@link Fragment#onDestroyView()} is
|
||||
* called and is memory efficient when in backstack.
|
||||
* */
|
||||
public abstract class BaseLocalListFragment<I, N> extends BaseStateFragment<I>
|
||||
implements ListViewContract<I, N> {
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Views
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
protected View headerRootView;
|
||||
protected View footerRootView;
|
||||
|
||||
protected LocalItemListAdapter itemListAdapter;
|
||||
protected RecyclerView itemsList;
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Lifecycle - Creation
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setHasOptionsMenu(true);
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Lifecycle - View
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
protected View getListHeader() {
|
||||
return null;
|
||||
}
|
||||
|
||||
protected View getListFooter() {
|
||||
return activity.getLayoutInflater().inflate(R.layout.pignate_footer, itemsList, false);
|
||||
}
|
||||
|
||||
protected RecyclerView.LayoutManager getListLayoutManager() {
|
||||
return new LinearLayoutManager(activity);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void initViews(View rootView, Bundle savedInstanceState) {
|
||||
super.initViews(rootView, savedInstanceState);
|
||||
|
||||
itemsList = rootView.findViewById(R.id.items_list);
|
||||
itemsList.setLayoutManager(getListLayoutManager());
|
||||
|
||||
itemListAdapter = new LocalItemListAdapter(activity);
|
||||
itemListAdapter.setHeader(headerRootView = getListHeader());
|
||||
itemListAdapter.setFooter(footerRootView = getListFooter());
|
||||
|
||||
itemsList.setAdapter(itemListAdapter);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void initListeners() {
|
||||
super.initListeners();
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Lifecycle - Menu
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
|
||||
super.onCreateOptionsMenu(menu, inflater);
|
||||
if (DEBUG) Log.d(TAG, "onCreateOptionsMenu() called with: menu = [" + menu +
|
||||
"], inflater = [" + inflater + "]");
|
||||
|
||||
final ActionBar supportActionBar = activity.getSupportActionBar();
|
||||
if (supportActionBar == null) return;
|
||||
|
||||
supportActionBar.setDisplayShowTitleEnabled(true);
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Lifecycle - Destruction
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public void onDestroyView() {
|
||||
super.onDestroyView();
|
||||
itemsList = null;
|
||||
itemListAdapter = null;
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Contract
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public void startLoading(boolean forceLoad) {
|
||||
super.startLoading(forceLoad);
|
||||
resetFragment();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void showLoading() {
|
||||
super.showLoading();
|
||||
if (itemsList != null) animateView(itemsList, false, 200);
|
||||
if (headerRootView != null) animateView(headerRootView, false, 200);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void hideLoading() {
|
||||
super.hideLoading();
|
||||
if (itemsList != null) animateView(itemsList, true, 200);
|
||||
if (headerRootView != null) animateView(headerRootView, true, 200);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void showError(String message, boolean showRetryButton) {
|
||||
super.showError(message, showRetryButton);
|
||||
showListFooter(false);
|
||||
|
||||
if (itemsList != null) animateView(itemsList, false, 200);
|
||||
if (headerRootView != null) animateView(headerRootView, false, 200);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void showEmptyState() {
|
||||
super.showEmptyState();
|
||||
showListFooter(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void showListFooter(final boolean show) {
|
||||
itemsList.post(() -> itemListAdapter.showFooter(show));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleNextItems(N result) {
|
||||
isLoading.set(false);
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Error handling
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
protected void resetFragment() {
|
||||
if (itemListAdapter != null) itemListAdapter.clearStreamItemList();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean onError(Throwable exception) {
|
||||
resetFragment();
|
||||
return super.onError(exception);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,309 @@
|
||||
package org.schabi.newpipe.fragments.local.bookmark;
|
||||
|
||||
import android.app.AlertDialog;
|
||||
import android.os.Bundle;
|
||||
import android.os.Parcelable;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v4.app.FragmentManager;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import org.reactivestreams.Subscriber;
|
||||
import org.reactivestreams.Subscription;
|
||||
import org.schabi.newpipe.NewPipeDatabase;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.database.AppDatabase;
|
||||
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.fragments.local.LocalPlaylistManager;
|
||||
import org.schabi.newpipe.fragments.local.RemotePlaylistManager;
|
||||
import org.schabi.newpipe.report.UserAction;
|
||||
import org.schabi.newpipe.util.NavigationHelper;
|
||||
import org.schabi.newpipe.util.OnClickGesture;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import icepick.State;
|
||||
import io.reactivex.Flowable;
|
||||
import io.reactivex.Single;
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.disposables.CompositeDisposable;
|
||||
|
||||
public final class BookmarkFragment
|
||||
extends BaseLocalListFragment<List<PlaylistLocalItem>, Void> {
|
||||
|
||||
private View lastPlayedButton;
|
||||
private View mostPlayedButton;
|
||||
|
||||
@State
|
||||
protected Parcelable itemsListState;
|
||||
|
||||
private Subscription databaseSubscription;
|
||||
private CompositeDisposable disposables = new CompositeDisposable();
|
||||
private LocalPlaylistManager localPlaylistManager;
|
||||
private RemotePlaylistManager remotePlaylistManager;
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
// Fragment LifeCycle - Creation
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
final AppDatabase database = NewPipeDatabase.getInstance(getContext());
|
||||
localPlaylistManager = new LocalPlaylistManager(database);
|
||||
remotePlaylistManager = new RemotePlaylistManager(database);
|
||||
disposables = new CompositeDisposable();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater,
|
||||
@Nullable ViewGroup container,
|
||||
Bundle savedInstanceState) {
|
||||
if (activity != null && activity.getSupportActionBar() != null) {
|
||||
activity.getSupportActionBar().setDisplayShowTitleEnabled(true);
|
||||
activity.setTitle(R.string.tab_subscriptions);
|
||||
}
|
||||
|
||||
return inflater.inflate(R.layout.fragment_bookmarks, container, false);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void setUserVisibleHint(boolean isVisibleToUser) {
|
||||
super.setUserVisibleHint(isVisibleToUser);
|
||||
if (isVisibleToUser) setTitle(getString(R.string.tab_bookmarks));
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
// Fragment LifeCycle - Views
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@Override
|
||||
protected void initViews(View rootView, Bundle savedInstanceState) {
|
||||
super.initViews(rootView, savedInstanceState);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected View getListHeader() {
|
||||
final View headerRootLayout = activity.getLayoutInflater()
|
||||
.inflate(R.layout.bookmark_header, itemsList, false);
|
||||
lastPlayedButton = headerRootLayout.findViewById(R.id.lastPlayed);
|
||||
mostPlayedButton = headerRootLayout.findViewById(R.id.mostPlayed);
|
||||
return headerRootLayout;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void initListeners() {
|
||||
super.initListeners();
|
||||
|
||||
itemListAdapter.setSelectedListener(new OnClickGesture<LocalItem>() {
|
||||
@Override
|
||||
public void selected(LocalItem selectedItem) {
|
||||
// Requires the parent fragment to find holder for fragment replacement
|
||||
if (getParentFragment() == null) return;
|
||||
final FragmentManager fragmentManager = getParentFragment().getFragmentManager();
|
||||
|
||||
if (selectedItem instanceof PlaylistMetadataEntry) {
|
||||
final PlaylistMetadataEntry entry = ((PlaylistMetadataEntry) selectedItem);
|
||||
NavigationHelper.openLocalPlaylistFragment(fragmentManager, entry.uid,
|
||||
entry.name);
|
||||
|
||||
} else if (selectedItem instanceof PlaylistRemoteEntity) {
|
||||
final PlaylistRemoteEntity entry = ((PlaylistRemoteEntity) selectedItem);
|
||||
NavigationHelper.openPlaylistFragment(fragmentManager, entry.getServiceId(),
|
||||
entry.getUrl(), entry.getName());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void held(LocalItem selectedItem) {
|
||||
if (selectedItem instanceof PlaylistMetadataEntry) {
|
||||
showLocalDeleteDialog((PlaylistMetadataEntry) selectedItem);
|
||||
|
||||
} else if (selectedItem instanceof PlaylistRemoteEntity) {
|
||||
showRemoteDeleteDialog((PlaylistRemoteEntity) selectedItem);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
lastPlayedButton.setOnClickListener(view -> {
|
||||
if (getParentFragment() != null) {
|
||||
NavigationHelper.openLastPlayedFragment(getParentFragment().getFragmentManager());
|
||||
}
|
||||
});
|
||||
|
||||
mostPlayedButton.setOnClickListener(view -> {
|
||||
if (getParentFragment() != null) {
|
||||
NavigationHelper.openMostPlayedFragment(getParentFragment().getFragmentManager());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
// Fragment LifeCycle - Loading
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@Override
|
||||
public void startLoading(boolean forceLoad) {
|
||||
super.startLoading(forceLoad);
|
||||
|
||||
Flowable.combineLatest(
|
||||
localPlaylistManager.getPlaylists(),
|
||||
remotePlaylistManager.getPlaylists(),
|
||||
BookmarkFragment::merge
|
||||
).onBackpressureLatest()
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(getPlaylistsSubscriber());
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
// Fragment LifeCycle - Destruction
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@Override
|
||||
public void onPause() {
|
||||
super.onPause();
|
||||
itemsListState = itemsList.getLayoutManager().onSaveInstanceState();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyView() {
|
||||
super.onDestroyView();
|
||||
if (mostPlayedButton != null) mostPlayedButton.setOnClickListener(null);
|
||||
if (lastPlayedButton != null) lastPlayedButton.setOnClickListener(null);
|
||||
|
||||
if (disposables != null) disposables.clear();
|
||||
if (databaseSubscription != null) databaseSubscription.cancel();
|
||||
|
||||
databaseSubscription = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
if (disposables != null) disposables.dispose();
|
||||
|
||||
disposables = null;
|
||||
localPlaylistManager = null;
|
||||
remotePlaylistManager = null;
|
||||
itemsListState = null;
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
// Subscriptions Loader
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
|
||||
private Subscriber<List<PlaylistLocalItem>> getPlaylistsSubscriber() {
|
||||
return new Subscriber<List<PlaylistLocalItem>>() {
|
||||
@Override
|
||||
public void onSubscribe(Subscription s) {
|
||||
showLoading();
|
||||
if (databaseSubscription != null) databaseSubscription.cancel();
|
||||
databaseSubscription = s;
|
||||
databaseSubscription.request(1);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNext(List<PlaylistLocalItem> subscriptions) {
|
||||
handleResult(subscriptions);
|
||||
if (databaseSubscription != null) databaseSubscription.request(1);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(Throwable exception) {
|
||||
BookmarkFragment.this.onError(exception);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onComplete() {
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleResult(@NonNull List<PlaylistLocalItem> result) {
|
||||
super.handleResult(result);
|
||||
|
||||
itemListAdapter.clearStreamItemList();
|
||||
|
||||
if (result.isEmpty()) {
|
||||
showEmptyState();
|
||||
return;
|
||||
}
|
||||
|
||||
itemListAdapter.addItems(result);
|
||||
if (itemsListState != null) {
|
||||
itemsList.getLayoutManager().onRestoreInstanceState(itemsListState);
|
||||
itemsListState = null;
|
||||
}
|
||||
hideLoading();
|
||||
}
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
// Fragment Error Handling
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@Override
|
||||
protected boolean onError(Throwable exception) {
|
||||
if (super.onError(exception)) return true;
|
||||
|
||||
onUnrecoverableError(exception, UserAction.SOMETHING_ELSE,
|
||||
"none", "Bookmark", R.string.general_error);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void resetFragment() {
|
||||
super.resetFragment();
|
||||
if (disposables != null) disposables.clear();
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
// Utils
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
|
||||
private void showLocalDeleteDialog(final PlaylistMetadataEntry item) {
|
||||
showDeleteDialog(item.name, localPlaylistManager.deletePlaylist(item.uid));
|
||||
}
|
||||
|
||||
private void showRemoteDeleteDialog(final PlaylistRemoteEntity item) {
|
||||
showDeleteDialog(item.getName(), remotePlaylistManager.deletePlaylist(item.getUid()));
|
||||
}
|
||||
|
||||
private void showDeleteDialog(final String name, final Single<Integer> deleteReactor) {
|
||||
if (activity == null || disposables == null) return;
|
||||
|
||||
new AlertDialog.Builder(activity)
|
||||
.setTitle(name)
|
||||
.setMessage(R.string.delete_playlist_prompt)
|
||||
.setCancelable(true)
|
||||
.setPositiveButton(R.string.delete, (dialog, i) ->
|
||||
disposables.add(deleteReactor
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(ignored -> {/*Do nothing on success*/}, this::onError))
|
||||
)
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.show();
|
||||
}
|
||||
|
||||
private static List<PlaylistLocalItem> merge(final List<PlaylistMetadataEntry> localPlaylists,
|
||||
final List<PlaylistRemoteEntity> remotePlaylists) {
|
||||
List<PlaylistLocalItem> items = new ArrayList<>(
|
||||
localPlaylists.size() + remotePlaylists.size());
|
||||
items.addAll(localPlaylists);
|
||||
items.addAll(remotePlaylists);
|
||||
|
||||
Collections.sort(items, (left, right) ->
|
||||
left.getOrderingName().compareToIgnoreCase(right.getOrderingName()));
|
||||
|
||||
return items;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
package org.schabi.newpipe.fragments.local.bookmark;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.database.stream.StreamStatisticsEntry;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
public final class LastPlayedFragment extends StatisticsPlaylistFragment {
|
||||
@Override
|
||||
protected String getName() {
|
||||
return getString(R.string.title_last_played);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<StreamStatisticsEntry> processResult(List<StreamStatisticsEntry> results) {
|
||||
Collections.sort(results, (left, right) ->
|
||||
right.latestAccessDate.compareTo(left.latestAccessDate));
|
||||
return results;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,590 @@
|
||||
package org.schabi.newpipe.fragments.local.bookmark;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.os.Bundle;
|
||||
import android.os.Parcelable;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v7.app.AlertDialog;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.support.v7.widget.helper.ItemTouchHelper;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.EditText;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import org.reactivestreams.Subscriber;
|
||||
import org.reactivestreams.Subscription;
|
||||
import org.schabi.newpipe.NewPipeDatabase;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.database.LocalItem;
|
||||
import org.schabi.newpipe.database.playlist.PlaylistStreamEntry;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||
import org.schabi.newpipe.fragments.local.LocalPlaylistManager;
|
||||
import org.schabi.newpipe.info_list.InfoItemDialog;
|
||||
import org.schabi.newpipe.playlist.PlayQueue;
|
||||
import org.schabi.newpipe.playlist.SinglePlayQueue;
|
||||
import org.schabi.newpipe.report.UserAction;
|
||||
import org.schabi.newpipe.util.Localization;
|
||||
import org.schabi.newpipe.util.NavigationHelper;
|
||||
import org.schabi.newpipe.util.OnClickGesture;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
import icepick.State;
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.disposables.CompositeDisposable;
|
||||
import io.reactivex.disposables.Disposable;
|
||||
import io.reactivex.disposables.Disposables;
|
||||
import io.reactivex.subjects.PublishSubject;
|
||||
|
||||
import static org.schabi.newpipe.util.AnimationUtils.animateView;
|
||||
|
||||
public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistStreamEntry>, Void> {
|
||||
|
||||
// Save the list 10 seconds after the last change occurred
|
||||
private static final long SAVE_DEBOUNCE_MILLIS = 10000;
|
||||
private static final int MINIMUM_INITIAL_DRAG_VELOCITY = 12;
|
||||
|
||||
private View headerRootLayout;
|
||||
private TextView headerTitleView;
|
||||
private TextView headerStreamCount;
|
||||
|
||||
private View playlistControl;
|
||||
private View headerPlayAllButton;
|
||||
private View headerPopupButton;
|
||||
private View headerBackgroundButton;
|
||||
|
||||
@State
|
||||
protected Long playlistId;
|
||||
@State
|
||||
protected String name;
|
||||
@State
|
||||
protected Parcelable itemsListState;
|
||||
|
||||
private ItemTouchHelper itemTouchHelper;
|
||||
|
||||
private LocalPlaylistManager playlistManager;
|
||||
private Subscription databaseSubscription;
|
||||
|
||||
private PublishSubject<Long> debouncedSaveSignal;
|
||||
private CompositeDisposable disposables;
|
||||
|
||||
/* Has the playlist been fully loaded from db */
|
||||
private AtomicBoolean isLoadingComplete;
|
||||
/* Has the playlist been modified (e.g. items reordered or deleted) */
|
||||
private AtomicBoolean isModified;
|
||||
|
||||
public static LocalPlaylistFragment getInstance(long playlistId, String name) {
|
||||
LocalPlaylistFragment instance = new LocalPlaylistFragment();
|
||||
instance.setInitialData(playlistId, name);
|
||||
return instance;
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
// Fragment LifeCycle - Creation
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
playlistManager = new LocalPlaylistManager(NewPipeDatabase.getInstance(getContext()));
|
||||
debouncedSaveSignal = PublishSubject.create();
|
||||
|
||||
disposables = new CompositeDisposable();
|
||||
|
||||
isLoadingComplete = new AtomicBoolean();
|
||||
isModified = new AtomicBoolean();
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater,
|
||||
@Nullable ViewGroup container,
|
||||
@Nullable Bundle savedInstanceState) {
|
||||
return inflater.inflate(R.layout.fragment_playlist, container, false);
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
// Fragment Lifecycle - Views
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@Override
|
||||
public void setTitle(final String title) {
|
||||
super.setTitle(title);
|
||||
|
||||
if (headerTitleView != null) {
|
||||
headerTitleView.setText(title);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void initViews(View rootView, Bundle savedInstanceState) {
|
||||
super.initViews(rootView, savedInstanceState);
|
||||
setTitle(name);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected View getListHeader() {
|
||||
headerRootLayout = activity.getLayoutInflater().inflate(R.layout.local_playlist_header,
|
||||
itemsList, false);
|
||||
|
||||
headerTitleView = headerRootLayout.findViewById(R.id.playlist_title_view);
|
||||
headerTitleView.setSelected(true);
|
||||
|
||||
headerStreamCount = headerRootLayout.findViewById(R.id.playlist_stream_count);
|
||||
|
||||
playlistControl = headerRootLayout.findViewById(R.id.playlist_control);
|
||||
headerPlayAllButton = headerRootLayout.findViewById(R.id.playlist_ctrl_play_all_button);
|
||||
headerPopupButton = headerRootLayout.findViewById(R.id.playlist_ctrl_play_popup_button);
|
||||
headerBackgroundButton = headerRootLayout.findViewById(R.id.playlist_ctrl_play_bg_button);
|
||||
|
||||
return headerRootLayout;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void initListeners() {
|
||||
super.initListeners();
|
||||
|
||||
headerTitleView.setOnClickListener(view -> createRenameDialog());
|
||||
|
||||
itemTouchHelper = new ItemTouchHelper(getItemTouchCallback());
|
||||
itemTouchHelper.attachToRecyclerView(itemsList);
|
||||
|
||||
itemListAdapter.setSelectedListener(new OnClickGesture<LocalItem>() {
|
||||
@Override
|
||||
public void selected(LocalItem selectedItem) {
|
||||
if (selectedItem instanceof PlaylistStreamEntry) {
|
||||
final PlaylistStreamEntry item = (PlaylistStreamEntry) selectedItem;
|
||||
NavigationHelper.openVideoDetailFragment(getFragmentManager(),
|
||||
item.serviceId, item.url, item.title);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void held(LocalItem selectedItem) {
|
||||
if (selectedItem instanceof PlaylistStreamEntry) {
|
||||
showStreamDialog((PlaylistStreamEntry) selectedItem);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void drag(LocalItem selectedItem, RecyclerView.ViewHolder viewHolder) {
|
||||
if (itemTouchHelper != null) itemTouchHelper.startDrag(viewHolder);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
// Fragment Lifecycle - Loading
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@Override
|
||||
public void showLoading() {
|
||||
super.showLoading();
|
||||
if (headerRootLayout != null) animateView(headerRootLayout, false, 200);
|
||||
if (playlistControl != null) animateView(playlistControl, false, 200);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void hideLoading() {
|
||||
super.hideLoading();
|
||||
if (headerRootLayout != null) animateView(headerRootLayout, true, 200);
|
||||
if (playlistControl != null) animateView(playlistControl, true, 200);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void startLoading(boolean forceLoad) {
|
||||
super.startLoading(forceLoad);
|
||||
|
||||
if (disposables != null) disposables.clear();
|
||||
disposables.add(getDebouncedSaver());
|
||||
|
||||
isLoadingComplete.set(false);
|
||||
isModified.set(false);
|
||||
|
||||
playlistManager.getPlaylistStreams(playlistId)
|
||||
.onBackpressureLatest()
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(getPlaylistObserver());
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
// Fragment Lifecycle - Destruction
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@Override
|
||||
public void onPause() {
|
||||
super.onPause();
|
||||
itemsListState = itemsList.getLayoutManager().onSaveInstanceState();
|
||||
|
||||
// Save on exit
|
||||
saveImmediate();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyView() {
|
||||
super.onDestroyView();
|
||||
|
||||
if (itemListAdapter != null) itemListAdapter.unsetSelectedListener();
|
||||
if (headerBackgroundButton != null) headerBackgroundButton.setOnClickListener(null);
|
||||
if (headerPlayAllButton != null) headerPlayAllButton.setOnClickListener(null);
|
||||
if (headerPopupButton != null) headerPopupButton.setOnClickListener(null);
|
||||
|
||||
if (databaseSubscription != null) databaseSubscription.cancel();
|
||||
if (disposables != null) disposables.clear();
|
||||
|
||||
databaseSubscription = null;
|
||||
itemTouchHelper = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
if (debouncedSaveSignal != null) debouncedSaveSignal.onComplete();
|
||||
if (disposables != null) disposables.dispose();
|
||||
|
||||
debouncedSaveSignal = null;
|
||||
playlistManager = null;
|
||||
disposables = null;
|
||||
|
||||
isLoadingComplete = null;
|
||||
isModified = null;
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
// Playlist Stream Loader
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
|
||||
private Subscriber<List<PlaylistStreamEntry>> getPlaylistObserver() {
|
||||
return new Subscriber<List<PlaylistStreamEntry>>() {
|
||||
@Override
|
||||
public void onSubscribe(Subscription s) {
|
||||
showLoading();
|
||||
isLoadingComplete.set(false);
|
||||
|
||||
if (databaseSubscription != null) databaseSubscription.cancel();
|
||||
databaseSubscription = s;
|
||||
databaseSubscription.request(1);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNext(List<PlaylistStreamEntry> streams) {
|
||||
// Skip handling the result after it has been modified
|
||||
if (isModified == null || !isModified.get()) {
|
||||
handleResult(streams);
|
||||
isLoadingComplete.set(true);
|
||||
}
|
||||
|
||||
if (databaseSubscription != null) databaseSubscription.request(1);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(Throwable exception) {
|
||||
LocalPlaylistFragment.this.onError(exception);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onComplete() {}
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleResult(@NonNull List<PlaylistStreamEntry> result) {
|
||||
super.handleResult(result);
|
||||
if (itemListAdapter == null) return;
|
||||
|
||||
itemListAdapter.clearStreamItemList();
|
||||
|
||||
if (result.isEmpty()) {
|
||||
showEmptyState();
|
||||
return;
|
||||
}
|
||||
|
||||
itemListAdapter.addItems(result);
|
||||
if (itemsListState != null) {
|
||||
itemsList.getLayoutManager().onRestoreInstanceState(itemsListState);
|
||||
itemsListState = null;
|
||||
}
|
||||
setVideoCount(itemListAdapter.getItemsList().size());
|
||||
|
||||
headerPlayAllButton.setOnClickListener(view ->
|
||||
NavigationHelper.playOnMainPlayer(activity, getPlayQueue()));
|
||||
headerPopupButton.setOnClickListener(view ->
|
||||
NavigationHelper.playOnPopupPlayer(activity, getPlayQueue()));
|
||||
headerBackgroundButton.setOnClickListener(view ->
|
||||
NavigationHelper.playOnBackgroundPlayer(activity, getPlayQueue()));
|
||||
|
||||
hideLoading();
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
// Fragment Error Handling
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@Override
|
||||
protected void resetFragment() {
|
||||
super.resetFragment();
|
||||
if (databaseSubscription != null) databaseSubscription.cancel();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean onError(Throwable exception) {
|
||||
if (super.onError(exception)) return true;
|
||||
|
||||
onUnrecoverableError(exception, UserAction.SOMETHING_ELSE,
|
||||
"none", "Local Playlist", R.string.general_error);
|
||||
return true;
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Playlist Metadata/Streams Manipulation
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
private void createRenameDialog() {
|
||||
if (playlistId == null || name == null || getContext() == null) return;
|
||||
|
||||
final View dialogView = View.inflate(getContext(), R.layout.dialog_playlist_name, null);
|
||||
EditText nameEdit = dialogView.findViewById(R.id.playlist_name);
|
||||
nameEdit.setText(name);
|
||||
nameEdit.setSelection(nameEdit.getText().length());
|
||||
|
||||
final AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(getContext())
|
||||
.setTitle(R.string.rename_playlist)
|
||||
.setView(dialogView)
|
||||
.setCancelable(true)
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.setPositiveButton(R.string.rename, (dialogInterface, i) -> {
|
||||
changePlaylistName(nameEdit.getText().toString());
|
||||
});
|
||||
|
||||
dialogBuilder.show();
|
||||
}
|
||||
|
||||
private void changePlaylistName(final String name) {
|
||||
if (playlistManager == null) return;
|
||||
|
||||
this.name = name;
|
||||
setTitle(name);
|
||||
|
||||
Log.d(TAG, "Updating playlist id=[" + playlistId +
|
||||
"] with new name=[" + name + "] items");
|
||||
|
||||
final Disposable disposable = playlistManager.renamePlaylist(playlistId, name)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(longs -> {/*Do nothing on success*/}, this::onError);
|
||||
disposables.add(disposable);
|
||||
}
|
||||
|
||||
private void changeThumbnailUrl(final String thumbnailUrl) {
|
||||
if (playlistManager == null) return;
|
||||
|
||||
final Toast successToast = Toast.makeText(getActivity(),
|
||||
R.string.playlist_thumbnail_change_success,
|
||||
Toast.LENGTH_SHORT);
|
||||
|
||||
Log.d(TAG, "Updating playlist id=[" + playlistId +
|
||||
"] with new thumbnail url=[" + thumbnailUrl + "]");
|
||||
|
||||
final Disposable disposable = playlistManager
|
||||
.changePlaylistThumbnail(playlistId, thumbnailUrl)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(ignore -> successToast.show(), this::onError);
|
||||
disposables.add(disposable);
|
||||
}
|
||||
|
||||
private void deleteItem(final PlaylistStreamEntry item) {
|
||||
if (itemListAdapter == null) return;
|
||||
|
||||
itemListAdapter.removeItem(item);
|
||||
setVideoCount(itemListAdapter.getItemsList().size());
|
||||
saveChanges();
|
||||
}
|
||||
|
||||
private void saveChanges() {
|
||||
if (isModified == null || debouncedSaveSignal == null) return;
|
||||
|
||||
isModified.set(true);
|
||||
debouncedSaveSignal.onNext(System.currentTimeMillis());
|
||||
}
|
||||
|
||||
private Disposable getDebouncedSaver() {
|
||||
if (debouncedSaveSignal == null) return Disposables.empty();
|
||||
|
||||
return debouncedSaveSignal
|
||||
.debounce(SAVE_DEBOUNCE_MILLIS, TimeUnit.MILLISECONDS)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(ignored -> saveImmediate(), this::onError);
|
||||
}
|
||||
|
||||
private void saveImmediate() {
|
||||
if (playlistManager == null || itemListAdapter == null) return;
|
||||
|
||||
// List must be loaded and modified in order to save
|
||||
if (isLoadingComplete == null || isModified == null ||
|
||||
!isLoadingComplete.get() || !isModified.get()) {
|
||||
Log.w(TAG, "Attempting to save playlist when local playlist " +
|
||||
"is not loaded or not modified: playlist id=[" + playlistId + "]");
|
||||
return;
|
||||
}
|
||||
|
||||
final List<LocalItem> items = itemListAdapter.getItemsList();
|
||||
List<Long> streamIds = new ArrayList<>(items.size());
|
||||
for (final LocalItem item : items) {
|
||||
if (item instanceof PlaylistStreamEntry) {
|
||||
streamIds.add(((PlaylistStreamEntry) item).streamId);
|
||||
}
|
||||
}
|
||||
|
||||
Log.d(TAG, "Updating playlist id=[" + playlistId +
|
||||
"] with [" + streamIds.size() + "] items");
|
||||
|
||||
final Disposable disposable = playlistManager.updateJoin(playlistId, streamIds)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
() -> { if (isModified != null) isModified.set(false); },
|
||||
this::onError
|
||||
);
|
||||
disposables.add(disposable);
|
||||
}
|
||||
|
||||
|
||||
private ItemTouchHelper.SimpleCallback getItemTouchCallback() {
|
||||
return new ItemTouchHelper.SimpleCallback(ItemTouchHelper.UP | ItemTouchHelper.DOWN,
|
||||
ItemTouchHelper.ACTION_STATE_IDLE) {
|
||||
@Override
|
||||
public int interpolateOutOfBoundsScroll(RecyclerView recyclerView, int viewSize,
|
||||
int viewSizeOutOfBounds, int totalSize,
|
||||
long msSinceStartScroll) {
|
||||
final int standardSpeed = super.interpolateOutOfBoundsScroll(recyclerView, viewSize,
|
||||
viewSizeOutOfBounds, totalSize, msSinceStartScroll);
|
||||
final int minimumAbsVelocity = Math.max(MINIMUM_INITIAL_DRAG_VELOCITY,
|
||||
Math.abs(standardSpeed));
|
||||
return minimumAbsVelocity * (int) Math.signum(viewSizeOutOfBounds);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder source,
|
||||
RecyclerView.ViewHolder target) {
|
||||
if (source.getItemViewType() != target.getItemViewType() ||
|
||||
itemListAdapter == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final int sourceIndex = source.getAdapterPosition();
|
||||
final int targetIndex = target.getAdapterPosition();
|
||||
final boolean isSwapped = itemListAdapter.swapItems(sourceIndex, targetIndex);
|
||||
if (isSwapped) saveChanges();
|
||||
return isSwapped;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isLongPressDragEnabled() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isItemViewSwipeEnabled() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSwiped(RecyclerView.ViewHolder viewHolder, int swipeDir) {}
|
||||
};
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Utils
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
protected void showStreamDialog(final PlaylistStreamEntry item) {
|
||||
final Context context = getContext();
|
||||
final Activity activity = getActivity();
|
||||
if (context == null || context.getResources() == null || getActivity() == null) return;
|
||||
|
||||
final StreamInfoItem infoItem = item.toStreamInfoItem();
|
||||
|
||||
final String[] commands = new String[]{
|
||||
context.getResources().getString(R.string.enqueue_on_background),
|
||||
context.getResources().getString(R.string.enqueue_on_popup),
|
||||
context.getResources().getString(R.string.start_here_on_main),
|
||||
context.getResources().getString(R.string.start_here_on_background),
|
||||
context.getResources().getString(R.string.start_here_on_popup),
|
||||
context.getResources().getString(R.string.set_as_playlist_thumbnail),
|
||||
context.getResources().getString(R.string.delete)
|
||||
};
|
||||
|
||||
final DialogInterface.OnClickListener actions = (dialogInterface, i) -> {
|
||||
final int index = Math.max(itemListAdapter.getItemsList().indexOf(item), 0);
|
||||
switch (i) {
|
||||
case 0:
|
||||
NavigationHelper.enqueueOnBackgroundPlayer(context,
|
||||
new SinglePlayQueue(infoItem));
|
||||
break;
|
||||
case 1:
|
||||
NavigationHelper.enqueueOnPopupPlayer(activity, new
|
||||
SinglePlayQueue(infoItem));
|
||||
break;
|
||||
case 2:
|
||||
NavigationHelper.playOnMainPlayer(context, getPlayQueue(index));
|
||||
break;
|
||||
case 3:
|
||||
NavigationHelper.playOnBackgroundPlayer(context, getPlayQueue(index));
|
||||
break;
|
||||
case 4:
|
||||
NavigationHelper.playOnPopupPlayer(activity, getPlayQueue(index));
|
||||
break;
|
||||
case 5:
|
||||
changeThumbnailUrl(item.thumbnailUrl);
|
||||
break;
|
||||
case 6:
|
||||
deleteItem(item);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
new InfoItemDialog(getActivity(), infoItem, commands, actions).show();
|
||||
}
|
||||
|
||||
private void setInitialData(long playlistId, String name) {
|
||||
this.playlistId = playlistId;
|
||||
this.name = !TextUtils.isEmpty(name) ? name : "";
|
||||
}
|
||||
|
||||
private void setVideoCount(final long count) {
|
||||
if (activity != null && headerStreamCount != null) {
|
||||
headerStreamCount.setText(Localization.localizeStreamCount(activity, count));
|
||||
}
|
||||
}
|
||||
|
||||
private PlayQueue getPlayQueue() {
|
||||
return getPlayQueue(0);
|
||||
}
|
||||
|
||||
private PlayQueue getPlayQueue(final int index) {
|
||||
if (itemListAdapter == null) {
|
||||
return new SinglePlayQueue(Collections.emptyList(), 0);
|
||||
}
|
||||
|
||||
final List<LocalItem> infoItems = itemListAdapter.getItemsList();
|
||||
List<StreamInfoItem> streamInfoItems = new ArrayList<>(infoItems.size());
|
||||
for (final LocalItem item : infoItems) {
|
||||
if (item instanceof PlaylistStreamEntry) {
|
||||
streamInfoItems.add(((PlaylistStreamEntry) item).toStreamInfoItem());
|
||||
}
|
||||
}
|
||||
return new SinglePlayQueue(streamInfoItems, index);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
package org.schabi.newpipe.fragments.local.bookmark;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.database.stream.StreamStatisticsEntry;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
public final class MostPlayedFragment extends StatisticsPlaylistFragment {
|
||||
@Override
|
||||
protected String getName() {
|
||||
return getString(R.string.title_most_played);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<StreamStatisticsEntry> processResult(List<StreamStatisticsEntry> results) {
|
||||
Collections.sort(results, (left, right) ->
|
||||
((Long) right.watchCount).compareTo(left.watchCount));
|
||||
return results;
|
||||
}
|
||||
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user