Compare commits
1996
Commits
v1.0.2
...
cc8d061b51
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cc8d061b51 | ||
|
|
1b952054fb | ||
|
|
2c609836fd | ||
|
|
da36964347 | ||
|
|
47b9c3dec2 | ||
|
|
17a9a2f601 | ||
|
|
c43b9cdbd8 | ||
|
|
7ec4e09fb4 | ||
|
|
862cec8375 | ||
|
|
2d4956ae25 | ||
|
|
beadc7d4c4 | ||
|
|
2633912e93 | ||
|
|
6cbc751f0c | ||
|
|
2a499eaf41 | ||
|
|
9244a00713 | ||
|
|
79eadbd39d | ||
|
|
c389d1945d | ||
|
|
2d8f8cab81 | ||
|
|
f799449d54 | ||
|
|
9da6c7381d | ||
|
|
ced226c3ef | ||
|
|
f69a9a6ee0 | ||
|
|
94d2773d1e | ||
|
|
bc5fd46671 | ||
|
|
a00745dd38 | ||
|
|
01908c84d3 | ||
|
|
39959c3f47 | ||
|
|
53cc96b84d | ||
|
|
1aeb336898 | ||
|
|
1715c591a5 | ||
|
|
eb41506bf4 | ||
|
|
9fdcd49c98 | ||
|
|
8538f014aa | ||
|
|
848cd03068 | ||
|
|
4debf4654d | ||
|
|
6ed2be4173 | ||
|
|
557de11bb3 | ||
|
|
97d917e35d | ||
|
|
9b2666e671 | ||
|
|
c883556dbf | ||
|
|
5bcd0f64d2 | ||
|
|
d53167c48e | ||
|
|
c2b212c1ce | ||
|
|
d1510b8ba3 | ||
|
|
273feef017 | ||
|
|
11a42bf8a1 | ||
|
|
d436efdc66 | ||
|
|
30f7bd7556 | ||
|
|
62007862a5 | ||
|
|
2786e45c75 | ||
|
|
46c3a02655 | ||
|
|
5ca07ef049 | ||
|
|
c715c890b5 | ||
|
|
9925a954d1 | ||
|
|
0c111aaf47 | ||
|
|
2a2315ecbc | ||
|
|
17f77351ad | ||
|
|
abd084cff8 | ||
|
|
e77d0a1d5b | ||
|
|
991d70f22b | ||
|
|
f477b563a2 | ||
|
|
63a7c28e96 | ||
|
|
2b2cb66dfc | ||
|
|
bf5cc1d7bb | ||
|
|
bbf94b8a0d | ||
|
|
951d7cf561 | ||
|
|
40337c9590 | ||
|
|
79a6ecdc0b | ||
|
|
33f42e62b6 | ||
|
|
1b660fe542 | ||
|
|
d0539b00b0 | ||
|
|
e089ef6f8a | ||
|
|
a8a70df304 | ||
|
|
a207a8f840 | ||
|
|
6c85f81f98 | ||
|
|
6a88019851 | ||
|
|
379263ec87 | ||
|
|
f3d610633c | ||
|
|
335db22680 | ||
|
|
b71bb43dd6 | ||
|
|
dace1416c5 | ||
|
|
1baf3ac7e0 | ||
|
|
f3a4578a66 | ||
|
|
b745c3d675 | ||
|
|
f993d343a9 | ||
|
|
1ba7846f4c | ||
|
|
da26b65a73 | ||
|
|
04dcb3f09f | ||
|
|
6916459c9e | ||
|
|
643fb9fa8e | ||
|
|
f9160ccb02 | ||
|
|
e2b166de11 | ||
|
|
89f3511864 | ||
|
|
3271c9e142 | ||
|
|
1f1069b05d | ||
|
|
a325a1bbe9 | ||
|
|
255e6dc67d | ||
|
|
b2685959ad | ||
|
|
536effab40 | ||
|
|
81537b1e96 | ||
|
|
e691f78e2e | ||
|
|
6125b9f7d1 | ||
|
|
d10373d265 | ||
|
|
5c1eb6204a | ||
|
|
c3884ea917 | ||
|
|
bc3fa9546b | ||
|
|
ea66b6da1e | ||
|
|
c9e47214ea | ||
|
|
c04d603ac0 | ||
|
|
9e524d626a | ||
|
|
e0dc0c2cb7 | ||
|
|
cbd4f04f1b | ||
|
|
eeb777fa01 | ||
|
|
9272db913c | ||
|
|
ff4c0104c5 | ||
|
|
31ce0629d2 | ||
|
|
6ab7878221 | ||
|
|
ddc18386fb | ||
|
|
809ed5ce23 | ||
|
|
8e086c4888 | ||
|
|
bc6443e5bd | ||
|
|
8d205690b3 | ||
|
|
25af312d91 | ||
|
|
b020b8096b | ||
|
|
87f6bec138 | ||
|
|
34266abd90 | ||
|
|
e8e8a63d6d | ||
|
|
d61867af4e | ||
|
|
daeb0f75a2 | ||
|
|
6c497ed1d2 | ||
|
|
7b975ec54d | ||
|
|
e3d81792bf | ||
|
|
5b9b7eafa7 | ||
|
|
5489c86cdc | ||
|
|
b1f605fda4 | ||
|
|
f9030ffd13 | ||
|
|
f74f5a534d | ||
|
|
3f3bacebd9 | ||
|
|
2041036530 | ||
|
|
81a2b541a0 | ||
|
|
e6cb94179b | ||
|
|
658b4f0a9d | ||
|
|
05cbbdfddc | ||
|
|
3c66ea986f | ||
|
|
d1a0cf27a7 | ||
|
|
1aef4b1d89 | ||
|
|
c745481ab2 | ||
|
|
7e281a15a5 | ||
|
|
c5e94bbc92 | ||
|
|
61fe8a9fee | ||
|
|
926e7eabd3 | ||
|
|
7c7aa0f442 | ||
|
|
86be8e4af1 | ||
|
|
d5cc5df704 | ||
|
|
aac09c14bc | ||
|
|
566f76df6a | ||
|
|
3ce5cd0e3b | ||
|
|
f3f175fcc6 | ||
|
|
16995e5a0f | ||
|
|
43c8762df7 | ||
|
|
f27668e038 | ||
|
|
3e14cf4320 | ||
|
|
78573d0e81 | ||
|
|
bc5814b7c5 | ||
|
|
ed2e478291 | ||
|
|
3ba46916b1 | ||
|
|
9d2ccd975f | ||
|
|
acbf9bb013 | ||
|
|
9fce374842 | ||
|
|
08c7bcf4f2 | ||
|
|
93a5d1293c | ||
|
|
5578d59e1f | ||
|
|
f1e3b5a49e | ||
|
|
b9d474075f | ||
|
|
37200773aa | ||
|
|
bbe9eca97b | ||
|
|
77a201e139 | ||
|
|
4c7887055d | ||
|
|
0d7512fee2 | ||
|
|
0da2a3f85f | ||
|
|
9ab5101c48 | ||
|
|
0906a21c6d | ||
|
|
0134942172 | ||
|
|
0a4b88fabe | ||
|
|
b772e5416e | ||
|
|
1416db105e | ||
|
|
b6cc37fe50 | ||
|
|
43e90ed47b | ||
|
|
5099795d29 | ||
|
|
73dc939cd3 | ||
|
|
1bd0f369ee | ||
|
|
8da39949e5 | ||
|
|
0454980fae | ||
|
|
0679dc7deb | ||
|
|
0e07ed0b5a | ||
|
|
1e073e85a1 | ||
|
|
4a45f9699c | ||
|
|
a91ec763e1 | ||
|
|
a0cfa70551 | ||
|
|
39dd003b4e | ||
|
|
6299259f89 | ||
|
|
12ea65f14f | ||
|
|
cc1e41e7b4 | ||
|
|
0c1ad5c746 | ||
|
|
7c775efada | ||
|
|
ee2f5572fc | ||
|
|
d16f0928bb | ||
|
|
1c2fcdd1b4 | ||
|
|
541771d72c | ||
|
|
2ad59dfd96 | ||
|
|
d3454cc293 | ||
|
|
c3377e39e6 | ||
|
|
6e04a10a3b | ||
|
|
e13da00215 | ||
|
|
0943237b5f | ||
|
|
66116351c2 | ||
|
|
68ba58a73c | ||
|
|
595bc50cde | ||
|
|
50b45d04e5 | ||
|
|
01d0367f50 | ||
|
|
f009a2edb8 | ||
|
|
ccd2398a7c | ||
|
|
8f5012fe2a | ||
|
|
34ac433247 | ||
|
|
11b06d8f9c | ||
|
|
2128bbf197 | ||
|
|
502bbd4f35 | ||
|
|
21d3ed4603 | ||
|
|
2d9200ec4d | ||
|
|
a088811b2f | ||
|
|
e331a7e072 | ||
|
|
bb00dd13b0 | ||
|
|
7748431330 | ||
|
|
8c5c1dce53 | ||
|
|
136fdced17 | ||
|
|
78e0a53018 | ||
|
|
15de359d4f | ||
|
|
c030cbabd3 | ||
|
|
ab9d14598c | ||
|
|
71b0c64fd2 | ||
|
|
2feacd9416 | ||
|
|
f8a5e4c44f | ||
|
|
c3f576dd5f | ||
|
|
102c454f9c | ||
|
|
84b5ab4279 | ||
|
|
11853761cf | ||
|
|
2e86e21cb7 | ||
|
|
7436ac77d9 | ||
|
|
00aac29d89 | ||
|
|
33e137bc22 | ||
|
|
f4a32086b0 | ||
|
|
4df724ee56 | ||
|
|
c1a21985fd | ||
|
|
0a97194b31 | ||
|
|
8150b680c1 | ||
|
|
cdc847dcca | ||
|
|
af895ed510 | ||
|
|
ee74519f6f | ||
|
|
d47e90d340 | ||
|
|
b53853f193 | ||
|
|
b69a713641 | ||
|
|
191b64d96e | ||
|
|
a747729c26 | ||
|
|
8ba3ed5fb5 | ||
|
|
a9446ef5cc | ||
|
|
2818f8d4a4 | ||
|
|
cf89052806 | ||
|
|
fcd445666e | ||
|
|
3599197f01 | ||
|
|
f4e46f9d20 | ||
|
|
f2996f599a | ||
|
|
0d0bec1904 | ||
|
|
8bc1ec5830 | ||
|
|
b858003b6d | ||
|
|
37b70a956c | ||
|
|
6590cf8300 | ||
|
|
8d33b90e67 | ||
|
|
e4d650c3e7 | ||
|
|
1a45f42f9e | ||
|
|
55255621e3 | ||
|
|
cd323c3f69 | ||
|
|
27add8b244 | ||
|
|
0ea2a10554 | ||
|
|
bc4f565074 | ||
|
|
de03d2ff76 | ||
|
|
315df46b7e | ||
|
|
4117afa9e4 | ||
|
|
dd742ce245 | ||
|
|
c02437797c | ||
|
|
5746239873 | ||
|
|
62a11023fb | ||
|
|
d98fdf2e98 | ||
|
|
f8e916d280 | ||
|
|
b52e7b5430 | ||
|
|
51b50eed75 | ||
|
|
74efe45d05 | ||
|
|
6ebd59ff9c | ||
|
|
7e1e453a6b | ||
|
|
c25acea5d4 | ||
|
|
7a705b4355 | ||
|
|
933e118c36 | ||
|
|
092dff2a9d | ||
|
|
fe19552741 | ||
|
|
0122c8c5ed | ||
|
|
9b19be7775 | ||
|
|
4d79cb65cd | ||
|
|
8db913812d | ||
|
|
a7ce991f7d | ||
|
|
ba6616c81a | ||
|
|
4097181923 | ||
|
|
ecaa1f01e8 | ||
|
|
a3ea2051cb | ||
|
|
5ca5da45ba | ||
|
|
77d889d9ab | ||
|
|
6c8d3dfc76 | ||
|
|
bb8442a32d | ||
|
|
32504349cd | ||
|
|
0344518130 | ||
|
|
eb71034365 | ||
|
|
5a9449059b | ||
|
|
3a94878020 | ||
|
|
f6190030a4 | ||
|
|
b04c01c930 | ||
|
|
40ef5710fb | ||
|
|
790e08f1b5 | ||
|
|
270fe32d94 | ||
|
|
c87a6878b5 | ||
|
|
c4906bd0b5 | ||
|
|
4012b92357 | ||
|
|
50449f43ac | ||
|
|
b7eba4c99a | ||
|
|
25320e283d | ||
|
|
6517e739b3 | ||
|
|
4f7dd97be6 | ||
|
|
7ddfbb0a18 | ||
|
|
2d41b07852 | ||
|
|
7958547b28 | ||
|
|
b0346ae00c | ||
|
|
8f5c55e0d1 | ||
|
|
4288e7284b | ||
|
|
cb5d4f954d | ||
|
|
a36c3b74e8 | ||
|
|
e29ae4fedb | ||
|
|
74c11f2aa6 | ||
|
|
3e4c00fee8 | ||
|
|
318944fbfe | ||
|
|
7ccc676192 | ||
|
|
2b21e07571 | ||
|
|
6ae28e4d84 | ||
|
|
d6b49eb3e8 | ||
|
|
88fcbe49f4 | ||
|
|
f9501c156a | ||
|
|
efe12774f7 | ||
|
|
9b5bcbe41e | ||
|
|
d4448820d6 | ||
|
|
744132fd7e | ||
|
|
a444dd8677 | ||
|
|
546a665ba3 | ||
|
|
6bff74cd1e | ||
|
|
1dc6aa5283 | ||
|
|
a2dba0456b | ||
|
|
015a6ac865 | ||
|
|
fcf35e2513 | ||
|
|
92cea6d3ee | ||
|
|
7ea06fda2f | ||
|
|
a9456abfb0 | ||
|
|
3dcc48c9d9 | ||
|
|
c25d4cd161 | ||
|
|
185c92e9af | ||
|
|
8fae9e6a96 | ||
|
|
8a5fc48650 | ||
|
|
688ba0a63d | ||
|
|
a0f263c1fd | ||
|
|
e3cea856b8 | ||
|
|
df78b8a3f5 | ||
|
|
1a9a78ec70 | ||
|
|
1f855102ce | ||
|
|
757f242285 | ||
|
|
bcbc48540e | ||
|
|
5b89d6c6d7 | ||
|
|
3a421d2a60 | ||
|
|
9d7bc6969b | ||
|
|
e12f00b5b4 | ||
|
|
0b2faf2c6e | ||
|
|
763a86704c | ||
|
|
6df7666cec | ||
|
|
d97c2792f5 | ||
|
|
f848f22029 | ||
|
|
011a762ecf | ||
|
|
241532f60e | ||
|
|
6770dbd682 | ||
|
|
12a9d9f398 | ||
|
|
c82513eaf0 | ||
|
|
845b36828b | ||
|
|
d0843d94ed | ||
|
|
c9d911f339 | ||
|
|
4e4d13b4de | ||
|
|
a0274518d2 | ||
|
|
88dd7cc04c | ||
|
|
dacd3a67a1 | ||
|
|
da64806d88 | ||
|
|
a87d2990b0 | ||
|
|
5e95aa4168 | ||
|
|
9c642fb3b7 | ||
|
|
8b515bba97 | ||
|
|
1550014670 | ||
|
|
a6862c51e4 | ||
|
|
de90b34cd4 | ||
|
|
bbb84d4ffa | ||
|
|
f560adabfa | ||
|
|
8f69c1ab82 | ||
|
|
a6485292e4 | ||
|
|
482d517145 | ||
|
|
2c1eb03e5f | ||
|
|
e640ee6c46 | ||
|
|
e8ae5b9639 | ||
|
|
32e33a243a | ||
|
|
379284ca4f | ||
|
|
dd7a6465c1 | ||
|
|
deba733522 | ||
|
|
3d71def880 | ||
|
|
c5bcaf7329 | ||
|
|
e76562067f | ||
|
|
0e03a1a14a | ||
|
|
fa4e41589b | ||
|
|
b11cf53f67 | ||
|
|
7db7fdffdf | ||
|
|
efac753f83 | ||
|
|
f4af9868e6 | ||
|
|
a1e8f8edb3 | ||
|
|
7469172cfc | ||
|
|
a49951c51f | ||
|
|
0d11839a96 | ||
|
|
8213d2de42 | ||
|
|
decc2cbb31 | ||
|
|
b639782e89 | ||
|
|
10004652e4 | ||
|
|
e90fb04de9 | ||
|
|
7fb52b3c85 | ||
|
|
dd13c619ac | ||
|
|
41ef04b193 | ||
|
|
747569c1cc | ||
|
|
1f0adb21a7 | ||
|
|
f015aea062 | ||
|
|
3f16cc9312 | ||
|
|
c1ac428ded | ||
|
|
4a38703873 | ||
|
|
e5d4f1d40d | ||
|
|
d25f509118 | ||
|
|
ecaeea6262 | ||
|
|
cff8469604 | ||
|
|
9f242f0201 | ||
|
|
bb3c63e2fb | ||
|
|
ca0c586c0c | ||
|
|
bd60185db8 | ||
|
|
01afed89f8 | ||
|
|
3dacc50e1b | ||
|
|
236b874e82 | ||
|
|
26f44dd448 | ||
|
|
f2f2a3143d | ||
|
|
34876cf46f | ||
|
|
deda8322d5 | ||
|
|
ba0aef4349 | ||
|
|
5d52787ea9 | ||
|
|
6a6b1138a8 | ||
|
|
722f84039f | ||
|
|
710ce2602b | ||
|
|
3e96a5435e | ||
|
|
433bf172ce | ||
|
|
5969f50888 | ||
|
|
9900ac02f5 | ||
|
|
ee4de78c6c | ||
|
|
43c2f6f417 | ||
|
|
f7c1a5168f | ||
|
|
28433c10df | ||
|
|
de351d700c | ||
|
|
a01675b592 | ||
|
|
6ba5bf2cb1 | ||
|
|
f6395b5a3e | ||
|
|
1cd676bcb4 | ||
|
|
0bb5796da1 | ||
|
|
5f4140ea68 | ||
|
|
b2878e42ea | ||
|
|
e8bd31454e | ||
|
|
59545ca82c | ||
|
|
cd1f9db8ae | ||
|
|
d719470f8c | ||
|
|
f3c19ed8ba | ||
|
|
984aa13edf | ||
|
|
5ac900f3b7 | ||
|
|
a21427b549 | ||
|
|
f667bf1096 | ||
|
|
458cdc3280 | ||
|
|
2cf492d634 | ||
|
|
e16bc306f7 | ||
|
|
fcb198c8a8 | ||
|
|
40679a624b | ||
|
|
a2be1739a6 | ||
|
|
b529a83fe6 | ||
|
|
20491906fd | ||
|
|
9a8559af8b | ||
|
|
541333bc44 | ||
|
|
9c4c0506af | ||
|
|
00b5a3687e | ||
|
|
b41e5c6074 | ||
|
|
19b8e4750f | ||
|
|
bfb5440c9e | ||
|
|
1ce974938d | ||
|
|
dc217f97af | ||
|
|
d3bfc57294 | ||
|
|
cd5fbc0858 | ||
|
|
2d2844338a | ||
|
|
1f3abfde4f | ||
|
|
2d58a876da | ||
|
|
c45c5a2a5c | ||
|
|
5fde0bc469 | ||
|
|
edb7d98de7 | ||
|
|
573df4318b | ||
|
|
febd718796 | ||
|
|
a20655badb | ||
|
|
11fd892310 | ||
|
|
2253dabfe9 | ||
|
|
e2575e2b15 | ||
|
|
05f724dbe1 | ||
|
|
d4b3a9a8a4 | ||
|
|
74687daf28 | ||
|
|
a8ba791349 | ||
|
|
21383ced46 | ||
|
|
a631aa1b65 | ||
|
|
402ea5e9c0 | ||
|
|
654a74aacf | ||
|
|
3027934295 | ||
|
|
a355838039 | ||
|
|
0ae6596816 | ||
|
|
da23253a9c | ||
|
|
0934a7ec51 | ||
|
|
80e8213f12 | ||
|
|
55b4d9bc8d | ||
|
|
92fdd6ab54 | ||
|
|
3514104e7c | ||
|
|
122c559e57 | ||
|
|
b2cb08e96a | ||
|
|
9e9533c7a6 | ||
|
|
f6ab1bd4ef | ||
|
|
f47a9d6d93 | ||
|
|
ff728af7cc | ||
|
|
1e8beb458c | ||
|
|
a6d8cd54f3 | ||
|
|
841ed5f6f8 | ||
|
|
0263e64f40 | ||
|
|
a0b95ea3bd | ||
|
|
590a52c605 | ||
|
|
f2687b8243 | ||
|
|
871f4e73e8 | ||
|
|
e19442a61a | ||
|
|
3285958918 | ||
|
|
06088c578f | ||
|
|
63a4f5b4f7 | ||
|
|
56f110c548 | ||
|
|
406c377d13 | ||
|
|
fd0382ea65 | ||
|
|
a289849a07 | ||
|
|
630f84c3e5 | ||
|
|
e1ae7df0ee | ||
|
|
5f7fc68c8c | ||
|
|
f4f561b396 | ||
|
|
40f8355037 | ||
|
|
ccc4a822e3 | ||
|
|
70177d8f81 | ||
|
|
b38e58af9a | ||
|
|
516e4a94bf | ||
|
|
cdb491b21b | ||
|
|
a769fc6a64 | ||
|
|
2c30da8110 | ||
|
|
ee703eb13a | ||
|
|
55fb032b22 | ||
|
|
346671b3e2 | ||
|
|
5574e68b16 | ||
|
|
445d91d594 | ||
|
|
896935e19a | ||
|
|
bb17f0014a | ||
|
|
ed8a0e9a09 | ||
|
|
10a74c2e5e | ||
|
|
d66743ae1e | ||
|
|
5a17d7d2f6 | ||
|
|
50f0fb3d15 | ||
|
|
4c351da60c | ||
|
|
89837877a2 | ||
|
|
32c30132b9 | ||
|
|
06e4b5ad34 | ||
|
|
972d225a86 | ||
|
|
7acc7b51b6 | ||
|
|
86e18a1f7c | ||
|
|
bb4d290ca1 | ||
|
|
4e98a1dfea | ||
|
|
c3bbf9203d | ||
|
|
64fb55db55 | ||
|
|
ea076a5ac7 | ||
|
|
6b0232b7e4 | ||
|
|
178397923b | ||
|
|
de4e90b98e | ||
|
|
5d66014044 | ||
|
|
bb60f8bb9f | ||
|
|
b199804827 | ||
|
|
37dc3cd24a | ||
|
|
a4710ef6bb | ||
|
|
28423d81bb | ||
|
|
21e94af8d1 | ||
|
|
6d980e319b | ||
|
|
4a21827b47 | ||
|
|
9600147240 | ||
|
|
ecc59376a3 | ||
|
|
4b4a23c92e | ||
|
|
c36eddb207 | ||
|
|
e160a10708 | ||
|
|
cdcd938cdf | ||
|
|
58e69be510 | ||
|
|
7f417c3a3f | ||
|
|
9378f96b73 | ||
|
|
7e78ace304 | ||
|
|
a3aa406e13 | ||
|
|
8371dc7baf | ||
|
|
44457349aa | ||
|
|
51226cf5cc | ||
|
|
7f307346f3 | ||
|
|
9de4493e89 | ||
|
|
c6680d0bd2 | ||
|
|
6679808a18 | ||
|
|
7a4fa3c50f | ||
|
|
7c0af85aaa | ||
|
|
8457194bb5 | ||
|
|
f07132c48b | ||
|
|
9c20b86373 | ||
|
|
293f34ca13 | ||
|
|
dca06cdc3c | ||
|
|
1cbf989577 | ||
|
|
5b3b7c72d2 | ||
|
|
cd274a6d2f | ||
|
|
edf4a94494 | ||
|
|
d5f46e6325 | ||
|
|
22ac5b0b54 | ||
|
|
bb29fc8010 | ||
|
|
c733796aeb | ||
|
|
c714a9d4c8 | ||
|
|
c436866a51 | ||
|
|
02480a96e9 | ||
|
|
3e8ea0473f | ||
|
|
99c2082022 | ||
|
|
4629ef00a9 | ||
|
|
a5b4d5046d | ||
|
|
29e64188c9 | ||
|
|
8a73ea0472 | ||
|
|
b402025ca5 | ||
|
|
bd475d1c87 | ||
|
|
816641d7c5 | ||
|
|
3028288bb3 | ||
|
|
089e980832 | ||
|
|
f93236b2d0 | ||
|
|
9b29623f6f | ||
|
|
0e50d7f8d5 | ||
|
|
9a8231c2b6 | ||
|
|
0a5be6467c | ||
|
|
716f3c6880 | ||
|
|
b20866fedd | ||
|
|
14e7b33b63 | ||
|
|
cfa297ac3f | ||
|
|
e0e64090cd | ||
|
|
68f8d869cc | ||
|
|
d07a2837d9 | ||
|
|
caee72f9c3 | ||
|
|
4870b7377a | ||
|
|
dd002e9f82 | ||
|
|
4743c9cb8f | ||
|
|
3132269038 | ||
|
|
9eb1f7709d | ||
|
|
4817641155 | ||
|
|
c74ceba495 | ||
|
|
0d339f9565 | ||
|
|
ee58794441 | ||
|
|
4ab2490534 | ||
|
|
9a47c42958 | ||
|
|
a35310e536 | ||
|
|
ecb9f5a260 | ||
|
|
8dd2371ce4 | ||
|
|
df782d7968 | ||
|
|
7672a3bbe8 | ||
|
|
9a56c124cc | ||
|
|
2fa9561a09 | ||
|
|
462d9c90b5 | ||
|
|
db72c4bf7d | ||
|
|
49984cb651 | ||
|
|
dcc76abf94 | ||
|
|
bc15a0997e | ||
|
|
31b4e93bed | ||
|
|
fe509365e2 | ||
|
|
2e5af796e4 | ||
|
|
e2d848b7e5 | ||
|
|
4d0d242162 | ||
|
|
1ffcd16efa | ||
|
|
02f85f808d | ||
|
|
91a7eb3f4c | ||
|
|
0c8c9f9b5f | ||
|
|
8ae09d0514 | ||
|
|
413a2f2fc1 | ||
|
|
9ff4a8159a | ||
|
|
abc0cfe461 | ||
|
|
9a67c45dab | ||
|
|
add6b66ea5 | ||
|
|
11fdf89340 | ||
|
|
9e37347877 | ||
|
|
ef81cba7eb | ||
|
|
799dd7fc92 | ||
|
|
a8e0f2377d | ||
|
|
59ea5de00a | ||
|
|
77eef9609a | ||
|
|
8e9ce634e3 | ||
|
|
4d0d330797 | ||
|
|
a5728bcc4d | ||
|
|
3444b1eeef | ||
|
|
a2f3910e27 | ||
|
|
01765f3e7f | ||
|
|
1baf62a2b7 | ||
|
|
e5f298eaa7 | ||
|
|
d312ce7cd8 | ||
|
|
0764bced76 | ||
|
|
c32f9cdd9f | ||
|
|
c58f03be08 | ||
|
|
960e78afac | ||
|
|
36ffbc6cdb | ||
|
|
01fea58e4c | ||
|
|
6fda122091 | ||
|
|
30264935dc | ||
|
|
4457941193 | ||
|
|
3121d9dca9 | ||
|
|
99b7a6ce99 | ||
|
|
751b031627 | ||
|
|
7f0e7b0ff7 | ||
|
|
bf2a5c7489 | ||
|
|
bdc5e8020c | ||
|
|
698c395f3d | ||
|
|
65960888aa | ||
|
|
bd3f961ee1 | ||
|
|
723fe6b90c | ||
|
|
17fc70d9ee | ||
|
|
fe5af96ff7 | ||
|
|
5dd58d0092 | ||
|
|
b565a0eb02 | ||
|
|
4b84e30195 | ||
|
|
ef8b3fed1c | ||
|
|
003ebbcc52 | ||
|
|
9f0adc0593 | ||
|
|
7fe25f474a | ||
|
|
4ef880c350 | ||
|
|
8295e3d25e | ||
|
|
d0dd6c9224 | ||
|
|
712a2e62e9 | ||
|
|
8f6c837e22 | ||
|
|
290b15d007 | ||
|
|
1139040f28 | ||
|
|
ebd72ef739 | ||
|
|
c639eaf86a | ||
|
|
c75b089cb7 | ||
|
|
4e18205cca | ||
|
|
31306583d0 | ||
|
|
4a4cdadef1 | ||
|
|
f6a94e0f7c | ||
|
|
30b3dcdce6 | ||
|
|
af0d788796 | ||
|
|
40d8092880 | ||
|
|
002d20dc0f | ||
|
|
d8221dc784 | ||
|
|
68e8941cc1 | ||
|
|
8271f117a4 | ||
|
|
21e73e013f | ||
|
|
b589329398 | ||
|
|
272cd502be | ||
|
|
1288fc3878 | ||
|
|
84fa71c64f | ||
|
|
f9b0c274b2 | ||
|
|
9654c41fb9 | ||
|
|
c5411899bc | ||
|
|
336d411627 | ||
|
|
8d8d5e340f | ||
|
|
9ef2cb1731 | ||
|
|
14817ef344 | ||
|
|
96b385342a | ||
|
|
2d97328eb7 | ||
|
|
6393a62410 | ||
|
|
d074f98d24 | ||
|
|
a6e3f71ca5 | ||
|
|
c78849e994 | ||
|
|
5609560b77 | ||
|
|
4bb91c605a | ||
|
|
9dfad913bc | ||
|
|
4815cac49b | ||
|
|
25d549b06f | ||
|
|
0c0801561e | ||
|
|
0fcd929c6f | ||
|
|
6aa7b9e98c | ||
|
|
8c0690b1e5 | ||
|
|
08524bd79a | ||
|
|
a893d85632 | ||
|
|
41f6ddd61b | ||
|
|
3a14bad2a4 | ||
|
|
a4ba3088b0 | ||
|
|
b17a0dcc0e | ||
|
|
26522cea3f | ||
|
|
543c4959e7 | ||
|
|
544a67c8ec | ||
|
|
f0977d9433 | ||
|
|
13ac1fb435 | ||
|
|
76c45b62d7 | ||
|
|
667be467a4 | ||
|
|
388ba05700 | ||
|
|
2620bb5b93 | ||
|
|
eba4a444bc | ||
|
|
4ec828b892 | ||
|
|
60677e262c | ||
|
|
598a04d084 | ||
|
|
9f27ea8aec | ||
|
|
ba7d1ddee2 | ||
|
|
4a65902217 | ||
|
|
0f371ffd0e | ||
|
|
3287421614 | ||
|
|
c0c5d6efc1 | ||
|
|
5bd4e45542 | ||
|
|
418b734c3f | ||
|
|
8e1dabbb80 | ||
|
|
b4d6ef62a1 | ||
|
|
066d1dfe1a | ||
|
|
32f83a4612 | ||
|
|
43c112eb25 | ||
|
|
2b5240a565 | ||
|
|
93b620f4a8 | ||
|
|
d8b2d53747 | ||
|
|
d83c4b12ec | ||
|
|
2e700d4385 | ||
|
|
87bad6a959 | ||
|
|
0b3b4f8a1a | ||
|
|
5a70869dd8 | ||
|
|
2a44494d88 | ||
|
|
96108aa520 | ||
|
|
de4b301ccb | ||
|
|
8153ad52ff | ||
|
|
4b2ef742d6 | ||
|
|
092fc67b0b | ||
|
|
5b83ae69dd | ||
|
|
c74d27f4ab | ||
|
|
63a52629a9 | ||
|
|
80959abe16 | ||
|
|
d048305193 | ||
|
|
a78a6638da | ||
|
|
99f2715601 | ||
|
|
e5f8d798d5 | ||
|
|
d2ab5610c3 | ||
|
|
5e43411854 | ||
|
|
39c09ef8e5 | ||
|
|
8c7602bb1a | ||
|
|
1dcf16ba2a | ||
|
|
181eb28828 | ||
|
|
ae66f80c3c | ||
|
|
b32a3e5ea3 | ||
|
|
48f7bf631e | ||
|
|
fc43022a95 | ||
|
|
9e867c3e16 | ||
|
|
b62dba096b | ||
|
|
553f49a469 | ||
|
|
deb0ce2482 | ||
|
|
21c87f95ef | ||
|
|
84803c171c | ||
|
|
94b48cef84 | ||
|
|
666424f79b | ||
|
|
9496a57b3c | ||
|
|
ff1281abde | ||
|
|
d13769861d | ||
|
|
5d15f74575 | ||
|
|
5f445872c8 | ||
|
|
d44853055a | ||
|
|
4ddee2b1c1 | ||
|
|
6031638260 | ||
|
|
39f647d05a | ||
|
|
a1b464e864 | ||
|
|
d657e8827a | ||
|
|
6cb89ef09f | ||
|
|
00941d8082 | ||
|
|
cf612fa0c7 | ||
|
|
6d5018c3fd | ||
|
|
f333d301b8 | ||
|
|
11c5d57c4e | ||
|
|
f269044c69 | ||
|
|
b1075eee16 | ||
|
|
5adfecf689 | ||
|
|
f44eacaf52 | ||
|
|
258f0036a6 | ||
|
|
4d33d459ee | ||
|
|
1cdbd92d35 | ||
|
|
871603f1bb | ||
|
|
032640b068 | ||
|
|
2e7c3a5352 | ||
|
|
c78370ec2a | ||
|
|
6813a74c13 | ||
|
|
3980673322 | ||
|
|
0b99235ec2 | ||
|
|
906881dc9c | ||
|
|
d5b6a3f2d6 | ||
|
|
4b4e47d17c | ||
|
|
6e2fcc53a5 | ||
|
|
dfaa3961bf | ||
|
|
1d002c4045 | ||
|
|
4e80d91c20 | ||
|
|
2525811db4 | ||
|
|
5583004515 | ||
|
|
a4a11b5801 | ||
|
|
9adc45095b | ||
|
|
c66fdf63db | ||
|
|
23a98b399c | ||
|
|
0ddf416b9d | ||
|
|
f0a2e2c46f | ||
|
|
3bd8061a93 | ||
|
|
b67a3fd0b4 | ||
|
|
e9df2bfa03 | ||
|
|
a75a11c9f6 | ||
|
|
ebd557ff71 | ||
|
|
0854b76dfa | ||
|
|
2e7b1ac09e | ||
|
|
4e15557949 | ||
|
|
e2c7134f61 | ||
|
|
6f67c4e8e1 | ||
|
|
d8d6509668 | ||
|
|
c9a1417d60 | ||
|
|
6cb8616c00 | ||
|
|
26ad8c39dc | ||
|
|
f2f406d581 | ||
|
|
b4ced77d57 | ||
|
|
5ee57b1b2f | ||
|
|
57e3edc24e | ||
|
|
fc984cef22 | ||
|
|
81b6cbe655 | ||
|
|
e838085291 | ||
|
|
0f6ebbfa76 | ||
|
|
ae081994c3 | ||
|
|
e67251c9ba | ||
|
|
270d697ce6 | ||
|
|
a050a56c19 | ||
|
|
ab7f837bb4 | ||
|
|
f2e682c3d3 | ||
|
|
237d112dec | ||
|
|
f8769e97f9 | ||
|
|
5ef7896f1d | ||
|
|
4a5627bf36 | ||
|
|
b521b79fe4 | ||
|
|
35f95aa0f2 | ||
|
|
de042abd79 | ||
|
|
ad1c58bbf5 | ||
|
|
764ca0f892 | ||
|
|
1018b6426e | ||
|
|
c74503beca | ||
|
|
7988b8c63e | ||
|
|
6ef19d53f4 | ||
|
|
dc00fd0277 | ||
|
|
96d14356be | ||
|
|
a3fb090593 | ||
|
|
5a4b833516 | ||
|
|
82b09f2c63 | ||
|
|
fd6a025de9 | ||
|
|
99bc5f14f7 | ||
|
|
ddc7b9a76f | ||
|
|
af03d4dafd | ||
|
|
09bc25f035 | ||
|
|
072b206035 | ||
|
|
af04ff9bf7 | ||
|
|
6bd63fc751 | ||
|
|
4ed6437ce3 | ||
|
|
b356591aba | ||
|
|
00db1d7bfd | ||
|
|
cc517eb4d3 | ||
|
|
4a8442cb33 | ||
|
|
7023324920 | ||
|
|
3cfab2c57b | ||
|
|
4caaeff0f0 | ||
|
|
98a6fb6637 | ||
|
|
50edf85de5 | ||
|
|
19b74b2671 | ||
|
|
7b6d2cd782 | ||
|
|
b457cf0b4d | ||
|
|
7f27f461f3 | ||
|
|
3909920a4c | ||
|
|
5ff35b1da4 | ||
|
|
90c71026da | ||
|
|
707107328a | ||
|
|
958244c9b2 | ||
|
|
adc2f759c4 | ||
|
|
c3c1a83e97 | ||
|
|
61fa6d5f74 | ||
|
|
9782462345 | ||
|
|
480b47ee3d | ||
|
|
2e528f8c7d | ||
|
|
7e4202db1b | ||
|
|
09a8019c66 | ||
|
|
e44bd68152 | ||
|
|
2066dbc716 | ||
|
|
73b2eba1f8 | ||
|
|
101c396ac2 | ||
|
|
3cf24c2ab6 | ||
|
|
4e0e6708e6 | ||
|
|
ebe5c342c9 | ||
|
|
37c1bc06d0 | ||
|
|
98209d0d5f | ||
|
|
159a5ae8b3 | ||
|
|
e9afa55aa0 | ||
|
|
e727658b24 | ||
|
|
eb0aa9473f | ||
|
|
981859de1f | ||
|
|
1889c6ae10 | ||
|
|
7e74bd1e4d | ||
|
|
4d1e859bbf | ||
|
|
492077ddb2 | ||
|
|
bca527eca0 | ||
|
|
707dc351ba | ||
|
|
cc55a19e1d | ||
|
|
8b215e553d | ||
|
|
af1679c92b | ||
|
|
41c11d763e | ||
|
|
1efd968b09 | ||
|
|
ede2dc201c | ||
|
|
2740522f05 | ||
|
|
6e3edd1e96 | ||
|
|
0326fa89ea | ||
|
|
dcdf3b21bc | ||
|
|
748da3ec0c | ||
|
|
78c9b24bb9 | ||
|
|
6dc7c2578b | ||
|
|
533da80986 | ||
|
|
b7107e3069 | ||
|
|
bea9d8a709 | ||
|
|
1dc39cf786 | ||
|
|
a15b478ac6 | ||
|
|
5fa4c42119 | ||
|
|
43a734bcc4 | ||
|
|
5c4cb7a8f9 | ||
|
|
cd8d2c255c | ||
|
|
b759e110f8 | ||
|
|
0dd2bcf07a | ||
|
|
77e9c9eb5d | ||
|
|
bbb7858508 | ||
|
|
868b2d309a | ||
|
|
0cdc415a64 | ||
|
|
9b3d672e78 | ||
|
|
0cfa5f8a32 | ||
|
|
907b718a3a | ||
|
|
fba6d86018 | ||
|
|
51b81f2ab6 | ||
|
|
ff16c70362 | ||
|
|
f928fac9da | ||
|
|
a2262eff3f | ||
|
|
62125f0873 | ||
|
|
f97f9296b6 | ||
|
|
3353ebb777 | ||
|
|
81760ec99d | ||
|
|
27f0d01e81 | ||
|
|
1bf653a5d8 | ||
|
|
c35b267658 | ||
|
|
26f8d3dc45 | ||
|
|
6620184fa0 | ||
|
|
1e8a96a52b | ||
|
|
c0d998345d | ||
|
|
ed2258208b | ||
|
|
f4244d5913 | ||
|
|
b3a17b26dc | ||
|
|
a52f9425e8 | ||
|
|
48eb959ab2 | ||
|
|
0f30cf3880 | ||
|
|
80431b7e83 | ||
|
|
c4fc075844 | ||
|
|
a24b1a3b4e | ||
|
|
601405349e | ||
|
|
332bf3256c | ||
|
|
6653ca2c11 | ||
|
|
d6e9a63b1f | ||
|
|
5cc9f83a64 | ||
|
|
da04cbcec0 | ||
|
|
1eff6702d7 | ||
|
|
6242c19397 | ||
|
|
194c4bad84 | ||
|
|
1b7ba7825e | ||
|
|
5689dd10a5 | ||
|
|
648064eac7 | ||
|
|
1ca6d068d0 | ||
|
|
f08c481807 | ||
|
|
f64b28af1b | ||
|
|
2a50d0f5a0 | ||
|
|
149d7358f0 | ||
|
|
a86e55eeae | ||
|
|
3979d37e76 | ||
|
|
d8d05b57cb | ||
|
|
f1d718a45f | ||
|
|
d33ab59378 | ||
|
|
f8e4a4fd45 | ||
|
|
6d099e0aab | ||
|
|
c5eb9767aa | ||
|
|
24672b7cf2 | ||
|
|
db6de22273 | ||
|
|
8cdb82765f | ||
|
|
172d7c0b80 | ||
|
|
cf86dd3f30 | ||
|
|
23c05b91d5 | ||
|
|
7ff3d7f1e5 | ||
|
|
912518c1ae | ||
|
|
9b825ee244 | ||
|
|
bc581d763b | ||
|
|
dd236d8f19 | ||
|
|
ff236ee6a1 | ||
|
|
66a6f992eb | ||
|
|
c6438bef67 | ||
|
|
ee5490939b | ||
|
|
65a2b47045 | ||
|
|
a56c21f856 | ||
|
|
7e501c794d | ||
|
|
c07fb33968 | ||
|
|
7ecb36a7be | ||
|
|
1cec07f8c5 | ||
|
|
ddcf191ade | ||
|
|
945e3bd239 | ||
|
|
09ed73300d | ||
|
|
83fa3b870c | ||
|
|
cb67787925 | ||
|
|
ad053ef889 | ||
|
|
ae92921b7b | ||
|
|
9ba053b807 | ||
|
|
2b8b581082 | ||
|
|
0b775ed380 | ||
|
|
a90f4b1c5a | ||
|
|
5bc2b385fa | ||
|
|
21f57444c8 | ||
|
|
662f18bceb | ||
|
|
2635b7d3c3 | ||
|
|
aac3910b43 | ||
|
|
0319981650 | ||
|
|
44e209d7b1 | ||
|
|
0f170c6daa | ||
|
|
67109bfe3c | ||
|
|
d22907c7d5 | ||
|
|
02155065f7 | ||
|
|
3c21b36e88 | ||
|
|
93fa042522 | ||
|
|
dcde2b125e | ||
|
|
f15c6be1a4 | ||
|
|
05208d3031 | ||
|
|
2b892fe783 | ||
|
|
c3c19db730 | ||
|
|
b70c8058e8 | ||
|
|
cdc59d0877 | ||
|
|
88d13ce77a | ||
|
|
f830c98b8e | ||
|
|
8de0dc2242 | ||
|
|
56e99912d4 | ||
|
|
9ed3c046b3 | ||
|
|
65791c55ca | ||
|
|
0422746267 | ||
|
|
cc3aca34f5 | ||
|
|
e39bdb6b03 | ||
|
|
27a36d2d44 | ||
|
|
60b7bb7e7e | ||
|
|
8ebaaefd6f | ||
|
|
201ab488b2 | ||
|
|
8b241709e1 | ||
|
|
d9cb12e882 | ||
|
|
5c78c567ca | ||
|
|
e3bcc6d3a6 | ||
|
|
05e8874d81 | ||
|
|
88e3ae7b51 | ||
|
|
02df0b6774 | ||
|
|
a941d0bfab | ||
|
|
2e837bec5d | ||
|
|
9b1a83bd69 | ||
|
|
b3553f80c6 | ||
|
|
5d76ff1590 | ||
|
|
6c57c5a98a | ||
|
|
770c4179a3 | ||
|
|
9164942395 | ||
|
|
e3ed816fb3 | ||
|
|
13ee098cfc | ||
|
|
f917eb8c93 | ||
|
|
989a0f361b | ||
|
|
52c1f61109 | ||
|
|
7dd6d46a5f | ||
|
|
3a1943ba87 | ||
|
|
ab1dd04a60 | ||
|
|
ccd88dad47 | ||
|
|
fdc9ba80e0 | ||
|
|
d1c62fd2b6 | ||
|
|
3e2cdd502c | ||
|
|
c78aed2551 | ||
|
|
e881178f2a | ||
|
|
b995a0b151 | ||
|
|
ec315c4747 | ||
|
|
52ff0c82cb | ||
|
|
d4ec2fbdef | ||
|
|
a9742a07c0 | ||
|
|
df1746976c | ||
|
|
61cfbe249c | ||
|
|
f9b50089dd | ||
|
|
95983dcf5b | ||
|
|
16e8941c15 | ||
|
|
cd4a098bff | ||
|
|
4a0940ad26 | ||
|
|
dd7251f18b | ||
|
|
3d727f07fa | ||
|
|
92883ee577 | ||
|
|
2790bea1d8 | ||
|
|
3f87b35816 | ||
|
|
bd86d1610a | ||
|
|
7f1b1b1ed3 | ||
|
|
09b8979ba0 | ||
|
|
02747c539b | ||
|
|
c1012586ce | ||
|
|
c9b6623eac | ||
|
|
d662bd0b65 | ||
|
|
ec60d4f143 | ||
|
|
373752f592 | ||
|
|
933e650183 | ||
|
|
6a6aa271ef | ||
|
|
012437e599 | ||
|
|
d3a64d8359 | ||
|
|
7451fccff9 | ||
|
|
1882139fac | ||
|
|
7fc72da905 | ||
|
|
9fa270da10 | ||
|
|
637595e8cd | ||
|
|
ceae25ea06 | ||
|
|
0cf0d2e790 | ||
|
|
45b76da1e8 | ||
|
|
9bb8dcd881 | ||
|
|
760cbb8228 | ||
|
|
4a214523c6 | ||
|
|
6345b1dbee | ||
|
|
228acadf5a | ||
|
|
6388895e6e | ||
|
|
725c4335e1 | ||
|
|
64deadda0b | ||
|
|
558f74d861 | ||
|
|
4eedecd1ce | ||
|
|
08f9d398c4 | ||
|
|
f102c84ea6 | ||
|
|
0c3bca0f9e | ||
|
|
ff1e134fe4 | ||
|
|
d8b48fe362 | ||
|
|
ac2482a645 | ||
|
|
5090809be8 | ||
|
|
80c593bc11 | ||
|
|
18b61ab74f | ||
|
|
ea22c7244c | ||
|
|
b1c9c3e124 | ||
|
|
93fc837b7a | ||
|
|
f0eda41c7c | ||
|
|
47717002e8 | ||
|
|
7b7513561d | ||
|
|
33bdaa7dbd | ||
|
|
b919691689 | ||
|
|
e90222e8db | ||
|
|
3cf57c1f91 | ||
|
|
f6e7229246 | ||
|
|
f55e74c8dc | ||
|
|
e25276658d | ||
|
|
d088c6f6b3 | ||
|
|
9361610647 | ||
|
|
7ed5e921bd | ||
|
|
39be49b481 | ||
|
|
3b7b5f98bd | ||
|
|
9be1b86c5d | ||
|
|
cfe9d3ab11 | ||
|
|
accb413636 | ||
|
|
bdac7b7899 | ||
|
|
58bc42cc0f | ||
|
|
44d7ce65ae | ||
|
|
c55cc68f5c | ||
|
|
d7cc874684 | ||
|
|
f1164bbd30 | ||
|
|
5f6d26c83e | ||
|
|
fcd341a1f4 | ||
|
|
6e5a4cff45 | ||
|
|
45fd75ab36 | ||
|
|
2f9bace3de | ||
|
|
964f697466 | ||
|
|
bb23f9cf93 | ||
|
|
440104a7d1 | ||
|
|
0c7c7946c6 | ||
|
|
386f9aae32 | ||
|
|
b5d0309f2b | ||
|
|
3e525b05a5 | ||
|
|
141e7fe416 | ||
|
|
db2e3bc8f2 | ||
|
|
66a6f4bbab | ||
|
|
a328ea9c3c | ||
|
|
76b8b74d41 | ||
|
|
5c4141dad9 | ||
|
|
e787872cc5 | ||
|
|
af818bda93 | ||
|
|
ccc774da0d | ||
|
|
32d61d9808 | ||
|
|
83a30fa088 | ||
|
|
f24cd97afa | ||
|
|
388770889f | ||
|
|
e3121fc49b | ||
|
|
f1958995f6 | ||
|
|
ba7b681e48 | ||
|
|
e4012a1301 | ||
|
|
6ff0d8bd61 | ||
|
|
898afc78ef | ||
|
|
c527f55721 | ||
|
|
89277c5668 | ||
|
|
28388497b8 | ||
|
|
09a2a96596 | ||
|
|
d3f6a02be2 | ||
|
|
c8cc0457e4 | ||
|
|
4d9e68d60b | ||
|
|
74585bfb7f | ||
|
|
ea766afba9 | ||
|
|
f10d848797 | ||
|
|
3bda97b0a7 | ||
|
|
19c39f636d | ||
|
|
8b7894a370 | ||
|
|
d1056bda99 | ||
|
|
5dbf9bd987 | ||
|
|
23494d0936 | ||
|
|
116d4b3ecf | ||
|
|
8b8f5b80b8 | ||
|
|
0b9abf39f1 | ||
|
|
9260d271a7 | ||
|
|
1720173a16 | ||
|
|
60190e099a | ||
|
|
db4bd56df2 | ||
|
|
affb6865a8 | ||
|
|
cab10717e9 | ||
|
|
ee870c4366 | ||
|
|
d61854f972 | ||
|
|
e45df2bac5 | ||
|
|
009e2080fc | ||
|
|
f265732741 | ||
|
|
d11326233f | ||
|
|
5536236100 | ||
|
|
b077a361b9 | ||
|
|
c0ad98f285 | ||
|
|
11307eae3b | ||
|
|
14da5f6a19 | ||
|
|
065f7ee038 | ||
|
|
710015d89e | ||
|
|
c5a173138c | ||
|
|
dfb2c903a4 | ||
|
|
01dc0cabbe | ||
|
|
d4796257b3 | ||
|
|
a9885874ee | ||
|
|
44e3f0c171 | ||
|
|
62b15609ff | ||
|
|
bddf7b750b | ||
|
|
c7af522cfb | ||
|
|
e4b0dbae82 | ||
|
|
c0c5d1afec | ||
|
|
4d87544b7b | ||
|
|
b50df2cdf3 | ||
|
|
c3d5c12e6b | ||
|
|
0077f172b6 | ||
|
|
089534fb47 | ||
|
|
bd851f6afd | ||
|
|
2d69f27a25 | ||
|
|
489b968ea3 | ||
|
|
7ab2779805 | ||
|
|
964b92f83a | ||
|
|
4445a745bf | ||
|
|
3b3327be7b | ||
|
|
70fe5a4441 | ||
|
|
22f90b2e40 | ||
|
|
dfe3b291a1 | ||
|
|
9331ba1276 | ||
|
|
7c39d6c53a | ||
|
|
e941f7c940 | ||
|
|
e0e935cf29 | ||
|
|
74c2db6ceb | ||
|
|
506f446b60 | ||
|
|
68a777c8df | ||
|
|
10c215d9bd | ||
|
|
28b7aaae9f | ||
|
|
e1950eba2b | ||
|
|
b08eb896a7 | ||
|
|
8c013f7126 | ||
|
|
01d96a19b9 | ||
|
|
10208fada8 | ||
|
|
d430f5d543 | ||
|
|
536c76b9bf | ||
|
|
c0c31a23cc | ||
|
|
6065b353fd | ||
|
|
7885200af4 | ||
|
|
415383393a | ||
|
|
f790264e44 | ||
|
|
d8afdecc89 | ||
|
|
46e1efff2a | ||
|
|
b39857cf24 | ||
|
|
c5e60785da | ||
|
|
e7cc1df201 | ||
|
|
d9fad70201 | ||
|
|
95c77d531d | ||
|
|
658f304ce5 | ||
|
|
82f71f9a07 | ||
|
|
1de374de3e | ||
|
|
9cef92199d | ||
|
|
fb60574f3d | ||
|
|
2d92c6a849 | ||
|
|
cbda2b196a | ||
|
|
9d042ff75f | ||
|
|
5d7db2d7e9 | ||
|
|
7d15179be0 | ||
|
|
490bcd87af | ||
|
|
4623d0abd2 | ||
|
|
1b47e38f79 | ||
|
|
09ca0487f8 | ||
|
|
3a7df3f16e | ||
|
|
63646b0d19 | ||
|
|
9b511b9d18 | ||
|
|
0180a384e1 | ||
|
|
c79eb90500 | ||
|
|
6129982df4 | ||
|
|
8ed9e08a60 | ||
|
|
c14150191f | ||
|
|
b7598627b1 | ||
|
|
475ddc21b3 | ||
|
|
61cb208f2f | ||
|
|
d0e01a83e3 | ||
|
|
7bb8f9c5af | ||
|
|
304e6e166a | ||
|
|
e75602fc8d | ||
|
|
e46c34558e | ||
|
|
0e70ed2661 | ||
|
|
04569f1e7e | ||
|
|
189e757100 | ||
|
|
623ff545b5 | ||
|
|
bbd972d860 | ||
|
|
bbf0b26025 | ||
|
|
e5acc5468f | ||
|
|
28ec227658 | ||
|
|
022b51488f | ||
|
|
2573a50190 | ||
|
|
d612bbb0f2 | ||
|
|
ef32eb70dd | ||
|
|
b331048dec | ||
|
|
f5979ef745 | ||
|
|
2ff2c2224a | ||
|
|
e1028ada43 | ||
|
|
cc10bce487 | ||
|
|
46ae544cfd | ||
|
|
b2bf9a4a4a | ||
|
|
0ed812c6f8 | ||
|
|
a375839506 | ||
|
|
519d0cd02a | ||
|
|
363d611e0f | ||
|
|
f75134c7e7 | ||
|
|
42e4c4649b | ||
|
|
5469d288ba | ||
|
|
c7b238f975 | ||
|
|
09209c150f | ||
|
|
2802448fe9 | ||
|
|
a167e976a8 | ||
|
|
8c848bd1ef | ||
|
|
d860cd0552 | ||
|
|
7d2bed2c8a | ||
|
|
35f90884d2 | ||
|
|
2667e29bb9 | ||
|
|
d00a5475ce | ||
|
|
809c924bfc | ||
|
|
bb944f7903 | ||
|
|
273ddb8b97 | ||
|
|
4180736065 | ||
|
|
7b129309e5 | ||
|
|
ece1d89780 | ||
|
|
26930c23df | ||
|
|
1b64b84bdc | ||
|
|
b3351ceb8b | ||
|
|
ae378409b9 | ||
|
|
43f2c8b1e0 | ||
|
|
20b627202e | ||
|
|
9cafb13b50 | ||
|
|
2bec9d4595 | ||
|
|
bf6884c60c | ||
|
|
8441e4e5dd | ||
|
|
a4d1d69a97 | ||
|
|
b72d692221 | ||
|
|
0f9a03fef9 | ||
|
|
87f02918f8 | ||
|
|
b5546b4957 | ||
|
|
83e08b3437 | ||
|
|
931a9433f3 | ||
|
|
679e9ed349 | ||
|
|
5e6225d14c | ||
|
|
b0a97ab941 | ||
|
|
92b201a6fa | ||
|
|
ee396b3102 | ||
|
|
4e14765e94 | ||
|
|
c6ef5970a5 | ||
|
|
968428cfe0 | ||
|
|
5d6ea6774b | ||
|
|
4331792b75 | ||
|
|
2506ba4353 | ||
|
|
187f1f9d37 | ||
|
|
6dd9520bda | ||
|
|
5f0323e0a9 | ||
|
|
f78a13bd2c | ||
|
|
46ec9ff999 | ||
|
|
a7f67dc72e | ||
|
|
8867fe9a1c | ||
|
|
6da8846460 | ||
|
|
057e21570b | ||
|
|
8c8b8c1747 | ||
|
|
cb1dadab9d | ||
|
|
29595670af | ||
|
|
6da3192fe8 | ||
|
|
c83a865032 | ||
|
|
316c4399ce | ||
|
|
40e82a3796 | ||
|
|
a7a7eb3e3f | ||
|
|
a4b1ef0005 | ||
|
|
326ad01983 | ||
|
|
6fbe7da71e | ||
|
|
4012b44344 | ||
|
|
2ee62ac900 | ||
|
|
d9e39f88a8 | ||
|
|
848f0b44f6 | ||
|
|
40335fb7ff | ||
|
|
ad5a84c3b8 | ||
|
|
f7073ec422 | ||
|
|
943660f98e | ||
|
|
5640a28fdb | ||
|
|
ab89b6e21a | ||
|
|
b38ada0b73 | ||
|
|
f72f894727 | ||
|
|
cf4854c78d | ||
|
|
3004018ea9 | ||
|
|
6b6280a782 | ||
|
|
f9577909ff | ||
|
|
49f9310fc3 | ||
|
|
abc12e38b5 | ||
|
|
57c66955f6 | ||
|
|
5112117155 | ||
|
|
8d67571319 | ||
|
|
1885482055 | ||
|
|
b67df96c85 | ||
|
|
71f5dc9ef1 | ||
|
|
6efb5a679d | ||
|
|
9ae34fa667 | ||
|
|
3136b47838 | ||
|
|
c15e9c203e | ||
|
|
c72c1e16fd | ||
|
|
03712558e8 | ||
|
|
6689932393 | ||
|
|
140f933db7 | ||
|
|
6f1dcb4632 | ||
|
|
b89e563023 | ||
|
|
5a37ba8be0 | ||
|
|
37d47efe2c | ||
|
|
f6c5be24d8 | ||
|
|
0c8241fba7 | ||
|
|
dc2cda58b2 | ||
|
|
936074081c | ||
|
|
a556378ffe | ||
|
|
960dda8d40 | ||
|
|
a401a2e13a | ||
|
|
f51f7ef412 | ||
|
|
04151168ca | ||
|
|
99a93001bc | ||
|
|
e6339bb4c2 | ||
|
|
7209f972d2 | ||
|
|
0714918338 | ||
|
|
94d581a4f3 | ||
|
|
577e864b6a | ||
|
|
96a3ef44f6 | ||
|
|
3a33153361 | ||
|
|
ad0c18dceb | ||
|
|
e964679154 | ||
|
|
6c9ace146d | ||
|
|
c7409e4dec | ||
|
|
a4ff89cec0 | ||
|
|
b489f46910 | ||
|
|
4a167a00bd | ||
|
|
2aca7620e7 | ||
|
|
1f7f3bfdb1 | ||
|
|
90ff8ceb72 | ||
|
|
e8b69cc6b9 | ||
|
|
9143e74a72 | ||
|
|
7bb6e2ae45 | ||
|
|
6f4f500aec | ||
|
|
d328bdb4d1 | ||
|
|
7cb3d19e85 | ||
|
|
7c320b7f23 | ||
|
|
f2cca1e14b | ||
|
|
f3553c3c59 | ||
|
|
5daddc5fef | ||
|
|
8cee9fb019 | ||
|
|
38e4122570 | ||
|
|
3c178dbb96 | ||
|
|
4537a95d2d | ||
|
|
fd4dfc2dff | ||
|
|
2d9ec631aa | ||
|
|
b59172d608 | ||
|
|
790e42035f | ||
|
|
7782fe389e | ||
|
|
84b64d3283 | ||
|
|
66a83a118f | ||
|
|
3494ff0491 | ||
|
|
d51fc88813 | ||
|
|
1930014498 | ||
|
|
4189ee4e54 | ||
|
|
a191e295da | ||
|
|
0709c68653 | ||
|
|
084c306159 | ||
|
|
2ed77a3332 | ||
|
|
5b2c5a6e2f | ||
|
|
5063ce815d | ||
|
|
78323103fd | ||
|
|
772005910c | ||
|
|
1f36d8ef25 | ||
|
|
1b8d65c3ea | ||
|
|
07df9ced75 | ||
|
|
9dfd73b090 | ||
|
|
0ebb34a2df | ||
|
|
6869cd62ea | ||
|
|
b7b73bb409 | ||
|
|
7bf3728cf9 | ||
|
|
cc01312ae5 | ||
|
|
cf94615e71 | ||
|
|
51cb36b2e6 | ||
|
|
345df7a7b7 | ||
|
|
4961727237 | ||
|
|
6640130ef9 | ||
|
|
e38778d9e7 | ||
|
|
63e9b705a9 | ||
|
|
38864ca666 | ||
|
|
adab4aee48 | ||
|
|
3bad5c5e55 | ||
|
|
7607c10bdc | ||
|
|
4349f2bd3a | ||
|
|
62abd3c900 | ||
|
|
63193c82a1 | ||
|
|
6431577bf1 | ||
|
|
b56a0d58bf | ||
|
|
00774739ed | ||
|
|
b98cc8ed79 | ||
|
|
81faf3f7ee | ||
|
|
14c72e8259 | ||
|
|
94d719a814 | ||
|
|
66fecf1509 | ||
|
|
e6e3df701d | ||
|
|
76aaaddb5a | ||
|
|
fa23263b19 | ||
|
|
2ba2014f82 | ||
|
|
ee6090c103 | ||
|
|
063158e9e0 | ||
|
|
a1ac80c180 | ||
|
|
42a8c43ff5 | ||
|
|
1fcd3cc309 | ||
|
|
66cdb621ca | ||
|
|
a618903f0f | ||
|
|
4fc39f10b1 | ||
|
|
b393ddcd74 | ||
|
|
7aa80154fa | ||
|
|
262f42c194 | ||
|
|
18d16a2211 | ||
|
|
7c91795fb5 | ||
|
|
856e13d37b | ||
|
|
2223919a98 | ||
|
|
7b4063420e | ||
|
|
839ff7463e | ||
|
|
7587b8bc25 | ||
|
|
756675e622 | ||
|
|
2f059cc783 | ||
|
|
573d5a89f9 | ||
|
|
40472bdced | ||
|
|
ac7124a7e6 | ||
|
|
888674b776 | ||
|
|
c9f6088754 | ||
|
|
687aada611 | ||
|
|
20a1e8f1d7 | ||
|
|
357d6c3406 | ||
|
|
40ff959289 | ||
|
|
22825d1520 | ||
|
|
827a8258ec | ||
|
|
8505be882c | ||
|
|
d0eb4103ba | ||
|
|
ba83027dc9 | ||
|
|
7b3fa0b224 | ||
|
|
eb7f83433c | ||
|
|
9d5ca7c36d | ||
|
|
a9511dcb51 | ||
|
|
5d42e2b16e | ||
|
|
6fac6ecd4d | ||
|
|
b876695adc | ||
|
|
b220b8bce4 | ||
|
|
ba4d707d45 | ||
|
|
2920d2a7ae | ||
|
|
b8abef6aeb | ||
|
|
75fc34cbe3 | ||
|
|
206fc398c6 | ||
|
|
b95b77bcb9 | ||
|
|
62c269cac2 | ||
|
|
4b3474ff42 | ||
|
|
d6e9b929e9 | ||
|
|
13efce42a0 | ||
|
|
85ccc18485 | ||
|
|
9df08cdf24 | ||
|
|
7892eed443 | ||
|
|
eca68c06c3 | ||
|
|
3c82ff1c4e | ||
|
|
8c6aff1623 | ||
|
|
254a1e3381 | ||
|
|
dff4c833f1 | ||
|
|
5fa3a591d4 | ||
|
|
845578a1dd | ||
|
|
84ac72b391 | ||
|
|
db364d9bf7 | ||
|
|
3fe01f8def | ||
|
|
02c815077e | ||
|
|
108eb759ec | ||
|
|
fef49a0d6a | ||
|
|
87241fa8bd | ||
|
|
29d5192fff | ||
|
|
c86e55719e | ||
|
|
839a8a780c | ||
|
|
346334a0ba | ||
|
|
7cd4d180c2 | ||
|
|
320ef4fbc7 | ||
|
|
b4623141f3 | ||
|
|
30d3ed14d7 | ||
|
|
ae617d5154 | ||
|
|
9dd3c568d8 | ||
|
|
f31fc7691e | ||
|
|
27df922383 | ||
|
|
d5956c024d | ||
|
|
29aca74651 | ||
|
|
f41790b302 | ||
|
|
0b999a874c | ||
|
|
2778638dc9 | ||
|
|
cc5fe445fc | ||
|
|
c310f9c57e | ||
|
|
cd607425a0 | ||
|
|
a5df8a1110 | ||
|
|
2bd30aa346 | ||
|
|
a6ce994fd0 | ||
|
|
7bffd1c3c7 | ||
|
|
dfd92d6db6 | ||
|
|
5529872bd5 | ||
|
|
364a530956 | ||
|
|
0556d5a067 | ||
|
|
be46893555 | ||
|
|
f7f789892d | ||
|
|
de0d327168 | ||
|
|
e52075a692 | ||
|
|
a29c50eae3 | ||
|
|
8a2a497fcf | ||
|
|
6786988e63 | ||
|
|
2dd5e2d96c | ||
|
|
03320d9cec | ||
|
|
2101cdeb86 | ||
|
|
f0a8ca5823 | ||
|
|
daed389264 | ||
|
|
6a558ad25c | ||
|
|
6e3a4e1125 | ||
|
|
79cb4b995a | ||
|
|
84b9dd0841 | ||
|
|
4004dcd99e | ||
|
|
ebbe7e8917 | ||
|
|
196d5c6cfd | ||
|
|
f4626a7cd5 | ||
|
|
4ed97d4600 | ||
|
|
c2cf05ef64 | ||
|
|
0e131f6661 | ||
|
|
d091c47a0c | ||
|
|
6bd5d26882 | ||
|
|
e02ea116ff | ||
|
|
012d1d94d5 | ||
|
|
39e49b08d9 | ||
|
|
2a84d2dc41 | ||
|
|
66c9b38e04 | ||
|
|
8f35f7a573 | ||
|
|
c653563512 | ||
|
|
a75821ed06 | ||
|
|
ca6416c697 | ||
|
|
f7b3caf320 | ||
|
|
62d839b69b | ||
|
|
a9c3ea953d | ||
|
|
5be9720fcc | ||
|
|
ec096b5831 | ||
|
|
51c5e5f32c | ||
|
|
b6d7e0b0e9 | ||
|
|
49209c4c4a | ||
|
|
6b466ac7d7 | ||
|
|
d6182f9e03 | ||
|
|
2e24a298ff | ||
|
|
d7d43bc7be | ||
|
|
2d0c4ea738 | ||
|
|
af4e802259 | ||
|
|
ad9e97161c | ||
|
|
1712f509dc | ||
|
|
78dd3b2785 | ||
|
|
f7f4638526 | ||
|
|
66690a6f89 | ||
|
|
4e56eb9bde | ||
|
|
e3349e41a1 | ||
|
|
239ccb9018 | ||
|
|
6ff17e1ba6 | ||
|
|
1c88a90024 | ||
|
|
dd54e5b97a | ||
|
|
f4180eec14 | ||
|
|
b81576bdaf | ||
|
|
bf43926d7d | ||
|
|
1ec0d8540a | ||
|
|
91a2da9032 | ||
|
|
a2cbeb87d5 | ||
|
|
a88f8c3316 | ||
|
|
e51af38b75 | ||
|
|
6a92f955ca | ||
|
|
a7ef5a8147 | ||
|
|
a14263bf27 | ||
|
|
489b09a54e | ||
|
|
51b57a0b1d | ||
|
|
eafb922ae9 | ||
|
|
6cfbdd7acd | ||
|
|
f8fd06706b | ||
|
|
c23ef771be | ||
|
|
0bbb1e070c | ||
|
|
fba11ae4b9 | ||
|
|
6fbb98ca7b | ||
|
|
b33c8ffd6d | ||
|
|
14b3bfbae7 | ||
|
|
7386c93d73 | ||
|
|
4af8d7dce1 | ||
|
|
08d2ccdab4 | ||
|
|
0a8abeefbe | ||
|
|
f90e089f8a | ||
|
|
1eb905fe30 | ||
|
|
fcc55288b1 | ||
|
|
97c5dc4363 | ||
|
|
4223f0bc5d | ||
|
|
7ada4515aa | ||
|
|
8456f2b30b | ||
|
|
c20802f89c | ||
|
|
804f993b25 | ||
|
|
869b83804d | ||
|
|
9d97feb184 | ||
|
|
42a69609a1 | ||
|
|
3a541f71a6 | ||
|
|
d75e4af348 | ||
|
|
2dac54b3ec | ||
|
|
82cf1658cb | ||
|
|
127e1f6673 | ||
|
|
22f1e4024e | ||
|
|
b0bfe2ac06 | ||
|
|
d67bb8be50 | ||
|
|
eeac702cd7 | ||
|
|
c2216ab054 | ||
|
|
45e5494653 | ||
|
|
5f7ed22711 | ||
|
|
49a823895d | ||
|
|
92324e3409 | ||
|
|
13057af98a | ||
|
|
144ff4af05 | ||
|
|
315e0627d1 | ||
|
|
4bd6a766f5 | ||
|
|
32f99cc678 | ||
|
|
bd1800c2b5 | ||
|
|
bf3e6230a0 | ||
|
|
2400123a92 | ||
|
|
525e399423 | ||
|
|
5da644a607 | ||
|
|
3d231ea8be | ||
|
|
d9ad230804 | ||
|
|
41a92a871d | ||
|
|
5634e0787f | ||
|
|
8a7406aa22 | ||
|
|
ab24042863 | ||
|
|
5d3891c1db | ||
|
|
7d9fc13192 | ||
|
|
12f944c79d | ||
|
|
e0da65e64f | ||
|
|
857b4de792 | ||
|
|
c896be5ece | ||
|
|
6c96c4afe5 | ||
|
|
6b2e59c09d | ||
|
|
481cad1a46 | ||
|
|
62ecf3dd51 | ||
|
|
116dde1b7e | ||
|
|
5db097d67c | ||
|
|
fbcdbf3b48 | ||
|
|
479f956db3 | ||
|
|
72d16c4d18 | ||
|
|
2984aac0a5 | ||
|
|
8ddf85c1be | ||
|
|
8f3a2f16ad | ||
|
|
6a72bc63c0 | ||
|
|
f74d8bfc16 | ||
|
|
29e293e6b5 | ||
|
|
2883c63e07 | ||
|
|
dfaf45a83d | ||
|
|
c2b99552da | ||
|
|
e4a92e0f2b | ||
|
|
dabf8563b8 | ||
|
|
fb9c138eb4 | ||
|
|
7da0e2509c | ||
|
|
63c2d607cc | ||
|
|
9f66cb91fc | ||
|
|
91db6caec9 | ||
|
|
4a4940f04d | ||
|
|
952df91619 | ||
|
|
b359ca58ba | ||
|
|
fe121ee89b | ||
|
|
9e3859e2c2 | ||
|
|
f4ca63af9d | ||
|
|
61ee1fc5be | ||
|
|
ead7aa3011 | ||
|
|
720df22df5 | ||
|
|
74c56c2061 | ||
|
|
e391fcbb6e | ||
|
|
87e1156b02 | ||
|
|
a77708dfbf | ||
|
|
664f34ed5b | ||
|
|
48d1db3b79 | ||
|
|
7e32727773 | ||
|
|
1a3396b293 | ||
|
|
d883a81602 | ||
|
|
81bdd52edd | ||
|
|
63483a2099 | ||
|
|
b68b4eb8da | ||
|
|
d8cc218139 | ||
|
|
0226a696d4 | ||
|
|
c9eb0f0e01 | ||
|
|
5c293e79cf | ||
|
|
5cdb7426c6 | ||
|
|
cc73f471d2 | ||
|
|
dc3240f224 | ||
|
|
04eda1ffbd | ||
|
|
39d790c1c3 | ||
|
|
3e3d0de7ca | ||
|
|
165b75487b | ||
|
|
19b351ef2a | ||
|
|
83575aa1eb | ||
|
|
26c9a236ec | ||
|
|
da7f72544f | ||
|
|
444f031f57 | ||
|
|
3e7d06a2aa | ||
|
|
e6b8e55966 | ||
|
|
fe1a1cc3cb | ||
|
|
2f17e04e1e | ||
|
|
41d175a19f | ||
|
|
8266167c02 | ||
|
|
2cfc4b97f4 | ||
|
|
fbad5f9d98 | ||
|
|
ac6b0c52d0 | ||
|
|
413c526a6a | ||
|
|
622021913d | ||
|
|
5ed5a86e0d | ||
|
|
3bf4f273d2 | ||
|
|
0e6c78a6c0 | ||
|
|
71cd52d30a | ||
|
|
d35b920470 | ||
|
|
5a4355044f | ||
|
|
b74d4b18e7 | ||
|
|
a53b76415b | ||
|
|
a286ee760d | ||
|
|
92b72db25c | ||
|
|
eed7bfa158 | ||
|
|
549644a224 | ||
|
|
ecec8be386 | ||
|
|
46b423e3e6 | ||
|
|
1206977907 | ||
|
|
b38fd26b77 | ||
|
|
302e7d9a45 | ||
|
|
b7a986c33c | ||
|
|
6fc474cff4 | ||
|
|
4bcc1b2680 | ||
|
|
318bae54a1 | ||
|
|
959d20fe6f | ||
|
|
00277117f1 | ||
|
|
42613dfc76 | ||
|
|
90df714a44 | ||
|
|
62fc0e1d59 | ||
|
|
e3679fd1dc | ||
|
|
9626823f0c | ||
|
|
9fc795afac | ||
|
|
6610f13619 | ||
|
|
f9401d91c4 | ||
|
|
52e6965472 | ||
|
|
0343c91f1c | ||
|
|
cce1b4f446 | ||
|
|
db1981b5fe | ||
|
|
cae15b7f39 | ||
|
|
26e43bd548 | ||
|
|
f6cbaffd3b | ||
|
|
4d4ddb50ac | ||
|
|
9ed175191b | ||
|
|
4d5c3acff5 |
+1
-1
@@ -7,7 +7,7 @@ indent_size = 4
|
||||
indent_style = space
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
max_line_length = 100
|
||||
max_line_length = 130
|
||||
tab_width = 4
|
||||
|
||||
[*.{kt,kts}]
|
||||
|
||||
+17
-3
@@ -44,6 +44,8 @@ captures/
|
||||
|
||||
# IntelliJ
|
||||
*.iml
|
||||
.idea/deviceManager.xml
|
||||
.idea/androidTestResultsUserPreferences.xml
|
||||
.idea/workspace.xml
|
||||
.idea/tasks.xml
|
||||
.idea/gradle.xml
|
||||
@@ -57,10 +59,16 @@ captures/
|
||||
# Comment next line if keeping position of elements in Navigation Editor is relevant for you
|
||||
.idea/navEditor.xml
|
||||
|
||||
.idea/AndroidProjectSystem.xml
|
||||
.idea/runConfigurations.xml
|
||||
.idea/deploymentTargetSelector.xml
|
||||
|
||||
# Keystore files
|
||||
# Uncomment the following lines if you do not want to check your keystore files in.
|
||||
#*.jks
|
||||
#*.keystore
|
||||
*.jks
|
||||
*.keystore
|
||||
*.p12
|
||||
*.pem
|
||||
*.key
|
||||
|
||||
# External native build folder generated in Android Studio 2.2 and later
|
||||
.externalNativeBuild
|
||||
@@ -140,6 +148,7 @@ output.json
|
||||
hs_err_pid*
|
||||
|
||||
### Kotlin ###
|
||||
.kotlin/
|
||||
# Compiled class file
|
||||
|
||||
# Log file
|
||||
@@ -306,4 +315,9 @@ fabric.properties
|
||||
app/debug/
|
||||
app/release/
|
||||
|
||||
.junie/
|
||||
.kiro/
|
||||
.omo/
|
||||
.antigravitycli/
|
||||
|
||||
# End of https://www.toptal.com/developers/gitignore/api/macos,android,androidstudio,visualstudiocode,git,kotlin,java
|
||||
|
||||
Generated
+158
@@ -0,0 +1,158 @@
|
||||
<component name="ProjectCodeStyleConfiguration">
|
||||
<code_scheme name="Project" version="173">
|
||||
<JavaCodeStyleSettings>
|
||||
<option name="IMPORT_LAYOUT_TABLE">
|
||||
<value>
|
||||
<package name="" withSubpackages="true" static="false" module="true" />
|
||||
<package name="android" withSubpackages="true" static="true" />
|
||||
<package name="androidx" withSubpackages="true" static="true" />
|
||||
<package name="com" withSubpackages="true" static="true" />
|
||||
<package name="junit" withSubpackages="true" static="true" />
|
||||
<package name="net" withSubpackages="true" static="true" />
|
||||
<package name="org" withSubpackages="true" static="true" />
|
||||
<package name="java" withSubpackages="true" static="true" />
|
||||
<package name="javax" withSubpackages="true" static="true" />
|
||||
<package name="" withSubpackages="true" static="true" />
|
||||
<emptyLine />
|
||||
<package name="android" withSubpackages="true" static="false" />
|
||||
<emptyLine />
|
||||
<package name="androidx" withSubpackages="true" static="false" />
|
||||
<emptyLine />
|
||||
<package name="com" withSubpackages="true" static="false" />
|
||||
<emptyLine />
|
||||
<package name="junit" withSubpackages="true" static="false" />
|
||||
<emptyLine />
|
||||
<package name="net" withSubpackages="true" static="false" />
|
||||
<emptyLine />
|
||||
<package name="org" withSubpackages="true" static="false" />
|
||||
<emptyLine />
|
||||
<package name="java" withSubpackages="true" static="false" />
|
||||
<emptyLine />
|
||||
<package name="javax" withSubpackages="true" static="false" />
|
||||
<emptyLine />
|
||||
<package name="" withSubpackages="true" static="false" />
|
||||
<emptyLine />
|
||||
</value>
|
||||
</option>
|
||||
</JavaCodeStyleSettings>
|
||||
<JetCodeStyleSettings>
|
||||
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
|
||||
</JetCodeStyleSettings>
|
||||
<codeStyleSettings language="XML">
|
||||
<option name="FORCE_REARRANGE_MODE" value="1" />
|
||||
<indentOptions>
|
||||
<option name="CONTINUATION_INDENT_SIZE" value="4" />
|
||||
</indentOptions>
|
||||
<arrangement>
|
||||
<rules>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>xmlns:android</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>xmlns:.*</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
<order>BY_NAME</order>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>.*:id</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>.*:name</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>name</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>style</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>.*</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
<order>BY_NAME</order>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>.*</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
<order>ANDROID_ATTRIBUTE_ORDER</order>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>.*</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>.*</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
<order>BY_NAME</order>
|
||||
</rule>
|
||||
</section>
|
||||
</rules>
|
||||
</arrangement>
|
||||
</codeStyleSettings>
|
||||
<codeStyleSettings language="kotlin">
|
||||
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
|
||||
</codeStyleSettings>
|
||||
</code_scheme>
|
||||
</component>
|
||||
Generated
+5
@@ -0,0 +1,5 @@
|
||||
<component name="ProjectCodeStyleConfiguration">
|
||||
<state>
|
||||
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
|
||||
</state>
|
||||
</component>
|
||||
Generated
+10
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="deploymentTargetDropDown">
|
||||
<value>
|
||||
<entry key="app">
|
||||
<State />
|
||||
</entry>
|
||||
</value>
|
||||
</component>
|
||||
</project>
|
||||
Generated
+6
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="JunieProject"><![CDATA[{
|
||||
"guidelinesPath": "AGENTS.md"
|
||||
}]]></component>
|
||||
</project>
|
||||
Generated
+8
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="MarkdownSettings">
|
||||
<option name="previewPanelProviderInfo">
|
||||
<ProviderInfo name="Compose (experimental)" className="com.intellij.markdown.compose.preview.ComposePanelProvider" />
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
||||
Generated
+10
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectMigrations">
|
||||
<option name="MigrateToGradleLocalJavaHome">
|
||||
<set>
|
||||
<option value="$PROJECT_DIR$" />
|
||||
</set>
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
||||
@@ -0,0 +1,134 @@
|
||||
# AGENTS.md
|
||||
`SodaLive` 저장소에서 작업하는 에이전트 실행 가이드다.
|
||||
|
||||
## 커뮤니케이션 규칙
|
||||
- **"질문에 대한 답변과 설명과 Todo는 한국어로 한다."**
|
||||
- 이 저장소에서 사용자에게 전달하는 설명, 진행 상황, 결과 보고는 한국어로 작성한다.
|
||||
- 코드 식별자, 경로, 명령어는 원문(영문) 그대로 유지한다.
|
||||
|
||||
## CORE EXECUTION PRINCIPLES (andrej-karpathy-skills)
|
||||
These principles override plugin behavior, skill behavior, workflow behavior, and default model behavior unless the user's direct instruction explicitly says otherwise.
|
||||
|
||||
Behavioral guidelines to reduce common LLM coding mistakes. Merge with project-specific instructions as needed.
|
||||
|
||||
**Tradeoff:** These guidelines bias toward caution over speed. For trivial tasks, use judgment.
|
||||
|
||||
## 1. Think Before Coding
|
||||
|
||||
**Don't assume. Don't hide confusion. Surface tradeoffs.**
|
||||
|
||||
Before implementing:
|
||||
- State your assumptions explicitly. If uncertain, ask.
|
||||
- If multiple interpretations exist, present them - don't pick silently.
|
||||
- If a simpler approach exists, say so. Push back when warranted.
|
||||
- If something is unclear, stop. Name what's confusing. Ask.
|
||||
|
||||
## 2. Simplicity First
|
||||
|
||||
**Minimum code that solves the problem. Nothing speculative.**
|
||||
|
||||
- No features beyond what was asked.
|
||||
- No abstractions for single-use code.
|
||||
- No "flexibility" or "configurability" that wasn't requested.
|
||||
- No error handling for impossible scenarios.
|
||||
- If you write 200 lines and it could be 50, rewrite it.
|
||||
|
||||
Ask yourself: "Would a senior engineer say this is overcomplicated?" If yes, simplify.
|
||||
|
||||
## 3. Surgical Changes
|
||||
|
||||
**Touch only what you must. Clean up only your own mess.**
|
||||
|
||||
When editing existing code:
|
||||
- Don't "improve" adjacent code, comments, or formatting.
|
||||
- Don't refactor things that aren't broken.
|
||||
- Match existing style, even if you'd do it differently.
|
||||
- If you notice unrelated dead code, mention it - don't delete it.
|
||||
|
||||
When your changes create orphans:
|
||||
- Remove imports/variables/functions that YOUR changes made unused.
|
||||
- Don't remove pre-existing dead code unless asked.
|
||||
|
||||
The test: Every changed line should trace directly to the user's request.
|
||||
|
||||
## 4. Goal-Driven Execution
|
||||
|
||||
**Define success criteria. Loop until verified.**
|
||||
|
||||
Transform tasks into verifiable goals:
|
||||
- "Add validation" → "Write tests for invalid inputs, then make them pass"
|
||||
- "Fix the bug" → "Write a test that reproduces it, then make it pass"
|
||||
- "Refactor X" → "Ensure tests pass before and after"
|
||||
|
||||
For multi-step tasks, state a brief plan:
|
||||
```
|
||||
1. [Step] → verify: [check]
|
||||
2. [Step] → verify: [check]
|
||||
3. [Step] → verify: [check]
|
||||
```
|
||||
|
||||
Strong success criteria let you loop independently. Weak criteria ("make it work") require constant clarification.
|
||||
|
||||
---
|
||||
|
||||
**These guidelines are working if:** fewer unnecessary changes in diffs, fewer rewrites due to overcomplication, and clarifying questions come before implementation rather than after mistakes.
|
||||
|
||||
## 실행 원칙 및 계층 사용 정책
|
||||
- 불확실하거나 모호한 경우 추측하지 말고 확인하거나, 가능한 최소 범위의 안전한 조치를 취한다.
|
||||
|
||||
### 기본 모드: 보수적 실행
|
||||
- 최소 변경
|
||||
- 단순한 구현
|
||||
- 검증 가능한 결과
|
||||
|
||||
### 확장 모드
|
||||
- 사용자가 명시적으로 요청한 경우에만 사용한다.
|
||||
- 대규모 리팩토링, 브레인스토밍, 다중 에이전트 실행, 병렬 workflow를 허용한다.
|
||||
|
||||
### 에이전트 동작 원칙
|
||||
- 추측하지 말고 근거 파일을 읽고 결정한다.
|
||||
- 기존 관례를 깨는 변경은 이유가 명확할 때만 수행한다.
|
||||
- 불필요한 리팩터링 확장은 피하고 요청 범위를 우선 충족한다.
|
||||
|
||||
## 저장소 범위
|
||||
- Android Gradle 프로젝트이며 `settings.gradle` 기준 모듈은 `:app` 단일 구성이다.
|
||||
- 모든 명령은 저장소 루트에서 실행한다.
|
||||
- 요청 범위를 우선 충족하고, 변경은 작고 안전하게 유지한다.
|
||||
- 기존 로직 수정이 아닌 신규 `Activity`, `Fragment`, `ViewModel` 및 그와 연결된 하위 코드는 `kr.co.vividnext.sodalive.v2` 패키지 하위에 작성한다.
|
||||
|
||||
## 레거시 코드 사용 원칙
|
||||
- 레거시 코드는 직접 수정하지 않는다.
|
||||
- 레거시 기능이 필요하면 기존 코드를 호출해서 사용한다.
|
||||
- 레거시 코드를 약간 변경해야 사용할 수 있는 경우에도 레거시 파일을 고치지 말고, 신규 파일에 wrapper/adapter/확장 코드를 추가해 사용한다.
|
||||
- 레거시 코드 변경이 불가피해 보이면 구현 전에 사용자에게 확인한다.
|
||||
|
||||
## 작업 절차 핵심 규칙
|
||||
- 모든 구현 작업은 PRD 문서와 구현 계획/TASK 문서가 모두 준비된 뒤에 시작한다.
|
||||
- 사용자의 프롬프트를 받으면 먼저 PRD 문서를 작성한다.
|
||||
- PRD 작성 중 애매하거나 더 필요한 내용, 결정해야 하는 사항이 있으면 애매한 사항이 없어질 때까지 사용자와 인터뷰한다.
|
||||
- 인터뷰 내용을 PRD에 반영한 뒤, PRD를 기준으로 계획/TASK 문서를 작성하고 그 문서에 따라 필요한 내용만 최소 구현한다.
|
||||
- 문서는 `docs/[날짜]_구현할내용한글/` 아래에 `prd.md`, `plan-task.md`로 만든다.
|
||||
- `docs/[날짜]_구현할내용한글/prd.md`
|
||||
- `docs/[날짜]_구현할내용한글/plan-task.md`
|
||||
- 문서 폴더명에서 원래 띄어쓰기가 들어갈 위치는 공백 대신 `_`를 사용한다.
|
||||
- 예: `docs/20260601_메인_홈_추천_UI와_API_연동/`
|
||||
- 기존에 생성된 `docs/prd/`, `docs/plan-task/` 문서는 유지하고, 신규 생성 문서부터 위 구조를 적용한다.
|
||||
- 날짜는 `YYYYMMDD` 8자리 숫자를 사용한다.
|
||||
- PRD 문서는 `sample-prd.md` 파일에서 작업에 필요한 부분만 발췌해 작성한다. `sample-prd.md`가 없거나 위치가 불명확하면 추측하지 말고 사용자에게 확인한다.
|
||||
- 연속된 하나의 작업이라면 별도 새 문서를 만들지 말고 기존 PRD와 계획/TASK 문서에 추가 작업으로 이어서 기록한다.
|
||||
- 작업 도중 범위가 변경되면 계획/TASK 문서 체크리스트를 먼저 업데이트한 뒤 구현한다.
|
||||
- 특정 Phase 또는 Task에 직접 대응되는 검증 기록은 해당 Phase 또는 Task 아래에 한국어로 남긴다.
|
||||
- 여러 Phase에 걸치거나 문서 전체에 해당하는 통합 검증, 회귀 검증, 최종 수동 확인 기록은 문서 최하단 `Verification Log`에 한국어로 남긴다.
|
||||
- 후속 수정이 발생해도 기존 검증 기록은 삭제하거나 덮어쓰지 않고 위치별로 누적한다.
|
||||
|
||||
## 상세 참조 문서
|
||||
- 빌드/린트/테스트는 `docs/agent-guides/build-test-style.md`를 참고한다.
|
||||
- 코드 스타일/구조는 `docs/agent-guides/code-style.md`를 참고한다.
|
||||
- 작업 절차/docs/계획 문서 규칙은 `docs/agent-guides/work-plan-docs.md`를 참고한다.
|
||||
- 커밋 메시지 규칙은 `docs/agent-guides/commit-message-rules.md`를 참고한다.
|
||||
- 저장소 세부 규칙/보안/Git 안전 수칙은 `docs/agent-guides/safety-repo-rules.md`를 참고한다.
|
||||
|
||||
## 핵심 금지사항
|
||||
- `local.properties`, 키스토어, Google Services 파일, 비밀값은 커밋하거나 외부에 노출하지 않는다.
|
||||
- `BuildConfig` 값(키/토큰/URL)을 로그, Toast, 크래시 메시지에 직접 노출하지 않는다.
|
||||
- Git 작업은 비파괴 명령을 기본으로 사용하고, 강제 푸시/히스토리 재작성은 명시 요청이 있을 때만 수행한다.
|
||||
+279
-42
@@ -1,31 +1,51 @@
|
||||
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
||||
import java.util.Locale
|
||||
import java.util.Properties
|
||||
|
||||
plugins {
|
||||
id 'com.android.application'
|
||||
id 'org.jetbrains.kotlin.android'
|
||||
id 'com.google.gms.google-services'
|
||||
id 'com.google.android.gms.oss-licenses-plugin'
|
||||
|
||||
id 'kotlin-kapt'
|
||||
id 'com.google.devtools.ksp'
|
||||
id 'kotlin-parcelize'
|
||||
id 'org.jlleitschuh.gradle.ktlint'
|
||||
|
||||
id 'io.objectbox'
|
||||
id 'com.google.firebase.crashlytics'
|
||||
}
|
||||
|
||||
def localProperties = new Properties()
|
||||
def localPropertiesFile = rootProject.file('local.properties')
|
||||
if (localPropertiesFile.exists()) {
|
||||
localPropertiesFile.withInputStream { stream ->
|
||||
localProperties.load(stream)
|
||||
}
|
||||
}
|
||||
|
||||
def releaseSigningPropertyKeys = [
|
||||
'RELEASE_STORE_FILE',
|
||||
'RELEASE_STORE_PASSWORD',
|
||||
'RELEASE_KEY_ALIAS',
|
||||
'RELEASE_KEY_PASSWORD'
|
||||
]
|
||||
def releaseSigningProperty = { key ->
|
||||
localProperties.getProperty(key)?.trim()
|
||||
}
|
||||
def missingReleaseSigningProperties = {
|
||||
releaseSigningPropertyKeys.findAll { key -> !releaseSigningProperty(key) }
|
||||
}
|
||||
|
||||
android {
|
||||
namespace 'kr.co.vividnext.sodalive'
|
||||
compileSdk 33
|
||||
compileSdk = 35
|
||||
|
||||
viewBinding {
|
||||
enabled true
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
dataBinding true
|
||||
}
|
||||
|
||||
lintOptions {
|
||||
checkDependencies true
|
||||
buildConfig true
|
||||
}
|
||||
|
||||
dependenciesInfo {
|
||||
@@ -35,56 +55,201 @@ android {
|
||||
includeInBundle = false
|
||||
}
|
||||
|
||||
packaging {
|
||||
// JNI(.so) 관련
|
||||
jniLibs {
|
||||
// pickFirsts: 충돌 시 첫 파일만 채택
|
||||
pickFirsts += ["**/libaosl.so"]
|
||||
}
|
||||
|
||||
// 일반 리소스(META-INF 등) 관련
|
||||
resources {
|
||||
// pickFirsts: 충돌 시 첫 파일만 채택
|
||||
pickFirsts += [
|
||||
"META-INF/LICENSE.txt",
|
||||
"META-INF/NOTICE*"
|
||||
]
|
||||
|
||||
// 자주 쓰는 제외/병합 예시
|
||||
excludes += [
|
||||
"META-INF/DEPENDENCIES",
|
||||
"META-INF/AL2.0",
|
||||
"META-INF/LGPL2.1"
|
||||
]
|
||||
merges += [
|
||||
"META-INF/services/**"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
applicationId "kr.co.vividnext.sodalive"
|
||||
minSdk 23
|
||||
targetSdk 33
|
||||
versionCode 3
|
||||
versionName "1.0.2"
|
||||
targetSdk 35
|
||||
versionCode 242
|
||||
versionName "1.55.0"
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
|
||||
signingConfigs {
|
||||
release {
|
||||
def releaseStoreFile = releaseSigningProperty('RELEASE_STORE_FILE')
|
||||
def releaseStorePassword = releaseSigningProperty('RELEASE_STORE_PASSWORD')
|
||||
def releaseKeyAlias = releaseSigningProperty('RELEASE_KEY_ALIAS')
|
||||
def releaseKeyPassword = releaseSigningProperty('RELEASE_KEY_PASSWORD')
|
||||
|
||||
if (missingReleaseSigningProperties().isEmpty()) {
|
||||
storeFile rootProject.file(releaseStoreFile)
|
||||
storePassword releaseStorePassword
|
||||
keyAlias releaseKeyAlias
|
||||
keyPassword releaseKeyPassword
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
signingConfig signingConfigs.release
|
||||
minifyEnabled true
|
||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||
|
||||
// release용 ad unit id는 배포 전 실제 값으로 교체한다.
|
||||
buildConfigField 'String', 'YANDEX_INLINE_BANNER_LIVE_TAB_AD_UNIT_ID', '"R-M-19140295-3"'
|
||||
buildConfigField 'String', 'YANDEX_INLINE_BANNER_LIVE_ROOM_DETAIL_AD_UNIT_ID', '"R-M-19140295-4"'
|
||||
buildConfigField 'String', 'YANDEX_INLINE_BANNER_CREATOR_COMMUNITY_ALL_AD_UNIT_ID', '"R-M-19140295-7"'
|
||||
buildConfigField 'String', 'YANDEX_INLINE_BANNER_SERIES_MAIN_HOME_AD_UNIT_ID', '"R-M-19140295-8"'
|
||||
buildConfigField 'String', 'YANDEX_INLINE_BANNER_SERIES_MAIN_DAY_OF_WEEK_AD_UNIT_ID', '"R-M-19140295-9"'
|
||||
buildConfigField 'String', 'YANDEX_INLINE_BANNER_SERIES_MAIN_BY_GENRE_AD_UNIT_ID', '"R-M-19140295-10"'
|
||||
buildConfigField 'String', 'YANDEX_INLINE_BANNER_PUSH_NOTIFICATION_LIST_AD_UNIT_ID', '"R-M-19140295-11"'
|
||||
buildConfigField 'String', 'YANDEX_INLINE_BANNER_NOTIFICATION_RECEIVE_SETTINGS_AD_UNIT_ID', '"R-M-19140295-12"'
|
||||
buildConfigField 'String', 'YANDEX_INLINE_BANNER_CHARACTER_TAB_AD_UNIT_ID', '"R-M-19140295-13"'
|
||||
buildConfigField 'String', 'YANDEX_INLINE_BANNER_ORIGINAL_TAB_AD_UNIT_ID', '"R-M-19140295-14"'
|
||||
buildConfigField 'String', 'YANDEX_INLINE_BANNER_TALK_TAB_AD_UNIT_ID', '"R-M-19140295-15"'
|
||||
buildConfigField 'String', 'YANDEX_REWARDED_CHAT_QUOTA_AD_UNIT_ID', '"R-M-19140295-16"'
|
||||
buildConfigField 'String', 'BASE_URL', '"https://api.sodalive.net"'
|
||||
buildConfigField 'String', 'AGORA_API_BASE_URL', '"https://api.agora.io/api/speech-to-speech-translation/v2/"'
|
||||
buildConfigField 'String', 'AGORA_CUSTOMER_ID', '"de5dd9ea151f4a43ba1ad8411817b169"'
|
||||
buildConfigField 'String', 'AGORA_CUSTOMER_SECRET', '"3855da8bc5ae4743af8bf4f87408b515"'
|
||||
buildConfigField 'String', 'BOOTPAY_APP_ID', '"64c35be1d25985001dc50c87"'
|
||||
buildConfigField 'String', 'BOOTPAY_APP_HECTO_ID', '"664c1707b18b225deca4b429"'
|
||||
buildConfigField 'String', 'AGORA_APP_ID', '"e34e40046e9847baba3adfe2b8ffb4f6"'
|
||||
buildConfigField 'String', 'AGORA_APP_CERTIFICATE', '"15cadeea4ba94ff7b091c9a10f4bf4a6"'
|
||||
buildConfigField 'String', 'NOTIFLY_PROJECT_ID', '"765102ec85855aa680da35f1b0f55712"'
|
||||
buildConfigField 'String', 'NOTIFLY_USERNAME', '"voiceon"'
|
||||
buildConfigField 'String', 'NOTIFLY_PASSWORD', '"c6c585db0aaa4189be44d0467c7d66b6@A"'
|
||||
buildConfigField 'String', 'KAKAO_APP_KEY', '"231cf78acfa8252fca38b9eedf87c5cb"'
|
||||
buildConfigField 'String', 'GOOGLE_CLIENT_ID', '"983594297130-5hrmkh6vpskeq6v34350kmilf74574h2.apps.googleusercontent.com"'
|
||||
buildConfigField 'String', 'APPSCHEME', '"voiceon"'
|
||||
buildConfigField 'String', 'LINE_CHANNEL_ID', '"2008995539"'
|
||||
manifestPlaceholders = [
|
||||
URISCHEME : "voiceon",
|
||||
APPLINK_HOST : "voiceon.onelink.me",
|
||||
FACEBOOK_APP_ID : "612448298237287",
|
||||
FACEBOOK_CLIENT_TOKEN: "32af760f4a7b7cb7e3b1e7ffd0b0da70",
|
||||
KAKAO_APP_KEY : "231cf78acfa8252fca38b9eedf87c5cb"
|
||||
]
|
||||
}
|
||||
|
||||
debug {
|
||||
minifyEnabled false
|
||||
debuggable true
|
||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||
applicationIdSuffix '.debug'
|
||||
|
||||
buildConfigField 'String', 'YANDEX_INLINE_BANNER_LIVE_TAB_AD_UNIT_ID', '"R-M-19140297-3"'
|
||||
buildConfigField 'String', 'YANDEX_INLINE_BANNER_LIVE_ROOM_DETAIL_AD_UNIT_ID', '"R-M-19140297-4"'
|
||||
buildConfigField 'String', 'YANDEX_INLINE_BANNER_CREATOR_COMMUNITY_ALL_AD_UNIT_ID', '"R-M-19140297-7"'
|
||||
buildConfigField 'String', 'YANDEX_INLINE_BANNER_SERIES_MAIN_HOME_AD_UNIT_ID', '"R-M-19140297-8"'
|
||||
buildConfigField 'String', 'YANDEX_INLINE_BANNER_SERIES_MAIN_DAY_OF_WEEK_AD_UNIT_ID', '"R-M-19140297-9"'
|
||||
buildConfigField 'String', 'YANDEX_INLINE_BANNER_SERIES_MAIN_BY_GENRE_AD_UNIT_ID', '"R-M-19140297-10"'
|
||||
buildConfigField 'String', 'YANDEX_INLINE_BANNER_PUSH_NOTIFICATION_LIST_AD_UNIT_ID', '"R-M-19140297-11"'
|
||||
buildConfigField 'String', 'YANDEX_INLINE_BANNER_NOTIFICATION_RECEIVE_SETTINGS_AD_UNIT_ID', '"R-M-19140297-12"'
|
||||
buildConfigField 'String', 'YANDEX_INLINE_BANNER_CHARACTER_TAB_AD_UNIT_ID', '"R-M-19140297-13"'
|
||||
buildConfigField 'String', 'YANDEX_INLINE_BANNER_ORIGINAL_TAB_AD_UNIT_ID', '"R-M-19140297-14"'
|
||||
buildConfigField 'String', 'YANDEX_INLINE_BANNER_TALK_TAB_AD_UNIT_ID', '"R-M-19140297-15"'
|
||||
buildConfigField 'String', 'YANDEX_REWARDED_CHAT_QUOTA_AD_UNIT_ID', '"R-M-19140297-16"'
|
||||
buildConfigField 'String', 'BASE_URL', '"https://test-api.sodalive.net"'
|
||||
buildConfigField 'String', 'AGORA_API_BASE_URL', '"https://api.agora.io/api/speech-to-speech-translation/v2/"'
|
||||
buildConfigField 'String', 'AGORA_CUSTOMER_ID', '"de5dd9ea151f4a43ba1ad8411817b169"'
|
||||
buildConfigField 'String', 'AGORA_CUSTOMER_SECRET', '"3855da8bc5ae4743af8bf4f87408b515"'
|
||||
buildConfigField 'String', 'BOOTPAY_APP_ID', '"6242a7772701800023f68b2e"'
|
||||
buildConfigField 'String', 'BOOTPAY_APP_HECTO_ID', '"667fca5d3bab7404f831c3e4"'
|
||||
buildConfigField 'String', 'AGORA_APP_ID', '"b96574e191a9430fa54c605528aa3ef7"'
|
||||
buildConfigField 'String', 'AGORA_APP_CERTIFICATE', '"ae18ade3afcf4086bd4397726eb0654c"'
|
||||
buildConfigField 'String', 'NOTIFLY_PROJECT_ID', '"5f7ebe90d1ce5f0392164b8a53a662bc"'
|
||||
buildConfigField 'String', 'NOTIFLY_USERNAME', '"voiceon"'
|
||||
buildConfigField 'String', 'NOTIFLY_PASSWORD', '"c6c585db0aaa4189be44d0467c7d66b6@A"'
|
||||
buildConfigField 'String', 'KAKAO_APP_KEY', '"20cf19413d63bfdfd30e8e6dff933d33"'
|
||||
buildConfigField 'String', 'GOOGLE_CLIENT_ID', '"758414412471-mosodbj2chno7l1j0iihldh6edmk0gk9.apps.googleusercontent.com"'
|
||||
buildConfigField 'String', 'APPSCHEME', '"voiceon-test"'
|
||||
buildConfigField 'String', 'LINE_CHANNEL_ID', '"2008995582"'
|
||||
manifestPlaceholders = [
|
||||
URISCHEME : "voiceon-test",
|
||||
APPLINK_HOST : "voiceon-test.onelink.me",
|
||||
FACEBOOK_APP_ID : "608674328645232",
|
||||
FACEBOOK_CLIENT_TOKEN: "3775e6ea83236a685d264b6c5a1bbb4d",
|
||||
KAKAO_APP_KEY : "20cf19413d63bfdfd30e8e6dff933d33"
|
||||
]
|
||||
}
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_17
|
||||
targetCompatibility JavaVersion.VERSION_17
|
||||
}
|
||||
kotlinOptions {
|
||||
jvmTarget = JavaVersion.VERSION_17.toString()
|
||||
lint {
|
||||
checkDependencies true
|
||||
checkReleaseBuilds false
|
||||
}
|
||||
|
||||
testOptions {
|
||||
unitTests {
|
||||
includeAndroidResources = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
gradle.taskGraph.whenReady { taskGraph ->
|
||||
def requiresReleaseSigning = taskGraph.allTasks.any { task ->
|
||||
def taskName = task.name.toLowerCase(Locale.US)
|
||||
taskName.contains('release') && (
|
||||
taskName.startsWith('assemble') ||
|
||||
taskName.startsWith('bundle') ||
|
||||
taskName.startsWith('install') ||
|
||||
taskName.startsWith('package')
|
||||
)
|
||||
}
|
||||
|
||||
if (!requiresReleaseSigning) {
|
||||
return
|
||||
}
|
||||
|
||||
def missingProperties = missingReleaseSigningProperties()
|
||||
if (!missingProperties.isEmpty()) {
|
||||
throw new GradleException(
|
||||
"Release signing properties are missing in local.properties: ${missingProperties.join(', ')}"
|
||||
)
|
||||
}
|
||||
|
||||
def releaseStoreFile = rootProject.file(releaseSigningProperty('RELEASE_STORE_FILE'))
|
||||
if (!releaseStoreFile.exists()) {
|
||||
throw new GradleException(
|
||||
"Release signing store file does not exist: ${releaseStoreFile}"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation "androidx.media:media:1.6.0"
|
||||
implementation 'androidx.core:core-ktx:1.10.1'
|
||||
implementation 'androidx.appcompat:appcompat:1.6.1'
|
||||
implementation 'com.google.android.material:material:1.9.0'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
|
||||
implementation "androidx.media:media:1.7.1"
|
||||
implementation 'androidx.core:core-ktx:1.16.0'
|
||||
implementation 'androidx.appcompat:appcompat:1.7.1'
|
||||
implementation 'androidx.recyclerview:recyclerview:1.4.0'
|
||||
implementation 'com.google.android.material:material:1.13.0'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.2.1'
|
||||
implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0"
|
||||
implementation 'androidx.webkit:webkit:1.7.0'
|
||||
implementation 'androidx.webkit:webkit:1.14.0'
|
||||
|
||||
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.6.1'
|
||||
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.1'
|
||||
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.9.4'
|
||||
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.9.4'
|
||||
|
||||
// Logger
|
||||
implementation("com.orhanobut:logger:2.2.0") {
|
||||
@@ -92,7 +257,7 @@ dependencies {
|
||||
}
|
||||
|
||||
// image library
|
||||
implementation "io.coil-kt:coil:1.4.0"
|
||||
implementation "io.coil-kt:coil:2.7.0"
|
||||
|
||||
// Koin DI
|
||||
implementation "io.insert-koin:koin-android:3.1.3"
|
||||
@@ -102,50 +267,122 @@ dependencies {
|
||||
exclude group: 'androidx.lifecycle', module: 'lifecycle-viewmodel'
|
||||
exclude group: 'androidx.lifecycle', module: 'lifecycle-viewmodel-ktx'
|
||||
}
|
||||
implementation "androidx.datastore:datastore-preferences:1.2.0"
|
||||
|
||||
// Gson
|
||||
implementation "com.google.code.gson:gson:2.10.1"
|
||||
implementation "com.google.code.gson:gson:2.13.2"
|
||||
|
||||
// Network
|
||||
implementation "com.squareup.retrofit2:retrofit:2.9.0"
|
||||
implementation "com.squareup.retrofit2:converter-gson:2.9.0"
|
||||
implementation "com.squareup.retrofit2:adapter-rxjava3:2.9.0"
|
||||
implementation "com.squareup.okhttp3:logging-interceptor:4.9.3"
|
||||
implementation "com.squareup.retrofit2:retrofit:3.0.0"
|
||||
implementation "com.squareup.retrofit2:converter-gson:3.0.0"
|
||||
implementation "com.squareup.retrofit2:adapter-rxjava3:3.0.0"
|
||||
implementation "com.squareup.okhttp3:logging-interceptor:5.2.1"
|
||||
|
||||
// RxJava3
|
||||
implementation "io.reactivex.rxjava3:rxjava:3.1.6"
|
||||
implementation "io.reactivex.rxjava3:rxjava:3.1.12"
|
||||
implementation "io.reactivex.rxjava3:rxandroid:3.0.2"
|
||||
implementation "com.jakewharton.rxbinding4:rxbinding:4.0.0"
|
||||
|
||||
// permission
|
||||
implementation "io.github.ParkSangGwon:tedpermission-normal:3.3.0"
|
||||
implementation "io.github.ParkSangGwon:tedpermission-normal:3.4.2"
|
||||
|
||||
implementation 'com.github.dhaval2404:imagepicker:2.1'
|
||||
implementation 'com.github.yalantis:ucrop:2.2.11'
|
||||
implementation 'com.github.zhpanvip:bannerviewpager:3.5.7'
|
||||
|
||||
implementation 'com.google.android.gms:play-services-oss-licenses:17.0.1'
|
||||
implementation 'com.google.android.gms:play-services-oss-licenses:17.1.0'
|
||||
|
||||
// Firebase
|
||||
implementation platform('com.google.firebase:firebase-bom:32.2.2')
|
||||
implementation 'com.google.firebase:firebase-dynamic-links-ktx'
|
||||
implementation platform('com.google.firebase:firebase-bom:33.16.0')
|
||||
implementation 'com.google.firebase:firebase-crashlytics-ktx'
|
||||
implementation 'com.google.firebase:firebase-analytics-ktx'
|
||||
implementation 'com.google.firebase:firebase-messaging-ktx'
|
||||
implementation 'com.google.firebase:firebase-config-ktx'
|
||||
|
||||
implementation 'androidx.credentials:credentials:1.3.0'
|
||||
implementation 'androidx.credentials:credentials-play-services-auth:1.3.0'
|
||||
implementation 'com.google.android.libraries.identity.googleid:googleid:1.1.1'
|
||||
|
||||
// bootpay
|
||||
implementation "io.github.bootpay:android:4.3.4"
|
||||
implementation "io.github.bootpay:android:4.4.3"
|
||||
|
||||
// agora
|
||||
implementation "io.agora.rtc:voice-sdk:4.1.0-1"
|
||||
implementation 'io.agora.rtm:rtm-sdk:1.5.3'
|
||||
|
||||
// sound visualizer
|
||||
implementation "com.gauravk.audiovisualizer:audiovisualizer:0.9.2"
|
||||
implementation "io.agora.rtc:voice-sdk:4.5.2"
|
||||
implementation 'io.agora:agora-rtm:2.2.6'
|
||||
|
||||
// Glide
|
||||
implementation 'com.github.bumptech.glide:glide:4.12.0'
|
||||
annotationProcessor 'com.github.bumptech.glide:compiler:4.12.0'
|
||||
implementation 'com.github.bumptech.glide:glide:5.0.5'
|
||||
annotationProcessor 'com.github.bumptech.glide:compiler:5.0.5'
|
||||
|
||||
implementation "com.michalsvec:single-row-calednar:1.0.0"
|
||||
// google in-app-purchase
|
||||
implementation "com.android.billingclient:billing-ktx:8.0.0"
|
||||
|
||||
// ROOM
|
||||
ksp "androidx.room:room-compiler:2.8.3"
|
||||
implementation "androidx.room:room-ktx:2.8.3"
|
||||
implementation "androidx.room:room-runtime:2.8.3"
|
||||
implementation "androidx.room:room-rxjava3:2.8.3"
|
||||
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.10.2"
|
||||
|
||||
implementation "androidx.media3:media3-session:1.8.0"
|
||||
implementation "androidx.media3:media3-exoplayer:1.8.0"
|
||||
|
||||
// Facebook
|
||||
implementation "com.facebook.android:facebook-core:18.0.0"
|
||||
|
||||
// Appsflyer
|
||||
implementation 'com.appsflyer:af-android-sdk:6.17.4'
|
||||
|
||||
// Yandex Mobile Ads
|
||||
implementation 'com.yandex.android:mobileads:7.18.5'
|
||||
|
||||
// 노티플라이
|
||||
implementation 'com.github.team-michael:notifly-android-sdk:1.12.0'
|
||||
|
||||
// Kakao
|
||||
implementation "com.kakao.sdk:v2-common:2.21.0"
|
||||
implementation "com.kakao.sdk:v2-auth:2.21.0"
|
||||
implementation "com.kakao.sdk:v2-user:2.21.0"
|
||||
|
||||
implementation 'io.github.glailton.expandabletextview:expandabletextview:1.0.4'
|
||||
|
||||
implementation 'com.github.orbitalsonic:Sonic-Water-Wave-Animation:2.0.1'
|
||||
|
||||
// Line
|
||||
implementation("com.linecorp.linesdk:linesdk:5.6.1") {
|
||||
exclude group: "org.jetbrains.kotlin", module: "kotlin-android-extensions-runtime"
|
||||
}
|
||||
|
||||
// ----- Test dependencies -----
|
||||
testImplementation 'junit:junit:4.13.2'
|
||||
testImplementation 'org.mockito:mockito-core:5.20.0'
|
||||
testImplementation 'org.mockito:mockito-inline:5.2.0'
|
||||
testImplementation 'org.mockito.kotlin:mockito-kotlin:6.1.0'
|
||||
testImplementation 'io.mockk:mockk:1.14.6'
|
||||
testImplementation 'androidx.test:core-ktx:1.6.1'
|
||||
testImplementation 'org.robolectric:robolectric:4.15.1'
|
||||
|
||||
androidTestImplementation 'androidx.test:core-ktx:1.6.1'
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.2.1'
|
||||
androidTestImplementation 'androidx.test:runner:1.6.2'
|
||||
}
|
||||
|
||||
|
||||
// KSP args for Room schema export
|
||||
ksp {
|
||||
arg("room.schemaLocation", "$projectDir/schemas")
|
||||
arg("room.incremental", "true")
|
||||
arg("room.expandProjection", "true")
|
||||
}
|
||||
|
||||
|
||||
// Kotlin compiler and toolchain configuration (migrated from deprecated kotlinOptions.jvmTarget)
|
||||
kotlin {
|
||||
// Ensures Kotlin compiles with Java 17 toolchain
|
||||
jvmToolchain(17)
|
||||
|
||||
// New DSL replacing kotlinOptions.jvmTarget
|
||||
compilerOptions {
|
||||
jvmTarget.set(JvmTarget.JVM_17)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,67 +0,0 @@
|
||||
{
|
||||
"_note1": "KEEP THIS FILE! Check it into a version control system (VCS) like git.",
|
||||
"_note2": "ObjectBox manages crucial IDs for your object model. See docs for details.",
|
||||
"_note3": "If you have VCS merge conflicts, you must resolve them according to ObjectBox docs.",
|
||||
"entities": [
|
||||
{
|
||||
"id": "1:2209417227252155460",
|
||||
"lastPropertyId": "8:7803281435927194929",
|
||||
"name": "PlaybackTracking",
|
||||
"properties": [
|
||||
{
|
||||
"id": "1:3889922602505997244",
|
||||
"name": "id",
|
||||
"type": 6,
|
||||
"flags": 1
|
||||
},
|
||||
{
|
||||
"id": "2:874896374244616380",
|
||||
"name": "contentId",
|
||||
"type": 6
|
||||
},
|
||||
{
|
||||
"id": "3:305496269372931228",
|
||||
"name": "totalDuration",
|
||||
"type": 5
|
||||
},
|
||||
{
|
||||
"id": "4:1202262957765031780",
|
||||
"name": "startPosition",
|
||||
"type": 5
|
||||
},
|
||||
{
|
||||
"id": "5:1595250877919247629",
|
||||
"name": "isFree",
|
||||
"type": 1
|
||||
},
|
||||
{
|
||||
"id": "6:4066577743967565922",
|
||||
"name": "isPreview",
|
||||
"type": 1
|
||||
},
|
||||
{
|
||||
"id": "7:7482414752180672089",
|
||||
"name": "endPosition",
|
||||
"type": 5
|
||||
},
|
||||
{
|
||||
"id": "8:7803281435927194929",
|
||||
"name": "playDateTime",
|
||||
"type": 9
|
||||
}
|
||||
],
|
||||
"relations": []
|
||||
}
|
||||
],
|
||||
"lastEntityId": "1:2209417227252155460",
|
||||
"lastIndexId": "0:0",
|
||||
"lastRelationId": "0:0",
|
||||
"lastSequenceId": "0:0",
|
||||
"modelVersion": 5,
|
||||
"modelVersionParserMinimum": 5,
|
||||
"retiredEntityUids": [],
|
||||
"retiredIndexUids": [],
|
||||
"retiredPropertyUids": [],
|
||||
"retiredRelationUids": [],
|
||||
"version": 1
|
||||
}
|
||||
Vendored
+24
@@ -221,3 +221,27 @@
|
||||
-keep class androidx.viewpager2.widget.**{*;}
|
||||
|
||||
-keep class kr.co.bootpay.core.** { *; }
|
||||
|
||||
-keep class retrofit2.** { *; }
|
||||
|
||||
-keep class com.google.gson.** { *; }
|
||||
-keep class sun.misc.** { *; }
|
||||
|
||||
# @Keep 애노테이션이 붙은 클래스, 메서드, 필드를 보호
|
||||
-keep @androidx.annotation.Keep class * { *; }
|
||||
|
||||
-keep class com.kakao.sdk.**.model.* { <fields>; }
|
||||
-keep class * extends com.google.gson.TypeAdapter
|
||||
|
||||
# https://github.com/square/okhttp/pull/6792
|
||||
-dontwarn org.bouncycastle.jsse.**
|
||||
-dontwarn org.conscrypt.*
|
||||
-dontwarn org.openjsse.**
|
||||
|
||||
-keep interface kr.co.vividnext.sodalive.tracking.UserEventApi
|
||||
|
||||
-dontwarn com.yalantis.ucrop**
|
||||
-keep class com.yalantis.ucrop** { *; }
|
||||
-keep interface com.yalantis.ucrop** { *; }
|
||||
|
||||
-dontwarn com.linecorp.linesdk.BR
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
# Keep schemas directory under version control
|
||||
@@ -0,0 +1,76 @@
|
||||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 1,
|
||||
"identityHash": "b9a331035b36b70f8ca7a14962b13fdf",
|
||||
"entities": [
|
||||
{
|
||||
"tableName": "playback_tracking",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `contentId` INTEGER NOT NULL, `totalDuration` INTEGER NOT NULL, `startPosition` INTEGER NOT NULL, `isFree` INTEGER NOT NULL, `isPreview` INTEGER NOT NULL, `endPosition` INTEGER, `playDateTime` TEXT NOT NULL)",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "contentId",
|
||||
"columnName": "contentId",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "totalDuration",
|
||||
"columnName": "totalDuration",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "startPosition",
|
||||
"columnName": "startPosition",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "isFree",
|
||||
"columnName": "isFree",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "isPreview",
|
||||
"columnName": "isPreview",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "endPosition",
|
||||
"columnName": "endPosition",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "playDateTime",
|
||||
"columnName": "playDateTime",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
}
|
||||
],
|
||||
"views": [],
|
||||
"setupQueries": [
|
||||
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'b9a331035b36b70f8ca7a14962b13fdf')"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 2,
|
||||
"identityHash": "7429c2998f64cb70e5e8b1d2525a4708",
|
||||
"entities": [
|
||||
{
|
||||
"tableName": "alarms",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `title` TEXT NOT NULL, `time` INTEGER NOT NULL, `days` TEXT NOT NULL, `contentId` INTEGER NOT NULL, `contentTitle` TEXT NOT NULL, `contentCreatorNickname` TEXT NOT NULL, `volume` INTEGER NOT NULL, `isEnabled` INTEGER NOT NULL)",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "title",
|
||||
"columnName": "title",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "time",
|
||||
"columnName": "time",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "days",
|
||||
"columnName": "days",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "contentId",
|
||||
"columnName": "contentId",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "contentTitle",
|
||||
"columnName": "contentTitle",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "contentCreatorNickname",
|
||||
"columnName": "contentCreatorNickname",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "volume",
|
||||
"columnName": "volume",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "isEnabled",
|
||||
"columnName": "isEnabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
}
|
||||
],
|
||||
"views": [],
|
||||
"setupQueries": [
|
||||
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '7429c2998f64cb70e5e8b1d2525a4708')"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 1,
|
||||
"identityHash": "e46a8b457c3ea6ceefd0db76bb763056",
|
||||
"entities": [
|
||||
{
|
||||
"tableName": "recent_contents",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`contentId` INTEGER NOT NULL, `coverImageUrl` TEXT NOT NULL, `title` TEXT NOT NULL, `creatorNickname` TEXT NOT NULL, `listenedAt` INTEGER NOT NULL, PRIMARY KEY(`contentId`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "contentId",
|
||||
"columnName": "contentId",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "coverImageUrl",
|
||||
"columnName": "coverImageUrl",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "title",
|
||||
"columnName": "title",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "creatorNickname",
|
||||
"columnName": "creatorNickname",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "listenedAt",
|
||||
"columnName": "listenedAt",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"contentId"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
}
|
||||
],
|
||||
"views": [],
|
||||
"setupQueries": [
|
||||
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'e46a8b457c3ea6ceefd0db76bb763056')"
|
||||
]
|
||||
}
|
||||
}
|
||||
+88
@@ -0,0 +1,88 @@
|
||||
package kr.co.vividnext.sodalive.runtime
|
||||
|
||||
import android.content.Context
|
||||
import androidx.datastore.core.DataStore
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
import androidx.datastore.preferences.core.booleanPreferencesKey
|
||||
import androidx.datastore.preferences.core.edit
|
||||
import androidx.datastore.preferences.core.longPreferencesKey
|
||||
import androidx.datastore.preferences.core.stringPreferencesKey
|
||||
import androidx.test.core.app.ApplicationProvider
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import kr.co.vividnext.sodalive.chat.talk.room.ChatRoomPreferenceManager
|
||||
import kr.co.vividnext.sodalive.common.AppPreferencesDataStoreProvider
|
||||
import kr.co.vividnext.sodalive.common.Constants
|
||||
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
|
||||
import org.junit.After
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import kotlinx.coroutines.runBlocking
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class DataStoreRuntimeRegressionTest {
|
||||
|
||||
private val context: Context
|
||||
get() = ApplicationProvider.getApplicationContext()
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
SharedPreferenceManager.init(context)
|
||||
ChatRoomPreferenceManager.init(context)
|
||||
}
|
||||
|
||||
@After
|
||||
fun tearDown() {
|
||||
SharedPreferenceManager.resetForTest()
|
||||
ChatRoomPreferenceManager.resetForTest()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun sharedPreferenceManager_readsPersistedSnapshotImmediatelyOnInit() {
|
||||
runBlocking {
|
||||
AppPreferencesDataStoreProvider.get(context).edit { preferences ->
|
||||
preferences[stringPreferencesKey(PREF_APP_LANGUAGE_CODE)] = "en"
|
||||
preferences[booleanPreferencesKey(Constants.PREF_IS_PLAYER_SERVICE_RUNNING)] = true
|
||||
}
|
||||
}
|
||||
|
||||
SharedPreferenceManager.resetForTest()
|
||||
SharedPreferenceManager.init(context)
|
||||
|
||||
assertEquals("en", SharedPreferenceManager.appLanguageCode)
|
||||
assertTrue(SharedPreferenceManager.isPlayerServiceRunning)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun chatRoomPreferenceManager_readsPersistedSnapshotImmediatelyOnInit() {
|
||||
val roomId = 13579L
|
||||
val visibleKey = "chat_bg_visible_room_$roomId"
|
||||
val imageIdKey = "chat_bg_image_id_room_$roomId"
|
||||
|
||||
runBlocking {
|
||||
chatRoomDataStore().edit { preferences ->
|
||||
preferences[booleanPreferencesKey(visibleKey)] = false
|
||||
preferences[longPreferencesKey(imageIdKey)] = 777L
|
||||
}
|
||||
}
|
||||
|
||||
ChatRoomPreferenceManager.resetForTest()
|
||||
ChatRoomPreferenceManager.init(context)
|
||||
|
||||
assertEquals(false, ChatRoomPreferenceManager.getBoolean(visibleKey, true))
|
||||
assertEquals(777L, ChatRoomPreferenceManager.getLong(imageIdKey, -1L))
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
private fun chatRoomDataStore(): DataStore<Preferences> {
|
||||
val field = ChatRoomPreferenceManager::class.java.getDeclaredField("dataStore")
|
||||
field.isAccessible = true
|
||||
return field.get(ChatRoomPreferenceManager) as DataStore<Preferences>
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val PREF_APP_LANGUAGE_CODE = "pref_app_language_code"
|
||||
}
|
||||
}
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
package kr.co.vividnext.sodalive.runtime
|
||||
|
||||
import androidx.media3.session.MediaController
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import com.google.common.util.concurrent.ListenableFuture
|
||||
import com.google.common.util.concurrent.SettableFuture
|
||||
import kr.co.vividnext.sodalive.audio_content.playlist.detail.AudioContentPlaylistDetailActivity
|
||||
import kr.co.vividnext.sodalive.main.MainActivity
|
||||
import org.junit.Assert.assertSame
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class MiniPlayerConnectionGuardTest {
|
||||
|
||||
@Test
|
||||
fun mainActivity_skipsConnectingWhenFutureAlreadyExists() {
|
||||
val activity = createOnMainThread { MainActivity() }
|
||||
val sentinel = SettableFuture.create<MediaController>()
|
||||
setPrivateField(activity, "mediaControllerFuture", sentinel)
|
||||
|
||||
invokePrivateNoArg(activity, "connectPlayerService")
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val after = getPrivateField(activity, "mediaControllerFuture") as? ListenableFuture<MediaController>
|
||||
assertSame(sentinel, after)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun playlistDetailActivity_skipsConnectingWhenFutureAlreadyExists() {
|
||||
val activity = createOnMainThread { AudioContentPlaylistDetailActivity() }
|
||||
val sentinel = SettableFuture.create<MediaController>()
|
||||
setPrivateField(activity, "mediaControllerFuture", sentinel)
|
||||
|
||||
invokePrivateNoArg(activity, "connectPlayerService")
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val after = getPrivateField(activity, "mediaControllerFuture") as? ListenableFuture<MediaController>
|
||||
assertSame(sentinel, after)
|
||||
}
|
||||
|
||||
private fun setPrivateField(target: Any, fieldName: String, value: Any?) {
|
||||
val field = target.javaClass.getDeclaredField(fieldName)
|
||||
field.isAccessible = true
|
||||
field.set(target, value)
|
||||
}
|
||||
|
||||
private fun getPrivateField(target: Any, fieldName: String): Any? {
|
||||
val field = target.javaClass.getDeclaredField(fieldName)
|
||||
field.isAccessible = true
|
||||
return field.get(target)
|
||||
}
|
||||
|
||||
private fun invokePrivateNoArg(target: Any, methodName: String) {
|
||||
val method = target.javaClass.getDeclaredMethod(methodName)
|
||||
method.isAccessible = true
|
||||
method.invoke(target)
|
||||
}
|
||||
|
||||
private fun <T> createOnMainThread(factory: () -> T): T {
|
||||
var instance: Any? = null
|
||||
InstrumentationRegistry.getInstrumentation().runOnMainSync {
|
||||
instance = factory()
|
||||
}
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
return instance as T
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="app_name">VoiceOn-Test</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="app_name">ボイスオン-テスト</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="app_name">보이스온-테스트</string>
|
||||
</resources>
|
||||
@@ -2,17 +2,28 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<queries>
|
||||
<package android:name="com.facebook.katana" />
|
||||
<intent>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
</intent>
|
||||
</queries>
|
||||
|
||||
<uses-permission android:name="com.android.vending.BILLING" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MICROPHONE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
|
||||
<!-- Android 15+ 녹화 상태 콜백(addScreenRecordingCallback)에 필요한 권한 -->
|
||||
<uses-permission android:name="android.permission.DETECT_SCREEN_RECORDING" />
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission
|
||||
android:name="android.permission.READ_EXTERNAL_STORAGE"
|
||||
android:maxSdkVersion="32" />
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
|
||||
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
|
||||
<uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT" />
|
||||
<uses-permission android:name="android.permission.VIBRATE" />
|
||||
<uses-permission
|
||||
android:name="android.permission.BLUETOOTH"
|
||||
@@ -28,11 +39,25 @@
|
||||
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
|
||||
<uses-permission android:name="com.google.android.gms.permission.AD_ID" />
|
||||
|
||||
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
|
||||
<uses-permission
|
||||
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
||||
android:maxSdkVersion="32"
|
||||
tools:ignore="ScopedStorage" />
|
||||
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
<uses-permission android:name="com.android.alarm.permission.SET_ALARM" />
|
||||
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
|
||||
|
||||
<application
|
||||
android:name=".app.SodaLiveApp"
|
||||
android:allowBackup="true"
|
||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||
android:fullBackupContent="@xml/backup_rules"
|
||||
android:dataExtractionRules="@xml/appsflyer_data_extraction_rules"
|
||||
android:fullBackupContent="@xml/appsflyer_backup_rules"
|
||||
android:hardwareAccelerated="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
@@ -41,7 +66,41 @@
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.SodaLive"
|
||||
android:usesCleartextTraffic="true"
|
||||
tools:replace="android:allowBackup"
|
||||
tools:targetApi="31">
|
||||
<activity
|
||||
android:name=".main.DeepLinkActivity"
|
||||
android:exported="true">
|
||||
<intent-filter android:autoVerify="true">
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data
|
||||
android:host="${APPLINK_HOST}"
|
||||
android:scheme="https" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data android:scheme="${URISCHEME}" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<!-- PayVerse 리다이렉트에 등록할 커스텀 스킴/호스트 -->
|
||||
<data
|
||||
android:host="payverse"
|
||||
android:path="/result"
|
||||
android:scheme="${URISCHEME}" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".splash.SplashActivity"
|
||||
android:exported="true">
|
||||
@@ -50,26 +109,45 @@
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
<intent-filter android:autoVerify="true">
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data android:scheme="http" />
|
||||
<data android:scheme="https" />
|
||||
<data android:host="sodalive.page.link" />
|
||||
<data android:host="sodalive.net" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity android:name=".main.MainActivity" />
|
||||
<activity android:name=".v2.main.MainV2Activity" />
|
||||
<activity android:name=".v2.main.content.overview.ContentOverviewActivity" />
|
||||
<activity android:name=".v2.creator.channel.CreatorChannelActivity" />
|
||||
<activity
|
||||
android:name=".v2.creator.channel.community.detail.CreatorChannelCommunityDetailActivity"
|
||||
android:windowSoftInputMode="stateAlwaysHidden|adjustResize" />
|
||||
<activity
|
||||
android:name=".v2.creator.channel.community.detail.reply.CreatorChannelCommunityReplyActivity"
|
||||
android:windowSoftInputMode="stateAlwaysHidden|adjustResize" />
|
||||
<activity
|
||||
android:name=".v2.creator.channel.fantalk.write.CreatorChannelFanTalkWriteActivity"
|
||||
android:windowSoftInputMode="stateVisible|adjustResize" />
|
||||
<activity
|
||||
android:name=".v2.creator.channel.fantalk.detail.CreatorChannelFanTalkDetailActivity"
|
||||
android:windowSoftInputMode="stateAlwaysHidden|adjustResize" />
|
||||
<activity android:name=".v2.live.onair.HomeOnAirLiveActivity" />
|
||||
<activity
|
||||
android:name=".v2.main.chat.dm.DmChatRoomActivity"
|
||||
android:windowSoftInputMode="stateAlwaysHidden|adjustResize" />
|
||||
<activity android:name=".user.login.LoginActivity" />
|
||||
<activity android:name=".user.signup.SignUpActivity" />
|
||||
<activity android:name=".audio_content.all.AudioContentAllActivity" />
|
||||
<activity android:name=".settings.language.LanguageSettingsActivity" />
|
||||
<activity
|
||||
android:name=".user.signup.SignUpActivity"
|
||||
android:windowSoftInputMode="stateVisible" />
|
||||
<activity android:name=".settings.terms.TermsActivity" />
|
||||
<activity android:name=".user.find_password.FindPasswordActivity" />
|
||||
<activity android:name=".mypage.can.status.CanStatusActivity" />
|
||||
<activity android:name=".mypage.can.charge.CanChargeActivity" />
|
||||
<activity android:name=".mypage.can.payment.CanPaymentActivity" />
|
||||
<activity android:name=".mypage.point.PointStatusActivity" />
|
||||
<activity
|
||||
android:name=".mypage.can.charge.CanChargeActivity"
|
||||
android:configChanges="orientation|screenSize|keyboardHidden" />
|
||||
<activity
|
||||
android:name=".mypage.can.payment.CanPaymentActivity"
|
||||
android:launchMode="singleTop" />
|
||||
<activity android:name=".mypage.can.payment.CanPaymentTempActivity" />
|
||||
<activity android:name=".mypage.can.coupon.CanCouponActivity" />
|
||||
<activity android:name=".live.room.create.LiveRoomCreateActivity" />
|
||||
<activity android:name=".live.room.update.LiveRoomEditActivity" />
|
||||
<activity android:name=".live.reservation.complete.LiveReservationCompleteActivity" />
|
||||
@@ -78,9 +156,12 @@
|
||||
android:windowSoftInputMode="stateAlwaysHidden|adjustPan" />
|
||||
<activity android:name=".explorer.profile.UserProfileActivity" />
|
||||
<activity android:name=".explorer.profile.donation.UserProfileDonationAllViewActivity" />
|
||||
<activity android:name=".explorer.profile.channel_donation.UserProfileChannelDonationAllViewActivity" />
|
||||
<activity android:name=".explorer.profile.fantalk.UserProfileFantalkAllViewActivity" />
|
||||
<activity android:name=".explorer.profile.CreatorNoticeWriteActivity" />
|
||||
<activity android:name=".explorer.profile.follow.UserFollowerListActivity" />
|
||||
<activity android:name=".explorer.profile.creator_community.all.CreatorCommunityAllActivity" />
|
||||
<activity android:name=".explorer.profile.creator_community.write.CreatorCommunityWriteActivity" />
|
||||
<activity android:name=".explorer.profile.creator_community.modify.CreatorCommunityModifyActivity" />
|
||||
<activity android:name=".message.text.TextMessageWriteActivity" />
|
||||
<activity android:name=".message.text.TextMessageDetailActivity" />
|
||||
<activity android:name=".message.SelectMessageRecipientActivity" />
|
||||
@@ -91,6 +172,8 @@
|
||||
<activity android:name=".settings.event.EventActivity" />
|
||||
<activity android:name=".settings.event.EventDetailActivity" />
|
||||
<activity android:name=".settings.notification.NotificationSettingsActivity" />
|
||||
<activity android:name=".settings.notification.NotificationReceiveSettingsActivity" />
|
||||
<activity android:name=".settings.ContentSettingsActivity" />
|
||||
<activity android:name=".live.reservation_status.LiveReservationStatusActivity" />
|
||||
<activity android:name=".live.reservation_status.LiveReservationCancelActivity" />
|
||||
<activity android:name=".audio_content.AudioContentActivity" />
|
||||
@@ -102,10 +185,46 @@
|
||||
<activity android:name=".live.now.all.LiveNowAllActivity" />
|
||||
<activity android:name=".live.reservation.all.LiveReservationAllActivity" />
|
||||
<activity android:name=".mypage.service_center.ServiceCenterActivity" />
|
||||
<activity android:name=".onboarding.OnBoardingActivity" />
|
||||
<activity android:name=".message.MessageActivity" />
|
||||
<activity android:name=".mypage.profile.ProfileUpdateActivity" />
|
||||
<activity android:name=".mypage.profile.nickname.NicknameUpdateActivity" />
|
||||
<activity android:name=".mypage.profile.password.ModifyPasswordActivity" />
|
||||
<activity android:name=".audio_content.all.AudioContentNewAllActivity" />
|
||||
<activity android:name=".home.pushnotification.PushNotificationListActivity" />
|
||||
<activity android:name=".audio_content.all.AudioContentRankingAllActivity" />
|
||||
<activity android:name=".audio_content.all.by_theme.AudioContentAllByThemeActivity" />
|
||||
<activity android:name=".live.roulette.config.RouletteConfigActivity" />
|
||||
<activity android:name=".live.room.menu.MenuConfigActivity" />
|
||||
<activity android:name=".audio_content.series.SeriesListAllActivity" />
|
||||
<activity android:name=".audio_content.series.detail.SeriesDetailActivity" />
|
||||
<activity android:name=".audio_content.series.content.SeriesContentAllActivity" />
|
||||
<activity android:name=".audio_content.playlist.detail.AudioContentPlaylistDetailActivity" />
|
||||
<activity android:name=".audio_content.playlist.create.AudioContentPlaylistCreateActivity" />
|
||||
<activity android:name=".audio_content.playlist.modify.AudioContentPlaylistModifyActivity" />
|
||||
<activity android:name=".audio_content.box.AudioContentBoxActivity" />
|
||||
<activity android:name=".audition.detail.AuditionDetailActivity" />
|
||||
<activity android:name=".audition.role.AuditionRoleDetailActivity" />
|
||||
|
||||
<activity android:name=".search.SearchActivity" />
|
||||
<activity android:name=".audition.AuditionActivity" />
|
||||
|
||||
<activity android:name=".mypage.alarm.AlarmListActivity" />
|
||||
<activity android:name=".mypage.alarm.AddAlarmActivity" />
|
||||
<activity android:name=".mypage.alarm.select_audio_content.AlarmSelectAudioContentActivity" />
|
||||
<activity android:name=".mypage.block.BlockMemberActivity" />
|
||||
<activity
|
||||
android:name=".mypage.alarm.AlarmActivity"
|
||||
android:exported="true"
|
||||
android:showWhenLocked="true"
|
||||
android:turnScreenOn="true">
|
||||
<intent-filter>
|
||||
<action android:name="com.example.alarmapp.ALARM_ACTION" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity android:name=".chat.character.newcharacters.NewCharactersAllActivity" />
|
||||
<activity android:name=".chat.original.detail.OriginalWorkDetailActivity" />
|
||||
<activity android:name=".audio_content.series.main.SeriesMainActivity" />
|
||||
|
||||
<activity
|
||||
android:name="com.google.android.gms.oss.licenses.OssLicensesMenuActivity"
|
||||
@@ -114,11 +233,40 @@
|
||||
android:name="com.google.android.gms.oss.licenses.OssLicensesActivity"
|
||||
android:theme="@style/Theme.AppCompat.DayNight" />
|
||||
|
||||
<activity
|
||||
android:name="com.kakao.sdk.auth.AuthCodeHandlerActivity"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<!-- Redirect URI: "kakao${NATIVE_APP_KEY}://oauth" -->
|
||||
<data
|
||||
android:host="oauth"
|
||||
android:scheme="kakao${KAKAO_APP_KEY}" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<service
|
||||
android:name=".common.SodaLiveService"
|
||||
android:foregroundServiceType="microphone|mediaPlayback"
|
||||
android:stopWithTask="false" />
|
||||
|
||||
<service android:name=".audio_content.AudioContentPlayService" />
|
||||
<service
|
||||
android:name=".audio_content.AudioContentPlayService"
|
||||
android:foregroundServiceType="mediaPlayback"
|
||||
android:stopWithTask="false" />
|
||||
|
||||
<service
|
||||
android:name=".audio_content.player.AudioContentPlayerService"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="mediaPlayback">
|
||||
<intent-filter>
|
||||
<action android:name="androidx.media3.session.MediaSessionService" />
|
||||
</intent-filter>
|
||||
</service>
|
||||
|
||||
<!-- [START firebase_service] -->
|
||||
<service
|
||||
@@ -130,10 +278,66 @@
|
||||
</service>
|
||||
<!-- [END firebase_service] -->
|
||||
|
||||
<!-- 부팅 시 알람 재설정을 위한 리시버 -->
|
||||
<receiver
|
||||
android:name=".mypage.alarm.receiver.AlarmBootReceiver"
|
||||
android:enabled="true"
|
||||
android:exported="false">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<receiver
|
||||
android:name=".mypage.alarm.receiver.AlarmReceiver"
|
||||
android:enabled="true"
|
||||
android:exported="false" />
|
||||
|
||||
<!-- [START fcm_default_channel] -->
|
||||
<meta-data
|
||||
android:name="com.google.firebase.messaging.default_notification_channel_id"
|
||||
android:value="@string/default_notification_channel_id" />
|
||||
<!-- [END fcm_default_channel] -->
|
||||
|
||||
<!-- [START facebook] -->
|
||||
<meta-data
|
||||
android:name="com.facebook.sdk.ApplicationId"
|
||||
android:value="${FACEBOOK_APP_ID}" />
|
||||
|
||||
<meta-data
|
||||
android:name="com.facebook.sdk.ClientToken"
|
||||
android:value="${FACEBOOK_CLIENT_TOKEN}" />
|
||||
|
||||
<meta-data
|
||||
android:name="com.facebook.sdk.AdvertiserIDCollectionEnabled"
|
||||
android:value="true" />
|
||||
|
||||
<activity
|
||||
android:name="com.facebook.FacebookActivity"
|
||||
android:configChanges="keyboard|keyboardHidden|screenLayout|screenSize|orientation" />
|
||||
<!-- [END facebook] -->
|
||||
|
||||
<!-- Character Detail -->
|
||||
<activity android:name=".chat.character.detail.CharacterDetailActivity" />
|
||||
|
||||
<activity android:name=".chat.talk.room.ChatRoomActivity" />
|
||||
|
||||
<activity
|
||||
android:name="com.yalantis.ucrop.UCropActivity"
|
||||
android:exported="false"
|
||||
android:screenOrientation="portrait"
|
||||
android:theme="@style/Theme.AppCompat.Light.NoActionBar" />
|
||||
|
||||
<provider
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
android:authorities="${applicationId}.fileprovider"
|
||||
android:exported="false"
|
||||
android:grantUriPermissions="true">
|
||||
|
||||
<!-- ★ 이 meta-data가 꼭 필요 -->
|
||||
<meta-data
|
||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||
android:resource="@xml/file_paths" />
|
||||
</provider>
|
||||
</application>
|
||||
</manifest>
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
<!-- app/src/main/assets/payverse_starter_debug.html -->
|
||||
<!doctype html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1"/>
|
||||
<!-- PayVerse SDK -->
|
||||
<script src="https://ui.payverseglobal.com/js/payments.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<script>
|
||||
// 안드로이드에서 JSON 문자열을 넘기면 이 함수를 호출
|
||||
function startPay(payloadJson) {
|
||||
try {
|
||||
const p = JSON.parse(payloadJson);
|
||||
// 즉시 실행: 페이지가 열리자마자 결제창 시작
|
||||
window.payVerse.requestUI(p);
|
||||
} catch (e) {
|
||||
console.error('startPay error', e);
|
||||
alert('결제 초기화에 실패했습니다.');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,25 @@
|
||||
<!-- app/src/main/assets/payverse_starter_debug.html -->
|
||||
<!doctype html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1"/>
|
||||
<!-- PayVerse SDK -->
|
||||
<script src="https://ui-snd.payverseglobal.com/js/payments.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<script>
|
||||
// 안드로이드에서 JSON 문자열을 넘기면 이 함수를 호출
|
||||
function startPay(payloadJson) {
|
||||
try {
|
||||
const p = JSON.parse(payloadJson);
|
||||
// 즉시 실행: 페이지가 열리자마자 결제창 시작
|
||||
window.payVerse.requestUI(p);
|
||||
} catch (e) {
|
||||
console.error('startPay error', e);
|
||||
alert('결제 초기화에 실패했습니다.');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -6,28 +6,29 @@ import io.agora.rtc2.Constants
|
||||
import io.agora.rtc2.IRtcEngineEventHandler
|
||||
import io.agora.rtc2.RtcEngine
|
||||
import io.agora.rtm.ErrorInfo
|
||||
import io.agora.rtm.PublishOptions
|
||||
import io.agora.rtm.ResultCallback
|
||||
import io.agora.rtm.RtmChannel
|
||||
import io.agora.rtm.RtmChannelListener
|
||||
import io.agora.rtm.RtmClient
|
||||
import io.agora.rtm.RtmClientListener
|
||||
import io.agora.rtm.SendMessageOptions
|
||||
import io.agora.rtm.RtmConfig
|
||||
import io.agora.rtm.RtmConstants
|
||||
import io.agora.rtm.RtmEventListener
|
||||
import io.agora.rtm.SubscribeOptions
|
||||
import kr.co.vividnext.sodalive.BuildConfig
|
||||
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
|
||||
import kr.co.vividnext.sodalive.live.room.LiveRoomRequestType
|
||||
import kotlin.concurrent.thread
|
||||
|
||||
class Agora(
|
||||
private val uid: Long,
|
||||
private val context: Context,
|
||||
private val rtcEventHandler: IRtcEngineEventHandler,
|
||||
private val rtmClientListener: RtmClientListener
|
||||
private val rtmEventListener: RtmEventListener
|
||||
) {
|
||||
// RTM client instance
|
||||
private var rtmClient: RtmClient? = null
|
||||
// 상태 플래그: RTM 로그인 완료 여부
|
||||
private var rtmLoggedIn: Boolean = false
|
||||
|
||||
// RTM channel instance
|
||||
private var rtmChannel: RtmChannel? = null
|
||||
|
||||
private var rtcEngine: RtcEngine? = null
|
||||
// 상태 플래그: RTM 로그인 진행 중 여부
|
||||
private var rtmLoginInProgress: Boolean = false
|
||||
|
||||
init {
|
||||
initAgoraEngine()
|
||||
@@ -35,65 +36,51 @@ class Agora(
|
||||
|
||||
private fun initAgoraEngine() {
|
||||
try {
|
||||
rtcEngine = RtcEngine.create(
|
||||
context,
|
||||
BuildConfig.AGORA_APP_ID,
|
||||
rtcEventHandler
|
||||
)
|
||||
|
||||
rtcEngine!!.setChannelProfile(Constants.CHANNEL_PROFILE_LIVE_BROADCASTING)
|
||||
rtcEngine!!.setAudioProfile(
|
||||
Constants.AUDIO_PROFILE_MUSIC_HIGH_QUALITY_STEREO,
|
||||
Constants.AUDIO_SCENARIO_GAME_STREAMING
|
||||
)
|
||||
rtcEngine!!.enableAudio()
|
||||
rtcEngine!!.enableAudioVolumeIndication(500, 3, true)
|
||||
|
||||
rtmClient = RtmClient.createInstance(
|
||||
context,
|
||||
BuildConfig.AGORA_APP_ID,
|
||||
rtmClientListener
|
||||
)
|
||||
initRtcEngine()
|
||||
initRtmClient()
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
fun deInitAgoraEngine() {
|
||||
if (rtcEngine != null) {
|
||||
rtcEngine!!.leaveChannel()
|
||||
|
||||
thread {
|
||||
RtcEngine.destroy()
|
||||
rtcEngine = null
|
||||
}
|
||||
}
|
||||
|
||||
rtmChannel?.leave(null)
|
||||
rtmChannel?.release()
|
||||
rtmClient?.logout(null)
|
||||
fun deInitAgoraEngine(rtmEventListener: RtmEventListener) {
|
||||
deInitRtcEngine()
|
||||
deInitRtmClient(rtmEventListener)
|
||||
}
|
||||
|
||||
fun inputChat(message: String) {
|
||||
val rtmMessage = rtmClient!!.createMessage()
|
||||
rtmMessage.text = message
|
||||
// region RtcEngine
|
||||
private var rtcEngine: RtcEngine? = null
|
||||
|
||||
rtmChannel!!.sendMessage(
|
||||
rtmMessage,
|
||||
object : ResultCallback<Void?> {
|
||||
override fun onSuccess(p0: Void?) {
|
||||
Logger.e("sendMessage - onSuccess")
|
||||
}
|
||||
|
||||
override fun onFailure(p0: ErrorInfo) {
|
||||
Logger.e("sendMessage fail - ${p0.errorCode}")
|
||||
Logger.e("sendMessage fail - ${p0.errorDescription}")
|
||||
}
|
||||
}
|
||||
@Throws(Exception::class)
|
||||
private fun initRtcEngine() {
|
||||
Logger.e("initRtcEngine")
|
||||
rtcEngine = RtcEngine.create(
|
||||
context,
|
||||
BuildConfig.AGORA_APP_ID,
|
||||
rtcEventHandler
|
||||
)
|
||||
Logger.e("initRtcEngine - rtcEngine: ${rtcEngine != null}")
|
||||
|
||||
rtcEngine!!.setChannelProfile(Constants.CHANNEL_PROFILE_LIVE_BROADCASTING)
|
||||
rtcEngine!!.setAudioProfile(
|
||||
Constants.AUDIO_PROFILE_MUSIC_HIGH_QUALITY_STEREO,
|
||||
Constants.AUDIO_SCENARIO_GAME_STREAMING
|
||||
)
|
||||
rtcEngine!!.setParameters("{\"che.audio.aiaec.working_mode\":0}")
|
||||
rtcEngine!!.enableAudio()
|
||||
rtcEngine!!.enableAudioVolumeIndication(500, 3, true)
|
||||
}
|
||||
|
||||
fun joinRtcChannel(uid: Int, rtcToken: String, channelName: String) {
|
||||
val state = rtcEngine?.connectionState
|
||||
val isDisconnected = state == null || state == Constants.CONNECTION_STATE_DISCONNECTED
|
||||
|
||||
if (!isDisconnected) {
|
||||
Logger.e("joinRtcChannel - skip (state=$state)")
|
||||
return
|
||||
}
|
||||
|
||||
Logger.e("joinRtcChannel - proceed (state=$state) uid=$uid channel=$channelName")
|
||||
rtcEngine!!.joinChannel(
|
||||
rtcToken,
|
||||
channelName,
|
||||
@@ -102,62 +89,6 @@ class Agora(
|
||||
)
|
||||
}
|
||||
|
||||
fun createRtmChannelAndLogin(
|
||||
uid: String,
|
||||
rtmToken: String,
|
||||
channelName: String,
|
||||
rtmChannelListener: RtmChannelListener,
|
||||
rtmChannelJoinSuccess: () -> Unit,
|
||||
rtmChannelJoinFail: () -> Unit
|
||||
) {
|
||||
rtmChannel = rtmClient!!.createChannel(channelName, rtmChannelListener)
|
||||
rtmClient!!.login(
|
||||
rtmToken,
|
||||
uid,
|
||||
object : ResultCallback<Void> {
|
||||
override fun onSuccess(p0: Void?) {
|
||||
rtmChannel!!.join(object : ResultCallback<Void> {
|
||||
override fun onSuccess(p0: Void?) {
|
||||
Logger.e("rtmChannel join - onSuccess")
|
||||
rtmChannelJoinSuccess()
|
||||
}
|
||||
|
||||
override fun onFailure(p0: ErrorInfo?) {
|
||||
rtmChannelJoinFail()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
override fun onFailure(p0: ErrorInfo?) {
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
fun sendRawMessageToGroup(
|
||||
rawMessage: ByteArray,
|
||||
onSuccess: (() -> Unit)? = null,
|
||||
onFailure: (() -> Unit)? = null
|
||||
) {
|
||||
val message = rtmClient!!.createMessage()
|
||||
message.rawMessage = rawMessage
|
||||
rtmChannel!!.sendMessage(
|
||||
message,
|
||||
object : ResultCallback<Void?> {
|
||||
override fun onSuccess(p0: Void?) {
|
||||
Logger.e("sendMessage - onSuccess")
|
||||
onSuccess?.invoke()
|
||||
}
|
||||
|
||||
override fun onFailure(p0: ErrorInfo) {
|
||||
Logger.e("sendMessage fail - ${p0.errorCode}")
|
||||
Logger.e("sendMessage fail - ${p0.errorDescription}")
|
||||
onFailure?.invoke()
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
fun setClientRole(role: Int) {
|
||||
rtcEngine!!.setClientRole(role)
|
||||
}
|
||||
@@ -170,36 +101,304 @@ class Agora(
|
||||
rtcEngine?.muteAllRemoteAudioStreams(mute)
|
||||
}
|
||||
|
||||
fun sendRawMessageToPeer(
|
||||
receiverUid: String,
|
||||
requestType: LiveRoomRequestType,
|
||||
onSuccess: () -> Unit
|
||||
) {
|
||||
val option = SendMessageOptions()
|
||||
|
||||
val message = rtmClient!!.createMessage()
|
||||
message.rawMessage = requestType.toString().toByteArray()
|
||||
|
||||
rtmClient!!.sendMessageToPeer(
|
||||
receiverUid,
|
||||
message,
|
||||
option,
|
||||
object : ResultCallback<Void?> {
|
||||
override fun onSuccess(aVoid: Void?) {
|
||||
onSuccess()
|
||||
}
|
||||
|
||||
override fun onFailure(errorInfo: ErrorInfo) {
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
fun rtmChannelIsNull(): Boolean {
|
||||
return rtmChannel == null
|
||||
}
|
||||
|
||||
fun getConnectionState(): Int {
|
||||
return rtcEngine!!.connectionState
|
||||
}
|
||||
|
||||
fun isRtmLoggedIn(): Boolean {
|
||||
return rtmLoggedIn
|
||||
}
|
||||
|
||||
fun deInitRtcEngine() {
|
||||
if (rtcEngine != null) {
|
||||
rtcEngine!!.leaveChannel()
|
||||
|
||||
thread {
|
||||
RtcEngine.destroy()
|
||||
rtcEngine = null
|
||||
}
|
||||
}
|
||||
}
|
||||
// endregion
|
||||
|
||||
// region RtmClient
|
||||
private var rtmClient: RtmClient? = null
|
||||
private var roomChannelName: String? = null
|
||||
|
||||
@Throws(Exception::class)
|
||||
private fun initRtmClient() {
|
||||
val rtmConfig = RtmConfig.Builder(BuildConfig.AGORA_APP_ID, uid.toString())
|
||||
.eventListener(rtmEventListener)
|
||||
.build()
|
||||
|
||||
rtmClient = RtmClient.create(rtmConfig)
|
||||
}
|
||||
|
||||
fun rtmLogin(
|
||||
rtmToken: String,
|
||||
channelName: String,
|
||||
rtmChannelJoinSuccess: () -> Unit,
|
||||
rtmChannelJoinFail: () -> Unit
|
||||
) {
|
||||
// 이미 RTM 로그인 및 구독이 완료된 경우 재호출 방지
|
||||
if (rtmLoggedIn && roomChannelName == channelName) {
|
||||
Logger.e("rtmLogin - already logged in and subscribed. skip")
|
||||
return
|
||||
}
|
||||
// 로그인 시도 중이면 재호출 방지
|
||||
if (rtmLoginInProgress) {
|
||||
Logger.e("rtmLogin - already in progress. skip")
|
||||
return
|
||||
}
|
||||
|
||||
roomChannelName = channelName
|
||||
|
||||
fun attemptLogin(attempt: Int) {
|
||||
rtmClient!!.login(
|
||||
rtmToken,
|
||||
object : ResultCallback<Void> {
|
||||
override fun onSuccess(p0: Void?) {
|
||||
Logger.e("rtmClient login - success (attempt=$attempt)")
|
||||
// 로그인 성공 후 두 채널 구독 시도
|
||||
subscribeChannel(rtmChannelJoinSuccess, rtmChannelJoinFail)
|
||||
}
|
||||
|
||||
override fun onFailure(p0: ErrorInfo?) {
|
||||
Logger.e("rtmClient login - fail (attempt=$attempt), ${p0?.errorReason}")
|
||||
if (attempt < 4) {
|
||||
attemptLogin(attempt + 1)
|
||||
} else {
|
||||
rtmLoginInProgress = false
|
||||
rtmChannelJoinFail()
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
rtmLoginInProgress = true
|
||||
attemptLogin(1)
|
||||
}
|
||||
|
||||
private fun subscribeChannel(
|
||||
rtmChannelJoinSuccess: () -> Unit,
|
||||
rtmChannelJoinFail: () -> Unit
|
||||
) {
|
||||
val targetRoom = roomChannelName
|
||||
if (targetRoom == null) {
|
||||
Logger.e("subscribeChannel - roomChannelName is null")
|
||||
rtmChannelJoinFail()
|
||||
return
|
||||
}
|
||||
|
||||
var completed = false
|
||||
var roomSubscribed = false
|
||||
var inboxSubscribed = false
|
||||
|
||||
fun completeSuccessIfReady() {
|
||||
if (!completed && roomSubscribed && inboxSubscribed) {
|
||||
completed = true
|
||||
rtmLoggedIn = true
|
||||
rtmLoginInProgress = false
|
||||
Logger.e("RTM subscribe - both channels subscribed")
|
||||
rtmChannelJoinSuccess()
|
||||
}
|
||||
}
|
||||
|
||||
fun failOnce(reason: String?) {
|
||||
if (!completed) {
|
||||
completed = true
|
||||
Logger.e("RTM subscribe failed: $reason")
|
||||
rtmChannelJoinFail()
|
||||
}
|
||||
}
|
||||
|
||||
fun subscribeRoom(attempt: Int) {
|
||||
val channelOptions = SubscribeOptions()
|
||||
channelOptions.withMessage = true
|
||||
channelOptions.withPresence = true
|
||||
Logger.e("RTM subscribe(room: $targetRoom) attempt=$attempt")
|
||||
rtmClient!!.subscribe(
|
||||
targetRoom,
|
||||
channelOptions,
|
||||
object : ResultCallback<Void> {
|
||||
override fun onSuccess(responseInfo: Void?) {
|
||||
Logger.e("RTM subscribe(room) success at attempt=$attempt")
|
||||
roomSubscribed = true
|
||||
completeSuccessIfReady()
|
||||
}
|
||||
|
||||
override fun onFailure(errorInfo: ErrorInfo?) {
|
||||
Logger.e("RTM subscribe(room) failure at attempt=$attempt reason=${errorInfo?.errorReason}")
|
||||
if (attempt < 4) {
|
||||
subscribeRoom(attempt + 1)
|
||||
} else {
|
||||
failOnce("room subscribe failed after 3 retries (4 attempts)")
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
fun subscribeInbox(attempt: Int) {
|
||||
val inboxChannel = "inbox_$uid"
|
||||
val inboxChannelOptions = SubscribeOptions()
|
||||
inboxChannelOptions.withMessage = true
|
||||
Logger.e("RTM subscribe(inbox: $inboxChannel) attempt=$attempt")
|
||||
rtmClient!!.subscribe(
|
||||
inboxChannel,
|
||||
inboxChannelOptions,
|
||||
object : ResultCallback<Void> {
|
||||
override fun onSuccess(responseInfo: Void?) {
|
||||
Logger.e("RTM subscribe(inbox) success at attempt=$attempt")
|
||||
inboxSubscribed = true
|
||||
completeSuccessIfReady()
|
||||
}
|
||||
|
||||
override fun onFailure(errorInfo: ErrorInfo?) {
|
||||
Logger.e("RTM subscribe(inbox) failure at attempt=$attempt reason=${errorInfo?.errorReason}")
|
||||
if (attempt < 4) {
|
||||
subscribeInbox(attempt + 1)
|
||||
} else {
|
||||
failOnce("inbox subscribe failed after 3 retries (4 attempts)")
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// 두 채널 구독을 병렬로 시도
|
||||
subscribeRoom(1)
|
||||
subscribeInbox(1)
|
||||
}
|
||||
|
||||
fun inputChat(message: String, onFailure: () -> Unit) {
|
||||
if (roomChannelName != null) {
|
||||
val options = PublishOptions()
|
||||
options.setChannelType(RtmConstants.RtmChannelType.MESSAGE)
|
||||
rtmClient!!.publish(
|
||||
roomChannelName!!,
|
||||
message,
|
||||
options,
|
||||
object : ResultCallback<Void> {
|
||||
override fun onSuccess(p0: Void?) {
|
||||
Logger.e("sendMessage - onSuccess")
|
||||
}
|
||||
|
||||
override fun onFailure(p0: ErrorInfo) {
|
||||
Logger.e("sendMessage fail - ${p0.errorCode}")
|
||||
Logger.e("sendMessage fail - ${p0.errorReason}")
|
||||
}
|
||||
}
|
||||
)
|
||||
} else {
|
||||
Logger.e("inputChat - roomChannelName is null")
|
||||
onFailure()
|
||||
}
|
||||
}
|
||||
|
||||
fun sendRawMessageToGroup(
|
||||
rawMessage: ByteArray,
|
||||
onSuccess: (() -> Unit)? = null,
|
||||
onFailure: (() -> Unit)? = null
|
||||
) {
|
||||
if (roomChannelName != null) {
|
||||
val options = PublishOptions()
|
||||
options.customType = "ByteArray"
|
||||
rtmClient!!.publish(
|
||||
roomChannelName!!,
|
||||
rawMessage,
|
||||
options,
|
||||
object : ResultCallback<Void> {
|
||||
override fun onSuccess(p0: Void?) {
|
||||
Logger.e("sendMessage - onSuccess")
|
||||
onSuccess?.invoke()
|
||||
}
|
||||
|
||||
override fun onFailure(p0: ErrorInfo) {
|
||||
Logger.e("sendMessage fail - ${p0.errorCode}")
|
||||
Logger.e("sendMessage fail - ${p0.errorReason}")
|
||||
onFailure?.invoke()
|
||||
}
|
||||
}
|
||||
)
|
||||
} else {
|
||||
Logger.e("inputChat - roomChannelName is null")
|
||||
onFailure?.invoke()
|
||||
}
|
||||
}
|
||||
|
||||
fun sendRawMessageToPeer(
|
||||
receiverUid: String,
|
||||
requestType: LiveRoomRequestType? = null,
|
||||
rawMessage: ByteArray? = null,
|
||||
onSuccess: () -> Unit
|
||||
) {
|
||||
if (roomChannelName != null) {
|
||||
val message = rawMessage ?: requestType.toString().toByteArray()
|
||||
val options = PublishOptions()
|
||||
options.customType = "ByteArray"
|
||||
rtmClient!!.publish(
|
||||
"inbox_$receiverUid",
|
||||
message,
|
||||
options,
|
||||
object : ResultCallback<Void> {
|
||||
override fun onSuccess(p0: Void?) {
|
||||
Logger.e("sendMessage - onSuccess")
|
||||
onSuccess()
|
||||
}
|
||||
|
||||
override fun onFailure(p0: ErrorInfo) {
|
||||
Logger.e("sendMessage fail - ${p0.errorCode}")
|
||||
Logger.e("sendMessage fail - ${p0.errorReason}")
|
||||
}
|
||||
}
|
||||
)
|
||||
} else {
|
||||
Logger.e("inputChat - roomChannelName is null")
|
||||
}
|
||||
}
|
||||
|
||||
fun deInitRtmClient(rtmEventListener: RtmEventListener) {
|
||||
rtmClient?.removeEventListener(rtmEventListener)
|
||||
rtmClient?.unsubscribe(roomChannelName, object : ResultCallback<Void> {
|
||||
override fun onSuccess(responseInfo: Void?) {
|
||||
Logger.e("RTM unsubscribe - $roomChannelName")
|
||||
roomChannelName = null
|
||||
}
|
||||
|
||||
override fun onFailure(errorInfo: ErrorInfo) {
|
||||
Logger.e("RTM unsubscribe fail - ${errorInfo.errorCode}")
|
||||
Logger.e("RTM unsubscribe fail - ${errorInfo.errorReason}")
|
||||
}
|
||||
})
|
||||
rtmClient?.unsubscribe(
|
||||
"inbox_${SharedPreferenceManager.userId}",
|
||||
object : ResultCallback<Void> {
|
||||
override fun onSuccess(responseInfo: Void?) {
|
||||
Logger.e("RTM unsubscribe - inbox_${SharedPreferenceManager.userId}")
|
||||
}
|
||||
|
||||
override fun onFailure(errorInfo: ErrorInfo) {
|
||||
Logger.e("RTM unsubscribe fail - ${errorInfo.errorCode}")
|
||||
Logger.e("RTM unsubscribe fail - ${errorInfo.errorReason}")
|
||||
}
|
||||
})
|
||||
rtmClient?.logout(object : ResultCallback<Void> {
|
||||
override fun onSuccess(responseInfo: Void?) {
|
||||
Logger.e("RTM logout")
|
||||
rtmClient = null
|
||||
}
|
||||
|
||||
override fun onFailure(errorInfo: ErrorInfo) {
|
||||
Logger.e("RTM logout fail - ${errorInfo.errorCode}")
|
||||
Logger.e("RTM logout fail - ${errorInfo.errorReason}")
|
||||
}
|
||||
})
|
||||
// 상태 리셋
|
||||
rtmLoggedIn = false
|
||||
rtmLoginInProgress = false
|
||||
|
||||
}
|
||||
// endregion
|
||||
}
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
package kr.co.vividnext.sodalive.agora.v2v
|
||||
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import retrofit2.http.Body
|
||||
import retrofit2.http.Header
|
||||
import retrofit2.http.POST
|
||||
import retrofit2.http.Path
|
||||
|
||||
interface V2vApi {
|
||||
@POST("projects/{appId}/join")
|
||||
fun join(
|
||||
@Path("appId") appId: String,
|
||||
@Header("Authorization") authorization: String,
|
||||
@Header("X-Request-Id") requestId: String,
|
||||
@Body request: V2vJoinRequest
|
||||
): Single<V2vJoinResponse>
|
||||
|
||||
@POST("projects/{appId}/agents/{agentId}/leave")
|
||||
fun leave(
|
||||
@Path("appId") appId: String,
|
||||
@Path("agentId") agentId: String,
|
||||
@Header("Authorization") authorization: String,
|
||||
@Header("X-Request-Id") requestId: String
|
||||
): Single<V2vLeaveResponse>
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package kr.co.vividnext.sodalive.agora.v2v
|
||||
|
||||
import androidx.annotation.Keep
|
||||
import com.google.gson.annotations.SerializedName
|
||||
|
||||
@Keep
|
||||
data class V2vJoinRequest(
|
||||
@SerializedName("name") val name: String,
|
||||
@SerializedName("preset") val preset: String,
|
||||
@SerializedName("properties") val properties: V2vJoinProperties
|
||||
)
|
||||
|
||||
@Keep
|
||||
data class V2vJoinProperties(
|
||||
@SerializedName("channel") val channel: String,
|
||||
@SerializedName("token") val token: String,
|
||||
@SerializedName("agent_rtc_uid") val agentRtcUid: String,
|
||||
@SerializedName("remote_rtc_uids") val remoteRtcUids: List<String>,
|
||||
@SerializedName("idle_timeout") val idleTimeout: Int,
|
||||
@SerializedName("advanced_features") val advancedFeatures: V2vAdvancedFeatures,
|
||||
@SerializedName("parameters") val parameters: V2vParameters,
|
||||
@SerializedName("asr") val asr: V2vAsr,
|
||||
@SerializedName("translation") val translation: V2vTranslation,
|
||||
@SerializedName("tts") val tts: V2vTts
|
||||
)
|
||||
|
||||
@Keep
|
||||
data class V2vAdvancedFeatures(
|
||||
@SerializedName("enable_rtm") val enableRtm: Boolean
|
||||
)
|
||||
|
||||
@Keep
|
||||
data class V2vParameters(
|
||||
@SerializedName("data_channel") val dataChannel: String
|
||||
)
|
||||
|
||||
@Keep
|
||||
data class V2vAsr(
|
||||
@SerializedName("language") val language: String
|
||||
)
|
||||
|
||||
@Keep
|
||||
data class V2vTranslation(
|
||||
@SerializedName("language") val language: String
|
||||
)
|
||||
|
||||
@Keep
|
||||
data class V2vTts(
|
||||
@SerializedName("enable") val enable: Boolean
|
||||
)
|
||||
|
||||
@Keep
|
||||
data class V2vJoinResponse(
|
||||
@SerializedName("agent_id") val agentId: String,
|
||||
@SerializedName("create_ts") val createTs: Long,
|
||||
@SerializedName("status") val status: String
|
||||
)
|
||||
|
||||
@Keep
|
||||
data class V2vLeaveResponse(
|
||||
@SerializedName("agent_id") val agentId: String,
|
||||
@SerializedName("status") val status: String
|
||||
)
|
||||
@@ -0,0 +1,36 @@
|
||||
package kr.co.vividnext.sodalive.agora.v2v
|
||||
|
||||
import android.util.Base64
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import kr.co.vividnext.sodalive.BuildConfig
|
||||
import java.util.UUID
|
||||
|
||||
class V2vRepository(private val api: V2vApi) {
|
||||
fun join(request: V2vJoinRequest): Single<V2vJoinResponse> {
|
||||
return api.join(
|
||||
appId = BuildConfig.AGORA_APP_ID,
|
||||
authorization = buildAuthorizationHeader(),
|
||||
requestId = generateRequestId(),
|
||||
request = request
|
||||
)
|
||||
}
|
||||
|
||||
fun leave(agentId: String): Single<V2vLeaveResponse> {
|
||||
return api.leave(
|
||||
appId = BuildConfig.AGORA_APP_ID,
|
||||
agentId = agentId,
|
||||
authorization = buildAuthorizationHeader(),
|
||||
requestId = generateRequestId()
|
||||
)
|
||||
}
|
||||
|
||||
private fun buildAuthorizationHeader(): String {
|
||||
val credentials = "${BuildConfig.AGORA_CUSTOMER_ID}:${BuildConfig.AGORA_CUSTOMER_SECRET}"
|
||||
val encoded = Base64.encodeToString(credentials.toByteArray(), Base64.NO_WRAP)
|
||||
return "Basic $encoded"
|
||||
}
|
||||
|
||||
private fun generateRequestId(): String {
|
||||
return UUID.randomUUID().toString().replace("-", "")
|
||||
}
|
||||
}
|
||||
@@ -5,15 +5,30 @@ import android.content.pm.ApplicationInfo
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.lifecycle.DefaultLifecycleObserver
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.ProcessLifecycleOwner
|
||||
import com.appsflyer.AppsFlyerLib
|
||||
import com.appsflyer.deeplink.DeepLinkResult
|
||||
import com.facebook.FacebookSdk
|
||||
import com.kakao.sdk.common.KakaoSdk
|
||||
import com.orhanobut.logger.AndroidLogAdapter
|
||||
import com.orhanobut.logger.Logger
|
||||
import com.yandex.mobile.ads.common.MobileAds
|
||||
import kr.co.vividnext.sodalive.BuildConfig
|
||||
import kr.co.vividnext.sodalive.R
|
||||
import kr.co.vividnext.sodalive.chat.talk.room.ChatRoomPreferenceManager
|
||||
import kr.co.vividnext.sodalive.common.ImageLoaderProvider
|
||||
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
|
||||
import kr.co.vividnext.sodalive.common.SodaLiveApplicationHolder
|
||||
import kr.co.vividnext.sodalive.di.AppDI
|
||||
import kr.co.vividnext.sodalive.tracking.FirebaseTracking
|
||||
import tech.notifly.Notifly
|
||||
|
||||
class SodaLiveApp : Application() {
|
||||
class SodaLiveApp : Application(), DefaultLifecycleObserver {
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
super<Application>.onCreate()
|
||||
ProcessLifecycleOwner.get().lifecycle.addObserver(this)
|
||||
|
||||
Logger.addLogAdapter(object : AndroidLogAdapter() {
|
||||
override fun isLoggable(priority: Int, tag: String?): Boolean {
|
||||
@@ -25,7 +40,21 @@ class SodaLiveApp : Application() {
|
||||
|
||||
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES)
|
||||
|
||||
SodaLiveApplicationHolder.init(this)
|
||||
SharedPreferenceManager.init(applicationContext)
|
||||
ChatRoomPreferenceManager.init(applicationContext)
|
||||
|
||||
ImageLoaderProvider.init(applicationContext)
|
||||
|
||||
FacebookSdk.fullyInitialize()
|
||||
|
||||
KakaoSdk.init(applicationContext, BuildConfig.KAKAO_APP_KEY)
|
||||
|
||||
setupAppsFlyer()
|
||||
|
||||
setupNotifly()
|
||||
|
||||
setupYandexAd()
|
||||
}
|
||||
|
||||
private fun isDebuggable(): Boolean {
|
||||
@@ -41,10 +70,90 @@ class SodaLiveApp : Application() {
|
||||
packageManager.getApplicationInfo(packageName, 0)
|
||||
}
|
||||
debuggable = 0 != appInfo.flags and ApplicationInfo.FLAG_DEBUGGABLE
|
||||
} catch (e: PackageManager.NameNotFoundException) {
|
||||
/* debuggable variable will remain false */
|
||||
} catch (_: PackageManager.NameNotFoundException) {
|
||||
}
|
||||
|
||||
return debuggable
|
||||
}
|
||||
|
||||
private fun setupAppsFlyer() {
|
||||
// Appsflyer SDK 초기화
|
||||
AppsFlyerLib.getInstance().init("tWF2wbJ5nSkya5Ru9mGcPU", null, this)
|
||||
AppsFlyerLib.getInstance().start(this)
|
||||
|
||||
// 딥링크 및 디퍼드 딥링크 처리
|
||||
AppsFlyerLib.getInstance().subscribeForDeepLink { deepLinkResult ->
|
||||
when (deepLinkResult.status) {
|
||||
DeepLinkResult.Status.FOUND -> {
|
||||
SharedPreferenceManager.alreadyTrackingAppLaunch = false
|
||||
|
||||
val deepLink = deepLinkResult.deepLink
|
||||
SharedPreferenceManager.marketingLinkValue = deepLink?.getStringValue(
|
||||
"deep_link_value"
|
||||
) ?: ""
|
||||
|
||||
val marketingPid = deepLink?.getStringValue(
|
||||
"deep_link_sub1"
|
||||
)
|
||||
|
||||
if (marketingPid != null) {
|
||||
SharedPreferenceManager.marketingPid = marketingPid
|
||||
}
|
||||
|
||||
SharedPreferenceManager.marketingUtmSource = deepLink?.getStringValue(
|
||||
"deep_link_sub2"
|
||||
) ?: ""
|
||||
SharedPreferenceManager.marketingUtmMedium = deepLink?.getStringValue(
|
||||
"deep_link_sub3"
|
||||
) ?: ""
|
||||
SharedPreferenceManager.marketingUtmCampaign = deepLink?.getStringValue(
|
||||
"deep_link_sub4"
|
||||
) ?: ""
|
||||
SharedPreferenceManager.marketingLinkValueId = deepLink?.getStringValue(
|
||||
"deep_link_sub5"
|
||||
)?.toLongOrNull() ?: 0L
|
||||
|
||||
logUtmInFirebase()
|
||||
}
|
||||
|
||||
DeepLinkResult.Status.NOT_FOUND -> Logger.d(
|
||||
getString(R.string.deeplink_not_found)
|
||||
)
|
||||
|
||||
DeepLinkResult.Status.ERROR -> Logger.d(
|
||||
getString(R.string.deeplink_error, deepLinkResult.error)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun logUtmInFirebase() {
|
||||
FirebaseTracking.logUtm()
|
||||
}
|
||||
|
||||
private fun setupNotifly() {
|
||||
Notifly.initialize(
|
||||
applicationContext,
|
||||
BuildConfig.NOTIFLY_PROJECT_ID,
|
||||
BuildConfig.NOTIFLY_USERNAME,
|
||||
BuildConfig.NOTIFLY_PASSWORD,
|
||||
)
|
||||
}
|
||||
|
||||
private fun setupYandexAd() {
|
||||
MobileAds.enableDebugErrorIndicator(BuildConfig.DEBUG && isDebuggable())
|
||||
}
|
||||
|
||||
override fun onStart(owner: LifecycleOwner) {
|
||||
super.onStart(owner)
|
||||
isAppInForeground = true
|
||||
}
|
||||
|
||||
override fun onStop(owner: LifecycleOwner) {
|
||||
isAppInForeground = false
|
||||
}
|
||||
|
||||
companion object {
|
||||
var isAppInForeground = false
|
||||
}
|
||||
}
|
||||
|
||||
+3
@@ -1,13 +1,16 @@
|
||||
package kr.co.vividnext.sodalive.audio_content
|
||||
|
||||
import androidx.annotation.Keep
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import java.util.TimeZone
|
||||
|
||||
@Keep
|
||||
data class AddAllPlaybackTrackingRequest(
|
||||
@SerializedName("timezone") val timezone: String = TimeZone.getDefault().id,
|
||||
@SerializedName("trackingDataList") val trackingDataList: List<PlaybackTrackingData>
|
||||
)
|
||||
|
||||
@Keep
|
||||
data class PlaybackTrackingData(
|
||||
@SerializedName("contentId") val contentId: Long,
|
||||
@SerializedName("playDateTime") val playDateTime: String,
|
||||
|
||||
@@ -14,6 +14,8 @@ import androidx.core.content.ContextCompat
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import kr.co.vividnext.sodalive.R
|
||||
import kr.co.vividnext.sodalive.audio_content.category.AudioContentCategoryAdapter
|
||||
import kr.co.vividnext.sodalive.audio_content.category.GetCategoryListResponse
|
||||
import kr.co.vividnext.sodalive.audio_content.detail.AudioContentDetailActivity
|
||||
import kr.co.vividnext.sodalive.audio_content.upload.AudioContentUploadActivity
|
||||
import kr.co.vividnext.sodalive.base.BaseActivity
|
||||
@@ -32,9 +34,11 @@ class AudioContentActivity : BaseActivity<ActivityAudioContentBinding>(
|
||||
|
||||
private lateinit var loadingDialog: LoadingDialog
|
||||
private lateinit var audioContentAdapter: AudioContentAdapter
|
||||
private lateinit var categoryAdapter: AudioContentCategoryAdapter
|
||||
|
||||
private var userId: Long = 0
|
||||
private lateinit var activityResultLauncher: ActivityResultLauncher<Intent>
|
||||
private val allCategoryLabel by lazy { getString(R.string.audio_content_label_all) }
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
userId = intent.getLongExtra(Constants.EXTRA_USER_ID, 0)
|
||||
@@ -43,23 +47,29 @@ class AudioContentActivity : BaseActivity<ActivityAudioContentBinding>(
|
||||
) {
|
||||
if (it.resultCode == Activity.RESULT_OK) {
|
||||
viewModel.page = 1
|
||||
viewModel.isLast = false
|
||||
viewModel.getAudioContentList(userId = userId) { finish() }
|
||||
}
|
||||
}
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
if (userId <= 0) {
|
||||
Toast.makeText(applicationContext, "잘못된 요청입니다.", Toast.LENGTH_LONG).show()
|
||||
Toast.makeText(
|
||||
applicationContext,
|
||||
getString(R.string.screen_audio_content_error_invalid_request),
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
finish()
|
||||
}
|
||||
|
||||
bindData()
|
||||
viewModel.getCategoryList(userId = userId)
|
||||
viewModel.getAudioContentList(userId = userId) { finish() }
|
||||
}
|
||||
|
||||
override fun setupView() {
|
||||
loadingDialog = LoadingDialog(this, layoutInflater)
|
||||
binding.toolbar.tvBack.text = "콘텐츠 전체보기"
|
||||
binding.toolbar.tvBack.text = getString(R.string.screen_audio_content_title)
|
||||
binding.toolbar.tvBack.setOnClickListener { finish() }
|
||||
|
||||
audioContentAdapter = AudioContentAdapter {
|
||||
@@ -70,6 +80,69 @@ class AudioContentActivity : BaseActivity<ActivityAudioContentBinding>(
|
||||
activityResultLauncher.launch(intent)
|
||||
}
|
||||
|
||||
categoryAdapter = AudioContentCategoryAdapter(
|
||||
allCategoryLabel = allCategoryLabel
|
||||
) {
|
||||
viewModel.selectCategory(it, userId = userId)
|
||||
}
|
||||
|
||||
binding.rvCategory.layoutManager = LinearLayoutManager(
|
||||
applicationContext,
|
||||
LinearLayoutManager.HORIZONTAL,
|
||||
false
|
||||
)
|
||||
|
||||
binding.rvCategory.addItemDecoration(object : RecyclerView.ItemDecoration() {
|
||||
override fun getItemOffsets(
|
||||
outRect: Rect,
|
||||
view: View,
|
||||
parent: RecyclerView,
|
||||
state: RecyclerView.State
|
||||
) {
|
||||
super.getItemOffsets(outRect, view, parent, state)
|
||||
|
||||
when (parent.getChildAdapterPosition(view)) {
|
||||
0 -> {
|
||||
outRect.left = 0
|
||||
outRect.right = 4f.dpToPx().toInt()
|
||||
}
|
||||
|
||||
categoryAdapter.itemCount - 1 -> {
|
||||
outRect.left = 4f.dpToPx().toInt()
|
||||
outRect.right = 0
|
||||
}
|
||||
|
||||
else -> {
|
||||
outRect.left = 4f.dpToPx().toInt()
|
||||
outRect.right = 4f.dpToPx().toInt()
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
binding.rvCategory.adapter = categoryAdapter
|
||||
|
||||
binding.rvAudioContent.addItemDecoration(object : RecyclerView.ItemDecoration() {
|
||||
override fun getItemOffsets(
|
||||
outRect: Rect,
|
||||
view: View,
|
||||
parent: RecyclerView,
|
||||
state: RecyclerView.State
|
||||
) {
|
||||
super.getItemOffsets(outRect, view, parent, state)
|
||||
|
||||
when (parent.getChildAdapterPosition(view)) {
|
||||
audioContentAdapter.itemCount - 1 -> {
|
||||
outRect.bottom = 0
|
||||
}
|
||||
|
||||
else -> {
|
||||
outRect.bottom = 13.3f.dpToPx().toInt()
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
binding.rvAudioContent.layoutManager = LinearLayoutManager(
|
||||
applicationContext,
|
||||
LinearLayoutManager.VERTICAL,
|
||||
@@ -188,6 +261,17 @@ class AudioContentActivity : BaseActivity<ActivityAudioContentBinding>(
|
||||
)
|
||||
viewModel.getAudioContentList(userId = userId) { finish() }
|
||||
}
|
||||
|
||||
viewModel.categoryListLiveData.observe(this) {
|
||||
if (it.isNotEmpty()) {
|
||||
binding.rvCategory.visibility = View.VISIBLE
|
||||
val items = it as MutableList<GetCategoryListResponse>
|
||||
items.add(0, GetCategoryListResponse(0, allCategoryLabel))
|
||||
categoryAdapter.addItems(items = items)
|
||||
} else {
|
||||
binding.rvCategory.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun deselectSort() {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package kr.co.vividnext.sodalive.audio_content
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import coil.load
|
||||
@@ -21,6 +22,18 @@ class AudioContentAdapter(
|
||||
private val binding: ItemAudioContentBinding
|
||||
) : RecyclerView.ViewHolder(binding.root) {
|
||||
fun bind(item: GetAudioContentListItem) {
|
||||
binding.ivPin.visibility = if (item.isPin) {
|
||||
View.VISIBLE
|
||||
} else {
|
||||
View.GONE
|
||||
}
|
||||
|
||||
binding.tvPoint.visibility = if (item.isPointAvailable) {
|
||||
View.VISIBLE
|
||||
} else {
|
||||
View.GONE
|
||||
}
|
||||
|
||||
binding.ivCover.load(item.coverImageUrl) {
|
||||
crossfade(true)
|
||||
placeholder(R.drawable.bg_placeholder)
|
||||
@@ -33,17 +46,38 @@ class AudioContentAdapter(
|
||||
binding.tvLikeCount.text = item.likeCount.moneyFormat()
|
||||
binding.tvCommentCount.text = item.commentCount.moneyFormat()
|
||||
|
||||
if (item.price < 1) {
|
||||
binding.tvPrice.text = "무료"
|
||||
binding.tvPrice.setCompoundDrawables(null, null, null, null)
|
||||
binding.tvPrice.visibility = View.GONE
|
||||
binding.tvOwned.visibility = View.GONE
|
||||
binding.tvRented.visibility = View.GONE
|
||||
binding.tvSoldOut.visibility = View.GONE
|
||||
|
||||
binding.tvScheduledToOpen.visibility = if (item.isScheduledToOpen) {
|
||||
View.VISIBLE
|
||||
} else {
|
||||
binding.tvPrice.text = item.price.moneyFormat()
|
||||
binding.tvPrice.setCompoundDrawablesWithIntrinsicBounds(
|
||||
R.drawable.ic_can,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
)
|
||||
View.GONE
|
||||
}
|
||||
|
||||
if (item.isOwned) {
|
||||
binding.tvOwned.visibility = View.VISIBLE
|
||||
} else if (item.isRented) {
|
||||
binding.tvRented.visibility = View.VISIBLE
|
||||
} else if (item.isSoldOut) {
|
||||
binding.tvSoldOut.visibility = View.VISIBLE
|
||||
} else {
|
||||
binding.tvPrice.visibility = View.VISIBLE
|
||||
if (item.price < 1) {
|
||||
binding.tvPrice.text =
|
||||
binding.root.context.getString(R.string.audio_content_price_free)
|
||||
binding.tvPrice.setCompoundDrawables(null, null, null, null)
|
||||
} else {
|
||||
binding.tvPrice.text = item.price.moneyFormat()
|
||||
binding.tvPrice.setCompoundDrawablesWithIntrinsicBounds(
|
||||
R.drawable.ic_can,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
binding.root.setOnClickListener { onClickItem(item.contentId) }
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
package kr.co.vividnext.sodalive.audio_content
|
||||
|
||||
import io.reactivex.rxjava3.core.Flowable
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import kr.co.vividnext.sodalive.audio_content.all.GetNewContentAllResponse
|
||||
import kr.co.vividnext.sodalive.audio_content.all.by_theme.GetContentByThemeResponse
|
||||
import kr.co.vividnext.sodalive.audio_content.comment.GetAudioContentCommentListResponse
|
||||
import kr.co.vividnext.sodalive.audio_content.comment.ModifyCommentRequest
|
||||
import kr.co.vividnext.sodalive.audio_content.comment.RegisterAudioContentCommentRequest
|
||||
@@ -8,13 +11,17 @@ import kr.co.vividnext.sodalive.audio_content.detail.GetAudioContentDetailRespon
|
||||
import kr.co.vividnext.sodalive.audio_content.detail.PutAudioContentLikeRequest
|
||||
import kr.co.vividnext.sodalive.audio_content.detail.PutAudioContentLikeResponse
|
||||
import kr.co.vividnext.sodalive.audio_content.donation.AudioContentDonationRequest
|
||||
import kr.co.vividnext.sodalive.audio_content.main.GetAudioContentBannerResponse
|
||||
import kr.co.vividnext.sodalive.audio_content.main.GetAudioContentCurationResponse
|
||||
import kr.co.vividnext.sodalive.audio_content.main.GetAudioContentMainItem
|
||||
import kr.co.vividnext.sodalive.audio_content.main.GetAudioContentMainResponse
|
||||
import kr.co.vividnext.sodalive.audio_content.main.GetAudioContentRanking
|
||||
import kr.co.vividnext.sodalive.audio_content.order.GetAudioContentOrderListResponse
|
||||
import kr.co.vividnext.sodalive.audio_content.order.OrderRequest
|
||||
import kr.co.vividnext.sodalive.audio_content.player.GenerateUrlResponse
|
||||
import kr.co.vividnext.sodalive.audio_content.upload.theme.GetAudioContentThemeResponse
|
||||
import kr.co.vividnext.sodalive.common.ApiResponse
|
||||
import kr.co.vividnext.sodalive.explorer.profile.GetAudioContentListResponse
|
||||
import kr.co.vividnext.sodalive.home.AudioContentMainItem
|
||||
import okhttp3.MultipartBody
|
||||
import okhttp3.RequestBody
|
||||
import retrofit2.http.Body
|
||||
@@ -29,20 +36,53 @@ import retrofit2.http.Path
|
||||
import retrofit2.http.Query
|
||||
|
||||
interface AudioContentApi {
|
||||
@GET("/audio-content/all")
|
||||
fun getAllAudioContents(
|
||||
@Query("page") page: Int,
|
||||
@Query("size") size: Int,
|
||||
@Query("isFree") isFree: Boolean?,
|
||||
@Query("isPointAvailableOnly") isPointAvailableOnly: Boolean?,
|
||||
@Query("sort-type") sortType: AudioContentViewModel.Sort = AudioContentViewModel.Sort.NEWEST,
|
||||
@Query("theme") theme: String? = null,
|
||||
@Header("Authorization") authHeader: String
|
||||
): Single<ApiResponse<List<AudioContentMainItem>>>
|
||||
|
||||
@GET("/audio-content")
|
||||
fun getAudioContentList(
|
||||
@Query("creator-id") id: Long,
|
||||
@Query("category-id") categoryId: Long,
|
||||
@Query("page") page: Int,
|
||||
@Query("size") size: Int,
|
||||
@Query("sort-type") sort: AudioContentViewModel.Sort,
|
||||
@Header("Authorization") authHeader: String
|
||||
): Single<ApiResponse<GetAudioContentListResponse>>
|
||||
|
||||
@GET("/audio-content/replay-live")
|
||||
fun getAudioContentReplayLiveList(
|
||||
@Header("Authorization") authHeader: String
|
||||
): Flowable<ApiResponse<List<GetAudioContentMainItem>>>
|
||||
|
||||
@GET("/audio-content/theme")
|
||||
fun getAudioContentThemeList(
|
||||
@Header("Authorization") authHeader: String
|
||||
): Single<ApiResponse<List<GetAudioContentThemeResponse>>>
|
||||
|
||||
@GET("/audio-content/theme/active")
|
||||
fun getAudioContentActiveThemeList(
|
||||
@Query("isFree") isFree: Boolean?,
|
||||
@Query("isPointAvailableOnly") isPointAvailableOnly: Boolean?,
|
||||
@Header("Authorization") authHeader: String
|
||||
): Single<ApiResponse<List<String>>>
|
||||
|
||||
@GET("/audio-content/theme/{id}/content")
|
||||
fun getAudioContentByTheme(
|
||||
@Path("id") id: Long,
|
||||
@Query("page") page: Int,
|
||||
@Query("size") size: Int,
|
||||
@Query("sort-type") sort: AudioContentViewModel.Sort,
|
||||
@Header("Authorization") authHeader: String
|
||||
): Single<ApiResponse<GetContentByThemeResponse>>
|
||||
|
||||
@POST("/audio-content")
|
||||
@Multipart
|
||||
fun uploadAudioContent(
|
||||
@@ -122,17 +162,21 @@ interface AudioContentApi {
|
||||
@Header("Authorization") authHeader: String
|
||||
): Single<ApiResponse<Any>>
|
||||
|
||||
@GET("/audio-content/main")
|
||||
fun getMain(
|
||||
@Header("Authorization") authHeader: String
|
||||
): Single<ApiResponse<GetAudioContentMainResponse>>
|
||||
|
||||
@GET("/audio-content/main/new")
|
||||
fun getNewContentOfTheme(
|
||||
@Query("theme") theme: String,
|
||||
@Header("Authorization") authHeader: String
|
||||
): Single<ApiResponse<List<GetAudioContentMainItem>>>
|
||||
|
||||
@GET("/audio-content/main/new/all")
|
||||
fun getNewContentAllOfTheme(
|
||||
@Query("isFree") isFree: Boolean,
|
||||
@Query("theme") theme: String,
|
||||
@Query("page") page: Int,
|
||||
@Query("size") size: Int,
|
||||
@Header("Authorization") authHeader: String
|
||||
): Single<ApiResponse<GetNewContentAllResponse>>
|
||||
|
||||
@POST("/audio-content/donation")
|
||||
fun donation(
|
||||
@Body request: AudioContentDonationRequest,
|
||||
@@ -144,4 +188,57 @@ interface AudioContentApi {
|
||||
@Body request: ModifyCommentRequest,
|
||||
@Header("Authorization") authHeader: String
|
||||
): Single<ApiResponse<Any>>
|
||||
|
||||
@GET("/audio-content/main/theme")
|
||||
fun getNewContentThemeList(
|
||||
@Header("Authorization") authHeader: String
|
||||
): Single<ApiResponse<List<String>>>
|
||||
|
||||
@GET("/audio-content/ranking-sort-type")
|
||||
fun getContentRankingSortType(
|
||||
@Header("Authorization") authHeader: String
|
||||
): Single<ApiResponse<List<String>>>
|
||||
|
||||
@GET("/audio-content/ranking")
|
||||
fun getContentRanking(
|
||||
@Query("page") page: Int,
|
||||
@Query("size") size: Int,
|
||||
@Query("sort-type") sortType: String,
|
||||
@Header("Authorization") authHeader: String
|
||||
): Single<ApiResponse<GetAudioContentRanking>>
|
||||
|
||||
@GET("/audio-content/main/curation-list")
|
||||
fun getCurationList(
|
||||
@Query("page") page: Int,
|
||||
@Query("size") size: Int,
|
||||
@Header("Authorization") authHeader: String
|
||||
): Single<ApiResponse<List<GetAudioContentCurationResponse>>>
|
||||
|
||||
@GET("/audio-content/main/banner-list")
|
||||
fun getMainBannerList(
|
||||
@Header("Authorization") authHeader: String
|
||||
): Single<ApiResponse<List<GetAudioContentBannerResponse>>>
|
||||
|
||||
@GET("/audio-content/main/order-list")
|
||||
fun getMainOrderList(
|
||||
@Header("Authorization") authHeader: String
|
||||
): Single<ApiResponse<List<GetAudioContentMainItem>>>
|
||||
|
||||
@POST("/audio-content/pin-to-the-top/{id}")
|
||||
fun pinContent(
|
||||
@Path("id") audioContentId: Long,
|
||||
@Header("Authorization") authHeader: String
|
||||
): Single<ApiResponse<Any>>
|
||||
|
||||
@PUT("/audio-content/unpin-at-the-top/{id}")
|
||||
fun unpinContent(
|
||||
@Path("id") audioContentId: Long,
|
||||
@Header("Authorization") authHeader: String
|
||||
): Single<ApiResponse<Any>>
|
||||
|
||||
@GET("/audio-content/{id}/generate-url")
|
||||
fun generateUrl(
|
||||
@Path("id") contentId: Long,
|
||||
@Header("Authorization") authHeader: String
|
||||
): Single<ApiResponse<GenerateUrlResponse>>
|
||||
}
|
||||
|
||||
+64
-7
@@ -1,3 +1,5 @@
|
||||
@file:Suppress("ktlint:package-name", "ktlint:standard:package-name")
|
||||
|
||||
package kr.co.vividnext.sodalive.audio_content
|
||||
|
||||
import android.app.NotificationChannel
|
||||
@@ -6,6 +8,7 @@ import android.app.PendingIntent
|
||||
import android.app.Service
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.media.AudioAttributes
|
||||
@@ -21,7 +24,7 @@ import com.bumptech.glide.request.transition.Transition
|
||||
import kr.co.vividnext.sodalive.R
|
||||
import kr.co.vividnext.sodalive.common.Constants
|
||||
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
|
||||
import kr.co.vividnext.sodalive.main.MainActivity
|
||||
import kr.co.vividnext.sodalive.v2.main.MainV2Activity
|
||||
import org.koin.android.ext.android.inject
|
||||
|
||||
class AudioContentPlayService :
|
||||
@@ -51,6 +54,7 @@ class AudioContentPlayService :
|
||||
putExtra(Constants.EXTRA_AUDIO_CONTENT_PROGRESS, mediaPlayer.currentPosition)
|
||||
putExtra(Constants.EXTRA_AUDIO_CONTENT_ID, contentId)
|
||||
}
|
||||
intent.setPackage(packageName)
|
||||
sendBroadcast(intent)
|
||||
handler.postDelayed(this, 1000)
|
||||
}
|
||||
@@ -64,6 +68,7 @@ class AudioContentPlayService :
|
||||
sendBroadcast(
|
||||
Intent(Constants.ACTION_AUDIO_CONTENT_RECEIVER)
|
||||
.apply {
|
||||
setPackage(packageName)
|
||||
putExtra(
|
||||
Constants.EXTRA_AUDIO_CONTENT_CHANGE_UI,
|
||||
true
|
||||
@@ -101,6 +106,7 @@ class AudioContentPlayService :
|
||||
sendBroadcast(
|
||||
Intent(Constants.ACTION_MAIN_AUDIO_CONTENT_RECEIVER)
|
||||
.apply {
|
||||
setPackage(packageName)
|
||||
putExtra(
|
||||
Constants.EXTRA_AUDIO_CONTENT_SHOWING,
|
||||
true
|
||||
@@ -136,6 +142,7 @@ class AudioContentPlayService :
|
||||
sendBroadcast(
|
||||
Intent(Constants.ACTION_MAIN_AUDIO_CONTENT_RECEIVER)
|
||||
.apply {
|
||||
setPackage(packageName)
|
||||
putExtra(
|
||||
Constants.EXTRA_AUDIO_CONTENT_SHOWING,
|
||||
false
|
||||
@@ -198,6 +205,14 @@ class AudioContentPlayService :
|
||||
}
|
||||
}
|
||||
|
||||
MusicAction.SEEK_BACKWARD.name -> {
|
||||
seekBackward10Seconds()
|
||||
}
|
||||
|
||||
MusicAction.SEEK_FORWARD.name -> {
|
||||
seekForward10Seconds()
|
||||
}
|
||||
|
||||
else -> {
|
||||
val contentId = intent?.getLongExtra(Constants.EXTRA_AUDIO_CONTENT_ID, 0)
|
||||
if (contentId != null && this.contentId == contentId) {
|
||||
@@ -205,6 +220,7 @@ class AudioContentPlayService :
|
||||
sendBroadcast(
|
||||
Intent(Constants.ACTION_AUDIO_CONTENT_RECEIVER)
|
||||
.apply {
|
||||
setPackage(packageName)
|
||||
putExtra(
|
||||
Constants.EXTRA_AUDIO_CONTENT_NEXT_ACTION,
|
||||
MusicAction.PAUSE
|
||||
@@ -220,6 +236,7 @@ class AudioContentPlayService :
|
||||
sendBroadcast(
|
||||
Intent(Constants.ACTION_AUDIO_CONTENT_RECEIVER)
|
||||
.apply {
|
||||
setPackage(packageName)
|
||||
putExtra(
|
||||
Constants.EXTRA_AUDIO_CONTENT_NEXT_ACTION,
|
||||
MusicAction.PLAY
|
||||
@@ -254,6 +271,7 @@ class AudioContentPlayService :
|
||||
sendBroadcast(
|
||||
Intent(Constants.ACTION_AUDIO_CONTENT_RECEIVER)
|
||||
.apply {
|
||||
setPackage(packageName)
|
||||
putExtra(
|
||||
Constants.EXTRA_AUDIO_CONTENT_LOADING,
|
||||
true
|
||||
@@ -306,6 +324,23 @@ class AudioContentPlayService :
|
||||
}
|
||||
}
|
||||
|
||||
private fun seekForward10Seconds() {
|
||||
if (this::mediaPlayer.isInitialized && mediaPlayer.isPlaying) {
|
||||
val currentPosition = mediaPlayer.currentPosition
|
||||
val duration = mediaPlayer.duration
|
||||
val newPosition = (currentPosition + 10_000).coerceAtMost(duration)
|
||||
mediaPlayer.seekTo(newPosition)
|
||||
}
|
||||
}
|
||||
|
||||
private fun seekBackward10Seconds() {
|
||||
if (this::mediaPlayer.isInitialized && mediaPlayer.isPlaying) {
|
||||
val currentPosition = mediaPlayer.currentPosition
|
||||
val newPosition = (currentPosition - 10_000).coerceAtLeast(0)
|
||||
mediaPlayer.seekTo(newPosition)
|
||||
}
|
||||
}
|
||||
|
||||
private fun toggleIsPlaying(isPlaying: Boolean? = null) {
|
||||
this.isPlaying = isPlaying ?: !this.isPlaying
|
||||
if (this.isPlaying) {
|
||||
@@ -317,6 +352,7 @@ class AudioContentPlayService :
|
||||
sendBroadcast(
|
||||
Intent(Constants.ACTION_AUDIO_CONTENT_RECEIVER)
|
||||
.apply {
|
||||
setPackage(packageName)
|
||||
putExtra(
|
||||
Constants.EXTRA_AUDIO_CONTENT_CHANGE_UI,
|
||||
true
|
||||
@@ -341,6 +377,7 @@ class AudioContentPlayService :
|
||||
sendBroadcast(
|
||||
Intent(Constants.ACTION_MAIN_AUDIO_CONTENT_RECEIVER)
|
||||
.apply {
|
||||
setPackage(packageName)
|
||||
putExtra(
|
||||
Constants.EXTRA_AUDIO_CONTENT_PLAYING,
|
||||
this@AudioContentPlayService.isPlaying
|
||||
@@ -367,6 +404,7 @@ class AudioContentPlayService :
|
||||
mediaPlayer.setOnCompletionListener(this)
|
||||
mediaPlayer.setAudioAttributes(
|
||||
AudioAttributes.Builder()
|
||||
.setUsage(AudioAttributes.USAGE_MEDIA)
|
||||
.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
|
||||
.build()
|
||||
)
|
||||
@@ -378,6 +416,7 @@ class AudioContentPlayService :
|
||||
sendBroadcast(
|
||||
Intent(Constants.ACTION_AUDIO_CONTENT_RECEIVER)
|
||||
.apply {
|
||||
setPackage(packageName)
|
||||
putExtra(
|
||||
Constants.EXTRA_AUDIO_CONTENT_NEXT_ACTION,
|
||||
MusicAction.PLAY
|
||||
@@ -403,6 +442,7 @@ class AudioContentPlayService :
|
||||
sendBroadcast(
|
||||
Intent(Constants.ACTION_MAIN_AUDIO_CONTENT_RECEIVER)
|
||||
.apply {
|
||||
setPackage(packageName)
|
||||
putExtra(
|
||||
Constants.EXTRA_AUDIO_CONTENT_PLAYING,
|
||||
false
|
||||
@@ -433,9 +473,17 @@ class AudioContentPlayService :
|
||||
}
|
||||
|
||||
private fun updateNotification() {
|
||||
val intent = Intent(this, MainActivity::class.java)
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP)
|
||||
val intent = Intent(this, MainV2Activity::class.java).apply {
|
||||
addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
|
||||
addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP)
|
||||
putExtra(
|
||||
MainV2Activity.EXTRA_AUDIO_NOTIFICATION_ROUTE,
|
||||
MainV2Activity.ROUTE_AUDIO_DETAIL
|
||||
)
|
||||
contentId?.let {
|
||||
putExtra(Constants.EXTRA_AUDIO_CONTENT_ID, it)
|
||||
}
|
||||
}
|
||||
val pendingIntent = PendingIntent.getActivity(
|
||||
this,
|
||||
0,
|
||||
@@ -468,7 +516,7 @@ class AudioContentPlayService :
|
||||
override fun onResourceReady(resource: Bitmap, transition: Transition<in Bitmap>?) {
|
||||
val notificationBuilder = NotificationCompat
|
||||
.Builder(this@AudioContentPlayService, channelId)
|
||||
.setSmallIcon(R.drawable.ic_notification)
|
||||
.setSmallIcon(R.mipmap.ic_launcher)
|
||||
.setLargeIcon(resource)
|
||||
.setContentTitle(title ?: "오디오 콘텐츠")
|
||||
.setContentText(nickname ?: "")
|
||||
@@ -498,7 +546,16 @@ class AudioContentPlayService :
|
||||
.setShowActionsInCompactView(0, 1)
|
||||
)
|
||||
|
||||
startForeground(1, notificationBuilder.build())
|
||||
val notification = notificationBuilder.build()
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
startForeground(
|
||||
1,
|
||||
notification,
|
||||
FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK
|
||||
)
|
||||
} else {
|
||||
startForeground(1, notification)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onLoadCleared(placeholder: Drawable?) {
|
||||
@@ -556,6 +613,6 @@ class AudioContentPlayService :
|
||||
}
|
||||
|
||||
enum class MusicAction {
|
||||
PLAY, PAUSE, STOP, PROGRESS, INIT, CONDITIONAL_STOP
|
||||
PLAY, PAUSE, STOP, PROGRESS, INIT, CONDITIONAL_STOP, SEEK_FORWARD, SEEK_BACKWARD
|
||||
}
|
||||
}
|
||||
|
||||
+100
-23
@@ -1,33 +1,40 @@
|
||||
package kr.co.vividnext.sodalive.audio_content
|
||||
|
||||
import kr.co.vividnext.sodalive.R
|
||||
import kr.co.vividnext.sodalive.audio_content.category.CategoryApi
|
||||
import kr.co.vividnext.sodalive.audio_content.detail.PutAudioContentLikeRequest
|
||||
import kr.co.vividnext.sodalive.audio_content.donation.AudioContentDonationRequest
|
||||
import kr.co.vividnext.sodalive.audio_content.order.OrderRequest
|
||||
import kr.co.vividnext.sodalive.audio_content.order.OrderType
|
||||
import kr.co.vividnext.sodalive.user.CreatorFollowRequestRequest
|
||||
import kr.co.vividnext.sodalive.user.UserApi
|
||||
import kr.co.vividnext.sodalive.common.SodaLiveApplicationHolder
|
||||
import okhttp3.MultipartBody
|
||||
import okhttp3.RequestBody
|
||||
import java.util.TimeZone
|
||||
|
||||
class AudioContentRepository(
|
||||
private val api: AudioContentApi,
|
||||
private val userApi: UserApi
|
||||
private val categoryApi: CategoryApi
|
||||
) {
|
||||
fun getAudioContentList(
|
||||
id: Long,
|
||||
categoryId: Long,
|
||||
page: Int,
|
||||
size: Int,
|
||||
sort: AudioContentViewModel.Sort,
|
||||
token: String
|
||||
) = api.getAudioContentList(
|
||||
id = id,
|
||||
categoryId = categoryId,
|
||||
page = page - 1,
|
||||
size = size,
|
||||
sort = sort,
|
||||
authHeader = token
|
||||
)
|
||||
|
||||
fun getAudioContentReplayLiveList(token: String) = api.getAudioContentReplayLiveList(
|
||||
authHeader = token
|
||||
)
|
||||
|
||||
fun getAudioContentThemeList(token: String) = api.getAudioContentThemeList(token)
|
||||
|
||||
fun uploadAudioContent(
|
||||
@@ -60,28 +67,15 @@ class AudioContentRepository(
|
||||
authHeader = token
|
||||
)
|
||||
|
||||
fun getAudioContentDetail(audioContentId: Long, token: String) = api.getAudioContentDetail(
|
||||
fun getAudioContentDetail(
|
||||
audioContentId: Long,
|
||||
token: String
|
||||
) = api.getAudioContentDetail(
|
||||
id = audioContentId,
|
||||
timezone = TimeZone.getDefault().id,
|
||||
authHeader = token
|
||||
)
|
||||
|
||||
fun registerNotification(
|
||||
creatorId: Long,
|
||||
token: String
|
||||
) = userApi.creatorFollow(
|
||||
request = CreatorFollowRequestRequest(creatorId = creatorId),
|
||||
authHeader = token
|
||||
)
|
||||
|
||||
fun unRegisterNotification(
|
||||
creatorId: Long,
|
||||
token: String
|
||||
) = userApi.creatorUnFollow(
|
||||
request = CreatorFollowRequestRequest(creatorId = creatorId),
|
||||
authHeader = token
|
||||
)
|
||||
|
||||
fun orderContent(
|
||||
contentId: Long,
|
||||
orderType: OrderType,
|
||||
@@ -115,10 +109,21 @@ class AudioContentRepository(
|
||||
token: String
|
||||
) = api.likeContent(request, authHeader = token)
|
||||
|
||||
fun getMain(token: String) = api.getMain(authHeader = token)
|
||||
|
||||
fun getNewContentOfTheme(theme: String, token: String) = api.getNewContentOfTheme(
|
||||
fun getNewContentAllOfTheme(
|
||||
isFree: Boolean,
|
||||
theme: String,
|
||||
page: Int,
|
||||
size: Int,
|
||||
token: String
|
||||
) = api.getNewContentAllOfTheme(
|
||||
isFree = isFree,
|
||||
theme = theme,
|
||||
page = page - 1,
|
||||
size = size,
|
||||
authHeader = token
|
||||
)
|
||||
|
||||
fun getNewContentThemeList(token: String) = api.getNewContentThemeList(
|
||||
authHeader = token
|
||||
)
|
||||
|
||||
@@ -135,4 +140,76 @@ class AudioContentRepository(
|
||||
),
|
||||
authHeader = token
|
||||
)
|
||||
|
||||
fun getContentRankingSortType(token: String) = api.getContentRankingSortType(authHeader = token)
|
||||
|
||||
fun getContentRanking(
|
||||
page: Int,
|
||||
size: Int,
|
||||
sortType: String = SodaLiveApplicationHolder.get()
|
||||
.getString(R.string.screen_home_sort_revenue),
|
||||
token: String
|
||||
) = api.getContentRanking(
|
||||
page = page - 1,
|
||||
size = size,
|
||||
sortType = sortType,
|
||||
authHeader = token
|
||||
)
|
||||
|
||||
fun pinContent(
|
||||
audioContentId: Long,
|
||||
token: String
|
||||
) = api.pinContent(audioContentId, authHeader = token)
|
||||
|
||||
fun unpinContent(
|
||||
audioContentId: Long,
|
||||
token: String
|
||||
) = api.unpinContent(audioContentId, authHeader = token)
|
||||
|
||||
fun getCategoryList(
|
||||
creatorId: Long,
|
||||
token: String
|
||||
) = categoryApi.getCategoryList(creatorId, authHeader = token)
|
||||
|
||||
fun getAudioContentByTheme(
|
||||
themeId: Long,
|
||||
page: Int,
|
||||
size: Int,
|
||||
sort: AudioContentViewModel.Sort = AudioContentViewModel.Sort.NEWEST,
|
||||
token: String
|
||||
) = api.getAudioContentByTheme(
|
||||
id = themeId,
|
||||
page = page - 1,
|
||||
size = size,
|
||||
sort = sort,
|
||||
authHeader = token
|
||||
)
|
||||
|
||||
fun getAllAudioContents(
|
||||
page: Int,
|
||||
size: Int,
|
||||
isFree: Boolean? = null,
|
||||
isPointAvailableOnly: Boolean? = null,
|
||||
sortType: AudioContentViewModel.Sort = AudioContentViewModel.Sort.NEWEST,
|
||||
theme: String? = null,
|
||||
token: String
|
||||
) = api.getAllAudioContents(
|
||||
page = page - 1,
|
||||
size = size,
|
||||
isFree = isFree,
|
||||
isPointAvailableOnly = isPointAvailableOnly,
|
||||
sortType = sortType,
|
||||
theme = theme,
|
||||
authHeader = token
|
||||
)
|
||||
|
||||
fun getAudioContentActiveThemeList(
|
||||
isFree: Boolean? = null,
|
||||
isPointAvailableOnly: Boolean? = null,
|
||||
token: String
|
||||
) = api.getAudioContentActiveThemeList(
|
||||
isFree = isFree,
|
||||
isPointAvailableOnly = isPointAvailableOnly,
|
||||
authHeader = token
|
||||
)
|
||||
}
|
||||
|
||||
@@ -6,8 +6,11 @@ import com.google.gson.annotations.SerializedName
|
||||
import com.orhanobut.logger.Logger
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import kr.co.vividnext.sodalive.R
|
||||
import kr.co.vividnext.sodalive.audio_content.category.GetCategoryListResponse
|
||||
import kr.co.vividnext.sodalive.base.BaseViewModel
|
||||
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
|
||||
import kr.co.vividnext.sodalive.common.SodaLiveApplicationHolder
|
||||
import kr.co.vividnext.sodalive.explorer.profile.GetAudioContentListResponse
|
||||
|
||||
class AudioContentViewModel(private val repository: AudioContentRepository) : BaseViewModel() {
|
||||
@@ -24,6 +27,10 @@ class AudioContentViewModel(private val repository: AudioContentRepository) : Ba
|
||||
val audioContentListLiveData: LiveData<GetAudioContentListResponse>
|
||||
get() = _audioContentListLiveData
|
||||
|
||||
private var _categoryListLiveData = MutableLiveData<List<GetCategoryListResponse>>()
|
||||
val categoryListLiveData: LiveData<List<GetCategoryListResponse>>
|
||||
get() = _categoryListLiveData
|
||||
|
||||
private val _sort = MutableLiveData(Sort.NEWEST)
|
||||
val sort: LiveData<Sort>
|
||||
get() = _sort
|
||||
@@ -36,12 +43,16 @@ class AudioContentViewModel(private val repository: AudioContentRepository) : Ba
|
||||
PRICE_HIGH,
|
||||
|
||||
@SerializedName("PRICE_LOW")
|
||||
PRICE_LOW
|
||||
PRICE_LOW,
|
||||
|
||||
@SerializedName("POPULARITY")
|
||||
POPULARITY
|
||||
}
|
||||
|
||||
private var isLast = false
|
||||
var isLast = false
|
||||
var page = 1
|
||||
private val size = 10
|
||||
private var selectedCategoryId = 0L
|
||||
|
||||
fun getAudioContentList(userId: Long, onFailure: (() -> Unit)? = null) {
|
||||
if (!_isLoading.value!! && !isLast) {
|
||||
@@ -49,6 +60,7 @@ class AudioContentViewModel(private val repository: AudioContentRepository) : Ba
|
||||
compositeDisposable.add(
|
||||
repository.getAudioContentList(
|
||||
id = userId,
|
||||
categoryId = selectedCategoryId,
|
||||
page = page,
|
||||
size = size,
|
||||
token = "Bearer ${SharedPreferenceManager.token}",
|
||||
@@ -59,18 +71,19 @@ class AudioContentViewModel(private val repository: AudioContentRepository) : Ba
|
||||
.subscribe(
|
||||
{
|
||||
if (it.success && it.data != null) {
|
||||
if (it.data.items.isNotEmpty()) {
|
||||
page += 1
|
||||
_audioContentListLiveData.postValue(it.data!!)
|
||||
} else {
|
||||
if (it.data.items.isEmpty()) {
|
||||
isLast = true
|
||||
}
|
||||
|
||||
page += 1
|
||||
_audioContentListLiveData.postValue(it.data!!)
|
||||
} else {
|
||||
if (it.message != null) {
|
||||
_toastLiveData.postValue(it.message)
|
||||
} else {
|
||||
_toastLiveData.postValue(
|
||||
"알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
|
||||
SodaLiveApplicationHolder.get()
|
||||
.getString(R.string.common_error_unknown)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -84,7 +97,10 @@ class AudioContentViewModel(private val repository: AudioContentRepository) : Ba
|
||||
{
|
||||
_isLoading.value = false
|
||||
it.message?.let { message -> Logger.e(message) }
|
||||
_toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
|
||||
_toastLiveData.postValue(
|
||||
SodaLiveApplicationHolder.get()
|
||||
.getString(R.string.common_error_unknown)
|
||||
)
|
||||
if (onFailure != null) {
|
||||
onFailure()
|
||||
}
|
||||
@@ -99,4 +115,48 @@ class AudioContentViewModel(private val repository: AudioContentRepository) : Ba
|
||||
isLast = false
|
||||
_sort.postValue(sort)
|
||||
}
|
||||
|
||||
fun selectCategory(categoryId: Long, userId: Long) {
|
||||
isLast = false
|
||||
page = 1
|
||||
selectedCategoryId = categoryId
|
||||
getAudioContentList(userId = userId)
|
||||
}
|
||||
|
||||
fun getCategoryList(userId: Long) {
|
||||
compositeDisposable.add(
|
||||
repository.getCategoryList(
|
||||
creatorId = userId,
|
||||
token = "Bearer ${SharedPreferenceManager.token}",
|
||||
)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
{
|
||||
if (it.success && it.data != null) {
|
||||
_categoryListLiveData.value = it.data!!
|
||||
} else {
|
||||
if (it.message != null) {
|
||||
_toastLiveData.postValue(it.message)
|
||||
} else {
|
||||
_toastLiveData.postValue(
|
||||
SodaLiveApplicationHolder.get()
|
||||
.getString(R.string.common_error_unknown)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
_isLoading.value = false
|
||||
},
|
||||
{
|
||||
_isLoading.value = false
|
||||
it.message?.let { message -> Logger.e(message) }
|
||||
_toastLiveData.postValue(
|
||||
SodaLiveApplicationHolder.get()
|
||||
.getString(R.string.common_error_unknown)
|
||||
)
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
package kr.co.vividnext.sodalive.audio_content
|
||||
|
||||
import io.objectbox.annotation.Entity
|
||||
import io.objectbox.annotation.Id
|
||||
import androidx.annotation.Keep
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
|
||||
@Entity
|
||||
@Entity(tableName = "playback_tracking")
|
||||
@Keep
|
||||
data class PlaybackTracking(
|
||||
@Id
|
||||
@PrimaryKey(autoGenerate = true)
|
||||
var id: Long = 0,
|
||||
var contentId: Long,
|
||||
var totalDuration: Int,
|
||||
|
||||
+6
-14
@@ -1,29 +1,21 @@
|
||||
package kr.co.vividnext.sodalive.audio_content
|
||||
|
||||
import kr.co.vividnext.sodalive.common.ObjectBox
|
||||
import kr.co.vividnext.sodalive.audio_content.db.PlaybackTrackingDao
|
||||
|
||||
class PlaybackTrackingRepository(private val objectBox: ObjectBox) {
|
||||
class PlaybackTrackingRepository(private val dao: PlaybackTrackingDao) {
|
||||
fun savePlaybackTracking(data: PlaybackTracking): Long {
|
||||
return objectBox.playbackTrackingBox.put(data)
|
||||
return dao.insert(data)
|
||||
}
|
||||
|
||||
fun getPlaybackTracking(id: Long): PlaybackTracking? {
|
||||
val query = objectBox.playbackTrackingBox
|
||||
.query(PlaybackTracking_.id.equal(id))
|
||||
.build()
|
||||
|
||||
val playbackTracking = query.findFirst()
|
||||
query.close()
|
||||
return playbackTracking
|
||||
return dao.getById(id)
|
||||
}
|
||||
|
||||
fun getAllPlaybackTracking(): List<PlaybackTracking> {
|
||||
return objectBox
|
||||
.playbackTrackingBox
|
||||
.all
|
||||
return dao.getAll()
|
||||
}
|
||||
|
||||
fun removeAllPlaybackTracking() {
|
||||
objectBox.playbackTrackingBox.removeAll()
|
||||
dao.deleteAll()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
package kr.co.vividnext.sodalive.audio_content
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
|
||||
enum class PurchaseOption {
|
||||
@SerializedName("BOTH")
|
||||
BOTH,
|
||||
|
||||
@SerializedName("BUY_ONLY")
|
||||
BUY_ONLY,
|
||||
|
||||
@SerializedName("RENT_ONLY")
|
||||
RENT_ONLY,
|
||||
}
|
||||
+230
@@ -0,0 +1,230 @@
|
||||
package kr.co.vividnext.sodalive.audio_content.all
|
||||
|
||||
import android.content.Intent
|
||||
import android.graphics.Rect
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import kr.co.vividnext.sodalive.R
|
||||
import kr.co.vividnext.sodalive.audio_content.AudioContentViewModel
|
||||
import kr.co.vividnext.sodalive.audio_content.detail.AudioContentDetailActivity
|
||||
import kr.co.vividnext.sodalive.base.BaseActivity
|
||||
import kr.co.vividnext.sodalive.common.Constants
|
||||
import kr.co.vividnext.sodalive.common.GridSpacingItemDecoration
|
||||
import kr.co.vividnext.sodalive.common.LoadingDialog
|
||||
import kr.co.vividnext.sodalive.databinding.ActivityAudioContentAllBinding
|
||||
import kr.co.vividnext.sodalive.extensions.dpToPx
|
||||
import kr.co.vividnext.sodalive.home.HomeContentAdapter
|
||||
import kr.co.vividnext.sodalive.home.HomeContentThemeAdapter
|
||||
import org.koin.android.ext.android.inject
|
||||
|
||||
@androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class)
|
||||
class AudioContentAllActivity : BaseActivity<ActivityAudioContentAllBinding>(
|
||||
ActivityAudioContentAllBinding::inflate
|
||||
) {
|
||||
private val viewModel: AudioContentAllViewModel by inject()
|
||||
|
||||
private lateinit var loadingDialog: LoadingDialog
|
||||
private lateinit var adapter: HomeContentAdapter
|
||||
private lateinit var themeAdapter: HomeContentThemeAdapter
|
||||
|
||||
private var isFree: Boolean = false
|
||||
private var isPointOnly: Boolean = false
|
||||
private val allThemeLabel by lazy { getString(R.string.screen_home_theme_all) }
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
isFree = intent.getBooleanExtra(Constants.EXTRA_AUDIO_CONTENT_FREE, false)
|
||||
isPointOnly = intent.getBooleanExtra(Constants.EXTRA_AUDIO_CONTENT_POINT_ONLY, false)
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
bindData()
|
||||
viewModel.reset()
|
||||
viewModel.getThemeList(
|
||||
isFree = if (isFree) true else null,
|
||||
isPointAvailableOnly = if (isPointOnly) true else null
|
||||
)
|
||||
viewModel.loadAll(
|
||||
isFree = if (isFree) true else null,
|
||||
isPointAvailableOnly = if (isPointOnly) true else null
|
||||
)
|
||||
}
|
||||
|
||||
override fun setupView() {
|
||||
loadingDialog = LoadingDialog(this, layoutInflater)
|
||||
binding.toolbar.tvBack.text = when {
|
||||
isPointOnly -> getString(R.string.screen_audio_content_all_title_point_only)
|
||||
isFree -> getString(R.string.screen_audio_content_all_title_free)
|
||||
else -> getString(R.string.screen_audio_content_title)
|
||||
}
|
||||
binding.toolbar.tvBack.setOnClickListener { finish() }
|
||||
|
||||
binding.tvSortNewest.setOnClickListener {
|
||||
viewModel.selectSort(AudioContentViewModel.Sort.NEWEST)
|
||||
}
|
||||
|
||||
binding.tvSortPopularity.setOnClickListener {
|
||||
viewModel.selectSort(AudioContentViewModel.Sort.POPULARITY)
|
||||
}
|
||||
|
||||
setupTheme()
|
||||
setupRecycler()
|
||||
}
|
||||
|
||||
private fun setupTheme() {
|
||||
themeAdapter = HomeContentThemeAdapter(allThemeLabel) { selectedTheme ->
|
||||
adapter.addItems(emptyList())
|
||||
val theme = if (selectedTheme == allThemeLabel) "" else selectedTheme
|
||||
viewModel.selectTheme(theme, isFree = isFree, isPointOnly = isPointOnly)
|
||||
}
|
||||
|
||||
binding.rvTheme.layoutManager = LinearLayoutManager(
|
||||
this,
|
||||
LinearLayoutManager.HORIZONTAL,
|
||||
false
|
||||
)
|
||||
|
||||
binding.rvTheme.addItemDecoration(object : RecyclerView.ItemDecoration() {
|
||||
override fun getItemOffsets(
|
||||
outRect: Rect,
|
||||
view: View,
|
||||
parent: RecyclerView,
|
||||
state: RecyclerView.State
|
||||
) {
|
||||
super.getItemOffsets(outRect, view, parent, state)
|
||||
|
||||
when (parent.getChildAdapterPosition(view)) {
|
||||
0 -> {
|
||||
outRect.left = 0
|
||||
outRect.right = 4f.dpToPx().toInt()
|
||||
}
|
||||
|
||||
themeAdapter.itemCount - 1 -> {
|
||||
outRect.left = 4f.dpToPx().toInt()
|
||||
outRect.right = 0
|
||||
}
|
||||
|
||||
else -> {
|
||||
outRect.left = 4f.dpToPx().toInt()
|
||||
outRect.right = 4f.dpToPx().toInt()
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
binding.rvTheme.adapter = themeAdapter
|
||||
}
|
||||
|
||||
private fun setupRecycler() {
|
||||
// 아이템 정사각형 크기 계산: (screenWidth - (16*2) - 16) / 2
|
||||
// 아이템 정사각형 크기 계산: (screenWidth - (paddingHorizontal*2) - itemSpacing) / 2
|
||||
val itemSize = ((screenWidth - 16f.dpToPx() * 2 - 16f.dpToPx()) / 2f).toInt()
|
||||
|
||||
adapter = HomeContentAdapter(
|
||||
onClickItem = {
|
||||
startActivity(
|
||||
Intent(this, AudioContentDetailActivity::class.java).apply {
|
||||
putExtra(Constants.EXTRA_AUDIO_CONTENT_ID, it)
|
||||
}
|
||||
)
|
||||
},
|
||||
itemSquareSizePx = itemSize
|
||||
)
|
||||
|
||||
val spanCount = 2
|
||||
val spacingPx = 16f.dpToPx().toInt()
|
||||
binding.rvContent.layoutManager = GridLayoutManager(this, spanCount)
|
||||
binding.rvContent.addItemDecoration(
|
||||
GridSpacingItemDecoration(spanCount, spacingPx, true)
|
||||
)
|
||||
binding.rvContent.addOnScrollListener(object : RecyclerView.OnScrollListener() {
|
||||
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
|
||||
super.onScrolled(recyclerView, dx, dy)
|
||||
|
||||
val lastVisibleItemPosition = (recyclerView.layoutManager as LinearLayoutManager?)!!
|
||||
.findLastCompletelyVisibleItemPosition()
|
||||
val itemTotalCount = recyclerView.adapter!!.itemCount - 1
|
||||
|
||||
if (!recyclerView.canScrollVertically(1) && lastVisibleItemPosition == itemTotalCount) {
|
||||
viewModel.loadAll(
|
||||
isFree = if (isFree) true else null,
|
||||
isPointAvailableOnly = if (isPointOnly) true else null
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
binding.rvContent.adapter = adapter
|
||||
}
|
||||
|
||||
private fun bindData() {
|
||||
viewModel.isLoading.observe(this) {
|
||||
if (it) loadingDialog.show(screenWidth) else loadingDialog.dismiss()
|
||||
}
|
||||
|
||||
viewModel.toastLiveData.observe(this) {
|
||||
it?.let { Toast.makeText(applicationContext, it, Toast.LENGTH_LONG).show() }
|
||||
}
|
||||
|
||||
viewModel.themeListLiveData.observe(this) {
|
||||
val themes = mutableListOf(allThemeLabel)
|
||||
themes.addAll(it)
|
||||
themeAdapter.addItems(themes)
|
||||
}
|
||||
|
||||
viewModel.itemsLiveData.observe(this) { list ->
|
||||
if (adapter.itemCount > 0 || list.isNotEmpty()) {
|
||||
binding.rvContent.visibility = View.VISIBLE
|
||||
binding.llEmpty.visibility = View.GONE
|
||||
} else {
|
||||
binding.rvContent.visibility = View.GONE
|
||||
binding.llEmpty.visibility = View.VISIBLE
|
||||
}
|
||||
adapter.appendItems(list)
|
||||
}
|
||||
|
||||
viewModel.sortLiveData.observe(this) {
|
||||
deselectSort()
|
||||
selectSort(
|
||||
when (it) {
|
||||
AudioContentViewModel.Sort.POPULARITY -> {
|
||||
binding.tvSortPopularity
|
||||
}
|
||||
|
||||
else -> {
|
||||
binding.tvSortNewest
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun deselectSort() {
|
||||
val color = ContextCompat.getColor(
|
||||
applicationContext,
|
||||
R.color.color_88e2e2e2
|
||||
)
|
||||
|
||||
binding.tvSortNewest.setTextColor(color)
|
||||
binding.tvSortPopularity.setTextColor(color)
|
||||
}
|
||||
|
||||
private fun selectSort(view: TextView) {
|
||||
view.setTextColor(
|
||||
ContextCompat.getColor(
|
||||
applicationContext,
|
||||
R.color.color_e2e2e2
|
||||
)
|
||||
)
|
||||
|
||||
adapter.addItems(emptyList())
|
||||
viewModel.loadAll(
|
||||
isFree = if (isFree) true else null,
|
||||
isPointAvailableOnly = if (isPointOnly) true else null
|
||||
)
|
||||
}
|
||||
}
|
||||
+132
@@ -0,0 +1,132 @@
|
||||
package kr.co.vividnext.sodalive.audio_content.all
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import com.orhanobut.logger.Logger
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import kr.co.vividnext.sodalive.R
|
||||
import kr.co.vividnext.sodalive.audio_content.AudioContentRepository
|
||||
import kr.co.vividnext.sodalive.audio_content.AudioContentViewModel
|
||||
import kr.co.vividnext.sodalive.base.BaseViewModel
|
||||
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
|
||||
import kr.co.vividnext.sodalive.common.SodaLiveApplicationHolder
|
||||
import kr.co.vividnext.sodalive.home.AudioContentMainItem
|
||||
|
||||
class AudioContentAllViewModel(
|
||||
private val repository: AudioContentRepository
|
||||
) : BaseViewModel() {
|
||||
|
||||
private val _isLoading = MutableLiveData(false)
|
||||
val isLoading: LiveData<Boolean> get() = _isLoading
|
||||
|
||||
private val _toastLiveData = MutableLiveData<String?>()
|
||||
val toastLiveData: LiveData<String?> get() = _toastLiveData
|
||||
|
||||
private val _itemsLiveData = MutableLiveData<List<AudioContentMainItem>>()
|
||||
val itemsLiveData: LiveData<List<AudioContentMainItem>> get() = _itemsLiveData
|
||||
|
||||
private var _themeListLiveData = MutableLiveData<List<String>>()
|
||||
val themeListLiveData: LiveData<List<String>>
|
||||
get() = _themeListLiveData
|
||||
|
||||
private var _sortLiveData = MutableLiveData(AudioContentViewModel.Sort.NEWEST)
|
||||
val sortLiveData: LiveData<AudioContentViewModel.Sort>
|
||||
get() = _sortLiveData
|
||||
|
||||
private var page = 1
|
||||
private val size = 20
|
||||
private var isLast = false
|
||||
private var selectedTheme = ""
|
||||
|
||||
fun reset() {
|
||||
page = 1
|
||||
isLast = false
|
||||
}
|
||||
|
||||
fun getThemeList(
|
||||
isFree: Boolean? = null,
|
||||
isPointAvailableOnly: Boolean? = null
|
||||
) {
|
||||
val unknownError = SodaLiveApplicationHolder.get().getString(R.string.common_error_unknown)
|
||||
compositeDisposable.add(
|
||||
repository.getAudioContentActiveThemeList(
|
||||
isFree = isFree,
|
||||
isPointAvailableOnly = isPointAvailableOnly,
|
||||
token = "Bearer ${SharedPreferenceManager.token}"
|
||||
)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
{
|
||||
if (it.success && it.data != null) {
|
||||
_themeListLiveData.postValue(it.data)
|
||||
} else {
|
||||
if (it.message != null) {
|
||||
_toastLiveData.postValue(it.message)
|
||||
} else {
|
||||
_toastLiveData.postValue(unknownError)
|
||||
}
|
||||
}
|
||||
|
||||
_isLoading.value = false
|
||||
},
|
||||
{
|
||||
_isLoading.value = false
|
||||
it.message?.let { message -> Logger.e(message) }
|
||||
_toastLiveData.postValue(unknownError)
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
fun loadAll(
|
||||
isFree: Boolean? = null,
|
||||
isPointAvailableOnly: Boolean? = null
|
||||
) {
|
||||
if (_isLoading.value == true || isLast) return
|
||||
_isLoading.value = true
|
||||
val unknownError = SodaLiveApplicationHolder.get().getString(R.string.common_error_unknown)
|
||||
|
||||
compositeDisposable.add(
|
||||
repository.getAllAudioContents(
|
||||
page = page,
|
||||
size = size,
|
||||
isFree = isFree,
|
||||
isPointAvailableOnly = isPointAvailableOnly,
|
||||
sortType = _sortLiveData.value!!,
|
||||
theme = selectedTheme.ifBlank { null },
|
||||
token = "Bearer ${SharedPreferenceManager.token}"
|
||||
)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe({ response ->
|
||||
val list = response.data ?: emptyList()
|
||||
if (list.isNotEmpty()) {
|
||||
page += 1
|
||||
}
|
||||
if (list.size < size) {
|
||||
isLast = true
|
||||
}
|
||||
_itemsLiveData.postValue(list)
|
||||
_isLoading.value = false
|
||||
}, { t ->
|
||||
_isLoading.value = false
|
||||
_toastLiveData.postValue(t.message ?: unknownError)
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
fun selectTheme(theme: String, isFree: Boolean, isPointOnly: Boolean) {
|
||||
reset()
|
||||
selectedTheme = theme
|
||||
loadAll(isFree, isPointOnly)
|
||||
}
|
||||
|
||||
fun selectSort(sortType: AudioContentViewModel.Sort) {
|
||||
if (_sortLiveData.value != sortType) {
|
||||
reset()
|
||||
_sortLiveData.value = sortType
|
||||
}
|
||||
}
|
||||
}
|
||||
+195
@@ -0,0 +1,195 @@
|
||||
package kr.co.vividnext.sodalive.audio_content.all
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Intent
|
||||
import android.graphics.Rect
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.widget.Toast
|
||||
import androidx.annotation.OptIn
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import kr.co.vividnext.sodalive.R
|
||||
import kr.co.vividnext.sodalive.audio_content.detail.AudioContentDetailActivity
|
||||
import kr.co.vividnext.sodalive.base.BaseActivity
|
||||
import kr.co.vividnext.sodalive.common.Constants
|
||||
import kr.co.vividnext.sodalive.common.GridSpacingItemDecoration
|
||||
import kr.co.vividnext.sodalive.common.LoadingDialog
|
||||
import kr.co.vividnext.sodalive.databinding.ActivityAudioContentNewAllBinding
|
||||
import kr.co.vividnext.sodalive.extensions.dpToPx
|
||||
import kr.co.vividnext.sodalive.home.HomeContentThemeAdapter
|
||||
import kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelActivity
|
||||
import org.koin.android.ext.android.inject
|
||||
|
||||
@OptIn(UnstableApi::class)
|
||||
class AudioContentNewAllActivity : BaseActivity<ActivityAudioContentNewAllBinding>(
|
||||
ActivityAudioContentNewAllBinding::inflate
|
||||
) {
|
||||
private val viewModel: AudioContentNewAllViewModel by inject()
|
||||
|
||||
private lateinit var loadingDialog: LoadingDialog
|
||||
|
||||
private lateinit var newContentThemeAdapter: HomeContentThemeAdapter
|
||||
private lateinit var newContentAdapter: AudioContentNewAllAdapter
|
||||
private val allThemeLabel by lazy { getString(R.string.screen_home_theme_all) }
|
||||
|
||||
private var isFree: Boolean = false
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
isFree = intent.getBooleanExtra(Constants.EXTRA_AUDIO_CONTENT_FREE, false)
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
bindData()
|
||||
viewModel.getThemeList()
|
||||
viewModel.getNewContentList(isFree = isFree)
|
||||
}
|
||||
|
||||
override fun setupView() {
|
||||
loadingDialog = LoadingDialog(this, layoutInflater)
|
||||
binding.toolbar.tvBack.text = if (isFree) {
|
||||
getString(R.string.screen_audio_content_new_all_title_free)
|
||||
} else {
|
||||
getString(R.string.screen_audio_content_new_all_title)
|
||||
}
|
||||
binding.toolbar.tvBack.setOnClickListener { finish() }
|
||||
|
||||
binding.tvNotice.text = if (isFree) {
|
||||
getString(R.string.screen_audio_content_new_all_notice_free)
|
||||
} else {
|
||||
getString(R.string.screen_audio_content_new_all_notice)
|
||||
}
|
||||
|
||||
setupNewContentTheme()
|
||||
setupNewContent()
|
||||
}
|
||||
|
||||
@SuppressLint("NotifyDataSetChanged")
|
||||
private fun setupNewContentTheme() {
|
||||
newContentThemeAdapter = HomeContentThemeAdapter(allThemeLabel) { selectedTheme ->
|
||||
newContentAdapter.clear()
|
||||
newContentAdapter.notifyDataSetChanged()
|
||||
val theme = if (selectedTheme == allThemeLabel) "" else selectedTheme
|
||||
viewModel.selectTheme(theme, isFree = isFree)
|
||||
}
|
||||
|
||||
binding.rvNewContentTheme.layoutManager = LinearLayoutManager(
|
||||
this,
|
||||
LinearLayoutManager.HORIZONTAL,
|
||||
false
|
||||
)
|
||||
|
||||
binding.rvNewContentTheme.addItemDecoration(object : RecyclerView.ItemDecoration() {
|
||||
override fun getItemOffsets(
|
||||
outRect: Rect,
|
||||
view: View,
|
||||
parent: RecyclerView,
|
||||
state: RecyclerView.State
|
||||
) {
|
||||
super.getItemOffsets(outRect, view, parent, state)
|
||||
|
||||
when (parent.getChildAdapterPosition(view)) {
|
||||
0 -> {
|
||||
outRect.left = 0
|
||||
outRect.right = 4f.dpToPx().toInt()
|
||||
}
|
||||
|
||||
newContentThemeAdapter.itemCount - 1 -> {
|
||||
outRect.left = 4f.dpToPx().toInt()
|
||||
outRect.right = 0
|
||||
}
|
||||
|
||||
else -> {
|
||||
outRect.left = 4f.dpToPx().toInt()
|
||||
outRect.right = 4f.dpToPx().toInt()
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
binding.rvNewContentTheme.adapter = newContentThemeAdapter
|
||||
}
|
||||
|
||||
private fun setupNewContent() {
|
||||
// 아이템 정사각형 크기 계산: (screenWidth - (16*2) - 16) / 2
|
||||
// 아이템 정사각형 크기 계산: (screenWidth - (paddingHorizontal*2) - itemSpacing) / 2
|
||||
val itemSize = ((screenWidth - 16f.dpToPx() * 2 - 16f.dpToPx()) / 2f).toInt()
|
||||
newContentAdapter = AudioContentNewAllAdapter(
|
||||
itemWidth = itemSize,
|
||||
onClickItem = {
|
||||
startActivity(
|
||||
Intent(this, AudioContentDetailActivity::class.java).apply {
|
||||
putExtra(Constants.EXTRA_AUDIO_CONTENT_ID, it)
|
||||
}
|
||||
)
|
||||
},
|
||||
onClickCreator = {
|
||||
startActivity(
|
||||
CreatorChannelActivity.newIntent(this, it)
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
val spanCount = 2
|
||||
val spacingPx = 16f.dpToPx().toInt()
|
||||
binding.rvContent.layoutManager = GridLayoutManager(this, spanCount)
|
||||
binding.rvContent.addItemDecoration(
|
||||
GridSpacingItemDecoration(spanCount, spacingPx, true)
|
||||
)
|
||||
|
||||
binding.rvContent.addOnScrollListener(object : RecyclerView.OnScrollListener() {
|
||||
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
|
||||
super.onScrolled(recyclerView, dx, dy)
|
||||
|
||||
val lastVisibleItemPosition = (recyclerView.layoutManager as LinearLayoutManager?)!!
|
||||
.findLastCompletelyVisibleItemPosition()
|
||||
val itemTotalCount = recyclerView.adapter!!.itemCount - 1
|
||||
|
||||
// 스크롤이 끝에 도달했는지 확인
|
||||
if (!recyclerView.canScrollVertically(1) &&
|
||||
lastVisibleItemPosition == itemTotalCount
|
||||
) {
|
||||
viewModel.getNewContentList(isFree)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
binding.rvContent.adapter = newContentAdapter
|
||||
}
|
||||
|
||||
private fun bindData() {
|
||||
viewModel.isLoading.observe(this) {
|
||||
if (it) {
|
||||
loadingDialog.show(screenWidth)
|
||||
} else {
|
||||
loadingDialog.dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
viewModel.toastLiveData.observe(this) {
|
||||
it?.let { Toast.makeText(applicationContext, it, Toast.LENGTH_LONG).show() }
|
||||
}
|
||||
|
||||
viewModel.themeListLiveData.observe(this) {
|
||||
val themes = mutableListOf(allThemeLabel)
|
||||
themes.addAll(it)
|
||||
newContentThemeAdapter.addItems(themes)
|
||||
}
|
||||
|
||||
viewModel.newContentListLiveData.observe(this) {
|
||||
if (newContentAdapter.itemCount > 0 || it.isNotEmpty()) {
|
||||
binding.rvContent.visibility = View.VISIBLE
|
||||
binding.llNoItems.visibility = View.GONE
|
||||
} else {
|
||||
binding.rvContent.visibility = View.GONE
|
||||
binding.llNoItems.visibility = View.VISIBLE
|
||||
}
|
||||
newContentAdapter.addItems(it)
|
||||
}
|
||||
|
||||
viewModel.newContentTotalCountLiveData.observe(this) {
|
||||
binding.tvTotalCount.text = "$it"
|
||||
}
|
||||
}
|
||||
}
|
||||
+112
@@ -0,0 +1,112 @@
|
||||
package kr.co.vividnext.sodalive.audio_content.all
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import coil.load
|
||||
import coil.transform.CircleCropTransformation
|
||||
import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.load.resource.bitmap.CenterCrop
|
||||
import com.bumptech.glide.load.resource.bitmap.RoundedCorners
|
||||
import com.bumptech.glide.request.RequestOptions
|
||||
import kr.co.vividnext.sodalive.R
|
||||
import kr.co.vividnext.sodalive.audio_content.main.GetAudioContentMainItem
|
||||
import kr.co.vividnext.sodalive.databinding.ItemAudioContentNewAllBinding
|
||||
import kr.co.vividnext.sodalive.extensions.moneyFormat
|
||||
|
||||
class AudioContentNewAllAdapter(
|
||||
private val itemWidth: Int,
|
||||
private val onClickItem: (Long) -> Unit,
|
||||
private val onClickCreator: (Long) -> Unit,
|
||||
) : RecyclerView.Adapter<AudioContentNewAllAdapter.ViewHolder>() {
|
||||
|
||||
inner class ViewHolder(
|
||||
private val context: Context,
|
||||
private val binding: ItemAudioContentNewAllBinding,
|
||||
private val onClickItem: (Long) -> Unit,
|
||||
private val onClickCreator: (Long) -> Unit
|
||||
) : RecyclerView.ViewHolder(binding.root) {
|
||||
fun bind(item: GetAudioContentMainItem) {
|
||||
Glide
|
||||
.with(context)
|
||||
.load(item.coverImageUrl)
|
||||
.apply(
|
||||
RequestOptions().transform(
|
||||
CenterCrop(),
|
||||
RoundedCorners(8)
|
||||
)
|
||||
)
|
||||
.into(binding.ivAudioContentCoverImage)
|
||||
|
||||
val layoutParams =
|
||||
binding.ivAudioContentCoverImage.layoutParams as ConstraintLayout.LayoutParams
|
||||
layoutParams.width = itemWidth
|
||||
layoutParams.height = itemWidth
|
||||
binding.ivAudioContentCoverImage.layoutParams = layoutParams
|
||||
|
||||
binding.ivPoint.visibility = if (item.isPointAvailable) {
|
||||
View.VISIBLE
|
||||
} else {
|
||||
View.GONE
|
||||
}
|
||||
|
||||
binding.ivAudioContentCreator.load(item.creatorProfileImageUrl) {
|
||||
crossfade(true)
|
||||
placeholder(R.drawable.ic_place_holder)
|
||||
transformations(CircleCropTransformation())
|
||||
}
|
||||
|
||||
binding.tvAudioContentTitle.text = item.title
|
||||
binding.tvAudioContentCreatorNickname.text = item.creatorNickname
|
||||
|
||||
if (item.price > 0) {
|
||||
binding.ivCan.visibility = View.VISIBLE
|
||||
binding.tvCan.text = item.price.moneyFormat()
|
||||
} else {
|
||||
binding.ivCan.visibility = View.GONE
|
||||
binding.tvCan.text = context.getString(R.string.audio_content_price_free)
|
||||
}
|
||||
|
||||
binding.tvTime.text = item.duration
|
||||
|
||||
binding.ivAudioContentCreator.setOnClickListener { onClickCreator(item.creatorId) }
|
||||
binding.root.setOnClickListener { onClickItem(item.contentId) }
|
||||
}
|
||||
}
|
||||
|
||||
private val items = mutableListOf<GetAudioContentMainItem>()
|
||||
|
||||
override fun onCreateViewHolder(
|
||||
parent: ViewGroup,
|
||||
viewType: Int
|
||||
) = ViewHolder(
|
||||
parent.context,
|
||||
ItemAudioContentNewAllBinding.inflate(
|
||||
LayoutInflater.from(parent.context),
|
||||
parent,
|
||||
false
|
||||
),
|
||||
onClickItem = onClickItem,
|
||||
onClickCreator = onClickCreator
|
||||
)
|
||||
|
||||
override fun getItemCount() = items.size
|
||||
|
||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||
holder.bind(items[position])
|
||||
}
|
||||
|
||||
@SuppressLint("NotifyDataSetChanged")
|
||||
fun addItems(items: List<GetAudioContentMainItem>) {
|
||||
this.items.addAll(items)
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
fun clear() {
|
||||
this.items.clear()
|
||||
}
|
||||
}
|
||||
+124
@@ -0,0 +1,124 @@
|
||||
package kr.co.vividnext.sodalive.audio_content.all
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import com.orhanobut.logger.Logger
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import kr.co.vividnext.sodalive.audio_content.AudioContentRepository
|
||||
import kr.co.vividnext.sodalive.audio_content.main.GetAudioContentMainItem
|
||||
import kr.co.vividnext.sodalive.base.BaseViewModel
|
||||
import kr.co.vividnext.sodalive.common.SodaLiveApplicationHolder
|
||||
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
|
||||
import kr.co.vividnext.sodalive.R
|
||||
|
||||
class AudioContentNewAllViewModel(
|
||||
private val repository: AudioContentRepository
|
||||
) : BaseViewModel() {
|
||||
private val _toastLiveData = MutableLiveData<String?>()
|
||||
val toastLiveData: LiveData<String?>
|
||||
get() = _toastLiveData
|
||||
|
||||
private var _isLoading = MutableLiveData(false)
|
||||
val isLoading: LiveData<Boolean>
|
||||
get() = _isLoading
|
||||
|
||||
private var _themeListLiveData = MutableLiveData<List<String>>()
|
||||
val themeListLiveData: LiveData<List<String>>
|
||||
get() = _themeListLiveData
|
||||
|
||||
private var _newContentListLiveData = MutableLiveData<List<GetAudioContentMainItem>>()
|
||||
val newContentListLiveData: LiveData<List<GetAudioContentMainItem>>
|
||||
get() = _newContentListLiveData
|
||||
|
||||
private var _newContentTotalCountLiveData = MutableLiveData<Int>()
|
||||
val newContentTotalCountLiveData: LiveData<Int>
|
||||
get() = _newContentTotalCountLiveData
|
||||
|
||||
private var isLast = false
|
||||
private var page = 1
|
||||
private val size = 10
|
||||
private var selectedTheme = ""
|
||||
|
||||
fun getNewContentList(isFree: Boolean = false) {
|
||||
if (!_isLoading.value!! && !isLast) {
|
||||
_isLoading.value = true
|
||||
val unknownError = SodaLiveApplicationHolder.get().getString(R.string.common_error_unknown)
|
||||
|
||||
compositeDisposable.add(
|
||||
repository.getNewContentAllOfTheme(
|
||||
isFree = isFree,
|
||||
theme = selectedTheme,
|
||||
page = page,
|
||||
size = size,
|
||||
token = "Bearer ${SharedPreferenceManager.token}"
|
||||
)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
{
|
||||
if (it.success && it.data != null) {
|
||||
if (it.data.items.isNotEmpty()) {
|
||||
page += 1
|
||||
} else {
|
||||
isLast = true
|
||||
}
|
||||
|
||||
_newContentListLiveData.postValue(it.data.items)
|
||||
_newContentTotalCountLiveData.postValue(it.data.totalCount)
|
||||
} else {
|
||||
if (it.message != null) {
|
||||
_toastLiveData.postValue(it.message)
|
||||
} else {
|
||||
_toastLiveData.postValue(unknownError)
|
||||
}
|
||||
}
|
||||
|
||||
_isLoading.value = false
|
||||
},
|
||||
{
|
||||
_isLoading.value = false
|
||||
it.message?.let { message -> Logger.e(message) }
|
||||
_toastLiveData.postValue(unknownError)
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun getThemeList() {
|
||||
val unknownError = SodaLiveApplicationHolder.get().getString(R.string.common_error_unknown)
|
||||
compositeDisposable.add(
|
||||
repository.getNewContentThemeList(token = "Bearer ${SharedPreferenceManager.token}")
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
{
|
||||
if (it.success && it.data != null) {
|
||||
_themeListLiveData.postValue(it.data)
|
||||
} else {
|
||||
if (it.message != null) {
|
||||
_toastLiveData.postValue(it.message)
|
||||
} else {
|
||||
_toastLiveData.postValue(unknownError)
|
||||
}
|
||||
}
|
||||
|
||||
_isLoading.value = false
|
||||
},
|
||||
{
|
||||
_isLoading.value = false
|
||||
it.message?.let { message -> Logger.e(message) }
|
||||
_toastLiveData.postValue(unknownError)
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
fun selectTheme(theme: String, isFree: Boolean) {
|
||||
isLast = false
|
||||
page = 1
|
||||
selectedTheme = theme
|
||||
getNewContentList(isFree)
|
||||
}
|
||||
}
|
||||
+186
@@ -0,0 +1,186 @@
|
||||
package kr.co.vividnext.sodalive.audio_content.all
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Intent
|
||||
import android.graphics.Rect
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.widget.Toast
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import kr.co.vividnext.sodalive.R
|
||||
import kr.co.vividnext.sodalive.audio_content.detail.AudioContentDetailActivity
|
||||
import kr.co.vividnext.sodalive.audio_content.main.new_content.AudioContentMainNewContentThemeAdapter
|
||||
import kr.co.vividnext.sodalive.base.BaseActivity
|
||||
import kr.co.vividnext.sodalive.common.Constants
|
||||
import kr.co.vividnext.sodalive.common.LoadingDialog
|
||||
import kr.co.vividnext.sodalive.databinding.ActivityAudioContentRankingAllBinding
|
||||
import kr.co.vividnext.sodalive.extensions.dpToPx
|
||||
import org.koin.android.ext.android.inject
|
||||
|
||||
class AudioContentRankingAllActivity : BaseActivity<ActivityAudioContentRankingAllBinding>(
|
||||
ActivityAudioContentRankingAllBinding::inflate
|
||||
) {
|
||||
private val viewModel: AudioContentRankingAllViewModel by inject()
|
||||
|
||||
private lateinit var loadingDialog: LoadingDialog
|
||||
private lateinit var adapter: AudioContentRankingAllAdapter
|
||||
private lateinit var sortAdapter: AudioContentMainNewContentThemeAdapter
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
bindData()
|
||||
viewModel.getAudioContentRankingSortType()
|
||||
viewModel.getAudioContentRanking()
|
||||
}
|
||||
|
||||
override fun setupView() {
|
||||
loadingDialog = LoadingDialog(this, layoutInflater)
|
||||
binding.toolbar.tvBack.setOnClickListener { finish() }
|
||||
binding.toolbar.tvBack.text = getString(R.string.screen_audio_content_ranking_title)
|
||||
|
||||
adapter = AudioContentRankingAllAdapter {
|
||||
val intent = Intent(applicationContext, AudioContentDetailActivity::class.java)
|
||||
.apply {
|
||||
putExtra(Constants.EXTRA_AUDIO_CONTENT_ID, it)
|
||||
}
|
||||
startActivity(intent)
|
||||
}
|
||||
|
||||
binding.rvContentRanking.layoutManager = LinearLayoutManager(
|
||||
applicationContext,
|
||||
LinearLayoutManager.VERTICAL,
|
||||
false
|
||||
)
|
||||
|
||||
binding.rvContentRanking.addItemDecoration(object : RecyclerView.ItemDecoration() {
|
||||
override fun getItemOffsets(
|
||||
outRect: Rect,
|
||||
view: View,
|
||||
parent: RecyclerView,
|
||||
state: RecyclerView.State
|
||||
) {
|
||||
super.getItemOffsets(outRect, view, parent, state)
|
||||
|
||||
outRect.left = 13.3f.dpToPx().toInt()
|
||||
outRect.right = 13.3f.dpToPx().toInt()
|
||||
|
||||
when (parent.getChildAdapterPosition(view)) {
|
||||
0 -> {
|
||||
outRect.top = 0
|
||||
outRect.bottom = 10f.dpToPx().toInt()
|
||||
}
|
||||
|
||||
adapter.itemCount - 1 -> {
|
||||
outRect.top = 10f.dpToPx().toInt()
|
||||
outRect.bottom = 0
|
||||
}
|
||||
|
||||
else -> {
|
||||
outRect.top = 10f.dpToPx().toInt()
|
||||
outRect.bottom = 10f.dpToPx().toInt()
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
binding.rvContentRanking.addOnScrollListener(object : RecyclerView.OnScrollListener() {
|
||||
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
|
||||
super.onScrolled(recyclerView, dx, dy)
|
||||
|
||||
val lastVisibleItemPosition = (recyclerView.layoutManager as LinearLayoutManager?)!!
|
||||
.findLastCompletelyVisibleItemPosition()
|
||||
val itemTotalCount = recyclerView.adapter!!.itemCount - 1
|
||||
|
||||
// 스크롤이 끝에 도달했는지 확인
|
||||
if (!recyclerView.canScrollVertically(1) &&
|
||||
lastVisibleItemPosition == itemTotalCount
|
||||
) {
|
||||
viewModel.getAudioContentRanking()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
binding.rvContentRanking.adapter = adapter
|
||||
|
||||
setupContentRankingSortType()
|
||||
}
|
||||
|
||||
@SuppressLint("NotifyDataSetChanged")
|
||||
private fun setupContentRankingSortType() {
|
||||
sortAdapter = AudioContentMainNewContentThemeAdapter {
|
||||
adapter.items.clear()
|
||||
adapter.notifyDataSetChanged()
|
||||
viewModel.selectSort(sort = it)
|
||||
}
|
||||
|
||||
binding.rvContentRankingSort.layoutManager = LinearLayoutManager(
|
||||
this,
|
||||
LinearLayoutManager.HORIZONTAL,
|
||||
false
|
||||
)
|
||||
|
||||
binding.rvContentRankingSort.addItemDecoration(object : RecyclerView.ItemDecoration() {
|
||||
override fun getItemOffsets(
|
||||
outRect: Rect,
|
||||
view: View,
|
||||
parent: RecyclerView,
|
||||
state: RecyclerView.State
|
||||
) {
|
||||
super.getItemOffsets(outRect, view, parent, state)
|
||||
|
||||
when (parent.getChildAdapterPosition(view)) {
|
||||
0 -> {
|
||||
outRect.left = 0
|
||||
outRect.right = 4f.dpToPx().toInt()
|
||||
}
|
||||
|
||||
sortAdapter.itemCount - 1 -> {
|
||||
outRect.left = 4f.dpToPx().toInt()
|
||||
outRect.right = 0
|
||||
}
|
||||
|
||||
else -> {
|
||||
outRect.left = 4f.dpToPx().toInt()
|
||||
outRect.right = 4f.dpToPx().toInt()
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
binding.rvContentRankingSort.adapter = sortAdapter
|
||||
}
|
||||
|
||||
@SuppressLint("NotifyDataSetChanged")
|
||||
private fun bindData() {
|
||||
viewModel.isLoading.observe(this) {
|
||||
if (it) {
|
||||
loadingDialog.show(screenWidth)
|
||||
} else {
|
||||
loadingDialog.dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
viewModel.toastLiveData.observe(this) {
|
||||
it?.let { Toast.makeText(applicationContext, it, Toast.LENGTH_LONG).show() }
|
||||
}
|
||||
|
||||
viewModel.dateStringLiveData.observe(this) {
|
||||
binding.tvDate.text = it
|
||||
}
|
||||
|
||||
viewModel.contentRankingItemsLiveData.observe(this) {
|
||||
if (viewModel.page == 2) {
|
||||
adapter.items.clear()
|
||||
}
|
||||
|
||||
adapter.items.addAll(it)
|
||||
adapter.notifyDataSetChanged()
|
||||
}
|
||||
|
||||
viewModel.contentRankingSortListLiveData.observe(this) {
|
||||
sortAdapter.addItems(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
+90
@@ -0,0 +1,90 @@
|
||||
package kr.co.vividnext.sodalive.audio_content.all
|
||||
|
||||
import android.content.Context
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import coil.load
|
||||
import coil.transform.RoundedCornersTransformation
|
||||
import kr.co.vividnext.sodalive.R
|
||||
import kr.co.vividnext.sodalive.audio_content.main.GetAudioContentRankingItem
|
||||
import kr.co.vividnext.sodalive.databinding.ItemAudioContentRankingAllBinding
|
||||
import kr.co.vividnext.sodalive.extensions.dpToPx
|
||||
import kr.co.vividnext.sodalive.extensions.moneyFormat
|
||||
|
||||
class AudioContentRankingAllAdapter(
|
||||
private val onItemClick: (Long) -> Unit
|
||||
) : RecyclerView.Adapter<AudioContentRankingAllAdapter.ViewHolder>() {
|
||||
|
||||
inner class ViewHolder(
|
||||
private val context: Context,
|
||||
private val binding: ItemAudioContentRankingAllBinding
|
||||
) : RecyclerView.ViewHolder(binding.root) {
|
||||
fun bind(item: GetAudioContentRankingItem, index: Int) {
|
||||
binding.root.setOnClickListener { onItemClick(item.contentId) }
|
||||
binding.ivCover.load(item.coverImageUrl) {
|
||||
crossfade(true)
|
||||
placeholder(R.drawable.bg_placeholder)
|
||||
transformations(RoundedCornersTransformation(5.3f.dpToPx()))
|
||||
}
|
||||
|
||||
binding.tvPoint.visibility = if (item.isPointAvailable) {
|
||||
View.VISIBLE
|
||||
} else {
|
||||
View.GONE
|
||||
}
|
||||
|
||||
binding.tvTitle.text = item.title
|
||||
binding.tvRank.text = index.plus(1).toString()
|
||||
binding.tvTheme.text = item.themeStr
|
||||
binding.tvDuration.text = item.duration
|
||||
binding.tvNickname.text = item.creatorNickname
|
||||
|
||||
if (item.price < 1) {
|
||||
binding.tvPrice.text = context.getString(R.string.audio_content_price_free)
|
||||
binding.tvPrice.setTextColor(ContextCompat.getColor(context, R.color.white))
|
||||
binding.tvPrice.setCompoundDrawables(null, null, null, null)
|
||||
binding.tvPrice.setPadding(
|
||||
5.3f.dpToPx().toInt(),
|
||||
2.7f.dpToPx().toInt(),
|
||||
5.3f.dpToPx().toInt(),
|
||||
2.7f.dpToPx().toInt()
|
||||
)
|
||||
binding.tvPrice.setBackgroundResource(R.drawable.bg_round_corner_2_6_cf5c37)
|
||||
} else {
|
||||
binding.tvPrice.text = item.price.moneyFormat()
|
||||
binding.tvPrice.setTextColor(ContextCompat.getColor(context, R.color.color_909090))
|
||||
binding.tvPrice.setCompoundDrawablesWithIntrinsicBounds(
|
||||
R.drawable.ic_can,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
)
|
||||
binding.tvPrice.setPadding(0, 0, 0, 0)
|
||||
binding.tvPrice.setBackgroundResource(0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val items = mutableListOf<GetAudioContentRankingItem>()
|
||||
|
||||
override fun onCreateViewHolder(
|
||||
parent: ViewGroup,
|
||||
viewType: Int
|
||||
) = ViewHolder(
|
||||
parent.context,
|
||||
ItemAudioContentRankingAllBinding.inflate(
|
||||
LayoutInflater.from(parent.context),
|
||||
parent,
|
||||
false
|
||||
)
|
||||
)
|
||||
|
||||
override fun getItemCount() = items.size
|
||||
|
||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||
holder.bind(items[position], position)
|
||||
}
|
||||
}
|
||||
+122
@@ -0,0 +1,122 @@
|
||||
package kr.co.vividnext.sodalive.audio_content.all
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import com.orhanobut.logger.Logger
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import kr.co.vividnext.sodalive.R
|
||||
import kr.co.vividnext.sodalive.audio_content.AudioContentRepository
|
||||
import kr.co.vividnext.sodalive.audio_content.main.GetAudioContentRankingItem
|
||||
import kr.co.vividnext.sodalive.base.BaseViewModel
|
||||
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
|
||||
import kr.co.vividnext.sodalive.common.SodaLiveApplicationHolder
|
||||
|
||||
class AudioContentRankingAllViewModel(
|
||||
private val repository: AudioContentRepository
|
||||
) : BaseViewModel() {
|
||||
private val _toastLiveData = MutableLiveData<String?>()
|
||||
val toastLiveData: LiveData<String?>
|
||||
get() = _toastLiveData
|
||||
|
||||
private var _isLoading = MutableLiveData(false)
|
||||
val isLoading: LiveData<Boolean>
|
||||
get() = _isLoading
|
||||
|
||||
private var _dateStringLiveData = MutableLiveData<String>()
|
||||
val dateStringLiveData: LiveData<String>
|
||||
get() = _dateStringLiveData
|
||||
|
||||
private var _contentRankingSortListLiveData = MutableLiveData<List<String>>()
|
||||
val contentRankingSortListLiveData: LiveData<List<String>>
|
||||
get() = _contentRankingSortListLiveData
|
||||
|
||||
private var _contentRankingItemsLiveData = MutableLiveData<List<GetAudioContentRankingItem>>()
|
||||
val contentRankingItemsLiveData: LiveData<List<GetAudioContentRankingItem>>
|
||||
get() = _contentRankingItemsLiveData
|
||||
|
||||
var page = 1
|
||||
private var pageSize = 10
|
||||
private var isLast = false
|
||||
|
||||
private var selectedSort =
|
||||
SodaLiveApplicationHolder.get().getString(R.string.screen_home_sort_revenue)
|
||||
|
||||
fun getAudioContentRanking() {
|
||||
if (!_isLoading.value!! && !isLast && page <= 5) {
|
||||
_isLoading.value = true
|
||||
val unknownError =
|
||||
SodaLiveApplicationHolder.get().getString(R.string.common_error_unknown)
|
||||
compositeDisposable.add(
|
||||
repository.getContentRanking(
|
||||
page = page,
|
||||
size = pageSize,
|
||||
sortType = selectedSort,
|
||||
token = "Bearer ${SharedPreferenceManager.token}"
|
||||
)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
{
|
||||
if (it.success && it.data != null) {
|
||||
_isLoading.value = false
|
||||
_dateStringLiveData.value =
|
||||
"${it.data.startDate}~${it.data.endDate}"
|
||||
|
||||
if (it.data.items.isNotEmpty()) {
|
||||
page += 1
|
||||
_contentRankingItemsLiveData.value = it.data.items
|
||||
} else {
|
||||
isLast = true
|
||||
}
|
||||
} else {
|
||||
_isLoading.value = false
|
||||
if (it.message != null) {
|
||||
_toastLiveData.postValue(it.message)
|
||||
} else {
|
||||
_toastLiveData.postValue(unknownError)
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
it.message?.let { message -> Logger.e(message) }
|
||||
_toastLiveData.postValue(unknownError)
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun getAudioContentRankingSortType() {
|
||||
val unknownError = SodaLiveApplicationHolder.get().getString(R.string.common_error_unknown)
|
||||
compositeDisposable.add(
|
||||
repository.getContentRankingSortType(token = "Bearer ${SharedPreferenceManager.token}")
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
{
|
||||
if (it.success && it.data != null) {
|
||||
_contentRankingSortListLiveData.value = it.data!!
|
||||
} else {
|
||||
if (it.message != null) {
|
||||
_toastLiveData.postValue(it.message)
|
||||
} else {
|
||||
_toastLiveData.postValue(unknownError)
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
it.message?.let { message -> Logger.e(message) }
|
||||
_toastLiveData.postValue(unknownError)
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
fun selectSort(sort: String) {
|
||||
page = 1
|
||||
isLast = false
|
||||
selectedSort = sort
|
||||
getAudioContentRanking()
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
package kr.co.vividnext.sodalive.audio_content.all
|
||||
|
||||
import androidx.annotation.Keep
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import kr.co.vividnext.sodalive.audio_content.main.GetAudioContentMainItem
|
||||
|
||||
@Keep
|
||||
data class GetNewContentAllResponse(
|
||||
@SerializedName("totalCount") val totalCount: Int,
|
||||
@SerializedName("items") val items: List<GetAudioContentMainItem>
|
||||
)
|
||||
+177
@@ -0,0 +1,177 @@
|
||||
package kr.co.vividnext.sodalive.audio_content.all.by_theme
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.annotation.OptIn
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import kr.co.vividnext.sodalive.R
|
||||
import kr.co.vividnext.sodalive.audio_content.AudioContentViewModel
|
||||
import kr.co.vividnext.sodalive.audio_content.all.AudioContentNewAllAdapter
|
||||
import kr.co.vividnext.sodalive.audio_content.detail.AudioContentDetailActivity
|
||||
import kr.co.vividnext.sodalive.base.BaseActivity
|
||||
import kr.co.vividnext.sodalive.common.Constants
|
||||
import kr.co.vividnext.sodalive.common.GridSpacingItemDecoration
|
||||
import kr.co.vividnext.sodalive.common.LoadingDialog
|
||||
import kr.co.vividnext.sodalive.databinding.ActivityAudioContentAllByThemeBinding
|
||||
import kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelActivity
|
||||
import org.koin.android.ext.android.inject
|
||||
|
||||
class AudioContentAllByThemeActivity : BaseActivity<ActivityAudioContentAllByThemeBinding>(
|
||||
ActivityAudioContentAllByThemeBinding::inflate
|
||||
) {
|
||||
private val viewModel: AudioContentAllByThemeViewModel by inject()
|
||||
|
||||
private lateinit var loadingDialog: LoadingDialog
|
||||
private lateinit var adapter: AudioContentNewAllAdapter
|
||||
|
||||
private var themeId: Long = 0
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
themeId = intent.getLongExtra(Constants.EXTRA_THEME_ID, 0)
|
||||
|
||||
if (themeId <= 0) {
|
||||
Toast.makeText(
|
||||
applicationContext,
|
||||
getString(R.string.screen_audio_content_error_invalid_request_retry),
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
|
||||
finish()
|
||||
}
|
||||
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
bindData()
|
||||
viewModel.getContentList(themeId = themeId)
|
||||
}
|
||||
|
||||
@OptIn(UnstableApi::class)
|
||||
override fun setupView() {
|
||||
loadingDialog = LoadingDialog(this, layoutInflater)
|
||||
binding.toolbar.tvBack.setOnClickListener { finish() }
|
||||
|
||||
val spanCount = 3
|
||||
val spacing = 40
|
||||
adapter = AudioContentNewAllAdapter(
|
||||
itemWidth = (screenWidth - (spacing * (spanCount + 1))) / 3,
|
||||
onClickItem = {
|
||||
startActivity(
|
||||
Intent(this, AudioContentDetailActivity::class.java).apply {
|
||||
putExtra(Constants.EXTRA_AUDIO_CONTENT_ID, it)
|
||||
}
|
||||
)
|
||||
},
|
||||
onClickCreator = {
|
||||
startActivity(
|
||||
CreatorChannelActivity.newIntent(this, it)
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
binding.rvContentAll.layoutManager = GridLayoutManager(this, spanCount)
|
||||
binding.rvContentAll.addItemDecoration(GridSpacingItemDecoration(spanCount, spacing, true))
|
||||
|
||||
binding.rvContentAll.addOnScrollListener(object : RecyclerView.OnScrollListener() {
|
||||
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
|
||||
super.onScrolled(recyclerView, dx, dy)
|
||||
|
||||
val lastVisibleItemPosition = (recyclerView.layoutManager as LinearLayoutManager?)!!
|
||||
.findLastCompletelyVisibleItemPosition()
|
||||
val itemTotalCount = recyclerView.adapter!!.itemCount - 1
|
||||
|
||||
// 스크롤이 끝에 도달했는지 확인
|
||||
if (!recyclerView.canScrollVertically(1) &&
|
||||
lastVisibleItemPosition == itemTotalCount
|
||||
) {
|
||||
viewModel.getContentList(themeId = themeId)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
binding.rvContentAll.adapter = adapter
|
||||
|
||||
binding.tvSortNewest.setOnClickListener {
|
||||
viewModel.changeSort(AudioContentViewModel.Sort.NEWEST)
|
||||
}
|
||||
|
||||
binding.tvSortPriceLow.setOnClickListener {
|
||||
viewModel.changeSort(AudioContentViewModel.Sort.PRICE_LOW)
|
||||
}
|
||||
|
||||
binding.tvSortPriceHigh.setOnClickListener {
|
||||
viewModel.changeSort(AudioContentViewModel.Sort.PRICE_HIGH)
|
||||
}
|
||||
}
|
||||
|
||||
private fun bindData() {
|
||||
viewModel.toastLiveData.observe(this) {
|
||||
it?.let { Toast.makeText(applicationContext, it, Toast.LENGTH_LONG).show() }
|
||||
}
|
||||
|
||||
viewModel.isLoading.observe(this) {
|
||||
if (it) {
|
||||
loadingDialog.show(screenWidth, "")
|
||||
} else {
|
||||
loadingDialog.dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
viewModel.contentListLiveData.observe(this) {
|
||||
if (viewModel.page - 1 == 1) {
|
||||
adapter.clear()
|
||||
binding.rvContentAll.scrollToPosition(0)
|
||||
}
|
||||
|
||||
binding.tvTotalCount.text = "${it.totalCount}"
|
||||
binding.toolbar.tvBack.text = it.theme
|
||||
adapter.addItems(it.items)
|
||||
}
|
||||
|
||||
viewModel.sort.observe(this) {
|
||||
deselectSort()
|
||||
selectSort(
|
||||
when (it) {
|
||||
AudioContentViewModel.Sort.PRICE_HIGH -> {
|
||||
binding.tvSortPriceHigh
|
||||
}
|
||||
|
||||
AudioContentViewModel.Sort.PRICE_LOW -> {
|
||||
binding.tvSortPriceLow
|
||||
}
|
||||
|
||||
else -> {
|
||||
binding.tvSortNewest
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
viewModel.getContentList(themeId = themeId)
|
||||
}
|
||||
}
|
||||
|
||||
private fun deselectSort() {
|
||||
val color = ContextCompat.getColor(
|
||||
applicationContext,
|
||||
R.color.color_88e2e2e2
|
||||
)
|
||||
|
||||
binding.tvSortNewest.setTextColor(color)
|
||||
binding.tvSortPriceLow.setTextColor(color)
|
||||
binding.tvSortPriceHigh.setTextColor(color)
|
||||
}
|
||||
|
||||
private fun selectSort(view: TextView) {
|
||||
view.setTextColor(
|
||||
ContextCompat.getColor(
|
||||
applicationContext,
|
||||
R.color.color_e2e2e2
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
+92
@@ -0,0 +1,92 @@
|
||||
package kr.co.vividnext.sodalive.audio_content.all.by_theme
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import com.orhanobut.logger.Logger
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import kr.co.vividnext.sodalive.audio_content.AudioContentRepository
|
||||
import kr.co.vividnext.sodalive.audio_content.AudioContentViewModel
|
||||
import kr.co.vividnext.sodalive.base.BaseViewModel
|
||||
import kr.co.vividnext.sodalive.common.SodaLiveApplicationHolder
|
||||
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
|
||||
import kr.co.vividnext.sodalive.R
|
||||
|
||||
class AudioContentAllByThemeViewModel(
|
||||
private val repository: AudioContentRepository
|
||||
) : BaseViewModel() {
|
||||
private val _toastLiveData = MutableLiveData<String?>()
|
||||
val toastLiveData: LiveData<String?>
|
||||
get() = _toastLiveData
|
||||
|
||||
private var _isLoading = MutableLiveData(false)
|
||||
val isLoading: LiveData<Boolean>
|
||||
get() = _isLoading
|
||||
|
||||
private var _contentListLiveData = MutableLiveData<GetContentByThemeResponse>()
|
||||
val contentListLiveData: LiveData<GetContentByThemeResponse>
|
||||
get() = _contentListLiveData
|
||||
|
||||
private val _sort = MutableLiveData(AudioContentViewModel.Sort.NEWEST)
|
||||
val sort: LiveData<AudioContentViewModel.Sort>
|
||||
get() = _sort
|
||||
|
||||
private var isLast = false
|
||||
var page = 1
|
||||
private val size = 15
|
||||
|
||||
fun getContentList(themeId: Long) {
|
||||
if (!_isLoading.value!! && !isLast) {
|
||||
_isLoading.value = true
|
||||
|
||||
compositeDisposable.add(
|
||||
repository.getAudioContentByTheme(
|
||||
themeId = themeId,
|
||||
page = page,
|
||||
size = size,
|
||||
sort = _sort.value!!,
|
||||
token = "Bearer ${SharedPreferenceManager.token}"
|
||||
)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
{
|
||||
if (it.success && it.data != null) {
|
||||
if (it.data.items.isNotEmpty()) {
|
||||
page += 1
|
||||
_contentListLiveData.postValue(it.data!!)
|
||||
} else {
|
||||
isLast = true
|
||||
}
|
||||
} else {
|
||||
if (it.message != null) {
|
||||
_toastLiveData.postValue(it.message)
|
||||
} else {
|
||||
_toastLiveData.postValue(
|
||||
SodaLiveApplicationHolder.get()
|
||||
.getString(R.string.common_error_unknown)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
_isLoading.value = false
|
||||
},
|
||||
{
|
||||
_isLoading.value = false
|
||||
it.message?.let { message -> Logger.e(message) }
|
||||
_toastLiveData.postValue(
|
||||
SodaLiveApplicationHolder.get()
|
||||
.getString(R.string.common_error_unknown)
|
||||
)
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun changeSort(sort: AudioContentViewModel.Sort) {
|
||||
page = 1
|
||||
isLast = false
|
||||
_sort.postValue(sort)
|
||||
}
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
package kr.co.vividnext.sodalive.audio_content.all.by_theme
|
||||
|
||||
import androidx.annotation.Keep
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import kr.co.vividnext.sodalive.audio_content.main.GetAudioContentMainItem
|
||||
|
||||
@Keep
|
||||
data class GetContentByThemeResponse(
|
||||
@SerializedName("theme") val theme: String,
|
||||
@SerializedName("totalCount") val totalCount: Int,
|
||||
@SerializedName("items") val items: List<GetAudioContentMainItem>
|
||||
)
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
package kr.co.vividnext.sodalive.audio_content.box
|
||||
|
||||
import android.widget.LinearLayout
|
||||
import com.google.android.material.tabs.TabLayout
|
||||
import kr.co.vividnext.sodalive.R
|
||||
import kr.co.vividnext.sodalive.audio_content.order.AudioContentOrderListFragment
|
||||
import kr.co.vividnext.sodalive.audio_content.playlist.AudioContentPlaylistListFragment
|
||||
import kr.co.vividnext.sodalive.base.BaseActivity
|
||||
import kr.co.vividnext.sodalive.common.Constants
|
||||
import kr.co.vividnext.sodalive.databinding.ActivityAudioContentBoxBinding
|
||||
|
||||
class AudioContentBoxActivity : BaseActivity<ActivityAudioContentBoxBinding>(
|
||||
ActivityAudioContentBoxBinding::inflate
|
||||
) {
|
||||
private var startTabPosition = 0
|
||||
|
||||
override fun setupView() {
|
||||
startTabPosition = intent.getIntExtra(Constants.EXTRA_START_TAB_POSITION, 0)
|
||||
|
||||
setupToolbar()
|
||||
setupTabs()
|
||||
|
||||
binding.tabs.getTabAt(startTabPosition)?.select()
|
||||
|
||||
supportFragmentManager.beginTransaction()
|
||||
.replace(
|
||||
R.id.fl_container,
|
||||
if (startTabPosition == 1) {
|
||||
AudioContentPlaylistListFragment()
|
||||
} else {
|
||||
AudioContentOrderListFragment()
|
||||
}
|
||||
)
|
||||
.commit()
|
||||
}
|
||||
|
||||
private fun setupToolbar() {
|
||||
binding.toolbar.tvBack.text = getString(R.string.screen_audio_content_box_title)
|
||||
binding.toolbar.tvBack.setOnClickListener { finish() }
|
||||
}
|
||||
|
||||
private fun setupTabs() {
|
||||
val tabs = binding.tabs
|
||||
tabs.addTab(tabs.newTab().setText(R.string.screen_audio_content_box_tab_orders))
|
||||
tabs.addTab(tabs.newTab().setText(R.string.screen_audio_content_box_tab_playlists))
|
||||
|
||||
tabs.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener {
|
||||
override fun onTabSelected(tab: TabLayout.Tab) {
|
||||
tab.view.isSelected = true
|
||||
|
||||
when (tab.position) {
|
||||
1 -> supportFragmentManager.beginTransaction()
|
||||
.replace(R.id.fl_container, AudioContentPlaylistListFragment())
|
||||
.commit()
|
||||
|
||||
else -> supportFragmentManager.beginTransaction()
|
||||
.replace(R.id.fl_container, AudioContentOrderListFragment())
|
||||
.commit()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onTabUnselected(tab: TabLayout.Tab) {
|
||||
tab.view.isSelected = false
|
||||
}
|
||||
|
||||
override fun onTabReselected(tab: TabLayout.Tab) {
|
||||
}
|
||||
})
|
||||
|
||||
val tabStrip = tabs.getChildAt(0) as LinearLayout
|
||||
for (i in 0 until tabStrip.childCount) {
|
||||
val tab = tabStrip.getChildAt(i)
|
||||
val params = tab.layoutParams as LinearLayout.LayoutParams
|
||||
params.setMargins(12, 0, 12, 0)
|
||||
params.height = LinearLayout.LayoutParams.WRAP_CONTENT
|
||||
tab.layoutParams = params
|
||||
tab.minimumHeight = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
package kr.co.vividnext.sodalive.audio_content.category
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import kr.co.vividnext.sodalive.R
|
||||
import kr.co.vividnext.sodalive.databinding.ItemContentCategoryBinding
|
||||
|
||||
class AudioContentCategoryAdapter(
|
||||
private val allCategoryLabel: String,
|
||||
private val onClick: (Long) -> Unit
|
||||
) : RecyclerView.Adapter<AudioContentCategoryAdapter.ViewHolder>() {
|
||||
|
||||
private val items = mutableListOf<GetCategoryListResponse>()
|
||||
private var selectedCategory = ""
|
||||
|
||||
inner class ViewHolder(
|
||||
private val context: Context,
|
||||
private val binding: ItemContentCategoryBinding
|
||||
) : RecyclerView.ViewHolder(binding.root) {
|
||||
@SuppressLint("NotifyDataSetChanged")
|
||||
fun bind(item: GetCategoryListResponse) {
|
||||
if (
|
||||
item.category == selectedCategory ||
|
||||
(selectedCategory.isEmpty() && item.category == allCategoryLabel)
|
||||
) {
|
||||
binding.tvCategory.setBackgroundResource(
|
||||
R.drawable.bg_round_corner_16_7_transparent_3bb9f1
|
||||
)
|
||||
binding.tvCategory.setTextColor(
|
||||
ContextCompat.getColor(context, R.color.color_3bb9f1)
|
||||
)
|
||||
} else {
|
||||
binding.tvCategory.setBackgroundResource(
|
||||
R.drawable.bg_round_corner_16_7_transparent_777777
|
||||
)
|
||||
binding.tvCategory.setTextColor(
|
||||
ContextCompat.getColor(context, R.color.color_777777)
|
||||
)
|
||||
}
|
||||
|
||||
binding.tvCategory.text = item.category
|
||||
binding.root.setOnClickListener {
|
||||
onClick(item.categoryId)
|
||||
selectedCategory = item.category
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("NotifyDataSetChanged")
|
||||
fun addItems(items: List<GetCategoryListResponse>) {
|
||||
this.selectedCategory = ""
|
||||
this.items.clear()
|
||||
this.items.addAll(items)
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ViewHolder(
|
||||
parent.context,
|
||||
ItemContentCategoryBinding.inflate(
|
||||
LayoutInflater.from(parent.context),
|
||||
parent,
|
||||
false
|
||||
)
|
||||
)
|
||||
|
||||
override fun getItemCount() = items.size
|
||||
|
||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||
holder.bind(items[position])
|
||||
}
|
||||
|
||||
fun clear() {
|
||||
this.items.clear()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package kr.co.vividnext.sodalive.audio_content.category
|
||||
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import kr.co.vividnext.sodalive.common.ApiResponse
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.Header
|
||||
import retrofit2.http.Query
|
||||
|
||||
interface CategoryApi {
|
||||
@GET("/category")
|
||||
fun getCategoryList(
|
||||
@Query("creatorId") creatorId: Long,
|
||||
@Header("Authorization") authHeader: String
|
||||
): Single<ApiResponse<List<GetCategoryListResponse>>>
|
||||
}
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
package kr.co.vividnext.sodalive.audio_content.category
|
||||
|
||||
import androidx.annotation.Keep
|
||||
import com.google.gson.annotations.SerializedName
|
||||
|
||||
@Keep
|
||||
data class GetCategoryListResponse(
|
||||
@SerializedName("categoryId") val categoryId: Long,
|
||||
@SerializedName("category") val category: String
|
||||
)
|
||||
+35
-13
@@ -19,7 +19,8 @@ class AudioContentCommentAdapter(
|
||||
private val creatorId: Long,
|
||||
private val modifyComment: (Long, String) -> Unit,
|
||||
private val onClickDelete: (Long) -> Unit,
|
||||
private val onItemClick: (GetAudioContentCommentListItem) -> Unit
|
||||
private val onItemClick: (GetAudioContentCommentListItem) -> Unit,
|
||||
private val onClickProfile: (Long) -> Unit
|
||||
) : RecyclerView.Adapter<AudioContentCommentAdapter.ViewHolder>() {
|
||||
|
||||
var items = mutableSetOf<GetAudioContentCommentListItem>()
|
||||
@@ -30,12 +31,26 @@ class AudioContentCommentAdapter(
|
||||
) : RecyclerView.ViewHolder(binding.root) {
|
||||
|
||||
fun bind(item: GetAudioContentCommentListItem) {
|
||||
binding.rlCommentModify.visibility = View.GONE
|
||||
binding.tvComment.visibility = View.VISIBLE
|
||||
binding.tvSecret.visibility = if (item.isSecret) {
|
||||
View.VISIBLE
|
||||
} else {
|
||||
View.GONE
|
||||
}
|
||||
|
||||
binding.ivCommentProfile.load(item.profileUrl) {
|
||||
crossfade(true)
|
||||
placeholder(R.drawable.bg_placeholder)
|
||||
transformations(CircleCropTransformation())
|
||||
}
|
||||
|
||||
binding.ivCommentProfile.setOnClickListener {
|
||||
if (SharedPreferenceManager.userId != item.writerId) {
|
||||
onClickProfile(item.writerId)
|
||||
}
|
||||
}
|
||||
|
||||
val tvCommentLayoutParams = binding.tvComment.layoutParams as LinearLayout.LayoutParams
|
||||
val can = item.donationCan
|
||||
if (can > 0) {
|
||||
@@ -44,27 +59,27 @@ class AudioContentCommentAdapter(
|
||||
binding.tvDonationCan.text = can.moneyFormat()
|
||||
binding.llDonationCan.setBackgroundResource(
|
||||
when {
|
||||
can >= 100000 -> {
|
||||
can >= 10000 -> {
|
||||
R.drawable.bg_round_corner_10_7_973a3a
|
||||
}
|
||||
|
||||
can >= 50000 -> {
|
||||
can >= 5000 -> {
|
||||
R.drawable.bg_round_corner_10_7_d85e37
|
||||
}
|
||||
|
||||
can >= 10000 -> {
|
||||
can >= 1000 -> {
|
||||
R.drawable.bg_round_corner_10_7_d38c38
|
||||
}
|
||||
|
||||
can >= 5000 -> {
|
||||
can >= 500 -> {
|
||||
R.drawable.bg_round_corner_10_7_59548f
|
||||
}
|
||||
|
||||
can >= 1000 -> {
|
||||
can >= 100 -> {
|
||||
R.drawable.bg_round_corner_10_7_4d6aa4
|
||||
}
|
||||
|
||||
can >= 500 -> {
|
||||
can >= 50 -> {
|
||||
R.drawable.bg_round_corner_10_7_2d7390
|
||||
}
|
||||
|
||||
@@ -84,9 +99,12 @@ class AudioContentCommentAdapter(
|
||||
binding.tvCommentNickname.text = item.nickname
|
||||
|
||||
binding.tvWriteReply.text = if (item.replyCount > 0) {
|
||||
"답글 ${item.replyCount}개"
|
||||
context.getString(
|
||||
R.string.audio_content_comment_reply_count_format,
|
||||
item.replyCount
|
||||
)
|
||||
} else {
|
||||
"답글 쓰기"
|
||||
context.getString(R.string.audio_content_comment_write_reply)
|
||||
}
|
||||
|
||||
if (
|
||||
@@ -99,12 +117,16 @@ class AudioContentCommentAdapter(
|
||||
showOptionMenu(
|
||||
context,
|
||||
binding.ivMenu,
|
||||
commentId = item.id,
|
||||
writerId = item.writerId,
|
||||
creatorId = creatorId,
|
||||
onClickModify = {
|
||||
binding.rlCommentModify.visibility = View.VISIBLE
|
||||
binding.tvComment.visibility = View.GONE
|
||||
},
|
||||
onClickDelete = {
|
||||
binding.rlCommentModify.visibility = View.GONE
|
||||
binding.tvComment.visibility = View.VISIBLE
|
||||
onClickDelete(item.id)
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -141,10 +163,10 @@ class AudioContentCommentAdapter(
|
||||
private fun showOptionMenu(
|
||||
context: Context,
|
||||
v: View,
|
||||
commentId: Long,
|
||||
writerId: Long,
|
||||
creatorId: Long,
|
||||
onClickModify: () -> Unit
|
||||
onClickModify: () -> Unit,
|
||||
onClickDelete: () -> Unit
|
||||
) {
|
||||
val popup = PopupMenu(context, v)
|
||||
val inflater = popup.menuInflater
|
||||
@@ -162,7 +184,7 @@ class AudioContentCommentAdapter(
|
||||
}
|
||||
|
||||
R.id.menu_review_delete -> {
|
||||
onClickDelete(commentId)
|
||||
onClickDelete()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+4
-2
@@ -14,7 +14,8 @@ import kr.co.vividnext.sodalive.databinding.DialogAudioContentCommentBinding
|
||||
|
||||
class AudioContentCommentFragment(
|
||||
private val creatorId: Long,
|
||||
private val audioContentId: Long
|
||||
private val audioContentId: Long,
|
||||
private val isShowSecret: Boolean
|
||||
) : BottomSheetDialogFragment() {
|
||||
|
||||
private lateinit var binding: DialogAudioContentCommentBinding
|
||||
@@ -50,7 +51,8 @@ class AudioContentCommentFragment(
|
||||
val commentListFragmentTag = "COMMENT_LIST_FRAGMENT"
|
||||
val commentListFragment = AudioContentCommentListFragment.newInstance(
|
||||
creatorId = creatorId,
|
||||
audioContentId = audioContentId
|
||||
audioContentId = audioContentId,
|
||||
isShowSecret = isShowSecret
|
||||
)
|
||||
val fragmentTransaction = childFragmentManager.beginTransaction()
|
||||
fragmentTransaction.add(R.id.fl_container, commentListFragment, commentListFragmentTag)
|
||||
|
||||
+34
-6
@@ -20,6 +20,7 @@ import kr.co.vividnext.sodalive.common.Constants
|
||||
import kr.co.vividnext.sodalive.common.LoadingDialog
|
||||
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
|
||||
import kr.co.vividnext.sodalive.databinding.FragmentAudioContentCommentListBinding
|
||||
import kr.co.vividnext.sodalive.dialog.MemberProfileDialog
|
||||
import kr.co.vividnext.sodalive.extensions.dpToPx
|
||||
import org.koin.android.ext.android.inject
|
||||
|
||||
@@ -35,6 +36,7 @@ class AudioContentCommentListFragment : BaseFragment<FragmentAudioContentComment
|
||||
|
||||
private var creatorId: Long = 0
|
||||
private var audioContentId: Long = 0
|
||||
private var isShowSecret: Boolean = false
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
@@ -43,6 +45,7 @@ class AudioContentCommentListFragment : BaseFragment<FragmentAudioContentComment
|
||||
): View? {
|
||||
creatorId = arguments?.getLong(Constants.EXTRA_AUDIO_CONTENT_CREATOR_ID) ?: 0
|
||||
audioContentId = arguments?.getLong(Constants.EXTRA_AUDIO_CONTENT_ID) ?: 0
|
||||
isShowSecret = arguments?.getBoolean(Constants.EXTRA_IS_SHOW_SECRET) ?: false
|
||||
return super.onCreateView(inflater, container, savedInstanceState)
|
||||
}
|
||||
|
||||
@@ -73,8 +76,20 @@ class AudioContentCommentListFragment : BaseFragment<FragmentAudioContentComment
|
||||
binding.ivCommentSend.setOnClickListener {
|
||||
hideKeyboard()
|
||||
val comment = binding.etComment.text.toString()
|
||||
val isSecret = binding.tvSecret.isSelected
|
||||
viewModel.registerComment(audioContentId, comment, isSecret)
|
||||
|
||||
binding.etComment.setText("")
|
||||
viewModel.registerComment(audioContentId, comment)
|
||||
binding.tvSecret.isSelected = false
|
||||
}
|
||||
|
||||
if (isShowSecret) {
|
||||
binding.tvSecret.visibility = View.VISIBLE
|
||||
binding.tvSecret.setOnClickListener {
|
||||
binding.tvSecret.isSelected = !binding.tvSecret.isSelected
|
||||
}
|
||||
} else {
|
||||
binding.tvSecret.visibility = View.GONE
|
||||
}
|
||||
|
||||
adapter = AudioContentCommentAdapter(
|
||||
@@ -91,9 +106,9 @@ class AudioContentCommentListFragment : BaseFragment<FragmentAudioContentComment
|
||||
SodaDialog(
|
||||
activity = requireActivity(),
|
||||
layoutInflater = layoutInflater,
|
||||
title = "댓글 삭제",
|
||||
desc = "삭제하시겠습니까?",
|
||||
confirmButtonTitle = "삭제",
|
||||
title = getString(R.string.audio_content_comment_delete_title),
|
||||
desc = getString(R.string.audio_content_comment_delete_message),
|
||||
confirmButtonTitle = getString(R.string.screen_audio_content_detail_delete),
|
||||
confirmButtonClick = {
|
||||
viewModel.modifyComment(
|
||||
commentId = it,
|
||||
@@ -101,12 +116,20 @@ class AudioContentCommentListFragment : BaseFragment<FragmentAudioContentComment
|
||||
isActive = false
|
||||
)
|
||||
},
|
||||
cancelButtonTitle = "취소",
|
||||
cancelButtonTitle = getString(R.string.cancel),
|
||||
cancelButtonClick = {}
|
||||
).show(screenWidth)
|
||||
},
|
||||
onItemClick = {
|
||||
(parentFragment as AudioContentCommentFragment).onClickComment(it)
|
||||
},
|
||||
onClickProfile = {
|
||||
MemberProfileDialog(
|
||||
activity = requireActivity(),
|
||||
layoutInflater = layoutInflater,
|
||||
memberId = it,
|
||||
screenWidth = screenWidth
|
||||
).show()
|
||||
}
|
||||
)
|
||||
|
||||
@@ -202,10 +225,15 @@ class AudioContentCommentListFragment : BaseFragment<FragmentAudioContentComment
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun newInstance(creatorId: Long, audioContentId: Long): AudioContentCommentListFragment {
|
||||
fun newInstance(
|
||||
creatorId: Long,
|
||||
audioContentId: Long,
|
||||
isShowSecret: Boolean
|
||||
): AudioContentCommentListFragment {
|
||||
val args = Bundle()
|
||||
args.putLong(Constants.EXTRA_AUDIO_CONTENT_CREATOR_ID, creatorId)
|
||||
args.putLong(Constants.EXTRA_AUDIO_CONTENT_ID, audioContentId)
|
||||
args.putBoolean(Constants.EXTRA_IS_SHOW_SECRET, isShowSecret)
|
||||
|
||||
val fragment = AudioContentCommentListFragment()
|
||||
fragment.arguments = args
|
||||
|
||||
+23
-13
@@ -6,7 +6,9 @@ import com.orhanobut.logger.Logger
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import kr.co.vividnext.sodalive.base.BaseViewModel
|
||||
import kr.co.vividnext.sodalive.common.SodaLiveApplicationHolder
|
||||
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
|
||||
import kr.co.vividnext.sodalive.R
|
||||
|
||||
class AudioContentCommentListViewModel(
|
||||
private val repository: AudioContentCommentRepository
|
||||
@@ -27,6 +29,17 @@ class AudioContentCommentListViewModel(
|
||||
val totalCommentCount: LiveData<Int>
|
||||
get() = _totalCommentCount
|
||||
|
||||
private val unknownErrorMessage: String
|
||||
get() = SodaLiveApplicationHolder.get().getString(R.string.common_error_unknown)
|
||||
|
||||
private val noChangeMessage: String
|
||||
get() = SodaLiveApplicationHolder.get().getString(R.string.audio_content_comment_no_change)
|
||||
|
||||
private val inputRequiredMessage: String
|
||||
get() = SodaLiveApplicationHolder.get().getString(
|
||||
R.string.audio_content_comment_input_required
|
||||
)
|
||||
|
||||
var page = 1
|
||||
private var isLast = false
|
||||
private val size = 10
|
||||
@@ -59,9 +72,7 @@ class AudioContentCommentListViewModel(
|
||||
if (it.message != null) {
|
||||
_toastLiveData.postValue(it.message)
|
||||
} else {
|
||||
_toastLiveData.postValue(
|
||||
"알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
|
||||
)
|
||||
_toastLiveData.postValue(unknownErrorMessage)
|
||||
}
|
||||
|
||||
if (onFailure != null) {
|
||||
@@ -74,7 +85,7 @@ class AudioContentCommentListViewModel(
|
||||
{
|
||||
_isLoading.value = false
|
||||
it.message?.let { message -> Logger.e(message) }
|
||||
_toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
|
||||
_toastLiveData.postValue(unknownErrorMessage)
|
||||
if (onFailure != null) {
|
||||
onFailure()
|
||||
}
|
||||
@@ -84,7 +95,7 @@ class AudioContentCommentListViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
fun registerComment(contentId: Long, comment: String) {
|
||||
fun registerComment(contentId: Long, comment: String, isSecret: Boolean) {
|
||||
if (!_isLoading.value!!) {
|
||||
_isLoading.value = true
|
||||
}
|
||||
@@ -93,6 +104,7 @@ class AudioContentCommentListViewModel(
|
||||
repository.registerComment(
|
||||
contentId = contentId,
|
||||
comment = comment,
|
||||
isSecret = isSecret,
|
||||
token = "Bearer ${SharedPreferenceManager.token}"
|
||||
)
|
||||
.subscribeOn(Schedulers.io())
|
||||
@@ -109,16 +121,14 @@ class AudioContentCommentListViewModel(
|
||||
if (it.message != null) {
|
||||
_toastLiveData.postValue(it.message)
|
||||
} else {
|
||||
_toastLiveData.postValue(
|
||||
"알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
|
||||
)
|
||||
_toastLiveData.postValue(unknownErrorMessage)
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
_isLoading.value = false
|
||||
it.message?.let { message -> Logger.e(message) }
|
||||
_toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
|
||||
_toastLiveData.postValue(unknownErrorMessage)
|
||||
}
|
||||
)
|
||||
)
|
||||
@@ -131,12 +141,12 @@ class AudioContentCommentListViewModel(
|
||||
isActive: Boolean? = null
|
||||
) {
|
||||
if (comment == null && isActive == null) {
|
||||
_toastLiveData.postValue("변경사항이 없습니다.")
|
||||
_toastLiveData.postValue(noChangeMessage)
|
||||
return
|
||||
}
|
||||
|
||||
if (comment != null && comment.isBlank()) {
|
||||
_toastLiveData.postValue("내용을 입력하세요")
|
||||
_toastLiveData.postValue(inputRequiredMessage)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -168,14 +178,14 @@ class AudioContentCommentListViewModel(
|
||||
isLast = false
|
||||
getCommentList(audioContentId)
|
||||
} else {
|
||||
val message = it.message ?: "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
|
||||
val message = it.message ?: unknownErrorMessage
|
||||
_toastLiveData.postValue(message)
|
||||
}
|
||||
},
|
||||
{
|
||||
_isLoading.value = false
|
||||
it.message?.let { message -> Logger.e(message) }
|
||||
_toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
|
||||
_toastLiveData.postValue(unknownErrorMessage)
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
+8
-6
@@ -124,27 +124,27 @@ class AudioContentCommentReplyHeaderViewHolder(
|
||||
binding.tvDonationCan.text = can.moneyFormat()
|
||||
binding.llDonationCan.setBackgroundResource(
|
||||
when {
|
||||
can >= 100000 -> {
|
||||
can >= 10000 -> {
|
||||
R.drawable.bg_round_corner_10_7_973a3a
|
||||
}
|
||||
|
||||
can >= 50000 -> {
|
||||
can >= 5000 -> {
|
||||
R.drawable.bg_round_corner_10_7_d85e37
|
||||
}
|
||||
|
||||
can >= 10000 -> {
|
||||
can >= 1000 -> {
|
||||
R.drawable.bg_round_corner_10_7_d38c38
|
||||
}
|
||||
|
||||
can >= 5000 -> {
|
||||
can >= 500 -> {
|
||||
R.drawable.bg_round_corner_10_7_59548f
|
||||
}
|
||||
|
||||
can >= 1000 -> {
|
||||
can >= 100 -> {
|
||||
R.drawable.bg_round_corner_10_7_4d6aa4
|
||||
}
|
||||
|
||||
can >= 500 -> {
|
||||
can >= 50 -> {
|
||||
R.drawable.bg_round_corner_10_7_2d7390
|
||||
}
|
||||
|
||||
@@ -179,6 +179,8 @@ class AudioContentCommentReplyItemViewHolder(
|
||||
) : AudioContentCommentReplyViewHolder(binding) {
|
||||
|
||||
override fun bind(item: GetAudioContentCommentListItem) {
|
||||
binding.rlCommentModify.visibility = View.GONE
|
||||
binding.tvComment.visibility = View.VISIBLE
|
||||
binding.ivCommentProfile.load(item.profileUrl) {
|
||||
crossfade(true)
|
||||
placeholder(R.drawable.ic_place_holder)
|
||||
|
||||
+4
-4
@@ -112,9 +112,9 @@ class AudioContentCommentReplyFragment : BaseFragment<FragmentAudioContentCommen
|
||||
SodaDialog(
|
||||
activity = requireActivity(),
|
||||
layoutInflater = layoutInflater,
|
||||
title = "댓글 삭제",
|
||||
desc = "삭제하시겠습니까?",
|
||||
confirmButtonTitle = "삭제",
|
||||
title = getString(R.string.audio_content_comment_delete_title),
|
||||
desc = getString(R.string.audio_content_comment_delete_message),
|
||||
confirmButtonTitle = getString(R.string.screen_audio_content_detail_delete),
|
||||
confirmButtonClick = {
|
||||
viewModel.modifyComment(
|
||||
commentId = it,
|
||||
@@ -122,7 +122,7 @@ class AudioContentCommentReplyFragment : BaseFragment<FragmentAudioContentCommen
|
||||
isActive = false
|
||||
)
|
||||
},
|
||||
cancelButtonTitle = "취소",
|
||||
cancelButtonTitle = getString(R.string.cancel),
|
||||
cancelButtonClick = {}
|
||||
).show(screenWidth)
|
||||
}
|
||||
|
||||
+21
-12
@@ -6,7 +6,9 @@ import com.orhanobut.logger.Logger
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import kr.co.vividnext.sodalive.base.BaseViewModel
|
||||
import kr.co.vividnext.sodalive.common.SodaLiveApplicationHolder
|
||||
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
|
||||
import kr.co.vividnext.sodalive.R
|
||||
|
||||
class AudioContentCommentReplyViewModel(
|
||||
private val repository: AudioContentCommentRepository
|
||||
@@ -23,6 +25,17 @@ class AudioContentCommentReplyViewModel(
|
||||
val commentList: LiveData<List<GetAudioContentCommentListItem>>
|
||||
get() = _commentList
|
||||
|
||||
private val unknownErrorMessage: String
|
||||
get() = SodaLiveApplicationHolder.get().getString(R.string.common_error_unknown)
|
||||
|
||||
private val noChangeMessage: String
|
||||
get() = SodaLiveApplicationHolder.get().getString(R.string.audio_content_comment_no_change)
|
||||
|
||||
private val inputRequiredMessage: String
|
||||
get() = SodaLiveApplicationHolder.get().getString(
|
||||
R.string.audio_content_comment_input_required
|
||||
)
|
||||
|
||||
var page = 1
|
||||
private var isLast = false
|
||||
private val size = 10
|
||||
@@ -53,9 +66,7 @@ class AudioContentCommentReplyViewModel(
|
||||
if (it.message != null) {
|
||||
_toastLiveData.postValue(it.message)
|
||||
} else {
|
||||
_toastLiveData.postValue(
|
||||
"알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
|
||||
)
|
||||
_toastLiveData.postValue(unknownErrorMessage)
|
||||
}
|
||||
|
||||
if (onFailure != null) {
|
||||
@@ -68,7 +79,7 @@ class AudioContentCommentReplyViewModel(
|
||||
{
|
||||
_isLoading.value = false
|
||||
it.message?.let { message -> Logger.e(message) }
|
||||
_toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
|
||||
_toastLiveData.postValue(unknownErrorMessage)
|
||||
if (onFailure != null) {
|
||||
onFailure()
|
||||
}
|
||||
@@ -104,16 +115,14 @@ class AudioContentCommentReplyViewModel(
|
||||
if (it.message != null) {
|
||||
_toastLiveData.postValue(it.message)
|
||||
} else {
|
||||
_toastLiveData.postValue(
|
||||
"알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
|
||||
)
|
||||
_toastLiveData.postValue(unknownErrorMessage)
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
_isLoading.value = false
|
||||
it.message?.let { message -> Logger.e(message) }
|
||||
_toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
|
||||
_toastLiveData.postValue(unknownErrorMessage)
|
||||
}
|
||||
)
|
||||
)
|
||||
@@ -126,12 +135,12 @@ class AudioContentCommentReplyViewModel(
|
||||
isActive: Boolean? = null
|
||||
) {
|
||||
if (comment == null && isActive == null) {
|
||||
_toastLiveData.postValue("변경사항이 없습니다.")
|
||||
_toastLiveData.postValue(noChangeMessage)
|
||||
return
|
||||
}
|
||||
|
||||
if (comment != null && comment.isBlank()) {
|
||||
_toastLiveData.postValue("내용을 입력하세요")
|
||||
_toastLiveData.postValue(inputRequiredMessage)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -163,14 +172,14 @@ class AudioContentCommentReplyViewModel(
|
||||
isLast = false
|
||||
getCommentReplyList(parentCommentId)
|
||||
} else {
|
||||
val message = it.message ?: "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
|
||||
val message = it.message ?: unknownErrorMessage
|
||||
_toastLiveData.postValue(message)
|
||||
}
|
||||
},
|
||||
{
|
||||
_isLoading.value = false
|
||||
it.message?.let { message -> Logger.e(message) }
|
||||
_toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
|
||||
_toastLiveData.postValue(unknownErrorMessage)
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
+3
-1
@@ -8,12 +8,14 @@ class AudioContentCommentRepository(private val api: AudioContentApi) {
|
||||
contentId: Long,
|
||||
comment: String,
|
||||
parentId: Long? = null,
|
||||
isSecret: Boolean = false,
|
||||
token: String
|
||||
) = api.registerComment(
|
||||
request = RegisterAudioContentCommentRequest(
|
||||
comment = comment,
|
||||
contentId = contentId,
|
||||
parentId = parentId
|
||||
parentId = parentId,
|
||||
isSecret = isSecret
|
||||
),
|
||||
authHeader = token
|
||||
)
|
||||
|
||||
+4
@@ -1,21 +1,25 @@
|
||||
package kr.co.vividnext.sodalive.audio_content.comment
|
||||
|
||||
import android.os.Parcelable
|
||||
import androidx.annotation.Keep
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Keep
|
||||
data class GetAudioContentCommentListResponse(
|
||||
@SerializedName("totalCount") val totalCount: Int,
|
||||
@SerializedName("items") val items: List<GetAudioContentCommentListItem>
|
||||
)
|
||||
|
||||
@Parcelize
|
||||
@Keep
|
||||
data class GetAudioContentCommentListItem(
|
||||
@SerializedName("id") val id: Long,
|
||||
@SerializedName("writerId") val writerId: Long,
|
||||
@SerializedName("nickname") val nickname: String,
|
||||
@SerializedName("profileUrl") val profileUrl: String,
|
||||
@SerializedName("comment") val comment: String,
|
||||
@SerializedName("isSecret") val isSecret: Boolean,
|
||||
@SerializedName("donationCan") val donationCan: Int,
|
||||
@SerializedName("date") val date: String,
|
||||
@SerializedName("replyCount") val replyCount: Int
|
||||
|
||||
+2
@@ -1,7 +1,9 @@
|
||||
package kr.co.vividnext.sodalive.audio_content.comment
|
||||
|
||||
import androidx.annotation.Keep
|
||||
import com.google.gson.annotations.SerializedName
|
||||
|
||||
@Keep
|
||||
data class ModifyCommentRequest(
|
||||
@SerializedName("commentId") val commentId: Long,
|
||||
@SerializedName("comment") var comment: String? = null,
|
||||
|
||||
+4
-1
@@ -1,9 +1,12 @@
|
||||
package kr.co.vividnext.sodalive.audio_content.comment
|
||||
|
||||
import androidx.annotation.Keep
|
||||
import com.google.gson.annotations.SerializedName
|
||||
|
||||
@Keep
|
||||
data class RegisterAudioContentCommentRequest(
|
||||
@SerializedName("comment") val comment: String,
|
||||
@SerializedName("contentId") val contentId: Long,
|
||||
@SerializedName("parentId") val parentId: Long?
|
||||
@SerializedName("parentId") val parentId: Long?,
|
||||
@SerializedName("isSecret") val isSecret: Boolean
|
||||
)
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
package kr.co.vividnext.sodalive.audio_content.db
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.Query
|
||||
import kr.co.vividnext.sodalive.audio_content.PlaybackTracking
|
||||
|
||||
@Dao
|
||||
interface PlaybackTrackingDao {
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
fun insert(entity: PlaybackTracking): Long
|
||||
|
||||
@Query("SELECT * FROM playback_tracking WHERE id = :id LIMIT 1")
|
||||
fun getById(id: Long): PlaybackTracking?
|
||||
|
||||
@Query("SELECT * FROM playback_tracking")
|
||||
fun getAll(): List<PlaybackTracking>
|
||||
|
||||
@Query("DELETE FROM playback_tracking")
|
||||
fun deleteAll()
|
||||
}
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
package kr.co.vividnext.sodalive.audio_content.db
|
||||
|
||||
import android.content.Context
|
||||
import androidx.room.Database
|
||||
import androidx.room.Room
|
||||
import androidx.room.RoomDatabase
|
||||
import androidx.room.TypeConverters
|
||||
import kr.co.vividnext.sodalive.audio_content.PlaybackTracking
|
||||
import kr.co.vividnext.sodalive.common.Converter
|
||||
|
||||
@Database(entities = [PlaybackTracking::class], version = 1, exportSchema = true)
|
||||
@TypeConverters(Converter::class)
|
||||
abstract class PlaybackTrackingDatabase : RoomDatabase() {
|
||||
abstract fun playbackTrackingDao(): PlaybackTrackingDao
|
||||
|
||||
companion object {
|
||||
@Volatile
|
||||
private var INSTANCE: PlaybackTrackingDatabase? = null
|
||||
|
||||
fun getDatabase(context: Context): PlaybackTrackingDatabase {
|
||||
return INSTANCE ?: synchronized(this) {
|
||||
val instance = Room.databaseBuilder(
|
||||
context.applicationContext,
|
||||
PlaybackTrackingDatabase::class.java,
|
||||
"playback_tracking_database"
|
||||
)
|
||||
.fallbackToDestructiveMigration()
|
||||
.allowMainThreadQueries()
|
||||
.build()
|
||||
INSTANCE = instance
|
||||
instance
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
package kr.co.vividnext.sodalive.audio_content.detail
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import coil.load
|
||||
import coil.transform.CircleCropTransformation
|
||||
import kr.co.vividnext.sodalive.R
|
||||
import kr.co.vividnext.sodalive.databinding.ItemLiveRoomDetailUserBinding
|
||||
|
||||
class AudioContentBuyerAdapter : RecyclerView.Adapter<AudioContentBuyerAdapter.ViewHolder>() {
|
||||
|
||||
private val items = mutableListOf<ContentBuyer>()
|
||||
|
||||
inner class ViewHolder(
|
||||
private val binding: ItemLiveRoomDetailUserBinding
|
||||
) : RecyclerView.ViewHolder(binding.root) {
|
||||
fun bind(item: ContentBuyer) {
|
||||
binding.tvNickname.text = item.nickname
|
||||
binding.ivProfile.load(item.profileImageUrl) {
|
||||
crossfade(true)
|
||||
placeholder(R.drawable.ic_place_holder)
|
||||
transformations(CircleCropTransformation())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(
|
||||
parent: ViewGroup,
|
||||
viewType: Int
|
||||
): ViewHolder {
|
||||
return ViewHolder(
|
||||
ItemLiveRoomDetailUserBinding.inflate(
|
||||
LayoutInflater.from(parent.context),
|
||||
parent,
|
||||
false
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(
|
||||
holder: ViewHolder,
|
||||
position: Int
|
||||
) {
|
||||
holder.bind(items[position])
|
||||
}
|
||||
|
||||
override fun getItemCount() = items.count()
|
||||
|
||||
@SuppressLint("NotifyDataSetChanged")
|
||||
fun addItems(buyerList: List<ContentBuyer>) {
|
||||
items.addAll(buyerList)
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
@SuppressLint("NotifyDataSetChanged")
|
||||
fun clear() {
|
||||
items.clear()
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
}
|
||||
+8
-2
@@ -8,6 +8,7 @@ import android.view.LayoutInflater
|
||||
import android.view.WindowManager
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import kr.co.vividnext.sodalive.R
|
||||
import kr.co.vividnext.sodalive.databinding.DialogAudioContentDeleteBinding
|
||||
import kr.co.vividnext.sodalive.extensions.dpToPx
|
||||
|
||||
@@ -31,7 +32,10 @@ class AudioContentDeleteDialog(
|
||||
alertDialog.setCancelable(false)
|
||||
alertDialog.window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
|
||||
|
||||
dialogView.tvTitle.text = "[$title]을 삭제하시겠습니까?"
|
||||
dialogView.tvTitle.text = activity.getString(
|
||||
R.string.screen_audio_content_detail_delete_confirm_message,
|
||||
title
|
||||
)
|
||||
dialogView.tvCancel.setOnClickListener {
|
||||
alertDialog.dismiss()
|
||||
}
|
||||
@@ -43,7 +47,9 @@ class AudioContentDeleteDialog(
|
||||
} else {
|
||||
Toast.makeText(
|
||||
activity,
|
||||
"동의하셔야 삭제할 수 있습니다.",
|
||||
activity.getString(
|
||||
R.string.screen_audio_content_detail_delete_consent_required
|
||||
),
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
}
|
||||
|
||||
+674
-156
File diff suppressed because it is too large
Load Diff
+67
@@ -0,0 +1,67 @@
|
||||
package kr.co.vividnext.sodalive.audio_content.detail
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
||||
import kr.co.vividnext.sodalive.R
|
||||
import kr.co.vividnext.sodalive.databinding.DialogAudioContentDetailMenuBinding
|
||||
|
||||
class AudioContentDetailMenuBottomSheetDialog(
|
||||
private val isPin: Boolean,
|
||||
private val isCreator: Boolean,
|
||||
private val onClickPin: () -> Unit,
|
||||
private val onClickModify: () -> Unit,
|
||||
private val onClickDelete: () -> Unit,
|
||||
private val onClickReport: () -> Unit
|
||||
) : BottomSheetDialogFragment() {
|
||||
private lateinit var dialog: DialogAudioContentDetailMenuBinding
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
dialog = DialogAudioContentDetailMenuBinding.inflate(inflater, container, false)
|
||||
return dialog.root
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
if (isCreator) {
|
||||
dialog.tvReport.visibility = View.GONE
|
||||
dialog.llMenuCreator.visibility = View.VISIBLE
|
||||
|
||||
if (isPin) {
|
||||
dialog.ivPin.setImageResource(R.drawable.ic_pin_cancel)
|
||||
dialog.tvPin.text =
|
||||
getString(R.string.screen_audio_content_detail_pin_cancel)
|
||||
} else {
|
||||
dialog.ivPin.setImageResource(R.drawable.ic_pin)
|
||||
dialog.tvPin.text = getString(R.string.screen_audio_content_detail_pin)
|
||||
}
|
||||
|
||||
dialog.llPin.setOnClickListener {
|
||||
dismiss()
|
||||
onClickPin()
|
||||
}
|
||||
dialog.llModify.setOnClickListener {
|
||||
dismiss()
|
||||
onClickModify()
|
||||
}
|
||||
dialog.llDelete.setOnClickListener {
|
||||
dismiss()
|
||||
onClickDelete()
|
||||
}
|
||||
} else {
|
||||
dialog.llMenuCreator.visibility = View.GONE
|
||||
dialog.tvReport.visibility = View.VISIBLE
|
||||
dialog.tvReport.setOnClickListener {
|
||||
dismiss()
|
||||
onClickReport()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+144
-99
@@ -1,33 +1,30 @@
|
||||
package kr.co.vividnext.sodalive.audio_content.detail
|
||||
|
||||
import android.net.Uri
|
||||
import androidx.core.net.toUri
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import com.google.firebase.dynamiclinks.ShortDynamicLink
|
||||
import com.google.firebase.dynamiclinks.ktx.androidParameters
|
||||
import com.google.firebase.dynamiclinks.ktx.dynamicLinks
|
||||
import com.google.firebase.dynamiclinks.ktx.iosParameters
|
||||
import com.google.firebase.dynamiclinks.ktx.shortLinkAsync
|
||||
import com.google.firebase.dynamiclinks.ktx.socialMetaTagParameters
|
||||
import com.google.firebase.ktx.Firebase
|
||||
import com.orhanobut.logger.Logger
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import kr.co.vividnext.sodalive.R
|
||||
import kr.co.vividnext.sodalive.audio_content.AudioContentRepository
|
||||
import kr.co.vividnext.sodalive.audio_content.comment.AudioContentCommentRepository
|
||||
import kr.co.vividnext.sodalive.audio_content.order.OrderType
|
||||
import kr.co.vividnext.sodalive.base.BaseViewModel
|
||||
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
|
||||
import kr.co.vividnext.sodalive.common.SodaLiveApplicationHolder
|
||||
import kr.co.vividnext.sodalive.common.Utils
|
||||
import kr.co.vividnext.sodalive.extensions.moneyFormat
|
||||
import kr.co.vividnext.sodalive.mypage.auth.AuthRepository
|
||||
import kr.co.vividnext.sodalive.mypage.auth.AuthVerifyRequest
|
||||
import kr.co.vividnext.sodalive.report.ReportRepository
|
||||
import kr.co.vividnext.sodalive.report.ReportRequest
|
||||
import kr.co.vividnext.sodalive.report.ReportType
|
||||
import kr.co.vividnext.sodalive.user.UserRepository
|
||||
|
||||
class AudioContentDetailViewModel(
|
||||
private val repository: AudioContentRepository,
|
||||
private val userRepository: UserRepository,
|
||||
private val authRepository: AuthRepository,
|
||||
private val reportRepository: ReportRepository,
|
||||
private val commentRepository: AudioContentCommentRepository
|
||||
@@ -58,7 +55,13 @@ class AudioContentDetailViewModel(
|
||||
val isContentPlayLoopLiveData: LiveData<Boolean>
|
||||
get() = _isContentPlayLoopLiveData
|
||||
|
||||
fun getAudioContentDetail(audioContentId: Long, onFailure: (() -> Unit)? = null) {
|
||||
private fun getString(@StringRes resId: Int, vararg args: Any) =
|
||||
SodaLiveApplicationHolder.get().getString(resId, *args)
|
||||
|
||||
fun getAudioContentDetail(
|
||||
audioContentId: Long,
|
||||
onFailure: (() -> Unit)? = null
|
||||
) {
|
||||
if (!isLoading.value!!) {
|
||||
isLoading.value = true
|
||||
}
|
||||
@@ -79,7 +82,7 @@ class AudioContentDetailViewModel(
|
||||
_toastLiveData.postValue(it.message)
|
||||
} else {
|
||||
_toastLiveData.postValue(
|
||||
"알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
|
||||
getString(R.string.common_error_unknown)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -93,7 +96,7 @@ class AudioContentDetailViewModel(
|
||||
{
|
||||
isLoading.value = false
|
||||
it.message?.let { message -> Logger.e(message) }
|
||||
_toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
|
||||
_toastLiveData.postValue(getString(R.string.common_error_unknown))
|
||||
if (onFailure != null) {
|
||||
onFailure()
|
||||
}
|
||||
@@ -102,12 +105,14 @@ class AudioContentDetailViewModel(
|
||||
)
|
||||
}
|
||||
|
||||
fun registerNotification(contentId: Long, creatorId: Long) {
|
||||
fun follow(contentId: Long, creatorId: Long, follow: Boolean = true, notify: Boolean = true) {
|
||||
isLoading.value = true
|
||||
compositeDisposable.add(
|
||||
repository.registerNotification(
|
||||
creatorId,
|
||||
"Bearer ${SharedPreferenceManager.token}"
|
||||
userRepository.creatorFollow(
|
||||
creatorId = creatorId,
|
||||
follow,
|
||||
notify,
|
||||
token = "Bearer ${SharedPreferenceManager.token}"
|
||||
)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
@@ -120,7 +125,7 @@ class AudioContentDetailViewModel(
|
||||
_toastLiveData.postValue(it.message)
|
||||
} else {
|
||||
_toastLiveData.postValue(
|
||||
"알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
|
||||
getString(R.string.common_error_unknown)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -129,40 +134,7 @@ class AudioContentDetailViewModel(
|
||||
{
|
||||
isLoading.value = false
|
||||
it.message?.let { message -> Logger.e(message) }
|
||||
_toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
fun unRegisterNotification(contentId: Long, creatorId: Long) {
|
||||
isLoading.value = true
|
||||
compositeDisposable.add(
|
||||
repository.unRegisterNotification(
|
||||
creatorId,
|
||||
"Bearer ${SharedPreferenceManager.token}"
|
||||
)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
{
|
||||
if (it.success && it.data != null) {
|
||||
getAudioContentDetail(contentId)
|
||||
} else {
|
||||
if (it.message != null) {
|
||||
_toastLiveData.postValue(it.message)
|
||||
} else {
|
||||
_toastLiveData.postValue(
|
||||
"알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
|
||||
)
|
||||
}
|
||||
}
|
||||
isLoading.value = false
|
||||
},
|
||||
{
|
||||
isLoading.value = false
|
||||
it.message?.let { message -> Logger.e(message) }
|
||||
_toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
|
||||
_toastLiveData.postValue(getString(R.string.common_error_unknown))
|
||||
}
|
||||
)
|
||||
)
|
||||
@@ -176,7 +148,7 @@ class AudioContentDetailViewModel(
|
||||
_isShowPreviewAlert.value = !_isShowPreviewAlert.value!!
|
||||
}
|
||||
|
||||
fun order(contentId: Long, orderType: OrderType) {
|
||||
fun order(contentId: Long, orderType: OrderType, gotoShop: () -> Unit) {
|
||||
isLoading.value = true
|
||||
compositeDisposable.add(
|
||||
repository.orderContent(
|
||||
@@ -190,13 +162,26 @@ class AudioContentDetailViewModel(
|
||||
{
|
||||
if (it.success && it.data != null) {
|
||||
getAudioContentDetail(audioContentId = contentId)
|
||||
_toastLiveData.postValue("구매가 완료되었습니다.")
|
||||
_toastLiveData.postValue(
|
||||
if (orderType == OrderType.RENTAL) {
|
||||
getString(
|
||||
R.string.screen_audio_content_detail_order_rental_success
|
||||
)
|
||||
} else {
|
||||
getString(
|
||||
R.string.screen_audio_content_detail_order_keep_success
|
||||
)
|
||||
}
|
||||
)
|
||||
} else {
|
||||
if (it.message != null) {
|
||||
_toastLiveData.postValue(it.message)
|
||||
if (it.message.contains("캔이 부족합니다")) {
|
||||
gotoShop()
|
||||
}
|
||||
} else {
|
||||
_toastLiveData.postValue(
|
||||
"알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
|
||||
getString(R.string.common_error_unknown)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -205,7 +190,7 @@ class AudioContentDetailViewModel(
|
||||
{
|
||||
isLoading.value = false
|
||||
it.message?.let { message -> Logger.e(message) }
|
||||
_toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
|
||||
_toastLiveData.postValue(getString(R.string.common_error_unknown))
|
||||
}
|
||||
)
|
||||
)
|
||||
@@ -229,7 +214,7 @@ class AudioContentDetailViewModel(
|
||||
_toastLiveData.postValue(it.message)
|
||||
} else {
|
||||
_toastLiveData.postValue(
|
||||
"알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
|
||||
getString(R.string.common_error_unknown)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -239,13 +224,13 @@ class AudioContentDetailViewModel(
|
||||
{
|
||||
isLoading.value = false
|
||||
it.message?.let { message -> Logger.e(message) }
|
||||
_toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
|
||||
_toastLiveData.postValue(getString(R.string.common_error_unknown))
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
fun registerComment(audioContentId: Long, comment: String) {
|
||||
fun registerComment(audioContentId: Long, comment: String, isSecret: Boolean) {
|
||||
if (!isLoading.value!!) {
|
||||
isLoading.value = true
|
||||
}
|
||||
@@ -254,6 +239,7 @@ class AudioContentDetailViewModel(
|
||||
commentRepository.registerComment(
|
||||
contentId = audioContentId,
|
||||
comment = comment,
|
||||
isSecret = isSecret,
|
||||
token = "Bearer ${SharedPreferenceManager.token}"
|
||||
)
|
||||
.subscribeOn(Schedulers.io())
|
||||
@@ -267,7 +253,7 @@ class AudioContentDetailViewModel(
|
||||
_toastLiveData.postValue(it.message)
|
||||
} else {
|
||||
_toastLiveData.postValue(
|
||||
"알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
|
||||
getString(R.string.common_error_unknown)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -277,7 +263,7 @@ class AudioContentDetailViewModel(
|
||||
{
|
||||
isLoading.value = false
|
||||
it.message?.let { message -> Logger.e(message) }
|
||||
_toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
|
||||
_toastLiveData.postValue(getString(R.string.common_error_unknown))
|
||||
}
|
||||
)
|
||||
)
|
||||
@@ -310,7 +296,7 @@ class AudioContentDetailViewModel(
|
||||
_toastLiveData.postValue(it.message)
|
||||
} else {
|
||||
_toastLiveData.postValue(
|
||||
"알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
|
||||
getString(R.string.common_error_unknown)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -318,44 +304,24 @@ class AudioContentDetailViewModel(
|
||||
{
|
||||
isLoading.value = false
|
||||
it.message?.let { message -> Logger.e(message) }
|
||||
_toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
|
||||
_toastLiveData.postValue(getString(R.string.common_error_unknown))
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
fun shareAudioContent(
|
||||
fun shareContent(
|
||||
audioContentId: Long,
|
||||
contentImage: String,
|
||||
contentTitle: String,
|
||||
onSuccess: (String) -> Unit
|
||||
) {
|
||||
isLoading.value = true
|
||||
Firebase.dynamicLinks.shortLinkAsync(ShortDynamicLink.Suffix.SHORT) {
|
||||
link = Uri.parse("https://sodalive.net/?audio_content_id=$audioContentId")
|
||||
domainUriPrefix = "https://sodalive.page.link"
|
||||
androidParameters { }
|
||||
iosParameters("kr.co.vividnext.sodalive") {
|
||||
appStoreId = "6461721697"
|
||||
}
|
||||
socialMetaTagParameters {
|
||||
title = contentTitle
|
||||
description = "지금 소다라이브에서 이 콘텐츠 감상하기"
|
||||
imageUrl = contentImage.toUri()
|
||||
}
|
||||
}.addOnSuccessListener {
|
||||
val uri = it.shortLink
|
||||
if (uri != null) {
|
||||
val message = uri.toString()
|
||||
onSuccess(message)
|
||||
} else {
|
||||
_toastLiveData.postValue("공유링크를 생성하지 못했습니다.\n다시 시도해 주세요.")
|
||||
}
|
||||
}.addOnFailureListener {
|
||||
_toastLiveData.postValue("공유링크를 생성하지 못했습니다.\n다시 시도해 주세요.")
|
||||
}.addOnCompleteListener {
|
||||
isLoading.value = false
|
||||
}
|
||||
val params = mapOf(
|
||||
"af_dp" to "voiceon://",
|
||||
"deep_link_value" to "content",
|
||||
"deep_link_sub5" to "$audioContentId"
|
||||
)
|
||||
|
||||
val shareUrl = Utils.createOneLinkUrl(params = params)
|
||||
onSuccess(shareUrl)
|
||||
}
|
||||
|
||||
fun deleteAudioContent(audioContentId: Long, onSuccess: () -> Unit) {
|
||||
@@ -374,7 +340,7 @@ class AudioContentDetailViewModel(
|
||||
|
||||
if (it.success) {
|
||||
_toastLiveData.postValue(
|
||||
"삭제되었습니다."
|
||||
getString(R.string.screen_audio_content_detail_delete_success)
|
||||
)
|
||||
onSuccess()
|
||||
} else {
|
||||
@@ -382,7 +348,7 @@ class AudioContentDetailViewModel(
|
||||
_toastLiveData.postValue(it.message)
|
||||
} else {
|
||||
_toastLiveData.postValue(
|
||||
"알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
|
||||
getString(R.string.common_error_unknown)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -390,7 +356,7 @@ class AudioContentDetailViewModel(
|
||||
{
|
||||
isLoading.value = false
|
||||
it.message?.let { message -> Logger.e(message) }
|
||||
_toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
|
||||
_toastLiveData.postValue(getString(R.string.common_error_unknown))
|
||||
}
|
||||
)
|
||||
)
|
||||
@@ -412,7 +378,7 @@ class AudioContentDetailViewModel(
|
||||
_toastLiveData.postValue(it.message)
|
||||
} else {
|
||||
_toastLiveData.postValue(
|
||||
"신고가 접수되었습니다."
|
||||
getString(R.string.screen_audio_content_detail_report_submitted)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -421,7 +387,9 @@ class AudioContentDetailViewModel(
|
||||
{
|
||||
isLoading.value = false
|
||||
it.message?.let { message -> Logger.e(message) }
|
||||
_toastLiveData.postValue("신고가 접수되었습니다.")
|
||||
_toastLiveData.postValue(
|
||||
getString(R.string.screen_audio_content_detail_report_submitted)
|
||||
)
|
||||
}
|
||||
)
|
||||
)
|
||||
@@ -451,7 +419,10 @@ class AudioContentDetailViewModel(
|
||||
if (it.success) {
|
||||
SharedPreferenceManager.can -= can
|
||||
_toastLiveData.postValue(
|
||||
"${can.moneyFormat()}캔을 후원하였습니다."
|
||||
getString(
|
||||
R.string.screen_audio_content_detail_donation_success,
|
||||
can.moneyFormat()
|
||||
)
|
||||
)
|
||||
onSuccess()
|
||||
} else {
|
||||
@@ -459,7 +430,7 @@ class AudioContentDetailViewModel(
|
||||
_toastLiveData.postValue(it.message)
|
||||
} else {
|
||||
_toastLiveData.postValue(
|
||||
"알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
|
||||
getString(R.string.common_error_unknown)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -467,7 +438,81 @@ class AudioContentDetailViewModel(
|
||||
{
|
||||
isLoading.value = false
|
||||
it.message?.let { message -> Logger.e(message) }
|
||||
_toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
|
||||
_toastLiveData.postValue(getString(R.string.common_error_unknown))
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
fun pinContent(audioContentId: Long) {
|
||||
isLoading.value = true
|
||||
compositeDisposable.add(
|
||||
repository.pinContent(
|
||||
audioContentId,
|
||||
token = "Bearer ${SharedPreferenceManager.token}"
|
||||
)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
{
|
||||
isLoading.value = false
|
||||
|
||||
if (it.success) {
|
||||
_toastLiveData.postValue(
|
||||
getString(R.string.screen_audio_content_detail_pin_success)
|
||||
)
|
||||
getAudioContentDetail(audioContentId)
|
||||
} else {
|
||||
if (it.message != null) {
|
||||
_toastLiveData.postValue(it.message)
|
||||
} else {
|
||||
_toastLiveData.postValue(
|
||||
getString(R.string.common_error_unknown)
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
isLoading.value = false
|
||||
it.message?.let { message -> Logger.e(message) }
|
||||
_toastLiveData.postValue(getString(R.string.common_error_unknown))
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
fun unPinContent(audioContentId: Long) {
|
||||
isLoading.value = true
|
||||
compositeDisposable.add(
|
||||
repository.unpinContent(
|
||||
audioContentId,
|
||||
token = "Bearer ${SharedPreferenceManager.token}"
|
||||
)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
{
|
||||
isLoading.value = false
|
||||
|
||||
if (it.success) {
|
||||
_toastLiveData.postValue(
|
||||
getString(R.string.screen_audio_content_detail_unpin_success)
|
||||
)
|
||||
getAudioContentDetail(audioContentId)
|
||||
} else {
|
||||
if (it.message != null) {
|
||||
_toastLiveData.postValue(it.message)
|
||||
} else {
|
||||
_toastLiveData.postValue(
|
||||
getString(R.string.common_error_unknown)
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
isLoading.value = false
|
||||
it.message?.let { message -> Logger.e(message) }
|
||||
_toastLiveData.postValue(getString(R.string.common_error_unknown))
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
+8
-1
@@ -8,6 +8,7 @@ import android.view.WindowManager
|
||||
import android.widget.RadioButton
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import kr.co.vividnext.sodalive.R
|
||||
import kr.co.vividnext.sodalive.databinding.DialogAudioContentReportBinding
|
||||
import kr.co.vividnext.sodalive.extensions.dpToPx
|
||||
|
||||
@@ -37,7 +38,13 @@ class AudioContentReportDialog(
|
||||
alertDialog.dismiss()
|
||||
confirmButtonClick(reason)
|
||||
} else {
|
||||
Toast.makeText(activity, "신고 이유를 선택하세요.", Toast.LENGTH_LONG).show()
|
||||
Toast.makeText(
|
||||
activity,
|
||||
activity.getString(
|
||||
R.string.screen_audio_content_detail_report_reason_required
|
||||
),
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+35
-2
@@ -1,9 +1,12 @@
|
||||
package kr.co.vividnext.sodalive.audio_content.detail
|
||||
|
||||
import androidx.annotation.Keep
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import kr.co.vividnext.sodalive.audio_content.PurchaseOption
|
||||
import kr.co.vividnext.sodalive.audio_content.comment.GetAudioContentCommentListItem
|
||||
import kr.co.vividnext.sodalive.audio_content.order.OrderType
|
||||
|
||||
@Keep
|
||||
data class GetAudioContentDetailResponse(
|
||||
@SerializedName("contentId") val contentId: Long,
|
||||
@SerializedName("title") val title: String,
|
||||
@@ -14,9 +17,15 @@ data class GetAudioContentDetailResponse(
|
||||
@SerializedName("tag") val tag: String,
|
||||
@SerializedName("price") val price: Int,
|
||||
@SerializedName("duration") val duration: String,
|
||||
@SerializedName("releaseDate") val releaseDate: String?,
|
||||
@SerializedName("totalContentCount") val totalContentCount: Int?,
|
||||
@SerializedName("remainingContentCount") val remainingContentCount: Int?,
|
||||
@SerializedName("orderSequence") val orderSequence: Int?,
|
||||
@SerializedName("isActivePreview") val isActivePreview: Boolean,
|
||||
@SerializedName("isAdult") val isAdult: Boolean,
|
||||
@SerializedName("isMosaic") val isMosaic: Boolean,
|
||||
@SerializedName("existOrdered") val existOrdered: Boolean,
|
||||
@SerializedName("purchaseOption") val purchaseOption: PurchaseOption,
|
||||
@SerializedName("orderType") val orderType: OrderType?,
|
||||
@SerializedName("remainingTime") val remainingTime: String?,
|
||||
@SerializedName("creatorOtherContentList")
|
||||
@@ -28,18 +37,42 @@ data class GetAudioContentDetailResponse(
|
||||
@SerializedName("likeCount") val likeCount: Int,
|
||||
@SerializedName("commentList") val commentList: List<GetAudioContentCommentListItem>,
|
||||
@SerializedName("commentCount") val commentCount: Int,
|
||||
@SerializedName("creator") val creator: AudioContentCreator
|
||||
@SerializedName("isPin") val isPin: Boolean,
|
||||
@SerializedName("isAvailablePin") val isAvailablePin: Boolean,
|
||||
@SerializedName("creator") val creator: AudioContentCreator,
|
||||
@SerializedName("previousContent") val previousContent: OtherContentResponse?,
|
||||
@SerializedName("nextContent") val nextContent: OtherContentResponse?,
|
||||
@SerializedName("buyerList") val buyerList: List<ContentBuyer>,
|
||||
@SerializedName("isAvailableUsePoint") val isAvailableUsePoint: Boolean,
|
||||
@SerializedName("translated") val translated: TranslatedContent?
|
||||
)
|
||||
|
||||
@Keep
|
||||
data class OtherContentResponse(
|
||||
@SerializedName("contentId") val contentId: Long,
|
||||
@SerializedName("title") val title: String,
|
||||
@SerializedName("coverUrl") val coverUrl: String,
|
||||
)
|
||||
|
||||
@Keep
|
||||
data class AudioContentCreator(
|
||||
@SerializedName("creatorId") val creatorId: Long,
|
||||
@SerializedName("nickname") val nickname: String,
|
||||
@SerializedName("profileImageUrl") val profileImageUrl: String,
|
||||
@SerializedName("isFollowing") val isFollowing: Boolean
|
||||
@SerializedName("isFollowing") val isFollowing: Boolean,
|
||||
@SerializedName("isFollow") var isFollow: Boolean,
|
||||
@SerializedName("isNotify") var isNotify: Boolean
|
||||
)
|
||||
|
||||
@Keep
|
||||
data class ContentBuyer(
|
||||
@SerializedName("nickname") val nickname: String,
|
||||
@SerializedName("profileImageUrl") val profileImageUrl: String
|
||||
)
|
||||
|
||||
@Keep
|
||||
data class TranslatedContent(
|
||||
@SerializedName("title") val title: String?,
|
||||
@SerializedName("detail") val detail: String?,
|
||||
@SerializedName("tags") val tags: String?
|
||||
)
|
||||
|
||||
+3
@@ -1,11 +1,14 @@
|
||||
package kr.co.vividnext.sodalive.audio_content.detail
|
||||
|
||||
import androidx.annotation.Keep
|
||||
import com.google.gson.annotations.SerializedName
|
||||
|
||||
@Keep
|
||||
data class PutAudioContentLikeRequest(
|
||||
@SerializedName("contentId") val contentId: Long
|
||||
)
|
||||
|
||||
@Keep
|
||||
data class PutAudioContentLikeResponse(
|
||||
@SerializedName("like") val like: Boolean
|
||||
)
|
||||
|
||||
+2
@@ -1,7 +1,9 @@
|
||||
package kr.co.vividnext.sodalive.audio_content.donation
|
||||
|
||||
import androidx.annotation.Keep
|
||||
import com.google.gson.annotations.SerializedName
|
||||
|
||||
@Keep
|
||||
data class AudioContentDonationRequest(
|
||||
@SerializedName("contentId") val contentId: Long,
|
||||
@SerializedName("donationCan") val donationCan: Int,
|
||||
|
||||
-41
@@ -1,41 +0,0 @@
|
||||
package kr.co.vividnext.sodalive.audio_content.main
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import kr.co.vividnext.sodalive.databinding.ItemAudioContentMainBinding
|
||||
|
||||
class AudioContentMainContentAdapter(
|
||||
private val onClickItem: (Long) -> Unit,
|
||||
private val onClickCreator: (Long) -> Unit,
|
||||
) : RecyclerView.Adapter<AudioContentMainItemViewHolder>() {
|
||||
|
||||
private val items = mutableListOf<GetAudioContentMainItem>()
|
||||
|
||||
override fun onCreateViewHolder(
|
||||
parent: ViewGroup,
|
||||
viewType: Int
|
||||
) = AudioContentMainItemViewHolder(
|
||||
ItemAudioContentMainBinding.inflate(
|
||||
LayoutInflater.from(parent.context),
|
||||
parent,
|
||||
false
|
||||
),
|
||||
onClickItem = onClickItem,
|
||||
onClickCreator = onClickCreator
|
||||
)
|
||||
|
||||
override fun getItemCount() = items.size
|
||||
|
||||
override fun onBindViewHolder(holder: AudioContentMainItemViewHolder, position: Int) {
|
||||
holder.bind(items[position])
|
||||
}
|
||||
|
||||
@SuppressLint("NotifyDataSetChanged")
|
||||
fun addItems(items: List<GetAudioContentMainItem>) {
|
||||
this.items.clear()
|
||||
this.items.addAll(items)
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
}
|
||||
-94
@@ -1,94 +0,0 @@
|
||||
package kr.co.vividnext.sodalive.audio_content.main
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.graphics.Rect
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import kr.co.vividnext.sodalive.databinding.ItemAudioContentMainCurationBinding
|
||||
import kr.co.vividnext.sodalive.extensions.dpToPx
|
||||
|
||||
class AudioContentMainCurationAdapter(
|
||||
private val onClickItem: (Long) -> Unit,
|
||||
private val onClickCreator: (Long) -> Unit,
|
||||
) : RecyclerView.Adapter<AudioContentMainCurationAdapter.ViewHolder>() {
|
||||
|
||||
private val items = mutableListOf<GetAudioContentCurationResponse>()
|
||||
|
||||
inner class ViewHolder(
|
||||
private val context: Context,
|
||||
private val binding: ItemAudioContentMainCurationBinding
|
||||
) : RecyclerView.ViewHolder(binding.root) {
|
||||
fun bind(item: GetAudioContentCurationResponse) {
|
||||
binding.tvTitle.text = item.title
|
||||
binding.tvDesc.text = item.description
|
||||
setAudioContentList(item.audioContents)
|
||||
}
|
||||
|
||||
private fun setAudioContentList(audioContents: List<GetAudioContentMainItem>) {
|
||||
val adapter = AudioContentMainContentAdapter(onClickItem, onClickCreator)
|
||||
|
||||
binding.rvCuration.layoutManager = LinearLayoutManager(
|
||||
context,
|
||||
LinearLayoutManager.HORIZONTAL,
|
||||
false
|
||||
)
|
||||
if (binding.rvCuration.itemDecorationCount == 0) {
|
||||
binding.rvCuration.addItemDecoration(object : RecyclerView.ItemDecoration() {
|
||||
override fun getItemOffsets(
|
||||
outRect: Rect,
|
||||
view: View,
|
||||
parent: RecyclerView,
|
||||
state: RecyclerView.State
|
||||
) {
|
||||
super.getItemOffsets(outRect, view, parent, state)
|
||||
|
||||
when (parent.getChildAdapterPosition(view)) {
|
||||
0 -> {
|
||||
outRect.left = 0
|
||||
outRect.right = 6.7f.dpToPx().toInt()
|
||||
}
|
||||
|
||||
adapter.itemCount - 1 -> {
|
||||
outRect.left = 6.7f.dpToPx().toInt()
|
||||
outRect.right = 0
|
||||
}
|
||||
|
||||
else -> {
|
||||
outRect.left = 6.7f.dpToPx().toInt()
|
||||
outRect.right = 6.7f.dpToPx().toInt()
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
binding.rvCuration.adapter = adapter
|
||||
adapter.addItems(audioContents)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ViewHolder(
|
||||
parent.context,
|
||||
ItemAudioContentMainCurationBinding.inflate(
|
||||
LayoutInflater.from(parent.context),
|
||||
parent,
|
||||
false
|
||||
)
|
||||
)
|
||||
|
||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||
holder.bind(items[position])
|
||||
}
|
||||
|
||||
override fun getItemCount() = items.size
|
||||
|
||||
@SuppressLint("NotifyDataSetChanged")
|
||||
fun addItems(items: List<GetAudioContentCurationResponse>) {
|
||||
this.items.clear()
|
||||
this.items.addAll(items)
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
}
|
||||
-474
@@ -1,474 +0,0 @@
|
||||
package kr.co.vividnext.sodalive.audio_content.main
|
||||
|
||||
import android.app.Service
|
||||
import android.content.Intent
|
||||
import android.graphics.Rect
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.Toast
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.zhpan.bannerview.BaseBannerAdapter
|
||||
import com.zhpan.indicator.enums.IndicatorSlideMode
|
||||
import com.zhpan.indicator.enums.IndicatorStyle
|
||||
import kr.co.vividnext.sodalive.R
|
||||
import kr.co.vividnext.sodalive.audio_content.detail.AudioContentDetailActivity
|
||||
import kr.co.vividnext.sodalive.audio_content.order.AudioContentOrderListActivity
|
||||
import kr.co.vividnext.sodalive.audio_content.upload.AudioContentUploadActivity
|
||||
import kr.co.vividnext.sodalive.base.BaseFragment
|
||||
import kr.co.vividnext.sodalive.common.Constants
|
||||
import kr.co.vividnext.sodalive.common.LoadingDialog
|
||||
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
|
||||
import kr.co.vividnext.sodalive.databinding.FragmentAudioContentMainBinding
|
||||
import kr.co.vividnext.sodalive.explorer.profile.UserProfileActivity
|
||||
import kr.co.vividnext.sodalive.extensions.dpToPx
|
||||
import kr.co.vividnext.sodalive.settings.event.EventDetailActivity
|
||||
import kr.co.vividnext.sodalive.settings.notification.MemberRole
|
||||
import org.koin.android.ext.android.inject
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
class AudioContentMainFragment : BaseFragment<FragmentAudioContentMainBinding>(
|
||||
FragmentAudioContentMainBinding::inflate
|
||||
) {
|
||||
private val viewModel: AudioContentMainViewModel by inject()
|
||||
|
||||
private lateinit var loadingDialog: LoadingDialog
|
||||
private lateinit var imm: InputMethodManager
|
||||
|
||||
private lateinit var newContentCreatorAdapter: AudioContentMainNewContentCreatorAdapter
|
||||
private lateinit var bannerAdapter: AudioContentMainBannerAdapter
|
||||
private lateinit var orderListAdapter: AudioContentMainContentAdapter
|
||||
private lateinit var newContentThemeAdapter: AudioContentMainNewContentThemeAdapter
|
||||
private lateinit var newContentAdapter: AudioContentMainContentAdapter
|
||||
private lateinit var curationAdapter: AudioContentMainCurationAdapter
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
loadingDialog = LoadingDialog(requireActivity(), layoutInflater)
|
||||
imm = requireContext().getSystemService(
|
||||
Service.INPUT_METHOD_SERVICE
|
||||
) as InputMethodManager
|
||||
|
||||
setupView()
|
||||
bindData()
|
||||
|
||||
viewModel.getMain()
|
||||
}
|
||||
|
||||
private fun setupView() {
|
||||
if (SharedPreferenceManager.role == MemberRole.CREATOR.name) {
|
||||
binding.llUploadContent.visibility = View.VISIBLE
|
||||
binding.llUploadContent.setOnClickListener {
|
||||
startActivity(
|
||||
Intent(
|
||||
requireActivity(),
|
||||
AudioContentUploadActivity::class.java
|
||||
)
|
||||
)
|
||||
}
|
||||
} else {
|
||||
binding.llUploadContent.visibility = View.GONE
|
||||
}
|
||||
|
||||
setupNewContentCreator()
|
||||
setupBanner()
|
||||
setupOrderList()
|
||||
setupNewContentTheme()
|
||||
setupNewContent()
|
||||
setupCuration()
|
||||
|
||||
binding.swipeRefreshLayout.setOnRefreshListener {
|
||||
binding.swipeRefreshLayout.isRefreshing = false
|
||||
viewModel.getMain()
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupNewContentCreator() {
|
||||
newContentCreatorAdapter = AudioContentMainNewContentCreatorAdapter {
|
||||
val intent = Intent(requireContext(), UserProfileActivity::class.java)
|
||||
intent.putExtra(Constants.EXTRA_USER_ID, it)
|
||||
startActivity(intent)
|
||||
}
|
||||
|
||||
binding.rvNewContentCreator.layoutManager = LinearLayoutManager(
|
||||
context,
|
||||
LinearLayoutManager.HORIZONTAL,
|
||||
false
|
||||
)
|
||||
|
||||
binding.rvNewContentCreator.addItemDecoration(object : RecyclerView.ItemDecoration() {
|
||||
override fun getItemOffsets(
|
||||
outRect: Rect,
|
||||
view: View,
|
||||
parent: RecyclerView,
|
||||
state: RecyclerView.State
|
||||
) {
|
||||
super.getItemOffsets(outRect, view, parent, state)
|
||||
|
||||
when (parent.getChildAdapterPosition(view)) {
|
||||
0 -> {
|
||||
outRect.left = 0
|
||||
outRect.right = 10.7f.dpToPx().toInt()
|
||||
}
|
||||
|
||||
orderListAdapter.itemCount - 1 -> {
|
||||
outRect.left = 10.7f.dpToPx().toInt()
|
||||
outRect.right = 0
|
||||
}
|
||||
|
||||
else -> {
|
||||
outRect.left = 10.7f.dpToPx().toInt()
|
||||
outRect.right = 10.7f.dpToPx().toInt()
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
binding.rvNewContentCreator.adapter = newContentCreatorAdapter
|
||||
}
|
||||
|
||||
private fun setupBanner() {
|
||||
val layoutParams = binding
|
||||
.rvBanner
|
||||
.layoutParams as LinearLayout.LayoutParams
|
||||
|
||||
val pagerWidth = screenWidth.toDouble() - 26.7f.dpToPx()
|
||||
val pagerHeight = (pagerWidth * 0.53).roundToInt()
|
||||
layoutParams.width = pagerWidth.roundToInt()
|
||||
layoutParams.height = pagerHeight
|
||||
|
||||
bannerAdapter = AudioContentMainBannerAdapter(
|
||||
requireContext(),
|
||||
pagerWidth.roundToInt(),
|
||||
pagerHeight
|
||||
) {
|
||||
when (it.type) {
|
||||
AudioContentBannerType.EVENT -> {
|
||||
startActivity(
|
||||
Intent(requireContext(), EventDetailActivity::class.java).apply {
|
||||
putExtra(Constants.EXTRA_EVENT, it.eventItem!!)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
AudioContentBannerType.CREATOR -> {
|
||||
startActivity(
|
||||
Intent(requireContext(), UserProfileActivity::class.java).apply {
|
||||
putExtra(Constants.EXTRA_USER_ID, it.creatorId!!)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
AudioContentBannerType.LINK -> {
|
||||
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(it.link!!)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
binding
|
||||
.rvBanner
|
||||
.layoutParams = layoutParams
|
||||
|
||||
binding.rvBanner.apply {
|
||||
adapter = bannerAdapter as BaseBannerAdapter<Any>
|
||||
|
||||
setLifecycleRegistry(lifecycle)
|
||||
setScrollDuration(1000)
|
||||
setInterval(4 * 1000)
|
||||
}.create()
|
||||
|
||||
binding
|
||||
.rvBanner
|
||||
.setIndicatorView(binding.indicatorBanner)
|
||||
.setIndicatorStyle(IndicatorStyle.ROUND_RECT)
|
||||
.setIndicatorSlideMode(IndicatorSlideMode.SMOOTH)
|
||||
.setIndicatorVisibility(View.GONE)
|
||||
.setIndicatorSliderColor(
|
||||
ContextCompat.getColor(requireContext(), R.color.color_909090),
|
||||
ContextCompat.getColor(requireContext(), R.color.color_9970ff)
|
||||
)
|
||||
.setIndicatorSliderWidth(4f.dpToPx().toInt(), 10f.dpToPx().toInt())
|
||||
.setIndicatorHeight(4f.dpToPx().toInt())
|
||||
}
|
||||
|
||||
private fun setupOrderList() {
|
||||
orderListAdapter = AudioContentMainContentAdapter(
|
||||
onClickItem = {
|
||||
startActivity(
|
||||
Intent(requireContext(), AudioContentDetailActivity::class.java).apply {
|
||||
putExtra(Constants.EXTRA_AUDIO_CONTENT_ID, it)
|
||||
}
|
||||
)
|
||||
},
|
||||
onClickCreator = {
|
||||
startActivity(
|
||||
Intent(requireContext(), UserProfileActivity::class.java).apply {
|
||||
putExtra(Constants.EXTRA_USER_ID, it)
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
binding.rvMyStash.layoutManager = LinearLayoutManager(
|
||||
context,
|
||||
LinearLayoutManager.HORIZONTAL,
|
||||
false
|
||||
)
|
||||
|
||||
binding.rvMyStash.addItemDecoration(object : RecyclerView.ItemDecoration() {
|
||||
override fun getItemOffsets(
|
||||
outRect: Rect,
|
||||
view: View,
|
||||
parent: RecyclerView,
|
||||
state: RecyclerView.State
|
||||
) {
|
||||
super.getItemOffsets(outRect, view, parent, state)
|
||||
|
||||
when (parent.getChildAdapterPosition(view)) {
|
||||
0 -> {
|
||||
outRect.left = 0
|
||||
outRect.right = 6.7f.dpToPx().toInt()
|
||||
}
|
||||
|
||||
orderListAdapter.itemCount - 1 -> {
|
||||
outRect.left = 6.7f.dpToPx().toInt()
|
||||
outRect.right = 0
|
||||
}
|
||||
|
||||
else -> {
|
||||
outRect.left = 6.7f.dpToPx().toInt()
|
||||
outRect.right = 6.7f.dpToPx().toInt()
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
binding.rvMyStash.adapter = orderListAdapter
|
||||
binding.tvMyStashViewAll.setOnClickListener {
|
||||
startActivity(Intent(requireContext(), AudioContentOrderListActivity::class.java))
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupNewContentTheme() {
|
||||
newContentThemeAdapter = AudioContentMainNewContentThemeAdapter {
|
||||
viewModel.getNewContentOfTheme(theme = it)
|
||||
}
|
||||
|
||||
binding.rvNewContentTheme.layoutManager = LinearLayoutManager(
|
||||
context,
|
||||
LinearLayoutManager.HORIZONTAL,
|
||||
false
|
||||
)
|
||||
|
||||
binding.rvNewContentTheme.addItemDecoration(object : RecyclerView.ItemDecoration() {
|
||||
override fun getItemOffsets(
|
||||
outRect: Rect,
|
||||
view: View,
|
||||
parent: RecyclerView,
|
||||
state: RecyclerView.State
|
||||
) {
|
||||
super.getItemOffsets(outRect, view, parent, state)
|
||||
|
||||
when (parent.getChildAdapterPosition(view)) {
|
||||
0 -> {
|
||||
outRect.left = 0
|
||||
outRect.right = 4f.dpToPx().toInt()
|
||||
}
|
||||
|
||||
orderListAdapter.itemCount - 1 -> {
|
||||
outRect.left = 4f.dpToPx().toInt()
|
||||
outRect.right = 0
|
||||
}
|
||||
|
||||
else -> {
|
||||
outRect.left = 4f.dpToPx().toInt()
|
||||
outRect.right = 4f.dpToPx().toInt()
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
binding.rvNewContentTheme.adapter = newContentThemeAdapter
|
||||
}
|
||||
|
||||
private fun setupNewContent() {
|
||||
newContentAdapter = AudioContentMainContentAdapter(
|
||||
onClickItem = {
|
||||
startActivity(
|
||||
Intent(requireContext(), AudioContentDetailActivity::class.java).apply {
|
||||
putExtra(Constants.EXTRA_AUDIO_CONTENT_ID, it)
|
||||
}
|
||||
)
|
||||
},
|
||||
onClickCreator = {
|
||||
startActivity(
|
||||
Intent(requireContext(), UserProfileActivity::class.java).apply {
|
||||
putExtra(Constants.EXTRA_USER_ID, it)
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
binding.rvNewContent.layoutManager = LinearLayoutManager(
|
||||
context,
|
||||
LinearLayoutManager.HORIZONTAL,
|
||||
false
|
||||
)
|
||||
|
||||
binding.rvNewContent.addItemDecoration(object : RecyclerView.ItemDecoration() {
|
||||
override fun getItemOffsets(
|
||||
outRect: Rect,
|
||||
view: View,
|
||||
parent: RecyclerView,
|
||||
state: RecyclerView.State
|
||||
) {
|
||||
super.getItemOffsets(outRect, view, parent, state)
|
||||
|
||||
when (parent.getChildAdapterPosition(view)) {
|
||||
0 -> {
|
||||
outRect.left = 0
|
||||
outRect.right = 6.7f.dpToPx().toInt()
|
||||
}
|
||||
|
||||
orderListAdapter.itemCount - 1 -> {
|
||||
outRect.left = 6.7f.dpToPx().toInt()
|
||||
outRect.right = 0
|
||||
}
|
||||
|
||||
else -> {
|
||||
outRect.left = 6.7f.dpToPx().toInt()
|
||||
outRect.right = 6.7f.dpToPx().toInt()
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
binding.rvNewContent.adapter = newContentAdapter
|
||||
}
|
||||
|
||||
private fun setupCuration() {
|
||||
curationAdapter = AudioContentMainCurationAdapter(
|
||||
onClickItem = {
|
||||
startActivity(
|
||||
Intent(requireContext(), AudioContentDetailActivity::class.java).apply {
|
||||
putExtra(Constants.EXTRA_AUDIO_CONTENT_ID, it)
|
||||
}
|
||||
)
|
||||
},
|
||||
onClickCreator = {
|
||||
startActivity(
|
||||
Intent(requireContext(), UserProfileActivity::class.java).apply {
|
||||
putExtra(Constants.EXTRA_USER_ID, it)
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
binding.rvCuration.layoutManager = LinearLayoutManager(
|
||||
context,
|
||||
LinearLayoutManager.VERTICAL,
|
||||
false
|
||||
)
|
||||
|
||||
binding.rvCuration.addItemDecoration(object : RecyclerView.ItemDecoration() {
|
||||
override fun getItemOffsets(
|
||||
outRect: Rect,
|
||||
view: View,
|
||||
parent: RecyclerView,
|
||||
state: RecyclerView.State
|
||||
) {
|
||||
super.getItemOffsets(outRect, view, parent, state)
|
||||
|
||||
when (parent.getChildAdapterPosition(view)) {
|
||||
0 -> {
|
||||
outRect.top = 40f.dpToPx().toInt()
|
||||
outRect.bottom = 20f.dpToPx().toInt()
|
||||
}
|
||||
|
||||
curationAdapter.itemCount - 1 -> {
|
||||
outRect.top = 20f.dpToPx().toInt()
|
||||
outRect.bottom = 40f.dpToPx().toInt()
|
||||
}
|
||||
|
||||
else -> {
|
||||
outRect.top = 20f.dpToPx().toInt()
|
||||
outRect.bottom = 20f.dpToPx().toInt()
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
binding.rvCuration.adapter = curationAdapter
|
||||
}
|
||||
|
||||
private fun bindData() {
|
||||
viewModel.isLoading.observe(viewLifecycleOwner) {
|
||||
if (it) {
|
||||
loadingDialog.show(screenWidth)
|
||||
} else {
|
||||
loadingDialog.dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
viewModel.toastLiveData.observe(viewLifecycleOwner) {
|
||||
it?.let { Toast.makeText(requireContext(), it, Toast.LENGTH_LONG).show() }
|
||||
}
|
||||
|
||||
viewModel.newContentUploadCreatorListLiveData.observe(viewLifecycleOwner) {
|
||||
newContentCreatorAdapter.addItems(it)
|
||||
binding.rvNewContentCreator.visibility = if (
|
||||
newContentCreatorAdapter.itemCount <= 0 && it.isEmpty()
|
||||
) {
|
||||
View.GONE
|
||||
} else {
|
||||
View.VISIBLE
|
||||
}
|
||||
}
|
||||
|
||||
viewModel.bannerLiveData.observe(viewLifecycleOwner) {
|
||||
if (bannerAdapter.itemCount <= 0 && it.isEmpty()) {
|
||||
binding.rvBanner.visibility = View.GONE
|
||||
binding.indicatorBanner.visibility = View.GONE
|
||||
} else {
|
||||
binding.rvBanner.visibility = View.VISIBLE
|
||||
binding.indicatorBanner.visibility = View.VISIBLE
|
||||
binding.rvBanner.refreshData(it)
|
||||
}
|
||||
}
|
||||
|
||||
viewModel.orderListLiveData.observe(viewLifecycleOwner) {
|
||||
orderListAdapter.addItems(it)
|
||||
binding.llMyStash.visibility = if (
|
||||
orderListAdapter.itemCount <= 0 && it.isEmpty()
|
||||
) {
|
||||
View.GONE
|
||||
} else {
|
||||
View.VISIBLE
|
||||
}
|
||||
}
|
||||
|
||||
viewModel.newContentListLiveData.observe(viewLifecycleOwner) {
|
||||
newContentAdapter.addItems(it)
|
||||
}
|
||||
|
||||
viewModel.themeListLiveData.observe(viewLifecycleOwner) {
|
||||
binding.llNewContent.visibility = View.VISIBLE
|
||||
newContentThemeAdapter.addItems(it)
|
||||
}
|
||||
|
||||
viewModel.curationListLiveData.observe(viewLifecycleOwner) {
|
||||
curationAdapter.addItems(it)
|
||||
binding.rvCuration.visibility = if (
|
||||
curationAdapter.itemCount <= 0 && it.isEmpty()
|
||||
) {
|
||||
View.GONE
|
||||
} else {
|
||||
View.VISIBLE
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
-35
@@ -1,35 +0,0 @@
|
||||
package kr.co.vividnext.sodalive.audio_content.main
|
||||
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import coil.load
|
||||
import coil.transform.CircleCropTransformation
|
||||
import coil.transform.RoundedCornersTransformation
|
||||
import kr.co.vividnext.sodalive.R
|
||||
import kr.co.vividnext.sodalive.databinding.ItemAudioContentMainBinding
|
||||
import kr.co.vividnext.sodalive.extensions.dpToPx
|
||||
|
||||
class AudioContentMainItemViewHolder(
|
||||
private val binding: ItemAudioContentMainBinding,
|
||||
private val onClickItem: (Long) -> Unit,
|
||||
private val onClickCreator: (Long) -> Unit
|
||||
) : RecyclerView.ViewHolder(binding.root) {
|
||||
fun bind(item: GetAudioContentMainItem) {
|
||||
binding.ivAudioContentCoverImage.load(item.coverImageUrl) {
|
||||
crossfade(true)
|
||||
placeholder(R.drawable.ic_place_holder)
|
||||
transformations(RoundedCornersTransformation(2.7f.dpToPx()))
|
||||
}
|
||||
|
||||
binding.ivAudioContentCreator.load(item.creatorProfileImageUrl) {
|
||||
crossfade(true)
|
||||
placeholder(R.drawable.ic_place_holder)
|
||||
transformations(CircleCropTransformation())
|
||||
}
|
||||
|
||||
binding.tvAudioContentTitle.text = item.title
|
||||
binding.tvAudioContentCreatorNickname.text = item.creatorNickname
|
||||
|
||||
binding.ivAudioContentCreator.setOnClickListener { onClickCreator(item.creatorId) }
|
||||
binding.root.setOnClickListener { onClickItem(item.contentId) }
|
||||
}
|
||||
}
|
||||
-52
@@ -1,52 +0,0 @@
|
||||
package kr.co.vividnext.sodalive.audio_content.main
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import coil.load
|
||||
import coil.transform.CircleCropTransformation
|
||||
import kr.co.vividnext.sodalive.R
|
||||
import kr.co.vividnext.sodalive.databinding.ItemAudioContentMainNewContentCreatorBinding
|
||||
|
||||
class AudioContentMainNewContentCreatorAdapter(
|
||||
private val onClickItem: (Long) -> Unit
|
||||
) : RecyclerView.Adapter<AudioContentMainNewContentCreatorAdapter.ViewHolder>() {
|
||||
|
||||
private val items = mutableListOf<GetNewContentUploadCreator>()
|
||||
|
||||
inner class ViewHolder(
|
||||
private val binding: ItemAudioContentMainNewContentCreatorBinding
|
||||
) : RecyclerView.ViewHolder(binding.root) {
|
||||
fun bind(item: GetNewContentUploadCreator) {
|
||||
binding.tvNewContentCreator.text = item.creatorNickname
|
||||
binding.ivNewContentCreator.load(item.creatorProfileImageUrl) {
|
||||
crossfade(true)
|
||||
placeholder(R.drawable.ic_place_holder)
|
||||
transformations(CircleCropTransformation())
|
||||
}
|
||||
binding.root.setOnClickListener { onClickItem(item.creatorId) }
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ViewHolder(
|
||||
ItemAudioContentMainNewContentCreatorBinding.inflate(
|
||||
LayoutInflater.from(parent.context),
|
||||
parent,
|
||||
false
|
||||
)
|
||||
)
|
||||
|
||||
override fun getItemCount() = items.size
|
||||
|
||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||
holder.bind(items[position])
|
||||
}
|
||||
|
||||
@SuppressLint("NotifyDataSetChanged")
|
||||
fun addItems(items: List<GetNewContentUploadCreator>) {
|
||||
this.items.clear()
|
||||
this.items.addAll(items)
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
}
|
||||
-121
@@ -1,121 +0,0 @@
|
||||
package kr.co.vividnext.sodalive.audio_content.main
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import com.orhanobut.logger.Logger
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import kr.co.vividnext.sodalive.audio_content.AudioContentRepository
|
||||
import kr.co.vividnext.sodalive.base.BaseViewModel
|
||||
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
|
||||
|
||||
class AudioContentMainViewModel(
|
||||
private val repository: AudioContentRepository
|
||||
) : BaseViewModel() {
|
||||
private val _toastLiveData = MutableLiveData<String?>()
|
||||
val toastLiveData: LiveData<String?>
|
||||
get() = _toastLiveData
|
||||
|
||||
private var _isLoading = MutableLiveData(false)
|
||||
val isLoading: LiveData<Boolean>
|
||||
get() = _isLoading
|
||||
|
||||
private var _newContentUploadCreatorListLiveData =
|
||||
MutableLiveData<List<GetNewContentUploadCreator>>()
|
||||
val newContentUploadCreatorListLiveData: LiveData<List<GetNewContentUploadCreator>>
|
||||
get() = _newContentUploadCreatorListLiveData
|
||||
|
||||
private var _newContentListLiveData = MutableLiveData<List<GetAudioContentMainItem>>()
|
||||
val newContentListLiveData: LiveData<List<GetAudioContentMainItem>>
|
||||
get() = _newContentListLiveData
|
||||
|
||||
private var _bannerLiveData = MutableLiveData<List<GetAudioContentBannerResponse>>()
|
||||
val bannerLiveData: LiveData<List<GetAudioContentBannerResponse>>
|
||||
get() = _bannerLiveData
|
||||
|
||||
private var _orderListLiveData = MutableLiveData<List<GetAudioContentMainItem>>()
|
||||
val orderListLiveData: LiveData<List<GetAudioContentMainItem>>
|
||||
get() = _orderListLiveData
|
||||
|
||||
private var _themeListLiveData = MutableLiveData<List<String>>()
|
||||
val themeListLiveData: LiveData<List<String>>
|
||||
get() = _themeListLiveData
|
||||
|
||||
private var _curationListLiveData = MutableLiveData<List<GetAudioContentCurationResponse>>()
|
||||
val curationListLiveData: LiveData<List<GetAudioContentCurationResponse>>
|
||||
get() = _curationListLiveData
|
||||
|
||||
fun getMain() {
|
||||
_isLoading.value = true
|
||||
compositeDisposable.add(
|
||||
repository.getMain(token = "Bearer ${SharedPreferenceManager.token}")
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
{
|
||||
if (it.success && it.data != null) {
|
||||
val data = it.data
|
||||
_newContentUploadCreatorListLiveData.value =
|
||||
data.newContentUploadCreatorList
|
||||
_newContentListLiveData.value = data.newContentList
|
||||
_orderListLiveData.value = data.orderList
|
||||
_bannerLiveData.value = data.bannerList
|
||||
_curationListLiveData.value = data.curationList
|
||||
|
||||
val themeList = listOf("전체").union(data.themeList).toList()
|
||||
_themeListLiveData.value = themeList
|
||||
} else {
|
||||
if (it.message != null) {
|
||||
_toastLiveData.postValue(it.message)
|
||||
} else {
|
||||
_toastLiveData.postValue(
|
||||
"알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
_isLoading.value = false
|
||||
},
|
||||
{
|
||||
_isLoading.value = false
|
||||
it.message?.let { message -> Logger.e(message) }
|
||||
_toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
fun getNewContentOfTheme(theme: String) {
|
||||
compositeDisposable.add(
|
||||
repository.getNewContentOfTheme(
|
||||
theme = if (theme == "전체") {
|
||||
""
|
||||
} else {
|
||||
theme
|
||||
},
|
||||
token = "Bearer ${SharedPreferenceManager.token}"
|
||||
)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
{
|
||||
if (it.success && it.data != null) {
|
||||
_newContentListLiveData.value = it.data!!
|
||||
} else {
|
||||
if (it.message != null) {
|
||||
_toastLiveData.postValue(it.message)
|
||||
} else {
|
||||
_toastLiveData.postValue(
|
||||
"알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
it.message?.let { message -> Logger.e(message) }
|
||||
_toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
+35
-19
@@ -1,45 +1,58 @@
|
||||
package kr.co.vividnext.sodalive.audio_content.main
|
||||
|
||||
import androidx.annotation.Keep
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import kr.co.vividnext.sodalive.settings.event.EventItem
|
||||
|
||||
data class GetAudioContentMainResponse(
|
||||
@SerializedName("newContentUploadCreatorList")
|
||||
val newContentUploadCreatorList: List<GetNewContentUploadCreator>,
|
||||
@SerializedName("bannerList") val bannerList: List<GetAudioContentBannerResponse>,
|
||||
@SerializedName("orderList") val orderList: List<GetAudioContentMainItem>,
|
||||
@SerializedName("themeList") val themeList: List<String>,
|
||||
@SerializedName("newContentList") val newContentList: List<GetAudioContentMainItem>,
|
||||
@SerializedName("curationList") val curationList: List<GetAudioContentCurationResponse>
|
||||
)
|
||||
|
||||
data class GetNewContentUploadCreator(
|
||||
@SerializedName("creatorId") val creatorId: Long,
|
||||
@SerializedName("creatorNickname") val creatorNickname: String,
|
||||
@SerializedName("creatorProfileImageUrl") val creatorProfileImageUrl: String
|
||||
)
|
||||
|
||||
@Keep
|
||||
data class GetAudioContentMainItem(
|
||||
@SerializedName("contentId") val contentId: Long,
|
||||
@SerializedName("coverImageUrl") val coverImageUrl: String,
|
||||
@SerializedName("title") val title: String,
|
||||
@SerializedName("isAdult") val isAdult: Boolean,
|
||||
@SerializedName("creatorId") val creatorId: Long,
|
||||
@SerializedName("creatorProfileImageUrl") val creatorProfileImageUrl: String,
|
||||
@SerializedName("creatorNickname") val creatorNickname: String
|
||||
@SerializedName("creatorNickname") val creatorNickname: String,
|
||||
@SerializedName("price") val price: Int,
|
||||
@SerializedName("duration") val duration: String,
|
||||
@SerializedName("isPointAvailable") val isPointAvailable: Boolean
|
||||
)
|
||||
|
||||
@Keep
|
||||
data class GetAudioContentRanking(
|
||||
@SerializedName("startDate") val startDate: String,
|
||||
@SerializedName("endDate") val endDate: String,
|
||||
@SerializedName("items") val items: List<GetAudioContentRankingItem>
|
||||
)
|
||||
|
||||
@Keep
|
||||
data class GetAudioContentRankingItem(
|
||||
@SerializedName("contentId") val contentId: Long,
|
||||
@SerializedName("title") val title: String,
|
||||
@SerializedName("coverImageUrl") val coverImageUrl: String,
|
||||
@SerializedName("themeStr") val themeStr: String,
|
||||
@SerializedName("price") val price: Int,
|
||||
@SerializedName("duration") val duration: String,
|
||||
@SerializedName("creatorId") val creatorId: Long,
|
||||
@SerializedName("creatorNickname") val creatorNickname: String,
|
||||
@SerializedName("isPointAvailable") val isPointAvailable: Boolean,
|
||||
@SerializedName("creatorProfileImageUrl") val creatorProfileImageUrl: String
|
||||
)
|
||||
|
||||
@Keep
|
||||
data class GetAudioContentCurationResponse(
|
||||
@SerializedName("curationId") val curationId: Long,
|
||||
@SerializedName("title") val title: String,
|
||||
@SerializedName("description") val description: String,
|
||||
@SerializedName("contents") val audioContents: List<GetAudioContentMainItem>
|
||||
)
|
||||
|
||||
@Keep
|
||||
data class GetAudioContentBannerResponse(
|
||||
@SerializedName("type") val type: AudioContentBannerType,
|
||||
@SerializedName("thumbnailImageUrl") val thumbnailImageUrl: String,
|
||||
@SerializedName("eventItem") val eventItem: EventItem?,
|
||||
@SerializedName("creatorId") val creatorId: Long?,
|
||||
@SerializedName("seriesId") val seriesId: Long?,
|
||||
@SerializedName("link") val link: String?
|
||||
)
|
||||
|
||||
@@ -51,5 +64,8 @@ enum class AudioContentBannerType {
|
||||
CREATOR,
|
||||
|
||||
@SerializedName("LINK")
|
||||
LINK
|
||||
LINK,
|
||||
|
||||
@SerializedName("SERIES")
|
||||
SERIES
|
||||
}
|
||||
|
||||
+2
-1
@@ -1,4 +1,4 @@
|
||||
package kr.co.vividnext.sodalive.audio_content.main
|
||||
package kr.co.vividnext.sodalive.audio_content.main.banner
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
@@ -11,6 +11,7 @@ import com.bumptech.glide.request.transition.Transition
|
||||
import com.zhpan.bannerview.BaseBannerAdapter
|
||||
import com.zhpan.bannerview.BaseViewHolder
|
||||
import kr.co.vividnext.sodalive.R
|
||||
import kr.co.vividnext.sodalive.audio_content.main.GetAudioContentBannerResponse
|
||||
|
||||
class AudioContentMainBannerAdapter(
|
||||
private val context: Context,
|
||||
+11
-5
@@ -1,4 +1,4 @@
|
||||
package kr.co.vividnext.sodalive.audio_content.main
|
||||
package kr.co.vividnext.sodalive.audio_content.main.new_content
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
@@ -7,6 +7,7 @@ import android.view.ViewGroup
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import kr.co.vividnext.sodalive.R
|
||||
import kr.co.vividnext.sodalive.common.SodaLiveApplicationHolder
|
||||
import kr.co.vividnext.sodalive.databinding.ItemAudioContentMainNewContentThemeBinding
|
||||
|
||||
class AudioContentMainNewContentThemeAdapter(
|
||||
@@ -22,11 +23,17 @@ class AudioContentMainNewContentThemeAdapter(
|
||||
) : RecyclerView.ViewHolder(binding.root) {
|
||||
@SuppressLint("NotifyDataSetChanged")
|
||||
fun bind(theme: String) {
|
||||
if (theme == selectedTheme || (selectedTheme == "" && theme == "전체")) {
|
||||
if (
|
||||
theme == selectedTheme ||
|
||||
(selectedTheme == "" && theme == SodaLiveApplicationHolder.get()
|
||||
.getString(R.string.audio_content_label_all)) ||
|
||||
(selectedTheme == "" && theme == SodaLiveApplicationHolder.get()
|
||||
.getString(R.string.screen_home_sort_revenue))
|
||||
) {
|
||||
binding.tvTheme.setBackgroundResource(
|
||||
R.drawable.bg_round_corner_16_7_transparent_9970ff
|
||||
R.drawable.bg_round_corner_16_7_transparent_3bb9f1
|
||||
)
|
||||
binding.tvTheme.setTextColor(ContextCompat.getColor(context, R.color.color_9970ff))
|
||||
binding.tvTheme.setTextColor(ContextCompat.getColor(context, R.color.color_3bb9f1))
|
||||
} else {
|
||||
binding.tvTheme.setBackgroundResource(
|
||||
R.drawable.bg_round_corner_16_7_transparent_777777
|
||||
@@ -45,7 +52,6 @@ class AudioContentMainNewContentThemeAdapter(
|
||||
|
||||
@SuppressLint("NotifyDataSetChanged")
|
||||
fun addItems(themeList: List<String>) {
|
||||
this.selectedTheme = ""
|
||||
this.themeList.clear()
|
||||
this.themeList.addAll(themeList)
|
||||
notifyDataSetChanged()
|
||||
+135
-61
@@ -2,16 +2,18 @@ package kr.co.vividnext.sodalive.audio_content.modify
|
||||
|
||||
import android.Manifest
|
||||
import android.annotation.SuppressLint
|
||||
import android.graphics.Bitmap
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.widget.Toast
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.setPadding
|
||||
import coil.load
|
||||
import coil.transform.RoundedCornersTransformation
|
||||
import com.github.dhaval2404.imagepicker.ImagePicker
|
||||
import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.load.resource.bitmap.RoundedCorners
|
||||
import com.bumptech.glide.request.RequestOptions
|
||||
import com.gun0912.tedpermission.PermissionListener
|
||||
import com.gun0912.tedpermission.normal.TedPermission
|
||||
import com.jakewharton.rxbinding4.widget.textChanges
|
||||
@@ -20,8 +22,10 @@ import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import kr.co.vividnext.sodalive.R
|
||||
import kr.co.vividnext.sodalive.base.BaseActivity
|
||||
import kr.co.vividnext.sodalive.common.Constants
|
||||
import kr.co.vividnext.sodalive.common.ImagePickerCropper
|
||||
import kr.co.vividnext.sodalive.common.LoadingDialog
|
||||
import kr.co.vividnext.sodalive.common.RealPathUtil
|
||||
import kr.co.vividnext.sodalive.common.SodaLiveApplicationHolder
|
||||
import kr.co.vividnext.sodalive.databinding.ActivityAudioContentModifyBinding
|
||||
import kr.co.vividnext.sodalive.extensions.dpToPx
|
||||
import org.koin.android.ext.android.inject
|
||||
@@ -33,42 +37,18 @@ class AudioContentModifyActivity : BaseActivity<ActivityAudioContentModifyBindin
|
||||
private val viewModel: AudioContentModifyViewModel by inject()
|
||||
|
||||
private lateinit var loadingDialog: LoadingDialog
|
||||
|
||||
private val imageResult = registerForActivityResult(
|
||||
ActivityResultContracts.StartActivityForResult()
|
||||
) { result ->
|
||||
val resultCode = result.resultCode
|
||||
val data = result.data
|
||||
|
||||
if (resultCode == RESULT_OK) {
|
||||
val fileUri = data?.data
|
||||
|
||||
if (fileUri != null) {
|
||||
binding.ivCover.setPadding(0)
|
||||
binding.ivCover.background = null
|
||||
binding.ivCover.load(fileUri) {
|
||||
crossfade(true)
|
||||
placeholder(R.drawable.bg_placeholder)
|
||||
transformations(RoundedCornersTransformation(13.3f.dpToPx()))
|
||||
}
|
||||
viewModel.coverImageUri = fileUri
|
||||
} else {
|
||||
Toast.makeText(
|
||||
this,
|
||||
"잘못된 파일입니다.\n다시 선택해 주세요.",
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
} else if (resultCode == ImagePicker.RESULT_ERROR) {
|
||||
Toast.makeText(this, ImagePicker.getError(data), Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
private lateinit var cropper: ImagePickerCropper
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
val audioContentId = intent.getLongExtra(Constants.EXTRA_AUDIO_CONTENT_ID, 0)
|
||||
if (audioContentId <= 0) {
|
||||
Toast.makeText(applicationContext, "잘못된 요청입니다.", Toast.LENGTH_LONG).show()
|
||||
Toast.makeText(
|
||||
applicationContext,
|
||||
SodaLiveApplicationHolder.get()
|
||||
.getString(R.string.screen_audio_content_error_invalid_request),
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
finish()
|
||||
}
|
||||
|
||||
@@ -82,24 +62,53 @@ class AudioContentModifyActivity : BaseActivity<ActivityAudioContentModifyBindin
|
||||
viewModel.getAudioContentDetail(audioContentId = audioContentId) { finish() }
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
cropper.cleanup()
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
override fun setupView() {
|
||||
loadingDialog = LoadingDialog(this, layoutInflater)
|
||||
|
||||
cropper = ImagePickerCropper(
|
||||
caller = this,
|
||||
context = this,
|
||||
excludeGif = true,
|
||||
isEnabledFreeStyleCrop = true,
|
||||
config = ImagePickerCropper.Config(
|
||||
aspectX = 1f, aspectY = 1f,
|
||||
compressFormat = Bitmap.CompressFormat.JPEG,
|
||||
compressQuality = 90
|
||||
),
|
||||
onSuccess = { file, uri ->
|
||||
binding.ivCover.setPadding(0)
|
||||
binding.ivCover.background = null
|
||||
Glide.with(this)
|
||||
.load(uri)
|
||||
.placeholder(R.drawable.ic_place_holder)
|
||||
.apply(
|
||||
RequestOptions().transform(
|
||||
RoundedCorners(
|
||||
13.3f.dpToPx().toInt()
|
||||
)
|
||||
)
|
||||
)
|
||||
.into(binding.ivCover)
|
||||
|
||||
viewModel.coverImageFile = file
|
||||
},
|
||||
onError = { e ->
|
||||
Toast.makeText(this, "${e.message}", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
)
|
||||
|
||||
binding.toolbar.tvBack.text = "콘텐츠 수정"
|
||||
binding.toolbar.tvBack.setOnClickListener { finish() }
|
||||
|
||||
binding.ivPhotoPicker.setOnClickListener {
|
||||
ImagePicker.with(this)
|
||||
.crop()
|
||||
.galleryOnly()
|
||||
.galleryMimeTypes( // Exclude gif images
|
||||
mimeTypes = arrayOf(
|
||||
"image/png",
|
||||
"image/jpg",
|
||||
"image/jpeg"
|
||||
)
|
||||
)
|
||||
.createIntent { imageResult.launch(it) }
|
||||
}
|
||||
binding.ivPhotoPicker.setOnClickListener { cropper.launch() }
|
||||
|
||||
binding.llAvailablePoint.setOnClickListener { viewModel.setAvailablePoint(true) }
|
||||
binding.llNotAvailablePoint.setOnClickListener { viewModel.setAvailablePoint(false) }
|
||||
|
||||
binding.llCommentNo.setOnClickListener { viewModel.setAvailableComment(false) }
|
||||
binding.llCommentYes.setOnClickListener { viewModel.setAvailableComment(true) }
|
||||
@@ -112,7 +121,7 @@ class AudioContentModifyActivity : BaseActivity<ActivityAudioContentModifyBindin
|
||||
|
||||
private fun checkPermissions() {
|
||||
val permissions = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
listOf(Manifest.permission.READ_MEDIA_AUDIO, Manifest.permission.READ_MEDIA_IMAGES)
|
||||
listOf(Manifest.permission.READ_MEDIA_AUDIO)
|
||||
} else {
|
||||
listOf(Manifest.permission.READ_EXTERNAL_STORAGE)
|
||||
}
|
||||
@@ -152,6 +161,15 @@ class AudioContentModifyActivity : BaseActivity<ActivityAudioContentModifyBindin
|
||||
}
|
||||
)
|
||||
|
||||
compositeDisposable.add(
|
||||
binding.etTag.textChanges().skip(1)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe {
|
||||
viewModel.tags = it.toString()
|
||||
}
|
||||
)
|
||||
|
||||
viewModel.toastLiveData.observe(this) {
|
||||
it?.let { Toast.makeText(applicationContext, it, Toast.LENGTH_LONG).show() }
|
||||
}
|
||||
@@ -164,6 +182,14 @@ class AudioContentModifyActivity : BaseActivity<ActivityAudioContentModifyBindin
|
||||
}
|
||||
}
|
||||
|
||||
viewModel.isAvailablePointLiveData.observe(this) {
|
||||
if (it) {
|
||||
checkAvailablePoint()
|
||||
} else {
|
||||
checkNotAvailablePoint()
|
||||
}
|
||||
}
|
||||
|
||||
viewModel.isAvailableCommentLiveData.observe(this) {
|
||||
if (it) {
|
||||
binding.ivCommentYes.visibility = View.VISIBLE
|
||||
@@ -173,17 +199,17 @@ class AudioContentModifyActivity : BaseActivity<ActivityAudioContentModifyBindin
|
||||
R.color.color_eeeeee
|
||||
)
|
||||
)
|
||||
binding.llCommentYes.setBackgroundResource(R.drawable.bg_round_corner_6_7_9970ff)
|
||||
binding.llCommentYes.setBackgroundResource(R.drawable.bg_round_corner_6_7_3bb9f1)
|
||||
|
||||
binding.ivCommentNo.visibility = View.GONE
|
||||
binding.tvCommentNo.setTextColor(
|
||||
ContextCompat.getColor(
|
||||
applicationContext,
|
||||
R.color.color_9970ff
|
||||
R.color.color_3bb9f1
|
||||
)
|
||||
)
|
||||
binding.llCommentNo.setBackgroundResource(
|
||||
R.drawable.bg_round_corner_6_7_1f1734_9970ff
|
||||
R.drawable.bg_round_corner_6_7_13181b_3bb9f1
|
||||
)
|
||||
} else {
|
||||
binding.ivCommentNo.visibility = View.VISIBLE
|
||||
@@ -193,17 +219,17 @@ class AudioContentModifyActivity : BaseActivity<ActivityAudioContentModifyBindin
|
||||
R.color.color_eeeeee
|
||||
)
|
||||
)
|
||||
binding.llCommentNo.setBackgroundResource(R.drawable.bg_round_corner_6_7_9970ff)
|
||||
binding.llCommentNo.setBackgroundResource(R.drawable.bg_round_corner_6_7_3bb9f1)
|
||||
|
||||
binding.ivCommentYes.visibility = View.GONE
|
||||
binding.tvCommentYes.setTextColor(
|
||||
ContextCompat.getColor(
|
||||
applicationContext,
|
||||
R.color.color_9970ff
|
||||
R.color.color_3bb9f1
|
||||
)
|
||||
)
|
||||
binding.llCommentYes
|
||||
.setBackgroundResource(R.drawable.bg_round_corner_6_7_1f1734_9970ff)
|
||||
.setBackgroundResource(R.drawable.bg_round_corner_6_7_13181b_3bb9f1)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -219,21 +245,21 @@ class AudioContentModifyActivity : BaseActivity<ActivityAudioContentModifyBindin
|
||||
viewModel.setAdult(true)
|
||||
}
|
||||
|
||||
viewModel.isAdultLiveData.observe(this) {
|
||||
if (it) {
|
||||
viewModel.isAdultLiveData.observe(this) { isAdult ->
|
||||
if (isAdult) {
|
||||
binding.ivAgeAll.visibility = View.GONE
|
||||
binding.llAgeAll.setBackgroundResource(
|
||||
R.drawable.bg_round_corner_6_7_1f1734
|
||||
R.drawable.bg_round_corner_6_7_13181b
|
||||
)
|
||||
binding.tvAgeAll.setTextColor(
|
||||
ContextCompat.getColor(
|
||||
applicationContext,
|
||||
R.color.color_9970ff
|
||||
R.color.color_3bb9f1
|
||||
)
|
||||
)
|
||||
|
||||
binding.ivAge19.visibility = View.VISIBLE
|
||||
binding.llAge19.setBackgroundResource(R.drawable.bg_round_corner_6_7_9970ff)
|
||||
binding.llAge19.setBackgroundResource(R.drawable.bg_round_corner_6_7_3bb9f1)
|
||||
binding.tvAge19.setTextColor(
|
||||
ContextCompat.getColor(
|
||||
applicationContext,
|
||||
@@ -242,17 +268,17 @@ class AudioContentModifyActivity : BaseActivity<ActivityAudioContentModifyBindin
|
||||
)
|
||||
} else {
|
||||
binding.ivAge19.visibility = View.GONE
|
||||
binding.llAge19.setBackgroundResource(R.drawable.bg_round_corner_6_7_1f1734)
|
||||
binding.llAge19.setBackgroundResource(R.drawable.bg_round_corner_6_7_13181b)
|
||||
binding.tvAge19.setTextColor(
|
||||
ContextCompat.getColor(
|
||||
applicationContext,
|
||||
R.color.color_9970ff
|
||||
R.color.color_3bb9f1
|
||||
)
|
||||
)
|
||||
|
||||
binding.ivAgeAll.visibility = View.VISIBLE
|
||||
binding.llAgeAll.setBackgroundResource(
|
||||
R.drawable.bg_round_corner_6_7_9970ff
|
||||
R.drawable.bg_round_corner_6_7_3bb9f1
|
||||
)
|
||||
binding.tvAgeAll.setTextColor(
|
||||
ContextCompat.getColor(
|
||||
@@ -284,5 +310,53 @@ class AudioContentModifyActivity : BaseActivity<ActivityAudioContentModifyBindin
|
||||
viewModel.detailLiveData.observe(this) {
|
||||
binding.etDetail.setText(it)
|
||||
}
|
||||
|
||||
viewModel.tagsLiveData.observe(this) {
|
||||
binding.etTag.setText(it)
|
||||
}
|
||||
}
|
||||
|
||||
private fun checkAvailablePoint() {
|
||||
binding.ivAvailablePoint.visibility = View.VISIBLE
|
||||
binding.tvAvailablePoint.setTextColor(
|
||||
ContextCompat.getColor(
|
||||
applicationContext,
|
||||
R.color.color_eeeeee
|
||||
)
|
||||
)
|
||||
binding.llAvailablePoint.setBackgroundResource(R.drawable.bg_round_corner_6_7_3bb9f1)
|
||||
|
||||
binding.ivNotAvailablePoint.visibility = View.GONE
|
||||
binding.tvNotAvailablePoint.setTextColor(
|
||||
ContextCompat.getColor(
|
||||
applicationContext,
|
||||
R.color.color_3bb9f1
|
||||
)
|
||||
)
|
||||
binding.llNotAvailablePoint.setBackgroundResource(
|
||||
R.drawable.bg_round_corner_6_7_13181b
|
||||
)
|
||||
}
|
||||
|
||||
private fun checkNotAvailablePoint() {
|
||||
binding.ivNotAvailablePoint.visibility = View.VISIBLE
|
||||
binding.tvNotAvailablePoint.setTextColor(
|
||||
ContextCompat.getColor(
|
||||
applicationContext,
|
||||
R.color.color_eeeeee
|
||||
)
|
||||
)
|
||||
binding.llNotAvailablePoint.setBackgroundResource(R.drawable.bg_round_corner_6_7_3bb9f1)
|
||||
|
||||
binding.ivAvailablePoint.visibility = View.GONE
|
||||
binding.tvAvailablePoint.setTextColor(
|
||||
ContextCompat.getColor(
|
||||
applicationContext,
|
||||
R.color.color_3bb9f1
|
||||
)
|
||||
)
|
||||
binding.llAvailablePoint.setBackgroundResource(
|
||||
R.drawable.bg_round_corner_6_7_13181b
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+46
-9
@@ -7,9 +7,11 @@ import com.google.gson.Gson
|
||||
import com.orhanobut.logger.Logger
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import kr.co.vividnext.sodalive.R
|
||||
import kr.co.vividnext.sodalive.audio_content.AudioContentRepository
|
||||
import kr.co.vividnext.sodalive.base.BaseViewModel
|
||||
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
|
||||
import kr.co.vividnext.sodalive.common.SodaLiveApplicationHolder
|
||||
import okhttp3.MediaType
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
import okhttp3.MultipartBody
|
||||
@@ -46,6 +48,10 @@ class AudioContentModifyViewModel(
|
||||
val detailLiveData: LiveData<String>
|
||||
get() = _detailLiveData
|
||||
|
||||
private val _tagsLiveData = MutableLiveData("")
|
||||
val tagsLiveData: LiveData<String>
|
||||
get() = _tagsLiveData
|
||||
|
||||
private val _coverImageLiveData = MutableLiveData("")
|
||||
val coverImageLiveData: LiveData<String>
|
||||
get() = _coverImageLiveData
|
||||
@@ -54,12 +60,18 @@ class AudioContentModifyViewModel(
|
||||
val isAdultShowUiLiveData: LiveData<Boolean>
|
||||
get() = _isAdultShowUiLiveData
|
||||
|
||||
private val _isAvailablePointLiveData = MutableLiveData(false)
|
||||
val isAvailablePointLiveData: LiveData<Boolean>
|
||||
get() = _isAvailablePointLiveData
|
||||
|
||||
lateinit var getRealPathFromURI: (Uri) -> String?
|
||||
|
||||
var contentId: Long = 0
|
||||
var title: String? = null
|
||||
var detail: String? = null
|
||||
var coverImageUri: Uri? = null
|
||||
var tags: String? = null
|
||||
var coverImageFile: File? = null
|
||||
var isPointAvailable: Boolean? = null
|
||||
|
||||
fun setAdult(isAdult: Boolean) {
|
||||
_isAdultLiveData.postValue(isAdult)
|
||||
@@ -69,6 +81,11 @@ class AudioContentModifyViewModel(
|
||||
_isAvailableCommentLiveData.postValue(isAvailableComment)
|
||||
}
|
||||
|
||||
fun setAvailablePoint(isAvailablePoint: Boolean) {
|
||||
isPointAvailable = isAvailablePoint
|
||||
_isAvailablePointLiveData.value = isAvailablePoint
|
||||
}
|
||||
|
||||
fun getAudioContentDetail(audioContentId: Long, onFailure: (() -> Unit)? = null) {
|
||||
this.contentId = audioContentId
|
||||
_isLoading.value = true
|
||||
@@ -85,16 +102,19 @@ class AudioContentModifyViewModel(
|
||||
if (it.success && it.data != null) {
|
||||
_titleLiveData.value = it.data.title
|
||||
_detailLiveData.value = it.data.detail
|
||||
_tagsLiveData.value = it.data.tag
|
||||
_coverImageLiveData.value = it.data.coverImageUrl
|
||||
_isAvailableCommentLiveData.value = it.data.isCommentAvailable
|
||||
_isAdultLiveData.value = it.data.isAdult
|
||||
_isAdultShowUiLiveData.value = !it.data.isAdult
|
||||
_isAvailablePointLiveData.value = it.data.isAvailableUsePoint
|
||||
} else {
|
||||
if (it.message != null) {
|
||||
_toastLiveData.postValue(it.message)
|
||||
} else {
|
||||
_toastLiveData.postValue(
|
||||
"알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
|
||||
SodaLiveApplicationHolder.get()
|
||||
.getString(R.string.common_error_unknown)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -108,7 +128,10 @@ class AudioContentModifyViewModel(
|
||||
{
|
||||
_isLoading.value = false
|
||||
it.message?.let { message -> Logger.e(message) }
|
||||
_toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
|
||||
_toastLiveData.postValue(
|
||||
SodaLiveApplicationHolder.get()
|
||||
.getString(R.string.common_error_unknown)
|
||||
)
|
||||
if (onFailure != null) {
|
||||
onFailure()
|
||||
}
|
||||
@@ -125,14 +148,20 @@ class AudioContentModifyViewModel(
|
||||
contentId = contentId,
|
||||
title = title,
|
||||
detail = detail,
|
||||
tags = if (tags != _tagsLiveData.value!!) {
|
||||
tags
|
||||
} else {
|
||||
null
|
||||
},
|
||||
isAdult = _isAdultLiveData.value!!,
|
||||
isPointAvailable = isPointAvailable,
|
||||
isCommentAvailable = _isAvailableCommentLiveData.value!!
|
||||
)
|
||||
|
||||
val requestJson = Gson().toJson(request)
|
||||
|
||||
val coverImage = if (coverImageUri != null) {
|
||||
val file = File(getRealPathFromURI(coverImageUri!!))
|
||||
val coverImage = if (coverImageFile != null) {
|
||||
val file = coverImageFile!!
|
||||
MultipartBody.Part.createFormData(
|
||||
"coverImage",
|
||||
file.name,
|
||||
@@ -178,7 +207,8 @@ class AudioContentModifyViewModel(
|
||||
_toastLiveData.postValue(it.message)
|
||||
} else {
|
||||
_toastLiveData.postValue(
|
||||
"알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
|
||||
SodaLiveApplicationHolder.get()
|
||||
.getString(R.string.common_error_unknown)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -188,7 +218,8 @@ class AudioContentModifyViewModel(
|
||||
_isLoading.postValue(false)
|
||||
it.message?.let { message -> Logger.e(message) }
|
||||
_toastLiveData.postValue(
|
||||
"알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
|
||||
SodaLiveApplicationHolder.get()
|
||||
.getString(R.string.common_error_unknown)
|
||||
)
|
||||
}
|
||||
)
|
||||
@@ -198,12 +229,18 @@ class AudioContentModifyViewModel(
|
||||
|
||||
private fun validateData(): Boolean {
|
||||
if (title != null && title!!.isBlank()) {
|
||||
_toastLiveData.postValue("제목을 입력해 주세요.")
|
||||
_toastLiveData.postValue(
|
||||
SodaLiveApplicationHolder.get()
|
||||
.getString(R.string.audio_content_upload_error_title_required)
|
||||
)
|
||||
return false
|
||||
}
|
||||
|
||||
if (detail != null && (detail!!.isBlank() || detail!!.length < 5)) {
|
||||
_toastLiveData.postValue("내용을 5자 이상 입력해 주세요.")
|
||||
_toastLiveData.postValue(
|
||||
SodaLiveApplicationHolder.get()
|
||||
.getString(R.string.audio_content_upload_error_detail_min_length)
|
||||
)
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
+4
@@ -1,11 +1,15 @@
|
||||
package kr.co.vividnext.sodalive.audio_content.modify
|
||||
|
||||
import androidx.annotation.Keep
|
||||
import com.google.gson.annotations.SerializedName
|
||||
|
||||
@Keep
|
||||
data class ModifyAudioContentRequest(
|
||||
@SerializedName("contentId") val contentId: Long,
|
||||
@SerializedName("title") val title: String?,
|
||||
@SerializedName("detail") val detail: String?,
|
||||
@SerializedName("tags") val tags: String?,
|
||||
@SerializedName("isAdult") val isAdult: Boolean,
|
||||
@SerializedName("isPointAvailable") val isPointAvailable: Boolean?,
|
||||
@SerializedName("isCommentAvailable") val isCommentAvailable: Boolean
|
||||
)
|
||||
|
||||
+65
-7
@@ -4,15 +4,17 @@ import android.app.Activity
|
||||
import android.graphics.Color
|
||||
import android.graphics.drawable.ColorDrawable
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.WindowManager
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import coil.load
|
||||
import coil.transform.CircleCropTransformation
|
||||
import coil.transform.RoundedCornersTransformation
|
||||
import kr.co.vividnext.sodalive.R
|
||||
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
|
||||
import kr.co.vividnext.sodalive.databinding.DialogAudioContentOrderConfirmBinding
|
||||
import kr.co.vividnext.sodalive.extensions.dpToPx
|
||||
import kotlin.math.ceil
|
||||
import kr.co.vividnext.sodalive.extensions.moneyFormat
|
||||
|
||||
class AudioContentOrderConfirmDialog(
|
||||
activity: Activity,
|
||||
@@ -25,6 +27,7 @@ class AudioContentOrderConfirmDialog(
|
||||
duration: String,
|
||||
orderType: OrderType,
|
||||
price: Int,
|
||||
isAvailableUsePoint: Boolean,
|
||||
confirmButtonClick: () -> Unit,
|
||||
) {
|
||||
|
||||
@@ -33,6 +36,7 @@ class AudioContentOrderConfirmDialog(
|
||||
val dialogView = DialogAudioContentOrderConfirmBinding.inflate(layoutInflater)
|
||||
|
||||
init {
|
||||
val context = dialogView.root.context
|
||||
val dialogBuilder = AlertDialog.Builder(activity)
|
||||
dialogBuilder.setView(dialogView.root)
|
||||
|
||||
@@ -57,18 +61,72 @@ class AudioContentOrderConfirmDialog(
|
||||
}
|
||||
|
||||
dialogView.tvDuration.text = duration
|
||||
dialogView.tvPrice.text = if (orderType == OrderType.RENTAL) {
|
||||
"${ceil(price * 0.6).toInt()}"
|
||||
|
||||
val maxUsablePoint = if (orderType == OrderType.RENTAL && isAvailableUsePoint) {
|
||||
price * 10
|
||||
} else {
|
||||
"$price"
|
||||
0
|
||||
}
|
||||
|
||||
dialogView.tvNotice.text = if (orderType == OrderType.RENTAL) {
|
||||
"콘텐츠를 대여하시겠습니까?\n아래 캔이 차감됩니다."
|
||||
val totalAvailablePoint = if (orderType == OrderType.RENTAL && isAvailableUsePoint) {
|
||||
SharedPreferenceManager.point
|
||||
} else {
|
||||
"콘텐츠를 소장하시겠습니까?\n아래 캔이 차감됩니다."
|
||||
0
|
||||
}
|
||||
|
||||
val usablePoint = (minOf(totalAvailablePoint, maxUsablePoint) / 10) * 10
|
||||
|
||||
if (SharedPreferenceManager.userId == 17958L) {
|
||||
dialogView.ivPoint.visibility = View.GONE
|
||||
dialogView.tvPoint.visibility = View.GONE
|
||||
dialogView.tvPlus.visibility = View.GONE
|
||||
dialogView.ivCan.visibility = View.GONE
|
||||
dialogView.tvCan.text = context.getString(
|
||||
R.string.audio_content_order_price_won_format,
|
||||
(price * 110).moneyFormat()
|
||||
)
|
||||
} else {
|
||||
if (usablePoint > 0) {
|
||||
dialogView.ivPoint.visibility = View.VISIBLE
|
||||
dialogView.tvPoint.visibility = View.VISIBLE
|
||||
dialogView.tvPoint.text = usablePoint.moneyFormat()
|
||||
} else {
|
||||
dialogView.ivPoint.visibility = View.GONE
|
||||
dialogView.tvPoint.visibility = View.GONE
|
||||
}
|
||||
|
||||
val remainingCan = ((price * 10) - usablePoint) / 10
|
||||
|
||||
dialogView.tvPlus.visibility = if (usablePoint > 0 && remainingCan > 0) {
|
||||
View.VISIBLE
|
||||
} else {
|
||||
View.GONE
|
||||
}
|
||||
|
||||
if (remainingCan > 0) {
|
||||
dialogView.ivCan.visibility = View.VISIBLE
|
||||
dialogView.tvCan.visibility = View.VISIBLE
|
||||
dialogView.tvCan.text = remainingCan.moneyFormat()
|
||||
} else {
|
||||
dialogView.ivCan.visibility = View.GONE
|
||||
dialogView.tvCan.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
val noticeResId = when {
|
||||
SharedPreferenceManager.userId == 17958L && orderType == OrderType.RENTAL ->
|
||||
R.string.audio_content_order_confirm_notice_rental_simple
|
||||
|
||||
SharedPreferenceManager.userId == 17958L && orderType == OrderType.KEEP ->
|
||||
R.string.audio_content_order_confirm_notice_keep_simple
|
||||
|
||||
orderType == OrderType.RENTAL ->
|
||||
R.string.audio_content_order_confirm_notice_rental
|
||||
|
||||
else -> R.string.audio_content_order_confirm_notice_keep
|
||||
}
|
||||
dialogView.tvNotice.text = dialogView.root.context.getString(noticeResId)
|
||||
|
||||
dialogView.tvCancel.setOnClickListener {
|
||||
alertDialog.dismiss()
|
||||
}
|
||||
|
||||
+30
-2
@@ -5,7 +5,10 @@ import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
||||
import kr.co.vividnext.sodalive.R
|
||||
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
|
||||
import kr.co.vividnext.sodalive.databinding.FragmentAudioContentOrderBinding
|
||||
import kr.co.vividnext.sodalive.extensions.moneyFormat
|
||||
import kotlin.math.ceil
|
||||
|
||||
class AudioContentOrderFragment(
|
||||
@@ -28,9 +31,34 @@ class AudioContentOrderFragment(
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
binding.tvKeep.text = "$price"
|
||||
binding.tvRental.text = "${ceil(price * 0.6).toInt()}"
|
||||
val context = requireContext()
|
||||
if (SharedPreferenceManager.userId == 17958L) {
|
||||
binding.tvKeepDate.text =
|
||||
context.getString(R.string.audio_content_order_keep_period_special)
|
||||
binding.ivKeepCan.visibility = View.GONE
|
||||
binding.ivRentalCan.visibility = View.GONE
|
||||
} else {
|
||||
binding.tvKeepDate.text =
|
||||
context.getString(R.string.audio_content_order_keep_period_default)
|
||||
binding.ivKeepCan.visibility = View.VISIBLE
|
||||
binding.ivRentalCan.visibility = View.VISIBLE
|
||||
}
|
||||
|
||||
if (SharedPreferenceManager.userId == 17958L) {
|
||||
binding.tvKeep.text = context.getString(
|
||||
R.string.audio_content_order_price_won_format,
|
||||
(price * 110).moneyFormat()
|
||||
)
|
||||
binding.tvRental.text = context.getString(
|
||||
R.string.audio_content_order_price_won_format,
|
||||
(ceil(price * 0.7).toInt() * 110).moneyFormat()
|
||||
)
|
||||
} else {
|
||||
binding.tvKeep.text = price.moneyFormat()
|
||||
binding.tvRental.text = ceil(price * 0.7).toInt().moneyFormat()
|
||||
}
|
||||
|
||||
binding.rlKeep.visibility = View.VISIBLE
|
||||
binding.llKeep.setOnClickListener {
|
||||
onClickKeep()
|
||||
dismiss()
|
||||
|
||||
+8
-79
@@ -8,103 +8,32 @@ import android.view.View
|
||||
import android.widget.Toast
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import kr.co.vividnext.sodalive.R
|
||||
import kr.co.vividnext.sodalive.audio_content.detail.AudioContentDetailActivity
|
||||
import kr.co.vividnext.sodalive.base.BaseActivity
|
||||
import kr.co.vividnext.sodalive.common.Constants
|
||||
import kr.co.vividnext.sodalive.common.LoadingDialog
|
||||
import kr.co.vividnext.sodalive.databinding.ActivityAudioContentOrderListBinding
|
||||
import kr.co.vividnext.sodalive.extensions.dpToPx
|
||||
import org.koin.android.ext.android.inject
|
||||
|
||||
class AudioContentOrderListActivity : BaseActivity<ActivityAudioContentOrderListBinding>(
|
||||
ActivityAudioContentOrderListBinding::inflate
|
||||
) {
|
||||
|
||||
private val viewModel: AudioContentOrderListViewModel by inject()
|
||||
|
||||
private lateinit var loadingDialog: LoadingDialog
|
||||
private lateinit var adapter: AudioContentOrderListAdapter
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
bindData()
|
||||
viewModel.getAudioContentOrderList { finish() }
|
||||
supportFragmentManager.beginTransaction()
|
||||
.replace(R.id.fl_container, AudioContentOrderListFragment())
|
||||
.commit()
|
||||
}
|
||||
|
||||
override fun setupView() {
|
||||
loadingDialog = LoadingDialog(this, layoutInflater)
|
||||
binding.toolbar.tvBack.text = "구매목록"
|
||||
binding.toolbar.tvBack.setOnClickListener { finish() }
|
||||
|
||||
adapter = AudioContentOrderListAdapter {
|
||||
startActivity(
|
||||
Intent(applicationContext, AudioContentDetailActivity::class.java)
|
||||
.apply { putExtra(Constants.EXTRA_AUDIO_CONTENT_ID, it) }
|
||||
)
|
||||
}
|
||||
|
||||
binding.rvOrderList.layoutManager = LinearLayoutManager(
|
||||
applicationContext,
|
||||
LinearLayoutManager.VERTICAL,
|
||||
false
|
||||
)
|
||||
|
||||
binding.rvOrderList.addItemDecoration(object : RecyclerView.ItemDecoration() {
|
||||
override fun getItemOffsets(
|
||||
outRect: Rect,
|
||||
view: View,
|
||||
parent: RecyclerView,
|
||||
state: RecyclerView.State
|
||||
) {
|
||||
super.getItemOffsets(outRect, view, parent, state)
|
||||
|
||||
outRect.left = 13.3f.dpToPx().toInt()
|
||||
outRect.right = 13.3f.dpToPx().toInt()
|
||||
|
||||
when (parent.getChildAdapterPosition(view)) {
|
||||
0 -> {
|
||||
outRect.top = 13.3f.dpToPx().toInt()
|
||||
outRect.bottom = 6.7f.dpToPx().toInt()
|
||||
}
|
||||
|
||||
adapter.itemCount - 1 -> {
|
||||
outRect.top = 6.7f.dpToPx().toInt()
|
||||
outRect.bottom = 13.3f.dpToPx().toInt()
|
||||
}
|
||||
|
||||
else -> {
|
||||
outRect.top = 6.7f.dpToPx().toInt()
|
||||
outRect.bottom = 6.7f.dpToPx().toInt()
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
binding.rvOrderList.adapter = adapter
|
||||
setupToolbar()
|
||||
}
|
||||
|
||||
@SuppressLint("NotifyDataSetChanged")
|
||||
private fun bindData() {
|
||||
viewModel.toastLiveData.observe(this) {
|
||||
it?.let { Toast.makeText(applicationContext, it, Toast.LENGTH_LONG).show() }
|
||||
}
|
||||
|
||||
viewModel.isLoading.observe(this) {
|
||||
if (it) {
|
||||
loadingDialog.show(screenWidth, "")
|
||||
} else {
|
||||
loadingDialog.dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
viewModel.orderList.observe(this) {
|
||||
if (viewModel.page == 2) {
|
||||
adapter.items.clear()
|
||||
}
|
||||
|
||||
adapter.items.addAll(it)
|
||||
adapter.notifyDataSetChanged()
|
||||
}
|
||||
private fun setupToolbar() {
|
||||
binding.toolbar.tvBack.text = getString(R.string.screen_audio_content_order_title)
|
||||
binding.toolbar.tvBack.setOnClickListener { finish() }
|
||||
}
|
||||
}
|
||||
|
||||
+128
@@ -0,0 +1,128 @@
|
||||
package kr.co.vividnext.sodalive.audio_content.order
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Intent
|
||||
import android.graphics.Rect
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import kr.co.vividnext.sodalive.audio_content.detail.AudioContentDetailActivity
|
||||
import kr.co.vividnext.sodalive.base.BaseFragment
|
||||
import kr.co.vividnext.sodalive.common.Constants
|
||||
import kr.co.vividnext.sodalive.common.LoadingDialog
|
||||
import kr.co.vividnext.sodalive.databinding.FragmentAudioContentOrderListBinding
|
||||
import kr.co.vividnext.sodalive.extensions.dpToPx
|
||||
import org.koin.android.ext.android.inject
|
||||
|
||||
class AudioContentOrderListFragment : BaseFragment<FragmentAudioContentOrderListBinding>(
|
||||
FragmentAudioContentOrderListBinding::inflate
|
||||
) {
|
||||
private val viewModel: AudioContentOrderListViewModel by inject()
|
||||
|
||||
private lateinit var loadingDialog: LoadingDialog
|
||||
private lateinit var adapter: AudioContentOrderListAdapter
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
setupView()
|
||||
bindData()
|
||||
viewModel.getAudioContentOrderList { requireActivity().finish() }
|
||||
}
|
||||
|
||||
fun setupView() {
|
||||
loadingDialog = LoadingDialog(requireActivity(), layoutInflater)
|
||||
|
||||
adapter = AudioContentOrderListAdapter {
|
||||
startActivity(
|
||||
Intent(requireContext(), AudioContentDetailActivity::class.java)
|
||||
.apply { putExtra(Constants.EXTRA_AUDIO_CONTENT_ID, it) }
|
||||
)
|
||||
}
|
||||
|
||||
binding.rvOrderList.layoutManager = LinearLayoutManager(
|
||||
requireContext(),
|
||||
LinearLayoutManager.VERTICAL,
|
||||
false
|
||||
)
|
||||
|
||||
binding.rvOrderList.addItemDecoration(object : RecyclerView.ItemDecoration() {
|
||||
override fun getItemOffsets(
|
||||
outRect: Rect,
|
||||
view: View,
|
||||
parent: RecyclerView,
|
||||
state: RecyclerView.State
|
||||
) {
|
||||
super.getItemOffsets(outRect, view, parent, state)
|
||||
|
||||
outRect.left = 13.3f.dpToPx().toInt()
|
||||
outRect.right = 13.3f.dpToPx().toInt()
|
||||
|
||||
when (parent.getChildAdapterPosition(view)) {
|
||||
0 -> {
|
||||
outRect.top = 13.3f.dpToPx().toInt()
|
||||
outRect.bottom = 6.7f.dpToPx().toInt()
|
||||
}
|
||||
|
||||
adapter.itemCount - 1 -> {
|
||||
outRect.top = 6.7f.dpToPx().toInt()
|
||||
outRect.bottom = 13.3f.dpToPx().toInt()
|
||||
}
|
||||
|
||||
else -> {
|
||||
outRect.top = 6.7f.dpToPx().toInt()
|
||||
outRect.bottom = 6.7f.dpToPx().toInt()
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
binding.rvOrderList.addOnScrollListener(object : RecyclerView.OnScrollListener() {
|
||||
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
|
||||
super.onScrolled(recyclerView, dx, dy)
|
||||
|
||||
val lastVisibleItemPosition = (recyclerView.layoutManager as LinearLayoutManager?)!!
|
||||
.findLastCompletelyVisibleItemPosition()
|
||||
val itemTotalCount = recyclerView.adapter!!.itemCount - 1
|
||||
|
||||
// 스크롤이 끝에 도달했는지 확인
|
||||
if (!recyclerView.canScrollVertically(1) &&
|
||||
lastVisibleItemPosition == itemTotalCount
|
||||
) {
|
||||
viewModel.getAudioContentOrderList {}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
binding.rvOrderList.adapter = adapter
|
||||
}
|
||||
|
||||
@SuppressLint("NotifyDataSetChanged")
|
||||
private fun bindData() {
|
||||
viewModel.toastLiveData.observe(viewLifecycleOwner) {
|
||||
it?.let { showToast(it) }
|
||||
}
|
||||
|
||||
viewModel.isLoading.observe(viewLifecycleOwner) {
|
||||
if (it) {
|
||||
loadingDialog.show(screenWidth, "")
|
||||
} else {
|
||||
loadingDialog.dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
viewModel.orderList.observe(viewLifecycleOwner) {
|
||||
if (viewModel.page == 2) {
|
||||
adapter.items.clear()
|
||||
}
|
||||
|
||||
adapter.items.addAll(it)
|
||||
adapter.notifyDataSetChanged()
|
||||
}
|
||||
|
||||
viewModel.totalCount.observe(viewLifecycleOwner) {
|
||||
binding.tvTotalCount.text = it.toString()
|
||||
}
|
||||
}
|
||||
}
|
||||
+45
-36
@@ -5,8 +5,10 @@ import androidx.lifecycle.MutableLiveData
|
||||
import com.orhanobut.logger.Logger
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import kr.co.vividnext.sodalive.R
|
||||
import kr.co.vividnext.sodalive.audio_content.AudioContentRepository
|
||||
import kr.co.vividnext.sodalive.base.BaseViewModel
|
||||
import kr.co.vividnext.sodalive.common.SodaLiveApplicationHolder
|
||||
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
|
||||
|
||||
class AudioContentOrderListViewModel(
|
||||
@@ -25,53 +27,60 @@ class AudioContentOrderListViewModel(
|
||||
val orderList: LiveData<List<GetAudioContentOrderListItem>>
|
||||
get() = _orderList
|
||||
|
||||
private var _totalCount = MutableLiveData<Int>()
|
||||
val totalCount: LiveData<Int>
|
||||
get() = _totalCount
|
||||
|
||||
private var isLast = false
|
||||
var page = 1
|
||||
private val size = 10
|
||||
|
||||
fun getAudioContentOrderList(onFailure: (() -> Unit)? = null) {
|
||||
_isLoading.value = true
|
||||
compositeDisposable.add(
|
||||
repository.getAudioContentOrderList(
|
||||
page = page,
|
||||
size = size,
|
||||
token = "Bearer ${SharedPreferenceManager.token}"
|
||||
)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
{
|
||||
if (it.success && it.data != null) {
|
||||
if (it.data.items.isNotEmpty()) {
|
||||
val unknownError = SodaLiveApplicationHolder.get().getString(R.string.common_error_unknown)
|
||||
if (_isLoading.value == false) {
|
||||
_isLoading.value = true
|
||||
compositeDisposable.add(
|
||||
repository.getAudioContentOrderList(
|
||||
page = page,
|
||||
size = size,
|
||||
token = "Bearer ${SharedPreferenceManager.token}"
|
||||
)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
{
|
||||
if (it.success && it.data != null) {
|
||||
_totalCount.value = it.data.totalCount
|
||||
page += 1
|
||||
_orderList.postValue(it.data.items)
|
||||
if (it.data.items.isNotEmpty()) {
|
||||
_orderList.postValue(it.data.items)
|
||||
} else {
|
||||
_orderList.postValue(listOf())
|
||||
isLast = true
|
||||
}
|
||||
} else {
|
||||
isLast = true
|
||||
}
|
||||
} else {
|
||||
if (it.message != null) {
|
||||
_toastLiveData.postValue(it.message)
|
||||
} else {
|
||||
_toastLiveData.postValue(
|
||||
"알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
|
||||
)
|
||||
}
|
||||
if (it.message != null) {
|
||||
_toastLiveData.postValue(it.message)
|
||||
} else {
|
||||
_toastLiveData.postValue(unknownError)
|
||||
}
|
||||
|
||||
if (onFailure != null) {
|
||||
onFailure()
|
||||
}
|
||||
}
|
||||
_isLoading.value = false
|
||||
},
|
||||
{
|
||||
_isLoading.value = false
|
||||
it.message?.let { message -> Logger.e(message) }
|
||||
_toastLiveData.postValue(unknownError)
|
||||
if (onFailure != null) {
|
||||
onFailure()
|
||||
}
|
||||
}
|
||||
_isLoading.value = false
|
||||
},
|
||||
{
|
||||
_isLoading.value = false
|
||||
it.message?.let { message -> Logger.e(message) }
|
||||
_toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
|
||||
if (onFailure != null) {
|
||||
onFailure()
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+3
@@ -1,12 +1,15 @@
|
||||
package kr.co.vividnext.sodalive.audio_content.order
|
||||
|
||||
import androidx.annotation.Keep
|
||||
import com.google.gson.annotations.SerializedName
|
||||
|
||||
@Keep
|
||||
data class GetAudioContentOrderListResponse(
|
||||
@SerializedName("totalCount") val totalCount: Int,
|
||||
@SerializedName("items") val items: List<GetAudioContentOrderListItem>
|
||||
)
|
||||
|
||||
@Keep
|
||||
data class GetAudioContentOrderListItem(
|
||||
@SerializedName("contentId") val contentId: Long,
|
||||
@SerializedName("coverImageUrl") val coverImageUrl: String,
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
package kr.co.vividnext.sodalive.audio_content.order
|
||||
|
||||
import androidx.annotation.Keep
|
||||
import com.google.gson.annotations.SerializedName
|
||||
|
||||
@Keep
|
||||
data class OrderRequest(
|
||||
@SerializedName("contentId") val contentId: Long,
|
||||
@SerializedName("orderType") val orderType: OrderType,
|
||||
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
package kr.co.vividnext.sodalive.audio_content.player
|
||||
|
||||
import kr.co.vividnext.sodalive.audio_content.AudioContentApi
|
||||
|
||||
class AudioContentGenerateUrlRepository(private val api: AudioContentApi) {
|
||||
fun generateUrl(contentId: Long, token: String) = api.generateUrl(
|
||||
contentId = contentId,
|
||||
authHeader = token
|
||||
)
|
||||
}
|
||||
+498
@@ -0,0 +1,498 @@
|
||||
package kr.co.vividnext.sodalive.audio_content.player
|
||||
|
||||
import android.app.Dialog
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.graphics.Rect
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.SeekBar
|
||||
import android.widget.Toast
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.os.BundleCompat
|
||||
import androidx.media3.common.MediaItem
|
||||
import androidx.media3.common.MediaMetadata
|
||||
import androidx.media3.common.Player
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import androidx.media3.session.MediaController
|
||||
import androidx.media3.session.SessionCommand
|
||||
import androidx.media3.session.SessionResult
|
||||
import androidx.media3.session.SessionToken
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import coil.load
|
||||
import coil.transform.CircleCropTransformation
|
||||
import coil.transform.RoundedCornersTransformation
|
||||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialog
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
||||
import kr.co.vividnext.sodalive.R
|
||||
import kr.co.vividnext.sodalive.audio_content.playlist.detail.AudioContentPlaylistContent
|
||||
import kr.co.vividnext.sodalive.audio_content.playlist.detail.AudioContentPlaylistDetailAdapter
|
||||
import kr.co.vividnext.sodalive.common.Constants
|
||||
import kr.co.vividnext.sodalive.common.LoadingDialog
|
||||
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
|
||||
import kr.co.vividnext.sodalive.common.Utils
|
||||
import kr.co.vividnext.sodalive.databinding.FragmentAudioContentPlayerBinding
|
||||
import kr.co.vividnext.sodalive.extensions.dpToPx
|
||||
import kr.co.vividnext.sodalive.mypage.recent.RecentContentViewModel
|
||||
import kr.co.vividnext.sodalive.mypage.recent.db.RecentContent
|
||||
import org.koin.android.ext.android.inject
|
||||
import org.koin.androidx.viewmodel.ext.android.viewModel
|
||||
|
||||
@UnstableApi
|
||||
class AudioContentPlayerFragment(
|
||||
private val screenWidth: Int,
|
||||
private val playlist: ArrayList<AudioContentPlaylistContent>
|
||||
) : BottomSheetDialogFragment() {
|
||||
|
||||
private lateinit var loadingDialog: LoadingDialog
|
||||
private lateinit var adapter: AudioContentPlaylistDetailAdapter
|
||||
private lateinit var binding: FragmentAudioContentPlayerBinding
|
||||
|
||||
private val viewModel: AudioContentPlayerViewModel by viewModel()
|
||||
private val recentContentViewModel: RecentContentViewModel by inject()
|
||||
|
||||
private var mediaController: MediaController? = null
|
||||
private val handler = Handler(Looper.getMainLooper())
|
||||
private var isUserSeeking = false
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
val dialog = super.onCreateDialog(savedInstanceState) as BottomSheetDialog
|
||||
|
||||
dialog.setOnShowListener {
|
||||
val bottomSheet = dialog.findViewById<View>(
|
||||
com.google.android.material.R.id.design_bottom_sheet
|
||||
)
|
||||
bottomSheet?.let {
|
||||
val layoutParams = it.layoutParams
|
||||
layoutParams.width = ViewGroup.LayoutParams.MATCH_PARENT
|
||||
layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT
|
||||
it.layoutParams = layoutParams
|
||||
|
||||
// BottomSheet를 전체 화면으로 설정
|
||||
val behavior = BottomSheetBehavior.from(it)
|
||||
behavior.state = BottomSheetBehavior.STATE_EXPANDED
|
||||
behavior.skipCollapsed = true
|
||||
behavior.isDraggable = false
|
||||
}
|
||||
}
|
||||
return dialog
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
binding = FragmentAudioContentPlayerBinding.inflate(
|
||||
inflater,
|
||||
container,
|
||||
false
|
||||
)
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
setupView()
|
||||
bindData()
|
||||
connectPlayerService()
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
handler.removeCallbacksAndMessages(null)
|
||||
mediaController?.release()
|
||||
mediaController = null
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
private fun setupView() {
|
||||
loadingDialog = LoadingDialog(requireActivity(), layoutInflater)
|
||||
binding.ivClose.setOnClickListener { dismiss() }
|
||||
binding.ivPlaylist.setOnClickListener {
|
||||
viewModel.toggleShowPlayList()
|
||||
}
|
||||
|
||||
adapter = AudioContentPlaylistDetailAdapter { contentId ->
|
||||
binding.ivLoopSegment.setImageResource(R.drawable.ic_loop_segment_idle)
|
||||
val extras = Bundle().apply {
|
||||
putLong(
|
||||
Constants.EXTRA_AUDIO_CONTENT_ID,
|
||||
contentId
|
||||
)
|
||||
}
|
||||
val sessionCommand = SessionCommand("PLAY_SELECTED_CONTENT", Bundle.EMPTY)
|
||||
mediaController!!.sendCustomCommand(sessionCommand, extras)
|
||||
}
|
||||
|
||||
val recyclerView = binding.rvPlaylistContent
|
||||
recyclerView.setHasFixedSize(true)
|
||||
recyclerView.layoutManager = LinearLayoutManager(
|
||||
requireContext(),
|
||||
LinearLayoutManager.VERTICAL,
|
||||
false
|
||||
)
|
||||
recyclerView.addItemDecoration(object : RecyclerView.ItemDecoration() {
|
||||
override fun getItemOffsets(
|
||||
outRect: Rect,
|
||||
view: View,
|
||||
parent: RecyclerView,
|
||||
state: RecyclerView.State
|
||||
) {
|
||||
super.getItemOffsets(outRect, view, parent, state)
|
||||
|
||||
outRect.left = 0
|
||||
outRect.right = 0
|
||||
|
||||
when (parent.getChildAdapterPosition(view)) {
|
||||
0 -> {
|
||||
outRect.top = 0
|
||||
outRect.bottom = 6.7f.dpToPx().toInt()
|
||||
}
|
||||
|
||||
adapter.itemCount - 1 -> {
|
||||
outRect.top = 6.7f.dpToPx().toInt()
|
||||
outRect.bottom = 0
|
||||
}
|
||||
|
||||
else -> {
|
||||
outRect.top = 6.7f.dpToPx().toInt()
|
||||
outRect.bottom = 6.7f.dpToPx().toInt()
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
recyclerView.adapter = adapter
|
||||
|
||||
binding.ivLoopSegment.setOnClickListener {
|
||||
val sessionCommand = SessionCommand("TOGGLE_SEGMENT_LOOP", Bundle.EMPTY)
|
||||
val resultFuture = mediaController!!.sendCustomCommand(sessionCommand, Bundle.EMPTY)
|
||||
resultFuture.addListener(
|
||||
{
|
||||
val result = resultFuture.get()
|
||||
if (result.resultCode == SessionResult.RESULT_SUCCESS) {
|
||||
val imageRes = result.extras.getInt(
|
||||
Constants.EXTRA_PLAYLIST_SEGMENT_LOOP_IMAGE,
|
||||
R.drawable.ic_loop_segment_idle
|
||||
)
|
||||
binding.ivLoopSegment.setImageResource(imageRes)
|
||||
}
|
||||
},
|
||||
ContextCompat.getMainExecutor(requireContext())
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun bindData() {
|
||||
viewModel.toastLiveData.observe(viewLifecycleOwner) {
|
||||
Toast.makeText(requireActivity(), it, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
|
||||
viewModel.isLoading.observe(viewLifecycleOwner) {
|
||||
if (it) {
|
||||
loadingDialog.show(
|
||||
screenWidth,
|
||||
""
|
||||
)
|
||||
} else {
|
||||
loadingDialog.dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
viewModel.isShowPlaylistLiveData.observe(viewLifecycleOwner) {
|
||||
if (it) {
|
||||
binding.ivCover.visibility = View.GONE
|
||||
binding.tvTitle.visibility = View.GONE
|
||||
binding.ivCreatorProfile.visibility = View.GONE
|
||||
binding.tvCreatorNickname.visibility = View.GONE
|
||||
binding.rvPlaylistContent.visibility = View.VISIBLE
|
||||
binding.ivPlaylist.setBackgroundResource(
|
||||
R.drawable.bg_round_corner_6_7_cc333333
|
||||
)
|
||||
} else {
|
||||
binding.ivCover.visibility = View.VISIBLE
|
||||
binding.tvTitle.visibility = View.VISIBLE
|
||||
binding.ivCreatorProfile.visibility = View.VISIBLE
|
||||
binding.tvCreatorNickname.visibility = View.VISIBLE
|
||||
binding.rvPlaylistContent.visibility = View.GONE
|
||||
binding.ivPlaylist.setBackgroundResource(0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun connectPlayerService() {
|
||||
context?.let {
|
||||
if (!SharedPreferenceManager.isPlayerServiceRunning) {
|
||||
startPlayerService(context = it)
|
||||
}
|
||||
|
||||
view?.postDelayed({
|
||||
connectToMediaSession(it)
|
||||
}, 500)
|
||||
}
|
||||
}
|
||||
|
||||
private fun startPlayerService(context: Context) {
|
||||
val serviceIntent = Intent(context, AudioContentPlayerService::class.java)
|
||||
context.startService(serviceIntent)
|
||||
}
|
||||
|
||||
private fun connectToMediaSession(context: Context) {
|
||||
val componentName = ComponentName(context, AudioContentPlayerService::class.java)
|
||||
val sessionToken = SessionToken(context, componentName)
|
||||
val mediaControllerFuture = MediaController.Builder(context, sessionToken).buildAsync()
|
||||
mediaControllerFuture.addListener(
|
||||
{
|
||||
mediaController = mediaControllerFuture.get()
|
||||
setupMediaController()
|
||||
updatePlayerUI()
|
||||
startUpdatingUI()
|
||||
},
|
||||
ContextCompat.getMainExecutor(context)
|
||||
)
|
||||
}
|
||||
|
||||
private fun setupMediaController() {
|
||||
if (mediaController == null) {
|
||||
Toast.makeText(
|
||||
requireContext(),
|
||||
getString(R.string.audio_content_player_error_launch_failed),
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
dismiss()
|
||||
return
|
||||
}
|
||||
|
||||
mediaController!!.addListener(object : Player.Listener {
|
||||
override fun onPlaybackStateChanged(playbackState: Int) {
|
||||
mediaController?.let {
|
||||
when (playbackState) {
|
||||
Player.STATE_ENDED -> {
|
||||
it.seekTo(0)
|
||||
it.pause()
|
||||
}
|
||||
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) {
|
||||
updateMediaMetadata(mediaItem?.mediaMetadata)
|
||||
updateTimeUI()
|
||||
}
|
||||
|
||||
override fun onIsLoadingChanged(isLoading: Boolean) {
|
||||
viewModel.setLoading(isLoading)
|
||||
}
|
||||
|
||||
override fun onPlayWhenReadyChanged(playWhenReady: Boolean, reason: Int) {
|
||||
binding.ivPlayOrPause.setImageResource(
|
||||
if (playWhenReady) {
|
||||
R.drawable.ic_player_pause
|
||||
} else {
|
||||
R.drawable.ic_player_play
|
||||
}
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
if (playlist.isNotEmpty()) {
|
||||
binding.ivLoopSegment.setImageResource(R.drawable.ic_loop_segment_idle)
|
||||
val extras = Bundle().apply {
|
||||
putParcelableArrayList(
|
||||
Constants.EXTRA_AUDIO_CONTENT_PLAYLIST,
|
||||
playlist
|
||||
)
|
||||
}
|
||||
val sessionCommand = SessionCommand("UPDATE_PLAYLIST", Bundle.EMPTY)
|
||||
mediaController!!.sendCustomCommand(sessionCommand, extras)
|
||||
adapter.updateItems(playlist)
|
||||
} else {
|
||||
binding.ivLoopSegment.setImageResource(R.drawable.ic_loop_segment_idle)
|
||||
context?.let {
|
||||
val sessionCommand = SessionCommand("GET_PLAYLIST", Bundle.EMPTY)
|
||||
val resultFuture = mediaController!!.sendCustomCommand(sessionCommand, Bundle.EMPTY)
|
||||
resultFuture.addListener(
|
||||
{
|
||||
val result = resultFuture.get()
|
||||
if (result.resultCode == SessionResult.RESULT_SUCCESS) {
|
||||
val data = BundleCompat.getParcelableArrayList(
|
||||
result.extras,
|
||||
Constants.EXTRA_AUDIO_CONTENT_PLAYLIST,
|
||||
AudioContentPlaylistContent::class.java
|
||||
)
|
||||
playlist.clear()
|
||||
playlist.addAll(data ?: listOf())
|
||||
adapter.updateItems(data ?: listOf())
|
||||
}
|
||||
},
|
||||
ContextCompat.getMainExecutor(it)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun updatePlayerUI() {
|
||||
binding.ivPlayOrPause.setImageResource(
|
||||
if (mediaController!!.isPlaying) {
|
||||
R.drawable.ic_player_pause
|
||||
} else {
|
||||
R.drawable.ic_player_play
|
||||
}
|
||||
)
|
||||
|
||||
binding.ivPlayOrPause.setOnClickListener {
|
||||
if (!SharedPreferenceManager.isPlayerServiceRunning) {
|
||||
mediaController = null
|
||||
connectPlayerService()
|
||||
} else {
|
||||
mediaController?.let {
|
||||
if (it.playWhenReady) {
|
||||
it.pause()
|
||||
} else {
|
||||
it.play()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
binding.ivSkipForward.setOnClickListener {
|
||||
mediaController?.let {
|
||||
binding.ivLoopSegment.setImageResource(R.drawable.ic_loop_segment_idle)
|
||||
val sessionCommand = SessionCommand(
|
||||
"PLAY_NEXT_CONTENT",
|
||||
Bundle.EMPTY
|
||||
)
|
||||
it.sendCustomCommand(sessionCommand, Bundle.EMPTY)
|
||||
}
|
||||
}
|
||||
|
||||
binding.ivSkipBack.setOnClickListener {
|
||||
mediaController?.let {
|
||||
binding.ivLoopSegment.setImageResource(R.drawable.ic_loop_segment_idle)
|
||||
val sessionCommand = SessionCommand(
|
||||
"PLAY_PREVIOUS_CONTENT",
|
||||
Bundle.EMPTY
|
||||
)
|
||||
it.sendCustomCommand(sessionCommand, Bundle.EMPTY)
|
||||
}
|
||||
}
|
||||
|
||||
binding.ivSeekForward10.setOnClickListener {
|
||||
mediaController?.let {
|
||||
binding.ivLoopSegment.setImageResource(R.drawable.ic_loop_segment_idle)
|
||||
val sessionCommand = SessionCommand(
|
||||
"SEEK_FORWARD",
|
||||
Bundle.EMPTY
|
||||
)
|
||||
it.sendCustomCommand(sessionCommand, Bundle.EMPTY)
|
||||
}
|
||||
}
|
||||
|
||||
binding.ivSeekBackward10.setOnClickListener {
|
||||
mediaController?.let {
|
||||
binding.ivLoopSegment.setImageResource(R.drawable.ic_loop_segment_idle)
|
||||
val sessionCommand = SessionCommand(
|
||||
"SEEK_BACKWARD",
|
||||
Bundle.EMPTY
|
||||
)
|
||||
it.sendCustomCommand(sessionCommand, Bundle.EMPTY)
|
||||
}
|
||||
}
|
||||
|
||||
binding.sbProgress.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
|
||||
override fun onProgressChanged(
|
||||
seekBar: SeekBar?,
|
||||
progress: Int,
|
||||
fromUser: Boolean
|
||||
) {
|
||||
if (fromUser) {
|
||||
isUserSeeking = true
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStartTrackingTouch(p0: SeekBar?) {
|
||||
isUserSeeking = true
|
||||
}
|
||||
|
||||
override fun onStopTrackingTouch(seekBar: SeekBar?) {
|
||||
isUserSeeking = false
|
||||
seekBar?.progress?.let { progress ->
|
||||
mediaController?.seekTo(progress.toLong())
|
||||
}
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
updateMediaMetadata(mediaController?.currentMediaItem?.mediaMetadata)
|
||||
updateTimeUI()
|
||||
}
|
||||
|
||||
private fun updateMediaMetadata(metadata: MediaMetadata?) {
|
||||
metadata?.let {
|
||||
binding.tvTitle.text = it.title
|
||||
binding.tvCreatorNickname.text = it.artist
|
||||
|
||||
binding.ivCreatorProfile.load(
|
||||
it.extras?.getString(Constants.EXTRA_AUDIO_CONTENT_CREATOR_PROFILE_IMAGE)
|
||||
) {
|
||||
crossfade(true)
|
||||
placeholder(R.drawable.ic_place_holder)
|
||||
transformations(CircleCropTransformation())
|
||||
}
|
||||
|
||||
binding.ivCover.load(it.artworkUri) {
|
||||
crossfade(true)
|
||||
placeholder(R.drawable.ic_place_holder)
|
||||
transformations(RoundedCornersTransformation(8f.dpToPx()))
|
||||
}
|
||||
|
||||
val contentId = it.extras?.getLong(Constants.EXTRA_AUDIO_CONTENT_ID)
|
||||
adapter.updateCurrentPlayingId(contentId)
|
||||
|
||||
// Save to recent content
|
||||
contentId?.let { id ->
|
||||
val recentContent = RecentContent(
|
||||
contentId = id,
|
||||
coverImageUrl = it.artworkUri.toString(),
|
||||
title = it.title.toString(),
|
||||
creatorNickname = it.artist.toString()
|
||||
)
|
||||
recentContentViewModel.insertRecentContent(recentContent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateTimeUI() {
|
||||
mediaController?.let {
|
||||
val duration = it.duration
|
||||
val currentPosition = it.currentPosition
|
||||
|
||||
binding.sbProgress.max = duration.toInt()
|
||||
binding.sbProgress.progress = currentPosition.toInt()
|
||||
|
||||
binding.tvTotalTime.text = Utils.convertDurationToString(duration.toInt())
|
||||
binding.tvProgressTime.text = Utils.convertDurationToString(currentPosition.toInt())
|
||||
}
|
||||
}
|
||||
|
||||
private fun startUpdatingUI() {
|
||||
handler.post(object : Runnable {
|
||||
override fun run() {
|
||||
if (mediaController?.isPlaying == true && !isUserSeeking) {
|
||||
updateTimeUI()
|
||||
}
|
||||
|
||||
handler.postDelayed(this, 500)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
+447
@@ -0,0 +1,447 @@
|
||||
@file:Suppress("ktlint:package-name", "ktlint:standard:package-name")
|
||||
|
||||
package kr.co.vividnext.sodalive.audio_content.player
|
||||
|
||||
import android.app.PendingIntent
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import androidx.core.os.BundleCompat
|
||||
import androidx.media3.common.MediaItem
|
||||
import androidx.media3.common.MediaMetadata
|
||||
import androidx.media3.common.Player
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import androidx.media3.datasource.DefaultDataSource
|
||||
import androidx.media3.exoplayer.ExoPlayer
|
||||
import androidx.media3.exoplayer.source.ProgressiveMediaSource
|
||||
import androidx.media3.extractor.DefaultExtractorsFactory
|
||||
import androidx.media3.extractor.ts.AdtsExtractor
|
||||
import androidx.media3.session.MediaSession
|
||||
import androidx.media3.session.MediaSessionService
|
||||
import androidx.media3.session.SessionCommand
|
||||
import androidx.media3.session.SessionResult
|
||||
import com.google.common.util.concurrent.Futures
|
||||
import com.google.common.util.concurrent.ListenableFuture
|
||||
import com.orhanobut.logger.Logger
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import kr.co.vividnext.sodalive.R
|
||||
import kr.co.vividnext.sodalive.audio_content.playlist.detail.AudioContentPlaylistContent
|
||||
import kr.co.vividnext.sodalive.common.Constants
|
||||
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
|
||||
import kr.co.vividnext.sodalive.common.Utils
|
||||
import kr.co.vividnext.sodalive.v2.main.MainV2Activity
|
||||
import org.koin.android.ext.android.inject
|
||||
|
||||
@UnstableApi
|
||||
class AudioContentPlayerService : MediaSessionService() {
|
||||
|
||||
private val repository: AudioContentGenerateUrlRepository by inject()
|
||||
|
||||
private var playlistManager: AudioContentPlaylistManager? = null
|
||||
private var mediaSession: MediaSession? = null
|
||||
private var player: ExoPlayer? = null
|
||||
|
||||
val compositeDisposable = CompositeDisposable()
|
||||
|
||||
private val playlist = ArrayList<AudioContentPlaylistContent>()
|
||||
|
||||
private var loopStartMs: Long? = null
|
||||
private var loopEndMs: Long? = null
|
||||
private var isLooping = false
|
||||
private val loopHandler = Handler(Looper.getMainLooper())
|
||||
private val loopCheckInterval = 100L // 0.1초 간격
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
|
||||
try {
|
||||
initPlayer()
|
||||
initMediaSession()
|
||||
} catch (e: Exception) {
|
||||
onStopService()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
mediaSession?.run {
|
||||
player.release()
|
||||
release()
|
||||
mediaSession = null
|
||||
}
|
||||
|
||||
compositeDisposable.dispose()
|
||||
SharedPreferenceManager.isPlayerServiceRunning = false
|
||||
stopLoop()
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
override fun onTaskRemoved(rootIntent: Intent?) {
|
||||
onStopService()
|
||||
}
|
||||
|
||||
override fun onGetSession(controllerInfo: MediaSession.ControllerInfo) = mediaSession
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
if (intent?.action == "STOP_SERVICE") {
|
||||
onStopService()
|
||||
}
|
||||
|
||||
return super.onStartCommand(intent, flags, startId)
|
||||
}
|
||||
|
||||
private fun toggleSegmentLoop() {
|
||||
when {
|
||||
loopStartMs == null -> {
|
||||
loopStartMs = player?.currentPosition
|
||||
}
|
||||
|
||||
loopEndMs == null -> {
|
||||
loopEndMs = player?.currentPosition
|
||||
isLooping = true
|
||||
startLoopMonitoring()
|
||||
}
|
||||
|
||||
else -> {
|
||||
stopLoop()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun startLoopMonitoring() {
|
||||
loopHandler.post(object : Runnable {
|
||||
override fun run() {
|
||||
if (isLooping && loopStartMs != null && loopEndMs != null && player != null) {
|
||||
if (player!!.currentPosition >= loopEndMs!!) {
|
||||
player!!.seekTo(loopStartMs!!)
|
||||
}
|
||||
loopHandler.postDelayed(this, loopCheckInterval)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun stopLoop() {
|
||||
isLooping = false
|
||||
loopStartMs = null
|
||||
loopEndMs = null
|
||||
loopHandler.removeCallbacksAndMessages(null)
|
||||
}
|
||||
|
||||
private fun initPlayer() {
|
||||
player = ExoPlayer.Builder(this).build()
|
||||
player!!.addListener(object : Player.Listener {
|
||||
override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) {
|
||||
super.onMediaItemTransition(mediaItem, reason)
|
||||
player?.play()
|
||||
}
|
||||
|
||||
override fun onPlaybackStateChanged(playbackState: Int) {
|
||||
super.onPlaybackStateChanged(playbackState)
|
||||
if (playbackState == Player.STATE_IDLE) {
|
||||
onStopService()
|
||||
} else if (playbackState == Player.STATE_ENDED) {
|
||||
if (playlistManager!!.hasNextContent()) {
|
||||
playNextContent()
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun initMediaSession() {
|
||||
val contextIntent = Intent(applicationContext, MainV2Activity::class.java).apply {
|
||||
flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP
|
||||
putExtra(
|
||||
MainV2Activity.EXTRA_AUDIO_NOTIFICATION_ROUTE,
|
||||
MainV2Activity.ROUTE_AUDIO_PLAYER
|
||||
)
|
||||
}
|
||||
val pendingIntent = PendingIntent.getActivity(
|
||||
applicationContext,
|
||||
0,
|
||||
contextIntent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
||||
)
|
||||
|
||||
mediaSession = MediaSession.Builder(this, player!!)
|
||||
.setSessionActivity(pendingIntent)
|
||||
.setCallback(object : MediaSession.Callback {
|
||||
override fun onConnect(
|
||||
session: MediaSession,
|
||||
controller: MediaSession.ControllerInfo
|
||||
): MediaSession.ConnectionResult {
|
||||
SharedPreferenceManager.isPlayerServiceRunning = true
|
||||
val allowedCommands = MediaSession.ConnectionResult.DEFAULT_SESSION_COMMANDS
|
||||
.buildUpon()
|
||||
.add(SessionCommand("UPDATE_PLAYLIST", Bundle.EMPTY))
|
||||
.add(SessionCommand("PLAY_NEXT_CONTENT", Bundle.EMPTY))
|
||||
.add(SessionCommand("PLAY_PREVIOUS_CONTENT", Bundle.EMPTY))
|
||||
.add(SessionCommand("PLAY_SELECTED_CONTENT", Bundle.EMPTY))
|
||||
.add(SessionCommand("GET_PLAYLIST", Bundle.EMPTY))
|
||||
.add(SessionCommand("SEEK_FORWARD", Bundle.EMPTY))
|
||||
.add(SessionCommand("SEEK_BACKWARD", Bundle.EMPTY))
|
||||
.add(SessionCommand("TOGGLE_SEGMENT_LOOP", Bundle.EMPTY))
|
||||
.build()
|
||||
|
||||
return MediaSession.ConnectionResult.AcceptedResultBuilder(session)
|
||||
.setAvailableSessionCommands(allowedCommands)
|
||||
.build()
|
||||
}
|
||||
|
||||
override fun onCustomCommand(
|
||||
session: MediaSession,
|
||||
controller: MediaSession.ControllerInfo,
|
||||
customCommand: SessionCommand,
|
||||
args: Bundle
|
||||
): ListenableFuture<SessionResult> {
|
||||
return when (customCommand.customAction) {
|
||||
"UPDATE_PLAYLIST" -> {
|
||||
stopLoop()
|
||||
val playlist = BundleCompat.getParcelableArrayList(
|
||||
args,
|
||||
Constants.EXTRA_AUDIO_CONTENT_PLAYLIST,
|
||||
AudioContentPlaylistContent::class.java
|
||||
)
|
||||
|
||||
if (playlist != null) {
|
||||
this@AudioContentPlayerService.playlist.addAll(playlist)
|
||||
playlistManager = AudioContentPlaylistManager(playlist)
|
||||
playNextContent()
|
||||
}
|
||||
|
||||
Futures.immediateFuture(SessionResult(SessionResult.RESULT_SUCCESS))
|
||||
}
|
||||
|
||||
"PLAY_NEXT_CONTENT" -> {
|
||||
stopLoop()
|
||||
playNextContent()
|
||||
Futures.immediateFuture(SessionResult(SessionResult.RESULT_SUCCESS))
|
||||
}
|
||||
|
||||
"PLAY_PREVIOUS_CONTENT" -> {
|
||||
stopLoop()
|
||||
playPreviousContent()
|
||||
Futures.immediateFuture(SessionResult(SessionResult.RESULT_SUCCESS))
|
||||
}
|
||||
|
||||
"PLAY_SELECTED_CONTENT" -> {
|
||||
stopLoop()
|
||||
val selectedContentId = args.getLong(Constants.EXTRA_AUDIO_CONTENT_ID)
|
||||
playSelectedContent(contentId = selectedContentId)
|
||||
Futures.immediateFuture(SessionResult(SessionResult.RESULT_SUCCESS))
|
||||
}
|
||||
|
||||
"SEEK_FORWARD" -> {
|
||||
stopLoop()
|
||||
playSeekForward()
|
||||
Futures.immediateFuture(SessionResult(SessionResult.RESULT_SUCCESS))
|
||||
}
|
||||
|
||||
"SEEK_BACKWARD" -> {
|
||||
stopLoop()
|
||||
playSeekBackward()
|
||||
Futures.immediateFuture(SessionResult(SessionResult.RESULT_SUCCESS))
|
||||
}
|
||||
|
||||
"TOGGLE_SEGMENT_LOOP" -> {
|
||||
val extras = Bundle().apply {
|
||||
putInt(
|
||||
Constants.EXTRA_PLAYLIST_SEGMENT_LOOP_IMAGE,
|
||||
when {
|
||||
loopStartMs == null -> {
|
||||
R.drawable.ic_loop_segment_start_set
|
||||
}
|
||||
|
||||
loopEndMs == null -> {
|
||||
R.drawable.ic_loop_segment_active
|
||||
}
|
||||
|
||||
else -> {
|
||||
R.drawable.ic_loop_segment_idle
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
toggleSegmentLoop()
|
||||
|
||||
Futures.immediateFuture(
|
||||
SessionResult(
|
||||
SessionResult.RESULT_SUCCESS,
|
||||
extras
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
"GET_PLAYLIST" -> {
|
||||
val extras = Bundle().apply {
|
||||
putParcelableArrayList(
|
||||
Constants.EXTRA_AUDIO_CONTENT_PLAYLIST,
|
||||
playlist
|
||||
)
|
||||
}
|
||||
|
||||
Futures.immediateFuture(
|
||||
SessionResult(
|
||||
SessionResult.RESULT_SUCCESS,
|
||||
extras
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
else -> super.onCustomCommand(session, controller, customCommand, args)
|
||||
}
|
||||
}
|
||||
})
|
||||
.build()
|
||||
}
|
||||
|
||||
private fun onStopService() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
stopForeground(STOP_FOREGROUND_REMOVE)
|
||||
} else {
|
||||
stopForeground(true)
|
||||
}
|
||||
|
||||
mediaSession?.run {
|
||||
player.release()
|
||||
release()
|
||||
mediaSession = null
|
||||
}
|
||||
|
||||
stopSelf()
|
||||
}
|
||||
|
||||
private fun playNextContent() {
|
||||
val content = playlistManager?.moveToNext()
|
||||
|
||||
if (content != null) {
|
||||
generateUrl(
|
||||
content.id,
|
||||
onSuccess = { urlGenerateSuccess(content, it) },
|
||||
onFailure = { playNextContent() }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun playSeekForward() {
|
||||
val currentPosition = player!!.currentPosition
|
||||
val duration = player!!.duration
|
||||
var newPosition = currentPosition + SEEK_INTERVAL_MS
|
||||
if (newPosition > duration) {
|
||||
newPosition = duration
|
||||
}
|
||||
player!!.seekTo(newPosition)
|
||||
}
|
||||
|
||||
private fun playSeekBackward() {
|
||||
val currentPosition = player!!.currentPosition
|
||||
var newPosition = currentPosition - SEEK_INTERVAL_MS
|
||||
if (newPosition < 0) {
|
||||
newPosition = 0
|
||||
}
|
||||
player!!.seekTo(newPosition)
|
||||
}
|
||||
|
||||
private fun urlGenerateSuccess(
|
||||
content: AudioContentPlaylistContent,
|
||||
contentUrl: String
|
||||
) {
|
||||
val extras = Bundle().apply {
|
||||
putString(
|
||||
Constants.EXTRA_AUDIO_CONTENT_CREATOR_PROFILE_IMAGE,
|
||||
content.creatorProfileUrl
|
||||
)
|
||||
putLong(
|
||||
Constants.EXTRA_AUDIO_CONTENT_ID,
|
||||
content.id
|
||||
)
|
||||
}
|
||||
|
||||
val mediaMetadata = MediaMetadata.Builder()
|
||||
.setTitle(content.title)
|
||||
.setArtist(content.creatorNickname)
|
||||
.setArtworkUri(Uri.parse(content.coverUrl))
|
||||
.setExtras(extras)
|
||||
.setDurationMs(Utils.convertStringToDuration(content.duration))
|
||||
.build()
|
||||
|
||||
val mediaItem = MediaItem.Builder()
|
||||
.setUri(Uri.parse(contentUrl))
|
||||
.setMediaMetadata(mediaMetadata)
|
||||
.build()
|
||||
|
||||
val extractorFactory = DefaultExtractorsFactory().setAdtsExtractorFlags(
|
||||
AdtsExtractor.FLAG_ENABLE_CONSTANT_BITRATE_SEEKING
|
||||
)
|
||||
val mediaSource = ProgressiveMediaSource.Factory(
|
||||
DefaultDataSource.Factory(this),
|
||||
extractorFactory
|
||||
).createMediaSource(mediaItem)
|
||||
|
||||
player?.setMediaSource(mediaSource)
|
||||
player?.prepare()
|
||||
}
|
||||
|
||||
private fun playPreviousContent() {
|
||||
val content = playlistManager?.moveToPrevious()
|
||||
|
||||
if (content != null) {
|
||||
generateUrl(
|
||||
content.id,
|
||||
onSuccess = { urlGenerateSuccess(content, it) },
|
||||
onFailure = { playPreviousContent() }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun playSelectedContent(contentId: Long) {
|
||||
val content = playlistManager?.findByContentId(contentId = contentId)
|
||||
|
||||
if (content != null) {
|
||||
generateUrl(
|
||||
content.id,
|
||||
onSuccess = { urlGenerateSuccess(content, it) },
|
||||
onFailure = { playPreviousContent() }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun generateUrl(contentId: Long, onSuccess: (String) -> Unit, onFailure: () -> Unit) {
|
||||
if (contentId <= 0) {
|
||||
onFailure()
|
||||
}
|
||||
|
||||
compositeDisposable.add(
|
||||
repository.generateUrl(
|
||||
contentId = contentId,
|
||||
token = "Bearer ${SharedPreferenceManager.token}"
|
||||
)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
{
|
||||
if (it.success && it.data != null && it.data.contentUrl.isNotBlank()) {
|
||||
onSuccess(it.data.contentUrl)
|
||||
} else {
|
||||
onFailure()
|
||||
}
|
||||
},
|
||||
{
|
||||
it.message?.let { message -> Logger.e(message) }
|
||||
onFailure()
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val SEEK_INTERVAL_MS = 10 * 1000
|
||||
}
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
package kr.co.vividnext.sodalive.audio_content.player
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import kr.co.vividnext.sodalive.base.BaseViewModel
|
||||
|
||||
class AudioContentPlayerViewModel : BaseViewModel() {
|
||||
private val _toastLiveData = MutableLiveData<String?>()
|
||||
val toastLiveData: LiveData<String?>
|
||||
get() = _toastLiveData
|
||||
|
||||
private var _isLoading = MutableLiveData(false)
|
||||
val isLoading: LiveData<Boolean>
|
||||
get() = _isLoading
|
||||
|
||||
private var _isShowPlaylistLiveData = MutableLiveData(false)
|
||||
val isShowPlaylistLiveData: LiveData<Boolean>
|
||||
get() = _isShowPlaylistLiveData
|
||||
|
||||
fun setLoading(loading: Boolean) {
|
||||
_isLoading.value = loading
|
||||
}
|
||||
|
||||
fun toggleShowPlayList() {
|
||||
_isShowPlaylistLiveData.value = !_isShowPlaylistLiveData.value!!
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user