mirror of
https://github.com/TeamNewPipe/NewPipe
synced 2026-01-14 10:42:40 +00:00
Compare commits
1726 Commits
v0.19.6-fd
...
v0.21.3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2829851e49 | ||
|
|
2b8fda3511 | ||
|
|
d31959990e | ||
|
|
139ced885d | ||
|
|
f0bb2e8687 | ||
|
|
4643ccef6f | ||
|
|
753ca7cb53 | ||
|
|
87d2f33e55 | ||
|
|
fc7944d287 | ||
|
|
376e5c1546 | ||
|
|
e8ad947d37 | ||
|
|
067528211f | ||
|
|
92ab9cae27 | ||
|
|
fa2b11b768 | ||
|
|
82f43ac6a6 | ||
|
|
c7660b8c2d | ||
|
|
847831c195 | ||
|
|
e0b246431f | ||
|
|
c808beec30 | ||
|
|
2d4a3c2554 | ||
|
|
eb1cefe2fa | ||
|
|
5eb5dbddde | ||
|
|
bfe3eff5ff | ||
|
|
e7936e6c9a | ||
|
|
514f92e6f2 | ||
|
|
68fd7a031f | ||
|
|
95f61542b5 | ||
|
|
9fc6f19702 | ||
|
|
4038617d59 | ||
|
|
98ccd577d6 | ||
|
|
1d43a2362c | ||
|
|
0ff675171b | ||
|
|
59594c6637 | ||
|
|
9595733563 | ||
|
|
5eb1d49857 | ||
|
|
fa1fdbf73e | ||
|
|
52e52b3ca1 | ||
|
|
5b4fbe32b1 | ||
|
|
31ea44ccf1 | ||
|
|
d26ca194b3 | ||
|
|
f80b1fb2fe | ||
|
|
38ed07caa7 | ||
|
|
72ee4be495 | ||
|
|
c85b97a484 | ||
|
|
c7510c628f | ||
|
|
3ca1e550fe | ||
|
|
01e8944077 | ||
|
|
d6ab3298a3 | ||
|
|
97b28bba4d | ||
|
|
7f6674a0e6 | ||
|
|
2c1df5f875 | ||
|
|
e7ae215ab0 | ||
|
|
4a9c790652 | ||
|
|
91ca680911 | ||
|
|
01376aba86 | ||
|
|
d56ffa3531 | ||
|
|
5a5a24bf1a | ||
|
|
3d2c65b398 | ||
|
|
bacb35fb1c | ||
|
|
0a2ed805a2 | ||
|
|
e70c153cd3 | ||
|
|
b54c2b7f57 | ||
|
|
3fe80ec5ac | ||
|
|
e52048c69e | ||
|
|
ceb930aed6 | ||
|
|
e775037366 | ||
|
|
0039312a64 | ||
|
|
57f1152751 | ||
|
|
bfb9be1225 | ||
|
|
8837b54aab | ||
|
|
8ab5a4d394 | ||
|
|
29bcf94d50 | ||
|
|
dd68bf8eeb | ||
|
|
fc4dd4524a | ||
|
|
c12ac64678 | ||
|
|
264044272a | ||
|
|
c74162c586 | ||
|
|
fa6ff4e5eb | ||
|
|
01bbc50c68 | ||
|
|
e5457e5029 | ||
|
|
b025bdf0c7 | ||
|
|
600e156c4c | ||
|
|
13ba708adc | ||
|
|
edf8bf2c9d | ||
|
|
c9e0bf4f02 | ||
|
|
8f9eaa22e6 | ||
|
|
3fcd580491 | ||
|
|
cf3cc2e984 | ||
|
|
76322d8089 | ||
|
|
9e29d8d692 | ||
|
|
5d5f8b4d51 | ||
|
|
4d74be881d | ||
|
|
425a312151 | ||
|
|
ea294e8e5d | ||
|
|
e75d0de135 | ||
|
|
81cacbd917 | ||
|
|
c0c78ae9bb | ||
|
|
eb572e8d8f | ||
|
|
781c499806 | ||
|
|
a3d74ea444 | ||
|
|
86a19aa037 | ||
|
|
e484339cca | ||
|
|
6b5a1d0202 | ||
|
|
24247fd6a6 | ||
|
|
2af20d5c40 | ||
|
|
dfb983c3cf | ||
|
|
e1a6b69f9a | ||
|
|
8df935f5fe | ||
|
|
04c5acd1d7 | ||
|
|
c9766d25ef | ||
|
|
aaea661b70 | ||
|
|
7061859112 | ||
|
|
66c24af3d2 | ||
|
|
083c315fd6 | ||
|
|
29b44a181b | ||
|
|
4fdc5ea646 | ||
|
|
e17bfa029c | ||
|
|
279e4c2fa8 | ||
|
|
856a39855e | ||
|
|
4a9d21062a | ||
|
|
ad8f3aa6c9 | ||
|
|
43f85408be | ||
|
|
5739caaa5a | ||
|
|
73cfa5499d | ||
|
|
8f0323fb8d | ||
|
|
83d16932a4 | ||
|
|
84e3f6ca18 | ||
|
|
5d6a568308 | ||
|
|
3e8cba745a | ||
|
|
be4d12789d | ||
|
|
00fbfb5a56 | ||
|
|
27d0f7f277 | ||
|
|
fb1aab2a49 | ||
|
|
cc72fa4793 | ||
|
|
e9c60eff85 | ||
|
|
5b7c87ee79 | ||
|
|
c8b4685fc9 | ||
|
|
561d5675f7 | ||
|
|
c906cb57ee | ||
|
|
84de865daf | ||
|
|
24a264d78c | ||
|
|
62c3c7ac21 | ||
|
|
a7d6ad5162 | ||
|
|
0dbb212d13 | ||
|
|
8002cc2771 | ||
|
|
8d64eac853 | ||
|
|
80d1c5b9f5 | ||
|
|
7175f27da8 | ||
|
|
d6f9aace8c | ||
|
|
aeccb5b472 | ||
|
|
09a7b7718a | ||
|
|
ef2c76efaf | ||
|
|
69793049c3 | ||
|
|
67942a906a | ||
|
|
6a9cae3de8 | ||
|
|
0afdac5683 | ||
|
|
609d09a8e2 | ||
|
|
01e8654fbd | ||
|
|
f477ab84d5 | ||
|
|
9f59d4baa3 | ||
|
|
2e9a1d958c | ||
|
|
e4f2c58933 | ||
|
|
a9e8b3e06b | ||
|
|
daa5b7827a | ||
|
|
dd00152485 | ||
|
|
df52a6ea6b | ||
|
|
7d5197e6fd | ||
|
|
81e08d0cc4 | ||
|
|
32a159d48f | ||
|
|
44f3a7484d | ||
|
|
38eb8e40ea | ||
|
|
fd14c8cdce | ||
|
|
216da63276 | ||
|
|
08d8f2564a | ||
|
|
1d51002173 | ||
|
|
610d0b272e | ||
|
|
b3e2418b93 | ||
|
|
464d0e50b0 | ||
|
|
7411c54f9e | ||
|
|
7c74deb700 | ||
|
|
e63165e80f | ||
|
|
e2bc9dfacd | ||
|
|
48789dbab7 | ||
|
|
196f0f0475 | ||
|
|
085b59f2e1 | ||
|
|
2fdc2664ff | ||
|
|
a33a5c5527 | ||
|
|
a7c0f37904 | ||
|
|
4889ab3462 | ||
|
|
3923deeaad | ||
|
|
80d6fff0ca | ||
|
|
fe43b4da39 | ||
|
|
67afd05e22 | ||
|
|
0fcaf20221 | ||
|
|
0277b94b37 | ||
|
|
6a9d5fd4cc | ||
|
|
a83106f717 | ||
|
|
4c2a6e346d | ||
|
|
cae63a7ada | ||
|
|
c7efa8c4f1 | ||
|
|
bf6645e829 | ||
|
|
ea1b42510c | ||
|
|
72818ffa42 | ||
|
|
985308bf0c | ||
|
|
86381696f4 | ||
|
|
08b960cc6e | ||
|
|
731c65cd59 | ||
|
|
a85e8a29ff | ||
|
|
22b2f52f8c | ||
|
|
a713ce2126 | ||
|
|
4fac3cf304 | ||
|
|
74e20a8c52 | ||
|
|
8cf4ba25f5 | ||
|
|
feb65cf8f3 | ||
|
|
93592d23f4 | ||
|
|
338a4837bc | ||
|
|
8e19fe535c | ||
|
|
2aeccc0c5c | ||
|
|
8db1234a59 | ||
|
|
80fb351ad3 | ||
|
|
523f85d4d1 | ||
|
|
bfff500915 | ||
|
|
3e83bb0d95 | ||
|
|
bdaee25e61 | ||
|
|
71d3227791 | ||
|
|
a28aa6a8c4 | ||
|
|
292e103073 | ||
|
|
39a3f03e79 | ||
|
|
404a6c12a6 | ||
|
|
8271409afe | ||
|
|
985f659026 | ||
|
|
7c36cbffd0 | ||
|
|
285ea4e3fd | ||
|
|
8ce18647f1 | ||
|
|
aee0478235 | ||
|
|
c3cf1d81c2 | ||
|
|
c2b6cec37d | ||
|
|
b265cabc22 | ||
|
|
463dd8ea74 | ||
|
|
0263125e11 | ||
|
|
1fc8e4c148 | ||
|
|
c43bca6007 | ||
|
|
4c31636d19 | ||
|
|
3a61ab59f2 | ||
|
|
1db3c57ef0 | ||
|
|
eeaf3496d5 | ||
|
|
70f421b787 | ||
|
|
86fa629591 | ||
|
|
553b80164b | ||
|
|
8518933ca8 | ||
|
|
ea53b7d4ad | ||
|
|
37a96d063f | ||
|
|
b360920472 | ||
|
|
9e1744f904 | ||
|
|
85a468bda9 | ||
|
|
7955ef8105 | ||
|
|
80cd41893b | ||
|
|
9b09f2ad71 | ||
|
|
c45d9559c4 | ||
|
|
f0d978b4c6 | ||
|
|
8734f4bbe3 | ||
|
|
9f03280075 | ||
|
|
427ac4ef35 | ||
|
|
d9c4495e8e | ||
|
|
d09070b61d | ||
|
|
d6855a6b50 | ||
|
|
9be970a4c4 | ||
|
|
b236bb407b | ||
|
|
8978187c64 | ||
|
|
eba0b07782 | ||
|
|
1f77e00df4 | ||
|
|
41c70cc85d | ||
|
|
5bc0a8fba1 | ||
|
|
687020e595 | ||
|
|
8c75b96c38 | ||
|
|
6f7a01bc53 | ||
|
|
5383a0af0b | ||
|
|
3e50466024 | ||
|
|
45b703daf6 | ||
|
|
f1e1f6424a | ||
|
|
460f031cef | ||
|
|
25aaf4e48b | ||
|
|
a26baa3061 | ||
|
|
138513d790 | ||
|
|
1e5dc01825 | ||
|
|
8c4b1b967d | ||
|
|
7faa107547 | ||
|
|
85ccc2384f | ||
|
|
469a5b1974 | ||
|
|
80161c36c6 | ||
|
|
aea912f499 | ||
|
|
156d7139fa | ||
|
|
0ad3d0247d | ||
|
|
092f9170cc | ||
|
|
b820e9a888 | ||
|
|
b9cd55188e | ||
|
|
ebd45dfae3 | ||
|
|
40195b2d98 | ||
|
|
0b0305eaed | ||
|
|
950997ea66 | ||
|
|
be4beb41b6 | ||
|
|
c55f87c962 | ||
|
|
bdc85b435c | ||
|
|
522d6d8b01 | ||
|
|
e98838ad7e | ||
|
|
3829565ea0 | ||
|
|
c16a8dacd0 | ||
|
|
02db971b7c | ||
|
|
0d522aae6c | ||
|
|
fdb0f01b38 | ||
|
|
376cba696e | ||
|
|
cade272501 | ||
|
|
4f828fbe00 | ||
|
|
3d348c63d9 | ||
|
|
fe10c19956 | ||
|
|
9ada979484 | ||
|
|
2926cb7682 | ||
|
|
8c15cc1c17 | ||
|
|
bea4fb6ae6 | ||
|
|
cafc64534b | ||
|
|
327fc742d3 | ||
|
|
0c5df29417 | ||
|
|
cce896e900 | ||
|
|
50c0f9e622 | ||
|
|
10ec67854e | ||
|
|
e327f7ba2c | ||
|
|
9a65f02d5b | ||
|
|
d1fc9c5880 | ||
|
|
3c9ae68314 | ||
|
|
5e7c2c11f6 | ||
|
|
053b6ab8c6 | ||
|
|
5814743d59 | ||
|
|
fa7613b8d1 | ||
|
|
d3d05d613d | ||
|
|
23b5cd5b72 | ||
|
|
d4a33603ab | ||
|
|
39724de6e6 | ||
|
|
156adaa1a0 | ||
|
|
3868243c2a | ||
|
|
243f539439 | ||
|
|
71d92c8d1b | ||
|
|
e840d42fb9 | ||
|
|
750c4ffbd3 | ||
|
|
d043a4f410 | ||
|
|
4c3ba0fe3d | ||
|
|
a314f55a17 | ||
|
|
78a9811fe3 | ||
|
|
6277639ded | ||
|
|
d1c807487a | ||
|
|
fe92abde0e | ||
|
|
098c954ef1 | ||
|
|
01396923f1 | ||
|
|
e0de66b1be | ||
|
|
77675b361f | ||
|
|
e2dd058430 | ||
|
|
a188125982 | ||
|
|
9e5f079cf2 | ||
|
|
51a948bfcf | ||
|
|
9d27d49c1f | ||
|
|
761f6568fa | ||
|
|
ee94b296ae | ||
|
|
b387946d34 | ||
|
|
46afe5153f | ||
|
|
68be87724a | ||
|
|
8c9f2af855 | ||
|
|
594f0b10ba | ||
|
|
79e98db3bd | ||
|
|
a57fd69fb4 | ||
|
|
b73eb9438d | ||
|
|
920e560b4b | ||
|
|
0d33f8b460 | ||
|
|
5b58850c31 | ||
|
|
17746f35f9 | ||
|
|
ca0b211854 | ||
|
|
54cb26ff03 | ||
|
|
a7ff73dbfd | ||
|
|
815dd0f706 | ||
|
|
7455dc93ac | ||
|
|
337662bd40 | ||
|
|
91305771bc | ||
|
|
98ed80d305 | ||
|
|
5313e1861a | ||
|
|
d8665366ef | ||
|
|
c216f29fb0 | ||
|
|
302fde6004 | ||
|
|
14ddf37988 | ||
|
|
87568b6590 | ||
|
|
37aa41afae | ||
|
|
41968918bb | ||
|
|
8fd48a88be | ||
|
|
10c35f354e | ||
|
|
9ee7740fcc | ||
|
|
94b086de20 | ||
|
|
c90696e67e | ||
|
|
8378789f6a | ||
|
|
059bb7622d | ||
|
|
cece83328a | ||
|
|
4a12b0ab2d | ||
|
|
f6e2dd1480 | ||
|
|
f04b5fd42f | ||
|
|
5994cd8ea2 | ||
|
|
83f33a7d1b | ||
|
|
f80e1bd214 | ||
|
|
97672f06de | ||
|
|
6039484a02 | ||
|
|
7682ebd245 | ||
|
|
7c581ec108 | ||
|
|
910d22daa6 | ||
|
|
979102a2d9 | ||
|
|
98be89a20a | ||
|
|
0264383ad2 | ||
|
|
e2ea217bc5 | ||
|
|
fa75c79d34 | ||
|
|
0c86a4e608 | ||
|
|
031585be3f | ||
|
|
92a87a5ed2 | ||
|
|
4c26e597e4 | ||
|
|
5108bf1742 | ||
|
|
6215faa06c | ||
|
|
fee1fed0a1 | ||
|
|
50dcf308a2 | ||
|
|
486e720e00 | ||
|
|
a6c09e2dac | ||
|
|
43e4dc8170 | ||
|
|
5c4d72ec42 | ||
|
|
114806db55 | ||
|
|
0ff7170ab1 | ||
|
|
6b2f084cda | ||
|
|
907106156f | ||
|
|
50a026183d | ||
|
|
716d795970 | ||
|
|
fcfdcd1025 | ||
|
|
af119db1d7 | ||
|
|
122e80fae9 | ||
|
|
8fceffd6fd | ||
|
|
f778c48923 | ||
|
|
19cd3a17df | ||
|
|
ea91a62c89 | ||
|
|
cef791ba1b | ||
|
|
f78a7fa630 | ||
|
|
ac59382b84 | ||
|
|
68175c1cf0 | ||
|
|
aeca8dc5b2 | ||
|
|
e75ef086af | ||
|
|
14a2171035 | ||
|
|
0cdd866393 | ||
|
|
24c1cfbf72 | ||
|
|
31899d2ab9 | ||
|
|
16c44f3a30 | ||
|
|
1b4bde4e78 | ||
|
|
ff9ae57f39 | ||
|
|
71add5a7c2 | ||
|
|
ce2719d77e | ||
|
|
8193a0df63 | ||
|
|
48a5107296 | ||
|
|
ebd589c9cb | ||
|
|
1f15368b7b | ||
|
|
8fe1a76ec6 | ||
|
|
83faf119a9 | ||
|
|
0a05534c84 | ||
|
|
137fbb34d9 | ||
|
|
d45ce19b04 | ||
|
|
7153506ddb | ||
|
|
0483d3ff32 | ||
|
|
7e784ce9a7 | ||
|
|
8343d9cc18 | ||
|
|
db0ecd92ca | ||
|
|
b5140cfecd | ||
|
|
36aea35a92 | ||
|
|
1984436b41 | ||
|
|
8ba2f5f964 | ||
|
|
c923d35a1f | ||
|
|
c550779472 | ||
|
|
90d3c9ced0 | ||
|
|
43cbc09f1f | ||
|
|
feea084c60 | ||
|
|
d403a83a24 | ||
|
|
35fc27cfb0 | ||
|
|
81742565a4 | ||
|
|
923d0b7c80 | ||
|
|
d416465371 | ||
|
|
2586c543d3 | ||
|
|
c32bc26328 | ||
|
|
4074c71b6a | ||
|
|
a7493d1039 | ||
|
|
d7cab6a8d8 | ||
|
|
eddc12693a | ||
|
|
ced3898499 | ||
|
|
318a5df109 | ||
|
|
b2e9981313 | ||
|
|
5f092e37f9 | ||
|
|
81bbef04dc | ||
|
|
74f43639ad | ||
|
|
fc342bd458 | ||
|
|
adfbf5b49f | ||
|
|
2cb7bb84f7 | ||
|
|
84d1792e7f | ||
|
|
531859ac60 | ||
|
|
b5bf0d7e1d | ||
|
|
bf071d65d7 | ||
|
|
e4aa7a90c7 | ||
|
|
6d4e3c5633 | ||
|
|
19f9b4f502 | ||
|
|
2b8837609b | ||
|
|
f3dbb19364 | ||
|
|
0a831ec84e | ||
|
|
0c656abb8e | ||
|
|
7a7a90bf79 | ||
|
|
908dff3931 | ||
|
|
a786cff036 | ||
|
|
28802805f8 | ||
|
|
f59099395f | ||
|
|
0fe3fe7594 | ||
|
|
467dacd35a | ||
|
|
173150591d | ||
|
|
e4d94b1a4e | ||
|
|
75e34a5a8e | ||
|
|
d6121c8e21 | ||
|
|
b4d77df1be | ||
|
|
e6021465f6 | ||
|
|
22ec70e94d | ||
|
|
a1a70a94a8 | ||
|
|
a65ed7e914 | ||
|
|
4545b8e92d | ||
|
|
ba0c0fb109 | ||
|
|
18d530021c | ||
|
|
31bb70e333 | ||
|
|
a919a039e5 | ||
|
|
aacb1f46a8 | ||
|
|
96862cbcb3 | ||
|
|
10f79e1307 | ||
|
|
e0ee3dce40 | ||
|
|
13e7d2e7ac | ||
|
|
a7723373a0 | ||
|
|
7e469ead45 | ||
|
|
5397a4e410 | ||
|
|
99b59f0126 | ||
|
|
d46c7eb8fe | ||
|
|
e4a1fc9d95 | ||
|
|
276f50a944 | ||
|
|
40fcd93312 | ||
|
|
807e4d4af9 | ||
|
|
480348f11a | ||
|
|
30613b7064 | ||
|
|
79189dcc83 | ||
|
|
c2210330b6 | ||
|
|
917f459569 | ||
|
|
0ced9ba799 | ||
|
|
5e95277d7c | ||
|
|
efb417dba7 | ||
|
|
5c2d4c4d9d | ||
|
|
e8bd9920fd | ||
|
|
69ed531a5c | ||
|
|
b967d7c148 | ||
|
|
90150c42ed | ||
|
|
8e2fd9ccce | ||
|
|
567ffad41d | ||
|
|
3c306a0971 | ||
|
|
c0d6c8aeb3 | ||
|
|
b27b49e4f3 | ||
|
|
7ed0dbcf1a | ||
|
|
8a23de6b20 | ||
|
|
6cc3089204 | ||
|
|
093e95c078 | ||
|
|
7c8ac04e35 | ||
|
|
dc88f8b172 | ||
|
|
a00ac6b9ca | ||
|
|
c94f0ded27 | ||
|
|
b553aa2159 | ||
|
|
a7bd2666f0 | ||
|
|
fe2fc60581 | ||
|
|
ce59c05d5b | ||
|
|
a4858bc702 | ||
|
|
a2bb58a991 | ||
|
|
f7b41227d2 | ||
|
|
5b1a6831d5 | ||
|
|
42b1bbe414 | ||
|
|
ac86fe80c8 | ||
|
|
db9f20a22f | ||
|
|
b30e025bda | ||
|
|
5f3eb4871a | ||
|
|
9a223532c5 | ||
|
|
cf67b592da | ||
|
|
e867bfbc82 | ||
|
|
f341f43427 | ||
|
|
9a671851df | ||
|
|
4b92f78cc8 | ||
|
|
c585982557 | ||
|
|
6bf22e7ad0 | ||
|
|
2f8dccf7f6 | ||
|
|
027768d97d | ||
|
|
085f63b8c5 | ||
|
|
6f7c337e00 | ||
|
|
16a968f3bb | ||
|
|
d7e0167fed | ||
|
|
41c4f515cf | ||
|
|
d9a8218372 | ||
|
|
dd9bd4da8b | ||
|
|
cf98500b7f | ||
|
|
2ce8facc05 | ||
|
|
d1b117d07c | ||
|
|
c0377c7ebf | ||
|
|
a2490a5730 | ||
|
|
177334ba62 | ||
|
|
f7f00293cc | ||
|
|
7bce588767 | ||
|
|
4bb67c634f | ||
|
|
3653afbcc4 | ||
|
|
1f4a4ea09f | ||
|
|
3d38add4b4 | ||
|
|
124b7eefb5 | ||
|
|
b52924048c | ||
|
|
93393f5dff | ||
|
|
275a75ebaa | ||
|
|
3e4a7a19cc | ||
|
|
734af457f3 | ||
|
|
55bdb1f47a | ||
|
|
adff0d199d | ||
|
|
f95b3262a0 | ||
|
|
794a14e76c | ||
|
|
ba857b5ef7 | ||
|
|
2aed04a8c2 | ||
|
|
5f9e6b51da | ||
|
|
e7b5c99ed6 | ||
|
|
9c0b3d35be | ||
|
|
a54bc96eab | ||
|
|
a2a8e4b965 | ||
|
|
81ad2c61d9 | ||
|
|
32616493b3 | ||
|
|
05183ffd0f | ||
|
|
e72ddc9439 | ||
|
|
32e3caecac | ||
|
|
df43389183 | ||
|
|
19b77809ec | ||
|
|
be05b827f3 | ||
|
|
5dfc6f822d | ||
|
|
c3e004da03 | ||
|
|
8bae73b6ea | ||
|
|
d1e19d3b63 | ||
|
|
ffca897ddf | ||
|
|
4277b6e262 | ||
|
|
506c4ce701 | ||
|
|
d251e58984 | ||
|
|
4a1213c081 | ||
|
|
8b7609255c | ||
|
|
ef78fe0653 | ||
|
|
70b3ccb422 | ||
|
|
81d6b367fe | ||
|
|
0a78ae60be | ||
|
|
a61830a860 | ||
|
|
86bae9ddc9 | ||
|
|
033780862a | ||
|
|
6094d8a74e | ||
|
|
356ca3d177 | ||
|
|
d69806faa9 | ||
|
|
ab67635dcb | ||
|
|
cee3d49458 | ||
|
|
5b53a7aef7 | ||
|
|
9b29665cc0 | ||
|
|
f447c87b45 | ||
|
|
e3eea45d86 | ||
|
|
f61a06ce0a | ||
|
|
539842aa99 | ||
|
|
5925f1d2aa | ||
|
|
61eb150825 | ||
|
|
cf95de4d27 | ||
|
|
fdad7ec1ba | ||
|
|
850efb4237 | ||
|
|
853cb3887f | ||
|
|
412f2c1664 | ||
|
|
2810a69bd4 | ||
|
|
5347f95f50 | ||
|
|
6b469f0621 | ||
|
|
0021562c93 | ||
|
|
f2bd2b0a59 | ||
|
|
647eb8bbf5 | ||
|
|
816d13ae3f | ||
|
|
578fea4a9c | ||
|
|
1a660d9a4a | ||
|
|
227ac6d9e3 | ||
|
|
bb57407733 | ||
|
|
13ddcce0a2 | ||
|
|
53767a78d1 | ||
|
|
5600e8a2ad | ||
|
|
c6ed52c592 | ||
|
|
3ad14e4adf | ||
|
|
8a22bdea5d | ||
|
|
6135a3c3e2 | ||
|
|
1e3c979303 | ||
|
|
d0228406b6 | ||
|
|
507a2237b7 | ||
|
|
c15c597d99 | ||
|
|
7c26cd3270 | ||
|
|
938af73059 | ||
|
|
1c047366d2 | ||
|
|
cb20f0cbb0 | ||
|
|
468251c84e | ||
|
|
ca86ae0c9a | ||
|
|
59221b0b4e | ||
|
|
d3e0640400 | ||
|
|
bcb72321f5 | ||
|
|
4060af715d | ||
|
|
2ec0237e83 | ||
|
|
c5593880f2 | ||
|
|
3673cbce4f | ||
|
|
1f6f7be4b2 | ||
|
|
580cce3506 | ||
|
|
36ba546fc6 | ||
|
|
7f37799cbe | ||
|
|
5570eeeff9 | ||
|
|
2b186ce6e0 | ||
|
|
72938fed69 | ||
|
|
d54c806e03 | ||
|
|
7eb3551485 | ||
|
|
57abe27895 | ||
|
|
a628a36082 | ||
|
|
0d3e04ff25 | ||
|
|
0c78a3f7b0 | ||
|
|
fb1f574c26 | ||
|
|
7f15c18fca | ||
|
|
e274650956 | ||
|
|
2a1db4a338 | ||
|
|
0b6ea9ec61 | ||
|
|
257a826d45 | ||
|
|
e9f48f5134 | ||
|
|
fcf04624d4 | ||
|
|
16218d6dc5 | ||
|
|
071f33e3cd | ||
|
|
6d15389da8 | ||
|
|
2e8530ec00 | ||
|
|
4edd1c5497 | ||
|
|
8e693b8b42 | ||
|
|
29376066e8 | ||
|
|
a44f3071bf | ||
|
|
b66047e084 | ||
|
|
f0ca916432 | ||
|
|
c88b4032ef | ||
|
|
6f5e99be6f | ||
|
|
fd4c37e9b3 | ||
|
|
7a8dab2d58 | ||
|
|
6f3dfad550 | ||
|
|
18fb0a13d7 | ||
|
|
e88f9ae03b | ||
|
|
30c010ad3f | ||
|
|
e84e70bdc6 | ||
|
|
1e6b6165ae | ||
|
|
c6d149d091 | ||
|
|
d55d8d78de | ||
|
|
eff59f7b5e | ||
|
|
a7a5437245 | ||
|
|
6671b9e55b | ||
|
|
2dde1cc589 | ||
|
|
17866c29ae | ||
|
|
8dc4e6dc2a | ||
|
|
1197f44262 | ||
|
|
a86ed1f801 | ||
|
|
e98d3423e4 | ||
|
|
95333d37c8 | ||
|
|
8bcf0c6498 | ||
|
|
340b92e32b | ||
|
|
6e68ab19f9 | ||
|
|
15fed32d92 | ||
|
|
897c754dd4 | ||
|
|
ec1e746a22 | ||
|
|
001f078ba9 | ||
|
|
c0ff1e86b9 | ||
|
|
b5321152fd | ||
|
|
66d15ea635 | ||
|
|
72177033d2 | ||
|
|
06b7072240 | ||
|
|
4700f35739 | ||
|
|
bbfa280e86 | ||
|
|
2669ba944d | ||
|
|
c8788dbfbe | ||
|
|
5e7c3b53f8 | ||
|
|
99348c2300 | ||
|
|
ab2b9797fd | ||
|
|
637653ea11 | ||
|
|
04cb6ba3d0 | ||
|
|
eb1cddd85a | ||
|
|
723b230093 | ||
|
|
7185fae491 | ||
|
|
0274cd6beb | ||
|
|
ad2ea0b807 | ||
|
|
c24999075d | ||
|
|
773bde14ab | ||
|
|
00b08318a5 | ||
|
|
39e5d8ccc2 | ||
|
|
e25622df4b | ||
|
|
ea5939c1b7 | ||
|
|
4734d04d4f | ||
|
|
493e47f7e6 | ||
|
|
ef2b32eb05 | ||
|
|
1da91d44e1 | ||
|
|
ebd7ab3e46 | ||
|
|
99bddfdf0a | ||
|
|
144f48c9a6 | ||
|
|
8d0d2ba07b | ||
|
|
a6ad334dc0 | ||
|
|
468ca30070 | ||
|
|
7b2d2d9338 | ||
|
|
0e08819cf3 | ||
|
|
1d3f7b49dc | ||
|
|
3566ec7012 | ||
|
|
f37a36efa4 | ||
|
|
2ea069cd8c | ||
|
|
08e111f6dc | ||
|
|
6339881684 | ||
|
|
e222538575 | ||
|
|
c4a67ce420 | ||
|
|
77e348ba62 | ||
|
|
bde39d8c37 | ||
|
|
ebb906c273 | ||
|
|
46b91bf8b0 | ||
|
|
7a432b38e9 | ||
|
|
9b6a201bbb | ||
|
|
a79d7c8417 | ||
|
|
7a6e0d651f | ||
|
|
7476498823 | ||
|
|
4c7b5d44a0 | ||
|
|
620bb54881 | ||
|
|
73be747cbe | ||
|
|
4d874451c9 | ||
|
|
3245d620c3 | ||
|
|
a59f80589a | ||
|
|
6b269c7559 | ||
|
|
53cadeab61 | ||
|
|
e90d388fdb | ||
|
|
219f059834 | ||
|
|
dc2dac66a3 | ||
|
|
e04ee666b8 | ||
|
|
7d27003bb2 | ||
|
|
b866c9dd08 | ||
|
|
d17236fe45 | ||
|
|
d2580ec87c | ||
|
|
7c10f414dc | ||
|
|
2a4717cb7f | ||
|
|
be9cb8a4da | ||
|
|
e887363910 | ||
|
|
a274159726 | ||
|
|
83384e0de4 | ||
|
|
748904b8ad | ||
|
|
3e72df8b1e | ||
|
|
2921563e9c | ||
|
|
01c1346696 | ||
|
|
796e0456ef | ||
|
|
eeb68497fe | ||
|
|
7eadb6acad | ||
|
|
9d588aa7e7 | ||
|
|
204b5f7f09 | ||
|
|
e0f53b63ce | ||
|
|
83f4dbe40e | ||
|
|
8d8ba68838 | ||
|
|
3f25940dec | ||
|
|
745773b207 | ||
|
|
35f5575595 | ||
|
|
e4746f8b32 | ||
|
|
7e0552efde | ||
|
|
6075b98634 | ||
|
|
ebe9f518d0 | ||
|
|
37ceddd11b | ||
|
|
d1d8b911b9 | ||
|
|
796e656328 | ||
|
|
8b869915e7 | ||
|
|
9b05243d61 | ||
|
|
81c24510a8 | ||
|
|
9e7fb4d21a | ||
|
|
7805f8a9b1 | ||
|
|
ae7f04578d | ||
|
|
ce814cffd1 | ||
|
|
703b310ef0 | ||
|
|
da6c4ad36a | ||
|
|
a8c849d38a | ||
|
|
d8d5e04a51 | ||
|
|
fa348cb98f | ||
|
|
f4ec2d8107 | ||
|
|
de39d828de | ||
|
|
25d3d0d0ba | ||
|
|
6f3b1000a7 | ||
|
|
1ebb8d8d14 | ||
|
|
4e4acdaecc | ||
|
|
f7fb03bf56 | ||
|
|
429aafc7ba | ||
|
|
acdfede2a8 | ||
|
|
b822c5a039 | ||
|
|
8366c4c165 | ||
|
|
4c7260b043 | ||
|
|
c878f7dc25 | ||
|
|
aca21f6ef2 | ||
|
|
10c582bafb | ||
|
|
7b3bd26631 | ||
|
|
731f88da84 | ||
|
|
b7fb9a65b6 | ||
|
|
843c24b17a | ||
|
|
18dbbfc95a | ||
|
|
5b6e187b49 | ||
|
|
9025a9b88c | ||
|
|
07b2891671 | ||
|
|
c4a739bef6 | ||
|
|
1008c74cd7 | ||
|
|
60dc9d27bc | ||
|
|
9eb0f48a7a | ||
|
|
7b1fccdd06 | ||
|
|
64ae07b03b | ||
|
|
6ecbbd1f79 | ||
|
|
a1fb268764 | ||
|
|
868661edf0 | ||
|
|
9899e63d53 | ||
|
|
0b37b8b059 | ||
|
|
1b34ca822f | ||
|
|
a4e3a874ad | ||
|
|
8ec3df552a | ||
|
|
b4b1c9256b | ||
|
|
acee20d897 | ||
|
|
6ea3ebb72d | ||
|
|
55f23e9304 | ||
|
|
ad223a04f8 | ||
|
|
0b150ea475 | ||
|
|
167e9fbc6d | ||
|
|
77ea160cd9 | ||
|
|
f9204450f1 | ||
|
|
21ef76816f | ||
|
|
f166cfbac8 | ||
|
|
e5f64710f4 | ||
|
|
32a5062081 | ||
|
|
e6bc29281e | ||
|
|
617ee0afc0 | ||
|
|
1b47a1a994 | ||
|
|
5a87cfc25d | ||
|
|
00a178f7d3 | ||
|
|
2a2c82e73b | ||
|
|
bb882ada2c | ||
|
|
1d42e45d78 | ||
|
|
15c4a5c9ea | ||
|
|
f4435f9031 | ||
|
|
5a423c89a3 | ||
|
|
8b02154f5a | ||
|
|
97c454ea77 | ||
|
|
f07a6d03b5 | ||
|
|
1f18fb5446 | ||
|
|
92ee5b66ab | ||
|
|
fcc92c3e27 | ||
|
|
3e91b5a793 | ||
|
|
42e5cc3bef | ||
|
|
16c61a1919 | ||
|
|
c2b4b0490b | ||
|
|
a310a06e3c | ||
|
|
9228511527 | ||
|
|
bbc4174501 | ||
|
|
5f3196b74c | ||
|
|
725bd8029f | ||
|
|
479ab5df0e | ||
|
|
18c45ad30b | ||
|
|
6f32f098eb | ||
|
|
e8289d3912 | ||
|
|
472d9322ce | ||
|
|
0233ffafb6 | ||
|
|
468ee4756f | ||
|
|
abf9365bbe | ||
|
|
1e0789162f | ||
|
|
31f407f4e8 | ||
|
|
77330ffc50 | ||
|
|
6f132f3fed | ||
|
|
c193b4f07c | ||
|
|
c745b845c5 | ||
|
|
3b69e0dd25 | ||
|
|
8ec55ef394 | ||
|
|
ef5084036c | ||
|
|
7dd317e530 | ||
|
|
e5db3ed9b7 | ||
|
|
3a00dc5b5f | ||
|
|
2d848020fc | ||
|
|
70123d19fe | ||
|
|
ea3770260a | ||
|
|
eea1a80de6 | ||
|
|
4889775ae6 | ||
|
|
355effd93d | ||
|
|
f1583b6e0c | ||
|
|
347566c311 | ||
|
|
1f73572dd3 | ||
|
|
2bfb83c4cd | ||
|
|
ceed1c4962 | ||
|
|
2b5b9d3599 | ||
|
|
96e3709b7b | ||
|
|
239fc2f6f8 | ||
|
|
4c77e5cdd2 | ||
|
|
974f8f692c | ||
|
|
e97d0b9a69 | ||
|
|
b0b0a75c87 | ||
|
|
abcacf8c74 | ||
|
|
290428b981 | ||
|
|
37d1541d6b | ||
|
|
1500ce7490 | ||
|
|
a48529872d | ||
|
|
31cffa68c5 | ||
|
|
6909d1e527 | ||
|
|
972235bfba | ||
|
|
6db560fd2c | ||
|
|
1a64d8aec9 | ||
|
|
1e1fb32558 | ||
|
|
008eb5ba4a | ||
|
|
f46e0acc89 | ||
|
|
b615ef5810 | ||
|
|
d773279de8 | ||
|
|
56d721651a | ||
|
|
ee17abff92 | ||
|
|
952bb1a2eb | ||
|
|
c287813e00 | ||
|
|
eab4fd80d7 | ||
|
|
5c8f8869d4 | ||
|
|
4954dfe107 | ||
|
|
4eb8094fb8 | ||
|
|
1dd2423a0b | ||
|
|
6fcf989c62 | ||
|
|
67d1a4f643 | ||
|
|
3df9433baf | ||
|
|
b12d568147 | ||
|
|
8691e035a0 | ||
|
|
2683043762 | ||
|
|
be5aa59f61 | ||
|
|
9398dfb7cf | ||
|
|
ff6d2b30e4 | ||
|
|
29bb999a32 | ||
|
|
9320507e26 | ||
|
|
181fc4fa0a | ||
|
|
4f3dd4b662 | ||
|
|
7b09de99ea | ||
|
|
f9f0da18e1 | ||
|
|
ba0fdb9478 | ||
|
|
8c684bca22 | ||
|
|
1266a75549 | ||
|
|
3871d5aed7 | ||
|
|
25d555126d | ||
|
|
f529d15d7a | ||
|
|
9eda30fc71 | ||
|
|
dfd6424d9c | ||
|
|
fdc961f2de | ||
|
|
a6d4000d24 | ||
|
|
e1024e59c3 | ||
|
|
990164802d | ||
|
|
34f18fbdb3 | ||
|
|
17e24bb038 | ||
|
|
df7e2b7734 | ||
|
|
1ddef06bd2 | ||
|
|
18bd910bf0 | ||
|
|
0db44f6e33 | ||
|
|
7aac3d38f0 | ||
|
|
e406b6f780 | ||
|
|
2dad9666a9 | ||
|
|
063abf1688 | ||
|
|
a86f8e9a22 | ||
|
|
0ce6d4fe92 | ||
|
|
886f6c721c | ||
|
|
9d7d089279 | ||
|
|
0bd624dfa9 | ||
|
|
8c29760d93 | ||
|
|
9b893d841d | ||
|
|
e8c0163153 | ||
|
|
256568d966 | ||
|
|
34bed47a52 | ||
|
|
9b0996fade | ||
|
|
7c1028df5d | ||
|
|
eaea60e0cb | ||
|
|
efab05dcfc | ||
|
|
d79f77f7e0 | ||
|
|
f0d459d490 | ||
|
|
e269c073ac | ||
|
|
85d5609144 | ||
|
|
671c593db1 | ||
|
|
8b14c7a2cb | ||
|
|
23862419eb | ||
|
|
43d54db4dd | ||
|
|
b8b0060440 | ||
|
|
64d79ceb30 | ||
|
|
25f7b44d48 | ||
|
|
ada7e628da | ||
|
|
76f2338c3d | ||
|
|
a0ed8036c0 | ||
|
|
863ce65b10 | ||
|
|
85190b16cb | ||
|
|
abc6fd8b2a | ||
|
|
b6f603154e | ||
|
|
90cb9d3de1 | ||
|
|
d47c9a2e29 | ||
|
|
b7aea96ca0 | ||
|
|
a5879a4407 | ||
|
|
0452c69771 | ||
|
|
4eb9dff45e | ||
|
|
b7c1e88b59 | ||
|
|
23814330d9 | ||
|
|
8bc75aacea | ||
|
|
17576d223a | ||
|
|
90e8f0ca63 | ||
|
|
adc4a811b7 | ||
|
|
7c80233f26 | ||
|
|
f05ae2de35 | ||
|
|
cf9da556a8 | ||
|
|
32a142bf79 | ||
|
|
2680d41a3d | ||
|
|
1550fc4398 | ||
|
|
f2a85f3b7e | ||
|
|
49c2b4c196 | ||
|
|
a8281e174e | ||
|
|
d7f5c8bd55 | ||
|
|
b3337df88b | ||
|
|
d760616e55 | ||
|
|
914a4d32b4 | ||
|
|
148f53e21e | ||
|
|
5e7fa0f964 | ||
|
|
0973ceb9d2 | ||
|
|
5214bfe8cb | ||
|
|
187aaafddc | ||
|
|
208cb405ca | ||
|
|
9b9d267cd4 | ||
|
|
6f90a27f9f | ||
|
|
4e1dddc06d | ||
|
|
1f504d6f23 | ||
|
|
2db1fd813f | ||
|
|
f39383a3d8 | ||
|
|
b593bdbf1b | ||
|
|
4de93ba3c0 | ||
|
|
2eb7a91987 | ||
|
|
94e4264c2a | ||
|
|
e54d28f157 | ||
|
|
6111c8bde0 | ||
|
|
3bacdfd4fc | ||
|
|
65db645ff9 | ||
|
|
de2e2c45a5 | ||
|
|
9b655e18e3 | ||
|
|
fb4b9b5f76 | ||
|
|
820b39840a | ||
|
|
4b1052eb70 | ||
|
|
c0eb3972a7 | ||
|
|
66ba8d56b7 | ||
|
|
a73baf32f1 | ||
|
|
333cf0a2f0 | ||
|
|
8347d8700a | ||
|
|
09af0e2448 | ||
|
|
001914764a | ||
|
|
6938dd6267 | ||
|
|
941028ba6f | ||
|
|
4ca7ed9f8c | ||
|
|
03d99887c5 | ||
|
|
293e2ff5e3 | ||
|
|
55d242fa08 | ||
|
|
7e9fba2d96 | ||
|
|
175652f23b | ||
|
|
3329e0c4a1 | ||
|
|
3c5ed2c885 | ||
|
|
c4af93c363 | ||
|
|
e2685c4503 | ||
|
|
48b1d3fff8 | ||
|
|
6c4920949d | ||
|
|
69447b75af | ||
|
|
e1104570a9 | ||
|
|
2c23678fb9 | ||
|
|
613070d39f | ||
|
|
6deae64f45 | ||
|
|
aced2b124c | ||
|
|
e6b08de2e8 | ||
|
|
d2d02d0749 | ||
|
|
28d27801b2 | ||
|
|
be340dd275 | ||
|
|
58090fb3de | ||
|
|
ae33c6cf18 | ||
|
|
f8cd6afbf8 | ||
|
|
6fce06906d | ||
|
|
84694a8bbd | ||
|
|
1639e68424 | ||
|
|
ff48fe8b49 | ||
|
|
efe06267ec | ||
|
|
53647ea5a8 | ||
|
|
7742de5af4 | ||
|
|
724a260f71 | ||
|
|
cf75e40332 | ||
|
|
3c67df263c | ||
|
|
c4ae72c3c1 | ||
|
|
f6925fc5b8 | ||
|
|
18be9655d6 | ||
|
|
6235b6123e | ||
|
|
b3555385e6 | ||
|
|
c9fbdb322b | ||
|
|
94bac7d8db | ||
|
|
f38119be96 | ||
|
|
bc1d2ba839 | ||
|
|
18f5b70b1f | ||
|
|
7df9b07305 | ||
|
|
cf01c1fd1f | ||
|
|
2ce6fe420b | ||
|
|
f55381d689 | ||
|
|
c4084c4f97 | ||
|
|
58b720b004 | ||
|
|
f6d0c1f05e | ||
|
|
f4620be859 | ||
|
|
e2b3a98690 | ||
|
|
4cd391d5ef | ||
|
|
01c37c34dd | ||
|
|
f4827cde0e | ||
|
|
3e722295b0 | ||
|
|
0d2eab3ad4 | ||
|
|
9e1bc631cf | ||
|
|
ca9fbe2f11 | ||
|
|
2e28fad102 | ||
|
|
69760200dd | ||
|
|
f945ee1288 | ||
|
|
5e3486c481 | ||
|
|
c15a943cf4 | ||
|
|
b8cb29c66c | ||
|
|
003badcb5a | ||
|
|
5b2493fa68 | ||
|
|
bc342b9b33 | ||
|
|
314615bfef | ||
|
|
44e82217c1 | ||
|
|
cbf364f24f | ||
|
|
44dfcb927b | ||
|
|
12f615c6da | ||
|
|
ed6fc4d848 | ||
|
|
cd515993f5 | ||
|
|
a918eaac3f | ||
|
|
9f63e2d39a | ||
|
|
36248ff046 | ||
|
|
a88f5113e0 | ||
|
|
06fb89fae2 | ||
|
|
733531356f | ||
|
|
3f8fb30066 | ||
|
|
1a4a2d2b30 | ||
|
|
eb6d3b3f8d | ||
|
|
e8e3363d06 | ||
|
|
dd55ad61f4 | ||
|
|
2464bfd70b | ||
|
|
09bc36bb13 | ||
|
|
39da89b556 | ||
|
|
c23de4b3b0 | ||
|
|
7c79d7f5d7 | ||
|
|
710507da51 | ||
|
|
10e95bf1b1 | ||
|
|
66a893c84e | ||
|
|
dd6392e380 | ||
|
|
ea0a0c7c5a | ||
|
|
b8f7ba62c7 | ||
|
|
25b318ba00 | ||
|
|
9add51b59d | ||
|
|
535a0504d8 | ||
|
|
365c49d6d2 | ||
|
|
b70bea48f2 | ||
|
|
996f8644c4 | ||
|
|
b3882ec6e3 | ||
|
|
f87d447397 | ||
|
|
9d8570d0d2 | ||
|
|
796755dad8 | ||
|
|
9387753995 | ||
|
|
618d36dc07 | ||
|
|
f11b0be483 | ||
|
|
1b8b15b136 | ||
|
|
bb63673cce | ||
|
|
6770ad68d5 | ||
|
|
bfe90c58d1 | ||
|
|
f1cbeb3c29 | ||
|
|
554ab4ea16 | ||
|
|
a2becac2e6 | ||
|
|
67a651f5e9 | ||
|
|
ac8efe19d8 | ||
|
|
ffd65d5afa | ||
|
|
e86677178f | ||
|
|
3c49a3341a | ||
|
|
3433b2a73e | ||
|
|
730988e7b7 | ||
|
|
2a3b89e596 | ||
|
|
f1a31bf58c | ||
|
|
f1b62a9056 | ||
|
|
dd943d24c8 | ||
|
|
801320a3f3 | ||
|
|
222ed2debd | ||
|
|
7aab782c5f | ||
|
|
3836f2f353 | ||
|
|
5bfaa9a5db | ||
|
|
95581771d6 | ||
|
|
db5e3f2479 | ||
|
|
b5a9631bcc | ||
|
|
4a2d62ece0 | ||
|
|
c3836decee | ||
|
|
f7a030c895 | ||
|
|
f171a692d3 | ||
|
|
df5e73192b | ||
|
|
fc1447d614 | ||
|
|
77fd206b06 | ||
|
|
23bdc03490 | ||
|
|
9fe4de5709 | ||
|
|
54fd601809 | ||
|
|
63d54e6570 | ||
|
|
71d027a966 | ||
|
|
8b63aa2fe6 | ||
|
|
5a35842c28 | ||
|
|
d6a1ae3b3a | ||
|
|
be5f4cb562 | ||
|
|
d8b5464833 | ||
|
|
5e7bbcd3bc | ||
|
|
5383e53c4d | ||
|
|
5b6fc713d6 | ||
|
|
272be025e1 | ||
|
|
e4ab250729 | ||
|
|
4dcca9d5af | ||
|
|
1988a08631 | ||
|
|
34de0e569f | ||
|
|
343d0fa09d | ||
|
|
8eb6686103 | ||
|
|
9e9687b5b8 | ||
|
|
ef888d1afe | ||
|
|
0e70e1a37a | ||
|
|
06aaceb673 | ||
|
|
703a4b7858 | ||
|
|
32ba2ba83d | ||
|
|
272b03ed92 | ||
|
|
ecf19214ee | ||
|
|
f3eb0c497f | ||
|
|
c1f29a7565 | ||
|
|
fb745b9108 | ||
|
|
9410bf40d3 | ||
|
|
c7a695cb04 | ||
|
|
b991d5cab6 | ||
|
|
42fd318321 | ||
|
|
903aeec383 | ||
|
|
8768fe4dcf | ||
|
|
d8ba2ceed4 | ||
|
|
ef8a1bcf47 | ||
|
|
2b1469e02e | ||
|
|
83ea91586b | ||
|
|
dbb86d25e1 | ||
|
|
794c74e514 | ||
|
|
fbcdaa77e3 | ||
|
|
dbdc04c45e | ||
|
|
a4bb22280f | ||
|
|
c0e1bbbfb6 | ||
|
|
196b9dc771 | ||
|
|
09578b4e46 | ||
|
|
4d88dadf8c | ||
|
|
d4fda5847d | ||
|
|
b1ea7d6cbc | ||
|
|
4e7632949d | ||
|
|
26a8bd147b | ||
|
|
dd726fac02 | ||
|
|
3a3ecc7775 | ||
|
|
6665d630ec | ||
|
|
7706d7471a | ||
|
|
ed87d6b268 | ||
|
|
ed51c8b318 | ||
|
|
6ffbb7b1ed | ||
|
|
06764db118 | ||
|
|
4864fa3f2d | ||
|
|
2d25b6a1f4 | ||
|
|
be76b3d105 | ||
|
|
81cbeb4b24 | ||
|
|
f0b658ba14 | ||
|
|
295836fc7e | ||
|
|
54e9858148 | ||
|
|
b68f015825 | ||
|
|
87ae26ede3 | ||
|
|
49615f81b4 | ||
|
|
87ce5140fa | ||
|
|
3ba9fb375c | ||
|
|
f4bd20361a | ||
|
|
54f8a17aac | ||
|
|
7a1e5026c4 | ||
|
|
323161c6de | ||
|
|
1ac4890893 | ||
|
|
e9c88fecc5 | ||
|
|
33deaaefac | ||
|
|
bb6438ebe4 | ||
|
|
d42af74afa | ||
|
|
ea1f2f4ad4 | ||
|
|
95b45651bb | ||
|
|
0d5730d33e | ||
|
|
0625a35ddf | ||
|
|
2d3271ee13 | ||
|
|
6da2e80027 | ||
|
|
439edbf85c | ||
|
|
e0237a0b86 | ||
|
|
e1845ba603 | ||
|
|
1cf757d401 | ||
|
|
14985b1727 | ||
|
|
6b2788be57 | ||
|
|
ac888f4cb2 | ||
|
|
df06cfc4c5 | ||
|
|
dcba3a681c | ||
|
|
7fd49c22a8 | ||
|
|
314287a6d9 | ||
|
|
f5e7b8f229 | ||
|
|
de84db070e | ||
|
|
c1d5a5cd98 | ||
|
|
160a04c3c7 | ||
|
|
23bfc30c57 | ||
|
|
d9329bffd1 | ||
|
|
bafc1df988 | ||
|
|
30b8835919 | ||
|
|
44b19e75f6 | ||
|
|
2a558ad11d | ||
|
|
123d8972e1 | ||
|
|
e550a8ea27 | ||
|
|
8b29460fed | ||
|
|
e380d63c57 | ||
|
|
89b4f2c4d4 | ||
|
|
6c2f63f738 | ||
|
|
77c612f0f5 | ||
|
|
2d06c01192 | ||
|
|
d13c19f05f | ||
|
|
3d2ba05c77 | ||
|
|
6898b9d9a4 | ||
|
|
a65aaa6b83 | ||
|
|
b3136c20c4 | ||
|
|
0ae3dfd9cc | ||
|
|
0370fa6c00 | ||
|
|
7ab323b00c | ||
|
|
541eb70b9c | ||
|
|
e53e5ca20e | ||
|
|
609bf64856 | ||
|
|
a9fafe91a5 | ||
|
|
0466b320dd | ||
|
|
5b74d22d0a | ||
|
|
4152c7f956 | ||
|
|
d5f603303d | ||
|
|
fc9c073a60 | ||
|
|
9a0c2c40bd | ||
|
|
d0fc9fda71 | ||
|
|
df9823988e | ||
|
|
eeb09c074c | ||
|
|
af0928e2bd | ||
|
|
d9cf4de3f7 | ||
|
|
2d6dd4b3be | ||
|
|
160312393a | ||
|
|
e3ff9f9c86 | ||
|
|
95570d796d | ||
|
|
cd0d58a915 | ||
|
|
2f1007c725 | ||
|
|
b53d5d8c00 | ||
|
|
3c4a4e5384 | ||
|
|
0e5f85db95 | ||
|
|
ad3364671d | ||
|
|
3add24b8aa | ||
|
|
e0f02d4080 | ||
|
|
00c4c10472 | ||
|
|
a2b8cc9dc2 | ||
|
|
3465002cbb | ||
|
|
631cb73305 | ||
|
|
b3b6384bef | ||
|
|
941ca575fb | ||
|
|
2d65c3595d | ||
|
|
b3812d913a | ||
|
|
adcc420c81 | ||
|
|
b97ad99bb4 | ||
|
|
e93a2850d6 | ||
|
|
411d0691fa | ||
|
|
c843e77183 | ||
|
|
093d6e5336 | ||
|
|
8a3c752d42 | ||
|
|
b4e073cde7 | ||
|
|
814efbf8df | ||
|
|
11e048abb1 | ||
|
|
34e7855af6 | ||
|
|
de54dc27ad | ||
|
|
790133978d | ||
|
|
b914d67d9d | ||
|
|
518eb97e3a | ||
|
|
f41549ccf1 | ||
|
|
b69e477ecd | ||
|
|
0062ff9cfa | ||
|
|
f8de72f59f | ||
|
|
7317737e90 | ||
|
|
5b8eda4805 | ||
|
|
886a949a00 | ||
|
|
92e13dafe5 | ||
|
|
c9be812330 | ||
|
|
59e7ebabfa | ||
|
|
1afc48fce3 | ||
|
|
a1e4ef9e8e | ||
|
|
5ada0ae2c7 | ||
|
|
a5312c1341 | ||
|
|
150e156d26 | ||
|
|
6d38615ea8 | ||
|
|
011cc7d337 | ||
|
|
b747d09836 | ||
|
|
4b7311bafd | ||
|
|
eeba9c0a5f | ||
|
|
11d9a037f7 | ||
|
|
883e4fcd7c | ||
|
|
2017e6a3e3 | ||
|
|
bccfe500b3 | ||
|
|
5846fbabce | ||
|
|
52e89c1d1c | ||
|
|
1605e50cef | ||
|
|
2215ce58a4 | ||
|
|
a13e6b69e3 | ||
|
|
1d6370e11c | ||
|
|
bd34c7ede3 | ||
|
|
bc8954fbba | ||
|
|
9cf0bc6c82 | ||
|
|
71b32fe641 | ||
|
|
530f745e44 | ||
|
|
a5a2313851 | ||
|
|
4e98c2e7f6 | ||
|
|
14486782dc | ||
|
|
31814b70da | ||
|
|
408e819d32 | ||
|
|
622676f9bc | ||
|
|
5b631e0387 | ||
|
|
06d54ef77e | ||
|
|
6c5ef567ed | ||
|
|
f86b40302d | ||
|
|
273c287fbf | ||
|
|
6cb16be5df | ||
|
|
a4feb3fc09 | ||
|
|
ba6c7de35a | ||
|
|
79e2bb382f | ||
|
|
0090256ded | ||
|
|
00ce077758 | ||
|
|
a801d0994f | ||
|
|
628575dc5f | ||
|
|
0a22f21410 | ||
|
|
97ff9e9c5b | ||
|
|
8b3a09306b | ||
|
|
7766fd13fd | ||
|
|
c79997ebe3 | ||
|
|
0fd1e2fcd9 | ||
|
|
99442b6e04 | ||
|
|
b8a35e9e4a | ||
|
|
e833d415e3 | ||
|
|
8030312924 | ||
|
|
a84b54f940 | ||
|
|
c66c81294e | ||
|
|
bfdc215c65 | ||
|
|
e10c7beedb | ||
|
|
18c3286364 | ||
|
|
8d2ec30818 | ||
|
|
e5ffddfc6b | ||
|
|
59fc1e4b5f | ||
|
|
7ead581953 | ||
|
|
b860980df4 | ||
|
|
97a366d62e | ||
|
|
9ad68097d0 | ||
|
|
331999fb95 | ||
|
|
6519d7051d | ||
|
|
552d585fca | ||
|
|
24c24d6c72 | ||
|
|
b7f50c3e12 | ||
|
|
aed1687a45 | ||
|
|
daa427dc15 | ||
|
|
e9d4303fdb | ||
|
|
5485e994ee | ||
|
|
87228673b4 | ||
|
|
d306513319 | ||
|
|
e08480f345 | ||
|
|
13c9096417 | ||
|
|
d3d65c8e3a | ||
|
|
12ac5ef781 | ||
|
|
adef9a8acf | ||
|
|
5ef407d15f | ||
|
|
970b636eb4 | ||
|
|
fcf9131aae | ||
|
|
7fd27fac45 | ||
|
|
6e17af91fb | ||
|
|
68555573ad | ||
|
|
fb9905a89e | ||
|
|
1e7504dc5a | ||
|
|
d7af019511 | ||
|
|
04bb070afa | ||
|
|
e693d80857 | ||
|
|
45ae05f1b5 | ||
|
|
05a83beb44 | ||
|
|
8a1a42e83b | ||
|
|
1b3f3cedb3 | ||
|
|
f815ae5973 | ||
|
|
fb7035bf22 | ||
|
|
02bcbc3221 | ||
|
|
f0a51d4ab4 | ||
|
|
24fe8fe9a0 | ||
|
|
244e95d959 | ||
|
|
ad72c64e32 | ||
|
|
767ac6a51b | ||
|
|
3a6f87659a | ||
|
|
b7ac16c7d9 | ||
|
|
f9f84cbd89 | ||
|
|
701c87eefa | ||
|
|
5379cf0544 | ||
|
|
6b5c37f17f | ||
|
|
50b2fad180 | ||
|
|
b9cb65b24e | ||
|
|
d7574973e9 | ||
|
|
ff48c93d59 | ||
|
|
d2e6700dd1 | ||
|
|
05b8c3f35f | ||
|
|
70b643e7ba | ||
|
|
dce973a519 | ||
|
|
eb2f75579a | ||
|
|
773316ce4f | ||
|
|
5fd7ae33b4 | ||
|
|
13a065f2dc | ||
|
|
1a8ff81087 | ||
|
|
65637fce40 | ||
|
|
b11fa7a28e | ||
|
|
45408caf33 | ||
|
|
963ee4dbab | ||
|
|
3b46d5a440 | ||
|
|
212fddd8e1 | ||
|
|
433485470e | ||
|
|
e160a1f794 | ||
|
|
7911b7e637 | ||
|
|
7fc5a77e7e | ||
|
|
f0fb55640e | ||
|
|
d5685f2255 | ||
|
|
a732233db6 | ||
|
|
a5918c29ee | ||
|
|
3b719803bb | ||
|
|
d77463c9f1 | ||
|
|
2d8fd9bedf | ||
|
|
b17a667a9d | ||
|
|
b7287a070b | ||
|
|
fbb5c8cdd6 | ||
|
|
baaf2815e4 | ||
|
|
d8b5549fd9 | ||
|
|
6de03f2bf0 | ||
|
|
caf7c55069 | ||
|
|
7d499ffba1 | ||
|
|
4abf6b2f5c | ||
|
|
a842b06301 | ||
|
|
0bca4925d7 | ||
|
|
ae3953cbec | ||
|
|
a99667c54c | ||
|
|
465963a8c2 | ||
|
|
b9b4762faf | ||
|
|
a56b128a4b | ||
|
|
d43cc089fd | ||
|
|
771513d287 | ||
|
|
c387678217 | ||
|
|
847368718b | ||
|
|
458b3daac3 | ||
|
|
3ea5278b12 | ||
|
|
20b9748a8c | ||
|
|
79487adbec | ||
|
|
89f3fca6b1 | ||
|
|
f290b2bf5a | ||
|
|
04e7d13043 | ||
|
|
e41218c46b | ||
|
|
8562fbdbbe | ||
|
|
c841d7a32b | ||
|
|
8827ae4d2c | ||
|
|
2e1029e157 | ||
|
|
a7dc0c2d55 | ||
|
|
7f1749d853 | ||
|
|
b4a34d58db | ||
|
|
4a50fcab2c | ||
|
|
c9ef089199 | ||
|
|
94ecf9a081 | ||
|
|
67aaa9a655 | ||
|
|
823f5640f7 | ||
|
|
45d1c63895 | ||
|
|
d3b6781bb8 | ||
|
|
21d1f69d6d | ||
|
|
1b9f5989ef | ||
|
|
348e46ff3b | ||
|
|
7918e3a1aa | ||
|
|
d6d8c7830c | ||
|
|
f53a0f0d07 | ||
|
|
17edd1c3d4 | ||
|
|
b0685c153a | ||
|
|
a5ca20ee4c | ||
|
|
03685db2fc | ||
|
|
68ed738dcd | ||
|
|
5293d17e32 | ||
|
|
f2e4b69466 | ||
|
|
ec8b00042b | ||
|
|
08db1d59e5 | ||
|
|
7c79d421e8 | ||
|
|
7385aa09a8 | ||
|
|
185a5fad88 | ||
|
|
a1e2477d14 | ||
|
|
a1200a5fff | ||
|
|
91a0257c8f | ||
|
|
53ffc82fe2 | ||
|
|
801267df18 | ||
|
|
6e73e0b395 | ||
|
|
7aa8a5c368 | ||
|
|
3ecbbea7cb | ||
|
|
77cd3182f1 | ||
|
|
c7ccf9bab8 | ||
|
|
06e70abb86 | ||
|
|
88c86e88b0 | ||
|
|
2d6cf48532 | ||
|
|
19e152a54b | ||
|
|
2898bead66 | ||
|
|
381c329441 | ||
|
|
a3de3705f7 | ||
|
|
dc3dc6b77f | ||
|
|
e028a63f30 | ||
|
|
d196f8b4b2 | ||
|
|
4274827dbe | ||
|
|
7a30f4a7d2 | ||
|
|
d0c03a0211 | ||
|
|
787b136d13 | ||
|
|
08412d6108 | ||
|
|
d8f7db4715 | ||
|
|
bff238774e | ||
|
|
d2aaa6f691 | ||
|
|
b2164ce5fc | ||
|
|
5cc60ed760 | ||
|
|
a7fbe05a73 | ||
|
|
c796e2ae3c | ||
|
|
398cbe9284 | ||
|
|
d87e488c23 | ||
|
|
5c2ff9b777 | ||
|
|
6d7e37610c | ||
|
|
a47e6dd8c5 | ||
|
|
f334a2740f | ||
|
|
26e487c01a | ||
|
|
cc438fdb7b | ||
|
|
92ff98d99a | ||
|
|
d1609cba90 | ||
|
|
0c394b123c | ||
|
|
421b8214cb | ||
|
|
6fc91312d2 | ||
|
|
22bb129bd9 | ||
|
|
4c57893312 | ||
|
|
a2d5314cf7 | ||
|
|
e063967734 | ||
|
|
4519dd010d | ||
|
|
bc2dc8d933 | ||
|
|
fc9b63298c | ||
|
|
c45514b989 |
68
.github/CONTRIBUTING.md
vendored
68
.github/CONTRIBUTING.md
vendored
@@ -1,64 +1,46 @@
|
||||
NewPipe contribution guidelines
|
||||
===============================
|
||||
|
||||
PLEASE READ THESE GUIDELINES CAREFULLY BEFORE ANY CONTRIBUTION!
|
||||
|
||||
## Crash reporting
|
||||
|
||||
Do not report crashes in the GitHub issue tracker. NewPipe has an automated crash report system that will ask you to
|
||||
send a report via e-mail when a crash occurs. This contains all the data we need for debugging, and allows you to even
|
||||
add a comment to it. You'll see exactly what is sent, the system is 100% transparent.
|
||||
Report crashes through the automated crash report system of NewPipe.
|
||||
This way all the data needed for debugging is included in your bugreport for GitHub.
|
||||
You'll see exactly what is sent, be able to add your comments, and then send it.
|
||||
|
||||
## Issue reporting/feature requests
|
||||
|
||||
* Search the [existing issues](https://github.com/TeamNewPipe/NewPipe/issues) first to make sure your issue/feature
|
||||
hasn't been reported/requested before.
|
||||
* Check whether your issue/feature is already fixed/implemented.
|
||||
* Check if the issue still exists in the latest release/beta version.
|
||||
* If you are an Android/Java developer, you are always welcome to fix an issue or implement a feature yourself. PRs welcome!
|
||||
* We use English for development. Issues in other languages will be closed and ignored.
|
||||
* Please only add *one* issue at a time. Do not put multiple issues into one thread.
|
||||
* Follow the template! Issues or feature requests not matching the template might be closed.
|
||||
* **Already reported**? Browse the [existing issues](https://github.com/TeamNewPipe/NewPipe/issues) to make sure your issue/feature hasn't been reported/requested.
|
||||
* **Already fixed**? Check whether your issue/feature is already fixed/implemented.
|
||||
* **Still relevant**? Check if the issue still exists in the latest release/beta version.
|
||||
* **Can you fix it**? If you are an Android/Java developer, you are always welcome to fix an issue or implement a feature yourself. PRs welcome! See [Code contribution](#code-contribution) for more info.
|
||||
* **Is it in English**? Issues in other languages will be ignored unless someone translates them.
|
||||
* **Is it one issue**? Multiple issues require multiple reports, that can be linked to track their statuses.
|
||||
* **The template**: Fill it out, everyone wins. Your issue has a chance of getting fixed.
|
||||
|
||||
## Bug Fixing
|
||||
* If you want to help NewPipe to become free of bugs (this is our utopic goal for NewPipe), you can send us an email to
|
||||
<a href="mailto:tnp@newpipe.schabi.org">tnp@newpipe.schabi.org</a> to let us know that you intend to help. We'll send you further instructions. You may, on request,
|
||||
register at our [Sentry](https://sentry.schabi.org) instance (see section "Crash reporting" for more information).
|
||||
|
||||
## Translation
|
||||
|
||||
* NewPipe is translated via [Weblate](https://hosted.weblate.org/projects/newpipe/strings/). You can log in there
|
||||
with your GitHub account.
|
||||
* If the language you want to translate is not on Weblate, you can add it: see [How to add a new language](https://github.com/TeamNewPipe/NewPipe/wiki/How-to-add-a-new-language-to-NewPipe) in the wiki.
|
||||
* NewPipe is translated via [Weblate](https://hosted.weblate.org/projects/newpipe/strings/). Log in there with your GitHub account, or register.
|
||||
* Add the language you want to translate if it is not there already: see [How to add a new language](https://github.com/TeamNewPipe/NewPipe/wiki/How-to-add-a-new-language-to-NewPipe) in the wiki.
|
||||
|
||||
## Code contribution
|
||||
|
||||
* Stick to NewPipe's style conventions: follow [checkStyle](https://github.com/checkstyle/checkstyle). It will run each time you build the project.
|
||||
* Do not bring non-free software (e.g. binary blobs) into the project. Also, make sure you do not introduce Google
|
||||
* If you want to help out with an existing bug report or feature request, leave a comment on that issue saying you want to try your hand at it.
|
||||
* If there is no existing issue for what you want to work on, open a new one describing your changes. This gives the team and the community a chance to give feedback before you spend time on something that is already in development, should be done differently, or should be avoided completely.
|
||||
* Stick to NewPipe's style conventions of [checkStyle](https://github.com/checkstyle/checkstyle). It runs each time you build the project.
|
||||
* Do not bring non-free software (e.g. binary blobs) into the project. Make sure you do not introduce Google
|
||||
libraries.
|
||||
* Stick to [F-Droid contribution guidelines](https://f-droid.org/wiki/page/Inclusion_Policy).
|
||||
* Make changes on a separate branch with a meaningful name, not on the master neither dev branch. This is commonly known as *feature branch workflow*. You
|
||||
may then send your changes as a pull request (PR) on GitHub.
|
||||
* When submitting changes, you confirm that your code is licensed under the terms of the
|
||||
[GNU General Public License v3](https://www.gnu.org/licenses/gpl-3.0.html).
|
||||
* Please test (compile and run) your code before you submit changes! Ideally, provide test feedback in the PR
|
||||
description. Untested code will **not** be merged!
|
||||
* Make changes on a separate branch with a meaningful name, not on the _master_ branch or the _dev_ branch. This is commonly known as *feature branch workflow*. You may then send your changes as a pull request (PR) on GitHub.
|
||||
* Please test (compile and run) your code before submitting changes! Ideally, provide test feedback in the PR description. Untested code will **not** be merged!
|
||||
* Make sure your PR is up-to-date with the rest of the code. Often, a simple click on "Update branch" will do the job, but if not, you must rebase the dev branch manually and resolve the problems on your own. You can find help [on the wiki](https://github.com/TeamNewPipe/NewPipe/wiki/How-to-merge-a-PR). That makes the maintainers' jobs way easier.
|
||||
* Please show intention to maintain your features and code after you contribute a PR. Unmaintained code is a hassle for core developers. If you do not intend to maintain features you plan to contribute, please rethink your submission, or clearly state that in the PR description.
|
||||
* Respond if someone requests changes or otherwise raises issues about your PRs.
|
||||
* Send PRs that only cover one specific issue/solution/bug. Do not send PRs that are huge and consist of multiple independent solutions.
|
||||
* Try to figure out yourself why builds on our CI fail.
|
||||
* Make sure your PR is up-to-date with the rest of the code. Often, a simple click on "Update branch" will do the job,
|
||||
but if not, you are asked to rebase the dev branch manually and resolve the problems on your own. You can find help [on the wiki](https://github.com/TeamNewPipe/NewPipe/wiki/How-to-merge-a-PR). That will make the
|
||||
maintainers' jobs way easier.
|
||||
* Please show intention to maintain your features and code after you contributed it. Unmaintained code is a hassle for
|
||||
the core developers, and just adds work. If you do not intend to maintain features you contributed, please think again
|
||||
about submission, or clearly state that in the description of your PR.
|
||||
* Respond yourselves if someone requests changes or otherwise raises issues about your PRs.
|
||||
* Send PR that only cover one specific issue/solution/bug. Do not send PRs that are huge and consists of multiple
|
||||
independent solutions.
|
||||
|
||||
## Communication
|
||||
|
||||
* There is an IRC channel on Freenode which is regularly visited by the core team and other developers:
|
||||
[#newpipe](irc:irc.freenode.net/newpipe). [Click here for Webchat](https://webchat.freenode.net/?channels=newpipe)!
|
||||
* If you want to get in touch with the core team or one of our other contributors you can send an email to
|
||||
<a href="mailto:tnp@newpipe.schabi.org">tnp@newpipe.schabi.org</a>. Please do not send issue reports, they will be ignored and remain unanswered! Use the GitHub issue
|
||||
tracker described above!
|
||||
* Feel free to post suggestions, changes, ideas etc. on GitHub or IRC!
|
||||
* The [#newpipe](irc:irc.freenode.net/newpipe) channel on freenode has the core team and other developers in it. [Click here for webchat](https://webchat.freenode.net/?channels=newpipe)!
|
||||
* You can also use a Matrix account to join the Newpipe channel at [#freenode_#newpipe:matrix.org](https://matrix.to/#/#freenode_#newpipe:matrix.org).
|
||||
* Post suggestions, changes, ideas etc. on GitHub or IRC.
|
||||
|
||||
47
.github/ISSUE_TEMPLATE/bug_report.md
vendored
47
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -7,20 +7,20 @@ assignees: ''
|
||||
---
|
||||
|
||||
<!--
|
||||
Oh no, a bug! It happens. Thanks for reporting an issue with NewPipe. If this is your first bug report, read the following information before proceeding:
|
||||
|
||||
Please note, we only support the latest version of NewPipe. In order to check your app version, open the left drawer and click on "About". If you don't have the latest version, upgrade to it and reproduce the problem before opening the issue. The release page (https://github.com/TeamNewPipe/NewPipe/releases/latest) is where you can get it.
|
||||
|
||||
P.S.: Our contribution guidelines might be a nice document to read before you fill out the report :) You can find it at https://github.com/TeamNewPipe/NewPipe/blob/HEAD/.github/CONTRIBUTING.md
|
||||
|
||||
To make it easier for us to help you please enter detailed information in the template we have provided below. If a section isn't relevant, just delete it, though it would be helpful to still provide as much detail as possible.
|
||||
Oh no, a bug! It happens. Thanks for reporting an issue with NewPipe. To make it easier for us to help you please enter detailed information in the template we have provided below. If a section isn't relevant, just delete it, though it would be helpful to still provide as much detail as possible.
|
||||
-->
|
||||
|
||||
<!-- The comments between these brackets won't show up in the submitted issue (as you can see in the preview). -->
|
||||
<!-- IF YOU DON'T FILL IN THE TEMPLATE PROPERLY, YOUR ISSUE IS LIABLE TO BE CLOSED. If you feel tired/lazy right now, open your issue some other time. We'll wait. -->
|
||||
|
||||
### Version
|
||||
<!-- Which version are you using? Hopefully the latest! We just told you that above! -->
|
||||
-
|
||||
<!-- The comments between these brackets won't show up in the submitted issue (as you can see in the Preview). -->
|
||||
|
||||
### Checklist
|
||||
<!-- This checklist is COMPULSORY. The first box has been checked for you to show you how it is done. -->
|
||||
|
||||
- [x] I am using the latest version - x.xx.x <!-- Check https://github.com/TeamNewPipe/NewPipe/releases -->
|
||||
- [ ] I checked, but didn't find any duplicates (open OR closed) of this issue in the repo. <!-- Seriously, check. O_O -->
|
||||
- [ ] I have read the contribution guidelines given at https://github.com/TeamNewPipe/NewPipe/blob/HEAD/.github/CONTRIBUTING.md.
|
||||
- [ ] This issue contains only one bug. I will open one issue for every bug report I want to file.
|
||||
|
||||
### Steps to reproduce the bug
|
||||
<!--
|
||||
@@ -31,16 +31,35 @@ To make it easier for us to help you please enter detailed information in the te
|
||||
|
||||
<!-- If you can't cause the bug to show up again reliably (and hence don't have a proper set of steps to give us), please still try to give as many details as possible on how you think you encountered the bug. -->
|
||||
|
||||
|
||||
|
||||
### Actual behaviour
|
||||
<!-- Tell us what happens with the steps given above. -->
|
||||
|
||||
|
||||
|
||||
### Expected behavior
|
||||
<!-- Tell us what you expect to happen. -->
|
||||
|
||||
### Actual behaviour
|
||||
<!-- Tell us what happens instead. -->
|
||||
|
||||
|
||||
### Screenshots/Screen recordings
|
||||
<!-- If applicable, add screenshots or a screen recording to help explain your problem. GitHub supports uploading them directly in the issue text box. If your file is too big for Github to accept, feel free to paste a link from an image/video hoster here instead. -->
|
||||
|
||||
<!-- DON'T POST SCREENSHOTS OF THE ERROR PAGE. Use the buttons given on the error page to paste the error as text in the Logs section below. -->
|
||||
|
||||
|
||||
|
||||
### Logs
|
||||
<!-- If your bug includes a crash (where you're shown the Error Report page with a bunch of info), copy it to the clipboard (there is a share button for this), head over to our bug report to markdown converter at https://teamnewpipe.github.io/CrashReportToMarkdown/ and paste it. Copy the converted text (it is MUCH easier to read this way) from there and paste it here: -->
|
||||
<!-- If your bug includes a crash (where you're shown the Error Report page with a bunch of info), tap on "Copy formatted report" at the bottom and paste it here: -->
|
||||
|
||||
<!-- That's right, here! -->
|
||||
|
||||
|
||||
|
||||
<!-- Please fill this out when you do not provide a log generate by NewPipe -->
|
||||
|
||||
### Device info
|
||||
|
||||
- Android version/Custom ROM version:
|
||||
- Device model:
|
||||
|
||||
8
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
8
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: 💬 IRC
|
||||
url: https://webchat.freenode.net/#newpipe
|
||||
about: Chat with us via IRC for quick Q/A
|
||||
- name: 💬 Matrix
|
||||
url: https://matrix.to/#/#freenode_#newpipe:matrix.org
|
||||
about: Chat with us via Matrix for quick Q/A
|
||||
25
.github/ISSUE_TEMPLATE/feature_request.md
vendored
25
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@@ -5,35 +5,42 @@ labels: enhancement
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
<!-- Hey. Our contribution guidelines (https://github.com/TeamNewPipe/NewPipe/blob/HEAD/.github/CONTRIBUTING.md) might be an appropriate
|
||||
document to read before you fill out the request :) -->
|
||||
<!-- IF YOU DON'T FILL IN THE TEMPLATE PROPERLY, YOUR ISSUE IS LIABLE TO BE CLOSED. If you feel tired/lazy right now, open your issue some other time. We'll wait. -->
|
||||
|
||||
|
||||
<!-- The comments between these brackets won't show up in the submitted issue (as you can see in the Preview). -->
|
||||
|
||||
### Checklist
|
||||
<!-- This checklist is COMPULSORY. The first box has been checked for you to show you how it is done. -->
|
||||
|
||||
- [x] I checked, but didn't find any duplicates (open OR closed) of this issue in the repo. <!-- Seriously, check. O_O -->
|
||||
- [ ] I have read the contribution guidelines given at https://github.com/TeamNewPipe/NewPipe/blob/HEAD/.github/CONTRIBUTING.md.
|
||||
- [ ] This issue contains only one feature request. I will open one issue for every feature I want to request.
|
||||
|
||||
<!-- The comments between these brackets won't show up in the submitted issue (as you can see in the preview). -->
|
||||
|
||||
#### Describe the feature you want
|
||||
<!-- A clear and concise description of what you want to happen. PLEASE MAKE SURE it is one feature ONLY. You should open separate issues for separate feature requests, because those issues will be used to track their status.
|
||||
<!-- A clear and concise description of what you wish should happen.
|
||||
Example: *I think it would be nice if you add feature Y which makes X possible.*
|
||||
|
||||
Optionally, also describe alternatives you've considered.
|
||||
Example: *Z is also a good alternative. Not as good as Y, but at least...* or *I considered Z, but that didn't turn out to be a good idea because...* -->
|
||||
|
||||
<!-- Write below this -->
|
||||
|
||||
|
||||
#### Is your feature request related to a problem? Please describe it
|
||||
<!-- A clear and concise description of what the problem is. Maybe the developers could brainstorm and come up with a better solution to your problem. If they exist, link to related Issues and/or PRs for developers to keep track easier.
|
||||
<!-- A clear and concise description of what the problem is. Maybe the developers and the community could brainstorm and come up with a better solution to your problem. If they exist, link to related Issues and/or PRs for developers to keep track easier.
|
||||
Example: *I want to do X, but there is no way to do it.* -->
|
||||
|
||||
<!-- Write below this -->
|
||||
|
||||
|
||||
#### Additional context
|
||||
<!-- Add any other context, like screenshots, about the feature request here.
|
||||
Example: *Here's a photo of my cat!* -->
|
||||
|
||||
<!-- Write below this -->
|
||||
|
||||
|
||||
#### How will you/everyone benefit from this feature?
|
||||
<!-- Convince us! How does it change your NewPipe experience and/or your life?
|
||||
The better this paragraph is, the more likely a developer will think about working on it.
|
||||
Example: *This feature will help us colonize the galaxy! -->
|
||||
|
||||
<!-- Write below this -->
|
||||
|
||||
23
.github/PULL_REQUEST_TEMPLATE.md
vendored
23
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -1,28 +1,29 @@
|
||||
<!-- Hey there. Thank you so much for improving NewPipe. Please take a moment to fill out the following suggestion on how to structure this PR description. Having roughly the same layout helps everyone considerably :)-->
|
||||
<!-- Hey there. Thank you so much for improving NewPipe, and filling out the details. Having roughly the same layout helps everyone considerably :)-->
|
||||
|
||||
#### What is it?
|
||||
- [ ] Bug fix (user facing)
|
||||
- [ ] Bugfix (user facing)
|
||||
- [ ] Feature (user facing)
|
||||
- [ ] Code base improvement (dev facing)
|
||||
- [ ] Codebase improvement (dev facing)
|
||||
- [ ] Meta improvement to the project (dev facing)
|
||||
|
||||
#### Description of the changes in your PR
|
||||
<!-- While bullet points are the norm in this section, feel free to write a text instead if you can't fit it in a list -->
|
||||
<!-- While bullet points are the norm in this section, feel free to write free-form text instead of a list -->
|
||||
- record videos
|
||||
- create clones
|
||||
- take over the world
|
||||
|
||||
#### Fixes the following issue(s)
|
||||
<!-- Also add reddit or other links which are relevant to your change. -->
|
||||
-
|
||||
<!-- Prefix issues with "Fixes" so that GitHub closes them when the PR is merged (note that each "Fixes #" should be in its own item). Also add any other relevant links. -->
|
||||
- Fixes #
|
||||
|
||||
#### Relies on the following changes
|
||||
<!-- Delete this if it doesn't apply to you. -->
|
||||
-
|
||||
|
||||
#### Testing apk
|
||||
<!-- Ensure that you have your changes on a new branch which has a meaningful name. This name will be used as a suffix for the app ID to allow installing and testing multiple versions of NewPipe. Do NOT name your branches like "patch-0" and "feature-1". For example, if your PR implements a bug fix for comments, an appropriate branch name would be "commentfix". -->
|
||||
debug.zip
|
||||
#### APK testing
|
||||
<!-- Use a new, meaningfully named branch. The name is used as a suffix for the app ID to allow installing and testing multiple versions of NewPipe, e.g. "commentfix", if your PR implements a bugfix for comments. (No names like "patch-0" and "feature-1".) -->
|
||||
<!-- Remove the following line if you directly link the APK created by the CI pipeline. Directly linking is preferred if you need to let users test.-->
|
||||
On the website the APK can be found by going to the "Checks" tab below the title and then on "artifacts" on the right.
|
||||
|
||||
#### Agreement
|
||||
- [ ] I carefully read the [contribution guidelines](https://github.com/TeamNewPipe/NewPipe/blob/HEAD/.github/CONTRIBUTING.md) and agree to them.
|
||||
#### Due diligence
|
||||
- [ ] I read the [contribution guidelines](https://github.com/TeamNewPipe/NewPipe/blob/HEAD/.github/CONTRIBUTING.md).
|
||||
|
||||
102
.github/workflows/ci.yml
vendored
Normal file
102
.github/workflows/ci.yml
vendored
Normal file
@@ -0,0 +1,102 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- dev
|
||||
push:
|
||||
branches:
|
||||
- dev
|
||||
- master
|
||||
|
||||
jobs:
|
||||
build-and-test-jvm:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: gradle/wrapper-validation-action@v1
|
||||
|
||||
- name: create and checkout branch
|
||||
# push events already checked out the branch
|
||||
if: github.event_name == 'pull_request'
|
||||
run: git checkout -B ${{ github.head_ref }}
|
||||
|
||||
- name: set up JDK 8
|
||||
uses: actions/setup-java@v2
|
||||
with:
|
||||
java-version: 8
|
||||
distribution: "zulu"
|
||||
|
||||
- name: Cache Gradle dependencies
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ~/.gradle/caches
|
||||
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }}
|
||||
restore-keys: ${{ runner.os }}-gradle
|
||||
|
||||
- name: Build debug APK and run jvm tests
|
||||
run: ./gradlew assembleDebug lintDebug testDebugUnitTest --stacktrace
|
||||
|
||||
- name: Upload APK
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: app
|
||||
path: app/build/outputs/apk/debug/*.apk
|
||||
test-android:
|
||||
runs-on: macos-latest
|
||||
strategy:
|
||||
matrix:
|
||||
api-level: [21, 29]
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: set up JDK 8
|
||||
uses: actions/setup-java@v2
|
||||
with:
|
||||
java-version: 8
|
||||
distribution: "zulu"
|
||||
|
||||
- name: Cache Gradle dependencies
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ~/.gradle/caches
|
||||
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }}
|
||||
restore-keys: ${{ runner.os }}-gradle
|
||||
|
||||
- name: Run android tests
|
||||
uses: reactivecircus/android-emulator-runner@v2
|
||||
with:
|
||||
api-level: ${{ matrix.api-level }}
|
||||
script: ./gradlew connectedCheck
|
||||
# sonar:
|
||||
# runs-on: ubuntu-latest
|
||||
# steps:
|
||||
# - uses: actions/checkout@v2
|
||||
# with:
|
||||
# fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis
|
||||
|
||||
# - name: Set up JDK 11
|
||||
# uses: actions/setup-java@v2
|
||||
# with:
|
||||
# java-version: 11 # Sonar requires JDK 11
|
||||
# distribution: "zulu"
|
||||
|
||||
# - name: Cache SonarCloud packages
|
||||
# uses: actions/cache@v2
|
||||
# with:
|
||||
# path: ~/.sonar/cache
|
||||
# key: ${{ runner.os }}-sonar
|
||||
# restore-keys: ${{ runner.os }}-sonar
|
||||
|
||||
# - name: Cache Gradle packages
|
||||
# uses: actions/cache@v2
|
||||
# with:
|
||||
# path: ~/.gradle/caches
|
||||
# key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }}
|
||||
# restore-keys: ${{ runner.os }}-gradle
|
||||
|
||||
# - name: Build and analyze
|
||||
# env:
|
||||
# GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any
|
||||
# SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
||||
# run: ./gradlew build sonarqube --info
|
||||
0
.gitmodules
vendored
0
.gitmodules
vendored
18
.travis.yml
18
.travis.yml
@@ -1,18 +0,0 @@
|
||||
language: android
|
||||
jdk:
|
||||
- oraclejdk8
|
||||
android:
|
||||
components:
|
||||
# The BuildTools version used by NewPipe
|
||||
- tools
|
||||
- build-tools-29.0.3
|
||||
|
||||
# The SDK version used to compile NewPipe
|
||||
- android-29
|
||||
|
||||
before_install:
|
||||
- yes | sdkmanager "platforms;android-29"
|
||||
script: ./gradlew -Dorg.gradle.jvmargs=-Xmx1536m assembleDebug lintDebug testDebugUnitTest
|
||||
|
||||
licenses:
|
||||
- '.+'
|
||||
140
README.es.md
Normal file
140
README.es.md
Normal file
@@ -0,0 +1,140 @@
|
||||
<p align="center"><a href="https://newpipe.net"><img src="assets/new_pipe_icon_5.png" width="150"></a></p>
|
||||
<h2 align="center"><b>NewPipe</b></h2>
|
||||
<h4 align="center">Una interfaz de streaming lijera y libre para Android.</h4>
|
||||
|
||||
<p align="center"><a href="https://f-droid.org/packages/org.schabi.newpipe/"><img src="https://f-droid.org/wiki/images/0/06/F-Droid-button_get-it-on.png"></a></p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/TeamNewPipe/NewPipe/releases" alt="GitHub release"><img src="https://img.shields.io/badge/Lanzamiento-v0.20.11-blue.svg" ></a>
|
||||
<a href="https://www.gnu.org/licenses/gpl-3.0" alt="License: GPLv3"><img src="https://img.shields.io/badge/Licencia-GPL%20v3-blue.svg"></a>
|
||||
<a href="https://github.com/TeamNewPipe/NewPipe/actions" alt="Build Status"><img src="https://github.com/TeamNewPipe/NewPipe/workflows/CI/badge.svg?branch=dev&event=push"></a>
|
||||
<a href="https://hosted.weblate.org/engage/newpipe/es/" alt="Estado de la traducción"><img src="https://hosted.weblate.org/widgets/newpipe/es/svg-badge.svg"></a>
|
||||
<a href="https://webchat.freenode.net/#newpipe" alt="IRC channel: #newpipe"><img src="https://img.shields.io/badge/Canal%20de%20IRC%20-%23newpipe-brightgreen.svg"></a>
|
||||
<a href="https://www.bountysource.com/teams/newpipe" alt="Bountysource bounties"><img src="https://img.shields.io/bountysource/team/newpipe/activity.svg?colorB=cd201f"></a>
|
||||
</p>
|
||||
<hr>
|
||||
|
||||
<p align="center"><a href="#capturas-de-pantalla">Capturas de pantalla</a> • <a href="#descripción">Descripción</a> • <a href="#características">Características</a> • <a href="#installación-y-actualizaciones">Installación y actualizaciones</a> • <a href="#contribución">Contribución</a> • <a href="#donar">Donar</a> • <a href="#licencias">Licencias</a></p>
|
||||
<p align="center"><a href="https://newpipe.net">Sitio web</a> • <a href="https://newpipe.net/blog/">Blog</a> • <a href="https://newpipe.net/FAQ/">Preguntas Frecuentes</a> • <a href="https://newpipe.net/press/">Prensa</a></p>
|
||||
<hr>
|
||||
|
||||
*Lea esto en otros idiomas: [English](README.md), [한국어](README.ko.md), [Soomaali](README.so.md), [Português Brasil](README.pt_BR.md), [日本語](README.ja.md), [Română](README.ro.md), [Türkçe](README.tr.md).*
|
||||
|
||||
<b>AVISO: ESTA ES UNA VERSIÓN BETA, POR LO TANTO, PUEDE ENCONTRAR BUGS (ERRORES). SI ENCUENTRA UNO, ABRA UN ISSUE A TRAVÉS DE NUESTRO REPOSITORIO GITHUB.</b>
|
||||
|
||||
<b>COLOCAR NEWPIPE O CUALQUIER FORK (BIFURCACIÓN) REALIZADO DE ELLO EN GOOGLE PLAY STORE VIOLA SUS TÉRMINOS Y CONDICIONES.</b>
|
||||
|
||||
## Capturas de pantalla
|
||||
|
||||
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_01.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_01.png)
|
||||
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_02.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_02.png)
|
||||
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_03.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_03.png)
|
||||
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_04.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_04.png)
|
||||
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_05.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_05.png)
|
||||
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_06.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_06.png)
|
||||
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_07.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_07.png)
|
||||
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_08.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_08.png)
|
||||
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_09.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_09.png)
|
||||
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_10.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_10.png)
|
||||
[<img src="fastlane/metadata/android/en-US/images/tenInchScreenshots/shot_11.png" width=405>](fastlane/metadata/android/en-US/images/tenInchScreenshots/shot_11.png)
|
||||
[<img src="fastlane/metadata/android/en-US/images/tenInchScreenshots/shot_12.png" width=405>](fastlane/metadata/android/en-US/images/tenInchScreenshots/shot_12.png)
|
||||
|
||||
## Descripción
|
||||
NewPipe no usa ninguna librería de framework de Google, ni la API de YouTube. Los sitios web solamente se analizan para extraer la información requerida, asi que esta app se puede usar sin los servicios de Google instalados. Además, no se necesita una cuenta de YouTube para usar NewPipe, lo cual es un software libre de copyleft.
|
||||
|
||||
### Características
|
||||
* Buscar videos
|
||||
* Mostrar información general sobre videos
|
||||
* Mirar videos de YouTube
|
||||
* Escuchar audio de YouTube
|
||||
* Modo popup (reproductor flotante)
|
||||
* Elegir reproductor para mirar el video
|
||||
* Descargar videos
|
||||
* Descargar solamente audio
|
||||
* Abrir video en Kodi
|
||||
* Mostrar videos próximos/relacionados
|
||||
* Buscar a través de YouTube en un idioma específico
|
||||
* Mirar/Bloquear materiales restringidas por edad.
|
||||
* Mostrar información general sobre canales
|
||||
* Buscar canales
|
||||
* Mirar videos de un canal
|
||||
* Apoyo Orbot/Tor (todavía no directamente)
|
||||
* Apoyo 1080p/2K/4K
|
||||
* Ver historias
|
||||
* Subscribirse a canales
|
||||
* Buscar historias
|
||||
* Buscar/mirar listas de reproducción
|
||||
* Mirar listas de reproducción en fila
|
||||
* Poner videos en fila
|
||||
* Listas locales de reproducción
|
||||
* Subtítulos
|
||||
* Apoyo de medios en directo
|
||||
* Mostrar comentarios
|
||||
|
||||
### Servicios apoyados
|
||||
NewPipe apoya varios servicios. Nuestras [documentaciones](https://teamnewpipe.github.io/documentation/) proveen más información en como se puede agregar un servicio nuevo a la app y el extractor. Por favor contáctenos si pretende agregar uno nuevo. Actualmente los servicios apoyados son:
|
||||
|
||||
* YouTube
|
||||
* SoundCloud \[beta\]
|
||||
* media.ccc.de \[beta\]
|
||||
* PeerTube instances \[beta\]
|
||||
* Bandcamp \[beta\]
|
||||
|
||||
<!-- Brecha escondida para mantener compatibles los enlaces viejos. -->
|
||||
<span id="actualizaciones"></span>
|
||||
|
||||
## Installación y actualizaciones
|
||||
Se puede instalar NewPipe usando uno de los métodos siguientes:
|
||||
1. Agregar nuestro repositorio personalizado a F-Droid e instalarlo desde allí. Las instrucciones están aquí: https://newpipe.net/FAQ/tutorials/install-add-fdroid-repo/
|
||||
2. Descargar el archivo APK del enlace [Github Releases](https://github.com/TeamNewPipe/NewPipe/releases) e instalarlo.
|
||||
3. Actualizar a través de F-Droid. Este es el método más lento para obtener la actualización, como F-Droid debe reconocer cambios, construir el APK aparte, firmarlo con una clave, y finalmente empujar la actualización a los usuarios.
|
||||
4. Construir un APK de depuración por si mismo. Este es el modo más rápido para realizar nuevas características en su dispositivo, pero es mucho más complicado, asi que recomendamos uno de los otros métodos.
|
||||
|
||||
Recomendamos el método 1 para la mayoría de usuarios. Los APKs instalados usando método 1 o 2 son compatibles el uno con el otro, pero no con las instalaciones usando método 3. Esta es debida a la misma clave digital (la nuestra), siendo utilizado en los métodos 1 y 2, pero una clave digital diferente (la de F-Droid) siendo utilizado en el método 3. Construir un APK de depuración usando método 4 excluye una clave enteramente. Firmando con claves digitales ayuda a asegurar de que un
|
||||
usuario no esté engañado para instalar una actualización maliciosa a una app.
|
||||
|
||||
Mientras tanto, si quiere cambiar los fuentes por alguna razón (por ejemplo, la funcionalidad del nucleo de NewPipe se rompe y F-Droid aun no tiene la actualización), recomendamos el siguiente procedimiento:
|
||||
1. Repaldear sus datos a través de Ajustes > Contenido > Exporta base de datos para guardar su historia, subscripciones, y listas de reproducción
|
||||
2. Desinstalar NewPipe
|
||||
3. Descargar el APK del nuevo fuente e instalarlo.
|
||||
4. Importar los datos del paso 1 a través de Ajustes > Contenido > Importa base de datos.
|
||||
|
||||
## Contribución
|
||||
Si tiene ideas, traducciónes, cambios de diseño, limpieza de código, o cambios grandes de código, su ayuda es siempre bienvenida.
|
||||
Cuanto más realizamos, mejor se pone la aplicación!
|
||||
|
||||
Si quiere involucrarse, fíjese en nuestras [notas de contribución](.github/CONTRIBUTING.md).
|
||||
|
||||
<a href="https://hosted.weblate.org/engage/newpipe/es/">
|
||||
<img src="https://hosted.weblate.org/widgets/newpipe/es/287x66-grey.png" alt="Estado de la traducción" />
|
||||
</a>
|
||||
|
||||
## Donar
|
||||
Si le gusta el NewPipe estaremos felices con una donación. O puede enviar bitcoin o donar a través de Bountysource o Liberapay. Para obtener más información sobre como donar a NewPipe, por favor visita nuestro [sitio web](https://newpipe.net/donate).
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<td><img src="https://bitcoin.org/img/icons/logotop.svg" alt="Bitcoin"></td>
|
||||
<td><img src="assets/bitcoin_qr_code.png" alt="Bitcoin QR code" width="100px"></td>
|
||||
<td><samp>16A9J59ahMRqkLSZjhYj33n9j3fMztFxnh</samp></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="https://liberapay.com/TeamNewPipe/"><img src="https://upload.wikimedia.org/wikipedia/commons/2/27/Liberapay_logo_v2_white-on-yellow.svg" alt="Liberapay" width="80px" ></a></td>
|
||||
<td><a href="https://liberapay.com/TeamNewPipe/"><img src="assets/liberapay_qr_code.png" alt="Visit NewPipe at liberapay.com" width="100px"></a></td>
|
||||
<td><a href="https://liberapay.com/TeamNewPipe/donate"><img src="assets/liberapay_donate_button.svg" alt="Donate via Liberapay" height="35px"></a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="https://www.bountysource.com/teams/newpipe"><img src="https://upload.wikimedia.org/wikipedia/commons/thumb/2/22/Bountysource.png/320px-Bountysource.png" alt="Bountysource" width="190px"></a></td>
|
||||
<td><a href="https://www.bountysource.com/teams/newpipe"><img src="assets/bountysource_qr_code.png" alt="Visit NewPipe at bountysource.com" width="100px"></a></td>
|
||||
<td><a href="https://www.bountysource.com/teams/newpipe/issues"><img src="https://img.shields.io/bountysource/team/newpipe/activity.svg?colorB=cd201f" height="30px" alt="Check out how many bounties you can earn."></a></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
## Política de privacidad
|
||||
El proyecto NewPipe tiene como objetivo proveer una experience privada y anónima para usar servicios de medios web.
|
||||
Por lo tanto, la app no colecciona ningunos datos sin su consentimiento. La politica de privacidad de NewPipe explica en detalle los datos enviados y almacenados cuando envia un informe de error, o comentario en nuestro blog. Puede encontrar el documento [aqui](https://newpipe.net/legal/privacy/).
|
||||
|
||||
## Licencia
|
||||
[](https://www.gnu.org/licenses/gpl-3.0.en.html)
|
||||
|
||||
NewPipe es Software Libre: Puede usar, estudiar, compartir, y mejorarlo a su voluntad. Especificamente puede redistribuir y/o modificarlo bajo los términos de la [GNU General Public License](https://www.gnu.org/licenses/gpl.html) como publicado por la Free Software Foundation, o versión 3 de la licencia, o (en su opción) cualquier versión posterior.
|
||||
149
README.ja.md
Normal file
149
README.ja.md
Normal file
@@ -0,0 +1,149 @@
|
||||
<p align="center"><a href="https://newpipe.net"><img src="assets/new_pipe_icon_5.png" width="150"></a></p>
|
||||
<h2 align="center"><b>NewPipe</b></h2>
|
||||
<h4 align="center">自由で軽量な Android 向けストリーミングフロントエンド</h4>
|
||||
|
||||
<p align="center"><a href="https://f-droid.org/packages/org.schabi.newpipe/"><img src="https://fdroid.gitlab.io/artwork/badge/get-it-on-ja.svg"></a></p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/TeamNewPipe/NewPipe/releases" alt="GitHub リリース"><img src="https://img.shields.io/github/release/TeamNewPipe/NewPipe.svg"></a>
|
||||
<a href="https://www.gnu.org/licenses/gpl-3.0" alt="ライセンス: GPLv3"><img src="https://img.shields.io/badge/License-GPL%20v3-blue.svg"></a>
|
||||
<a href="https://github.com/TeamNewPipe/NewPipe/actions" alt="ビルド状態"><img src="https://github.com/TeamNewPipe/NewPipe/workflows/CI/badge.svg?branch=dev&event=push"></a>
|
||||
<a href="https://hosted.weblate.org/engage/newpipe/" alt="翻訳状態"><img src="https://hosted.weblate.org/widgets/newpipe/-/svg-badge.svg"></a>
|
||||
<a href="https://webchat.freenode.net/#newpipe" alt="IRC チャンネル: #newpipe"><img src="https://img.shields.io/badge/IRC%20chat-%23newpipe-brightgreen.svg"></a>
|
||||
<a href="https://www.bountysource.com/teams/newpipe" alt="Bountysource 寄付"><img src="https://img.shields.io/bountysource/team/newpipe/activity.svg?colorB=cd201f"></a>
|
||||
</p>
|
||||
<hr>
|
||||
<p align="center"><a href="#screenshots">スクリーンショット</a> • <a href="#description">説明</a> • <a href="#features">機能</a> • <a href="#installation-and-updates">インストールと更新</a> • <a href="#contribution">貢献</a> • <a href="#donate">寄付</a> • <a href="#license">ライセンス</a></p>
|
||||
<p align="center"><a href="https://newpipe.net">ウェブサイト</a> • <a href="https://newpipe.net/blog/">ブログ</a> • <a href="https://newpipe.net/FAQ/">FAQ</a> • <a href="https://newpipe.net/press/">ニュース</a></p>
|
||||
<hr>
|
||||
|
||||
*他の言語で読む: [English](README.md), [Español](README.es.md), [한국어](README.ko.md), [Soomaali](README.so.md), [Português Brasil](README.pt.br.md), [日本語](README.ja.md), [Română](README.ro.md), [Türkçe](README.tr.md)。*
|
||||
|
||||
<b>注意: これはベータ版のため、バグが発生する可能性があります。もしバグが発生した場合、GitHub のリポジトリで Issue を開いてください。</b>
|
||||
|
||||
<b>NewPipe 及びいずれのフォークを Google Play ストアに公開すると、Google の取引条件の違反になります。</b>
|
||||
|
||||
<span id="screenshots"></span>
|
||||
## スクリーンショット
|
||||
|
||||
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_01.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_01.png)
|
||||
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_02.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_02.png)
|
||||
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_03.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_03.png)
|
||||
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_04.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_04.png)
|
||||
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_05.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_05.png)
|
||||
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_06.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_06.png)
|
||||
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_07.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_07.png)
|
||||
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_08.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_08.png)
|
||||
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_09.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_09.png)
|
||||
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_10.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_10.png)
|
||||
[<img src="fastlane/metadata/android/en-US/images/tenInchScreenshots/shot_11.png" width=405>](fastlane/metadata/android/en-US/images/tenInchScreenshots/shot_11.png)
|
||||
[<img src="fastlane/metadata/android/en-US/images/tenInchScreenshots/shot_12.png" width=405>](fastlane/metadata/android/en-US/images/tenInchScreenshots/shot_12.png)
|
||||
|
||||
<span id="description"></span>
|
||||
## 説明
|
||||
|
||||
自由なコピーレフトソフトウェアの NewPipe は一切の Google フレームワークライブラリ及び、YouTube API を使用しません。ウェブサイトは必要な情報のためだけに読み込まれるため、このアプリは Google のサービスがインストールされていない端末で使用ができます。また、NewPipe の使用に YouTube アカウントは必要となりません。
|
||||
|
||||
<span id="features"></span>
|
||||
### 機能
|
||||
|
||||
* 動画の検索
|
||||
* 動画の基本情報の表示
|
||||
* YouTube の動画の視聴
|
||||
* YouTube の動画のバックグラウンド再生
|
||||
* ポップアップモード (フローティングプレイヤー)
|
||||
* 動画を視聴するストリーミングプレイヤーの選択
|
||||
* 動画のダウンロード
|
||||
* 音声のみのダウンロード
|
||||
* Kodi での動画再生
|
||||
* 次の動画/関連動画の表示
|
||||
* 特定の言語の YouTube の検索
|
||||
* 年齢制限のあるコンテンツの視聴/ブロック
|
||||
* チャンネルの基本情報の表示
|
||||
* チャンネルの検索
|
||||
* チャンネルからの動画の視聴
|
||||
* Orbot/Tor 対応 (直接的なものは未実装)
|
||||
* 1080p/2K/4K 対応
|
||||
* 履歴の表示
|
||||
* チャンネルの登録
|
||||
* 履歴の検索
|
||||
* 再生リストの検索/視聴
|
||||
* 再生リストをキューに追加して再生
|
||||
* 動画のキューへの追加
|
||||
* 端末内の再生リスト
|
||||
* 字幕
|
||||
* ライブ配信の対応
|
||||
* コメントの表示
|
||||
|
||||
### 対応しているサービス
|
||||
|
||||
NewPipe は複数のサービスに対応しています。[ドキュメント](https://teamnewpipe.github.io/documentation/)は、どのようにしてアプリと NewPipe Extractor にサービスを追加できるかについて詳細な情報を提供しています。もし、新しいサービスを追加するならば、是非私たちに連絡をお願いします。現在対応しているサービスは:
|
||||
|
||||
* YouTube
|
||||
* SoundCloud \[ベータ\]
|
||||
* media.ccc.de \[ベータ\]
|
||||
* PeerTube インスタンス \[ベータ\]
|
||||
* Bandcamp \[ベータ\]
|
||||
|
||||
<!-- Hidden span to keep old links compatible. -->
|
||||
<span id="updates"></span>
|
||||
|
||||
<span id="installation-and-updates"></span>
|
||||
## インストールと更新
|
||||
以下の方法のいずれかに従うことによって NewPipe をインストールできます。
|
||||
1. カスタムリポジトリを F-Droid に追加してリリースが公開され次第インストールする。この方法の説明はこちら: https://newpipe.net/FAQ/tutorials/install-add-fdroid-repo/
|
||||
2. リリースが公開され次第[GitHub のリリース](https://github.com/TeamNewPipe/NewPipe/releases)から APK をダウンロードしてインストールする。
|
||||
3. F-Droid から更新する。これは更新を手にする上で最も遅い方法です。F-Droid が変更を検知して、APK をビルドし、署名、そしてユーザーに更新を届ける必要があるためです。
|
||||
4. 自分でデバッグ APK をビルドする。これは新しい機能を使用する上で最も早い方法ですが、他と比べてとても複雑なので、他の方法の使用を推奨します。
|
||||
|
||||
私たちはほとんどのユーザーに方法1を推奨します。方法1と2でインストールされた APK は互換性がありますが、方法3でインストールされたものにはありません。これは方法1と2では、同じ署名鍵 (私たちが使用するもの)が使用されますが、方法3では異なった署名鍵 (F-Droidが使用するもの)が使用されるためです。方法4を使ったデバッグ APK のビルドは根本的に署名鍵の問題を除きます。署名鍵はユーザーが騙されて悪意のある更新がアプリにインストールされないことを助けるためにあります。
|
||||
|
||||
もし、何かしらの理由によりソースを切り替えたい場合 (例: NewPipe のコア機能が壊れてしまったが F-Droid はまだ更新をしていない) は、この手順を推奨します。
|
||||
1. 履歴や登録チャンネル、再生リストを保つために 設定 > コンテンツ > データベースをエクスポート からデータをバックアップ
|
||||
2. NewPipe をアンインストール
|
||||
3. 新しいソースから APK をダウンロードしてインストール
|
||||
4. 設定 > コンテンツ > データベースをインポート からステップ1で作ったデータベースをインポート
|
||||
|
||||
<span id="contribution"></span>
|
||||
## 貢献
|
||||
翻訳、デザインの変更、コードの整理、大規模なコードの変更などの助けはいつでも歓迎します。
|
||||
より良いものを一緒に作り上げましょう!
|
||||
|
||||
もし貢献をしたい場合、[貢献ノート](.github/CONTRIBUTING.md)をご確認ください。
|
||||
|
||||
<a href="https://hosted.weblate.org/engage/newpipe/">
|
||||
<img src="https://hosted.weblate.org/widgets/newpipe/-/287x66-grey.png" alt="翻訳状態" />
|
||||
</a>
|
||||
|
||||
<span id="donate"></span>
|
||||
## 寄付
|
||||
もし、NewPipe を気に入っていただけたら、寄付をしていただけると嬉しいです。Bitcoin または Bountysource, Liberapay から寄付をすることができます。NewPipe への寄付については、[ウェブサイト](https://newpipe.net/donate)からお願いします。
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<td><img src="https://bitcoin.org/img/icons/logotop.svg" alt="Bitcoin"></td>
|
||||
<td><img src="assets/bitcoin_qr_code.png" alt="Bitcoin QR コード" width="100px"></td>
|
||||
<td><samp>16A9J59ahMRqkLSZjhYj33n9j3fMztFxnh</samp></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="https://liberapay.com/TeamNewPipe/"><img src="https://upload.wikimedia.org/wikipedia/commons/2/27/Liberapay_logo_v2_white-on-yellow.svg" alt="Liberapay" width="80px" ></a></td>
|
||||
<td><a href="https://liberapay.com/TeamNewPipe/"><img src="assets/liberapay_qr_code.png" alt="liberapay.com で NewPipe を訪れる" width="100px"></a></td>
|
||||
<td><a href="https://liberapay.com/TeamNewPipe/donate"><img src="assets/liberapay_donate_button.svg" alt="Liberapay で寄付" height="35px"></a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="https://www.bountysource.com/teams/newpipe"><img src="https://upload.wikimedia.org/wikipedia/commons/thumb/2/22/Bountysource.png/320px-Bountysource.png" alt="Bountysource" width="190px"></a></td>
|
||||
<td><a href="https://www.bountysource.com/teams/newpipe"><img src="assets/bountysource_qr_code.png" alt="bountysource.com で NewPipe を訪れる" 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="あなたがどれほどの寄付を得られるのか確認しましょう。"></a></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
## プライバシーポリシー
|
||||
NewPipe プロジェクトはメディアウェブサービスを使用する上でのプライベートで匿名の体験を提供することを目的としています。
|
||||
そのため、アプリはあなたの同意なしで一切のデータを収集しません。NewPipe のプライバシーポリシーはあなたがクラッシュレポートまたは、私たちのブログでコメントを送信した場合にどのようなデータが送信され、保存されるのかを詳細に説明しています。そのドキュメントは[こちら](https://newpipe.net/legal/privacy/)から見つけることができます。
|
||||
|
||||
<span id="license"></span>
|
||||
## ライセンス
|
||||
[](https://www.gnu.org/licenses/gpl-3.0.en.html)
|
||||
|
||||
NewPipe はフリーソフトウェアなので、あなたはあなたの望むように使用、習得、共有、改善を行えます。
|
||||
具体的には、フリーソフトウェア財団により公開された [GNU General Public License](https://www.gnu.org/licenses/gpl.html) のバージョン3のライセンスもしくは、(あなたの選択で) いずれかの後継バージョンの規約の元で配布または改変を行うことができます。
|
||||
146
README.ko.md
Normal file
146
README.ko.md
Normal file
@@ -0,0 +1,146 @@
|
||||
<p align="center"><a href="https://newpipe.net"><img src="assets/new_pipe_icon_5.png" width="150"></a></p>
|
||||
<h2 align="center"><b>NewPipe</b></h2>
|
||||
<h4 align="center">A libre lightweight streaming frontend for Android.</h4>
|
||||
|
||||
<p align="center"><a href="https://f-droid.org/packages/org.schabi.newpipe/"><img src="https://fdroid.gitlab.io/artwork/badge/get-it-on-ko.svg"></a></p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/TeamNewPipe/NewPipe/releases" alt="GitHub release"><img src="https://img.shields.io/github/release/TeamNewPipe/NewPipe.svg" ></a>
|
||||
<a href="https://www.gnu.org/licenses/gpl-3.0" alt="License: GPLv3"><img src="https://img.shields.io/badge/License-GPL%20v3-blue.svg"></a>
|
||||
<a href="https://github.com/TeamNewPipe/NewPipe/actions" alt="Build Status"><img src="https://github.com/TeamNewPipe/NewPipe/workflows/CI/badge.svg?branch=dev&event=push"></a>
|
||||
<a href="https://hosted.weblate.org/engage/newpipe/" alt="Translation Status"><img src="https://hosted.weblate.org/widgets/newpipe/-/svg-badge.svg"></a>
|
||||
<a href="https://webchat.freenode.net/#newpipe" alt="IRC channel: #newpipe"><img src="https://img.shields.io/badge/IRC%20chat-%23newpipe-brightgreen.svg"></a>
|
||||
<a href="https://www.bountysource.com/teams/newpipe" alt="Bountysource bounties"><img src="https://img.shields.io/bountysource/team/newpipe/activity.svg?colorB=cd201f"></a>
|
||||
</p>
|
||||
<hr>
|
||||
<p align="center"><a href="#screenshots">Screenshots</a> • <a href="#description">Description</a> • <a href="#features">Features</a> • <a href="#updates">Updates</a> • <a href="#contribution">Contribution</a> • <a href="#donate">Donate</a> • <a href="#license">License</a></p>
|
||||
<p align="center"><a href="https://newpipe.net">Website</a> • <a href="https://newpipe.net/blog/">Blog</a> • <a href="https://newpipe.net/FAQ/">FAQ</a> • <a href="https://newpipe.net/press/">Press</a></p>
|
||||
<hr>
|
||||
|
||||
*Read this in other languages: [English](README.md), [Español](README.es.md), [한국어](README.ko.md), [Soomaali](README.so.md), [Português Brasil](README.pt_BR.md), [日本語](README.ja.md), [Română](README.ro.md), [Türkçe](README.tr.md).*
|
||||
|
||||
<b>경고: 이 버전은 베타 버전이므로, 버그가 발생할 수도 있습니다. 만약 버그가 발생하였다면, 우리의 GITHUB 저장소에서 ISSUE를 열람하여 주십시오.</b>
|
||||
|
||||
<b>NEWPIPE 또는 이것의 FORK을 구글 플레이스토어에 올리는 것은 그들의 이용약관을 위반합니다.</b>
|
||||
|
||||
## Screenshots
|
||||
|
||||
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_01.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_01.png)
|
||||
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_02.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_02.png)
|
||||
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_03.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_03.png)
|
||||
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_04.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_04.png)
|
||||
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_05.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_05.png)
|
||||
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_06.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_06.png)
|
||||
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_07.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_07.png)
|
||||
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_08.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_08.png)
|
||||
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_09.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_09.png)
|
||||
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_10.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_10.png)
|
||||
[<img src="fastlane/metadata/android/en-US/images/tenInchScreenshots/shot_11.png" width=405>](fastlane/metadata/android/en-US/images/tenInchScreenshots/shot_11.png)
|
||||
[<img src="fastlane/metadata/android/en-US/images/tenInchScreenshots/shot_12.png" width=405>](fastlane/metadata/android/en-US/images/tenInchScreenshots/shot_12.png)
|
||||
|
||||
## Description
|
||||
|
||||
NewPipe는 어떤 구글 프레임워크 라이브러리나, 유튜브 API를 사용하지 않습니다. 웹사이트는 단지 필요한 정보를 가져오기 위해 구문 분석 됩니다. 따라서 이 앱은 구글 서비스의 설치 없이 기기에서 사용될 수 있습니다. 또한, 카피레프트 자유 소프트웨어인 NewPipe를 사용하기 위해 유튜브 계정이 필요하지 않습니다.
|
||||
|
||||
### Features
|
||||
|
||||
* 영상 검색
|
||||
* 영상의 일반적인 정보 표시
|
||||
* 유튜브 영상 보기
|
||||
* 유튜브 영상 듣기
|
||||
* 팝업 모드 (floating player)
|
||||
* 영상 공유
|
||||
* 영상 다운로드
|
||||
* 음성만 다운로드
|
||||
* Kodi에서 영상 열람
|
||||
* 다음/관련된 영상 표시
|
||||
* 특정 언어로 유튜브 검색
|
||||
* 연령 제한 컨텐츠 시청/차단
|
||||
* 채널에 대한 일반적인 정보 표시
|
||||
* 채널 검색
|
||||
* 채널에서 영상 시청
|
||||
* Orbot/Tor 지원 (아직 직접적이지 않음)
|
||||
* 1080p/2K/4K 지원
|
||||
* 기록 보기
|
||||
* 채널 구독
|
||||
* 기록 검색
|
||||
* 재생목록 검색/시청
|
||||
* 추가된 재생목록 시청
|
||||
* 영상 추가
|
||||
* 지역 재생목록
|
||||
* 자막
|
||||
* 실시간 방송 지원
|
||||
* 댓글 표시
|
||||
|
||||
### Supported Services
|
||||
|
||||
NewPipe는 여러가지 서비스를 지원합니다. 우리의 [문서](https://teamnewpipe.github.io/documentation/)는 새로운 서비스가 앱과 추출기에 어떻게 추가될 수 있는지에 대한 더 많은 정보를 제공합니다. 만약 새로운 서비스를 추가하고자 한다면, 우리에게 연락해 주시기 바랍니다. 현재 지원되는 서비스:
|
||||
|
||||
* YouTube
|
||||
* SoundCloud \[beta\]
|
||||
* media.ccc.de \[beta\]
|
||||
* PeerTube instances \[beta\]
|
||||
* Bandcamp \[beta\]
|
||||
|
||||
## Updates
|
||||
NewPipe 코드의 변경이 있을 때(기능 추가 또는 버그 수정으로 인해), 결국 릴리즈가 발생할 것입니다. 이것들의 형식은 x.xx.x 입니다.
|
||||
이 새로운 버전을 얻기 위해서, 당신은:
|
||||
1. 직접 디버그 APK를 생성할 수 있습니다. 이 방법은 당신의 기기에서 새로운 기능을 얻을 수 있는 가장 빠른 방법이지만, 꽤 많이 복잡합니다.
|
||||
따라서 우리는 다른 방법들 중 하나를 사용하는 것을 추천합니다.
|
||||
2. 우리의 커스텀 저장소를 F-Droid에 추가하고 우리가 릴리즈를 게시하는 대로 저곳에서 릴리즈를 설치할 수 있습니다.
|
||||
이에 대한 설명서는 이곳에서 확인할 수 있습니다: https://newpipe.net/FAQ/tutorials/install-add-fdroid-repo/
|
||||
3. 우리가 릴리즈를 게시하는 대로 [Github Releases](https://github.com/TeamNewPipe/NewPipe/releases)에서 APK를 다운받고 이것을 설치할 수 있습니다.
|
||||
4. F-Droid를 통해 업데이트 할 수 있습니다. F-Droid는 변화를 인식하고, 스스로 APK를 생성하고, 이것에 서명하고, 사용자들에서 업데이트를 전달해야만 하기 때문에,
|
||||
이것은 업데이트를 받는 가장 느린 방법입니다.
|
||||
|
||||
우리는 대부분의 사용자에게 2번쨰 방법을 추천합니다. 방법 2 또는 3을 사용하여 설치된 APK는 서로 호환되지만, 방법 4를 사용하여 설치된 것들과는 호환되지 않습니다. 이것은 방법 2 또는 3에서는 같은 (우리의)서명 키가 사용되지만, 방법 4에서는 다른 (F-Droid의)서명 키가 사용되기 때문입니다. 방법 1을 사용하여 디버그 APK를 생성하는 것에서는 키가 완전히 제외됩니다. 서명 키는 사용자가 앱에 악의적인 업데이트를 설치하는 것에 대해 속지 않도록 보장하는 것을 도와줍니다.
|
||||
|
||||
한편, 만약 어떠한 이유(예. NewPipe의 핵심 기능이 손상되었고 F-Droid가 아직 업데이트를 가지지 않는 경우) 때문에 소스를 바꾸길 원한다면,
|
||||
우리는 다음과 같은 절차를 따르는 것을 권장합니다:
|
||||
1. 당신의 기록, 구독, 그리고 재생목록을 유지할 수 있도록 Settings > Content > Export Database 를 통해 데이터를 백업하십시오.
|
||||
2. NewPipe를 삭제하십시오.
|
||||
3. 새로운 소스에서 APK를 다운로드하고 이것을 설치하십시오.
|
||||
4. Step 1의 Settings > Content > Import Database 을 통해 데이터를 불러오십시오.
|
||||
|
||||
## Contribution
|
||||
당신이 아이디어, 번역, 디자인 변경, 코드 정리, 또는 정말 큰 코드 수정에 대한 의견이 있다면, 도움은 항상 환영합니다.
|
||||
더 많이 수행될수록 더 많이 발전할 수 있습니다!
|
||||
|
||||
만약 참여하고 싶다면, 우리의 [컨트리뷰션 공지](.github/CONTRIBUTING.md)를 참고하십시오.
|
||||
|
||||
<a href="https://hosted.weblate.org/engage/newpipe/">
|
||||
<img src="https://hosted.weblate.org/widgets/newpipe/-/287x66-grey.png" alt="Translation status" />
|
||||
</a>
|
||||
|
||||
## Donate
|
||||
만약 NewPipe가 마음에 들었다면, 우리는 기부에 대해 기꺼이 환영합니다. bitcoin을 보내거나, Bountysource 또는 Liberapay를 통해 기부할 수 있습니다. NewPipe에 기부하는 것에 대한 자세한 정보를 원한다면, 우리의 [웹사이트](https://newpipe.net/donate)를 방문하여 주십시오.
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<td><img src="https://bitcoin.org/img/icons/logotop.svg" alt="Bitcoin"></td>
|
||||
<td><img src="assets/bitcoin_qr_code.png" alt="Bitcoin QR code" width="100px"></td>
|
||||
<td><samp>16A9J59ahMRqkLSZjhYj33n9j3fMztFxnh</samp></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="https://liberapay.com/TeamNewPipe/"><img src="https://upload.wikimedia.org/wikipedia/commons/2/27/Liberapay_logo_v2_white-on-yellow.svg" alt="Liberapay" width="80px" ></a></td>
|
||||
<td><a href="https://liberapay.com/TeamNewPipe/"><img src="assets/liberapay_qr_code.png" alt="Visit NewPipe at liberapay.com" width="100px"></a></td>
|
||||
<td><a href="https://liberapay.com/TeamNewPipe/donate"><img src="assets/liberapay_donate_button.svg" alt="Donate via Liberapay" height="35px"></a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="https://www.bountysource.com/teams/newpipe"><img src="https://upload.wikimedia.org/wikipedia/commons/thumb/2/22/Bountysource.png/320px-Bountysource.png" alt="Bountysource" width="190px"></a></td>
|
||||
<td><a href="https://www.bountysource.com/teams/newpipe"><img src="assets/bountysource_qr_code.png" alt="Visit NewPipe at bountysource.com" width="100px"></a></td>
|
||||
<td><a href="https://www.bountysource.com/teams/newpipe/issues"><img src="https://img.shields.io/bountysource/team/newpipe/activity.svg?colorB=cd201f" height="30px" alt="Check out how many bounties you can earn."></a></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
## Privacy Policy
|
||||
|
||||
NewPipe 프로젝트는 미디어 웹 서비스를 사용하는 것에 대한 사적의, 익명의 경험을 제공하는 것을 목표로 하고 있습니다.
|
||||
그러므로, 앱은 당신의 동의 없이 어떤 데이터도 수집하지 않습니다. NewPipe의 개인정보보호정책은 당신이 충돌 리포트를 보내거나, 또는 우리의 블로그에 글을 남길 때 어떤 데이터가 보내지고 저장되는지에 대해 상세히 설명합니다. 이 문서는 [여기](https://newpipe.net/legal/privacy/)에서 확인할 수 있습니다.
|
||||
|
||||
## License
|
||||
[](https://www.gnu.org/licenses/gpl-3.0.en.html)
|
||||
|
||||
NewPipe는 자유 소프트웨어입니다: 당신의 마음대로 이것을 사용하고, 연구하고, 공유하고, 개선할 수 있습니다.
|
||||
구체적으로 당신은 자유 소프트웨어 재단에서 발행되는, 버전 3 또는 (당신의 선택에 따라)이후 버전의,
|
||||
[GNU General Public License](https://www.gnu.org/licenses/gpl.html) 하에서 이것을 재배포 및/또는 수정할 수 있습니다.
|
||||
56
README.md
56
README.md
@@ -1,24 +1,27 @@
|
||||
<p align="center"><a href="https://newpipe.schabi.org"><img src="assets/new_pipe_icon_5.png" width="150"></a></p>
|
||||
<p align="center"><a href="https://newpipe.net"><img src="assets/new_pipe_icon_5.png" width="150"></a></p>
|
||||
<h2 align="center"><b>NewPipe</b></h2>
|
||||
<h4 align="center">A libre lightweight streaming frontend for Android.</h4>
|
||||
<p align="center"><a href="https://f-droid.org/packages/org.schabi.newpipe/"><img src="https://f-droid.org/wiki/images/0/06/F-Droid-button_get-it-on.png"></a></p>
|
||||
|
||||
<p align="center"><a href="https://f-droid.org/packages/org.schabi.newpipe/"><img src="https://f-droid.org/wiki/images/0/06/F-Droid-button_get-it-on.png"></a></p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/TeamNewPipe/NewPipe/releases" alt="GitHub release"><img src="https://img.shields.io/github/release/TeamNewPipe/NewPipe.svg" ></a>
|
||||
<a href="https://www.gnu.org/licenses/gpl-3.0" alt="License: GPLv3"><img src="https://img.shields.io/badge/License-GPL%20v3-blue.svg"></a>
|
||||
<a href="https://travis-ci.org/TeamNewPipe/NewPipe" alt="Build Status"><img src="https://travis-ci.org/TeamNewPipe/NewPipe.svg"></a>
|
||||
<a href="https://github.com/TeamNewPipe/NewPipe/actions" alt="Build Status"><img src="https://github.com/TeamNewPipe/NewPipe/workflows/CI/badge.svg?branch=dev&event=push"></a>
|
||||
<a href="https://hosted.weblate.org/engage/newpipe/" alt="Translation Status"><img src="https://hosted.weblate.org/widgets/newpipe/-/svg-badge.svg"></a>
|
||||
<a href="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://webchat.freenode.net/#newpipe" alt="IRC channel: #newpipe"><img src="https://img.shields.io/badge/IRC%20chat-%23newpipe-brightgreen.svg"></a>
|
||||
<a href="https://www.bountysource.com/teams/newpipe" alt="Bountysource bounties"><img src="https://img.shields.io/bountysource/team/newpipe/activity.svg?colorB=cd201f"></a>
|
||||
</p>
|
||||
<hr>
|
||||
<p align="center"><a href="#screenshots">Screenshots</a> • <a href="#description">Description</a> • <a href="#features">Features</a> • <a href="#updates">Updates</a> • <a href="#contribution">Contribution</a> • <a href="#donate">Donate</a> • <a href="#license">License</a></p>
|
||||
<p align="center"><a href="https://newpipe.schabi.org">Website</a> • <a href="https://newpipe.schabi.org/blog/">Blog</a> • <a href="https://newpipe.schabi.org/FAQ/">FAQ</a> • <a href="https://newpipe.schabi.org/press/">Press</a></p>
|
||||
<p align="center"><a href="#screenshots">Screenshots</a> • <a href="#description">Description</a> • <a href="#features">Features</a> • <a href="#installation-and-updates">Installation and updates</a> • <a href="#contribution">Contribution</a> • <a href="#donate">Donate</a> • <a href="#license">License</a></p>
|
||||
<p align="center"><a href="https://newpipe.net">Website</a> • <a href="https://newpipe.net/blog/">Blog</a> • <a href="https://newpipe.net/FAQ/">FAQ</a> • <a href="https://newpipe.net/press/">Press</a></p>
|
||||
<hr>
|
||||
|
||||
*Read this in other languages: [English](README.md), [Español](README.es.md), [한국어](README.ko.md), [Soomaali](README.so.md), [Português Brasil](README.pt_BR.md), [日本語](README.ja.md), [Română](README.ro.md), [Türkçe](README.tr.md).*
|
||||
|
||||
<b>WARNING: THIS IS A BETA VERSION, THEREFORE YOU MAY ENCOUNTER BUGS. IF YOU DO, OPEN AN ISSUE VIA OUR GITHUB REPOSITORY.</b>
|
||||
|
||||
<b>PUTTING NEWPIPE OR ANY FORK OF IT INTO GOOGLE PLAYSTORE VIOLATES THEIR TERMS OF CONDITIONS.</b>
|
||||
<b>PUTTING NEWPIPE OR ANY FORK OF IT INTO THE GOOGLE PLAY STORE VIOLATES THEIR TERMS AND CONDITIONS.</b>
|
||||
|
||||
## Screenshots
|
||||
|
||||
@@ -42,6 +45,7 @@ NewPipe does not use any Google framework libraries, nor the YouTube API. Websit
|
||||
### Features
|
||||
|
||||
* Search videos
|
||||
* No Login Required
|
||||
* Display general info about videos
|
||||
* Watch YouTube videos
|
||||
* Listen to YouTube videos
|
||||
@@ -69,11 +73,6 @@ NewPipe does not use any Google framework libraries, nor the YouTube API. Websit
|
||||
* Livestream support
|
||||
* Show comments
|
||||
|
||||
### Coming Features
|
||||
|
||||
* Cast to UPnP and Cast
|
||||
* … and many more
|
||||
|
||||
### Supported Services
|
||||
|
||||
NewPipe supports multiple services. Our [docs](https://teamnewpipe.github.io/documentation/) provide more info on how a new service can be added to the app and the extractor. Please get in touch with us if you intend to add a new one. Currently supported services are:
|
||||
@@ -82,20 +81,25 @@ NewPipe supports multiple services. Our [docs](https://teamnewpipe.github.io/doc
|
||||
* SoundCloud \[beta\]
|
||||
* media.ccc.de \[beta\]
|
||||
* PeerTube instances \[beta\]
|
||||
* Bandcamp \[beta\]
|
||||
|
||||
## Updates
|
||||
When a change to the NewPipe code occurs (due to either adding features or bug fixing), eventually a release will occur. These are in the format x.xx.x . In order to get this new version, you can:
|
||||
* Build a debug APK yourself. This is the fastest way to get new features on your device, but is much more complicated, so we recommend using one of the other methods.
|
||||
* Download the APK from [releases](https://github.com/TeamNewPipe/NewPipe/releases) and install it.
|
||||
* Update via F-droid. This is the slowest method of getting updates, as F-Droid must recognize changes, build the APK itself, sign it, then push the update to users.
|
||||
<!-- Hidden span to keep old links compatible. -->
|
||||
<span id="updates"></span>
|
||||
|
||||
When you install an APK from one of these options, it will be incompatible with an APK from one of the other options. This is due to different signing keys being used. Signing keys help ensure that a user isn't tricked into installing a malicious update to an app, and are independent. F-Droid and GitHub use different signing keys, and building an APK debug excludes a key. The signing key issue is being discussed in issue [#1981](https://github.com/TeamNewPipe/NewPipe/issues/1981), and may be fixed by setting up our own repository on F-Droid.
|
||||
## Installation and updates
|
||||
You can install NewPipe using one of the following methods:
|
||||
1. Add our custom repo to F-Droid and install it from there. The instructions are here: https://newpipe.net/FAQ/tutorials/install-add-fdroid-repo/
|
||||
2. Download the APK from [Github Releases](https://github.com/TeamNewPipe/NewPipe/releases) and install it.
|
||||
3. Update via F-Droid. This is the slowest method of getting updates, as F-Droid must recognize changes, build the APK itself, sign it, then push the update to users.
|
||||
4. Build a debug APK yourself. This is the fastest way to get new features on your device, but is much more complicated, so we recommend using one of the other methods.
|
||||
|
||||
In the meanwhile, if you want to switch sources for some reason (e.g. NewPipe's core functionality was broken and F-Droid doesn't have the update yet), we recommend following this procedure:
|
||||
1. Back up your data via "Settings>Content>Export Database" so you keep your history, subscriptions, and playlists
|
||||
We recommend method 1 for most users. APKs installed using method 1 or 2 are compatible with each other, but not with those installed using method 3. This is due to the same signing key (ours) being used for 1 and 2, but a different signing key (F-Droid's) being used for 3. Building a debug APK using method 4 excludes a key entirely. Signing keys help ensure that a user isn't tricked into installing a malicious update to an app.
|
||||
|
||||
In the meanwhile, if you want to switch sources for some reason (e.g. NewPipe's core functionality breaks and F-Droid doesn't have the latest update yet), we recommend following this procedure:
|
||||
1. Back up your data via Settings > Content > Export Database so you keep your history, subscriptions, and playlists
|
||||
2. Uninstall NewPipe
|
||||
3. Download the APK from the new source and install it
|
||||
4. Import the data from step 1 via "Settings>Content>Import Database"
|
||||
4. Import the data from step 1 via Settings > Content > Import Database
|
||||
|
||||
## Contribution
|
||||
Whether you have ideas, translations, design changes, code cleaning, or real heavy code changes, help is always welcome.
|
||||
@@ -103,8 +107,12 @@ The more is done the better it gets!
|
||||
|
||||
If you'd like to get involved, check our [contribution notes](.github/CONTRIBUTING.md).
|
||||
|
||||
<a href="https://hosted.weblate.org/engage/newpipe/">
|
||||
<img src="https://hosted.weblate.org/widgets/newpipe/-/287x66-grey.png" alt="Translation status" />
|
||||
</a>
|
||||
|
||||
## Donate
|
||||
If you like NewPipe we'd be happy about a donation. You can either send bitcoin or donate via Bountysource or Liberapay. For further info on donating to NewPipe, please visit our [website](https://newpipe.schabi.org/donate).
|
||||
If you like NewPipe we'd be happy about a donation. You can either send bitcoin or donate via Bountysource or Liberapay. For further info on donating to NewPipe, please visit our [website](https://newpipe.net/donate).
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
@@ -127,10 +135,10 @@ If you like NewPipe we'd be happy about a donation. You can either send bitcoin
|
||||
## Privacy Policy
|
||||
|
||||
The NewPipe project aims to provide a private, anonymous experience for using media web services.
|
||||
Therefore, the app does not collect any data without your consent. NewPipe's privacy policy explains in detail what data is sent and stored when you send a crash report, or comment in our blog. You can find the document [here](https://newpipe.schabi.org/legal/privacy/).
|
||||
Therefore, the app does not collect any data without your consent. NewPipe's privacy policy explains in detail what data is sent and stored when you send a crash report, or comment in our blog. You can find the document [here](https://newpipe.net/legal/privacy/).
|
||||
|
||||
## License
|
||||
[](http://www.gnu.org/licenses/gpl-3.0.en.html)
|
||||
[](https://www.gnu.org/licenses/gpl-3.0.en.html)
|
||||
|
||||
NewPipe is Free Software: You can use, study share and improve it at your
|
||||
will. Specifically you can redistribute and/or modify it under the terms of the
|
||||
|
||||
143
README.pt_BR.md
Normal file
143
README.pt_BR.md
Normal file
@@ -0,0 +1,143 @@
|
||||
<p align="center"><a href="https://newpipe.net"><img src="assets/new_pipe_icon_5.png" width="150"></a></p>
|
||||
|
||||
<h2 align="center"><b>NewPipe</b></h2>
|
||||
<h4 align="center">Uma interface de streaming leve e gratuita para Android.</h4>
|
||||
|
||||
<p align="center"><a href="https://f-droid.org/packages/org.schabi.newpipe/"><img src="https://fdroid.gitlab.io/artwork/badge/get-it-on-pt-br.svg"></a></p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/TeamNewPipe/NewPipe/releases" alt="GitHub release"><img src="https://img.shields.io/github/release/TeamNewPipe/NewPipe.svg" ></a>
|
||||
<a href="https://www.gnu.org/licenses/gpl-3.0" alt="License: GPLv3"><img src="https://img.shields.io/badge/License-GPL%20v3-blue.svg"></a>
|
||||
<a href="https://github.com/TeamNewPipe/NewPipe/actions" alt="Build Status"><img src="https://github.com/TeamNewPipe/NewPipe/workflows/CI/badge.svg?branch=dev&event=push"></a>
|
||||
<a href="https://hosted.weblate.org/engage/newpipe/" alt="Translation Status"><img src="https://hosted.weblate.org/widgets/newpipe/-/svg-badge.svg"></a>
|
||||
<a href="https://webchat.freenode.net/#newpipe" 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">Descrição</a> • <a href="#features">Características</a> • <a href="#updates">Atualizações</a> • <a href="#contribution">Contribuição</a> • <a href="#donate">Doar</a> • <a href="#license">Licença</a></p>
|
||||
<p align="center"><a href="https://newpipe.net">Site</a> • <a href="https://newpipe.net/blog/">Blog</a> • <a href="https://newpipe.net/FAQ/">FAQ</a> • <a href="https://newpipe.net/press/">Press</a></p>
|
||||
<hr>
|
||||
|
||||
*Read this in other languages: [English](README.md), [Español](README.es.md), [한국어](README.ko.md), [Soomaali](README.so.md), [Português Brasil](README.pt_BR.md), [日本語](README.ja.md), [Română](README.ro.md), [Türkçe](README.tr.md).*
|
||||
|
||||
<b>AVISO: ESTA É UMA VERSÃO BETA, PORTANTO, VOCÊ PODE ENCONTRAR BUGS. ENCONTROU ALGUM, ABRA UM ISSUE ATRAVÉS DO NOSSO REPOSITÓRIO GITHUB.</b>
|
||||
|
||||
<b>COLOCAR NEWPIPE OU QUALQUER FORK DELE NA GOOGLE PLAY STORE VIOLA SEUS TERMOS E CONDIÇÕES.</b>
|
||||
|
||||
## Screenshots
|
||||
|
||||
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_01.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_01.png)
|
||||
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_02.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_02.png)
|
||||
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_03.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_03.png)
|
||||
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_04.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_04.png)
|
||||
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_05.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_05.png)
|
||||
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_06.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_06.png)
|
||||
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_07.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_07.png)
|
||||
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_08.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_08.png)
|
||||
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_09.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_09.png)
|
||||
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_10.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_10.png)
|
||||
[<img src="fastlane/metadata/android/en-US/images/tenInchScreenshots/shot_11.png" width=405>](fastlane/metadata/android/en-US/images/tenInchScreenshots/shot_11.png)
|
||||
[<img src="fastlane/metadata/android/en-US/images/tenInchScreenshots/shot_12.png" width=405>](fastlane/metadata/android/en-US/images/tenInchScreenshots/shot_12.png)
|
||||
|
||||
## Descrição
|
||||
|
||||
O NewPipe não usa nenhuma biblioteca de framework do Google, nem a API do YouTube. Os sites são apenas analisados para obter informações necessárias, para que este aplicativo possa ser usado em dispositivos sem os serviços do Google instalados. Além disso, você não precisa de uma conta no YouTube para usar o NewPipe, que é um software livre com copyleft.
|
||||
|
||||
### Características
|
||||
|
||||
* Procurar vídeos
|
||||
* Exibir informações gerais sobre vídeos
|
||||
* Assista aos vídeos do YouTube
|
||||
* Ouça vídeos do YouTube
|
||||
* Modo popup (player flutuante)
|
||||
* Selecione o player para assistir streaming
|
||||
* Baixar vídeos
|
||||
* Baixar somente áudio
|
||||
* Abrir vídeo no Kodi
|
||||
* Mostrar vídeos próximos/relacionados
|
||||
* Pesquise no YouTube em um idioma específico
|
||||
* Assistir/Bloquear material restrito
|
||||
* Exibir informações gerais sobre canais
|
||||
* Pesquisar canais
|
||||
* Assista a vídeos de um canal
|
||||
* Suporte Orbot/Tor (ainda não diretamente)
|
||||
* Suporte 1080p/2K/4K
|
||||
* Ver histórico
|
||||
* Inscreva-se nos canais
|
||||
* Procurar histórico
|
||||
* Porcurar/Assistir playlists
|
||||
* Assistir playlists em fila
|
||||
* Vídeos em fila
|
||||
* Playlists Local
|
||||
* Legenda
|
||||
* Suporte a live
|
||||
* Mostrar comentários
|
||||
|
||||
### Serviços Suportados
|
||||
|
||||
O NewPipe suporta vários serviços. Nosso [documentação](https://teamnewpipe.github.io/documentation/) fornecer mais informações sobre como um novo serviço pode ser adicionado ao aplicativo e ao extrator. Por favor, entre em contato conosco se você pretende adicionar um novo. Atualmente, os serviços suportados são:
|
||||
|
||||
* YouTube
|
||||
* SoundCloud \[beta\]
|
||||
* media.ccc.de \[beta\]
|
||||
* PeerTube instances \[beta\]
|
||||
* Bandcamp \[beta\]
|
||||
|
||||
## Atualizações
|
||||
Quando uma alteração no código NewPipe (devido à adição de recursos ou fixação de bugs), eventualmente ocorrerá uma versão. Estes estão no formato x.xx.x . A fim de obter esta nova versão, você pode:
|
||||
1. Construa um APK de depuração você mesmo. Esta é a maneira mais rápida de obter novos recursos em seu dispositivo, mas é muito mais complicado, por isso recomendamos usar um dos outros métodos.
|
||||
2. Adicione nosso repo personalizado ao F-Droid e instale-o a partir daí assim que publicarmos um lançamento. As instruções estão aqui.: https://newpipe.net/FAQ/tutorials/install-add-fdroid-repo/
|
||||
3. Baixe o APK do [Github Releases](https://github.com/TeamNewPipe/NewPipe/releases) e instalá-lo assim que publicarmos um lançamento.
|
||||
4. Atualização via F-droid. Este é o método mais lento para obter atualizações, pois o F-Droid deve reconhecer alterações, construir o próprio APK, assiná-lo e, em seguida, enviar a atualização para os usuários.
|
||||
|
||||
Recomendamos o método 2 para a maioria dos usuários. Os APKs instalados usando o método 2 ou 3 são compatíveis entre si, mas não com aqueles instalados usando o método 4. Isso se deve à mesma chave de assinatura (nossa) sendo usada para 2 e 3, mas uma chave de assinatura diferente (F-Droid's) está sendo usada para 4. Construir um APK depuração usando o método 1 exclui totalmente uma chave. Assinar chaves ajudam a garantir que um usuário não seja enganado para instalar uma atualização maliciosa em um aplicativo.
|
||||
|
||||
Enquanto isso, se você quiser trocar de fontes por algum motivo (por exemplo, a funcionalidade principal do NewPipe foi quebrada e o F-Droid ainda não tem a atualização), recomendamos seguir este procedimento:
|
||||
1. Faça backup de seus dados através de Configurações > Conteúdo > Exportar Base de Dados para que você mantenha seu histórico, inscrições e playlists
|
||||
2. Desinstale o NewPipe
|
||||
3. Baixe o APK da nova fonte e instale-o
|
||||
4. Importe os dados da etapa 1 via Configurações > Conteúdo > Inportar Banco de Dados
|
||||
|
||||
## Contribuição
|
||||
Se você tem ideias, traduções, alterações de design, limpeza de códigos ou mudanças reais de código, a ajuda é sempre bem-vinda.
|
||||
Quanto mais for feito, melhor fica!
|
||||
|
||||
Se você quiser se envolver, verifique nossa [notas de contribuição](.github/CONTRIBUTING.md).
|
||||
|
||||
<a href="https://hosted.weblate.org/engage/newpipe/">
|
||||
<img src="https://hosted.weblate.org/widgets/newpipe/-/287x66-grey.png" alt="Translation status" />
|
||||
</a>
|
||||
|
||||
## Doar
|
||||
Se você gosta de NewPipe, ficaríamos felizes com uma doação. Você pode enviar bitcoin ou doar via Bountysource ou Liberapay. Para obter mais informações sobre como doar para a NewPipe, visite nosso [site](https://newpipe.net/donate).
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<td><img src="https://bitcoin.org/img/icons/logotop.svg" alt="Bitcoin"></td>
|
||||
<td><img src="assets/bitcoin_qr_code.png" alt="Bitcoin QR code" width="100px"></td>
|
||||
<td><samp>16A9J59ahMRqkLSZjhYj33n9j3fMztFxnh</samp></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="https://liberapay.com/TeamNewPipe/"><img src="https://upload.wikimedia.org/wikipedia/commons/2/27/Liberapay_logo_v2_white-on-yellow.svg" alt="Liberapay" width="80px" ></a></td>
|
||||
<td><a href="https://liberapay.com/TeamNewPipe/"><img src="assets/liberapay_qr_code.png" alt="Visit NewPipe at liberapay.com" width="100px"></a></td>
|
||||
<td><a href="https://liberapay.com/TeamNewPipe/donate"><img src="assets/liberapay_donate_button.svg" alt="Donate via Liberapay" height="35px"></a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="https://www.bountysource.com/teams/newpipe"><img src="https://upload.wikimedia.org/wikipedia/commons/thumb/2/22/Bountysource.png/320px-Bountysource.png" alt="Bountysource" width="190px"></a></td>
|
||||
<td><a href="https://www.bountysource.com/teams/newpipe"><img src="assets/bountysource_qr_code.png" alt="Visit NewPipe at bountysource.com" width="100px"></a></td>
|
||||
<td><a href="https://www.bountysource.com/teams/newpipe/issues"><img src="https://img.shields.io/bountysource/team/newpipe/activity.svg?colorB=cd201f" height="30px" alt="Check out how many bounties you can earn."></a></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
## Política de Privacidade
|
||||
|
||||
O projeto NewPipe tem como objetivo proporcionar uma experiência privada e anônima para o uso de serviços web de mídia.
|
||||
Portanto, o aplicativo não coleta nenhum dado sem o seu consentimento. A política de privacidade da NewPipe explica em detalhes quais dados são enviados e armazenados quando você envia um relatório de erro ou comenta em nosso blog. Você pode encontrar o documento [aqui](https://newpipe.net/legal/privacy/).
|
||||
|
||||
## Licença
|
||||
[](https://www.gnu.org/licenses/gpl-3.0.en.html)
|
||||
|
||||
NewPipe é Software Livre: Você pode usar, estudar compartilhamento e melhorá-lo à sua vontade.
|
||||
Especificamente, você pode redistribuir e/ou modificá-lo sob os termos do
|
||||
[GNU General Public License](https://www.gnu.org/licenses/gpl.html) publicado pela Free Software Foundation, seja a versão 3 da Licença, ou
|
||||
(a sua opção) qualquer versão posterior.
|
||||
145
README.ro.md
Normal file
145
README.ro.md
Normal file
@@ -0,0 +1,145 @@
|
||||
<p align="center"><a href="https://newpipe.net"><img src="assets/new_pipe_icon_5.png" width="150"></a></p>
|
||||
<h2 align="center"><b>NewPipe</b></h2>
|
||||
<h4 align="center">Un front-end de streaming „uşor” liber, pentru Android.</h4>
|
||||
|
||||
<p align="center"><a href="https://f-droid.org/packages/org.schabi.newpipe/"><img src="https://fdroid.gitlab.io/artwork/badge/get-it-on-ro.svg"></a></p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/TeamNewPipe/NewPipe/releases" alt="GitHub release"><img src="https://img.shields.io/github/release/TeamNewPipe/NewPipe.svg" ></a>
|
||||
<a href="https://www.gnu.org/licenses/gpl-3.0" alt="License: GPLv3"><img src="https://img.shields.io/badge/License-GPL%20v3-blue.svg"></a>
|
||||
<a href="https://github.com/TeamNewPipe/NewPipe/actions" alt="Build Status"><img src="https://github.com/TeamNewPipe/NewPipe/workflows/CI/badge.svg?branch=dev&event=push"></a>
|
||||
<a href="https://hosted.weblate.org/engage/newpipe/" alt="Translation Status"><img src="https://hosted.weblate.org/widgets/newpipe/-/svg-badge.svg"></a>
|
||||
<a href="https://webchat.freenode.net/#newpipe" 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">Capturi de ecran</a> • <a href="#description">Descriere</a> • <a href="#features">Funcţii</a> • <a href="#installation-and-updates">Instalare şi actualizări</a> • <a href="#contribution">Contribuţie</a> • <a href="#donate">Donaţi</a> • <a href="#license">Licenţă</a></p>
|
||||
<p align="center"><a href="https://newpipe.net">Website</a> • <a href="https://newpipe.net/blog/">Blog</a> • <a href="https://newpipe.net/FAQ/">FAQ</a> • <a href="https://newpipe.net/press/">Presă</a></p>
|
||||
<hr>
|
||||
|
||||
*Citiţi în alte limbi: [English](README.md), [Español](README.es.md), [한국어](README.ko.md), [Soomaali](README.so.md), [Português Brasil](README.pt_BR.md), [日本語](README.ja.md), [Română](README.ro.md), [Türkçe](README.tr.md).*
|
||||
|
||||
<b>Atenţionare: ACEASTA ESTE O VERSIUNE BETA, AŞA CĂ S-AR PUTE SĂ ÎNTÂLNIŢI ERORI. DACĂ SE ÎNTÂMPLĂ ACEST LUCRU, DESCHIDEŢI UN ISSUE PRIN REPSITORY-UL NOSTRU GITHUB.</b>
|
||||
|
||||
<b>PUNERA NEWPIPE SAU ORICĂRUI FORK AL ACESTUIA ÎN MAGAZINUL GOOGLE PLAY LE ÎNCALCĂ TERMENII ŞI CONDIŢIILE.</b>
|
||||
|
||||
## Capturi de ecran
|
||||
|
||||
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_01.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_01.png)
|
||||
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_02.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_02.png)
|
||||
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_03.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_03.png)
|
||||
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_04.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_04.png)
|
||||
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_05.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_05.png)
|
||||
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_06.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_06.png)
|
||||
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_07.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_07.png)
|
||||
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_08.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_08.png)
|
||||
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_09.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_09.png)
|
||||
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_10.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_10.png)
|
||||
[<img src="fastlane/metadata/android/en-US/images/tenInchScreenshots/shot_11.png" width=405>](fastlane/metadata/android/en-US/images/tenInchScreenshots/shot_11.png)
|
||||
[<img src="fastlane/metadata/android/en-US/images/tenInchScreenshots/shot_12.png" width=405>](fastlane/metadata/android/en-US/images/tenInchScreenshots/shot_12.png)
|
||||
|
||||
## Descriere
|
||||
|
||||
NewPipe nu foloseşte nici-o bibliotecă Google framework sau API-ul Youtube. Website-urile sunt doar analizate pentru a prelua informaţia necesară, aşa că această aplicaţie poate fi folosită pe telefoane fără Serviciile Google instalate. De asemenea, nu aveţi nevoie de un cont Youtube pentru a folosi Newpipe, care este sofware liber şi copylefted.
|
||||
|
||||
### Funcţii
|
||||
|
||||
* Căutarea videoclipurilor
|
||||
* Nu este necesară logarea
|
||||
* Afişarea informaţiilor generale despre videoclipuri
|
||||
* Urmărirea videoclipurilor Youtube
|
||||
* Ascultarea videoclipurilor Youtube
|
||||
* Modul popup (player plutitor)
|
||||
* Selectarea playerului de streaming pentru vizionarea videoclipului
|
||||
* Descărcarea videoclipurilor
|
||||
* Doar descărcarea sunetului
|
||||
* Deschiderea videoclipurilor cu Kodi
|
||||
* Expunerea videoclipurilor următoare/asociate
|
||||
* Căutarea YouTube într-o limbă specifică
|
||||
* Vizionarea/Blocarea materialului restricţionat în funcţie de vârstă
|
||||
* Afişarea informaţiilor generale despre canale
|
||||
* Căutarea canalelor
|
||||
* Vizionarea videoclipurilor dintr-un canal
|
||||
* Suport Orbot/Tor (încă nu direct)
|
||||
* Suport 1080p/2K/4K
|
||||
* Vizionarea istoricului
|
||||
* Abonarea la canale
|
||||
* Căutarea în istoric
|
||||
* Căutarea/vizionarea playlisturilor
|
||||
* Vizionarea ca playlisturi puse în coadă
|
||||
* Punerea în coadă a videoclipurilor
|
||||
* Playlisturi locale
|
||||
* Subtitrări
|
||||
* Suport al transmiterilor live
|
||||
* Afişarea comentariilor
|
||||
|
||||
### Servicii întreţinute
|
||||
|
||||
NewPipe suportă servicii multiple. [Documentele](https://teamnewpipe.github.io/documentation/) noastre furnizează mai multe informaţii în legătură cu modalităţile prin care un nou serviciu poate fi adăugat aplicaţiei şi extractorului. Vă rugăm să ne contactaţi dacă doriţi să adăugaţi unul nou. Serviciile întreţinute acum sunt:
|
||||
|
||||
* YouTube
|
||||
* SoundCloud \[beta\]
|
||||
* media.ccc.de \[beta\]
|
||||
* Instanţe PeerTube \[beta\]
|
||||
* Bandcamp \[beta\]
|
||||
|
||||
<!-- Hidden span to keep old links compatible. -->
|
||||
<span id="updates"></span>
|
||||
|
||||
## Instalare şi actualizări
|
||||
Puteţi instala NewPipe folosind una dintre următoarele metode:
|
||||
1. Adăugaţi depozitul nostru F-droid personalizat. Instrucţiunile sunt aici: https://newpipe.net/FAQ/tutorials/install-add-fdroid-repo/
|
||||
2. Descărcaţi APK-ul din [Github Releases](https://github.com/TeamNewPipe/NewPipe/releases) şi instalaţi-l.
|
||||
3. Actualizaţi via F-Droid. Aceasta este cea mai lentă metodă de a obţine actualizări, deoarece F-Droid trebuie să recunoască schimbările, să constriască APK-ul, să îl semneze, iar apoi să îl trimită utilizatorilor.
|
||||
4. Construiţi un APK de depanare. Aceasta este cea mai rapidă metodă de a primi funcţii noi, dar este mult mai complicată, aşa că vă recomandăm să folosiţi una dintre celelalte metode.
|
||||
|
||||
Recomandăm metoda 1 pentru majoritatea utilizatorilor. APK-urile din metodele 1 şi 2 suntcompatibile una cu cealaltă, dar nu cu cele din metoda 3. Acest lucru se datorează faptului că aceeași cheie de semnare (a noastră) este utilizată pentru 1 și 2, dar o altă cheie de semnare (F-Droid) este utilizată pentru 3. Construirea unui APK de depanare folosind metoda 4 exclude o cheie în întregime. Cheile de semnare vă asigură că un utilizator nu este păcălit să instaleze o actualizare rău intenționată a unei aplicații.
|
||||
|
||||
Între timp, dacă doriți să schimbați sursa dintr-un anumit motiv (de exemplu, funcționalitatea de bază a NewPipe a fost întreruptă și F-Droid nu are încă actualizarea), vă recomandăm să folosiţi următoarea procedură:
|
||||
1. Faceți o copie de rezervă a datelor prin Setări> Conținut> Exportați baza de date, astfel încât să vă păstrați istoricul, abonamentele și playlisturile
|
||||
2. Dezinstalaţi NewPipe
|
||||
3. Descărcaţi APK-ul din noua sursă şi instalaţi-l
|
||||
4. Importați datele de la pasul 1 prin Setări> Conținut> Importare bază de date
|
||||
|
||||
## Contribuţie
|
||||
Dacă aveţi idei, traduceri, schimbări de design, curaţarea codului, sau schimbări majore ale codului, ajutorul este întotdeauna binevenit.
|
||||
Cu cât se face mai mult cu atât mai bună devine aplicaţia!
|
||||
|
||||
Dacă doriţi să vă implicaţi, accesaţi [notele noastre de contribuţie](.github/CONTRIBUTING.md).
|
||||
|
||||
<a href="https://hosted.weblate.org/engage/newpipe/">
|
||||
<img src="https://hosted.weblate.org/widgets/newpipe/-/287x66-grey.png" alt="Translation status" />
|
||||
</a>
|
||||
|
||||
## Donaţii
|
||||
Dacă vă place NewPipe, am fi bucuroşi să primim o donaţie. Puteţi să ne trimiteţi bitcoin sau să ne donaţi cu Bountysource sau Liberapay. Pentru mai multe informaţii în legătură cu donaţiile către NewPipe, vă rugăm vizitaţi [website-ul nostru](https://newpipe.net/donate).
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<td><img src="https://bitcoin.org/img/icons/logotop.svg" alt="Bitcoin"></td>
|
||||
<td><img src="assets/bitcoin_qr_code.png" alt="Bitcoin QR code" width="100px"></td>
|
||||
<td><samp>16A9J59ahMRqkLSZjhYj33n9j3fMztFxnh</samp></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="https://liberapay.com/TeamNewPipe/"><img src="https://upload.wikimedia.org/wikipedia/commons/2/27/Liberapay_logo_v2_white-on-yellow.svg" alt="Liberapay" width="80px" ></a></td>
|
||||
<td><a href="https://liberapay.com/TeamNewPipe/"><img src="assets/liberapay_qr_code.png" alt="Visit NewPipe at liberapay.com" width="100px"></a></td>
|
||||
<td><a href="https://liberapay.com/TeamNewPipe/donate"><img src="assets/liberapay_donate_button.svg" alt="Donate via Liberapay" height="35px"></a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="https://www.bountysource.com/teams/newpipe"><img src="https://upload.wikimedia.org/wikipedia/commons/thumb/2/22/Bountysource.png/320px-Bountysource.png" alt="Bountysource" width="190px"></a></td>
|
||||
<td><a href="https://www.bountysource.com/teams/newpipe"><img src="assets/bountysource_qr_code.png" alt="Visit NewPipe at bountysource.com" width="100px"></a></td>
|
||||
<td><a href="https://www.bountysource.com/teams/newpipe/issues"><img src="https://img.shields.io/bountysource/team/newpipe/activity.svg?colorB=cd201f" height="30px" alt="Check out how many bounties you can earn."></a></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
## Politica de Confidenţialitate
|
||||
|
||||
Proiectul NewPipe îşi propune să furnizeze o experienţă privată şi anonimă pentru utilizarea serviciilor web media.
|
||||
Prin urmare, aplicaţia nu colectează niciun fel de informaţii fără acordul dumneavoastră. Politica de confidențialitate a NewPipe explică în detaliu ce date sunt trimise și stocate atunci când trimiteți un raport de blocare sau comentați pe blogul nostru. Puteți găsi documentul [aici](https://newpipe.net/legal/privacy/).
|
||||
|
||||
## Licenţă
|
||||
[](https://www.gnu.org/licenses/gpl-3.0.en.html)
|
||||
|
||||
NewPipe este Software Gratuit: Îl puteţi folosi şi împărtăşi cum doriţi. Mai exact, îl puteți redistribui și / sau modifica în conformitate cu termenii
|
||||
[GNU General Public License](https://www.gnu.org/licenses/gpl.html) aşa cum a fost publicat de Free Software Foundation, fie versiunea 3 a Licenței, fie
|
||||
(la alegerea dvs.) orice versiune ulterioară.
|
||||
138
README.so.md
Normal file
138
README.so.md
Normal file
@@ -0,0 +1,138 @@
|
||||
<p align="center"><a href="https://newpipe.net"><img src="assets/new_pipe_icon_5.png" width="150"></a></p>
|
||||
<h2 align="center"><b>NewPipe</b></h2>
|
||||
<h4 align="center">App bilaash ah oo fudud looguna talagalay in Android-ka wax loogu daawado.</h4>
|
||||
|
||||
<p align="center"><a href="https://f-droid.org/packages/org.schabi.newpipe/"><img src="https://fdroid.gitlab.io/artwork/badge/get-it-on-so.svg"></a></p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/TeamNewPipe/NewPipe/releases" alt="Siidaynta GitHub "><img src="https://img.shields.io/github/release/TeamNewPipe/NewPipe.svg" ></a>
|
||||
<a href="https://www.gnu.org/licenses/gpl-3.0" alt="Laysinka: GPLv3"><img src="https://img.shields.io/badge/License-GPL%20v3-blue.svg"></a>
|
||||
<a href="https://github.com/TeamNewPipe/NewPipe/actions" alt="Darajada Dhismaha"><img src="https://github.com/TeamNewPipe/NewPipe/workflows/CI/badge.svg?branch=dev&event=push"></a>
|
||||
<a href="https://hosted.weblate.org/engage/newpipe/" alt="Heerka Turjimaada"><img src="https://hosted.weblate.org/widgets/newpipe/-/svg-badge.svg"></a>
|
||||
<a href="https://webchat.freenode.net/#newpipe" alt="Kanaalka IRC: #newpipe"><img src="https://img.shields.io/badge/IRC%20chat-%23newpipe-brightgreen.svg"></a>
|
||||
<a href="https://www.bountysource.com/teams/newpipe" alt="Kuwa Bountysource "><img src="https://img.shields.io/bountysource/team/newpipe/activity.svg?colorB=cd201f"></a>
|
||||
</p>
|
||||
<hr>
|
||||
<p align="center"><a href="#sawir-shaashadeed">Sawir-shaashadeed</a> • <a href="#faahfaahin">Faahfaahin</a> • <a href="#waxqabadka">Waxqabadka</a> • <a href="#kushubida-iyo-cusboonaysiinta">Kushubida iyo cusboonaysiinta</a> • <a href="#kusoo-kordhin">Kusoo Kordhin</a> • <a href="#ugu-deeq">Ugu Deeq</a> • <a href="#laysinka">Laysinka</a></p>
|
||||
<p align="center"><a href="https://newpipe.net">Website-ka</a> • <a href="https://newpipe.net/blog/">Maqaalada</a> • <a href="https://newpipe.net/FAQ/">Su'aalaha Aalaa La-iswaydiiyo</a> • <a href="https://newpipe.net/press/">Warbaahinta</a></p>
|
||||
<hr>
|
||||
|
||||
*Ku akhri luuqad kale: [English](README.md), [Español](README.es.md), [한국어](README.ko.md), [Soomaali](README.so.md), [Português Brasil](README.pt_BR.md), [日本語](README.ja.md), [Română](README.ro.md), [Türkçe](README.tr.md).*
|
||||
|
||||
<b>DIGNIIN: MIDKAN, NOOCA APP-KA EE HADDA WALI TIJAABO AYUU KU JIRAA, SIDAA DARTEED CILLADO AYAAD LA KULMI KARTAA. HADAAD LA KULANTO, KA FUR ARIN SHARAXAYA QAYBTANADA ARRIMAHA EE GITHUB-KA.</b>
|
||||
|
||||
<b>NEWPIPE AMA KUWA KU SALAYSAN IN PLAYSTORE-KA LA GALIYO WAXAY KA HOR IMANAYSAA SHARCIGA IYO SHURUUDAHA AY LEEYIHIIN.</b>
|
||||
|
||||
## Sawir-shaashadeed
|
||||
|
||||
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_01.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_01.png)
|
||||
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_02.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_02.png)
|
||||
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_03.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_03.png)
|
||||
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_04.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_04.png)
|
||||
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_05.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_05.png)
|
||||
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_06.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_06.png)
|
||||
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_07.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_07.png)
|
||||
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_08.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_08.png)
|
||||
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_09.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_09.png)
|
||||
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_10.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_10.png)
|
||||
[<img src="fastlane/metadata/android/en-US/images/tenInchScreenshots/shot_11.png" width=405>](fastlane/metadata/android/en-US/images/tenInchScreenshots/shot_11.png)
|
||||
[<img src="fastlane/metadata/android/en-US/images/tenInchScreenshots/shot_12.png" width=405>](fastlane/metadata/android/en-US/images/tenInchScreenshots/shot_12.png)
|
||||
|
||||
## Faahfaahin
|
||||
|
||||
NewPipe ma isticmalo nidaamka wada shaqaynta Google, ama API-ga YouTube. Kaliya website-yada ayaa la furaa si xogta loo baahanyahay loogala soo dhex baxo, App-kan waxaa lagu isticmaali karaa aaladaha aysa ku jirin Adeegyada Google. Sidoo kale, uma baahnid akoon YouTube ah si aad u isticmaasho NewPipe, kaasoo ah barnaamij bilaash ah.
|
||||
|
||||
### Waxqabadka
|
||||
|
||||
* Raadi muuqaalo
|
||||
* Soo bandhiga faahfaahin guud oo muuqaalada ku saabsan
|
||||
* Ku daawo muuqaalada YouTube
|
||||
* Dhagayso muuqaalada YouTube
|
||||
* Qaab daaqad ah (muuqaal daare yar oo application-nada dul fuula)
|
||||
* Dooro muuqaal daareha aad rabto inaad wax ku daawato
|
||||
* Daji muuqaalada
|
||||
* Daji dhagaysiga kaliya (cod)
|
||||
* Ku fur muuqaal Kodi
|
||||
* Tus muuqaalada ka xiga/kuwa lamidka ah
|
||||
* Inaad luuqada aad rabto wax kaga dhex raadiso YouTube
|
||||
* Daawo/xanib muuqaalada da'da ku xidhan
|
||||
* Soo bandhig xog guud oo ku saabsan kanaalada
|
||||
* Raadi kanaalo
|
||||
* Daawo muuqaalada kanaal
|
||||
* Taageerida Orbot/Tor (wali toos ma aha)
|
||||
* Taageerida muuqaalada 1080p/2K/4K
|
||||
* Kaydka wixii hore [aad u daawatay]
|
||||
* Inaad rukumato kanaalada
|
||||
* Kaydinta waxaad raadisay
|
||||
* Raadi/daawo xulalka
|
||||
* U daawo sidii xulal la horay
|
||||
* Hormo gali muuqaalada
|
||||
* Xulal gudaha [aalada] ah
|
||||
* Qoraal-hooseed
|
||||
* Taageerida waxyaabaha tooska ah
|
||||
* Soo bandhiga faalooyinka
|
||||
|
||||
### Adeegyada la Taageero
|
||||
|
||||
NewPipe wuxuu taageeraa adeegyo badan. [warqadan](https://teamnewpipe.github.io/documentation/) ayaa si faahfaahsan u sharaxaysa sida adeeg cusub loogu soo dari lahaa iyo kala fur-furaha. Fadlan nala soo xidhiidh hadaad rabto inaad mid cusub kusoo darto. Adeegyada aan hadda taageero waxaa kamid ah:
|
||||
|
||||
* YouTube
|
||||
* SoundCloud \[tijaabo\]
|
||||
* media.ccc.de \[tijaabo\]
|
||||
* PeerTube instances \[tijaabo\]
|
||||
* Bandcamp \[tijaabo\]
|
||||
|
||||
## Kushubida iyo cusboonaysiinta
|
||||
Marka koodhka NewPipe isbadal ku dhaco (wax cusub oo lagusoo kordhiyay ama cilad bixin), ugu dambayn waxaa lasii daayaa mid cusub (Siidayn). Siidaynta qaabkeedu waa x.xx.x . Si aad midka cusub u hesho, waxaad samayn kartaa:
|
||||
1. Inaad mid cusub (APK) adigu dhisato. Tani waa mida ugu dagdag badan eed waxyaabaha cusub ku heli karto, laakiin way adagtahay, sidaa darteed waxaan soojeedinaynaa inaad isticmaasho qababka kale.
|
||||
2. Ku dar qayb gaar ah xaganaga F-Droid oo xagaas kaga shub isla markay siidayn soobaxdo. Hagitaanka xagan ka eeg: https://newpipe.net/FAQ/tutorials/install-add-fdroid-repo/
|
||||
3. Kasoo dajiso APK-ga xaga [Siidaynta Github](https://github.com/TeamNewPipe/NewPipe/releases) oo ku shubo isla markay siidayn soobaxdo.
|
||||
4. Ka cusboonaysii xaga F-Droid. Tani waa mida ugu daahitaanka badan, sababtoo ah F-Droid waxay fiirin isbadalka waxayna iyadu dhisi mid (app), sixiixi, kadibna ay cusboonaysiinta usiidayn isticmaalayaasha.
|
||||
|
||||
Waxaan usoojeedinaynaa isticmaaalka qaabka 2 dadka badankood. APK-yada loogu shubo qaabka 2 ama 3 way isqaadan karaan, laakiin isma qaadan karaan kuwa loogu shubay qaabka 4. Sababtuna waxaa weeye furaha sixiixa oo iskumid ah (kaanaga weeye) oo loo isticmaalay 2 iyo 3, laakiin furo sixiixeed ka duwan (midka F-Droid) oo loo isticmaalay 4. Dhisida APK ayadoo la isticmaalayo qaabka 1 waxay gabi ahaanba ka reebtaa wax fure ah. Furayaasha sixiixa waxay xaqiijiyaan in isticmaalaha aan lagu khaldin inuu ku shubto cusboonaysiin khalad ah (wax lasoo dhexraaciyay) app-ka.
|
||||
|
||||
Waxaa kale, hadaad rabto inaad tixraacayada kala badasho sabab jirta awgeed (tusaale shaqaynta aasaasiga ah ee NewPipe ayaa khalkhashay F-Droid-na wali cusboonysiin ma hayo), waxaan soojeedinaynaa isticmaalka qaabkan:
|
||||
1. Xogtaada koobi ka samee adoo raacaya Fadhiga > Luuqada & Fadhiga Kale > Gudbi Xog Diyaaran si aysa kaaga bixin kaydka wixii hore, rukunka, iyo xulalka
|
||||
2. Saar NewPipe
|
||||
3. Kasoo daji APK-ga tixraaca cusub oo ku shub
|
||||
4. Kasoo gali xogta talaabada 1 xaga Fadhiga > Luuqada & Fadhiga Kale > Soo Gali Xog Kaydsan
|
||||
|
||||
## Kusoo Kordhin
|
||||
Hadaad hayso fikrado; rogid, qaab badal, nadiifin koodh, ama koodhka ood si wayn wax oga badashaa—caawinta marwalba waa lasoo dhawaynayaa. Waxbadan hadii la qabto waxbadan ayaa fiicnaan!
|
||||
|
||||
Hadaad jeceshahay inaad qayb ka noqoto, fiiri [ogaysiisyada kusoo kordhinta](.github/CONTRIBUTING.md).
|
||||
|
||||
<a href="https://hosted.weblate.org/engage/newpipe/">
|
||||
<img src="https://hosted.weblate.org/widgets/newpipe/-/287x66-grey.png" alt="Heerka turjimaada" />
|
||||
</a>
|
||||
|
||||
## Ugu Deeq
|
||||
Hadaad jeceshahay NewPipe waan ku faraxsanaan lahayn deeq. Waxaad soo diri kartaa bitcoin ama sidoo kale waxaad deeqda kusoo diri kartaa xaga Bountysource ama Liberapay. Faahfaahin dheeraad ah oo kusaabsan ugu deeqida NewPipe, fadlan booqo [website-kanaga](https://newpipe.net/donate).
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<td><img src="https://bitcoin.org/img/icons/logotop.svg" alt="Bitcoin"></td>
|
||||
<td><img src="assets/bitcoin_qr_code.png" alt="Bitcoin QR code" width="100px"></td>
|
||||
<td><samp>16A9J59ahMRqkLSZjhYj33n9j3fMztFxnh</samp></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="https://liberapay.com/TeamNewPipe/"><img src="https://upload.wikimedia.org/wikipedia/commons/2/27/Liberapay_logo_v2_white-on-yellow.svg" alt="Liberapay" width="80px" ></a></td>
|
||||
<td><a href="https://liberapay.com/TeamNewPipe/"><img src="assets/liberapay_qr_code.png" alt="Visit NewPipe at liberapay.com" width="100px"></a></td>
|
||||
<td><a href="https://liberapay.com/TeamNewPipe/donate"><img src="assets/liberapay_donate_button.svg" alt="Donate via Liberapay" height="35px"></a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="https://www.bountysource.com/teams/newpipe"><img src="https://upload.wikimedia.org/wikipedia/commons/thumb/2/22/Bountysource.png/320px-Bountysource.png" alt="Bountysource" width="190px"></a></td>
|
||||
<td><a href="https://www.bountysource.com/teams/newpipe"><img src="assets/bountysource_qr_code.png" alt="Visit NewPipe at bountysource.com" width="100px"></a></td>
|
||||
<td><a href="https://www.bountysource.com/teams/newpipe/issues"><img src="https://img.shields.io/bountysource/team/newpipe/activity.svg?colorB=cd201f" height="30px" alt="Check out how many bounties you can earn."></a></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
## Siyaasada Sirdhawrka
|
||||
|
||||
Mashruuca NewPipe waxay ujeedadiisu tahay inuu bixiyo wax kuu gaar ah, oo adoon shaqsi ahaan laguu aqoonsan aad isticmaasho website-yada wax laga daawado/dhagaysto.
|
||||
Sidaa darteed, app-ku wax xog ah ma uruuriyo fasaxaaga la'aantii. Siyaasada Sirdhawrka NewPipe ayaa si faahfaahsan u sharaxda waxii xog ah ee la diro markaad cillad wariso, ama aad bogganaga faallo ka dhiibato. Warqada waxaad ka heli kartaa [halkan](https://newpipe.net/legal/privacy/).
|
||||
|
||||
## Laysinka
|
||||
[](https://www.gnu.org/licenses/gpl-3.0.en.html)
|
||||
|
||||
NewPipe waa barnaamij bilaash ah oon lahayn xuquuqda daabacaada: Waad isticmaali kartaa, waad wadaagi kartaa waadna hormarin kartaa hadaad rabto. Gaar ahaan waad sii daabici kartaa ama wax baad ka badali kartaa ayadoo la raacayo shuruudaha sharciga guud ee [GNU](https://www.gnu.org/licenses/gpl.html) sida ay soosaareen Ururka Barnaamijyada Bilaashka ah, soosaarista 3aad ee laysinka, ama (hadaad doonto) nooc walba oo kasii dambeeyay laysinkii 3aad.
|
||||
145
README.tr.md
Normal file
145
README.tr.md
Normal file
@@ -0,0 +1,145 @@
|
||||
<p align="center"><a href="https://newpipe.net"><img src="assets/new_pipe_icon_5.png" width="150"></a></p>
|
||||
<h2 align="center"><b>NewPipe</b></h2>
|
||||
<h4 align="center">Android için hafif ve özgür bir akış arayüzü.</h4>
|
||||
|
||||
<p align="center"><a href="https://f-droid.org/packages/org.schabi.newpipe/"><img src="https://fdroid.gitlab.io/artwork/badge/get-it-on-tr.svg"></a></p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/TeamNewPipe/NewPipe/releases" alt="GitHub sürümleri"><img src="https://img.shields.io/github/release/TeamNewPipe/NewPipe.svg" ></a>
|
||||
<a href="https://www.gnu.org/licenses/gpl-3.0" alt="Lisans: GPLv3"><img src="https://img.shields.io/badge/License-GPL%20v3-blue.svg"></a>
|
||||
<a href="https://github.com/TeamNewPipe/NewPipe/actions" alt="Derleme Durumu"><img src="https://github.com/TeamNewPipe/NewPipe/workflows/CI/badge.svg?branch=dev&event=push"></a>
|
||||
<a href="https://hosted.weblate.org/engage/newpipe/" alt="Çeviri Durumu"><img src="https://hosted.weblate.org/widgets/newpipe/-/svg-badge.svg"></a>
|
||||
<a href="https://webchat.freenode.net/#newpipe" alt="IRC kanalı: #newpipe"><img src="https://img.shields.io/badge/IRC%20chat-%23newpipe-brightgreen.svg"></a>
|
||||
<a href="https://www.bountysource.com/teams/newpipe" alt="Bountysource ödülleri"><img src="https://img.shields.io/bountysource/team/newpipe/activity.svg?colorB=cd201f"></a>
|
||||
</p>
|
||||
<hr>
|
||||
<p align="center"><a href="#ekran-fotoğrafları">Ekran fotoğrafları</a> • <a href="#açıklama">Açıklama</a> • <a href="#özellikler">Özellikler</a> • <a href="#kurulum-ve-güncellemeler">Kurulum ve güncellemeler</a> • <a href="#katkıda-bulunma">Katkıda bulunma</a> • <a href="#bağış">Bağış</a> • <a href="#lisans">Lisans</a></p>
|
||||
<p align="center"><a href="https://newpipe.net">Web sitesi</a> • <a href="https://newpipe.net/blog/">Blog</a> • <a href="https://newpipe.net/FAQ/">SSS</a> • <a href="https://newpipe.net/press/">Basın</a></p>
|
||||
<hr>
|
||||
|
||||
*Bu sayfayı diğer dillerde okuyun: [English](README.md), [Español](README.es.md), [한국어](README.ko.md), [Soomaali](README.so.md), [Português Brasil](README.pt_BR.md), [日本語](README.ja.md), [Română](README.ro.md), [Türkçe](README.tr.md).*
|
||||
|
||||
<b>UYARI: BU SÜRÜM BETA SÜRÜMÜDÜR, BU NEDENLE HATALARLA KARŞILAŞABİLİRSİNİZ. HATA BULURSANIZ BU GITHUB DEPOSUNDA BUNU BİLDİRİN.</b>
|
||||
|
||||
<b>GOOGLE PLAY STORE'A NEWPIPE VEYA BAŞKA BİR KOPYASINI KOYMAK, PLAY STORE ŞARTLARINI VE KOŞULLARINI İHLAL EDER.</b>
|
||||
|
||||
## Ekran fotoğrafları
|
||||
|
||||
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_01.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_01.png)
|
||||
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_02.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_02.png)
|
||||
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_03.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_03.png)
|
||||
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_04.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_04.png)
|
||||
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_05.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_05.png)
|
||||
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_06.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_06.png)
|
||||
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_07.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_07.png)
|
||||
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_08.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_08.png)
|
||||
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_09.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_09.png)
|
||||
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_10.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_10.png)
|
||||
[<img src="fastlane/metadata/android/en-US/images/tenInchScreenshots/shot_11.png" width=405>](fastlane/metadata/android/en-US/images/tenInchScreenshots/shot_11.png)
|
||||
[<img src="fastlane/metadata/android/en-US/images/tenInchScreenshots/shot_12.png" width=405>](fastlane/metadata/android/en-US/images/tenInchScreenshots/shot_12.png)
|
||||
|
||||
## Açıklama
|
||||
|
||||
NewPipe herhangi bir Google çerçeve kütüphanesini, ya da YouTube API hizmetlerini kullanmaz. Gerekli web hizmetleri yalnızca gerekli bilgileri almak için kaynak olarak kullanılır, bu nedenle bu uygulama Google hizmetleri yüklü olmayan cihazlarda da kullanılabilir. Ayrıca, copyleft özgür yazılımı olan NewPipe'ı kullanmak için bir YouTube hesabına ihtiyacınız yoktur.
|
||||
|
||||
### Özellikler
|
||||
|
||||
* Video arama
|
||||
* Videolar hakkında genel bilgileri görüntüleme
|
||||
* YouTube videoları izleme
|
||||
* YouTube videolarını dinleme
|
||||
* Pop-up modu (hareketli oynatıcı)
|
||||
* Video izlemek için akış oynatıcısını seçme
|
||||
* Video indirme
|
||||
* Sadece ses indirme
|
||||
* Videoyu Kodi'de açma
|
||||
* Sonraki video/ilgili videolar
|
||||
* YouTube'u belirli bir dilde arayın
|
||||
* Yaş sınırlı içeriği izleme/engelleme
|
||||
* Kanallar hakkındaki genel bilgileri görüntüleme
|
||||
* Kanal arama
|
||||
* Bir kanaldaki videoları izleme
|
||||
* Orbot/Tor desteği (henüz direkt olarak değil)
|
||||
* 1080p/2K/4K desteği
|
||||
* Geçmişi görme
|
||||
* Kanallara abone olma
|
||||
* Geçmişte arama
|
||||
* Oynatma listesi arama/oynatma
|
||||
* Çalma listelerini sıralayıp oynatın
|
||||
* Videoları sırayla oynatın
|
||||
* Yerel oynatma listeleri
|
||||
* Altyazılar
|
||||
* Canlı yayın desteği
|
||||
* Yorumları görme
|
||||
|
||||
### Desteklenen servisler
|
||||
|
||||
NewPipe birden fazla hizmeti destekler. Uygulamaya ve ayıklayıcıya yeni bir hizmet ekleme konusunda daha fazla bilgiye [kılavuzlarımızdan](https://teamnewpipe.github.io/documentation/) ulaşabilirsiniz. Yeni bir hizmet eklemek istiyorsanız lütfen bizimle iletişime geçin. Şu anda desteklenen hizmetler şunlardır:
|
||||
|
||||
* YouTube
|
||||
* SoundCloud \[beta\]
|
||||
* media.ccc.de \[beta\]
|
||||
* PeerTube \[beta\]
|
||||
* Bandcamp \[beta\]
|
||||
|
||||
<!-- Eski bağlantıları uyumlu tutmak için gizli span. -->
|
||||
<span id="updates"></span>
|
||||
|
||||
## Kurulum ve güncellemeler
|
||||
Aşağıdaki yöntemlerden birini kullanarak NewPipe'ı kurabilirsiniz:
|
||||
1. Özel depomuzu F-Droid'e ekleyin ve oradan yükleyin. Kılavuzu şurada bulabilirsiniz: https://newpipe.net/FAQ/tutorials/install-add-fdroid-repo/
|
||||
2. APK'yı [Github sürümlerinden](https://github.com/TeamNewPipe/NewPipe/releases) indirin ve kurun.
|
||||
3. F-Droid ile güncelleyin. Bu, güncellemeleri almanın en yavaş yöntemidir, çünkü F-Droid değişiklikleri tanımalı, APK'yı kendisi oluşturmalı, imzalamalı ve ardından güncellemeyi kullanıcılara dağıtmalıdır.
|
||||
4. Kendiniz bir APK derleyin. Bu yöntem, cihazınızda yeni özellikler edinmenin en hızlı yoludur, ancak çok daha karmaşıktır, bu nedenle diğer yöntemlerden birini kullanmanızı öneririz.
|
||||
|
||||
Çoğu kullanıcı için yöntem 1'i öneririz. Yöntem 1 veya 2 kullanılarak yüklenen APK'lar birbiriyle uyumludur, ancak yöntem 3 kullanılarak yüklenenlerle uyumlu değildir. Bu durum, 1 ve 2 için kullanılan aynı imzalama anahtarıın (bizim anahtarımız) 3 için kullanılan imzalama anahtarından (F-Droid'in anahtarı) farklı olmasından kaynaklanmaktadır. Yöntem 4 kullanılarak oluşturulan deneysel APK'larda anahtar yoktur. İmzalama anahtarları, bir kullanıcının bir uygulamaya kötü amaçlı bir güncelleme yüklemek için kandırılmadığından emin olmanıza yardımcı olur.
|
||||
|
||||
Bu arada, herhangi bir nedenle kaynakları değiştirmek istiyorsanız (örneğin, NewPipe'ın temel bir işlevi bozuldu ve F-Droid tarafında henüz bir güncelleme yayınlanmadı), bu prosedürü izlemenizi öneririz:
|
||||
1. Verilerinizi yedekleyin. `NewPipe Ayarları > İçerik > Veritabanını dışa aktar` seçeneklerini izleyerek aboneliklerinizi, oynatma listelerinizi ve geçmişinizi yedekleyin.
|
||||
2. NewPipe'ı kaldırın
|
||||
3. APK dosyasını yeni bir kaynaktan indirin ve yükleyin
|
||||
4. `Ayarlar > İçerik > Veritabanını içe aktar` seçeneklerini izleyerek 1. adımdaki verileri içe aktarın
|
||||
|
||||
## Katkıda bulunma
|
||||
Fikirleriniz, çevirileriniz, tasarım değişiklikleriniz, kod temizlemeniz veya ağır kod değişiklikleriniz olsun, yardımınıza her zaman açığız.
|
||||
Yapılan her değişiklikle NewPipe daha da iyi bir konuma geliyor!
|
||||
|
||||
Eğer yer almak istiyorsanız, [katkı sağlayanlar için hazırladığımız notları](.github/CONTRIBUTING.md) kontrol edin.
|
||||
|
||||
<a href="https://hosted.weblate.org/engage/newpipe/">
|
||||
<img src="https://hosted.weblate.org/widgets/newpipe/-/287x66-grey.png" alt="Çeviri istatistikleri" />
|
||||
</a>
|
||||
|
||||
## Bağış
|
||||
NewPipe'ı beğendiyseniz, yapacağınız bağışlar bizi motive eder. Bitcoin gönderebilir veya Bountysource veya Liberapay aracılığıyla bağış yapabilirsiniz. NewPipe'a bağış yapma hakkında daha fazla bilgi için lütfen [web sitemizi](https://newpipe.net/donate) ziyaret edin.
|
||||
|
||||
<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 kodu" width="100px"></td>
|
||||
<td><samp>16A9J59ahMRqkLSZjhYj33n9j3fMztFxnh</samp></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="https://liberapay.com/TeamNewPipe/"><img src="https://upload.wikimedia.org/wikipedia/commons/2/27/Liberapay_logo_v2_white-on-yellow.svg" alt="Liberapay" width="80px" ></a></td>
|
||||
<td><a href="https://liberapay.com/TeamNewPipe/"><img src="assets/liberapay_qr_code.png" alt="liberapay.com üzerinde NewPipe'ı ziyaret edin" width="100px"></a></td>
|
||||
<td><a href="https://liberapay.com/TeamNewPipe/donate"><img src="assets/liberapay_donate_button.svg" alt="Liberapay aracılığıyla bağış yapın" height="35px"></a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="https://www.bountysource.com/teams/newpipe"><img src="https://upload.wikimedia.org/wikipedia/commons/thumb/2/22/Bountysource.png/320px-Bountysource.png" alt="Bountysource" width="190px"></a></td>
|
||||
<td><a href="https://www.bountysource.com/teams/newpipe"><img src="assets/bountysource_qr_code.png" alt="bountysource.com üzerinde NewPipe'ı ziyaret edin" 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="Ne kadar ödül kazanabileceğinizi kontrol edin."></a></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
## Gizlilik politikası
|
||||
|
||||
NewPipe projesi, çevrimiçi akış hizmetlerini kullanmak için özel, özgür ve anonim bir deneyim sunmayı amaçlamaktadır.
|
||||
Bu doğrultuda, uygulama sizin izniniz olmadan herhangi bir veri toplamaz. NewPipe'ın Gizlilik Politikası, bir çökme raporu gönderdiğinizde veya blogumuzda yorum yaptığınızda hangi verilerin gönderildiğini ve saklandığını ayrıntılı olarak açıklar. İlgili belgeyi [burada](https://newpipe.net/legal/privacy/) bulabilirsiniz.
|
||||
|
||||
## Lisans
|
||||
[](https://www.gnu.org/licenses/gpl-3.0.en.html)
|
||||
|
||||
NewPipe özgür bir yazılımdır. Kendi başınıza kullanabilir, öğrenebilir, paylaşabilir
|
||||
ve geliştirebilirsiniz. Free Software Foundation tarafından yayınlanan GNU Genel Kamu Lisansı,
|
||||
Lisansın 3. sürümü veya (isteğe bağlı olarak) daha sonraki bir sürümü şartları ve
|
||||
koşulları altında yeniden dağıtabilir ve/veya değiştirebilirsiniz.
|
||||
192
app/build.gradle
192
app/build.gradle
@@ -1,3 +1,7 @@
|
||||
plugins {
|
||||
id "org.sonarqube" version "3.1.1"
|
||||
}
|
||||
|
||||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'kotlin-android'
|
||||
apply plugin: 'kotlin-android-extensions'
|
||||
@@ -13,8 +17,10 @@ android {
|
||||
resValue "string", "app_name", "NewPipe"
|
||||
minSdkVersion 19
|
||||
targetSdkVersion 29
|
||||
versionCode 951
|
||||
versionName "0.19.6"
|
||||
versionCode 969
|
||||
versionName "0.21.3"
|
||||
|
||||
multiDexEnabled true
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
vectorDrawables.useSupportLibrary = true
|
||||
@@ -28,12 +34,11 @@ android {
|
||||
|
||||
buildTypes {
|
||||
debug {
|
||||
multiDexEnabled true
|
||||
debuggable true
|
||||
|
||||
// suffix the app id and the app name with git branch name
|
||||
def workingBranch = getGitWorkingBranch()
|
||||
def normalizedWorkingBranch = workingBranch.replaceAll("[^A-Za-z]+", "").toLowerCase()
|
||||
def normalizedWorkingBranch = workingBranch.replaceFirst("^[^A-Za-z]+", "").replaceAll("[^0-9A-Za-z]+", "")
|
||||
if (normalizedWorkingBranch.isEmpty() || workingBranch == "master" || workingBranch == "dev") {
|
||||
// default values when branch name could not be determined or is master or dev
|
||||
applicationIdSuffix ".debug"
|
||||
@@ -61,14 +66,24 @@ android {
|
||||
// Or, if you prefer, you can continue to check for errors in release builds,
|
||||
// but continue the build even when errors are found:
|
||||
abortOnError false
|
||||
// suppress false warning ("Resource IDs will be non-final in Android Gradle Plugin version
|
||||
// 5.0, avoid using them in switch case statements"), which affects only library projects
|
||||
disable 'NonConstantResourceId'
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
// Flag to enable support for the new language APIs
|
||||
coreLibraryDesugaringEnabled true
|
||||
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
encoding 'utf-8'
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
// Required and used only by groupie
|
||||
androidExtensions {
|
||||
experimental = true
|
||||
@@ -77,18 +92,27 @@ android {
|
||||
sourceSets {
|
||||
androidTest.assets.srcDirs += files("$projectDir/schemas".toString())
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
viewBinding true
|
||||
}
|
||||
}
|
||||
|
||||
ext {
|
||||
icepickVersion = '3.2.0'
|
||||
checkstyleVersion = '8.32'
|
||||
stethoVersion = '1.5.1'
|
||||
leakCanaryVersion = '2.2'
|
||||
exoPlayerVersion = '2.11.6'
|
||||
checkstyleVersion = '8.38'
|
||||
|
||||
androidxLifecycleVersion = '2.2.0'
|
||||
androidxRoomVersion = '2.2.5'
|
||||
groupieVersion = '2.8.0'
|
||||
markwonVersion = '4.3.1'
|
||||
androidxRoomVersion = '2.3.0-alpha03'
|
||||
|
||||
icepickVersion = '3.2.0'
|
||||
exoPlayerVersion = '2.12.3'
|
||||
googleAutoServiceVersion = '1.0-rc7'
|
||||
groupieVersion = '2.8.1'
|
||||
markwonVersion = '4.6.0'
|
||||
|
||||
leakCanaryVersion = '2.5'
|
||||
stethoVersion = '1.5.1'
|
||||
mockitoVersion = '3.6.0'
|
||||
}
|
||||
|
||||
configurations {
|
||||
@@ -97,7 +121,7 @@ configurations {
|
||||
}
|
||||
|
||||
checkstyle {
|
||||
configFile rootProject.file('checkstyle.xml')
|
||||
configDir rootProject.file(".")
|
||||
ignoreFailures false
|
||||
showViolations true
|
||||
toolVersion = checkstyleVersion
|
||||
@@ -121,92 +145,140 @@ task runCheckstyle(type: Checkstyle) {
|
||||
}
|
||||
}
|
||||
|
||||
def outputDir = "${project.buildDir}/reports/ktlint/"
|
||||
def inputFiles = project.fileTree(dir: "src", include: "**/*.kt")
|
||||
|
||||
task runKtlint(type: JavaExec) {
|
||||
inputs.files(inputFiles)
|
||||
outputs.dir(outputDir)
|
||||
main = "com.pinterest.ktlint.Main"
|
||||
classpath = configurations.ktlint
|
||||
args "src/**/*.kt"
|
||||
}
|
||||
|
||||
task formatKtlint(type: JavaExec) {
|
||||
inputs.files(inputFiles)
|
||||
outputs.dir(outputDir)
|
||||
main = "com.pinterest.ktlint.Main"
|
||||
classpath = configurations.ktlint
|
||||
args "-F", "src/**/*.kt"
|
||||
}
|
||||
|
||||
afterEvaluate {
|
||||
preDebugBuild.dependsOn runCheckstyle, runKtlint
|
||||
preDebugBuild.dependsOn formatKtlint, runCheckstyle, runKtlint
|
||||
}
|
||||
|
||||
sonarqube {
|
||||
properties {
|
||||
property "sonar.projectKey", "TeamNewPipe_NewPipe"
|
||||
property "sonar.organization", "teamnewpipe"
|
||||
property "sonar.host.url", "https://sonarcloud.io"
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
||||
/** Desugaring **/
|
||||
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.1'
|
||||
|
||||
/** NewPipe libraries **/
|
||||
// You can use a local version by uncommenting a few lines in settings.gradle
|
||||
implementation 'com.github.TeamNewPipe:nanojson:1d9e1aea9049fc9f85e68b43ba39fe7be1c1f751'
|
||||
implementation 'com.github.TeamNewPipe:NewPipeExtractor:v0.21.3'
|
||||
|
||||
/** Checkstyle **/
|
||||
checkstyle "com.puppycrawl.tools:checkstyle:${checkstyleVersion}"
|
||||
ktlint 'com.pinterest:ktlint:0.40.0'
|
||||
|
||||
/** Kotlin **/
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:${kotlin_version}"
|
||||
|
||||
/** AndroidX **/
|
||||
implementation 'androidx.appcompat:appcompat:1.2.0'
|
||||
implementation 'androidx.cardview:cardview:1.0.0'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
|
||||
implementation 'androidx.core:core-ktx:1.3.2'
|
||||
implementation 'androidx.documentfile:documentfile:1.0.1'
|
||||
implementation "androidx.lifecycle:lifecycle-livedata:${androidxLifecycleVersion}"
|
||||
implementation "androidx.lifecycle:lifecycle-viewmodel:${androidxLifecycleVersion}"
|
||||
implementation 'androidx.localbroadcastmanager:localbroadcastmanager:1.0.0'
|
||||
implementation 'androidx.media:media:1.2.1'
|
||||
implementation 'androidx.multidex:multidex:2.0.1'
|
||||
implementation 'androidx.preference:preference:1.1.1'
|
||||
implementation 'androidx.recyclerview:recyclerview:1.1.0'
|
||||
implementation "androidx.room:room-runtime:${androidxRoomVersion}"
|
||||
implementation "androidx.room:room-rxjava3:${androidxRoomVersion}"
|
||||
kapt "androidx.room:room-compiler:${androidxRoomVersion}"
|
||||
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
|
||||
implementation 'androidx.webkit:webkit:1.4.0'
|
||||
implementation 'com.google.android.material:material:1.2.1'
|
||||
|
||||
/** Third-party libraries **/
|
||||
// Instance state boilerplate elimination
|
||||
implementation "frankiesardo:icepick:${icepickVersion}"
|
||||
kapt "frankiesardo:icepick-processor:${icepickVersion}"
|
||||
|
||||
checkstyle "com.puppycrawl.tools:checkstyle:${checkstyleVersion}"
|
||||
ktlint "com.pinterest:ktlint:0.35.0"
|
||||
|
||||
debugImplementation "com.facebook.stetho:stetho:${stethoVersion}"
|
||||
debugImplementation "com.facebook.stetho:stetho-okhttp3:${stethoVersion}"
|
||||
|
||||
debugImplementation "com.squareup.leakcanary:leakcanary-android:${leakCanaryVersion}"
|
||||
implementation "com.squareup.leakcanary:leakcanary-object-watcher-android:${leakCanaryVersion}"
|
||||
|
||||
debugImplementation "androidx.multidex:multidex:2.0.1"
|
||||
|
||||
testImplementation 'junit:junit:4.13'
|
||||
testImplementation 'org.mockito:mockito-core:3.3.3'
|
||||
|
||||
androidTestImplementation "androidx.test.ext:junit:1.1.1"
|
||||
androidTestImplementation "androidx.room:room-testing:${androidxRoomVersion}"
|
||||
androidTestImplementation "androidx.test.espresso:espresso-core:3.2.0", {
|
||||
exclude module: 'support-annotations'
|
||||
}
|
||||
|
||||
implementation 'com.github.TeamNewPipe:NewPipeExtractor:a70cb0283ffc3bba2709815673a5a7940aab0a3a'
|
||||
|
||||
implementation "com.github.TeamNewPipe:nanojson:1d9e1aea9049fc9f85e68b43ba39fe7be1c1f751"
|
||||
// HTML parser
|
||||
implementation "org.jsoup:jsoup:1.13.1"
|
||||
|
||||
implementation "com.squareup.okhttp3:okhttp:3.12.11"
|
||||
// HTTP client
|
||||
//noinspection GradleDependency --> do not update okhttp to keep supporting Android 4.4 users
|
||||
implementation "com.squareup.okhttp3:okhttp:3.12.13"
|
||||
|
||||
// Media player
|
||||
implementation "com.google.android.exoplayer:exoplayer:${exoPlayerVersion}"
|
||||
implementation "com.google.android.exoplayer:extension-mediasession:${exoPlayerVersion}"
|
||||
|
||||
implementation "com.google.android.material:material:1.1.0"
|
||||
|
||||
implementation "androidx.appcompat:appcompat:1.1.0"
|
||||
implementation "androidx.preference:preference:1.1.1"
|
||||
implementation "androidx.recyclerview:recyclerview:1.1.0"
|
||||
implementation "androidx.cardview:cardview:1.0.0"
|
||||
implementation "androidx.constraintlayout:constraintlayout:1.1.3"
|
||||
|
||||
implementation "androidx.lifecycle:lifecycle-livedata:${androidxLifecycleVersion}"
|
||||
implementation "androidx.lifecycle:lifecycle-viewmodel:${androidxLifecycleVersion}"
|
||||
implementation "androidx.lifecycle:lifecycle-extensions:${androidxLifecycleVersion}"
|
||||
|
||||
implementation "androidx.room:room-runtime:${androidxRoomVersion}"
|
||||
implementation "androidx.room:room-rxjava2:${androidxRoomVersion}"
|
||||
kapt "androidx.room:room-compiler:${androidxRoomVersion}"
|
||||
// Metadata generator for service descriptors
|
||||
compileOnly "com.google.auto.service:auto-service-annotations:${googleAutoServiceVersion}"
|
||||
kapt "com.google.auto.service:auto-service:${googleAutoServiceVersion}"
|
||||
|
||||
// Manager for complex RecyclerView layouts
|
||||
implementation "com.xwray:groupie:${groupieVersion}"
|
||||
implementation "com.xwray:groupie-kotlin-android-extensions:${groupieVersion}"
|
||||
implementation "com.xwray:groupie-viewbinding:${groupieVersion}"
|
||||
|
||||
// Circular ImageView
|
||||
implementation "de.hdodenhof:circleimageview:3.1.0"
|
||||
// Image loading
|
||||
implementation "com.nostra13.universalimageloader:universal-image-loader:1.9.5"
|
||||
|
||||
// Markdown library for Android
|
||||
implementation "io.noties.markwon:core:${markwonVersion}"
|
||||
implementation "io.noties.markwon:linkify:${markwonVersion}"
|
||||
|
||||
// File picker
|
||||
implementation "com.nononsenseapps:filepicker:4.2.1"
|
||||
|
||||
implementation "ch.acra:acra-core:5.5.0"
|
||||
// Crash reporting
|
||||
implementation "ch.acra:acra-core:5.7.0"
|
||||
|
||||
implementation "io.reactivex.rxjava2:rxjava:2.2.19"
|
||||
implementation "io.reactivex.rxjava2:rxandroid:2.1.1"
|
||||
implementation "com.jakewharton.rxbinding2:rxbinding:2.2.0"
|
||||
// Reactive extensions for Java VM
|
||||
implementation "io.reactivex.rxjava3:rxjava:3.0.7"
|
||||
implementation "io.reactivex.rxjava3:rxandroid:3.0.0"
|
||||
// RxJava binding APIs for Android UI widgets
|
||||
implementation "com.jakewharton.rxbinding4:rxbinding:4.0.0"
|
||||
|
||||
implementation "org.ocpsoft.prettytime:prettytime:4.0.5.Final"
|
||||
// Date and time formatting
|
||||
implementation "org.ocpsoft.prettytime:prettytime:5.0.0.Final"
|
||||
|
||||
/** Debugging **/
|
||||
// Memory leak detection
|
||||
implementation "com.squareup.leakcanary:leakcanary-object-watcher-android:${leakCanaryVersion}"
|
||||
implementation "com.squareup.leakcanary:plumber-android:${leakCanaryVersion}"
|
||||
debugImplementation "com.squareup.leakcanary:leakcanary-android:${leakCanaryVersion}"
|
||||
// Debug bridge for Android
|
||||
debugImplementation "com.facebook.stetho:stetho:${stethoVersion}"
|
||||
debugImplementation "com.facebook.stetho:stetho-okhttp3:${stethoVersion}"
|
||||
|
||||
/** Testing **/
|
||||
testImplementation 'junit:junit:4.13.1'
|
||||
testImplementation "org.mockito:mockito-core:${mockitoVersion}"
|
||||
testImplementation "org.mockito:mockito-inline:${mockitoVersion}"
|
||||
|
||||
androidTestImplementation "androidx.test.ext:junit:1.1.2"
|
||||
androidTestImplementation "androidx.room:room-testing:${androidxRoomVersion}"
|
||||
androidTestImplementation "androidx.test.espresso:espresso-core:3.3.0", {
|
||||
exclude module: 'support-annotations'
|
||||
}
|
||||
}
|
||||
|
||||
static String getGitWorkingBranch() {
|
||||
|
||||
@@ -31,49 +31,62 @@ class AppDatabaseTest {
|
||||
}
|
||||
|
||||
@get:Rule
|
||||
val testHelper = MigrationTestHelper(InstrumentationRegistry.getInstrumentation(),
|
||||
AppDatabase::class.java.canonicalName, FrameworkSQLiteOpenHelperFactory())
|
||||
val testHelper = MigrationTestHelper(
|
||||
InstrumentationRegistry.getInstrumentation(),
|
||||
AppDatabase::class.java.canonicalName, FrameworkSQLiteOpenHelperFactory()
|
||||
)
|
||||
|
||||
@Test
|
||||
fun migrateDatabaseFrom2to3() {
|
||||
val databaseInV2 = testHelper.createDatabase(AppDatabase.DATABASE_NAME, Migrations.DB_VER_2)
|
||||
|
||||
databaseInV2.run {
|
||||
insert("streams", SQLiteDatabase.CONFLICT_FAIL, ContentValues().apply {
|
||||
// put("uid", null)
|
||||
put("service_id", DEFAULT_SERVICE_ID)
|
||||
put("url", DEFAULT_URL)
|
||||
put("title", DEFAULT_TITLE)
|
||||
put("stream_type", DEFAULT_TYPE.name)
|
||||
put("duration", DEFAULT_DURATION)
|
||||
put("uploader", DEFAULT_UPLOADER_NAME)
|
||||
put("thumbnail_url", DEFAULT_THUMBNAIL)
|
||||
})
|
||||
insert("streams", SQLiteDatabase.CONFLICT_FAIL, ContentValues().apply {
|
||||
// put("uid", null)
|
||||
put("service_id", DEFAULT_SECOND_SERVICE_ID)
|
||||
put("url", DEFAULT_SECOND_URL)
|
||||
// put("title", null)
|
||||
// put("stream_type", null)
|
||||
// put("duration", null)
|
||||
// put("uploader", null)
|
||||
// put("thumbnail_url", null)
|
||||
})
|
||||
insert("streams", SQLiteDatabase.CONFLICT_FAIL, ContentValues().apply {
|
||||
// put("uid", null)
|
||||
put("service_id", DEFAULT_SERVICE_ID)
|
||||
// put("url", null)
|
||||
// put("title", null)
|
||||
// put("stream_type", null)
|
||||
// put("duration", null)
|
||||
// put("uploader", null)
|
||||
// put("thumbnail_url", null)
|
||||
})
|
||||
insert(
|
||||
"streams", SQLiteDatabase.CONFLICT_FAIL,
|
||||
ContentValues().apply {
|
||||
// put("uid", null)
|
||||
put("service_id", DEFAULT_SERVICE_ID)
|
||||
put("url", DEFAULT_URL)
|
||||
put("title", DEFAULT_TITLE)
|
||||
put("stream_type", DEFAULT_TYPE.name)
|
||||
put("duration", DEFAULT_DURATION)
|
||||
put("uploader", DEFAULT_UPLOADER_NAME)
|
||||
put("thumbnail_url", DEFAULT_THUMBNAIL)
|
||||
}
|
||||
)
|
||||
insert(
|
||||
"streams", SQLiteDatabase.CONFLICT_FAIL,
|
||||
ContentValues().apply {
|
||||
// put("uid", null)
|
||||
put("service_id", DEFAULT_SECOND_SERVICE_ID)
|
||||
put("url", DEFAULT_SECOND_URL)
|
||||
// put("title", null)
|
||||
// put("stream_type", null)
|
||||
// put("duration", null)
|
||||
// put("uploader", null)
|
||||
// put("thumbnail_url", null)
|
||||
}
|
||||
)
|
||||
insert(
|
||||
"streams", SQLiteDatabase.CONFLICT_FAIL,
|
||||
ContentValues().apply {
|
||||
// put("uid", null)
|
||||
put("service_id", DEFAULT_SERVICE_ID)
|
||||
// put("url", null)
|
||||
// put("title", null)
|
||||
// put("stream_type", null)
|
||||
// put("duration", null)
|
||||
// put("uploader", null)
|
||||
// put("thumbnail_url", null)
|
||||
}
|
||||
)
|
||||
close()
|
||||
}
|
||||
|
||||
testHelper.runMigrationsAndValidate(AppDatabase.DATABASE_NAME, Migrations.DB_VER_3,
|
||||
true, Migrations.MIGRATION_2_3)
|
||||
testHelper.runMigrationsAndValidate(
|
||||
AppDatabase.DATABASE_NAME, Migrations.DB_VER_3,
|
||||
true, Migrations.MIGRATION_2_3
|
||||
)
|
||||
|
||||
val migratedDatabaseV3 = getMigratedDatabase()
|
||||
val listFromDB = migratedDatabaseV3.streamDAO().all.blockingFirst()
|
||||
@@ -110,9 +123,11 @@ class AppDatabaseTest {
|
||||
}
|
||||
|
||||
private fun getMigratedDatabase(): AppDatabase {
|
||||
val database: AppDatabase = Room.databaseBuilder(ApplicationProvider.getApplicationContext(),
|
||||
AppDatabase::class.java, AppDatabase.DATABASE_NAME)
|
||||
.build()
|
||||
val database: AppDatabase = Room.databaseBuilder(
|
||||
ApplicationProvider.getApplicationContext(),
|
||||
AppDatabase::class.java, AppDatabase.DATABASE_NAME
|
||||
)
|
||||
.build()
|
||||
testHelper.closeWhenFinished(database)
|
||||
return database
|
||||
}
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
package org.schabi.newpipe.error;
|
||||
|
||||
import android.os.Parcel;
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import androidx.test.filters.LargeTest;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.extractor.ServiceList;
|
||||
import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
/**
|
||||
* Instrumented tests for {@link ErrorInfo}.
|
||||
*/
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
@LargeTest
|
||||
public class ErrorInfoTest {
|
||||
|
||||
@Test
|
||||
public void errorInfoTestParcelable() {
|
||||
final ErrorInfo info = new ErrorInfo(new ParsingException("Hello"),
|
||||
UserAction.USER_REPORT, "request", ServiceList.YouTube.getServiceId());
|
||||
// Obtain a Parcel object and write the parcelable object to it:
|
||||
final Parcel parcel = Parcel.obtain();
|
||||
info.writeToParcel(parcel, 0);
|
||||
parcel.setDataPosition(0);
|
||||
final ErrorInfo infoFromParcel = (ErrorInfo) ErrorInfo.CREATOR.createFromParcel(parcel);
|
||||
|
||||
assertTrue(Arrays.toString(infoFromParcel.getStackTraces())
|
||||
.contains(ErrorInfoTest.class.getSimpleName()));
|
||||
assertEquals(UserAction.USER_REPORT, infoFromParcel.getUserAction());
|
||||
assertEquals(ServiceList.YouTube.getServiceInfo().getName(),
|
||||
infoFromParcel.getServiceName());
|
||||
assertEquals("request", infoFromParcel.getRequest());
|
||||
assertEquals(R.string.parsing_error, infoFromParcel.getMessageStringId());
|
||||
|
||||
parcel.recycle();
|
||||
}
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
package org.schabi.newpipe.report;
|
||||
|
||||
import android.os.Parcel;
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import androidx.test.filters.LargeTest;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.report.ErrorActivity.ErrorInfo;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
/**
|
||||
* Instrumented tests for {@link ErrorInfo}.
|
||||
*/
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
@LargeTest
|
||||
public class ErrorInfoTest {
|
||||
|
||||
@Test
|
||||
public void errorInfoTestParcelable() {
|
||||
ErrorInfo info = ErrorInfo.make(UserAction.USER_REPORT, "youtube", "request",
|
||||
R.string.general_error);
|
||||
// Obtain a Parcel object and write the parcelable object to it:
|
||||
Parcel parcel = Parcel.obtain();
|
||||
info.writeToParcel(parcel, 0);
|
||||
parcel.setDataPosition(0);
|
||||
ErrorInfo infoFromParcel = ErrorInfo.CREATOR.createFromParcel(parcel);
|
||||
|
||||
assertEquals(UserAction.USER_REPORT, infoFromParcel.userAction);
|
||||
assertEquals("youtube", infoFromParcel.serviceName);
|
||||
assertEquals("request", infoFromParcel.request);
|
||||
assertEquals(R.string.general_error, infoFromParcel.message);
|
||||
|
||||
parcel.recycle();
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,5 @@
|
||||
package org.schabi.newpipe
|
||||
|
||||
import android.content.Context
|
||||
import androidx.multidex.MultiDex
|
||||
import androidx.preference.PreferenceManager
|
||||
import com.facebook.stetho.Stetho
|
||||
import com.facebook.stetho.okhttp3.StethoInterceptor
|
||||
@@ -11,25 +9,28 @@ import okhttp3.OkHttpClient
|
||||
import org.schabi.newpipe.extractor.downloader.Downloader
|
||||
|
||||
class DebugApp : App() {
|
||||
override fun attachBaseContext(base: Context) {
|
||||
super.attachBaseContext(base)
|
||||
MultiDex.install(this)
|
||||
}
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
initStetho()
|
||||
|
||||
// Give each object 10 seconds to be GC'ed, before LeakCanary gets nosy on it
|
||||
AppWatcher.config = AppWatcher.config.copy(watchDurationMillis = 10000)
|
||||
LeakCanary.config = LeakCanary.config.copy(dumpHeap = PreferenceManager
|
||||
.getDefaultSharedPreferences(this).getBoolean(getString(
|
||||
R.string.allow_heap_dumping_key), false))
|
||||
LeakCanary.config = LeakCanary.config.copy(
|
||||
dumpHeap = PreferenceManager
|
||||
.getDefaultSharedPreferences(this).getBoolean(
|
||||
getString(
|
||||
R.string.allow_heap_dumping_key
|
||||
),
|
||||
false
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
override fun getDownloader(): Downloader {
|
||||
val downloader = DownloaderImpl.init(OkHttpClient.Builder()
|
||||
.addNetworkInterceptor(StethoInterceptor()))
|
||||
val downloader = DownloaderImpl.init(
|
||||
OkHttpClient.Builder()
|
||||
.addNetworkInterceptor(StethoInterceptor())
|
||||
)
|
||||
setCookiesToDownloader(downloader)
|
||||
return downloader
|
||||
}
|
||||
@@ -43,7 +44,8 @@ class DebugApp : App() {
|
||||
|
||||
// Enable command line interface
|
||||
initializerBuilder.enableDumpapp(
|
||||
Stetho.defaultDumperPluginsProvider(applicationContext))
|
||||
Stetho.defaultDumperPluginsProvider(applicationContext)
|
||||
)
|
||||
|
||||
// Use the InitializerBuilder to generate an Initializer
|
||||
val initializer = initializerBuilder.build()
|
||||
@@ -54,6 +56,6 @@ class DebugApp : App() {
|
||||
|
||||
override fun isDisposedRxExceptionsReported(): Boolean {
|
||||
return PreferenceManager.getDefaultSharedPreferences(this)
|
||||
.getBoolean(getString(R.string.allow_disposed_exceptions_key), false)
|
||||
.getBoolean(getString(R.string.allow_disposed_exceptions_key), false)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
package org.schabi.newpipe.settings;
|
||||
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.preference.Preference;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
|
||||
import leakcanary.LeakCanary;
|
||||
|
||||
public class DebugSettingsFragment extends BasePreferenceFragment {
|
||||
@Override
|
||||
public void onCreatePreferences(final Bundle savedInstanceState, final String rootKey) {
|
||||
addPreferencesFromResource(R.xml.debug_settings);
|
||||
|
||||
final Preference showMemoryLeaksPreference
|
||||
= findPreference(getString(R.string.show_memory_leaks_key));
|
||||
final Preference crashTheAppPreference
|
||||
= findPreference(getString(R.string.crash_the_app_key));
|
||||
|
||||
assert showMemoryLeaksPreference != null;
|
||||
assert crashTheAppPreference != null;
|
||||
|
||||
showMemoryLeaksPreference.setOnPreferenceClickListener(preference -> {
|
||||
startActivity(LeakCanary.INSTANCE.newLeakDisplayActivityIntent());
|
||||
return true;
|
||||
});
|
||||
|
||||
crashTheAppPreference.setOnPreferenceClickListener(preference -> {
|
||||
throw new RuntimeException();
|
||||
});
|
||||
}
|
||||
}
|
||||
56
app/src/debug/res/xml/main_settings.xml
Normal file
56
app/src/debug/res/xml/main_settings.xml
Normal file
@@ -0,0 +1,56 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:key="general_preferences"
|
||||
android:title="@string/settings">
|
||||
|
||||
<PreferenceScreen
|
||||
android:fragment="org.schabi.newpipe.settings.VideoAudioSettingsFragment"
|
||||
android:icon="@drawable/ic_headset"
|
||||
android:title="@string/settings_category_video_audio_title"
|
||||
app:iconSpaceReserved="false" />
|
||||
|
||||
<PreferenceScreen
|
||||
android:fragment="org.schabi.newpipe.settings.DownloadSettingsFragment"
|
||||
android:icon="@drawable/ic_file_download"
|
||||
android:title="@string/settings_category_downloads_title"
|
||||
app:iconSpaceReserved="false" />
|
||||
|
||||
<PreferenceScreen
|
||||
android:fragment="org.schabi.newpipe.settings.AppearanceSettingsFragment"
|
||||
android:icon="@drawable/ic_palette"
|
||||
android:title="@string/settings_category_appearance_title"
|
||||
app:iconSpaceReserved="false" />
|
||||
|
||||
<PreferenceScreen
|
||||
android:fragment="org.schabi.newpipe.settings.HistorySettingsFragment"
|
||||
android:icon="@drawable/ic_history"
|
||||
android:title="@string/settings_category_history_title"
|
||||
app:iconSpaceReserved="false" />
|
||||
|
||||
<PreferenceScreen
|
||||
android:fragment="org.schabi.newpipe.settings.ContentSettingsFragment"
|
||||
android:icon="@drawable/ic_language"
|
||||
android:title="@string/content"
|
||||
app:iconSpaceReserved="false" />
|
||||
|
||||
<PreferenceScreen
|
||||
android:fragment="org.schabi.newpipe.settings.NotificationSettingsFragment"
|
||||
android:icon="@drawable/ic_play_arrow"
|
||||
android:title="@string/settings_category_notification_title"
|
||||
app:iconSpaceReserved="false" />
|
||||
|
||||
<PreferenceScreen
|
||||
android:fragment="org.schabi.newpipe.settings.UpdateSettingsFragment"
|
||||
android:icon="@drawable/ic_cloud_download"
|
||||
android:key="update_pref_screen_key"
|
||||
android:title="@string/settings_category_updates_title"
|
||||
app:iconSpaceReserved="false" />
|
||||
|
||||
<PreferenceScreen
|
||||
android:fragment="org.schabi.newpipe.settings.DebugSettingsFragment"
|
||||
android:icon="@drawable/ic_bug_report"
|
||||
android:key="@string/debug_pref_screen_key"
|
||||
android:title="@string/settings_category_debug_title"
|
||||
app:iconSpaceReserved="false" />
|
||||
</PreferenceScreen>
|
||||
@@ -1,7 +1,8 @@
|
||||
<?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">
|
||||
package="org.schabi.newpipe"
|
||||
android:installLocation="auto">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
@@ -23,6 +24,7 @@
|
||||
android:logo="@mipmap/ic_launcher"
|
||||
android:requestLegacyExternalStorage="true"
|
||||
android:theme="@style/OpeningTheme"
|
||||
android:resizeableActivity="true"
|
||||
tools:ignore="AllowBackup">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
@@ -43,34 +45,19 @@
|
||||
</receiver>
|
||||
|
||||
<service
|
||||
android:name=".player.BackgroundPlayer"
|
||||
android:exported="false">
|
||||
android:name=".player.MainPlayer"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="mediaPlayback">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MEDIA_BUTTON" />
|
||||
</intent-filter>
|
||||
</service>
|
||||
|
||||
<activity
|
||||
android:name=".player.BackgroundPlayerActivity"
|
||||
android:label="@string/title_activity_background_player"
|
||||
android:name=".player.PlayQueueActivity"
|
||||
android:label="@string/title_activity_play_queue"
|
||||
android:launchMode="singleTask" />
|
||||
|
||||
<activity
|
||||
android:name=".player.PopupVideoPlayerActivity"
|
||||
android:label="@string/title_activity_popup_player"
|
||||
android:launchMode="singleTask" />
|
||||
|
||||
<service
|
||||
android:name=".player.PopupVideoPlayer"
|
||||
android:exported="false" />
|
||||
|
||||
<activity
|
||||
android:name=".player.MainVideoPlayer"
|
||||
android:configChanges="keyboard|keyboardHidden|orientation|screenSize"
|
||||
android:label="@string/app_name"
|
||||
android:launchMode="singleTask"
|
||||
android:theme="@style/VideoPlayerTheme" />
|
||||
|
||||
<activity
|
||||
android:name=".settings.SettingsActivity"
|
||||
android:label="@string/settings" />
|
||||
@@ -99,7 +86,7 @@
|
||||
android:name=".ExitActivity"
|
||||
android:label="@string/general_error"
|
||||
android:theme="@android:style/Theme.NoDisplay" />
|
||||
<activity android:name=".report.ErrorActivity" />
|
||||
<activity android:name=".error.ErrorActivity" />
|
||||
|
||||
<!-- giga get related -->
|
||||
<activity
|
||||
@@ -120,7 +107,7 @@
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".ReCaptchaActivity"
|
||||
android:name=".error.ReCaptchaActivity"
|
||||
android:label="@string/recaptcha" />
|
||||
|
||||
<provider
|
||||
@@ -241,20 +228,22 @@
|
||||
<data android:host="invidio.us" />
|
||||
<data android:host="dev.invidio.us" />
|
||||
<data android:host="www.invidio.us" />
|
||||
<data android:host="redirect.invidious.io" />
|
||||
<data android:host="invidious.snopyta.org" />
|
||||
<data android:host="fi.invidious.snopyta.org" />
|
||||
<data android:host="yewtu.be" />
|
||||
<data android:host="invidious.ggc-project.de" />
|
||||
<data android:host="yt.maisputain.ovh" />
|
||||
<data android:host="invidious.13ad.de" />
|
||||
<data android:host="invidious.toot.koeln" />
|
||||
<data android:host="tube.connect.cafe" />
|
||||
<data android:host="invidious.zapashcanon.fr" />
|
||||
<data android:host="invidious.kavin.rocks" />
|
||||
<data android:host="invidious.tube" />
|
||||
<data android:host="invidious.site" />
|
||||
<data android:host="invidious.xyz" />
|
||||
<data android:host="vid.mint.lgbt" />
|
||||
<data android:host="invidiou.site" />
|
||||
<data android:host="invidious.fdn.fr" />
|
||||
<data android:host="watch.nettohikari.com" />
|
||||
<data android:host="invidious.snwmds.net" />
|
||||
<data android:host="invidious.snwmds.org" />
|
||||
<data android:host="invidious.snwmds.com" />
|
||||
<data android:host="invidious.sunsetravens.com" />
|
||||
<data android:host="invidious.gachirangers.com" />
|
||||
<data android:host="invidious.048596.xyz" />
|
||||
<data android:host="invidious.zee.li" />
|
||||
<data android:host="vid.puffyan.us" />
|
||||
<data android:host="ytprivate.com" />
|
||||
<data android:pathPrefix="/" />
|
||||
</intent-filter>
|
||||
|
||||
@@ -282,7 +271,7 @@
|
||||
<data android:mimeType="text/plain" />
|
||||
</intent-filter>
|
||||
|
||||
<!-- MediaCCC filter -->
|
||||
<!-- media.ccc.de filter -->
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<action android:name="android.media.action.MEDIA_PLAY_FROM_SEARCH" />
|
||||
@@ -319,8 +308,7 @@
|
||||
<data android:host="peertube.cpy.re" />
|
||||
<data android:host="peertube.mastodon.host" />
|
||||
<data android:host="peertube.fr" />
|
||||
<data android:host="peertube.live" />
|
||||
<data android:host="peertube.video" />
|
||||
<data android:host="tilvids.com" />
|
||||
<data android:host="tube.privacytools.io" />
|
||||
<data android:host="video.ploud.fr" />
|
||||
<data android:host="video.lqdn.fr" />
|
||||
@@ -330,9 +318,33 @@
|
||||
<data android:pathPrefix="/accounts/" />
|
||||
<data android:pathPrefix="/video-channels/" />
|
||||
</intent-filter>
|
||||
|
||||
<!-- Bandcamp filter -->
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW"/>
|
||||
<action android:name="android.media.action.MEDIA_PLAY_FROM_SEARCH"/>
|
||||
<action android:name="android.nfc.action.NDEF_DISCOVERED"/>
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
<category android:name="android.intent.category.BROWSABLE"/>
|
||||
|
||||
<data android:scheme="http"/>
|
||||
<data android:scheme="https"/>
|
||||
<data android:host="bandcamp.com"/>
|
||||
<data android:host="*.bandcamp.com"/>
|
||||
<data android:pathPrefix="/"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<service
|
||||
android:name=".RouterActivity$FetcherService"
|
||||
android:exported="false" />
|
||||
|
||||
<!-- opting out of sending metrics to Google in Android System WebView -->
|
||||
<meta-data android:name="android.webkit.WebView.MetricsOptOut" android:value="true" />
|
||||
<!-- see https://github.com/TeamNewPipe/NewPipe/issues/3947 -->
|
||||
<!-- Version < 3.0. DeX Mode and Screen Mirroring support -->
|
||||
<meta-data android:name="com.samsung.android.keepalive.density" android:value="true"/>
|
||||
<!-- Version >= 3.0. DeX Dual Mode support -->
|
||||
<meta-data android:name="com.samsung.android.multidisplay.keep_process_alive" android:value="true"/>
|
||||
</application>
|
||||
</manifest>
|
||||
|
||||
245
app/src/main/assets/epl1.html
Normal file
245
app/src/main/assets/epl1.html
Normal file
@@ -0,0 +1,245 @@
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
|
||||
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||
<!-- saved from url=(0050)https://www.eclipse.org/org/documents/epl-v10.html -->
|
||||
<html xmlns="http://www.w3.org/1999/xhtml">
|
||||
<head>
|
||||
<meta content="text/html; charset=utf-8" http-equiv="Content-Type">
|
||||
|
||||
<title>Eclipse Public License - Version 1.0</title>
|
||||
|
||||
</head>
|
||||
|
||||
<body cz-shortcut-listen="true" lang="EN-US">
|
||||
|
||||
<h2>Eclipse Public License - v 1.0</h2>
|
||||
|
||||
<p>THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE
|
||||
PUBLIC LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR
|
||||
DISTRIBUTION OF THE PROGRAM CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS
|
||||
AGREEMENT.</p>
|
||||
|
||||
<p><b>1. DEFINITIONS</b></p>
|
||||
|
||||
<p>"Contribution" means:</p>
|
||||
|
||||
<p class="list">a) in the case of the initial Contributor, the initial
|
||||
code and documentation distributed under this Agreement, and</p>
|
||||
<p class="list">b) in the case of each subsequent Contributor:</p>
|
||||
<p class="list">i) changes to the Program, and</p>
|
||||
<p class="list">ii) additions to the Program;</p>
|
||||
<p class="list">where such changes and/or additions to the Program
|
||||
originate from and are distributed by that particular Contributor. A
|
||||
Contribution 'originates' from a Contributor if it was added to the
|
||||
Program by such Contributor itself or anyone acting on such
|
||||
Contributor's behalf. Contributions do not include additions to the
|
||||
Program which: (i) are separate modules of software distributed in
|
||||
conjunction with the Program under their own license agreement, and (ii)
|
||||
are not derivative works of the Program.</p>
|
||||
|
||||
<p>"Contributor" means any person or entity that distributes
|
||||
the Program.</p>
|
||||
|
||||
<p>"Licensed Patents" mean patent claims licensable by a
|
||||
Contributor which are necessarily infringed by the use or sale of its
|
||||
Contribution alone or when combined with the Program.</p>
|
||||
|
||||
<p>"Program" means the Contributions distributed in accordance
|
||||
with this Agreement.</p>
|
||||
|
||||
<p>"Recipient" means anyone who receives the Program under
|
||||
this Agreement, including all Contributors.</p>
|
||||
|
||||
<p><b>2. GRANT OF RIGHTS</b></p>
|
||||
|
||||
<p class="list">a) Subject to the terms of this Agreement, each
|
||||
Contributor hereby grants Recipient a non-exclusive, worldwide,
|
||||
royalty-free copyright license to reproduce, prepare derivative works
|
||||
of, publicly display, publicly perform, distribute and sublicense the
|
||||
Contribution of such Contributor, if any, and such derivative works, in
|
||||
source code and object code form.</p>
|
||||
|
||||
<p class="list">b) Subject to the terms of this Agreement, each
|
||||
Contributor hereby grants Recipient a non-exclusive, worldwide,
|
||||
royalty-free patent license under Licensed Patents to make, use, sell,
|
||||
offer to sell, import and otherwise transfer the Contribution of such
|
||||
Contributor, if any, in source code and object code form. This patent
|
||||
license shall apply to the combination of the Contribution and the
|
||||
Program if, at the time the Contribution is added by the Contributor,
|
||||
such addition of the Contribution causes such combination to be covered
|
||||
by the Licensed Patents. The patent license shall not apply to any other
|
||||
combinations which include the Contribution. No hardware per se is
|
||||
licensed hereunder.</p>
|
||||
|
||||
<p class="list">c) Recipient understands that although each Contributor
|
||||
grants the licenses to its Contributions set forth herein, no assurances
|
||||
are provided by any Contributor that the Program does not infringe the
|
||||
patent or other intellectual property rights of any other entity. Each
|
||||
Contributor disclaims any liability to Recipient for claims brought by
|
||||
any other entity based on infringement of intellectual property rights
|
||||
or otherwise. As a condition to exercising the rights and licenses
|
||||
granted hereunder, each Recipient hereby assumes sole responsibility to
|
||||
secure any other intellectual property rights needed, if any. For
|
||||
example, if a third party patent license is required to allow Recipient
|
||||
to distribute the Program, it is Recipient's responsibility to acquire
|
||||
that license before distributing the Program.</p>
|
||||
|
||||
<p class="list">d) Each Contributor represents that to its knowledge it
|
||||
has sufficient copyright rights in its Contribution, if any, to grant
|
||||
the copyright license set forth in this Agreement.</p>
|
||||
|
||||
<p><b>3. REQUIREMENTS</b></p>
|
||||
|
||||
<p>A Contributor may choose to distribute the Program in object code
|
||||
form under its own license agreement, provided that:</p>
|
||||
|
||||
<p class="list">a) it complies with the terms and conditions of this
|
||||
Agreement; and</p>
|
||||
|
||||
<p class="list">b) its license agreement:</p>
|
||||
|
||||
<p class="list">i) effectively disclaims on behalf of all Contributors
|
||||
all warranties and conditions, express and implied, including warranties
|
||||
or conditions of title and non-infringement, and implied warranties or
|
||||
conditions of merchantability and fitness for a particular purpose;</p>
|
||||
|
||||
<p class="list">ii) effectively excludes on behalf of all Contributors
|
||||
all liability for damages, including direct, indirect, special,
|
||||
incidental and consequential damages, such as lost profits;</p>
|
||||
|
||||
<p class="list">iii) states that any provisions which differ from this
|
||||
Agreement are offered by that Contributor alone and not by any other
|
||||
party; and</p>
|
||||
|
||||
<p class="list">iv) states that source code for the Program is available
|
||||
from such Contributor, and informs licensees how to obtain it in a
|
||||
reasonable manner on or through a medium customarily used for software
|
||||
exchange.</p>
|
||||
|
||||
<p>When the Program is made available in source code form:</p>
|
||||
|
||||
<p class="list">a) it must be made available under this Agreement; and</p>
|
||||
|
||||
<p class="list">b) a copy of this Agreement must be included with each
|
||||
copy of the Program.</p>
|
||||
|
||||
<p>Contributors may not remove or alter any copyright notices contained
|
||||
within the Program.</p>
|
||||
|
||||
<p>Each Contributor must identify itself as the originator of its
|
||||
Contribution, if any, in a manner that reasonably allows subsequent
|
||||
Recipients to identify the originator of the Contribution.</p>
|
||||
|
||||
<p><b>4. COMMERCIAL DISTRIBUTION</b></p>
|
||||
|
||||
<p>Commercial distributors of software may accept certain
|
||||
responsibilities with respect to end users, business partners and the
|
||||
like. While this license is intended to facilitate the commercial use of
|
||||
the Program, the Contributor who includes the Program in a commercial
|
||||
product offering should do so in a manner which does not create
|
||||
potential liability for other Contributors. Therefore, if a Contributor
|
||||
includes the Program in a commercial product offering, such Contributor
|
||||
("Commercial Contributor") hereby agrees to defend and
|
||||
indemnify every other Contributor ("Indemnified Contributor")
|
||||
against any losses, damages and costs (collectively "Losses")
|
||||
arising from claims, lawsuits and other legal actions brought by a third
|
||||
party against the Indemnified Contributor to the extent caused by the
|
||||
acts or omissions of such Commercial Contributor in connection with its
|
||||
distribution of the Program in a commercial product offering. The
|
||||
obligations in this section do not apply to any claims or Losses
|
||||
relating to any actual or alleged intellectual property infringement. In
|
||||
order to qualify, an Indemnified Contributor must: a) promptly notify
|
||||
the Commercial Contributor in writing of such claim, and b) allow the
|
||||
Commercial Contributor to control, and cooperate with the Commercial
|
||||
Contributor in, the defense and any related settlement negotiations. The
|
||||
Indemnified Contributor may participate in any such claim at its own
|
||||
expense.</p>
|
||||
|
||||
<p>For example, a Contributor might include the Program in a commercial
|
||||
product offering, Product X. That Contributor is then a Commercial
|
||||
Contributor. If that Commercial Contributor then makes performance
|
||||
claims, or offers warranties related to Product X, those performance
|
||||
claims and warranties are such Commercial Contributor's responsibility
|
||||
alone. Under this section, the Commercial Contributor would have to
|
||||
defend claims against the other Contributors related to those
|
||||
performance claims and warranties, and if a court requires any other
|
||||
Contributor to pay any damages as a result, the Commercial Contributor
|
||||
must pay those damages.</p>
|
||||
|
||||
<p><b>5. NO WARRANTY</b></p>
|
||||
|
||||
<p>EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, THE PROGRAM IS
|
||||
PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS
|
||||
OF ANY KIND, EITHER EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION,
|
||||
ANY WARRANTIES OR CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY
|
||||
OR FITNESS FOR A PARTICULAR PURPOSE. Each Recipient is solely
|
||||
responsible for determining the appropriateness of using and
|
||||
distributing the Program and assumes all risks associated with its
|
||||
exercise of rights under this Agreement , including but not limited to
|
||||
the risks and costs of program errors, compliance with applicable laws,
|
||||
damage to or loss of data, programs or equipment, and unavailability or
|
||||
interruption of operations.</p>
|
||||
|
||||
<p><b>6. DISCLAIMER OF LIABILITY</b></p>
|
||||
|
||||
<p>EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, NEITHER RECIPIENT
|
||||
NOR ANY CONTRIBUTORS SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT,
|
||||
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING
|
||||
WITHOUT LIMITATION LOST PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF
|
||||
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
||||
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OR
|
||||
DISTRIBUTION OF THE PROGRAM OR THE EXERCISE OF ANY RIGHTS GRANTED
|
||||
HEREUNDER, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.</p>
|
||||
|
||||
<p><b>7. GENERAL</b></p>
|
||||
|
||||
<p>If any provision of this Agreement is invalid or unenforceable under
|
||||
applicable law, it shall not affect the validity or enforceability of
|
||||
the remainder of the terms of this Agreement, and without further action
|
||||
by the parties hereto, such provision shall be reformed to the minimum
|
||||
extent necessary to make such provision valid and enforceable.</p>
|
||||
|
||||
<p>If Recipient institutes patent litigation against any entity
|
||||
(including a cross-claim or counterclaim in a lawsuit) alleging that the
|
||||
Program itself (excluding combinations of the Program with other
|
||||
software or hardware) infringes such Recipient's patent(s), then such
|
||||
Recipient's rights granted under Section 2(b) shall terminate as of the
|
||||
date such litigation is filed.</p>
|
||||
|
||||
<p>All Recipient's rights under this Agreement shall terminate if it
|
||||
fails to comply with any of the material terms or conditions of this
|
||||
Agreement and does not cure such failure in a reasonable period of time
|
||||
after becoming aware of such noncompliance. If all Recipient's rights
|
||||
under this Agreement terminate, Recipient agrees to cease use and
|
||||
distribution of the Program as soon as reasonably practicable. However,
|
||||
Recipient's obligations under this Agreement and any licenses granted by
|
||||
Recipient relating to the Program shall continue and survive.</p>
|
||||
|
||||
<p>Everyone is permitted to copy and distribute copies of this
|
||||
Agreement, but in order to avoid inconsistency the Agreement is
|
||||
copyrighted and may only be modified in the following manner. The
|
||||
Agreement Steward reserves the right to publish new versions (including
|
||||
revisions) of this Agreement from time to time. No one other than the
|
||||
Agreement Steward has the right to modify this Agreement. The Eclipse
|
||||
Foundation is the initial Agreement Steward. The Eclipse Foundation may
|
||||
assign the responsibility to serve as the Agreement Steward to a
|
||||
suitable separate entity. Each new version of the Agreement will be
|
||||
given a distinguishing version number. The Program (including
|
||||
Contributions) may always be distributed subject to the version of the
|
||||
Agreement under which it was received. In addition, after a new version
|
||||
of the Agreement is published, Contributor may elect to distribute the
|
||||
Program (including its Contributions) under the new version. Except as
|
||||
expressly stated in Sections 2(a) and 2(b) above, Recipient receives no
|
||||
rights or licenses to the intellectual property of any Contributor under
|
||||
this Agreement, whether expressly, by implication, estoppel or
|
||||
otherwise. All rights in the Program not expressly granted under this
|
||||
Agreement are reserved.</p>
|
||||
|
||||
<p>This Agreement is governed by the laws of the State of New York and
|
||||
the intellectual property laws of the United States of America. No party
|
||||
to this Agreement will bring a legal action under this Agreement more
|
||||
than one year after the cause of action arose. Each party waives its
|
||||
rights to a jury trial in any resulting litigation.</p>
|
||||
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,400 +0,0 @@
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
|
||||
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
|
||||
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
||||
<title>GNU General Public License v2.0 - GNU Project - Free Software Foundation (FSF)</title>
|
||||
<link rel="alternate" type="application/rdf+xml"
|
||||
href="http://www.gnu.org/licenses/old-licenses/gpl-2.0.rdf" />
|
||||
</head>
|
||||
<body>
|
||||
<h3><a id="SEC1">GNU GENERAL PUBLIC LICENSE</a></h3>
|
||||
<p>
|
||||
Version 2, June 1991
|
||||
</p>
|
||||
|
||||
<pre>
|
||||
Copyright (C) 1989, 1991 Free Software Foundation, Inc.<br/>
|
||||
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA<br/>
|
||||
<br/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
</pre>
|
||||
|
||||
<h3 id="preamble"><a id="SEC2">Preamble</a></h3>
|
||||
|
||||
<p>
|
||||
The licenses for most software are designed to take away your
|
||||
freedom to share and change it. By contrast, the GNU General Public
|
||||
License is intended to guarantee your freedom to share and change free
|
||||
software--to make sure the software is free for all its users. This
|
||||
General Public License applies to most of the Free Software
|
||||
Foundation's software and to any other program whose authors commit to
|
||||
using it. (Some other Free Software Foundation software is covered by
|
||||
the GNU Lesser General Public License instead.) You can apply it to
|
||||
your programs, too.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
this service if you wish), that you receive source code or can get it
|
||||
if you want it, that you can change the software or use pieces of it
|
||||
in new free programs; and that you know you can do these things.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
To protect your rights, we need to make restrictions that forbid
|
||||
anyone to deny you these rights or to ask you to surrender the rights.
|
||||
These restrictions translate to certain responsibilities for you if you
|
||||
distribute copies of the software, or if you modify it.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
For example, if you distribute copies of such a program, whether
|
||||
gratis or for a fee, you must give the recipients all the rights that
|
||||
you have. You must make sure that they, too, receive or can get the
|
||||
source code. And you must show them these terms so they know their
|
||||
rights.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
We protect your rights with two steps: (1) copyright the software, and
|
||||
(2) offer you this license which gives you legal permission to copy,
|
||||
distribute and/or modify the software.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Also, for each author's protection and ours, we want to make certain
|
||||
that everyone understands that there is no warranty for this free
|
||||
software. If the software is modified by someone else and passed on, we
|
||||
want its recipients to know that what they have is not the original, so
|
||||
that any problems introduced by others will not reflect on the original
|
||||
authors' reputations.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Finally, any free program is threatened constantly by software
|
||||
patents. We wish to avoid the danger that redistributors of a free
|
||||
program will individually obtain patent licenses, in effect making the
|
||||
program proprietary. To prevent this, we have made it clear that any
|
||||
patent must be licensed for everyone's free use or not licensed at all.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
</p>
|
||||
|
||||
|
||||
<h3 id="terms"><a id="SEC3">TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION</a></h3>
|
||||
|
||||
|
||||
<p id="section0">
|
||||
<strong>0.</strong>
|
||||
This License applies to any program or other work which contains
|
||||
a notice placed by the copyright holder saying it may be distributed
|
||||
under the terms of this General Public License. The "Program", below,
|
||||
refers to any such program or work, and a "work based on the Program"
|
||||
means either the Program or any derivative work under copyright law:
|
||||
that is to say, a work containing the Program or a portion of it,
|
||||
either verbatim or with modifications and/or translated into another
|
||||
language. (Hereinafter, translation is included without limitation in
|
||||
the term "modification".) Each licensee is addressed as "you".
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Activities other than copying, distribution and modification are not
|
||||
covered by this License; they are outside its scope. The act of
|
||||
running the Program is not restricted, and the output from the Program
|
||||
is covered only if its contents constitute a work based on the
|
||||
Program (independent of having been made by running the Program).
|
||||
Whether that is true depends on what the Program does.
|
||||
</p>
|
||||
|
||||
<p id="section1">
|
||||
<strong>1.</strong>
|
||||
You may copy and distribute verbatim copies of the Program's
|
||||
source code as you receive it, in any medium, provided that you
|
||||
conspicuously and appropriately publish on each copy an appropriate
|
||||
copyright notice and disclaimer of warranty; keep intact all the
|
||||
notices that refer to this License and to the absence of any warranty;
|
||||
and give any other recipients of the Program a copy of this License
|
||||
along with the Program.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
You may charge a fee for the physical act of transferring a copy, and
|
||||
you may at your option offer warranty protection in exchange for a fee.
|
||||
</p>
|
||||
|
||||
<p id="section2">
|
||||
<strong>2.</strong>
|
||||
You may modify your copy or copies of the Program or any portion
|
||||
of it, thus forming a work based on the Program, and copy and
|
||||
distribute such modifications or work under the terms of Section 1
|
||||
above, provided that you also meet all of these conditions:
|
||||
</p>
|
||||
|
||||
<dl>
|
||||
<dt></dt>
|
||||
<dd>
|
||||
<strong>a)</strong>
|
||||
You must cause the modified files to carry prominent notices
|
||||
stating that you changed the files and the date of any change.
|
||||
</dd>
|
||||
<dt></dt>
|
||||
<dd>
|
||||
<strong>b)</strong>
|
||||
You must cause any work that you distribute or publish, that in
|
||||
whole or in part contains or is derived from the Program or any
|
||||
part thereof, to be licensed as a whole at no charge to all third
|
||||
parties under the terms of this License.
|
||||
</dd>
|
||||
<dt></dt>
|
||||
<dd>
|
||||
<strong>c)</strong>
|
||||
If the modified program normally reads commands interactively
|
||||
when run, you must cause it, when started running for such
|
||||
interactive use in the most ordinary way, to print or display an
|
||||
announcement including an appropriate copyright notice and a
|
||||
notice that there is no warranty (or else, saying that you provide
|
||||
a warranty) and that users may redistribute the program under
|
||||
these conditions, and telling the user how to view a copy of this
|
||||
License. (Exception: if the Program itself is interactive but
|
||||
does not normally print such an announcement, your work based on
|
||||
the Program is not required to print an announcement.)
|
||||
</dd>
|
||||
</dl>
|
||||
|
||||
<p>
|
||||
These requirements apply to the modified work as a whole. If
|
||||
identifiable sections of that work are not derived from the Program,
|
||||
and can be reasonably considered independent and separate works in
|
||||
themselves, then this License, and its terms, do not apply to those
|
||||
sections when you distribute them as separate works. But when you
|
||||
distribute the same sections as part of a whole which is a work based
|
||||
on the Program, the distribution of the whole must be on the terms of
|
||||
this License, whose permissions for other licensees extend to the
|
||||
entire whole, and thus to each and every part regardless of who wrote it.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Thus, it is not the intent of this section to claim rights or contest
|
||||
your rights to work written entirely by you; rather, the intent is to
|
||||
exercise the right to control the distribution of derivative or
|
||||
collective works based on the Program.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
In addition, mere aggregation of another work not based on the Program
|
||||
with the Program (or with a work based on the Program) on a volume of
|
||||
a storage or distribution medium does not bring the other work under
|
||||
the scope of this License.
|
||||
</p>
|
||||
|
||||
<p id="section3">
|
||||
<strong>3.</strong>
|
||||
You may copy and distribute the Program (or a work based on it,
|
||||
under Section 2) in object code or executable form under the terms of
|
||||
Sections 1 and 2 above provided that you also do one of the following:
|
||||
</p>
|
||||
|
||||
<!-- we use this doubled UL to get the sub-sections indented, -->
|
||||
<!-- while making the bullets as unobvious as possible. -->
|
||||
|
||||
<dl>
|
||||
<dt></dt>
|
||||
<dd>
|
||||
<strong>a)</strong>
|
||||
Accompany it with the complete corresponding machine-readable
|
||||
source code, which must be distributed under the terms of Sections
|
||||
1 and 2 above on a medium customarily used for software interchange; or,
|
||||
</dd>
|
||||
<dt></dt>
|
||||
<dd>
|
||||
<strong>b)</strong>
|
||||
Accompany it with a written offer, valid for at least three
|
||||
years, to give any third party, for a charge no more than your
|
||||
cost of physically performing source distribution, a complete
|
||||
machine-readable copy of the corresponding source code, to be
|
||||
distributed under the terms of Sections 1 and 2 above on a medium
|
||||
customarily used for software interchange; or,
|
||||
</dd>
|
||||
<dt></dt>
|
||||
<dd>
|
||||
<strong>c)</strong>
|
||||
Accompany it with the information you received as to the offer
|
||||
to distribute corresponding source code. (This alternative is
|
||||
allowed only for noncommercial distribution and only if you
|
||||
received the program in object code or executable form with such
|
||||
an offer, in accord with Subsection b above.)
|
||||
</dd>
|
||||
</dl>
|
||||
|
||||
<p>
|
||||
The source code for a work means the preferred form of the work for
|
||||
making modifications to it. For an executable work, complete source
|
||||
code means all the source code for all modules it contains, plus any
|
||||
associated interface definition files, plus the scripts used to
|
||||
control compilation and installation of the executable. However, as a
|
||||
special exception, the source code distributed need not include
|
||||
anything that is normally distributed (in either source or binary
|
||||
form) with the major softwareComponents (compiler, kernel, and so on) of the
|
||||
operating system on which the executable runs, unless that component
|
||||
itself accompanies the executable.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
If distribution of executable or object code is made by offering
|
||||
access to copy from a designated place, then offering equivalent
|
||||
access to copy the source code from the same place counts as
|
||||
distribution of the source code, even though third parties are not
|
||||
compelled to copy the source along with the object code.
|
||||
</p>
|
||||
|
||||
<p id="section4">
|
||||
<strong>4.</strong>
|
||||
You may not copy, modify, sublicense, or distribute the Program
|
||||
except as expressly provided under this License. Any attempt
|
||||
otherwise to copy, modify, sublicense or distribute the Program is
|
||||
void, and will automatically terminate your rights under this License.
|
||||
However, parties who have received copies, or rights, from you under
|
||||
this License will not have their licenses terminated so long as such
|
||||
parties remain in full compliance.
|
||||
</p>
|
||||
|
||||
<p id="section5">
|
||||
<strong>5.</strong>
|
||||
You are not required to accept this License, since you have not
|
||||
signed it. However, nothing else grants you permission to modify or
|
||||
distribute the Program or its derivative works. These actions are
|
||||
prohibited by law if you do not accept this License. Therefore, by
|
||||
modifying or distributing the Program (or any work based on the
|
||||
Program), you indicate your acceptance of this License to do so, and
|
||||
all its terms and conditions for copying, distributing or modifying
|
||||
the Program or works based on it.
|
||||
</p>
|
||||
|
||||
<p id="section6">
|
||||
<strong>6.</strong>
|
||||
Each time you redistribute the Program (or any work based on the
|
||||
Program), the recipient automatically receives a license from the
|
||||
original licensor to copy, distribute or modify the Program subject to
|
||||
these terms and conditions. You may not impose any further
|
||||
restrictions on the recipients' exercise of the rights granted herein.
|
||||
You are not responsible for enforcing compliance by third parties to
|
||||
this License.
|
||||
</p>
|
||||
|
||||
<p id="section7">
|
||||
<strong>7.</strong>
|
||||
If, as a consequence of a court judgment or allegation of patent
|
||||
infringement or for any other reason (not limited to patent issues),
|
||||
conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot
|
||||
distribute so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you
|
||||
may not distribute the Program at all. For example, if a patent
|
||||
license would not permit royalty-free redistribution of the Program by
|
||||
all those who receive copies directly or indirectly through you, then
|
||||
the only way you could satisfy both it and this License would be to
|
||||
refrain entirely from distribution of the Program.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
If any portion of this section is held invalid or unenforceable under
|
||||
any particular circumstance, the balance of the section is intended to
|
||||
apply and the section as a whole is intended to apply in other
|
||||
circumstances.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
It is not the purpose of this section to induce you to infringe any
|
||||
patents or other property right claims or to contest validity of any
|
||||
such claims; this section has the sole purpose of protecting the
|
||||
integrity of the free software distribution system, which is
|
||||
implemented by public license practices. Many people have made
|
||||
generous contributions to the wide range of software distributed
|
||||
through that system in reliance on consistent application of that
|
||||
system; it is up to the author/donor to decide if he or she is willing
|
||||
to distribute software through any other system and a licensee cannot
|
||||
impose that choice.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
This section is intended to make thoroughly clear what is believed to
|
||||
be a consequence of the rest of this License.
|
||||
</p>
|
||||
|
||||
<p id="section8">
|
||||
<strong>8.</strong>
|
||||
If the distribution and/or use of the Program is restricted in
|
||||
certain countries either by patents or by copyrighted interfaces, the
|
||||
original copyright holder who places the Program under this License
|
||||
may add an explicit geographical distribution limitation excluding
|
||||
those countries, so that distribution is permitted only in or among
|
||||
countries not thus excluded. In such case, this License incorporates
|
||||
the limitation as if written in the body of this License.
|
||||
</p>
|
||||
|
||||
<p id="section9">
|
||||
<strong>9.</strong>
|
||||
The Free Software Foundation may publish revised and/or new versions
|
||||
of the General Public License from time to time. Such new versions will
|
||||
be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Each version is given a distinguishing version number. If the Program
|
||||
specifies a version number of this License which applies to it and "any
|
||||
later version", you have the option of following the terms and conditions
|
||||
either of that version or of any later version published by the Free
|
||||
Software Foundation. If the Program does not specify a version number of
|
||||
this License, you may choose any version ever published by the Free Software
|
||||
Foundation.
|
||||
</p>
|
||||
|
||||
<p id="section10">
|
||||
<strong>10.</strong>
|
||||
If you wish to incorporate parts of the Program into other free
|
||||
programs whose distribution conditions are different, write to the author
|
||||
to ask for permission. For software which is copyrighted by the Free
|
||||
Software Foundation, write to the Free Software Foundation; we sometimes
|
||||
make exceptions for this. Our decision will be guided by the two goals
|
||||
of preserving the free status of all derivatives of our free software and
|
||||
of promoting the sharing and reuse of software generally.
|
||||
</p>
|
||||
|
||||
<p id="section11"><strong>NO WARRANTY</strong></p>
|
||||
|
||||
<p>
|
||||
<strong>11.</strong>
|
||||
BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
|
||||
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
|
||||
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
|
||||
PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
|
||||
OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
|
||||
TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
|
||||
PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
|
||||
REPAIR OR CORRECTION.
|
||||
</p>
|
||||
|
||||
<p id="section12">
|
||||
<strong>12.</strong>
|
||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
|
||||
REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
|
||||
OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
|
||||
TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
|
||||
YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
|
||||
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
|
||||
POSSIBILITY OF SUCH DAMAGES.
|
||||
</p>
|
||||
</body></html>
|
||||
@@ -86,8 +86,8 @@ public abstract class FragmentStatePagerAdapterMenuWorkaround extends PagerAdapt
|
||||
private final int mBehavior;
|
||||
private FragmentTransaction mCurTransaction = null;
|
||||
|
||||
private ArrayList<Fragment.SavedState> mSavedState = new ArrayList<Fragment.SavedState>();
|
||||
private ArrayList<Fragment> mFragments = new ArrayList<Fragment>();
|
||||
private final ArrayList<Fragment.SavedState> mSavedState = new ArrayList<Fragment.SavedState>();
|
||||
private final ArrayList<Fragment> mFragments = new ArrayList<Fragment>();
|
||||
private Fragment mCurrentPrimaryItem = null;
|
||||
|
||||
/**
|
||||
@@ -150,7 +150,7 @@ public abstract class FragmentStatePagerAdapterMenuWorkaround extends PagerAdapt
|
||||
// from its saved state, where the fragment manager has already
|
||||
// taken care of restoring the fragments we previously had instantiated.
|
||||
if (mFragments.size() > position) {
|
||||
Fragment f = mFragments.get(position);
|
||||
final Fragment f = mFragments.get(position);
|
||||
if (f != null) {
|
||||
return f;
|
||||
}
|
||||
@@ -160,12 +160,12 @@ public abstract class FragmentStatePagerAdapterMenuWorkaround extends PagerAdapt
|
||||
mCurTransaction = mFragmentManager.beginTransaction();
|
||||
}
|
||||
|
||||
Fragment fragment = getItem(position);
|
||||
final Fragment fragment = getItem(position);
|
||||
if (DEBUG) {
|
||||
Log.v(TAG, "Adding item #" + position + ": f=" + fragment);
|
||||
}
|
||||
if (mSavedState.size() > position) {
|
||||
Fragment.SavedState fss = mSavedState.get(position);
|
||||
final Fragment.SavedState fss = mSavedState.get(position);
|
||||
if (fss != null) {
|
||||
fragment.setInitialSavedState(fss);
|
||||
}
|
||||
@@ -191,7 +191,7 @@ public abstract class FragmentStatePagerAdapterMenuWorkaround extends PagerAdapt
|
||||
@Override
|
||||
public void destroyItem(@NonNull final ViewGroup container, final int position,
|
||||
@NonNull final Object object) {
|
||||
Fragment fragment = (Fragment) object;
|
||||
final Fragment fragment = (Fragment) object;
|
||||
|
||||
if (mCurTransaction == null) {
|
||||
mCurTransaction = mFragmentManager.beginTransaction();
|
||||
@@ -217,7 +217,7 @@ public abstract class FragmentStatePagerAdapterMenuWorkaround extends PagerAdapt
|
||||
@SuppressWarnings({"ReferenceEquality", "deprecation"})
|
||||
public void setPrimaryItem(@NonNull final ViewGroup container, final int position,
|
||||
@NonNull final Object object) {
|
||||
Fragment fragment = (Fragment) object;
|
||||
final Fragment fragment = (Fragment) object;
|
||||
if (fragment != mCurrentPrimaryItem) {
|
||||
if (mCurrentPrimaryItem != null) {
|
||||
mCurrentPrimaryItem.setMenuVisibility(false);
|
||||
@@ -267,17 +267,17 @@ public abstract class FragmentStatePagerAdapterMenuWorkaround extends PagerAdapt
|
||||
Bundle state = null;
|
||||
if (mSavedState.size() > 0) {
|
||||
state = new Bundle();
|
||||
Fragment.SavedState[] fss = new Fragment.SavedState[mSavedState.size()];
|
||||
final Fragment.SavedState[] fss = new Fragment.SavedState[mSavedState.size()];
|
||||
mSavedState.toArray(fss);
|
||||
state.putParcelableArray("states", fss);
|
||||
}
|
||||
for (int i = 0; i < mFragments.size(); i++) {
|
||||
Fragment f = mFragments.get(i);
|
||||
final Fragment f = mFragments.get(i);
|
||||
if (f != null && f.isAdded()) {
|
||||
if (state == null) {
|
||||
state = new Bundle();
|
||||
}
|
||||
String key = "f" + i;
|
||||
final String key = "f" + i;
|
||||
mFragmentManager.putFragment(state, key, f);
|
||||
|
||||
//!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
@@ -294,21 +294,21 @@ public abstract class FragmentStatePagerAdapterMenuWorkaround extends PagerAdapt
|
||||
@Override
|
||||
public void restoreState(@Nullable final Parcelable state, @Nullable final ClassLoader loader) {
|
||||
if (state != null) {
|
||||
Bundle bundle = (Bundle) state;
|
||||
final Bundle bundle = (Bundle) state;
|
||||
bundle.setClassLoader(loader);
|
||||
Parcelable[] fss = bundle.getParcelableArray("states");
|
||||
final Parcelable[] fss = bundle.getParcelableArray("states");
|
||||
mSavedState.clear();
|
||||
mFragments.clear();
|
||||
if (fss != null) {
|
||||
for (int i = 0; i < fss.length; i++) {
|
||||
mSavedState.add((Fragment.SavedState) fss[i]);
|
||||
for (final Parcelable parcelable : fss) {
|
||||
mSavedState.add((Fragment.SavedState) parcelable);
|
||||
}
|
||||
}
|
||||
Iterable<String> keys = bundle.keySet();
|
||||
for (String key: keys) {
|
||||
final Iterable<String> keys = bundle.keySet();
|
||||
for (final String key : keys) {
|
||||
if (key.startsWith("f")) {
|
||||
int index = Integer.parseInt(key.substring(1));
|
||||
Fragment f = mFragmentManager.getFragment(bundle, key);
|
||||
final int index = Integer.parseInt(key.substring(1));
|
||||
final Fragment f = mFragmentManager.getFragment(bundle, key);
|
||||
if (f != null) {
|
||||
while (mFragments.size() <= index) {
|
||||
mFragments.add(null);
|
||||
|
||||
@@ -4,13 +4,18 @@ import android.content.Context;
|
||||
import android.graphics.Rect;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.widget.OverScroller;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.coordinatorlayout.widget.CoordinatorLayout;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
// See https://stackoverflow.com/questions/56849221#57997489
|
||||
public final class FlingBehavior extends AppBarLayout.Behavior {
|
||||
@@ -20,23 +25,28 @@ public final class FlingBehavior extends AppBarLayout.Behavior {
|
||||
super(context, attrs);
|
||||
}
|
||||
|
||||
private boolean allowScroll = true;
|
||||
private final Rect globalRect = new Rect();
|
||||
private final List<Integer> skipInterceptionOfElements = Arrays.asList(
|
||||
R.id.itemsListPanel, R.id.playbackSeekBar,
|
||||
R.id.playPauseButton, R.id.playPreviousButton, R.id.playNextButton);
|
||||
|
||||
@Override
|
||||
public boolean onRequestChildRectangleOnScreen(
|
||||
@NonNull final CoordinatorLayout coordinatorLayout, @NonNull final AppBarLayout child,
|
||||
@NonNull final Rect rectangle, final boolean immediate) {
|
||||
|
||||
focusScrollRect.set(rectangle);
|
||||
|
||||
coordinatorLayout.offsetDescendantRectToMyCoords(child, focusScrollRect);
|
||||
|
||||
int height = coordinatorLayout.getHeight();
|
||||
final int height = coordinatorLayout.getHeight();
|
||||
|
||||
if (focusScrollRect.top <= 0 && focusScrollRect.bottom >= height) {
|
||||
// the child is too big to fit inside ourselves completely, ignore request
|
||||
return false;
|
||||
}
|
||||
|
||||
int dy;
|
||||
final int dy;
|
||||
|
||||
if (focusScrollRect.bottom > height) {
|
||||
dy = focusScrollRect.top;
|
||||
@@ -48,13 +58,24 @@ public final class FlingBehavior extends AppBarLayout.Behavior {
|
||||
return false;
|
||||
}
|
||||
|
||||
int consumed = scroll(coordinatorLayout, child, dy, getMaxDragOffset(child), 0);
|
||||
final int consumed = scroll(coordinatorLayout, child, dy, getMaxDragOffset(child), 0);
|
||||
|
||||
return consumed == dy;
|
||||
}
|
||||
|
||||
public boolean onInterceptTouchEvent(final CoordinatorLayout parent, final AppBarLayout child,
|
||||
final MotionEvent ev) {
|
||||
for (final Integer element : skipInterceptionOfElements) {
|
||||
final View view = child.findViewById(element);
|
||||
if (view != null) {
|
||||
final boolean visible = view.getGlobalVisibleRect(globalRect);
|
||||
if (visible && globalRect.contains((int) ev.getRawX(), (int) ev.getRawY())) {
|
||||
allowScroll = false;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
allowScroll = true;
|
||||
switch (ev.getActionMasked()) {
|
||||
case MotionEvent.ACTION_DOWN:
|
||||
// remove reference to old nested scrolling child
|
||||
@@ -68,17 +89,37 @@ public final class FlingBehavior extends AppBarLayout.Behavior {
|
||||
return super.onInterceptTouchEvent(parent, child, ev);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onStartNestedScroll(@NonNull final CoordinatorLayout parent,
|
||||
@NonNull final AppBarLayout child,
|
||||
@NonNull final View directTargetChild,
|
||||
final View target,
|
||||
final int nestedScrollAxes,
|
||||
final int type) {
|
||||
return allowScroll && super.onStartNestedScroll(
|
||||
parent, child, directTargetChild, target, nestedScrollAxes, type);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onNestedFling(@NonNull final CoordinatorLayout coordinatorLayout,
|
||||
@NonNull final AppBarLayout child,
|
||||
@NonNull final View target, final float velocityX,
|
||||
final float velocityY, final boolean consumed) {
|
||||
return allowScroll && super.onNestedFling(
|
||||
coordinatorLayout, child, target, velocityX, velocityY, consumed);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private OverScroller getScrollerField() {
|
||||
try {
|
||||
Class<?> headerBehaviorType = this.getClass()
|
||||
final Class<?> headerBehaviorType = this.getClass()
|
||||
.getSuperclass().getSuperclass().getSuperclass();
|
||||
if (headerBehaviorType != null) {
|
||||
Field field = headerBehaviorType.getDeclaredField("scroller");
|
||||
final Field field = headerBehaviorType.getDeclaredField("scroller");
|
||||
field.setAccessible(true);
|
||||
return ((OverScroller) field.get(this));
|
||||
}
|
||||
} catch (NoSuchFieldException | IllegalAccessException e) {
|
||||
} catch (final NoSuchFieldException | IllegalAccessException e) {
|
||||
// ?
|
||||
}
|
||||
return null;
|
||||
@@ -87,34 +128,35 @@ public final class FlingBehavior extends AppBarLayout.Behavior {
|
||||
@Nullable
|
||||
private Field getLastNestedScrollingChildRefField() {
|
||||
try {
|
||||
Class<?> headerBehaviorType = this.getClass().getSuperclass().getSuperclass();
|
||||
final Class<?> headerBehaviorType = this.getClass().getSuperclass().getSuperclass();
|
||||
if (headerBehaviorType != null) {
|
||||
Field field = headerBehaviorType.getDeclaredField("lastNestedScrollingChildRef");
|
||||
final Field field
|
||||
= headerBehaviorType.getDeclaredField("lastNestedScrollingChildRef");
|
||||
field.setAccessible(true);
|
||||
return field;
|
||||
}
|
||||
} catch (NoSuchFieldException e) {
|
||||
} catch (final NoSuchFieldException e) {
|
||||
// ?
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private void resetNestedScrollingChild() {
|
||||
Field field = getLastNestedScrollingChildRefField();
|
||||
final Field field = getLastNestedScrollingChildRefField();
|
||||
if (field != null) {
|
||||
try {
|
||||
Object value = field.get(this);
|
||||
final Object value = field.get(this);
|
||||
if (value != null) {
|
||||
field.set(this, null);
|
||||
}
|
||||
} catch (IllegalAccessException e) {
|
||||
} catch (final IllegalAccessException e) {
|
||||
// ?
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void stopAppBarLayoutFling() {
|
||||
OverScroller scroller = getScrollerField();
|
||||
final OverScroller scroller = getScrollerField();
|
||||
if (scroller != null) {
|
||||
scroller.forceFinished(true);
|
||||
}
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
package org.schabi.newpipe;
|
||||
|
||||
/*
|
||||
* Created by Christian Schabesberger on 24.12.15.
|
||||
*
|
||||
* Copyright (C) Christian Schabesberger 2015 <chris.schabesberger@mailbox.org>
|
||||
* ActivityCommunicator.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/>.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Singleton:
|
||||
* Used to send data between certain Activity/Services within the same process.
|
||||
* This can be considered as an ugly hack inside the Android universe.
|
||||
**/
|
||||
public class ActivityCommunicator {
|
||||
|
||||
private static ActivityCommunicator activityCommunicator;
|
||||
private volatile Class returnActivity;
|
||||
|
||||
public static ActivityCommunicator getCommunicator() {
|
||||
if (activityCommunicator == null) {
|
||||
activityCommunicator = new ActivityCommunicator();
|
||||
}
|
||||
return activityCommunicator;
|
||||
}
|
||||
|
||||
public Class getReturnActivity() {
|
||||
return returnActivity;
|
||||
}
|
||||
|
||||
public void setReturnActivity(final Class returnActivity) {
|
||||
this.returnActivity = returnActivity;
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,5 @@
|
||||
package org.schabi.newpipe;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.app.Application;
|
||||
import android.app.NotificationChannel;
|
||||
import android.app.NotificationManager;
|
||||
import android.content.Context;
|
||||
@@ -9,6 +7,9 @@ import android.content.SharedPreferences;
|
||||
import android.os.Build;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.multidex.MultiDexApplication;
|
||||
import androidx.preference.PreferenceManager;
|
||||
|
||||
import com.nostra13.universalimageloader.cache.memory.impl.LRULimitedMemoryCache;
|
||||
@@ -19,14 +20,14 @@ import org.acra.ACRA;
|
||||
import org.acra.config.ACRAConfigurationException;
|
||||
import org.acra.config.CoreConfiguration;
|
||||
import org.acra.config.CoreConfigurationBuilder;
|
||||
import org.acra.sender.ReportSenderFactory;
|
||||
import org.schabi.newpipe.error.ErrorActivity;
|
||||
import org.schabi.newpipe.error.ErrorInfo;
|
||||
import org.schabi.newpipe.error.ReCaptchaActivity;
|
||||
import org.schabi.newpipe.error.UserAction;
|
||||
import org.schabi.newpipe.extractor.NewPipe;
|
||||
import org.schabi.newpipe.extractor.downloader.Downloader;
|
||||
import org.schabi.newpipe.report.AcraReportSenderFactory;
|
||||
import org.schabi.newpipe.report.ErrorActivity;
|
||||
import org.schabi.newpipe.report.UserAction;
|
||||
import org.schabi.newpipe.ktx.ExceptionUtils;
|
||||
import org.schabi.newpipe.settings.SettingsActivity;
|
||||
import org.schabi.newpipe.util.ExceptionUtils;
|
||||
import org.schabi.newpipe.util.Localization;
|
||||
import org.schabi.newpipe.util.ServiceHelper;
|
||||
import org.schabi.newpipe.util.StateSaver;
|
||||
@@ -34,16 +35,17 @@ import org.schabi.newpipe.util.StateSaver;
|
||||
import java.io.IOException;
|
||||
import java.io.InterruptedIOException;
|
||||
import java.net.SocketException;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import io.reactivex.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;
|
||||
import io.reactivex.rxjava3.disposables.Disposable;
|
||||
import io.reactivex.rxjava3.exceptions.CompositeException;
|
||||
import io.reactivex.rxjava3.exceptions.MissingBackpressureException;
|
||||
import io.reactivex.rxjava3.exceptions.OnErrorNotImplementedException;
|
||||
import io.reactivex.rxjava3.exceptions.UndeliverableException;
|
||||
import io.reactivex.rxjava3.functions.Consumer;
|
||||
import io.reactivex.rxjava3.plugins.RxJavaPlugins;
|
||||
|
||||
/*
|
||||
* Copyright (C) Hans-Christoph Steiner 2016 <hans@eds.org>
|
||||
@@ -63,13 +65,15 @@ import io.reactivex.plugins.RxJavaPlugins;
|
||||
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
public class App extends Application {
|
||||
public class App extends MultiDexApplication {
|
||||
protected static final String TAG = App.class.toString();
|
||||
@SuppressWarnings("unchecked")
|
||||
private static final Class<? extends ReportSenderFactory>[]
|
||||
REPORT_SENDER_FACTORY_CLASSES = new Class[]{AcraReportSenderFactory.class};
|
||||
private static App app;
|
||||
public static final String PACKAGE_NAME = BuildConfig.APPLICATION_ID;
|
||||
|
||||
@Nullable
|
||||
private Disposable disposable = null;
|
||||
|
||||
@NonNull
|
||||
public static App getApp() {
|
||||
return app;
|
||||
}
|
||||
@@ -77,7 +81,6 @@ public class App extends Application {
|
||||
@Override
|
||||
protected void attachBaseContext(final Context base) {
|
||||
super.attachBaseContext(base);
|
||||
|
||||
initACRA();
|
||||
}
|
||||
|
||||
@@ -91,12 +94,12 @@ public class App extends Application {
|
||||
SettingsActivity.initSettings(this);
|
||||
|
||||
NewPipe.init(getDownloader(),
|
||||
Localization.getPreferredLocalization(this),
|
||||
Localization.getPreferredContentCountry(this));
|
||||
Localization.init(getApplicationContext());
|
||||
Localization.getPreferredLocalization(this),
|
||||
Localization.getPreferredContentCountry(this));
|
||||
Localization.initPrettyTime(Localization.resolvePrettyTime(getApplicationContext()));
|
||||
|
||||
StateSaver.init(this);
|
||||
initNotificationChannel();
|
||||
initNotificationChannels();
|
||||
|
||||
ServiceHelper.initServices(this);
|
||||
|
||||
@@ -106,11 +109,19 @@ public class App extends Application {
|
||||
configureRxJavaErrorHandler();
|
||||
|
||||
// Check for new version
|
||||
new CheckForNewAppVersionTask().execute();
|
||||
disposable = CheckForNewAppVersion.checkNewVersion(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTerminate() {
|
||||
if (disposable != null) {
|
||||
disposable.dispose();
|
||||
}
|
||||
super.onTerminate();
|
||||
}
|
||||
|
||||
protected Downloader getDownloader() {
|
||||
DownloaderImpl downloader = DownloaderImpl.init(null);
|
||||
final DownloaderImpl downloader = DownloaderImpl.init(null);
|
||||
setCookiesToDownloader(downloader);
|
||||
return downloader;
|
||||
}
|
||||
@@ -119,7 +130,7 @@ public class App extends Application {
|
||||
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(
|
||||
getApplicationContext());
|
||||
final String key = getApplicationContext().getString(R.string.recaptcha_cookies_key);
|
||||
downloader.setCookie(ReCaptchaActivity.RECAPTCHA_COOKIES_KEY, prefs.getString(key, ""));
|
||||
downloader.setCookie(ReCaptchaActivity.RECAPTCHA_COOKIES_KEY, prefs.getString(key, null));
|
||||
downloader.updateYoutubeRestrictedModeCookies(getApplicationContext());
|
||||
}
|
||||
|
||||
@@ -200,67 +211,61 @@ public class App extends Application {
|
||||
.build();
|
||||
}
|
||||
|
||||
private void initACRA() {
|
||||
try {
|
||||
final CoreConfiguration acraConfig = new CoreConfigurationBuilder(this)
|
||||
.setReportSenderFactoryClasses(REPORT_SENDER_FACTORY_CLASSES)
|
||||
.setBuildConfigClass(BuildConfig.class)
|
||||
.build();
|
||||
ACRA.init(this, acraConfig);
|
||||
} catch (ACRAConfigurationException ace) {
|
||||
ace.printStackTrace();
|
||||
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 void initNotificationChannel() {
|
||||
if (Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.O) {
|
||||
/**
|
||||
* Called in {@link #attachBaseContext(Context)} after calling the {@code super} method.
|
||||
* Should be overridden if MultiDex is enabled, since it has to be initialized before ACRA.
|
||||
*/
|
||||
protected void initACRA() {
|
||||
if (ACRA.isACRASenderServiceProcess()) {
|
||||
return;
|
||||
}
|
||||
|
||||
final 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);
|
||||
|
||||
setUpUpdateNotificationChannel(importance);
|
||||
try {
|
||||
final CoreConfiguration acraConfig = new CoreConfigurationBuilder(this)
|
||||
.setBuildConfigClass(BuildConfig.class)
|
||||
.build();
|
||||
ACRA.init(this, acraConfig);
|
||||
} catch (final ACRAConfigurationException exception) {
|
||||
exception.printStackTrace();
|
||||
ErrorActivity.reportError(this, new ErrorInfo(exception,
|
||||
UserAction.SOMETHING_ELSE, "Could not initialize ACRA crash report"));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up notification channel for app update.
|
||||
*
|
||||
* @param importance
|
||||
*/
|
||||
@TargetApi(Build.VERSION_CODES.O)
|
||||
private void setUpUpdateNotificationChannel(final int importance) {
|
||||
final String appUpdateId
|
||||
= getString(R.string.app_update_notification_channel_id);
|
||||
final CharSequence appUpdateName
|
||||
= getString(R.string.app_update_notification_channel_name);
|
||||
final String appUpdateDescription
|
||||
= getString(R.string.app_update_notification_channel_description);
|
||||
private void initNotificationChannels() {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
|
||||
return;
|
||||
}
|
||||
|
||||
NotificationChannel appUpdateChannel
|
||||
= new NotificationChannel(appUpdateId, appUpdateName, importance);
|
||||
appUpdateChannel.setDescription(appUpdateDescription);
|
||||
String id = getString(R.string.notification_channel_id);
|
||||
String name = getString(R.string.notification_channel_name);
|
||||
String description = getString(R.string.notification_channel_description);
|
||||
|
||||
NotificationManager appUpdateNotificationManager
|
||||
= (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
|
||||
appUpdateNotificationManager.createNotificationChannel(appUpdateChannel);
|
||||
// Keep this below DEFAULT to avoid making noise on every notification update for the main
|
||||
// and update channels
|
||||
int importance = NotificationManager.IMPORTANCE_LOW;
|
||||
|
||||
final NotificationChannel mainChannel = new NotificationChannel(id, name, importance);
|
||||
mainChannel.setDescription(description);
|
||||
|
||||
id = getString(R.string.app_update_notification_channel_id);
|
||||
name = getString(R.string.app_update_notification_channel_name);
|
||||
description = getString(R.string.app_update_notification_channel_description);
|
||||
|
||||
final NotificationChannel appUpdateChannel = new NotificationChannel(id, name, importance);
|
||||
appUpdateChannel.setDescription(description);
|
||||
|
||||
id = getString(R.string.hash_channel_id);
|
||||
name = getString(R.string.hash_channel_name);
|
||||
description = getString(R.string.hash_channel_description);
|
||||
importance = NotificationManager.IMPORTANCE_HIGH;
|
||||
|
||||
final NotificationChannel hashChannel = new NotificationChannel(id, name, importance);
|
||||
hashChannel.setDescription(description);
|
||||
|
||||
final NotificationManager notificationManager = getSystemService(NotificationManager.class);
|
||||
notificationManager.createNotificationChannels(Arrays.asList(mainChannel,
|
||||
appUpdateChannel, hashChannel));
|
||||
}
|
||||
|
||||
protected boolean isDisposedRxExceptionsReported() {
|
||||
|
||||
@@ -35,7 +35,7 @@ public abstract class BaseFragment extends Fragment {
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public void onAttach(final Context context) {
|
||||
public void onAttach(@NonNull final Context context) {
|
||||
super.onAttach(context);
|
||||
activity = (AppCompatActivity) context;
|
||||
}
|
||||
@@ -61,7 +61,7 @@ public abstract class BaseFragment extends Fragment {
|
||||
|
||||
|
||||
@Override
|
||||
public void onViewCreated(final View rootView, final Bundle savedInstanceState) {
|
||||
public void onViewCreated(@NonNull final View rootView, final Bundle savedInstanceState) {
|
||||
super.onViewCreated(rootView, savedInstanceState);
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "onViewCreated() called with: "
|
||||
@@ -73,7 +73,7 @@ public abstract class BaseFragment extends Fragment {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSaveInstanceState(final Bundle outState) {
|
||||
public void onSaveInstanceState(@NonNull final Bundle outState) {
|
||||
super.onSaveInstanceState(outState);
|
||||
Icepick.saveInstanceState(this, outState);
|
||||
}
|
||||
|
||||
247
app/src/main/java/org/schabi/newpipe/CheckForNewAppVersion.java
Normal file
247
app/src/main/java/org/schabi/newpipe/CheckForNewAppVersion.java
Normal file
@@ -0,0 +1,247 @@
|
||||
package org.schabi.newpipe;
|
||||
|
||||
import android.app.Application;
|
||||
import android.app.PendingIntent;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.pm.PackageInfo;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.content.pm.Signature;
|
||||
import android.net.ConnectivityManager;
|
||||
import android.net.Uri;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.app.NotificationCompat;
|
||||
import androidx.core.app.NotificationManagerCompat;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.preference.PreferenceManager;
|
||||
|
||||
import com.grack.nanojson.JsonObject;
|
||||
import com.grack.nanojson.JsonParser;
|
||||
import com.grack.nanojson.JsonParserException;
|
||||
|
||||
import org.schabi.newpipe.error.ErrorActivity;
|
||||
import org.schabi.newpipe.error.ErrorInfo;
|
||||
import org.schabi.newpipe.error.UserAction;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.InputStream;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.cert.CertificateEncodingException;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.security.cert.CertificateFactory;
|
||||
import java.security.cert.X509Certificate;
|
||||
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.rxjava3.core.Maybe;
|
||||
import io.reactivex.rxjava3.disposables.Disposable;
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers;
|
||||
|
||||
public final class CheckForNewAppVersion {
|
||||
private CheckForNewAppVersion() { }
|
||||
|
||||
private static final boolean DEBUG = MainActivity.DEBUG;
|
||||
private static final String TAG = CheckForNewAppVersion.class.getSimpleName();
|
||||
|
||||
private static final String GITHUB_APK_SHA1
|
||||
= "B0:2E:90:7C:1C:D6:FC:57:C3:35:F0:88:D0:8F:50:5F:94:E4:D2:15";
|
||||
private static final String NEWPIPE_API_URL = "https://newpipe.net/api/data.json";
|
||||
|
||||
/**
|
||||
* Method to get the APK's SHA1 key. See https://stackoverflow.com/questions/9293019/#22506133.
|
||||
*
|
||||
* @param application The application
|
||||
* @return String with the APK's SHA1 fingerprint in hexadecimal
|
||||
*/
|
||||
@NonNull
|
||||
private static String getCertificateSHA1Fingerprint(@NonNull final Application application) {
|
||||
final PackageInfo packageInfo;
|
||||
try {
|
||||
packageInfo = application.getPackageManager().getPackageInfo(
|
||||
application.getPackageName(), PackageManager.GET_SIGNATURES);
|
||||
} catch (final PackageManager.NameNotFoundException e) {
|
||||
ErrorActivity.reportError(application, new ErrorInfo(e,
|
||||
UserAction.CHECK_FOR_NEW_APP_VERSION, "Could not find package info"));
|
||||
return "";
|
||||
}
|
||||
|
||||
final X509Certificate c;
|
||||
try {
|
||||
final Signature[] signatures = packageInfo.signatures;
|
||||
final byte[] cert = signatures[0].toByteArray();
|
||||
final InputStream input = new ByteArrayInputStream(cert);
|
||||
final CertificateFactory cf = CertificateFactory.getInstance("X509");
|
||||
c = (X509Certificate) cf.generateCertificate(input);
|
||||
} catch (final CertificateException e) {
|
||||
ErrorActivity.reportError(application, new ErrorInfo(e,
|
||||
UserAction.CHECK_FOR_NEW_APP_VERSION, "Certificate error"));
|
||||
return "";
|
||||
}
|
||||
|
||||
try {
|
||||
final MessageDigest md = MessageDigest.getInstance("SHA1");
|
||||
final byte[] publicKey = md.digest(c.getEncoded());
|
||||
return byte2HexFormatted(publicKey);
|
||||
} catch (NoSuchAlgorithmException | CertificateEncodingException e) {
|
||||
ErrorActivity.reportError(application, new ErrorInfo(e,
|
||||
UserAction.CHECK_FOR_NEW_APP_VERSION, "Could not retrieve SHA1 key"));
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
private static String byte2HexFormatted(final byte[] arr) {
|
||||
final StringBuilder str = new StringBuilder(arr.length * 2);
|
||||
|
||||
for (int i = 0; i < arr.length; i++) {
|
||||
String h = Integer.toHexString(arr[i]);
|
||||
final int l = h.length();
|
||||
if (l == 1) {
|
||||
h = "0" + h;
|
||||
}
|
||||
if (l > 2) {
|
||||
h = h.substring(l - 2, l);
|
||||
}
|
||||
str.append(h.toUpperCase());
|
||||
if (i < (arr.length - 1)) {
|
||||
str.append(':');
|
||||
}
|
||||
}
|
||||
return str.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to compare the current and latest available app version.
|
||||
* If a newer version is available, we show the update notification.
|
||||
*
|
||||
* @param application The application
|
||||
* @param versionName Name of new version
|
||||
* @param apkLocationUrl Url with the new apk
|
||||
* @param versionCode Code of new version
|
||||
*/
|
||||
private static void compareAppVersionAndShowNotification(@NonNull final Application application,
|
||||
final String versionName,
|
||||
final String apkLocationUrl,
|
||||
final int versionCode) {
|
||||
final int notificationId = 2000;
|
||||
|
||||
if (BuildConfig.VERSION_CODE < versionCode) {
|
||||
// A pending intent to open the apk location url in the browser.
|
||||
final Intent viewIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(apkLocationUrl));
|
||||
|
||||
final Intent intent = new Intent(Intent.ACTION_CHOOSER);
|
||||
intent.putExtra(Intent.EXTRA_INTENT, viewIntent);
|
||||
intent.putExtra(Intent.EXTRA_TITLE, R.string.open_with);
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
|
||||
final PendingIntent pendingIntent
|
||||
= PendingIntent.getActivity(application, 0, intent, 0);
|
||||
|
||||
final String channelId = application
|
||||
.getString(R.string.app_update_notification_channel_id);
|
||||
final NotificationCompat.Builder notificationBuilder
|
||||
= new NotificationCompat.Builder(application, channelId)
|
||||
.setSmallIcon(R.drawable.ic_newpipe_update)
|
||||
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
|
||||
.setContentIntent(pendingIntent)
|
||||
.setAutoCancel(true)
|
||||
.setContentTitle(application
|
||||
.getString(R.string.app_update_notification_content_title))
|
||||
.setContentText(application
|
||||
.getString(R.string.app_update_notification_content_text)
|
||||
+ " " + versionName);
|
||||
|
||||
final NotificationManagerCompat notificationManager
|
||||
= NotificationManagerCompat.from(application);
|
||||
notificationManager.notify(notificationId, notificationBuilder.build());
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isConnected(@NonNull final App app) {
|
||||
final ConnectivityManager connectivityManager =
|
||||
ContextCompat.getSystemService(app, ConnectivityManager.class);
|
||||
return connectivityManager != null && connectivityManager.getActiveNetworkInfo() != null
|
||||
&& connectivityManager.getActiveNetworkInfo().isConnected();
|
||||
}
|
||||
|
||||
public static boolean isGithubApk(@NonNull final App app) {
|
||||
return getCertificateSHA1Fingerprint(app).equals(GITHUB_APK_SHA1);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static Disposable checkNewVersion(@NonNull final App app) {
|
||||
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(app);
|
||||
final NewVersionManager manager = new NewVersionManager();
|
||||
|
||||
// Check if user has enabled/disabled update checking
|
||||
// and if the current apk is a github one or not.
|
||||
if (!prefs.getBoolean(app.getString(R.string.update_app_key), true) || !isGithubApk(app)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Check if the last request has happened a certain time ago
|
||||
// to reduce the number of API requests.
|
||||
final long expiry = prefs.getLong(app.getString(R.string.update_expiry_key), 0);
|
||||
if (!manager.isExpired(expiry)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return Maybe
|
||||
.fromCallable(() -> {
|
||||
if (!isConnected(app)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Make a network request to get latest NewPipe data.
|
||||
return DownloaderImpl.getInstance().get(NEWPIPE_API_URL);
|
||||
})
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
response -> {
|
||||
try {
|
||||
// Store a timestamp which needs to be exceeded,
|
||||
// before a new request to the API is made.
|
||||
final long newExpiry = manager
|
||||
.coerceExpiry(response.getHeader("expires"));
|
||||
prefs.edit()
|
||||
.putLong(app.getString(R.string.update_expiry_key), newExpiry)
|
||||
.apply();
|
||||
} catch (final Exception e) {
|
||||
if (DEBUG) {
|
||||
Log.w(TAG, "Could not extract and save new expiry date", e);
|
||||
}
|
||||
}
|
||||
|
||||
// Parse the json from the response.
|
||||
try {
|
||||
final JsonObject githubStableObject = JsonParser.object()
|
||||
.from(response.responseBody()).getObject("flavors")
|
||||
.getObject("github").getObject("stable");
|
||||
|
||||
final String versionName = githubStableObject
|
||||
.getString("version");
|
||||
final int versionCode = githubStableObject
|
||||
.getInt("version_code");
|
||||
final String apkLocationUrl = githubStableObject
|
||||
.getString("apk");
|
||||
|
||||
compareAppVersionAndShowNotification(app, versionName,
|
||||
apkLocationUrl, versionCode);
|
||||
} catch (final JsonParserException e) {
|
||||
// connectivity problems, do not alarm user and fail silently
|
||||
if (DEBUG) {
|
||||
Log.w(TAG, "Could not get NewPipe API: invalid json", e);
|
||||
}
|
||||
}
|
||||
},
|
||||
e -> {
|
||||
// connectivity problems, do not alarm user and fail silently
|
||||
if (DEBUG) {
|
||||
Log.w(TAG, "Could not get NewPipe API: network problem", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,221 +0,0 @@
|
||||
package org.schabi.newpipe;
|
||||
|
||||
import android.app.Application;
|
||||
import android.app.PendingIntent;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.pm.PackageInfo;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.content.pm.Signature;
|
||||
import android.net.ConnectivityManager;
|
||||
import android.net.Uri;
|
||||
import android.os.AsyncTask;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.core.app.NotificationCompat;
|
||||
import androidx.core.app.NotificationManagerCompat;
|
||||
|
||||
import com.grack.nanojson.JsonObject;
|
||||
import com.grack.nanojson.JsonParser;
|
||||
import com.grack.nanojson.JsonParserException;
|
||||
|
||||
import org.schabi.newpipe.extractor.exceptions.ReCaptchaException;
|
||||
import org.schabi.newpipe.report.ErrorActivity;
|
||||
import org.schabi.newpipe.report.UserAction;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.cert.CertificateEncodingException;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.security.cert.CertificateFactory;
|
||||
import java.security.cert.X509Certificate;
|
||||
|
||||
/**
|
||||
* AsyncTask to check if there is a newer version of the NewPipe github apk available or not.
|
||||
* If there is a newer version we show a notification, informing the user. On tapping
|
||||
* the notification, the user will be directed to the download link.
|
||||
*/
|
||||
public class CheckForNewAppVersionTask extends AsyncTask<Void, Void, String> {
|
||||
private static final boolean DEBUG = MainActivity.DEBUG;
|
||||
private static final String TAG = CheckForNewAppVersionTask.class.getSimpleName();
|
||||
|
||||
private static final Application APP = App.getApp();
|
||||
private static final String GITHUB_APK_SHA1
|
||||
= "B0:2E:90:7C:1C:D6:FC:57:C3:35:F0:88:D0:8F:50:5F:94:E4:D2:15";
|
||||
private static final String NEWPIPE_API_URL = "https://newpipe.schabi.org/api/data.json";
|
||||
|
||||
/**
|
||||
* Method to get the apk's SHA1 key. See https://stackoverflow.com/questions/9293019/#22506133.
|
||||
*
|
||||
* @return String with the apk's SHA1 fingeprint in hexadecimal
|
||||
*/
|
||||
private static String getCertificateSHA1Fingerprint() {
|
||||
final PackageManager pm = APP.getPackageManager();
|
||||
final String packageName = APP.getPackageName();
|
||||
final int flags = PackageManager.GET_SIGNATURES;
|
||||
PackageInfo packageInfo = null;
|
||||
|
||||
try {
|
||||
packageInfo = pm.getPackageInfo(packageName, flags);
|
||||
} catch (PackageManager.NameNotFoundException e) {
|
||||
ErrorActivity.reportError(APP, e, null, null,
|
||||
ErrorActivity.ErrorInfo.make(UserAction.SOMETHING_ELSE, "none",
|
||||
"Could not find package info", R.string.app_ui_crash));
|
||||
}
|
||||
|
||||
final Signature[] signatures = packageInfo.signatures;
|
||||
final byte[] cert = signatures[0].toByteArray();
|
||||
final InputStream input = new ByteArrayInputStream(cert);
|
||||
|
||||
X509Certificate c = null;
|
||||
|
||||
try {
|
||||
final CertificateFactory cf = CertificateFactory.getInstance("X509");
|
||||
c = (X509Certificate) cf.generateCertificate(input);
|
||||
} catch (CertificateException e) {
|
||||
ErrorActivity.reportError(APP, e, null, null,
|
||||
ErrorActivity.ErrorInfo.make(UserAction.SOMETHING_ELSE, "none",
|
||||
"Certificate error", R.string.app_ui_crash));
|
||||
}
|
||||
|
||||
String hexString = null;
|
||||
|
||||
try {
|
||||
MessageDigest md = MessageDigest.getInstance("SHA1");
|
||||
final byte[] publicKey = md.digest(c.getEncoded());
|
||||
hexString = byte2HexFormatted(publicKey);
|
||||
} catch (NoSuchAlgorithmException | CertificateEncodingException e) {
|
||||
ErrorActivity.reportError(APP, e, null, null,
|
||||
ErrorActivity.ErrorInfo.make(UserAction.SOMETHING_ELSE, "none",
|
||||
"Could not retrieve SHA1 key", R.string.app_ui_crash));
|
||||
}
|
||||
|
||||
return hexString;
|
||||
}
|
||||
|
||||
private static String byte2HexFormatted(final byte[] arr) {
|
||||
final StringBuilder str = new StringBuilder(arr.length * 2);
|
||||
|
||||
for (int i = 0; i < arr.length; i++) {
|
||||
String h = Integer.toHexString(arr[i]);
|
||||
final int l = h.length();
|
||||
if (l == 1) {
|
||||
h = "0" + h;
|
||||
}
|
||||
if (l > 2) {
|
||||
h = h.substring(l - 2, l);
|
||||
}
|
||||
str.append(h.toUpperCase());
|
||||
if (i < (arr.length - 1)) {
|
||||
str.append(':');
|
||||
}
|
||||
}
|
||||
return str.toString();
|
||||
}
|
||||
|
||||
public static boolean isGithubApk() {
|
||||
return getCertificateSHA1Fingerprint().equals(GITHUB_APK_SHA1);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPreExecute() {
|
||||
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(APP);
|
||||
|
||||
// Check if user has enabled/disabled update checking
|
||||
// and if the current apk is a github one or not.
|
||||
if (!prefs.getBoolean(APP.getString(R.string.update_app_key), true) || !isGithubApk()) {
|
||||
this.cancel(true);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String doInBackground(final Void... voids) {
|
||||
if (isCancelled() || !isConnected()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Make a network request to get latest NewPipe data.
|
||||
try {
|
||||
return DownloaderImpl.getInstance().get(NEWPIPE_API_URL).responseBody();
|
||||
} catch (IOException | ReCaptchaException e) {
|
||||
// connectivity problems, do not alarm user and fail silently
|
||||
if (DEBUG) {
|
||||
Log.w(TAG, Log.getStackTraceString(e));
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(final String response) {
|
||||
// Parse the json from the response.
|
||||
if (response != null) {
|
||||
|
||||
try {
|
||||
final JsonObject githubStableObject = JsonParser.object().from(response)
|
||||
.getObject("flavors").getObject("github").getObject("stable");
|
||||
|
||||
final String versionName = githubStableObject.getString("version");
|
||||
final int versionCode = githubStableObject.getInt("version_code");
|
||||
final String apkLocationUrl = githubStableObject.getString("apk");
|
||||
|
||||
compareAppVersionAndShowNotification(versionName, apkLocationUrl, versionCode);
|
||||
|
||||
} catch (JsonParserException e) {
|
||||
// connectivity problems, do not alarm user and fail silently
|
||||
if (DEBUG) {
|
||||
Log.w(TAG, Log.getStackTraceString(e));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to compare the current and latest available app version.
|
||||
* If a newer version is available, we show the update notification.
|
||||
*
|
||||
* @param versionName Name of new version
|
||||
* @param apkLocationUrl Url with the new apk
|
||||
* @param versionCode Code of new version
|
||||
*/
|
||||
private void compareAppVersionAndShowNotification(final String versionName,
|
||||
final String apkLocationUrl,
|
||||
final int versionCode) {
|
||||
int notificationId = 2000;
|
||||
|
||||
if (BuildConfig.VERSION_CODE < versionCode) {
|
||||
|
||||
// A pending intent to open the apk location url in the browser.
|
||||
final Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(apkLocationUrl));
|
||||
final PendingIntent pendingIntent
|
||||
= PendingIntent.getActivity(APP, 0, intent, 0);
|
||||
|
||||
final NotificationCompat.Builder notificationBuilder = new NotificationCompat
|
||||
.Builder(APP, APP.getString(R.string.app_update_notification_channel_id))
|
||||
.setSmallIcon(R.drawable.ic_newpipe_update)
|
||||
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
|
||||
.setContentIntent(pendingIntent)
|
||||
.setAutoCancel(true)
|
||||
.setContentTitle(APP.getString(R.string.app_update_notification_content_title))
|
||||
.setContentText(APP.getString(R.string.app_update_notification_content_text)
|
||||
+ " " + versionName);
|
||||
|
||||
final NotificationManagerCompat notificationManager
|
||||
= NotificationManagerCompat.from(APP);
|
||||
notificationManager.notify(notificationId, notificationBuilder.build());
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isConnected() {
|
||||
final ConnectivityManager cm =
|
||||
(ConnectivityManager) APP.getSystemService(Context.CONNECTIVITY_SERVICE);
|
||||
return cm.getActiveNetworkInfo() != null
|
||||
&& cm.getActiveNetworkInfo().isConnected();
|
||||
}
|
||||
}
|
||||
@@ -2,11 +2,12 @@ package org.schabi.newpipe;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Build;
|
||||
import android.preference.PreferenceManager;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.preference.PreferenceManager;
|
||||
|
||||
import org.schabi.newpipe.error.ReCaptchaActivity;
|
||||
import org.schabi.newpipe.extractor.downloader.Downloader;
|
||||
import org.schabi.newpipe.extractor.downloader.Request;
|
||||
import org.schabi.newpipe.extractor.downloader.Response;
|
||||
@@ -43,15 +44,15 @@ import static org.schabi.newpipe.MainActivity.DEBUG;
|
||||
|
||||
public final class DownloaderImpl extends Downloader {
|
||||
public static final String USER_AGENT
|
||||
= "Mozilla/5.0 (Windows NT 10.0; WOW64; rv:68.0) Gecko/20100101 Firefox/68.0";
|
||||
= "Mozilla/5.0 (Windows NT 10.0; rv:78.0) Gecko/20100101 Firefox/78.0";
|
||||
public static final String YOUTUBE_RESTRICTED_MODE_COOKIE_KEY
|
||||
= "youtube_restricted_mode_key";
|
||||
public static final String YOUTUBE_RESTRICTED_MODE_COOKIE = "PREF=f2=8000000";
|
||||
public static final String YOUTUBE_DOMAIN = "youtube.com";
|
||||
|
||||
private static DownloaderImpl instance;
|
||||
private Map<String, String> mCookies;
|
||||
private OkHttpClient client;
|
||||
private final Map<String, String> mCookies;
|
||||
private final OkHttpClient client;
|
||||
|
||||
private DownloaderImpl(final OkHttpClient.Builder builder) {
|
||||
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.KITKAT) {
|
||||
@@ -94,18 +95,18 @@ public final class DownloaderImpl extends Downloader {
|
||||
private static void enableModernTLS(final OkHttpClient.Builder builder) {
|
||||
try {
|
||||
// get the default TrustManager
|
||||
TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(
|
||||
final TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(
|
||||
TrustManagerFactory.getDefaultAlgorithm());
|
||||
trustManagerFactory.init((KeyStore) null);
|
||||
TrustManager[] trustManagers = trustManagerFactory.getTrustManagers();
|
||||
final TrustManager[] trustManagers = trustManagerFactory.getTrustManagers();
|
||||
if (trustManagers.length != 1 || !(trustManagers[0] instanceof X509TrustManager)) {
|
||||
throw new IllegalStateException("Unexpected default trust managers:"
|
||||
+ Arrays.toString(trustManagers));
|
||||
}
|
||||
X509TrustManager trustManager = (X509TrustManager) trustManagers[0];
|
||||
final X509TrustManager trustManager = (X509TrustManager) trustManagers[0];
|
||||
|
||||
// insert our own TLSSocketFactory
|
||||
SSLSocketFactory sslSocketFactory = TLSSocketFactoryCompat.getInstance();
|
||||
final SSLSocketFactory sslSocketFactory = TLSSocketFactoryCompat.getInstance();
|
||||
|
||||
builder.sslSocketFactory(sslSocketFactory, trustManager);
|
||||
|
||||
@@ -114,16 +115,16 @@ public final class DownloaderImpl extends Downloader {
|
||||
// Necessary because some servers (e.g. Framatube.org)
|
||||
// don't support the old cipher suites.
|
||||
// https://github.com/square/okhttp/issues/4053#issuecomment-402579554
|
||||
List<CipherSuite> cipherSuites = new ArrayList<>();
|
||||
cipherSuites.addAll(ConnectionSpec.MODERN_TLS.cipherSuites());
|
||||
final List<CipherSuite> cipherSuites =
|
||||
new ArrayList<>(ConnectionSpec.MODERN_TLS.cipherSuites());
|
||||
cipherSuites.add(CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA);
|
||||
cipherSuites.add(CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA);
|
||||
ConnectionSpec legacyTLS = new ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS)
|
||||
final ConnectionSpec legacyTLS = new ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS)
|
||||
.cipherSuites(cipherSuites.toArray(new CipherSuite[0]))
|
||||
.build();
|
||||
|
||||
builder.connectionSpecs(Arrays.asList(legacyTLS, ConnectionSpec.CLEARTEXT));
|
||||
} catch (KeyManagementException | NoSuchAlgorithmException | KeyStoreException e) {
|
||||
} catch (final KeyManagementException | NoSuchAlgorithmException | KeyStoreException e) {
|
||||
if (DEBUG) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
@@ -131,15 +132,15 @@ public final class DownloaderImpl extends Downloader {
|
||||
}
|
||||
|
||||
public String getCookies(final String url) {
|
||||
List<String> resultCookies = new ArrayList<>();
|
||||
final List<String> resultCookies = new ArrayList<>();
|
||||
if (url.contains(YOUTUBE_DOMAIN)) {
|
||||
String youtubeCookie = getCookie(YOUTUBE_RESTRICTED_MODE_COOKIE_KEY);
|
||||
final String youtubeCookie = getCookie(YOUTUBE_RESTRICTED_MODE_COOKIE_KEY);
|
||||
if (youtubeCookie != null) {
|
||||
resultCookies.add(youtubeCookie);
|
||||
}
|
||||
}
|
||||
// Recaptcha cookie is always added TODO: not sure if this is necessary
|
||||
String recaptchaCookie = getCookie(ReCaptchaActivity.RECAPTCHA_COOKIES_KEY);
|
||||
final String recaptchaCookie = getCookie(ReCaptchaActivity.RECAPTCHA_COOKIES_KEY);
|
||||
if (recaptchaCookie != null) {
|
||||
resultCookies.add(recaptchaCookie);
|
||||
}
|
||||
@@ -159,9 +160,9 @@ public final class DownloaderImpl extends Downloader {
|
||||
}
|
||||
|
||||
public void updateYoutubeRestrictedModeCookies(final Context context) {
|
||||
String restrictedModeEnabledKey =
|
||||
final String restrictedModeEnabledKey =
|
||||
context.getString(R.string.youtube_restricted_mode_enabled);
|
||||
boolean restrictedModeEnabled = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
final boolean restrictedModeEnabled = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
.getBoolean(restrictedModeEnabledKey, false);
|
||||
updateYoutubeRestrictedModeCookies(restrictedModeEnabled);
|
||||
}
|
||||
@@ -186,9 +187,9 @@ public final class DownloaderImpl extends Downloader {
|
||||
try {
|
||||
final Response response = head(url);
|
||||
return Long.parseLong(response.getHeader("Content-Length"));
|
||||
} catch (NumberFormatException e) {
|
||||
} catch (final NumberFormatException e) {
|
||||
throw new IOException("Invalid content length", e);
|
||||
} catch (ReCaptchaException e) {
|
||||
} catch (final ReCaptchaException e) {
|
||||
throw new IOException(e);
|
||||
}
|
||||
}
|
||||
@@ -199,7 +200,7 @@ public final class DownloaderImpl extends Downloader {
|
||||
.method("GET", null).url(siteUrl)
|
||||
.addHeader("User-Agent", USER_AGENT);
|
||||
|
||||
String cookies = getCookies(siteUrl);
|
||||
final String cookies = getCookies(siteUrl);
|
||||
if (!cookies.isEmpty()) {
|
||||
requestBuilder.addHeader("Cookie", cookies);
|
||||
}
|
||||
@@ -218,7 +219,7 @@ public final class DownloaderImpl extends Downloader {
|
||||
}
|
||||
|
||||
return body.byteStream();
|
||||
} catch (ReCaptchaException e) {
|
||||
} catch (final ReCaptchaException e) {
|
||||
throw new IOException(e.getMessage(), e.getCause());
|
||||
}
|
||||
}
|
||||
@@ -240,18 +241,18 @@ public final class DownloaderImpl extends Downloader {
|
||||
.method(httpMethod, requestBody).url(url)
|
||||
.addHeader("User-Agent", USER_AGENT);
|
||||
|
||||
String cookies = getCookies(url);
|
||||
final String cookies = getCookies(url);
|
||||
if (!cookies.isEmpty()) {
|
||||
requestBuilder.addHeader("Cookie", cookies);
|
||||
}
|
||||
|
||||
for (Map.Entry<String, List<String>> pair : headers.entrySet()) {
|
||||
for (final Map.Entry<String, List<String>> pair : headers.entrySet()) {
|
||||
final String headerName = pair.getKey();
|
||||
final List<String> headerValueList = pair.getValue();
|
||||
|
||||
if (headerValueList.size() > 1) {
|
||||
requestBuilder.removeHeader(headerName);
|
||||
for (String headerValue : headerValueList) {
|
||||
for (final String headerValue : headerValueList) {
|
||||
requestBuilder.addHeader(headerName, headerValue);
|
||||
}
|
||||
} else if (headerValueList.size() == 1) {
|
||||
|
||||
@@ -27,7 +27,7 @@ import android.os.Bundle;
|
||||
public class ExitActivity extends Activity {
|
||||
|
||||
public static void exitAndRemoveFromRecentApps(final Activity activity) {
|
||||
Intent intent = new Intent(activity, ExitActivity.class);
|
||||
final Intent intent = new Intent(activity, ExitActivity.class);
|
||||
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
| Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS
|
||||
@@ -42,7 +42,7 @@ public class ExitActivity extends Activity {
|
||||
protected void onCreate(final Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
if (Build.VERSION.SDK_INT >= 21) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
finishAndRemoveTask();
|
||||
} else {
|
||||
finish();
|
||||
|
||||
@@ -4,7 +4,8 @@ import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.res.Resources;
|
||||
import android.preference.PreferenceManager;
|
||||
|
||||
import androidx.preference.PreferenceManager;
|
||||
|
||||
import com.nostra13.universalimageloader.core.download.BaseImageDownloader;
|
||||
|
||||
|
||||
@@ -20,15 +20,18 @@
|
||||
|
||||
package org.schabi.newpipe;
|
||||
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.util.Log;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
@@ -36,23 +39,28 @@ import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.AdapterView;
|
||||
import android.widget.ArrayAdapter;
|
||||
import android.widget.Button;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.Spinner;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
import androidx.appcompat.app.ActionBarDrawerToggle;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.core.app.ActivityCompat;
|
||||
import androidx.core.view.GravityCompat;
|
||||
import androidx.drawerlayout.widget.DrawerLayout;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
import androidx.preference.PreferenceManager;
|
||||
|
||||
import com.google.android.material.navigation.NavigationView;
|
||||
import com.google.android.material.bottomsheet.BottomSheetBehavior;
|
||||
|
||||
import org.schabi.newpipe.databinding.ActivityMainBinding;
|
||||
import org.schabi.newpipe.databinding.DrawerHeaderBinding;
|
||||
import org.schabi.newpipe.databinding.DrawerLayoutBinding;
|
||||
import org.schabi.newpipe.databinding.InstanceSpinnerLayoutBinding;
|
||||
import org.schabi.newpipe.databinding.ToolbarLayoutBinding;
|
||||
import org.schabi.newpipe.error.ErrorActivity;
|
||||
import org.schabi.newpipe.extractor.NewPipe;
|
||||
import org.schabi.newpipe.extractor.StreamingService;
|
||||
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||
@@ -61,14 +69,18 @@ import org.schabi.newpipe.fragments.BackPressable;
|
||||
import org.schabi.newpipe.fragments.MainFragment;
|
||||
import org.schabi.newpipe.fragments.detail.VideoDetailFragment;
|
||||
import org.schabi.newpipe.fragments.list.search.SearchFragment;
|
||||
import org.schabi.newpipe.report.ErrorActivity;
|
||||
import org.schabi.newpipe.util.AndroidTvUtils;
|
||||
import org.schabi.newpipe.player.Player;
|
||||
import org.schabi.newpipe.player.event.OnKeyDownListener;
|
||||
import org.schabi.newpipe.player.helper.PlayerHolder;
|
||||
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
||||
import org.schabi.newpipe.util.Constants;
|
||||
import org.schabi.newpipe.util.DeviceUtils;
|
||||
import org.schabi.newpipe.util.KioskTranslator;
|
||||
import org.schabi.newpipe.util.Localization;
|
||||
import org.schabi.newpipe.util.NavigationHelper;
|
||||
import org.schabi.newpipe.util.PeertubeHelper;
|
||||
import org.schabi.newpipe.util.PermissionHelper;
|
||||
import org.schabi.newpipe.util.SerializedCache;
|
||||
import org.schabi.newpipe.util.ServiceHelper;
|
||||
import org.schabi.newpipe.util.StateSaver;
|
||||
import org.schabi.newpipe.util.TLSSocketFactoryCompat;
|
||||
@@ -77,6 +89,7 @@ import org.schabi.newpipe.views.FocusOverlayView;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
|
||||
|
||||
@@ -84,15 +97,16 @@ public class MainActivity extends AppCompatActivity {
|
||||
private static final String TAG = "MainActivity";
|
||||
public static final boolean DEBUG = !BuildConfig.BUILD_TYPE.equals("release");
|
||||
|
||||
private ActivityMainBinding mainBinding;
|
||||
private DrawerHeaderBinding drawerHeaderBinding;
|
||||
private DrawerLayoutBinding drawerLayoutBinding;
|
||||
private ToolbarLayoutBinding toolbarLayoutBinding;
|
||||
|
||||
private ActionBarDrawerToggle toggle;
|
||||
private DrawerLayout drawer;
|
||||
private NavigationView drawerItems;
|
||||
private ImageView headerServiceIcon;
|
||||
private TextView headerServiceView;
|
||||
private Button toggleServiceButton;
|
||||
|
||||
private boolean servicesShown = false;
|
||||
private ImageView serviceArrow;
|
||||
|
||||
private BroadcastReceiver broadcastReceiver;
|
||||
|
||||
private static final int ITEM_ID_SUBSCRIPTIONS = -1;
|
||||
private static final int ITEM_ID_FEED = -2;
|
||||
@@ -115,82 +129,86 @@ public class MainActivity extends AppCompatActivity {
|
||||
+ "savedInstanceState = [" + savedInstanceState + "]");
|
||||
}
|
||||
|
||||
// enable TLS1.1/1.2 for kitkat devices, to fix download and play for mediaCCC sources
|
||||
// enable TLS1.1/1.2 for kitkat devices, to fix download and play for media.ccc.de sources
|
||||
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.KITKAT) {
|
||||
TLSSocketFactoryCompat.setAsDefault();
|
||||
}
|
||||
|
||||
ThemeHelper.setDayNightMode(this);
|
||||
ThemeHelper.setTheme(this, ServiceHelper.getSelectedServiceId(this));
|
||||
|
||||
assureCorrectAppLanguage(this);
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_main);
|
||||
|
||||
if (getSupportFragmentManager() != null
|
||||
&& getSupportFragmentManager().getBackStackEntryCount() == 0) {
|
||||
mainBinding = ActivityMainBinding.inflate(getLayoutInflater());
|
||||
drawerLayoutBinding = mainBinding.drawerLayout;
|
||||
drawerHeaderBinding = DrawerHeaderBinding.bind(drawerLayoutBinding.navigation
|
||||
.getHeaderView(0));
|
||||
toolbarLayoutBinding = mainBinding.toolbarLayout;
|
||||
setContentView(mainBinding.getRoot());
|
||||
|
||||
if (getSupportFragmentManager().getBackStackEntryCount() == 0) {
|
||||
initFragments();
|
||||
}
|
||||
|
||||
setSupportActionBar(findViewById(R.id.toolbar));
|
||||
setSupportActionBar(toolbarLayoutBinding.toolbar);
|
||||
try {
|
||||
setupDrawer();
|
||||
} catch (Exception e) {
|
||||
ErrorActivity.reportUiError(this, e);
|
||||
} catch (final Exception e) {
|
||||
ErrorActivity.reportUiErrorInSnackbar(this, "Setting up drawer", e);
|
||||
}
|
||||
|
||||
if (AndroidTvUtils.isTv(this)) {
|
||||
if (DeviceUtils.isTv(this)) {
|
||||
FocusOverlayView.setupFocusObserver(this);
|
||||
}
|
||||
openMiniPlayerUponPlayerStarted();
|
||||
}
|
||||
|
||||
private void setupDrawer() throws Exception {
|
||||
final Toolbar toolbar = findViewById(R.id.toolbar);
|
||||
drawer = findViewById(R.id.drawer_layout);
|
||||
drawerItems = findViewById(R.id.navigation);
|
||||
|
||||
//Tabs
|
||||
int currentServiceId = ServiceHelper.getSelectedServiceId(this);
|
||||
StreamingService service = NewPipe.getService(currentServiceId);
|
||||
final int currentServiceId = ServiceHelper.getSelectedServiceId(this);
|
||||
final StreamingService service = NewPipe.getService(currentServiceId);
|
||||
|
||||
int kioskId = 0;
|
||||
|
||||
for (final String ks : service.getKioskList().getAvailableKiosks()) {
|
||||
drawerItems.getMenu()
|
||||
drawerLayoutBinding.navigation.getMenu()
|
||||
.add(R.id.menu_tabs_group, kioskId, 0, KioskTranslator
|
||||
.getTranslatedKioskName(ks, this))
|
||||
.setIcon(KioskTranslator.getKioskIcon(ks, this));
|
||||
kioskId++;
|
||||
}
|
||||
|
||||
drawerItems.getMenu()
|
||||
drawerLayoutBinding.navigation.getMenu()
|
||||
.add(R.id.menu_tabs_group, ITEM_ID_SUBSCRIPTIONS, ORDER,
|
||||
R.string.tab_subscriptions)
|
||||
.setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.ic_channel));
|
||||
drawerItems.getMenu()
|
||||
.setIcon(R.drawable.ic_tv);
|
||||
drawerLayoutBinding.navigation.getMenu()
|
||||
.add(R.id.menu_tabs_group, ITEM_ID_FEED, ORDER, R.string.fragment_feed_title)
|
||||
.setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.ic_rss));
|
||||
drawerItems.getMenu()
|
||||
.setIcon(R.drawable.ic_rss_feed);
|
||||
drawerLayoutBinding.navigation.getMenu()
|
||||
.add(R.id.menu_tabs_group, ITEM_ID_BOOKMARKS, ORDER, R.string.tab_bookmarks)
|
||||
.setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.ic_bookmark));
|
||||
drawerItems.getMenu()
|
||||
.setIcon(R.drawable.ic_bookmark);
|
||||
drawerLayoutBinding.navigation.getMenu()
|
||||
.add(R.id.menu_tabs_group, ITEM_ID_DOWNLOADS, ORDER, R.string.downloads)
|
||||
.setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.ic_file_download));
|
||||
drawerItems.getMenu()
|
||||
.setIcon(R.drawable.ic_file_download);
|
||||
drawerLayoutBinding.navigation.getMenu()
|
||||
.add(R.id.menu_tabs_group, ITEM_ID_HISTORY, ORDER, R.string.action_history)
|
||||
.setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.ic_history));
|
||||
.setIcon(R.drawable.ic_history);
|
||||
|
||||
//Settings and About
|
||||
drawerItems.getMenu()
|
||||
drawerLayoutBinding.navigation.getMenu()
|
||||
.add(R.id.menu_options_about_group, ITEM_ID_SETTINGS, ORDER, R.string.settings)
|
||||
.setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.ic_settings));
|
||||
drawerItems.getMenu()
|
||||
.setIcon(R.drawable.ic_settings);
|
||||
drawerLayoutBinding.navigation.getMenu()
|
||||
.add(R.id.menu_options_about_group, ITEM_ID_ABOUT, ORDER, R.string.tab_about)
|
||||
.setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.ic_info_outline));
|
||||
.setIcon(R.drawable.ic_info_outline);
|
||||
|
||||
toggle = new ActionBarDrawerToggle(this, drawer, toolbar, R.string.drawer_open,
|
||||
R.string.drawer_close);
|
||||
toggle = new ActionBarDrawerToggle(this, mainBinding.getRoot(),
|
||||
toolbarLayoutBinding.toolbar, R.string.drawer_open, R.string.drawer_close);
|
||||
toggle.syncState();
|
||||
drawer.addDrawerListener(toggle);
|
||||
drawer.addDrawerListener(new DrawerLayout.SimpleDrawerListener() {
|
||||
mainBinding.getRoot().addDrawerListener(toggle);
|
||||
mainBinding.getRoot().addDrawerListener(new DrawerLayout.SimpleDrawerListener() {
|
||||
private int lastService;
|
||||
|
||||
@Override
|
||||
@@ -204,12 +222,12 @@ public class MainActivity extends AppCompatActivity {
|
||||
toggleServices();
|
||||
}
|
||||
if (lastService != ServiceHelper.getSelectedServiceId(MainActivity.this)) {
|
||||
new Handler(Looper.getMainLooper()).post(MainActivity.this::recreate);
|
||||
ActivityCompat.recreate(MainActivity.this);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
drawerItems.setNavigationItemSelectedListener(this::drawerItemSelected);
|
||||
drawerLayoutBinding.navigation.setNavigationItemSelectedListener(this::drawerItemSelected);
|
||||
setupDrawerHeader();
|
||||
}
|
||||
|
||||
@@ -221,8 +239,8 @@ public class MainActivity extends AppCompatActivity {
|
||||
case R.id.menu_tabs_group:
|
||||
try {
|
||||
tabSelected(item);
|
||||
} catch (Exception e) {
|
||||
ErrorActivity.reportUiError(this, e);
|
||||
} catch (final Exception e) {
|
||||
ErrorActivity.reportUiErrorInSnackbar(this, "Selecting main page tab", e);
|
||||
}
|
||||
break;
|
||||
case R.id.menu_options_about_group:
|
||||
@@ -232,15 +250,17 @@ public class MainActivity extends AppCompatActivity {
|
||||
return false;
|
||||
}
|
||||
|
||||
drawer.closeDrawers();
|
||||
mainBinding.getRoot().closeDrawers();
|
||||
return true;
|
||||
}
|
||||
|
||||
private void changeService(final MenuItem item) {
|
||||
drawerItems.getMenu().getItem(ServiceHelper.getSelectedServiceId(this))
|
||||
drawerLayoutBinding.navigation.getMenu()
|
||||
.getItem(ServiceHelper.getSelectedServiceId(this))
|
||||
.setChecked(false);
|
||||
ServiceHelper.setSelectedServiceId(this, item.getItemId());
|
||||
drawerItems.getMenu().getItem(ServiceHelper.getSelectedServiceId(this))
|
||||
drawerLayoutBinding.navigation.getMenu()
|
||||
.getItem(ServiceHelper.getSelectedServiceId(this))
|
||||
.setChecked(true);
|
||||
}
|
||||
|
||||
@@ -262,8 +282,8 @@ public class MainActivity extends AppCompatActivity {
|
||||
NavigationHelper.openStatisticFragment(getSupportFragmentManager());
|
||||
break;
|
||||
default:
|
||||
int currentServiceId = ServiceHelper.getSelectedServiceId(this);
|
||||
StreamingService service = NewPipe.getService(currentServiceId);
|
||||
final int currentServiceId = ServiceHelper.getSelectedServiceId(this);
|
||||
final StreamingService service = NewPipe.getService(currentServiceId);
|
||||
String serviceName = "";
|
||||
|
||||
int kioskId = 0;
|
||||
@@ -292,26 +312,19 @@ public class MainActivity extends AppCompatActivity {
|
||||
}
|
||||
|
||||
private void setupDrawerHeader() {
|
||||
NavigationView navigationView = findViewById(R.id.navigation);
|
||||
View hView = navigationView.getHeaderView(0);
|
||||
|
||||
serviceArrow = hView.findViewById(R.id.drawer_arrow);
|
||||
headerServiceIcon = hView.findViewById(R.id.drawer_header_service_icon);
|
||||
headerServiceView = hView.findViewById(R.id.drawer_header_service_view);
|
||||
toggleServiceButton = hView.findViewById(R.id.drawer_header_action_button);
|
||||
toggleServiceButton.setOnClickListener(view -> toggleServices());
|
||||
drawerHeaderBinding.drawerHeaderActionButton.setOnClickListener(view -> toggleServices());
|
||||
|
||||
// If the current app name is bigger than the default "NewPipe" (7 chars),
|
||||
// let the text view grow a little more as well.
|
||||
if (getString(R.string.app_name).length() > "NewPipe".length()) {
|
||||
final TextView headerTitle = hView.findViewById(R.id.drawer_header_newpipe_title);
|
||||
final ViewGroup.LayoutParams layoutParams = headerTitle.getLayoutParams();
|
||||
final ViewGroup.LayoutParams layoutParams =
|
||||
drawerHeaderBinding.drawerHeaderNewpipeTitle.getLayoutParams();
|
||||
layoutParams.width = ViewGroup.LayoutParams.WRAP_CONTENT;
|
||||
headerTitle.setLayoutParams(layoutParams);
|
||||
headerTitle.setMaxLines(2);
|
||||
headerTitle.setMinWidth(getResources()
|
||||
drawerHeaderBinding.drawerHeaderNewpipeTitle.setLayoutParams(layoutParams);
|
||||
drawerHeaderBinding.drawerHeaderNewpipeTitle.setMaxLines(2);
|
||||
drawerHeaderBinding.drawerHeaderNewpipeTitle.setMinWidth(getResources()
|
||||
.getDimensionPixelSize(R.dimen.drawer_header_newpipe_title_default_width));
|
||||
headerTitle.setMaxWidth(getResources()
|
||||
drawerHeaderBinding.drawerHeaderNewpipeTitle.setMaxWidth(getResources()
|
||||
.getDimensionPixelSize(R.dimen.drawer_header_newpipe_title_max_width));
|
||||
}
|
||||
}
|
||||
@@ -319,29 +332,29 @@ public class MainActivity extends AppCompatActivity {
|
||||
private void toggleServices() {
|
||||
servicesShown = !servicesShown;
|
||||
|
||||
drawerItems.getMenu().removeGroup(R.id.menu_services_group);
|
||||
drawerItems.getMenu().removeGroup(R.id.menu_tabs_group);
|
||||
drawerItems.getMenu().removeGroup(R.id.menu_options_about_group);
|
||||
drawerLayoutBinding.navigation.getMenu().removeGroup(R.id.menu_services_group);
|
||||
drawerLayoutBinding.navigation.getMenu().removeGroup(R.id.menu_tabs_group);
|
||||
drawerLayoutBinding.navigation.getMenu().removeGroup(R.id.menu_options_about_group);
|
||||
|
||||
if (servicesShown) {
|
||||
showServices();
|
||||
} else {
|
||||
try {
|
||||
showTabs();
|
||||
} catch (Exception e) {
|
||||
ErrorActivity.reportUiError(this, e);
|
||||
} catch (final Exception e) {
|
||||
ErrorActivity.reportUiErrorInSnackbar(this, "Showing main page tabs", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void showServices() {
|
||||
serviceArrow.setImageResource(R.drawable.ic_arrow_drop_up_white_24dp);
|
||||
drawerHeaderBinding.drawerArrow.setImageResource(R.drawable.ic_arrow_drop_up);
|
||||
|
||||
for (StreamingService s : NewPipe.getServices()) {
|
||||
for (final StreamingService s : NewPipe.getServices()) {
|
||||
final String title = s.getServiceInfo().getName()
|
||||
+ (ServiceHelper.isBeta(s) ? " (beta)" : "");
|
||||
|
||||
MenuItem menuItem = drawerItems.getMenu()
|
||||
final MenuItem menuItem = drawerLayoutBinding.navigation.getMenu()
|
||||
.add(R.id.menu_services_group, s.getServiceId(), ORDER, title)
|
||||
.setIcon(ServiceHelper.getIcon(s.getServiceId()));
|
||||
|
||||
@@ -350,25 +363,26 @@ public class MainActivity extends AppCompatActivity {
|
||||
enhancePeertubeMenu(s, menuItem);
|
||||
}
|
||||
}
|
||||
drawerItems.getMenu().getItem(ServiceHelper.getSelectedServiceId(this))
|
||||
drawerLayoutBinding.navigation.getMenu()
|
||||
.getItem(ServiceHelper.getSelectedServiceId(this))
|
||||
.setChecked(true);
|
||||
}
|
||||
|
||||
private void enhancePeertubeMenu(final StreamingService s, final MenuItem menuItem) {
|
||||
PeertubeInstance currentInstace = PeertubeHelper.getCurrentInstance();
|
||||
menuItem.setTitle(currentInstace.getName() + (ServiceHelper.isBeta(s) ? " (beta)" : ""));
|
||||
Spinner spinner = (Spinner) LayoutInflater.from(this)
|
||||
.inflate(R.layout.instance_spinner_layout, null);
|
||||
List<PeertubeInstance> instances = PeertubeHelper.getInstanceList(this);
|
||||
List<String> items = new ArrayList<>();
|
||||
final PeertubeInstance currentInstance = PeertubeHelper.getCurrentInstance();
|
||||
menuItem.setTitle(currentInstance.getName() + (ServiceHelper.isBeta(s) ? " (beta)" : ""));
|
||||
final Spinner spinner = InstanceSpinnerLayoutBinding.inflate(LayoutInflater.from(this))
|
||||
.getRoot();
|
||||
final List<PeertubeInstance> instances = PeertubeHelper.getInstanceList(this);
|
||||
final List<String> items = new ArrayList<>();
|
||||
int defaultSelect = 0;
|
||||
for (PeertubeInstance instance : instances) {
|
||||
for (final PeertubeInstance instance : instances) {
|
||||
items.add(instance.getName());
|
||||
if (instance.getUrl().equals(currentInstace.getUrl())) {
|
||||
if (instance.getUrl().equals(currentInstance.getUrl())) {
|
||||
defaultSelect = items.size() - 1;
|
||||
}
|
||||
}
|
||||
ArrayAdapter<String> adapter = new ArrayAdapter<>(this,
|
||||
final ArrayAdapter<String> adapter = new ArrayAdapter<>(this,
|
||||
R.layout.instance_spinner_item, items);
|
||||
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
|
||||
spinner.setAdapter(adapter);
|
||||
@@ -377,13 +391,13 @@ public class MainActivity extends AppCompatActivity {
|
||||
@Override
|
||||
public void onItemSelected(final AdapterView<?> parent, final View view,
|
||||
final int position, final long id) {
|
||||
PeertubeInstance newInstance = instances.get(position);
|
||||
final PeertubeInstance newInstance = instances.get(position);
|
||||
if (newInstance.getUrl().equals(PeertubeHelper.getCurrentInstance().getUrl())) {
|
||||
return;
|
||||
}
|
||||
PeertubeHelper.selectInstance(newInstance, getApplicationContext());
|
||||
changeService(menuItem);
|
||||
drawer.closeDrawers();
|
||||
mainBinding.getRoot().closeDrawers();
|
||||
new Handler(Looper.getMainLooper()).postDelayed(() -> {
|
||||
getSupportFragmentManager().popBackStack(null,
|
||||
FragmentManager.POP_BACK_STACK_INCLUSIVE);
|
||||
@@ -400,45 +414,45 @@ public class MainActivity extends AppCompatActivity {
|
||||
}
|
||||
|
||||
private void showTabs() throws ExtractionException {
|
||||
serviceArrow.setImageResource(R.drawable.ic_arrow_drop_down_white_24dp);
|
||||
drawerHeaderBinding.drawerArrow.setImageResource(R.drawable.ic_arrow_drop_down);
|
||||
|
||||
//Tabs
|
||||
int currentServiceId = ServiceHelper.getSelectedServiceId(this);
|
||||
StreamingService service = NewPipe.getService(currentServiceId);
|
||||
final int currentServiceId = ServiceHelper.getSelectedServiceId(this);
|
||||
final StreamingService service = NewPipe.getService(currentServiceId);
|
||||
|
||||
int kioskId = 0;
|
||||
|
||||
for (final String ks : service.getKioskList().getAvailableKiosks()) {
|
||||
drawerItems.getMenu()
|
||||
drawerLayoutBinding.navigation.getMenu()
|
||||
.add(R.id.menu_tabs_group, kioskId, ORDER,
|
||||
KioskTranslator.getTranslatedKioskName(ks, this))
|
||||
.setIcon(KioskTranslator.getKioskIcon(ks, this));
|
||||
kioskId++;
|
||||
}
|
||||
|
||||
drawerItems.getMenu()
|
||||
drawerLayoutBinding.navigation.getMenu()
|
||||
.add(R.id.menu_tabs_group, ITEM_ID_SUBSCRIPTIONS, ORDER, R.string.tab_subscriptions)
|
||||
.setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.ic_channel));
|
||||
drawerItems.getMenu()
|
||||
.setIcon(R.drawable.ic_tv);
|
||||
drawerLayoutBinding.navigation.getMenu()
|
||||
.add(R.id.menu_tabs_group, ITEM_ID_FEED, ORDER, R.string.fragment_feed_title)
|
||||
.setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.ic_rss));
|
||||
drawerItems.getMenu()
|
||||
.setIcon(R.drawable.ic_rss_feed);
|
||||
drawerLayoutBinding.navigation.getMenu()
|
||||
.add(R.id.menu_tabs_group, ITEM_ID_BOOKMARKS, ORDER, R.string.tab_bookmarks)
|
||||
.setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.ic_bookmark));
|
||||
drawerItems.getMenu()
|
||||
.setIcon(R.drawable.ic_bookmark);
|
||||
drawerLayoutBinding.navigation.getMenu()
|
||||
.add(R.id.menu_tabs_group, ITEM_ID_DOWNLOADS, ORDER, R.string.downloads)
|
||||
.setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.ic_file_download));
|
||||
drawerItems.getMenu()
|
||||
.setIcon(R.drawable.ic_file_download);
|
||||
drawerLayoutBinding.navigation.getMenu()
|
||||
.add(R.id.menu_tabs_group, ITEM_ID_HISTORY, ORDER, R.string.action_history)
|
||||
.setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.ic_history));
|
||||
.setIcon(R.drawable.ic_history);
|
||||
|
||||
//Settings and About
|
||||
drawerItems.getMenu()
|
||||
drawerLayoutBinding.navigation.getMenu()
|
||||
.add(R.id.menu_options_about_group, ITEM_ID_SETTINGS, ORDER, R.string.settings)
|
||||
.setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.ic_settings));
|
||||
drawerItems.getMenu()
|
||||
.setIcon(R.drawable.ic_settings);
|
||||
drawerLayoutBinding.navigation.getMenu()
|
||||
.add(R.id.menu_options_about_group, ITEM_ID_ABOUT, ORDER, R.string.tab_about)
|
||||
.setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.ic_info_outline));
|
||||
.setIcon(R.drawable.ic_info_outline);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -447,42 +461,45 @@ public class MainActivity extends AppCompatActivity {
|
||||
if (!isChangingConfigurations()) {
|
||||
StateSaver.clearStateFiles();
|
||||
}
|
||||
if (broadcastReceiver != null) {
|
||||
unregisterReceiver(broadcastReceiver);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
assureCorrectAppLanguage(this);
|
||||
// Change the date format to match the selected language on resume
|
||||
Localization.init(getApplicationContext());
|
||||
Localization.initPrettyTime(Localization.resolvePrettyTime(getApplicationContext()));
|
||||
super.onResume();
|
||||
|
||||
// Close drawer on return, and don't show animation,
|
||||
// so it looks like the drawer isn't open when the user returns to MainActivity
|
||||
drawer.closeDrawer(GravityCompat.START, false);
|
||||
mainBinding.getRoot().closeDrawer(GravityCompat.START, false);
|
||||
try {
|
||||
final int selectedServiceId = ServiceHelper.getSelectedServiceId(this);
|
||||
final String selectedServiceName = NewPipe.getService(selectedServiceId)
|
||||
.getServiceInfo().getName();
|
||||
headerServiceView.setText(selectedServiceName);
|
||||
headerServiceIcon.setImageResource(ServiceHelper.getIcon(selectedServiceId));
|
||||
drawerHeaderBinding.drawerHeaderServiceView.setText(selectedServiceName);
|
||||
drawerHeaderBinding.drawerHeaderServiceIcon.setImageResource(ServiceHelper
|
||||
.getIcon(selectedServiceId));
|
||||
|
||||
headerServiceView.post(() -> headerServiceView.setSelected(true));
|
||||
toggleServiceButton.setContentDescription(
|
||||
drawerHeaderBinding.drawerHeaderServiceView.post(() -> drawerHeaderBinding
|
||||
.drawerHeaderServiceView.setSelected(true));
|
||||
drawerHeaderBinding.drawerHeaderActionButton.setContentDescription(
|
||||
getString(R.string.drawer_header_description) + selectedServiceName);
|
||||
} catch (Exception e) {
|
||||
ErrorActivity.reportUiError(this, e);
|
||||
} catch (final Exception e) {
|
||||
ErrorActivity.reportUiErrorInSnackbar(this, "Setting up service toggle", e);
|
||||
}
|
||||
|
||||
SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
|
||||
final 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();
|
||||
// https://stackoverflow.com/questions/10844112/
|
||||
// Briefly, let the activity resume
|
||||
// properly posting the recreate call to end of the message queue
|
||||
new Handler(Looper.getMainLooper()).post(MainActivity.this::recreate);
|
||||
ActivityCompat.recreate(this);
|
||||
}
|
||||
|
||||
if (sharedPreferences.getBoolean(Constants.KEY_MAIN_PAGE_CHANGE, false)) {
|
||||
@@ -495,7 +512,8 @@ public class MainActivity extends AppCompatActivity {
|
||||
|
||||
final boolean isHistoryEnabled = sharedPreferences.getBoolean(
|
||||
getString(R.string.enable_watch_history_key), true);
|
||||
drawerItems.getMenu().findItem(ITEM_ID_HISTORY).setVisible(isHistoryEnabled);
|
||||
drawerLayoutBinding.navigation.getMenu().findItem(ITEM_ID_HISTORY)
|
||||
.setVisible(isHistoryEnabled);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -506,7 +524,7 @@ public class MainActivity extends AppCompatActivity {
|
||||
if (intent != null) {
|
||||
// Return if launched from a launcher (e.g. Nova Launcher, Pixel Launcher ...)
|
||||
// to not destroy the already created backstack
|
||||
String action = intent.getAction();
|
||||
final String action = intent.getAction();
|
||||
if ((action != null && action.equals(Intent.ACTION_MAIN))
|
||||
&& intent.hasCategory(Intent.CATEGORY_LAUNCHER)) {
|
||||
return;
|
||||
@@ -518,25 +536,57 @@ public class MainActivity extends AppCompatActivity {
|
||||
handleIntent(intent);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onKeyDown(final int keyCode, final KeyEvent event) {
|
||||
final Fragment fragment = getSupportFragmentManager()
|
||||
.findFragmentById(R.id.fragment_player_holder);
|
||||
if (fragment instanceof OnKeyDownListener
|
||||
&& !bottomSheetHiddenOrCollapsed()) {
|
||||
// Provide keyDown event to fragment which then sends this event
|
||||
// to the main player service
|
||||
return ((OnKeyDownListener) fragment).onKeyDown(keyCode)
|
||||
|| super.onKeyDown(keyCode, event);
|
||||
}
|
||||
return super.onKeyDown(keyCode, event);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "onBackPressed() called");
|
||||
}
|
||||
|
||||
if (AndroidTvUtils.isTv(this)) {
|
||||
View drawerPanel = findViewById(R.id.navigation);
|
||||
if (drawer.isDrawerOpen(drawerPanel)) {
|
||||
drawer.closeDrawers();
|
||||
if (DeviceUtils.isTv(this)) {
|
||||
if (mainBinding.getRoot().isDrawerOpen(drawerLayoutBinding.navigation)) {
|
||||
mainBinding.getRoot().closeDrawers();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
Fragment fragment = getSupportFragmentManager().findFragmentById(R.id.fragment_holder);
|
||||
// If current fragment implements BackPressable (i.e. can/wanna handle back press)
|
||||
// delegate the back press to it
|
||||
if (fragment instanceof BackPressable) {
|
||||
if (((BackPressable) fragment).onBackPressed()) {
|
||||
// In case bottomSheet is not visible on the screen or collapsed we can assume that the user
|
||||
// interacts with a fragment inside fragment_holder so all back presses should be
|
||||
// handled by it
|
||||
if (bottomSheetHiddenOrCollapsed()) {
|
||||
final Fragment fragment = getSupportFragmentManager()
|
||||
.findFragmentById(R.id.fragment_holder);
|
||||
// If current fragment implements BackPressable (i.e. can/wanna handle back press)
|
||||
// delegate the back press to it
|
||||
if (fragment instanceof BackPressable) {
|
||||
if (((BackPressable) fragment).onBackPressed()) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
final Fragment fragmentPlayer = getSupportFragmentManager()
|
||||
.findFragmentById(R.id.fragment_player_holder);
|
||||
// If current fragment implements BackPressable (i.e. can/wanna handle back press)
|
||||
// delegate the back press to it
|
||||
if (fragmentPlayer instanceof BackPressable) {
|
||||
if (!((BackPressable) fragmentPlayer).onBackPressed()) {
|
||||
BottomSheetBehavior.from(mainBinding.fragmentPlayerHolder)
|
||||
.setState(BottomSheetBehavior.STATE_COLLAPSED);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -552,7 +602,7 @@ public class MainActivity extends AppCompatActivity {
|
||||
public void onRequestPermissionsResult(final int requestCode,
|
||||
@NonNull final String[] permissions,
|
||||
@NonNull final int[] grantResults) {
|
||||
for (int i : grantResults) {
|
||||
for (final int i : grantResults) {
|
||||
if (i == PackageManager.PERMISSION_DENIED) {
|
||||
return;
|
||||
}
|
||||
@@ -562,8 +612,8 @@ public class MainActivity extends AppCompatActivity {
|
||||
NavigationHelper.openDownloads(this);
|
||||
break;
|
||||
case PermissionHelper.DOWNLOAD_DIALOG_REQUEST_CODE:
|
||||
Fragment fragment = getSupportFragmentManager()
|
||||
.findFragmentById(R.id.fragment_holder);
|
||||
final Fragment fragment = getSupportFragmentManager()
|
||||
.findFragmentById(R.id.fragment_player_holder);
|
||||
if (fragment instanceof VideoDetailFragment) {
|
||||
((VideoDetailFragment) fragment).openDownloadDialog();
|
||||
}
|
||||
@@ -614,17 +664,13 @@ public class MainActivity extends AppCompatActivity {
|
||||
}
|
||||
super.onCreateOptionsMenu(menu);
|
||||
|
||||
Fragment fragment = getSupportFragmentManager().findFragmentById(R.id.fragment_holder);
|
||||
if (!(fragment instanceof VideoDetailFragment)) {
|
||||
findViewById(R.id.toolbar).findViewById(R.id.toolbar_spinner).setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
final Fragment fragment
|
||||
= getSupportFragmentManager().findFragmentById(R.id.fragment_holder);
|
||||
if (!(fragment instanceof SearchFragment)) {
|
||||
findViewById(R.id.toolbar).findViewById(R.id.toolbar_search_container)
|
||||
.setVisibility(View.GONE);
|
||||
toolbarLayoutBinding.toolbarSearchContainer.getRoot().setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
ActionBar actionBar = getSupportActionBar();
|
||||
final ActionBar actionBar = getSupportActionBar();
|
||||
if (actionBar != null) {
|
||||
actionBar.setDisplayHomeAsUpEnabled(false);
|
||||
}
|
||||
@@ -635,19 +681,16 @@ public class MainActivity extends AppCompatActivity {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(final MenuItem item) {
|
||||
public boolean onOptionsItemSelected(@NonNull final MenuItem item) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "onOptionsItemSelected() called with: item = [" + item + "]");
|
||||
}
|
||||
int id = item.getItemId();
|
||||
|
||||
switch (id) {
|
||||
case android.R.id.home:
|
||||
onHomeButtonPressed();
|
||||
return true;
|
||||
default:
|
||||
return super.onOptionsItemSelected(item);
|
||||
if (item.getItemId() == android.R.id.home) {
|
||||
onHomeButtonPressed();
|
||||
return true;
|
||||
}
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
@@ -660,6 +703,13 @@ public class MainActivity extends AppCompatActivity {
|
||||
}
|
||||
StateSaver.clearStateFiles();
|
||||
if (getIntent() != null && getIntent().hasExtra(Constants.KEY_LINK_TYPE)) {
|
||||
// When user watch a video inside popup and then tries to open the video in main player
|
||||
// while the app is closed he will see a blank fragment on place of kiosk.
|
||||
// Let's open it first
|
||||
if (getSupportFragmentManager().getBackStackEntryCount() == 0) {
|
||||
NavigationHelper.openMainFragment(getSupportFragmentManager());
|
||||
}
|
||||
|
||||
handleIntent(getIntent());
|
||||
} else {
|
||||
NavigationHelper.gotoMainFragment(getSupportFragmentManager());
|
||||
@@ -675,21 +725,20 @@ public class MainActivity extends AppCompatActivity {
|
||||
return;
|
||||
}
|
||||
|
||||
final Toolbar toolbar = findViewById(R.id.toolbar);
|
||||
|
||||
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);
|
||||
toolbarLayoutBinding.toolbar.setNavigationOnClickListener(v -> mainBinding.getRoot()
|
||||
.openDrawer(GravityCompat.START));
|
||||
mainBinding.getRoot().setDrawerLockMode(DrawerLayout.LOCK_MODE_UNDEFINED);
|
||||
}
|
||||
} else {
|
||||
drawer.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED);
|
||||
mainBinding.getRoot().setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED);
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
toolbar.setNavigationOnClickListener(v -> onHomeButtonPressed());
|
||||
toolbarLayoutBinding.toolbar.setNavigationOnClickListener(v -> onHomeButtonPressed());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -700,28 +749,38 @@ public class MainActivity extends AppCompatActivity {
|
||||
}
|
||||
|
||||
if (intent.hasExtra(Constants.KEY_LINK_TYPE)) {
|
||||
String url = intent.getStringExtra(Constants.KEY_URL);
|
||||
int serviceId = intent.getIntExtra(Constants.KEY_SERVICE_ID, 0);
|
||||
final String url = intent.getStringExtra(Constants.KEY_URL);
|
||||
final int serviceId = intent.getIntExtra(Constants.KEY_SERVICE_ID, 0);
|
||||
String title = intent.getStringExtra(Constants.KEY_TITLE);
|
||||
switch (((StreamingService.LinkType) intent
|
||||
.getSerializableExtra(Constants.KEY_LINK_TYPE))) {
|
||||
if (title == null) {
|
||||
title = "";
|
||||
}
|
||||
|
||||
final StreamingService.LinkType linkType = ((StreamingService.LinkType) intent
|
||||
.getSerializableExtra(Constants.KEY_LINK_TYPE));
|
||||
assert linkType != null;
|
||||
switch (linkType) {
|
||||
case STREAM:
|
||||
boolean autoPlay = intent
|
||||
.getBooleanExtra(VideoDetailFragment.AUTO_PLAY, false);
|
||||
NavigationHelper.openVideoDetailFragment(getSupportFragmentManager(),
|
||||
serviceId, url, title, autoPlay);
|
||||
final String intentCacheKey = intent.getStringExtra(
|
||||
Player.PLAY_QUEUE_KEY);
|
||||
final PlayQueue playQueue = intentCacheKey != null
|
||||
? SerializedCache.getInstance()
|
||||
.take(intentCacheKey, PlayQueue.class)
|
||||
: null;
|
||||
|
||||
final boolean switchingPlayers = intent.getBooleanExtra(
|
||||
VideoDetailFragment.KEY_SWITCHING_PLAYERS, false);
|
||||
NavigationHelper.openVideoDetailFragment(
|
||||
getApplicationContext(), getSupportFragmentManager(),
|
||||
serviceId, url, title, playQueue, switchingPlayers);
|
||||
break;
|
||||
case CHANNEL:
|
||||
NavigationHelper.openChannelFragment(getSupportFragmentManager(),
|
||||
serviceId,
|
||||
url,
|
||||
title);
|
||||
serviceId, url, title);
|
||||
break;
|
||||
case PLAYLIST:
|
||||
NavigationHelper.openPlaylistFragment(getSupportFragmentManager(),
|
||||
serviceId,
|
||||
url,
|
||||
title);
|
||||
serviceId, url, title);
|
||||
break;
|
||||
}
|
||||
} else if (intent.hasExtra(Constants.KEY_OPEN_SEARCH)) {
|
||||
@@ -729,7 +788,7 @@ public class MainActivity extends AppCompatActivity {
|
||||
if (searchString == null) {
|
||||
searchString = "";
|
||||
}
|
||||
int serviceId = intent.getIntExtra(Constants.KEY_SERVICE_ID, 0);
|
||||
final int serviceId = intent.getIntExtra(Constants.KEY_SERVICE_ID, 0);
|
||||
NavigationHelper.openSearchFragment(
|
||||
getSupportFragmentManager(),
|
||||
serviceId,
|
||||
@@ -738,8 +797,60 @@ public class MainActivity extends AppCompatActivity {
|
||||
} else {
|
||||
NavigationHelper.gotoMainFragment(getSupportFragmentManager());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
ErrorActivity.reportUiError(this, e);
|
||||
} catch (final Exception e) {
|
||||
ErrorActivity.reportUiErrorInSnackbar(this, "Handling intent", e);
|
||||
}
|
||||
}
|
||||
|
||||
private void openMiniPlayerIfMissing() {
|
||||
final Fragment fragmentPlayer = getSupportFragmentManager()
|
||||
.findFragmentById(R.id.fragment_player_holder);
|
||||
if (fragmentPlayer == null) {
|
||||
// We still don't have a fragment attached to the activity. It can happen when a user
|
||||
// started popup or background players without opening a stream inside the fragment.
|
||||
// Adding it in a collapsed state (only mini player will be visible).
|
||||
NavigationHelper.showMiniPlayer(getSupportFragmentManager());
|
||||
}
|
||||
}
|
||||
|
||||
private void openMiniPlayerUponPlayerStarted() {
|
||||
if (getIntent().getSerializableExtra(Constants.KEY_LINK_TYPE)
|
||||
== StreamingService.LinkType.STREAM) {
|
||||
// handleIntent() already takes care of opening video detail fragment
|
||||
// due to an intent containing a STREAM link
|
||||
return;
|
||||
}
|
||||
|
||||
if (PlayerHolder.isPlayerOpen()) {
|
||||
// if the player is already open, no need for a broadcast receiver
|
||||
openMiniPlayerIfMissing();
|
||||
} else {
|
||||
// listen for player start intent being sent around
|
||||
broadcastReceiver = new BroadcastReceiver() {
|
||||
@Override
|
||||
public void onReceive(final Context context, final Intent intent) {
|
||||
if (Objects.equals(intent.getAction(),
|
||||
VideoDetailFragment.ACTION_PLAYER_STARTED)) {
|
||||
openMiniPlayerIfMissing();
|
||||
// At this point the player is added 100%, we can unregister. Other actions
|
||||
// are useless since the fragment will not be removed after that.
|
||||
unregisterReceiver(broadcastReceiver);
|
||||
broadcastReceiver = null;
|
||||
}
|
||||
}
|
||||
};
|
||||
final IntentFilter intentFilter = new IntentFilter();
|
||||
intentFilter.addAction(VideoDetailFragment.ACTION_PLAYER_STARTED);
|
||||
registerReceiver(broadcastReceiver, intentFilter);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean bottomSheetHiddenOrCollapsed() {
|
||||
final BottomSheetBehavior<FrameLayout> bottomSheetBehavior =
|
||||
BottomSheetBehavior.from(mainBinding.fragmentPlayerHolder);
|
||||
|
||||
final int sheetState = bottomSheetBehavior.getState();
|
||||
return sheetState == BottomSheetBehavior.STATE_HIDDEN
|
||||
|| sheetState == BottomSheetBehavior.STATE_COLLAPSED;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@ public final class NewPipeDatabase {
|
||||
if (databaseInstance == null) {
|
||||
throw new IllegalStateException("database is not initialized");
|
||||
}
|
||||
Cursor c = databaseInstance.query("pragma wal_checkpoint(full)", null);
|
||||
final Cursor c = databaseInstance.query("pragma wal_checkpoint(full)", null);
|
||||
if (c.moveToFirst() && c.getInt(0) == 1) {
|
||||
throw new RuntimeException("Checkpoint was blocked from completing");
|
||||
}
|
||||
|
||||
28
app/src/main/java/org/schabi/newpipe/NewVersionManager.kt
Normal file
28
app/src/main/java/org/schabi/newpipe/NewVersionManager.kt
Normal file
@@ -0,0 +1,28 @@
|
||||
package org.schabi.newpipe
|
||||
|
||||
import java.time.Instant
|
||||
import java.time.ZonedDateTime
|
||||
import java.time.format.DateTimeFormatter
|
||||
|
||||
class NewVersionManager {
|
||||
|
||||
fun isExpired(expiry: Long): Boolean {
|
||||
return Instant.ofEpochSecond(expiry).isBefore(Instant.now())
|
||||
}
|
||||
|
||||
/**
|
||||
* Coerce expiry date time in between 6 hours and 72 hours from now
|
||||
*
|
||||
* @return Epoch second of expiry date time
|
||||
*/
|
||||
fun coerceExpiry(expiryString: String?): Long {
|
||||
val now = ZonedDateTime.now()
|
||||
return expiryString?.let {
|
||||
|
||||
var expiry = ZonedDateTime.from(DateTimeFormatter.RFC_1123_DATE_TIME.parse(expiryString))
|
||||
expiry = maxOf(expiry, now.plusHours(6))
|
||||
expiry = minOf(expiry, now.plusHours(72))
|
||||
expiry.toEpochSecond()
|
||||
} ?: now.plusHours(6).toEpochSecond()
|
||||
}
|
||||
}
|
||||
@@ -31,7 +31,7 @@ public class PanicResponderActivity extends Activity {
|
||||
@Override
|
||||
protected void onCreate(final Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
Intent intent = getIntent();
|
||||
final Intent intent = getIntent();
|
||||
if (intent != null && PANIC_TRIGGER_ACTION.equals(intent.getAction())) {
|
||||
// TODO: Explicitly clear the search results
|
||||
// once they are restored when the app restarts
|
||||
@@ -40,7 +40,7 @@ public class PanicResponderActivity extends Activity {
|
||||
ExitActivity.exitAndRemoveFromRecentApps(this);
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= 21) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
finishAndRemoveTask();
|
||||
} else {
|
||||
finish();
|
||||
|
||||
@@ -8,14 +8,12 @@ import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.os.Bundle;
|
||||
import android.preference.PreferenceManager;
|
||||
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;
|
||||
@@ -27,29 +25,51 @@ import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.appcompat.content.res.AppCompatResources;
|
||||
import androidx.core.app.NotificationCompat;
|
||||
import androidx.core.app.ServiceCompat;
|
||||
import androidx.core.widget.TextViewCompat;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
import androidx.preference.PreferenceManager;
|
||||
|
||||
import org.schabi.newpipe.databinding.ListRadioIconItemBinding;
|
||||
import org.schabi.newpipe.databinding.SingleChoiceDialogViewBinding;
|
||||
import org.schabi.newpipe.download.DownloadDialog;
|
||||
import org.schabi.newpipe.error.ErrorActivity;
|
||||
import org.schabi.newpipe.error.ErrorInfo;
|
||||
import org.schabi.newpipe.error.ReCaptchaActivity;
|
||||
import org.schabi.newpipe.error.UserAction;
|
||||
import org.schabi.newpipe.extractor.Info;
|
||||
import org.schabi.newpipe.extractor.NewPipe;
|
||||
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.AgeRestrictedContentException;
|
||||
import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException;
|
||||
import org.schabi.newpipe.extractor.exceptions.ContentNotSupportedException;
|
||||
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||
import org.schabi.newpipe.extractor.exceptions.GeographicRestrictionException;
|
||||
import org.schabi.newpipe.extractor.exceptions.PaidContentException;
|
||||
import org.schabi.newpipe.extractor.exceptions.PrivateContentException;
|
||||
import org.schabi.newpipe.extractor.exceptions.ReCaptchaException;
|
||||
import org.schabi.newpipe.extractor.exceptions.SoundCloudGoPlusContentException;
|
||||
import org.schabi.newpipe.extractor.exceptions.YoutubeMusicPremiumContentException;
|
||||
import org.schabi.newpipe.extractor.playlist.PlaylistInfo;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||
import org.schabi.newpipe.extractor.stream.VideoStream;
|
||||
import org.schabi.newpipe.ktx.ExceptionUtils;
|
||||
import org.schabi.newpipe.player.MainPlayer;
|
||||
import org.schabi.newpipe.player.helper.PlayerHelper;
|
||||
import org.schabi.newpipe.player.helper.PlayerHolder;
|
||||
import org.schabi.newpipe.player.playqueue.ChannelPlayQueue;
|
||||
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
||||
import org.schabi.newpipe.player.playqueue.PlaylistPlayQueue;
|
||||
import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
|
||||
import org.schabi.newpipe.report.UserAction;
|
||||
import org.schabi.newpipe.util.AndroidTvUtils;
|
||||
import org.schabi.newpipe.util.Constants;
|
||||
import org.schabi.newpipe.util.DeviceUtils;
|
||||
import org.schabi.newpipe.util.ExtractorHelper;
|
||||
import org.schabi.newpipe.util.ListHelper;
|
||||
import org.schabi.newpipe.util.NavigationHelper;
|
||||
import org.schabi.newpipe.util.PermissionHelper;
|
||||
import org.schabi.newpipe.util.ShareUtils;
|
||||
import org.schabi.newpipe.util.ThemeHelper;
|
||||
import org.schabi.newpipe.util.urlfinder.UrlFinder;
|
||||
import org.schabi.newpipe.views.FocusOverlayView;
|
||||
@@ -57,35 +77,25 @@ import org.schabi.newpipe.views.FocusOverlayView;
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
|
||||
import icepick.Icepick;
|
||||
import icepick.State;
|
||||
import io.reactivex.Observable;
|
||||
import io.reactivex.Single;
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.disposables.CompositeDisposable;
|
||||
import io.reactivex.disposables.Disposable;
|
||||
import io.reactivex.functions.Consumer;
|
||||
import io.reactivex.schedulers.Schedulers;
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.rxjava3.core.Observable;
|
||||
import io.reactivex.rxjava3.core.Single;
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
||||
import io.reactivex.rxjava3.disposables.Disposable;
|
||||
import io.reactivex.rxjava3.functions.Consumer;
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers;
|
||||
|
||||
import static org.schabi.newpipe.extractor.StreamingService.ServiceInfo.MediaCapability.AUDIO;
|
||||
import static org.schabi.newpipe.extractor.StreamingService.ServiceInfo.MediaCapability.VIDEO;
|
||||
import static org.schabi.newpipe.util.ThemeHelper.resolveResourceIdFromAttr;
|
||||
|
||||
/**
|
||||
* Get the url from the intent and open it in the chosen preferred player.
|
||||
*/
|
||||
public class RouterActivity extends AppCompatActivity {
|
||||
public static final String INTERNAL_ROUTE_KEY = "internalRoute";
|
||||
/**
|
||||
* Removes invisible separators (\p{Z}) and punctuation characters including
|
||||
* brackets (\p{P}). See http://www.regular-expressions.info/unicode.html for
|
||||
* more details.
|
||||
*/
|
||||
private static final String REGEX_REMOVE_FROM_URL = "[\\p{Z}\\p{P}]";
|
||||
protected final CompositeDisposable disposables = new CompositeDisposable();
|
||||
@State
|
||||
protected int currentServiceId = -1;
|
||||
@@ -95,7 +105,6 @@ public class RouterActivity extends AppCompatActivity {
|
||||
protected int selectedRadioPosition = -1;
|
||||
protected int selectedPreviously = -1;
|
||||
protected String currentUrl;
|
||||
protected boolean internalRoute = false;
|
||||
private StreamingService currentService;
|
||||
private boolean selectionIsDownload = false;
|
||||
|
||||
@@ -113,14 +122,12 @@ public class RouterActivity extends AppCompatActivity {
|
||||
}
|
||||
}
|
||||
|
||||
internalRoute = getIntent().getBooleanExtra(INTERNAL_ROUTE_KEY, false);
|
||||
|
||||
setTheme(ThemeHelper.isLightThemeSelected(this)
|
||||
? R.style.RouterActivityThemeLight : R.style.RouterActivityThemeDark);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onSaveInstanceState(final Bundle outState) {
|
||||
protected void onSaveInstanceState(@NonNull final Bundle outState) {
|
||||
super.onSaveInstanceState(outState);
|
||||
Icepick.saveInstanceState(this, outState);
|
||||
}
|
||||
@@ -142,44 +149,95 @@ public class RouterActivity extends AppCompatActivity {
|
||||
private void handleUrl(final String url) {
|
||||
disposables.add(Observable
|
||||
.fromCallable(() -> {
|
||||
if (currentServiceId == -1) {
|
||||
currentService = NewPipe.getServiceByUrl(url);
|
||||
currentServiceId = currentService.getServiceId();
|
||||
currentLinkType = currentService.getLinkTypeByUrl(url);
|
||||
currentUrl = url;
|
||||
} else {
|
||||
currentService = NewPipe.getService(currentServiceId);
|
||||
}
|
||||
try {
|
||||
if (currentServiceId == -1) {
|
||||
currentService = NewPipe.getServiceByUrl(url);
|
||||
currentServiceId = currentService.getServiceId();
|
||||
currentLinkType = currentService.getLinkTypeByUrl(url);
|
||||
currentUrl = url;
|
||||
} else {
|
||||
currentService = NewPipe.getService(currentServiceId);
|
||||
}
|
||||
|
||||
return currentLinkType != LinkType.NONE;
|
||||
// return whether the url was found to be supported or not
|
||||
return currentLinkType != LinkType.NONE;
|
||||
} catch (final ExtractionException e) {
|
||||
// this can be reached only when the url is completely unsupported
|
||||
return false;
|
||||
}
|
||||
})
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(result -> {
|
||||
if (result) {
|
||||
.subscribe(isUrlSupported -> {
|
||||
if (isUrlSupported) {
|
||||
onSuccess();
|
||||
} else {
|
||||
onError();
|
||||
showUnsupportedUrlDialog(url);
|
||||
}
|
||||
}, this::handleError));
|
||||
}, throwable -> handleError(this, new ErrorInfo(throwable,
|
||||
UserAction.SHARE_TO_NEWPIPE, "Getting service from url: " + url))));
|
||||
}
|
||||
|
||||
private void handleError(final 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);
|
||||
/**
|
||||
* @param context the context. It will be {@code finish()}ed at the end of the handling if it is
|
||||
* an instance of {@link RouterActivity}.
|
||||
* @param errorInfo the error information
|
||||
*/
|
||||
private static void handleError(final Context context, final ErrorInfo errorInfo) {
|
||||
if (errorInfo.getThrowable() != null) {
|
||||
errorInfo.getThrowable().printStackTrace();
|
||||
}
|
||||
|
||||
finish();
|
||||
if (errorInfo.getThrowable() instanceof ReCaptchaException) {
|
||||
Toast.makeText(context, R.string.recaptcha_request_toast, Toast.LENGTH_LONG).show();
|
||||
// Starting ReCaptcha Challenge Activity
|
||||
final Intent intent = new Intent(context, ReCaptchaActivity.class);
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
context.startActivity(intent);
|
||||
} else if (errorInfo.getThrowable() != null
|
||||
&& ExceptionUtils.isNetworkRelated(errorInfo.getThrowable())) {
|
||||
Toast.makeText(context, R.string.network_error, Toast.LENGTH_LONG).show();
|
||||
} else if (errorInfo.getThrowable() instanceof AgeRestrictedContentException) {
|
||||
Toast.makeText(context, R.string.restricted_video_no_stream,
|
||||
Toast.LENGTH_LONG).show();
|
||||
} else if (errorInfo.getThrowable() instanceof GeographicRestrictionException) {
|
||||
Toast.makeText(context, R.string.georestricted_content, Toast.LENGTH_LONG).show();
|
||||
} else if (errorInfo.getThrowable() instanceof PaidContentException) {
|
||||
Toast.makeText(context, R.string.paid_content, Toast.LENGTH_LONG).show();
|
||||
} else if (errorInfo.getThrowable() instanceof PrivateContentException) {
|
||||
Toast.makeText(context, R.string.private_content, Toast.LENGTH_LONG).show();
|
||||
} else if (errorInfo.getThrowable() instanceof SoundCloudGoPlusContentException) {
|
||||
Toast.makeText(context, R.string.soundcloud_go_plus_content,
|
||||
Toast.LENGTH_LONG).show();
|
||||
} else if (errorInfo.getThrowable() instanceof YoutubeMusicPremiumContentException) {
|
||||
Toast.makeText(context, R.string.youtube_music_premium_content,
|
||||
Toast.LENGTH_LONG).show();
|
||||
} else if (errorInfo.getThrowable() instanceof ContentNotAvailableException) {
|
||||
Toast.makeText(context, R.string.content_not_available, Toast.LENGTH_LONG).show();
|
||||
} else if (errorInfo.getThrowable() instanceof ContentNotSupportedException) {
|
||||
Toast.makeText(context, R.string.content_not_supported, Toast.LENGTH_LONG).show();
|
||||
} else {
|
||||
ErrorActivity.reportError(context, errorInfo);
|
||||
}
|
||||
|
||||
if (context instanceof RouterActivity) {
|
||||
((RouterActivity) context).finish();
|
||||
}
|
||||
}
|
||||
|
||||
private void onError() {
|
||||
Toast.makeText(this, R.string.url_not_supported_toast, Toast.LENGTH_LONG).show();
|
||||
finish();
|
||||
private void showUnsupportedUrlDialog(final String url) {
|
||||
final Context context = getThemeWrapperContext();
|
||||
new AlertDialog.Builder(context)
|
||||
.setTitle(R.string.unsupported_url)
|
||||
.setMessage(R.string.unsupported_url_dialog_message)
|
||||
.setIcon(R.drawable.ic_share)
|
||||
.setPositiveButton(R.string.open_in_browser,
|
||||
(dialog, which) -> ShareUtils.openUrlInBrowser(this, url))
|
||||
.setNegativeButton(R.string.share,
|
||||
(dialog, which) -> ShareUtils.shareText(this, "", url)) // no subject
|
||||
.setNeutralButton(R.string.cancel, null)
|
||||
.setOnDismissListener(dialog -> finish())
|
||||
.show();
|
||||
}
|
||||
|
||||
protected void onSuccess() {
|
||||
@@ -257,9 +315,8 @@ public class RouterActivity extends AppCompatActivity {
|
||||
final Context themeWrapperContext = getThemeWrapperContext();
|
||||
|
||||
final LayoutInflater inflater = LayoutInflater.from(themeWrapperContext);
|
||||
final LinearLayout rootLayout = (LinearLayout) inflater.inflate(
|
||||
R.layout.preferred_player_dialog_view, null, false);
|
||||
final RadioGroup radioGroup = rootLayout.findViewById(android.R.id.list);
|
||||
final RadioGroup radioGroup = SingleChoiceDialogViewBinding.inflate(getLayoutInflater())
|
||||
.list;
|
||||
|
||||
final DialogInterface.OnClickListener dialogButtonsClickListener = (dialog, which) -> {
|
||||
final int indexOfChild = radioGroup.indexOfChild(
|
||||
@@ -268,6 +325,7 @@ public class RouterActivity extends AppCompatActivity {
|
||||
|
||||
handleChoice(choice.key);
|
||||
|
||||
// open future streams always like this one, because "always" button was used by user
|
||||
if (which == DialogInterface.BUTTON_POSITIVE) {
|
||||
preferences.edit()
|
||||
.putString(getString(R.string.preferred_open_action_key), choice.key)
|
||||
@@ -310,12 +368,11 @@ public class RouterActivity extends AppCompatActivity {
|
||||
};
|
||||
|
||||
int id = 12345;
|
||||
for (AdapterChoiceItem item : choices) {
|
||||
final RadioButton radioButton
|
||||
= (RadioButton) inflater.inflate(R.layout.list_radio_icon_item, null);
|
||||
for (final AdapterChoiceItem item : choices) {
|
||||
final RadioButton radioButton = ListRadioIconItemBinding.inflate(inflater).getRoot();
|
||||
radioButton.setText(item.description);
|
||||
radioButton.setCompoundDrawablesWithIntrinsicBounds(
|
||||
AppCompatResources.getDrawable(getApplicationContext(), item.icon),
|
||||
TextViewCompat.setCompoundDrawablesRelativeWithIntrinsicBounds(radioButton,
|
||||
AppCompatResources.getDrawable(themeWrapperContext, item.icon),
|
||||
null, null, null);
|
||||
radioButton.setChecked(false);
|
||||
radioButton.setId(id++);
|
||||
@@ -330,7 +387,7 @@ public class RouterActivity extends AppCompatActivity {
|
||||
getString(R.string.preferred_open_action_last_selected_key), null);
|
||||
if (!TextUtils.isEmpty(lastSelectedPlayer)) {
|
||||
for (int i = 0; i < choices.size(); i++) {
|
||||
AdapterChoiceItem c = choices.get(i);
|
||||
final AdapterChoiceItem c = choices.get(i);
|
||||
if (lastSelectedPlayer.equals(c.key)) {
|
||||
selectedRadioPosition = i;
|
||||
break;
|
||||
@@ -347,7 +404,7 @@ public class RouterActivity extends AppCompatActivity {
|
||||
|
||||
alertDialog.show();
|
||||
|
||||
if (AndroidTvUtils.isTv(this)) {
|
||||
if (DeviceUtils.isTv(this)) {
|
||||
FocusOverlayView.setupFocusObserver(alertDialog);
|
||||
}
|
||||
}
|
||||
@@ -362,33 +419,68 @@ public class RouterActivity extends AppCompatActivity {
|
||||
|
||||
final SharedPreferences preferences = PreferenceManager
|
||||
.getDefaultSharedPreferences(this);
|
||||
boolean isExtVideoEnabled = preferences.getBoolean(
|
||||
final boolean isExtVideoEnabled = preferences.getBoolean(
|
||||
getString(R.string.use_external_video_player_key), false);
|
||||
boolean isExtAudioEnabled = preferences.getBoolean(
|
||||
final boolean isExtAudioEnabled = preferences.getBoolean(
|
||||
getString(R.string.use_external_audio_player_key), false);
|
||||
|
||||
returnList.add(new AdapterChoiceItem(getString(R.string.show_info_key),
|
||||
getString(R.string.show_info),
|
||||
resolveResourceIdFromAttr(context, R.attr.ic_info_outline)));
|
||||
final AdapterChoiceItem videoPlayer = new AdapterChoiceItem(
|
||||
getString(R.string.video_player_key), getString(R.string.video_player),
|
||||
R.drawable.ic_play_arrow);
|
||||
final AdapterChoiceItem showInfo = new AdapterChoiceItem(
|
||||
getString(R.string.show_info_key), getString(R.string.show_info),
|
||||
R.drawable.ic_info_outline);
|
||||
final AdapterChoiceItem popupPlayer = new AdapterChoiceItem(
|
||||
getString(R.string.popup_player_key), getString(R.string.popup_player),
|
||||
R.drawable.ic_picture_in_picture);
|
||||
final AdapterChoiceItem backgroundPlayer = new AdapterChoiceItem(
|
||||
getString(R.string.background_player_key), getString(R.string.background_player),
|
||||
R.drawable.ic_headset);
|
||||
|
||||
if (capabilities.contains(VIDEO) && !(isExtVideoEnabled && linkType != LinkType.STREAM)) {
|
||||
returnList.add(new AdapterChoiceItem(getString(R.string.video_player_key),
|
||||
getString(R.string.video_player),
|
||||
resolveResourceIdFromAttr(context, R.attr.ic_play_arrow)));
|
||||
returnList.add(new AdapterChoiceItem(getString(R.string.popup_player_key),
|
||||
getString(R.string.popup_player),
|
||||
resolveResourceIdFromAttr(context, R.attr.ic_popup)));
|
||||
}
|
||||
if (linkType == LinkType.STREAM) {
|
||||
if (isExtVideoEnabled) {
|
||||
// show both "show info" and "video player", they are two different activities
|
||||
returnList.add(showInfo);
|
||||
returnList.add(videoPlayer);
|
||||
} else {
|
||||
final MainPlayer.PlayerType playerType = PlayerHolder.getType();
|
||||
if (capabilities.contains(VIDEO)
|
||||
&& PlayerHelper.isAutoplayAllowedByUser(context)
|
||||
&& playerType == null || playerType == MainPlayer.PlayerType.VIDEO) {
|
||||
// show only "video player" since the details activity will be opened and the
|
||||
// video will be auto played there. Since "show info" would do the exact same
|
||||
// thing, use that as a key to let VideoDetailFragment load the stream instead
|
||||
// of using FetcherService (see comment in handleChoice())
|
||||
returnList.add(new AdapterChoiceItem(
|
||||
showInfo.key, videoPlayer.description, videoPlayer.icon));
|
||||
} else {
|
||||
// show only "show info" if video player is not applicable, auto play is
|
||||
// disabled or a video is playing in a player different than the main one
|
||||
returnList.add(showInfo);
|
||||
}
|
||||
}
|
||||
|
||||
if (capabilities.contains(AUDIO) && !(isExtAudioEnabled && linkType != LinkType.STREAM)) {
|
||||
returnList.add(new AdapterChoiceItem(getString(R.string.background_player_key),
|
||||
getString(R.string.background_player),
|
||||
resolveResourceIdFromAttr(context, R.attr.ic_headset)));
|
||||
if (capabilities.contains(VIDEO)) {
|
||||
returnList.add(popupPlayer);
|
||||
}
|
||||
if (capabilities.contains(AUDIO)) {
|
||||
returnList.add(backgroundPlayer);
|
||||
}
|
||||
|
||||
} else {
|
||||
returnList.add(showInfo);
|
||||
if (capabilities.contains(VIDEO) && !isExtVideoEnabled) {
|
||||
returnList.add(videoPlayer);
|
||||
returnList.add(popupPlayer);
|
||||
}
|
||||
if (capabilities.contains(AUDIO) && !isExtAudioEnabled) {
|
||||
returnList.add(backgroundPlayer);
|
||||
}
|
||||
}
|
||||
|
||||
returnList.add(new AdapterChoiceItem(getString(R.string.download_key),
|
||||
getString(R.string.download),
|
||||
resolveResourceIdFromAttr(context, R.attr.ic_file_download)));
|
||||
R.drawable.ic_file_download));
|
||||
|
||||
return returnList;
|
||||
}
|
||||
@@ -410,9 +502,9 @@ public class RouterActivity extends AppCompatActivity {
|
||||
}
|
||||
|
||||
private void handleText() {
|
||||
String searchString = getIntent().getStringExtra(Intent.EXTRA_TEXT);
|
||||
int serviceId = getIntent().getIntExtra(Constants.KEY_SERVICE_ID, 0);
|
||||
Intent intent = new Intent(getThemeWrapperContext(), MainActivity.class);
|
||||
final String searchString = getIntent().getStringExtra(Intent.EXTRA_TEXT);
|
||||
final int serviceId = getIntent().getIntExtra(Constants.KEY_SERVICE_ID, 0);
|
||||
final Intent intent = new Intent(getThemeWrapperContext(), MainActivity.class);
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
startActivity(intent);
|
||||
NavigationHelper.openSearch(getThemeWrapperContext(), serviceId, searchString);
|
||||
@@ -452,14 +544,10 @@ public class RouterActivity extends AppCompatActivity {
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(intent -> {
|
||||
if (!internalRoute) {
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK);
|
||||
}
|
||||
startActivity(intent);
|
||||
|
||||
finish();
|
||||
}, this::handleError)
|
||||
}, throwable -> handleError(this, new ErrorInfo(throwable,
|
||||
UserAction.SHARE_TO_NEWPIPE, "Starting info activity: " + currentUrl)))
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -475,36 +563,33 @@ public class RouterActivity extends AppCompatActivity {
|
||||
|
||||
@SuppressLint("CheckResult")
|
||||
private void openDownloadDialog() {
|
||||
ExtractorHelper.getStreamInfo(currentServiceId, currentUrl, true)
|
||||
disposables.add(ExtractorHelper.getStreamInfo(currentServiceId, currentUrl, true)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe((@NonNull StreamInfo result) -> {
|
||||
List<VideoStream> sortedVideoStreams = ListHelper
|
||||
.subscribe(result -> {
|
||||
final List<VideoStream> sortedVideoStreams = ListHelper
|
||||
.getSortedStreamVideosList(this, result.getVideoStreams(),
|
||||
result.getVideoOnlyStreams(), false);
|
||||
int selectedVideoStreamIndex = ListHelper
|
||||
final int selectedVideoStreamIndex = ListHelper
|
||||
.getDefaultResolutionIndex(this, sortedVideoStreams);
|
||||
|
||||
FragmentManager fm = getSupportFragmentManager();
|
||||
DownloadDialog downloadDialog = DownloadDialog.newInstance(result);
|
||||
final FragmentManager fm = getSupportFragmentManager();
|
||||
final DownloadDialog downloadDialog = DownloadDialog.newInstance(result);
|
||||
downloadDialog.setVideoStreams(sortedVideoStreams);
|
||||
downloadDialog.setAudioStreams(result.getAudioStreams());
|
||||
downloadDialog.setSelectedVideoStream(selectedVideoStreamIndex);
|
||||
downloadDialog.show(fm, "downloadDialog");
|
||||
fm.executePendingTransactions();
|
||||
downloadDialog.getDialog().setOnDismissListener(dialog -> {
|
||||
finish();
|
||||
});
|
||||
}, (@NonNull Throwable throwable) -> {
|
||||
onError();
|
||||
});
|
||||
downloadDialog.requireDialog().setOnDismissListener(dialog -> finish());
|
||||
}, throwable ->
|
||||
showUnsupportedUrlDialog(currentUrl)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRequestPermissionsResult(final int requestCode,
|
||||
@NonNull final String[] permissions,
|
||||
@NonNull final int[] grantResults) {
|
||||
for (int i : grantResults) {
|
||||
for (final int i : grantResults) {
|
||||
if (i == PackageManager.PERMISSION_DENIED) {
|
||||
finish();
|
||||
return;
|
||||
@@ -515,66 +600,6 @@ public class RouterActivity extends AppCompatActivity {
|
||||
}
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Service Fetcher
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
private String removeHeadingGibberish(final String input) {
|
||||
int start = 0;
|
||||
for (int i = input.indexOf("://") - 1; i >= 0; i--) {
|
||||
if (!input.substring(i, i + 1).matches("\\p{L}")) {
|
||||
start = i + 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return input.substring(start);
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Utils
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
private String trim(final String input) {
|
||||
if (input == null || input.length() < 1) {
|
||||
return input;
|
||||
} else {
|
||||
String output = input;
|
||||
while (output.length() > 0 && output.substring(0, 1).matches(REGEX_REMOVE_FROM_URL)) {
|
||||
output = output.substring(1);
|
||||
}
|
||||
while (output.length() > 0
|
||||
&& output.substring(output.length() - 1).matches(REGEX_REMOVE_FROM_URL)) {
|
||||
output = output.substring(0, output.length() - 1);
|
||||
}
|
||||
return output;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves all Strings which look remotely like URLs from a text.
|
||||
* Used if NewPipe was called through share menu.
|
||||
*
|
||||
* @param sharedText text to scan for URLs.
|
||||
* @return potential URLs
|
||||
*/
|
||||
protected String[] getUris(final String sharedText) {
|
||||
final Collection<String> result = new HashSet<>();
|
||||
if (sharedText != null) {
|
||||
final String[] array = sharedText.split("\\p{Space}");
|
||||
for (String s : array) {
|
||||
s = trim(s);
|
||||
if (s.length() != 0) {
|
||||
if (s.matches(".+://.+")) {
|
||||
result.add(removeHeadingGibberish(s));
|
||||
} else if (s.matches(".+\\..+")) {
|
||||
result.add("http://" + s);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return result.toArray(new String[result.size()]);
|
||||
}
|
||||
|
||||
private static class AdapterChoiceItem {
|
||||
final String description;
|
||||
final String key;
|
||||
@@ -602,6 +627,7 @@ public class RouterActivity extends AppCompatActivity {
|
||||
this.playerChoice = playerChoice;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String toString() {
|
||||
return serviceId + ":" + url + " > " + linkType + " ::: " + playerChoice;
|
||||
@@ -634,7 +660,7 @@ public class RouterActivity extends AppCompatActivity {
|
||||
if (!(serializable instanceof Choice)) {
|
||||
return;
|
||||
}
|
||||
Choice playerChoice = (Choice) serializable;
|
||||
final Choice playerChoice = (Choice) serializable;
|
||||
handleChoice(playerChoice);
|
||||
}
|
||||
|
||||
@@ -668,9 +694,9 @@ public class RouterActivity extends AppCompatActivity {
|
||||
if (fetcher != null) {
|
||||
fetcher.dispose();
|
||||
}
|
||||
}, throwable -> ExtractorHelper.handleGeneralException(this,
|
||||
choice.serviceId, choice.url, throwable, finalUserAction,
|
||||
", opened with " + choice.playerChoice));
|
||||
}, throwable -> handleError(this, new ErrorInfo(throwable, finalUserAction,
|
||||
choice.url + " opened with " + choice.playerChoice,
|
||||
choice.serviceId)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -682,46 +708,35 @@ public class RouterActivity extends AppCompatActivity {
|
||||
|
||||
final SharedPreferences preferences = PreferenceManager
|
||||
.getDefaultSharedPreferences(this);
|
||||
boolean isExtVideoEnabled = preferences.getBoolean(
|
||||
final boolean isExtVideoEnabled = preferences.getBoolean(
|
||||
getString(R.string.use_external_video_player_key), false);
|
||||
boolean isExtAudioEnabled = preferences.getBoolean(
|
||||
final boolean isExtAudioEnabled = preferences.getBoolean(
|
||||
getString(R.string.use_external_audio_player_key), false);
|
||||
|
||||
PlayQueue playQueue;
|
||||
String playerChoice = choice.playerChoice;
|
||||
|
||||
final PlayQueue playQueue;
|
||||
if (info instanceof StreamInfo) {
|
||||
if (playerChoice.equals(backgroundPlayerKey) && isExtAudioEnabled) {
|
||||
if (choice.playerChoice.equals(backgroundPlayerKey) && isExtAudioEnabled) {
|
||||
NavigationHelper.playOnExternalAudioPlayer(this, (StreamInfo) info);
|
||||
|
||||
} else if (playerChoice.equals(videoPlayerKey) && isExtVideoEnabled) {
|
||||
return;
|
||||
} else if (choice.playerChoice.equals(videoPlayerKey) && isExtVideoEnabled) {
|
||||
NavigationHelper.playOnExternalVideoPlayer(this, (StreamInfo) info);
|
||||
|
||||
} else {
|
||||
playQueue = new SinglePlayQueue((StreamInfo) info);
|
||||
|
||||
if (playerChoice.equals(videoPlayerKey)) {
|
||||
NavigationHelper.playOnMainPlayer(this, playQueue, true);
|
||||
} else if (playerChoice.equals(backgroundPlayerKey)) {
|
||||
NavigationHelper.enqueueOnBackgroundPlayer(this, playQueue, true);
|
||||
} else if (playerChoice.equals(popupPlayerKey)) {
|
||||
NavigationHelper.enqueueOnPopupPlayer(this, playQueue, true);
|
||||
}
|
||||
return;
|
||||
}
|
||||
playQueue = new SinglePlayQueue((StreamInfo) info);
|
||||
} else if (info instanceof ChannelInfo) {
|
||||
playQueue = new ChannelPlayQueue((ChannelInfo) info);
|
||||
} else if (info instanceof PlaylistInfo) {
|
||||
playQueue = new PlaylistPlayQueue((PlaylistInfo) info);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
if (info instanceof ChannelInfo || info instanceof PlaylistInfo) {
|
||||
playQueue = info instanceof ChannelInfo
|
||||
? new ChannelPlayQueue((ChannelInfo) info)
|
||||
: new PlaylistPlayQueue((PlaylistInfo) info);
|
||||
|
||||
if (playerChoice.equals(videoPlayerKey)) {
|
||||
NavigationHelper.playOnMainPlayer(this, playQueue, true);
|
||||
} else if (playerChoice.equals(backgroundPlayerKey)) {
|
||||
NavigationHelper.playOnBackgroundPlayer(this, playQueue, true);
|
||||
} else if (playerChoice.equals(popupPlayerKey)) {
|
||||
NavigationHelper.playOnPopupPlayer(this, playQueue, true);
|
||||
}
|
||||
if (choice.playerChoice.equals(videoPlayerKey)) {
|
||||
NavigationHelper.playOnMainPlayer(this, playQueue, false);
|
||||
} else if (choice.playerChoice.equals(backgroundPlayerKey)) {
|
||||
NavigationHelper.playOnBackgroundPlayer(this, playQueue, true);
|
||||
} else if (choice.playerChoice.equals(popupPlayerKey)) {
|
||||
NavigationHelper.playOnPopupPlayer(this, playQueue, true);
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -729,7 +744,7 @@ public class RouterActivity extends AppCompatActivity {
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
stopForeground(true);
|
||||
ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE);
|
||||
if (fetcher != null) {
|
||||
fetcher.dispose();
|
||||
}
|
||||
|
||||
@@ -1,201 +0,0 @@
|
||||
package org.schabi.newpipe.about;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
import androidx.fragment.app.FragmentPagerAdapter;
|
||||
import androidx.fragment.app.FragmentStatePagerAdapter;
|
||||
import androidx.viewpager.widget.PagerAdapter;
|
||||
import androidx.viewpager.widget.ViewPager;
|
||||
|
||||
import com.google.android.material.tabs.TabLayout;
|
||||
|
||||
import org.schabi.newpipe.BuildConfig;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.util.ThemeHelper;
|
||||
|
||||
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
|
||||
import static org.schabi.newpipe.util.ShareUtils.openUrlInBrowser;
|
||||
|
||||
public class AboutActivity extends AppCompatActivity {
|
||||
/**
|
||||
* List of all software components.
|
||||
*/
|
||||
private static final SoftwareComponent[] SOFTWARE_COMPONENTS = new SoftwareComponent[]{
|
||||
new SoftwareComponent("Giga Get", "2014 - 2015", "Peter Cai",
|
||||
"https://github.com/PaperAirplane-Dev-Team/GigaGet", StandardLicenses.GPL2),
|
||||
new SoftwareComponent("NewPipe Extractor", "2017 - 2020", "Christian Schabesberger",
|
||||
"https://github.com/TeamNewPipe/NewPipeExtractor", StandardLicenses.GPL3),
|
||||
new SoftwareComponent("Jsoup", "2017", "Jonathan Hedley",
|
||||
"https://github.com/jhy/jsoup", StandardLicenses.MIT),
|
||||
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("CircleImageView", "2014 - 2020", "Henning Dodenhof",
|
||||
"https://github.com/hdodenhof/CircleImageView", StandardLicenses.APACHE2),
|
||||
new SoftwareComponent("NoNonsense-FilePicker", "2016", "Jonas Kalderstam",
|
||||
"https://github.com/spacecowboy/NoNonsense-FilePicker", StandardLicenses.MPL2),
|
||||
new SoftwareComponent("ExoPlayer", "2014 - 2020", "Google Inc",
|
||||
"https://github.com/google/ExoPlayer", StandardLicenses.APACHE2),
|
||||
new SoftwareComponent("RxAndroid", "2015 - 2018", "The RxAndroid authors",
|
||||
"https://github.com/ReactiveX/RxAndroid", StandardLicenses.APACHE2),
|
||||
new SoftwareComponent("RxJava", "2016 - 2020", "RxJava Contributors",
|
||||
"https://github.com/ReactiveX/RxJava", StandardLicenses.APACHE2),
|
||||
new SoftwareComponent("RxBinding", "2015 - 2018", "Jake Wharton",
|
||||
"https://github.com/JakeWharton/RxBinding", StandardLicenses.APACHE2),
|
||||
new SoftwareComponent("PrettyTime", "2012 - 2020", "Lincoln Baxter, III",
|
||||
"https://github.com/ocpsoft/prettytime", StandardLicenses.APACHE2),
|
||||
new SoftwareComponent("Markwon", "2017 - 2020", "Noties",
|
||||
"https://github.com/noties/Markwon", StandardLicenses.APACHE2),
|
||||
new SoftwareComponent("Groupie", "2016", "Lisa Wray",
|
||||
"https://github.com/lisawray/groupie", StandardLicenses.MIT)
|
||||
};
|
||||
|
||||
/**
|
||||
* The {@link PagerAdapter} that will provide
|
||||
* fragments for each of the sections. We use a
|
||||
* {@link FragmentPagerAdapter} derivative, which will keep every
|
||||
* loaded fragment in memory. If this becomes too memory intensive, it
|
||||
* may be best to switch to a
|
||||
* {@link FragmentStatePagerAdapter}.
|
||||
*/
|
||||
private SectionsPagerAdapter mSectionsPagerAdapter;
|
||||
|
||||
/**
|
||||
* The {@link ViewPager} that will host the section contents.
|
||||
*/
|
||||
private ViewPager mViewPager;
|
||||
|
||||
@Override
|
||||
protected void onCreate(final Bundle savedInstanceState) {
|
||||
assureCorrectAppLanguage(this);
|
||||
super.onCreate(savedInstanceState);
|
||||
ThemeHelper.setTheme(this);
|
||||
this.setTitle(getString(R.string.title_activity_about));
|
||||
|
||||
setContentView(R.layout.activity_about);
|
||||
|
||||
Toolbar toolbar = findViewById(R.id.toolbar);
|
||||
setSupportActionBar(toolbar);
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
// Create the adapter that will return a fragment for each of the three
|
||||
// primary sections of the activity.
|
||||
mSectionsPagerAdapter = new SectionsPagerAdapter(getSupportFragmentManager());
|
||||
|
||||
// Set up the ViewPager with the sections adapter.
|
||||
mViewPager = findViewById(R.id.container);
|
||||
mViewPager.setAdapter(mSectionsPagerAdapter);
|
||||
|
||||
TabLayout tabLayout = findViewById(R.id.tabs);
|
||||
tabLayout.setupWithViewPager(mViewPager);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(final MenuItem item) {
|
||||
int id = item.getItemId();
|
||||
|
||||
switch (id) {
|
||||
case android.R.id.home:
|
||||
finish();
|
||||
return true;
|
||||
}
|
||||
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
/**
|
||||
* A placeholder fragment containing a simple view.
|
||||
*/
|
||||
public static class AboutFragment extends Fragment {
|
||||
public AboutFragment() { }
|
||||
|
||||
/**
|
||||
* Created a new instance of this fragment for the given section number.
|
||||
*
|
||||
* @return New instance of {@link AboutFragment}
|
||||
*/
|
||||
public static AboutFragment newInstance() {
|
||||
return new AboutFragment();
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(final LayoutInflater inflater, final ViewGroup container,
|
||||
final Bundle savedInstanceState) {
|
||||
View rootView = inflater.inflate(R.layout.fragment_about, container, false);
|
||||
Context context = this.getContext();
|
||||
|
||||
TextView version = rootView.findViewById(R.id.app_version);
|
||||
version.setText(BuildConfig.VERSION_NAME);
|
||||
|
||||
View githubLink = rootView.findViewById(R.id.github_link);
|
||||
githubLink.setOnClickListener(nv ->
|
||||
openUrlInBrowser(context, context.getString(R.string.github_url)));
|
||||
|
||||
View donationLink = rootView.findViewById(R.id.donation_link);
|
||||
donationLink.setOnClickListener(v ->
|
||||
openUrlInBrowser(context, context.getString(R.string.donation_url)));
|
||||
|
||||
View websiteLink = rootView.findViewById(R.id.website_link);
|
||||
websiteLink.setOnClickListener(nv ->
|
||||
openUrlInBrowser(context, context.getString(R.string.website_url)));
|
||||
|
||||
View privacyPolicyLink = rootView.findViewById(R.id.privacy_policy_link);
|
||||
privacyPolicyLink.setOnClickListener(v ->
|
||||
openUrlInBrowser(context, context.getString(R.string.privacy_policy_url)));
|
||||
|
||||
return rootView;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* A {@link FragmentPagerAdapter} that returns a fragment corresponding to
|
||||
* one of the sections/tabs/pages.
|
||||
*/
|
||||
public class SectionsPagerAdapter extends FragmentPagerAdapter {
|
||||
public SectionsPagerAdapter(final FragmentManager fm) {
|
||||
super(fm);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Fragment getItem(final int position) {
|
||||
switch (position) {
|
||||
case 0:
|
||||
return AboutFragment.newInstance();
|
||||
case 1:
|
||||
return LicenseFragment.newInstance(SOFTWARE_COMPONENTS);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCount() {
|
||||
// Show 2 total pages.
|
||||
return 2;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CharSequence getPageTitle(final int position) {
|
||||
switch (position) {
|
||||
case 0:
|
||||
return getString(R.string.tab_about);
|
||||
case 1:
|
||||
return getString(R.string.tab_licenses);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
191
app/src/main/java/org/schabi/newpipe/about/AboutActivity.kt
Normal file
191
app/src/main/java/org/schabi/newpipe/about/AboutActivity.kt
Normal file
@@ -0,0 +1,191 @@
|
||||
package org.schabi.newpipe.about
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.Button
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import androidx.viewpager2.adapter.FragmentStateAdapter
|
||||
import com.google.android.material.tabs.TabLayout
|
||||
import com.google.android.material.tabs.TabLayoutMediator
|
||||
import org.schabi.newpipe.BuildConfig
|
||||
import org.schabi.newpipe.R
|
||||
import org.schabi.newpipe.databinding.ActivityAboutBinding
|
||||
import org.schabi.newpipe.databinding.FragmentAboutBinding
|
||||
import org.schabi.newpipe.util.Localization
|
||||
import org.schabi.newpipe.util.ShareUtils
|
||||
import org.schabi.newpipe.util.ThemeHelper
|
||||
|
||||
class AboutActivity : AppCompatActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
Localization.assureCorrectAppLanguage(this)
|
||||
super.onCreate(savedInstanceState)
|
||||
ThemeHelper.setTheme(this)
|
||||
title = getString(R.string.title_activity_about)
|
||||
val aboutBinding = ActivityAboutBinding.inflate(layoutInflater)
|
||||
setContentView(aboutBinding.root)
|
||||
setSupportActionBar(aboutBinding.aboutToolbar)
|
||||
supportActionBar!!.setDisplayHomeAsUpEnabled(true)
|
||||
// Create the adapter that will return a fragment for each of the three
|
||||
// primary sections of the activity.
|
||||
val mAboutStateAdapter = AboutStateAdapter(this)
|
||||
|
||||
// Set up the ViewPager with the sections adapter.
|
||||
aboutBinding.aboutViewPager2.adapter = mAboutStateAdapter
|
||||
TabLayoutMediator(
|
||||
aboutBinding.aboutTabLayout,
|
||||
aboutBinding.aboutViewPager2
|
||||
) { tab: TabLayout.Tab, position: Int ->
|
||||
when (position) {
|
||||
POS_ABOUT -> tab.setText(R.string.tab_about)
|
||||
POS_LICENSE -> tab.setText(R.string.tab_licenses)
|
||||
else -> throw IllegalArgumentException("Unknown position for ViewPager2")
|
||||
}
|
||||
}.attach()
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
if (item.itemId == android.R.id.home) {
|
||||
finish()
|
||||
return true
|
||||
}
|
||||
return super.onOptionsItemSelected(item)
|
||||
}
|
||||
|
||||
/**
|
||||
* A placeholder fragment containing a simple view.
|
||||
*/
|
||||
class AboutFragment : Fragment() {
|
||||
private fun Button.openLink(url: Int) {
|
||||
setOnClickListener {
|
||||
ShareUtils.openUrlInBrowser(
|
||||
context,
|
||||
requireContext().getString(url),
|
||||
false
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
val aboutBinding = FragmentAboutBinding.inflate(inflater, container, false)
|
||||
aboutBinding.aboutAppVersion.text = BuildConfig.VERSION_NAME
|
||||
aboutBinding.aboutGithubLink.openLink(R.string.github_url)
|
||||
aboutBinding.aboutDonationLink.openLink(R.string.donation_url)
|
||||
aboutBinding.aboutWebsiteLink.openLink(R.string.website_url)
|
||||
aboutBinding.aboutPrivacyPolicyLink.openLink(R.string.privacy_policy_url)
|
||||
return aboutBinding.root
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A [FragmentStateAdapter] that returns a fragment corresponding to
|
||||
* one of the sections/tabs/pages.
|
||||
*/
|
||||
private class AboutStateAdapter(fa: FragmentActivity) : FragmentStateAdapter(fa) {
|
||||
override fun createFragment(position: Int): Fragment {
|
||||
return when (position) {
|
||||
POS_ABOUT -> AboutFragment()
|
||||
POS_LICENSE -> LicenseFragment.newInstance(SOFTWARE_COMPONENTS)
|
||||
else -> throw IllegalArgumentException("Unknown position for ViewPager2")
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int {
|
||||
// Show 2 total pages.
|
||||
return TOTAL_COUNT
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* List of all software components.
|
||||
*/
|
||||
private val SOFTWARE_COMPONENTS = arrayOf(
|
||||
SoftwareComponent(
|
||||
"ACRA", "2013", "Kevin Gaudin",
|
||||
"https://github.com/ACRA/acra", StandardLicenses.APACHE2
|
||||
),
|
||||
SoftwareComponent(
|
||||
"AndroidX", "2005 - 2011", "The Android Open Source Project",
|
||||
"https://developer.android.com/jetpack", StandardLicenses.APACHE2
|
||||
),
|
||||
SoftwareComponent(
|
||||
"CircleImageView", "2014 - 2020", "Henning Dodenhof",
|
||||
"https://github.com/hdodenhof/CircleImageView", StandardLicenses.APACHE2
|
||||
),
|
||||
SoftwareComponent(
|
||||
"ExoPlayer", "2014 - 2020", "Google, Inc.",
|
||||
"https://github.com/google/ExoPlayer", StandardLicenses.APACHE2
|
||||
),
|
||||
SoftwareComponent(
|
||||
"GigaGet", "2014 - 2015", "Peter Cai",
|
||||
"https://github.com/PaperAirplane-Dev-Team/GigaGet", StandardLicenses.GPL3
|
||||
),
|
||||
SoftwareComponent(
|
||||
"Groupie", "2016", "Lisa Wray",
|
||||
"https://github.com/lisawray/groupie", StandardLicenses.MIT
|
||||
),
|
||||
SoftwareComponent(
|
||||
"Icepick", "2015", "Frankie Sardo",
|
||||
"https://github.com/frankiesardo/icepick", StandardLicenses.EPL1
|
||||
),
|
||||
SoftwareComponent(
|
||||
"Jsoup", "2009 - 2020", "Jonathan Hedley",
|
||||
"https://github.com/jhy/jsoup", StandardLicenses.MIT
|
||||
),
|
||||
SoftwareComponent(
|
||||
"Markwon", "2019", "Dimitry Ivanov",
|
||||
"https://github.com/noties/Markwon", StandardLicenses.APACHE2
|
||||
),
|
||||
SoftwareComponent(
|
||||
"Material Components for Android", "2016 - 2020", "Google, Inc.",
|
||||
"https://github.com/material-components/material-components-android",
|
||||
StandardLicenses.APACHE2
|
||||
),
|
||||
SoftwareComponent(
|
||||
"NewPipe Extractor", "2017 - 2020", "Christian Schabesberger",
|
||||
"https://github.com/TeamNewPipe/NewPipeExtractor", StandardLicenses.GPL3
|
||||
),
|
||||
SoftwareComponent(
|
||||
"NoNonsense-FilePicker", "2016", "Jonas Kalderstam",
|
||||
"https://github.com/spacecowboy/NoNonsense-FilePicker", StandardLicenses.MPL2
|
||||
),
|
||||
SoftwareComponent(
|
||||
"OkHttp", "2019", "Square, Inc.",
|
||||
"https://square.github.io/okhttp/", StandardLicenses.APACHE2
|
||||
),
|
||||
SoftwareComponent(
|
||||
"PrettyTime", "2012 - 2020", "Lincoln Baxter, III",
|
||||
"https://github.com/ocpsoft/prettytime", StandardLicenses.APACHE2
|
||||
),
|
||||
SoftwareComponent(
|
||||
"RxAndroid", "2015", "The RxAndroid authors",
|
||||
"https://github.com/ReactiveX/RxAndroid", StandardLicenses.APACHE2
|
||||
),
|
||||
SoftwareComponent(
|
||||
"RxBinding", "2015", "Jake Wharton",
|
||||
"https://github.com/JakeWharton/RxBinding", StandardLicenses.APACHE2
|
||||
),
|
||||
SoftwareComponent(
|
||||
"RxJava", "2016 - 2020", "RxJava Contributors",
|
||||
"https://github.com/ReactiveX/RxJava", StandardLicenses.APACHE2
|
||||
),
|
||||
SoftwareComponent(
|
||||
"Universal Image Loader", "2011 - 2015", "Sergey Tarasevich",
|
||||
"https://github.com/nostra13/Android-Universal-Image-Loader",
|
||||
StandardLicenses.APACHE2
|
||||
)
|
||||
)
|
||||
private const val POS_ABOUT = 0
|
||||
private const val POS_LICENSE = 1
|
||||
private const val TOTAL_COUNT = 2
|
||||
}
|
||||
}
|
||||
@@ -1,78 +0,0 @@
|
||||
package org.schabi.newpipe.about;
|
||||
|
||||
import android.net.Uri;
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
|
||||
/**
|
||||
* Class for storing information about a software license.
|
||||
*/
|
||||
public class License implements Parcelable {
|
||||
public static final Creator<License> CREATOR = new Creator<License>() {
|
||||
@Override
|
||||
public License createFromParcel(final Parcel source) {
|
||||
return new License(source);
|
||||
}
|
||||
|
||||
@Override
|
||||
public License[] newArray(final int size) {
|
||||
return new License[size];
|
||||
}
|
||||
};
|
||||
private final String abbreviation;
|
||||
private final String name;
|
||||
private String filename;
|
||||
|
||||
public License(final String name, final String abbreviation, final String filename) {
|
||||
if (name == null) {
|
||||
throw new NullPointerException("name is null");
|
||||
}
|
||||
if (abbreviation == null) {
|
||||
throw new NullPointerException("abbreviation is null");
|
||||
}
|
||||
if (filename == null) {
|
||||
throw new NullPointerException("filename is null");
|
||||
}
|
||||
this.name = name;
|
||||
this.filename = filename;
|
||||
this.abbreviation = abbreviation;
|
||||
}
|
||||
|
||||
protected License(final Parcel in) {
|
||||
this.filename = in.readString();
|
||||
this.abbreviation = in.readString();
|
||||
this.name = in.readString();
|
||||
}
|
||||
|
||||
public Uri getContentUri() {
|
||||
return new Uri.Builder()
|
||||
.scheme("file")
|
||||
.path("/android_asset")
|
||||
.appendPath(filename)
|
||||
.build();
|
||||
}
|
||||
|
||||
public String getAbbreviation() {
|
||||
return abbreviation;
|
||||
}
|
||||
|
||||
public String getFilename() {
|
||||
return filename;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int describeContents() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeToParcel(final Parcel dest, final int flags) {
|
||||
dest.writeString(this.filename);
|
||||
dest.writeString(this.abbreviation);
|
||||
dest.writeString(this.name);
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
}
|
||||
11
app/src/main/java/org/schabi/newpipe/about/License.kt
Normal file
11
app/src/main/java/org/schabi/newpipe/about/License.kt
Normal file
@@ -0,0 +1,11 @@
|
||||
package org.schabi.newpipe.about
|
||||
|
||||
import android.os.Parcelable
|
||||
import kotlinx.android.parcel.Parcelize
|
||||
import java.io.Serializable
|
||||
|
||||
/**
|
||||
* Class for storing information about a software license.
|
||||
*/
|
||||
@Parcelize
|
||||
class License(val name: String, val abbreviation: String, val filename: String) : Parcelable, Serializable
|
||||
@@ -1,119 +0,0 @@
|
||||
package org.schabi.newpipe.about;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
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.widget.TextView;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.Fragment;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.util.ShareUtils;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* Fragment containing the software licenses.
|
||||
*/
|
||||
public class LicenseFragment extends Fragment {
|
||||
private static final String ARG_COMPONENTS = "components";
|
||||
private SoftwareComponent[] softwareComponents;
|
||||
private SoftwareComponent componentForContextMenu;
|
||||
|
||||
public static LicenseFragment newInstance(final SoftwareComponent[] softwareComponents) {
|
||||
if (softwareComponents == null) {
|
||||
throw new NullPointerException("softwareComponents is null");
|
||||
}
|
||||
LicenseFragment fragment = new LicenseFragment();
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putParcelableArray(ARG_COMPONENTS, softwareComponents);
|
||||
fragment.setArguments(bundle);
|
||||
return fragment;
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows a popup containing the license.
|
||||
*
|
||||
* @param context the context to use
|
||||
* @param license the license to show
|
||||
*/
|
||||
private static void showLicense(final Context context, final License license) {
|
||||
new LicenseFragmentHelper((Activity) context).execute(license);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(@Nullable final Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
softwareComponents = (SoftwareComponent[]) getArguments()
|
||||
.getParcelableArray(ARG_COMPONENTS);
|
||||
|
||||
// Sort components by name
|
||||
Arrays.sort(softwareComponents, (o1, o2) -> o1.getName().compareTo(o2.getName()));
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public View onCreateView(final LayoutInflater inflater, @Nullable final ViewGroup container,
|
||||
@Nullable final Bundle savedInstanceState) {
|
||||
final View rootView = inflater.inflate(R.layout.fragment_licenses, container, false);
|
||||
final ViewGroup softwareComponentsView = rootView.findViewById(R.id.software_components);
|
||||
|
||||
final View licenseLink = rootView.findViewById(R.id.app_read_license);
|
||||
licenseLink.setOnClickListener(v ->
|
||||
showLicense(getActivity(), StandardLicenses.GPL3));
|
||||
|
||||
for (final SoftwareComponent component : softwareComponents) {
|
||||
final View componentView = inflater
|
||||
.inflate(R.layout.item_software_component, container, false);
|
||||
final TextView softwareName = componentView.findViewById(R.id.name);
|
||||
final TextView copyright = componentView.findViewById(R.id.copyright);
|
||||
softwareName.setText(component.getName());
|
||||
copyright.setText(getString(R.string.copyright,
|
||||
component.getYears(),
|
||||
component.getCopyrightOwner(),
|
||||
component.getLicense().getAbbreviation()));
|
||||
|
||||
componentView.setTag(component);
|
||||
componentView.setOnClickListener(v ->
|
||||
showLicense(getActivity(), component.getLicense()));
|
||||
softwareComponentsView.addView(componentView);
|
||||
registerForContextMenu(componentView);
|
||||
}
|
||||
return rootView;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreateContextMenu(final ContextMenu menu, final View v,
|
||||
final ContextMenu.ContextMenuInfo menuInfo) {
|
||||
final MenuInflater inflater = getActivity().getMenuInflater();
|
||||
final SoftwareComponent component = (SoftwareComponent) v.getTag();
|
||||
menu.setHeaderTitle(component.getName());
|
||||
inflater.inflate(R.menu.software_component, menu);
|
||||
super.onCreateContextMenu(menu, v, menuInfo);
|
||||
componentForContextMenu = (SoftwareComponent) v.getTag();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onContextItemSelected(final MenuItem item) {
|
||||
// item.getMenuInfo() is null so we use the tag of the view
|
||||
final SoftwareComponent component = componentForContextMenu;
|
||||
if (component == null) {
|
||||
return false;
|
||||
}
|
||||
switch (item.getItemId()) {
|
||||
case R.id.action_website:
|
||||
ShareUtils.openUrlInBrowser(getActivity(), component.getLink());
|
||||
return true;
|
||||
case R.id.action_show_license:
|
||||
showLicense(getActivity(), component.getLicense());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
131
app/src/main/java/org/schabi/newpipe/about/LicenseFragment.kt
Normal file
131
app/src/main/java/org/schabi/newpipe/about/LicenseFragment.kt
Normal file
@@ -0,0 +1,131 @@
|
||||
package org.schabi.newpipe.about
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.ContextMenu
|
||||
import android.view.ContextMenu.ContextMenuInfo
|
||||
import android.view.LayoutInflater
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.fragment.app.Fragment
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||
import org.schabi.newpipe.R
|
||||
import org.schabi.newpipe.about.LicenseFragmentHelper.showLicense
|
||||
import org.schabi.newpipe.databinding.FragmentLicensesBinding
|
||||
import org.schabi.newpipe.databinding.ItemSoftwareComponentBinding
|
||||
import org.schabi.newpipe.util.ShareUtils
|
||||
import java.util.Arrays
|
||||
import java.util.Objects
|
||||
|
||||
/**
|
||||
* Fragment containing the software licenses.
|
||||
*/
|
||||
class LicenseFragment : Fragment() {
|
||||
private lateinit var softwareComponents: Array<SoftwareComponent>
|
||||
private var componentForContextMenu: SoftwareComponent? = null
|
||||
private var activeLicense: License? = null
|
||||
private val compositeDisposable = CompositeDisposable()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
softwareComponents =
|
||||
arguments?.getParcelableArray(ARG_COMPONENTS) as Array<SoftwareComponent>
|
||||
if (savedInstanceState != null) {
|
||||
val license = savedInstanceState.getSerializable(LICENSE_KEY)
|
||||
if (license != null) {
|
||||
activeLicense = license as License?
|
||||
}
|
||||
}
|
||||
// Sort components by name
|
||||
Arrays.sort(softwareComponents, Comparator.comparing(SoftwareComponent::name))
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
compositeDisposable.dispose()
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
val binding = FragmentLicensesBinding.inflate(inflater, container, false)
|
||||
binding.licensesAppReadLicense.setOnClickListener {
|
||||
activeLicense = StandardLicenses.GPL3
|
||||
compositeDisposable.add(
|
||||
showLicense(activity, StandardLicenses.GPL3)
|
||||
)
|
||||
}
|
||||
for (component in softwareComponents) {
|
||||
val componentBinding = ItemSoftwareComponentBinding
|
||||
.inflate(inflater, container, false)
|
||||
componentBinding.name.text = component.name
|
||||
componentBinding.copyright.text = getString(
|
||||
R.string.copyright,
|
||||
component.years,
|
||||
component.copyrightOwner,
|
||||
component.license.abbreviation
|
||||
)
|
||||
val root: View = componentBinding.root
|
||||
root.tag = component
|
||||
root.setOnClickListener {
|
||||
activeLicense = component.license
|
||||
compositeDisposable.add(
|
||||
showLicense(activity, component.license)
|
||||
)
|
||||
}
|
||||
binding.licensesSoftwareComponents.addView(root)
|
||||
registerForContextMenu(root)
|
||||
}
|
||||
if (activeLicense != null) {
|
||||
compositeDisposable.add(
|
||||
showLicense(activity, activeLicense!!)
|
||||
)
|
||||
}
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onCreateContextMenu(menu: ContextMenu, v: View, menuInfo: ContextMenuInfo?) {
|
||||
val inflater = requireActivity().menuInflater
|
||||
val component = v.tag as SoftwareComponent
|
||||
menu.setHeaderTitle(component.name)
|
||||
inflater.inflate(R.menu.software_component, menu)
|
||||
super.onCreateContextMenu(menu, v, menuInfo)
|
||||
componentForContextMenu = component
|
||||
}
|
||||
|
||||
override fun onContextItemSelected(item: MenuItem): Boolean {
|
||||
// item.getMenuInfo() is null so we use the tag of the view
|
||||
val component = componentForContextMenu ?: return false
|
||||
when (item.itemId) {
|
||||
R.id.menu_software_website -> {
|
||||
ShareUtils.openUrlInBrowser(activity, component.link)
|
||||
return true
|
||||
}
|
||||
R.id.menu_software_show_license -> compositeDisposable.add(
|
||||
showLicense(activity, component.license)
|
||||
)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(savedInstanceState: Bundle) {
|
||||
super.onSaveInstanceState(savedInstanceState)
|
||||
if (activeLicense != null) {
|
||||
savedInstanceState.putSerializable(LICENSE_KEY, activeLicense)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val ARG_COMPONENTS = "components"
|
||||
private const val LICENSE_KEY = "ACTIVE_LICENSE"
|
||||
fun newInstance(softwareComponents: Array<SoftwareComponent>): LicenseFragment {
|
||||
val fragment = LicenseFragment()
|
||||
fragment.arguments =
|
||||
bundleOf(ARG_COMPONENTS to Objects.requireNonNull(softwareComponents))
|
||||
return fragment
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,128 +0,0 @@
|
||||
package org.schabi.newpipe.about;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.os.AsyncTask;
|
||||
import android.util.Base64;
|
||||
import android.webkit.WebView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.util.ThemeHelper;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
|
||||
|
||||
public class LicenseFragmentHelper extends AsyncTask<Object, Void, Integer> {
|
||||
private final WeakReference<Activity> weakReference;
|
||||
private License license;
|
||||
|
||||
public LicenseFragmentHelper(@Nullable final Activity activity) {
|
||||
weakReference = new WeakReference<>(activity);
|
||||
}
|
||||
|
||||
/**
|
||||
* @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
|
||||
*/
|
||||
private static String getFormattedLicense(@NonNull final Context context,
|
||||
@NonNull final License license) {
|
||||
final StringBuilder licenseContent = new StringBuilder();
|
||||
final String webViewData;
|
||||
try {
|
||||
final BufferedReader in = new BufferedReader(new InputStreamReader(
|
||||
context.getAssets().open(license.getFilename()), StandardCharsets.UTF_8));
|
||||
String str;
|
||||
while ((str = in.readLine()) != null) {
|
||||
licenseContent.append(str);
|
||||
}
|
||||
in.close();
|
||||
|
||||
// split the HTML file and insert the stylesheet into the HEAD of the file
|
||||
webViewData = licenseContent.toString().replace("</head>",
|
||||
"<style>" + getLicenseStylesheet(context) + "</style></head>");
|
||||
} catch (IOException e) {
|
||||
throw new IllegalArgumentException(
|
||||
"Could not get license file: " + license.getFilename(), e);
|
||||
}
|
||||
return webViewData;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param context
|
||||
* @return String which is a CSS stylesheet according to the context's theme
|
||||
*/
|
||||
private static String getLicenseStylesheet(final Context context) {
|
||||
final 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
|
||||
*/
|
||||
private static String getHexRGBColor(final Context context, final int color) {
|
||||
return context.getResources().getString(color).substring(3);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private Activity getActivity() {
|
||||
final Activity activity = weakReference.get();
|
||||
|
||||
if (activity != null && activity.isFinishing()) {
|
||||
return null;
|
||||
} else {
|
||||
return activity;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Integer doInBackground(final Object... objects) {
|
||||
license = (License) objects[0];
|
||||
return 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(final Integer result) {
|
||||
final Activity activity = getActivity();
|
||||
if (activity == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final String webViewData = Base64.encodeToString(getFormattedLicense(activity, license)
|
||||
.getBytes(StandardCharsets.UTF_8), Base64.NO_PADDING);
|
||||
final WebView webView = new WebView(activity);
|
||||
webView.loadData(webViewData, "text/html; charset=UTF-8", "base64");
|
||||
|
||||
final AlertDialog.Builder alert = new AlertDialog.Builder(activity);
|
||||
alert.setTitle(license.getName());
|
||||
alert.setView(webView);
|
||||
assureCorrectAppLanguage(activity);
|
||||
alert.setNegativeButton(activity.getString(R.string.finish),
|
||||
(dialog, which) -> dialog.dismiss());
|
||||
alert.show();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
package org.schabi.newpipe.about
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Base64
|
||||
import android.webkit.WebView
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.core.Observable
|
||||
import io.reactivex.rxjava3.disposables.Disposable
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import org.schabi.newpipe.R
|
||||
import org.schabi.newpipe.util.Localization
|
||||
import org.schabi.newpipe.util.ThemeHelper
|
||||
import java.io.BufferedReader
|
||||
import java.io.IOException
|
||||
import java.io.InputStreamReader
|
||||
import java.nio.charset.StandardCharsets
|
||||
|
||||
object LicenseFragmentHelper {
|
||||
/**
|
||||
* @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
|
||||
*/
|
||||
private fun getFormattedLicense(context: Context, license: License): String {
|
||||
val licenseContent = StringBuilder()
|
||||
val webViewData: String
|
||||
try {
|
||||
BufferedReader(
|
||||
InputStreamReader(
|
||||
context.assets.open(license.filename),
|
||||
StandardCharsets.UTF_8
|
||||
)
|
||||
).use { `in` ->
|
||||
var str: String?
|
||||
while (`in`.readLine().also { str = it } != null) {
|
||||
licenseContent.append(str)
|
||||
}
|
||||
|
||||
// split the HTML file and insert the stylesheet into the HEAD of the file
|
||||
webViewData = "$licenseContent".replace(
|
||||
"</head>",
|
||||
"<style>" + getLicenseStylesheet(context) + "</style></head>"
|
||||
)
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
throw IllegalArgumentException(
|
||||
"Could not get license file: " + license.filename, e
|
||||
)
|
||||
}
|
||||
return webViewData
|
||||
}
|
||||
|
||||
/**
|
||||
* @param context the Android context
|
||||
* @return String which is a CSS stylesheet according to the context's theme
|
||||
*/
|
||||
private fun getLicenseStylesheet(context: Context): String {
|
||||
val isLightTheme = ThemeHelper.isLightThemeSelected(context)
|
||||
return (
|
||||
"body{padding:12px 15px;margin:0;" + "background:#" + getHexRGBColor(
|
||||
context,
|
||||
if (isLightTheme) R.color.light_license_background_color
|
||||
else R.color.dark_license_background_color
|
||||
) + ";" + "color:#" + getHexRGBColor(
|
||||
context,
|
||||
if (isLightTheme) R.color.light_license_text_color
|
||||
else R.color.dark_license_text_color
|
||||
) + "}" + "a[href]{color:#" + getHexRGBColor(
|
||||
context,
|
||||
if (isLightTheme) R.color.light_youtube_primary_color
|
||||
else 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
|
||||
*/
|
||||
private fun getHexRGBColor(context: Context, color: Int): String {
|
||||
return context.getString(color).substring(3)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun showLicense(context: Context?, license: License): Disposable {
|
||||
return if (context == null) {
|
||||
Disposable.empty()
|
||||
} else {
|
||||
Observable.fromCallable { getFormattedLicense(context, license) }
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe { formattedLicense: String ->
|
||||
val webViewData = Base64.encodeToString(
|
||||
formattedLicense
|
||||
.toByteArray(StandardCharsets.UTF_8),
|
||||
Base64.NO_PADDING
|
||||
)
|
||||
val webView = WebView(context)
|
||||
webView.loadData(webViewData, "text/html; charset=UTF-8", "base64")
|
||||
val alert = AlertDialog.Builder(context)
|
||||
alert.setTitle(license.name)
|
||||
alert.setView(webView)
|
||||
Localization.assureCorrectAppLanguage(context)
|
||||
alert.setNegativeButton(
|
||||
context.getString(R.string.finish)
|
||||
) { dialog, _ -> dialog.dismiss() }
|
||||
alert.show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
package org.schabi.newpipe.about;
|
||||
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
|
||||
public class SoftwareComponent implements Parcelable {
|
||||
public static final Creator<SoftwareComponent> CREATOR = new Creator<SoftwareComponent>() {
|
||||
@Override
|
||||
public SoftwareComponent createFromParcel(final Parcel source) {
|
||||
return new SoftwareComponent(source);
|
||||
}
|
||||
|
||||
@Override
|
||||
public SoftwareComponent[] newArray(final int size) {
|
||||
return new SoftwareComponent[size];
|
||||
}
|
||||
};
|
||||
|
||||
private final License license;
|
||||
private final String name;
|
||||
private final String years;
|
||||
private final String copyrightOwner;
|
||||
private final String link;
|
||||
private final String version;
|
||||
|
||||
public SoftwareComponent(final String name, final String years, final String copyrightOwner,
|
||||
final String link, final License license) {
|
||||
this.name = name;
|
||||
this.years = years;
|
||||
this.copyrightOwner = copyrightOwner;
|
||||
this.link = link;
|
||||
this.license = license;
|
||||
this.version = null;
|
||||
}
|
||||
|
||||
protected SoftwareComponent(final Parcel in) {
|
||||
this.name = in.readString();
|
||||
this.license = in.readParcelable(License.class.getClassLoader());
|
||||
this.copyrightOwner = in.readString();
|
||||
this.link = in.readString();
|
||||
this.years = in.readString();
|
||||
this.version = in.readString();
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public String getYears() {
|
||||
return years;
|
||||
}
|
||||
|
||||
public String getCopyrightOwner() {
|
||||
return copyrightOwner;
|
||||
}
|
||||
|
||||
public String getLink() {
|
||||
return link;
|
||||
}
|
||||
|
||||
public String getVersion() {
|
||||
return version;
|
||||
}
|
||||
|
||||
public License getLicense() {
|
||||
return license;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int describeContents() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeToParcel(final Parcel dest, final int flags) {
|
||||
dest.writeString(name);
|
||||
dest.writeParcelable(license, flags);
|
||||
dest.writeString(copyrightOwner);
|
||||
dest.writeString(link);
|
||||
dest.writeString(years);
|
||||
dest.writeString(version);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package org.schabi.newpipe.about
|
||||
|
||||
import android.os.Parcelable
|
||||
import kotlinx.android.parcel.Parcelize
|
||||
|
||||
@Parcelize
|
||||
class SoftwareComponent
|
||||
@JvmOverloads
|
||||
constructor(
|
||||
val name: String,
|
||||
val years: String,
|
||||
val copyrightOwner: String,
|
||||
val link: String,
|
||||
val license: License,
|
||||
val version: String? = null
|
||||
) : Parcelable
|
||||
@@ -1,19 +0,0 @@
|
||||
package org.schabi.newpipe.about;
|
||||
|
||||
/**
|
||||
* Class containing information about standard software licenses.
|
||||
*/
|
||||
public final class StandardLicenses {
|
||||
public static final License GPL2
|
||||
= new License("GNU General Public License, Version 2.0", "GPLv2", "gpl_2.html");
|
||||
public static final License GPL3
|
||||
= new License("GNU General Public License, Version 3.0", "GPLv3", "gpl_3.html");
|
||||
public static final License APACHE2
|
||||
= new License("Apache License, Version 2.0", "ALv2", "apache2.html");
|
||||
public static final License MPL2
|
||||
= new License("Mozilla Public License, Version 2.0", "MPL 2.0", "mpl2.html");
|
||||
public static final License MIT
|
||||
= new License("MIT License", "MIT", "mit.html");
|
||||
|
||||
private StandardLicenses() { }
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package org.schabi.newpipe.about
|
||||
|
||||
/**
|
||||
* Class containing information about standard software licenses.
|
||||
*/
|
||||
object StandardLicenses {
|
||||
@JvmField
|
||||
val GPL3 = License("GNU General Public License, Version 3.0", "GPLv3", "gpl_3.html")
|
||||
|
||||
@JvmField
|
||||
val APACHE2 = License("Apache License, Version 2.0", "ALv2", "apache2.html")
|
||||
|
||||
@JvmField
|
||||
val MPL2 = License("Mozilla Public License, Version 2.0", "MPL 2.0", "mpl2.html")
|
||||
|
||||
@JvmField
|
||||
val MIT = License("MIT License", "MIT", "mit.html")
|
||||
|
||||
@JvmField
|
||||
val EPL1 = License("Eclipse Public License, Version 1.0", "EPL 1.0", "epl1.html")
|
||||
}
|
||||
@@ -9,18 +9,18 @@ import androidx.room.Update;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
import io.reactivex.Flowable;
|
||||
import io.reactivex.rxjava3.core.Flowable;
|
||||
|
||||
@Dao
|
||||
public interface BasicDAO<Entity> {
|
||||
/* Inserts */
|
||||
@Insert(onConflict = OnConflictStrategy.FAIL)
|
||||
@Insert(onConflict = OnConflictStrategy.ABORT)
|
||||
long insert(Entity entity);
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.FAIL)
|
||||
@Insert(onConflict = OnConflictStrategy.ABORT)
|
||||
List<Long> insertAll(Entity... entities);
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.FAIL)
|
||||
@Insert(onConflict = OnConflictStrategy.ABORT)
|
||||
List<Long> insertAll(Collection<Entity> entities);
|
||||
|
||||
/* Searches */
|
||||
|
||||
@@ -5,31 +5,35 @@ import androidx.room.TypeConverter;
|
||||
import org.schabi.newpipe.extractor.stream.StreamType;
|
||||
import org.schabi.newpipe.local.subscription.FeedGroupIcon;
|
||||
|
||||
import java.util.Date;
|
||||
import java.time.Instant;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.time.ZoneOffset;
|
||||
|
||||
public final class Converters {
|
||||
private Converters() { }
|
||||
|
||||
/**
|
||||
* Convert a long value to a date.
|
||||
* Convert a long value to a {@link OffsetDateTime}.
|
||||
*
|
||||
* @param value the long value
|
||||
* @return the date
|
||||
* @return the {@code OffsetDateTime}
|
||||
*/
|
||||
@TypeConverter
|
||||
public static Date fromTimestamp(final Long value) {
|
||||
return value == null ? null : new Date(value);
|
||||
public static OffsetDateTime offsetDateTimeFromTimestamp(final Long value) {
|
||||
return value == null ? null : OffsetDateTime.ofInstant(Instant.ofEpochMilli(value),
|
||||
ZoneOffset.UTC);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a date to a long value.
|
||||
* Convert a {@link OffsetDateTime} to a long value.
|
||||
*
|
||||
* @param date the date
|
||||
* @param offsetDateTime the {@code OffsetDateTime}
|
||||
* @return the long value
|
||||
*/
|
||||
@TypeConverter
|
||||
public static Long dateToTimestamp(final Date date) {
|
||||
return date == null ? null : date.getTime();
|
||||
public static Long offsetDateTimeToTimestamp(final OffsetDateTime offsetDateTime) {
|
||||
return offsetDateTime == null ? null : offsetDateTime.withOffsetSameInstant(ZoneOffset.UTC)
|
||||
.toInstant().toEpochMilli();
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
@@ -49,7 +53,7 @@ public final class Converters {
|
||||
|
||||
@TypeConverter
|
||||
public static FeedGroupIcon feedGroupIconOf(final Integer id) {
|
||||
for (FeedGroupIcon icon : FeedGroupIcon.values()) {
|
||||
for (final FeedGroupIcon icon : FeedGroupIcon.values()) {
|
||||
if (icon.getId() == id) {
|
||||
return icon;
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import androidx.annotation.NonNull;
|
||||
import androidx.room.migration.Migration;
|
||||
import androidx.sqlite.db.SupportSQLiteDatabase;
|
||||
|
||||
import org.schabi.newpipe.BuildConfig;
|
||||
import org.schabi.newpipe.MainActivity;
|
||||
|
||||
public final class Migrations {
|
||||
public static final int DB_VER_1 = 1;
|
||||
@@ -14,7 +14,7 @@ public final class Migrations {
|
||||
public static final int DB_VER_3 = 3;
|
||||
|
||||
private static final String TAG = Migrations.class.getName();
|
||||
public static final boolean DEBUG = !BuildConfig.BUILD_TYPE.equals("release");
|
||||
public static final boolean DEBUG = MainActivity.DEBUG;
|
||||
|
||||
public static final Migration MIGRATION_1_2 = new Migration(DB_VER_1, DB_VER_2) {
|
||||
@Override
|
||||
|
||||
@@ -6,19 +6,20 @@ import androidx.room.OnConflictStrategy
|
||||
import androidx.room.Query
|
||||
import androidx.room.Transaction
|
||||
import androidx.room.Update
|
||||
import io.reactivex.Flowable
|
||||
import java.util.Date
|
||||
import io.reactivex.rxjava3.core.Flowable
|
||||
import org.schabi.newpipe.database.feed.model.FeedEntity
|
||||
import org.schabi.newpipe.database.feed.model.FeedLastUpdatedEntity
|
||||
import org.schabi.newpipe.database.stream.model.StreamEntity
|
||||
import org.schabi.newpipe.database.subscription.SubscriptionEntity
|
||||
import java.time.OffsetDateTime
|
||||
|
||||
@Dao
|
||||
abstract class FeedDAO {
|
||||
@Query("DELETE FROM feed")
|
||||
abstract fun deleteAll(): Int
|
||||
|
||||
@Query("""
|
||||
@Query(
|
||||
"""
|
||||
SELECT s.* FROM streams s
|
||||
|
||||
INNER JOIN feed f
|
||||
@@ -27,10 +28,12 @@ abstract class FeedDAO {
|
||||
ORDER BY s.upload_date IS NULL DESC, s.upload_date DESC, s.uploader ASC
|
||||
|
||||
LIMIT 500
|
||||
""")
|
||||
"""
|
||||
)
|
||||
abstract fun getAllStreams(): Flowable<List<StreamEntity>>
|
||||
|
||||
@Query("""
|
||||
@Query(
|
||||
"""
|
||||
SELECT s.* FROM streams s
|
||||
|
||||
INNER JOIN feed f
|
||||
@@ -46,10 +49,12 @@ abstract class FeedDAO {
|
||||
|
||||
ORDER BY s.upload_date IS NULL DESC, s.upload_date DESC, s.uploader ASC
|
||||
LIMIT 500
|
||||
""")
|
||||
"""
|
||||
)
|
||||
abstract fun getAllStreamsFromGroup(groupId: Long): Flowable<List<StreamEntity>>
|
||||
|
||||
@Query("""
|
||||
@Query(
|
||||
"""
|
||||
DELETE FROM feed WHERE
|
||||
|
||||
feed.stream_id IN (
|
||||
@@ -58,12 +63,14 @@ abstract class FeedDAO {
|
||||
INNER JOIN feed f
|
||||
ON s.uid = f.stream_id
|
||||
|
||||
WHERE s.upload_date < :date
|
||||
WHERE s.upload_date < :offsetDateTime
|
||||
)
|
||||
""")
|
||||
abstract fun unlinkStreamsOlderThan(date: Date)
|
||||
"""
|
||||
)
|
||||
abstract fun unlinkStreamsOlderThan(offsetDateTime: OffsetDateTime)
|
||||
|
||||
@Query("""
|
||||
@Query(
|
||||
"""
|
||||
DELETE FROM feed
|
||||
|
||||
WHERE feed.subscription_id = :subscriptionId
|
||||
@@ -76,7 +83,8 @@ abstract class FeedDAO {
|
||||
|
||||
WHERE s.stream_type = "LIVE_STREAM" OR s.stream_type = "AUDIO_LIVE_STREAM"
|
||||
)
|
||||
""")
|
||||
"""
|
||||
)
|
||||
abstract fun unlinkOldLivestreams(subscriptionId: Long)
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
||||
@@ -100,21 +108,24 @@ abstract class FeedDAO {
|
||||
}
|
||||
}
|
||||
|
||||
@Query("""
|
||||
@Query(
|
||||
"""
|
||||
SELECT MIN(lu.last_updated) FROM feed_last_updated lu
|
||||
|
||||
INNER JOIN feed_group_subscription_join fgs
|
||||
ON fgs.subscription_id = lu.subscription_id AND fgs.group_id = :groupId
|
||||
""")
|
||||
abstract fun oldestSubscriptionUpdate(groupId: Long): Flowable<List<Date>>
|
||||
"""
|
||||
)
|
||||
abstract fun oldestSubscriptionUpdate(groupId: Long): Flowable<List<OffsetDateTime>>
|
||||
|
||||
@Query("SELECT MIN(last_updated) FROM feed_last_updated")
|
||||
abstract fun oldestSubscriptionUpdateFromAll(): Flowable<List<Date>>
|
||||
abstract fun oldestSubscriptionUpdateFromAll(): Flowable<List<OffsetDateTime>>
|
||||
|
||||
@Query("SELECT COUNT(*) FROM feed_last_updated WHERE last_updated IS NULL")
|
||||
abstract fun notLoadedCount(): Flowable<Long>
|
||||
|
||||
@Query("""
|
||||
@Query(
|
||||
"""
|
||||
SELECT COUNT(*) FROM subscriptions s
|
||||
|
||||
INNER JOIN feed_group_subscription_join fgs
|
||||
@@ -124,20 +135,24 @@ abstract class FeedDAO {
|
||||
ON s.uid = lu.subscription_id
|
||||
|
||||
WHERE lu.last_updated IS NULL
|
||||
""")
|
||||
"""
|
||||
)
|
||||
abstract fun notLoadedCountForGroup(groupId: Long): Flowable<Long>
|
||||
|
||||
@Query("""
|
||||
@Query(
|
||||
"""
|
||||
SELECT s.* FROM subscriptions s
|
||||
|
||||
LEFT JOIN feed_last_updated lu
|
||||
ON s.uid = lu.subscription_id
|
||||
|
||||
WHERE lu.last_updated IS NULL OR lu.last_updated < :outdatedThreshold
|
||||
""")
|
||||
abstract fun getAllOutdated(outdatedThreshold: Date): Flowable<List<SubscriptionEntity>>
|
||||
"""
|
||||
)
|
||||
abstract fun getAllOutdated(outdatedThreshold: OffsetDateTime): Flowable<List<SubscriptionEntity>>
|
||||
|
||||
@Query("""
|
||||
@Query(
|
||||
"""
|
||||
SELECT s.* FROM subscriptions s
|
||||
|
||||
INNER JOIN feed_group_subscription_join fgs
|
||||
@@ -147,6 +162,7 @@ abstract class FeedDAO {
|
||||
ON s.uid = lu.subscription_id
|
||||
|
||||
WHERE lu.last_updated IS NULL OR lu.last_updated < :outdatedThreshold
|
||||
""")
|
||||
abstract fun getAllOutdatedForGroup(groupId: Long, outdatedThreshold: Date): Flowable<List<SubscriptionEntity>>
|
||||
"""
|
||||
)
|
||||
abstract fun getAllOutdatedForGroup(groupId: Long, outdatedThreshold: OffsetDateTime): Flowable<List<SubscriptionEntity>>
|
||||
}
|
||||
|
||||
@@ -6,8 +6,8 @@ import androidx.room.OnConflictStrategy
|
||||
import androidx.room.Query
|
||||
import androidx.room.Transaction
|
||||
import androidx.room.Update
|
||||
import io.reactivex.Flowable
|
||||
import io.reactivex.Maybe
|
||||
import io.reactivex.rxjava3.core.Flowable
|
||||
import io.reactivex.rxjava3.core.Maybe
|
||||
import org.schabi.newpipe.database.feed.model.FeedGroupEntity
|
||||
import org.schabi.newpipe.database.feed.model.FeedGroupSubscriptionEntity
|
||||
|
||||
|
||||
@@ -10,21 +10,24 @@ import org.schabi.newpipe.database.feed.model.FeedEntity.Companion.SUBSCRIPTION_
|
||||
import org.schabi.newpipe.database.stream.model.StreamEntity
|
||||
import org.schabi.newpipe.database.subscription.SubscriptionEntity
|
||||
|
||||
@Entity(tableName = FEED_TABLE,
|
||||
primaryKeys = [STREAM_ID, SUBSCRIPTION_ID],
|
||||
indices = [Index(SUBSCRIPTION_ID)],
|
||||
foreignKeys = [
|
||||
ForeignKey(
|
||||
entity = StreamEntity::class,
|
||||
parentColumns = [StreamEntity.STREAM_ID],
|
||||
childColumns = [STREAM_ID],
|
||||
onDelete = ForeignKey.CASCADE, onUpdate = ForeignKey.CASCADE, deferred = true),
|
||||
ForeignKey(
|
||||
entity = SubscriptionEntity::class,
|
||||
parentColumns = [SubscriptionEntity.SUBSCRIPTION_UID],
|
||||
childColumns = [SUBSCRIPTION_ID],
|
||||
onDelete = ForeignKey.CASCADE, onUpdate = ForeignKey.CASCADE, deferred = true)
|
||||
]
|
||||
@Entity(
|
||||
tableName = FEED_TABLE,
|
||||
primaryKeys = [STREAM_ID, SUBSCRIPTION_ID],
|
||||
indices = [Index(SUBSCRIPTION_ID)],
|
||||
foreignKeys = [
|
||||
ForeignKey(
|
||||
entity = StreamEntity::class,
|
||||
parentColumns = [StreamEntity.STREAM_ID],
|
||||
childColumns = [STREAM_ID],
|
||||
onDelete = ForeignKey.CASCADE, onUpdate = ForeignKey.CASCADE, deferred = true
|
||||
),
|
||||
ForeignKey(
|
||||
entity = SubscriptionEntity::class,
|
||||
parentColumns = [SubscriptionEntity.SUBSCRIPTION_UID],
|
||||
childColumns = [SUBSCRIPTION_ID],
|
||||
onDelete = ForeignKey.CASCADE, onUpdate = ForeignKey.CASCADE, deferred = true
|
||||
)
|
||||
]
|
||||
)
|
||||
data class FeedEntity(
|
||||
@ColumnInfo(name = STREAM_ID)
|
||||
|
||||
@@ -9,8 +9,8 @@ import org.schabi.newpipe.database.feed.model.FeedGroupEntity.Companion.SORT_ORD
|
||||
import org.schabi.newpipe.local.subscription.FeedGroupIcon
|
||||
|
||||
@Entity(
|
||||
tableName = FEED_GROUP_TABLE,
|
||||
indices = [Index(SORT_ORDER)]
|
||||
tableName = FEED_GROUP_TABLE,
|
||||
indices = [Index(SORT_ORDER)]
|
||||
)
|
||||
data class FeedGroupEntity(
|
||||
@PrimaryKey(autoGenerate = true)
|
||||
|
||||
@@ -11,22 +11,24 @@ import org.schabi.newpipe.database.feed.model.FeedGroupSubscriptionEntity.Compan
|
||||
import org.schabi.newpipe.database.subscription.SubscriptionEntity
|
||||
|
||||
@Entity(
|
||||
tableName = FEED_GROUP_SUBSCRIPTION_TABLE,
|
||||
primaryKeys = [GROUP_ID, SUBSCRIPTION_ID],
|
||||
indices = [Index(SUBSCRIPTION_ID)],
|
||||
foreignKeys = [
|
||||
ForeignKey(
|
||||
entity = FeedGroupEntity::class,
|
||||
parentColumns = [FeedGroupEntity.ID],
|
||||
childColumns = [GROUP_ID],
|
||||
onDelete = CASCADE, onUpdate = CASCADE, deferred = true),
|
||||
tableName = FEED_GROUP_SUBSCRIPTION_TABLE,
|
||||
primaryKeys = [GROUP_ID, SUBSCRIPTION_ID],
|
||||
indices = [Index(SUBSCRIPTION_ID)],
|
||||
foreignKeys = [
|
||||
ForeignKey(
|
||||
entity = FeedGroupEntity::class,
|
||||
parentColumns = [FeedGroupEntity.ID],
|
||||
childColumns = [GROUP_ID],
|
||||
onDelete = CASCADE, onUpdate = CASCADE, deferred = true
|
||||
),
|
||||
|
||||
ForeignKey(
|
||||
entity = SubscriptionEntity::class,
|
||||
parentColumns = [SubscriptionEntity.SUBSCRIPTION_UID],
|
||||
childColumns = [SUBSCRIPTION_ID],
|
||||
onDelete = CASCADE, onUpdate = CASCADE, deferred = true)
|
||||
]
|
||||
ForeignKey(
|
||||
entity = SubscriptionEntity::class,
|
||||
parentColumns = [SubscriptionEntity.SUBSCRIPTION_UID],
|
||||
childColumns = [SUBSCRIPTION_ID],
|
||||
onDelete = CASCADE, onUpdate = CASCADE, deferred = true
|
||||
)
|
||||
]
|
||||
)
|
||||
data class FeedGroupSubscriptionEntity(
|
||||
@ColumnInfo(name = GROUP_ID)
|
||||
|
||||
@@ -4,20 +4,21 @@ import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.ForeignKey
|
||||
import androidx.room.PrimaryKey
|
||||
import java.util.Date
|
||||
import org.schabi.newpipe.database.feed.model.FeedLastUpdatedEntity.Companion.FEED_LAST_UPDATED_TABLE
|
||||
import org.schabi.newpipe.database.feed.model.FeedLastUpdatedEntity.Companion.SUBSCRIPTION_ID
|
||||
import org.schabi.newpipe.database.subscription.SubscriptionEntity
|
||||
import java.time.OffsetDateTime
|
||||
|
||||
@Entity(
|
||||
tableName = FEED_LAST_UPDATED_TABLE,
|
||||
foreignKeys = [
|
||||
ForeignKey(
|
||||
entity = SubscriptionEntity::class,
|
||||
parentColumns = [SubscriptionEntity.SUBSCRIPTION_UID],
|
||||
childColumns = [SUBSCRIPTION_ID],
|
||||
onDelete = ForeignKey.CASCADE, onUpdate = ForeignKey.CASCADE, deferred = true)
|
||||
]
|
||||
tableName = FEED_LAST_UPDATED_TABLE,
|
||||
foreignKeys = [
|
||||
ForeignKey(
|
||||
entity = SubscriptionEntity::class,
|
||||
parentColumns = [SubscriptionEntity.SUBSCRIPTION_UID],
|
||||
childColumns = [SUBSCRIPTION_ID],
|
||||
onDelete = ForeignKey.CASCADE, onUpdate = ForeignKey.CASCADE, deferred = true
|
||||
)
|
||||
]
|
||||
)
|
||||
data class FeedLastUpdatedEntity(
|
||||
@PrimaryKey
|
||||
@@ -25,9 +26,8 @@ data class FeedLastUpdatedEntity(
|
||||
var subscriptionId: Long,
|
||||
|
||||
@ColumnInfo(name = LAST_UPDATED)
|
||||
var lastUpdated: Date? = null
|
||||
var lastUpdated: OffsetDateTime? = null
|
||||
) {
|
||||
|
||||
companion object {
|
||||
const val FEED_LAST_UPDATED_TABLE = "feed_last_updated"
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import org.schabi.newpipe.database.history.model.SearchHistoryEntry;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import io.reactivex.Flowable;
|
||||
import io.reactivex.rxjava3.core.Flowable;
|
||||
|
||||
import static org.schabi.newpipe.database.history.model.SearchHistoryEntry.CREATION_DATE;
|
||||
import static org.schabi.newpipe.database.history.model.SearchHistoryEntry.ID;
|
||||
|
||||
@@ -10,7 +10,7 @@ import org.schabi.newpipe.database.stream.StreamStatisticsEntry;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import io.reactivex.Flowable;
|
||||
import io.reactivex.rxjava3.core.Flowable;
|
||||
|
||||
import static org.schabi.newpipe.database.history.model.StreamHistoryEntity.JOIN_STREAM_ID;
|
||||
import static org.schabi.newpipe.database.history.model.StreamHistoryEntity.STREAM_ACCESS_DATE;
|
||||
@@ -20,6 +20,9 @@ import static org.schabi.newpipe.database.stream.StreamStatisticsEntry.STREAM_LA
|
||||
import static org.schabi.newpipe.database.stream.StreamStatisticsEntry.STREAM_WATCH_COUNT;
|
||||
import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_ID;
|
||||
import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_TABLE;
|
||||
import static org.schabi.newpipe.database.stream.model.StreamStateEntity.JOIN_STREAM_ID_ALIAS;
|
||||
import static org.schabi.newpipe.database.stream.model.StreamStateEntity.STREAM_PROGRESS_TIME;
|
||||
import static org.schabi.newpipe.database.stream.model.StreamStateEntity.STREAM_STATE_TABLE;
|
||||
|
||||
@Dao
|
||||
public abstract class StreamHistoryDAO implements HistoryDAO<StreamHistoryEntity> {
|
||||
@@ -73,6 +76,12 @@ public abstract class StreamHistoryDAO implements HistoryDAO<StreamHistoryEntity
|
||||
+ " SUM(" + STREAM_REPEAT_COUNT + ") AS " + STREAM_WATCH_COUNT
|
||||
+ " FROM " + STREAM_HISTORY_TABLE + " GROUP BY " + JOIN_STREAM_ID + ")"
|
||||
|
||||
+ " ON " + STREAM_ID + " = " + JOIN_STREAM_ID)
|
||||
+ " ON " + STREAM_ID + " = " + JOIN_STREAM_ID
|
||||
|
||||
+ " LEFT JOIN "
|
||||
+ "(SELECT " + JOIN_STREAM_ID + " AS " + JOIN_STREAM_ID_ALIAS + ", "
|
||||
+ STREAM_PROGRESS_TIME
|
||||
+ " FROM " + STREAM_STATE_TABLE + " )"
|
||||
+ " ON " + STREAM_ID + " = " + JOIN_STREAM_ID_ALIAS)
|
||||
public abstract Flowable<List<StreamStatisticsEntry>> getStatistics();
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import androidx.room.Ignore;
|
||||
import androidx.room.Index;
|
||||
import androidx.room.PrimaryKey;
|
||||
|
||||
import java.util.Date;
|
||||
import java.time.OffsetDateTime;
|
||||
|
||||
import static org.schabi.newpipe.database.history.model.SearchHistoryEntry.SEARCH;
|
||||
|
||||
@@ -24,7 +24,7 @@ public class SearchHistoryEntry {
|
||||
private long id;
|
||||
|
||||
@ColumnInfo(name = CREATION_DATE)
|
||||
private Date creationDate;
|
||||
private OffsetDateTime creationDate;
|
||||
|
||||
@ColumnInfo(name = SERVICE_ID)
|
||||
private int serviceId;
|
||||
@@ -32,7 +32,8 @@ public class SearchHistoryEntry {
|
||||
@ColumnInfo(name = SEARCH)
|
||||
private String search;
|
||||
|
||||
public SearchHistoryEntry(final Date creationDate, final int serviceId, final String search) {
|
||||
public SearchHistoryEntry(final OffsetDateTime creationDate, final int serviceId,
|
||||
final String search) {
|
||||
this.serviceId = serviceId;
|
||||
this.creationDate = creationDate;
|
||||
this.search = search;
|
||||
@@ -46,11 +47,11 @@ public class SearchHistoryEntry {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public Date getCreationDate() {
|
||||
public OffsetDateTime getCreationDate() {
|
||||
return creationDate;
|
||||
}
|
||||
|
||||
public void setCreationDate(final Date creationDate) {
|
||||
public void setCreationDate(final OffsetDateTime creationDate) {
|
||||
this.creationDate = creationDate;
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ import androidx.room.Index;
|
||||
|
||||
import org.schabi.newpipe.database.stream.model.StreamEntity;
|
||||
|
||||
import java.util.Date;
|
||||
import java.time.OffsetDateTime;
|
||||
|
||||
import static androidx.room.ForeignKey.CASCADE;
|
||||
import static org.schabi.newpipe.database.history.model.StreamHistoryEntity.JOIN_STREAM_ID;
|
||||
@@ -37,12 +37,12 @@ public class StreamHistoryEntity {
|
||||
|
||||
@NonNull
|
||||
@ColumnInfo(name = STREAM_ACCESS_DATE)
|
||||
private Date accessDate;
|
||||
private OffsetDateTime accessDate;
|
||||
|
||||
@ColumnInfo(name = STREAM_REPEAT_COUNT)
|
||||
private long repeatCount;
|
||||
|
||||
public StreamHistoryEntity(final long streamUid, @NonNull final Date accessDate,
|
||||
public StreamHistoryEntity(final long streamUid, @NonNull final OffsetDateTime accessDate,
|
||||
final long repeatCount) {
|
||||
this.streamUid = streamUid;
|
||||
this.accessDate = accessDate;
|
||||
@@ -50,7 +50,7 @@ public class StreamHistoryEntity {
|
||||
}
|
||||
|
||||
@Ignore
|
||||
public StreamHistoryEntity(final long streamUid, @NonNull final Date accessDate) {
|
||||
public StreamHistoryEntity(final long streamUid, @NonNull final OffsetDateTime accessDate) {
|
||||
this(streamUid, accessDate, 1);
|
||||
}
|
||||
|
||||
@@ -62,11 +62,12 @@ public class StreamHistoryEntity {
|
||||
this.streamUid = streamUid;
|
||||
}
|
||||
|
||||
public Date getAccessDate() {
|
||||
@NonNull
|
||||
public OffsetDateTime getAccessDate() {
|
||||
return accessDate;
|
||||
}
|
||||
|
||||
public void setAccessDate(@NonNull final Date accessDate) {
|
||||
public void setAccessDate(@NonNull final OffsetDateTime accessDate) {
|
||||
this.accessDate = accessDate;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@ package org.schabi.newpipe.database.history.model
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Embedded
|
||||
import java.util.Date
|
||||
import org.schabi.newpipe.database.stream.model.StreamEntity
|
||||
import java.time.OffsetDateTime
|
||||
|
||||
data class StreamHistoryEntry(
|
||||
@Embedded
|
||||
@@ -13,7 +13,7 @@ data class StreamHistoryEntry(
|
||||
val streamId: Long,
|
||||
|
||||
@ColumnInfo(name = StreamHistoryEntity.STREAM_ACCESS_DATE)
|
||||
val accessDate: Date,
|
||||
val accessDate: OffsetDateTime,
|
||||
|
||||
@ColumnInfo(name = StreamHistoryEntity.STREAM_REPEAT_COUNT)
|
||||
val repeatCount: Long
|
||||
@@ -25,6 +25,6 @@ data class StreamHistoryEntry(
|
||||
|
||||
fun hasEqualValues(other: StreamHistoryEntry): Boolean {
|
||||
return this.streamEntity.uid == other.streamEntity.uid && streamId == other.streamId &&
|
||||
accessDate.compareTo(other.accessDate) == 0
|
||||
accessDate.isEqual(other.accessDate)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
|
||||
public interface PlaylistLocalItem extends LocalItem {
|
||||
@@ -18,15 +19,8 @@ public interface PlaylistLocalItem extends LocalItem {
|
||||
items.addAll(localPlaylists);
|
||||
items.addAll(remotePlaylists);
|
||||
|
||||
Collections.sort(items, (left, right) -> {
|
||||
final String on1 = left.getOrderingName();
|
||||
final String on2 = right.getOrderingName();
|
||||
if (on1 == null) {
|
||||
return on2 == null ? 0 : 1;
|
||||
} else {
|
||||
return on2 == null ? -1 : on1.compareToIgnoreCase(on2);
|
||||
}
|
||||
});
|
||||
Collections.sort(items, Comparator.comparing(PlaylistLocalItem::getOrderingName,
|
||||
Comparator.nullsLast(String.CASE_INSENSITIVE_ORDER)));
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
@@ -5,12 +5,16 @@ import androidx.room.Embedded
|
||||
import org.schabi.newpipe.database.LocalItem
|
||||
import org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity
|
||||
import org.schabi.newpipe.database.stream.model.StreamEntity
|
||||
import org.schabi.newpipe.database.stream.model.StreamStateEntity
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem
|
||||
|
||||
class PlaylistStreamEntry(
|
||||
data class PlaylistStreamEntry(
|
||||
@Embedded
|
||||
val streamEntity: StreamEntity,
|
||||
|
||||
@ColumnInfo(name = StreamStateEntity.STREAM_PROGRESS_TIME, defaultValue = "0")
|
||||
val progressTime: Long,
|
||||
|
||||
@ColumnInfo(name = PlaylistStreamEntity.JOIN_STREAM_ID)
|
||||
val streamId: Long,
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import org.schabi.newpipe.database.playlist.model.PlaylistEntity;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import io.reactivex.Flowable;
|
||||
import io.reactivex.rxjava3.core.Flowable;
|
||||
|
||||
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_ID;
|
||||
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_TABLE;
|
||||
@@ -33,4 +33,7 @@ public abstract class PlaylistDAO implements BasicDAO<PlaylistEntity> {
|
||||
|
||||
@Query("DELETE FROM " + PLAYLIST_TABLE + " WHERE " + PLAYLIST_ID + " = :playlistId")
|
||||
public abstract int deletePlaylist(long playlistId);
|
||||
|
||||
@Query("SELECT COUNT(*) FROM " + PLAYLIST_TABLE)
|
||||
public abstract Flowable<Long> getCount();
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import io.reactivex.Flowable;
|
||||
import io.reactivex.rxjava3.core.Flowable;
|
||||
|
||||
import static org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity.REMOTE_PLAYLIST_ID;
|
||||
import static org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity.REMOTE_PLAYLIST_SERVICE_ID;
|
||||
|
||||
@@ -11,7 +11,7 @@ import org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import io.reactivex.Flowable;
|
||||
import io.reactivex.rxjava3.core.Flowable;
|
||||
|
||||
import static org.schabi.newpipe.database.playlist.PlaylistMetadataEntry.PLAYLIST_STREAM_COUNT;
|
||||
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_ID;
|
||||
@@ -24,6 +24,9 @@ import static org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity.JO
|
||||
import static org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity.PLAYLIST_STREAM_JOIN_TABLE;
|
||||
import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_ID;
|
||||
import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_TABLE;
|
||||
import static org.schabi.newpipe.database.stream.model.StreamStateEntity.JOIN_STREAM_ID_ALIAS;
|
||||
import static org.schabi.newpipe.database.stream.model.StreamStateEntity.STREAM_PROGRESS_TIME;
|
||||
import static org.schabi.newpipe.database.stream.model.StreamStateEntity.STREAM_STATE_TABLE;
|
||||
|
||||
@Dao
|
||||
public abstract class PlaylistStreamDAO implements BasicDAO<PlaylistStreamEntity> {
|
||||
@@ -58,6 +61,13 @@ public abstract class PlaylistStreamDAO implements BasicDAO<PlaylistStreamEntity
|
||||
|
||||
// then merge with the stream metadata
|
||||
+ " ON " + STREAM_ID + " = " + JOIN_STREAM_ID
|
||||
|
||||
+ " LEFT JOIN "
|
||||
+ "(SELECT " + JOIN_STREAM_ID + " AS " + JOIN_STREAM_ID_ALIAS + ", "
|
||||
+ STREAM_PROGRESS_TIME
|
||||
+ " FROM " + STREAM_STATE_TABLE + " )"
|
||||
+ " ON " + STREAM_ID + " = " + JOIN_STREAM_ID_ALIAS
|
||||
|
||||
+ " ORDER BY " + JOIN_INDEX + " ASC")
|
||||
public abstract Flowable<List<PlaylistStreamEntry>> getOrderedStreamsOf(long playlistId);
|
||||
|
||||
|
||||
@@ -2,26 +2,29 @@ package org.schabi.newpipe.database.stream
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Embedded
|
||||
import java.util.Date
|
||||
import org.schabi.newpipe.database.LocalItem
|
||||
import org.schabi.newpipe.database.history.model.StreamHistoryEntity
|
||||
import org.schabi.newpipe.database.stream.model.StreamEntity
|
||||
import org.schabi.newpipe.database.stream.model.StreamStateEntity.STREAM_PROGRESS_TIME
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem
|
||||
import java.time.OffsetDateTime
|
||||
|
||||
class StreamStatisticsEntry(
|
||||
@Embedded
|
||||
val streamEntity: StreamEntity,
|
||||
|
||||
@ColumnInfo(name = STREAM_PROGRESS_TIME, defaultValue = "0")
|
||||
val progressTime: Long,
|
||||
|
||||
@ColumnInfo(name = StreamHistoryEntity.JOIN_STREAM_ID)
|
||||
val streamId: Long,
|
||||
|
||||
@ColumnInfo(name = STREAM_LATEST_DATE)
|
||||
val latestAccessDate: Date,
|
||||
val latestAccessDate: OffsetDateTime,
|
||||
|
||||
@ColumnInfo(name = STREAM_WATCH_COUNT)
|
||||
val watchCount: Long
|
||||
) : LocalItem {
|
||||
|
||||
fun toStreamInfoItem(): StreamInfoItem {
|
||||
val item = StreamInfoItem(streamEntity.serviceId, streamEntity.url, streamEntity.title, streamEntity.streamType)
|
||||
item.duration = streamEntity.duration
|
||||
|
||||
@@ -6,14 +6,14 @@ import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.Query
|
||||
import androidx.room.Transaction
|
||||
import io.reactivex.Flowable
|
||||
import java.util.Date
|
||||
import io.reactivex.rxjava3.core.Flowable
|
||||
import org.schabi.newpipe.database.BasicDAO
|
||||
import org.schabi.newpipe.database.stream.model.StreamEntity
|
||||
import org.schabi.newpipe.database.stream.model.StreamEntity.Companion.STREAM_ID
|
||||
import org.schabi.newpipe.extractor.stream.StreamType
|
||||
import org.schabi.newpipe.extractor.stream.StreamType.AUDIO_LIVE_STREAM
|
||||
import org.schabi.newpipe.extractor.stream.StreamType.LIVE_STREAM
|
||||
import java.time.OffsetDateTime
|
||||
|
||||
@Dao
|
||||
abstract class StreamDAO : BasicDAO<StreamEntity> {
|
||||
@@ -35,10 +35,12 @@ abstract class StreamDAO : BasicDAO<StreamEntity> {
|
||||
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
||||
internal abstract fun silentInsertAllInternal(streams: List<StreamEntity>): List<Long>
|
||||
|
||||
@Query("""
|
||||
@Query(
|
||||
"""
|
||||
SELECT uid, stream_type, textual_upload_date, upload_date, is_upload_date_approximation, duration
|
||||
FROM streams WHERE url = :url AND service_id = :serviceId
|
||||
""")
|
||||
"""
|
||||
)
|
||||
internal abstract fun getMinimalStreamForCompare(serviceId: Int, url: String): StreamCompareFeed?
|
||||
|
||||
@Transaction
|
||||
@@ -79,7 +81,7 @@ abstract class StreamDAO : BasicDAO<StreamEntity> {
|
||||
|
||||
private fun compareAndUpdateStream(newerStream: StreamEntity) {
|
||||
val existentMinimalStream = getMinimalStreamForCompare(newerStream.serviceId, newerStream.url)
|
||||
?: throw IllegalStateException("Stream cannot be null just after insertion.")
|
||||
?: throw IllegalStateException("Stream cannot be null just after insertion.")
|
||||
newerStream.uid = existentMinimalStream.uid
|
||||
|
||||
val isNewerStreamLive = newerStream.streamType == AUDIO_LIVE_STREAM || newerStream.streamType == LIVE_STREAM
|
||||
@@ -88,7 +90,7 @@ abstract class StreamDAO : BasicDAO<StreamEntity> {
|
||||
// Use the existent upload date if the newer stream does not have a better precision
|
||||
// (i.e. is an approximation). This is done to prevent unnecessary changes.
|
||||
val hasBetterPrecision =
|
||||
newerStream.uploadDate != null && newerStream.isUploadDateApproximation != true
|
||||
newerStream.uploadDate != null && newerStream.isUploadDateApproximation != true
|
||||
if (existentMinimalStream.uploadDate != null && !hasBetterPrecision) {
|
||||
newerStream.uploadDate = existentMinimalStream.uploadDate
|
||||
newerStream.textualUploadDate = existentMinimalStream.textualUploadDate
|
||||
@@ -101,7 +103,8 @@ abstract class StreamDAO : BasicDAO<StreamEntity> {
|
||||
}
|
||||
}
|
||||
|
||||
@Query("""
|
||||
@Query(
|
||||
"""
|
||||
DELETE FROM streams WHERE
|
||||
|
||||
NOT EXISTS (SELECT 1 FROM stream_history sh
|
||||
@@ -112,7 +115,8 @@ abstract class StreamDAO : BasicDAO<StreamEntity> {
|
||||
|
||||
AND NOT EXISTS (SELECT 1 FROM feed f
|
||||
WHERE f.stream_id = streams.uid)
|
||||
""")
|
||||
"""
|
||||
)
|
||||
abstract fun deleteOrphans(): Int
|
||||
|
||||
/**
|
||||
@@ -129,7 +133,7 @@ abstract class StreamDAO : BasicDAO<StreamEntity> {
|
||||
var textualUploadDate: String? = null,
|
||||
|
||||
@ColumnInfo(name = StreamEntity.STREAM_UPLOAD_DATE)
|
||||
var uploadDate: Date? = null,
|
||||
var uploadDate: OffsetDateTime? = null,
|
||||
|
||||
@ColumnInfo(name = StreamEntity.STREAM_IS_UPLOAD_DATE_APPROXIMATION)
|
||||
var isUploadDateApproximation: Boolean? = null,
|
||||
|
||||
@@ -11,7 +11,7 @@ import org.schabi.newpipe.database.stream.model.StreamStateEntity;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import io.reactivex.Flowable;
|
||||
import io.reactivex.rxjava3.core.Flowable;
|
||||
|
||||
import static org.schabi.newpipe.database.stream.model.StreamStateEntity.JOIN_STREAM_ID;
|
||||
import static org.schabi.newpipe.database.stream.model.StreamStateEntity.STREAM_STATE_TABLE;
|
||||
|
||||
@@ -5,9 +5,6 @@ import androidx.room.Entity
|
||||
import androidx.room.Ignore
|
||||
import androidx.room.Index
|
||||
import androidx.room.PrimaryKey
|
||||
import java.io.Serializable
|
||||
import java.util.Calendar
|
||||
import java.util.Date
|
||||
import org.schabi.newpipe.database.stream.model.StreamEntity.Companion.STREAM_SERVICE_ID
|
||||
import org.schabi.newpipe.database.stream.model.StreamEntity.Companion.STREAM_TABLE
|
||||
import org.schabi.newpipe.database.stream.model.StreamEntity.Companion.STREAM_URL
|
||||
@@ -16,11 +13,14 @@ import org.schabi.newpipe.extractor.stream.StreamInfo
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem
|
||||
import org.schabi.newpipe.extractor.stream.StreamType
|
||||
import org.schabi.newpipe.player.playqueue.PlayQueueItem
|
||||
import java.io.Serializable
|
||||
import java.time.OffsetDateTime
|
||||
|
||||
@Entity(tableName = STREAM_TABLE,
|
||||
indices = [
|
||||
Index(value = [STREAM_SERVICE_ID, STREAM_URL], unique = true)
|
||||
]
|
||||
@Entity(
|
||||
tableName = STREAM_TABLE,
|
||||
indices = [
|
||||
Index(value = [STREAM_SERVICE_ID, STREAM_URL], unique = true)
|
||||
]
|
||||
)
|
||||
data class StreamEntity(
|
||||
@PrimaryKey(autoGenerate = true)
|
||||
@@ -55,35 +55,34 @@ data class StreamEntity(
|
||||
var textualUploadDate: String? = null,
|
||||
|
||||
@ColumnInfo(name = STREAM_UPLOAD_DATE)
|
||||
var uploadDate: Date? = null,
|
||||
var uploadDate: OffsetDateTime? = null,
|
||||
|
||||
@ColumnInfo(name = STREAM_IS_UPLOAD_DATE_APPROXIMATION)
|
||||
var isUploadDateApproximation: Boolean? = null
|
||||
) : Serializable {
|
||||
|
||||
@Ignore
|
||||
constructor(item: StreamInfoItem) : this(
|
||||
serviceId = item.serviceId, url = item.url, title = item.name,
|
||||
streamType = item.streamType, duration = item.duration, uploader = item.uploaderName,
|
||||
thumbnailUrl = item.thumbnailUrl, viewCount = item.viewCount,
|
||||
textualUploadDate = item.textualUploadDate, uploadDate = item.uploadDate?.date()?.time,
|
||||
isUploadDateApproximation = item.uploadDate?.isApproximation
|
||||
serviceId = item.serviceId, url = item.url, title = item.name,
|
||||
streamType = item.streamType, duration = item.duration, uploader = item.uploaderName,
|
||||
thumbnailUrl = item.thumbnailUrl, viewCount = item.viewCount,
|
||||
textualUploadDate = item.textualUploadDate, uploadDate = item.uploadDate?.offsetDateTime(),
|
||||
isUploadDateApproximation = item.uploadDate?.isApproximation
|
||||
)
|
||||
|
||||
@Ignore
|
||||
constructor(info: StreamInfo) : this(
|
||||
serviceId = info.serviceId, url = info.url, title = info.name,
|
||||
streamType = info.streamType, duration = info.duration, uploader = info.uploaderName,
|
||||
thumbnailUrl = info.thumbnailUrl, viewCount = info.viewCount,
|
||||
textualUploadDate = info.textualUploadDate, uploadDate = info.uploadDate?.date()?.time,
|
||||
isUploadDateApproximation = info.uploadDate?.isApproximation
|
||||
serviceId = info.serviceId, url = info.url, title = info.name,
|
||||
streamType = info.streamType, duration = info.duration, uploader = info.uploaderName,
|
||||
thumbnailUrl = info.thumbnailUrl, viewCount = info.viewCount,
|
||||
textualUploadDate = info.textualUploadDate, uploadDate = info.uploadDate?.offsetDateTime(),
|
||||
isUploadDateApproximation = info.uploadDate?.isApproximation
|
||||
)
|
||||
|
||||
@Ignore
|
||||
constructor(item: PlayQueueItem) : this(
|
||||
serviceId = item.serviceId, url = item.url, title = item.title,
|
||||
streamType = item.streamType, duration = item.duration, uploader = item.uploader,
|
||||
thumbnailUrl = item.thumbnailUrl
|
||||
serviceId = item.serviceId, url = item.url, title = item.title,
|
||||
streamType = item.streamType, duration = item.duration, uploader = item.uploader,
|
||||
thumbnailUrl = item.thumbnailUrl
|
||||
)
|
||||
|
||||
fun toStreamInfoItem(): StreamInfoItem {
|
||||
@@ -95,8 +94,7 @@ data class StreamEntity(
|
||||
if (viewCount != null) item.viewCount = viewCount as Long
|
||||
item.textualUploadDate = textualUploadDate
|
||||
item.uploadDate = uploadDate?.let {
|
||||
DateWrapper(Calendar.getInstance().apply { time = it }, isUploadDateApproximation
|
||||
?: false)
|
||||
DateWrapper(it, isUploadDateApproximation ?: false)
|
||||
}
|
||||
|
||||
return item
|
||||
|
||||
@@ -22,6 +22,9 @@ import static org.schabi.newpipe.database.stream.model.StreamStateEntity.STREAM_
|
||||
public class StreamStateEntity {
|
||||
public static final String STREAM_STATE_TABLE = "stream_state";
|
||||
public static final String JOIN_STREAM_ID = "stream_id";
|
||||
// This additional field is required for the SQL query because 'stream_id' is used
|
||||
// for some other joins already
|
||||
public static final String JOIN_STREAM_ID_ALIAS = "stream_id_alias";
|
||||
public static final String STREAM_PROGRESS_TIME = "progress_time";
|
||||
|
||||
/**
|
||||
|
||||
@@ -5,8 +5,8 @@ import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.Query
|
||||
import androidx.room.Transaction
|
||||
import io.reactivex.Flowable
|
||||
import io.reactivex.Maybe
|
||||
import io.reactivex.rxjava3.core.Flowable
|
||||
import io.reactivex.rxjava3.core.Maybe
|
||||
import org.schabi.newpipe.database.BasicDAO
|
||||
|
||||
@Dao
|
||||
@@ -20,16 +20,19 @@ abstract class SubscriptionDAO : BasicDAO<SubscriptionEntity> {
|
||||
@Query("SELECT * FROM subscriptions ORDER BY name COLLATE NOCASE ASC")
|
||||
abstract override fun getAll(): Flowable<List<SubscriptionEntity>>
|
||||
|
||||
@Query("""
|
||||
@Query(
|
||||
"""
|
||||
SELECT * FROM subscriptions
|
||||
|
||||
WHERE name LIKE '%' || :filter || '%'
|
||||
|
||||
ORDER BY name COLLATE NOCASE ASC
|
||||
""")
|
||||
"""
|
||||
)
|
||||
abstract fun getSubscriptionsFiltered(filter: String): Flowable<List<SubscriptionEntity>>
|
||||
|
||||
@Query("""
|
||||
@Query(
|
||||
"""
|
||||
SELECT * FROM subscriptions s
|
||||
|
||||
LEFT JOIN feed_group_subscription_join fgs
|
||||
@@ -38,12 +41,14 @@ abstract class SubscriptionDAO : BasicDAO<SubscriptionEntity> {
|
||||
WHERE (fgs.subscription_id IS NULL OR fgs.group_id = :currentGroupId)
|
||||
|
||||
ORDER BY name COLLATE NOCASE ASC
|
||||
""")
|
||||
"""
|
||||
)
|
||||
abstract fun getSubscriptionsOnlyUngrouped(
|
||||
currentGroupId: Long
|
||||
): Flowable<List<SubscriptionEntity>>
|
||||
|
||||
@Query("""
|
||||
@Query(
|
||||
"""
|
||||
SELECT * FROM subscriptions s
|
||||
|
||||
LEFT JOIN feed_group_subscription_join fgs
|
||||
@@ -53,7 +58,8 @@ abstract class SubscriptionDAO : BasicDAO<SubscriptionEntity> {
|
||||
AND s.name LIKE '%' || :filter || '%'
|
||||
|
||||
ORDER BY name COLLATE NOCASE ASC
|
||||
""")
|
||||
"""
|
||||
)
|
||||
abstract fun getSubscriptionsOnlyUngroupedFiltered(
|
||||
currentGroupId: Long,
|
||||
filter: String
|
||||
|
||||
@@ -50,7 +50,7 @@ public class SubscriptionEntity {
|
||||
|
||||
@Ignore
|
||||
public static SubscriptionEntity from(@NonNull final ChannelInfo info) {
|
||||
SubscriptionEntity result = new SubscriptionEntity();
|
||||
final SubscriptionEntity result = new SubscriptionEntity();
|
||||
result.setServiceId(info.getServiceId());
|
||||
result.setUrl(info.getUrl());
|
||||
result.setData(info.getName(), info.getAvatarUrl(), info.getDescription(),
|
||||
@@ -124,7 +124,7 @@ public class SubscriptionEntity {
|
||||
|
||||
@Ignore
|
||||
public ChannelInfoItem toChannelInfoItem() {
|
||||
ChannelInfoItem item = new ChannelInfoItem(getServiceId(), getUrl(), getName());
|
||||
final ChannelInfoItem item = new ChannelInfoItem(getServiceId(), getUrl(), getName());
|
||||
item.setThumbnailUrl(getAvatarUrl());
|
||||
item.setSubscriberCount(getSubscriberCount());
|
||||
item.setDescription(getDescription());
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package org.schabi.newpipe.download;
|
||||
|
||||
import android.app.FragmentTransaction;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.view.Menu;
|
||||
@@ -10,10 +9,11 @@ import android.view.ViewTreeObserver;
|
||||
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.fragment.app.FragmentTransaction;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.util.AndroidTvUtils;
|
||||
import org.schabi.newpipe.databinding.ActivityDownloaderBinding;
|
||||
import org.schabi.newpipe.util.DeviceUtils;
|
||||
import org.schabi.newpipe.util.ThemeHelper;
|
||||
import org.schabi.newpipe.views.FocusOverlayView;
|
||||
|
||||
@@ -29,19 +29,22 @@ public class DownloadActivity extends AppCompatActivity {
|
||||
@Override
|
||||
protected void onCreate(final Bundle savedInstanceState) {
|
||||
// Service
|
||||
Intent i = new Intent();
|
||||
final Intent i = new Intent();
|
||||
i.setClass(this, DownloadManagerService.class);
|
||||
startService(i);
|
||||
|
||||
assureCorrectAppLanguage(this);
|
||||
ThemeHelper.setTheme(this);
|
||||
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_downloader);
|
||||
|
||||
Toolbar toolbar = findViewById(R.id.toolbar);
|
||||
setSupportActionBar(toolbar);
|
||||
final ActivityDownloaderBinding downloaderBinding =
|
||||
ActivityDownloaderBinding.inflate(getLayoutInflater());
|
||||
setContentView(downloaderBinding.getRoot());
|
||||
|
||||
ActionBar actionBar = getSupportActionBar();
|
||||
setSupportActionBar(downloaderBinding.toolbarLayout.toolbar);
|
||||
|
||||
final ActionBar actionBar = getSupportActionBar();
|
||||
if (actionBar != null) {
|
||||
actionBar.setDisplayHomeAsUpEnabled(true);
|
||||
actionBar.setTitle(R.string.downloads_title);
|
||||
@@ -57,13 +60,13 @@ public class DownloadActivity extends AppCompatActivity {
|
||||
}
|
||||
});
|
||||
|
||||
if (AndroidTvUtils.isTv(this)) {
|
||||
if (DeviceUtils.isTv(this)) {
|
||||
FocusOverlayView.setupFocusObserver(this);
|
||||
}
|
||||
}
|
||||
|
||||
private void updateFragments() {
|
||||
MissionsFragment fragment = new MissionsFragment();
|
||||
final MissionsFragment fragment = new MissionsFragment();
|
||||
|
||||
getSupportFragmentManager().beginTransaction()
|
||||
.replace(R.id.frame, fragment, MISSIONS_FRAGMENT_TAG)
|
||||
@@ -74,7 +77,7 @@ public class DownloadActivity extends AppCompatActivity {
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(final Menu menu) {
|
||||
super.onCreateOptionsMenu(menu);
|
||||
MenuInflater inflater = getMenuInflater();
|
||||
final MenuInflater inflater = getMenuInflater();
|
||||
|
||||
inflater.inflate(R.menu.download_menu, menu);
|
||||
|
||||
|
||||
@@ -10,19 +10,14 @@ import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.os.Environment;
|
||||
import android.os.IBinder;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.util.Log;
|
||||
import android.util.SparseArray;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.AdapterView;
|
||||
import android.widget.EditText;
|
||||
import android.widget.RadioButton;
|
||||
import android.widget.RadioGroup;
|
||||
import android.widget.SeekBar;
|
||||
import android.widget.Spinner;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.IdRes;
|
||||
@@ -34,12 +29,17 @@ import androidx.appcompat.view.menu.ActionMenuItemView;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.documentfile.provider.DocumentFile;
|
||||
import androidx.fragment.app.DialogFragment;
|
||||
import androidx.preference.PreferenceManager;
|
||||
|
||||
import com.nononsenseapps.filepicker.Utils;
|
||||
|
||||
import org.schabi.newpipe.MainActivity;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.RouterActivity;
|
||||
import org.schabi.newpipe.databinding.DownloadDialogBinding;
|
||||
import org.schabi.newpipe.error.ErrorActivity;
|
||||
import org.schabi.newpipe.error.ErrorInfo;
|
||||
import org.schabi.newpipe.error.UserAction;
|
||||
import org.schabi.newpipe.extractor.MediaFormat;
|
||||
import org.schabi.newpipe.extractor.NewPipe;
|
||||
import org.schabi.newpipe.extractor.localization.Localization;
|
||||
@@ -48,8 +48,6 @@ import org.schabi.newpipe.extractor.stream.Stream;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||
import org.schabi.newpipe.extractor.stream.SubtitlesStream;
|
||||
import org.schabi.newpipe.extractor.stream.VideoStream;
|
||||
import org.schabi.newpipe.report.ErrorActivity;
|
||||
import org.schabi.newpipe.report.UserAction;
|
||||
import org.schabi.newpipe.settings.NewPipeSettings;
|
||||
import org.schabi.newpipe.util.FilePickerActivityHelper;
|
||||
import org.schabi.newpipe.util.FilenameUtils;
|
||||
@@ -63,13 +61,12 @@ import org.schabi.newpipe.util.ThemeHelper;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
import icepick.Icepick;
|
||||
import icepick.State;
|
||||
import io.reactivex.disposables.CompositeDisposable;
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
||||
import us.shandian.giga.get.MissionRecoveryInfo;
|
||||
import us.shandian.giga.io.StoredDirectoryHelper;
|
||||
import us.shandian.giga.io.StoredFileHelper;
|
||||
@@ -115,16 +112,12 @@ public class DownloadDialog extends DialogFragment
|
||||
|
||||
private final CompositeDisposable disposables = new CompositeDisposable();
|
||||
|
||||
private EditText nameEditText;
|
||||
private Spinner streamsSpinner;
|
||||
private RadioGroup radioStreamsGroup;
|
||||
private TextView threadsCountTextView;
|
||||
private SeekBar threadsSeekBar;
|
||||
private DownloadDialogBinding dialogBinding;
|
||||
|
||||
private SharedPreferences prefs;
|
||||
|
||||
public static DownloadDialog newInstance(final StreamInfo info) {
|
||||
DownloadDialog dialog = new DownloadDialog();
|
||||
final DownloadDialog dialog = new DownloadDialog();
|
||||
dialog.setInfo(info);
|
||||
return dialog;
|
||||
}
|
||||
@@ -208,14 +201,15 @@ public class DownloadDialog extends DialogFragment
|
||||
setStyle(STYLE_NO_TITLE, ThemeHelper.getDialogTheme(context));
|
||||
Icepick.restoreInstanceState(this, savedInstanceState);
|
||||
|
||||
SparseArray<SecondaryStreamHelper<AudioStream>> secondaryStreams = new SparseArray<>(4);
|
||||
List<VideoStream> videoStreams = wrappedVideoStreams.getStreamsList();
|
||||
final SparseArray<SecondaryStreamHelper<AudioStream>> secondaryStreams
|
||||
= new SparseArray<>(4);
|
||||
final List<VideoStream> videoStreams = wrappedVideoStreams.getStreamsList();
|
||||
|
||||
for (int i = 0; i < videoStreams.size(); i++) {
|
||||
if (!videoStreams.get(i).isVideoOnly()) {
|
||||
continue;
|
||||
}
|
||||
AudioStream audioStream = SecondaryStreamHelper
|
||||
final AudioStream audioStream = SecondaryStreamHelper
|
||||
.getAudioStreamFor(wrappedAudioStreams.getStreamsList(), videoStreams.get(i));
|
||||
|
||||
if (audioStream != null) {
|
||||
@@ -232,13 +226,13 @@ public class DownloadDialog extends DialogFragment
|
||||
this.audioStreamsAdapter = new StreamItemAdapter<>(context, wrappedAudioStreams);
|
||||
this.subtitleStreamsAdapter = new StreamItemAdapter<>(context, wrappedSubtitleStreams);
|
||||
|
||||
Intent intent = new Intent(context, DownloadManagerService.class);
|
||||
final Intent intent = new Intent(context, DownloadManagerService.class);
|
||||
context.startService(intent);
|
||||
|
||||
context.bindService(intent, new ServiceConnection() {
|
||||
@Override
|
||||
public void onServiceConnected(final ComponentName cname, final IBinder service) {
|
||||
DownloadManagerBinder mgr = (DownloadManagerBinder) service;
|
||||
final DownloadManagerBinder mgr = (DownloadManagerBinder) service;
|
||||
|
||||
mainStorageAudio = mgr.getMainStorageAudio();
|
||||
mainStorageVideo = mgr.getMainStorageVideo();
|
||||
@@ -275,38 +269,35 @@ public class DownloadDialog extends DialogFragment
|
||||
@Override
|
||||
public void onViewCreated(@NonNull final View view, @Nullable final Bundle savedInstanceState) {
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
nameEditText = view.findViewById(R.id.file_name);
|
||||
nameEditText.setText(FilenameUtils.createFilename(getContext(), currentInfo.getName()));
|
||||
dialogBinding = DownloadDialogBinding.bind(view);
|
||||
|
||||
dialogBinding.fileName.setText(FilenameUtils.createFilename(getContext(),
|
||||
currentInfo.getName()));
|
||||
selectedAudioIndex = ListHelper
|
||||
.getDefaultAudioFormat(getContext(), currentInfo.getAudioStreams());
|
||||
|
||||
selectedSubtitleIndex = getSubtitleIndexBy(subtitleStreamsAdapter.getAll());
|
||||
|
||||
streamsSpinner = view.findViewById(R.id.quality_spinner);
|
||||
streamsSpinner.setOnItemSelectedListener(this);
|
||||
dialogBinding.qualitySpinner.setOnItemSelectedListener(this);
|
||||
|
||||
threadsCountTextView = view.findViewById(R.id.threads_count);
|
||||
threadsSeekBar = view.findViewById(R.id.threads);
|
||||
dialogBinding.videoAudioGroup.setOnCheckedChangeListener(this);
|
||||
|
||||
radioStreamsGroup = view.findViewById(R.id.video_audio_group);
|
||||
radioStreamsGroup.setOnCheckedChangeListener(this);
|
||||
|
||||
initToolbar(view.findViewById(R.id.toolbar));
|
||||
initToolbar(dialogBinding.toolbarLayout.toolbar);
|
||||
setupDownloadOptions();
|
||||
|
||||
prefs = PreferenceManager.getDefaultSharedPreferences(getContext());
|
||||
prefs = PreferenceManager.getDefaultSharedPreferences(requireContext());
|
||||
|
||||
int threads = prefs.getInt(getString(R.string.default_download_threads), 3);
|
||||
threadsCountTextView.setText(String.valueOf(threads));
|
||||
threadsSeekBar.setProgress(threads - 1);
|
||||
threadsSeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
|
||||
final int threads = prefs.getInt(getString(R.string.default_download_threads), 3);
|
||||
dialogBinding.threadsCount.setText(String.valueOf(threads));
|
||||
dialogBinding.threads.setProgress(threads - 1);
|
||||
dialogBinding.threads.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
|
||||
@Override
|
||||
public void onProgressChanged(final SeekBar seekbar, final int progress,
|
||||
final boolean fromUser) {
|
||||
final int newProgress = progress + 1;
|
||||
prefs.edit().putInt(getString(R.string.default_download_threads), newProgress)
|
||||
.apply();
|
||||
threadsCountTextView.setText(String.valueOf(newProgress));
|
||||
dialogBinding.threadsCount.setText(String.valueOf(newProgress));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -324,19 +315,19 @@ public class DownloadDialog extends DialogFragment
|
||||
|
||||
disposables.add(StreamSizeWrapper.fetchSizeForWrapper(wrappedVideoStreams)
|
||||
.subscribe(result -> {
|
||||
if (radioStreamsGroup.getCheckedRadioButtonId() == R.id.video_button) {
|
||||
if (dialogBinding.videoAudioGroup.getCheckedRadioButtonId() == R.id.video_button) {
|
||||
setupVideoSpinner();
|
||||
}
|
||||
}));
|
||||
disposables.add(StreamSizeWrapper.fetchSizeForWrapper(wrappedAudioStreams)
|
||||
.subscribe(result -> {
|
||||
if (radioStreamsGroup.getCheckedRadioButtonId() == R.id.audio_button) {
|
||||
if (dialogBinding.videoAudioGroup.getCheckedRadioButtonId() == R.id.audio_button) {
|
||||
setupAudioSpinner();
|
||||
}
|
||||
}));
|
||||
disposables.add(StreamSizeWrapper.fetchSizeForWrapper(wrappedSubtitleStreams)
|
||||
.subscribe(result -> {
|
||||
if (radioStreamsGroup.getCheckedRadioButtonId() == R.id.subtitle_button) {
|
||||
if (dialogBinding.videoAudioGroup.getCheckedRadioButtonId() == R.id.subtitle_button) {
|
||||
setupSubtitleSpinner();
|
||||
}
|
||||
}));
|
||||
@@ -348,6 +339,12 @@ public class DownloadDialog extends DialogFragment
|
||||
disposables.clear();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyView() {
|
||||
dialogBinding = null;
|
||||
super.onDestroyView();
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Radio group Video&Audio options - Listener
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
@@ -373,13 +370,13 @@ public class DownloadDialog extends DialogFragment
|
||||
}
|
||||
|
||||
if (FilePickerActivityHelper.isOwnFileUri(context, data.getData())) {
|
||||
File file = Utils.getFileForUri(data.getData());
|
||||
final File file = Utils.getFileForUri(data.getData());
|
||||
checkSelectedDownload(null, Uri.fromFile(file), file.getName(),
|
||||
StoredFileHelper.DEFAULT_MIME);
|
||||
return;
|
||||
}
|
||||
|
||||
DocumentFile docFile = DocumentFile.fromSingleUri(context, data.getData());
|
||||
final DocumentFile docFile = DocumentFile.fromSingleUri(context, data.getData());
|
||||
if (docFile == null) {
|
||||
showFailedDialog(R.string.general_error);
|
||||
return;
|
||||
@@ -397,8 +394,7 @@ public class DownloadDialog extends DialogFragment
|
||||
}
|
||||
|
||||
toolbar.setTitle(R.string.download_dialog_title);
|
||||
toolbar.setNavigationIcon(
|
||||
ThemeHelper.resolveResourceIdFromAttr(requireContext(), R.attr.ic_arrow_back));
|
||||
toolbar.setNavigationIcon(R.drawable.ic_arrow_back);
|
||||
toolbar.inflateMenu(R.menu.dialog_url);
|
||||
toolbar.setNavigationOnClickListener(v -> requireDialog().dismiss());
|
||||
toolbar.setNavigationContentDescription(R.string.cancel);
|
||||
@@ -427,8 +423,8 @@ public class DownloadDialog extends DialogFragment
|
||||
return;
|
||||
}
|
||||
|
||||
streamsSpinner.setAdapter(audioStreamsAdapter);
|
||||
streamsSpinner.setSelection(selectedAudioIndex);
|
||||
dialogBinding.qualitySpinner.setAdapter(audioStreamsAdapter);
|
||||
dialogBinding.qualitySpinner.setSelection(selectedAudioIndex);
|
||||
setRadioButtonsState(true);
|
||||
}
|
||||
|
||||
@@ -437,8 +433,8 @@ public class DownloadDialog extends DialogFragment
|
||||
return;
|
||||
}
|
||||
|
||||
streamsSpinner.setAdapter(videoStreamsAdapter);
|
||||
streamsSpinner.setSelection(selectedVideoIndex);
|
||||
dialogBinding.qualitySpinner.setAdapter(videoStreamsAdapter);
|
||||
dialogBinding.qualitySpinner.setSelection(selectedVideoIndex);
|
||||
setRadioButtonsState(true);
|
||||
}
|
||||
|
||||
@@ -447,8 +443,8 @@ public class DownloadDialog extends DialogFragment
|
||||
return;
|
||||
}
|
||||
|
||||
streamsSpinner.setAdapter(subtitleStreamsAdapter);
|
||||
streamsSpinner.setSelection(selectedSubtitleIndex);
|
||||
dialogBinding.qualitySpinner.setAdapter(subtitleStreamsAdapter);
|
||||
dialogBinding.qualitySpinner.setSelection(selectedSubtitleIndex);
|
||||
setRadioButtonsState(true);
|
||||
}
|
||||
|
||||
@@ -473,7 +469,7 @@ public class DownloadDialog extends DialogFragment
|
||||
break;
|
||||
}
|
||||
|
||||
threadsSeekBar.setEnabled(flag);
|
||||
dialogBinding.threads.setEnabled(flag);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -484,7 +480,7 @@ public class DownloadDialog extends DialogFragment
|
||||
+ "parent = [" + parent + "], view = [" + view + "], "
|
||||
+ "position = [" + position + "], id = [" + id + "]");
|
||||
}
|
||||
switch (radioStreamsGroup.getCheckedRadioButtonId()) {
|
||||
switch (dialogBinding.videoAudioGroup.getCheckedRadioButtonId()) {
|
||||
case R.id.audio_button:
|
||||
selectedAudioIndex = position;
|
||||
break;
|
||||
@@ -504,25 +500,39 @@ public class DownloadDialog extends DialogFragment
|
||||
protected void setupDownloadOptions() {
|
||||
setRadioButtonsState(false);
|
||||
|
||||
final RadioButton audioButton = radioStreamsGroup.findViewById(R.id.audio_button);
|
||||
final RadioButton videoButton = radioStreamsGroup.findViewById(R.id.video_button);
|
||||
final RadioButton subtitleButton = radioStreamsGroup.findViewById(R.id.subtitle_button);
|
||||
final boolean isVideoStreamsAvailable = videoStreamsAdapter.getCount() > 0;
|
||||
final boolean isAudioStreamsAvailable = audioStreamsAdapter.getCount() > 0;
|
||||
final boolean isSubtitleStreamsAvailable = subtitleStreamsAdapter.getCount() > 0;
|
||||
|
||||
audioButton.setVisibility(isAudioStreamsAvailable ? View.VISIBLE : View.GONE);
|
||||
videoButton.setVisibility(isVideoStreamsAvailable ? View.VISIBLE : View.GONE);
|
||||
subtitleButton.setVisibility(isSubtitleStreamsAvailable ? View.VISIBLE : View.GONE);
|
||||
dialogBinding.audioButton.setVisibility(isAudioStreamsAvailable ? View.VISIBLE : View.GONE);
|
||||
dialogBinding.videoButton.setVisibility(isVideoStreamsAvailable ? View.VISIBLE : View.GONE);
|
||||
dialogBinding.subtitleButton.setVisibility(isSubtitleStreamsAvailable
|
||||
? View.VISIBLE : View.GONE);
|
||||
|
||||
if (isVideoStreamsAvailable) {
|
||||
videoButton.setChecked(true);
|
||||
prefs = PreferenceManager.getDefaultSharedPreferences(getContext());
|
||||
final String defaultMedia = prefs.getString(getString(R.string.last_used_download_type),
|
||||
getString(R.string.last_download_type_video_key));
|
||||
|
||||
if (isVideoStreamsAvailable
|
||||
&& (defaultMedia.equals(getString(R.string.last_download_type_video_key)))) {
|
||||
dialogBinding.videoButton.setChecked(true);
|
||||
setupVideoSpinner();
|
||||
} else if (isAudioStreamsAvailable
|
||||
&& (defaultMedia.equals(getString(R.string.last_download_type_audio_key)))) {
|
||||
dialogBinding.audioButton.setChecked(true);
|
||||
setupAudioSpinner();
|
||||
} else if (isSubtitleStreamsAvailable
|
||||
&& (defaultMedia.equals(getString(R.string.last_download_type_subtitle_key)))) {
|
||||
dialogBinding.subtitleButton.setChecked(true);
|
||||
setupSubtitleSpinner();
|
||||
} else if (isVideoStreamsAvailable) {
|
||||
dialogBinding.videoButton.setChecked(true);
|
||||
setupVideoSpinner();
|
||||
} else if (isAudioStreamsAvailable) {
|
||||
audioButton.setChecked(true);
|
||||
dialogBinding.audioButton.setChecked(true);
|
||||
setupAudioSpinner();
|
||||
} else if (isSubtitleStreamsAvailable) {
|
||||
subtitleButton.setChecked(true);
|
||||
dialogBinding.subtitleButton.setChecked(true);
|
||||
setupSubtitleSpinner();
|
||||
} else {
|
||||
Toast.makeText(getContext(), R.string.no_streams_available_download,
|
||||
@@ -532,9 +542,9 @@ public class DownloadDialog extends DialogFragment
|
||||
}
|
||||
|
||||
private void setRadioButtonsState(final boolean enabled) {
|
||||
radioStreamsGroup.findViewById(R.id.audio_button).setEnabled(enabled);
|
||||
radioStreamsGroup.findViewById(R.id.video_button).setEnabled(enabled);
|
||||
radioStreamsGroup.findViewById(R.id.subtitle_button).setEnabled(enabled);
|
||||
dialogBinding.audioButton.setEnabled(enabled);
|
||||
dialogBinding.videoButton.setEnabled(enabled);
|
||||
dialogBinding.subtitleButton.setEnabled(enabled);
|
||||
}
|
||||
|
||||
private int getSubtitleIndexBy(final List<SubtitlesStream> streams) {
|
||||
@@ -564,7 +574,7 @@ public class DownloadDialog extends DialogFragment
|
||||
}
|
||||
|
||||
private String getNameEditText() {
|
||||
String str = nameEditText.getText().toString().trim();
|
||||
final String str = dialogBinding.fileName.getText().toString().trim();
|
||||
|
||||
return FilenameUtils.createFilename(context, str.isEmpty() ? currentInfo.getName() : str);
|
||||
}
|
||||
@@ -579,29 +589,20 @@ public class DownloadDialog extends DialogFragment
|
||||
.show();
|
||||
}
|
||||
|
||||
private void showErrorActivity(final Exception e) {
|
||||
ErrorActivity.reportError(
|
||||
context,
|
||||
Collections.singletonList(e),
|
||||
null,
|
||||
null,
|
||||
ErrorActivity.ErrorInfo
|
||||
.make(UserAction.SOMETHING_ELSE, "-", "-", R.string.general_error)
|
||||
);
|
||||
}
|
||||
|
||||
private void prepareSelectedDownload() {
|
||||
StoredDirectoryHelper mainStorage;
|
||||
MediaFormat format;
|
||||
String mime;
|
||||
final StoredDirectoryHelper mainStorage;
|
||||
final MediaFormat format;
|
||||
final String mime;
|
||||
final String selectedMediaType;
|
||||
|
||||
// first, build the filename and get the output folder (if possible)
|
||||
// later, run a very very very large file checking logic
|
||||
|
||||
String filename = getNameEditText().concat(".");
|
||||
|
||||
switch (radioStreamsGroup.getCheckedRadioButtonId()) {
|
||||
switch (dialogBinding.videoAudioGroup.getCheckedRadioButtonId()) {
|
||||
case R.id.audio_button:
|
||||
selectedMediaType = getString(R.string.last_download_type_audio_key);
|
||||
mainStorage = mainStorageAudio;
|
||||
format = audioStreamsAdapter.getItem(selectedAudioIndex).getFormat();
|
||||
switch (format) {
|
||||
@@ -616,16 +617,18 @@ public class DownloadDialog extends DialogFragment
|
||||
}
|
||||
break;
|
||||
case R.id.video_button:
|
||||
selectedMediaType = getString(R.string.last_download_type_video_key);
|
||||
mainStorage = mainStorageVideo;
|
||||
format = videoStreamsAdapter.getItem(selectedVideoIndex).getFormat();
|
||||
mime = format.mimeType;
|
||||
filename += format.suffix;
|
||||
break;
|
||||
case R.id.subtitle_button:
|
||||
selectedMediaType = getString(R.string.last_download_type_subtitle_key);
|
||||
mainStorage = mainStorageVideo; // subtitle & video files go together
|
||||
format = subtitleStreamsAdapter.getItem(selectedSubtitleIndex).getFormat();
|
||||
mime = format.mimeType;
|
||||
filename += format == MediaFormat.TTML ? MediaFormat.SRT.suffix : format.suffix;
|
||||
filename += (format == MediaFormat.TTML ? MediaFormat.SRT : format).suffix;
|
||||
break;
|
||||
default:
|
||||
throw new RuntimeException("No stream selected");
|
||||
@@ -647,7 +650,7 @@ public class DownloadDialog extends DialogFragment
|
||||
filename, mime);
|
||||
} else {
|
||||
File initialSavePath;
|
||||
if (radioStreamsGroup.getCheckedRadioButtonId() == R.id.audio_button) {
|
||||
if (dialogBinding.videoAudioGroup.getCheckedRadioButtonId() == R.id.audio_button) {
|
||||
initialSavePath = NewPipeSettings.getDir(Environment.DIRECTORY_MUSIC);
|
||||
} else {
|
||||
initialSavePath = NewPipeSettings.getDir(Environment.DIRECTORY_MOVIES);
|
||||
@@ -663,6 +666,11 @@ public class DownloadDialog extends DialogFragment
|
||||
|
||||
// check for existing file with the same name
|
||||
checkSelectedDownload(mainStorage, mainStorage.findFile(filename), filename, mime);
|
||||
|
||||
// remember the last media type downloaded by the user
|
||||
prefs.edit()
|
||||
.putString(getString(R.string.last_used_download_type), selectedMediaType)
|
||||
.apply();
|
||||
}
|
||||
|
||||
private void checkSelectedDownload(final StoredDirectoryHelper mainStorage,
|
||||
@@ -683,15 +691,18 @@ public class DownloadDialog extends DialogFragment
|
||||
storage = new StoredFileHelper(context, mainStorage.getUri(), targetFile,
|
||||
mainStorage.getTag());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
showErrorActivity(e);
|
||||
} catch (final Exception e) {
|
||||
ErrorActivity.reportErrorInSnackbar(this,
|
||||
new ErrorInfo(e, UserAction.DOWNLOAD_FAILED, "Getting storage"));
|
||||
return;
|
||||
}
|
||||
|
||||
// check if is our file
|
||||
MissionState state = downloadManager.checkForExistingMission(storage);
|
||||
@StringRes int msgBtn;
|
||||
@StringRes int msgBody;
|
||||
final MissionState state = downloadManager.checkForExistingMission(storage);
|
||||
@StringRes
|
||||
final int msgBtn;
|
||||
@StringRes
|
||||
final int msgBody;
|
||||
|
||||
switch (state) {
|
||||
case Finished:
|
||||
@@ -744,8 +755,7 @@ public class DownloadDialog extends DialogFragment
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
AlertDialog.Builder askDialog = new AlertDialog.Builder(context)
|
||||
final AlertDialog.Builder askDialog = new AlertDialog.Builder(context)
|
||||
.setTitle(R.string.download_dialog_title)
|
||||
.setMessage(msgBody)
|
||||
.setNegativeButton(android.R.string.cancel, null);
|
||||
@@ -787,7 +797,7 @@ public class DownloadDialog extends DialogFragment
|
||||
// try take (or steal) the file
|
||||
storageNew = new StoredFileHelper(context, mainStorage.getUri(),
|
||||
targetFile, mainStorage.getTag());
|
||||
} catch (IOException e) {
|
||||
} catch (final IOException e) {
|
||||
Log.e(TAG, "Failed to take (or steal) the file in "
|
||||
+ targetFile.toString());
|
||||
storageNew = null;
|
||||
@@ -825,24 +835,24 @@ public class DownloadDialog extends DialogFragment
|
||||
if (storage.length() > 0) {
|
||||
storage.truncate();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
} catch (final IOException e) {
|
||||
Log.e(TAG, "failed to truncate the file: " + storage.getUri().toString(), e);
|
||||
showFailedDialog(R.string.overwrite_failed);
|
||||
return;
|
||||
}
|
||||
|
||||
Stream selectedStream;
|
||||
final Stream selectedStream;
|
||||
Stream secondaryStream = null;
|
||||
char kind;
|
||||
int threads = threadsSeekBar.getProgress() + 1;
|
||||
String[] urls;
|
||||
MissionRecoveryInfo[] recoveryInfo;
|
||||
final char kind;
|
||||
int threads = dialogBinding.threads.getProgress() + 1;
|
||||
final String[] urls;
|
||||
final MissionRecoveryInfo[] recoveryInfo;
|
||||
String psName = null;
|
||||
String[] psArgs = null;
|
||||
long nearLength = 0;
|
||||
|
||||
// more download logic: select muxer, subtitle converter, etc.
|
||||
switch (radioStreamsGroup.getCheckedRadioButtonId()) {
|
||||
switch (dialogBinding.videoAudioGroup.getCheckedRadioButtonId()) {
|
||||
case R.id.audio_button:
|
||||
kind = 'a';
|
||||
selectedStream = audioStreamsAdapter.getItem(selectedAudioIndex);
|
||||
@@ -857,7 +867,7 @@ public class DownloadDialog extends DialogFragment
|
||||
kind = 'v';
|
||||
selectedStream = videoStreamsAdapter.getItem(selectedVideoIndex);
|
||||
|
||||
SecondaryStreamHelper<AudioStream> secondary = videoStreamsAdapter
|
||||
final SecondaryStreamHelper<AudioStream> secondary = videoStreamsAdapter
|
||||
.getAllSecondary()
|
||||
.get(wrappedVideoStreams.getStreamsList().indexOf(selectedStream));
|
||||
|
||||
@@ -871,7 +881,7 @@ public class DownloadDialog extends DialogFragment
|
||||
}
|
||||
|
||||
psArgs = null;
|
||||
long videoSize = wrappedVideoStreams
|
||||
final long videoSize = wrappedVideoStreams
|
||||
.getSizeInBytes((VideoStream) selectedStream);
|
||||
|
||||
// set nearLength, only, if both sizes are fetched or known. This probably
|
||||
@@ -916,6 +926,9 @@ public class DownloadDialog extends DialogFragment
|
||||
DownloadManagerService.startMission(context, urls, storage, kind, threads,
|
||||
currentInfo.getUrl(), psName, psArgs, nearLength, recoveryInfo);
|
||||
|
||||
Toast.makeText(context, getString(R.string.download_has_started),
|
||||
Toast.LENGTH_SHORT).show();
|
||||
|
||||
dismiss();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
package org.schabi.newpipe.report;
|
||||
package org.schabi.newpipe.error;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.acra.ReportField;
|
||||
import org.acra.data.CrashReportData;
|
||||
import org.acra.sender.ReportSender;
|
||||
import org.schabi.newpipe.R;
|
||||
@@ -32,8 +33,12 @@ public class AcraReportSender implements ReportSender {
|
||||
|
||||
@Override
|
||||
public void send(@NonNull final Context context, @NonNull final CrashReportData report) {
|
||||
ErrorActivity.reportError(context, report,
|
||||
ErrorActivity.ErrorInfo.make(UserAction.UI_ERROR, "none",
|
||||
"App crash, UI failure", R.string.app_ui_crash));
|
||||
ErrorActivity.reportError(context, new ErrorInfo(
|
||||
new String[]{report.getString(ReportField.STACK_TRACE)},
|
||||
UserAction.UI_ERROR,
|
||||
ErrorInfo.SERVICE_NONE,
|
||||
"ACRA report",
|
||||
R.string.app_ui_crash,
|
||||
null));
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,15 @@
|
||||
package org.schabi.newpipe.report;
|
||||
package org.schabi.newpipe.error;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.google.auto.service.AutoService;
|
||||
|
||||
import org.acra.config.CoreConfiguration;
|
||||
import org.acra.sender.ReportSender;
|
||||
import org.acra.sender.ReportSenderFactory;
|
||||
import org.schabi.newpipe.App;
|
||||
|
||||
/*
|
||||
* Created by Christian Schabesberger on 13.09.16.
|
||||
@@ -28,6 +31,10 @@ import org.acra.sender.ReportSenderFactory;
|
||||
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Used by ACRA in {@link App}.initAcra() as the factory for report senders.
|
||||
*/
|
||||
@AutoService(ReportSenderFactory.class)
|
||||
public class AcraReportSenderFactory implements ReportSenderFactory {
|
||||
@NonNull
|
||||
public ReportSender create(@NonNull final Context context,
|
||||
406
app/src/main/java/org/schabi/newpipe/error/ErrorActivity.java
Normal file
406
app/src/main/java/org/schabi/newpipe/error/ErrorActivity.java
Normal file
@@ -0,0 +1,406 @@
|
||||
package org.schabi.newpipe.error;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.graphics.Color;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.fragment.app.Fragment;
|
||||
|
||||
import com.google.android.material.snackbar.Snackbar;
|
||||
import com.grack.nanojson.JsonWriter;
|
||||
|
||||
import org.schabi.newpipe.BuildConfig;
|
||||
import org.schabi.newpipe.MainActivity;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.databinding.ActivityErrorBinding;
|
||||
import org.schabi.newpipe.util.Localization;
|
||||
import org.schabi.newpipe.util.ShareUtils;
|
||||
import org.schabi.newpipe.util.ThemeHelper;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.Arrays;
|
||||
|
||||
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
|
||||
|
||||
/*
|
||||
* Created by Christian Schabesberger on 24.10.15.
|
||||
*
|
||||
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
|
||||
* ErrorActivity.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 ErrorActivity extends AppCompatActivity {
|
||||
// LOG TAGS
|
||||
public static final String TAG = ErrorActivity.class.toString();
|
||||
// BUNDLE TAGS
|
||||
public static final String ERROR_INFO = "error_info";
|
||||
|
||||
public static final String ERROR_EMAIL_ADDRESS = "crashreport@newpipe.schabi.org";
|
||||
public static final String ERROR_EMAIL_SUBJECT = "Exception in ";
|
||||
|
||||
public static final String ERROR_GITHUB_ISSUE_URL
|
||||
= "https://github.com/TeamNewPipe/NewPipe/issues";
|
||||
|
||||
public static final DateTimeFormatter CURRENT_TIMESTAMP_FORMATTER
|
||||
= DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm");
|
||||
|
||||
|
||||
private ErrorInfo errorInfo;
|
||||
private String currentTimeStamp;
|
||||
|
||||
private ActivityErrorBinding activityErrorBinding;
|
||||
|
||||
public static void reportError(final Context context, final ErrorInfo errorInfo) {
|
||||
final Intent intent = new Intent(context, ErrorActivity.class);
|
||||
intent.putExtra(ERROR_INFO, errorInfo);
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
context.startActivity(intent);
|
||||
}
|
||||
|
||||
public static void reportErrorInSnackbar(final Context context, final ErrorInfo errorInfo) {
|
||||
final View rootView = context instanceof Activity
|
||||
? ((Activity) context).findViewById(android.R.id.content) : null;
|
||||
reportErrorInSnackbar(context, rootView, errorInfo);
|
||||
}
|
||||
|
||||
public static void reportErrorInSnackbar(final Fragment fragment, final ErrorInfo errorInfo) {
|
||||
View rootView = fragment.getView();
|
||||
if (rootView == null && fragment.getActivity() != null) {
|
||||
rootView = fragment.getActivity().findViewById(android.R.id.content);
|
||||
}
|
||||
reportErrorInSnackbar(fragment.requireContext(), rootView, errorInfo);
|
||||
}
|
||||
|
||||
public static void reportUiErrorInSnackbar(final Context context,
|
||||
final String request,
|
||||
final Throwable throwable) {
|
||||
reportErrorInSnackbar(context, new ErrorInfo(throwable, UserAction.UI_ERROR, request));
|
||||
}
|
||||
|
||||
public static void reportUiErrorInSnackbar(final Fragment fragment,
|
||||
final String request,
|
||||
final Throwable throwable) {
|
||||
reportErrorInSnackbar(fragment, new ErrorInfo(throwable, UserAction.UI_ERROR, request));
|
||||
}
|
||||
|
||||
|
||||
////////////////////////////////////////////////////////////////////////
|
||||
// Utils
|
||||
////////////////////////////////////////////////////////////////////////
|
||||
|
||||
private static void reportErrorInSnackbar(final Context context,
|
||||
@Nullable final View rootView,
|
||||
final ErrorInfo errorInfo) {
|
||||
if (rootView != null) {
|
||||
Snackbar.make(rootView, R.string.error_snackbar_message, Snackbar.LENGTH_LONG)
|
||||
.setActionTextColor(Color.YELLOW)
|
||||
.setAction(context.getString(R.string.error_snackbar_action).toUpperCase(), v ->
|
||||
reportError(context, errorInfo)).show();
|
||||
} else {
|
||||
reportError(context, errorInfo);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
////////////////////////////////////////////////////////////////////////
|
||||
// Activity lifecycle
|
||||
////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@Override
|
||||
protected void onCreate(final Bundle savedInstanceState) {
|
||||
assureCorrectAppLanguage(this);
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
ThemeHelper.setDayNightMode(this);
|
||||
ThemeHelper.setTheme(this);
|
||||
|
||||
activityErrorBinding = ActivityErrorBinding.inflate(getLayoutInflater());
|
||||
setContentView(activityErrorBinding.getRoot());
|
||||
|
||||
final Intent intent = getIntent();
|
||||
|
||||
setSupportActionBar(activityErrorBinding.toolbarLayout.toolbar);
|
||||
|
||||
final ActionBar actionBar = getSupportActionBar();
|
||||
if (actionBar != null) {
|
||||
actionBar.setDisplayHomeAsUpEnabled(true);
|
||||
actionBar.setTitle(R.string.error_report_title);
|
||||
actionBar.setDisplayShowTitleEnabled(true);
|
||||
}
|
||||
|
||||
errorInfo = intent.getParcelableExtra(ERROR_INFO);
|
||||
|
||||
// important add guru meditation
|
||||
addGuruMeditation();
|
||||
currentTimeStamp = CURRENT_TIMESTAMP_FORMATTER.format(LocalDateTime.now());
|
||||
|
||||
activityErrorBinding.errorReportEmailButton.setOnClickListener(v ->
|
||||
openPrivacyPolicyDialog(this, "EMAIL"));
|
||||
|
||||
activityErrorBinding.errorReportCopyButton.setOnClickListener(v ->
|
||||
ShareUtils.copyToClipboard(this, buildMarkdown()));
|
||||
|
||||
activityErrorBinding.errorReportGitHubButton.setOnClickListener(v ->
|
||||
openPrivacyPolicyDialog(this, "GITHUB"));
|
||||
|
||||
// normal bugreport
|
||||
buildInfo(errorInfo);
|
||||
activityErrorBinding.errorMessageView.setText(errorInfo.getMessageStringId());
|
||||
activityErrorBinding.errorView.setText(formErrorText(errorInfo.getStackTraces()));
|
||||
|
||||
// print stack trace once again for debugging:
|
||||
for (final String e : errorInfo.getStackTraces()) {
|
||||
Log.e(TAG, e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(final Menu menu) {
|
||||
final MenuInflater inflater = getMenuInflater();
|
||||
inflater.inflate(R.menu.error_menu, menu);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(final MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case android.R.id.home:
|
||||
onBackPressed();
|
||||
return true;
|
||||
case R.id.menu_item_share_error:
|
||||
ShareUtils.shareText(this, getString(R.string.error_report_title), buildJson());
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private void openPrivacyPolicyDialog(final Context context, final String action) {
|
||||
new AlertDialog.Builder(context)
|
||||
.setIcon(android.R.drawable.ic_dialog_alert)
|
||||
.setTitle(R.string.privacy_policy_title)
|
||||
.setMessage(R.string.start_accept_privacy_policy)
|
||||
.setCancelable(false)
|
||||
.setNeutralButton(R.string.read_privacy_policy, (dialog, which) ->
|
||||
ShareUtils.openUrlInBrowser(context,
|
||||
context.getString(R.string.privacy_policy_url)))
|
||||
.setPositiveButton(R.string.accept, (dialog, which) -> {
|
||||
if (action.equals("EMAIL")) { // send on email
|
||||
final Intent i = new Intent(Intent.ACTION_SENDTO)
|
||||
.setData(Uri.parse("mailto:")) // only email apps should handle this
|
||||
.putExtra(Intent.EXTRA_EMAIL, new String[]{ERROR_EMAIL_ADDRESS})
|
||||
.putExtra(Intent.EXTRA_SUBJECT, ERROR_EMAIL_SUBJECT
|
||||
+ getString(R.string.app_name) + " "
|
||||
+ BuildConfig.VERSION_NAME)
|
||||
.putExtra(Intent.EXTRA_TEXT, buildJson());
|
||||
if (i.resolveActivity(getPackageManager()) != null) {
|
||||
ShareUtils.openIntentInApp(context, i);
|
||||
}
|
||||
} else if (action.equals("GITHUB")) { // open the NewPipe issue page on GitHub
|
||||
ShareUtils.openUrlInBrowser(this, ERROR_GITHUB_ISSUE_URL, false);
|
||||
}
|
||||
|
||||
})
|
||||
.setNegativeButton(R.string.decline, (dialog, which) -> {
|
||||
// do nothing
|
||||
})
|
||||
.show();
|
||||
}
|
||||
|
||||
private String formErrorText(final String[] el) {
|
||||
final StringBuilder text = new StringBuilder();
|
||||
if (el != null) {
|
||||
for (final String e : el) {
|
||||
text.append("-------------------------------------\n").append(e);
|
||||
}
|
||||
}
|
||||
text.append("-------------------------------------");
|
||||
return text.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the checked activity.
|
||||
*
|
||||
* @param returnActivity the activity to return to
|
||||
* @return the casted return activity or null
|
||||
*/
|
||||
@Nullable
|
||||
static Class<? extends Activity> getReturnActivity(final Class<?> returnActivity) {
|
||||
Class<? extends Activity> checkedReturnActivity = null;
|
||||
if (returnActivity != null) {
|
||||
if (Activity.class.isAssignableFrom(returnActivity)) {
|
||||
checkedReturnActivity = returnActivity.asSubclass(Activity.class);
|
||||
} else {
|
||||
checkedReturnActivity = MainActivity.class;
|
||||
}
|
||||
}
|
||||
return checkedReturnActivity;
|
||||
}
|
||||
|
||||
private void buildInfo(final ErrorInfo info) {
|
||||
String text = "";
|
||||
|
||||
activityErrorBinding.errorInfoLabelsView.setText(getString(R.string.info_labels)
|
||||
.replace("\\n", "\n"));
|
||||
|
||||
text += getUserActionString(info.getUserAction()) + "\n"
|
||||
+ info.getRequest() + "\n"
|
||||
+ getContentLanguageString() + "\n"
|
||||
+ getContentCountryString() + "\n"
|
||||
+ getAppLanguage() + "\n"
|
||||
+ info.getServiceName() + "\n"
|
||||
+ currentTimeStamp + "\n"
|
||||
+ getPackageName() + "\n"
|
||||
+ BuildConfig.VERSION_NAME + "\n"
|
||||
+ getOsString();
|
||||
|
||||
activityErrorBinding.errorInfosView.setText(text);
|
||||
}
|
||||
|
||||
private String buildJson() {
|
||||
try {
|
||||
return JsonWriter.string()
|
||||
.object()
|
||||
.value("user_action", getUserActionString(errorInfo.getUserAction()))
|
||||
.value("request", errorInfo.getRequest())
|
||||
.value("content_language", getContentLanguageString())
|
||||
.value("content_country", getContentCountryString())
|
||||
.value("app_language", getAppLanguage())
|
||||
.value("service", errorInfo.getServiceName())
|
||||
.value("package", getPackageName())
|
||||
.value("version", BuildConfig.VERSION_NAME)
|
||||
.value("os", getOsString())
|
||||
.value("time", currentTimeStamp)
|
||||
.array("exceptions", Arrays.asList(errorInfo.getStackTraces()))
|
||||
.value("user_comment", activityErrorBinding.errorCommentBox.getText()
|
||||
.toString())
|
||||
.end()
|
||||
.done();
|
||||
} catch (final Throwable e) {
|
||||
Log.e(TAG, "Error while erroring: Could not build json");
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
private String buildMarkdown() {
|
||||
try {
|
||||
final StringBuilder htmlErrorReport = new StringBuilder();
|
||||
|
||||
final String userComment = activityErrorBinding.errorCommentBox.getText().toString();
|
||||
if (!userComment.isEmpty()) {
|
||||
htmlErrorReport.append(userComment).append("\n");
|
||||
}
|
||||
|
||||
// basic error info
|
||||
htmlErrorReport
|
||||
.append("## Exception")
|
||||
.append("\n* __User Action:__ ")
|
||||
.append(getUserActionString(errorInfo.getUserAction()))
|
||||
.append("\n* __Request:__ ").append(errorInfo.getRequest())
|
||||
.append("\n* __Content Country:__ ").append(getContentCountryString())
|
||||
.append("\n* __Content Language:__ ").append(getContentLanguageString())
|
||||
.append("\n* __App Language:__ ").append(getAppLanguage())
|
||||
.append("\n* __Service:__ ").append(errorInfo.getServiceName())
|
||||
.append("\n* __Version:__ ").append(BuildConfig.VERSION_NAME)
|
||||
.append("\n* __OS:__ ").append(getOsString()).append("\n");
|
||||
|
||||
|
||||
// Collapse all logs to a single paragraph when there are more than one
|
||||
// to keep the GitHub issue clean.
|
||||
if (errorInfo.getStackTraces().length > 1) {
|
||||
htmlErrorReport
|
||||
.append("<details><summary><b>Exceptions (")
|
||||
.append(errorInfo.getStackTraces().length)
|
||||
.append(")</b></summary><p>\n");
|
||||
}
|
||||
|
||||
// add the logs
|
||||
for (int i = 0; i < errorInfo.getStackTraces().length; i++) {
|
||||
htmlErrorReport.append("<details><summary><b>Crash log ");
|
||||
if (errorInfo.getStackTraces().length > 1) {
|
||||
htmlErrorReport.append(i + 1);
|
||||
}
|
||||
htmlErrorReport.append("</b>")
|
||||
.append("</summary><p>\n")
|
||||
.append("\n```\n").append(errorInfo.getStackTraces()[i]).append("\n```\n")
|
||||
.append("</details>\n");
|
||||
}
|
||||
|
||||
// make sure to close everything
|
||||
if (errorInfo.getStackTraces().length > 1) {
|
||||
htmlErrorReport.append("</p></details>\n");
|
||||
}
|
||||
htmlErrorReport.append("<hr>\n");
|
||||
return htmlErrorReport.toString();
|
||||
} catch (final Throwable e) {
|
||||
Log.e(TAG, "Error while erroring: Could not build markdown");
|
||||
e.printStackTrace();
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
private String getUserActionString(final UserAction userAction) {
|
||||
if (userAction == null) {
|
||||
return "Your description is in another castle.";
|
||||
} else {
|
||||
return userAction.getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
private String getContentCountryString() {
|
||||
return Localization.getPreferredContentCountry(this).getCountryCode();
|
||||
}
|
||||
|
||||
private String getContentLanguageString() {
|
||||
return Localization.getPreferredLocalization(this).getLocalizationCode();
|
||||
}
|
||||
|
||||
private String getAppLanguage() {
|
||||
return Localization.getAppLocale(getApplicationContext()).toString();
|
||||
}
|
||||
|
||||
private String getOsString() {
|
||||
final String osBase = Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
|
||||
? Build.VERSION.BASE_OS : "Android";
|
||||
return System.getProperty("os.name")
|
||||
+ " " + (osBase.isEmpty() ? "Android" : osBase)
|
||||
+ " " + Build.VERSION.RELEASE
|
||||
+ " - " + Build.VERSION.SDK_INT;
|
||||
}
|
||||
|
||||
private void addGuruMeditation() {
|
||||
//just an easter egg
|
||||
String text = activityErrorBinding.errorSorryView.getText().toString();
|
||||
text += "\n" + getString(R.string.guru_meditation);
|
||||
activityErrorBinding.errorSorryView.setText(text);
|
||||
}
|
||||
}
|
||||
113
app/src/main/java/org/schabi/newpipe/error/ErrorInfo.kt
Normal file
113
app/src/main/java/org/schabi/newpipe/error/ErrorInfo.kt
Normal file
@@ -0,0 +1,113 @@
|
||||
package org.schabi.newpipe.error
|
||||
|
||||
import android.os.Parcelable
|
||||
import androidx.annotation.StringRes
|
||||
import kotlinx.android.parcel.Parcelize
|
||||
import org.schabi.newpipe.R
|
||||
import org.schabi.newpipe.extractor.Info
|
||||
import org.schabi.newpipe.extractor.NewPipe
|
||||
import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException
|
||||
import org.schabi.newpipe.extractor.exceptions.ContentNotSupportedException
|
||||
import org.schabi.newpipe.extractor.exceptions.ExtractionException
|
||||
import org.schabi.newpipe.extractor.services.youtube.extractors.YoutubeStreamExtractor.DeobfuscateException
|
||||
import org.schabi.newpipe.ktx.isNetworkRelated
|
||||
import java.io.PrintWriter
|
||||
import java.io.StringWriter
|
||||
|
||||
@Parcelize
|
||||
class ErrorInfo(
|
||||
val stackTraces: Array<String>,
|
||||
val userAction: UserAction,
|
||||
val serviceName: String,
|
||||
val request: String,
|
||||
val messageStringId: Int,
|
||||
@Transient // no need to store throwable, all data for report is in other variables
|
||||
var throwable: Throwable? = null
|
||||
) : Parcelable {
|
||||
|
||||
private constructor(
|
||||
throwable: Throwable,
|
||||
userAction: UserAction,
|
||||
serviceName: String,
|
||||
request: String
|
||||
) : this(
|
||||
throwableToStringList(throwable),
|
||||
userAction,
|
||||
serviceName,
|
||||
request,
|
||||
getMessageStringId(throwable, userAction),
|
||||
throwable
|
||||
)
|
||||
|
||||
private constructor(
|
||||
throwable: List<Throwable>,
|
||||
userAction: UserAction,
|
||||
serviceName: String,
|
||||
request: String
|
||||
) : this(
|
||||
throwableListToStringList(throwable),
|
||||
userAction,
|
||||
serviceName,
|
||||
request,
|
||||
getMessageStringId(throwable.firstOrNull(), userAction),
|
||||
throwable.firstOrNull()
|
||||
)
|
||||
|
||||
// constructors with single throwable
|
||||
constructor(throwable: Throwable, userAction: UserAction, request: String) :
|
||||
this(throwable, userAction, SERVICE_NONE, request)
|
||||
constructor(throwable: Throwable, userAction: UserAction, request: String, serviceId: Int) :
|
||||
this(throwable, userAction, NewPipe.getNameOfService(serviceId), request)
|
||||
constructor(throwable: Throwable, userAction: UserAction, request: String, info: Info?) :
|
||||
this(throwable, userAction, getInfoServiceName(info), request)
|
||||
|
||||
// constructors with list of throwables
|
||||
constructor(throwable: List<Throwable>, userAction: UserAction, request: String) :
|
||||
this(throwable, userAction, SERVICE_NONE, request)
|
||||
constructor(throwable: List<Throwable>, userAction: UserAction, request: String, serviceId: Int) :
|
||||
this(throwable, userAction, NewPipe.getNameOfService(serviceId), request)
|
||||
constructor(throwable: List<Throwable>, userAction: UserAction, request: String, info: Info?) :
|
||||
this(throwable, userAction, getInfoServiceName(info), request)
|
||||
|
||||
companion object {
|
||||
const val SERVICE_NONE = "none"
|
||||
|
||||
private fun getStackTrace(throwable: Throwable): String {
|
||||
StringWriter().use { stringWriter ->
|
||||
PrintWriter(stringWriter, true).use { printWriter ->
|
||||
throwable.printStackTrace(printWriter)
|
||||
return stringWriter.buffer.toString()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun throwableToStringList(throwable: Throwable) = arrayOf(getStackTrace(throwable))
|
||||
|
||||
fun throwableListToStringList(throwable: List<Throwable>) =
|
||||
Array(throwable.size) { i -> getStackTrace(throwable[i]) }
|
||||
|
||||
private fun getInfoServiceName(info: Info?) =
|
||||
if (info == null) SERVICE_NONE else NewPipe.getNameOfService(info.serviceId)
|
||||
|
||||
@StringRes
|
||||
private fun getMessageStringId(
|
||||
throwable: Throwable?,
|
||||
action: UserAction
|
||||
): Int {
|
||||
return when {
|
||||
throwable is ContentNotAvailableException -> R.string.content_not_available
|
||||
throwable != null && throwable.isNetworkRelated -> R.string.network_error
|
||||
throwable is ContentNotSupportedException -> R.string.content_not_supported
|
||||
throwable is DeobfuscateException -> R.string.youtube_signature_deobfuscation_error
|
||||
throwable is ExtractionException -> R.string.parsing_error
|
||||
action == UserAction.UI_ERROR -> R.string.app_ui_crash
|
||||
action == UserAction.REQUESTED_COMMENTS -> R.string.error_unable_to_load_comments
|
||||
action == UserAction.SUBSCRIPTION_CHANGE -> R.string.subscription_change_failed
|
||||
action == UserAction.SUBSCRIPTION_UPDATE -> R.string.subscription_update_failed
|
||||
action == UserAction.LOAD_IMAGE -> R.string.could_not_load_thumbnails
|
||||
action == UserAction.DOWNLOAD_OPEN_DIALOG -> R.string.could_not_setup_download_menu
|
||||
else -> R.string.general_error
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
132
app/src/main/java/org/schabi/newpipe/error/ErrorPanelHelper.kt
Normal file
132
app/src/main/java/org/schabi/newpipe/error/ErrorPanelHelper.kt
Normal file
@@ -0,0 +1,132 @@
|
||||
package org.schabi.newpipe.error
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.util.Log
|
||||
import android.view.View
|
||||
import android.widget.Button
|
||||
import android.widget.TextView
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.fragment.app.Fragment
|
||||
import com.jakewharton.rxbinding4.view.clicks
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.disposables.Disposable
|
||||
import org.schabi.newpipe.MainActivity
|
||||
import org.schabi.newpipe.R
|
||||
import org.schabi.newpipe.extractor.exceptions.AgeRestrictedContentException
|
||||
import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException
|
||||
import org.schabi.newpipe.extractor.exceptions.ContentNotSupportedException
|
||||
import org.schabi.newpipe.extractor.exceptions.GeographicRestrictionException
|
||||
import org.schabi.newpipe.extractor.exceptions.PaidContentException
|
||||
import org.schabi.newpipe.extractor.exceptions.PrivateContentException
|
||||
import org.schabi.newpipe.extractor.exceptions.ReCaptchaException
|
||||
import org.schabi.newpipe.extractor.exceptions.SoundCloudGoPlusContentException
|
||||
import org.schabi.newpipe.extractor.exceptions.YoutubeMusicPremiumContentException
|
||||
import org.schabi.newpipe.ktx.animate
|
||||
import org.schabi.newpipe.ktx.isInterruptedCaused
|
||||
import org.schabi.newpipe.ktx.isNetworkRelated
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class ErrorPanelHelper(
|
||||
private val fragment: Fragment,
|
||||
rootView: View,
|
||||
onRetry: Runnable
|
||||
) {
|
||||
private val context: Context = rootView.context!!
|
||||
private val errorPanelRoot: View = rootView.findViewById(R.id.error_panel)
|
||||
private val errorTextView: TextView = errorPanelRoot.findViewById(R.id.error_message_view)
|
||||
private val errorButtonAction: Button = errorPanelRoot.findViewById(R.id.error_button_action)
|
||||
private val errorButtonRetry: Button = errorPanelRoot.findViewById(R.id.error_button_retry)
|
||||
|
||||
private var errorDisposable: Disposable? = null
|
||||
|
||||
init {
|
||||
errorDisposable = errorButtonRetry.clicks()
|
||||
.debounce(300, TimeUnit.MILLISECONDS)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe { onRetry.run() }
|
||||
}
|
||||
|
||||
fun showError(errorInfo: ErrorInfo) {
|
||||
|
||||
if (errorInfo.throwable != null && errorInfo.throwable!!.isInterruptedCaused) {
|
||||
if (DEBUG) {
|
||||
Log.w(TAG, "onError() isInterruptedCaused! = [$errorInfo.throwable]")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
errorButtonAction.isVisible = true
|
||||
if (errorInfo.throwable is ReCaptchaException) {
|
||||
errorButtonAction.setText(R.string.recaptcha_solve)
|
||||
errorButtonAction.setOnClickListener {
|
||||
// Starting ReCaptcha Challenge Activity
|
||||
val intent = Intent(context, ReCaptchaActivity::class.java)
|
||||
intent.putExtra(
|
||||
ReCaptchaActivity.RECAPTCHA_URL_EXTRA,
|
||||
(errorInfo.throwable as ReCaptchaException).url
|
||||
)
|
||||
fragment.startActivityForResult(intent, ReCaptchaActivity.RECAPTCHA_REQUEST)
|
||||
errorButtonAction.setOnClickListener(null)
|
||||
}
|
||||
errorTextView.setText(R.string.recaptcha_request_toast)
|
||||
errorButtonRetry.isVisible = true
|
||||
} else {
|
||||
errorButtonAction.setText(R.string.error_snackbar_action)
|
||||
errorButtonAction.setOnClickListener {
|
||||
ErrorActivity.reportError(context, errorInfo)
|
||||
}
|
||||
|
||||
// hide retry button by default, then show only if not unavailable/unsupported content
|
||||
errorButtonRetry.isVisible = false
|
||||
errorTextView.setText(
|
||||
when (errorInfo.throwable) {
|
||||
is AgeRestrictedContentException -> R.string.restricted_video_no_stream
|
||||
is GeographicRestrictionException -> R.string.georestricted_content
|
||||
is PaidContentException -> R.string.paid_content
|
||||
is PrivateContentException -> R.string.private_content
|
||||
is SoundCloudGoPlusContentException -> R.string.soundcloud_go_plus_content
|
||||
is YoutubeMusicPremiumContentException -> R.string.youtube_music_premium_content
|
||||
is ContentNotAvailableException -> R.string.content_not_available
|
||||
is ContentNotSupportedException -> R.string.content_not_supported
|
||||
else -> {
|
||||
// show retry button only for content which is not unavailable or unsupported
|
||||
errorButtonRetry.isVisible = true
|
||||
if (errorInfo.throwable != null && errorInfo.throwable!!.isNetworkRelated) {
|
||||
R.string.network_error
|
||||
} else {
|
||||
R.string.error_snackbar_message
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
errorPanelRoot.animate(true, 300)
|
||||
}
|
||||
|
||||
fun showTextError(errorString: String) {
|
||||
errorButtonAction.isVisible = false
|
||||
errorButtonRetry.isVisible = false
|
||||
errorTextView.text = errorString
|
||||
}
|
||||
|
||||
fun hide() {
|
||||
errorButtonAction.setOnClickListener(null)
|
||||
errorPanelRoot.animate(false, 150)
|
||||
}
|
||||
|
||||
fun isVisible(): Boolean {
|
||||
return errorPanelRoot.isVisible
|
||||
}
|
||||
|
||||
fun dispose() {
|
||||
errorButtonAction.setOnClickListener(null)
|
||||
errorButtonRetry.setOnClickListener(null)
|
||||
errorDisposable?.dispose()
|
||||
}
|
||||
|
||||
companion object {
|
||||
val TAG: String = ErrorPanelHelper::class.simpleName!!
|
||||
val DEBUG: Boolean = MainActivity.DEBUG
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.schabi.newpipe;
|
||||
package org.schabi.newpipe.error;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
@@ -8,20 +8,21 @@ import android.util.Log;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.webkit.CookieManager;
|
||||
import android.webkit.WebResourceRequest;
|
||||
import android.webkit.WebSettings;
|
||||
import android.webkit.WebView;
|
||||
import android.webkit.WebViewClient;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.RequiresApi;
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.core.app.NavUtils;
|
||||
import androidx.preference.PreferenceManager;
|
||||
import androidx.webkit.WebViewClientCompat;
|
||||
|
||||
import org.schabi.newpipe.databinding.ActivityRecaptchaBinding;
|
||||
import org.schabi.newpipe.DownloaderImpl;
|
||||
import org.schabi.newpipe.MainActivity;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.util.ThemeHelper;
|
||||
|
||||
import java.io.UnsupportedEncodingException;
|
||||
@@ -53,46 +54,37 @@ public class ReCaptchaActivity extends AppCompatActivity {
|
||||
public static final String YT_URL = "https://www.youtube.com";
|
||||
public static final String RECAPTCHA_COOKIES_KEY = "recaptcha_cookies";
|
||||
|
||||
private WebView webView;
|
||||
public static String sanitizeRecaptchaUrl(@Nullable final String url) {
|
||||
if (url == null || url.trim().isEmpty()) {
|
||||
return YT_URL; // YouTube is the most likely service to have thrown a recaptcha
|
||||
} else {
|
||||
// remove "pbj=1" parameter from YouYube urls, as it makes the page JSON and not HTML
|
||||
return url.replace("&pbj=1", "").replace("pbj=1&", "").replace("?pbj=1", "");
|
||||
}
|
||||
}
|
||||
|
||||
private ActivityRecaptchaBinding recaptchaBinding;
|
||||
private String foundCookies = "";
|
||||
|
||||
@Override
|
||||
protected void onCreate(final Bundle savedInstanceState) {
|
||||
ThemeHelper.setTheme(this);
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_recaptcha);
|
||||
Toolbar toolbar = findViewById(R.id.toolbar);
|
||||
setSupportActionBar(toolbar);
|
||||
|
||||
String url = getIntent().getStringExtra(RECAPTCHA_URL_EXTRA);
|
||||
if (url == null || url.isEmpty()) {
|
||||
url = YT_URL;
|
||||
}
|
||||
recaptchaBinding = ActivityRecaptchaBinding.inflate(getLayoutInflater());
|
||||
setContentView(recaptchaBinding.getRoot());
|
||||
setSupportActionBar(recaptchaBinding.toolbar);
|
||||
|
||||
final String url = sanitizeRecaptchaUrl(getIntent().getStringExtra(RECAPTCHA_URL_EXTRA));
|
||||
// set return to Cancel by default
|
||||
setResult(RESULT_CANCELED);
|
||||
|
||||
|
||||
webView = findViewById(R.id.reCaptchaWebView);
|
||||
|
||||
// enable Javascript
|
||||
WebSettings webSettings = webView.getSettings();
|
||||
final WebSettings webSettings = recaptchaBinding.reCaptchaWebView.getSettings();
|
||||
webSettings.setJavaScriptEnabled(true);
|
||||
webSettings.setUserAgentString(DownloaderImpl.USER_AGENT);
|
||||
|
||||
webView.setWebViewClient(new WebViewClient() {
|
||||
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
|
||||
@Override
|
||||
public boolean shouldOverrideUrlLoading(final WebView view,
|
||||
final WebResourceRequest request) {
|
||||
String url = request.getUrl().toString();
|
||||
if (MainActivity.DEBUG) {
|
||||
Log.d(TAG, "shouldOverrideUrlLoading: request.url=" + url);
|
||||
}
|
||||
|
||||
handleCookiesFromUrl(url);
|
||||
return false;
|
||||
}
|
||||
|
||||
recaptchaBinding.reCaptchaWebView.setWebViewClient(new WebViewClientCompat() {
|
||||
@Override
|
||||
public boolean shouldOverrideUrlLoading(final WebView view, final String url) {
|
||||
if (MainActivity.DEBUG) {
|
||||
@@ -111,24 +103,23 @@ public class ReCaptchaActivity extends AppCompatActivity {
|
||||
});
|
||||
|
||||
// cleaning cache, history and cookies from webView
|
||||
webView.clearCache(true);
|
||||
webView.clearHistory();
|
||||
android.webkit.CookieManager cookieManager = CookieManager.getInstance();
|
||||
recaptchaBinding.reCaptchaWebView.clearCache(true);
|
||||
recaptchaBinding.reCaptchaWebView.clearHistory();
|
||||
final CookieManager cookieManager = CookieManager.getInstance();
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
cookieManager.removeAllCookies(aBoolean -> {
|
||||
});
|
||||
cookieManager.removeAllCookies(value -> { });
|
||||
} else {
|
||||
cookieManager.removeAllCookie();
|
||||
}
|
||||
|
||||
webView.loadUrl(url);
|
||||
recaptchaBinding.reCaptchaWebView.loadUrl(url);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(final Menu menu) {
|
||||
getMenuInflater().inflate(R.menu.menu_recaptcha, menu);
|
||||
|
||||
ActionBar actionBar = getSupportActionBar();
|
||||
final ActionBar actionBar = getSupportActionBar();
|
||||
if (actionBar != null) {
|
||||
actionBar.setDisplayHomeAsUpEnabled(false);
|
||||
actionBar.setTitle(R.string.title_activity_recaptcha);
|
||||
@@ -145,18 +136,16 @@ public class ReCaptchaActivity extends AppCompatActivity {
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(final MenuItem item) {
|
||||
int id = item.getItemId();
|
||||
switch (id) {
|
||||
case R.id.menu_item_done:
|
||||
saveCookiesAndFinish();
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
if (item.getItemId() == R.id.menu_item_done) {
|
||||
saveCookiesAndFinish();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private void saveCookiesAndFinish() {
|
||||
handleCookiesFromUrl(webView.getUrl()); // try to get cookies of unclosed page
|
||||
// try to get cookies of unclosed page
|
||||
handleCookiesFromUrl(recaptchaBinding.reCaptchaWebView.getUrl());
|
||||
if (MainActivity.DEBUG) {
|
||||
Log.d(TAG, "saveCookiesAndFinish: foundCookies=" + foundCookies);
|
||||
}
|
||||
@@ -173,7 +162,7 @@ public class ReCaptchaActivity extends AppCompatActivity {
|
||||
setResult(RESULT_OK);
|
||||
}
|
||||
|
||||
Intent intent = new Intent(this, org.schabi.newpipe.MainActivity.class);
|
||||
final Intent intent = new Intent(this, org.schabi.newpipe.MainActivity.class);
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
|
||||
NavUtils.navigateUpTo(this, intent);
|
||||
}
|
||||
@@ -188,13 +177,13 @@ public class ReCaptchaActivity extends AppCompatActivity {
|
||||
return;
|
||||
}
|
||||
|
||||
String cookies = CookieManager.getInstance().getCookie(url);
|
||||
final String cookies = CookieManager.getInstance().getCookie(url);
|
||||
handleCookies(cookies);
|
||||
|
||||
// sometimes cookies are inside the url
|
||||
int abuseStart = url.indexOf("google_abuse=");
|
||||
final int abuseStart = url.indexOf("google_abuse=");
|
||||
if (abuseStart != -1) {
|
||||
int abuseEnd = url.indexOf("+path");
|
||||
final int abuseEnd = url.indexOf("+path");
|
||||
|
||||
try {
|
||||
String abuseCookie = url.substring(abuseStart + 13, abuseEnd);
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.schabi.newpipe.report;
|
||||
package org.schabi.newpipe.error;
|
||||
|
||||
/**
|
||||
* The user actions that can cause an error.
|
||||
@@ -6,9 +6,12 @@ package org.schabi.newpipe.report;
|
||||
public enum UserAction {
|
||||
USER_REPORT("user report"),
|
||||
UI_ERROR("ui error"),
|
||||
SUBSCRIPTION("subscription"),
|
||||
SUBSCRIPTION_CHANGE("subscription change"),
|
||||
SUBSCRIPTION_UPDATE("subscription update"),
|
||||
SUBSCRIPTION_GET("get subscription"),
|
||||
SUBSCRIPTION_IMPORT_EXPORT("subscription import or export"),
|
||||
LOAD_IMAGE("load image"),
|
||||
SOMETHING_ELSE("something"),
|
||||
SOMETHING_ELSE("something else"),
|
||||
SEARCHED("searched"),
|
||||
GET_SUGGESTIONS("get suggestions"),
|
||||
REQUESTED_STREAM("requested stream"),
|
||||
@@ -17,10 +20,15 @@ public enum UserAction {
|
||||
REQUESTED_KIOSK("requested kiosk"),
|
||||
REQUESTED_COMMENTS("requested comments"),
|
||||
REQUESTED_FEED("requested feed"),
|
||||
REQUESTED_BOOKMARK("bookmark"),
|
||||
DELETE_FROM_HISTORY("delete from history"),
|
||||
PLAY_STREAM("Play stream"),
|
||||
PLAY_STREAM("play stream"),
|
||||
DOWNLOAD_OPEN_DIALOG("download open dialog"),
|
||||
DOWNLOAD_POSTPROCESSING("download post-processing"),
|
||||
DOWNLOAD_FAILED("download failed");
|
||||
DOWNLOAD_FAILED("download failed"),
|
||||
PREFERENCES_MIGRATION("migration of preferences"),
|
||||
SHARE_TO_NEWPIPE("share to newpipe"),
|
||||
CHECK_FOR_NEW_APP_VERSION("check for new app version");
|
||||
|
||||
|
||||
private final String message;
|
||||
@@ -1,41 +1,25 @@
|
||||
package org.schabi.newpipe.fragments;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
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 androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.StringRes;
|
||||
|
||||
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.ContentNotAvailableException;
|
||||
import org.schabi.newpipe.extractor.exceptions.ContentNotSupportedException;
|
||||
import org.schabi.newpipe.extractor.exceptions.ReCaptchaException;
|
||||
import org.schabi.newpipe.report.ErrorActivity;
|
||||
import org.schabi.newpipe.report.UserAction;
|
||||
import org.schabi.newpipe.util.ExceptionUtils;
|
||||
import org.schabi.newpipe.error.ErrorActivity;
|
||||
import org.schabi.newpipe.error.ErrorInfo;
|
||||
import org.schabi.newpipe.error.ErrorPanelHelper;
|
||||
import org.schabi.newpipe.util.InfoCache;
|
||||
|
||||
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 static org.schabi.newpipe.util.AnimationUtils.animateView;
|
||||
import static org.schabi.newpipe.ktx.ViewUtils.animate;
|
||||
|
||||
public abstract class BaseStateFragment<I> extends BaseFragment implements ViewContract<I> {
|
||||
@State
|
||||
@@ -47,12 +31,13 @@ public abstract class BaseStateFragment<I> extends BaseFragment implements ViewC
|
||||
@Nullable
|
||||
private ProgressBar loadingProgressBar;
|
||||
|
||||
protected View errorPanelRoot;
|
||||
private Button errorButtonRetry;
|
||||
private TextView errorTextView;
|
||||
private ErrorPanelHelper errorPanelHelper;
|
||||
@Nullable
|
||||
@State
|
||||
protected ErrorInfo lastPanelError = null;
|
||||
|
||||
@Override
|
||||
public void onViewCreated(final View rootView, final Bundle savedInstanceState) {
|
||||
public void onViewCreated(@NonNull final View rootView, final Bundle savedInstanceState) {
|
||||
super.onViewCreated(rootView, savedInstanceState);
|
||||
doInitialLoadLogic();
|
||||
}
|
||||
@@ -63,6 +48,14 @@ public abstract class BaseStateFragment<I> extends BaseFragment implements ViewC
|
||||
wasLoading.set(isLoading.get());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
if (lastPanelError != null) {
|
||||
showError(lastPanelError);
|
||||
}
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Init
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
@@ -70,22 +63,17 @@ public abstract class BaseStateFragment<I> extends BaseFragment implements ViewC
|
||||
@Override
|
||||
protected void initViews(final View rootView, final 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);
|
||||
errorPanelHelper = new ErrorPanelHelper(this, rootView, this::onRetryButtonClicked);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void initListeners() {
|
||||
super.initListeners();
|
||||
RxView.clicks(errorButtonRetry)
|
||||
.debounce(300, TimeUnit.MILLISECONDS)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(o -> onRetryButtonClicked());
|
||||
public void onDestroyView() {
|
||||
super.onDestroyView();
|
||||
if (errorPanelHelper != null) {
|
||||
errorPanelHelper.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
protected void onRetryButtonClicked() {
|
||||
@@ -119,54 +107,34 @@ public abstract class BaseStateFragment<I> extends BaseFragment implements ViewC
|
||||
@Override
|
||||
public void showLoading() {
|
||||
if (emptyStateView != null) {
|
||||
animateView(emptyStateView, false, 150);
|
||||
animate(emptyStateView, false, 150);
|
||||
}
|
||||
if (loadingProgressBar != null) {
|
||||
animateView(loadingProgressBar, true, 400);
|
||||
animate(loadingProgressBar, true, 400);
|
||||
}
|
||||
animateView(errorPanelRoot, false, 150);
|
||||
hideErrorPanel();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void hideLoading() {
|
||||
if (emptyStateView != null) {
|
||||
animateView(emptyStateView, false, 150);
|
||||
animate(emptyStateView, false, 150);
|
||||
}
|
||||
if (loadingProgressBar != null) {
|
||||
animateView(loadingProgressBar, false, 0);
|
||||
animate(loadingProgressBar, false, 0);
|
||||
}
|
||||
animateView(errorPanelRoot, false, 150);
|
||||
hideErrorPanel();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void showEmptyState() {
|
||||
isLoading.set(false);
|
||||
if (emptyStateView != null) {
|
||||
animateView(emptyStateView, true, 200);
|
||||
animate(emptyStateView, true, 200);
|
||||
}
|
||||
if (loadingProgressBar != null) {
|
||||
animateView(loadingProgressBar, false, 0);
|
||||
animate(loadingProgressBar, false, 0);
|
||||
}
|
||||
animateView(errorPanelRoot, false, 150);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void showError(final String message, final 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);
|
||||
hideErrorPanel();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -177,120 +145,69 @@ public abstract class BaseStateFragment<I> extends BaseFragment implements ViewC
|
||||
hideLoading();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleError() {
|
||||
isLoading.set(false);
|
||||
InfoCache.getInstance().clearCache();
|
||||
if (emptyStateView != null) {
|
||||
animate(emptyStateView, false, 150);
|
||||
}
|
||||
if (loadingProgressBar != null) {
|
||||
animate(loadingProgressBar, false, 0);
|
||||
}
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Error handling
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
/**
|
||||
* Default implementation handles some general exceptions.
|
||||
*
|
||||
* @param exception The exception that should be handled
|
||||
* @return If the exception was handled
|
||||
*/
|
||||
protected boolean onError(final Throwable exception) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "onError() called with: exception = [" + exception + "]");
|
||||
}
|
||||
isLoading.set(false);
|
||||
public final void showError(final ErrorInfo errorInfo) {
|
||||
handleError();
|
||||
|
||||
if (isDetached() || isRemoving()) {
|
||||
if (DEBUG) {
|
||||
Log.w(TAG, "onError() is detached or removing = [" + exception + "]");
|
||||
Log.w(TAG, "showError() is detached or removing = [" + errorInfo + "]");
|
||||
}
|
||||
return true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (ExceptionUtils.isInterruptedCaused(exception)) {
|
||||
errorPanelHelper.showError(errorInfo);
|
||||
lastPanelError = errorInfo;
|
||||
}
|
||||
|
||||
public final void showTextError(@NonNull final String errorString) {
|
||||
handleError();
|
||||
|
||||
if (isDetached() || isRemoving()) {
|
||||
if (DEBUG) {
|
||||
Log.w(TAG, "onError() isInterruptedCaused! = [" + exception + "]");
|
||||
Log.w(TAG, "showTextError() is detached or removing = [" + errorString + "]");
|
||||
}
|
||||
return true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (exception instanceof ReCaptchaException) {
|
||||
onReCaptchaException((ReCaptchaException) exception);
|
||||
return true;
|
||||
} else if (exception instanceof ContentNotAvailableException) {
|
||||
showError(getString(R.string.content_not_available), false);
|
||||
return true;
|
||||
} else if (ExceptionUtils.isNetworkRelated(exception)) {
|
||||
showError(getString(R.string.network_error), true);
|
||||
return true;
|
||||
} else if (exception instanceof ContentNotSupportedException) {
|
||||
showError(getString(R.string.content_not_supported), false);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
errorPanelHelper.showTextError(errorString);
|
||||
}
|
||||
|
||||
public void onReCaptchaException(final ReCaptchaException exception) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "onReCaptchaException() called");
|
||||
}
|
||||
Toast.makeText(activity, R.string.recaptcha_request_toast, Toast.LENGTH_LONG).show();
|
||||
// Starting ReCaptcha Challenge Activity
|
||||
Intent intent = new Intent(activity, ReCaptchaActivity.class);
|
||||
intent.putExtra(ReCaptchaActivity.RECAPTCHA_URL_EXTRA, exception.getUrl());
|
||||
startActivityForResult(intent, ReCaptchaActivity.RECAPTCHA_REQUEST);
|
||||
|
||||
showError(getString(R.string.recaptcha_request_toast), false);
|
||||
public final void hideErrorPanel() {
|
||||
errorPanelHelper.hide();
|
||||
lastPanelError = null;
|
||||
}
|
||||
|
||||
public void onUnrecoverableError(final Throwable exception, final UserAction userAction,
|
||||
final String serviceName, final String request,
|
||||
@StringRes final int errorId) {
|
||||
onUnrecoverableError(Collections.singletonList(exception), userAction, serviceName,
|
||||
request, errorId);
|
||||
}
|
||||
|
||||
public void onUnrecoverableError(final List<Throwable> exception, final UserAction userAction,
|
||||
final String serviceName, final String request,
|
||||
@StringRes final int errorId) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "onUnrecoverableError() called with: exception = [" + exception + "]");
|
||||
}
|
||||
|
||||
ErrorActivity.reportError(getContext(), exception, MainActivity.class, null,
|
||||
ErrorActivity.ErrorInfo.make(userAction, serviceName == null ? "none" : serviceName,
|
||||
request == null ? "none" : request, errorId));
|
||||
}
|
||||
|
||||
public void showSnackBarError(final Throwable exception, final UserAction userAction,
|
||||
final String serviceName, final String request,
|
||||
@StringRes final int errorId) {
|
||||
showSnackBarError(Collections.singletonList(exception), userAction, serviceName, request,
|
||||
errorId);
|
||||
public final boolean isErrorPanelVisible() {
|
||||
return errorPanelHelper.isVisible();
|
||||
}
|
||||
|
||||
/**
|
||||
* Show a SnackBar and only call
|
||||
* {@link ErrorActivity#reportError(Context, List, Class, View, ErrorActivity.ErrorInfo)}
|
||||
* {@link ErrorActivity#reportErrorInSnackbar(androidx.fragment.app.Fragment, ErrorInfo)}
|
||||
* IF we a find a valid view (otherwise the error screen appears).
|
||||
*
|
||||
* @param exception List of the exceptions to show
|
||||
* @param userAction The user action that caused the exception
|
||||
* @param serviceName The service where the exception happened
|
||||
* @param request The page that was requested
|
||||
* @param errorId The ID of the error
|
||||
* @param errorInfo The error information
|
||||
*/
|
||||
public void showSnackBarError(final List<Throwable> exception, final UserAction userAction,
|
||||
final String serviceName, final String request,
|
||||
@StringRes final int errorId) {
|
||||
public void showSnackBarError(final ErrorInfo errorInfo) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "showSnackBarError() called with: "
|
||||
+ "exception = [" + exception + "], userAction = [" + userAction + "], "
|
||||
+ "request = [" + request + "], errorId = [" + errorId + "]");
|
||||
Log.d(TAG, "showSnackBarError() called with: errorInfo = [" + errorInfo + "]");
|
||||
}
|
||||
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));
|
||||
ErrorActivity.reportErrorInSnackbar(this, errorInfo);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,9 +11,18 @@ import org.schabi.newpipe.BaseFragment;
|
||||
import org.schabi.newpipe.R;
|
||||
|
||||
public class EmptyFragment extends BaseFragment {
|
||||
final boolean showMessage;
|
||||
|
||||
public EmptyFragment(final boolean showMessage) {
|
||||
this.showMessage = showMessage;
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(final LayoutInflater inflater, @Nullable final ViewGroup container,
|
||||
final Bundle savedInstanceState) {
|
||||
return inflater.inflate(R.layout.fragment_empty, container, false);
|
||||
final View view = inflater.inflate(R.layout.fragment_empty, container, false);
|
||||
view.findViewById(R.id.empty_state_view).setVisibility(
|
||||
showMessage ? View.VISIBLE : View.GONE);
|
||||
return view;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
package org.schabi.newpipe.fragments;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.ColorStateList;
|
||||
import android.os.Bundle;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
@@ -15,35 +13,31 @@ import android.view.ViewGroup;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
import androidx.fragment.app.FragmentStatePagerAdapterMenuWorkaround;
|
||||
import androidx.viewpager.widget.ViewPager;
|
||||
import androidx.preference.PreferenceManager;
|
||||
|
||||
import com.google.android.material.tabs.TabLayout;
|
||||
|
||||
import org.schabi.newpipe.BaseFragment;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.databinding.FragmentMainBinding;
|
||||
import org.schabi.newpipe.error.ErrorActivity;
|
||||
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||
import org.schabi.newpipe.report.ErrorActivity;
|
||||
import org.schabi.newpipe.report.UserAction;
|
||||
import org.schabi.newpipe.settings.tabs.Tab;
|
||||
import org.schabi.newpipe.settings.tabs.TabsManager;
|
||||
import org.schabi.newpipe.util.NavigationHelper;
|
||||
import org.schabi.newpipe.util.ServiceHelper;
|
||||
import org.schabi.newpipe.util.ThemeHelper;
|
||||
import org.schabi.newpipe.views.ScrollableTabLayout;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class MainFragment extends BaseFragment implements TabLayout.OnTabSelectedListener {
|
||||
private ViewPager viewPager;
|
||||
private FragmentMainBinding binding;
|
||||
private SelectedTabsPagerAdapter pagerAdapter;
|
||||
private ScrollableTabLayout tabLayout;
|
||||
|
||||
private List<Tab> tabsList = new ArrayList<>();
|
||||
private final List<Tab> tabsList = new ArrayList<>();
|
||||
private TabsManager tabsManager;
|
||||
|
||||
private boolean hasTabsChanged = false;
|
||||
@@ -74,7 +68,7 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte
|
||||
|
||||
youtubeRestrictedModeEnabledKey = getString(R.string.youtube_restricted_mode_enabled);
|
||||
previousYoutubeRestrictedModeEnabled =
|
||||
PreferenceManager.getDefaultSharedPreferences(getContext())
|
||||
PreferenceManager.getDefaultSharedPreferences(requireContext())
|
||||
.getBoolean(youtubeRestrictedModeEnabledKey, false);
|
||||
}
|
||||
|
||||
@@ -89,13 +83,12 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte
|
||||
protected void initViews(final View rootView, final Bundle savedInstanceState) {
|
||||
super.initViews(rootView, savedInstanceState);
|
||||
|
||||
tabLayout = rootView.findViewById(R.id.main_tab_layout);
|
||||
viewPager = rootView.findViewById(R.id.pager);
|
||||
binding = FragmentMainBinding.bind(rootView);
|
||||
|
||||
tabLayout.setTabIconTint(ColorStateList.valueOf(
|
||||
ThemeHelper.resolveColorFromAttr(requireContext(), R.attr.colorAccent)));
|
||||
tabLayout.setupWithViewPager(viewPager);
|
||||
tabLayout.addOnTabSelectedListener(this);
|
||||
binding.mainTabLayout.setupWithViewPager(binding.pager);
|
||||
binding.mainTabLayout.addOnTabSelectedListener(this);
|
||||
binding.mainTabLayout.setTabRippleColor(binding.mainTabLayout.getTabRippleColor()
|
||||
.withAlpha(32));
|
||||
|
||||
setupTabs();
|
||||
}
|
||||
@@ -104,8 +97,8 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
|
||||
boolean youtubeRestrictedModeEnabled =
|
||||
PreferenceManager.getDefaultSharedPreferences(getContext())
|
||||
final boolean youtubeRestrictedModeEnabled =
|
||||
PreferenceManager.getDefaultSharedPreferences(requireContext())
|
||||
.getBoolean(youtubeRestrictedModeEnabledKey, false);
|
||||
if (previousYoutubeRestrictedModeEnabled != youtubeRestrictedModeEnabled) {
|
||||
previousYoutubeRestrictedModeEnabled = youtubeRestrictedModeEnabled;
|
||||
@@ -119,8 +112,9 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
tabsManager.unsetSavedTabsListener();
|
||||
if (viewPager != null) {
|
||||
viewPager.setAdapter(null);
|
||||
if (binding != null) {
|
||||
binding.pager.setAdapter(null);
|
||||
binding = null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -129,7 +123,8 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public void onCreateOptionsMenu(final Menu menu, final MenuInflater inflater) {
|
||||
public void onCreateOptionsMenu(@NonNull final Menu menu,
|
||||
@NonNull final MenuInflater inflater) {
|
||||
super.onCreateOptionsMenu(menu, inflater);
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "onCreateOptionsMenu() called with: "
|
||||
@@ -137,7 +132,7 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte
|
||||
}
|
||||
inflater.inflate(R.menu.main_fragment_menu, menu);
|
||||
|
||||
ActionBar supportActionBar = activity.getSupportActionBar();
|
||||
final ActionBar supportActionBar = activity.getSupportActionBar();
|
||||
if (supportActionBar != null) {
|
||||
supportActionBar.setDisplayHomeAsUpEnabled(false);
|
||||
}
|
||||
@@ -145,17 +140,14 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(final MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case R.id.action_search:
|
||||
try {
|
||||
NavigationHelper.openSearchFragment(
|
||||
getFragmentManager(),
|
||||
ServiceHelper.getSelectedServiceId(activity),
|
||||
"");
|
||||
} catch (Exception e) {
|
||||
ErrorActivity.reportUiError((AppCompatActivity) getActivity(), e);
|
||||
}
|
||||
return true;
|
||||
if (item.getItemId() == R.id.action_search) {
|
||||
try {
|
||||
NavigationHelper.openSearchFragment(getFM(),
|
||||
ServiceHelper.getSelectedServiceId(activity), "");
|
||||
} catch (final Exception e) {
|
||||
ErrorActivity.reportUiErrorInSnackbar(this, "Opening search fragment", e);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
@@ -173,19 +165,19 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte
|
||||
getChildFragmentManager(), tabsList);
|
||||
}
|
||||
|
||||
viewPager.setAdapter(null);
|
||||
viewPager.setOffscreenPageLimit(tabsList.size());
|
||||
viewPager.setAdapter(pagerAdapter);
|
||||
binding.pager.setAdapter(null);
|
||||
binding.pager.setOffscreenPageLimit(tabsList.size());
|
||||
binding.pager.setAdapter(pagerAdapter);
|
||||
|
||||
updateTabsIconAndDescription();
|
||||
updateTitleForTab(viewPager.getCurrentItem());
|
||||
updateTitleForTab(binding.pager.getCurrentItem());
|
||||
|
||||
hasTabsChanged = false;
|
||||
}
|
||||
|
||||
private void updateTabsIconAndDescription() {
|
||||
for (int i = 0; i < tabsList.size(); i++) {
|
||||
final TabLayout.Tab tabToSet = tabLayout.getTabAt(i);
|
||||
final TabLayout.Tab tabToSet = binding.mainTabLayout.getTabAt(i);
|
||||
if (tabToSet != null) {
|
||||
final Tab tab = tabsList.get(i);
|
||||
tabToSet.setIcon(tab.getTabIconRes(requireContext()));
|
||||
@@ -239,13 +231,12 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte
|
||||
Fragment fragment = null;
|
||||
try {
|
||||
fragment = tab.getFragment(context);
|
||||
} catch (ExtractionException e) {
|
||||
} catch (final ExtractionException e) {
|
||||
throwable = e;
|
||||
}
|
||||
|
||||
if (throwable != null) {
|
||||
ErrorActivity.reportError(context, throwable, null, null, ErrorActivity.ErrorInfo
|
||||
.make(UserAction.UI_ERROR, "none", "", R.string.app_ui_crash));
|
||||
ErrorActivity.reportUiErrorInSnackbar(context, "Getting fragment item", throwable);
|
||||
return new BlankFragment();
|
||||
}
|
||||
|
||||
@@ -257,7 +248,7 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemPosition(final Object object) {
|
||||
public int getItemPosition(@NonNull final Object object) {
|
||||
// Causes adapter to reload all Fragments when
|
||||
// notifyDataSetChanged is called
|
||||
return POSITION_NONE;
|
||||
|
||||
@@ -14,19 +14,17 @@ public abstract class OnScrollBelowItemsListener extends RecyclerView.OnScrollLi
|
||||
super.onScrolled(recyclerView, dx, dy);
|
||||
if (dy > 0) {
|
||||
int pastVisibleItems = 0;
|
||||
int visibleItemCount;
|
||||
int totalItemCount;
|
||||
RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager();
|
||||
final RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager();
|
||||
|
||||
visibleItemCount = layoutManager.getChildCount();
|
||||
totalItemCount = layoutManager.getItemCount();
|
||||
final int visibleItemCount = layoutManager.getChildCount();
|
||||
final int totalItemCount = layoutManager.getItemCount();
|
||||
|
||||
// Already covers the GridLayoutManager case
|
||||
if (layoutManager instanceof LinearLayoutManager) {
|
||||
pastVisibleItems = ((LinearLayoutManager) layoutManager)
|
||||
.findFirstVisibleItemPosition();
|
||||
} else if (layoutManager instanceof StaggeredGridLayoutManager) {
|
||||
int[] positions = ((StaggeredGridLayoutManager) layoutManager)
|
||||
final int[] positions = ((StaggeredGridLayoutManager) layoutManager)
|
||||
.findFirstVisibleItemPositions(null);
|
||||
if (positions != null && positions.length > 0) {
|
||||
pastVisibleItems = positions[0];
|
||||
|
||||
@@ -7,7 +7,7 @@ public interface ViewContract<I> {
|
||||
|
||||
void showEmptyState();
|
||||
|
||||
void showError(String message, boolean showRetryButton);
|
||||
|
||||
void handleResult(I result);
|
||||
|
||||
void handleError();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
package org.schabi.newpipe.fragments.detail;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.text.HtmlCompat;
|
||||
|
||||
import org.schabi.newpipe.BaseFragment;
|
||||
import org.schabi.newpipe.databinding.FragmentDescriptionBinding;
|
||||
import org.schabi.newpipe.extractor.stream.Description;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||
import org.schabi.newpipe.util.Localization;
|
||||
import org.schabi.newpipe.util.TextLinkifier;
|
||||
|
||||
import icepick.State;
|
||||
import io.reactivex.rxjava3.disposables.Disposable;
|
||||
|
||||
import static android.text.TextUtils.isEmpty;
|
||||
|
||||
public class DescriptionFragment extends BaseFragment {
|
||||
|
||||
@State
|
||||
StreamInfo streamInfo = null;
|
||||
@Nullable
|
||||
Disposable descriptionDisposable = null;
|
||||
|
||||
public DescriptionFragment() {
|
||||
}
|
||||
|
||||
public DescriptionFragment(final StreamInfo streamInfo) {
|
||||
this.streamInfo = streamInfo;
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(@NonNull final LayoutInflater inflater,
|
||||
@Nullable final ViewGroup container,
|
||||
@Nullable final Bundle savedInstanceState) {
|
||||
final FragmentDescriptionBinding binding =
|
||||
FragmentDescriptionBinding.inflate(inflater, container, false);
|
||||
if (streamInfo != null) {
|
||||
setupUploadDate(binding.detailUploadDateView);
|
||||
setupDescription(binding.detailDescriptionView);
|
||||
}
|
||||
return binding.getRoot();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
if (descriptionDisposable != null) {
|
||||
descriptionDisposable.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
private void setupUploadDate(final TextView uploadDateTextView) {
|
||||
if (streamInfo.getUploadDate() != null) {
|
||||
uploadDateTextView.setText(Localization
|
||||
.localizeUploadDate(activity, streamInfo.getUploadDate().offsetDateTime()));
|
||||
} else {
|
||||
uploadDateTextView.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
private void setupDescription(final TextView descriptionTextView) {
|
||||
final Description description = streamInfo.getDescription();
|
||||
if (description == null || isEmpty(description.getContent())
|
||||
|| description == Description.emptyDescription) {
|
||||
descriptionTextView.setText("");
|
||||
return;
|
||||
}
|
||||
|
||||
switch (description.getType()) {
|
||||
case Description.HTML:
|
||||
descriptionDisposable = TextLinkifier.createLinksFromHtmlBlock(requireContext(),
|
||||
description.getContent(), descriptionTextView,
|
||||
HtmlCompat.FROM_HTML_MODE_LEGACY);
|
||||
break;
|
||||
case Description.MARKDOWN:
|
||||
descriptionDisposable = TextLinkifier.createLinksFromMarkdownText(requireContext(),
|
||||
description.getContent(), descriptionTextView);
|
||||
break;
|
||||
case Description.PLAIN_TEXT: default:
|
||||
descriptionDisposable = TextLinkifier.createLinksFromPlainText(requireContext(),
|
||||
description.getContent(), descriptionTextView);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,29 @@
|
||||
package org.schabi.newpipe.fragments.detail;
|
||||
|
||||
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
class StackItem implements Serializable {
|
||||
private final int serviceId;
|
||||
private final String url;
|
||||
private String url;
|
||||
private String title;
|
||||
private PlayQueue playQueue;
|
||||
|
||||
StackItem(final int serviceId, final String url, final String title) {
|
||||
StackItem(final int serviceId, final String url,
|
||||
final String title, final PlayQueue playQueue) {
|
||||
this.serviceId = serviceId;
|
||||
this.url = url;
|
||||
this.title = title;
|
||||
this.playQueue = playQueue;
|
||||
}
|
||||
|
||||
public void setUrl(final String url) {
|
||||
this.url = url;
|
||||
}
|
||||
|
||||
public void setPlayQueue(final PlayQueue queue) {
|
||||
this.playQueue = queue;
|
||||
}
|
||||
|
||||
public int getServiceId() {
|
||||
@@ -29,6 +42,10 @@ class StackItem implements Serializable {
|
||||
return url;
|
||||
}
|
||||
|
||||
public PlayQueue getPlayQueue() {
|
||||
return playQueue;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return getServiceId() + ":" + getUrl() + " > " + getTitle();
|
||||
|
||||
@@ -2,6 +2,7 @@ package org.schabi.newpipe.fragments.detail;
|
||||
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
@@ -10,16 +11,20 @@ import androidx.fragment.app.FragmentPagerAdapter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class TabAdaptor extends FragmentPagerAdapter {
|
||||
public class TabAdapter extends FragmentPagerAdapter {
|
||||
private final List<Fragment> mFragmentList = new ArrayList<>();
|
||||
private final List<String> mFragmentTitleList = new ArrayList<>();
|
||||
private final FragmentManager fragmentManager;
|
||||
|
||||
public TabAdaptor(final FragmentManager fm) {
|
||||
super(fm);
|
||||
public TabAdapter(final FragmentManager fm) {
|
||||
// if changed to BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT => crash if enqueueing stream in
|
||||
// the background and then clicking on it to open VideoDetailFragment:
|
||||
// "Cannot setMaxLifecycle for Fragment not attached to FragmentManager"
|
||||
super(fm, BEHAVIOR_SET_USER_VISIBLE_HINT);
|
||||
this.fragmentManager = fm;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Fragment getItem(final int position) {
|
||||
return mFragmentList.get(position);
|
||||
@@ -50,14 +55,14 @@ public class TabAdaptor extends FragmentPagerAdapter {
|
||||
}
|
||||
|
||||
public void updateItem(final String title, final Fragment fragment) {
|
||||
int index = mFragmentTitleList.indexOf(title);
|
||||
final int index = mFragmentTitleList.indexOf(title);
|
||||
if (index != -1) {
|
||||
updateItem(index, fragment);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemPosition(final Object object) {
|
||||
public int getItemPosition(@NonNull final Object object) {
|
||||
if (mFragmentList.contains(object)) {
|
||||
return mFragmentList.indexOf(object);
|
||||
} else {
|
||||
@@ -82,7 +87,9 @@ public class TabAdaptor extends FragmentPagerAdapter {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroyItem(final ViewGroup container, final int position, final Object object) {
|
||||
public void destroyItem(@NonNull final ViewGroup container,
|
||||
final int position,
|
||||
@NonNull final Object object) {
|
||||
fragmentManager.beginTransaction().remove((Fragment) object).commitNowAllowingStateLoss();
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -6,19 +6,22 @@ import android.content.SharedPreferences;
|
||||
import android.content.res.Configuration;
|
||||
import android.content.res.Resources;
|
||||
import android.os.Bundle;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.util.Log;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.View;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.preference.PreferenceManager;
|
||||
import androidx.recyclerview.widget.GridLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import androidx.viewbinding.ViewBinding;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.databinding.PignateFooterBinding;
|
||||
import org.schabi.newpipe.error.ErrorActivity;
|
||||
import org.schabi.newpipe.extractor.InfoItem;
|
||||
import org.schabi.newpipe.extractor.channel.ChannelInfoItem;
|
||||
import org.schabi.newpipe.extractor.comments.CommentsInfoItem;
|
||||
@@ -29,23 +32,28 @@ import org.schabi.newpipe.fragments.BaseStateFragment;
|
||||
import org.schabi.newpipe.fragments.OnScrollBelowItemsListener;
|
||||
import org.schabi.newpipe.info_list.InfoItemDialog;
|
||||
import org.schabi.newpipe.info_list.InfoListAdapter;
|
||||
import org.schabi.newpipe.report.ErrorActivity;
|
||||
import org.schabi.newpipe.player.helper.PlayerHolder;
|
||||
import org.schabi.newpipe.util.KoreUtil;
|
||||
import org.schabi.newpipe.util.NavigationHelper;
|
||||
import org.schabi.newpipe.util.OnClickGesture;
|
||||
import org.schabi.newpipe.util.StateSaver;
|
||||
import org.schabi.newpipe.util.StreamDialogEntry;
|
||||
import org.schabi.newpipe.views.SuperScrollLayoutManager;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Queue;
|
||||
|
||||
import static org.schabi.newpipe.util.AnimationUtils.animateView;
|
||||
import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty;
|
||||
import static org.schabi.newpipe.ktx.ViewUtils.animate;
|
||||
import static org.schabi.newpipe.ktx.ViewUtils.animateHideRecyclerViewAllowingScrolling;
|
||||
|
||||
public abstract class BaseListFragment<I, N> extends BaseStateFragment<I>
|
||||
implements ListViewContract<I, N>, StateSaver.WriteRead,
|
||||
SharedPreferences.OnSharedPreferenceChangeListener {
|
||||
private static final int LIST_MODE_UPDATE_FLAG = 0x32;
|
||||
protected StateSaver.SavedState savedState;
|
||||
protected org.schabi.newpipe.util.SavedState savedState;
|
||||
|
||||
private boolean useDefaultStateSaving = true;
|
||||
private int updateFlags = 0;
|
||||
@@ -63,7 +71,7 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I>
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public void onAttach(final Context context) {
|
||||
public void onAttach(@NonNull final Context context) {
|
||||
super.onAttach(context);
|
||||
|
||||
if (infoListAdapter == null) {
|
||||
@@ -117,8 +125,8 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I>
|
||||
/**
|
||||
* If the default implementation of {@link StateSaver.WriteRead} should be used.
|
||||
*
|
||||
* @see StateSaver
|
||||
* @param useDefaultStateSaving Whether the default implementation should be used
|
||||
* @see StateSaver
|
||||
*/
|
||||
public void setUseDefaultStateSaving(final boolean useDefaultStateSaving) {
|
||||
this.useDefaultStateSaving = useDefaultStateSaving;
|
||||
@@ -136,7 +144,7 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I>
|
||||
final RecyclerView.ViewHolder itemHolder =
|
||||
itemsList.findContainingViewHolder(focusedItem);
|
||||
return itemHolder.getAdapterPosition();
|
||||
} catch (NullPointerException e) {
|
||||
} catch (final NullPointerException e) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
@@ -169,7 +177,7 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I>
|
||||
}
|
||||
|
||||
itemsList.post(() -> {
|
||||
RecyclerView.ViewHolder focusedHolder =
|
||||
final RecyclerView.ViewHolder focusedHolder =
|
||||
itemsList.findViewHolderForAdapterPosition(position);
|
||||
|
||||
if (focusedHolder != null) {
|
||||
@@ -179,7 +187,7 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I>
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSaveInstanceState(final Bundle bundle) {
|
||||
public void onSaveInstanceState(@NonNull final Bundle bundle) {
|
||||
super.onSaveInstanceState(bundle);
|
||||
if (useDefaultStateSaving) {
|
||||
savedState = StateSaver
|
||||
@@ -211,12 +219,13 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I>
|
||||
// Init
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
protected View getListHeader() {
|
||||
@Nullable
|
||||
protected ViewBinding getListHeader() {
|
||||
return null;
|
||||
}
|
||||
|
||||
protected View getListFooter() {
|
||||
return activity.getLayoutInflater().inflate(R.layout.pignate_footer, itemsList, false);
|
||||
protected ViewBinding getListFooter() {
|
||||
return PignateFooterBinding.inflate(activity.getLayoutInflater(), itemsList, false);
|
||||
}
|
||||
|
||||
protected RecyclerView.LayoutManager getListLayoutManager() {
|
||||
@@ -243,8 +252,12 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I>
|
||||
itemsList.setLayoutManager(useGrid ? getGridLayoutManager() : getListLayoutManager());
|
||||
|
||||
infoListAdapter.setUseGridVariant(useGrid);
|
||||
infoListAdapter.setFooter(getListFooter());
|
||||
infoListAdapter.setHeader(getListHeader());
|
||||
infoListAdapter.setFooter(getListFooter().getRoot());
|
||||
|
||||
final ViewBinding listHeader = getListHeader();
|
||||
if (listHeader != null) {
|
||||
infoListAdapter.setHeader(listHeader.getRoot());
|
||||
}
|
||||
|
||||
itemsList.setAdapter(infoListAdapter);
|
||||
}
|
||||
@@ -279,8 +292,9 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I>
|
||||
selectedItem.getServiceId(),
|
||||
selectedItem.getUrl(),
|
||||
selectedItem.getName());
|
||||
} catch (Exception e) {
|
||||
ErrorActivity.reportUiError((AppCompatActivity) getActivity(), e);
|
||||
} catch (final Exception e) {
|
||||
ErrorActivity.reportUiErrorInSnackbar(
|
||||
BaseListFragment.this, "Opening channel fragment", e);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -294,8 +308,9 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I>
|
||||
selectedItem.getServiceId(),
|
||||
selectedItem.getUrl(),
|
||||
selectedItem.getName());
|
||||
} catch (Exception e) {
|
||||
ErrorActivity.reportUiError((AppCompatActivity) getActivity(), e);
|
||||
} catch (final Exception e) {
|
||||
ErrorActivity.reportUiErrorInSnackbar(BaseListFragment.this,
|
||||
"Opening playlist fragment", e);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -318,8 +333,9 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I>
|
||||
|
||||
private void onStreamSelected(final StreamInfoItem selectedItem) {
|
||||
onItemSelected(selectedItem);
|
||||
NavigationHelper.openVideoDetailFragment(getFM(),
|
||||
selectedItem.getServiceId(), selectedItem.getUrl(), selectedItem.getName());
|
||||
NavigationHelper.openVideoDetailFragment(requireContext(), getFM(),
|
||||
selectedItem.getServiceId(), selectedItem.getUrl(), selectedItem.getName(),
|
||||
null, false);
|
||||
}
|
||||
|
||||
protected void onScrollToBottom() {
|
||||
@@ -328,7 +344,6 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I>
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
protected void showStreamDialog(final StreamInfoItem item) {
|
||||
final Context context = getContext();
|
||||
final Activity activity = getActivity();
|
||||
@@ -336,21 +351,34 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I>
|
||||
return;
|
||||
}
|
||||
|
||||
final List<StreamDialogEntry> entries = new ArrayList<>();
|
||||
|
||||
if (PlayerHolder.getType() != null) {
|
||||
entries.add(StreamDialogEntry.enqueue);
|
||||
}
|
||||
if (item.getStreamType() == StreamType.AUDIO_STREAM) {
|
||||
StreamDialogEntry.setEnabledEntries(
|
||||
StreamDialogEntry.enqueue_on_background,
|
||||
entries.addAll(Arrays.asList(
|
||||
StreamDialogEntry.start_here_on_background,
|
||||
StreamDialogEntry.append_playlist,
|
||||
StreamDialogEntry.share);
|
||||
StreamDialogEntry.share
|
||||
));
|
||||
} else {
|
||||
StreamDialogEntry.setEnabledEntries(
|
||||
StreamDialogEntry.enqueue_on_background,
|
||||
StreamDialogEntry.enqueue_on_popup,
|
||||
entries.addAll(Arrays.asList(
|
||||
StreamDialogEntry.start_here_on_background,
|
||||
StreamDialogEntry.start_here_on_popup,
|
||||
StreamDialogEntry.append_playlist,
|
||||
StreamDialogEntry.share);
|
||||
StreamDialogEntry.share
|
||||
));
|
||||
}
|
||||
if (KoreUtil.shouldShowPlayWithKodi(context, item.getServiceId())) {
|
||||
entries.add(StreamDialogEntry.play_with_kodi);
|
||||
}
|
||||
|
||||
if (!isNullOrEmpty(item.getUploaderUrl())) {
|
||||
entries.add(StreamDialogEntry.show_channel_details);
|
||||
}
|
||||
|
||||
StreamDialogEntry.setEnabledEntries(entries);
|
||||
|
||||
new InfoItemDialog(activity, item, StreamDialogEntry.getCommands(context),
|
||||
(dialog, which) -> StreamDialogEntry.clickOn(which, this, item)).show();
|
||||
@@ -367,14 +395,10 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I>
|
||||
+ "menu = [" + menu + "], inflater = [" + inflater + "]");
|
||||
}
|
||||
super.onCreateOptionsMenu(menu, inflater);
|
||||
ActionBar supportActionBar = activity.getSupportActionBar();
|
||||
final ActionBar supportActionBar = activity.getSupportActionBar();
|
||||
if (supportActionBar != null) {
|
||||
supportActionBar.setDisplayShowTitleEnabled(true);
|
||||
if (useAsFrontPage) {
|
||||
supportActionBar.setDisplayHomeAsUpEnabled(false);
|
||||
} else {
|
||||
supportActionBar.setDisplayHomeAsUpEnabled(true);
|
||||
}
|
||||
supportActionBar.setDisplayHomeAsUpEnabled(!useAsFrontPage);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -393,26 +417,20 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I>
|
||||
@Override
|
||||
public void showLoading() {
|
||||
super.showLoading();
|
||||
// animateView(itemsList, false, 400);
|
||||
animateHideRecyclerViewAllowingScrolling(itemsList);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void hideLoading() {
|
||||
super.hideLoading();
|
||||
animateView(itemsList, true, 300);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void showError(final String message, final boolean showRetryButton) {
|
||||
super.showError(message, showRetryButton);
|
||||
showListFooter(false);
|
||||
animateView(itemsList, false, 200);
|
||||
animate(itemsList, true, 300);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void showEmptyState() {
|
||||
super.showEmptyState();
|
||||
showListFooter(false);
|
||||
animateHideRecyclerViewAllowingScrolling(itemsList);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -429,6 +447,13 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I>
|
||||
isLoading.set(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleError() {
|
||||
super.handleError();
|
||||
showListFooter(false);
|
||||
animateHideRecyclerViewAllowingScrolling(itemsList);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSharedPreferenceChanged(final SharedPreferences sharedPreferences,
|
||||
final String key) {
|
||||
|
||||
@@ -7,19 +7,25 @@ import android.view.View;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.schabi.newpipe.error.ErrorInfo;
|
||||
import org.schabi.newpipe.error.UserAction;
|
||||
import org.schabi.newpipe.extractor.ListExtractor;
|
||||
import org.schabi.newpipe.extractor.ListInfo;
|
||||
import org.schabi.newpipe.extractor.Page;
|
||||
import org.schabi.newpipe.extractor.channel.ChannelInfo;
|
||||
import org.schabi.newpipe.extractor.exceptions.ContentNotSupportedException;
|
||||
import org.schabi.newpipe.util.Constants;
|
||||
import org.schabi.newpipe.views.NewPipeRecyclerView;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Queue;
|
||||
|
||||
import icepick.State;
|
||||
import io.reactivex.Single;
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.disposables.Disposable;
|
||||
import io.reactivex.schedulers.Schedulers;
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.rxjava3.core.Single;
|
||||
import io.reactivex.rxjava3.disposables.Disposable;
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers;
|
||||
|
||||
public abstract class BaseListInfoFragment<I extends ListInfo>
|
||||
extends BaseListFragment<I, ListExtractor.InfoItemsPage> {
|
||||
@@ -30,10 +36,15 @@ public abstract class BaseListInfoFragment<I extends ListInfo>
|
||||
@State
|
||||
protected String url;
|
||||
|
||||
private final UserAction errorUserAction;
|
||||
protected I currentInfo;
|
||||
protected Page currentNextPage;
|
||||
protected Disposable currentWorker;
|
||||
|
||||
protected BaseListInfoFragment(final UserAction errorUserAction) {
|
||||
this.errorUserAction = errorUserAction;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void initViews(final View rootView, final Bundle savedInstanceState) {
|
||||
super.initViews(rootView, savedInstanceState);
|
||||
@@ -133,7 +144,9 @@ public abstract class BaseListInfoFragment<I extends ListInfo>
|
||||
currentInfo = result;
|
||||
currentNextPage = result.getNextPage();
|
||||
handleResult(result);
|
||||
}, (@NonNull Throwable throwable) -> onError(throwable));
|
||||
}, throwable ->
|
||||
showError(new ErrorInfo(throwable, errorUserAction,
|
||||
"Start loading: " + url, serviceId)));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -158,14 +171,12 @@ public abstract class BaseListInfoFragment<I extends ListInfo>
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.doFinally(this::allowDownwardFocusScroll)
|
||||
.subscribe((@io.reactivex.annotations.NonNull
|
||||
ListExtractor.InfoItemsPage InfoItemsPage) -> {
|
||||
.subscribe((@NonNull ListExtractor.InfoItemsPage InfoItemsPage) -> {
|
||||
isLoading.set(false);
|
||||
handleNextItems(InfoItemsPage);
|
||||
}, (@io.reactivex.annotations.NonNull Throwable throwable) -> {
|
||||
isLoading.set(false);
|
||||
onError(throwable);
|
||||
});
|
||||
}, (@NonNull Throwable throwable) ->
|
||||
dynamicallyShowErrorPanelOrSnackbar(new ErrorInfo(throwable,
|
||||
errorUserAction, "Loading more items: " + url, serviceId)));
|
||||
}
|
||||
|
||||
private void forbidDownwardFocusScroll() {
|
||||
@@ -183,10 +194,16 @@ public abstract class BaseListInfoFragment<I extends ListInfo>
|
||||
@Override
|
||||
public void handleNextItems(final ListExtractor.InfoItemsPage result) {
|
||||
super.handleNextItems(result);
|
||||
|
||||
currentNextPage = result.getNextPage();
|
||||
infoListAdapter.addInfoItemList(result.getItems());
|
||||
|
||||
showListFooter(hasMoreItems());
|
||||
|
||||
if (!result.getErrors().isEmpty()) {
|
||||
dynamicallyShowErrorPanelOrSnackbar(new ErrorInfo(result.getErrors(), errorUserAction,
|
||||
"Get next items of: " + url, serviceId));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -205,13 +222,29 @@ public abstract class BaseListInfoFragment<I extends ListInfo>
|
||||
name = result.getName();
|
||||
setTitle(name);
|
||||
|
||||
if (infoListAdapter.getItemsList().size() == 0) {
|
||||
if (infoListAdapter.getItemsList().isEmpty()) {
|
||||
if (result.getRelatedItems().size() > 0) {
|
||||
infoListAdapter.addInfoItemList(result.getRelatedItems());
|
||||
showListFooter(hasMoreItems());
|
||||
} else {
|
||||
infoListAdapter.clearStreamItemList();
|
||||
showEmptyState();
|
||||
// showEmptyState should be called only if there is no item as
|
||||
// well as no header in infoListAdapter
|
||||
if (!(result instanceof ChannelInfo && infoListAdapter.getItemCount() == 1)) {
|
||||
showEmptyState();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!result.getErrors().isEmpty()) {
|
||||
final List<Throwable> errors = new ArrayList<>(result.getErrors());
|
||||
// handling ContentNotSupportedException not to show the error but an appropriate string
|
||||
// so that crashes won't be sent uselessly and the user will understand what happened
|
||||
errors.removeIf(throwable -> throwable instanceof ContentNotSupportedException);
|
||||
|
||||
if (!errors.isEmpty()) {
|
||||
dynamicallyShowErrorPanelOrSnackbar(new ErrorInfo(result.getErrors(),
|
||||
errorUserAction, "Start loading: " + url, serviceId));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -225,4 +258,14 @@ public abstract class BaseListInfoFragment<I extends ListInfo>
|
||||
this.url = u;
|
||||
this.name = !TextUtils.isEmpty(title) ? title : "";
|
||||
}
|
||||
|
||||
private void dynamicallyShowErrorPanelOrSnackbar(final ErrorInfo errorInfo) {
|
||||
if (infoListAdapter.getItemCount() == 0) {
|
||||
// show error panel only if no items already visible
|
||||
showError(errorInfo);
|
||||
} else {
|
||||
isLoading.set(false);
|
||||
showSnackBarError(errorInfo);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
package org.schabi.newpipe.fragments.list.channel;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
@@ -14,34 +12,33 @@ 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 androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.viewbinding.ViewBinding;
|
||||
|
||||
import com.jakewharton.rxbinding2.view.RxView;
|
||||
import com.jakewharton.rxbinding4.view.RxView;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.database.subscription.SubscriptionEntity;
|
||||
import org.schabi.newpipe.databinding.ChannelHeaderBinding;
|
||||
import org.schabi.newpipe.databinding.FragmentChannelBinding;
|
||||
import org.schabi.newpipe.databinding.PlaylistControlBinding;
|
||||
import org.schabi.newpipe.error.ErrorActivity;
|
||||
import org.schabi.newpipe.error.ErrorInfo;
|
||||
import org.schabi.newpipe.error.UserAction;
|
||||
import org.schabi.newpipe.extractor.InfoItem;
|
||||
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.ContentNotSupportedException;
|
||||
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.ktx.AnimationType;
|
||||
import org.schabi.newpipe.local.subscription.SubscriptionManager;
|
||||
import org.schabi.newpipe.player.playqueue.ChannelPlayQueue;
|
||||
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
||||
import org.schabi.newpipe.report.ErrorActivity;
|
||||
import org.schabi.newpipe.report.UserAction;
|
||||
import org.schabi.newpipe.util.AnimationUtils;
|
||||
import org.schabi.newpipe.util.ExtractorHelper;
|
||||
import org.schabi.newpipe.util.ImageDisplayConstants;
|
||||
import org.schabi.newpipe.util.Localization;
|
||||
@@ -50,23 +47,22 @@ import org.schabi.newpipe.util.ShareUtils;
|
||||
import org.schabi.newpipe.util.ThemeHelper;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import io.reactivex.Observable;
|
||||
import io.reactivex.Single;
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.disposables.CompositeDisposable;
|
||||
import io.reactivex.disposables.Disposable;
|
||||
import io.reactivex.functions.Action;
|
||||
import io.reactivex.functions.Consumer;
|
||||
import io.reactivex.functions.Function;
|
||||
import io.reactivex.schedulers.Schedulers;
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.rxjava3.core.Observable;
|
||||
import io.reactivex.rxjava3.core.Single;
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
||||
import io.reactivex.rxjava3.disposables.Disposable;
|
||||
import io.reactivex.rxjava3.functions.Action;
|
||||
import io.reactivex.rxjava3.functions.Consumer;
|
||||
import io.reactivex.rxjava3.functions.Function;
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers;
|
||||
|
||||
import static org.schabi.newpipe.util.AnimationUtils.animateBackgroundColor;
|
||||
import static org.schabi.newpipe.util.AnimationUtils.animateTextColor;
|
||||
import static org.schabi.newpipe.util.AnimationUtils.animateView;
|
||||
import static org.schabi.newpipe.ktx.TextViewUtils.animateTextColor;
|
||||
import static org.schabi.newpipe.ktx.ViewUtils.animate;
|
||||
import static org.schabi.newpipe.ktx.ViewUtils.animateBackgroundColor;
|
||||
|
||||
public class ChannelFragment extends BaseListInfoFragment<ChannelInfo>
|
||||
implements View.OnClickListener {
|
||||
@@ -79,30 +75,24 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo>
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
private SubscriptionManager subscriptionManager;
|
||||
private View headerRootLayout;
|
||||
private ImageView headerChannelBanner;
|
||||
private ImageView headerAvatarView;
|
||||
private TextView headerTitleView;
|
||||
private ImageView headerSubChannelAvatarView;
|
||||
private TextView headerSubChannelTitleView;
|
||||
private TextView headerSubscribersTextView;
|
||||
private Button headerSubscribeButton;
|
||||
private View playlistCtrl;
|
||||
private LinearLayout headerPlayAllButton;
|
||||
private LinearLayout headerPopupButton;
|
||||
private LinearLayout headerBackgroundButton;
|
||||
|
||||
private FragmentChannelBinding channelBinding;
|
||||
private ChannelHeaderBinding headerBinding;
|
||||
private PlaylistControlBinding playlistControlBinding;
|
||||
|
||||
private MenuItem menuRssButton;
|
||||
private TextView contentNotSupportedTextView;
|
||||
private TextView kaomojiTextView;
|
||||
private TextView noVideosTextView;
|
||||
|
||||
public static ChannelFragment getInstance(final int serviceId, final String url,
|
||||
final String name) {
|
||||
ChannelFragment instance = new ChannelFragment();
|
||||
final ChannelFragment instance = new ChannelFragment();
|
||||
instance.setInitialData(serviceId, url, name);
|
||||
return instance;
|
||||
}
|
||||
|
||||
public ChannelFragment() {
|
||||
super(UserAction.REQUESTED_CHANNEL);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setUserVisibleHint(final boolean isVisibleToUser) {
|
||||
super.setUserVisibleHint(isVisibleToUser);
|
||||
@@ -118,7 +108,7 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo>
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public void onAttach(final Context context) {
|
||||
public void onAttach(@NonNull final Context context) {
|
||||
super.onAttach(context);
|
||||
subscriptionManager = new SubscriptionManager(activity);
|
||||
}
|
||||
@@ -131,55 +121,42 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo>
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(final View rootView, final Bundle savedInstanceState) {
|
||||
public void onViewCreated(@NonNull final View rootView, final Bundle savedInstanceState) {
|
||||
super.onViewCreated(rootView, savedInstanceState);
|
||||
contentNotSupportedTextView = rootView.findViewById(R.id.error_content_not_supported);
|
||||
kaomojiTextView = rootView.findViewById(R.id.channel_kaomoji);
|
||||
noVideosTextView = rootView.findViewById(R.id.channel_no_videos);
|
||||
channelBinding = FragmentChannelBinding.bind(rootView);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
if (disposables != null) {
|
||||
disposables.clear();
|
||||
}
|
||||
disposables.clear();
|
||||
if (subscribeButtonMonitor != null) {
|
||||
subscribeButtonMonitor.dispose();
|
||||
}
|
||||
channelBinding = null;
|
||||
headerBinding = null;
|
||||
playlistControlBinding = null;
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// 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);
|
||||
headerSubChannelAvatarView =
|
||||
headerRootLayout.findViewById(R.id.sub_channel_avatar_view);
|
||||
headerSubChannelTitleView =
|
||||
headerRootLayout.findViewById(R.id.sub_channel_title_view);
|
||||
@Override
|
||||
protected ViewBinding getListHeader() {
|
||||
headerBinding = ChannelHeaderBinding
|
||||
.inflate(activity.getLayoutInflater(), itemsList, false);
|
||||
playlistControlBinding = headerBinding.playlistControl;
|
||||
|
||||
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;
|
||||
return headerBinding;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void initListeners() {
|
||||
super.initListeners();
|
||||
|
||||
headerSubChannelTitleView.setOnClickListener(this);
|
||||
headerSubChannelAvatarView.setOnClickListener(this);
|
||||
headerBinding.subChannelTitleView.setOnClickListener(this);
|
||||
headerBinding.subChannelAvatarView.setOnClickListener(this);
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
@@ -189,7 +166,7 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo>
|
||||
@Override
|
||||
public void onCreateOptionsMenu(final Menu menu, final MenuInflater inflater) {
|
||||
super.onCreateOptionsMenu(menu, inflater);
|
||||
ActionBar supportActionBar = activity.getSupportActionBar();
|
||||
final ActionBar supportActionBar = activity.getSupportActionBar();
|
||||
if (useAsFrontPage && supportActionBar != null) {
|
||||
supportActionBar.setDisplayHomeAsUpEnabled(false);
|
||||
} else {
|
||||
@@ -206,8 +183,7 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo>
|
||||
private void openRssFeed() {
|
||||
final ChannelInfo info = currentInfo;
|
||||
if (info != null) {
|
||||
Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(info.getFeedUrl()));
|
||||
startActivity(intent);
|
||||
ShareUtils.openUrlInBrowser(requireContext(), info.getFeedUrl(), false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -227,7 +203,7 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo>
|
||||
break;
|
||||
case R.id.menu_item_share:
|
||||
if (currentInfo != null) {
|
||||
ShareUtils.shareUrl(requireContext(), name, currentInfo.getOriginalUrl());
|
||||
ShareUtils.shareText(requireContext(), name, currentInfo.getOriginalUrl());
|
||||
}
|
||||
break;
|
||||
default:
|
||||
@@ -242,10 +218,9 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo>
|
||||
|
||||
private void monitorSubscription(final ChannelInfo info) {
|
||||
final Consumer<Throwable> onError = (Throwable throwable) -> {
|
||||
animateView(headerSubscribeButton, false, 100);
|
||||
showSnackBarError(throwable, UserAction.SUBSCRIPTION,
|
||||
NewPipe.getNameOfService(currentInfo.getServiceId()),
|
||||
"Get subscription status", 0);
|
||||
animate(headerBinding.channelSubscribeButton, false, 100);
|
||||
showSnackBarError(new ErrorInfo(throwable, UserAction.SUBSCRIPTION_GET,
|
||||
"Get subscription status", currentInfo));
|
||||
};
|
||||
|
||||
final Observable<List<SubscriptionEntity>> observable = subscriptionManager
|
||||
@@ -295,11 +270,8 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo>
|
||||
};
|
||||
|
||||
final Consumer<Throwable> onError = (@NonNull Throwable throwable) ->
|
||||
onUnrecoverableError(throwable,
|
||||
UserAction.SUBSCRIPTION,
|
||||
NewPipe.getNameOfService(info.getServiceId()),
|
||||
"Updating Subscription for " + info.getUrl(),
|
||||
R.string.subscription_update_failed);
|
||||
showSnackBarError(new ErrorInfo(throwable, UserAction.SUBSCRIPTION_UPDATE,
|
||||
"Updating subscription for " + info.getUrl(), info));
|
||||
|
||||
disposables.add(subscriptionManager.updateChannelInfo(info)
|
||||
.subscribeOn(Schedulers.io())
|
||||
@@ -316,11 +288,8 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo>
|
||||
};
|
||||
|
||||
final Consumer<Throwable> onError = (@NonNull Throwable throwable) ->
|
||||
onUnrecoverableError(throwable,
|
||||
UserAction.SUBSCRIPTION,
|
||||
NewPipe.getNameOfService(currentInfo.getServiceId()),
|
||||
"Subscription Change",
|
||||
R.string.subscription_change_failed);
|
||||
showSnackBarError(new ErrorInfo(throwable, UserAction.SUBSCRIPTION_CHANGE,
|
||||
"Changing subscription for " + currentInfo.getUrl(), currentInfo));
|
||||
|
||||
/* Emit clicks from main thread unto io thread */
|
||||
return RxView.clicks(subscribeButton)
|
||||
@@ -345,22 +314,22 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo>
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "No subscription to this channel!");
|
||||
}
|
||||
SubscriptionEntity channel = new SubscriptionEntity();
|
||||
final 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, info));
|
||||
subscribeButtonMonitor = monitorSubscribeButton(
|
||||
headerBinding.channelSubscribeButton, mapOnSubscribe(channel, info));
|
||||
} else {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "Found subscription to this channel!");
|
||||
}
|
||||
final SubscriptionEntity subscription = subscriptionEntities.get(0);
|
||||
subscribeButtonMonitor = monitorSubscribeButton(headerSubscribeButton,
|
||||
mapOnUnsubscribe(subscription));
|
||||
subscribeButtonMonitor = monitorSubscribeButton(
|
||||
headerBinding.channelSubscribeButton, mapOnUnsubscribe(subscription));
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -371,30 +340,34 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo>
|
||||
+ "isSubscribed = [" + isSubscribed + "]");
|
||||
}
|
||||
|
||||
boolean isButtonVisible = headerSubscribeButton.getVisibility() == View.VISIBLE;
|
||||
int backgroundDuration = isButtonVisible ? 300 : 0;
|
||||
int textDuration = isButtonVisible ? 200 : 0;
|
||||
final boolean isButtonVisible = headerBinding.channelSubscribeButton.getVisibility()
|
||||
== View.VISIBLE;
|
||||
final int backgroundDuration = isButtonVisible ? 300 : 0;
|
||||
final int textDuration = isButtonVisible ? 200 : 0;
|
||||
|
||||
int subscribeBackground = ThemeHelper
|
||||
final int subscribeBackground = ThemeHelper
|
||||
.resolveColorFromAttr(activity, R.attr.colorPrimary);
|
||||
int subscribeText = ContextCompat.getColor(activity, R.color.subscribe_text_color);
|
||||
int subscribedBackground = ContextCompat
|
||||
final int subscribeText = ContextCompat.getColor(activity, R.color.subscribe_text_color);
|
||||
final int subscribedBackground = ContextCompat
|
||||
.getColor(activity, R.color.subscribed_background_color);
|
||||
int subscribedText = ContextCompat.getColor(activity, R.color.subscribed_text_color);
|
||||
final 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);
|
||||
headerBinding.channelSubscribeButton.setText(R.string.subscribe_button_title);
|
||||
animateBackgroundColor(headerBinding.channelSubscribeButton, backgroundDuration,
|
||||
subscribedBackground, subscribeBackground);
|
||||
animateTextColor(headerBinding.channelSubscribeButton, textDuration, subscribedText,
|
||||
subscribeText);
|
||||
} else {
|
||||
headerSubscribeButton.setText(R.string.subscribed_button_title);
|
||||
animateBackgroundColor(headerSubscribeButton, backgroundDuration, subscribeBackground,
|
||||
subscribedBackground);
|
||||
animateTextColor(headerSubscribeButton, textDuration, subscribeText, subscribedText);
|
||||
headerBinding.channelSubscribeButton.setText(R.string.subscribed_button_title);
|
||||
animateBackgroundColor(headerBinding.channelSubscribeButton, backgroundDuration,
|
||||
subscribeBackground, subscribedBackground);
|
||||
animateTextColor(headerBinding.channelSubscribeButton, textDuration, subscribeText,
|
||||
subscribedText);
|
||||
}
|
||||
|
||||
animateView(headerSubscribeButton, AnimationUtils.Type.LIGHT_SCALE_AND_ALPHA, true, 100);
|
||||
animate(headerBinding.channelSubscribeButton, true, 100,
|
||||
AnimationType.LIGHT_SCALE_AND_ALPHA);
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
@@ -426,11 +399,11 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo>
|
||||
case R.id.sub_channel_title_view:
|
||||
if (!TextUtils.isEmpty(currentInfo.getParentChannelUrl())) {
|
||||
try {
|
||||
NavigationHelper.openChannelFragment(getFragmentManager(),
|
||||
currentInfo.getServiceId(), currentInfo.getParentChannelUrl(),
|
||||
NavigationHelper.openChannelFragment(getFM(), currentInfo.getServiceId(),
|
||||
currentInfo.getParentChannelUrl(),
|
||||
currentInfo.getParentChannelName());
|
||||
} catch (Exception e) {
|
||||
ErrorActivity.reportUiError((AppCompatActivity) getActivity(), e);
|
||||
} catch (final Exception e) {
|
||||
ErrorActivity.reportUiErrorInSnackbar(this, "Opening channel fragment", e);
|
||||
}
|
||||
} else if (DEBUG) {
|
||||
Log.i(TAG, "Can't open parent channel because we got no channel URL");
|
||||
@@ -447,100 +420,95 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo>
|
||||
public void showLoading() {
|
||||
super.showLoading();
|
||||
|
||||
IMAGE_LOADER.cancelDisplayTask(headerChannelBanner);
|
||||
IMAGE_LOADER.cancelDisplayTask(headerAvatarView);
|
||||
IMAGE_LOADER.cancelDisplayTask(headerSubChannelAvatarView);
|
||||
animateView(headerSubscribeButton, false, 100);
|
||||
IMAGE_LOADER.cancelDisplayTask(headerBinding.channelBannerImage);
|
||||
IMAGE_LOADER.cancelDisplayTask(headerBinding.channelAvatarView);
|
||||
IMAGE_LOADER.cancelDisplayTask(headerBinding.subChannelAvatarView);
|
||||
animate(headerBinding.channelSubscribeButton, false, 100);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleResult(@NonNull final ChannelInfo result) {
|
||||
super.handleResult(result);
|
||||
|
||||
headerRootLayout.setVisibility(View.VISIBLE);
|
||||
IMAGE_LOADER.displayImage(result.getBannerUrl(), headerChannelBanner,
|
||||
headerBinding.getRoot().setVisibility(View.VISIBLE);
|
||||
IMAGE_LOADER.displayImage(result.getBannerUrl(), headerBinding.channelBannerImage,
|
||||
ImageDisplayConstants.DISPLAY_BANNER_OPTIONS);
|
||||
IMAGE_LOADER.displayImage(result.getAvatarUrl(), headerAvatarView,
|
||||
IMAGE_LOADER.displayImage(result.getAvatarUrl(), headerBinding.channelAvatarView,
|
||||
ImageDisplayConstants.DISPLAY_AVATAR_OPTIONS);
|
||||
IMAGE_LOADER.displayImage(result.getParentChannelAvatarUrl(), headerSubChannelAvatarView,
|
||||
IMAGE_LOADER.displayImage(result.getParentChannelAvatarUrl(),
|
||||
headerBinding.subChannelAvatarView,
|
||||
ImageDisplayConstants.DISPLAY_AVATAR_OPTIONS);
|
||||
|
||||
headerSubscribersTextView.setVisibility(View.VISIBLE);
|
||||
headerBinding.channelSubscriberView.setVisibility(View.VISIBLE);
|
||||
if (result.getSubscriberCount() >= 0) {
|
||||
headerSubscribersTextView.setText(Localization
|
||||
headerBinding.channelSubscriberView.setText(Localization
|
||||
.shortSubscriberCount(activity, result.getSubscriberCount()));
|
||||
} else {
|
||||
headerSubscribersTextView.setText(R.string.subscribers_count_not_available);
|
||||
headerBinding.channelSubscriberView.setText(R.string.subscribers_count_not_available);
|
||||
}
|
||||
|
||||
if (!TextUtils.isEmpty(currentInfo.getParentChannelName())) {
|
||||
headerSubChannelTitleView.setText(String.format(
|
||||
getString(R.string.channel_created_by),
|
||||
currentInfo.getParentChannelName())
|
||||
headerBinding.subChannelTitleView.setText(String.format(
|
||||
getString(R.string.channel_created_by),
|
||||
currentInfo.getParentChannelName())
|
||||
);
|
||||
headerSubChannelTitleView.setVisibility(View.VISIBLE);
|
||||
headerSubChannelAvatarView.setVisibility(View.VISIBLE);
|
||||
headerBinding.subChannelTitleView.setVisibility(View.VISIBLE);
|
||||
headerBinding.subChannelAvatarView.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
headerSubChannelTitleView.setVisibility(View.GONE);
|
||||
headerBinding.subChannelTitleView.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
if (menuRssButton != null) {
|
||||
menuRssButton.setVisible(!TextUtils.isEmpty(result.getFeedUrl()));
|
||||
}
|
||||
|
||||
playlistCtrl.setVisibility(View.VISIBLE);
|
||||
// PlaylistControls should be visible only if there is some item in
|
||||
// infoListAdapter other than header
|
||||
if (infoListAdapter.getItemCount() != 1) {
|
||||
playlistControlBinding.getRoot().setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
playlistControlBinding.getRoot().setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
List<Throwable> errors = new ArrayList<>(result.getErrors());
|
||||
if (!errors.isEmpty()) {
|
||||
|
||||
// handling ContentNotSupportedException not to show the error but an appropriate string
|
||||
// so that crashes won't be sent uselessly and the user will understand what happened
|
||||
for (Iterator<Throwable> it = errors.iterator(); it.hasNext();) {
|
||||
Throwable throwable = it.next();
|
||||
if (throwable instanceof ContentNotSupportedException) {
|
||||
showContentNotSupported();
|
||||
it.remove();
|
||||
}
|
||||
}
|
||||
|
||||
if (!errors.isEmpty()) {
|
||||
showSnackBarError(errors, UserAction.REQUESTED_CHANNEL,
|
||||
NewPipe.getNameOfService(result.getServiceId()), result.getUrl(), 0);
|
||||
for (final Throwable throwable : result.getErrors()) {
|
||||
if (throwable instanceof ContentNotSupportedException) {
|
||||
showContentNotSupported();
|
||||
}
|
||||
}
|
||||
|
||||
if (disposables != null) {
|
||||
disposables.clear();
|
||||
}
|
||||
disposables.clear();
|
||||
if (subscribeButtonMonitor != null) {
|
||||
subscribeButtonMonitor.dispose();
|
||||
}
|
||||
updateSubscription(result);
|
||||
monitorSubscription(result);
|
||||
|
||||
headerPlayAllButton.setOnClickListener(view -> NavigationHelper
|
||||
.playOnMainPlayer(activity, getPlayQueue(), false));
|
||||
headerPopupButton.setOnClickListener(view -> NavigationHelper
|
||||
.playOnPopupPlayer(activity, getPlayQueue(), false));
|
||||
headerBackgroundButton.setOnClickListener(view -> NavigationHelper
|
||||
.playOnBackgroundPlayer(activity, getPlayQueue(), false));
|
||||
playlistControlBinding.playlistCtrlPlayAllButton
|
||||
.setOnClickListener(view -> NavigationHelper
|
||||
.playOnMainPlayer(activity, getPlayQueue()));
|
||||
playlistControlBinding.playlistCtrlPlayPopupButton
|
||||
.setOnClickListener(view -> NavigationHelper
|
||||
.playOnPopupPlayer(activity, getPlayQueue(), false));
|
||||
playlistControlBinding.playlistCtrlPlayBgButton
|
||||
.setOnClickListener(view -> NavigationHelper
|
||||
.playOnBackgroundPlayer(activity, getPlayQueue(), false));
|
||||
|
||||
headerPopupButton.setOnLongClickListener(view -> {
|
||||
playlistControlBinding.playlistCtrlPlayPopupButton.setOnLongClickListener(view -> {
|
||||
NavigationHelper.enqueueOnPopupPlayer(activity, getPlayQueue(), true);
|
||||
return true;
|
||||
});
|
||||
|
||||
headerBackgroundButton.setOnLongClickListener(view -> {
|
||||
playlistControlBinding.playlistCtrlPlayBgButton.setOnLongClickListener(view -> {
|
||||
NavigationHelper.enqueueOnBackgroundPlayer(activity, getPlayQueue(), true);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
private void showContentNotSupported() {
|
||||
contentNotSupportedTextView.setVisibility(View.VISIBLE);
|
||||
kaomojiTextView.setText("(︶︹︺)");
|
||||
kaomojiTextView.setTextSize(TypedValue.COMPLEX_UNIT_SP, 45f);
|
||||
noVideosTextView.setVisibility(View.GONE);
|
||||
channelBinding.errorContentNotSupported.setVisibility(View.VISIBLE);
|
||||
channelBinding.channelKaomoji.setText("(︶︹︺)");
|
||||
channelBinding.channelKaomoji.setTextSize(TypedValue.COMPLEX_UNIT_SP, 45f);
|
||||
channelBinding.channelNoVideos.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
private PlayQueue getPlayQueue() {
|
||||
@@ -549,7 +517,7 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo>
|
||||
|
||||
private PlayQueue getPlayQueue(final int index) {
|
||||
final List<StreamInfoItem> streamItems = new ArrayList<>();
|
||||
for (InfoItem i : infoListAdapter.getItemsList()) {
|
||||
for (final InfoItem i : infoListAdapter.getItemsList()) {
|
||||
if (i instanceof StreamInfoItem) {
|
||||
streamItems.add((StreamInfoItem) i);
|
||||
}
|
||||
@@ -558,38 +526,6 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo>
|
||||
currentInfo.getNextPage(), streamItems, index);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleNextItems(final ListExtractor.InfoItemsPage 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(final 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
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
@@ -598,7 +534,7 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo>
|
||||
public void setTitle(final String title) {
|
||||
super.setTitle(title);
|
||||
if (!useAsFrontPage) {
|
||||
headerTitleView.setText(title);
|
||||
headerBinding.channelTitleView.setText(title);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user