Compare commits
765 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1c43f27bf7 | ||
|
|
3f8c4bf662 | ||
|
|
c3890f7ac0 | ||
|
|
ab165301d8 | ||
|
|
16dfd24849 | ||
|
|
9a189c57b7 | ||
|
|
d7d774ffbf | ||
|
|
271e0cf320 | ||
|
|
8b4275dea2 | ||
|
|
5b09d3b113 | ||
|
|
62f98901b0 | ||
|
|
19381684ae | ||
|
|
176a7e3391 | ||
|
|
2dbdbe8c28 | ||
|
|
1502dc805b | ||
|
|
29fc35f215 | ||
|
|
af0255440c | ||
|
|
389c48a1fd | ||
|
|
7d84011d81 | ||
|
|
36a307cd7f | ||
|
|
2a122803c2 | ||
|
|
dbf6dc0f9a | ||
|
|
fd663aff39 | ||
|
|
30e34568c1 | ||
|
|
5fb77314c4 | ||
|
|
d436c5de3d | ||
|
|
8157e3e5c1 | ||
|
|
3656c18e8f | ||
|
|
9f48b7da39 | ||
|
|
ef899f98bc | ||
|
|
de6c7904f0 | ||
|
|
0a79d5b60f | ||
|
|
36e0bc1edc | ||
|
|
8faeb3aac1 | ||
|
|
160ef4914f | ||
|
|
474c0774f1 | ||
|
|
0c9ba1ccec | ||
|
|
0c667ab615 | ||
|
|
fa231f7b9b | ||
|
|
21dd49d1d3 | ||
|
|
b12e4a6978 | ||
|
|
27a3cda6a9 | ||
|
|
de49e106c5 | ||
|
|
2d451e51a0 | ||
|
|
a9ab6c86ea | ||
|
|
444e4e79b3 | ||
|
|
fa4ccd3c0f | ||
|
|
cc7df2483a | ||
|
|
4b933c247a | ||
|
|
01fecadac3 | ||
|
|
108d24f55f | ||
|
|
4488a679c9 | ||
|
|
3c2839462a | ||
|
|
dcbf87488c | ||
|
|
da8e5e8396 | ||
|
|
63ab22f31d | ||
|
|
38589ef0c7 | ||
|
|
a54a5945ee | ||
|
|
6390a95314 | ||
|
|
bf60489403 | ||
|
|
07eddcd559 | ||
|
|
3165459b8c | ||
|
|
1edf91aa7c | ||
|
|
b2939bfe38 | ||
|
|
0a4b4c1fd6 | ||
|
|
263b485ba4 | ||
|
|
a9b035016f | ||
|
|
cbcaea6d2d | ||
|
|
cb7054f4c2 | ||
|
|
3170670c23 | ||
|
|
ad18e5b0d6 | ||
|
|
631096433c | ||
|
|
08345f2b07 | ||
|
|
80cb7092af | ||
|
|
5900197af6 | ||
|
|
85791fc7d6 | ||
|
|
8ce0f7a420 | ||
|
|
1c80f6e7a4 | ||
|
|
5891264476 | ||
|
|
f5e1b18543 | ||
|
|
7e1d94795c | ||
|
|
181bb62adb | ||
|
|
9a9fc6752b | ||
|
|
93272f4614 | ||
|
|
9e3a81df19 | ||
|
|
801c043807 | ||
|
|
644a4d5112 | ||
|
|
352a3af46e | ||
|
|
96174e5d00 | ||
|
|
aafebfc546 | ||
|
|
540b5e619e | ||
|
|
4a2115f093 | ||
|
|
9e41a8b114 | ||
|
|
b22095a460 | ||
|
|
d2dd5fdb4d | ||
|
|
223348365a | ||
|
|
fd5ca3ffea | ||
|
|
b1356ac6a0 | ||
|
|
5febfaa1a8 | ||
|
|
f6dfe02a3d | ||
|
|
d6ab991b27 | ||
|
|
b4defa717f | ||
|
|
748fd2f4aa | ||
|
|
e9bbdc4085 | ||
|
|
cd40860ca5 | ||
|
|
24191b3698 | ||
|
|
c69e9bf292 | ||
|
|
0d82a786e1 | ||
|
|
8e0a18414e | ||
|
|
f94468c2a9 | ||
|
|
08cb004b99 | ||
|
|
eacbfe7005 | ||
|
|
19bfaa620e | ||
|
|
fda20e07ac | ||
|
|
f6f81c290d | ||
|
|
50fcdb44c7 | ||
|
|
9ead8ddc34 | ||
|
|
9d3626ccdd | ||
|
|
f8b7087f16 | ||
|
|
862607ee10 | ||
|
|
3bd33cc0ec | ||
|
|
483cfc8cb8 | ||
|
|
16ceaf642d | ||
|
|
52d69f33fe | ||
|
|
3feb8798c9 | ||
|
|
191a528143 | ||
|
|
3a800663ee | ||
|
|
ec5bd66a74 | ||
|
|
95c6ed3dcd | ||
|
|
3ce4e3d50d | ||
|
|
64131c1784 | ||
|
|
ae879b296f | ||
|
|
73a96256eb | ||
|
|
501e362dae | ||
|
|
a17922cfdb | ||
|
|
107890a1e0 | ||
|
|
ff9d659976 | ||
|
|
2241cf43fa | ||
|
|
eb32016d86 | ||
|
|
1c75bd2d10 | ||
|
|
d82d09c12e | ||
|
|
c24cf01853 | ||
|
|
f3a89d2051 | ||
|
|
1ca2e1b956 | ||
|
|
5dc68367b1 | ||
|
|
a61f51b1d3 | ||
|
|
052dccaac7 | ||
|
|
d9f6d1a411 | ||
|
|
a82fcf89b6 | ||
|
|
72e4bf82b7 | ||
|
|
9a918db958 | ||
|
|
f6dd2a1a87 | ||
|
|
5e08f1f8a7 | ||
|
|
d2ab816f40 | ||
|
|
6a5dc7b8ae | ||
|
|
1db2da98aa | ||
|
|
97b61189cc | ||
|
|
c58f3cda6a | ||
|
|
4e33b0fa28 | ||
|
|
6e631adad3 | ||
|
|
a47c97d9e9 | ||
|
|
bb4af4a2a5 | ||
|
|
63c01d0493 | ||
|
|
c643113cfc | ||
|
|
a1895a755f | ||
|
|
14d4963050 | ||
|
|
a6c7446fc8 | ||
|
|
0dbfa38822 | ||
|
|
e69c0d57f9 | ||
|
|
f0f4e853e6 | ||
|
|
391de8329b | ||
|
|
fcfee4a942 | ||
|
|
5f1d39ab06 | ||
|
|
ddc9a01949 | ||
|
|
3c3126f776 | ||
|
|
a9908a8db9 | ||
|
|
ccad1fe4fd | ||
|
|
31eed48c36 | ||
|
|
d709ca8805 | ||
|
|
538c4132ff | ||
|
|
27583592a5 | ||
|
|
e4bf303e21 | ||
|
|
3674eb78b8 | ||
|
|
97d5a907b8 | ||
|
|
c312256a46 | ||
|
|
fa8055eba3 | ||
|
|
48d9acea15 | ||
|
|
3d4d77928f | ||
|
|
39730f6407 | ||
|
|
efa0e0d63b | ||
|
|
d7aefa5f5e | ||
|
|
2b5f72eb0b | ||
|
|
5f3dd47243 | ||
|
|
d08e57c287 | ||
|
|
5e7b76b334 | ||
|
|
05485a4765 | ||
|
|
5eee574f41 | ||
|
|
af366b90af | ||
|
|
c65a16e350 | ||
|
|
3ce1059f27 | ||
|
|
c7fb82ef1f | ||
|
|
91c2dc79a1 | ||
|
|
4617bc787c | ||
|
|
862f8f2313 | ||
|
|
48128f20d9 | ||
|
|
b5ffc8562b | ||
|
|
071bdac6aa | ||
|
|
dc06409618 | ||
|
|
20454ed2f6 | ||
|
|
87c8ae525b | ||
|
|
50919a123d | ||
|
|
4e958db404 | ||
|
|
cadbc559d0 | ||
|
|
62c32d00d1 | ||
|
|
7c4b91a3d2 | ||
|
|
472705614a | ||
|
|
f0554b26c1 | ||
|
|
b6a73f7088 | ||
|
|
6438236e49 | ||
|
|
6f9b4780d4 | ||
|
|
8cf76a463b | ||
|
|
ce4389ce3d | ||
|
|
e5299d72c0 | ||
|
|
b2a6411860 | ||
|
|
1217fbf4f1 | ||
|
|
7dfc8bc337 | ||
|
|
d02cb0ef9f | ||
|
|
8d125424d9 | ||
|
|
f43916f04f | ||
|
|
ab97adb80a | ||
|
|
3afedc8c1a | ||
|
|
093167a99c | ||
|
|
5aa3c1eb63 | ||
|
|
0e3a3c21f9 | ||
|
|
e0e4f51c09 | ||
|
|
af68d6cd9d | ||
|
|
5ce9cd13db | ||
|
|
6728ba3dc0 | ||
|
|
ffe3cc9b8e | ||
|
|
f293bf1361 | ||
|
|
b2e5afc7d5 | ||
|
|
2034073bc1 | ||
|
|
e38ff37e5f | ||
|
|
196c262a71 | ||
|
|
99146e5a91 | ||
|
|
732b8b706e | ||
|
|
992bbe0f6b | ||
|
|
4751c7fd69 | ||
|
|
5ec3b9a1dd | ||
|
|
0a9c8c83ff | ||
|
|
a0c9640018 | ||
|
|
d65118d56e | ||
|
|
4d7546d765 | ||
|
|
fcfa8aa988 | ||
|
|
903e30c91e | ||
|
|
dfd03c3099 | ||
|
|
a13b06df0f | ||
|
|
5473c5295d | ||
|
|
900beb8275 | ||
|
|
f93fa8703e | ||
|
|
d996598b69 | ||
|
|
369a285b9a | ||
|
|
48fbe8ae46 | ||
|
|
5007deb408 | ||
|
|
2fadedd165 | ||
|
|
e54ee5a8ff | ||
|
|
58ce8a1dfe | ||
|
|
c1b651d6c6 | ||
|
|
afa5c4aec9 | ||
|
|
bdc9255187 | ||
|
|
e287e05403 | ||
|
|
0013d418f0 | ||
|
|
7ed15332da | ||
|
|
867484c616 | ||
|
|
d9eb4fbd0a | ||
|
|
f03e74978c | ||
|
|
aae0f27a75 | ||
|
|
1fb8342b0c | ||
|
|
3d797cf08a | ||
|
|
bfa26bd2f9 | ||
|
|
8bf7380056 | ||
|
|
6eb97d6178 | ||
|
|
f31a8095a0 | ||
|
|
29da028fe6 | ||
|
|
0e94140c18 | ||
|
|
aa8a2fbdc8 | ||
|
|
f10f8042af | ||
|
|
505669445f | ||
|
|
07f34695ff | ||
|
|
cc6d0dfd06 | ||
|
|
f36c40cddb | ||
|
|
3f71142ab2 | ||
|
|
17fa838a98 | ||
|
|
d584ee788f | ||
|
|
3a84b16e9a | ||
|
|
44a4cfec0c | ||
|
|
879831d601 | ||
|
|
04ed562334 | ||
|
|
1b4e787f3b | ||
|
|
60be450620 | ||
|
|
d992cc6a70 | ||
|
|
8ed0896e4e | ||
|
|
4870e6c5f0 | ||
|
|
c91dfdd539 | ||
|
|
162233b8be | ||
|
|
c2e1847d0c | ||
|
|
0581afc2ab | ||
|
|
f32cf16a4d | ||
|
|
dd5d6e7b3e | ||
|
|
ead9df84df | ||
|
|
f49b5af185 | ||
|
|
b1c953050d | ||
|
|
6085f31d85 | ||
|
|
17519ed827 | ||
|
|
861cc723e9 | ||
|
|
feafdf2162 | ||
|
|
7926aafae7 | ||
|
|
e9d0f856d2 | ||
|
|
8f9e6cf58b | ||
|
|
76ba6bc528 | ||
|
|
e9ba530272 | ||
|
|
50b70a5269 | ||
|
|
50e812f946 | ||
|
|
78f6c98edd | ||
|
|
92552fcbdb | ||
|
|
759d56b7f1 | ||
|
|
135ccac2bf | ||
|
|
88e8a28f9e | ||
|
|
a953860d64 | ||
|
|
03ac5b61cb | ||
|
|
a46aa958a1 | ||
|
|
faddfd15a0 | ||
|
|
0971d8918e | ||
|
|
53f2c6681d | ||
|
|
2df9e3dda4 | ||
|
|
70b07d1ceb | ||
|
|
2d1dba654b | ||
|
|
a4cf654d6b | ||
|
|
419c01babe | ||
|
|
7bf5998f6c | ||
|
|
5d0a677265 | ||
|
|
dc8f97c71b | ||
|
|
c8850147a2 | ||
|
|
a22f976d30 | ||
|
|
725b5d3dd8 | ||
|
|
e8bb6811b7 | ||
|
|
f7722584c5 | ||
|
|
b6022bc133 | ||
|
|
aee6f26af6 | ||
|
|
9249c91321 | ||
|
|
2fc5cecb76 | ||
|
|
d212572df8 | ||
|
|
1b7f4de4c9 | ||
|
|
5db06f1390 | ||
|
|
51d1dc9a3e | ||
|
|
d3a35875df | ||
|
|
128a212027 | ||
|
|
bda1514b8f | ||
|
|
8b8ae207ee | ||
|
|
25a24801d6 | ||
|
|
369f183a2d | ||
|
|
45d0d18223 | ||
|
|
d48c1e6517 | ||
|
|
4c95258fdf | ||
|
|
54f575d838 | ||
|
|
2d0369a5f4 | ||
|
|
050702716a | ||
|
|
79ce9e3ca4 | ||
|
|
0b47d5d7c8 | ||
|
|
6942d92590 | ||
|
|
59153d47fe | ||
|
|
84a16804fa | ||
|
|
359a9b8899 | ||
|
|
50212a1a00 | ||
|
|
7d0c7c441e | ||
|
|
1da4a8e69b | ||
|
|
a0f8ed343d | ||
|
|
fc5940ec1d | ||
|
|
4b8d7a0680 | ||
|
|
9844882ef1 | ||
|
|
d514e55e11 | ||
|
|
60bab91aab | ||
|
|
32d49e7167 | ||
|
|
02a8050528 | ||
|
|
8ed8ddbde3 | ||
|
|
e1a3c50adc | ||
|
|
01642e644c | ||
|
|
73626e185e | ||
|
|
e596cf80a0 | ||
|
|
5d17b3cf33 | ||
|
|
f5b3d5f20c | ||
|
|
b4440300af | ||
|
|
070b9b2cc3 | ||
|
|
912f4808e2 | ||
|
|
723e83887d | ||
|
|
5ef7bbc573 | ||
|
|
c539b1da17 | ||
|
|
5f4ee497a5 | ||
|
|
07e833d8df | ||
|
|
cd13187cf4 | ||
|
|
190310a24d | ||
|
|
6d19682e78 | ||
|
|
af77ccbad0 | ||
|
|
67c654ab4e | ||
|
|
2a91691476 | ||
|
|
ce7397a7c4 | ||
|
|
3cb22b0c8f | ||
|
|
758fbcb207 | ||
|
|
20ea9e59f1 | ||
|
|
902c826844 | ||
|
|
675ad8d536 | ||
|
|
80107acf2c | ||
|
|
7fded02c76 | ||
|
|
5915d5c956 | ||
|
|
7bab2a064e | ||
|
|
a86e1feb98 | ||
|
|
c059a13cf3 | ||
|
|
5748e35905 | ||
|
|
589d5e951f | ||
|
|
bc4fcb088f | ||
|
|
1c9952f414 | ||
|
|
0416997e15 | ||
|
|
d6e7c48b65 | ||
|
|
be3bda90e2 | ||
|
|
f1f6c9aacd | ||
|
|
940d41322e | ||
|
|
47c96fd189 | ||
|
|
6d6a76e4f5 | ||
|
|
31f8176aad | ||
|
|
3e863bbe02 | ||
|
|
b031041048 | ||
|
|
c108df7e05 | ||
|
|
a086a2d241 | ||
|
|
9c068290bb | ||
|
|
17f16208fd | ||
|
|
e6b990cd42 | ||
|
|
a9928fb51e | ||
|
|
3b524ccd4b | ||
|
|
40d2f81cac | ||
|
|
e5ce29eb62 | ||
|
|
6ef67c8961 | ||
|
|
314af7200d | ||
|
|
424259aa4d | ||
|
|
dc0962c2ed | ||
|
|
ec32ab4e6f | ||
|
|
0e142ce33b | ||
|
|
d0191dac89 | ||
|
|
272a12cadb | ||
|
|
3460aca9d9 | ||
|
|
c58e44a940 | ||
|
|
94ca41bae0 | ||
|
|
58f6a9f295 | ||
|
|
3ae6ff2709 | ||
|
|
d6a4586dfb | ||
|
|
6d9ee9ff74 | ||
|
|
837123b11a | ||
|
|
c7b55fbb19 | ||
|
|
408526c9e6 | ||
|
|
ebf0c72c8b | ||
|
|
664a86dd2c | ||
|
|
aa6c81df26 | ||
|
|
cae496d60c | ||
|
|
e0abc4fe0a | ||
|
|
d7b5689057 | ||
|
|
857a24ea72 | ||
|
|
01d8b3ce33 | ||
|
|
12b07baabf | ||
|
|
e6eb0adc34 | ||
|
|
2cdfc55bd6 | ||
|
|
1675bb648d | ||
|
|
f277ccf1f7 | ||
|
|
e36268712b | ||
|
|
673f50440d | ||
|
|
d1233b9423 | ||
|
|
fa57cbf1d5 | ||
|
|
6c314b8b30 | ||
|
|
529d17d62d | ||
|
|
00059d544f | ||
|
|
d09af32f6b | ||
|
|
d8f2e9f070 | ||
|
|
9fd84b97db | ||
|
|
9d8e23d7a9 | ||
|
|
c4d096cd2a | ||
|
|
63bce6f0c2 | ||
|
|
1cb0757a8e | ||
|
|
796830ed7d | ||
|
|
30cc171309 | ||
|
|
eb0e429cf4 | ||
|
|
9f90bf7c33 | ||
|
|
bb5ea9be8b | ||
|
|
138ae58555 | ||
|
|
e1d98dfbc0 | ||
|
|
07364b950b | ||
|
|
c4ca734842 | ||
|
|
84e262fe2c | ||
|
|
05b2453df1 | ||
|
|
50e5f24464 | ||
|
|
0b3b843b23 | ||
|
|
20177183ae | ||
|
|
e0b1e60def | ||
|
|
1d6610eaa7 | ||
|
|
dbde98ac50 | ||
|
|
a13babee94 | ||
|
|
971ad74f62 | ||
|
|
cec0a64dbc | ||
|
|
87e6c78564 | ||
|
|
4656f8cf53 | ||
|
|
ede43bdfb2 | ||
|
|
d56acedd08 | ||
|
|
a2e8386f39 | ||
|
|
f91f6b92c9 | ||
|
|
d22ab694e2 | ||
|
|
f89343e8b0 | ||
|
|
6dac3ed0e1 | ||
|
|
be78e7685c | ||
|
|
517cd5e0f3 | ||
|
|
1a1c932b05 | ||
|
|
090f53f917 | ||
|
|
320778732e | ||
|
|
68acbb52e5 | ||
|
|
4ba9e18cfa | ||
|
|
82ef53785b | ||
|
|
09a43d9ec3 | ||
|
|
cfeb5f37cd | ||
|
|
3381689b13 | ||
|
|
65725657f2 | ||
|
|
8309968440 | ||
|
|
9edca009ac | ||
|
|
580b62c8c0 | ||
|
|
327e76094e | ||
|
|
74e5189509 | ||
|
|
5870a3d74a | ||
|
|
be826c3a47 | ||
|
|
6fa02af542 | ||
|
|
9b1939811f | ||
|
|
f8f49dab35 | ||
|
|
f906998fdc | ||
|
|
2784a1bcbb | ||
|
|
b903bc0897 | ||
|
|
47282c09b0 | ||
|
|
dd5002beac | ||
|
|
07692d83b2 | ||
|
|
9b889556d3 | ||
|
|
991a139d70 | ||
|
|
e1cdbb4f81 | ||
|
|
f2e802a300 | ||
|
|
3cbf34c7d8 | ||
|
|
1591f89fe3 | ||
|
|
7bffb6b97a | ||
|
|
60e694959c | ||
|
|
a2ec0098ee | ||
|
|
07a3ef431a | ||
|
|
eba5ef0e03 | ||
|
|
6556c4c02e | ||
|
|
9ba83d141f | ||
|
|
a40a2b5cc8 | ||
|
|
a71c2c8cba | ||
|
|
2615fa017f | ||
|
|
53a045f141 | ||
|
|
ac24d88e82 | ||
|
|
35f274ff58 | ||
|
|
94c560f19a | ||
|
|
d85492aacd | ||
|
|
5674a93390 | ||
|
|
06132531b0 | ||
|
|
2cea38cea9 | ||
|
|
84569ef55d | ||
|
|
79f748b235 | ||
|
|
8dde71c91b | ||
|
|
3ad5375db4 | ||
|
|
e76c56b55d | ||
|
|
27de820faf | ||
|
|
d892addefd | ||
|
|
a31c84685e | ||
|
|
bb13a7454a | ||
|
|
07122dd48c | ||
|
|
8b7564ec82 | ||
|
|
f9aeb20458 | ||
|
|
bf6085e235 | ||
|
|
79d200c27b | ||
|
|
6cfdf7fe80 | ||
|
|
180cfa3e5c | ||
|
|
efc6be8d3d | ||
|
|
1e058c997a | ||
|
|
0a11f9cdd5 | ||
|
|
e3b0ca36aa | ||
|
|
4fedf0f13d | ||
|
|
d8755c395c | ||
|
|
9f2901ecd5 | ||
|
|
8b87dafb26 | ||
|
|
f23bb7c2be | ||
|
|
c2c11d40e9 | ||
|
|
004d4e06de | ||
|
|
fb25e79233 | ||
|
|
bd704c4ac9 | ||
|
|
a433920179 | ||
|
|
e7a43c295b | ||
|
|
0950a80509 | ||
|
|
0e395353ba | ||
|
|
af1d83bd05 | ||
|
|
80ee21ab1c | ||
|
|
fb34fdf0f0 | ||
|
|
7611fcc188 | ||
|
|
3da43bfa50 | ||
|
|
0476a66796 | ||
|
|
68b578057d | ||
|
|
c33186509d | ||
|
|
345e20afd9 | ||
|
|
0a0a3fa0f6 | ||
|
|
5ff02c7e5d | ||
|
|
6b317e8360 | ||
|
|
25cfd427c9 | ||
|
|
d5c5bcc41c | ||
|
|
6c14b9d0b2 | ||
|
|
43e058e01e | ||
|
|
88b1d39bc1 | ||
|
|
82ab40517b | ||
|
|
1532db7a5d | ||
|
|
d2f0967a27 | ||
|
|
c28700db9d | ||
|
|
49c573d381 | ||
|
|
60eac66d90 | ||
|
|
abf46d42f5 | ||
|
|
d206e82837 | ||
|
|
9af6dda60f | ||
|
|
c482d488d4 | ||
|
|
b03cd05bc5 | ||
|
|
4214e364d9 | ||
|
|
667325fec6 | ||
|
|
dfe37a38ee | ||
|
|
1fdaa87d97 | ||
|
|
c581047733 | ||
|
|
7ca6ae92d0 | ||
|
|
b5a3238d02 | ||
|
|
f65290744f | ||
|
|
b8f74fa116 | ||
|
|
05cea3bbc4 | ||
|
|
9ec9de0ba1 | ||
|
|
89c075fc94 | ||
|
|
62aace820f | ||
|
|
3849bb421a | ||
|
|
b46f1acc99 | ||
|
|
2d4273773a | ||
|
|
dae3137ada | ||
|
|
ddcf561aef | ||
|
|
7ff4734be5 | ||
|
|
cb8692fb9c | ||
|
|
4e203323e5 | ||
|
|
c644cb2449 | ||
|
|
16ec3c0e26 | ||
|
|
c92b806c03 | ||
|
|
3392078bc8 | ||
|
|
66b5b93b98 | ||
|
|
d466420858 | ||
|
|
f5125e937a | ||
|
|
1fb3e0399c | ||
|
|
17b76e1536 | ||
|
|
476e991deb | ||
|
|
a3bbe84558 | ||
|
|
701ff6afb5 | ||
|
|
9f2a772fa3 | ||
|
|
2e879197b8 | ||
|
|
924ae07535 | ||
|
|
8060a9d9d0 | ||
|
|
eac2e14afd | ||
|
|
f4f02528ec | ||
|
|
766db51dd7 | ||
|
|
81b4fcdf78 | ||
|
|
5497d070a3 | ||
|
|
11fba9274e | ||
|
|
6ee6130f9b | ||
|
|
440d1e48f8 | ||
|
|
a72de76df1 | ||
|
|
a6d3e42672 | ||
|
|
df86b9445c | ||
|
|
e5136fdc55 | ||
|
|
ccd92845c0 | ||
|
|
8db1db029d | ||
|
|
3532bb0c49 | ||
|
|
cdd9dd6842 | ||
|
|
63946b06c9 | ||
|
|
7ed6a07fd1 | ||
|
|
d1a36f6f22 | ||
|
|
d2d026eec0 | ||
|
|
bb9d503b19 | ||
|
|
beb5987948 | ||
|
|
2a77b47fa8 | ||
|
|
f3ddf14de9 | ||
|
|
5b80360a59 | ||
|
|
75981f36e7 | ||
|
|
93f6c7cebb | ||
|
|
fac11d92d1 | ||
|
|
b7b4de967e | ||
|
|
ee786a67b5 | ||
|
|
b1d66e930d | ||
|
|
a57386677a | ||
|
|
06b0209b15 | ||
|
|
50985bfad8 | ||
|
|
7256152db8 | ||
|
|
3578ac264c | ||
|
|
0a304fd350 | ||
|
|
9a5dbc052f | ||
|
|
1edd5c3127 | ||
|
|
492c6060a8 | ||
|
|
706a05eea0 | ||
|
|
8ee9b43e8d | ||
|
|
1a04779a05 | ||
|
|
43860a4d37 | ||
|
|
d721482127 | ||
|
|
b4af6eb98f | ||
|
|
6867c319bb | ||
|
|
1fbc19740b | ||
|
|
11b5affdd9 | ||
|
|
41256f77e2 | ||
|
|
ea916c23e4 | ||
|
|
2e8b4db01b | ||
|
|
0116530794 | ||
|
|
fe168d46af | ||
|
|
a2956773c9 | ||
|
|
dc48347a99 | ||
|
|
0bf24c25f9 | ||
|
|
8406bad80c | ||
|
|
27a5f3923d | ||
|
|
10ac85c08e | ||
|
|
da99fbd1d8 | ||
|
|
7cf5d8d5ae | ||
|
|
b8b7b2a9fb | ||
|
|
0e14ce9013 | ||
|
|
90799f6a6a | ||
|
|
3e40859f6d | ||
|
|
3ac0baf653 | ||
|
|
823af2c94a | ||
|
|
7198df297f | ||
|
|
fdc3586008 | ||
|
|
fb44030d83 | ||
|
|
a16959d167 | ||
|
|
27b1d671a3 | ||
|
|
9c97a63a43 | ||
|
|
deb29c22b4 | ||
|
|
76acd17d81 | ||
|
|
e646192196 | ||
|
|
1f0345f16f | ||
|
|
e354d6bee1 | ||
|
|
b91fd1a0f5 | ||
|
|
332b6af3da | ||
|
|
a0f44ed5a0 | ||
|
|
9b20fa189c | ||
|
|
8e960d955f | ||
|
|
e72cdf3cbd | ||
|
|
bd204fdf87 | ||
|
|
b9e19b2515 | ||
|
|
2bbbb93dbf | ||
|
|
f248b2b254 | ||
|
|
3465e24bbe | ||
|
|
8b43402192 | ||
|
|
c70f814064 | ||
|
|
1e3abd9ae5 | ||
|
|
fdf0fceb28 | ||
|
|
2282c26400 | ||
|
|
0637be405d | ||
|
|
75b6b12147 | ||
|
|
aea6e98974 | ||
|
|
450348311e | ||
|
|
e8af0397a7 | ||
|
|
a97f287467 |
@@ -2,16 +2,17 @@ root = true
|
||||
|
||||
[*]
|
||||
insert_final_newline = true
|
||||
max_line_length = 140
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[{*.kts, *.kt}]
|
||||
[{*.kts,*.kt}]
|
||||
charset = utf-8
|
||||
indent_style = tab
|
||||
tab_width = 4
|
||||
# Disable wildcard imports in IntelliJ/Android Studio
|
||||
ij_kotlin_name_count_to_use_star_import = 1000
|
||||
ij_kotlin_name_count_to_use_star_import_for_members = 1000
|
||||
ij_kotlin_packages_to_use_import_on_demand =
|
||||
ij_kotlin_packages_to_use_import_on_demand = unset
|
||||
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
||||
trim_trailing_whitespace = false
|
||||
|
||||
8
.github/workflows/app-build.yaml
vendored
8
.github/workflows/app-build.yaml
vendored
@@ -15,20 +15,20 @@ jobs:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
||||
- name: Setup Java
|
||||
uses: actions/setup-java@387ac29b308b003ca37ba93a6cab5eb57c8f5f93 # v4.0.0
|
||||
uses: actions/setup-java@99b8673ff64fbf99d8d325f52d9a5bdedb8483e9 # v4.2.1
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: 17
|
||||
- name: Setup Gradle
|
||||
uses: gradle/gradle-build-action@8cbcb9948b5892387aed077daf6f90e1f0ba5b27 # v2.11.0
|
||||
uses: gradle/actions/setup-gradle@d9c87d481d55275bb5441eef3fe0e46805f9ef70 # v3.5.0
|
||||
- name: Assemble debug APKs
|
||||
run: ./gradlew assembleDebug
|
||||
- name: Create publish bundle
|
||||
run: mkdir -p build/gh-app-publish/; find app/build/ -iname "*.apk" -exec mv "{}" build/gh-app-publish/ \;
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@c7d193f32edcb7bfad88892161225aeda64e9392 # v4.0.0
|
||||
uses: actions/upload-artifact@0b2256b8c012f0828dc542b3febcab082c67f72b # v4.3.4
|
||||
with:
|
||||
name: build-artifacts
|
||||
retention-days: 14
|
||||
|
||||
8
.github/workflows/app-lint.yaml
vendored
8
.github/workflows/app-lint.yaml
vendored
@@ -17,18 +17,18 @@ jobs:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
||||
- name: Setup Java
|
||||
uses: actions/setup-java@387ac29b308b003ca37ba93a6cab5eb57c8f5f93 # v4.0.0
|
||||
uses: actions/setup-java@99b8673ff64fbf99d8d325f52d9a5bdedb8483e9 # v4.2.1
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: 17
|
||||
- name: Setup Gradle
|
||||
uses: gradle/gradle-build-action@8cbcb9948b5892387aed077daf6f90e1f0ba5b27 # v2.11.0
|
||||
uses: gradle/actions/setup-gradle@d9c87d481d55275bb5441eef3fe0e46805f9ef70 # v3.5.0
|
||||
- name: Run detekt and lint tasks
|
||||
run: ./gradlew detekt lint
|
||||
- name: Upload SARIF files
|
||||
uses: github/codeql-action/upload-sarif@b374143c1149a9115d881581d29b8390bbcbb59c # v3.22.11
|
||||
uses: github/codeql-action/upload-sarif@5cf07d8b700b67e235fbb65cbc84f69c0cf10464 # v3.25.14
|
||||
if: ${{ always() }}
|
||||
with:
|
||||
sarif_file: .
|
||||
|
||||
40
.github/workflows/app-publish.yaml
vendored
40
.github/workflows/app-publish.yaml
vendored
@@ -12,14 +12,14 @@ jobs:
|
||||
if: ${{ contains(github.repository_owner, 'jellyfin') }}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
||||
- name: Setup Java
|
||||
uses: actions/setup-java@387ac29b308b003ca37ba93a6cab5eb57c8f5f93 # v4.0.0
|
||||
uses: actions/setup-java@99b8673ff64fbf99d8d325f52d9a5bdedb8483e9 # v4.2.1
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: 17
|
||||
- name: Setup Gradle
|
||||
uses: gradle/gradle-build-action@8cbcb9948b5892387aed077daf6f90e1f0ba5b27 # v2.11.0
|
||||
uses: gradle/actions/setup-gradle@d9c87d481d55275bb5441eef3fe0e46805f9ef70 # v3.5.0
|
||||
- name: Set JELLYFIN_VERSION
|
||||
run: echo "JELLYFIN_VERSION=$(echo ${GITHUB_REF#refs/tags/v} | tr / -)" >> $GITHUB_ENV
|
||||
- name: Assemble release files
|
||||
@@ -27,6 +27,8 @@ jobs:
|
||||
- name: Sign APK
|
||||
id: signApk
|
||||
uses: r0adkll/sign-android-release@349ebdef58775b1e0d8099458af0816dc79b6407 # tag=v1
|
||||
env:
|
||||
BUILD_TOOLS_VERSION: "34.0.0"
|
||||
with:
|
||||
releaseDirectory: app/build/outputs/apk/release
|
||||
signingKeyBase64: ${{ secrets.KEYSTORE }}
|
||||
@@ -36,6 +38,8 @@ jobs:
|
||||
- name: Sign app bundle
|
||||
id: signAab
|
||||
uses: r0adkll/sign-android-release@349ebdef58775b1e0d8099458af0816dc79b6407 # tag=v1
|
||||
env:
|
||||
BUILD_TOOLS_VERSION: "34.0.0"
|
||||
with:
|
||||
releaseDirectory: app/build/outputs/bundle/release
|
||||
signingKeyBase64: ${{ secrets.KEYSTORE }}
|
||||
@@ -51,7 +55,7 @@ jobs:
|
||||
mv ${{ steps.signAab.outputs.signedReleaseFile }} build/jellyfin-publish/jellyfin-androidtv-v${{ env.JELLYFIN_VERSION }}-release.aab
|
||||
mv app/build/version.txt build/jellyfin-publish/
|
||||
- name: Upload release archive to GitHub release
|
||||
uses: alexellis/upload-assets@259de5111cb56966d046ced998941e93f91d2c93 # tag=0.4.0
|
||||
uses: alexellis/upload-assets@13926a61cdb2cb35f5fdef1c06b8b591523236d3 # 0.4.1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.JF_BOT_TOKEN }}
|
||||
with:
|
||||
@@ -61,20 +65,24 @@ jobs:
|
||||
with:
|
||||
switches: -vrptz
|
||||
path: build/jellyfin-publish/
|
||||
remote_path: /srv/repository/releases/client/androidtv/versions/v${{ env.JELLYFIN_VERSION }}
|
||||
remote_host: ${{ secrets.DEPLOY_HOST }}
|
||||
remote_user: ${{ secrets.DEPLOY_USER }}
|
||||
remote_key: ${{ secrets.DEPLOY_KEY }}
|
||||
remote_path: /srv/incoming/androidtv/v${{ env.JELLYFIN_VERSION }}/
|
||||
remote_host: ${{ secrets.REPO_HOST }}
|
||||
remote_user: ${{ secrets.REPO_USER }}
|
||||
remote_key: ${{ secrets.REPO_KEY }}
|
||||
- name: Update repo.jellyfin.org symlinks
|
||||
uses: appleboy/ssh-action@55dabf81b49d4120609345970c91507e2d734799 # v1.0.0
|
||||
uses: appleboy/ssh-action@029f5b4aeeeb58fdfe1410a5d17f967dacf36262 # v1.0.3
|
||||
with:
|
||||
host: ${{ secrets.DEPLOY_HOST }}
|
||||
username: ${{ secrets.DEPLOY_USER }}
|
||||
key: ${{ secrets.DEPLOY_KEY }}
|
||||
host: ${{ secrets.REPO_HOST }}
|
||||
username: ${{ secrets.REPO_USER }}
|
||||
key: ${{ secrets.REPO_KEY }}
|
||||
envs: JELLYFIN_VERSION
|
||||
script_stop: true
|
||||
script: |
|
||||
cd /srv/repository/releases/client/androidtv;
|
||||
rm -rf *.apk version.txt;
|
||||
ln -s versions/v${JELLYFIN_VERSION}/jellyfin-androidtv-v${JELLYFIN_VERSION}-*.apk .;
|
||||
ln -s versions/v${JELLYFIN_VERSION}/version.txt .;
|
||||
if [ -d "/srv/repository/main/client/androidtv/versions/v${{ env.JELLYFIN_VERSION }}" ] && [ -n "${{ env.JELLYFIN_VERSION }}" ]; then
|
||||
sudo rm -r /srv/repository/main/client/androidtv/versions/v${{ env.JELLYFIN_VERSION }}
|
||||
fi
|
||||
sudo mv /srv/incoming/androidtv/v${{ env.JELLYFIN_VERSION }} /srv/repository/main/client/androidtv/versions/v${{ env.JELLYFIN_VERSION }}
|
||||
cd /srv/repository/main/client/androidtv;
|
||||
sudo rm -rf *.apk version.txt;
|
||||
sudo ln -s versions/v${JELLYFIN_VERSION}/jellyfin-androidtv-v${JELLYFIN_VERSION}-*.apk .;
|
||||
sudo ln -s versions/v${JELLYFIN_VERSION}/version.txt .;
|
||||
|
||||
6
.github/workflows/app-test.yaml
vendored
6
.github/workflows/app-test.yaml
vendored
@@ -16,13 +16,13 @@ jobs:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
||||
- name: Setup Java
|
||||
uses: actions/setup-java@387ac29b308b003ca37ba93a6cab5eb57c8f5f93 # v4.0.0
|
||||
uses: actions/setup-java@99b8673ff64fbf99d8d325f52d9a5bdedb8483e9 # v4.2.1
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: 17
|
||||
- name: Setup Gradle
|
||||
uses: gradle/gradle-build-action@8cbcb9948b5892387aed077daf6f90e1f0ba5b27 # v2.11.0
|
||||
uses: gradle/actions/setup-gradle@d9c87d481d55275bb5441eef3fe0e46805f9ef70 # v3.5.0
|
||||
- name: Run test task
|
||||
run: ./gradlew test
|
||||
|
||||
4
.github/workflows/gradlew-validate.yaml
vendored
4
.github/workflows/gradlew-validate.yaml
vendored
@@ -17,6 +17,6 @@ jobs:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
||||
- name: Validate Gradle Wrapper
|
||||
uses: gradle/wrapper-validation-action@56b90f209b02bf6d1deae490e9ef18b21a389cd4 # v1.1.0
|
||||
uses: gradle/actions/wrapper-validation@d9c87d481d55275bb5441eef3fe0e46805f9ef70 # v3.5.0
|
||||
|
||||
2
.github/workflows/repo-merge-conflict.yaml
vendored
2
.github/workflows/repo-merge-conflict.yaml
vendored
@@ -12,7 +12,7 @@ jobs:
|
||||
runs-on: ubuntu-22.04
|
||||
if: ${{ contains(github.repository_owner, 'jellyfin') }}
|
||||
steps:
|
||||
- uses: eps1lon/actions-label-merge-conflict@fd1f295ee7443d13745804bc49fe158e240f6c6e # tag=v2.1.0
|
||||
- uses: eps1lon/actions-label-merge-conflict@1b1b1fcde06a9b3d089f3464c96417961dde1168 # v3.0.2
|
||||
with:
|
||||
dirtyLabel: merge conflict
|
||||
repoToken: ${{ secrets.JF_BOT_TOKEN }}
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -4,5 +4,6 @@ local.properties
|
||||
|
||||
/.idea
|
||||
.gradle
|
||||
.kotlin/
|
||||
build/
|
||||
captures/
|
||||
|
||||
@@ -12,6 +12,8 @@
|
||||
- [GodTamIt](https://github.com/GodTamIt)
|
||||
- [sparky3387](https://github.com/sparky3387)
|
||||
- [mohd-akram](https://github.com/mohd-akram)
|
||||
- [3l0w](https://github.com/3l0w)
|
||||
- [MajMongoose](https://github.com/majmongoose)
|
||||
|
||||
# Emby Contributors
|
||||
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
buildscript {
|
||||
repositories {
|
||||
mavenCentral()
|
||||
google()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
classpath libs.android.gradle
|
||||
}
|
||||
}
|
||||
|
||||
import com.android.tools.profgen.ArtProfileKt
|
||||
import com.android.tools.profgen.ArtProfileSerializer
|
||||
import com.android.tools.profgen.DexFile
|
||||
|
||||
/**
|
||||
* This is a temporary workaround for https://issuetracker.google.com/issues/231837768
|
||||
* which will be fixed in AGP 8.1.0, at which point the workaround may be deleted.
|
||||
*/
|
||||
project.afterEvaluate {
|
||||
tasks.compileReleaseArtProfile.doLast {
|
||||
outputs.files.each { file ->
|
||||
if (file.toString().endsWith(".profm")) {
|
||||
println("Sorting ${file}")
|
||||
def version = ArtProfileSerializer.valueOf("METADATA_0_0_2")
|
||||
def profile = ArtProfileKt.ArtProfile(file)
|
||||
def keys = new ArrayList(profile.profileData.keySet())
|
||||
def sortedData = new LinkedHashMap()
|
||||
Collections.sort keys, new DexFile.Companion()
|
||||
keys.each { key -> sortedData[key] = profile.profileData[key] }
|
||||
new FileOutputStream(file).with {
|
||||
write(version.magicBytes$profgen)
|
||||
write(version.versionBytes$profgen)
|
||||
version.write$profgen(it, sortedData, "")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,19 +2,17 @@ plugins {
|
||||
id("com.android.application")
|
||||
kotlin("android")
|
||||
alias(libs.plugins.kotlin.serialization)
|
||||
alias(libs.plugins.kotlin.compose)
|
||||
alias(libs.plugins.aboutlibraries)
|
||||
}
|
||||
|
||||
// Apply workaround
|
||||
apply("baselineWorkaround.gradle")
|
||||
|
||||
android {
|
||||
namespace = "org.jellyfin.androidtv"
|
||||
compileSdk = 34
|
||||
compileSdk = libs.versions.android.compileSdk.get().toInt()
|
||||
|
||||
defaultConfig {
|
||||
minSdk = 21
|
||||
targetSdk = 33
|
||||
minSdk = libs.versions.android.minSdk.get().toInt()
|
||||
targetSdk = libs.versions.android.targetSdk.get().toInt()
|
||||
|
||||
// Release version
|
||||
applicationId = namespace
|
||||
@@ -33,10 +31,6 @@ android {
|
||||
isCoreLibraryDesugaringEnabled = true
|
||||
}
|
||||
|
||||
composeOptions {
|
||||
kotlinCompilerExtensionVersion = libs.versions.androidx.compose.compiler.get()
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
val release by getting {
|
||||
isMinifyEnabled = false
|
||||
@@ -86,7 +80,7 @@ aboutLibraries {
|
||||
}
|
||||
|
||||
val versionTxt by tasks.registering {
|
||||
val path = buildDir.resolve("version.txt")
|
||||
val path = layout.buildDirectory.asFile.get().resolve("version.txt")
|
||||
|
||||
doLast {
|
||||
val versionString = "v${android.defaultConfig.versionName}=${android.defaultConfig.versionCode}"
|
||||
@@ -98,9 +92,9 @@ val versionTxt by tasks.registering {
|
||||
dependencies {
|
||||
// Jellyfin
|
||||
implementation(projects.playback.core)
|
||||
implementation(projects.playback.exoplayer)
|
||||
implementation(projects.playback.jellyfin)
|
||||
implementation(projects.playback.ui)
|
||||
implementation(projects.playback.media3.exoplayer)
|
||||
implementation(projects.playback.media3.session)
|
||||
implementation(projects.preference)
|
||||
implementation(libs.jellyfin.apiclient)
|
||||
implementation(libs.jellyfin.sdk) {
|
||||
@@ -121,6 +115,7 @@ dependencies {
|
||||
implementation(libs.androidx.core)
|
||||
implementation(libs.androidx.activity)
|
||||
implementation(libs.androidx.fragment)
|
||||
implementation(libs.androidx.fragment.compose)
|
||||
implementation(libs.androidx.leanback.core)
|
||||
implementation(libs.androidx.leanback.preference)
|
||||
implementation(libs.androidx.preference)
|
||||
@@ -139,15 +134,15 @@ dependencies {
|
||||
implementation(libs.bundles.koin)
|
||||
|
||||
// Media players
|
||||
implementation(libs.exoplayer)
|
||||
implementation(libs.jellyfin.exoplayer.ffmpegextension)
|
||||
implementation(libs.libvlc)
|
||||
implementation(libs.androidx.media3.exoplayer)
|
||||
implementation(libs.androidx.media3.exoplayer.hls)
|
||||
implementation(libs.androidx.media3.ui)
|
||||
implementation(libs.jellyfin.androidx.media3.ffmpeg.decoder)
|
||||
|
||||
// Markdown
|
||||
implementation(libs.bundles.markwon)
|
||||
|
||||
// Image utility
|
||||
implementation(libs.blurhash)
|
||||
implementation(libs.bundles.coil)
|
||||
|
||||
// Crash Reporting
|
||||
@@ -160,10 +155,6 @@ dependencies {
|
||||
implementation(libs.timber)
|
||||
implementation(libs.slf4j.timber)
|
||||
|
||||
// Debugging
|
||||
if (getProperty("leakcanary.enable")?.toBoolean() == true)
|
||||
debugImplementation(libs.leakcanary)
|
||||
|
||||
// Compatibility (desugaring)
|
||||
coreLibraryDesugaring(libs.android.desugar)
|
||||
|
||||
|
||||
@@ -28,17 +28,25 @@
|
||||
android:name="android.hardware.microphone"
|
||||
android:required="false" />
|
||||
|
||||
<queries>
|
||||
<intent>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<data
|
||||
android:host="youtube.com"
|
||||
android:scheme="https" />
|
||||
</intent>
|
||||
</queries>
|
||||
|
||||
<application
|
||||
android:name=".JellyfinApplication"
|
||||
android:allowBackup="true"
|
||||
android:appCategory="video"
|
||||
android:banner="@drawable/app_banner"
|
||||
android:banner="@mipmap/app_banner"
|
||||
android:dataExtractionRules="@xml/backup_rules"
|
||||
android:fullBackupContent="@xml/backup_content"
|
||||
android:icon="@drawable/app_icon"
|
||||
android:icon="@mipmap/app_icon"
|
||||
android:label="@string/app_name"
|
||||
android:largeHeap="true"
|
||||
android:roundIcon="@drawable/app_icon_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.Jellyfin"
|
||||
android:usesCleartextTraffic="true">
|
||||
@@ -100,6 +108,7 @@
|
||||
<activity
|
||||
android:name=".ui.startup.StartupActivity"
|
||||
android:exported="true"
|
||||
android:launchMode="singleTask"
|
||||
android:noHistory="true"
|
||||
android:screenOrientation="landscape"
|
||||
android:windowSoftInputMode="adjustNothing">
|
||||
@@ -123,6 +132,7 @@
|
||||
<!-- Main application -->
|
||||
<activity
|
||||
android:name=".ui.browsing.MainActivity"
|
||||
android:launchMode="singleTask"
|
||||
android:screenOrientation="landscape"
|
||||
android:windowSoftInputMode="adjustNothing" />
|
||||
|
||||
@@ -132,8 +142,5 @@
|
||||
android:theme="@style/Theme.Jellyfin.Preferences" />
|
||||
|
||||
<activity android:name=".ui.playback.ExternalPlayerActivity" />
|
||||
|
||||
<!-- Playback Rewrite - Temporary -->
|
||||
<activity android:name=".ui.playback.rewrite.PlaybackForwardingActivity" />
|
||||
</application>
|
||||
</manifest>
|
||||
|
||||
@@ -2,14 +2,11 @@ package org.jellyfin.androidtv
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import androidx.lifecycle.ProcessLifecycleOwner
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.work.BackoffPolicy
|
||||
import androidx.work.ExistingPeriodicWorkPolicy
|
||||
import androidx.work.PeriodicWorkRequestBuilder
|
||||
import androidx.work.WorkManager
|
||||
import androidx.work.await
|
||||
import com.vanniktech.blurhash.BlurHash
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
@@ -18,7 +15,6 @@ import org.jellyfin.androidtv.data.eventhandling.SocketHandler
|
||||
import org.jellyfin.androidtv.data.repository.NotificationsRepository
|
||||
import org.jellyfin.androidtv.integration.LeanbackChannelWorker
|
||||
import org.jellyfin.androidtv.telemetry.TelemetryService
|
||||
import org.jellyfin.androidtv.util.AutoBitrate
|
||||
import org.koin.android.ext.android.inject
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
@@ -39,7 +35,6 @@ class JellyfinApplication : Application() {
|
||||
*/
|
||||
suspend fun onSessionStart() = withContext(Dispatchers.IO) {
|
||||
val workManager by inject<WorkManager>()
|
||||
val autoBitrate by inject<AutoBitrate>()
|
||||
val socketListener by inject<SocketHandler>()
|
||||
|
||||
// Update background worker
|
||||
@@ -59,16 +54,6 @@ class JellyfinApplication : Application() {
|
||||
|
||||
// Update WebSockets
|
||||
launch { socketListener.updateSession() }
|
||||
|
||||
// Detect auto bitrate
|
||||
// running in a different scope to prevent slow startups
|
||||
ProcessLifecycleOwner.get().lifecycleScope.launch { autoBitrate.detect() }
|
||||
}
|
||||
|
||||
override fun onLowMemory() {
|
||||
super.onLowMemory()
|
||||
|
||||
BlurHash.clearCache()
|
||||
}
|
||||
|
||||
override fun attachBaseContext(base: Context?) {
|
||||
|
||||
@@ -6,7 +6,7 @@ import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.UseSerializers
|
||||
import org.jellyfin.sdk.model.serializer.UUIDSerializer
|
||||
import java.util.Date
|
||||
import java.time.Instant
|
||||
import java.util.UUID
|
||||
|
||||
/**
|
||||
@@ -20,7 +20,7 @@ data class AuthenticationStoreServer(
|
||||
@SerialName("login_disclaimer") val loginDisclaimer: String? = null,
|
||||
@SerialName("splashscreen_enabled") val splashscreenEnabled: Boolean = false,
|
||||
@SerialName("setup_completed") val setupCompleted: Boolean = true,
|
||||
@SerialName("last_used") val lastUsed: Long = Date().time,
|
||||
@SerialName("last_refreshed") val lastRefreshed: Long = Date().time,
|
||||
@SerialName("last_used") val lastUsed: Long = Instant.now().toEpochMilli(),
|
||||
@SerialName("last_refreshed") val lastRefreshed: Long = Instant.now().toEpochMilli(),
|
||||
val users: Map<UUID, AuthenticationStoreUser> = emptyMap(),
|
||||
)
|
||||
|
||||
@@ -2,7 +2,7 @@ package org.jellyfin.androidtv.auth.model
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import java.util.Date
|
||||
import java.time.Instant
|
||||
|
||||
/**
|
||||
* Locally stored user information. New properties require default values or deserialization will fail.
|
||||
@@ -10,8 +10,7 @@ import java.util.Date
|
||||
@Serializable
|
||||
data class AuthenticationStoreUser(
|
||||
val name: String,
|
||||
@SerialName("last_used") val lastUsed: Long = Date().time,
|
||||
@SerialName("require_password") val requirePassword: Boolean = true,
|
||||
@SerialName("last_used") val lastUsed: Long = Instant.now().toEpochMilli(),
|
||||
@SerialName("image_tag") val imageTag: String? = null,
|
||||
@SerialName("access_token") val accessToken: String? = null,
|
||||
)
|
||||
|
||||
@@ -3,9 +3,9 @@ package org.jellyfin.androidtv.auth.model
|
||||
import org.jellyfin.sdk.api.client.exception.ApiClientException
|
||||
|
||||
sealed class LoginState
|
||||
object AuthenticatingState : LoginState()
|
||||
object RequireSignInState : LoginState()
|
||||
object ServerUnavailableState : LoginState()
|
||||
data object AuthenticatingState : LoginState()
|
||||
data object RequireSignInState : LoginState()
|
||||
data object ServerUnavailableState : LoginState()
|
||||
data class ServerVersionNotSupported(val server: Server) : LoginState()
|
||||
data class ApiClientErrorLoginState(val error: ApiClientException) : LoginState()
|
||||
object AuthenticatedState : LoginState()
|
||||
data object AuthenticatedState : LoginState()
|
||||
|
||||
@@ -5,12 +5,12 @@ sealed class QuickConnectState
|
||||
/**
|
||||
* State unknown untill first poll completed.
|
||||
*/
|
||||
object UnknownQuickConnectState : QuickConnectState()
|
||||
data object UnknownQuickConnectState : QuickConnectState()
|
||||
|
||||
/**
|
||||
* Server does not have QuickConnect enabled.
|
||||
*/
|
||||
object UnavailableQuickConnectState : QuickConnectState()
|
||||
data object UnavailableQuickConnectState : QuickConnectState()
|
||||
|
||||
/**
|
||||
* Connection is pending.
|
||||
@@ -20,4 +20,4 @@ data class PendingQuickConnectState(val code: String) : QuickConnectState()
|
||||
/**
|
||||
* User connected.
|
||||
*/
|
||||
object ConnectedQuickConnectState : QuickConnectState()
|
||||
data object ConnectedQuickConnectState : QuickConnectState()
|
||||
|
||||
@@ -2,7 +2,7 @@ package org.jellyfin.androidtv.auth.model
|
||||
|
||||
import org.jellyfin.androidtv.auth.repository.ServerRepository
|
||||
import org.jellyfin.sdk.model.ServerVersion
|
||||
import java.util.Date
|
||||
import java.time.Instant
|
||||
import java.util.UUID
|
||||
|
||||
/**
|
||||
@@ -16,7 +16,7 @@ data class Server(
|
||||
val loginDisclaimer: String? = null,
|
||||
val splashscreenEnabled: Boolean = false,
|
||||
val setupCompleted: Boolean = true,
|
||||
var dateLastAccessed: Date = Date(0),
|
||||
var dateLastAccessed: Instant = Instant.MIN,
|
||||
) {
|
||||
private val serverVersion = version?.let(ServerVersion::fromString)
|
||||
val versionSupported = serverVersion != null && serverVersion >= ServerRepository.minimumServerVersion
|
||||
|
||||
@@ -10,7 +10,6 @@ sealed class User {
|
||||
abstract val serverId: UUID
|
||||
abstract val name: String
|
||||
abstract val accessToken: String?
|
||||
abstract val requirePassword: Boolean
|
||||
abstract val imageTag: String?
|
||||
|
||||
abstract fun withToken(accessToken: String): User
|
||||
@@ -34,7 +33,6 @@ data class PrivateUser(
|
||||
override val serverId: UUID,
|
||||
override val name: String,
|
||||
override val accessToken: String?,
|
||||
override val requirePassword: Boolean,
|
||||
override val imageTag: String?,
|
||||
val lastUsed: Long,
|
||||
) : User() {
|
||||
@@ -49,7 +47,6 @@ data class PublicUser(
|
||||
override val serverId: UUID,
|
||||
override val name: String,
|
||||
override val accessToken: String?,
|
||||
override val requirePassword: Boolean,
|
||||
override val imageTag: String?,
|
||||
) : User() {
|
||||
override fun withToken(accessToken: String) = copy(accessToken = accessToken)
|
||||
|
||||
@@ -21,7 +21,7 @@ import org.jellyfin.androidtv.auth.model.ServerVersionNotSupported
|
||||
import org.jellyfin.androidtv.auth.model.User
|
||||
import org.jellyfin.androidtv.auth.store.AuthenticationPreferences
|
||||
import org.jellyfin.androidtv.auth.store.AuthenticationStore
|
||||
import org.jellyfin.androidtv.util.ImageUtils
|
||||
import org.jellyfin.androidtv.util.ImageHelper
|
||||
import org.jellyfin.androidtv.util.sdk.forUser
|
||||
import org.jellyfin.sdk.Jellyfin
|
||||
import org.jellyfin.sdk.api.client.ApiClient
|
||||
@@ -33,10 +33,9 @@ import org.jellyfin.sdk.api.client.extensions.imageApi
|
||||
import org.jellyfin.sdk.api.client.extensions.userApi
|
||||
import org.jellyfin.sdk.model.DeviceInfo
|
||||
import org.jellyfin.sdk.model.api.AuthenticationResult
|
||||
import org.jellyfin.sdk.model.api.ImageType
|
||||
import org.jellyfin.sdk.model.api.UserDto
|
||||
import timber.log.Timber
|
||||
import java.util.Date
|
||||
import java.time.Instant
|
||||
|
||||
/**
|
||||
* Repository to manage authentication of the user in the app.
|
||||
@@ -67,7 +66,7 @@ class AuthenticationRepositoryImpl(
|
||||
}
|
||||
|
||||
private fun authenticateAutomatic(server: Server, user: User): Flow<LoginState> {
|
||||
Timber.i("Authenticating user %s", user)
|
||||
Timber.i("Authenticating user ${user.id}")
|
||||
|
||||
// Automatic logic is disabled when the always authenticate preference is enabled
|
||||
if (authenticationPreferences[AuthenticationPreferences.alwaysAuthenticate]) return flowOf(RequireSignInState)
|
||||
@@ -75,8 +74,6 @@ class AuthenticationRepositoryImpl(
|
||||
val authStoreUser = authenticationStore.getUser(server.id, user.id)
|
||||
// Try login with access token
|
||||
return if (authStoreUser?.accessToken != null) authenticateToken(server, user.withToken(authStoreUser.accessToken))
|
||||
// Try login without password
|
||||
else if (!user.requirePassword) authenticateCredential(server, user.name, "")
|
||||
// Require login
|
||||
else flowOf(RequireSignInState)
|
||||
}
|
||||
@@ -125,9 +122,8 @@ class AuthenticationRepositoryImpl(
|
||||
serverId = server.id,
|
||||
name = userInfo.name!!,
|
||||
accessToken = result.accessToken,
|
||||
requirePassword = userInfo.hasPassword,
|
||||
imageTag = userInfo.primaryImageTag,
|
||||
lastUsed = Date().time,
|
||||
lastUsed = Instant.now().toEpochMilli(),
|
||||
)
|
||||
|
||||
authenticateFinish(server, userInfo, accessToken)
|
||||
@@ -166,13 +162,11 @@ class AuthenticationRepositoryImpl(
|
||||
|
||||
val updatedUser = currentUser?.copy(
|
||||
name = userInfo.name!!,
|
||||
lastUsed = Date().time,
|
||||
requirePassword = userInfo.hasPassword,
|
||||
lastUsed = Instant.now().toEpochMilli(),
|
||||
imageTag = userInfo.primaryImageTag,
|
||||
accessToken = accessToken,
|
||||
) ?: AuthenticationStoreUser(
|
||||
name = userInfo.name!!,
|
||||
requirePassword = userInfo.hasPassword,
|
||||
imageTag = userInfo.primaryImageTag,
|
||||
accessToken = accessToken,
|
||||
)
|
||||
@@ -185,11 +179,11 @@ class AuthenticationRepositoryImpl(
|
||||
if (authenticated) {
|
||||
// Update last use in store
|
||||
authenticationStore.getServer(server.id)?.let { storedServer ->
|
||||
authenticationStore.putServer(server.id, storedServer.copy(lastUsed = Date().time))
|
||||
authenticationStore.putServer(server.id, storedServer.copy(lastUsed = Instant.now().toEpochMilli()))
|
||||
}
|
||||
|
||||
authenticationStore.getUser(server.id, user.id)?.let { storedUser ->
|
||||
authenticationStore.putUser(server.id, user.id, storedUser.copy(lastUsed = Date().time))
|
||||
authenticationStore.putUser(server.id, user.id, storedUser.copy(lastUsed = Instant.now().toEpochMilli()))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -209,8 +203,7 @@ class AuthenticationRepositoryImpl(
|
||||
jellyfin.createApi(server.address).imageApi.getUserImageUrl(
|
||||
userId = user.id,
|
||||
tag = tag,
|
||||
imageType = ImageType.PRIMARY,
|
||||
maxHeight = ImageUtils.MAX_PRIMARY_IMAGE_HEIGHT
|
||||
maxHeight = ImageHelper.MAX_PRIMARY_IMAGE_HEIGHT
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,11 +22,12 @@ import org.jellyfin.sdk.api.client.extensions.brandingApi
|
||||
import org.jellyfin.sdk.api.client.extensions.systemApi
|
||||
import org.jellyfin.sdk.discovery.RecommendedServerInfo
|
||||
import org.jellyfin.sdk.discovery.RecommendedServerInfoScore
|
||||
import org.jellyfin.sdk.model.ServerVersion
|
||||
import org.jellyfin.sdk.model.api.BrandingOptions
|
||||
import org.jellyfin.sdk.model.api.ServerDiscoveryInfo
|
||||
import org.jellyfin.sdk.model.serializer.toUUID
|
||||
import timber.log.Timber
|
||||
import java.util.Date
|
||||
import java.time.Instant
|
||||
import java.util.UUID
|
||||
|
||||
/**
|
||||
@@ -47,6 +48,8 @@ interface ServerRepository {
|
||||
companion object {
|
||||
val minimumServerVersion = Jellyfin.minimumVersion.copy(build = null)
|
||||
val recommendedServerVersion = Jellyfin.apiVersion.copy(build = null)
|
||||
|
||||
val upcomingMinimumServerVersion = ServerVersion(10, 9, 0)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -135,7 +138,7 @@ class ServerRepositoryImpl(
|
||||
loginDisclaimer = branding.loginDisclaimer,
|
||||
splashscreenEnabled = branding.splashscreenEnabled,
|
||||
setupCompleted = systemInfo.startupWizardCompleted ?: true,
|
||||
lastUsed = Date().time
|
||||
lastUsed = Instant.now().toEpochMilli()
|
||||
) ?: AuthenticationStoreServer(
|
||||
name = systemInfo.serverName ?: "Jellyfin Server",
|
||||
address = chosenRecommendation.address,
|
||||
@@ -185,7 +188,7 @@ class ServerRepositoryImpl(
|
||||
}
|
||||
|
||||
private suspend fun updateServerInternal(id: UUID, server: AuthenticationStoreServer): AuthenticationStoreServer? {
|
||||
val now = Date().time
|
||||
val now = Instant.now().toEpochMilli()
|
||||
|
||||
// Only update every 10 minutes
|
||||
if (now - server.lastRefreshed < 600000 && server.version != null) return null
|
||||
@@ -223,7 +226,7 @@ class ServerRepositoryImpl(
|
||||
loginDisclaimer = loginDisclaimer,
|
||||
splashscreenEnabled = splashscreenEnabled,
|
||||
setupCompleted = setupCompleted,
|
||||
dateLastAccessed = Date(lastUsed),
|
||||
dateLastAccessed = Instant.ofEpochMilli(lastUsed),
|
||||
)
|
||||
|
||||
/**
|
||||
|
||||
@@ -35,7 +35,6 @@ class ServerUserRepositoryImpl(
|
||||
serverId = server.id,
|
||||
name = userInfo.name,
|
||||
accessToken = authInfo?.accessToken,
|
||||
requirePassword = userInfo.requirePassword,
|
||||
imageTag = userInfo.imageTag,
|
||||
lastUsed = userInfo.lastUsed,
|
||||
)
|
||||
|
||||
@@ -141,11 +141,6 @@ class SessionRepositoryImpl(
|
||||
val applied = userApiClient.applySession(session, deviceInfo)
|
||||
|
||||
if (applied && session != null) {
|
||||
// Update crash reporting URL
|
||||
val crashReportUrl = userApiClient.clientLogApi.logFileUrl(includeCredentials = false)
|
||||
telemetryPreferences[TelemetryPreferences.crashReportUrl] = crashReportUrl
|
||||
telemetryPreferences[TelemetryPreferences.crashReportToken] = session.accessToken
|
||||
|
||||
try {
|
||||
val user by userApiClient.userApi.getCurrentUser()
|
||||
userRepository.updateCurrentUser(user)
|
||||
@@ -154,6 +149,11 @@ class SessionRepositoryImpl(
|
||||
destroyCurrentSession()
|
||||
return false
|
||||
}
|
||||
|
||||
// Update crash reporting URL
|
||||
val crashReportUrl = userApiClient.clientLogApi.logFileUrl()
|
||||
telemetryPreferences[TelemetryPreferences.crashReportUrl] = crashReportUrl
|
||||
telemetryPreferences[TelemetryPreferences.crashReportToken] = session.accessToken
|
||||
} else {
|
||||
userRepository.updateCurrentUser(null)
|
||||
}
|
||||
@@ -184,18 +184,21 @@ class SessionRepositoryImpl(
|
||||
}
|
||||
|
||||
private fun ApiClient.applySession(session: Session?, newDeviceInfo: DeviceInfo = defaultDeviceInfo): Boolean {
|
||||
deviceInfo = newDeviceInfo
|
||||
|
||||
if (session == null) {
|
||||
baseUrl = null
|
||||
accessToken = null
|
||||
userId = null
|
||||
update(
|
||||
baseUrl = null,
|
||||
accessToken = null,
|
||||
deviceInfo = newDeviceInfo,
|
||||
)
|
||||
} else {
|
||||
val server = authenticationStore.getServer(session.serverId)
|
||||
?: return false
|
||||
baseUrl = server.address
|
||||
accessToken = session.accessToken
|
||||
userId = session.userId
|
||||
|
||||
update(
|
||||
baseUrl = server.address,
|
||||
accessToken = session.accessToken,
|
||||
deviceInfo = newDeviceInfo,
|
||||
)
|
||||
}
|
||||
|
||||
return true
|
||||
|
||||
@@ -17,6 +17,6 @@ class UserRepositoryImpl : UserRepository {
|
||||
override val currentUser = MutableStateFlow<UserDto?>(null)
|
||||
|
||||
override fun updateCurrentUser(user: UserDto?) {
|
||||
if (currentUser.value !== user) currentUser.value = user
|
||||
currentUser.value = user
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,9 +4,6 @@ enum class ChangeTriggerType {
|
||||
LibraryUpdated,
|
||||
MoviePlayback,
|
||||
TvPlayback,
|
||||
GuideNeedsLoad,
|
||||
MusicPlayback,
|
||||
Always,
|
||||
VideoQueueChange,
|
||||
FavoriteUpdate,
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ object Codec {
|
||||
const val ASF = "asf"
|
||||
const val AVI = "avi"
|
||||
const val DVR_MS = "dvr-ms"
|
||||
const val HLS = "hls"
|
||||
const val M2V = "m2v"
|
||||
const val M4V = "m4v"
|
||||
const val MKV = "mkv"
|
||||
@@ -47,8 +48,10 @@ object Codec {
|
||||
const val PCM_ALAW = "pcm_alaw"
|
||||
const val PCM_MULAW = "pcm_mulaw"
|
||||
const val PCM_S16LE = "pcm_s16le"
|
||||
const val PCM_S20LE = "pcm_s20le"
|
||||
const val PCM_S24LE = "pcm_s24le"
|
||||
const val TRUEHD = "truehd"
|
||||
const val VORBIS = "vorbis"
|
||||
const val WAV = "wav"
|
||||
const val WEBMA = "webma"
|
||||
const val WMA = "wma"
|
||||
|
||||
@@ -2,5 +2,6 @@ package org.jellyfin.androidtv.constant
|
||||
|
||||
object Extras {
|
||||
const val Folder = "folder"
|
||||
const val IsLiveTvSeriesRecordings = "is_livetv_series_recordings"
|
||||
const val IncludeType = "type_include"
|
||||
}
|
||||
|
||||
@@ -5,8 +5,7 @@ import org.jellyfin.androidtv.R
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
private val qualityOptions = setOf(
|
||||
0.0, // auto
|
||||
120.0, 110.0, 100.0, // 100 >=
|
||||
200.0, 180.0, 140.0, 120.0, 110.0, 100.0, // 100 >=
|
||||
90.0, 80.0, 70.0, 60.0, 50.0, 40.0, 30.0, 20.0, 15.0, 10.0, // 10 >=
|
||||
5.0, 3.0, 2.0, 1.0, // 1 >=
|
||||
0.72, 0.42 // 0 >=
|
||||
@@ -17,7 +16,6 @@ fun getQualityProfiles(
|
||||
context: Context
|
||||
): Map<String, String> = qualityOptions.associate {
|
||||
val value = when {
|
||||
it == 0.0 -> context.getString(R.string.bitrate_auto)
|
||||
it >= 1.0 -> context.getString(R.string.bitrate_mbit, it)
|
||||
else -> context.getString(R.string.bitrate_kbit, it * 1000.0)
|
||||
}
|
||||
|
||||
@@ -18,7 +18,6 @@ enum class QueryType {
|
||||
LiveTvProgram,
|
||||
LiveTvRecording,
|
||||
StaticItems,
|
||||
Persons,
|
||||
StaticAudioQueueItems,
|
||||
Artists,
|
||||
AlbumArtists,
|
||||
|
||||
@@ -3,11 +3,12 @@ package org.jellyfin.androidtv.data.compat
|
||||
import org.jellyfin.apiclient.model.dlna.DeviceProfile
|
||||
import org.jellyfin.apiclient.model.dlna.EncodingContext
|
||||
import org.jellyfin.sdk.model.api.MediaSourceInfo
|
||||
import java.util.UUID
|
||||
|
||||
open class AudioOptions {
|
||||
var enableDirectPlay = true
|
||||
var enableDirectStream = true
|
||||
var itemId: String? = null
|
||||
var itemId: UUID? = null
|
||||
var mediaSources: List<MediaSourceInfo>? = null
|
||||
var profile: DeviceProfile? = null
|
||||
|
||||
|
||||
@@ -2,12 +2,10 @@ package org.jellyfin.androidtv.data.compat;
|
||||
|
||||
import org.jellyfin.androidtv.util.Utils;
|
||||
import org.jellyfin.apiclient.model.dlna.DeviceProfile;
|
||||
import org.jellyfin.apiclient.model.dlna.DlnaProfileType;
|
||||
import org.jellyfin.apiclient.model.dlna.EncodingContext;
|
||||
import org.jellyfin.apiclient.model.dlna.SubtitleDeliveryMethod;
|
||||
import org.jellyfin.apiclient.model.dlna.SubtitleProfile;
|
||||
import org.jellyfin.apiclient.model.dlna.TranscodeSeekInfo;
|
||||
import org.jellyfin.apiclient.model.dto.NameValuePair;
|
||||
import org.jellyfin.apiclient.model.session.PlayMethod;
|
||||
import org.jellyfin.sdk.model.api.MediaProtocol;
|
||||
import org.jellyfin.sdk.model.api.MediaSourceInfo;
|
||||
@@ -15,23 +13,16 @@ import org.jellyfin.sdk.model.api.MediaStream;
|
||||
import org.jellyfin.sdk.model.api.MediaStreamType;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.UUID;
|
||||
|
||||
public class StreamInfo {
|
||||
private static final String START_TIME_TICKS = "StartTimeTicks";
|
||||
private static final String SUBTITLE_STREAM_INDEX = "SubtitleStreamIndex";
|
||||
private static final String STATIC = "Static";
|
||||
private UUID ItemId;
|
||||
|
||||
public StreamInfo() {
|
||||
setAudioCodecs(new String[]{});
|
||||
}
|
||||
|
||||
private String ItemId;
|
||||
|
||||
public final String getItemId() {
|
||||
public final UUID getItemId() {
|
||||
return ItemId;
|
||||
}
|
||||
|
||||
public final void setItemId(String value) {
|
||||
public final void setItemId(UUID value) {
|
||||
ItemId = value;
|
||||
}
|
||||
|
||||
@@ -65,16 +56,6 @@ public class StreamInfo {
|
||||
Context = value;
|
||||
}
|
||||
|
||||
private DlnaProfileType MediaType = DlnaProfileType.values()[0];
|
||||
|
||||
public final DlnaProfileType getMediaType() {
|
||||
return MediaType;
|
||||
}
|
||||
|
||||
public final void setMediaType(DlnaProfileType value) {
|
||||
MediaType = value;
|
||||
}
|
||||
|
||||
private String Container;
|
||||
|
||||
public final String getContainer() {
|
||||
@@ -95,16 +76,6 @@ public class StreamInfo {
|
||||
StartPositionTicks = value;
|
||||
}
|
||||
|
||||
private String[] AudioCodecs;
|
||||
|
||||
public final String[] getAudioCodecs() {
|
||||
return AudioCodecs;
|
||||
}
|
||||
|
||||
public final void setAudioCodecs(String[] value) {
|
||||
AudioCodecs = value;
|
||||
}
|
||||
|
||||
private DeviceProfile DeviceProfile;
|
||||
|
||||
public final DeviceProfile getDeviceProfile() {
|
||||
@@ -115,16 +86,6 @@ public class StreamInfo {
|
||||
DeviceProfile = value;
|
||||
}
|
||||
|
||||
private String DeviceId;
|
||||
|
||||
public final String getDeviceId() {
|
||||
return DeviceId;
|
||||
}
|
||||
|
||||
public final void setDeviceId(String value) {
|
||||
DeviceId = value;
|
||||
}
|
||||
|
||||
private Long RunTimeTicks = null;
|
||||
|
||||
public final Long getRunTimeTicks() {
|
||||
@@ -171,122 +132,6 @@ public class StreamInfo {
|
||||
return getMediaSource() == null ? null : getMediaSource().getId();
|
||||
}
|
||||
|
||||
public final boolean getIsDirectStream() {
|
||||
return getPlayMethod() == PlayMethod.DirectStream || getPlayMethod() == PlayMethod.DirectPlay;
|
||||
}
|
||||
|
||||
public final String toUrl(String baseUrl, String accessToken) {
|
||||
if (!Utils.isEmpty(getMediaUrl())) {
|
||||
return getMediaUrl();
|
||||
}
|
||||
|
||||
if (getPlayMethod() == PlayMethod.DirectPlay) {
|
||||
return getMediaSource().getPath();
|
||||
}
|
||||
|
||||
if (Utils.isEmpty(baseUrl)) {
|
||||
throw new IllegalArgumentException(baseUrl);
|
||||
}
|
||||
|
||||
ArrayList<String> list = new ArrayList<String>();
|
||||
for (NameValuePair pair : buildParams(this, accessToken, false)) {
|
||||
if (Utils.isEmpty(pair.getValue())) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Try to keep the url clean by omitting defaults
|
||||
if (START_TIME_TICKS.equalsIgnoreCase(pair.getName()) && "0".equalsIgnoreCase(pair.getValue())) {
|
||||
continue;
|
||||
}
|
||||
if (SUBTITLE_STREAM_INDEX.equalsIgnoreCase(pair.getName()) && "-1".equalsIgnoreCase(pair.getValue())) {
|
||||
continue;
|
||||
}
|
||||
if (STATIC.equalsIgnoreCase(pair.getName()) && "false".equalsIgnoreCase(pair.getValue())) {
|
||||
continue;
|
||||
}
|
||||
|
||||
list.add(String.format("%1$s=%2$s", pair.getName(), pair.getValue()));
|
||||
}
|
||||
|
||||
String queryString = Utils.join("&", list);
|
||||
|
||||
return getUrl(baseUrl, queryString);
|
||||
}
|
||||
|
||||
private String getUrl(String baseUrl, String queryString) {
|
||||
if (Utils.isEmpty(baseUrl)) {
|
||||
throw new IllegalArgumentException(baseUrl);
|
||||
}
|
||||
|
||||
String extension = Utils.isEmpty(getContainer()) ? "" : "." + getContainer();
|
||||
|
||||
// remove trailing slashes
|
||||
baseUrl = baseUrl.replaceAll("[/]+$", "");
|
||||
|
||||
if (getMediaType() == DlnaProfileType.Audio) {
|
||||
return String.format("%1$s/audio/%2$s/stream%3$s?%4$s", baseUrl, getItemId(), extension, queryString);
|
||||
}
|
||||
|
||||
return String.format("%1$s/videos/%2$s/stream%3$s?%4$s", baseUrl, getItemId(), extension, queryString);
|
||||
}
|
||||
|
||||
private static ArrayList<NameValuePair> buildParams(StreamInfo item, String accessToken, boolean isDlna) {
|
||||
ArrayList<NameValuePair> list = new ArrayList<NameValuePair>();
|
||||
|
||||
String audioCodecs = item.getAudioCodecs().length == 0 ? "" : Utils.join(",", item.getAudioCodecs());
|
||||
|
||||
list.add(new NameValuePair("DeviceProfileId", ""));
|
||||
String tempVar2 = item.getDeviceId();
|
||||
list.add(new NameValuePair("DeviceId", (tempVar2 != null) ? tempVar2 : ""));
|
||||
String tempVar3 = item.getMediaSourceId();
|
||||
list.add(new NameValuePair("MediaSourceId", (tempVar3 != null) ? tempVar3 : ""));
|
||||
list.add(new NameValuePair(STATIC, Boolean.valueOf(item.getIsDirectStream()).toString().toLowerCase()));
|
||||
list.add(new NameValuePair("VideoCodec", ""));
|
||||
list.add(new NameValuePair("AudioCodec", audioCodecs));
|
||||
list.add(new NameValuePair("AudioStreamIndex", ""));
|
||||
list.add(new NameValuePair("SubtitleStreamIndex", ""));
|
||||
list.add(new NameValuePair("VideoBitrate", ""));
|
||||
list.add(new NameValuePair("AudioBitrate", ""));
|
||||
list.add(new NameValuePair("MaxAudioChannels", ""));
|
||||
list.add(new NameValuePair("MaxFramerate", ""));
|
||||
list.add(new NameValuePair("MaxWidth", ""));
|
||||
list.add(new NameValuePair("MaxHeight", ""));
|
||||
|
||||
list.add(new NameValuePair(START_TIME_TICKS, String.valueOf(item.getStartPositionTicks())));
|
||||
|
||||
list.add(new NameValuePair("Level", ""));
|
||||
|
||||
list.add(new NameValuePair("MaxRefFrames", ""));
|
||||
list.add(new NameValuePair("MaxVideoBitDepth", ""));
|
||||
list.add(new NameValuePair("Profile", ""));
|
||||
|
||||
// no longer used
|
||||
list.add(new NameValuePair("Cabac", ""));
|
||||
|
||||
String tempVar6 = item.getPlaySessionId();
|
||||
list.add(new NameValuePair("PlaySessionId", (tempVar6 != null) ? tempVar6 : ""));
|
||||
list.add(new NameValuePair("api_key", (accessToken != null) ? accessToken : ""));
|
||||
|
||||
String liveStreamId = item.getMediaSource() == null ? null : item.getMediaSource().getLiveStreamId();
|
||||
list.add(new NameValuePair("LiveStreamId", (liveStreamId != null) ? liveStreamId : ""));
|
||||
|
||||
if (isDlna) {
|
||||
list.add(new NameValuePair("ItemId", item.getItemId()));
|
||||
}
|
||||
|
||||
list.add(new NameValuePair("CopyTimestamps", "false"));
|
||||
list.add(new NameValuePair("ForceLiveStream", "false"));
|
||||
list.add(new NameValuePair("SubtitleMethod", ""));
|
||||
|
||||
list.add(new NameValuePair("TranscodingMaxAudioChannels", ""));
|
||||
list.add(new NameValuePair("EnableSubtitlesInManifest", "false"));
|
||||
|
||||
String tempVar7 = item.getMediaSource().getETag();
|
||||
list.add(new NameValuePair("Tag", (tempVar7 != null) ? tempVar7 : ""));
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
public final ArrayList<SubtitleStreamInfo> getSubtitleProfiles(boolean includeSelectedTrackOnly, String baseUrl, String accessToken) {
|
||||
return getSubtitleProfiles(includeSelectedTrackOnly, false, baseUrl, accessToken);
|
||||
}
|
||||
|
||||
@@ -6,56 +6,61 @@ import android.os.Build
|
||||
import android.widget.Toast
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.jellyfin.androidtv.data.model.DataRefreshService
|
||||
import org.jellyfin.androidtv.ui.itemhandling.ItemLauncher
|
||||
import org.jellyfin.androidtv.ui.navigation.Destinations
|
||||
import org.jellyfin.androidtv.ui.navigation.NavigationRepository
|
||||
import org.jellyfin.androidtv.ui.playback.MediaManager
|
||||
import org.jellyfin.androidtv.ui.playback.PlaybackControllerContainer
|
||||
import org.jellyfin.androidtv.util.apiclient.PlaybackHelper
|
||||
import org.jellyfin.androidtv.util.PlaybackHelper
|
||||
import org.jellyfin.sdk.api.client.ApiClient
|
||||
import org.jellyfin.sdk.api.client.exception.ApiClientException
|
||||
import org.jellyfin.sdk.api.client.extensions.sessionApi
|
||||
import org.jellyfin.sdk.api.client.extensions.userLibraryApi
|
||||
import org.jellyfin.sdk.api.sockets.SocketInstance
|
||||
import org.jellyfin.sdk.api.sockets.SocketInstanceState
|
||||
import org.jellyfin.sdk.api.sockets.addGeneralCommandsListener
|
||||
import org.jellyfin.sdk.api.sockets.addListener
|
||||
import org.jellyfin.sdk.api.sockets.subscribe
|
||||
import org.jellyfin.sdk.api.sockets.subscribeGeneralCommand
|
||||
import org.jellyfin.sdk.api.sockets.subscribeGeneralCommands
|
||||
import org.jellyfin.sdk.model.api.BaseItemKind
|
||||
import org.jellyfin.sdk.model.api.GeneralCommandType
|
||||
import org.jellyfin.sdk.model.api.LibraryChangedMessage
|
||||
import org.jellyfin.sdk.model.api.LibraryUpdateInfo
|
||||
import org.jellyfin.sdk.model.api.MediaType
|
||||
import org.jellyfin.sdk.model.api.PlayMessage
|
||||
import org.jellyfin.sdk.model.api.PlaystateCommand
|
||||
import org.jellyfin.sdk.model.constant.MediaType
|
||||
import org.jellyfin.sdk.model.api.PlaystateMessage
|
||||
import org.jellyfin.sdk.model.extensions.get
|
||||
import org.jellyfin.sdk.model.extensions.getValue
|
||||
import org.jellyfin.sdk.model.serializer.toUUIDOrNull
|
||||
import org.jellyfin.sdk.model.socket.LibraryChangedMessage
|
||||
import org.jellyfin.sdk.model.socket.PlayMessage
|
||||
import org.jellyfin.sdk.model.socket.PlayStateMessage
|
||||
import timber.log.Timber
|
||||
import java.time.Instant
|
||||
import java.util.UUID
|
||||
|
||||
class SocketHandler(
|
||||
private val context: Context,
|
||||
private val api: ApiClient,
|
||||
private val socketInstance: SocketInstance,
|
||||
private val dataRefreshService: DataRefreshService,
|
||||
private val mediaManager: MediaManager,
|
||||
private val playbackControllerContainer: PlaybackControllerContainer,
|
||||
private val navigationRepository: NavigationRepository,
|
||||
private val audioManager: AudioManager,
|
||||
private val itemLauncher: ItemLauncher,
|
||||
private val playbackHelper: PlaybackHelper,
|
||||
) {
|
||||
private val coroutineScope = CoroutineScope(Dispatchers.IO)
|
||||
val state = socketInstance.state
|
||||
|
||||
suspend fun updateSession() {
|
||||
try {
|
||||
api.sessionApi.postCapabilities(
|
||||
playableMediaTypes = listOf(MediaType.Video, MediaType.Audio),
|
||||
playableMediaTypes = listOf(MediaType.VIDEO, MediaType.AUDIO),
|
||||
supportsMediaControl = true,
|
||||
supportedCommands = buildList {
|
||||
add(GeneralCommandType.DISPLAY_CONTENT)
|
||||
add(GeneralCommandType.SET_SUBTITLE_STREAM_INDEX)
|
||||
add(GeneralCommandType.SET_AUDIO_STREAM_INDEX)
|
||||
|
||||
add(GeneralCommandType.DISPLAY_MESSAGE)
|
||||
add(GeneralCommandType.SEND_STRING)
|
||||
@@ -76,38 +81,70 @@ class SocketHandler(
|
||||
Timber.e(err, "Unable to update capabilities")
|
||||
return
|
||||
}
|
||||
|
||||
val isOffline = state.value == SocketInstanceState.DISCONNECTED || state.value == SocketInstanceState.ERROR
|
||||
if (isOffline) socketInstance.reconnect()
|
||||
else socketInstance.updateCredentials()
|
||||
}
|
||||
|
||||
init {
|
||||
socketInstance.apply {
|
||||
api.webSocket.apply {
|
||||
// Library
|
||||
addListener<LibraryChangedMessage> { message -> onLibraryChanged(message.info) }
|
||||
subscribe<LibraryChangedMessage>()
|
||||
.onEach { message -> message.data?.let(::onLibraryChanged) }
|
||||
.launchIn(coroutineScope)
|
||||
|
||||
// Media playback
|
||||
addListener<PlayMessage> { message -> onPlayMessage(message) }
|
||||
addListener<PlayStateMessage> { message -> onPlayStateMessage(message) }
|
||||
subscribe<PlayMessage>()
|
||||
.onEach { message -> onPlayMessage(message) }
|
||||
.launchIn(coroutineScope)
|
||||
|
||||
subscribe<PlaystateMessage>()
|
||||
.onEach { message -> onPlayStateMessage(message) }
|
||||
.launchIn(coroutineScope)
|
||||
|
||||
subscribeGeneralCommand(GeneralCommandType.SET_SUBTITLE_STREAM_INDEX)
|
||||
.onEach { message ->
|
||||
val index = message["index"]?.toIntOrNull() ?: return@onEach
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
playbackControllerContainer.playbackController?.switchSubtitleStream(index)
|
||||
}
|
||||
}
|
||||
.launchIn(coroutineScope)
|
||||
|
||||
subscribeGeneralCommand(GeneralCommandType.SET_AUDIO_STREAM_INDEX)
|
||||
.onEach { message ->
|
||||
val index = message["index"]?.toIntOrNull() ?: return@onEach
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
playbackControllerContainer.playbackController?.switchAudioStream(index)
|
||||
}
|
||||
}
|
||||
.launchIn(coroutineScope)
|
||||
|
||||
// General commands
|
||||
addGeneralCommandsListener(setOf(GeneralCommandType.DISPLAY_CONTENT)) { message ->
|
||||
val itemId by message
|
||||
val itemType by message
|
||||
subscribeGeneralCommand(GeneralCommandType.DISPLAY_CONTENT)
|
||||
.onEach { message ->
|
||||
val itemId by message
|
||||
val itemType by message
|
||||
|
||||
val itemUuid = itemId?.toUUIDOrNull()
|
||||
val itemKind = itemType?.let { type -> BaseItemKind.values().find { value -> value.serialName.equals(type, true) } }
|
||||
val itemUuid = itemId?.toUUIDOrNull()
|
||||
val itemKind = itemType?.let { type ->
|
||||
BaseItemKind.entries.find { value ->
|
||||
value.serialName.equals(type, true)
|
||||
}
|
||||
}
|
||||
|
||||
if (itemUuid != null && itemKind != null) onDisplayContent(itemUuid, itemKind)
|
||||
}
|
||||
addGeneralCommandsListener(setOf(GeneralCommandType.DISPLAY_MESSAGE, GeneralCommandType.SEND_STRING)) { message ->
|
||||
val header by message
|
||||
val text by message
|
||||
val string by message
|
||||
if (itemUuid != null && itemKind != null) onDisplayContent(itemUuid, itemKind)
|
||||
}
|
||||
.launchIn(coroutineScope)
|
||||
|
||||
onDisplayMessage(header, text ?: string)
|
||||
}
|
||||
subscribeGeneralCommands(setOf(GeneralCommandType.DISPLAY_MESSAGE, GeneralCommandType.SEND_STRING))
|
||||
.onEach { message ->
|
||||
val header by message
|
||||
val text by message
|
||||
val string by message
|
||||
|
||||
onDisplayMessage(header, text ?: string)
|
||||
}
|
||||
.launchIn(coroutineScope)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -120,62 +157,69 @@ class SocketHandler(
|
||||
})
|
||||
|
||||
if (info.itemsAdded.any() || info.itemsRemoved.any())
|
||||
dataRefreshService.lastLibraryChange = System.currentTimeMillis()
|
||||
dataRefreshService.lastLibraryChange = Instant.now()
|
||||
}
|
||||
|
||||
private fun onPlayMessage(message: PlayMessage) {
|
||||
val itemId = message.request.itemIds?.firstOrNull() ?: return
|
||||
val itemId = message.data?.itemIds?.firstOrNull() ?: return
|
||||
|
||||
PlaybackHelper.retrieveAndPlay(
|
||||
itemId.toString(),
|
||||
false,
|
||||
message.request.startPositionTicks,
|
||||
context
|
||||
)
|
||||
runCatching {
|
||||
playbackHelper.retrieveAndPlay(
|
||||
itemId,
|
||||
false,
|
||||
message.data?.startPositionTicks,
|
||||
context
|
||||
)
|
||||
}.onFailure { Timber.w(it, "Failed to start playback") }
|
||||
}
|
||||
|
||||
@Suppress("ComplexMethod")
|
||||
private fun onPlayStateMessage(message: PlayStateMessage) = coroutineScope.launch(Dispatchers.Main) {
|
||||
Timber.i("Received PlayStateMessage with command ${message.request.command}")
|
||||
val playbackController = playbackControllerContainer.playbackController
|
||||
// Audio playback uses the mediaManager, video playback and live tv use the playbackController
|
||||
if (mediaManager.isAudioPlayerInitialized) when (message.request.command) {
|
||||
PlaystateCommand.STOP -> mediaManager.stopAudio(true)
|
||||
PlaystateCommand.PAUSE, PlaystateCommand.UNPAUSE, PlaystateCommand.PLAY_PAUSE -> mediaManager.playPauseAudio()
|
||||
PlaystateCommand.NEXT_TRACK -> mediaManager.nextAudioItem()
|
||||
PlaystateCommand.PREVIOUS_TRACK -> mediaManager.prevAudioItem()
|
||||
// Not implemented
|
||||
PlaystateCommand.SEEK,
|
||||
PlaystateCommand.REWIND,
|
||||
PlaystateCommand.FAST_FORWARD -> Unit
|
||||
} else when (message.request.command) {
|
||||
PlaystateCommand.STOP -> playbackController?.endPlayback(true)
|
||||
PlaystateCommand.PAUSE, PlaystateCommand.UNPAUSE, PlaystateCommand.PLAY_PAUSE -> playbackController?.playPause()
|
||||
PlaystateCommand.NEXT_TRACK -> playbackController?.next()
|
||||
PlaystateCommand.PREVIOUS_TRACK -> playbackController?.prev()
|
||||
PlaystateCommand.SEEK -> playbackController?.seek(
|
||||
(message.request.seekPositionTicks ?: 0) / TICKS_TO_MS
|
||||
)
|
||||
PlaystateCommand.REWIND -> playbackController?.rewind()
|
||||
PlaystateCommand.FAST_FORWARD -> playbackController?.fastForward()
|
||||
private suspend fun onPlayStateMessage(message: PlaystateMessage) = withContext(Dispatchers.Main) {
|
||||
Timber.i("Received PlayStateMessage with command ${message.data?.command}")
|
||||
|
||||
// Audio playback uses (Rewrite)MediaManager, (legacy) video playback uses playbackController
|
||||
when {
|
||||
mediaManager.hasAudioQueueItems() -> {
|
||||
Timber.i("Ignoring PlayStateMessage: should be handled by PlaySessionSocketService")
|
||||
return@withContext
|
||||
}
|
||||
|
||||
// PlaybackController
|
||||
else -> {
|
||||
val playbackController = playbackControllerContainer.playbackController
|
||||
when (message.data?.command) {
|
||||
PlaystateCommand.STOP -> playbackController?.endPlayback(true)
|
||||
PlaystateCommand.PAUSE, PlaystateCommand.UNPAUSE, PlaystateCommand.PLAY_PAUSE -> playbackController?.playPause()
|
||||
PlaystateCommand.NEXT_TRACK -> playbackController?.next()
|
||||
PlaystateCommand.PREVIOUS_TRACK -> playbackController?.prev()
|
||||
PlaystateCommand.SEEK -> playbackController?.seek(
|
||||
(message.data?.seekPositionTicks ?: 0) / TICKS_TO_MS
|
||||
)
|
||||
|
||||
PlaystateCommand.REWIND -> playbackController?.rewind()
|
||||
PlaystateCommand.FAST_FORWARD -> playbackController?.fastForward()
|
||||
|
||||
null -> Unit
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun onDisplayContent(itemId: UUID, itemKind: BaseItemKind) {
|
||||
private suspend fun onDisplayContent(itemId: UUID, itemKind: BaseItemKind) = withContext(Dispatchers.Main) {
|
||||
val playbackController = playbackControllerContainer.playbackController
|
||||
|
||||
if (playbackController?.isPlaying == true || playbackController?.isPaused == true) {
|
||||
Timber.i("Not launching $itemId: playback in progress")
|
||||
return
|
||||
return@withContext
|
||||
}
|
||||
|
||||
Timber.i("Launching $itemId")
|
||||
|
||||
when (itemKind) {
|
||||
BaseItemKind.USER_VIEW,
|
||||
BaseItemKind.COLLECTION_FOLDER -> coroutineScope.launch {
|
||||
BaseItemKind.COLLECTION_FOLDER -> {
|
||||
val item by api.userLibraryApi.getItem(itemId = itemId)
|
||||
ItemLauncher.launchUserView(item)
|
||||
itemLauncher.launchUserView(item)
|
||||
}
|
||||
|
||||
else -> navigationRepository.navigate(Destinations.itemDetails(itemId))
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
package org.jellyfin.androidtv.data.model
|
||||
|
||||
import org.jellyfin.sdk.model.api.BaseItemDto
|
||||
import java.time.Instant
|
||||
import java.util.UUID
|
||||
|
||||
class DataRefreshService {
|
||||
var lastDeletedItemId: UUID? = null
|
||||
var lastPlayback: Long = 0
|
||||
var lastMoviePlayback: Long = 0
|
||||
var lastTvPlayback: Long = 0
|
||||
var lastMusicPlayback: Long = 0
|
||||
var lastLibraryChange: Long = 0
|
||||
var lastVideoQueueChange: Long = 0
|
||||
var lastFavoriteUpdate: Long = 0
|
||||
var lastPlayback: Instant? = null
|
||||
var lastMoviePlayback: Instant? = null
|
||||
var lastTvPlayback: Instant? = null
|
||||
var lastLibraryChange: Instant? = null
|
||||
var lastFavoriteUpdate: Instant? = null
|
||||
var lastPlayedItem: BaseItemDto? = null
|
||||
}
|
||||
|
||||
@@ -1,18 +1,15 @@
|
||||
package org.jellyfin.androidtv.data.model
|
||||
|
||||
import org.jellyfin.apiclient.model.querying.ItemFilter
|
||||
import org.jellyfin.sdk.model.api.ItemFilter
|
||||
|
||||
|
||||
class FilterOptions {
|
||||
var isFavoriteOnly = false
|
||||
var isUnwatchedOnly = false
|
||||
|
||||
val filters: Array<ItemFilter>?
|
||||
get() {
|
||||
if (!isUnwatchedOnly && !isFavoriteOnly) return null
|
||||
|
||||
return buildList {
|
||||
if (isFavoriteOnly) add(ItemFilter.IsFavorite)
|
||||
if (isUnwatchedOnly) add(ItemFilter.IsUnplayed)
|
||||
}.toTypedArray()
|
||||
val filters: Set<ItemFilter>
|
||||
get() = buildSet {
|
||||
if (isFavoriteOnly) add(ItemFilter.IS_FAVORITE)
|
||||
if (isUnwatchedOnly) add(ItemFilter.IS_UNPLAYED)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
package org.jellyfin.androidtv.data.querying;
|
||||
|
||||
data class AdditionalPartsQuery(val itemId: String)
|
||||
@@ -1,5 +0,0 @@
|
||||
package org.jellyfin.androidtv.data.querying
|
||||
|
||||
import org.jellyfin.apiclient.model.querying.ArtistsQuery
|
||||
|
||||
class AlbumArtistsQuery : ArtistsQuery()
|
||||
@@ -0,0 +1,7 @@
|
||||
package org.jellyfin.androidtv.data.querying;
|
||||
|
||||
import java.util.UUID
|
||||
|
||||
data class GetAdditionalPartsRequest(
|
||||
val itemId: UUID,
|
||||
)
|
||||
@@ -0,0 +1,3 @@
|
||||
package org.jellyfin.androidtv.data.querying
|
||||
|
||||
data object GetSeriesTimersRequest
|
||||
@@ -0,0 +1,7 @@
|
||||
package org.jellyfin.androidtv.data.querying
|
||||
|
||||
import java.util.UUID
|
||||
|
||||
data class GetSpecialsRequest(
|
||||
val itemId: UUID,
|
||||
)
|
||||
@@ -0,0 +1,7 @@
|
||||
package org.jellyfin.androidtv.data.querying
|
||||
|
||||
import java.util.UUID
|
||||
|
||||
data class GetTrailersRequest(
|
||||
val itemId: UUID,
|
||||
)
|
||||
@@ -1,3 +1,3 @@
|
||||
package org.jellyfin.androidtv.data.querying
|
||||
|
||||
object ViewQuery
|
||||
data object GetUserViewsRequest
|
||||
@@ -1,5 +0,0 @@
|
||||
package org.jellyfin.androidtv.data.querying
|
||||
|
||||
data class SpecialsQuery(
|
||||
val itemId: String,
|
||||
)
|
||||
@@ -1,26 +0,0 @@
|
||||
package org.jellyfin.androidtv.data.querying
|
||||
|
||||
import org.jellyfin.androidtv.auth.repository.UserRepository
|
||||
import org.jellyfin.apiclient.model.querying.ItemFields
|
||||
import org.jellyfin.apiclient.model.querying.ItemQuery
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.get
|
||||
|
||||
class StdItemQuery @JvmOverloads constructor(
|
||||
fields: Array<ItemFields> = defaultFields
|
||||
) : ItemQuery(), KoinComponent {
|
||||
companion object {
|
||||
val defaultFields = arrayOf(
|
||||
ItemFields.PrimaryImageAspectRatio,
|
||||
ItemFields.Overview,
|
||||
ItemFields.ItemCounts,
|
||||
ItemFields.DisplayPreferencesId,
|
||||
ItemFields.ChildCount
|
||||
)
|
||||
}
|
||||
|
||||
init {
|
||||
userId = get<UserRepository>().currentUser.value!!.id.toString()
|
||||
setFields(fields)
|
||||
}
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
package org.jellyfin.androidtv.data.querying
|
||||
|
||||
data class TrailersQuery(
|
||||
val itemId: String,
|
||||
)
|
||||
@@ -6,6 +6,7 @@ import org.jellyfin.sdk.api.client.extensions.playStateApi
|
||||
import org.jellyfin.sdk.api.client.extensions.userLibraryApi
|
||||
import org.jellyfin.sdk.model.UUID
|
||||
import org.jellyfin.sdk.model.api.UserItemDataDto
|
||||
import java.time.Instant
|
||||
|
||||
interface ItemMutationRepository {
|
||||
suspend fun setFavorite(item: UUID, favorite: Boolean): UserItemDataDto
|
||||
@@ -22,7 +23,7 @@ class ItemMutationRepositoryImpl(
|
||||
else -> api.userLibraryApi.unmarkFavoriteItem(itemId = item)
|
||||
}
|
||||
|
||||
dataRefreshService.lastFavoriteUpdate = System.currentTimeMillis()
|
||||
dataRefreshService.lastFavoriteUpdate = Instant.now()
|
||||
return response
|
||||
}
|
||||
|
||||
|
||||
@@ -1,25 +1,27 @@
|
||||
package org.jellyfin.androidtv.data.repository
|
||||
|
||||
import android.app.UiModeManager
|
||||
import android.content.Context
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import org.jellyfin.androidtv.BuildConfig
|
||||
import org.jellyfin.androidtv.R
|
||||
import org.jellyfin.androidtv.auth.model.Server
|
||||
import org.jellyfin.androidtv.auth.repository.ServerRepository
|
||||
import org.jellyfin.androidtv.data.model.AppNotification
|
||||
import org.jellyfin.androidtv.preference.SystemPreferences
|
||||
import org.jellyfin.androidtv.util.isTvDevice
|
||||
import org.jellyfin.sdk.model.ServerVersion
|
||||
|
||||
interface NotificationsRepository {
|
||||
val notifications: StateFlow<List<AppNotification>>
|
||||
|
||||
fun dismissNotification(item: AppNotification)
|
||||
fun addDefaultNotifications()
|
||||
fun updateServerNotifications(server: Server?)
|
||||
}
|
||||
|
||||
class NotificationsRepositoryImpl(
|
||||
private val context: Context,
|
||||
private val uiModeManager: UiModeManager,
|
||||
private val systemPreferences: SystemPreferences,
|
||||
) : NotificationsRepository {
|
||||
override val notifications = MutableStateFlow(emptyList<AppNotification>())
|
||||
@@ -34,15 +36,28 @@ class NotificationsRepositoryImpl(
|
||||
addBetaNotification()
|
||||
}
|
||||
|
||||
private fun addNotification(message: String, public: Boolean = false, dismiss: () -> Unit = {}) {
|
||||
notifications.value = notifications.value + AppNotification(message, dismiss, public)
|
||||
private fun addNotification(
|
||||
message: String,
|
||||
public: Boolean = false,
|
||||
dismiss: () -> Unit = {}
|
||||
): AppNotification {
|
||||
val notification = AppNotification(message, dismiss, public)
|
||||
notifications.value += notification
|
||||
return notification
|
||||
}
|
||||
|
||||
private fun removeNotification(notification: AppNotification) {
|
||||
notifications.value -= notification
|
||||
}
|
||||
|
||||
private fun addUiModeNotification() {
|
||||
val disableUiModeWarning = systemPreferences[SystemPreferences.disableUiModeWarning]
|
||||
|
||||
if (!context.isTvDevice() && !disableUiModeWarning) {
|
||||
addNotification(context.getString(R.string.app_notification_uimode_invalid), public = true)
|
||||
addNotification(
|
||||
context.getString(R.string.app_notification_uimode_invalid),
|
||||
public = true
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,8 +68,28 @@ class NotificationsRepositoryImpl(
|
||||
|
||||
if (isBeta && currentVersion != dismissedVersion) {
|
||||
addNotification(context.getString(R.string.app_notification_beta, currentVersion)) {
|
||||
systemPreferences[SystemPreferences.dismissedBetaNotificationVersion] = currentVersion
|
||||
systemPreferences[SystemPreferences.dismissedBetaNotificationVersion] =
|
||||
currentVersion
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update server notification
|
||||
private var _updateServerNotification: AppNotification? = null
|
||||
override fun updateServerNotifications(server: Server?) {
|
||||
// Remove current update notification
|
||||
_updateServerNotification?.let(::removeNotification)
|
||||
|
||||
val currentServerVersion = server?.version?.let(ServerVersion::fromString) ?: return
|
||||
if (currentServerVersion < ServerRepository.upcomingMinimumServerVersion) {
|
||||
_updateServerNotification =
|
||||
addNotification(
|
||||
message = context.getString(
|
||||
R.string.app_notification_update_soon,
|
||||
currentServerVersion,
|
||||
ServerRepository.upcomingMinimumServerVersion
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,14 +5,14 @@ import kotlinx.coroutines.flow.flow
|
||||
import org.jellyfin.sdk.api.client.ApiClient
|
||||
import org.jellyfin.sdk.api.client.extensions.userViewsApi
|
||||
import org.jellyfin.sdk.model.api.BaseItemDto
|
||||
import org.jellyfin.sdk.model.constant.CollectionType
|
||||
import org.jellyfin.sdk.model.api.CollectionType
|
||||
|
||||
interface UserViewsRepository {
|
||||
val views: Flow<Collection<BaseItemDto>>
|
||||
|
||||
fun isSupported(collectionType: String?): Boolean
|
||||
fun allowViewSelection(collectionType: String?): Boolean
|
||||
fun allowGridView(collectionType: String?): Boolean
|
||||
fun isSupported(collectionType: CollectionType?): Boolean
|
||||
fun allowViewSelection(collectionType: CollectionType?): Boolean
|
||||
fun allowGridView(collectionType: CollectionType?): Boolean
|
||||
}
|
||||
|
||||
class UserViewsRepositoryImpl(
|
||||
@@ -26,25 +26,25 @@ class UserViewsRepositoryImpl(
|
||||
emit(filteredViews)
|
||||
}
|
||||
|
||||
override fun isSupported(collectionType: String?) = collectionType !in unsupportedCollectionTypes
|
||||
override fun allowViewSelection(collectionType: String?) = collectionType !in disallowViewSelectionCollectionTypes
|
||||
override fun allowGridView(collectionType: String?) = collectionType !in disallowGridViewCollectionTypes
|
||||
override fun isSupported(collectionType: CollectionType?) = collectionType !in unsupportedCollectionTypes
|
||||
override fun allowViewSelection(collectionType: CollectionType?) = collectionType !in disallowViewSelectionCollectionTypes
|
||||
override fun allowGridView(collectionType: CollectionType?) = collectionType !in disallowGridViewCollectionTypes
|
||||
|
||||
private companion object {
|
||||
private val unsupportedCollectionTypes = arrayOf(
|
||||
CollectionType.Books,
|
||||
CollectionType.Folders
|
||||
CollectionType.BOOKS,
|
||||
CollectionType.FOLDERS
|
||||
)
|
||||
|
||||
private val disallowViewSelectionCollectionTypes = arrayOf(
|
||||
CollectionType.LiveTv,
|
||||
CollectionType.Music,
|
||||
CollectionType.Photos,
|
||||
CollectionType.LIVETV,
|
||||
CollectionType.MUSIC,
|
||||
CollectionType.PHOTOS,
|
||||
)
|
||||
|
||||
private val disallowGridViewCollectionTypes = arrayOf(
|
||||
CollectionType.LiveTv,
|
||||
CollectionType.Music
|
||||
CollectionType.LIVETV,
|
||||
CollectionType.MUSIC
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ import org.jellyfin.sdk.api.client.ApiClient
|
||||
import org.jellyfin.sdk.api.client.extensions.imageApi
|
||||
import org.jellyfin.sdk.model.api.BaseItemDto
|
||||
import org.jellyfin.sdk.model.api.ImageType
|
||||
import java.time.Instant
|
||||
import java.util.UUID
|
||||
import kotlin.time.Duration
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
@@ -47,7 +48,11 @@ class BackgroundService(
|
||||
private var _backgrounds = emptyList<ImageBitmap>()
|
||||
private var _currentIndex = 0
|
||||
private var _currentBackground = MutableStateFlow<ImageBitmap?>(null)
|
||||
private var _blurBackground = MutableStateFlow(false)
|
||||
private var _enabled = MutableStateFlow(true)
|
||||
val currentBackground get() = _currentBackground.asStateFlow()
|
||||
val blurBackground get() = _blurBackground.asStateFlow()
|
||||
val enabled get() = _enabled.asStateFlow()
|
||||
|
||||
// Helper function for [setBackground]
|
||||
private fun List<String>?.getUrls(itemId: UUID?): List<String> {
|
||||
@@ -74,6 +79,9 @@ class BackgroundService(
|
||||
if (baseItem == null || !userPreferences[UserPreferences.backdropEnabled])
|
||||
return clearBackgrounds()
|
||||
|
||||
// Enable blur for backdrops
|
||||
_blurBackground.value = true
|
||||
|
||||
// Get all backdrop urls
|
||||
val itemBackdropUrls = baseItem.backdropImageTags.getUrls(baseItem.id)
|
||||
val parentBackdropUrls = baseItem.parentBackdropImageTags.getUrls(baseItem.parentBackdropItemId)
|
||||
@@ -94,6 +102,9 @@ class BackgroundService(
|
||||
if (!server.splashscreenEnabled)
|
||||
return clearBackgrounds()
|
||||
|
||||
// Disable blur on splashscreen
|
||||
_blurBackground.value = false
|
||||
|
||||
// Manually grab the backdrop URL
|
||||
val api = jellyfin.createApi(baseUrl = server.address)
|
||||
val splashscreenUrl = api.imageApi.getSplashscreenUrl()
|
||||
@@ -104,6 +115,9 @@ class BackgroundService(
|
||||
private fun loadBackgrounds(backdropUrls: Set<String>) {
|
||||
if (backdropUrls.isEmpty()) return clearBackgrounds()
|
||||
|
||||
// Re-enable backgrounds if disabled
|
||||
_enabled.value = true
|
||||
|
||||
// Cancel current loading job
|
||||
loadBackgroundsJob?.cancel()
|
||||
loadBackgroundsJob = scope.launch(Dispatchers.IO) {
|
||||
@@ -122,14 +136,24 @@ class BackgroundService(
|
||||
fun clearBackgrounds() {
|
||||
loadBackgroundsJob?.cancel()
|
||||
|
||||
// Re-enable backgrounds if disabled
|
||||
_enabled.value = true
|
||||
|
||||
if (_backgrounds.isEmpty()) return
|
||||
|
||||
_backgrounds = emptyList()
|
||||
update()
|
||||
}
|
||||
|
||||
/**
|
||||
* Disable the showing of backgrounds until any function manipulating the backgrounds is called.
|
||||
*/
|
||||
fun disable() {
|
||||
_enabled.value = false
|
||||
}
|
||||
|
||||
internal fun update() {
|
||||
val now = System.currentTimeMillis()
|
||||
val now = Instant.now().toEpochMilli()
|
||||
if (lastBackgroundTimerUpdate > now - TRANSITION_DURATION.inWholeMilliseconds)
|
||||
return setTimer((lastBackgroundTimerUpdate - now).milliseconds + TRANSITION_DURATION, false)
|
||||
|
||||
|
||||
@@ -21,7 +21,9 @@ import org.jellyfin.androidtv.data.repository.NotificationsRepositoryImpl
|
||||
import org.jellyfin.androidtv.data.repository.UserViewsRepository
|
||||
import org.jellyfin.androidtv.data.repository.UserViewsRepositoryImpl
|
||||
import org.jellyfin.androidtv.data.service.BackgroundService
|
||||
import org.jellyfin.androidtv.integration.dream.DreamViewModel
|
||||
import org.jellyfin.androidtv.ui.ScreensaverViewModel
|
||||
import org.jellyfin.androidtv.ui.itemhandling.ItemLauncher
|
||||
import org.jellyfin.androidtv.ui.navigation.Destinations
|
||||
import org.jellyfin.androidtv.ui.navigation.NavigationRepository
|
||||
import org.jellyfin.androidtv.ui.navigation.NavigationRepositoryImpl
|
||||
@@ -35,13 +37,16 @@ import org.jellyfin.androidtv.ui.search.SearchViewModel
|
||||
import org.jellyfin.androidtv.ui.startup.ServerAddViewModel
|
||||
import org.jellyfin.androidtv.ui.startup.StartupViewModel
|
||||
import org.jellyfin.androidtv.ui.startup.UserLoginViewModel
|
||||
import org.jellyfin.androidtv.util.KeyProcessor
|
||||
import org.jellyfin.androidtv.util.MarkdownRenderer
|
||||
import org.jellyfin.androidtv.util.PlaybackHelper
|
||||
import org.jellyfin.androidtv.util.apiclient.ReportingHelper
|
||||
import org.jellyfin.androidtv.util.sdk.SdkPlaybackHelper
|
||||
import org.jellyfin.androidtv.util.sdk.legacy
|
||||
import org.jellyfin.apiclient.AppInfo
|
||||
import org.jellyfin.apiclient.android
|
||||
import org.jellyfin.apiclient.logging.AndroidLogger
|
||||
import org.jellyfin.sdk.android.androidDevice
|
||||
import org.jellyfin.sdk.api.client.ApiClient
|
||||
import org.jellyfin.sdk.createJellyfin
|
||||
import org.jellyfin.sdk.model.ClientInfo
|
||||
import org.jellyfin.sdk.model.DeviceInfo
|
||||
@@ -76,9 +81,7 @@ val appModule = module {
|
||||
get<JellyfinSdk>().createApi()
|
||||
}
|
||||
|
||||
single { get<ApiClient>().ws() }
|
||||
|
||||
single { SocketHandler(get(), get(), get(), get(), get(), get(), get(), get()) }
|
||||
single { SocketHandler(get(), get(), get(), get(), get(), get(), get(), get(), get()) }
|
||||
|
||||
// Old apiclient
|
||||
single {
|
||||
@@ -112,7 +115,7 @@ val appModule = module {
|
||||
|
||||
single<UserRepository> { UserRepositoryImpl() }
|
||||
single<UserViewsRepository> { UserViewsRepositoryImpl(get()) }
|
||||
single<NotificationsRepository> { NotificationsRepositoryImpl(get(), get(), get()) }
|
||||
single<NotificationsRepository> { NotificationsRepositoryImpl(get(), get()) }
|
||||
single<ItemMutationRepository> { ItemMutationRepositoryImpl(get(), get()) }
|
||||
single<CustomMessageRepository> { CustomMessageRepositoryImpl() }
|
||||
single<NavigationRepository> { NavigationRepositoryImpl(Destinations.home) }
|
||||
@@ -125,10 +128,15 @@ val appModule = module {
|
||||
viewModel { PictureViewerViewModel(get()) }
|
||||
viewModel { ScreensaverViewModel(get()) }
|
||||
viewModel { SearchViewModel(get()) }
|
||||
viewModel { DreamViewModel(get(), get(), get(), get(), get()) }
|
||||
|
||||
single { BackgroundService(get(), get(), get(), get(), get()) }
|
||||
|
||||
single { MarkdownRenderer(get()) }
|
||||
single { ItemLauncher() }
|
||||
single { KeyProcessor() }
|
||||
single { ReportingHelper() }
|
||||
single<PlaybackHelper> { SdkPlaybackHelper(get(), get(), get(), get(), get(), get(), get()) }
|
||||
|
||||
factory { (context: Context) -> SearchFragmentDelegate(context, get()) }
|
||||
factory { (context: Context) -> SearchFragmentDelegate(context, get(), get()) }
|
||||
}
|
||||
|
||||
@@ -12,15 +12,17 @@ import org.jellyfin.androidtv.preference.UserPreferences
|
||||
import org.jellyfin.androidtv.preference.UserSettingPreferences
|
||||
import org.jellyfin.androidtv.ui.browsing.MainActivity
|
||||
import org.jellyfin.androidtv.ui.playback.GarbagePlaybackLauncher
|
||||
import org.jellyfin.androidtv.ui.playback.LegacyMediaManager
|
||||
import org.jellyfin.androidtv.ui.playback.MediaManager
|
||||
import org.jellyfin.androidtv.ui.playback.RewritePlaybackLauncher
|
||||
import org.jellyfin.androidtv.ui.playback.VideoQueueManager
|
||||
import org.jellyfin.androidtv.ui.playback.rewrite.RewriteMediaManager
|
||||
import org.jellyfin.playback.core.mediasession.MediaSessionOptions
|
||||
import org.jellyfin.playback.core.mediasession.mediaSessionPlugin
|
||||
import org.jellyfin.playback.core.playbackManager
|
||||
import org.jellyfin.playback.exoplayer.exoPlayerPlugin
|
||||
import org.jellyfin.playback.jellyfin.jellyfinPlugin
|
||||
import org.jellyfin.playback.media3.exoplayer.ExoPlayerOptions
|
||||
import org.jellyfin.playback.media3.exoplayer.exoPlayerPlugin
|
||||
import org.jellyfin.playback.media3.session.MediaSessionOptions
|
||||
import org.jellyfin.playback.media3.session.media3SessionPlugin
|
||||
import org.jellyfin.sdk.api.client.ApiClient
|
||||
import org.koin.android.ext.koin.androidContext
|
||||
import org.koin.core.scope.Scope
|
||||
import org.koin.dsl.module
|
||||
@@ -30,16 +32,7 @@ import org.jellyfin.androidtv.ui.playback.PlaybackManager as LegacyPlaybackManag
|
||||
val playbackModule = module {
|
||||
single { LegacyPlaybackManager(get()) }
|
||||
single { VideoQueueManager() }
|
||||
single { LegacyMediaManager(get()) }
|
||||
single { RewriteMediaManager(get(), get(), get(), get()) }
|
||||
|
||||
factory {
|
||||
val preferences = get<UserPreferences>()
|
||||
val useRewrite = preferences[UserPreferences.playbackRewriteAudioEnabled] && BuildConfig.DEVELOPMENT
|
||||
|
||||
if (useRewrite) get<RewriteMediaManager>()
|
||||
else get<LegacyMediaManager>()
|
||||
}
|
||||
single<MediaManager> { RewriteMediaManager(get(), get(), get(), get()) }
|
||||
|
||||
factory {
|
||||
val preferences = get<UserPreferences>()
|
||||
@@ -53,13 +46,10 @@ val playbackModule = module {
|
||||
}
|
||||
|
||||
fun Scope.createPlaybackManager() = playbackManager(androidContext()) {
|
||||
install(exoPlayerPlugin(get()))
|
||||
install(jellyfinPlugin(get(), get()))
|
||||
|
||||
val activityIntent = Intent(get(), MainActivity::class.java)
|
||||
val pendingIntent = PendingIntent.getActivity(get(), 0, activityIntent, PendingIntent.FLAG_IMMUTABLE)
|
||||
|
||||
val notificationChannelId = "mediasession"
|
||||
val notificationChannelId = "session"
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val channel = NotificationChannel(
|
||||
notificationChannelId,
|
||||
@@ -69,12 +59,22 @@ fun Scope.createPlaybackManager() = playbackManager(androidContext()) {
|
||||
NotificationManagerCompat.from(get()).createNotificationChannel(channel)
|
||||
}
|
||||
|
||||
install(mediaSessionPlugin(get(), MediaSessionOptions(
|
||||
val api = get<ApiClient>()
|
||||
val exoPlayerOptions = ExoPlayerOptions(
|
||||
httpConnectTimeout = api.httpClientOptions.connectTimeout,
|
||||
httpReadTimeout = api.httpClientOptions.requestTimeout
|
||||
)
|
||||
install(exoPlayerPlugin(get(), exoPlayerOptions))
|
||||
|
||||
val mediaSessionOptions = MediaSessionOptions(
|
||||
channelId = notificationChannelId,
|
||||
notificationId = 1,
|
||||
iconSmall = R.drawable.app_icon_foreground,
|
||||
openIntent = pendingIntent,
|
||||
)))
|
||||
)
|
||||
install(media3SessionPlugin(get(), mediaSessionOptions))
|
||||
|
||||
install(jellyfinPlugin(get()))
|
||||
|
||||
// Options
|
||||
val userSettingPreferences = get<UserSettingPreferences>()
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
package org.jellyfin.androidtv.di
|
||||
|
||||
import org.jellyfin.androidtv.util.AutoBitrate
|
||||
import org.jellyfin.androidtv.util.ImageHelper
|
||||
import org.koin.dsl.module
|
||||
|
||||
val utilsModule = module {
|
||||
single { AutoBitrate(get()) }
|
||||
single { ImageHelper(get()) }
|
||||
}
|
||||
|
||||
@@ -25,9 +25,10 @@ import org.jellyfin.androidtv.data.repository.UserViewsRepository
|
||||
import org.jellyfin.androidtv.integration.provider.ImageProvider
|
||||
import org.jellyfin.androidtv.preference.UserPreferences
|
||||
import org.jellyfin.androidtv.ui.startup.StartupActivity
|
||||
import org.jellyfin.androidtv.util.ImageUtils
|
||||
import org.jellyfin.androidtv.util.ImageHelper
|
||||
import org.jellyfin.androidtv.util.dp
|
||||
import org.jellyfin.androidtv.util.sdk.isUsable
|
||||
import org.jellyfin.androidtv.util.stripHtml
|
||||
import org.jellyfin.sdk.api.client.ApiClient
|
||||
import org.jellyfin.sdk.api.client.exception.ApiClientException
|
||||
import org.jellyfin.sdk.api.client.exception.TimeoutException
|
||||
@@ -41,11 +42,12 @@ import org.jellyfin.sdk.model.api.BaseItemKind
|
||||
import org.jellyfin.sdk.model.api.ImageFormat
|
||||
import org.jellyfin.sdk.model.api.ImageType
|
||||
import org.jellyfin.sdk.model.api.ItemFields
|
||||
import org.jellyfin.sdk.model.constant.MediaType
|
||||
import org.jellyfin.sdk.model.api.MediaType
|
||||
import org.jellyfin.sdk.model.extensions.ticks
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.inject
|
||||
import timber.log.Timber
|
||||
import java.time.Instant
|
||||
import java.time.ZoneOffset
|
||||
import java.time.format.DateTimeFormatter
|
||||
import kotlin.time.Duration
|
||||
@@ -67,6 +69,7 @@ class LeanbackChannelWorker(
|
||||
private val api by inject<ApiClient>()
|
||||
private val userPreferences by inject<UserPreferences>()
|
||||
private val userViewsRepository by inject<UserViewsRepository>()
|
||||
private val imageHelper by inject<ImageHelper>()
|
||||
|
||||
/**
|
||||
* Check if the app can use Leanback features and is API level 26 or higher.
|
||||
@@ -198,7 +201,7 @@ class LeanbackChannelWorker(
|
||||
}
|
||||
|
||||
// Update logo
|
||||
ResourcesCompat.getDrawable(context.resources, R.drawable.app_icon, context.theme)?.let {
|
||||
ResourcesCompat.getDrawable(context.resources, R.mipmap.app_icon, context.theme)?.let {
|
||||
ChannelLogoUtils.storeChannelLogo(
|
||||
context,
|
||||
ContentUris.parseId(uri),
|
||||
@@ -256,7 +259,7 @@ class LeanbackChannelWorker(
|
||||
tag = imageTags?.get(ImageType.PRIMARY),
|
||||
)
|
||||
|
||||
else -> ImageUtils.getResourceUrl(context, R.drawable.tile_land_tv)
|
||||
else -> imageHelper.getResourceUrl(context, R.drawable.tile_land_tv)
|
||||
}.let(ImageProvider::getImageUri)
|
||||
|
||||
/**
|
||||
@@ -271,7 +274,7 @@ class LeanbackChannelWorker(
|
||||
fields = listOf(ItemFields.DATE_CREATED),
|
||||
imageTypeLimit = 1,
|
||||
limit = 10,
|
||||
mediaTypes = listOf(MediaType.Video),
|
||||
mediaTypes = listOf(MediaType.VIDEO),
|
||||
includeItemTypes = listOf(BaseItemKind.EPISODE, BaseItemKind.MOVIE),
|
||||
excludeActiveSessions = true,
|
||||
).content.items.orEmpty()
|
||||
@@ -279,9 +282,9 @@ class LeanbackChannelWorker(
|
||||
|
||||
val nextUp = async {
|
||||
api.tvShowsApi.getNextUp(
|
||||
userId = api.userId,
|
||||
imageTypeLimit = 1,
|
||||
limit = 10,
|
||||
enableResumable = false,
|
||||
fields = listOf(ItemFields.DATE_CREATED),
|
||||
).content.items.orEmpty()
|
||||
}
|
||||
@@ -361,7 +364,7 @@ class LeanbackChannelWorker(
|
||||
.setEpisodeTitle(if (item.type == BaseItemKind.EPISODE) item.name else null)
|
||||
.setSeasonNumber(seasonString, item.parentIndexNumber ?: 0)
|
||||
.setEpisodeNumber(episodeString, item.indexNumber ?: 0)
|
||||
.setDescription(item.overview)
|
||||
.setDescription(item.overview?.stripHtml())
|
||||
.setReleaseDate(
|
||||
if (item.premiereDate != null) DateTimeFormatter.ISO_DATE.format(item.premiereDate)
|
||||
else null
|
||||
@@ -455,7 +458,7 @@ class LeanbackChannelWorker(
|
||||
|
||||
setLastEngagementTimeUtcMillis(
|
||||
engagement?.toInstant(ZoneOffset.UTC)?.toEpochMilli()
|
||||
?: System.currentTimeMillis()
|
||||
?: Instant.now().toEpochMilli()
|
||||
)
|
||||
|
||||
// Episode runtime has been determined
|
||||
|
||||
@@ -13,7 +13,7 @@ import kotlinx.coroutines.runBlocking
|
||||
import org.jellyfin.androidtv.BuildConfig
|
||||
import org.jellyfin.androidtv.R
|
||||
import org.jellyfin.androidtv.integration.provider.ImageProvider
|
||||
import org.jellyfin.androidtv.util.ImageUtils
|
||||
import org.jellyfin.androidtv.util.ImageHelper
|
||||
import org.jellyfin.androidtv.util.sdk.isUsable
|
||||
import org.jellyfin.sdk.api.client.ApiClient
|
||||
import org.jellyfin.sdk.api.client.exception.ApiClientException
|
||||
@@ -41,6 +41,7 @@ class MediaContentProvider : ContentProvider(), KoinComponent {
|
||||
}
|
||||
|
||||
private val api by inject<ApiClient>()
|
||||
private val imageHelper by inject<ImageHelper>()
|
||||
|
||||
override fun onCreate(): Boolean = api.isUsable
|
||||
|
||||
@@ -70,7 +71,7 @@ class MediaContentProvider : ContentProvider(), KoinComponent {
|
||||
* Gets the resumable items or returns null
|
||||
*/
|
||||
private suspend fun searchItems(query: String, limit: Int): BaseItemDtoQueryResult? = try {
|
||||
val items by api.itemsApi.getItemsByUserId(
|
||||
val items by api.itemsApi.getItems(
|
||||
searchTerm = query,
|
||||
recursive = true,
|
||||
limit = limit,
|
||||
@@ -106,7 +107,7 @@ class MediaContentProvider : ContentProvider(), KoinComponent {
|
||||
val imageUri = if (item.imageTags?.contains(ImageType.PRIMARY) == true)
|
||||
ImageProvider.getImageUri(api.imageApi.getItemImageUrl(item.id, ImageType.PRIMARY))
|
||||
else
|
||||
ImageUtils.getResourceUrl(context, R.drawable.tile_land_tv)
|
||||
imageHelper.getResourceUrl(context!!, R.drawable.tile_land_tv)
|
||||
|
||||
cursor.newRow().apply {
|
||||
add(BaseColumns._ID, item.id)
|
||||
|
||||
@@ -4,15 +4,19 @@ import android.service.dreams.DreamService
|
||||
import androidx.annotation.CallSuper
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.platform.ComposeView
|
||||
import androidx.compose.ui.platform.ViewCompositionStrategy
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleRegistry
|
||||
import androidx.lifecycle.ViewModelStore
|
||||
import androidx.lifecycle.ViewModelStoreOwner
|
||||
import androidx.lifecycle.setViewTreeLifecycleOwner
|
||||
import androidx.lifecycle.setViewTreeViewModelStoreOwner
|
||||
import androidx.savedstate.SavedStateRegistry
|
||||
import androidx.savedstate.SavedStateRegistryController
|
||||
import androidx.savedstate.SavedStateRegistryOwner
|
||||
import androidx.savedstate.setViewTreeSavedStateRegistryOwner
|
||||
|
||||
abstract class DreamServiceCompat : DreamService(), SavedStateRegistryOwner {
|
||||
abstract class DreamServiceCompat : DreamService(), SavedStateRegistryOwner, ViewModelStoreOwner {
|
||||
@Suppress("LeakingThis")
|
||||
private val lifecycleRegistry = LifecycleRegistry(this)
|
||||
|
||||
@@ -22,6 +26,7 @@ abstract class DreamServiceCompat : DreamService(), SavedStateRegistryOwner {
|
||||
}
|
||||
|
||||
override val lifecycle: Lifecycle get() = lifecycleRegistry
|
||||
override val viewModelStore = ViewModelStore()
|
||||
override val savedStateRegistry: SavedStateRegistry get() = savedStateRegistryController.savedStateRegistry
|
||||
|
||||
@CallSuper
|
||||
@@ -46,9 +51,12 @@ abstract class DreamServiceCompat : DreamService(), SavedStateRegistryOwner {
|
||||
|
||||
fun setContent(content: @Composable () -> Unit) {
|
||||
val view = ComposeView(this)
|
||||
// Set composition strategy
|
||||
view.setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
|
||||
|
||||
// Inject dependencies normally added by appcompat activities
|
||||
view.setViewTreeLifecycleOwner(this)
|
||||
view.setViewTreeViewModelStoreOwner(this)
|
||||
view.setViewTreeSavedStateRegistryOwner(this)
|
||||
|
||||
// Set content composable
|
||||
|
||||
@@ -0,0 +1,168 @@
|
||||
package org.jellyfin.androidtv.integration.dream
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import androidx.core.graphics.drawable.toBitmap
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import coil.ImageLoader
|
||||
import coil.request.ImageRequest
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.channels.awaitClose
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.callbackFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.jellyfin.androidtv.integration.dream.model.DreamContent
|
||||
import org.jellyfin.androidtv.preference.UserPreferences
|
||||
import org.jellyfin.androidtv.ui.playback.AudioEventListener
|
||||
import org.jellyfin.androidtv.ui.playback.MediaManager
|
||||
import org.jellyfin.androidtv.ui.playback.PlaybackController
|
||||
import org.jellyfin.sdk.api.client.ApiClient
|
||||
import org.jellyfin.sdk.api.client.exception.ApiClientException
|
||||
import org.jellyfin.sdk.api.client.extensions.imageApi
|
||||
import org.jellyfin.sdk.api.client.extensions.itemsApi
|
||||
import org.jellyfin.sdk.model.api.BaseItemDto
|
||||
import org.jellyfin.sdk.model.api.BaseItemKind
|
||||
import org.jellyfin.sdk.model.api.ImageFormat
|
||||
import org.jellyfin.sdk.model.api.ImageType
|
||||
import org.jellyfin.sdk.model.api.ItemSortBy
|
||||
import timber.log.Timber
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
class DreamViewModel(
|
||||
private val api: ApiClient,
|
||||
private val imageLoader: ImageLoader,
|
||||
private val context: Context,
|
||||
private val mediaManager: MediaManager,
|
||||
private val userPreferences: UserPreferences,
|
||||
) : ViewModel() {
|
||||
private val _mediaContent = callbackFlow {
|
||||
trySend(mediaManager.currentAudioItem)
|
||||
|
||||
val listener = object : AudioEventListener {
|
||||
override fun onPlaybackStateChange(
|
||||
newState: PlaybackController.PlaybackState,
|
||||
currentItem: BaseItemDto?
|
||||
) {
|
||||
trySend(currentItem)
|
||||
}
|
||||
|
||||
override fun onQueueStatusChanged(hasQueue: Boolean) {
|
||||
trySend(mediaManager.currentAudioItem)
|
||||
}
|
||||
}
|
||||
|
||||
mediaManager.addAudioEventListener(listener)
|
||||
awaitClose { mediaManager.removeAudioEventListener(listener) }
|
||||
}
|
||||
.distinctUntilChanged()
|
||||
.map { it?.let(DreamContent::NowPlaying) }
|
||||
.stateIn(
|
||||
viewModelScope,
|
||||
SharingStarted.WhileSubscribed(),
|
||||
DreamContent.NowPlaying(mediaManager.currentAudioItem)
|
||||
)
|
||||
|
||||
private val _libraryContent = flow {
|
||||
// Load first library item after 2 seconds
|
||||
// to force the logo at the start of the screensaver
|
||||
emit(null)
|
||||
delay(2.seconds)
|
||||
|
||||
while (true) {
|
||||
val next = getRandomLibraryShowcase()
|
||||
if (next != null) {
|
||||
emit(next)
|
||||
delay(30.seconds)
|
||||
} else {
|
||||
delay(3.seconds)
|
||||
}
|
||||
}
|
||||
}
|
||||
.distinctUntilChanged()
|
||||
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
|
||||
|
||||
val content = combine(_mediaContent, _libraryContent) { mediaContent, libraryContent ->
|
||||
mediaContent ?: libraryContent ?: DreamContent.Logo
|
||||
}.stateIn(
|
||||
scope = viewModelScope,
|
||||
started = SharingStarted.WhileSubscribed(),
|
||||
initialValue = _mediaContent.value ?: _libraryContent.value ?: DreamContent.Logo,
|
||||
)
|
||||
|
||||
private suspend fun getRandomLibraryShowcase(): DreamContent.LibraryShowcase? {
|
||||
val requireParentalRating = userPreferences[UserPreferences.screensaverAgeRatingRequired]
|
||||
val maxParentalRating = userPreferences[UserPreferences.screensaverAgeRatingMax]
|
||||
|
||||
try {
|
||||
val response by api.itemsApi.getItems(
|
||||
includeItemTypes = listOf(BaseItemKind.MOVIE, BaseItemKind.SERIES),
|
||||
recursive = true,
|
||||
sortBy = listOf(ItemSortBy.RANDOM),
|
||||
limit = 5,
|
||||
imageTypes = listOf(ImageType.BACKDROP),
|
||||
maxOfficialRating = if (maxParentalRating == -1) null else maxParentalRating.toString(),
|
||||
hasParentalRating = if (requireParentalRating) true else null,
|
||||
)
|
||||
|
||||
val item = response.items?.firstOrNull { item ->
|
||||
!item.backdropImageTags.isNullOrEmpty()
|
||||
} ?: return null
|
||||
|
||||
Timber.i("Loading random library showcase item ${item.id}")
|
||||
|
||||
val backdropTag = item.backdropImageTags!!.randomOrNull()
|
||||
?: item.imageTags?.get(ImageType.BACKDROP)
|
||||
|
||||
val logoTag = item.imageTags?.get(ImageType.LOGO)
|
||||
|
||||
val backdropUrl = api.imageApi.getItemImageUrl(
|
||||
itemId = item.id,
|
||||
imageType = ImageType.BACKDROP,
|
||||
tag = backdropTag,
|
||||
format = ImageFormat.WEBP,
|
||||
)
|
||||
|
||||
val logoUrl = api.imageApi.getItemImageUrl(
|
||||
itemId = item.id,
|
||||
imageType = ImageType.LOGO,
|
||||
tag = logoTag,
|
||||
format = ImageFormat.WEBP,
|
||||
)
|
||||
|
||||
val (logo, backdrop) = withContext(Dispatchers.IO) {
|
||||
val logoDeferred = async {
|
||||
imageLoader.execute(
|
||||
request = ImageRequest.Builder(context).data(logoUrl).build()
|
||||
).drawable?.toBitmap()
|
||||
}
|
||||
|
||||
val backdropDeferred = async {
|
||||
imageLoader.execute(
|
||||
request = ImageRequest.Builder(context).data(backdropUrl).build()
|
||||
).drawable?.toBitmap()
|
||||
}
|
||||
|
||||
awaitAll(logoDeferred, backdropDeferred)
|
||||
}
|
||||
|
||||
if (backdrop == null) {
|
||||
return null
|
||||
}
|
||||
|
||||
return DreamContent.LibraryShowcase(item, backdrop, logo)
|
||||
} catch (err: ApiClientException) {
|
||||
Timber.e(err)
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
@@ -12,7 +12,9 @@ import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.asImageBitmap
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.tv.material3.Text
|
||||
import org.jellyfin.androidtv.integration.dream.model.DreamContent
|
||||
import org.jellyfin.androidtv.ui.composable.ZoomBox
|
||||
import org.jellyfin.androidtv.ui.composable.overscan
|
||||
@@ -47,12 +49,21 @@ fun DreamContentLibraryShowcase(
|
||||
.align(Alignment.BottomStart)
|
||||
.overscan(),
|
||||
) {
|
||||
Text(
|
||||
text = content.item.name.orEmpty(),
|
||||
style = TextStyle(
|
||||
color = Color.White,
|
||||
fontSize = 32.sp
|
||||
),
|
||||
)
|
||||
if (content.logo != null) {
|
||||
Image(
|
||||
bitmap = content.logo.asImageBitmap(),
|
||||
contentDescription = content.item.name,
|
||||
modifier = Modifier
|
||||
.height(75.dp)
|
||||
)
|
||||
} else {
|
||||
Text(
|
||||
text = content.item.name.orEmpty(),
|
||||
style = TextStyle(
|
||||
color = Color.White,
|
||||
fontSize = 32.sp
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package org.jellyfin.androidtv.integration.dream.composable
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -26,5 +27,6 @@ fun DreamContentLogo() = Box(
|
||||
modifier = Modifier
|
||||
.align(Alignment.Center)
|
||||
.width(400.dp)
|
||||
.fillMaxHeight()
|
||||
)
|
||||
}
|
||||
|
||||
@@ -13,8 +13,6 @@ import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.LinearProgressIndicator
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
@@ -24,13 +22,14 @@ import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.draw.drawWithContent
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.StrokeCap
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.unit.IntSize
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.tv.material3.Text
|
||||
import org.jellyfin.androidtv.integration.dream.model.DreamContent
|
||||
import org.jellyfin.androidtv.ui.composable.AsyncImage
|
||||
import org.jellyfin.androidtv.ui.composable.blurHashPainter
|
||||
@@ -42,7 +41,7 @@ import org.jellyfin.sdk.api.client.extensions.imageApi
|
||||
import org.jellyfin.sdk.model.api.ImageFormat
|
||||
import org.jellyfin.sdk.model.api.ImageType
|
||||
import org.jellyfin.sdk.model.extensions.ticks
|
||||
import org.koin.compose.rememberKoinInject
|
||||
import org.koin.compose.koinInject
|
||||
import kotlin.time.Duration
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
|
||||
@@ -52,8 +51,8 @@ fun DreamContentNowPlaying(
|
||||
) = Box(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
) {
|
||||
val api = rememberKoinInject<ApiClient>()
|
||||
val mediaManager = rememberKoinInject<MediaManager>()
|
||||
val api = koinInject<ApiClient>()
|
||||
val mediaManager = koinInject<MediaManager>()
|
||||
val item = content.item ?: return@Box
|
||||
|
||||
val primaryImageTag = item.imageTags?.get(ImageType.PRIMARY)
|
||||
@@ -63,7 +62,8 @@ fun DreamContentNowPlaying(
|
||||
else -> null to null
|
||||
}
|
||||
|
||||
val imageBlurHash = imageTag?.let { tag -> item.imageBlurHashes?.get(ImageType.PRIMARY)?.get(tag) }
|
||||
val imageBlurHash =
|
||||
imageTag?.let { tag -> item.imageBlurHashes?.get(ImageType.PRIMARY)?.get(tag) }
|
||||
if (imageBlurHash != null) {
|
||||
Image(
|
||||
painter = blurHashPainter(imageBlurHash, IntSize(32, 32)),
|
||||
@@ -114,11 +114,15 @@ fun DreamContentNowPlaying(
|
||||
|
||||
Text(
|
||||
text = item.run {
|
||||
if (!artists.isNullOrEmpty()) return@run artists?.joinToString(", ")
|
||||
val albumArtistNames = albumArtists?.mapNotNull { it.name }
|
||||
if (!albumArtistNames.isNullOrEmpty()) return@run albumArtistNames.joinToString(", ")
|
||||
return@run albumArtist
|
||||
}.orEmpty(),
|
||||
val artistNames = artists.orEmpty()
|
||||
val albumArtistNames = albumArtists?.mapNotNull { it.name }.orEmpty()
|
||||
|
||||
when {
|
||||
artistNames.isNotEmpty() -> artistNames
|
||||
albumArtistNames.isNotEmpty() -> albumArtistNames
|
||||
else -> listOfNotNull(albumArtist)
|
||||
}.joinToString(", ")
|
||||
},
|
||||
style = TextStyle(
|
||||
color = Color(0.8f, 0.8f, 0.8f),
|
||||
fontSize = 18.sp,
|
||||
@@ -143,12 +147,17 @@ fun DreamContentNowPlaying(
|
||||
}
|
||||
}
|
||||
|
||||
LinearProgressIndicator(
|
||||
progress = progress,
|
||||
color = Color.White,
|
||||
backgroundColor = Color.White.copy(alpha = 0.2f),
|
||||
strokeCap = StrokeCap.Round,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(4.dp)
|
||||
.clip(RoundedCornerShape(2.dp))
|
||||
.drawWithContent {
|
||||
// Background
|
||||
drawRect(Color.White, alpha = 0.2f)
|
||||
// Foreground
|
||||
drawRect(Color.White, size = size.copy(width = size.width * progress))
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,6 @@ import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
@@ -20,6 +19,7 @@ import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.tv.material3.Text
|
||||
import org.jellyfin.androidtv.R
|
||||
import org.jellyfin.androidtv.ui.composable.overscan
|
||||
import org.jellyfin.androidtv.ui.composable.rememberCurrentTime
|
||||
|
||||
@@ -1,108 +1,25 @@
|
||||
package org.jellyfin.androidtv.integration.dream.composable
|
||||
|
||||
import android.content.Context
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.core.graphics.drawable.toBitmap
|
||||
import coil.ImageLoader
|
||||
import coil.request.ImageRequest
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.jellyfin.androidtv.integration.dream.model.DreamContent
|
||||
import org.jellyfin.androidtv.integration.dream.DreamViewModel
|
||||
import org.jellyfin.androidtv.preference.UserPreferences
|
||||
import org.jellyfin.androidtv.preference.constant.ClockBehavior
|
||||
import org.jellyfin.androidtv.ui.composable.rememberMediaItem
|
||||
import org.jellyfin.androidtv.ui.playback.MediaManager
|
||||
import org.jellyfin.sdk.api.client.ApiClient
|
||||
import org.jellyfin.sdk.api.client.exception.ApiClientException
|
||||
import org.jellyfin.sdk.api.client.extensions.imageApi
|
||||
import org.jellyfin.sdk.api.client.extensions.itemsApi
|
||||
import org.jellyfin.sdk.model.api.BaseItemKind
|
||||
import org.jellyfin.sdk.model.api.ImageFormat
|
||||
import org.jellyfin.sdk.model.api.ImageType
|
||||
import org.jellyfin.sdk.model.constant.ItemSortBy
|
||||
import org.koin.compose.rememberKoinInject
|
||||
import timber.log.Timber
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
import org.koin.androidx.compose.koinViewModel
|
||||
import org.koin.compose.koinInject
|
||||
|
||||
@Composable
|
||||
fun DreamHost() {
|
||||
val api = rememberKoinInject<ApiClient>()
|
||||
val userPreferences = rememberKoinInject<UserPreferences>()
|
||||
val mediaManager = rememberKoinInject<MediaManager>()
|
||||
val imageLoader = rememberKoinInject<ImageLoader>()
|
||||
val context = LocalContext.current
|
||||
|
||||
var libraryShowcase by remember { mutableStateOf<DreamContent.LibraryShowcase?>(null) }
|
||||
val mediaItem by rememberMediaItem(mediaManager)
|
||||
|
||||
LaunchedEffect(true) {
|
||||
delay(2.seconds)
|
||||
|
||||
while (true) {
|
||||
libraryShowcase = getRandomLibraryShowcase(api, imageLoader, context)
|
||||
delay(30.seconds)
|
||||
}
|
||||
}
|
||||
val viewModel = koinViewModel<DreamViewModel>()
|
||||
val userPreferences = koinInject<UserPreferences>()
|
||||
val content by viewModel.content.collectAsState()
|
||||
|
||||
DreamView(
|
||||
content = when {
|
||||
mediaItem != null -> DreamContent.NowPlaying(mediaItem)
|
||||
libraryShowcase != null -> libraryShowcase!!
|
||||
else -> DreamContent.Logo
|
||||
},
|
||||
content = content,
|
||||
showClock = when (userPreferences[UserPreferences.clockBehavior]) {
|
||||
ClockBehavior.ALWAYS, ClockBehavior.IN_MENUS -> true
|
||||
else -> false
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun getRandomLibraryShowcase(
|
||||
api: ApiClient,
|
||||
imageLoader: ImageLoader,
|
||||
context: Context
|
||||
): DreamContent.LibraryShowcase? {
|
||||
try {
|
||||
val response by api.itemsApi.getItemsByUserId(
|
||||
includeItemTypes = listOf(BaseItemKind.MOVIE, BaseItemKind.SERIES),
|
||||
recursive = true,
|
||||
sortBy = listOf(ItemSortBy.Random),
|
||||
limit = 5,
|
||||
imageTypes = listOf(ImageType.BACKDROP),
|
||||
)
|
||||
|
||||
val item = response.items?.firstOrNull { item ->
|
||||
!item.backdropImageTags.isNullOrEmpty()
|
||||
} ?: return null
|
||||
|
||||
Timber.i("Loading random library showcase item ${item.id}")
|
||||
|
||||
val tag = item.backdropImageTags!!.randomOrNull()
|
||||
?: item.imageTags?.get(ImageType.BACKDROP)
|
||||
|
||||
val backdropUrl = api.imageApi.getItemImageUrl(
|
||||
itemId = item.id,
|
||||
imageType = ImageType.BACKDROP,
|
||||
tag = tag,
|
||||
format = ImageFormat.WEBP,
|
||||
)
|
||||
|
||||
val backdrop = withContext(Dispatchers.IO) {
|
||||
imageLoader.execute(
|
||||
request = ImageRequest.Builder(context).data(backdropUrl).build()
|
||||
).drawable?.toBitmap()
|
||||
} ?: return null
|
||||
|
||||
return DreamContent.LibraryShowcase(item, backdrop)
|
||||
} catch (err: ApiClientException) {
|
||||
Timber.e(err)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package org.jellyfin.androidtv.integration.dream.composable
|
||||
|
||||
import androidx.compose.animation.AnimatedContent
|
||||
import androidx.compose.animation.ExperimentalAnimationApi
|
||||
import androidx.compose.animation.core.snap
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.animation.fadeIn
|
||||
@@ -13,7 +12,6 @@ import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import org.jellyfin.androidtv.integration.dream.model.DreamContent
|
||||
|
||||
@OptIn(ExperimentalAnimationApi::class)
|
||||
@Composable
|
||||
fun DreamView(
|
||||
content: DreamContent,
|
||||
|
||||
@@ -5,6 +5,6 @@ import org.jellyfin.sdk.model.api.BaseItemDto
|
||||
|
||||
sealed interface DreamContent {
|
||||
data object Logo : DreamContent
|
||||
data class LibraryShowcase(val item: BaseItemDto, val backdrop: Bitmap) : DreamContent
|
||||
data class LibraryShowcase(val item: BaseItemDto, val backdrop: Bitmap, val logo: Bitmap?) : DreamContent
|
||||
data class NowPlaying(val item: BaseItemDto?) : DreamContent
|
||||
}
|
||||
|
||||
@@ -6,10 +6,9 @@ import org.jellyfin.androidtv.constant.PosterSize
|
||||
import org.jellyfin.androidtv.preference.store.DisplayPreferencesStore
|
||||
import org.jellyfin.preference.booleanPreference
|
||||
import org.jellyfin.preference.enumPreference
|
||||
import org.jellyfin.preference.stringPreference
|
||||
import org.jellyfin.sdk.api.client.ApiClient
|
||||
import org.jellyfin.sdk.model.api.ItemSortBy
|
||||
import org.jellyfin.sdk.model.api.SortOrder
|
||||
import org.jellyfin.sdk.model.constant.ItemSortBy
|
||||
|
||||
class LibraryPreferences(
|
||||
displayPreferencesId: String,
|
||||
@@ -29,7 +28,7 @@ class LibraryPreferences(
|
||||
val filterUnwatchedOnly = booleanPreference("FilterUnwatchedOnly", false)
|
||||
|
||||
// Item sorting
|
||||
val sortBy = stringPreference("SortBy", ItemSortBy.SortName)
|
||||
val sortBy = enumPreference("SortBy", ItemSortBy.SORT_NAME)
|
||||
val sortOrder = enumPreference("SortOrder", SortOrder.ASCENDING)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import org.jellyfin.androidtv.preference.store.DisplayPreferencesStore
|
||||
import org.jellyfin.preference.booleanPreference
|
||||
import org.jellyfin.preference.stringPreference
|
||||
import org.jellyfin.sdk.api.client.ApiClient
|
||||
import org.jellyfin.sdk.model.constant.ItemSortBy
|
||||
import org.jellyfin.sdk.model.api.ItemSortBy
|
||||
|
||||
class LiveTvPreferences(
|
||||
api: ApiClient,
|
||||
@@ -13,7 +13,7 @@ class LiveTvPreferences(
|
||||
api = api,
|
||||
) {
|
||||
companion object {
|
||||
val channelOrder = stringPreference("livetv-channelorder", ItemSortBy.DatePlayed)
|
||||
val channelOrder = stringPreference("livetv-channelorder", ItemSortBy.DATE_PLAYED.name)
|
||||
val colorCodeGuide = booleanPreference("guide-colorcodedbackgrounds", false)
|
||||
val favsAtTop = booleanPreference("livetv-favoritechannelsattop", true)
|
||||
val showHDIndicator = booleanPreference("guide-indicator-hd", false)
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
package org.jellyfin.androidtv.preference
|
||||
|
||||
import android.content.Context
|
||||
import org.jellyfin.androidtv.preference.constant.PreferredVideoPlayer
|
||||
import org.jellyfin.preference.booleanPreference
|
||||
import org.jellyfin.preference.enumPreference
|
||||
import org.jellyfin.preference.store.SharedPreferenceStore
|
||||
import org.jellyfin.preference.stringPreference
|
||||
|
||||
@@ -60,11 +58,6 @@ class SystemPreferences(context: Context) : SharedPreferenceStore(
|
||||
val liveTvGuideFilterSports = booleanPreference("guide_filter_sports", false)
|
||||
|
||||
// Other persistent variables
|
||||
/**
|
||||
* Chosen player for play with button. Changes every time user chooses a player with "play with" button.
|
||||
*/
|
||||
var chosenPlayer = enumPreference("chosen_player", PreferredVideoPlayer.VLC)
|
||||
|
||||
/**
|
||||
* The version name for the latest dismissed beta notification or empty if none.
|
||||
*/
|
||||
|
||||
@@ -4,14 +4,12 @@ import android.content.Context
|
||||
import android.view.KeyEvent
|
||||
import androidx.preference.PreferenceManager
|
||||
import org.jellyfin.androidtv.preference.constant.AppTheme
|
||||
import org.jellyfin.androidtv.preference.constant.AudioBehavior
|
||||
import org.jellyfin.androidtv.preference.constant.ClockBehavior
|
||||
import org.jellyfin.androidtv.preference.constant.NextUpBehavior
|
||||
import org.jellyfin.androidtv.preference.constant.PreferredVideoPlayer
|
||||
import org.jellyfin.androidtv.preference.constant.RatingType
|
||||
import org.jellyfin.androidtv.preference.constant.RefreshRateSwitchingBehavior
|
||||
import org.jellyfin.androidtv.preference.constant.WatchedIndicatorBehavior
|
||||
import org.jellyfin.androidtv.preference.constant.defaultAudioBehavior
|
||||
import org.jellyfin.androidtv.util.DeviceUtils
|
||||
import org.jellyfin.preference.booleanPreference
|
||||
import org.jellyfin.preference.enumPreference
|
||||
import org.jellyfin.preference.intPreference
|
||||
@@ -30,11 +28,6 @@ class UserPreferences(context: Context) : SharedPreferenceStore(
|
||||
sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
) {
|
||||
companion object {
|
||||
/**
|
||||
* The value used for automatic detection in [maxBitrate].
|
||||
*/
|
||||
const val MAX_BITRATE_AUTO = "0"
|
||||
|
||||
/* Display */
|
||||
/**
|
||||
* Select the app theme
|
||||
@@ -58,8 +51,7 @@ class UserPreferences(context: Context) : SharedPreferenceStore(
|
||||
|
||||
/* Playback - General*/
|
||||
/**
|
||||
* Maximum bitrate in megabit for playback. A value of [MAX_BITRATE_AUTO] is used when
|
||||
* the bitrate should be automatically detected.
|
||||
* Maximum bitrate in megabit for playback.
|
||||
*/
|
||||
var maxBitrate = stringPreference("pref_max_bitrate", "100")
|
||||
|
||||
@@ -91,9 +83,9 @@ class UserPreferences(context: Context) : SharedPreferenceStore(
|
||||
|
||||
/* Playback - Video */
|
||||
/**
|
||||
* Preferred video player.
|
||||
* Whether to use an external playback application or not.
|
||||
*/
|
||||
var videoPlayer = enumPreference("video_player", PreferredVideoPlayer.EXOPLAYER)
|
||||
var useExternalPlayer = booleanPreference("external_player", false)
|
||||
|
||||
/**
|
||||
* Change refresh rate to match media when device supports it
|
||||
@@ -104,32 +96,17 @@ class UserPreferences(context: Context) : SharedPreferenceStore(
|
||||
/**
|
||||
* Preferred behavior for audio streaming.
|
||||
*/
|
||||
var audioBehaviour = enumPreference("audio_behavior", defaultAudioBehavior)
|
||||
var audioBehaviour = enumPreference("audio_behavior", AudioBehavior.DIRECT_STREAM)
|
||||
|
||||
/**
|
||||
* Preferred behavior for audio streaming.
|
||||
*/
|
||||
var audioNightMode = enumPreference("audio_night_mode", false)
|
||||
|
||||
/**
|
||||
* Enable DTS
|
||||
*/
|
||||
var dtsEnabled = booleanPreference("pref_bitstream_dts", false)
|
||||
|
||||
/**
|
||||
* Enable AC3
|
||||
*/
|
||||
var ac3Enabled = booleanPreference("pref_bitstream_ac3", !DeviceUtils.isFireTvStickGen1)
|
||||
|
||||
/**
|
||||
* Default audio delay in milliseconds for libVLC
|
||||
*/
|
||||
var libVLCAudioDelay = intPreference("libvlc_audio_delay", 0)
|
||||
|
||||
/**
|
||||
* Use playback rewrite module for audio
|
||||
*/
|
||||
var playbackRewriteAudioEnabled = booleanPreference("playback_new_audio", true)
|
||||
var ac3Enabled = booleanPreference("pref_bitstream_ac3", true)
|
||||
|
||||
/* Live TV */
|
||||
/**
|
||||
@@ -137,11 +114,6 @@ class UserPreferences(context: Context) : SharedPreferenceStore(
|
||||
*/
|
||||
var liveTvDirectPlayEnabled = booleanPreference("pref_live_direct", true)
|
||||
|
||||
/**
|
||||
* Preferred video player for live TV
|
||||
*/
|
||||
var liveTvVideoPlayer = enumPreference("live_tv_video_player", PreferredVideoPlayer.AUTO)
|
||||
|
||||
/**
|
||||
* Shortcut used for changing the audio track
|
||||
*/
|
||||
@@ -219,9 +191,14 @@ class UserPreferences(context: Context) : SharedPreferenceStore(
|
||||
var screensaverInAppTimeout = longPreference("screensaver_inapp_timeout", 5.minutes.inWholeMilliseconds)
|
||||
|
||||
/**
|
||||
* Enable reactive homepage
|
||||
* Age rating used to filter items in the screensaver. Use -1 to disable (omits parameter from requests).
|
||||
*/
|
||||
var homeReactive = booleanPreference("home_reactive", false)
|
||||
var screensaverAgeRatingMax = intPreference("screensaver_agerating_max", 13)
|
||||
|
||||
/**
|
||||
* Whether items shown in the screensaver are required to have an age rating set.
|
||||
*/
|
||||
var screensaverAgeRatingRequired = booleanPreference("screensaver_agerating_required", true)
|
||||
}
|
||||
|
||||
init {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package org.jellyfin.androidtv.preference.constant
|
||||
|
||||
import org.jellyfin.androidtv.R
|
||||
import org.jellyfin.androidtv.util.DeviceUtils
|
||||
import org.jellyfin.preference.PreferenceEnum
|
||||
|
||||
enum class AudioBehavior(
|
||||
@@ -17,6 +16,3 @@ enum class AudioBehavior(
|
||||
*/
|
||||
DOWNMIX_TO_STEREO(R.string.pref_audio_compat),
|
||||
}
|
||||
|
||||
val defaultAudioBehavior = if (DeviceUtils.isChromecastWithGoogleTV) AudioBehavior.DOWNMIX_TO_STEREO
|
||||
else AudioBehavior.DIRECT_STREAM
|
||||
|
||||
@@ -2,17 +2,17 @@ package org.jellyfin.androidtv.preference.constant
|
||||
|
||||
import org.jellyfin.androidtv.R
|
||||
import org.jellyfin.preference.PreferenceEnum
|
||||
import org.jellyfin.sdk.model.constant.ItemSortBy
|
||||
import org.jellyfin.sdk.model.api.ItemSortBy
|
||||
|
||||
enum class LiveTvChannelOrder(
|
||||
override val nameRes: Int,
|
||||
val stringValue: String,
|
||||
) : PreferenceEnum {
|
||||
LAST_PLAYED(R.string.lbl_guide_option_played, ItemSortBy.DatePlayed),
|
||||
CHANNEL_NUMBER(R.string.lbl_guide_option_number, ItemSortBy.SortName);
|
||||
LAST_PLAYED(R.string.lbl_guide_option_played, ItemSortBy.DATE_PLAYED.serialName),
|
||||
CHANNEL_NUMBER(R.string.lbl_guide_option_number, ItemSortBy.SORT_NAME.serialName);
|
||||
|
||||
companion object {
|
||||
fun fromString(value: String) = values()
|
||||
fun fromString(value: String) = entries
|
||||
.firstOrNull { it.stringValue.equals(value, ignoreCase = true) }
|
||||
?: LAST_PLAYED
|
||||
}
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
package org.jellyfin.androidtv.preference.constant
|
||||
|
||||
import org.jellyfin.androidtv.R
|
||||
import org.jellyfin.preference.PreferenceEnum
|
||||
|
||||
enum class PreferredVideoPlayer(
|
||||
override val nameRes: Int,
|
||||
) : PreferenceEnum {
|
||||
/**
|
||||
* Automatically selects between exoplayer and vlc
|
||||
*/
|
||||
AUTO(R.string.pref_video_player_auto),
|
||||
|
||||
/**
|
||||
* Force ExoPlayer
|
||||
*/
|
||||
EXOPLAYER(R.string.pref_video_player_exoplayer),
|
||||
|
||||
/**
|
||||
* Force libVLC
|
||||
*/
|
||||
VLC(R.string.pref_video_player_vlc),
|
||||
|
||||
/**
|
||||
* Use external player
|
||||
*/
|
||||
EXTERNAL(R.string.pref_video_player_external),
|
||||
|
||||
/**
|
||||
* Choose a player - play with button
|
||||
*/
|
||||
CHOOSE(R.string.pref_video_player_choose),
|
||||
}
|
||||
@@ -19,7 +19,7 @@ enum class WatchedIndicatorBehavior(
|
||||
/**
|
||||
* Hide unwatched count indicator, show watched check mark on individual episodes only.
|
||||
*/
|
||||
EPISODES_ONLY(R.string.lbl_episode_marks),
|
||||
EPISODES_ONLY(R.string.lbl_hide_watched_checkmark),
|
||||
|
||||
/**
|
||||
* Never show watched indicators.
|
||||
|
||||
@@ -119,8 +119,8 @@ abstract class DisplayPreferencesStore(
|
||||
override fun <V : Enum<V>> setEnum(preference: Preference<*>, value: Enum<V>) =
|
||||
setString(
|
||||
preference.key, when (value) {
|
||||
is PreferenceEnum -> value.serializedName ?: value.toString()
|
||||
else -> value.toString()
|
||||
is PreferenceEnum -> value.serializedName ?: value.name
|
||||
else -> value.name
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -12,11 +12,11 @@ import androidx.lifecycle.lifecycleScope
|
||||
import coil.ImageLoader
|
||||
import coil.request.ImageRequest
|
||||
import coil.transform.CircleCropTransformation
|
||||
import com.vanniktech.blurhash.BlurHash
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.jellyfin.androidtv.R
|
||||
import org.jellyfin.androidtv.util.BlurHashDecoder
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.inject
|
||||
import kotlin.math.round
|
||||
@@ -65,7 +65,7 @@ class AsyncImageView @JvmOverloads constructor(
|
||||
|
||||
// Only show blurhash if an image is going to be loaded from the network
|
||||
if (url != null && blurHash != null) withContext(Dispatchers.IO) {
|
||||
val blurHashBitmap = BlurHash.decode(
|
||||
val blurHashBitmap = BlurHashDecoder.decode(
|
||||
blurHash,
|
||||
if (aspectRatio > 1) round(blurHashResolution * aspectRatio).toInt() else blurHashResolution,
|
||||
if (aspectRatio >= 1) blurHashResolution else round(blurHashResolution / aspectRatio).toInt(),
|
||||
@@ -82,7 +82,10 @@ class AsyncImageView @JvmOverloads constructor(
|
||||
}.build())
|
||||
} else {
|
||||
imageLoader.enqueue(ImageRequest.Builder(context).apply {
|
||||
crossfade(crossFadeDuration.inWholeMilliseconds.toInt())
|
||||
val crossFadeDurationMs = crossFadeDuration.inWholeMilliseconds.toInt()
|
||||
if (crossFadeDurationMs > 0) crossfade(crossFadeDurationMs)
|
||||
else crossfade(false)
|
||||
|
||||
target(this@AsyncImageView)
|
||||
data(url)
|
||||
placeholder(placeholderOrBlurHash)
|
||||
|
||||
@@ -11,7 +11,9 @@ import org.jellyfin.androidtv.auth.repository.UserRepository
|
||||
import org.jellyfin.androidtv.databinding.ClockUserBugBinding
|
||||
import org.jellyfin.androidtv.preference.UserPreferences
|
||||
import org.jellyfin.androidtv.preference.constant.ClockBehavior
|
||||
import org.jellyfin.androidtv.util.ImageUtils
|
||||
import org.jellyfin.androidtv.ui.navigation.Destinations
|
||||
import org.jellyfin.androidtv.ui.navigation.NavigationRepository
|
||||
import org.jellyfin.androidtv.util.ImageHelper
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.inject
|
||||
|
||||
@@ -24,6 +26,8 @@ class ClockUserView @JvmOverloads constructor(
|
||||
private val binding: ClockUserBugBinding = ClockUserBugBinding.inflate(LayoutInflater.from(context), this, true)
|
||||
private val userPreferences by inject<UserPreferences>()
|
||||
private val userRepository by inject<UserRepository>()
|
||||
private val navigationRepository by inject<NavigationRepository>()
|
||||
private val imageHelper by inject<ImageHelper>()
|
||||
|
||||
var isVideoPlayer = false
|
||||
set(value) {
|
||||
@@ -31,17 +35,23 @@ class ClockUserView @JvmOverloads constructor(
|
||||
updateClockVisibility()
|
||||
}
|
||||
|
||||
val homeButton get() = binding.home
|
||||
|
||||
init {
|
||||
updateClockVisibility()
|
||||
|
||||
val currentUser = userRepository.currentUser.value
|
||||
|
||||
binding.clockUserImage.load(
|
||||
url = currentUser?.let(ImageUtils::getPrimaryImageUrl),
|
||||
url = currentUser?.let(imageHelper::getPrimaryImageUrl),
|
||||
placeholder = ContextCompat.getDrawable(context, R.drawable.ic_user)
|
||||
)
|
||||
|
||||
binding.clockUserImage.isVisible = currentUser != null
|
||||
|
||||
binding.home.setOnClickListener {
|
||||
navigationRepository.reset(Destinations.home, clearHistory = true)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateClockVisibility() {
|
||||
@@ -53,5 +63,7 @@ class ClockUserView @JvmOverloads constructor(
|
||||
ClockBehavior.IN_VIDEO -> isVideoPlayer
|
||||
ClockBehavior.IN_MENUS -> !isVideoPlayer
|
||||
}
|
||||
|
||||
binding.home.isVisible = !isVideoPlayer
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,14 +8,15 @@ import android.widget.FrameLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.jellyfin.androidtv.R;
|
||||
import org.jellyfin.androidtv.util.DateTimeExtensionsKt;
|
||||
import org.jellyfin.androidtv.util.TimeUtils;
|
||||
|
||||
import java.util.Date;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
public class FriendlyDateButton extends FrameLayout {
|
||||
private long dateVal;
|
||||
private LocalDateTime dateVal;
|
||||
|
||||
public FriendlyDateButton(Context context, long thisDate, OnClickListener listener) {
|
||||
public FriendlyDateButton(Context context, LocalDateTime thisDate, OnClickListener listener) {
|
||||
super(context);
|
||||
|
||||
LayoutInflater inflater = LayoutInflater.from(context);
|
||||
@@ -25,14 +26,13 @@ public class FriendlyDateButton extends FrameLayout {
|
||||
setOnClickListener(listener);
|
||||
|
||||
dateVal = thisDate;
|
||||
Date date = new Date(thisDate);
|
||||
|
||||
((TextView)v.findViewById(R.id.friendlyName)).setText(TimeUtils.getFriendlyDate(context, date, true));
|
||||
((TextView)v.findViewById(R.id.date)).setText(android.text.format.DateFormat.getDateFormat(context).format(date));
|
||||
((TextView)v.findViewById(R.id.friendlyName)).setText(TimeUtils.getFriendlyDate(context, thisDate, true));
|
||||
((TextView)v.findViewById(R.id.date)).setText(DateTimeExtensionsKt.getDateFormatter(context).format(thisDate));
|
||||
|
||||
}
|
||||
|
||||
public long getDate() { return dateVal; }
|
||||
public LocalDateTime getDate() { return dateVal; }
|
||||
|
||||
@Override
|
||||
protected void onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect) {
|
||||
|
||||
@@ -14,25 +14,24 @@ import androidx.core.content.ContextCompat;
|
||||
import org.jellyfin.androidtv.R;
|
||||
import org.jellyfin.androidtv.ui.livetv.LiveTvGuide;
|
||||
import org.jellyfin.androidtv.ui.livetv.LiveTvGuideFragment;
|
||||
import org.jellyfin.androidtv.util.ImageUtils;
|
||||
import org.jellyfin.androidtv.util.ImageHelper;
|
||||
import org.jellyfin.androidtv.util.Utils;
|
||||
import org.jellyfin.apiclient.interaction.ApiClient;
|
||||
import org.jellyfin.apiclient.model.livetv.ChannelInfoDto;
|
||||
import org.jellyfin.sdk.model.api.BaseItemDto;
|
||||
import org.koin.java.KoinJavaComponent;
|
||||
|
||||
public class GuideChannelHeader extends RelativeLayout {
|
||||
private AsyncImageView mChannelImage;
|
||||
private ImageView mFavImage;
|
||||
private ChannelInfoDto mChannel;
|
||||
private BaseItemDto mChannel;
|
||||
private Context mContext;
|
||||
private LiveTvGuide mTvGuide;
|
||||
|
||||
public GuideChannelHeader(Context context, LiveTvGuide tvGuide, ChannelInfoDto channel) {
|
||||
public GuideChannelHeader(Context context, LiveTvGuide tvGuide, BaseItemDto channel) {
|
||||
super(context);
|
||||
initComponent(context, tvGuide, channel);
|
||||
}
|
||||
|
||||
private void initComponent(Context context, LiveTvGuide tvGuide, ChannelInfoDto channel) {
|
||||
private void initComponent(Context context, LiveTvGuide tvGuide, BaseItemDto channel) {
|
||||
mContext = context;
|
||||
mChannel = channel;
|
||||
mTvGuide = tvGuide;
|
||||
@@ -50,13 +49,14 @@ public class GuideChannelHeader extends RelativeLayout {
|
||||
mChannelImage = findViewById(R.id.channelImage);
|
||||
mFavImage = findViewById(R.id.favImage);
|
||||
|
||||
if (mChannel.getUserData() != null && mChannel.getUserData().getIsFavorite())
|
||||
if (mChannel.getUserData() != null && mChannel.getUserData().isFavorite())
|
||||
mFavImage.setVisibility(View.VISIBLE);
|
||||
}
|
||||
|
||||
public void loadImage() {
|
||||
ImageHelper imageHelper = KoinJavaComponent.<ImageHelper>get(ImageHelper.class);
|
||||
mChannelImage.load(
|
||||
ImageUtils.getPrimaryImageUrl(mChannel, KoinJavaComponent.<ApiClient>get(ApiClient.class)),
|
||||
imageHelper.getPrimaryImageUrl(mChannel,null, ImageHelper.MAX_PRIMARY_IMAGE_HEIGHT),
|
||||
null,
|
||||
null,
|
||||
0.0,
|
||||
@@ -64,10 +64,11 @@ public class GuideChannelHeader extends RelativeLayout {
|
||||
);
|
||||
}
|
||||
|
||||
public ChannelInfoDto getChannel() { return mChannel; }
|
||||
public BaseItemDto getChannel() { return mChannel; }
|
||||
public void setChannel(BaseItemDto channel) { mChannel = channel; }
|
||||
|
||||
public void refreshFavorite() {
|
||||
if (mChannel.getUserData() != null && mChannel.getUserData().getIsFavorite())
|
||||
if (mChannel.getUserData() != null && mChannel.getUserData().isFavorite())
|
||||
mFavImage.setVisibility(View.VISIBLE);
|
||||
else
|
||||
mFavImage.setVisibility(View.GONE);
|
||||
@@ -85,5 +86,4 @@ public class GuideChannelHeader extends RelativeLayout {
|
||||
setBackground(ContextCompat.getDrawable(mContext, R.drawable.light_border));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -46,19 +46,12 @@ public class ItemListView extends FrameLayout {
|
||||
mItemIds.clear();
|
||||
}
|
||||
|
||||
public void addItems(List<BaseItemDto> items) {
|
||||
int i = 0;
|
||||
for (BaseItemDto item : items) {
|
||||
addItem(item, i++);
|
||||
}
|
||||
}
|
||||
|
||||
public void addItem(BaseItemDto item, int ndx) {
|
||||
mList.addView(new ItemRowView(mContext, item, ndx, mRowSelectedListener, mRowClickedListener));
|
||||
mItemIds.add(item.getId());
|
||||
}
|
||||
|
||||
public ItemRowView updatePlaying(String id) {
|
||||
public ItemRowView updatePlaying(UUID id) {
|
||||
//look through our song rows and update the playing indicator
|
||||
ItemRowView ret = null;
|
||||
for (int i = 0; i < mList.getChildCount(); i++) {
|
||||
|
||||
@@ -12,7 +12,7 @@ fun ItemListView.refresh() {
|
||||
val api by KoinJavaComponent.inject<ApiClient>(ApiClient::class.java)
|
||||
|
||||
findViewTreeLifecycleOwner()?.lifecycleScope?.launch {
|
||||
val response by api.itemsApi.getItemsByUserId(
|
||||
val response by api.itemsApi.getItems(
|
||||
ids = mItemIds,
|
||||
fields = setOf(
|
||||
ItemFields.PRIMARY_IMAGE_ASPECT_RATIO,
|
||||
|
||||
@@ -16,7 +16,9 @@ import org.jellyfin.androidtv.databinding.ItemRowBinding;
|
||||
import org.jellyfin.androidtv.util.TimeUtils;
|
||||
import org.jellyfin.androidtv.util.sdk.BaseItemExtensionsKt;
|
||||
import org.jellyfin.sdk.model.api.BaseItemDto;
|
||||
import org.jellyfin.sdk.model.constant.MediaType;
|
||||
import org.jellyfin.sdk.model.api.MediaType;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
public class ItemRowView extends FrameLayout {
|
||||
Context mContext;
|
||||
@@ -118,7 +120,7 @@ public class ItemRowView extends FrameLayout {
|
||||
|
||||
public void updateWatched() {
|
||||
if (mBaseItem == null) return;
|
||||
if (MediaType.Video.equals(mBaseItem.getMediaType()) && mBaseItem.getUserData() != null && mBaseItem.getUserData().getPlayed()) {
|
||||
if (MediaType.VIDEO.equals(mBaseItem.getMediaType()) && mBaseItem.getUserData() != null && mBaseItem.getUserData().getPlayed()) {
|
||||
mWatchedMark.setText("✓");
|
||||
} else {
|
||||
mWatchedMark.setText("");
|
||||
@@ -149,7 +151,7 @@ public class ItemRowView extends FrameLayout {
|
||||
return playing;
|
||||
}
|
||||
|
||||
public boolean setPlaying(String id) {
|
||||
public boolean setPlaying(UUID id) {
|
||||
return setPlaying(getItem().getId().equals(id));
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
package org.jellyfin.androidtv.ui;
|
||||
|
||||
import static org.koin.java.KoinJavaComponent.inject;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.AlertDialog;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
@@ -18,36 +15,28 @@ import android.widget.TextView;
|
||||
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.lifecycle.Lifecycle;
|
||||
import androidx.lifecycle.LifecycleOwner;
|
||||
import androidx.lifecycle.ViewTreeLifecycleOwner;
|
||||
|
||||
import org.jellyfin.androidtv.R;
|
||||
import org.jellyfin.androidtv.auth.repository.UserRepository;
|
||||
import org.jellyfin.androidtv.data.model.DataRefreshService;
|
||||
import org.jellyfin.androidtv.ui.livetv.LiveTvGuide;
|
||||
import org.jellyfin.androidtv.ui.livetv.TvManager;
|
||||
import org.jellyfin.androidtv.util.InfoLayoutHelper;
|
||||
import org.jellyfin.androidtv.util.TimeUtils;
|
||||
import org.jellyfin.androidtv.util.Utils;
|
||||
import org.jellyfin.androidtv.util.apiclient.EmptyLifecycleAwareResponse;
|
||||
import org.jellyfin.androidtv.util.sdk.compat.ModelCompat;
|
||||
import org.jellyfin.apiclient.interaction.ApiClient;
|
||||
import org.jellyfin.apiclient.interaction.Response;
|
||||
import org.jellyfin.apiclient.model.dto.BaseItemDto;
|
||||
import org.jellyfin.apiclient.model.dto.UserItemDataDto;
|
||||
import org.jellyfin.apiclient.model.livetv.ChannelInfoDto;
|
||||
import org.jellyfin.apiclient.model.livetv.SeriesTimerInfoDto;
|
||||
import org.jellyfin.sdk.model.api.BaseItemDto;
|
||||
import org.koin.java.KoinJavaComponent;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
import kotlin.Lazy;
|
||||
import timber.log.Timber;
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
public class LiveProgramDetailPopup {
|
||||
private PopupWindow mPopup;
|
||||
private BaseItemDto mProgram;
|
||||
private ProgramGridCell mSelectedProgramView;
|
||||
private Activity mActivity;
|
||||
private final Lifecycle lifecycle;
|
||||
final Context mContext;
|
||||
final Lifecycle lifecycle;
|
||||
private LiveTvGuide mTvGuide;
|
||||
private TextView mDTitle;
|
||||
private TextView mDSummary;
|
||||
@@ -65,17 +54,15 @@ public class LiveProgramDetailPopup {
|
||||
private int mPosLeft;
|
||||
private int mPosTop;
|
||||
|
||||
private Lazy<ApiClient> apiClient = inject(ApiClient.class);
|
||||
|
||||
public LiveProgramDetailPopup(Activity activity, Lifecycle lifecycle, LiveTvGuide tvGuide, int width, EmptyLifecycleAwareResponse tuneAction) {
|
||||
mActivity = activity;
|
||||
this.lifecycle = lifecycle;
|
||||
public LiveProgramDetailPopup(Context context, LifecycleOwner lifecycleOwner, LiveTvGuide tvGuide, int width, EmptyLifecycleAwareResponse tuneAction) {
|
||||
mContext = context;
|
||||
lifecycle = lifecycleOwner.getLifecycle();
|
||||
mTvGuide = tvGuide;
|
||||
mTuneAction = tuneAction;
|
||||
LayoutInflater inflater = (LayoutInflater) mActivity.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
|
||||
View layout = inflater.inflate(R.layout.program_detail_popup, null);
|
||||
int popupHeight = Utils.convertDpToPixel(activity, 400);
|
||||
View layout = LayoutInflater.from(context).inflate(R.layout.program_detail_popup, null);
|
||||
int popupHeight = Utils.convertDpToPixel(context, 400);
|
||||
mPopup = new PopupWindow(layout, width, popupHeight);
|
||||
ViewTreeLifecycleOwner.set(mPopup.getContentView(), lifecycleOwner);
|
||||
mPopup.setFocusable(true);
|
||||
mPopup.setOutsideTouchable(true);
|
||||
mPopup.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT)); // necessary for popup to dismiss
|
||||
@@ -96,216 +83,127 @@ public class LiveProgramDetailPopup {
|
||||
public void setContent(final BaseItemDto program, final ProgramGridCell selectedGridView) {
|
||||
mProgram = program;
|
||||
mSelectedProgramView = selectedGridView;
|
||||
mDTitle.setText(program.getName());
|
||||
mDTitle.setText(mProgram.getName());
|
||||
mDButtonRow.removeAllViews();
|
||||
if (program.getId() == null) {
|
||||
//empty item, just offer tune button
|
||||
mFirstButton = createTuneButton();
|
||||
createFavoriteButton();
|
||||
mDInfoRow.removeAllViews();
|
||||
mDTimeline.removeAllViews();
|
||||
mDSummary.setText("");
|
||||
return;
|
||||
}
|
||||
|
||||
mDSummary.setText(program.getOverview());
|
||||
mDSummary.setText(mProgram.getOverview());
|
||||
if (mDSummary.getLineCount() < 2) {
|
||||
mDSummary.setGravity(Gravity.CENTER);
|
||||
} else {
|
||||
mDSummary.setGravity(Gravity.LEFT);
|
||||
mDSummary.setGravity(Gravity.START);
|
||||
}
|
||||
|
||||
// build timeline info
|
||||
TvManager.setTimelineRow(mActivity, mDTimeline, program);
|
||||
TvManager.setTimelineRow(mContext, mDTimeline, mProgram);
|
||||
|
||||
//info row
|
||||
InfoLayoutHelper.addInfoRow(mActivity, ModelCompat.asSdk(program), mDInfoRow, false, false);
|
||||
// TODO: Requires LifecycleOwner for compose
|
||||
// InfoLayoutHelper.addInfoRow(mContext, mProgram, mDInfoRow, false);
|
||||
|
||||
//buttons
|
||||
mFirstButton = null;
|
||||
Date now = new Date();
|
||||
Date local = TimeUtils.convertToLocalDate(program.getStartDate());
|
||||
if (TimeUtils.convertToLocalDate(program.getEndDate()).getTime() > now.getTime()) {
|
||||
if (local.getTime() <= now.getTime()) {
|
||||
if (mProgram.getEndDate().isAfter(LocalDateTime.now())) {
|
||||
if (mProgram.getStartDate().isBefore(LocalDateTime.now())) {
|
||||
// program in progress - tune first button
|
||||
mFirstButton = createTuneButton();
|
||||
}
|
||||
|
||||
if (Utils.canManageRecordings(KoinJavaComponent.<UserRepository>get(UserRepository.class).getCurrentUser().getValue())) {
|
||||
if (program.getTimerId() != null) {
|
||||
if (mProgram.getTimerId() != null) {
|
||||
// cancel button
|
||||
Button cancel = new Button(mActivity);
|
||||
cancel.setText(mActivity.getResources().getString(R.string.lbl_cancel_recording));
|
||||
Button cancel = new Button(mContext);
|
||||
cancel.setText(mContext.getResources().getString(R.string.lbl_cancel_recording));
|
||||
cancel.setTextColor(Color.WHITE);
|
||||
cancel.setBackgroundResource(R.drawable.jellyfin_button);
|
||||
cancel.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
apiClient.getValue().CancelLiveTvTimerAsync(program.getTimerId(), new EmptyLifecycleAwareResponse(lifecycle) {
|
||||
@Override
|
||||
public void onResponse() {
|
||||
if (!getActive()) return;
|
||||
|
||||
selectedGridView.setRecTimer(null);
|
||||
program.setTimerId(null);
|
||||
dismiss();
|
||||
Utils.showToast(mActivity, R.string.msg_recording_cancelled);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(Exception ex) {
|
||||
if (!getActive()) return;
|
||||
|
||||
Utils.showToast(mActivity, R.string.msg_unable_to_cancel);
|
||||
}
|
||||
LiveProgramDetailPopupHelperKt.cancelTimer(LiveProgramDetailPopup.this, mProgram.getTimerId(), () -> {
|
||||
selectedGridView.setRecTimer(null);
|
||||
mProgram = LiveProgramDetailPopupHelperKt.copyWithTimerId(mProgram, null);
|
||||
dismiss();
|
||||
Utils.showToast(mContext, R.string.msg_recording_cancelled);
|
||||
return null;
|
||||
});
|
||||
}
|
||||
});
|
||||
mDButtonRow.addView(cancel);
|
||||
if (mFirstButton == null) mFirstButton = cancel;
|
||||
// recording info
|
||||
mDRecordInfo.setText(local.getTime() <= now.getTime() ? mActivity.getResources().getString(R.string.msg_recording_now) : mActivity.getResources().getString(R.string.msg_will_record));
|
||||
mDRecordInfo.setText(mProgram.getStartDate().isBefore(LocalDateTime.now()) ? mContext.getResources().getString(R.string.msg_recording_now) : mContext.getResources().getString(R.string.msg_will_record));
|
||||
} else {
|
||||
// record button
|
||||
Button rec = new Button(mActivity);
|
||||
rec.setText(mActivity.getResources().getString(R.string.lbl_record));
|
||||
Button rec = new Button(mContext);
|
||||
rec.setText(mContext.getResources().getString(R.string.lbl_record));
|
||||
rec.setTextColor(Color.WHITE);
|
||||
rec.setBackgroundResource(R.drawable.jellyfin_button);
|
||||
rec.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
//Create one-off recording with defaults
|
||||
apiClient.getValue().GetDefaultLiveTvTimerInfo(mProgram.getId(), new Response<SeriesTimerInfoDto>() {
|
||||
@Override
|
||||
public void onResponse(SeriesTimerInfoDto response) {
|
||||
apiClient.getValue().CreateLiveTvTimerAsync(response, new EmptyLifecycleAwareResponse(lifecycle) {
|
||||
@Override
|
||||
public void onResponse() {
|
||||
if (!getActive()) return;
|
||||
|
||||
// we have to re-retrieve the program to get the timer id
|
||||
apiClient.getValue().GetLiveTvProgramAsync(mProgram.getId(), KoinJavaComponent.<UserRepository>get(UserRepository.class).getCurrentUser().getValue().getId().toString(), new Response<BaseItemDto>() {
|
||||
@Override
|
||||
public void onResponse(BaseItemDto response) {
|
||||
mProgram = response;
|
||||
mSelectedProgramView.setRecSeriesTimer(response.getSeriesTimerId());
|
||||
mSelectedProgramView.setRecTimer(response.getTimerId());
|
||||
Utils.showToast(mActivity, R.string.msg_set_to_record);
|
||||
dismiss();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(Exception ex) {
|
||||
if (!getActive()) return;
|
||||
|
||||
Timber.e(ex, "Error creating recording");
|
||||
Utils.showToast(mActivity, R.string.msg_unable_to_create_recording);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(Exception exception) {
|
||||
Timber.e(exception, "Error creating recording");
|
||||
Utils.showToast(mActivity, R.string.msg_unable_to_create_recording);
|
||||
}
|
||||
LiveProgramDetailPopupHelperKt.recordProgram(LiveProgramDetailPopup.this, mProgram.getId(), program -> {
|
||||
mProgram = program;
|
||||
mSelectedProgramView.setRecSeriesTimer(program.getSeriesTimerId());
|
||||
mSelectedProgramView.setRecTimer(program.getTimerId());
|
||||
if (mSeriesSettingsButton != null)
|
||||
mSeriesSettingsButton.setVisibility(View.VISIBLE);
|
||||
Utils.showToast(mContext, R.string.msg_set_to_record);
|
||||
dismiss();
|
||||
return null;
|
||||
});
|
||||
}
|
||||
});
|
||||
mDButtonRow.addView(rec);
|
||||
if (mFirstButton == null) mFirstButton = rec;
|
||||
mDRecordInfo.setText(program.getSeriesTimerId() == null ? "" : mActivity.getString(R.string.lbl_episode_not_record));
|
||||
mDRecordInfo.setText(mProgram.getSeriesTimerId() == null ? "" : mContext.getString(R.string.lbl_episode_not_record));
|
||||
}
|
||||
if (Utils.isTrue(program.getIsSeries())) {
|
||||
if (program.getSeriesTimerId() != null) {
|
||||
if (Utils.isTrue(mProgram.isSeries())) {
|
||||
if (mProgram.getSeriesTimerId() != null) {
|
||||
// cancel series button
|
||||
Button cancel = new Button(mActivity);
|
||||
cancel.setText(mActivity.getResources().getString(R.string.lbl_cancel_series));
|
||||
Button cancel = new Button(mContext);
|
||||
cancel.setText(mContext.getResources().getString(R.string.lbl_cancel_series));
|
||||
cancel.setTextColor(Color.WHITE);
|
||||
cancel.setBackgroundResource(R.drawable.jellyfin_button);
|
||||
cancel.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
new AlertDialog.Builder(mActivity)
|
||||
.setTitle(mActivity.getResources().getString(R.string.lbl_cancel_series))
|
||||
.setMessage(mActivity.getResources().getString(R.string.msg_cancel_entire_series))
|
||||
new AlertDialog.Builder(mContext)
|
||||
.setTitle(mContext.getResources().getString(R.string.lbl_cancel_series))
|
||||
.setMessage(mContext.getResources().getString(R.string.msg_cancel_entire_series))
|
||||
.setNegativeButton(R.string.lbl_no, null)
|
||||
.setPositiveButton(R.string.lbl_yes, new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
apiClient.getValue().CancelLiveTvSeriesTimerAsync(program.getSeriesTimerId(), new EmptyLifecycleAwareResponse(lifecycle) {
|
||||
@Override
|
||||
public void onResponse() {
|
||||
if (!getActive()) return;
|
||||
|
||||
selectedGridView.setRecSeriesTimer(null);
|
||||
program.setSeriesTimerId(null);
|
||||
mSeriesSettingsButton.setVisibility(View.GONE);
|
||||
dismiss();
|
||||
Utils.showToast(mActivity, R.string.msg_recording_cancelled);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(Exception ex) {
|
||||
if (!getActive()) return;
|
||||
|
||||
Utils.showToast(mActivity, R.string.msg_unable_to_cancel);
|
||||
}
|
||||
LiveProgramDetailPopupHelperKt.cancelSeriesTimer(LiveProgramDetailPopup.this, mProgram.getSeriesTimerId(), () -> {
|
||||
selectedGridView.setRecSeriesTimer(null);
|
||||
mProgram = LiveProgramDetailPopupHelperKt.copyWithSeriesTimerId(mProgram, null);
|
||||
mSeriesSettingsButton.setVisibility(View.GONE);
|
||||
dismiss();
|
||||
Utils.showToast(mContext, R.string.msg_recording_cancelled);
|
||||
return null;
|
||||
});
|
||||
}
|
||||
}).show();
|
||||
}
|
||||
});
|
||||
mDButtonRow.addView(cancel);
|
||||
}else {
|
||||
} else {
|
||||
// record series button
|
||||
Button rec = new Button(mActivity);
|
||||
rec.setText(mActivity.getResources().getString(R.string.lbl_record_series));
|
||||
Button rec = new Button(mContext);
|
||||
rec.setText(mContext.getResources().getString(R.string.lbl_record_series));
|
||||
rec.setTextColor(Color.WHITE);
|
||||
rec.setBackgroundResource(R.drawable.jellyfin_button);
|
||||
rec.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
//Create series recording with defaults
|
||||
apiClient.getValue().GetDefaultLiveTvTimerInfo(mProgram.getId(), new Response<SeriesTimerInfoDto>() {
|
||||
@Override
|
||||
public void onResponse(SeriesTimerInfoDto response) {
|
||||
apiClient.getValue().CreateLiveTvSeriesTimerAsync(response, new EmptyLifecycleAwareResponse(lifecycle) {
|
||||
@Override
|
||||
public void onResponse() {
|
||||
if (!getActive()) return;
|
||||
|
||||
// we have to re-retrieve the program to get the timer id
|
||||
apiClient.getValue().GetLiveTvProgramAsync(mProgram.getId(), KoinJavaComponent.<UserRepository>get(UserRepository.class).getCurrentUser().getValue().getId().toString(), new Response<BaseItemDto>() {
|
||||
@Override
|
||||
public void onResponse(BaseItemDto response) {
|
||||
mProgram = response;
|
||||
mSelectedProgramView.setRecSeriesTimer(response.getSeriesTimerId());
|
||||
mSelectedProgramView.setRecTimer(response.getTimerId());
|
||||
if (mSeriesSettingsButton != null) mSeriesSettingsButton.setVisibility(View.VISIBLE);
|
||||
Utils.showToast(mActivity, R.string.msg_set_to_record);
|
||||
dismiss();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(Exception ex) {
|
||||
if (!getActive()) return;
|
||||
|
||||
Timber.e(ex, "Error creating recording");
|
||||
Utils.showToast(mActivity, R.string.msg_unable_to_create_recording);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(Exception exception) {
|
||||
Timber.e(exception, "Error creating recording");
|
||||
Utils.showToast(mActivity, R.string.msg_unable_to_create_recording);
|
||||
}
|
||||
LiveProgramDetailPopupHelperKt.recordSeries(LiveProgramDetailPopup.this, mProgram.getId(), program -> {
|
||||
mProgram = program;
|
||||
mSelectedProgramView.setRecSeriesTimer(program.getSeriesTimerId());
|
||||
mSelectedProgramView.setRecTimer(program.getTimerId());
|
||||
if (mSeriesSettingsButton != null)
|
||||
mSeriesSettingsButton.setVisibility(View.VISIBLE);
|
||||
Utils.showToast(mContext, R.string.msg_set_to_record);
|
||||
dismiss();
|
||||
return null;
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -313,8 +211,8 @@ public class LiveProgramDetailPopup {
|
||||
}
|
||||
|
||||
// manage series button
|
||||
mSeriesSettingsButton = new Button(mActivity);
|
||||
mSeriesSettingsButton.setText(mActivity.getResources().getString(R.string.lbl_series_settings));
|
||||
mSeriesSettingsButton = new Button(mContext);
|
||||
mSeriesSettingsButton.setText(mContext.getResources().getString(R.string.lbl_series_settings));
|
||||
mSeriesSettingsButton.setTextColor(Color.WHITE);
|
||||
mSeriesSettingsButton.setBackgroundResource(R.drawable.jellyfin_button);
|
||||
mSeriesSettingsButton.setVisibility(mProgram.getSeriesTimerId() != null ? View.VISIBLE : View.GONE);
|
||||
@@ -330,27 +228,18 @@ public class LiveProgramDetailPopup {
|
||||
|
||||
}
|
||||
|
||||
if (local.getTime() > now.getTime()) {
|
||||
if (mProgram.getStartDate().isAfter(LocalDateTime.now())) {
|
||||
// add tune to button for programs that haven't started yet
|
||||
createTuneButton();
|
||||
}
|
||||
|
||||
createFavoriteButton();
|
||||
|
||||
|
||||
} else {
|
||||
// program has already ended
|
||||
mDRecordInfo.setText(mActivity.getResources().getString(R.string.lbl_program_ended));
|
||||
mDRecordInfo.setText(mContext.getResources().getString(R.string.lbl_program_ended));
|
||||
mFirstButton = createTuneButton();
|
||||
}
|
||||
// if (program.getIsMovie()) {
|
||||
// mDSimilarRow.setVisibility(View.VISIBLE);
|
||||
// mPopup.setHeight(MOVIE_HEIGHT);
|
||||
// } else {
|
||||
mDSimilarRow.setVisibility(View.GONE);
|
||||
// mPopup.setHeight(NORMAL_HEIGHT);
|
||||
//
|
||||
// }
|
||||
}
|
||||
|
||||
public Button createTuneButton() {
|
||||
@@ -367,22 +256,19 @@ public class LiveProgramDetailPopup {
|
||||
}
|
||||
|
||||
public android.widget.ImageButton createFavoriteButton() {
|
||||
ChannelInfoDto channel = TvManager.getChannel(TvManager.getAllChannelsIndex(mProgram.getChannelId()));
|
||||
boolean isFav = channel.getUserData() != null && channel.getUserData().getIsFavorite();
|
||||
BaseItemDto channel = TvManager.getChannel(TvManager.getAllChannelsIndex(mProgram.getChannelId()));
|
||||
boolean isFav = channel.getUserData() != null && channel.getUserData().isFavorite();
|
||||
|
||||
android.widget.ImageButton fave = addImgButton(mDButtonRow, isFav ? R.drawable.ic_heart_red : R.drawable.ic_heart);
|
||||
fave.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
apiClient.getValue().UpdateFavoriteStatusAsync(channel.getId(), KoinJavaComponent.<UserRepository>get(UserRepository.class).getCurrentUser().getValue().getId().toString(), !channel.getUserData().getIsFavorite(), new Response<UserItemDataDto>() {
|
||||
@Override
|
||||
public void onResponse(UserItemDataDto response) {
|
||||
channel.setUserData(response);
|
||||
fave.setImageDrawable(ContextCompat.getDrawable(mActivity, response.getIsFavorite() ? R.drawable.ic_heart_red : R.drawable.ic_heart));
|
||||
mTvGuide.refreshFavorite(channel.getId());
|
||||
DataRefreshService dataRefreshService = KoinJavaComponent.<DataRefreshService>get(DataRefreshService.class);
|
||||
dataRefreshService.setLastFavoriteUpdate(System.currentTimeMillis());
|
||||
}
|
||||
LiveProgramDetailPopupHelperKt.toggleFavorite(LiveProgramDetailPopup.this, channel, channel -> {
|
||||
fave.setImageDrawable(ContextCompat.getDrawable(mContext, channel.getUserData().isFavorite() ? R.drawable.ic_heart_red : R.drawable.ic_heart));
|
||||
mTvGuide.refreshFavorite(channel.getId());
|
||||
DataRefreshService dataRefreshService = KoinJavaComponent.<DataRefreshService>get(DataRefreshService.class);
|
||||
dataRefreshService.setLastFavoriteUpdate(Instant.now());
|
||||
return null;
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -391,16 +277,16 @@ public class LiveProgramDetailPopup {
|
||||
}
|
||||
|
||||
private android.widget.ImageButton addImgButton(LinearLayout layout, int imgResource) {
|
||||
android.widget.ImageButton btn = new android.widget.ImageButton(mActivity);
|
||||
btn.setImageDrawable(ContextCompat.getDrawable(mActivity, imgResource));
|
||||
android.widget.ImageButton btn = new android.widget.ImageButton(mContext);
|
||||
btn.setImageDrawable(ContextCompat.getDrawable(mContext, imgResource));
|
||||
btn.setBackgroundResource(R.drawable.jellyfin_button);
|
||||
layout.addView(btn);
|
||||
return btn;
|
||||
}
|
||||
|
||||
private Button addButton(LinearLayout layout, int stringResource) {
|
||||
Button btn = new Button(mActivity);
|
||||
btn.setText(mActivity.getResources().getString(stringResource));
|
||||
Button btn = new Button(mContext);
|
||||
btn.setText(mContext.getResources().getString(stringResource));
|
||||
btn.setTextColor(Color.WHITE);
|
||||
btn.setBackgroundResource(R.drawable.jellyfin_button);
|
||||
layout.addView(btn);
|
||||
@@ -424,15 +310,13 @@ public class LiveProgramDetailPopup {
|
||||
private RecordPopup mRecordPopup;
|
||||
|
||||
public void showRecordingOptions(final boolean recordSeries) {
|
||||
if (mRecordPopup == null) mRecordPopup = new RecordPopup(mActivity, lifecycle, mAnchor, mPosLeft, mPosTop, mPopup.getWidth());
|
||||
apiClient.getValue().GetLiveTvSeriesTimerAsync(mProgram.getSeriesTimerId(), new Response<SeriesTimerInfoDto>() {
|
||||
@Override
|
||||
public void onResponse(SeriesTimerInfoDto response) {
|
||||
mRecordPopup.setContent(mActivity, ModelCompat.asSdk(mProgram), response, mSelectedProgramView, recordSeries);
|
||||
mRecordPopup.show();
|
||||
}
|
||||
if (mRecordPopup == null)
|
||||
mRecordPopup = new RecordPopup(mContext, lifecycle, mAnchor, mPosLeft, mPosTop, mPopup.getWidth());
|
||||
|
||||
LiveProgramDetailPopupHelperKt.getSeriesTimer(this, mProgram.getSeriesTimerId(), seriesTimer -> {
|
||||
mRecordPopup.setContent(mContext, mProgram, seriesTimer, mSelectedProgramView, recordSeries);
|
||||
mRecordPopup.show();
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
package org.jellyfin.androidtv.ui
|
||||
|
||||
import androidx.lifecycle.coroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import org.jellyfin.androidtv.data.repository.ItemMutationRepository
|
||||
import org.jellyfin.androidtv.util.getActivity
|
||||
import org.jellyfin.sdk.api.client.ApiClient
|
||||
import org.jellyfin.sdk.api.client.extensions.liveTvApi
|
||||
import org.jellyfin.sdk.model.api.BaseItemDto
|
||||
import org.jellyfin.sdk.model.api.SeriesTimerInfoDto
|
||||
import org.jellyfin.sdk.model.api.TimerInfoDto
|
||||
import org.koin.android.ext.android.inject
|
||||
import java.util.UUID
|
||||
|
||||
fun BaseItemDto.copyWithTimerId(
|
||||
timerId: String?,
|
||||
) = copy(
|
||||
timerId = timerId,
|
||||
)
|
||||
|
||||
fun BaseItemDto.copyWithSeriesTimerId(
|
||||
seriesTimerId: String?,
|
||||
) = copy(
|
||||
seriesTimerId = seriesTimerId,
|
||||
)
|
||||
|
||||
fun LiveProgramDetailPopup.cancelTimer(
|
||||
timerId: String,
|
||||
callback: () -> Unit,
|
||||
) {
|
||||
val api by mContext.getActivity()!!.inject<ApiClient>()
|
||||
|
||||
lifecycle.coroutineScope.launch {
|
||||
runCatching {
|
||||
api.liveTvApi.cancelTimer(timerId)
|
||||
}.onSuccess {
|
||||
callback()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun LiveProgramDetailPopup.cancelSeriesTimer(
|
||||
seriesTimerId: String,
|
||||
callback: () -> Unit,
|
||||
) {
|
||||
val api by mContext.getActivity()!!.inject<ApiClient>()
|
||||
|
||||
lifecycle.coroutineScope.launch {
|
||||
runCatching {
|
||||
api.liveTvApi.cancelSeriesTimer(seriesTimerId)
|
||||
}.onSuccess {
|
||||
callback()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun SeriesTimerInfoDto.asTimerInfoDto() = TimerInfoDto(
|
||||
id = id,
|
||||
type = type,
|
||||
serverId = serverId,
|
||||
externalId = externalId,
|
||||
channelId = channelId,
|
||||
externalChannelId = externalChannelId,
|
||||
channelName = channelName,
|
||||
channelPrimaryImageTag = channelPrimaryImageTag,
|
||||
programId = programId,
|
||||
externalProgramId = externalProgramId,
|
||||
name = name,
|
||||
overview = overview,
|
||||
startDate = startDate,
|
||||
endDate = endDate,
|
||||
serviceName = serviceName,
|
||||
priority = priority,
|
||||
prePaddingSeconds = prePaddingSeconds,
|
||||
postPaddingSeconds = postPaddingSeconds,
|
||||
isPrePaddingRequired = isPrePaddingRequired,
|
||||
parentBackdropItemId = parentBackdropItemId,
|
||||
parentBackdropImageTags = parentBackdropImageTags,
|
||||
isPostPaddingRequired = isPostPaddingRequired,
|
||||
keepUntil = keepUntil,
|
||||
)
|
||||
|
||||
fun LiveProgramDetailPopup.recordProgram(
|
||||
programId: UUID,
|
||||
callback: (program: BaseItemDto) -> Unit,
|
||||
) {
|
||||
val api by mContext.getActivity()!!.inject<ApiClient>()
|
||||
|
||||
lifecycle.coroutineScope.launch {
|
||||
runCatching {
|
||||
val seriesTimer by api.liveTvApi.getDefaultTimer(programId.toString())
|
||||
val timer = seriesTimer.asTimerInfoDto()
|
||||
api.liveTvApi.createTimer(timer)
|
||||
api.liveTvApi.getProgram(programId.toString()).content
|
||||
}.onSuccess { program ->
|
||||
callback(program)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun LiveProgramDetailPopup.recordSeries(
|
||||
programId: UUID,
|
||||
callback: (program: BaseItemDto) -> Unit,
|
||||
) {
|
||||
val api by mContext.getActivity()!!.inject<ApiClient>()
|
||||
|
||||
lifecycle.coroutineScope.launch {
|
||||
runCatching {
|
||||
val timer by api.liveTvApi.getDefaultTimer(programId.toString())
|
||||
api.liveTvApi.createSeriesTimer(timer)
|
||||
api.liveTvApi.getProgram(programId.toString()).content
|
||||
}.onSuccess { program ->
|
||||
callback(program)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun LiveProgramDetailPopup.getSeriesTimer(
|
||||
seriesTimerId: String,
|
||||
callback: (seriesTimer: SeriesTimerInfoDto) -> Unit,
|
||||
) {
|
||||
val api by mContext.getActivity()!!.inject<ApiClient>()
|
||||
|
||||
lifecycle.coroutineScope.launch {
|
||||
runCatching {
|
||||
api.liveTvApi.getSeriesTimer(seriesTimerId).content
|
||||
}.onSuccess { seriesTimer ->
|
||||
callback(seriesTimer)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun LiveProgramDetailPopup.toggleFavorite(
|
||||
item: BaseItemDto,
|
||||
callback: (item: BaseItemDto) -> Unit,
|
||||
) {
|
||||
val itemMutationRepository by mContext.getActivity()!!.inject<ItemMutationRepository>()
|
||||
|
||||
lifecycle.coroutineScope.launch {
|
||||
runCatching {
|
||||
val userData = itemMutationRepository.setFavorite(
|
||||
item = item.id,
|
||||
favorite = !(item.userData?.isFavorite ?: false)
|
||||
)
|
||||
|
||||
item.copy(userData = userData)
|
||||
}.onSuccess { item ->
|
||||
callback(item)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,7 @@ import org.jellyfin.androidtv.ui.navigation.NavigationRepository
|
||||
import org.jellyfin.androidtv.ui.playback.AudioEventListener
|
||||
import org.jellyfin.androidtv.ui.playback.MediaManager
|
||||
import org.jellyfin.androidtv.ui.playback.PlaybackController
|
||||
import org.jellyfin.androidtv.util.ImageUtils
|
||||
import org.jellyfin.androidtv.util.ImageHelper
|
||||
import org.jellyfin.androidtv.util.TimeUtils
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.inject
|
||||
@@ -29,6 +29,7 @@ class NowPlayingView @JvmOverloads constructor(
|
||||
|
||||
private val mediaManager by inject<MediaManager>()
|
||||
private val navigationRepository by inject<NavigationRepository>()
|
||||
private val imageHelper by inject<ImageHelper>()
|
||||
private var currentDuration: String = ""
|
||||
|
||||
init {
|
||||
@@ -63,7 +64,7 @@ class NowPlayingView @JvmOverloads constructor(
|
||||
private fun setInfo(item: org.jellyfin.sdk.model.api.BaseItemDto) {
|
||||
val placeholder = ContextCompat.getDrawable(context, R.drawable.ic_album)
|
||||
val blurHash = item.imageBlurHashes?.get(org.jellyfin.sdk.model.api.ImageType.PRIMARY)?.get(item.imageTags?.get(org.jellyfin.sdk.model.api.ImageType.PRIMARY))
|
||||
binding.npIcon.load(ImageUtils.getPrimaryImageUrl(item), blurHash, placeholder, item.primaryImageAspectRatio ?: 1.0)
|
||||
binding.npIcon.load(imageHelper.getPrimaryImageUrl(item), blurHash, placeholder, item.primaryImageAspectRatio ?: 1.0)
|
||||
|
||||
currentDuration = TimeUtils.formatMillis(if (item.runTimeTicks != null) item.runTimeTicks!! / 10_000 else 0)
|
||||
binding.npDesc.text = if (item.albumArtist != null) item.albumArtist else item.name
|
||||
|
||||
@@ -17,14 +17,12 @@ import android.widget.TextView;
|
||||
import org.jellyfin.androidtv.R;
|
||||
import org.jellyfin.androidtv.preference.LiveTvPreferences;
|
||||
import org.jellyfin.androidtv.ui.livetv.LiveTvGuide;
|
||||
import org.jellyfin.androidtv.util.InfoLayoutHelper;
|
||||
import org.jellyfin.androidtv.util.TimeUtils;
|
||||
import org.jellyfin.androidtv.util.DateTimeExtensionsKt;
|
||||
import org.jellyfin.androidtv.util.Utils;
|
||||
import org.jellyfin.androidtv.util.sdk.BaseItemExtensionsKt;
|
||||
import org.jellyfin.androidtv.util.sdk.compat.ModelCompat;
|
||||
import org.jellyfin.apiclient.model.dto.BaseItemDto;
|
||||
import org.jellyfin.sdk.model.api.BaseItemDto;
|
||||
|
||||
import java.util.Date;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
public class ProgramGridCell extends RelativeLayout implements RecordingIndicatorView {
|
||||
|
||||
@@ -60,37 +58,37 @@ public class ProgramGridCell extends RelativeLayout implements RecordingIndicato
|
||||
setCellBackground();
|
||||
|
||||
if (program.getStartDate() != null && program.getEndDate() != null) {
|
||||
Date localStart = TimeUtils.convertToLocalDate(program.getStartDate());
|
||||
if (localStart.getTime() + 60000 < activity.getCurrentLocalStartDate()) {
|
||||
LocalDateTime localStart = program.getStartDate();
|
||||
if (localStart.plusMinutes(1).isBefore(activity.getCurrentLocalStartDate())) {
|
||||
mProgramName.setText("<< "+mProgramName.getText());
|
||||
TextView time = new TextView(context);
|
||||
time.setTypeface(Typeface.create("sans-serif-light", Typeface.NORMAL));
|
||||
time.setTextSize(12);
|
||||
time.setText(android.text.format.DateFormat.getTimeFormat(getContext()).format(TimeUtils.convertToLocalDate(program.getStartDate())));
|
||||
time.setText(DateTimeExtensionsKt.getTimeFormatter(getContext()).format(program.getStartDate()));
|
||||
mInfoRow.addView(time);
|
||||
}
|
||||
}
|
||||
|
||||
LiveTvPreferences liveTvPreferences = get(LiveTvPreferences.class);
|
||||
|
||||
if (liveTvPreferences.get(LiveTvPreferences.Companion.getShowNewIndicator()) && BaseItemExtensionsKt.isNew(ModelCompat.asSdk(program)) && (!liveTvPreferences.get(LiveTvPreferences.Companion.getShowPremiereIndicator()) || !Utils.isTrue(program.getIsPremiere()))) {
|
||||
InfoLayoutHelper.addBlockText(context, mInfoRow, context.getString(R.string.lbl_new), 10, Color.GRAY, R.drawable.dark_green_gradient);
|
||||
if (liveTvPreferences.get(LiveTvPreferences.Companion.getShowNewIndicator()) && BaseItemExtensionsKt.isNew(program) && (!liveTvPreferences.get(LiveTvPreferences.Companion.getShowPremiereIndicator()) || !Utils.isTrue(program.isPremiere()))) {
|
||||
addBlockText(context.getString(R.string.lbl_new), 10, Color.GRAY, R.drawable.dark_green_gradient);
|
||||
}
|
||||
|
||||
if (liveTvPreferences.get(LiveTvPreferences.Companion.getShowPremiereIndicator()) && Utils.isTrue(program.getIsPremiere())) {
|
||||
InfoLayoutHelper.addBlockText(context, mInfoRow, context.getString(R.string.lbl_premiere), 10, Color.GRAY, R.drawable.dark_green_gradient);
|
||||
if (liveTvPreferences.get(LiveTvPreferences.Companion.getShowPremiereIndicator()) && Utils.isTrue(program.isPremiere())) {
|
||||
addBlockText(context.getString(R.string.lbl_premiere), 10, Color.GRAY, R.drawable.dark_green_gradient);
|
||||
}
|
||||
|
||||
if (liveTvPreferences.get(LiveTvPreferences.Companion.getShowRepeatIndicator()) && Utils.isTrue(program.getIsRepeat())) {
|
||||
InfoLayoutHelper.addBlockText(context, mInfoRow, context.getString(R.string.lbl_repeat), 10, Color.GRAY, androidx.leanback.R.color.lb_default_brand_color);
|
||||
if (liveTvPreferences.get(LiveTvPreferences.Companion.getShowRepeatIndicator()) && Utils.isTrue(program.isRepeat())) {
|
||||
addBlockText(context.getString(R.string.lbl_repeat), 10, Color.GRAY, androidx.leanback.R.color.lb_default_brand_color);
|
||||
}
|
||||
|
||||
if (program.getOfficialRating() != null && !program.getOfficialRating().equals("0")) {
|
||||
InfoLayoutHelper.addBlockText(context, mInfoRow, program.getOfficialRating(), 10);
|
||||
addBlockText(program.getOfficialRating(), 10, Color.BLACK, R.drawable.block_text_bg);
|
||||
}
|
||||
|
||||
if (liveTvPreferences.get(LiveTvPreferences.Companion.getShowHDIndicator()) && Utils.isTrue(program.getIsHD())) {
|
||||
InfoLayoutHelper.addBlockText(context, mInfoRow, "HD", 10);
|
||||
if (liveTvPreferences.get(LiveTvPreferences.Companion.getShowHDIndicator()) && Utils.isTrue(program.isHd())) {
|
||||
addBlockText("HD", 10, Color.BLACK, R.drawable.block_text_bg);
|
||||
}
|
||||
|
||||
if (program.getSeriesTimerId() != null) {
|
||||
@@ -111,17 +109,29 @@ public class ProgramGridCell extends RelativeLayout implements RecordingIndicato
|
||||
|
||||
}
|
||||
|
||||
private void addBlockText(String text, int size, int textColor, int backgroundRes) {
|
||||
TextView view = new TextView(getContext());
|
||||
view.setTextSize(size);
|
||||
view.setTextColor(textColor);
|
||||
view.setText(" " + text + " ");
|
||||
view.setBackgroundResource(backgroundRes);
|
||||
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(new LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT));
|
||||
params.setMargins(0, Utils.convertDpToPixel(getContext(), -2), 0, 0);
|
||||
view.setLayoutParams(params);
|
||||
mInfoRow.addView(view);
|
||||
}
|
||||
|
||||
public void setCellBackground() {
|
||||
LiveTvPreferences liveTvPreferences = get(LiveTvPreferences.class);
|
||||
|
||||
if (liveTvPreferences.get(LiveTvPreferences.Companion.getColorCodeGuide())) {
|
||||
if (Utils.isTrue(mProgram.getIsMovie())) {
|
||||
if (Utils.isTrue(mProgram.isMovie())) {
|
||||
mBackgroundColor = getResources().getColor(R.color.guide_movie_bg);
|
||||
} else if (Utils.isTrue(mProgram.getIsNews())) {
|
||||
} else if (Utils.isTrue(mProgram.isNews())) {
|
||||
mBackgroundColor = getResources().getColor(R.color.guide_news_bg);
|
||||
} else if (Utils.isTrue(mProgram.getIsSports())) {
|
||||
} else if (Utils.isTrue(mProgram.isSports())) {
|
||||
mBackgroundColor = getResources().getColor(R.color.guide_sports_bg);
|
||||
} else if (Utils.isTrue(mProgram.getIsKids())) {
|
||||
} else if (Utils.isTrue(mProgram.isKids())) {
|
||||
mBackgroundColor = getResources().getColor(R.color.guide_kids_bg);
|
||||
}
|
||||
|
||||
@@ -150,11 +160,13 @@ public class ProgramGridCell extends RelativeLayout implements RecordingIndicato
|
||||
public boolean isFirst() { return isFirst; }
|
||||
|
||||
public void setRecTimer(String id) {
|
||||
mProgram.setTimerId(id);
|
||||
if (mProgram == null) return;
|
||||
mProgram = LiveProgramDetailPopupHelperKt.copyWithTimerId(mProgram, id);
|
||||
mRecIndicator.setImageResource(id != null ? (mProgram.getSeriesTimerId() != null ? R.drawable.ic_record_series_red : R.drawable.ic_record_red) : mProgram.getSeriesTimerId() != null ? R.drawable.ic_record_series : R.drawable.blank10x10);
|
||||
}
|
||||
public void setRecSeriesTimer(String id) {
|
||||
mProgram.setSeriesTimerId(id);
|
||||
if (mProgram == null) return;
|
||||
mProgram = LiveProgramDetailPopupHelperKt.copyWithSeriesTimerId(mProgram, id);
|
||||
mRecIndicator.setImageResource(id != null ? R.drawable.ic_record_series_red : R.drawable.blank10x10);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ package org.jellyfin.androidtv.ui;
|
||||
|
||||
import static org.koin.java.KoinJavaComponent.inject;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Typeface;
|
||||
@@ -23,29 +22,28 @@ import android.widget.TextView;
|
||||
import androidx.lifecycle.Lifecycle;
|
||||
|
||||
import org.jellyfin.androidtv.R;
|
||||
import org.jellyfin.androidtv.auth.repository.UserRepository;
|
||||
import org.jellyfin.androidtv.constant.CustomMessage;
|
||||
import org.jellyfin.androidtv.data.repository.CustomMessageRepository;
|
||||
import org.jellyfin.androidtv.util.ContextExtensionsKt;
|
||||
import org.jellyfin.androidtv.util.DateTimeExtensionsKt;
|
||||
import org.jellyfin.androidtv.util.TimeUtils;
|
||||
import org.jellyfin.androidtv.util.Utils;
|
||||
import org.jellyfin.androidtv.util.apiclient.EmptyLifecycleAwareResponse;
|
||||
import org.jellyfin.apiclient.interaction.ApiClient;
|
||||
import org.jellyfin.apiclient.interaction.Response;
|
||||
import org.jellyfin.apiclient.model.livetv.SeriesTimerInfoDto;
|
||||
import org.jellyfin.apiclient.model.livetv.TimerInfoDto;
|
||||
import org.jellyfin.sdk.model.api.BaseItemDto;
|
||||
import org.koin.java.KoinJavaComponent;
|
||||
import org.jellyfin.sdk.model.api.SeriesTimerInfoDto;
|
||||
import org.jellyfin.sdk.model.api.TimerInfoDto;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneOffset;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Date;
|
||||
import java.util.UUID;
|
||||
|
||||
import kotlin.Lazy;
|
||||
|
||||
public class RecordPopup {
|
||||
PopupWindow mPopup;
|
||||
String mProgramId;
|
||||
UUID mProgramId;
|
||||
SeriesTimerInfoDto mCurrentOptions;
|
||||
RecordingIndicatorView mSelectedView;
|
||||
View mAnchorView;
|
||||
@@ -53,8 +51,8 @@ public class RecordPopup {
|
||||
int mPosTop;
|
||||
boolean mRecordSeries;
|
||||
|
||||
Activity mActivity;
|
||||
private final Lifecycle lifecycle;
|
||||
Context mContext;
|
||||
final Lifecycle lifecycle;
|
||||
TextView mDTitle;
|
||||
LinearLayout mDTimeline;
|
||||
View mSeriesOptions;
|
||||
@@ -70,43 +68,41 @@ public class RecordPopup {
|
||||
ArrayList<String> mPaddingDisplayOptions;
|
||||
ArrayList<Integer> mPaddingValues = new ArrayList<>(Arrays.asList(0,60,300,900,1800,3600,5400,7200,10800));
|
||||
|
||||
private Lazy<ApiClient> apiClient = inject(ApiClient.class);
|
||||
private Lazy<CustomMessageRepository> customMessageRepository = inject(CustomMessageRepository.class);
|
||||
|
||||
public RecordPopup(Activity activity, Lifecycle lifecycle, View anchorView, int left, int top, int width) {
|
||||
mActivity = activity;
|
||||
public RecordPopup(Context context, Lifecycle lifecycle, View anchorView, int left, int top, int width) {
|
||||
mContext = context;
|
||||
this.lifecycle = lifecycle;
|
||||
mAnchorView = anchorView;
|
||||
mPosLeft = left;
|
||||
mPosTop = top;
|
||||
|
||||
mPaddingDisplayOptions = new ArrayList<>(Arrays.asList(
|
||||
mActivity.getString(R.string.lbl_on_schedule),
|
||||
ContextExtensionsKt.getQuantityString(activity, R.plurals.minutes, 1),
|
||||
ContextExtensionsKt.getQuantityString(activity, R.plurals.minutes, 5),
|
||||
ContextExtensionsKt.getQuantityString(activity, R.plurals.minutes, 15),
|
||||
ContextExtensionsKt.getQuantityString(activity, R.plurals.minutes, 30),
|
||||
ContextExtensionsKt.getQuantityString(activity, R.plurals.minutes, 60),
|
||||
ContextExtensionsKt.getQuantityString(activity, R.plurals.minutes, 90),
|
||||
ContextExtensionsKt.getQuantityString(activity, R.plurals.hours, 2),
|
||||
ContextExtensionsKt.getQuantityString(activity, R.plurals.hours, 3)
|
||||
context.getString(R.string.lbl_on_schedule),
|
||||
ContextExtensionsKt.getQuantityString(context, R.plurals.minutes, 1),
|
||||
ContextExtensionsKt.getQuantityString(context, R.plurals.minutes, 5),
|
||||
ContextExtensionsKt.getQuantityString(context, R.plurals.minutes, 15),
|
||||
ContextExtensionsKt.getQuantityString(context, R.plurals.minutes, 30),
|
||||
ContextExtensionsKt.getQuantityString(context, R.plurals.minutes, 60),
|
||||
ContextExtensionsKt.getQuantityString(context, R.plurals.minutes, 90),
|
||||
ContextExtensionsKt.getQuantityString(context, R.plurals.hours, 2),
|
||||
ContextExtensionsKt.getQuantityString(context, R.plurals.hours, 3)
|
||||
));
|
||||
|
||||
LayoutInflater inflater = (LayoutInflater) mActivity.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
|
||||
View layout = inflater.inflate(R.layout.new_program_record_popup, null);
|
||||
int popupHeight = Utils.convertDpToPixel(activity, 330);
|
||||
View layout = LayoutInflater.from(context).inflate(R.layout.new_program_record_popup, null);
|
||||
int popupHeight = Utils.convertDpToPixel(context, 330);
|
||||
mPopup = new PopupWindow(layout, width, popupHeight);
|
||||
mPopup.setFocusable(true);
|
||||
mPopup.setOutsideTouchable(true);
|
||||
mPopup.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT)); // necessary for popup to dismiss
|
||||
mDTitle = (TextView)layout.findViewById(R.id.title);
|
||||
mDTitle = layout.findViewById(R.id.title);
|
||||
|
||||
mPrePadding = (Spinner) layout.findViewById(R.id.prePadding);
|
||||
mPrePadding.setAdapter(new ArrayAdapter<>(mActivity, android.R.layout.simple_spinner_item, mPaddingDisplayOptions));
|
||||
mPrePadding = layout.findViewById(R.id.prePadding);
|
||||
mPrePadding.setAdapter(new ArrayAdapter<>(mContext, android.R.layout.simple_spinner_item, mPaddingDisplayOptions));
|
||||
mPrePadding.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
|
||||
@Override
|
||||
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
|
||||
mCurrentOptions.setPrePaddingSeconds(mPaddingValues.get(position));
|
||||
mCurrentOptions = RecordPopupHelperKt.copyWithPrePaddingSeconds(mCurrentOptions, mPaddingValues.get(position));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -115,11 +111,11 @@ public class RecordPopup {
|
||||
}
|
||||
});
|
||||
mPostPadding = (Spinner) layout.findViewById(R.id.postPadding);
|
||||
mPostPadding.setAdapter(new ArrayAdapter<>(mActivity, android.R.layout.simple_spinner_item, mPaddingDisplayOptions));
|
||||
mPostPadding.setAdapter(new ArrayAdapter<>(mContext, android.R.layout.simple_spinner_item, mPaddingDisplayOptions));
|
||||
mPostPadding.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
|
||||
@Override
|
||||
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
|
||||
mCurrentOptions.setPostPaddingSeconds(mPaddingValues.get(position));
|
||||
mCurrentOptions = RecordPopupHelperKt.copyWithPostPaddingSeconds(mCurrentOptions, mPaddingValues.get(position));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -128,68 +124,39 @@ public class RecordPopup {
|
||||
}
|
||||
});
|
||||
|
||||
mOnlyNew = (CheckBox) layout.findViewById(R.id.onlyNew);
|
||||
mAnyChannel = (CheckBox) layout.findViewById(R.id.anyChannel);
|
||||
mAnyTime = (CheckBox) layout.findViewById(R.id.anyTime);
|
||||
mOnlyNew = layout.findViewById(R.id.onlyNew);
|
||||
mAnyChannel = layout.findViewById(R.id.anyChannel);
|
||||
mAnyTime = layout.findViewById(R.id.anyTime);
|
||||
|
||||
mSeriesOptions = layout.findViewById(R.id.seriesOptions);
|
||||
|
||||
mOkButton = (Button) layout.findViewById(R.id.okButton);
|
||||
mOkButton = layout.findViewById(R.id.okButton);
|
||||
mOkButton.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
if (mRecordSeries) {
|
||||
mCurrentOptions.setRecordNewOnly(mOnlyNew.isChecked());
|
||||
mCurrentOptions.setRecordAnyChannel(mAnyChannel.isChecked());
|
||||
mCurrentOptions.setRecordAnyTime(mAnyTime.isChecked());
|
||||
|
||||
apiClient.getValue().UpdateLiveTvSeriesTimerAsync(mCurrentOptions, new EmptyLifecycleAwareResponse(lifecycle) {
|
||||
@Override
|
||||
public void onResponse() {
|
||||
mPopup.dismiss();
|
||||
customMessageRepository.getValue().pushMessage(CustomMessage.ActionComplete.INSTANCE);
|
||||
Utils.showToast(mActivity, R.string.msg_settings_updated);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(Exception ex) {
|
||||
Utils.showToast(mActivity, R.string.msg_unable_to_create_recording);
|
||||
}
|
||||
mCurrentOptions = RecordPopupHelperKt.copyWithFilters(mCurrentOptions, mOnlyNew.isChecked(), mAnyChannel.isChecked(), mAnyTime.isChecked());
|
||||
|
||||
RecordPopupHelperKt.updateSeriesTimer(RecordPopup.this, mCurrentOptions, () -> {
|
||||
mPopup.dismiss();
|
||||
customMessageRepository.getValue().pushMessage(CustomMessage.ActionComplete.INSTANCE);
|
||||
Utils.showToast(mContext, R.string.msg_settings_updated);
|
||||
return null;
|
||||
});
|
||||
|
||||
} else {
|
||||
TimerInfoDto updated = new TimerInfoDto();
|
||||
updated.setProgramId(mProgramId);
|
||||
updated.setPrePaddingSeconds(mCurrentOptions.getPrePaddingSeconds());
|
||||
updated.setPostPaddingSeconds(mCurrentOptions.getPostPaddingSeconds());
|
||||
updated.setIsPrePaddingRequired(mCurrentOptions.getIsPrePaddingRequired());
|
||||
updated.setIsPostPaddingRequired(mCurrentOptions.getIsPostPaddingRequired());
|
||||
TimerInfoDto updated = RecordPopupHelperKt.createProgramTimerInfo(mProgramId, mCurrentOptions);
|
||||
|
||||
apiClient.getValue().UpdateLiveTvTimerAsync(updated, new EmptyLifecycleAwareResponse(lifecycle) {
|
||||
@Override
|
||||
public void onResponse() {
|
||||
if (!getActive()) return;
|
||||
|
||||
mPopup.dismiss();
|
||||
customMessageRepository.getValue().pushMessage(CustomMessage.ActionComplete.INSTANCE);
|
||||
// we have to re-retrieve the program to get the timer id
|
||||
apiClient.getValue().GetLiveTvProgramAsync(mProgramId, KoinJavaComponent.<UserRepository>get(UserRepository.class).getCurrentUser().getValue().getId().toString(), new Response<org.jellyfin.apiclient.model.dto.BaseItemDto>() {
|
||||
@Override
|
||||
public void onResponse(org.jellyfin.apiclient.model.dto.BaseItemDto response) {
|
||||
mSelectedView.setRecTimer(response.getTimerId());
|
||||
mSelectedView.setRecSeriesTimer(response.getSeriesTimerId());
|
||||
}
|
||||
});
|
||||
Utils.showToast(mActivity, R.string.msg_set_to_record);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(Exception ex) {
|
||||
if (!getActive()) return;
|
||||
|
||||
Utils.showToast(mActivity, R.string.msg_unable_to_create_recording);
|
||||
}
|
||||
RecordPopupHelperKt.updateTimer(RecordPopup.this, updated, () -> {
|
||||
mPopup.dismiss();
|
||||
customMessageRepository.getValue().pushMessage(CustomMessage.ActionComplete.INSTANCE);
|
||||
// we have to re-retrieve the program to get the timer id
|
||||
RecordPopupHelperKt.getLiveTvProgram(RecordPopup.this, mProgramId, program -> {
|
||||
mSelectedView.setRecTimer(program.getTimerId());
|
||||
mSelectedView.setRecSeriesTimer(program.getSeriesTimerId());
|
||||
return null;
|
||||
});
|
||||
Utils.showToast(mContext, R.string.msg_set_to_record);
|
||||
return null;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -211,7 +178,7 @@ public class RecordPopup {
|
||||
}
|
||||
|
||||
public void setContent(Context context, BaseItemDto program, SeriesTimerInfoDto current, RecordingIndicatorView selectedView, boolean recordSeries) {
|
||||
mProgramId = program.getId().toString();
|
||||
mProgramId = program.getId();
|
||||
mCurrentOptions = current;
|
||||
mRecordSeries = recordSeries;
|
||||
mSelectedView = selectedView;
|
||||
@@ -263,17 +230,24 @@ public class RecordPopup {
|
||||
timelineRow.removeAllViews();
|
||||
if (program.getStartDate() == null) return;
|
||||
|
||||
Date local = TimeUtils.getDate(program.getStartDate());
|
||||
TextView on = new TextView(mActivity);
|
||||
on.setText(mActivity.getString(R.string.lbl_on));
|
||||
LocalDateTime local = program.getStartDate();
|
||||
TextView on = new TextView(mContext);
|
||||
on.setText(mContext.getString(R.string.lbl_on));
|
||||
timelineRow.addView(on);
|
||||
TextView channel = new TextView(mActivity);
|
||||
TextView channel = new TextView(mContext);
|
||||
channel.setText(program.getChannelName());
|
||||
channel.setTypeface(null, Typeface.BOLD);
|
||||
channel.setTextColor(mActivity.getResources().getColor(android.R.color.holo_blue_light));
|
||||
channel.setTextColor(mContext.getResources().getColor(android.R.color.holo_blue_light));
|
||||
timelineRow.addView(channel);
|
||||
TextView datetime = new TextView(mActivity);
|
||||
datetime.setText(TimeUtils.getFriendlyDate(context, local)+ " @ "+android.text.format.DateFormat.getTimeFormat(mActivity).format(local)+ " ("+ DateUtils.getRelativeTimeSpanString(local.getTime())+")");
|
||||
TextView datetime = new TextView(mContext);
|
||||
datetime.setText(new StringBuilder()
|
||||
.append(TimeUtils.getFriendlyDate(context, local))
|
||||
.append(" @ ")
|
||||
.append(DateTimeExtensionsKt.getTimeFormatter(mContext).format(local))
|
||||
.append(" (")
|
||||
.append(DateUtils.getRelativeTimeSpanString(local.toInstant(ZoneOffset.UTC).toEpochMilli(), Instant.now().toEpochMilli(), 0))
|
||||
.append(")")
|
||||
);
|
||||
timelineRow.addView(datetime);
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
package org.jellyfin.androidtv.ui
|
||||
|
||||
import androidx.lifecycle.coroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import org.jellyfin.androidtv.util.getActivity
|
||||
import org.jellyfin.sdk.api.client.ApiClient
|
||||
import org.jellyfin.sdk.api.client.extensions.liveTvApi
|
||||
import org.jellyfin.sdk.model.api.BaseItemDto
|
||||
import org.jellyfin.sdk.model.api.SeriesTimerInfoDto
|
||||
import org.jellyfin.sdk.model.api.TimerInfoDto
|
||||
import org.koin.android.ext.android.inject
|
||||
import java.util.UUID
|
||||
|
||||
fun SeriesTimerInfoDto.copyWithPrePaddingSeconds(
|
||||
prePaddingSeconds: Int,
|
||||
) = copy(
|
||||
prePaddingSeconds = prePaddingSeconds,
|
||||
)
|
||||
|
||||
fun SeriesTimerInfoDto.copyWithPostPaddingSeconds(
|
||||
postPaddingSeconds: Int,
|
||||
) = copy(
|
||||
postPaddingSeconds = postPaddingSeconds,
|
||||
)
|
||||
|
||||
fun SeriesTimerInfoDto.copyWithFilters(
|
||||
recordNewOnly: Boolean,
|
||||
recordAnyChannel: Boolean,
|
||||
recordAnyTime: Boolean,
|
||||
) = copy(
|
||||
recordNewOnly = recordNewOnly,
|
||||
recordAnyChannel = recordAnyChannel,
|
||||
recordAnyTime = recordAnyTime,
|
||||
)
|
||||
|
||||
fun RecordPopup.updateSeriesTimer(
|
||||
seriesTimer: SeriesTimerInfoDto,
|
||||
callback: () -> Unit,
|
||||
) {
|
||||
val api by mContext.getActivity()!!.inject<ApiClient>()
|
||||
|
||||
lifecycle.coroutineScope.launch {
|
||||
runCatching {
|
||||
val id = seriesTimer.id
|
||||
if (id == null) api.liveTvApi.createSeriesTimer(seriesTimer)
|
||||
else api.liveTvApi.updateSeriesTimer(id, seriesTimer)
|
||||
}.onSuccess {
|
||||
callback()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun RecordPopup.updateTimer(
|
||||
timer: TimerInfoDto,
|
||||
callback: () -> Unit,
|
||||
) {
|
||||
val api by mContext.getActivity()!!.inject<ApiClient>()
|
||||
|
||||
lifecycle.coroutineScope.launch {
|
||||
runCatching {
|
||||
val id = timer.id
|
||||
if (id == null) api.liveTvApi.createTimer(timer)
|
||||
else api.liveTvApi.updateTimer(id, timer)
|
||||
}.onSuccess {
|
||||
callback()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun createProgramTimerInfo(
|
||||
programId: UUID,
|
||||
options: SeriesTimerInfoDto,
|
||||
) = TimerInfoDto(
|
||||
programId = programId.toString(),
|
||||
prePaddingSeconds = options.prePaddingSeconds,
|
||||
postPaddingSeconds = options.postPaddingSeconds,
|
||||
isPrePaddingRequired = options.isPrePaddingRequired,
|
||||
isPostPaddingRequired = options.isPostPaddingRequired,
|
||||
)
|
||||
|
||||
fun RecordPopup.getLiveTvProgram(
|
||||
id: UUID,
|
||||
callback: (program: BaseItemDto) -> Unit,
|
||||
) {
|
||||
val api by mContext.getActivity()!!.inject<ApiClient>()
|
||||
|
||||
lifecycle.coroutineScope.launch {
|
||||
runCatching {
|
||||
api.liveTvApi.getProgram(id.toString()).content
|
||||
}.onSuccess { program ->
|
||||
callback(program)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
package org.jellyfin.androidtv.ui
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Color
|
||||
import android.graphics.drawable.ColorDrawable
|
||||
import android.view.Gravity
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.widget.PopupWindow
|
||||
import androidx.core.view.isVisible
|
||||
import org.jellyfin.androidtv.databinding.SettingsPopupBinding
|
||||
import org.jellyfin.androidtv.util.dp
|
||||
|
||||
class SettingsPopup(
|
||||
private val context: Context,
|
||||
private val anchor: View,
|
||||
audioChangedListener: ValueChangedListener<Long>?,
|
||||
subtitleChangedListener: ValueChangedListener<Long>?
|
||||
) {
|
||||
private val binding = SettingsPopupBinding.inflate(LayoutInflater.from(context), null, false)
|
||||
.apply {
|
||||
subtitleDelay.increment = SUBTITLE_DELAY_INCREMENT
|
||||
subtitleDelay.valueChangedListener = subtitleChangedListener
|
||||
audioDelay.valueChangedListener = audioChangedListener
|
||||
}
|
||||
|
||||
val popupWindow = PopupWindow(binding.root, WIDTH.dp(context), FULL_HEIGHT.dp(context))
|
||||
.apply {
|
||||
isFocusable = true
|
||||
isOutsideTouchable = true
|
||||
setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
|
||||
}
|
||||
|
||||
fun show(
|
||||
hasSubtitles: Boolean,
|
||||
audioDelay: Long,
|
||||
subtitleDelay: Long
|
||||
) {
|
||||
binding.audioDelay.value = audioDelay
|
||||
|
||||
binding.subtitleDelayTxt.isVisible = hasSubtitles
|
||||
binding.subtitleDelay.isVisible = hasSubtitles
|
||||
binding.subtitleDelay.value = subtitleDelay
|
||||
|
||||
popupWindow.height = (if (hasSubtitles) FULL_HEIGHT else SINGLE_HEIGHT).dp(context)
|
||||
popupWindow.showAsDropDown(anchor, 0, 0, Gravity.END)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val WIDTH = 240
|
||||
private const val FULL_HEIGHT = 230
|
||||
private const val SINGLE_HEIGHT = 130
|
||||
private const val SUBTITLE_DELAY_INCREMENT = 500L
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,6 @@ package org.jellyfin.androidtv.ui.background
|
||||
|
||||
import android.graphics.drawable.ColorDrawable
|
||||
import androidx.compose.animation.AnimatedContent
|
||||
import androidx.compose.animation.ExperimentalAnimationApi
|
||||
import androidx.compose.animation.core.snap
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.animation.fadeIn
|
||||
@@ -18,6 +17,7 @@ import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.blur
|
||||
import androidx.compose.ui.graphics.BlendMode
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.ColorFilter
|
||||
@@ -25,16 +25,17 @@ import androidx.compose.ui.graphics.asImageBitmap
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.colorResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.graphics.drawable.toBitmap
|
||||
import org.jellyfin.androidtv.R
|
||||
import org.jellyfin.androidtv.data.service.BackgroundService
|
||||
import org.koin.compose.rememberKoinInject
|
||||
import org.koin.compose.koinInject
|
||||
|
||||
@Composable
|
||||
private fun AppThemeBackground() {
|
||||
val context = LocalContext.current
|
||||
val themeBackground = remember(context.theme) {
|
||||
val attributes = context.theme.obtainStyledAttributes(intArrayOf(android.R.attr.windowBackground))
|
||||
val attributes = context.theme.obtainStyledAttributes(intArrayOf(R.attr.defaultBackground))
|
||||
val drawable = attributes.getDrawable(0)
|
||||
attributes.recycle()
|
||||
|
||||
@@ -60,30 +61,35 @@ private fun AppThemeBackground() {
|
||||
}
|
||||
|
||||
@Composable
|
||||
@OptIn(ExperimentalAnimationApi::class)
|
||||
fun AppBackground() {
|
||||
val backgroundService = rememberKoinInject<BackgroundService>()
|
||||
val backgroundService = koinInject<BackgroundService>()
|
||||
val currentBackground by backgroundService.currentBackground.collectAsState()
|
||||
val blurBackground by backgroundService.blurBackground.collectAsState()
|
||||
val enabled by backgroundService.enabled.collectAsState()
|
||||
|
||||
AnimatedContent(
|
||||
targetState = currentBackground,
|
||||
transitionSpec = {
|
||||
val duration = (BackgroundService.TRANSITION_DURATION.inWholeMilliseconds / 2).toInt()
|
||||
fadeIn(tween(durationMillis = duration)) togetherWith fadeOut(snap(delayMillis = duration))
|
||||
},
|
||||
label = "BackgroundTransition",
|
||||
) { background ->
|
||||
if (background != null) {
|
||||
Image(
|
||||
bitmap = background,
|
||||
contentDescription = null,
|
||||
alignment = Alignment.Center,
|
||||
contentScale = ContentScale.Crop,
|
||||
colorFilter = ColorFilter.tint(colorResource(R.color.background_filter), BlendMode.SrcAtop),
|
||||
modifier = Modifier.fillMaxSize()
|
||||
)
|
||||
} else {
|
||||
AppThemeBackground()
|
||||
if (enabled) {
|
||||
AnimatedContent(
|
||||
targetState = currentBackground,
|
||||
transitionSpec = {
|
||||
val duration = (BackgroundService.TRANSITION_DURATION.inWholeMilliseconds / 2).toInt()
|
||||
fadeIn(tween(durationMillis = duration)) togetherWith fadeOut(snap(delayMillis = duration))
|
||||
},
|
||||
label = "BackgroundTransition",
|
||||
) { background ->
|
||||
if (background != null) {
|
||||
Image(
|
||||
bitmap = background,
|
||||
contentDescription = null,
|
||||
alignment = Alignment.Center,
|
||||
contentScale = ContentScale.Crop,
|
||||
colorFilter = ColorFilter.tint(colorResource(R.color.background_filter), BlendMode.SrcAtop),
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.then(if (blurBackground) Modifier.blur(10.dp) else Modifier)
|
||||
)
|
||||
} else {
|
||||
AppThemeBackground()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
package org.jellyfin.androidtv.ui.browsing
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.Gravity
|
||||
import android.view.ViewGroup
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.TextView
|
||||
import androidx.leanback.app.BrowseSupportFragment
|
||||
import androidx.leanback.widget.HeaderItem
|
||||
import androidx.leanback.widget.ListRow
|
||||
@@ -20,17 +16,12 @@ import kotlinx.coroutines.launch
|
||||
import kotlinx.serialization.json.Json
|
||||
import org.jellyfin.androidtv.constant.Extras
|
||||
import org.jellyfin.androidtv.data.service.BackgroundService
|
||||
import org.jellyfin.androidtv.preference.UserPreferences
|
||||
import org.jellyfin.androidtv.preference.UserPreferences.Companion.clockBehavior
|
||||
import org.jellyfin.androidtv.preference.constant.ClockBehavior
|
||||
import org.jellyfin.androidtv.ui.ClockUserView
|
||||
import org.jellyfin.androidtv.ui.itemhandling.BaseRowItem
|
||||
import org.jellyfin.androidtv.ui.itemhandling.ItemLauncher
|
||||
import org.jellyfin.androidtv.ui.itemhandling.ItemRowAdapter
|
||||
import org.jellyfin.androidtv.ui.presentation.CardPresenter
|
||||
import org.jellyfin.androidtv.ui.presentation.MutableObjectAdapter
|
||||
import org.jellyfin.androidtv.ui.presentation.PositionableListRowPresenter
|
||||
import org.jellyfin.androidtv.util.Utils.convertDpToPixel
|
||||
import org.jellyfin.sdk.model.api.BaseItemDto
|
||||
import org.koin.android.ext.android.inject
|
||||
|
||||
@@ -40,8 +31,8 @@ abstract class BrowseFolderFragment : BrowseSupportFragment(), RowLoader {
|
||||
|
||||
protected var rows = mutableListOf<BrowseRowDef>()
|
||||
|
||||
private val userPreferences by inject<UserPreferences>()
|
||||
private val backgroundService by inject<BackgroundService>()
|
||||
private val itemLauncher by inject<ItemLauncher>()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
@@ -57,10 +48,9 @@ abstract class BrowseFolderFragment : BrowseSupportFragment(), RowLoader {
|
||||
// Add event listeners
|
||||
onItemViewClickedListener = OnItemViewClickedListener { _: Presenter.ViewHolder?, item: Any?, _: RowPresenter.ViewHolder?, row: Row ->
|
||||
if (item is BaseRowItem) {
|
||||
ItemLauncher.launch(
|
||||
itemLauncher.launch(
|
||||
item,
|
||||
(row as ListRow).adapter as ItemRowAdapter,
|
||||
item.index,
|
||||
requireContext()
|
||||
)
|
||||
}
|
||||
@@ -70,7 +60,7 @@ abstract class BrowseFolderFragment : BrowseSupportFragment(), RowLoader {
|
||||
backgroundService.clearBackgrounds()
|
||||
} else {
|
||||
val adapter = (row as? ListRow)?.adapter
|
||||
if (adapter is ItemRowAdapter) adapter.loadMoreItemsIfNeeded(item.index.toLong())
|
||||
if (adapter is ItemRowAdapter) adapter.loadMoreItemsIfNeeded(adapter.indexOf(item))
|
||||
|
||||
backgroundService.setBackground(item.baseItem)
|
||||
}
|
||||
@@ -112,31 +102,4 @@ abstract class BrowseFolderFragment : BrowseSupportFragment(), RowLoader {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
|
||||
// Check if clock should be visible
|
||||
val showClock = userPreferences[clockBehavior]
|
||||
if (showClock !== ClockBehavior.ALWAYS && showClock !== ClockBehavior.IN_MENUS) return
|
||||
|
||||
val root = requireActivity().findViewById<ViewGroup>(androidx.leanback.R.id.browse_frame)
|
||||
|
||||
// Move the title to the left to make way for the clock
|
||||
val titleView = root.findViewById<TextView>(androidx.leanback.R.id.title_text)
|
||||
if (titleView != null) {
|
||||
val layoutParams = titleView.layoutParams as FrameLayout.LayoutParams
|
||||
layoutParams.rightMargin = convertDpToPixel(root.context, 120)
|
||||
titleView.layoutParams = layoutParams
|
||||
}
|
||||
|
||||
// Add the clock element
|
||||
val layoutParams = FrameLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT)
|
||||
val clockView = ClockUserView(root.context, null)
|
||||
layoutParams.gravity = Gravity.TOP or Gravity.END
|
||||
layoutParams.rightMargin = convertDpToPixel(root.context, 40)
|
||||
layoutParams.topMargin = convertDpToPixel(root.context, 20)
|
||||
clockView.layoutParams = layoutParams
|
||||
root.addView(clockView)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ package org.jellyfin.androidtv.ui.browsing;
|
||||
|
||||
import static org.koin.java.KoinJavaComponent.inject;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.util.DisplayMetrics;
|
||||
@@ -19,6 +18,7 @@ import android.widget.PopupWindow;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentActivity;
|
||||
import androidx.leanback.widget.BaseGridView;
|
||||
import androidx.leanback.widget.OnItemViewClickedListener;
|
||||
import androidx.leanback.widget.OnItemViewSelectedListener;
|
||||
@@ -38,9 +38,7 @@ import org.jellyfin.androidtv.constant.ImageType;
|
||||
import org.jellyfin.androidtv.constant.PosterSize;
|
||||
import org.jellyfin.androidtv.constant.QueryType;
|
||||
import org.jellyfin.androidtv.data.model.FilterOptions;
|
||||
import org.jellyfin.androidtv.data.querying.AlbumArtistsQuery;
|
||||
import org.jellyfin.androidtv.data.querying.StdItemQuery;
|
||||
import org.jellyfin.androidtv.data.querying.ViewQuery;
|
||||
import org.jellyfin.androidtv.data.querying.GetUserViewsRequest;
|
||||
import org.jellyfin.androidtv.data.repository.CustomMessageRepository;
|
||||
import org.jellyfin.androidtv.data.repository.UserViewsRepository;
|
||||
import org.jellyfin.androidtv.data.service.BackgroundService;
|
||||
@@ -52,24 +50,23 @@ import org.jellyfin.androidtv.ui.AlphaPickerView;
|
||||
import org.jellyfin.androidtv.ui.itemhandling.BaseRowItem;
|
||||
import org.jellyfin.androidtv.ui.itemhandling.ItemLauncher;
|
||||
import org.jellyfin.androidtv.ui.itemhandling.ItemRowAdapter;
|
||||
import org.jellyfin.androidtv.ui.itemhandling.ItemRowAdapterHelperKt;
|
||||
import org.jellyfin.androidtv.ui.navigation.Destinations;
|
||||
import org.jellyfin.androidtv.ui.navigation.NavigationRepository;
|
||||
import org.jellyfin.androidtv.ui.presentation.CardPresenter;
|
||||
import org.jellyfin.androidtv.ui.presentation.HorizontalGridPresenter;
|
||||
import org.jellyfin.androidtv.util.CoroutineUtils;
|
||||
import org.jellyfin.androidtv.util.ImageUtils;
|
||||
import org.jellyfin.androidtv.util.ImageHelper;
|
||||
import org.jellyfin.androidtv.util.InfoLayoutHelper;
|
||||
import org.jellyfin.androidtv.util.KeyProcessor;
|
||||
import org.jellyfin.androidtv.util.Utils;
|
||||
import org.jellyfin.androidtv.util.apiclient.EmptyLifecycleAwareResponse;
|
||||
import org.jellyfin.androidtv.util.apiclient.LifecycleAwareResponse;
|
||||
import org.jellyfin.apiclient.model.querying.ArtistsQuery;
|
||||
import org.jellyfin.apiclient.model.querying.ItemFields;
|
||||
import org.jellyfin.sdk.api.client.ApiClient;
|
||||
import org.jellyfin.sdk.model.api.BaseItemDto;
|
||||
import org.jellyfin.sdk.model.api.BaseItemKind;
|
||||
import org.jellyfin.sdk.model.api.CollectionType;
|
||||
import org.jellyfin.sdk.model.api.ItemSortBy;
|
||||
import org.jellyfin.sdk.model.api.SortOrder;
|
||||
import org.jellyfin.sdk.model.constant.CollectionType;
|
||||
import org.jellyfin.sdk.model.constant.ItemSortBy;
|
||||
|
||||
import java.text.MessageFormat;
|
||||
import java.util.HashMap;
|
||||
@@ -85,7 +82,7 @@ public class BrowseGridFragment extends Fragment implements View.OnKeyListener {
|
||||
private final static int CHUNK_SIZE_MINIMUM = 25;
|
||||
|
||||
private String mainTitle;
|
||||
private Activity mActivity;
|
||||
private FragmentActivity mActivity;
|
||||
private BaseRowItem mCurrentItem;
|
||||
private CompositeClickedListener mClickedListener = new CompositeClickedListener();
|
||||
private CompositeSelectedListener mSelectedListener = new CompositeSelectedListener();
|
||||
@@ -123,6 +120,9 @@ public class BrowseGridFragment extends Fragment implements View.OnKeyListener {
|
||||
private final Lazy<UserRepository> userRepository = inject(UserRepository.class);
|
||||
private final Lazy<CustomMessageRepository> customMessageRepository = inject(CustomMessageRepository.class);
|
||||
private final Lazy<NavigationRepository> navigationRepository = inject(NavigationRepository.class);
|
||||
private final Lazy<ItemLauncher> itemLauncher = inject(ItemLauncher.class);
|
||||
private final Lazy<KeyProcessor> keyProcessor = inject(KeyProcessor.class);
|
||||
private final Lazy<ApiClient> api = inject(ApiClient.class);
|
||||
|
||||
private int mCardsScreenEst = 0;
|
||||
private int mCardsScreenStride = 0;
|
||||
@@ -130,7 +130,6 @@ public class BrowseGridFragment extends Fragment implements View.OnKeyListener {
|
||||
private final int MIN_NUM_CARDS = 5; // minimum number of visible cards we allow, this results in more empty space
|
||||
private final double CARD_SPACING_PCT = 1.0; // 100% expressed as relative to the padding_left/top, which depends on the mCardFocusScale and AspectRatio
|
||||
private final double CARD_SPACING_HORIZONTAL_BANNER_PCT = 0.5; // 50% allow horizontal card overlapping for banners, otherwise spacing is too large
|
||||
private final int MIN_GRIDSIZE_CHANGE_DELTA = 4; // minimum pixel size changes, to trigger a recreate of the grid via onGridSizeMeasurements
|
||||
private final int VIEW_SELECT_UPDATE_DELAY = 250; // delay in ms until we update the top-row info for a selected item
|
||||
|
||||
private boolean mDirty = true; // CardHeight, RowDef or GridSize changed
|
||||
@@ -145,16 +144,6 @@ public class BrowseGridFragment extends Fragment implements View.OnKeyListener {
|
||||
mGridHeight = Math.round(display.heightPixels / getResources().getDisplayMetrics().density - 130.6f);
|
||||
mGridWidth = Math.round(display.widthPixels / getResources().getDisplayMetrics().density);
|
||||
|
||||
sortOptions = new HashMap<>();
|
||||
{
|
||||
sortOptions.put(0, new SortOption(getString(R.string.lbl_name), ItemSortBy.SortName, SortOrder.ASCENDING));
|
||||
sortOptions.put(1, new SortOption(getString(R.string.lbl_date_added), ItemSortBy.DateCreated + "," + ItemSortBy.SortName, SortOrder.DESCENDING));
|
||||
sortOptions.put(2, new SortOption(getString(R.string.lbl_premier_date), ItemSortBy.PremiereDate + "," + ItemSortBy.SortName, SortOrder.DESCENDING));
|
||||
sortOptions.put(3, new SortOption(getString(R.string.lbl_rating), ItemSortBy.OfficialRating + "," + ItemSortBy.SortName, SortOrder.ASCENDING));
|
||||
sortOptions.put(4, new SortOption(getString(R.string.lbl_community_rating), ItemSortBy.CommunityRating + "," + ItemSortBy.SortName, SortOrder.DESCENDING));
|
||||
sortOptions.put(5, new SortOption(getString(R.string.lbl_critic_rating), ItemSortBy.CriticRating + "," + ItemSortBy.SortName, SortOrder.DESCENDING));
|
||||
sortOptions.put(6, new SortOption(getString(R.string.lbl_last_played), ItemSortBy.DatePlayed + "," + ItemSortBy.SortName, SortOrder.DESCENDING));
|
||||
}
|
||||
|
||||
mActivity = getActivity();
|
||||
|
||||
@@ -172,6 +161,21 @@ public class BrowseGridFragment extends Fragment implements View.OnKeyListener {
|
||||
else
|
||||
setGridPresenter(new HorizontalGridPresenter());
|
||||
|
||||
sortOptions = new HashMap<>();
|
||||
{
|
||||
sortOptions.put(0, new SortOption(getString(R.string.lbl_name), ItemSortBy.SORT_NAME, SortOrder.ASCENDING));
|
||||
sortOptions.put(1, new SortOption(getString(R.string.lbl_date_added), ItemSortBy.DATE_CREATED, SortOrder.DESCENDING));
|
||||
sortOptions.put(2, new SortOption(getString(R.string.lbl_premier_date), ItemSortBy.PREMIERE_DATE, SortOrder.DESCENDING));
|
||||
sortOptions.put(3, new SortOption(getString(R.string.lbl_rating), ItemSortBy.OFFICIAL_RATING, SortOrder.ASCENDING));
|
||||
sortOptions.put(4, new SortOption(getString(R.string.lbl_community_rating), ItemSortBy.COMMUNITY_RATING, SortOrder.DESCENDING));
|
||||
sortOptions.put(5, new SortOption(getString(R.string.lbl_critic_rating), ItemSortBy.CRITIC_RATING, SortOrder.DESCENDING));
|
||||
sortOptions.put(6, new SortOption(getString(R.string.lbl_last_played), ItemSortBy.DATE_PLAYED, SortOrder.DESCENDING));
|
||||
|
||||
if (mFolder.getCollectionType() != null && mFolder.getCollectionType() == CollectionType.MOVIES) {
|
||||
sortOptions.put(7, new SortOption(getString(R.string.lbl_runtime), ItemSortBy.RUNTIME, SortOrder.ASCENDING));
|
||||
}
|
||||
}
|
||||
|
||||
setDefaultGridRowCols(mPosterSizeSetting, mImageType);
|
||||
setAutoCardGridValues();
|
||||
setupQueries();
|
||||
@@ -202,7 +206,7 @@ public class BrowseGridFragment extends Fragment implements View.OnKeyListener {
|
||||
@Override
|
||||
public boolean onKey(View v, int keyCode, KeyEvent event) {
|
||||
if (event.getAction() != KeyEvent.ACTION_UP) return false;
|
||||
return KeyProcessor.HandleKey(keyCode, mCurrentItem, mActivity);
|
||||
return keyProcessor.getValue().handleKey(keyCode, mCurrentItem, mActivity);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -272,7 +276,7 @@ public class BrowseGridFragment extends Fragment implements View.OnKeyListener {
|
||||
public void setItem(BaseRowItem item) {
|
||||
if (item != null) {
|
||||
binding.title.setText(item.getFullName(requireContext()));
|
||||
InfoLayoutHelper.addInfoRow(requireContext(), item, binding.infoRow, true, true);
|
||||
InfoLayoutHelper.addInfoRow(requireContext(), item.getBaseItem(), binding.infoRow, true);
|
||||
} else {
|
||||
binding.title.setText("");
|
||||
binding.infoRow.removeAllViews();
|
||||
@@ -281,10 +285,10 @@ public class BrowseGridFragment extends Fragment implements View.OnKeyListener {
|
||||
|
||||
public class SortOption {
|
||||
public String name;
|
||||
public String value;
|
||||
public ItemSortBy value;
|
||||
public SortOrder order;
|
||||
|
||||
public SortOption(String name, String value, SortOrder order) {
|
||||
public SortOption(String name, ItemSortBy value, SortOrder order) {
|
||||
this.name = name;
|
||||
this.value = value;
|
||||
this.order = order;
|
||||
@@ -293,13 +297,12 @@ public class BrowseGridFragment extends Fragment implements View.OnKeyListener {
|
||||
|
||||
private Map<Integer, SortOption> sortOptions;
|
||||
|
||||
private SortOption getSortOption(String value) {
|
||||
for (Integer key : sortOptions.keySet()) {
|
||||
SortOption option = sortOptions.get(key);
|
||||
if (Objects.requireNonNull(option).value.equals(value)) return option;
|
||||
private SortOption getSortOption(ItemSortBy value) {
|
||||
for (SortOption sortOption : sortOptions.values()) {
|
||||
if (sortOption.value.equals(value)) return sortOption;
|
||||
}
|
||||
|
||||
return new SortOption("Unknown", "", SortOrder.ASCENDING);
|
||||
return new SortOption(getString(R.string.lbl_bracket_unknown), ItemSortBy.SORT_NAME, SortOrder.ASCENDING);
|
||||
}
|
||||
|
||||
public void setStatusText(String folderName) {
|
||||
@@ -359,15 +362,15 @@ public class BrowseGridFragment extends Fragment implements View.OnKeyListener {
|
||||
BaseItemKind fType = folder.getType();
|
||||
if (fType == BaseItemKind.AUDIO || fType == BaseItemKind.GENRE || fType == BaseItemKind.MUSIC_ALBUM || fType == BaseItemKind.MUSIC_ARTIST || fType == BaseItemKind.MUSIC_GENRE) {
|
||||
return cardHeight;
|
||||
} else if (fType == BaseItemKind.COLLECTION_FOLDER && CollectionType.Music.equals(folder.getCollectionType())) {
|
||||
} else if (fType == BaseItemKind.COLLECTION_FOLDER && CollectionType.MUSIC.equals(folder.getCollectionType())) {
|
||||
return cardHeight;
|
||||
} else {
|
||||
return cardHeight * ImageUtils.ASPECT_RATIO_2_3;
|
||||
return cardHeight * ImageHelper.ASPECT_RATIO_2_3;
|
||||
}
|
||||
case THUMB:
|
||||
return cardHeight * ImageUtils.ASPECT_RATIO_16_9;
|
||||
return cardHeight * ImageHelper.ASPECT_RATIO_16_9;
|
||||
case BANNER:
|
||||
return cardHeight * CardPresenter.ASPECT_RATIO_BANNER;
|
||||
return cardHeight * ImageHelper.ASPECT_RATIO_BANNER;
|
||||
default:
|
||||
throw new IllegalStateException("Unexpected value: " + imageType);
|
||||
}
|
||||
@@ -380,15 +383,15 @@ public class BrowseGridFragment extends Fragment implements View.OnKeyListener {
|
||||
BaseItemKind fType = folder.getType();
|
||||
if (fType == BaseItemKind.AUDIO || fType == BaseItemKind.GENRE || fType == BaseItemKind.MUSIC_ALBUM || fType == BaseItemKind.MUSIC_ARTIST || fType == BaseItemKind.MUSIC_GENRE) {
|
||||
return cardWidth;
|
||||
} else if (fType == BaseItemKind.COLLECTION_FOLDER && CollectionType.Music.equals(folder.getCollectionType())) {
|
||||
} else if (fType == BaseItemKind.COLLECTION_FOLDER && CollectionType.MUSIC.equals(folder.getCollectionType())) {
|
||||
return cardWidth;
|
||||
} else {
|
||||
return cardWidth / ImageUtils.ASPECT_RATIO_2_3;
|
||||
return cardWidth / ImageHelper.ASPECT_RATIO_2_3;
|
||||
}
|
||||
case THUMB:
|
||||
return cardWidth / ImageUtils.ASPECT_RATIO_16_9;
|
||||
return cardWidth / ImageHelper.ASPECT_RATIO_16_9;
|
||||
case BANNER:
|
||||
return cardWidth / CardPresenter.ASPECT_RATIO_BANNER;
|
||||
return cardWidth / ImageHelper.ASPECT_RATIO_BANNER;
|
||||
default:
|
||||
throw new IllegalArgumentException("Unexpected value: " + imageType);
|
||||
}
|
||||
@@ -542,63 +545,22 @@ public class BrowseGridFragment extends Fragment implements View.OnKeyListener {
|
||||
}
|
||||
|
||||
private void setupQueries() {
|
||||
StdItemQuery query = new StdItemQuery(new ItemFields[]{
|
||||
ItemFields.PrimaryImageAspectRatio,
|
||||
ItemFields.ChildCount,
|
||||
ItemFields.MediaSources,
|
||||
ItemFields.MediaStreams,
|
||||
ItemFields.DisplayPreferencesId
|
||||
});
|
||||
query.setParentId(mParentId.toString());
|
||||
if (mFolder.getType() == BaseItemKind.USER_VIEW || mFolder.getType() == BaseItemKind.COLLECTION_FOLDER) {
|
||||
String type = mFolder.getCollectionType() != null ? mFolder.getCollectionType().toLowerCase() : "";
|
||||
switch (type) {
|
||||
case CollectionType.Movies:
|
||||
query.setIncludeItemTypes(new String[]{"Movie"});
|
||||
query.setRecursive(true);
|
||||
break;
|
||||
case CollectionType.TvShows:
|
||||
query.setIncludeItemTypes(new String[]{"Series"});
|
||||
query.setRecursive(true);
|
||||
break;
|
||||
case CollectionType.BoxSets:
|
||||
query.setIncludeItemTypes(new String[]{"BoxSet"});
|
||||
query.setParentId(null);
|
||||
query.setRecursive(true);
|
||||
break;
|
||||
case CollectionType.Music:
|
||||
//Special queries needed for album artists
|
||||
String includeType = getArguments().getString(Extras.IncludeType);
|
||||
if ("AlbumArtist".equals(includeType)) {
|
||||
AlbumArtistsQuery albumArtists = new AlbumArtistsQuery();
|
||||
albumArtists.setUserId(userRepository.getValue().getCurrentUser().getValue().getId().toString());
|
||||
albumArtists.setFields(new ItemFields[]{
|
||||
ItemFields.PrimaryImageAspectRatio,
|
||||
ItemFields.ItemCounts,
|
||||
ItemFields.ChildCount
|
||||
});
|
||||
albumArtists.setParentId(mParentId.toString());
|
||||
setRowDef(new BrowseRowDef("", albumArtists, CHUNK_SIZE_MINIMUM, new ChangeTriggerType[]{}));
|
||||
return;
|
||||
} else if ("Artist".equals(includeType)) {
|
||||
ArtistsQuery artists = new ArtistsQuery();
|
||||
artists.setUserId(userRepository.getValue().getCurrentUser().getValue().getId().toString());
|
||||
artists.setFields(new ItemFields[]{
|
||||
ItemFields.PrimaryImageAspectRatio,
|
||||
ItemFields.ItemCounts,
|
||||
ItemFields.ChildCount
|
||||
});
|
||||
artists.setParentId(mParentId.toString());
|
||||
setRowDef(new BrowseRowDef("", artists, CHUNK_SIZE_MINIMUM, new ChangeTriggerType[]{}));
|
||||
return;
|
||||
}
|
||||
query.setIncludeItemTypes(new String[]{includeType != null ? includeType : "MusicAlbum"});
|
||||
query.setRecursive(true);
|
||||
break;
|
||||
CollectionType type = mFolder.getCollectionType() != null ? mFolder.getCollectionType() : CollectionType.UNKNOWN;
|
||||
if (type == CollectionType.MUSIC) {
|
||||
//Special queries needed for album artists
|
||||
String includeType = getArguments().getString(Extras.IncludeType, null);
|
||||
if ("AlbumArtist".equals(includeType)) {
|
||||
setRowDef(new BrowseRowDef("", BrowsingUtils.createAlbumArtistsRequest(mParentId), CHUNK_SIZE_MINIMUM, new ChangeTriggerType[]{}));
|
||||
return;
|
||||
} else if ("Artist".equals(includeType)) {
|
||||
setRowDef(new BrowseRowDef("", BrowsingUtils.createArtistsRequest(mParentId), CHUNK_SIZE_MINIMUM, new ChangeTriggerType[]{}));
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setRowDef(new BrowseRowDef("", query, CHUNK_SIZE_MINIMUM, false, true));
|
||||
setRowDef(new BrowseRowDef("", BrowsingUtils.createBrowseGridItemsRequest(mFolder), CHUNK_SIZE_MINIMUM, false, true));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -609,7 +571,6 @@ public class BrowseGridFragment extends Fragment implements View.OnKeyListener {
|
||||
ImageType imageType = libraryPreferences.get(LibraryPreferences.Companion.getImageType());
|
||||
GridDirection gridDirection = libraryPreferences.get(LibraryPreferences.Companion.getGridDirection());
|
||||
|
||||
|
||||
if (mImageType != imageType || mPosterSizeSetting != posterSizeSetting || mGridDirection != gridDirection || mDirty) {
|
||||
determiningPosterSize = true;
|
||||
|
||||
@@ -665,14 +626,8 @@ public class BrowseGridFragment extends Fragment implements View.OnKeyListener {
|
||||
case NextUp:
|
||||
mAdapter = new ItemRowAdapter(requireContext(), mRowDef.getNextUpQuery(), true, mCardPresenter, null);
|
||||
break;
|
||||
case Season:
|
||||
mAdapter = new ItemRowAdapter(requireContext(), mRowDef.getSeasonQuery(), mCardPresenter, null);
|
||||
break;
|
||||
case Upcoming:
|
||||
mAdapter = new ItemRowAdapter(requireContext(), mRowDef.getUpcomingQuery(), mCardPresenter, null);
|
||||
break;
|
||||
case Views:
|
||||
mAdapter = new ItemRowAdapter(requireContext(), ViewQuery.INSTANCE, mCardPresenter, null);
|
||||
mAdapter = new ItemRowAdapter(requireContext(), GetUserViewsRequest.INSTANCE, mCardPresenter, null);
|
||||
break;
|
||||
case SimilarSeries:
|
||||
mAdapter = new ItemRowAdapter(requireContext(), mRowDef.getSimilarQuery(), QueryType.SimilarSeries, mCardPresenter, null);
|
||||
@@ -680,9 +635,6 @@ public class BrowseGridFragment extends Fragment implements View.OnKeyListener {
|
||||
case SimilarMovies:
|
||||
mAdapter = new ItemRowAdapter(requireContext(), mRowDef.getSimilarQuery(), QueryType.SimilarMovies, mCardPresenter, null);
|
||||
break;
|
||||
case Persons:
|
||||
mAdapter = new ItemRowAdapter(requireContext(), mRowDef.getPersonsQuery(), chunkSize, mCardPresenter, null);
|
||||
break;
|
||||
case LiveTvChannel:
|
||||
mAdapter = new ItemRowAdapter(requireContext(), mRowDef.getTvChannelQuery(), 40, mCardPresenter, null);
|
||||
break;
|
||||
@@ -718,7 +670,7 @@ public class BrowseGridFragment extends Fragment implements View.OnKeyListener {
|
||||
setItem(null);
|
||||
updateCounter(mAdapter.getTotalItems() > 0 ? 1 : 0);
|
||||
}
|
||||
mLetterButton.setVisibility(ItemSortBy.SortName.equals(mAdapter.getSortBy()) ? View.VISIBLE : View.GONE);
|
||||
mLetterButton.setVisibility(ItemSortBy.SORT_NAME.equals(mAdapter.getSortBy()) ? View.VISIBLE : View.GONE);
|
||||
if (mAdapter.getItemsLoaded() == 0) {
|
||||
mGridView.setFocusable(false);
|
||||
mHandler.postDelayed(() -> {
|
||||
@@ -754,11 +706,13 @@ public class BrowseGridFragment extends Fragment implements View.OnKeyListener {
|
||||
private ImageButton mLetterButton;
|
||||
|
||||
private void updateDisplayPrefs() {
|
||||
libraryPreferences.set(LibraryPreferences.Companion.getFilterFavoritesOnly(), mAdapter.getFilters().isFavoriteOnly());
|
||||
libraryPreferences.set(LibraryPreferences.Companion.getFilterUnwatchedOnly(), mAdapter.getFilters().isUnwatchedOnly());
|
||||
libraryPreferences.set(LibraryPreferences.Companion.getSortBy(), mAdapter.getSortBy());
|
||||
libraryPreferences.set(LibraryPreferences.Companion.getSortOrder(), getSortOption(mAdapter.getSortBy()).order);
|
||||
CoroutineUtils.runBlocking((coroutineScope, continuation) -> libraryPreferences.commit(continuation));
|
||||
CoroutineUtils.runOnLifecycle(getLifecycle(), (coroutineScope, continuation) -> {
|
||||
libraryPreferences.set(LibraryPreferences.Companion.getFilterFavoritesOnly(), mAdapter.getFilters().isFavoriteOnly());
|
||||
libraryPreferences.set(LibraryPreferences.Companion.getFilterUnwatchedOnly(), mAdapter.getFilters().isUnwatchedOnly());
|
||||
libraryPreferences.set(LibraryPreferences.Companion.getSortBy(), mAdapter.getSortBy());
|
||||
libraryPreferences.set(LibraryPreferences.Companion.getSortOrder(), getSortOption(mAdapter.getSortBy()).order);
|
||||
return libraryPreferences.commit(continuation);
|
||||
});
|
||||
}
|
||||
|
||||
private void addTools() {
|
||||
@@ -774,12 +728,9 @@ public class BrowseGridFragment extends Fragment implements View.OnKeyListener {
|
||||
public void onClick(View v) {
|
||||
//Create sort menu
|
||||
PopupMenu sortMenu = new PopupMenu(getActivity(), binding.toolBar, Gravity.END);
|
||||
for (Integer key : sortOptions.keySet()) {
|
||||
SortOption option = sortOptions.get(key);
|
||||
if (option == null) option = sortOptions.get(0);
|
||||
MenuItem item = sortMenu.getMenu().add(0, key, key, Objects.requireNonNull(option).name);
|
||||
if (option.value.equals(libraryPreferences.get(LibraryPreferences.Companion.getSortBy())))
|
||||
item.setChecked(true);
|
||||
for (Map.Entry<Integer, SortOption> entry : sortOptions.entrySet()) {
|
||||
MenuItem item = sortMenu.getMenu().add(0, entry.getKey(), entry.getKey(), entry.getValue().name);
|
||||
item.setChecked(entry.getValue().value.equals(libraryPreferences.get(LibraryPreferences.Companion.getSortBy())));
|
||||
}
|
||||
sortMenu.getMenu().setGroupCheckable(0, true, true);
|
||||
sortMenu.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() {
|
||||
@@ -928,31 +879,20 @@ public class BrowseGridFragment extends Fragment implements View.OnKeyListener {
|
||||
}
|
||||
|
||||
private void refreshCurrentItem() {
|
||||
if (mCurrentItem != null && mCurrentItem.getBaseItemType() != BaseItemKind.PHOTO && mCurrentItem.getBaseItemType() != BaseItemKind.PHOTO_ALBUM
|
||||
&& mCurrentItem.getBaseItemType() != BaseItemKind.MUSIC_ARTIST && mCurrentItem.getBaseItemType() != BaseItemKind.MUSIC_ALBUM) {
|
||||
Timber.d("Refresh item \"%s\"", mCurrentItem.getFullName(requireContext()));
|
||||
BaseRowItem item = mCurrentItem;
|
||||
item.refresh(new LifecycleAwareResponse<BaseItemDto>(getLifecycle()) {
|
||||
@Override
|
||||
public void onResponse(BaseItemDto response) {
|
||||
if (!getActive()) return;
|
||||
|
||||
if (response == null) mAdapter.removeAt(mAdapter.indexOf(item), 1);
|
||||
else mAdapter.notifyItemRangeChanged(mAdapter.indexOf(item), 1);
|
||||
|
||||
//Now - if filtered make sure we still pass
|
||||
if (response != null && mAdapter.getFilters() != null) {
|
||||
if ((mAdapter.getFilters().isFavoriteOnly() && !item.isFavorite()) || (mAdapter.getFilters().isUnwatchedOnly() && item.isPlayed())) {
|
||||
// if we are about to remove the current item, throw focus to toolbar so framework doesn't crash
|
||||
binding.toolBar.requestFocus();
|
||||
mAdapter.remove(item);
|
||||
mAdapter.setTotalItems(mAdapter.getTotalItems() - 1);
|
||||
updateCounter(item.getIndex());
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
if (mCurrentItem == null) return;
|
||||
Timber.d("Refresh item \"%s\"", mCurrentItem.getFullName(requireContext()));
|
||||
ItemRowAdapterHelperKt.refreshItem(mAdapter, api.getValue(), this, mCurrentItem, () -> {
|
||||
//Now - if filtered make sure we still pass
|
||||
if (mAdapter.getFilters() == null) return null;
|
||||
if ((mAdapter.getFilters().isFavoriteOnly() && !mCurrentItem.isFavorite()) || (mAdapter.getFilters().isUnwatchedOnly() && mCurrentItem.isPlayed())) {
|
||||
// if we are about to remove the current item, throw focus to toolbar so framework doesn't crash
|
||||
binding.toolBar.requestFocus();
|
||||
mAdapter.remove(mCurrentItem);
|
||||
mAdapter.setTotalItems(mAdapter.getTotalItems() - 1);
|
||||
updateCounter(mAdapter.indexOf(mCurrentItem));
|
||||
}
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
private final class ItemViewClickedListener implements OnItemViewClickedListener {
|
||||
@@ -961,7 +901,7 @@ public class BrowseGridFragment extends Fragment implements View.OnKeyListener {
|
||||
RowPresenter.ViewHolder rowViewHolder, Row row) {
|
||||
|
||||
if (!(item instanceof BaseRowItem)) return;
|
||||
ItemLauncher.launch((BaseRowItem) item, mAdapter, ((BaseRowItem) item).getIndex(), requireContext());
|
||||
itemLauncher.getValue().launch((BaseRowItem) item, mAdapter, requireContext());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -992,7 +932,7 @@ public class BrowseGridFragment extends Fragment implements View.OnKeyListener {
|
||||
mHandler.postDelayed(mDelayedSetItem, VIEW_SELECT_UPDATE_DELAY);
|
||||
|
||||
if (!determiningPosterSize)
|
||||
mAdapter.loadMoreItemsIfNeeded(mCurrentItem.getIndex());
|
||||
mAdapter.loadMoreItemsIfNeeded(mAdapter.indexOf(mCurrentItem));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,30 +13,18 @@ import androidx.leanback.widget.Row;
|
||||
import androidx.lifecycle.Lifecycle;
|
||||
|
||||
import org.jellyfin.androidtv.R;
|
||||
import org.jellyfin.androidtv.auth.repository.UserRepository;
|
||||
import org.jellyfin.androidtv.ui.GridButton;
|
||||
import org.jellyfin.androidtv.ui.itemhandling.ItemRowAdapter;
|
||||
import org.jellyfin.androidtv.ui.presentation.GridButtonPresenter;
|
||||
import org.jellyfin.androidtv.ui.presentation.MutableObjectAdapter;
|
||||
import org.jellyfin.androidtv.util.TimeUtils;
|
||||
import org.jellyfin.androidtv.util.Utils;
|
||||
import org.jellyfin.androidtv.util.apiclient.LifecycleAwareResponse;
|
||||
import org.jellyfin.apiclient.interaction.ApiClient;
|
||||
import org.jellyfin.apiclient.model.dto.BaseItemDto;
|
||||
import org.jellyfin.apiclient.model.dto.BaseItemType;
|
||||
import org.jellyfin.apiclient.model.entities.LocationType;
|
||||
import org.jellyfin.apiclient.model.livetv.RecordingQuery;
|
||||
import org.jellyfin.apiclient.model.livetv.TimerInfoDto;
|
||||
import org.jellyfin.apiclient.model.livetv.TimerQuery;
|
||||
import org.jellyfin.apiclient.model.querying.ItemFields;
|
||||
import org.jellyfin.apiclient.model.results.TimerInfoDtoResult;
|
||||
import org.koin.java.KoinJavaComponent;
|
||||
import org.jellyfin.sdk.model.api.BaseItemDto;
|
||||
import org.jellyfin.sdk.model.api.TimerInfoDto;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import timber.log.Timber;
|
||||
|
||||
public class BrowseRecordingsFragment extends EnhancedBrowseFragment {
|
||||
@Override
|
||||
public void onResume() {
|
||||
@@ -54,125 +42,56 @@ public class BrowseRecordingsFragment extends EnhancedBrowseFragment {
|
||||
protected void setupQueries(final RowLoader rowLoader) {
|
||||
showViews = true;
|
||||
//Latest Recordings
|
||||
RecordingQuery recordings = new RecordingQuery();
|
||||
recordings.setFields(new ItemFields[]{
|
||||
ItemFields.Overview,
|
||||
ItemFields.PrimaryImageAspectRatio,
|
||||
ItemFields.ChildCount
|
||||
});
|
||||
recordings.setUserId(KoinJavaComponent.<UserRepository>get(UserRepository.class).getCurrentUser().getValue().getId().toString());
|
||||
recordings.setEnableImages(true);
|
||||
recordings.setLimit(40);
|
||||
mRows.add(new BrowseRowDef(getString(R.string.lbl_recent_recordings), recordings, 40));
|
||||
mRows.add(new BrowseRowDef(getString(R.string.lbl_recent_recordings), BrowsingUtils.createLiveTVRecordingsRequest(40), 40));
|
||||
|
||||
//Movies
|
||||
RecordingQuery movies = new RecordingQuery();
|
||||
movies.setFields(new ItemFields[]{
|
||||
ItemFields.Overview,
|
||||
ItemFields.PrimaryImageAspectRatio,
|
||||
ItemFields.ChildCount
|
||||
});
|
||||
movies.setUserId(KoinJavaComponent.<UserRepository>get(UserRepository.class).getCurrentUser().getValue().getId().toString());
|
||||
movies.setEnableImages(true);
|
||||
movies.setIsMovie(true);
|
||||
BrowseRowDef moviesDef = new BrowseRowDef(getString(R.string.lbl_movies), movies, 60);
|
||||
BrowseRowDef moviesDef = new BrowseRowDef(getString(R.string.lbl_movies), BrowsingUtils.createLiveTVMovieRecordingsRequest(), 60);
|
||||
|
||||
//Shows
|
||||
RecordingQuery shows = new RecordingQuery();
|
||||
shows.setFields(new ItemFields[]{
|
||||
ItemFields.Overview,
|
||||
ItemFields.PrimaryImageAspectRatio,
|
||||
ItemFields.ChildCount
|
||||
});
|
||||
shows.setUserId(KoinJavaComponent.<UserRepository>get(UserRepository.class).getCurrentUser().getValue().getId().toString());
|
||||
shows.setEnableImages(true);
|
||||
shows.setIsSeries(true);
|
||||
BrowseRowDef showsDef = new BrowseRowDef(getString(R.string.lbl_tv_series), shows, 60);
|
||||
BrowseRowDef showsDef = new BrowseRowDef(getString(R.string.lbl_tv_series), BrowsingUtils.createLiveTVSeriesRecordingsRequest(), 60);
|
||||
|
||||
mRows.add(showsDef);
|
||||
mRows.add(moviesDef);
|
||||
|
||||
//Sports
|
||||
RecordingQuery sports = new RecordingQuery();
|
||||
sports.setFields(new ItemFields[]{
|
||||
ItemFields.Overview,
|
||||
ItemFields.PrimaryImageAspectRatio,
|
||||
ItemFields.ChildCount
|
||||
});
|
||||
sports.setUserId(KoinJavaComponent.<UserRepository>get(UserRepository.class).getCurrentUser().getValue().getId().toString());
|
||||
sports.setEnableImages(true);
|
||||
sports.setIsSports(true);
|
||||
mRows.add(new BrowseRowDef(getString(R.string.lbl_sports), sports, 60));
|
||||
mRows.add(new BrowseRowDef(getString(R.string.lbl_sports), BrowsingUtils.createLiveTVSportsRecordingsRequest(), 60));
|
||||
|
||||
//Kids
|
||||
RecordingQuery kids = new RecordingQuery();
|
||||
kids.setFields(new ItemFields[]{
|
||||
ItemFields.Overview,
|
||||
ItemFields.PrimaryImageAspectRatio,
|
||||
ItemFields.ChildCount
|
||||
});
|
||||
kids.setUserId(KoinJavaComponent.<UserRepository>get(UserRepository.class).getCurrentUser().getValue().getId().toString());
|
||||
kids.setEnableImages(true);
|
||||
kids.setIsKids(true);
|
||||
mRows.add(new BrowseRowDef(getString(R.string.lbl_kids), kids, 60));
|
||||
mRows.add(new BrowseRowDef(getString(R.string.lbl_kids), BrowsingUtils.createLiveTVKidsRecordingsRequest(), 60));
|
||||
|
||||
rowLoader.loadRows(mRows);
|
||||
addNext24Timers();
|
||||
}
|
||||
|
||||
private void addNext24Timers() {
|
||||
final TimerQuery scheduled = new TimerQuery();
|
||||
final long ticks24 = 1000 * 60 * 60 * 24;
|
||||
KoinJavaComponent.<ApiClient>get(ApiClient.class).GetLiveTvTimersAsync(scheduled, new LifecycleAwareResponse<TimerInfoDtoResult>(getLifecycle()) {
|
||||
@Override
|
||||
public void onResponse(TimerInfoDtoResult response) {
|
||||
if (!getActive()) return;
|
||||
BrowseViewFragmentHelperKt.getLiveTvTimers(this, timers -> {
|
||||
List<BaseItemDto> nearTimers = new ArrayList<>();
|
||||
LocalDateTime next24 = LocalDateTime.now().plusDays(1);
|
||||
//Get scheduled items for next 24 hours
|
||||
for (TimerInfoDto timer : timers.getItems()) {
|
||||
if (timer.getStartDate().isBefore(next24)) {
|
||||
nearTimers.add(BrowseViewFragmentHelperKt.getTimerProgramInfo(timer));
|
||||
}
|
||||
}
|
||||
if (!nearTimers.isEmpty()) {
|
||||
ItemRowAdapter scheduledAdapter = new ItemRowAdapter(requireContext(), nearTimers, mCardPresenter, mRowsAdapter, true);
|
||||
scheduledAdapter.Retrieve();
|
||||
ListRow scheduleRow = new ListRow(new HeaderItem(getString(R.string.scheduled_in_next_24_hours)), scheduledAdapter);
|
||||
mRowsAdapter.add(0, scheduleRow);
|
||||
new Handler().postDelayed(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (!getLifecycle().getCurrentState().isAtLeast(Lifecycle.State.STARTED))
|
||||
return;
|
||||
|
||||
List<BaseItemDto> nearTimers = new ArrayList<>();
|
||||
long next24 = System.currentTimeMillis() + ticks24;
|
||||
//Get scheduled items for next 24 hours
|
||||
for (TimerInfoDto timer : response.getItems()) {
|
||||
if (TimeUtils.convertToLocalDate(timer.getStartDate()).getTime() <= next24) {
|
||||
BaseItemDto programInfo = timer.getProgramInfo();
|
||||
if (programInfo == null) {
|
||||
programInfo = new BaseItemDto();
|
||||
programInfo.setId(timer.getId());
|
||||
programInfo.setChannelName(timer.getChannelName());
|
||||
programInfo.setName(Utils.getSafeValue(timer.getName(), "Unknown"));
|
||||
Timber.w("No program info for timer %s. Creating one...", programInfo.getName());
|
||||
programInfo.setBaseItemType(BaseItemType.Program);
|
||||
programInfo.setTimerId(timer.getId());
|
||||
programInfo.setSeriesTimerId(timer.getSeriesTimerId());
|
||||
programInfo.setStartDate(timer.getStartDate());
|
||||
programInfo.setEndDate(timer.getEndDate());
|
||||
}
|
||||
programInfo.setLocationType(LocationType.Virtual);
|
||||
nearTimers.add(programInfo);
|
||||
mRowsFragment.setSelectedPosition(0, true);
|
||||
}
|
||||
}
|
||||
if (nearTimers.size() > 0) {
|
||||
ItemRowAdapter scheduledAdapter = new ItemRowAdapter(requireContext(), nearTimers, mCardPresenter, mRowsAdapter, true);
|
||||
scheduledAdapter.Retrieve();
|
||||
ListRow scheduleRow = new ListRow(new HeaderItem("Scheduled in Next 24 Hours"), scheduledAdapter);
|
||||
mRowsAdapter.add(0, scheduleRow);
|
||||
new Handler().postDelayed(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (!getLifecycle().getCurrentState().isAtLeast(Lifecycle.State.STARTED)) return;
|
||||
|
||||
mRowsFragment.setSelectedPosition(0, true);
|
||||
}
|
||||
}, 500);
|
||||
}
|
||||
}, 500);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(Exception exception) {
|
||||
if (!getActive()) return;
|
||||
|
||||
Utils.showToast(getContext(), exception.getLocalizedMessage());
|
||||
}
|
||||
|
||||
return null;
|
||||
}, exception -> {
|
||||
Utils.showToast(getContext(), exception.getLocalizedMessage());
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -2,41 +2,35 @@ package org.jellyfin.androidtv.ui.browsing;
|
||||
|
||||
import org.jellyfin.androidtv.constant.ChangeTriggerType;
|
||||
import org.jellyfin.androidtv.constant.QueryType;
|
||||
import org.jellyfin.androidtv.data.querying.AlbumArtistsQuery;
|
||||
import org.jellyfin.androidtv.data.querying.ViewQuery;
|
||||
import org.jellyfin.apiclient.model.livetv.LiveTvChannelQuery;
|
||||
import org.jellyfin.apiclient.model.livetv.RecommendedProgramQuery;
|
||||
import org.jellyfin.apiclient.model.livetv.RecordingQuery;
|
||||
import org.jellyfin.apiclient.model.livetv.SeriesTimerQuery;
|
||||
import org.jellyfin.apiclient.model.querying.ArtistsQuery;
|
||||
import org.jellyfin.apiclient.model.querying.ItemQuery;
|
||||
import org.jellyfin.apiclient.model.querying.LatestItemsQuery;
|
||||
import org.jellyfin.apiclient.model.querying.NextUpQuery;
|
||||
import org.jellyfin.apiclient.model.querying.PersonsQuery;
|
||||
import org.jellyfin.apiclient.model.querying.SeasonQuery;
|
||||
import org.jellyfin.apiclient.model.querying.SimilarItemsQuery;
|
||||
import org.jellyfin.apiclient.model.querying.UpcomingEpisodesQuery;
|
||||
import org.jellyfin.androidtv.data.querying.GetSeriesTimersRequest;
|
||||
import org.jellyfin.androidtv.data.querying.GetSpecialsRequest;
|
||||
import org.jellyfin.androidtv.data.querying.GetUserViewsRequest;
|
||||
import org.jellyfin.sdk.model.api.request.GetAlbumArtistsRequest;
|
||||
import org.jellyfin.sdk.model.api.request.GetArtistsRequest;
|
||||
import org.jellyfin.sdk.model.api.request.GetItemsRequest;
|
||||
import org.jellyfin.sdk.model.api.request.GetLatestMediaRequest;
|
||||
import org.jellyfin.sdk.model.api.request.GetLiveTvChannelsRequest;
|
||||
import org.jellyfin.sdk.model.api.request.GetNextUpRequest;
|
||||
import org.jellyfin.sdk.model.api.request.GetRecommendedProgramsRequest;
|
||||
import org.jellyfin.sdk.model.api.request.GetRecordingsRequest;
|
||||
import org.jellyfin.sdk.model.api.request.GetResumeItemsRequest;
|
||||
import org.jellyfin.sdk.model.api.request.GetSimilarItemsRequest;
|
||||
|
||||
public class BrowseRowDef {
|
||||
private String headerText;
|
||||
private ItemQuery query;
|
||||
private NextUpQuery nextUpQuery;
|
||||
private UpcomingEpisodesQuery upcomingQuery;
|
||||
private SimilarItemsQuery similarQuery;
|
||||
private LatestItemsQuery latestItemsQuery;
|
||||
private GetItemsRequest query;
|
||||
private GetNextUpRequest nextUpQuery;
|
||||
private GetSimilarItemsRequest similarQuery;
|
||||
private GetLatestMediaRequest latestItemsQuery;
|
||||
private GetLiveTvChannelsRequest tvChannelQuery;
|
||||
private GetRecommendedProgramsRequest programQuery;
|
||||
private GetRecordingsRequest recordingQuery;
|
||||
private GetSeriesTimersRequest seriesTimerQuery;
|
||||
|
||||
private PersonsQuery personsQuery;
|
||||
|
||||
private LiveTvChannelQuery tvChannelQuery;
|
||||
private RecommendedProgramQuery programQuery;
|
||||
private RecordingQuery recordingQuery;
|
||||
private SeriesTimerQuery seriesTimerQuery;
|
||||
|
||||
private ArtistsQuery artistsQuery;
|
||||
private AlbumArtistsQuery albumArtistsQuery;
|
||||
private SeasonQuery seasonQuery;
|
||||
private GetArtistsRequest artistsQuery;
|
||||
private GetAlbumArtistsRequest albumArtistsQuery;
|
||||
private GetResumeItemsRequest resumeQuery;
|
||||
private GetSpecialsRequest specialsQuery;
|
||||
private QueryType queryType;
|
||||
|
||||
private int chunkSize = 0;
|
||||
@@ -45,14 +39,11 @@ public class BrowseRowDef {
|
||||
|
||||
private ChangeTriggerType[] changeTriggers;
|
||||
|
||||
public BrowseRowDef(String header, ItemQuery query, int chunkSize) {
|
||||
this(header, query, chunkSize, false);
|
||||
}
|
||||
public BrowseRowDef(String header, ItemQuery query, int chunkSize, boolean preferParentThumb) {
|
||||
this(header, query, chunkSize, preferParentThumb, false);
|
||||
public BrowseRowDef(String header, GetItemsRequest query, int chunkSize) {
|
||||
this(header, query, chunkSize, false, false);
|
||||
}
|
||||
|
||||
public BrowseRowDef(String header, ItemQuery query, int chunkSize, boolean preferParentThumb, boolean staticHeight) {
|
||||
public BrowseRowDef(String header, GetItemsRequest query, int chunkSize, boolean preferParentThumb, boolean staticHeight) {
|
||||
headerText = header;
|
||||
this.query = query;
|
||||
this.chunkSize = chunkSize;
|
||||
@@ -61,15 +52,15 @@ public class BrowseRowDef {
|
||||
this.queryType = QueryType.Items;
|
||||
}
|
||||
|
||||
public BrowseRowDef(String header, ItemQuery query, int chunkSize, ChangeTriggerType[] changeTriggers) {
|
||||
public BrowseRowDef(String header, GetItemsRequest query, int chunkSize, ChangeTriggerType[] changeTriggers) {
|
||||
this(header, query, chunkSize, false, false, changeTriggers);
|
||||
}
|
||||
|
||||
public BrowseRowDef(String header, ItemQuery query, int chunkSize, boolean preferParentThumb, boolean staticHeight, ChangeTriggerType[] changeTriggers) {
|
||||
public BrowseRowDef(String header, GetItemsRequest query, int chunkSize, boolean preferParentThumb, boolean staticHeight, ChangeTriggerType[] changeTriggers) {
|
||||
this(header,query,chunkSize,preferParentThumb,staticHeight,changeTriggers,QueryType.Items);
|
||||
}
|
||||
|
||||
public BrowseRowDef(String header, ItemQuery query, int chunkSize, boolean preferParentThumb, boolean staticHeight, ChangeTriggerType[] changeTriggers, QueryType queryType) {
|
||||
public BrowseRowDef(String header, GetItemsRequest query, int chunkSize, boolean preferParentThumb, boolean staticHeight, ChangeTriggerType[] changeTriggers, QueryType queryType) {
|
||||
headerText = header;
|
||||
this.query = query;
|
||||
this.chunkSize = chunkSize;
|
||||
@@ -79,7 +70,7 @@ public class BrowseRowDef {
|
||||
this.changeTriggers = changeTriggers;
|
||||
}
|
||||
|
||||
public BrowseRowDef(String header, ArtistsQuery query, int chunkSize, ChangeTriggerType[] changeTriggers) {
|
||||
public BrowseRowDef(String header, GetArtistsRequest query, int chunkSize, ChangeTriggerType[] changeTriggers) {
|
||||
headerText = header;
|
||||
this.artistsQuery = query;
|
||||
this.chunkSize = chunkSize;
|
||||
@@ -87,7 +78,7 @@ public class BrowseRowDef {
|
||||
this.changeTriggers = changeTriggers;
|
||||
}
|
||||
|
||||
public BrowseRowDef(String header, AlbumArtistsQuery query, int chunkSize, ChangeTriggerType[] changeTriggers) {
|
||||
public BrowseRowDef(String header, GetAlbumArtistsRequest query, int chunkSize, ChangeTriggerType[] changeTriggers) {
|
||||
headerText = header;
|
||||
this.albumArtistsQuery = query;
|
||||
this.chunkSize = chunkSize;
|
||||
@@ -95,20 +86,14 @@ public class BrowseRowDef {
|
||||
this.changeTriggers = changeTriggers;
|
||||
}
|
||||
|
||||
public BrowseRowDef(String header, NextUpQuery query) {
|
||||
headerText = header;
|
||||
this.nextUpQuery = query;
|
||||
this.queryType = QueryType.NextUp;
|
||||
}
|
||||
|
||||
public BrowseRowDef(String header, SeriesTimerQuery query) {
|
||||
public BrowseRowDef(String header, GetSeriesTimersRequest query) {
|
||||
headerText = header;
|
||||
this.seriesTimerQuery = query;
|
||||
this.staticHeight = true;
|
||||
this.queryType = QueryType.SeriesTimer;
|
||||
}
|
||||
|
||||
public BrowseRowDef(String header, NextUpQuery query, ChangeTriggerType[] changeTriggers) {
|
||||
public BrowseRowDef(String header, GetNextUpRequest query, ChangeTriggerType[] changeTriggers) {
|
||||
headerText = header;
|
||||
this.nextUpQuery = query;
|
||||
this.queryType = QueryType.NextUp;
|
||||
@@ -116,7 +101,7 @@ public class BrowseRowDef {
|
||||
this.changeTriggers = changeTriggers;
|
||||
}
|
||||
|
||||
public BrowseRowDef(String header, LatestItemsQuery query, ChangeTriggerType[] changeTriggers) {
|
||||
public BrowseRowDef(String header, GetLatestMediaRequest query, ChangeTriggerType[] changeTriggers) {
|
||||
headerText = header;
|
||||
this.latestItemsQuery = query;
|
||||
this.queryType = QueryType.LatestItems;
|
||||
@@ -124,62 +109,36 @@ public class BrowseRowDef {
|
||||
this.changeTriggers = changeTriggers;
|
||||
}
|
||||
|
||||
public BrowseRowDef(String header, SimilarItemsQuery query) {
|
||||
headerText = header;
|
||||
this.similarQuery = query;
|
||||
this.queryType = QueryType.SimilarSeries;
|
||||
}
|
||||
|
||||
public BrowseRowDef(String header, LiveTvChannelQuery query) {
|
||||
public BrowseRowDef(String header, GetLiveTvChannelsRequest query) {
|
||||
headerText = header;
|
||||
this.tvChannelQuery = query;
|
||||
this.queryType = QueryType.LiveTvChannel;
|
||||
}
|
||||
|
||||
public BrowseRowDef(String header, RecommendedProgramQuery query) {
|
||||
public BrowseRowDef(String header, GetRecommendedProgramsRequest query) {
|
||||
headerText = header;
|
||||
this.programQuery = query;
|
||||
this.queryType = QueryType.LiveTvProgram;
|
||||
this.changeTriggers = new ChangeTriggerType[] {ChangeTriggerType.GuideNeedsLoad};
|
||||
}
|
||||
|
||||
public BrowseRowDef(String header, RecordingQuery query) {
|
||||
public BrowseRowDef(String header, GetRecordingsRequest query) {
|
||||
this(header, query, 0);
|
||||
}
|
||||
|
||||
public BrowseRowDef(String header, RecordingQuery query, int chunkSize) {
|
||||
public BrowseRowDef(String header, GetRecordingsRequest query, int chunkSize) {
|
||||
headerText = header;
|
||||
this.recordingQuery = query;
|
||||
this.chunkSize = chunkSize;
|
||||
this.queryType = QueryType.LiveTvRecording;
|
||||
}
|
||||
|
||||
public BrowseRowDef(String header, PersonsQuery query, int chunkSize) {
|
||||
headerText = header;
|
||||
this.personsQuery = query;
|
||||
this.queryType = QueryType.Persons;
|
||||
this.chunkSize = chunkSize;
|
||||
}
|
||||
|
||||
public BrowseRowDef(String header, SimilarItemsQuery query, QueryType type) {
|
||||
public BrowseRowDef(String header, GetSimilarItemsRequest query, QueryType type) {
|
||||
headerText = header;
|
||||
this.similarQuery = query;
|
||||
this.queryType = type;
|
||||
}
|
||||
|
||||
public BrowseRowDef(String header, SeasonQuery query) {
|
||||
headerText = header;
|
||||
this.seasonQuery = query;
|
||||
this.queryType = QueryType.Season;
|
||||
}
|
||||
|
||||
public BrowseRowDef(String header, UpcomingEpisodesQuery query) {
|
||||
headerText = header;
|
||||
this.upcomingQuery = query;
|
||||
this.queryType = QueryType.Upcoming;
|
||||
}
|
||||
|
||||
public BrowseRowDef(String header, ViewQuery query) {
|
||||
public BrowseRowDef(String header, GetUserViewsRequest query) {
|
||||
headerText = header;
|
||||
this.staticHeight = true;
|
||||
this.queryType = QueryType.Views;
|
||||
@@ -195,6 +154,12 @@ public class BrowseRowDef {
|
||||
this.changeTriggers = changeTriggers;
|
||||
}
|
||||
|
||||
public BrowseRowDef(String header, GetSpecialsRequest query) {
|
||||
headerText = header;
|
||||
this.specialsQuery = query;
|
||||
this.queryType = QueryType.Specials;
|
||||
}
|
||||
|
||||
public int getChunkSize() {
|
||||
return chunkSize;
|
||||
}
|
||||
@@ -205,55 +170,43 @@ public class BrowseRowDef {
|
||||
return headerText;
|
||||
}
|
||||
|
||||
public void setHeaderText(String headerText) {
|
||||
this.headerText = headerText;
|
||||
}
|
||||
|
||||
public ItemQuery getQuery() {
|
||||
public GetItemsRequest getQuery() {
|
||||
return query;
|
||||
}
|
||||
|
||||
public NextUpQuery getNextUpQuery() {
|
||||
public GetNextUpRequest getNextUpQuery() {
|
||||
return nextUpQuery;
|
||||
}
|
||||
|
||||
public LatestItemsQuery getLatestItemsQuery() { return latestItemsQuery; }
|
||||
public GetLatestMediaRequest getLatestItemsQuery() { return latestItemsQuery; }
|
||||
|
||||
public SimilarItemsQuery getSimilarQuery() { return similarQuery; }
|
||||
public GetSimilarItemsRequest getSimilarQuery() { return similarQuery; }
|
||||
|
||||
public QueryType getQueryType() {
|
||||
return queryType;
|
||||
}
|
||||
|
||||
public SeasonQuery getSeasonQuery() { return seasonQuery; }
|
||||
|
||||
public UpcomingEpisodesQuery getUpcomingQuery() {
|
||||
return upcomingQuery;
|
||||
}
|
||||
|
||||
public LiveTvChannelQuery getTvChannelQuery() {
|
||||
public GetLiveTvChannelsRequest getTvChannelQuery() {
|
||||
return tvChannelQuery;
|
||||
}
|
||||
|
||||
public RecommendedProgramQuery getProgramQuery() {
|
||||
public GetRecommendedProgramsRequest getProgramQuery() {
|
||||
return programQuery;
|
||||
}
|
||||
|
||||
public RecordingQuery getRecordingQuery() { return recordingQuery; }
|
||||
public GetRecordingsRequest getRecordingQuery() { return recordingQuery; }
|
||||
|
||||
public boolean getPreferParentThumb() { return preferParentThumb; }
|
||||
|
||||
public PersonsQuery getPersonsQuery() {
|
||||
return personsQuery;
|
||||
}
|
||||
public GetArtistsRequest getArtistsQuery() { return artistsQuery; }
|
||||
public GetAlbumArtistsRequest getAlbumArtistsQuery() { return albumArtistsQuery; }
|
||||
|
||||
public ArtistsQuery getArtistsQuery() { return artistsQuery; }
|
||||
public AlbumArtistsQuery getAlbumArtistsQuery() { return albumArtistsQuery; }
|
||||
|
||||
public SeriesTimerQuery getSeriesTimerQuery() { return seriesTimerQuery; }
|
||||
public GetSeriesTimersRequest getSeriesTimerQuery() { return seriesTimerQuery; }
|
||||
|
||||
public GetResumeItemsRequest getResumeQuery() { return resumeQuery; }
|
||||
|
||||
public GetSpecialsRequest getSpecialsQuery() { return specialsQuery; }
|
||||
|
||||
public ChangeTriggerType[] getChangeTriggers() {
|
||||
return changeTriggers;
|
||||
}
|
||||
|
||||
@@ -2,8 +2,6 @@ package org.jellyfin.androidtv.ui.browsing;
|
||||
|
||||
import org.jellyfin.androidtv.ui.livetv.TvManager;
|
||||
import org.jellyfin.androidtv.ui.presentation.CardPresenter;
|
||||
import org.jellyfin.androidtv.util.apiclient.LifecycleAwareResponse;
|
||||
import org.jellyfin.apiclient.model.livetv.TimerQuery;
|
||||
|
||||
public class BrowseScheduleFragment extends EnhancedBrowseFragment {
|
||||
|
||||
@@ -14,7 +12,6 @@ public class BrowseScheduleFragment extends EnhancedBrowseFragment {
|
||||
|
||||
@Override
|
||||
protected void setupQueries(final RowLoader rowLoader) {
|
||||
TvManager.getScheduleRowsAsync(requireContext(), new TimerQuery(), new CardPresenter(true), mRowsAdapter, new LifecycleAwareResponse<Integer>(getLifecycle()) {
|
||||
});
|
||||
TvManager.getScheduleRowsAsync(this, null, new CardPresenter(true), mRowsAdapter);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
package org.jellyfin.androidtv.ui.browsing;
|
||||
|
||||
import static org.koin.java.KoinJavaComponent.inject;
|
||||
|
||||
import androidx.leanback.widget.ArrayObjectAdapter;
|
||||
import androidx.leanback.widget.HeaderItem;
|
||||
import androidx.leanback.widget.ListRow;
|
||||
@@ -10,478 +8,185 @@ import androidx.leanback.widget.Row;
|
||||
import org.jellyfin.androidtv.R;
|
||||
import org.jellyfin.androidtv.auth.repository.UserRepository;
|
||||
import org.jellyfin.androidtv.constant.ChangeTriggerType;
|
||||
import org.jellyfin.androidtv.constant.Extras;
|
||||
import org.jellyfin.androidtv.constant.LiveTvOption;
|
||||
import org.jellyfin.androidtv.constant.QueryType;
|
||||
import org.jellyfin.androidtv.data.querying.StdItemQuery;
|
||||
import org.jellyfin.androidtv.data.querying.GetSeriesTimersRequest;
|
||||
import org.jellyfin.androidtv.preference.UserPreferences;
|
||||
import org.jellyfin.androidtv.ui.GridButton;
|
||||
import org.jellyfin.androidtv.ui.itemhandling.ItemRowAdapter;
|
||||
import org.jellyfin.androidtv.ui.presentation.GridButtonPresenter;
|
||||
import org.jellyfin.androidtv.ui.presentation.MutableObjectAdapter;
|
||||
import org.jellyfin.androidtv.util.TimeUtils;
|
||||
import org.jellyfin.androidtv.util.Utils;
|
||||
import org.jellyfin.androidtv.util.apiclient.LifecycleAwareResponse;
|
||||
import org.jellyfin.apiclient.interaction.ApiClient;
|
||||
import org.jellyfin.apiclient.model.dto.BaseItemDto;
|
||||
import org.jellyfin.apiclient.model.dto.BaseItemType;
|
||||
import org.jellyfin.apiclient.model.entities.LocationType;
|
||||
import org.jellyfin.apiclient.model.entities.SortOrder;
|
||||
import org.jellyfin.apiclient.model.livetv.LiveTvChannelQuery;
|
||||
import org.jellyfin.apiclient.model.livetv.RecommendedProgramQuery;
|
||||
import org.jellyfin.apiclient.model.livetv.RecordingQuery;
|
||||
import org.jellyfin.apiclient.model.livetv.SeriesTimerQuery;
|
||||
import org.jellyfin.apiclient.model.livetv.TimerInfoDto;
|
||||
import org.jellyfin.apiclient.model.livetv.TimerQuery;
|
||||
import org.jellyfin.apiclient.model.querying.ItemFields;
|
||||
import org.jellyfin.apiclient.model.querying.ItemFilter;
|
||||
import org.jellyfin.apiclient.model.querying.ItemQuery;
|
||||
import org.jellyfin.apiclient.model.querying.ItemsResult;
|
||||
import org.jellyfin.apiclient.model.querying.LatestItemsQuery;
|
||||
import org.jellyfin.apiclient.model.querying.NextUpQuery;
|
||||
import org.jellyfin.apiclient.model.results.TimerInfoDtoResult;
|
||||
import org.jellyfin.sdk.model.constant.CollectionType;
|
||||
import org.jellyfin.sdk.model.constant.ItemSortBy;
|
||||
import org.jellyfin.sdk.model.api.BaseItemDto;
|
||||
import org.jellyfin.sdk.model.api.BaseItemKind;
|
||||
import org.jellyfin.sdk.model.api.CollectionType;
|
||||
import org.jellyfin.sdk.model.api.TimerInfoDto;
|
||||
import org.jellyfin.sdk.model.api.request.GetNextUpRequest;
|
||||
import org.koin.java.KoinJavaComponent;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
import kotlin.Lazy;
|
||||
import timber.log.Timber;
|
||||
|
||||
public class BrowseViewFragment extends EnhancedBrowseFragment {
|
||||
private boolean isLiveTvLibrary;
|
||||
|
||||
private Lazy<ApiClient> apiClient = inject(ApiClient.class);
|
||||
|
||||
@Override
|
||||
protected void setupQueries(final RowLoader rowLoader) {
|
||||
String type = mFolder.getCollectionType() != null ? mFolder.getCollectionType().toLowerCase() : "";
|
||||
CollectionType type = mFolder != null && mFolder.getCollectionType() != null ? mFolder.getCollectionType() : CollectionType.UNKNOWN;
|
||||
switch (type) {
|
||||
case CollectionType.Movies:
|
||||
itemTypeString = "Movie";
|
||||
case MOVIES:
|
||||
itemType = BaseItemKind.MOVIE;
|
||||
|
||||
//Resume
|
||||
StdItemQuery resumeMovies = new StdItemQuery(new ItemFields[]{
|
||||
ItemFields.PrimaryImageAspectRatio,
|
||||
ItemFields.Overview,
|
||||
ItemFields.ItemCounts,
|
||||
ItemFields.DisplayPreferencesId,
|
||||
ItemFields.ChildCount,
|
||||
ItemFields.MediaStreams,
|
||||
ItemFields.MediaSources
|
||||
});
|
||||
resumeMovies.setIncludeItemTypes(new String[]{"Movie"});
|
||||
resumeMovies.setRecursive(true);
|
||||
resumeMovies.setParentId(mFolder.getId().toString());
|
||||
resumeMovies.setImageTypeLimit(1);
|
||||
resumeMovies.setLimit(50);
|
||||
resumeMovies.setCollapseBoxSetItems(false);
|
||||
resumeMovies.setEnableTotalRecordCount(false);
|
||||
resumeMovies.setFilters(new ItemFilter[]{ItemFilter.IsResumable});
|
||||
resumeMovies.setSortBy(new String[]{ItemSortBy.DatePlayed});
|
||||
resumeMovies.setSortOrder(SortOrder.Descending);
|
||||
mRows.add(new BrowseRowDef(getString(R.string.lbl_continue_watching), resumeMovies, 0, new ChangeTriggerType[]{ChangeTriggerType.MoviePlayback}));
|
||||
mRows.add(new BrowseRowDef(getString(R.string.lbl_continue_watching), BrowsingUtils.createResumeItemsRequest(mFolder.getId(), BaseItemKind.MOVIE), 0, new ChangeTriggerType[]{ChangeTriggerType.MoviePlayback}));
|
||||
|
||||
//Latest
|
||||
LatestItemsQuery latestMovies = new LatestItemsQuery();
|
||||
latestMovies.setFields(new ItemFields[]{
|
||||
ItemFields.PrimaryImageAspectRatio,
|
||||
ItemFields.Overview,
|
||||
ItemFields.ChildCount,
|
||||
ItemFields.MediaSources,
|
||||
ItemFields.MediaStreams
|
||||
});
|
||||
latestMovies.setParentId(mFolder.getId().toString());
|
||||
latestMovies.setLimit(50);
|
||||
latestMovies.setImageTypeLimit(1);
|
||||
mRows.add(new BrowseRowDef(getString(R.string.lbl_latest), latestMovies, new ChangeTriggerType[]{ChangeTriggerType.MoviePlayback, ChangeTriggerType.LibraryUpdated}));
|
||||
mRows.add(new BrowseRowDef(getString(R.string.lbl_latest), BrowsingUtils.createLatestMediaRequest(mFolder.getId()), new ChangeTriggerType[]{ChangeTriggerType.MoviePlayback, ChangeTriggerType.LibraryUpdated}));
|
||||
|
||||
//Favorites
|
||||
StdItemQuery favorites = new StdItemQuery(new ItemFields[]{
|
||||
ItemFields.PrimaryImageAspectRatio,
|
||||
ItemFields.Overview,
|
||||
ItemFields.ItemCounts,
|
||||
ItemFields.DisplayPreferencesId,
|
||||
ItemFields.ChildCount,
|
||||
ItemFields.MediaStreams,
|
||||
ItemFields.MediaSources
|
||||
});
|
||||
favorites.setIncludeItemTypes(new String[]{"Movie"});
|
||||
favorites.setRecursive(true);
|
||||
favorites.setParentId(mFolder.getId().toString());
|
||||
favorites.setImageTypeLimit(1);
|
||||
favorites.setFilters(new ItemFilter[]{ItemFilter.IsFavorite});
|
||||
favorites.setSortBy(new String[]{ItemSortBy.SortName});
|
||||
mRows.add(new BrowseRowDef(getString(R.string.lbl_favorites), favorites, 60, new ChangeTriggerType[]{ChangeTriggerType.LibraryUpdated, ChangeTriggerType.FavoriteUpdate}));
|
||||
mRows.add(new BrowseRowDef(getString(R.string.lbl_favorites), BrowsingUtils.createFavoriteItemsRequest(mFolder.getId(), BaseItemKind.MOVIE), 60, new ChangeTriggerType[]{ChangeTriggerType.LibraryUpdated, ChangeTriggerType.FavoriteUpdate}));
|
||||
|
||||
//Collections
|
||||
StdItemQuery collections = new StdItemQuery();
|
||||
collections.setFields(new ItemFields[]{
|
||||
ItemFields.ChildCount
|
||||
});
|
||||
collections.setIncludeItemTypes(new String[]{"BoxSet"});
|
||||
collections.setRecursive(true);
|
||||
collections.setImageTypeLimit(1);
|
||||
//collections.setParentId(mFolder.getId());
|
||||
collections.setSortBy(new String[]{ItemSortBy.SortName});
|
||||
mRows.add(new BrowseRowDef(getString(R.string.lbl_collections), collections, 60, new ChangeTriggerType[]{ChangeTriggerType.LibraryUpdated}));
|
||||
mRows.add(new BrowseRowDef(getString(R.string.lbl_collections), BrowsingUtils.createCollectionsRequest(mFolder.getId()), 60, new ChangeTriggerType[]{ChangeTriggerType.LibraryUpdated}));
|
||||
|
||||
rowLoader.loadRows(mRows);
|
||||
break;
|
||||
case CollectionType.TvShows:
|
||||
itemTypeString = "Series";
|
||||
case TVSHOWS:
|
||||
itemType = BaseItemKind.SERIES;
|
||||
|
||||
//Resume
|
||||
StdItemQuery resumeEpisodes = new StdItemQuery(new ItemFields[]{
|
||||
ItemFields.PrimaryImageAspectRatio,
|
||||
ItemFields.Overview,
|
||||
ItemFields.ChildCount,
|
||||
ItemFields.MediaSources,
|
||||
ItemFields.MediaStreams
|
||||
});
|
||||
resumeEpisodes.setIncludeItemTypes(new String[]{"Episode"});
|
||||
resumeEpisodes.setLimit(50);
|
||||
resumeEpisodes.setParentId(mFolder.getId().toString());
|
||||
resumeEpisodes.setRecursive(true);
|
||||
resumeEpisodes.setImageTypeLimit(1);
|
||||
resumeEpisodes.setFilters(new ItemFilter[]{ItemFilter.IsResumable});
|
||||
resumeEpisodes.setSortBy(new String[]{ItemSortBy.DatePlayed});
|
||||
resumeEpisodes.setSortOrder(SortOrder.Descending);
|
||||
mRows.add(new BrowseRowDef(getString(R.string.lbl_continue_watching), resumeEpisodes, 0, new ChangeTriggerType[]{ChangeTriggerType.TvPlayback}));
|
||||
mRows.add(new BrowseRowDef(getString(R.string.lbl_continue_watching), BrowsingUtils.createResumeItemsRequest(mFolder.getId(), BaseItemKind.EPISODE), 0, new ChangeTriggerType[]{ChangeTriggerType.TvPlayback}));
|
||||
|
||||
//Next up
|
||||
NextUpQuery nextUpQuery = new NextUpQuery();
|
||||
nextUpQuery.setUserId(KoinJavaComponent.<UserRepository>get(UserRepository.class).getCurrentUser().getValue().getId().toString());
|
||||
nextUpQuery.setLimit(50);
|
||||
nextUpQuery.setParentId(mFolder.getId().toString());
|
||||
nextUpQuery.setImageTypeLimit(1);
|
||||
nextUpQuery.setFields(new ItemFields[]{
|
||||
ItemFields.PrimaryImageAspectRatio,
|
||||
ItemFields.Overview,
|
||||
ItemFields.ChildCount,
|
||||
ItemFields.MediaSources,
|
||||
ItemFields.MediaStreams
|
||||
});
|
||||
mRows.add(new BrowseRowDef(getString(R.string.lbl_next_up), nextUpQuery, new ChangeTriggerType[]{ChangeTriggerType.TvPlayback}));
|
||||
GetNextUpRequest getNextUpRequest = BrowsingUtils.createGetNextUpRequest(mFolder.getId());
|
||||
mRows.add(new BrowseRowDef(getString(R.string.lbl_next_up), getNextUpRequest, new ChangeTriggerType[]{ChangeTriggerType.TvPlayback}));
|
||||
|
||||
//Premieres
|
||||
if (KoinJavaComponent.<UserPreferences>get(UserPreferences.class).get(UserPreferences.Companion.getPremieresEnabled())) {
|
||||
StdItemQuery newQuery = new StdItemQuery(new ItemFields[]{
|
||||
ItemFields.DateCreated,
|
||||
ItemFields.PrimaryImageAspectRatio,
|
||||
ItemFields.Overview,
|
||||
ItemFields.ChildCount
|
||||
});
|
||||
newQuery.setUserId(KoinJavaComponent.<UserRepository>get(UserRepository.class).getCurrentUser().getValue().getId().toString());
|
||||
newQuery.setIncludeItemTypes(new String[]{"Episode"});
|
||||
newQuery.setParentId(mFolder.getId().toString());
|
||||
newQuery.setRecursive(true);
|
||||
newQuery.setIsVirtualUnaired(false);
|
||||
newQuery.setIsMissing(false);
|
||||
newQuery.setImageTypeLimit(1);
|
||||
newQuery.setFilters(new ItemFilter[]{ItemFilter.IsUnplayed});
|
||||
newQuery.setSortBy(new String[]{ItemSortBy.DateCreated});
|
||||
newQuery.setSortOrder(SortOrder.Descending);
|
||||
newQuery.setEnableTotalRecordCount(false);
|
||||
newQuery.setLimit(300);
|
||||
mRows.add(new BrowseRowDef(getString(R.string.lbl_new_premieres), newQuery, 0, true, true, new ChangeTriggerType[]{ChangeTriggerType.TvPlayback}, QueryType.Premieres));
|
||||
mRows.add(new BrowseRowDef(getString(R.string.lbl_new_premieres), BrowsingUtils.createPremieresRequest(mFolder.getId()), 0, true, true, new ChangeTriggerType[]{ChangeTriggerType.TvPlayback}, QueryType.Premieres));
|
||||
}
|
||||
|
||||
//Latest content added
|
||||
LatestItemsQuery latestSeries = new LatestItemsQuery();
|
||||
latestSeries.setFields(new ItemFields[]{
|
||||
ItemFields.PrimaryImageAspectRatio,
|
||||
ItemFields.Overview,
|
||||
ItemFields.ChildCount,
|
||||
ItemFields.MediaSources,
|
||||
ItemFields.MediaStreams
|
||||
});
|
||||
latestSeries.setIncludeItemTypes(new String[]{"Episode"});
|
||||
latestSeries.setGroupItems(true);
|
||||
latestSeries.setParentId(mFolder.getId().toString());
|
||||
latestSeries.setLimit(50);
|
||||
latestSeries.setImageTypeLimit(1);
|
||||
mRows.add(new BrowseRowDef(getString(R.string.lbl_latest), latestSeries, new ChangeTriggerType[]{ChangeTriggerType.LibraryUpdated}));
|
||||
mRows.add(new BrowseRowDef(getString(R.string.lbl_latest), BrowsingUtils.createLatestMediaRequest(mFolder.getId(), BaseItemKind.EPISODE, true), new ChangeTriggerType[]{ChangeTriggerType.LibraryUpdated}));
|
||||
|
||||
//Favorites
|
||||
StdItemQuery tvFavorites = new StdItemQuery();
|
||||
tvFavorites.setIncludeItemTypes(new String[]{"Series"});
|
||||
tvFavorites.setRecursive(true);
|
||||
tvFavorites.setParentId(mFolder.getId().toString());
|
||||
tvFavorites.setImageTypeLimit(1);
|
||||
tvFavorites.setFilters(new ItemFilter[]{ItemFilter.IsFavorite});
|
||||
tvFavorites.setSortBy(new String[]{ItemSortBy.SortName});
|
||||
mRows.add(new BrowseRowDef(getString(R.string.lbl_favorites), tvFavorites, 60, new ChangeTriggerType[]{ChangeTriggerType.LibraryUpdated, ChangeTriggerType.FavoriteUpdate}));
|
||||
mRows.add(new BrowseRowDef(getString(R.string.lbl_favorites), BrowsingUtils.createFavoriteItemsRequest(mFolder.getId(), BaseItemKind.SERIES), 60, new ChangeTriggerType[]{ChangeTriggerType.LibraryUpdated, ChangeTriggerType.FavoriteUpdate}));
|
||||
|
||||
rowLoader.loadRows(mRows);
|
||||
break;
|
||||
case CollectionType.Music:
|
||||
case MUSIC:
|
||||
//Latest
|
||||
LatestItemsQuery latestAlbums = new LatestItemsQuery();
|
||||
latestAlbums.setFields(new ItemFields[]{
|
||||
ItemFields.PrimaryImageAspectRatio,
|
||||
ItemFields.Overview,
|
||||
ItemFields.ChildCount
|
||||
});
|
||||
latestAlbums.setIncludeItemTypes(new String[]{"Audio"});
|
||||
latestAlbums.setGroupItems(true);
|
||||
latestAlbums.setImageTypeLimit(1);
|
||||
latestAlbums.setParentId(mFolder.getId().toString());
|
||||
latestAlbums.setLimit(50);
|
||||
mRows.add(new BrowseRowDef(getString(R.string.lbl_latest), latestAlbums, new ChangeTriggerType[]{ChangeTriggerType.LibraryUpdated}));
|
||||
mRows.add(new BrowseRowDef(getString(R.string.lbl_latest), BrowsingUtils.createLatestMediaRequest(mFolder.getId(), BaseItemKind.AUDIO, true), new ChangeTriggerType[]{ChangeTriggerType.LibraryUpdated}));
|
||||
|
||||
//Last Played
|
||||
StdItemQuery lastPlayed = new StdItemQuery();
|
||||
lastPlayed.setIncludeItemTypes(new String[]{"Audio"});
|
||||
lastPlayed.setRecursive(true);
|
||||
lastPlayed.setParentId(mFolder.getId().toString());
|
||||
lastPlayed.setImageTypeLimit(1);
|
||||
lastPlayed.setFilters(new ItemFilter[]{ItemFilter.IsPlayed});
|
||||
lastPlayed.setSortBy(new String[]{ItemSortBy.DatePlayed});
|
||||
lastPlayed.setSortOrder(SortOrder.Descending);
|
||||
lastPlayed.setEnableTotalRecordCount(false);
|
||||
lastPlayed.setLimit(50);
|
||||
mRows.add(new BrowseRowDef(getString(R.string.lbl_last_played), lastPlayed, 0, false, true, new ChangeTriggerType[]{ChangeTriggerType.MusicPlayback, ChangeTriggerType.LibraryUpdated}));
|
||||
mRows.add(new BrowseRowDef(getString(R.string.lbl_last_played), BrowsingUtils.createLastPlayedRequest(mFolder.getId()), 0, false, true, new ChangeTriggerType[]{ChangeTriggerType.MusicPlayback, ChangeTriggerType.LibraryUpdated}));
|
||||
|
||||
//Favorites
|
||||
StdItemQuery favAlbums = new StdItemQuery();
|
||||
favAlbums.setIncludeItemTypes(new String[]{"MusicAlbum", "MusicArtist"});
|
||||
favAlbums.setRecursive(true);
|
||||
favAlbums.setParentId(mFolder.getId().toString());
|
||||
favAlbums.setImageTypeLimit(1);
|
||||
favAlbums.setFilters(new ItemFilter[]{ItemFilter.IsFavorite});
|
||||
favAlbums.setSortBy(new String[]{ItemSortBy.SortName});
|
||||
mRows.add(new BrowseRowDef(getString(R.string.lbl_favorites), favAlbums, 60, false, true, new ChangeTriggerType[]{ChangeTriggerType.LibraryUpdated, ChangeTriggerType.FavoriteUpdate}));
|
||||
mRows.add(new BrowseRowDef(getString(R.string.lbl_favorites), BrowsingUtils.createFavoriteItemsRequest(mFolder.getId(), BaseItemKind.MUSIC_ALBUM), 60, false, true, new ChangeTriggerType[]{ChangeTriggerType.LibraryUpdated, ChangeTriggerType.FavoriteUpdate}));
|
||||
|
||||
//AudioPlaylists
|
||||
StdItemQuery playlists = new StdItemQuery(new ItemFields[]{
|
||||
ItemFields.PrimaryImageAspectRatio,
|
||||
ItemFields.CumulativeRunTimeTicks,
|
||||
ItemFields.ChildCount
|
||||
});
|
||||
playlists.setIncludeItemTypes(new String[]{"Playlist"});
|
||||
playlists.setImageTypeLimit(1);
|
||||
playlists.setRecursive(true);
|
||||
playlists.setSortBy(new String[]{ItemSortBy.DateCreated});
|
||||
playlists.setSortOrder(SortOrder.Descending);
|
||||
mRows.add(new BrowseRowDef(getString(R.string.lbl_playlists), playlists, 60, false, true, new ChangeTriggerType[]{ChangeTriggerType.LibraryUpdated}, QueryType.AudioPlaylists));
|
||||
mRows.add(new BrowseRowDef(getString(R.string.lbl_playlists), BrowsingUtils.createPlaylistsRequest(), 60, false, true, new ChangeTriggerType[]{ChangeTriggerType.LibraryUpdated}, QueryType.AudioPlaylists));
|
||||
|
||||
rowLoader.loadRows(mRows);
|
||||
break;
|
||||
case CollectionType.LiveTv:
|
||||
case LIVETV:
|
||||
isLiveTvLibrary = true;
|
||||
showViews = true;
|
||||
|
||||
//On now
|
||||
RecommendedProgramQuery onNow = new RecommendedProgramQuery();
|
||||
onNow.setIsAiring(true);
|
||||
onNow.setFields(new ItemFields[]{
|
||||
ItemFields.Overview,
|
||||
ItemFields.PrimaryImageAspectRatio,
|
||||
ItemFields.ChannelInfo,
|
||||
ItemFields.ChildCount
|
||||
});
|
||||
onNow.setUserId(KoinJavaComponent.<UserRepository>get(UserRepository.class).getCurrentUser().getValue().getId().toString());
|
||||
onNow.setImageTypeLimit(1);
|
||||
onNow.setEnableTotalRecordCount(false);
|
||||
onNow.setLimit(150);
|
||||
mRows.add(new BrowseRowDef(getString(R.string.lbl_on_now), onNow));
|
||||
mRows.add(new BrowseRowDef(getString(R.string.lbl_on_now), BrowsingUtils.createLiveTVOnNowRequest()));
|
||||
|
||||
//Upcoming
|
||||
RecommendedProgramQuery upcomingTv = new RecommendedProgramQuery();
|
||||
upcomingTv.setUserId(KoinJavaComponent.<UserRepository>get(UserRepository.class).getCurrentUser().getValue().getId().toString());
|
||||
upcomingTv.setFields(new ItemFields[]{
|
||||
ItemFields.Overview,
|
||||
ItemFields.PrimaryImageAspectRatio,
|
||||
ItemFields.ChannelInfo,
|
||||
ItemFields.ChildCount
|
||||
});
|
||||
upcomingTv.setIsAiring(false);
|
||||
upcomingTv.setHasAired(false);
|
||||
upcomingTv.setImageTypeLimit(1);
|
||||
upcomingTv.setEnableTotalRecordCount(false);
|
||||
upcomingTv.setLimit(150);
|
||||
mRows.add(new BrowseRowDef(getString(R.string.lbl_coming_up), upcomingTv));
|
||||
mRows.add(new BrowseRowDef(getString(R.string.lbl_coming_up), BrowsingUtils.createLiveTVUpcomingRequest()));
|
||||
|
||||
//Fav Channels
|
||||
LiveTvChannelQuery favTv = new LiveTvChannelQuery();
|
||||
favTv.setUserId(KoinJavaComponent.<UserRepository>get(UserRepository.class).getCurrentUser().getValue().getId().toString());
|
||||
favTv.setEnableFavoriteSorting(true);
|
||||
favTv.setIsFavorite(true);
|
||||
mRows.add(new BrowseRowDef(getString(R.string.lbl_favorite_channels), favTv));
|
||||
mRows.add(new BrowseRowDef(getString(R.string.lbl_favorite_channels), BrowsingUtils.createLiveTVChannelsRequest(true)));
|
||||
|
||||
//Other Channels
|
||||
LiveTvChannelQuery otherTv = new LiveTvChannelQuery();
|
||||
otherTv.setUserId(KoinJavaComponent.<UserRepository>get(UserRepository.class).getCurrentUser().getValue().getId().toString());
|
||||
otherTv.setIsFavorite(false);
|
||||
mRows.add(new BrowseRowDef(getString(R.string.lbl_other_channels), otherTv));
|
||||
mRows.add(new BrowseRowDef(getString(R.string.lbl_other_channels), BrowsingUtils.createLiveTVChannelsRequest(false)));
|
||||
|
||||
//Latest Recordings
|
||||
RecordingQuery recordings = new RecordingQuery();
|
||||
recordings.setFields(new ItemFields[]{
|
||||
ItemFields.Overview,
|
||||
ItemFields.PrimaryImageAspectRatio,
|
||||
ItemFields.ChildCount
|
||||
});
|
||||
recordings.setUserId(KoinJavaComponent.<UserRepository>get(UserRepository.class).getCurrentUser().getValue().getId().toString());
|
||||
recordings.setEnableImages(true);
|
||||
recordings.setLimit(40);
|
||||
|
||||
//Do a straight query and then split the returned items into logical groups
|
||||
apiClient.getValue().GetLiveTvRecordingsAsync(recordings, new LifecycleAwareResponse<ItemsResult>(getLifecycle()) {
|
||||
@Override
|
||||
public void onResponse(ItemsResult response) {
|
||||
if (!getActive()) return;
|
||||
|
||||
final ItemsResult recordingsResponse = response;
|
||||
final long ticks24 = 1000 * 60 * 60 * 24;
|
||||
|
||||
// Also get scheduled recordings for next 24 hours
|
||||
final TimerQuery scheduled = new TimerQuery();
|
||||
apiClient.getValue().GetLiveTvTimersAsync(scheduled, new LifecycleAwareResponse<TimerInfoDtoResult>(getLifecycle()) {
|
||||
@Override
|
||||
public void onResponse(TimerInfoDtoResult response) {
|
||||
if (!getActive()) return;
|
||||
|
||||
List<BaseItemDto> nearTimers = new ArrayList<>();
|
||||
long next24 = System.currentTimeMillis() + ticks24;
|
||||
//Get scheduled items for next 24 hours
|
||||
for (TimerInfoDto timer : response.getItems()) {
|
||||
if (TimeUtils.convertToLocalDate(timer.getStartDate()).getTime() <= next24) {
|
||||
BaseItemDto programInfo = timer.getProgramInfo();
|
||||
if (programInfo == null) {
|
||||
programInfo = new BaseItemDto();
|
||||
programInfo.setId(timer.getId());
|
||||
programInfo.setChannelName(timer.getChannelName());
|
||||
programInfo.setName(Utils.getSafeValue(timer.getName(), "Unknown"));
|
||||
Timber.w("No program info for timer %s. Creating one...", programInfo.getName());
|
||||
programInfo.setBaseItemType(BaseItemType.Program);
|
||||
programInfo.setTimerId(timer.getId());
|
||||
programInfo.setSeriesTimerId(timer.getSeriesTimerId());
|
||||
programInfo.setStartDate(timer.getStartDate());
|
||||
programInfo.setEndDate(timer.getEndDate());
|
||||
}
|
||||
programInfo.setLocationType(LocationType.Virtual);
|
||||
nearTimers.add(programInfo);
|
||||
}
|
||||
}
|
||||
|
||||
if (recordingsResponse.getTotalRecordCount() > 0) {
|
||||
List<BaseItemDto> dayItems = new ArrayList<>();
|
||||
List<BaseItemDto> weekItems = new ArrayList<>();
|
||||
|
||||
long past24 = System.currentTimeMillis() - ticks24;
|
||||
long pastWeek = System.currentTimeMillis() - (ticks24 * 7);
|
||||
for (BaseItemDto item : recordingsResponse.getItems()) {
|
||||
if (item.getDateCreated() != null) {
|
||||
if (TimeUtils.convertToLocalDate(item.getDateCreated()).getTime() >= past24) {
|
||||
dayItems.add(item);
|
||||
} else if (TimeUtils.convertToLocalDate(item.getDateCreated()).getTime() >= pastWeek) {
|
||||
weekItems.add(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//First put all recordings in and retrieve
|
||||
//All Recordings
|
||||
RecordingQuery recordings = new RecordingQuery();
|
||||
recordings.setFields(new ItemFields[]{
|
||||
ItemFields.Overview,
|
||||
ItemFields.PrimaryImageAspectRatio,
|
||||
ItemFields.ChildCount
|
||||
});
|
||||
recordings.setUserId(KoinJavaComponent.<UserRepository>get(UserRepository.class).getCurrentUser().getValue().getId().toString());
|
||||
recordings.setEnableImages(true);
|
||||
mRows.add(new BrowseRowDef(getString(R.string.lbl_recent_recordings), recordings, 50));
|
||||
rowLoader.loadRows(mRows);
|
||||
|
||||
//Now insert our smart rows
|
||||
if (weekItems.size() > 0) {
|
||||
ItemRowAdapter weekAdapter = new ItemRowAdapter(requireContext(), weekItems, mCardPresenter, mRowsAdapter, true);
|
||||
weekAdapter.Retrieve();
|
||||
ListRow weekRow = new ListRow(new HeaderItem("Past Week"), weekAdapter);
|
||||
mRowsAdapter.add(0, weekRow);
|
||||
}
|
||||
if (nearTimers.size() > 0) {
|
||||
ItemRowAdapter scheduledAdapter = new ItemRowAdapter(requireContext(), nearTimers, mCardPresenter, mRowsAdapter, true);
|
||||
scheduledAdapter.Retrieve();
|
||||
ListRow scheduleRow = new ListRow(new HeaderItem("Scheduled in Next 24 Hours"), scheduledAdapter);
|
||||
mRowsAdapter.add(0, scheduleRow);
|
||||
}
|
||||
if (dayItems.size() > 0) {
|
||||
ItemRowAdapter dayAdapter = new ItemRowAdapter(requireContext(), dayItems, mCardPresenter, mRowsAdapter, true);
|
||||
dayAdapter.Retrieve();
|
||||
ListRow dayRow = new ListRow(new HeaderItem("Past 24 Hours"), dayAdapter);
|
||||
mRowsAdapter.add(0, dayRow);
|
||||
}
|
||||
|
||||
} else {
|
||||
// no recordings
|
||||
rowLoader.loadRows(mRows);
|
||||
if (nearTimers.size() > 0) {
|
||||
ItemRowAdapter scheduledAdapter = new ItemRowAdapter(requireContext(), nearTimers, mCardPresenter, mRowsAdapter, true);
|
||||
scheduledAdapter.Retrieve();
|
||||
ListRow scheduleRow = new ListRow(new HeaderItem("Scheduled in Next 24 Hours"), scheduledAdapter);
|
||||
mRowsAdapter.add(0, scheduleRow);
|
||||
} else {
|
||||
mTitle.setText(R.string.lbl_no_recordings);
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
BrowseViewFragmentHelperKt.getLiveTvRecordingsAndTimers(this, (recordings, timers) -> {
|
||||
List<BaseItemDto> nearTimers = new ArrayList<>();
|
||||
LocalDateTime next24 = LocalDateTime.now().plusDays(1);
|
||||
//Get scheduled items for next 24 hours
|
||||
for (TimerInfoDto timer : timers.getItems()) {
|
||||
if (timer.getStartDate().isBefore(next24)) {
|
||||
nearTimers.add(BrowseViewFragmentHelperKt.getTimerProgramInfo(timer));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(Exception exception) {
|
||||
if (!getActive()) return;
|
||||
if (recordings.getTotalRecordCount() > 0) {
|
||||
List<BaseItemDto> dayItems = new ArrayList<>();
|
||||
List<BaseItemDto> weekItems = new ArrayList<>();
|
||||
|
||||
Utils.showToast(getContext(), exception.getLocalizedMessage());
|
||||
}
|
||||
});
|
||||
|
||||
break;
|
||||
|
||||
case "seriestimers":
|
||||
mRows.add(new BrowseRowDef(getString(R.string.lbl_series_recordings), new SeriesTimerQuery()));
|
||||
rowLoader.loadRows(mRows);
|
||||
break;
|
||||
default:
|
||||
// Fall back to rows defined by the view children
|
||||
final List<BrowseRowDef> rows = new ArrayList<>();
|
||||
final UUID userId = KoinJavaComponent.<UserRepository>get(UserRepository.class).getCurrentUser().getValue().getId();
|
||||
|
||||
ItemQuery query = new ItemQuery();
|
||||
query.setParentId(mFolder.getId().toString());
|
||||
query.setUserId(userId.toString());
|
||||
query.setImageTypeLimit(1);
|
||||
query.setSortBy(new String[]{ItemSortBy.SortName});
|
||||
|
||||
apiClient.getValue().GetItemsAsync(query, new LifecycleAwareResponse<ItemsResult>(getLifecycle()) {
|
||||
@Override
|
||||
public void onResponse(ItemsResult response) {
|
||||
if (!getActive()) return;
|
||||
|
||||
if (response.getTotalRecordCount() > 0) {
|
||||
for (BaseItemDto item : response.getItems()) {
|
||||
ItemQuery rowQuery = new StdItemQuery();
|
||||
rowQuery.setParentId(item.getId());
|
||||
rowQuery.setUserId(userId.toString());
|
||||
rows.add(new BrowseRowDef(item.getName(), rowQuery, 60, new ChangeTriggerType[]{ChangeTriggerType.LibraryUpdated}));
|
||||
LocalDateTime past24 = LocalDateTime.now().minusDays(1);
|
||||
LocalDateTime pastWeek = LocalDateTime.now().minusWeeks(1);
|
||||
for (BaseItemDto item : recordings.getItems()) {
|
||||
if (item.getDateCreated() != null) {
|
||||
if (item.getDateCreated().isAfter(past24)) {
|
||||
dayItems.add(item);
|
||||
} else if (item.getDateCreated().isAfter(pastWeek)) {
|
||||
weekItems.add(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
rowLoader.loadRows(rows);
|
||||
//First put all recordings in and retrieve
|
||||
//All Recordings
|
||||
mRows.add(new BrowseRowDef(getString(R.string.lbl_recent_recordings), BrowsingUtils.createLiveTVRecordingsRequest(), 50));
|
||||
rowLoader.loadRows(mRows);
|
||||
|
||||
//Now insert our smart rows
|
||||
if (!weekItems.isEmpty()) {
|
||||
ItemRowAdapter weekAdapter = new ItemRowAdapter(requireContext(), weekItems, mCardPresenter, mRowsAdapter, true);
|
||||
weekAdapter.Retrieve();
|
||||
ListRow weekRow = new ListRow(new HeaderItem(getString(R.string.past_week)), weekAdapter);
|
||||
mRowsAdapter.add(0, weekRow);
|
||||
}
|
||||
if (!nearTimers.isEmpty()) {
|
||||
ItemRowAdapter scheduledAdapter = new ItemRowAdapter(requireContext(), nearTimers, mCardPresenter, mRowsAdapter, true);
|
||||
scheduledAdapter.Retrieve();
|
||||
ListRow scheduleRow = new ListRow(new HeaderItem(getString(R.string.scheduled_in_next_24_hours)), scheduledAdapter);
|
||||
mRowsAdapter.add(0, scheduleRow);
|
||||
}
|
||||
if (!dayItems.isEmpty()) {
|
||||
ItemRowAdapter dayAdapter = new ItemRowAdapter(requireContext(), dayItems, mCardPresenter, mRowsAdapter, true);
|
||||
dayAdapter.Retrieve();
|
||||
ListRow dayRow = new ListRow(new HeaderItem(getString(R.string.past_24_hours)), dayAdapter);
|
||||
mRowsAdapter.add(0, dayRow);
|
||||
}
|
||||
|
||||
} else {
|
||||
// no recordings
|
||||
rowLoader.loadRows(mRows);
|
||||
if (!nearTimers.isEmpty()) {
|
||||
ItemRowAdapter scheduledAdapter = new ItemRowAdapter(requireContext(), nearTimers, mCardPresenter, mRowsAdapter, true);
|
||||
scheduledAdapter.Retrieve();
|
||||
ListRow scheduleRow = new ListRow(new HeaderItem(getString(R.string.scheduled_in_next_24_hours)), scheduledAdapter);
|
||||
mRowsAdapter.add(0, scheduleRow);
|
||||
} else {
|
||||
mTitle.setText(R.string.lbl_no_recordings);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(Exception exception) {
|
||||
exception.printStackTrace();
|
||||
}
|
||||
return null;
|
||||
}, exception -> {
|
||||
Utils.showToast(getContext(), exception.getLocalizedMessage());
|
||||
return null;
|
||||
});
|
||||
|
||||
break;
|
||||
|
||||
default:
|
||||
boolean isRecordingsView = getArguments().getBoolean(Extras.IsLiveTvSeriesRecordings, false);
|
||||
if (isRecordingsView) {
|
||||
mRows.add(new BrowseRowDef(getString(R.string.lbl_series_recordings), GetSeriesTimersRequest.INSTANCE));
|
||||
rowLoader.loadRows(mRows);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
package org.jellyfin.androidtv.ui.browsing
|
||||
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import kotlinx.coroutines.launch
|
||||
import org.jellyfin.sdk.api.client.ApiClient
|
||||
import org.jellyfin.sdk.api.client.extensions.liveTvApi
|
||||
import org.jellyfin.sdk.model.api.BaseItemDto
|
||||
import org.jellyfin.sdk.model.api.BaseItemDtoQueryResult
|
||||
import org.jellyfin.sdk.model.api.BaseItemKind
|
||||
import org.jellyfin.sdk.model.api.ItemFields
|
||||
import org.jellyfin.sdk.model.api.LocationType
|
||||
import org.jellyfin.sdk.model.api.MediaType
|
||||
import org.jellyfin.sdk.model.api.TimerInfoDto
|
||||
import org.jellyfin.sdk.model.api.TimerInfoDtoQueryResult
|
||||
import org.jellyfin.sdk.model.serializer.toUUIDOrNull
|
||||
import org.koin.android.ext.android.inject
|
||||
|
||||
fun EnhancedBrowseFragment.getLiveTvRecordingsAndTimers(
|
||||
callback: (recordings: BaseItemDtoQueryResult, timers: TimerInfoDtoQueryResult) -> Unit,
|
||||
errorCallback: (exception: Throwable) -> Unit,
|
||||
) {
|
||||
val api by inject<ApiClient>()
|
||||
|
||||
lifecycleScope.launch {
|
||||
runCatching {
|
||||
val recordings by api.liveTvApi.getRecordings(
|
||||
fields = setOf(
|
||||
ItemFields.OVERVIEW,
|
||||
ItemFields.PRIMARY_IMAGE_ASPECT_RATIO,
|
||||
ItemFields.CHILD_COUNT,
|
||||
),
|
||||
enableImages = true,
|
||||
limit = 40,
|
||||
)
|
||||
|
||||
val timers by api.liveTvApi.getTimers()
|
||||
|
||||
recordings to timers
|
||||
}.fold(
|
||||
onSuccess = { (recordings, timers) -> callback(recordings, timers) },
|
||||
onFailure = { exception -> errorCallback(exception) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun EnhancedBrowseFragment.getLiveTvTimers(
|
||||
callback: (timers: TimerInfoDtoQueryResult) -> Unit,
|
||||
errorCallback: (exception: Throwable) -> Unit,
|
||||
) {
|
||||
val api by inject<ApiClient>()
|
||||
|
||||
lifecycleScope.launch {
|
||||
runCatching {
|
||||
api.liveTvApi.getTimers().content
|
||||
}.fold(
|
||||
onSuccess = { timers -> callback(timers) },
|
||||
onFailure = { exception -> errorCallback(exception) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun getTimerProgramInfo(timer: TimerInfoDto): BaseItemDto {
|
||||
val programInfo = timer.programInfo ?: BaseItemDto(
|
||||
id = requireNotNull(timer.id?.toUUIDOrNull()),
|
||||
channelName = timer.channelName,
|
||||
name = timer.name.orEmpty(),
|
||||
type = BaseItemKind.PROGRAM,
|
||||
timerId = timer.id,
|
||||
seriesTimerId = timer.seriesTimerId,
|
||||
startDate = timer.startDate,
|
||||
endDate = timer.endDate,
|
||||
mediaType = MediaType.UNKNOWN,
|
||||
)
|
||||
return programInfo.copy(
|
||||
locationType = LocationType.VIRTUAL,
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,434 @@
|
||||
package org.jellyfin.androidtv.ui.browsing
|
||||
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import kotlinx.coroutines.launch
|
||||
import org.jellyfin.sdk.api.client.ApiClient
|
||||
import org.jellyfin.sdk.api.client.exception.ApiClientException
|
||||
import org.jellyfin.sdk.api.client.extensions.itemsApi
|
||||
import org.jellyfin.sdk.model.api.BaseItemDto
|
||||
import org.jellyfin.sdk.model.api.BaseItemKind
|
||||
import org.jellyfin.sdk.model.api.CollectionType
|
||||
import org.jellyfin.sdk.model.api.ItemFields
|
||||
import org.jellyfin.sdk.model.api.ItemFilter
|
||||
import org.jellyfin.sdk.model.api.ItemSortBy
|
||||
import org.jellyfin.sdk.model.api.SortOrder
|
||||
import org.jellyfin.sdk.model.api.request.GetAlbumArtistsRequest
|
||||
import org.jellyfin.sdk.model.api.request.GetArtistsRequest
|
||||
import org.jellyfin.sdk.model.api.request.GetItemsRequest
|
||||
import org.jellyfin.sdk.model.api.request.GetLatestMediaRequest
|
||||
import org.jellyfin.sdk.model.api.request.GetLiveTvChannelsRequest
|
||||
import org.jellyfin.sdk.model.api.request.GetNextUpRequest
|
||||
import org.jellyfin.sdk.model.api.request.GetRecommendedProgramsRequest
|
||||
import org.jellyfin.sdk.model.api.request.GetRecordingsRequest
|
||||
import org.jellyfin.sdk.model.api.request.GetSeasonsRequest
|
||||
import org.jellyfin.sdk.model.api.request.GetSimilarItemsRequest
|
||||
import org.jellyfin.sdk.model.api.request.GetUpcomingEpisodesRequest
|
||||
import timber.log.Timber
|
||||
import java.util.UUID
|
||||
|
||||
object BrowsingUtils {
|
||||
@JvmStatic
|
||||
fun getRandomItem(
|
||||
api: ApiClient,
|
||||
lifecycle: LifecycleOwner,
|
||||
library: BaseItemDto,
|
||||
type: BaseItemKind,
|
||||
callback: (item: BaseItemDto?) -> Unit
|
||||
) {
|
||||
lifecycle.lifecycleScope.launch {
|
||||
try {
|
||||
val result by api.itemsApi.getItems(
|
||||
parentId = library.id,
|
||||
includeItemTypes = setOf(type),
|
||||
recursive = true,
|
||||
sortBy = setOf(ItemSortBy.RANDOM),
|
||||
limit = 1,
|
||||
)
|
||||
|
||||
callback(result.items?.firstOrNull())
|
||||
} catch (error: ApiClientException) {
|
||||
Timber.w(error, "Failed to retrieve random item")
|
||||
callback(null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun createGetNextUpRequest(parentId: UUID) = GetNextUpRequest(
|
||||
limit = 50,
|
||||
parentId = parentId,
|
||||
imageTypeLimit = 1,
|
||||
fields = setOf(
|
||||
ItemFields.PRIMARY_IMAGE_ASPECT_RATIO,
|
||||
ItemFields.OVERVIEW,
|
||||
ItemFields.CHILD_COUNT,
|
||||
ItemFields.MEDIA_SOURCES,
|
||||
ItemFields.MEDIA_STREAMS,
|
||||
)
|
||||
)
|
||||
|
||||
@JvmStatic
|
||||
fun createSeriesGetNextUpRequest(parentId: UUID) = GetNextUpRequest(
|
||||
seriesId = parentId,
|
||||
fields = setOf(
|
||||
ItemFields.PRIMARY_IMAGE_ASPECT_RATIO,
|
||||
ItemFields.CHILD_COUNT,
|
||||
)
|
||||
)
|
||||
|
||||
@JvmStatic
|
||||
@JvmOverloads
|
||||
fun createLatestMediaRequest(
|
||||
parentId: UUID,
|
||||
itemType: BaseItemKind? = null,
|
||||
groupItems: Boolean? = null
|
||||
) = GetLatestMediaRequest(
|
||||
fields = setOf(
|
||||
ItemFields.PRIMARY_IMAGE_ASPECT_RATIO,
|
||||
ItemFields.OVERVIEW,
|
||||
ItemFields.CHILD_COUNT,
|
||||
ItemFields.MEDIA_SOURCES,
|
||||
ItemFields.MEDIA_STREAMS,
|
||||
),
|
||||
parentId = parentId,
|
||||
limit = 50,
|
||||
imageTypeLimit = 1,
|
||||
includeItemTypes = itemType?.let(::setOf),
|
||||
groupItems = groupItems,
|
||||
)
|
||||
|
||||
@JvmStatic
|
||||
fun createSeasonsRequest(seriesId: UUID) = GetSeasonsRequest(
|
||||
seriesId = seriesId,
|
||||
fields = setOf(
|
||||
ItemFields.PRIMARY_IMAGE_ASPECT_RATIO,
|
||||
ItemFields.DISPLAY_PREFERENCES_ID,
|
||||
ItemFields.CHILD_COUNT,
|
||||
),
|
||||
)
|
||||
|
||||
@JvmStatic
|
||||
fun createUpcomingEpisodesRequest(parentId: UUID) = GetUpcomingEpisodesRequest(
|
||||
parentId = parentId,
|
||||
fields = setOf(
|
||||
ItemFields.PRIMARY_IMAGE_ASPECT_RATIO,
|
||||
ItemFields.CHILD_COUNT,
|
||||
),
|
||||
)
|
||||
|
||||
@JvmStatic
|
||||
fun createSimilarItemsRequest(itemId: UUID) = GetSimilarItemsRequest(
|
||||
itemId = itemId,
|
||||
fields = setOf(
|
||||
ItemFields.PRIMARY_IMAGE_ASPECT_RATIO,
|
||||
ItemFields.DISPLAY_PREFERENCES_ID,
|
||||
ItemFields.CHILD_COUNT,
|
||||
),
|
||||
limit = 20,
|
||||
)
|
||||
|
||||
@JvmStatic
|
||||
fun createLiveTVOnNowRequest() = GetRecommendedProgramsRequest(
|
||||
isAiring = true,
|
||||
fields = setOf(
|
||||
ItemFields.OVERVIEW,
|
||||
ItemFields.PRIMARY_IMAGE_ASPECT_RATIO,
|
||||
ItemFields.CHANNEL_INFO,
|
||||
ItemFields.CHILD_COUNT,
|
||||
),
|
||||
imageTypeLimit = 1,
|
||||
enableTotalRecordCount = false,
|
||||
limit = 150,
|
||||
)
|
||||
|
||||
@JvmStatic
|
||||
fun createLiveTVUpcomingRequest() = GetRecommendedProgramsRequest(
|
||||
isAiring = false,
|
||||
hasAired = false,
|
||||
fields = setOf(
|
||||
ItemFields.OVERVIEW,
|
||||
ItemFields.PRIMARY_IMAGE_ASPECT_RATIO,
|
||||
ItemFields.CHANNEL_INFO,
|
||||
ItemFields.CHILD_COUNT,
|
||||
),
|
||||
imageTypeLimit = 1,
|
||||
enableTotalRecordCount = false,
|
||||
limit = 150,
|
||||
)
|
||||
|
||||
@JvmStatic
|
||||
@JvmOverloads
|
||||
fun createLiveTVRecordingsRequest(limit: Int? = null) = GetRecordingsRequest(
|
||||
fields = setOf(
|
||||
ItemFields.OVERVIEW,
|
||||
ItemFields.PRIMARY_IMAGE_ASPECT_RATIO,
|
||||
ItemFields.CHILD_COUNT,
|
||||
),
|
||||
enableImages = true,
|
||||
limit = limit,
|
||||
)
|
||||
|
||||
@JvmStatic
|
||||
fun createLiveTVMovieRecordingsRequest() = GetRecordingsRequest(
|
||||
fields = setOf(
|
||||
ItemFields.OVERVIEW,
|
||||
ItemFields.PRIMARY_IMAGE_ASPECT_RATIO,
|
||||
ItemFields.CHILD_COUNT,
|
||||
),
|
||||
enableImages = true,
|
||||
limit = 60,
|
||||
isMovie = true,
|
||||
)
|
||||
|
||||
@JvmStatic
|
||||
fun createLiveTVSeriesRecordingsRequest() = GetRecordingsRequest(
|
||||
fields = setOf(
|
||||
ItemFields.OVERVIEW,
|
||||
ItemFields.PRIMARY_IMAGE_ASPECT_RATIO,
|
||||
ItemFields.CHILD_COUNT,
|
||||
),
|
||||
enableImages = true,
|
||||
limit = 60,
|
||||
isSeries = true,
|
||||
)
|
||||
|
||||
@JvmStatic
|
||||
fun createLiveTVSportsRecordingsRequest() = GetRecordingsRequest(
|
||||
fields = setOf(
|
||||
ItemFields.OVERVIEW,
|
||||
ItemFields.PRIMARY_IMAGE_ASPECT_RATIO,
|
||||
ItemFields.CHILD_COUNT,
|
||||
),
|
||||
enableImages = true,
|
||||
limit = 60,
|
||||
isSports = true,
|
||||
)
|
||||
|
||||
@JvmStatic
|
||||
fun createLiveTVKidsRecordingsRequest() = GetRecordingsRequest(
|
||||
fields = setOf(
|
||||
ItemFields.OVERVIEW,
|
||||
ItemFields.PRIMARY_IMAGE_ASPECT_RATIO,
|
||||
ItemFields.CHILD_COUNT,
|
||||
),
|
||||
enableImages = true,
|
||||
limit = 60,
|
||||
isKids = true,
|
||||
)
|
||||
|
||||
@JvmStatic
|
||||
fun createLiveTVChannelsRequest(isFavorite: Boolean) = GetLiveTvChannelsRequest(
|
||||
isFavorite = isFavorite,
|
||||
)
|
||||
|
||||
@JvmStatic
|
||||
fun createAlbumArtistsRequest(parentId: UUID) = GetAlbumArtistsRequest(
|
||||
fields = setOf(
|
||||
ItemFields.PRIMARY_IMAGE_ASPECT_RATIO,
|
||||
ItemFields.ITEM_COUNTS,
|
||||
ItemFields.CHILD_COUNT,
|
||||
),
|
||||
parentId = parentId,
|
||||
)
|
||||
|
||||
@JvmStatic
|
||||
fun createArtistsRequest(parentId: UUID) = GetArtistsRequest(
|
||||
fields = setOf(
|
||||
ItemFields.PRIMARY_IMAGE_ASPECT_RATIO,
|
||||
ItemFields.ITEM_COUNTS,
|
||||
ItemFields.CHILD_COUNT,
|
||||
),
|
||||
parentId = parentId,
|
||||
)
|
||||
|
||||
@JvmStatic
|
||||
fun createPersonItemsRequest(personId: UUID, itemType: BaseItemKind) = GetItemsRequest(
|
||||
fields = setOf(
|
||||
ItemFields.PRIMARY_IMAGE_ASPECT_RATIO,
|
||||
ItemFields.DISPLAY_PREFERENCES_ID,
|
||||
ItemFields.CHILD_COUNT,
|
||||
),
|
||||
personIds = setOf(personId),
|
||||
recursive = true,
|
||||
includeItemTypes = setOf(itemType),
|
||||
sortBy = setOf(ItemSortBy.SORT_NAME),
|
||||
)
|
||||
|
||||
@JvmStatic
|
||||
fun createArtistItemsRequest(artistId: UUID, itemType: BaseItemKind) = GetItemsRequest(
|
||||
fields = setOf(
|
||||
ItemFields.PRIMARY_IMAGE_ASPECT_RATIO,
|
||||
ItemFields.DISPLAY_PREFERENCES_ID,
|
||||
ItemFields.CHILD_COUNT,
|
||||
),
|
||||
artistIds = setOf(artistId),
|
||||
recursive = true,
|
||||
includeItemTypes = setOf(itemType),
|
||||
sortBy = setOf(ItemSortBy.SORT_NAME),
|
||||
)
|
||||
|
||||
@JvmStatic
|
||||
fun createNextEpisodesRequest(seasonId: UUID, indexNumber: Int) = GetItemsRequest(
|
||||
fields = setOf(
|
||||
ItemFields.PRIMARY_IMAGE_ASPECT_RATIO,
|
||||
ItemFields.OVERVIEW,
|
||||
ItemFields.ITEM_COUNTS,
|
||||
ItemFields.DISPLAY_PREFERENCES_ID,
|
||||
ItemFields.CHILD_COUNT,
|
||||
),
|
||||
parentId = seasonId,
|
||||
includeItemTypes = setOf(BaseItemKind.EPISODE),
|
||||
startIndex = indexNumber,
|
||||
limit = 20,
|
||||
)
|
||||
|
||||
@JvmStatic
|
||||
fun createResumeItemsRequest(parentId: UUID, itemType: BaseItemKind) = GetItemsRequest(
|
||||
fields = setOf(
|
||||
ItemFields.PRIMARY_IMAGE_ASPECT_RATIO,
|
||||
ItemFields.OVERVIEW,
|
||||
ItemFields.ITEM_COUNTS,
|
||||
ItemFields.DISPLAY_PREFERENCES_ID,
|
||||
ItemFields.CHILD_COUNT,
|
||||
ItemFields.MEDIA_STREAMS,
|
||||
ItemFields.MEDIA_SOURCES,
|
||||
),
|
||||
includeItemTypes = setOf(itemType),
|
||||
recursive = true,
|
||||
parentId = parentId,
|
||||
imageTypeLimit = 1,
|
||||
limit = 50,
|
||||
collapseBoxSetItems = false,
|
||||
enableTotalRecordCount = false,
|
||||
filters = setOf(ItemFilter.IS_RESUMABLE),
|
||||
sortBy = setOf(ItemSortBy.DATE_PLAYED),
|
||||
sortOrder = setOf(SortOrder.DESCENDING),
|
||||
)
|
||||
|
||||
@JvmStatic
|
||||
fun createFavoriteItemsRequest(parentId: UUID, itemType: BaseItemKind) = GetItemsRequest(
|
||||
fields = setOf(
|
||||
ItemFields.PRIMARY_IMAGE_ASPECT_RATIO,
|
||||
ItemFields.OVERVIEW,
|
||||
ItemFields.ITEM_COUNTS,
|
||||
ItemFields.DISPLAY_PREFERENCES_ID,
|
||||
ItemFields.CHILD_COUNT,
|
||||
ItemFields.MEDIA_STREAMS,
|
||||
ItemFields.MEDIA_SOURCES,
|
||||
),
|
||||
includeItemTypes = setOf(itemType),
|
||||
recursive = true,
|
||||
parentId = parentId,
|
||||
imageTypeLimit = 1,
|
||||
filters = setOf(ItemFilter.IS_FAVORITE),
|
||||
sortBy = setOf(ItemSortBy.SORT_NAME),
|
||||
)
|
||||
|
||||
@JvmStatic
|
||||
fun createCollectionsRequest(parentId: UUID) = GetItemsRequest(
|
||||
fields = setOf(ItemFields.CHILD_COUNT),
|
||||
includeItemTypes = setOf(BaseItemKind.BOX_SET),
|
||||
recursive = true,
|
||||
imageTypeLimit = 1,
|
||||
parentId = parentId,
|
||||
sortBy = setOf(ItemSortBy.SORT_NAME),
|
||||
)
|
||||
|
||||
@JvmStatic
|
||||
fun createPremieresRequest(parentId: UUID) = GetItemsRequest(
|
||||
fields = setOf(
|
||||
ItemFields.DATE_CREATED,
|
||||
ItemFields.PRIMARY_IMAGE_ASPECT_RATIO,
|
||||
ItemFields.OVERVIEW,
|
||||
ItemFields.CHILD_COUNT,
|
||||
),
|
||||
includeItemTypes = setOf(BaseItemKind.EPISODE),
|
||||
parentId = parentId,
|
||||
recursive = true,
|
||||
isMissing = false,
|
||||
imageTypeLimit = 1,
|
||||
filters = setOf(ItemFilter.IS_UNPLAYED),
|
||||
sortBy = setOf(ItemSortBy.DATE_CREATED),
|
||||
sortOrder = setOf(SortOrder.DESCENDING),
|
||||
enableTotalRecordCount = false,
|
||||
limit = 300,
|
||||
)
|
||||
|
||||
@JvmStatic
|
||||
fun createLastPlayedRequest(parentId: UUID) = GetItemsRequest(
|
||||
fields = setOf(
|
||||
ItemFields.PRIMARY_IMAGE_ASPECT_RATIO,
|
||||
ItemFields.OVERVIEW,
|
||||
ItemFields.ITEM_COUNTS,
|
||||
ItemFields.DISPLAY_PREFERENCES_ID,
|
||||
ItemFields.CHILD_COUNT,
|
||||
),
|
||||
includeItemTypes = setOf(BaseItemKind.AUDIO),
|
||||
recursive = true,
|
||||
parentId = parentId,
|
||||
imageTypeLimit = 1,
|
||||
filters = setOf(ItemFilter.IS_PLAYED),
|
||||
sortBy = setOf(ItemSortBy.DATE_PLAYED),
|
||||
sortOrder = setOf(SortOrder.DESCENDING),
|
||||
enableTotalRecordCount = false,
|
||||
limit = 50,
|
||||
)
|
||||
|
||||
@JvmStatic
|
||||
fun createPlaylistsRequest() = GetItemsRequest(
|
||||
fields = setOf(
|
||||
ItemFields.PRIMARY_IMAGE_ASPECT_RATIO,
|
||||
ItemFields.CUMULATIVE_RUN_TIME_TICKS,
|
||||
ItemFields.CHILD_COUNT,
|
||||
),
|
||||
includeItemTypes = setOf(BaseItemKind.PLAYLIST),
|
||||
imageTypeLimit = 1,
|
||||
recursive = true,
|
||||
sortBy = setOf(ItemSortBy.DATE_CREATED),
|
||||
sortOrder = setOf(SortOrder.DESCENDING),
|
||||
)
|
||||
|
||||
@JvmStatic
|
||||
fun createBrowseGridItemsRequest(parent: BaseItemDto): GetItemsRequest {
|
||||
val baseRequest = GetItemsRequest(
|
||||
fields = setOf(
|
||||
ItemFields.PRIMARY_IMAGE_ASPECT_RATIO,
|
||||
ItemFields.CHILD_COUNT,
|
||||
ItemFields.MEDIA_SOURCES,
|
||||
ItemFields.MEDIA_STREAMS,
|
||||
ItemFields.DISPLAY_PREFERENCES_ID,
|
||||
),
|
||||
parentId = parent.id,
|
||||
)
|
||||
|
||||
if (parent.type == BaseItemKind.USER_VIEW || parent.type == BaseItemKind.COLLECTION_FOLDER) {
|
||||
return when (parent.collectionType) {
|
||||
CollectionType.MOVIES -> baseRequest.copy(
|
||||
includeItemTypes = setOf(BaseItemKind.MOVIE),
|
||||
recursive = true,
|
||||
)
|
||||
|
||||
CollectionType.TVSHOWS -> baseRequest.copy(
|
||||
includeItemTypes = setOf(BaseItemKind.SERIES),
|
||||
recursive = true,
|
||||
)
|
||||
|
||||
CollectionType.BOXSETS -> baseRequest.copy(
|
||||
includeItemTypes = setOf(BaseItemKind.BOX_SET),
|
||||
parentId = null,
|
||||
recursive = true,
|
||||
)
|
||||
|
||||
CollectionType.MUSIC -> baseRequest.copy(
|
||||
includeItemTypes = setOf(BaseItemKind.MUSIC_ALBUM),
|
||||
recursive = true,
|
||||
)
|
||||
|
||||
else -> baseRequest
|
||||
}
|
||||
}
|
||||
|
||||
return baseRequest
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,11 @@
|
||||
package org.jellyfin.androidtv.ui.browsing
|
||||
|
||||
import org.jellyfin.androidtv.data.querying.StdItemQuery
|
||||
import org.jellyfin.sdk.api.client.ApiClient
|
||||
import org.jellyfin.sdk.api.client.extensions.genresApi
|
||||
import org.jellyfin.sdk.model.constant.ItemSortBy
|
||||
import org.jellyfin.sdk.model.api.BaseItemKind
|
||||
import org.jellyfin.sdk.model.api.ItemFields
|
||||
import org.jellyfin.sdk.model.api.ItemSortBy
|
||||
import org.jellyfin.sdk.model.api.request.GetItemsRequest
|
||||
import org.koin.android.ext.android.inject
|
||||
|
||||
class ByGenreFragment : BrowseFolderFragment() {
|
||||
@@ -16,19 +18,25 @@ class ByGenreFragment : BrowseFolderFragment() {
|
||||
// Get all genres for this folder
|
||||
val genresResponse by apiClient.genresApi.getGenres(
|
||||
parentId = folder?.id,
|
||||
sortBy = setOf(ItemSortBy.SortName),
|
||||
userId = apiClient.userId,
|
||||
sortBy = setOf(ItemSortBy.SORT_NAME),
|
||||
)
|
||||
|
||||
for (genre in genresResponse.items.orEmpty()) {
|
||||
val itemsQuery = StdItemQuery().apply {
|
||||
parentId = folder?.id.toString()
|
||||
sortBy = arrayOf(ItemSortBy.SortName)
|
||||
includeType?.let { includeItemTypes = arrayOf(it) }
|
||||
genres = arrayOf(genre.name)
|
||||
recursive = true
|
||||
}
|
||||
rows.add(BrowseRowDef(genre.name, itemsQuery, 40))
|
||||
val itemsRequest = GetItemsRequest(
|
||||
parentId = folder?.id,
|
||||
sortBy = setOf(ItemSortBy.SORT_NAME),
|
||||
includeItemTypes = includeType?.let(BaseItemKind::fromNameOrNull)?.let(::setOf),
|
||||
genres = setOf(genre.name.orEmpty()),
|
||||
recursive = true,
|
||||
fields = setOf(
|
||||
ItemFields.PRIMARY_IMAGE_ASPECT_RATIO,
|
||||
ItemFields.OVERVIEW,
|
||||
ItemFields.ITEM_COUNTS,
|
||||
ItemFields.DISPLAY_PREFERENCES_ID,
|
||||
ItemFields.CHILD_COUNT,
|
||||
),
|
||||
)
|
||||
rows.add(BrowseRowDef(genre.name, itemsRequest, 40))
|
||||
}
|
||||
|
||||
rowLoader.loadRows(rows)
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
package org.jellyfin.androidtv.ui.browsing
|
||||
|
||||
import org.jellyfin.androidtv.R
|
||||
import org.jellyfin.androidtv.data.querying.StdItemQuery
|
||||
import org.jellyfin.sdk.model.constant.ItemSortBy
|
||||
import org.jellyfin.sdk.model.api.BaseItemKind
|
||||
import org.jellyfin.sdk.model.api.ItemFields
|
||||
import org.jellyfin.sdk.model.api.ItemSortBy
|
||||
import org.jellyfin.sdk.model.api.request.GetItemsRequest
|
||||
|
||||
class ByLetterFragment : BrowseFolderFragment() {
|
||||
override suspend fun setupQueries(rowLoader: RowLoader) {
|
||||
@@ -12,27 +14,41 @@ class ByLetterFragment : BrowseFolderFragment() {
|
||||
val letters = getString(R.string.byletter_letters)
|
||||
|
||||
// Add a '#' item
|
||||
val numbersQuery = StdItemQuery().apply {
|
||||
parentId = folder?.id?.toString()
|
||||
sortBy = arrayOf(ItemSortBy.SortName)
|
||||
includeType?.let { includeItemTypes = arrayOf(it) }
|
||||
nameLessThan = letters.substring(0, 1)
|
||||
recursive = true
|
||||
}
|
||||
val numbersItemsRequest = GetItemsRequest(
|
||||
parentId = folder?.id,
|
||||
sortBy = setOf(ItemSortBy.SORT_NAME),
|
||||
includeItemTypes = includeType?.let(BaseItemKind::fromNameOrNull)?.let(::setOf),
|
||||
nameLessThan = letters.substring(0, 1),
|
||||
recursive = true,
|
||||
fields = setOf(
|
||||
ItemFields.PRIMARY_IMAGE_ASPECT_RATIO,
|
||||
ItemFields.OVERVIEW,
|
||||
ItemFields.ITEM_COUNTS,
|
||||
ItemFields.DISPLAY_PREFERENCES_ID,
|
||||
ItemFields.CHILD_COUNT,
|
||||
),
|
||||
)
|
||||
|
||||
rows.add(BrowseRowDef("#", numbersQuery, 40))
|
||||
rows.add(BrowseRowDef("#", numbersItemsRequest, 40))
|
||||
|
||||
// Add all the defined letters
|
||||
for (letter in letters.toCharArray()) {
|
||||
val letterQuery = StdItemQuery().apply {
|
||||
parentId = folder?.id?.toString()
|
||||
sortBy = arrayOf(ItemSortBy.SortName)
|
||||
includeType?.let { includeItemTypes = arrayOf(it) }
|
||||
nameStartsWith = letter.toString()
|
||||
recursive = true
|
||||
}
|
||||
val letterItemsRequest = GetItemsRequest(
|
||||
parentId = folder?.id,
|
||||
sortBy = setOf(ItemSortBy.SORT_NAME),
|
||||
includeItemTypes = includeType?.let(BaseItemKind::fromNameOrNull)?.let(::setOf),
|
||||
nameStartsWith = letter.toString(),
|
||||
recursive = true,
|
||||
fields = setOf(
|
||||
ItemFields.PRIMARY_IMAGE_ASPECT_RATIO,
|
||||
ItemFields.OVERVIEW,
|
||||
ItemFields.ITEM_COUNTS,
|
||||
ItemFields.DISPLAY_PREFERENCES_ID,
|
||||
ItemFields.CHILD_COUNT,
|
||||
),
|
||||
)
|
||||
|
||||
rows.add(BrowseRowDef(letter.toString(), letterQuery, 40))
|
||||
rows.add(BrowseRowDef(letter.toString(), letterItemsRequest, 40))
|
||||
}
|
||||
|
||||
rowLoader.loadRows(rows)
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
package org.jellyfin.androidtv.ui.browsing;
|
||||
|
||||
import android.os.Bundle;
|
||||
|
||||
import org.jellyfin.androidtv.R;
|
||||
import org.jellyfin.androidtv.data.querying.StdItemQuery;
|
||||
import org.jellyfin.androidtv.util.Utils;
|
||||
import org.jellyfin.apiclient.model.querying.ItemFields;
|
||||
|
||||
public class CollectionFragment extends EnhancedBrowseFragment {
|
||||
|
||||
@Override
|
||||
public void onActivityCreated(Bundle savedInstanceState) {
|
||||
super.onActivityCreated(savedInstanceState);
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void setupQueries(RowLoader rowLoader) {
|
||||
if (Utils.getSafeValue(mFolder.getChildCount(), 0) > 0) {
|
||||
StdItemQuery movies = new StdItemQuery(new ItemFields[]{
|
||||
ItemFields.PrimaryImageAspectRatio,
|
||||
ItemFields.Overview,
|
||||
ItemFields.ItemCounts,
|
||||
ItemFields.DisplayPreferencesId,
|
||||
ItemFields.ChildCount,
|
||||
ItemFields.MediaStreams,
|
||||
ItemFields.MediaSources
|
||||
});
|
||||
movies.setParentId(mFolder.getId().toString());
|
||||
movies.setIncludeItemTypes(new String[]{"Movie"});
|
||||
mRows.add(new BrowseRowDef(getString(R.string.lbl_movies), movies, 100));
|
||||
|
||||
StdItemQuery series = new StdItemQuery();
|
||||
series.setParentId(mFolder.getId().toString());
|
||||
series.setIncludeItemTypes(new String[]{"Series"});
|
||||
mRows.add(new BrowseRowDef(getString(R.string.lbl_tv_series), series, 100));
|
||||
|
||||
StdItemQuery others = new StdItemQuery();
|
||||
others.setParentId(mFolder.getId().toString());
|
||||
others.setExcludeItemTypes(new String[]{"Movie", "Series"});
|
||||
mRows.add(new BrowseRowDef(getString(R.string.lbl_other), others, 100));
|
||||
|
||||
|
||||
rowLoader.loadRows(mRows);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package org.jellyfin.androidtv.ui.browsing
|
||||
|
||||
import org.jellyfin.androidtv.R
|
||||
import org.jellyfin.sdk.model.api.BaseItemKind
|
||||
import org.jellyfin.sdk.model.api.ItemFields
|
||||
import org.jellyfin.sdk.model.api.request.GetItemsRequest
|
||||
|
||||
class CollectionFragment : EnhancedBrowseFragment() {
|
||||
companion object {
|
||||
private val itemFields = setOf(
|
||||
ItemFields.PRIMARY_IMAGE_ASPECT_RATIO,
|
||||
ItemFields.OVERVIEW,
|
||||
ItemFields.ITEM_COUNTS,
|
||||
ItemFields.DISPLAY_PREFERENCES_ID,
|
||||
ItemFields.CHILD_COUNT,
|
||||
ItemFields.MEDIA_STREAMS,
|
||||
ItemFields.MEDIA_SOURCES,
|
||||
)
|
||||
}
|
||||
|
||||
override fun setupQueries(rowLoader: RowLoader) {
|
||||
val movies = GetItemsRequest(
|
||||
fields = itemFields,
|
||||
parentId = mFolder.id,
|
||||
includeItemTypes = setOf(BaseItemKind.MOVIE),
|
||||
)
|
||||
mRows.add(BrowseRowDef(getString(R.string.lbl_movies), movies, 100))
|
||||
|
||||
val series = GetItemsRequest(
|
||||
fields = itemFields,
|
||||
parentId = mFolder.id,
|
||||
includeItemTypes = setOf(BaseItemKind.SERIES),
|
||||
)
|
||||
mRows.add(BrowseRowDef(getString(R.string.lbl_tv_series), series, 100))
|
||||
|
||||
val others = GetItemsRequest(
|
||||
fields = itemFields,
|
||||
parentId = mFolder.id,
|
||||
excludeItemTypes = setOf(BaseItemKind.MOVIE, BaseItemKind.SERIES),
|
||||
)
|
||||
mRows.add(BrowseRowDef(getString(R.string.lbl_other), others, 100))
|
||||
|
||||
rowLoader.loadRows(mRows)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,191 @@
|
||||
package org.jellyfin.androidtv.ui.browsing
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.os.Parcel
|
||||
import android.os.Parcelable
|
||||
import android.util.AttributeSet
|
||||
import android.widget.FrameLayout
|
||||
import androidx.core.os.BundleCompat
|
||||
import androidx.core.os.ParcelCompat
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentContainerView
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import androidx.fragment.app.FragmentTransaction
|
||||
import org.jellyfin.androidtv.R
|
||||
import org.jellyfin.androidtv.ui.navigation.NavigationAction
|
||||
import java.util.Stack
|
||||
|
||||
private class HistoryEntry(
|
||||
val name: Class<out Fragment>,
|
||||
val arguments: Bundle = bundleOf(),
|
||||
|
||||
var fragment: Fragment? = null,
|
||||
var savedState: Fragment.SavedState? = null,
|
||||
) : Parcelable {
|
||||
override fun describeContents(): Int = 0
|
||||
|
||||
override fun writeToParcel(dest: Parcel, flags: Int) {
|
||||
dest.writeString(name.name)
|
||||
dest.writeBundle(arguments)
|
||||
dest.writeParcelable(savedState, 0)
|
||||
}
|
||||
|
||||
companion object CREATOR : Parcelable.Creator<HistoryEntry> {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun createFromParcel(parcel: Parcel): HistoryEntry = HistoryEntry(
|
||||
name = Class.forName(parcel.readString()!!) as Class<out Fragment>,
|
||||
arguments = parcel.readBundle(this::class.java.classLoader)!!,
|
||||
fragment = null,
|
||||
savedState = ParcelCompat.readParcelable(parcel, this::class.java.classLoader, Fragment.SavedState::class.java)!!,
|
||||
)
|
||||
|
||||
override fun newArray(size: Int): Array<HistoryEntry?> = arrayOfNulls(size)
|
||||
}
|
||||
}
|
||||
|
||||
class DestinationFragmentView @JvmOverloads constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null,
|
||||
) : FrameLayout(context, attrs) {
|
||||
private companion object {
|
||||
private const val FRAGMENT_TAG_CONTENT = "content"
|
||||
private const val BUNDLE_SUPER = "super"
|
||||
private const val BUNDLE_HISTORY = "history"
|
||||
}
|
||||
|
||||
private val fragmentManager by lazy {
|
||||
FragmentManager.findFragmentManager(this)
|
||||
}
|
||||
|
||||
private val container by lazy {
|
||||
FragmentContainerView(context).also { view ->
|
||||
view.id = R.id.container
|
||||
addView(view)
|
||||
}
|
||||
}
|
||||
|
||||
private val history = Stack<HistoryEntry>()
|
||||
|
||||
fun navigate(action: NavigationAction.NavigateFragment) {
|
||||
val entry = HistoryEntry(action.destination.fragment.java, action.destination.arguments)
|
||||
|
||||
// Create the base transaction so we can mutate everything at once
|
||||
val transaction = fragmentManager.beginTransaction()
|
||||
|
||||
if (action.clear) {
|
||||
// Clear all current fragments from the history before adding the new entry
|
||||
history.mapNotNull { it.fragment }.forEach { transaction.remove(it) }
|
||||
history.clear()
|
||||
history.push(entry)
|
||||
} else if (action.replace && action.addToBackStack && history.isNotEmpty()) {
|
||||
// Remove the top-most entry before replacing it with the next
|
||||
val currentFragment = history[history.size - 1].fragment
|
||||
if (currentFragment != null) transaction.remove(currentFragment)
|
||||
history[history.size - 1] = entry
|
||||
} else {
|
||||
// Add to the end of the history
|
||||
saveCurrentFragmentState()
|
||||
history.push(entry)
|
||||
}
|
||||
|
||||
activateHistoryEntry(entry, transaction)
|
||||
}
|
||||
|
||||
fun goBack(): Boolean {
|
||||
// Require at least 2 items (current & previous) to go back
|
||||
if (history.size < 2) return false
|
||||
|
||||
// Create the base transaction so we can mutate everything at once
|
||||
val transaction = fragmentManager.beginTransaction()
|
||||
|
||||
// Remove current entry
|
||||
val currentEntry = history.pop()
|
||||
|
||||
// Make sure to remove the associated fragment
|
||||
val currentFragment = currentEntry.fragment
|
||||
if (currentFragment != null) transaction.remove(currentFragment)
|
||||
|
||||
// Read & set previous entry
|
||||
val entry = history.last()
|
||||
activateHistoryEntry(entry, transaction)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
private fun saveCurrentFragmentState() {
|
||||
if (history.isEmpty()) return
|
||||
|
||||
// Update the top-most history entry with state from current fragment
|
||||
val fragment = requireNotNull(fragmentManager.findFragmentByTag(FRAGMENT_TAG_CONTENT))
|
||||
history[history.size - 1].savedState = fragmentManager.saveFragmentInstanceState(fragment)
|
||||
}
|
||||
|
||||
@SuppressLint("CommitTransaction")
|
||||
private fun activateHistoryEntry(
|
||||
entry: HistoryEntry,
|
||||
transaction: FragmentTransaction,
|
||||
) {
|
||||
var fragment = entry.fragment
|
||||
|
||||
// Create if there is no existing fragment
|
||||
if (fragment == null) {
|
||||
fragment = fragmentManager.fragmentFactory.instantiate(context.classLoader, entry.name.name).apply {
|
||||
setInitialSavedState(entry.savedState)
|
||||
}
|
||||
entry.fragment = fragment
|
||||
}
|
||||
|
||||
// Update arguments
|
||||
fragment.arguments = entry.arguments
|
||||
|
||||
transaction.apply {
|
||||
// Set options
|
||||
setReorderingAllowed(true)
|
||||
setCustomAnimations(R.anim.fade_in, R.anim.fade_out, R.anim.fade_in, R.anim.fade_out)
|
||||
|
||||
// Detach current fragment
|
||||
fragmentManager.findFragmentByTag(FRAGMENT_TAG_CONTENT)?.let(::detach)
|
||||
|
||||
// Attach or add next fragment
|
||||
if (fragment.isDetached) attach(fragment)
|
||||
else add(container.id, fragment, FRAGMENT_TAG_CONTENT)
|
||||
}
|
||||
|
||||
// Commit
|
||||
if (fragmentManager.isStateSaved) transaction.commitNowAllowingStateLoss()
|
||||
else transaction.commitNow()
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(): Parcelable {
|
||||
// Always retrieve current state before writing
|
||||
saveCurrentFragmentState()
|
||||
|
||||
// Save state
|
||||
return bundleOf(
|
||||
BUNDLE_SUPER to super.onSaveInstanceState(),
|
||||
BUNDLE_HISTORY to history.toTypedArray()
|
||||
)
|
||||
}
|
||||
|
||||
override fun onRestoreInstanceState(state: Parcelable?) {
|
||||
// Ignore if not a bundle
|
||||
if (state !is Bundle) return super.onRestoreInstanceState(state)
|
||||
|
||||
// Call parent
|
||||
@Suppress("DEPRECATION")
|
||||
val parent = state.getParcelable<Parcelable>(BUNDLE_SUPER)
|
||||
if (parent != null) super.onRestoreInstanceState(parent)
|
||||
|
||||
// Restore history
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val savedHistory = BundleCompat.getParcelableArray(state, BUNDLE_HISTORY, HistoryEntry::class.java) as Array<HistoryEntry>?
|
||||
if (savedHistory != null) {
|
||||
history.clear()
|
||||
history.addAll(savedHistory)
|
||||
if (history.isNotEmpty()) activateHistoryEntry(history.last(), fragmentManager.beginTransaction())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -30,17 +30,20 @@ import androidx.lifecycle.Lifecycle;
|
||||
import org.jellyfin.androidtv.R;
|
||||
import org.jellyfin.androidtv.constant.CustomMessage;
|
||||
import org.jellyfin.androidtv.constant.Extras;
|
||||
import org.jellyfin.androidtv.constant.ImageType;
|
||||
import org.jellyfin.androidtv.constant.LiveTvOption;
|
||||
import org.jellyfin.androidtv.constant.QueryType;
|
||||
import org.jellyfin.androidtv.data.model.DataRefreshService;
|
||||
import org.jellyfin.androidtv.data.querying.ViewQuery;
|
||||
import org.jellyfin.androidtv.data.querying.GetUserViewsRequest;
|
||||
import org.jellyfin.androidtv.data.repository.CustomMessageRepository;
|
||||
import org.jellyfin.androidtv.data.service.BackgroundService;
|
||||
import org.jellyfin.androidtv.databinding.EnhancedDetailBrowseBinding;
|
||||
import org.jellyfin.androidtv.ui.GridButton;
|
||||
import org.jellyfin.androidtv.ui.itemhandling.BaseRowItem;
|
||||
import org.jellyfin.androidtv.ui.itemhandling.GridButtonBaseRowItem;
|
||||
import org.jellyfin.androidtv.ui.itemhandling.ItemLauncher;
|
||||
import org.jellyfin.androidtv.ui.itemhandling.ItemRowAdapter;
|
||||
import org.jellyfin.androidtv.ui.itemhandling.ItemRowAdapterHelperKt;
|
||||
import org.jellyfin.androidtv.ui.navigation.Destinations;
|
||||
import org.jellyfin.androidtv.ui.navigation.NavigationRepository;
|
||||
import org.jellyfin.androidtv.ui.presentation.CardPresenter;
|
||||
@@ -51,12 +54,10 @@ import org.jellyfin.androidtv.util.CoroutineUtils;
|
||||
import org.jellyfin.androidtv.util.InfoLayoutHelper;
|
||||
import org.jellyfin.androidtv.util.KeyProcessor;
|
||||
import org.jellyfin.androidtv.util.MarkdownRenderer;
|
||||
import org.jellyfin.androidtv.util.apiclient.LifecycleAwareResponse;
|
||||
import org.jellyfin.androidtv.util.sdk.compat.FakeBaseItem;
|
||||
import org.jellyfin.androidtv.util.sdk.compat.JavaCompat;
|
||||
import org.jellyfin.sdk.api.client.ApiClient;
|
||||
import org.jellyfin.sdk.model.api.BaseItemDto;
|
||||
import org.jellyfin.sdk.model.api.BaseItemKind;
|
||||
import org.jellyfin.sdk.model.constant.CollectionType;
|
||||
import org.koin.java.KoinJavaComponent;
|
||||
|
||||
import java.util.ArrayList;
|
||||
@@ -72,6 +73,7 @@ public class EnhancedBrowseFragment extends Fragment implements RowLoader, View.
|
||||
|
||||
protected static final int BY_LETTER = 0;
|
||||
protected static final int GENRES = 1;
|
||||
protected static final int RANDOM = 2;
|
||||
protected static final int SUGGESTED = 4;
|
||||
protected static final int GRID = 6;
|
||||
protected static final int ALBUMS = 7;
|
||||
@@ -81,12 +83,10 @@ public class EnhancedBrowseFragment extends Fragment implements RowLoader, View.
|
||||
protected static final int SERIES = 11;
|
||||
protected static final int ALBUM_ARTISTS = 12;
|
||||
protected BaseItemDto mFolder;
|
||||
protected String itemTypeString;
|
||||
protected BaseItemKind itemType;
|
||||
protected boolean showViews = true;
|
||||
protected boolean justLoaded = true;
|
||||
|
||||
protected BaseRowItem favSongsRowItem;
|
||||
|
||||
protected RowsSupportFragment mRowsFragment;
|
||||
protected CompositeClickedListener mClickedListener = new CompositeClickedListener();
|
||||
protected CompositeSelectedListener mSelectedListener = new CompositeSelectedListener();
|
||||
@@ -100,11 +100,13 @@ public class EnhancedBrowseFragment extends Fragment implements RowLoader, View.
|
||||
private Lazy<MarkdownRenderer> markdownRenderer = inject(MarkdownRenderer.class);
|
||||
private final Lazy<CustomMessageRepository> customMessageRepository = inject(CustomMessageRepository.class);
|
||||
private final Lazy<NavigationRepository> navigationRepository = inject(NavigationRepository.class);
|
||||
private final Lazy<ApiClient> api = inject(ApiClient.class);
|
||||
private final Lazy<ItemLauncher> itemLauncher = inject(ItemLauncher.class);
|
||||
private final Lazy<KeyProcessor> keyProcessor = inject(KeyProcessor.class);
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
favSongsRowItem = new BaseRowItem(FakeBaseItem.INSTANCE.getFAV_SONGS());
|
||||
|
||||
mRowsAdapter = new MutableObjectAdapter<Row>(new PositionableListRowPresenter());
|
||||
|
||||
@@ -162,16 +164,16 @@ public class EnhancedBrowseFragment extends Fragment implements RowLoader, View.
|
||||
|
||||
if (mFolder.getCollectionType() != null) {
|
||||
switch (mFolder.getCollectionType()) {
|
||||
case CollectionType.Movies:
|
||||
itemTypeString = "Movie";
|
||||
case MOVIES:
|
||||
itemType = BaseItemKind.MOVIE;
|
||||
break;
|
||||
case CollectionType.TvShows:
|
||||
itemTypeString = "Series";
|
||||
case TVSHOWS:
|
||||
itemType = BaseItemKind.SERIES;
|
||||
break;
|
||||
case CollectionType.Music:
|
||||
itemTypeString = "MusicAlbum";
|
||||
case MUSIC:
|
||||
itemType = BaseItemKind.MUSIC_ALBUM;
|
||||
break;
|
||||
case CollectionType.Folders:
|
||||
case FOLDERS:
|
||||
showViews = false;
|
||||
break;
|
||||
default:
|
||||
@@ -188,7 +190,7 @@ public class EnhancedBrowseFragment extends Fragment implements RowLoader, View.
|
||||
|
||||
// React to deletion
|
||||
DataRefreshService dataRefreshService = KoinJavaComponent.<DataRefreshService>get(DataRefreshService.class);
|
||||
if (mCurrentRow != null && mCurrentItem != null && mCurrentItem.getItemId() != null && mCurrentItem.getBaseItem().getId().equals(dataRefreshService.getLastDeletedItemId())) {
|
||||
if (mCurrentRow != null && mCurrentItem != null && mCurrentItem.getItemId() != null && mCurrentItem.getItemId().equals(dataRefreshService.getLastDeletedItemId())) {
|
||||
((ItemRowAdapter) mCurrentRow.getAdapter()).remove(mCurrentItem);
|
||||
dataRefreshService.setLastDeletedItemId(null);
|
||||
}
|
||||
@@ -222,8 +224,8 @@ public class EnhancedBrowseFragment extends Fragment implements RowLoader, View.
|
||||
mRowsAdapter = new MutableObjectAdapter<Row>(new PositionableListRowPresenter());
|
||||
mCardPresenter = new CardPresenter(false, 140);
|
||||
ClassPresenterSelector ps = new ClassPresenterSelector();
|
||||
ps.addClassPresenter(GridButtonBaseRowItem.class, new GridButtonPresenter(155, 140));
|
||||
ps.addClassPresenter(BaseRowItem.class, mCardPresenter);
|
||||
ps.addClassPresenter(GridButton.class, new GridButtonPresenter(155, 140));
|
||||
|
||||
for (BrowseRowDef def : rows) {
|
||||
HeaderItem header = new HeaderItem(def.getHeaderText());
|
||||
@@ -235,14 +237,8 @@ public class EnhancedBrowseFragment extends Fragment implements RowLoader, View.
|
||||
case LatestItems:
|
||||
rowAdapter = new ItemRowAdapter(requireContext(), def.getLatestItemsQuery(), true, mCardPresenter, mRowsAdapter);
|
||||
break;
|
||||
case Season:
|
||||
rowAdapter = new ItemRowAdapter(requireContext(), def.getSeasonQuery(), mCardPresenter, mRowsAdapter);
|
||||
break;
|
||||
case Upcoming:
|
||||
rowAdapter = new ItemRowAdapter(requireContext(), def.getUpcomingQuery(), mCardPresenter, mRowsAdapter);
|
||||
break;
|
||||
case Views:
|
||||
rowAdapter = new ItemRowAdapter(requireContext(), ViewQuery.INSTANCE, mCardPresenter, mRowsAdapter);
|
||||
rowAdapter = new ItemRowAdapter(requireContext(), GetUserViewsRequest.INSTANCE, mCardPresenter, mRowsAdapter);
|
||||
break;
|
||||
case SimilarSeries:
|
||||
rowAdapter = new ItemRowAdapter(requireContext(), def.getSimilarQuery(), QueryType.SimilarSeries, mCardPresenter, mRowsAdapter);
|
||||
@@ -250,9 +246,6 @@ public class EnhancedBrowseFragment extends Fragment implements RowLoader, View.
|
||||
case SimilarMovies:
|
||||
rowAdapter = new ItemRowAdapter(requireContext(), def.getSimilarQuery(), QueryType.SimilarMovies, mCardPresenter, mRowsAdapter);
|
||||
break;
|
||||
case Persons:
|
||||
rowAdapter = new ItemRowAdapter(requireContext(), def.getPersonsQuery(), def.getChunkSize(), mCardPresenter, mRowsAdapter);
|
||||
break;
|
||||
case LiveTvChannel:
|
||||
rowAdapter = new ItemRowAdapter(requireContext(), def.getTvChannelQuery(), 40, mCardPresenter, mRowsAdapter);
|
||||
break;
|
||||
@@ -268,6 +261,9 @@ public class EnhancedBrowseFragment extends Fragment implements RowLoader, View.
|
||||
case SeriesTimer:
|
||||
rowAdapter = new ItemRowAdapter(requireContext(), def.getSeriesTimerQuery(), mCardPresenter, mRowsAdapter);
|
||||
break;
|
||||
case Specials:
|
||||
rowAdapter = new ItemRowAdapter(requireContext(), def.getSpecialsQuery(), new CardPresenter(true, ImageType.THUMB, 150), mRowsAdapter);
|
||||
break;
|
||||
default:
|
||||
rowAdapter = new ItemRowAdapter(requireContext(), def.getQuery(), def.getChunkSize(), def.getPreferParentThumb(), def.isStaticHeight(), ps, mRowsAdapter, def.getQueryType());
|
||||
break;
|
||||
@@ -287,23 +283,30 @@ public class EnhancedBrowseFragment extends Fragment implements RowLoader, View.
|
||||
}
|
||||
|
||||
protected void addAdditionalRows(MutableObjectAdapter<Row> rowAdapter) {
|
||||
if (!showViews) return;
|
||||
if (!showViews || itemType == null) return;
|
||||
|
||||
HeaderItem gridHeader = new HeaderItem(rowAdapter.size(), getString(R.string.lbl_views));
|
||||
GridButtonPresenter mGridPresenter = new GridButtonPresenter();
|
||||
ArrayObjectAdapter gridRowAdapter = new ArrayObjectAdapter(mGridPresenter);
|
||||
|
||||
switch (itemTypeString) {
|
||||
case "Movie":
|
||||
switch (itemType) {
|
||||
case MOVIE:
|
||||
gridRowAdapter.add(new GridButton(SUGGESTED, getString(R.string.lbl_suggested)));
|
||||
addStandardViewButtons(gridRowAdapter);
|
||||
gridRowAdapter.add(new GridButton(RANDOM, getString(R.string.random)));
|
||||
break;
|
||||
|
||||
case "MusicAlbum":
|
||||
case MUSIC_ALBUM:
|
||||
gridRowAdapter.add(new GridButton(ALBUMS, getString(R.string.lbl_albums)));
|
||||
gridRowAdapter.add(new GridButton(ALBUM_ARTISTS, getString(R.string.lbl_album_artists)));
|
||||
gridRowAdapter.add(new GridButton(ARTISTS, getString(R.string.lbl_artists)));
|
||||
gridRowAdapter.add(new GridButton(GENRES, getString(R.string.lbl_genres)));
|
||||
gridRowAdapter.add(new GridButton(RANDOM, getString(R.string.random)));
|
||||
break;
|
||||
|
||||
case SERIES:
|
||||
addStandardViewButtons(gridRowAdapter);
|
||||
gridRowAdapter.add(new GridButton(RANDOM, getString(R.string.random)));
|
||||
break;
|
||||
|
||||
default:
|
||||
@@ -339,33 +342,18 @@ public class EnhancedBrowseFragment extends Fragment implements RowLoader, View.
|
||||
@Override
|
||||
public boolean onKey(View v, int keyCode, KeyEvent event) {
|
||||
if (event.getAction() != KeyEvent.ACTION_UP) return false;
|
||||
return KeyProcessor.HandleKey(keyCode, mCurrentItem, requireActivity());
|
||||
return keyProcessor.getValue().handleKey(keyCode, mCurrentItem, requireActivity());
|
||||
}
|
||||
|
||||
private void refreshCurrentItem() {
|
||||
if (mCurrentItem != null &&
|
||||
mCurrentItem.getBaseItemType() != BaseItemKind.PHOTO &&
|
||||
mCurrentItem.getBaseItemType() != BaseItemKind.MUSIC_ARTIST &&
|
||||
mCurrentItem.getBaseItemType() != BaseItemKind.MUSIC_ALBUM &&
|
||||
mCurrentItem.getBaseItemType() != BaseItemKind.PLAYLIST
|
||||
) {
|
||||
BaseRowItem item = mCurrentItem;
|
||||
item.refresh(new LifecycleAwareResponse<BaseItemDto>(getLifecycle()) {
|
||||
@Override
|
||||
public void onResponse(BaseItemDto response) {
|
||||
if (!getActive()) return;
|
||||
|
||||
ItemRowAdapter adapter = (ItemRowAdapter) mCurrentRow.getAdapter();
|
||||
if (response == null) adapter.removeAt(adapter.indexOf(item), 1);
|
||||
else adapter.notifyItemRangeChanged(adapter.indexOf(item), 1);
|
||||
}
|
||||
});
|
||||
}
|
||||
if (mCurrentRow == null || mCurrentItem == null) return;
|
||||
ItemRowAdapterHelperKt.refreshItem((ItemRowAdapter) mCurrentRow.getAdapter(), api.getValue(), this, mCurrentItem);
|
||||
}
|
||||
|
||||
private final class SpecialViewClickedListener implements OnItemViewClickedListener {
|
||||
@Override
|
||||
public void onItemClicked(Presenter.ViewHolder itemViewHolder, Object item, RowPresenter.ViewHolder rowViewHolder, Row row) {
|
||||
if (item instanceof GridButtonBaseRowItem) item = ((GridButtonBaseRowItem) item).getGridButton();
|
||||
if (item instanceof GridButton) {
|
||||
switch (((GridButton) item).getId()) {
|
||||
case GRID:
|
||||
@@ -375,7 +363,7 @@ public class EnhancedBrowseFragment extends Fragment implements RowLoader, View.
|
||||
case ALBUMS:
|
||||
mFolder = JavaCompat.copyWithDisplayPreferencesId(mFolder, mFolder.getId() + "AL");
|
||||
|
||||
navigationRepository.getValue().navigate(Destinations.INSTANCE.libraryBrowser(mFolder, "MusicAlbum"));
|
||||
navigationRepository.getValue().navigate(Destinations.INSTANCE.libraryBrowser(mFolder, BaseItemKind.MUSIC_ALBUM.getSerialName()));
|
||||
break;
|
||||
|
||||
case ALBUM_ARTISTS:
|
||||
@@ -391,11 +379,25 @@ public class EnhancedBrowseFragment extends Fragment implements RowLoader, View.
|
||||
break;
|
||||
|
||||
case BY_LETTER:
|
||||
navigationRepository.getValue().navigate(Destinations.INSTANCE.libraryByLetter(mFolder, itemTypeString));
|
||||
navigationRepository.getValue().navigate(Destinations.INSTANCE.libraryByLetter(mFolder, itemType.getSerialName()));
|
||||
break;
|
||||
|
||||
case GENRES:
|
||||
navigationRepository.getValue().navigate(Destinations.INSTANCE.libraryByGenres(mFolder, itemTypeString));
|
||||
navigationRepository.getValue().navigate(Destinations.INSTANCE.libraryByGenres(mFolder, itemType.getSerialName()));
|
||||
break;
|
||||
|
||||
case RANDOM:
|
||||
BrowsingUtils.getRandomItem(api.getValue(), getViewLifecycleOwner(), mFolder, itemType, randomItem -> {
|
||||
if (randomItem != null) {
|
||||
if (randomItem.getType() == BaseItemKind.MUSIC_ALBUM) {
|
||||
navigationRepository.getValue().navigate(Destinations.INSTANCE.itemList(randomItem.getId()));
|
||||
} else {
|
||||
navigationRepository.getValue().navigate(Destinations.INSTANCE.itemDetails(randomItem.getId()));
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
});
|
||||
break;
|
||||
|
||||
case SUGGESTED:
|
||||
@@ -403,12 +405,12 @@ public class EnhancedBrowseFragment extends Fragment implements RowLoader, View.
|
||||
break;
|
||||
|
||||
case FAVSONGS:
|
||||
navigationRepository.getValue().navigate(Destinations.INSTANCE.itemList(FakeBaseItem.INSTANCE.getFAV_SONGS_ID(), mFolder.getId()));
|
||||
navigationRepository.getValue().navigate(Destinations.INSTANCE.musicFavorites(mFolder.getId()));
|
||||
break;
|
||||
|
||||
case SERIES:
|
||||
case LiveTvOption.LIVE_TV_SERIES_OPTION_ID:
|
||||
navigationRepository.getValue().navigate(Destinations.INSTANCE.librarySmartScreen(FakeBaseItem.INSTANCE.getSERIES_TIMERS()));
|
||||
navigationRepository.getValue().navigate(Destinations.INSTANCE.getLiveTvSeriesRecordings());
|
||||
break;
|
||||
|
||||
case SCHEDULE:
|
||||
@@ -437,7 +439,7 @@ public class EnhancedBrowseFragment extends Fragment implements RowLoader, View.
|
||||
public void onItemClicked(final Presenter.ViewHolder itemViewHolder, Object item, RowPresenter.ViewHolder rowViewHolder, Row row) {
|
||||
if (!(item instanceof BaseRowItem)) return;
|
||||
|
||||
ItemLauncher.launch((BaseRowItem) item, (ItemRowAdapter) ((ListRow) row).getAdapter(), ((BaseRowItem) item).getIndex(), requireContext());
|
||||
itemLauncher.getValue().launch((BaseRowItem) item, (ItemRowAdapter) ((ListRow) row).getAdapter(), requireContext());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -445,15 +447,12 @@ public class EnhancedBrowseFragment extends Fragment implements RowLoader, View.
|
||||
@Override
|
||||
public void onItemSelected(Presenter.ViewHolder itemViewHolder, Object item,
|
||||
RowPresenter.ViewHolder rowViewHolder, Row row) {
|
||||
if (item instanceof GridButton && ((GridButton) item).getId() == FAVSONGS) {
|
||||
// Set to specialized item
|
||||
mCurrentItem = favSongsRowItem;
|
||||
}
|
||||
|
||||
if (!(item instanceof BaseRowItem)) {
|
||||
mTitle.setText(mFolder != null ? mFolder.getName() : "");
|
||||
mInfoRow.removeAllViews();
|
||||
mSummary.setText("");
|
||||
mCurrentItem = null;
|
||||
mCurrentRow = null;
|
||||
// Fill in default background
|
||||
backgroundService.getValue().clearBackgrounds();
|
||||
return;
|
||||
@@ -472,10 +471,10 @@ public class EnhancedBrowseFragment extends Fragment implements RowLoader, View.
|
||||
mSummary.setText(markdownRenderer.getValue().toMarkdownSpanned(summary));
|
||||
else mSummary.setText(null);
|
||||
|
||||
InfoLayoutHelper.addInfoRow(requireContext(), rowItem, mInfoRow, true, true);
|
||||
InfoLayoutHelper.addInfoRow(requireContext(), rowItem.getBaseItem(), mInfoRow, true);
|
||||
|
||||
ItemRowAdapter adapter = (ItemRowAdapter) ((ListRow) row).getAdapter();
|
||||
adapter.loadMoreItemsIfNeeded(rowItem.getIndex());
|
||||
adapter.loadMoreItemsIfNeeded(adapter.indexOf(rowItem));
|
||||
|
||||
backgroundService.getValue().setBackground(rowItem.getBaseItem());
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user